@walkthru-earth/objex 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +11 -2
  2. package/dist/components/browser/FileBrowser.svelte +41 -54
  3. package/dist/components/browser/FileTreeSidebar.svelte +43 -7
  4. package/dist/components/layout/ConnectionDialog.svelte +100 -1
  5. package/dist/components/layout/Sidebar.svelte +43 -25
  6. package/dist/components/viewers/CodeViewer.svelte +23 -0
  7. package/dist/components/viewers/CogControls.svelte +208 -0
  8. package/dist/components/viewers/CogControls.svelte.d.ts +12 -0
  9. package/dist/components/viewers/CogViewer.svelte +353 -1160
  10. package/dist/components/viewers/CogViewer.svelte.d.ts +1 -1
  11. package/dist/components/viewers/DatabaseViewer.svelte +345 -37
  12. package/dist/components/viewers/MarkdownViewer.svelte +1 -1
  13. package/dist/components/viewers/TableViewer.svelte +123 -41
  14. package/dist/components/viewers/ZarrMapViewer.svelte +29 -0
  15. package/dist/components/viewers/ZarrViewer.svelte +1 -4
  16. package/dist/constants.d.ts +6 -2
  17. package/dist/constants.js +6 -2
  18. package/dist/file-icons/index.d.ts +1 -1
  19. package/dist/file-icons/index.js +12 -2
  20. package/dist/i18n/ar.js +24 -0
  21. package/dist/i18n/en.js +24 -0
  22. package/dist/i18n/index.svelte.d.ts +0 -1
  23. package/dist/i18n/index.svelte.js +0 -3
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.js +10 -0
  26. package/dist/query/engine.d.ts +20 -4
  27. package/dist/query/index.d.ts +2 -1
  28. package/dist/query/index.js +1 -0
  29. package/dist/query/source.d.ts +30 -0
  30. package/dist/query/source.js +37 -0
  31. package/dist/query/wasm.d.ts +7 -5
  32. package/dist/query/wasm.js +138 -85
  33. package/dist/storage/providers.d.ts +47 -0
  34. package/dist/storage/providers.js +160 -0
  35. package/dist/stores/connections.svelte.js +5 -31
  36. package/dist/stores/files.svelte.d.ts +2 -8
  37. package/dist/stores/files.svelte.js +5 -38
  38. package/dist/stores/query-history.svelte.js +3 -25
  39. package/dist/stores/settings.svelte.d.ts +1 -0
  40. package/dist/stores/settings.svelte.js +10 -30
  41. package/dist/stores/tabs.svelte.d.ts +9 -2
  42. package/dist/stores/tabs.svelte.js +11 -2
  43. package/dist/types.d.ts +11 -0
  44. package/dist/utils/cloud-url.d.ts +27 -0
  45. package/dist/utils/cloud-url.js +61 -0
  46. package/dist/utils/cog.d.ts +244 -0
  47. package/dist/utils/cog.js +1039 -0
  48. package/dist/utils/deck.d.ts +0 -18
  49. package/dist/utils/deck.js +0 -36
  50. package/dist/utils/export.d.ts +22 -2
  51. package/dist/utils/export.js +35 -10
  52. package/dist/utils/file-sort.d.ts +20 -0
  53. package/dist/utils/file-sort.js +41 -0
  54. package/dist/utils/geometry-type.d.ts +52 -0
  55. package/dist/utils/geometry-type.js +76 -0
  56. package/dist/utils/local-storage.d.ts +16 -0
  57. package/dist/utils/local-storage.js +37 -0
  58. package/dist/utils/markdown-sql.d.ts +1 -1
  59. package/dist/utils/markdown-sql.js +3 -4
  60. package/dist/utils/pmtiles-tile.d.ts +0 -2
  61. package/dist/utils/pmtiles-tile.js +0 -8
  62. package/dist/utils/url-state.d.ts +6 -0
  63. package/dist/utils/url-state.js +34 -26
  64. package/dist/utils/url.d.ts +13 -25
  65. package/dist/utils/url.js +17 -78
  66. package/dist/utils/zarr-tab.d.ts +22 -0
  67. package/dist/utils/zarr-tab.js +30 -0
  68. package/dist/utils/zarr.d.ts +0 -2
  69. package/dist/utils/zarr.js +73 -44
  70. package/package.json +50 -46
  71. package/dist/components/ui/tabs/index.d.ts +0 -5
  72. package/dist/components/ui/tabs/index.js +0 -7
  73. package/dist/components/ui/tabs/tabs-content.svelte +0 -17
  74. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +0 -4
  75. package/dist/components/ui/tabs/tabs-list.svelte +0 -16
  76. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +0 -4
  77. package/dist/components/ui/tabs/tabs-trigger.svelte +0 -20
  78. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +0 -4
  79. package/dist/components/ui/tabs/tabs.svelte +0 -19
  80. package/dist/components/ui/tabs/tabs.svelte.d.ts +0 -4
  81. package/dist/components/viewers/MapViewer.svelte +0 -234
  82. package/dist/components/viewers/MapViewer.svelte.d.ts +0 -7
  83. package/dist/components/viewers/StyleEditorOverlay.svelte +0 -27
  84. package/dist/components/viewers/StyleEditorOverlay.svelte.d.ts +0 -7
