@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.
Files changed (169) hide show
  1. package/dist/assigneepicker.css +13 -0
  2. package/dist/assigneepicker.d.ts +7 -0
  3. package/dist/assigneepicker.layered.css +15 -0
  4. package/dist/button.css +1 -1
  5. package/dist/button.layered.css +1 -1
  6. package/dist/cjs/components/button/button.recipe.js +15 -10
  7. package/dist/cjs/components/datatable/datatable.js +6 -3
  8. package/dist/cjs/components/datatable/datatable.recipe.js +35 -2
  9. package/dist/cjs/components/menu/menu.js +48 -8
  10. package/dist/cjs/components/multiselect/multiselect.js +3 -0
  11. package/dist/cjs/components/multiselect/multiselect.recipe.js +18 -0
  12. package/dist/cjs/components/select/select.js +2 -2
  13. package/dist/cjs/components/select/select.recipe.js +19 -0
  14. package/dist/cjs/core/index.js +2 -1
  15. package/dist/cjs/core/place-floating.js +38 -0
  16. package/dist/cjs/index.js +8 -0
  17. package/dist/cjs/product/assigneepicker/assigneepicker.js +127 -0
  18. package/dist/cjs/product/assigneepicker/assigneepicker.recipe.js +36 -0
  19. package/dist/cjs/product/assigneepicker/index.js +7 -0
  20. package/dist/cjs/product/notesgallery/index.js +7 -0
  21. package/dist/cjs/product/notesgallery/notesgallery.js +80 -0
  22. package/dist/cjs/product/notesgallery/notesgallery.recipe.js +46 -0
  23. package/dist/cjs/product/objectpagemenu/index.js +17 -0
  24. package/dist/cjs/product/objectpagemenu/objectpagemenu.js +26 -0
  25. package/dist/cjs/product/objectpagemenu/objectpagemenu.recipe.js +30 -0
  26. package/dist/cjs/product/objectsettings/objectsettings.js +107 -14
  27. package/dist/cjs/product/recordpage/recordpage.js +64 -5
  28. package/dist/cjs/product/recordpage/recordpage.recipe.js +38 -0
  29. package/dist/cjs/product/recordpicker/index.js +7 -0
  30. package/dist/cjs/product/recordpicker/recordpicker.js +100 -0
  31. package/dist/cjs/product/recordpicker/recordpicker.recipe.js +36 -0
  32. package/dist/cjs/product/savedviews/index.js +2 -1
  33. package/dist/cjs/product/savedviews/savedviews.js +22 -0
  34. package/dist/cjs/product/savedviews/savedviews.recipe.js +47 -0
  35. package/dist/cjs/product/share/index.js +7 -0
  36. package/dist/cjs/product/share/share.js +179 -0
  37. package/dist/cjs/product/share/share.recipe.js +114 -0
  38. package/dist/cjs/product/shortcutssheet/index.js +17 -0
  39. package/dist/cjs/product/shortcutssheet/shortcutssheet.js +125 -0
  40. package/dist/cjs/product/shortcutssheet/shortcutssheet.recipe.js +50 -0
  41. package/dist/cjs/product/taskeditpopover/index.js +7 -0
  42. package/dist/cjs/product/taskeditpopover/taskeditpopover.js +159 -0
  43. package/dist/cjs/product/taskeditpopover/taskeditpopover.recipe.js +68 -0
  44. package/dist/cjs/product/workspaceswitchermenu/index.js +17 -0
  45. package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.js +38 -0
  46. package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +34 -0
  47. package/dist/cjs/tokens/palette.js +27 -0
  48. package/dist/cjs/tokens/semantic.js +20 -0
  49. package/dist/datatable.css +40 -7
  50. package/dist/datatable.layered.css +40 -7
  51. package/dist/esm/components/button/button.recipe.js +15 -10
  52. package/dist/esm/components/datatable/datatable.js +6 -3
  53. package/dist/esm/components/datatable/datatable.recipe.js +35 -2
  54. package/dist/esm/components/menu/menu.js +47 -7
  55. package/dist/esm/components/multiselect/multiselect.js +3 -0
  56. package/dist/esm/components/multiselect/multiselect.recipe.js +18 -0
  57. package/dist/esm/components/select/select.js +2 -2
  58. package/dist/esm/components/select/select.recipe.js +19 -0
  59. package/dist/esm/core/index.js +1 -1
  60. package/dist/esm/core/place-floating.js +37 -0
  61. package/dist/esm/index.js +8 -0
  62. package/dist/esm/product/assigneepicker/assigneepicker.js +91 -0
  63. package/dist/esm/product/assigneepicker/assigneepicker.recipe.js +33 -0
  64. package/dist/esm/product/assigneepicker/index.js +2 -0
  65. package/dist/esm/product/notesgallery/index.js +2 -0
  66. package/dist/esm/product/notesgallery/notesgallery.js +44 -0
  67. package/dist/esm/product/notesgallery/notesgallery.recipe.js +43 -0
  68. package/dist/esm/product/objectpagemenu/index.js +1 -0
  69. package/dist/esm/product/objectpagemenu/objectpagemenu.js +23 -0
  70. package/dist/esm/product/objectpagemenu/objectpagemenu.recipe.js +27 -0
  71. package/dist/esm/product/objectsettings/objectsettings.js +106 -13
  72. package/dist/esm/product/recordpage/recordpage.js +64 -5
  73. package/dist/esm/product/recordpage/recordpage.recipe.js +38 -0
  74. package/dist/esm/product/recordpicker/index.js +2 -0
  75. package/dist/esm/product/recordpicker/recordpicker.js +64 -0
  76. package/dist/esm/product/recordpicker/recordpicker.recipe.js +33 -0
  77. package/dist/esm/product/savedviews/index.js +1 -1
  78. package/dist/esm/product/savedviews/savedviews.js +22 -1
  79. package/dist/esm/product/savedviews/savedviews.recipe.js +47 -0
  80. package/dist/esm/product/share/index.js +2 -0
  81. package/dist/esm/product/share/share.js +143 -0
  82. package/dist/esm/product/share/share.recipe.js +111 -0
  83. package/dist/esm/product/shortcutssheet/index.js +1 -0
  84. package/dist/esm/product/shortcutssheet/shortcutssheet.js +88 -0
  85. package/dist/esm/product/shortcutssheet/shortcutssheet.recipe.js +47 -0
  86. package/dist/esm/product/taskeditpopover/index.js +2 -0
  87. package/dist/esm/product/taskeditpopover/taskeditpopover.js +123 -0
  88. package/dist/esm/product/taskeditpopover/taskeditpopover.recipe.js +65 -0
  89. package/dist/esm/product/workspaceswitchermenu/index.js +1 -0
  90. package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.js +35 -0
  91. package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +31 -0
  92. package/dist/esm/tokens/palette.js +27 -0
  93. package/dist/esm/tokens/semantic.js +20 -0
  94. package/dist/multiselect.css +18 -0
  95. package/dist/multiselect.layered.css +18 -0
  96. package/dist/native/tokens.d.ts +1 -1
  97. package/dist/native/tokens.js +56 -4
  98. package/dist/notesgallery.css +15 -0
  99. package/dist/notesgallery.d.ts +7 -0
  100. package/dist/notesgallery.layered.css +17 -0
  101. package/dist/objectpagemenu.css +3 -0
  102. package/dist/objectpagemenu.d.ts +7 -0
  103. package/dist/objectpagemenu.layered.css +5 -0
  104. package/dist/recordpage.css +36 -0
  105. package/dist/recordpage.layered.css +36 -0
  106. package/dist/recordpicker.css +13 -0
  107. package/dist/recordpicker.d.ts +7 -0
  108. package/dist/recordpicker.layered.css +15 -0
  109. package/dist/savedviews.css +47 -0
  110. package/dist/savedviews.layered.css +47 -0
  111. package/dist/select.css +19 -0
  112. package/dist/select.layered.css +19 -0
  113. package/dist/share.css +84 -0
  114. package/dist/share.d.ts +7 -0
  115. package/dist/share.layered.css +86 -0
  116. package/dist/shortcutssheet.css +24 -0
  117. package/dist/shortcutssheet.d.ts +7 -0
  118. package/dist/shortcutssheet.layered.css +26 -0
  119. package/dist/styles.css +387 -8
  120. package/dist/styles.layered.css +387 -8
  121. package/dist/taskeditpopover.css +36 -0
  122. package/dist/taskeditpopover.d.ts +7 -0
  123. package/dist/taskeditpopover.layered.css +38 -0
  124. package/dist/tokens.css +26 -0
  125. package/dist/tokens.json +56 -4
  126. package/dist/types/components/button/button.recipe.d.ts +2 -3
  127. package/dist/types/components/datatable/datatable.d.ts +8 -0
  128. package/dist/types/components/menu/menu.d.ts +50 -7
  129. package/dist/types/components/multiselect/multiselect.d.ts +9 -0
  130. package/dist/types/components/select/select.d.ts +10 -0
  131. package/dist/types/core/index.d.ts +1 -1
  132. package/dist/types/core/place-floating.d.ts +37 -0
  133. package/dist/types/index.d.ts +8 -0
  134. package/dist/types/product/assigneepicker/assigneepicker.d.ts +36 -0
  135. package/dist/types/product/assigneepicker/assigneepicker.recipe.d.ts +3 -0
  136. package/dist/types/product/assigneepicker/index.d.ts +2 -0
  137. package/dist/types/product/notesgallery/index.d.ts +2 -0
  138. package/dist/types/product/notesgallery/notesgallery.d.ts +54 -0
  139. package/dist/types/product/notesgallery/notesgallery.recipe.d.ts +3 -0
  140. package/dist/types/product/objectpagemenu/index.d.ts +1 -0
  141. package/dist/types/product/objectpagemenu/objectpagemenu.d.ts +40 -0
  142. package/dist/types/product/objectpagemenu/objectpagemenu.recipe.d.ts +17 -0
  143. package/dist/types/product/objectsettings/objectsettings.d.ts +71 -15
  144. package/dist/types/product/recordpage/index.d.ts +1 -1
  145. package/dist/types/product/recordpage/recordpage.d.ts +96 -1
  146. package/dist/types/product/recordpicker/index.d.ts +2 -0
  147. package/dist/types/product/recordpicker/recordpicker.d.ts +38 -0
  148. package/dist/types/product/recordpicker/recordpicker.recipe.d.ts +3 -0
  149. package/dist/types/product/savedviews/index.d.ts +1 -1
  150. package/dist/types/product/savedviews/savedviews.d.ts +36 -0
  151. package/dist/types/product/share/index.d.ts +2 -0
  152. package/dist/types/product/share/share.d.ts +85 -0
  153. package/dist/types/product/share/share.recipe.d.ts +3 -0
  154. package/dist/types/product/shortcutssheet/index.d.ts +1 -0
  155. package/dist/types/product/shortcutssheet/shortcutssheet.d.ts +49 -0
  156. package/dist/types/product/shortcutssheet/shortcutssheet.recipe.d.ts +3 -0
  157. package/dist/types/product/taskeditpopover/index.d.ts +2 -0
  158. package/dist/types/product/taskeditpopover/taskeditpopover.d.ts +67 -0
  159. package/dist/types/product/taskeditpopover/taskeditpopover.recipe.d.ts +3 -0
  160. package/dist/types/product/workspaceswitchermenu/index.d.ts +1 -0
  161. package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.d.ts +54 -0
  162. package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.recipe.d.ts +19 -0
  163. package/dist/types/tokens/contract.d.ts +13 -0
  164. package/dist/types/tokens/palette.d.ts +52 -0
  165. package/dist/types/tokens/semantic.d.ts +20 -0
  166. package/dist/workspaceswitchermenu.css +4 -0
  167. package/dist/workspaceswitchermenu.d.ts +7 -0
  168. package/dist/workspaceswitchermenu.layered.css +6 -0
  169. 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,2 @@
1
+ export { SharePopover, } from "./share.js";
2
+ export { shareRecipe } from "./share.recipe.js";
@@ -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,2 @@
1
+ export { TaskEditPopover } from "./taskeditpopover.js";
2
+ export { taskeditpopoverRecipe } from "./taskeditpopover.recipe.js";
@@ -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";