@walkthru-earth/objex 1.3.1 → 1.4.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 (184) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +20 -12
  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 +1 -1
  6. package/dist/components/layout/SettingsSheet.svelte +237 -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 +1 -1
  11. package/dist/components/layout/TabBar.svelte +2 -2
  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 +2 -2
  25. package/dist/components/viewers/CodeViewer.svelte +31 -22
  26. package/dist/components/viewers/CogControls.svelte +338 -184
  27. package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
  28. package/dist/components/viewers/CogViewer.svelte +263 -112
  29. package/dist/components/viewers/CopcViewer.svelte +1 -1
  30. package/dist/components/viewers/FlatGeobufViewer.svelte +1 -1
  31. package/dist/components/viewers/GeoParquetMapViewer.svelte +6 -6
  32. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
  33. package/dist/components/viewers/ImageViewer.svelte +2 -2
  34. package/dist/components/viewers/MarkdownViewer.svelte +12 -9
  35. package/dist/components/viewers/MediaViewer.svelte +2 -2
  36. package/dist/components/viewers/ModelViewer.svelte +1 -1
  37. package/dist/components/viewers/MultiCogViewer.svelte +467 -102
  38. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
  39. package/dist/components/viewers/NotebookViewer.svelte +6 -3
  40. package/dist/components/viewers/PdfViewer.svelte +2 -2
  41. package/dist/components/viewers/PmtilesViewer.svelte +3 -6
  42. package/dist/components/viewers/RawViewer.svelte +6 -3
  43. package/dist/components/viewers/StacMapViewer.svelte +1 -1
  44. package/dist/components/viewers/StacMosaicViewer.svelte +1760 -408
  45. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
  46. package/dist/components/viewers/StacTabViewer.svelte +24 -13
  47. package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
  48. package/dist/components/viewers/TableGrid.svelte +4 -4
  49. package/dist/components/viewers/TableStatusBar.svelte +1 -1
  50. package/dist/components/viewers/TableToolbar.svelte +1 -1
  51. package/dist/components/viewers/TableViewer.svelte +25 -17
  52. package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
  53. package/dist/components/viewers/ViewerRouter.svelte +16 -8
  54. package/dist/components/viewers/ZarrMapViewer.svelte +11 -9
  55. package/dist/components/viewers/ZarrViewer.svelte +4 -4
  56. package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
  57. package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
  58. package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
  59. package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
  60. package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
  61. package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
  62. package/dist/components/viewers/map/AttributeTable.svelte +1 -1
  63. package/dist/components/viewers/map/MapContainer.svelte +37 -11
  64. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +1 -1
  65. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +1 -1
  66. package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
  67. package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
  68. package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
  69. package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
  70. package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
  71. package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
  72. package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
  73. package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
  74. package/dist/file-icons/index.d.ts +1 -1
  75. package/dist/file-icons/index.js +1 -1
  76. package/dist/i18n/ar.js +110 -2
  77. package/dist/i18n/en.js +110 -2
  78. package/dist/index.d.ts +2 -28
  79. package/dist/index.js +7 -23
  80. package/dist/query/engine.d.ts +10 -0
  81. package/dist/query/source.js +1 -1
  82. package/dist/query/stac-source-factory.d.ts +65 -0
  83. package/dist/query/stac-source-factory.js +77 -0
  84. package/dist/query/stac-source-parquet.d.ts +135 -0
  85. package/dist/query/stac-source-parquet.js +465 -0
  86. package/dist/query/wasm.d.ts +8 -0
  87. package/dist/query/wasm.js +304 -2
  88. package/dist/storage/presign.js +1 -1
  89. package/dist/storage/providers.js +5 -5
  90. package/dist/stores/config.svelte.d.ts +15 -0
  91. package/dist/stores/config.svelte.js +46 -0
  92. package/dist/stores/connections.svelte.d.ts +2 -2
  93. package/dist/stores/connections.svelte.js +1 -2
  94. package/dist/stores/files.svelte.d.ts +1 -1
  95. package/dist/stores/files.svelte.js +1 -1
  96. package/dist/stores/query-history.svelte.js +1 -1
  97. package/dist/stores/settings.svelte.d.ts +16 -1
  98. package/dist/stores/settings.svelte.js +104 -48
  99. package/dist/stores/tabs.svelte.d.ts +3 -0
  100. package/dist/stores/tabs.svelte.js +17 -0
  101. package/dist/utils/cog-histogram.d.ts +121 -0
  102. package/dist/utils/cog-histogram.js +424 -0
  103. package/dist/utils/cog.d.ts +177 -20
  104. package/dist/utils/cog.js +361 -76
  105. package/dist/utils/colormap-sprite.d.ts +0 -9
  106. package/dist/utils/colormap-sprite.js +0 -21
  107. package/dist/utils/deck.d.ts +16 -12
  108. package/dist/utils/deck.js +10 -4
  109. package/dist/utils/pmtiles-tile.js +2 -2
  110. package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
  111. package/dist/utils/{url.js → signed-url.js} +32 -10
  112. package/dist/utils/url-state.d.ts +36 -0
  113. package/dist/utils/url-state.js +72 -2
  114. package/dist/utils/zarr-tab.d.ts +1 -2
  115. package/dist/utils/zarr-tab.js +1 -2
  116. package/dist/utils/zarr.d.ts +0 -17
  117. package/dist/utils/zarr.js +1 -45
  118. package/package.json +55 -84
  119. package/dist/components/browser/Breadcrumb.svelte +0 -50
  120. package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
  121. package/dist/components/browser/CreateFolderDialog.svelte +0 -98
  122. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
  123. package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
  124. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
  125. package/dist/components/browser/DropZone.svelte +0 -83
  126. package/dist/components/browser/DropZone.svelte.d.ts +0 -7
  127. package/dist/components/browser/FileBrowser.svelte +0 -252
  128. package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
  129. package/dist/components/browser/FileRow.svelte +0 -117
  130. package/dist/components/browser/FileRow.svelte.d.ts +0 -9
  131. package/dist/components/browser/RenameDialog.svelte +0 -101
  132. package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
  133. package/dist/components/browser/SearchBar.svelte +0 -40
  134. package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
  135. package/dist/components/browser/UploadButton.svelte +0 -65
  136. package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
  137. package/dist/query/stac-geoparquet.d.ts +0 -31
  138. package/dist/query/stac-geoparquet.js +0 -136
  139. package/dist/utils/clipboard.d.ts +0 -13
  140. package/dist/utils/clipboard.js +0 -38
  141. package/dist/utils/cloud-url.d.ts +0 -27
  142. package/dist/utils/cloud-url.js +0 -61
  143. package/dist/utils/cog-pure.d.ts +0 -25
  144. package/dist/utils/cog-pure.js +0 -35
  145. package/dist/utils/column-types.d.ts +0 -5
  146. package/dist/utils/column-types.js +0 -137
  147. package/dist/utils/connection-identity.d.ts +0 -51
  148. package/dist/utils/connection-identity.js +0 -97
  149. package/dist/utils/error.d.ts +0 -8
  150. package/dist/utils/error.js +0 -12
  151. package/dist/utils/evidence-context.d.ts +0 -22
  152. package/dist/utils/evidence-context.js +0 -56
  153. package/dist/utils/export.d.ts +0 -22
  154. package/dist/utils/export.js +0 -76
  155. package/dist/utils/file-sort.d.ts +0 -20
  156. package/dist/utils/file-sort.js +0 -41
  157. package/dist/utils/format.d.ts +0 -24
  158. package/dist/utils/format.js +0 -78
  159. package/dist/utils/geoarrow.d.ts +0 -32
  160. package/dist/utils/geoarrow.js +0 -672
  161. package/dist/utils/geometry-type.d.ts +0 -52
  162. package/dist/utils/geometry-type.js +0 -76
  163. package/dist/utils/hex.d.ts +0 -10
  164. package/dist/utils/hex.js +0 -27
  165. package/dist/utils/host-detection.d.ts +0 -23
  166. package/dist/utils/host-detection.js +0 -95
  167. package/dist/utils/local-storage.d.ts +0 -16
  168. package/dist/utils/local-storage.js +0 -37
  169. package/dist/utils/markdown-sql.d.ts +0 -30
  170. package/dist/utils/markdown-sql.js +0 -72
  171. package/dist/utils/notebook.d.ts +0 -59
  172. package/dist/utils/notebook.js +0 -211
  173. package/dist/utils/parquet-metadata.d.ts +0 -64
  174. package/dist/utils/parquet-metadata.js +0 -262
  175. package/dist/utils/stac-geoparquet.d.ts +0 -90
  176. package/dist/utils/stac-geoparquet.js +0 -223
  177. package/dist/utils/stac-hydrate.d.ts +0 -38
  178. package/dist/utils/stac-hydrate.js +0 -243
  179. package/dist/utils/stac.d.ts +0 -136
  180. package/dist/utils/stac.js +0 -176
  181. package/dist/utils/storage-url.d.ts +0 -90
  182. package/dist/utils/storage-url.js +0 -568
  183. package/dist/utils/wkb.d.ts +0 -43
  184. package/dist/utils/wkb.js +0 -359
