@crisp-ui-kit/crisp 0.54.10 → 0.55.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/datatable/datatable.js +249 -46
- package/dist/cjs/components/datatable/datatable.recipe.js +24 -0
- package/dist/cjs/components/resizablepanel/resizablepanel.js +10 -1
- package/dist/cjs/components/table/table.js +8 -1
- package/dist/cjs/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/cjs/components/tabletoolbar/tabletoolbar.js +176 -43
- package/dist/cjs/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
- package/dist/cjs/index.js +0 -2
- package/dist/cjs/product/recordpage/column.js +19 -0
- package/dist/cjs/product/recordpage/index.js +6 -1
- package/dist/cjs/product/recordpage/recordpage.js +25 -8
- package/dist/cjs/product/recordpage/test-fixtures.js +55 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/datatable.css +14 -0
- package/dist/datatable.layered.css +14 -0
- package/dist/esm/components/datatable/datatable.js +250 -47
- package/dist/esm/components/datatable/datatable.recipe.js +24 -0
- package/dist/esm/components/resizablepanel/resizablepanel.js +10 -1
- package/dist/esm/components/table/table.js +8 -1
- package/dist/esm/components/tabletoolbar/tabletoolbar.chrome.js +25 -2
- package/dist/esm/components/tabletoolbar/tabletoolbar.js +177 -44
- package/dist/esm/components/tabletoolbar/tabletoolbar.recipe.js +28 -9
- package/dist/esm/index.js +0 -2
- package/dist/esm/product/recordpage/column.js +16 -0
- package/dist/esm/product/recordpage/index.js +2 -0
- package/dist/esm/product/recordpage/recordpage.js +22 -5
- package/dist/esm/product/recordpage/test-fixtures.js +52 -0
- package/dist/esm/product/savedviews/savedviews.recipe.js +13 -11
- package/dist/savedviews.css +6 -18
- package/dist/savedviews.layered.css +6 -18
- package/dist/styles.css +43 -276
- package/dist/styles.layered.css +43 -276
- package/dist/tabletoolbar.css +23 -3
- package/dist/tabletoolbar.layered.css +23 -3
- package/dist/types/components/datatable/datatable.d.ts +30 -3
- package/dist/types/components/table/table.d.ts +14 -0
- package/dist/types/components/tabletoolbar/tabletoolbar.d.ts +26 -1
- package/dist/types/index.d.ts +0 -2
- package/dist/types/product/recordpage/column.d.ts +112 -0
- package/dist/types/product/recordpage/index.d.ts +2 -0
- package/dist/types/product/recordpage/recordpage.d.ts +1 -1
- package/dist/types/product/recordpage/test-fixtures.d.ts +3 -0
- package/dist/types/product/recordpanel/recordpanel.d.ts +1 -1
- package/package.json +2 -12
- package/dist/cjs/product/recordstable/company-logos.js +0 -29
- package/dist/cjs/product/recordstable/index.js +0 -13
- package/dist/cjs/product/recordstable/recordstable.js +0 -1357
- package/dist/cjs/product/recordstable/recordstable.recipe.js +0 -175
- package/dist/cjs/product/recordstable/recordstable.tag.js +0 -26
- package/dist/cjs/product/taskstable/index.js +0 -7
- package/dist/cjs/product/taskstable/taskstable.js +0 -27
- package/dist/cjs/product/taskstable/taskstable.recipe.js +0 -56
- package/dist/esm/product/recordstable/company-logos.js +0 -26
- package/dist/esm/product/recordstable/index.js +0 -3
- package/dist/esm/product/recordstable/recordstable.js +0 -1317
- package/dist/esm/product/recordstable/recordstable.recipe.js +0 -172
- package/dist/esm/product/recordstable/recordstable.tag.js +0 -23
- package/dist/esm/product/taskstable/index.js +0 -2
- package/dist/esm/product/taskstable/taskstable.js +0 -24
- package/dist/esm/product/taskstable/taskstable.recipe.js +0 -53
- package/dist/recordstable.css +0 -223
- package/dist/recordstable.d.ts +0 -7
- package/dist/recordstable.layered.css +0 -225
- package/dist/taskstable.css +0 -30
- package/dist/taskstable.d.ts +0 -7
- package/dist/taskstable.layered.css +0 -32
- package/dist/types/product/recordstable/company-logos.d.ts +0 -1
- package/dist/types/product/recordstable/index.d.ts +0 -3
- package/dist/types/product/recordstable/recordstable.d.ts +0 -289
- package/dist/types/product/recordstable/recordstable.recipe.d.ts +0 -5
- package/dist/types/product/recordstable/recordstable.tag.d.ts +0 -15
- package/dist/types/product/taskstable/index.d.ts +0 -2
- package/dist/types/product/taskstable/taskstable.d.ts +0 -54
- package/dist/types/product/taskstable/taskstable.recipe.d.ts +0 -3
- /package/dist/cjs/product/{recordstable → recordpage}/editable-field.js +0 -0
- /package/dist/esm/product/{recordstable → recordpage}/editable-field.js +0 -0
- /package/dist/types/product/{recordstable → recordpage}/editable-field.d.ts +0 -0
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ArrowDownWideNarrow, ArrowUpDown, ArrowUpNarrowWide, Check, ChevronDown, ChevronLeft, ChevronRight, CircleHelp, Download, GripVertical, ListFilter, MoreVertical, Plus, SlidersHorizontal, Trash2, Upload, X, } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { FloatingPortal, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, useCrispMessages, useFocusAfterRemoval, } from "../../core/index.js";
|
|
5
6
|
import { DatePicker, } from "../datepicker/index.js";
|
|
6
7
|
import { MenuCheckboxItem, useMenuKeyboard } from "../menu/index.js";
|
|
7
8
|
import { Select } from "../select/index.js";
|
|
8
9
|
import { tabletoolbarRecipe } from "./tabletoolbar.recipe.js";
|
|
10
|
+
// crisp/#766, #772: portals View settings/Import-Export into a
|
|
11
|
+
// consumer-supplied target (e.g. a `<span>` rendered next to `SavedViews`,
|
|
12
|
+
// or another next to the primary action) instead of this toolbar's own row,
|
|
13
|
+
// when one is given -- a plain passthrough otherwise, so the default (no
|
|
14
|
+
// ref given) stays byte-identical to before this existed. Kept outside the
|
|
15
|
+
// main component (no closure over its state) so it's a stable reference
|
|
16
|
+
// across renders.
|
|
17
|
+
//
|
|
18
|
+
// #772 fix: every `.crisp-tabletoolbar-*` chrome rule is nested under this
|
|
19
|
+
// recipe's own root class (`& .crisp-tabletoolbar-tool { ... }`), which
|
|
20
|
+
// compiles to a real CSS descendant combinator -- it needs actual DOM
|
|
21
|
+
// containment under an element carrying that class. A portal target
|
|
22
|
+
// rendered by the CONSUMER, outside this component's own root `<div>`,
|
|
23
|
+
// breaks that containment even though React still considers the portaled
|
|
24
|
+
// content a child for event/context purposes: the classes were still
|
|
25
|
+
// applied, they just matched nothing, so the buttons rendered as unstyled
|
|
26
|
+
// native chrome. Same failure class `select.recipe.ts`'s own doc comment
|
|
27
|
+
// describes for its listbox portal to `document.body`. Fixed the same way
|
|
28
|
+
// `ToolbarTool` (below) already lets a bare `.crisp-tabletoolbar-tool` be
|
|
29
|
+
// used outside an actual `TableToolbar` tree: wrap the portaled content in
|
|
30
|
+
// its own `tabletoolbarRecipe()`-classed span. `display: contents` on that
|
|
31
|
+
// recipe's own base rule means the wrapper contributes no box of its own,
|
|
32
|
+
// so it changes nothing about layout -- only re-establishes the selector
|
|
33
|
+
// match.
|
|
34
|
+
function ViewBarSlot({ target, children, }) {
|
|
35
|
+
return target
|
|
36
|
+
? createPortal(_jsx("span", { className: tabletoolbarRecipe(), children: children }), target)
|
|
37
|
+
: children;
|
|
38
|
+
}
|
|
9
39
|
// #592: shared by every direction-bearing ChevronRight usage below (the
|
|
10
40
|
// field picker's own disclosure chevron, a flattened search result's
|
|
11
41
|
// breadcrumb separator, and a filter chip's own breadcrumb separator) so
|
|
@@ -299,7 +329,7 @@ export function applySorts(rows, sorts, get = (row, key) => row[key]) {
|
|
|
299
329
|
* this (Sort: Due date/Assignee/Creation date; Filter: Assignee/Record/
|
|
300
330
|
* Created by), which one shared list can't reproduce.
|
|
301
331
|
*/
|
|
302
|
-
export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
|
|
332
|
+
export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColumns: filterColumnsProp, sorts: sortsProp, defaultSorts, onSortsChange, filters: filtersProp, defaultFilters, onFiltersChange, count, children, helpHref, className, sortLabel: sortLabelProp, filterLabel: filterLabelProp, reorderSortLabel: reorderSortLabelProp, noSortsLabel: noSortsLabelProp, addSortLabel: addSortLabelProp, learnSortingLabel: learnSortingLabelProp, selectConditionLabel: selectConditionLabelProp, sortFieldLabel: sortFieldLabelProp, sortDirectionLabel: sortDirectionLabelProp, removeSortLabel: removeSortLabelProp, filterFieldLabel: filterFieldLabelProp, filterFieldPlaceholder: filterFieldPlaceholderProp, filterValuePlaceholder: filterValuePlaceholderProp, filterOptionsLabel: filterOptionsLabelProp, deleteFilterLabel: deleteFilterLabelProp, ascendingLabel: ascendingLabelProp, descendingLabel: descendingLabelProp, sortTriggerLabel, filterTriggerLabel, inline, showSort = true, showViewSettings = true, viewSettingsRef, importExportRef, viewColumns: viewColumnsProp, shownKeys: shownKeysProp, defaultShownKeys, onShownKeysChange, viewSettingsLabel: viewSettingsLabelProp, reorderAttributeLabel: reorderAttributeLabelProp, hideAttributeLabel, addAttributeToViewLabel: addAttributeToViewLabelProp, searchAttributesPlaceholder: searchAttributesPlaceholderProp, importExportLabel: importExportLabelProp, onImportCsv, onExportCsv, onExportExcel, importExportHelpHref, importCsvLabel: importCsvLabelProp, exportCsvLabel: exportCsvLabelProp, exportExcelLabel: exportExcelLabelProp, learnImportExportLabel: learnImportExportLabelProp, backLabel: backLabelProp, nestedFieldPlaceholder, ...rest }) {
|
|
303
333
|
// crisp/#471: an explicit prop always wins; missing, it reads the nearest
|
|
304
334
|
// CrispMessagesProvider's translation of the English default below, or
|
|
305
335
|
// that default itself with no provider — unchanged from before this
|
|
@@ -369,6 +399,25 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
369
399
|
setShownKeysState(next);
|
|
370
400
|
onShownKeysChange?.(next);
|
|
371
401
|
};
|
|
402
|
+
// crisp/#766: a ref's `.current` is null during THIS render (a ref only
|
|
403
|
+
// attaches during commit, and the consumer's own target element may not
|
|
404
|
+
// even exist in the DOM yet on first paint) -- an effect after mount picks
|
|
405
|
+
// it up and re-renders once, the same one-render-late pattern the floating
|
|
406
|
+
// popovers elsewhere in this file already use for a portal target.
|
|
407
|
+
const [viewSettingsTarget, setViewSettingsTarget] = React.useState(null);
|
|
408
|
+
React.useEffect(() => {
|
|
409
|
+
setViewSettingsTarget(viewSettingsRef?.current ?? null);
|
|
410
|
+
}, [viewSettingsRef]);
|
|
411
|
+
const [importExportTarget, setImportExportTarget] = React.useState(null);
|
|
412
|
+
React.useEffect(() => {
|
|
413
|
+
setImportExportTarget(importExportRef?.current ?? null);
|
|
414
|
+
}, [importExportRef]);
|
|
415
|
+
const rootRef = React.useRef(null);
|
|
416
|
+
const viewSettingsTriggerRef = React.useRef(null);
|
|
417
|
+
const viewSettingsContentRef = React.useRef(null);
|
|
418
|
+
const [viewSettingsPos, setViewSettingsPos] = React.useState({});
|
|
419
|
+
const importExportTriggerRef = React.useRef(null);
|
|
420
|
+
const [importExportPos, setImportExportPos] = React.useState({});
|
|
372
421
|
const [openMenu, setOpenMenu] = React.useState(null);
|
|
373
422
|
const [dragSort, setDragSort] = React.useState(null);
|
|
374
423
|
const [filterQuery, setFilterQuery] = React.useState("");
|
|
@@ -396,6 +445,65 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
396
445
|
// built here -- hiding an attribute is a trailing control instead, see
|
|
397
446
|
// the hide button below).
|
|
398
447
|
const [addingAttribute, setAddingAttribute] = React.useState(false);
|
|
448
|
+
// crisp/#807: `viewSettingsRef`/`importExportRef` (#766) relocate the
|
|
449
|
+
// TRIGGER out of this toolbar's own row, but their POPOVERS stayed
|
|
450
|
+
// `position: absolute` inside the (now relocated) trigger's own
|
|
451
|
+
// `.crisp-tabletoolbar-toolgroup` -- correct chrome-wise (#772 already
|
|
452
|
+
// fixed that), but its stacking is trapped inside whatever DOM branch the
|
|
453
|
+
// consumer's own `viewSettingsRef` slot lives in. A consumer header
|
|
454
|
+
// sitting in an EARLIER sibling branch than the toolbar/grid below it
|
|
455
|
+
// (GRC's own layout) has that later branch simply paint over the popover
|
|
456
|
+
// regardless of its own z-index: 30, because z-index only orders siblings
|
|
457
|
+
// WITHIN the same stacking context, and the two branches aren't one.
|
|
458
|
+
// Fixed the same way `Menu`/`Select`/`Popover` already solve this: the
|
|
459
|
+
// popover content itself portals to `document.body` (`FloatingPortal`),
|
|
460
|
+
// positioned with `placeFloating` against the trigger's own rect (a real
|
|
461
|
+
// `position: fixed` box, anchored by viewport coordinates, escapes ANY
|
|
462
|
+
// ancestor's stacking context) -- not just relocated alongside the
|
|
463
|
+
// trigger. Wrapped in its own `tabletoolbarRecipe()` span (the same
|
|
464
|
+
// reason `ViewBarSlot` wraps the trigger) so every nested
|
|
465
|
+
// `.crisp-tabletoolbar-*` class inside the popover still resolves once
|
|
466
|
+
// portaled outside the toolbar's own root. Sort/Filter's own popovers
|
|
467
|
+
// are untouched -- they never relocate, so they were never trapped.
|
|
468
|
+
const placeViewSettingsMenu = () => {
|
|
469
|
+
const rect = viewSettingsTriggerRef.current?.getBoundingClientRect();
|
|
470
|
+
if (!rect)
|
|
471
|
+
return;
|
|
472
|
+
const { style } = placeFloating({
|
|
473
|
+
rect,
|
|
474
|
+
gap: 4,
|
|
475
|
+
flipBelow: 280,
|
|
476
|
+
anchor: viewSettingsTriggerRef.current,
|
|
477
|
+
});
|
|
478
|
+
setViewSettingsPos(style);
|
|
479
|
+
};
|
|
480
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reposition each time it opens (and when its own screen changes, since the two screens differ in height)
|
|
481
|
+
React.useLayoutEffect(() => {
|
|
482
|
+
if (openMenu === "viewsettings")
|
|
483
|
+
placeViewSettingsMenu();
|
|
484
|
+
}, [openMenu, addingAttribute]);
|
|
485
|
+
React.useEffect(() => {
|
|
486
|
+
if (openMenu !== "viewsettings" || !viewSettingsContentRef.current)
|
|
487
|
+
return;
|
|
488
|
+
return registerFloatingLayer(viewSettingsContentRef.current);
|
|
489
|
+
}, [openMenu]);
|
|
490
|
+
const placeImportExportMenu = () => {
|
|
491
|
+
const rect = importExportTriggerRef.current?.getBoundingClientRect();
|
|
492
|
+
if (!rect)
|
|
493
|
+
return;
|
|
494
|
+
const { style } = placeFloating({
|
|
495
|
+
rect,
|
|
496
|
+
gap: 4,
|
|
497
|
+
flipBelow: 280,
|
|
498
|
+
anchor: importExportTriggerRef.current,
|
|
499
|
+
});
|
|
500
|
+
setImportExportPos(style);
|
|
501
|
+
};
|
|
502
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reposition each time it opens
|
|
503
|
+
React.useLayoutEffect(() => {
|
|
504
|
+
if (openMenu === "importexport")
|
|
505
|
+
placeImportExportMenu();
|
|
506
|
+
}, [openMenu]);
|
|
399
507
|
// close any open menu on outside click (anything not inside a menu or its
|
|
400
508
|
// trigger) or Escape
|
|
401
509
|
React.useEffect(() => {
|
|
@@ -546,6 +654,13 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
546
654
|
close: () => setOpenMenu(null),
|
|
547
655
|
contentRef: importExportMenuRef,
|
|
548
656
|
});
|
|
657
|
+
// crisp/#807: registers the (now-portaled) Import/Export menu on the
|
|
658
|
+
// shared floating-layer stack, the same reason `Menu` itself does.
|
|
659
|
+
React.useEffect(() => {
|
|
660
|
+
if (openMenu !== "importexport" || !importExportMenuRef.current)
|
|
661
|
+
return;
|
|
662
|
+
return registerFloatingLayer(importExportMenuRef.current);
|
|
663
|
+
}, [openMenu]);
|
|
549
664
|
// #754: generalized from the Filter-only `filterFieldPicker` (#592) to
|
|
550
665
|
// also drive Sort's own field picker and View settings' add-attribute
|
|
551
666
|
// screen — all three offer the identical nested-drill-down construct on
|
|
@@ -602,16 +717,23 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
602
717
|
// worse than an imperfect word order); `crispMessages` rather than a
|
|
603
718
|
// second `useCrispMessage` call, since this only fires on the branch that
|
|
604
719
|
// needs it.
|
|
720
|
+
// crisp/#788: "Sorted by" and the field name are NOT a font-weight split
|
|
721
|
+
// (both measure 500/medium live) -- the difference is colour/opacity only:
|
|
722
|
+
// "Sorted by" at 63% black, the field name at full-strength near-black
|
|
723
|
+
// (rgb(16,17,18)). The "+N" suffix is smaller (12px vs 14px) and its own
|
|
724
|
+
// mid-grey (rgb(80,81,85)), not muted-63%-black either -- three distinct
|
|
725
|
+
// treatments, not two. `sortField` isolates the resolved label so the
|
|
726
|
+
// default (non-`sortTriggerLabel`) render can wrap each part in its own
|
|
727
|
+
// span; `sortedLabel` stays a single string for the `sortTriggerLabel`
|
|
728
|
+
// escape hatch (a caller-supplied formatter can't be split, by definition)
|
|
729
|
+
// and the empty-state `sortLabel`.
|
|
730
|
+
const sortField = resolveColumnPath(sortColumns, sorts[0]?.key ?? "").at(-1)?.label;
|
|
605
731
|
const sortedLabel = sortTriggerLabel
|
|
606
732
|
? sortTriggerLabel(sorts, sortColumns)
|
|
607
733
|
: sorts.length
|
|
608
|
-
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${
|
|
609
|
-
// #754: sorts[0].key may be a dot-joined nested path
|
|
610
|
-
// ("team.jobTitle") — resolve it the same way the chip and sort
|
|
611
|
-
// row do, rather than a flat find that fails on a nested key.
|
|
612
|
-
resolveColumnPath(sortColumns, sorts[0].key).at(-1)?.label}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
734
|
+
? `${crispMessages["Sorted by"] ?? "Sorted by"} ${sortField}${sorts.length > 1 ? ` +${sorts.length - 1}` : ""}`
|
|
613
735
|
: sortLabel;
|
|
614
|
-
return (_jsx("div", { className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : ""}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortedLabel] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
736
|
+
return (_jsx("div", { ref: rootRef, className: tabletoolbarRecipe({ className }), ...rest, children: _jsxs("div", { className: "crisp-tabletoolbar-toolbar", "data-inline": inline ? "" : undefined, children: [showSort && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool${sorts.length ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => setOpenMenu(openMenu === "sort" ? null : "sort"), "aria-haspopup": "dialog", "aria-expanded": openMenu === "sort", "data-menu-trigger": true, children: [_jsx(ArrowUpDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), sortTriggerLabel || !sorts.length ? (sortedLabel) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "crisp-tabletoolbar-sorted-prefix", children: crispMessages["Sorted by"] ?? "Sorted by" }), " ", _jsx("span", { className: "crisp-tabletoolbar-sorted-field", children: sortField }), sorts.length > 1 && (_jsxs("span", { className: "crisp-tabletoolbar-sorted-count", children: [" ", "+", sorts.length - 1] }))] }))] }), openMenu === "sort" && (_jsxs("div", { ref: sortListRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": sortLabel, children: [sorts.length === 0 ? (_jsx("div", { className: "crisp-tabletoolbar-sort-empty", children: noSortsLabel })) : (sorts.map((s, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
615
737
|
if (dragSort !== null)
|
|
616
738
|
e.preventDefault();
|
|
617
739
|
}, onDrop: () => {
|
|
@@ -648,7 +770,7 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
648
770
|
label: (_jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel", children: [_jsx(ArrowDownWideNarrow, { size: 15, strokeWidth: 1.75 }), descendingLabel] })),
|
|
649
771
|
keywords: descendingLabel,
|
|
650
772
|
},
|
|
651
|
-
], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool
|
|
773
|
+
], "aria-label": sortDirectionLabel }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => removeSort(i), "aria-label": removeSortLabel, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, s.key)))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { ref: addSortRef, type: "button", className: "crisp-tabletoolbar-sort-add", onClick: addSort, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addSortLabel] }) }), helpHref && (_jsx("div", { className: "crisp-tabletoolbar-sort-foot", children: _jsxs("a", { className: "crisp-tabletoolbar-sort-learn", href: helpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 14, strokeWidth: 1.75 }), learnSortingLabel] }) }))] }))] })), showSort && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: `crisp-tabletoolbar-tool${activeFilters ? " crisp-tabletoolbar-tool--active" : " crisp-tabletoolbar-tool--dashed"}`, onClick: () => {
|
|
652
774
|
setFieldPickerPath([]);
|
|
653
775
|
setOpenMenu(openMenu === "filter" ? null : "filter");
|
|
654
776
|
}, "aria-haspopup": "dialog", "aria-expanded": openMenu === "filter", "data-menu-trigger": true, children: [_jsx(ListFilter, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), filterTriggerLabel
|
|
@@ -747,43 +869,54 @@ export function TableToolbar({ columns, sortColumns: sortColumnsProp, filterColu
|
|
|
747
869
|
}, children: label }, label))) })] })) : (_jsx("input", { className: "crisp-tabletoolbar-menu-input", type: col?.type === "number" ? "number" : "text", placeholder: filterValuePlaceholder, "aria-label": `${col?.label} filter value`,
|
|
748
870
|
// biome-ignore lint/a11y/noAutofocus: focus the value field
|
|
749
871
|
autoFocus: true, value: cond.value, onChange: (e) => patchFilter(cond.id, { value: e.target.value }) })) }))] }, cond.id));
|
|
750
|
-
}), showViewSettings && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
},
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
}, children: [
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
872
|
+
}), showViewSettings && !viewSettingsTarget && (_jsx("span", { className: "crisp-tabletoolbar-divider", "aria-hidden": "true" })), showViewSettings && (_jsx(ViewBarSlot, { target: viewSettingsTarget, children: _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { ref: viewSettingsTriggerRef, type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => {
|
|
873
|
+
if (openMenu === "viewsettings") {
|
|
874
|
+
setOpenMenu(null);
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
setAddingAttribute(false);
|
|
878
|
+
setFieldPickerPath([]);
|
|
879
|
+
setFilterQuery("");
|
|
880
|
+
setOpenMenu("viewsettings");
|
|
881
|
+
}
|
|
882
|
+
}, "aria-haspopup": "dialog", "aria-expanded": openMenu === "viewsettings", "data-menu-trigger": true, children: [_jsx(SlidersHorizontal, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), viewSettingsLabel, _jsx(ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "viewsettings" && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("span", { className: tabletoolbarRecipe(), children: addingAttribute ? (_jsx("div", { ref: viewSettingsContentRef, className: "crisp-tabletoolbar-menu", role: "dialog", "aria-label": addAttributeToViewLabel, style: {
|
|
883
|
+
position: "fixed",
|
|
884
|
+
zIndex: 70,
|
|
885
|
+
...viewSettingsPos,
|
|
886
|
+
}, children: nestedFieldPicker(hiddenAttrs, showAttr, searchAttributesPlaceholder, searchAttributesPlaceholder) })) : (_jsxs("div", { ref: viewSettingsContentRef, className: "crisp-tabletoolbar-menu crisp-tabletoolbar-sortpop", role: "dialog", "aria-label": viewSettingsLabel, style: {
|
|
887
|
+
position: "fixed",
|
|
888
|
+
zIndex: 70,
|
|
889
|
+
...viewSettingsPos,
|
|
890
|
+
}, children: [shownAttrs.map((col, i) => (_jsxs("div", { className: "crisp-tabletoolbar-sort-row", onDragOver: (e) => {
|
|
891
|
+
if (dragAttr !== null)
|
|
892
|
+
e.preventDefault();
|
|
893
|
+
}, onDrop: () => {
|
|
894
|
+
if (dragAttr !== null)
|
|
895
|
+
moveAttr(dragAttr, i);
|
|
896
|
+
setDragAttr(null);
|
|
897
|
+
}, children: [_jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-grip", "aria-label": reorderAttributeLabel, draggable: true, onDragStart: () => setDragAttr(i), onDragEnd: () => setDragAttr(null), children: _jsx(GripVertical, { size: 15, strokeWidth: 1.75 }) }), _jsxs("span", { className: "crisp-tabletoolbar-sort-optlabel crisp-tabletoolbar-sort-field", children: [col.icon ? (_jsx(col.icon, { size: 15, strokeWidth: 1.75 })) : null, col.label] }), _jsx("button", { type: "button", className: "crisp-tabletoolbar-sort-remove", onClick: () => hideAttr(col.key), "aria-label": hideAttributeLabel?.(col.label) ??
|
|
898
|
+
`${crispMessages.Hide ?? "Hide"} ${col.label} ${crispMessages["from view"] ?? "from view"}`, children: _jsx(X, { size: 15, strokeWidth: 1.75 }) })] }, col.key))), _jsx("div", { className: "crisp-tabletoolbar-sort-addrow", children: _jsxs("button", { type: "button", className: "crisp-tabletoolbar-sort-add", onClick: () => {
|
|
899
|
+
setAddingAttribute(true);
|
|
900
|
+
setFieldPickerPath([]);
|
|
901
|
+
setFilterQuery("");
|
|
902
|
+
}, children: [_jsx(Plus, { size: 15, strokeWidth: 1.75 }), addAttributeToViewLabel] }) })] })) }) }))] }) })), (onImportCsv ||
|
|
774
903
|
onExportCsv ||
|
|
775
904
|
onExportExcel ||
|
|
776
|
-
importExportHelpHref) && (_jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => setOpenMenu(openMenu === "importexport" ? null : "importexport"), "aria-haspopup": "menu", "aria-expanded": openMenu === "importexport", "data-menu-trigger": true, children: [_jsx(Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, _jsx(ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && (_jsxs("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown,
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
905
|
+
importExportHelpHref) && (_jsx(ViewBarSlot, { target: importExportTarget, children: _jsxs("span", { className: "crisp-tabletoolbar-toolgroup", children: [_jsxs("button", { ref: importExportTriggerRef, type: "button", className: "crisp-tabletoolbar-tool crisp-tabletoolbar-tool--raised", onClick: () => setOpenMenu(openMenu === "importexport" ? null : "importexport"), "aria-haspopup": "menu", "aria-expanded": openMenu === "importexport", "data-menu-trigger": true, children: [_jsx(Upload, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" }), importExportLabel, _jsx(ChevronDown, { size: 14, strokeWidth: 1.75, "aria-hidden": "true" })] }), openMenu === "importexport" && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsx("span", { className: tabletoolbarRecipe(), children: _jsxs("div", { ref: importExportMenuRef, className: "crisp-tabletoolbar-menu", role: "menu", "aria-label": importExportLabel, onKeyDown: onImportExportKeyDown, style: {
|
|
906
|
+
position: "fixed",
|
|
907
|
+
zIndex: 70,
|
|
908
|
+
...importExportPos,
|
|
909
|
+
}, children: [onImportCsv && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
910
|
+
onImportCsv();
|
|
911
|
+
setOpenMenu(null);
|
|
912
|
+
}, children: [_jsx(Upload, { size: 15, strokeWidth: 1.75 }), importCsvLabel] })), onImportCsv && (onExportCsv || onExportExcel) && (_jsx("hr", { className: "crisp-tabletoolbar-menu-sep" })), onExportCsv && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
913
|
+
onExportCsv();
|
|
914
|
+
setOpenMenu(null);
|
|
915
|
+
}, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportCsvLabel] })), onExportExcel && (_jsxs("button", { type: "button", role: "menuitem", className: "crisp-tabletoolbar-menu-item", onClick: () => {
|
|
916
|
+
onExportExcel();
|
|
917
|
+
setOpenMenu(null);
|
|
918
|
+
}, children: [_jsx(Download, { size: 15, strokeWidth: 1.75 }), exportExcelLabel] })), importExportHelpHref &&
|
|
919
|
+
(onImportCsv || onExportCsv || onExportExcel) && (_jsx("hr", { className: "crisp-tabletoolbar-menu-sep" })), importExportHelpHref && (_jsxs("a", { className: "crisp-tabletoolbar-menu-item", role: "menuitem", href: importExportHelpHref, target: "_blank", rel: "noreferrer", children: [_jsx(CircleHelp, { size: 15, strokeWidth: 1.75 }), learnImportExportLabel] }))] }) }) }))] }) })), _jsx("span", { className: "crisp-tabletoolbar-spacer" }), count != null && (_jsx("span", { className: "crisp-tabletoolbar-count", children: count })), children] }) }));
|
|
787
920
|
}
|
|
788
921
|
/**
|
|
789
922
|
* The Sort/Filter chip's own chrome (`.crisp-tabletoolbar-tool`), standalone
|
|
@@ -71,14 +71,21 @@ const filterChipChrome = `
|
|
|
71
71
|
& .crisp-tabletoolbar-datepop-presets { display: flex; flex-wrap: wrap; gap: ${c.space["1"]}; margin-top: ${c.space["2"]}; padding: 0 ${c.space["1"]}; }
|
|
72
72
|
& .crisp-tabletoolbar-datepop-preset { height: ${c.size.chip}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.chip}; background: ${c.bgRaised}; color: ${c.fgPrimary}; font-family: inherit; font-size: ${c.text.sm}; cursor: pointer; }
|
|
73
73
|
& .crisp-tabletoolbar-datepop-preset:hover { background: color-mix(in srgb, ${c.fgPrimary} 8%, transparent); }`;
|
|
74
|
-
// #595
|
|
75
|
-
// view bar: `28px` tall, `radius.md`
|
|
76
|
-
// (`rgb(
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
74
|
+
// #595, corrected #772 (re-measured live 2026-09-25): the view bar's raised
|
|
75
|
+
// trigger chrome — `app.attio.com`'s own view bar: `28px` tall, `radius.md`
|
|
76
|
+
// (8px), background `bgSurface` (white, `rgb(255,255,255)` exact match — the
|
|
77
|
+
// #595 pass originally measured `bgSurfaceMuted`/`rgb(246,247,247)`, which
|
|
78
|
+
// re-measuring live this pass shows was wrong, not a later Attio change: ALL
|
|
79
|
+
// THREE of the switcher, View settings and Import/Export read pure white)
|
|
80
|
+
// and `elevation.field`'s exact 3-layer shadow (`inset 0 0 0 1px transparent,
|
|
81
|
+
// 0 0 2px rgba(28,40,64,.18), 0 1px 3px rgba(0,0,0,.04)`) — distinct from
|
|
82
|
+
// Sort/Filter's plain ringed `.tool` (no elevation shadow; see `--dashed` in
|
|
83
|
+
// `tabletoolbar.chrome.ts` for their own OWN empty-state chrome). Originally
|
|
84
|
+
// built for Import/Export alone ("the only trigger... with a trailing
|
|
85
|
+
// chevron") — that was also wrong: View settings and the view switcher both
|
|
86
|
+
// carry one too, confirmed live (3 children each: icon, label, chevron).
|
|
87
|
+
// Now shared by View settings' own trigger (`tabletoolbar.tsx`) as well as
|
|
88
|
+
// Import/Export's. Overrides `.tool`'s own base background/shadow by cascade
|
|
82
89
|
// order (this block is appended after `toolbarChromeBody`, which defines
|
|
83
90
|
// them), the same way `.tool--ghost` already does. `border`/`font-family`/
|
|
84
91
|
// `cursor` are repeated here too, even though `.tool`'s own base rule (in
|
|
@@ -88,7 +95,18 @@ const filterChipChrome = `
|
|
|
88
95
|
// behind `chrome.ts`'s function call is invisible to it; harmless repeats,
|
|
89
96
|
// not a real gap.
|
|
90
97
|
const importExportChrome = `
|
|
91
|
-
& .crisp-tabletoolbar-tool--raised { border: none; background: ${c.
|
|
98
|
+
& .crisp-tabletoolbar-tool--raised { border: none; background: ${c.bgSurface}; box-shadow: ${e.field}; font-family: inherit; cursor: pointer; }`;
|
|
99
|
+
// crisp/#788: the Sort trigger's own "Sorted by X (+N)" label, measured live
|
|
100
|
+
// against Attio's own chip -- three parts, not one flat string, and NOT a
|
|
101
|
+
// font-weight split (all three read 500/medium): "Sorted by" is muted (63%
|
|
102
|
+
// black), the resolved field name is full-strength near-black, and a
|
|
103
|
+
// trailing "+N" (when more than one sort is active) is both smaller (12px
|
|
104
|
+
// vs 14px) and its own mid-grey, distinct from the "Sorted by" muted tone.
|
|
105
|
+
// TableToolbar-only, same reasoning as `filterChipChrome` above (RecordsTable
|
|
106
|
+
// keeps its own older Sort label markup, not touched here).
|
|
107
|
+
const sortedLabelChrome = `
|
|
108
|
+
& .crisp-tabletoolbar-sorted-prefix { color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
109
|
+
& .crisp-tabletoolbar-sorted-count { font-size: ${c.text.sm}; color: color-mix(in srgb, ${c.fgPrimary} 73%, transparent); }`;
|
|
92
110
|
// The table toolbar, measured: 28px controls, the Sort/Filter buttons and
|
|
93
111
|
// the menus they open, sort-condition rows, and the filter chips. The chrome
|
|
94
112
|
// itself lives in ./tabletoolbar.chrome.ts because RecordsTable renders the
|
|
@@ -107,6 +125,7 @@ ${toolbarChromeHead("tabletoolbar")}
|
|
|
107
125
|
${toolbarChromeBody("tabletoolbar")}
|
|
108
126
|
${filterChipChrome}
|
|
109
127
|
${importExportChrome}
|
|
128
|
+
${sortedLabelChrome}
|
|
110
129
|
${rtlFlipCss}
|
|
111
130
|
`,
|
|
112
131
|
});
|
package/dist/esm/index.js
CHANGED
|
@@ -86,7 +86,6 @@ export * from "./product/objectsettings/index.js";
|
|
|
86
86
|
export * from "./product/onboarding/index.js";
|
|
87
87
|
export * from "./product/recordpage/index.js";
|
|
88
88
|
export * from "./product/recordpanel/index.js";
|
|
89
|
-
export * from "./product/recordstable/index.js";
|
|
90
89
|
export * from "./product/richtext/index.js";
|
|
91
90
|
export * from "./product/savedviews/index.js";
|
|
92
91
|
export * from "./product/settingssection/index.js";
|
|
@@ -94,7 +93,6 @@ export * from "./product/settingsshell/index.js";
|
|
|
94
93
|
export * from "./product/sidebar/index.js";
|
|
95
94
|
export * from "./product/statlist/index.js";
|
|
96
95
|
export * from "./product/tasklist/index.js";
|
|
97
|
-
export * from "./product/taskstable/index.js";
|
|
98
96
|
export * from "./product/themepicker/index.js";
|
|
99
97
|
export * from "./product/trialbanner/index.js";
|
|
100
98
|
export * from "./product/viewtoggle/index.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// deterministic pastel colour per tag value (a select option keeps its colour)
|
|
2
|
+
const TAG_TONES = [
|
|
3
|
+
"brand",
|
|
4
|
+
"success",
|
|
5
|
+
"warning",
|
|
6
|
+
"danger",
|
|
7
|
+
"purple",
|
|
8
|
+
"cyan",
|
|
9
|
+
"orange",
|
|
10
|
+
];
|
|
11
|
+
export function tagTone(t) {
|
|
12
|
+
let h = 0;
|
|
13
|
+
for (let i = 0; i < t.length; i++)
|
|
14
|
+
h = (h * 31 + t.charCodeAt(i)) >>> 0;
|
|
15
|
+
return TAG_TONES[h % TAG_TONES.length];
|
|
16
|
+
}
|
|
@@ -9,8 +9,23 @@ import { Spinner } from "../../components/spinner/index.js";
|
|
|
9
9
|
import { Switch } from "../../components/switch/index.js";
|
|
10
10
|
import { Tabs } from "../../components/tabs/index.js";
|
|
11
11
|
import { cx, isRtlElement, useCrispComponentDefaults } from "../../core/index.js";
|
|
12
|
-
import {
|
|
12
|
+
import { tagTone } from "./column.js";
|
|
13
|
+
import { editableInputType, useEditableField } from "./editable-field.js";
|
|
13
14
|
import { recordpageRecipe } from "./recordpage.recipe.js";
|
|
15
|
+
// crisp/#798: the details column's own drag clamp -- re-measured live on
|
|
16
|
+
// Attio's own record page (a real drag test, not a source read): min 619px,
|
|
17
|
+
// max 1170px, default (also the double-click-reset target) 734px. These
|
|
18
|
+
// replace an earlier, much narrower 300/560/320 that was never actually
|
|
19
|
+
// checked against Attio and turned out roughly half the real max. Confirmed
|
|
20
|
+
// live these are literal px, not a viewport ratio: at a narrower emulated
|
|
21
|
+
// width the already-dragged-to-max column shrank below 1170px, but its OWN
|
|
22
|
+
// `getComputedStyle(...).width` still read a plain px number the whole
|
|
23
|
+
// time (not a `%`) — that shrink is ordinary flex-shrink (both panels
|
|
24
|
+
// share `flex: 1 1 auto` and can't both keep their preferred width once the
|
|
25
|
+
// window is too narrow), not the clamp itself changing with viewport width.
|
|
26
|
+
const DETAILS_MIN_W = 619;
|
|
27
|
+
const DETAILS_MAX_W = 1170;
|
|
28
|
+
const DETAILS_DEFAULT_W = 734;
|
|
14
29
|
function valueEmpty(v) {
|
|
15
30
|
return v == null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
16
31
|
}
|
|
@@ -418,7 +433,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
418
433
|
const tasks = tasksProp ?? internalTasks;
|
|
419
434
|
const [showAllActivity, setShowAllActivity] = React.useState(false);
|
|
420
435
|
// Drag-resizable left details column.
|
|
421
|
-
const [detailsW, setDetailsW] = React.useState(
|
|
436
|
+
const [detailsW, setDetailsW] = React.useState(DETAILS_DEFAULT_W);
|
|
422
437
|
const [resizing, setResizing] = React.useState(false);
|
|
423
438
|
const dragRef = React.useRef(null);
|
|
424
439
|
const onResizeDown = (e) => {
|
|
@@ -436,7 +451,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
436
451
|
if (!dragRef.current)
|
|
437
452
|
return;
|
|
438
453
|
const next = dragRef.current.w + dragRef.current.dir * (e.clientX - dragRef.current.x);
|
|
439
|
-
setDetailsW(Math.max(
|
|
454
|
+
setDetailsW(Math.max(DETAILS_MIN_W, Math.min(DETAILS_MAX_W, next)));
|
|
440
455
|
};
|
|
441
456
|
const onResizeUp = (e) => {
|
|
442
457
|
dragRef.current = null;
|
|
@@ -621,7 +636,7 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
621
636
|
const effectivePanel = tabsProp
|
|
622
637
|
? (active) => tabsProp.find((tb) => tb.id === active)?.render()
|
|
623
638
|
: builtinPanel;
|
|
624
|
-
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: _jsx(Copy, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: _jsx(FileText, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: _jsx(Share2, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: _jsx(SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin":
|
|
639
|
+
return (_jsxs("div", { className: recordpageRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpage-topbar", children: [_jsxs("nav", { className: "crisp-recordpage-crumbs", "aria-label": t.breadcrumb, children: [_jsx("button", { type: "button", className: "crisp-recordpage-crumb", onClick: onBack, "aria-label": `Back to ${collection}`, children: collection }), _jsx("span", { className: "crisp-recordpage-crumb-sep", children: "/" }), _jsx("span", { className: "crisp-recordpage-crumb-name", children: name })] }), _jsxs("div", { className: "crisp-recordpage-actions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--ghost", children: [_jsx(Sparkles, { size: 14, strokeWidth: 1.75, color: "var(--crisp-bg-brand-solid)" }), t.askAttio] }), onToggleFull && (_jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", onClick: onToggleFull, "aria-label": full ? t.exitFullScreen : t.fullScreen, children: full ? (_jsx(Minimize2, { size: 15, strokeWidth: 1.75 })) : (_jsx(Maximize2, { size: 15, strokeWidth: 1.75 })) })), _jsxs(Menu, { align: "end", "aria-label": t.recordActions, trigger: _jsx("button", { type: "button", className: "crisp-recordpage-action crisp-recordpage-action--icon", "aria-label": t.recordActions, children: _jsx(MoreHorizontal, { size: 16, strokeWidth: 1.75 }) }), children: [_jsx(MenuItem, { icon: _jsx(Copy, { size: 15, strokeWidth: 1.75 }), shortcut: "\u2318C", children: t.copyRecordLink }), _jsx(MenuItem, { icon: _jsx(Plus, { size: 15, strokeWidth: 1.75 }), children: t.addToList }), _jsx(MenuItem, { icon: _jsx(Star, { size: 15, strokeWidth: 1.75 }), onSelect: () => setFavorite((f) => !f), children: favorite ? t.removeFromFavourites : t.addToFavourites }), _jsx(MenuSeparator, {}), _jsx(MenuItem, { icon: _jsx(Trash2, { size: 15, strokeWidth: 1.75 }), danger: true, shortcut: "\u232B", children: t.deleteRecord })] })] })] }), _jsxs("div", { className: "crisp-recordpage-body", children: [_jsxs("aside", { className: "crisp-recordpage-details", style: { width: detailsW }, children: [_jsxs("div", { className: "crisp-recordpage-entity", children: [entity?.avatar ? (_jsx("img", { className: "crisp-recordpage-entity-logo", src: entity.avatar, alt: "" })) : (_jsx(Avatar, { size: "lg", shape: "rounded", tone: "brand", children: name.charAt(0) })), _jsx("span", { className: "crisp-recordpage-entity-name", children: name }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "aria-label": t.editRecord, children: _jsx(Pencil, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-entity-iconbtn", "data-active": favorite ? "" : undefined, "aria-pressed": favorite, onClick: () => setFavorite((f) => !f), "aria-label": favorite ? t.removeFromFavourites : t.addToFavourites, children: _jsx(Star, { size: 15, strokeWidth: 1.75 }) })] }), _jsxs("div", { className: "crisp-recordpage-quickactions", children: [_jsxs("button", { type: "button", className: "crisp-recordpage-compose", children: [_jsx(Mail, { size: 16, strokeWidth: 1.75 }), t.composeEmail] }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.copyRecordLink, children: _jsx(Copy, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.logANote, children: _jsx(FileText, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.linkRecords, children: _jsx(Share2, { size: 15, strokeWidth: 1.75 }) }), _jsx("button", { type: "button", className: "crisp-recordpage-quickbtn", "aria-label": t.createATask, children: _jsx(SquareCheck, { size: 15, strokeWidth: 1.75 }) })] }), _jsx(RecordDetails, { record: record, columns: columns, sections: sections, onEdit: onEdit, labels: labels }), _jsxs("div", { className: "crisp-recordpage-section", children: [_jsx(SectionHead, { title: t.tabs.lists, action: _jsxs("button", { type: "button", className: "crisp-recordpage-addbtn", children: [_jsx(Plus, { size: 14, strokeWidth: 2 }), t.addToList] }) }), _jsx("div", { className: "crisp-recordpage-emptystate", children: t.noLists })] })] }), _jsx("div", { className: "crisp-recordpage-resizer", "data-active": resizing ? "" : undefined, role: "separator", "aria-orientation": "vertical", "aria-label": t.resizeDetails, "aria-valuenow": Math.round(detailsW), "aria-valuemin": DETAILS_MIN_W, "aria-valuemax": DETAILS_MAX_W, tabIndex: 0, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onKeyDown: (e) => {
|
|
625
640
|
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight")
|
|
626
641
|
return;
|
|
627
642
|
// The pointer drag learned its direction in #212; this did not.
|
|
@@ -630,7 +645,9 @@ export function RecordPage({ record, columns, collection = "Records", labels, on
|
|
|
630
645
|
// half-conversion the drag had, one handler away (#255).
|
|
631
646
|
const rtl = isRtlElement(e.currentTarget);
|
|
632
647
|
const grow = e.key === (rtl ? "ArrowLeft" : "ArrowRight");
|
|
633
|
-
setDetailsW((w) => grow
|
|
648
|
+
setDetailsW((w) => grow
|
|
649
|
+
? Math.min(DETAILS_MAX_W, w + 16)
|
|
650
|
+
: Math.max(DETAILS_MIN_W, w - 16));
|
|
634
651
|
} }), _jsx("main", { className: "crisp-recordpage-main", children: _jsx(Tabs, { tabs: effectiveTabs, defaultValue: tabsProp ? defaultTab : "overview", "aria-label": `${name} record`, children: effectivePanel }) })] })] }));
|
|
635
652
|
}
|
|
636
653
|
RecordPage.displayName = "RecordPage";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AtSign, Briefcase, Building2, Calendar, Globe, Hash, MapPin, Tag, TrendingUp, Users, } from "lucide-react";
|
|
2
|
+
// Moved here from product/recordstable/recordstable.tsx's `defaultColumns`/
|
|
3
|
+
// `defaultRows` (crisp#786): RecordsTable is deprecated and its whole
|
|
4
|
+
// directory is being deleted, but RecordPage's own tests (and the shared
|
|
5
|
+
// RTL-keyboard-convention tests that live alongside them) still need a
|
|
6
|
+
// real, 12-column-deep dataset to exercise "a field past the first 5 needs
|
|
7
|
+
// View all values" and similar shape-dependent assertions. Simplified from
|
|
8
|
+
// the original: no baked-in company-logo data URIs (nothing here asserts on
|
|
9
|
+
// avatars), a plain favicon URL instead.
|
|
10
|
+
export const testColumns = [
|
|
11
|
+
{ key: "company", label: "Company", type: "entity", icon: Building2 },
|
|
12
|
+
{ key: "domain", label: "Domain", type: "url", icon: Globe },
|
|
13
|
+
{
|
|
14
|
+
key: "categories",
|
|
15
|
+
label: "Categories",
|
|
16
|
+
type: "tags",
|
|
17
|
+
icon: Tag,
|
|
18
|
+
width: 240,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "employees",
|
|
22
|
+
label: "Employees",
|
|
23
|
+
type: "number",
|
|
24
|
+
icon: Users,
|
|
25
|
+
width: 120,
|
|
26
|
+
},
|
|
27
|
+
{ key: "location", label: "Location", type: "text", icon: MapPin },
|
|
28
|
+
{ key: "stage", label: "Stage", type: "tags", icon: TrendingUp, width: 150 },
|
|
29
|
+
{ key: "ceo", label: "CEO", type: "text", icon: Briefcase, width: 170 },
|
|
30
|
+
{ key: "twitter", label: "Twitter", type: "url", icon: AtSign, width: 150 },
|
|
31
|
+
{
|
|
32
|
+
key: "followers",
|
|
33
|
+
label: "Followers",
|
|
34
|
+
type: "number",
|
|
35
|
+
icon: Users,
|
|
36
|
+
width: 120,
|
|
37
|
+
},
|
|
38
|
+
{ key: "linkedin", label: "LinkedIn", type: "url", icon: Globe, width: 180 },
|
|
39
|
+
{
|
|
40
|
+
key: "founded",
|
|
41
|
+
label: "Founded",
|
|
42
|
+
type: "date",
|
|
43
|
+
icon: Calendar,
|
|
44
|
+
width: 110,
|
|
45
|
+
},
|
|
46
|
+
{ key: "arr", label: "Est. ARR", type: "number", icon: Hash, width: 130 },
|
|
47
|
+
];
|
|
48
|
+
// biome-ignore format: one row per line keeps the wide demo dataset readable
|
|
49
|
+
export const testRows = [
|
|
50
|
+
{ id: "google", company: { name: "Google", avatar: "https://www.google.com/s2/favicons?domain=google.com&sz=64" }, domain: "google.com", categories: ["Search", "Ads", "Cloud", "AI", "Android", "Maps", "Hardware", "Workspace", "Chrome"], employees: 182000, location: "Mountain View, CA", stage: ["Public"], ceo: "Sundar Pichai", twitter: "twitter.com/google", followers: 29800000, linkedin: "linkedin.com/company/google", founded: "1998", arr: 307000000000 },
|
|
51
|
+
{ id: "stripe", company: { name: "Stripe", avatar: "https://www.google.com/s2/favicons?domain=stripe.com&sz=64" }, domain: "stripe.com", categories: ["Payments", "Fintech"], employees: 8000, location: "San Francisco, CA", stage: ["Series I"], ceo: "Patrick Collison", twitter: "twitter.com/stripe", followers: 1200000, linkedin: "linkedin.com/company/stripe", founded: "2010", arr: 16000000000 },
|
|
52
|
+
];
|
|
@@ -23,24 +23,26 @@ const config = defineRecipe({
|
|
|
23
23
|
position: relative; display: inline-flex; font-family: ${c.font.sans};
|
|
24
24
|
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
25
25
|
|
|
26
|
-
/*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
/* crisp/#772 follow-up, re-measured live 2026-09-25 in the trigger's
|
|
27
|
+
actual position (Attio's real view bar, next to View settings --
|
|
28
|
+
fixed there in #766/#768/#778): white (bgSurface, rgb(255,255,255)
|
|
29
|
+
exact) with elevation.field's 3-layer shadow, the same raised
|
|
30
|
+
chrome View settings and Import/Export both carry
|
|
31
|
+
(tabletoolbar.recipe.ts's own --raised, not reused here directly
|
|
32
|
+
since this is a separate recipe/component, but the identical measured
|
|
33
|
+
values). Supersedes the original #555 measurement (rgb(246,247,247),
|
|
34
|
+
reused as bgRaised -- a flat grey fill with no shadow at all), which
|
|
35
|
+
was wrong: this trigger was never re-checked once it moved into its
|
|
36
|
+
real view-bar position. */
|
|
35
37
|
& .crisp-savedviews-trigger {
|
|
36
38
|
box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1.5"]};
|
|
37
39
|
height: ${c.size.row}; margin: 0; padding: ${c.space["1"]} ${c.space["1.5"]};
|
|
38
|
-
border: none; border-radius: ${c.radius.md}; background: ${c.
|
|
40
|
+
border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: ${e.field};
|
|
39
41
|
font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary};
|
|
40
42
|
cursor: pointer; white-space: nowrap; transition: background-color .12s ease;
|
|
41
43
|
}
|
|
42
44
|
& .crisp-savedviews-trigger:hover,
|
|
43
|
-
& .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, ${c.fgPrimary} 4%, ${c.
|
|
45
|
+
& .crisp-savedviews-trigger[data-open] { background: color-mix(in srgb, ${c.fgPrimary} 4%, ${c.bgSurface}); }
|
|
44
46
|
& .crisp-savedviews-trigger-icon { flex-shrink: 0; display: inline-flex; width: ${c.size.icon}; height: ${c.size.icon}; }
|
|
45
47
|
& .crisp-savedviews-trigger-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
46
48
|
& .crisp-savedviews-trigger-chevron { flex-shrink: 0; color: ${c.fgMuted}; }
|