@pallasoft/ps-file-components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +10 -0
  3. package/dist/components/ps-file-delete-confirm.d.ts +11 -0
  4. package/dist/components/ps-file-delete-confirm.js +8 -0
  5. package/dist/components/ps-file-kind-icon.d.ts +6 -0
  6. package/dist/components/ps-file-kind-icon.js +10 -0
  7. package/dist/components/ps-file-list.d.ts +12 -0
  8. package/dist/components/ps-file-list.js +27 -0
  9. package/dist/components/ps-file-picker.d.ts +18 -0
  10. package/dist/components/ps-file-picker.js +48 -0
  11. package/dist/components/ps-file-viewer-button.d.ts +10 -0
  12. package/dist/components/ps-file-viewer-button.js +7 -0
  13. package/dist/components/ps-file-viewer-content.d.ts +2 -0
  14. package/dist/components/ps-file-viewer-content.js +28 -0
  15. package/dist/components/ps-file-viewer.d.ts +14 -0
  16. package/dist/components/ps-file-viewer.js +102 -0
  17. package/dist/components/ps-progress-bar.d.ts +7 -0
  18. package/dist/components/ps-progress-bar.js +15 -0
  19. package/dist/components/ps-upload-modal.d.ts +10 -0
  20. package/dist/components/ps-upload-modal.js +19 -0
  21. package/dist/components/ps-upload-row.d.ts +11 -0
  22. package/dist/components/ps-upload-row.js +28 -0
  23. package/dist/components/viewers/ps-highlighted-text.d.ts +6 -0
  24. package/dist/components/viewers/ps-highlighted-text.js +8 -0
  25. package/dist/components/viewers/ps-image-viewer.d.ts +2 -0
  26. package/dist/components/viewers/ps-image-viewer.js +33 -0
  27. package/dist/components/viewers/ps-media-viewer.d.ts +2 -0
  28. package/dist/components/viewers/ps-media-viewer.js +8 -0
  29. package/dist/components/viewers/ps-pdf-page.d.ts +12 -0
  30. package/dist/components/viewers/ps-pdf-page.js +37 -0
  31. package/dist/components/viewers/ps-pdf-thumbnail.d.ts +11 -0
  32. package/dist/components/viewers/ps-pdf-thumbnail.js +28 -0
  33. package/dist/components/viewers/ps-pdf-viewer.d.ts +2 -0
  34. package/dist/components/viewers/ps-pdf-viewer.js +100 -0
  35. package/dist/components/viewers/ps-text-viewer.d.ts +2 -0
  36. package/dist/components/viewers/ps-text-viewer.js +42 -0
  37. package/dist/components/viewers/ps-unsupported-viewer.d.ts +2 -0
  38. package/dist/components/viewers/ps-unsupported-viewer.js +8 -0
  39. package/dist/constants/ps-file-kind-icons.d.ts +3 -0
  40. package/dist/constants/ps-file-kind-icons.js +14 -0
  41. package/dist/constants/ps-file-kind.d.ts +10 -0
  42. package/dist/constants/ps-file-kind.js +12 -0
  43. package/dist/constants/ps-file-list-labels.d.ts +6 -0
  44. package/dist/constants/ps-file-list-labels.js +9 -0
  45. package/dist/constants/ps-file-picker-labels.d.ts +7 -0
  46. package/dist/constants/ps-file-picker-labels.js +10 -0
  47. package/dist/constants/ps-file-type-config.d.ts +7 -0
  48. package/dist/constants/ps-file-type-config.js +30 -0
  49. package/dist/constants/ps-file-viewer-config.d.ts +9 -0
  50. package/dist/constants/ps-file-viewer-config.js +12 -0
  51. package/dist/constants/ps-file-viewer-kinds.d.ts +5 -0
  52. package/dist/constants/ps-file-viewer-kinds.js +9 -0
  53. package/dist/constants/ps-file-viewer-labels.d.ts +21 -0
  54. package/dist/constants/ps-file-viewer-labels.js +24 -0
  55. package/dist/constants/ps-upload-labels.d.ts +14 -0
  56. package/dist/constants/ps-upload-labels.js +17 -0
  57. package/dist/constants/ps-upload-messages.d.ts +4 -0
  58. package/dist/constants/ps-upload-messages.js +7 -0
  59. package/dist/constants/ps-upload-queue-config.d.ts +4 -0
  60. package/dist/constants/ps-upload-queue-config.js +7 -0
  61. package/dist/constants/ps-upload-status-tones.d.ts +2 -0
  62. package/dist/constants/ps-upload-status-tones.js +12 -0
  63. package/dist/constants/ps-upload-status.d.ts +9 -0
  64. package/dist/constants/ps-upload-status.js +11 -0
  65. package/dist/esm/components/ps-file-delete-confirm.js +4 -0
  66. package/dist/esm/components/ps-file-kind-icon.js +6 -0
  67. package/dist/esm/components/ps-file-list.js +23 -0
  68. package/dist/esm/components/ps-file-picker.js +44 -0
  69. package/dist/esm/components/ps-file-viewer-button.js +3 -0
  70. package/dist/esm/components/ps-file-viewer-content.js +24 -0
  71. package/dist/esm/components/ps-file-viewer.js +98 -0
  72. package/dist/esm/components/ps-progress-bar.js +11 -0
  73. package/dist/esm/components/ps-upload-modal.js +15 -0
  74. package/dist/esm/components/ps-upload-row.js +24 -0
  75. package/dist/esm/components/viewers/ps-highlighted-text.js +4 -0
  76. package/dist/esm/components/viewers/ps-image-viewer.js +29 -0
  77. package/dist/esm/components/viewers/ps-media-viewer.js +4 -0
  78. package/dist/esm/components/viewers/ps-pdf-page.js +33 -0
  79. package/dist/esm/components/viewers/ps-pdf-thumbnail.js +24 -0
  80. package/dist/esm/components/viewers/ps-pdf-viewer.js +96 -0
  81. package/dist/esm/components/viewers/ps-text-viewer.js +38 -0
  82. package/dist/esm/components/viewers/ps-unsupported-viewer.js +4 -0
  83. package/dist/esm/constants/ps-file-kind-icons.js +11 -0
  84. package/dist/esm/constants/ps-file-kind.js +9 -0
  85. package/dist/esm/constants/ps-file-list-labels.js +6 -0
  86. package/dist/esm/constants/ps-file-picker-labels.js +7 -0
  87. package/dist/esm/constants/ps-file-type-config.js +27 -0
  88. package/dist/esm/constants/ps-file-viewer-config.js +9 -0
  89. package/dist/esm/constants/ps-file-viewer-kinds.js +6 -0
  90. package/dist/esm/constants/ps-file-viewer-labels.js +21 -0
  91. package/dist/esm/constants/ps-upload-labels.js +14 -0
  92. package/dist/esm/constants/ps-upload-messages.js +4 -0
  93. package/dist/esm/constants/ps-upload-queue-config.js +4 -0
  94. package/dist/esm/constants/ps-upload-status-tones.js +9 -0
  95. package/dist/esm/constants/ps-upload-status.js +8 -0
  96. package/dist/esm/hooks/use-ps-active-match-scroll.js +8 -0
  97. package/dist/esm/hooks/use-ps-file-text.js +21 -0
  98. package/dist/esm/hooks/use-ps-in-view.js +17 -0
  99. package/dist/esm/hooks/use-ps-upload-queue.js +67 -0
  100. package/dist/esm/index.js +62 -0
  101. package/dist/esm/models/ps-file-rejection.js +1 -0
  102. package/dist/esm/models/ps-file-selection-rules.js +1 -0
  103. package/dist/esm/models/ps-file-selection.js +1 -0
  104. package/dist/esm/models/ps-file-text-state.js +1 -0
  105. package/dist/esm/models/ps-file-viewer-content-props.js +1 -0
  106. package/dist/esm/models/ps-label-set.js +1 -0
  107. package/dist/esm/models/ps-page-size.js +1 -0
  108. package/dist/esm/models/ps-pdf-match.js +1 -0
  109. package/dist/esm/models/ps-point.js +1 -0
  110. package/dist/esm/models/ps-text-match.js +1 -0
  111. package/dist/esm/models/ps-text-segment.js +1 -0
  112. package/dist/esm/models/ps-upload-handler-options.js +1 -0
  113. package/dist/esm/models/ps-upload-item.js +1 -0
  114. package/dist/esm/models/ps-upload-queue-options.js +1 -0
  115. package/dist/esm/models/ps-upload-queue-state.js +1 -0
  116. package/dist/esm/models/ps-upload-summary.js +1 -0
  117. package/dist/esm/models/ps-viewer-file.js +1 -0
  118. package/dist/esm/models/ps-xhr-upload-request.js +1 -0
  119. package/dist/esm/models/ps-xhr-upload-response.js +1 -0
  120. package/dist/esm/services/ps-csv-service.js +42 -0
  121. package/dist/esm/services/ps-file-selection-service.js +16 -0
  122. package/dist/esm/services/ps-file-size-service.js +14 -0
  123. package/dist/esm/services/ps-file-type-service.js +38 -0
  124. package/dist/esm/services/ps-pdf-service.js +59 -0
  125. package/dist/esm/services/ps-text-search-service.js +32 -0
  126. package/dist/esm/services/ps-upload-queue-service.js +37 -0
  127. package/dist/esm/services/ps-xhr-upload-service.js +52 -0
  128. package/dist/hooks/use-ps-active-match-scroll.d.ts +2 -0
  129. package/dist/hooks/use-ps-active-match-scroll.js +12 -0
  130. package/dist/hooks/use-ps-file-text.d.ts +2 -0
  131. package/dist/hooks/use-ps-file-text.js +25 -0
  132. package/dist/hooks/use-ps-in-view.d.ts +2 -0
  133. package/dist/hooks/use-ps-in-view.js +21 -0
  134. package/dist/hooks/use-ps-upload-queue.d.ts +3 -0
  135. package/dist/hooks/use-ps-upload-queue.js +71 -0
  136. package/dist/index.d.ts +62 -0
  137. package/dist/index.js +78 -0
  138. package/dist/models/ps-file-rejection.d.ts +4 -0
  139. package/dist/models/ps-file-rejection.js +2 -0
  140. package/dist/models/ps-file-selection-rules.d.ts +6 -0
  141. package/dist/models/ps-file-selection-rules.js +2 -0
  142. package/dist/models/ps-file-selection.d.ts +5 -0
  143. package/dist/models/ps-file-selection.js +2 -0
  144. package/dist/models/ps-file-text-state.d.ts +4 -0
  145. package/dist/models/ps-file-text-state.js +2 -0
  146. package/dist/models/ps-file-viewer-content-props.d.ts +13 -0
  147. package/dist/models/ps-file-viewer-content-props.js +2 -0
  148. package/dist/models/ps-label-set.d.ts +3 -0
  149. package/dist/models/ps-label-set.js +2 -0
  150. package/dist/models/ps-page-size.d.ts +4 -0
  151. package/dist/models/ps-page-size.js +2 -0
  152. package/dist/models/ps-pdf-match.d.ts +7 -0
  153. package/dist/models/ps-pdf-match.js +2 -0
  154. package/dist/models/ps-point.d.ts +4 -0
  155. package/dist/models/ps-point.js +2 -0
  156. package/dist/models/ps-text-match.d.ts +4 -0
  157. package/dist/models/ps-text-match.js +2 -0
  158. package/dist/models/ps-text-segment.d.ts +5 -0
  159. package/dist/models/ps-text-segment.js +2 -0
  160. package/dist/models/ps-upload-handler-options.d.ts +4 -0
  161. package/dist/models/ps-upload-handler-options.js +2 -0
  162. package/dist/models/ps-upload-item.d.ts +8 -0
  163. package/dist/models/ps-upload-item.js +2 -0
  164. package/dist/models/ps-upload-queue-options.d.ts +6 -0
  165. package/dist/models/ps-upload-queue-options.js +2 -0
  166. package/dist/models/ps-upload-queue-state.d.ts +10 -0
  167. package/dist/models/ps-upload-queue-state.js +2 -0
  168. package/dist/models/ps-upload-summary.d.ts +5 -0
  169. package/dist/models/ps-upload-summary.js +2 -0
  170. package/dist/models/ps-viewer-file.d.ts +9 -0
  171. package/dist/models/ps-viewer-file.js +2 -0
  172. package/dist/models/ps-xhr-upload-request.d.ts +8 -0
  173. package/dist/models/ps-xhr-upload-request.js +2 -0
  174. package/dist/models/ps-xhr-upload-response.d.ts +4 -0
  175. package/dist/models/ps-xhr-upload-response.js +2 -0
  176. package/dist/services/ps-csv-service.d.ts +3 -0
  177. package/dist/services/ps-csv-service.js +46 -0
  178. package/dist/services/ps-file-selection-service.d.ts +5 -0
  179. package/dist/services/ps-file-selection-service.js +20 -0
  180. package/dist/services/ps-file-size-service.d.ts +3 -0
  181. package/dist/services/ps-file-size-service.js +18 -0
  182. package/dist/services/ps-file-type-service.d.ts +6 -0
  183. package/dist/services/ps-file-type-service.js +42 -0
  184. package/dist/services/ps-pdf-service.d.ts +11 -0
  185. package/dist/services/ps-pdf-service.js +96 -0
  186. package/dist/services/ps-text-search-service.d.ts +6 -0
  187. package/dist/services/ps-text-search-service.js +36 -0
  188. package/dist/services/ps-upload-queue-service.d.ts +10 -0
  189. package/dist/services/ps-upload-queue-service.js +41 -0
  190. package/dist/services/ps-xhr-upload-service.d.ts +7 -0
  191. package/dist/services/ps-xhr-upload-service.js +56 -0
  192. package/package.json +85 -0
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PsFileKind } from '../constants/ps-file-kind';
3
+ import { PsFileTypeService } from '../services/ps-file-type-service';
4
+ import { PsImageViewer } from './viewers/ps-image-viewer';
5
+ import { PsMediaViewer } from './viewers/ps-media-viewer';
6
+ import { PsPdfViewer } from './viewers/ps-pdf-viewer';
7
+ import { PsTextViewer } from './viewers/ps-text-viewer';
8
+ import { PsUnsupportedViewer } from './viewers/ps-unsupported-viewer';
9
+ export const PsFileViewerContent = (props) => {
10
+ switch (PsFileTypeService.kindOf(props.file.contentType, props.file.name)) {
11
+ case PsFileKind.image:
12
+ return _jsx(PsImageViewer, { ...props });
13
+ case PsFileKind.pdf:
14
+ return _jsx(PsPdfViewer, { ...props });
15
+ case PsFileKind.csv:
16
+ case PsFileKind.text:
17
+ return _jsx(PsTextViewer, { ...props });
18
+ case PsFileKind.audio:
19
+ case PsFileKind.video:
20
+ return _jsx(PsMediaViewer, { ...props });
21
+ default:
22
+ return _jsx(PsUnsupportedViewer, { ...props });
23
+ }
24
+ };
@@ -0,0 +1,98 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { PsLayer, usePsDebouncedValue, usePsExitTransition, usePsLayerZIndex, } from '@pallasoft/ps-core-components';
4
+ import { PallasoftClassNameService, PallasoftZIndex } from '@pallasoft/ps-design-system';
5
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
6
+ import { createPortal } from 'react-dom';
7
+ import { PsFileViewerConfig } from '../constants/ps-file-viewer-config';
8
+ import { PsFileViewerKinds } from '../constants/ps-file-viewer-kinds';
9
+ import { PsFileViewerLabels } from '../constants/ps-file-viewer-labels';
10
+ import { PsFileSizeService } from '../services/ps-file-size-service';
11
+ import { PsFileTypeService } from '../services/ps-file-type-service';
12
+ import { PsFileDeleteConfirm } from './ps-file-delete-confirm';
13
+ import { PsFileViewerButton } from './ps-file-viewer-button';
14
+ import { PsFileViewerContent } from './ps-file-viewer-content';
15
+ export const PsFileViewer = ({ files, index, isOpen, labels, onClose, onDelete, onIndexChange, pdfWorkerSrc, }) => {
16
+ const text = { ...PsFileViewerLabels, ...labels };
17
+ const { isClosing, shouldRender } = usePsExitTransition(isOpen);
18
+ const zIndex = usePsLayerZIndex(PallasoftZIndex.Modal);
19
+ const [zoom, setZoom] = useState(1);
20
+ const [rotation, setRotation] = useState(0);
21
+ const [search, setSearch] = useState('');
22
+ const [activeMatch, setActiveMatch] = useState(0);
23
+ const [matchCount, setMatchCount] = useState(0);
24
+ const [isConfirming, setIsConfirming] = useState(false);
25
+ const [isDeleting, setIsDeleting] = useState(false);
26
+ const searchInput = useRef(null);
27
+ const query = usePsDebouncedValue(search, PsFileViewerConfig.searchDebounceMs);
28
+ const file = files.length === 0 ? null : files[Math.min(index, files.length - 1)];
29
+ const kind = file === null ? null : PsFileTypeService.kindOf(file.contentType, file.name);
30
+ const canSearch = kind !== null && PsFileViewerKinds.searchable.includes(kind);
31
+ const canZoom = kind !== null && PsFileViewerKinds.zoomable.includes(kind);
32
+ const canRotate = kind !== null && PsFileViewerKinds.rotatable.includes(kind);
33
+ useEffect(() => {
34
+ setZoom(1);
35
+ setRotation(0);
36
+ setSearch('');
37
+ setActiveMatch(0);
38
+ setMatchCount(0);
39
+ }, [file === null ? null : file.id]);
40
+ useEffect(() => setActiveMatch(0), [query]);
41
+ const go = (step) => {
42
+ if (files.length > 1)
43
+ onIndexChange((index + step + files.length) % files.length);
44
+ };
45
+ const stepMatch = (step) => {
46
+ if (matchCount > 0)
47
+ setActiveMatch((current) => (current + step + matchCount) % matchCount);
48
+ };
49
+ const changeZoom = (step) => setZoom((current) => Math.min(PsFileViewerConfig.maxZoom, Math.max(PsFileViewerConfig.minZoom, current + step)));
50
+ useEffect(() => {
51
+ if (!isOpen)
52
+ return;
53
+ const onKey = (event) => {
54
+ const isTyping = event.target instanceof HTMLInputElement;
55
+ if (event.key === 'Escape' && !isConfirming)
56
+ onClose();
57
+ else if ((event.metaKey || event.ctrlKey) && event.key === 'f' && canSearch) {
58
+ event.preventDefault();
59
+ searchInput.current?.focus();
60
+ }
61
+ else if (isTyping)
62
+ return;
63
+ else if (event.key === 'ArrowRight')
64
+ go(1);
65
+ else if (event.key === 'ArrowLeft')
66
+ go(-1);
67
+ else if (event.key === '+' || event.key === '=')
68
+ changeZoom(PsFileViewerConfig.zoomStep);
69
+ else if (event.key === '-')
70
+ changeZoom(-PsFileViewerConfig.zoomStep);
71
+ };
72
+ window.addEventListener('keydown', onKey);
73
+ return () => window.removeEventListener('keydown', onKey);
74
+ }, [isOpen, isConfirming, canSearch, index, files.length]);
75
+ if (!shouldRender || file === null || typeof document === 'undefined')
76
+ return null;
77
+ const confirmDelete = async () => {
78
+ if (!onDelete)
79
+ return;
80
+ setIsDeleting(true);
81
+ try {
82
+ await onDelete(file);
83
+ setIsConfirming(false);
84
+ }
85
+ finally {
86
+ setIsDeleting(false);
87
+ }
88
+ };
89
+ return createPortal(_jsx("div", { "aria-label": file.name, "aria-modal": 'true', className: PallasoftClassNameService.merge('fixed inset-0 flex flex-col bg-slate-950/95 text-white backdrop-blur-md', isClosing ? 'animate-ps-fade-out' : 'animate-ps-fade-in'), role: 'dialog', style: { zIndex }, children: _jsxs(PsLayer, { zIndex: zIndex, children: [_jsxs("header", { className: 'flex shrink-0 flex-wrap items-center gap-2 border-b border-white/10 px-3 py-2 sm:px-4', children: [_jsx("div", { className: 'flex min-w-0 flex-1 items-center gap-3', children: _jsxs("div", { className: 'min-w-0', children: [_jsx("p", { className: 'truncate text-sm font-semibold', children: file.name }), _jsx("p", { className: 'truncate text-xs text-white/55', children: [
90
+ files.length > 1 ? `${index + 1} / ${files.length}` : null,
91
+ file.sizeBytes === undefined ? null : PsFileSizeService.format(file.sizeBytes),
92
+ ]
93
+ .filter((value) => value !== null)
94
+ .join(' · ') })] }) }), canSearch ? (_jsxs("div", { className: 'order-last flex w-full items-center gap-1 rounded-full bg-white/10 px-3 sm:order-none sm:w-72', children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4 shrink-0 text-white/60', iconKey: PsIconKey.Search }), _jsx("input", { "aria-label": text.search, className: 'h-9 min-w-0 flex-1 bg-transparent text-sm text-white placeholder:text-white/45 focus:outline-none', onChange: (event) => setSearch(event.target.value), onKeyDown: (event) => {
95
+ if (event.key === 'Enter')
96
+ stepMatch(event.shiftKey ? -1 : 1);
97
+ }, placeholder: text.search, ref: searchInput, type: 'search', value: search }), query.trim().length > 0 ? (_jsx("span", { className: 'shrink-0 text-xs text-white/60', children: matchCount === 0 ? text.noMatches : `${activeMatch + 1} / ${matchCount}` })) : null, _jsx(PsFileViewerButton, { disabled: matchCount === 0, label: text.previousMatch, onClick: () => stepMatch(-1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4 -rotate-90', iconKey: PsIconKey.ChevronRight }) }), _jsx(PsFileViewerButton, { disabled: matchCount === 0, label: text.nextMatch, onClick: () => stepMatch(1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4 rotate-90', iconKey: PsIconKey.ChevronRight }) })] })) : null, _jsxs("div", { className: 'flex items-center gap-0.5', children: [canZoom ? (_jsxs(_Fragment, { children: [_jsx(PsFileViewerButton, { label: text.zoomOut, onClick: () => changeZoom(-PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.ZoomOut }) }), _jsxs(PsFileViewerButton, { className: 'w-14 text-xs', label: text.zoomReset, onClick: () => setZoom(1), children: [Math.round(zoom * 100), "%"] }), _jsx(PsFileViewerButton, { label: text.zoomIn, onClick: () => changeZoom(PsFileViewerConfig.zoomStep), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.ZoomIn }) })] })) : null, canRotate ? (_jsx(PsFileViewerButton, { label: text.rotate, onClick: () => setRotation((current) => (current + PsFileViewerConfig.rotateStep) % 360), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Rotate }) })) : null, _jsx("a", { "aria-label": text.download, className: 'ps-focus-ring inline-flex h-9 w-9 items-center justify-center rounded-full text-white/80 transition hover:bg-white/15 hover:text-white', download: file.name, href: file.downloadUrl === undefined ? file.url : file.downloadUrl, title: text.download, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Download }) }), onDelete ? (_jsx(PsFileViewerButton, { label: text.delete, onClick: () => setIsConfirming(true), tone: 'danger', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Trash }) })) : null, _jsx(PsFileViewerButton, { label: text.close, onClick: onClose, children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-5 w-5', iconKey: PsIconKey.Close }) })] })] }), _jsxs("div", { className: 'relative flex min-h-0 flex-1', children: [files.length > 1 ? (_jsx(PsFileViewerButton, { className: 'absolute left-2 top-1/2 z-10 h-11 w-11 -translate-y-1/2 bg-black/40', label: text.previous, onClick: () => go(-1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-5 w-5 rotate-180', iconKey: PsIconKey.ChevronRight }) })) : null, _jsx("div", { className: 'flex min-h-0 min-w-0 flex-1 p-3 sm:p-6', children: _jsx(PsFileViewerContent, { activeMatch: activeMatch, file: file, labels: text, onMatchCount: setMatchCount, pdfWorkerSrc: pdfWorkerSrc, query: canSearch ? query : '', rotation: rotation, zoom: zoom }, file.id) }), files.length > 1 ? (_jsx(PsFileViewerButton, { className: 'absolute right-2 top-1/2 z-10 h-11 w-11 -translate-y-1/2 bg-black/40', label: text.next, onClick: () => go(1), children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-5 w-5', iconKey: PsIconKey.ChevronRight }) })) : null] }), _jsx(PsFileDeleteConfirm, { fileName: file.name, isDeleting: isDeleting, isOpen: isConfirming, labels: text, onCancel: () => setIsConfirming(false), onConfirm: () => void confirmDelete() })] }) }), document.body);
98
+ };
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ const toneClassNames = {
4
+ danger: 'bg-danger',
5
+ primary: 'bg-primary',
6
+ success: 'bg-success',
7
+ };
8
+ export const PsProgressBar = ({ className, label, tone = 'primary', value, }) => {
9
+ const percent = Math.round(Math.min(1, Math.max(0, value)) * 100);
10
+ return (_jsx("div", { "aria-label": label, "aria-valuemax": 100, "aria-valuemin": 0, "aria-valuenow": percent, className: PallasoftClassNameService.merge('h-1.5 w-full overflow-hidden rounded-full bg-line/70', className), role: 'progressbar', children: _jsx("div", { className: PallasoftClassNameService.merge('h-full rounded-full transition-[width] duration-200 ease-out', toneClassNames[tone]), style: { width: `${percent}%` } }) }));
11
+ };
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsButton } from '@pallasoft/ps-core-components';
3
+ import { PsModal } from '@pallasoft/ps-popovers';
4
+ import { PsUploadLabels } from '../constants/ps-upload-labels';
5
+ import { PsUploadQueueService } from '../services/ps-upload-queue-service';
6
+ import { PsProgressBar } from './ps-progress-bar';
7
+ import { PsUploadRow } from './ps-upload-row';
8
+ export const PsUploadModal = ({ isOpen, labels, onClose, queue }) => {
9
+ const text = { ...PsUploadLabels, ...labels };
10
+ const summary = PsUploadQueueService.summary(queue.items);
11
+ const overall = queue.items.length === 0
12
+ ? 0
13
+ : queue.items.reduce((total, item) => total + item.progress, 0) / queue.items.length;
14
+ return (_jsx(PsModal, { eyebrow: `${summary.done} / ${summary.total}`, isOpen: isOpen, onClose: onClose, size: 'md', title: queue.isBusy ? text.title : text.titleDone, children: _jsxs("div", { className: 'flex flex-col gap-4', children: [_jsx(PsProgressBar, { label: text.title, tone: summary.failed > 0 ? 'danger' : queue.isBusy ? 'primary' : 'success', value: overall }), _jsx("ul", { className: 'flex max-h-[50vh] flex-col divide-y divide-line/70 overflow-y-auto', children: queue.items.map((item) => (_jsx(PsUploadRow, { item: item, labels: text, onCancel: () => queue.cancel(item.id), onRemove: () => queue.remove(item.id), onRetry: () => queue.retry(item.id) }, item.id))) }), _jsx("div", { className: 'flex justify-end', children: _jsx(PsButton, { onClick: onClose, type: 'button', variant: queue.isBusy ? 'ghost' : 'primary', children: text.close }) })] }) }));
15
+ };
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsButton } from '@pallasoft/ps-core-components';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsUploadStatus } from '../constants/ps-upload-status';
5
+ import { PsUploadStatusTones } from '../constants/ps-upload-status-tones';
6
+ import { PsFileSizeService } from '../services/ps-file-size-service';
7
+ import { PsUploadQueueService } from '../services/ps-upload-queue-service';
8
+ import { PsFileKindIcon } from './ps-file-kind-icon';
9
+ import { PsProgressBar } from './ps-progress-bar';
10
+ export const PsUploadRow = ({ item, labels, onCancel, onRemove, onRetry }) => {
11
+ const isActive = PsUploadQueueService.isActive(item);
12
+ const canRetry = item.status === PsUploadStatus.failed || item.status === PsUploadStatus.cancelled;
13
+ const percent = Math.round(item.progress * 100);
14
+ const statusText = item.status === PsUploadStatus.uploading
15
+ ? `${labels.uploading} ${percent}%`
16
+ : item.status === PsUploadStatus.failed && item.error
17
+ ? item.error
18
+ : labels[item.status];
19
+ return (_jsxs("li", { className: 'flex items-center gap-3 py-3', children: [_jsx(PsFileKindIcon, { contentType: item.file.type, name: item.file.name }), _jsxs("div", { className: 'flex min-w-0 flex-1 flex-col gap-1.5', children: [_jsxs("div", { className: 'flex items-baseline justify-between gap-3', children: [_jsx("p", { className: 'truncate text-sm font-medium text-ink', children: item.file.name }), _jsx("span", { className: 'shrink-0 text-xs text-ink/55', children: PsFileSizeService.format(item.file.size) })] }), _jsx(PsProgressBar, { label: item.file.name, tone: PsUploadStatusTones[item.status], value: item.status === PsUploadStatus.failed ? 1 : item.progress }), _jsxs("p", { className: item.status === PsUploadStatus.failed || item.status === PsUploadStatus.cancelled
20
+ ? 'text-xs text-danger'
21
+ : item.status === PsUploadStatus.done
22
+ ? 'flex items-center gap-1 text-xs text-success'
23
+ : 'text-xs text-ink/60', role: item.status === PsUploadStatus.failed ? 'alert' : undefined, children: [item.status === PsUploadStatus.done ? (_jsx(PsIcon, { "aria-hidden": 'true', className: 'h-3.5 w-3.5', iconKey: PsIconKey.Check })) : null, statusText] })] }), _jsxs("div", { className: 'flex shrink-0 items-center gap-1', children: [canRetry ? (_jsx(PsButton, { leadingIcon: _jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Refresh }), onClick: onRetry, size: 'compact', type: 'button', variant: 'ghost', children: labels.retry })) : null, _jsx(PsButton, { "aria-label": `${isActive ? labels.cancel : labels.remove} ${item.file.name}`, onClick: isActive ? onCancel : onRemove, size: 'compact', type: 'button', variant: 'ghost', children: isActive ? labels.cancel : labels.remove })] })] }));
24
+ };
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ export const PsHighlightedText = ({ activeMatch, segments }) => (_jsx(_Fragment, { children: segments.map((segment, index) => segment.isMatch ? (_jsx("mark", { className: segment.matchIndex === activeMatch
3
+ ? 'rounded-sm bg-warning px-0.5 text-ink ring-2 ring-warning'
4
+ : 'rounded-sm bg-warning/40 px-0.5 text-inherit', "data-ps-match": segment.matchIndex === null ? undefined : segment.matchIndex, children: segment.text }, index)) : (_jsx("span", { children: segment.text }, index))) }));
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ export const PsImageViewer = ({ file, labels, rotation, zoom }) => {
4
+ const [offset, setOffset] = useState({ x: 0, y: 0 });
5
+ const [hasFailed, setHasFailed] = useState(false);
6
+ const drag = useRef(null);
7
+ useEffect(() => {
8
+ setOffset({ x: 0, y: 0 });
9
+ setHasFailed(false);
10
+ }, [file.id, zoom === 1]);
11
+ const onPointerDown = (event) => {
12
+ if (zoom <= 1)
13
+ return;
14
+ drag.current = { x: event.clientX - offset.x, y: event.clientY - offset.y };
15
+ event.currentTarget.setPointerCapture(event.pointerId);
16
+ };
17
+ const onPointerMove = (event) => {
18
+ if (drag.current === null)
19
+ return;
20
+ setOffset({ x: event.clientX - drag.current.x, y: event.clientY - drag.current.y });
21
+ };
22
+ if (hasFailed)
23
+ return _jsx("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
24
+ return (_jsx("div", { className: `flex h-full w-full touch-none items-center justify-center overflow-hidden ${zoom > 1 ? 'cursor-grab active:cursor-grabbing' : ''}`, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: () => {
25
+ drag.current = null;
26
+ }, children: _jsx("img", { alt: file.name, className: 'max-h-full max-w-full select-none object-contain shadow-2xl transition-transform duration-150', draggable: false, onError: () => setHasFailed(true), src: file.url, style: {
27
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom}) rotate(${rotation}deg)`,
28
+ } }) }));
29
+ };
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PsFileKind } from '../../constants/ps-file-kind';
3
+ import { PsFileTypeService } from '../../services/ps-file-type-service';
4
+ export const PsMediaViewer = ({ file }) => PsFileTypeService.kindOf(file.contentType, file.name) === PsFileKind.video ? (_jsx("video", { className: 'm-auto max-h-full max-w-full rounded-lg shadow-2xl', controls: true, src: file.url })) : (_jsx("audio", { className: 'm-auto w-full max-w-xl', controls: true, src: file.url }));
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { usePsInView } from '../../hooks/use-ps-in-view';
4
+ import { PsPdfService } from '../../services/ps-pdf-service';
5
+ export const PsPdfPage = ({ activeMatch, document, label, matches, pageNumber, rotation, scale, }) => {
6
+ const canvas = useRef(null);
7
+ const [container, isInView] = usePsInView('600px');
8
+ const [size, setSize] = useState(null);
9
+ useEffect(() => {
10
+ let isCurrent = true;
11
+ let task = null;
12
+ document.getPage(pageNumber).then((page) => {
13
+ if (!isCurrent)
14
+ return;
15
+ const viewport = page.getViewport({ rotation: page.rotate + rotation, scale });
16
+ setSize({ height: viewport.height, width: viewport.width });
17
+ if (isInView && canvas.current)
18
+ task = PsPdfService.render(page, canvas.current, scale, rotation);
19
+ });
20
+ return () => {
21
+ isCurrent = false;
22
+ task?.cancel();
23
+ };
24
+ }, [document, pageNumber, rotation, scale, isInView]);
25
+ return (_jsxs("div", { "aria-label": `${label} ${pageNumber}`, className: 'relative mx-auto bg-white shadow-2xl', "data-ps-pdf-page": pageNumber, ref: container, style: size === null ? { height: 800, width: 600 } : { height: size.height, width: size.width }, children: [_jsx("canvas", { className: 'block', ref: canvas }), matches.map((match, index) => (_jsx("span", { className: match === activeMatch
26
+ ? 'pointer-events-none absolute rounded-sm bg-warning/60 ring-2 ring-warning'
27
+ : 'pointer-events-none absolute rounded-sm bg-warning/35', style: {
28
+ height: `${match.height * 100}%`,
29
+ left: `${match.left * 100}%`,
30
+ top: `${match.top * 100}%`,
31
+ width: `${match.width * 100}%`,
32
+ } }, index)))] }));
33
+ };
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { PsFileViewerConfig } from '../../constants/ps-file-viewer-config';
4
+ import { usePsInView } from '../../hooks/use-ps-in-view';
5
+ import { PsPdfService } from '../../services/ps-pdf-service';
6
+ export const PsPdfThumbnail = ({ document, hasMatches, isCurrent, label, onSelect, pageNumber, rotation, }) => {
7
+ const canvas = useRef(null);
8
+ const [container, isInView] = usePsInView('200px');
9
+ useEffect(() => {
10
+ if (!isInView)
11
+ return;
12
+ let isCurrent = true;
13
+ let task = null;
14
+ document.getPage(pageNumber).then((page) => {
15
+ if (isCurrent && canvas.current)
16
+ task = PsPdfService.render(page, canvas.current, PsFileViewerConfig.pdfThumbnailScale, rotation);
17
+ });
18
+ return () => {
19
+ isCurrent = false;
20
+ task?.cancel();
21
+ };
22
+ }, [document, pageNumber, rotation, isInView]);
23
+ return (_jsxs("button", { "aria-current": isCurrent ? 'page' : undefined, "aria-label": `${label} ${pageNumber}`, className: `ps-focus-ring relative flex flex-col items-center gap-1 rounded-md p-1.5 transition ${isCurrent ? 'bg-white/15' : 'hover:bg-white/10'}`, onClick: onSelect, ref: container, type: 'button', children: [_jsx("canvas", { className: `block min-h-16 min-w-12 bg-white ${isCurrent ? 'ring-2 ring-primary' : ''}`, ref: canvas }), _jsx("span", { className: 'text-[0.7rem] text-white/70', children: pageNumber }), hasMatches ? (_jsx("span", { className: 'absolute right-1 top-1 h-2 w-2 rounded-full bg-warning' })) : null] }));
24
+ };
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { PsSpinner, usePsElementSize } from '@pallasoft/ps-core-components';
4
+ import { PsFileViewerConfig } from '../../constants/ps-file-viewer-config';
5
+ import { PsPdfService } from '../../services/ps-pdf-service';
6
+ import { PsPdfPage } from './ps-pdf-page';
7
+ import { PsPdfThumbnail } from './ps-pdf-thumbnail';
8
+ export const PsPdfViewer = ({ activeMatch, file, labels, onMatchCount, pdfWorkerSrc, query, rotation, zoom, }) => {
9
+ const [document, setDocument] = useState(null);
10
+ const [hasFailed, setHasFailed] = useState(false);
11
+ const [pageWidth, setPageWidth] = useState(0);
12
+ const [matches, setMatches] = useState([]);
13
+ const [currentPage, setCurrentPage] = useState(1);
14
+ const [frame, frameSize] = usePsElementSize();
15
+ const scroller = useRef(null);
16
+ useEffect(() => {
17
+ let isCurrent = true;
18
+ let opened = null;
19
+ setDocument(null);
20
+ setHasFailed(false);
21
+ if (pdfWorkerSrc === undefined) {
22
+ setHasFailed(true);
23
+ return;
24
+ }
25
+ PsPdfService.open(file.url, pdfWorkerSrc)
26
+ .then(async (loaded) => {
27
+ opened = loaded;
28
+ const firstPage = await loaded.getPage(1);
29
+ if (!isCurrent)
30
+ return;
31
+ setPageWidth(firstPage.getViewport({ rotation: firstPage.rotate + rotation, scale: 1 }).width);
32
+ setDocument(loaded);
33
+ })
34
+ .catch(() => {
35
+ if (isCurrent)
36
+ setHasFailed(true);
37
+ });
38
+ return () => {
39
+ isCurrent = false;
40
+ void opened?.loadingTask.destroy();
41
+ };
42
+ }, [file.url, pdfWorkerSrc]);
43
+ useEffect(() => {
44
+ if (document === null)
45
+ return;
46
+ let isCurrent = true;
47
+ document.getPage(1).then((page) => {
48
+ if (isCurrent)
49
+ setPageWidth(page.getViewport({ rotation: page.rotate + rotation, scale: 1 }).width);
50
+ });
51
+ return () => {
52
+ isCurrent = false;
53
+ };
54
+ }, [document, rotation]);
55
+ useEffect(() => {
56
+ if (document === null || query.trim().length === 0) {
57
+ setMatches([]);
58
+ onMatchCount(0);
59
+ return;
60
+ }
61
+ let isCurrent = true;
62
+ PsPdfService.search(document, query, rotation).then((found) => {
63
+ if (!isCurrent)
64
+ return;
65
+ setMatches(found);
66
+ onMatchCount(found.length);
67
+ });
68
+ return () => {
69
+ isCurrent = false;
70
+ };
71
+ }, [document, query, rotation]);
72
+ const active = matches.length > 0 ? matches[activeMatch % matches.length] : null;
73
+ const scrollToPage = (pageNumber) => {
74
+ const node = scroller.current?.querySelector(`[data-ps-pdf-page='${pageNumber}']`);
75
+ if (node instanceof HTMLElement)
76
+ node.scrollIntoView({ behavior: 'smooth', block: 'start' });
77
+ setCurrentPage(pageNumber);
78
+ };
79
+ useEffect(() => {
80
+ if (active !== null)
81
+ scrollToPage(active.pageNumber);
82
+ }, [active]);
83
+ const scale = useMemo(() => {
84
+ const fit = frameSize.width > 0 && pageWidth > 0
85
+ ? (frameSize.width - 48) / pageWidth
86
+ : PsFileViewerConfig.pdfBaseScale;
87
+ return Math.max(0.1, fit * zoom);
88
+ }, [frameSize.width, pageWidth, zoom]);
89
+ const pages = document === null ? [] : Array.from({ length: document.numPages }, (_, index) => index + 1);
90
+ return (_jsxs("div", { className: 'flex h-full w-full min-w-0', children: [document !== null && document.numPages > 1 ? (_jsx("nav", { className: 'ps-scrollbar-hidden hidden w-24 shrink-0 flex-col items-center gap-2 overflow-y-auto border-r border-white/10 py-3 md:flex', children: pages.map((pageNumber) => (_jsx(PsPdfThumbnail, { document: document, hasMatches: matches.some((match) => match.pageNumber === pageNumber), isCurrent: pageNumber === currentPage, label: labels.page, onSelect: () => scrollToPage(pageNumber), pageNumber: pageNumber, rotation: rotation }, pageNumber))) })) : null, _jsx("div", { className: 'min-w-0 flex-1', ref: frame, children: hasFailed ? (_jsx("p", { className: 'flex h-full items-center justify-center text-sm text-white/70', children: labels.loadFailed })) : document === null ? (_jsx("div", { className: 'flex h-full items-center justify-center text-white', children: _jsx(PsSpinner, { color: 'current', size: 'large' }) })) : (_jsx("div", { className: 'flex h-full flex-col items-center gap-4 overflow-auto px-6 py-6', onScroll: (event) => {
91
+ const top = event.currentTarget.scrollTop + event.currentTarget.clientHeight / 3;
92
+ const visible = Array.from(event.currentTarget.querySelectorAll('[data-ps-pdf-page]')).find((node) => node.offsetTop + node.offsetHeight > top);
93
+ if (visible)
94
+ setCurrentPage(Number(visible.dataset.psPdfPage));
95
+ }, ref: scroller, children: pages.map((pageNumber) => (_jsx(PsPdfPage, { activeMatch: active, document: document, label: labels.page, matches: matches.filter((match) => match.pageNumber === pageNumber), pageNumber: pageNumber, rotation: rotation, scale: scale }, pageNumber))) })) })] }));
96
+ };
@@ -0,0 +1,38 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from 'react';
3
+ import { PsSpinner } from '@pallasoft/ps-core-components';
4
+ import { PsFileKind } from '../../constants/ps-file-kind';
5
+ import { usePsActiveMatchScroll } from '../../hooks/use-ps-active-match-scroll';
6
+ import { usePsFileText } from '../../hooks/use-ps-file-text';
7
+ import { PsCsvService } from '../../services/ps-csv-service';
8
+ import { PsFileTypeService } from '../../services/ps-file-type-service';
9
+ import { PsTextSearchService } from '../../services/ps-text-search-service';
10
+ import { PsHighlightedText } from './ps-highlighted-text';
11
+ export const PsTextViewer = ({ activeMatch, file, labels, onMatchCount, query, zoom, }) => {
12
+ const { hasFailed, text } = usePsFileText(file.url);
13
+ const container = useRef(null);
14
+ const isCsv = PsFileTypeService.kindOf(file.contentType, file.name) === PsFileKind.csv;
15
+ const rows = useMemo(() => (isCsv && text !== null ? PsCsvService.parse(text) : []), [isCsv, text]);
16
+ const cells = useMemo(() => {
17
+ let offset = 0;
18
+ return rows.map((row) => row.map((cell) => {
19
+ const matches = PsTextSearchService.find(cell, query);
20
+ const segments = PsTextSearchService.segments(cell, matches, offset);
21
+ offset += matches.length;
22
+ return segments;
23
+ }));
24
+ }, [rows, query]);
25
+ const segments = useMemo(() => isCsv || text === null
26
+ ? []
27
+ : PsTextSearchService.segments(text, PsTextSearchService.find(text, query)), [isCsv, text, query]);
28
+ const count = isCsv
29
+ ? cells.reduce((total, row) => total + row.reduce((sum, cell) => sum + cell.filter((part) => part.isMatch).length, 0), 0)
30
+ : segments.filter((part) => part.isMatch).length;
31
+ useEffect(() => onMatchCount(count), [count]);
32
+ usePsActiveMatchScroll(container, activeMatch, query);
33
+ if (hasFailed)
34
+ return _jsx("p", { className: 'm-auto text-sm text-white/70', children: labels.loadFailed });
35
+ if (text === null)
36
+ return _jsx(PsSpinner, { className: 'm-auto', color: 'current', size: 'large' });
37
+ return (_jsx("div", { className: 'h-full w-full overflow-auto', ref: container, children: _jsx("div", { className: 'mx-auto w-full max-w-5xl rounded-lg bg-surface p-5 text-ink shadow-2xl', style: { fontSize: `${zoom * 0.875}rem` }, children: isCsv ? (_jsx("table", { className: 'w-full border-collapse text-left', children: _jsx("tbody", { children: cells.map((row, rowIndex) => (_jsx("tr", { className: rowIndex === 0 ? 'bg-primary-soft/50 font-semibold' : 'border-t border-line', children: row.map((cell, cellIndex) => (_jsx("td", { className: 'whitespace-nowrap px-3 py-1.5 align-top', children: _jsx(PsHighlightedText, { activeMatch: activeMatch, segments: cell }) }, cellIndex))) }, rowIndex))) }) })) : (_jsx("pre", { className: 'whitespace-pre-wrap break-words font-mono leading-relaxed', children: _jsx(PsHighlightedText, { activeMatch: activeMatch, segments: segments }) })) }) }));
38
+ };
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
3
+ import { PsFileKindIcon } from '../ps-file-kind-icon';
4
+ export const PsUnsupportedViewer = ({ file, labels }) => (_jsxs("div", { className: 'm-auto flex max-w-sm flex-col items-center gap-4 rounded-2xl bg-surface p-8 text-center shadow-2xl', children: [_jsx(PsFileKindIcon, { className: 'h-14 w-14', contentType: file.contentType, name: file.name }), _jsx("p", { className: 'break-all font-semibold text-ink', children: file.name }), _jsx("p", { className: 'text-sm text-ink/60', children: labels.unsupported }), _jsxs("a", { className: 'ps-focus-ring inline-flex items-center gap-2 rounded-full bg-primary px-4 py-2 text-sm font-semibold text-white hover:bg-primary/90', download: file.name, href: file.downloadUrl === undefined ? file.url : file.downloadUrl, children: [_jsx(PsIcon, { "aria-hidden": 'true', className: 'h-4 w-4', iconKey: PsIconKey.Download }), labels.download] })] }));
@@ -0,0 +1,11 @@
1
+ import { PsIconKey } from '@pallasoft/ps-icons';
2
+ import { PsFileKind } from './ps-file-kind';
3
+ export const PsFileKindIcons = {
4
+ [PsFileKind.audio]: PsIconKey.Audio,
5
+ [PsFileKind.csv]: PsIconKey.Sheet,
6
+ [PsFileKind.image]: PsIconKey.Image,
7
+ [PsFileKind.other]: PsIconKey.File,
8
+ [PsFileKind.pdf]: PsIconKey.Doc,
9
+ [PsFileKind.text]: PsIconKey.Text,
10
+ [PsFileKind.video]: PsIconKey.Video,
11
+ };
@@ -0,0 +1,9 @@
1
+ export const PsFileKind = {
2
+ audio: 'audio',
3
+ csv: 'csv',
4
+ image: 'image',
5
+ other: 'other',
6
+ pdf: 'pdf',
7
+ text: 'text',
8
+ video: 'video',
9
+ };
@@ -0,0 +1,6 @@
1
+ export const PsFileListLabels = {
2
+ delete: 'Delete',
3
+ download: 'Download',
4
+ empty: 'No files yet',
5
+ open: 'Open',
6
+ };
@@ -0,0 +1,7 @@
1
+ export const PsFilePickerLabels = {
2
+ browse: 'or click to browse your files',
3
+ dropActive: 'Drop to upload',
4
+ formats: 'Accepted formats:',
5
+ invalidType: 'is not a supported file type.',
6
+ tooLarge: 'is too large.',
7
+ };
@@ -0,0 +1,27 @@
1
+ import { PsFileKind } from './ps-file-kind';
2
+ export const PsFileTypeConfig = {
3
+ csvTypes: ['text/csv', 'application/csv', 'application/vnd.ms-excel'],
4
+ extensionKinds: {
5
+ csv: PsFileKind.csv,
6
+ gif: PsFileKind.image,
7
+ heic: PsFileKind.image,
8
+ jpeg: PsFileKind.image,
9
+ jpg: PsFileKind.image,
10
+ json: PsFileKind.text,
11
+ log: PsFileKind.text,
12
+ md: PsFileKind.text,
13
+ mov: PsFileKind.video,
14
+ mp3: PsFileKind.audio,
15
+ mp4: PsFileKind.video,
16
+ pdf: PsFileKind.pdf,
17
+ png: PsFileKind.image,
18
+ svg: PsFileKind.image,
19
+ txt: PsFileKind.text,
20
+ wav: PsFileKind.audio,
21
+ webm: PsFileKind.video,
22
+ webp: PsFileKind.image,
23
+ xml: PsFileKind.text,
24
+ },
25
+ sizeUnits: ['B', 'KB', 'MB', 'GB'],
26
+ textTypes: ['application/json', 'application/xml'],
27
+ };
@@ -0,0 +1,9 @@
1
+ export const PsFileViewerConfig = {
2
+ maxZoom: 4,
3
+ minZoom: 0.25,
4
+ pdfBaseScale: 1.4,
5
+ pdfThumbnailScale: 0.2,
6
+ rotateStep: 90,
7
+ searchDebounceMs: 250,
8
+ zoomStep: 0.25,
9
+ };
@@ -0,0 +1,6 @@
1
+ import { PsFileKind } from './ps-file-kind';
2
+ export const PsFileViewerKinds = {
3
+ rotatable: [PsFileKind.image, PsFileKind.pdf],
4
+ searchable: [PsFileKind.csv, PsFileKind.pdf, PsFileKind.text],
5
+ zoomable: [PsFileKind.csv, PsFileKind.image, PsFileKind.pdf, PsFileKind.text],
6
+ };
@@ -0,0 +1,21 @@
1
+ export const PsFileViewerLabels = {
2
+ close: 'Close',
3
+ delete: 'Delete',
4
+ deleteConfirm: 'Delete file',
5
+ deleteConfirmBody: 'This file will be removed. This cannot be undone from here.',
6
+ deleteCancel: 'Keep file',
7
+ download: 'Download',
8
+ loadFailed: 'This file could not be opened.',
9
+ next: 'Next file',
10
+ nextMatch: 'Next match',
11
+ noMatches: 'No matches',
12
+ page: 'Page',
13
+ previous: 'Previous file',
14
+ previousMatch: 'Previous match',
15
+ rotate: 'Rotate',
16
+ search: 'Search in file',
17
+ unsupported: 'This file type cannot be previewed here. Download it to open it.',
18
+ zoomIn: 'Zoom in',
19
+ zoomOut: 'Zoom out',
20
+ zoomReset: 'Fit',
21
+ };
@@ -0,0 +1,14 @@
1
+ export const PsUploadLabels = {
2
+ cancel: 'Cancel',
3
+ cancelled: 'Cancelled',
4
+ close: 'Close',
5
+ done: 'Uploaded',
6
+ failed: 'Failed',
7
+ processing: 'Saving',
8
+ remove: 'Remove',
9
+ retry: 'Retry',
10
+ title: 'Uploading files',
11
+ titleDone: 'Upload finished',
12
+ uploading: 'Uploading',
13
+ waiting: 'Waiting',
14
+ };
@@ -0,0 +1,4 @@
1
+ export const PsUploadMessages = {
2
+ cancelled: 'Upload cancelled',
3
+ failed: 'Upload failed',
4
+ };
@@ -0,0 +1,4 @@
1
+ export const PsUploadQueueConfig = {
2
+ concurrency: 3,
3
+ progressDone: 1,
4
+ };
@@ -0,0 +1,9 @@
1
+ import { PsUploadStatus } from './ps-upload-status';
2
+ export const PsUploadStatusTones = {
3
+ [PsUploadStatus.cancelled]: 'danger',
4
+ [PsUploadStatus.done]: 'success',
5
+ [PsUploadStatus.failed]: 'danger',
6
+ [PsUploadStatus.processing]: 'primary',
7
+ [PsUploadStatus.uploading]: 'primary',
8
+ [PsUploadStatus.waiting]: 'primary',
9
+ };
@@ -0,0 +1,8 @@
1
+ export const PsUploadStatus = {
2
+ cancelled: 'cancelled',
3
+ done: 'done',
4
+ failed: 'failed',
5
+ processing: 'processing',
6
+ uploading: 'uploading',
7
+ waiting: 'waiting',
8
+ };
@@ -0,0 +1,8 @@
1
+ import { useEffect } from 'react';
2
+ export const usePsActiveMatchScroll = (container, activeMatch, dependency) => {
3
+ useEffect(() => {
4
+ const node = container.current?.querySelector(`[data-ps-match='${activeMatch}']`);
5
+ if (node instanceof HTMLElement)
6
+ node.scrollIntoView({ behavior: 'smooth', block: 'center' });
7
+ }, [activeMatch, dependency]);
8
+ };
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState } from 'react';
2
+ export const usePsFileText = (url) => {
3
+ const [state, setState] = useState({ hasFailed: false, text: null });
4
+ useEffect(() => {
5
+ const controller = new AbortController();
6
+ setState({ hasFailed: false, text: null });
7
+ fetch(url, { credentials: 'include', signal: controller.signal })
8
+ .then((response) => {
9
+ if (!response.ok)
10
+ throw new Error(response.statusText);
11
+ return response.text();
12
+ })
13
+ .then((text) => setState({ hasFailed: false, text }))
14
+ .catch(() => {
15
+ if (!controller.signal.aborted)
16
+ setState({ hasFailed: true, text: null });
17
+ });
18
+ return () => controller.abort();
19
+ }, [url]);
20
+ return state;
21
+ };