sh3-core 0.19.6 → 0.20.1

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 (66) hide show
  1. package/dist/app/admin/AuthSettingsView.svelte +3 -9
  2. package/dist/app/admin/MountsView.svelte +276 -0
  3. package/dist/app/admin/MountsView.svelte.d.ts +3 -0
  4. package/dist/app/admin/SystemView.svelte +6 -6
  5. package/dist/app/admin/UsersView.svelte +103 -7
  6. package/dist/app/admin/adminApp.js +1 -0
  7. package/dist/app/admin/adminShard.svelte.js +10 -0
  8. package/dist/apps/lifecycle.js +1 -0
  9. package/dist/apps/types.d.ts +7 -0
  10. package/dist/assets/iconIds.generated.d.ts +1 -1
  11. package/dist/assets/iconIds.generated.js +1 -0
  12. package/dist/assets/icons.svg +5 -0
  13. package/dist/auth/admin-users.svelte.js +2 -1
  14. package/dist/auth/auth.svelte.d.ts +4 -5
  15. package/dist/auth/auth.svelte.js +5 -6
  16. package/dist/auth/types.d.ts +0 -2
  17. package/dist/chrome/CompactChrome.svelte +25 -6
  18. package/dist/chrome/FloatsSheet.svelte +7 -32
  19. package/dist/chrome/FloatsSheet.svelte.d.ts +1 -2
  20. package/dist/chrome/FloatsSheet.svelte.test.js +8 -14
  21. package/dist/chrome/MenuSheet.svelte +154 -148
  22. package/dist/chrome/MenuSheet.svelte.d.ts +1 -2
  23. package/dist/chrome/MenuSheet.svelte.test.js +24 -12
  24. package/dist/createShell.js +32 -21
  25. package/dist/createShell.remoteAuth.test.js +9 -3
  26. package/dist/documents/browse.d.ts +18 -1
  27. package/dist/documents/browse.js +40 -7
  28. package/dist/documents/browse.test.js +35 -35
  29. package/dist/documents/config.d.ts +4 -0
  30. package/dist/documents/config.js +15 -2
  31. package/dist/documents/handle.js +25 -17
  32. package/dist/documents/http-backend.js +10 -2
  33. package/dist/documents/index.d.ts +2 -2
  34. package/dist/documents/index.js +1 -1
  35. package/dist/documents/picker-api.d.ts +4 -2
  36. package/dist/documents/picker-api.test.d.ts +1 -1
  37. package/dist/documents/picker-api.test.js +87 -57
  38. package/dist/documents/picker-primitive.d.ts +4 -0
  39. package/dist/documents/picker-primitive.js +27 -29
  40. package/dist/documents/types.d.ts +17 -5
  41. package/dist/documents/types.js +2 -0
  42. package/dist/layout/presets.test.js +4 -4
  43. package/dist/layout/types.d.ts +1 -1
  44. package/dist/layouts-shard/LayoutsSection.svelte +3 -16
  45. package/dist/primitives/widgets/DocumentFilePicker.svelte +4 -4
  46. package/dist/primitives/widgets/PickerList.svelte +1 -0
  47. package/dist/primitives/widgets/_DocumentBrowser.svelte +5 -8
  48. package/dist/projects-shard/DeleteProjectDialog.svelte +32 -1
  49. package/dist/projects-shard/ProjectManage.svelte +197 -28
  50. package/dist/projects-shard/ProjectManage.svelte.test.d.ts +1 -0
  51. package/dist/projects-shard/ProjectManage.svelte.test.js +320 -0
  52. package/dist/projects-shard/ProjectsSection.svelte +3 -16
  53. package/dist/projects-shard/projectsApi.js +2 -1
  54. package/dist/registry/permission-descriptions.js +4 -0
  55. package/dist/server-shard/types.d.ts +21 -0
  56. package/dist/sh3core-shard/HomeSection.svelte +107 -0
  57. package/dist/sh3core-shard/HomeSection.svelte.d.ts +10 -0
  58. package/dist/sh3core-shard/Sh3Home.svelte +9 -23
  59. package/dist/shards/activate.svelte.d.ts +4 -0
  60. package/dist/shards/activate.svelte.js +9 -1
  61. package/dist/shards/types.d.ts +7 -0
  62. package/dist/shell-shard/tenant-fs-client.js +2 -1
  63. package/dist/transport/apiFetch.js +12 -5
  64. package/dist/version.d.ts +1 -1
  65. package/dist/version.js +1 -1
  66. package/package.json +1 -1
