@superdangerous/app-framework 4.14.0 → 4.15.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.
Files changed (36) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/middleware/validation.d.ts +12 -12
  6. package/dist/services/emailService.d.ts +146 -0
  7. package/dist/services/emailService.d.ts.map +1 -0
  8. package/dist/services/emailService.js +649 -0
  9. package/dist/services/emailService.js.map +1 -0
  10. package/dist/services/index.d.ts +2 -0
  11. package/dist/services/index.d.ts.map +1 -1
  12. package/dist/services/index.js +2 -0
  13. package/dist/services/index.js.map +1 -1
  14. package/package.json +9 -1
  15. package/src/index.ts +14 -0
  16. package/src/services/emailService.ts +812 -0
  17. package/src/services/index.ts +14 -0
  18. package/ui/data-table/components/BatchActionsBar.tsx +53 -0
  19. package/ui/data-table/components/ColumnVisibility.tsx +111 -0
  20. package/ui/data-table/components/DataTablePage.tsx +238 -0
  21. package/ui/data-table/components/Pagination.tsx +203 -0
  22. package/ui/data-table/components/PaginationControls.tsx +122 -0
  23. package/ui/data-table/components/TableFilters.tsx +139 -0
  24. package/ui/data-table/components/index.ts +27 -0
  25. package/ui/data-table/hooks/index.ts +17 -0
  26. package/ui/data-table/hooks/useColumnOrder.ts +233 -0
  27. package/ui/data-table/hooks/useColumnVisibility.ts +128 -0
  28. package/ui/data-table/hooks/usePagination.ts +160 -0
  29. package/ui/data-table/hooks/useResizableColumns.ts +280 -0
  30. package/ui/data-table/index.ts +74 -0
  31. package/ui/dist/index.d.mts +207 -5
  32. package/ui/dist/index.d.ts +207 -5
  33. package/ui/dist/index.js +36 -43
  34. package/ui/dist/index.js.map +1 -1
  35. package/ui/dist/index.mjs +36 -43
  36. package/ui/dist/index.mjs.map +1 -1
