@jinaan/uikit 0.1.46 → 0.1.47

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jinaan/uikit",
3
- "version": "0.1.46",
3
+ "version": "0.1.47",
4
4
  "description": "Reusable React and Tailwind UI components for Jinaan projects.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -13,6 +13,7 @@ import {
13
13
  X,
14
14
  } from "@phosphor-icons/react";
15
15
  import {
16
+ Fragment,
16
17
  useCallback,
17
18
  useEffect,
18
19
  useLayoutEffect,
@@ -138,6 +139,11 @@ interface DataTableProps<T> {
138
139
  bulkActions?: BulkAction<T>[];
139
140
 
140
141
  rowActions?: (row: T) => ReactNode;
142
+ /**
143
+ * Expanding rows: renders a chevron toggle cell at the row start and the
144
+ * returned content in a full-width muted band beneath the row.
145
+ */
146
+ renderExpanded?: (row: T) => ReactNode;
141
147
  onRowClick?: (row: T) => void;
142
148
 
143
149
  defaultSort?: { id: string; dir: "asc" | "desc" };
@@ -233,6 +239,7 @@ export function DataTable<T>({
233
239
  selectable = false,
234
240
  bulkActions,
235
241
  rowActions,
242
+ renderExpanded,
236
243
  onRowClick,
237
244
  defaultSort,
238
245
  sort: sortProp,
@@ -268,6 +275,15 @@ export function DataTable<T>({
268
275
  const [page, setPage] = useState(0);
269
276
  const [pageSize, setPageSize] = useState(initialPageSize);
270
277
  const [selected, setSelected] = useState<Set<string>>(new Set());
278
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
279
+ const toggleExpanded = (id: string) => {
280
+ setExpanded((prev) => {
281
+ const next = new Set(prev);
282
+ if (next.has(id)) next.delete(id);
283
+ else next.add(id);
284
+ return next;
285
+ });
286
+ };
271
287
  const [activeFilterIds, setActiveFilterIds] = useState<string[]>([]);
272
288
 
273
289
  const propOrder = columns.map((c) => c.id);
@@ -398,7 +414,7 @@ export function DataTable<T>({
398
414
  // order). The pinned colgroup maps positionally onto the columns, so any
399
415
  // structural change invalidates the measured widths — otherwise the new
400
416
  // column steals its neighbor's width and every column shifts by one.
401
- const structureKey = `${selectable ? "s" : "-"}|${rowActions ? "a" : "-"}|${columnKey}`;
417
+ const structureKey = `${selectable ? "s" : "-"}|${rowActions ? "a" : "-"}|${renderExpanded ? "e" : "-"}|${columnKey}`;
402
418
  const structureRef = useRef(structureKey);
403
419
  const pinned = columnWidths.length > 0;
404
420
 
@@ -772,6 +788,7 @@ export function DataTable<T>({
772
788
  />
773
789
  </th>
774
790
  )}
791
+ {renderExpanded && <th className="h-(--table-header-h) w-11 px-(--table-pad-x)" />}
775
792
  {visibleColumns.map((c) => (
776
793
  <th
777
794
  key={c.id}
@@ -886,6 +903,7 @@ export function DataTable<T>({
886
903
  <thead className="sr-only">
887
904
  <tr>
888
905
  {selectable && <th scope="col">Select</th>}
906
+ {renderExpanded && <th scope="col">Details</th>}
889
907
  {visibleColumns.map((c) => (
890
908
  <th key={c.id} scope="col" aria-sort={ariaSort(c)}>
891
909
  {c.header}
@@ -898,22 +916,49 @@ export function DataTable<T>({
898
916
  {pageRows.map((row) => {
899
917
  const id = getRowId(row);
900
918
  const isSelected = selected.has(id);
919
+ const isExpanded = expanded.has(id);
920
+ const columnCount =
921
+ visibleColumns.length +
922
+ (selectable ? 1 : 0) +
923
+ (renderExpanded ? 1 : 0) +
924
+ (rowActions ? 1 : 0);
901
925
  return (
902
- <TableRow
903
- key={id}
904
- data-state={isSelected ? "selected" : undefined}
905
- onClick={onRowClick ? () => onRowClick(row) : undefined}
906
- className={onRowClick ? "cursor-pointer" : undefined}
907
- >
908
- {selectable && (
909
- <TableCell className="w-11" onClick={(e) => e.stopPropagation()}>
910
- <Checkbox
911
- aria-label="Select row"
912
- checked={isSelected}
913
- onCheckedChange={() => toggleOne(id)}
914
- />
915
- </TableCell>
916
- )}
926
+ <Fragment key={id}>
927
+ <TableRow
928
+ data-state={isSelected ? "selected" : undefined}
929
+ onClick={onRowClick ? () => onRowClick(row) : undefined}
930
+ className={onRowClick ? "cursor-pointer" : undefined}
931
+ >
932
+ {selectable && (
933
+ <TableCell className="w-11" onClick={(e) => e.stopPropagation()}>
934
+ <Checkbox
935
+ aria-label="Select row"
936
+ checked={isSelected}
937
+ onCheckedChange={() => toggleOne(id)}
938
+ />
939
+ </TableCell>
940
+ )}
941
+ {renderExpanded && (
942
+ <TableCell className="w-11" onClick={(e) => e.stopPropagation()}>
943
+ <Button
944
+ type="button"
945
+ variant="ghost"
946
+ size="icon"
947
+ className="size-7"
948
+ aria-label={isExpanded ? "Collapse row" : "Expand row"}
949
+ aria-expanded={isExpanded}
950
+ onClick={() => toggleExpanded(id)}
951
+ >
952
+ <CaretDown
953
+ className={cn(
954
+ "size-3.5 text-muted transition-transform motion-safe:duration-200",
955
+ isExpanded ? "" : "-rotate-90",
956
+ )}
957
+ weight="bold"
958
+ />
959
+ </Button>
960
+ </TableCell>
961
+ )}
917
962
  {visibleColumns.map((c) => (
918
963
  <TableCell
919
964
  key={c.id}
@@ -923,23 +968,34 @@ export function DataTable<T>({
923
968
  {c.cell(row)}
924
969
  </TableCell>
925
970
  ))}
926
- {rowActions && (
927
- <TableCell
928
- className="w-44 px-4 text-right"
929
- onClick={(e) => {
930
- // Dead-zone fix: only swallow clicks on the
931
- // action controls themselves (menus, buttons);
932
- // the cell's padding stays part of the clickable
933
- // row.
934
- if ((e.target as HTMLElement).closest("button, a, input, [role='menuitem']")) {
935
- e.stopPropagation();
936
- }
937
- }}
938
- >
939
- {rowActions(row)}
940
- </TableCell>
941
- )}
942
- </TableRow>
971
+ {rowActions && (
972
+ <TableCell
973
+ className="w-44 px-4 text-right"
974
+ onClick={(e) => {
975
+ // Dead-zone fix: only swallow clicks on the
976
+ // action controls themselves (menus, buttons);
977
+ // the cell's padding stays part of the clickable
978
+ // row.
979
+ if ((e.target as HTMLElement).closest("button, a, input, [role='menuitem']")) {
980
+ e.stopPropagation();
981
+ }
982
+ }}
983
+ >
984
+ {rowActions(row)}
985
+ </TableCell>
986
+ )}
987
+ </TableRow>
988
+ {renderExpanded && isExpanded ? (
989
+ <TableRow className="hover:bg-inherit" data-expanded="">
990
+ <TableCell
991
+ colSpan={columnCount}
992
+ className="border-t border-line bg-elevated/60 px-(--table-pad-x) py-3 align-top text-[13px] text-muted"
993
+ >
994
+ {renderExpanded(row)}
995
+ </TableCell>
996
+ </TableRow>
997
+ ) : null}
998
+ </Fragment>
943
999
  );
944
1000
  })}
945
1001
  </TableBody>