@moloodev/ui 0.6.0 → 0.8.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/package.json +4 -3
- package/registry/components.ts +1 -1
- package/src/components/ui/data-table.tsx +150 -37
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moloodev/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"@shadcn/react": "^0.2.1",
|
|
95
95
|
"@tabler/icons-react": "^3.46.0",
|
|
96
96
|
"@tanstack/react-table": "^9.2.4",
|
|
97
|
+
"@tanstack/table-core": "^9.2.4",
|
|
97
98
|
"class-variance-authority": "^0.7.1",
|
|
98
99
|
"clsx": "^2.1.1",
|
|
99
100
|
"cmdk": "^1.1.1",
|
|
@@ -122,8 +123,8 @@
|
|
|
122
123
|
"tailwindcss": "^4.3.3",
|
|
123
124
|
"tsx": "^4.23.13",
|
|
124
125
|
"typescript": "^7.0.2",
|
|
125
|
-
"@moloodev/platform": "0.
|
|
126
|
-
"@moloodev/typescript-config": "0.
|
|
126
|
+
"@moloodev/platform": "0.8.0",
|
|
127
|
+
"@moloodev/typescript-config": "0.8.0"
|
|
127
128
|
},
|
|
128
129
|
"publishConfig": {
|
|
129
130
|
"access": "public"
|
package/registry/components.ts
CHANGED
|
@@ -95,7 +95,7 @@ export const components: ComponentMeta[] = [
|
|
|
95
95
|
{ slug: "kind-icon", title: "Kind Icon", description: "Lazily loads any Tabler icon by kebab-case name for asset kinds.", category: "Data Display" },
|
|
96
96
|
{ slug: "kind-mark", title: "Kind Mark", description: "Coloured kind tile with a Tabler glyph. Props match platform kind values.", category: "Data Display" },
|
|
97
97
|
{ slug: "chart", title: "Chart", description: "Charts built with Recharts in the moloo palette.", category: "Data Display" },
|
|
98
|
-
{ slug: "data-table", title: "Data Table", description: "Headless table from TanStack Table createTableHook, with sorting, row selection, and
|
|
98
|
+
{ slug: "data-table", title: "Data Table", description: "Headless table from TanStack Table createTableHook, with sorting, row selection, pagination, and per-column size and class names.", category: "Data Display" },
|
|
99
99
|
{ slug: "bubble", title: "Bubble", description: "Chat bubble content with variants for messages.", category: "Data Display" },
|
|
100
100
|
{ slug: "message", title: "Message", description: "Compose chat messages with alignment and metadata.", category: "Data Display" },
|
|
101
101
|
{ slug: "message-scroller", title: "Message Scroller", description: "Scroll container helpers for chat threads.", category: "Data Display" },
|
|
@@ -28,11 +28,14 @@ import {
|
|
|
28
28
|
sortFn_text,
|
|
29
29
|
Subscribe,
|
|
30
30
|
tableFeatures,
|
|
31
|
+
type CellData,
|
|
31
32
|
type ColumnFiltersState,
|
|
32
33
|
type PaginationState,
|
|
33
34
|
type RowData,
|
|
34
35
|
type RowSelectionState,
|
|
35
36
|
type SortingState,
|
|
37
|
+
type TableFeature,
|
|
38
|
+
type TableFeatures,
|
|
36
39
|
type TableState,
|
|
37
40
|
} from "@tanstack/react-table"
|
|
38
41
|
|
|
@@ -65,6 +68,7 @@ import {
|
|
|
65
68
|
} from "./table"
|
|
66
69
|
import { toast } from "./toast"
|
|
67
70
|
import { cn } from "../../lib/utils"
|
|
71
|
+
import { formatRelativeTime } from "../../lib/format-relative-time"
|
|
68
72
|
|
|
69
73
|
export type EnumFilterOption = {
|
|
70
74
|
value: string
|
|
@@ -83,7 +87,39 @@ export type ColumnMetaWithCopyable = {
|
|
|
83
87
|
copyable?: boolean
|
|
84
88
|
}
|
|
85
89
|
|
|
86
|
-
|
|
90
|
+
export interface ColumnMetaWithLayout {
|
|
91
|
+
className?: string
|
|
92
|
+
headerClassName?: string
|
|
93
|
+
cellClassName?: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
declare module "@tanstack/table-core" {
|
|
97
|
+
interface ColumnMeta<
|
|
98
|
+
in out TFeatures extends TableFeatures,
|
|
99
|
+
in out TData extends RowData,
|
|
100
|
+
TValue extends CellData = CellData,
|
|
101
|
+
> extends ColumnMetaWithLayout {}
|
|
102
|
+
|
|
103
|
+
interface Plugins {
|
|
104
|
+
explicitColumnSizeFeature?: TableFeature
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface ColumnDef_FeatureMap<
|
|
108
|
+
in out TFeatures extends TableFeatures,
|
|
109
|
+
in out TData extends RowData,
|
|
110
|
+
TValue extends CellData,
|
|
111
|
+
> {
|
|
112
|
+
explicitColumnSizeFeature: { size?: number }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type DataTableColumnMeta = ColumnMetaWithEnumFilter &
|
|
117
|
+
ColumnMetaWithCopyable &
|
|
118
|
+
ColumnMetaWithLayout
|
|
119
|
+
|
|
120
|
+
const explicitColumnSizeFeature = {
|
|
121
|
+
getDefaultColumnDef: () => ({}),
|
|
122
|
+
} satisfies TableFeature
|
|
87
123
|
|
|
88
124
|
export function compareValues(av: unknown, bv: unknown): number {
|
|
89
125
|
if (av == null && bv == null) return 0
|
|
@@ -297,19 +333,26 @@ function BadgeCell({
|
|
|
297
333
|
return <Badge variant={variant}>{String(value)}</Badge>
|
|
298
334
|
}
|
|
299
335
|
|
|
300
|
-
function
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return date.toLocaleString(locale)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function DateTime({ locale }: { locale?: string } = {}) {
|
|
336
|
+
function DateTime({
|
|
337
|
+
locale,
|
|
338
|
+
relative = false,
|
|
339
|
+
}: { locale?: string; relative?: boolean } = {}) {
|
|
308
340
|
const cell = useCellContext()
|
|
341
|
+
const value = cell.getValue()
|
|
342
|
+
const date = value instanceof Date ? value : new Date(String(value))
|
|
343
|
+
if (value == null || value === "" || Number.isNaN(date.getTime())) {
|
|
344
|
+
return <span className="text-muted-foreground tabular-nums">—</span>
|
|
345
|
+
}
|
|
346
|
+
const exact = date.toLocaleString(locale)
|
|
309
347
|
return (
|
|
310
|
-
<
|
|
311
|
-
{
|
|
312
|
-
|
|
348
|
+
<time
|
|
349
|
+
dateTime={date.toISOString()}
|
|
350
|
+
title={exact}
|
|
351
|
+
suppressHydrationWarning
|
|
352
|
+
className="text-muted-foreground tabular-nums"
|
|
353
|
+
>
|
|
354
|
+
{relative ? formatRelativeTime(date, Date.now(), locale) : exact}
|
|
355
|
+
</time>
|
|
313
356
|
)
|
|
314
357
|
}
|
|
315
358
|
|
|
@@ -550,6 +593,7 @@ function SelectHeader({
|
|
|
550
593
|
|
|
551
594
|
const features = tableFeatures({
|
|
552
595
|
columnMeta: metaHelper<DataTableColumnMeta>(),
|
|
596
|
+
explicitColumnSizeFeature,
|
|
553
597
|
columnFilteringFeature,
|
|
554
598
|
rowPaginationFeature,
|
|
555
599
|
rowSelectionFeature,
|
|
@@ -593,6 +637,8 @@ const {
|
|
|
593
637
|
|
|
594
638
|
export const useTable = useAppTable
|
|
595
639
|
export const createColumnHelper = createAppColumnHelper
|
|
640
|
+
export type ColumnDef<TData extends RowData> =
|
|
641
|
+
Parameters<typeof useTable<TData>>[0]["columns"][number]
|
|
596
642
|
export { useTableContext, useCellContext, useHeaderContext, appFeatures, features }
|
|
597
643
|
|
|
598
644
|
export type Table<
|
|
@@ -609,6 +655,7 @@ type RootProps<TData extends RowData, TSelected> = {
|
|
|
609
655
|
loading?: boolean
|
|
610
656
|
loadingFallback?: React.ReactNode
|
|
611
657
|
empty?: React.ReactNode
|
|
658
|
+
onRowActivate?: (record: TData, element: HTMLTableRowElement) => void
|
|
612
659
|
selector?: (state: TableState<typeof features>) => ViewState
|
|
613
660
|
}
|
|
614
661
|
|
|
@@ -630,6 +677,30 @@ function reorderManualSortPage<TData extends RowData, TSelected>(
|
|
|
630
677
|
})
|
|
631
678
|
}
|
|
632
679
|
|
|
680
|
+
function explicitColumnSize(column: {
|
|
681
|
+
columnDef: { size?: number }
|
|
682
|
+
}): number | undefined {
|
|
683
|
+
const size = column.columnDef.size
|
|
684
|
+
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) return undefined
|
|
685
|
+
return size
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function columnClassName(
|
|
689
|
+
meta: ColumnMetaWithLayout | undefined,
|
|
690
|
+
part: "header" | "cell",
|
|
691
|
+
): string | undefined {
|
|
692
|
+
const classes = cn(
|
|
693
|
+
meta?.className,
|
|
694
|
+
part === "header" ? meta?.headerClassName : meta?.cellClassName,
|
|
695
|
+
)
|
|
696
|
+
return classes === "" ? undefined : classes
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function columnSizeStyle(size: number | undefined): React.CSSProperties | undefined {
|
|
700
|
+
if (size == null) return undefined
|
|
701
|
+
return { width: size, minWidth: size, maxWidth: size }
|
|
702
|
+
}
|
|
703
|
+
|
|
633
704
|
function columnAriaSort(
|
|
634
705
|
sorting: SortingState | undefined,
|
|
635
706
|
columnId: string,
|
|
@@ -647,6 +718,7 @@ export function Root<TData extends RowData, TSelected>({
|
|
|
647
718
|
loading,
|
|
648
719
|
loadingFallback,
|
|
649
720
|
empty = "No results.",
|
|
721
|
+
onRowActivate,
|
|
650
722
|
selector = (state) => state,
|
|
651
723
|
}: RootProps<TData, TSelected>) {
|
|
652
724
|
return (
|
|
@@ -654,30 +726,51 @@ export function Root<TData extends RowData, TSelected>({
|
|
|
654
726
|
{(state) => {
|
|
655
727
|
const rows = reorderManualSortPage(table, state.sorting)
|
|
656
728
|
const showEmpty = !loading && rows.length === 0
|
|
729
|
+
const leaves = table.getAllLeafColumns()
|
|
730
|
+
const sized = leaves.some((column) => explicitColumnSize(column) != null)
|
|
657
731
|
|
|
658
732
|
return (
|
|
659
733
|
<div data-slot="data-table" className="flex w-full flex-col gap-3">
|
|
660
734
|
{toolbar}
|
|
661
735
|
<div className="overflow-hidden rounded-md border-2 border-border">
|
|
662
|
-
<Table>
|
|
736
|
+
<Table className={sized ? "table-fixed" : undefined}>
|
|
737
|
+
{sized ? (
|
|
738
|
+
<colgroup>
|
|
739
|
+
{leaves.map((column) => (
|
|
740
|
+
<col
|
|
741
|
+
key={column.id}
|
|
742
|
+
style={columnSizeStyle(explicitColumnSize(column))}
|
|
743
|
+
/>
|
|
744
|
+
))}
|
|
745
|
+
</colgroup>
|
|
746
|
+
) : null}
|
|
663
747
|
<TableHeader>
|
|
664
748
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
665
749
|
<TableRow key={headerGroup.id}>
|
|
666
|
-
{headerGroup.headers.map((header) =>
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
750
|
+
{headerGroup.headers.map((header) => {
|
|
751
|
+
const leaf = header.column.columns.length === 0
|
|
752
|
+
const size = leaf ? explicitColumnSize(header.column) : undefined
|
|
753
|
+
return (
|
|
754
|
+
<table.AppHeader key={header.id} header={header}>
|
|
755
|
+
{(bound) => (
|
|
756
|
+
<TableHead
|
|
757
|
+
aria-sort={
|
|
758
|
+
header.column.getCanSort()
|
|
759
|
+
? columnAriaSort(state.sorting, header.column.id)
|
|
760
|
+
: undefined
|
|
761
|
+
}
|
|
762
|
+
className={cn(
|
|
763
|
+
size != null && "overflow-hidden",
|
|
764
|
+
columnClassName(header.column.columnDef.meta, "header"),
|
|
765
|
+
)}
|
|
766
|
+
style={columnSizeStyle(size)}
|
|
767
|
+
>
|
|
768
|
+
{header.isPlaceholder ? null : <bound.FlexRender />}
|
|
769
|
+
</TableHead>
|
|
770
|
+
)}
|
|
771
|
+
</table.AppHeader>
|
|
772
|
+
)
|
|
773
|
+
})}
|
|
681
774
|
</TableRow>
|
|
682
775
|
))}
|
|
683
776
|
</TableHeader>
|
|
@@ -699,16 +792,36 @@ export function Root<TData extends RowData, TSelected>({
|
|
|
699
792
|
<TableRow
|
|
700
793
|
key={row.id}
|
|
701
794
|
data-state={row.getIsSelected() ? "selected" : undefined}
|
|
795
|
+
tabIndex={onRowActivate ? 0 : undefined}
|
|
796
|
+
className={onRowActivate ? "cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring" : undefined}
|
|
797
|
+
onClick={onRowActivate ? (event) => {
|
|
798
|
+
if ((event.target as Element).closest("button, a, input, select, textarea, label, [role=button], [role=checkbox], [role=menuitem], [data-row-control]")) return
|
|
799
|
+
onRowActivate(row.original, event.currentTarget)
|
|
800
|
+
} : undefined}
|
|
801
|
+
onKeyDown={onRowActivate ? (event) => {
|
|
802
|
+
if (event.target !== event.currentTarget || !["Enter", " "].includes(event.key)) return
|
|
803
|
+
event.preventDefault()
|
|
804
|
+
onRowActivate(row.original, event.currentTarget)
|
|
805
|
+
} : undefined}
|
|
702
806
|
>
|
|
703
|
-
{row.getAllCells().map((cell) =>
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
807
|
+
{row.getAllCells().map((cell) => {
|
|
808
|
+
const size = explicitColumnSize(cell.column)
|
|
809
|
+
return (
|
|
810
|
+
<table.AppCell key={cell.id} cell={cell}>
|
|
811
|
+
{(bound) => (
|
|
812
|
+
<TableCell
|
|
813
|
+
className={cn(
|
|
814
|
+
size != null && "overflow-hidden",
|
|
815
|
+
columnClassName(cell.column.columnDef.meta, "cell"),
|
|
816
|
+
)}
|
|
817
|
+
style={columnSizeStyle(size)}
|
|
818
|
+
>
|
|
819
|
+
<bound.FlexRender />
|
|
820
|
+
</TableCell>
|
|
821
|
+
)}
|
|
822
|
+
</table.AppCell>
|
|
823
|
+
)
|
|
824
|
+
})}
|
|
712
825
|
</TableRow>
|
|
713
826
|
))
|
|
714
827
|
)}
|