@archbase/components 3.0.25 → 4.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 (168) hide show
  1. package/dist/buttons/ArchbaseSpeedDial.d.ts +59 -0
  2. package/dist/buttons/index.d.ts +2 -0
  3. package/dist/charts/ArchbaseArcGauge.d.ts +55 -0
  4. package/dist/charts/ArchbaseGantt.d.ts +70 -0
  5. package/dist/charts/ArchbaseLinearGauge.d.ts +55 -0
  6. package/dist/charts/ArchbaseResourceTimeline.d.ts +61 -0
  7. package/dist/charts/ArchbaseSparkline.d.ts +45 -0
  8. package/dist/charts/index.d.ts +10 -0
  9. package/dist/containers/ArchbaseExpansionPanel.d.ts +60 -0
  10. package/dist/containers/ArchbaseFloatingPanel.d.ts +91 -0
  11. package/dist/containers/index.d.ts +4 -0
  12. package/dist/datagrid/export/ArchbaseDataGridExcelExport.d.ts +67 -0
  13. package/dist/datagrid/export/index.d.ts +2 -0
  14. package/dist/datagrid/index.d.ts +1 -0
  15. package/dist/display/ArchbaseAnimationWrapper.d.ts +73 -0
  16. package/dist/display/ArchbaseBarcodeGenerator.d.ts +50 -0
  17. package/dist/display/ArchbaseCarousel.d.ts +100 -0
  18. package/dist/display/ArchbaseCodeViewer.d.ts +63 -0
  19. package/dist/display/ArchbaseDiffViewer.d.ts +41 -0
  20. package/dist/display/ArchbaseJsonTree.d.ts +34 -0
  21. package/dist/display/ArchbaseLightbox.d.ts +89 -0
  22. package/dist/display/ArchbaseMarquee.d.ts +46 -0
  23. package/dist/display/ArchbasePhotoAlbum.d.ts +58 -0
  24. package/dist/display/ArchbaseQRCode.d.ts +44 -0
  25. package/dist/display/ArchbaseTooltipRich.d.ts +82 -0
  26. package/dist/display/ArchbaseVirtualList.d.ts +116 -0
  27. package/dist/display/index.d.ts +24 -0
  28. package/dist/editors/ArchbaseAIPromptInput.d.ts +69 -0
  29. package/dist/editors/ArchbaseAsyncMultiSelect.d.ts +1 -1
  30. package/dist/editors/ArchbaseAsyncSelect.d.ts +1 -1
  31. package/dist/editors/ArchbaseBarcodeScanner.d.ts +63 -0
  32. package/dist/editors/ArchbaseCepInput.d.ts +26 -0
  33. package/dist/editors/ArchbaseColorGradientPicker.d.ts +80 -0
  34. package/dist/editors/ArchbaseCpfCnpjInput.d.ts +21 -0
  35. package/dist/editors/ArchbaseCurrencyInput.d.ts +25 -0
  36. package/dist/editors/ArchbaseDualListbox.d.ts +61 -0
  37. package/dist/editors/ArchbaseInPlaceEditor.d.ts +67 -0
  38. package/dist/editors/ArchbaseLightGrid.d.ts +56 -0
  39. package/dist/editors/ArchbaseMentionInput.d.ts +75 -0
  40. package/dist/editors/ArchbaseMultiEmail.d.ts +53 -0
  41. package/dist/editors/ArchbaseMultiViewCalendar.d.ts +47 -0
  42. package/dist/editors/ArchbaseNumberStepper.d.ts +66 -0
  43. package/dist/editors/ArchbaseOTPInput.d.ts +27 -0
  44. package/dist/editors/ArchbasePhoneInput.d.ts +52 -0
  45. package/dist/editors/ArchbaseRangeSlider.d.ts +22 -0
  46. package/dist/editors/ArchbaseSignaturePad.d.ts +60 -0
  47. package/dist/editors/ArchbaseSpeechToTextInput.d.ts +106 -0
  48. package/dist/editors/ArchbaseSpreadsheet.d.ts +56 -0
  49. package/dist/editors/ArchbaseTagInput.d.ts +55 -0
  50. package/dist/editors/index.d.ts +40 -0
  51. package/dist/feedback/ArchbaseChunkProgressBar.d.ts +35 -0
  52. package/dist/feedback/ArchbaseRipple.d.ts +43 -0
  53. package/dist/feedback/ArchbaseSkeleton.d.ts +115 -0
  54. package/dist/feedback/index.d.ts +6 -0
  55. package/dist/filters/ArchbaseColumnSelector.d.ts +38 -0
  56. package/dist/filters/index.d.ts +2 -0
  57. package/dist/forms/ArchbaseFieldArray.d.ts +80 -0
  58. package/dist/forms/ArchbaseFormWizard.d.ts +73 -0
  59. package/dist/forms/ArchbaseStepper.d.ts +51 -0
  60. package/dist/forms/index.d.ts +8 -0
  61. package/dist/forms/useArchbaseFormWizard.d.ts +74 -0
  62. package/dist/hooks/index.d.ts +2 -0
  63. package/dist/hooks/useArchbaseConfirm.d.ts +59 -0
  64. package/dist/image/editor/index.d.ts +43 -4
  65. package/dist/image/index.d.ts +1 -0
  66. package/dist/index.css +1 -1
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.js +22510 -13820
  69. package/dist/list/ArchbaseTreeList.d.ts +76 -0
  70. package/dist/list/index.d.ts +2 -0
  71. package/dist/navigation/ArchbaseActionSheet.d.ts +70 -0
  72. package/dist/navigation/ArchbaseAppBar.d.ts +63 -0
  73. package/dist/navigation/ArchbaseBottomNavigation.d.ts +61 -0
  74. package/dist/navigation/ArchbaseContextMenu.d.ts +76 -0
  75. package/dist/navigation/ArchbaseKeyboardShortcuts.d.ts +85 -0
  76. package/dist/navigation/ArchbaseSpotlight.d.ts +69 -0
  77. package/dist/navigation/index.d.ts +12 -0
  78. package/dist/notification/ArchbaseNotificationCenter.d.ts +56 -0
  79. package/dist/notification/index.d.ts +2 -0
  80. package/dist/onboarding/ArchbaseOnboardingTour.d.ts +27 -76
  81. package/dist/viewers/ArchbaseDocViewer.d.ts +40 -0
  82. package/dist/viewers/ArchbasePdfBuilder.d.ts +53 -0
  83. package/dist/viewers/ArchbaseXmlViewer.d.ts +36 -0
  84. package/dist/viewers/index.d.ts +6 -0
  85. package/package.json +41 -18
  86. package/src/buttons/ArchbaseSpeedDial.tsx +282 -0
  87. package/src/buttons/index.ts +3 -0
  88. package/src/charts/ArchbaseArcGauge.tsx +325 -0
  89. package/src/charts/ArchbaseGantt.tsx +494 -0
  90. package/src/charts/ArchbaseLinearGauge.tsx +288 -0
  91. package/src/charts/ArchbaseResourceTimeline.tsx +433 -0
  92. package/src/charts/ArchbaseSparkline.tsx +193 -0
  93. package/src/charts/index.ts +20 -1
  94. package/src/containers/ArchbaseExpansionPanel.tsx +208 -0
  95. package/src/containers/ArchbaseFloatingPanel.tsx +448 -0
  96. package/src/containers/index.ts +17 -0
  97. package/src/datagrid/components/pagination/grid-pagination.tsx +2 -2
  98. package/src/datagrid/export/ArchbaseDataGridExcelExport.tsx +293 -0
  99. package/src/datagrid/export/index.ts +8 -0
  100. package/src/datagrid/index.tsx +2 -0
  101. package/src/display/ArchbaseAnimationWrapper.tsx +272 -0
  102. package/src/display/ArchbaseBarcodeGenerator.tsx +303 -0
  103. package/src/display/ArchbaseCarousel.tsx +679 -0
  104. package/src/display/ArchbaseCodeViewer.tsx +271 -0
  105. package/src/display/ArchbaseDiffViewer.tsx +108 -0
  106. package/src/display/ArchbaseJsonTree.tsx +150 -0
  107. package/src/display/ArchbaseLightbox.tsx +279 -0
  108. package/src/display/ArchbaseMarquee.tsx +268 -0
  109. package/src/display/ArchbasePhotoAlbum.tsx +415 -0
  110. package/src/display/ArchbaseQRCode.tsx +137 -0
  111. package/src/display/ArchbaseTooltipRich.tsx +317 -0
  112. package/src/display/ArchbaseVirtualList.tsx +618 -0
  113. package/src/display/index.ts +94 -1
  114. package/src/editors/ArchbaseAIPromptInput.tsx +432 -0
  115. package/src/editors/ArchbaseBarcodeScanner.tsx +570 -0
  116. package/src/editors/ArchbaseCepInput.tsx +173 -0
  117. package/src/editors/ArchbaseColorGradientPicker.tsx +460 -0
  118. package/src/editors/ArchbaseCpfCnpjInput.tsx +193 -0
  119. package/src/editors/ArchbaseCurrencyInput.tsx +100 -0
  120. package/src/editors/ArchbaseDualListbox.tsx +671 -0
  121. package/src/editors/ArchbaseInPlaceEditor.tsx +406 -0
  122. package/src/editors/ArchbaseLightGrid.tsx +538 -0
  123. package/src/editors/ArchbaseMentionInput.tsx +384 -0
  124. package/src/editors/ArchbaseMultiEmail.tsx +367 -0
  125. package/src/editors/ArchbaseMultiViewCalendar.tsx +439 -0
  126. package/src/editors/ArchbaseNumberStepper.tsx +219 -0
  127. package/src/editors/ArchbaseOTPInput.tsx +133 -0
  128. package/src/editors/ArchbaseOperationHoursEditor.tsx +1 -1
  129. package/src/editors/ArchbasePhoneInput.tsx +355 -0
  130. package/src/editors/ArchbaseRangeSlider.tsx +109 -0
  131. package/src/editors/ArchbaseSignaturePad.tsx +425 -0
  132. package/src/editors/ArchbaseSpeechToTextInput.tsx +444 -0
  133. package/src/editors/ArchbaseSpreadsheet.tsx +388 -0
  134. package/src/editors/ArchbaseTagInput.tsx +326 -0
  135. package/src/editors/index.tsx +92 -0
  136. package/src/feedback/ArchbaseChunkProgressBar.tsx +156 -0
  137. package/src/feedback/ArchbaseRipple.tsx +278 -0
  138. package/src/feedback/ArchbaseSkeleton.tsx +419 -0
  139. package/src/feedback/index.ts +22 -1
  140. package/src/filters/ArchbaseColumnSelector.tsx +268 -0
  141. package/src/filters/index.ts +2 -0
  142. package/src/forms/ArchbaseFieldArray.tsx +305 -0
  143. package/src/forms/ArchbaseFormWizard.tsx +284 -0
  144. package/src/forms/ArchbaseStepper.tsx +137 -0
  145. package/src/forms/index.ts +24 -2
  146. package/src/forms/useArchbaseFormWizard.ts +283 -0
  147. package/src/hooks/index.ts +12 -0
  148. package/src/hooks/useArchbaseConfirm.ts +293 -0
  149. package/src/image/editor/functions/image-processing.ts +5 -0
  150. package/src/image/editor/index.tsx +305 -391
  151. package/src/image/index.ts +1 -0
  152. package/src/index.ts +3 -0
  153. package/src/list/ArchbaseTreeList.tsx +422 -0
  154. package/src/list/index.ts +3 -0
  155. package/src/navigation/ArchbaseActionSheet.tsx +301 -0
  156. package/src/navigation/ArchbaseAppBar.tsx +233 -0
  157. package/src/navigation/ArchbaseBottomNavigation.tsx +243 -0
  158. package/src/navigation/ArchbaseContextMenu.tsx +298 -0
  159. package/src/navigation/ArchbaseKeyboardShortcuts.tsx +472 -0
  160. package/src/navigation/ArchbaseSpotlight.tsx +483 -0
  161. package/src/navigation/index.ts +48 -1
  162. package/src/notification/ArchbaseNotificationCenter.tsx +502 -0
  163. package/src/notification/index.ts +2 -0
  164. package/src/onboarding/ArchbaseOnboardingTour.tsx +229 -102
  165. package/src/viewers/ArchbaseDocViewer.tsx +584 -0
  166. package/src/viewers/ArchbasePdfBuilder.tsx +291 -0
  167. package/src/viewers/ArchbaseXmlViewer.tsx +142 -0
  168. package/src/viewers/index.ts +12 -0
