@walkthru-earth/objex 0.1.0 → 1.1.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 (84) hide show
  1. package/README.md +9 -2
  2. package/dist/components/browser/FileBrowser.svelte +53 -41
  3. package/dist/components/browser/FileRow.svelte +8 -3
  4. package/dist/components/browser/FileTreeSidebar.svelte +2 -4
  5. package/dist/components/layout/AboutSheet.svelte +126 -0
  6. package/dist/components/layout/AboutSheet.svelte.d.ts +6 -0
  7. package/dist/components/layout/ConnectionDialog.svelte +186 -138
  8. package/dist/components/layout/ConnectionDialog.svelte.d.ts +1 -0
  9. package/dist/components/layout/Sidebar.svelte +19 -3
  10. package/dist/components/layout/TabBar.svelte +4 -7
  11. package/dist/components/viewers/CodeViewer.svelte +17 -9
  12. package/dist/components/viewers/ImageViewer.svelte +6 -16
  13. package/dist/components/viewers/MarkdownViewer.svelte +8 -16
  14. package/dist/components/viewers/MediaViewer.svelte +6 -17
  15. package/dist/components/viewers/ModelViewer.svelte +4 -2
  16. package/dist/components/viewers/NotebookViewer.svelte +90 -40
  17. package/dist/components/viewers/PdfViewer.svelte +5 -3
  18. package/dist/components/viewers/RawViewer.svelte +4 -2
  19. package/dist/components/viewers/TableGrid.svelte +3 -2
  20. package/dist/components/viewers/ZarrMapViewer.svelte +334 -40
  21. package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +3 -8
  22. package/dist/components/viewers/ZarrViewer.svelte +459 -178
  23. package/dist/components/viewers/map/AttributeTable.svelte +1 -6
  24. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +2 -6
  25. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +96 -22
  26. package/dist/constants.d.ts +28 -0
  27. package/dist/constants.js +34 -0
  28. package/dist/file-icons/index.js +6 -0
  29. package/dist/i18n/ar.js +34 -0
  30. package/dist/i18n/en.js +34 -0
  31. package/dist/index.d.ts +13 -1
  32. package/dist/index.js +16 -1
  33. package/dist/query/wasm.js +5 -4
  34. package/dist/storage/browser-cloud.d.ts +7 -0
  35. package/dist/storage/browser-cloud.js +74 -7
  36. package/dist/storage/providers.d.ts +53 -0
  37. package/dist/storage/providers.js +318 -0
  38. package/dist/stores/connections.svelte.js +8 -34
  39. package/dist/stores/files.svelte.d.ts +1 -6
  40. package/dist/stores/files.svelte.js +4 -36
  41. package/dist/stores/query-history.svelte.js +5 -28
  42. package/dist/stores/settings.svelte.d.ts +1 -0
  43. package/dist/stores/settings.svelte.js +11 -31
  44. package/dist/types.d.ts +2 -2
  45. package/dist/utils/clipboard.d.ts +13 -0
  46. package/dist/utils/clipboard.js +38 -0
  47. package/dist/utils/cloud-url.d.ts +27 -0
  48. package/dist/utils/cloud-url.js +61 -0
  49. package/dist/utils/error.d.ts +8 -0
  50. package/dist/utils/error.js +12 -0
  51. package/dist/utils/export.d.ts +22 -2
  52. package/dist/utils/export.js +35 -10
  53. package/dist/utils/file-sort.d.ts +20 -0
  54. package/dist/utils/file-sort.js +41 -0
  55. package/dist/utils/format.d.ts +10 -0
  56. package/dist/utils/format.js +22 -0
  57. package/dist/utils/host-detection.js +78 -18
  58. package/dist/utils/local-storage.d.ts +16 -0
  59. package/dist/utils/local-storage.js +37 -0
  60. package/dist/utils/notebook.d.ts +59 -0
  61. package/dist/utils/notebook.js +211 -0
  62. package/dist/utils/parquet-metadata.js +1 -1
  63. package/dist/utils/pmtiles-tile.js +2 -1
  64. package/dist/utils/pmtiles.js +2 -1
  65. package/dist/utils/storage-url.d.ts +1 -1
  66. package/dist/utils/storage-url.js +82 -24
  67. package/dist/utils/url-state.js +2 -7
  68. package/dist/utils/url.d.ts +0 -2
  69. package/dist/utils/url.js +3 -29
  70. package/dist/utils/zarr.d.ts +60 -20
  71. package/dist/utils/zarr.js +450 -103
  72. package/package.json +66 -54
  73. package/dist/assets/favicon.svg +0 -17
  74. package/dist/components/CLAUDE.md +0 -44
  75. package/dist/components/viewers/CLAUDE.md +0 -60
  76. package/dist/file-icons/CLAUDE.md +0 -21
  77. package/dist/i18n/CLAUDE.md +0 -19
  78. package/dist/query/CLAUDE.md +0 -22
  79. package/dist/storage/CLAUDE.md +0 -23
  80. package/dist/stores/CLAUDE.md +0 -29
  81. package/dist/types/notebookjs.d.ts +0 -14
  82. package/dist/utils/CLAUDE.md +0 -54
  83. package/dist/utils/analytics.d.ts +0 -10
  84. package/dist/utils/analytics.js +0 -38
