@walkthru-earth/objex 1.0.0 → 1.2.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 +11 -2
  2. package/dist/components/browser/FileBrowser.svelte +41 -54
  3. package/dist/components/browser/FileTreeSidebar.svelte +43 -7
  4. package/dist/components/layout/ConnectionDialog.svelte +100 -1
  5. package/dist/components/layout/Sidebar.svelte +43 -25
  6. package/dist/components/viewers/CodeViewer.svelte +23 -0
  7. package/dist/components/viewers/CogControls.svelte +208 -0
  8. package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
  9. package/dist/components/viewers/CogViewer.svelte +353 -1160
  10. package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
  11. package/dist/components/viewers/DatabaseViewer.svelte +345 -37
  12. package/dist/components/viewers/MarkdownViewer.svelte +1 -1
  13. package/dist/components/viewers/TableViewer.svelte +123 -41
  14. package/dist/components/viewers/ZarrMapViewer.svelte +29 -0
  15. package/dist/components/viewers/ZarrViewer.svelte +1 -4
  16. package/dist/constants.d.ts +6 -2
  17. package/dist/constants.js +6 -2
  18. package/dist/file-icons/index.d.ts +1 -1
  19. package/dist/file-icons/index.js +12 -2
  20. package/dist/i18n/ar.js +24 -0
  21. package/dist/i18n/en.js +24 -0
  22. package/dist/i18n/index.svelte.d.ts +0 -1
  23. package/dist/i18n/index.svelte.js +0 -3
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.js +10 -0
  26. package/dist/query/engine.d.ts +20 -4
  27. package/dist/query/index.d.ts +2 -1
  28. package/dist/query/index.js +1 -0
  29. package/dist/query/source.d.ts +30 -0
  30. package/dist/query/source.js +37 -0
  31. package/dist/query/wasm.d.ts +7 -5
  32. package/dist/query/wasm.js +138 -85
  33. package/dist/storage/providers.d.ts +47 -0
  34. package/dist/storage/providers.js +160 -0
  35. package/dist/stores/connections.svelte.js +5 -31
  36. package/dist/stores/files.svelte.d.ts +2 -8
  37. package/dist/stores/files.svelte.js +5 -38
  38. package/dist/stores/query-history.svelte.js +3 -25
  39. package/dist/stores/settings.svelte.d.ts +1 -0
  40. package/dist/stores/settings.svelte.js +10 -30
  41. package/dist/stores/tabs.svelte.d.ts +9 -2
  42. package/dist/stores/tabs.svelte.js +11 -2
  43. package/dist/types.d.ts +11 -0
  44. package/dist/utils/cloud-url.d.ts +27 -0
  45. package/dist/utils/cloud-url.js +61 -0
  46. package/dist/utils/cog.d.ts +244 -0
  47. package/dist/utils/cog.js +1039 -0
  48. package/dist/utils/deck.d.ts +0 -18
  49. package/dist/utils/deck.js +0 -36
  50. package/dist/utils/export.d.ts +22 -2
  51. package/dist/utils/export.js +35 -10
  52. package/dist/utils/file-sort.d.ts +20 -0
  53. package/dist/utils/file-sort.js +41 -0
  54. package/dist/utils/geometry-type.d.ts +52 -0
  55. package/dist/utils/geometry-type.js +76 -0
  56. package/dist/utils/local-storage.d.ts +16 -0
  57. package/dist/utils/local-storage.js +37 -0
  58. package/dist/utils/markdown-sql.d.ts +1 -1
  59. package/dist/utils/markdown-sql.js +3 -4
  60. package/dist/utils/pmtiles-tile.d.ts +0 -2
  61. package/dist/utils/pmtiles-tile.js +0 -8
  62. package/dist/utils/url-state.d.ts +6 -0
  63. package/dist/utils/url-state.js +34 -26
  64. package/dist/utils/url.d.ts +13 -25
  65. package/dist/utils/url.js +17 -78
  66. package/dist/utils/zarr-tab.d.ts +22 -0
  67. package/dist/utils/zarr-tab.js +30 -0
  68. package/dist/utils/zarr.d.ts +0 -2
  69. package/dist/utils/zarr.js +73 -44
  70. package/package.json +50 -46
  71. package/dist/components/ui/tabs/index.d.ts +0 -5
  72. package/dist/components/ui/tabs/index.js +0 -7
  73. package/dist/components/ui/tabs/tabs-content.svelte +0 -17
  74. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
  75. package/dist/components/ui/tabs/tabs-list.svelte +0 -16
  76. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
  77. package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
  78. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
  79. package/dist/components/ui/tabs/tabs.svelte +0 -19
  80. package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
  81. package/dist/components/viewers/MapViewer.svelte +0 -234
  82. package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
  83. package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
  84. package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
