@walkthru-earth/objex 1.4.0 → 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 (58) hide show
  1. package/README.md +9 -9
  2. package/dist/components/layout/ConnectionDialog.svelte +7 -2
  3. package/dist/components/layout/SettingsSheet.svelte +2 -1
  4. package/dist/components/layout/StatusBar.svelte +16 -13
  5. package/dist/components/layout/TabBar.svelte +2 -2
  6. package/dist/components/viewers/ArchiveViewer.svelte +139 -112
  7. package/dist/components/viewers/CodeViewer.svelte +15 -27
  8. package/dist/components/viewers/CodeViewer.svelte.d.ts +1 -1
  9. package/dist/components/viewers/CogViewer.svelte +8 -6
  10. package/dist/components/viewers/CopcViewer.svelte +8 -15
  11. package/dist/components/viewers/DatabaseViewer.svelte +22 -21
  12. package/dist/components/viewers/FileInfo.svelte +16 -16
  13. package/dist/components/viewers/FlatGeobufViewer.svelte +15 -45
  14. package/dist/components/viewers/GeoParquetMapViewer.svelte +5 -3
  15. package/dist/components/viewers/ImageViewer.svelte +10 -12
  16. package/dist/components/viewers/LoadProgress.svelte +6 -6
  17. package/dist/components/viewers/MarkdownViewer.svelte +17 -21
  18. package/dist/components/viewers/MediaViewer.svelte +11 -12
  19. package/dist/components/viewers/ModelViewer.svelte +17 -20
  20. package/dist/components/viewers/MultiCogViewer.svelte +11 -8
  21. package/dist/components/viewers/NotebookViewer.svelte +22 -26
  22. package/dist/components/viewers/PdfViewer.svelte +22 -31
  23. package/dist/components/viewers/PmtilesViewer.svelte +10 -9
  24. package/dist/components/viewers/QueryHistoryPanel.svelte +18 -18
  25. package/dist/components/viewers/RawViewer.svelte +21 -18
  26. package/dist/components/viewers/StacMapViewer.svelte +6 -13
  27. package/dist/components/viewers/StacMosaicViewer.svelte +9 -7
  28. package/dist/components/viewers/StacTabViewer.svelte +2 -2
  29. package/dist/components/viewers/TableGrid.svelte +34 -30
  30. package/dist/components/viewers/TableStatusBar.svelte +6 -6
  31. package/dist/components/viewers/TableToolbar.svelte +9 -8
  32. package/dist/components/viewers/TableViewer.svelte +22 -13
  33. package/dist/components/viewers/ViewerHeader.svelte +18 -0
  34. package/dist/components/viewers/ViewerHeader.svelte.d.ts +10 -0
  35. package/dist/components/viewers/ViewerStatus.svelte +19 -0
  36. package/dist/components/viewers/ViewerStatus.svelte.d.ts +7 -0
  37. package/dist/components/viewers/ZarrMapViewer.svelte +13 -12
  38. package/dist/components/viewers/ZarrViewer.svelte +94 -61
  39. package/dist/components/viewers/map/AttributeTable.svelte +6 -6
  40. package/dist/components/viewers/map/MapContainer.svelte +2 -2
  41. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +109 -83
  42. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +16 -16
  43. package/dist/constants.d.ts +6 -0
  44. package/dist/constants.js +8 -0
  45. package/dist/i18n/ar.js +3 -0
  46. package/dist/i18n/en.js +3 -0
  47. package/dist/query/stac-source-parquet.js +8 -5
  48. package/dist/query/wasm.js +6 -63
  49. package/dist/storage/presign.js +2 -1
  50. package/dist/storage/providers.js +2 -1
  51. package/dist/stores/settings.svelte.js +3 -3
  52. package/dist/utils/deck.d.ts +2 -0
  53. package/dist/utils/deck.js +5 -3
  54. package/dist/utils/media-query.svelte.d.ts +14 -0
  55. package/dist/utils/media-query.svelte.js +29 -0
  56. package/dist/utils/signed-url-effect.d.ts +7 -0
  57. package/dist/utils/signed-url-effect.js +19 -0
  58. package/package.json +2 -2
