@crisp-ui-kit/crisp 0.54.3 → 0.54.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 (48) hide show
  1. package/dist/board.css +13 -1
  2. package/dist/board.layered.css +13 -1
  3. package/dist/cjs/components/tabletoolbar/tabletoolbar.js +41 -4
  4. package/dist/cjs/index.js +2 -0
  5. package/dist/cjs/product/board/board.js +60 -2
  6. package/dist/cjs/product/board/board.recipe.js +10 -0
  7. package/dist/cjs/product/comments/comments.js +426 -0
  8. package/dist/cjs/product/comments/comments.recipe.js +126 -0
  9. package/dist/cjs/product/comments/index.js +8 -0
  10. package/dist/cjs/product/listpage/listpage.recipe.js +10 -1
  11. package/dist/cjs/product/objectsettings/index.js +16 -0
  12. package/dist/cjs/product/objectsettings/objectsettings.js +365 -0
  13. package/dist/cjs/product/objectsettings/objectsettings.recipe.js +65 -0
  14. package/dist/cjs/product/taskstable/taskstable.recipe.js +10 -1
  15. package/dist/comments.css +80 -0
  16. package/dist/comments.d.ts +7 -0
  17. package/dist/comments.layered.css +82 -0
  18. package/dist/esm/components/tabletoolbar/tabletoolbar.js +43 -6
  19. package/dist/esm/index.js +2 -0
  20. package/dist/esm/product/board/board.js +61 -3
  21. package/dist/esm/product/board/board.recipe.js +10 -0
  22. package/dist/esm/product/comments/comments.js +388 -0
  23. package/dist/esm/product/comments/comments.recipe.js +123 -0
  24. package/dist/esm/product/comments/index.js +2 -0
  25. package/dist/esm/product/listpage/listpage.recipe.js +10 -1
  26. package/dist/esm/product/objectsettings/index.js +2 -0
  27. package/dist/esm/product/objectsettings/objectsettings.js +322 -0
  28. package/dist/esm/product/objectsettings/objectsettings.recipe.js +62 -0
  29. package/dist/esm/product/taskstable/taskstable.recipe.js +10 -1
  30. package/dist/listpage.css +8 -1
  31. package/dist/listpage.layered.css +8 -1
  32. package/dist/objectsettings.css +49 -0
  33. package/dist/objectsettings.d.ts +7 -0
  34. package/dist/objectsettings.layered.css +51 -0
  35. package/dist/styles.css +160 -3
  36. package/dist/styles.layered.css +160 -3
  37. package/dist/taskstable.css +8 -1
  38. package/dist/taskstable.layered.css +8 -1
  39. package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +8 -2
  40. package/dist/types/index.d.ts +2 -0
  41. package/dist/types/product/board/board.d.ts +20 -1
  42. package/dist/types/product/comments/comments.d.ts +192 -0
  43. package/dist/types/product/comments/comments.recipe.d.ts +3 -0
  44. package/dist/types/product/comments/index.d.ts +2 -0
  45. package/dist/types/product/objectsettings/index.d.ts +2 -0
  46. package/dist/types/product/objectsettings/objectsettings.d.ts +261 -0
  47. package/dist/types/product/objectsettings/objectsettings.recipe.d.ts +3 -0
  48. package/package.json +11 -1