@@ -266,6 +266,148 @@ export const PROVIDERS = {
266
266
  schemes: []
267
267
  }
268
268
  };
269
+ export const CORS_HELP = {
270
+ s3: {
271
+ defaultEnabled: false,
272
+ docsUrl: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/enabling-cors-examples.html',
273
+ note: 'Enable via S3 Console: Bucket > Permissions > CORS, or use the AWS CLI.'
274
+ },
275
+ gcs: {
276
+ defaultEnabled: false,
277
+ docsUrl: 'https://cloud.google.com/storage/docs/using-cors',
278
+ note: 'CORS cannot be configured via the Cloud Console. Use the gcloud CLI.',
279
+ cliSteps: [
280
+ 'Create a cors.json file:\n[\n {\n "origin": ["*"],\n "method": ["GET", "HEAD"],\n "responseHeader": [\n "Content-Type",\n "Content-Length",\n "Content-Range",\n "Accept-Ranges",\n "ETag"\n ],\n "maxAgeSeconds": 3600\n }\n]',
281
+ 'gcloud storage buckets update gs://BUCKET --cors-file=cors.json'
282
+ ]
283
+ },
284
+ r2: {
285
+ defaultEnabled: false,
286
+ docsUrl: 'https://developers.cloudflare.com/r2/buckets/cors/',
287
+ note: 'Enable via R2 Dashboard: Bucket > Settings > CORS Policy.'
288
+ },
289
+ azure: {
290
+ defaultEnabled: false,
291
+ docsUrl: 'https://learn.microsoft.com/en-us/rest/api/storageservices/cross-origin-resource-sharing--cors--support-for-the-azure-storage-services',
292
+ note: 'Enable via Azure Portal: Storage Account > Blob Service > CORS, or use the Azure CLI.',
293
+ cliSteps: [
294
+ 'az storage cors add --services b --methods GET HEAD \\\n --origins "*" --allowed-headers "*" \\\n --exposed-headers "*" --max-age 3600 \\\n --account-name ACCOUNT'
295
+ ]
296
+ },
297
+ minio: {
298
+ defaultEnabled: true,
299
+ docsUrl: 'https://docs.min.io/enterprise/aistor-object-store/reference/cli/mc-cors/',
300
+ note: 'MinIO allows all origins by default. For custom rules, use mc cors set.'
301
+ },
302
+ storj: {
303
+ defaultEnabled: true,
304
+ note: 'Storj S3 gateway returns CORS headers by default.'
305
+ },
306
+ b2: {
307
+ defaultEnabled: false,
308
+ docsUrl: 'https://www.backblaze.com/docs/cloud-storage-cross-origin-resource-sharing-rules',
309
+ note: 'Enable via B2 Console: Bucket Settings > CORS Rules, or use the B2 CLI.',
310
+ cliSteps: [
311
+ 'b2 bucket update --cors-rules \'[{\n "corsRuleName": "allow-all",\n "allowedOrigins": ["*"],\n "allowedOperations": ["s3_head", "s3_get"],\n "allowedHeaders": ["*"],\n "maxAgeSeconds": 3600\n}]\' BUCKET allPublic'
312
+ ]
313
+ },
314
+ digitalocean: {
315
+ defaultEnabled: false,
316
+ docsUrl: 'https://docs.digitalocean.com/products/spaces/how-to/configure-cors/',
317
+ note: 'Enable via Control Panel: Space > Settings > CORS Configurations.'
318
+ },
319
+ wasabi: {
320
+ defaultEnabled: true,
321
+ docsUrl: 'https://docs.wasabi.com/docs/bucket-policy',
322
+ note: 'Wasabi returns CORS headers by default for all buckets.'
323
+ },
324
+ contabo: {
325
+ defaultEnabled: false,
326
+ note: 'S3-compatible CORS via the AWS CLI.',
327
+ cliSteps: [
328
+ 'Create a cors.json file:\n{\n "CORSRules": [{\n "AllowedOrigins": ["*"],\n "AllowedMethods": ["GET", "HEAD"],\n "AllowedHeaders": ["*"],\n "ExposeHeaders": ["ETag", "Content-Length", "Content-Type", "Content-Range", "Accept-Ranges"],\n "MaxAgeSeconds": 3600\n }]\n}',
329
+ 'aws s3api put-bucket-cors --bucket BUCKET \\\n --cors-configuration file://cors.json \\\n --endpoint-url https://REGION.contaboobj.com'
330
+ ]
331
+ },
332
+ hetzner: {
333
+ defaultEnabled: false,
334
+ docsUrl: 'https://docs.hetzner.com/storage/object-storage/howto-protect-objects/cors/',
335
+ note: 'S3-compatible CORS via the AWS CLI.',
336
+ cliSteps: [
337
+ 'Create a cors.json file:\n{\n "CORSRules": [{\n "AllowedOrigins": ["*"],\n "AllowedMethods": ["GET", "HEAD"],\n "AllowedHeaders": ["*"],\n "ExposeHeaders": ["ETag", "Content-Length", "Content-Type", "Content-Range", "Accept-Ranges"],\n "MaxAgeSeconds": 3600\n }]\n}',
338
+ 'aws s3api put-bucket-cors --bucket BUCKET \\\n --cors-configuration file://cors.json \\\n --endpoint-url https://REGION.your-objectstorage.com \\\n --region REGION'
339
+ ]
340
+ },
341
+ linode: {
342
+ defaultEnabled: false,
343
+ docsUrl: 'https://www.linode.com/docs/guides/working-with-cors-linode-object-storage/',
344
+ note: 'S3-compatible CORS via the AWS CLI.',
345
+ cliSteps: [
346
+ 'Create a cors.json file:\n{\n "CORSRules": [{\n "AllowedOrigins": ["*"],\n "AllowedMethods": ["GET", "HEAD"],\n "AllowedHeaders": ["*"],\n "ExposeHeaders": ["ETag", "Content-Length", "Content-Type", "Content-Range", "Accept-Ranges"],\n "MaxAgeSeconds": 3600\n }]\n}',
347
+ 'aws s3api put-bucket-cors --bucket BUCKET \\\n --cors-configuration file://cors.json \\\n --endpoint-url https://REGION.linodeobjects.com'
348
+ ]
349
+ },
350
+ ovhcloud: {
351
+ defaultEnabled: false,
352
+ docsUrl: 'https://help.ovhcloud.com/csm/en-public-cloud-storage-s3-cors?id=kb_article_view&sysparm_article=KB0058291',
353
+ note: 'S3-compatible CORS via the AWS CLI.',
354
+ cliSteps: [
355
+ 'Create a cors.json file:\n{\n "CORSRules": [{\n "AllowedOrigins": ["*"],\n "AllowedMethods": ["GET", "HEAD"],\n "AllowedHeaders": ["*"],\n "ExposeHeaders": ["ETag", "Content-Length", "Content-Type", "Content-Range", "Accept-Ranges"],\n "MaxAgeSeconds": 3600\n }]\n}',
356
+ 'aws s3api put-bucket-cors --bucket BUCKET \\\n --cors-configuration file://cors.json \\\n --endpoint-url https://s3.REGION.io.cloud.ovh.net'
357
+ ]
358
+ }
359
+ };
360
+ export const READ_ONLY_HELP = {
361
+ s3: {
362
+ note: 'Use IAM policies to create a read-only user, or apply a bucket policy that allows only s3:GetObject and s3:ListBucket.',
363
+ docsUrl: 'https://docs.aws.amazon.com/AmazonS3/latest/userguide/example-policies-s3.html'
364
+ },
365
+ gcs: {
366
+ note: 'Assign the Storage Object Viewer role (roles/storage.objectViewer) to the service account.',
367
+ docsUrl: 'https://cloud.google.com/storage/docs/access-control/iam-roles'
368
+ },
369
+ r2: {
370
+ note: 'Create an API token with Object Read permissions in the R2 dashboard.',
371
+ docsUrl: 'https://developers.cloudflare.com/r2/api/tokens/'
372
+ },
373
+ azure: {
374
+ note: 'Generate a SAS token with Read and List permissions only. Avoid granting Write or Delete.',
375
+ docsUrl: 'https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview'
376
+ },
377
+ b2: {
378
+ note: 'Create an application key with readFiles and listBuckets capabilities only.',
379
+ docsUrl: 'https://www.backblaze.com/docs/cloud-storage-application-keys'
380
+ },
381
+ hetzner: {
382
+ note: 'Keys have full read/write by default. Use a bucket policy with the correct ARN format to deny write and policy actions. To undo, generate a new admin key from the Hetzner Console.',
383
+ docsUrl: 'https://docs.hetzner.com/storage/object-storage/faq/s3-credentials/#how-do-i-restrict-access-per-key',
384
+ cliSteps: [
385
+ 'Find your project ID from the Hetzner Console URL:\nhttps://console.hetzner.com/projects/<PROJECT_ID>/servers',
386
+ 'Create a policy.json file:\n{\n "Version": "2012-10-17",\n "Statement": [\n {\n "Sid": "DenyWrites",\n "Effect": "Deny",\n "Principal": {\n "AWS": "arn:aws:iam:::user/p<PROJECT_ID>:<ACCESS_KEY>"\n },\n "Action": [\n "s3:PutObject",\n "s3:DeleteObject",\n "s3:AbortMultipartUpload",\n "s3:PutBucketPolicy",\n "s3:DeleteBucketPolicy"\n ],\n "Resource": [\n "arn:aws:s3:::BUCKET",\n "arn:aws:s3:::BUCKET/*"\n ]\n }\n ]\n}',
387
+ 'aws s3api put-bucket-policy --bucket BUCKET \\\n --policy file://policy.json \\\n --endpoint-url https://REGION.your-objectstorage.com \\\n --region REGION',
388
+ 'Note: This key can no longer modify the policy.\nTo restore write access, generate a new key in the\nHetzner Console and use it to delete the policy.'
389
+ ]
390
+ },
391
+ minio: {
392
+ note: 'Create a read-only policy with mc admin policy, or use the built-in readonly canned policy.',
393
+ docsUrl: 'https://docs.min.io/enterprise/aistor-object-store/administration/iam/access/'
394
+ },
395
+ digitalocean: {
396
+ note: 'Spaces keys are project-wide. Use a bucket policy to restrict write actions for a specific key.',
397
+ docsUrl: 'https://docs.digitalocean.com/products/spaces/how-to/manage-access/'
398
+ },
399
+ wasabi: {
400
+ note: 'Create a sub-user with a read-only policy in the Wasabi Console.',
401
+ docsUrl: 'https://docs.wasabi.com/docs/creating-a-user-account-and-access-key'
402
+ },
403
+ contabo: {
404
+ note: 'S3-compatible bucket policies. Use a Deny policy for write actions with the key ARN.',
405
+ cliSteps: [
406
+ 'Create a policy.json with a Deny statement for s3:PutObject and s3:DeleteObject.',
407
+ 'aws s3api put-bucket-policy --bucket BUCKET \\\n --policy file://policy.json \\\n --endpoint-url https://REGION.contaboobj.com'
408
+ ]
409
+ }
410
+ };
269
411
  // ---------------------------------------------------------------------------
