@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 ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
3
+ import { formatFileSize, handleLoadError } from '@walkthru-earth/objex-utils';
3
4
  import type { PMTiles } from 'pmtiles';
4
5
  import { tileIdToZxy } from 'pmtiles';
5
6
  import {
@@ -8,9 +9,9 @@ import {
8
9
  ResizablePaneGroup
9
10
  } from '../../ui/resizable/index.js';
10
11
  import { t } from '../../../i18n/index.svelte.js';
11
- import { formatFileSize } from '../../../utils/format.js';
12
12
  import type { PmtilesMetadata } from '../../../utils/pmtiles';
13
13
  import { highlightCode } from '../../../utils/shiki';
14
+ import { useIsWide } from '../../../utils/media-query.svelte.js';
14
15
 
15
16
  let {
16
17
  metadata,
@@ -22,6 +23,8 @@ let {
22
23
  onOpenInspector?: (z: number, x: number, y: number) => void;
23
24
  } = $props();
24
25
 
26
+ const isWide = useIsWide();
27
+
25
28
  interface ZoomSummary {
26
29
  zoom: number;
27
30
  count: number;
@@ -126,7 +129,7 @@ async function selectZoom(zoom: number) {
126
129
  if (result.length > 5000) break;
127
130
  }
128
131
  } catch (err) {
129
- errorMsg = err instanceof Error ? err.message : String(err);
132
+ errorMsg = handleLoadError(err) ?? '';
130
133
  }
131
134
  }
132
135
  } else {
@@ -165,7 +168,7 @@ const dedupRatio = $derived(
165
168
  {#snippet entryDetails()}
166
169
  {#if selectedEntry}
167
170
  <div
168
- 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"
171
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
169
172
  >
170
173
  {t('pmtiles.entryDetails')}
171
174
  </div>
@@ -212,7 +215,7 @@ const dedupRatio = $derived(
212
215
  <div class="flex h-full flex-col overflow-hidden">
213
216
  <!-- Stats grid -->
214
217
  <div
215
- class="shrink-0 border-b border-zinc-200 px-3 py-3 sm:px-4 dark:border-zinc-800"
218
+ class="shrink-0 border-b border-border px-3 py-3 sm:px-4"
216
219
  >
217
220
  <div class="grid grid-cols-2 gap-x-4 gap-y-2 text-xs sm:grid-cols-3 lg:grid-cols-6">
218
221
  <div>
@@ -278,96 +281,119 @@ const dedupRatio = $derived(
278
281
  {/if}
279
282
  </div>
280
283
 
281
- <!-- Column browser (resizable) -->
282
- <ResizablePaneGroup direction="horizontal" class="min-h-0 flex-1">
283
- <!-- Column 1: Zoom levels -->
284
- <ResizablePane defaultSize={28} minSize={15}>
285
- <div class="flex h-full flex-col">
284
+ <!-- Column browser (resizable or stacked) -->
285
+ {#snippet zoomLevels()}
286
+ <div class="flex h-full flex-col">
287
+ <div
288
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
289
+ >
290
+ {t('pmtiles.zoomLevels')}
291
+ </div>
292
+ <div class="flex-1 overflow-auto">
293
+ {#each zoomSummaries as s}
294
+ <button
295
+ 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"
296
+ class:bg-muted={selectedZoom === s.zoom}
297
+ onclick={() => selectZoom(s.zoom)}
298
+ >
299
+ <span class="w-7 shrink-0 font-mono text-muted-foreground">z{s.zoom}</span>
300
+ <div class="min-w-0 flex-1">
301
+ <div
302
+ class="h-1.5 rounded-full bg-blue-500/60"
303
+ style="width: {Math.max(2, (s.count / maxCount) * 100)}%"
304
+ ></div>
305
+ </div>
306
+ <span class="shrink-0 text-[10px] tabular-nums text-muted-foreground">
307
+ {s.count.toLocaleString()}
308
+ </span>
309
+ <ChevronRightIcon class="size-3 shrink-0 text-muted-foreground" />
310
+ </button>
311
+ {/each}
312
+ </div>
313
+ </div>
314
+ {/snippet}
315
+
316
+ {#snippet zoomEntryList()}
317
+ <div class="flex h-full flex-col">
318
+ {#if selectedZoom !== null}
286
319
  <div
287
- 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"
320
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
288
321
  >
289
- {t('pmtiles.zoomLevels')}
322
+ {t('pmtiles.tilesAtZoom').replace('{zoom}', String(selectedZoom))}
323
+ <span class="ms-1 normal-case tracking-normal">({zoomEntries.length.toLocaleString()})</span>
290
324
  </div>
291
325
  <div class="flex-1 overflow-auto">
292
- {#each zoomSummaries as s}
293
- <button
294
- 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"
295
- class:bg-zinc-100={selectedZoom === s.zoom}
296
- class:dark:bg-zinc-800={selectedZoom === s.zoom}
297
- onclick={() => selectZoom(s.zoom)}
298
- >
299
- <span class="w-7 shrink-0 font-mono text-muted-foreground">z{s.zoom}</span>
300
- <div class="min-w-0 flex-1">
301
- <div
302
- class="h-1.5 rounded-full bg-blue-500/60"
303
- style="width: {Math.max(2, (s.count / maxCount) * 100)}%"
304
- ></div>
305
- </div>
306
- <span class="shrink-0 text-[10px] tabular-nums text-muted-foreground">
307
- {s.count.toLocaleString()}
308
- </span>
309
- <ChevronRightIcon class="size-3 shrink-0 text-muted-foreground" />
310
- </button>
311
- {/each}
326
+ {#if loadingEntries}
327
+ <div class="p-4 text-center text-xs text-muted-foreground">Loading...</div>
328
+ {:else if errorMsg}
329
+ <div class="p-4 text-center text-xs text-destructive">{errorMsg}</div>
330
+ {:else if zoomEntries.length === 0}
331
+ <div class="p-4 text-center text-xs text-muted-foreground">{t('pmtiles.noEntries')}</div>
332
+ {:else}
333
+ {#each zoomEntries as entry}
334
+ <button
335
+ class="flex w-full items-center gap-2 px-3 py-1 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
336
+ class:bg-muted={selectedEntry?.tileId === entry.tileId}
337
+ onclick={() => (selectedEntry = entry)}
338
+ >
339
+ <span class="shrink-0 truncate font-mono text-[11px]">
340
+ {entry.z}/{entry.x}/{entry.y}
341
+ </span>
342
+ <span class="ms-auto shrink-0 text-[10px] tabular-nums text-muted-foreground">
343
+ {formatBytes(entry.length)}
344
+ </span>
345
+ <ChevronRightIcon class="size-3 shrink-0 text-muted-foreground" />
346
+ </button>
347
+ {/each}
348
+ {/if}
312
349
  </div>
313
- </div>
314
- </ResizablePane>
350
+ {:else}
351
+ <div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
352
+ Select a zoom level
353
+ </div>
354
+ {/if}
355
+ </div>
356
+ {/snippet}
315
357
 
316
- <ResizableHandle />
358
+ {#if isWide.value}
359
+ <ResizablePaneGroup direction="horizontal" class="min-h-0 flex-1">
360
+ <!-- Column 1: Zoom levels -->
361
+ <ResizablePane defaultSize={28} minSize={15}>
362
+ {@render zoomLevels()}
363
+ </ResizablePane>
317
364
 
318
- <!-- Column 2: Entries at zoom -->
319
- <ResizablePane defaultSize={42} minSize={20}>
320
- <div class="flex h-full flex-col">
321
- {#if selectedZoom !== null}
322
- <div
323
- 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"
324
- >
325
- {t('pmtiles.tilesAtZoom').replace('{zoom}', String(selectedZoom))}
326
- <span class="ms-1 normal-case tracking-normal">({zoomEntries.length.toLocaleString()})</span>
327
- </div>
328
- <div class="flex-1 overflow-auto">
329
- {#if loadingEntries}
330
- <div class="p-4 text-center text-xs text-muted-foreground">Loading...</div>
331
- {:else if errorMsg}
332
- <div class="p-4 text-center text-xs text-red-400">{errorMsg}</div>
333
- {:else if zoomEntries.length === 0}
334
- <div class="p-4 text-center text-xs text-muted-foreground">{t('pmtiles.noEntries')}</div>
335
- {:else}
336
- {#each zoomEntries as entry}
337
- <button
338
- class="flex w-full items-center gap-2 px-3 py-1 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
339
- class:bg-zinc-100={selectedEntry?.tileId === entry.tileId}
340
- class:dark:bg-zinc-800={selectedEntry?.tileId === entry.tileId}
341
- onclick={() => (selectedEntry = entry)}
342
- >
343
- <span class="shrink-0 truncate font-mono text-[11px]">
344
- {entry.z}/{entry.x}/{entry.y}
345
- </span>
346
- <span class="ms-auto shrink-0 text-[10px] tabular-nums text-muted-foreground">
347
- {formatBytes(entry.length)}
348
- </span>
349
- <ChevronRightIcon class="size-3 shrink-0 text-muted-foreground" />
350
- </button>
351
- {/each}
352
- {/if}
353
- </div>
354
- {:else}
355
- <div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
356
- Select a zoom level
357
- </div>
358
- {/if}
359
- </div>
360
- </ResizablePane>
365
+ <ResizableHandle />
361
366
 
362
- <ResizableHandle />
367
+ <!-- Column 2: Entries at zoom -->
368
+ <ResizablePane defaultSize={42} minSize={20}>
369
+ {@render zoomEntryList()}
370
+ </ResizablePane>
363
371
 
364
- <!-- Column 3: Entry details -->
365
- <ResizablePane defaultSize={30} minSize={15}>
366
- <div class="flex h-full flex-col">
372
+ <ResizableHandle />
373
+
374
+ <!-- Column 3: Entry details -->
375
+ <ResizablePane defaultSize={30} minSize={15}>
376
+ <div class="flex h-full flex-col">
377
+ {@render entryDetails()}
378
+ </div>
379
+ </ResizablePane>
380
+ </ResizablePaneGroup>
381
+ {:else}
382
+ <div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
383
+ <!-- Zoom level list: compact fixed height -->
384
+ <div class="max-h-48 shrink-0 border-b border-border">
385
+ {@render zoomLevels()}
386
+ </div>
387
+ <!-- Entries at selected zoom: fixed height -->
388
+ <div class="max-h-56 shrink-0 border-b border-border">
389
+ {@render zoomEntryList()}
390
+ </div>
391
+ <!-- Entry details: grows to fill remaining space -->
392
+ <div class="flex flex-1 flex-col">
367
393
  {@render entryDetails()}
368
394
  </div>
369
- </ResizablePane>
370
- </ResizablePaneGroup>
395
+ </div>
396
+ {/if}
371
397
  </div>
372
398
 
373
399
  <style>
@@ -1,10 +1,10 @@
1
1
  <script lang="ts">
2
2
  import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
3
3
  import XIcon from '@lucide/svelte/icons/x';
4
+ import { formatFileSize, handleLoadError } from '@walkthru-earth/objex-utils';
4
5
  import type { PMTiles } from 'pmtiles';
5
6
  import { onDestroy } from 'svelte';
6
7
  import { t } from '../../../i18n/index.svelte.js';
7
- import { formatFileSize } from '../../../utils/format.js';
8
8
  import type { PmtilesMetadata } from '../../../utils/pmtiles';
9
9
  import {
10
10
  type DecodedTile,
@@ -121,7 +121,7 @@ async function fetchTile() {
121
121
  }
122
122
  }
123
123
  } catch (e) {
124
- error = e instanceof Error ? e.message : String(e);
124
+ error = handleLoadError(e);
125
125
  } finally {
126
126
  loading = false;
127
127
  }
@@ -180,7 +180,7 @@ function formatValue(v: unknown): string {
180
180
  <div class="flex h-full flex-col overflow-hidden">
181
181
  <!-- Navigation bar -->
182
182
  <div
183
- class="flex shrink-0 flex-wrap items-center gap-2 border-b border-zinc-200 px-3 py-2 dark:border-zinc-800"
183
+ class="flex shrink-0 flex-wrap items-center gap-2 border-b border-border px-3 py-2"
184
184
  >
185
185
  <!-- Z/X/Y inputs -->
186
186
  <div class="flex items-center gap-1 text-xs">
@@ -190,7 +190,7 @@ function formatValue(v: unknown): string {
190
190
  bind:value={inputZ}
191
191
  min={0}
192
192
  max={30}
193
- class="w-12 rounded border border-zinc-300 bg-transparent px-1.5 py-0.5 text-center font-mono text-xs dark:border-zinc-700"
193
+ class="w-12 rounded border border-border bg-transparent px-1.5 py-0.5 text-center font-mono text-xs"
194
194
  onkeydown={handleKeydown}
195
195
  />
196
196
  <span class="text-muted-foreground">x</span>
@@ -198,7 +198,7 @@ function formatValue(v: unknown): string {
198
198
  type="number"
199
199
  bind:value={inputX}
200
200
  min={0}
201
- class="w-16 rounded border border-zinc-300 bg-transparent px-1.5 py-0.5 text-center font-mono text-xs dark:border-zinc-700"
201
+ class="w-16 rounded border border-border bg-transparent px-1.5 py-0.5 text-center font-mono text-xs"
202
202
  onkeydown={handleKeydown}
203
203
  />
204
204
  <span class="text-muted-foreground">y</span>
@@ -206,7 +206,7 @@ function formatValue(v: unknown): string {
206
206
  type="number"
207
207
  bind:value={inputY}
208
208
  min={0}
209
- class="w-16 rounded border border-zinc-300 bg-transparent px-1.5 py-0.5 text-center font-mono text-xs dark:border-zinc-700"
209
+ class="w-16 rounded border border-border bg-transparent px-1.5 py-0.5 text-center font-mono text-xs"
210
210
  onkeydown={handleKeydown}
211
211
  />
212
212
  </div>
@@ -253,13 +253,13 @@ function formatValue(v: unknown): string {
253
253
  </div>
254
254
 
255
255
  <!-- Main content -->
256
- <div class="flex min-h-0 flex-1 overflow-hidden">
256
+ <div class="flex min-h-0 flex-1 flex-col overflow-hidden sm:flex-row">
257
257
  {#if loading}
258
258
  <div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
259
259
  Loading tile...
260
260
  </div>
261
261
  {:else if error}
262
- <div class="flex flex-1 items-center justify-center text-xs text-red-400">
262
+ <div class="flex flex-1 items-center justify-center text-xs text-destructive">
263
263
  {error}
264
264
  </div>
265
265
  {:else if tile}
@@ -312,16 +312,16 @@ function formatValue(v: unknown): string {
312
312
 
313
313
  <!-- Feature properties panel -->
314
314
  <div
315
- class="flex w-56 shrink-0 flex-col border-s border-zinc-200 lg:w-64 dark:border-zinc-800"
315
+ class="flex w-full flex-col border-t border-border sm:w-56 sm:shrink-0 sm:border-s sm:border-t-0 lg:w-64"
316
316
  >
317
317
  <div
318
- 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"
319
319
  >
320
320
  {t('pmtiles.featureProperties')}
321
321
  </div>
322
322
  {#if selectedFeature && selectedLayerName !== null}
323
323
  <div class="flex-1 overflow-auto">
324
- <div class="border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
324
+ <div class="border-b border-border px-3 py-2">
325
325
  <div class="flex items-center gap-1.5 text-xs">
326
326
  <span
327
327
  class="inline-block size-2 rounded-sm"
@@ -337,14 +337,14 @@ function formatValue(v: unknown): string {
337
337
  · #{selectedFeatureIdx}
338
338
  </div>
339
339
  </div>
340
- <div class="divide-y divide-zinc-100 dark:divide-zinc-800">
340
+ <div class="divide-y divide-border">
341
341
  {#each Object.entries(selectedFeature.properties) as [key, value]}
342
342
  <div class="px-3 py-1.5">
343
- <div class="text-[10px] font-medium text-zinc-500 dark:text-zinc-400">
343
+ <div class="text-[10px] font-medium text-muted-foreground">
344
344
  {key}
345
345
  </div>
346
346
  <div
347
- class="break-all text-xs text-zinc-700 dark:text-zinc-300"
347
+ class="break-all text-xs text-foreground"
348
348
  title={formatValue(value)}
349
349
  >
350
350
  {formatValue(value)}
@@ -402,10 +402,10 @@ function formatValue(v: unknown): string {
402
402
 
403
403
  <!-- Raster tile info panel -->
404
404
  <div
405
- class="flex w-56 shrink-0 flex-col border-s border-zinc-200 lg:w-64 dark:border-zinc-800"
405
+ class="flex w-full flex-col border-t border-border sm:w-56 sm:shrink-0 sm:border-s sm:border-t-0 lg:w-64"
406
406
  >
407
407
  <div
408
- 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"
408
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
409
409
  >
410
410
  {t('pmtiles.tileInfo')}
411
411
  </div>
@@ -0,0 +1,175 @@
1
+ <script lang="ts">
2
+ import type { DatetimeFacet, FacetState } from '@walkthru-earth/objex-utils';
3
+ import { formatDate } from '@walkthru-earth/objex-utils';
4
+ import { t } from '../../../i18n/index.svelte.js';
5
+ import { RangeSlider } from '../../ui/slider/index.js';
6
+
7
+ /**
8
+ * Datetime range picker that sits above the item strip. Replaces the older
9
+ * preset dropdown ("last 7 days / 30 days / ...") with a continuous slider
10
+ * over the loaded items' min/max datetime, plus two `<input type="date">`
11
+ * fields for exact start / end picking. The histogram of loaded items is
12
+ * drawn behind the slider so the user can see where data is dense.
13
+ *
14
+ * State flows one-way: this component reads `facet` (built from the loaded
15
+ * items) and `state.datetime`, and emits `onChange(next)` with the merged
16
+ * `FacetState`. The parent decides how to apply it (push-down vs client-side).
17
+ *
18
+ * **Bbox scoping**: The parent (`StacMosaicViewer`) builds `facet` from
19
+ * `committedViews`, which is bbox-scoped in `api` and `parquet` modes (those
20
+ * sources push the viewport bbox to the server / SQL). So in viewport modes
21
+ * the histogram always reflects "what's available in the current bbox" and
22
+ * a pan triggers a fresh build via `reloadViewport()`. In `static` mode the
23
+ * histogram is global to the catalog by design (see the parent's `facets`
24
+ * derivation comment for why we do not client-side clip there).
25
+ */
26
+ let {
27
+ facet,
28
+ state,
29
+ onChange
30
+ }: {
31
+ /** DatetimeFacet built from the loaded items, or null when no datetime variance. */
32
+ facet: DatetimeFacet | null;
33
+ state: FacetState;
34
+ onChange: (next: FacetState) => void;
35
+ } = $props();
36
+
37
+ const bounds = $derived(
38
+ facet ? ([Date.parse(facet.min), Date.parse(facet.max)] as [number, number]) : null
39
+ );
40
+
41
+ const sliderValue = $derived.by((): [number, number] | null => {
42
+ if (!bounds) return null;
43
+ const [lo, hi] = bounds;
44
+ const stateLo = state.datetime?.min ? Date.parse(state.datetime.min) : lo;
45
+ const stateHi = state.datetime?.max ? Date.parse(state.datetime.max) : hi;
46
+ return [Number.isFinite(stateLo) ? stateLo : lo, Number.isFinite(stateHi) ? stateHi : hi];
47
+ });
48
+
49
+ function emit(min: string | undefined, max: string | undefined): void {
50
+ onChange({
51
+ ...state,
52
+ datetime: min || max ? { min, max } : undefined
53
+ });
54
+ }
55
+
56
+ function setSlider(next: [number, number]): void {
57
+ if (!bounds) return;
58
+ const lo = next[0] <= bounds[0] ? undefined : new Date(next[0]).toISOString();
59
+ const hi = next[1] >= bounds[1] ? undefined : new Date(next[1]).toISOString();
60
+ emit(lo, hi);
61
+ }
62
+
63
+ /** ISO 8601 → `YYYY-MM-DD` for `<input type="date">` value. */
64
+ function isoToDateInput(iso: string | undefined): string {
65
+ if (!iso) return '';
66
+ const t = Date.parse(iso);
67
+ if (!Number.isFinite(t)) return '';
68
+ return new Date(t).toISOString().slice(0, 10);
69
+ }
70
+
71
+ /** `<input type="date">` value → ISO 8601 (start of UTC day for min, end for max). */
72
+ function dateInputToIso(value: string, kind: 'min' | 'max'): string | undefined {
73
+ if (!value) return undefined;
74
+ const stamp = kind === 'min' ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;
75
+ const t = Date.parse(stamp);
76
+ return Number.isFinite(t) ? new Date(t).toISOString() : undefined;
77
+ }
78
+
79
+ function onMinInput(e: Event): void {
80
+ const v = (e.target as HTMLInputElement).value;
81
+ emit(dateInputToIso(v, 'min'), state.datetime?.max);
82
+ }
83
+
84
+ function onMaxInput(e: Event): void {
85
+ const v = (e.target as HTMLInputElement).value;
86
+ emit(state.datetime?.min, dateInputToIso(v, 'max'));
87
+ }
88
+
89
+ function clearRange(): void {
90
+ emit(undefined, undefined);
91
+ }
92
+
93
+ /** `YYYY-MM-DD` for today (UTC) — used as the max-input default. */
94
+ function todayDateInput(): string {
95
+ return new Date().toISOString().slice(0, 10);
96
+ }
97
+
98
+ // Display defaults when the user has not set a filter yet:
99
+ // - min input falls back to the earliest datetime in the loaded data
100
+ // (`facet.min`), so the input hints at the available range instead of
101
+ // showing `mm / dd / yyyy`.
102
+ // - max input falls back to "today" so the visible window always extends
103
+ // to "now" regardless of whether items in the current viewport are
104
+ // stale. Both fallbacks are display-only — the actual `state.datetime`
105
+ // stays undefined until the user picks a value, so an empty `state`
106
+ // means "no filter" not "filter by today".
107
+ const minInputValue = $derived(
108
+ isoToDateInput(state.datetime?.min) || (facet ? isoToDateInput(facet.min) : '')
109
+ );
110
+ const maxInputValue = $derived(isoToDateInput(state.datetime?.max) || todayDateInput());
111
+ const isActive = $derived(Boolean(state.datetime?.min || state.datetime?.max));
112
+
113
+ function fmtDate(ms: number): string {
114
+ if (!Number.isFinite(ms)) return '-';
115
+ return formatDate(ms);
116
+ }
117
+
118
+ const granularityLabel = $derived.by((): string | null => {
119
+ if (!facet) return null;
120
+ const word = t(`stac.granularity.${facet.granularity}`);
121
+ return t('stac.granularityLabel', { granularity: word });
122
+ });
123
+ </script>
124
+
125
+ <div
126
+ class="pointer-events-auto flex flex-col gap-1.5 rounded-md border border-border bg-card/90 px-3 py-2 text-xs text-card-foreground shadow backdrop-blur-sm"
127
+ >
128
+ <div class="flex flex-wrap items-center justify-between gap-2">
129
+ <span class="font-medium">{t('stac.filterDatetime')}</span>
130
+ <div class="flex flex-wrap items-center gap-1.5">
131
+ <input
132
+ type="date"
133
+ value={minInputValue}
134
+ onchange={onMinInput}
135
+ class="min-h-8 rounded border border-input bg-background px-2 py-1 text-xs tabular-nums sm:min-h-0 sm:px-1.5 sm:py-0.5 sm:text-[11px]"
136
+ aria-label={t('stac.filterDatetime')}
137
+ />
138
+ <span class="text-muted-foreground">&rarr;</span>
139
+ <input
140
+ type="date"
141
+ value={maxInputValue}
142
+ onchange={onMaxInput}
143
+ class="min-h-8 rounded border border-input bg-background px-2 py-1 text-xs tabular-nums sm:min-h-0 sm:px-1.5 sm:py-0.5 sm:text-[11px]"
144
+ aria-label={t('stac.filterDatetime')}
145
+ />
146
+ {#if isActive}
147
+ <button
148
+ type="button"
149
+ class="inline-flex min-h-8 items-center rounded border border-input px-2 py-1 text-xs hover:bg-accent sm:min-h-0 sm:px-1.5 sm:py-0.5 sm:text-[10px]"
150
+ style="touch-action: manipulation;"
151
+ onclick={clearRange}
152
+ >
153
+ {t('stac.resetFilters')}
154
+ </button>
155
+ {/if}
156
+ </div>
157
+ </div>
158
+
159
+ {#if facet && bounds && sliderValue}
160
+ <RangeSlider
161
+ min={bounds[0]}
162
+ max={bounds[1]}
163
+ value={sliderValue}
164
+ step={86_400_000}
165
+ histogram={facet.bins}
166
+ formatLabel={fmtDate}
167
+ onValueCommit={setSlider}
168
+ />
169
+ {#if granularityLabel}
170
+ <div class="text-[10px] text-muted-foreground">{granularityLabel}</div>
171
+ {/if}
172
+ {:else}
173
+ <div class="text-[10px] text-muted-foreground">{t('stac.facetNoneAvailable')}</div>
174
+ {/if}
175
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { DatetimeFacet, FacetState } from '@walkthru-earth/objex-utils';
2
+ type $$ComponentProps = {
3
+ /** DatetimeFacet built from the loaded items, or null when no datetime variance. */
4
+ facet: DatetimeFacet | null;
5
+ state: FacetState;
6
+ onChange: (next: FacetState) => void;
7
+ };
8
+ declare const StacDatetimeBar: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type StacDatetimeBar = ReturnType<typeof StacDatetimeBar>;
10
+ export default StacDatetimeBar;