inicontent 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 (205) hide show
  1. package/.github/pull_request.yml +18 -0
  2. package/CONTRIBUTING.md +67 -0
  3. package/LICENSE +21 -0
  4. package/README.md +142 -0
  5. package/app/app.vue +106 -0
  6. package/app/assets/main.css +974 -0
  7. package/app/components/Api/EndpointCard.vue +169 -0
  8. package/app/components/Api/SchemaDocsTable.vue +208 -0
  9. package/app/components/Asset/Card.vue +1363 -0
  10. package/app/components/Asset/Grid.vue +295 -0
  11. package/app/components/Asset/Icon.vue +38 -0
  12. package/app/components/Asset/Preview.vue +486 -0
  13. package/app/components/Asset/Scanner.vue +700 -0
  14. package/app/components/Asset/Thumb.vue +708 -0
  15. package/app/components/Auth/Reset.vue +99 -0
  16. package/app/components/Auth/index.vue +346 -0
  17. package/app/components/ChatInterface.vue +2326 -0
  18. package/app/components/ChatPagePreview.vue +34 -0
  19. package/app/components/Column/Array.vue +29 -0
  20. package/app/components/Column/Asset.vue +21 -0
  21. package/app/components/Column/Boolean.vue +12 -0
  22. package/app/components/Column/Color.vue +9 -0
  23. package/app/components/Column/Date.vue +57 -0
  24. package/app/components/Column/Edit.vue +215 -0
  25. package/app/components/Column/Email.vue +9 -0
  26. package/app/components/Column/Html.vue +27 -0
  27. package/app/components/Column/Id.vue +20 -0
  28. package/app/components/Column/Locale.vue +32 -0
  29. package/app/components/Column/Object.vue +18 -0
  30. package/app/components/Column/Password.vue +7 -0
  31. package/app/components/Column/Role.vue +15 -0
  32. package/app/components/Column/S.vue +14 -0
  33. package/app/components/Column/Table/Single.vue +49 -0
  34. package/app/components/Column/Table/index.vue +42 -0
  35. package/app/components/Column/Tags.vue +31 -0
  36. package/app/components/Column/Text.vue +9 -0
  37. package/app/components/Column/Url.vue +9 -0
  38. package/app/components/Column/index.vue +41 -0
  39. package/app/components/Dashboard/Editor.vue +221 -0
  40. package/app/components/Dashboard/Grid.vue +270 -0
  41. package/app/components/Dashboard/View.vue +131 -0
  42. package/app/components/Dashboard/Widget/BarChart.vue +73 -0
  43. package/app/components/Dashboard/Widget/Counter.vue +51 -0
  44. package/app/components/Dashboard/Widget/LineChart.vue +76 -0
  45. package/app/components/Dashboard/Widget/PieChart.vue +75 -0
  46. package/app/components/Dashboard/Widget/Table.vue +94 -0
  47. package/app/components/Dashboard/WidgetEditor.vue +274 -0
  48. package/app/components/Data/Array.vue +102 -0
  49. package/app/components/Data/Html.vue +9 -0
  50. package/app/components/Data/Icon.vue +9 -0
  51. package/app/components/Data/Object.vue +10 -0
  52. package/app/components/Data/S.vue +16 -0
  53. package/app/components/Data/Table.vue +53 -0
  54. package/app/components/Data/Value.vue +34 -0
  55. package/app/components/Data/index.vue +14 -0
  56. package/app/components/Field/Array.vue +324 -0
  57. package/app/components/Field/Asset/Actions.vue +97 -0
  58. package/app/components/Field/Asset/index.vue +639 -0
  59. package/app/components/Field/Boolean.vue +26 -0
  60. package/app/components/Field/Checkbox.vue +48 -0
  61. package/app/components/Field/Color.vue +27 -0
  62. package/app/components/Field/Date.vue +30 -0
  63. package/app/components/Field/Email.vue +51 -0
  64. package/app/components/Field/EmailTemplate.vue +420 -0
  65. package/app/components/Field/Html/ColorPicker.vue +127 -0
  66. package/app/components/Field/Html/Editor.vue +544 -0
  67. package/app/components/Field/Html/index.vue +25 -0
  68. package/app/components/Field/Icon.vue +114 -0
  69. package/app/components/Field/JSON.vue +113 -0
  70. package/app/components/Field/Mention.vue +36 -0
  71. package/app/components/Field/Number.vue +27 -0
  72. package/app/components/Field/Object.vue +92 -0
  73. package/app/components/Field/Password.vue +55 -0
  74. package/app/components/Field/Radio.vue +47 -0
  75. package/app/components/Field/S.vue +44 -0
  76. package/app/components/Field/Select.vue +49 -0
  77. package/app/components/Field/Table.vue +348 -0
  78. package/app/components/Field/Tags.vue +113 -0
  79. package/app/components/Field/Text.vue +31 -0
  80. package/app/components/Field/Textarea.vue +33 -0
  81. package/app/components/Field/Url.vue +40 -0
  82. package/app/components/Field/Wrapper.vue +128 -0
  83. package/app/components/Field/index.vue +68 -0
  84. package/app/components/FloatingChatbot.vue +73 -0
  85. package/app/components/Form/Card.vue +151 -0
  86. package/app/components/Form/Drawer.vue +195 -0
  87. package/app/components/Form/index.vue +621 -0
  88. package/app/components/Header.vue +328 -0
  89. package/app/components/Offline/ConflictModal.vue +200 -0
  90. package/app/components/Offline/SyncStatus.vue +249 -0
  91. package/app/components/PlatformFeatureLanding.vue +65 -0
  92. package/app/components/Table/Backups.vue +638 -0
  93. package/app/components/Table/Flows.vue +814 -0
  94. package/app/components/Table/Grid.vue +296 -0
  95. package/app/components/Table/Icon.vue +36 -0
  96. package/app/components/Table/Logs.vue +181 -0
  97. package/app/components/Table/Schedules.vue +722 -0
  98. package/app/components/Table/Search/Button.vue +256 -0
  99. package/app/components/Table/Search/Items.vue +286 -0
  100. package/app/components/Table/Search/index.vue +87 -0
  101. package/app/components/Table/Settings/Schema.vue +801 -0
  102. package/app/components/Table/Settings/index.vue +631 -0
  103. package/app/components/Table/TranslateDrawer.vue +526 -0
  104. package/app/components/Table/Views/Kanban.vue +289 -0
  105. package/app/components/Table/Views/Table.vue +1273 -0
  106. package/app/components/Table/index.vue +890 -0
  107. package/app/composables/Translation/define.ts +41 -0
  108. package/app/composables/Translation/fetch.ts +107 -0
  109. package/app/composables/Translation/languages.ts +1 -0
  110. package/app/composables/Translation/loadCoreTranslations.ts +38 -0
  111. package/app/composables/Translation/translate.ts +250 -0
  112. package/app/composables/databaseCookies.ts +79 -0
  113. package/app/composables/fieldValidator.ts +72 -0
  114. package/app/composables/fieldsList.ts +301 -0
  115. package/app/composables/formatDatabase.ts +101 -0
  116. package/app/composables/index.ts +444 -0
  117. package/app/composables/openDrawer.ts +118 -0
  118. package/app/composables/optimizeFile.ts +287 -0
  119. package/app/composables/renderLabel.ts +88 -0
  120. package/app/composables/schemaClipboard.ts +95 -0
  121. package/app/composables/search.ts +353 -0
  122. package/app/composables/setThemeConfig.ts +48 -0
  123. package/app/composables/translationValue.ts +169 -0
  124. package/app/composables/useAssetPreview.ts +84 -0
  125. package/app/composables/useAssetUploader.ts +56 -0
  126. package/app/composables/useDashboardData.ts +310 -0
  127. package/app/composables/useDocumentThumbnail.ts +351 -0
  128. package/app/composables/useEcharts.ts +33 -0
  129. package/app/composables/useLocalDatabaseConfig.ts +51 -0
  130. package/app/composables/useOcr.ts +300 -0
  131. package/app/composables/useOfflineFetch.ts +194 -0
  132. package/app/composables/useOfflineItem.ts +107 -0
  133. package/app/composables/useOfflineQueue.ts +302 -0
  134. package/app/composables/useOfflineSync.ts +235 -0
  135. package/app/composables/usePasskeyAuth.ts +156 -0
  136. package/app/composables/usePdfCompressor.ts +180 -0
  137. package/app/composables/usePlatformRedirect.ts +72 -0
  138. package/app/composables/useRealtimeSync.ts +233 -0
  139. package/app/composables/useReferencedItems.ts +180 -0
  140. package/app/composables/useScanner.ts +664 -0
  141. package/app/composables/useSubscription.ts +650 -0
  142. package/app/composables/useThumbnailCache.ts +136 -0
  143. package/app/composables/useVideoCompressor.ts +194 -0
  144. package/app/error.vue +112 -0
  145. package/app/layouts/api.vue +188 -0
  146. package/app/layouts/dashboard.vue +91 -0
  147. package/app/layouts/default.vue +228 -0
  148. package/app/layouts/table.vue +471 -0
  149. package/app/locales/ar.ts +996 -0
  150. package/app/locales/en.ts +655 -0
  151. package/app/locales/es.ts +996 -0
  152. package/app/locales/fr.ts +997 -0
  153. package/app/middleware/dashboard.ts +33 -0
  154. package/app/middleware/database.ts +83 -0
  155. package/app/middleware/global.ts +1 -0
  156. package/app/middleware/sid-ingest.global.ts +29 -0
  157. package/app/middleware/table.ts +54 -0
  158. package/app/middleware/user.ts +26 -0
  159. package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
  160. package/app/pages/[[database]]/admin/api/index.vue +18 -0
  161. package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
  162. package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
  163. package/app/pages/[[database]]/admin/billing/index.vue +16 -0
  164. package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
  165. package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
  166. package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
  167. package/app/pages/[[database]]/admin/index.vue +28 -0
  168. package/app/pages/[[database]]/admin/settings.vue +526 -0
  169. package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
  170. package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
  171. package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
  172. package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
  173. package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
  174. package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
  175. package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
  176. package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
  177. package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
  178. package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
  179. package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
  180. package/app/pages/[[database]]/admin/tables/index.vue +20 -0
  181. package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
  182. package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
  183. package/app/pages/[[database]]/api/index.vue +120 -0
  184. package/app/pages/[[database]]/auth/index.vue +12 -0
  185. package/app/pages/[[database]]/auth/reset.vue +10 -0
  186. package/app/pages/[[database]]/index.vue +10 -0
  187. package/app/plugins/offline-warmup.client.ts +53 -0
  188. package/app/plugins/template-globals.ts +9 -0
  189. package/app/spa-loading-template.html +93 -0
  190. package/biome.json +53 -0
  191. package/index.d.ts +380 -0
  192. package/modules/naiveui.ts +41 -0
  193. package/nuxt.config.ts +269 -0
  194. package/package.json +56 -0
  195. package/pnpm-workspace.yaml +5 -0
  196. package/public/apple-touch-icon.png +0 -0
  197. package/public/favicon-16x16.png +0 -0
  198. package/public/favicon-32x32.png +0 -0
  199. package/public/favicon.ico +0 -0
  200. package/public/pwa-192x192.png +0 -0
  201. package/public/pwa-512x512.png +0 -0
  202. package/public/pwa-maskable-192x192.png +0 -0
  203. package/public/pwa-maskable-512x512.png +0 -0
  204. package/public/vendor/opencv.js +48 -0
  205. package/tsconfig.json +9 -0
