@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,5 @@
1
+ import { type StacRoutableKind } from '@walkthru-earth/objex-utils';
1
2
  import type { Tab } from '../../types.js';
2
- import { type StacRoutableKind } from '../../utils/stac.js';
3
3
  type $$ComponentProps = {
4
4
  tab: Tab;
5
5
  classified?: StacRoutableKind;
@@ -1,5 +1,11 @@
1
1
  <script lang="ts">
2
2
  import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
3
+ import {
4
+ copyToClipboard,
5
+ handleLoadError,
6
+ renderNotebook,
7
+ wireCodeCopyButtons
8
+ } from '@walkthru-earth/objex-utils';
3
9
  import type { BundledLanguage } from 'shiki';
4
10
  import { onDestroy, tick } from 'svelte';
5
11
  import { Badge } from '../ui/badge/index.js';
@@ -9,10 +15,9 @@ import { t } from '../../i18n/index.svelte.js';
9
15
  import { getAdapter } from '../../storage/index.js';
10
16
  import { tabResources } from '../../stores/tab-resources.svelte.js';
11
17
  import type { Tab } from '../../types';
12
- import { copyToClipboard, wireCodeCopyButtons } from '../../utils/clipboard.js';
13
- import { handleLoadError } from '../../utils/error.js';
14
- import { renderNotebook } from '../../utils/notebook';
15
18
  import { highlightCodeReversed } from '../../utils/shiki';
19
+ import ViewerHeader from './ViewerHeader.svelte';
20
+ import ViewerStatus from './ViewerStatus.svelte';
16
21
 
17
22
  let { tab }: { tab: Tab } = $props();
18
23
 
@@ -132,23 +137,21 @@ async function copyRaw() {
132
137
  </script>
133
138
 
134
139
  <div class="flex h-full flex-col">
135
- <div
136
- 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"
137
- >
138
- <span class="truncate max-w-[120px] text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300">{tab.name}</span>
139
- <Badge variant="secondary">{t('notebook.badge')}</Badge>
140
- {#if kernelName}
141
- <Badge variant="outline" class="hidden border-orange-200 text-orange-600 sm:inline-flex dark:border-orange-800 dark:text-orange-300">
142
- {kernelName}
143
- </Badge>
144
- {/if}
145
- {#if cellCount > 0}
146
- <span class="hidden text-xs text-muted-foreground sm:inline">
147
- {cellCount} {t('notebook.cells')}
148
- </span>
149
- {/if}
150
-
151
- <div class="ms-auto flex items-center gap-1 sm:gap-2">
140
+ <ViewerHeader {tab}>
141
+ {#snippet badge()}
142
+ <Badge variant="secondary">{t('notebook.badge')}</Badge>
143
+ {#if kernelName}
144
+ <Badge variant="outline" class="hidden border-orange-200 text-orange-600 sm:inline-flex dark:border-orange-800 dark:text-orange-300">
145
+ {kernelName}
146
+ </Badge>
147
+ {/if}
148
+ {#if cellCount > 0}
149
+ <span class="hidden text-xs text-muted-foreground sm:inline">
150
+ {cellCount} {t('notebook.cells')}
151
+ </span>
152
+ {/if}
153
+ {/snippet}
154
+ {#snippet actions()}
152
155
  <!-- Desktop controls -->
153
156
  <div class="hidden items-center gap-1 sm:flex">
154
157
  <Button variant="ghost" size="sm" class="h-7 px-2 text-xs" onclick={toggleCode}>
@@ -162,7 +165,7 @@ async function copyRaw() {
162
165
  <!-- Mobile overflow menu -->
163
166
  <div class="flex sm:hidden">
164
167
  <DropdownMenu.Root>
165
- <DropdownMenu.Trigger class="rounded p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800">
168
+ <DropdownMenu.Trigger class="rounded p-1 text-muted-foreground hover:bg-muted">
166
169
  <EllipsisVerticalIcon class="size-4" />
167
170
  </DropdownMenu.Trigger>
168
171
  <DropdownMenu.Content align="end" class="w-44">
@@ -175,18 +178,14 @@ async function copyRaw() {
175
178
  </DropdownMenu.Content>
176
179
  </DropdownMenu.Root>
177
180
  </div>
178
- </div>
179
- </div>
181
+ {/snippet}
182
+ </ViewerHeader>
180
183
 
181
184
  <div class="notebook-viewer flex-1 overflow-auto" dir="ltr">
182
185
  {#if loading}
183
- <div class="flex h-full items-center justify-center">
184
- <p class="text-sm text-zinc-400">{t('notebook.loading')}</p>
185
- </div>
186
+ <ViewerStatus kind="loading" message={t('notebook.loading')} />
186
187
  {:else if error}
187
- <div class="flex h-full items-center justify-center">
188
- <p class="text-sm text-red-400">{error}</p>
189
- </div>
188
+ <ViewerStatus kind="error" message={error} />
190
189
  {/if}
191
190
  <div bind:this={container} class="notebook-content" class:hidden={loading || !!error}></div>
192
191
  </div>
@@ -4,6 +4,7 @@ import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
4
4
  import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
5
5
  import MinusIcon from '@lucide/svelte/icons/minus';
6
6
  import PlusIcon from '@lucide/svelte/icons/plus';
7
+ import { handleLoadError } from '@walkthru-earth/objex-utils';
7
8
  import type { PDFDocumentLoadingTask, PDFDocumentProxy } from 'pdfjs-dist';
8
9
  import { onDestroy, untrack } from 'svelte';
9
10
  import { Badge } from '../ui/badge/index.js';
@@ -14,9 +15,10 @@ 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
18
  import { loadPdfDocument, loadPdfFromUrl } from '../../utils/pdf';
19
- import { buildHttpsUrl, canStreamDirectly } from '../../utils/url.js';
19
+ import { buildHttpsUrl, canStreamDirectly } from '../../utils/signed-url.js';
20
+ import ViewerHeader from './ViewerHeader.svelte';
21
+ import ViewerStatus from './ViewerStatus.svelte';
20
22
 
21
23
  const LOAD_TIMEOUT_MS = 20_000;
22
24
 
@@ -136,7 +138,7 @@ async function renderPage(
136
138
  await page.render({ canvasContext: ctx, viewport, canvas } as any).promise;
137
139
  } catch (err) {
138
140
  if (gen === renderGeneration) {
139
- error = err instanceof Error ? err.message : String(err);
141
+ error = handleLoadError(err);
140
142
  }
141
143
  }
142
144
  }
@@ -174,17 +176,10 @@ onDestroy(cleanup);
174
176
  </script>
175
177
 
176
178
  <div class="flex h-full flex-col">
177
- <div
178
- 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"
179
- >
180
- <span
181
- class="truncate max-w-[120px] text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300"
182
- >{tab.name}</span
183
- >
184
- <Badge variant="secondary">{t("pdf.badge")}</Badge>
185
-
186
- {#if totalPages > 0}
187
- <div class="ms-auto flex items-center gap-1 sm:gap-2">
179
+ <ViewerHeader {tab}>
180
+ {#snippet badge()}<Badge variant="secondary">{t('pdf.badge')}</Badge>{/snippet}
181
+ {#snippet actions()}
182
+ {#if totalPages > 0}
188
183
  <!-- Pagination (always visible) -->
189
184
  <Button
190
185
  variant="ghost"
@@ -194,9 +189,9 @@ onDestroy(cleanup);
194
189
  disabled={currentPage <= 1}
195
190
  >
196
191
  <ChevronLeftIcon class="size-3.5" />
197
- <span class="hidden sm:inline">{t("pdf.prev")}</span>
192
+ <span class="hidden sm:inline">{t('pdf.prev')}</span>
198
193
  </Button>
199
- <span class="text-xs text-zinc-500 dark:text-zinc-400">
194
+ <span class="text-xs text-muted-foreground">
200
195
  {currentPage} / {totalPages}
201
196
  </span>
202
197
  <Button
@@ -206,7 +201,7 @@ onDestroy(cleanup);
206
201
  onclick={nextPage}
207
202
  disabled={currentPage >= totalPages}
208
203
  >
209
- <span class="hidden sm:inline">{t("pdf.next")}</span>
204
+ <span class="hidden sm:inline">{t('pdf.next')}</span>
210
205
  <ChevronRightIcon class="size-3.5" />
211
206
  </Button>
212
207
 
@@ -218,11 +213,11 @@ onDestroy(cleanup);
218
213
  size="sm"
219
214
  class="h-7 px-1.5"
220
215
  onclick={zoomOut}
221
- title={t("pdf.zoomOut")}
216
+ title={t('pdf.zoomOut')}
222
217
  >
223
218
  <MinusIcon class="size-3.5" />
224
219
  </Button>
225
- <span class="text-xs text-zinc-500 dark:text-zinc-400">
220
+ <span class="text-xs text-muted-foreground">
226
221
  {Math.round(scale * 100)}%
227
222
  </span>
228
223
  <Button
@@ -230,7 +225,7 @@ onDestroy(cleanup);
230
225
  size="sm"
231
226
  class="h-7 px-1.5"
232
227
  onclick={zoomIn}
233
- title={t("pdf.zoomIn")}
228
+ title={t('pdf.zoomIn')}
234
229
  >
235
230
  <PlusIcon class="size-3.5" />
236
231
  </Button>
@@ -240,39 +235,35 @@ onDestroy(cleanup);
240
235
  <div class="flex sm:hidden">
241
236
  <DropdownMenu.Root>
242
237
  <DropdownMenu.Trigger
243
- class="rounded p-1 text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800"
238
+ class="rounded p-1 text-muted-foreground hover:bg-muted"
244
239
  >
245
240
  <EllipsisVerticalIcon class="size-4" />
246
241
  </DropdownMenu.Trigger>
247
242
  <DropdownMenu.Content align="end" class="w-44">
248
243
  <DropdownMenu.Item onclick={zoomIn}>
249
- {t("pdf.zoomIn")}
244
+ {t('pdf.zoomIn')}
250
245
  </DropdownMenu.Item>
251
246
  <DropdownMenu.Item onclick={zoomOut}>
252
- {t("pdf.zoomOut")}
247
+ {t('pdf.zoomOut')}
253
248
  </DropdownMenu.Item>
254
249
  <DropdownMenu.Separator />
255
250
  <DropdownMenu.Item disabled>
256
- {t("pdf.zoom")}: {Math.round(scale * 100)}%
251
+ {t('pdf.zoom')}: {Math.round(scale * 100)}%
257
252
  </DropdownMenu.Item>
258
253
  </DropdownMenu.Content>
259
254
  </DropdownMenu.Root>
260
255
  </div>
261
- </div>
262
- {/if}
263
- </div>
256
+ {/if}
257
+ {/snippet}
258
+ </ViewerHeader>
264
259
 
265
260
  <div
266
261
  class="flex flex-1 items-start justify-center overflow-auto bg-zinc-200 p-4 dark:bg-zinc-800"
267
262
  >
268
263
  {#if loading}
269
- <div class="flex h-full items-center justify-center">
270
- <p class="text-sm text-zinc-400">{t("pdf.loading")}</p>
271
- </div>
264
+ <ViewerStatus kind="loading" message={t('pdf.loading')} />
272
265
  {:else if error}
273
- <div class="flex h-full items-center justify-center">
274
- <p class="text-sm text-red-400">{error}</p>
275
- </div>
266
+ <ViewerStatus kind="error" message={error} />
276
267
  {:else}
277
268
  <canvas bind:this={canvasEl} class="shadow-lg"></canvas>
278
269
  {/if}
@@ -2,6 +2,7 @@
2
2
  import ArchiveIcon from '@lucide/svelte/icons/archive';
3
3
  import GridIcon from '@lucide/svelte/icons/grid-3x3';
4
4
  import MapIcon from '@lucide/svelte/icons/map';
5
+ import { handleLoadError } from '@walkthru-earth/objex-utils';
5
6
  import type { PMTiles } from 'pmtiles';
6
7
  import { onDestroy, untrack } from 'svelte';
7
8
  import { Badge } from '../ui/badge/index.js';
@@ -11,8 +12,8 @@ import { t } from '../../i18n/index.svelte.js';
11
12
  import { tabResources } from '../../stores/tab-resources.svelte.js';
12
13
  import type { Tab } from '../../types';
13
14
  import { loadPmtiles, type PmtilesMetadata } from '../../utils/pmtiles';
14
- import { buildHttpsUrlAsync } from '../../utils/url.js';
15
- import { getUrlView, updateUrlView } from '../../utils/url-state.js';
15
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
16
+ import { pickViewMode, updateUrlView } from '../../utils/url-state.js';
16
17
 
17
18
  let { tab }: { tab: Tab } = $props();
18
19
 
@@ -25,10 +26,7 @@ let pmtilesInstance = $state<PMTiles | null>(null);
25
26
  let pmtilesUrl = $state('');
26
27
 
27
28
  // Read initial view from URL hash
28
- const urlView = getUrlView();
29
- let viewMode = $state<ViewMode>(
30
- urlView === 'archive' ? 'archive' : urlView === 'inspector' ? 'inspector' : 'map'
31
- );
29
+ let viewMode = $state<ViewMode>(pickViewMode<ViewMode>(['map', 'archive', 'inspector'], 'map'));
32
30
 
33
31
  // Tile inspector initial coordinates (set when navigating from archive)
34
32
  let inspectorZ = $state<number | undefined>();
@@ -78,7 +76,7 @@ async function load() {
78
76
  pmtilesInstance = result.pmtiles;
79
77
  metadata = result.metadata;
80
78
  } catch (err) {
81
- error = err instanceof Error ? err.message : String(err);
79
+ error = handleLoadError(err);
82
80
  } finally {
83
81
  loading = false;
84
82
  }
@@ -91,18 +89,18 @@ const fileName = $derived(tab.path.split('/').pop() ?? 'pmtiles');
91
89
  <!-- Toolbar -->
92
90
  {#if !loading && !error && metadata}
93
91
  <div
94
- 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"
92
+ class="flex items-center gap-1 border-b border-border px-2 py-1.5 sm:gap-2 sm:px-4"
95
93
  >
96
94
  <!-- File info -->
97
95
  <span
98
- class="max-w-[100px] truncate text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300"
96
+ class="max-w-[100px] truncate text-sm font-medium text-foreground sm:max-w-none"
99
97
  >
100
98
  {fileName}
101
99
  </span>
102
100
  <Badge variant="outline" class="hidden text-[10px] sm:inline-flex">
103
101
  {metadata.formatLabel}
104
102
  </Badge>
105
- <span class="hidden text-xs text-zinc-400 sm:inline dark:text-zinc-500">
103
+ <span class="hidden text-xs text-muted-foreground sm:inline">
106
104
  z{metadata.minZoom}-{metadata.maxZoom} · {metadata.numAddressedTiles.toLocaleString()} tiles
107
105
  </span>
108
106
 
@@ -112,7 +110,7 @@ const fileName = $derived(tab.path.split('/').pop() ?? 'pmtiles');
112
110
  variant={viewMode === 'map' ? 'default' : 'outline'}
113
111
  size="sm"
114
112
  class="h-7 gap-1 px-2 text-xs {viewMode !== 'map'
115
- ? 'border-zinc-300 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-900'
113
+ ? 'border-border text-muted-foreground hover:bg-muted'
116
114
  : ''}"
117
115
  onclick={() => setViewMode('map')}
118
116
  >
@@ -124,7 +122,7 @@ const fileName = $derived(tab.path.split('/').pop() ?? 'pmtiles');
124
122
  variant={viewMode === 'archive' ? 'default' : 'outline'}
125
123
  size="sm"
126
124
  class="h-7 gap-1 px-2 text-xs {viewMode !== 'archive'
127
- ? 'border-zinc-300 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-900'
125
+ ? 'border-border text-muted-foreground hover:bg-muted'
128
126
  : ''}"
129
127
  onclick={() => setViewMode('archive')}
130
128
  >
@@ -136,7 +134,7 @@ const fileName = $derived(tab.path.split('/').pop() ?? 'pmtiles');
136
134
  variant={viewMode === 'inspector' ? 'default' : 'outline'}
137
135
  size="sm"
138
136
  class="h-7 gap-1 px-2 text-xs {viewMode !== 'inspector'
139
- ? 'border-zinc-300 text-zinc-600 hover:bg-zinc-50 dark:border-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-900'
137
+ ? 'border-border text-muted-foreground hover:bg-muted'
140
138
  : ''}"
141
139
  onclick={() => setViewMode('inspector')}
142
140
  >
@@ -151,11 +149,11 @@ const fileName = $derived(tab.path.split('/').pop() ?? 'pmtiles');
151
149
  <div class="min-h-0 flex-1 overflow-hidden">
152
150
  {#if loading}
153
151
  <div class="flex h-full items-center justify-center">
154
- <p class="text-sm text-zinc-400">{t('map.loadingPmtiles')}</p>
152
+ <p class="text-sm text-muted-foreground">{t('map.loadingPmtiles')}</p>
155
153
  </div>
156
154
  {:else if error}
157
155
  <div class="flex h-full items-center justify-center">
158
- <p class="text-sm text-red-400">{error}</p>
156
+ <p class="text-sm text-destructive">{error}</p>
159
157
  </div>
160
158
  {:else if metadata && pmtilesInstance}
161
159
  {#if viewMode === 'map'}
@@ -50,22 +50,22 @@ function truncateSql(sql: string, maxLen = 120): string {
50
50
  {#if visible}
51
51
  <!-- Mobile: absolute overlay; Desktop: flex sidebar -->
52
52
  <div
53
- class="absolute inset-y-0 end-0 z-10 flex w-72 flex-col overflow-hidden border-s border-zinc-200 bg-zinc-50 sm:relative sm:z-auto sm:shrink-0 dark:border-zinc-800 dark:bg-zinc-900"
53
+ class="absolute inset-y-0 end-0 z-10 flex w-72 flex-col overflow-hidden border-s border-border bg-muted sm:relative sm:z-auto sm:shrink-0"
54
54
  >
55
55
  <!-- Header -->
56
56
  <div
57
- class="flex items-center justify-between border-b border-zinc-200 px-3 py-2 dark:border-zinc-800"
57
+ class="flex items-center justify-between border-b border-border px-3 py-2"
58
58
  >
59
59
  <div class="flex items-center gap-1.5">
60
- <ClockIcon class="size-3.5 text-zinc-500" />
61
- <h3 class="text-xs font-medium text-zinc-500 dark:text-zinc-400">
60
+ <ClockIcon class="size-3.5 text-muted-foreground" />
61
+ <h3 class="text-xs font-medium text-muted-foreground">
62
62
  {t('queryHistory.title')}
63
63
  </h3>
64
64
  </div>
65
65
  <div class="flex items-center gap-2">
66
66
  {#if queryHistory.entries.length > 0}
67
67
  <button
68
- class="text-[10px] text-zinc-400 hover:text-red-500 dark:hover:text-red-400"
68
+ class="text-[10px] text-muted-foreground hover:text-destructive"
69
69
  onclick={() => queryHistory.clear()}
70
70
  >
71
71
  {t('queryHistory.clearAll')}
@@ -73,7 +73,7 @@ function truncateSql(sql: string, maxLen = 120): string {
73
73
  {/if}
74
74
  {#if onClose}
75
75
  <button
76
- class="rounded p-0.5 text-zinc-400 hover:bg-zinc-200 hover:text-zinc-600 sm:hidden dark:hover:bg-zinc-700 dark:hover:text-zinc-300"
76
+ class="rounded p-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground sm:hidden"
77
77
  onclick={onClose}
78
78
  >
79
79
  <XIcon class="size-3.5" />
@@ -83,20 +83,20 @@ function truncateSql(sql: string, maxLen = 120): string {
83
83
  </div>
84
84
 
85
85
  <!-- Search -->
86
- <div class="border-b border-zinc-200 px-3 py-1.5 dark:border-zinc-800">
86
+ <div class="border-b border-border px-3 py-1.5">
87
87
  <div
88
- class="flex items-center gap-1.5 rounded border border-zinc-200 bg-white px-2 py-1 dark:border-zinc-700 dark:bg-zinc-800"
88
+ class="flex items-center gap-1.5 rounded border border-border bg-background px-2 py-1"
89
89
  >
90
- <SearchIcon class="size-3 shrink-0 text-zinc-400" />
90
+ <SearchIcon class="size-3 shrink-0 text-muted-foreground" />
91
91
  <input
92
92
  type="text"
93
- class="w-full bg-transparent text-xs outline-none placeholder:text-zinc-400"
93
+ class="w-full bg-transparent text-xs outline-none placeholder:text-muted-foreground"
94
94
  placeholder={t('queryHistory.searchPlaceholder')}
95
95
  bind:value={searchQuery}
96
96
  />
97
97
  {#if searchQuery}
98
98
  <button
99
- class="shrink-0 text-zinc-400 hover:text-zinc-600"
99
+ class="shrink-0 text-muted-foreground hover:text-foreground"
100
100
  onclick={() => {
101
101
  searchQuery = '';
102
102
  }}
@@ -110,14 +110,14 @@ function truncateSql(sql: string, maxLen = 120): string {
110
110
  <!-- Entries -->
111
111
  <ScrollArea class="flex-1">
112
112
  {#if filteredEntries.length === 0}
113
- <div class="px-3 py-6 text-center text-xs text-zinc-400">
113
+ <div class="px-3 py-6 text-center text-xs text-muted-foreground">
114
114
  {searchQuery ? 'No matching queries' : 'No query history yet'}
115
115
  </div>
116
116
  {:else}
117
- <div class="divide-y divide-zinc-100 dark:divide-zinc-800">
117
+ <div class="divide-y divide-border">
118
118
  {#each filteredEntries as entry (entry.id)}
119
119
  <div
120
- class="group flex w-full cursor-pointer flex-col gap-0.5 px-3 py-2 text-start hover:bg-zinc-100 dark:hover:bg-zinc-800"
120
+ class="group flex w-full cursor-pointer flex-col gap-0.5 px-3 py-2 text-start hover:bg-accent"
121
121
  role="button"
122
122
  tabindex="0"
123
123
  onclick={() => onSelect?.(entry.sql)}
@@ -126,18 +126,18 @@ function truncateSql(sql: string, maxLen = 120): string {
126
126
  }}
127
127
  >
128
128
  <div
129
- class="font-mono text-[11px] leading-snug text-zinc-600 dark:text-zinc-300"
129
+ class="font-mono text-[11px] leading-snug text-foreground"
130
130
  >
131
131
  {truncateSql(entry.sql)}
132
132
  </div>
133
- <div class="flex items-center gap-2 text-[10px] text-zinc-400">
133
+ <div class="flex items-center gap-2 text-[10px] text-muted-foreground">
134
134
  <span>{formatTime(entry.timestamp)}</span>
135
135
  <span>{entry.durationMs}ms</span>
136
136
  {#if entry.rowCount > 0}
137
137
  <span>{entry.rowCount.toLocaleString()} rows</span>
138
138
  {/if}
139
139
  {#if entry.error}
140
- <span class="text-red-400">error</span>
140
+ <span class="text-destructive">error</span>
141
141
  {/if}
142
142
  <button
143
143
  class="ms-auto opacity-0 group-hover:opacity-100"
@@ -147,7 +147,7 @@ function truncateSql(sql: string, maxLen = 120): string {
147
147
  }}
148
148
  title="Remove"
149
149
  >
150
- <TrashIcon class="size-3 text-zinc-400 hover:text-red-500" />
150
+ <TrashIcon class="size-3 text-muted-foreground hover:text-destructive" />
151
151
  </button>
152
152
  </div>
153
153
  </div>
@@ -1,13 +1,18 @@
1
1
  <script lang="ts">
2
+ import {
3
+ formatFileSize,
4
+ generateHexDump,
5
+ type HexRow,
6
+ handleLoadError
7
+ } from '@walkthru-earth/objex-utils';
2
8
  import { onDestroy } from 'svelte';
3
9
  import { Badge } from '../ui/badge/index.js';
4
10
  import { t } from '../../i18n/index.svelte.js';
5
11
  import { getAdapter } from '../../storage/index.js';
6
12
  import { tabResources } from '../../stores/tab-resources.svelte.js';
7
13
  import type { Tab } from '../../types';
8
- import { handleLoadError } from '../../utils/error.js';
9
- import { formatFileSize } from '../../utils/format';
10
- import { generateHexDump, type HexRow } from '../../utils/hex';
14
+ import ViewerHeader from './ViewerHeader.svelte';
15
+ import ViewerStatus from './ViewerStatus.svelte';
11
16
 
12
17
  let { tab }: { tab: Tab } = $props();
13
18
 
@@ -66,30 +71,31 @@ async function loadHexDump() {
66
71
  </script>
67
72
 
68
73
  <div class="flex h-full flex-col">
69
- <div
70
- 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"
71
- >
72
- <span class="truncate max-w-[120px] text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300">{tab.name}</span>
73
- {#if tab.extension}
74
- <Badge variant="secondary">{tab.extension}</Badge>
75
- {/if}
76
- {#if !loading && fileSize > 0}
77
- <span class="hidden text-xs text-zinc-400 sm:inline dark:text-zinc-500">
78
- {formatFileSize(fileSize)}
79
- </span>
80
- {#if truncated}
81
- <span class="hidden text-xs text-amber-500 sm:inline">
82
- ({t('raw.showingFirst').replace('{size}', formatFileSize(MAX_BYTES))})
74
+ <ViewerHeader {tab}>
75
+ {#snippet badge()}
76
+ {#if tab.extension}
77
+ <Badge variant="secondary">{tab.extension}</Badge>
78
+ {/if}
79
+ {/snippet}
80
+ {#snippet actions()}
81
+ {#if !loading && fileSize > 0}
82
+ <span class="hidden text-xs text-muted-foreground sm:inline">
83
+ {formatFileSize(fileSize)}
83
84
  </span>
85
+ {#if truncated}
86
+ <span class="hidden text-xs text-amber-500 sm:inline">
87
+ ({t('raw.showingFirst').replace('{size}', formatFileSize(MAX_BYTES))})
88
+ </span>
89
+ {/if}
84
90
  {/if}
85
- {/if}
86
- </div>
91
+ {/snippet}
92
+ </ViewerHeader>
87
93
 
88
94
  <div class="flex-1 overflow-auto bg-zinc-950 p-4 font-mono text-xs">
89
95
  {#if loading}
90
- <p class="text-zinc-400">{t('raw.loading')}</p>
96
+ <ViewerStatus kind="loading" message={t('raw.loading')} />
91
97
  {:else if error}
92
- <p class="text-red-400">{error}</p>
98
+ <ViewerStatus kind="error" message={error} />
93
99
  {:else}
94
100
  <table class="w-full border-collapse">
95
101
  <thead>
@@ -1,23 +1,16 @@
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, variant = 'stac-map' }: { tab: Tab; variant?: 'stac-map' | 'stac-browser' } = $props();
6
6
 
7
7
  let fileUrl = $state('');
8
8
 
9
- $effect(() => {
10
- const id = tab.id;
11
- let cancelled = false;
12
- (async () => {
13
- const url = await buildHttpsUrlAsync(tab);
14
- if (cancelled || id !== tab.id) return;
15
- fileUrl = url;
16
- })();
17
- return () => {
18
- cancelled = true;
19
- };
20
- });
9
+ $effect(() =>
10
+ resolveSignedTabUrl(tab, (u) => {
11
+ fileUrl = u;
12
+ })
13
+ );
21
14
 
22
15
  const iframeSrc = $derived.by(() => {
23
16
  if (!fileUrl) return '';