@kala-ui/react-app 0.1.0-beta.0
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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/components/app-shell/app-shell.d.ts +54 -0
- package/dist/components/app-shell/app-shell.js +91 -0
- package/dist/components/app-shell/index.d.ts +1 -0
- package/dist/components/app-shell/index.js +2 -0
- package/dist/components/charts/area-chart.d.ts +6 -0
- package/dist/components/charts/area-chart.js +172 -0
- package/dist/components/charts/bar-chart.d.ts +6 -0
- package/dist/components/charts/bar-chart.js +153 -0
- package/dist/components/charts/chart-skeleton.d.ts +36 -0
- package/dist/components/charts/chart-skeleton.js +13 -0
- package/dist/components/charts/chart.d.ts +25 -0
- package/dist/components/charts/chart.js +48 -0
- package/dist/components/charts/chart.types.d.ts +196 -0
- package/dist/components/charts/chart.types.js +6 -0
- package/dist/components/charts/donut-chart.d.ts +7 -0
- package/dist/components/charts/donut-chart.js +146 -0
- package/dist/components/charts/index.d.ts +14 -0
- package/dist/components/charts/index.js +15 -0
- package/dist/components/charts/line-chart.d.ts +6 -0
- package/dist/components/charts/line-chart.js +158 -0
- package/dist/components/charts/radial-bar-chart.d.ts +6 -0
- package/dist/components/charts/radial-bar-chart.js +115 -0
- package/dist/components/charts/theme-utils.d.ts +77 -0
- package/dist/components/charts/theme-utils.js +243 -0
- package/dist/components/charts/use-theme-aware-chart.d.ts +12 -0
- package/dist/components/charts/use-theme-aware-chart.js +68 -0
- package/dist/components/charts/utils.d.ts +56 -0
- package/dist/components/charts/utils.js +135 -0
- package/dist/components/data-table/column-filters.d.ts +14 -0
- package/dist/components/data-table/column-filters.js +51 -0
- package/dist/components/data-table/column-header-filter.d.ts +14 -0
- package/dist/components/data-table/column-header-filter.js +75 -0
- package/dist/components/data-table/data-table-body-rows.d.ts +23 -0
- package/dist/components/data-table/data-table-body-rows.js +43 -0
- package/dist/components/data-table/data-table-filter-chips.d.ts +14 -0
- package/dist/components/data-table/data-table-filter-chips.js +42 -0
- package/dist/components/data-table/data-table-header-row.d.ts +21 -0
- package/dist/components/data-table/data-table-header-row.js +35 -0
- package/dist/components/data-table/data-table-skeleton.d.ts +49 -0
- package/dist/components/data-table/data-table-skeleton.js +32 -0
- package/dist/components/data-table/data-table-toolbar.d.ts +18 -0
- package/dist/components/data-table/data-table-toolbar.js +25 -0
- package/dist/components/data-table/data-table.d.ts +23 -0
- package/dist/components/data-table/data-table.js +250 -0
- package/dist/components/data-table/data-table.types.d.ts +305 -0
- package/dist/components/data-table/data-table.types.js +6 -0
- package/dist/components/data-table/index.d.ts +6 -0
- package/dist/components/data-table/index.js +6 -0
- package/dist/components/data-table/pagination-nav.d.ts +15 -0
- package/dist/components/data-table/pagination-nav.js +23 -0
- package/dist/components/data-table/useTableState.d.ts +29 -0
- package/dist/components/data-table/useTableState.js +312 -0
- package/dist/components/dnd/dnd.d.ts +118 -0
- package/dist/components/dnd/dnd.js +128 -0
- package/dist/components/dnd/index.d.ts +1 -0
- package/dist/components/dnd/index.js +2 -0
- package/dist/components/footer/footer.d.ts +37 -0
- package/dist/components/footer/footer.js +8 -0
- package/dist/components/footer/index.d.ts +2 -0
- package/dist/components/footer/index.js +2 -0
- package/dist/components/header/header-skeleton.d.ts +54 -0
- package/dist/components/header/header-skeleton.js +23 -0
- package/dist/components/header/header.d.ts +40 -0
- package/dist/components/header/header.js +77 -0
- package/dist/components/header/header.types.d.ts +39 -0
- package/dist/components/header/header.types.js +2 -0
- package/dist/components/header/index.d.ts +4 -0
- package/dist/components/header/index.js +3 -0
- package/dist/components/metric-card/index.d.ts +2 -0
- package/dist/components/metric-card/index.js +3 -0
- package/dist/components/metric-card/metric-card-skeleton.d.ts +32 -0
- package/dist/components/metric-card/metric-card-skeleton.js +29 -0
- package/dist/components/metric-card/metric-card.d.ts +16 -0
- package/dist/components/metric-card/metric-card.js +55 -0
- package/dist/components/metric-card/metric-card.types.d.ts +14 -0
- package/dist/components/metric-card/metric-card.types.js +2 -0
- package/dist/components/nav-link/index.d.ts +1 -0
- package/dist/components/nav-link/index.js +2 -0
- package/dist/components/nav-link/nav-link.d.ts +26 -0
- package/dist/components/nav-link/nav-link.js +23 -0
- package/dist/components/navigation/index.d.ts +4 -0
- package/dist/components/navigation/index.js +3 -0
- package/dist/components/navigation/navigation-skeleton.d.ts +54 -0
- package/dist/components/navigation/navigation-skeleton.js +23 -0
- package/dist/components/navigation/navigation.d.ts +26 -0
- package/dist/components/navigation/navigation.js +44 -0
- package/dist/components/session-card/index.d.ts +2 -0
- package/dist/components/session-card/index.js +3 -0
- package/dist/components/session-card/session-card-skeleton.d.ts +31 -0
- package/dist/components/session-card/session-card-skeleton.js +28 -0
- package/dist/components/session-card/session-card.d.ts +27 -0
- package/dist/components/session-card/session-card.js +67 -0
- package/dist/components/sidebar/index.d.ts +3 -0
- package/dist/components/sidebar/index.js +3 -0
- package/dist/components/sidebar/sidebar-skeleton.d.ts +54 -0
- package/dist/components/sidebar/sidebar-skeleton.js +23 -0
- package/dist/components/sidebar/sidebar.d.ts +46 -0
- package/dist/components/sidebar/sidebar.js +69 -0
- package/dist/components/social-login-button/index.d.ts +1 -0
- package/dist/components/social-login-button/index.js +2 -0
- package/dist/components/social-login-button/social-login-button.d.ts +20 -0
- package/dist/components/social-login-button/social-login-button.js +57 -0
- package/dist/components/social-login-buttons/index.d.ts +1 -0
- package/dist/components/social-login-buttons/index.js +2 -0
- package/dist/components/social-login-buttons/social-login-buttons.d.ts +34 -0
- package/dist/components/social-login-buttons/social-login-buttons.js +18 -0
- package/dist/components/sparkline-chart/index.d.ts +2 -0
- package/dist/components/sparkline-chart/index.js +2 -0
- package/dist/components/sparkline-chart/sparkline-chart.d.ts +51 -0
- package/dist/components/sparkline-chart/sparkline-chart.js +134 -0
- package/dist/components/user-menu-dropdown/index.d.ts +1 -0
- package/dist/components/user-menu-dropdown/index.js +2 -0
- package/dist/components/user-menu-dropdown/user-menu-dropdown.d.ts +31 -0
- package/dist/components/user-menu-dropdown/user-menu-dropdown.js +27 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +19 -0
- package/dist/lib/active-path.d.ts +5 -0
- package/dist/lib/active-path.js +12 -0
- package/dist/styles/app.css +2125 -0
- package/package.json +164 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTableState Hook
|
|
3
|
+
* Manages table state including sorting, filtering, pagination, and search
|
|
4
|
+
*/
|
|
5
|
+
import type { ColumnDef, FilterConfig, SearchConfig, SortConfig, TableState } from "./data-table.types";
|
|
6
|
+
interface UseTableStateOptions<TData> {
|
|
7
|
+
data: TData[];
|
|
8
|
+
columns: ColumnDef<TData>[];
|
|
9
|
+
defaultSort?: SortConfig<TData>;
|
|
10
|
+
defaultFilters?: FilterConfig<TData>[];
|
|
11
|
+
searchConfig?: SearchConfig<TData> | boolean;
|
|
12
|
+
pageSize?: number;
|
|
13
|
+
total?: number;
|
|
14
|
+
/** Controlled page number (1-indexed); omitted = uncontrolled */
|
|
15
|
+
page?: number;
|
|
16
|
+
/** Callback when sort changes */
|
|
17
|
+
onSortChange?: (sort: SortConfig<TData> | null) => void;
|
|
18
|
+
/** Callback when filters change */
|
|
19
|
+
onFilterChange?: (filters: FilterConfig<TData>[]) => void;
|
|
20
|
+
/** Callback when search query changes */
|
|
21
|
+
onSearchChange?: (query: string) => void;
|
|
22
|
+
/** Callback when pagination changes */
|
|
23
|
+
onPaginationChange?: (page: number, pageSize: number) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hook to manage table state (sorting, filtering, search, pagination)
|
|
27
|
+
*/
|
|
28
|
+
export declare function useTableState<TData>({ data, columns, defaultSort, defaultFilters, searchConfig, pageSize: initialPageSize, total: manualTotal, page, onSortChange, onFilterChange, onSearchChange, onPaginationChange, }: UseTableStateOptions<TData>): TableState<TData>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* useTableState Hook
|
|
4
|
+
* Manages table state including sorting, filtering, pagination, and search
|
|
5
|
+
*/
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
/**
|
|
8
|
+
* Hook to manage table state (sorting, filtering, search, pagination)
|
|
9
|
+
*/
|
|
10
|
+
export function useTableState({ data, columns, defaultSort, defaultFilters = [], searchConfig, pageSize: initialPageSize = 10, total: manualTotal, page, onSortChange, onFilterChange, onSearchChange, onPaginationChange, }) {
|
|
11
|
+
// State
|
|
12
|
+
const [sortConfig, setSortConfig] = useState(defaultSort ?? null);
|
|
13
|
+
const [filterConfigs, setFilterConfigs] = useState(defaultFilters);
|
|
14
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
15
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
16
|
+
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
17
|
+
// Controlled page: mirror prop changes into internal state during render
|
|
18
|
+
// (adjust-state-on-prop-change). The hook keeps working uncontrolled when
|
|
19
|
+
// no `page` is supplied.
|
|
20
|
+
const prevControlledPageRef = useRef(undefined);
|
|
21
|
+
if (page !== undefined &&
|
|
22
|
+
page !== prevControlledPageRef.current &&
|
|
23
|
+
page !== currentPage) {
|
|
24
|
+
prevControlledPageRef.current = page;
|
|
25
|
+
setCurrentPage(page);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
prevControlledPageRef.current = page;
|
|
29
|
+
}
|
|
30
|
+
// Sync pageSize when initialPageSize changes (e.g. data loads and pagination is false)
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setPageSize(initialPageSize);
|
|
33
|
+
}, [initialPageSize]);
|
|
34
|
+
// Get search keys
|
|
35
|
+
const searchKeys = useMemo(() => {
|
|
36
|
+
if (!searchConfig)
|
|
37
|
+
return [];
|
|
38
|
+
const defaultKeys = columns
|
|
39
|
+
.filter((col) => col.accessorKey)
|
|
40
|
+
.map((col) => col.accessorKey);
|
|
41
|
+
if (typeof searchConfig === "boolean") {
|
|
42
|
+
return defaultKeys;
|
|
43
|
+
}
|
|
44
|
+
return searchConfig.searchKeys ?? defaultKeys;
|
|
45
|
+
}, [searchConfig, columns]);
|
|
46
|
+
// Filter data based on search query
|
|
47
|
+
// When onSearchChange is provided, data is already server-filtered — skip client-side filtering
|
|
48
|
+
const searchedData = useMemo(() => {
|
|
49
|
+
if (onSearchChange)
|
|
50
|
+
return data;
|
|
51
|
+
if (!searchQuery.trim() || searchKeys.length === 0)
|
|
52
|
+
return data;
|
|
53
|
+
const query = searchQuery.toLowerCase();
|
|
54
|
+
return data.filter((row) => {
|
|
55
|
+
return searchKeys.some((key) => {
|
|
56
|
+
const value = row[key];
|
|
57
|
+
if (value === null || value === undefined)
|
|
58
|
+
return false;
|
|
59
|
+
return String(value).toLowerCase().includes(query);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}, [data, searchQuery, searchKeys, onSearchChange]);
|
|
63
|
+
// Apply filters
|
|
64
|
+
const filteredData = useMemo(() => {
|
|
65
|
+
if (filterConfigs.length === 0)
|
|
66
|
+
return searchedData;
|
|
67
|
+
return searchedData.filter((row) => {
|
|
68
|
+
return filterConfigs.every((filter) => {
|
|
69
|
+
const value = row[filter.key];
|
|
70
|
+
const filterValue = filter.value;
|
|
71
|
+
switch (filter.operator) {
|
|
72
|
+
case "equals":
|
|
73
|
+
if (typeof value === "boolean") {
|
|
74
|
+
return value === (filterValue === "true" || filterValue === true);
|
|
75
|
+
}
|
|
76
|
+
return value === filterValue;
|
|
77
|
+
case "contains":
|
|
78
|
+
return String(value)
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.includes(String(filterValue).toLowerCase());
|
|
81
|
+
case "startsWith":
|
|
82
|
+
return String(value)
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.startsWith(String(filterValue).toLowerCase());
|
|
85
|
+
case "endsWith":
|
|
86
|
+
return String(value)
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.endsWith(String(filterValue).toLowerCase());
|
|
89
|
+
case "in":
|
|
90
|
+
if (typeof value === "boolean") {
|
|
91
|
+
const strVal = String(value);
|
|
92
|
+
return (Array.isArray(filterValue) &&
|
|
93
|
+
filterValue.some((v) => v === strVal || v === value));
|
|
94
|
+
}
|
|
95
|
+
return Array.isArray(filterValue) && filterValue.includes(value);
|
|
96
|
+
default:
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}, [searchedData, filterConfigs]);
|
|
102
|
+
// Apply sorting
|
|
103
|
+
const sortedData = useMemo(() => {
|
|
104
|
+
if (!sortConfig)
|
|
105
|
+
return filteredData;
|
|
106
|
+
const { key, direction } = sortConfig;
|
|
107
|
+
const column = columns.find((col) => col.accessorKey === key);
|
|
108
|
+
return [...filteredData].sort((a, b) => {
|
|
109
|
+
// Use custom sort function if provided
|
|
110
|
+
if (column?.sortFn) {
|
|
111
|
+
return column.sortFn(a, b, direction);
|
|
112
|
+
}
|
|
113
|
+
// Default sort
|
|
114
|
+
const aValue = a[key];
|
|
115
|
+
const bValue = b[key];
|
|
116
|
+
if (aValue === bValue)
|
|
117
|
+
return 0;
|
|
118
|
+
if (aValue === null || aValue === undefined)
|
|
119
|
+
return 1;
|
|
120
|
+
if (bValue === null || bValue === undefined)
|
|
121
|
+
return -1;
|
|
122
|
+
// Handle different types
|
|
123
|
+
if (typeof aValue === "string" && typeof bValue === "string") {
|
|
124
|
+
const result = aValue.localeCompare(bValue);
|
|
125
|
+
return direction === "asc" ? result : -result;
|
|
126
|
+
}
|
|
127
|
+
if (typeof aValue === "number" && typeof bValue === "number") {
|
|
128
|
+
return direction === "asc" ? aValue - bValue : bValue - aValue;
|
|
129
|
+
}
|
|
130
|
+
if (aValue instanceof Date && bValue instanceof Date) {
|
|
131
|
+
return direction === "asc"
|
|
132
|
+
? aValue.getTime() - bValue.getTime()
|
|
133
|
+
: bValue.getTime() - aValue.getTime();
|
|
134
|
+
}
|
|
135
|
+
// Fallback to string comparison
|
|
136
|
+
const result = String(aValue).localeCompare(String(bValue));
|
|
137
|
+
return direction === "asc" ? result : -result;
|
|
138
|
+
});
|
|
139
|
+
}, [filteredData, sortConfig, columns]);
|
|
140
|
+
// Pagination
|
|
141
|
+
const totalPages = Math.ceil((manualTotal ?? sortedData.length) / pageSize);
|
|
142
|
+
const hasNextPage = currentPage < totalPages;
|
|
143
|
+
const hasPreviousPage = currentPage > 1;
|
|
144
|
+
const pageData = useMemo(() => {
|
|
145
|
+
// If manualTotal AND onPaginationChange are provided, assume server-side pagination
|
|
146
|
+
// Otherwise, do client-side pagination
|
|
147
|
+
if (manualTotal !== undefined && onPaginationChange !== undefined) {
|
|
148
|
+
return sortedData;
|
|
149
|
+
}
|
|
150
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
151
|
+
const endIndex = startIndex + pageSize;
|
|
152
|
+
return sortedData.slice(startIndex, endIndex);
|
|
153
|
+
}, [sortedData, currentPage, pageSize, manualTotal, onPaginationChange]);
|
|
154
|
+
// Toggle sort
|
|
155
|
+
const toggleSort = useCallback((key) => {
|
|
156
|
+
setSortConfig((prev) => {
|
|
157
|
+
let newSort;
|
|
158
|
+
if (!prev || prev.key !== key) {
|
|
159
|
+
newSort = { key, direction: "asc" };
|
|
160
|
+
}
|
|
161
|
+
else if (prev.direction === "asc") {
|
|
162
|
+
newSort = { key, direction: "desc" };
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
newSort = null; // Clear sort
|
|
166
|
+
}
|
|
167
|
+
return newSort;
|
|
168
|
+
});
|
|
169
|
+
setCurrentPage(1); // Reset to first page when sorting changes
|
|
170
|
+
}, []);
|
|
171
|
+
// Notify parent of sort changes after render. Calling the callback directly
|
|
172
|
+
// inside useEffect is safe — the setState that triggered this effect has
|
|
173
|
+
// already committed; the callback runs in the passive-effect phase, not
|
|
174
|
+
// during render. (The prior queueMicrotask deferral broke act() in tests:
|
|
175
|
+
// the microtask fired outside act()'s flush window, so sync assertions saw
|
|
176
|
+
// zero calls.)
|
|
177
|
+
const prevSortRef = useRef(defaultSort ?? null);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (sortConfig !== prevSortRef.current) {
|
|
180
|
+
prevSortRef.current = sortConfig;
|
|
181
|
+
onSortChange?.(sortConfig);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// Notify parent of filter changes after render.
|
|
185
|
+
const prevFilterRef = useRef(defaultFilters);
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (filterConfigs !== prevFilterRef.current) {
|
|
188
|
+
prevFilterRef.current = filterConfigs;
|
|
189
|
+
onFilterChange?.(filterConfigs);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Extract debounce ms from searchConfig
|
|
193
|
+
const debounceMs = useMemo(() => {
|
|
194
|
+
if (!searchConfig || typeof searchConfig === "boolean")
|
|
195
|
+
return 0;
|
|
196
|
+
return searchConfig.debounce ?? 0;
|
|
197
|
+
}, [searchConfig]);
|
|
198
|
+
// Notify parent of search changes after render. When a debounce is
|
|
199
|
+
// configured, the parent (server-side search) is notified only after the
|
|
200
|
+
// debounce window elapses — firing on every keystroke would hammer the
|
|
201
|
+
// server. The local searchQuery state updates immediately so the local
|
|
202
|
+
// (client-side) filtering stays responsive; only the parent callback waits.
|
|
203
|
+
// The effect depends on [searchQuery] only: re-running it on unrelated
|
|
204
|
+
// re-renders would clear the pending timer and silently swallow the
|
|
205
|
+
// callback, so the latest callback is read through a ref instead.
|
|
206
|
+
const prevSearchRef = useRef("");
|
|
207
|
+
const onSearchChangeRef = useRef(onSearchChange);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
onSearchChangeRef.current = onSearchChange;
|
|
210
|
+
});
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (searchQuery === prevSearchRef.current)
|
|
213
|
+
return;
|
|
214
|
+
prevSearchRef.current = searchQuery;
|
|
215
|
+
const callback = onSearchChangeRef.current;
|
|
216
|
+
if (!callback)
|
|
217
|
+
return;
|
|
218
|
+
if (debounceMs <= 0) {
|
|
219
|
+
callback(searchQuery);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const timer = setTimeout(() => callback(searchQuery), debounceMs);
|
|
223
|
+
return () => clearTimeout(timer);
|
|
224
|
+
}, [searchQuery, debounceMs]);
|
|
225
|
+
// Notify parent of pagination changes after render.
|
|
226
|
+
const prevPaginationRef = useRef({ page: 1, pageSize: initialPageSize });
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
const prev = prevPaginationRef.current;
|
|
229
|
+
if (currentPage !== prev.page || pageSize !== prev.pageSize) {
|
|
230
|
+
prevPaginationRef.current = { page: currentPage, pageSize };
|
|
231
|
+
onPaginationChange?.(currentPage, pageSize);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
// Filter management
|
|
235
|
+
const setFilter = useCallback((filter) => {
|
|
236
|
+
setFilterConfigs((prev) => {
|
|
237
|
+
const filtered = prev.filter((f) => f.key !== filter.key);
|
|
238
|
+
return [...filtered, filter];
|
|
239
|
+
});
|
|
240
|
+
setCurrentPage(1);
|
|
241
|
+
}, []);
|
|
242
|
+
const removeFilter = useCallback((key) => {
|
|
243
|
+
setFilterConfigs((prev) => prev.filter((f) => f.key !== key));
|
|
244
|
+
setCurrentPage(1);
|
|
245
|
+
}, []);
|
|
246
|
+
const clearFilters = useCallback(() => {
|
|
247
|
+
setFilterConfigs([]);
|
|
248
|
+
setCurrentPage(1);
|
|
249
|
+
}, []);
|
|
250
|
+
const searchDebounceRef = useRef(null);
|
|
251
|
+
// Search management
|
|
252
|
+
const handleSetSearchQuery = useCallback((query) => {
|
|
253
|
+
setSearchQuery(query);
|
|
254
|
+
if (searchDebounceRef.current)
|
|
255
|
+
clearTimeout(searchDebounceRef.current);
|
|
256
|
+
if (debounceMs > 0) {
|
|
257
|
+
searchDebounceRef.current = setTimeout(() => {
|
|
258
|
+
setCurrentPage(1);
|
|
259
|
+
}, debounceMs);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
setCurrentPage(1);
|
|
263
|
+
}
|
|
264
|
+
}, [debounceMs]);
|
|
265
|
+
// Set current page and notify server-side listener
|
|
266
|
+
const handleSetCurrentPage = useCallback((pageOrUpdater) => {
|
|
267
|
+
setCurrentPage((prev) => {
|
|
268
|
+
const newPage = typeof pageOrUpdater === "function"
|
|
269
|
+
? pageOrUpdater(prev)
|
|
270
|
+
: pageOrUpdater;
|
|
271
|
+
return newPage;
|
|
272
|
+
});
|
|
273
|
+
}, []);
|
|
274
|
+
// Pagination navigation
|
|
275
|
+
const nextPage = useCallback(() => {
|
|
276
|
+
if (hasNextPage) {
|
|
277
|
+
handleSetCurrentPage((prev) => prev + 1);
|
|
278
|
+
}
|
|
279
|
+
}, [hasNextPage, handleSetCurrentPage]);
|
|
280
|
+
const previousPage = useCallback(() => {
|
|
281
|
+
if (hasPreviousPage) {
|
|
282
|
+
handleSetCurrentPage((prev) => prev - 1);
|
|
283
|
+
}
|
|
284
|
+
}, [hasPreviousPage, handleSetCurrentPage]);
|
|
285
|
+
// Page size change — reset page without direct callback
|
|
286
|
+
const handleSetPageSize = useCallback((size) => {
|
|
287
|
+
setPageSize(size);
|
|
288
|
+
setCurrentPage(1);
|
|
289
|
+
}, []);
|
|
290
|
+
return {
|
|
291
|
+
processedData: sortedData,
|
|
292
|
+
pageData,
|
|
293
|
+
sortConfig,
|
|
294
|
+
setSortConfig,
|
|
295
|
+
toggleSort,
|
|
296
|
+
filterConfigs,
|
|
297
|
+
setFilter,
|
|
298
|
+
removeFilter,
|
|
299
|
+
clearFilters,
|
|
300
|
+
searchQuery,
|
|
301
|
+
setSearchQuery: handleSetSearchQuery,
|
|
302
|
+
currentPage,
|
|
303
|
+
setCurrentPage: handleSetCurrentPage,
|
|
304
|
+
pageSize,
|
|
305
|
+
setPageSize: handleSetPageSize,
|
|
306
|
+
totalPages,
|
|
307
|
+
hasNextPage,
|
|
308
|
+
hasPreviousPage,
|
|
309
|
+
nextPage,
|
|
310
|
+
previousPage,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DnD Component - Drag and Drop primitives for @dnd-kit
|
|
3
|
+
* Provides consistent wrappers with animations, sortable support, and drag overlays
|
|
4
|
+
*/
|
|
5
|
+
import type { Active, Collision, CollisionDetection, DndContextProps as DndKitContextProps, DragEndEvent, DraggableAttributes, DraggableSyntheticListeners, DragOverEvent, DragStartEvent, Modifier, Over, PointerActivationConstraint, UniqueIdentifier } from "@dnd-kit/core";
|
|
6
|
+
import { KeyboardSensor, PointerSensor, TouchSensor, useDraggable, useDroppable, useSensor, useSensors } from "@dnd-kit/core";
|
|
7
|
+
import { type SortingStrategy, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
|
8
|
+
import type { Transform } from "@dnd-kit/utilities";
|
|
9
|
+
import * as React from "react";
|
|
10
|
+
type SyntheticListenerMap = DraggableSyntheticListeners;
|
|
11
|
+
export type DragDropModifier = Modifier;
|
|
12
|
+
export type DragDropCollisionDetection = CollisionDetection;
|
|
13
|
+
export type DragDropSortingStrategy = SortingStrategy;
|
|
14
|
+
export type DragDropPointerConstraint = PointerActivationConstraint;
|
|
15
|
+
export declare const collisionDetectionAlgorithms: {
|
|
16
|
+
readonly closestCenter: CollisionDetection;
|
|
17
|
+
readonly closestCorners: CollisionDetection;
|
|
18
|
+
readonly rectIntersection: CollisionDetection;
|
|
19
|
+
readonly pointerWithin: CollisionDetection;
|
|
20
|
+
};
|
|
21
|
+
export declare const modifiers: {
|
|
22
|
+
readonly restrictToHorizontalAxis: Modifier;
|
|
23
|
+
readonly restrictToVerticalAxis: Modifier;
|
|
24
|
+
readonly restrictToWindowEdges: Modifier;
|
|
25
|
+
readonly restrictToParentElement: Modifier;
|
|
26
|
+
readonly restrictToFirstScrollableAncestor: Modifier;
|
|
27
|
+
};
|
|
28
|
+
export declare const sortingStrategies: {
|
|
29
|
+
readonly vertical: SortingStrategy;
|
|
30
|
+
readonly horizontal: SortingStrategy;
|
|
31
|
+
readonly rect: SortingStrategy;
|
|
32
|
+
readonly rectSwapping: SortingStrategy;
|
|
33
|
+
};
|
|
34
|
+
export interface UseDragDropSensorsOptions {
|
|
35
|
+
activationConstraint?: PointerActivationConstraint;
|
|
36
|
+
keyboardCoordinateGetter?: typeof sortableKeyboardCoordinates;
|
|
37
|
+
}
|
|
38
|
+
declare function createDragDropSensors(options?: UseDragDropSensorsOptions): import("@dnd-kit/core").SensorDescriptor<import("@dnd-kit/core").SensorOptions>[];
|
|
39
|
+
export interface DragDropContextProps extends Omit<DndKitContextProps, "sensors"> {
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
sensors?: ReturnType<typeof createDragDropSensors>;
|
|
42
|
+
useSensors?: UseDragDropSensorsOptions;
|
|
43
|
+
}
|
|
44
|
+
declare function DragDropContext({ children, sensors: sensorsProp, useSensors: useSensorsOptions, ...props }: DragDropContextProps): React.JSX.Element;
|
|
45
|
+
export interface DroppableProps {
|
|
46
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
47
|
+
id: UniqueIdentifier;
|
|
48
|
+
children: React.ReactNode | ((args: {
|
|
49
|
+
isOver: boolean;
|
|
50
|
+
setNodeRef: (node: HTMLElement | null) => void;
|
|
51
|
+
}) => React.ReactNode);
|
|
52
|
+
className?: string;
|
|
53
|
+
disabled?: boolean;
|
|
54
|
+
}
|
|
55
|
+
declare function Droppable({ ref, children, className, id, disabled, ...props }: DroppableProps): React.JSX.Element;
|
|
56
|
+
export interface DraggableProps {
|
|
57
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
58
|
+
id: UniqueIdentifier;
|
|
59
|
+
children: React.ReactNode | ((args: {
|
|
60
|
+
attributes: DraggableAttributes;
|
|
61
|
+
listeners: SyntheticListenerMap | undefined;
|
|
62
|
+
setNodeRef: (node: HTMLElement | null) => void;
|
|
63
|
+
transform: {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
scaleX: number;
|
|
67
|
+
scaleY: number;
|
|
68
|
+
} | null;
|
|
69
|
+
isDragging: boolean;
|
|
70
|
+
}) => React.ReactNode);
|
|
71
|
+
className?: string;
|
|
72
|
+
disabled?: boolean;
|
|
73
|
+
}
|
|
74
|
+
declare function Draggable({ ref, children, className, id, disabled, ...props }: DraggableProps): React.JSX.Element;
|
|
75
|
+
export interface SortableContextProps {
|
|
76
|
+
id?: UniqueIdentifier;
|
|
77
|
+
items: UniqueIdentifier[] | {
|
|
78
|
+
id: UniqueIdentifier;
|
|
79
|
+
}[];
|
|
80
|
+
strategy?: SortingStrategy;
|
|
81
|
+
children: React.ReactNode;
|
|
82
|
+
}
|
|
83
|
+
declare function SortableContext({ children, items, strategy, }: SortableContextProps): React.JSX.Element;
|
|
84
|
+
export interface SortableItemProps {
|
|
85
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
86
|
+
id: UniqueIdentifier;
|
|
87
|
+
children: React.ReactNode | ((args: {
|
|
88
|
+
attributes: DraggableAttributes;
|
|
89
|
+
listeners: SyntheticListenerMap | undefined;
|
|
90
|
+
setNodeRef: (node: HTMLElement | null) => void;
|
|
91
|
+
transform: Transform | null;
|
|
92
|
+
transition: string | undefined;
|
|
93
|
+
isDragging: boolean;
|
|
94
|
+
isSorting: boolean;
|
|
95
|
+
isOver: boolean;
|
|
96
|
+
}) => React.ReactNode);
|
|
97
|
+
className?: string;
|
|
98
|
+
disabled?: boolean;
|
|
99
|
+
handle?: boolean;
|
|
100
|
+
}
|
|
101
|
+
declare function SortableItem({ ref, children, className, id, disabled, handle, ...props }: SortableItemProps): React.JSX.Element;
|
|
102
|
+
export interface SortableHandleProps {
|
|
103
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
104
|
+
children: React.ReactNode | ((listeners: SyntheticListenerMap | undefined) => React.ReactNode);
|
|
105
|
+
className?: string;
|
|
106
|
+
}
|
|
107
|
+
declare function SortableHandle({ ref, children, className, ...props }: SortableHandleProps): React.JSX.Element;
|
|
108
|
+
export interface DragOverlayComponentProps {
|
|
109
|
+
children: React.ReactNode;
|
|
110
|
+
className?: string;
|
|
111
|
+
dropAnimation?: {
|
|
112
|
+
duration?: number;
|
|
113
|
+
easing?: string;
|
|
114
|
+
} | null;
|
|
115
|
+
}
|
|
116
|
+
declare function DragOverlayComponent({ children, className, dropAnimation, ...props }: DragOverlayComponentProps): React.JSX.Element;
|
|
117
|
+
export type { Active, Collision, DragEndEvent as DropResult, DraggableAttributes, DraggableSyntheticListeners, DragOverEvent as DragUpdate, DragOverEvent, DragStartEvent as DragStart, Over, SortingStrategy, Transform, UniqueIdentifier, };
|
|
118
|
+
export { createDragDropSensors as useDragDropSensors, DragDropContext, Draggable, DragOverlayComponent, Droppable, KeyboardSensor, PointerSensor, SortableContext, SortableHandle, SortableItem, sortableKeyboardCoordinates, TouchSensor, useDraggable, useDroppable, useSensor, useSensors, useSortable, };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* DnD Component - Drag and Drop primitives for @dnd-kit
|
|
4
|
+
* Provides consistent wrappers with animations, sortable support, and drag overlays
|
|
5
|
+
*/
|
|
6
|
+
"use client";
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
import { closestCenter, closestCorners, DndContext as DndKitContext, DragOverlay, KeyboardSensor, PointerSensor, pointerWithin, rectIntersection, TouchSensor, useDraggable, useDroppable, useSensor, useSensors, } from "@dnd-kit/core";
|
|
9
|
+
import { restrictToFirstScrollableAncestor, restrictToHorizontalAxis, restrictToParentElement, restrictToVerticalAxis, restrictToWindowEdges, } from "@dnd-kit/modifiers";
|
|
10
|
+
import { horizontalListSortingStrategy, rectSortingStrategy, rectSwappingStrategy, SortableContext as SortableContextKit, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable";
|
|
11
|
+
import { CSS } from "@dnd-kit/utilities";
|
|
12
|
+
import { cn } from "@kala-ui/react/lib/utils";
|
|
13
|
+
import { useMergedRef } from "@kala-ui/react-hooks";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Collision Detection Algorithms
|
|
17
|
+
// ============================================================================
|
|
18
|
+
export const collisionDetectionAlgorithms = {
|
|
19
|
+
closestCenter,
|
|
20
|
+
closestCorners,
|
|
21
|
+
rectIntersection,
|
|
22
|
+
pointerWithin,
|
|
23
|
+
};
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Modifiers
|
|
26
|
+
// ============================================================================
|
|
27
|
+
export const modifiers = {
|
|
28
|
+
restrictToHorizontalAxis,
|
|
29
|
+
restrictToVerticalAxis,
|
|
30
|
+
restrictToWindowEdges,
|
|
31
|
+
restrictToParentElement,
|
|
32
|
+
restrictToFirstScrollableAncestor,
|
|
33
|
+
};
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Sorting Strategies
|
|
36
|
+
// ============================================================================
|
|
37
|
+
export const sortingStrategies = {
|
|
38
|
+
vertical: verticalListSortingStrategy,
|
|
39
|
+
horizontal: horizontalListSortingStrategy,
|
|
40
|
+
rect: rectSortingStrategy,
|
|
41
|
+
rectSwapping: rectSwappingStrategy,
|
|
42
|
+
};
|
|
43
|
+
function createDragDropSensors(options = {}) {
|
|
44
|
+
const { activationConstraint, keyboardCoordinateGetter = sortableKeyboardCoordinates, } = options;
|
|
45
|
+
const sensors = useSensors(useSensor(PointerSensor, {
|
|
46
|
+
activationConstraint,
|
|
47
|
+
}), useSensor(TouchSensor, {
|
|
48
|
+
activationConstraint,
|
|
49
|
+
}), useSensor(KeyboardSensor, {
|
|
50
|
+
coordinateGetter: keyboardCoordinateGetter,
|
|
51
|
+
}));
|
|
52
|
+
return sensors;
|
|
53
|
+
}
|
|
54
|
+
function DragDropContext({ children, sensors: sensorsProp, useSensors: useSensorsOptions, ...props }) {
|
|
55
|
+
const defaultSensors = createDragDropSensors(useSensorsOptions);
|
|
56
|
+
const sensors = sensorsProp ?? defaultSensors;
|
|
57
|
+
return (_jsx(DndKitContext, { sensors: sensors, ...props, children: children }));
|
|
58
|
+
}
|
|
59
|
+
function Droppable({ ref, children, className, id, disabled = false, ...props }) {
|
|
60
|
+
const { isOver, setNodeRef } = useDroppable({
|
|
61
|
+
id,
|
|
62
|
+
disabled,
|
|
63
|
+
});
|
|
64
|
+
const content = typeof children === "function"
|
|
65
|
+
? children({ isOver, setNodeRef })
|
|
66
|
+
: children;
|
|
67
|
+
return (_jsx("div", { ref: useMergedRef(ref, setNodeRef), className: cn(className), ...props, children: content }));
|
|
68
|
+
}
|
|
69
|
+
function Draggable({ ref, children, className, id, disabled = false, ...props }) {
|
|
70
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({
|
|
71
|
+
id,
|
|
72
|
+
disabled,
|
|
73
|
+
});
|
|
74
|
+
const content = typeof children === "function"
|
|
75
|
+
? children({ attributes, listeners, setNodeRef, transform, isDragging })
|
|
76
|
+
: children;
|
|
77
|
+
const style = transform
|
|
78
|
+
? {
|
|
79
|
+
transform: CSS.Translate.toString(transform),
|
|
80
|
+
}
|
|
81
|
+
: undefined;
|
|
82
|
+
return (_jsx("div", { ref: useMergedRef(ref, setNodeRef), style: style, className: cn(className), ...attributes, ...listeners, ...props, children: content }));
|
|
83
|
+
}
|
|
84
|
+
function SortableContext({ children, items, strategy = verticalListSortingStrategy, }) {
|
|
85
|
+
const itemIds = React.useMemo(() => items.map((item) => typeof item === "object" && "id" in item ? item.id : item), [items]);
|
|
86
|
+
return (_jsx(SortableContextKit, { items: itemIds, strategy: strategy, children: children }));
|
|
87
|
+
}
|
|
88
|
+
const SortableHandleContext = React.createContext(null);
|
|
89
|
+
function SortableItem({ ref, children, className, id, disabled = false, handle = false, ...props }) {
|
|
90
|
+
const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging, isSorting, isOver, } = useSortable({
|
|
91
|
+
id,
|
|
92
|
+
disabled,
|
|
93
|
+
});
|
|
94
|
+
const content = typeof children === "function"
|
|
95
|
+
? children({
|
|
96
|
+
attributes,
|
|
97
|
+
listeners,
|
|
98
|
+
setNodeRef,
|
|
99
|
+
transform,
|
|
100
|
+
transition,
|
|
101
|
+
isDragging,
|
|
102
|
+
isSorting,
|
|
103
|
+
isOver,
|
|
104
|
+
})
|
|
105
|
+
: children;
|
|
106
|
+
const style = {
|
|
107
|
+
transform: CSS.Transform.toString(transform),
|
|
108
|
+
transition,
|
|
109
|
+
zIndex: isDragging ? 9999 : undefined,
|
|
110
|
+
};
|
|
111
|
+
return (_jsx(SortableHandleContext.Provider, { value: { attributes, listeners, setActivatorNodeRef }, children: _jsx("div", { ref: useMergedRef(ref, setNodeRef), style: style, className: cn(className), ...attributes, ...(handle ? {} : listeners), ...props, children: content }) }));
|
|
112
|
+
}
|
|
113
|
+
function SortableHandle({ ref, children, className, ...props }) {
|
|
114
|
+
const sortable = React.useContext(SortableHandleContext);
|
|
115
|
+
if (!sortable && process.env.NODE_ENV !== "production") {
|
|
116
|
+
console.warn("SortableHandle must be rendered inside a SortableItem to receive drag listeners.");
|
|
117
|
+
}
|
|
118
|
+
const content = typeof children === "function" ? children(sortable?.listeners) : children;
|
|
119
|
+
return (_jsx("div", { ref: useMergedRef(ref, sortable?.setActivatorNodeRef), className: cn("cursor-grab active:cursor-grabbing", className), ...(sortable?.attributes ?? {}), ...(sortable?.listeners ?? {}), ...props, children: content }));
|
|
120
|
+
}
|
|
121
|
+
function DragOverlayComponent({ children, className, dropAnimation, ...props }) {
|
|
122
|
+
return (_jsx(DragOverlay, { dropAnimation: dropAnimation, ...props, children: children ? _jsx("div", { className: cn(className), children: children }) : null }));
|
|
123
|
+
}
|
|
124
|
+
export { createDragDropSensors as useDragDropSensors, DragDropContext, Draggable, DragOverlayComponent, Droppable,
|
|
125
|
+
// Re-export sensors
|
|
126
|
+
KeyboardSensor, PointerSensor, SortableContext, SortableHandle, SortableItem, sortableKeyboardCoordinates, TouchSensor,
|
|
127
|
+
// Re-export useful hooks
|
|
128
|
+
useDraggable, useDroppable, useSensor, useSensors, useSortable, };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DragDropContext, type DragDropContextProps, Draggable, type DraggableAttributes, type DraggableProps, type DragStart, type DragUpdate, Droppable, type DroppableProps, type DropResult, type UniqueIdentifier, } from "./dnd";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export interface FooterLink {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
}
|
|
6
|
+
export interface FooterLinkSection {
|
|
7
|
+
title: string;
|
|
8
|
+
links: FooterLink[];
|
|
9
|
+
}
|
|
10
|
+
export interface SocialLink {
|
|
11
|
+
name: string;
|
|
12
|
+
href: string;
|
|
13
|
+
icon: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
export interface FooterProps extends React.HTMLAttributes<HTMLElement>, React.RefAttributes<HTMLElement> {
|
|
16
|
+
/**
|
|
17
|
+
* Sections of links to display in columns
|
|
18
|
+
*/
|
|
19
|
+
linkSections?: FooterLinkSection[];
|
|
20
|
+
/**
|
|
21
|
+
* Social media links with icons
|
|
22
|
+
*/
|
|
23
|
+
socialLinks?: SocialLink[];
|
|
24
|
+
/**
|
|
25
|
+
* Copyright text
|
|
26
|
+
*/
|
|
27
|
+
copyright?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Additional content to display in footer (e.g., newsletter signup)
|
|
30
|
+
*/
|
|
31
|
+
children?: React.ReactNode;
|
|
32
|
+
/**
|
|
33
|
+
* Center align all content
|
|
34
|
+
*/
|
|
35
|
+
centered?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare function Footer({ ref, className, linkSections, socialLinks, copyright, children, centered, ...props }: FooterProps): React.JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from "@kala-ui/react/lib/utils";
|
|
4
|
+
export function Footer({ ref, className, linkSections = [], socialLinks = [], copyright, children, centered = false, ...props }) {
|
|
5
|
+
const currentYear = new Date().getFullYear();
|
|
6
|
+
const copyrightText = copyright || `© ${currentYear} All rights reserved.`;
|
|
7
|
+
return (_jsx("footer", { ref: ref, "data-comp": "footer", className: cn("w-full bg-muted text-muted-foreground border-t", className), ...props, children: _jsxs("div", { className: "container mx-auto px-4 py-8 md:py-12", children: [centered && children && (_jsx("div", { className: "mb-12 flex flex-col items-center text-center", children: children })), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 mb-8", children: [linkSections.map((section) => (_jsxs("nav", { "aria-label": `${section.title} links`, children: [_jsx("h3", { className: "text-foreground font-semibold text-sm uppercase tracking-wider mb-4", children: section.title }), _jsx("ul", { className: "space-y-2", children: section.links.map((link) => (_jsx("li", { children: _jsx("a", { href: link.href, className: "text-muted-foreground hover:text-primary transition-colors text-sm", children: link.label }) }, `${link.href}-${link.label}`))) })] }, section.title))), !centered && children && (_jsx("div", { className: "md:col-span-2 lg:col-span-1", children: children }))] }), _jsxs("div", { className: cn("border-t pt-8 flex flex-col gap-4", !centered && "md:flex-row md:justify-between md:items-center", centered && "items-center"), children: [_jsx("div", { className: "text-sm text-muted-foreground order-2 md:order-1", children: copyrightText }), socialLinks.length > 0 && (_jsx("div", { className: "flex items-center gap-4 order-1 md:order-2", children: socialLinks.map((social) => (_jsx("a", { href: social.href, target: "_blank", rel: "noopener noreferrer", className: "text-muted-foreground hover:text-primary transition-colors", "aria-label": social.name, children: social.icon }, social.href))) }))] })] }) }));
|
|
8
|
+
}
|