@@ -0,0 +1,486 @@
1
+ <template>
2
+ <!-- class="assetLightbox" is load-bearing: Form/Drawer.vue's Esc handler and the :has(.assetLightbox) z-index rule in main.css depend on it -->
3
+ <NModal v-if="activeAsset" class="assetLightbox" :show="showPreview" :auto-focus="false" :mask-closable="true"
4
+ :close-on-esc="true" @mask-click="closePreview" @close="closePreview" @esc="closePreview"
5
+ preset="card"
6
+ style="width: min(90vw, 1080px);max-width: 1080px;"
7
+ :segmented="{ content: true, footer: true }"
8
+ :show-mask="false"
9
+ :title="`${activeAsset.name}${activeAsset.extension ? `.${activeAsset.extension}` : ''}`">
10
+ <template #header-extra>
11
+ <NText depth="3" style="margin:0 10px">{{ previewAssetIndex + 1 }} / {{ previewAssetList.length }}</NText>
12
+ </template>
13
+ <div class="assetLightboxBody">
14
+ <video v-if="isVideoAsset(activeAsset)" :key="activeAsset.id" controls autoplay playsinline
15
+ class="assetLightboxMedia" @loadeddata="Loading.previewModal = false"
16
+ @canplay="Loading.previewModal = false" @error="Loading.previewModal = false">
17
+ <source :src="activeAsset.publicURL" :type="activeAsset.type" />
18
+ </video>
19
+ <NImage v-else-if="isImageAsset(activeAsset)" :src="activeAsset.publicURL" preview-disabled
20
+ class="assetLightboxMedia" object-fit="contain" :img-props="{ style: imageTransformStyle }" />
21
+ <div v-else-if="isPdfAsset(activeAsset)" ref="pdfPanelRef" class="assetPdfPanel" @scroll.passive="updatePdfCurrentPage">
22
+ <div ref="pdfPagesContainerRef" class="assetPdfPages" />
23
+ <NText v-if="pdfError" depth="3">{{ pdfError }}</NText>
24
+ <div v-if="pdfPageCount" class="assetPdfPageBadge">{{ pdfCurrentPage }} / {{ pdfPageCount }}</div>
25
+ </div>
26
+ <iframe v-else-if="isHtmlAsset(activeAsset)" :src="activeAsset.publicURL" class="assetLightboxFrame"
27
+ title="HTML Preview" loading="lazy" referrerpolicy="no-referrer" />
28
+ <div v-else class="assetDocPanel">
29
+ <LazyAssetThumb :asset="activeAsset" :hide-tooltip="true" :size="340" :preview-disabled="true"
30
+ :disable-default-click-action="true" @click="openAssetInNewTab(activeAsset)" />
31
+ <button class="assetDocOpenButton" type="button" @click="openAssetInNewTab(activeAsset)">
32
+ <NIcon :size="30">
33
+ <Icon name="tabler:external-link" />
34
+ </NIcon>
35
+ </button>
36
+ </div>
37
+ </div>
38
+ <template #footer>
39
+ <NFlex v-if="isPdfAsset(activeAsset)" justify="center" aria-label="PDF controls">
40
+ <NButton circle tertiary size="large" @click="openAssetInNewTab(activeAsset)">
41
+ <NIcon :size="18">
42
+ <Icon name="tabler:external-link" />
43
+ </NIcon>
44
+ </NButton>
45
+ </NFlex>
46
+ <NFlex v-if="isImageAsset(activeAsset)" justify="center" align="center" aria-label="Image rotation controls">
47
+ <NButton circle tertiary @click="rotatePreviewImageLeft">
48
+ <NIcon :size="18">
49
+ <Icon name="tabler:rotate-2" />
50
+ </NIcon>
51
+ </NButton>
52
+ <NButton circle tertiary size="large" @click="resetPreviewImageRotation">
53
+ <NIcon :size="18">
54
+ <Icon name="tabler:reload" />
55
+ </NIcon>
56
+ </NButton>
57
+ <NButton circle tertiary @click="rotatePreviewImageRight">
58
+ <NIcon :size="18">
59
+ <Icon name="tabler:rotate-clockwise-2" />
60
+ </NIcon>
61
+ </NButton>
62
+ </NFlex>
63
+ </template>
64
+ <NButton v-if="previewAssetList.length > 1" circle tertiary size="large" class="assetLightboxNav assetLightboxPrev" @click="showPrevByLanguage">
65
+ <NIcon :size="22">
66
+ <Icon :name="Language === 'ar' ? 'tabler:chevron-right' : 'tabler:chevron-left'" />
67
+ </NIcon>
68
+ </NButton>
69
+ <NButton v-if="previewAssetList.length > 1" circle tertiary size="large" class="assetLightboxNav assetLightboxNext" type="button" @click="showNextByLanguage">
70
+ <NIcon :size="22">
71
+ <Icon :name="Language === 'ar' ? 'tabler:chevron-left' : 'tabler:chevron-right'" />
72
+ </NIcon>
73
+ </NButton>
74
+ </NModal>
75
+ </template>
76
+
77
+ <script setup lang="ts">
78
+ import { Icon, NIcon } from "#components";
79
+ import { imageExtensions, videoExtensions } from "~/composables";
80
+ import { useAssetPreview } from "~/composables/useAssetPreview";
81
+
82
+ const database = useState<Database>("database");
83
+ const Language = useScopedCookie<LanguagesType>("language", database.value?.slug);
84
+ const Loading = useState<Record<string, boolean>>("Loading", () => ({}));
85
+ const {
86
+ currentPreviewAsset,
87
+ previewAssetList,
88
+ previewAssetIndex,
89
+ previewImageRotation,
90
+ closePreview,
91
+ showPrevPreviewAsset,
92
+ showNextPreviewAsset,
93
+ rotatePreviewImageLeft,
94
+ rotatePreviewImageRight,
95
+ resetPreviewImageRotation,
96
+ } = useAssetPreview();
97
+
98
+ const showPreview = computed(() => !!currentPreviewAsset.value);
99
+ const activeAsset = computed(() => currentPreviewAsset.value);
100
+ const pdfPagesContainerRef = ref<HTMLDivElement>();
101
+ const pdfPanelRef = ref<HTMLDivElement>();
102
+ const pdfError = ref("");
103
+ const pdfPageCount = ref(0);
104
+ const pdfCurrentPage = ref(1);
105
+
106
+ type PdfViewport = {
107
+ width: number;
108
+ height: number;
109
+ };
110
+
111
+ type PdfRenderTask = {
112
+ promise: Promise<void>;
113
+ cancel: () => void;
114
+ };
115
+
116
+ type PdfPage = {
117
+ getViewport: (params: { scale: number }) => PdfViewport;
118
+ render: (params: {
119
+ canvasContext: CanvasRenderingContext2D;
120
+ viewport: PdfViewport;
121
+ canvas: HTMLCanvasElement;
122
+ }) => PdfRenderTask;
123
+ };
124
+
125
+ type PdfDocument = {
126
+ numPages: number;
127
+ getPage: (pageNumber: number) => Promise<PdfPage>;
128
+ destroy: () => Promise<void>;
129
+ };
130
+
131
+ let pdfjsLib: Awaited<typeof import("pdfjs-dist")> | undefined;
132
+ let pdfDocument: PdfDocument | undefined;
133
+ let pdfLoadToken = 0;
134
+
135
+ const imageTransformStyle = computed(() => {
136
+ const rotation = previewImageRotation.value % 360;
137
+ return `transform: rotate(${rotation}deg); transform-origin: center center; transition: transform .2s ease;width:100%;`;
138
+ });
139
+
140
+ function isVideoAsset(asset: Asset) {
141
+ return asset.type !== "dir" && videoExtensions.includes(asset.extension);
142
+ }
143
+
144
+ function isImageAsset(asset: Asset) {
145
+ return asset.type !== "dir" && imageExtensions.includes(asset.extension);
146
+ }
147
+
148
+ function isPdfAsset(asset: Asset) {
149
+ if (asset.type === "dir") return false;
150
+ const ext = String(asset.extension || "").toLowerCase();
151
+ return ext === "pdf" || asset.type === "application/pdf";
152
+ }
153
+
154
+ function isHtmlAsset(asset: Asset) {
155
+ if (asset.type === "dir") return false;
156
+ const ext = String(asset.extension || "").toLowerCase();
157
+ return ext === "html" || ext === "htm" || asset.type === "text/html";
158
+ }
159
+
160
+ function openAssetInNewTab(asset?: Asset) {
161
+ if (!asset?.publicURL) return;
162
+ window.open(asset.publicURL, "_blank", "noopener");
163
+ }
164
+
165
+ function showPrevByLanguage() {
166
+ Language.value === "ar" ? showNextPreviewAsset() : showPrevPreviewAsset();
167
+ }
168
+
169
+ function showNextByLanguage() {
170
+ Language.value === "ar" ? showPrevPreviewAsset() : showNextPreviewAsset();
171
+ }
172
+
173
+ async function ensurePdfJsLoaded() {
174
+ if (!pdfjsLib) {
175
+ pdfjsLib = await import("pdfjs-dist");
176
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version}/build/pdf.worker.min.mjs`;
177
+ }
178
+ return pdfjsLib;
179
+ }
180
+
181
+ async function cleanupPdfDocument() {
182
+ if (!pdfDocument) return;
183
+ try {
184
+ await pdfDocument.destroy();
185
+ } catch {
186
+ // Ignore teardown errors.
187
+ }
188
+ pdfDocument = undefined;
189
+ }
190
+
191
+ function updatePdfCurrentPage() {
192
+ const panel = pdfPanelRef.value;
193
+ const container = pdfPagesContainerRef.value;
194
+ if (!panel || !container || !pdfPageCount.value) return;
195
+ const canvases = Array.from(
196
+ container.querySelectorAll<HTMLCanvasElement>(".assetPdfCanvas"),
197
+ );
198
+ if (!canvases.length) return;
199
+ const marker = panel.getBoundingClientRect().top + panel.clientHeight * 0.3;
200
+ let current = 1;
201
+ for (const [index, canvas] of canvases.entries()) {
202
+ if (canvas.getBoundingClientRect().top <= marker) current = index + 1;
203
+ }
204
+ pdfCurrentPage.value = current;
205
+ }
206
+
207
+ async function renderAllPdfPages() {
208
+ await nextTick();
209
+ if (!pdfDocument || !pdfPagesContainerRef.value) return;
210
+ const token = pdfLoadToken;
211
+ const container = pdfPagesContainerRef.value;
212
+ const panel = pdfPanelRef.value;
213
+ container.innerHTML = "";
214
+
215
+ const availableWidth = (panel?.clientWidth || container.clientWidth || 800) - 8;
216
+ const availableHeight = panel?.clientHeight || 0;
217
+ const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
218
+ const qualityBoost = 1.5;
219
+
220
+ for (let pageNum = 1; pageNum <= pdfPageCount.value; pageNum++) {
221
+ if (token !== pdfLoadToken) return;
222
+ const page = await pdfDocument.getPage(pageNum);
223
+ if (token !== pdfLoadToken) return;
224
+ const baseViewport = page.getViewport({ scale: 1 });
225
+ // Fit each page inside the modal viewport instead of stretching it to full width.
226
+ const widthScale = availableWidth / baseViewport.width;
227
+ const heightScale = availableHeight
228
+ ? availableHeight / baseViewport.height
229
+ : widthScale;
230
+ const fitScale = Math.max(Math.min(widthScale, heightScale), 0.1);
231
+ const oversample = pixelRatio * qualityBoost;
232
+ const renderScale = Math.min(fitScale * oversample, fitScale * 4);
233
+ const viewport = page.getViewport({ scale: renderScale });
234
+ const canvas = document.createElement("canvas");
235
+ canvas.className = "assetPdfCanvas";
236
+ canvas.width = viewport.width;
237
+ canvas.height = viewport.height;
238
+ // Display at the fitted size; oversample only adds render resolution, not layout size.
239
+ canvas.style.width = `${(baseViewport.width * fitScale)}px`;
240
+ canvas.style.height = `${(baseViewport.height * fitScale)}px`;
241
+ container.appendChild(canvas);
242
+ const context = canvas.getContext("2d");
243
+ if (!context) continue;
244
+ const renderTask = page.render({ canvasContext: context, viewport, canvas });
245
+ await renderTask.promise;
246
+ }
247
+ updatePdfCurrentPage();
248
+ }
249
+
250
+ async function initializePdfPreview(asset: Asset) {
251
+ pdfLoadToken += 1;
252
+ const currentLoadToken = pdfLoadToken;
253
+ Loading.value.previewModal = true;
254
+ pdfError.value = "";
255
+ pdfPageCount.value = 0;
256
+ pdfCurrentPage.value = 1;
257
+
258
+ try {
259
+ await cleanupPdfDocument();
260
+ const library = await ensurePdfJsLoaded();
261
+ const loadingTask = library.getDocument({
262
+ url: asset.publicURL as string,
263
+ // Needed so non-embedded/base-14 fonts get correct glyph widths instead of garbled spacing.
264
+ cMapUrl: `https://unpkg.com/pdfjs-dist@${library.version}/cmaps/`,
265
+ cMapPacked: true,
266
+ standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${library.version}/standard_fonts/`,
267
+ // Fall back to the system's Arabic/CJK fonts when they aren't embedded in the PDF.
268
+ useSystemFonts: true,
269
+ // Draw embedded glyph outlines directly instead of via @font-face+fillText, which breaks Arabic letter joining for some font subsets.
270
+ disableFontFace: true,
271
+ });
272
+ const document = (await loadingTask.promise) as unknown as PdfDocument;
273
+
274
+ if (currentLoadToken !== pdfLoadToken) {
275
+ await document.destroy();
276
+ return;
277
+ }
278
+
279
+ pdfDocument = document;
280
+ pdfPageCount.value = document.numPages || 1;
281
+ await renderAllPdfPages();
282
+ } catch {
283
+ pdfError.value = "Unable to preview this PDF file.";
284
+ } finally {
285
+ if (currentLoadToken === pdfLoadToken) {
286
+ Loading.value.previewModal = false;
287
+ }
288
+ }
289
+ }
290
+
291
+ function onPreviewKeydown(event: KeyboardEvent) {
292
+ if (!showPreview.value) return;
293
+ if (event.key === "Escape") {
294
+ event.preventDefault();
295
+ closePreview();
296
+ }
297
+ if (event.key === "ArrowLeft") {
298
+ event.preventDefault();
299
+ showPrevByLanguage();
300
+ }
301
+ if (event.key === "ArrowRight") {
302
+ event.preventDefault();
303
+ showNextByLanguage();
304
+ }
305
+ }
306
+
307
+ watch(currentPreviewAsset, (asset) => {
308
+ if (!asset) {
309
+ pdfLoadToken += 1;
310
+ void cleanupPdfDocument();
311
+ pdfError.value = "";
312
+ pdfPageCount.value = 0;
313
+ pdfCurrentPage.value = 1;
314
+ Loading.value.previewModal = false;
315
+ return;
316
+ }
317
+ if (isPdfAsset(asset)) {
318
+ void initializePdfPreview(asset);
319
+ return;
320
+ }
321
+ pdfLoadToken += 1;
322
+ void cleanupPdfDocument();
323
+ pdfError.value = "";
324
+ pdfPageCount.value = 0;
325
+ pdfCurrentPage.value = 1;
326
+ Loading.value.previewModal = isVideoAsset(asset);
327
+ });
328
+
329
+ onMounted(() => {
330
+ window.addEventListener("keydown", onPreviewKeydown);
331
+ });
332
+
333
+ onBeforeUnmount(() => {
334
+ window.removeEventListener("keydown", onPreviewKeydown);
335
+ void cleanupPdfDocument();
336
+ });
337
+ </script>
338
+
339
+ <style scoped>
340
+ .assetLightboxBody {
341
+ position: relative;
342
+ display: flex;
343
+ align-items: center;
344
+ justify-content: center;
345
+ min-height: min(72vh, 760px);
346
+ text-align: center;
347
+ }
348
+
349
+ .assetLightboxMedia {
350
+ width: 100%;
351
+ max-height: min(72vh, 760px);
352
+ object-fit: contain;
353
+ margin: 0 auto;
354
+ }
355
+
356
+ .assetLightboxFrame {
357
+ width: 100%;
358
+ height: min(72vh, 760px);
359
+ border: 0;
360
+ border-radius: 10px;
361
+ background: #fff;
362
+ }
363
+
364
+ .assetPdfPanel {
365
+ position: relative;
366
+ display: flex;
367
+ flex-direction: column;
368
+ align-items: center;
369
+ gap: 10px;
370
+ width: 100%;
371
+ height: min(72vh, 760px);
372
+ max-height: min(72vh, 760px);
373
+ overflow-x: hidden;
374
+ overflow-y: auto;
375
+ padding: 4px;
376
+ }
377
+
378
+ .assetPdfPageBadge {
379
+ position: sticky;
380
+ bottom: 8px;
381
+ align-self: center;
382
+ padding: 4px 12px;
383
+ border-radius: 999px;
384
+ font-size: 12px;
385
+ color: #fff;
386
+ background: color-mix(in srgb, #000 62%, transparent);
387
+ pointer-events: none;
388
+ }
389
+
390
+ .assetPdfPages {
391
+ display: flex;
392
+ flex-direction: column;
393
+ align-items: center;
394
+ gap: 12px;
395
+ width: 100%;
396
+ }
397
+
398
+ .assetPdfCanvas {
399
+ max-width: 100%;
400
+ background: #fff;
401
+ border-radius: 10px;
402
+ box-shadow: 0 6px 20px color-mix(in srgb, #000 14%, transparent);
403
+ }
404
+
405
+ .assetPdfControls {
406
+ display: flex;
407
+ align-items: center;
408
+ justify-content: center;
409
+ gap: 12px;
410
+ padding-top: 12px;
411
+ }
412
+
413
+ .assetDocPanel {
414
+ position: relative;
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: center;
418
+ padding: 24px;
419
+ width: 100%;
420
+ }
421
+
422
+ .assetDocOpenButton {
423
+ position: absolute;
424
+ top: 50%;
425
+ left: 50%;
426
+ transform: translate(-50%, -50%);
427
+ border: 0;
428
+ width: 68px;
429
+ height: 68px;
430
+ border-radius: 999px;
431
+ display: inline-flex;
432
+ align-items: center;
433
+ justify-content: center;
434
+ cursor: pointer;
435
+ color: var(--n-text-color);
436
+ background: color-mix(in srgb, var(--n-color) 70%, #000 30%);
437
+ box-shadow: 0 6px 24px color-mix(in srgb, #000 30%, transparent);
438
+ backdrop-filter: blur(4px);
439
+ }
440
+
441
+ .assetLightboxControls {
442
+ display: flex;
443
+ justify-content: center;
444
+ align-items: center;
445
+ gap: 10px;
446
+ padding-top: 12px;
447
+ }
448
+
449
+ .assetLightboxControl {
450
+ border: 0;
451
+ width: 38px;
452
+ height: 38px;
453
+ border-radius: 999px;
454
+ display: inline-flex;
455
+ align-items: center;
456
+ justify-content: center;
457
+ cursor: pointer;
458
+ color: var(--n-text-color);
459
+ background: color-mix(in srgb, var(--n-color) 76%, #000 24%);
460
+ }
461
+
462
+ .assetLightboxNav {
463
+ position: absolute;
464
+ top: 50%;
465
+ transform: translateY(-50%);
466
+ z-index: 3;
467
+ border: 0;
468
+ display: inline-flex;
469
+ align-items: center;
470
+ justify-content: center;
471
+ cursor: pointer;
472
+ }
473
+
474
+ .assetLightboxNav:disabled {
475
+ opacity: 0.35;
476
+ cursor: default;
477
+ }
478
+
479
+ .assetLightboxPrev {
480
+ left: 14px;
481
+ }
482
+
483
+ .assetLightboxNext {
484
+ right: 14px;
485
+ }
486
+ </style>