@walkthru-earth/objex 1.3.1 → 1.5.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 (199) hide show
  1. package/LICENSE +5 -0
  2. package/README.md +28 -20
  3. package/dist/components/browser/FileTreeSidebar.svelte +32 -17
  4. package/dist/components/layout/AboutSheet.svelte +5 -2
  5. package/dist/components/layout/ConnectionDialog.svelte +7 -2
  6. package/dist/components/layout/SettingsSheet.svelte +238 -0
  7. package/dist/components/layout/SettingsSheet.svelte.d.ts +6 -0
  8. package/dist/components/layout/Sidebar.svelte +73 -6
  9. package/dist/components/layout/Sidebar.svelte.d.ts +4 -1
  10. package/dist/components/layout/StatusBar.svelte +17 -14
  11. package/dist/components/layout/TabBar.svelte +4 -4
  12. package/dist/components/ui/context-menu/context-menu-radio-group.svelte.d.ts +1 -1
  13. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte.d.ts +1 -1
  14. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte.d.ts +1 -1
  15. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  16. package/dist/components/ui/resizable/index.d.ts +1 -1
  17. package/dist/components/ui/resizable/index.js +2 -2
  18. package/dist/components/ui/slider/index.d.ts +3 -0
  19. package/dist/components/ui/slider/index.js +5 -0
  20. package/dist/components/ui/slider/range-slider.svelte +94 -0
  21. package/dist/components/ui/slider/range-slider.svelte.d.ts +21 -0
  22. package/dist/components/ui/slider/slider.svelte +83 -0
  23. package/dist/components/ui/slider/slider.svelte.d.ts +7 -0
  24. package/dist/components/viewers/ArchiveViewer.svelte +140 -113
  25. package/dist/components/viewers/CodeViewer.svelte +45 -48
  26. package/dist/components/viewers/CodeViewer.svelte.d.ts +1 -1
  27. package/dist/components/viewers/CogControls.svelte +338 -184
  28. package/dist/components/viewers/CogControls.svelte.d.ts +33 -10
  29. package/dist/components/viewers/CogViewer.svelte +269 -116
  30. package/dist/components/viewers/CopcViewer.svelte +8 -15
  31. package/dist/components/viewers/DatabaseViewer.svelte +22 -21
  32. package/dist/components/viewers/FileInfo.svelte +16 -16
  33. package/dist/components/viewers/FlatGeobufViewer.svelte +16 -46
  34. package/dist/components/viewers/GeoParquetMapViewer.svelte +11 -9
  35. package/dist/components/viewers/GeoParquetMapViewer.svelte.d.ts +1 -1
  36. package/dist/components/viewers/ImageViewer.svelte +12 -14
  37. package/dist/components/viewers/LoadProgress.svelte +6 -6
  38. package/dist/components/viewers/MarkdownViewer.svelte +29 -30
  39. package/dist/components/viewers/MediaViewer.svelte +13 -14
  40. package/dist/components/viewers/ModelViewer.svelte +18 -21
  41. package/dist/components/viewers/MultiCogViewer.svelte +474 -106
  42. package/dist/components/viewers/MultiCogViewer.svelte.d.ts +1 -1
  43. package/dist/components/viewers/NotebookViewer.svelte +28 -29
  44. package/dist/components/viewers/PdfViewer.svelte +24 -33
  45. package/dist/components/viewers/PmtilesViewer.svelte +13 -15
  46. package/dist/components/viewers/QueryHistoryPanel.svelte +18 -18
  47. package/dist/components/viewers/RawViewer.svelte +27 -21
  48. package/dist/components/viewers/StacMapViewer.svelte +6 -13
  49. package/dist/components/viewers/StacMosaicViewer.svelte +1764 -410
  50. package/dist/components/viewers/StacMosaicViewer.svelte.d.ts +1 -1
  51. package/dist/components/viewers/StacTabViewer.svelte +26 -15
  52. package/dist/components/viewers/StacTabViewer.svelte.d.ts +1 -1
  53. package/dist/components/viewers/TableGrid.svelte +38 -34
  54. package/dist/components/viewers/TableStatusBar.svelte +7 -7
  55. package/dist/components/viewers/TableToolbar.svelte +10 -9
  56. package/dist/components/viewers/TableViewer.svelte +47 -30
  57. package/dist/components/viewers/TableViewer.svelte.d.ts +1 -0
  58. package/dist/components/viewers/ViewerHeader.svelte +18 -0
  59. package/dist/components/viewers/ViewerHeader.svelte.d.ts +10 -0
  60. package/dist/components/viewers/ViewerRouter.svelte +16 -8
  61. package/dist/components/viewers/ViewerStatus.svelte +19 -0
  62. package/dist/components/viewers/ViewerStatus.svelte.d.ts +7 -0
  63. package/dist/components/viewers/ZarrMapViewer.svelte +24 -21
  64. package/dist/components/viewers/ZarrViewer.svelte +98 -65
  65. package/dist/components/viewers/cog/ChannelPicker.svelte +83 -0
  66. package/dist/components/viewers/cog/ChannelPicker.svelte.d.ts +13 -0
  67. package/dist/components/viewers/cog/PixelInspectorPanel.svelte +87 -0
  68. package/dist/components/viewers/cog/PixelInspectorPanel.svelte.d.ts +17 -0
  69. package/dist/components/viewers/cog/buildRgbLayer.d.ts +78 -0
  70. package/dist/components/viewers/cog/buildRgbLayer.js +176 -0
  71. package/dist/components/viewers/map/AttributeTable.svelte +7 -7
  72. package/dist/components/viewers/map/MapContainer.svelte +38 -12
  73. package/dist/components/viewers/pmtiles/PmtilesArchiveView.svelte +109 -83
  74. package/dist/components/viewers/pmtiles/PmtilesTileInspector.svelte +16 -16
  75. package/dist/components/viewers/stac/StacDatetimeBar.svelte +175 -0
  76. package/dist/components/viewers/stac/StacDatetimeBar.svelte.d.ts +10 -0
  77. package/dist/components/viewers/stac/StacFilterPanel.svelte +243 -0
  78. package/dist/components/viewers/stac/StacFilterPanel.svelte.d.ts +14 -0
  79. package/dist/components/viewers/stac/StacItemInspector.svelte +223 -0
  80. package/dist/components/viewers/stac/StacItemInspector.svelte.d.ts +10 -0
  81. package/dist/components/viewers/stac/StacItemStrip.svelte +228 -0
  82. package/dist/components/viewers/stac/StacItemStrip.svelte.d.ts +12 -0
  83. package/dist/constants.d.ts +6 -0
  84. package/dist/constants.js +8 -0
  85. package/dist/file-icons/index.d.ts +1 -1
  86. package/dist/file-icons/index.js +1 -1
  87. package/dist/i18n/ar.js +113 -2
  88. package/dist/i18n/en.js +113 -2
  89. package/dist/index.d.ts +2 -28
  90. package/dist/index.js +7 -23
  91. package/dist/query/engine.d.ts +10 -0
  92. package/dist/query/source.js +1 -1
  93. package/dist/query/stac-source-factory.d.ts +65 -0
  94. package/dist/query/stac-source-factory.js +77 -0
  95. package/dist/query/stac-source-parquet.d.ts +135 -0
  96. package/dist/query/stac-source-parquet.js +468 -0
  97. package/dist/query/wasm.d.ts +8 -0
  98. package/dist/query/wasm.js +310 -65
  99. package/dist/storage/presign.js +3 -2
  100. package/dist/storage/providers.js +7 -6
  101. package/dist/stores/config.svelte.d.ts +15 -0
  102. package/dist/stores/config.svelte.js +46 -0
  103. package/dist/stores/connections.svelte.d.ts +2 -2
  104. package/dist/stores/connections.svelte.js +1 -2
  105. package/dist/stores/files.svelte.d.ts +1 -1
  106. package/dist/stores/files.svelte.js +1 -1
  107. package/dist/stores/query-history.svelte.js +1 -1
  108. package/dist/stores/settings.svelte.d.ts +16 -1
  109. package/dist/stores/settings.svelte.js +104 -48
  110. package/dist/stores/tabs.svelte.d.ts +3 -0
  111. package/dist/stores/tabs.svelte.js +17 -0
  112. package/dist/utils/cog-histogram.d.ts +121 -0
  113. package/dist/utils/cog-histogram.js +424 -0
  114. package/dist/utils/cog.d.ts +177 -20
  115. package/dist/utils/cog.js +361 -76
  116. package/dist/utils/colormap-sprite.d.ts +0 -9
  117. package/dist/utils/colormap-sprite.js +0 -21
  118. package/dist/utils/deck.d.ts +18 -12
  119. package/dist/utils/deck.js +15 -7
  120. package/dist/utils/media-query.svelte.d.ts +14 -0
  121. package/dist/utils/media-query.svelte.js +29 -0
  122. package/dist/utils/pmtiles-tile.js +2 -2
  123. package/dist/utils/signed-url-effect.d.ts +7 -0
  124. package/dist/utils/signed-url-effect.js +19 -0
  125. package/dist/utils/{url.d.ts → signed-url.d.ts} +15 -1
  126. package/dist/utils/{url.js → signed-url.js} +32 -10
  127. package/dist/utils/url-state.d.ts +36 -0
  128. package/dist/utils/url-state.js +72 -2
  129. package/dist/utils/zarr-tab.d.ts +1 -2
  130. package/dist/utils/zarr-tab.js +1 -2
  131. package/dist/utils/zarr.d.ts +0 -17
  132. package/dist/utils/zarr.js +1 -45
  133. package/package.json +55 -84
  134. package/dist/components/browser/Breadcrumb.svelte +0 -50
  135. package/dist/components/browser/Breadcrumb.svelte.d.ts +0 -7
  136. package/dist/components/browser/CreateFolderDialog.svelte +0 -98
  137. package/dist/components/browser/CreateFolderDialog.svelte.d.ts +0 -6
  138. package/dist/components/browser/DeleteConfirmDialog.svelte +0 -90
  139. package/dist/components/browser/DeleteConfirmDialog.svelte.d.ts +0 -8
  140. package/dist/components/browser/DropZone.svelte +0 -83
  141. package/dist/components/browser/DropZone.svelte.d.ts +0 -7
  142. package/dist/components/browser/FileBrowser.svelte +0 -252
  143. package/dist/components/browser/FileBrowser.svelte.d.ts +0 -3
  144. package/dist/components/browser/FileRow.svelte +0 -117
  145. package/dist/components/browser/FileRow.svelte.d.ts +0 -9
  146. package/dist/components/browser/RenameDialog.svelte +0 -101
  147. package/dist/components/browser/RenameDialog.svelte.d.ts +0 -8
  148. package/dist/components/browser/SearchBar.svelte +0 -40
  149. package/dist/components/browser/SearchBar.svelte.d.ts +0 -6
  150. package/dist/components/browser/UploadButton.svelte +0 -65
  151. package/dist/components/browser/UploadButton.svelte.d.ts +0 -3
  152. package/dist/query/stac-geoparquet.d.ts +0 -31
  153. package/dist/query/stac-geoparquet.js +0 -136
  154. package/dist/utils/clipboard.d.ts +0 -13
  155. package/dist/utils/clipboard.js +0 -38
  156. package/dist/utils/cloud-url.d.ts +0 -27
  157. package/dist/utils/cloud-url.js +0 -61
  158. package/dist/utils/cog-pure.d.ts +0 -25
  159. package/dist/utils/cog-pure.js +0 -35
  160. package/dist/utils/column-types.d.ts +0 -5
  161. package/dist/utils/column-types.js +0 -137
  162. package/dist/utils/connection-identity.d.ts +0 -51
  163. package/dist/utils/connection-identity.js +0 -97
  164. package/dist/utils/error.d.ts +0 -8
  165. package/dist/utils/error.js +0 -12
  166. package/dist/utils/evidence-context.d.ts +0 -22
  167. package/dist/utils/evidence-context.js +0 -56
  168. package/dist/utils/export.d.ts +0 -22
  169. package/dist/utils/export.js +0 -76
  170. package/dist/utils/file-sort.d.ts +0 -20
  171. package/dist/utils/file-sort.js +0 -41
  172. package/dist/utils/format.d.ts +0 -24
  173. package/dist/utils/format.js +0 -78
  174. package/dist/utils/geoarrow.d.ts +0 -32
  175. package/dist/utils/geoarrow.js +0 -672
  176. package/dist/utils/geometry-type.d.ts +0 -52
  177. package/dist/utils/geometry-type.js +0 -76
  178. package/dist/utils/hex.d.ts +0 -10
  179. package/dist/utils/hex.js +0 -27
  180. package/dist/utils/host-detection.d.ts +0 -23
  181. package/dist/utils/host-detection.js +0 -95
  182. package/dist/utils/local-storage.d.ts +0 -16
  183. package/dist/utils/local-storage.js +0 -37
  184. package/dist/utils/markdown-sql.d.ts +0 -30
  185. package/dist/utils/markdown-sql.js +0 -72
  186. package/dist/utils/notebook.d.ts +0 -59
  187. package/dist/utils/notebook.js +0 -211
  188. package/dist/utils/parquet-metadata.d.ts +0 -64
  189. package/dist/utils/parquet-metadata.js +0 -262
  190. package/dist/utils/stac-geoparquet.d.ts +0 -90
  191. package/dist/utils/stac-geoparquet.js +0 -223
  192. package/dist/utils/stac-hydrate.d.ts +0 -38
  193. package/dist/utils/stac-hydrate.js +0 -243
  194. package/dist/utils/stac.d.ts +0 -136
  195. package/dist/utils/stac.js +0 -176
  196. package/dist/utils/storage-url.d.ts +0 -90
  197. package/dist/utils/storage-url.js +0 -568
  198. package/dist/utils/wkb.d.ts +0 -43
  199. package/dist/utils/wkb.js +0 -359
