@g4rcez/components 0.0.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 (139) hide show
  1. package/.idea/bigweld.iml +12 -0
  2. package/.idea/codeStyles/Project.xml +72 -0
  3. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  4. package/.idea/inspectionProfiles/Project_Default.xml +30 -0
  5. package/.idea/jsLibraryMappings.xml +6 -0
  6. package/.idea/modules.xml +8 -0
  7. package/.idea/prettier.xml +7 -0
  8. package/.idea/reason.xml +6 -0
  9. package/.idea/vcs.xml +6 -0
  10. package/.prettierrc.json +13 -0
  11. package/README.md +35 -0
  12. package/app/client-table.tsx +35 -0
  13. package/app/favicon.ico +0 -0
  14. package/app/layout.tsx +39 -0
  15. package/app/page.tsx +72 -0
  16. package/dist/components/core/button.d.ts +21 -0
  17. package/dist/components/core/button.d.ts.map +1 -0
  18. package/dist/components/core/polymorph.d.ts +10 -0
  19. package/dist/components/core/polymorph.d.ts.map +1 -0
  20. package/dist/components/display/card.d.ts +4 -0
  21. package/dist/components/display/card.d.ts.map +1 -0
  22. package/dist/components/floating/dropdown.d.ts +11 -0
  23. package/dist/components/floating/dropdown.d.ts.map +1 -0
  24. package/dist/components/floating/tooltip.d.ts +9 -0
  25. package/dist/components/floating/tooltip.d.ts.map +1 -0
  26. package/dist/components/form/autocomplete.d.ts +16 -0
  27. package/dist/components/form/autocomplete.d.ts.map +1 -0
  28. package/dist/components/form/file-upload.d.ts +12 -0
  29. package/dist/components/form/file-upload.d.ts.map +1 -0
  30. package/dist/components/form/form.d.ts +4 -0
  31. package/dist/components/form/form.d.ts.map +1 -0
  32. package/dist/components/form/input-field.d.ts +25 -0
  33. package/dist/components/form/input-field.d.ts.map +1 -0
  34. package/dist/components/form/input.d.ts +9 -0
  35. package/dist/components/form/input.d.ts.map +1 -0
  36. package/dist/components/form/select.d.ts +11 -0
  37. package/dist/components/form/select.d.ts.map +1 -0
  38. package/dist/components/form/switch.d.ts +7 -0
  39. package/dist/components/form/switch.d.ts.map +1 -0
  40. package/dist/components/index.d.ts +15 -0
  41. package/dist/components/index.d.ts.map +1 -0
  42. package/dist/components/table/filter.d.ts +70 -0
  43. package/dist/components/table/filter.d.ts.map +1 -0
  44. package/dist/components/table/group.d.ts +17 -0
  45. package/dist/components/table/group.d.ts.map +1 -0
  46. package/dist/components/table/index.d.ts +28 -0
  47. package/dist/components/table/index.d.ts.map +1 -0
  48. package/dist/components/table/metadata.d.ts +3 -0
  49. package/dist/components/table/metadata.d.ts.map +1 -0
  50. package/dist/components/table/sort.d.ts +28 -0
  51. package/dist/components/table/sort.d.ts.map +1 -0
  52. package/dist/components/table/table-lib.d.ts +99 -0
  53. package/dist/components/table/table-lib.d.ts.map +1 -0
  54. package/dist/components/table/thead.d.ts +7 -0
  55. package/dist/components/table/thead.d.ts.map +1 -0
  56. package/dist/hooks/use-form.d.ts +28 -0
  57. package/dist/hooks/use-form.d.ts.map +1 -0
  58. package/dist/hooks/use-previous.d.ts +2 -0
  59. package/dist/hooks/use-previous.d.ts.map +1 -0
  60. package/dist/hooks/use-reactive.d.ts +2 -0
  61. package/dist/hooks/use-reactive.d.ts.map +1 -0
  62. package/dist/index.css +1670 -0
  63. package/dist/index.d.ts +7 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.mjs +21864 -0
  66. package/dist/index.mjs.map +1 -0
  67. package/dist/index.umd.js +151 -0
  68. package/dist/index.umd.js.map +1 -0
  69. package/dist/lib/dom.d.ts +6 -0
  70. package/dist/lib/dom.d.ts.map +1 -0
  71. package/dist/lib/fns.d.ts +5 -0
  72. package/dist/lib/fns.d.ts.map +1 -0
  73. package/dist/next.svg +1 -0
  74. package/dist/styles/design-tokens.d.ts +26 -0
  75. package/dist/styles/design-tokens.d.ts.map +1 -0
  76. package/dist/tailwind.config.d.ts +32 -0
  77. package/dist/tailwind.config.d.ts.map +1 -0
  78. package/dist/tailwind.config.js +153 -0
  79. package/dist/types.d.ts +9 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/vercel.svg +1 -0
  82. package/docs/README.md +36 -0
  83. package/docs/next.config.mjs +4 -0
  84. package/docs/package.json +28 -0
  85. package/docs/pnpm-lock.yaml +1030 -0
  86. package/docs/postcss.config.mjs +8 -0
  87. package/docs/public/next.svg +1 -0
  88. package/docs/public/vercel.svg +1 -0
  89. package/docs/src/app/favicon.ico +0 -0
  90. package/docs/src/app/globals.css +33 -0
  91. package/docs/src/app/layout.tsx +22 -0
  92. package/docs/src/app/page.tsx +10 -0
  93. package/docs/tailwind.config.ts +15 -0
  94. package/docs/tsconfig.json +26 -0
  95. package/next-env.d.ts +5 -0
  96. package/next.config.mjs +4 -0
  97. package/package.json +72 -0
  98. package/postcss.config.mjs +8 -0
  99. package/public/next.svg +1 -0
  100. package/public/vercel.svg +1 -0
  101. package/src/components/core/button.tsx +91 -0
  102. package/src/components/core/polymorph.tsx +17 -0
  103. package/src/components/display/card.tsx +8 -0
  104. package/src/components/floating/dropdown.tsx +93 -0
  105. package/src/components/floating/tooltip.tsx +67 -0
  106. package/src/components/form/autocomplete.tsx +222 -0
  107. package/src/components/form/file-upload.tsx +129 -0
  108. package/src/components/form/form.tsx +28 -0
  109. package/src/components/form/input-field.tsx +105 -0
  110. package/src/components/form/input.tsx +73 -0
  111. package/src/components/form/select.tsx +58 -0
  112. package/src/components/form/switch.tsx +40 -0
  113. package/src/components/index.ts +14 -0
  114. package/src/components/table/filter.tsx +186 -0
  115. package/src/components/table/group.tsx +123 -0
  116. package/src/components/table/index.tsx +207 -0
  117. package/src/components/table/metadata.tsx +55 -0
  118. package/src/components/table/sort.tsx +141 -0
  119. package/src/components/table/table-lib.ts +130 -0
  120. package/src/components/table/thead.tsx +108 -0
  121. package/src/hooks/use-form.ts +155 -0
  122. package/src/hooks/use-previous.ts +9 -0
  123. package/src/hooks/use-reactive.ts +10 -0
  124. package/src/index.css +37 -0
  125. package/src/index.ts +6 -0
  126. package/src/lib/dom.ts +27 -0
  127. package/src/lib/fns.ts +23 -0
  128. package/src/styles/dark.json +66 -0
  129. package/src/styles/design-tokens.ts +57 -0
  130. package/src/styles/light.json +49 -0
  131. package/src/types.ts +11 -0
  132. package/styles.config.ts +42 -0
  133. package/tailwind.config.ts +11 -0
  134. package/tsconfig.json +55 -0
  135. package/tsconfig.lib.json +50 -0
  136. package/tsconfig.lib.tsbuildinfo +1 -0
  137. package/tsconfig.tailwind.json +32 -0
  138. package/tsconfig.tsbuildinfo +1 -0
  139. package/vite.config.mts +39 -0
