@walkthru-earth/objex 1.3.1 → 1.5.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 (199) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +28 -20
  3. package/dist/components/browser/FileTreeSidebar.svelte +32 -17
  4. package/dist/components/layout/AboutSheet.svelte +5 -2
  5. package/dist/components/layout/ConnectionDialog.svelte +7 -2
  6. package/dist/components/layout/SettingsSheet.svelte +238 -0
  7. package/dist/components/layout/SettingsSheet.svelte.d.ts +6 -0
  8. package/dist/components/layout/Sidebar.svelte +73 -6
  9. package/dist/components/layout/Sidebar.svelte.d.ts +4 -1
  10. package/dist/components/layout/StatusBar.svelte +17 -14
  11. package/dist/components/layout/TabBar.svelte +4 -4
  12. package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
  13. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
  14. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  15. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  16. package/dist/components/ui/resizable/index.d.ts +1 -1
  17. package/dist/components/ui/resizable/index.js +2 -2
  18. package/dist/components/ui/slider/index.d.ts +3 -0
  19. package/dist/components/ui/slider/index.js +5 -0
  20. package/dist/components/ui/slider/range-slider.svelte +94 -0
  21. package/dist/components/ui/slider/range-slider.svelte.d.ts +21 -0
  22. package/dist/components/ui/slider/slider.svelte +83 -0
  23. package/dist/components/ui/slider/slider.svelte.d.ts +7 -0
  24. package/dist/components/viewers/ArchiveViewer.svelte +140 -113
  25. package/dist/components/viewers/CodeViewer.svelte +45 -48
  26. package/dist/components/viewers/CodeViewer.svelte.d.ts +1 -1
  27. package/dist/components/viewers/CogControls.svelte +338 -184
  28. package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
  29. package/dist/components/viewers/CogViewer.svelte +269 -116
  30. package/dist/components/viewers/CopcViewer.svelte +8 -15
  31. package/dist/components/viewers/DatabaseViewer.svelte +22 -21
  32. package/dist/components/viewers/FileInfo.svelte +16 -16
  33. package/dist/components/viewers/FlatGeobufViewer.svelte +16 -46
  34. package/dist/components/viewers/GeoParquetMapViewer.svelte +11 -9
  35. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
  36. package/dist/components/viewers/ImageViewer.svelte +12 -14
  37. package/dist/components/viewers/LoadProgress.svelte +6 -6
  38. package/dist/components/viewers/MarkdownViewer.svelte +29 -30
  39. package/dist/components/viewers/MediaViewer.svelte +13 -14
  40. package/dist/components/viewers/ModelViewer.svelte +18 -21
  41. package/dist/components/viewers/MultiCogViewer.svelte +474 -106
  42. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
  43. package/dist/components/viewers/NotebookViewer.svelte +28 -29
  44. package/dist/components/viewers/PdfViewer.svelte +24 -33
  45. package/dist/components/viewers/PmtilesViewer.svelte +13 -15
  46. package/dist/components/viewers/QueryHistoryPanel.svelte +18 -18
  47. package/dist/components/viewers/RawViewer.svelte +27 -21
  48. package/dist/components/viewers/StacMapViewer.svelte +6 -13
  49. package/dist/components/viewers/StacMosaicViewer.svelte +1764 -410
  50. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
  51. package/dist/components/viewers/StacTabViewer.svelte +26 -15
  52. package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
  53. package/dist/components/viewers/TableGrid.svelte +38 -34
  54. package/dist/components/viewers/TableStatusBar.svelte +7 -7
  55. package/dist/components/viewers/TableToolbar.svelte +10 -9
  56. package/dist/components/viewers/TableViewer.svelte +47 -30
  57. package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
  58. package/dist/components/viewers/ViewerHeader.svelte +18 -0
  59. package/dist/components/viewers/ViewerHeader.svelte.d.ts +10 -0
  60. package/dist/components/viewers/ViewerRouter.svelte +16 -8
  61. package/dist/components/viewers/ViewerStatus.svelte +19 -0
  62. package/dist/components/viewers/ViewerStatus.svelte.d.ts +7 -0
  63. package/dist/components/viewers/ZarrMapViewer.svelte +24 -21
  64. package/dist/components/viewers/ZarrViewer.svelte +98 -65
  65. package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
  66. package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
  67. package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
  68. package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
  69. package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
  70. package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
  71. package/dist/components/viewers/map/AttributeTable.svelte +7 -7
  72. package/dist/components/viewers/map/MapContainer.svelte +38 -12
  73. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +109 -83
  74. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +16 -16
  75. package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
  76. package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
  77. package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
  78. package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
  79. package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
  80. package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
  81. package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
  82. package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
  83. package/dist/constants.d.ts +6 -0
  84. package/dist/constants.js +8 -0
  85. package/dist/file-icons/index.d.ts +1 -1
  86. package/dist/file-icons/index.js +1 -1
  87. package/dist/i18n/ar.js +113 -2
  88. package/dist/i18n/en.js +113 -2
  89. package/dist/index.d.ts +2 -28
  90. package/dist/index.js +7 -23
  91. package/dist/query/engine.d.ts +10 -0
  92. package/dist/query/source.js +1 -1
  93. package/dist/query/stac-source-factory.d.ts +65 -0
  94. package/dist/query/stac-source-factory.js +77 -0
  95. package/dist/query/stac-source-parquet.d.ts +135 -0
  96. package/dist/query/stac-source-parquet.js +468 -0
  97. package/dist/query/wasm.d.ts +8 -0
  98. package/dist/query/wasm.js +310 -65
  99. package/dist/storage/presign.js +3 -2
  100. package/dist/storage/providers.js +7 -6
  101. package/dist/stores/config.svelte.d.ts +15 -0
  102. package/dist/stores/config.svelte.js +46 -0
  103. package/dist/stores/connections.svelte.d.ts +2 -2
  104. package/dist/stores/connections.svelte.js +1 -2
  105. package/dist/stores/files.svelte.d.ts +1 -1
  106. package/dist/stores/files.svelte.js +1 -1
  107. package/dist/stores/query-history.svelte.js +1 -1
  108. package/dist/stores/settings.svelte.d.ts +16 -1
  109. package/dist/stores/settings.svelte.js +104 -48
  110. package/dist/stores/tabs.svelte.d.ts +3 -0
  111. package/dist/stores/tabs.svelte.js +17 -0
  112. package/dist/utils/cog-histogram.d.ts +121 -0
  113. package/dist/utils/cog-histogram.js +424 -0
  114. package/dist/utils/cog.d.ts +177 -20
  115. package/dist/utils/cog.js +361 -76
  116. package/dist/utils/colormap-sprite.d.ts +0 -9
  117. package/dist/utils/colormap-sprite.js +0 -21
  118. package/dist/utils/deck.d.ts +18 -12
  119. package/dist/utils/deck.js +15 -7
  120. package/dist/utils/media-query.svelte.d.ts +14 -0
  121. package/dist/utils/media-query.svelte.js +29 -0
  122. package/dist/utils/pmtiles-tile.js +2 -2
  123. package/dist/utils/signed-url-effect.d.ts +7 -0
  124. package/dist/utils/signed-url-effect.js +19 -0
  125. package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
  126. package/dist/utils/{url.js → signed-url.js} +32 -10
  127. package/dist/utils/url-state.d.ts +36 -0
  128. package/dist/utils/url-state.js +72 -2
  129. package/dist/utils/zarr-tab.d.ts +1 -2
  130. package/dist/utils/zarr-tab.js +1 -2
  131. package/dist/utils/zarr.d.ts +0 -17
  132. package/dist/utils/zarr.js +1 -45
  133. package/package.json +55 -84
  134. package/dist/components/browser/Breadcrumb.svelte +0 -50
  135. package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
  136. package/dist/components/browser/CreateFolderDialog.svelte +0 -98
  137. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
  138. package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
  139. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
  140. package/dist/components/browser/DropZone.svelte +0 -83
  141. package/dist/components/browser/DropZone.svelte.d.ts +0 -7
  142. package/dist/components/browser/FileBrowser.svelte +0 -252
  143. package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
  144. package/dist/components/browser/FileRow.svelte +0 -117
  145. package/dist/components/browser/FileRow.svelte.d.ts +0 -9
  146. package/dist/components/browser/RenameDialog.svelte +0 -101
  147. package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
  148. package/dist/components/browser/SearchBar.svelte +0 -40
  149. package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
  150. package/dist/components/browser/UploadButton.svelte +0 -65
  151. package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
  152. package/dist/query/stac-geoparquet.d.ts +0 -31
  153. package/dist/query/stac-geoparquet.js +0 -136
  154. package/dist/utils/clipboard.d.ts +0 -13
  155. package/dist/utils/clipboard.js +0 -38
  156. package/dist/utils/cloud-url.d.ts +0 -27
  157. package/dist/utils/cloud-url.js +0 -61
  158. package/dist/utils/cog-pure.d.ts +0 -25
  159. package/dist/utils/cog-pure.js +0 -35
  160. package/dist/utils/column-types.d.ts +0 -5
  161. package/dist/utils/column-types.js +0 -137
  162. package/dist/utils/connection-identity.d.ts +0 -51
  163. package/dist/utils/connection-identity.js +0 -97
  164. package/dist/utils/error.d.ts +0 -8
  165. package/dist/utils/error.js +0 -12
  166. package/dist/utils/evidence-context.d.ts +0 -22
  167. package/dist/utils/evidence-context.js +0 -56
  168. package/dist/utils/export.d.ts +0 -22
  169. package/dist/utils/export.js +0 -76
  170. package/dist/utils/file-sort.d.ts +0 -20
  171. package/dist/utils/file-sort.js +0 -41
  172. package/dist/utils/format.d.ts +0 -24
  173. package/dist/utils/format.js +0 -78
  174. package/dist/utils/geoarrow.d.ts +0 -32
  175. package/dist/utils/geoarrow.js +0 -672
  176. package/dist/utils/geometry-type.d.ts +0 -52
  177. package/dist/utils/geometry-type.js +0 -76
  178. package/dist/utils/hex.d.ts +0 -10
  179. package/dist/utils/hex.js +0 -27
  180. package/dist/utils/host-detection.d.ts +0 -23
  181. package/dist/utils/host-detection.js +0 -95
  182. package/dist/utils/local-storage.d.ts +0 -16
  183. package/dist/utils/local-storage.js +0 -37
  184. package/dist/utils/markdown-sql.d.ts +0 -30
  185. package/dist/utils/markdown-sql.js +0 -72
  186. package/dist/utils/notebook.d.ts +0 -59
  187. package/dist/utils/notebook.js +0 -211
  188. package/dist/utils/parquet-metadata.d.ts +0 -64
  189. package/dist/utils/parquet-metadata.js +0 -262
  190. package/dist/utils/stac-geoparquet.d.ts +0 -90
  191. package/dist/utils/stac-geoparquet.js +0 -223
  192. package/dist/utils/stac-hydrate.d.ts +0 -38
  193. package/dist/utils/stac-hydrate.js +0 -243
  194. package/dist/utils/stac.d.ts +0 -136
  195. package/dist/utils/stac.js +0 -176
  196. package/dist/utils/storage-url.d.ts +0 -90
  197. package/dist/utils/storage-url.js +0 -568
  198. package/dist/utils/wkb.d.ts +0 -43
  199. package/dist/utils/wkb.js +0 -359
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { Archive, ChevronRight, Download, File, Folder, Loader } from '@lucide/svelte';
3
+ import { formatFileSize, handleLoadError, isAbortError } from '@walkthru-earth/objex-utils';
3
4
  import type { Entry } from '@zip.js/zip.js';
