@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,8 +1,10 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte';
3
+ import { getMimeType } from '../../file-icons/index.js';
3
4
  import { getAdapter } from '../../storage/index.js';
4
5
  import { tabResources } from '../../stores/tab-resources.svelte.js';
5
6
  import type { Tab } from '../../types';
7
+ import { handleLoadError } from '../../utils/error.js';
6
8
  import { buildHttpsUrl, canStreamDirectly } from '../../utils/url.js';
7
9
 
8
10
  let { tab }: { tab: Tab } = $props();
@@ -10,19 +12,6 @@ let { tab }: { tab: Tab } = $props();
10
12
  const videoExtensions = new Set(['mp4', 'webm', 'mov', 'avi', 'mkv']);
11
13
  const mediaType = $derived(videoExtensions.has(tab.extension.toLowerCase()) ? 'video' : 'audio');
12
14
 
13
- const mimeMap: Record<string, string> = {
14
- mp4: 'video/mp4',
15
- webm: 'video/webm',
16
- mov: 'video/quicktime',
17
- avi: 'video/x-msvideo',
18
- mkv: 'video/x-matroska',
19
- mp3: 'audio/mpeg',
20
- wav: 'audio/wav',
21
- ogg: 'audio/ogg',
22
- flac: 'audio/flac',
23
- aac: 'audio/aac'
24
- };
25
-
26
15
  let abortController: AbortController | null = null;
27
16
  let mediaSrc = $state<string | null>(null);
28
17
  let blobUrl = $state<string | null>(null);
@@ -51,14 +40,14 @@ async function loadMedia() {
51
40
  // Authenticated S3 — download via storage adapter (blob fallback)
52
41
  const adapter = getAdapter(tab.source, tab.connectionId);
53
42
  const data = await adapter.read(tab.path, undefined, undefined, signal);
54
- const mime = mimeMap[tab.extension.toLowerCase()] || 'application/octet-stream';
55
- const blob = new Blob([data as unknown as BlobPart], { type: mime });
43
+ const blob = new Blob([data as unknown as BlobPart], { type: getMimeType(tab.extension) });
56
44
  blobUrl = URL.createObjectURL(blob);
57
45
  mediaSrc = blobUrl;
58
46
  }
59
47
  } catch (err) {
60
- if (err instanceof DOMException && err.name === 'AbortError') return;
61
- error = err instanceof Error ? err.message : String(err);
48
+ const msg = handleLoadError(err);
49
+ if (msg === null) return;
50
+ error = msg;
62
51
  } finally {
63
52
  loading = false;
64
53
  }
@@ -11,6 +11,7 @@ import { t } from '../../i18n/index.svelte.js';
11
11
  import { getAdapter } from '../../storage/index.js';
12
12
  import { tabResources } from '../../stores/tab-resources.svelte.js';
13
13
  import type { Tab } from '../../types';