@@ -0,0 +1,322 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Calendar, CheckSquare, Clock, DollarSign, FileText, Flag, Hash, Link2, ListChecks, MapPin, MoreHorizontal, Pencil, Phone, Search, SlidersHorizontal, Sparkles, Star, Type as TypeIcon, User as UserIcon, } from "lucide-react";
3
+ import * as React from "react";
4
+ import { Badge } from "../../components/badge/index.js";
5
+ import { DataTable } from "../../components/datatable/index.js";
6
+ import { FormField } from "../../components/formfield/index.js";
7
+ import { Input } from "../../components/input/index.js";
8
+ import { Menu, MenuItem } from "../../components/menu/index.js";
9
+ import { Select } from "../../components/select/index.js";
10
+ import { Switch } from "../../components/switch/index.js";
11
+ import { Tabs } from "../../components/tabs/index.js";
12
+ import { Textarea } from "../../components/textarea/index.js";
13
+ import { useCrispMessage } from "../../core/index.js";
14
+ import { FormDialog } from "../formdialog/index.js";
15
+ import { SettingsSection } from "../settingssection/index.js";
16
+ import { objectsettingsRecipe } from "./objectsettings.recipe.js";
17
+ /**
18
+ * Settings → Objects (crisp#637, P-122): H1 + subtitle, a search+Filter row,
19
+ * and a DataTable of every object (icon+name, type, record count,
20
+ * attribute count, a per-row "…" menu). Measured live against
21
+ * app.attio.com/<workspace>/settings/data/objects — the plan-gate banner
22
+ * above the table and the "5/3" object-count pill in the "Object" header
23
+ * (both billing-tier chrome) are skipped by decision, not oversight.
24
+ */
25
+ export function ObjectsList({ objects, onOpenObject, onEditObject, onCopyObjectId, onCreateObject, title: titleProp, subtitle: subtitleProp, newObjectLabel: newObjectLabelProp, searchPlaceholder: searchPlaceholderProp, filterLabel: filterLabelProp, objectColumnLabel: objectColumnLabelProp, typeColumnLabel: typeColumnLabelProp, recordsColumnLabel: recordsColumnLabelProp, attributesColumnLabel: attributesColumnLabelProp, noRecordsLabel: noRecordsLabelProp, editLabel: editLabelProp, copyIdLabel: copyIdLabelProp, rowMenuLabel, emptyLabel, className, ...rest }) {
26
+ const title = useCrispMessage(titleProp, "Objects");
27
+ const subtitle = useCrispMessage(subtitleProp, "Modify and add objects in your workspace");
28
+ const newObjectLabel = useCrispMessage(newObjectLabelProp, "New custom object");
29
+ const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search objects");
30
+ const filterLabel = useCrispMessage(filterLabelProp, "Filter");
31
+ const objectColumnLabel = useCrispMessage(objectColumnLabelProp, "Object");
32
+ const typeColumnLabel = useCrispMessage(typeColumnLabelProp, "Type");
33
+ const recordsColumnLabel = useCrispMessage(recordsColumnLabelProp, "Records");
34
+ const attributesColumnLabel = useCrispMessage(attributesColumnLabelProp, "Attributes");
35
+ const noRecordsLabel = useCrispMessage(noRecordsLabelProp, "No records");
36
+ const editLabel = useCrispMessage(editLabelProp, "Edit");
37
+ const copyIdLabel = useCrispMessage(copyIdLabelProp, "Copy ID");
38
+ const [query, setQuery] = React.useState("");
39
+ const filtered = React.useMemo(() => {
40
+ if (!query.trim())
41
+ return objects;
42
+ const q = query.trim().toLowerCase();
43
+ return objects.filter((o) => String(o.name ?? "")
44
+ .toLowerCase()
45
+ .includes(q));
46
+ }, [objects, query]);
47
+ const columns = React.useMemo(() => [
48
+ {
49
+ key: "name",
50
+ header: objectColumnLabel,
51
+ render: (_v, row) => (_jsxs("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { className: "crisp-objectsettings-icon", style: { background: row.iconColor ?? "currentColor" }, children: row.icon }), row.name] })),
52
+ },
53
+ {
54
+ key: "typeLabel",
55
+ header: typeColumnLabel,
56
+ render: (_v, row) => (_jsx("span", { className: "crisp-objectsettings-typebadge", children: row.typeLabel })),
57
+ },
58
+ {
59
+ key: "recordCount",
60
+ header: recordsColumnLabel,
61
+ align: "left",
62
+ render: (_v, row) => row.recordCount ? (row.recordCount) : (_jsx("span", { className: "crisp-objectsettings-muted", children: noRecordsLabel })),
63
+ },
64
+ {
65
+ key: "attributeCount",
66
+ header: attributesColumnLabel,
67
+ },
68
+ {
69
+ key: "actions",
70
+ header: "",
71
+ align: "right",
72
+ render: (_v, row) => (_jsxs(Menu, { align: "end", trigger: _jsx("button", { type: "button", "aria-label": rowMenuLabel?.(row) ?? `${row.name}`, style: {
73
+ display: "inline-flex",
74
+ border: "none",
75
+ background: "none",
76
+ cursor: "pointer",
77
+ color: "inherit",
78
+ padding: 4,
79
+ }, children: _jsx(MoreHorizontal, { size: 16 }) }), children: [_jsx(MenuItem, { icon: _jsx(Pencil, { size: 14 }), onSelect: () => onEditObject?.(row.id), children: editLabel }), _jsx(MenuItem, { onSelect: () => onCopyObjectId?.(row.id), children: copyIdLabel })] })),
80
+ },
81
+ ], [
82
+ objectColumnLabel,
83
+ typeColumnLabel,
84
+ recordsColumnLabel,
85
+ attributesColumnLabel,
86
+ noRecordsLabel,
87
+ editLabel,
88
+ copyIdLabel,
89
+ onEditObject,
90
+ onCopyObjectId,
91
+ rowMenuLabel,
92
+ ]);
93
+ return (_jsx("div", { className: objectsettingsRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-objectsettings-page", children: [_jsxs("div", { className: "crisp-objectsettings-head", children: [_jsxs("div", { className: "crisp-objectsettings-heading", children: [_jsx("h1", { className: "crisp-objectsettings-title", children: title }), _jsx("p", { className: "crisp-objectsettings-subtitle", children: subtitle })] }), onCreateObject && (_jsx("button", { type: "button", className: "crisp-objectsettings-filterbtn", onClick: onCreateObject, children: newObjectLabel }))] }), _jsxs("div", { className: "crisp-objectsettings-toolbar", children: [_jsxs("span", { className: "crisp-objectsettings-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchPlaceholder })] }), _jsxs("button", { type: "button", className: "crisp-objectsettings-filterbtn", children: [_jsx(SlidersHorizontal, { size: 14 }), filterLabel] })] }), _jsx(DataTable, { columns: columns, data: filtered, onRowClick: onOpenObject ? (row) => onOpenObject(row.id) : undefined, empty: emptyLabel })] }) }));
94
+ }
95
+ ObjectsList.displayName = "ObjectsList";
96
+ /**
97
+ * Settings → Objects → an object (crisp#637, P-123, verified live against
98
+ * .../objects/companies/general): 48px icon tile + name + type badge, a
99
+ * description line, then a Configuration/Attributes tab bar (Attio's own
100
+ * tab bar also carries Permissions/Templates/Notifications/Imports — out of
101
+ * scope here, see #613/#638; only the two rows this task covers are built).
102
+ * Tab content is a render prop, the same shape crisp's own `Tabs` already
103
+ * takes, so a consumer composes `ObjectGeneralSection`/
104
+ * `ObjectAppearanceSection` and `AttributesTable` underneath.
105
+ */
106
+ export function ObjectDetailPage({ icon, iconColor, name, typeLabel, description, tab, defaultTab = "configuration", onTabChange, attributeCount, configurationLabel: configurationLabelProp, attributesLabel: attributesLabelProp, children, }) {
107
+ const configurationLabel = useCrispMessage(configurationLabelProp, "Configuration");
108
+ const attributesLabel = useCrispMessage(attributesLabelProp, "Attributes");
109
+ const [internalTab, setInternalTab] = React.useState(defaultTab);
110
+ const active = tab ?? internalTab;
111
+ const setTab = (v) => {
112
+ const next = v;
113
+ if (tab === undefined)
114
+ setInternalTab(next);
115
+ onTabChange?.(next);
116
+ };
117
+ return (_jsx("div", { className: objectsettingsRecipe(), children: _jsxs("div", { className: "crisp-objectsettings-page", children: [_jsx("div", { className: "crisp-objectsettings-head", children: _jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 }, children: [_jsx("span", { className: "crisp-objectsettings-icon crisp-objectsettings-icon--lg", style: { background: iconColor ?? "currentColor" }, children: icon }), _jsxs("div", { className: "crisp-objectsettings-heading", children: [_jsxs("h1", { className: "crisp-objectsettings-title", children: [name, typeLabel != null && (_jsx(Badge, { tone: "neutral", size: "small", children: typeLabel }))] }), description != null && (_jsx("p", { className: "crisp-objectsettings-subtitle", children: description }))] })] }) }), _jsx(Tabs, { value: active, onValueChange: setTab, tabs: [
118
+ { value: "configuration", label: configurationLabel },
119
+ {
120
+ value: "attributes",
121
+ label: attributesLabel,
122
+ badge: attributeCount,
123
+ },
124
+ ], children: (v) => children(v) })] }) }));
125
+ }
126
+ ObjectDetailPage.displayName = "ObjectDetailPage";
127
+ /** The Configuration tab's "General" section (crisp#637, P-123): plural/
128
+ * singular noun inputs side by side, then a slug input Attio locks for
129
+ * system objects. Composes `SettingsSection` + `FormField`, no new chrome. */
130
+ export function ObjectGeneralSection({ pluralNoun, onPluralNounChange, singularNoun, onSingularNounChange, slug, slugEditable = false, onSlugChange, titleLabel: titleLabelProp, descriptionLabel: descriptionLabelProp, pluralLabel: pluralLabelProp, singularLabel: singularLabelProp, slugLabel: slugLabelProp, slugHelpLabel: slugHelpLabelProp, }) {
131
+ const titleLabel = useCrispMessage(titleLabelProp, "General");
132
+ const descriptionLabel = useCrispMessage(descriptionLabelProp, "Set words to describe a single and multiple objects of this type");
133
+ const pluralLabel = useCrispMessage(pluralLabelProp, "Plural noun");
134
+ const singularLabel = useCrispMessage(singularLabelProp, "Singular noun");
135
+ const slugLabel = useCrispMessage(slugLabelProp, "Slug");
136
+ const slugHelpLabel = useCrispMessage(slugHelpLabelProp, "You can't change the slug of a system object");
137
+ const pluralId = React.useId();
138
+ const singularId = React.useId();
139
+ const slugId = React.useId();
140
+ return (_jsxs(SettingsSection, { title: titleLabel, description: descriptionLabel, children: [_jsxs("div", { className: "crisp-objectsettings-fieldrow", children: [_jsx(FormField, { label: pluralLabel, htmlFor: pluralId, children: _jsx(Input, { id: pluralId, value: pluralNoun, onChange: (e) => onPluralNounChange?.(e.target.value) }) }), _jsx(FormField, { label: singularLabel, htmlFor: singularId, children: _jsx(Input, { id: singularId, value: singularNoun, onChange: (e) => onSingularNounChange?.(e.target.value) }) })] }), _jsx(FormField, { label: slugLabel, htmlFor: slugId, description: !slugEditable ? slugHelpLabel : undefined, children: _jsx(Input, { id: slugId, value: slug, disabled: !slugEditable, onChange: (e) => onSlugChange?.(e.target.value) }) })] }));
141
+ }
142
+ ObjectGeneralSection.displayName = "ObjectGeneralSection";
143
+ /** The Configuration tab's "Appearance" section (crisp#637, P-123): a single
144
+ * "Record page layout" row with a "Configure record page" button — the
145
+ * target flow itself (crisp's own record-page-layout editor) is a separate,
146
+ * unbuilt surface; this only renders the row and its click. */
147
+ export function ObjectAppearanceSection({ onConfigureRecordPage, titleLabel: titleLabelProp, rowLabel: rowLabelProp, rowDescriptionLabel: rowDescriptionLabelProp, actionLabel: actionLabelProp, }) {
148
+ const titleLabel = useCrispMessage(titleLabelProp, "Appearance");
149
+ const rowLabel = useCrispMessage(rowLabelProp, "Record page layout");
150
+ const rowDescriptionLabel = useCrispMessage(rowDescriptionLabelProp, "Configure tabs, widgets, and layout for this object's record pages");
151
+ const actionLabel = useCrispMessage(actionLabelProp, "Configure record page");
152
+ return (_jsx(SettingsSection, { title: titleLabel, children: _jsxs("div", { className: "crisp-objectsettings-fieldrow", style: { alignItems: "center" }, children: [_jsxs("div", { children: [_jsx("div", { style: { fontWeight: 500 }, children: rowLabel }), _jsx("p", { className: "crisp-objectsettings-subtitle", children: rowDescriptionLabel })] }), onConfigureRecordPage && (_jsx("button", { type: "button", className: "crisp-objectsettings-filterbtn", style: { justifySelf: "end" }, onClick: onConfigureRecordPage, children: actionLabel }))] }) }));
153
+ }
154
+ ObjectAppearanceSection.displayName = "ObjectAppearanceSection";
155
+ /**
156
+ * The Attributes tab's table (crisp#637, P-124, verified live): H2 +
157
+ * "Create attribute", a search+Filter row, then a DataTable of every
158
+ * attribute — drag handle (via `DataTable`'s own `onRowsReorder`) + type
159
+ * icon + name, Data type, a constraint label, and System (grey)/Enriched
160
+ * (purple) property badges. Row height is DataTable's own 36px; Attio's
161
+ * measures 40px here — a real, small divergence left to DataTable's owner
162
+ * rather than patched locally (see the PR).
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, }) {
165
+ const title = useCrispMessage(titleProp, "Attributes");
166
+ const subtitle = useCrispMessage(subtitleProp, "Modify and add object attributes");
167
+ const createLabel = useCrispMessage(createLabelProp, "Create attribute");
168
+ const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search attributes");
169
+ const filterLabel = useCrispMessage(filterLabelProp, "Filter");
170
+ const nameColumnLabel = useCrispMessage(nameColumnLabelProp, "Name");
171
+ const dataTypeColumnLabel = useCrispMessage(dataTypeColumnLabelProp, "Data type");
172
+ const constraintsColumnLabel = useCrispMessage(constraintsColumnLabelProp, "Constraints");
173
+ const propertiesColumnLabel = useCrispMessage(propertiesColumnLabelProp, "Properties");
174
+ const systemLabel = useCrispMessage(systemLabelProp, "System");
175
+ const enrichedLabel = useCrispMessage(enrichedLabelProp, "Enriched");
176
+ const editLabel = useCrispMessage(editLabelProp, "Edit");
177
+ const archiveLabel = useCrispMessage(archiveLabelProp, "Archive");
178
+ const [query, setQuery] = React.useState("");
179
+ const filtered = React.useMemo(() => {
180
+ if (!query.trim())
181
+ return attributes;
182
+ const q = query.trim().toLowerCase();
183
+ return attributes.filter((a) => String(a.name ?? "")
184
+ .toLowerCase()
185
+ .includes(q));
186
+ }, [attributes, query]);
187
+ const columns = React.useMemo(() => [
188
+ {
189
+ key: "name",
190
+ header: nameColumnLabel,
191
+ render: (_v, row) => (_jsxs("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [_jsx("span", { style: {
192
+ display: "inline-flex",
193
+ color: "var(--crisp-fg-muted, inherit)",
194
+ }, children: row.icon }), row.name] })),
195
+ },
196
+ { key: "dataType", header: dataTypeColumnLabel },
197
+ {
198
+ key: "constraint",
199
+ header: constraintsColumnLabel,
200
+ render: (_v, row) => row.constraint ?? null,
201
+ },
202
+ {
203
+ key: "properties",
204
+ header: propertiesColumnLabel,
205
+ render: (_v, row) => (_jsxs("span", { style: { display: "flex", gap: 6 }, children: [row.system && (_jsx(Badge, { tone: "neutral", size: "small", children: systemLabel })), row.enriched && (_jsx(Badge, { tone: "purple", size: "small", children: enrichedLabel }))] })),
206
+ },
207
+ {
208
+ key: "actions",
209
+ header: "",
210
+ align: "right",
211
+ render: (_v, row) => (_jsx(Menu, { align: "end", trigger: _jsx("button", { type: "button", "aria-label": `${row.name}`, style: {
212
+ display: "inline-flex",
213
+ border: "none",
214
+ background: "none",
215
+ cursor: "pointer",
216
+ color: "inherit",
217
+ 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 })] })) })),
219
+ },
220
+ ], [
221
+ nameColumnLabel,
222
+ dataTypeColumnLabel,
223
+ constraintsColumnLabel,
224
+ propertiesColumnLabel,
225
+ systemLabel,
226
+ enrichedLabel,
227
+ editLabel,
228
+ archiveLabel,
229
+ onEditAttribute,
230
+ onArchiveAttribute,
231
+ rowActions,
232
+ ]);
233
+ return (_jsx("div", { className: objectsettingsRecipe(), children: _jsxs("div", { className: "crisp-objectsettings-page", children: [_jsxs("div", { className: "crisp-objectsettings-head", children: [_jsxs("div", { className: "crisp-objectsettings-heading", children: [_jsx("h2", { className: "crisp-objectsettings-title", style: { fontSize: 16, lineHeight: "20px" }, children: title }), _jsx("p", { className: "crisp-objectsettings-subtitle", children: subtitle })] }), onCreateAttribute && (_jsx("button", { type: "button", className: "crisp-objectsettings-filterbtn", onClick: onCreateAttribute, children: createLabel }))] }), _jsxs("div", { className: "crisp-objectsettings-toolbar", children: [_jsxs("span", { className: "crisp-objectsettings-search", children: [_jsx(Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), _jsx("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchPlaceholder })] }), _jsxs("button", { type: "button", className: "crisp-objectsettings-filterbtn", children: [_jsx(SlidersHorizontal, { size: 14 }), filterLabel] })] }), _jsx(DataTable, { columns: columns, data: filtered, onRowClick: onOpenAttribute ? (row) => onOpenAttribute(row.id) : undefined, onRowsReorder: onReorder, empty: emptyLabel })] }) }));
234
+ }
235
+ AttributesTable.displayName = "AttributesTable";
236
+ /** Attio's own "Type" group, verbatim (crisp#637, P-126) — icons are
237
+ * crisp's Lucide choices, not Attio's proprietary set (golden rule 4). */
238
+ export const ATTRIBUTE_TYPE_OPTIONS = [
239
+ { value: "checkbox", label: "Checkbox", icon: CheckSquare },
240
+ { value: "currency", label: "Currency", icon: DollarSign },
241
+ { value: "date", label: "Date", icon: Calendar },
242
+ { value: "location", label: "Location", icon: MapPin },
243
+ { value: "multiselect", label: "Multi-select", icon: ListChecks },
244
+ { value: "number", label: "Number", icon: Hash },
245
+ { value: "phone", label: "Phone Number", icon: Phone },
246
+ { value: "rating", label: "Rating", icon: Star },
247
+ { value: "record", label: "Record", icon: FileText },
248
+ { value: "relationship", label: "Relationship", icon: Link2, badge: "Pro" },
249
+ { value: "select", label: "Select", icon: ListChecks },
250
+ { value: "status", label: "Status", icon: Flag },
251
+ { value: "text", label: "Text", icon: TypeIcon },
252
+ { value: "timestamp", label: "Timestamp", icon: Clock },
253
+ { value: "user", label: "User", icon: UserIcon },
254
+ ];
255
+ /** Attio's own "AI Autofill" group, verbatim (crisp#637, P-126) — opt in via
256
+ * `AttributeTypeSelect`'s `aiAutofillOptions`; omitted by default, since a
257
+ * consumer with no autofill backend of its own (GRC) has nothing to wire
258
+ * these to. */
259
+ export const ATTRIBUTE_AI_AUTOFILL_OPTIONS = [
260
+ { value: "classify-record", label: "Classify record", icon: Sparkles },
261
+ { value: "prompt-completion", label: "Prompt completion", icon: Sparkles },
262
+ { value: "summarize-record", label: "Summarize record", icon: Sparkles },
263
+ { value: "web-agent", label: "Web agent", icon: Sparkles },
264
+ ];
265
+ function toSelectOption(o, group) {
266
+ const Icon = o.icon;
267
+ return {
268
+ value: o.value,
269
+ keywords: o.label,
270
+ group,
271
+ label: (_jsxs("span", { className: "crisp-objectsettings-typeoption", children: [_jsx(Icon, { size: 14 }), _jsx("span", { className: "crisp-objectsettings-typeoption-label", children: o.label }), o.badge != null && (_jsx("span", { className: "crisp-objectsettings-typeoption-badge", children: o.badge }))] })),
272
+ };
273
+ }
274
+ /** The "Attribute Type" dropdown (crisp#637, P-126, verified live): a
275
+ * searchable, grouped `Select` — reused as-is, not a new picker primitive.
276
+ * Grouping and the per-item trailing "Pro" badge are composed through
277
+ * `Select`'s existing `group` field and rich `label` ReactNode. */
278
+ export function AttributeTypeSelect({ value, onValueChange, options = ATTRIBUTE_TYPE_OPTIONS, aiAutofillOptions, aiAutofillGroupLabel: aiAutofillGroupLabelProp, typeGroupLabel: typeGroupLabelProp, searchPlaceholder: searchPlaceholderProp, id, }) {
279
+ const aiAutofillGroupLabel = useCrispMessage(aiAutofillGroupLabelProp, "AI Autofill");
280
+ const typeGroupLabel = useCrispMessage(typeGroupLabelProp, "Type");
281
+ const searchPlaceholder = useCrispMessage(searchPlaceholderProp, "Search attribute types...");
282
+ const selectOptions = React.useMemo(() => {
283
+ const ai = (aiAutofillOptions ?? []).map((o) => toSelectOption(o, aiAutofillGroupLabel));
284
+ const type = options.map((o) => toSelectOption(o, typeGroupLabel));
285
+ return [...ai, ...type];
286
+ }, [aiAutofillOptions, options, aiAutofillGroupLabel, typeGroupLabel]);
287
+ return (_jsx(Select, { id: id, options: selectOptions, value: value, onValueChange: onValueChange, searchable: true, searchPlaceholder: searchPlaceholder, width: "full" }));
288
+ }
289
+ AttributeTypeSelect.displayName = "AttributeTypeSelect";
290
+ /**
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.
298
+ */
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, }) {
300
+ const title = useCrispMessage(titleProp, "Create attribute");
301
+ const attributeTypeLabel = useCrispMessage(attributeTypeLabelProp, "Attribute Type");
302
+ const nameLabel = useCrispMessage(nameLabelProp, "Name");
303
+ const descriptionLabel = useCrispMessage(descriptionLabelProp, "Description");
304
+ const descriptionPlaceholder = useCrispMessage(descriptionPlaceholderProp, "Add a description for this attribute");
305
+ const aiAutofillLabel = useCrispMessage(aiAutofillLabelProp, "AI autofill");
306
+ const setUpAiAutofillLabel = useCrispMessage(setUpAiAutofillLabelProp, "Set up AI autofill");
307
+ const createLabel = useCrispMessage(createLabelProp, "Create attribute");
308
+ const switchId = React.useId();
309
+ 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 })] })] })] }));
311
+ }
312
+ CreateAttributeDialog.displayName = "CreateAttributeDialog";
313
+ /**
314
+ * This folder holds several components (`ObjectsList`, `ObjectDetailPage`,
315
+ * `AttributesTable`, `CreateAttributeDialog`, …), not one named after the
316
+ * slug — unlike every other product dir, whose docs `<ManualInstallation>`
317
+ * resolves an import binding from the folder slug ("avatargroup" →
318
+ * `AvatarGroup`) by reading this file's exports. This alias exists only so
319
+ * that resolver finds a match for "objectsettings"; consumers should import
320
+ * the real names above, never this one.
321
+ */
322
+ export const ObjectSettings = ObjectsList;
@@ -0,0 +1,62 @@
1
+ import { defineRecipe, resolveRecipe } from "../../core/index.js";
2
+ import { contract as c } from "../../tokens/index.js";
3
+ // Measured live against app.attio.com/<workspace>/settings/data/objects and
4
+ // .../objects/companies/{general,attributes} (crisp#637, P-122/P-123/P-124/
5
+ // P-125/P-126). Values with no matching scale token are one-off literals,
6
+ // same precedent as authshell's own 20px title (crisp#637 golden rule 3):
7
+ // the 24/600/28/-0.48 page title, the 12px/16px badge dot sizes, and the
8
+ // 30% squircle radius on the object-icon tile.
9
+ const config = defineRecipe({
10
+ name: "objectsettings",
11
+ base: `& .crisp-objectsettings-page { display: flex; flex-direction: column; gap: ${c.space["6"]}; }
12
+ & .crisp-objectsettings-head { display: flex; align-items: flex-start; justify-content: space-between; gap: ${c.space["4"]}; }
13
+ & .crisp-objectsettings-heading { display: flex; flex-direction: column; gap: ${c.space["0.5"]}; min-width: 0; }
14
+ & .crisp-objectsettings-title { display: flex; align-items: center; gap: ${c.space["2"]}; font-size: 24px; line-height: 28px; font-weight: ${c.weight.semibold}; letter-spacing: -0.48px; color: ${c.fgPrimary}; margin: 0; }
15
+ & .crisp-objectsettings-subtitle { font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; margin: 0; }
16
+
17
+ & .crisp-objectsettings-toolbar { display: flex; align-items: center; gap: ${c.space["2"]}; }
18
+ & .crisp-objectsettings-search { display: flex; align-items: center; flex: 1; gap: ${c.space["1.5"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2.5"]}; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderDefault}; color: ${c.fgMuted}; transition: box-shadow .15s; }
19
+ & .crisp-objectsettings-search:focus-within { box-shadow: inset 0 0 0 1px ${c.bgBrandSolid}; }
20
+ & .crisp-objectsettings-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; }
21
+ & .crisp-objectsettings-search input::placeholder { color: ${c.fgMuted}; }
22
+ & .crisp-objectsettings-filterbtn { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2.5"]} 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.lg}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px ${c.borderDefault}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; cursor: pointer; white-space: nowrap; }
23
+ & .crisp-objectsettings-filterbtn:hover { background: ${c.bgRaised}; }
24
+
25
+ /* Object / attribute icon tile — Attio's own squircle (measured
26
+ border-radius: 30%, not a scale token: it's a proportional radius
27
+ tied to the tile's own size, unlike every other radius crisp uses,
28
+ which is a fixed px). */
29
+ & .crisp-objectsettings-icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 16px; height: 16px; border-radius: 30%; color: ${c.fgOnBrand}; }
30
+ & .crisp-objectsettings-icon--lg { width: 48px; height: 48px; border-radius: 16px; }
31
+
32
+ /* "Standard"/"Custom" type badge and the muted "No records" cell —
33
+ plain text, not a Badge pill (measured: transparent background). */
34
+ & .crisp-objectsettings-typebadge { font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgNeutralSoft}; }
35
+ & .crisp-objectsettings-muted { color: ${c.fgMuted}; }
36
+
37
+ /* Attribute-type option row (icon + label + optional trailing badge),
38
+ composed as a Select option's \`label\` ReactNode. */
39
+ & .crisp-objectsettings-typeoption { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; min-width: 0; }
40
+ & .crisp-objectsettings-typeoption-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
41
+ & .crisp-objectsettings-typeoption-badge { flex-shrink: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${c.fgBrandSoft}; }
42
+
43
+ /* Create-attribute dialog's AI-autofill row and the submit button's
44
+ ⌘↵ hint (measured off the same dialog, same shape as Dialog's own
45
+ .crisp-dialog-submit-key, kept local rather than reaching into
46
+ another component's scoped classes). */
47
+ & .crisp-objectsettings-aiautofill { display: flex; flex-direction: column; gap: ${c.space["1.5"]}; }
48
+ & .crisp-objectsettings-aiautofill-label { font-size: ${c.text.md}; font-weight: ${c.weight.medium}; color: ${c.fgMuted}; }
49
+ & .crisp-objectsettings-aiautofill-row { display: flex; align-items: center; gap: ${c.space["2"]}; height: ${c.size.menuRow}; padding: 0 ${c.space["2.5"]}; border-radius: ${c.radius.md}; box-shadow: inset 0 0 0 1px ${c.borderDefault}; font-size: ${c.text.lg}; color: ${c.fgPrimary}; cursor: pointer; }
50
+ & .crisp-objectsettings-aiautofill-row > svg { flex-shrink: 0; color: ${c.fgMuted}; }
51
+ & .crisp-objectsettings-aiautofill-row > span { flex: 1; }
52
+ & .crisp-objectsettings-kbd { display: inline-flex; align-items: center; gap: ${c.space["1"]}; margin-inline-start: ${c.space["1"]}; }
53
+ & .crisp-objectsettings-kbd kbd { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0 3px; border-radius: 4px; background: transparent; color: inherit; font-family: inherit; font-style: normal; font-size: 10px; }
54
+
55
+ & .crisp-objectsettings-fieldrow { display: grid; grid-template-columns: 1fr 1fr; gap: ${c.space["4"]}; }
56
+ `,
57
+ variants: {},
58
+ defaultVariants: {},
59
+ compoundVariants: [],
60
+ });
61
+ export const objectsettingsRecipe = resolveRecipe(config);
62
+ export default config;
@@ -23,7 +23,16 @@ const config = defineRecipe({
23
23
  & .crisp-taskstable-colhead-cell { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; min-width: 0; }
24
24
 
25
25
  & .crisp-taskstable-row { display: flex; align-items: center; gap: ${c.space["1.5"]}; }
26
- & .crisp-taskstable-cell { display: flex; align-items: center; min-width: 0; }
26
+ /* #642: measured live -- title, due date and assignee name are ALL
27
+ 14px/500 on Attio's own Tasks page. Only -title/-due set their own
28
+ font-size (and only -title its own weight), so an unstyled consumer
29
+ slot (a plain PersonChip/RecordChip name span with no font-size of
30
+ its own) fell through to the browser/body default, 16px/400. Set
31
+ the ambient font here so it cascades to any slot content that
32
+ doesn't override it -- -title/-due's own declarations still apply,
33
+ unchanged, they're just now redundant with this baseline rather
34
+ than the only source of it. */
35
+ & .crisp-taskstable-cell { display: flex; align-items: center; min-width: 0; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; }
27
36
 
28
37
  & .crisp-taskstable-check { appearance: none; -webkit-appearance: none; box-sizing: border-box; flex-shrink: 0; margin: 0; width: 16px; height: 16px; border: none; border-radius: ${c.radius.full}; background: ${c.bgSurface}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 9%, transparent); cursor: pointer; transition: background-color .12s, box-shadow .12s; }
29
38
  & .crisp-taskstable-check:hover { box-shadow: inset 0 0 0 1px color-mix(in srgb, ${c.fgPrimary} 30%, transparent); }
package/dist/listpage.css CHANGED
@@ -3,7 +3,14 @@
3
3
  & .crisp-listpage-toolbar { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-header); flex-shrink: 0; padding: 0 var(--crisp-space-3); }