@@ -0,0 +1,243 @@
1
+ <script lang="ts">
2
+ import {
3
+ type FacetSet,
4
+ type FacetState,
5
+ formatDate,
6
+ hasActiveFilters
7
+ } from '@walkthru-earth/objex-utils';
8
+ import type { Snippet } from 'svelte';
9
+ import { t } from '../../../i18n/index.svelte.js';
10
+ import { RangeSlider } from '../../ui/slider/index.js';
11
+
12
+ /**
13
+ * Auto-faceted filter panel. Reads a `FacetSet` derived from the loaded
14
+ * item views and renders only the controls that have variance for *this*
15
+ * dataset. Currently surfaces:
16
+ * - Datetime range slider with histogram (when `facets.datetime` set)
17
+ * - Numeric range sliders for cloud cover / GSD (when present)
18
+ * - Enum chip lists for collection / platform / constellation /
19
+ * instruments / asset roles (when ≥2 distinct values)
20
+ *
21
+ * Mode awareness: this component does NOT push down to the API itself,
22
+ * the parent decides whether the current `state` should be applied
23
+ * client-side via `applyFacets` or translated to native query params via
24
+ * `toNativeQuery`. We just edit `state` and emit `onChange`.
25
+ */
26
+ let {
27
+ facets,
28
+ state,
29
+ onChange,
30
+ onClose,
31
+ onReset,
32
+ footer
33
+ }: {
34
+ facets: FacetSet;
35
+ state: FacetState;
36
+ onChange: (next: FacetState) => void;
37
+ onClose: () => void;
38
+ onReset: () => void;
39
+ /** Optional footer slot for fetch options (timeRange, itemLimit, mode label). */
40
+ footer?: Snippet;
41
+ } = $props();
42
+
43
+ const NUMERIC_LABEL_KEYS: Record<string, string> = {
44
+ cloudCover: 'stac.cloudCover',
45
+ gsd: 'stac.gsd'
46
+ };
47
+
48
+ const ENUM_LABEL_KEYS: Record<string, string> = {
49
+ collection: 'stac.collection',
50
+ platform: 'stac.platform',
51
+ constellation: 'stac.constellation',
52
+ instruments: 'stac.instruments',
53
+ assetRoles: 'stac.assetRoles'
54
+ };
55
+
56
+ function setDatetime(next: [number, number]): void {
57
+ if (!facets.datetime) return;
58
+ const minMs = Date.parse(facets.datetime.min);
59
+ const maxMs = Date.parse(facets.datetime.max);
60
+ const lo = next[0] <= minMs ? undefined : new Date(next[0]).toISOString();
61
+ const hi = next[1] >= maxMs ? undefined : new Date(next[1]).toISOString();
62
+ onChange({
63
+ ...state,
64
+ datetime: lo || hi ? { min: lo, max: hi } : undefined
65
+ });
66
+ }
67
+
68
+ function setNumeric(
69
+ field: string,
70
+ next: [number, number],
71
+ facetMin: number,
72
+ facetMax: number
73
+ ): void {
74
+ const lo = next[0] <= facetMin ? undefined : next[0];
75
+ const hi = next[1] >= facetMax ? undefined : next[1];
76
+ const numeric = { ...(state.numeric ?? {}) };
77
+ if (lo == null && hi == null) {
78
+ delete (numeric as Record<string, unknown>)[field];
79
+ } else {
80
+ (numeric as Record<string, unknown>)[field] = { min: lo, max: hi };
81
+ }
82
+ onChange({
83
+ ...state,
84
+ numeric: Object.keys(numeric).length > 0 ? numeric : undefined
85
+ });
86
+ }
87
+
88
+ function toggleEnum(field: string, value: string): void {
89
+ const enums = { ...(state.enums ?? {}) };
90
+ const current = (enums as Record<string, string[] | undefined>)[field] ?? [];
91
+ const next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
92
+ if (next.length === 0) {
93
+ delete (enums as Record<string, unknown>)[field];
94
+ } else {
95
+ (enums as Record<string, unknown>)[field] = next;
96
+ }
97
+ onChange({
98
+ ...state,
99
+ enums: Object.keys(enums).length > 0 ? enums : undefined
100
+ });
101
+ }
102
+
103
+ function isEnumActive(field: string, value: string): boolean {
104
+ const list = (state.enums as Record<string, string[] | undefined> | undefined)?.[field];
105
+ return Array.isArray(list) && list.includes(value);
106
+ }
107
+
108
+ const datetimeBounds = $derived(
109
+ facets.datetime
110
+ ? ([Date.parse(facets.datetime.min), Date.parse(facets.datetime.max)] as [number, number])
111
+ : null
112
+ );
113
+
114
+ const datetimeValue = $derived.by((): [number, number] | null => {
115
+ if (!datetimeBounds) return null;
116
+ const [lo, hi] = datetimeBounds;
117
+ const stateLo = state.datetime?.min ? Date.parse(state.datetime.min) : lo;
118
+ const stateHi = state.datetime?.max ? Date.parse(state.datetime.max) : hi;
119
+ return [Number.isFinite(stateLo) ? stateLo : lo, Number.isFinite(stateHi) ? stateHi : hi];
120
+ });
121
+
122
+ function fmtDate(ms: number): string {
123
+ if (!Number.isFinite(ms)) return '-';
124
+ return formatDate(ms);
125
+ }
126
+
127
+ function fmtNumber(n: number): string {
128
+ if (!Number.isFinite(n)) return '-';
129
+ if (Math.abs(n) >= 100) return Math.round(n).toString();
130
+ return n.toFixed(2);
131
+ }
132
+
133
+ const active = $derived(hasActiveFilters(state));
134
+ </script>
135
+
136
+ <div
137
+ class="pointer-events-auto absolute inset-x-0 bottom-0 z-20 flex max-h-[70vh] flex-col gap-3 overflow-hidden rounded-t-xl border border-border bg-card/95 p-3 text-xs text-card-foreground shadow-lg backdrop-blur-sm sm:inset-x-auto sm:bottom-auto sm:end-2 sm:top-12 sm:max-h-[calc(100%-3.5rem)] sm:w-[min(360px,calc(100%-1rem))] sm:rounded-md"
138
+ >
139
+ <header class="flex items-center justify-between gap-2">
140
+ <div class="flex items-center gap-2">
141
+ <span class="font-medium">{t('stac.filters')}</span>
142
+ <span class="text-[10px] text-muted-foreground tabular-nums">
143
+ {t('stac.facetTotal', { count: facets.total })}
144
+ </span>
145
+ </div>
146
+ <div class="flex items-center gap-1">
147
+ {#if active}
148
+ <button
149
+ class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
150
+ onclick={onReset}
151
+ >
152
+ {t('stac.resetFilters')}
153
+ </button>
154
+ {/if}
155
+ <button
156
+ class="inline-flex min-h-9 min-w-9 items-center justify-center rounded p-0.5 text-base text-muted-foreground hover:bg-accent hover:text-card-foreground sm:min-h-0 sm:min-w-0 sm:text-xs"
157
+ onclick={onClose}
158
+ aria-label={t('stac.close')}
159
+ style="touch-action: manipulation;"
160
+ >
161
+ &times;
162
+ </button>
163
+ </div>
164
+ </header>
165
+
166
+ <div class="overflow-y-auto pr-1">
167
+ {#if !facets.datetime && facets.numeric.length === 0 && facets.enums.length === 0}
168
+ <div class="text-[10px] text-muted-foreground">{t('stac.facetNoneAvailable')}</div>
169
+ {/if}
170
+
171
+ {#if facets.datetime && datetimeBounds && datetimeValue}
172
+ <section class="mb-3">
173
+ <div class="mb-1 flex items-baseline justify-between">
174
+ <span class="text-muted-foreground">{t('stac.filterDatetime')}</span>
175
+ <span class="text-[10px] tabular-nums text-muted-foreground">
176
+ {facets.datetime.count}
177
+ </span>
178
+ </div>
179
+ <RangeSlider
180
+ min={datetimeBounds[0]}
181
+ max={datetimeBounds[1]}
182
+ value={datetimeValue}
183
+ step={86_400_000}
184
+ histogram={facets.datetime.bins}
185
+ formatLabel={fmtDate}
186
+ onValueCommit={setDatetime}
187
+ />
188
+ </section>
189
+ {/if}
190
+
191
+ {#each facets.numeric as facet (facet.field)}
192
+ {@const stateRange = state.numeric?.[facet.field]}
193
+ {@const lo = stateRange?.min ?? facet.min}
194
+ {@const hi = stateRange?.max ?? facet.max}
195
+ <section class="mb-3">
196
+ <div class="mb-1 flex items-baseline justify-between">
197
+ <span class="text-muted-foreground">{t(NUMERIC_LABEL_KEYS[facet.field] ?? facet.field)}</span>
198
+ <span class="text-[10px] tabular-nums text-muted-foreground">{facet.count}</span>
199
+ </div>
200
+ <RangeSlider
201
+ min={facet.min}
202
+ max={facet.max}
203
+ value={[lo, hi]}
204
+ step={Math.max((facet.max - facet.min) / 200, 0.01)}
205
+ formatLabel={fmtNumber}
206
+ onValueCommit={(next) => setNumeric(facet.field, next, facet.min, facet.max)}
207
+ />
208
+ </section>
209
+ {/each}
210
+
211
+ {#each facets.enums as facet (facet.field)}
212
+ <section class="mb-3">
213
+ <div class="mb-1 text-muted-foreground">
214
+ {t(ENUM_LABEL_KEYS[facet.field] ?? facet.field)}
215
+ </div>
216
+ <div class="flex flex-wrap gap-1">
217
+ {#each facet.values as entry (entry.value)}
218
+ {@const on = isEnumActive(facet.field, entry.value)}
219
+ <button
220
+ type="button"
221
+ class="rounded-full border px-2 py-0.5 text-[10px] transition-colors"
222
+ class:border-primary={on}
223
+ class:bg-primary={on}
224
+ class:text-primary-foreground={on}
225
+ class:border-input={!on}
226
+ class:hover:bg-accent={!on}
227
+ onclick={() => toggleEnum(facet.field, entry.value)}
228
+ >
229
+ {entry.value}
230
+ <span class="ms-1 text-[9px] opacity-70 tabular-nums">{entry.count}</span>
231
+ </button>
232
+ {/each}
233
+ </div>
234
+ </section>
235
+ {/each}
236
+
237
+ {#if footer}
238
+ <div class="mt-2 border-t border-border pt-3">
239
+ {@render footer()}
240
+ </div>
241
+ {/if}
242
+ </div>
243
+ </div>
@@ -0,0 +1,14 @@
1
+ import { type FacetSet, type FacetState } from '@walkthru-earth/objex-utils';
2
+ import type { Snippet } from 'svelte';
3
+ type $$ComponentProps = {
4
+ facets: FacetSet;
5
+ state: FacetState;
6
+ onChange: (next: FacetState) => void;
7
+ onClose: () => void;
8
+ onReset: () => void;
9
+ /** Optional footer slot for fetch options (timeRange, itemLimit, mode label). */
10
+ footer?: Snippet;
11
+ };
12
+ declare const StacFilterPanel: import("svelte").Component<$$ComponentProps, {}, "">;
13
+ type StacFilterPanel = ReturnType<typeof StacFilterPanel>;
14
+ export default StacFilterPanel;
@@ -0,0 +1,223 @@
1
+ <script lang="ts">
2
+ import type { StacItemView } from '@walkthru-earth/objex-utils';
3
+ import { copyToClipboard, formatDate, jsonReplacerBigInt } from '@walkthru-earth/objex-utils';
4
+ import { onDestroy } from 'svelte';
5
+ import { t } from '../../../i18n/index.svelte.js';
6
+
7
+ /**
8
+ * Right-side slide-over showing a single STAC item's metadata, asset
9
+ * table, and (collapsible) raw JSON. Rendered when the parent has a
10
+ * non-null `selectedView`. The "x" button calls `onClose` so the parent
11
+ * can clear `selectedId` and trigger a footprint-layer refresh.
12
+ *
13
+ * Asset hrefs are presigned via the parent's `presign` callback before
14
+ * being shown to the user (the "Open" link). Without that, a click on
15
+ * an `s3://` href on a private bucket would 403, and an absolute
16
+ * `https://` href that belongs to the user's own bucket would lose its
17
+ * SigV4 query string. Same helper the strip and mosaic use, so the
18
+ * presign cache is shared and warm.
19
+ */
20
+ let {
21
+ view,
22
+ presign,
23
+ onClose,
24
+ onFlyTo
25
+ }: {
26
+ view: StacItemView;
27
+ presign: (href: string) => Promise<string>;
28
+ onClose: () => void;
29
+ onFlyTo?: () => void;
30
+ } = $props();
31
+
32
+ let showRaw = $state(false);
33
+ let copyLabel = $state<string | null>(null);
34
+ // Per-href resolved URL for the asset Open links, fetched on click. We
35
+ // don't pre-resolve every asset because most users only open one or two
36
+ // per item, and presigning is async + sometimes signs a remote endpoint.
37
+ let resolved = $state<Record<string, string>>({});
38
+ const inflight = new Set<string>();
39
+
40
+ function formatDt(iso: string | null): string {
41
+ if (!iso) return '-';
42
+ const t = Date.parse(iso);
43
+ return Number.isFinite(t) ? formatDate(t) : iso;
44
+ }
45
+
46
+ async function openAsset(href: string): Promise<void> {
47
+ let url = resolved[href];
48
+ if (!url && !inflight.has(href)) {
49
+ inflight.add(href);
50
+ try {
51
+ url = await presign(href);
52
+ resolved = { ...resolved, [href]: url };
53
+ } catch {
54
+ url = href;
55
+ } finally {
56
+ inflight.delete(href);
57
+ }
58
+ }
59
+ if (url) window.open(url, '_blank', 'noopener,noreferrer');
60
+ }
61
+
62
+ async function copyId(): Promise<void> {
63
+ if (await copyToClipboard(view.id)) {
64
+ copyLabel = t('stac.copied');
65
+ setTimeout(() => {
66
+ copyLabel = null;
67
+ }, 1200);
68
+ }
69
+ }
70
+
71
+ async function copyJson(): Promise<void> {
72
+ const json = JSON.stringify(view.raw, jsonReplacerBigInt, 2);
73
+ if (await copyToClipboard(json)) {
74
+ copyLabel = t('stac.copied');
75
+ setTimeout(() => {
76
+ copyLabel = null;
77
+ }, 1200);
78
+ }
79
+ }
80
+
81
+ onDestroy(() => {
82
+ resolved = {};
83
+ copyLabel = null;
84
+ });
85
+
86
+ const assets = $derived(Object.entries(view.raw.assets ?? {}));
87
+ </script>
88
+
89
+ <aside
90
+ class="pointer-events-auto absolute inset-x-0 bottom-0 z-20 flex max-h-[65vh] flex-col gap-2 overflow-hidden rounded-t-xl border border-border bg-card/95 p-3 text-xs text-card-foreground shadow-lg backdrop-blur-sm sm:inset-x-auto sm:bottom-auto sm:end-2 sm:top-12 sm:max-h-[calc(100%-3.5rem)] sm:w-[min(360px,calc(100%-1rem))] sm:rounded-md"
91
+ >
92
+ <header class="flex items-start justify-between gap-2">
93
+ <div class="min-w-0 flex-1">
94
+ <div class="truncate font-medium" title={view.id}>{view.id}</div>
95
+ {#if view.collection}
96
+ <div class="truncate text-[10px] text-muted-foreground">{view.collection}</div>
97
+ {/if}
98
+ </div>
99
+ <div class="flex items-center gap-1">
100
+ {#if onFlyTo && view.bbox}
101
+ <button
102
+ class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
103
+ onclick={onFlyTo}
104
+ title={t('stac.flyTo')}
105
+ >
106
+ {t('stac.flyTo')}
107
+ </button>
108
+ {/if}
109
+ <button
110
+ class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
111
+ onclick={copyId}
112
+ >
113
+ {copyLabel ?? t('stac.copyId')}
114
+ </button>
115
+ <button
116
+ class="inline-flex min-h-9 min-w-9 items-center justify-center rounded p-0.5 text-base text-muted-foreground hover:bg-accent hover:text-card-foreground sm:min-h-0 sm:min-w-0 sm:text-xs"
117
+ onclick={onClose}
118
+ aria-label={t('stac.close')}
119
+ style="touch-action: manipulation;"
120
+ >
121
+ &times;
122
+ </button>
123
+ </div>
124
+ </header>
125
+
126
+ <div class="overflow-y-auto">
127
+ <dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1">
128
+ <dt class="text-muted-foreground">{t('stac.datetime')}</dt>
129
+ <dd class="tabular-nums">{formatDt(view.datetime)}</dd>
130
+ {#if view.endDatetime}
131
+ <dt class="text-muted-foreground">{t('stac.endDatetime')}</dt>
132
+ <dd class="tabular-nums">{formatDt(view.endDatetime)}</dd>
133
+ {/if}
134
+ {#if view.cloudCover != null}
135
+ <dt class="text-muted-foreground">{t('stac.cloudCover')}</dt>
136
+ <dd class="tabular-nums">{view.cloudCover.toFixed(1)}%</dd>
137
+ {/if}
138
+ {#if view.gsd != null}
139
+ <dt class="text-muted-foreground">{t('stac.gsd')}</dt>
140
+ <dd class="tabular-nums">{view.gsd} m</dd>
141
+ {/if}
142
+ {#if view.platform}
143
+ <dt class="text-muted-foreground">{t('stac.platform')}</dt>
144
+ <dd>{view.platform}</dd>
145
+ {/if}
146
+ {#if view.constellation}
147
+ <dt class="text-muted-foreground">{t('stac.constellation')}</dt>
148
+ <dd>{view.constellation}</dd>
149
+ {/if}
150
+ {#if view.instruments.length > 0}
151
+ <dt class="text-muted-foreground">{t('stac.instruments')}</dt>
152
+ <dd>{view.instruments.join(', ')}</dd>
153
+ {/if}
154
+ {#if view.epsg != null}
155
+ <dt class="text-muted-foreground">{t('stac.epsg')}</dt>
156
+ <dd class="tabular-nums">EPSG:{view.epsg}</dd>
157
+ {/if}
158
+ {#if view.bbox}
159
+ <dt class="text-muted-foreground">{t('mapInfo.bounds')}</dt>
160
+ <dd class="tabular-nums text-[10px]">
161
+ W {view.bbox[0].toFixed(3)}, S {view.bbox[1].toFixed(3)}<br />
162
+ E {view.bbox[2].toFixed(3)}, N {view.bbox[3].toFixed(3)}
163
+ </dd>
164
+ {/if}
165
+ </dl>
166
+
167
+ {#if assets.length > 0}
168
+ <div class="mt-3">
169
+ <div class="mb-1 text-muted-foreground">
170
+ {t('stac.assets', { count: assets.length })}
171
+ </div>
172
+ <ul class="space-y-1">
173
+ {#each assets as [key, asset] (key)}
174
+ <li class="rounded border border-border px-1.5 py-1">
175
+ <div class="flex items-center justify-between gap-2">
176
+ <span class="truncate font-medium">{key}</span>
177
+ <button
178
+ class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
179
+ onclick={() => void openAsset(asset.href)}
180
+ >
181
+ {t('stac.assetOpen')}
182
+ </button>
183
+ </div>
184
+ {#if asset.title}
185
+ <div class="truncate text-[10px] text-muted-foreground">{asset.title}</div>
186
+ {/if}
187
+ {#if asset.type}
188
+ <div class="truncate text-[10px] text-muted-foreground">{asset.type}</div>
189
+ {/if}
190
+ {#if Array.isArray(asset.roles) && asset.roles.length > 0}
191
+ <div class="mt-0.5 flex flex-wrap gap-1">
192
+ {#each asset.roles as role (role)}
193
+ <span class="rounded bg-muted px-1 text-[9px] text-muted-foreground">{role}</span>
194
+ {/each}
195
+ </div>
196
+ {/if}
197
+ </li>
198
+ {/each}
199
+ </ul>
200
+ </div>
201
+ {/if}
202
+
203
+ <div class="mt-3 border-t border-border pt-2">
204
+ <button
205
+ class="text-[10px] text-muted-foreground hover:text-card-foreground"
206
+ onclick={() => (showRaw = !showRaw)}
207
+ >
208
+ {showRaw ? t('stac.hideRaw') : t('stac.showRaw')}
209
+ </button>
210
+ {#if showRaw}
211
+ <div class="mt-1 flex justify-end">
212
+ <button
213
+ class="rounded border border-input px-1.5 py-0.5 text-[10px] hover:bg-accent"
214
+ onclick={copyJson}
215
+ >
216
+ {copyLabel ?? t('stac.copyJson')}
217
+ </button>
218
+ </div>
219
+ <pre class="mt-1 max-h-72 overflow-auto rounded bg-muted p-2 font-mono text-[10px] leading-tight">{JSON.stringify(view.raw, jsonReplacerBigInt, 2)}</pre>
220
+ {/if}
221
+ </div>
222
+ </div>
223
+ </aside>
@@ -0,0 +1,10 @@
1
+ import type { StacItemView } from '@walkthru-earth/objex-utils';
2
+ type $$ComponentProps = {
3
+ view: StacItemView;
4
+ presign: (href: string) => Promise<string>;
5
+ onClose: () => void;
6
+ onFlyTo?: () => void;
7
+ };
8
+ declare const StacItemInspector: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type StacItemInspector = ReturnType<typeof StacItemInspector>;
10
+ export default StacItemInspector;