14
+ import { handleLoadError } from '../../utils/error.js';
14
15
  import {
15
16
  createModelScene,
16
17
  disposeModelScene,
@@ -52,8 +53,9 @@ async function loadModelFile() {
52
53
  meshCount = info.meshCount;
53
54
  vertexCount = info.vertexCount;
54
55
  } catch (err) {
55
- if (err instanceof DOMException && err.name === 'AbortError') return;
56
- error = err instanceof Error ? err.message : String(err);
56
+ const msg = handleLoadError(err);
57
+ if (msg === null) return;
58
+ error = msg;
57
59
  } finally {
58
60
  loading = false;
59
61
  }
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
3
- import { onDestroy } from 'svelte';
3
+ import type { BundledLanguage } from 'shiki';
4
+ import { onDestroy, tick } from 'svelte';
4
5
  import { Badge } from '../ui/badge/index.js';
5
6
  import { Button } from '../ui/button/index.js';
6
7
  import * as DropdownMenu from '../ui/dropdown-menu/index.js';
@@ -8,7 +9,10 @@ import { t } from '../../i18n/index.svelte.js';
8
9
  import { getAdapter } from '../../storage/index.js';
9
10
  import { tabResources } from '../../stores/tab-resources.svelte.js';
10
11
  import type { Tab } from '../../types';
11
- import { highlightCode } from '../../utils/shiki';
12
+ import { copyToClipboard, wireCodeCopyButtons } from '../../utils/clipboard.js';
13
+ import { handleLoadError } from '../../utils/error.js';
14
+ import { renderNotebook } from '../../utils/notebook';
15
+ import { highlightCodeReversed } from '../../utils/shiki';
12
16
 
13
17
  let { tab }: { tab: Tab } = $props();
14
18
 
@@ -58,60 +62,58 @@ async function loadNotebook() {
58
62
  rawContent = new TextDecoder().decode(data);
59
63
  const notebook = JSON.parse(rawContent);
60
64
 
61
- if (typeof notebook.nbformat !== 'number' || !Array.isArray(notebook.cells)) {
65
+ // Accept nbformat 2–5: v4+ has top-level `cells`, v2/v3 uses `worksheets`
66
+ if (
67
+ typeof notebook.nbformat !== 'number' ||
68
+ (!Array.isArray(notebook.cells) && !Array.isArray(notebook.worksheets))
69
+ ) {
62
70
  throw new Error('Not a valid Jupyter notebook');
63
71
  }
64
72
 
65
- cellCount = notebook.cells.length;
66
- kernelName =
67
- notebook.metadata?.kernelspec?.display_name ?? notebook.metadata?.language_info?.name ?? '';
68
-
69
- await renderNotebook(notebook);
73
+ await renderNotebookContent(notebook);
70
74
  } catch (err) {
71
- if (err instanceof DOMException && err.name === 'AbortError') return;
72
- error = err instanceof Error ? err.message : String(err);
75
+ const msg = handleLoadError(err);
76
+ if (msg === null) return;
77
+ error = msg;
73
78
  } finally {
74
79
  loading = false;
75
80
  }
76
81
  }
77
82
 
78
- async function renderNotebook(notebook: any) {
83
+ async function renderNotebookContent(notebook: any) {
84
+ await tick();
79
85
  if (!container) return;
80
86
 
81
- const [nb, { marked }, { AnsiUp }] = await Promise.all([
82
- import('notebookjs').then((m) => m.default || m),
83
- import('marked'),
84
- import('ansi_up')
85
- ]);
87
+ const [{ marked }, { AnsiUp }] = await Promise.all([import('marked'), import('ansi_up')]);
86
88
 
87
- // Configure notebookjs
88
- nb.markdown = (md: string) => marked.parse(md, { async: false }) as string;
89
89
  const ansiUp = new AnsiUp();
90
- nb.ansi = (text: string) => ansiUp.ansi_to_html(text);
91
- nb.highlighter = (code: string, lang: string) => {
92
- // Return escaped code — Shiki highlighting is async so we apply it after render
93
- return code.replace(/</g, '&lt;').replace(/>/g, '&gt;');
94
- };
90
+ const { element, meta } = renderNotebook(notebook, {
91
+ markdown: (md: string) => marked.parse(md, { async: false }) as string,
92
+ ansi: (text: string) => ansiUp.ansi_to_html(text),
93
+ highlighter: (code: string) => code
94
+ });
95
95
 
96
- const parsed = nb.parse(notebook);
97
- const rendered: HTMLElement = parsed.render();
96
+ cellCount = meta.cellCount;
97
+ kernelName = meta.kernelName;
98
98
 
99
99
  container.innerHTML = '';
100
- container.appendChild(rendered);
100
+ container.appendChild(element);
101
101
 
102
- // Apply Shiki syntax highlighting to code cells asynchronously
102
+ // Apply Shiki syntax highlighting + copy buttons to code cells
103
103
  const codeBlocks = container.querySelectorAll('.nb-input pre');
104
104
  for (const block of codeBlocks) {
105
105
  const code = block.textContent ?? '';
106
- const lang =
107
- notebook.metadata?.kernelspec?.language ?? notebook.metadata?.language_info?.name ?? 'python';
108
106
  try {
109
- const html = await highlightCode(code, lang);
110
- block.outerHTML = html;
107
+ const highlighted = await highlightCodeReversed(code, meta.language as BundledLanguage);
108
+ const copyBtn = `<button class="nb-copy-btn" data-code="${encodeURIComponent(code)}" title="Copy"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg></button>`;
109
+ block.outerHTML = copyBtn + highlighted;
111
110
  } catch {
112
111
  // Shiki doesn't support this language — keep the escaped HTML
113
112
  }
114
113
  }
114
+
115
+ // Wire copy button click handlers
116
+ wireCodeCopyButtons(container, '.nb-copy-btn');
115
117
  }
116
118
 
117
119
  function toggleCode() {
@@ -125,13 +127,7 @@ function toggleCode() {
125
127
  }
126
128
 
127
129
  async function copyRaw() {
128
- try {
129
- await navigator.clipboard.writeText(rawContent);
130
- copied = true;
131
- setTimeout(() => (copied = false), 2000);
132
- } catch {
133
- // clipboard not available
134
- }
130
+ await copyToClipboard(rawContent, (v) => (copied = v));
135
131
  }
136
132
  </script>
137
133
 
@@ -191,9 +187,8 @@ async function copyRaw() {
191
187
  <div class="flex h-full items-center justify-center">
192
188
  <p class="text-sm text-red-400">{error}</p>
193
189
  </div>
194
- {:else}
195
- <div bind:this={container} class="notebook-content"></div>
196
190
  {/if}
191
+ <div bind:this={container} class="notebook-content" class:hidden={loading || !!error}></div>
197
192
  </div>
198
193
  </div>
199
194
 
@@ -215,6 +210,7 @@ async function copyRaw() {
215
210
 
216
211
  /* Code input cells */
217
212
  .notebook-content :global(.nb-input) {
213
+ position: relative;
218
214
  border: 1px solid var(--border);
219
215
  border-radius: 0.375rem;
220
216
  overflow: hidden;
@@ -386,4 +382,58 @@ async function copyRaw() {
386
382
  font-family: monospace;
387
383
  font-size: 0.8125rem;
388
384
  }
385
+
386
+ /* Copy button on code cells */
387
+ .notebook-content :global(.nb-copy-btn) {
388
+ position: absolute;
389
+ top: 6px;
390
+ right: 6px;
391
+ z-index: 1;
392
+ display: flex;
393
+ align-items: center;
394
+ gap: 4px;
395
+ padding: 4px 6px;
396
+ border: none;
397
+ border-radius: 4px;
398
+ background: rgba(255, 255, 255, 0.1);
399
+ color: rgba(255, 255, 255, 0.6);
400
+ cursor: pointer;
401
+ font-size: 0.6875rem;
402
+ opacity: 0;
403
+ transition: opacity 0.15s;
404
+ }
405
+
406
+ .notebook-content :global(.nb-input:hover .nb-copy-btn) {
407
+ opacity: 1;
408
+ }
409
+
410
+ .notebook-content :global(.nb-copy-btn:hover) {
411
+ background: rgba(255, 255, 255, 0.2);
412
+ color: rgba(255, 255, 255, 0.9);
413
+ }
414
+
415
+ .notebook-content :global(.nb-copy-btn.copied) {
416
+ opacity: 1;
417
+ color: #4ade80;
418
+ }
419
+
420
+ .notebook-content :global(.nb-copy-btn.copied)::after {
421
+ content: '\2713';
422
+ margin-left: 2px;
423
+ }
424
+
425
+ /* Dark mode: reversed theme means light code bg */
426
+ :global(.dark) .notebook-content :global(.nb-copy-btn) {
427
+ background: rgba(0, 0, 0, 0.1);
428
+ color: rgba(0, 0, 0, 0.5);
429
+ }
430
+
431
+ :global(.dark) .notebook-content :global(.nb-copy-btn:hover) {
432
+ background: rgba(0, 0, 0, 0.2);
433
+ color: rgba(0, 0, 0, 0.8);
434
+ }
435
+
436
+ :global(.dark) .notebook-content :global(.nb-copy-btn.copied) {
437
+ color: #16a34a;
438
+ }
389
439
  </style>
@@ -14,6 +14,7 @@ import { t } from '../../i18n/index.svelte.js';
14
14
  import { getAdapter } from '../../storage/index.js';
15
15
  import { tabResources } from '../../stores/tab-resources.svelte.js';
16
16
  import type { Tab } from '../../types';
17
+ import { handleLoadError } from '../../utils/error.js';
17
18
  import { loadPdfDocument, loadPdfFromUrl } from '../../utils/pdf';
18
19
  import { buildHttpsUrl, canStreamDirectly } from '../../utils/url.js';
19
20
 
@@ -73,10 +74,11 @@ async function loadPdf() {
73
74
  totalPages = doc.numPages;
74
75
  currentPage = 1;
75
76
  } catch (err: any) {
76
- // Ignore cancellation errors (destroyed loading task) and aborts
77
- if (err instanceof DOMException && err.name === 'AbortError') return;
77
+ // Ignore PDF-specific cancellation errors (destroyed loading task)
78
78
  if (err?.name === 'PasswordException' || err?.message?.includes('destroy')) return;
79
- error = err instanceof Error ? err.message : String(err);
79
+ const msg = handleLoadError(err);
80
+ if (msg === null) return;
81
+ error = msg;
80
82
  } finally {
81
83
  loading = false;
82
84
  }
@@ -5,6 +5,7 @@ import { t } from '../../i18n/index.svelte.js';
5
5
  import { getAdapter } from '../../storage/index.js';
6
6
  import { tabResources } from '../../stores/tab-resources.svelte.js';
7
7
  import type { Tab } from '../../types';
8
+ import { handleLoadError } from '../../utils/error.js';
8
9
  import { formatFileSize } from '../../utils/format';
9
10
  import { generateHexDump, type HexRow } from '../../utils/hex';
10
11
 
@@ -55,8 +56,9 @@ async function loadHexDump() {
55
56
  truncated = fileSize > MAX_BYTES;
56
57
  rows = generateHexDump(data);
57
58
  } catch (err) {
58
- if (err instanceof DOMException && err.name === 'AbortError') return;
59
- error = err instanceof Error ? err.message : String(err);
59
+ const msg = handleLoadError(err);
60
+ if (msg === null) return;
61
+ error = msg;
60
62
  } finally {
61
63
  loading = false;
62
64
  }
@@ -15,6 +15,7 @@ import {
15
15
  typeBadgeClass,
16
16
  typeLabel
17
17
  } from '../../utils/column-types.js';
18
+ import { jsonReplacerBigInt } from '../../utils/format.js';
18
19
 
19
20
  const INITIAL_ROWS = 100;
20
21
  const BATCH_SIZE = 100;
@@ -167,7 +168,7 @@ function copyRow() {
167
168
  for (const [k, v] of Object.entries(ctxMenu.rowData)) {
168
169
  if (!k.startsWith('__')) clean[k] = v;
169
170
  }
170
- copyToClipboard(JSON.stringify(clean, (_k, v) => (typeof v === 'bigint' ? v.toString() : v), 2));
171
+ copyToClipboard(JSON.stringify(clean, jsonReplacerBigInt, 2));
171
172
  }
172
173
 
173
174
  function copyColumn() {
@@ -195,7 +196,7 @@ function formatCell(value: any, category: TypeCategory): string {
195
196
  }
196
197
  if (typeof value === 'bigint') return value.toString();
197
198
  if (typeof value === 'object') {
198
- return JSON.stringify(value, (_k, v) => (typeof v === 'bigint' ? v.toString() : v));
199
+ return JSON.stringify(value, jsonReplacerBigInt);
199
200
  }
200
201
  return String(value);
201
202
  }