@douglasneuroinformatics/libui 2.2.4 → 2.3.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.
Files changed (48) hide show
  1. package/dist/components/ActionDropdown/ActionDropdown.d.ts +20 -0
  2. package/dist/components/ActionDropdown/ActionDropdown.d.ts.map +1 -0
  3. package/dist/components/ActionDropdown/ActionDropdown.js +15 -0
  4. package/dist/components/ClientTable/ClientTable.d.ts +33 -0
  5. package/dist/components/ClientTable/ClientTable.d.ts.map +1 -0
  6. package/dist/components/ClientTable/ClientTable.js +69 -0
  7. package/dist/components/ClientTable/ClientTablePagination.d.ts +11 -0
  8. package/dist/components/ClientTable/ClientTablePagination.d.ts.map +1 -0
  9. package/dist/components/ClientTable/ClientTablePagination.js +20 -0
  10. package/dist/components/ListboxDropdown/ListboxDropdown.d.ts +18 -0
  11. package/dist/components/ListboxDropdown/ListboxDropdown.d.ts.map +1 -0
  12. package/dist/components/ListboxDropdown/ListboxDropdown.js +23 -0
  13. package/dist/components/Sheet/Sheet.d.ts +1 -1
  14. package/dist/components/Sheet/SheetContent.d.ts +2 -2
  15. package/dist/components/Table/TableCell.d.ts.map +1 -1
  16. package/dist/components/Table/TableCell.js +1 -1
  17. package/dist/components/Table/TableHead.js +1 -1
  18. package/dist/components.d.ts +3 -4
  19. package/dist/components.d.ts.map +1 -1
  20. package/dist/components.js +3 -4
  21. package/package.json +1 -1
  22. package/src/components/ActionDropdown/ActionDropdown.tsx +60 -0
  23. package/src/components/ClientTable/ClientTable.tsx +165 -0
  24. package/src/components/ClientTable/ClientTablePagination.tsx +60 -0
  25. package/src/components/ListboxDropdown/ListboxDropdown.tsx +61 -0
  26. package/src/components/Table/TableCell.tsx +3 -1
  27. package/src/components/Table/TableHead.tsx +1 -1
  28. package/src/components.ts +3 -4
  29. package/dist/components/LegacyDropdown/LegacyDropdown.d.ts +0 -21
  30. package/dist/components/LegacyDropdown/LegacyDropdown.d.ts.map +0 -1
  31. package/dist/components/LegacyDropdown/LegacyDropdown.js +0 -17
  32. package/dist/components/LegacySelectDropdown/LegacySelectDropdown.d.ts +0 -21
  33. package/dist/components/LegacySelectDropdown/LegacySelectDropdown.d.ts.map +0 -1
  34. package/dist/components/LegacySelectDropdown/LegacySelectDropdown.js +0 -18
  35. package/dist/components/LegacyTable/LegacyClientTable.d.ts +0 -5
  36. package/dist/components/LegacyTable/LegacyClientTable.d.ts.map +0 -1
  37. package/dist/components/LegacyTable/LegacyClientTable.js +0 -29
  38. package/dist/components/LegacyTable/LegacyTable.d.ts +0 -29
  39. package/dist/components/LegacyTable/LegacyTable.d.ts.map +0 -1
  40. package/dist/components/LegacyTable/LegacyTable.js +0 -54
  41. package/dist/components/LegacyTable/LegacyTableColumnHeader.d.ts +0 -16
  42. package/dist/components/LegacyTable/LegacyTableColumnHeader.d.ts.map +0 -1
  43. package/dist/components/LegacyTable/LegacyTableColumnHeader.js +0 -20
  44. package/src/components/LegacyDropdown/LegacyDropdown.tsx +0 -79
  45. package/src/components/LegacySelectDropdown/LegacySelectDropdown.tsx +0 -87
  46. package/src/components/LegacyTable/LegacyClientTable.tsx +0 -55
  47. package/src/components/LegacyTable/LegacyTable.tsx +0 -118
  48. package/src/components/LegacyTable/LegacyTableColumnHeader.tsx +0 -71
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import type { DropdownMenuContentProps } from '../DropdownMenu/DropdownMenuContent.js';
3
+ type ActionDropdownOptions = {
4
+ [key: string]: string;
5
+ } | readonly string[];
6
+ type ActionDropdownOptionKey<T> = T extends readonly string[] ? T[number] : T extends {
7
+ [key: string]: string;
8
+ } ? Extract<keyof T, string> : never;
9
+ export type ActionDropdownProps<T extends ActionDropdownOptions> = {
10
+ align?: DropdownMenuContentProps['align'];
11
+ /** Callback function invoked when user clicks an option */
12
+ onSelection: (option: ActionDropdownOptionKey<T>) => void;
13
+ /** Either a list of options for the dropdown, or an object with options mapped to custom labels */
14
+ options: T;
15
+ /** The text content for the dropdown toggle */
16
+ title: string;
17
+ };
18
+ export declare function ActionDropdown<const T extends ActionDropdownOptions>({ align, onSelection, options, title }: ActionDropdownProps<T>): React.JSX.Element;
19
+ export {};
20
+ //# sourceMappingURL=ActionDropdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActionDropdown.d.ts","sourceRoot":"","sources":["../../../src/components/ActionDropdown/ActionDropdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC;AAEvF,KAAK,qBAAqB,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,SAAS,MAAM,EAAE,CAAC;AAE3E,KAAK,uBAAuB,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,MAAM,EAAE,GACzD,CAAC,CAAC,MAAM,CAAC,GACT,CAAC,SAAS;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACjC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,GACxB,KAAK,CAAC;AAEZ,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,qBAAqB,IAAI;IACjE,KAAK,CAAC,EAAE,wBAAwB,CAAC,OAAO,CAAC,CAAC;IAE1C,2DAA2D;IAC3D,WAAW,EAAE,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IAE1D,oGAAoG;IACpG,OAAO,EAAE,CAAC,CAAC;IAEX,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAGF,wBAAgB,cAAc,CAAC,KAAK,CAAC,CAAC,SAAS,qBAAqB,EAAE,EACpE,KAAe,EACf,WAAW,EACX,OAAO,EACP,KAAK,EACN,EAAE,mBAAmB,CAAC,CAAC,CAAC,qBAyBxB"}
@@ -0,0 +1,15 @@
1
+ import React from 'react';
2
+ import { DropdownButton } from '../DropdownButton/DropdownButton.js';
3
+ import { DropdownMenu } from '../DropdownMenu/DropdownMenu.js';
4
+ // eslint-disable-next-line react/function-component-definition
5
+ export function ActionDropdown({ align = 'start', onSelection, options, title }) {
6
+ const optionKeys = options instanceof Array ? options : Object.keys(options);
7
+ return (React.createElement(DropdownMenu, null,
8
+ React.createElement("div", { className: "w-full" },
9
+ React.createElement(DropdownMenu.Trigger, { asChild: true },
10
+ React.createElement(DropdownButton, null, title)),
11
+ React.createElement(DropdownMenu.Content, { align: align, style: { width: 'var(--radix-popper-anchor-width)' } },
12
+ React.createElement(DropdownMenu.Group, null, optionKeys.map((option) => (React.createElement(DropdownMenu.Item, { key: option, onClick: () => {
13
+ onSelection(option);
14
+ } }, Array.isArray(options) ? option : options[option]))))))));
15
+ }
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ export type ClientTableEntry = {
3
+ [key: string]: unknown;
4
+ };
5
+ export type ClientFieldFactory<T extends ClientTableEntry = ClientTableEntry> = (entry: T) => string;
6
+ export type ClientTableColumn<T extends ClientTableEntry> = {
7
+ /** How to determine the values for column */
8
+ field: ClientFieldFactory<T> | keyof T;
9
+ /** Override the default formatter for this field */
10
+ formatter?: (value: any) => string;
11
+ /** The label to be displayed on the header */
12
+ label: string;
13
+ };
14
+ export type ClientTableDropdownOptions<T extends ClientTableEntry> = {
15
+ icon?: React.ComponentType<Omit<React.SVGProps<SVGSVGElement>, 'ref'>>;
16
+ label: string;
17
+ onSelection: (column: ClientTableColumn<T>) => void;
18
+ }[];
19
+ export type ClientTableColumnProps<T extends ClientTableEntry> = {
20
+ column: ClientTableColumn<T>;
21
+ dropdownOptions?: ClientTableDropdownOptions<T>;
22
+ };
23
+ export type ClientTableProps<T extends ClientTableEntry> = {
24
+ className?: string;
25
+ columnDropdownOptions?: ClientTableDropdownOptions<T>;
26
+ columns: ClientTableColumn<T>[];
27
+ data: T[];
28
+ entriesPerPage?: number;
29
+ minRows?: number;
30
+ onEntryClick?: (entry: T) => void;
31
+ };
32
+ export declare const ClientTable: <T extends ClientTableEntry>({ className, columnDropdownOptions, columns, data, entriesPerPage, minRows, onEntryClick }: ClientTableProps<T>) => React.JSX.Element;
33
+ //# sourceMappingURL=ClientTable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientTable.d.ts","sourceRoot":"","sources":["../../../src/components/ClientTable/ClientTable.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AA0BxC,MAAM,MAAM,gBAAgB,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAE1D,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,gBAAgB,GAAG,gBAAgB,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC;AAErG,MAAM,MAAM,iBAAiB,CAAC,CAAC,SAAS,gBAAgB,IAAI;IAC1D,6CAA6C;IAC7C,KAAK,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;IAEvC,oDAAoD;IAEpD,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,MAAM,CAAC;IAEnC,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,SAAS,gBAAgB,IAAI;IACnE,IAAI,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;IACvE,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;CACrD,EAAE,CAAC;AAEJ,MAAM,MAAM,sBAAsB,CAAC,CAAC,SAAS,gBAAgB,IAAI;IAC/D,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAC7B,eAAe,CAAC,EAAE,0BAA0B,CAAC,CAAC,CAAC,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,IAAI;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qBAAqB,CAAC,EAAE,0BAA0B,CAAC,CAAC,CAAC,CAAC;IACtD,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC;IAChC,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,eAAO,MAAM,WAAW,2HAQrB,iBAAiB,CAAC,CAAC,sBA6FrB,CAAC"}
@@ -0,0 +1,69 @@
1
+ import React, { useState } from 'react';
2
+ import { toBasicISOString } from '@douglasneuroinformatics/libjs';
3
+ import { range } from 'lodash-es';
4
+ import { ChevronDownIcon } from 'lucide-react';
5
+ import { cn } from '../../utils.js';
6
+ import { DropdownMenu } from '../DropdownMenu/DropdownMenu.js';
7
+ import { Table } from '../Table/Table.js';
8
+ import { ClientTablePagination } from './ClientTablePagination.js';
9
+ /** Coerces the value in a cell to a string for consistant display purposes */
10
+ function defaultFormatter(value) {
11
+ if (typeof value === 'string') {
12
+ return value;
13
+ }
14
+ else if (typeof value === 'number') {
15
+ return value.toFixed(2).toString();
16
+ }
17
+ else if (typeof value === 'undefined') {
18
+ return 'NA';
19
+ }
20
+ if (value instanceof Date) {
21
+ return toBasicISOString(value);
22
+ }
23
+ return JSON.stringify(value);
24
+ }
25
+ export const ClientTable = ({ className, columnDropdownOptions, columns, data, entriesPerPage = 10, minRows, onEntryClick }) => {
26
+ const [currentPage, setCurrentPage] = useState(1);
27
+ const pageCount = Math.max(Math.ceil(data.length / entriesPerPage), 1);
28
+ const firstEntry = data.length === 0 ? 0 : (currentPage - 1) * entriesPerPage + 1;
29
+ const lastEntry = Math.min(firstEntry + entriesPerPage - 1, data.length);
30
+ const currentEntries = data.slice(firstEntry - 1, lastEntry);
31
+ const nRows = Math.max(currentEntries.length, minRows ?? -1);
32
+ return (React.createElement("div", { className: className },
33
+ React.createElement("div", { className: "rounded-md border bg-card tracking-tight text-muted-foreground shadow" },
34
+ React.createElement(Table, null,
35
+ React.createElement(Table.Header, null,
36
+ React.createElement(Table.Row, null, columns.map((column, i) => (React.createElement(Table.Head, { className: "whitespace-nowrap text-foreground", key: i }, columnDropdownOptions ? (React.createElement(DropdownMenu, null,
37
+ React.createElement(DropdownMenu.Trigger, { className: "flex items-center justify-between gap-3" },
38
+ React.createElement("span", null, column.label),
39
+ React.createElement(ChevronDownIcon, null)),
40
+ React.createElement(DropdownMenu.Content, { align: "start" },
41
+ React.createElement(DropdownMenu.Group, null, columnDropdownOptions.map((option) => {
42
+ const Icon = option.icon;
43
+ return (React.createElement(DropdownMenu.Item, { key: option.label, onClick: () => {
44
+ option.onSelection(column);
45
+ } },
46
+ Icon && React.createElement(Icon, { className: "mr-2", height: 16, width: 16 }),
47
+ option.label));
48
+ }))))) : (column.label)))))),
49
+ React.createElement(Table.Body, null, range(nRows).map((i) => {
50
+ const entry = currentEntries[i];
51
+ return (React.createElement(Table.Row, { className: cn(typeof onEntryClick === 'function' && 'cursor-pointer hover:backdrop-brightness-95'), key: i, onClick: () => {
52
+ onEntryClick?.(entry);
53
+ } }, columns.map(({ field, formatter }, j) => {
54
+ let value;
55
+ if (!entry) {
56
+ value = 'NA';
57
+ }
58
+ else if (typeof field === 'function') {
59
+ value = field(entry);
60
+ }
61
+ else {
62
+ value = entry[field];
63
+ }
64
+ const formattedValue = entry && formatter ? formatter(value) : defaultFormatter(value);
65
+ return (React.createElement(Table.Cell, { className: cn('text-ellipsis leading-none', !entry && 'invisible'), key: j }, formattedValue));
66
+ })));
67
+ })))),
68
+ React.createElement(ClientTablePagination, { currentPage: currentPage, firstEntry: firstEntry, lastEntry: lastEntry, pageCount: pageCount, setCurrentPage: setCurrentPage, totalEntries: data.length })));
69
+ };
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ export type ClientPagePaginationProps = {
3
+ currentPage: number;
4
+ firstEntry: number;
5
+ lastEntry: number;
6
+ pageCount: number;
7
+ setCurrentPage: (value: number) => void;
8
+ totalEntries: number;
9
+ };
10
+ export declare const ClientTablePagination: ({ currentPage, firstEntry, lastEntry, pageCount, setCurrentPage, totalEntries }: ClientPagePaginationProps) => React.JSX.Element;
11
+ //# sourceMappingURL=ClientTablePagination.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClientTablePagination.d.ts","sourceRoot":"","sources":["../../../src/components/ClientTable/ClientTablePagination.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,qBAAqB,oFAO/B,yBAAyB,sBAqC3B,CAAC"}
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button } from '../Button/Button.js';
4
+ export const ClientTablePagination = ({ currentPage, firstEntry, lastEntry, pageCount, setCurrentPage, totalEntries }) => {
5
+ const { t } = useTranslation('libui');
6
+ return (React.createElement("div", { className: "flex items-center justify-between py-3" },
7
+ React.createElement("div", { className: "hidden sm:block" },
8
+ React.createElement("p", { className: "text-sm font-medium text-muted-foreground" }, t('pagination.info', {
9
+ first: firstEntry,
10
+ last: lastEntry,
11
+ total: totalEntries
12
+ }))),
13
+ React.createElement("div", { className: "flex flex-1 justify-between gap-3 sm:justify-end" },
14
+ React.createElement(Button, { disabled: currentPage === 1, type: "button", variant: "outline", onClick: () => {
15
+ setCurrentPage(currentPage - 1);
16
+ } }, t('pagination.previous')),
17
+ React.createElement(Button, { disabled: currentPage === pageCount, type: "button", variant: "outline", onClick: () => {
18
+ setCurrentPage(currentPage + 1);
19
+ } }, t('pagination.next')))));
20
+ };
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { type ButtonProps } from '../Button/Button.js';
3
+ export type ListboxDropdownOption = {
4
+ key: string;
5
+ label: string;
6
+ };
7
+ export type ListboxDropdownProps<T extends ListboxDropdownOption> = {
8
+ checkPosition?: 'left' | 'right';
9
+ className?: string;
10
+ options: T[];
11
+ selected: T[];
12
+ setSelected: React.Dispatch<React.SetStateAction<T[]>>;
13
+ title: string;
14
+ /** The button variant to use for the dropdown toggle */
15
+ variant?: ButtonProps['variant'];
16
+ };
17
+ export declare const ListboxDropdown: <T extends ListboxDropdownOption>({ options, selected, setSelected, title }: ListboxDropdownProps<T>) => React.JSX.Element;
18
+ //# sourceMappingURL=ListboxDropdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ListboxDropdown.d.ts","sourceRoot":"","sources":["../../../src/components/ListboxDropdown/ListboxDropdown.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,qBAAqB,IAAI;IAClE,aAAa,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,QAAQ,EAAE,CAAC,EAAE,CAAC;IACd,WAAW,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;CAClC,CAAC;AAEF,eAAO,MAAM,eAAe,+EAKzB,qBAAqB,CAAC,CAAC,sBAiCzB,CAAC"}
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import {} from '../Button/Button.js';
3
+ import { DropdownButton } from '../DropdownButton/DropdownButton.js';
4
+ import { DropdownMenu } from '../DropdownMenu/DropdownMenu.js';
5
+ export const ListboxDropdown = ({ options, selected, setSelected, title }) => {
6
+ return (React.createElement(DropdownMenu, null,
7
+ React.createElement(DropdownMenu.Trigger, { asChild: true, className: "w-full" },
8
+ React.createElement(DropdownButton, null, title)),
9
+ React.createElement(DropdownMenu.Content, { widthFull: true, align: "start" }, options.map((option) => {
10
+ const checked = Boolean(selected.find((selectedOption) => selectedOption.key === option.key));
11
+ return (React.createElement(DropdownMenu.CheckboxItem, { checked: checked, className: "flex w-full items-center whitespace-nowrap bg-slate-50 p-2 text-sm hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 ", "data-cy": "select-dropdown-option", key: option.key, onSelect: (event) => {
12
+ event.preventDefault();
13
+ if (checked) {
14
+ setSelected((prevSelected) => {
15
+ return prevSelected.filter((selectedOption) => selectedOption.key !== option.key);
16
+ });
17
+ }
18
+ else {
19
+ setSelected((prevSelected) => [...prevSelected, option]);
20
+ }
21
+ } }, option.label));
22
+ }))));
23
+ };
@@ -2,7 +2,7 @@
2
2
  export declare const Sheet: import("react").FC<import("@radix-ui/react-dialog").DialogProps> & {
3
3
  Close: import("react").ForwardRefExoticComponent<import("@radix-ui/react-dialog").DialogCloseProps & import("react").RefAttributes<HTMLButtonElement>>;
4
4
  Content: import("react").ForwardRefExoticComponent<Omit<import("@radix-ui/react-dialog").DialogContentProps & import("react").RefAttributes<HTMLDivElement>, "ref"> & import("class-variance-authority").VariantProps<(props?: ({
5
- side?: "left" | "right" | "top" | "bottom" | null | undefined;
5
+ side?: "top" | "right" | "bottom" | "left" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string> & import("react").RefAttributes<HTMLDivElement>>;
7
7
  Description: import("react").ForwardRefExoticComponent<Omit<import("@radix-ui/react-dialog").DialogDescriptionProps & import("react").RefAttributes<HTMLParagraphElement>, "ref"> & import("react").RefAttributes<HTMLParagraphElement>>;
8
8
  Footer: ({ className, ...props }: import("react").HTMLAttributes<HTMLDivElement>) => import("react").JSX.Element;
@@ -2,10 +2,10 @@ import React from 'react';
2
2
  import { Content } from '@radix-ui/react-dialog';
3
3
  import { type VariantProps } from 'class-variance-authority';
4
4
  export declare const sheetVariants: (props?: ({
5
- side?: "left" | "right" | "top" | "bottom" | null | undefined;
5
+ side?: "top" | "right" | "bottom" | "left" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export type SheetContentProps = React.ComponentPropsWithoutRef<typeof Content> & VariantProps<typeof sheetVariants>;
8
8
  export declare const SheetContent: React.ForwardRefExoticComponent<Omit<import("@radix-ui/react-dialog").DialogContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & VariantProps<(props?: ({
9
- side?: "left" | "right" | "top" | "bottom" | null | undefined;
9
+ side?: "top" | "right" | "bottom" | "left" | null | undefined;
10
10
  } & import("class-variance-authority/types").ClassProp) | undefined) => string> & React.RefAttributes<HTMLDivElement>>;
11
11
  //# sourceMappingURL=SheetContent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"TableCell.d.ts","sourceRoot":"","sources":["../../../src/components/Table/TableCell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,eAAO,MAAM,SAAS,2HAIrB,CAAC"}
1
+ {"version":3,"file":"TableCell.d.ts","sourceRoot":"","sources":["../../../src/components/Table/TableCell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,eAAO,MAAM,SAAS,2HAMrB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
2
  import { cn } from '../../utils.js';
3
3
  export const TableCell = React.forwardRef(function TableCell({ className, ...props }, ref) {
4
- return React.createElement("td", { className: cn('p-4 align-middle [&:has([role=checkbox])]:pr-0', className), ref: ref, ...props });
4
+ return (React.createElement("td", { className: cn('px-6 py-3 align-middle [&:has([role=checkbox])]:pr-0', className), ref: ref, ...props }));
5
5
  });
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
2
  import { cn } from '../../utils.js';
3
3
  export const TableHead = React.forwardRef(function TableHead({ className, ...props }, ref) {
4
- return (React.createElement("th", { className: cn('h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0', className), ref: ref, ...props }));
4
+ return (React.createElement("th", { className: cn('px-6 py-3 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0', className), ref: ref, ...props }));
5
5
  });
@@ -1,4 +1,5 @@
1
1
  export * from './components/Accordion/Accordion.js';
2
+ export * from './components/ActionDropdown/ActionDropdown.js';
2
3
  export * from './components/AlertDialog/AlertDialog.js';
3
4
  export * from './components/ArrowToggle/ArrowToggle.js';
4
5
  export * from './components/Avatar/Avatar.js';
@@ -7,6 +8,7 @@ export * from './components/Breadcrumb/Breadcrumb.js';
7
8
  export * from './components/Button/Button.js';
8
9
  export * from './components/Card/Card.js';
9
10
  export * from './components/Checkbox/Checkbox.js';
11
+ export * from './components/ClientTable/ClientTable.js';
10
12
  export * from './components/Collapsible/Collapsible.js';
11
13
  export * from './components/Command/Command.js';
12
14
  export * from './components/ContextMenu/ContextMenu.js';
@@ -23,14 +25,11 @@ export * from './components/HoverCard/HoverCard.js';
23
25
  export * from './components/Input/Input.js';
24
26
  export * from './components/Label/Label.js';
25
27
  export * from './components/LanguageToggle/LanguageToggle.js';
26
- export * from './components/LegacyDropdown/LegacyDropdown.js';
27
28
  export * from './components/LegacyModal/LegacyModal.js';
28
- export * from './components/LegacySelectDropdown/LegacySelectDropdown.js';
29
29
  export * from './components/LegacySlider/LegacySlider.js';
30
30
  export * from './components/LegacyStepper/LegacyStepper.js';
31
- export * from './components/LegacyTable/LegacyClientTable.js';
32
- export * from './components/LegacyTable/LegacyTable.js';
33
31
  export * from './components/LineGraph/LineGraph.js';
32
+ export * from './components/ListboxDropdown/ListboxDropdown.js';
34
33
  export * from './components/MenuBar/MenuBar.js';
35
34
  export * from './components/NotificationHub/NotificationHub.js';
36
35
  export * from './components/Pagination/Pagination.js';
@@ -1 +1 @@
1
- {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+CAA+C,CAAC;AAC9D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+CAA+C,CAAC;AAC9D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yCAAyC,CAAC;AACxD,cAAc,2DAA2D,CAAC;AAC1E,cAAc,2CAA2C,CAAC;AAC1D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yCAAyC,CAAC;AACxD,cAAc,qCAAqC,CAAC;AACpD,cAAc,iCAAiC,CAAC;AAChD,cAAc,iDAAiD,CAAC;AAChE,cAAc,uCAAuC,CAAC;AACtD,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,iCAAiC,CAAC"}
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAAA,cAAc,qCAAqC,CAAC;AACpD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,yCAAyC,CAAC;AACxD,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,uCAAuC,CAAC;AACtD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,+CAA+C,CAAC;AAC9D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+CAA+C,CAAC;AAC9D,cAAc,yCAAyC,CAAC;AACxD,cAAc,2CAA2C,CAAC;AAC1D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,qCAAqC,CAAC;AACpD,cAAc,iDAAiD,CAAC;AAChE,cAAc,iCAAiC,CAAC;AAChD,cAAc,iDAAiD,CAAC;AAChE,cAAc,uCAAuC,CAAC;AACtD,cAAc,iCAAiC,CAAC;AAChD,cAAc,mCAAmC,CAAC;AAClD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,uCAAuC,CAAC;AACtD,cAAc,qCAAqC,CAAC;AACpD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,qCAAqC,CAAC;AACpD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,yCAAyC,CAAC;AACxD,cAAc,+BAA+B,CAAC;AAC9C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,mCAAmC,CAAC;AAClD,cAAc,yCAAyC,CAAC;AACxD,cAAc,iCAAiC,CAAC"}
@@ -1,4 +1,5 @@
1
1
  export * from './components/Accordion/Accordion.js';
2
+ export * from './components/ActionDropdown/ActionDropdown.js';
2
3
  export * from './components/AlertDialog/AlertDialog.js';
3
4
  export * from './components/ArrowToggle/ArrowToggle.js';
4
5
  export * from './components/Avatar/Avatar.js';
@@ -7,6 +8,7 @@ export * from './components/Breadcrumb/Breadcrumb.js';
7
8
  export * from './components/Button/Button.js';
8
9
  export * from './components/Card/Card.js';
9
10
  export * from './components/Checkbox/Checkbox.js';
11
+ export * from './components/ClientTable/ClientTable.js';
10
12
  export * from './components/Collapsible/Collapsible.js';
11
13
  export * from './components/Command/Command.js';
12
14
  export * from './components/ContextMenu/ContextMenu.js';
@@ -23,14 +25,11 @@ export * from './components/HoverCard/HoverCard.js';
23
25
  export * from './components/Input/Input.js';
24
26
  export * from './components/Label/Label.js';
25
27
  export * from './components/LanguageToggle/LanguageToggle.js';
26
- export * from './components/LegacyDropdown/LegacyDropdown.js';
27
28
  export * from './components/LegacyModal/LegacyModal.js';
28
- export * from './components/LegacySelectDropdown/LegacySelectDropdown.js';
29
29
  export * from './components/LegacySlider/LegacySlider.js';
30
30
  export * from './components/LegacyStepper/LegacyStepper.js';
31
- export * from './components/LegacyTable/LegacyClientTable.js';
32
- export * from './components/LegacyTable/LegacyTable.js';
33
31
  export * from './components/LineGraph/LineGraph.js';
32
+ export * from './components/ListboxDropdown/ListboxDropdown.js';
34
33
  export * from './components/MenuBar/MenuBar.js';
35
34
  export * from './components/NotificationHub/NotificationHub.js';
36
35
  export * from './components/Pagination/Pagination.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@douglasneuroinformatics/libui",
3
3
  "type": "module",
4
- "version": "2.2.4",
4
+ "version": "2.3.1",
5
5
  "packageManager": "pnpm@8.15.3",
6
6
  "description": "Generic UI components for DNP projects, built using React and TailwindCSS",
7
7
  "author": {
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+
3
+ import { DropdownButton } from '../DropdownButton/DropdownButton.js';
4
+ import { DropdownMenu } from '../DropdownMenu/DropdownMenu.js';
5
+
6
+ import type { DropdownMenuContentProps } from '../DropdownMenu/DropdownMenuContent.js';
7
+
8
+ type ActionDropdownOptions = { [key: string]: string } | readonly string[];
9
+
10
+ type ActionDropdownOptionKey<T> = T extends readonly string[]
11
+ ? T[number]
12
+ : T extends { [key: string]: string }
13
+ ? Extract<keyof T, string>
14
+ : never;
15
+
16
+ export type ActionDropdownProps<T extends ActionDropdownOptions> = {
17
+ align?: DropdownMenuContentProps['align'];
18
+
19
+ /** Callback function invoked when user clicks an option */
20
+ onSelection: (option: ActionDropdownOptionKey<T>) => void;
21
+
22
+ /** Either a list of options for the dropdown, or an object with options mapped to custom labels */
23
+ options: T;
24
+
25
+ /** The text content for the dropdown toggle */
26
+ title: string;
27
+ };
28
+
29
+ // eslint-disable-next-line react/function-component-definition
30
+ export function ActionDropdown<const T extends ActionDropdownOptions>({
31
+ align = 'start',
32
+ onSelection,
33
+ options,
34
+ title
35
+ }: ActionDropdownProps<T>) {
36
+ const optionKeys: readonly string[] = options instanceof Array ? options : Object.keys(options);
37
+ return (
38
+ <DropdownMenu>
39
+ <div className="w-full">
40
+ <DropdownMenu.Trigger asChild>
41
+ <DropdownButton>{title}</DropdownButton>
42
+ </DropdownMenu.Trigger>
43
+ <DropdownMenu.Content align={align} style={{ width: 'var(--radix-popper-anchor-width)' }}>
44
+ <DropdownMenu.Group>
45
+ {optionKeys.map((option) => (
46
+ <DropdownMenu.Item
47
+ key={option}
48
+ onClick={() => {
49
+ onSelection(option as ActionDropdownOptionKey<T>);
50
+ }}
51
+ >
52
+ {Array.isArray(options) ? option : (options[option as keyof T] as string)}
53
+ </DropdownMenu.Item>
54
+ ))}
55
+ </DropdownMenu.Group>
56
+ </DropdownMenu.Content>
57
+ </div>
58
+ </DropdownMenu>
59
+ );
60
+ }
@@ -0,0 +1,165 @@
1
+ import React, { useState } from 'react';
2
+
3
+ import { toBasicISOString } from '@douglasneuroinformatics/libjs';
4
+ import { range } from 'lodash-es';
5
+ import { ChevronDownIcon } from 'lucide-react';
6
+
7
+ import { cn } from '../../utils.js';
8
+ import { DropdownMenu } from '../DropdownMenu/DropdownMenu.js';
9
+ import { Table } from '../Table/Table.js';
10
+ import { ClientTablePagination } from './ClientTablePagination.js';
11
+
12
+ /** Coerces the value in a cell to a string for consistant display purposes */
13
+ function defaultFormatter(value: unknown): string {
14
+ if (typeof value === 'string') {
15
+ return value;
16
+ } else if (typeof value === 'number') {
17
+ return value.toFixed(2).toString();
18
+ } else if (typeof value === 'undefined') {
19
+ return 'NA';
20
+ }
21
+ if (value instanceof Date) {
22
+ return toBasicISOString(value);
23
+ }
24
+ return JSON.stringify(value);
25
+ }
26
+
27
+ export type ClientTableEntry = { [key: string]: unknown };
28
+
29
+ export type ClientFieldFactory<T extends ClientTableEntry = ClientTableEntry> = (entry: T) => string;
30
+
31
+ export type ClientTableColumn<T extends ClientTableEntry> = {
32
+ /** How to determine the values for column */
33
+ field: ClientFieldFactory<T> | keyof T;
34
+
35
+ /** Override the default formatter for this field */
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ formatter?: (value: any) => string;
38
+
39
+ /** The label to be displayed on the header */
40
+ label: string;
41
+ };
42
+
43
+ export type ClientTableDropdownOptions<T extends ClientTableEntry> = {
44
+ icon?: React.ComponentType<Omit<React.SVGProps<SVGSVGElement>, 'ref'>>;
45
+ label: string;
46
+ onSelection: (column: ClientTableColumn<T>) => void;
47
+ }[];
48
+
49
+ export type ClientTableColumnProps<T extends ClientTableEntry> = {
50
+ column: ClientTableColumn<T>;
51
+ dropdownOptions?: ClientTableDropdownOptions<T>;
52
+ };
53
+
54
+ export type ClientTableProps<T extends ClientTableEntry> = {
55
+ className?: string;
56
+ columnDropdownOptions?: ClientTableDropdownOptions<T>;
57
+ columns: ClientTableColumn<T>[];
58
+ data: T[];
59
+ entriesPerPage?: number;
60
+ minRows?: number;
61
+ onEntryClick?: (entry: T) => void;
62
+ };
63
+
64
+ export const ClientTable = <T extends ClientTableEntry>({
65
+ className,
66
+ columnDropdownOptions,
67
+ columns,
68
+ data,
69
+ entriesPerPage = 10,
70
+ minRows,
71
+ onEntryClick
72
+ }: ClientTableProps<T>) => {
73
+ const [currentPage, setCurrentPage] = useState(1);
74
+
75
+ const pageCount = Math.max(Math.ceil(data.length / entriesPerPage), 1);
76
+
77
+ const firstEntry = data.length === 0 ? 0 : (currentPage - 1) * entriesPerPage + 1;
78
+ const lastEntry = Math.min(firstEntry + entriesPerPage - 1, data.length);
79
+ const currentEntries = data.slice(firstEntry - 1, lastEntry);
80
+ const nRows = Math.max(currentEntries.length, minRows ?? -1);
81
+
82
+ return (
83
+ <div className={className}>
84
+ <div className="rounded-md border bg-card tracking-tight text-muted-foreground shadow">
85
+ <Table>
86
+ <Table.Header>
87
+ <Table.Row>
88
+ {columns.map((column, i) => (
89
+ <Table.Head className="whitespace-nowrap text-foreground" key={i}>
90
+ {columnDropdownOptions ? (
91
+ <DropdownMenu>
92
+ <DropdownMenu.Trigger className="flex items-center justify-between gap-3">
93
+ <span>{column.label}</span>
94
+ <ChevronDownIcon />
95
+ </DropdownMenu.Trigger>
96
+ <DropdownMenu.Content align="start">
97
+ <DropdownMenu.Group>
98
+ {columnDropdownOptions.map((option) => {
99
+ const Icon = option.icon;
100
+ return (
101
+ <DropdownMenu.Item
102
+ key={option.label}
103
+ onClick={() => {
104
+ option.onSelection(column);
105
+ }}
106
+ >
107
+ {Icon && <Icon className="mr-2" height={16} width={16} />}
108
+ {option.label}
109
+ </DropdownMenu.Item>
110
+ );
111
+ })}
112
+ </DropdownMenu.Group>
113
+ </DropdownMenu.Content>
114
+ </DropdownMenu>
115
+ ) : (
116
+ column.label
117
+ )}
118
+ </Table.Head>
119
+ ))}
120
+ </Table.Row>
121
+ </Table.Header>
122
+ <Table.Body>
123
+ {range(nRows).map((i) => {
124
+ const entry = currentEntries[i];
125
+ return (
126
+ <Table.Row
127
+ className={cn(typeof onEntryClick === 'function' && 'cursor-pointer hover:backdrop-brightness-95')}
128
+ key={i}
129
+ onClick={() => {
130
+ onEntryClick?.(entry);
131
+ }}
132
+ >
133
+ {columns.map(({ field, formatter }, j) => {
134
+ let value: unknown;
135
+ if (!entry) {
136
+ value = 'NA';
137
+ } else if (typeof field === 'function') {
138
+ value = field(entry);
139
+ } else {
140
+ value = entry[field];
141
+ }
142
+ const formattedValue = entry && formatter ? formatter(value) : defaultFormatter(value);
143
+ return (
144
+ <Table.Cell className={cn('text-ellipsis leading-none', !entry && 'invisible')} key={j}>
145
+ {formattedValue}
146
+ </Table.Cell>
147
+ );
148
+ })}
149
+ </Table.Row>
150
+ );
151
+ })}
152
+ </Table.Body>
153
+ </Table>
154
+ </div>
155
+ <ClientTablePagination
156
+ currentPage={currentPage}
157
+ firstEntry={firstEntry}
158
+ lastEntry={lastEntry}
159
+ pageCount={pageCount}
160
+ setCurrentPage={setCurrentPage}
161
+ totalEntries={data.length}
162
+ />
163
+ </div>
164
+ );
165
+ };