@@ -1,7 +1,7 @@
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 } from '@walkthru-earth/objex-utils';
4
+ import { formatFileSize, handleLoadError } from '@walkthru-earth/objex-utils';
5
5
  import type { PMTiles } from 'pmtiles';
6
6
  import { onDestroy } from 'svelte';
7
7
  import { t } from '../../../i18n/index.svelte.js';
@@ -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>
@@ -30,3 +30,9 @@ export declare const VIEWER_DIR_EXTENSIONS: Set<string>;
30
30
  export declare const LAYER_HUE_MULTIPLIER = 137;
31
31
  /** Duration (ms) to show "Copied!" feedback before resetting. */
32
32
  export declare const COPY_FEEDBACK_MS = 2000;
33
+ /** Region assumed when a connection or bucket name yields none. AWS's global default. */
34
+ export declare const DEFAULT_AWS_REGION = "us-east-1";
35
+ /** deck.gl tile-layer debounce (ms) before fetching after a viewport change. */
36
+ export declare const TILE_DEBOUNCE_MS = 200;
37
+ /** Zoom level used when flying to the first feature of a vector dataset. */
38
+ export declare const FIRST_FEATURE_FLY_ZOOM = 14;
package/dist/constants.js CHANGED
@@ -36,3 +36,11 @@ export const LAYER_HUE_MULTIPLIER = 137;
36
36
  // ── Clipboard ──
37
37
  /** Duration (ms) to show "Copied!" feedback before resetting. */
38
38
  export const COPY_FEEDBACK_MS = 2000;