@@ -11,6 +11,7 @@
11
11
  import { projectsState, openProjectManage } from './projectsShard.svelte';
12
12
  import { sessionState, setActiveProjectId } from '../projects/session-state.svelte';
13
13
  import { isAdmin } from '../auth/auth.svelte';
14
+ import HomeSection from '../sh3core-shard/HomeSection.svelte';
14
15
 
15
16
  const visible = $derived(projectsState.projects.length > 0);
16
17
  const activeId = $derived(sessionState.activeProjectId);
@@ -28,8 +29,7 @@
28
29
  </script>
29
30
 
30
31
  {#if visible}
31
- <section class="projects-section">
32
- <h2 class="projects-heading">Projects</h2>
32
+ <HomeSection title="Projects" persistKey="projects">
33
33
  <div class="projects-grid">
34
34
  {#each projectsState.projects as project (project.id)}
35
35
  <div class="project-card-wrap">
@@ -55,23 +55,10 @@
55
55
  </div>
56
56
  {/each}
57
57
  </div>
58
- </section>
58
+ </HomeSection>
59
59
  {/if}
60
60
 
61
61
  <style>
62
- .projects-section {
63
- width: 100%;
64
- max-width: 720px;
65
- margin-bottom: 28px;
66
- }
67
- .projects-heading {
68
- font-size: 13px;
69
- font-weight: 600;
70
- text-transform: uppercase;
71
- letter-spacing: 0.06em;
72
- color: var(--sh3-fg-subtle);
73
- margin: 0 0 12px;
74
- }
75
62
  .projects-grid {
76
63
  display: grid;
77
64
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
@@ -4,9 +4,10 @@
4
4
  * Returns plain JSON objects; the shard layer is responsible for keeping
5
5
  * a reactive copy and for converting these to actions / views.
6
6
  */
7
+ import { apiFetch } from '../transport/apiFetch';
7
8
  async function jsonFetch(url, init) {
8
9
  var _a;
9
- const res = await fetch(url, Object.assign({ credentials: 'include' }, init));
10
+ const res = await apiFetch(url, init);
10
11
  if (!res.ok)
11
12
  throw new Error(`${(_a = init === null || init === void 0 ? void 0 : init.method) !== null && _a !== void 0 ? _a : 'GET'} ${url} failed: ${res.status}`);
12
13
  return res.json();
@@ -27,6 +27,10 @@ export const PERMISSION_DESCRIPTIONS = {
27
27
  title: 'Sync documents with peers',
28
28
  description: 'Participate in cross-peer document synchronization.',
29
29
  },
30
+ 'documents:mount': {
31
+ title: 'Manage document mounts',
32
+ description: 'Define and manage filesystem mount points that feed into the document API.',
33
+ },
30
34
  'sync:peer': {
31
35
  title: 'Act as a sync peer',
32
36
  description: 'Exchange document updates with remote peers.',
@@ -95,6 +95,27 @@ export interface ServerShardContext {
95
95
  * Absent => 'primary' behavior at the store.
96
96
  */
97
97
  setPeerRole(tenant: string, role: 'primary' | 'replica'): void;
98
+ /**
99
+ * Translate an SH3 document path to a real filesystem path on the host.
100
+ *
101
+ * Symmetric with `documents(tenant)` — the same `(shardId, path)` pair
102
+ * resolves through both APIs. Use this when a server shard needs to
103
+ * hand the path to a native consumer (spawn, fs.watch, streaming I/O)
104
+ * rather than read bytes via `documents().read()`.
105
+ *
106
+ * - `shardId === 'mounts'` resolves through the host's MountedPathResolver
107
+ * and surfaces the real on-disk path of the mount. Throws on unknown,
108
+ * unattached, or otherwise unresolvable mounts.
109
+ * - Any other `shardId` returns `<dataDir>/docs/<tenant>/<shardId>/<path>`,
110
+ * the canonical native-doc location. The file may or may not exist —
111
+ * use `documents(tenant).exists` to check.
112
+ *
113
+ * Note: once a shard takes the fs path and does its own I/O, the doc store
114
+ * loses its grip on conflict bookkeeping for those operations. Safe for
115
+ * mount-backed assets (read-mostly, outside the sync envelope); use with
116
+ * care for native shard docs that participate in Mode A/B sync.
117
+ */
118
+ resolveFsPath(tenant: string, shardId: string, path: string): string;
98
119
  }
99
120
  /**
100
121
  * The interface a server shard bundle must default-export.
@@ -0,0 +1,107 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Collapsible section wrapper used by Sh3Home and section components.
4
+ * Title row is a button that toggles the content; expanded state is
5
+ * persisted to localStorage under `sh3:home-section:<persistKey>` so it
6
+ * survives reloads. Heading chrome (typography, spacing, max-width) is
7
+ * centralised here so individual sections only supply their grid/content.
8
+ */
9
+
10
+ import type { Snippet } from 'svelte';
11
+ import iconsUrl from '../assets/icons.svg';
12
+
13
+ interface Props {
14
+ title: string;
15
+ persistKey: string;
16
+ defaultExpanded?: boolean;
17
+ children: Snippet;
18
+ }
19
+
20
+ const { title, persistKey, defaultExpanded = true, children }: Props = $props();
21
+
22
+ const storageKey = $derived(`sh3:home-section:${persistKey}`);
23
+ const sectionId = $derived(`sh3-home-section-${persistKey}`);
24
+
25
+ function readInitial(): boolean {
26
+ if (typeof localStorage === 'undefined') return defaultExpanded;
27
+ const raw = localStorage.getItem(`sh3:home-section:${persistKey}`);
28
+ if (raw === '1') return true;
29
+ if (raw === '0') return false;
30
+ return defaultExpanded;
31
+ }
32
+
33
+ let expanded = $state(readInitial());
34
+
35
+ function toggle(): void {
36
+ expanded = !expanded;
37
+ if (typeof localStorage !== 'undefined') {
38
+ localStorage.setItem(storageKey, expanded ? '1' : '0');
39
+ }
40
+ }
41
+ </script>
42
+
43
+ <section class="home-section">
44
+ <button
45
+ type="button"
46
+ class="home-section-header"
47
+ aria-expanded={expanded}
48
+ aria-controls={sectionId}
49
+ onclick={toggle}
50
+ >
51
+ <svg class="home-section-chevron" class:open={expanded} aria-hidden="true">
52
+ <use href="{iconsUrl}#chevron-right" />
53
+ </svg>
54
+ <span class="home-section-title">{title}</span>
55
+ </button>
56
+ {#if expanded}
57
+ <div id={sectionId} class="home-section-body">
58
+ {@render children()}
59
+ </div>
60
+ {/if}
61
+ </section>
62
+
63
+ <style>
64
+ .home-section {
65
+ width: 100%;
66
+ max-width: 720px;
67
+ margin-bottom: 28px;
68
+ }
69
+ .home-section-header {
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 6px;
73
+ width: 100%;
74
+ margin: 0 0 12px;
75
+ padding: 0;
76
+ background: transparent;
77
+ border: none;
78
+ color: var(--sh3-fg-subtle);
79
+ font: inherit;
80
+ font-size: 13px;
81
+ font-weight: 600;
82
+ text-transform: uppercase;
83
+ letter-spacing: 0.06em;
84
+ text-align: left;
85
+ cursor: pointer;
86
+ }
87
+ .home-section-header:hover {
88
+ color: var(--sh3-fg);
89
+ }
90
+ .home-section-header:focus-visible {
91
+ outline: 2px solid var(--sh3-accent);
92
+ outline-offset: 2px;
93
+ border-radius: 2px;
94
+ }
95
+ .home-section-chevron {
96
+ width: 12px;
97
+ height: 12px;
98
+ flex-shrink: 0;
99
+ transition: transform 120ms ease;
100
+ }
101
+ .home-section-chevron.open {
102
+ transform: rotate(90deg);
103
+ }
104
+ .home-section-title {
105
+ flex: 1;
106
+ }
107
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ title: string;
4
+ persistKey: string;
5
+ defaultExpanded?: boolean;
6
+ children: Snippet;
7
+ }
8
+ declare const HomeSection: import("svelte").Component<Props, {}, "">;
9
+ type HomeSection = ReturnType<typeof HomeSection>;
10
+ export default HomeSection;
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { listRegisteredApps, launchApp, isAdmin, VERSION } from '../api';
11
11
  import Sh3Title from './Sh3Title.svelte';
12
+ import HomeSection from './HomeSection.svelte';
12
13
  import ProjectsSection from '../projects-shard/ProjectsSection.svelte';
13
14
  import LayoutsSection from '../layouts-shard/LayoutsSection.svelte';
14
15
  import { sessionState } from '../projects/session-state.svelte';
@@ -56,7 +57,7 @@
56
57
  apps.filter((m) => !m.admin && matches(m, filter) && inAllowlist(m.id)),
57
58
  );
58
59
  const adminApps = $derived(apps.filter((m) => m.admin && matches(m, filter)));
59
- const totalVisible = $derived(userApps.length + (elevated ? adminApps.length : 0));
60
+ const totalVisible = $derived(userApps.length + (elevated && !activeProject ? adminApps.length : 0));
60
61
  </script>
61
62
 
62
63
  <div class="sh3-home">
@@ -83,11 +84,8 @@
83
84
 
84
85
  <ProjectsSection />
85
86
 
86
- <LayoutsSection />
87
-
88
87
  {#if userApps.length > 0}
89
- <section class="sh3-home-section">
90
- <h2 class="sh3-home-section-title">Apps</h2>
88
+ <HomeSection title="Apps" persistKey="apps">
91
89
  <div class="sh3-home-grid">
92
90
  {#each userApps as manifest (manifest.id)}
93
91
  {@const appearance = getAppearance(manifest.id)}
@@ -107,12 +105,13 @@
107
105
  </button>
108
106
  {/each}
109
107
  </div>
110
- </section>
108
+ </HomeSection>
111
109
  {/if}
112
110
 
113
- {#if elevated && adminApps.length > 0}
114
- <section class="sh3-home-section">
115
- <h2 class="sh3-home-section-title">Admin</h2>
111
+ <LayoutsSection />
112
+
113
+ {#if elevated && !activeProject && adminApps.length > 0}
114
+ <HomeSection title="Admin" persistKey="admin">
116
115
  <div class="sh3-home-grid">
117
116
  {#each adminApps as manifest (manifest.id)}
118
117
  {@const appearance = getAppearance(manifest.id)}
@@ -132,7 +131,7 @@
132
131
  </button>
133
132
  {/each}
134
133
  </div>
135
- </section>
134
+ </HomeSection>
136
135
  {/if}
137
136
 
138
137
  {#if totalVisible === 0}
@@ -233,19 +232,6 @@
233
232
  color: var(--sh3-fg-muted);
234
233
  font-style: italic;
235
234
  }
236
- .sh3-home-section {
237
- width: 100%;
238
- max-width: 720px;
239
- margin-bottom: 28px;
240
- }
241
- .sh3-home-section-title {
242
- font-size: 13px;
243
- font-weight: 600;
244
- text-transform: uppercase;
245
- letter-spacing: 0.06em;
246
- color: var(--sh3-fg-subtle);
247
- margin: 0 0 12px;
248
- }
249
235
  .sh3-home-grid {
250
236
  display: grid;
251
237
  grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
@@ -22,6 +22,10 @@ export interface ShardErrorEntry {
22
22
  timestamp: number;
23
23
  }
24
24
  export declare const erroredShards: Map<string, ShardErrorEntry>;
25
+ /** Host-only. Register a callback that resolves whether the current scope
26
+ * is a personal tenant or an active project. Wired by createShell after
27
+ * bootstrap. Avoids circular dependencies with session-state. */
28
+ export declare function __setScopeResolver(resolver: (() => 'tenant' | 'project') | null): void;
25
29
  /**
26
30
  * Register (or re-register) a shard with the framework so it can later be
27
31
  * activated. Records the shard in `registeredShards` but does not run
@@ -52,6 +52,13 @@ export const registeredShards = $state(new Map());
52
52
  const active = new Map();
53
53
  export const activeShards = $state(new Map());
54
54
  export const erroredShards = $state(new Map());
55
+ let scopeResolver = null;
56
+ /** Host-only. Register a callback that resolves whether the current scope
57
+ * is a personal tenant or an active project. Wired by createShell after
58
+ * bootstrap. Avoids circular dependencies with session-state. */
59
+ export function __setScopeResolver(resolver) {
60
+ scopeResolver = resolver;
61
+ }
55
62
  /**
56
63
  * Register (or re-register) a shard with the framework so it can later be
57
64
  * activated. Records the shard in `registeredShards` but does not run
@@ -146,7 +153,7 @@ export async function activateShard(id, opts) {
146
153
  };
147
154
  const hasBrowse = (_a = shard.manifest.permissions) === null || _a === void 0 ? void 0 : _a.includes(PERMISSION_DOCUMENTS_BROWSE);
148
155
  const browseCap = hasBrowse
149
- ? createBrowseCapability(getTenantId(), getDocumentBackend(), {
156
+ ? createBrowseCapability(() => getTenantId(), getDocumentBackend(), {
150
157
  canRead: (_c = (_b = shard.manifest.permissions) === null || _b === void 0 ? void 0 : _b.includes(PERMISSION_DOCUMENTS_READ)) !== null && _c !== void 0 ? _c : false,
151
158
  canWrite: (_e = (_d = shard.manifest.permissions) === null || _d === void 0 ? void 0 : _d.includes(PERMISSION_DOCUMENTS_WRITE)) !== null && _e !== void 0 ? _e : false,
152
159
  })
@@ -220,6 +227,7 @@ export async function activateShard(id, opts) {
220
227
  get tenantId() {
221
228
  return getTenantId();
222
229
  },
230
+ getScope: () => { var _a; return (_a = scopeResolver === null || scopeResolver === void 0 ? void 0 : scopeResolver()) !== null && _a !== void 0 ? _a : 'tenant'; },
223
231
  zones: ((_f = shard.manifest.permissions) === null || _f === void 0 ? void 0 : _f.includes(PERMISSION_STATE_MANAGE))
224
232
  ? createZoneManager()
225
233
  : undefined,
@@ -274,6 +274,13 @@ export interface ShardContext {
274
274
  * serialize into persistent storage.
275
275
  */
276
276
  tenantId: string;
277
+ /**
278
+ * Whether this shard is running in a 'tenant' (personal) or 'project'
279
+ * scope. Shards that need to scope their behavior differently per context
280
+ * can read this at any time — it is reactive through the session state.
281
+ * Returns 'tenant' when no project is active, 'project' otherwise.
282
+ */
283
+ getScope(): 'tenant' | 'project';
277
284
  /**
278
285
  * Cross-shard zone management API. Only present when the shard's
279
286
  * manifest declares the `'state:manage'` permission. Check with
@@ -8,6 +8,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
8
8
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
9
9
  };
10
10
  var _TenantFsClient_instances, _TenantFsClient_get;
11
+ import { apiFetch } from '../transport/apiFetch';
11
12
  export class TenantFsClient {
12
13
  constructor(base = '') {
13
14
  _TenantFsClient_instances.add(this);
@@ -29,7 +30,7 @@ export class TenantFsClient {
29
30
  }
30
31
  _TenantFsClient_instances = new WeakSet(), _TenantFsClient_get = async function _TenantFsClient_get(route, path) {
31
32
  const url = `${this.base}${route}?path=${encodeURIComponent(path)}`;
32
- const res = await fetch(url, { credentials: 'include' });
33
+ const res = await apiFetch(url);
33
34
  if (!res.ok) {
34
35
  let msg = `HTTP ${res.status}`;
35
36
  try {
@@ -43,7 +43,7 @@ async function getTauriFetch() {
43
43
  }
44
44
  return tauriFetch;
45
45
  }
46
- export async function apiFetch(url, init) {
46
+ export function apiFetch(url, init) {
47
47
  var _a;
48
48
  // Inject Authorization: Bearer <session-token> if a session is active
49
49
  // and the caller didn't already supply one. Cookies don't survive the
@@ -58,8 +58,15 @@ export async function apiFetch(url, init) {
58
58
  finalInit = Object.assign(Object.assign({}, finalInit), { headers });
59
59
  }
60
60
  }
61
- const tf = await getTauriFetch();
62
- if (tf)
63
- return tf(url, finalInit);
64
- return fetch(url, Object.assign({ credentials: 'include' }, finalInit));
61
+ // Fast path: outside the Tauri webview, fire fetch synchronously so
62
+ // call-timing assertions in tests still hold and web builds skip the
63
+ // dynamic-import probe entirely.
64
+ if (!inTauriRuntime()) {
65
+ return fetch(url, Object.assign({ credentials: 'include' }, finalInit));
66
+ }
67
+ return getTauriFetch().then((tf) => {
68
+ if (tf)
69
+ return tf(url, finalInit);
70
+ return fetch(url, Object.assign({ credentials: 'include' }, finalInit));
71
+ });
65
72
  }
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export declare const VERSION = "0.19.6";
2
+ export declare const VERSION = "0.20.1";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** Auto-generated from package.json — do not edit manually. */
2
- export const VERSION = '0.19.6';
2
+ export const VERSION = '0.20.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sh3-core",
3
- "version": "0.19.6",
3
+ "version": "0.20.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist"