@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.
- package/.idea/bigweld.iml +12 -0
- package/.idea/codeStyles/Project.xml +72 -0
- package/.idea/codeStyles/codeStyleConfig.xml +5 -0
- package/.idea/inspectionProfiles/Project_Default.xml +30 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/prettier.xml +7 -0
- package/.idea/reason.xml +6 -0
- package/.idea/vcs.xml +6 -0
- package/.prettierrc.json +13 -0
- package/README.md +35 -0
- package/app/client-table.tsx +35 -0
- package/app/favicon.ico +0 -0
- package/app/layout.tsx +39 -0
- package/app/page.tsx +72 -0
- package/dist/components/core/button.d.ts +21 -0
- package/dist/components/core/button.d.ts.map +1 -0
- package/dist/components/core/polymorph.d.ts +10 -0
- package/dist/components/core/polymorph.d.ts.map +1 -0
- package/dist/components/display/card.d.ts +4 -0
- package/dist/components/display/card.d.ts.map +1 -0
- package/dist/components/floating/dropdown.d.ts +11 -0
- package/dist/components/floating/dropdown.d.ts.map +1 -0
- package/dist/components/floating/tooltip.d.ts +9 -0
- package/dist/components/floating/tooltip.d.ts.map +1 -0
- package/dist/components/form/autocomplete.d.ts +16 -0
- package/dist/components/form/autocomplete.d.ts.map +1 -0
- package/dist/components/form/file-upload.d.ts +12 -0
- package/dist/components/form/file-upload.d.ts.map +1 -0
- package/dist/components/form/form.d.ts +4 -0
- package/dist/components/form/form.d.ts.map +1 -0
- package/dist/components/form/input-field.d.ts +25 -0
- package/dist/components/form/input-field.d.ts.map +1 -0
- package/dist/components/form/input.d.ts +9 -0
- package/dist/components/form/input.d.ts.map +1 -0
- package/dist/components/form/select.d.ts +11 -0
- package/dist/components/form/select.d.ts.map +1 -0
- package/dist/components/form/switch.d.ts +7 -0
- package/dist/components/form/switch.d.ts.map +1 -0
- package/dist/components/index.d.ts +15 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/table/filter.d.ts +70 -0
- package/dist/components/table/filter.d.ts.map +1 -0
- package/dist/components/table/group.d.ts +17 -0
- package/dist/components/table/group.d.ts.map +1 -0
- package/dist/components/table/index.d.ts +28 -0
- package/dist/components/table/index.d.ts.map +1 -0
- package/dist/components/table/metadata.d.ts +3 -0
- package/dist/components/table/metadata.d.ts.map +1 -0
- package/dist/components/table/sort.d.ts +28 -0
- package/dist/components/table/sort.d.ts.map +1 -0
- package/dist/components/table/table-lib.d.ts +99 -0
- package/dist/components/table/table-lib.d.ts.map +1 -0
- package/dist/components/table/thead.d.ts +7 -0
- package/dist/components/table/thead.d.ts.map +1 -0
- package/dist/hooks/use-form.d.ts +28 -0
- package/dist/hooks/use-form.d.ts.map +1 -0
- package/dist/hooks/use-previous.d.ts +2 -0
- package/dist/hooks/use-previous.d.ts.map +1 -0
- package/dist/hooks/use-reactive.d.ts +2 -0
- package/dist/hooks/use-reactive.d.ts.map +1 -0
- package/dist/index.css +1670 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +21864 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.js +151 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/lib/dom.d.ts +6 -0
- package/dist/lib/dom.d.ts.map +1 -0
- package/dist/lib/fns.d.ts +5 -0
- package/dist/lib/fns.d.ts.map +1 -0
- package/dist/next.svg +1 -0
- package/dist/styles/design-tokens.d.ts +26 -0
- package/dist/styles/design-tokens.d.ts.map +1 -0
- package/dist/tailwind.config.d.ts +32 -0
- package/dist/tailwind.config.d.ts.map +1 -0
- package/dist/tailwind.config.js +153 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/vercel.svg +1 -0
- package/docs/README.md +36 -0
- package/docs/next.config.mjs +4 -0
- package/docs/package.json +28 -0
- package/docs/pnpm-lock.yaml +1030 -0
- package/docs/postcss.config.mjs +8 -0
- package/docs/public/next.svg +1 -0
- package/docs/public/vercel.svg +1 -0
- package/docs/src/app/favicon.ico +0 -0
- package/docs/src/app/globals.css +33 -0
- package/docs/src/app/layout.tsx +22 -0
- package/docs/src/app/page.tsx +10 -0
- package/docs/tailwind.config.ts +15 -0
- package/docs/tsconfig.json +26 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +4 -0
- package/package.json +72 -0
- package/postcss.config.mjs +8 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/src/components/core/button.tsx +91 -0
- package/src/components/core/polymorph.tsx +17 -0
- package/src/components/display/card.tsx +8 -0
- package/src/components/floating/dropdown.tsx +93 -0
- package/src/components/floating/tooltip.tsx +67 -0
- package/src/components/form/autocomplete.tsx +222 -0
- package/src/components/form/file-upload.tsx +129 -0
- package/src/components/form/form.tsx +28 -0
- package/src/components/form/input-field.tsx +105 -0
- package/src/components/form/input.tsx +73 -0
- package/src/components/form/select.tsx +58 -0
- package/src/components/form/switch.tsx +40 -0
- package/src/components/index.ts +14 -0
- package/src/components/table/filter.tsx +186 -0
- package/src/components/table/group.tsx +123 -0
- package/src/components/table/index.tsx +207 -0
- package/src/components/table/metadata.tsx +55 -0
- package/src/components/table/sort.tsx +141 -0
- package/src/components/table/table-lib.ts +130 -0
- package/src/components/table/thead.tsx +108 -0
- package/src/hooks/use-form.ts +155 -0
- package/src/hooks/use-previous.ts +9 -0
- package/src/hooks/use-reactive.ts +10 -0
- package/src/index.css +37 -0
- package/src/index.ts +6 -0
- package/src/lib/dom.ts +27 -0
- package/src/lib/fns.ts +23 -0
- package/src/styles/dark.json +66 -0
- package/src/styles/design-tokens.ts +57 -0
- package/src/styles/light.json +49 -0
- package/src/types.ts +11 -0
- package/styles.config.ts +42 -0
- package/tailwind.config.ts +11 -0
- package/tsconfig.json +55 -0
- package/tsconfig.lib.json +50 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/tsconfig.tailwind.json +32 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vite.config.mts +39 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { ChevronDownIcon, ChevronUpIcon, PlusIcon, SortAscIcon, Trash2Icon } from "lucide-react";
|
|
3
|
+
import React, { Fragment, useEffect, useState } from "react";
|
|
4
|
+
import { Dropdown } from "~/components/floating/dropdown";
|
|
5
|
+
import { OptionProps, Select } from "~/components/form/select";
|
|
6
|
+
import { uuid } from "~/lib/fns";
|
|
7
|
+
import { Label } from "~/types";
|
|
8
|
+
import { Col, TableConfiguration, TableOperationProps } from "./table-lib";
|
|
9
|
+
|
|
10
|
+
type Keyof<T extends {}> = keyof T extends infer R extends string ? R : never;
|
|
11
|
+
|
|
12
|
+
enum Order {
|
|
13
|
+
Asc = "asc",
|
|
14
|
+
Desc = "desc",
|
|
15
|
+
Undefined = "undefined",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type Sorter<T extends {}> = { value: Keyof<T>; type: Order; label: Label; id: string };
|
|
19
|
+
|
|
20
|
+
const createSorterFn =
|
|
21
|
+
<T extends {}>(fields: Sorter<T>[]) =>
|
|
22
|
+
(a: any, b: any) =>
|
|
23
|
+
fields.reduce<number>((acc, x) => {
|
|
24
|
+
const reverse = x.type === "desc" ? -1 : 1;
|
|
25
|
+
const property = x.value;
|
|
26
|
+
const p = a[property] > b[property] ? reverse : a[property] < b[property] ? -reverse : 0;
|
|
27
|
+
return acc !== 0 ? acc : p;
|
|
28
|
+
}, 0);
|
|
29
|
+
|
|
30
|
+
export const multiSort = <T extends {}>(array: T[], fields: Sorter<T>[]) => array.sort(createSorterFn(fields));
|
|
31
|
+
|
|
32
|
+
const orders = {
|
|
33
|
+
asc: { label: "Ascending", value: Order.Asc },
|
|
34
|
+
desc: { label: "Descending", value: Order.Desc },
|
|
35
|
+
} satisfies Omit<Record<Order, OptionProps>, Order.Undefined>;
|
|
36
|
+
|
|
37
|
+
const orderOptions: OptionProps[] = [orders.asc, orders.desc];
|
|
38
|
+
|
|
39
|
+
type Props<T extends {}> = TableConfiguration<
|
|
40
|
+
T,
|
|
41
|
+
{
|
|
42
|
+
cols: Col<T>[];
|
|
43
|
+
sorters: Sorter<T>[];
|
|
44
|
+
set: React.Dispatch<React.SetStateAction<Sorter<T>[]>>;
|
|
45
|
+
}
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
const createSorter = <T extends {}>(col: Col<T>, order: Order = Order.Asc): Sorter<T> => ({
|
|
49
|
+
id: uuid(),
|
|
50
|
+
type: order,
|
|
51
|
+
value: col.id as any,
|
|
52
|
+
label: orders[Order.Asc].label,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const Sort = <T extends {}>(props: Props<T>) => {
|
|
56
|
+
const onAddSorter = () => {
|
|
57
|
+
const col = props.cols[0];
|
|
58
|
+
if (col) props.set((prev) => [...prev, createSorter(col)]);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const onSetSorter = (id: string) => (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
62
|
+
const value = e.target.value;
|
|
63
|
+
props.set((prev) => prev.map((x) => (x.id === id ? { ...x, value: value as Keyof<T> } : x)));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const onSortOrderType = (id: string) => (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
67
|
+
const type = e.target.value;
|
|
68
|
+
props.set((prev) => prev.map((x) => (x.id === id ? { ...x, type: type as Order } : x)));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const onDelete = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
72
|
+
const id = e.currentTarget.dataset.id || "";
|
|
73
|
+
props.set((prev) => prev.filter((x) => x.id !== id));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Fragment>
|
|
78
|
+
<Dropdown
|
|
79
|
+
arrow={false}
|
|
80
|
+
title="Order By"
|
|
81
|
+
trigger={
|
|
82
|
+
<span className="flex items-center gap-1 proportional-nums text-foreground-description">
|
|
83
|
+
<SortAscIcon size={14} />
|
|
84
|
+
Order by {props.sorters.length === 0 ? "" : ` (${props.sorters.length})`}
|
|
85
|
+
</span>
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
<ul className="mt-4 space-y-2">
|
|
89
|
+
{props.sorters.map((sorter) => {
|
|
90
|
+
return (
|
|
91
|
+
<li key={`sorter-select-${sorter.id}`} className="flex flex-nowrap gap-3">
|
|
92
|
+
<Select
|
|
93
|
+
onChange={onSetSorter(sorter.id)}
|
|
94
|
+
options={props.options}
|
|
95
|
+
placeholder="Selecione um campo..."
|
|
96
|
+
value={sorter.value as string}
|
|
97
|
+
/>
|
|
98
|
+
<Select onChange={onSortOrderType(sorter.id)} value={sorter.type} options={orderOptions} placeholder="Operação..." />
|
|
99
|
+
<button className="mt-4" data-id={sorter.id} onClick={onDelete}>
|
|
100
|
+
<Trash2Icon className="text-danger" size={14} />
|
|
101
|
+
</button>
|
|
102
|
+
</li>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
<li>
|
|
106
|
+
<button onClick={onAddSorter} className="text-primary flex items-center gap-1">
|
|
107
|
+
<PlusIcon size={14} /> Adicionar ordenação
|
|
108
|
+
</button>
|
|
109
|
+
</li>
|
|
110
|
+
</ul>
|
|
111
|
+
</Dropdown>
|
|
112
|
+
</Fragment>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type SorterHeadProps<T extends {}> = Pick<TableOperationProps<T>, "sorters" | "setSorters"> & { col: Col<T> };
|
|
117
|
+
|
|
118
|
+
export const SorterHead = <T extends {}>(props: SorterHeadProps<T>) => {
|
|
119
|
+
const sorter = props.sorters.find((sort) => sort.id === props.col.id);
|
|
120
|
+
const [status, setStatus] = useState(sorter ? sorter.type : Order.Undefined);
|
|
121
|
+
|
|
122
|
+
const onClick = () => setStatus((prev) => (prev === Order.Undefined ? Order.Asc : prev === Order.Asc ? Order.Desc : Order.Undefined));
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
props.setSorters((prev) => {
|
|
126
|
+
if (status === Order.Undefined) return prev.filter((x) => (x.value as string) !== props.col.id);
|
|
127
|
+
const findIndex = prev.findIndex((p) => (p.value as string) === props.col.id);
|
|
128
|
+
if (findIndex === -1) return [...prev, createSorter(props.col, status)];
|
|
129
|
+
prev[findIndex] = createSorter(props.col, status);
|
|
130
|
+
return [...prev];
|
|
131
|
+
});
|
|
132
|
+
}, [status, props.col]);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<button className="isolate flex items-center" onClick={onClick}>
|
|
136
|
+
{status === Order.Asc ? <ChevronDownIcon size={14} /> : null}
|
|
137
|
+
{status === Order.Desc ? <ChevronUpIcon size={14} /> : null}
|
|
138
|
+
{status === Order.Undefined ? <SortAscIcon size={14} /> : null}
|
|
139
|
+
</button>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { LocalStorage } from "storage-manager-js";
|
|
4
|
+
import { useReducer } from "use-typed-reducer";
|
|
5
|
+
import { OptionProps } from "~/components/form/select";
|
|
6
|
+
import { isSsr } from "~/lib/fns";
|
|
7
|
+
import { POJO, SetState } from "~/types";
|
|
8
|
+
import { FilterConfig } from "./filter";
|
|
9
|
+
import { GroupItem } from "./group";
|
|
10
|
+
import { Sorter } from "./sort";
|
|
11
|
+
|
|
12
|
+
export const getLabel = <T extends {}>(col: Col<T>) => col.headerLabel ?? col.thead ?? (col.id as string);
|
|
13
|
+
|
|
14
|
+
export type TableConfiguration<T extends {}, M extends {} = {}> = M & {
|
|
15
|
+
cols: Col<T>[];
|
|
16
|
+
options: OptionProps[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const createOptionCols = <T extends {}>(cols: Col<T>[]): OptionProps[] =>
|
|
20
|
+
cols.map((opt) => ({
|
|
21
|
+
value: opt.id as string,
|
|
22
|
+
label: (opt.thead ?? opt.headerLabel ?? (opt.id as string)) as string,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
export enum ColType {
|
|
26
|
+
Boolean = "boolean",
|
|
27
|
+
Number = "number",
|
|
28
|
+
Select = "select",
|
|
29
|
+
Text = "text",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const valueFromType = (input: HTMLInputElement) => (input.type === "number" ? input.valueAsNumber : input.value);
|
|
33
|
+
|
|
34
|
+
type THead = React.ReactElement | React.ReactNode;
|
|
35
|
+
|
|
36
|
+
export type ColMatrix = `${number},${number}`;
|
|
37
|
+
|
|
38
|
+
export type CellPropsElement<T extends {}, K extends keyof T> = {
|
|
39
|
+
row: T;
|
|
40
|
+
value: T[K];
|
|
41
|
+
rowIndex: number;
|
|
42
|
+
matrix: ColMatrix;
|
|
43
|
+
col: ColOptions<T, K> & { id: K; thead: THead };
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type ColOptions<T extends {}, K extends keyof T> = Partial<{
|
|
47
|
+
cellProps: React.HTMLAttributes<HTMLTableCellElement>;
|
|
48
|
+
thProps: React.HTMLAttributes<HTMLTableCellElement>;
|
|
49
|
+
type: ColType;
|
|
50
|
+
headerLabel: string;
|
|
51
|
+
allowFilter: boolean;
|
|
52
|
+
Element: (props: CellPropsElement<T, K>) => React.ReactNode;
|
|
53
|
+
}>;
|
|
54
|
+
|
|
55
|
+
export type ColConstructor<T extends {}> = {
|
|
56
|
+
remove: <K extends keyof T>(id: K) => void;
|
|
57
|
+
add: <K extends keyof T>(id: K, thead: THead, props?: ColOptions<T, K>) => void;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const cols =
|
|
61
|
+
<T extends POJO>() =>
|
|
62
|
+
<K extends keyof T>(id: K, thead: THead, options: ColOptions<T, K>) => ({ ...options, id, thead });
|
|
63
|
+
|
|
64
|
+
export type Col<T extends {}> = ReturnType<ReturnType<typeof cols<T>>>;
|
|
65
|
+
|
|
66
|
+
type TableGetters<T extends POJO> = {
|
|
67
|
+
rows: T[];
|
|
68
|
+
cols: Col<T>[];
|
|
69
|
+
groups: GroupItem<T>[];
|
|
70
|
+
sorters: Sorter<T>[];
|
|
71
|
+
filters: FilterConfig<T>[];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type TableSetters<T extends POJO> = {
|
|
75
|
+
setCols: SetState<Col<T>[]>;
|
|
76
|
+
setSorters: SetState<Sorter<T>[]>;
|
|
77
|
+
setGroups: SetState<GroupItem<T>[]>;
|
|
78
|
+
setFilters: SetState<FilterConfig<T>[]>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type TableOperationProps<T extends {}> = TableConfiguration<
|
|
82
|
+
T,
|
|
83
|
+
TableSetters<T> &
|
|
84
|
+
TableGetters<T> & {
|
|
85
|
+
set?: (v: TableGetters<T>) => void;
|
|
86
|
+
}
|
|
87
|
+
>;
|
|
88
|
+
|
|
89
|
+
export const createColumns = <T extends {}>(callback: (o: ColConstructor<T>) => void) => {
|
|
90
|
+
let items: Col<T>[] = [];
|
|
91
|
+
const add: ColConstructor<T>["add"] = (id, thead, options) => items.push({ ...options, id, thead } as any);
|
|
92
|
+
const remove: ColConstructor<T>["remove"] = (id) => (items = items.filter((x) => x.id !== id));
|
|
93
|
+
callback({ add, remove });
|
|
94
|
+
return items;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type TablePreferenceState<T extends POJO> = {
|
|
98
|
+
name: string;
|
|
99
|
+
cols: Col<T>[];
|
|
100
|
+
groups: GroupItem<T>[];
|
|
101
|
+
sorters: Sorter<T>[];
|
|
102
|
+
filters: FilterConfig<T>[];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const noop = {};
|
|
106
|
+
|
|
107
|
+
export const useTablePreferences = <T extends POJO>(name: string, options: Partial<TableGetters<T>> = noop) => {
|
|
108
|
+
const init: TableGetters<T> | null = isSsr() ? null : (LocalStorage.get(`@unamed/table-${name}`) as TableGetters<T>) || null;
|
|
109
|
+
const [state, dispatch] = useReducer(
|
|
110
|
+
{
|
|
111
|
+
name,
|
|
112
|
+
groups: options.groups || init?.groups || [],
|
|
113
|
+
sorters: options.sorters || init?.sorters || [],
|
|
114
|
+
filters: options.filters || init?.filters || [],
|
|
115
|
+
cols: options.cols || init?.cols || [],
|
|
116
|
+
} as Omit<TableGetters<T>, "rows"> & { name: string },
|
|
117
|
+
(get) => {
|
|
118
|
+
const intercept = (partial: Partial<TablePreferenceState<T>>) => {
|
|
119
|
+
const prev = get.state();
|
|
120
|
+
const result = { ...prev, ...partial };
|
|
121
|
+
if (!isSsr()) LocalStorage.set(`@unamed/table-${prev.name}`, result);
|
|
122
|
+
return result;
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
set: (getters: TableGetters<T>) => intercept(getters),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
return { ...state, ...dispatch, name };
|
|
130
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { AnimatePresence, Reorder, TargetAndTransition } from "framer-motion";
|
|
2
|
+
import { PlusIcon, SearchIcon } from "lucide-react";
|
|
3
|
+
import { Dropdown } from "~/components/floating/dropdown";
|
|
4
|
+
import { useReactive } from "~/hooks/use-reactive";
|
|
5
|
+
import { ColumnHeaderFilter, createFilterFromCol } from "./filter";
|
|
6
|
+
import { SorterHead } from "./sort";
|
|
7
|
+
import { Col, getLabel, TableOperationProps } from "./table-lib";
|
|
8
|
+
|
|
9
|
+
type TableHeaderProps<T extends {}> = {
|
|
10
|
+
headers: Col<T>[];
|
|
11
|
+
} & Pick<TableOperationProps<T>, "filters" | "setFilters" | "setCols" | "setSorters" | "sorters">;
|
|
12
|
+
|
|
13
|
+
const targetTransitionAnimate: TargetAndTransition = { opacity: 1 };
|
|
14
|
+
|
|
15
|
+
const whileDrag: TargetAndTransition = { opacity: 0.75, backgroundColor: "#a1a1aa" };
|
|
16
|
+
|
|
17
|
+
const exit: TargetAndTransition = { opacity: 0, transition: { duration: 0.4, type: "spring" } };
|
|
18
|
+
|
|
19
|
+
type HeaderChildProps<T extends {}> = {
|
|
20
|
+
header: Col<T>;
|
|
21
|
+
} & Pick<TableOperationProps<T>, "filters" | "setFilters" | "sorters" | "setSorters">;
|
|
22
|
+
|
|
23
|
+
const HeaderChild = <T extends {}>(props: HeaderChildProps<T>) => {
|
|
24
|
+
const ownFilters = props.filters.filter((x) => x.name === props.header.id);
|
|
25
|
+
return (
|
|
26
|
+
<Reorder.Item
|
|
27
|
+
{...(props.header.thProps as {})}
|
|
28
|
+
as="th"
|
|
29
|
+
exit={exit}
|
|
30
|
+
initial={false}
|
|
31
|
+
dragSnapToOrigin
|
|
32
|
+
dragDirectionLock
|
|
33
|
+
value={props.header}
|
|
34
|
+
whileDrag={whileDrag}
|
|
35
|
+
animate={targetTransitionAnimate}
|
|
36
|
+
className={`hidden px-2 py-4 first:table-cell md:table-cell ${props.header.thProps?.className ?? ""}`}
|
|
37
|
+
>
|
|
38
|
+
<span className="flex items-center justify-between">
|
|
39
|
+
<span className="flex items-center gap-1">
|
|
40
|
+
<Dropdown
|
|
41
|
+
arrow
|
|
42
|
+
trigger={<SearchIcon size={14} />}
|
|
43
|
+
onChange={(opened) => {
|
|
44
|
+
if (!opened) return;
|
|
45
|
+
props.setFilters((prev) => prev.concat(createFilterFromCol(props.header, {})));
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
Filter by: {getLabel(props.header)}
|
|
49
|
+
{(ownFilters.length === 0) === null ? null : (
|
|
50
|
+
<ul>
|
|
51
|
+
{ownFilters.map((filter) => (
|
|
52
|
+
<li key={`thead-filter-${filter.id}`} className="my-1">
|
|
53
|
+
<ColumnHeaderFilter filter={filter} set={props.setFilters} />
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
<li>
|
|
57
|
+
<button
|
|
58
|
+
onClick={() => props.setFilters((prev) => prev.concat(createFilterFromCol(props.header)))}
|
|
59
|
+
className="text-primary-muted flex items-center"
|
|
60
|
+
>
|
|
61
|
+
<PlusIcon size={14} /> Add
|
|
62
|
+
</button>
|
|
63
|
+
</li>
|
|
64
|
+
</ul>
|
|
65
|
+
)}
|
|
66
|
+
</Dropdown>
|
|
67
|
+
<span className="pointer-events-auto text-balance text-base">{props.header.thead}</span>
|
|
68
|
+
<SorterHead col={props.header} setSorters={props.setSorters} sorters={props.sorters} />
|
|
69
|
+
</span>
|
|
70
|
+
</span>
|
|
71
|
+
</Reorder.Item>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const TableHeader = <T extends {}>(props: TableHeaderProps<T>) => {
|
|
76
|
+
const [headers, setHeaders] = useReactive<Col<T>[]>(props.headers);
|
|
77
|
+
|
|
78
|
+
const onPointerUp = () => props.setCols(headers);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Reorder.Group
|
|
82
|
+
as="tr"
|
|
83
|
+
axis="x"
|
|
84
|
+
drag
|
|
85
|
+
layout
|
|
86
|
+
layoutRoot
|
|
87
|
+
layoutScroll
|
|
88
|
+
initial={false}
|
|
89
|
+
values={headers}
|
|
90
|
+
onReorder={setHeaders}
|
|
91
|
+
onPointerUp={onPointerUp}
|
|
92
|
+
className="bg-table-background border-none text-lg"
|
|
93
|
+
>
|
|
94
|
+
<AnimatePresence>
|
|
95
|
+
{headers.map((header) => (
|
|
96
|
+
<HeaderChild<T>
|
|
97
|
+
key={`header-child-item-${header.id as string}`}
|
|
98
|
+
setFilters={props.setFilters}
|
|
99
|
+
filters={props.filters}
|
|
100
|
+
setSorters={props.setSorters}
|
|
101
|
+
sorters={props.sorters}
|
|
102
|
+
header={header}
|
|
103
|
+
/>
|
|
104
|
+
))}
|
|
105
|
+
</AnimatePresence>
|
|
106
|
+
</Reorder.Group>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { parse } from "qs";
|
|
2
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { AllPaths, Is, setPath } from "sidekicker";
|
|
4
|
+
import { z, ZodArray, ZodNumber } from "zod";
|
|
5
|
+
import { formReset } from "~/components/form/form";
|
|
6
|
+
import { InputProps } from "~/components/form/input";
|
|
7
|
+
import { SelectProps } from "~/components/form/select";
|
|
8
|
+
|
|
9
|
+
const sort = (a: string, b: string) => a.localeCompare(b);
|
|
10
|
+
|
|
11
|
+
const options = {
|
|
12
|
+
sort,
|
|
13
|
+
allowDots: true,
|
|
14
|
+
charset: "utf-8",
|
|
15
|
+
parseArrays: true,
|
|
16
|
+
plainObjects: true,
|
|
17
|
+
charsetSentinel: true,
|
|
18
|
+
allowPrototypes: false,
|
|
19
|
+
depth: Number.MAX_SAFE_INTEGER,
|
|
20
|
+
arrayLimit: Number.MAX_SAFE_INTEGER,
|
|
21
|
+
parameterLimit: Number.MAX_SAFE_INTEGER,
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
export const formToJson = (form: HTMLFormElement): any => {
|
|
25
|
+
const formData = new FormData(form);
|
|
26
|
+
const urlSearchParams = new URLSearchParams(formData as any);
|
|
27
|
+
return parse(urlSearchParams.toString(), options) as never;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const convertPath = (path: string) => path.replace("[", ".").replace("]", "").split(".");
|
|
31
|
+
|
|
32
|
+
export const getSchemaShape = <T extends z.ZodObject<any>>(name: string, schema: T) =>
|
|
33
|
+
convertPath(name).reduce((acc, el) => {
|
|
34
|
+
if (el === "") return acc;
|
|
35
|
+
const shape = acc.shape?.[el] || acc;
|
|
36
|
+
return shape instanceof ZodArray ? shape.element : shape;
|
|
37
|
+
}, schema);
|
|
38
|
+
|
|
39
|
+
const getValueByType = (e: HTMLInputElement) => {
|
|
40
|
+
if (e.type === "checkbox") return e.checked;
|
|
41
|
+
if (e.type === "number") return e.valueAsNumber;
|
|
42
|
+
return e.value;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type CustomOnInvalid = (args: { form: HTMLFormElement; errors: Record<string, string> }) => any;
|
|
46
|
+
|
|
47
|
+
type CustomOnSubmit<T> = (args: { json: T; form: HTMLFormElement; reset: () => void; event: React.FormEvent<HTMLFormElement> }) => any;
|
|
48
|
+
|
|
49
|
+
export const useForm = <T extends z.ZodObject<any>>(schema: T) => {
|
|
50
|
+
const [errors, setErrors] = useState<Record<string, string | undefined> | null>(null);
|
|
51
|
+
const ref = useRef<Record<string, { element: HTMLElement; schema: z.ZodType }>>({});
|
|
52
|
+
|
|
53
|
+
const select = <Props extends SelectProps>(name: AllPaths<z.infer<T>>, props?: Props): Props => {
|
|
54
|
+
const validator = getSchemaShape(name, schema);
|
|
55
|
+
return {
|
|
56
|
+
...props,
|
|
57
|
+
name,
|
|
58
|
+
id: name,
|
|
59
|
+
error: errors?.[name],
|
|
60
|
+
ref: (e: HTMLSelectElement) => {
|
|
61
|
+
if (e === null) return;
|
|
62
|
+
ref.current[name] = { element: e, schema: validator };
|
|
63
|
+
},
|
|
64
|
+
} as any;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const input = <Props extends InputProps>(name: AllPaths<z.infer<T>>, props?: Props): Props => {
|
|
68
|
+
const validator = getSchemaShape(name, schema);
|
|
69
|
+
return {
|
|
70
|
+
...props,
|
|
71
|
+
name,
|
|
72
|
+
id: name,
|
|
73
|
+
type: Is.instance(validator, ZodNumber) ? "number" : props?.type ?? "text",
|
|
74
|
+
error: errors?.[name],
|
|
75
|
+
ref: (e: HTMLInputElement) => {
|
|
76
|
+
if (e === null) return;
|
|
77
|
+
ref.current[name] = { element: e, schema: validator };
|
|
78
|
+
},
|
|
79
|
+
} as any;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const events = Object.values(ref.current).map((input) => {
|
|
84
|
+
const validation = input.schema.safeParse(getValueByType(input.element as any));
|
|
85
|
+
const onBlurField = (e: any) => {
|
|
86
|
+
const validation = input.schema.safeParse(getValueByType(e.target));
|
|
87
|
+
const html = input.element as HTMLInputElement;
|
|
88
|
+
const name = html.name;
|
|
89
|
+
if (validation.success) {
|
|
90
|
+
html.setCustomValidity("");
|
|
91
|
+
return setErrors((prev) => {
|
|
92
|
+
const { [name]: removed, ...rest } = prev || {};
|
|
93
|
+
return rest === null || Is.empty(rest) ? null : rest;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (html.required) {
|
|
97
|
+
const errorMessage = validation.error.issues[0].message;
|
|
98
|
+
html.setCustomValidity(errorMessage);
|
|
99
|
+
setErrors((prev) => ({ ...prev, [name]: errorMessage }));
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const event = input.element.tagName === "INPUT" ? "blur" : "change";
|
|
103
|
+
input.element.addEventListener(event, onBlurField);
|
|
104
|
+
return {
|
|
105
|
+
input,
|
|
106
|
+
hasInitialError: (input.element as HTMLInputElement).required ? !validation.success : false,
|
|
107
|
+
unsubscribe: () => input.element.removeEventListener(event, onBlurField),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
const hasErrors = events.some((x) => x.hasInitialError);
|
|
111
|
+
if (hasErrors) setErrors((prev) => (prev === null ? {} : prev));
|
|
112
|
+
return () => events.forEach((item) => item.unsubscribe());
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const onInvalid = useCallback(
|
|
116
|
+
(exec?: CustomOnInvalid) => (event: React.FormEvent<HTMLFormElement>) => {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
const form = event.currentTarget;
|
|
119
|
+
const validationErrors = Object.values(ref.current).reduce((acc, input) => {
|
|
120
|
+
const field = input.element as HTMLInputElement;
|
|
121
|
+
const validation = input.schema.safeParse(getValueByType(field));
|
|
122
|
+
if (validation.success) return acc;
|
|
123
|
+
const errorMessage = validation.error.issues[0].message;
|
|
124
|
+
field.setAttribute("data-initialized", "true");
|
|
125
|
+
return { ...acc, [field.name]: errorMessage };
|
|
126
|
+
}, {});
|
|
127
|
+
const e = Is.empty(validationErrors) ? null : {};
|
|
128
|
+
setErrors(e);
|
|
129
|
+
exec?.({ form, errors: e || {} });
|
|
130
|
+
},
|
|
131
|
+
[]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const onSubmit = useCallback(
|
|
135
|
+
(exec: CustomOnSubmit<z.infer<T>>) => (event: React.FormEvent<HTMLFormElement>) => {
|
|
136
|
+
event.preventDefault();
|
|
137
|
+
const form = event.currentTarget;
|
|
138
|
+
let json = formToJson(form);
|
|
139
|
+
Array.from(form.elements).forEach((field) => {
|
|
140
|
+
if (field.tagName === "SELECT") {
|
|
141
|
+
const input = field as HTMLSelectElement;
|
|
142
|
+
json = setPath<any>(json as any, input.name, input.value);
|
|
143
|
+
}
|
|
144
|
+
if (field.tagName === "INPUT") {
|
|
145
|
+
const input = field as HTMLInputElement;
|
|
146
|
+
json = setPath<any>(json as any, input.name, getValueByType(input));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
exec({ form, json, event, reset: () => formReset(form) });
|
|
150
|
+
},
|
|
151
|
+
[]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
return { input, select, onSubmit, errors, onInvalid, disabled: errors !== null };
|
|
155
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
export const useReactive = <T extends unknown>(t: T, initial?: T) => {
|
|
4
|
+
const [state, setState] = useState(() => (initial ? initial : t));
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
setState(t);
|
|
7
|
+
}, [t]);
|
|
8
|
+
return [state, setState] as const;
|
|
9
|
+
};
|
|
10
|
+
|
package/src/index.css
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
select.select {
|
|
6
|
+
@apply appearance-none pr-10 bg-no-repeat;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
input::-webkit-outer-spin-button,
|
|
10
|
+
input::-webkit-inner-spin-button {
|
|
11
|
+
-webkit-appearance: none;
|
|
12
|
+
margin: 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
input[type="number"] {
|
|
16
|
+
-moz-appearance: textfield;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
input[type="color"],
|
|
20
|
+
input[type="date"],
|
|
21
|
+
input[type="datetime"],
|
|
22
|
+
input[type="datetime-local"],
|
|
23
|
+
input[type="email"],
|
|
24
|
+
input[type="month"],
|
|
25
|
+
input[type="number"],
|
|
26
|
+
input[type="password"],
|
|
27
|
+
input[type="search"],
|
|
28
|
+
input[type="tel"],
|
|
29
|
+
input[type="text"],
|
|
30
|
+
input[type="time"],
|
|
31
|
+
input[type="url"],
|
|
32
|
+
input[type="week"],
|
|
33
|
+
select,
|
|
34
|
+
select:focus,
|
|
35
|
+
textarea {
|
|
36
|
+
@apply text-base;
|
|
37
|
+
}
|
package/src/index.ts
ADDED
package/src/lib/dom.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ClassValue, clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
import React, { LegacyRef, MutableRefObject, RefCallback } from "react";
|
|
4
|
+
|
|
5
|
+
export const mergeRefs =
|
|
6
|
+
<T extends any = any>(...refs: Array<MutableRefObject<T> | LegacyRef<T> | undefined | null>): RefCallback<T> =>
|
|
7
|
+
(value) => {
|
|
8
|
+
refs.forEach((ref) => {
|
|
9
|
+
if (typeof ref === "function") {
|
|
10
|
+
ref(value);
|
|
11
|
+
} else if (ref !== null) {
|
|
12
|
+
(ref as MutableRefObject<T | null>).current = value;
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const isReactComponent = (a: any): a is React.FC => {
|
|
18
|
+
if (a.$$typeof === Symbol.for("react.forward_ref")) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (a.$$typeof === Symbol.for("react.fragment")) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return a.$$typeof === Symbol.for("react.element");
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const css = (...styles: ClassValue[]) => twMerge(clsx(styles));
|
package/src/lib/fns.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AllPaths } from "sidekicker";
|
|
2
|
+
|
|
3
|
+
export const uuid = (): string =>
|
|
4
|
+
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
5
|
+
let r = (Math.random() * 16) | 0;
|
|
6
|
+
let v = c == "x" ? r : (r & 0x3) | 0x8;
|
|
7
|
+
return v.toString(16);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const travel = (path: string, regexp: RegExp, obj: any) =>
|
|
11
|
+
path
|
|
12
|
+
.split(regexp)
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.reduce((res, key) => (res !== null && res !== undefined ? (res as any)[key] : res), obj);
|
|
15
|
+
|
|
16
|
+
const regexPaths = { basic: /[,[\]]+?/, extend: /[,[\].]+?/ };
|
|
17
|
+
|
|
18
|
+
export const path = <T extends {}, K extends AllPaths<T>>(obj: T, path: K) => {
|
|
19
|
+
const result = travel(path, regexPaths.basic, obj) || travel(path, regexPaths.extend, obj);
|
|
20
|
+
return result === undefined || result === obj ? undefined : result;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const isSsr = () => typeof window === 'undefined';
|