@crisp-ui-kit/crisp 0.57.0 → 0.57.2
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/accentpicker.css +5 -0
- package/dist/accentpicker.d.ts +7 -0
- package/dist/accentpicker.layered.css +7 -0
- package/dist/authshell.css +9 -4
- package/dist/authshell.layered.css +9 -4
- package/dist/avatar.css +1 -1
- package/dist/avatar.layered.css +1 -1
- package/dist/badge.css +5 -5
- package/dist/badge.layered.css +5 -5
- package/dist/board.css +28 -8
- package/dist/board.layered.css +28 -8
- package/dist/bottomsheet.css +26 -0
- package/dist/bottomsheet.d.ts +7 -0
- package/dist/bottomsheet.layered.css +28 -0
- package/dist/button.css +9 -8
- package/dist/button.layered.css +9 -8
- package/dist/checkbox.css +7 -3
- package/dist/checkbox.layered.css +7 -3
- package/dist/chip.css +15 -0
- package/dist/chip.d.ts +3 -1
- package/dist/chip.layered.css +15 -0
- package/dist/chipgroup.css +5 -0
- package/dist/chipgroup.d.ts +7 -0
- package/dist/chipgroup.layered.css +7 -0
- package/dist/cjs/components/accentpicker/accentpicker.js +66 -0
- package/dist/cjs/components/accentpicker/accentpicker.recipe.js +25 -0
- package/dist/cjs/components/accentpicker/index.js +7 -0
- package/dist/cjs/components/avatar/avatar.recipe.js +4 -1
- package/dist/cjs/components/badge/badge.recipe.js +38 -10
- package/dist/cjs/components/bottomsheet/bottomsheet.js +90 -0
- package/dist/cjs/components/bottomsheet/bottomsheet.recipe.js +77 -0
- package/dist/cjs/components/bottomsheet/index.js +9 -0
- package/dist/cjs/components/button/button.recipe.js +55 -21
- package/dist/cjs/components/checkbox/checkbox.recipe.js +61 -9
- package/dist/cjs/components/chip/chip.js +14 -2
- package/dist/cjs/components/chip/chip.recipe.js +69 -1
- package/dist/cjs/components/chipgroup/chipgroup.js +51 -0
- package/dist/cjs/components/chipgroup/chipgroup.recipe.js +36 -0
- package/dist/cjs/components/chipgroup/index.js +7 -0
- package/dist/cjs/components/datatable/datatable.js +5 -1
- package/dist/cjs/components/datatable/datatable.recipe.js +11 -2
- package/dist/cjs/components/dialog/dialog.recipe.js +21 -10
- package/dist/cjs/components/emptystate/emptystate.recipe.js +7 -2
- package/dist/cjs/components/fileupload/fileupload.js +21 -4
- package/dist/cjs/components/input/index.js +2 -1
- package/dist/cjs/components/input/input.js +33 -3
- package/dist/cjs/components/input/input.recipe.js +22 -7
- package/dist/cjs/components/menu/menu.recipe.js +21 -7
- package/dist/cjs/components/menusheet/index.js +9 -0
- package/dist/cjs/components/menusheet/menusheet.js +133 -0
- package/dist/cjs/components/menusheet/menusheet.recipe.js +103 -0
- package/dist/cjs/components/multiselect/multiselect.js +2 -2
- package/dist/cjs/components/multiselect/multiselect.recipe.js +45 -12
- package/dist/cjs/components/progress/progress.js +21 -4
- package/dist/cjs/components/progress/progress.recipe.js +30 -0
- package/dist/cjs/components/select/select.js +3 -3
- package/dist/cjs/components/select/select.recipe.js +63 -10
- package/dist/cjs/components/snackbar/index.js +8 -0
- package/dist/cjs/components/snackbar/snackbar.js +121 -0
- package/dist/cjs/components/snackbar/snackbar.recipe.js +71 -0
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +37 -12
- package/dist/cjs/components/tabs/tabs.js +10 -4
- package/dist/cjs/components/tabs/tabs.recipe.js +23 -6
- package/dist/cjs/components/text/text.recipe.js +8 -7
- package/dist/cjs/components/textarea/textarea.js +3 -2
- package/dist/cjs/components/textarea/textarea.recipe.js +13 -9
- package/dist/cjs/components/textlink/textlink.recipe.js +6 -3
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/product/authshell/authshell.recipe.js +5 -1
- package/dist/cjs/product/command/command.recipe.js +4 -1
- package/dist/cjs/product/comments/comments.js +7 -2
- package/dist/cjs/product/comments/comments.recipe.js +25 -7
- package/dist/cjs/product/memberssettings/index.js +6 -0
- package/dist/cjs/product/memberssettings/memberssettings.js +99 -0
- package/dist/cjs/product/memberssettings/memberssettings.recipe.js +83 -0
- package/dist/cjs/product/notificationssettings/index.js +5 -0
- package/dist/cjs/product/notificationssettings/notificationssettings.js +19 -0
- package/dist/cjs/product/notificationssettings/notificationssettings.recipe.js +44 -0
- package/dist/cjs/product/recordpage/recordpage.js +101 -9
- package/dist/cjs/product/recordpage/recordpage.recipe.js +43 -6
- package/dist/cjs/product/workspacegeneralsettings/index.js +5 -0
- package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.js +21 -0
- package/dist/cjs/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +51 -0
- package/dist/cjs/tokens/contract.js +1 -0
- package/dist/cjs/tokens/elevation.js +8 -5
- package/dist/cjs/tokens/palette.js +107 -33
- package/dist/cjs/tokens/scale.js +45 -2
- package/dist/cjs/tokens/semantic.js +26 -0
- package/dist/columncalc.css +15 -7
- package/dist/columncalc.layered.css +15 -7
- package/dist/command.css +23 -19
- package/dist/command.layered.css +23 -19
- package/dist/comments.css +28 -19
- package/dist/comments.d.ts +2 -2
- package/dist/comments.layered.css +28 -19
- package/dist/datatable.css +161 -67
- package/dist/datatable.layered.css +161 -67
- package/dist/datepicker.css +13 -41
- package/dist/datepicker.layered.css +13 -41
- package/dist/datepickerfield.css +4 -1
- package/dist/datepickerfield.layered.css +4 -1
- package/dist/dialog.css +12 -7
- package/dist/dialog.layered.css +12 -7
- package/dist/emptystate.css +5 -3
- package/dist/emptystate.layered.css +5 -3
- package/dist/esm/components/accentpicker/accentpicker.js +30 -0
- package/dist/esm/components/accentpicker/accentpicker.recipe.js +22 -0
- package/dist/esm/components/accentpicker/index.js +2 -0
- package/dist/esm/components/avatar/avatar.recipe.js +4 -1
- package/dist/esm/components/badge/badge.recipe.js +38 -10
- package/dist/esm/components/bottomsheet/bottomsheet.js +52 -0
- package/dist/esm/components/bottomsheet/bottomsheet.recipe.js +74 -0
- package/dist/esm/components/bottomsheet/index.js +2 -0
- package/dist/esm/components/button/button.recipe.js +56 -22
- package/dist/esm/components/checkbox/checkbox.recipe.js +61 -9
- package/dist/esm/components/chip/chip.js +14 -2
- package/dist/esm/components/chip/chip.recipe.js +69 -1
- package/dist/esm/components/chipgroup/chipgroup.js +15 -0
- package/dist/esm/components/chipgroup/chipgroup.recipe.js +33 -0
- package/dist/esm/components/chipgroup/index.js +2 -0
- package/dist/esm/components/datatable/datatable.js +5 -1
- package/dist/esm/components/datatable/datatable.recipe.js +11 -2
- package/dist/esm/components/dialog/dialog.recipe.js +22 -11
- package/dist/esm/components/emptystate/emptystate.recipe.js +7 -2
- package/dist/esm/components/fileupload/fileupload.js +20 -5
- package/dist/esm/components/input/index.js +1 -1
- package/dist/esm/components/input/input.js +33 -3
- package/dist/esm/components/input/input.recipe.js +23 -8
- package/dist/esm/components/menu/menu.recipe.js +21 -7
- package/dist/esm/components/menusheet/index.js +2 -0
- package/dist/esm/components/menusheet/menusheet.js +95 -0
- package/dist/esm/components/menusheet/menusheet.recipe.js +100 -0
- package/dist/esm/components/multiselect/multiselect.js +2 -2
- package/dist/esm/components/multiselect/multiselect.recipe.js +45 -12
- package/dist/esm/components/progress/progress.js +22 -5
- package/dist/esm/components/progress/progress.recipe.js +30 -0
- package/dist/esm/components/select/select.js +3 -3
- package/dist/esm/components/select/select.recipe.js +63 -10
- package/dist/esm/components/snackbar/index.js +2 -0
- package/dist/esm/components/snackbar/snackbar.js +84 -0
- package/dist/esm/components/snackbar/snackbar.recipe.js +68 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +7 -2
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +37 -12
- package/dist/esm/components/tabs/tabs.js +10 -4
- package/dist/esm/components/tabs/tabs.recipe.js +23 -6
- package/dist/esm/components/text/text.recipe.js +8 -7
- package/dist/esm/components/textarea/textarea.js +3 -2
- package/dist/esm/components/textarea/textarea.recipe.js +14 -10
- package/dist/esm/components/textlink/textlink.recipe.js +6 -3
- package/dist/esm/index.js +8 -0
- package/dist/esm/product/authshell/authshell.recipe.js +5 -1
- package/dist/esm/product/command/command.recipe.js +4 -1
- package/dist/esm/product/comments/comments.js +7 -2
- package/dist/esm/product/comments/comments.recipe.js +25 -7
- package/dist/esm/product/memberssettings/index.js +1 -0
- package/dist/esm/product/memberssettings/memberssettings.js +62 -0
- package/dist/esm/product/memberssettings/memberssettings.recipe.js +80 -0
- package/dist/esm/product/notificationssettings/index.js +1 -0
- package/dist/esm/product/notificationssettings/notificationssettings.js +16 -0
- package/dist/esm/product/notificationssettings/notificationssettings.recipe.js +41 -0
- package/dist/esm/product/recordpage/recordpage.js +102 -10
- package/dist/esm/product/recordpage/recordpage.recipe.js +43 -6
- package/dist/esm/product/workspacegeneralsettings/index.js +1 -0
- package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.js +18 -0
- package/dist/esm/product/workspacegeneralsettings/workspacegeneralsettings.recipe.js +48 -0
- package/dist/esm/tokens/contract.js +2 -1
- package/dist/esm/tokens/elevation.js +8 -5
- package/dist/esm/tokens/palette.js +107 -33
- package/dist/esm/tokens/scale.js +44 -1
- package/dist/esm/tokens/semantic.js +26 -0
- package/dist/fileupload.css +3 -1
- package/dist/fileupload.layered.css +3 -1
- package/dist/input.css +11 -2
- package/dist/input.layered.css +11 -2
- package/dist/listpage.css +3 -1
- package/dist/listpage.layered.css +3 -1
- package/dist/memberssettings.css +67 -0
- package/dist/memberssettings.d.ts +7 -0
- package/dist/memberssettings.layered.css +69 -0
- package/dist/menu.css +2 -2
- package/dist/menu.layered.css +2 -2
- package/dist/menusheet.css +36 -0
- package/dist/menusheet.d.ts +7 -0
- package/dist/menusheet.layered.css +38 -0
- package/dist/multiselect.css +17 -6
- package/dist/multiselect.d.ts +2 -2
- package/dist/multiselect.layered.css +17 -6
- package/dist/native/tokens.d.ts +2 -1
- package/dist/native/tokens.js +53 -25
- package/dist/notificationssettings.css +22 -0
- package/dist/notificationssettings.d.ts +7 -0
- package/dist/notificationssettings.layered.css +24 -0
- package/dist/objectsettings.css +4 -4
- package/dist/objectsettings.layered.css +4 -4
- package/dist/progress.css +3 -0
- package/dist/progress.layered.css +3 -0
- package/dist/recordpage.css +67 -23
- package/dist/recordpage.layered.css +67 -23
- package/dist/recordpanel.css +35 -16
- package/dist/recordpanel.layered.css +35 -16
- package/dist/resizablepanel.css +8 -5
- package/dist/resizablepanel.layered.css +8 -5
- package/dist/richtext.css +24 -8
- package/dist/richtext.layered.css +24 -8
- package/dist/savedviews.css +9 -2
- package/dist/savedviews.layered.css +9 -2
- package/dist/select.css +47 -7
- package/dist/select.d.ts +2 -2
- package/dist/select.layered.css +47 -7
- package/dist/settingsshell.css +2 -3
- package/dist/settingsshell.layered.css +2 -3
- package/dist/slider.css +6 -27
- package/dist/slider.layered.css +6 -27
- package/dist/snackbar.css +24 -0
- package/dist/snackbar.d.ts +7 -0
- package/dist/snackbar.layered.css +26 -0
- package/dist/statcard.css +3 -1
- package/dist/statcard.layered.css +3 -1
- package/dist/styles.css +927 -365
- package/dist/styles.layered.css +927 -365
- package/dist/table.css +28 -10
- package/dist/table.layered.css +28 -10
- package/dist/tabletoolbar.css +35 -24
- package/dist/tabletoolbar.layered.css +35 -24
- package/dist/tabs.css +18 -4
- package/dist/tabs.layered.css +18 -4
- package/dist/text.css +1 -1
- package/dist/text.layered.css +1 -1
- package/dist/textarea.css +4 -7
- package/dist/textarea.layered.css +4 -7
- package/dist/textlink.css +2 -2
- package/dist/textlink.layered.css +2 -2
- package/dist/themepicker.css +2 -1
- package/dist/themepicker.layered.css +2 -1
- package/dist/tokens.css +31 -13
- package/dist/tokens.json +53 -25
- package/dist/types/components/accentpicker/accentpicker.d.ts +23 -0
- package/dist/types/components/accentpicker/accentpicker.recipe.d.ts +3 -0
- package/dist/types/components/accentpicker/index.d.ts +2 -0
- package/dist/types/components/bottomsheet/bottomsheet.d.ts +35 -0
- package/dist/types/components/bottomsheet/bottomsheet.recipe.d.ts +3 -0
- package/dist/types/components/bottomsheet/index.d.ts +2 -0
- package/dist/types/components/button/button.recipe.d.ts +13 -8
- package/dist/types/components/checkbox/checkbox.recipe.d.ts +28 -5
- package/dist/types/components/chip/chip.d.ts +22 -0
- package/dist/types/components/chip/chip.recipe.d.ts +48 -0
- package/dist/types/components/chipgroup/chipgroup.d.ts +14 -0
- package/dist/types/components/chipgroup/chipgroup.recipe.d.ts +5 -0
- package/dist/types/components/chipgroup/index.d.ts +2 -0
- package/dist/types/components/datatable/datatable.d.ts +19 -0
- package/dist/types/components/fileupload/fileupload.d.ts +26 -0
- package/dist/types/components/input/index.d.ts +1 -1
- package/dist/types/components/input/input.d.ts +84 -0
- package/dist/types/components/menusheet/index.d.ts +2 -0
- package/dist/types/components/menusheet/menusheet.d.ts +42 -0
- package/dist/types/components/menusheet/menusheet.recipe.d.ts +3 -0
- package/dist/types/components/multiselect/multiselect.d.ts +4 -3
- package/dist/types/components/multiselect/multiselect.recipe.d.ts +3 -0
- package/dist/types/components/progress/progress.d.ts +16 -4
- package/dist/types/components/progress/progress.recipe.d.ts +22 -0
- package/dist/types/components/select/select.d.ts +6 -5
- package/dist/types/components/select/select.recipe.d.ts +3 -0
- package/dist/types/components/snackbar/index.d.ts +2 -0
- package/dist/types/components/snackbar/snackbar.d.ts +36 -0
- package/dist/types/components/snackbar/snackbar.recipe.d.ts +5 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +28 -1
- package/dist/types/components/tabs/tabs.d.ts +11 -1
- package/dist/types/components/textarea/textarea.d.ts +16 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/product/comments/comments.d.ts +9 -3
- package/dist/types/product/comments/comments.recipe.d.ts +12 -2
- package/dist/types/product/memberssettings/index.d.ts +1 -0
- package/dist/types/product/memberssettings/memberssettings.d.ts +85 -0
- package/dist/types/product/memberssettings/memberssettings.recipe.d.ts +3 -0
- package/dist/types/product/notificationssettings/index.d.ts +1 -0
- package/dist/types/product/notificationssettings/notificationssettings.d.ts +37 -0
- package/dist/types/product/notificationssettings/notificationssettings.recipe.d.ts +3 -0
- package/dist/types/product/recordpage/recordpage.d.ts +89 -2
- package/dist/types/product/workspacegeneralsettings/index.d.ts +1 -0
- package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.d.ts +48 -0
- package/dist/types/product/workspacegeneralsettings/workspacegeneralsettings.recipe.d.ts +3 -0
- package/dist/types/tokens/contract.d.ts +7 -1
- package/dist/types/tokens/elevation.d.ts +8 -5
- package/dist/types/tokens/palette.d.ts +28 -12
- package/dist/types/tokens/scale.d.ts +12 -1
- package/dist/types/tokens/semantic.d.ts +17 -0
- package/dist/wheelpicker.css +4 -2
- package/dist/wheelpicker.layered.css +4 -2
- package/dist/workspacegeneralsettings.css +30 -0
- package/dist/workspacegeneralsettings.d.ts +7 -0
- package/dist/workspacegeneralsettings.layered.css +32 -0
- package/package.json +41 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Filter, MoreHorizontal, Search, User, UsersRound } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Avatar } from "../../components/avatar/index.js";
|
|
5
|
+
import { Badge } from "../../components/badge/index.js";
|
|
6
|
+
import { Button } from "../../components/button/index.js";
|
|
7
|
+
import { Dialog, DialogBody, DialogFooter } from "../../components/dialog/index.js";
|
|
8
|
+
import { FormField } from "../../components/formfield/index.js";
|
|
9
|
+
import { Select } from "../../components/select/index.js";
|
|
10
|
+
import { useTablistKeyboard } from "../../components/tabs/tabs-keyboard.js";
|
|
11
|
+
import { Textarea } from "../../components/textarea/index.js";
|
|
12
|
+
import { TextLink } from "../../components/textlink/index.js";
|
|
13
|
+
import { memberssettingsRecipe } from "./memberssettings.recipe.js";
|
|
14
|
+
/**
|
|
15
|
+
* Attio's "Members and teams" settings page (crisp#636, P-119/P-120):
|
|
16
|
+
* title + description, a Members/Teams segmented switcher, a
|
|
17
|
+
* search+Filter+Invite toolbar, and the members table (User/Role/Teams).
|
|
18
|
+
* Compose `InviteMembersDialog` alongside for the "Invite members" flow.
|
|
19
|
+
*/
|
|
20
|
+
export function MembersSettings({ members, searchValue, onSearchChange, onFilterClick, onInviteClick, onRowMenuClick, tab: tabProp, onTabChange, teamsContent, className, title = "Members and teams", description = "Manage workspace members and teams, set access levels, and invite new users.", learnMoreLabel = "Learn more", learnMoreHref = "#", membersTabLabel = "Members", teamsTabLabel = "Teams", tablistLabel = "Members and teams", searchPlaceholder = "Search name or email", filterLabel = "Filter", inviteLabel = "Invite members", userColumnLabel = "User", roleColumnLabel = "Role", teamsColumnLabel = "Teams", memberActionsLabel = "Member actions", noTeamsLabel = "No teams yet", ...rest }) {
|
|
21
|
+
const [internalTab, setInternalTab] = React.useState("members");
|
|
22
|
+
const tab = tabProp ?? internalTab;
|
|
23
|
+
const setTab = (next) => {
|
|
24
|
+
if (tabProp === undefined)
|
|
25
|
+
setInternalTab(next);
|
|
26
|
+
onTabChange?.(next);
|
|
27
|
+
};
|
|
28
|
+
const listRef = React.useRef(null);
|
|
29
|
+
const tabEntries = [
|
|
30
|
+
{
|
|
31
|
+
value: "members",
|
|
32
|
+
label: membersTabLabel,
|
|
33
|
+
icon: _jsx(User, { size: 16 }),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
value: "teams",
|
|
37
|
+
label: teamsTabLabel,
|
|
38
|
+
icon: _jsx(UsersRound, { size: 16 }),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
const { onKeyDown, tabIndexFor } = useTablistKeyboard({
|
|
42
|
+
tabs: tabEntries,
|
|
43
|
+
active: tab,
|
|
44
|
+
select: (value) => setTab(value),
|
|
45
|
+
listRef,
|
|
46
|
+
});
|
|
47
|
+
return (_jsx("div", { className: memberssettingsRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-memberssettings-page", children: [_jsxs("div", { className: "crisp-memberssettings-heading", children: [_jsx("h1", { className: "crisp-memberssettings-title", children: title }), _jsxs("p", { className: "crisp-memberssettings-desc", children: [description, " ", _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel })] })] }), _jsx("div", { ref: listRef, className: "crisp-memberssettings-tabpair", role: "tablist", "aria-label": tablistLabel, onKeyDown: onKeyDown, children: tabEntries.map((t) => (_jsxs("button", { type: "button", role: "tab", "data-value": t.value, "aria-selected": tab === t.value, "data-selected": tab === t.value ? "" : undefined, tabIndex: tabIndexFor(t.value), className: "crisp-memberssettings-tab", onClick: () => setTab(t.value), children: [t.icon, t.label] }, t.value))) }), tab === "members" ? (_jsxs("div", { role: "tabpanel", "aria-label": membersTabLabel, children: [_jsxs("div", { className: "crisp-memberssettings-toolbar", children: [_jsxs("div", { className: "crisp-memberssettings-search", children: [_jsx(Search, { size: 16 }), _jsx("input", { value: searchValue, onChange: (e) => onSearchChange?.(e.target.value), placeholder: searchPlaceholder })] }), _jsxs("button", { type: "button", className: "crisp-memberssettings-filterbtn", onClick: onFilterClick, children: [_jsx(Filter, { size: 14 }), filterLabel] }), _jsx("button", { type: "button", className: "crisp-memberssettings-invitebtn", onClick: onInviteClick, children: inviteLabel })] }), _jsx("div", { className: "crisp-memberssettings-table-wrap", children: _jsxs("table", { className: "crisp-memberssettings-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: userColumnLabel }), _jsx("th", { children: roleColumnLabel }), _jsx("th", { children: teamsColumnLabel })] }) }), _jsx("tbody", { children: members.map((m) => (_jsxs("tr", { children: [_jsx("td", { children: _jsxs("div", { className: "crisp-memberssettings-user", children: [m.avatarSrc ? (_jsx("img", { className: "crisp-memberssettings-avatarimg", src: m.avatarSrc, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "circle", tone: "brand", children: String(m.name).charAt(0) })), _jsxs("div", { className: "crisp-memberssettings-usermeta", children: [_jsxs("span", { children: [m.name, m.isYou ? " (You)" : ""] }), _jsx("span", { className: "crisp-memberssettings-useremail", children: m.email })] })] }) }), _jsx("td", { children: m.role === "Admin" ? (_jsx(Badge, { tone: "purple", children: m.role })) : (m.role) }), _jsxs("td", { children: [m.teams ?? "—", _jsx("button", { type: "button", className: "crisp-memberssettings-kebab", "aria-label": memberActionsLabel, onClick: () => onRowMenuClick?.(m), children: _jsx(MoreHorizontal, { size: 16 }) })] })] }, m.id))) })] }) })] })) : (_jsx("div", { role: "tabpanel", "aria-label": teamsTabLabel, children: teamsContent ?? (_jsx("div", { className: "crisp-memberssettings-teamspanel", children: noTeamsLabel })) }))] }) }));
|
|
48
|
+
}
|
|
49
|
+
MembersSettings.displayName = "MembersSettings";
|
|
50
|
+
/**
|
|
51
|
+
* Attio's "Invite members" modal (crisp#636, P-120), measured live: 708px
|
|
52
|
+
* wide, a multi-email composer, an "Invite as" select (default Member) and
|
|
53
|
+
* an optional "Add to team" search-or-create select, footer "Send Invites".
|
|
54
|
+
*/
|
|
55
|
+
export function InviteMembersDialog({ open, onOpenChange, emails, onEmailsChange, inviteAs, onInviteAsChange, team, onTeamChange, teamOptions = [], onSend, title = "Invite members", emailsPlaceholder = "Send Invite to ...", inviteAsFieldLabel = "Invite as", memberOptionLabel = "Member — can edit", adminOptionLabel = "Admin — full access", addToTeamFieldLabel = "Add to team", addToTeamPlaceholder = "Search or create a team", sendLabel = "Send Invites ⌘⏎", ...rest }) {
|
|
56
|
+
const inviteAsOptions = [
|
|
57
|
+
{ value: "member", label: memberOptionLabel },
|
|
58
|
+
{ value: "admin", label: adminOptionLabel },
|
|
59
|
+
];
|
|
60
|
+
return (_jsxs(Dialog, { open: open, onOpenChange: onOpenChange, title: title, width: 708, ...rest, children: [_jsxs(DialogBody, { children: [_jsx("div", { className: "crisp-memberssettings-invitefield", children: _jsx(Textarea, { value: emails, onChange: (e) => onEmailsChange(e.target.value), placeholder: emailsPlaceholder, rows: 3 }) }), _jsx(FormField, { label: inviteAsFieldLabel, children: _jsx(Select, { options: inviteAsOptions, value: inviteAs, onValueChange: (v) => onInviteAsChange(v) }) }), _jsx(FormField, { label: addToTeamFieldLabel, optional: true, children: _jsx(Select, { options: teamOptions, value: team, onValueChange: onTeamChange, placeholder: addToTeamPlaceholder, searchable: true }) })] }), _jsx(DialogFooter, { children: _jsx(Button, { intent: "primary", onClick: onSend, children: sendLabel }) })] }));
|
|
61
|
+
}
|
|
62
|
+
InviteMembersDialog.displayName = "InviteMembersDialog";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// Measured live against app.attio.com/<workspace>/settings/members-and-teams
|
|
4
|
+
// (crisp#636, P-119/P-120). The Members/Teams switcher is a plain segmented
|
|
5
|
+
// pill toggle -- NOT the shared `Tabs` primitive, which always reserves an
|
|
6
|
+
// 8px baseline strip and draws an ink underline (crisp#749's own tabs
|
|
7
|
+
// recipe comment): this page's tab container measured a clean 28px, no
|
|
8
|
+
// baseline, no underline, bg transparent. Reusing `Tabs` here would paint a
|
|
9
|
+
// baseline Attio's own switcher doesn't have.
|
|
10
|
+
const config = defineRecipe({
|
|
11
|
+
name: "memberssettings",
|
|
12
|
+
base: `& .crisp-memberssettings-page { display: flex; flex-direction: column; gap: ${c.space["6"]}; font-family: ${c.font.sans}; }
|
|
13
|
+
& .crisp-memberssettings-heading { display: flex; flex-direction: column; gap: ${c.space["0.5"]}; }
|
|
14
|
+
& .crisp-memberssettings-title { font-size: 24px; line-height: 28px; font-weight: ${c.weight.semibold}; letter-spacing: -0.48px; color: ${c.fgPrimary}; margin: 0; }
|
|
15
|
+
& .crisp-memberssettings-desc { font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgMuted}; margin: 0; }
|
|
16
|
+
|
|
17
|
+
/* Segmented Members/Teams switcher -- measured: 28px pills, radius 8,
|
|
18
|
+
4px gap, 6px icon-label gap, 14/500 label, selected bg bgSubtle +
|
|
19
|
+
inset borderSubtle ring, unselected transparent + a flat 63%-opacity
|
|
20
|
+
fgPrimary mix (measured rgba(0,0,0,.63) on the live page, not the
|
|
21
|
+
solid fgMuted token — same alpha technique this file's table header
|
|
22
|
+
already uses). */
|
|
23
|
+
& .crisp-memberssettings-tabpair { display: inline-flex; align-items: center; gap: 4px; }
|
|
24
|
+
& .crisp-memberssettings-tab { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 28px; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: none; box-shadow: inset 0 0 0 1px transparent; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); cursor: pointer; white-space: nowrap; transition: color .12s ease, background-color .12s ease, box-shadow .12s ease; }
|
|
25
|
+
& .crisp-memberssettings-tab svg { flex-shrink: 0; }
|
|
26
|
+
& .crisp-memberssettings-tab:hover:not([data-selected]) { color: ${c.fgPrimary}; }
|
|
27
|
+
& .crisp-memberssettings-tab[data-selected] { color: ${c.fgPrimary}; background: ${c.bgSubtle}; box-shadow: inset 0 0 0 1px ${c.borderSubtle}; }
|
|
28
|
+
& .crisp-memberssettings-tab:focus-visible { outline: 2px solid ${c.bgBrandSolid}; outline-offset: 1px; }
|
|
29
|
+
|
|
30
|
+
/* Toolbar row: search grows, Filter + Invite sit trailing -- measured
|
|
31
|
+
32px height on all three, search radius 9 with a 6% ink ring,
|
|
32
|
+
Filter/Invite radius 9 with the button drop-shadow pairing. */
|
|
33
|
+
& .crisp-memberssettings-toolbar { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
34
|
+
& .crisp-memberssettings-search { display: flex; align-items: center; flex: 1; min-width: 0; gap: ${c.space["1.5"]}; height: 32px; padding: 0 ${c.space["2.5"]} 0 ${c.space["2"]}; border-radius: 9px; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgMuted}; }
|
|
35
|
+
& .crisp-memberssettings-search:focus-within { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
|
|
36
|
+
& .crisp-memberssettings-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
|
|
37
|
+
& .crisp-memberssettings-search input::placeholder { color: ${c.fgMuted}; }
|
|
38
|
+
& .crisp-memberssettings-filterbtn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 32px; padding: 6px 10px 6px 8px; border: none; border-radius: 9px; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px transparent, 0 0 2px 0 rgba(28,40,64,.18), 0 1px 3px 0 rgba(0,0,0,.04); color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; cursor: pointer; white-space: nowrap; }
|
|
39
|
+
& .crisp-memberssettings-filterbtn:hover { background: ${c.bgRaised}; }
|
|
40
|
+
/* your-moon/crisp#979: bgBrandContrast, not bgBrandSolid -- the accent
|
|
41
|
+
(#0298d9) only clears 3.23:1 with white; the darker sibling clears
|
|
42
|
+
4.5:1+, same fix already applied to Button/Badge/Avatar (#897) and
|
|
43
|
+
TableToolbar/Dialog/Command/Comments/AuthShell (#953/#961), which
|
|
44
|
+
explicitly flagged this button as found-not-fixed. */
|
|
45
|
+
& .crisp-memberssettings-invitebtn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 32px; padding: 6px 10px 6px 8px; border: none; border-radius: 9px; background: ${c.bgBrandContrast}; box-shadow: inset 0 0 0 1px rgba(0,0,0,.06), 0 2px 4px -2px color-mix(in srgb, ${c.bgBrandContrast} 12%, transparent), 0 3px 6px -2px color-mix(in srgb, ${c.bgBrandContrast} 8%, transparent); color: ${c.fgOnBrand}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; cursor: pointer; white-space: nowrap; }
|
|
46
|
+
& .crisp-memberssettings-invitebtn:hover { filter: brightness(0.95); }
|
|
47
|
+
|
|
48
|
+
/* Table wrapper -- measured card: radius 12, 1px borderSubtle, 40px
|
|
49
|
+
rows, header label 12/500/63%-opacity fgPrimary. */
|
|
50
|
+
& .crisp-memberssettings-table-wrap { border-radius: 12px; border: 1px solid ${c.borderSubtle}; overflow: hidden; }
|
|
51
|
+
& .crisp-memberssettings-table { width: 100%; border-collapse: collapse; font-family: inherit; }
|
|
52
|
+
& .crisp-memberssettings-table th { height: 32px; padding: 0 ${c.space["4"]}; text-align: start; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); border-bottom: 1px solid ${c.borderSubtle}; background: ${c.bgSurface}; }
|
|
53
|
+
& .crisp-memberssettings-table td { height: 40px; padding: 0 ${c.space["4"]}; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; border-bottom: 1px solid ${c.borderSubtle}; }
|
|
54
|
+
& .crisp-memberssettings-table tr:last-child td { border-bottom: none; }
|
|
55
|
+
& .crisp-memberssettings-user { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
56
|
+
& .crisp-memberssettings-avatarimg { width: 24px; height: 24px; border-radius: ${c.radius.full}; object-fit: cover; }
|
|
57
|
+
/* Name + email sit on ONE line, not stacked -- confirmed live: both spans
|
|
58
|
+
share the same y and the same 14px size, only the email's colour
|
|
59
|
+
differs (a flat 63%-opacity fgPrimary mix, not a smaller muted font). */
|
|
60
|
+
& .crisp-memberssettings-usermeta { display: flex; align-items: baseline; gap: ${c.space["2"]}; min-width: 0; }
|
|
61
|
+
& .crisp-memberssettings-usermeta > span:first-child { flex-shrink: 0; }
|
|
62
|
+
& .crisp-memberssettings-useremail { font-size: ${c.text.lg}; font-weight: ${c.weight.regular}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
63
|
+
& .crisp-memberssettings-kebab { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-inline-start: auto; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; color: ${c.fgMuted}; cursor: pointer; }
|
|
64
|
+
& .crisp-memberssettings-kebab:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
|
|
65
|
+
|
|
66
|
+
& .crisp-memberssettings-teamspanel { display: flex; align-items: center; justify-content: center; padding: ${c.space["8"]} 0; color: ${c.fgMuted}; font-size: ${c.text.lg}; }
|
|
67
|
+
|
|
68
|
+
/* Invite dialog fields -- measured 708x386 dialog, radius 16; the
|
|
69
|
+
"Invite as"/"Add to team" selects are 32/34px with a flat 1px
|
|
70
|
+
6%-ink border and 9/10px radius respectively (Attio's own two
|
|
71
|
+
selects don't share one radius). */
|
|
72
|
+
& .crisp-memberssettings-invitefield { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; }
|
|
73
|
+
& .crisp-memberssettings-invitelabel { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
|
|
74
|
+
`,
|
|
75
|
+
variants: {},
|
|
76
|
+
defaultVariants: {},
|
|
77
|
+
compoundVariants: [],
|
|
78
|
+
});
|
|
79
|
+
export const memberssettingsRecipe = resolveRecipe(config);
|
|
80
|
+
export default config;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { NotificationsSettings, } from "./notificationssettings.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Bell, Mail, MonitorSmartphone } from "lucide-react";
|
|
3
|
+
import { Checkbox } from "../../components/checkbox/index.js";
|
|
4
|
+
import { Switch } from "../../components/switch/index.js";
|
|
5
|
+
import { TextLink } from "../../components/textlink/index.js";
|
|
6
|
+
import { notificationssettingsRecipe } from "./notificationssettings.recipe.js";
|
|
7
|
+
/**
|
|
8
|
+
* Attio's "Notifications" settings page (crisp#630, P-083): title +
|
|
9
|
+
* description, a "Daily digest" card (a single toggle row), and a
|
|
10
|
+
* "Collaboration notifications" matrix — a header row plus one row per
|
|
11
|
+
* event, each with an Email and an App checkbox. Measured live.
|
|
12
|
+
*/
|
|
13
|
+
export function NotificationsSettings({ dailyDigestEnabled, onDailyDigestChange, rows, onEmailChange, onAppChange, className, title = "Notifications", description = "Customize your notification settings to stay informed without being overwhelmed.", learnMoreLabel = "Learn more", learnMoreHref = "#", dailyDigestSectionTitle = "Daily digest", dailyDigestRowTitle = "Enable daily digest", dailyDigestRowDescription = "Includes tasks overdue and due today. Sent every morning if any tasks are due or overdue.", collaborationSectionTitle = "Collaboration notifications", notifyMeAboutLabel = "Notify me about", emailColumnLabel = "Email", appColumnLabel = "App", ...rest }) {
|
|
14
|
+
return (_jsx("div", { className: notificationssettingsRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-notificationssettings-page", children: [_jsxs("div", { className: "crisp-notificationssettings-heading", children: [_jsx("h1", { className: "crisp-notificationssettings-title", children: title }), _jsxs("p", { className: "crisp-notificationssettings-desc", children: [description, " ", _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel })] })] }), _jsxs("div", { className: "crisp-notificationssettings-section", children: [_jsx("h2", { className: "crisp-notificationssettings-sectiontitle", children: dailyDigestSectionTitle }), _jsxs("div", { className: "crisp-notificationssettings-digestcard", children: [_jsxs("div", { className: "crisp-notificationssettings-digesttext", children: [_jsx("span", { className: "crisp-notificationssettings-rowtitle", children: dailyDigestRowTitle }), _jsx("span", { className: "crisp-notificationssettings-rowdesc", children: dailyDigestRowDescription })] }), _jsx(Switch, { checked: dailyDigestEnabled, onCheckedChange: onDailyDigestChange, "aria-label": dailyDigestRowTitle })] })] }), _jsxs("div", { className: "crisp-notificationssettings-section", children: [_jsx("h2", { className: "crisp-notificationssettings-sectiontitle", children: collaborationSectionTitle }), _jsxs("div", { className: "crisp-notificationssettings-matrix", children: [_jsxs("div", { className: "crisp-notificationssettings-matrixhead", children: [_jsxs("span", { className: "crisp-notificationssettings-matrixhead-about", children: [_jsx(Bell, { size: 14 }), notifyMeAboutLabel] }), _jsxs("span", { className: "crisp-notificationssettings-matrixhead-col", children: [_jsx(Mail, { size: 14 }), emailColumnLabel] }), _jsxs("span", { className: "crisp-notificationssettings-matrixhead-col", children: [_jsx(MonitorSmartphone, { size: 14 }), appColumnLabel] })] }), rows.map((row) => (_jsxs("div", { className: "crisp-notificationssettings-matrixrow", children: [_jsxs("div", { className: "crisp-notificationssettings-matrixrow-about", children: [_jsx("span", { className: "crisp-notificationssettings-rowtitle", children: row.title }), _jsx("span", { className: "crisp-notificationssettings-rowdesc", children: row.description })] }), _jsx("div", { className: "crisp-notificationssettings-matrixrow-col", children: _jsx(Checkbox, { checked: row.email, onChange: (e) => onEmailChange(row.id, e.target.checked), "aria-label": `${emailColumnLabel}: ${typeof row.title === "string" ? row.title : row.id}` }) }), _jsx("div", { className: "crisp-notificationssettings-matrixrow-col", children: _jsx(Checkbox, { checked: row.app, onChange: (e) => onAppChange(row.id, e.target.checked), "aria-label": `${appColumnLabel}: ${typeof row.title === "string" ? row.title : row.id}` }) })] }, row.id)))] })] })] }) }));
|
|
15
|
+
}
|
|
16
|
+
NotificationsSettings.displayName = "NotificationsSettings";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// Measured live against app.attio.com/<workspace>/settings/notifications
|
|
4
|
+
// (crisp#630, P-083). Two sections, each a 16/600 heading over a bordered
|
|
5
|
+
// 12px-radius card: "Daily digest" (a single 56px row, label 14/500 +
|
|
6
|
+
// description 12/500/63%-opacity, a Switch trailing) and "Collaboration
|
|
7
|
+
// notifications" (a header row -- 12/500/63%-opacity labels -- then 7 rows,
|
|
8
|
+
// each 52px, a title/description pair on the left and two Checkboxes,
|
|
9
|
+
// right-aligned under Email/App, per row).
|
|
10
|
+
const config = defineRecipe({
|
|
11
|
+
name: "notificationssettings",
|
|
12
|
+
base: `& .crisp-notificationssettings-page { display: flex; flex-direction: column; gap: ${c.space["6"]}; font-family: ${c.font.sans}; }
|
|
13
|
+
& .crisp-notificationssettings-heading { display: flex; flex-direction: column; gap: ${c.space["0.5"]}; }
|
|
14
|
+
& .crisp-notificationssettings-title { font-size: 24px; line-height: 28px; font-weight: ${c.weight.semibold}; letter-spacing: -0.48px; color: ${c.fgPrimary}; margin: 0; }
|
|
15
|
+
& .crisp-notificationssettings-desc { font-size: ${c.text.lg}; line-height: 20px; color: ${c.fgMuted}; margin: 0; }
|
|
16
|
+
|
|
17
|
+
& .crisp-notificationssettings-section { display: flex; flex-direction: column; gap: ${c.space["4"]}; }
|
|
18
|
+
& .crisp-notificationssettings-sectiontitle { font-size: 16px; line-height: 20px; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; margin: 0; }
|
|
19
|
+
|
|
20
|
+
/* Daily digest card: one bordered 12px-radius row. */
|
|
21
|
+
& .crisp-notificationssettings-digestcard { display: flex; align-items: center; justify-content: space-between; gap: ${c.space["6"]}; padding: ${c.space["2"]} ${c.space["3"]}; border: 1px solid ${c.borderSubtle}; border-radius: 12px; background: ${c.bgSurface}; }
|
|
22
|
+
& .crisp-notificationssettings-digesttext { display: flex; flex-direction: column; gap: ${c.space["1"]}; }
|
|
23
|
+
& .crisp-notificationssettings-rowtitle { font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
|
|
24
|
+
& .crisp-notificationssettings-rowdesc { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
25
|
+
|
|
26
|
+
/* Collaboration matrix: a bordered card, header row + N body rows. */
|
|
27
|
+
& .crisp-notificationssettings-matrix { border: 1px solid ${c.borderSubtle}; border-radius: 12px; overflow: hidden; background: ${c.bgSurface}; }
|
|
28
|
+
& .crisp-notificationssettings-matrixhead { display: flex; align-items: center; gap: ${c.space["3"]}; padding: 0 ${c.space["4"]}; height: 40px; border-bottom: 1px solid ${c.borderSubtle}; }
|
|
29
|
+
& .crisp-notificationssettings-matrixhead-about { display: flex; align-items: center; gap: ${c.space["1.5"]}; flex: 1; min-width: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
30
|
+
& .crisp-notificationssettings-matrixhead-col { display: flex; align-items: center; justify-content: center; gap: ${c.space["1.5"]}; width: 64px; flex-shrink: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
31
|
+
& .crisp-notificationssettings-matrixrow { display: flex; align-items: center; gap: ${c.space["3"]}; padding: ${c.space["2"]} ${c.space["4"]}; min-height: 52px; }
|
|
32
|
+
& .crisp-notificationssettings-matrixrow + .crisp-notificationssettings-matrixrow { border-top: 1px solid ${c.borderSubtle}; }
|
|
33
|
+
& .crisp-notificationssettings-matrixrow-about { display: flex; flex-direction: column; gap: ${c.space["1"]}; flex: 1; min-width: 0; }
|
|
34
|
+
& .crisp-notificationssettings-matrixrow-col { display: flex; align-items: center; justify-content: center; width: 64px; flex-shrink: 0; }
|
|
35
|
+
`,
|
|
36
|
+
variants: {},
|
|
37
|
+
defaultVariants: {},
|
|
38
|
+
compoundVariants: [],
|
|
39
|
+
});
|
|
40
|
+
export const notificationssettingsRecipe = resolveRecipe(config);
|
|
41
|
+
export default config;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Activity as ActivityIcon, CalendarClock, ChevronDown, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, Workflow, X, Zap, } from "lucide-react";
|
|
2
|
+
import { Activity as ActivityIcon, CalendarClock, ChevronDown, ChevronUp, CircleDollarSign, Copy, DollarSign, FileText, Folder, Inbox, Landmark, LayoutGrid, Mail, Maximize2, Merge, MessageSquare, Minimize2, MoreHorizontal, Pencil, Phone, Plus, Search, Settings, Sparkles, SquareCheck, Star, Trash2, Users, UsersRound, Workflow, X, Zap, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Avatar } from "../../components/avatar/index.js";
|
|
5
5
|
import { Checkbox } from "../../components/checkbox/index.js";
|
|
6
|
+
import { Drawer } from "../../components/drawer/index.js";
|
|
6
7
|
import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
|
|
7
8
|
import { Select } from "../../components/select/index.js";
|
|
8
9
|
import { Spinner } from "../../components/spinner/index.js";
|
|
9
10
|
import { Switch } from "../../components/switch/index.js";
|
|
10
11
|
import { Tabs } from "../../components/tabs/index.js";
|
|
11
12
|
import { cx, isRtlElement, useCrispComponentDefaults } from "../../core/index.js";
|
|
13
|
+
import { CommentsPanel, } from "../comments/index.js";
|
|
12
14
|
import { tagTone } from "./column.js";
|
|
13
15
|
import { editableInputType, useEditableField } from "./editable-field.js";
|
|
14
16
|
import { recordpageRecipe } from "./recordpage.recipe.js";
|
|
@@ -33,9 +35,16 @@ export const recordPageLabels = {
|
|
|
33
35
|
breadcrumb: "Breadcrumb",
|
|
34
36
|
editRecord: "Edit record",
|
|
35
37
|
recordActions: "Record actions",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
// US spelling, matching Attio's own live text exactly (#621) — this string
|
|
39
|
+
// is shared with the entity header's favourite-star aria-label, so both
|
|
40
|
+
// read "favorites" now, not just the menu.
|
|
41
|
+
addToFavourites: "Add to favorites",
|
|
42
|
+
removeFromFavourites: "Remove from favorites",
|
|
38
43
|
copyRecordLink: "Copy record link",
|
|
44
|
+
configurePage: "Configure page",
|
|
45
|
+
copyPageUrl: "Copy page URL",
|
|
46
|
+
copyRecordId: "Copy record ID",
|
|
47
|
+
mergeRecords: "Merge records",
|
|
39
48
|
deleteRecord: "Delete record",
|
|
40
49
|
linkRecords: "Link records",
|
|
41
50
|
// Tooltip copy re-measured live for #865 (was "Log a note"/"Create a
|
|
@@ -50,6 +59,8 @@ export const recordPageLabels = {
|
|
|
50
59
|
composeEmail: "Compose email",
|
|
51
60
|
askAttio: "Ask AI",
|
|
52
61
|
addToList: "Add to list",
|
|
62
|
+
share: "Share",
|
|
63
|
+
comments: "Comments",
|
|
53
64
|
noLists: "This record has not been added to any lists.",
|
|
54
65
|
highlights: "Highlights",
|
|
55
66
|
recordDetails: "Record Details",
|
|
@@ -262,7 +273,14 @@ function NotesSection({ notes, onAdd, labels, }) {
|
|
|
262
273
|
return (_jsxs("section", { children: [_jsx(SectionHead, { title: labels.tabs.notes, count: notes.length, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", onClick: () => setOpen(true), children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), labels.newNote] }) }), open && (_jsx(Composer, { labels: labels, multiline: true, placeholder: labels.notePlaceholder, submitLabel: labels.saveNote, onCancel: () => setOpen(false), onSubmit: (t) => {
|
|
263
274
|
onAdd(t);
|
|
264
275
|
setOpen(false);
|
|
265
|
-
} })), notes.length === 0 && !open ? (_jsx("div", { className: "crisp-recordpage-emptystate", children: labels.noNotesYet })) : (
|
|
276
|
+
} })), notes.length === 0 && !open ? (_jsx("div", { className: "crisp-recordpage-emptystate", children: labels.noNotesYet })) : (
|
|
277
|
+
// #871: one line, not a two-row card -- measured live: avatar,
|
|
278
|
+
// then the note's own title and body run together inline, then a
|
|
279
|
+
// relative time at the far end of the SAME row. No author name at
|
|
280
|
+
// all (Attio's real preview never shows one here, only the note's
|
|
281
|
+
// own content) -- `labels.you` stays defined for a consumer that
|
|
282
|
+
// still wants it elsewhere, just unused in this exact spot now.
|
|
283
|
+
notes.map((n) => (_jsxs("div", { className: "crisp-recordpage-note", children: [_jsxs("span", { className: "crisp-recordpage-note-left", children: [_jsx(Avatar, { size: "sm", shape: "circle", tone: "neutral", children: "Y" }), n.title && (_jsx("span", { className: "crisp-recordpage-note-title", children: n.title })), _jsx("span", { className: "crisp-recordpage-note-text", children: n.text })] }), _jsx("span", { className: "crisp-recordpage-note-time", children: n.time })] }, n.id))))] }));
|
|
266
284
|
}
|
|
267
285
|
function TasksSection({ tasks, onAdd, onToggle, labels, }) {
|
|
268
286
|
const [open, setOpen] = React.useState(false);
|
|
@@ -295,13 +313,51 @@ function EmptyCard({ icon, children, }) {
|
|
|
295
313
|
* `RecordPage`'s own root does: `<div className={recordpageRecipe()}>`
|
|
296
314
|
* (also exported from this package).
|
|
297
315
|
*/
|
|
298
|
-
export function RecordDetails({ record, columns, sections, onEdit, labels, className, ...rest }) {
|
|
316
|
+
export function RecordDetails({ record, columns, sections, onEdit, labels, className, resizable = false, width: controlledWidth, defaultWidth = DETAILS_DEFAULT_W, onWidthChange, minWidth = DETAILS_MIN_W, maxWidth = DETAILS_MAX_W, ...rest }) {
|
|
299
317
|
const t = useRecordPageLabels(labels);
|
|
300
318
|
const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = useEditableField(onEdit);
|
|
301
319
|
const [detailsOpen, setDetailsOpen] = React.useState(true);
|
|
302
320
|
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
303
321
|
const [fieldQuery, setFieldQuery] = React.useState("");
|
|
304
322
|
const [showAllValues, setShowAllValues] = React.useState(false);
|
|
323
|
+
// #930: controlled/uncontrolled width, same split as RecordPanel's own
|
|
324
|
+
// resizable width -- only read when `resizable`, same reasoning as
|
|
325
|
+
// RecordPanel's own (a non-resizing consumer has no business carrying
|
|
326
|
+
// width state at all).
|
|
327
|
+
const [internalWidth, setInternalWidth] = React.useState(defaultWidth);
|
|
328
|
+
const width = controlledWidth ?? internalWidth;
|
|
329
|
+
const setWidth = (next) => {
|
|
330
|
+
const clamped = Math.max(minWidth, Math.min(maxWidth, next));
|
|
331
|
+
if (controlledWidth === undefined)
|
|
332
|
+
setInternalWidth(clamped);
|
|
333
|
+
onWidthChange?.(clamped);
|
|
334
|
+
};
|
|
335
|
+
const [detailsResizing, setDetailsResizing] = React.useState(false);
|
|
336
|
+
const detailsDragRef = React.useRef(null);
|
|
337
|
+
const onDetailsResizeDown = (e) => {
|
|
338
|
+
// This column is a leading (left-docked) rail, the mirror image of
|
|
339
|
+
// RecordPanel's own trailing (right-docked) one -- a rightward pull
|
|
340
|
+
// grows it in LTR, the same direction RecordPage's own internal aside
|
|
341
|
+
// resize already uses (#212/#255).
|
|
342
|
+
detailsDragRef.current = {
|
|
343
|
+
x: e.clientX,
|
|
344
|
+
w: width,
|
|
345
|
+
dir: isRtlElement(e.currentTarget) ? -1 : 1,
|
|
346
|
+
};
|
|
347
|
+
setDetailsResizing(true);
|
|
348
|
+
e.target.setPointerCapture(e.pointerId);
|
|
349
|
+
};
|
|
350
|
+
const onDetailsResizeMove = (e) => {
|
|
351
|
+
const r = detailsDragRef.current;
|
|
352
|
+
if (!r)
|
|
353
|
+
return;
|
|
354
|
+
setWidth(r.w + r.dir * (e.clientX - r.x));
|
|
355
|
+
};
|
|
356
|
+
const onDetailsResizeUp = (e) => {
|
|
357
|
+
detailsDragRef.current = null;
|
|
358
|
+
setDetailsResizing(false);
|
|
359
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
360
|
+
};
|
|
305
361
|
const fields = columns.filter((col) => col.type !== "entity");
|
|
306
362
|
const q = fieldQuery.trim().toLowerCase();
|
|
307
363
|
const matches = (col) => !q || col.label.toLowerCase().includes(q);
|
|
@@ -338,7 +394,7 @@ export function RecordDetails({ record, columns, sections, onEdit, labels, class
|
|
|
338
394
|
groups = [{ title: undefined, fields: shown }];
|
|
339
395
|
}
|
|
340
396
|
const totalShown = groups.reduce((n, g) => n + g.fields.length, 0);
|
|
341
|
-
return (_jsxs("div", { className: cx("crisp-recordpage-section", className), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? (_jsx("input", {
|
|
397
|
+
return (_jsxs("div", { className: cx("crisp-recordpage-section", className), "data-resizable": resizable ? "" : undefined, style: resizable ? { width } : undefined, ...rest, children: [_jsxs("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? (_jsx("input", {
|
|
342
398
|
// biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
|
|
343
399
|
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) => {
|
|
344
400
|
if (e.key === "Escape") {
|
|
@@ -412,7 +468,13 @@ export function RecordDetails({ record, columns, sections, onEdit, labels, class
|
|
|
412
468
|
setSearchOpen(next);
|
|
413
469
|
if (!next)
|
|
414
470
|
setFieldQuery("");
|
|
415
|
-
}, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsx("div", { className: "crisp-recordpage-noresults", children: t.noFieldsMatch(fieldQuery) })))
|
|
471
|
+
}, children: showAllValues ? t.viewLess : t.viewAllValues }))] })) : (_jsx("div", { className: "crisp-recordpage-noresults", children: t.noFieldsMatch(fieldQuery) }))), resizable && (_jsx("div", { className: "crisp-recordpage-detailresize", "data-active": detailsResizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(width), "aria-valuemin": minWidth, "aria-valuemax": maxWidth, tabIndex: 0, onPointerDown: onDetailsResizeDown, onPointerMove: onDetailsResizeMove, onPointerUp: onDetailsResizeUp, onKeyDown: (e) => {
|
|
472
|
+
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
473
|
+
return;
|
|
474
|
+
const rtl = isRtlElement(e.currentTarget);
|
|
475
|
+
const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
476
|
+
setWidth(grow ? width + 16 : width - 16);
|
|
477
|
+
} }))] }));
|
|
416
478
|
}
|
|
417
479
|
RecordDetails.displayName = "RecordDetails";
|
|
418
480
|
/**
|
|
@@ -423,7 +485,7 @@ RecordDetails.displayName = "RecordDetails";
|
|
|
423
485
|
* Emails sections (Notes & Tasks are live — add and check things off). Drop it
|
|
424
486
|
* into the app's main content area.
|
|
425
487
|
*/
|
|
426
|
-
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, quickActions: quickActionsProp, ...rest }) {
|
|
488
|
+
export function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, onClose, closeLabel = "Close", onPrevious, previousRecordLabel = "Previous record", onNext, nextRecordLabel = "Next record", counterLabel, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, quickActions: quickActionsProp, comments: commentsProp, onPostComment, currentUser, onShare, ...rest }) {
|
|
427
489
|
const t = useRecordPageLabels(labels);
|
|
428
490
|
const [favorite, setFavorite] = React.useState(false);
|
|
429
491
|
// Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
|
|
@@ -435,6 +497,11 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
435
497
|
const [internalTasks, setInternalTasks] = React.useState([]);
|
|
436
498
|
const notes = notesProp ?? internalNotes;
|
|
437
499
|
const tasks = tasksProp ?? internalTasks;
|
|
500
|
+
// Comments follow the same controlled/self-contained duality (#864).
|
|
501
|
+
const [internalComments, setInternalComments] = React.useState([]);
|
|
502
|
+
const comments = commentsProp ?? internalComments;
|
|
503
|
+
const [commentsOpen, setCommentsOpen] = React.useState(false);
|
|
504
|
+
const me = currentUser ?? { id: "me", name: "You" };
|
|
438
505
|
const [showAllActivity, setShowAllActivity] = React.useState(false);
|
|
439
506
|
// Drag-resizable left details column.
|
|
440
507
|
const [detailsW, setDetailsW] = React.useState(DETAILS_DEFAULT_W);
|
|
@@ -593,6 +660,31 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
593
660
|
}
|
|
594
661
|
setInternalTasks((n) => n.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
|
|
595
662
|
};
|
|
663
|
+
const postComment = (text, threadId) => {
|
|
664
|
+
if (commentsProp) {
|
|
665
|
+
onPostComment?.(text, threadId);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (threadId == null) {
|
|
669
|
+
setInternalComments((prev) => [
|
|
670
|
+
{
|
|
671
|
+
id: nextId(),
|
|
672
|
+
root: { id: nextId(), author: me, time: t.justNow, body: text },
|
|
673
|
+
},
|
|
674
|
+
...prev,
|
|
675
|
+
]);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
setInternalComments((prev) => prev.map((th) => th.id === threadId
|
|
679
|
+
? {
|
|
680
|
+
...th,
|
|
681
|
+
replies: [
|
|
682
|
+
...(th.replies ?? []),
|
|
683
|
+
{ id: nextId(), author: me, time: t.justNow, body: text },
|
|
684
|
+
],
|
|
685
|
+
}
|
|
686
|
+
: th));
|
|
687
|
+
};
|
|
596
688
|
// A small, plausible activity feed derived from the record's own fields.
|
|
597
689
|
const domain = byKey.get("domain");
|
|
598
690
|
const cats = byKey.get("categories");
|
|
@@ -649,7 +741,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
649
741
|
const effectivePanel = tabsProp
|
|
650
742
|
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
651
743
|
: builtinPanel;
|
|
652
|
-
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(
|
|
744
|
+
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", "data-peek": onClose ? "" : undefined, children: [onClose ? (_jsxs("div", { className: "crisp-recordpage-peekbar", children: [_jsx("button", { type: "button", className: "crisp-recordpage-peek-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75 }) }), (onPrevious || onNext) && (_jsxs("span", { className: "crisp-recordpage-peek-pager", children: [_jsx("button", { type: "button", className: "crisp-recordpage-peek-navbtn", "aria-label": previousRecordLabel, disabled: !onPrevious, onClick: onPrevious, children: _jsx(ChevronUp, { size: 16, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-peek-navbtn", "aria-label": nextRecordLabel, disabled: !onNext, onClick: onNext, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] })), counterLabel != null && (_jsx("span", { className: "crisp-recordpage-peek-counter", children: counterLabel }))] })) : (_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: [_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", onClick: onShare, children: t.share }), _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: () => setCommentsOpen(true), "aria-label": t.comments, children: _jsx(MessageSquare, { size: 15, strokeWidth: 1.75 }) }), _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(Merge, { size: 15, strokeWidth: 1.75 }), children: t.mergeRecords }), _jsx(MenuItem, { icon: _jsx(Settings, { size: 15, strokeWidth: 1.75 }), children: t.configurePage }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyPageUrl }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyRecordId }), _jsx(MenuSeparator, {}), _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, 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] }), _jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => (_jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: _jsx(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), _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": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
|
|
653
745
|
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
654
746
|
return;
|
|
655
747
|
// The pointer drag learned its direction in #212; this did not.
|
|
@@ -661,6 +753,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
661
753
|
setDetailsW((w) => grow
|
|
662
754
|
? Math.min(DETAILS_MAX_W, w + 16)
|
|
663
755
|
: Math.max(DETAILS_MIN_W, w - 16));
|
|
664
|
-
} }), _jsx("main", { className: "crisp-recordpage-main", children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] })] }));
|
|
756
|
+
} }), _jsx("main", { className: "crisp-recordpage-main", children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] }), _jsx(Drawer, { open: commentsOpen, onOpenChange: setCommentsOpen, "aria-label": t.comments, style: { width: 320 }, children: _jsx(CommentsPanel, { threads: comments, currentUserId: me.id, onPost: postComment, onClose: () => setCommentsOpen(false), role: undefined }) })] }));
|
|
665
757
|
}
|
|
666
758
|
RecordPage.displayName = "RecordPage";
|
|
@@ -23,6 +23,21 @@ const config = defineRecipe({
|
|
|
23
23
|
& * { box-sizing: border-box; }
|
|
24
24
|
|
|
25
25
|
& .crisp-recordpage-topbar { display: flex; align-items: center; gap: ${c.space["2"]}; height: 44px; flex-shrink: 0; padding: 0 ${c.space["3"]}; border-bottom: 1px solid ${c.borderSubtle}; }
|
|
26
|
+
/* #624: expanding a peek to its full record keeps a hybrid header
|
|
27
|
+
(Close/Previous/Next/counter) instead of reverting to the plain
|
|
28
|
+
breadcrumb above -- measured live on a real expanded record: 49px
|
|
29
|
+
tall (not 44px), 8px/10px padding, same borderSubtle hairline. Same
|
|
30
|
+
tokens as product/recordpanel's own already-shipped toolbar, not
|
|
31
|
+
new values -- this is the same row, reused in a second component. */
|
|
32
|
+
& .crisp-recordpage-topbar[data-peek] { height: 49px; padding: ${c.space["2"]} ${c.space["2.5"]}; }
|
|
33
|
+
& .crisp-recordpage-peekbar { display: flex; align-items: center; }
|
|
34
|
+
& .crisp-recordpage-peek-iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: transparent; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
|
|
35
|
+
& .crisp-recordpage-peek-iconbtn:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
|
|
36
|
+
& .crisp-recordpage-peek-pager { display: flex; align-items: center; gap: ${c.space["1.5"]}; margin-inline-start: ${c.space["3"]}; }
|
|
37
|
+
& .crisp-recordpage-peek-navbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: ${e.field}; color: ${c.fgPrimary}; font-family: inherit; cursor: pointer; }
|
|
38
|
+
& .crisp-recordpage-peek-navbtn:hover:not(:disabled) { background: ${c.bgRaised}; }
|
|
39
|
+
& .crisp-recordpage-peek-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
40
|
+
& .crisp-recordpage-peek-counter { margin-inline-start: ${c.space["3"]}; font-size: 12px; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
26
41
|
& .crisp-recordpage-crumbs { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; }
|
|
27
42
|
& .crisp-recordpage-crumb { display: inline-flex; align-items: center; height: 26px; padding: 0 ${c.space["1.5"]}; margin: 0 calc(-1 * ${c.space["1.5"]}) 0 0; border: none; border-radius: ${c.radius.md}; background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; cursor: pointer; }
|
|
28
43
|
& .crisp-recordpage-crumb:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
|
|
@@ -42,6 +57,17 @@ const config = defineRecipe({
|
|
|
42
57
|
& .crisp-recordpage-resizer::before { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -4px; width: 9px; }
|
|
43
58
|
& .crisp-recordpage-resizer::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-start: -1px; width: 2px; background: transparent; transition: background-color .12s ease; }
|
|
44
59
|
& .crisp-recordpage-resizer:hover::after, & .crisp-recordpage-resizer[data-active]::after { background: color-mix(in srgb, ${c.bgBrandSolid} 55%, transparent); }
|
|
60
|
+
/* #930: RecordDetails' own standalone resize handle -- same visible
|
|
61
|
+
hit-slop/line-highlight shape as .crisp-recordpage-resizer just above,
|
|
62
|
+
just absolutely positioned at the trailing edge instead of a 0-width
|
|
63
|
+
flex sibling, since RecordDetails always returns a single root (the
|
|
64
|
+
same internal-strip placement RecordPanel's own resize handle uses,
|
|
65
|
+
mirrored to the opposite edge -- this column is leading, RecordPanel
|
|
66
|
+
is trailing). */
|
|
67
|
+
& .crisp-recordpage-section[data-resizable] { position: relative; }
|
|
68
|
+
& .crisp-recordpage-detailresize { position: absolute; top: 0; bottom: 0; inset-inline-end: -4px; width: 9px; cursor: col-resize; z-index: 5; touch-action: none; }
|
|
69
|
+
& .crisp-recordpage-detailresize::after { content: ""; position: absolute; top: 0; bottom: 0; inset-inline-end: 3px; width: 2px; background: transparent; transition: background-color .12s ease; }
|
|
70
|
+
& .crisp-recordpage-detailresize:hover::after, & .crisp-recordpage-detailresize[data-active]::after { background: color-mix(in srgb, ${c.bgBrandSolid} 55%, transparent); }
|
|
45
71
|
& .crisp-recordpage-entity { display: flex; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["1"]} ${c.space["1"]} ${c.space["2"]}; }
|
|
46
72
|
& .crisp-recordpage-entity-logo { width: 32px; height: 32px; border-radius: 30%; flex-shrink: 0; object-fit: cover; background: ${c.bgRaised}; }
|
|
47
73
|
& .crisp-recordpage-entity-name { flex: 0 1 auto; min-width: 0; font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; letter-spacing: -0.3px; color: ${c.fgPrimary}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -175,12 +201,23 @@ const config = defineRecipe({
|
|
|
175
201
|
& .crisp-recordpage-btn--primary { box-shadow: none; background: ${c.bgBrandSolid}; color: ${c.fgOnBrand}; }
|
|
176
202
|
& .crisp-recordpage-btn--primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
177
203
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
204
|
+
/* #871: one bordered card per note (border-radius.xl, borderSubtle),
|
|
205
|
+
not a flush divider-list row -- measured live on a real populated
|
|
206
|
+
note: the whole row is flex space-between, avatar+title+body on the
|
|
207
|
+
left (its own flex, gap.2), the relative time alone on the right.
|
|
208
|
+
The real DOM nests an inner clickable button (padding 6/8, radius.md)
|
|
209
|
+
inside an outer card (padding 4px, radius.xl) -- collapsed into this
|
|
210
|
+
one non-interactive div's own combined padding (2.5/3) since crisp's
|
|
211
|
+
NotesSection has no click-to-open behaviour to justify the extra
|
|
212
|
+
layer. The 6px gap between stacked cards is NOT independently
|
|
213
|
+
measured (this workspace only ever had one real note to check) --
|
|
214
|
+
a judgement call off the existing space scale, not a live value. */
|
|
215
|
+
& .crisp-recordpage-note { display: flex; align-items: center; justify-content: space-between; gap: ${c.space["3"]}; padding: ${c.space["2.5"]} ${c.space["3"]}; border: 1px solid ${c.borderSubtle}; border-radius: ${c.radius.xl}; }
|
|
216
|
+
& .crisp-recordpage-note + .crisp-recordpage-note { margin-top: ${c.space["1.5"]}; }
|
|
217
|
+
& .crisp-recordpage-note-left { display: flex; align-items: center; gap: ${c.space["2"]}; min-width: 0; overflow: hidden; }
|
|
218
|
+
& .crisp-recordpage-note-title { flex-shrink: 0; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
|
|
219
|
+
& .crisp-recordpage-note-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
220
|
+
& .crisp-recordpage-note-time { flex-shrink: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent); }
|
|
184
221
|
|
|
185
222
|
& .crisp-recordpage-task { display: flex; align-items: flex-start; gap: ${c.space["2"]}; padding: ${c.space["1.5"]} 0; border-top: 1px solid ${c.borderSubtle}; }
|
|
186
223
|
& .crisp-recordpage-task-text { flex: 1; min-width: 0; font-size: ${c.text.lg}; color: ${c.fgPrimary}; padding-top: 1px; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { WorkspaceGeneralSettings, } from "./workspacegeneralsettings.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Copy } from "lucide-react";
|
|
3
|
+
import { Avatar } from "../../components/avatar/index.js";
|
|
4
|
+
import { Button } from "../../components/button/index.js";
|
|
5
|
+
import { FormField } from "../../components/formfield/index.js";
|
|
6
|
+
import { Input } from "../../components/input/index.js";
|
|
7
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../../components/table/index.js";
|
|
8
|
+
import { TextLink } from "../../components/textlink/index.js";
|
|
9
|
+
import { workspacegeneralsettingsRecipe } from "./workspacegeneralsettings.recipe.js";
|
|
10
|
+
/**
|
|
11
|
+
* Attio's Settings → Workspace → General page (crisp#638, P-128), measured
|
|
12
|
+
* live: a logo + Name/Slug section, an "Export workspace data" table, and a
|
|
13
|
+
* "Danger zone" delete-workspace row.
|
|
14
|
+
*/
|
|
15
|
+
export function WorkspaceGeneralSettings({ logoSrc, workspaceInitial, onUploadLogo, name, onNameChange, slug, onCopySlug, exports: exportRows, onStartExport, onDeleteWorkspace, className, title = "General", description = "Change the settings for your current workspace.", learnMoreLabel = "Learn more", learnMoreHref = "#", logoHelpText = "We only support PNGs, JPEGs and GIFs under 10MB", uploadLogoLabel = "Upload logo", nameFieldLabel = "Name", slugFieldLabel = "Slug", copySlugLabel = "Copy slug", exportSectionTitle = "Export workspace data", exportSectionDescription = "Exports are in CSV format and can be downloaded within 7 days", startExportLabel = "Start new export", typeColumnLabel = "Type", dateColumnLabel = "Date", noExportsLabel = "No exports yet", dangerSectionTitle = "Danger zone", deleteWorkspaceTitle = "Delete workspace", deleteWorkspaceWarning = "Once deleted, your workspace cannot be recovered", deleteWorkspaceLabel = "Delete workspace", ...rest }) {
|
|
16
|
+
return (_jsx("div", { className: workspacegeneralsettingsRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-workspacegeneralsettings-page", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-heading", children: [_jsx("h1", { className: "crisp-workspacegeneralsettings-title", children: title }), _jsxs("p", { className: "crisp-workspacegeneralsettings-desc", children: [description, " ", _jsx(TextLink, { href: learnMoreHref, children: learnMoreLabel })] })] }), _jsxs("div", { className: "crisp-workspacegeneralsettings-section", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-logorow", children: [logoSrc ? (_jsx("img", { src: logoSrc, alt: "", width: 48, height: 48, style: { borderRadius: "30%", objectFit: "cover" } })) : (_jsx(Avatar, { size: "xl", shape: "rounded", tone: "brand", children: workspaceInitial })), _jsxs("div", { className: "crisp-workspacegeneralsettings-logotext", children: [_jsx("span", { className: "crisp-workspacegeneralsettings-logohelp", children: logoHelpText }), _jsx(Button, { intent: "neutral", size: "sm", onClick: onUploadLogo, children: uploadLogoLabel })] })] }), _jsxs("div", { className: "crisp-workspacegeneralsettings-fieldrow", children: [_jsx(FormField, { label: nameFieldLabel, children: _jsx(Input, { value: name, onChange: (e) => onNameChange(e.target.value) }) }), _jsx(FormField, { label: slugFieldLabel, children: _jsxs("div", { className: "crisp-workspacegeneralsettings-slugfield", children: [_jsx(Input, { value: slug, readOnly: true }), _jsx("button", { type: "button", className: "crisp-workspacegeneralsettings-copybtn", "aria-label": copySlugLabel, onClick: onCopySlug, children: _jsx(Copy, { size: 16 }) })] }) })] })] }), _jsxs("div", { className: "crisp-workspacegeneralsettings-section", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-sectionhead", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-sectionhead-text", children: [_jsx("h2", { className: "crisp-workspacegeneralsettings-sectiontitle", children: exportSectionTitle }), _jsx("p", { className: "crisp-workspacegeneralsettings-desc", children: exportSectionDescription })] }), _jsx(Button, { intent: "neutral", size: "sm", onClick: onStartExport, children: startExportLabel })] }), _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: typeColumnLabel }), _jsx(TableHead, { children: dateColumnLabel })] }) }), _jsx(TableBody, { children: exportRows.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 2, children: _jsx("div", { className: "crisp-workspacegeneralsettings-emptyrow", children: noExportsLabel }) }) })) : (exportRows.map((row) => (_jsxs(TableRow, { children: [_jsx(TableCell, { children: row.type }), _jsx(TableCell, { children: row.date })] }, row.id)))) })] })] }), _jsxs("div", { className: "crisp-workspacegeneralsettings-section", children: [_jsx("h2", { className: "crisp-workspacegeneralsettings-sectiontitle", children: dangerSectionTitle }), _jsxs("div", { className: "crisp-workspacegeneralsettings-dangerrow", children: [_jsxs("div", { className: "crisp-workspacegeneralsettings-dangertext", children: [_jsx("span", { className: "crisp-workspacegeneralsettings-rowtitle", children: deleteWorkspaceTitle }), _jsx("span", { className: "crisp-workspacegeneralsettings-dangerwarning", children: deleteWorkspaceWarning })] }), _jsx(Button, { intent: "danger", size: "sm", onClick: onDeleteWorkspace, children: deleteWorkspaceLabel })] })] })] }) }));
|
|
17
|
+
}
|
|
18
|
+
WorkspaceGeneralSettings.displayName = "WorkspaceGeneralSettings";
|