@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.
- package/README.md +9 -2
- package/dist/components/browser/FileBrowser.svelte +53 -41
- package/dist/components/browser/FileRow.svelte +8 -3
- package/dist/components/browser/FileTreeSidebar.svelte +2 -4
- package/dist/components/layout/AboutSheet.svelte +126 -0
- package/dist/components/layout/AboutSheet.svelte.d.ts +6 -0
- package/dist/components/layout/ConnectionDialog.svelte +186 -138
- package/dist/components/layout/ConnectionDialog.svelte.d.ts +1 -0
- package/dist/components/layout/Sidebar.svelte +19 -3
- package/dist/components/layout/TabBar.svelte +4 -7
- package/dist/components/viewers/CodeViewer.svelte +17 -9
- package/dist/components/viewers/ImageViewer.svelte +6 -16
- package/dist/components/viewers/MarkdownViewer.svelte +8 -16
- package/dist/components/viewers/MediaViewer.svelte +6 -17
- package/dist/components/viewers/ModelViewer.svelte +4 -2
- package/dist/components/viewers/NotebookViewer.svelte +90 -40
- package/dist/components/viewers/PdfViewer.svelte +5 -3
- package/dist/components/viewers/RawViewer.svelte +4 -2
- package/dist/components/viewers/TableGrid.svelte +3 -2
- package/dist/components/viewers/ZarrMapViewer.svelte +334 -40
- package/dist/components/viewers/ZarrMapViewer.svelte.d.ts +3 -8
- package/dist/components/viewers/ZarrViewer.svelte +459 -178
- package/dist/components/viewers/map/AttributeTable.svelte +1 -6
- package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +2 -6
- package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +96 -22
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +34 -0
- package/dist/file-icons/index.js +6 -0
- package/dist/i18n/ar.js +34 -0
- package/dist/i18n/en.js +34 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +16 -1
- package/dist/query/wasm.js +5 -4
- package/dist/storage/browser-cloud.d.ts +7 -0
- package/dist/storage/browser-cloud.js +74 -7
- package/dist/storage/providers.d.ts +53 -0
- package/dist/storage/providers.js +318 -0
- package/dist/stores/connections.svelte.js +8 -34
- package/dist/stores/files.svelte.d.ts +1 -6
- package/dist/stores/files.svelte.js +4 -36
- package/dist/stores/query-history.svelte.js +5 -28
- package/dist/stores/settings.svelte.d.ts +1 -0
- package/dist/stores/settings.svelte.js +11 -31
- package/dist/types.d.ts +2 -2
- package/dist/utils/clipboard.d.ts +13 -0
- package/dist/utils/clipboard.js +38 -0
- package/dist/utils/cloud-url.d.ts +27 -0
- package/dist/utils/cloud-url.js +61 -0
- package/dist/utils/error.d.ts +8 -0
- package/dist/utils/error.js +12 -0
- package/dist/utils/export.d.ts +22 -2
- package/dist/utils/export.js +35 -10
- package/dist/utils/file-sort.d.ts +20 -0
- package/dist/utils/file-sort.js +41 -0
- package/dist/utils/format.d.ts +10 -0
- package/dist/utils/format.js +22 -0
- package/dist/utils/host-detection.js +78 -18
- package/dist/utils/local-storage.d.ts +16 -0
- package/dist/utils/local-storage.js +37 -0
- package/dist/utils/notebook.d.ts +59 -0
- package/dist/utils/notebook.js +211 -0
- package/dist/utils/parquet-metadata.js +1 -1
- package/dist/utils/pmtiles-tile.js +2 -1
- package/dist/utils/pmtiles.js +2 -1
- package/dist/utils/storage-url.d.ts +1 -1
- package/dist/utils/storage-url.js +82 -24
- package/dist/utils/url-state.js +2 -7
- package/dist/utils/url.d.ts +0 -2
- package/dist/utils/url.js +3 -29
- package/dist/utils/zarr.d.ts +60 -20
- package/dist/utils/zarr.js +450 -103
- package/package.json +66 -54
- package/dist/assets/favicon.svg +0 -17
- package/dist/components/CLAUDE.md +0 -44
- package/dist/components/viewers/CLAUDE.md +0 -60
- package/dist/file-icons/CLAUDE.md +0 -21
- package/dist/i18n/CLAUDE.md +0 -19
- package/dist/query/CLAUDE.md +0 -22
- package/dist/storage/CLAUDE.md +0 -23
- package/dist/stores/CLAUDE.md +0 -29
- package/dist/types/notebookjs.d.ts +0 -14
- package/dist/utils/CLAUDE.md +0 -54
- package/dist/utils/analytics.d.ts +0 -10
- package/dist/utils/analytics.js +0 -38
|
@@ -2,16 +2,28 @@
|
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
3
|
import { Badge } from '../ui/badge/index.js';
|
|
4
4
|
import { Button } from '../ui/button/index.js';
|
|
5
|
+
import {
|
|
6
|
+
ResizableHandle,
|
|
7
|
+
ResizablePane,
|
|
8
|
+
ResizablePaneGroup
|
|
9
|
+
} from '../ui/resizable/index.js';
|
|
5
10
|
import { t } from '../../i18n/index.svelte.js';
|
|
6
11
|
import type { Tab } from '../../types';
|
|
7
12
|
import { buildHttpsUrl } from '../../utils/url.js';
|
|
8
13
|
import { getUrlView, updateUrlView } from '../../utils/url-state.js';
|
|
9
14
|
import {
|
|
10
|
-
|
|
15
|
+
computeChunkCount,
|
|
16
|
+
computeChunkSize,
|
|
17
|
+
computeUncompressed,
|
|
18
|
+
DIM_LIKE_NAMES,
|
|
19
|
+
extractZarrStoreUrl,
|
|
20
|
+
fetchHierarchy,
|
|
21
|
+
formatChunkKeys,
|
|
22
|
+
formatCodecs,
|
|
11
23
|
formatShape,
|
|
12
|
-
|
|
13
|
-
type
|
|
14
|
-
type
|
|
24
|
+
inferDims,
|
|
25
|
+
type ZarrHierarchy,
|
|
26
|
+
type ZarrNode
|
|
15
27
|
} from '../../utils/zarr.js';
|
|
16
28
|
|
|
17
29
|
let { tab }: { tab: Tab } = $props();
|
|
@@ -21,17 +33,68 @@ let error = $state<string | null>(null);
|
|
|
21
33
|
const urlView = getUrlView();
|
|
22
34
|
let viewMode = $state<'inspect' | 'map'>(urlView === 'map' ? 'map' : 'inspect');
|
|
23
35
|
|
|
24
|
-
let
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
let
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
let hierarchy = $state.raw<ZarrHierarchy | null>(null);
|
|
37
|
+
let selectedNode = $state<ZarrNode | null>(null);
|
|
38
|
+
/** Set of expanded node paths for the tree view. */
|
|
39
|
+
let expanded = $state(new Set<string>());
|
|
40
|
+
|
|
41
|
+
const hasStoreAttrs = $derived(hierarchy ? Object.keys(hierarchy.storeAttrs).length > 0 : false);
|
|
42
|
+
/** When true, detail panel shows store attrs instead of node details. */
|
|
43
|
+
let showingStoreAttrs = $state(false);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build mapArrays: only include variables that zarr-layer can actually render.
|
|
47
|
+
* All dimensions must have resolvable coordinate arrays at the store root,
|
|
48
|
+
* because @carbonplan/zarr-layer resolves coordinates from root.
|
|
49
|
+
*/
|
|
50
|
+
const mapArrays = $derived.by(() => {
|
|
51
|
+
if (!hierarchy) return [];
|
|
52
|
+
// Collect root-level array names (coordinate arrays that zarr-layer can find)
|
|
53
|
+
const rootArrayNames = new Set(
|
|
54
|
+
hierarchy.root.children.filter((c) => c.kind === 'array').map((c) => c.name)
|
|
55
|
+
);
|
|
56
|
+
const spatialNames = new Set(['x', 'y', 'lat', 'lon', 'latitude', 'longitude']);
|
|
57
|
+
const result: ZarrNode[] = [];
|
|
58
|
+
function walk(n: ZarrNode) {
|
|
59
|
+
if (n.kind === 'array' && n.shape && n.shape.length >= 2) {
|
|
60
|
+
const dims = n.dims?.length ? n.dims : inferDims(n.name, n.shape);
|
|
61
|
+
// Must have at least one spatial dim pair
|
|
62
|
+
let hasLat = false;
|
|
63
|
+
let hasLon = false;
|
|
64
|
+
for (const d of dims) {
|
|
65
|
+
const lower = d.toLowerCase();
|
|
66
|
+
if (lower === 'y' || lower === 'lat' || lower === 'latitude') hasLat = true;
|
|
67
|
+
if (lower === 'x' || lower === 'lon' || lower === 'longitude') hasLon = true;
|
|
68
|
+
}
|
|
69
|
+
if (hasLat && hasLon) {
|
|
70
|
+
// Non-spatial dims must have root-level coordinate arrays
|
|
71
|
+
const nonSpatialResolvable = dims.every(
|
|
72
|
+
(d) => spatialNames.has(d.toLowerCase()) || rootArrayNames.has(d)
|
|
73
|
+
);
|
|
74
|
+
if (nonSpatialResolvable) result.push(n);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const c of n.children) walk(c);
|
|
78
|
+
}
|
|
79
|
+
walk(hierarchy.root);
|
|
80
|
+
return result;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const coordArrays = $derived.by(() => {
|
|
84
|
+
if (!hierarchy) return [];
|
|
85
|
+
const result: ZarrNode[] = [];
|
|
86
|
+
function walk(n: ZarrNode) {
|
|
87
|
+
if (n.kind === 'array' && (DIM_LIKE_NAMES.has(n.name) || (n.shape?.length ?? 0) <= 1))
|
|
88
|
+
result.push(n);
|
|
89
|
+
for (const c of n.children) walk(c);
|
|
90
|
+
}
|
|
91
|
+
walk(hierarchy.root);
|
|
92
|
+
return result;
|
|
93
|
+
});
|
|
30
94
|
|
|
31
|
-
const
|
|
32
|
-
const hasMapVars = $derived(mapVars.length > 0);
|
|
95
|
+
const hasMapVars = $derived(mapArrays.length > 0);
|
|
33
96
|
|
|
34
|
-
// Reset view mode when tab changes
|
|
97
|
+
// Reset view mode when tab changes
|
|
35
98
|
let prevTabId = '';
|
|
36
99
|
$effect(() => {
|
|
37
100
|
const id = tab.id;
|
|
@@ -46,7 +109,7 @@ $effect(() => {
|
|
|
46
109
|
if (!tab) return;
|
|
47
110
|
const _tabId = tab.id;
|
|
48
111
|
untrack(() => {
|
|
49
|
-
|
|
112
|
+
loadHierarchy();
|
|
50
113
|
});
|
|
51
114
|
});
|
|
52
115
|
|
|
@@ -55,27 +118,22 @@ function setViewMode(mode: 'inspect' | 'map') {
|
|
|
55
118
|
updateUrlView(viewMode);
|
|
56
119
|
}
|
|
57
120
|
|
|
58
|
-
async function
|
|
121
|
+
async function loadHierarchy() {
|
|
59
122
|
loading = true;
|
|
60
123
|
error = null;
|
|
61
124
|
|
|
62
125
|
try {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
let meta: ZarrMetadata | null = await fetchConsolidated(url);
|
|
68
|
-
|
|
69
|
-
if (!meta) {
|
|
70
|
-
meta = await probeWithZarrita(url, tab.name.replace(/\.(zarr|zr3)$/, ''));
|
|
71
|
-
}
|
|
126
|
+
const rawUrl = buildHttpsUrl(tab).replace(/\/+$/, '');
|
|
127
|
+
const url = extractZarrStoreUrl(rawUrl) ?? rawUrl;
|
|
128
|
+
const storeName = tab.name.replace(/\.(zarr|zr3)$/, '');
|
|
72
129
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
const h = await fetchHierarchy(url, storeName);
|
|
131
|
+
if (h) {
|
|
132
|
+
hierarchy = h;
|
|
133
|
+
selectedNode = null;
|
|
134
|
+
showingStoreAttrs = false;
|
|
135
|
+
// Auto-expand root children
|
|
136
|
+
expanded = new Set(['/']);
|
|
79
137
|
}
|
|
80
138
|
} catch (err) {
|
|
81
139
|
error = err instanceof Error ? err.message : String(err);
|
|
@@ -84,173 +142,396 @@ async function loadZarrMetadata() {
|
|
|
84
142
|
updateUrlView(viewMode);
|
|
85
143
|
}
|
|
86
144
|
}
|
|
145
|
+
|
|
146
|
+
function toggleExpand(path: string) {
|
|
147
|
+
const next = new Set(expanded);
|
|
148
|
+
if (next.has(path)) {
|
|
149
|
+
next.delete(path);
|
|
150
|
+
} else {
|
|
151
|
+
next.add(path);
|
|
152
|
+
}
|
|
153
|
+
expanded = next;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function selectNode(node: ZarrNode) {
|
|
157
|
+
selectedNode = node;
|
|
158
|
+
showingStoreAttrs = false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function selectStoreAttrs() {
|
|
162
|
+
selectedNode = null;
|
|
163
|
+
showingStoreAttrs = true;
|
|
164
|
+
}
|
|
87
165
|
</script>
|
|
88
166
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
>
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
167
|
+
{#snippet treeNode(node: ZarrNode, depth: number)}
|
|
168
|
+
{@const isExpanded = expanded.has(node.path)}
|
|
169
|
+
{@const hasChildren = node.children.length > 0}
|
|
170
|
+
{@const isSelected = !showingStoreAttrs && selectedNode?.path === node.path}
|
|
171
|
+
<div>
|
|
172
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
173
|
+
<div
|
|
174
|
+
class="flex w-full cursor-pointer items-center gap-1 py-1 pe-3 text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800/50"
|
|
175
|
+
class:bg-blue-50={isSelected}
|
|
176
|
+
class:dark:bg-blue-950={isSelected}
|
|
177
|
+
style="padding-inline-start: {depth * 16 + 8}px"
|
|
178
|
+
role="treeitem"
|
|
179
|
+
tabindex="0"
|
|
180
|
+
aria-selected={isSelected}
|
|
181
|
+
aria-expanded={hasChildren ? isExpanded : undefined}
|
|
182
|
+
onclick={() => selectNode(node)}
|
|
183
|
+
>
|
|
184
|
+
<!-- Expand/collapse toggle -->
|
|
185
|
+
{#if hasChildren}
|
|
186
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
187
|
+
<span
|
|
188
|
+
class="flex size-4 shrink-0 items-center justify-center rounded hover:bg-zinc-200 dark:hover:bg-zinc-700"
|
|
189
|
+
role="button"
|
|
190
|
+
tabindex="-1"
|
|
191
|
+
aria-label={isExpanded ? 'Collapse' : 'Expand'}
|
|
192
|
+
onclick={(e: MouseEvent) => {
|
|
193
|
+
e.stopPropagation();
|
|
194
|
+
toggleExpand(node.path);
|
|
195
|
+
}}
|
|
196
|
+
>
|
|
197
|
+
<svg
|
|
198
|
+
class="size-3 text-zinc-400 transition-transform"
|
|
199
|
+
class:rotate-90={isExpanded}
|
|
200
|
+
viewBox="0 0 16 16"
|
|
201
|
+
fill="currentColor"
|
|
202
|
+
>
|
|
203
|
+
<path
|
|
204
|
+
fill-rule="evenodd"
|
|
205
|
+
d="M6.22 4.22a.75.75 0 0 1 1.06 0l3.25 3.25a.75.75 0 0 1 0 1.06l-3.25 3.25a.75.75 0 0 1-1.06-1.06L8.94 8 6.22 5.28a.75.75 0 0 1 0-1.06Z"
|
|
206
|
+
clip-rule="evenodd"
|
|
207
|
+
/>
|
|
208
|
+
</svg>
|
|
209
|
+
</span>
|
|
210
|
+
{:else}
|
|
211
|
+
<span class="size-4 shrink-0"></span>
|
|
212
|
+
{/if}
|
|
100
213
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
214
|
+
<!-- Icon -->
|
|
215
|
+
{#if node.kind === 'group'}
|
|
216
|
+
<svg class="size-3.5 shrink-0 text-amber-500" viewBox="0 0 16 16" fill="currentColor">
|
|
217
|
+
<path
|
|
218
|
+
d="M1 3.5A1.5 1.5 0 0 1 2.5 2h3.879a1.5 1.5 0 0 1 1.06.44l1.122 1.12A1.5 1.5 0 0 0 9.62 4H13.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9Z"
|
|
219
|
+
/>
|
|
220
|
+
</svg>
|
|
221
|
+
{:else}
|
|
222
|
+
<svg class="size-3.5 shrink-0 text-blue-500" viewBox="0 0 16 16" fill="currentColor">
|
|
223
|
+
<path
|
|
224
|
+
fill-rule="evenodd"
|
|
225
|
+
d="M1.5 2A1.5 1.5 0 0 0 0 3.5v2A1.5 1.5 0 0 0 1.5 7h3A1.5 1.5 0 0 0 6 5.5V5h4v.5A1.5 1.5 0 0 0 11.5 7h3A1.5 1.5 0 0 0 16 5.5v-2A1.5 1.5 0 0 0 14.5 2h-3A1.5 1.5 0 0 0 10 3.5V4H6v-.5A1.5 1.5 0 0 0 4.5 2h-3ZM10 9.5A1.5 1.5 0 0 1 11.5 8h3A1.5 1.5 0 0 1 16 9.5v2a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 10 11.5v-2ZM0 9.5A1.5 1.5 0 0 1 1.5 8h3A1.5 1.5 0 0 1 6 9.5v2A1.5 1.5 0 0 1 4.5 13h-3A1.5 1.5 0 0 1 0 11.5v-2Z"
|
|
226
|
+
clip-rule="evenodd"
|
|
227
|
+
/>
|
|
228
|
+
</svg>
|
|
229
|
+
{/if}
|
|
230
|
+
|
|
231
|
+
<!-- Name -->
|
|
232
|
+
<span
|
|
233
|
+
class="truncate"
|
|
234
|
+
class:font-medium={node.kind === 'array'}
|
|
235
|
+
class:text-zinc-700={node.kind === 'array'}
|
|
236
|
+
class:dark:text-zinc-300={node.kind === 'array'}
|
|
237
|
+
class:text-zinc-600={node.kind === 'group'}
|
|
238
|
+
class:dark:text-zinc-400={node.kind === 'group'}
|
|
107
239
|
>
|
|
108
|
-
{
|
|
109
|
-
</
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</Button>
|
|
240
|
+
{node.path === '/' ? '/ (root)' : node.name}
|
|
241
|
+
</span>
|
|
242
|
+
|
|
243
|
+
<!-- Right badge -->
|
|
244
|
+
{#if node.kind === 'array' && node.dtype}
|
|
245
|
+
<span class="ms-auto shrink-0 text-[10px] tabular-nums text-muted-foreground">
|
|
246
|
+
{node.dtype}
|
|
247
|
+
</span>
|
|
248
|
+
{:else if node.kind === 'group'}
|
|
249
|
+
<span class="ms-auto shrink-0 text-[10px] text-muted-foreground">{t('zarr.group')}</span>
|
|
119
250
|
{/if}
|
|
120
251
|
</div>
|
|
252
|
+
|
|
253
|
+
<!-- Children (lazy: only render when expanded) -->
|
|
254
|
+
{#if hasChildren && isExpanded}
|
|
255
|
+
{#each node.children as child}
|
|
256
|
+
{@render treeNode(child, depth + 1)}
|
|
257
|
+
{/each}
|
|
258
|
+
{/if}
|
|
121
259
|
</div>
|
|
260
|
+
{/snippet}
|
|
122
261
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
262
|
+
{#snippet nodeDetails()}
|
|
263
|
+
{#if showingStoreAttrs && hierarchy}
|
|
264
|
+
<div
|
|
265
|
+
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"
|
|
266
|
+
>
|
|
267
|
+
{t('zarr.storeAttributes')}
|
|
268
|
+
</div>
|
|
269
|
+
<div class="flex-1 overflow-auto p-3">
|
|
270
|
+
<div
|
|
271
|
+
class="rounded border border-zinc-200 bg-zinc-100 p-2 text-xs dark:border-zinc-700 dark:bg-zinc-800"
|
|
272
|
+
>
|
|
273
|
+
{#each Object.entries(hierarchy.storeAttrs) as [key, value]}
|
|
274
|
+
<div class="flex gap-2 py-0.5">
|
|
275
|
+
<span class="shrink-0 font-medium text-muted-foreground">{key}:</span>
|
|
276
|
+
<span class="break-all text-zinc-700 dark:text-zinc-300">
|
|
277
|
+
{typeof value === 'string' ? value : JSON.stringify(value)}
|
|
278
|
+
</span>
|
|
279
|
+
</div>
|
|
280
|
+
{/each}
|
|
132
281
|
</div>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
{
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
class="
|
|
145
|
-
>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
282
|
+
</div>
|
|
283
|
+
{:else if selectedNode}
|
|
284
|
+
<div
|
|
285
|
+
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"
|
|
286
|
+
>
|
|
287
|
+
{selectedNode.path}
|
|
288
|
+
</div>
|
|
289
|
+
<div class="flex-1 overflow-auto p-3">
|
|
290
|
+
<dl class="space-y-2 text-xs">
|
|
291
|
+
<div>
|
|
292
|
+
<dt class="text-muted-foreground">{t('zarr.nodeType')}</dt>
|
|
293
|
+
<dd class="font-mono text-[11px]">{selectedNode.kind}</dd>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{#if hierarchy}
|
|
297
|
+
<div>
|
|
298
|
+
<dt class="text-muted-foreground">{t('zarr.format')}</dt>
|
|
299
|
+
<dd class="font-mono text-[11px]">
|
|
300
|
+
{hierarchy.zarrVersion ? `v${hierarchy.zarrVersion}` : 'unknown'}
|
|
301
|
+
</dd>
|
|
302
|
+
</div>
|
|
303
|
+
{/if}
|
|
304
|
+
|
|
305
|
+
{#if selectedNode.shape}
|
|
306
|
+
<div>
|
|
307
|
+
<dt class="text-muted-foreground">{t('zarr.shape')}</dt>
|
|
308
|
+
<dd class="font-mono text-[11px]">{formatShape(selectedNode.shape)}</dd>
|
|
309
|
+
</div>
|
|
310
|
+
{/if}
|
|
311
|
+
|
|
312
|
+
{#if selectedNode.dims && selectedNode.dims.length > 0}
|
|
313
|
+
<div>
|
|
314
|
+
<dt class="text-muted-foreground">{t('zarr.dimensions')}</dt>
|
|
315
|
+
<dd class="font-mono text-[11px]">({selectedNode.dims.join(', ')})</dd>
|
|
316
|
+
</div>
|
|
317
|
+
{/if}
|
|
318
|
+
|
|
319
|
+
{#if selectedNode.dtype}
|
|
320
|
+
<div>
|
|
321
|
+
<dt class="text-muted-foreground">{t('zarr.dtype')}</dt>
|
|
322
|
+
<dd class="font-mono text-[11px]">{selectedNode.dtype}</dd>
|
|
323
|
+
</div>
|
|
324
|
+
{/if}
|
|
325
|
+
|
|
326
|
+
{#if selectedNode.fillValue != null}
|
|
327
|
+
<div>
|
|
328
|
+
<dt class="text-muted-foreground">{t('zarr.fillValue')}</dt>
|
|
329
|
+
<dd class="font-mono text-[11px]">
|
|
330
|
+
{typeof selectedNode.fillValue === 'string'
|
|
331
|
+
? selectedNode.fillValue
|
|
332
|
+
: JSON.stringify(selectedNode.fillValue)}
|
|
333
|
+
</dd>
|
|
334
|
+
</div>
|
|
335
|
+
{/if}
|
|
336
|
+
|
|
337
|
+
{#if selectedNode.chunks && selectedNode.chunks.length > 0}
|
|
338
|
+
<div>
|
|
339
|
+
<dt class="text-muted-foreground">{t('zarr.chunks')}</dt>
|
|
340
|
+
<dd class="font-mono text-[11px]">[{selectedNode.chunks.join(', ')}]</dd>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{@const chunkCount = computeChunkCount(selectedNode.shape, selectedNode.chunks)}
|
|
344
|
+
{#if chunkCount}
|
|
345
|
+
<div>
|
|
346
|
+
<dt class="text-muted-foreground">{t('zarr.chunkCount')}</dt>
|
|
347
|
+
<dd class="font-mono text-[11px]">{chunkCount}</dd>
|
|
154
348
|
</div>
|
|
155
349
|
{/if}
|
|
156
350
|
|
|
157
|
-
{
|
|
158
|
-
|
|
159
|
-
|
|
351
|
+
{@const chunkSize = computeChunkSize(selectedNode.chunks, selectedNode.dtype)}
|
|
352
|
+
{#if chunkSize}
|
|
353
|
+
<div>
|
|
354
|
+
<dt class="text-muted-foreground">{t('zarr.chunkSize')}</dt>
|
|
355
|
+
<dd class="font-mono text-[11px]">{chunkSize}</dd>
|
|
160
356
|
</div>
|
|
161
|
-
{#each variables as v}
|
|
162
|
-
<button
|
|
163
|
-
class="flex w-full items-center gap-2 px-3 py-1.5 text-start text-xs hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
|
164
|
-
class:bg-blue-50={selectedNode?.name === v.name}
|
|
165
|
-
class:dark:bg-blue-950={selectedNode?.name === v.name}
|
|
166
|
-
onclick={() => (selectedNode = v)}
|
|
167
|
-
>
|
|
168
|
-
<span class="font-medium text-zinc-700 dark:text-zinc-300">{v.name}</span>
|
|
169
|
-
<span class="ms-auto text-zinc-400">{v.dtype}</span>
|
|
170
|
-
</button>
|
|
171
|
-
{/each}
|
|
172
357
|
{/if}
|
|
358
|
+
{/if}
|
|
173
359
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
360
|
+
{#if computeUncompressed(selectedNode.shape, selectedNode.dtype)}
|
|
361
|
+
<div>
|
|
362
|
+
<dt class="text-muted-foreground">{t('zarr.uncompressed')}</dt>
|
|
363
|
+
<dd class="font-mono text-[11px]">
|
|
364
|
+
{computeUncompressed(selectedNode.shape, selectedNode.dtype)}
|
|
365
|
+
</dd>
|
|
366
|
+
</div>
|
|
367
|
+
{/if}
|
|
368
|
+
|
|
369
|
+
{#if formatCodecs(selectedNode)}
|
|
370
|
+
<div>
|
|
371
|
+
<dt class="text-muted-foreground">{t('zarr.codecs')}</dt>
|
|
372
|
+
<dd class="font-mono text-[11px]">{formatCodecs(selectedNode)}</dd>
|
|
373
|
+
</div>
|
|
374
|
+
{/if}
|
|
375
|
+
|
|
376
|
+
{#if formatChunkKeys(selectedNode)}
|
|
377
|
+
<div>
|
|
378
|
+
<dt class="text-muted-foreground">{t('zarr.chunkKeys')}</dt>
|
|
379
|
+
<dd class="font-mono text-[11px]">{formatChunkKeys(selectedNode)}</dd>
|
|
380
|
+
</div>
|
|
381
|
+
{/if}
|
|
382
|
+
|
|
383
|
+
{#if selectedNode.kind === 'group'}
|
|
384
|
+
<div>
|
|
385
|
+
<dt class="text-muted-foreground">{t('zarr.children')}</dt>
|
|
386
|
+
<dd class="font-mono text-[11px]">{selectedNode.children.length}</dd>
|
|
387
|
+
</div>
|
|
388
|
+
{/if}
|
|
389
|
+
|
|
390
|
+
{#if Object.keys(selectedNode.attributes).length > 0}
|
|
391
|
+
<div>
|
|
392
|
+
<dt class="text-muted-foreground">{t('zarr.attributes')}</dt>
|
|
393
|
+
<dd>
|
|
394
|
+
<div
|
|
395
|
+
class="mt-1 rounded border border-zinc-200 bg-zinc-100 p-2 dark:border-zinc-700 dark:bg-zinc-800"
|
|
396
|
+
>
|
|
397
|
+
{#each Object.entries(selectedNode.attributes) as [key, value]}
|
|
398
|
+
<div class="flex gap-2 py-0.5">
|
|
399
|
+
<span class="shrink-0 font-medium text-muted-foreground">{key}:</span>
|
|
400
|
+
<span class="break-all text-zinc-700 dark:text-zinc-300">
|
|
401
|
+
{typeof value === 'string' ? value : JSON.stringify(value)}
|
|
402
|
+
</span>
|
|
403
|
+
</div>
|
|
404
|
+
{/each}
|
|
405
|
+
</div>
|
|
406
|
+
</dd>
|
|
407
|
+
</div>
|
|
408
|
+
{/if}
|
|
409
|
+
</dl>
|
|
410
|
+
</div>
|
|
411
|
+
{:else}
|
|
412
|
+
<div class="flex flex-1 items-center justify-center text-xs text-muted-foreground">
|
|
413
|
+
{t('zarr.selectNode')}
|
|
414
|
+
</div>
|
|
415
|
+
{/if}
|
|
416
|
+
{/snippet}
|
|
417
|
+
|
|
418
|
+
<div class="flex h-full flex-col">
|
|
419
|
+
<!-- Header bar -->
|
|
420
|
+
<div class="shrink-0 border-b border-zinc-200 px-3 py-2 sm:px-4 dark:border-zinc-800">
|
|
421
|
+
<div class="flex items-center gap-1.5 sm:gap-2">
|
|
422
|
+
<span
|
|
423
|
+
class="max-w-[140px] truncate text-sm font-medium text-zinc-700 sm:max-w-none dark:text-zinc-300"
|
|
424
|
+
>{tab.name}</span
|
|
425
|
+
>
|
|
426
|
+
<Badge
|
|
427
|
+
variant="secondary"
|
|
428
|
+
class="bg-purple-100 text-purple-600 dark:bg-purple-900 dark:text-purple-300"
|
|
429
|
+
>
|
|
430
|
+
{hierarchy?.zarrVersion ? `Zarr v${hierarchy.zarrVersion}` : t('zarr.badge')}
|
|
431
|
+
</Badge>
|
|
432
|
+
|
|
433
|
+
{#if hierarchy}
|
|
434
|
+
<span class="hidden text-xs text-muted-foreground sm:inline">
|
|
435
|
+
{hierarchy.totalNodes} {t('zarr.nodes')}
|
|
436
|
+
</span>
|
|
437
|
+
{/if}
|
|
438
|
+
|
|
439
|
+
<div class="ms-auto flex items-center gap-1">
|
|
440
|
+
<Button
|
|
441
|
+
variant={viewMode === 'inspect' ? 'secondary' : 'ghost'}
|
|
442
|
+
size="sm"
|
|
443
|
+
class="h-7 px-2 text-xs"
|
|
444
|
+
onclick={() => setViewMode('inspect')}
|
|
445
|
+
>
|
|
446
|
+
{t('zarr.inspect')}
|
|
447
|
+
</Button>
|
|
448
|
+
{#if hasMapVars}
|
|
449
|
+
<Button
|
|
450
|
+
variant={viewMode === 'map' ? 'secondary' : 'ghost'}
|
|
451
|
+
size="sm"
|
|
452
|
+
class="h-7 px-2 text-xs"
|
|
453
|
+
onclick={() => setViewMode('map')}
|
|
454
|
+
>
|
|
455
|
+
{t('zarr.map')}
|
|
456
|
+
</Button>
|
|
457
|
+
{/if}
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<!-- Content -->
|
|
463
|
+
{#if loading}
|
|
464
|
+
<div class="flex flex-1 items-center justify-center">
|
|
465
|
+
<p class="text-sm text-zinc-400">{t('zarr.loading')}</p>
|
|
466
|
+
</div>
|
|
467
|
+
{:else if error}
|
|
468
|
+
<div class="flex flex-1 items-center justify-center">
|
|
469
|
+
<p class="max-w-md text-center text-sm text-red-400">{error}</p>
|
|
470
|
+
</div>
|
|
471
|
+
{:else if viewMode === 'map' && hasMapVars}
|
|
472
|
+
{#key viewMode}
|
|
473
|
+
{#await import('./ZarrMapViewer.svelte') then ZarrMapViewer}
|
|
474
|
+
<ZarrMapViewer.default
|
|
475
|
+
{tab}
|
|
476
|
+
variables={mapArrays}
|
|
477
|
+
coords={coordArrays}
|
|
478
|
+
spatialRefAttrs={hierarchy?.spatialRefAttrs ?? null}
|
|
479
|
+
zarrVersion={hierarchy?.zarrVersion}
|
|
480
|
+
/>
|
|
481
|
+
{/await}
|
|
482
|
+
{/key}
|
|
483
|
+
{:else if hierarchy}
|
|
484
|
+
<!-- Inspect mode (tree + detail panel) -->
|
|
485
|
+
<ResizablePaneGroup direction="horizontal" class="min-h-0 flex-1">
|
|
486
|
+
<!-- Left: Tree view -->
|
|
487
|
+
<ResizablePane defaultSize={40} minSize={20}>
|
|
488
|
+
<div class="flex h-full flex-col">
|
|
489
|
+
<div
|
|
490
|
+
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"
|
|
491
|
+
>
|
|
492
|
+
{t('zarr.contents')}
|
|
493
|
+
<span class="ms-1 normal-case tracking-normal"
|
|
494
|
+
>({hierarchy.totalNodes})</span
|
|
495
|
+
>
|
|
496
|
+
</div>
|
|
497
|
+
<div class="flex-1 overflow-auto">
|
|
498
|
+
{#if hasStoreAttrs}
|
|
179
499
|
<button
|
|
180
|
-
class="flex w-full items-center gap-2 px-3 py-1
|
|
181
|
-
class:bg-blue-50={
|
|
182
|
-
class:dark:bg-blue-950={
|
|
183
|
-
onclick={
|
|
500
|
+
class="flex w-full items-center gap-2 border-b border-zinc-100 px-3 py-1 text-xs hover:bg-zinc-100 dark:border-zinc-800/50 dark:hover:bg-zinc-800/50"
|
|
501
|
+
class:bg-blue-50={showingStoreAttrs}
|
|
502
|
+
class:dark:bg-blue-950={showingStoreAttrs}
|
|
503
|
+
onclick={selectStoreAttrs}
|
|
184
504
|
>
|
|
185
|
-
<span class="
|
|
186
|
-
<
|
|
505
|
+
<span class="size-4 shrink-0"></span>
|
|
506
|
+
<svg
|
|
507
|
+
class="size-3.5 shrink-0 text-zinc-400"
|
|
508
|
+
viewBox="0 0 16 16"
|
|
509
|
+
fill="currentColor"
|
|
510
|
+
>
|
|
511
|
+
<path
|
|
512
|
+
fill-rule="evenodd"
|
|
513
|
+
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7H3a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-1.5V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
|
|
514
|
+
clip-rule="evenodd"
|
|
515
|
+
/>
|
|
516
|
+
</svg>
|
|
517
|
+
<span class="truncate font-medium text-muted-foreground">
|
|
518
|
+
{t('zarr.storeAttributes')}
|
|
519
|
+
</span>
|
|
187
520
|
</button>
|
|
188
|
-
{/
|
|
189
|
-
|
|
521
|
+
{/if}
|
|
522
|
+
{@render treeNode(hierarchy.root, 0)}
|
|
523
|
+
</div>
|
|
190
524
|
</div>
|
|
525
|
+
</ResizablePane>
|
|
191
526
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<dl class="space-y-2 text-xs">
|
|
199
|
-
<dt class="font-medium text-zinc-500 dark:text-zinc-400">Shape</dt>
|
|
200
|
-
<dd class="font-mono text-zinc-700 dark:text-zinc-300">{formatShape(selectedNode.shape)}</dd>
|
|
201
|
-
|
|
202
|
-
{#if selectedNode.dims.length > 0}
|
|
203
|
-
<dt class="font-medium text-zinc-500 dark:text-zinc-400">Dimensions</dt>
|
|
204
|
-
<dd class="font-mono text-zinc-700 dark:text-zinc-300">
|
|
205
|
-
({selectedNode.dims.join(', ')})
|
|
206
|
-
</dd>
|
|
207
|
-
{/if}
|
|
208
|
-
|
|
209
|
-
<dt class="font-medium text-zinc-500 dark:text-zinc-400">Data Type</dt>
|
|
210
|
-
<dd class="font-mono text-zinc-700 dark:text-zinc-300">{selectedNode.dtype}</dd>
|
|
211
|
-
|
|
212
|
-
{#if selectedNode.chunks.length > 0}
|
|
213
|
-
<dt class="font-medium text-zinc-500 dark:text-zinc-400">Chunks</dt>
|
|
214
|
-
<dd class="font-mono text-zinc-700 dark:text-zinc-300">[{selectedNode.chunks.join(', ')}]</dd>
|
|
215
|
-
{/if}
|
|
216
|
-
|
|
217
|
-
{#if Object.keys(selectedNode.attributes).length > 0}
|
|
218
|
-
<dt class="mt-3 font-medium text-zinc-500 dark:text-zinc-400">Attributes</dt>
|
|
219
|
-
<dd>
|
|
220
|
-
<div class="mt-1 rounded border border-zinc-200 bg-zinc-100 p-2 dark:border-zinc-700 dark:bg-zinc-800">
|
|
221
|
-
{#each Object.entries(selectedNode.attributes) as [key, value]}
|
|
222
|
-
<div class="flex gap-2 py-0.5">
|
|
223
|
-
<span class="shrink-0 font-medium text-zinc-500 dark:text-zinc-400">{key}:</span>
|
|
224
|
-
<span class="break-all text-zinc-700 dark:text-zinc-300">
|
|
225
|
-
{typeof value === 'string' ? value : JSON.stringify(value)}
|
|
226
|
-
</span>
|
|
227
|
-
</div>
|
|
228
|
-
{/each}
|
|
229
|
-
</div>
|
|
230
|
-
</dd>
|
|
231
|
-
{/if}
|
|
232
|
-
</dl>
|
|
233
|
-
{:else if Object.keys(storeAttrs).length > 0}
|
|
234
|
-
<h2 class="mb-3 text-sm font-semibold text-zinc-700 dark:text-zinc-200">
|
|
235
|
-
Store Attributes
|
|
236
|
-
</h2>
|
|
237
|
-
<div class="rounded border border-zinc-200 bg-zinc-100 p-2 text-xs dark:border-zinc-700 dark:bg-zinc-800">
|
|
238
|
-
{#each Object.entries(storeAttrs) as [key, value]}
|
|
239
|
-
<div class="flex gap-2 py-0.5">
|
|
240
|
-
<span class="shrink-0 font-medium text-zinc-500 dark:text-zinc-400">{key}:</span>
|
|
241
|
-
<span class="break-all text-zinc-700 dark:text-zinc-300">
|
|
242
|
-
{typeof value === 'string' ? value : JSON.stringify(value)}
|
|
243
|
-
</span>
|
|
244
|
-
</div>
|
|
245
|
-
{/each}
|
|
246
|
-
</div>
|
|
247
|
-
{:else}
|
|
248
|
-
<div class="flex h-full items-center justify-center">
|
|
249
|
-
<p class="text-sm text-zinc-400">Select a variable from the list</p>
|
|
250
|
-
</div>
|
|
251
|
-
{/if}
|
|
527
|
+
<ResizableHandle />
|
|
528
|
+
|
|
529
|
+
<!-- Right: Detail panel -->
|
|
530
|
+
<ResizablePane defaultSize={60} minSize={30}>
|
|
531
|
+
<div class="flex h-full flex-col">
|
|
532
|
+
{@render nodeDetails()}
|
|
252
533
|
</div>
|
|
253
|
-
</
|
|
254
|
-
|
|
255
|
-
|
|
534
|
+
</ResizablePane>
|
|
535
|
+
</ResizablePaneGroup>
|
|
536
|
+
{/if}
|
|
256
537
|
</div>
|