@@ -0,0 +1,584 @@
1
+ import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import {
3
+ Box,
4
+ Center,
5
+ Group,
6
+ Input,
7
+ Loader,
8
+ Stack,
9
+ Text,
10
+ ActionIcon,
11
+ Tooltip,
12
+ useMantineTheme,
13
+ useComputedColorScheme,
14
+ } from '@mantine/core';
15
+ import {
16
+ IconFile,
17
+ IconFileTypePdf,
18
+ IconPhoto,
19
+ IconFileText,
20
+ IconDownload,
21
+ IconExternalLink,
22
+ IconAlertCircle,
23
+ } from '@tabler/icons-react';
24
+ import {
25
+ useArchbaseV1V2Compatibility,
26
+ useArchbaseDataSourceListener,
27
+ } from '@archbase/data';
28
+ import type { IArchbaseDataSourceBase } from '@archbase/data';
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Types
32
+ // ---------------------------------------------------------------------------
33
+
34
+ type DocumentType = 'pdf' | 'image' | 'text' | 'office' | 'unknown';
35
+
36
+ export interface ArchbaseDocViewerProps<T> {
37
+ /** DataSource to read the URL or base64 value from */
38
+ dataSource?: IArchbaseDataSourceBase<T>;
39
+ /** Field name within the DataSource */
40
+ dataField?: string;
41
+ /** Standalone URL string */
42
+ uri?: string;
43
+ /** Standalone base64 string (include the data URI prefix or provide mimeType) */
44
+ base64?: string;
45
+ /** Explicit MIME type – auto-detected from the URL extension when omitted */
46
+ mimeType?: string;
47
+ /** Height of the viewer @default '600px' */
48
+ height?: string | number;
49
+ /** Width of the viewer @default '100%' */
50
+ width?: string | number;
51
+ /** Show the toolbar with download / open-in-new-tab buttons @default true */
52
+ showToolbar?: boolean;
53
+ /** Label for Input.Wrapper */
54
+ label?: string;
55
+ /** Description for Input.Wrapper */
56
+ description?: string;
57
+ /** Error message for Input.Wrapper */
58
+ error?: string;
59
+ /** Callback fired when the document finishes loading */
60
+ onDocumentLoad?: () => void;
61
+ /** Callback fired when the document fails to load */
62
+ onDocumentError?: (error: Error) => void;
63
+ /** Custom style applied to the outer container */
64
+ style?: React.CSSProperties;
65
+ /** Custom class name */
66
+ className?: string;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Helpers
71
+ // ---------------------------------------------------------------------------
72
+
73
+ const EXTENSION_MIME: Record<string, string> = {
74
+ pdf: 'application/pdf',
75
+ png: 'image/png',
76
+ jpg: 'image/jpeg',
77
+ jpeg: 'image/jpeg',
78
+ gif: 'image/gif',
79
+ svg: 'image/svg+xml',
80
+ webp: 'image/webp',
81
+ bmp: 'image/bmp',
82
+ txt: 'text/plain',
83
+ json: 'application/json',
84
+ xml: 'application/xml',
85
+ csv: 'text/csv',
86
+ html: 'text/html',
87
+ htm: 'text/html',
88
+ md: 'text/markdown',
89
+ log: 'text/plain',
90
+ doc: 'application/msword',
91
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
92
+ xls: 'application/vnd.ms-excel',
93
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
94
+ ppt: 'application/vnd.ms-powerpoint',
95
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
96
+ };
97
+
98
+ function getExtensionFromUrl(url: string): string {
99
+ try {
100
+ const pathname = new URL(url, 'https://dummy.base').pathname;
101
+ const ext = pathname.split('.').pop()?.toLowerCase() ?? '';
102
+ return ext;
103
+ } catch {
104
+ return '';
105
+ }
106
+ }
107
+
108
+ function detectMimeType(url: string | undefined, explicitMime?: string): string {
109
+ if (explicitMime) return explicitMime;
110
+ if (!url) return 'application/octet-stream';
111
+ const ext = getExtensionFromUrl(url);
112
+ return EXTENSION_MIME[ext] ?? 'application/octet-stream';
113
+ }
114
+
115
+ function classifyDocument(mime: string): DocumentType {
116
+ if (mime === 'application/pdf') return 'pdf';
117
+ if (mime.startsWith('image/')) return 'image';
118
+ if (
119
+ mime.startsWith('text/') ||
120
+ mime === 'application/json' ||
121
+ mime === 'application/xml'
122
+ )
123
+ return 'text';
124
+ if (
125
+ mime.includes('officedocument') ||
126
+ mime.includes('msword') ||
127
+ mime.includes('ms-excel') ||
128
+ mime.includes('ms-powerpoint')
129
+ )
130
+ return 'office';
131
+ return 'unknown';
132
+ }
133
+
134
+ function isBase64DataUri(value: string): boolean {
135
+ return value.startsWith('data:');
136
+ }
137
+
138
+ function buildGoogleViewerUrl(url: string): string {
139
+ return `https://docs.google.com/viewer?url=${encodeURIComponent(url)}&embedded=true`;
140
+ }
141
+
142
+ function buildOfficeOnlineUrl(url: string): string {
143
+ return `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Component
148
+ // ---------------------------------------------------------------------------
149
+
150
+ function ArchbaseDocViewer<T>({
151
+ dataSource,
152
+ dataField,
153
+ uri,
154
+ base64,
155
+ mimeType: mimeTypeProp,
156
+ height = '600px',
157
+ width = '100%',
158
+ showToolbar = true,
159
+ label,
160
+ description,
161
+ error: errorProp,
162
+ onDocumentLoad,
163
+ onDocumentError,
164
+ style,
165
+ className,
166
+ }: ArchbaseDocViewerProps<T>) {
167
+ const theme = useMantineTheme();
168
+ const colorScheme = useComputedColorScheme('light');
169
+ const isDark = colorScheme === 'dark';
170
+
171
+ // Resolve value from dataSource OR standalone props
172
+ const standaloneProp = uri ?? base64;
173
+
174
+ const { currentValue, loadDataSourceFieldValue } =
175
+ useArchbaseV1V2Compatibility<string>(
176
+ 'ArchbaseDocViewer',
177
+ dataSource,
178
+ dataField,
179
+ standaloneProp,
180
+ );
181
+
182
+ useEffect(() => {
183
+ if (dataSource && dataField) {
184
+ loadDataSourceFieldValue();
185
+ }
186
+ }, [dataSource, dataField, loadDataSourceFieldValue]);
187
+
188
+ const dataSourceEvent = useCallback(
189
+ (_event: any) => {
190
+ loadDataSourceFieldValue();
191
+ },
192
+ [loadDataSourceFieldValue],
193
+ );
194
+
195
+ useArchbaseDataSourceListener({ dataSource, listener: dataSourceEvent });
196
+
197
+ // Resolved document URL / data URI
198
+ const documentUrl = currentValue ?? '';
199
+
200
+ const mime = useMemo(
201
+ () => detectMimeType(documentUrl, mimeTypeProp),
202
+ [documentUrl, mimeTypeProp],
203
+ );
204
+ const docType = useMemo(() => classifyDocument(mime), [mime]);
205
+
206
+ // State
207
+ const [isLoading, setIsLoading] = useState(true);
208
+ const [loadError, setLoadError] = useState<string | null>(null);
209
+ const [textContent, setTextContent] = useState<string | null>(null);
210
+ const iframeRef = useRef<HTMLIFrameElement>(null);
211
+
212
+ // Reset loading state when URL changes
213
+ useEffect(() => {
214
+ if (!documentUrl) {
215
+ setIsLoading(false);
216
+ setLoadError(null);
217
+ setTextContent(null);
218
+ return;
219
+ }
220
+ setIsLoading(true);
221
+ setLoadError(null);
222
+ setTextContent(null);
223
+ }, [documentUrl]);
224
+
225
+ // Fetch text content for text-type documents
226
+ useEffect(() => {
227
+ if (!documentUrl || docType !== 'text') return;
228
+ if (isBase64DataUri(documentUrl)) {
229
+ try {
230
+ const base64Part = documentUrl.split(',')[1] ?? '';
231
+ const decoded = atob(base64Part);
232
+ setTextContent(decoded);
233
+ setIsLoading(false);
234
+ onDocumentLoad?.();
235
+ } catch (err) {
236
+ const error = err instanceof Error ? err : new Error(String(err));
237
+ setLoadError('Failed to decode base64 content.');
238
+ setIsLoading(false);
239
+ onDocumentError?.(error);
240
+ }
241
+ return;
242
+ }
243
+ let cancelled = false;
244
+ fetch(documentUrl)
245
+ .then((res) => {
246
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
247
+ return res.text();
248
+ })
249
+ .then((text) => {
250
+ if (!cancelled) {
251
+ setTextContent(text);
252
+ setIsLoading(false);
253
+ onDocumentLoad?.();
254
+ }
255
+ })
256
+ .catch((err) => {
257
+ if (!cancelled) {
258
+ const error = err instanceof Error ? err : new Error(String(err));
259
+ setLoadError(error.message);
260
+ setIsLoading(false);
261
+ onDocumentError?.(error);
262
+ }
263
+ });
264
+ return () => {
265
+ cancelled = true;
266
+ };
267
+ }, [documentUrl, docType, onDocumentLoad, onDocumentError]);
268
+
269
+ // Handlers
270
+ const handleIframeLoad = useCallback(() => {
271
+ setIsLoading(false);
272
+ onDocumentLoad?.();
273
+ }, [onDocumentLoad]);
274
+
275
+ const handleIframeError = useCallback(() => {
276
+ const error = new Error('Failed to load document in iframe.');
277
+ setLoadError(error.message);
278
+ setIsLoading(false);
279
+ onDocumentError?.(error);
280
+ }, [onDocumentError]);
281
+
282
+ const handleImageLoad = useCallback(() => {
283
+ setIsLoading(false);
284
+ onDocumentLoad?.();
285
+ }, [onDocumentLoad]);
286
+
287
+ const handleImageError = useCallback(() => {
288
+ const error = new Error('Failed to load image.');
289
+ setLoadError(error.message);
290
+ setIsLoading(false);
291
+ onDocumentError?.(error);
292
+ }, [onDocumentError]);
293
+
294
+ // Download helper
295
+ const handleDownload = useCallback(() => {
296
+ if (!documentUrl) return;
297
+ const a = document.createElement('a');
298
+ a.href = documentUrl;
299
+ a.download = '';
300
+ a.target = '_blank';
301
+ document.body.appendChild(a);
302
+ a.click();
303
+ document.body.removeChild(a);
304
+ }, [documentUrl]);
305
+
306
+ const handleOpenNewTab = useCallback(() => {
307
+ if (!documentUrl) return;
308
+ window.open(documentUrl, '_blank', 'noopener,noreferrer');
309
+ }, [documentUrl]);
310
+
311
+ // Background colors
312
+ const bgColor = isDark ? theme.colors.dark[6] : theme.colors.gray[0];
313
+ const borderColor = isDark ? theme.colors.dark[4] : theme.colors.gray[3];
314
+
315
+ // ---- Render helpers ----
316
+
317
+ const renderToolbar = () => {
318
+ if (!showToolbar || !documentUrl) return null;
319
+ return (
320
+ <Group
321
+ justify="flex-end"
322
+ gap="xs"
323
+ px="sm"
324
+ py={6}
325
+ style={{
326
+ borderBottom: `1px solid ${borderColor}`,
327
+ backgroundColor: isDark ? theme.colors.dark[7] : theme.colors.gray[1],
328
+ }}
329
+ >
330
+ <Tooltip label="Download">
331
+ <ActionIcon variant="subtle" onClick={handleDownload} aria-label="Download">
332
+ <IconDownload size={18} />
333
+ </ActionIcon>
334
+ </Tooltip>
335
+ <Tooltip label="Open in new tab">
336
+ <ActionIcon variant="subtle" onClick={handleOpenNewTab} aria-label="Open in new tab">
337
+ <IconExternalLink size={18} />
338
+ </ActionIcon>
339
+ </Tooltip>
340
+ </Group>
341
+ );
342
+ };
343
+
344
+ const renderLoading = () => (
345
+ <Center style={{ width: '100%', height: '100%', minHeight: 200 }}>
346
+ <Stack align="center" gap="xs">
347
+ <Loader size="md" />
348
+ <Text size="sm" c="dimmed">
349
+ Loading document...
350
+ </Text>
351
+ </Stack>
352
+ </Center>
353
+ );
354
+
355
+ const renderError = () => (
356
+ <Center style={{ width: '100%', height: '100%', minHeight: 200 }}>
357
+ <Stack align="center" gap="xs">
358
+ <IconAlertCircle size={40} color={theme.colors.red[6]} />
359
+ <Text size="sm" c="red">
360
+ {loadError ?? 'An error occurred while loading the document.'}
361
+ </Text>
362
+ </Stack>
363
+ </Center>
364
+ );
365
+
366
+ const renderEmpty = () => (
367
+ <Center style={{ width: '100%', height: '100%', minHeight: 200 }}>
368
+ <Stack align="center" gap="xs">
369
+ <IconFile size={40} color={isDark ? theme.colors.dark[2] : theme.colors.gray[5]} />
370
+ <Text size="sm" c="dimmed">
371
+ No document to display.
372
+ </Text>
373
+ </Stack>
374
+ </Center>
375
+ );
376
+
377
+ const renderDocumentIcon = () => {
378
+ switch (docType) {
379
+ case 'pdf':
380
+ return <IconFileTypePdf size={20} />;
381
+ case 'image':
382
+ return <IconPhoto size={20} />;
383
+ case 'text':
384
+ return <IconFileText size={20} />;
385
+ default:
386
+ return <IconFile size={20} />;
387
+ }
388
+ };
389
+
390
+ const renderPdf = () => (
391
+ <Box style={{ width: '100%', height: '100%', position: 'relative' }}>
392
+ {isLoading && renderLoading()}
393
+ <object
394
+ data={documentUrl}
395
+ type="application/pdf"
396
+ style={{
397
+ width: '100%',
398
+ height: '100%',
399
+ border: 'none',
400
+ display: isLoading ? 'none' : 'block',
401
+ }}
402
+ onLoad={handleIframeLoad}
403
+ >
404
+ {/* Fallback: iframe for browsers that don't support object/pdf */}
405
+ <iframe
406
+ ref={iframeRef}
407
+ src={documentUrl}
408
+ title="PDF Document"
409
+ style={{ width: '100%', height: '100%', border: 'none' }}
410
+ onLoad={handleIframeLoad}
411
+ onError={handleIframeError}
412
+ />
413
+ </object>
414
+ </Box>
415
+ );
416
+
417
+ const renderImage = () => (
418
+ <Box style={{ width: '100%', height: '100%', overflow: 'auto', position: 'relative' }}>
419
+ {isLoading && renderLoading()}
420
+ <Center style={{ width: '100%', height: '100%' }}>
421
+ <img
422
+ src={documentUrl}
423
+ alt="Document"
424
+ style={{
425
+ maxWidth: '100%',
426
+ maxHeight: '100%',
427
+ objectFit: 'contain',
428
+ display: isLoading ? 'none' : 'block',
429
+ }}
430
+ onLoad={handleImageLoad}
431
+ onError={handleImageError}
432
+ />
433
+ </Center>
434
+ </Box>
435
+ );
436
+
437
+ const renderText = () => {
438
+ if (isLoading) return renderLoading();
439
+ return (
440
+ <Box
441
+ style={{
442
+ width: '100%',
443
+ height: '100%',
444
+ overflow: 'auto',
445
+ padding: theme.spacing.sm,
446
+ }}
447
+ >
448
+ <pre
449
+ style={{
450
+ margin: 0,
451
+ fontFamily: 'monospace',
452
+ fontSize: 13,
453
+ whiteSpace: 'pre-wrap',
454
+ wordBreak: 'break-word',
455
+ color: isDark ? theme.colors.gray[3] : theme.colors.dark[8],
456
+ }}
457
+ >
458
+ {textContent ?? ''}
459
+ </pre>
460
+ </Box>
461
+ );
462
+ };
463
+
464
+ const renderOffice = () => {
465
+ // Office viewer only works with public HTTP(S) URLs
466
+ if (isBase64DataUri(documentUrl)) {
467
+ return (
468
+ <Center style={{ width: '100%', height: '100%', minHeight: 200 }}>
469
+ <Stack align="center" gap="xs">
470
+ {renderDocumentIcon()}
471
+ <Text size="sm" c="dimmed">
472
+ Office document preview is not available for base64 content.
473
+ </Text>
474
+ <Text
475
+ size="sm"
476
+ c="blue"
477
+ style={{ cursor: 'pointer', textDecoration: 'underline' }}
478
+ onClick={handleDownload}
479
+ >
480
+ Download file
481
+ </Text>
482
+ </Stack>
483
+ </Center>
484
+ );
485
+ }
486
+ // Try Office Online first, fallback to Google Docs Viewer on error
487
+ return (
488
+ <Box style={{ width: '100%', height: '100%', position: 'relative' }}>
489
+ {isLoading && renderLoading()}
490
+ <iframe
491
+ ref={iframeRef}
492
+ src={buildOfficeOnlineUrl(documentUrl)}
493
+ title="Office Document"
494
+ style={{
495
+ width: '100%',
496
+ height: '100%',
497
+ border: 'none',
498
+ display: isLoading ? 'none' : 'block',
499
+ }}
500
+ onLoad={handleIframeLoad}
501
+ onError={() => {
502
+ // Fallback to Google Docs Viewer
503
+ if (iframeRef.current) {
504
+ iframeRef.current.src = buildGoogleViewerUrl(documentUrl);
505
+ }
506
+ }}
507
+ />
508
+ </Box>
509
+ );
510
+ };
511
+
512
+ const renderUnknown = () => (
513
+ <Center style={{ width: '100%', height: '100%', minHeight: 200 }}>
514
+ <Stack align="center" gap="xs">
515
+ <IconFile size={48} color={isDark ? theme.colors.dark[2] : theme.colors.gray[5]} />
516
+ <Text size="sm" c="dimmed">
517
+ Preview is not available for this file type.
518
+ </Text>
519
+ {documentUrl && (
520
+ <Text
521
+ size="sm"
522
+ c="blue"
523
+ style={{ cursor: 'pointer', textDecoration: 'underline' }}
524
+ onClick={handleDownload}
525
+ >
526
+ Download file
527
+ </Text>
528
+ )}
529
+ </Stack>
530
+ </Center>
531
+ );
532
+
533
+ const renderContent = () => {
534
+ if (!documentUrl) return renderEmpty();
535
+ if (loadError) return renderError();
536
+ switch (docType) {
537
+ case 'pdf':
538
+ return renderPdf();
539
+ case 'image':
540
+ return renderImage();
541
+ case 'text':
542
+ return renderText();
543
+ case 'office':
544
+ return renderOffice();
545
+ default:
546
+ return renderUnknown();
547
+ }
548
+ };
549
+
550
+ const viewer = (
551
+ <Box
552
+ style={{
553
+ width,
554
+ height,
555
+ border: `1px solid ${borderColor}`,
556
+ borderRadius: theme.radius.sm,
557
+ overflow: 'hidden',
558
+ backgroundColor: bgColor,
559
+ display: 'flex',
560
+ flexDirection: 'column',
561
+ ...style,
562
+ }}
563
+ className={className}
564
+ >
565
+ {renderToolbar()}
566
+ <Box style={{ flex: 1, overflow: 'hidden' }}>{renderContent()}</Box>
567
+ </Box>
568
+ );
569
+
570
+ if (label || description || errorProp) {
571
+ return (
572
+ <Input.Wrapper label={label} description={description} error={errorProp}>
573
+ {viewer}
574
+ </Input.Wrapper>
575
+ );
576
+ }
577
+
578
+ return viewer;
579
+ }
580
+
581
+ ArchbaseDocViewer.displayName = 'ArchbaseDocViewer';
582
+
583
+ export { ArchbaseDocViewer };
584
+ export type { DocumentType };