4
5
  import { onDestroy, untrack } from 'svelte';
5
6
  import { Badge } from '../ui/badge/index.js';
@@ -28,11 +29,13 @@ import {
28
29
  streamTarGzEntriesFromUrl,
29
30
  streamZipEntriesFromUrl
30
31
  } from '../../utils/archive';
31
- import { formatFileSize } from '../../utils/format';
32
- import { buildHttpsUrlAsync } from '../../utils/url.js';
32
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
33
+ import { useIsWide } from '../../utils/media-query.svelte.js';
33
34
 
34
35
  let { tab }: { tab: Tab } = $props();
35
36
 
37
+ const isWide = useIsWide();
38
+
36
39
  const MAX_ITEMS = 500;
37
40
 
38
41
  // ── State ──────────────────────────────────────────────────────────────
@@ -165,8 +168,8 @@ async function loadArchive() {
165
168
  error = t('archive.unsupported');
166
169
  }
167
170
  } catch (err) {
168
- if ((err as DOMException)?.name === 'AbortError') return;
169
- error = err instanceof Error ? err.message : String(err);
171
+ if (isAbortError(err)) return;
172
+ error = handleLoadError(err);
170
173
  } finally {
171
174
  scanning = false;
172
175
  if (initializing) initializing = false;
@@ -187,7 +190,7 @@ async function loadZip() {
187
190
  loadMethod = 'range';
188
191
  return;
189
192
  } catch (err) {
190
- if ((err as DOMException)?.name === 'AbortError') throw err;
193
+ if (isAbortError(err)) throw err;
191
194
  entryList = [];
192
195
  scanCount = 0;
193
196
  zipEntryMap.clear();
@@ -218,7 +221,7 @@ async function loadTar() {
218
221
  loadMethod = 'range';
219
222
  return;
220
223
  } catch (err) {
221
- if ((err as DOMException)?.name === 'AbortError') throw err;
224
+ if (isAbortError(err)) throw err;
222
225
  entryList = [];
223
226
  scanCount = 0;
224
227
  remoteUrl = '';
@@ -260,7 +263,7 @@ async function loadTarGz() {
260
263
  loadMethod = 'full';
261
264
  return;
262
265
  } catch (err) {
263
- if ((err as DOMException)?.name === 'AbortError') throw err;
266
+ if (isAbortError(err)) throw err;
264
267
  // Fall through to full-buffer approach
265
268
  entryList = [];
266
269
  scanCount = 0;
@@ -312,7 +315,7 @@ const totalFiles = $derived(contents.files.length);
312
315
  {#if selectedFile}
313
316
  {@const fileName = selectedFile.filename.split('/').pop()}
314
317
  <div
315
- class="shrink-0 border-b border-zinc-200 px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground dark:border-zinc-800"
318
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
316
319
  >
317
320
  {t('archive.fileDetails')}
318
321
  </div>
@@ -367,10 +370,10 @@ const totalFiles = $derived(contents.files.length);
367
370
 
368
371
  <div class="flex h-full flex-col">
369
372
  <!-- Header bar -->
370
- <div class="shrink-0 border-b border-zinc-200 px-3 py-2 sm:px-4 dark:border-zinc-800">
373
+ <div class="shrink-0 border-b border-border px-3 py-2 sm:px-4">
371
374
  <div class="flex items-center gap-1.5 sm:gap-2">
372
375
  <Archive class="h-4 w-4 shrink-0 text-amber-500" />
373
- <span class="max-w-[140px] truncate text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300">
376
+ <span class="max-w-[140px] truncate text-sm font-medium text-foreground sm:max-w-none">
374
377
  {tab.name}
375
378
  </span>
376
379
  <Badge variant="outline" class="text-[10px]">{formatLabel}</Badge>
@@ -434,60 +437,118 @@ const totalFiles = $derived(contents.files.length);
434
437
  <!-- Content area -->
435
438
  {#if initializing}
436
439
  <div class="flex flex-1 items-center justify-center gap-2">
437
- <Loader class="h-5 w-5 animate-spin text-zinc-400" />
438
- <span class="text-sm text-zinc-400">{t('archive.loading')}</span>
440
+ <Loader class="h-5 w-5 animate-spin text-muted-foreground" />
441
+ <span class="text-sm text-muted-foreground">{t('archive.loading')}</span>
439
442
  </div>
440
443
  {:else if error}
441
444
  <div class="flex flex-1 items-center justify-center px-4">
442
- <p class="text-sm text-red-400">{error}</p>
445
+ <p class="text-sm text-destructive">{error}</p>
443
446
  </div>
444
447
  {:else}
445
448
  <!-- Column browser (resizable) -->
446
- <ResizablePaneGroup direction="horizontal" class="min-h-0 flex-1">
447
- <!-- Column 1: Current path entries -->
448
- <ResizablePane defaultSize={35} minSize={20}>
449
- <div class="flex h-full flex-col">
449
+ {#snippet archiveContents()}
450
+ <div class="flex h-full flex-col">
451
+ <div
452
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
453
+ >
454
+ {t('archive.contents')}
455
+ <span class="ms-1 normal-case tracking-normal">({(totalDirs + totalFiles).toLocaleString()})</span>
456
+ </div>
457
+ <div class="flex-1 overflow-auto">
458
+ {#if contents.directories.length === 0 && contents.files.length === 0 && !scanning}
459
+ <div class="p-4 text-center text-xs text-muted-foreground">
460
+ {t('archive.empty')}
461
+ </div>
462
+ {/if}
463
+
464
+ {#each contents.directories as dir, i}
465
+ {#if i < MAX_ITEMS}
466
+ {@const dirName = dir.split('/').pop()}
467
+ <button
468
+ class="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-muted"
469
+ class:bg-muted={selectedDir === dir}
470
+ onclick={() => selectDirectory(dir)}
471
+ ondblclick={() => navigateIntoDir(dir)}
472
+ >
473
+ <Folder class="size-3.5 shrink-0 text-amber-500/70" />
474
+ <span class="truncate font-medium">{dirName}</span>
475
+ <ChevronRight class="ms-auto size-3 shrink-0 text-muted-foreground" />
476
+ </button>
477
+ {:else if i === MAX_ITEMS}
478
+ <div class="px-3 py-1.5 text-[10px] text-muted-foreground">
479
+ +{contents.directories.length - MAX_ITEMS} more
480
+ </div>
481
+ {/if}
482
+ {/each}
483
+
484
+ {#each contents.files as file, i}
485
+ {#if i < MAX_ITEMS}
486
+ {@const fileName = file.filename.split('/').pop()}
487
+ <button
488
+ class="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-muted"
489
+ class:bg-muted={selectedFile?.filename === file.filename}
490
+ onclick={() => selectFile(file)}
491
+ >
492
+ <File class="size-3.5 shrink-0 text-muted-foreground/70" />
493
+ <span class="truncate">{fileName}</span>
494
+ <span class="ms-auto shrink-0 text-[10px] tabular-nums text-muted-foreground">
495
+ {formatFileSize(file.uncompressedSize)}
496
+ </span>
497
+ </button>
498
+ {:else if i === MAX_ITEMS}
499
+ <div class="px-3 py-1.5 text-[10px] text-muted-foreground">
500
+ +{contents.files.length - MAX_ITEMS} more
501
+ </div>
502
+ {/if}
503
+ {/each}
504
+
505
+ {#if scanning}
506
+ <div class="flex items-center gap-2 px-3 py-2 text-[10px] text-muted-foreground">
507
+ <Loader class="h-3 w-3 animate-spin" />
508
+ <span>{t('archive.scanningProgress', { count: scanCount.toLocaleString() })}</span>
509
+ </div>
510
+ {/if}
511
+ </div>
512
+ </div>
513
+ {/snippet}
514
+
515
+ {#snippet archiveSelectedDir()}
516
+ <div class="flex h-full flex-col">
517
+ {#if selectedDir}
518
+ {@const dirName = selectedDir.split('/').pop()}
450
519
  <div
451
- class="shrink-0 border-b border-zinc-200 px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground dark:border-zinc-800"
520
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
452
521
  >
453
- {t('archive.contents')}
454
- <span class="ms-1 normal-case tracking-normal">({(totalDirs + totalFiles).toLocaleString()})</span>
522
+ {dirName}
523
+ <span class="ms-1 normal-case tracking-normal">({(selectedDirContents.directories.length + selectedDirContents.files.length).toLocaleString()})</span>
455
524
  </div>
456
525
  <div class="flex-1 overflow-auto">
457
- {#if contents.directories.length === 0 && contents.files.length === 0 && !scanning}
526
+ {#if selectedDirContents.directories.length === 0 && selectedDirContents.files.length === 0}
458
527
  <div class="p-4 text-center text-xs text-muted-foreground">
459
528
  {t('archive.empty')}
460
529
  </div>
461
530
  {/if}
462
531
 
463
- {#each contents.directories as dir, i}
532
+ {#each selectedDirContents.directories as subDir, i}
464
533
  {#if i < MAX_ITEMS}
465
- {@const dirName = dir.split('/').pop()}
534
+ {@const subDirName = subDir.split('/').pop()}
466
535
  <button
467
- class="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
468
- class:bg-zinc-100={selectedDir === dir}
469
- class:dark:bg-zinc-800={selectedDir === dir}
470
- onclick={() => selectDirectory(dir)}
471
- ondblclick={() => navigateIntoDir(dir)}
536
+ class="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-muted"
537
+ onclick={() => navigateIntoDir(subDir)}
472
538
  >
473
539
  <Folder class="size-3.5 shrink-0 text-amber-500/70" />
474
- <span class="truncate font-medium">{dirName}</span>
540
+ <span class="truncate font-medium">{subDirName}</span>
475
541
  <ChevronRight class="ms-auto size-3 shrink-0 text-muted-foreground" />
476
542
  </button>
477
- {:else if i === MAX_ITEMS}
478
- <div class="px-3 py-1.5 text-[10px] text-muted-foreground">
479
- +{contents.directories.length - MAX_ITEMS} more
480
- </div>
481
543
  {/if}
482
544
  {/each}
483
545
 
484
- {#each contents.files as file, i}
546
+ {#each selectedDirContents.files as file, i}
485
547
  {#if i < MAX_ITEMS}
486
548
  {@const fileName = file.filename.split('/').pop()}
487
549
  <button
488
- class="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
489
- class:bg-zinc-100={selectedFile?.filename === file.filename}
490
- class:dark:bg-zinc-800={selectedFile?.filename === file.filename}
550
+ class="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-muted"
551
+ class:bg-muted={selectedFile?.filename === file.filename}
491
552
  onclick={() => selectFile(file)}
492
553
  >
493
554
  <File class="size-3.5 shrink-0 text-muted-foreground/70" />
@@ -496,91 +557,57 @@ const totalFiles = $derived(contents.files.length);
496
557
  {formatFileSize(file.uncompressedSize)}
497
558
  </span>
498
559
  </button>
499
- {:else if i === MAX_ITEMS}
500
- <div class="px-3 py-1.5 text-[10px] text-muted-foreground">
501
- +{contents.files.length - MAX_ITEMS} more
502
- </div>
503
560
  {/if}
504
561
  {/each}
505
-
506
- {#if scanning}
507
- <div class="flex items-center gap-2 px-3 py-2 text-[10px] text-muted-foreground">
508
- <Loader class="h-3 w-3 animate-spin" />
509
- <span>{t('archive.scanningProgress', { count: scanCount.toLocaleString() })}</span>
510
- </div>
511
- {/if}
512
562
  </div>
513
- </div>
514
- </ResizablePane>
563
+ {:else}
564
+ <div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
565
+ {t('archive.selectFolder')}
566
+ </div>
567
+ {/if}
568
+ </div>
569
+ {/snippet}
515
570
 
516
- <ResizableHandle />
571
+ {#if isWide.value}
572
+ <ResizablePaneGroup direction="horizontal" class="min-h-0 flex-1">
573
+ <!-- Column 1: Current path entries -->
574
+ <ResizablePane defaultSize={35} minSize={20}>
575
+ {@render archiveContents()}
576
+ </ResizablePane>
517
577
 
518
- <!-- Column 2: Selected directory contents -->
519
- <ResizablePane defaultSize={35} minSize={20}>
520
- <div class="flex h-full flex-col">
521
- {#if selectedDir}
522
- {@const dirName = selectedDir.split('/').pop()}
523
- <div
524
- class="shrink-0 border-b border-zinc-200 px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground dark:border-zinc-800"
525
- >
526
- {dirName}
527
- <span class="ms-1 normal-case tracking-normal">({(selectedDirContents.directories.length + selectedDirContents.files.length).toLocaleString()})</span>
528
- </div>
529
- <div class="flex-1 overflow-auto">
530
- {#if selectedDirContents.directories.length === 0 && selectedDirContents.files.length === 0}
531
- <div class="p-4 text-center text-xs text-muted-foreground">
532
- {t('archive.empty')}
533
- </div>
534
- {/if}
578
+ <ResizableHandle />
535
579
 
536
- {#each selectedDirContents.directories as subDir, i}
537
- {#if i < MAX_ITEMS}
538
- {@const subDirName = subDir.split('/').pop()}
539
- <button
540
- class="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
541
- onclick={() => navigateIntoDir(subDir)}
542
- >
543
- <Folder class="size-3.5 shrink-0 text-amber-500/70" />
544
- <span class="truncate font-medium">{subDirName}</span>
545
- <ChevronRight class="ms-auto size-3 shrink-0 text-muted-foreground" />
546
- </button>
547
- {/if}
548
- {/each}
549
-
550
- {#each selectedDirContents.files as file, i}
551
- {#if i < MAX_ITEMS}
552
- {@const fileName = file.filename.split('/').pop()}
553
- <button
554
- class="flex w-full items-center gap-2 px-3 py-1.5 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
555
- class:bg-zinc-100={selectedFile?.filename === file.filename}
556
- class:dark:bg-zinc-800={selectedFile?.filename === file.filename}
557
- onclick={() => selectFile(file)}
558
- >
559
- <File class="size-3.5 shrink-0 text-muted-foreground/70" />
560
- <span class="truncate">{fileName}</span>
561
- <span class="ms-auto shrink-0 text-[10px] tabular-nums text-muted-foreground">
562
- {formatFileSize(file.uncompressedSize)}
563
- </span>
564
- </button>
565
- {/if}
566
- {/each}
567
- </div>
568
- {:else}
569
- <div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
570
- {t('archive.selectFolder')}
571
- </div>
572
- {/if}
573
- </div>
574
- </ResizablePane>
580
+ <!-- Column 2: Selected directory contents -->
581
+ <ResizablePane defaultSize={35} minSize={20}>
582
+ {@render archiveSelectedDir()}
583
+ </ResizablePane>
575
584
 
576
- <ResizableHandle />
585
+ <ResizableHandle />
577
586
 
578
- <!-- Column 3: File details -->
579
- <ResizablePane defaultSize={30} minSize={15}>
580
- <div class="flex h-full flex-col">
587
+ <!-- Column 3: File details -->
588
+ <ResizablePane defaultSize={30} minSize={15}>
589
+ <div class="flex h-full flex-col">
590
+ {@render fileDetails()}
591
+ </div>
592
+ </ResizablePane>
593
+ </ResizablePaneGroup>
594
+ {:else}
595
+ <div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
596
+ <!-- Contents: fixed height list -->
597
+ <div class="max-h-52 shrink-0 border-b border-border">
598
+ {@render archiveContents()}
599
+ </div>
600
+ <!-- Selected dir: fixed height list (hidden when empty) -->
601
+ {#if selectedDir}
602
+ <div class="max-h-52 shrink-0 border-b border-border">
603
+ {@render archiveSelectedDir()}
604
+ </div>
605
+ {/if}
606
+ <!-- File details: grows to fill remaining space -->
607
+ <div class="flex flex-1 flex-col">
581
608
  {@render fileDetails()}
582
609
  </div>
583
- </ResizablePane>
584
- </ResizablePaneGroup>
610
+ </div>
611
+ {/if}
585
612
  {/if}
586
613
  </div>
@@ -1,5 +1,12 @@
1
1
  <script lang="ts">
2
2
  import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
3
+ import {
4
+ copyToClipboard,
5
+ handleLoadError,
6
+ isStacCatalog,
7
+ isStacCollection,
8
+ isStacItem
9
+ } from '@walkthru-earth/objex-utils';
3
10
  import { onDestroy } from 'svelte';
4
11
  import { Badge } from '../ui/badge/index.js';
5
12
  import { Button } from '../ui/button/index.js';
@@ -8,13 +15,13 @@ import { t } from '../../i18n/index.svelte.js';
8
15
  import { getAdapter } from '../../storage/index.js';
9
16
  import { tabResources } from '../../stores/tab-resources.svelte.js';
10
17
  import type { Tab } from '../../types';
11
- import { copyToClipboard } from '../../utils/clipboard.js';
12
- import { handleLoadError } from '../../utils/error.js';
13
18
  import { extensionToShikiLang, highlightCode } from '../../utils/shiki';
14
- import { buildHttpsUrl, buildHttpsUrlAsync, canStreamDirectly } from '../../utils/url.js';
15
- import { getUrlView, updateUrlView } from '../../utils/url-state.js';
19
+ import { buildHttpsUrl, canStreamDirectly } from '../../utils/signed-url.js';
20
+ import { resolveSignedTabUrl } from '../../utils/signed-url-effect.js';
21
+ import { getUrlView, pickViewMode, updateUrlView } from '../../utils/url-state.js';
16
22
  import { openZarrTab } from '../../utils/zarr-tab.js';
17
- import { isStacCatalog, isStacCollection, isStacItem } from '../../utils/stac.js';
23
+ import ViewerHeader from './ViewerHeader.svelte';
24
+ import ViewerStatus from './ViewerStatus.svelte';
18
25
 
19
26
  interface CodeActions {
20
27
  toggleFormat: () => Promise<void>;
@@ -44,20 +51,20 @@ let error = $state<string | null>(null);
44
51
  let copied = $state(false);
45
52
  let formatted = $state(false);
46
53
  const urlView = getUrlView();
47
- function getInitialViewMode():
48
- | 'code'
49
- | 'render'
50
- | 'stac-browser'
51
- | 'kepler'
52
- | 'maputnik'
53
- | 'marimo' {
54
- if (urlView === 'stac-browser') return 'stac-browser';
55
- if (urlView === 'kepler') return 'kepler';
56
- if (urlView === 'maputnik') return 'maputnik';
57
- if (urlView === 'marimo') return 'marimo';
58
- if (urlView === 'code') return 'code';
59
- if (tab.extension.toLowerCase() === 'html') return 'render';
60
- return 'code';
54
+ type CodeViewMode = 'code' | 'render' | 'stac-browser' | 'kepler' | 'maputnik' | 'marimo';
55
+ const CODE_VIEW_MODES = [
56
+ 'code',
57
+ 'render',
58
+ 'stac-browser',
59
+ 'kepler',
60
+ 'maputnik',
61
+ 'marimo'
62
+ ] as const satisfies readonly CodeViewMode[];
63
+ function getInitialViewMode(): CodeViewMode {
64
+ const explicit = pickViewMode<CodeViewMode>(CODE_VIEW_MODES, 'code');
65
+ if (explicit !== 'code' || urlView === 'code') return explicit;
66
+ // No (or unknown) hash: default to render for HTML, code otherwise.
67
+ return tab.extension.toLowerCase() === 'html' ? 'render' : 'code';
61
68
  }
62
69
  let viewMode = $state(getInitialViewMode());
63
70
 
@@ -119,17 +126,10 @@ const stacBadgeKey = $derived<Record<string, string>>({
119
126
  // must wait for the presign so the iframe never loads a bare `s3://` href.
120
127
  let styleUrl = $state('');
121
128
  $effect(() => {
122
- const id = tab.id;
123
129
  styleUrl = canStreamDirectly(tab) ? buildHttpsUrl(tab) : '';
124
- let cancelled = false;
125
- (async () => {
126
- const url = await buildHttpsUrlAsync(tab);
127
- if (cancelled || id !== tab.id) return;
128
- styleUrl = url;
129
- })();
130
- return () => {
131
- cancelled = true;
132
- };
130
+ return resolveSignedTabUrl(tab, (u) => {
131
+ styleUrl = u;
132
+ });
133
133
  });
134
134
  const stacBrowserSrc = $derived(
135
135
  `https://radiantearth.github.io/stac-browser/#/external/${styleUrl}`
@@ -207,12 +207,17 @@ $effect(() => {
207
207
  };
208
208
  });
209
209
 
210
- // Auto-switch to STAC Browser when STAC JSON is detected (unless URL explicitly set #code).
210
+ // Auto-switch to STAC Browser when STAC JSON is detected and the user did NOT
211
+ // request a specific view via the URL hash. Any explicit hash (#map, #stac-map,
212
+ // #stac-browser, #code, …) MUST be honored, because while ViewerRouter's async
213
+ // detectStac is pending it falls back to plain CodeViewer for .json tabs;
214
+ // rewriting the hash here would race the eventual StacTabViewer mount and
215
+ // clobber the shared link the user opened.
211
216
  // Skipped when nested in StacTabViewer since the outer wrapper owns the view toggle.
212
217
  let stacAutoSwitched = false;
213
218
  $effect(() => {
214
219
  if (nested) return;
215
- if (isStacJson && !stacAutoSwitched && viewMode === 'code' && urlView !== 'code') {
220
+ if (isStacJson && !stacAutoSwitched && viewMode === 'code' && !urlView) {
216
221
  stacAutoSwitched = true;
217
222
  viewMode = 'stac-browser';
218
223
  updateUrlView('stac-browser');
@@ -349,7 +354,7 @@ async function toggleFormat() {
349
354
  formatted = true;
350
355
  }
351
356
 
352
- function setViewMode(mode: 'code' | 'render' | 'stac-browser' | 'kepler' | 'maputnik' | 'marimo') {
357
+ function setViewMode(mode: CodeViewMode) {
353
358
  viewMode = viewMode === mode ? (isHtml ? 'render' : 'code') : mode;
354
359
  updateUrlView(viewMode === 'render' ? '' : viewMode);
355
360
  }
@@ -361,13 +366,9 @@ async function copyCode() {
361
366
 
362
367
  <div class="flex h-full flex-col">
363
368
  {#if !nested}
364
- <div
365
- class="flex items-center gap-1 border-b border-zinc-200 px-2 py-1.5 sm:gap-2 sm:px-4 dark:border-zinc-800"
366
- >
367
- <span class="truncate max-w-[120px] text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300">{tab.name}</span>
368
- <Badge variant="secondary">{language}</Badge>
369
-
370
- <div class="ms-auto flex items-center gap-1 sm:gap-2">
369
+ <ViewerHeader {tab}>
370
+ {#snippet badge()}<Badge variant="secondary">{language}</Badge>{/snippet}
371
+ {#snippet actions()}
371
372
  {#if jsonKind === 'maplibre-style'}
372
373
  <Badge variant="outline" class="hidden border-blue-200 text-blue-600 sm:inline-flex dark:border-blue-800 dark:text-blue-300">
373
374
  {t('code.maplibreStyle')}
@@ -473,7 +474,7 @@ async function copyCode() {
473
474
  <!-- Mobile overflow menu -->
474
475
  <div class="flex sm:hidden">
475
476
  <DropdownMenu.Root>
476
- <DropdownMenu.Trigger class="rounded p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
477
+ <DropdownMenu.Trigger class="rounded p-1 text-muted-foreground hover:bg-muted">
477
478
  <EllipsisVerticalIcon class="size-4" />
478
479
  </DropdownMenu.Trigger>
479
480
  <DropdownMenu.Content align="end" class="w-44">
@@ -530,8 +531,8 @@ async function copyCode() {
530
531
  </DropdownMenu.Content>
531
532
  </DropdownMenu.Root>
532
533
  </div>
533
- </div>
534
- </div>
534
+ {/snippet}
535
+ </ViewerHeader>
535
536
  {/if}
536
537
 
537
538
  {#if viewMode === 'stac-browser' && styleUrl}
@@ -587,13 +588,9 @@ async function copyCode() {
587
588
  class:word-wrap={wordWrap}
588
589
  >
589
590
  {#if loading}
590
- <div class="flex h-full items-center justify-center">
591
- <p class="text-sm text-zinc-400">{t('code.loading')}</p>
592
- </div>
591
+ <ViewerStatus kind="loading" message={t('code.loading')} />
593
592
  {:else if error}
594
- <div class="flex h-full items-center justify-center">
595
- <p class="text-sm text-red-400">{error}</p>
596
- </div>
593
+ <ViewerStatus kind="error" message={error} />
597
594
  {:else}
598
595
  {@html html}
599
596
  {/if}
@@ -12,6 +12,6 @@ type $$ComponentProps = {
12
12
  wordWrap?: boolean;
13
13
  actions?: CodeActions | null;
14
14
  };
15
- declare const CodeViewer: import("svelte").Component<$$ComponentProps, {}, "wordWrap" | "actions">;
15
+ declare const CodeViewer: import("svelte").Component<$$ComponentProps, {}, "actions" | "wordWrap">;
16
16
  type CodeViewer = ReturnType<typeof CodeViewer>;
17
17
  export default CodeViewer;