@crisp-ui-kit/crisp 0.57.3 → 0.57.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assigneepicker.css +13 -0
- package/dist/assigneepicker.d.ts +7 -0
- package/dist/assigneepicker.layered.css +15 -0
- package/dist/button.css +1 -1
- package/dist/button.layered.css +1 -1
- package/dist/cjs/components/button/button.recipe.js +15 -10
- package/dist/cjs/components/datatable/datatable.js +6 -3
- package/dist/cjs/components/datatable/datatable.recipe.js +35 -2
- package/dist/cjs/components/menu/menu.js +48 -8
- package/dist/cjs/components/multiselect/multiselect.js +3 -0
- package/dist/cjs/components/multiselect/multiselect.recipe.js +18 -0
- package/dist/cjs/components/select/select.js +2 -2
- package/dist/cjs/components/select/select.recipe.js +19 -0
- package/dist/cjs/core/index.js +2 -1
- package/dist/cjs/core/place-floating.js +38 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/product/assigneepicker/assigneepicker.js +127 -0
- package/dist/cjs/product/assigneepicker/assigneepicker.recipe.js +36 -0
- package/dist/cjs/product/assigneepicker/index.js +7 -0
- package/dist/cjs/product/notesgallery/index.js +7 -0
- package/dist/cjs/product/notesgallery/notesgallery.js +80 -0
- package/dist/cjs/product/notesgallery/notesgallery.recipe.js +46 -0
- package/dist/cjs/product/objectpagemenu/index.js +17 -0
- package/dist/cjs/product/objectpagemenu/objectpagemenu.js +26 -0
- package/dist/cjs/product/objectpagemenu/objectpagemenu.recipe.js +30 -0
- package/dist/cjs/product/objectsettings/objectsettings.js +107 -14
- package/dist/cjs/product/recordpage/recordpage.js +64 -5
- package/dist/cjs/product/recordpage/recordpage.recipe.js +38 -0
- package/dist/cjs/product/recordpicker/index.js +7 -0
- package/dist/cjs/product/recordpicker/recordpicker.js +100 -0
- package/dist/cjs/product/recordpicker/recordpicker.recipe.js +36 -0
- package/dist/cjs/product/savedviews/index.js +2 -1
- package/dist/cjs/product/savedviews/savedviews.js +22 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +47 -0
- package/dist/cjs/product/share/index.js +7 -0
- package/dist/cjs/product/share/share.js +179 -0
- package/dist/cjs/product/share/share.recipe.js +114 -0
- package/dist/cjs/product/shortcutssheet/index.js +17 -0
- package/dist/cjs/product/shortcutssheet/shortcutssheet.js +125 -0
- package/dist/cjs/product/shortcutssheet/shortcutssheet.recipe.js +50 -0
- package/dist/cjs/product/taskeditpopover/index.js +7 -0
- package/dist/cjs/product/taskeditpopover/taskeditpopover.js +159 -0
- package/dist/cjs/product/taskeditpopover/taskeditpopover.recipe.js +68 -0
- package/dist/cjs/product/workspaceswitchermenu/index.js +17 -0
- package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.js +38 -0
- package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +34 -0
- package/dist/cjs/tokens/palette.js +27 -0
- package/dist/cjs/tokens/semantic.js +20 -0
- package/dist/datatable.css +40 -7
- package/dist/datatable.layered.css +40 -7
- package/dist/esm/components/button/button.recipe.js +15 -10
- package/dist/esm/components/datatable/datatable.js +6 -3
- package/dist/esm/components/datatable/datatable.recipe.js +35 -2
- package/dist/esm/components/menu/menu.js +47 -7
- package/dist/esm/components/multiselect/multiselect.js +3 -0
- package/dist/esm/components/multiselect/multiselect.recipe.js +18 -0
- package/dist/esm/components/select/select.js +2 -2
- package/dist/esm/components/select/select.recipe.js +19 -0
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/core/place-floating.js +37 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/product/assigneepicker/assigneepicker.js +91 -0
- package/dist/esm/product/assigneepicker/assigneepicker.recipe.js +33 -0
- package/dist/esm/product/assigneepicker/index.js +2 -0
- package/dist/esm/product/notesgallery/index.js +2 -0
- package/dist/esm/product/notesgallery/notesgallery.js +44 -0
- package/dist/esm/product/notesgallery/notesgallery.recipe.js +43 -0
- package/dist/esm/product/objectpagemenu/index.js +1 -0
- package/dist/esm/product/objectpagemenu/objectpagemenu.js +23 -0
- package/dist/esm/product/objectpagemenu/objectpagemenu.recipe.js +27 -0
- package/dist/esm/product/objectsettings/objectsettings.js +106 -13
- package/dist/esm/product/recordpage/recordpage.js +64 -5
- package/dist/esm/product/recordpage/recordpage.recipe.js +38 -0
- package/dist/esm/product/recordpicker/index.js +2 -0
- package/dist/esm/product/recordpicker/recordpicker.js +64 -0
- package/dist/esm/product/recordpicker/recordpicker.recipe.js +33 -0
- package/dist/esm/product/savedviews/index.js +1 -1
- package/dist/esm/product/savedviews/savedviews.js +22 -1
- package/dist/esm/product/savedviews/savedviews.recipe.js +47 -0
- package/dist/esm/product/share/index.js +2 -0
- package/dist/esm/product/share/share.js +143 -0
- package/dist/esm/product/share/share.recipe.js +111 -0
- package/dist/esm/product/shortcutssheet/index.js +1 -0
- package/dist/esm/product/shortcutssheet/shortcutssheet.js +88 -0
- package/dist/esm/product/shortcutssheet/shortcutssheet.recipe.js +47 -0
- package/dist/esm/product/taskeditpopover/index.js +2 -0
- package/dist/esm/product/taskeditpopover/taskeditpopover.js +123 -0
- package/dist/esm/product/taskeditpopover/taskeditpopover.recipe.js +65 -0
- package/dist/esm/product/workspaceswitchermenu/index.js +1 -0
- package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.js +35 -0
- package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +31 -0
- package/dist/esm/tokens/palette.js +27 -0
- package/dist/esm/tokens/semantic.js +20 -0
- package/dist/multiselect.css +18 -0
- package/dist/multiselect.layered.css +18 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +56 -4
- package/dist/notesgallery.css +15 -0
- package/dist/notesgallery.d.ts +7 -0
- package/dist/notesgallery.layered.css +17 -0
- package/dist/objectpagemenu.css +3 -0
- package/dist/objectpagemenu.d.ts +7 -0
- package/dist/objectpagemenu.layered.css +5 -0
- package/dist/recordpage.css +36 -0
- package/dist/recordpage.layered.css +36 -0
- package/dist/recordpicker.css +13 -0
- package/dist/recordpicker.d.ts +7 -0
- package/dist/recordpicker.layered.css +15 -0
- package/dist/savedviews.css +47 -0
- package/dist/savedviews.layered.css +47 -0
- package/dist/select.css +19 -0
- package/dist/select.layered.css +19 -0
- package/dist/share.css +84 -0
- package/dist/share.d.ts +7 -0
- package/dist/share.layered.css +86 -0
- package/dist/shortcutssheet.css +24 -0
- package/dist/shortcutssheet.d.ts +7 -0
- package/dist/shortcutssheet.layered.css +26 -0
- package/dist/styles.css +387 -8
- package/dist/styles.layered.css +387 -8
- package/dist/taskeditpopover.css +36 -0
- package/dist/taskeditpopover.d.ts +7 -0
- package/dist/taskeditpopover.layered.css +38 -0
- package/dist/tokens.css +26 -0
- package/dist/tokens.json +56 -4
- package/dist/types/components/button/button.recipe.d.ts +2 -3
- package/dist/types/components/datatable/datatable.d.ts +8 -0
- package/dist/types/components/menu/menu.d.ts +50 -7
- package/dist/types/components/multiselect/multiselect.d.ts +9 -0
- package/dist/types/components/select/select.d.ts +10 -0
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/core/place-floating.d.ts +37 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/product/assigneepicker/assigneepicker.d.ts +36 -0
- package/dist/types/product/assigneepicker/assigneepicker.recipe.d.ts +3 -0
- package/dist/types/product/assigneepicker/index.d.ts +2 -0
- package/dist/types/product/notesgallery/index.d.ts +2 -0
- package/dist/types/product/notesgallery/notesgallery.d.ts +54 -0
- package/dist/types/product/notesgallery/notesgallery.recipe.d.ts +3 -0
- package/dist/types/product/objectpagemenu/index.d.ts +1 -0
- package/dist/types/product/objectpagemenu/objectpagemenu.d.ts +40 -0
- package/dist/types/product/objectpagemenu/objectpagemenu.recipe.d.ts +17 -0
- package/dist/types/product/objectsettings/objectsettings.d.ts +71 -15
- package/dist/types/product/recordpage/index.d.ts +1 -1
- package/dist/types/product/recordpage/recordpage.d.ts +96 -1
- package/dist/types/product/recordpicker/index.d.ts +2 -0
- package/dist/types/product/recordpicker/recordpicker.d.ts +38 -0
- package/dist/types/product/recordpicker/recordpicker.recipe.d.ts +3 -0
- package/dist/types/product/savedviews/index.d.ts +1 -1
- package/dist/types/product/savedviews/savedviews.d.ts +36 -0
- package/dist/types/product/share/index.d.ts +2 -0
- package/dist/types/product/share/share.d.ts +85 -0
- package/dist/types/product/share/share.recipe.d.ts +3 -0
- package/dist/types/product/shortcutssheet/index.d.ts +1 -0
- package/dist/types/product/shortcutssheet/shortcutssheet.d.ts +49 -0
- package/dist/types/product/shortcutssheet/shortcutssheet.recipe.d.ts +3 -0
- package/dist/types/product/taskeditpopover/index.d.ts +2 -0
- package/dist/types/product/taskeditpopover/taskeditpopover.d.ts +67 -0
- package/dist/types/product/taskeditpopover/taskeditpopover.recipe.d.ts +3 -0
- package/dist/types/product/workspaceswitchermenu/index.d.ts +1 -0
- package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.d.ts +54 -0
- package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.recipe.d.ts +19 -0
- package/dist/types/tokens/contract.d.ts +13 -0
- package/dist/types/tokens/palette.d.ts +52 -0
- package/dist/types/tokens/semantic.d.ts +20 -0
- package/dist/workspaceswitchermenu.css +4 -0
- package/dist/workspaceswitchermenu.d.ts +7 -0
- package/dist/workspaceswitchermenu.layered.css +6 -0
- package/package.json +41 -1
|
@@ -120,6 +120,53 @@ const config = defineRecipe({
|
|
|
120
120
|
font-size: ${c.text.sm}; font-weight: ${c.weight.medium};
|
|
121
121
|
color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent);
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
/* crisp#568: the "Discard changes" / "Save for everyone ▾" bar a
|
|
125
|
+
consumer swaps into its own toolbar's trailing slot (TableToolbar's
|
|
126
|
+
own children prop already documents "right-aligned actions after the
|
|
127
|
+
count") while the loaded view is dirty -- measured live on
|
|
128
|
+
app.attio.com replacing that exact slot's usual Import/Export + New
|
|
129
|
+
button. Not rendered inside SavedViews' own trigger/panel, which
|
|
130
|
+
stays untouched; this is its own export, SavedViewsSaveBar. */
|
|
131
|
+
.crisp-savedviews-savebar { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
132
|
+
/* Rest-state color is a 55% color-mix of fgPrimary -- measured
|
|
133
|
+
identical to recordpanel's own ".crisp-recordpanel-section-action"
|
|
134
|
+
(its own comment: "measured 10px/400, a lighter mix..." -- font size
|
|
135
|
+
there was the SAME outer-vs-inner-div miss #688 corrected; the 55%
|
|
136
|
+
mix itself checks out independently here). Hover repeats crisp's own
|
|
137
|
+
already-measured Button ghost intent exactly (bgRaised + fgPrimary),
|
|
138
|
+
confirmed live (hover bg rgba(0,0,0,.04) + ink text matches bgRaised
|
|
139
|
+
+ fgPrimary bit for bit) -- reused, not re-derived. */
|
|
140
|
+
.crisp-savedviews-savebar-discard {
|
|
141
|
+
box-sizing: border-box; display: inline-flex; align-items: center; height: 28px;
|
|
142
|
+
margin: 0; padding-inline: ${c.space["2"]}; border: none; border-radius: ${c.radius.md};
|
|
143
|
+
background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium};
|
|
144
|
+
color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer;
|
|
145
|
+
transition: background-color .12s ease, color .12s ease;
|
|
146
|
+
}
|
|
147
|
+
.crisp-savedviews-savebar-discard:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
|
|
148
|
+
/* the split "Save for everyone ▾" pill -- two adjacent buttons sharing
|
|
149
|
+
one visual pill (measured: label half rounds only its outer/left
|
|
150
|
+
corners, the chevron half only its outer/right corners, seamless
|
|
151
|
+
between). Fill is crisp's OWN measured bgBrandContrast, not Attio's
|
|
152
|
+
own literal button blue -- the owner's directive is Attio structure/
|
|
153
|
+
sizing, seed-identity color (crisp direction doc). Hover/active reuse
|
|
154
|
+
Button's own already-measured primary-intent brightness filter
|
|
155
|
+
(crisp#910) rather than Attio's own literal darker fill, for the
|
|
156
|
+
same reason. */
|
|
157
|
+
.crisp-savedviews-savebar-save-group { display: flex; }
|
|
158
|
+
.crisp-savedviews-savebar-save, .crisp-savedviews-savebar-save-chevron {
|
|
159
|
+
box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
|
|
160
|
+
height: 28px; margin: 0; border: none; background: ${c.bgBrandContrast}; color: ${c.fgOnBrand};
|
|
161
|
+
cursor: pointer; transition: filter .2s;
|
|
162
|
+
}
|
|
163
|
+
.crisp-savedviews-savebar-save:hover, .crisp-savedviews-savebar-save-chevron:hover { filter: brightness(0.96); }
|
|
164
|
+
.crisp-savedviews-savebar-save:active, .crisp-savedviews-savebar-save-chevron:active { filter: brightness(0.92); }
|
|
165
|
+
.crisp-savedviews-savebar-save {
|
|
166
|
+
padding-inline: ${c.space["2"]} ${c.space["1.5"]}; border-radius: ${c.radius.md} 0 0 ${c.radius.md};
|
|
167
|
+
font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium};
|
|
168
|
+
}
|
|
169
|
+
.crisp-savedviews-savebar-save-chevron { width: 28px; padding: 0; border-radius: 0 ${c.radius.md} ${c.radius.md} 0; }
|
|
123
170
|
`,
|
|
124
171
|
variants: {},
|
|
125
172
|
defaultVariants: {},
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HelpCircle, Play, Users } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Avatar } from "../../components/avatar/index.js";
|
|
5
|
+
import { Input } from "../../components/input/index.js";
|
|
6
|
+
import { Select } from "../../components/select/index.js";
|
|
7
|
+
import { useTablistKeyboard } from "../../components/tabs/index.js";
|
|
8
|
+
import { FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
|
|
9
|
+
import { shareRecipe } from "./share.recipe.js";
|
|
10
|
+
/**
|
|
11
|
+
* A Share/permissions popover — measured live on app.attio.com (both an
|
|
12
|
+
* object table's and a List's own "Share" trigger, techpartners workspace):
|
|
13
|
+
* Members/Automations pill tabs, a "Workspace access" row (avatar + label +
|
|
14
|
+
* a `Read and write ⌄`-style role select), an optional plan-gated upsell
|
|
15
|
+
* slot, a "Clear permissions" action, an "Individual members" list (one row
|
|
16
|
+
* per member, its own role select), and — on the Automations tab — a
|
|
17
|
+
* search box + role select + "Add" row. Both tabs share a trailing "Learn
|
|
18
|
+
* more about permissions" link.
|
|
19
|
+
*
|
|
20
|
+
* Its own floating shell (not the generic `Popover`): the measured surface
|
|
21
|
+
* is 440px wide, past `Popover`'s 360px content cap. See share.recipe.ts.
|
|
22
|
+
*/
|
|
23
|
+
export function SharePopover({ trigger, align = "end", open: openProp, defaultOpen, onOpenChange, tab: tabProp, defaultTab = "members", onTabChange, membersTabLabel: membersTabLabelProp, automationsTabLabel: automationsTabLabelProp, workspaceIcon, workspaceAccessLabel: workspaceAccessLabelProp, workspaceAccessValue, workspaceAccessOptions, onWorkspaceAccessChange, advancedSharing, onClearPermissions, clearPermissionsLabel: clearPermissionsLabelProp, members, membersSectionLabel: membersSectionLabelProp, memberRoleOptions, onMemberAccessChange, automationSearchValue, defaultAutomationSearchValue, onAutomationSearchChange, automationSearchPlaceholder: automationSearchPlaceholderProp, automationRoleValue, automationRoleOptions, onAutomationRoleChange, onAddAutomation, addAutomationDisabled, addAutomationLabel: addAutomationLabelProp, learnMoreHref, learnMoreLabel: learnMoreLabelProp, "aria-label": ariaLabel, className, ...rest }) {
|
|
24
|
+
const membersTabLabel = useCrispMessage(membersTabLabelProp, "Members");
|
|
25
|
+
const automationsTabLabel = useCrispMessage(automationsTabLabelProp, "Automations");
|
|
26
|
+
const workspaceAccessLabel = useCrispMessage(workspaceAccessLabelProp, "Workspace access");
|
|
27
|
+
const clearPermissionsLabel = useCrispMessage(clearPermissionsLabelProp, "Clear permissions");
|
|
28
|
+
const membersSectionLabel = useCrispMessage(membersSectionLabelProp, "Individual members");
|
|
29
|
+
const automationSearchPlaceholder = useCrispMessage(automationSearchPlaceholderProp, "Search...");
|
|
30
|
+
const addAutomationLabel = useCrispMessage(addAutomationLabelProp, "Add");
|
|
31
|
+
const learnMoreLabel = useCrispMessage(learnMoreLabelProp, "Learn more about permissions");
|
|
32
|
+
const surfaceId = React.useId();
|
|
33
|
+
const triggerId = trigger.props.id ?? `${surfaceId}-trigger`;
|
|
34
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
|
|
35
|
+
const open = openProp ?? internalOpen;
|
|
36
|
+
const setOpen = React.useCallback((next) => {
|
|
37
|
+
if (openProp === undefined)
|
|
38
|
+
setInternalOpen(next);
|
|
39
|
+
onOpenChange?.(next);
|
|
40
|
+
}, [openProp, onOpenChange]);
|
|
41
|
+
const [internalTab, setInternalTab] = React.useState(defaultTab);
|
|
42
|
+
const activeTab = tabProp ?? internalTab;
|
|
43
|
+
const selectTab = (t) => {
|
|
44
|
+
if (tabProp === undefined)
|
|
45
|
+
setInternalTab(t);
|
|
46
|
+
onTabChange?.(t);
|
|
47
|
+
};
|
|
48
|
+
const [internalSearch, setInternalSearch] = React.useState(defaultAutomationSearchValue ?? "");
|
|
49
|
+
const searchValue = automationSearchValue ?? internalSearch;
|
|
50
|
+
const setSearch = (q) => {
|
|
51
|
+
if (automationSearchValue === undefined)
|
|
52
|
+
setInternalSearch(q);
|
|
53
|
+
onAutomationSearchChange?.(q);
|
|
54
|
+
};
|
|
55
|
+
const rootRef = React.useRef(null);
|
|
56
|
+
const triggerRef = React.useRef(null);
|
|
57
|
+
const contentRef = React.useRef(null);
|
|
58
|
+
const [pos, setPos] = React.useState({});
|
|
59
|
+
const [up, setUp] = React.useState(false);
|
|
60
|
+
const place = () => {
|
|
61
|
+
const el = triggerRef.current;
|
|
62
|
+
if (!el)
|
|
63
|
+
return;
|
|
64
|
+
const { style, up: flipUp } = placeFloating({
|
|
65
|
+
rect: el.getBoundingClientRect(),
|
|
66
|
+
align,
|
|
67
|
+
gap: 6,
|
|
68
|
+
flipBelow: 320,
|
|
69
|
+
anchor: el,
|
|
70
|
+
});
|
|
71
|
+
setUp(flipUp);
|
|
72
|
+
setPos(style);
|
|
73
|
+
};
|
|
74
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reposition each time it opens
|
|
75
|
+
React.useLayoutEffect(() => {
|
|
76
|
+
if (open)
|
|
77
|
+
place();
|
|
78
|
+
}, [open]);
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
if (!open || !contentRef.current)
|
|
81
|
+
return;
|
|
82
|
+
return registerFloatingLayer(contentRef.current);
|
|
83
|
+
}, [open]);
|
|
84
|
+
const close = React.useCallback(() => {
|
|
85
|
+
setOpen(false);
|
|
86
|
+
}, [setOpen]);
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
if (!open)
|
|
89
|
+
return;
|
|
90
|
+
const onDown = (e) => {
|
|
91
|
+
const target = e.target;
|
|
92
|
+
if (rootRef.current?.contains(target))
|
|
93
|
+
return;
|
|
94
|
+
if (contentRef.current?.contains(target))
|
|
95
|
+
return;
|
|
96
|
+
if (isInsideLaterFloatingLayer(target, contentRef.current))
|
|
97
|
+
return;
|
|
98
|
+
close();
|
|
99
|
+
};
|
|
100
|
+
document.addEventListener("pointerdown", onDown);
|
|
101
|
+
return () => document.removeEventListener("pointerdown", onDown);
|
|
102
|
+
}, [open, close]);
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (!open)
|
|
105
|
+
return;
|
|
106
|
+
const onKey = (e) => {
|
|
107
|
+
if (e.key !== "Escape")
|
|
108
|
+
return;
|
|
109
|
+
if (hasNestedFloatingLayer(contentRef.current))
|
|
110
|
+
return;
|
|
111
|
+
close();
|
|
112
|
+
};
|
|
113
|
+
document.addEventListener("keydown", onKey);
|
|
114
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
115
|
+
}, [open, close]);
|
|
116
|
+
const triggerEl = React.cloneElement(trigger, {
|
|
117
|
+
ref: triggerRef,
|
|
118
|
+
id: triggerId,
|
|
119
|
+
"aria-haspopup": "dialog",
|
|
120
|
+
"aria-expanded": open,
|
|
121
|
+
"aria-controls": open ? surfaceId : undefined,
|
|
122
|
+
onClick: (e) => {
|
|
123
|
+
trigger.props.onClick?.(e);
|
|
124
|
+
setOpen(!open);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const tablistRef = React.useRef(null);
|
|
128
|
+
const tabEntries = React.useMemo(() => [{ value: "members" }, { value: "automations" }], []);
|
|
129
|
+
const { onKeyDown: onTabKeyDown, tabIndexFor } = useTablistKeyboard({
|
|
130
|
+
tabs: tabEntries,
|
|
131
|
+
active: activeTab,
|
|
132
|
+
select: (v) => selectTab(v),
|
|
133
|
+
listRef: tablistRef,
|
|
134
|
+
});
|
|
135
|
+
return (_jsxs("div", { ref: rootRef, className: shareRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: contentRef, className: "crisp-share-content", role: "dialog", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-up": up ? "" : undefined, style: pos, ...rest, children: [_jsxs("div", { ref: tablistRef, className: "crisp-share-tabs", role: "tablist", onKeyDown: onTabKeyDown, children: [_jsxs("button", { type: "button", role: "tab", "data-value": "members", "aria-selected": activeTab === "members", tabIndex: tabIndexFor("members"), className: "crisp-share-tab", onClick: () => selectTab("members"), children: [_jsx(Users, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), membersTabLabel] }), _jsxs("button", { type: "button", role: "tab", "data-value": "automations", "aria-selected": activeTab === "automations", tabIndex: tabIndexFor("automations"), className: "crisp-share-tab", onClick: () => selectTab("automations"), children: [_jsx(Play, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), automationsTabLabel] })] }), activeTab === "members" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "crisp-share-row", children: [workspaceIcon ?? (_jsx(Avatar, { size: "lg", tone: "brand", children: typeof workspaceAccessLabel === "string"
|
|
136
|
+
? workspaceAccessLabel.charAt(0)
|
|
137
|
+
: null })), _jsx("span", { className: "crisp-share-row-label", children: workspaceAccessLabel }), _jsx(Select, { variant: "ghost", width: "auto", options: workspaceAccessOptions, value: workspaceAccessValue, onValueChange: onWorkspaceAccessChange, "aria-label": workspaceAccessLabel })] }), advancedSharing, onClearPermissions && (_jsx("button", { type: "button", className: "crisp-share-clear", onClick: onClearPermissions, children: clearPermissionsLabel })), members && members.length > 0 && (_jsxs("div", { children: [_jsx("p", { className: "crisp-share-section-label", children: membersSectionLabel }), _jsx("div", { className: "crisp-share-members", children: members.map((m) => (_jsxs("div", { className: "crisp-share-member", children: [m.avatar ?? (_jsx(Avatar, { size: "lg", tone: "brand", children: typeof m.name === "string"
|
|
138
|
+
? m.name.charAt(0).toUpperCase()
|
|
139
|
+
: null })), _jsxs("span", { className: "crisp-share-member-text", children: [_jsx("span", { className: "crisp-share-member-name", children: m.name }), m.email && (_jsx("span", { className: "crisp-share-member-email", children: m.email }))] }), _jsx(Select, { variant: "ghost", width: "auto", options: memberRoleOptions, value: m.access, disabled: m.accessDisabled, onValueChange: (v) => onMemberAccessChange?.(m.id, v), "aria-label": typeof m.name === "string"
|
|
140
|
+
? `${m.name}'s access`
|
|
141
|
+
: undefined })] }, m.id))) })] }))] })) : (_jsxs("div", { className: "crisp-share-automations-row", children: [_jsx(Input, { value: searchValue, onChange: (e) => setSearch(e.target.value), placeholder: automationSearchPlaceholder, "aria-label": automationSearchPlaceholder }), _jsx(Select, { variant: "ghost", width: "auto", options: automationRoleOptions, value: automationRoleValue, onValueChange: onAutomationRoleChange, "aria-label": automationsTabLabel }), _jsx("button", { type: "button", className: "crisp-share-add", disabled: addAutomationDisabled, onClick: onAddAutomation, children: addAutomationLabel })] })), _jsxs("a", { className: "crisp-share-learnmore", href: learnMoreHref, target: "_blank", rel: "noreferrer", children: [_jsx(HelpCircle, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), learnMoreLabel] })] }) }))] }));
|
|
142
|
+
}
|
|
143
|
+
SharePopover.displayName = "SharePopover";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
3
|
+
// SharePopover's shell — measured live on app.attio.com (both the object
|
|
4
|
+
// table's and a List's own "Share" trigger, techpartners workspace,
|
|
5
|
+
// 2026-09-27): a 440px-wide floating panel, radius 12 (xl), the same
|
|
6
|
+
// layered popover shadow every other floating surface uses (`e.floating`
|
|
7
|
+
// matched the measured box-shadow's four layers exactly, so no new shadow
|
|
8
|
+
// literal was needed).
|
|
9
|
+
//
|
|
10
|
+
// This is its own floating shell rather than a reuse of the generic
|
|
11
|
+
// `Popover` (components/popover): that recipe caps its content at
|
|
12
|
+
// `min(360px, 92vw)`, and 440px measured wider than that on every sample.
|
|
13
|
+
// `product/savedviews` already sets the precedent for a product surface
|
|
14
|
+
// owning its own portaled shell over the generic one when the measured
|
|
15
|
+
// geometry doesn't fit it.
|
|
16
|
+
//
|
|
17
|
+
// Three muted-text alphas were measured that don't match any existing
|
|
18
|
+
// color token: the workspace-access/role value text (`rgba(0,0,0,.55)`),
|
|
19
|
+
// the "Individual members" section label (`rgba(0,0,0,.5)`), and the
|
|
20
|
+
// inactive-tab/footer-link text (`rgba(0,0,0,.63)` — this one incidentally
|
|
21
|
+
// equals `fgAlertDescription`'s light value, but that token is Dialog's own
|
|
22
|
+
// alert-description colour, a different surface, so it is not reused here).
|
|
23
|
+
// Rather than add new palette entries for three one-off alphas (tokens/**
|
|
24
|
+
// is another lane's file), each is expressed as `color-mix(in srgb,
|
|
25
|
+
// fgPrimary N%, transparent)` — the same idiom `segmented`'s own track
|
|
26
|
+
// background already uses for its 5% tint — so the value is theme-reactive
|
|
27
|
+
// (it scales with fgPrimary's dark value too) without a new token or a
|
|
28
|
+
// flat literal that would go stale in dark mode.
|
|
29
|
+
const mutedText = (pct) => `color-mix(in srgb, ${c.fgPrimary} ${pct}%, transparent)`;
|
|
30
|
+
const config = defineRecipe({
|
|
31
|
+
name: "share",
|
|
32
|
+
base: `display: contents; font-family: ${c.font.sans};`,
|
|
33
|
+
globalCss: `
|
|
34
|
+
.crisp-share-content {
|
|
35
|
+
font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
36
|
+
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box;
|
|
37
|
+
width: 440px; max-width: 92vw;
|
|
38
|
+
padding: ${c.space["3"]}; display: flex; flex-direction: column; gap: ${c.space["3"]};
|
|
39
|
+
border-radius: ${c.radius.xl}; background: ${c.bgPopover}; color: ${c.fgPrimary};
|
|
40
|
+
box-shadow: ${e.floating};
|
|
41
|
+
opacity: 1; transform: none; transform-origin: top right;
|
|
42
|
+
transition: opacity 0.08s ease, transform 0.08s ease;
|
|
43
|
+
@starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); }
|
|
44
|
+
}
|
|
45
|
+
.crisp-share-content[data-up] { transform-origin: bottom right; }
|
|
46
|
+
|
|
47
|
+
.crisp-share-tabs { display: flex; align-items: center; gap: ${c.space["1"]}; }
|
|
48
|
+
.crisp-share-tab {
|
|
49
|
+
box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1.5"]};
|
|
50
|
+
height: ${c.size.row}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md};
|
|
51
|
+
background: transparent; color: ${mutedText(63)};
|
|
52
|
+
font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.07px;
|
|
53
|
+
cursor: pointer; transition: background-color .12s ease, color .12s ease;
|
|
54
|
+
}
|
|
55
|
+
.crisp-share-tab svg { flex-shrink: 0; }
|
|
56
|
+
.crisp-share-tab:hover { color: ${c.fgPrimary}; }
|
|
57
|
+
.crisp-share-tab[aria-selected="true"] { background: ${c.bgSurfaceMuted}; color: ${c.fgPrimary}; }
|
|
58
|
+
|
|
59
|
+
.crisp-share-row { display: flex; align-items: center; gap: ${c.space["2"]}; min-height: 34px; }
|
|
60
|
+
.crisp-share-row-label { flex: 1; min-width: 0; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; letter-spacing: -0.07px; }
|
|
61
|
+
.crisp-share-row .crisp-select { flex-shrink: 0; }
|
|
62
|
+
.crisp-share-row .crisp-select .crisp-select-value,
|
|
63
|
+
.crisp-share-row .crisp-select .crisp-select-chevron { color: ${mutedText(55)}; }
|
|
64
|
+
|
|
65
|
+
.crisp-share-clear {
|
|
66
|
+
align-self: flex-start; margin: 0; padding: 0; border: none; background: none;
|
|
67
|
+
font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium};
|
|
68
|
+
color: ${mutedText(63)}; cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
.crisp-share-clear:hover { color: ${c.fgPrimary}; text-decoration: underline; }
|
|
71
|
+
|
|
72
|
+
.crisp-share-section-label {
|
|
73
|
+
margin: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${mutedText(50)};
|
|
74
|
+
}
|
|
75
|
+
.crisp-share-members { display: flex; flex-direction: column; gap: ${c.space["2"]}; }
|
|
76
|
+
.crisp-share-member { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
77
|
+
.crisp-share-member-text { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: ${c.space["1.5"]}; overflow: hidden; }
|
|
78
|
+
.crisp-share-member-name { flex-shrink: 0; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; letter-spacing: -0.07px; }
|
|
79
|
+
.crisp-share-member-email { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${mutedText(55)}; letter-spacing: -0.07px; }
|
|
80
|
+
.crisp-share-member .crisp-select { flex-shrink: 0; }
|
|
81
|
+
.crisp-share-member .crisp-select .crisp-select-value,
|
|
82
|
+
.crisp-share-member .crisp-select .crisp-select-chevron { color: ${mutedText(55)}; }
|
|
83
|
+
|
|
84
|
+
.crisp-share-automations-row { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
85
|
+
.crisp-share-automations-row .crisp-input { flex: 1; min-width: 0; }
|
|
86
|
+
.crisp-share-automations-row .crisp-select { flex-shrink: 0; }
|
|
87
|
+
|
|
88
|
+
.crisp-share-add {
|
|
89
|
+
flex-shrink: 0; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
|
|
90
|
+
height: 32px; padding: 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.lg};
|
|
91
|
+
background: ${c.bgBrandSolid}; color: ${c.fgOnBrand};
|
|
92
|
+
font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.07px;
|
|
93
|
+
cursor: pointer; transition: background-color .12s ease, opacity .12s ease;
|
|
94
|
+
}
|
|
95
|
+
.crisp-share-add:hover:not(:disabled) { background: ${c.bgBrandSolidPressed}; }
|
|
96
|
+
.crisp-share-add:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
97
|
+
|
|
98
|
+
.crisp-share-learnmore {
|
|
99
|
+
display: inline-flex; align-items: center; gap: ${c.space["1.5"]};
|
|
100
|
+
font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${mutedText(63)};
|
|
101
|
+
text-decoration: none;
|
|
102
|
+
}
|
|
103
|
+
.crisp-share-learnmore:hover { color: ${c.fgPrimary}; }
|
|
104
|
+
.crisp-share-learnmore svg { flex-shrink: 0; }
|
|
105
|
+
`,
|
|
106
|
+
variants: {},
|
|
107
|
+
defaultVariants: {},
|
|
108
|
+
compoundVariants: [],
|
|
109
|
+
});
|
|
110
|
+
export const shareRecipe = resolveRecipe(config);
|
|
111
|
+
export default config;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./shortcutssheet.js";
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CornerDownLeft } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Drawer, DrawerBody } from "../../components/drawer/index.js";
|
|
5
|
+
import { Kbd } from "../../components/kbd/index.js";
|
|
6
|
+
import { cx, useCrispMessage } from "../../core/index.js";
|
|
7
|
+
/** The physical Enter/Return key's printed glyph — not a reading-direction
|
|
8
|
+
* indicator (a real keyboard prints the same ↵ shape under any locale), so
|
|
9
|
+
* unlike `LogOut` elsewhere it is deliberately left unmirrored (excused in
|
|
10
|
+
* `build/spec-check.mjs`'s own RTL icon-mirroring ALLOW list). */
|
|
11
|
+
const enterKey = () => _jsx(CornerDownLeft, { size: 16, strokeWidth: 1.75 });
|
|
12
|
+
/**
|
|
13
|
+
* Attio's own canonical keyboard-shortcut list (crisp#615), live-measured
|
|
14
|
+
* off app.attio.com/techpartners' "Keyboard shortcuts" panel (Help →
|
|
15
|
+
* "Shortcuts") — 34 rows across 4 sections. The issue's own quoted table
|
|
16
|
+
* collapsed a few of these ("Navigate to the cell above/right/below/left",
|
|
17
|
+
* "Go to next/previous cell") into one row each; the live panel renders
|
|
18
|
+
* them as separate rows, which is what this list follows.
|
|
19
|
+
*/
|
|
20
|
+
export const defaultShortcutSections = [
|
|
21
|
+
{
|
|
22
|
+
label: "General",
|
|
23
|
+
rows: [
|
|
24
|
+
{ description: "Open quick actions", steps: [["⌘", "K"]] },
|
|
25
|
+
{ description: "Open search", steps: [["/"]] },
|
|
26
|
+
{ description: "Create a new note", steps: [["N"]] },
|
|
27
|
+
{ description: "Create a new entry", steps: [["E"]] },
|
|
28
|
+
{ description: "Create a new task", steps: [["T"]] },
|
|
29
|
+
{ description: "Create a new email", steps: [["C"]] },
|
|
30
|
+
{ description: "Go to home page", steps: [["G"], ["H"]] },
|
|
31
|
+
{ description: "Go to tasks page", steps: [["G"], ["T"]] },
|
|
32
|
+
{ description: "Go to all notes page", steps: [["G"], ["N"]] },
|
|
33
|
+
{ description: "Go to all people page", steps: [["G"], ["P"]] },
|
|
34
|
+
{ description: "Go to all companies page", steps: [["G"], ["C"]] },
|
|
35
|
+
{ description: "Go to email drafts page", steps: [["G"], ["E"]] },
|
|
36
|
+
{ description: "Toggle sidebar", steps: [["⌘", "."]] },
|
|
37
|
+
{ description: "Navigate up", steps: [["↑"]] },
|
|
38
|
+
{ description: "Navigate down", steps: [["↓"]] },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: "Record page",
|
|
43
|
+
rows: [
|
|
44
|
+
{ description: "Go to next tab", steps: [["⌥", "→"]] },
|
|
45
|
+
{ description: "Go to previous tab", steps: [["⌥", "←"]] },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "Table view",
|
|
50
|
+
rows: [
|
|
51
|
+
{ description: "Navigate to the cell above", steps: [["↑"]] },
|
|
52
|
+
{ description: "Navigate to the cell to the right", steps: [["→"]] },
|
|
53
|
+
{ description: "Navigate to the cell below", steps: [["↓"]] },
|
|
54
|
+
{ description: "Navigate to the cell to the left", steps: [["←"]] },
|
|
55
|
+
{ description: "Jump to bottom", steps: [["⌘", "↓"]] },
|
|
56
|
+
{ description: "Jump to top", steps: [["⌘", "↑"]] },
|
|
57
|
+
{ description: "Select/deselect current row", steps: [["⇧", "␣"]] },
|
|
58
|
+
{ description: "Group select rows up", steps: [["⇧", "↑"]] },
|
|
59
|
+
{ description: "Group select rows down", steps: [["⇧", "↓"]] },
|
|
60
|
+
{ description: "Group select to top", steps: [["⌘", "⇧", "↑"]] },
|
|
61
|
+
{ description: "Group select to bottom", steps: [["⌘", "⇧", "↓"]] },
|
|
62
|
+
{
|
|
63
|
+
description: "Open record page for current entry",
|
|
64
|
+
steps: [["⌘", enterKey()]],
|
|
65
|
+
},
|
|
66
|
+
{ description: "Select/deselect all rows", steps: [["⌘", "A"]] },
|
|
67
|
+
{ description: "Go to next cell", steps: [["⇥"]] },
|
|
68
|
+
{ description: "Go to previous cell", steps: [["⇧", "⇥"]] },
|
|
69
|
+
{ description: "Edit cell / go to next cell", steps: [[enterKey()]] },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
label: "Help",
|
|
74
|
+
rows: [{ description: "Open help drawer", steps: [["?"]] }],
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
/**
|
|
78
|
+
* Attio's "Keyboard shortcuts" panel (crisp#615, parity row P-106) — a
|
|
79
|
+
* `Drawer` narrowed to the measured 360px (Drawer's own default is 420px),
|
|
80
|
+
* listing `sections` grouped with a hairline-free 10px row rhythm and a
|
|
81
|
+
* 20px gap before each new section label.
|
|
82
|
+
*/
|
|
83
|
+
export function ShortcutsSheet({ open, onOpenChange, sections = defaultShortcutSections, title: titleProp, thenLabel: thenLabelProp, closeLabel, className, ...rest }) {
|
|
84
|
+
const title = useCrispMessage(titleProp, "Keyboard shortcuts");
|
|
85
|
+
const thenLabel = useCrispMessage(thenLabelProp, "then");
|
|
86
|
+
return (_jsx(Drawer, { open: open, onOpenChange: onOpenChange, title: title, closeLabel: closeLabel, className: cx("crisp-shortcutssheet-panel", className), ...rest, children: _jsx(DrawerBody, { children: _jsx("div", { className: "crisp-shortcutssheet-body", children: sections.map((section) => (_jsxs(React.Fragment, { children: [_jsx("div", { className: "crisp-shortcutssheet-section-label", children: section.label }), section.rows.map((row) => (_jsxs("div", { className: "crisp-shortcutssheet-row", children: [_jsx("div", { className: "crisp-shortcutssheet-desc", children: row.description }), _jsx("div", { className: "crisp-shortcutssheet-keys", children: row.steps.map((step, stepIndex) => (_jsxs(React.Fragment, { children: [stepIndex > 0 && (_jsx("span", { className: "crisp-shortcutssheet-then", children: thenLabel })), step.map((glyph, glyphIndex) => (_jsx(Kbd, { children: glyph }, glyphIndex)))] }, stepIndex))) })] }, String(row.description))))] }, String(section.label)))) }) }) }));
|
|
87
|
+
}
|
|
88
|
+
ShortcutsSheet.displayName = "ShortcutsSheet";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// Attio's "Keyboard shortcuts" panel (crisp#615), live-measured off
|
|
4
|
+
// app.attio.com/techpartners — a second overlay reached from Help →
|
|
5
|
+
// "Shortcuts": 360px wide (narrower than Drawer's own 420px default, hence
|
|
6
|
+
// the width override below), 16px body padding (already Drawer's own
|
|
7
|
+
// `crisp-drawer-body`, unchanged), rows 10px apart, a 20px gap before a new
|
|
8
|
+
// section (expressed as the flex column's own 10px `gap` plus another 10px
|
|
9
|
+
// on every section label but the first, rather than a second gap value),
|
|
10
|
+
// section labels 12px/500, row descriptions 14px/500 — both the SAME
|
|
11
|
+
// `color-mix(fgPrimary, 63%)` muted-text tint RecordPage's own notes already
|
|
12
|
+
// established (crisp#871), and the "then" connector between a sequential
|
|
13
|
+
// shortcut's two steps at 11px/500 in Kbd's own 50%-fgPrimary tint (the
|
|
14
|
+
// same de-emphasis, on the same kind of supporting glyph).
|
|
15
|
+
const config = defineRecipe({
|
|
16
|
+
name: "shortcutssheet",
|
|
17
|
+
base: "",
|
|
18
|
+
globalCss: `
|
|
19
|
+
/* Overrides Drawer's own 420px default (specificity: .crisp-drawer
|
|
20
|
+
.crisp-drawer-content.crisp-shortcutssheet-panel, higher than
|
|
21
|
+
Drawer's .crisp-drawer .crisp-drawer-content regardless of which
|
|
22
|
+
chunk of dist/styles.css concatenates first). */
|
|
23
|
+
.crisp-drawer .crisp-drawer-content.crisp-shortcutssheet-panel { width: 360px; }
|
|
24
|
+
|
|
25
|
+
.crisp-shortcutssheet-body { display: flex; flex-direction: column; gap: ${c.space["2.5"]}; }
|
|
26
|
+
.crisp-shortcutssheet-section-label {
|
|
27
|
+
font-size: ${c.text.sm}; font-weight: ${c.weight.medium};
|
|
28
|
+
color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent);
|
|
29
|
+
}
|
|
30
|
+
.crisp-shortcutssheet-section-label:not(:first-child) { margin-top: ${c.space["2.5"]}; }
|
|
31
|
+
.crisp-shortcutssheet-row { display: flex; align-items: center; justify-content: space-between; gap: ${c.space["1"]}; }
|
|
32
|
+
.crisp-shortcutssheet-desc {
|
|
33
|
+
font-size: ${c.text.lg}; font-weight: ${c.weight.medium};
|
|
34
|
+
color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent);
|
|
35
|
+
}
|
|
36
|
+
.crisp-shortcutssheet-keys { display: flex; align-items: center; gap: ${c.space["1"]}; flex-shrink: 0; }
|
|
37
|
+
.crisp-shortcutssheet-then {
|
|
38
|
+
font-size: ${c.text.xs}; font-weight: ${c.weight.medium};
|
|
39
|
+
color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent);
|
|
40
|
+
}
|
|
41
|
+
`,
|
|
42
|
+
variants: {},
|
|
43
|
+
defaultVariants: {},
|
|
44
|
+
compoundVariants: [],
|
|
45
|
+
});
|
|
46
|
+
export const shortcutssheetRecipe = resolveRecipe(config);
|
|
47
|
+
export default config;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CalendarDays, CircleCheckBig, MoreHorizontal, X } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Popover } from "../../components/popover/index.js";
|
|
5
|
+
import { FloatingPortal, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
|
|
6
|
+
import { AssigneePicker, } from "../assigneepicker/assigneepicker.js";
|
|
7
|
+
import { RecordPicker, } from "../recordpicker/recordpicker.js";
|
|
8
|
+
import { taskeditpopoverRecipe } from "./taskeditpopover.recipe.js";
|
|
9
|
+
/**
|
|
10
|
+
* A task's "Edit task" popover -- measured live on app.attio.com's Tasks
|
|
11
|
+
* page: a checkbox + editable title, then a chip row (due date / assigned /
|
|
12
|
+
* add record), a "more actions" kebab, and Cancel/Save. Composes
|
|
13
|
+
* `AssigneePicker` and `RecordPicker` (both crisp#626) behind their own
|
|
14
|
+
* chips, using the same core floating primitives `Popover` itself is built
|
|
15
|
+
* on rather than `Popover` directly (its content hardcodes a 360px max
|
|
16
|
+
* width, well under this popover's own measured 800px).
|
|
17
|
+
*/
|
|
18
|
+
export function TaskEditPopover({ trigger, open: openProp, defaultOpen, onOpenChange, title, onTitleChange, done = false, onToggleDone, dueDateLabel, dueDatePicker, assignee = null, users = [], onAssigneeChange, linkedRecordCount = 0, recordCandidates = [], recordCandidatesLoading, onRecordQueryChange, onSelectRecord, menu, onCancel, onSave, headingLabel: headingLabelProp, closeLabel: closeLabelProp, dueDatePlaceholder: dueDatePlaceholderProp, assignLabel: assignLabelProp, assignedToLabel: assignedToLabelProp, addRecordLabel: addRecordLabelProp, linkedRecordLabel: linkedRecordLabelProp, moreActionsLabel: moreActionsLabelProp, cancelLabel: cancelLabelProp, cancelKeyLabel: cancelKeyLabelProp, saveLabel: saveLabelProp, className, }) {
|
|
19
|
+
const headingLabel = useCrispMessage(headingLabelProp, "Edit task");
|
|
20
|
+
const closeLabel = useCrispMessage(closeLabelProp, "Close");
|
|
21
|
+
const dueDatePlaceholder = useCrispMessage(dueDatePlaceholderProp, "Set due date");
|
|
22
|
+
const assignLabel = useCrispMessage(assignLabelProp, "Assign");
|
|
23
|
+
const assignedToLabel = useCrispMessage(assignedToLabelProp, "Assigned to");
|
|
24
|
+
const addRecordLabel = useCrispMessage(addRecordLabelProp, "Add record");
|
|
25
|
+
const linkedRecordLabel = useCrispMessage(linkedRecordLabelProp, linkedRecordCount === 1
|
|
26
|
+
? "1 linked record"
|
|
27
|
+
: `${linkedRecordCount} linked records`);
|
|
28
|
+
const moreActionsLabel = useCrispMessage(moreActionsLabelProp, "More actions");
|
|
29
|
+
const cancelLabel = useCrispMessage(cancelLabelProp, "Cancel");
|
|
30
|
+
const cancelKeyLabel = useCrispMessage(cancelKeyLabelProp, "Esc");
|
|
31
|
+
const saveLabel = useCrispMessage(saveLabelProp, "Save");
|
|
32
|
+
const surfaceId = React.useId();
|
|
33
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
|
|
34
|
+
const open = openProp ?? internalOpen;
|
|
35
|
+
const rootRef = React.useRef(null);
|
|
36
|
+
const triggerRef = React.useRef(null);
|
|
37
|
+
const contentRef = React.useRef(null);
|
|
38
|
+
const [pos, setPos] = React.useState({});
|
|
39
|
+
const setOpen = React.useCallback((next) => {
|
|
40
|
+
if (openProp === undefined)
|
|
41
|
+
setInternalOpen(next);
|
|
42
|
+
onOpenChange?.(next);
|
|
43
|
+
}, [openProp, onOpenChange]);
|
|
44
|
+
const place = () => {
|
|
45
|
+
const el = triggerRef.current;
|
|
46
|
+
if (!el)
|
|
47
|
+
return;
|
|
48
|
+
const { style } = placeFloating({
|
|
49
|
+
rect: el.getBoundingClientRect(),
|
|
50
|
+
align: "start",
|
|
51
|
+
gap: 6,
|
|
52
|
+
flipBelow: 300,
|
|
53
|
+
anchor: el,
|
|
54
|
+
});
|
|
55
|
+
setPos(style);
|
|
56
|
+
};
|
|
57
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reposition each time it opens
|
|
58
|
+
React.useLayoutEffect(() => {
|
|
59
|
+
if (open)
|
|
60
|
+
place();
|
|
61
|
+
}, [open]);
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
if (!open || !contentRef.current)
|
|
64
|
+
return;
|
|
65
|
+
return registerFloatingLayer(contentRef.current);
|
|
66
|
+
}, [open]);
|
|
67
|
+
React.useEffect(() => {
|
|
68
|
+
if (!open)
|
|
69
|
+
return;
|
|
70
|
+
const onDown = (e) => {
|
|
71
|
+
const target = e.target;
|
|
72
|
+
if (rootRef.current?.contains(target))
|
|
73
|
+
return;
|
|
74
|
+
if (contentRef.current?.contains(target))
|
|
75
|
+
return;
|
|
76
|
+
if (isInsideLaterFloatingLayer(target, contentRef.current))
|
|
77
|
+
return;
|
|
78
|
+
setOpen(false);
|
|
79
|
+
onCancel?.();
|
|
80
|
+
};
|
|
81
|
+
document.addEventListener("pointerdown", onDown);
|
|
82
|
+
return () => document.removeEventListener("pointerdown", onDown);
|
|
83
|
+
}, [open, setOpen, onCancel]);
|
|
84
|
+
const cancel = () => {
|
|
85
|
+
setOpen(false);
|
|
86
|
+
onCancel?.();
|
|
87
|
+
};
|
|
88
|
+
const save = () => {
|
|
89
|
+
setOpen(false);
|
|
90
|
+
onSave?.();
|
|
91
|
+
};
|
|
92
|
+
const triggerEl = React.cloneElement(trigger, {
|
|
93
|
+
ref: triggerRef,
|
|
94
|
+
"aria-haspopup": "dialog",
|
|
95
|
+
"aria-expanded": open,
|
|
96
|
+
"aria-controls": open ? surfaceId : undefined,
|
|
97
|
+
onClick: (e) => {
|
|
98
|
+
trigger.props.onClick?.(e);
|
|
99
|
+
setOpen(!open);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
return (_jsxs("div", { ref: rootRef, style: { display: "contents" }, children: [triggerEl, open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: contentRef, id: surfaceId, role: "dialog", "aria-label": headingLabel, className: taskeditpopoverRecipe({ className }), style: pos, onKeyDown: (e) => {
|
|
103
|
+
if (e.key === "Escape") {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
cancel();
|
|
106
|
+
}
|
|
107
|
+
if (e.key === "Enter" &&
|
|
108
|
+
e.metaKey === false &&
|
|
109
|
+
e.ctrlKey === false &&
|
|
110
|
+
e.target.tagName !== "TEXTAREA") {
|
|
111
|
+
// Enter saves, same as Attio's own "Save ⏎" -- but never
|
|
112
|
+
// steals it from a nested picker's own Enter-to-commit.
|
|
113
|
+
if (!isInsideLaterFloatingLayer(e.target, contentRef.current)) {
|
|
114
|
+
save();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}, children: [_jsxs("div", { className: "crisp-taskeditpopover-header", children: [_jsxs("span", { className: "crisp-taskeditpopover-heading", children: [_jsx(CircleCheckBig, { size: 16, strokeWidth: 1.75 }), headingLabel] }), _jsx("button", { type: "button", className: "crisp-taskeditpopover-close", "aria-label": closeLabel, onClick: cancel, children: _jsx(X, { size: 16, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-taskeditpopover-body", children: [_jsx("input", { type: "checkbox", className: "crisp-taskeditpopover-check", checked: done, "aria-label": title, onChange: (e) => onToggleDone?.(e.target.checked) }), _jsx("input", { type: "text", className: "crisp-taskeditpopover-title", value: title, onChange: (e) => onTitleChange?.(e.target.value) })] }), _jsxs("div", { className: "crisp-taskeditpopover-footer", children: [_jsxs("div", { className: "crisp-taskeditpopover-chips", children: [_jsx(Popover, { align: "start", "aria-label": dueDatePlaceholder, trigger: _jsxs("button", { type: "button", className: "crisp-taskeditpopover-chip", children: [_jsx(CalendarDays, { size: 14, strokeWidth: 1.75 }), dueDateLabel ?? dueDatePlaceholder] }), children: dueDatePicker }), _jsx(Popover, { align: "start", "aria-label": assignedToLabel, trigger: _jsxs("button", { type: "button", className: "crisp-taskeditpopover-chip", children: [_jsx("span", { "aria-hidden": "true", children: "@" }), assignee != null
|
|
118
|
+
? `${assignedToLabel} ${assignee.name}`
|
|
119
|
+
: assignLabel] }), children: _jsx(AssigneePicker, { users: users, assignee: assignee, onAssigneeChange: onAssigneeChange }) }), _jsx(Popover, { align: "start", "aria-label": addRecordLabel, trigger: _jsxs("button", { type: "button", className: "crisp-taskeditpopover-chip", children: [_jsx("span", { "aria-hidden": "true", children: "\u2197" }), linkedRecordCount > 0
|
|
120
|
+
? linkedRecordLabel
|
|
121
|
+
: addRecordLabel] }), children: _jsx(RecordPicker, { records: recordCandidates, loading: recordCandidatesLoading, onQueryChange: onRecordQueryChange, onSelect: onSelectRecord }) })] }), _jsxs("div", { className: "crisp-taskeditpopover-actions", children: [menu != null && (_jsx(Popover, { align: "end", "aria-label": moreActionsLabel, trigger: _jsx("button", { type: "button", className: "crisp-taskeditpopover-kebab", "aria-label": moreActionsLabel, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: menu })), _jsxs("button", { type: "button", className: "crisp-taskeditpopover-cancel", onClick: cancel, children: [cancelLabel, _jsx("kbd", { className: "crisp-taskeditpopover-key", children: cancelKeyLabel })] }), _jsxs("button", { type: "button", className: "crisp-taskeditpopover-save", onClick: save, children: [saveLabel, _jsx("kbd", { className: "crisp-taskeditpopover-key", children: "\u23CE" })] })] })] })] }) }))] }));
|
|
122
|
+
}
|
|
123
|
+
TaskEditPopover.displayName = "TaskEditPopover";
|