@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.
- package/dist/board.css +13 -1
- package/dist/board.layered.css +13 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +41 -4
- package/dist/cjs/index.js +2 -0
- package/dist/cjs/product/board/board.js +60 -2
- package/dist/cjs/product/board/board.recipe.js +10 -0
- package/dist/cjs/product/comments/comments.js +426 -0
- package/dist/cjs/product/comments/comments.recipe.js +126 -0
- package/dist/cjs/product/comments/index.js +8 -0
- package/dist/cjs/product/listpage/listpage.recipe.js +10 -1
- package/dist/cjs/product/objectsettings/index.js +16 -0
- package/dist/cjs/product/objectsettings/objectsettings.js +365 -0
- package/dist/cjs/product/objectsettings/objectsettings.recipe.js +65 -0
- package/dist/cjs/product/taskstable/taskstable.recipe.js +10 -1
- package/dist/comments.css +80 -0
- package/dist/comments.d.ts +7 -0
- package/dist/comments.layered.css +82 -0
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +43 -6
- package/dist/esm/index.js +2 -0
- package/dist/esm/product/board/board.js +61 -3
- package/dist/esm/product/board/board.recipe.js +10 -0
- package/dist/esm/product/comments/comments.js +388 -0
- package/dist/esm/product/comments/comments.recipe.js +123 -0
- package/dist/esm/product/comments/index.js +2 -0
- package/dist/esm/product/listpage/listpage.recipe.js +10 -1
- package/dist/esm/product/objectsettings/index.js +2 -0
- package/dist/esm/product/objectsettings/objectsettings.js +322 -0
- package/dist/esm/product/objectsettings/objectsettings.recipe.js +62 -0
- package/dist/esm/product/taskstable/taskstable.recipe.js +10 -1
- package/dist/listpage.css +8 -1
- package/dist/listpage.layered.css +8 -1
- package/dist/objectsettings.css +49 -0
- package/dist/objectsettings.d.ts +7 -0
- package/dist/objectsettings.layered.css +51 -0
- package/dist/styles.css +160 -3
- package/dist/styles.layered.css +160 -3
- package/dist/taskstable.css +8 -1
- package/dist/taskstable.layered.css +8 -1
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +8 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/product/board/board.d.ts +20 -1
- package/dist/types/product/comments/comments.d.ts +192 -0
- package/dist/types/product/comments/comments.recipe.d.ts +3 -0
- package/dist/types/product/comments/index.d.ts +2 -0
- package/dist/types/product/objectsettings/index.d.ts +2 -0
- package/dist/types/product/objectsettings/objectsettings.d.ts +261 -0
- package/dist/types/product/objectsettings/objectsettings.recipe.d.ts +3 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|