@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
@@ -0,0 +1,27 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ /**
3
+ * `ObjectPageMenu` owns no styling of its own — every visible pixel is
4
+ * `Menu`/`MenuItem`'s own already-measured recipe. The one thing it DOES
5
+ * need is the RTL mirror for its Notifications submenu chevron (crisp#616):
6
+ * that icon is the first direction-bearing one ever passed as a `MenuItem`
7
+ * `trailingIcon`, so nothing already mirrors it.
8
+ *
9
+ * Not written as `${rtlFlipCss}` (core/rtl.ts): that string relies on native
10
+ * CSS nesting's `&` resolving to an ENCLOSING rule, which is exactly what
11
+ * `Menu`'s own recipe deliberately avoids for its portaled content (see its
12
+ * own doc comment) — nesting this under a `.crisp-objectpagemenu` root would
13
+ * need a root class this component doesn't have. A plain absolute selector,
14
+ * matching `Menu`'s own pattern for the identical reason.
15
+ */
16
+ const config = defineRecipe({
17
+ name: "objectpagemenu",
18
+ base: "",
19
+ globalCss: `
20
+ [dir="rtl"] .crisp-menu-item svg[data-rtl-flip] { transform: scaleX(-1); }
21
+ `,
22
+ variants: {},
23
+ defaultVariants: {},
24
+ compoundVariants: [],
25
+ });
26
+ export const objectpagemenuRecipe = resolveRecipe(config);
27
+ export default config;
@@ -161,7 +161,7 @@ ObjectAppearanceSection.displayName = "ObjectAppearanceSection";
161
161
  * measures 40px here — a real, small divergence left to DataTable's owner
162
162
  * rather than patched locally (see the PR).
163
163
  */
164
- export function AttributesTable({ attributes, onOpenAttribute, onEditAttribute, onArchiveAttribute, onReorder, onCreateAttribute, title: titleProp, subtitle: subtitleProp, createLabel: createLabelProp, searchPlaceholder: searchPlaceholderProp, filterLabel: filterLabelProp, nameColumnLabel: nameColumnLabelProp, dataTypeColumnLabel: dataTypeColumnLabelProp, constraintsColumnLabel: constraintsColumnLabelProp, propertiesColumnLabel: propertiesColumnLabelProp, systemLabel: systemLabelProp, enrichedLabel: enrichedLabelProp, editLabel: editLabelProp, archiveLabel: archiveLabelProp, rowActions, emptyLabel, }) {
164
+ export function AttributesTable({ attributes, onOpenAttribute, onEditAttribute, onDuplicateAttribute, onCopyId, onCopySlug, onChangeSlug, onArchiveAttribute, onReorder, onCreateAttribute, title: titleProp, subtitle: subtitleProp, createLabel: createLabelProp, searchPlaceholder: searchPlaceholderProp, filterLabel: filterLabelProp, nameColumnLabel: nameColumnLabelProp, dataTypeColumnLabel: dataTypeColumnLabelProp, constraintsColumnLabel: constraintsColumnLabelProp, propertiesColumnLabel: propertiesColumnLabelProp, systemLabel: systemLabelProp, enrichedLabel: enrichedLabelProp, editLabel: editLabelProp, duplicateLabel: duplicateLabelProp, copyIdLabel: copyIdLabelProp, copySlugLabel: copySlugLabelProp, changeSlugLabel: changeSlugLabelProp, archiveLabel: archiveLabelProp, rowActions, emptyLabel, }) {
165
165
  const title = useCrispMessage(titleProp, "Attributes");
166
166
  const subtitle = useCrispMessage(subtitleProp, "Modify and add object attributes");
167
167
  const createLabel = useCrispMessage(createLabelProp, "Create attribute");
@@ -173,8 +173,37 @@ export function AttributesTable({ attributes, onOpenAttribute, onEditAttribute,
173
173
  const propertiesColumnLabel = useCrispMessage(propertiesColumnLabelProp, "Properties");
174
174
  const systemLabel = useCrispMessage(systemLabelProp, "System");
175
175
  const enrichedLabel = useCrispMessage(enrichedLabelProp, "Enriched");
176
- const editLabel = useCrispMessage(editLabelProp, "Edit");
177
- const archiveLabel = useCrispMessage(archiveLabelProp, "Archive");
176
+ // crisp#673, G-107 (verbatim): "Edit attribute, Duplicate attribute, Copy
177
+ // ID, Copy slug, Change slug, Archive attribute" — corrects this
178
+ // component's former "Edit"/"Archive" defaults, which were a paraphrase
179
+ // from an earlier, less precise measurement.
180
+ const editLabel = useCrispMessage(editLabelProp, "Edit attribute");
181
+ const duplicateLabel = useCrispMessage(duplicateLabelProp, "Duplicate attribute");
182
+ const copyIdLabel = useCrispMessage(copyIdLabelProp, "Copy ID");
183
+ const copySlugLabel = useCrispMessage(copySlugLabelProp, "Copy slug");
184
+ const changeSlugLabel = useCrispMessage(changeSlugLabelProp, "Change slug");
185
+ const archiveLabel = useCrispMessage(archiveLabelProp, "Archive attribute");
186
+ // Default-with-override (same shape as `CommentsPanel`'s `onCopyLink`):
187
+ // a consumer that only wants to know a copy happened (a toast, analytics)
188
+ // gets it for free; one that needs the real copy still owns it.
189
+ // `useCallback`, not a plain closure: the `columns` memo below references
190
+ // these, not `onCopyId`/`onCopySlug` directly, so their own identity has
191
+ // to track those props for the memo to stay correct without re-listing
192
+ // props it never mentions by name.
193
+ const copyId = React.useCallback((id) => {
194
+ if (onCopyId) {
195
+ onCopyId(id);
196
+ return;
197
+ }
198
+ navigator.clipboard?.writeText(id).catch(() => { });
199
+ }, [onCopyId]);
200
+ const copySlug = React.useCallback((slug) => {
201
+ if (onCopySlug) {
202
+ onCopySlug(slug);
203
+ return;
204
+ }
205
+ navigator.clipboard?.writeText(slug).catch(() => { });
206
+ }, [onCopySlug]);
178
207
  const [query, setQuery] = React.useState("");
179
208
  const filtered = React.useMemo(() => {
180
209
  if (!query.trim())
@@ -215,7 +244,7 @@ export function AttributesTable({ attributes, onOpenAttribute, onEditAttribute,
215
244
  cursor: "pointer",
216
245
  color: "inherit",
217
246
  padding: 4,
218
- }, children: _jsx(MoreHorizontal, { size: 16 }) }), children: rowActions ? (rowActions(row)) : (_jsxs(_Fragment, { children: [_jsx(MenuItem, { onSelect: () => onEditAttribute?.(row.id), children: editLabel }), _jsx(MenuItem, { danger: true, onSelect: () => onArchiveAttribute?.(row.id), children: archiveLabel })] })) })),
247
+ }, children: _jsx(MoreHorizontal, { size: 16 }) }), children: rowActions ? (rowActions(row)) : (_jsxs(_Fragment, { children: [_jsx(MenuItem, { onSelect: () => onEditAttribute?.(row.id), children: editLabel }), _jsx(MenuItem, { onSelect: () => onDuplicateAttribute?.(row.id), children: duplicateLabel }), _jsx(MenuItem, { onSelect: () => copyId(row.id), children: copyIdLabel }), _jsx(MenuItem, { disabled: !row.slug, onSelect: () => row.slug && copySlug(row.slug), children: copySlugLabel }), _jsx(MenuItem, { onSelect: () => onChangeSlug?.(row.id), children: changeSlugLabel }), _jsx(MenuItem, { danger: true, onSelect: () => onArchiveAttribute?.(row.id), children: archiveLabel })] })) })),
219
248
  },
