gbs-add-block 1.2.7 → 1.2.8

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 (41) hide show
  1. package/README.md +3 -3
  2. package/index.cjs +9 -9
  3. package/package.json +1 -1
  4. package/source/components/datagridbeta/README.md +232 -0
  5. package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
  6. package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
  7. package/source/components/datagridbeta/core/columnHelper.ts +21 -0
  8. package/source/components/datagridbeta/core/columns.ts +288 -0
  9. package/source/components/datagridbeta/core/filtering.ts +237 -0
  10. package/source/components/datagridbeta/core/grid.ts +616 -0
  11. package/source/components/datagridbeta/core/index.ts +14 -0
  12. package/source/components/datagridbeta/core/rows.ts +64 -0
  13. package/source/components/datagridbeta/core/sorting.ts +111 -0
  14. package/source/components/datagridbeta/core/state.ts +82 -0
  15. package/source/components/datagridbeta/core/store.ts +52 -0
  16. package/source/components/datagridbeta/core/types.ts +266 -0
  17. package/source/components/datagridbeta/core/values.ts +98 -0
  18. package/source/components/datagridbeta/core/virtual.ts +85 -0
  19. package/source/components/datagridbeta/export/csv.ts +32 -0
  20. package/source/components/datagridbeta/export/download.ts +12 -0
  21. package/source/components/datagridbeta/export/pdf.ts +61 -0
  22. package/source/components/datagridbeta/export/table.ts +73 -0
  23. package/source/components/datagridbeta/export/xlsx.ts +145 -0
  24. package/source/components/datagridbeta/export/zip.ts +87 -0
  25. package/source/components/datagridbeta/index.ts +7 -0
  26. package/source/components/datagridbeta/react/Cell.tsx +282 -0
  27. package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
  28. package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
  29. package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
  30. package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
  31. package/source/components/datagridbeta/react/Pagination.tsx +107 -0
  32. package/source/components/datagridbeta/react/Popover.tsx +79 -0
  33. package/source/components/datagridbeta/react/Row.tsx +85 -0
  34. package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
  35. package/source/components/datagridbeta/react/Viewport.tsx +167 -0
  36. package/source/components/datagridbeta/react/context.ts +57 -0
  37. package/source/components/datagridbeta/react/hooks.ts +80 -0
  38. package/source/components/datagridbeta/react/icons.tsx +72 -0
  39. package/source/components/datagridbeta/react/keyboard.ts +110 -0
  40. package/source/components/datagridbeta/react/locale.ts +128 -0
  41. package/source/components/datagridbeta/styles.css +717 -0