@@ -0,0 +1,186 @@
1
+ import { Symbols } from "linq-arrays";
2
+ import { PlusIcon, SearchIcon, Trash2Icon } from "lucide-react";
3
+ import React, { Fragment } from "react";
4
+ import { Dropdown } from "~/components/floating/dropdown";
5
+ import { Input } from "~/components/form/input";
6
+ import { OptionProps, Select } from "~/components/form/select";
7
+ import { uuid } from "~/lib/fns";
8
+ import { Label } from "~/types";
9
+ import { Col, ColType, getLabel, TableConfiguration, valueFromType } from "./table-lib";
10
+
11
+ const operators = {
12
+ contains: { value: "contains", label: "Contains", symbol: "includes" },
13
+ is: { value: "is", label: "Is", symbol: "is" },
14
+ isNot: { value: "isNot", label: "Is not", symbol: "!==" },
15
+ notContains: { value: "notContains", label: "Does not contains", symbol: "notIncludes" },
16
+ lessThan: { value: "lessThan", label: "Less Than", symbol: "<=" },
17
+ greaterThan: { value: "greaterThan", label: "Greater than", symbol: ">=" },
18
+ startsWith: { value: "startsWith", label: "Starts with", symbol: "startsWith" },
19
+ endsWith: { value: "endsWith", label: "Ends with", symbol: "endsWith" },
20
+ } satisfies Record<string, OptionProps & { symbol: Symbols }>;
21
+
22
+ type Operator = keyof typeof operators;
23
+
24
+ type Operators = (typeof operators)[Operator];
25
+
26
+ const operatorOptions: Partial<Record<ColType, OptionProps[]>> = {
27
+ [ColType.Text]: [operators.is, operators.isNot, operators.contains, operators.notContains, operators.startsWith, operators.endsWith],
28
+ [ColType.Boolean]: [operators.is, operators.isNot],
29
+ [ColType.Number]: [operators.is, operators.isNot, operators.greaterThan, operators.lessThan],
30
+ };
31
+
32
+ type FilterValue = string | number | string[] | boolean;
33
+
34
+ export type FilterConfig<T extends {} = {}> = {
35
+ id: string;
36
+ label: Label;
37
+ name: keyof T;
38
+ type: ColType;
39
+ operation: Operators;
40
+ value: FilterValue;
41
+ };
42
+
43
+ type Props<T extends {}> = TableConfiguration<
44
+ T,
45
+ {
46
+ cols: Col<T>[];
47
+ filters: FilterConfig<T>[];
48
+ set: React.Dispatch<React.SetStateAction<FilterConfig<T>[]>>;
49
+ }
50
+ >;
51
+
52
+ export const createFilterFromCol = <T extends {}>(f: Col<T>, rest: Partial<FilterConfig<T>> = {}): FilterConfig<T> => {
53
+ const name = f.id;
54
+ const type = f.type ?? ColType.Text;
55
+ const operatorId = operatorOptions[type]?.[0]!.value as Operator;
56
+ const operation = operators[operatorId];
57
+ return { id: uuid(), operation, label: getLabel(f), name, type, value: "", ...rest };
58
+ };
59
+
60
+ export const Filter = <T extends {}>(props: Props<T>) => {
61
+ const onAddFilter = () => {
62
+ const col = props.cols.at(0)!;
63
+ props.set((prev) => [...prev, createFilterFromCol(col)]);
64
+ };
65
+
66
+ const onSelectProperty = (e: React.ChangeEvent<HTMLSelectElement>) => {
67
+ const changedId = e.target.dataset.id || "";
68
+ const newId = e.target.value;
69
+ props.set((prev) =>
70
+ prev.map((x) => {
71
+ if (changedId !== x.id) return x;
72
+ const col = props.cols.find((x) => newId === x.id)!;
73
+ return createFilterFromCol(col, { value: "" });
74
+ })
75
+ );
76
+ };
77
+
78
+ const onSelectOperation = (e: React.ChangeEvent<HTMLSelectElement>) => {
79
+ const id = e.target.dataset.id || "";
80
+ const operator = e.target.value;
81
+ props.set((prev) => prev.map((x) => (x.id === id ? { ...x, operation: operators[operator as Operator] } : x)));
82
+ };
83
+
84
+ const onDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
85
+ const id = e.currentTarget.dataset.id || "";
86
+ console.log({ id, f: props.filters });
87
+ props.set((prev) => prev.filter((x) => x.id !== id));
88
+ };
89
+
90
+ const onChangeValue = (e: React.ChangeEvent<HTMLInputElement>) => {
91
+ const id = e.target.dataset.id || "";
92
+ const value = valueFromType(e.target);
93
+ props.set((prev) => prev.map((x) => (x.id === id ? { ...x, value } : x)));
94
+ };
95
+
96
+ return (
97
+ <Fragment>
98
+ <Dropdown
99
+ arrow={false}
100
+ title="Filters"
101
+ trigger={
102
+ <span className="flex items-center gap-1 proportional-nums">
103
+ <SearchIcon size={14} />
104
+ Filtros {props.filters.length === 0 ? "" : ` (${props.filters.length})`}
105
+ </span>
106
+ }
107
+ >
108
+ <ul className="mt-4 space-y-2">
109
+ {props.filters.map((filter) => {
110
+ const operators = operatorOptions[filter.type]!;
111
+ return (
112
+ <li key={`filter-select-${filter.id}`} className="flex flex-nowrap gap-3">
113
+ <Select
114
+ title="Filtro"
115
+ options={props.options}
116
+ placeholder="Seleciona um campo..."
117
+ value={filter.name as string}
118
+ data-id={filter.id}
119
+ onChange={onSelectProperty}
120
+ />
121
+ <Select
122
+ title="Tipo do filtro"
123
+ data-id={filter.id}
124
+ onChange={onSelectOperation}
125
+ value={filter.operation.value}
126
+ options={operators}
127
+ placeholder="Operação..."
128
+ />
129
+ <Input
130
+ data-id={filter.id}
131
+ onChange={onChangeValue}
132
+ placeholder="Buscar por..."
133
+ title="Valor do filtro"
134
+ type={filter.type as any}
135
+ value={filter.value as string}
136
+ />
137
+ <div className="flex items-center justify-center mt-5">
138
+ <button data-id={filter.id} type="button" onClick={onDelete}>
139
+ <Trash2Icon className="text-danger" size={16} />
140
+ </button>
141
+ </div>
142
+ </li>
143
+ );
144
+ })}
145
+ <li>
146
+ <button type="button" onClick={onAddFilter} className="text-primary flex items-center gap-1">
147
+ <PlusIcon size={14} /> Adicionar novo filtro
148
+ </button>
149
+ </li>
150
+ </ul>
151
+ </Dropdown>
152
+ </Fragment>
153
+ );
154
+ };
155
+
156
+ type ColumnHeaderFilterProps<T extends {}> = {
157
+ filter: FilterConfig<T>;
158
+ set: React.Dispatch<React.SetStateAction<FilterConfig<T>[]>>;
159
+ };
160
+
161
+ export const ColumnHeaderFilter = <T extends {}>({ filter, set }: ColumnHeaderFilterProps<T>) => {
162
+ const onSelectOperation = (e: React.ChangeEvent<HTMLSelectElement>) => {
163
+ const operator = e.target.value;
164
+ const id = e.target.dataset.id || "";
165
+ set((prev) => prev.map((x) => (x.id === id ? { ...x, operation: operators[operator as Operator] } : x)));
166
+ };
167
+
168
+ const onChangeValue = (e: React.ChangeEvent<HTMLInputElement>) => {
169
+ const id = e.target.dataset.id || "";
170
+ const value = valueFromType(e.target);
171
+ set((prev) => prev.map((x) => (x.id === id ? { ...x, value } : x)));
172
+ };
173
+
174
+ return (
175
+ <div className="flex flex-nowrap items-center gap-4 py-2">
176
+ <Select onChange={onSelectOperation} value={filter.operation.value} options={operatorOptions[filter.type]!} placeholder="Operation..." />
177
+ <Input
178
+ type={filter.type as any}
179
+ data-id={filter.id}
180
+ onChange={onChangeValue}
181
+ placeholder="Looking for..."
182
+ value={filter.value as string}
183
+ />
184
+ </div>
185
+ );
186
+ };
@@ -0,0 +1,123 @@
1
+ "use client";
2
+ import { LayoutGroup, Reorder, useDragControls, useMotionValue } from "framer-motion";
3
+ import Linq from "linq-arrays";
4
+ import { GripVerticalIcon, GroupIcon, Trash2Icon } from "lucide-react";
5
+ import React, { Fragment, useState } from "react";
6
+ import { keys } from "sidekicker";
7
+ import { Button } from "~/components/core/button";
8
+ import { Dropdown } from "~/components/floating/dropdown";
9
+ import { Select } from "~/components/form/select";
10
+ import { uuid } from "~/lib/fns";
11
+ import { Col, createOptionCols, TableConfiguration } from "./table-lib";
12
+
13
+ export type GroupItem<T extends {}> = Col<T> & {
14
+ rows: T[];
15
+ index: number;
16
+ groupId: string;
17
+ groupName: string;
18
+ groupKey: keyof T;
19
+ };
20
+
21
+ type Props<T extends {}> = TableConfiguration<
22
+ T,
23
+ {
24
+ rows: T[];
25
+ groups: GroupItem<T>[];
26
+ setGroups: React.Dispatch<React.SetStateAction<GroupItem<T>[]>>;
27
+ }
28
+ >;
29
+
30
+ const Item = ({ item, onPointerDown }: { item: GroupItem<any>; onPointerDown: any }) => {
31
+ const y = useMotionValue(0);
32
+ return (
33
+ <Reorder.Item
34
+ onPointerDown={onPointerDown}
35
+ id={item.groupId}
36
+ className="flex flex-row items-center gap-2"
37
+ key={item.groupId}
38
+ value={item}
39
+ style={{ y }}
40
+ >
41
+ <button className="cursor-grab">
42
+ <GripVerticalIcon size={14} />
43
+ </button>
44
+ <span>{item.groupName}</span>
45
+ </Reorder.Item>
46
+ );
47
+ };
48
+
49
+ export const Group = <T extends {}>(props: Props<T>) => {
50
+ const options = createOptionCols(props.cols);
51
+ const controls = useDragControls();
52
+ const [group, setGroup] = useState((props.groups[0]?.thead as string) || "");
53
+
54
+ const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
55
+ const select = e.target;
56
+ const key = select.value as keyof T;
57
+ const index = select.options.selectedIndex;
58
+ const label = select.options.item(index)?.label || "";
59
+ setGroup(label);
60
+ const groupBy = new Linq(props.rows).GroupBy(key);
61
+ const col = props.cols.find((x) => x.id === key)!;
62
+ props.setGroups(
63
+ keys(groupBy).map((groupName, index): GroupItem<T> => {
64
+ const rows = groupBy[groupName];
65
+ return { ...col, groupId: uuid(), groupKey: key, index, rows, groupName: groupName as string };
66
+ })
67
+ );
68
+ };
69
+
70
+ const onDelete = (e: React.MouseEvent<HTMLButtonElement>) => props.setGroups([]);
71
+
72
+ return (
73
+ <Fragment>
74
+ <Dropdown
75
+ arrow={false}
76
+ title="Groups"
77
+ trigger={
78
+ <span className="flex items-center gap-1 proportional-nums">
79
+ <GroupIcon size={14} />
80
+ Groups{props.groups.length > 0 ? ` - ${group}(${props.groups.length})` : ""}
81
+ </span>
82
+ }
83
+ >
84
+ <div className="flex flex-nowrap items-center">
85
+ <Select value={group} title="Tipo de agrupamento" onChange={onChange} options={options} placeholder="Agrupar por..." />
86
+ <Button className="mt-4" onClick={onDelete} theme="raw" data-id={group}>
87
+ <Trash2Icon size={16} className="text-danger" />
88
+ </Button>
89
+ </div>
90
+ {props.groups.length > 0 ? (
91
+ <section className="my-4">
92
+ <header>
93
+ <h2 className="text-xl font-medium">Ordenar grupos</h2>
94
+ </header>
95
+ <LayoutGroup>
96
+ <Reorder.Group
97
+ axis="y"
98
+ className="relative space-y-2"
99
+ dragListener={false}
100
+ dragControls={controls}
101
+ drag
102
+ layoutScroll
103
+ onReorder={props.setGroups}
104
+ values={props.groups}
105
+ >
106
+ {props.groups.map((item) => (
107
+ <Item
108
+ key={item.groupId}
109
+ item={item as any}
110
+ onPointerDown={(e: any) => {
111
+ controls.start(e);
112
+ props.setGroups([...props.groups]);
113
+ }}
114
+ />
115
+ ))}
116
+ </Reorder.Group>
117
+ </LayoutGroup>
118
+ </section>
119
+ ) : null}
120
+ </Dropdown>
121
+ </Fragment>
122
+ );
123
+ };
@@ -0,0 +1,207 @@
1
+ import { AnimatePresence, motion } from "framer-motion";
2
+ import Linq from "linq-arrays";
3
+ import React, { Fragment, HTMLAttributes, useMemo } from "react";
4
+ import { TableBodyProps, TableVirtuoso } from "react-virtuoso";
5
+ import { Is } from "sidekicker";
6
+ import { useReducer } from "use-typed-reducer";
7
+ import { OptionProps } from "~/components/form/select";
8
+ import { path } from "~/lib/fns";
9
+ import { FilterConfig } from "./filter";
10
+ import { GroupItem } from "./group";
11
+ import { Metadata } from "./metadata";
12
+ import { multiSort, Sorter } from "./sort";
13
+ import { CellPropsElement, Col, ColMatrix, createOptionCols, TableOperationProps } from "./table-lib";
14
+ import { TableHeader } from "./thead";
15
+
16
+ type InnerTableProps<T extends {}> = HTMLAttributes<HTMLTableElement> &
17
+ TableOperationProps<T> & {
18
+ loading?: boolean;
19
+ group?: GroupItem<T>;
20
+ groups?: GroupItem<T>[];
21
+ optionCols: OptionProps[];
22
+ index: number;
23
+ rows: T[];
24
+ cols: Col<T>[];
25
+ sorters?: Sorter<T>[];
26
+ showMetadata?: boolean;
27
+ filters?: FilterConfig<T>[];
28
+ setGroups: React.Dispatch<React.SetStateAction<GroupItem<T>[]>>;
29
+ };
30
+
31
+ const TableBody = React.forwardRef((props: TableBodyProps, ref: any) => (
32
+ <tbody {...props} className={`divide-y divide-table-row ${props.className ?? ""}`} ref={ref}>
33
+ <AnimatePresence>{props.children}</AnimatePresence>
34
+ </tbody>
35
+ ));
36
+
37
+ const VirtualTable = React.forwardRef((props: any, ref) => (
38
+ <table {...props} ref={ref as any} className={`table min-w-full divide-y divide-table-border table-auto text-left ${props.className ?? ""}`} />
39
+ ));
40
+
41
+ const Thead = React.forwardRef((props: any, ref: any) => <thead {...props} className="bg-content-bg shadow-xs group:sticky top-0" ref={ref} />);
42
+
43
+ const TRow = React.forwardRef((props: any, ref: any) => <tr {...props} ref={ref} className={`table-row ${props.className ?? ""}`} />);
44
+
45
+ const components = {
46
+ TableHead: Thead as any,
47
+ Table: VirtualTable as any,
48
+ TableBody: TableBody as any,
49
+ TableRow: TRow as any,
50
+ };
51
+
52
+ const loadingArray = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
53
+
54
+ const InnerTable = <T extends {}>({ filters, setCols, setFilters, sorters, cols, setSorters, ...props }: InnerTableProps<T>) => {
55
+ const rows = useMemo(() => {
56
+ if (props.loading) return loadingArray as any as T[];
57
+ const linq = new Linq(props.rows);
58
+ if (filters.length > 0) {
59
+ filters.forEach((x) => (x.value === "" || Number.isNaN(x.value) ? undefined : linq.Where(x.name, x.operation.symbol, x.value)));
60
+ }
61
+ if (sorters.length === 0) return linq.Select();
62
+ return multiSort(linq.Select(), sorters);
63
+ }, [props.rows, filters, sorters, props.loading]);
64
+
65
+ return (
66
+ <div className="group border border-table-border rounded-lg px-1 min-w-full">
67
+ <TableVirtuoso
68
+ data={rows}
69
+ totalCount={rows.length}
70
+ useWindowScroll
71
+ components={components}
72
+ fixedHeaderContent={() => (
73
+ <TableHeader<T>
74
+ sorters={sorters}
75
+ setSorters={setSorters}
76
+ filters={filters}
77
+ setFilters={setFilters}
78
+ headers={cols}
79
+ setCols={setCols}
80
+ />
81
+ )}
82
+ itemContent={(index, row) =>
83
+ cols.map((col, colIndex) => {
84
+ const matrix: ColMatrix = `${colIndex},${index}`;
85
+ const value: any = path(row, col.id as any);
86
+ const Component: React.FC<CellPropsElement<T, any>> = col.Element as any;
87
+ return (
88
+ <td
89
+ {...col.cellProps}
90
+ data-matrix={matrix}
91
+ key={`accessor-${index}-${colIndex}`}
92
+ className="px-2 h-14 border-none first:table-cell hidden md:table-cell"
93
+ >
94
+ {props.loading ? (
95
+ <div className="animate-pulse h-2 bg-table-border rounded" />
96
+ ) : Component ? (
97
+ <Component row={row} matrix={matrix} col={col} rowIndex={index} value={value} />
98
+ ) : (
99
+ <Fragment>{Is.nil(value) ? "" : value}</Fragment>
100
+ )}
101
+ </td>
102
+ );
103
+ })
104
+ }
105
+ />
106
+ </div>
107
+ );
108
+ };
109
+
110
+ export type TableProps<T extends {}> = Pick<InnerTableProps<T>, "cols" | "rows"> & {
111
+ name: string;
112
+ } & Partial<TableOperationProps<T> & { reference: keyof T; loading: boolean }>;
113
+
114
+ const dispatcherFun = <Prev extends any, T extends Prev | ((prev: Prev) => Prev)>(prev: Prev, setter: T) =>
115
+ typeof setter === "function" ? setter(prev) : setter;
116
+
117
+ type DispatcherFun<T extends any> = T | ((prev: T) => T);
118
+
119
+ export const Table = <T extends {}>(props: TableProps<T>) => {
120
+ const optionCols = useMemo(() => createOptionCols(props.cols), [props.cols]);
121
+ const [state, dispatch] = useReducer(
122
+ {
123
+ cols: props.cols as Col<T>[],
124
+ filters: (props.filters ?? []) as FilterConfig<T>[],
125
+ groups: (props.groups ?? []) as GroupItem<T>[],
126
+ sorters: (props.sorters ?? []) as Sorter<T>[],
127
+ },
128
+ (get) => {
129
+ const create =
130
+ <T extends any>(key: string) =>
131
+ (arg: T) => {
132
+ const state = get.state();
133
+ return { ...state, [key]: dispatcherFun(state[key as keyof typeof state], arg as any) };
134
+ };
135
+ return {
136
+ cols: create<DispatcherFun<Col<T>[]>>("cols"),
137
+ filters: create<DispatcherFun<FilterConfig<T>[]>>("filters"),
138
+ groups: create<DispatcherFun<GroupItem<T>[]>>("groups"),
139
+ sorters: create<DispatcherFun<Sorter<T>[]>>("sorters"),
140
+ };
141
+ },
142
+ {
143
+ interceptor: [
144
+ (state) => {
145
+ props.set?.(state as any);
146
+ return state;
147
+ },
148
+ ],
149
+ }
150
+ );
151
+
152
+ return (
153
+ <div className="relative min-w-full">
154
+ <Metadata
155
+ setCols={dispatch.cols}
156
+ rows={props.rows}
157
+ cols={state.cols}
158
+ filters={state.filters}
159
+ groups={state.groups}
160
+ options={optionCols}
161
+ setFilters={dispatch.filters}
162
+ setGroups={dispatch.groups}
163
+ setSorters={dispatch.sorters}
164
+ sorters={state.sorters}
165
+ />
166
+ {state.groups.length === 0 ? (
167
+ <InnerTable
168
+ {...props}
169
+ cols={state.cols}
170
+ filters={state.filters}
171
+ groups={state.groups}
172
+ index={0}
173
+ optionCols={optionCols}
174
+ options={optionCols}
175
+ setCols={dispatch.cols}
176
+ setFilters={dispatch.filters}
177
+ setGroups={dispatch.groups}
178
+ setSorters={dispatch.sorters}
179
+ sorters={state.sorters}
180
+ />
181
+ ) : (
182
+ <div className="flex flex-wrap gap-4">
183
+ {state.groups.map((group, index) => (
184
+ <motion.div className="min-w-full" key={`group-${group.groupId}`}>
185
+ <InnerTable
186
+ {...props}
187
+ cols={state.cols}
188
+ filters={state.filters}
189
+ group={group}
190
+ groups={state.groups}
191
+ index={index}
192
+ optionCols={optionCols}
193
+ options={optionCols}
194
+ rows={group.rows}
195
+ setCols={dispatch.cols}
196
+ setFilters={dispatch.filters}
197
+ setGroups={dispatch.groups}
198
+ setSorters={dispatch.sorters}
199
+ sorters={state.sorters}
200
+ />
201
+ </motion.div>
202
+ ))}
203
+ </div>
204
+ )}
205
+ </div>
206
+ );
207
+ };
@@ -0,0 +1,55 @@
1
+ import { Filter } from "./filter";
2
+ import { Group } from "./group";
3
+ import { Sort } from "./sort";
4
+ import { TableOperationProps, valueFromType } from "./table-lib";
5
+
6
+ export const Metadata = <T extends {}>(props: TableOperationProps<T>) => (
7
+ <header className="min-w-full mb-1">
8
+ <div className="flex flex-wrap min-w-full items-center justify-between gap-x-4 gap-y-1">
9
+ <div className="flex w-fit items-centeend gap-4 whitespace-nowrap py-2">
10
+ <p>
11
+ <Filter cols={props.cols} options={props.options} filters={props.filters} set={props.setFilters} />
12
+ </p>
13
+ <p>
14
+ <Sort options={props.options} cols={props.cols} sorters={props.sorters} set={props.setSorters} />
15
+ </p>
16
+ <p>
17
+ <Group rows={props.rows} groups={props.groups} setGroups={props.setGroups} options={props.options} cols={props.cols} />
18
+ </p>
19
+ </div>
20
+ <ul className="flex flex-wrap w-full flex-1 flex-grow flex-row items-center md:justify-end gap-4">
21
+ {props.filters.map((x) => (
22
+ <li key={`filter-table-${x.id}`} className="flex gap-1 items-center rounded-xl border border-card-border px-4 py-0.5">
23
+ <span>
24
+ <span className="size-3 mr-2 aspect-square bg-primary inline-block rounded-full" aria-hidden="true" />
25
+ {x.label} {x.operation.label.toLowerCase()}:
26
+ </span>
27
+ <div className="relative w-min min-w-[1ch]">
28
+ <span aria-hidden="true" className="invisible whitespace-pre p-0">
29
+ {x.value || " "}
30
+ </span>
31
+ <input
32
+ type={x.type}
33
+ value={x.value as string}
34
+ className="absolute left-0 top-0 m-0 inline-block w-full bg-transparent p-0 placeholder-primary/70 outline-none [appearance:textfield] after:empty:text-primary/70 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
35
+ onChange={(e) => {
36
+ const value = valueFromType(e.target);
37
+ props.setFilters((prev) =>
38
+ prev.map((item) =>
39
+ x.id === item.id
40
+ ? {
41
+ ...item,
42
+ value,
43
+ }
44
+ : item
45
+ )
46
+ );
47
+ }}
48
+ />
49
+ </div>
50
+ </li>
51
+ ))}
52
+ </ul>
53
+ </div>
54
+ </header>
55
+ );