@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,365 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ObjectSettings = exports.ATTRIBUTE_AI_AUTOFILL_OPTIONS = exports.ATTRIBUTE_TYPE_OPTIONS = void 0;
37
+ exports.ObjectsList = ObjectsList;
38
+ exports.ObjectDetailPage = ObjectDetailPage;
39
+ exports.ObjectGeneralSection = ObjectGeneralSection;
40
+ exports.ObjectAppearanceSection = ObjectAppearanceSection;
41
+ exports.AttributesTable = AttributesTable;
42
+ exports.AttributeTypeSelect = AttributeTypeSelect;
43
+ exports.CreateAttributeDialog = CreateAttributeDialog;
44
+ const jsx_runtime_1 = require("react/jsx-runtime");
45
+ const lucide_react_1 = require("lucide-react");
46
+ const React = __importStar(require("react"));
47
+ const badge_1 = require("../../components/badge");
48
+ const datatable_1 = require("../../components/datatable");
49
+ const formfield_1 = require("../../components/formfield");
50
+ const input_1 = require("../../components/input");
51
+ const menu_1 = require("../../components/menu");
52
+ const select_1 = require("../../components/select");
53
+ const switch_1 = require("../../components/switch");
54
+ const tabs_1 = require("../../components/tabs");
55
+ const textarea_1 = require("../../components/textarea");
56
+ const core_1 = require("../../core");
57
+ const formdialog_1 = require("../formdialog");
58
+ const settingssection_1 = require("../settingssection");
59
+ const objectsettings_recipe_1 = require("./objectsettings.recipe");
60
+ /**
61
+ * Settings → Objects (crisp#637, P-122): H1 + subtitle, a search+Filter row,
62
+ * and a DataTable of every object (icon+name, type, record count,
63
+ * attribute count, a per-row "…" menu). Measured live against
64
+ * app.attio.com/<workspace>/settings/data/objects — the plan-gate banner
65
+ * above the table and the "5/3" object-count pill in the "Object" header
66
+ * (both billing-tier chrome) are skipped by decision, not oversight.
67
+ */
68
+ 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 }) {
69
+ const title = (0, core_1.useCrispMessage)(titleProp, "Objects");
70
+ const subtitle = (0, core_1.useCrispMessage)(subtitleProp, "Modify and add objects in your workspace");
71
+ const newObjectLabel = (0, core_1.useCrispMessage)(newObjectLabelProp, "New custom object");
72
+ const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search objects");
73
+ const filterLabel = (0, core_1.useCrispMessage)(filterLabelProp, "Filter");
74
+ const objectColumnLabel = (0, core_1.useCrispMessage)(objectColumnLabelProp, "Object");
75
+ const typeColumnLabel = (0, core_1.useCrispMessage)(typeColumnLabelProp, "Type");
76
+ const recordsColumnLabel = (0, core_1.useCrispMessage)(recordsColumnLabelProp, "Records");
77
+ const attributesColumnLabel = (0, core_1.useCrispMessage)(attributesColumnLabelProp, "Attributes");
78
+ const noRecordsLabel = (0, core_1.useCrispMessage)(noRecordsLabelProp, "No records");
79
+ const editLabel = (0, core_1.useCrispMessage)(editLabelProp, "Edit");
80
+ const copyIdLabel = (0, core_1.useCrispMessage)(copyIdLabelProp, "Copy ID");
81
+ const [query, setQuery] = React.useState("");
82
+ const filtered = React.useMemo(() => {
83
+ if (!query.trim())
84
+ return objects;
85
+ const q = query.trim().toLowerCase();
86
+ return objects.filter((o) => String(o.name ?? "")
87
+ .toLowerCase()
88
+ .includes(q));
89
+ }, [objects, query]);
90
+ const columns = React.useMemo(() => [
91
+ {
92
+ key: "name",
93
+ header: objectColumnLabel,
94
+ render: (_v, row) => ((0, jsx_runtime_1.jsxs)("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-icon", style: { background: row.iconColor ?? "currentColor" }, children: row.icon }), row.name] })),
95
+ },
96
+ {
97
+ key: "typeLabel",
98
+ header: typeColumnLabel,
99
+ render: (_v, row) => ((0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-typebadge", children: row.typeLabel })),
100
+ },
101
+ {
102
+ key: "recordCount",
103
+ header: recordsColumnLabel,
104
+ align: "left",
105
+ render: (_v, row) => row.recordCount ? (row.recordCount) : ((0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-muted", children: noRecordsLabel })),
106
+ },
107
+ {
108
+ key: "attributeCount",
109
+ header: attributesColumnLabel,
110
+ },
111
+ {
112
+ key: "actions",
113
+ header: "",
114
+ align: "right",
115
+ render: (_v, row) => ((0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": rowMenuLabel?.(row) ?? `${row.name}`, style: {
116
+ display: "inline-flex",
117
+ border: "none",
118
+ background: "none",
119
+ cursor: "pointer",
120
+ color: "inherit",
121
+ padding: 4,
122
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 14 }), onSelect: () => onEditObject?.(row.id), children: editLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => onCopyObjectId?.(row.id), children: copyIdLabel })] })),
123
+ },
124
+ ], [
125
+ objectColumnLabel,
126
+ typeColumnLabel,
127
+ recordsColumnLabel,
128
+ attributesColumnLabel,
129
+ noRecordsLabel,
130
+ editLabel,
131
+ copyIdLabel,
132
+ onEditObject,
133
+ onCopyObjectId,
134
+ rowMenuLabel,
135
+ ]);
136
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, objectsettings_recipe_1.objectsettingsRecipe)({ className }), ...rest, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-page", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-head", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-heading", children: [(0, jsx_runtime_1.jsx)("h1", { className: "crisp-objectsettings-title", children: title }), (0, jsx_runtime_1.jsx)("p", { className: "crisp-objectsettings-subtitle", children: subtitle })] }), onCreateObject && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-objectsettings-filterbtn", onClick: onCreateObject, children: newObjectLabel }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-toolbar", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-objectsettings-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchPlaceholder })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-objectsettings-filterbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14 }), filterLabel] })] }), (0, jsx_runtime_1.jsx)(datatable_1.DataTable, { columns: columns, data: filtered, onRowClick: onOpenObject ? (row) => onOpenObject(row.id) : undefined, empty: emptyLabel })] }) }));
137
+ }
138
+ ObjectsList.displayName = "ObjectsList";
139
+ /**
140
+ * Settings → Objects → an object (crisp#637, P-123, verified live against
141
+ * .../objects/companies/general): 48px icon tile + name + type badge, a
142
+ * description line, then a Configuration/Attributes tab bar (Attio's own
143
+ * tab bar also carries Permissions/Templates/Notifications/Imports — out of
144
+ * scope here, see #613/#638; only the two rows this task covers are built).
145
+ * Tab content is a render prop, the same shape crisp's own `Tabs` already
146
+ * takes, so a consumer composes `ObjectGeneralSection`/
147
+ * `ObjectAppearanceSection` and `AttributesTable` underneath.
148
+ */
149
+ function ObjectDetailPage({ icon, iconColor, name, typeLabel, description, tab, defaultTab = "configuration", onTabChange, attributeCount, configurationLabel: configurationLabelProp, attributesLabel: attributesLabelProp, children, }) {
150
+ const configurationLabel = (0, core_1.useCrispMessage)(configurationLabelProp, "Configuration");
151
+ const attributesLabel = (0, core_1.useCrispMessage)(attributesLabelProp, "Attributes");
152
+ const [internalTab, setInternalTab] = React.useState(defaultTab);
153
+ const active = tab ?? internalTab;
154
+ const setTab = (v) => {
155
+ const next = v;
156
+ if (tab === undefined)
157
+ setInternalTab(next);
158
+ onTabChange?.(next);
159
+ };
160
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, objectsettings_recipe_1.objectsettingsRecipe)(), children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-page", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-objectsettings-head", children: (0, jsx_runtime_1.jsxs)("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 }, children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-icon crisp-objectsettings-icon--lg", style: { background: iconColor ?? "currentColor" }, children: icon }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-heading", children: [(0, jsx_runtime_1.jsxs)("h1", { className: "crisp-objectsettings-title", children: [name, typeLabel != null && ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "neutral", size: "small", children: typeLabel }))] }), description != null && ((0, jsx_runtime_1.jsx)("p", { className: "crisp-objectsettings-subtitle", children: description }))] })] }) }), (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { value: active, onValueChange: setTab, tabs: [
161
+ { value: "configuration", label: configurationLabel },
162
+ {
163
+ value: "attributes",
164
+ label: attributesLabel,
165
+ badge: attributeCount,
166
+ },
167
+ ], children: (v) => children(v) })] }) }));
168
+ }
169
+ ObjectDetailPage.displayName = "ObjectDetailPage";
170
+ /** The Configuration tab's "General" section (crisp#637, P-123): plural/
171
+ * singular noun inputs side by side, then a slug input Attio locks for
172
+ * system objects. Composes `SettingsSection` + `FormField`, no new chrome. */
173
+ function ObjectGeneralSection({ pluralNoun, onPluralNounChange, singularNoun, onSingularNounChange, slug, slugEditable = false, onSlugChange, titleLabel: titleLabelProp, descriptionLabel: descriptionLabelProp, pluralLabel: pluralLabelProp, singularLabel: singularLabelProp, slugLabel: slugLabelProp, slugHelpLabel: slugHelpLabelProp, }) {
174
+ const titleLabel = (0, core_1.useCrispMessage)(titleLabelProp, "General");
175
+ const descriptionLabel = (0, core_1.useCrispMessage)(descriptionLabelProp, "Set words to describe a single and multiple objects of this type");
176
+ const pluralLabel = (0, core_1.useCrispMessage)(pluralLabelProp, "Plural noun");
177
+ const singularLabel = (0, core_1.useCrispMessage)(singularLabelProp, "Singular noun");
178
+ const slugLabel = (0, core_1.useCrispMessage)(slugLabelProp, "Slug");
179
+ const slugHelpLabel = (0, core_1.useCrispMessage)(slugHelpLabelProp, "You can't change the slug of a system object");
180
+ const pluralId = React.useId();
181
+ const singularId = React.useId();
182
+ const slugId = React.useId();
183
+ return ((0, jsx_runtime_1.jsxs)(settingssection_1.SettingsSection, { title: titleLabel, description: descriptionLabel, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-fieldrow", children: [(0, jsx_runtime_1.jsx)(formfield_1.FormField, { label: pluralLabel, htmlFor: pluralId, children: (0, jsx_runtime_1.jsx)(input_1.Input, { id: pluralId, value: pluralNoun, onChange: (e) => onPluralNounChange?.(e.target.value) }) }), (0, jsx_runtime_1.jsx)(formfield_1.FormField, { label: singularLabel, htmlFor: singularId, children: (0, jsx_runtime_1.jsx)(input_1.Input, { id: singularId, value: singularNoun, onChange: (e) => onSingularNounChange?.(e.target.value) }) })] }), (0, jsx_runtime_1.jsx)(formfield_1.FormField, { label: slugLabel, htmlFor: slugId, description: !slugEditable ? slugHelpLabel : undefined, children: (0, jsx_runtime_1.jsx)(input_1.Input, { id: slugId, value: slug, disabled: !slugEditable, onChange: (e) => onSlugChange?.(e.target.value) }) })] }));
184
+ }
185
+ ObjectGeneralSection.displayName = "ObjectGeneralSection";
186
+ /** The Configuration tab's "Appearance" section (crisp#637, P-123): a single
187
+ * "Record page layout" row with a "Configure record page" button — the
188
+ * target flow itself (crisp's own record-page-layout editor) is a separate,
189
+ * unbuilt surface; this only renders the row and its click. */
190
+ function ObjectAppearanceSection({ onConfigureRecordPage, titleLabel: titleLabelProp, rowLabel: rowLabelProp, rowDescriptionLabel: rowDescriptionLabelProp, actionLabel: actionLabelProp, }) {
191
+ const titleLabel = (0, core_1.useCrispMessage)(titleLabelProp, "Appearance");
192
+ const rowLabel = (0, core_1.useCrispMessage)(rowLabelProp, "Record page layout");
193
+ const rowDescriptionLabel = (0, core_1.useCrispMessage)(rowDescriptionLabelProp, "Configure tabs, widgets, and layout for this object's record pages");
194
+ const actionLabel = (0, core_1.useCrispMessage)(actionLabelProp, "Configure record page");
195
+ return ((0, jsx_runtime_1.jsx)(settingssection_1.SettingsSection, { title: titleLabel, children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-fieldrow", style: { alignItems: "center" }, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { style: { fontWeight: 500 }, children: rowLabel }), (0, jsx_runtime_1.jsx)("p", { className: "crisp-objectsettings-subtitle", children: rowDescriptionLabel })] }), onConfigureRecordPage && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-objectsettings-filterbtn", style: { justifySelf: "end" }, onClick: onConfigureRecordPage, children: actionLabel }))] }) }));
196
+ }
197
+ ObjectAppearanceSection.displayName = "ObjectAppearanceSection";
198
+ /**
199
+ * The Attributes tab's table (crisp#637, P-124, verified live): H2 +
200
+ * "Create attribute", a search+Filter row, then a DataTable of every
201
+ * attribute — drag handle (via `DataTable`'s own `onRowsReorder`) + type
202
+ * icon + name, Data type, a constraint label, and System (grey)/Enriched
203
+ * (purple) property badges. Row height is DataTable's own 36px; Attio's
204
+ * measures 40px here — a real, small divergence left to DataTable's owner
205
+ * rather than patched locally (see the PR).
206
+ */
207
+ 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, }) {
208
+ const title = (0, core_1.useCrispMessage)(titleProp, "Attributes");
209
+ const subtitle = (0, core_1.useCrispMessage)(subtitleProp, "Modify and add object attributes");
210
+ const createLabel = (0, core_1.useCrispMessage)(createLabelProp, "Create attribute");
211
+ const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search attributes");
212
+ const filterLabel = (0, core_1.useCrispMessage)(filterLabelProp, "Filter");
213
+ const nameColumnLabel = (0, core_1.useCrispMessage)(nameColumnLabelProp, "Name");
214
+ const dataTypeColumnLabel = (0, core_1.useCrispMessage)(dataTypeColumnLabelProp, "Data type");
215
+ const constraintsColumnLabel = (0, core_1.useCrispMessage)(constraintsColumnLabelProp, "Constraints");
216
+ const propertiesColumnLabel = (0, core_1.useCrispMessage)(propertiesColumnLabelProp, "Properties");
217
+ const systemLabel = (0, core_1.useCrispMessage)(systemLabelProp, "System");
218
+ const enrichedLabel = (0, core_1.useCrispMessage)(enrichedLabelProp, "Enriched");
219
+ const editLabel = (0, core_1.useCrispMessage)(editLabelProp, "Edit");
220
+ const archiveLabel = (0, core_1.useCrispMessage)(archiveLabelProp, "Archive");
221
+ const [query, setQuery] = React.useState("");
222
+ const filtered = React.useMemo(() => {
223
+ if (!query.trim())
224
+ return attributes;
225
+ const q = query.trim().toLowerCase();
226
+ return attributes.filter((a) => String(a.name ?? "")
227
+ .toLowerCase()
228
+ .includes(q));
229
+ }, [attributes, query]);
230
+ const columns = React.useMemo(() => [
231
+ {
232
+ key: "name",
233
+ header: nameColumnLabel,
234
+ render: (_v, row) => ((0, jsx_runtime_1.jsxs)("span", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [(0, jsx_runtime_1.jsx)("span", { style: {
235
+ display: "inline-flex",
236
+ color: "var(--crisp-fg-muted, inherit)",
237
+ }, children: row.icon }), row.name] })),
238
+ },
239
+ { key: "dataType", header: dataTypeColumnLabel },
240
+ {
241
+ key: "constraint",
242
+ header: constraintsColumnLabel,
243
+ render: (_v, row) => row.constraint ?? null,
244
+ },
245
+ {
246
+ key: "properties",
247
+ header: propertiesColumnLabel,
248
+ render: (_v, row) => ((0, jsx_runtime_1.jsxs)("span", { style: { display: "flex", gap: 6 }, children: [row.system && ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "neutral", size: "small", children: systemLabel })), row.enriched && ((0, jsx_runtime_1.jsx)(badge_1.Badge, { tone: "purple", size: "small", children: enrichedLabel }))] })),
249
+ },
250
+ {
251
+ key: "actions",
252
+ header: "",
253
+ align: "right",
254
+ render: (_v, row) => ((0, jsx_runtime_1.jsx)(menu_1.Menu, { align: "end", trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", "aria-label": `${row.name}`, style: {
255
+ display: "inline-flex",
256
+ border: "none",
257
+ background: "none",
258
+ cursor: "pointer",
259
+ color: "inherit",
260
+ padding: 4,
261
+ }, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16 }) }), children: rowActions ? (rowActions(row)) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { onSelect: () => onEditAttribute?.(row.id), children: editLabel }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { danger: true, onSelect: () => onArchiveAttribute?.(row.id), children: archiveLabel })] })) })),
262
+ },
263
+ ], [
264
+ nameColumnLabel,
265
+ dataTypeColumnLabel,
266
+ constraintsColumnLabel,
267
+ propertiesColumnLabel,
268
+ systemLabel,
269
+ enrichedLabel,
270
+ editLabel,
271
+ archiveLabel,
272
+ onEditAttribute,
273
+ onArchiveAttribute,
274
+ rowActions,
275
+ ]);
276
+ return ((0, jsx_runtime_1.jsx)("div", { className: (0, objectsettings_recipe_1.objectsettingsRecipe)(), children: (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-page", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-head", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-heading", children: [(0, jsx_runtime_1.jsx)("h2", { className: "crisp-objectsettings-title", style: { fontSize: 16, lineHeight: "20px" }, children: title }), (0, jsx_runtime_1.jsx)("p", { className: "crisp-objectsettings-subtitle", children: subtitle })] }), onCreateAttribute && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-objectsettings-filterbtn", onClick: onCreateAttribute, children: createLabel }))] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-toolbar", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-objectsettings-search", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 15, strokeWidth: 1.75, "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("input", { type: "search", placeholder: searchPlaceholder, value: query, onChange: (e) => setQuery(e.target.value), "aria-label": searchPlaceholder })] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-objectsettings-filterbtn", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.SlidersHorizontal, { size: 14 }), filterLabel] })] }), (0, jsx_runtime_1.jsx)(datatable_1.DataTable, { columns: columns, data: filtered, onRowClick: onOpenAttribute ? (row) => onOpenAttribute(row.id) : undefined, onRowsReorder: onReorder, empty: emptyLabel })] }) }));
277
+ }
278
+ AttributesTable.displayName = "AttributesTable";
279
+ /** Attio's own "Type" group, verbatim (crisp#637, P-126) — icons are
280
+ * crisp's Lucide choices, not Attio's proprietary set (golden rule 4). */
281
+ exports.ATTRIBUTE_TYPE_OPTIONS = [
282
+ { value: "checkbox", label: "Checkbox", icon: lucide_react_1.CheckSquare },
283
+ { value: "currency", label: "Currency", icon: lucide_react_1.DollarSign },
284
+ { value: "date", label: "Date", icon: lucide_react_1.Calendar },
285
+ { value: "location", label: "Location", icon: lucide_react_1.MapPin },
286
+ { value: "multiselect", label: "Multi-select", icon: lucide_react_1.ListChecks },
287
+ { value: "number", label: "Number", icon: lucide_react_1.Hash },
288
+ { value: "phone", label: "Phone Number", icon: lucide_react_1.Phone },
289
+ { value: "rating", label: "Rating", icon: lucide_react_1.Star },
290
+ { value: "record", label: "Record", icon: lucide_react_1.FileText },
291
+ { value: "relationship", label: "Relationship", icon: lucide_react_1.Link2, badge: "Pro" },
292
+ { value: "select", label: "Select", icon: lucide_react_1.ListChecks },
293
+ { value: "status", label: "Status", icon: lucide_react_1.Flag },
294
+ { value: "text", label: "Text", icon: lucide_react_1.Type },
295
+ { value: "timestamp", label: "Timestamp", icon: lucide_react_1.Clock },
296
+ { value: "user", label: "User", icon: lucide_react_1.User },
297
+ ];
298
+ /** Attio's own "AI Autofill" group, verbatim (crisp#637, P-126) — opt in via
299
+ * `AttributeTypeSelect`'s `aiAutofillOptions`; omitted by default, since a
300
+ * consumer with no autofill backend of its own (GRC) has nothing to wire
301
+ * these to. */
302
+ exports.ATTRIBUTE_AI_AUTOFILL_OPTIONS = [
303
+ { value: "classify-record", label: "Classify record", icon: lucide_react_1.Sparkles },
304
+ { value: "prompt-completion", label: "Prompt completion", icon: lucide_react_1.Sparkles },
305
+ { value: "summarize-record", label: "Summarize record", icon: lucide_react_1.Sparkles },
306
+ { value: "web-agent", label: "Web agent", icon: lucide_react_1.Sparkles },
307
+ ];
308
+ function toSelectOption(o, group) {
309
+ const Icon = o.icon;
310
+ return {
311
+ value: o.value,
312
+ keywords: o.label,
313
+ group,
314
+ label: ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-objectsettings-typeoption", children: [(0, jsx_runtime_1.jsx)(Icon, { size: 14 }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-typeoption-label", children: o.label }), o.badge != null && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-typeoption-badge", children: o.badge }))] })),
315
+ };
316
+ }
317
+ /** The "Attribute Type" dropdown (crisp#637, P-126, verified live): a
318
+ * searchable, grouped `Select` — reused as-is, not a new picker primitive.
319
+ * Grouping and the per-item trailing "Pro" badge are composed through
320
+ * `Select`'s existing `group` field and rich `label` ReactNode. */
321
+ function AttributeTypeSelect({ value, onValueChange, options = exports.ATTRIBUTE_TYPE_OPTIONS, aiAutofillOptions, aiAutofillGroupLabel: aiAutofillGroupLabelProp, typeGroupLabel: typeGroupLabelProp, searchPlaceholder: searchPlaceholderProp, id, }) {
322
+ const aiAutofillGroupLabel = (0, core_1.useCrispMessage)(aiAutofillGroupLabelProp, "AI Autofill");
323
+ const typeGroupLabel = (0, core_1.useCrispMessage)(typeGroupLabelProp, "Type");
324
+ const searchPlaceholder = (0, core_1.useCrispMessage)(searchPlaceholderProp, "Search attribute types...");
325
+ const selectOptions = React.useMemo(() => {
326
+ const ai = (aiAutofillOptions ?? []).map((o) => toSelectOption(o, aiAutofillGroupLabel));
327
+ const type = options.map((o) => toSelectOption(o, typeGroupLabel));
328
+ return [...ai, ...type];
329
+ }, [aiAutofillOptions, options, aiAutofillGroupLabel, typeGroupLabel]);
330
+ return ((0, jsx_runtime_1.jsx)(select_1.Select, { id: id, options: selectOptions, value: value, onValueChange: onValueChange, searchable: true, searchPlaceholder: searchPlaceholder, width: "full" }));
331
+ }
332
+ AttributeTypeSelect.displayName = "AttributeTypeSelect";
333
+ /**
334
+ * "Create attribute" (crisp#637, P-125, verified live): Attribute Type
335
+ * select (defaults to Text) + Name (autofocused) + Description (optional)
336
+ * + an AI-autofill switch row, footer Cancel/Create. Built on `FormDialog`
337
+ * in its plain (non-`createMode`) shape — Attio's own footer here has a
338
+ * real Cancel button, unlike the record-create dialog `createMode` models
339
+ * (crisp#548), so `createMode` would be the wrong fit even though the
340
+ * button carries the same ⌘↵ hint; that hint is rendered locally instead.
341
+ */
342
+ 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, }) {
343
+ const title = (0, core_1.useCrispMessage)(titleProp, "Create attribute");
344
+ const attributeTypeLabel = (0, core_1.useCrispMessage)(attributeTypeLabelProp, "Attribute Type");
345
+ const nameLabel = (0, core_1.useCrispMessage)(nameLabelProp, "Name");
346
+ const descriptionLabel = (0, core_1.useCrispMessage)(descriptionLabelProp, "Description");
347
+ const descriptionPlaceholder = (0, core_1.useCrispMessage)(descriptionPlaceholderProp, "Add a description for this attribute");
348
+ const aiAutofillLabel = (0, core_1.useCrispMessage)(aiAutofillLabelProp, "AI autofill");
349
+ const setUpAiAutofillLabel = (0, core_1.useCrispMessage)(setUpAiAutofillLabelProp, "Set up AI autofill");
350
+ const createLabel = (0, core_1.useCrispMessage)(createLabelProp, "Create attribute");
351
+ const switchId = React.useId();
352
+ const nameId = React.useId();
353
+ return ((0, jsx_runtime_1.jsxs)(formdialog_1.FormDialog, { title: title, submitLabel: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [createLabel, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-objectsettings-kbd", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("kbd", { children: "\u2318" }), (0, jsx_runtime_1.jsx)("kbd", { children: "\u21B5" })] })] }), onSubmit: onSubmit, onClose: onClose, busy: busy, error: error, size: 520, className: (0, objectsettings_recipe_1.objectsettingsRecipe)(), children: [(0, jsx_runtime_1.jsx)(formfield_1.FormField, { label: attributeTypeLabel, children: (0, jsx_runtime_1.jsx)(AttributeTypeSelect, { value: type, onValueChange: onTypeChange, options: typeOptions, aiAutofillOptions: aiAutofillTypeOptions }) }), (0, jsx_runtime_1.jsx)(formfield_1.FormField, { label: nameLabel, htmlFor: nameId, children: (0, jsx_runtime_1.jsx)(input_1.Input, { id: nameId, autoFocus: true, value: name, onChange: (e) => onNameChange(e.target.value) }) }), (0, jsx_runtime_1.jsx)(formfield_1.FormField, { label: descriptionLabel, optional: true, children: (0, jsx_runtime_1.jsx)(textarea_1.Textarea, { placeholder: descriptionPlaceholder, value: description, onChange: (e) => onDescriptionChange(e.target.value) }) }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-objectsettings-aiautofill", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-objectsettings-aiautofill-label", children: aiAutofillLabel }), (0, jsx_runtime_1.jsxs)("label", { className: "crisp-objectsettings-aiautofill-row", htmlFor: switchId, children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14 }), (0, jsx_runtime_1.jsx)("span", { children: setUpAiAutofillLabel }), (0, jsx_runtime_1.jsx)(switch_1.Switch, { id: switchId, checked: aiAutofillEnabled, onCheckedChange: onAiAutofillChange })] })] })] }));
354
+ }
355
+ CreateAttributeDialog.displayName = "CreateAttributeDialog";
356
+ /**
357
+ * This folder holds several components (`ObjectsList`, `ObjectDetailPage`,
358
+ * `AttributesTable`, `CreateAttributeDialog`, …), not one named after the
359
+ * slug — unlike every other product dir, whose docs `<ManualInstallation>`
360
+ * resolves an import binding from the folder slug ("avatargroup" →
361
+ * `AvatarGroup`) by reading this file's exports. This alias exists only so
362
+ * that resolver finds a match for "objectsettings"; consumers should import
363
+ * the real names above, never this one.
364
+ */
365
+ exports.ObjectSettings = ObjectsList;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.objectsettingsRecipe = void 0;
4
+ const core_1 = require("../../core");
5
+ const tokens_1 = require("../../tokens");
6
+ // Measured live against app.attio.com/<workspace>/settings/data/objects and
7
+ // .../objects/companies/{general,attributes} (crisp#637, P-122/P-123/P-124/
8
+ // P-125/P-126). Values with no matching scale token are one-off literals,
9
+ // same precedent as authshell's own 20px title (crisp#637 golden rule 3):
10
+ // the 24/600/28/-0.48 page title, the 12px/16px badge dot sizes, and the
11
+ // 30% squircle radius on the object-icon tile.
12
+ const config = (0, core_1.defineRecipe)({
13
+ name: "objectsettings",
14
+ base: `& .crisp-objectsettings-page { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["6"]}; }
15
+ & .crisp-objectsettings-head { display: flex; align-items: flex-start; justify-content: space-between; gap: ${tokens_1.contract.space["4"]}; }
16
+ & .crisp-objectsettings-heading { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["0.5"]}; min-width: 0; }
17
+ & .crisp-objectsettings-title { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; font-size: 24px; line-height: 28px; font-weight: ${tokens_1.contract.weight.semibold}; letter-spacing: -0.48px; color: ${tokens_1.contract.fgPrimary}; margin: 0; }
18
+ & .crisp-objectsettings-subtitle { font-size: ${tokens_1.contract.text.lg}; line-height: 20px; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; margin: 0; }
19
+
20
+ & .crisp-objectsettings-toolbar { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; }
21
+ & .crisp-objectsettings-search { display: flex; align-items: center; flex: 1; gap: ${tokens_1.contract.space["1.5"]}; height: ${tokens_1.contract.size.menuRow}; padding: 0 ${tokens_1.contract.space["2.5"]}; border-radius: ${tokens_1.contract.radius.md}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDefault}; color: ${tokens_1.contract.fgMuted}; transition: box-shadow .15s; }
22
+ & .crisp-objectsettings-search:focus-within { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
23
+ & .crisp-objectsettings-search input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
24
+ & .crisp-objectsettings-search input::placeholder { color: ${tokens_1.contract.fgMuted}; }
25
+ & .crisp-objectsettings-filterbtn { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; height: ${tokens_1.contract.size.menuRow}; padding: 0 ${tokens_1.contract.space["2.5"]} 0 ${tokens_1.contract.space["2"]}; border: none; border-radius: ${tokens_1.contract.radius.lg}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDefault}; color: ${tokens_1.contract.fgPrimary}; font-family: inherit; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; cursor: pointer; white-space: nowrap; }
26
+ & .crisp-objectsettings-filterbtn:hover { background: ${tokens_1.contract.bgRaised}; }
27
+
28
+ /* Object / attribute icon tile — Attio's own squircle (measured
29
+ border-radius: 30%, not a scale token: it's a proportional radius
30
+ tied to the tile's own size, unlike every other radius crisp uses,
31
+ which is a fixed px). */
32
+ & .crisp-objectsettings-icon { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 16px; height: 16px; border-radius: 30%; color: ${tokens_1.contract.fgOnBrand}; }
33
+ & .crisp-objectsettings-icon--lg { width: 48px; height: 48px; border-radius: 16px; }
34
+
35
+ /* "Standard"/"Custom" type badge and the muted "No records" cell —
36
+ plain text, not a Badge pill (measured: transparent background). */
37
+ & .crisp-objectsettings-typebadge { font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgNeutralSoft}; }
38
+ & .crisp-objectsettings-muted { color: ${tokens_1.contract.fgMuted}; }
39
+
40
+ /* Attribute-type option row (icon + label + optional trailing badge),
41
+ composed as a Select option's \`label\` ReactNode. */
42
+ & .crisp-objectsettings-typeoption { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; width: 100%; min-width: 0; }
43
+ & .crisp-objectsettings-typeoption-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
44
+ & .crisp-objectsettings-typeoption-badge { flex-shrink: 0; font-size: ${tokens_1.contract.text.sm}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgBrandSoft}; }
45
+
46
+ /* Create-attribute dialog's AI-autofill row and the submit button's
47
+ ⌘↵ hint (measured off the same dialog, same shape as Dialog's own
48
+ .crisp-dialog-submit-key, kept local rather than reaching into
49
+ another component's scoped classes). */
50
+ & .crisp-objectsettings-aiautofill { display: flex; flex-direction: column; gap: ${tokens_1.contract.space["1.5"]}; }
51
+ & .crisp-objectsettings-aiautofill-label { font-size: ${tokens_1.contract.text.md}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgMuted}; }
52
+ & .crisp-objectsettings-aiautofill-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["2"]}; height: ${tokens_1.contract.size.menuRow}; padding: 0 ${tokens_1.contract.space["2.5"]}; border-radius: ${tokens_1.contract.radius.md}; box-shadow: inset 0 0 0 1px ${tokens_1.contract.borderDefault}; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; cursor: pointer; }
53
+ & .crisp-objectsettings-aiautofill-row > svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
54
+ & .crisp-objectsettings-aiautofill-row > span { flex: 1; }
55
+ & .crisp-objectsettings-kbd { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; margin-inline-start: ${tokens_1.contract.space["1"]}; }
56
+ & .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; }
57
+
58
+ & .crisp-objectsettings-fieldrow { display: grid; grid-template-columns: 1fr 1fr; gap: ${tokens_1.contract.space["4"]}; }
59
+ `,
60
+ variants: {},
61
+ defaultVariants: {},
62
+ compoundVariants: [],
63
+ });
64
+ exports.objectsettingsRecipe = (0, core_1.resolveRecipe)(config);
65
+ exports.default = config;
@@ -26,7 +26,16 @@ const config = (0, core_1.defineRecipe)({
26
26
  & .crisp-taskstable-colhead-cell { display: inline-flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; min-width: 0; }
27
27
 
28
28
  & .crisp-taskstable-row { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; }
29
- & .crisp-taskstable-cell { display: flex; align-items: center; min-width: 0; }
29
+ /* #642: measured live -- title, due date and assignee name are ALL
30
+ 14px/500 on Attio's own Tasks page. Only -title/-due set their own
31
+ font-size (and only -title its own weight), so an unstyled consumer
32
+ slot (a plain PersonChip/RecordChip name span with no font-size of
33
+ its own) fell through to the browser/body default, 16px/400. Set
34
+ the ambient font here so it cascades to any slot content that
35
+ doesn't override it -- -title/-due's own declarations still apply,
36
+ unchanged, they're just now redundant with this baseline rather
37
+ than the only source of it. */
38
+ & .crisp-taskstable-cell { display: flex; align-items: center; min-width: 0; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; }
30
39
 
31
40
  & .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: ${tokens_1.contract.radius.full}; background: ${tokens_1.contract.bgSurface}; box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 9%, transparent); cursor: pointer; transition: background-color .12s, box-shadow .12s; }
32
41
  & .crisp-taskstable-check:hover { box-shadow: inset 0 0 0 1px color-mix(in srgb, ${tokens_1.contract.fgPrimary} 30%, transparent); }
@@ -0,0 +1,80 @@
1
+ .crisp-comments {
2
+ display: flex; flex-direction: column; box-sizing: border-box; width: 320px; height: 100%; background: var(--crisp-bg-surface); border-inline-start: 1px solid var(--crisp-border-subtle); font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
3
+ & * { box-sizing: border-box; }
4
+ & .crisp-comments-head { display: flex; align-items: center; gap: var(--crisp-space-1); flex-shrink: 0; height: 49px; padding: 0 var(--crisp-space-2) 0 var(--crisp-space-4); }
5
+ & .crisp-comments-headbtn { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
6
+ & .crisp-comments-headbtn:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
7
+ & .crisp-comments-title-wrap { flex: 1; min-width: 0; display: flex; flex-direction: column; }
8
+ & .crisp-comments-title { font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
9
+ & .crisp-comments-breadcrumb { display: flex; align-items: center; gap: var(--crisp-space-1); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
10
+ & .crisp-comments-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--crisp-space-4); display: flex; flex-direction: column; gap: var(--crisp-space-4); }
11
+ & .crisp-comments-body--pad-top { padding-top: var(--crisp-space-3); }
12
+ /* One comment row -- avatar 16px (Avatar size="sm"), 6px gap to the name/time line, 14px/600 name + 12px/500 63%-muted time, 14px/400 body two lines below. Hover actions fade in flush with the name line. */
13
+ & .crisp-comments-row { position: relative; display: flex; gap: var(--crisp-space-1-5); }
14
+ & .crisp-comments-row-main { flex: 1; min-width: 0; }
15
+ & .crisp-comments-row-head { display: flex; align-items: baseline; gap: var(--crisp-space-1-5); height: 20px; }
16
+ & .crisp-comments-author { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-primary); }
17
+ & .crisp-comments-time { font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); white-space: nowrap; }
18
+ & .crisp-comments-resolved-label { display: flex; align-items: center; gap: var(--crisp-space-1); margin-bottom: var(--crisp-space-1); font-size: 12px; font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
19
+ & .crisp-comments-body-text { font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-regular); color: var(--crisp-fg-primary); white-space: pre-wrap; word-break: break-word; }
20
+ /* Hover actions -- measured: 4 buttons, 28x28, radius 8, flush side-by-side (0 gap), transparent by default, 16px icon at 55% ink. */
21
+ & .crisp-comments-row-actions { position: absolute; top: 0; inset-inline-end: 0; display: flex; opacity: 0; pointer-events: none; background: var(--crisp-bg-surface); }
22
+ & .crisp-comments-row:hover .crisp-comments-row-actions, & .crisp-comments-row-actions:focus-within { opacity: 1; pointer-events: auto; }
23
+ & .crisp-comments-action { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: transparent; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
24
+ & .crisp-comments-action:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
25
+ & .crisp-comments-action[data-active] { color: var(--crisp-bg-brand-solid); }
26
+ /* Reactions -- measured: 28px pill, radius 8, subtle 4% ink fill, padding 4px 8px 4px 6px, gap 4px between the emoji glyph and count. */
27
+ & .crisp-comments-reactions { display: flex; flex-wrap: wrap; gap: var(--crisp-space-1); margin-top: var(--crisp-space-1-5); }
28
+ & .crisp-comments-reaction { display: inline-flex; align-items: center; gap: var(--crisp-space-1); height: 28px; padding: 4px 8px 4px 6px; border: none; border-radius: var(--crisp-radius-md); background: color-mix(in srgb, var(--crisp-fg-primary) 4%, transparent); font-family: inherit; font-size: 12px; font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); cursor: pointer; }
29
+ & .crisp-comments-reaction[data-reacted] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 12%, transparent); color: var(--crisp-bg-brand-solid); }
30
+ & .crisp-comments-reaction-add { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; border-radius: var(--crisp-radius-md); background: transparent; font-family: inherit; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; opacity: 0; transition: opacity .12s, background-color .12s; }
31
+ & .crisp-comments-row:hover .crisp-comments-reaction-add { opacity: 1; }
32
+ & .crisp-comments-reaction-add:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
33
+ /* Reply-in-thread view -- an indented block of replies below the root. */
34
+ & .crisp-comments-replies { display: flex; flex-direction: column; gap: var(--crisp-space-4); margin-top: var(--crisp-space-3); padding-inline-start: var(--crisp-space-4); border-inline-start: 1px solid var(--crisp-border-subtle); }
35
+ & .crisp-comments-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--crisp-space-1); padding: var(--crisp-space-6) var(--crisp-space-4); text-align: center; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-size: var(--crisp-text-lg); }
36
+ /* Composer -- measured: 14px/20px body text, 16px gap above the reply/mention icon row, Comment button 84x32 padding 6px 10px radius 9 (= radius.lg) brand-solid fill, 14px/500 white label, opacity .4 disabled / 1 enabled (no color/background change between the two). */
37
+ & .crisp-comments-composer { flex-shrink: 0; padding: var(--crisp-space-3) var(--crisp-space-4) var(--crisp-space-4); border-top: 1px solid var(--crisp-border-subtle); }
38
+ & .crisp-comments-field { min-height: 20px; max-height: 120px; overflow-y: auto; font-family: inherit; font-size: var(--crisp-text-lg); line-height: 20px; font-weight: var(--crisp-weight-regular); color: var(--crisp-fg-primary); outline: none; white-space: pre-wrap; word-break: break-word; }
39
+ & .crisp-comments-field:focus-visible { outline: 2px solid var(--crisp-stroke-focus-ring); outline-offset: 2px; }
40
+ & .crisp-comments-field:empty::before { content: attr(data-placeholder); color: color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); }
41
+ & .crisp-comments-composer-foot { display: flex; align-items: center; justify-content: space-between; margin-top: var(--crisp-space-4); }
42
+ & .crisp-comments-composer-tools { display: flex; align-items: center; gap: var(--crisp-space-1); }
43
+ & .crisp-comments-tool { display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: var(--crisp-radius-sm); background: none; color: color-mix(in srgb, var(--crisp-fg-primary) 45%, transparent); font-family: inherit; cursor: pointer; }
44
+ & .crisp-comments-tool:hover { color: var(--crisp-fg-primary); }
45
+ /* Same fill/shadow as Button's own intent="primary" (crisp/button.recipe.ts) --
46
+ confirmed byte-identical against the live "Comment" button's computed box-shadow, so this reuses that literal rather than re-deriving it. 32px/radius.lg/6px-10px padding has no matching Button `size` yet (24/28/40 are the three that exist), so this stays hand-rolled instead of stretching one of those. */
47
+ & .crisp-comments-post { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 6px var(--crisp-space-2-5); border: none; border-radius: var(--crisp-radius-lg); background: var(--crisp-bg-brand-solid); box-shadow: inset 0 0 0 1px rgba(0,0,0,0.06), 0 2px 4px -2px rgba(38,109,240,0.12), 0 3px 6px -2px rgba(38,109,240,0.08); font-family: inherit; font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-semibold); color: var(--crisp-fg-on-brand); cursor: pointer; opacity: 1; transition: opacity .12s, background-color .12s; }
48
+ & .crisp-comments-post:hover:not(:disabled) { background: var(--crisp-bg-brand-solid-pressed); }
49
+ & .crisp-comments-post:disabled { opacity: .4; cursor: default; }
50
+ /* Filter menu -- measured: 220px, radius 12 (radius.xl), 4px padding, elevation.floating -- a plain reuse of crisp's own Menu, so no rules
51
+ live here; kept as a comment for the record. */
52
+
53
+ /* An inserted "@mention" chip in the composer -- same shape as
54
+ RichTextEditor's own `.crisp-rte-mention` (crisp#428), reused here rather than re-derived since it's the same atomic-chip affordance. */
55
+ & .crisp-comments-mention { display: inline; padding: 0 2px; border-radius: 4px; background: color-mix(in srgb, var(--crisp-bg-brand-solid) 12%, transparent); color: var(--crisp-bg-brand-solid); font-weight: var(--crisp-weight-medium); user-select: none; }
56
+
57
+ /* Reaction picker -- see `ReactionPicker`'s own doc comment: the exact chrome wasn't independently pixel-measured this pass (screenshot
58
+ capture broke mid-session), so this reuses Menu's already-measured floating-popover shell rather than a new value. */
59
+ & .crisp-comments-reactionpicker { width: 240px; padding: var(--crisp-space-2); border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); }
60
+ & .crisp-comments-reactionpicker-search { box-sizing: border-box; width: 100%; height: 28px; margin-bottom: var(--crisp-space-2); padding: 0 var(--crisp-space-2); border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-raised); font-family: inherit; font-size: var(--crisp-text-md); color: var(--crisp-fg-primary); }
61
+ & .crisp-comments-reactionpicker-search::placeholder { color: color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); }
62
+ & .crisp-comments-reactionpicker-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: var(--crisp-space-0-5); max-height: 160px; overflow-y: auto; }
63
+ & .crisp-comments-reactionpicker-item { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; border-radius: var(--crisp-radius-sm); background: none; font-family: inherit; font-size: 16px; line-height: 1; cursor: pointer; }
64
+ & .crisp-comments-reactionpicker-item:hover { background: var(--crisp-bg-raised); }
65
+ /* Row badge (CommentCountBadge) -- lives in its own class family so the grid lanes can drop it into a cell without pulling in the panel's
66
+ CSS. Persistent state: a 6px grey dot. Hover state: a 12px comment
67
+ icon + 12px count text, both at 55% ink, replacing the dot. */
68
+ & .crisp-comments-badge { display: inline-flex; align-items: center; gap: var(--crisp-space-1); border: none; background: none; padding: 0; margin: 0; font-family: inherit; cursor: pointer; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); }
69
+ & .crisp-comments-badge-dot { width: 6px; height: 6px; border-radius: var(--crisp-radius-full); background: color-mix(in srgb, var(--crisp-fg-primary) 38%, transparent); }
70
+ & .crisp-comments-badge-count { font-size: 12px; line-height: 16px; }
71
+ [dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
72
+ }
73
+
74
+ @media (prefers-reduced-motion: reduce) {
75
+ .crisp-comments, .crisp-comments * {
76
+ animation-duration: 0.01ms !important;
77
+ animation-iteration-count: 1 !important;
78
+ transition-duration: 0.01ms !important;
79
+ }
80
+ }
@@ -0,0 +1,7 @@
1
+ import type { RecipeFn } from "../core/types";
2
+
3
+ export interface CommentsVariants {
4
+
5
+ }
6
+
7
+ export declare const commentsRecipe: RecipeFn<{ }>;