@tidecloak/ui-framework 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +377 -0
  2. package/dist/index.d.mts +2739 -0
  3. package/dist/index.d.ts +2739 -0
  4. package/dist/index.js +12869 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +12703 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +54 -0
  9. package/src/components/common/ActionButton.tsx +234 -0
  10. package/src/components/common/EmptyState.tsx +140 -0
  11. package/src/components/common/LoadingSkeleton.tsx +121 -0
  12. package/src/components/common/RefreshButton.tsx +127 -0
  13. package/src/components/common/StatusBadge.tsx +177 -0
  14. package/src/components/common/index.ts +31 -0
  15. package/src/components/data-table/DataTable.tsx +201 -0
  16. package/src/components/data-table/PaginatedTable.tsx +247 -0
  17. package/src/components/data-table/index.ts +2 -0
  18. package/src/components/dialogs/CollapsibleSection.tsx +184 -0
  19. package/src/components/dialogs/ConfirmDialog.tsx +264 -0
  20. package/src/components/dialogs/DetailDialog.tsx +228 -0
  21. package/src/components/dialogs/index.ts +3 -0
  22. package/src/components/index.ts +5 -0
  23. package/src/components/pages/base/ApprovalsPageBase.tsx +680 -0
  24. package/src/components/pages/base/LogsPageBase.tsx +581 -0
  25. package/src/components/pages/base/RolesPageBase.tsx +1470 -0
  26. package/src/components/pages/base/TemplatesPageBase.tsx +761 -0
  27. package/src/components/pages/base/UsersPageBase.tsx +843 -0
  28. package/src/components/pages/base/index.ts +58 -0
  29. package/src/components/pages/connected/ApprovalsPage.tsx +797 -0
  30. package/src/components/pages/connected/LogsPage.tsx +267 -0
  31. package/src/components/pages/connected/RolesPage.tsx +525 -0
  32. package/src/components/pages/connected/TemplatesPage.tsx +181 -0
  33. package/src/components/pages/connected/UsersPage.tsx +237 -0
  34. package/src/components/pages/connected/index.ts +36 -0
  35. package/src/components/pages/index.ts +5 -0
  36. package/src/components/tabs/TabsView.tsx +300 -0
  37. package/src/components/tabs/index.ts +1 -0
  38. package/src/components/ui/index.tsx +1001 -0
  39. package/src/hooks/index.ts +3 -0
  40. package/src/hooks/useAutoRefresh.ts +119 -0
  41. package/src/hooks/usePagination.ts +152 -0
  42. package/src/hooks/useSelection.ts +81 -0
  43. package/src/index.ts +256 -0
  44. package/src/theme.ts +185 -0
  45. package/src/tide/index.ts +19 -0
  46. package/src/tide/tidePolicy.ts +270 -0
  47. package/src/types/index.ts +484 -0
  48. package/src/utils/index.ts +121 -0
