@agent-native/core 0.124.0 → 0.124.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +25 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/assets/branding/favicon-base64.ts +5 -0
  5. package/corpus/core/src/cli/templates-meta.ts +1 -0
  6. package/corpus/core/src/server/email.ts +2 -5
  7. package/corpus/templates/analytics/server/lib/dashboard-report-render.ts +6 -3
  8. package/corpus/templates/analytics/server/lib/report-chart-svg.ts +18 -5
  9. package/corpus/templates/crm/actions/_crm-list-utils.ts +178 -0
  10. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +23 -2
  11. package/corpus/templates/crm/actions/create-crm-list.ts +229 -43
  12. package/corpus/templates/crm/actions/create-crm-record.ts +13 -5
  13. package/corpus/templates/crm/actions/update-crm-list-entry.ts +1 -1
  14. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +22 -2
  15. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +143 -32
  16. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +190 -75
  17. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +267 -115
  18. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +265 -131
  19. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +79 -100
  20. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +12 -8
  21. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +27 -31
  22. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +16 -13
  23. package/corpus/templates/crm/app/components/crm/record/attribute-row.tsx +108 -0
  24. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +201 -9
  25. package/corpus/templates/crm/app/components/crm/record/panel-resize.ts +61 -0
  26. package/corpus/templates/crm/app/components/crm/record/record-data.ts +5 -4
  27. package/corpus/templates/crm/app/components/crm/shared/README-tokens.md +103 -0
  28. package/corpus/templates/crm/app/components/crm/shared/RecordReferencePicker.tsx +117 -0
  29. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +84 -0
  30. package/corpus/templates/crm/app/components/crm/shared/ui-tokens.ts +89 -0
  31. package/corpus/templates/crm/app/global.css +170 -0
  32. package/corpus/templates/crm/app/i18n/en-US.ts +1 -0
  33. package/corpus/templates/crm/changelog/2026-07-26-a-half-typed-number-or-date-no-longer-saves-as-empty-the-fie.md +6 -0
  34. package/corpus/templates/crm/changelog/2026-07-26-a-new-pipeline-board-now-opens-with-stage-columns-a-summabl.md +6 -0
  35. package/corpus/templates/crm/changelog/2026-07-26-record-reference-fields-like-account-can-now-be-linked-from-.md +6 -0
  36. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-record-field-now-replaces-the-old-value-instead-o.md +6 -0
  37. package/dist/assets/branding/favicon-base64.d.ts +2 -0
  38. package/dist/assets/branding/favicon-base64.d.ts.map +1 -0
  39. package/dist/assets/branding/favicon-base64.js +5 -0
  40. package/dist/assets/branding/favicon-base64.js.map +1 -0
  41. package/dist/cli/templates-meta.d.ts.map +1 -1
  42. package/dist/cli/templates-meta.js +1 -0
  43. package/dist/cli/templates-meta.js.map +1 -1
  44. package/dist/collab/routes.d.ts +1 -1
  45. package/dist/collab/struct-routes.d.ts +1 -1
  46. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  47. package/dist/notifications/routes.d.ts +3 -3
  48. package/dist/secrets/routes.d.ts +3 -3
  49. package/dist/server/email.d.ts.map +1 -1
  50. package/dist/server/email.js +2 -2
  51. package/dist/server/email.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/assets/branding/favicon-base64.ts +5 -0
  54. package/src/cli/templates-meta.ts +1 -0
  55. package/src/server/email.ts +2 -5
@@ -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 { CellDisplay, CellEditor, ProvenanceMarker } from "./GridCell";
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 ROW_HEIGHT = 34;
90
- const SELECT_WIDTH = 36;
91
- const NAME_WIDTH = 240;
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
- SELECT_WIDTH +
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
- 40;
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-1.5 p-5">
472
- {Array.from({ length: 10 }, (_, index) => (
473
- <Skeleton key={index} className="h-8 w-full" />
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 className="sticky top-0 z-20 flex h-9 items-stretch border-b border-border bg-background/95 backdrop-blur">
512
- <div
513
- className="flex shrink-0 items-center justify-center border-r border-border/60"
514
- style={{ width: SELECT_WIDTH }}
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="flex shrink-0 items-center border-r border-border/60 px-2 text-xs font-medium text-muted-foreground"
533
- style={{ width: NAME_WIDTH }}
562
+ className="sticky left-0 z-20 flex shrink-0 items-stretch bg-background"
563
+ style={{ ...stickyStyle, width: frozenWidth }}
534
564
  >
535
- <span className="truncate">{props.nameLabel}</span>
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-r border-border/60 px-2 text-xs font-medium text-muted-foreground"
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
- <span className="truncate">
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={cn(
612
- "absolute right-0 top-0 h-full w-1 cursor-col-resize touch-none select-none hover:bg-ring",
613
- header.column.getIsResizing() && "bg-ring",
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
- className={cn(
640
- "absolute left-0 flex items-stretch border-b border-border/50 hover:bg-muted/25",
641
- selectedRows.has(row.id) && "bg-primary/5",
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
- className="flex shrink-0 items-center justify-center border-r border-border/50"
651
- style={{ width: SELECT_WIDTH }}
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
- <Checkbox
654
- checked={selectedRows.has(row.id)}
655
- onCheckedChange={() => toggleRow(row.id)}
656
- aria-label={t("grid.selectRow", {
657
- name: row.displayName,
658
- })}
659
- />
660
- </div>
661
- <div
662
- className="flex shrink-0 items-center overflow-hidden border-r border-border/50 px-2 text-sm font-medium"
663
- style={{ width: NAME_WIDTH }}
664
- >
665
- {props.rowHref ? (
666
- <Link
667
- to={props.rowHref(row)}
668
- className="truncate rounded-sm outline-none hover:underline focus-visible:ring-1 focus-visible:ring-ring"
669
- >
670
- {row.displayName}
671
- </Link>
672
- ) : (
673
- <span className="truncate">{row.displayName}</span>
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
- className={cn(
712
- "relative flex shrink-0 items-center overflow-hidden border-r border-border/50 px-2 text-sm",
713
- cellSpecFor(attribute).align === "right" &&
714
- "justify-end",
715
- cellSpecFor(attribute).align === "center" &&
716
- "justify-center",
717
- inRange && !active && "bg-primary/10",
718
- active && "ring-1 ring-inset ring-primary",
719
- isEditing && "z-10 bg-background p-0",
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
- <CellDisplay
741
- attribute={attribute}
742
- // A displayName that only duplicates this row's
743
- // name is hidden here, not dropped from the row:
744
- // an absent name must still show its own value.
745
- value={
746
- isSuppressedDisplayNameCell(
747
- attribute.apiSlug,
748
- row.values,
749
- )
750
- ? null
751
- : (row.values[attribute.apiSlug] ?? null)
752
- }
753
- {...(row.valuesSince?.[attribute.apiSlug]
754
- ? { since: row.valuesSince[attribute.apiSlug] }
755
- : {})}
756
- {...(props.now ? { now: props.now } : {})}
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 className="flex h-10 items-center px-3 text-xs text-muted-foreground">
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="flex w-10 shrink-0 cursor-pointer items-center justify-center text-muted-foreground hover:text-foreground"
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 border border-border bg-background/95 py-1.5 pl-4 pr-2 shadow-lg backdrop-blur">
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-border" />
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">