39
+ // ── AWS defaults ──
40
+ /** Region assumed when a connection or bucket name yields none. AWS's global default. */
41
+ export const DEFAULT_AWS_REGION = 'us-east-1';
42
+ // ── Map / tiles ──
43
+ /** deck.gl tile-layer debounce (ms) before fetching after a viewport change. */
44
+ export const TILE_DEBOUNCE_MS = 200;
45
+ /** Zoom level used when flying to the first feature of a vector dataset. */
46
+ export const FIRST_FEATURE_FLY_ZOOM = 14;
package/dist/i18n/ar.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export const ar = {
2
+ // Common
3
+ 'common.loading': 'جارٍ التحميل...',
4
+ 'common.error': 'خطأ',
2
5
  // Sidebar
3
6
  'sidebar.deleteConfirm': 'حذف الاتصال "{name}"؟',
4
7
  'sidebar.browseDetected': 'تصفح الحاوية المكتشفة: {name}',
package/dist/i18n/en.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export const en = {
2
+ // Common
3
+ 'common.loading': 'Loading...',
4
+ 'common.error': 'Error',
2
5
  // Sidebar
3
6
  'sidebar.deleteConfirm': 'Delete connection "{name}"?',
4
7
  'sidebar.browseDetected': 'Browse detected bucket: {name}',
@@ -27,7 +27,7 @@
27
27
  * Yields a single batch with `done: true`. Slice 3 turns this into a real
28
28
  * stream via `conn.send()` so large catalogs can render progressively.
29
29
  */
30
- import { emptyPushdown, parseWKB, stacRowToItem } from '@walkthru-earth/objex-utils';
30
+ import { DEFAULT_APP_CONFIG, emptyPushdown, parseWKB, stacRowToItem } from '@walkthru-earth/objex-utils';
31
31
  import { QueryCancelledError } from './engine.js';
32
32
  import { getQueryEngine } from './index.js';
33
33
  import { resolveTableSourceAsync } from './source.js';
@@ -36,14 +36,17 @@ import { resolveTableSourceAsync } from './source.js';
36
36
  * iOS Safari caps the WASM heap at ~1.8 GiB and rarely engages OPFS spill
37
37
  * (`credentialless` COEP only landed in 17.6), so STRUCT-heavy stac-geoparquet
38
38
  * scans OOM during the parquet decode before any rows reach the consumer.
39
+ * Evaluated per source construction so a device that rotates or resizes re-checks.
40
+ * Desktop browsers are never classified low-memory regardless of window size.
39
41
  */
40
42
  function detectLowMemoryDefault() {
41
43
  if (typeof navigator === 'undefined')
42
44
  return false;
43
- if (/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent))
44
- return true;
45
+ const isMobileUa = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
46
+ if (!isMobileUa)
47
+ return false; // desktop is never low-memory, regardless of window size
45
48
  if (typeof window === 'undefined')
46
- return false;
49
+ return true;
47
50
  return Math.min(window.innerWidth, window.innerHeight) <= 820;
48
51
  }
49
52
  /**
@@ -108,7 +111,7 @@ function joinWhere(parts) {
108
111
  const live = parts.filter((p) => p !== null && p.length > 0);
109
112
  return live.length === 0 ? '' : ` WHERE ${live.join(' AND ')}`;
110
113
  }
111
- const DEFAULT_LIMIT = 2000;
114
+ const DEFAULT_LIMIT = DEFAULT_APP_CONFIG.defaults.mosaicItemLimit;
112
115
  /**
113
116
  * Build the SELECT list. All columns are optional in the stac-geoparquet
114
117
  * spec, so we only project what we know we'll use and the spec requires.
@@ -1,5 +1,5 @@
1
1
  import { buildTransformExpr, wrapWkbWithCrs } from '@walkthru-earth/objex-utils';
2
- import { DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, WGS84_CODES } from '../constants.js';
2
+ import { DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, STORAGE_KEYS, WGS84_CODES } from '../constants.js';
3
3
  import { getAccessMode, resolveProviderEndpoint } from '../storage/providers.js';
4
4
  import { credentialStore } from '../stores/credentials.svelte.js';
5
5
  import { QueryCancelledError } from './engine';
@@ -579,67 +579,10 @@ function isBinaryType(typeStr) {
579
579
  }
580
580
  export class WasmQueryEngine {
581
581
  async query(connId, sql) {
582
- const t0 = performance.now();
583
- const sqlPreview = sql.length > 120 ? `${sql.slice(0, 120)}…` : sql;
584
- log(`query ${sqlPreview}`);
585
- const db = await getDB();
586
- const conn = await db.connect();
587
- const tConn = performance.now();
588
- log(`query → connected in ${elapsed(t0)}`);
589
- try {
590
- if (connId) {
591
- await this.configureStorage(conn, connId, sql);
592
- log(`query → storage configured in ${elapsed(tConn)}`);
593
- }
594
- const tQuery = performance.now();
595
- const result = await conn.query(sql);
596
- log(`query → executed in ${elapsed(tQuery)}, rows: ${result.numRows}`);
597
- // DuckDB WASM returns an Arrow Table (bundled apache-arrow@17).
598
- // Our project uses apache-arrow@21 — cross-version tableToIPC/tableFromIPC
599
- // loses data rows. Extract rows directly from DuckDB's own Arrow Table.
600
- const numRows = result.numRows;
601
- const cols = result.schema.fields.map((f) => f.name);
602
- const types = result.schema.fields.map((f) => String(f.type));
603
- if (numRows === 0) {
604
- log(`query → done (empty) in ${elapsed(t0)}`);
605
- return {
606
- columns: cols,
607
- types,
608
- rowCount: 0,
609
- rows: []
610
- };
611
- }
612
- // Arrow emits DECIMAL columns as multi-word BigInt / Uint32Array buffers.
613
- // `String(rawDecimal)` yields the unscaled integer (or "0,0,0,0"),
614
- // so rewrite each decimal cell through formatDecimal with the column scale.
615
- const decimalCols = [];
616
- for (let i = 0; i < cols.length; i++) {
617
- const s = decimalScale(types[i]);
618
- if (s >= 0)
619
- decimalCols.push({ name: cols[i], scale: s });
620
- }
621
- // Extract rows directly — avoids Arrow version mismatch
622
- const rows = result.toArray().map((row) => {
623
- const obj = typeof row.toJSON === 'function' ? row.toJSON() : {};
624
- if (typeof row.toJSON !== 'function') {
625
- for (const col of cols)
626
- obj[col] = row[col];
627
- }
628
- for (const { name, scale } of decimalCols) {
629
- obj[name] = formatDecimal(obj[name], scale);
630
- }
631
- return obj;
632
- });
633
- log(`query → done in ${elapsed(t0)}, ${numRows} rows, ${cols.length} cols`);
634
- return { columns: cols, types, rowCount: numRows, rows };
635
- }
636
- catch (err) {
637
- logWarn(`query → failed after ${elapsed(t0)}:`, err?.message ?? err);
638
- throw err;
639
- }
640
- finally {
641
- await conn.close();
642
- }
582
+ // Delegate to the send()-based path so a data query never blocks the
583
+ // single DuckDB worker (conn.query() is blocking). Same return shape.
584
+ const { result } = this.queryCancellable(connId, sql);
585
+ return result;
643
586
  }
644
587
  async queryForMap(connId, sql, geomCol, geomColType, sourceCrs) {
645
588
  const t0 = performance.now();
@@ -855,7 +798,7 @@ export class WasmQueryEngine {
855
798
  log('configureStorage → presigned HTTPS source, skipping S3 config');
856
799
  return;
857
800
  }
858
- const stored = localStorage.getItem('obstore-explore-connections');
801
+ const stored = localStorage.getItem(STORAGE_KEYS.CONNECTIONS);
859
802
  if (!stored) {
860
803
  log('configureStorage → no connections in localStorage');
861
804
  return;
@@ -1,4 +1,5 @@
1
1
  import { safeDecodeURIComponent } from '@walkthru-earth/objex-utils';
2
+ import { DEFAULT_AWS_REGION } from '../constants.js';
2
3
  import { credentialStore } from '../stores/credentials.svelte.js';
3
4
  import { buildProviderBaseUrl, getAccessMode } from './providers.js';
4
5
  // 7 days is the SigV4 protocol maximum and is the hard cap on every
@@ -38,7 +39,7 @@ export async function presignHttpsUrl(conn, key, expiresIn = DEFAULT_EXPIRES_IN_
38
39
  accessKeyId: creds.accessKey,
39
40
  secretAccessKey: creds.secretKey,
40
41
  service: 's3',
41
- region: conn.region || 'us-east-1'
42
+ region: conn.region || DEFAULT_AWS_REGION
42
43
  });
43
44
  const signed = await client.sign(url.toString(), {
44
45
  method: 'GET',
@@ -4,6 +4,7 @@
4
4
  * Centralizes endpoint patterns, regions, auth methods, and UI metadata.
5
5
  * Used by ConnectionDialog, browser-cloud adapter, host-detection, url-state, etc.
6
6
  */
7
+ import { DEFAULT_AWS_REGION } from '../constants.js';
7
8
  // ---------------------------------------------------------------------------
8
9
  // Registry
9
10
  // ---------------------------------------------------------------------------
@@ -463,7 +464,7 @@ export function buildProviderBaseUrl(provider, endpoint, bucket, region) {
463
464
  return `${resolved}/${bucket}`;
464
465
  }
465
466
  // Fallback: AWS S3 path-style
466
- return `https://s3.${region || 'us-east-1'}.amazonaws.com/${bucket}`;
467
+ return `https://s3.${region || DEFAULT_AWS_REGION}.amazonaws.com/${bucket}`;
467
468
  }