270
412
  // Helpers
271
413
  // ---------------------------------------------------------------------------
@@ -316,3 +458,21 @@ export function buildProviderBaseUrl(provider, endpoint, bucket, region) {
316
458
  export function isGcsProvider(provider, endpoint) {
317
459
  return provider === 'gcs' || (!!endpoint && /storage\.googleapis\.com/i.test(endpoint));
318
460
  }
461
+ export function getAccessMode(conn) {
462
+ if (conn.provider === 'azure')
463
+ return 'sas-https';
464
+ // Anonymous buckets: every provider serves files over plain HTTPS without
465
+ // signing (AWS path/vhost, GCS, R2 public, Storj, Wasabi, DO, etc.).
466
+ if (conn.anonymous)
467
+ return 'public-https';
468
+ // Authenticated: needs SigV4 signing.
469
+ return 'signed-s3';
470
+ }
471
+ /**
472
+ * True when the connection's files can be fetched by any HTTP client
473
+ * (fetch/img/video/DuckDB httpfs/COG/Zarr/etc.) without the storage adapter.
474
+ */
475
+ export function isPubliclyStreamable(conn) {
476
+ const mode = getAccessMode(conn);
477
+ return mode === 'public-https' || mode === 'sas-https';
478
+ }
@@ -1,33 +1,7 @@
1
1
  import { STORAGE_KEYS } from '../constants.js';
2
+ import { loadFromStorage, persistToStorage } from '../utils/local-storage.js';
2
3
  import { credentialStore, storeToNative } from './credentials.svelte.js';
3
4
  // ---------------------------------------------------------------------------
4
- // localStorage helpers
5
- // ---------------------------------------------------------------------------
6
- function loadFromLocalStorage() {
7
- if (typeof window === 'undefined')
8
- return [];
9
- try {
10
- const raw = localStorage.getItem(STORAGE_KEYS.CONNECTIONS);
11
- if (raw) {
12
- return JSON.parse(raw);
13
- }
14
- }
15
- catch {
16
- // ignore parse errors
17
- }
18
- return [];
19
- }
20
- function persistToLocalStorage(connections) {
21
- if (typeof window === 'undefined')
22
- return;
23
- try {
24
- localStorage.setItem(STORAGE_KEYS.CONNECTIONS, JSON.stringify(connections));
25
- }
26
- catch {
27
- // ignore storage errors
28
- }
29
- }
30
- // ---------------------------------------------------------------------------
31
5
  // Store
32
6
  // ---------------------------------------------------------------------------
33
7
  function createConnectionsStore() {
@@ -48,7 +22,7 @@ function createConnectionsStore() {
48
22
  async load() {
49
23
  if (loaded)
50
24
  return;
51
- connections = loadFromLocalStorage();
25
+ connections = loadFromStorage(STORAGE_KEYS.CONNECTIONS, []);
52
26
  loaded = true;
53
27
  },
54
28
  /**
@@ -75,7 +49,7 @@ function createConnectionsStore() {
75
49
  rootPrefix: config.rootPrefix
76
50
  };
77
51
  connections = [...connections, conn];
78
- persistToLocalStorage(connections);
52
+ persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
79
53
  // Store credentials in memory (never persisted to localStorage).
80
54
  if (!config.anonymous) {
81
55
  if (config.sas_token) {
@@ -114,7 +88,7 @@ function createConnectionsStore() {
114
88
  rootPrefix: config.rootPrefix
115
89
  };
116
90
  connections = [...connections];
117
- persistToLocalStorage(connections);
91
+ persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
118
92
  // Invalidate cached adapter for this connection
119
93
  import('../storage/index.js').then(({ clearAdapterCache }) => clearAdapterCache(id));
120
94
  // Update in-memory credentials.
@@ -148,7 +122,7 @@ function createConnectionsStore() {
148
122
  async remove(id) {
149
123
  const before = connections.length;
150
124
  connections = connections.filter((c) => c.id !== id);
151
- persistToLocalStorage(connections);
125
+ persistToStorage(STORAGE_KEYS.CONNECTIONS, connections);
152
126
  credentialStore.remove(id);
153
127
  // Invalidate cached adapter for this connection
154
128
  import('../storage/index.js').then(({ clearAdapterCache }) => clearAdapterCache(id));
@@ -1,11 +1,6 @@
1
1
  import type { FileEntry } from '../types.js';
2
- export type SortField = 'name' | 'size' | 'modified' | 'extension';
3
- export type SortDirection = 'asc' | 'desc';
4
- export interface SortConfig {
5
- field: SortField;
6
- direction: SortDirection;
7
- }
8
- export declare const fileStore: {
2
+ import { type SortConfig, type SortField } from '../utils/file-sort.js';
3
+ export declare const files: {
9
4
  readonly entries: FileEntry[];
10
5
  readonly currentPath: string;
11
6
  readonly loading: boolean;
@@ -17,4 +12,3 @@ export declare const fileStore: {
17
12
  setError(message: string | null): void;
18
13
  sort(field: SortField): void;
19
14
  };
20
- export { fileStore as files };
@@ -1,27 +1,4 @@
1
- function sortEntries(entries, config) {
2
- const sorted = [...entries];
3
- const dir = config.direction === 'asc' ? 1 : -1;
4
- sorted.sort((a, b) => {
5
- // Directories always come first
6
- if (a.is_dir && !b.is_dir)
7
- return -1;
8
- if (!a.is_dir && b.is_dir)
9
- return 1;
10
- switch (config.field) {
11
- case 'name':
12
- return dir * a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
13
- case 'size':
14
- return dir * (a.size - b.size);
15
- case 'modified':
16
- return dir * (a.modified - b.modified);
17
- case 'extension':
18
- return dir * a.extension.localeCompare(b.extension, undefined, { sensitivity: 'base' });
19
- default:
20
- return 0;
21
- }
22
- });
23
- return sorted;
24
- }
1
+ import { sortFileEntries, toggleSortField } from '../utils/file-sort.js';
25
2
  function createFilesStore() {
26
3
  let files = $state([]);
27
4
  let currentPath = $state('');
@@ -45,7 +22,7 @@ function createFilesStore() {
45
22
  return sortConfig;
46
23
  },
47
24
  setFiles(entries) {
48
- files = sortEntries(entries, sortConfig);
25
+ files = sortFileEntries(entries, sortConfig);
49
26
  error = null;
50
27
  },
51
28
  setPath(path) {
@@ -58,19 +35,9 @@ function createFilesStore() {
58
35
  error = message;
59
36
  },
60
37
  sort(field) {
61
- if (sortConfig.field === field) {
62
- // Toggle direction if clicking the same field
63
- sortConfig = {
64
- field,
65
- direction: sortConfig.direction === 'asc' ? 'desc' : 'asc'
66
- };
67
- }
68
- else {
69
- sortConfig = { field, direction: 'asc' };
70
- }
71
- files = sortEntries(files, sortConfig);
38
+ sortConfig = toggleSortField(sortConfig, field);
39
+ files = sortFileEntries(files, sortConfig);
72
40
  }
73
41
  };
74
42
  }
75
- export const fileStore = createFilesStore();
76
- export { fileStore as files };
43
+ export const files = createFilesStore();
@@ -1,31 +1,9 @@
1
1
  import { MAX_QUERY_HISTORY_ENTRIES, STORAGE_KEYS } from '../constants.js';
2
- function loadEntries() {
3
- if (typeof window === 'undefined')
4
- return [];
5
- try {
6
- const raw = localStorage.getItem(STORAGE_KEYS.QUERY_HISTORY);
7
- if (raw)
8
- return JSON.parse(raw);
9
- }
10
- catch {
11
- // ignore parse errors
12
- }
13
- return [];
14
- }
15
- function persistEntries(entries) {
16
- if (typeof window === 'undefined')
17
- return;
18
- try {
19
- localStorage.setItem(STORAGE_KEYS.QUERY_HISTORY, JSON.stringify(entries));
20
- }
21
- catch {
22
- // ignore storage errors
23
- }
24
- }
2
+ import { loadFromStorage, persistToStorage } from '../utils/local-storage.js';
25
3
  function createQueryHistoryStore() {
26
- let entries = $state(loadEntries());
4
+ let entries = $state(loadFromStorage(STORAGE_KEYS.QUERY_HISTORY, []));
27
5
  function save() {
28
- persistEntries(entries);
6
+ persistToStorage(STORAGE_KEYS.QUERY_HISTORY, entries);
29
7
  }
30
8
  return {
31
9
  get entries() {
@@ -1,5 +1,6 @@
1
1
  import { type Locale } from '../i18n/index.svelte.js';
2
2
  import type { Theme } from '../types.js';
3
+ export declare function resolveTheme(theme: Theme): 'light' | 'dark';
3
4
  export declare const settings: {
4
5
  readonly theme: Theme;
5
6
  readonly resolved: "light" | "dark";
@@ -1,36 +1,16 @@
1
1
  import { STORAGE_KEYS } from '../constants.js';
2
2
  import { setLocale } from '../i18n/index.svelte.js';
3
+ import { loadFromStorage, persistToStorage } from '../utils/local-storage.js';
4
+ const SETTINGS_DEFAULTS = { theme: 'system', locale: 'en', featureLimit: 1000 };
3
5
  function loadSettings() {
4
- if (typeof window === 'undefined') {
5
- return { theme: 'system', locale: 'en', featureLimit: 1000 };
6
- }
7
- try {
8
- const raw = localStorage.getItem(STORAGE_KEYS.SETTINGS);
9
- if (raw) {
10
- const parsed = JSON.parse(raw);
11
- return {
12
- theme: parsed.theme ?? 'system',
13
- locale: parsed.locale ?? 'en',
14
- featureLimit: parsed.featureLimit ?? 1000
15
- };
16
- }
17
- }
18
- catch {
19
- // ignore parse errors
20
- }
21
- return { theme: 'system', locale: 'en', featureLimit: 1000 };
22
- }
23
- function persistSettings(settings) {
24
- if (typeof window === 'undefined')
25
- return;
26
- try {
27
- localStorage.setItem(STORAGE_KEYS.SETTINGS, JSON.stringify(settings));
28
- }
29
- catch {
30
- // ignore storage errors
31
- }
6
+ const stored = loadFromStorage(STORAGE_KEYS.SETTINGS, {});
7
+ return {
8
+ theme: stored.theme ?? SETTINGS_DEFAULTS.theme,
9
+ locale: stored.locale ?? SETTINGS_DEFAULTS.locale,
10
+ featureLimit: stored.featureLimit ?? SETTINGS_DEFAULTS.featureLimit
11
+ };
32
12
  }
33
- function resolveTheme(theme) {
13
+ export function resolveTheme(theme) {
34
14
  if (theme !== 'system')
35
15
  return theme;
36
16
  if (typeof window === 'undefined')
@@ -51,7 +31,7 @@ function createSettingsStore() {
51
31
  document.documentElement.lang = initial.locale;
52
32
  }
53
33
  function persist() {
54
- persistSettings({ theme, locale, featureLimit });
34
+ persistToStorage(STORAGE_KEYS.SETTINGS, { theme, locale, featureLimit });
55
35
  }
56
36
  function applyTheme(t) {
57
37
  theme = t;
@@ -1,5 +1,13 @@
1
1
  import type { Tab } from '../types.js';
2
- export declare const tabStore: {
2
+ /**
3
+ * Tab-id for eagerly-opened direct-URL tabs (`source: 'url'`).
4
+ * The Sidebar's host-detection auto-migration closes an eager tab by its id
5
+ * and re-opens as a remote tab once a connection is available; the eager id
6
+ * is built in `+page.svelte::openUrlTab` and matched in `Sidebar.svelte::
7
+ * handleAutoDetection`, so both sides must agree on the format.
8
+ */
9
+ export declare function eagerUrlTabId(url: string): string;
10
+ export declare const tabs: {
3
11
  readonly items: Tab[];
4
12
  readonly activeTabId: string | null;
5
13
  readonly active: Tab | undefined;
@@ -14,4 +22,3 @@ export declare const tabStore: {
14
22
  setActive(id: string): void;
15
23
  update(id: string, partial: Partial<Omit<Tab, "id">>): void;
16
24
  };
17
- export { tabStore as tabs };
@@ -1,6 +1,16 @@
1
1
  import { tabResources } from './tab-resources.svelte.js';
2
2
  /** Maximum number of viewer instances kept alive (mounted but hidden). */
3
3
  const MAX_ALIVE = 5;
4
+ /**
5
+ * Tab-id for eagerly-opened direct-URL tabs (`source: 'url'`).
6
+ * The Sidebar's host-detection auto-migration closes an eager tab by its id
7
+ * and re-opens as a remote tab once a connection is available; the eager id
8
+ * is built in `+page.svelte::openUrlTab` and matched in `Sidebar.svelte::
9
+ * handleAutoDetection`, so both sides must agree on the format.
10
+ */
11
+ export function eagerUrlTabId(url) {
12
+ return `url:${url}`;
13
+ }
4
14
  function releaseDuckDbMemory() {
5
15
  import('../query/index.js')
6
16
  .then(({ getQueryEngine }) => getQueryEngine().then((engine) => engine.releaseMemory()))
@@ -106,5 +116,4 @@ function createTabsStore() {
106
116
  }
107
117
  };
108
118
  }
109
- export const tabStore = createTabsStore();
110
- export { tabStore as tabs };
119
+ export const tabs = createTabsStore();
package/dist/types.d.ts CHANGED
@@ -38,6 +38,17 @@ export interface Tab {
38
38
  connectionId?: string;
39
39
  extension: string;
40
40
  size?: number;
41
+ /**
42
+ * When set, the tab reads data from a SQL FROM-clause target (e.g. an
43
+ * attached DuckLake/DuckDB/SQLite table) rather than a file URL. The ref
44
+ * is inserted directly into generated SQL, so it must be fully-qualified
45
+ * and pre-quoted, e.g. `__objex_db__."main"."air_quality"`.
46
+ *
47
+ * When `sourceRef` is set, file-specific loading paths (hyparquet
48
+ * metadata, `parquet_kv_metadata`, etc.) are skipped, and schema / CRS /
49
+ * row count are derived from the SQL source directly via DuckDB.
50
+ */
51
+ sourceRef?: string;
41
52
  }
42
53
  export interface WriteResult {
43
54
  key: string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Cloud storage protocol URL utilities — pure TS, no Svelte dependency.
3
+ *
4
+ * Converts cloud protocol URLs (s3://, gs://) to HTTPS URLs for browser access.
5
+ * Provider-aware native scheme lookup.
6
+ */
7
+ /**
8
+ * Map provider to its native URI scheme prefix.
9
+ * Derived from the registry's `schemes` array (first entry is the primary scheme).
10
+ * Falls back to 's3' for providers without a scheme (S3-compatible).
11
+ */
12
+ export declare function getNativeScheme(provider: string): string;
13
+ /**
14
+ * Safely decode a percent-encoded URI component.
15
+ * Returns the original string if decoding fails (malformed sequences).
16
+ */
17
+ export declare function safeDecodeURIComponent(s: string): string;
18
+ /**
19
+ * Convert a cloud storage protocol URL (s3://, gs://) to an HTTPS URL
20
+ * for browser access. Returns the original URL if already HTTP(S) or unknown.
21
+ *
22
+ * Supported:
23
+ * - `s3://bucket/key` → `https://s3.{region}.amazonaws.com/{bucket}/{key}`
24
+ * (region auto-detected from bucket name when possible)
25
+ * - `gs://bucket/key` → `https://storage.googleapis.com/{bucket}/{key}`
26
+ */
27
+ export declare function resolveCloudUrl(url: string): string;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Cloud storage protocol URL utilities — pure TS, no Svelte dependency.
3
+ *
4
+ * Converts cloud protocol URLs (s3://, gs://) to HTTPS URLs for browser access.
5
+ * Provider-aware native scheme lookup.
6
+ */
7
+ import { buildProviderBaseUrl, PROVIDERS } from '../storage/providers.js';
8
+ /** AWS region pattern — matches prefixes like "us-west-2", "eu-central-1", etc. */
9
+ const AWS_REGION_RE = /^(us|eu|ap|sa|ca|me|af|il)-(north|south|east|west|central|northeast|southeast|northwest|southwest)-\d+/;
10
+ /**
11
+ * Map provider to its native URI scheme prefix.
12
+ * Derived from the registry's `schemes` array (first entry is the primary scheme).
13
+ * Falls back to 's3' for providers without a scheme (S3-compatible).
14
+ */
15
+ export function getNativeScheme(provider) {
16
+ const def = PROVIDERS[provider];
17
+ if (def?.schemes.length)
18
+ return def.schemes[0];
19
+ return 's3';
20
+ }
21
+ /**
22
+ * Safely decode a percent-encoded URI component.
23
+ * Returns the original string if decoding fails (malformed sequences).
24
+ */
25
+ export function safeDecodeURIComponent(s) {
26
+ try {
27
+ return decodeURIComponent(s);
28
+ }
29
+ catch {
30
+ return s;
31
+ }
32
+ }
33
+ /**
34
+ * Convert a cloud storage protocol URL (s3://, gs://) to an HTTPS URL
35
+ * for browser access. Returns the original URL if already HTTP(S) or unknown.
36
+ *
37
+ * Supported:
38
+ * - `s3://bucket/key` → `https://s3.{region}.amazonaws.com/{bucket}/{key}`
39
+ * (region auto-detected from bucket name when possible)
40
+ * - `gs://bucket/key` → `https://storage.googleapis.com/{bucket}/{key}`
41
+ */
42
+ export function resolveCloudUrl(url) {
43
+ // S3 / S3-compatible: s3://, s3a://, s3n://
44
+ const s3Match = url.match(/^s3[an]?:\/\/([^/]+)\/?(.*)$/);
45
+ if (s3Match) {
46
+ const [, bucket, key] = s3Match;
47
+ // Detect region from bucket name (e.g. "us-west-2.opendata.source.coop")
48
+ const regionMatch = bucket.match(AWS_REGION_RE);
49
+ const region = regionMatch ? regionMatch[0] : 'us-east-1';
50
+ const base = buildProviderBaseUrl('s3', '', bucket, region);
51
+ return key ? `${base}/${key}` : base;
52
+ }
53
+ // Google Cloud Storage: gs://, gcs://
54
+ const gcsMatch = url.match(/^gcs?:\/\/([^/]+)\/?(.*)$/);
55
+ if (gcsMatch) {
56
+ const [, bucket, key] = gcsMatch;
57
+ const base = buildProviderBaseUrl('gcs', '', bucket, '');
58
+ return key ? `${base}/${key}` : base;
59
+ }
60
+ return url;
61
+ }