@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,26 +1,19 @@
1
1
  <script lang="ts">
2
2
  import type { Tab } from '../../types';
3
- import { buildHttpsUrlAsync } from '../../utils/url.js';
3
+ import { resolveSignedTabUrl } from '../../utils/signed-url-effect.js';
4
4
 
5
5
  let { tab }: { tab: Tab } = $props();
6
6
 
7
7
  let fileUrl = $state('');
8
8
 
9
9
  $effect(() => {
10
- const id = tab.id;
11
- let cancelled = false;
12
- (async () => {
13
- if (tab.source === 'url') {
14
- fileUrl = tab.path;
15
- return;
16
- }
17
- const url = await buildHttpsUrlAsync(tab);
18
- if (cancelled || id !== tab.id) return;
19
- fileUrl = url;
20
- })();
21
- return () => {
22
- cancelled = true;
23
- };
10
+ if (tab.source === 'url') {
11
+ fileUrl = tab.path;
12
+ return;
13
+ }
14
+ return resolveSignedTabUrl(tab, (u) => {
15
+ fileUrl = u;
16
+ });
24
17
  });
25
18
 
26
19
  const viewerUrl = $derived(
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import ClockIcon from '@lucide/svelte/icons/clock';
3
+ import { handleLoadError } from '@walkthru-earth/objex-utils';
3
4
  import { onDestroy } from 'svelte';
4
5
  import SqlEditor from '../editor/SqlEditor.svelte';
5
6
  import { Badge } from '../ui/badge/index.js';
@@ -127,7 +128,7 @@ async function loadDatabase() {
127
128
  tables = (result.rows ?? []).map((row) => row.name).filter((name): name is string => !!name);
128
129
  }
129
130
  } catch (err) {
130
- error = err instanceof Error ? err.message : String(err);
131
+ error = handleLoadError(err);
131
132
  } finally {
132
133
  loading = false;
133
134
  }
@@ -254,7 +255,7 @@ async function switchSnapshot(id: number) {
254
255
  const connId = tab.connectionId ?? '';
255
256
  await loadDuckLake(engine, connId, id);
256
257
  } catch (err) {
257
- error = err instanceof Error ? err.message : String(err);
258
+ error = handleLoadError(err);
258
259
  } finally {
259
260
  switchingSnapshot = false;
260
261
  }
@@ -318,7 +319,7 @@ async function switchSchema(schema: string) {
318
319
  const connId = tab.connectionId ?? '';
319
320
  await loadDuckLakeTables(engine, connId);
320
321
  } catch (err) {
321
- error = err instanceof Error ? err.message : String(err);
322
+ error = handleLoadError(err);
322
323
  }
323
324
  }
324
325
 