220
249
  ], [
221
250
  nameColumnLabel,
@@ -225,8 +254,16 @@ export function AttributesTable({ attributes, onOpenAttribute, onEditAttribute,
225
254
  systemLabel,
226
255
  enrichedLabel,
227
256
  editLabel,
257
+ duplicateLabel,
258
+ copyIdLabel,
259
+ copySlugLabel,
260
+ changeSlugLabel,
228
261
  archiveLabel,
229
262
  onEditAttribute,
263
+ onDuplicateAttribute,
264
+ copyId,
265
+ copySlug,
266
+ onChangeSlug,
230
267
  onArchiveAttribute,
231
268
  rowActions,
232
269
  ]);
@@ -287,26 +324,82 @@ export function AttributeTypeSelect({ value, onValueChange, options = ATTRIBUTE_
287
324
  return (_jsx(Select, { id: id, options: selectOptions, value: value, onValueChange: onValueChange, searchable: true, searchPlaceholder: searchPlaceholder, width: "full" }));
288
325
  }
289
326
  AttributeTypeSelect.displayName = "AttributeTypeSelect";
327
+ /* ── Currency's own extra required fields (crisp#673, G-104) ──────────
328
+ * Attio's Create-attribute dialog isn't a fixed layout: choosing "Currency"
329
+ * inserts four required selects between Description and the AI-autofill
330
+ * row (Text/Number/Checkbox/Date/Phone Number/Location/User add none).
331
+ * Display, Decimal Places and Grouping are Attio's own small fixed UI
332
+ * vocabulary (verified live, `app.attio.com` Companies object) and ship as
333
+ * defaults below; Currency is real reference data (ISO 4217, ~150+
334
+ * entries) that crisp doesn't own, so it has no default of its own — a
335
+ * consumer supplies its own list, the same "GRC need: later" boundary
336
+ * `aiAutofillTypeOptions` already draws. Each option's own live-formatted
337
+ * preview (e.g. "Code — USD 1,200.50") is deliberately not modelled: it
338
+ * needs real number-formatting logic, a separate, bigger piece of work. */
339
+ export const CURRENCY_DISPLAY_OPTIONS = [
340
+ { value: "code", label: "Code" },
341
+ { value: "name", label: "Name" },
342
+ { value: "narrow-symbol", label: "Narrow symbol" },
343
+ { value: "symbol", label: "Symbol" },
344
+ ];
345
+ export const CURRENCY_DECIMAL_PLACES_OPTIONS = [
346
+ { value: "2", label: "2 decimals" },
347
+ { value: "0", label: "No decimals" },
348
+ ];
349
+ export const CURRENCY_GROUPING_OPTIONS = [
350
+ { value: "default", label: "Default" },
351
+ { value: "none", label: "No groups" },
352
+ ];
290
353
  /**
291
- * "Create attribute" (crisp#637, P-125, verified live): Attribute Type
292
- * select (defaults to Text) + Name (autofocused) + Description (optional)
293
- * + an AI-autofill switch row, footer Cancel/Create. Built on `FormDialog`
294
- * in its plain (non-`createMode`) shape — Attio's own footer here has a
295
- * real Cancel button, unlike the record-create dialog `createMode` models
296
- * (crisp#548), so `createMode` would be the wrong fit even though the
297
- * button carries the same ⌘↵ hint; that hint is rendered locally instead.
354
+ * "Create attribute" (crisp#637/#673, P-125, verified live): Attribute Type
355
+ * select (defaults to Text, type-ahead search built in via `Select`'s own
356
+ * `searchable`) + Name (autofocused on open, and again after picking a type
357
+ * — G-106) + Description (optional) + Currency's own four extra required
358
+ * fields when `type === "currency"` (G-104) + an AI-autofill switch row,
359
+ * footer Cancel/Create. Built on `FormDialog` in its plain (non-`createMode`)
360
+ * shape — Attio's own footer here has a real Cancel button, unlike the
361
+ * record-create dialog `createMode` models (crisp#548), so `createMode`
362
+ * would be the wrong fit even though the button carries the same ⌘↵ hint;
363
+ * that hint is rendered locally instead. The outside-click/Escape
364
+ * "Discard changes" confirm gated on dirty state (G-105) needed no work
365
+ * here at all — `FormDialog` already tracks dirtiness generically and shows
366
+ * it, so this dialog inherited it for free; `CreateAttributeDialog`'s own
367
+ * tests below just pin the behaviour to this component specifically.
298
368
  */
299
- export function CreateAttributeDialog({ type, onTypeChange, typeOptions, aiAutofillTypeOptions, name, onNameChange, description, onDescriptionChange, aiAutofillEnabled = false, onAiAutofillChange, onSubmit, onClose, busy, error, title: titleProp, attributeTypeLabel: attributeTypeLabelProp, nameLabel: nameLabelProp, descriptionLabel: descriptionLabelProp, descriptionPlaceholder: descriptionPlaceholderProp, aiAutofillLabel: aiAutofillLabelProp, setUpAiAutofillLabel: setUpAiAutofillLabelProp, createLabel: createLabelProp, }) {
369
+ export function CreateAttributeDialog({ type, onTypeChange, typeOptions, aiAutofillTypeOptions, name, onNameChange, description, onDescriptionChange, currency, onCurrencyChange, currencyOptions, display, onDisplayChange, displayOptions = CURRENCY_DISPLAY_OPTIONS, decimalPlaces, onDecimalPlacesChange, decimalPlacesOptions = CURRENCY_DECIMAL_PLACES_OPTIONS, grouping, onGroupingChange, groupingOptions = CURRENCY_GROUPING_OPTIONS, currencyFieldLabel: currencyFieldLabelProp, displayFieldLabel: displayFieldLabelProp, decimalPlacesFieldLabel: decimalPlacesFieldLabelProp, groupingFieldLabel: groupingFieldLabelProp, aiAutofillEnabled = false, onAiAutofillChange, onSubmit, onClose, busy, error, title: titleProp, attributeTypeLabel: attributeTypeLabelProp, nameLabel: nameLabelProp, descriptionLabel: descriptionLabelProp, descriptionPlaceholder: descriptionPlaceholderProp, aiAutofillLabel: aiAutofillLabelProp, setUpAiAutofillLabel: setUpAiAutofillLabelProp, createLabel: createLabelProp, }) {
300
370
  const title = useCrispMessage(titleProp, "Create attribute");
301
371
  const attributeTypeLabel = useCrispMessage(attributeTypeLabelProp, "Attribute Type");
302
372
  const nameLabel = useCrispMessage(nameLabelProp, "Name");
303
373
  const descriptionLabel = useCrispMessage(descriptionLabelProp, "Description");
304
374
  const descriptionPlaceholder = useCrispMessage(descriptionPlaceholderProp, "Add a description for this attribute");
375
+ // Attio's own copy for these four is the label plus a literal "(required)"
376
+ // suffix (not `FormField`'s asterisk convention) — the same "(optional)"-
377
+ // as-label-text shape `descriptionLabel` above already uses, so these are
378
+ // plain overridable strings rather than a new `FormField` capability.
379
+ const currencyFieldLabel = useCrispMessage(currencyFieldLabelProp, "Currency (required)");
380
+ const displayFieldLabel = useCrispMessage(displayFieldLabelProp, "Display (required)");
381
+ const decimalPlacesFieldLabel = useCrispMessage(decimalPlacesFieldLabelProp, "Decimal Places (required)");
382
+ const groupingFieldLabel = useCrispMessage(groupingFieldLabelProp, "Grouping (required)");
305
383
  const aiAutofillLabel = useCrispMessage(aiAutofillLabelProp, "AI autofill");
306
384
  const setUpAiAutofillLabel = useCrispMessage(setUpAiAutofillLabelProp, "Set up AI autofill");
307
385
  const createLabel = useCrispMessage(createLabelProp, "Create attribute");
308
386
  const switchId = React.useId();
309
387
  const nameId = React.useId();
310
- return (_jsxs(FormDialog, { title: title, submitLabel: _jsxs(_Fragment, { children: [createLabel, _jsxs("span", { className: "crisp-objectsettings-kbd", "aria-hidden": "true", children: [_jsx("kbd", { children: "\u2318" }), _jsx("kbd", { children: "\u21B5" })] })] }), onSubmit: onSubmit, onClose: onClose, busy: busy, error: error, size: 520, className: objectsettingsRecipe(), children: [_jsx(FormField, { label: attributeTypeLabel, children: _jsx(AttributeTypeSelect, { value: type, onValueChange: onTypeChange, options: typeOptions, aiAutofillOptions: aiAutofillTypeOptions }) }), _jsx(FormField, { label: nameLabel, htmlFor: nameId, children: _jsx(Input, { id: nameId, autoFocus: true, value: name, onChange: (e) => onNameChange(e.target.value) }) }), _jsx(FormField, { label: descriptionLabel, optional: true, children: _jsx(Textarea, { placeholder: descriptionPlaceholder, value: description, onChange: (e) => onDescriptionChange(e.target.value) }) }), _jsxs("div", { className: "crisp-objectsettings-aiautofill", children: [_jsx("span", { className: "crisp-objectsettings-aiautofill-label", children: aiAutofillLabel }), _jsxs("label", { className: "crisp-objectsettings-aiautofill-row", htmlFor: switchId, children: [_jsx(Sparkles, { size: 14 }), _jsx("span", { children: setUpAiAutofillLabel }), _jsx(Switch, { id: switchId, checked: aiAutofillEnabled, onCheckedChange: onAiAutofillChange })] })] })] }));
388
+ // G-106: selecting a type via the keyboard (filter to one match, Enter)
389
+ // auto-focuses Name right after — measured live. `Select`'s own
390
+ // `onValueChange` fires identically for a keyboard Enter and a mouse
391
+ // click (both go through its one `pick()`), so this can't distinguish
392
+ // input modality; only the keyboard path was independently re-measured,
393
+ // but there is no second code path to special-case even if there were
394
+ // one. The `requestAnimationFrame` wins the same race `Select`'s own
395
+ // `searchable` variant and `SavedViews`'s search box already schedule
396
+ // focus against (Select returns focus to its trigger synchronously on
397
+ // pick; this runs after that paint and wins).
398
+ const nameRef = React.useRef(null);
399
+ const handleTypeChange = (value) => {
400
+ onTypeChange(value);
401
+ requestAnimationFrame(() => nameRef.current?.focus({ preventScroll: true }));
402
+ };
403
+ return (_jsxs(FormDialog, { title: title, submitLabel: _jsxs(_Fragment, { children: [createLabel, _jsxs("span", { className: "crisp-objectsettings-kbd", "aria-hidden": "true", children: [_jsx("kbd", { children: "\u2318" }), _jsx("kbd", { children: "\u21B5" })] })] }), onSubmit: onSubmit, onClose: onClose, busy: busy, error: error, size: 520, className: objectsettingsRecipe(), children: [_jsx(FormField, { label: attributeTypeLabel, children: _jsx(AttributeTypeSelect, { value: type, onValueChange: handleTypeChange, options: typeOptions, aiAutofillOptions: aiAutofillTypeOptions }) }), _jsx(FormField, { label: nameLabel, htmlFor: nameId, children: _jsx(Input, { ref: nameRef, id: nameId, autoFocus: true, value: name, onChange: (e) => onNameChange(e.target.value) }) }), _jsx(FormField, { label: descriptionLabel, optional: true, children: _jsx(Textarea, { placeholder: descriptionPlaceholder, value: description, onChange: (e) => onDescriptionChange(e.target.value) }) }), type === "currency" && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "crisp-objectsettings-fieldrow", children: [_jsx(FormField, { label: currencyFieldLabel, children: _jsx(Select, { options: currencyOptions ?? [], value: currency ?? "", onValueChange: onCurrencyChange, searchable: true }) }), _jsx(FormField, { label: displayFieldLabel, children: _jsx(Select, { options: displayOptions, value: display ?? displayOptions[0]?.value, onValueChange: onDisplayChange }) })] }), _jsxs("div", { className: "crisp-objectsettings-fieldrow", children: [_jsx(FormField, { label: decimalPlacesFieldLabel, children: _jsx(Select, { options: decimalPlacesOptions, value: decimalPlaces ?? decimalPlacesOptions[0]?.value, onValueChange: onDecimalPlacesChange }) }), _jsx(FormField, { label: groupingFieldLabel, children: _jsx(Select, { options: groupingOptions, value: grouping ?? groupingOptions[0]?.value, onValueChange: onGroupingChange }) })] })] })), _jsxs("div", { className: "crisp-objectsettings-aiautofill", children: [_jsx("span", { className: "crisp-objectsettings-aiautofill-label", children: aiAutofillLabel }), _jsxs("label", { className: "crisp-objectsettings-aiautofill-row", htmlFor: switchId, children: [_jsx(Sparkles, { size: 14 }), _jsx("span", { children: setUpAiAutofillLabel }), _jsx(Switch, { id: switchId, checked: aiAutofillEnabled, onCheckedChange: onAiAutofillChange })] })] })] }));
311
404
  }