4
4
  & .crisp-listpage-toolbar-lead { display: flex; align-items: center; gap: var(--crisp-space-2); }
5
5
  & .crisp-listpage-count { margin-inline-start: auto; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); white-space: nowrap; }
6
- & .crisp-listpage-toolbar-trail { display: flex; align-items: center; gap: var(--crisp-space-2); }
6
+ /* #641: trailing (View settings / New) has to reach the toolbar's own
7
+ inline-end edge on its own -- the only thing pushing it there used
8
+ to be .crisp-listpage-count's margin-inline-start: auto, which a consumer with no count badge (TasksTable, measured live: trailing sat right after leading, nowhere near the row's own right edge)
9
+ never renders. A bare margin-inline-start: auto here is safely
10
+ overridden by the more specific rule right below whenever count IS
11
+ present, so the two cases don't fight -- count's own auto margin
12
+ still does the pushing there, this one only fires when it's absent. */
13
+ & .crisp-listpage-toolbar-trail { display: flex; align-items: center; gap: var(--crisp-space-2); margin-inline-start: auto; }
7
14
  & .crisp-listpage-count + .crisp-listpage-toolbar-trail { margin-inline-start: var(--crisp-space-3); }
8
15
  & .crisp-listpage-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--crisp-space-3) var(--crisp-space-3); }