@@ -357,23 +358,23 @@ function selectTable(tableName: string) {
357
358
 
358
359
  <div class="flex h-full flex-col">
359
360
  <div
360
- 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"
361
+ class="flex items-center gap-1 border-b border-border px-2 py-1.5 sm:gap-2 sm:px-4"
361
362
  >
362
- <span class="truncate max-w-[120px] text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300">{tab.name}</span>
363
+ <span class="truncate max-w-[120px] text-sm font-medium text-foreground sm:max-w-none">{tab.name}</span>
363
364
  {#if isDuckLake}
364
365
  <Badge variant="secondary" class="bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200">DuckLake</Badge>
365
366
  {:else}
366
367
  <Badge variant="secondary">{t('database.badge')}</Badge>
367
368
  {/if}
368
369
  {#if tables.length > 0}
369
- <span class="hidden text-xs text-zinc-400 sm:inline">{tables.length} {t('database.tables')}</span>
370
+ <span class="hidden text-xs text-muted-foreground sm:inline">{tables.length} {t('database.tables')}</span>
370
371
  {/if}
371
372
  {#if isDuckLake && snapshotVersion !== null}
372
- <div class="hidden items-center gap-1 text-xs text-zinc-400 sm:inline-flex">
373
+ <div class="hidden items-center gap-1 text-xs text-muted-foreground sm:inline-flex">
373
374
  <ClockIcon class="h-3 w-3" />
374
375
  {#if snapshots.length > 1}
375
376
  <select
376
- class="rounded bg-white px-1.5 py-0.5 text-xs text-zinc-700 disabled:opacity-60 dark:bg-zinc-800 dark:text-zinc-300"
377
+ class="rounded bg-background px-1.5 py-0.5 text-xs text-foreground disabled:opacity-60"
377
378
  disabled={switchingSnapshot}
378
379
  title={t('ducklake.snapshot')}
379
380
  value={snapshotVersion}
@@ -383,7 +384,7 @@ function selectTable(tableName: string) {
383
384
  <option value={snap.id}>{formatSnapshotLabel(snap)}</option>
384
385
  {/each}
385
386
  </select>
386
- <span class="text-zinc-400">({snapshots.length} {t('ducklake.snapshots')})</span>
387
+ <span class="text-muted-foreground">({snapshots.length} {t('ducklake.snapshots')})</span>
387
388
  {:else}
388
389
  {@const formatted = formatSnapshotTime(snapshotTimeMs)}
389
390
  <span>v{snapshotVersion}{#if formatted}&nbsp;({formatted}){/if}</span>
@@ -406,26 +407,26 @@ function selectTable(tableName: string) {
406
407
  <div class="flex flex-1 overflow-hidden">
407
408
  {#if loading}
408
409
  <div class="flex flex-1 items-center justify-center">
409
- <p class="text-sm text-zinc-400">{isDuckLake ? t('ducklake.loading') : t('database.loading')}</p>
410
+ <p class="text-sm text-muted-foreground">{isDuckLake ? t('ducklake.loading') : t('database.loading')}</p>
410
411
  </div>
411
412
  {:else if error}
412
413
  <div class="flex flex-1 items-center justify-center p-4">
413
414
  <div class="max-w-md text-center">
414
- <p class="text-sm text-red-400">{error}</p>
415
+ <p class="text-sm text-destructive">{error}</p>
415
416
  {#if isDuckLake && error.includes('ducklake')}
416
- <p class="mt-2 text-xs text-zinc-500">{t('ducklake.extensionHint')}</p>
417
+ <p class="mt-2 text-xs text-muted-foreground">{t('ducklake.extensionHint')}</p>
417
418
  {/if}
418
419
  </div>
419
420
  </div>
420
421
  {:else}
421
422
  <!-- Table list sidebar -->
422
423
  <div
423
- class="w-56 shrink-0 overflow-auto border-e border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900"
424
+ class="w-56 shrink-0 overflow-auto border-e border-border bg-muted"
424
425
  >
425
426
  {#if isDuckLake && schemas.length > 1}
426
- <div class="border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
427
+ <div class="border-b border-border px-3 py-2">
427
428
  <select
428
- class="w-full rounded bg-white px-2 py-1 text-xs text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"
429
+ class="w-full rounded bg-background px-2 py-1 text-xs text-foreground"
429
430
  onchange={(e) => switchSchema(e.currentTarget.value)}
430
431
  >
431
432
  {#each schemas as schema}
@@ -434,21 +435,21 @@ function selectTable(tableName: string) {
434
435
  </select>
435
436
  </div>
436
437
  {/if}
437
- <div class="border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
438
- <h3 class="text-xs font-medium text-zinc-500 dark:text-zinc-400">{t('database.tablesHeader')}</h3>
438
+ <div class="border-b border-border px-3 py-2">
439
+ <h3 class="text-xs font-medium text-muted-foreground">{t('database.tablesHeader')}</h3>
439
440
  </div>
440
441
  {#each tables as tableName}
441
442
  <button
442
- class="flex w-full items-center px-3 py-1.5 text-start text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800"
443
+ class="flex w-full items-center px-3 py-1.5 text-start text-xs hover:bg-accent"
443
444
  class:bg-blue-50={selectedTable === tableName}
444
445
  class:dark:bg-blue-950={selectedTable === tableName}
445
446
  onclick={() => selectTable(tableName)}
446
447
  >
447
- <span class="text-zinc-700 dark:text-zinc-300">{tableName}</span>
448
+ <span class="text-foreground">{tableName}</span>
448
449
  </button>
449
450
  {/each}
450
451
  {#if tables.length === 0}
451
- <div class="px-3 py-4 text-center text-xs text-zinc-400">
452
+ <div class="px-3 py-4 text-center text-xs text-muted-foreground">
452
453
  {isDuckLake ? t('ducklake.noTables') : t('database.selectTable')}
453
454
  </div>
454
455
  {/if}
@@ -468,7 +469,7 @@ function selectTable(tableName: string) {
468
469
  {/key}
469
470
  {:else}
470
471
  <div class="flex flex-1 items-center justify-center">
471
- <p class="text-sm text-zinc-400">{t('database.selectTable')}</p>
472
+ <p class="text-sm text-muted-foreground">{t('database.selectTable')}</p>
472
473
  </div>
473
474
  {/if}
474
475
  </div>
@@ -32,7 +32,7 @@ const geoEntries = $derived(entries.filter((e) => geoLabels.has(e.label)));
32
32
  <!-- File Metadata -->
33
33
  {#if fileEntries.length > 0}
34
34
  <section class="mx-auto max-w-2xl">
35
- <h3 class="text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">
35
+ <h3 class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
36
36
  {t('fileInfo.fileMetadata')}
37
37
  </h3>
38
38
  <div
@@ -42,17 +42,17 @@ const geoEntries = $derived(entries.filter((e) => geoLabels.has(e.label)));
42
42
  {#each fileEntries as entry}
43
43
  <div class="flex items-start gap-3 px-3.5 py-2">
44
44
  <span
45
- class="w-[5.5rem] shrink-0 text-xs text-zinc-400 dark:text-zinc-500"
45
+ class="w-[5.5rem] shrink-0 text-xs text-muted-foreground"
46
46
  >
47
47
  {entry.label}
48
48
  </span>
49
49
  <div class="min-w-0 flex-1">
50
- <span class="text-xs font-medium text-zinc-700 dark:text-zinc-300">
50
+ <span class="text-xs font-medium text-foreground">
51
51
  {entry.value}
52
52
  </span>
53
53
  {#if entry.detail}
54
54
  <p
55
- class="mt-0.5 truncate font-mono text-[10px] leading-tight text-zinc-400 dark:text-zinc-500"
55
+ class="mt-0.5 truncate font-mono text-[10px] leading-tight text-muted-foreground"
56
56
  >
57
57
  {entry.detail}
58
58
  </p>
@@ -68,7 +68,7 @@ const geoEntries = $derived(entries.filter((e) => geoLabels.has(e.label)));
68
68
  <!-- Geometry -->
69
69
  {#if geoEntries.length > 0}
70
70
  <section class="mx-auto max-w-2xl">
71
- <h3 class="text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">
71
+ <h3 class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
72
72
  {t('fileInfo.geometry')}
73
73
  </h3>
74
74
  <div
@@ -78,12 +78,12 @@ const geoEntries = $derived(entries.filter((e) => geoLabels.has(e.label)));
78
78
  {#each geoEntries as entry}
79
79
  <div class="flex items-start gap-3 px-3.5 py-2">
80
80
  <span
81
- class="w-[5.5rem] shrink-0 text-xs text-zinc-400 dark:text-zinc-500"
81
+ class="w-[5.5rem] shrink-0 text-xs text-muted-foreground"
82
82
  >
83
83
  {entry.label}
84
84
  </span>
85
85
  <div class="min-w-0 flex-1">
86
- <span class="text-xs font-medium text-zinc-700 dark:text-zinc-300">
86
+ <span class="text-xs font-medium text-foreground">
87
87
  {entry.value}
88
88
  </span>
89
89
  </div>
@@ -97,7 +97,7 @@ const geoEntries = $derived(entries.filter((e) => geoLabels.has(e.label)));
97
97
  <!-- Schema -->
98
98
  {#if schema.length > 0}
99
99
  <section class="mx-auto max-w-2xl">
100
- <h3 class="text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">
100
+ <h3 class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
101
101
  {t('fileInfo.schema')} ({schema.length})
102
102
  </h3>
103
103
  <div
@@ -107,17 +107,17 @@ const geoEntries = $derived(entries.filter((e) => geoLabels.has(e.label)));
107
107
  <thead>
108
108
  <tr class="bg-zinc-100/80 dark:bg-zinc-800/50">
109
109
  <th
110
- class="w-10 px-3 py-1.5 text-left font-medium text-zinc-400 dark:text-zinc-500"
110
+ class="w-10 px-3 py-1.5 text-left font-medium text-muted-foreground"
111
111
  >
112
112
  #
113
113
  </th>
114
114
  <th
115
- class="px-3 py-1.5 text-left font-medium text-zinc-500 dark:text-zinc-400"
115
+ class="px-3 py-1.5 text-left font-medium text-muted-foreground"
116
116
  >
117
117
  {t('fileInfo.column')}
118
118
  </th>
119
119
  <th
120
- class="px-3 py-1.5 text-left font-medium text-zinc-500 dark:text-zinc-400"
120
+ class="px-3 py-1.5 text-left font-medium text-muted-foreground"
121
121
  >
122
122
  {t('fileInfo.type')}
123
123
  </th>
@@ -128,13 +128,13 @@ const geoEntries = $derived(entries.filter((e) => geoLabels.has(e.label)));
128
128
  <tr
129
129
  class="border-t border-zinc-100/80 dark:border-zinc-800/50 {i % 2 === 0 ? 'bg-zinc-50/50 dark:bg-zinc-900/20' : ''}"
130
130
  >
131
- <td class="px-3 py-1.5 tabular-nums text-zinc-400 dark:text-zinc-600">
131
+ <td class="px-3 py-1.5 tabular-nums text-muted-foreground">
132
132
  {i + 1}
133
133
  </td>
134
- <td class="px-3 py-1.5 font-mono text-zinc-700 dark:text-zinc-300">
134
+ <td class="px-3 py-1.5 font-mono text-foreground">
135
135
  {field.name}
136
136
  </td>
137
- <td class="px-3 py-1.5 font-mono text-zinc-500 dark:text-zinc-400">
137
+ <td class="px-3 py-1.5 font-mono text-muted-foreground">
138
138
  {field.type}
139
139
  </td>
140
140
  </tr>
@@ -148,7 +148,7 @@ const geoEntries = $derived(entries.filter((e) => geoLabels.has(e.label)));
148
148
  <!-- Parquet Table Explorer -->
149
149
  {#if parquetTableSrc}
150
150
  <section>
151
- <h3 class="text-[11px] font-semibold uppercase tracking-wider text-zinc-400 dark:text-zinc-500">
151
+ <h3 class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
152
152
  {t('fileInfo.parquetExplorer')}
153
153
  </h3>
154
154
  <div
@@ -167,7 +167,7 @@ const geoEntries = $derived(entries.filter((e) => geoLabels.has(e.label)));
167
167
 
168
168
  {#if entries.length === 0 && schema.length === 0}
169
169
  <div class="flex flex-1 items-center justify-center py-16">
170
- <p class="text-sm text-zinc-400">{t('fileInfo.noMetadata')}</p>
170
+ <p class="text-sm text-muted-foreground">{t('fileInfo.noMetadata')}</p>
171
171
  </div>
172
172
  {/if}
173
173
  </div>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import LocateIcon from '@lucide/svelte/icons/locate';
3
+ import { FIRST_FEATURE_FLY_ZOOM, handleLoadError, isAbortError } from '@walkthru-earth/objex-utils';
3
4
  import { geojson as fgbGeojson } from 'flatgeobuf';
4
5
  import { magicbytes } from 'flatgeobuf/lib/mjs/constants.js';
5
6
  import { buildHeader as fgbBuildHeader } from 'flatgeobuf/lib/mjs/generic/featurecollection.js';
@@ -16,10 +17,11 @@ import {
16
17
  buildSelectionLayer,
17
18
  geojsonFillColor,
18
19
  geojsonLineColor,
20
+ HIGHLIGHT_COLOR,
19
21
  hoverCursor,
20
22
  loadDeckModules
21
23
  } from '../../utils/deck.js';
22
- import { buildHttpsUrlAsync } from '../../utils/url.js';
24
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
23
25
  import AttributeTable from './map/AttributeTable.svelte';
24
26
  import MapContainer from './map/MapContainer.svelte';
25
27
 
@@ -71,6 +73,10 @@ let headerInfo = $state<{
71
73
  hasIndex: boolean;
72
74
  } | null>(null);
73
75
 
76
+ // NOTE: intentionally broader than the canonical WGS84_CODES in constants.ts.
77
+ // 4267 (NAD27) / 4269 (NAD83) are treated as lon/lat here because FGB files
78
+ // commonly ship NAD-coded data that needs no reprojection for display.
79
+ // Do not narrow to [4326, 4979] without a NAD-coded FGB test file.
74
80
  const WGS84_CODES = new Set([4326, 4979, 4267, 4269]);
75
81
  const CRS84_NAMES = ['CRS84', 'CRS 84', 'OGC:CRS84'];
76
82
 
@@ -190,7 +196,7 @@ function extractFirstCoord(coords: any): [number, number] | null {
190
196
 
191
197
  function flyToFirstFeature() {
192
198
  if (!mapRef || !firstFeatureCoord) return;
193
- mapRef.flyTo({ center: firstFeatureCoord, zoom: 14 });
199
+ mapRef.flyTo({ center: firstFeatureCoord, zoom: FIRST_FEATURE_FLY_ZOOM });
194
200
  }
195
201
 
196
202
  function cleanup() {
@@ -238,7 +244,6 @@ $effect(() => {
238
244
  onDestroy(cleanup);
239
245
 
240
246
  async function loadFlatGeobuf() {
241
- console.log('[FGB]', 'loadFlatGeobuf() start');
242
247
  cleanup();
243
248
 
244
249
  loading = true;
@@ -280,13 +285,11 @@ async function loadFlatGeobuf() {
280
285
  storedHeader.crs.org && code
281
286
  ? `${storedHeader.crs.org}:${code}`
282
287
  : (storedHeader.crs.name ?? 'unknown');
283
- console.log('[FGB]', 'Projected CRS detected:', crsLabel, '→ fetching proj4 definition');
284
288
 
285
289
  try {
286
290
  const proj4Def = await fetchProj4Def(code);
287
291
  const converter = proj4(proj4Def, 'EPSG:4326') as any;
288
292
  proj4Forward = (coord) => converter.forward(coord);
289
- console.log('[FGB]', 'Reprojection ready:', crsLabel, '→ WGS84');
290
293
 
291
294
  // Reproject envelope bounds so fitBounds works
292
295
  if (storedHeader.envelope && storedHeader.envelope.length >= 4) {
@@ -298,7 +301,7 @@ async function loadFlatGeobuf() {
298
301
  }
299
302
  } catch (err) {
300
303
  console.error('[FGB]', 'Failed to set up reprojection:', err);
301
- error = `Cannot reproject CRS ${crsLabel} → WGS84: ${err instanceof Error ? err.message : err}`;
304
+ error = `Cannot reproject CRS ${crsLabel} → WGS84: ${handleLoadError(err) ?? String(err)}`;
302
305
  streaming = false;
303
306
  return;
304
307
  }
@@ -308,11 +311,10 @@ async function loadFlatGeobuf() {
308
311
  await streamFeatures(url, settings.featureLimit);
309
312
  } catch (err) {
310
313
  console.error('[FGB]', 'loadFlatGeobuf error:', err);
311
- if (err instanceof DOMException && err.name === 'AbortError') return;
312
- error = err instanceof Error ? err.message : String(err);
314
+ if (isAbortError(err)) return;
315
+ error = handleLoadError(err);
313
316
  loading = false;
314
317
  } finally {
315
- console.log('[FGB]', 'done, features:', features.length);
316
318
  streaming = false;
317
319
  }
318
320
  }
@@ -331,25 +333,10 @@ async function readHeaderWithRangeRequests(url: string): Promise<boolean> {
331
333
  }
332
334
 
333
335
  const header = reader.header;
334
- console.log('[FGB]', 'header:', {
335
- geometryType: header.geometryType,
336
- featuresCount: header.featuresCount,
337
- indexNodeSize: header.indexNodeSize,
338
- envelope: header.envelope ? Array.from(header.envelope) : null,
339
- columns: header.columns?.length
340
- });
341
336
  populateHeaderInfo(header);
342
337
 
343
338
  storedHeader = header;
344
339
  storedFeatureOffset = reader.lengthBeforeFeatures();
345
- console.log(
346
- '[FGB]',
347
- 'featureOffset:',
348
- storedFeatureOffset,
349
- '(index ~',
350
- ((storedFeatureOffset - 12) / 1024 / 1024).toFixed(1),
351
- 'MB skipped)'
352
- );
353
340
  return true;
354
341
  }
355
342
 
@@ -367,10 +354,9 @@ async function loadAllFeatures() {
367
354
  await streamFeatures(url);
368
355
  } catch (err) {
369
356
  console.error('[FGB]', 'loadAllFeatures error:', err);
370
- if (err instanceof DOMException && err.name === 'AbortError') return;
371
- error = err instanceof Error ? err.message : String(err);
357
+ if (isAbortError(err)) return;
358
+ error = handleLoadError(err);
372
359
  } finally {
373
- console.log('[FGB]', 'loadAllFeatures done, features:', features.length);
374
360
  streaming = false;
375
361
  }
376
362
  }
@@ -382,7 +368,6 @@ async function loadAllFeatures() {
382
368
  async function streamFeatures(url: string, limit?: number) {
383
369
  const ac = new AbortController();
384
370
  abortController = ac;
385
- const t0 = performance.now();
386
371
 
387
372
  let iter: AsyncGenerator;
388
373
  activeStreamCancel = null;
@@ -398,12 +383,6 @@ async function streamFeatures(url: string, limit?: number) {
398
383
  headers: { Range: `bytes=${storedFeatureOffset}-` },
399
384
  signal: ac.signal
400
385
  });
401
- console.log(
402
- '[FGB]',
403
- 'Range fetch:',
404
- featureResp.status,
405
- featureResp.headers.get('content-range')
406
- );
407
386
  if (!featureResp.ok && featureResp.status !== 206)
408
387
  throw new Error(`HTTP ${featureResp.status}: ${featureResp.statusText}`);
409
388
  if (!featureResp.body) throw new Error('No response body');
@@ -475,7 +454,6 @@ async function streamFeatures(url: string, limit?: number) {
475
454
  activeStreamCancel?.();
476
455
  activeStreamCancel = null;
477
456
  ac.abort();
478
- console.log('[FGB]', 'force-closed connection after', features.length, 'features');
479
457
  }
480
458
  }
481
459
 
@@ -487,14 +465,6 @@ async function streamFeatures(url: string, limit?: number) {
487
465
  featureCount = features.length;
488
466
  updateLayer();
489
467
  if (!flewToFeatures) flyToFeaturesBounds();
490
- console.log(
491
- '[FGB]',
492
- 'stream done:',
493
- features.length,
494
- 'features in',
495
- (performance.now() - t0).toFixed(0),
496
- 'ms'
497
- );
498
468
 
499
469
  if (hitLimit) {
500
470
  hasMore = totalFeatures != null && totalFeatures > features.length;
@@ -601,7 +571,7 @@ function updateLayer() {
601
571
  pointRadiusMinPixels: 4,
602
572
  pointRadiusMaxPixels: 12,
603
573
  autoHighlight: true,
604
- highlightColor: [255, 255, 255, 100],
574
+ highlightColor: HIGHLIGHT_COLOR,
605
575
  onHover: mapRef ? hoverCursor(mapRef) : undefined,
606
576
  onClick: (info: any) => {
607
577
  if (info.object?.properties) {
@@ -638,11 +608,11 @@ function onMapReady(map: maplibregl.Map) {
638
608
  <div class="relative flex h-full overflow-hidden">
639
609
  {#if loading}
640
610
  <div class="flex flex-1 items-center justify-center">
641
- <p class="text-sm text-zinc-400">{t('map.loadingFgb')}</p>
611
+ <p class="text-sm text-muted-foreground">{t('map.loadingFgb')}</p>
642
612
  </div>
643
613
  {:else if error && featureCount === 0}
644
614
  <div class="flex flex-1 items-center justify-center">
645
- <p class="text-sm text-red-400">{error}</p>
615
+ <p class="text-sm text-destructive">{error}</p>
646
616
  </div>
647
617
  {:else}
648
618
  <div class="flex-1">
@@ -1,5 +1,13 @@
1
1
  <script lang="ts">
2
2
  import LocateIcon from '@lucide/svelte/icons/locate';
3
+ import {
4
+ buildGeoArrowTables,
5
+ FIRST_FEATURE_FLY_ZOOM,
6
+ type GeoArrowGeomType,
7
+ type GeoArrowResult,
8
+ handleLoadError,
9
+ parseWKB
10
+ } from '@walkthru-earth/objex-utils';
3
11
  import type maplibregl from 'maplibre-gl';
4
12
  import { onDestroy } from 'svelte';
5
13
  import { t } from '../../i18n/index.svelte.js';
@@ -14,12 +22,6 @@ import {
14
22
  hoverCursor,
15
23
  loadGeoArrowModules
16
24
  } from '../../utils/deck.js';
17
- import {
18
- buildGeoArrowTables,
19
- type GeoArrowGeomType,
20
- type GeoArrowResult
21
- } from '../../utils/geoarrow.js';
22
- import { parseWKB } from '../../utils/wkb.js';
23
25
  import LoadProgress, { type ProgressEntry } from './LoadProgress.svelte';
24
26
  import AttributeTable from './map/AttributeTable.svelte';
25
27
  import MapContainer from './map/MapContainer.svelte';
@@ -73,7 +75,7 @@ function extractFirstCoord(coords: any): [number, number] | null {
73
75
 
74
76
  function flyToFirstFeature() {
75
77
  if (!mapRef || !firstFeatureCoord) return;
76
- mapRef.flyTo({ center: firstFeatureCoord, zoom: 14 });
78
+ mapRef.flyTo({ center: firstFeatureCoord, zoom: FIRST_FEATURE_FLY_ZOOM });
77
79
  }
78
80
 
79
81
  // mapData is read synchronously in loadGeoData (before any await),
@@ -173,7 +175,7 @@ async function loadGeoData() {
173
175
  loading = false;
174
176
  } catch (err) {
175
177
  if (gen !== loadGen) return;
176
- error = err instanceof Error ? err.message : String(err);
178
+ error = handleLoadError(err);
177
179
  loading = false;
178
180
  }
179
181
  }
@@ -225,7 +227,7 @@ function onMapReady(map: maplibregl.Map) {
225
227
  <LoadProgress stage={t('map.loadingGeometry')} entries={progressEntries} />
226
228
  {:else if error}
227
229
  <div class="flex flex-1 items-center justify-center">
228
- <p class="text-sm text-red-400">{error}</p>
230
+ <p class="text-sm text-destructive">{error}</p>
229
231
  </div>
230
232
  {:else}
231
233
  <div class="flex-1">
@@ -1,6 +1,6 @@
1
+ import { type GeoArrowGeomType } from '@walkthru-earth/objex-utils';
1
2
  import type { MapQueryResult, SchemaField } from '../../query/engine';
2
3
  import type { Tab } from '../../types';
3
- import { type GeoArrowGeomType } from '../../utils/geoarrow.js';
4
4
  import { type ProgressEntry } from './LoadProgress.svelte';
5
5
  type $$ComponentProps = {
6
6
  tab: Tab;
@@ -5,6 +5,7 @@ import MinusIcon from '@lucide/svelte/icons/minus';
5
5
  import PlusIcon from '@lucide/svelte/icons/plus';
6
6
  import RotateCwIcon from '@lucide/svelte/icons/rotate-cw';
7
7
  import ScanIcon from '@lucide/svelte/icons/scan';
8
+ import { handleLoadError } from '@walkthru-earth/objex-utils';
8
9
  import { onDestroy } from 'svelte';
9
10
  import { Badge } from '../ui/badge/index.js';
10
11
  import { Button } from '../ui/button/index.js';
@@ -14,8 +15,9 @@ import { t } from '../../i18n/index.svelte.js';
14
15
  import { getAdapter } from '../../storage/index.js';
15
16
  import { tabResources } from '../../stores/tab-resources.svelte.js';
16
17
  import type { Tab } from '../../types';
17
- import { handleLoadError } from '../../utils/error.js';
18
- import { buildHttpsUrl, canStreamDirectly } from '../../utils/url.js';
18
+ import { buildHttpsUrl, canStreamDirectly } from '../../utils/signed-url.js';
19
+ import ViewerHeader from './ViewerHeader.svelte';
20
+ import ViewerStatus from './ViewerStatus.svelte';
19
21
 
20
22
  let { tab }: { tab: Tab } = $props();
21
23
 
@@ -149,13 +151,9 @@ onDestroy(cleanup);
149
151
  </script>
150
152
 
151
153
  <div class="flex h-full flex-col">
152
- <div
153
- 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"
154
- >
155
- <span class="truncate max-w-[120px] text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300">{tab.name}</span>
156
- <Badge variant="secondary">{tab.extension.toUpperCase()}</Badge>
157
-
158
- <div class="ms-auto flex items-center gap-1">
154
+ <ViewerHeader {tab}>
155
+ {#snippet badge()}<Badge variant="secondary">{tab.extension.toUpperCase()}</Badge>{/snippet}
156
+ {#snippet actions()}
159
157
  <!-- Desktop controls -->
160
158
  <div class="hidden items-center gap-1 sm:flex">
161
159
  <Button variant="ghost" size="sm" class="h-7 px-1.5" onclick={zoomIn} title={t('image.zoomIn')}>
@@ -178,7 +176,7 @@ onDestroy(cleanup);
178
176
  <!-- Mobile overflow menu -->
179
177
  <div class="flex sm:hidden">
180
178
  <DropdownMenu.Root>
181
- <DropdownMenu.Trigger class="rounded p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
179
+ <DropdownMenu.Trigger class="rounded p-1 text-muted-foreground hover:bg-muted">
182
180
  <EllipsisVerticalIcon class="size-4" />
183
181
  </DropdownMenu.Trigger>
184
182
  <DropdownMenu.Content align="end" class="w-40">
@@ -190,8 +188,8 @@ onDestroy(cleanup);
190
188
  </DropdownMenu.Content>
191
189
  </DropdownMenu.Root>
192
190
  </div>
193
- </div>
194
- </div>
191
+ {/snippet}
192
+ </ViewerHeader>
195
193
 
196
194
  <!-- svelte-ignore a11y_no_static_element_interactions -->
197
195
  <div
@@ -203,9 +201,9 @@ onDestroy(cleanup);
203
201
  ondblclick={handleDblClick}
204
202
  >
205
203
  {#if loading}
206
- <p class="text-sm text-zinc-400">{t('image.loading')}</p>
204
+ <ViewerStatus kind="loading" message={t('image.loading')} />
207
205
  {:else if error}
208
- <p class="text-sm text-red-400">{error}</p>
206
+ <ViewerStatus kind="error" message={error} />
209
207
  {:else if imgSrc}
210
208
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
211
209
  <img
@@ -29,7 +29,7 @@ let {
29
29
  <!-- Active step -->
30
30
  <div class="flex items-center gap-2 text-center">
31
31
  <Loader2Icon class="size-4 shrink-0 animate-spin text-primary" />
32
- <p class="text-sm text-zinc-500 dark:text-zinc-400">{stage || t('table.loading')}</p>
32
+ <p class="text-sm text-muted-foreground">{stage || t('table.loading')}</p>
33
33
  </div>
34
34
 
35
35
  <!-- Discovered metadata -->
@@ -42,17 +42,17 @@ let {
42
42
  <div class="flex items-start gap-1.5 sm:gap-2">
43
43
  <CheckCircleIcon class="mt-0.5 size-3 shrink-0 text-green-500/80" />
44
44
  <span
45
- class="w-14 shrink-0 text-[11px] leading-4 text-zinc-400 sm:w-[4.5rem] sm:text-xs dark:text-zinc-500"
45
+ class="w-14 shrink-0 text-[11px] leading-4 text-muted-foreground sm:w-[4.5rem] sm:text-xs"
46
46
  >
47
47
  {entry.label}
48
48
  </span>
49
49
  <div class="min-w-0 flex-1">
50
- <span class="break-all text-[11px] font-medium leading-4 text-zinc-600 sm:text-xs dark:text-zinc-300">
50
+ <span class="break-all text-[11px] font-medium leading-4 text-foreground sm:text-xs">
51
51
  {entry.value}
52
52
  </span>
53
53
  {#if entry.detail}
54
54
  <p
55
- class="mt-0.5 truncate font-mono text-[10px] leading-tight text-zinc-400 dark:text-zinc-500"
55
+ class="mt-0.5 truncate font-mono text-[10px] leading-tight text-muted-foreground"
56
56
  >
57
57
  {entry.detail}
58
58
  </p>
@@ -68,7 +68,7 @@ let {
68
68
  {#if onCancel}
69
69
  <div class="flex flex-col items-center gap-2">
70
70
  <button
71
- class="flex items-center gap-1 rounded border border-zinc-300 px-3 py-1 text-xs text-zinc-500 hover:bg-zinc-100 dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800"
71
+ class="flex items-center gap-1 rounded border border-border px-3 py-1 text-xs text-muted-foreground hover:bg-muted"
72
72
  onclick={onCancel}
73
73
  >
74
74
  <XCircleIcon class="size-3" />
@@ -83,7 +83,7 @@ let {
83
83
  <XCircleIcon class="size-3" />
84
84
  {t('table.forceStop')}
85
85
  </button>
86
- <p class="max-w-xs text-center text-[10px] text-zinc-400 dark:text-zinc-500">
86
+ <p class="max-w-xs text-center text-[10px] text-muted-foreground">
87
87
  {t('table.forceStopWarning')}
88
88
  </p>
89
89
  </div>