@@ -33,24 +33,6 @@ export declare function loadDeckModules(): Promise<{
33
33
  MapboxOverlay: typeof import("@deck.gl/mapbox").MapboxOverlay;
34
34
  GeoJsonLayer: typeof import("@deck.gl/layers").GeoJsonLayer;
35
35
  }>;
36
- export interface DeckOverlayOptions {
37
- layerId: string;
38
- data: GeoJSON.FeatureCollection;
39
- /** Called with properties and the full GeoJSON Feature (for selection highlight). */
40
- onClick?: (properties: Record<string, any>, feature: GeoJSON.Feature) => void;
41
- /** Layer-level onHover — use hoverCursor(map) to toggle pointer on MapLibre canvas. */
42
- onHover?: (info: {
43
- picked?: boolean;
44
- }) => void;
45
- }
46
- /**
47
- * Create a MapboxOverlay with a single GeoJsonLayer.
48
- * Colors are assigned per-feature based on geometry type.
49
- */
50
- export declare function createDeckOverlay(modules: {
51
- MapboxOverlay: any;
52
- GeoJsonLayer: any;
53
- }, options: DeckOverlayOptions): any;
54
36
  /** Lazy-load GeoArrow deck.gl layers + MapboxOverlay + GeoJsonLayer (for selection). */
55
37
  export declare function loadGeoArrowModules(): Promise<{
56
38
  GeoArrowArcLayer: typeof import("@geoarrow/deck.gl-layers").GeoArrowArcLayer;
@@ -46,42 +46,6 @@ export async function loadDeckModules() {
46
46
  ]);
47
47
  return { MapboxOverlay, GeoJsonLayer };
48
48
  }
49
- /**
50
- * Create a MapboxOverlay with a single GeoJsonLayer.
51
- * Colors are assigned per-feature based on geometry type.
52
- */
53
- export function createDeckOverlay(modules, options) {
54
- const { MapboxOverlay, GeoJsonLayer } = modules;
55
- const { layerId, data, onClick, onHover } = options;
56
- return new MapboxOverlay({
57
- interleaved: false,
58
- layers: [
59
- new GeoJsonLayer({
60
- id: layerId,
61
- data,
62
- pickable: true,
63
- stroked: true,
64
- filled: true,
65
- pointType: 'circle',
66
- getFillColor: geojsonFillColor,
67
- getLineColor: geojsonLineColor,
68
- getPointRadius: 6,
69
- getLineWidth: 2.5,
70
- lineWidthMinPixels: 1.5,
71
- pointRadiusMinPixels: 4,
72
- pointRadiusMaxPixels: 12,
73
- autoHighlight: true,
74
- highlightColor: [255, 255, 255, 100],
75
- onHover,
76
- onClick: (info) => {
77
- if (info.object?.properties && onClick) {
78
- onClick({ ...info.object.properties }, info.object);
79
- }
80
- }
81
- })
82
- ]
83
- });
84
- }
85
49
  // ─── GeoArrow overlay (GeoParquetMapViewer) ──────────────────────────
86
50
  /** Lazy-load GeoArrow deck.gl layers + MapboxOverlay + GeoJsonLayer (for selection). */