@@ -0,0 +1,3 @@
1
+ export { useAutoRefresh } from "./useAutoRefresh";
2
+ export { useSelection } from "./useSelection";
3
+ export { usePagination, type PageSizeMode, type UsePaginationOptions } from "./usePagination";
@@ -0,0 +1,119 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import type { AutoRefreshOptions, AutoRefreshReturn } from "../types";
3
+
4
+ /**
5
+ * Hook for auto-refreshing data at a configurable interval
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const { secondsRemaining, refreshNow, lastError, failureCount } = useAutoRefresh({
10
+ * intervalSeconds: 15,
11
+ * refresh: fetchData,
12
+ * isBlocked: isLoading,
13
+ * onError: (err) => console.error('Refresh failed:', err),
14
+ * maxRetries: 3,
15
+ * });
16
+ * ```
17
+ */
18
+ export function useAutoRefresh(options: AutoRefreshOptions): AutoRefreshReturn {
19
+ const {
20
+ intervalSeconds,
21
+ enabled = true,
22
+ refresh,
23
+ isBlocked = false,
24
+ onError,
25
+ maxRetries,
26
+ } = options;
27
+
28
+ const refreshRef = useRef(refresh);
29
+ refreshRef.current = refresh;
30
+
31
+ const onErrorRef = useRef(onError);
32
+ onErrorRef.current = onError;
33
+
34
+ const isBlockedRef = useRef(isBlocked);
35
+ useEffect(() => {
36
+ isBlockedRef.current = isBlocked;
37
+ }, [isBlocked]);
38
+
39
+ const [nextRefreshAt, setNextRefreshAt] = useState(() => Date.now() + intervalSeconds * 1000);
40
+ const [now, setNow] = useState(() => Date.now());
41
+ const [lastError, setLastError] = useState<Error | null>(null);
42
+ const [failureCount, setFailureCount] = useState(0);
43
+ const [isStopped, setIsStopped] = useState(false);
44
+
45
+ const secondsRemaining = useMemo(() => {
46
+ if (!enabled || isStopped) return null;
47
+ return Math.max(0, Math.ceil((nextRefreshAt - now) / 1000));
48
+ }, [enabled, isStopped, nextRefreshAt, now]);
49
+
50
+ // Update current time every 250ms
51
+ useEffect(() => {
52
+ if (!enabled || isStopped) return;
53
+ const id = window.setInterval(() => setNow(Date.now()), 250);
54
+ return () => window.clearInterval(id);
55
+ }, [enabled, isStopped]);
56
+
57
+ const runRefresh = useCallback(async () => {
58
+ if (!enabled || isStopped) return;
59
+ if (isBlockedRef.current) return;
60
+
61
+ try {
62
+ await refreshRef.current();
63
+ // Success - reset error state
64
+ setLastError(null);
65
+ setFailureCount(0);
66
+ } catch (err) {
67
+ const error = err instanceof Error ? err : new Error(String(err));
68
+ setLastError(error);
69
+ setFailureCount(prev => {
70
+ const newCount = prev + 1;
71
+ // Stop if max retries exceeded
72
+ if (maxRetries !== undefined && newCount >= maxRetries) {
73
+ setIsStopped(true);
74
+ }
75
+ return newCount;
76
+ });
77
+ // Call error callback
78
+ onErrorRef.current?.(error);
79
+ }
80
+
81
+ setNextRefreshAt(Date.now() + intervalSeconds * 1000);
82
+ }, [enabled, isStopped, intervalSeconds, maxRetries]);
83
+
84
+ // Trigger refresh when time is up
85
+ useEffect(() => {
86
+ if (!enabled || isStopped) return;
87
+ if (isBlockedRef.current) return;
88
+ if (now < nextRefreshAt) return;
89
+ void runRefresh();
90
+ }, [enabled, isStopped, now, nextRefreshAt, runRefresh]);
91
+
92
+ const refreshNow = useCallback(async () => {
93
+ // Allow manual refresh even if stopped
94
+ if (!enabled) return;
95
+ if (isBlockedRef.current) return;
96
+
97
+ try {
98
+ await refreshRef.current();
99
+ setLastError(null);
100
+ setFailureCount(0);
101
+ setIsStopped(false); // Resume auto-refresh on successful manual refresh
102
+ } catch (err) {
103
+ const error = err instanceof Error ? err : new Error(String(err));
104
+ setLastError(error);
105
+ onErrorRef.current?.(error);
106
+ }
107
+
108
+ setNextRefreshAt(Date.now() + intervalSeconds * 1000);
109
+ }, [enabled, intervalSeconds]);
110
+
111
+ const resetTimer = useCallback(() => {
112
+ setNextRefreshAt(Date.now() + intervalSeconds * 1000);
113
+ setIsStopped(false); // Also resume auto-refresh
114
+ setFailureCount(0);
115
+ setLastError(null);
116
+ }, [intervalSeconds]);
117
+
118
+ return { secondsRemaining, refreshNow, resetTimer, lastError, failureCount, isStopped };
119
+ }
@@ -0,0 +1,152 @@
1
+ import { useCallback, useMemo, useState, useEffect, useRef } from "react";
2
+ import type { PaginationReturn } from "../types";
3
+ import { computeRowsForViewportHeight } from "../utils";
4
+
5
+ export type PageSizeMode = "auto" | "manual";
6
+
7
+ export interface UsePaginationOptions {
8
+ /** Initial page (0-indexed) */
9
+ initialPage?: number;
10
+ /** Initial page size */
11
+ initialPageSize?: number;
12
+ /** Total number of items (for calculating total pages) */
13
+ totalItems?: number;
14
+ /** Whether to enable auto page size based on viewport */
15
+ autoPageSize?: boolean;
16
+ /** Ref to viewport element for auto sizing */
17
+ viewportRef?: React.RefObject<HTMLDivElement>;
18
+ /** Callback when page changes */
19
+ onPageChange?: (page: number) => void;
20
+ /** Callback when page size changes */
21
+ onPageSizeChange?: (pageSize: number) => void;
22
+ }
23
+
24
+ /**
25
+ * Hook for managing pagination state
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * const pagination = usePagination({
30
+ * initialPageSize: 25,
31
+ * totalItems: data.length,
32
+ * });
33
+ *
34
+ * // Use in query
35
+ * const { data } = useQuery({
36
+ * queryKey: ['items', pagination.page, pagination.pageSize],
37
+ * queryFn: () => fetchItems(pagination.pageSize, pagination.page * pagination.pageSize),
38
+ * });
39
+ * ```
40
+ */
41
+ export function usePagination(options: UsePaginationOptions = {}): PaginationReturn & {
42
+ pageSizeMode: PageSizeMode;
43
+ setPageSizeMode: (mode: PageSizeMode) => void;
44
+ pageSizeSelectValue: string;
45
+ onPageSizeSelect: (value: string) => void;
46
+ } {
47
+ const {
48
+ initialPage = 0,
49
+ initialPageSize = 25,
50
+ totalItems = 0,
51
+ autoPageSize = false,
52
+ viewportRef,
53
+ onPageChange,
54
+ onPageSizeChange,
55
+ } = options;
56
+
57
+ const [page, setPageState] = useState(initialPage);
58
+ const [pageSize, setPageSizeState] = useState(initialPageSize);
59
+ const [pageSizeMode, setPageSizeMode] = useState<PageSizeMode>(
60
+ autoPageSize ? "auto" : "manual"
61
+ );
62
+
63
+ // Handle auto page size with ResizeObserver
64
+ useEffect(() => {
65
+ if (pageSizeMode !== "auto" || !viewportRef?.current) return;
66
+
67
+ const el = viewportRef.current;
68
+ const ro = new ResizeObserver((entries) => {
69
+ const h = entries[0]?.contentRect.height ?? 0;
70
+ if (!h) return;
71
+ const newSize = computeRowsForViewportHeight(h);
72
+ setPageSizeState(newSize);
73
+ onPageSizeChange?.(newSize);
74
+ });
75
+
76
+ ro.observe(el);
77
+ return () => ro.disconnect();
78
+ }, [pageSizeMode, viewportRef, onPageSizeChange]);
79
+
80
+ const setPage = useCallback(
81
+ (newPage: number) => {
82
+ setPageState(newPage);
83
+ onPageChange?.(newPage);
84
+ },
85
+ [onPageChange]
86
+ );
87
+
88
+ const setPageSize = useCallback(
89
+ (newSize: number) => {
90
+ setPageSizeState(newSize);
91
+ onPageSizeChange?.(newSize);
92
+ // Reset to first page when page size changes
93
+ setPageState(0);
94
+ onPageChange?.(0);
95
+ },
96
+ [onPageChange, onPageSizeChange]
97
+ );
98
+
99
+ const totalPages = useMemo(
100
+ () => Math.max(1, Math.ceil(totalItems / pageSize)),
101
+ [totalItems, pageSize]
102
+ );
103
+
104
+ const nextPage = useCallback(() => {
105
+ const newPage = Math.min(page + 1, totalPages - 1);
106
+ setPage(newPage);
107
+ }, [page, totalPages, setPage]);
108
+
109
+ const prevPage = useCallback(() => {
110
+ const newPage = Math.max(0, page - 1);
111
+ setPage(newPage);
112
+ }, [page, setPage]);
113
+
114
+ const canNextPage = page < totalPages - 1;
115
+ const canPrevPage = page > 0;
116
+
117
+ const startIndex = page * pageSize;
118
+ const endIndex = Math.min(startIndex + pageSize, totalItems);
119
+
120
+ // For select component
121
+ const pageSizeSelectValue = pageSizeMode === "auto" ? "auto" : String(pageSize);
122
+
123
+ const onPageSizeSelect = useCallback(
124
+ (value: string) => {
125
+ if (value === "auto") {
126
+ setPageSizeMode("auto");
127
+ } else {
128
+ setPageSizeMode("manual");
129
+ setPageSize(parseInt(value, 10));
130
+ }
131
+ },
132
+ [setPageSize]
133
+ );
134
+
135
+ return {
136
+ page,
137
+ pageSize,
138
+ setPage,
139
+ setPageSize,
140
+ nextPage,
141
+ prevPage,
142
+ canNextPage,
143
+ canPrevPage,
144
+ totalPages,
145
+ startIndex,
146
+ endIndex,
147
+ pageSizeMode,
148
+ setPageSizeMode,
149
+ pageSizeSelectValue,
150
+ onPageSizeSelect,
151
+ };
152
+ }
@@ -0,0 +1,81 @@
1
+ import { useCallback, useState } from "react";
2
+ import type { BaseDataItem, SelectionReturn } from "../types";
3
+
4
+ /**
5
+ * Hook for managing multi-select state in tables
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const selection = useSelection<MyItem>();
10
+ *
11
+ * // Toggle single item
12
+ * selection.toggle(item.id);
13
+ *
14
+ * // Check if selected
15
+ * const isSelected = selection.isSelected(item.id);
16
+ *
17
+ * // Toggle all
18
+ * selection.toggleAll(items);
19
+ * ```
20
+ */
21
+ export function useSelection<T extends BaseDataItem>(): SelectionReturn<T> {
22
+ const [selectedIds, setSelectedIds] = useState<string[]>([]);
23
+
24
+ const isSelected = useCallback(
25
+ (id: string) => selectedIds.includes(id),
26
+ [selectedIds]
27
+ );
28
+
29
+ const toggle = useCallback((id: string) => {
30
+ setSelectedIds((prev) =>
31
+ prev.includes(id) ? prev.filter((p) => p !== id) : [...prev, id]
32
+ );
33
+ }, []);
34
+
35
+ const selectAll = useCallback((items: T[]) => {
36
+ setSelectedIds(items.map((item) => item.id));
37
+ }, []);
38
+
39
+ const clearAll = useCallback(() => {
40
+ setSelectedIds([]);
41
+ }, []);
42
+
43
+ const toggleAll = useCallback(
44
+ (items: T[]) => {
45
+ if (selectedIds.length === items.length) {
46
+ setSelectedIds([]);
47
+ } else {
48
+ setSelectedIds(items.map((item) => item.id));
49
+ }
50
+ },
51
+ [selectedIds.length]
52
+ );
53
+
54
+ const getSelected = useCallback(
55
+ (items: T[]) => items.filter((item) => selectedIds.includes(item.id)),
56
+ [selectedIds]
57
+ );
58
+
59
+ const allSelected = useCallback(
60
+ (items: T[]) => items.length > 0 && selectedIds.length === items.length,
61
+ [selectedIds.length]
62
+ );
63
+
64
+ const someSelected = useCallback(
65
+ (items: T[]) =>
66
+ selectedIds.length > 0 && selectedIds.length < items.length,
67
+ [selectedIds.length]
68
+ );
69
+
70
+ return {
71
+ selectedIds,
72
+ isSelected,
73
+ toggle,
74
+ selectAll,
75
+ clearAll,
76
+ toggleAll,
77
+ getSelected,
78
+ allSelected,
79
+ someSelected,
80
+ };
81
+ }
package/src/index.ts ADDED
@@ -0,0 +1,256 @@
1
+ // @tidecloak/ui-framework
2
+
3
+ // Theme (colors, spacing, CSS variables)
4
+ export {
5
+ colors,
6
+ spacing,
7
+ radius,
8
+ fontSize,
9
+ zIndex,
10
+ duration,
11
+ focusRing,
12
+ shadow,
13
+ themeCSS,
14
+ injectThemeCSS,
15
+ } from "./theme";
16
+
17
+ // Re-export react-query so users don't need to install it separately
18
+ export {
19
+ QueryClient,
20
+ QueryClientProvider,
21
+ useQuery,
22
+ useMutation,
23
+ useQueryClient,
24
+ type QueryClientConfig,
25
+ } from "@tanstack/react-query";
26
+
27
+ // Types
28
+ export * from "./types";
29
+
30
+ // Utils
31
+ export {
32
+ cn,
33
+ base64ToBytes,
34
+ bytesToBase64,
35
+ formatTimestamp,
36
+ formatLogTimestamp,
37
+ computeRowsForViewportHeight,
38
+ delay,
39
+ safeJsonParse,
40
+ truncate,
41
+ isDefined,
42
+ generateId,
43
+ } from "./utils";
44
+
45
+ // Hooks
46
+ export {
47
+ useAutoRefresh,
48
+ useSelection,
49
+ usePagination,
50
+ type PageSizeMode,
51
+ type UsePaginationOptions,
52
+ } from "./hooks";
53
+
54
+ // Common Components
55
+ export {
56
+ RefreshButton,
57
+ StatusBadge,
58
+ STATUS_COLORS,
59
+ STATUS_ICONS,
60
+ getStatusConfig,
61
+ ActionButton,
62
+ ActionButtonGroup,
63
+ ACTION_COLORS,
64
+ ACTION_ICONS,
65
+ EmptyState,
66
+ EmptyStateNoData,
67
+ EmptyStateNoResults,
68
+ EmptyStateNoFiles,
69
+ Skeleton,
70
+ LoadingSkeleton,
71
+ TableRowSkeleton,
72
+ type RefreshButtonProps,
73
+ type StatusBadgeProps,
74
+ type ActionButtonProps,
75
+ type ActionButtonGroupProps,
76
+ type ActionType,
77
+ type EmptyStateProps,
78
+ type SkeletonProps,
79
+ type LoadingSkeletonProps,
80
+ } from "./components/common";
81
+
82
+ // Data Table Components
83
+ export {
84
+ DataTable,
85
+ PaginatedTable,
86
+ type DataTableProps,
87
+ type PaginatedTableProps,
88
+ } from "./components/data-table";
89
+
90
+ // Tabs Components
91
+ export {
92
+ TabsView,
93
+ type TabsViewProps,
94
+ } from "./components/tabs";
95
+
96
+ // Dialog Components
97
+ export {
98
+ DetailDialog,
99
+ ConfirmDialog,
100
+ CollapsibleSection,
101
+ CodePreview,
102
+ type DetailDialogProps,
103
+ type ConfirmDialogProps,
104
+ type CollapsibleSectionProps,
105
+ } from "./components/dialogs";
106
+
107
+ // Page Components - Base (headless)
108
+ export {
109
+ // Approvals
110
+ ApprovalsPageBase,
111
+ type ApprovalsPageBaseProps,
112
+ type ApprovalTabConfig,
113
+ type BaseApprovalItem,
114
+ type AccessApprovalItem,
115
+ type RoleApprovalItem,
116
+ type PolicyApprovalItem,
117
+ type ApprovalDecision,
118
+ createUserColumn,
119
+ createRoleColumn,
120
+ createTimestampColumn,
121
+ createStatusColumn,
122
+ createProgressColumn,
123
+ // Logs
124
+ LogsPageBase,
125
+ type LogsPageBaseProps,
126
+ type LogsTabConfig,
127
+ type BaseLogItem,
128
+ type AccessLogItem,
129
+ type PolicyLogItem,
130
+ createLogTimestampColumn,
131
+ createActionColumn,
132
+ createLogProgressColumn,
133
+ // Templates
134
+ TemplatesPageBase,
135
+ type TemplatesPageBaseProps,
136
+ type PolicyTemplateItem,
137
+ type TemplateParameter,
138
+ type TemplateFormData,
139
+ // Users
140
+ UsersPageBase,
141
+ type UsersPageBaseProps,
142
+ type UserItem,
143
+ type UserRoleItem,
144
+ type UserFormData,
145
+ type CreateUserFormData,
146
+ // Roles
147
+ RolesPageBase,
148
+ type RolesPageBaseProps,
149
+ type RoleItem,
150
+ type PolicyConfig,
151
+ type RolePolicyTemplateItem,
152
+ type RoleTemplateParameter,
153
+ } from "./components/pages";
154
+
155
+ // Page Components - Pre-made (uses @tidecloak/react)
156
+ export {
157
+ RolesPage,
158
+ UsersPage,
159
+ TemplatesPage,
160
+ LogsPage,
161
+ ApprovalsPage,
162
+ // Template API helpers
163
+ createLocalStorageTemplateAPI,
164
+ // Policy API helpers
165
+ createLocalStoragePolicyAPI,
166
+ // Policy Approvals API helpers
167
+ createLocalStoragePolicyApprovalsAPI,
168
+ // Access Metadata API helpers
169
+ createLocalStorageAccessMetadataAPI,
170
+ // Policy Logs API helpers
171
+ createLocalStoragePolicyLogsAPI,
172
+ type RolesPageProps,
173
+ type UsersPageProps,
174
+ type ChangeSetInfo,
175
+ type TemplatesPageProps,
176
+ type TemplateAPI,
177
+ type PolicyAPI,
178
+ type RolePolicy,
179
+ type LogsPageProps,
180
+ type PolicyLogsAPI,
181
+ type PolicyLogData,
182
+ type ApprovalsPageProps,
183
+ type PolicyApprovalsAPI,
184
+ type PolicyApprovalData,
185
+ type AccessMetadataAPI,
186
+ type AccessMetadataRecord,
187
+ type TideApprovalContext,
188
+ type TideApprovalResult,
189
+ type EnclaveApprovalTabConfig,
190
+ } from "./components/pages/connected";
191
+
192
+ // Tide Policy Workflow Helpers
193
+ export {
194
+ loadTideLibs,
195
+ areTideLibsAvailable,
196
+ computeContractId,
197
+ createTidePolicyRequest,
198
+ createTidePolicyHandler,
199
+ rolePolicyToTideConfig,
200
+ DEFAULT_MODEL_IDS,
201
+ DEFAULT_ROLE_CONTRACT,
202
+ type TidePolicyConfig,
203
+ type TideContextMethods,
204
+ } from "./tide";
205
+
206
+ // Default UI Components (works out of the box with inline styles)
207
+ export {
208
+ defaultComponents,
209
+ Card,
210
+ CardContent,
211
+ CardHeader,
212
+ CardTitle,
213
+ CardDescription,
214
+ Button,
215
+ Badge,
216
+ Input,
217
+ Label,
218
+ Textarea,
219
+ Checkbox,
220
+ Select,
221
+ SelectTrigger,
222
+ SelectValue,
223
+ SelectContent,
224
+ SelectItem,
225
+ Skeleton as UIComponentSkeleton,
226
+ Switch,
227
+ ScrollArea,
228
+ Alert,
229
+ AlertDescription,
230
+ CodeEditor,
231
+ Table,
232
+ TableHeader,
233
+ TableBody,
234
+ TableRow,
235
+ TableHead,
236
+ TableCell,
237
+ Dialog,
238
+ DialogContent,
239
+ DialogHeader,
240
+ DialogTitle,
241
+ DialogDescription,
242
+ DialogFooter,
243
+ AlertDialog,
244
+ AlertDialogContent,
245
+ AlertDialogHeader,
246
+ AlertDialogTitle,
247
+ AlertDialogDescription,
248
+ AlertDialogFooter,
249
+ AlertDialogCancel,
250
+ AlertDialogAction,
251
+ Separator,
252
+ Tabs,
253
+ TabsList,
254
+ TabsTrigger,
255
+ TabsContent,
256
+ } from "./components/ui";