468
469
  /** Check if a provider uses the GCS JSON API (not S3 XML). */
469
470
  export function isGcsProvider(provider, endpoint) {
@@ -1,4 +1,4 @@
1
- import { loadFromStorage, parseVisibilityParam, persistToStorage, resolveSetting } from '@walkthru-earth/objex-utils';
1
+ import { DEFAULT_APP_CONFIG, loadFromStorage, parseVisibilityParam, persistToStorage, resolveSetting } from '@walkthru-earth/objex-utils';
2
2
  import { STORAGE_KEYS } from '../constants.js';
3
3
  import { appConfig } from './config.svelte.js';
4
4
  /**
@@ -65,13 +65,13 @@ function createSettingsStore() {
65
65
  return resolveSetting(user.locale, cfg().defaults.locale, 'en');
66
66
  },
67
67
  get featureLimit() {
68
- return resolveSetting(user.featureLimit, cfg().defaults.featureLimit, 1000);
68
+ return resolveSetting(user.featureLimit, cfg().defaults.featureLimit, DEFAULT_APP_CONFIG.defaults.featureLimit);
69
69
  },
70
70
  get mosaicItemLimit() {
71
71
  // Explicit user/query choice always wins, at any value.
72
72
  if (user.mosaicItemLimit !== undefined)
73
73
  return user.mosaicItemLimit;
74
- const configured = resolveSetting(cfg().defaults.mosaicItemLimit, 2000);
74
+ const configured = resolveSetting(cfg().defaults.mosaicItemLimit, DEFAULT_APP_CONFIG.defaults.mosaicItemLimit);
75
75
  // Mobile heap safety: clamp the default so API/static mosaic loads don't OOM.
76
76
  return mobileLikeAtLoad ? Math.min(configured, MOBILE_MOSAIC_LIMIT) : configured;
77
77
  },
@@ -17,6 +17,8 @@ export declare function hoverCursor(map: {
17
17
  picked?: boolean;
18
18
  }) => void;
19
19
  type RGBA = [number, number, number, number];
20
+ /** RGBA used to highlight a hovered/selected feature across map viewers. */
21
+ export declare const HIGHLIGHT_COLOR: [number, number, number, number];
20
22
  /** Distinct fill/line colors per geometry type. */
21
23
  export declare const GEOMETRY_COLORS: Record<string, {
22
24
  fill: RGBA;
@@ -16,6 +16,8 @@ export function hoverCursor(map) {
16
16
  map.getCanvas().style.cursor = info.picked ? 'pointer' : '';
17
17
  };
18
18
  }
19
+ /** RGBA used to highlight a hovered/selected feature across map viewers. */
20
+ export const HIGHLIGHT_COLOR = [255, 255, 255, 100];
19
21
  /** Distinct fill/line colors per geometry type. */
20
22
  export const GEOMETRY_COLORS = {
21
23
  point: { fill: [66, 133, 244, 180], line: [25, 103, 210, 220] },
@@ -85,7 +87,7 @@ function createLayerForResult(modules, result, layerId, onClick, onHover) {
85
87
  radiusMaxPixels: 12,
86
88
  pickable: true,
87
89
  autoHighlight: true,
88
- highlightColor: [255, 255, 255, 100],
90
+ highlightColor: HIGHLIGHT_COLOR,
89
91
  _validate: false,
90
92
  onHover,
91
93
  onClick: handleClick
@@ -101,7 +103,7 @@ function createLayerForResult(modules, result, layerId, onClick, onHover) {
101
103
  widthMinPixels: 1.5,
102
104
  pickable: true,
103
105
  autoHighlight: true,
104
- highlightColor: [255, 255, 255, 100],
106
+ highlightColor: HIGHLIGHT_COLOR,
105
107
  _validate: false,
106
108
  onHover,
107
109
  onClick: handleClick
@@ -117,7 +119,7 @@ function createLayerForResult(modules, result, layerId, onClick, onHover) {
117
119
  lineWidthMinPixels: 1.5,
118
120
  pickable: true,
119
121
  autoHighlight: true,
120
- highlightColor: [255, 255, 255, 100],
122
+ highlightColor: HIGHLIGHT_COLOR,
121
123
  _validate: false,
122
124
  onHover,
123
125
  onClick: handleClick
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Reactive media-query helpers for Svelte 5 runes.
3
+ *
4
+ * Usage (inside a component .svelte file):
5
+ * import { useIsWide } from '../utils/media-query.svelte.js';
6
+ * const isWide = useIsWide(); // true when viewport >= 640 px (Tailwind sm)
7
+ * // then: {#if isWide.value} ... {/if}
8
+ *
9
+ * SSR-safe: guards with `typeof window` (this is a CSR-only SPA, but be defensive).
10
+ */
11
+ /** Reactive wrapper around a single MediaQueryList. */
12
+ export declare function useIsWide(): {
13
+ readonly value: boolean;
14
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Reactive media-query helpers for Svelte 5 runes.
3
+ *
4
+ * Usage (inside a component .svelte file):
5
+ * import { useIsWide } from '../utils/media-query.svelte.js';
6
+ * const isWide = useIsWide(); // true when viewport >= 640 px (Tailwind sm)
7
+ * // then: {#if isWide.value} ... {/if}
8
+ *
9
+ * SSR-safe: guards with `typeof window` (this is a CSR-only SPA, but be defensive).
10
+ */
11
+ /** Reactive wrapper around a single MediaQueryList. */
12
+ export function useIsWide() {
13
+ let value = $state(typeof window !== 'undefined' ? window.matchMedia('(min-width: 640px)').matches : true);
14
+ if (typeof window !== 'undefined') {
15
+ const mq = window.matchMedia('(min-width: 640px)');
16
+ const handler = (e) => {
17
+ value = e.matches;
18
+ };
19
+ $effect(() => {
20
+ mq.addEventListener('change', handler);
21
+ return () => mq.removeEventListener('change', handler);
22
+ });
23
+ }
24
+ return {
25
+ get value() {
26
+ return value;
27
+ }
28
+ };
29
+ }
@@ -0,0 +1,7 @@
1
+ import type { Tab } from '../types.js';
2
+ /**
3
+ * Resolve a tab's signed HTTPS URL reactively for iframe-style viewers.
4
+ * Call inside a component's $effect; returns a cleanup function.
5
+ * onResolved runs only if the tab is still current (guards the async race).
6
+ */
7
+ export declare function resolveSignedTabUrl(tab: Tab, onResolved: (url: string) => void): () => void;
@@ -0,0 +1,19 @@
1
+ import { buildHttpsUrlAsync } from './signed-url.js';
2
+ /**
3
+ * Resolve a tab's signed HTTPS URL reactively for iframe-style viewers.
4
+ * Call inside a component's $effect; returns a cleanup function.
5
+ * onResolved runs only if the tab is still current (guards the async race).
6
+ */
7
+ export function resolveSignedTabUrl(tab, onResolved) {
8
+ let cancelled = false;
9
+ const id = tab.id;
10
+ (async () => {
11
+ const url = await buildHttpsUrlAsync(tab);
12
+ if (cancelled || id !== tab.id)
13
+ return;
14
+ onResolved(url);
15
+ })();
16
+ return () => {
17
+ cancelled = true;
18
+ };
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkthru-earth/objex",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Svelte 5 components and utilities for exploring geospatial object storage — S3, GCS, Azure, R2",
5
5
  "author": "Youssef Harby <yharby@walkthru.earth>",
6
6
  "license": "CC-BY-4.0",
@@ -172,7 +172,7 @@
172
172
  "sql-formatter": "^15.8.0",
173
173
  "yaml": "^2.9.0",
174
174
  "zarrita": "^0.7.3",
175
- "@walkthru-earth/objex-utils": "1.4.0"
175
+ "@walkthru-earth/objex-utils": "1.5.0"
176
176
  },
177
177
  "scripts": {
178
178
  "dev": "vite dev",