@@ -1,28 +1,50 @@
1
1
  <script lang="ts">
2
2
  import type maplibregl from 'maplibre-gl';
3
+ import maplibreModule from 'maplibre-gl';
3
4
  import { onDestroy, untrack } from 'svelte';
4
5
  import { t } from '../../i18n/index.svelte.js';
5
6
  import { tabResources } from '../../stores/tab-resources.svelte.js';
6
7
  import type { Tab } from '../../types';
7
8
  import { buildHttpsUrl } from '../../utils/url.js';
9
+ import {
10
+ ensureCodecsRegistered,
11
+ extractZarrStoreUrl,
12
+ inferDims,
13
+ type ZarrNode
14
+ } from '../../utils/zarr.js';
8
15
  import MapContainer from './map/MapContainer.svelte';
9
16
 
10
- interface ZarrVarMeta {
17
+ /** Enriched selector dimension with coordinate metadata. */
18
+ interface SelectorDim {
11
19
  name: string;
12
- shape: number[];
13
- dtype: string;
14
- dims: string[];
15
- attributes: Record<string, any>;
20
+ size: number;
21
+ dtype: string | null;
22
+ units: string | null;
23
+ longName: string | null;
24
+ min: string | null;
25
+ max: string | null;
26
+ isDatetime: boolean;
27
+ minDate: Date | null;
28
+ maxDate: Date | null;
29
+ /** True when estimated step size < 1 day (e.g. 6-hourly forecasts). */
30
+ subDaily: boolean;
31
+ }
32
+
33
+ /** Get the relative path for a ZarrNode (strip leading slash). */
34
+ function varPath(node: ZarrNode): string {
35
+ return node.path.replace(/^\//, '');
16
36
  }
17
37
 
18
38
  let {
19
39
  tab,
20
40
  variables,
41
+ coords = [],
21
42
  spatialRefAttrs,
22
43
  zarrVersion = null
23
44
  }: {
24
45
  tab: Tab;
25
- variables: ZarrVarMeta[];
46
+ variables: ZarrNode[];
47
+ coords?: ZarrNode[];
26
48
  spatialRefAttrs: Record<string, any> | null;
27
49
  zarrVersion?: number | null;
28
50
  } = $props();
@@ -32,20 +54,24 @@ let error = $state<string | null>(null);
32
54
  let selectedVar = $state('');
33
55
  let zarrLayer: any = null;
34
56
  let mapRef: maplibregl.Map | null = null;
57
+ let inspectPopup: maplibregl.Popup | null = null;
35
58
 
36
59
  // Extract proj4 from spatial_ref if available
37
60
  const proj4String = $derived(extractProj4(spatialRefAttrs));
38
61
 
39
- // Initialize selectedVar from variables prop
62
+ // Initialize selectedVar from variables prop (store as relative path)
40
63
  $effect(() => {
41
64
  if (!selectedVar && variables.length > 0) {
42
- selectedVar = variables[0].name;
65
+ selectedVar = varPath(variables[0]);
43
66
  }
44
67
  });
45
68
 
69
+ // Build coord lookup: dimension name → coordinate variable metadata
70
+ const coordByName = $derived(new Map(coords.map((c) => [c.name, c])));
71
+
46
72
  // Identify non-spatial selector dimensions for the selected variable
47
- const selectedMeta = $derived(variables.find((v) => v.name === selectedVar));
48
- const selectorDims = $derived(getSelectorDims(selectedMeta));
73
+ const selectedMeta = $derived(variables.find((v) => varPath(v) === selectedVar));
74
+ const selectorDims = $derived(getSelectorDims(selectedMeta, coordByName));
49
75
 
50
76
  // Dimension slider state
51
77
  let selectorValues = $state<Record<string, number>>({});
@@ -104,33 +130,232 @@ function buildProj4FromCrsWkt(crsWkt: string | undefined): string | null {
104
130
  }
105
131
  }
106
132
 
107
- function getSelectorDims(meta: ZarrVarMeta | undefined): { name: string; size: number }[] {
108
- if (!meta) return [];
109
- const spatialNames = ['x', 'y', 'lat', 'lon', 'latitude', 'longitude'];
110
- const dims: { name: string; size: number }[] = [];
111
- for (let i = 0; i < meta.dims.length; i++) {
112
- const d = meta.dims[i];
113
- if (!spatialNames.includes(d.toLowerCase())) {
114
- dims.push({ name: d, size: meta.shape[i] });
133
+ const DATETIME_DIM_NAMES = new Set(['time', 'init_time', 'valid_time', 'date', 'datetime']);
134
+
135
+ /** Detect timedelta/duration dimension (forecast lead time, etc.). */
136
+ function isTimedeltaDim(attrs: Record<string, any>): boolean {
137
+ if (attrs.standard_name === 'forecast_period') return true;
138
+ if (typeof attrs.dtype === 'string' && attrs.dtype.includes('timedelta')) return true;
139
+ return false;
140
+ }
141
+
142
+ /** Detect temporal dimension via CF-convention signals. */
143
+ function isDatetimeDim(name: string, attrs: Record<string, any>): boolean {
144
+ if (isTimedeltaDim(attrs)) return false;
145
+ if (attrs.axis === 'T') return true;
146
+ if (attrs.standard_name === 'time' || attrs.standard_name === 'forecast_reference_time')
147
+ return true;
148
+ if (typeof attrs.units === 'string' && /\bsince\b/i.test(attrs.units)) return true;
149
+ if (DATETIME_DIM_NAMES.has(name.toLowerCase())) return true;
150
+ return false;
151
+ }
152
+
153
+ /** Parse a date string, treating "present"/"now" as today's date. */
154
+ function parseDateOrSentinel(value: string): Date | null {
155
+ const lower = value.trim().toLowerCase();
156
+ if (lower === 'present' || lower === 'now') return new Date();
157
+ const d = new Date(value);
158
+ return Number.isNaN(d.getTime()) ? null : d;
159
+ }
160
+
161
+ /** Linear interpolation: index → estimated date/datetime string.
162
+ * Sub-daily dims return "YYYY-MM-DDTHH:mm", daily+ return "YYYY-MM-DD". */
163
+ function indexToDateStr(index: number, dim: SelectorDim): string | null {
164
+ if (!dim.minDate || !dim.maxDate || dim.size < 2) return null;
165
+ const t = index / (dim.size - 1);
166
+ const ms = dim.minDate.getTime() + t * (dim.maxDate.getTime() - dim.minDate.getTime());
167
+ const iso = new Date(ms).toISOString();
168
+ return dim.subDaily ? iso.slice(0, 16) : iso.slice(0, 10);
169
+ }
170
+
171
+ /** Inverse: date string → nearest integer index, clamped to [0, size-1]. */
172
+ function dateToIndex(dateStr: string, dim: SelectorDim): number {
173
+ if (!dim.minDate || !dim.maxDate || dim.size < 2) return 0;
174
+ const range = dim.maxDate.getTime() - dim.minDate.getTime();
175
+ if (range === 0) return 0;
176
+ const target = new Date(dateStr).getTime();
177
+ const t = (target - dim.minDate.getTime()) / range;
178
+ return Math.round(Math.max(0, Math.min(dim.size - 1, t * (dim.size - 1))));
179
+ }
180
+
181
+ /** Spatial dimension name aliases → canonical ZarrLayer keys. */
182
+ const SPATIAL_ALIASES: Record<string, 'lat' | 'lon'> = {
183
+ x: 'lon',
184
+ y: 'lat',
185
+ lat: 'lat',
186
+ lon: 'lon',
187
+ latitude: 'lat',
188
+ longitude: 'lon'
189
+ };
190
+
191
+ /** Detect spatial dimension mapping for @carbonplan/zarr-layer. */
192
+ function detectSpatialDims(meta: ZarrNode | undefined): { lat: string; lon: string } | null {
193
+ if (!meta?.shape) return null;
194
+ const dimNames = meta.dims?.length ? meta.dims : inferDims(meta.name, meta.shape);
195
+ let lat: string | null = null;
196
+ let lon: string | null = null;
197
+ for (const d of dimNames) {
198
+ const role = SPATIAL_ALIASES[d.toLowerCase()];
199
+ if (role === 'lat' && !lat) lat = d;
200
+ else if (role === 'lon' && !lon) lon = d;
201
+ }
202
+ return lat && lon ? { lat, lon } : null;
203
+ }
204
+
205
+ function getSelectorDims(
206
+ meta: ZarrNode | undefined,
207
+ coordMap: Map<string, ZarrNode>
208
+ ): SelectorDim[] {
209
+ if (!meta?.shape) return [];
210
+ const shape = meta.shape;
211
+ // Use real dim names when available, fall back to inferDims
212
+ const dimNames = meta.dims?.length ? meta.dims : inferDims(meta.name, shape);
213
+ const dims: SelectorDim[] = [];
214
+ for (let i = 0; i < dimNames.length; i++) {
215
+ const d = dimNames[i];
216
+ if (SPATIAL_ALIASES[d.toLowerCase()]) continue;
217
+
218
+ const coord = coordMap.get(d);
219
+ const attrs = coord?.attributes ?? {};
220
+
221
+ // Extract min/max from statistics_approximate if available
222
+ let min: string | null = null;
223
+ let max: string | null = null;
224
+ const stats = attrs.statistics_approximate ?? attrs.statistics;
225
+ if (stats && typeof stats === 'object') {
226
+ if (stats.min != null) min = String(stats.min);
227
+ if (stats.max != null) max = String(stats.max);
228
+ }
229
+
230
+ // Detect datetime dimension and parse date range
231
+ const datetime = isDatetimeDim(d, attrs);
232
+ let minDate: Date | null = null;
233
+ let maxDate: Date | null = null;
234
+ if (datetime && min != null && max != null) {
235
+ const dMin = parseDateOrSentinel(min);
236
+ const dMax = parseDateOrSentinel(max);
237
+ if (dMin && dMax) {
238
+ minDate = dMin;
239
+ maxDate = dMax;
240
+ }
241
+ }
242
+
243
+ // Sub-daily: estimated step < 1 day (e.g. 6-hourly forecasts)
244
+ let subDaily = false;
245
+ if (minDate && maxDate && shape[i] >= 2) {
246
+ const stepMs = (maxDate.getTime() - minDate.getTime()) / (shape[i] - 1);
247
+ subDaily = stepMs < 86_400_000;
115
248
  }
249
+
250
+ dims.push({
251
+ name: d,
252
+ size: shape[i],
253
+ dtype: coord?.dtype ?? null,
254
+ units: attrs.units ?? null,
255
+ longName: attrs.long_name ?? null,
256
+ min,
257
+ max,
258
+ isDatetime: datetime,
259
+ minDate,
260
+ maxDate,
261
+ subDaily
262
+ });
116
263
  }
117
264
  return dims;
118
265
  }
119
266
 
267
+ /** Format a dimension label: show long_name or name, with dtype. */
268
+ function dimLabel(dim: SelectorDim): string {
269
+ const label = dim.longName ?? dim.name;
270
+ return dim.dtype ? `${label} (${dim.dtype})` : label;
271
+ }
272
+
120
273
  // Initialize selector values when variable changes
121
274
  $effect(() => {
122
275
  const dims = selectorDims;
123
276
  const prev = untrack(() => selectorValues);
124
277
  const newVals: Record<string, number> = {};
125
278
  for (const d of dims) {
126
- newVals[d.name] = prev[d.name] ?? 0;
279
+ newVals[d.name] = prev[d.name] ?? (d.isDatetime ? d.size - 1 : 0);
127
280
  }
128
281
  selectorValues = newVals;
129
282
  });
130
283
 
284
+ function getOrCreatePopup(): maplibregl.Popup {
285
+ if (!inspectPopup) {
286
+ inspectPopup = new maplibreModule.Popup({
287
+ closeButton: true,
288
+ closeOnClick: false,
289
+ maxWidth: '240px',
290
+ className: 'zarr-inspect-popup'
291
+ });
292
+ }
293
+ return inspectPopup;
294
+ }
295
+
296
+ function formatPopupHtml(value: number | null | undefined, lngLat: maplibregl.LngLat): string {
297
+ const varName = selectedVar;
298
+ const units = selectedMeta?.attributes?.units;
299
+ const noData = value == null || Number.isNaN(value);
300
+
301
+ let valueStr: string;
302
+ if (noData) {
303
+ valueStr = t('map.noValue');
304
+ } else {
305
+ valueStr = Number.isInteger(value) ? String(value) : value.toPrecision(4);
306
+ }
307
+
308
+ // Hide units when no data, or when units is "1" (CF dimensionless)
309
+ const showUnits = !noData && units && units !== '1';
310
+
311
+ const lat = lngLat.lat.toFixed(4);
312
+ const lon = lngLat.lng.toFixed(4);
313
+
314
+ return `<div class="text-xs space-y-0.5">
315
+ <div class="font-medium text-zinc-300">${varName}</div>
316
+ <div>${valueStr}${showUnits ? ` <span class="text-zinc-500">${units}</span>` : ''}</div>
317
+ <div class="text-zinc-500">${lat}, ${lon}</div>
318
+ </div>`;
319
+ }
320
+
321
+ async function handleMapClick(e: maplibregl.MapMouseEvent) {
322
+ if (!zarrLayer) return;
323
+
324
+ const popup = getOrCreatePopup();
325
+ popup
326
+ .setLngLat(e.lngLat)
327
+ .setHTML(`<span class="text-xs">${t('map.loadingZarr')}</span>`)
328
+ .addTo(mapRef!);
329
+
330
+ try {
331
+ const result = await zarrLayer.queryData({
332
+ type: 'Point',
333
+ coordinates: [e.lngLat.lng, e.lngLat.lat]
334
+ });
335
+
336
+ const raw = result?.[selectedVar];
337
+ // queryData may return Array, TypedArray (Float32Array), or scalar
338
+ const value = raw != null && typeof raw === 'object' && 'length' in raw ? raw[0] : raw;
339
+ popup.setHTML(formatPopupHtml(value, e.lngLat));
340
+ } catch {
341
+ popup.setHTML(`<span class="text-xs">${t('map.noValue')}</span>`);
342
+ }
343
+ }
344
+
345
+ /** Build the current selector object from selectorDims state. */
346
+ function buildSelector(): Record<string, any> {
347
+ const selector: Record<string, any> = {};
348
+ for (const d of selectorDims) {
349
+ const fallback = d.isDatetime ? d.size - 1 : 0;
350
+ selector[d.name] = { selected: selectorValues[d.name] ?? fallback, type: 'index' };
351
+ }
352
+ return selector;
353
+ }
354
+
131
355
  async function onMapReady(map: maplibregl.Map) {
132
356
  mapRef = map;
133
357
  await addZarrLayer(map);
358
+ map.on('click', handleMapClick);
134
359
  }
135
360
 
136
361
  async function addZarrLayer(map: maplibregl.Map) {
@@ -143,13 +368,12 @@ async function addZarrLayer(map: maplibregl.Map) {
143
368
  map.removeLayer(zarrLayer.id);
144
369
  }
145
370
 
371
+ // Ensure numcodecs codecs (shuffle, zlib, etc.) are registered before zarr-layer uses zarrita
372
+ await ensureCodecsRegistered();
146
373
  const { ZarrLayer } = await import('@carbonplan/zarr-layer');
147
374
 
148
375
  const storeUrl = buildStoreUrl();
149
- const selector: Record<string, any> = {};
150
- for (const [dim, val] of Object.entries(selectorValues)) {
151
- selector[dim] = { selected: val, type: 'index' };
152
- }
376
+ const selector = buildSelector();
153
377
 
154
378
  const opts: any = {
155
379
  id: 'zarr-data',
@@ -163,15 +387,32 @@ async function addZarrLayer(map: maplibregl.Map) {
163
387
  onLoadingStateChange: (state: any) => {
164
388
  if (state.error) {
165
389
  error = state.error.message;
390
+ loading = false;
391
+ // Immediately remove failed layer to prevent WebGL context corruption
392
+ try {
393
+ if (map.getLayer('zarr-data')) map.removeLayer('zarr-data');
394
+ } catch {
395
+ /* map may already be destroyed */
396
+ }
397
+ zarrLayer = null;
398
+ return;
166
399
  }
167
400
  loading = state.loading;
168
401
  }
169
402
  };
170
403
 
171
- // Add projection info if available
404
+ // Map spatial dimension names for @carbonplan/zarr-layer
405
+ const spatial = detectSpatialDims(selectedMeta);
406
+ if (!spatial && !proj4String) {
407
+ error = 'Cannot map this variable: no spatial dimensions (lat/lon, y/x) detected';
408
+ loading = false;
409
+ return;
410
+ }
172
411
  if (proj4String) {
173
412
  opts.proj4 = proj4String;
174
- opts.spatialDimensions = { lat: 'y', lon: 'x' };
413
+ opts.spatialDimensions = spatial ? spatial : { lat: 'y', lon: 'x' };
414
+ } else if (spatial) {
415
+ opts.spatialDimensions = spatial;
175
416
  }
176
417
 
177
418
  zarrLayer = new ZarrLayer(opts);
@@ -183,20 +424,16 @@ async function addZarrLayer(map: maplibregl.Map) {
183
424
  }
184
425
 
185
426
  function buildStoreUrl(): string {
186
- const url = buildHttpsUrl(tab);
187
- // Strip zarr.json suffix and trailing slashes
188
- return url.replace(/\/zarr\.json$/, '').replace(/\/+$/, '');
427
+ const rawUrl = buildHttpsUrl(tab).replace(/\/+$/, '');
428
+ return extractZarrStoreUrl(rawUrl) ?? rawUrl;
189
429
  }
190
430
 
191
431
  // Re-render when selector changes
192
432
  async function updateSelector() {
193
433
  if (!zarrLayer) return;
194
- const selector: Record<string, any> = {};
195
- for (const [dim, val] of Object.entries(selectorValues)) {
196
- selector[dim] = { selected: val, type: 'index' };
197
- }
434
+ inspectPopup?.remove();
198
435
  try {
199
- await zarrLayer.setSelector(selector);
436
+ await zarrLayer.setSelector(buildSelector());
200
437
  } catch (err) {
201
438
  error = err instanceof Error ? err.message : String(err);
202
439
  }
@@ -205,11 +442,15 @@ async function updateSelector() {
205
442
  // Re-render when variable changes
206
443
  async function changeVariable() {
207
444
  if (!mapRef) return;
445
+ inspectPopup?.remove();
208
446
  await addZarrLayer(mapRef);
209
447
  }
210
448
 
211
449
  function cleanup() {
450
+ inspectPopup?.remove();
451
+ inspectPopup = null;
212
452
  try {
453
+ mapRef?.off('click', handleMapClick);
213
454
  if (zarrLayer && mapRef?.getLayer('zarr-data')) {
214
455
  mapRef.removeLayer('zarr-data');
215
456
  }
@@ -231,7 +472,7 @@ onDestroy(cleanup);
231
472
  <div class="flex h-full w-full flex-col overflow-hidden">
232
473
  <!-- Controls bar -->
233
474
  <div
234
- class="flex items-center gap-2 border-b border-zinc-200 px-3 py-1.5 dark:border-zinc-800"
475
+ 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"
235
476
  >
236
477
  <label class="flex items-center gap-1 text-xs text-zinc-400">
237
478
  {t('map.variable')}
@@ -241,27 +482,57 @@ onDestroy(cleanup);
241
482
  onchange={changeVariable}
242
483
  >
243
484
  {#each variables as v}
244
- <option value={v.name}>{v.name}</option>
485
+ <option value={varPath(v)}>{v.name}</option>
245
486
  {/each}
246
487
  </select>
247
488
  </label>
248
489
 
249
490
  {#each selectorDims as dim}
250
- <label class="flex items-center gap-1 text-xs text-zinc-400">
251
- {dim.name}:
491
+ <label
492
+ 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"
493
+ title={dimLabel(dim)}
494
+ >
495
+ <span class="shrink-0 font-medium text-zinc-500 dark:text-zinc-400">{dim.name}</span>
252
496
  <input
253
497
  type="range"
254
498
  min="0"
255
499
  max={dim.size - 1}
256
- bind:value={selectorValues[dim.name]}
500
+ value={selectorValues[dim.name] ?? 0}
501
+ oninput={(e) => {
502
+ selectorValues[dim.name] = +e.currentTarget.value;
503
+ }}
257
504
  onchange={updateSelector}
258
505
  class="h-1 w-16"
259
506
  />
260
- <span class="w-6 text-end text-zinc-500">{selectorValues[dim.name] ?? 0}</span>
507
+ {#if dim.isDatetime && dim.minDate && dim.maxDate}
508
+ {@const dateVal = indexToDateStr(selectorValues[dim.name] ?? 0, dim)}
509
+ <span class="shrink-0 tabular-nums text-zinc-500">
510
+ {dateVal ? (dim.subDaily ? dateVal.replace('T', ' ') : dateVal) : (selectorValues[dim.name] ?? 0)}
511
+ </span>
512
+ <input
513
+ type={dim.subDaily ? 'datetime-local' : 'date'}
514
+ min={dim.minDate.toISOString().slice(0, dim.subDaily ? 16 : 10)}
515
+ max={dim.maxDate.toISOString().slice(0, dim.subDaily ? 16 : 10)}
516
+ value={dateVal ?? ''}
517
+ onchange={(e) => {
518
+ const val = /** @type {HTMLInputElement} */ (e.currentTarget).value;
519
+ if (val) {
520
+ selectorValues[dim.name] = dateToIndex(val, dim);
521
+ updateSelector();
522
+ }
523
+ }}
524
+ 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"
525
+ />
526
+ {:else}
527
+ <span class="shrink-0 tabular-nums text-zinc-500">{selectorValues[dim.name] ?? 0}<span class="text-zinc-500/60">/{dim.size - 1}</span></span>
528
+ {#if dim.dtype}
529
+ <span class="shrink-0 text-[10px] text-zinc-400/70">{dim.dtype}</span>
530
+ {/if}
531
+ {/if}
261
532
  </label>
262
533
  {/each}
263
534
 
264
- {#if selectedMeta}
535
+ {#if selectedMeta?.shape}
265
536
  <span class="ms-auto text-xs text-zinc-400">
266
537
  {selectedMeta.dtype} [{selectedMeta.shape.join(', ')}]
267
538
  </span>
@@ -286,3 +557,26 @@ onDestroy(cleanup);
286
557
  {/if}
287
558
  </div>
288
559
  </div>
560
+
561
+ <style>
562
+ :global(.zarr-inspect-popup .maplibregl-popup-content) {
563
+ background: rgba(24, 24, 27, 0.92);
564
+ color: #e4e4e7;
565
+ border-radius: 6px;
566
+ padding: 6px 8px;
567
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
568
+ border: 1px solid rgba(63, 63, 70, 0.5);
569
+ }
570
+ :global(.zarr-inspect-popup .maplibregl-popup-tip) {
571
+ border-top-color: rgba(24, 24, 27, 0.92);
572
+ }
573
+ :global(.zarr-inspect-popup .maplibregl-popup-close-button) {
574
+ color: #a1a1aa;
575
+ font-size: 14px;
576
+ padding: 2px 4px;
577
+ }
578
+ :global(.zarr-inspect-popup .maplibregl-popup-close-button:hover) {
579
+ color: #e4e4e7;
580
+ background: transparent;
581
+ }
582
+ </style>
@@ -1,14 +1,9 @@
1
1
  import type { Tab } from '../../types';
2
- interface ZarrVarMeta {
3
- name: string;
4
- shape: number[];
5
- dtype: string;
6
- dims: string[];
7
- attributes: Record<string, any>;
8
- }
2
+ import { type ZarrNode } from '../../utils/zarr.js';
9
3
  type $$ComponentProps = {
10
4
  tab: Tab;
11
- variables: ZarrVarMeta[];
5
+ variables: ZarrNode[];
6
+ coords?: ZarrNode[];
12
7
  spatialRefAttrs: Record<string, any> | null;
13
8
  zarrVersion?: number | null;
14
9
  };