312
405
  CreateAttributeDialog.displayName = "CreateAttributeDialog";
@@ -5,6 +5,7 @@ import { Avatar } from "../../components/avatar/index.js";
5
5
  import { Button } from "../../components/button/index.js";
6
6
  import { Checkbox } from "../../components/checkbox/index.js";
7
7
  import { Drawer } from "../../components/drawer/index.js";
8
+ import { Kbd } from "../../components/kbd/index.js";
8
9
  import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
9
10
  import { Select } from "../../components/select/index.js";
10
11
  import { Spinner } from "../../components/spinner/index.js";
@@ -79,6 +80,12 @@ export const recordPageLabels = {
79
80
  upgradeToViewCalls: "Upgrade to view calls",
80
81
  callsUpgradeHint: "No call recordings yet! Upgrade to Pro to start recording calls.",
81
82
  upgrade: "Upgrade",
83
+ selectWorkflowTitle: "Select workflow",
84
+ closeWorkflowPopover: "Close",
85
+ searchWorkflowsPlaceholder: "Search workflows...",
86
+ noWorkflows: "No workflows",
87
+ createNewWorkflow: "Create new workflow",
88
+ navigate: "Navigate",
82
89
  highlights: "Highlights",
83
90
  recordDetails: "Record Details",
84
91
  searchFields: "Search fields",
@@ -476,7 +483,7 @@ export function RecordDetails({ record, columns, sections, onEdit, labels, class
476
483
  commit(record.id, col.key, e.currentTarget.value);
477
484
  } }), isPending && (_jsx(Spinner, { size: 14, className: "crisp-recordpage-edit-pending" })), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
