@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
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Lightweight browser-native Jupyter notebook renderer.
3
+ * Replaces `notebookjs` which depends on jsdom/Buffer (Node.js only).
4
+ * Handles nbformat 2, 3, 4, and 5.
5
+ */
6
+ const PREFIX = 'nb-';
7
+ // ── Helpers ──────────────────────────────────────────────────────────────────
8
+ function el(tag, classNames = []) {
9
+ const e = document.createElement(tag);
10
+ if (classNames.length)
11
+ e.className = classNames.map((c) => PREFIX + c).join(' ');
12
+ return e;
13
+ }
14
+ function escapeHTML(raw) {
15
+ return raw.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
16
+ }
17
+ function joinText(text) {
18
+ if (Array.isArray(text))
19
+ return text.join('');
20
+ return text ?? '';
21
+ }
22
+ // ── Display renderers ────────────────────────────────────────────────────────
23
+ const IMAGE_FORMATS = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
24
+ function renderImage(format, data) {
25
+ const img = el('img', ['image-output']);
26
+ img.src = `data:${format};base64,${joinText(data).replace(/\n/g, '')}`;
27
+ return img;
28
+ }
29
+ function renderDisplayData(output, config) {
30
+ // Resolve data from v4 `data` field or v3 flat fields
31
+ const getData = (mime) => output.data?.[mime] ?? output[mime];
32
+ // Priority order (richest first)
33
+ for (const fmt of IMAGE_FORMATS) {
34
+ const d = getData(fmt);
35
+ if (d)
36
+ return renderImage(fmt, d);
37
+ }
38
+ const svg = getData('image/svg+xml') ?? getData('text/svg+xml');
39
+ if (svg) {
40
+ const wrapper = el('div', ['svg-output']);
41
+ wrapper.innerHTML = joinText(svg);
42
+ return wrapper;
43
+ }
44
+ const html = getData('text/html');
45
+ if (html) {
46
+ const wrapper = el('div', ['html-output']);
47
+ wrapper.innerHTML = joinText(html);
48
+ return wrapper;
49
+ }
50
+ const md = getData('text/markdown');
51
+ if (md) {
52
+ const wrapper = el('div', ['html-output']);
53
+ wrapper.innerHTML = config.markdown(joinText(md));
54
+ return wrapper;
55
+ }
56
+ const latex = getData('text/latex');
57
+ if (latex) {
58
+ const wrapper = el('div', ['latex-output']);
59
+ wrapper.textContent = joinText(latex);
60
+ return wrapper;
61
+ }
62
+ const plain = getData('text/plain');
63
+ if (plain) {
64
+ const pre = el('pre', ['text-output']);
65
+ pre.innerHTML = config.ansi(escapeHTML(joinText(plain)));
66
+ return pre;
67
+ }
68
+ return el('div', ['empty-output']);
69
+ }
70
+ function renderStream(output, config) {
71
+ const streamName = output.stream ?? output.name ?? 'stdout';
72
+ const pre = el('pre', [streamName]);
73
+ pre.innerHTML = config.ansi(escapeHTML(joinText(output.text ?? '')));
74
+ return pre;
75
+ }
76
+ function renderError(output, config) {
77
+ const pre = el('pre', ['pyerr']);
78
+ const raw = (output.traceback ?? []).join('\n');
79
+ pre.innerHTML = config.ansi(escapeHTML(raw));
80
+ return pre;
81
+ }
82
+ // ── Output coalescing (merge consecutive same-name streams) ──────────────────
83
+ function coalesceStreams(outputs) {
84
+ if (!outputs.length)
85
+ return outputs;
86
+ const result = [{ ...outputs[0] }];
87
+ for (let i = 1; i < outputs.length; i++) {
88
+ const o = outputs[i];
89
+ const last = result[result.length - 1];
90
+ if (o.output_type === 'stream' &&
91
+ last.output_type === 'stream' &&
92
+ (o.stream ?? o.name) === (last.stream ?? last.name)) {
93
+ // Merge text
94
+ const lastText = Array.isArray(last.text) ? last.text : [last.text ?? ''];
95
+ const oText = Array.isArray(o.text) ? o.text : [o.text ?? ''];
96
+ last.text = lastText.concat(oText);
97
+ }
98
+ else {
99
+ result.push({ ...o });
100
+ }
101
+ }
102
+ return result;
103
+ }
104
+ // ── Cell renderers ───────────────────────────────────────────────────────────
105
+ function renderCodeCell(cell, lang, config) {
106
+ const cellEl = el('div', ['cell', 'code-cell']);
107
+ // Input
108
+ const source = joinText(cell.source ?? cell.input ?? '');
109
+ if (source) {
110
+ const holder = el('div', ['input']);
111
+ const num = cell.prompt_number ?? cell.execution_count;
112
+ if (typeof num === 'number' && num > -1) {
113
+ holder.setAttribute('data-prompt-number', String(num));
114
+ }
115
+ const pre = el('pre');
116
+ const code = document.createElement('code');
117
+ code.setAttribute('data-language', lang);
118
+ code.className = `lang-${lang}`;
119
+ code.innerHTML = config.highlighter(escapeHTML(source), lang);
120
+ pre.appendChild(code);
121
+ holder.appendChild(pre);
122
+ cellEl.appendChild(holder);
123
+ }
124
+ // Outputs
125
+ const rawOutputs = coalesceStreams(cell.outputs ?? []);
126
+ for (const output of rawOutputs) {
127
+ const outer = el('div', ['output']);
128
+ let inner;
129
+ switch (output.output_type) {
130
+ case 'display_data':
131
+ case 'execute_result':
132
+ case 'pyout':
133
+ inner = renderDisplayData(output, config);
134
+ break;
135
+ case 'stream':
136
+ inner = renderStream(output, config);
137
+ break;
138
+ case 'error':
139
+ case 'pyerr':
140
+ inner = renderError(output, config);
141
+ break;
142
+ default:
143
+ inner = el('div', ['empty-output']);
144
+ }
145
+ outer.appendChild(inner);
146
+ cellEl.appendChild(outer);
147
+ }
148
+ return cellEl;
149
+ }
150
+ function renderMarkdownCell(cell, config) {
151
+ const cellEl = el('div', ['cell', 'markdown-cell']);
152
+ const source = joinText(cell.source ?? cell.input ?? '');
153
+ cellEl.innerHTML = config.markdown(source);
154
+ return cellEl;
155
+ }
156
+ function renderHeadingCell(cell) {
157
+ const level = Math.min(Math.max(cell.level ?? 1, 1), 6);
158
+ const heading = el(`h${level}`, ['cell', 'heading-cell']);
159
+ heading.innerHTML = escapeHTML(joinText(cell.source ?? cell.input ?? ''));
160
+ return heading;
161
+ }
162
+ function renderRawCell(cell) {
163
+ const cellEl = el('div', ['cell', 'raw-cell']);
164
+ cellEl.innerHTML = escapeHTML(joinText(cell.source ?? cell.input ?? ''));
165
+ return cellEl;
166
+ }
167
+ /**
168
+ * Parse and render a Jupyter notebook JSON to a DOM element.
169
+ * Returns the rendered element and metadata.
170
+ */
171
+ export function renderNotebook(raw, config) {
172
+ const meta = raw.metadata ?? {};
173
+ const lang = meta.kernelspec?.language ?? meta.language_info?.name ?? meta.language ?? 'python';
174
+ const kernelName = meta.kernelspec?.display_name ?? meta.language_info?.name ?? '';
175
+ // Normalize cells: v4+ has top-level `cells`, v2/v3 uses `worksheets`
176
+ let allCells = [];
177
+ if (Array.isArray(raw.cells)) {
178
+ allCells = raw.cells;
179
+ }
180
+ else if (Array.isArray(raw.worksheets)) {
181
+ for (const ws of raw.worksheets) {
182
+ if (Array.isArray(ws.cells))
183
+ allCells.push(...ws.cells);
184
+ }
185
+ }
186
+ const notebook = el('div', ['notebook']);
187
+ for (const cell of allCells) {
188
+ let rendered;
189
+ switch (cell.cell_type) {
190
+ case 'code':
191
+ rendered = renderCodeCell(cell, cell.language ?? lang, config);
192
+ break;
193
+ case 'markdown':
194
+ rendered = renderMarkdownCell(cell, config);
195
+ break;
196
+ case 'heading':
197
+ rendered = renderHeadingCell(cell);
198
+ break;
199
+ case 'raw':
200
+ rendered = renderRawCell(cell);
201
+ break;
202
+ default:
203
+ rendered = renderRawCell(cell);
204
+ }
205
+ notebook.appendChild(rendered);
206
+ }
207
+ return {
208
+ element: notebook,
209
+ meta: { kernelName, language: lang, cellCount: allCells.length }
210
+ };
211
+ }
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * This provides instant metadata display before DuckDB-WASM finishes loading.
8
8
  */
