@crisp-ui-kit/crisp 0.57.3 → 0.57.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assigneepicker.css +13 -0
- package/dist/assigneepicker.d.ts +7 -0
- package/dist/assigneepicker.layered.css +15 -0
- package/dist/button.css +1 -1
- package/dist/button.layered.css +1 -1
- package/dist/cjs/components/button/button.recipe.js +15 -10
- package/dist/cjs/components/datatable/datatable.js +6 -3
- package/dist/cjs/components/datatable/datatable.recipe.js +35 -2
- package/dist/cjs/components/menu/menu.js +48 -8
- package/dist/cjs/components/multiselect/multiselect.js +3 -0
- package/dist/cjs/components/multiselect/multiselect.recipe.js +18 -0
- package/dist/cjs/components/select/select.js +2 -2
- package/dist/cjs/components/select/select.recipe.js +19 -0
- package/dist/cjs/core/index.js +2 -1
- package/dist/cjs/core/place-floating.js +38 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/product/assigneepicker/assigneepicker.js +127 -0
- package/dist/cjs/product/assigneepicker/assigneepicker.recipe.js +36 -0
- package/dist/cjs/product/assigneepicker/index.js +7 -0
- package/dist/cjs/product/notesgallery/index.js +7 -0
- package/dist/cjs/product/notesgallery/notesgallery.js +80 -0
- package/dist/cjs/product/notesgallery/notesgallery.recipe.js +46 -0
- package/dist/cjs/product/objectpagemenu/index.js +17 -0
- package/dist/cjs/product/objectpagemenu/objectpagemenu.js +26 -0
- package/dist/cjs/product/objectpagemenu/objectpagemenu.recipe.js +30 -0
- package/dist/cjs/product/objectsettings/objectsettings.js +107 -14
- package/dist/cjs/product/recordpage/recordpage.js +64 -5
- package/dist/cjs/product/recordpage/recordpage.recipe.js +38 -0
- package/dist/cjs/product/recordpicker/index.js +7 -0
- package/dist/cjs/product/recordpicker/recordpicker.js +100 -0
- package/dist/cjs/product/recordpicker/recordpicker.recipe.js +36 -0
- package/dist/cjs/product/savedviews/index.js +2 -1
- package/dist/cjs/product/savedviews/savedviews.js +22 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +47 -0
- package/dist/cjs/product/share/index.js +7 -0
- package/dist/cjs/product/share/share.js +179 -0
- package/dist/cjs/product/share/share.recipe.js +114 -0
- package/dist/cjs/product/shortcutssheet/index.js +17 -0
- package/dist/cjs/product/shortcutssheet/shortcutssheet.js +125 -0
- package/dist/cjs/product/shortcutssheet/shortcutssheet.recipe.js +50 -0
- package/dist/cjs/product/taskeditpopover/index.js +7 -0
- package/dist/cjs/product/taskeditpopover/taskeditpopover.js +159 -0
- package/dist/cjs/product/taskeditpopover/taskeditpopover.recipe.js +68 -0
- package/dist/cjs/product/workspaceswitchermenu/index.js +17 -0
- package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.js +38 -0
- package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +34 -0
- package/dist/cjs/tokens/palette.js +27 -0
- package/dist/cjs/tokens/semantic.js +20 -0
- package/dist/datatable.css +40 -7
- package/dist/datatable.layered.css +40 -7
- package/dist/esm/components/button/button.recipe.js +15 -10
- package/dist/esm/components/datatable/datatable.js +6 -3
- package/dist/esm/components/datatable/datatable.recipe.js +35 -2
- package/dist/esm/components/menu/menu.js +47 -7
- package/dist/esm/components/multiselect/multiselect.js +3 -0
- package/dist/esm/components/multiselect/multiselect.recipe.js +18 -0
- package/dist/esm/components/select/select.js +2 -2
- package/dist/esm/components/select/select.recipe.js +19 -0
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/core/place-floating.js +37 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/product/assigneepicker/assigneepicker.js +91 -0
- package/dist/esm/product/assigneepicker/assigneepicker.recipe.js +33 -0
- package/dist/esm/product/assigneepicker/index.js +2 -0
- package/dist/esm/product/notesgallery/index.js +2 -0
- package/dist/esm/product/notesgallery/notesgallery.js +44 -0
- package/dist/esm/product/notesgallery/notesgallery.recipe.js +43 -0
- package/dist/esm/product/objectpagemenu/index.js +1 -0
- package/dist/esm/product/objectpagemenu/objectpagemenu.js +23 -0
- package/dist/esm/product/objectpagemenu/objectpagemenu.recipe.js +27 -0
- package/dist/esm/product/objectsettings/objectsettings.js +106 -13
- package/dist/esm/product/recordpage/recordpage.js +64 -5
- package/dist/esm/product/recordpage/recordpage.recipe.js +38 -0
- package/dist/esm/product/recordpicker/index.js +2 -0
- package/dist/esm/product/recordpicker/recordpicker.js +64 -0
- package/dist/esm/product/recordpicker/recordpicker.recipe.js +33 -0
- package/dist/esm/product/savedviews/index.js +1 -1
- package/dist/esm/product/savedviews/savedviews.js +22 -1
- package/dist/esm/product/savedviews/savedviews.recipe.js +47 -0
- package/dist/esm/product/share/index.js +2 -0
- package/dist/esm/product/share/share.js +143 -0
- package/dist/esm/product/share/share.recipe.js +111 -0
- package/dist/esm/product/shortcutssheet/index.js +1 -0
- package/dist/esm/product/shortcutssheet/shortcutssheet.js +88 -0
- package/dist/esm/product/shortcutssheet/shortcutssheet.recipe.js +47 -0
- package/dist/esm/product/taskeditpopover/index.js +2 -0
- package/dist/esm/product/taskeditpopover/taskeditpopover.js +123 -0
- package/dist/esm/product/taskeditpopover/taskeditpopover.recipe.js +65 -0
- package/dist/esm/product/workspaceswitchermenu/index.js +1 -0
- package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.js +35 -0
- package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +31 -0
- package/dist/esm/tokens/palette.js +27 -0
- package/dist/esm/tokens/semantic.js +20 -0
- package/dist/multiselect.css +18 -0
- package/dist/multiselect.layered.css +18 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +56 -4
- package/dist/notesgallery.css +15 -0
- package/dist/notesgallery.d.ts +7 -0
- package/dist/notesgallery.layered.css +17 -0
- package/dist/objectpagemenu.css +3 -0
- package/dist/objectpagemenu.d.ts +7 -0
- package/dist/objectpagemenu.layered.css +5 -0
- package/dist/recordpage.css +36 -0
- package/dist/recordpage.layered.css +36 -0
- package/dist/recordpicker.css +13 -0
- package/dist/recordpicker.d.ts +7 -0
- package/dist/recordpicker.layered.css +15 -0
- package/dist/savedviews.css +47 -0
- package/dist/savedviews.layered.css +47 -0
- package/dist/select.css +19 -0
- package/dist/select.layered.css +19 -0
- package/dist/share.css +84 -0
- package/dist/share.d.ts +7 -0
- package/dist/share.layered.css +86 -0
- package/dist/shortcutssheet.css +24 -0
- package/dist/shortcutssheet.d.ts +7 -0
- package/dist/shortcutssheet.layered.css +26 -0
- package/dist/styles.css +387 -8
- package/dist/styles.layered.css +387 -8
- package/dist/taskeditpopover.css +36 -0
- package/dist/taskeditpopover.d.ts +7 -0
- package/dist/taskeditpopover.layered.css +38 -0
- package/dist/tokens.css +26 -0
- package/dist/tokens.json +56 -4
- package/dist/types/components/button/button.recipe.d.ts +2 -3
- package/dist/types/components/datatable/datatable.d.ts +8 -0
- package/dist/types/components/menu/menu.d.ts +50 -7
- package/dist/types/components/multiselect/multiselect.d.ts +9 -0
- package/dist/types/components/select/select.d.ts +10 -0
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/core/place-floating.d.ts +37 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/product/assigneepicker/assigneepicker.d.ts +36 -0
- package/dist/types/product/assigneepicker/assigneepicker.recipe.d.ts +3 -0
- package/dist/types/product/assigneepicker/index.d.ts +2 -0
- package/dist/types/product/notesgallery/index.d.ts +2 -0
- package/dist/types/product/notesgallery/notesgallery.d.ts +54 -0
- package/dist/types/product/notesgallery/notesgallery.recipe.d.ts +3 -0
- package/dist/types/product/objectpagemenu/index.d.ts +1 -0
- package/dist/types/product/objectpagemenu/objectpagemenu.d.ts +40 -0
- package/dist/types/product/objectpagemenu/objectpagemenu.recipe.d.ts +17 -0
- package/dist/types/product/objectsettings/objectsettings.d.ts +71 -15
- package/dist/types/product/recordpage/index.d.ts +1 -1
- package/dist/types/product/recordpage/recordpage.d.ts +96 -1
- package/dist/types/product/recordpicker/index.d.ts +2 -0
- package/dist/types/product/recordpicker/recordpicker.d.ts +38 -0
- package/dist/types/product/recordpicker/recordpicker.recipe.d.ts +3 -0
- package/dist/types/product/savedviews/index.d.ts +1 -1
- package/dist/types/product/savedviews/savedviews.d.ts +36 -0
- package/dist/types/product/share/index.d.ts +2 -0
- package/dist/types/product/share/share.d.ts +85 -0
- package/dist/types/product/share/share.recipe.d.ts +3 -0
- package/dist/types/product/shortcutssheet/index.d.ts +1 -0
- package/dist/types/product/shortcutssheet/shortcutssheet.d.ts +49 -0
- package/dist/types/product/shortcutssheet/shortcutssheet.recipe.d.ts +3 -0
- package/dist/types/product/taskeditpopover/index.d.ts +2 -0
- package/dist/types/product/taskeditpopover/taskeditpopover.d.ts +67 -0
- package/dist/types/product/taskeditpopover/taskeditpopover.recipe.d.ts +3 -0
- package/dist/types/product/workspaceswitchermenu/index.d.ts +1 -0
- package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.d.ts +54 -0
- package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.recipe.d.ts +19 -0
- package/dist/types/tokens/contract.d.ts +13 -0
- package/dist/types/tokens/palette.d.ts +52 -0
- package/dist/types/tokens/semantic.d.ts +20 -0
- package/dist/workspaceswitchermenu.css +4 -0
- package/dist/workspaceswitchermenu.d.ts +7 -0
- package/dist/workspaceswitchermenu.layered.css +6 -0
- package/package.json +41 -1
|
@@ -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
|
-
|
|
177
|
-
|
|
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
|
|
293
|
-
* +
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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,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";
|