478
485
  }
479
- return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit ? (_jsx("button", { type: "button", className: "crisp-recordpage-edit-trigger", onClick: () => openEditing(col.key), children: _jsx(FieldValue, { col: col, row: record, labels: t }) })) : (_jsx(FieldValue, { col: col, row: record, labels: t })), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
486
+ return (_jsxs("div", { className: "crisp-recordpage-field", children: [label, _jsxs("span", { className: "crisp-recordpage-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit && col.type === "url" ? (_jsxs(_Fragment, { children: [_jsx(FieldValue, { col: col, row: record, labels: t }), _jsx("button", { type: "button", className: "crisp-recordpage-edit-affordance", "aria-label": `Edit ${col.label}`, onClick: () => openEditing(col.key), children: _jsx(Pencil, { size: 16, strokeWidth: 1.75 }) })] })) : canEdit ? (_jsx("button", { type: "button", className: "crisp-recordpage-edit-trigger", onClick: () => openEditing(col.key), children: _jsx(FieldValue, { col: col, row: record, labels: t }) })) : (_jsx(FieldValue, { col: col, row: record, labels: t })), fieldError && (_jsx("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
480
487
  })] }, g.title != null ? String(g.title) : `section-${i}`))), canRevealFields && (_jsx("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
481
488
  // Revealing all fields also opens the (focused) field
482
489
  // search; collapsing closes it.
@@ -516,6 +523,46 @@ export function RecordPageMain({ className, children, ...rest }) {
516
523
  return (_jsx("main", { className: cx("crisp-recordpage-main", className), ...rest, children: children }));
517
524
  }
518
525
  RecordPageMain.displayName = "RecordPageMain";
526
+ /**
527
+ * The "Run workflow" quick action's own popover (#620, P-018) — measured
528
+ * live on `app.attio.com`'s own record page: a centered, top-anchored
529
+ * overlay sharing the exact same frosted-bezel shell `product/command`'s
530
+ * own Cmd-K palette already uses (`elevation.modal`, `bgOverlayFrost` 60% +
531
+ * `blur(15px)`, radius `2xl`/`xl` outer/inner) — duplicated into this
532
+ * recipe rather than imported, the same "own copy per product surface"
533
+ * choice already made elsewhere in this codebase (`product/command` is
534
+ * outside this lane's own `product/recordpage` ownership).
535
+ *
536
+ * Chrome only, like the quick-action row's own Compose email/Add to list
537
+ * buttons above: crisp has no workflow data model to search or list yet, so
538
+ * the search box is real and typable but filters nothing, and the "No
539
+ * workflows" empty state always renders. The measured popover's own
540
+ * illustration is a bespoke Attio graphic (Golden Rule 4: Lucide only) —
541
+ * reuses crisp's own established "illustrated empty state" dotted-grid tile
542
+ * (`.crisp-recordpage-illustrated-icon`, already shipped for Files/Deals)
543
+ * with a `Workflow` glyph instead.
544
+ */
545
+ function WorkflowPopover({ open, onClose, labels, }) {
546
+ React.useEffect(() => {
547
+ if (!open)
548
+ return;
549
+ const onKey = (e) => {
550
+ if (e.key === "Escape")
551
+ onClose();
552
+ };
553
+ document.addEventListener("keydown", onKey);
554
+ return () => document.removeEventListener("keydown", onKey);
555
+ }, [open, onClose]);
556
+ if (!open)
557
+ return null;
558
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "crisp-recordpage-workflow-backdrop" }), _jsx("div", { className: "crisp-recordpage-workflow-positioner", onClick: (e) => {
559
+ if (e.target === e.currentTarget)
560
+ onClose();
561
+ }, children: _jsx("div", { className: "crisp-recordpage-workflow-content", children: _jsxs("div", { className: "crisp-recordpage-workflow-card", role: "dialog", "aria-modal": "true", "aria-label": labels.selectWorkflowTitle, children: [_jsxs("div", { className: "crisp-recordpage-workflow-header", children: [_jsx("span", { className: "crisp-recordpage-workflow-title", children: labels.selectWorkflowTitle }), _jsx("button", { type: "button", className: "crisp-recordpage-workflow-close", "aria-label": labels.closeWorkflowPopover, onClick: onClose, children: _jsx(X, { size: 14, strokeWidth: 1.75 }) })] }), _jsx("div", { className: "crisp-recordpage-workflow-search", children: _jsx("input", {
562
+ // biome-ignore lint/a11y/noAutofocus: focusing the just-opened popover's search is expected
563
+ autoFocus: true, type: "text", role: "combobox", "aria-expanded": "true", "aria-label": labels.selectWorkflowTitle, placeholder: labels.searchWorkflowsPlaceholder, className: "crisp-recordpage-workflow-input" }) }), _jsx("div", { className: "crisp-recordpage-workflow-sep" }), _jsxs("div", { className: "crisp-recordpage-workflow-empty", children: [_jsx("span", { className: "crisp-recordpage-illustrated-icon", "aria-hidden": "true", children: _jsx(Workflow, { size: 40, strokeWidth: 1.25 }) }), _jsx("div", { className: "crisp-recordpage-illustrated-desc", children: labels.noWorkflows }), _jsxs("button", { type: "button", className: "crisp-recordpage-workflow-create", children: [_jsx(Plus, { size: 14, strokeWidth: 1.75 }), labels.createNewWorkflow] })] }), _jsx("div", { className: "crisp-recordpage-workflow-footer", children: _jsxs("span", { className: "crisp-recordpage-workflow-footer-nav", children: [_jsx(Kbd, { children: "\u2191" }), _jsx(Kbd, { children: "\u2193" }), labels.navigate] }) })] }) }) })] }));
564
+ }
565
+ WorkflowPopover.displayName = "WorkflowPopover";
519
566
  /**
520
567
  * The full-page company record view — the page that opens when you click into a
521
568
  * record (as opposed to the peek [RecordPanel]). A top breadcrumb bar with
@@ -524,9 +571,15 @@ RecordPageMain.displayName = "RecordPageMain";
524
571
  * Emails sections (Notes & Tasks are live — add and check things off). Drop it
525
572
  * into the app's main content area.
526
573
  */
