@proteos/ui 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -159,8 +159,25 @@ export type DataTableProps<TRow> = {
159
159
  * Default false — read-only tables stay borderless in the body.
160
160
  */
161
161
  columnBorders?: boolean
162
+ /**
163
+ * Group consecutive rows under a full-width heading. Returns the group a row
164
+ * belongs to, or null to leave it ungrouped; a heading is emitted whenever
165
+ * the group id CHANGES between neighbours, so the caller is responsible for
166
+ * ordering rows by their group (with server-side sorting, sort by the
167
+ * grouping column first). That contract is what lets grouping work with
168
+ * infinite scroll — a group may continue on the next page without the table
169
+ * having to know the full row set up front.
170
+ */
171
+ groupBy?: (row: TRow) => { id: string; label: ReactNode } | null
172
+ /** Height of a group heading row. Defaults to `rowHeight`. */
173
+ groupHeaderHeight?: number
162
174
  }
163
175
 
176
+ /** One entry of the virtualized list: either a data row or a group heading. */
177
+ type ListItem<TRow> =
178
+ | { kind: 'row'; row: Row<TRow>; dataIndex: number }
179
+ | { kind: 'group'; id: string; label: ReactNode }
180
+
164
181
  const DEFAULT_ROW_HEIGHT = 32
165
182
 
166
183
  /** Column id of the leading checkbox column added by `selectionMode`. */
@@ -194,6 +211,8 @@ export function DataTable<TRow>({
194
211
  selectedIds,
195
212
  onSelectionChange,
196
213
  columnBorders = false,
214
+ groupBy,
215
+ groupHeaderHeight,
197
216
  }: DataTableProps<TRow>) {
198
217
  const rows = useMemo<TRow[]>(() => {
199
218
  const pages = query.data?.pages
@@ -327,6 +346,27 @@ export function DataTable<TRow>({
327
346
  const tableRows = table.getRowModel().rows
328
347
  const tableRowCount = tableRows.length
329
348
 
349
+ // Flatten rows into the virtualized list, splicing a heading in wherever the
350
+ // group changes. Without `groupBy` this is the rows themselves.
351
+ const listItems = useMemo<ListItem<TRow>[]>(() => {
352
+ if (!groupBy) {
353
+ return tableRows.map((row, dataIndex) => ({ kind: 'row', row, dataIndex }) as const)
354
+ }
355
+ const items: ListItem<TRow>[] = []
356
+ let currentGroupId: string | null = null
357
+ tableRows.forEach((row, dataIndex) => {
358
+ const group = groupBy(row.original)
359
+ if (group && group.id !== currentGroupId) {
360
+ currentGroupId = group.id
361
+ items.push({ kind: 'group', id: group.id, label: group.label })
362
+ } else if (!group) {
363
+ currentGroupId = null
364
+ }
365
+ items.push({ kind: 'row', row, dataIndex })
366
+ })
367
+ return items
368
+ }, [tableRows, groupBy])
369
+
330
370
  // Reset scroll to top when sorting changes — without this, the user is
331
371
  // staring at a different slice of a freshly-sorted dataset.
332
372
  const scrollerRef = useRef<HTMLDivElement | null>(null)
@@ -347,10 +387,12 @@ export function DataTable<TRow>({
347
387
  setListOffset((prev) => (prev === top ? prev : top))
348
388
  })
349
389
 
390
+ const headingHeight = groupHeaderHeight ?? rowHeight
391
+
350
392
  const virtualizer = useVirtualizer({
351
- count: tableRowCount,
393
+ count: listItems.length,
352
394
  getScrollElement: () => scrollerRef.current,
353
- estimateSize: () => rowHeight,
395
+ estimateSize: (index) => (listItems[index]?.kind === 'group' ? headingHeight : rowHeight),
354
396
  overscan,
355
397
  scrollMargin: listOffset,
356
398
  })
@@ -511,8 +553,8 @@ export function DataTable<TRow>({
511
553
  style={{ height: totalSize, position: 'relative', width: '100%' }}
512
554
  >
513
555
  {virtualItems.map((vRow) => {
514
- const row = tableRows[vRow.index]
515
- if (!row) return null
556
+ const item = listItems[vRow.index]
557
+ if (!item) return null
516
558
  const rowStyle: CSSProperties = {
517
559
  // Items' `start` includes the virtualizer's scrollMargin
518
560
  // (the list's offset below the sticky header); subtract
@@ -520,11 +562,18 @@ export function DataTable<TRow>({
520
562
  transform: `translateY(${vRow.start - listOffset}px)`,
521
563
  height: vRow.size,
522
564
  }
565
+ if (item.kind === 'group') {
566
+ return (
567
+ <GroupHeaderRow key={`group:${item.id}`} style={rowStyle}>
568
+ {item.label}
569
+ </GroupHeaderRow>
570
+ )
571
+ }
523
572
  return (
524
573
  <DataRow
525
- key={row.id}
526
- row={row}
527
- index={vRow.index}
574
+ key={item.row.id}
575
+ row={item.row}
576
+ index={item.dataIndex}
528
577
  zebra={zebra}
529
578
  onRowClick={onRowClick}
530
579
  style={rowStyle}
@@ -554,6 +603,25 @@ export function DataTable<TRow>({
554
603
  )
555
604
  }
556
605
 
606
+ /* ---------- Group heading ------------------------------------------------ */
607
+
608
+ function GroupHeaderRow({ style, children }: { style: CSSProperties; children: ReactNode }) {
609
+ return (
610
+ <TableRow
611
+ absolute
612
+ style={style}
613
+ className={cn(
614
+ 'bg-surface-2 border-b border-rule',
615
+ 'font-mono text-[11px] uppercase tracking-[0.06em] text-ink-3',
616
+ )}
617
+ >
618
+ {/* One cell spanning the row — the heading labels the rows beneath it
619
+ rather than any single column. */}
620
+ <TableCell className="gap-2 text-inherit">{children}</TableCell>
621
+ </TableRow>
622
+ )
623
+ }
624
+
557
625
  /* ---------- Row ---------------------------------------------------------- */
558
626
 
559
627
  type DataRowProps<TRow> = {
@@ -128,6 +128,8 @@ function ChipItem({
128
128
  return (
129
129
  <Badge
130
130
  variant={option?.variant}
131
+ shape="rounded"
132
+ hasDot={false}
131
133
  {...(option?.color ? { color: option.color } : {})}
132
134
  {...(option?.icon ? { icon: option.icon } : {})}
133
135
  >
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export * from './components/checkbox'
12
12
  export * from './components/color-input'
13
13
  export * from './components/combobox'
14
14
  export * from './components/command'
15
+ export * from './components/conversation-brief-card'
15
16
  export * from './components/currency-input'
16
17
  export * from './components/data-table'
17
18
  export * from './components/date-picker'
package/src/styles.css CHANGED
@@ -88,6 +88,7 @@
88
88
 
89
89
  /* Radii */
90
90
  --radius-sm: 6px;
91
+ --radius-chip: 9px; /* enum chips — softer than sm, still clearly not a pill */
91
92
  --radius-md: 10px;
92
93
  --radius-lg: 14px;
93
94
  --radius-xl: 18px;