@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,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import XIcon from '@lucide/svelte/icons/x';
3
+ import { formatValue } from '../../../utils/format.js';
3
4
 
4
5
  let {
5
6
  feature = null,
@@ -10,12 +11,6 @@ let {
10
11
  visible?: boolean;
11
12
  onClose?: () => void;
12
13
  } = $props();
13
-
14
- function formatValue(value: any): string {
15
- if (value === null || value === undefined) return 'NULL';
16
- if (typeof value === 'object') return JSON.stringify(value);
17
- return String(value);
18
- }
19
14
  </script>
20
15
 
21
16
  {#if visible && feature}
@@ -8,6 +8,7 @@ import {
8
8
  ResizablePaneGroup
9
9
  } from '../../ui/resizable/index.js';
10
10
  import { t } from '../../../i18n/index.svelte.js';
11
+ import { formatFileSize } from '../../../utils/format.js';
11
12
  import type { PmtilesMetadata } from '../../../utils/pmtiles';
12
13
  import { highlightCode } from '../../../utils/shiki';
13
14
 
@@ -151,12 +152,7 @@ async function selectZoom(zoom: number) {
151
152
  loadingEntries = false;
152
153
  }
153
154
 
154
- function formatBytes(bytes: number): string {
155
- if (bytes === 0) return '0 B';
156
- const units = ['B', 'KB', 'MB', 'GB'];
157
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
158
- return `${(bytes / 1024 ** i).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
159
- }
155
+ const formatBytes = formatFileSize;
160
156
 
161
157
  const maxCount = $derived(Math.max(1, ...zoomSummaries.map((s) => s.count)));
162
158
  const dedupRatio = $derived(
@@ -2,14 +2,15 @@
2
2
  import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
3
3
  import XIcon from '@lucide/svelte/icons/x';
4
4
  import type { PMTiles } from 'pmtiles';
5
+ import { onDestroy } from 'svelte';
5
6
  import { t } from '../../../i18n/index.svelte.js';
7
+ import { formatFileSize } from '../../../utils/format.js';
6
8
  import type { PmtilesMetadata } from '../../../utils/pmtiles';
7
9
  import {
8
10
  type DecodedTile,
9
11
  decodeMvtTile,
10
12
  layerHue,
11
- tileMimeType,
12
- tileToImageUrl
13
+ tileMimeType
13
14
  } from '../../../utils/pmtiles-tile.js';
14
15
  import SvgTileRenderer from './SvgTileRenderer.svelte';
15
16
 
@@ -49,9 +50,11 @@ $effect(() => {
49
50
 
50
51
  let tile = $state<DecodedTile | null>(null);
51
52
  let rasterUrl = $state<string | null>(null);
53
+ let rasterDims = $state<{ width: number; height: number } | null>(null);
52
54
  let loading = $state(false);
53
55
  let error = $state<string | null>(null);
54
56
  let tileSize = $state(0);
57
+ let rasterZoom = $state(1);
55
58
 
56
59
  let selectedLayerName = $state<string | null>(null);
57
60
  let selectedFeatureIdx = $state<number | null>(null);
@@ -82,7 +85,13 @@ async function fetchTile() {
82
85
  loading = true;
83
86
  error = null;
84
87
  tile = null;
85
- rasterUrl = null;
88
+ // Revoke previous blob URL to prevent memory leak
89
+ if (rasterUrl) {
90
+ URL.revokeObjectURL(rasterUrl);
91
+ rasterUrl = null;
92
+ }
93
+ rasterDims = null;
94
+ rasterZoom = 1;
86
95
  selectedLayerName = null;
87
96
  selectedFeatureIdx = null;
88
97
 
@@ -100,14 +109,15 @@ async function fetchTile() {
100
109
  layerVisibility = vis;
101
110
  }
102
111
  } else {
103
- const mime = tileMimeType(metadata.format);
104
- const url = await tileToImageUrl(pmtiles, inputZ, inputX, inputY, mime);
105
- if (!url) {
112
+ const resp = await pmtiles.getZxy(inputZ, inputX, inputY);
113
+ if (!resp) {
106
114
  error = t('pmtiles.tileNotFound');
107
115
  } else {
108
- rasterUrl = url;
109
- const resp = await pmtiles.getZxy(inputZ, inputX, inputY);
110
- tileSize = resp ? new Uint8Array(resp.data).length : 0;
116
+ const bytes = new Uint8Array(resp.data);
117
+ tileSize = bytes.length;
118
+ const mime = tileMimeType(metadata.format);
119
+ const blob = new Blob([bytes], { type: mime });
120
+ rasterUrl = URL.createObjectURL(blob);
111
121
  }
112
122
  }
113
123
  } catch (e) {
@@ -154,12 +164,11 @@ const selectedFeatureKey = $derived(
154
164
  : null
155
165
  );
156
166
 
157
- function formatBytes(bytes: number): string {
158
- if (bytes === 0) return '0 B';
159
- const units = ['B', 'KB', 'MB'];
160
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
161
- return `${(bytes / 1024 ** i).toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
162
- }
167
+ onDestroy(() => {
168
+ if (rasterUrl) URL.revokeObjectURL(rasterUrl);
169
+ });
170
+
171
+ const formatBytes = formatFileSize;
163
172
 
164
173
  function formatValue(v: unknown): string {
165
174
  if (v === null || v === undefined) return 'NULL';
@@ -354,13 +363,78 @@ function formatValue(v: unknown): string {
354
363
  </div>
355
364
  {:else if rasterUrl}
356
365
  <!-- Raster tile preview -->
357
- <div class="flex flex-1 items-center justify-center bg-zinc-950 p-4">
358
- <img
359
- src={rasterUrl}
360
- alt="Tile {inputZ}/{inputX}/{inputY}"
361
- class="max-h-full max-w-full rounded border border-zinc-800"
362
- style="image-rendering: pixelated;"
363
- />
366
+ <div class="relative min-w-0 flex-1 overflow-hidden bg-zinc-950">
367
+ <!-- Checkerboard background for transparency -->
368
+ <div
369
+ class="absolute inset-0 flex items-center justify-center overflow-auto p-4"
370
+ style="background-image: repeating-conic-gradient(#1a1a2e 0% 25%, #16162a 0% 50%); background-size: 16px 16px;"
371
+ >
372
+ <img
373
+ src={rasterUrl}
374
+ alt="Tile {inputZ}/{inputX}/{inputY}"
375
+ class="rounded border border-zinc-800"
376
+ style="image-rendering: pixelated; width: {(rasterDims?.width ?? 256) * rasterZoom}px; height: {(rasterDims?.height ?? 256) * rasterZoom}px;"
377
+ onload={(e) => {
378
+ const img = e.currentTarget as HTMLImageElement;
379
+ rasterDims = { width: img.naturalWidth, height: img.naturalHeight };
380
+ }}
381
+ />
382
+ </div>
383
+ <!-- Zoom controls -->
384
+ <div class="absolute bottom-3 right-3 z-10 flex items-center gap-1 rounded bg-card/90 px-1.5 py-1 text-xs shadow backdrop-blur-sm">
385
+ <button
386
+ class="rounded px-1.5 py-0.5 text-card-foreground hover:bg-zinc-700/50 disabled:opacity-30"
387
+ onclick={() => (rasterZoom = Math.max(0.25, rasterZoom / 2))}
388
+ disabled={rasterZoom <= 0.25}
389
+ >−</button>
390
+ <span class="w-10 text-center tabular-nums text-muted-foreground">{Math.round(rasterZoom * 100)}%</span>
391
+ <button
392
+ class="rounded px-1.5 py-0.5 text-card-foreground hover:bg-zinc-700/50 disabled:opacity-30"
393
+ onclick={() => (rasterZoom = Math.min(8, rasterZoom * 2))}
394
+ disabled={rasterZoom >= 8}
395
+ >+</button>
396
+ <button
397
+ class="ms-1 rounded px-1.5 py-0.5 text-[10px] text-muted-foreground hover:bg-zinc-700/50 hover:text-card-foreground"
398
+ onclick={() => (rasterZoom = 1)}
399
+ >1:1</button>
400
+ </div>
401
+ </div>
402
+
403
+ <!-- Raster tile info panel -->
404
+ <div
405
+ class="flex w-56 shrink-0 flex-col border-s border-zinc-200 lg:w-64 dark:border-zinc-800"
406
+ >
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"
409
+ >
410
+ {t('pmtiles.tileInfo')}
411
+ </div>
412
+ <div class="flex-1 overflow-auto p-3">
413
+ <dl class="space-y-2 text-xs">
414
+ <div>
415
+ <dt class="text-muted-foreground">{t('pmtiles.tileCoordinates')}</dt>
416
+ <dd class="font-mono">{inputZ}/{inputX}/{inputY}</dd>
417
+ </div>
418
+ <div>
419
+ <dt class="text-muted-foreground">{t('mapInfo.tileFormat')}</dt>
420
+ <dd class="font-medium">{metadata.formatLabel}</dd>
421
+ </div>
422
+ {#if rasterDims}
423
+ <div>
424
+ <dt class="text-muted-foreground">{t('pmtiles.dimensions')}</dt>
425
+ <dd class="font-mono">{rasterDims.width} × {rasterDims.height}px</dd>
426
+ </div>
427
+ {/if}
428
+ <div>
429
+ <dt class="text-muted-foreground">{t('pmtiles.compressedSize')}</dt>
430
+ <dd>{formatBytes(tileSize)}</dd>
431
+ </div>
432
+ <div>
433
+ <dt class="text-muted-foreground">{t('mapInfo.tileCompression')}</dt>
434
+ <dd>{metadata.tileCompression}</dd>
435
+ </div>
436
+ </dl>
437
+ </div>
364
438
  </div>
365
439
  {:else}
366
440
  <div
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared constants used across the application.
3
+ * Centralizes magic strings, numbers, and sets to prevent duplication.
4
+ */
5
+ export declare const STORAGE_KEYS: {
6
+ readonly SETTINGS: "obstore-explore-settings";
7
+ readonly CONNECTIONS: "obstore-explore-connections";
8
+ readonly QUERY_HISTORY: "obstore-explore-query-history";
9
+ };
10
+ /** EPSG codes considered WGS84 (no reprojection needed). */
11
+ export declare const WGS84_CODES: Set<number>;
12
+ /** Default target CRS for ST_Transform. */
13
+ export declare const DEFAULT_TARGET_CRS = "EPSG:4326";
14
+ /** DuckDB-WASM initialization timeout in ms. */
15
+ export declare const DUCKDB_INIT_TIMEOUT_MS = 30000;
16
+ /** Maximum entries kept in query history. */
17
+ export declare const MAX_QUERY_HISTORY_ENTRIES = 200;
18
+ /** SQL preview truncation length (characters). */
19
+ export declare const SQL_PREVIEW_LENGTH = 120;
20
+ /** Extensions that represent "virtual files" — directories that open as viewers. */
21
+ export declare const VIEWER_DIR_EXTENSIONS: Set<string>;
22
+ /**
23
+ * Golden-angle-based hue multiplier for evenly distributing layer colors.
24
+ * 137 ≈ 360 × (1 − 1/φ) where φ is the golden ratio.
25
+ */
26
+ export declare const LAYER_HUE_MULTIPLIER = 137;
27
+ /** Duration (ms) to show "Copied!" feedback before resetting. */
28
+ export declare const COPY_FEEDBACK_MS = 2000;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared constants used across the application.
3
+ * Centralizes magic strings, numbers, and sets to prevent duplication.
4
+ */
5
+ // ── localStorage keys ──
6
+ export const STORAGE_KEYS = {
7
+ SETTINGS: 'obstore-explore-settings',
8
+ CONNECTIONS: 'obstore-explore-connections',
9
+ QUERY_HISTORY: 'obstore-explore-query-history'
10
+ };
11
+ // ── Geo / CRS constants ──
12
+ /** EPSG codes considered WGS84 (no reprojection needed). */
13
+ export const WGS84_CODES = new Set([4326, 4979]);
14
+ /** Default target CRS for ST_Transform. */
15
+ export const DEFAULT_TARGET_CRS = 'EPSG:4326';
16
+ // ── Query engine constants ──
17
+ /** DuckDB-WASM initialization timeout in ms. */
18
+ export const DUCKDB_INIT_TIMEOUT_MS = 30_000;
19
+ /** Maximum entries kept in query history. */
20
+ export const MAX_QUERY_HISTORY_ENTRIES = 200;
21
+ /** SQL preview truncation length (characters). */
22
+ export const SQL_PREVIEW_LENGTH = 120;
23
+ // ── File browser constants ──
24
+ /** Extensions that represent "virtual files" — directories that open as viewers. */
25
+ export const VIEWER_DIR_EXTENSIONS = new Set(['zarr', 'zr3']);
26
+ // ── PMTiles ──
27
+ /**
28
+ * Golden-angle-based hue multiplier for evenly distributing layer colors.
29
+ * 137 ≈ 360 × (1 − 1/φ) where φ is the golden ratio.
30
+ */
31
+ export const LAYER_HUE_MULTIPLIER = 137;
32
+ // ── Clipboard ──
33
+ /** Duration (ms) to show "Copied!" feedback before resetting. */
34
+ export const COPY_FEEDBACK_MS = 2000;
@@ -1016,6 +1016,12 @@ const EXTENSIONS = {
1016
1016
  * Extension may be with or without leading dot: ".parquet" or "parquet".
1017
1017
  */
1018
1018
  export function getFileTypeInfo(extension, isDir = false) {
1019
+ if (isDir && extension) {
1020
+ const ext = extension.startsWith('.') ? extension.toLowerCase() : `.${extension.toLowerCase()}`;
1021
+ const found = EXTENSIONS[ext];
1022
+ if (found)
1023
+ return found;
1024
+ }
1019
1025
  if (isDir)
1020
1026
  return FOLDER_INFO;
1021
1027
  const ext = extension.startsWith('.') ? extension.toLowerCase() : `.${extension.toLowerCase()}`;
package/dist/i18n/ar.js CHANGED
@@ -94,6 +94,8 @@ export const ar = {
94
94
  'fileBrowser.modified': 'التعديل',
95
95
  'fileBrowser.noMatch': 'لا توجد ملفات مطابقة لـ "{query}"',
96
96
  'fileBrowser.empty': 'هذا المجلد فارغ',
97
+ 'fileBrowser.zarrDetected': 'تم اكتشاف مخزن Zarr الإصدار {version}',
98
+ 'fileBrowser.openAsZarr': 'فتح كـ Zarr',
97
99
  // Search Bar
98
100
  'searchBar.label': 'تصفية الملفات',
99
101
  'searchBar.placeholder': 'تصفية الملفات...',
@@ -226,6 +228,27 @@ export const ar = {
226
228
  'zarr.inspect': 'فحص',
227
229
  'zarr.map': 'خريطة',
228
230
  'zarr.loading': 'جارٍ تحميل بيانات Zarr...',
231
+ 'zarr.nodes': 'عقدة',
232
+ 'zarr.group': 'مجموعة',
233
+ 'zarr.array': 'مصفوفة',
234
+ 'zarr.selectGroup': 'اختر مجموعة',
235
+ 'zarr.selectNode': 'اختر عقدة',
236
+ 'zarr.storeAttributes': 'سمات المخزن',
237
+ 'zarr.nodeType': 'النوع',
238
+ 'zarr.shape': 'الشكل',
239
+ 'zarr.dimensions': 'الأبعاد',
240
+ 'zarr.dtype': 'نوع البيانات',
241
+ 'zarr.fillValue': 'قيمة التعبئة',
242
+ 'zarr.chunks': 'الأجزاء',
243
+ 'zarr.chunkCount': 'عدد الأجزاء',
244
+ 'zarr.chunkSize': 'حجم الجزء',
245
+ 'zarr.uncompressed': 'غير مضغوط',
246
+ 'zarr.codecs': 'المرمّزات',
247
+ 'zarr.chunkKeys': 'مفاتيح الأجزاء',
248
+ 'zarr.children': 'العناصر الفرعية',
249
+ 'zarr.attributes': 'السمات',
250
+ 'zarr.format': 'صيغة Zarr',
251
+ 'zarr.contents': 'المحتويات',
229
252
  // Archive Viewer
230
253
  'archive.badge': 'أرشيف',
231
254
  'archive.entries': 'عناصر',
@@ -314,6 +337,7 @@ export const ar = {
314
337
  'map.layersPlural': 'طبقات',
315
338
  'map.tiles': 'بلاطات',
316
339
  'map.variable': 'المتغير:',
340
+ 'map.noValue': 'لا توجد قيمة',
317
341
  // Map info panel labels
318
342
  'mapInfo.name': 'الاسم',
319
343
  'mapInfo.description': 'الوصف',
@@ -376,6 +400,10 @@ export const ar = {
376
400
  'pmtiles.featureProperties': 'خصائص المعلم',
377
401
  'pmtiles.selectFeature': 'انقر على معلم لفحصه',
378
402
  'pmtiles.rasterPreview': 'معاينة نقطية',
403
+ 'pmtiles.tileInfo': 'معلومات البلاطة',
404
+ 'pmtiles.tileCoordinates': 'الإحداثيات',
405
+ 'pmtiles.dimensions': 'الأبعاد',
406
+ 'pmtiles.compressedSize': 'الحجم المضغوط',
379
407
  'pmtiles.noTileLoaded': 'أدخل Z/X/Y واجلب بلاطة',
380
408
  'pmtiles.parent': 'الأب',
381
409
  'pmtiles.featureCount': 'معالم',
@@ -399,6 +427,12 @@ export const ar = {
399
427
  'notebook.loading': 'جارٍ تحميل الدفتر...',
400
428
  'notebook.hideCode': 'إخفاء الكود',
401
429
  'notebook.showCode': 'إظهار الكود',
430
+ // About
431
+ 'about.title': 'حول objex',
432
+ 'about.version': 'الإصدار {version}',
433
+ 'about.license': 'الرخصة: CC BY 4.0',
434
+ 'about.sourceCode': 'الكود المصدري',
435
+ 'about.openSourceLicenses': 'تراخيص المصادر المفتوحة',
402
436
  // Locale
403
437
  'locale.toggle': 'اللغة'
404
438
  };
package/dist/i18n/en.js CHANGED
@@ -94,6 +94,8 @@ export const en = {
94
94
  'fileBrowser.modified': 'Modified',
95
95
  'fileBrowser.noMatch': 'No files matching "{query}"',
96
96
  'fileBrowser.empty': 'This folder is empty',
97
+ 'fileBrowser.zarrDetected': 'Zarr v{version} store detected',
98
+ 'fileBrowser.openAsZarr': 'Open as Zarr',
97
99
  // Search Bar
98
100
  'searchBar.label': 'Filter files',
99
101
  'searchBar.placeholder': 'Filter files...',
@@ -226,6 +228,27 @@ export const en = {
226
228
  'zarr.inspect': 'Inspect',
227
229
  'zarr.map': 'Map',
228
230
  'zarr.loading': 'Loading Zarr metadata...',
231
+ 'zarr.nodes': 'nodes',
232
+ 'zarr.group': 'group',
233
+ 'zarr.array': 'array',
234
+ 'zarr.selectGroup': 'Select a group',
235
+ 'zarr.selectNode': 'Select a node',
236
+ 'zarr.storeAttributes': 'Store Attributes',
237
+ 'zarr.nodeType': 'Type',
238
+ 'zarr.shape': 'Shape',
239
+ 'zarr.dimensions': 'Dimensions',
240
+ 'zarr.dtype': 'Data Type',
241
+ 'zarr.fillValue': 'Fill Value',
242
+ 'zarr.chunks': 'Chunks',
243
+ 'zarr.chunkCount': 'Chunk Count',
244
+ 'zarr.chunkSize': 'Chunk Size',
245
+ 'zarr.uncompressed': 'Uncompressed',
246
+ 'zarr.codecs': 'Codecs',
247
+ 'zarr.chunkKeys': 'Chunk Keys',
248
+ 'zarr.children': 'Children',
249
+ 'zarr.attributes': 'Attributes',
250
+ 'zarr.format': 'Zarr Format',
251
+ 'zarr.contents': 'Contents',
229
252
  // Archive Viewer
230
253
  'archive.badge': 'Archive',
231
254
  'archive.entries': 'entries',
@@ -314,6 +337,7 @@ export const en = {
314
337
  'map.layersPlural': 'layers',
315
338
  'map.tiles': 'tiles',
316
339
  'map.variable': 'Variable:',
340
+ 'map.noValue': 'No data',
317
341
  // Map info panel labels
318
342
  'mapInfo.name': 'Name',
319
343
  'mapInfo.description': 'Description',
@@ -376,6 +400,10 @@ export const en = {
376
400
  'pmtiles.featureProperties': 'Feature Properties',
377
401
  'pmtiles.selectFeature': 'Click a feature to inspect',
378
402
  'pmtiles.rasterPreview': 'Raster preview',
403
+ 'pmtiles.tileInfo': 'Tile Info',
404
+ 'pmtiles.tileCoordinates': 'Coordinates',
405
+ 'pmtiles.dimensions': 'Dimensions',
406
+ 'pmtiles.compressedSize': 'Compressed Size',
379
407
  'pmtiles.noTileLoaded': 'Enter Z/X/Y and fetch a tile',
380
408
  'pmtiles.parent': 'Parent',
381
409
  'pmtiles.featureCount': 'features',
@@ -399,6 +427,12 @@ export const en = {
399
427
  'notebook.loading': 'Loading notebook...',
400
428
  'notebook.hideCode': 'Hide Code',
401
429
  'notebook.showCode': 'Show Code',
430
+ // About
431
+ 'about.title': 'About objex',
432
+ 'about.version': 'Version {version}',
433
+ 'about.license': 'License: CC BY 4.0',
434
+ 'about.sourceCode': 'Source Code',
435
+ 'about.openSourceLicenses': 'Open Source Licenses',
402
436
  // Locale
403
437
  'locale.toggle': 'Language'
404
438
  };
package/dist/index.d.ts CHANGED
@@ -1,17 +1,29 @@
1
+ export { COPY_FEEDBACK_MS, DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, LAYER_HUE_MULTIPLIER, MAX_QUERY_HISTORY_ENTRIES, SQL_PREVIEW_LENGTH, STORAGE_KEYS, VIEWER_DIR_EXTENSIONS, WGS84_CODES } from './constants.js';
1
2
  export type { DuckDbReadFn, FileCategory, FileTypeInfo, ViewerKind } from './file-icons/index.js';
2
3
  export { buildDuckDbSource, getDuckDbReadFn, getFileTypeInfo, getMimeType, getViewerKind, isCloudNativeFormat, isQueryable } from './file-icons/index.js';
3
4
  export type { MapQueryHandle, MapQueryResult, QueryEngine, QueryHandle, QueryResult, SchemaField } from './query/engine.js';
4
5
  export { QueryCancelledError } from './query/engine.js';
5
6
  export type { ListPage, StorageAdapter } from './storage/adapter.js';
7
+ export type { ProviderDef, ProviderId, ProviderRegion } from './storage/providers.js';
8
+ export { buildEndpointFromTemplate, buildProviderBaseUrl, getProvider, isGcsProvider, PROVIDER_IDS, PROVIDERS } from './storage/providers.js';
6
9
  export { UrlAdapter } from './storage/url-adapter.js';
7
10
  export type { Connection, ConnectionConfig, FileEntry, Tab, Theme, WriteResult } from './types.js';
11
+ export { copyToClipboard, wireCodeCopyButtons } from './utils/clipboard.js';
12
+ export { getNativeScheme, resolveCloudUrl, safeDecodeURIComponent } from './utils/cloud-url.js';
8
13
  export type { TypeCategory } from './utils/column-types.js';
9
14
  export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
10
- export { formatDate, formatFileSize, getFileExtension } from './utils/format.js';
15
+ export { handleLoadError } from './utils/error.js';
16
+ export { escapeCsvField, serializeToCsv, serializeToJson } from './utils/export.js';
17
+ export type { SortConfig, SortDirection, SortField } from './utils/file-sort.js';
18
+ export { sortFileEntries, toggleSortField } from './utils/file-sort.js';
19
+ export { formatDate, formatFileSize, formatValue, getFileExtension, jsonReplacerBigInt } from './utils/format.js';
11
20
  export type { GeoArrowGeomType, GeoArrowResult } from './utils/geoarrow.js';
12
21
  export { buildGeoArrowTables, normalizeGeomType } from './utils/geoarrow.js';
13
22
  export type { HexRow } from './utils/hex.js';
14
23
  export { generateHexDump } from './utils/hex.js';
24
+ export { loadFromStorage, persistToStorage } from './utils/local-storage.js';
25
+ export type { ParsedMarkdownDocument, SqlBlock } from './utils/markdown-sql.js';
26
+ export { interpolateTemplates, markSqlBlocks, parseMarkdownDocument } from './utils/markdown-sql.js';
15
27
  export type { GeoColumnMeta, GeoParquetMeta, ParquetFileMetadata } from './utils/parquet-metadata.js';
16
28
  export { extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, readParquetMetadata } from './utils/parquet-metadata.js';
17
29
  export type { Defaults, ParsedStorageUrl, StorageProvider } from './utils/storage-url.js';
package/dist/index.js CHANGED
@@ -1,12 +1,27 @@
1
1
  // Core types
2
+ // Constants
3
+ export { COPY_FEEDBACK_MS, DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, LAYER_HUE_MULTIPLIER, MAX_QUERY_HISTORY_ENTRIES, SQL_PREVIEW_LENGTH, STORAGE_KEYS, VIEWER_DIR_EXTENSIONS, WGS84_CODES } from './constants.js';
2
4
  // File icons registry
3
5
  export { buildDuckDbSource, getDuckDbReadFn, getFileTypeInfo, getMimeType, getViewerKind, isCloudNativeFormat, isQueryable } from './file-icons/index.js';
4
6
  export { QueryCancelledError } from './query/engine.js';
7
+ export { buildEndpointFromTemplate, buildProviderBaseUrl, getProvider, isGcsProvider, PROVIDER_IDS, PROVIDERS } from './storage/providers.js';
5
8
  export { UrlAdapter } from './storage/url-adapter.js';
9
+ // Clipboard
10
+ export { copyToClipboard, wireCodeCopyButtons } from './utils/clipboard.js';
11
+ // Cloud URL resolution
12
+ export { getNativeScheme, resolveCloudUrl, safeDecodeURIComponent } from './utils/cloud-url.js';
6
13
  export { classifyType, typeBadgeClass, typeColor, typeLabel } from './utils/column-types.js';
7
- export { formatDate, formatFileSize, getFileExtension } from './utils/format.js';
14
+ // Error handling
15
+ export { handleLoadError } from './utils/error.js';
16
+ // Data export / serialization
17
+ export { escapeCsvField, serializeToCsv, serializeToJson } from './utils/export.js';
18
+ export { sortFileEntries, toggleSortField } from './utils/file-sort.js';
19
+ export { formatDate, formatFileSize, formatValue, getFileExtension, jsonReplacerBigInt } from './utils/format.js';
8
20
  export { buildGeoArrowTables, normalizeGeomType } from './utils/geoarrow.js';
9
21
  export { generateHexDump } from './utils/hex.js';
22
+ // localStorage helpers
23
+ export { loadFromStorage, persistToStorage } from './utils/local-storage.js';
24
+ export { interpolateTemplates, markSqlBlocks, parseMarkdownDocument } from './utils/markdown-sql.js';
10
25
  export { extractBounds, extractEpsgFromGeoMeta, extractGeometryTypes, readParquetMetadata } from './utils/parquet-metadata.js';
11
26
  export { describeParseResult, looksLikeUrl, parseStorageUrl } from './utils/storage-url.js';
12
27
  // Utilities
@@ -1,3 +1,4 @@
1
+ import { DEFAULT_TARGET_CRS, DUCKDB_INIT_TIMEOUT_MS, WGS84_CODES } from '../constants.js';
1
2
  import { buildDuckDbSource } from '../file-icons/index.js';
2
3
  import { credentialStore } from '../stores/credentials.svelte.js';
3
4
  import { QueryCancelledError } from './engine';
@@ -7,7 +8,7 @@ const duckdb_wasm = `${CDN_BASE}/duckdb-mvp.wasm`;
7
8
  const mvp_worker = `${CDN_BASE}/duckdb-browser-mvp.worker.js`;
8
9
  const duckdb_wasm_eh = `${CDN_BASE}/duckdb-eh.wasm`;
9
10
  const eh_worker = `${CDN_BASE}/duckdb-browser-eh.worker.js`;
10
- const INIT_TIMEOUT_MS = 30_000;
11
+ const INIT_TIMEOUT_MS = DUCKDB_INIT_TIMEOUT_MS; // Centralized in constants.ts
11
12
  // ─── Performance & diagnostic logging ────────────────────────────────
12
13
  const LOG_PREFIX = '[DuckDB]';
13
14
  function log(...args) {
@@ -107,7 +108,7 @@ async function ensureGeoConversionDisabled(conn) {
107
108
  await conn.query('SET enable_geoparquet_conversion = false');
108
109
  }
109
110
  // ─── CRS detection helpers ───────────────────────────────────────────
110
- const WGS84_CODES = new Set([4326, 4979]);
111
+ // WGS84_CODES imported from constants.ts
111
112
  /** Extract EPSG code from a PROJJSON object. Returns null for WGS84/CRS84. */
112
113
  function extractEpsgFromProjjson(crs) {
113
114
  if (!crs)
@@ -358,7 +359,7 @@ export class WasmQueryEngine {
358
359
  // always_xy := true forces lon/lat (x/y) axis order for both source and
359
360
  // target, matching the GeoParquet convention regardless of CRS authority.
360
361
  if (sourceCrs) {
361
- geomExpr = `ST_Transform(${geomExpr}, '${sourceCrs}', 'EPSG:4326', always_xy := true)`;
362
+ geomExpr = `ST_Transform(${geomExpr}, '${sourceCrs}', '${DEFAULT_TARGET_CRS}', always_xy := true)`;
362
363
  }
363
364
  // ST_AsWKB needed — DuckDB GEOMETRY columns (from ST_ReadSHP, ST_Read)
364
365
  // use an internal binary format, not WKB, even though Arrow reports Binary type.
@@ -761,7 +762,7 @@ export class WasmQueryEngine {
761
762
  ? `ST_GeomFromWKB(${quoted})`
762
763
  : `ST_GeomFromGeoJSON(${quoted})`;
763
764
  if (sourceCrs) {
764
- geomExpr = `ST_Transform(${geomExpr}, '${sourceCrs}', 'EPSG:4326', always_xy := true)`;
765
+ geomExpr = `ST_Transform(${geomExpr}, '${sourceCrs}', '${DEFAULT_TARGET_CRS}', always_xy := true)`;
765
766
  }
766
767
  wkbExpr = `ST_AsWKB(${geomExpr})`;
767
768
  }
@@ -20,6 +20,13 @@ export declare class BrowserCloudAdapter implements StorageAdapter {
20
20
  get supportsWrite(): boolean;
21
21
  private getConnection;
22
22
  listPage(path: string, continuationToken?: string, pageSize?: number, signal?: AbortSignal): Promise<ListPage>;
23
+ /**
24
+ * List via GCS JSON API — works for public buckets without CORS configuration.
25
+ * Endpoint: `storage.googleapis.com/storage/v1/b/{bucket}/o`
26
+ */
27
+ private listPageGcs;
28
+ /** List via S3-compatible XML API (AWS, R2, MinIO, Storj, etc.). */
29
+ private listPageS3;
23
30
  list(path: string): Promise<FileEntry[]>;
24
31
  read(path: string, offset?: number, length?: number, signal?: AbortSignal): Promise<Uint8Array>;
25
32
  head(path: string, signal?: AbortSignal): Promise<FileEntry>;
@@ -1,6 +1,7 @@
1
1
  import { AwsClient } from 'aws4fetch';
2
2
  import { connectionStore } from '../stores/connections.svelte.js';
3
3
  import { credentialStore } from '../stores/credentials.svelte.js';
4
+ import { buildProviderBaseUrl, isGcsProvider } from './providers.js';
4
5
  // --- Helpers ---
5
6
  /** Extract the last path segment from an object key. */
6
7
  function nameFromKey(key) {
@@ -17,15 +18,18 @@ function extensionFromName(name) {
17
18
  }
18
19
  /**
19
20
  * Build the base URL for S3-compatible API requests.
20
- * Uses path-style addressing (safer for buckets with dots in names).
21
+ * Delegates to the provider registry for endpoint resolution.
21
22
  */
22
23
  function buildBaseUrl(conn) {
23
- if (conn.endpoint) {
24
- const base = conn.endpoint.replace(/\/$/, '');
25
- return `${base}/${conn.bucket}`;
26
- }
27
- // Default AWS S3 path-style
28
- return `https://s3.${conn.region}.amazonaws.com/${conn.bucket}`;
24
+ return buildProviderBaseUrl(conn.provider, conn.endpoint, conn.bucket, conn.region);
25
+ }
26
+ /**
27
+ * Build the GCS JSON API base URL for a bucket.
28
+ * JSON API includes CORS headers (`Access-Control-Allow-Origin: *`) for public data,
29
+ * unlike the S3-compatible XML API which requires bucket-level CORS configuration.
30
+ */
31
+ function gcsJsonApiBase(bucket) {
32
+ return `https://storage.googleapis.com/storage/v1/b/${encodeURIComponent(bucket)}/o`;
29
33
  }
30
34
  /** Decode a possibly percent-encoded S3 key (some providers URL-encode non-ASCII in XML). */
31
35
  function decodeKey(key) {
@@ -103,6 +107,69 @@ export class BrowserCloudAdapter {
103
107
  }
104
108
  async listPage(path, continuationToken, pageSize, signal) {
105
109
  const conn = this.getConnection();
110
+ if (isGcsProvider(conn.provider, conn.endpoint) && conn.anonymous) {
111
+ return this.listPageGcs(conn, path, continuationToken, pageSize, signal);
112
+ }
113
+ return this.listPageS3(conn, path, continuationToken, pageSize, signal);
114
+ }
115
+ /**
116
+ * List via GCS JSON API — works for public buckets without CORS configuration.
117
+ * Endpoint: `storage.googleapis.com/storage/v1/b/{bucket}/o`
118
+ */
119
+ async listPageGcs(conn, path, pageToken, pageSize, signal) {
120
+ const url = gcsJsonApiBase(conn.bucket);
121
+ const params = new URLSearchParams({ delimiter: '/' });
122
+ if (path)
123
+ params.set('prefix', path);
124
+ if (pageToken)
125
+ params.set('pageToken', pageToken);
126
+ if (pageSize)
127
+ params.set('maxResults', String(pageSize));
128
+ const res = await fetch(`${url}?${params}`, { signal });
129
+ if (!res.ok) {
130
+ const body = await res.text().catch(() => '');
131
+ throw new Error(`GCS list failed (${res.status}): ${body || res.statusText}`);
132
+ }
133
+ const json = await res.json();
134
+ const entries = [];
135
+ // Directories (prefixes)
136
+ if (Array.isArray(json.prefixes)) {
137
+ for (const prefix of json.prefixes) {
138
+ const dirName = nameFromKey(prefix);
139
+ entries.push({
140
+ name: decodeKey(dirName),
141
+ path: prefix,
142
+ is_dir: true,
143
+ size: 0,
144
+ modified: 0,
145
+ extension: dirName.endsWith('.zarr') || dirName.endsWith('.zr3') ? 'zarr' : ''
146
+ });
147
+ }
148
+ }
149
+ // Files (items)
150
+ if (Array.isArray(json.items)) {
151
+ for (const item of json.items) {
152
+ const key = item.name ?? '';
153
+ if (!key || key === path || key.endsWith('/'))
154
+ continue;
155
+ const name = decodeKey(nameFromKey(key));
156
+ const size = parseInt(item.size ?? '0', 10);
157
+ const lastMod = item.updated ?? '';
158
+ entries.push({
159
+ name,
160
+ path: key,
161
+ is_dir: false,
162
+ size,
163
+ modified: lastMod ? Date.parse(lastMod) || 0 : 0,
164
+ extension: extensionFromName(name)
165
+ });
166
+ }
167
+ }
168
+ const nextToken = json.nextPageToken ?? undefined;
169
+ return { entries, continuationToken: nextToken, hasMore: !!nextToken };
170
+ }
171
+ /** List via S3-compatible XML API (AWS, R2, MinIO, Storj, etc.). */
172
+ async listPageS3(conn, path, continuationToken, pageSize, signal) {
106
173
  const baseUrl = buildBaseUrl(conn);
107
174
  const cloudFetch = this.getFetcher();
108
175
  const params = new URLSearchParams({