package/LICENSE CHANGED
@@ -1,3 +1,8 @@
1
+ Copyright (c) 2026 walkthru.earth <hi@walkthru.earth>
2
+
3
+ This work is licensed under the Creative Commons Attribution 4.0 International License.
4
+ To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/
5
+
1
6
  Attribution 4.0 International
2
7
 
3
8
  =======================================================================
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![CI](https://github.com/walkthru-earth/objex/actions/workflows/ci.yml/badge.svg)](https://github.com/walkthru-earth/objex/actions/workflows/ci.yml)
6
6
  [![License: CC BY 4.0](https://img.shields.io/badge/license-CC%20BY%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by/4.0/)
7
7
 
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.
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.
9
9
 
10
10
  ```mermaid
11
11
  graph LR
@@ -22,9 +22,10 @@ graph LR
22
22
  - **Query** Parquet, CSV, JSONL with SQL (DuckDB-WASM, cancellable queries)
23
23
  - **Visualize** GeoParquet, GeoJSON, COG, PMTiles, FlatGeobuf, Zarr (incl. GeoZarr), STAC catalogs, and stac-geoparquet on maps (MapLibre + deck.gl)
24
24
  - **View** 100+ file formats: code (30+ languages), Jupyter notebooks, PDF, 3D models, archives, media
25
- - **Share** via URL -- `?url=<storage-url>#<view>` encodes full viewer state
26
- - **i18n** -- English + Arabic with automatic RTL layout
27
- - **Zero backend** -- everything runs client-side
25
+ - **Share** via URL - `?url=<storage-url>#<view>` encodes full viewer state
26
+ - **Configure** without a rebuild - bundled `config.json` (or remote `?config=<url>`) sets defaults, basemaps, and seed connections, with an in-app settings panel
27
+ - **i18n** - English + Arabic with automatic RTL layout
28
+ - **Zero backend** - everything runs client-side
28
29
 
29
30
  ## Supported Formats
30
31
 
@@ -47,7 +48,7 @@ graph LR
47
48
 
48
49
  Two packages are published for downstream use:
49
50
 
50
- ### `@walkthru-earth/objex` -- Full Svelte 5 Library
51
+ ### `@walkthru-earth/objex` - Full Svelte 5 Library
51
52
 
52
53
  Components, stores, and utilities for building geospatial storage explorers.
53
54
 
@@ -58,15 +59,12 @@ npm install @walkthru-earth/objex
58
59
  ```ts
59
60
  import { parseStorageUrl, formatFileSize } from '@walkthru-earth/objex';
60
61
  import { UrlAdapter } from '@walkthru-earth/objex/storage';
61
- import { parseWKB } from '@walkthru-earth/objex/utils/wkb';
62
- import { buildGeoArrowTables } from '@walkthru-earth/objex/utils/geoarrow';
63
- import { readParquetMetadata } from '@walkthru-earth/objex/utils/parquet-metadata';
64
62
  import { getFileTypeInfo } from '@walkthru-earth/objex/file-icons';
65
63
  ```
66
64
 
67
- Requires `svelte ^5` and `@sveltejs/kit ^2` as peer dependencies. Heavy deps (DuckDB, deck.gl, MapLibre, Arrow, hyparquet, hyparquet-compressors, yaml) are optional peers -- only install what you need.
65
+ Requires `svelte ^5` and `@sveltejs/kit ^2` as peer dependencies. Heavy deps (DuckDB, deck.gl, MapLibre, Arrow, hyparquet, hyparquet-compressors, yaml) are optional peers - only install what you need.
68
66
 
69
- ### `@walkthru-earth/objex-utils` -- Pure TypeScript Utilities
67
+ ### `@walkthru-earth/objex-utils` - Pure TypeScript Utilities
70
68
 
71
69
  Zero Svelte dependency. Works with any JS framework or Node.js.
72
70
 
@@ -95,20 +93,30 @@ Full per-module reference docs: [`packages/objex-utils/docs/`](packages/objex-ut
95
93
 
96
94
  | Export path | What |
97
95
  |-------------|------|
98
- | `@walkthru-earth/objex` | All types, utils, storage, query engine |
96
+ | `@walkthru-earth/objex` | All types, pure utils, storage, query engine |
99
97
  | `./storage` | `StorageAdapter`, `UrlAdapter` |
100
98
  | `./query` | `QueryEngine`, `QueryCancelledError` |
101
- | `./utils/wkb` | `parseWKB`, `toBinary`, `findGeoColumn` |
102
- | `./utils/geoarrow` | `buildGeoArrowTables`, `normalizeGeomType` |
103
- | `./utils/storage-url` | `parseStorageUrl`, `looksLikeUrl` |
104
- | `./utils/parquet-metadata` | `readParquetMetadata`, `extractEpsgFromGeoMeta` |
105
- | `./utils/format` | `formatFileSize`, `formatDate`, `formatValue`, `getFileExtension`, `jsonReplacerBigInt` |
106
- | `./utils/hex` | `generateHexDump` |
107
- | `./utils/column-types` | `classifyType`, `typeColor`, `typeBadgeClass` |
108
99
  | `./file-icons` | `getFileTypeInfo`, `getDuckDbReadFn`, `getViewerKind` |
109
100
  | `./types` | `FileEntry`, `Connection`, `Tab`, `WriteResult`, `Theme` |
110
101
 
111
- The main export also includes `copyToClipboard`, `handleLoadError`, the stac-geoparquet helpers (`isStacGeoparquetSchema`, `stacRowToItem`, `flattenStacBbox`, `pickStacPrimaryAsset`, `resolveStacAssetHref`), and shared constants (`WGS84_CODES`, `STORAGE_KEYS`, `DEFAULT_TARGET_CRS`, etc.).
102
+ The pure utilities live in `@walkthru-earth/objex-utils` and are also re-exported from the package root, so `parseStorageUrl`, `parseWKB`, `buildGeoArrowTables`, `readParquetMetadata`, `formatFileSize`, `generateHexDump`, `classifyType`, and the rest are importable straight from `@walkthru-earth/objex`. The root export also includes `copyToClipboard`, `handleLoadError`, the stac-geoparquet helpers (`isStacGeoparquetSchema`, `stacRowToItem`, `flattenStacBbox`, `pickStacPrimaryAsset`, `resolveStacAssetHref`), and shared constants (`WGS84_CODES`, `STORAGE_KEYS`, `DEFAULT_TARGET_CRS`, etc.).
103
+
104
+ ## Configuration
105
+
106
+ objex reads a bundled `static/config.json` at startup, so a host can customize the app without rebuilding. Pass `?config=<url>` to load a remote config that overrides the bundled one.
107
+
108
+ `config.json` sets the default theme and language, query and mosaic row limits, the basemap list with a per-theme default basemap, and seed connections that load on first visit. The `ui` block toggles whether the connection rail, file tree, and settings panel are shown.
109
+
110
+ Several of these are also reachable as query params, handy for embedding and deep links.
111
+
112
+ | Param | Effect |
113
+ |-------|--------|
114
+ | `?config=<url>` | Load a remote `config.json` instead of the bundled one |
115
+ | `?panel=settings` | Open the settings panel on load |
116
+ | `?rail=hide` / `?rail=show` | Hide or show the connection rail |
117
+ | `?tree=hide` / `?tree=show` | Hide or show the file tree |
118
+
119
+ Users can change theme, language, query limit, and basemap from the in-app settings panel (gear icon). Their changes persist locally and take precedence over config defaults, while settings they never touched keep following the config.
112
120
 
113
121
  ## Quick Start (Development)
114
122
 
@@ -123,4 +131,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, architecture, geos
123
131
 
124
132
  ## License
125
133
 
126
- [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) -- hi@walkthru.earth
134
+ [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) - hi@walkthru.earth
@@ -9,6 +9,7 @@ import LinkIcon from '@lucide/svelte/icons/link';
9
9
  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
+ import { getNativeScheme } from '@walkthru-earth/objex-utils';
12
13
  import * as ContextMenu from '../ui/context-menu/index.js';
13
14
  import { VIEWER_DIR_EXTENSIONS } from '../../constants.js';
14
15
  import FileTypeIcon from '../../file-icons/FileTypeIcon.svelte';
@@ -17,7 +18,7 @@ import { getAdapter } from '../../storage/index.js';
17
18
  import { browser } from '../../stores/browser.svelte.js';
18
19
  import { tabs } from '../../stores/tabs.svelte.js';
19
20
  import type { Connection, FileEntry } from '../../types.js';
20
- import { getNativeScheme } from '../../utils/cloud-url.js';
21
+ import { buildHttpsUrlForConnection } from '../../utils/signed-url.js';
21
22
  import { syncUrlParam } from '../../utils/url-state.js';
22
23
  import { detectZarrMarkers } from '../../utils/zarr.js';
23
24
  import { openZarrTab } from '../../utils/zarr-tab.js';
@@ -48,6 +49,8 @@ let rootLoading = $state(true);
48
49
  let rootLoadingMore = $state(false);
49
50
  let rootContinuationToken = $state<string | undefined>();
50
51
  let rootHasMore = $state(false);
52
+ /** Set when the root listing fails: 'cors' for a blocked fetch, 'generic' otherwise. */
53
+ let rootError = $state<'cors' | 'generic' | null>(null);
51
54
  let filterQuery = $state('');
52
55
  let scrollEl = $state<HTMLElement>();
53
56
  /** Paths of directories detected as Zarr stores (by marker files in children). */
@@ -222,15 +225,9 @@ function handleChevronClick(e: MouseEvent, node: TreeNode) {
222
225
 
223
226
  // ---------- URL builders ----------
224
227
 
225
- /** Build HTTPS URL for a file path. */
228
+ /** Build a provider-aware HTTPS URL for a file path (percent-encoded for copy). */
226
229
  function buildHttpUrl(path: string): string {
227
- const conn = connection;
228
- if (conn.endpoint) {
229
- const base = conn.endpoint.replace(/\/$/, '');
230
- return `${base}/${conn.bucket}/${encodeKeyPath(path)}`;
231
- }
232
- // Default AWS S3
233
- return `https://s3.${conn.region}.amazonaws.com/${conn.bucket}/${encodeKeyPath(path)}`;
230
+ return buildHttpsUrlForConnection(connection, path, { encode: true });
234
231
  }
235
232
 
236
233
  /** Build provider-native URI (s3://, gs://, r2://, az://). */
@@ -240,14 +237,8 @@ function buildNativeUri(path: string): string {
240
237
  return `${scheme}://${conn.bucket}/${path}`;
241
238
  }
242
239
 
243
- // getNativeScheme imported from $lib/utils/url.js
244
-
245
- function encodeKeyPath(key: string): string {
246
- return key
247
- .split('/')
248
- .map((s) => encodeURIComponent(s))
249
- .join('/');
250
- }
240
+ // getNativeScheme imported from @walkthru-earth/objex-utils (cloud-url.ts)
241
+ // buildHttpsUrlForConnection imported from utils/signed-url (provider-aware base)
251
242
 
252
243
  // ---------- Clipboard ----------
253
244
 
@@ -368,6 +359,19 @@ async function expandToPath(path: string) {
368
359
 
369
360
  // ---------- Root loading ----------
370
361
 
362
+ /**
363
+ * Browsers collapse a CORS block and a true network failure into the same
364
+ * opaque `TypeError` ('Failed to fetch' in Chrome, 'NetworkError when
365
+ * attempting to fetch resource' in Firefox, 'Load failed' in Safari), so this
366
+ * can't tell them apart. For a cloud bucket the dominant cause is a missing
367
+ * browser CORS policy, so we lead the message with that.
368
+ */
369
+ function isLikelyCorsError(err: unknown): boolean {
370
+ const e = err as { name?: string; message?: string } | null;
371
+ if (!e || e.name !== 'TypeError' || typeof e.message !== 'string') return false;
372
+ return /failed to fetch|networkerror|load failed/i.test(e.message);
373
+ }
374
+
371
375
  // Load root entries when connection changes
372
376
  $effect(() => {
373
377
  const _connId = connection.id;
@@ -376,6 +380,7 @@ $effect(() => {
376
380
 
377
381
  async function loadRoot() {
378
382
  rootLoading = true;
383
+ rootError = null;
379
384
  rootContinuationToken = undefined;
380
385
  rootHasMore = false;
381
386
  detectedZarrPaths = new Set();
@@ -399,6 +404,7 @@ async function loadRoot() {
399
404
  }
400
405
  } catch (err) {
401
406
  console.error('[FileTree] Error loading root:', err);
407
+ rootError = isLikelyCorsError(err) ? 'cors' : 'generic';
402
408
  } finally {
403
409
  rootLoading = false;
404
410
  }
@@ -456,6 +462,15 @@ async function loadMoreRoot() {
456
462
  <div class="flex items-center justify-center py-8">
457
463
  <Loader2Icon class="size-4 animate-spin text-muted-foreground" />
458
464
  </div>
465
+ {:else if rootError}
466
+ <div class="space-y-1.5 px-3 py-6 text-center text-xs">
467
+ <p class="font-medium text-foreground">
468
+ {rootError === 'cors' ? t('fileTree.corsError') : t('fileTree.loadError')}
469
+ </p>
470
+ <p class="text-muted-foreground">
471
+ {rootError === 'cors' ? t('fileTree.corsHint') : t('fileTree.loadErrorHint')}
472
+ </p>
473
+ </div>
459
474
  {:else if filteredNodes.length === 0}
460
475
  <div class="px-3 py-6 text-center text-xs text-muted-foreground">
461
476
  {filterQuery ? t('fileTree.noMatch') : t('fileTree.emptyBucket')}
@@ -9,7 +9,6 @@ declare const __THIRD_PARTY_LICENSES__: {
9
9
  <script lang="ts">
10
10
  import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
11
11
  import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
12
- import GithubIcon from '@lucide/svelte/icons/github';
13
12
  import {
14
13
  Sheet,
15
14
  SheetContent,
@@ -75,7 +74,11 @@ $effect(() => {
75
74
  rel="noopener noreferrer"
76
75
  class="inline-flex items-center gap-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
77
76
  >
78
- <GithubIcon class="size-4" />
77
+ <svg class="size-4" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
78
+ <path
79
+ d="M12 .5C5.37.5 0 5.78 0 12.292c0 5.211 3.438 9.63 8.205 11.188.6.111.82-.254.82-.567 0-.279-.01-1.02-.015-2.002-3.338.711-4.042-1.582-4.042-1.582-.546-1.361-1.333-1.724-1.333-1.724-1.089-.731.083-.716.083-.716 1.205.082 1.84 1.215 1.84 1.215 1.07 1.797 2.807 1.278 3.492.977.108-.76.42-1.279.762-1.573-2.665-.295-5.466-1.309-5.466-5.827 0-1.287.465-2.339 1.235-3.164-.135-.295-.54-1.494.105-3.116 0 0 1.005-.31 3.3 1.209.957-.262 1.98-.392 3-.397 1.02.005 2.04.135 3 .397 2.28-1.519 3.285-1.209 3.285-1.209.645 1.622.24 2.821.12 3.116.765.825 1.23 1.877 1.23 3.164 0 4.53-2.805 5.527-5.475 5.817.42.354.81 1.077.81 2.182 0 1.578-.015 2.846-.015 3.229 0 .309.21.678.825.561C20.565 21.917 24 17.495 24 12.292 24 5.78 18.63.5 12 .5z"
80
+ />
81
+ </svg>
79
82
  {t('about.sourceCode')}
80
83
  </a>
81
84
 
@@ -10,6 +10,12 @@ import LockIcon from '@lucide/svelte/icons/lock';
10
10
  import PlugZapIcon from '@lucide/svelte/icons/plug-zap';
11
11
  import ShieldIcon from '@lucide/svelte/icons/shield';
12
12
  import XIcon from '@lucide/svelte/icons/x';
13
+ import {
14
+ DEFAULT_AWS_REGION,
15
+ describeParseResult,
16
+ looksLikeUrl,
17
+ parseStorageUrl
18
+ } from '@walkthru-earth/objex-utils';
13
19
  import { Button } from '../ui/button/index.js';
14
20
  import { Input } from '../ui/input/index.js';
15
21
  import {
@@ -33,7 +39,6 @@ import {
33
39
  } from '../../storage/providers.js';
34
40
  import { connections, DuplicateConnectionError } from '../../stores/connections.svelte.js';
35
41
  import type { Connection, ConnectionConfig } from '../../types.js';
36
- import { describeParseResult, looksLikeUrl, parseStorageUrl } from '../../utils/storage-url.js';
37
42
 
38
43
  interface Props {
39
44
  open: boolean;
@@ -56,7 +61,7 @@ let {
56
61
  let name = $state('');
57
62
  let provider = $state<ProviderId>('s3');
58
63
  let bucket = $state('');
59
- let region = $state('us-east-1');
64
+ let region = $state(DEFAULT_AWS_REGION);
60
65
  let endpoint = $state('');
61
66
  let anonymous = $state(true);
62
67
  let accessKey = $state('');
@@ -0,0 +1,238 @@
1
+ <script lang="ts">
2
+ import CheckIcon from '@lucide/svelte/icons/check';
3
+ import CopyIcon from '@lucide/svelte/icons/copy';
4
+ import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
5
+ import { COPY_FEEDBACK_MS } from '@walkthru-earth/objex-utils';
6
+ import {
7
+ Sheet,
8
+ SheetContent,
9
+ SheetDescription,
10
+ SheetHeader,
11
+ SheetTitle
12
+ } from '../ui/sheet/index.js';
13
+ import { t } from '../../i18n/index.svelte.js';
14
+ import { appConfig } from '../../stores/config.svelte.js';
15
+ import { settings } from '../../stores/settings.svelte.js';
16
+ import type { Theme } from '../../types.js';
17
+
18
+ interface Props {
19
+ open: boolean;
20
+ }
21
+
22
+ let { open = $bindable(false) }: Props = $props();
23
+
24
+ const themes: Theme[] = ['light', 'dark', 'system'];
25
+
26
+ let copied = $state(false);
27
+
28
+ function buildExportConfig(): string {
29
+ const cfg = appConfig.value;
30
+ const exported = {
31
+ defaults: {
32
+ theme: settings.theme,
33
+ locale: settings.locale,
34
+ featureLimit: settings.featureLimit,
35
+ mosaicItemLimit: settings.mosaicItemLimit
36
+ },
37
+ ui: {
38
+ showConnectionRail: settings.showConnectionRail,
39
+ showFileTree: settings.showFileTree,
40
+ showSettings: cfg.ui.showSettings
41
+ },
42
+ basemaps: cfg.basemaps,
43
+ defaultBasemap: cfg.defaultBasemap,
44
+ connections: cfg.connections
45
+ };
46
+ return JSON.stringify(exported, null, 2);
47
+ }
48
+
49
+ async function copyConfig() {
50
+ await navigator.clipboard.writeText(buildExportConfig());
51
+ copied = true;
52
+ setTimeout(() => (copied = false), COPY_FEEDBACK_MS);
53
+ }
54
+ </script>
55
+
56
+ <Sheet bind:open>
57
+ <SheetContent side="bottom" class="max-h-[85vh] sm:mx-auto sm:max-w-lg sm:rounded-t-lg">
58
+ <SheetHeader>
59
+ <SheetTitle>{t('settings.title')}</SheetTitle>
60
+ <SheetDescription class="sr-only">{t('settings.title')}</SheetDescription>
61
+ </SheetHeader>
62
+
63
+ <div class="flex flex-col gap-6 overflow-y-auto px-4 py-6 sm:px-6">
64
+ {#if appConfig.status === 'custom'}
65
+ <div class="rounded-md bg-primary/10 px-3 py-1.5 text-xs text-primary">
66
+ {t('settings.customConfig')}
67
+ </div>
68
+ {/if}
69
+
70
+ <!-- Appearance -->
71
+ <section class="flex flex-col gap-2">
72
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
73
+ {t('settings.appearance')}
74
+ </h3>
75
+ <div class="flex gap-2">
76
+ {#each themes as th}
77
+ <button
78
+ class="flex-1 rounded-md border px-3 py-1.5 text-sm transition-colors {settings.theme ===
79
+ th
80
+ ? 'border-primary bg-primary/10 text-primary'
81
+ : 'border-border text-muted-foreground hover:text-foreground'}"
82
+ onclick={() => settings.setTheme(th)}
83
+ >
84
+ {t(`theme.${th}`)}
85
+ </button>
86
+ {/each}
87
+ </div>
88
+ </section>
89
+
90
+ <!-- Language -->
91
+ <section class="flex flex-col gap-2">
92
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
93
+ {t('settings.language')}
94
+ </h3>
95
+ <div class="flex gap-2">
96
+ <button
97
+ class="flex-1 rounded-md border px-3 py-1.5 text-sm transition-colors {settings.locale ===
98
+ 'en'
99
+ ? 'border-primary bg-primary/10 text-primary'
100
+ : 'border-border text-muted-foreground hover:text-foreground'}"
101
+ onclick={() => settings.setLocale('en')}
102
+ >
103
+ English
104
+ </button>
105
+ <button
106
+ class="flex-1 rounded-md border px-3 py-1.5 text-sm transition-colors {settings.locale ===
107
+ 'ar'
108
+ ? 'border-primary bg-primary/10 text-primary'
109
+ : 'border-border text-muted-foreground hover:text-foreground'}"
110
+ onclick={() => settings.setLocale('ar')}
111
+ >
112
+ العربية
113
+ </button>
114
+ </div>
115
+ </section>
116
+
117
+ <!-- Map -->
118
+ {#if appConfig.value.basemaps.length > 0}
119
+ <section class="flex flex-col gap-2">
120
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
121
+ {t('settings.map')}
122
+ </h3>
123
+ <div class="flex flex-wrap gap-2">
124
+ <button
125
+ class="rounded-md border px-3 py-1.5 text-sm transition-colors {settings.basemapId ===
126
+ undefined
127
+ ? 'border-primary bg-primary/10 text-primary'
128
+ : 'border-border text-muted-foreground hover:text-foreground'}"
129
+ onclick={() => settings.setBasemap(undefined)}
130
+ >
131
+ {t('settings.basemapAuto')}
132
+ </button>
133
+ {#each appConfig.value.basemaps as bm (bm.id)}
134
+ <button
135
+ class="rounded-md border px-3 py-1.5 text-sm transition-colors {settings.basemapId ===
136
+ bm.id
137
+ ? 'border-primary bg-primary/10 text-primary'
138
+ : 'border-border text-muted-foreground hover:text-foreground'}"
139
+ onclick={() => settings.setBasemap(bm.id)}
140
+ >
141
+ {bm.label}
142
+ </button>
143
+ {/each}
144
+ </div>
145
+ </section>
146
+ {/if}
147
+
148
+ <!-- Data -->
149
+ <section class="flex flex-col gap-3">
150
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
151
+ {t('settings.data')}
152
+ </h3>
153
+ <label class="flex flex-col gap-1 text-sm">
154
+ <span>{t('settings.rowLimit')}</span>
155
+ <input
156
+ type="number"
157
+ min="1"
158
+ class="rounded-md border border-border bg-background px-2 py-1 text-sm"
159
+ value={settings.featureLimit}
160
+ onchange={(e) => settings.setFeatureLimit(Number(e.currentTarget.value))}
161
+ />
162
+ <span class="text-xs text-muted-foreground">{t('settings.rowLimitHelp')}</span>
163
+ </label>
164
+ <label class="flex flex-col gap-1 text-sm">
165
+ <span>{t('settings.mosaicLimit')}</span>
166
+ <input
167
+ type="number"
168
+ min="1"
169
+ class="rounded-md border border-border bg-background px-2 py-1 text-sm"
170
+ value={settings.mosaicItemLimit}
171
+ onchange={(e) => settings.setMosaicItemLimit(Number(e.currentTarget.value))}
172
+ />
173
+ <span class="text-xs text-muted-foreground">{t('settings.mosaicLimitHelp')}</span>
174
+ </label>
175
+ </section>
176
+
177
+ <!-- Interface -->
178
+ <section class="flex flex-col gap-3">
179
+ <h3 class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
180
+ {t('settings.interface')}
181
+ </h3>
182
+ <label class="flex items-center justify-between gap-2 text-sm">
183
+ <span class="flex flex-col">
184
+ <span>{t('settings.showConnectionRail')}</span>
185
+ {#if settings.railLockedByParam}
186
+ <span class="text-xs text-muted-foreground">{t('settings.lockedByLink')}</span>
187
+ {/if}
188
+ </span>
189
+ <input
190
+ type="checkbox"
191
+ class="size-4"
192
+ disabled={settings.railLockedByParam}
193
+ checked={settings.showConnectionRail}
194
+ onchange={(e) => settings.setShowConnectionRail(e.currentTarget.checked)}
195
+ />
196
+ </label>
197
+ <label class="flex items-center justify-between gap-2 text-sm">
198
+ <span class="flex flex-col">
199
+ <span>{t('settings.showFileTree')}</span>
200
+ {#if settings.treeLockedByParam}
201
+ <span class="text-xs text-muted-foreground">{t('settings.lockedByLink')}</span>
202
+ {/if}
203
+ </span>
204
+ <input
205
+ type="checkbox"
206
+ class="size-4"
207
+ disabled={settings.treeLockedByParam}
208
+ checked={settings.showFileTree}
209
+ onchange={(e) => settings.setShowFileTree(e.currentTarget.checked)}
210
+ />
211
+ </label>
212
+ </section>
213
+
214
+ <!-- Footer actions -->
215
+ <div class="flex items-center justify-between gap-2 border-t pt-4">
216
+ <button
217
+ class="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
218
+ onclick={() => settings.reset()}
219
+ >
220
+ <RotateCcwIcon class="size-3.5" />
221
+ {t('settings.reset')}
222
+ </button>
223
+ <button
224
+ class="inline-flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
225
+ onclick={copyConfig}
226
+ >
227
+ {#if copied}
228
+ <CheckIcon class="size-3.5" />
229
+ {t('settings.copied')}
230
+ {:else}
231
+ <CopyIcon class="size-3.5" />
232
+ {t('settings.copyConfig')}
233
+ {/if}
234
+ </button>
235
+ </div>
236
+ </div>
237
+ </SheetContent>
238
+ </Sheet>
@@ -0,0 +1,6 @@
1
+ interface Props {
2
+ open: boolean;
3
+ }
4
+ declare const SettingsSheet: import("svelte").Component<Props, {}, "open">;
5
+ type SettingsSheet = ReturnType<typeof SettingsSheet>;
6
+ export default SettingsSheet;
@@ -4,7 +4,9 @@ import DatabaseIcon from '@lucide/svelte/icons/database';
4
4
  import GlobeIcon from '@lucide/svelte/icons/globe';
5
5
  import PencilIcon from '@lucide/svelte/icons/pencil';
6
6
  import PlusIcon from '@lucide/svelte/icons/plus';
7
+ import SettingsIcon from '@lucide/svelte/icons/settings';
7
8
  import TrashIcon from '@lucide/svelte/icons/trash-2';
9
+ import { type DetectedHost, detectHostBucket, parseStorageUrl } from '@walkthru-earth/objex-utils';
8
10
  import {
9
11
  ContextMenu,
10
12
  ContextMenuContent,
@@ -21,18 +23,21 @@ import {
21
23
  } from '../ui/tooltip/index.js';
22
24
  import { t } from '../../i18n/index.svelte.js';
23
25
  import { browser } from '../../stores/browser.svelte.js';
26
+ import { appConfig } from '../../stores/config.svelte.js';
24
27
  import { connections } from '../../stores/connections.svelte.js';
25
28
  import { credentialStore, loadFromNative } from '../../stores/credentials.svelte.js';
26
29
  import { eagerUrlTabId, tabs } from '../../stores/tabs.svelte.js';
27
30
  import type { Connection } from '../../types.js';
28
- import { type DetectedHost, detectHostBucket } from '../../utils/host-detection.js';
29
- import { parseStorageUrl } from '../../utils/storage-url.js';
30
31
  import { clearUrlState, syncUrlParam } from '../../utils/url-state.js';
31
32
  import AboutSheet from './AboutSheet.svelte';
32
33
  import ConnectionDialog from './ConnectionDialog.svelte';
33
34
  import LocaleToggle from './LocaleToggle.svelte';
34
35
  import ThemeToggle from './ThemeToggle.svelte';
35
36
 
37
+ // Settings panel is owned by +page.svelte so it stays reachable even when the
38
+ // connection rail is hidden; the gear button just requests it be opened.
39
+ let { onOpenSettings }: { onOpenSettings?: () => void } = $props();
40
+
36
41
  let aboutOpen = $state(false);
37
42
  let dialogOpen = $state(false);
38
43
  let editingConnection = $state<Connection | null>(null);
@@ -42,9 +47,9 @@ let autoConnecting = $state(false);
42
47
  $effect(() => {
43
48
  connections.load().then(async () => {
44
49
  await handleAutoDetection();
45
- // On first visit (no connections, no URL params), load the demo bucket
50
+ // On first visit (no connections, no URL params), seed connections from config
46
51
  if (connections.items.length === 0 && !new URL(window.location.href).searchParams.has('url')) {
47
- await loadDemoConnection();
52
+ await loadConfigConnections();
48
53
  }
49
54
  });
50
55
  });
@@ -94,9 +99,18 @@ async function handleAutoDetection() {
94
99
  return;
95
100
  }
96
101
 
102
+ const hasUrlParam = url.searchParams.has('url');
103
+
97
104
  // A recognizable storage provider was detected. Close the eagerly-opened
98
105
  // URL tab (if any) so we can re-open it with a proper connectionId that
99
106
  // provides S3 credentials and endpoint config for DuckDB httpfs.
107
+ // Mark the close + reopen as a migration so the tab-sync effect in
108
+ // +page.svelte doesn't clear `?url=` / `#hash` during the empty-tabs
109
+ // window between close and open. We end migration in `finally` so an
110
+ // abandoned credential prompt or thrown error still resets the flag.
111
+ const isMigrating = hasUrlParam;
112
+ if (isMigrating) tabs.beginMigration();
113
+
100
114
  if (rawUrl) {
101
115
  const eagerTabId = eagerUrlTabId(rawUrl);
102
116
  const eagerTab = tabs.items.find((t) => t.id === eagerTabId);
@@ -105,12 +119,17 @@ async function handleAutoDetection() {
105
119
  }
106
120
  }
107
121
 
108
- const hasUrlParam = url.searchParams.has('url');
109
-
110
122
  if (hasUrlParam) {
111
123
  // Auto-connect immediately for ?url= param (zero-friction)
112
124
  autoConnecting = true;
113
125
  try {
126
+ // TODO(stac-storage-ext): when `rawUrl` resolves to STAC content,
127
+ // peek-fetch the JSON, classify with `classifyStac`, pick the first
128
+ // item with non-empty hints, and pass `detected` through
129
+ // `applyStacItemStorageHints(detected, item)` BEFORE
130
+ // `saveHostConnection` so `storage:region` / `storage:platform` /
131
+ // `storage:requester_pays` flow into the auto-created connection.
132
+ // Helper lives in `utils/host-detection.ts` -- modular, callers opt in.
114
133
  const connId = await connections.saveHostConnection(detected);
115
134
  const conn = connections.getById(connId);
116
135
  if (!conn) return;
@@ -160,10 +179,44 @@ async function handleAutoDetection() {
160
179
  syncUrlParam(conn, prefixParam || undefined);
161
180
  } finally {
162
181
  autoConnecting = false;
182
+ if (isMigrating) tabs.endMigration();
163
183
  }
164
184
  } else {
165
185
  // Show indicator for hostname-detected bucket
166
186
  detectedHost = detected;
187
+ if (isMigrating) tabs.endMigration();
188
+ }
189
+ }
190
+
191
+ async function loadConfigConnections() {
192
+ const seeds = appConfig.value.connections;
193
+ if (seeds.length === 0) {
194
+ // No configured connections (e.g. config failed to load): preserve the
195
+ // historic first-run demo bucket so the empty app is never a dead end.
196
+ await loadDemoConnection();
197
+ return;
198
+ }
199
+ let firstAnon: Connection | null = null;
200
+ for (const seed of seeds) {
201
+ const { id } = await connections.save({
202
+ name: seed.name,
203
+ provider: seed.provider,
204
+ endpoint: seed.endpoint ?? '',
205
+ bucket: seed.bucket,
206
+ region: seed.region ?? '',
207
+ anonymous: seed.anonymous ?? false,
208
+ ...(seed.authMethod ? { authMethod: seed.authMethod } : {}),
209
+ ...(seed.rootPrefix ? { rootPrefix: seed.rootPrefix } : {})
210
+ });
211
+ const conn = connections.getById(id);
212
+ if (conn?.anonymous && !firstAnon) firstAnon = conn;
213
+ }
214
+ // Auto-open the first public bucket so the demo flow stays zero-click.
215
+ // Private seeds remain as un-browsed rows; clicking one runs the normal
216
+ // ensureCredentials prompt via handleBrowseConnection.
217
+ if (firstAnon) {
218
+ browser.browse(firstAnon);
219
+ syncUrlParam(firstAnon);
167
220
  }
168
221
  }
169
222
 
@@ -344,6 +397,20 @@ async function handleBrowseConnection(connection: Connection) {
344
397
 
345
398
  <!-- Bottom actions -->
346
399
  <div class="mt-auto flex flex-col items-center gap-1 pt-2">
400
+ {#if appConfig.value.ui.showSettings}
401
+ <Tooltip>
402
+ <TooltipTrigger>
403
+ <button
404
+ class="flex size-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
405
+ onclick={() => onOpenSettings?.()}
406
+ aria-label={t('settings.tooltip')}
407
+ >
408
+ <SettingsIcon class="size-4" />
409
+ </button>
410
+ </TooltipTrigger>
411
+ <TooltipContent side="right">{t('settings.tooltip')}</TooltipContent>
412
+ </Tooltip>
413
+ {/if}
347
414
  <LocaleToggle />
348
415
  <ThemeToggle />
349
416
  </div>
@@ -1,3 +1,6 @@
1
- declare const Sidebar: import("svelte").Component<Record<string, never>, {}, "">;
1
+ type $$ComponentProps = {
2
+ onOpenSettings?: () => void;
3
+ };
4
+ declare const Sidebar: import("svelte").Component<$$ComponentProps, {}, "">;
2
5
  type Sidebar = ReturnType<typeof Sidebar>;
3
6
  export default Sidebar;