9
16
  & .crisp-listpage-section-label { padding: var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -4,7 +4,14 @@
4
4
  & .crisp-listpage-toolbar { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-header); flex-shrink: 0; padding: 0 var(--crisp-space-3); }
5
5
  & .crisp-listpage-toolbar-lead { display: flex; align-items: center; gap: var(--crisp-space-2); }
6
6
  & .crisp-listpage-count { margin-inline-start: auto; font-size: var(--crisp-text-md); color: var(--crisp-fg-muted); white-space: nowrap; }
7
- & .crisp-listpage-toolbar-trail { display: flex; align-items: center; gap: var(--crisp-space-2); }
7
+ /* #641: trailing (View settings / New) has to reach the toolbar's own
8
+ inline-end edge on its own -- the only thing pushing it there used
9
+ to be .crisp-listpage-count's margin-inline-start: auto, which a consumer with no count badge (TasksTable, measured live: trailing sat right after leading, nowhere near the row's own right edge)
10
+ never renders. A bare margin-inline-start: auto here is safely
11
+ overridden by the more specific rule right below whenever count IS
12
+ present, so the two cases don't fight -- count's own auto margin
13
+ still does the pushing there, this one only fires when it's absent. */
14
+ & .crisp-listpage-toolbar-trail { display: flex; align-items: center; gap: var(--crisp-space-2); margin-inline-start: auto; }
8
15
  & .crisp-listpage-count + .crisp-listpage-toolbar-trail { margin-inline-start: var(--crisp-space-3); }
