@crisp-ui-kit/crisp 0.49.0 → 0.51.0
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/cjs/components/composewindow/composewindow.js +144 -0
- package/dist/cjs/components/composewindow/composewindow.recipe.js +45 -0
- package/dist/cjs/components/composewindow/index.js +8 -0
- package/dist/cjs/components/datatable/datatable.js +224 -43
- package/dist/cjs/components/datatable/datatable.recipe.js +38 -4
- package/dist/cjs/components/datatable/index.js +6 -0
- package/dist/cjs/components/datepickerfield/datepickerfield.js +25 -15
- package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +25 -8
- package/dist/cjs/components/dialog/dialog.recipe.js +9 -1
- package/dist/cjs/components/menu/menu.js +4 -2
- package/dist/cjs/components/multiselect/multiselect.recipe.js +14 -8
- package/dist/cjs/components/select/select.js +2 -1
- package/dist/cjs/components/table/table.recipe.js +12 -3
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +2 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +39 -8
- package/dist/cjs/core/cell-date.js +11 -0
- package/dist/cjs/core/index.js +2 -1
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/product/recordpage/recordpage.js +88 -10
- package/dist/cjs/product/recordpage/recordpage.recipe.js +30 -0
- package/dist/cjs/product/recordpanel/recordpanel.js +54 -4
- package/dist/cjs/product/recordpanel/recordpanel.recipe.js +29 -0
- package/dist/cjs/product/recordstable/editable-field.js +110 -0
- package/dist/cjs/product/recordstable/index.js +4 -1
- package/dist/cjs/product/recordstable/recordstable.js +67 -11
- package/dist/cjs/product/recordstable/recordstable.recipe.js +9 -1
- package/dist/cjs/product/settingsshell/settingsshell.recipe.js +13 -4
- package/dist/cjs/product/sidebar/sidebar.js +15 -3
- package/dist/cjs/product/sidebar/sidebar.recipe.js +21 -2
- package/dist/composewindow.css +24 -0
- package/dist/composewindow.d.ts +7 -0
- package/dist/composewindow.layered.css +26 -0
- package/dist/datatable.css +34 -15
- package/dist/datatable.layered.css +34 -15
- package/dist/datepickerfield.css +9 -4
- package/dist/datepickerfield.layered.css +9 -4
- package/dist/dialog.css +2 -1
- package/dist/dialog.layered.css +2 -1
- package/dist/esm/components/composewindow/composewindow.js +107 -0
- package/dist/esm/components/composewindow/composewindow.recipe.js +42 -0
- package/dist/esm/components/composewindow/index.js +2 -0
- package/dist/esm/components/datatable/datatable.js +226 -45
- package/dist/esm/components/datatable/datatable.recipe.js +38 -4
- package/dist/esm/components/datatable/index.js +6 -0
- package/dist/esm/components/datepickerfield/datepickerfield.js +26 -16
- package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +25 -8
- package/dist/esm/components/dialog/dialog.recipe.js +9 -1
- package/dist/esm/components/menu/menu.js +4 -2
- package/dist/esm/components/multiselect/multiselect.recipe.js +14 -8
- package/dist/esm/components/select/select.js +2 -1
- package/dist/esm/components/table/table.recipe.js +12 -3
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +2 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +38 -8
- package/dist/esm/core/cell-date.js +10 -0
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/product/recordpage/recordpage.js +89 -11
- package/dist/esm/product/recordpage/recordpage.recipe.js +30 -0
- package/dist/esm/product/recordpanel/recordpanel.js +55 -5
- package/dist/esm/product/recordpanel/recordpanel.recipe.js +29 -0
- package/dist/esm/product/recordstable/editable-field.js +73 -0
- package/dist/esm/product/recordstable/index.js +1 -0
- package/dist/esm/product/recordstable/recordstable.js +69 -13
- package/dist/esm/product/recordstable/recordstable.recipe.js +9 -1
- package/dist/esm/product/settingsshell/settingsshell.recipe.js +13 -4
- package/dist/esm/product/sidebar/sidebar.js +15 -3
- package/dist/esm/product/sidebar/sidebar.recipe.js +21 -2
- package/dist/multiselect.css +8 -8
- package/dist/multiselect.layered.css +8 -8
- package/dist/recordpage.css +25 -13
- package/dist/recordpage.layered.css +25 -13
- package/dist/recordpanel.css +22 -5
- package/dist/recordpanel.layered.css +22 -5
- package/dist/recordstable.css +10 -2
- package/dist/recordstable.layered.css +10 -2
- package/dist/settingsshell.css +2 -2
- package/dist/settingsshell.layered.css +2 -2
- package/dist/sidebar.css +7 -2
- package/dist/sidebar.layered.css +7 -2
- package/dist/styles.css +150 -55
- package/dist/styles.layered.css +150 -55
- package/dist/table.css +4 -2
- package/dist/table.layered.css +4 -2
- package/dist/tabletoolbar.css +2 -1
- package/dist/tabletoolbar.layered.css +2 -1
- package/dist/types/components/composewindow/composewindow.d.ts +80 -0
- package/dist/types/components/composewindow/composewindow.recipe.d.ts +3 -0
- package/dist/types/components/composewindow/index.d.ts +2 -0
- package/dist/types/components/datatable/datatable.d.ts +58 -4
- package/dist/types/components/datatable/index.d.ts +1 -1
- package/dist/types/components/datepickerfield/datepickerfield.d.ts +18 -4
- package/dist/types/components/menu/menu.d.ts +10 -1
- package/dist/types/components/select/select.d.ts +6 -1
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +62 -5
- package/dist/types/core/cell-date.d.ts +17 -0
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/product/recordpage/recordpage.d.ts +59 -1
- package/dist/types/product/recordpanel/recordpanel.d.ts +11 -1
- package/dist/types/product/recordstable/editable-field.d.ts +45 -0
- package/dist/types/product/recordstable/index.d.ts +1 -0
- package/dist/types/product/recordstable/recordstable.d.ts +29 -0
- package/dist/types/product/sidebar/sidebar.d.ts +60 -4
- package/package.json +6 -1
|
@@ -41,6 +41,9 @@ const React = __importStar(require("react"));
|
|
|
41
41
|
const avatar_1 = require("../../components/avatar");
|
|
42
42
|
const checkbox_1 = require("../../components/checkbox");
|
|
43
43
|
const menu_1 = require("../../components/menu");
|
|
44
|
+
const select_1 = require("../../components/select");
|
|
45
|
+
const spinner_1 = require("../../components/spinner");
|
|
46
|
+
const switch_1 = require("../../components/switch");
|
|
44
47
|
const tabs_1 = require("../../components/tabs");
|
|
45
48
|
const core_1 = require("../../core");
|
|
46
49
|
const recordstable_1 = require("../recordstable");
|
|
@@ -190,7 +193,8 @@ function EmptyCard({ icon, children, }) {
|
|
|
190
193
|
* Emails sections (Notes & Tasks are live — add and check things off). Drop it
|
|
191
194
|
* into the app's main content area.
|
|
192
195
|
*/
|
|
193
|
-
function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, ...rest }) {
|
|
196
|
+
function RecordPage({ record, columns, collection = "Records", labels, onBack, onToggleFull, full, className, onEdit, tabs: tabsProp, defaultTab, notes: notesProp, onAddNote, tasks: tasksProp, onAddTask, onToggleTask, ...rest }) {
|
|
197
|
+
const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0, recordstable_1.useEditableField)(onEdit);
|
|
194
198
|
const t = React.useMemo(() => ({
|
|
195
199
|
...exports.recordPageLabels,
|
|
196
200
|
...labels,
|
|
@@ -205,8 +209,15 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
205
209
|
},
|
|
206
210
|
}), [labels]);
|
|
207
211
|
const [favorite, setFavorite] = React.useState(false);
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
// Notes/Tasks are controlled when the caller supplies `notes`/`tasks` (even
|
|
213
|
+
// `[]`) -- otherwise RecordPage keeps its own self-contained state, as
|
|
214
|
+
// before (#379). "Controlled" here means the array prop alone decides the
|
|
215
|
+
// mode; there's no meaningful "default seed" worth a separate
|
|
216
|
+
// defaultNotes/defaultTasks prop the way Tabs' defaultValue is.
|
|
217
|
+
const [internalNotes, setInternalNotes] = React.useState([]);
|
|
218
|
+
const [internalTasks, setInternalTasks] = React.useState([]);
|
|
219
|
+
const notes = notesProp ?? internalNotes;
|
|
220
|
+
const tasks = tasksProp ?? internalTasks;
|
|
210
221
|
const [showAllActivity, setShowAllActivity] = React.useState(false);
|
|
211
222
|
// Collapsible + searchable Record Details.
|
|
212
223
|
const [detailsOpen, setDetailsOpen] = React.useState(true);
|
|
@@ -316,7 +327,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
316
327
|
},
|
|
317
328
|
];
|
|
318
329
|
const ti = { size: 15, strokeWidth: 1.75 };
|
|
319
|
-
const
|
|
330
|
+
const builtinTabs = [
|
|
320
331
|
{ value: "overview", label: t.tabs.overview, icon: (0, jsx_runtime_1.jsx)(lucide_react_1.LayoutGrid, { ...ti }) },
|
|
321
332
|
{
|
|
322
333
|
value: "activity",
|
|
@@ -346,9 +357,27 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
346
357
|
badge: 0,
|
|
347
358
|
},
|
|
348
359
|
];
|
|
349
|
-
const addNote = (text) =>
|
|
350
|
-
|
|
351
|
-
|
|
360
|
+
const addNote = (text) => {
|
|
361
|
+
if (notesProp) {
|
|
362
|
+
onAddNote?.(text);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
setInternalNotes((n) => [{ id: nextId(), text, time: t.justNow }, ...n]);
|
|
366
|
+
};
|
|
367
|
+
const addTask = (text) => {
|
|
368
|
+
if (tasksProp) {
|
|
369
|
+
onAddTask?.(text);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
setInternalTasks((n) => [{ id: nextId(), text, done: false }, ...n]);
|
|
373
|
+
};
|
|
374
|
+
const toggleTask = (id) => {
|
|
375
|
+
if (tasksProp) {
|
|
376
|
+
onToggleTask?.(id);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
setInternalTasks((n) => n.map((x) => (x.id === id ? { ...x, done: !x.done } : x)));
|
|
380
|
+
};
|
|
352
381
|
// A small, plausible activity feed derived from the record's own fields.
|
|
353
382
|
const domain = byKey.get("domain");
|
|
354
383
|
const cats = byKey.get("categories");
|
|
@@ -376,7 +405,7 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
376
405
|
const shownActivity = showAllActivity ? activity : activity.slice(0, 2);
|
|
377
406
|
const activityFeed = ((0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.activity, action: activity.length > 2 ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--inline", onClick: () => setShowAllActivity((s) => !s), children: showAllActivity ? t.showLess : t.viewAll })) : undefined }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-feed", children: shownActivity.map((a) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-feed-item", children: [(0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-feed-dot", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Activity, { size: 13, strokeWidth: 2 }) }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-feed-body", children: [a.text, (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-feed-time", children: a.time })] })] }, a.id))) })] }));
|
|
378
407
|
const emailsEmpty = ((0, jsx_runtime_1.jsx)(EmptyCard, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Inbox, { size: 22, strokeWidth: 1.5 }), children: t.noEmailsWith(name) }));
|
|
379
|
-
const
|
|
408
|
+
const builtinPanel = (active) => {
|
|
380
409
|
switch (active) {
|
|
381
410
|
case "overview":
|
|
382
411
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-panel", children: [(0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-section-title", children: t.highlights }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-card", children: (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-grid", children: highlights.map((h) => ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-hl-cell", children: [(0, jsx_runtime_1.jsx)(h.icon, { className: "crisp-recordpage-hl-icon", size: 15, strokeWidth: 1.75 }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-label", children: h.label }), (0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-hl-value", children: h.value ?? ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-hl-value--empty", children: h.empty })) })] }, h.label))) }) })] }), activityFeed, (0, jsx_runtime_1.jsxs)("section", { children: [(0, jsx_runtime_1.jsx)(SectionHead, { title: t.tabs.emails, count: 0 }), emailsEmpty] }), (0, jsx_runtime_1.jsx)(NotesSection, { notes: notes, onAdd: addNote, labels: t }), (0, jsx_runtime_1.jsx)(TasksSection, { tasks: tasks, onAdd: addTask, onToggle: toggleTask, labels: t })] }));
|
|
@@ -392,6 +421,19 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
392
421
|
return ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpage-panel", children: (0, jsx_runtime_1.jsx)(EmptyCard, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Folder, { size: 22, strokeWidth: 1.5 }), children: t.nothingHereYet }) }));
|
|
393
422
|
}
|
|
394
423
|
};
|
|
424
|
+
// #379: `tabs` (consumer-supplied) replaces the CRM set wholesale, rather
|
|
425
|
+
// than merging with it — a mixed set would leave a caller unable to ever
|
|
426
|
+
// drop a built-in tab it doesn't want. `count` maps to Tabs' own `badge`.
|
|
427
|
+
const effectiveTabs = tabsProp
|
|
428
|
+
? tabsProp.map((tb) => ({
|
|
429
|
+
value: tb.id,
|
|
430
|
+
label: tb.label,
|
|
431
|
+
badge: tb.count,
|
|
432
|
+
}))
|
|
433
|
+
: builtinTabs;
|
|
434
|
+
const effectivePanel = tabsProp
|
|
435
|
+
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
436
|
+
: builtinPanel;
|
|
395
437
|
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpage_recipe_1.recordpageRecipe)({ className }), ...rest, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-topbar", children: [(0, jsx_runtime_1.jsxs)("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-crumb-name", children: name })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-actions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? ((0, jsx_runtime_1.jsx)(lucide_react_1.Minimize2, { size: 15, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Maximize2, { size: 15, strokeWidth: 1.75 })) })), (0, jsx_runtime_1.jsxs)(menu_1.Menu, { align: "end", "aria-label": t.recordActions, trigger: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: (0, jsx_runtime_1.jsx)(lucide_react_1.MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [(0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), (0, jsx_runtime_1.jsx)(menu_1.MenuSeparator, {}), (0, jsx_runtime_1.jsx)(menu_1.MenuItem, { icon: (0, jsx_runtime_1.jsx)(lucide_react_1.Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-body", children: [(0, jsx_runtime_1.jsxs)("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-entity-name", children: name }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Pencil, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Star, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-quickactions", children: [(0, jsx_runtime_1.jsxs)("button", { type: "button", className: "crisp-recordpage-compose", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: (0, jsx_runtime_1.jsx)(lucide_react_1.FileText, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: (0, jsx_runtime_1.jsx)(lucide_react_1.Share2, { size: 15, strokeWidth: 1.75 }) }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: (0, jsx_runtime_1.jsx)(lucide_react_1.SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-section", children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-detailhead", children: [searchOpen ? ((0, jsx_runtime_1.jsx)("input", {
|
|
396
438
|
// biome-ignore lint/a11y/noAutofocus: focusing the just-opened field search is expected
|
|
397
439
|
autoFocus: true, type: "text", className: "crisp-recordpage-fieldsearch", placeholder: t.searchPlaceholder, "aria-label": t.searchFields, value: fieldQuery, onChange: (e) => setFieldQuery(e.target.value), onKeyDown: (e) => {
|
|
@@ -405,7 +447,43 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
405
447
|
if (!searchOpen)
|
|
406
448
|
setDetailsOpen(true);
|
|
407
449
|
}, children: searchOpen ? ((0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 14, strokeWidth: 1.75 })) : ((0, jsx_runtime_1.jsx)(lucide_react_1.Search, { size: 14, strokeWidth: 1.75 })) })] }), detailsOpen &&
|
|
408
|
-
(shownFields.length > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [shownFields.map((col) =>
|
|
450
|
+
(shownFields.length > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [shownFields.map((col) => {
|
|
451
|
+
// #431: a field only actually opens when there's an
|
|
452
|
+
// `onEdit` to commit through -- `editable` with nothing
|
|
453
|
+
// to call would be a dead click.
|
|
454
|
+
const canEdit = Boolean(col.editable && onEdit);
|
|
455
|
+
const isPending = pendingKey === col.key;
|
|
456
|
+
const fieldError = error?.key === col.key ? error.message : null;
|
|
457
|
+
const label = ((0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-label", children: [col.icon ? ((0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 })) : null, (0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-field-label-text", children: col.label })] }));
|
|
458
|
+
// select/switch are always their own live widget -- no
|
|
459
|
+
// click-to-open, no separate edit/view mode, the same
|
|
460
|
+
// convention DataTable's identical `editor` columns use
|
|
461
|
+
// (#385): choosing a value or flipping a toggle IS the
|
|
462
|
+
// commit gesture.
|
|
463
|
+
if (canEdit && col.editor) {
|
|
464
|
+
const value = record[col.key];
|
|
465
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", "aria-label": `Edit ${col.label}`, value: value != null ? String(value) : undefined, disabled: isPending, options: col.editOptions ?? [], onValueChange: (v) => commit(record.id, col.key, v) })) : ((0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) })), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" }))] }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
466
|
+
}
|
|
467
|
+
if (canEdit && editingKey === col.key) {
|
|
468
|
+
const value = record[col.key];
|
|
469
|
+
const editor = (0, recordstable_1.editableInputType)(col.type);
|
|
470
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-recordpage-edit",
|
|
471
|
+
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
472
|
+
autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
|
|
473
|
+
if (isPending)
|
|
474
|
+
return;
|
|
475
|
+
if (e.key === "Enter")
|
|
476
|
+
commit(record.id, col.key, e.currentTarget.value);
|
|
477
|
+
else if (e.key === "Escape")
|
|
478
|
+
closeEditing();
|
|
479
|
+
}, onBlur: (e) => {
|
|
480
|
+
if (isPending)
|
|
481
|
+
return;
|
|
482
|
+
commit(record.id, col.key, e.currentTarget.value);
|
|
483
|
+
} }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpage-edit-pending" })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
484
|
+
}
|
|
485
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpage-field", children: [label, (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpage-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-edit-trigger", onClick: () => openEditing(col.key), children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, labels: t }) })) : ((0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, labels: t })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpage-edit-error", children: fieldError }))] })] }, col.key));
|
|
486
|
+
}), canRevealFields && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpage-viewall crisp-recordpage-viewall--section", onClick: () => {
|
|
409
487
|
// Revealing all fields also opens the (focused)
|
|
410
488
|
// field search; collapsing closes it.
|
|
411
489
|
const next = !showAllValues;
|
|
@@ -423,6 +501,6 @@ function RecordPage({ record, columns, collection = "Records", labels, onBack, o
|
|
|
423
501
|
const rtl = (0, core_1.isRtlElement)(e.currentTarget);
|
|
424
502
|
const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
425
503
|
setDetailsW((w) => grow ? Math.min(560, w + 16) : Math.max(300, w - 16));
|
|
426
|
-
} }), (0, jsx_runtime_1.jsx)("main", { className: "crisp-recordpage-main", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs:
|
|
504
|
+
} }), (0, jsx_runtime_1.jsx)("main", { className: "crisp-recordpage-main", children: (0, jsx_runtime_1.jsx)(tabs_1.Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] })] }));
|
|
427
505
|
}
|
|
428
506
|
RecordPage.displayName = "RecordPage";
|
|
@@ -79,7 +79,37 @@ const config = (0, core_1.defineRecipe)({
|
|
|
79
79
|
& .crisp-recordpage-field-label svg { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
|
|
80
80
|
& .crisp-recordpage-field-label-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
81
81
|
& .crisp-recordpage-field-value { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; padding-top: 2px; line-height: 20px; font-size: ${tokens_1.contract.text.lg}; font-weight: ${tokens_1.contract.weight.medium}; color: ${tokens_1.contract.fgPrimary}; }
|
|
82
|
+
& .crisp-recordpage-field-value[data-editable] { cursor: pointer; }
|
|
83
|
+
/* the click-to-edit trigger is a real <button> (keyboard-operable),
|
|
84
|
+
reset back to plain inline text and made a flex-wrap container of its
|
|
85
|
+
own -- FieldValue can render several chips (tags), which still need
|
|
86
|
+
to wrap the way they did before this had a button around them at
|
|
87
|
+
all. It wraps only FieldValue, never the error slot beside it (see
|
|
88
|
+
the component's own comment) -- NOT \`display: contents\`, which
|
|
89
|
+
drops a button's accessible semantics in some engines. */
|
|
90
|
+
& .crisp-recordpage-edit-trigger { display: inline-flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; margin: 0; padding: 0; border: none; background: none; text-align: start; font: inherit; color: inherit; cursor: pointer; }
|
|
82
91
|
& .crisp-recordpage-empty { color: ${tokens_1.contract.fgMuted}; }
|
|
92
|
+
|
|
93
|
+
/* #431: measured live on app.attio.com's record page Details column --
|
|
94
|
+
clicking a field's value opens a plain, borderless field exactly
|
|
95
|
+
filling the value's own box (no border, no background, no
|
|
96
|
+
box-shadow), same font as the value it replaces (hence \`font:
|
|
97
|
+
inherit\`, not a restated size/weight). A field row's own hover tint
|
|
98
|
+
above already carries the "this is interactive" affordance; the
|
|
99
|
+
pending/error treatment isn't Attio-measured -- it reuses the same
|
|
100
|
+
tokens DataTable's identical states already use (opacity 0.5,
|
|
101
|
+
\`bgDanger\`), per #385/#430. */
|
|
102
|
+
& .crisp-recordpage-edit { display: block; width: 100%; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; box-shadow: none; outline: none; font: inherit; color: inherit; }
|
|
103
|
+
/* not Attio-measured (its own field shows no focus ring either, caret
|
|
104
|
+
only) -- kept for real keyboard users, the same brand ring every
|
|
105
|
+
other crisp field already shows on focus (DataTable's identical
|
|
106
|
+
.crisp-datatable-edit:focus, e.g.). */
|
|
107
|
+
& .crisp-recordpage-edit:focus { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
|
|
108
|
+
& .crisp-recordpage-edit:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
109
|
+
& .crisp-recordpage-edit-pending { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
|
|
110
|
+
& .crisp-recordpage-edit-error { flex-basis: 100%; margin-top: ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.bgDanger}; }
|
|
111
|
+
& .crisp-recordpage-edit-picker { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; }
|
|
112
|
+
& .crisp-recordpage-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
|
|
83
113
|
& .crisp-recordpage-link { color: ${tokens_1.contract.fgPrimary}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
84
114
|
& .crisp-recordpage-link:hover { text-decoration: underline; }
|
|
85
115
|
& .crisp-recordpage-emptystate { padding: ${tokens_1.contract.space["2"]} ${tokens_1.contract.space["1"]}; font-size: ${tokens_1.contract.text.md}; color: ${tokens_1.contract.fgMuted}; }
|
|
@@ -38,6 +38,9 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
38
38
|
const lucide_react_1 = require("lucide-react");
|
|
39
39
|
const React = __importStar(require("react"));
|
|
40
40
|
const avatar_1 = require("../../components/avatar");
|
|
41
|
+
const select_1 = require("../../components/select");
|
|
42
|
+
const spinner_1 = require("../../components/spinner");
|
|
43
|
+
const switch_1 = require("../../components/switch");
|
|
41
44
|
const core_1 = require("../../core");
|
|
42
45
|
const recordstable_1 = require("../recordstable");
|
|
43
46
|
const recordpanel_recipe_1 = require("./recordpanel.recipe");
|
|
@@ -63,8 +66,9 @@ function FieldValue({ col, row, emptyLabel, }) {
|
|
|
63
66
|
* relative` container (e.g. the app main area); it slides in from the right and
|
|
64
67
|
* overlays the content.
|
|
65
68
|
*/
|
|
66
|
-
function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", title, detailsLabel = "Details", emptyLabel = "Empty", footer, ...rest }) {
|
|
69
|
+
function RecordPanel({ record, columns, onClose, className, closeLabel = "Close panel", title, detailsLabel = "Details", emptyLabel = "Empty", footer, onEdit, ...rest }) {
|
|
67
70
|
const titleId = React.useId();
|
|
71
|
+
const { editingKey, pendingKey, error, openEditing, closeEditing, commit } = (0, recordstable_1.useEditableField)(onEdit);
|
|
68
72
|
// close on Escape while open -- but not when a Menu/Select/Popover/
|
|
69
73
|
// ContextMenu opened from inside the panel (e.g. a footer "More actions"
|
|
70
74
|
// menu) is itself open (#361): that Escape belongs to the nested layer,
|
|
@@ -89,13 +93,25 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
|
|
|
89
93
|
if (!record)
|
|
90
94
|
return;
|
|
91
95
|
const onKey = (e) => {
|
|
92
|
-
if (e.key !== "Escape"
|
|
96
|
+
if (e.key !== "Escape")
|
|
97
|
+
return;
|
|
98
|
+
// #431: Escape while a field is open cancels JUST that field, the
|
|
99
|
+
// same way DataTable's own editable cells treat it -- not the whole
|
|
100
|
+
// panel. Checked ahead of `hasOpenFloatingLayer()` (capture phase, so
|
|
101
|
+
// this runs before the field's own bubble-phase onKeyDown would ever
|
|
102
|
+
// get a chance to stopPropagation -- same reasoning as the
|
|
103
|
+
// floating-layer check below).
|
|
104
|
+
if (editingKey) {
|
|
105
|
+
closeEditing();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if ((0, core_1.hasOpenFloatingLayer)())
|
|
93
109
|
return;
|
|
94
110
|
onClose();
|
|
95
111
|
};
|
|
96
112
|
document.addEventListener("keydown", onKey, true);
|
|
97
113
|
return () => document.removeEventListener("keydown", onKey, true);
|
|
98
|
-
}, [record, onClose]);
|
|
114
|
+
}, [record, onClose, editingKey, closeEditing]);
|
|
99
115
|
if (!record)
|
|
100
116
|
return null;
|
|
101
117
|
const entityCol = columns.find((c) => c.type === "entity");
|
|
@@ -104,6 +120,40 @@ function RecordPanel({ record, columns, onClose, className, closeLabel = "Close
|
|
|
104
120
|
: undefined;
|
|
105
121
|
const fields = columns.filter((c) => c.type !== "entity");
|
|
106
122
|
const heading = title ?? entity?.name ?? "Record";
|
|
107
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpanel_recipe_1.recordpanelRecipe)({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, (0, jsx_runtime_1.jsx)("span", { id: titleId, className: "crisp-recordpanel-title", children: heading }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 18, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-body", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) =>
|
|
123
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: (0, recordpanel_recipe_1.recordpanelRecipe)({ className }), ...rest, role: "dialog", "aria-labelledby": titleId, children: [(0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-header", children: [entity?.avatar ? ((0, jsx_runtime_1.jsx)("img", { className: "crisp-recordpanel-fav", src: entity.avatar, alt: "" })) : entity ? ((0, jsx_runtime_1.jsx)(avatar_1.Avatar, { size: "md", shape: "rounded", tone: "brand", children: entity.name.charAt(0) })) : null, (0, jsx_runtime_1.jsx)("span", { id: titleId, className: "crisp-recordpanel-title", children: heading }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-iconbtn", "aria-label": closeLabel, onClick: onClose, children: (0, jsx_runtime_1.jsx)(lucide_react_1.X, { size: 18, strokeWidth: 1.75 }) })] }), (0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-body", children: [(0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-sectionlabel", children: detailsLabel }), fields.map((col) => {
|
|
124
|
+
// #431: a field only actually opens when there's an `onEdit` to
|
|
125
|
+
// commit through -- `editable` with nothing to call would be a
|
|
126
|
+
// dead click.
|
|
127
|
+
const canEdit = Boolean(col.editable && onEdit);
|
|
128
|
+
const isPending = pendingKey === col.key;
|
|
129
|
+
const fieldError = error?.key === col.key ? error.message : null;
|
|
130
|
+
// select/switch are always their own live widget -- no click-to-
|
|
131
|
+
// open, no separate edit/view mode, the same convention
|
|
132
|
+
// DataTable's identical `editor` columns use (#385): choosing a
|
|
133
|
+
// value or flipping a toggle IS the commit gesture.
|
|
134
|
+
if (canEdit && col.editor) {
|
|
135
|
+
const value = record[col.key];
|
|
136
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-value", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-edit-picker", children: [col.editor === "select" ? ((0, jsx_runtime_1.jsx)(select_1.Select, { variant: "ghost", "aria-label": `Edit ${col.label}`, value: value != null ? String(value) : undefined, disabled: isPending, options: col.editOptions ?? [], onValueChange: (v) => commit(record.id, col.key, v) })) : ((0, jsx_runtime_1.jsx)(switch_1.Switch, { "aria-label": `Edit ${col.label}`, checked: value === true || value === "true", disabled: isPending, onCheckedChange: (checked) => commit(record.id, col.key, String(checked)) })), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpanel-edit-pending" }))] }), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-edit-error", children: fieldError }))] })] }, col.key));
|
|
137
|
+
}
|
|
138
|
+
if (canEdit && editingKey === col.key) {
|
|
139
|
+
const value = record[col.key];
|
|
140
|
+
const editor = (0, recordstable_1.editableInputType)(col.type);
|
|
141
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-value", children: [(0, jsx_runtime_1.jsx)("input", { className: "crisp-recordpanel-edit",
|
|
142
|
+
// biome-ignore lint/a11y/noAutofocus: focus the field the moment it opens
|
|
143
|
+
autoFocus: true, ...editor, disabled: isPending, defaultValue: value == null ? "" : String(value), "aria-label": `Edit ${col.label}`, onKeyDown: (e) => {
|
|
144
|
+
if (isPending)
|
|
145
|
+
return;
|
|
146
|
+
if (e.key === "Enter")
|
|
147
|
+
commit(record.id, col.key, e.currentTarget.value);
|
|
148
|
+
else if (e.key === "Escape")
|
|
149
|
+
closeEditing();
|
|
150
|
+
}, onBlur: (e) => {
|
|
151
|
+
if (isPending)
|
|
152
|
+
return;
|
|
153
|
+
commit(record.id, col.key, e.currentTarget.value);
|
|
154
|
+
} }), isPending && ((0, jsx_runtime_1.jsx)(spinner_1.Spinner, { size: 14, className: "crisp-recordpanel-edit-pending" })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-edit-error", children: fieldError }))] })] }, col.key));
|
|
155
|
+
}
|
|
156
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "crisp-recordpanel-field", children: [(0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-label", children: [col.icon ? (0, jsx_runtime_1.jsx)(col.icon, { size: 15, strokeWidth: 1.75 }) : null, col.label] }), (0, jsx_runtime_1.jsxs)("span", { className: "crisp-recordpanel-field-value", "data-editable": canEdit ? "" : undefined, children: [canEdit ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "crisp-recordpanel-edit-trigger", onClick: () => openEditing(col.key), children: (0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, emptyLabel: emptyLabel }) })) : ((0, jsx_runtime_1.jsx)(FieldValue, { col: col, row: record, emptyLabel: emptyLabel })), fieldError && ((0, jsx_runtime_1.jsx)("span", { className: "crisp-recordpanel-edit-error", children: fieldError }))] })] }, col.key));
|
|
157
|
+
})] }), footer != null && ((0, jsx_runtime_1.jsx)("div", { className: "crisp-recordpanel-footer", children: footer }))] }));
|
|
108
158
|
}
|
|
109
159
|
RecordPanel.displayName = "RecordPanel";
|
|
@@ -34,8 +34,37 @@ const config = (0, core_1.defineRecipe)({
|
|
|
34
34
|
& .crisp-recordpanel-field-label { display: flex; align-items: center; gap: ${tokens_1.contract.space["1.5"]}; width: 132px; flex-shrink: 0; padding-top: 3px; color: ${tokens_1.contract.fgMuted}; font-size: ${tokens_1.contract.text.md}; }
|
|
35
35
|
& .crisp-recordpanel-field-label svg { flex-shrink: 0; }
|
|
36
36
|
& .crisp-recordpanel-field-value { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; padding-top: 3px; font-size: ${tokens_1.contract.text.lg}; color: ${tokens_1.contract.fgPrimary}; }
|
|
37
|
+
& .crisp-recordpanel-field-value[data-editable] { cursor: pointer; }
|
|
38
|
+
/* the click-to-edit trigger is a real <button> (keyboard-operable),
|
|
39
|
+
reset back to plain inline text and made a flex-wrap container of its
|
|
40
|
+
own -- FieldValue can render several chips (tags), which still need
|
|
41
|
+
to wrap the way they did before this had a button around them at
|
|
42
|
+
all. It wraps only FieldValue, never the error slot beside it (see
|
|
43
|
+
the component's own comment) -- NOT \`display: contents\`, which
|
|
44
|
+
drops a button's accessible semantics in some engines. */
|
|
45
|
+
& .crisp-recordpanel-edit-trigger { display: inline-flex; flex-wrap: wrap; align-items: center; gap: ${tokens_1.contract.space["1"]}; margin: 0; padding: 0; border: none; background: none; text-align: start; font: inherit; color: inherit; cursor: pointer; }
|
|
37
46
|
& .crisp-recordpanel-empty { color: ${tokens_1.contract.fgMuted}; }
|
|
38
47
|
|
|
48
|
+
/* #431: measured live on app.attio.com's record side panel -- clicking a
|
|
49
|
+
field's value opens a plain, borderless field exactly filling the
|
|
50
|
+
value's own box (no border, no background, no box-shadow), same font
|
|
51
|
+
as the value it replaces (hence \`font: inherit\`, not a restated
|
|
52
|
+
size/weight). A field row's own hover tint above already carries the
|
|
53
|
+
"this is interactive" affordance; the pending/error treatment isn't
|
|
54
|
+
Attio-measured -- it reuses the same tokens DataTable's identical
|
|
55
|
+
states already use (opacity 0.5, \`bgDanger\`), per #385/#430. */
|
|
56
|
+
& .crisp-recordpanel-edit { display: block; width: 100%; margin: 0; padding: 0; border: none; border-radius: ${tokens_1.contract.radius.sm}; background: none; box-shadow: none; outline: none; font: inherit; color: inherit; }
|
|
57
|
+
/* not Attio-measured (its own field shows no focus ring either, caret
|
|
58
|
+
only) -- kept for real keyboard users, the same brand ring every
|
|
59
|
+
other crisp field already shows on focus (DataTable's identical
|
|
60
|
+
.crisp-datatable-edit:focus, e.g.). */
|
|
61
|
+
& .crisp-recordpanel-edit:focus { box-shadow: inset 0 0 0 1px ${tokens_1.contract.bgBrandSolid}; }
|
|
62
|
+
& .crisp-recordpanel-edit:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
63
|
+
& .crisp-recordpanel-edit-pending { flex-shrink: 0; color: ${tokens_1.contract.fgMuted}; }
|
|
64
|
+
& .crisp-recordpanel-edit-error { flex-basis: 100%; margin-top: ${tokens_1.contract.space["0.5"]}; font-size: ${tokens_1.contract.text.sm}; color: ${tokens_1.contract.bgDanger}; }
|
|
65
|
+
& .crisp-recordpanel-edit-picker { display: flex; align-items: center; gap: ${tokens_1.contract.space["1"]}; width: 100%; }
|
|
66
|
+
& .crisp-recordpanel-edit-picker .crisp-select-trigger { flex: 1; min-width: 0; }
|
|
67
|
+
|
|
39
68
|
${recordstable_tag_1.recordTagCss}
|
|
40
69
|
& .crisp-recordpanel-link { color: ${tokens_1.contract.bgBrandSolid}; text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
41
70
|
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
@@ -0,0 +1,110 @@
|
|
|
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.useEditableField = useEditableField;
|
|
37
|
+
exports.editableInputType = editableInputType;
|
|
38
|
+
const React = __importStar(require("react"));
|
|
39
|
+
/**
|
|
40
|
+
* The async-commit state machine behind `RecordPanel`/`RecordPage`'s
|
|
41
|
+
* editable fields (#431) — the same shape `DataTable`'s own `commitEdit`
|
|
42
|
+
* uses (#385/#430): a sync `onEdit` (returns `undefined`) commits and closes
|
|
43
|
+
* immediately; a Promise-returning `onEdit` keeps the field open and pending
|
|
44
|
+
* while it settles, then either closes it (resolve) or closes it AND
|
|
45
|
+
* reverts while showing the rejection's message (reject). The revert needs
|
|
46
|
+
* no extra code here either: neither `RecordPanel` nor `RecordPage` mutates
|
|
47
|
+
* `record` itself, so once `editingKey` clears, the field simply renders
|
|
48
|
+
* `record[key]` again — whatever it still says.
|
|
49
|
+
*
|
|
50
|
+
* One hook shared by both consumers rather than two copies of the same
|
|
51
|
+
* three `useState`s and the same `.then` branching.
|
|
52
|
+
*/
|
|
53
|
+
function useEditableField(onEdit) {
|
|
54
|
+
const [editingKey, setEditingKey] = React.useState(null);
|
|
55
|
+
// Set only while an async `onEdit` is in flight -- a sync `onEdit` never
|
|
56
|
+
// touches this.
|
|
57
|
+
const [pendingKey, setPendingKey] = React.useState(null);
|
|
58
|
+
const [error, setError] = React.useState(null);
|
|
59
|
+
// Opening a field's editor also clears any stale error left over from a
|
|
60
|
+
// PREVIOUS failed commit on it -- otherwise a rejection from three edits
|
|
61
|
+
// ago would still be sitting there while the reader tries again.
|
|
62
|
+
const openEditing = React.useCallback((key) => {
|
|
63
|
+
setError(null);
|
|
64
|
+
setEditingKey(key);
|
|
65
|
+
}, []);
|
|
66
|
+
const closeEditing = React.useCallback(() => setEditingKey(null), []);
|
|
67
|
+
const commit = React.useCallback((id, key, value) => {
|
|
68
|
+
setError(null);
|
|
69
|
+
const result = onEdit?.(id, key, value);
|
|
70
|
+
if (result && typeof result.then === "function") {
|
|
71
|
+
setPendingKey(key);
|
|
72
|
+
result.then(() => {
|
|
73
|
+
setPendingKey(null);
|
|
74
|
+
setEditingKey(null);
|
|
75
|
+
}, (err) => {
|
|
76
|
+
setPendingKey(null);
|
|
77
|
+
setEditingKey(null);
|
|
78
|
+
setError({
|
|
79
|
+
key,
|
|
80
|
+
message: err instanceof Error
|
|
81
|
+
? err.message
|
|
82
|
+
: typeof err === "string"
|
|
83
|
+
? err
|
|
84
|
+
: "Could not save",
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
setEditingKey(null);
|
|
90
|
+
}, [onEdit]);
|
|
91
|
+
return { editingKey, pendingKey, error, openEditing, closeEditing, commit };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Which plain editor a column gets when it's `editable` without an
|
|
95
|
+
* `editor: "select" | "switch"` — identical to `DataTable`'s own
|
|
96
|
+
* `editorFor` (#385): number/currency get a numeric field, date a native
|
|
97
|
+
* date field, everything else (text, entity, tags, url, status, reference)
|
|
98
|
+
* a plain text field. Kept as one function so the two never drift apart.
|
|
99
|
+
*/
|
|
100
|
+
function editableInputType(colType) {
|
|
101
|
+
switch (colType) {
|
|
102
|
+
case "number":
|
|
103
|
+
case "currency":
|
|
104
|
+
return { type: "number", inputMode: "decimal" };
|
|
105
|
+
case "date":
|
|
106
|
+
return { type: "date" };
|
|
107
|
+
default:
|
|
108
|
+
return { type: "text", inputMode: "text" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.recordstableRecipe = exports.tagTone = exports.RecordsTable = exports.defaultRows = exports.defaultColumns = void 0;
|
|
3
|
+
exports.recordstableRecipe = exports.tagTone = exports.RecordsTable = exports.defaultRows = exports.defaultColumns = exports.useEditableField = exports.editableInputType = void 0;
|
|
4
|
+
var editable_field_1 = require("./editable-field");
|
|
5
|
+
Object.defineProperty(exports, "editableInputType", { enumerable: true, get: function () { return editable_field_1.editableInputType; } });
|
|
6
|
+
Object.defineProperty(exports, "useEditableField", { enumerable: true, get: function () { return editable_field_1.useEditableField; } });
|
|
4
7
|
var recordstable_1 = require("./recordstable");
|
|
5
8
|
Object.defineProperty(exports, "defaultColumns", { enumerable: true, get: function () { return recordstable_1.defaultColumns; } });
|
|
6
9
|
Object.defineProperty(exports, "defaultRows", { enumerable: true, get: function () { return recordstable_1.defaultRows; } });
|