@agent-native/core 0.123.2 → 0.124.1
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +28 -0
- package/corpus/core/package.json +1 -1
- package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +6 -3
- package/corpus/templates/analytics/server/lib/report-chart-svg.ts +18 -5
- package/corpus/templates/crm/actions/_crm-list-utils.ts +178 -0
- package/corpus/templates/crm/actions/add-crm-record-to-list.ts +23 -2
- package/corpus/templates/crm/actions/create-crm-list.ts +229 -43
- package/corpus/templates/crm/actions/create-crm-record.ts +13 -5
- package/corpus/templates/crm/actions/update-crm-list-entry.ts +1 -1
- package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +22 -2
- package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +143 -32
- package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +190 -75
- package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +267 -115
- package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +265 -131
- package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +79 -100
- package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +12 -8
- package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +27 -31
- package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +16 -13
- package/corpus/templates/crm/app/components/crm/record/attribute-row.tsx +108 -0
- package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +201 -9
- package/corpus/templates/crm/app/components/crm/record/panel-resize.ts +61 -0
- package/corpus/templates/crm/app/components/crm/record/record-data.ts +5 -4
- package/corpus/templates/crm/app/components/crm/shared/README-tokens.md +103 -0
- package/corpus/templates/crm/app/components/crm/shared/RecordReferencePicker.tsx +117 -0
- package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +84 -0
- package/corpus/templates/crm/app/components/crm/shared/ui-tokens.ts +89 -0
- package/corpus/templates/crm/app/global.css +170 -0
- package/corpus/templates/crm/app/i18n/en-US.ts +1 -0
- package/corpus/templates/crm/changelog/2026-07-26-a-half-typed-number-or-date-no-longer-saves-as-empty-the-fie.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-a-new-pipeline-board-now-opens-with-stage-columns-a-summabl.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-record-reference-fields-like-account-can-now-be-linked-from-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-retyping-a-record-field-now-replaces-the-old-value-instead-o.md +6 -0
- package/corpus/templates/design/server/handlers/index-design-system-with-builder.ts +38 -21
- package/corpus/templates/slides/server/handlers/index-design-system-with-builder.ts +23 -20
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +3 -3
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +1 -1
|
@@ -53,8 +53,22 @@ import {
|
|
|
53
53
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
54
54
|
import { cn } from "@/lib/utils";
|
|
55
55
|
|
|
56
|
+
import {
|
|
57
|
+
HEADER_HEIGHT,
|
|
58
|
+
overlayProps,
|
|
59
|
+
ROW_HEIGHT,
|
|
60
|
+
SELECTION_RING_INSET,
|
|
61
|
+
selectionCornerRadius,
|
|
62
|
+
} from "../shared/ui-tokens";
|
|
56
63
|
import { decodeTsv, encodeTsv } from "./clipboard";
|
|
57
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
AttributeTypeIcon,
|
|
66
|
+
CellDisplay,
|
|
67
|
+
CellEditor,
|
|
68
|
+
ProvenanceMarker,
|
|
69
|
+
RecordAvatar,
|
|
70
|
+
type CrmAvatarShape,
|
|
71
|
+
} from "./GridCell";
|
|
58
72
|
import {
|
|
59
73
|
cellSpecFor,
|
|
60
74
|
copyCell,
|
|
@@ -86,13 +100,27 @@ import {
|
|
|
86
100
|
type CrmGridSortEntry,
|
|
87
101
|
} from "./query";
|
|
88
102
|
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const DEFAULT_WIDTH = 200;
|
|
103
|
+
const ICON_WIDTH = 36;
|
|
104
|
+
const NAME_WIDTH = 333;
|
|
105
|
+
const DEFAULT_WIDTH = 180;
|
|
93
106
|
/** Rows from the bottom of the loaded set at which the next page is requested. */
|
|
94
107
|
const PREFETCH_ROWS = 8;
|
|
95
108
|
|
|
109
|
+
/**
|
|
110
|
+
* The frozen first columns. `clip-path` lets the blur spill to the right and
|
|
111
|
+
* nowhere else — unclipped it bleeds over the header and the row below, which
|
|
112
|
+
* reads as a smudge rather than a lifted edge.
|
|
113
|
+
*/
|
|
114
|
+
const STICKY_FLAT: React.CSSProperties = {
|
|
115
|
+
clipPath: "inset(0 -38px 0 0)",
|
|
116
|
+
boxShadow: "none",
|
|
117
|
+
transition: "box-shadow var(--motion-breezy) ease-in-out",
|
|
118
|
+
};
|
|
119
|
+
const STICKY_SCROLLED: React.CSSProperties = {
|
|
120
|
+
...STICKY_FLAT,
|
|
121
|
+
boxShadow: "var(--crm-shadow-sticky)",
|
|
122
|
+
};
|
|
123
|
+
|
|
96
124
|
export interface CrmGridCommit {
|
|
97
125
|
row: CrmGridRow;
|
|
98
126
|
attribute: CrmGridAttribute;
|
|
@@ -116,6 +144,10 @@ export interface CrmGridProps {
|
|
|
116
144
|
emptyDescription?: string;
|
|
117
145
|
/** Header for the fixed record-name column. */
|
|
118
146
|
nameLabel: string;
|
|
147
|
+
/** The object's own glyph, shown in the icon column until the row is hovered. */
|
|
148
|
+
objectIcon?: React.ComponentType<{ className?: string }>;
|
|
149
|
+
/** Avatar shape for this object type: round is a person, squircle is not. */
|
|
150
|
+
avatarShape?: CrmAvatarShape;
|
|
119
151
|
/** Href for one row's record page; the name column links to it when given. */
|
|
120
152
|
rowHref?: (row: CrmGridRow) => string;
|
|
121
153
|
onCommitCell: (commit: CrmGridCommit) => Promise<void>;
|
|
@@ -144,6 +176,9 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
144
176
|
} | null>(null);
|
|
145
177
|
const [selectedRows, setSelectedRows] = useState<Set<string>>(new Set());
|
|
146
178
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
179
|
+
// Drives the frozen-column shadow. It exists only once there is something
|
|
180
|
+
// hidden underneath the frozen columns.
|
|
181
|
+
const [scrolled, setScrolled] = useState(false);
|
|
147
182
|
|
|
148
183
|
const ordered = useMemo(
|
|
149
184
|
() => resolveGridColumns(props.columns, props.attributes),
|
|
@@ -426,15 +461,18 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
426
461
|
}, [hasNextPage, isFetchingNextPage, lastVisible, onLoadMore, rowCount]);
|
|
427
462
|
|
|
428
463
|
const totalWidth =
|
|
429
|
-
|
|
464
|
+
ICON_WIDTH +
|
|
430
465
|
NAME_WIDTH +
|
|
431
466
|
visible.reduce(
|
|
432
467
|
(sum, { column, attribute }) =>
|
|
433
468
|
sum +
|
|
434
469
|
(column.width ?? cellSpecFor(attribute).defaultWidth ?? DEFAULT_WIDTH),
|
|
435
470
|
0,
|
|
436
|
-
)
|
|
437
|
-
|
|
471
|
+
);
|
|
472
|
+
const stickyStyle = scrolled ? STICKY_SCROLLED : STICKY_FLAT;
|
|
473
|
+
const frozenWidth = ICON_WIDTH + NAME_WIDTH;
|
|
474
|
+
const ObjectIcon = props.objectIcon;
|
|
475
|
+
const avatarShape = props.avatarShape ?? "company";
|
|
438
476
|
|
|
439
477
|
function toggleRow(rowId: string) {
|
|
440
478
|
setSelectedRows((current) => {
|
|
@@ -468,9 +506,13 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
468
506
|
|
|
469
507
|
if (props.isLoading && props.rows.length === 0) {
|
|
470
508
|
return (
|
|
471
|
-
<div className="grid gap-
|
|
472
|
-
{Array.from({ length:
|
|
473
|
-
<Skeleton
|
|
509
|
+
<div className="grid gap-px">
|
|
510
|
+
{Array.from({ length: 12 }, (_, index) => (
|
|
511
|
+
<Skeleton
|
|
512
|
+
key={index}
|
|
513
|
+
className="w-full rounded-none"
|
|
514
|
+
style={{ height: ROW_HEIGHT - 1 }}
|
|
515
|
+
/>
|
|
474
516
|
))}
|
|
475
517
|
</div>
|
|
476
518
|
);
|
|
@@ -505,34 +547,56 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
505
547
|
event.preventDefault();
|
|
506
548
|
void pasteTsv(text);
|
|
507
549
|
}}
|
|
550
|
+
onScroll={(event) => {
|
|
551
|
+
const next = event.currentTarget.scrollLeft > 0;
|
|
552
|
+
setScrolled((current) => (current === next ? current : next));
|
|
553
|
+
}}
|
|
508
554
|
className="min-h-0 flex-1 overflow-auto outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
|
509
555
|
>
|
|
510
556
|
<div style={{ width: totalWidth, minWidth: "100%" }}>
|
|
511
|
-
<div
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
>
|
|
516
|
-
<Checkbox
|
|
517
|
-
checked={
|
|
518
|
-
selectedRows.size > 0 &&
|
|
519
|
-
selectedRows.size === props.rows.length
|
|
520
|
-
}
|
|
521
|
-
onCheckedChange={(next) =>
|
|
522
|
-
setSelectedRows(
|
|
523
|
-
next === true
|
|
524
|
-
? new Set(props.rows.map((row) => row.id))
|
|
525
|
-
: new Set(),
|
|
526
|
-
)
|
|
527
|
-
}
|
|
528
|
-
aria-label={t("grid.selectAllRows")}
|
|
529
|
-
/>
|
|
530
|
-
</div>
|
|
557
|
+
<div
|
|
558
|
+
className="sticky top-0 z-20 flex items-stretch border-y border-hairline bg-background"
|
|
559
|
+
style={{ height: HEADER_HEIGHT }}
|
|
560
|
+
>
|
|
531
561
|
<div
|
|
532
|
-
className="
|
|
533
|
-
style={{ width:
|
|
562
|
+
className="sticky left-0 z-20 flex shrink-0 items-stretch bg-background"
|
|
563
|
+
style={{ ...stickyStyle, width: frozenWidth }}
|
|
534
564
|
>
|
|
535
|
-
<
|
|
565
|
+
<div
|
|
566
|
+
className="flex shrink-0 items-center justify-center"
|
|
567
|
+
style={{ width: ICON_WIDTH }}
|
|
568
|
+
>
|
|
569
|
+
<Checkbox
|
|
570
|
+
checked={
|
|
571
|
+
selectedRows.size > 0 &&
|
|
572
|
+
selectedRows.size === props.rows.length
|
|
573
|
+
}
|
|
574
|
+
onCheckedChange={(next) =>
|
|
575
|
+
setSelectedRows(
|
|
576
|
+
next === true
|
|
577
|
+
? new Set(props.rows.map((row) => row.id))
|
|
578
|
+
: new Set(),
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
aria-label={t("grid.selectAllRows")}
|
|
582
|
+
/>
|
|
583
|
+
</div>
|
|
584
|
+
<div
|
|
585
|
+
className="flex shrink-0 items-center gap-1.5 border-l border-hairline px-3 text-sm font-medium text-content-secondary"
|
|
586
|
+
style={{ width: NAME_WIDTH }}
|
|
587
|
+
>
|
|
588
|
+
<span className="min-w-0 truncate">{props.nameLabel}</span>
|
|
589
|
+
{/* The add-column affordance rides the primary column, not the
|
|
590
|
+
far right of a header row that can be scrolled off-screen. */}
|
|
591
|
+
<ColumnPicker
|
|
592
|
+
attributes={props.attributes}
|
|
593
|
+
columns={ordered}
|
|
594
|
+
onColumnsChange={props.onColumnsChange}
|
|
595
|
+
{...(props.onAddAttribute
|
|
596
|
+
? { onAddAttribute: props.onAddAttribute }
|
|
597
|
+
: {})}
|
|
598
|
+
/>
|
|
599
|
+
</div>
|
|
536
600
|
</div>
|
|
537
601
|
{table.getHeaderGroups()[0]?.headers.map((header, index) => {
|
|
538
602
|
const attribute = visible[index]?.attribute;
|
|
@@ -558,23 +622,27 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
558
622
|
}
|
|
559
623
|
}}
|
|
560
624
|
style={{ width: header.getSize() }}
|
|
561
|
-
className="group/header relative flex shrink-0 items-center gap-1 border-
|
|
625
|
+
className="group/header relative flex shrink-0 items-center gap-1 border-l border-hairline px-3 text-sm font-medium text-content-secondary"
|
|
562
626
|
>
|
|
563
627
|
<button
|
|
564
628
|
type="button"
|
|
565
629
|
onClick={() => toggleSort(attribute)}
|
|
566
|
-
className="flex min-w-0 flex-1 cursor-pointer items-center gap-1 text-left hover:text-foreground"
|
|
630
|
+
className="flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 text-left transition-colors hover:text-foreground"
|
|
567
631
|
>
|
|
568
|
-
<
|
|
632
|
+
<AttributeTypeIcon
|
|
633
|
+
type={attribute.attributeType}
|
|
634
|
+
className="size-4 shrink-0 text-content-ghost"
|
|
635
|
+
/>
|
|
636
|
+
<span className="min-w-0 truncate">
|
|
569
637
|
{flexRender(
|
|
570
638
|
header.column.columnDef.header,
|
|
571
639
|
header.getContext(),
|
|
572
640
|
)}
|
|
573
641
|
</span>
|
|
574
642
|
{sorted?.direction === "asc" ? (
|
|
575
|
-
<IconArrowUp className="size-3 shrink-0" />
|
|
643
|
+
<IconArrowUp className="size-3.5 shrink-0" />
|
|
576
644
|
) : sorted?.direction === "desc" ? (
|
|
577
|
-
<IconArrowDown className="size-3 shrink-0" />
|
|
645
|
+
<IconArrowDown className="size-3.5 shrink-0" />
|
|
578
646
|
) : null}
|
|
579
647
|
</button>
|
|
580
648
|
<DropdownMenu>
|
|
@@ -608,22 +676,21 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
608
676
|
<div
|
|
609
677
|
onMouseDown={header.getResizeHandler()}
|
|
610
678
|
onTouchStart={header.getResizeHandler()}
|
|
611
|
-
className=
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
679
|
+
className="absolute -right-[3px] top-0 z-10 flex h-full w-1.5 cursor-col-resize touch-none select-none justify-center"
|
|
680
|
+
>
|
|
681
|
+
{/* The 100ms delay before the handle appears is deliberate:
|
|
682
|
+
without it, dragging a pointer across the header row
|
|
683
|
+
strobes a handle at every column boundary. */}
|
|
684
|
+
<span
|
|
685
|
+
className={cn(
|
|
686
|
+
"h-full w-0.5 rounded-[3px] bg-[hsl(var(--crm-accent))] opacity-0 transition-opacity duration-100 delay-100 group-hover/header:opacity-100",
|
|
687
|
+
header.column.getIsResizing() && "opacity-100",
|
|
688
|
+
)}
|
|
689
|
+
/>
|
|
690
|
+
</div>
|
|
616
691
|
</div>
|
|
617
692
|
);
|
|
618
693
|
})}
|
|
619
|
-
<ColumnPicker
|
|
620
|
-
attributes={props.attributes}
|
|
621
|
-
columns={ordered}
|
|
622
|
-
onColumnsChange={props.onColumnsChange}
|
|
623
|
-
{...(props.onAddAttribute
|
|
624
|
-
? { onAddAttribute: props.onAddAttribute }
|
|
625
|
-
: {})}
|
|
626
|
-
/>
|
|
627
694
|
</div>
|
|
628
695
|
|
|
629
696
|
<div
|
|
@@ -633,45 +700,85 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
633
700
|
const row = props.rows[virtualRow.index];
|
|
634
701
|
if (!row) return null;
|
|
635
702
|
const range = selection ? selectionRange(selection) : null;
|
|
703
|
+
const rowSelected = selectedRows.has(row.id);
|
|
636
704
|
return (
|
|
637
705
|
<div
|
|
638
706
|
key={row.id}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
707
|
+
{...overlayProps({
|
|
708
|
+
selected: rowSelected,
|
|
709
|
+
className:
|
|
710
|
+
"group/row absolute left-0 flex items-stretch border-b border-hairline",
|
|
711
|
+
})}
|
|
643
712
|
style={{
|
|
644
713
|
top: virtualRow.start,
|
|
645
714
|
height: ROW_HEIGHT,
|
|
646
715
|
width: totalWidth,
|
|
716
|
+
minWidth: "100%",
|
|
647
717
|
}}
|
|
648
718
|
>
|
|
719
|
+
{/* The frozen columns paint their own background, so the row's
|
|
720
|
+
overlay cannot reach them — they carry a second one driven
|
|
721
|
+
by the row's hover instead of a background swap. */}
|
|
649
722
|
<div
|
|
650
|
-
|
|
651
|
-
|
|
723
|
+
{...overlayProps({
|
|
724
|
+
selected: rowSelected,
|
|
725
|
+
className: cn(
|
|
726
|
+
"sticky left-0 z-20 flex shrink-0 items-stretch bg-background",
|
|
727
|
+
// Selection already owns the overlay at a stronger
|
|
728
|
+
// alpha; letting hover win would lighten it.
|
|
729
|
+
!rowSelected &&
|
|
730
|
+
"group-hover/row:before:opacity-[var(--crm-overlay-hover)]",
|
|
731
|
+
),
|
|
732
|
+
})}
|
|
733
|
+
style={{ ...stickyStyle, width: frozenWidth }}
|
|
652
734
|
>
|
|
653
|
-
<
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
735
|
+
<div
|
|
736
|
+
className="relative flex shrink-0 items-center justify-center"
|
|
737
|
+
style={{ width: ICON_WIDTH }}
|
|
738
|
+
>
|
|
739
|
+
{rowSelected || !ObjectIcon ? (
|
|
740
|
+
<Checkbox
|
|
741
|
+
checked={rowSelected}
|
|
742
|
+
onCheckedChange={() => toggleRow(row.id)}
|
|
743
|
+
aria-label={t("grid.selectRow", {
|
|
744
|
+
name: row.displayName,
|
|
745
|
+
})}
|
|
746
|
+
/>
|
|
747
|
+
) : (
|
|
748
|
+
<>
|
|
749
|
+
<ObjectIcon className="size-4 text-content-tertiary transition-opacity group-hover/row:opacity-0" />
|
|
750
|
+
<Checkbox
|
|
751
|
+
checked={false}
|
|
752
|
+
onCheckedChange={() => toggleRow(row.id)}
|
|
753
|
+
aria-label={t("grid.selectRow", {
|
|
754
|
+
name: row.displayName,
|
|
755
|
+
})}
|
|
756
|
+
className="absolute inset-0 m-auto opacity-0 transition-opacity focus-visible:opacity-100 group-hover/row:opacity-100"
|
|
757
|
+
/>
|
|
758
|
+
</>
|
|
759
|
+
)}
|
|
760
|
+
</div>
|
|
761
|
+
<div
|
|
762
|
+
className="flex shrink-0 items-center gap-2 overflow-hidden border-l border-hairline px-3 text-sm font-medium"
|
|
763
|
+
style={{ width: NAME_WIDTH }}
|
|
764
|
+
>
|
|
765
|
+
<RecordAvatar
|
|
766
|
+
name={row.displayName}
|
|
767
|
+
shape={avatarShape}
|
|
768
|
+
/>
|
|
769
|
+
{props.rowHref ? (
|
|
770
|
+
<Link
|
|
771
|
+
to={props.rowHref(row)}
|
|
772
|
+
className="min-w-0 truncate rounded-sm underline decoration-transparent underline-offset-[0.14em] outline-none transition-colors hover:decoration-content-ghost focus-visible:ring-1 focus-visible:ring-ring"
|
|
773
|
+
>
|
|
774
|
+
{row.displayName}
|
|
775
|
+
</Link>
|
|
776
|
+
) : (
|
|
777
|
+
<span className="min-w-0 truncate">
|
|
778
|
+
{row.displayName}
|
|
779
|
+
</span>
|
|
780
|
+
)}
|
|
781
|
+
</div>
|
|
675
782
|
</div>
|
|
676
783
|
{visible.map(({ column, attribute }, colIndex) => {
|
|
677
784
|
const ref = { row: virtualRow.index, col: colIndex };
|
|
@@ -682,6 +789,18 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
682
789
|
const isEditing =
|
|
683
790
|
editing?.ref.row === ref.row &&
|
|
684
791
|
editing.ref.col === ref.col;
|
|
792
|
+
const spec = cellSpecFor(attribute);
|
|
793
|
+
// A range is outlined once, not per cell: each cell draws
|
|
794
|
+
// only the segments that sit on the range's boundary, and a
|
|
795
|
+
// corner rounds only where two of them meet.
|
|
796
|
+
const edges = range
|
|
797
|
+
? {
|
|
798
|
+
top: ref.row === range.top,
|
|
799
|
+
right: ref.col === range.right,
|
|
800
|
+
bottom: ref.row === range.bottom,
|
|
801
|
+
left: ref.col === range.left,
|
|
802
|
+
}
|
|
803
|
+
: null;
|
|
685
804
|
return (
|
|
686
805
|
<div
|
|
687
806
|
key={attribute.apiSlug}
|
|
@@ -704,21 +823,37 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
704
823
|
}}
|
|
705
824
|
style={{
|
|
706
825
|
width:
|
|
707
|
-
column.width ??
|
|
708
|
-
cellSpecFor(attribute).defaultWidth ??
|
|
709
|
-
DEFAULT_WIDTH,
|
|
826
|
+
column.width ?? spec.defaultWidth ?? DEFAULT_WIDTH,
|
|
710
827
|
}}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
828
|
+
{...overlayProps({
|
|
829
|
+
selected: inRange && !active,
|
|
830
|
+
soft: true,
|
|
831
|
+
className: cn(
|
|
832
|
+
"relative flex shrink-0 items-start border-l border-hairline px-3 pt-2 text-sm",
|
|
833
|
+
!isCellEditable(attribute) &&
|
|
834
|
+
"text-content-tertiary",
|
|
835
|
+
// The ring overhangs the shared divider by 1px, so
|
|
836
|
+
// the cell has to paint above its neighbours or the
|
|
837
|
+
// next cell's border repaints over it.
|
|
838
|
+
inRange && "z-[1]",
|
|
839
|
+
isEditing && "z-[2] bg-background p-0",
|
|
840
|
+
),
|
|
841
|
+
})}
|
|
721
842
|
>
|
|
843
|
+
{inRange && edges ? (
|
|
844
|
+
<span
|
|
845
|
+
aria-hidden
|
|
846
|
+
className="pointer-events-none absolute z-[2] border-[hsl(var(--crm-accent))]"
|
|
847
|
+
style={{
|
|
848
|
+
inset: SELECTION_RING_INSET,
|
|
849
|
+
borderTopWidth: edges.top ? 1 : 0,
|
|
850
|
+
borderRightWidth: edges.right ? 1 : 0,
|
|
851
|
+
borderBottomWidth: edges.bottom ? 1 : 0,
|
|
852
|
+
borderLeftWidth: edges.left ? 1 : 0,
|
|
853
|
+
borderRadius: selectionCornerRadius(edges),
|
|
854
|
+
}}
|
|
855
|
+
/>
|
|
856
|
+
) : null}
|
|
722
857
|
{isEditing ? (
|
|
723
858
|
<CellEditor
|
|
724
859
|
attribute={attribute}
|
|
@@ -737,24 +872,38 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
737
872
|
/>
|
|
738
873
|
) : (
|
|
739
874
|
<>
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
875
|
+
{/* One 20px line box, so a chip, a checkbox and a
|
|
876
|
+
line of text all sit on the same baseline under
|
|
877
|
+
the cell's 8px top padding. */}
|
|
878
|
+
<span
|
|
879
|
+
className={cn(
|
|
880
|
+
"flex min-h-5 w-full min-w-0 items-center overflow-hidden",
|
|
881
|
+
spec.align === "right" &&
|
|
882
|
+
"justify-end tabular-nums",
|
|
883
|
+
spec.align === "center" && "justify-center",
|
|
884
|
+
)}
|
|
885
|
+
>
|
|
886
|
+
<CellDisplay
|
|
887
|
+
attribute={attribute}
|
|
888
|
+
// A displayName that only duplicates this row's
|
|
889
|
+
// name is hidden here, not dropped from the row:
|
|
890
|
+
// an absent name must still show its own value.
|
|
891
|
+
value={
|
|
892
|
+
isSuppressedDisplayNameCell(
|
|
893
|
+
attribute.apiSlug,
|
|
894
|
+
row.values,
|
|
895
|
+
)
|
|
896
|
+
? null
|
|
897
|
+
: (row.values[attribute.apiSlug] ?? null)
|
|
898
|
+
}
|
|
899
|
+
{...(row.valuesSince?.[attribute.apiSlug]
|
|
900
|
+
? {
|
|
901
|
+
since: row.valuesSince[attribute.apiSlug],
|
|
902
|
+
}
|
|
903
|
+
: {})}
|
|
904
|
+
{...(props.now ? { now: props.now } : {})}
|
|
905
|
+
/>
|
|
906
|
+
</span>
|
|
758
907
|
<ProvenanceMarker
|
|
759
908
|
provenance={row.provenance?.[attribute.apiSlug]}
|
|
760
909
|
attributeLabel={attribute.label}
|
|
@@ -769,7 +918,10 @@ export function CrmGrid(props: CrmGridProps) {
|
|
|
769
918
|
})}
|
|
770
919
|
</div>
|
|
771
920
|
{props.isFetchingNextPage ? (
|
|
772
|
-
<div
|
|
921
|
+
<div
|
|
922
|
+
className="sticky left-0 flex items-center px-3 text-sm text-content-tertiary"
|
|
923
|
+
style={{ height: ROW_HEIGHT }}
|
|
924
|
+
>
|
|
773
925
|
{t("grid.loadingMore")}
|
|
774
926
|
</div>
|
|
775
927
|
) : null}
|
|
@@ -894,7 +1046,7 @@ function ColumnPicker({
|
|
|
894
1046
|
<button
|
|
895
1047
|
type="button"
|
|
896
1048
|
aria-label={t("grid.configureColumns")}
|
|
897
|
-
className="
|
|
1049
|
+
className="ml-auto grid size-5 shrink-0 cursor-pointer place-items-center rounded-badge text-content-ghost transition-colors hover:bg-foreground/[0.06] hover:text-foreground"
|
|
898
1050
|
>
|
|
899
1051
|
<IconPlus className="size-4" />
|
|
900
1052
|
</button>
|
|
@@ -956,11 +1108,11 @@ function BulkBar({
|
|
|
956
1108
|
);
|
|
957
1109
|
return (
|
|
958
1110
|
<div className="pointer-events-none absolute inset-x-0 bottom-4 flex justify-center">
|
|
959
|
-
<div className="pointer-events-auto flex items-center gap-2 rounded-full
|
|
1111
|
+
<div className="pointer-events-auto flex items-center gap-2 rounded-full bg-background/95 py-1.5 pl-4 pr-2 shadow-e2 backdrop-blur">
|
|
960
1112
|
<span className="text-sm font-medium">
|
|
961
1113
|
{t("grid.selectedCount", { count })}
|
|
962
1114
|
</span>
|
|
963
|
-
<span className="h-4 w-px bg-
|
|
1115
|
+
<span className="h-4 w-px bg-hairline" />
|
|
964
1116
|
<Popover>
|
|
965
1117
|
<PopoverTrigger asChild>
|
|
966
1118
|
<Button variant="ghost" size="sm" className="gap-1.5">
|