87
51
  export async function loadGeoArrowModules() {
@@ -1,2 +1,22 @@
1
- export declare function exportToCsv(columns: string[], rows: Record<string, any>[], filename: string): void;
2
- export declare function exportToJson(columns: string[], rows: Record<string, any>[], filename: string): void;
1
+ /**
2
+ * Escape a CSV field value per RFC 4180.
3
+ */
4
+ export declare function escapeCsvField(value: string): string;
5
+ /**
6
+ * Serialize column/row data to a CSV string.
7
+ * Pure function — no browser APIs, works in Node.js.
8
+ */
9
+ export declare function serializeToCsv(columns: string[], rows: Record<string, unknown>[]): string;
10
+ /**
11
+ * Serialize column/row data to a formatted JSON string.
12
+ * Pure function — no browser APIs, works in Node.js.
13
+ */
14
+ export declare function serializeToJson(columns: string[], rows: Record<string, unknown>[]): string;
15
+ /**
16
+ * Export data as CSV file (triggers browser download).
17
+ */
18
+ export declare function exportToCsv(columns: string[], rows: Record<string, unknown>[], filename: string): void;
19
+ /**
20
+ * Export data as JSON file (triggers browser download).
21
+ */
22
+ export declare function exportToJson(columns: string[], rows: Record<string, unknown>[], filename: string): void;
@@ -1,3 +1,4 @@
1
+ import { jsonReplacerBigInt } from './format.js';
1
2
  function triggerDownload(content, filename, mimeType) {
2
3
  const blob = new Blob([content], { type: mimeType });
3
4
  const url = URL.createObjectURL(blob);
@@ -9,30 +10,43 @@ function triggerDownload(content, filename, mimeType) {
9
10
  document.body.removeChild(a);
10
11
  URL.revokeObjectURL(url);
11
12
  }
12
- function formatValue(value) {
13
+ /** Format a cell value for export (empty string for null/undefined). */
14
+ function formatCellValue(value) {
13
15
  if (value === null || value === undefined)
14
16
  return '';
15
17
  if (value instanceof Date)
16
18
  return value.toISOString();
19
+ if (typeof value === 'bigint')
20
+ return value.toString();
17
21
  if (typeof value === 'object')
18
- return JSON.stringify(value);
22
+ return JSON.stringify(value, jsonReplacerBigInt);
19
23
  return String(value);
20
24
  }
21
- function escapeCsvField(value) {
25
+ /**
26
+ * Escape a CSV field value per RFC 4180.
27
+ */
28
+ export function escapeCsvField(value) {
22
29
  if (value.includes(',') || value.includes('"') || value.includes('\n') || value.includes('\r')) {
23
30
  return `"${value.replace(/"/g, '""')}"`;
24
31
  }
25
32
  return value;
26
33
  }
27
- export function exportToCsv(columns, rows, filename) {
34
+ /**
35
+ * Serialize column/row data to a CSV string.
36
+ * Pure function — no browser APIs, works in Node.js.
37
+ */
38
+ export function serializeToCsv(columns, rows) {
28
39
  const header = columns.map(escapeCsvField).join(',');
29
40
  const body = rows
30
- .map((row) => columns.map((col) => escapeCsvField(formatValue(row[col]))).join(','))
41
+ .map((row) => columns.map((col) => escapeCsvField(formatCellValue(row[col]))).join(','))
31
42
  .join('\n');
32
- const csv = `${header}\n${body}`;
33
- triggerDownload(csv, filename.endsWith('.csv') ? filename : `${filename}.csv`, 'text/csv;charset=utf-8;');
43
+ return `${header}\n${body}`;
34
44
  }
35
- export function exportToJson(columns, rows, filename) {
45
+ /**
46
+ * Serialize column/row data to a formatted JSON string.
47
+ * Pure function — no browser APIs, works in Node.js.
48
+ */
49
+ export function serializeToJson(columns, rows) {
36
50
  const data = rows.map((row) => {
37
51
  const obj = {};
38
52
  for (const col of columns) {
@@ -46,6 +60,17 @@ export function exportToJson(columns, rows, filename) {
46
60
  }
47
61
  return obj;
48
62
  });
49
- const json = JSON.stringify(data, null, 2);
50
- triggerDownload(json, filename.endsWith('.json') ? filename : `${filename}.json`, 'application/json');
63
+ return JSON.stringify(data, jsonReplacerBigInt, 2);
64
+ }
65
+ /**
66
+ * Export data as CSV file (triggers browser download).
67
+ */
68
+ export function exportToCsv(columns, rows, filename) {
69
+ triggerDownload(serializeToCsv(columns, rows), filename.endsWith('.csv') ? filename : `${filename}.csv`, 'text/csv;charset=utf-8;');
70
+ }
71
+ /**
72
+ * Export data as JSON file (triggers browser download).
73
+ */
74
+ export function exportToJson(columns, rows, filename) {
75
+ triggerDownload(serializeToJson(columns, rows), filename.endsWith('.json') ? filename : `${filename}.json`, 'application/json');
51
76
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pure file entry sorting — framework-agnostic, works in Node.js.
3
+ */
4
+ import type { FileEntry } from '../types.js';
5
+ export type SortField = 'name' | 'size' | 'modified' | 'extension';
6
+ export type SortDirection = 'asc' | 'desc';
7
+ export interface SortConfig {
8
+ field: SortField;
9
+ direction: SortDirection;
10
+ }
11
+ /**
12
+ * Sort file entries by the given config.
13
+ * Directories always sort before files regardless of sort field.
14
+ * Returns a new array (does not mutate the input).
15
+ */
16
+ export declare function sortFileEntries(entries: FileEntry[], config: SortConfig): FileEntry[];
17
+ /**
18
+ * Toggle sort config: same field flips direction, new field starts ascending.
19
+ */
20
+ export declare function toggleSortField(current: SortConfig, field: SortField): SortConfig;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Pure file entry sorting — framework-agnostic, works in Node.js.
3
+ */
4
+ /**
5
+ * Sort file entries by the given config.
6
+ * Directories always sort before files regardless of sort field.
7
+ * Returns a new array (does not mutate the input).
8
+ */
9
+ export function sortFileEntries(entries, config) {
10
+ const sorted = [...entries];
11
+ const dir = config.direction === 'asc' ? 1 : -1;
12
+ sorted.sort((a, b) => {
13
+ // Directories always come first
14
+ if (a.is_dir && !b.is_dir)
15
+ return -1;
16
+ if (!a.is_dir && b.is_dir)
17
+ return 1;
18
+ switch (config.field) {
19
+ case 'name':
20
+ return dir * a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
21
+ case 'size':
22
+ return dir * (a.size - b.size);
23
+ case 'modified':
24
+ return dir * (a.modified - b.modified);
25
+ case 'extension':
26
+ return dir * a.extension.localeCompare(b.extension, undefined, { sensitivity: 'base' });
27
+ default:
28
+ return 0;
29
+ }
30
+ });
31
+ return sorted;
32
+ }
33
+ /**
34
+ * Toggle sort config: same field flips direction, new field starts ascending.
35
+ */
36
+ export function toggleSortField(current, field) {
37
+ if (current.field === field) {
38
+ return { field, direction: current.direction === 'asc' ? 'desc' : 'asc' };
39
+ }
40
+ return { field, direction: 'asc' };
41
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Helpers for parsing DuckDB v1.5 parameterized GEOMETRY type strings.
3
+ *
4
+ * DuckDB v1.5 made GEOMETRY a core type with an optional CRS parameter:
5
+ * GEOMETRY — no CRS attached
6
+ * GEOMETRY('EPSG:4326') — EPSG form
7
+ * GEOMETRY('OGC:CRS84') — OGC form (canonical for GeoParquet 1.1+)
8
+ * GEOMETRY('EPSG:27700') — projected CRS
9
+ *
10
+ * Type strings may come from `DESCRIBE`, from the Arrow schema, or from a
11
+ * legacy code path that still reports `BLOB`. Use these helpers everywhere
12
+ * instead of ad-hoc regex so behaviour stays consistent.
13
+ */
14
+ export interface GeometryTypeInfo {
15
+ /** True if the type is some form of GEOMETRY (with or without CRS). */
16
+ isGeometry: boolean;
17
+ /** True if the type carries a CRS parameter, e.g. GEOMETRY('EPSG:4326'). */
18
+ hasCrs: boolean;
19
+ /** The CRS string if present, otherwise null. Raw value, including WGS84. */
20
+ rawCrs: string | null;
21
+ /**
22
+ * The CRS string if present and NOT a WGS84 variant (EPSG:4326, EPSG:4979,
23
+ * OGC:CRS84). Returns null for WGS84 so callers can skip ST_Transform.
24
+ */
25
+ nonWgs84Crs: string | null;
26
+ }
27
+ /**
28
+ * Parse a DuckDB type string and report whether it is a GEOMETRY type, and
29
+ * whether a CRS parameter is attached.
30
+ */
31
+ export declare function parseGeometryTypeCrs(typeStr: string | null | undefined): GeometryTypeInfo;
32
+ /** True for EPSG:4326, EPSG:4979, OGC:CRS84 and equivalent strings. */
33
+ export declare function isWgs84Crs(crs: string | null | undefined): boolean;
34
+ /**
35
+ * Build a `ST_Transform(...)` SQL expression choosing the 2-arg form when the
36
+ * input already carries its CRS in the GEOMETRY type (DuckDB v1.5), and the
37
+ * 3-arg form otherwise.
38
+ *
39
+ * `geometry_always_xy` is set globally at DB init, so no per-call `always_xy`
40
+ * argument is needed.
41
+ */
42
+ export declare function buildTransformExpr(innerExpr: string, sourceType: string, sourceCrs: string, targetCrs: string): string;
43
+ /**
44
+ * Wrap a bare WKB expression with `ST_SetCRS(ST_GeomFromWKB(...))` so that the
45
+ * resulting GEOMETRY value carries a CRS through the rest of the pipeline.
46
+ * Used in the legacy GeoParquet fallback where we read the geometry column as
47
+ * BLOB but still know the source CRS from hyparquet metadata or the GeoParquet
48
+ * footer.
49
+ *
50
+ * If `sourceCrs` is null/empty, returns a plain `ST_GeomFromWKB(...)`.
51
+ */
52
+ export declare function wrapWkbWithCrs(wkbExpr: string, sourceCrs: string | null | undefined): string;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Helpers for parsing DuckDB v1.5 parameterized GEOMETRY type strings.
3
+ *
4
+ * DuckDB v1.5 made GEOMETRY a core type with an optional CRS parameter:
5
+ * GEOMETRY — no CRS attached
6
+ * GEOMETRY('EPSG:4326') — EPSG form
7
+ * GEOMETRY('OGC:CRS84') — OGC form (canonical for GeoParquet 1.1+)
8
+ * GEOMETRY('EPSG:27700') — projected CRS
9
+ *
10
+ * Type strings may come from `DESCRIBE`, from the Arrow schema, or from a
11
+ * legacy code path that still reports `BLOB`. Use these helpers everywhere
12
+ * instead of ad-hoc regex so behaviour stays consistent.
13
+ */
14
+ import { WGS84_CODES } from '../constants.js';
15
+ const GEOMETRY_PREFIX = /^GEOMETRY(\s*\(\s*'?([^')]+)'?\s*\))?/i;
16
+ /**
17
+ * Parse a DuckDB type string and report whether it is a GEOMETRY type, and
18
+ * whether a CRS parameter is attached.
19
+ */
20
+ export function parseGeometryTypeCrs(typeStr) {
21
+ if (!typeStr)
22
+ return { isGeometry: false, hasCrs: false, rawCrs: null, nonWgs84Crs: null };
23
+ const match = typeStr.match(GEOMETRY_PREFIX);
24
+ if (!match)
25
+ return { isGeometry: false, hasCrs: false, rawCrs: null, nonWgs84Crs: null };
26
+ const rawCrs = match[2]?.trim() ?? null;
27
+ if (!rawCrs)
28
+ return { isGeometry: true, hasCrs: false, rawCrs: null, nonWgs84Crs: null };
29
+ return {
30
+ isGeometry: true,
31
+ hasCrs: true,
32
+ rawCrs,
33
+ nonWgs84Crs: isWgs84Crs(rawCrs) ? null : rawCrs
34
+ };
35
+ }
36
+ /** True for EPSG:4326, EPSG:4979, OGC:CRS84 and equivalent strings. */
37
+ export function isWgs84Crs(crs) {
38
+ if (!crs)
39
+ return true;
40
+ const trimmed = crs.trim();
41
+ if (trimmed === 'OGC:CRS84' || trimmed === 'OGC:CRS83')
42
+ return true;
43
+ const epsgMatch = trimmed.match(/^EPSG:(\d+)$/i);
44
+ if (epsgMatch && WGS84_CODES.has(Number(epsgMatch[1])))
45
+ return true;
46
+ return false;
47
+ }
48
+ /**
49
+ * Build a `ST_Transform(...)` SQL expression choosing the 2-arg form when the
50
+ * input already carries its CRS in the GEOMETRY type (DuckDB v1.5), and the
51
+ * 3-arg form otherwise.
52
+ *
53
+ * `geometry_always_xy` is set globally at DB init, so no per-call `always_xy`
54
+ * argument is needed.
55
+ */
56
+ export function buildTransformExpr(innerExpr, sourceType, sourceCrs, targetCrs) {
57
+ const info = parseGeometryTypeCrs(sourceType);
58
+ if (info.hasCrs) {
59
+ return `ST_Transform(${innerExpr}, '${targetCrs}')`;
60
+ }
61
+ return `ST_Transform(${innerExpr}, '${sourceCrs}', '${targetCrs}')`;
62
+ }
63
+ /**
64
+ * Wrap a bare WKB expression with `ST_SetCRS(ST_GeomFromWKB(...))` so that the
65
+ * resulting GEOMETRY value carries a CRS through the rest of the pipeline.
66
+ * Used in the legacy GeoParquet fallback where we read the geometry column as
67
+ * BLOB but still know the source CRS from hyparquet metadata or the GeoParquet
68
+ * footer.
69
+ *
70
+ * If `sourceCrs` is null/empty, returns a plain `ST_GeomFromWKB(...)`.
71
+ */
72
+ export function wrapWkbWithCrs(wkbExpr, sourceCrs) {
73
+ if (!sourceCrs)
74
+ return `ST_GeomFromWKB(${wkbExpr})`;
75
+ return `ST_SetCRS(ST_GeomFromWKB(${wkbExpr}), '${sourceCrs}')`;
76
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Generic localStorage helpers with SSR safety.
3
+ *
4
+ * Used by connection, settings, and query-history stores to avoid
5
+ * repeating the same load/persist/try-catch/SSR-guard pattern.
6
+ */
7
+ /**
8
+ * Load a JSON value from localStorage.
9
+ * Returns `defaultValue` on SSR, missing key, or parse error.
10
+ */
11
+ export declare function loadFromStorage<T>(key: string, defaultValue: T): T;
12
+ /**
13
+ * Persist a JSON-serializable value to localStorage.
14
+ * Silently no-ops on SSR or storage errors (quota, private browsing).
15
+ */
16
+ export declare function persistToStorage(key: string, value: unknown): void;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Generic localStorage helpers with SSR safety.
3
+ *
4
+ * Used by connection, settings, and query-history stores to avoid
5
+ * repeating the same load/persist/try-catch/SSR-guard pattern.
6
+ */
7
+ /**
8
+ * Load a JSON value from localStorage.
9
+ * Returns `defaultValue` on SSR, missing key, or parse error.
10
+ */
11
+ export function loadFromStorage(key, defaultValue) {
12
+ if (typeof window === 'undefined')
13
+ return defaultValue;
14
+ try {
15
+ const raw = localStorage.getItem(key);
16
+ if (raw)
17
+ return JSON.parse(raw);
18
+ }
19
+ catch {
20
+ // ignore parse errors
21
+ }
22
+ return defaultValue;
23
+ }
24
+ /**
25
+ * Persist a JSON-serializable value to localStorage.
26
+ * Silently no-ops on SSR or storage errors (quota, private browsing).
27
+ */
28
+ export function persistToStorage(key, value) {
29
+ if (typeof window === 'undefined')
30
+ return;
31
+ try {
32
+ localStorage.setItem(key, JSON.stringify(value));
33
+ }
34
+ catch {
35
+ // ignore storage errors
36
+ }
37
+ }
@@ -17,7 +17,7 @@ export interface ParsedMarkdownDocument {
17
17
  * SELECT * FROM table
18
18
  * ```
19
19
  */
20
- export declare function parseMarkdownDocument(markdown: string): ParsedMarkdownDocument;
20
+ export declare function parseMarkdownDocument(markdown: string): Promise<ParsedMarkdownDocument>;
21
21
  /**
22
22
  * Interpolate template variables in markdown text.
23
23
  * Supports {queryName.rows[0].columnName} syntax.
@@ -1,4 +1,3 @@
1
- import YAML from 'yaml';
2
1
  /**
3
2
  * Parse a markdown document with YAML frontmatter and SQL code blocks.
4
3
  *
@@ -7,17 +6,17 @@ import YAML from 'yaml';
7
6
  * SELECT * FROM table
8
7
  * ```
9
8
  */
10
- export function parseMarkdownDocument(markdown) {
9
+ export async function parseMarkdownDocument(markdown) {
11
10
  let frontmatter = {};
12
11
  let content = markdown;
13
- // Extract YAML frontmatter
14
12
  const fmMatch = markdown.match(/^---\n([\s\S]*?)\n---\n/);
15
13
  if (fmMatch) {
16
14
  try {
15
+ const { default: YAML } = await import('yaml');
17
16
  frontmatter = YAML.parse(fmMatch[1]) || {};
18
17
  }
19
18
  catch {
20
- // Invalid YAML ignore
19
+ // Invalid YAML or yaml peer dep not installed, ignore frontmatter
21
20
  }
22
21
  content = markdown.slice(fmMatch[0].length);
23
22
  }
@@ -30,8 +30,6 @@ export interface DecodedFeature {
30
30
  * Returns null if the tile does not exist or for raster archives.
31
31
  */
32
32
  export declare function decodeMvtTile(pmtiles: PMTiles, z: number, x: number, y: number): Promise<DecodedTile | null>;
33
- /** Convert tile bytes to a Blob URL for raster tile preview. */
34
- export declare function tileToImageUrl(pmtiles: PMTiles, z: number, x: number, y: number, mimeType: string): Promise<string | null>;
35
33
  /** MIME type for a PMTiles tile format string. */
36
34
  export declare function tileMimeType(format: string): string;
37
35
  /** Compute the hue for layer index i (same palette as buildPmtilesLayers). */
@@ -41,14 +41,6 @@ export async function decodeMvtTile(pmtiles, z, x, y) {
41
41
  }
42
42
  return { z, x, y, layers, rawSize };
43
43
  }
44
- /** Convert tile bytes to a Blob URL for raster tile preview. */
45
- export async function tileToImageUrl(pmtiles, z, x, y, mimeType) {
46
- const resp = await pmtiles.getZxy(z, x, y);
47
- if (!resp)
48
- return null;
49
- const blob = new Blob([resp.data], { type: mimeType });
50
- return URL.createObjectURL(blob);
51
- }
52
44
  /** MIME type for a PMTiles tile format string. */
53
45
  export function tileMimeType(format) {
54
46
  const map = {
@@ -34,6 +34,12 @@ export declare function getUrlView(): string;
34
34
  * Read the prefix (file/folder path) from the ?url= param.
35
35
  */
36
36
  export declare function getUrlPrefix(): string;
37
+ /**
38
+ * True when a `?url=` param is present. Single source of truth, used by
39
+ * the tab-sync effect and Sidebar auto-detection to decide whether an
40
+ * auto-migration is in progress (see `+page.svelte` tab-sync effect).
41
+ */
42
+ export declare function hasUrlParam(): boolean;
37
43
  /**
38
44
  * Clear all URL state params.
39
45
  */
@@ -28,43 +28,43 @@ export function buildUrlParam(conn, prefix) {
28
28
  return `${base}/${prefix.replace(/^\//, '')}`;
29
29
  }
30
30
  /**
31
- * Set the ?url= param to a raw URL string (for direct URL tabs).
31
+ * Apply a URL mutation, skipping `replaceState` if nothing changed.
32
+ * Every public mutator below funnels through this to avoid `replaceState`
33
+ * thrash when the tab-sync effect re-fires on unrelated reactive changes.
32
34
  */
33
- export function setRawUrlParam(rawUrl) {
35
+ function writeLocation(mutate) {
34
36
  try {
35
37
  const url = new URL(window.location.href);
36
- url.searchParams.set('url', rawUrl);
37
- replaceState(url.pathname + url.search + url.hash, {});
38
+ const before = url.pathname + url.search + url.hash;
39
+ mutate(url);
40
+ const after = url.pathname + url.search + url.hash;
41
+ if (before === after)
42
+ return;
43
+ replaceState(after, {});
38
44
  }
39
45
  catch {
40
46
  /* ignore */
41
47
  }
42
48
  }
49
+ /**
50
+ * Set the ?url= param to a raw URL string (for direct URL tabs).
51
+ */
52
+ export function setRawUrlParam(rawUrl) {
53
+ writeLocation((url) => url.searchParams.set('url', rawUrl));
54
+ }
43
55
  /**
44
56
  * Sync the ?url= param in the browser URL.
45
57
  */
46
58
  export function syncUrlParam(conn, prefix) {
47
- try {
48
- const url = new URL(window.location.href);
49
- url.searchParams.set('url', buildUrlParam(conn, prefix));
50
- replaceState(url.pathname + url.search + url.hash, {});
51
- }
52
- catch {
53
- /* ignore */
54
- }
59
+ writeLocation((url) => url.searchParams.set('url', buildUrlParam(conn, prefix)));
55
60
  }
56
61
  /**
57
62
  * Update the #hash in the URL to reflect the current view mode.
58
63
  */
59
64
  export function updateUrlView(view) {
60
- try {
61
- const url = new URL(window.location.href);
65
+ writeLocation((url) => {
62
66
  url.hash = view || '';
63
- replaceState(url.pathname + url.search + url.hash, {});
64
- }
65
- catch {
66
- /* ignore */
67
- }
67
+ });
68
68
  }
69
69
  /**
70
70
  * Read the current #hash view mode from the URL.
@@ -93,16 +93,24 @@ export function getUrlPrefix() {
93
93
  }
94
94
  }
95
95
  /**
96
- * Clear all URL state params.
96
+ * True when a `?url=` param is present. Single source of truth, used by
97
+ * the tab-sync effect and Sidebar auto-detection to decide whether an
98
+ * auto-migration is in progress (see `+page.svelte` tab-sync effect).
97
99
  */
98
- export function clearUrlState() {
100
+ export function hasUrlParam() {
99
101
  try {
100
- const url = new URL(window.location.href);
101
- url.searchParams.delete('url');
102
- url.hash = '';
103
- replaceState(url.pathname + url.search, {});
102
+ return new URL(window.location.href).searchParams.has('url');
104
103
  }
105
104
  catch {
106
- /* ignore */
105
+ return false;
107
106
  }
108
107
  }
108
+ /**
109
+ * Clear all URL state params.
110
+ */
111
+ export function clearUrlState() {
112
+ writeLocation((url) => {
113
+ url.searchParams.delete('url');
114
+ url.hash = '';
115
+ });
116
+ }
@@ -4,38 +4,26 @@ import type { Tab } from '../types.js';
4
4
  * Works for any viewer that needs an HTTP-accessible URL (COG, PMTiles, Zarr, etc.)
5
5
  */
6
6
  export declare function buildHttpsUrl(tab: Tab): string;
7
- /**
8
- * Map provider to its native URI scheme prefix.
9
- * Derived from the registry's `schemes` array (first entry is the primary scheme).
10
- * Falls back to 's3' for providers without a scheme (S3-compatible).
11
- */
12
- export declare function getNativeScheme(provider: string): string;
13
7
  /**
14
8
  * Build a provider-native protocol URL (s3://bucket/path, sj://bucket/path, etc.).
15
9
  */
16
10
  export declare function buildStorageUrl(tab: Tab): string;
17
11
  /**
18
- * Build the URL that DuckDB should use for queries.
19
- * - Azure: always HTTPS URL with SAS token appended
20
- * - Anonymous with custom endpoint (Storj, R2, etc.): HTTPS URL — no S3 config needed,
21
- * avoids endpoint/auth complexity, works directly via httpfs
22
- * - AWS S3 (no endpoint): s3:// DuckDB routes via configured region
23
- * - Authenticated with endpoint: s3:// needs S3 endpoint config for SigV4 signing
12
+ * Build the URL DuckDB should query. Derived from the connection's access mode:
13
+ *
14
+ * | Access mode | DuckDB URL | Why |
15
+ * |-----------------|-----------------------------------|-------------------------------------------|
16
+ * | `sas-https` | HTTPS with SAS token | No DuckDB Azure support; SAS in URL works |
17
+ * | `public-https` | HTTPS (no auth) | httpfs fetches directly, no signing needed|
18
+ * | `signed-s3` | `s3://bucket/key` | DuckDB signs with configured S3 settings |
19
+ *
20
+ * Path is percent-decoded so DuckDB's httpfs doesn't double-encode
21
+ * (e.g. Arabic filenames `%D9%85` → `%25D9%2585`).
24
22
  */
25
23
  export declare function buildDuckDbUrl(tab: Tab): string;
26
24
  /**
27
- * Check if a tab's file can be loaded directly via HTTPS URL (streaming).
28
- * True for URL-sourced tabs, anonymous buckets, and Azure (SAS token in URL).
29
- * False for authenticated S3 (needs signed URLs or blob download via adapter).
25
+ * True when any HTTP client (fetch/img/video/deck.gl/COG/Zarr/PMTiles) can
26
+ * load the tab's file directly via its HTTPS URL. False when SigV4 signing
27
+ * is required and the viewer must go through the storage adapter instead.
30
28
  */
31
29
  export declare function canStreamDirectly(tab: Tab): boolean;
32
- /**
33
- * Convert a cloud storage protocol URL (s3://, gs://) to an HTTPS URL
34
- * for browser access. Returns the original URL if already HTTP(S) or unknown.
35
- *
36
- * Supported:
37
- * - `s3://bucket/key` → `https://s3.{region}.amazonaws.com/{bucket}/{key}`
38
- * (region auto-detected from bucket name when possible, e.g. "us-west-2.opendata.source.coop")
39
- * - `gs://bucket/key` → `https://storage.googleapis.com/{bucket}/{key}`
40
- */
41
- export declare function resolveCloudUrl(url: string): string;