527
- export function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, onClose, closeLabel = "Close", onPrevious, previousRecordLabel = "Previous record", onNext, nextRecordLabel = "Next record", counterLabel, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, quickActions: quickActionsProp, comments: commentsProp, onPostComment, currentUser, onShare, ...rest }) {
574
+ export function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, onClose, closeLabel = "Close", onPrevious, previousRecordLabel = "Previous record", onNext, nextRecordLabel = "Next record", counterLabel, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, sections, highlights: highlightsProp, quickActions: quickActionsProp, comments: commentsProp, onPostComment, currentUser, onShare, avatarShape = "rounded", onEditRecord, onDelete, actions, showBreadcrumb = true, showShare = true, showAskAttio = true, showComments = true, ...rest }) {
528
575
  const t = useRecordPageLabels(labels);
529
576
  const [favorite, setFavorite] = React.useState(false);
577
+ // #620: the "Run workflow" quick action's own popover -- only the
578
+ // BUILT-IN default quick-action list opens it (below); a caller supplying
579
+ // its own `quickActions` owns every button's behaviour itself, the same
580
+ // "caller's own list, not a merge" contract `tabs`/`highlights` already
581
+ // make.
582
+ const [workflowPopoverOpen, setWorkflowPopoverOpen] = React.useState(false);
530
583
  // Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
531
584
  // `[]`) -- otherwise RecordPage keeps its own self-contained state, as
532
585
  // before (#379). "Controlled" here means the array prop alone decides the
@@ -643,7 +696,13 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
643
696
  // choice `tabs` (#379) and `highlights` (#528) already make.
644
697
  const defaultQuickActions = [
645
698
  { label: t.logANote, icon: FileText },
646
- { label: t.runWorkflow, icon: Workflow },
699
+ // #620: only the built-in default wires this to crisp's own popover --
700
+ // see `workflowPopoverOpen`'s own comment above.
701
+ {
702
+ label: t.runWorkflow,
703
+ icon: Workflow,
704
+ onSelect: () => setWorkflowPopoverOpen(true),
705
+ },
647
706
  { label: t.createATask, icon: SquareCheck },
648
707
  ];
649
708
  const quickActions = quickActionsProp ?? defaultQuickActions;
@@ -786,7 +845,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
786
845
  const effectivePanel = tabsProp
787
846
  ? (active) => tabsProp.find((tb) => tb.id === active)?.render()
788
847
  : builtinPanel;
789
- return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", "data-peek": onClose ? "" : undefined, children: [onClose ? (_jsxs("div", { className: "crisp-recordpage-peekbar", children: [_jsx("button", { type: "button", className: "crisp-recordpage-peek-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75 }) }), (onPrevious || onNext) && (_jsxs("span", { className: "crisp-recordpage-peek-pager", children: [_jsx("button", { type: "button", className: "crisp-recordpage-peek-navbtn", "aria-label": previousRecordLabel, disabled: !onPrevious, onClick: onPrevious, children: _jsx(ChevronUp, { size: 16, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-peek-navbtn", "aria-label": nextRecordLabel, disabled: !onNext, onClick: onNext, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] })), counterLabel != null && (_jsx("span", { className: "crisp-recordpage-peek-counter", children: counterLabel }))] })) : (_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] })), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", onClick: onShare, children: t.share }), _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: () => setCommentsOpen(true), "aria-label": t.comments, children: _jsx(MessageSquare, { size: 15, strokeWidth: 1.75 }) }), _jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Merge, { size: 15, strokeWidth: 1.75 }), children: t.mergeRecords }), _jsx(MenuItem, { icon: _jsx(Settings, { size: 15, strokeWidth: 1.75 }), children: t.configurePage }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyPageUrl }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyRecordId }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => (_jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: _jsx(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
848
+ return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", "data-peek": onClose ? "" : undefined, children: [onClose ? (_jsxs("div", { className: "crisp-recordpage-peekbar", children: [_jsx("button", { type: "button", className: "crisp-recordpage-peek-iconbtn", "aria-label": closeLabel, onClick: onClose, children: _jsx(X, { size: 16, strokeWidth: 1.75 }) }), (onPrevious || onNext) && (_jsxs("span", { className: "crisp-recordpage-peek-pager", children: [_jsx("button", { type: "button", className: "crisp-recordpage-peek-navbtn", "aria-label": previousRecordLabel, disabled: !onPrevious, onClick: onPrevious, children: _jsx(ChevronUp, { size: 16, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-peek-navbtn", "aria-label": nextRecordLabel, disabled: !onNext, onClick: onNext, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) })] })), counterLabel != null && (_jsx("span", { className: "crisp-recordpage-peek-counter", children: counterLabel }))] })) : (showBreadcrumb && (_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }))), _jsxs("div", { className: "crisp-recordpage-actions", children: [showShare && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", onClick: onShare, children: t.share })), showComments && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: () => setCommentsOpen(true), "aria-label": t.comments, children: _jsx(MessageSquare, { size: 15, strokeWidth: 1.75 }) })), showAskAttio && (_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] })), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Merge, { size: 15, strokeWidth: 1.75 }), children: t.mergeRecords }), _jsx(MenuItem, { icon: _jsx(Settings, { size: 15, strokeWidth: 1.75 }), children: t.configurePage }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyPageUrl }), _jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), children: t.copyRecordId }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), actions && actions.length > 0 && (_jsxs(_Fragment, { children: [_jsx(MenuSeparator, {}), actions.map((a) => (_jsx(MenuItem, { icon: _jsx(a.icon, { size: 15, strokeWidth: 1.75 }), onSelect: a.onSelect, danger: a.danger, children: a.label }, a.label)))] })), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), onSelect: onDelete, danger: true, children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: avatarShape, tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, onClick: onEditRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Plus, { size: 16, strokeWidth: 1.75 }), t.addToList] }), quickActions.map((a, i) => (_jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": a.label, onClick: a.onSelect, children: _jsx(a.icon, { size: 15, strokeWidth: 1.75 }) }, i)))] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
790
849
  if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