@@ -0,0 +1,266 @@
1
+ "use client";
2
+
3
+ import { useMemo, type ReactNode } from "react";
4
+ import { isFilterActive } from "../core/filtering";
5
+ import type { ColumnFilter, Density, FilterValue, ResolvedColumn, RowId } from "../core/types";
6
+ import { toTime } from "../core/values";
7
+ import { cx, useGridContext } from "./context";
8
+ import { usePopoverState, useSelectionState } from "./hooks";
9
+ import { ColumnsIcon, DensityIcon, DownloadIcon, SearchIcon, XIcon } from "./icons";
10
+ import type { LocaleText } from "./locale";
11
+ import { Popover } from "./Popover";
12
+
13
+ export interface ToolbarOptions {
14
+ search?: boolean;
15
+ filterChips?: boolean;
16
+ columns?: boolean;
17
+ density?: boolean;
18
+ export?: boolean;
19
+ /** Rendered at the start of the toolbar. */
20
+ start?: ReactNode;
21
+ /** Rendered at the end of the toolbar. */
22
+ end?: ReactNode;
23
+ }
24
+
25
+ interface ToolbarProps<T> {
26
+ options: ToolbarOptions;
27
+ globalFilter: string;
28
+ filters: ColumnFilter[];
29
+ density: Density;
30
+ columns: ResolvedColumn<T>[];
31
+ columnVisibility: Record<string, boolean>;
32
+ }
33
+
34
+ export function Toolbar<T>({ options, globalFilter, filters, density, columns, columnVisibility }: ToolbarProps<T>) {
35
+ const { engine, locale, classNames, features } = useGridContext<T>();
36
+ const { api } = engine;
37
+ const activeFilters = filters.filter(isFilterActive);
38
+
39
+ const densities: [Density, string][] = [
40
+ ["compact", locale.densityCompact],
41
+ ["standard", locale.densityStandard],
42
+ ["comfortable", locale.densityComfortable],
43
+ ];
44
+
45
+ return (
46
+ <div className={cx("dg-toolbar", classNames.toolbar)}>
47
+ {options.start}
48
+
49
+ {options.search && (
50
+ <div className="dg-search">
51
+ <SearchIcon />
52
+ <input
53
+ type="search"
54
+ value={globalFilter}
55
+ placeholder={locale.searchPlaceholder}
56
+ aria-label={locale.search}
57
+ onChange={(event) => api.setGlobalFilter(event.target.value)}
58
+ />
59
+ </div>
60
+ )}
61
+
62
+ {options.filterChips && activeFilters.length > 0 && (
63
+ <div className="dg-chips">
64
+ {activeFilters.map((filter) => {
65
+ const column = columns.find((c) => c.id === filter.columnId);
66
+ if (!column) return null;
67
+ const label = describeFilter(filter, column, locale);
68
+ return (
69
+ <span key={filter.columnId} className="dg-chip">
70
+ {label}
71
+ <button
72
+ type="button"
73
+ className="dg-chip-remove"
74
+ aria-label={locale.removeFilter(label)}
75
+ onClick={() => api.setFilter(filter.columnId, null)}
76
+ >
77
+ <XIcon width={12} height={12} />
78
+ </button>
79
+ </span>
80
+ );
81
+ })}
82
+ <button type="button" className="dg-link-button" onClick={() => api.clearFilters()}>
83
+ {locale.clearFilters}
84
+ </button>
85
+ </div>
86
+ )}
87
+
88
+ <SelectionSummary />
89
+ <div className="dg-toolbar-spacer" />
90
+
91
+ {options.density && (
92
+ <ToolbarMenu label={locale.density} icon={<DensityIcon />}>
93
+ {(close) => (
94
+ <div className="dg-menu-section" role="radiogroup" aria-label={locale.density}>
95
+ {densities.map(([value, label]) => (
96
+ <button
97
+ key={value}
98
+ type="button"
99
+ role="radio"
100
+ aria-checked={density === value}
101
+ className="dg-menu-item"
102
+ onClick={() => {
103
+ api.setDensity(value);
104
+ close();
105
+ }}
106
+ >
107
+ {label}
108
+ </button>
109
+ ))}
110
+ </div>
111
+ )}
112
+ </ToolbarMenu>
113
+ )}
114
+
115
+ {options.columns && features.hiding && (
116
+ <ToolbarMenu label={locale.columns} icon={<ColumnsIcon />}>
117
+ {() => <ColumnVisibilityList columns={columns} columnVisibility={columnVisibility} />}
118
+ </ToolbarMenu>
119
+ )}
120
+
121
+ {options.export && (
122
+ <ToolbarMenu label={locale.export} icon={<DownloadIcon />}>
123
+ {(close) => (
124
+ <div className="dg-menu-section">
125
+ {(
126
+ [
127
+ [locale.exportCsv, () => api.exportCsv()],
128
+ [locale.exportExcel, () => api.exportExcel()],
129
+ [locale.exportPdf, () => api.exportPdf()],
130
+ ] as const
131
+ ).map(([label, action]) => (
132
+ <button
133
+ key={label}
134
+ type="button"
135
+ className="dg-menu-item"
136
+ onClick={() => {
137
+ close();
138
+ void action();
139
+ }}
140
+ >
141
+ {label}
142
+ </button>
143
+ ))}
144
+ </div>
145
+ )}
146
+ </ToolbarMenu>
147
+ )}
148
+
149
+ {options.end}
150
+ </div>
151
+ );
152
+ }
153
+
154
+ function ToolbarMenu({
155
+ label,
156
+ icon,
157
+ children,
158
+ }: {
159
+ label: string;
160
+ icon: ReactNode;
161
+ children: (close: () => void) => ReactNode;
162
+ }) {
163
+ const menu = usePopoverState();
164
+ const anchor = menu.state?.anchor ?? null;
165
+ const close = () => menu.close(anchor);
166
+
167
+ return (
168
+ <>
169
+ <button
170
+ type="button"
171
+ className="dg-button"
172
+ aria-haspopup="dialog"
173
+ aria-expanded={anchor !== null}
174
+ onClick={(event) => menu.open(event.currentTarget)}
175
+ >
176
+ {icon}
177
+ <span className="dg-button-label">{label}</span>
178
+ </button>
179
+ {anchor && (
180
+ <Popover anchor={anchor} onClose={close} label={label} align="end">
181
+ {children(close)}
182
+ </Popover>
183
+ )}
184
+ </>
185
+ );
186
+ }
187
+
188
+ function SelectionSummary<T>() {
189
+ const { engine, locale, features, numberFormat } = useGridContext<T>();
190
+ const selection = useSelectionState<T, Record<RowId, boolean>>((s) => s);
191
+ const count = useMemo(() => Object.values(selection).filter(Boolean).length, [selection]);
192
+
193
+ if (!features.selection || count === 0) return null;
194
+ return (
195
+ <div className="dg-selection" role="status">
196
+ {locale.selected(numberFormat.format(count))}
197
+ <button type="button" className="dg-link-button" onClick={() => engine.api.clearSelection()}>
198
+ {locale.clearSelection}
199
+ </button>
200
+ </div>
201
+ );
202
+ }
203
+
204
+ function ColumnVisibilityList<T>({
205
+ columns,
206
+ columnVisibility,
207
+ }: {
208
+ columns: ResolvedColumn<T>[];
209
+ columnVisibility: Record<string, boolean>;
210
+ }) {
211
+ const { engine, locale } = useGridContext<T>();
212
+ const visibleCount = columns.filter((c) => columnVisibility[c.id] !== false).length;
213
+
214
+ return (
215
+ <div className="dg-menu-section">
216
+ <div className="dg-check-list">
217
+ {columns.map((column) => {
218
+ const visible = columnVisibility[column.id] !== false;
219
+ return (
220
+ <label key={column.id}>
221
+ <input
222
+ type="checkbox"
223
+ className="dg-checkbox"
224
+ checked={visible}
225
+ disabled={!column.hideable || (visible && visibleCount === 1)}
226
+ onChange={() => engine.api.setColumnVisibility(column.id, !visible)}
227
+ />
228
+ {column.header}
229
+ </label>
230
+ );
231
+ })}
232
+ </div>
233
+ <button type="button" className="dg-menu-item" onClick={() => engine.api.resetColumns()}>
234
+ {locale.resetColumns}
235
+ </button>
236
+ </div>
237
+ );
238
+ }
239
+
240
+ function describeFilter<T>(filter: ColumnFilter, column: ResolvedColumn<T>, locale: LocaleText): string {
241
+ const operator = locale.operators[filter.operator];
242
+ const format = (value: FilterValue | undefined): string => {
243
+ if (value === null || value === undefined || value === "") return "…";
244
+ if (typeof value === "boolean") return value ? locale.yes : locale.no;
245
+ if (Array.isArray(value)) {
246
+ return value
247
+ .map((v) => column.def.options?.find((o) => o.value === v)?.label ?? String(v))
248
+ .join(", ");
249
+ }
250
+ if (column.type === "date") {
251
+ const time = toTime(value);
252
+ return time === null ? String(value) : new Date(time).toLocaleDateString();
253
+ }
254
+ return String(value);
255
+ };
256
+
257
+ switch (filter.operator) {
258
+ case "isEmpty":
259
+ case "isNotEmpty":
260
+ return `${column.header} ${operator}`;
261
+ case "between":
262
+ return `${column.header} ${format(filter.value)} – ${format(filter.value2)}`;
263
+ default:
264
+ return `${column.header} ${operator} ${format(filter.value)}`;
265
+ }
266
+ }
@@ -0,0 +1,167 @@
1
+ "use client";
2
+
3
+ import { useLayoutEffect, useMemo, useRef, type CSSProperties, type ReactNode, type UIEvent } from "react";
4
+ import { getLayoutVars } from "../core/columns";
5
+ import type { PageResult } from "../core/rows";
6
+ import type { ColumnFilter, ColumnLayout, GridRow, SortItem } from "../core/types";
7
+ import { getColumnRange, getRowRange, visibleRangeEqual, type VisibleRange } from "../core/virtual";
8
+ import { cx, useGridContext } from "./context";
9
+ import { HeaderRow } from "./HeaderRow";
10
+ import { useStoreSelector } from "./hooks";
11
+ import { handleGridKeyDown } from "./keyboard";
12
+ import { Row } from "./Row";
13
+
14
+ interface GridViewportProps<T> {
15
+ layout: ColumnLayout<T>;
16
+ page: PageResult<T>;
17
+ sortedRows: GridRow<T>[];
18
+ sorting: SortItem[];
19
+ filters: ColumnFilter[];
20
+ hasQuery: boolean;
21
+ rowHeight: number;
22
+ headerHeight: number;
23
+ height: number | string | undefined;
24
+ loading: boolean;
25
+ emptyState: ReactNode;
26
+ ariaLabel: string;
27
+ getRowClassName: ((row: T, rowIndex: number) => string | undefined) | undefined;
28
+ }
29
+
30
+ export function GridViewport<T>({
31
+ layout,
32
+ page,
33
+ sortedRows,
34
+ sorting,
35
+ filters,
36
+ hasQuery,
37
+ rowHeight,
38
+ headerHeight,
39
+ height,
40
+ loading,
41
+ emptyState,
42
+ ariaLabel,
43
+ getRowClassName,
44
+ }: GridViewportProps<T>) {
45
+ const { engine, locale, classNames, features } = useGridContext<T>();
46
+ const ref = useRef<HTMLDivElement>(null);
47
+ const rows = page.rows;
48
+
49
+ useLayoutEffect(() => {
50
+ const el = ref.current;
51
+ if (!el) return;
52
+ engine.attachViewport(el);
53
+ const measure = () =>
54
+ engine.viewport.setState((v) =>
55
+ v.width === el.clientWidth && v.height === el.clientHeight
56
+ ? v
57
+ : { ...v, width: el.clientWidth, height: el.clientHeight },
58
+ );
59
+ measure();
60
+ const observer = new ResizeObserver(measure);
61
+ observer.observe(el);
62
+ return () => {
63
+ observer.disconnect();
64
+ engine.attachViewport(null);
65
+ };
66
+ }, [engine]);
67
+
68
+ // Re-renders only when the visible window of rows or columns changes.
69
+ const range = useStoreSelector(
70
+ engine.viewport,
71
+ (v): VisibleRange => ({
72
+ rows: getRowRange(v, rows.length, rowHeight, headerHeight),
73
+ columns: getColumnRange(layout, v),
74
+ }),
75
+ visibleRangeEqual,
76
+ );
77
+
78
+ const active = useStoreSelector(engine.store, (s) => s.ui.activeCell);
79
+ const activeItem = active ? layout.columns[active.colIndex] : undefined;
80
+ const centerIndex = activeItem ? activeItem.visibleIndex - layout.left.length : -1;
81
+ const activeRendered =
82
+ !active ||
83
+ (activeItem !== undefined &&
84
+ (active.rowIndex === -1 || (active.rowIndex >= range.rows.start && active.rowIndex < range.rows.end)) &&
85
+ (activeItem.pinned !== false || (centerIndex >= range.columns.start && centerIndex < range.columns.end)));
86
+
87
+ const style = useMemo(
88
+ () => ({ ...getLayoutVars(layout), height }) as CSSProperties,
89
+ [layout, height],
90
+ );
91
+
92
+ const onScroll = (event: UIEvent<HTMLDivElement>) => {
93
+ const { scrollTop, scrollLeft } = event.currentTarget;
94
+ engine.viewport.setState((v) =>
95
+ v.scrollTop === scrollTop && v.scrollLeft === scrollLeft ? v : { ...v, scrollTop, scrollLeft },
96
+ );
97
+ };
98
+
99
+ const rendered: ReactNode[] = [];
100
+ for (let i = range.rows.start; i < Math.min(range.rows.end, rows.length); i++) {
101
+ const row = rows[i];
102
+ rendered.push(
103
+ <Row
104
+ key={row.id}
105
+ row={row}
106
+ rowIndex={i}
107
+ ariaRowIndex={page.pageOffset + i + 2}
108
+ top={i * rowHeight}
109
+ layout={layout}
110
+ colRange={range.columns}
111
+ className={getRowClassName?.(row.original, i)}
112
+ />,
113
+ );
114
+ }
115
+
116
+ return (
117
+ <div className="dg-viewport-wrap">
118
+ <div
119
+ ref={ref}
120
+ role="grid"
121
+ aria-label={ariaLabel}
122
+ aria-rowcount={page.rowCount + 1}
123
+ aria-colcount={layout.columns.length}
124
+ aria-multiselectable={features.selection && features.selectionMode === "multiple" ? true : undefined}
125
+ aria-busy={loading || undefined}
126
+ // The active cell is the tab stop; if it is scrolled out of the DOM, the grid itself is.
127
+ tabIndex={activeRendered ? -1 : 0}
128
+ className={cx("dg-viewport", classNames.viewport)}
129
+ style={style}
130
+ onScroll={onScroll}
131
+ onKeyDown={(event) => handleGridKeyDown(event, engine)}
132
+ onFocus={(event) => {
133
+ if (event.target === event.currentTarget && active && !activeRendered) {
134
+ engine.setActiveCell(active.rowIndex, active.colIndex);
135
+ }
136
+ }}
137
+ >
138
+ <HeaderRow layout={layout} colRange={range.columns} sorting={sorting} filters={filters} rows={sortedRows} />
139
+ <div role="rowgroup" className="dg-body" style={{ height: rows.length * rowHeight }}>
140
+ {rendered}
141
+ </div>
142
+ </div>
143
+
144
+ {rows.length === 0 && (
145
+ <div className="dg-overlay" role="status">
146
+ {loading
147
+ ? locale.loading
148
+ : (emptyState ?? (
149
+ <div className="dg-empty">
150
+ <span>{hasQuery ? locale.noResults : locale.noRows}</span>
151
+ {hasQuery && (
152
+ <button type="button" className="dg-button" onClick={() => engine.api.clearFilters()}>
153
+ {locale.clearFilters}
154
+ </button>
155
+ )}
156
+ </div>
157
+ ))}
158
+ </div>
159
+ )}
160
+ {loading && (
161
+ <div className="dg-loading" aria-hidden="true">
162
+ <div className="dg-loading-bar" />
163
+ </div>
164
+ )}
165
+ </div>
166
+ );
167
+ }
@@ -0,0 +1,57 @@
1
+ "use client";
2
+
3
+ import { createContext, use } from "react";
4
+ import type { GridEngine } from "../core/grid";
5
+ import type { RowId } from "../core/types";
6
+ import type { Formatters } from "../core/values";
7
+ import type { LocaleText } from "./locale";
8
+
9
+ export type GridSlot =
10
+ | "root"
11
+ | "toolbar"
12
+ | "viewport"
13
+ | "header"
14
+ | "headerCell"
15
+ | "row"
16
+ | "cell"
17
+ | "pagination";
18
+
19
+ export interface GridFeatures {
20
+ sorting: boolean;
21
+ filtering: boolean;
22
+ resizing: boolean;
23
+ reordering: boolean;
24
+ pinning: boolean;
25
+ hiding: boolean;
26
+ selection: boolean;
27
+ selectionMode: "single" | "multiple";
28
+ }
29
+
30
+ export interface GridContextValue<T> {
31
+ engine: GridEngine<T>;
32
+ locale: LocaleText;
33
+ classNames: Partial<Record<GridSlot, string>>;
34
+ features: GridFeatures;
35
+ formatters: Formatters;
36
+ numberFormat: Intl.NumberFormat;
37
+ /** Set when the parent controls `rowSelection`. */
38
+ controlledSelection: Record<RowId, boolean> | undefined;
39
+ isRowSelectable(row: T): boolean;
40
+ openColumnMenu(columnId: string, anchor: HTMLElement): void;
41
+ }
42
+
43
+ export const GridContext = createContext<GridContextValue<unknown> | null>(null);
44
+
45
+ export function useGridContext<T>(): GridContextValue<T> {
46
+ const context = use(GridContext);
47
+ if (!context) {
48
+ throw new Error("[DataGrid] Grid parts must be rendered inside <DataGrid>.");
49
+ }
50
+ return context as unknown as GridContextValue<T>;
51
+ }
52
+
53
+ /** Marks a cell without an in-flight async edit. */
54
+ export const NO_PENDING: unique symbol = Symbol("dg.noPending");
55
+
56
+ export const cx = (...names: (string | false | null | undefined)[]) =>
57
+ names.filter(Boolean).join(" ");
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { useCallback, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore, type RefObject } from "react";
4
+ import type { ReadableStore } from "../core/store";
5
+ import type { RowId } from "../core/types";
6
+ import { useGridContext } from "./context";
7
+
8
+ /** Snapshot getter that returns the previous selection while it is still equal. */
9
+ function createSelectionGetter<S, R>(
10
+ store: ReadableStore<S>,
11
+ selector: (snapshot: S) => R,
12
+ isEqual: (a: R, b: R) => boolean,
13
+ ): () => R {
14
+ let cache: { snapshot: S; value: R } | null = null;
15
+ return () => {
16
+ const snapshot = store.getSnapshot();
17
+ if (cache && Object.is(cache.snapshot, snapshot)) return cache.value;
18
+ const value = selector(snapshot);
19
+ cache = { snapshot, value: cache && isEqual(cache.value, value) ? cache.value : value };
20
+ return cache.value;
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Subscribes to a slice of an external store. The component re-renders only
26
+ * when the selected value changes according to `isEqual`.
27
+ */
28
+ export function useStoreSelector<S, R>(
29
+ store: ReadableStore<S>,
30
+ selector: (snapshot: S) => R,
31
+ isEqual: (a: R, b: R) => boolean = Object.is,
32
+ ): R {
33
+ const getSelection = useMemo(
34
+ () => createSelectionGetter(store, selector, isEqual),
35
+ [store, selector, isEqual],
36
+ );
37
+ return useSyncExternalStore(store.subscribe, getSelection, getSelection);
38
+ }
39
+
40
+ /** Row selection, from the controlled prop when present, otherwise internal state. */
41
+ export function useSelectionState<T, R>(
42
+ selector: (selection: Record<RowId, boolean>) => R,
43
+ ): R {
44
+ const { engine, controlledSelection } = useGridContext<T>();
45
+ const internal = useStoreSelector(engine.store, (s) => selector(s.state.rowSelection));
46
+ return controlledSelection ? selector(controlledSelection) : internal;
47
+ }
48
+
49
+ /** Moves DOM focus to a cell once it becomes active through keyboard navigation. */
50
+ export function useFocusWhenActive(
51
+ ref: RefObject<HTMLElement | null>,
52
+ active: boolean,
53
+ editing = false,
54
+ ) {
55
+ const { engine } = useGridContext();
56
+ useLayoutEffect(() => {
57
+ if (active && !editing && engine.consumeFocusRequest()) {
58
+ ref.current?.focus({ preventScroll: true });
59
+ }
60
+ }, [active, editing, engine, ref]);
61
+ }
62
+
63
+ /** Open state for an anchored popover, ignoring the click that just dismissed it. */
64
+ export function usePopoverState<P = void>() {
65
+ const [state, setState] = useState<{ anchor: HTMLElement; payload: P } | null>(null);
66
+ const lastClosed = useRef<{ anchor: HTMLElement | null; time: number }>({ anchor: null, time: 0 });
67
+
68
+ const open = useCallback((anchor: HTMLElement, payload: P) => {
69
+ const last = lastClosed.current;
70
+ if (last.anchor === anchor && performance.now() - last.time < 300) return;
71
+ setState({ anchor, payload });
72
+ }, []);
73
+
74
+ const close = useCallback((anchor: HTMLElement | null) => {
75
+ lastClosed.current = { anchor, time: performance.now() };
76
+ setState(null);
77
+ }, []);
78
+
79
+ return { state, open, close };
80
+ }
@@ -0,0 +1,72 @@
1
+ import type { SVGProps } from "react";
2
+
3
+ type IconProps = SVGProps<SVGSVGElement>;
4
+
5
+ function Svg(props: IconProps) {
6
+ return (
7
+ <svg
8
+ width="16"
9
+ height="16"
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth="2"
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ aria-hidden="true"
17
+ focusable="false"
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ export const SearchIcon = (p: IconProps) => (
24
+ <Svg {...p}><circle cx="11" cy="11" r="7" /><path d="m20 20-3.5-3.5" /></Svg>
25
+ );
26
+ export const ArrowUpIcon = (p: IconProps) => (
27
+ <Svg {...p}><path d="M12 19V5M5 12l7-7 7 7" /></Svg>
28
+ );
29
+ export const ArrowDownIcon = (p: IconProps) => (
30
+ <Svg {...p}><path d="M12 5v14M19 12l-7 7-7-7" /></Svg>
31
+ );
32
+ export const MoreIcon = (p: IconProps) => (
33
+ <Svg {...p}><path d="M12 5h.01M12 12h.01M12 19h.01" strokeWidth="3" /></Svg>
34
+ );
35
+ export const ChevronLeftIcon = (p: IconProps) => (
36
+ <Svg {...p}><path d="m15 18-6-6 6-6" /></Svg>
37
+ );
38
+ export const ChevronRightIcon = (p: IconProps) => (
39
+ <Svg {...p}><path d="m9 18 6-6-6-6" /></Svg>
40
+ );
41
+ export const ChevronsLeftIcon = (p: IconProps) => (
42
+ <Svg {...p}><path d="m11 17-5-5 5-5M18 17l-5-5 5-5" /></Svg>
43
+ );
44
+ export const ChevronsRightIcon = (p: IconProps) => (
45
+ <Svg {...p}><path d="m13 17 5-5-5-5M6 17l5-5-5-5" /></Svg>
46
+ );
47
+ export const XIcon = (p: IconProps) => (
48
+ <Svg {...p}><path d="M18 6 6 18M6 6l12 12" /></Svg>
49
+ );
50
+ export const ColumnsIcon = (p: IconProps) => (
51
+ <Svg {...p}><rect x="3" y="4" width="18" height="16" rx="2" /><path d="M9 4v16M15 4v16" /></Svg>
52
+ );
53
+ export const DownloadIcon = (p: IconProps) => (
54
+ <Svg {...p}><path d="M12 4v11m0 0-4-4m4 4 4-4M5 20h14" /></Svg>
55
+ );
56
+ export const DensityIcon = (p: IconProps) => (
57
+ <Svg {...p}><path d="M4 6h16M4 12h16M4 18h16" /></Svg>
58
+ );
59
+ export const PinIcon = (p: IconProps) => (
60
+ <Svg {...p}><path d="M12 17v5M9 3h6l-1 6 3 4H7l3-4z" /></Svg>
61
+ );
62
+ export const EyeOffIcon = (p: IconProps) => (
63
+ <Svg {...p}>
64
+ <path d="m3 3 18 18M10.6 10.6a2 2 0 0 0 2.8 2.8M9.9 5.1A10 10 0 0 1 12 5c6 0 10 7 10 7a17 17 0 0 1-3 3.9M6.6 6.6C3.9 8.4 2 12 2 12s4 7 10 7a9.7 9.7 0 0 0 5.4-1.6" />
65
+ </Svg>
66
+ );
67
+ export const MoveIcon = (p: IconProps) => (
68
+ <Svg {...p}><path d="M5 12h14M9 8l-4 4 4 4M15 8l4 4-4 4" /></Svg>
69
+ );
70
+ export const CheckIcon = (p: IconProps) => (
71
+ <Svg {...p}><path d="M20 6 9 17l-5-5" /></Svg>
72
+ );