@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
@@ -0,0 +1,18 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { Tab } from '../../types.js';
4
+
5
+ let { tab, badge, actions }: { tab: Tab; badge?: Snippet; actions?: Snippet } = $props();
6
+ </script>
7
+
8
+ <div
9
+ class="flex items-center gap-1 border-b border-border px-2 py-1.5 sm:gap-2 sm:px-4"
10
+ >
11
+ <span class="max-w-[120px] truncate text-sm font-medium text-foreground sm:max-w-none">
12
+ {tab.name}
13
+ </span>
14
+ {#if badge}{@render badge()}{/if}
15
+ {#if actions}
16
+ <div class="ms-auto flex items-center gap-1">{@render actions()}</div>
17
+ {/if}
18
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { Tab } from '../../types.js';
3
+ type $$ComponentProps = {
4
+ tab: Tab;
5
+ badge?: Snippet;
6
+ actions?: Snippet;
7
+ };
8
+ declare const ViewerHeader: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type ViewerHeader = ReturnType<typeof ViewerHeader>;
10
+ export default ViewerHeader;
@@ -1,17 +1,17 @@
1
1
  <script lang="ts">
2
- import { getViewerKind } from '../../file-icons/index.js';
3
- import { getAdapter } from '../../storage/index.js';
4
- import type { Tab } from '../../types.js';
5
- import { readParquetMetadata } from '../../utils/parquet-metadata.js';
6
2
  import {
7
3
  classifyStac,
8
4
  detectMosaicCapable,
9
5
  detectMultiCogCapable,
6
+ isStacGeoparquetSchema,
7
+ readParquetMetadata,
8
+ STAC_API_PATH_RE,
10
9
  type StacRoutableKind
11
- } from '../../utils/stac.js';
12
- import { isStacGeoparquetSchema } from '../../utils/stac-geoparquet.js';
13
- import { STAC_API_PATH_RE } from '../../utils/storage-url.js';
14
- import { buildHttpsUrlAsync } from '../../utils/url.js';
10
+ } from '@walkthru-earth/objex-utils';
11
+ import { getViewerKind } from '../../file-icons/index.js';
12
+ import { getAdapter } from '../../storage/index.js';
13
+ import type { Tab } from '../../types.js';
14
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
15
15
  import CodeViewer from './CodeViewer.svelte';
16
16
  import ImageViewer from './ImageViewer.svelte';
17
17
  import MediaViewer from './MediaViewer.svelte';
@@ -164,6 +164,14 @@ function pickMapKind(classified: StacRoutableKind): 'mosaic' | 'multicog' | null
164
164
 
165
165
  {#if stacRoute.kind === 'stac' && viewerKind === 'table'}
166
166
  <StacTabViewer {tab} mapKind={stacRoute.mapKind} classified={stacRoute.classified} />
167
+ {:else if stacRoute.kind === 'pending' && (viewerKind === 'table' || viewerKind === 'code' || viewerKind === 'raw')}
168
+ <!-- STAC detection (sniff parquet schema or peek 256KB JSON) is in flight.
169
+ Mounting TableViewer / CodeViewer here would let them read the URL hash,
170
+ pick a default viewMode, and potentially write back over an explicit
171
+ hash that StacTabViewer would otherwise own (e.g. `#map` on a STAC
172
+ collection JSON). The pending window is short — render an empty pane
173
+ until detection resolves and the right viewer takes over. -->
174
+ <div class="h-full"></div>
167
175
  {:else if viewerKind === 'table'}
168
176
  <TableViewer {tab} />
169
177
  {:else if viewerKind === 'image'}
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import { Loader } from '@lucide/svelte';
3
+ import { t } from '../../i18n/index.svelte.js';
4
+
5
+ let { kind, message }: { kind: 'loading' | 'error' | 'empty'; message?: string } = $props();
6
+ </script>
7
+
8
+ <div class="flex h-full items-center justify-center p-4">
9
+ {#if kind === 'loading'}
10
+ <div class="text-muted-foreground flex items-center gap-2 text-sm">
11
+ <Loader class="size-4 animate-spin" />
12
+ <span>{message ?? t('common.loading')}</span>
13
+ </div>
14
+ {:else if kind === 'error'}
15
+ <p class="text-destructive text-sm">{message ?? t('common.error')}</p>
16
+ {:else}
17
+ <p class="text-muted-foreground text-sm">{message}</p>
18
+ {/if}
19
+ </div>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ kind: 'loading' | 'error' | 'empty';
3
+ message?: string;
4
+ };
5
+ declare const ViewerStatus: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ViewerStatus = ReturnType<typeof ViewerStatus>;
7
+ export default ViewerStatus;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { MapboxOverlay } from '@deck.gl/mapbox';
3
+ import { handleLoadError } from '@walkthru-earth/objex-utils';
3
4
  import type maplibregl from 'maplibre-gl';
4
5
  import maplibreModule from 'maplibre-gl';
5
6
  import { onDestroy, untrack } from 'svelte';
@@ -7,7 +8,7 @@ import { t } from '../../i18n/index.svelte.js';
7
8
  import { tabResources } from '../../stores/tab-resources.svelte.js';
8
9
  import type { Tab } from '../../types.js';
9
10
  import { createEpsgResolver } from '../../utils/cog.js';
10
- import { buildHttpsUrlAsync } from '../../utils/url.js';
11
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
11
12
  import {
12
13
  detectGeoZarr,
13
14
  ensureCodecsRegistered,
@@ -17,6 +18,7 @@ import {
17
18
  type ZarrHierarchy,
18
19
  type ZarrNode
19
20
  } from '../../utils/zarr.js';
21
+ import { Slider } from '../ui/slider/index.js';
20
22
  import MapContainer from './map/MapContainer.svelte';
21
23
 
22
24
  /** Enriched selector dimension with coordinate metadata. */
@@ -490,7 +492,7 @@ async function addZarrLayer(map: maplibregl.Map) {
490
492
  zarrLayer = new ZarrLayer(opts);
491
493
  map.addLayer(zarrLayer);
492
494
  } catch (err) {
493
- error = err instanceof Error ? err.message : String(err);
495
+ error = handleLoadError(err);
494
496
  loading = false;
495
497
  }
496
498
  }
@@ -526,7 +528,7 @@ async function tryAddGeoZarrLayer(
526
528
  const zarrInfoSnapshot = $state.snapshot(geoZarrInfo) as GeoZarrInfo;
527
529
  const layer = new ZarrLayer({
528
530
  id: `geozarr-${tab.id}`,
529
- source: group,
531
+ node: group,
530
532
  variable: zarrInfoSnapshot.variantPath || undefined,
531
533
  selection: {},
532
534
  epsgResolver: dsZarrEpsg,
@@ -598,7 +600,7 @@ async function updateSelector() {
598
600
  try {
599
601
  await zarrLayer.setSelector(buildSelector());
600
602
  } catch (err) {
601
- error = err instanceof Error ? err.message : String(err);
603
+ error = handleLoadError(err);
602
604
  }
603
605
  }
604
606
 
@@ -640,12 +642,12 @@ onDestroy(cleanup);
640
642
  <div class="flex h-full w-full flex-col overflow-hidden">
641
643
  <!-- Controls bar -->
642
644
  <div
643
- class="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-zinc-200 px-3 py-1.5 dark:border-zinc-800"
645
+ class="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-border px-3 py-1.5"
644
646
  >
645
- <label class="flex items-center gap-1 text-xs text-zinc-400">
647
+ <label class="flex items-center gap-1 text-xs text-muted-foreground">
646
648
  {t('map.variable')}
647
649
  <select
648
- class="rounded border border-zinc-300 bg-white px-1.5 py-0.5 text-xs text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300"
650
+ class="rounded border border-border bg-background px-1.5 py-0.5 text-xs text-foreground"
649
651
  bind:value={selectedVar}
650
652
  onchange={changeVariable}
651
653
  >
@@ -657,24 +659,25 @@ onDestroy(cleanup);
657
659
 
658
660
  {#each selectorDims as dim}
659
661
  <label
660
- class="flex shrink-0 items-center gap-1.5 rounded border border-zinc-200 px-2 py-0.5 text-xs text-zinc-400 dark:border-zinc-700"
662
+ class="flex shrink-0 items-center gap-1.5 rounded border border-border px-2 py-0.5 text-xs text-muted-foreground"
661
663
  title={dimLabel(dim)}
662
664
  >
663
- <span class="shrink-0 font-medium text-zinc-500 dark:text-zinc-400">{dim.name}</span>
664
- <input
665
- type="range"
666
- min="0"
665
+ <span class="shrink-0 font-medium text-muted-foreground">{dim.name}</span>
666
+ <Slider
667
+ type="single"
668
+ min={0}
667
669
  max={dim.size - 1}
670
+ step={1}
668
671
  value={selectorValues[dim.name] ?? 0}
669
- oninput={(e) => {
670
- selectorValues[dim.name] = +e.currentTarget.value;
672
+ onValueChange={(v) => {
673
+ selectorValues[dim.name] = v as number;
671
674
  }}
672
- onchange={updateSelector}
673
- class="h-1 w-16"
675
+ onValueCommit={() => updateSelector()}
676
+ class="w-20"
674
677
  />
675
678
  {#if dim.isDatetime && dim.minDate && dim.maxDate}
676
679
  {@const dateVal = indexToDateStr(selectorValues[dim.name] ?? 0, dim)}
677
- <span class="shrink-0 tabular-nums text-zinc-500">
680
+ <span class="shrink-0 tabular-nums text-muted-foreground">
678
681
  {dateVal ? (dim.subDaily ? dateVal.replace('T', ' ') : dateVal) : (selectorValues[dim.name] ?? 0)}
679
682
  </span>
680
683
  <input
@@ -689,10 +692,10 @@ onDestroy(cleanup);
689
692
  updateSelector();
690
693
  }
691
694
  }}
692
- class="h-5 rounded border border-zinc-300 bg-white px-1 text-[10px] text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-400"
695
+ class="h-5 rounded border border-border bg-background px-1 text-[10px] text-muted-foreground"
693
696
  />
694
697
  {:else}
695
- <span class="shrink-0 tabular-nums text-zinc-500">{selectorValues[dim.name] ?? 0}<span class="text-zinc-500/60">/{dim.size - 1}</span></span>
698
+ <span class="shrink-0 tabular-nums text-muted-foreground">{selectorValues[dim.name] ?? 0}<span class="text-muted-foreground/60">/{dim.size - 1}</span></span>
696
699
  {#if dim.dtype}
697
700
  <span class="shrink-0 text-[10px] text-zinc-400/70">{dim.dtype}</span>
698
701
  {/if}
@@ -701,7 +704,7 @@ onDestroy(cleanup);
701
704
  {/each}
702
705
 
703
706
  {#if selectedMeta?.shape}
704
- <span class="ms-auto text-xs text-zinc-400">
707
+ <span class="ms-auto text-xs text-muted-foreground">
705
708
  {selectedMeta.dtype} [{selectedMeta.shape.join(', ')}]
706
709
  </span>
707
710
  {/if}
@@ -711,7 +714,7 @@ onDestroy(cleanup);
711
714
  <div class="relative min-h-0 flex-1">
712
715
  {#if error && !loading}
713
716
  <div class="flex h-full items-center justify-center">
714
- <p class="max-w-md text-center text-sm text-red-400">{error}</p>
717
+ <p class="max-w-md text-center text-sm text-destructive">{error}</p>
715
718
  </div>
716
719
  {:else}
717
720
  <MapContainer {onMapReady} bounds={[-130, 20, -60, 55]} />
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import { untrack } from 'svelte';
2
+ import { handleLoadError } from '@walkthru-earth/objex-utils';
3
+ import { onDestroy, untrack } from 'svelte';
3
4
  import { Badge } from '../ui/badge/index.js';
4
5
  import { Button } from '../ui/button/index.js';
5
6
  import {
@@ -8,9 +9,10 @@ import {
8
9
  ResizablePaneGroup
9
10
  } from '../ui/resizable/index.js';
10
11
  import { t } from '../../i18n/index.svelte.js';
12
+ import { tabResources } from '../../stores/tab-resources.svelte.js';
11
13
  import type { Tab } from '../../types';
12
- import { buildHttpsUrlAsync } from '../../utils/url.js';
13
- import { getUrlView, updateUrlView } from '../../utils/url-state.js';
14
+ import { buildHttpsUrlAsync } from '../../utils/signed-url.js';
15
+ import { pickViewMode, updateUrlView } from '../../utils/url-state.js';
14
16
  import {
15
17
  computeChunkCount,
16
18
  computeChunkSize,
@@ -25,13 +27,16 @@ import {
25
27
  type ZarrHierarchy,
26
28
  type ZarrNode
27
29
  } from '../../utils/zarr.js';
30
+ import { useIsWide } from '../../utils/media-query.svelte.js';
28
31
 
29
32
  let { tab }: { tab: Tab } = $props();
30
33
 
34
+ const isWide = useIsWide();
35
+
31
36
  let loading = $state(true);
32
37
  let error = $state<string | null>(null);
33
- const urlView = getUrlView();
34
- let viewMode = $state<'inspect' | 'map'>(urlView === 'map' ? 'map' : 'inspect');
38
+ type ZarrViewMode = 'inspect' | 'map';
39
+ let viewMode = $state<ZarrViewMode>(pickViewMode<ZarrViewMode>(['inspect', 'map'], 'inspect'));
35
40
 
36
41
  let hierarchy = $state.raw<ZarrHierarchy | null>(null);
37
42
  let selectedNode = $state<ZarrNode | null>(null);
@@ -110,6 +115,19 @@ $effect(() => {
110
115
  });
111
116
  });
112
117
 
118
+ function cleanup() {
119
+ hierarchy = null;
120
+ selectedNode = null;
121
+ expanded = new Set();
122
+ }
123
+
124
+ $effect(() => {
125
+ const id = tab.id;
126
+ const unregister = tabResources.register(id, cleanup);
127
+ return unregister;
128
+ });
129
+ onDestroy(cleanup);
130
+
113
131
  function setViewMode(mode: 'inspect' | 'map') {
114
132
  viewMode = mode;
115
133
  updateUrlView(viewMode);
@@ -133,7 +151,7 @@ async function loadHierarchy() {
133
151
  expanded = new Set(['/']);
134
152
  }
135
153
  } catch (err) {
136
- error = err instanceof Error ? err.message : String(err);
154
+ error = handleLoadError(err);
137
155
  } finally {
138
156
  loading = false;
139
157
  updateUrlView(viewMode);
@@ -182,7 +200,7 @@ function selectStoreAttrs() {
182
200
  {#if hasChildren}
183
201
  <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
184
202
  <span
185
- class="flex size-4 shrink-0 items-center justify-center rounded hover:bg-zinc-200 dark:hover:bg-zinc-700"
203
+ class="flex size-4 shrink-0 items-center justify-center rounded hover:bg-accent"
186
204
  role="button"
187
205
  tabindex="-1"
188
206
  aria-label={isExpanded ? 'Collapse' : 'Expand'}
@@ -192,7 +210,7 @@ function selectStoreAttrs() {
192
210
  }}
193
211
  >
194
212
  <svg
195
- class="size-3 text-zinc-400 transition-transform"
213
+ class="size-3 text-muted-foreground transition-transform"
196
214
  class:rotate-90={isExpanded}
197
215
  viewBox="0 0 16 16"
198
216
  fill="currentColor"
@@ -229,10 +247,8 @@ function selectStoreAttrs() {
229
247
  <span
230
248
  class="truncate"
231
249
  class:font-medium={node.kind === 'array'}
232
- class:text-zinc-700={node.kind === 'array'}
233
- class:dark:text-zinc-300={node.kind === 'array'}
234
- class:text-zinc-600={node.kind === 'group'}
235
- class:dark:text-zinc-400={node.kind === 'group'}
250
+ class:text-foreground={node.kind === 'array'}
251
+ class:text-muted-foreground={node.kind === 'group'}
236
252
  >
237
253
  {node.path === '/' ? '/ (root)' : node.name}
238
254
  </span>
@@ -259,18 +275,18 @@ function selectStoreAttrs() {
259
275
  {#snippet nodeDetails()}
260
276
  {#if showingStoreAttrs && hierarchy}
261
277
  <div
262
- 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"
278
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
263
279
  >
264
280
  {t('zarr.storeAttributes')}
265
281
  </div>
266
282
  <div class="flex-1 overflow-auto p-3">
267
283
  <div
268
- class="rounded border border-zinc-200 bg-zinc-100 p-2 text-xs dark:border-zinc-700 dark:bg-zinc-800"
284
+ class="rounded border border-border bg-muted p-2 text-xs"
269
285
  >
270
286
  {#each Object.entries(hierarchy.storeAttrs) as [key, value]}
271
287
  <div class="flex gap-2 py-0.5">
272
288
  <span class="shrink-0 font-medium text-muted-foreground">{key}:</span>
273
- <span class="break-all text-zinc-700 dark:text-zinc-300">
289
+ <span class="break-all text-foreground">
274
290
  {typeof value === 'string' ? value : JSON.stringify(value)}
275
291
  </span>
276
292
  </div>
@@ -279,7 +295,7 @@ function selectStoreAttrs() {
279
295
  </div>
280
296
  {:else if selectedNode}
281
297
  <div
282
- 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"
298
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
283
299
  >
284
300
  {selectedNode.path}
285
301
  </div>
@@ -389,12 +405,12 @@ function selectStoreAttrs() {
389
405
  <dt class="text-muted-foreground">{t('zarr.attributes')}</dt>
390
406
  <dd>
391
407
  <div
392
- class="mt-1 rounded border border-zinc-200 bg-zinc-100 p-2 dark:border-zinc-700 dark:bg-zinc-800"
408
+ class="mt-1 rounded border border-border bg-muted p-2"
393
409
  >
394
410
  {#each Object.entries(selectedNode.attributes) as [key, value]}
395
411
  <div class="flex gap-2 py-0.5">
396
412
  <span class="shrink-0 font-medium text-muted-foreground">{key}:</span>
397
- <span class="break-all text-zinc-700 dark:text-zinc-300">
413
+ <span class="break-all text-foreground">
398
414
  {typeof value === 'string' ? value : JSON.stringify(value)}
399
415
  </span>
400
416
  </div>
@@ -414,10 +430,10 @@ function selectStoreAttrs() {
414
430
 
415
431
  <div class="flex h-full flex-col">
416
432
  <!-- Header bar -->
417
- <div class="shrink-0 border-b border-zinc-200 px-3 py-2 sm:px-4 dark:border-zinc-800">
433
+ <div class="shrink-0 border-b border-border px-3 py-2 sm:px-4">
418
434
  <div class="flex items-center gap-1.5 sm:gap-2">
419
435
  <span
420
- class="max-w-[140px] truncate text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300"
436
+ class="max-w-[140px] truncate text-sm font-medium text-foreground sm:max-w-none"
421
437
  >{tab.name}</span
422
438
  >
423
439
  <Badge
@@ -459,11 +475,11 @@ function selectStoreAttrs() {
459
475
  <!-- Content -->
460
476
  {#if loading}
461
477
  <div class="flex flex-1 items-center justify-center">
462
- <p class="text-sm text-zinc-400">{t('zarr.loading')}</p>
478
+ <p class="text-sm text-muted-foreground">{t('zarr.loading')}</p>
463
479
  </div>
464
480
  {:else if error}
465
481
  <div class="flex flex-1 items-center justify-center">
466
- <p class="max-w-md text-center text-sm text-red-400">{error}</p>
482
+ <p class="max-w-md text-center text-sm text-destructive">{error}</p>
467
483
  </div>
468
484
  {:else if viewMode === 'map' && hasMapVars}
469
485
  {#key viewMode}
@@ -480,56 +496,73 @@ function selectStoreAttrs() {
480
496
  {/key}
481
497
  {:else if hierarchy}
482
498
  <!-- Inspect mode (tree + detail panel) -->
483
- <ResizablePaneGroup direction="horizontal" class="min-h-0 flex-1">
484
- <!-- Left: Tree view -->
485
- <ResizablePane defaultSize={40} minSize={20}>
486
- <div class="flex h-full flex-col">
487
- <div
488
- 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"
499
+ {#snippet zarrTree()}
500
+ <div class="flex h-full flex-col">
501
+ <div
502
+ class="shrink-0 border-b border-border px-3 py-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground"
503
+ >
504
+ {t('zarr.contents')}
505
+ <span class="ms-1 normal-case tracking-normal"
506
+ >({hierarchy!.totalNodes})</span
489
507
  >
490
- {t('zarr.contents')}
491
- <span class="ms-1 normal-case tracking-normal"
492
- >({hierarchy.totalNodes})</span
508
+ </div>
509
+ <div class="flex-1 overflow-auto">
510
+ {#if hasStoreAttrs}
511
+ <button
512
+ class="flex w-full items-center gap-2 border-b border-zinc-100 px-3 py-1 text-xs hover:bg-zinc-100 dark:border-zinc-800/50 dark:hover:bg-zinc-800/50"
513
+ class:bg-blue-50={showingStoreAttrs}
514
+ class:dark:bg-blue-950={showingStoreAttrs}
515
+ onclick={selectStoreAttrs}
493
516
  >
494
- </div>
495
- <div class="flex-1 overflow-auto">
496
- {#if hasStoreAttrs}
497
- <button
498
- class="flex w-full items-center gap-2 border-b border-zinc-100 px-3 py-1 text-xs hover:bg-zinc-100 dark:border-zinc-800/50 dark:hover:bg-zinc-800/50"
499
- class:bg-blue-50={showingStoreAttrs}
500
- class:dark:bg-blue-950={showingStoreAttrs}
501
- onclick={selectStoreAttrs}
517
+ <span class="size-4 shrink-0"></span>
518
+ <svg
519
+ class="size-3.5 shrink-0 text-muted-foreground"
520
+ viewBox="0 0 16 16"
521
+ fill="currentColor"
502
522
  >
503
- <span class="size-4 shrink-0"></span>
504
- <svg
505
- class="size-3.5 shrink-0 text-zinc-400"
506
- viewBox="0 0 16 16"
507
- fill="currentColor"
508
- >
509
- <path
510
- fill-rule="evenodd"
511
- d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H3a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-1.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
512
- clip-rule="evenodd"
513
- />
514
- </svg>
515
- <span class="truncate font-medium text-muted-foreground">
516
- {t('zarr.storeAttributes')}
517
- </span>
518
- </button>
519
- {/if}
520
- {@render treeNode(hierarchy.root, 0)}
521
- </div>
523
+ <path
524
+ fill-rule="evenodd"
525
+ d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H3a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-1.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
526
+ clip-rule="evenodd"
527
+ />
528
+ </svg>
529
+ <span class="truncate font-medium text-muted-foreground">
530
+ {t('zarr.storeAttributes')}
531
+ </span>
532
+ </button>
533
+ {/if}
534
+ {@render treeNode(hierarchy!.root, 0)}
522
535
  </div>
523
- </ResizablePane>
536
+ </div>
537
+ {/snippet}
538
+
539
+ {#if isWide.value}
540
+ <ResizablePaneGroup direction="horizontal" class="min-h-0 flex-1">
541
+ <!-- Left: Tree view -->
542
+ <ResizablePane defaultSize={40} minSize={20}>
543
+ {@render zarrTree()}
544
+ </ResizablePane>
524
545
 
525
- <ResizableHandle />
546
+ <ResizableHandle />
526
547
 
527
- <!-- Right: Detail panel -->
528
- <ResizablePane defaultSize={60} minSize={30}>
529
- <div class="flex h-full flex-col">
548
+ <!-- Right: Detail panel -->
549
+ <ResizablePane defaultSize={60} minSize={30}>
550
+ <div class="flex h-full flex-col">
551
+ {@render nodeDetails()}
552
+ </div>
553
+ </ResizablePane>
554
+ </ResizablePaneGroup>
555
+ {:else}
556
+ <div class="flex min-h-0 flex-1 flex-col overflow-y-auto">
557
+ <!-- Tree pane: fixed height so it doesn't crowd the detail section -->
558
+ <div class="max-h-64 shrink-0 border-b border-border">
559
+ {@render zarrTree()}
560
+ </div>
561
+ <!-- Detail panel: grows to fill remaining space -->
562
+ <div class="flex flex-1 flex-col">
530
563
  {@render nodeDetails()}
531
564
  </div>
532
- </ResizablePane>
533
- </ResizablePaneGroup>
565
+ </div>
566
+ {/if}
534
567
  {/if}
535
568
  </div>
@@ -0,0 +1,83 @@
1
+ <script lang="ts">
2
+ import type { ChannelRef, CogAsset } from '@walkthru-earth/objex-utils';
3
+ import { t } from '../../../i18n/index.svelte.js';
4
+
5
+ type Props = {
6
+ channel: 'r' | 'g' | 'b' | 'a';
7
+ label: string;
8
+ colorClass: string;
9
+ assets: CogAsset[];
10
+ value: ChannelRef;
11
+ onChange: (next: ChannelRef) => void;
12
+ allowNone?: boolean;
13
+ };
14
+
15
+ let { channel, label, colorClass, assets, value, onChange, allowNone = false }: Props = $props();
16
+
17
+ const assetByKey = $derived(new Map(assets.map((a) => [a.key, a])));
18
+ const currentAsset = $derived(assetByKey.get(value.assetKey) ?? null);
19
+ const bandCount = $derived(currentAsset?.bandCount ?? 1);
20
+ const bandIndices = $derived(Array.from({ length: bandCount }, (_, i) => i));
21
+
22
+ function assetLabel(a: CogAsset): string {
23
+ const cn = a.eoCommon[0];
24
+ const base = cn ? `${a.key} (${cn})` : a.key;
25
+ return a.bandCount > 1 ? `${base} · ${a.bandCount} bands` : base;
26
+ }
27
+
28
+ function bandLabel(i: number, asset: CogAsset | null): string {
29
+ if (!asset) return `${t('cog.band')} ${i + 1}`;
30
+ const cn = asset.eoCommon[i];
31
+ return cn ? `${t('cog.band')} ${i + 1} (${cn})` : `${t('cog.band')} ${i + 1}`;
32
+ }
33
+
34
+ function setAsset(key: string): void {
35
+ if (channel === 'a' && allowNone && key === '') {
36
+ onChange({ assetKey: '', bandIndex: 0 });
37
+ return;
38
+ }
39
+ const target = assetByKey.get(key);
40
+ const maxIdx = Math.max(0, (target?.bandCount ?? 1) - 1);
41
+ const nextBand = Math.min(value.bandIndex, maxIdx);
42
+ onChange({ assetKey: key, bandIndex: nextBand });
43
+ }
44
+
45
+ function setBand(idx: number): void {
46
+ onChange({ assetKey: value.assetKey, bandIndex: idx });
47
+ }
48
+ </script>
49
+
50
+ <div class="flex items-start gap-2">
51
+ <span class="mt-1 w-3 shrink-0 font-bold {colorClass}">{label}</span>
52
+ <div class="flex min-w-0 flex-1 flex-col gap-1 sm:flex-row sm:items-center">
53
+ <select
54
+ class="min-w-0 flex-1 truncate rounded border border-border bg-background px-1.5 py-0.5 text-xs"
55
+ aria-label={`${label} ${t('cog.asset')}`}
56
+ value={value.assetKey}
57
+ onchange={(e) => setAsset((e.target as HTMLSelectElement).value)}
58
+ >
59
+ {#if allowNone}
60
+ <option value="">{t('map.multiCogChannelNone')}</option>
61
+ {/if}
62
+ {#each assets as a (a.key)}
63
+ <option value={a.key}>{assetLabel(a)}</option>
64
+ {/each}
65
+ </select>
66
+ {#if currentAsset && bandCount > 1}
67
+ <select
68
+ class="min-w-0 flex-1 truncate rounded border border-border bg-background px-1.5 py-0.5 text-xs sm:flex-[0_1_auto] sm:min-w-24"
69
+ aria-label={`${label} ${t('cog.band')}`}
70
+ value={value.bandIndex}
71
+ onchange={(e) => setBand(Number((e.target as HTMLSelectElement).value))}
72
+ >
73
+ {#each bandIndices as i (i)}
74
+ <option value={i}>{bandLabel(i, currentAsset)}</option>
75
+ {/each}
76
+ </select>
77
+ {:else if currentAsset}
78
+ <span class="min-w-0 truncate px-1.5 py-0.5 text-[10px] text-muted-foreground sm:min-w-24">
79
+ {t('cog.band')} 1
80
+ </span>
81
+ {/if}
82
+ </div>
83
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { ChannelRef, CogAsset } from '@walkthru-earth/objex-utils';
2
+ type Props = {
3
+ channel: 'r' | 'g' | 'b' | 'a';
4
+ label: string;
5
+ colorClass: string;
6
+ assets: CogAsset[];
7
+ value: ChannelRef;
8
+ onChange: (next: ChannelRef) => void;
9
+ allowNone?: boolean;
10
+ };
11
+ declare const ChannelPicker: import("svelte").Component<Props, {}, "">;
12
+ type ChannelPicker = ReturnType<typeof ChannelPicker>;
13
+ export default ChannelPicker;