791
850
  return;
792
851
  // The pointer drag learned its direction in #212; this did not.
@@ -798,6 +857,6 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
798
857
  setDetailsW((w) => grow
799
858
  ? Math.min(DETAILS_MAX_W, w + 16)
800
859
  : Math.max(DETAILS_MIN_W, w - 16));
801
- } }), _jsx(RecordPageMain, { children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] }), _jsx(Drawer, { open: commentsOpen, onOpenChange: setCommentsOpen, "aria-label": t.comments, style: { width: 320 }, children: _jsx(CommentsPanel, { threads: comments, currentUserId: me.id, onPost: postComment, onClose: () => setCommentsOpen(false), role: undefined }) })] }));
860
+ } }), _jsx(RecordPageMain, { children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] }), _jsx(Drawer, { open: commentsOpen, onOpenChange: setCommentsOpen, "aria-label": t.comments, style: { width: 320 }, children: _jsx(CommentsPanel, { threads: comments, currentUserId: me.id, onPost: postComment, onClose: () => setCommentsOpen(false), role: undefined }) }), _jsx(WorkflowPopover, { open: workflowPopoverOpen, onClose: () => setWorkflowPopoverOpen(false), labels: t })] }));
802
861
  }
803
862
  RecordPage.displayName = "RecordPage";