9
- const WGS84_CODES = new Set([4326, 4979]);
9
+ import { WGS84_CODES } from '../constants.js';
10
10
  /** Map hyparquet schema element types to DuckDB-like type strings. */
11
11
  function mapParquetType(col) {
12
12
  const lt = col.logical_type;
@@ -5,6 +5,7 @@
5
5
  * vector tiles into a structure suitable for SVG rendering and
6
6
  * feature inspection.
7
7
  */
8
+ import { LAYER_HUE_MULTIPLIER } from '../constants.js';
8
9
  const GEOM_TYPES = ['Unknown', 'Point', 'LineString', 'Polygon'];
9
10
  /**
10
11
  * Fetch and decode an MVT tile from a PMTiles archive.
@@ -60,5 +61,5 @@ export function tileMimeType(format) {
60
61
  }
61
62
  /** Compute the hue for layer index i (same palette as buildPmtilesLayers). */
62
63
  export function layerHue(i) {
63
- return (i * 137) % 360;
64
+ return (i * LAYER_HUE_MULTIPLIER) % 360;
64
65
  }
@@ -1,4 +1,5 @@
1
1
  import { PMTiles, Protocol } from 'pmtiles';
2
+ import { LAYER_HUE_MULTIPLIER } from '../constants.js';
2
3
  let protocol = null;
3
4
  export function getPmtilesProtocol() {
4
5
  if (!protocol) {
@@ -96,7 +97,7 @@ export function buildPmtilesLayers(sourceId, metadata) {
96
97
  const layers = [];
97
98
  for (let i = 0; i < metadata.layers.length; i++) {
98
99
  const layerId = metadata.layers[i];
99
- const hue = (i * 137) % 360;
100
+ const hue = (i * LAYER_HUE_MULTIPLIER) % 360;
100
101
  layers.push({
101
102
  id: `${layerId}-fill`,
102
103
  type: 'fill',
@@ -35,7 +35,7 @@
35
35
  *
36
36
  * Also handles plain bucket names (no protocol).
37
37
  */
38
- export type StorageProvider = 's3' | 'gcs' | 'r2' | 'minio' | 'azure' | 'storj' | 'unknown';
38
+ export type StorageProvider = string;
39
39
  export interface ParsedStorageUrl {
40
40
  bucket: string;
41
41
  region: string;
@@ -35,25 +35,25 @@
35
35
  *
36
36
  * Also handles plain bucket names (no protocol).
37
37
  */
38
- /** All recognized URI scheme prefixes (lowercase) */
39
- const SCHEME_MAP = {
40
- 's3://': { provider: 's3', strip: 5 },
41
- 's3a://': { provider: 's3', strip: 6 },
42
- 's3n://': { provider: 's3', strip: 6 },
43
- 'aws://': { provider: 's3', strip: 6 },
44
- 'r2://': { provider: 'r2', strip: 5 },
45
- 'gs://': { provider: 'gcs', strip: 5 },
46
- 'gcs://': { provider: 'gcs', strip: 6 },
47
- 'azure://': { provider: 'azure', strip: 8 },
48
- 'az://': { provider: 'azure', strip: 5 },
49
- 'abfs://': { provider: 'azure', strip: 7 },
50
- 'abfss://': { provider: 'azure', strip: 8 },
51
- 'wasbs://': { provider: 'azure', strip: 8 },
52
- 'adl://': { provider: 'azure', strip: 6 },
53
- 'storj://': { provider: 'storj', strip: 8 },
54
- 'sj://': { provider: 'storj', strip: 5 },
55
- 'swift://': { provider: 'unknown', strip: 8 }
56
- };
38
+ import { PROVIDERS } from '../storage/providers.js';
39
+ /**
40
+ * Build SCHEME_MAP from the provider registry's `schemes` arrays.
41
+ * Each scheme like "s3" generates an entry `"s3://": { provider: "s3", strip: 5 }`.
42
+ */
43
+ function buildSchemeMap() {
44
+ const map = {};
45
+ for (const [id, def] of Object.entries(PROVIDERS)) {
46
+ for (const scheme of def.schemes) {
47
+ const key = `${scheme}://`;
48
+ map[key] = { provider: id, strip: key.length };
49
+ }
50
+ }
51
+ // Non-registry schemes (no corresponding provider)
52
+ map['swift://'] = { provider: 'unknown', strip: 8 };
53
+ return map;
54
+ }
55
+ /** All recognized URI scheme prefixes (lowercase), derived from provider registry */
56
+ const SCHEME_MAP = buildSchemeMap();
57
57
  function defaultResult(defaults) {
58
58
  return {
59
59
  bucket: '',
@@ -174,7 +174,7 @@ export function parseStorageUrl(input, defaults = {}) {
174
174
  bucket: doVhost[1],
175
175
  region: doVhost[2],
176
176
  endpoint: `${url.protocol}//${doVhost[2]}.digitaloceanspaces.com`,
177
- provider: 's3',
177
+ provider: 'digitalocean',
178
178
  prefix: pathParts.join('/')
179
179
  };
180
180
  }
@@ -185,7 +185,7 @@ export function parseStorageUrl(input, defaults = {}) {
185
185
  bucket: pathParts[0],
186
186
  region: doPath[1],
187
187
  endpoint: `${url.protocol}//${url.host}`,
188
- provider: 's3',
188
+ provider: 'digitalocean',
189
189
  prefix: pathParts.slice(1).join('/')
190
190
  };
191
191
  }
@@ -197,7 +197,7 @@ export function parseStorageUrl(input, defaults = {}) {
197
197
  bucket: pathParts[0],
198
198
  region: wasabiMatch[1],
199
199
  endpoint: `${url.protocol}//${url.host}`,
200
- provider: 's3',
200
+ provider: 'wasabi',
201
201
  prefix: pathParts.slice(1).join('/')
202
202
  };
203
203
  }
@@ -209,7 +209,7 @@ export function parseStorageUrl(input, defaults = {}) {
209
209
  bucket: b2S3[1],
210
210
  region: b2S3[2],
211
211
  endpoint: `${url.protocol}//s3.${b2S3[2]}.backblazeb2.com`,
212
- provider: 's3',
212
+ provider: 'b2',
213
213
  prefix: pathParts.join('/')
214
214
  };
215
215
  }
@@ -220,7 +220,7 @@ export function parseStorageUrl(input, defaults = {}) {
220
220
  bucket: pathParts[1],
221
221
  region: defaults.region || 'us-west-000',
222
222
  endpoint: `${url.protocol}//${url.host}`,
223
- provider: 's3',
223
+ provider: 'b2',
224
224
  prefix: pathParts.slice(2).join('/')
225
225
  };
226
226
  }
@@ -259,6 +259,64 @@ export function parseStorageUrl(input, defaults = {}) {
259
259
  prefix: pathParts.slice(1).join('/')
260
260
  };
261
261
  }
262
+ // --- Contabo ---
263
+ // <region>.contabostorage.com/<bucket>
264
+ const contaboMatch = host.match(/^([a-z0-9]+)\.contabostorage\.com$/);
265
+ if (contaboMatch && pathParts.length > 0) {
266
+ return {
267
+ bucket: pathParts[0],
268
+ region: contaboMatch[1],
269
+ endpoint: `${url.protocol}//${url.host}`,
270
+ provider: 'contabo',
271
+ prefix: pathParts.slice(1).join('/')
272
+ };
273
+ }
274
+ // --- Hetzner ---
275
+ // <region>.your-objectstorage.com/<bucket>
276
+ const hetznerMatch = host.match(/^([a-z0-9]+)\.your-objectstorage\.com$/);
277
+ if (hetznerMatch && pathParts.length > 0) {
278
+ return {
279
+ bucket: pathParts[0],
280
+ region: hetznerMatch[1],
281
+ endpoint: `${url.protocol}//${url.host}`,
282
+ provider: 'hetzner',
283
+ prefix: pathParts.slice(1).join('/')
284
+ };
285
+ }
286
+ // --- Linode / Akamai ---
287
+ // <bucket>.<region>.linodeobjects.com or <region>.linodeobjects.com/<bucket>
288
+ const linodeVhost = host.match(/^(.+)\.([a-z0-9-]+)\.linodeobjects\.com$/);
289
+ if (linodeVhost) {
290
+ return {
291
+ bucket: linodeVhost[1],
292
+ region: linodeVhost[2],
293
+ endpoint: `${url.protocol}//${linodeVhost[2]}.linodeobjects.com`,
294
+ provider: 'linode',
295
+ prefix: pathParts.join('/')
296
+ };
297
+ }
298
+ const linodePath = host.match(/^([a-z0-9-]+)\.linodeobjects\.com$/);
299
+ if (linodePath && pathParts.length > 0) {
300
+ return {
301
+ bucket: pathParts[0],
302
+ region: linodePath[1],
303
+ endpoint: `${url.protocol}//${url.host}`,
304
+ provider: 'linode',
305
+ prefix: pathParts.slice(1).join('/')
306
+ };
307
+ }
308
+ // --- OVHcloud ---
309
+ // s3.<region>.io.cloud.ovh.net/<bucket>
310
+ const ovhMatch = host.match(/^s3\.([a-z0-9-]+)\.io\.cloud\.ovh\.(?:net|us)$/);
311
+ if (ovhMatch && pathParts.length > 0) {
312
+ return {
313
+ bucket: pathParts[0],
314
+ region: ovhMatch[1],
315
+ endpoint: `${url.protocol}//${url.host}`,
316
+ provider: 'ovhcloud',
317
+ prefix: pathParts.slice(1).join('/')
318
+ };
319
+ }
262
320
  // --- MinIO ---
263
321
  // Common patterns: minio.<domain>, localhost with port
264
322
  const isMinioLike = host.includes('minio') ||
@@ -10,18 +10,13 @@
10
10
  * Uses SvelteKit's replaceState to avoid conflicts with the router.
11
11
  */
12
12
  import { replaceState } from '$app/navigation';
13
+ import { buildProviderBaseUrl } from '../storage/providers.js';
13
14
  import { parseStorageUrl } from './storage-url.js';
14
15
  /**
15
16
  * Build the base HTTPS URL for a connection (endpoint + bucket).
16
17
  */
17
18
  function buildBaseUrl(conn) {
18
- if (conn.endpoint) {
19
- return `${conn.endpoint.replace(/\/$/, '')}/${conn.bucket}`;
20
- }
21
- if (conn.provider === 'gcs') {
22
- return `https://storage.googleapis.com/${conn.bucket}`;
23
- }
24
- return `https://s3.${conn.region || 'us-east-1'}.amazonaws.com/${conn.bucket}`;
19
+ return buildProviderBaseUrl(conn.provider, conn.endpoint, conn.bucket, conn.region);
25
20
  }
26
21
  /**
27
22
  * Build a full storage URL from a Connection + optional object prefix.
@@ -4,8 +4,6 @@ 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
- /** Map provider to its native URI scheme prefix. */
8
- export declare function getNativeScheme(provider: string): string;
9
7
  /**
10
8
  * Build a provider-native protocol URL (s3://bucket/path, sj://bucket/path, etc.).
11
9
  */
package/dist/utils/url.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { buildProviderBaseUrl } from '../storage/providers.js';
1
2
  import { connections } from '../stores/connections.svelte.js';
2
3
  import { credentialStore } from '../stores/credentials.svelte.js';
4
+ import { getNativeScheme, safeDecodeURIComponent } from './cloud-url.js';
3
5
  /**
4
6
  * Build an HTTPS URL for a tab's file.
5
7
  * Works for any viewer that needs an HTTP-accessible URL (COG, PMTiles, Zarr, etc.)
@@ -16,27 +18,7 @@ export function buildHttpsUrl(tab) {
16
18
  : `https://${conn.bucket}.blob.core.windows.net/${cleanPath}`;
17
19
  return appendAzureSas(base, conn.id);
18
20
  }
19
- if (conn.endpoint) {
20
- return `${conn.endpoint.replace(/\/$/, '')}/${conn.bucket}/${cleanPath}`;
21
- }
22
- return `https://s3.${conn.region || 'us-east-1'}.amazonaws.com/${conn.bucket}/${cleanPath}`;
23
- }
24
- /** Map provider to its native URI scheme prefix. */
25
- export function getNativeScheme(provider) {
26
- switch (provider) {
27
- case 'gcs':
28
- return 'gs';
29
- case 'azure':
30
- return 'az';
31
- case 'r2':
32
- return 'r2';
33
- case 'storj':
34
- return 'sj';
35
- case 'minio':
36
- return 's3';
37
- default:
38
- return 's3';
39
- }
21
+ return `${buildProviderBaseUrl(conn.provider, conn.endpoint, conn.bucket, conn.region)}/${cleanPath}`;
40
22
  }
41
23
  /**
42
24
  * Build a provider-native protocol URL (s3://bucket/path, sj://bucket/path, etc.).
@@ -76,14 +58,6 @@ export function buildDuckDbUrl(tab) {
76
58
  const rawPath = safeDecodeURIComponent(tab.path.replace(/^\//, ''));
77
59
  return `s3://${conn.bucket}/${rawPath}`;
78
60
  }
79
- function safeDecodeURIComponent(s) {
80
- try {
81
- return decodeURIComponent(s);
82
- }
83
- catch {
84
- return s;
85
- }
86
- }
87
61
  /**
88
62
  * Check if a tab's file can be loaded directly via HTTPS URL (streaming).
89
63
  * True for URL-sourced tabs, anonymous buckets, and Azure (SAS token in URL).
@@ -1,39 +1,79 @@
1
1
  /**
2
2
  * Zarr metadata parsing utilities.
3
3
  *
4
- * Handles consolidated metadata for both Zarr v2 (.zmetadata) and v3 (zarr.json),
5
- * plus a zarrita fallback for non-consolidated stores.
4
+ * Builds a hierarchical tree of groups and arrays from consolidated metadata
5
+ * (Zarr v2 .zmetadata, v3 zarr.json), with zarrita fallback for non-consolidated stores.
6
6
  */
7
- export interface VarMeta {
7
+ /** Ensure numcodecs codecs are registered. Await before creating ZarrLayer. */
8
+ export declare function ensureCodecsRegistered(): Promise<void>;
9
+ /** Zarr store marker files — presence of any indicates a Zarr store. */
10
+ export declare const ZARR_MARKER_FILES: Set<string>;
11
+ /**
12
+ * Detect whether a set of file names contains Zarr marker files.
13
+ * Returns the detected version (2 or 3) or null if not detected.
14
+ */
15
+ export declare function detectZarrMarkers(fileNames: Iterable<string>): {
16
+ detected: boolean;
17
+ version: 2 | 3 | null;
18
+ };
19
+ /**
20
+ * If a URL points to a Zarr marker file, strip the marker suffix and return the store URL.
21
+ * Returns null if the URL doesn't end with a known marker suffix.
22
+ */
23
+ export declare function extractZarrStoreUrl(url: string): string | null;
24
+ export interface ZarrNode {
25
+ path: string;
8
26
  name: string;
9
- shape: number[];
10
- dtype: string;
11
- dims: string[];
12
- chunks: number[];
27
+ kind: 'group' | 'array';
28
+ children: ZarrNode[];
29
+ shape?: number[];
30
+ dtype?: string;
31
+ dims?: string[];
32
+ chunks?: number[];
33
+ fillValue?: any;
34
+ codecs?: any[];
35
+ compressor?: any;
36
+ filters?: any[];
37
+ chunkKeyEncoding?: string;
13
38
  attributes: Record<string, any>;
14
39
  }
15
- export interface ZarrMetadata {
40
+ export interface ZarrHierarchy {
41
+ root: ZarrNode;
42
+ zarrVersion: 2 | 3 | null;
43
+ totalNodes: number;
16
44
  storeAttrs: Record<string, any>;
17
- variables: VarMeta[];
18
- coords: VarMeta[];
19
45
  spatialRefAttrs: Record<string, any> | null;
20
- zarrVersion: number | null;
21
46
  }
47
+ /** Dimension-like variable names treated as coordinates. */
48
+ export declare const DIM_LIKE_NAMES: Set<string>;
22
49
  /** Guess dimension names from shape length when metadata is absent. */
23
50
  export declare function inferDims(name: string, shape: number[]): string[];
24
51
  /** Format shape array for display: `[3 × 256 × 512]` or `scalar`. */
25
52
  export declare function formatShape(shape: number[]): string;
26
- /** Parse Zarr v3 consolidated metadata (zarr.json). */
27
- export declare function parseV3Consolidated(data: any): Omit<ZarrMetadata, 'zarrVersion'>;
28
- /** Parse Zarr v2 consolidated metadata (.zmetadata). */
29
- export declare function parseV2Consolidated(data: any): Omit<ZarrMetadata, 'zarrVersion'>;
53
+ /** Byte size of a dtype string. Handles v2 (`<f4`, `|u1`) and v3 (`float32`, `uint8`). */
54
+ export declare function dtypeByteSize(dtype: string): number;
55
+ /** Format chunk count: `"36 [6 × 6]"` */
56
+ export declare function computeChunkCount(shape: number[] | undefined, chunks: number[] | undefined): string | null;
57
+ /** Format chunk size in bytes: `"817.6 KB"` */
58
+ export declare function computeChunkSize(chunks: number[] | undefined, dtype: string | undefined): string | null;
59
+ /** Format uncompressed size: `"28.7 MB"` */
60
+ export declare function computeUncompressed(shape: number[] | undefined, dtype: string | undefined): string | null;
61
+ /** Format codec pipeline for display. */
62
+ export declare function formatCodecs(node: ZarrNode): string | null;
63
+ /** Format chunk_key_encoding for display: `"default (sep: "/")"` */
64
+ export declare function formatChunkKeys(node: ZarrNode): string | null;
65
+ /** Find a node by slash-delimited path. */
66
+ export declare function findNodeByPath(root: ZarrNode, path: string): ZarrNode | null;
67
+ /** Build tree from Zarr v3 consolidated metadata (zarr.json). */
68
+ export declare function buildV3Tree(data: any): ZarrHierarchy;
69
+ /** Build tree from Zarr v2 consolidated metadata (.zmetadata). */
70
+ export declare function buildV2Tree(data: any): ZarrHierarchy;
30
71
  /**
31
- * Fetch consolidated metadata from a Zarr store URL.
32
- * Tries v3 (zarr.json) first, then v2 (.zmetadata).
72
+ * Fetch hierarchy from a Zarr store URL.
73
+ * Tries v3 (zarr.json) first, then v2 (.zmetadata), then zarrita fallback.
33
74
  */
34
- export declare function fetchConsolidated(storeUrl: string): Promise<ZarrMetadata | null>;
75
+ export declare function fetchHierarchy(storeUrl: string, storeName: string, signal?: AbortSignal): Promise<ZarrHierarchy | null>;
35
76
  /**
36
77
  * Fallback: probe a Zarr store using zarrita when consolidated metadata is unavailable.
37
- * @param storeName - Display name for the root array (e.g. file name without .zarr)
38
78
  */
39
- export declare function probeWithZarrita(storeUrl: string, storeName: string): Promise<ZarrMetadata | null>;
79
+ export declare function probeHierarchy(storeUrl: string, storeName: string): Promise<ZarrHierarchy | null>;