@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
package/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# objex
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@walkthru-earth/objex)
|
|
4
|
+
[](https://www.npmjs.com/package/@walkthru-earth/objex-utils)
|
|
5
|
+
[](https://github.com/walkthru-earth/objex/actions/workflows/ci.yml)
|
|
6
|
+
[](https://creativecommons.org/licenses/by/4.0/)
|
|
7
|
+
|
|
3
8
|
Cloud storage explorer that runs entirely in the browser. Connect to S3, Azure, GCS, R2, MinIO -- browse files, query data with SQL, and visualize geospatial formats on interactive maps. No backend required.
|
|
4
9
|
|
|
5
10
|
```mermaid
|
|
@@ -13,7 +18,7 @@ graph LR
|
|
|
13
18
|
|
|
14
19
|
## Features
|
|
15
20
|
|
|
16
|
-
- **Browse** cloud storage (S3, GCS, Azure, R2,
|
|
21
|
+
- **Browse** cloud storage (S3, GCS, Azure, R2, B2, DigitalOcean, Wasabi, Storj, Hetzner, Contabo, Linode, OVHcloud, MinIO, direct URLs)
|
|
17
22
|
- **Query** Parquet, CSV, JSONL with SQL (DuckDB-WASM, cancellable queries)
|
|
18
23
|
- **Visualize** GeoParquet, GeoJSON, COG, PMTiles, FlatGeobuf, Zarr on maps (MapLibre + deck.gl)
|
|
19
24
|
- **View** 100+ file formats: code (30+ languages), Jupyter notebooks, PDF, 3D models, archives, media
|
|
@@ -92,12 +97,14 @@ import {
|
|
|
92
97
|
| `./utils/geoarrow` | `buildGeoArrowTables`, `normalizeGeomType` |
|
|
93
98
|
| `./utils/storage-url` | `parseStorageUrl`, `looksLikeUrl` |
|
|
94
99
|
| `./utils/parquet-metadata` | `readParquetMetadata`, `extractEpsgFromGeoMeta` |
|
|
95
|
-
| `./utils/format` | `formatFileSize`, `formatDate`, `getFileExtension` |
|
|
100
|
+
| `./utils/format` | `formatFileSize`, `formatDate`, `formatValue`, `getFileExtension`, `jsonReplacerBigInt` |
|
|
96
101
|
| `./utils/hex` | `generateHexDump` |
|
|
97
102
|
| `./utils/column-types` | `classifyType`, `typeColor`, `typeBadgeClass` |
|
|
98
103
|
| `./file-icons` | `getFileTypeInfo`, `getDuckDbReadFn`, `getViewerKind` |
|
|
99
104
|
| `./types` | `FileEntry`, `Connection`, `Tab`, `WriteResult`, `Theme` |
|
|
100
105
|
|
|
106
|
+
The main export also includes `copyToClipboard`, `handleLoadError`, and shared constants (`WGS84_CODES`, `STORAGE_KEYS`, `DEFAULT_TARGET_CRS`, etc.).
|
|
107
|
+
|
|
101
108
|
## Quick Start (Development)
|
|
102
109
|
|
|
103
110
|
```bash
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { ArrowDown, ArrowUp, ArrowUpDown, FolderOpen, Loader2 } from '@lucide/svelte';
|
|
2
|
+
import { ArrowDown, ArrowUp, ArrowUpDown, FolderOpen, Layers, Loader2 } from '@lucide/svelte';
|
|
3
3
|
import FolderPlusIcon from '@lucide/svelte/icons/folder-plus';
|
|
4
4
|
import { Button } from '../ui/button/index.js';
|
|
5
5
|
import { ScrollArea } from '../ui/scroll-area/index.js';
|
|
6
6
|
import { t } from '../../i18n/index.svelte.js';
|
|
7
7
|
import { browser } from '../../stores/browser.svelte.js';
|
|
8
8
|
import { safeLock } from '../../stores/safelock.svelte.js';
|
|
9
|
+
import { tabs } from '../../stores/tabs.svelte.js';
|
|
9
10
|
import type { FileEntry } from '../../types.js';
|
|
11
|
+
import {
|
|
12
|
+
type SortConfig,
|
|
13
|
+
type SortField,
|
|
14
|
+
sortFileEntries,
|
|
15
|
+
toggleSortField
|
|
16
|
+
} from '../../utils/file-sort.js';
|
|
17
|
+
import { detectZarrMarkers } from '../../utils/zarr.js';
|
|
10
18
|
import Breadcrumb from './Breadcrumb.svelte';
|
|
11
19
|
import CreateFolderDialog from './CreateFolderDialog.svelte';
|
|
12
20
|
import DeleteConfirmDialog from './DeleteConfirmDialog.svelte';
|
|
@@ -16,12 +24,8 @@ import RenameDialog from './RenameDialog.svelte';
|
|
|
16
24
|
import SearchBar from './SearchBar.svelte';
|
|
17
25
|
import UploadButton from './UploadButton.svelte';
|
|
18
26
|
|
|
19
|
-
type SortField = 'name' | 'size' | 'modified' | 'extension';
|
|
20
|
-
type SortDirection = 'asc' | 'desc';
|
|
21
|
-
|
|
22
27
|
let filterQuery = $state('');
|
|
23
|
-
let
|
|
24
|
-
let sortDirection = $state<SortDirection>('asc');
|
|
28
|
+
let sortConfig = $state<SortConfig>({ field: 'name', direction: 'asc' });
|
|
25
29
|
|
|
26
30
|
let deleteDialogOpen = $state(false);
|
|
27
31
|
let deleteTarget = $state<FileEntry | null>(null);
|
|
@@ -31,8 +35,24 @@ let renameTarget = $state<FileEntry | null>(null);
|
|
|
31
35
|
|
|
32
36
|
let showWriteActions = $derived(browser.canWrite && !safeLock.locked);
|
|
33
37
|
|
|
38
|
+
const zarrDetection = $derived(detectZarrMarkers(browser.entries.map((e: FileEntry) => e.name)));
|
|
39
|
+
|
|
40
|
+
function openAsZarr() {
|
|
41
|
+
if (!browser.activeConnection) return;
|
|
42
|
+
const prefix = browser.currentPrefix.replace(/\/+$/, '');
|
|
43
|
+
const name = prefix.split('/').pop() || browser.activeConnection.bucket;
|
|
44
|
+
tabs.open({
|
|
45
|
+
id: `${browser.activeConnection.id}:${prefix}/`,
|
|
46
|
+
name,
|
|
47
|
+
path: `${prefix}/`,
|
|
48
|
+
source: 'remote',
|
|
49
|
+
connectionId: browser.activeConnection.id,
|
|
50
|
+
extension: 'zarr'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
34
54
|
const sortedAndFilteredEntries = $derived.by(() => {
|
|
35
|
-
let result =
|
|
55
|
+
let result = browser.entries;
|
|
36
56
|
|
|
37
57
|
// Filter
|
|
38
58
|
if (filterQuery) {
|
|
@@ -40,28 +60,7 @@ const sortedAndFilteredEntries = $derived.by(() => {
|
|
|
40
60
|
result = result.filter((entry: FileEntry) => entry.name.toLowerCase().includes(q));
|
|
41
61
|
}
|
|
42
62
|
|
|
43
|
-
|
|
44
|
-
const dir = sortDirection === 'asc' ? 1 : -1;
|
|
45
|
-
result.sort((a, b) => {
|
|
46
|
-
// Directories always come first
|
|
47
|
-
if (a.is_dir && !b.is_dir) return -1;
|
|
48
|
-
if (!a.is_dir && b.is_dir) return 1;
|
|
49
|
-
|
|
50
|
-
switch (sortField) {
|
|
51
|
-
case 'name':
|
|
52
|
-
return dir * a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
|
53
|
-
case 'size':
|
|
54
|
-
return dir * (a.size - b.size);
|
|
55
|
-
case 'modified':
|
|
56
|
-
return dir * (a.modified - b.modified);
|
|
57
|
-
case 'extension':
|
|
58
|
-
return dir * a.extension.localeCompare(b.extension, undefined, { sensitivity: 'base' });
|
|
59
|
-
default:
|
|
60
|
-
return 0;
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
return result;
|
|
63
|
+
return sortFileEntries(result, sortConfig);
|
|
65
64
|
});
|
|
66
65
|
|
|
67
66
|
function handleFilter(query: string) {
|
|
@@ -73,12 +72,7 @@ function handleNavigate(path: string) {
|
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
function handleSort(field: SortField) {
|
|
76
|
-
|
|
77
|
-
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
|
78
|
-
} else {
|
|
79
|
-
sortField = field;
|
|
80
|
-
sortDirection = 'asc';
|
|
81
|
-
}
|
|
75
|
+
sortConfig = toggleSortField(sortConfig, field);
|
|
82
76
|
}
|
|
83
77
|
|
|
84
78
|
function handleDelete(entry: FileEntry) {
|
|
@@ -134,8 +128,8 @@ function handleRename(entry: FileEntry) {
|
|
|
134
128
|
onclick={() => handleSort('name')}
|
|
135
129
|
>
|
|
136
130
|
{t('fileBrowser.name')}
|
|
137
|
-
{#if
|
|
138
|
-
{#if
|
|
131
|
+
{#if sortConfig.field === 'name'}
|
|
132
|
+
{#if sortConfig.direction === 'asc'}
|
|
139
133
|
<ArrowUp class="size-3" />
|
|
140
134
|
{:else}
|
|
141
135
|
<ArrowDown class="size-3" />
|
|
@@ -148,8 +142,8 @@ function handleRename(entry: FileEntry) {
|
|
|
148
142
|
class="text-muted-foreground hover:text-foreground flex w-20 shrink-0 items-center justify-end gap-1 transition-colors"
|
|
149
143
|
onclick={() => handleSort('size')}
|
|
150
144
|
>
|
|
151
|
-
{#if
|
|
152
|
-
{#if
|
|
145
|
+
{#if sortConfig.field === 'size'}
|
|
146
|
+
{#if sortConfig.direction === 'asc'}
|
|
153
147
|
<ArrowUp class="size-3" />
|
|
154
148
|
{:else}
|
|
155
149
|
<ArrowDown class="size-3" />
|
|
@@ -161,8 +155,8 @@ function handleRename(entry: FileEntry) {
|
|
|
161
155
|
class="text-muted-foreground hover:text-foreground flex w-24 shrink-0 items-center justify-end gap-1 transition-colors"
|
|
162
156
|
onclick={() => handleSort('modified')}
|
|
163
157
|
>
|
|
164
|
-
{#if
|
|
165
|
-
{#if
|
|
158
|
+
{#if sortConfig.field === 'modified'}
|
|
159
|
+
{#if sortConfig.direction === 'asc'}
|
|
166
160
|
<ArrowUp class="size-3" />
|
|
167
161
|
{:else}
|
|
168
162
|
<ArrowDown class="size-3" />
|
|
@@ -172,6 +166,24 @@ function handleRename(entry: FileEntry) {
|
|
|
172
166
|
</button>
|
|
173
167
|
</div>
|
|
174
168
|
|
|
169
|
+
<!-- Zarr detection banner -->
|
|
170
|
+
{#if zarrDetection.detected}
|
|
171
|
+
<div class="border-border flex items-center gap-2 border-b bg-purple-50 px-3 py-1.5 dark:bg-purple-950/30">
|
|
172
|
+
<Layers class="size-3.5 shrink-0 text-purple-500" />
|
|
173
|
+
<span class="flex-1 text-xs text-purple-700 dark:text-purple-300">
|
|
174
|
+
{t('fileBrowser.zarrDetected', { version: String(zarrDetection.version ?? '?') })}
|
|
175
|
+
</span>
|
|
176
|
+
<Button
|
|
177
|
+
variant="outline"
|
|
178
|
+
size="sm"
|
|
179
|
+
class="h-6 gap-1 px-2 text-[11px]"
|
|
180
|
+
onclick={openAsZarr}
|
|
181
|
+
>
|
|
182
|
+
{t('fileBrowser.openAsZarr')}
|
|
183
|
+
</Button>
|
|
184
|
+
</div>
|
|
185
|
+
{/if}
|
|
186
|
+
|
|
175
187
|
<!-- File list -->
|
|
176
188
|
<div class="relative min-h-0 flex-1">
|
|
177
189
|
<DropZone>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import PencilIcon from '@lucide/svelte/icons/pencil';
|
|
3
3
|
import Trash2Icon from '@lucide/svelte/icons/trash-2';
|
|
4
|
+
import { VIEWER_DIR_EXTENSIONS } from '../../constants.js';
|
|
4
5
|
import FileTypeIcon from '../../file-icons/FileTypeIcon.svelte';
|
|
5
6
|
import { getFileTypeInfo } from '../../file-icons/index.js';
|
|
6
7
|
import { t } from '../../i18n/index.svelte.js';
|
|
@@ -18,11 +19,15 @@ interface Props {
|
|
|
18
19
|
|
|
19
20
|
let { entry, onDelete, onRename }: Props = $props();
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
function isViewerDir(e: FileEntry): boolean {
|
|
23
|
+
return e.is_dir && VIEWER_DIR_EXTENSIONS.has(e.extension);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const info = $derived(getFileTypeInfo(entry.extension, entry.is_dir && !isViewerDir(entry)));
|
|
22
27
|
let showActions = $derived(browser.canWrite && !safeLock.locked);
|
|
23
28
|
|
|
24
29
|
function handleClick() {
|
|
25
|
-
if (entry.is_dir) {
|
|
30
|
+
if (entry.is_dir && !isViewerDir(entry)) {
|
|
26
31
|
browser.navigateTo(entry.path);
|
|
27
32
|
} else {
|
|
28
33
|
if (browser.activeConnection) {
|
|
@@ -68,7 +73,7 @@ function handleRenameClick(e: MouseEvent) {
|
|
|
68
73
|
>
|
|
69
74
|
<!-- Icon -->
|
|
70
75
|
<div class="flex shrink-0 items-center justify-center">
|
|
71
|
-
<FileTypeIcon extension={entry.extension} isDir={entry.is_dir} class="size-4" />
|
|
76
|
+
<FileTypeIcon extension={entry.extension} isDir={entry.is_dir && !isViewerDir(entry)} class="size-4" />
|
|
72
77
|
</div>
|
|
73
78
|
|
|
74
79
|
<!-- File name -->
|
|
@@ -10,13 +10,14 @@ import Loader2Icon from '@lucide/svelte/icons/loader-2';
|
|
|
10
10
|
import RefreshCwIcon from '@lucide/svelte/icons/refresh-cw';
|
|
11
11
|
import SearchIcon from '@lucide/svelte/icons/search';
|
|
12
12
|
import * as ContextMenu from '../ui/context-menu/index.js';
|
|
13
|
+
import { VIEWER_DIR_EXTENSIONS } from '../../constants.js';
|
|
13
14
|
import FileTypeIcon from '../../file-icons/FileTypeIcon.svelte';
|
|
14
15
|
import { t } from '../../i18n/index.svelte.js';
|
|
15
16
|
import { getAdapter } from '../../storage/index.js';
|
|
16
17
|
import { browser } from '../../stores/browser.svelte.js';
|
|
17
18
|
import { tabs } from '../../stores/tabs.svelte.js';
|
|
18
19
|
import type { Connection, FileEntry } from '../../types.js';
|
|
19
|
-
import { getNativeScheme } from '../../utils/url.js';
|
|
20
|
+
import { getNativeScheme } from '../../utils/cloud-url.js';
|
|
20
21
|
import { syncUrlParam } from '../../utils/url-state.js';
|
|
21
22
|
|
|
22
23
|
let {
|
|
@@ -170,9 +171,6 @@ function openFile(entry: FileEntry) {
|
|
|
170
171
|
syncUrlParam(connection, entry.path);
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
/** Extensions that represent "virtual files" — directories that open as viewers. */
|
|
174
|
-
const VIEWER_DIR_EXTENSIONS = new Set(['zarr', 'zr3']);
|
|
175
|
-
|
|
176
174
|
function isViewerDir(entry: FileEntry): boolean {
|
|
177
175
|
return entry.is_dir && VIEWER_DIR_EXTENSIONS.has(entry.extension);
|
|
178
176
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
declare const __APP_VERSION__: string;
|
|
3
|
+
declare const __THIRD_PARTY_LICENSES__: {
|
|
4
|
+
license: string;
|
|
5
|
+
packages: { name: string; url: string }[];
|
|
6
|
+
}[];
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
|
11
|
+
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
|
12
|
+
import GithubIcon from '@lucide/svelte/icons/github';
|
|
13
|
+
import {
|
|
14
|
+
Sheet,
|
|
15
|
+
SheetContent,
|
|
16
|
+
SheetDescription,
|
|
17
|
+
SheetHeader,
|
|
18
|
+
SheetTitle
|
|
19
|
+
} from '../ui/sheet/index.js';
|
|
20
|
+
import { t } from '../../i18n/index.svelte.js';
|
|
21
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
open: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let { open = $bindable(false) }: Props = $props();
|
|
27
|
+
|
|
28
|
+
let licensesOpen = $state(false);
|
|
29
|
+
|
|
30
|
+
const version = __APP_VERSION__;
|
|
31
|
+
|
|
32
|
+
const thirdPartyLicenses = __THIRD_PARTY_LICENSES__;
|
|
33
|
+
|
|
34
|
+
$effect(() => {
|
|
35
|
+
if (!open) licensesOpen = false;
|
|
36
|
+
});
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<Sheet bind:open>
|
|
40
|
+
<SheetContent side="bottom" class="max-h-[85vh] sm:mx-auto sm:max-w-lg sm:rounded-t-lg">
|
|
41
|
+
<SheetHeader>
|
|
42
|
+
<SheetTitle>{t('about.title')}</SheetTitle>
|
|
43
|
+
<SheetDescription class="sr-only">
|
|
44
|
+
{t('about.version', { version })}
|
|
45
|
+
</SheetDescription>
|
|
46
|
+
</SheetHeader>
|
|
47
|
+
|
|
48
|
+
<div class="flex flex-col items-center gap-4 overflow-y-auto px-4 py-6 sm:px-6">
|
|
49
|
+
<!-- walkthru.earth logo/link -->
|
|
50
|
+
<a
|
|
51
|
+
href="https://walkthru.earth/links"
|
|
52
|
+
target="_blank"
|
|
53
|
+
rel="noopener noreferrer"
|
|
54
|
+
class="group flex flex-col items-center gap-2 transition-opacity hover:opacity-80"
|
|
55
|
+
>
|
|
56
|
+
<img src="https://walkthru.earth/icon.svg" alt="walkthru.earth" class="size-12" />
|
|
57
|
+
<span class="flex items-center gap-1 text-lg font-semibold text-foreground">
|
|
58
|
+
walkthru.earth
|
|
59
|
+
<ExternalLinkIcon
|
|
60
|
+
class="size-3.5 opacity-0 transition-opacity group-hover:opacity-100"
|
|
61
|
+
/>
|
|
62
|
+
</span>
|
|
63
|
+
</a>
|
|
64
|
+
|
|
65
|
+
<!-- Version + License -->
|
|
66
|
+
<div class="flex flex-col items-center gap-1 text-sm text-muted-foreground">
|
|
67
|
+
<span>{t('about.version', { version })}</span>
|
|
68
|
+
<span>{t('about.license')}</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- GitHub link -->
|
|
72
|
+
<a
|
|
73
|
+
href="https://github.com/walkthru-earth/objex"
|
|
74
|
+
target="_blank"
|
|
75
|
+
rel="noopener noreferrer"
|
|
76
|
+
class="inline-flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
|
77
|
+
>
|
|
78
|
+
<GithubIcon class="size-4" />
|
|
79
|
+
{t('about.sourceCode')}
|
|
80
|
+
</a>
|
|
81
|
+
|
|
82
|
+
<!-- Third-party licenses -->
|
|
83
|
+
<div class="w-full border-t pt-3">
|
|
84
|
+
<button
|
|
85
|
+
class="flex w-full items-center justify-between rounded-md px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
86
|
+
onclick={() => {
|
|
87
|
+
licensesOpen = !licensesOpen;
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<span>{t('about.openSourceLicenses')}</span>
|
|
91
|
+
<ChevronDownIcon
|
|
92
|
+
class="size-3.5 transition-transform {licensesOpen ? 'rotate-180' : ''}"
|
|
93
|
+
/>
|
|
94
|
+
</button>
|
|
95
|
+
|
|
96
|
+
{#if licensesOpen}
|
|
97
|
+
<div
|
|
98
|
+
class="mt-2 flex max-h-48 flex-col gap-3 overflow-y-auto rounded-lg bg-muted/40 p-3 sm:max-h-60"
|
|
99
|
+
>
|
|
100
|
+
{#each thirdPartyLicenses as group}
|
|
101
|
+
<div>
|
|
102
|
+
<span
|
|
103
|
+
class="inline-block rounded bg-muted px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground"
|
|
104
|
+
>
|
|
105
|
+
{group.license}
|
|
106
|
+
</span>
|
|
107
|
+
<div class="mt-1.5 flex flex-wrap gap-1">
|
|
108
|
+
{#each group.packages as pkg}
|
|
109
|
+
<a
|
|
110
|
+
href={pkg.url}
|
|
111
|
+
target="_blank"
|
|
112
|
+
rel="noopener noreferrer"
|
|
113
|
+
class="rounded-md border border-border/50 bg-background px-2 py-0.5 text-[11px] text-muted-foreground transition-colors hover:border-border hover:text-foreground"
|
|
114
|
+
>
|
|
115
|
+
{pkg.name}
|
|
116
|
+
</a>
|
|
117
|
+
{/each}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
{/each}
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</SheetContent>
|
|
126
|
+
</Sheet>
|