9
16
  & .crisp-listpage-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--crisp-space-3) var(--crisp-space-3); }
10
17
  & .crisp-listpage-section-label { padding: var(--crisp-space-2) var(--crisp-space-1); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
@@ -0,0 +1,49 @@
1
+ .crisp-objectsettings {
2
+ & .crisp-objectsettings-page { display: flex; flex-direction: column; gap: var(--crisp-space-6); }
3
+ & .crisp-objectsettings-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--crisp-space-4); }
4
+ & .crisp-objectsettings-heading { display: flex; flex-direction: column; gap: var(--crisp-space-0-5); min-width: 0; }
5
+ & .crisp-objectsettings-title { display: flex; align-items: center; gap: var(--crisp-space-2); font-size: 24px; line-height: 28px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.48px; color: var(--crisp-fg-primary); margin: 0; }
6
+ & .crisp-objectsettings-subtitle { font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); margin: 0; }
7
+ & .crisp-objectsettings-toolbar { display: flex; align-items: center; gap: var(--crisp-space-2); }
8
+ & .crisp-objectsettings-search { display: flex; align-items: center; flex: 1; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2-5); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-default); color: var(--crisp-fg-muted); transition: box-shadow .15s; }
9
+ & .crisp-objectsettings-search:focus-within { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
10
+ & .crisp-objectsettings-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
11
+ & .crisp-objectsettings-search input::placeholder { color: var(--crisp-fg-muted); }
12
+ & .crisp-objectsettings-filterbtn { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2-5) 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-lg); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-default); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); cursor: pointer; white-space: nowrap; }
13
+ & .crisp-objectsettings-filterbtn:hover { background: var(--crisp-bg-raised); }
14
+ /* Object / attribute icon tile — Attio's own squircle (measured
15
+ border-radius: 30%, not a scale token: it's a proportional radius tied to the tile's own size, unlike every other radius crisp uses,
16
+ which is a fixed px). */
17
+ & .crisp-objectsettings-icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 16px; height: 16px; border-radius: 30%; color: var(--crisp-fg-on-brand); }
18
+ & .crisp-objectsettings-icon--lg { width: 48px; height: 48px; border-radius: 16px; }
19
+
20
+ /* "Standard"/"Custom" type badge and the muted "No records" cell —
21
+ plain text, not a Badge pill (measured: transparent background). */
22
+ & .crisp-objectsettings-typebadge { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-neutral-soft); }
23
+ & .crisp-objectsettings-muted { color: var(--crisp-fg-muted); }
24
+
25
+ /* Attribute-type option row (icon + label + optional trailing badge),
26
+ composed as a Select option's `label` ReactNode. */
27
+ & .crisp-objectsettings-typeoption { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; min-width: 0; }
28
+ & .crisp-objectsettings-typeoption-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
29
+ & .crisp-objectsettings-typeoption-badge { flex-shrink: 0; font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-brand-soft); }
30
+ /* Create-attribute dialog's AI-autofill row and the submit button's ⌘↵ hint (measured off the same dialog, same shape as Dialog's own
31
+ .crisp-dialog-submit-key, kept local rather than reaching into
32
+ another component's scoped classes). */
33
+ & .crisp-objectsettings-aiautofill { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); }
34
+ & .crisp-objectsettings-aiautofill-label { font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
35
+ & .crisp-objectsettings-aiautofill-row { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2-5); border-radius: var(--crisp-radius-md); box-shadow: inset 0 0 0 1px var(--crisp-border-default); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; }
36
+ & .crisp-objectsettings-aiautofill-row > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
37
+ & .crisp-objectsettings-aiautofill-row > span { flex: 1; }
38
+ & .crisp-objectsettings-kbd { display: inline-flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: var(--crisp-space-1); }
39
+ & .crisp-objectsettings-kbd kbd { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0 3px; border-radius: 4px; background: transparent; color: inherit; font-family: inherit; font-style: normal; font-size: 10px; }
40
+ & .crisp-objectsettings-fieldrow { display: grid; grid-template-columns: 1fr 1fr; gap: var(--crisp-space-4); }
41
+ }
42
+
43
+ @media (prefers-reduced-motion: reduce) {
44
+ .crisp-objectsettings, .crisp-objectsettings * {
45
+ animation-duration: 0.01ms !important;
46
+ animation-iteration-count: 1 !important;
47
+ transition-duration: 0.01ms !important;
48
+ }
49
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface ObjectsettingsVariants {
4
+
5
+ }
6
+
7
+ export declare const objectsettingsRecipe: RecipeFn<{ }>;
@@ -0,0 +1,51 @@
1
+ @layer crisp-components {
2
+ .crisp-objectsettings {
3
+ & .crisp-objectsettings-page { display: flex; flex-direction: column; gap: var(--crisp-space-6); }
4
+ & .crisp-objectsettings-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--crisp-space-4); }
5
+ & .crisp-objectsettings-heading { display: flex; flex-direction: column; gap: var(--crisp-space-0-5); min-width: 0; }
6
+ & .crisp-objectsettings-title { display: flex; align-items: center; gap: var(--crisp-space-2); font-size: 24px; line-height: 28px; font-weight: var(--crisp-weight-semibold); letter-spacing: -0.48px; color: var(--crisp-fg-primary); margin: 0; }
7
+ & .crisp-objectsettings-subtitle { font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); margin: 0; }
8
+ & .crisp-objectsettings-toolbar { display: flex; align-items: center; gap: var(--crisp-space-2); }
9
+ & .crisp-objectsettings-search { display: flex; align-items: center; flex: 1; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2-5); border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-default); color: var(--crisp-fg-muted); transition: box-shadow .15s; }
10
+ & .crisp-objectsettings-search:focus-within { box-shadow: inset 0 0 0 1px var(--crisp-bg-brand-solid); }
11
+ & .crisp-objectsettings-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); }
12
+ & .crisp-objectsettings-search input::placeholder { color: var(--crisp-fg-muted); }
13
+ & .crisp-objectsettings-filterbtn { display: inline-flex; align-items: center; gap: var(--crisp-space-1-5); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2-5) 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-lg); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-border-default); color: var(--crisp-fg-primary); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); cursor: pointer; white-space: nowrap; }
14
+ & .crisp-objectsettings-filterbtn:hover { background: var(--crisp-bg-raised); }
15
+ /* Object / attribute icon tile — Attio's own squircle (measured
16
+ border-radius: 30%, not a scale token: it's a proportional radius tied to the tile's own size, unlike every other radius crisp uses,
17
+ which is a fixed px). */
18
+ & .crisp-objectsettings-icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 16px; height: 16px; border-radius: 30%; color: var(--crisp-fg-on-brand); }
19
+ & .crisp-objectsettings-icon--lg { width: 48px; height: 48px; border-radius: 16px; }
20
+
21
+ /* "Standard"/"Custom" type badge and the muted "No records" cell —
22
+ plain text, not a Badge pill (measured: transparent background). */
23
+ & .crisp-objectsettings-typebadge { font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-neutral-soft); }
24
+ & .crisp-objectsettings-muted { color: var(--crisp-fg-muted); }
25
+
26
+ /* Attribute-type option row (icon + label + optional trailing badge),
27
+ composed as a Select option's `label` ReactNode. */
28
+ & .crisp-objectsettings-typeoption { display: flex; align-items: center; gap: var(--crisp-space-2); width: 100%; min-width: 0; }
29
+ & .crisp-objectsettings-typeoption-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
30
+ & .crisp-objectsettings-typeoption-badge { flex-shrink: 0; font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-brand-soft); }
31
+ /* Create-attribute dialog's AI-autofill row and the submit button's ⌘↵ hint (measured off the same dialog, same shape as Dialog's own
32
+ .crisp-dialog-submit-key, kept local rather than reaching into
33
+ another component's scoped classes). */
34
+ & .crisp-objectsettings-aiautofill { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); }
35
+ & .crisp-objectsettings-aiautofill-label { font-size: var(--crisp-text-md); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-muted); }
36
+ & .crisp-objectsettings-aiautofill-row { display: flex; align-items: center; gap: var(--crisp-space-2); height: var(--crisp-size-menuRow); padding: 0 var(--crisp-space-2-5); border-radius: var(--crisp-radius-md); box-shadow: inset 0 0 0 1px var(--crisp-border-default); font-size: var(--crisp-text-lg); color: var(--crisp-fg-primary); cursor: pointer; }
37
+ & .crisp-objectsettings-aiautofill-row > svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
38
+ & .crisp-objectsettings-aiautofill-row > span { flex: 1; }
39
+ & .crisp-objectsettings-kbd { display: inline-flex; align-items: center; gap: var(--crisp-space-1); margin-inline-start: var(--crisp-space-1); }
40
+ & .crisp-objectsettings-kbd kbd { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; padding: 0 3px; border-radius: 4px; background: transparent; color: inherit; font-family: inherit; font-style: normal; font-size: 10px; }
41
+ & .crisp-objectsettings-fieldrow { display: grid; grid-template-columns: 1fr 1fr; gap: var(--crisp-space-4); }
42
+ }
43
+
44
+ @media (prefers-reduced-motion: reduce) {
45
+ .crisp-objectsettings, .crisp-objectsettings * {
46
+ animation-duration: 0.01ms !important;
47
+ animation-iteration-count: 1 !important;
48
+ transition-duration: 0.01ms !important;
49
+ }
50
+ }
51
+ }