@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.
- package/README.md +377 -0
- package/dist/index.d.mts +2739 -0
- package/dist/index.d.ts +2739 -0
- package/dist/index.js +12869 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +12703 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +54 -0
- package/src/components/common/ActionButton.tsx +234 -0
- package/src/components/common/EmptyState.tsx +140 -0
- package/src/components/common/LoadingSkeleton.tsx +121 -0
- package/src/components/common/RefreshButton.tsx +127 -0
- package/src/components/common/StatusBadge.tsx +177 -0
- package/src/components/common/index.ts +31 -0
- package/src/components/data-table/DataTable.tsx +201 -0
- package/src/components/data-table/PaginatedTable.tsx +247 -0
- package/src/components/data-table/index.ts +2 -0
- package/src/components/dialogs/CollapsibleSection.tsx +184 -0
- package/src/components/dialogs/ConfirmDialog.tsx +264 -0
- package/src/components/dialogs/DetailDialog.tsx +228 -0
- package/src/components/dialogs/index.ts +3 -0
- package/src/components/index.ts +5 -0
- package/src/components/pages/base/ApprovalsPageBase.tsx +680 -0
- package/src/components/pages/base/LogsPageBase.tsx +581 -0
- package/src/components/pages/base/RolesPageBase.tsx +1470 -0
- package/src/components/pages/base/TemplatesPageBase.tsx +761 -0
- package/src/components/pages/base/UsersPageBase.tsx +843 -0
- package/src/components/pages/base/index.ts +58 -0
- package/src/components/pages/connected/ApprovalsPage.tsx +797 -0
- package/src/components/pages/connected/LogsPage.tsx +267 -0
- package/src/components/pages/connected/RolesPage.tsx +525 -0
- package/src/components/pages/connected/TemplatesPage.tsx +181 -0
- package/src/components/pages/connected/UsersPage.tsx +237 -0
- package/src/components/pages/connected/index.ts +36 -0
- package/src/components/pages/index.ts +5 -0
- package/src/components/tabs/TabsView.tsx +300 -0
- package/src/components/tabs/index.ts +1 -0
- package/src/components/ui/index.tsx +1001 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAutoRefresh.ts +119 -0
- package/src/hooks/usePagination.ts +152 -0
- package/src/hooks/useSelection.ts +81 -0
- package/src/index.ts +256 -0
- package/src/theme.ts +185 -0
- package/src/tide/index.ts +19 -0
- package/src/tide/tidePolicy.ts +270 -0
- package/src/types/index.ts +484 -0
- package/src/utils/index.ts +121 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// CORE TYPES - Generic data structures for the UI framework
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// BASE DATA TYPES
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Base interface for all data items - requires an id
|
|
13
|
+
*/
|
|
14
|
+
export interface BaseDataItem {
|
|
15
|
+
id: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generic change set request structure for approval workflows
|
|
20
|
+
*/
|
|
21
|
+
export interface ChangeSetRequest {
|
|
22
|
+
changeSetId: string;
|
|
23
|
+
changeSetType: string;
|
|
24
|
+
actionType: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Base interface for items that can be approved
|
|
29
|
+
*/
|
|
30
|
+
export interface ApprovableItem extends BaseDataItem {
|
|
31
|
+
status: string;
|
|
32
|
+
retrievalInfo?: ChangeSetRequest;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// TABLE TYPES
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Column definition for data tables
|
|
41
|
+
* Note: T can be any type with the properties needed for rendering
|
|
42
|
+
*/
|
|
43
|
+
export interface ColumnDef<T = any> {
|
|
44
|
+
/** Unique key for the column */
|
|
45
|
+
key: string;
|
|
46
|
+
/** Display header */
|
|
47
|
+
header: string | ReactNode;
|
|
48
|
+
/** Cell renderer */
|
|
49
|
+
cell: (item: T, index: number) => ReactNode;
|
|
50
|
+
/** Optional sorting key (if different from key) */
|
|
51
|
+
sortKey?: keyof T | string;
|
|
52
|
+
/** Enable sorting for this column */
|
|
53
|
+
sortable?: boolean;
|
|
54
|
+
/** Responsive visibility */
|
|
55
|
+
responsive?: "sm" | "md" | "lg" | "xl" | "always";
|
|
56
|
+
/** Column width class */
|
|
57
|
+
width?: string;
|
|
58
|
+
/** Alignment */
|
|
59
|
+
align?: "left" | "center" | "right";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Table action definition
|
|
64
|
+
*/
|
|
65
|
+
export interface TableAction<T extends BaseDataItem> {
|
|
66
|
+
/** Action identifier */
|
|
67
|
+
key: string;
|
|
68
|
+
/** Display label */
|
|
69
|
+
label: string;
|
|
70
|
+
/** Icon component */
|
|
71
|
+
icon?: ReactNode;
|
|
72
|
+
/** Action handler */
|
|
73
|
+
onClick: (item: T) => void | Promise<void>;
|
|
74
|
+
/** Visibility condition */
|
|
75
|
+
visible?: (item: T) => boolean;
|
|
76
|
+
/** Disabled condition */
|
|
77
|
+
disabled?: (item: T) => boolean;
|
|
78
|
+
/** Button variant */
|
|
79
|
+
variant?: "default" | "destructive" | "ghost" | "link" | "outline";
|
|
80
|
+
/** Color class */
|
|
81
|
+
colorClass?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Bulk action definition for multi-select
|
|
86
|
+
*/
|
|
87
|
+
export interface BulkAction<T extends BaseDataItem> {
|
|
88
|
+
key: string;
|
|
89
|
+
label: string;
|
|
90
|
+
icon?: ReactNode;
|
|
91
|
+
onClick: (items: T[]) => void | Promise<void>;
|
|
92
|
+
disabled?: (items: T[]) => boolean;
|
|
93
|
+
variant?: "default" | "destructive" | "ghost" | "outline";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sort configuration
|
|
98
|
+
*/
|
|
99
|
+
export interface SortConfig {
|
|
100
|
+
key: string;
|
|
101
|
+
direction: "asc" | "desc";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pagination configuration
|
|
106
|
+
*/
|
|
107
|
+
export interface PaginationConfig {
|
|
108
|
+
page: number;
|
|
109
|
+
pageSize: number;
|
|
110
|
+
totalItems: number;
|
|
111
|
+
totalPages: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// TAB TYPES
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Tab definition
|
|
120
|
+
*/
|
|
121
|
+
export interface TabDef {
|
|
122
|
+
/** Unique tab key */
|
|
123
|
+
key: string;
|
|
124
|
+
/** Display label */
|
|
125
|
+
label: string;
|
|
126
|
+
/** Optional badge count */
|
|
127
|
+
badge?: number | null;
|
|
128
|
+
/** Badge variant */
|
|
129
|
+
badgeVariant?: "default" | "secondary" | "destructive" | "outline";
|
|
130
|
+
/** Tab icon */
|
|
131
|
+
icon?: ReactNode;
|
|
132
|
+
/** Tab content */
|
|
133
|
+
content: ReactNode;
|
|
134
|
+
/** Disabled state */
|
|
135
|
+
disabled?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// DIALOG TYPES
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Dialog configuration
|
|
144
|
+
*/
|
|
145
|
+
export interface DialogConfig {
|
|
146
|
+
/** Dialog title */
|
|
147
|
+
title: string;
|
|
148
|
+
/** Dialog description */
|
|
149
|
+
description?: string;
|
|
150
|
+
/** Dialog size */
|
|
151
|
+
size?: "sm" | "md" | "lg" | "xl" | "full";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Confirmation dialog configuration
|
|
156
|
+
*/
|
|
157
|
+
export interface ConfirmDialogConfig extends DialogConfig {
|
|
158
|
+
/** Confirm button label */
|
|
159
|
+
confirmLabel?: string;
|
|
160
|
+
/** Cancel button label */
|
|
161
|
+
cancelLabel?: string;
|
|
162
|
+
/** Confirm button variant */
|
|
163
|
+
confirmVariant?: "default" | "destructive";
|
|
164
|
+
/** Whether action is in progress */
|
|
165
|
+
isLoading?: boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Detail dialog field definition
|
|
170
|
+
*/
|
|
171
|
+
export interface DetailField<T> {
|
|
172
|
+
/** Field key */
|
|
173
|
+
key: string;
|
|
174
|
+
/** Display label */
|
|
175
|
+
label: string;
|
|
176
|
+
/** Value renderer */
|
|
177
|
+
render: (item: T) => ReactNode;
|
|
178
|
+
/** Full width */
|
|
179
|
+
fullWidth?: boolean;
|
|
180
|
+
/** Hide condition */
|
|
181
|
+
hidden?: (item: T) => boolean;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================================================================
|
|
185
|
+
// FORM TYPES
|
|
186
|
+
// ============================================================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Form field types
|
|
190
|
+
*/
|
|
191
|
+
export type FormFieldType =
|
|
192
|
+
| "text"
|
|
193
|
+
| "number"
|
|
194
|
+
| "boolean"
|
|
195
|
+
| "select"
|
|
196
|
+
| "textarea"
|
|
197
|
+
| "code"
|
|
198
|
+
| "password"
|
|
199
|
+
| "email"
|
|
200
|
+
| "date"
|
|
201
|
+
| "datetime";
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Form field definition
|
|
205
|
+
*/
|
|
206
|
+
export interface FormFieldDef {
|
|
207
|
+
/** Field key */
|
|
208
|
+
key: string;
|
|
209
|
+
/** Display label */
|
|
210
|
+
label: string;
|
|
211
|
+
/** Field type */
|
|
212
|
+
type: FormFieldType;
|
|
213
|
+
/** Help text */
|
|
214
|
+
helpText?: string;
|
|
215
|
+
/** Required field */
|
|
216
|
+
required?: boolean;
|
|
217
|
+
/** Placeholder text */
|
|
218
|
+
placeholder?: string;
|
|
219
|
+
/** Default value */
|
|
220
|
+
defaultValue?: unknown;
|
|
221
|
+
/** Options for select fields */
|
|
222
|
+
options?: SelectOption[];
|
|
223
|
+
/** Validation function */
|
|
224
|
+
validate?: (value: unknown) => string | null;
|
|
225
|
+
/** Code editor language */
|
|
226
|
+
codeLanguage?: string;
|
|
227
|
+
/** Disabled condition */
|
|
228
|
+
disabled?: boolean;
|
|
229
|
+
/** Full width */
|
|
230
|
+
fullWidth?: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Select option
|
|
235
|
+
*/
|
|
236
|
+
export interface SelectOption {
|
|
237
|
+
label: string;
|
|
238
|
+
value: string | number | boolean;
|
|
239
|
+
description?: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Dynamic parameter definition (for policy templates etc.)
|
|
244
|
+
*/
|
|
245
|
+
export interface ParameterDef {
|
|
246
|
+
name: string;
|
|
247
|
+
type: "string" | "number" | "boolean" | "select";
|
|
248
|
+
helpText: string;
|
|
249
|
+
required: boolean;
|
|
250
|
+
defaultValue?: unknown;
|
|
251
|
+
options?: string[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// STATUS & BADGE TYPES
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Status types with associated colors
|
|
260
|
+
*/
|
|
261
|
+
export type StatusType =
|
|
262
|
+
| "pending"
|
|
263
|
+
| "approved"
|
|
264
|
+
| "rejected"
|
|
265
|
+
| "committed"
|
|
266
|
+
| "cancelled"
|
|
267
|
+
| "mixed"
|
|
268
|
+
| "ready"
|
|
269
|
+
| "error"
|
|
270
|
+
| "warning"
|
|
271
|
+
| "info"
|
|
272
|
+
| "success";
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Status configuration
|
|
276
|
+
*/
|
|
277
|
+
export interface StatusConfig {
|
|
278
|
+
label: string;
|
|
279
|
+
variant: StatusType;
|
|
280
|
+
icon?: ReactNode;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Badge configuration for custom badges
|
|
285
|
+
*/
|
|
286
|
+
export interface BadgeConfig {
|
|
287
|
+
label: string;
|
|
288
|
+
colorClass?: string;
|
|
289
|
+
variant?: "default" | "secondary" | "destructive" | "outline";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ============================================================================
|
|
293
|
+
// ACTION TYPES
|
|
294
|
+
// ============================================================================
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Action result for async operations
|
|
298
|
+
*/
|
|
299
|
+
export interface ActionResult {
|
|
300
|
+
success: boolean;
|
|
301
|
+
message?: string;
|
|
302
|
+
error?: Error;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Async action handler type
|
|
307
|
+
*/
|
|
308
|
+
export type AsyncActionHandler<T = void, R = ActionResult> = (arg: T) => Promise<R>;
|
|
309
|
+
|
|
310
|
+
// ============================================================================
|
|
311
|
+
// APPROVAL WORKFLOW TYPES
|
|
312
|
+
// ============================================================================
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Approval decision
|
|
316
|
+
*/
|
|
317
|
+
export interface ApprovalDecision {
|
|
318
|
+
userId: string;
|
|
319
|
+
userName?: string;
|
|
320
|
+
userEmail?: string;
|
|
321
|
+
approved: boolean;
|
|
322
|
+
timestamp: string | number;
|
|
323
|
+
signature?: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Approval status with threshold
|
|
328
|
+
*/
|
|
329
|
+
export interface ApprovalStatus {
|
|
330
|
+
approvalCount: number;
|
|
331
|
+
rejectionCount: number;
|
|
332
|
+
threshold: number;
|
|
333
|
+
decisions?: ApprovalDecision[];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Raw approval request for signing
|
|
338
|
+
*/
|
|
339
|
+
export interface RawApprovalRequest {
|
|
340
|
+
changesetId: string;
|
|
341
|
+
changeSetDraftRequests: string; // Base64 encoded
|
|
342
|
+
requiresApprovalPopup?: boolean;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Signed approval response
|
|
347
|
+
*/
|
|
348
|
+
export interface SignedApprovalResponse {
|
|
349
|
+
id: string;
|
|
350
|
+
approved?: {
|
|
351
|
+
request: Uint8Array;
|
|
352
|
+
};
|
|
353
|
+
denied?: boolean;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ============================================================================
|
|
357
|
+
// HOOK TYPES
|
|
358
|
+
// ============================================================================
|
|
359
|
+
|
|
360
|
+
export interface AutoRefreshOptions {
|
|
361
|
+
/** Refresh interval in seconds */
|
|
362
|
+
intervalSeconds: number;
|
|
363
|
+
/** Whether auto-refresh is enabled */
|
|
364
|
+
enabled?: boolean;
|
|
365
|
+
/** Refresh callback */
|
|
366
|
+
refresh: () => void | Promise<unknown>;
|
|
367
|
+
/** Block refresh while condition is true */
|
|
368
|
+
isBlocked?: boolean;
|
|
369
|
+
/** Error callback - called when refresh fails */
|
|
370
|
+
onError?: (error: Error) => void;
|
|
371
|
+
/** Max consecutive failures before stopping (default: unlimited) */
|
|
372
|
+
maxRetries?: number;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export interface AutoRefreshReturn {
|
|
376
|
+
/** Seconds until next refresh */
|
|
377
|
+
secondsRemaining: number | null;
|
|
378
|
+
/** Trigger immediate refresh */
|
|
379
|
+
refreshNow: () => Promise<void>;
|
|
380
|
+
/** Reset the timer */
|
|
381
|
+
resetTimer: () => void;
|
|
382
|
+
/** Last error from refresh (null if no error) */
|
|
383
|
+
lastError: Error | null;
|
|
384
|
+
/** Number of consecutive failures */
|
|
385
|
+
failureCount: number;
|
|
386
|
+
/** Whether auto-refresh stopped due to max retries */
|
|
387
|
+
isStopped: boolean;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Selection hook return type
|
|
392
|
+
*/
|
|
393
|
+
export interface SelectionReturn<T extends BaseDataItem> {
|
|
394
|
+
/** Currently selected item IDs */
|
|
395
|
+
selectedIds: string[];
|
|
396
|
+
/** Check if item is selected */
|
|
397
|
+
isSelected: (id: string) => boolean;
|
|
398
|
+
/** Toggle item selection */
|
|
399
|
+
toggle: (id: string) => void;
|
|
400
|
+
/** Select all items */
|
|
401
|
+
selectAll: (items: T[]) => void;
|
|
402
|
+
/** Clear all selections */
|
|
403
|
+
clearAll: () => void;
|
|
404
|
+
/** Toggle all items */
|
|
405
|
+
toggleAll: (items: T[]) => void;
|
|
406
|
+
/** Get selected items from a list */
|
|
407
|
+
getSelected: (items: T[]) => T[];
|
|
408
|
+
/** Whether all items are selected */
|
|
409
|
+
allSelected: (items: T[]) => boolean;
|
|
410
|
+
/** Whether some (but not all) items are selected */
|
|
411
|
+
someSelected: (items: T[]) => boolean;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Pagination hook return type
|
|
416
|
+
*/
|
|
417
|
+
export interface PaginationReturn {
|
|
418
|
+
page: number;
|
|
419
|
+
pageSize: number;
|
|
420
|
+
setPage: (page: number) => void;
|
|
421
|
+
setPageSize: (size: number) => void;
|
|
422
|
+
nextPage: () => void;
|
|
423
|
+
prevPage: () => void;
|
|
424
|
+
canNextPage: boolean;
|
|
425
|
+
canPrevPage: boolean;
|
|
426
|
+
totalPages: number;
|
|
427
|
+
startIndex: number;
|
|
428
|
+
endIndex: number;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ============================================================================
|
|
432
|
+
// UTILITY TYPES
|
|
433
|
+
// ============================================================================
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Toast notification type
|
|
437
|
+
*/
|
|
438
|
+
export interface ToastConfig {
|
|
439
|
+
title: string;
|
|
440
|
+
description?: string;
|
|
441
|
+
variant?: "default" | "destructive";
|
|
442
|
+
duration?: number;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Empty state configuration
|
|
447
|
+
*/
|
|
448
|
+
export interface EmptyStateConfig {
|
|
449
|
+
icon?: ReactNode;
|
|
450
|
+
title: string;
|
|
451
|
+
description?: string;
|
|
452
|
+
action?: {
|
|
453
|
+
label: string;
|
|
454
|
+
onClick: () => void;
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Loading state configuration
|
|
460
|
+
*/
|
|
461
|
+
export interface LoadingStateConfig {
|
|
462
|
+
/** Number of skeleton rows */
|
|
463
|
+
rows?: number;
|
|
464
|
+
/** Type of skeleton */
|
|
465
|
+
type?: "table" | "card" | "list";
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ============================================================================
|
|
469
|
+
// PROVIDER TYPES
|
|
470
|
+
// ============================================================================
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Framework configuration
|
|
474
|
+
*/
|
|
475
|
+
export interface FrameworkConfig {
|
|
476
|
+
/** Toast function (from your toast library) */
|
|
477
|
+
toast?: (config: ToastConfig) => void;
|
|
478
|
+
/** Custom class name generator */
|
|
479
|
+
cn?: (...inputs: unknown[]) => string;
|
|
480
|
+
/** Default page size */
|
|
481
|
+
defaultPageSize?: number;
|
|
482
|
+
/** Default auto-refresh interval */
|
|
483
|
+
defaultRefreshInterval?: number;
|
|
484
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merge Tailwind classes with clsx
|
|
6
|
+
*/
|
|
7
|
+
export function cn(...inputs: ClassValue[]) {
|
|
8
|
+
return twMerge(clsx(inputs));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert base64 string to Uint8Array
|
|
13
|
+
*/
|
|
14
|
+
export function base64ToBytes(base64: string): Uint8Array {
|
|
15
|
+
const binaryString = atob(base64);
|
|
16
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
17
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
18
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
19
|
+
}
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert Uint8Array to base64 string
|
|
25
|
+
*/
|
|
26
|
+
export function bytesToBase64(bytes: Uint8Array): string {
|
|
27
|
+
let binary = "";
|
|
28
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
29
|
+
binary += String.fromCharCode(bytes[i]);
|
|
30
|
+
}
|
|
31
|
+
return btoa(binary);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format timestamp to locale string
|
|
36
|
+
*/
|
|
37
|
+
export function formatTimestamp(
|
|
38
|
+
timestamp: string | number,
|
|
39
|
+
options?: Intl.DateTimeFormatOptions
|
|
40
|
+
): string {
|
|
41
|
+
const date = typeof timestamp === "number"
|
|
42
|
+
? new Date(timestamp > 1e12 ? timestamp : timestamp * 1000) // Handle both ms and seconds
|
|
43
|
+
: new Date(timestamp);
|
|
44
|
+
|
|
45
|
+
return date.toLocaleString("en-US", options ?? {
|
|
46
|
+
month: "short",
|
|
47
|
+
day: "numeric",
|
|
48
|
+
year: "numeric",
|
|
49
|
+
hour: "2-digit",
|
|
50
|
+
minute: "2-digit",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Format date for logs (includes seconds)
|
|
56
|
+
*/
|
|
57
|
+
export function formatLogTimestamp(timestampMs: number): string {
|
|
58
|
+
const date = new Date(timestampMs);
|
|
59
|
+
return date.toLocaleString("en-US", {
|
|
60
|
+
month: "short",
|
|
61
|
+
day: "numeric",
|
|
62
|
+
year: "numeric",
|
|
63
|
+
hour: "numeric",
|
|
64
|
+
minute: "2-digit",
|
|
65
|
+
second: "2-digit",
|
|
66
|
+
hour12: true,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Compute optimal number of rows for viewport height
|
|
72
|
+
*/
|
|
73
|
+
export function computeRowsForViewportHeight(
|
|
74
|
+
viewportHeight: number,
|
|
75
|
+
headerPx = 48,
|
|
76
|
+
rowPx = 44
|
|
77
|
+
): number {
|
|
78
|
+
const availablePx = Math.max(0, viewportHeight - headerPx);
|
|
79
|
+
const rows = Math.floor(availablePx / rowPx);
|
|
80
|
+
return Math.max(5, Math.min(200, rows));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Delay helper for async operations
|
|
85
|
+
*/
|
|
86
|
+
export function delay(ms: number): Promise<void> {
|
|
87
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Safe JSON parse
|
|
92
|
+
*/
|
|
93
|
+
export function safeJsonParse<T>(json: string, fallback: T): T {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(json);
|
|
96
|
+
} catch {
|
|
97
|
+
return fallback;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Truncate text with ellipsis
|
|
103
|
+
*/
|
|
104
|
+
export function truncate(text: string, maxLength: number): string {
|
|
105
|
+
if (text.length <= maxLength) return text;
|
|
106
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if a value is defined (not null or undefined)
|
|
111
|
+
*/
|
|
112
|
+
export function isDefined<T>(value: T | null | undefined): value is T {
|
|
113
|
+
return value !== null && value !== undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate unique ID
|
|
118
|
+
*/
|
|
119
|
+
export function generateId(): string {
|
|
120
|
+
return Math.random().toString(36).substring(2, 9);
|
|
121
|
+
}
|