@@ -124,6 +124,19 @@ const config = defineRecipe({
124
124
  the component's own comment) -- NOT \`display: contents\`, which
125
125
  drops a button's accessible semantics in some engines. */
126
126
  & .crisp-recordpage-edit-trigger { display: inline-flex; flex-wrap: wrap; align-items: center; gap: ${c.space["1"]}; margin: 0; padding: 0; border: none; background: none; text-align: start; font: inherit; color: inherit; cursor: pointer; }
127
+ /* #990: measured live on app.attio.com's URL/domain attribute (hover the
128
+ row) -- the link itself never carries the edit affordance, a
129
+ separate 20px button trails it, mounting only on hover: 16px icon,
130
+ rgba(0,0,0,0.55) (crisp's own color-mix(fgPrimary 55%) -- the exact
131
+ formula .crisp-datatable-entity-action's identical muted icon
132
+ already uses), 6px radius (radius.sm). Reveal is opacity-based,
133
+ not the mount/unmount Attio's own DOM does, so a keyboard user
134
+ tabbing to it (:focus-visible) doesn't need the row hovered first --
135
+ same mechanism DataTable's own row actions (#601) already use. */
136
+ & .crisp-recordpage-edit-affordance { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer; opacity: 0; transition: opacity .12s, background-color .12s, color .12s; }
137
+ & .crisp-recordpage-field:hover .crisp-recordpage-edit-affordance,
138
+ & .crisp-recordpage-edit-affordance:focus-visible { opacity: 1; }
139
+ & .crisp-recordpage-edit-affordance:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
127
140
  & .crisp-recordpage-empty { color: ${c.fgMuted}; }
128
141
 
129
142
  /* #431: measured live on app.attio.com's record page Details column --
@@ -261,6 +274,31 @@ const config = defineRecipe({
261
274
  & .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, ${c.bgPurple} 16%, transparent); color: color-mix(in srgb, ${c.bgPurple} 80%, ${c.fgPrimary}); }
262
275
  & .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${c.bgCyan} 18%, transparent); color: color-mix(in srgb, ${c.bgCyan} 82%, ${c.fgPrimary}); }
263
276
  & .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); }
277
+
278
+ /* #620: the "Run workflow" quick action's own popover -- measured live:
279
+ a centered, top-anchored overlay sharing product/command's own
280
+ Cmd-K-palette shell exactly (elevation.modal, bgOverlayFrost 60% +
281
+ blur(15px), radius 2xl/xl outer/inner) -- duplicated here rather than
282
+ imported, the same "own copy per product surface" choice already
283
+ made elsewhere in this codebase (product/command sits outside this
284
+ lane's own product/recordpage ownership). */
285
+ & .crisp-recordpage-workflow-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
286
+ & .crisp-recordpage-workflow-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh ${c.space["4"]} ${c.space["4"]}; }
287
+ & .crisp-recordpage-workflow-content { position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-width: 808px; padding: ${c.space["1"]}; border-radius: ${c.radius["2xl"]}; background: color-mix(in srgb, ${c.bgOverlayFrost} 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: ${e.modal}; opacity: 1; transform: scale(1); transition: opacity .2s ease, transform .2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
288
+ & .crisp-recordpage-workflow-card { display: flex; flex-direction: column; box-sizing: border-box; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; overflow: hidden; }
289
+ & .crisp-recordpage-workflow-header { display: flex; align-items: center; gap: ${c.space["2"]}; height: 52px; flex-shrink: 0; padding: 0 ${c.space["2"]} 0 ${c.space["4"]}; }
290
+ & .crisp-recordpage-workflow-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.07px; color: ${c.fgPrimary}; }
291
+ & .crisp-recordpage-workflow-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
292
+ & .crisp-recordpage-workflow-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
293
+ & .crisp-recordpage-workflow-search { display: flex; align-items: center; min-height: 52px; padding: ${c.space["4"]}; }
294
+ & .crisp-recordpage-workflow-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-family: inherit; font-size: ${c.text.xl}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
295
+ & .crisp-recordpage-workflow-input::placeholder { color: ${c.fgMuted}; }
296
+ & .crisp-recordpage-workflow-sep { height: 1px; flex-shrink: 0; background: ${c.borderSubtle}; }
297
+ & .crisp-recordpage-workflow-empty { display: flex; flex-direction: column; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["6"]} ${c.space["3"]}; text-align: center; }
298
+ & .crisp-recordpage-workflow-create { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 32px; padding: 0 ${c.space["3"]}; margin-top: ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: ${e.field}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; cursor: pointer; transition: background-color .12s; }
299
+ & .crisp-recordpage-workflow-create:hover { background: ${c.bgRaised}; }
300
+ & .crisp-recordpage-workflow-footer { display: flex; align-items: center; height: 44px; flex-shrink: 0; padding: 0 ${c.space["2.5"]}; border-top: 1px solid ${c.borderSubtle}; }
301
+ & .crisp-recordpage-workflow-footer-nav { display: flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
264
302
  `,
265
303
  variants: {},
266
304
  defaultVariants: {},
@@ -0,0 +1,2 @@
1
+ export { RecordPicker, } from "./recordpicker.js";
2
+ export { recordpickerRecipe } from "./recordpicker.recipe.js";
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Search } from "lucide-react";
3
+ import * as React from "react";
4
+ import { useListboxKeyboard } from "../../components/select/index.js";
5
+ import { Spinner } from "../../components/spinner/index.js";
6
+ import { useCrispMessage, useCrispMessages } from "../../core/index.js";
7
+ import { recordpickerRecipe } from "./recordpicker.recipe.js";
8
+ /**
9
+ * The record-search picker behind a task's "Add record" chip -- measured
10
+ * live on app.attio.com's Tasks page ("Edit task" -> "Add record"): a
11
+ * "Search..." row, then a FLAT list mixed across every object type (a
12
+ * person result sat first, ahead of several companies, with no section
13
+ * headers -- unlike the mention picker's own grouped Users/Records shape,
14
+ * crisp#646). Attio's own live search took a moment to resolve, showing a
15
+ * centred spinner + "Loading..." row meanwhile; not independently
16
+ * re-measured against a second empty-results case, so `emptyLabel`'s
17
+ * default copy ("No records") is inferred by symmetry with
18
+ * `AssigneePicker`'s own measured "No users", not itself pixel-measured.
19
+ */
20
+ export function RecordPicker({ records, loading = false, onSelect, query: queryProp, onQueryChange, searchLabel: searchLabelProp, loadingLabel: loadingLabelProp, emptyLabel: emptyLabelProp, className, ...rest }) {
21
+ const searchLabel = useCrispMessage(searchLabelProp, "Search...");
22
+ const loadingLabel = useCrispMessage(loadingLabelProp, "Loading...");
23
+ // crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
24
+ // hand rather than through `useCrispMessage`, which is string-only --
25
+ // same reasoning as `Inbox`'s own `emptyTitle`/`emptySubtitle`.
26
+ const crispMessages = useCrispMessages();
27
+ const emptyLabel = emptyLabelProp ?? crispMessages["No records"] ?? "No records";
28
+ const [internalQuery, setInternalQuery] = React.useState("");
29
+ const query = queryProp ?? internalQuery;
30
+ const setQuery = (q) => {
31
+ if (queryProp === undefined)
32
+ setInternalQuery(q);
33
+ onQueryChange?.(q);
34
+ };
35
+ const [highlight, setHighlight] = React.useState(0);
36
+ const autoId = React.useId();
37
+ const listboxId = `${autoId}-listbox`;
38
+ const optionId = (i) => `${autoId}-opt-${i}`;
39
+ const select = (r) => {
40
+ onSelect?.(r);
41
+ };
42
+ const { onKeyDown } = useListboxKeyboard({
43
+ enabledIndexes: records.map((_, i) => i),
44
+ highlight,
45
+ setHighlight,
46
+ commit: () => {
47
+ const r = records[highlight];
48
+ if (r)
49
+ select(r);
50
+ },
51
+ close: () => { },
52
+ dismiss: () => { },
53
+ });
54
+ const activeId = !loading && highlight >= 0 && highlight < records.length
55
+ ? optionId(highlight)
56
+ : undefined;
57
+ return (_jsxs("div", { className: recordpickerRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpicker-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { type: "text", className: "crisp-recordpicker-search-input", placeholder: searchLabel, value: query, onChange: (e) => {
58
+ setQuery(e.target.value);
59
+ setHighlight(0);
60
+ }, onKeyDown: onKeyDown, role: "combobox", "aria-expanded": "true", "aria-controls": listboxId, "aria-activedescendant": activeId,
61
+ // biome-ignore lint/a11y/noAutofocus: focus the search field the moment its enclosing popover opens
62
+ autoFocus: true })] }), loading ? (_jsxs("div", { className: "crisp-recordpicker-loading", children: [_jsx(Spinner, { size: 16 }), loadingLabel] })) : records.length === 0 ? (_jsx("div", { className: "crisp-recordpicker-empty", children: emptyLabel })) : (_jsx("div", { className: "crisp-recordpicker-list", role: "listbox", id: listboxId, children: records.map((r, i) => (_jsxs("button", { type: "button", id: optionId(i), role: "option", tabIndex: -1, "aria-selected": i === highlight, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-recordpicker-option", onMouseEnter: () => setHighlight(i), onClick: () => select(r), children: [r.icon, r.name] }, r.id))) }))] }));
63
+ }
64
+ RecordPicker.displayName = "RecordPicker";
@@ -0,0 +1,33 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // The record picker behind a task's "Add record" chip -- measured live on
4
+ // app.attio.com's Tasks page ("Edit task" -> "Add record" -> its own
5
+ // sub-popover, same measured 350px width as `AssigneePicker`'s): a
6
+ // borderless "Search..." row, then a flat list mixed across every object
7
+ // type (not grouped into sections -- that's the *mention* picker's own
8
+ // shape, crisp#646, a different, still-ungrouped-by-default surface here),
9
+ // each row a consumer-supplied type icon + the record's name. A "Loading..."
10
+ // row appears while results are in flight (the same measured centred
11
+ // Spinner + text DataTable's own #826 loading state already uses).
12
+ const config = defineRecipe({
13
+ name: "recordpicker",
14
+ base: `display: flex; flex-direction: column; box-sizing: border-box; width: 350px; font-family: ${c.font.sans};
15
+
16
+ & .crisp-recordpicker-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 36px; padding: 0 ${c.space["3"]}; color: ${c.fgMuted}; }
17
+ & .crisp-recordpicker-search svg { flex-shrink: 0; }
18
+ & .crisp-recordpicker-search-input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; color: ${c.fgPrimary}; }
19
+ & .crisp-recordpicker-search-input::placeholder { color: ${c.fgMuted}; }
20
+
21
+ & .crisp-recordpicker-loading { display: flex; align-items: center; justify-content: center; gap: 10px; padding: ${c.space["8"]} ${c.space["3"]}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; }
22
+ & .crisp-recordpicker-empty { padding: ${c.space["4"]} ${c.space["3"]}; text-align: center; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
23
+
24
+ & .crisp-recordpicker-list { display: flex; flex-direction: column; margin: 0; padding: ${c.space["1"]}; list-style: none; max-height: 280px; overflow-y: auto; }
25
+ & .crisp-recordpicker-option { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; box-sizing: border-box; height: 32px; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: start; cursor: pointer; }
26
+ & .crisp-recordpicker-option svg { flex-shrink: 0; color: ${c.fgMuted}; }
27
+ & .crisp-recordpicker-option:hover, & .crisp-recordpicker-option[data-highlighted] { background: ${c.bgRaised}; }`,
28
+ variants: {},
29
+ defaultVariants: {},
30
+ compoundVariants: [],
31
+ });
32
+ export const recordpickerRecipe = resolveRecipe(config);
33
+ export default config;
@@ -1,2 +1,2 @@
1
- export { SavedViews, } from "./savedviews.js";
1
+ export { SavedViews, SavedViewsSaveBar, } from "./savedviews.js";
2
2
  export { savedviewsRecipe } from "./savedviews.recipe.js";
@@ -3,7 +3,7 @@ import { ChevronDown, MoreVertical, Plus, Users2 } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
5
5
  import { useListboxKeyboard } from "../../components/select/index.js";
6
- import { FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
6
+ import { cx, FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
7
7
  import { savedviewsRecipe } from "./savedviews.recipe.js";
8
8
  // #618: the same fallback Select's own `optionText` gives a non-string
9
9
  // `label` — `keywords` first, then the plain string itself, then (rather
@@ -200,3 +200,24 @@ export function SavedViews({ label, icon, views, activeId, onSelect, onCreate, o
200
200
  }, children: [_jsx("span", { className: "crisp-savedviews-row-icon", "aria-hidden": "true", children: _jsx(Plus, { size: 16, strokeWidth: 1.75 }) }), _jsx("span", { className: "crisp-savedviews-row-label", children: createLabel })] })] }) }))] }));
201
201
  }
202
202
  SavedViews.displayName = "SavedViews";
203
+ /**
204
+ * The "unsaved changes" bar for a currently loaded saved view — Attio's own
205
+ * "Discard changes" + a split "Save for everyone ▾" button, measured live
206
+ * (app.attio.com, a saved Companies view with its sort changed). Not a part
207
+ * of `SavedViews`' own trigger/panel: Attio renders this in the page
208
+ * toolbar's own trailing slot, replacing whatever sits there at rest
209
+ * (Import/Export, "+ New …") — the same slot `TableToolbar`'s own
210
+ * `children` prop already documents ("Right-aligned actions after the
211
+ * count"). A consumer swaps this in for that slot's usual content once its
212
+ * own dirty check (live filters/sort/columns vs. the loaded view's
213
+ * snapshot) is true, and swaps it back out once it's false — this
214
+ * component itself carries no dirty state or visibility toggle.
215
+ */
216
+ export function SavedViewsSaveBar({ onSave, onDiscard, onSaveAsNew, discardLabel: discardLabelProp, saveLabel: saveLabelProp, saveAsNewLabel: saveAsNewLabelProp, saveOptionsLabel: saveOptionsLabelProp, className, ...rest }) {
217
+ const discardLabel = useCrispMessage(discardLabelProp, "Discard changes");
218
+ const saveLabel = useCrispMessage(saveLabelProp, "Save for everyone");
219
+ const saveAsNewLabel = useCrispMessage(saveAsNewLabelProp, "Save as new view");
220
+ const saveOptionsLabel = useCrispMessage(saveOptionsLabelProp, "More save options");
221
+ return (_jsxs("div", { className: cx("crisp-savedviews-savebar", className), ...rest, children: [_jsx("button", { type: "button", className: "crisp-savedviews-savebar-discard", onClick: onDiscard, children: discardLabel }), _jsxs("div", { className: "crisp-savedviews-savebar-save-group", children: [_jsx("button", { type: "button", className: "crisp-savedviews-savebar-save", onClick: onSave, children: saveLabel }), onSaveAsNew && (_jsx(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-savedviews-savebar-save-chevron", "aria-label": saveOptionsLabel, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) }), children: _jsx(MenuItem, { onSelect: onSaveAsNew, children: saveAsNewLabel }) }))] })] }));
222
+ }
223
+ SavedViewsSaveBar.displayName = "SavedViewsSaveBar";