@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.
- package/dist/buttons/ArchbaseSpeedDial.d.ts +59 -0
- package/dist/buttons/index.d.ts +2 -0
- package/dist/charts/ArchbaseArcGauge.d.ts +55 -0
- package/dist/charts/ArchbaseGantt.d.ts +70 -0
- package/dist/charts/ArchbaseLinearGauge.d.ts +55 -0
- package/dist/charts/ArchbaseResourceTimeline.d.ts +61 -0
- package/dist/charts/ArchbaseSparkline.d.ts +45 -0
- package/dist/charts/index.d.ts +10 -0
- package/dist/containers/ArchbaseExpansionPanel.d.ts +60 -0
- package/dist/containers/ArchbaseFloatingPanel.d.ts +91 -0
- package/dist/containers/index.d.ts +4 -0
- package/dist/datagrid/export/ArchbaseDataGridExcelExport.d.ts +67 -0
- package/dist/datagrid/export/index.d.ts +2 -0
- package/dist/datagrid/index.d.ts +1 -0
- package/dist/display/ArchbaseAnimationWrapper.d.ts +73 -0
- package/dist/display/ArchbaseBarcodeGenerator.d.ts +50 -0
- package/dist/display/ArchbaseCarousel.d.ts +100 -0
- package/dist/display/ArchbaseCodeViewer.d.ts +63 -0
- package/dist/display/ArchbaseDiffViewer.d.ts +41 -0
- package/dist/display/ArchbaseJsonTree.d.ts +34 -0
- package/dist/display/ArchbaseLightbox.d.ts +89 -0
- package/dist/display/ArchbaseMarquee.d.ts +46 -0
- package/dist/display/ArchbasePhotoAlbum.d.ts +58 -0
- package/dist/display/ArchbaseQRCode.d.ts +44 -0
- package/dist/display/ArchbaseTooltipRich.d.ts +82 -0
- package/dist/display/ArchbaseVirtualList.d.ts +116 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/editors/ArchbaseAIPromptInput.d.ts +69 -0
- package/dist/editors/ArchbaseAsyncMultiSelect.d.ts +1 -1
- package/dist/editors/ArchbaseAsyncSelect.d.ts +1 -1
- package/dist/editors/ArchbaseBarcodeScanner.d.ts +63 -0
- package/dist/editors/ArchbaseCepInput.d.ts +26 -0
- package/dist/editors/ArchbaseColorGradientPicker.d.ts +80 -0
- package/dist/editors/ArchbaseCpfCnpjInput.d.ts +21 -0
- package/dist/editors/ArchbaseCurrencyInput.d.ts +25 -0
- package/dist/editors/ArchbaseDualListbox.d.ts +61 -0
- package/dist/editors/ArchbaseInPlaceEditor.d.ts +67 -0
- package/dist/editors/ArchbaseLightGrid.d.ts +56 -0
- package/dist/editors/ArchbaseMentionInput.d.ts +75 -0
- package/dist/editors/ArchbaseMultiEmail.d.ts +53 -0
- package/dist/editors/ArchbaseMultiViewCalendar.d.ts +47 -0
- package/dist/editors/ArchbaseNumberStepper.d.ts +66 -0
- package/dist/editors/ArchbaseOTPInput.d.ts +27 -0
- package/dist/editors/ArchbasePhoneInput.d.ts +52 -0
- package/dist/editors/ArchbaseRangeSlider.d.ts +22 -0
- package/dist/editors/ArchbaseSignaturePad.d.ts +60 -0
- package/dist/editors/ArchbaseSpeechToTextInput.d.ts +106 -0
- package/dist/editors/ArchbaseSpreadsheet.d.ts +56 -0
- package/dist/editors/ArchbaseTagInput.d.ts +55 -0
- package/dist/editors/index.d.ts +40 -0
- package/dist/feedback/ArchbaseChunkProgressBar.d.ts +35 -0
- package/dist/feedback/ArchbaseRipple.d.ts +43 -0
- package/dist/feedback/ArchbaseSkeleton.d.ts +115 -0
- package/dist/feedback/index.d.ts +6 -0
- package/dist/filters/ArchbaseColumnSelector.d.ts +38 -0
- package/dist/filters/index.d.ts +2 -0
- package/dist/forms/ArchbaseFieldArray.d.ts +80 -0
- package/dist/forms/ArchbaseFormWizard.d.ts +73 -0
- package/dist/forms/ArchbaseStepper.d.ts +51 -0
- package/dist/forms/index.d.ts +8 -0
- package/dist/forms/useArchbaseFormWizard.d.ts +74 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useArchbaseConfirm.d.ts +59 -0
- package/dist/image/editor/index.d.ts +43 -4
- package/dist/image/index.d.ts +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +22510 -13820
- package/dist/list/ArchbaseTreeList.d.ts +76 -0
- package/dist/list/index.d.ts +2 -0
- package/dist/navigation/ArchbaseActionSheet.d.ts +70 -0
- package/dist/navigation/ArchbaseAppBar.d.ts +63 -0
- package/dist/navigation/ArchbaseBottomNavigation.d.ts +61 -0
- package/dist/navigation/ArchbaseContextMenu.d.ts +76 -0
- package/dist/navigation/ArchbaseKeyboardShortcuts.d.ts +85 -0
- package/dist/navigation/ArchbaseSpotlight.d.ts +69 -0
- package/dist/navigation/index.d.ts +12 -0
- package/dist/notification/ArchbaseNotificationCenter.d.ts +56 -0
- package/dist/notification/index.d.ts +2 -0
- package/dist/onboarding/ArchbaseOnboardingTour.d.ts +27 -76
- package/dist/viewers/ArchbaseDocViewer.d.ts +40 -0
- package/dist/viewers/ArchbasePdfBuilder.d.ts +53 -0
- package/dist/viewers/ArchbaseXmlViewer.d.ts +36 -0
- package/dist/viewers/index.d.ts +6 -0
- package/package.json +41 -18
- package/src/buttons/ArchbaseSpeedDial.tsx +282 -0
- package/src/buttons/index.ts +3 -0
- package/src/charts/ArchbaseArcGauge.tsx +325 -0
- package/src/charts/ArchbaseGantt.tsx +494 -0
- package/src/charts/ArchbaseLinearGauge.tsx +288 -0
- package/src/charts/ArchbaseResourceTimeline.tsx +433 -0
- package/src/charts/ArchbaseSparkline.tsx +193 -0
- package/src/charts/index.ts +20 -1
- package/src/containers/ArchbaseExpansionPanel.tsx +208 -0
- package/src/containers/ArchbaseFloatingPanel.tsx +448 -0
- package/src/containers/index.ts +17 -0
- package/src/datagrid/components/pagination/grid-pagination.tsx +2 -2
- package/src/datagrid/export/ArchbaseDataGridExcelExport.tsx +293 -0
- package/src/datagrid/export/index.ts +8 -0
- package/src/datagrid/index.tsx +2 -0
- package/src/display/ArchbaseAnimationWrapper.tsx +272 -0
- package/src/display/ArchbaseBarcodeGenerator.tsx +303 -0
- package/src/display/ArchbaseCarousel.tsx +679 -0
- package/src/display/ArchbaseCodeViewer.tsx +271 -0
- package/src/display/ArchbaseDiffViewer.tsx +108 -0
- package/src/display/ArchbaseJsonTree.tsx +150 -0
- package/src/display/ArchbaseLightbox.tsx +279 -0
- package/src/display/ArchbaseMarquee.tsx +268 -0
- package/src/display/ArchbasePhotoAlbum.tsx +415 -0
- package/src/display/ArchbaseQRCode.tsx +137 -0
- package/src/display/ArchbaseTooltipRich.tsx +317 -0
- package/src/display/ArchbaseVirtualList.tsx +618 -0
- package/src/display/index.ts +94 -1
- package/src/editors/ArchbaseAIPromptInput.tsx +432 -0
- package/src/editors/ArchbaseBarcodeScanner.tsx +570 -0
- package/src/editors/ArchbaseCepInput.tsx +173 -0
- package/src/editors/ArchbaseColorGradientPicker.tsx +460 -0
- package/src/editors/ArchbaseCpfCnpjInput.tsx +193 -0
- package/src/editors/ArchbaseCurrencyInput.tsx +100 -0
- package/src/editors/ArchbaseDualListbox.tsx +671 -0
- package/src/editors/ArchbaseInPlaceEditor.tsx +406 -0
- package/src/editors/ArchbaseLightGrid.tsx +538 -0
- package/src/editors/ArchbaseMentionInput.tsx +384 -0
- package/src/editors/ArchbaseMultiEmail.tsx +367 -0
- package/src/editors/ArchbaseMultiViewCalendar.tsx +439 -0
- package/src/editors/ArchbaseNumberStepper.tsx +219 -0
- package/src/editors/ArchbaseOTPInput.tsx +133 -0
- package/src/editors/ArchbaseOperationHoursEditor.tsx +1 -1
- package/src/editors/ArchbasePhoneInput.tsx +355 -0
- package/src/editors/ArchbaseRangeSlider.tsx +109 -0
- package/src/editors/ArchbaseSignaturePad.tsx +425 -0
- package/src/editors/ArchbaseSpeechToTextInput.tsx +444 -0
- package/src/editors/ArchbaseSpreadsheet.tsx +388 -0
- package/src/editors/ArchbaseTagInput.tsx +326 -0
- package/src/editors/index.tsx +92 -0
- package/src/feedback/ArchbaseChunkProgressBar.tsx +156 -0
- package/src/feedback/ArchbaseRipple.tsx +278 -0
- package/src/feedback/ArchbaseSkeleton.tsx +419 -0
- package/src/feedback/index.ts +22 -1
- package/src/filters/ArchbaseColumnSelector.tsx +268 -0
- package/src/filters/index.ts +2 -0
- package/src/forms/ArchbaseFieldArray.tsx +305 -0
- package/src/forms/ArchbaseFormWizard.tsx +284 -0
- package/src/forms/ArchbaseStepper.tsx +137 -0
- package/src/forms/index.ts +24 -2
- package/src/forms/useArchbaseFormWizard.ts +283 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/useArchbaseConfirm.ts +293 -0
- package/src/image/editor/functions/image-processing.ts +5 -0
- package/src/image/editor/index.tsx +305 -391
- package/src/image/index.ts +1 -0
- package/src/index.ts +3 -0
- package/src/list/ArchbaseTreeList.tsx +422 -0
- package/src/list/index.ts +3 -0
- package/src/navigation/ArchbaseActionSheet.tsx +301 -0
- package/src/navigation/ArchbaseAppBar.tsx +233 -0
- package/src/navigation/ArchbaseBottomNavigation.tsx +243 -0
- package/src/navigation/ArchbaseContextMenu.tsx +298 -0
- package/src/navigation/ArchbaseKeyboardShortcuts.tsx +472 -0
- package/src/navigation/ArchbaseSpotlight.tsx +483 -0
- package/src/navigation/index.ts +48 -1
- package/src/notification/ArchbaseNotificationCenter.tsx +502 -0
- package/src/notification/index.ts +2 -0
- package/src/onboarding/ArchbaseOnboardingTour.tsx +229 -102
- package/src/viewers/ArchbaseDocViewer.tsx +584 -0
- package/src/viewers/ArchbasePdfBuilder.tsx +291 -0
- package/src/viewers/ArchbaseXmlViewer.tsx +142 -0
- 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 };
|