@@ -0,0 +1,160 @@
1
+ import { useState, useMemo, useCallback, useEffect } from 'react';
2
+
3
+ interface UsePaginationOptions<T> {
4
+ data: T[];
5
+ pageSize?: number;
6
+ storageKey?: string;
7
+ }
8
+
9
+ interface PaginationState {
10
+ page: number;
11
+ pageSize: number;
12
+ }
13
+
14
+ interface UsePaginationResult<T> {
15
+ // Paginated data
16
+ paginatedData: T[];
17
+
18
+ // Current state
19
+ page: number;
20
+ pageSize: number;
21
+ totalPages: number;
22
+ totalItems: number;
23
+
24
+ // Navigation
25
+ setPage: (page: number) => void;
26
+ setPageSize: (size: number) => void;
27
+ nextPage: () => void;
28
+ prevPage: () => void;
29
+ firstPage: () => void;
30
+ lastPage: () => void;
31
+
32
+ // Info
33
+ startIndex: number;
34
+ endIndex: number;
35
+ canGoNext: boolean;
36
+ canGoPrev: boolean;
37
+
38
+ // Page size options
39
+ pageSizeOptions: number[];
40
+ }
41
+
42
+ const DEFAULT_PAGE_SIZE = 25;
43
+ const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
44
+
45
+ export function usePagination<T>({
46
+ data,
47
+ pageSize: initialPageSize = DEFAULT_PAGE_SIZE,
48
+ storageKey,
49
+ }: UsePaginationOptions<T>): UsePaginationResult<T> {
50
+ // Initialize state from localStorage if available
51
+ const [state, setState] = useState<PaginationState>(() => {
52
+ if (storageKey) {
53
+ try {
54
+ const stored = localStorage.getItem(`pagination-${storageKey}`);
55
+ if (stored) {
56
+ const parsed = JSON.parse(stored);
57
+ return {
58
+ page: 1, // Always start on first page
59
+ pageSize: parsed.pageSize || initialPageSize,
60
+ };
61
+ }
62
+ } catch {
63
+ // Ignore localStorage errors
64
+ }
65
+ }
66
+ return {
67
+ page: 1,
68
+ pageSize: initialPageSize,
69
+ };
70
+ });
71
+
72
+ const { page, pageSize } = state;
73
+
74
+ // Calculate total pages
75
+ const totalPages = useMemo(
76
+ () => Math.max(1, Math.ceil(data.length / pageSize)),
77
+ [data.length, pageSize]
78
+ );
79
+
80
+ // Reset to first page if current page is out of bounds
81
+ useEffect(() => {
82
+ if (page > totalPages) {
83
+ setState((prev) => ({ ...prev, page: Math.max(1, totalPages) }));
84
+ }
85
+ }, [totalPages, page]);
86
+
87
+ // Persist pageSize to localStorage
88
+ useEffect(() => {
89
+ if (storageKey) {
90
+ try {
91
+ localStorage.setItem(
92
+ `pagination-${storageKey}`,
93
+ JSON.stringify({ pageSize })
94
+ );
95
+ } catch {
96
+ // Ignore localStorage errors
97
+ }
98
+ }
99
+ }, [pageSize, storageKey]);
100
+
101
+ // Calculate start and end indices
102
+ const startIndex = (page - 1) * pageSize;
103
+ const endIndex = Math.min(startIndex + pageSize, data.length);
104
+
105
+ // Get paginated data
106
+ const paginatedData = useMemo(
107
+ () => data.slice(startIndex, endIndex),
108
+ [data, startIndex, endIndex]
109
+ );
110
+
111
+ // Navigation functions
112
+ const setPage = useCallback((newPage: number) => {
113
+ setState((prev) => ({
114
+ ...prev,
115
+ page: Math.max(1, Math.min(newPage, totalPages)),
116
+ }));
117
+ }, [totalPages]);
118
+
119
+ const setPageSize = useCallback((newSize: number) => {
120
+ setState({
121
+ page: 1, // Reset to first page when changing page size
122
+ pageSize: newSize,
123
+ });
124
+ }, []);
125
+
126
+ const nextPage = useCallback(() => {
127
+ setPage(page + 1);
128
+ }, [page, setPage]);
129
+
130
+ const prevPage = useCallback(() => {
131
+ setPage(page - 1);
132
+ }, [page, setPage]);
133
+
134
+ const firstPage = useCallback(() => {
135
+ setPage(1);
136
+ }, [setPage]);
137
+
138
+ const lastPage = useCallback(() => {
139
+ setPage(totalPages);
140
+ }, [totalPages, setPage]);
141
+
142
+ return {
143
+ paginatedData,
144
+ page,
145
+ pageSize,
146
+ totalPages,
147
+ totalItems: data.length,
148
+ setPage,
149
+ setPageSize,
150
+ nextPage,
151
+ prevPage,
152
+ firstPage,
153
+ lastPage,
154
+ startIndex: startIndex + 1, // 1-indexed for display
155
+ endIndex,
156
+ canGoNext: page < totalPages,
157
+ canGoPrev: page > 1,
158
+ pageSizeOptions: PAGE_SIZE_OPTIONS,
159
+ };
160
+ }
@@ -0,0 +1,280 @@
1
+ import { useState, useCallback, useEffect, useRef } from 'react';
2
+
3
+ interface ColumnConfig {
4
+ key: string;
5
+ minWidth?: number;
6
+ maxWidth?: number;
7
+ defaultWidth?: number;
8
+ }
9
+
10
+ interface UseResizableColumnsOptions {
11
+ tableId: string;
12
+ columns: ColumnConfig[];
13
+ storageKey?: string;
14
+ }
15
+
16
+ interface ResizableColumnState {
17
+ widths: Record<string, number>;
18
+ isResizing: boolean;
19
+ resizingColumn: string | null;
20
+ }
21
+
22
+ export interface ResizableColumnResult {
23
+ widths: Record<string, number>;
24
+ isResizing: boolean;
25
+ totalWidth: number;
26
+ getResizeHandleProps: (columnKey: string) => {
27
+ onPointerDown: (e: React.PointerEvent) => void;
28
+ onMouseDown: (e: React.MouseEvent) => void;
29
+ draggable: boolean;
30
+ onDragStart: (e: React.DragEvent) => void;
31
+ className: string;
32
+ 'data-resizing': boolean;
33
+ };
34
+ getColumnStyle: (columnKey: string) => React.CSSProperties;
35
+ getTableStyle: () => React.CSSProperties;
36
+ resetToDefaults: () => void;
37
+ }
38
+
39
+ const DEFAULT_MIN_WIDTH = 50;
40
+ const DEFAULT_WIDTH = 150;
41
+ const DRAG_THRESHOLD = 3; // Minimum pixels before resize activates
42
+
43
+ export function useResizableColumns({
44
+ tableId,
45
+ columns,
46
+ storageKey,
47
+ }: UseResizableColumnsOptions): ResizableColumnResult {
48
+ const effectiveStorageKey = storageKey || `table-columns-${tableId}`;
49
+
50
+ // Get default widths from column config
51
+ const getDefaultWidths = useCallback(() => {
52
+ return columns.reduce((acc, col) => {
53
+ acc[col.key] = col.defaultWidth || DEFAULT_WIDTH;
54
+ return acc;
55
+ }, {} as Record<string, number>);
56
+ }, [columns]);
57
+
58
+ // Initialize widths from localStorage or defaults
59
+ const [state, setState] = useState<ResizableColumnState>(() => {
60
+ try {
61
+ const stored = localStorage.getItem(effectiveStorageKey);
62
+ if (stored) {
63
+ const parsed = JSON.parse(stored);
64
+ // Merge with defaults in case new columns were added
65
+ const defaults = getDefaultWidths();
66
+ return {
67
+ widths: { ...defaults, ...parsed },
68
+ isResizing: false,
69
+ resizingColumn: null,
70
+ };
71
+ }
72
+ } catch {
73
+ // Ignore localStorage errors
74
+ }
75
+ return {
76
+ widths: getDefaultWidths(),
77
+ isResizing: false,
78
+ resizingColumn: null,
79
+ };
80
+ });
81
+
82
+ // Refs for resize handling - these are stable across renders
83
+ const startXRef = useRef<number>(0);
84
+ const startWidthRef = useRef<number>(0);
85
+ const activeColumnRef = useRef<string | null>(null);
86
+ const hasDraggedRef = useRef<boolean>(false);
87
+ const columnsRef = useRef(columns);
88
+
89
+ // Keep columnsRef up to date
90
+ useEffect(() => {
91
+ columnsRef.current = columns;
92
+ }, [columns]);
93
+
94
+ // Get column config by key - uses ref for stable reference
95
+ const getColumnConfig = useCallback(
96
+ (key: string) => columnsRef.current.find((c) => c.key === key),
97
+ []
98
+ );
99
+
100
+ // Persist widths to localStorage
101
+ useEffect(() => {
102
+ try {
103
+ localStorage.setItem(effectiveStorageKey, JSON.stringify(state.widths));
104
+ } catch {
105
+ // Ignore localStorage errors
106
+ }
107
+ }, [state.widths, effectiveStorageKey]);
108
+
109
+ // Stable event handlers using refs - these don't change between renders
110
+ const handlersRef = useRef<{
111
+ onPointerMove: (e: PointerEvent) => void;
112
+ onPointerUp: (e: PointerEvent) => void;
113
+ } | null>(null);
114
+
115
+ // Initialize handlers once
116
+ if (!handlersRef.current) {
117
+ handlersRef.current = {
118
+ onPointerMove: (e: PointerEvent) => {
119
+ if (!activeColumnRef.current) return;
120
+
121
+ const diff = e.clientX - startXRef.current;
122
+
123
+ // Check if we've exceeded the drag threshold
124
+ if (!hasDraggedRef.current) {
125
+ if (Math.abs(diff) < DRAG_THRESHOLD) {
126
+ return; // Not yet dragging
127
+ }
128
+ // Now we're actually dragging - set the resizing state
129
+ hasDraggedRef.current = true;
130
+ setState((prev) => ({
131
+ ...prev,
132
+ isResizing: true,
133
+ resizingColumn: activeColumnRef.current,
134
+ }));
135
+ document.body.style.cursor = 'col-resize';
136
+ document.body.style.userSelect = 'none';
137
+ }
138
+
139
+ const config = getColumnConfig(activeColumnRef.current);
140
+ const minWidth = config?.minWidth || DEFAULT_MIN_WIDTH;
141
+ const maxWidth = config?.maxWidth;
142
+
143
+ let newWidth = Math.max(minWidth, startWidthRef.current + diff);
144
+ if (maxWidth) {
145
+ newWidth = Math.min(maxWidth, newWidth);
146
+ }
147
+
148
+ setState((prev) => ({
149
+ ...prev,
150
+ widths: {
151
+ ...prev.widths,
152
+ [activeColumnRef.current!]: newWidth,
153
+ },
154
+ }));
155
+ },
156
+
157
+ onPointerUp: () => {
158
+ activeColumnRef.current = null;
159
+ hasDraggedRef.current = false;
160
+ setState((prev) => ({
161
+ ...prev,
162
+ isResizing: false,
163
+ resizingColumn: null,
164
+ }));
165
+
166
+ // Remove listeners using the same stable references
167
+ document.removeEventListener('pointermove', handlersRef.current!.onPointerMove);
168
+ document.removeEventListener('pointerup', handlersRef.current!.onPointerUp);
169
+ document.removeEventListener('pointercancel', handlersRef.current!.onPointerUp);
170
+
171
+ // Remove cursor override
172
+ document.body.style.cursor = '';
173
+ document.body.style.userSelect = '';
174
+ },
175
+ };
176
+ }
177
+
178
+ // Cleanup on unmount
179
+ useEffect(() => {
180
+ return () => {
181
+ if (handlersRef.current) {
182
+ document.removeEventListener('pointermove', handlersRef.current.onPointerMove);
183
+ document.removeEventListener('pointerup', handlersRef.current.onPointerUp);
184
+ document.removeEventListener('pointercancel', handlersRef.current.onPointerUp);
185
+ }
186
+ document.body.style.cursor = '';
187
+ document.body.style.userSelect = '';
188
+ };
189
+ }, []);
190
+
191
+ // Ref to access current widths without causing re-renders
192
+ const widthsRef = useRef(state.widths);
193
+ useEffect(() => {
194
+ widthsRef.current = state.widths;
195
+ }, [state.widths]);
196
+
197
+ // Start resize - uses stable handler refs
198
+ const startResize = useCallback(
199
+ (columnKey: string, clientX: number) => {
200
+ startXRef.current = clientX;
201
+ startWidthRef.current = widthsRef.current[columnKey] || DEFAULT_WIDTH;
202
+ activeColumnRef.current = columnKey;
203
+ hasDraggedRef.current = false;
204
+
205
+ // Add listeners using stable references - use pointer events consistently
206
+ document.addEventListener('pointermove', handlersRef.current!.onPointerMove);
207
+ document.addEventListener('pointerup', handlersRef.current!.onPointerUp);
208
+ document.addEventListener('pointercancel', handlersRef.current!.onPointerUp);
209
+ },
210
+ []
211
+ );
212
+
213
+ // Get resize handle props for a column
214
+ const getResizeHandleProps = useCallback(
215
+ (columnKey: string) => ({
216
+ onPointerDown: (e: React.PointerEvent) => {
217
+ e.preventDefault();
218
+ e.stopPropagation();
219
+ startResize(columnKey, e.clientX);
220
+ },
221
+ // Prevent other events from interfering
222
+ onMouseDown: (e: React.MouseEvent) => {
223
+ e.preventDefault();
224
+ e.stopPropagation();
225
+ },
226
+ draggable: false,
227
+ onDragStart: (e: React.DragEvent) => {
228
+ e.preventDefault();
229
+ e.stopPropagation();
230
+ },
231
+ className: 'resize-handle',
232
+ 'data-resizing': state.resizingColumn === columnKey,
233
+ }),
234
+ [startResize, state.resizingColumn]
235
+ );
236
+
237
+ // Get column style with width
238
+ const getColumnStyle = useCallback(
239
+ (columnKey: string): React.CSSProperties => {
240
+ const currentWidth = state.widths[columnKey] || DEFAULT_WIDTH;
241
+ return {
242
+ width: currentWidth,
243
+ minWidth: currentWidth,
244
+ position: 'relative',
245
+ };
246
+ },
247
+ [state.widths]
248
+ );
249
+
250
+ // Calculate total table width from all columns
251
+ const totalWidth = columns.reduce((sum, col) => {
252
+ return sum + (state.widths[col.key] || col.defaultWidth || DEFAULT_WIDTH);
253
+ }, 0);
254
+
255
+ // Get table style
256
+ const getTableStyle = useCallback(
257
+ (): React.CSSProperties => ({
258
+ minWidth: totalWidth,
259
+ }),
260
+ [totalWidth]
261
+ );
262
+
263
+ // Reset all columns to default widths
264
+ const resetToDefaults = useCallback(() => {
265
+ setState((prev) => ({
266
+ ...prev,
267
+ widths: getDefaultWidths(),
268
+ }));
269
+ }, [getDefaultWidths]);
270
+
271
+ return {
272
+ widths: state.widths,
273
+ isResizing: state.isResizing,
274
+ totalWidth,
275
+ getResizeHandleProps,
276
+ getColumnStyle,
277
+ getTableStyle,
278
+ resetToDefaults,
279
+ };
280
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @superdangerous/app-framework/data-table
3
+ *
4
+ * A comprehensive data table module providing hooks and components
5
+ * for building enterprise-grade data tables with:
6
+ *
7
+ * - Pagination with localStorage persistence
8
+ * - Column visibility toggling with locked columns
9
+ * - Column resizing with drag handles
10
+ * - Column reordering with drag-and-drop
11
+ * - Batch selection and actions
12
+ * - Search and filter controls
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * import {
17
+ * usePagination,
18
+ * useColumnVisibility,
19
+ * DataTablePage,
20
+ * BatchActionsBar
21
+ * } from '@superdangerous/app-framework/data-table';
22
+ *
23
+ * function MyTable() {
24
+ * const pagination = usePagination({ data: items, storageKey: 'my-table' });
25
+ * const columnVisibility = useColumnVisibility({ columns, storageKey: 'my-table-cols' });
26
+ *
27
+ * return (
28
+ * <DataTablePage
29
+ * title="Items"
30
+ * pagination={pagination}
31
+ * // ...
32
+ * >
33
+ * <table>...</table>
34
+ * </DataTablePage>
35
+ * );
36
+ * }
37
+ * ```
38
+ */
39
+
40
+ // Hooks
41
+ export {
42
+ usePagination,
43
+ useColumnVisibility,
44
+ useResizableColumns,
45
+ useColumnOrder,
46
+ useColumnDragDrop,
47
+ } from './hooks';
48
+
49
+ export type {
50
+ ColumnConfig,
51
+ ColumnVisibilityState,
52
+ ResizableColumnResult,
53
+ ColumnOrderConfig,
54
+ DragState,
55
+ } from './hooks';
56
+
57
+ // Components
58
+ export {
59
+ DataTablePage,
60
+ PaginationControls,
61
+ Pagination,
62
+ BatchActionsBar,
63
+ ColumnVisibility,
64
+ TableFilters,
65
+ } from './components';
66
+
67
+ export type {
68
+ DataTablePageProps,
69
+ FilterOption,
70
+ PaginationControlsProps,
71
+ BatchActionsBarProps,
72
+ TableFiltersProps,
73
+ TableFilterOption,
74
+ } from './components';