sh3-core 0.13.1 → 0.13.3

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 (172) hide show
  1. package/dist/BrandSlot.svelte +62 -13
  2. package/dist/__test__/setup-dom.js +5 -0
  3. package/dist/actions/MenuButton.svelte +2 -1
  4. package/dist/actions/contextMenuModel.d.ts +1 -1
  5. package/dist/actions/contextMenuModel.js +2 -1
  6. package/dist/actions/dispatcher.svelte.d.ts +1 -1
  7. package/dist/actions/dispatcher.svelte.js +2 -1
  8. package/dist/actions/listActive.d.ts +1 -1
  9. package/dist/actions/listActive.js +2 -1
  10. package/dist/actions/listeners.d.ts +1 -1
  11. package/dist/actions/listeners.js +6 -5
  12. package/dist/actions/menuBarModel.js +3 -2
  13. package/dist/actions/paletteModel.js +2 -1
  14. package/dist/actions/resolveLabel.test.js +14 -0
  15. package/dist/actions/types.d.ts +12 -1
  16. package/dist/actions/types.js +7 -1
  17. package/dist/api.d.ts +3 -0
  18. package/dist/api.js +3 -0
  19. package/dist/app/store/AppUpdateAvailableModal.svelte +87 -0
  20. package/dist/app/store/AppUpdateAvailableModal.svelte.d.ts +11 -0
  21. package/dist/app/store/InstalledView.svelte +8 -54
  22. package/dist/app/store/UninstallAppDialog.svelte +86 -0
  23. package/dist/app/store/UninstallAppDialog.svelte.d.ts +10 -0
  24. package/dist/app/store/permissionConfirm.d.ts +4 -0
  25. package/dist/app/store/permissionConfirm.js +28 -0
  26. package/dist/app/store/storeShard.svelte.d.ts +8 -1
  27. package/dist/app/store/storeShard.svelte.js +42 -9
  28. package/dist/app/store/updatePackage.test.d.ts +1 -0
  29. package/dist/app/store/updatePackage.test.js +34 -0
  30. package/dist/app/store/verbs.d.ts +1 -0
  31. package/dist/app/store/verbs.js +79 -5
  32. package/dist/app/store/verbs.test.d.ts +1 -0
  33. package/dist/app/store/verbs.test.js +56 -0
  34. package/dist/app-appearance/AppAppearanceModal.svelte +174 -0
  35. package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +8 -0
  36. package/dist/app-appearance/appearanceShard.svelte.d.ts +2 -0
  37. package/dist/app-appearance/appearanceShard.svelte.js +61 -0
  38. package/dist/app-appearance/appearanceState.svelte.d.ts +15 -0
  39. package/dist/app-appearance/appearanceState.svelte.js +59 -0
  40. package/dist/app-appearance/appearanceState.test.d.ts +1 -0
  41. package/dist/app-appearance/appearanceState.test.js +30 -0
  42. package/dist/app-appearance/index.d.ts +3 -0
  43. package/dist/app-appearance/index.js +2 -0
  44. package/dist/app-appearance/types.d.ts +11 -0
  45. package/dist/app-appearance/types.js +1 -0
  46. package/dist/apps/lifecycle.js +10 -2
  47. package/dist/apps/types.d.ts +18 -4
  48. package/dist/apps/workspace-rekey.d.ts +1 -0
  49. package/dist/apps/workspace-rekey.js +35 -0
  50. package/dist/apps/workspace-rekey.test.d.ts +1 -0
  51. package/dist/apps/workspace-rekey.test.js +23 -0
  52. package/dist/assets/iconIds.generated.d.ts +2 -0
  53. package/dist/assets/iconIds.generated.js +154 -0
  54. package/dist/auth/admin-users.svelte.d.ts +9 -0
  55. package/dist/auth/admin-users.svelte.js +42 -0
  56. package/dist/auth/admin-users.test.d.ts +1 -0
  57. package/dist/auth/admin-users.test.js +52 -0
  58. package/dist/createShell.js +5 -5
  59. package/dist/documents/config.d.ts +5 -1
  60. package/dist/documents/config.js +16 -8
  61. package/dist/documents/index.d.ts +1 -1
  62. package/dist/documents/index.js +1 -1
  63. package/dist/host-entry.d.ts +1 -1
  64. package/dist/host-entry.js +1 -1
  65. package/dist/host.d.ts +1 -1
  66. package/dist/host.js +9 -2
  67. package/dist/primitives/Button.svelte +50 -4
  68. package/dist/primitives/Button.svelte.d.ts +3 -1
  69. package/dist/primitives/Collapsible.svelte +110 -0
  70. package/dist/primitives/Collapsible.svelte.d.ts +14 -0
  71. package/dist/primitives/widgets/AppPicker.svelte +41 -0
  72. package/dist/primitives/widgets/AppPicker.svelte.d.ts +9 -0
  73. package/dist/primitives/widgets/AppPicker.svelte.test.d.ts +1 -0
  74. package/dist/primitives/widgets/AppPicker.svelte.test.js +26 -0
  75. package/dist/primitives/widgets/AppPicker.test.d.ts +1 -0
  76. package/dist/primitives/widgets/AppPicker.test.js +74 -0
  77. package/dist/primitives/widgets/ColorSwatch.svelte +7 -2
  78. package/dist/primitives/widgets/ColorSwatch.svelte.d.ts +2 -1
  79. package/dist/primitives/widgets/ColorSwatch.svelte.test.d.ts +1 -0
  80. package/dist/primitives/widgets/ColorSwatch.svelte.test.js +31 -0
  81. package/dist/primitives/widgets/Field.svelte +4 -2
  82. package/dist/primitives/widgets/Field.svelte.d.ts +2 -2
  83. package/dist/primitives/widgets/Field.svelte.test.d.ts +1 -0
  84. package/dist/primitives/widgets/Field.svelte.test.js +33 -0
  85. package/dist/primitives/widgets/FilePicker.svelte +2 -2
  86. package/dist/primitives/widgets/FilePicker.svelte.d.ts +2 -2
  87. package/dist/primitives/widgets/FilePicker.svelte.test.d.ts +1 -0
  88. package/dist/primitives/widgets/FilePicker.svelte.test.js +31 -0
  89. package/dist/primitives/widgets/IconPicker.svelte +115 -0
  90. package/dist/primitives/widgets/IconPicker.svelte.d.ts +9 -0
  91. package/dist/primitives/widgets/IconPicker.svelte.test.d.ts +1 -0
  92. package/dist/primitives/widgets/IconPicker.svelte.test.js +43 -0
  93. package/dist/primitives/widgets/IconToggleGroup.svelte +4 -4
  94. package/dist/primitives/widgets/IconToggleGroup.svelte.d.ts +3 -3
  95. package/dist/primitives/widgets/IconToggleGroup.svelte.test.d.ts +1 -0
  96. package/dist/primitives/widgets/IconToggleGroup.svelte.test.js +40 -0
  97. package/dist/primitives/widgets/NumberInput.svelte +19 -9
  98. package/dist/primitives/widgets/NumberInput.svelte.d.ts +2 -2
  99. package/dist/primitives/widgets/NumberInput.svelte.test.d.ts +1 -0
  100. package/dist/primitives/widgets/NumberInput.svelte.test.js +48 -0
  101. package/dist/primitives/widgets/PickerList.d.ts +24 -0
  102. package/dist/primitives/widgets/PickerList.js +21 -0
  103. package/dist/primitives/widgets/PickerList.svelte +150 -0
  104. package/dist/primitives/widgets/PickerList.svelte.d.ts +16 -0
  105. package/dist/primitives/widgets/PickerList.svelte.test.d.ts +1 -0
  106. package/dist/primitives/widgets/PickerList.svelte.test.js +31 -0
  107. package/dist/primitives/widgets/PickerList.test.d.ts +1 -0
  108. package/dist/primitives/widgets/PickerList.test.js +218 -0
  109. package/dist/primitives/widgets/RangeSlider.svelte +11 -4
  110. package/dist/primitives/widgets/RangeSlider.svelte.d.ts +2 -2
  111. package/dist/primitives/widgets/RangeSlider.svelte.test.d.ts +1 -0
  112. package/dist/primitives/widgets/RangeSlider.svelte.test.js +38 -0
  113. package/dist/primitives/widgets/Segmented.svelte +4 -4
  114. package/dist/primitives/widgets/Segmented.svelte.d.ts +3 -3
  115. package/dist/primitives/widgets/Segmented.svelte.test.d.ts +1 -0
  116. package/dist/primitives/widgets/Segmented.svelte.test.js +25 -0
  117. package/dist/primitives/widgets/Select.svelte +4 -4
  118. package/dist/primitives/widgets/Select.svelte.d.ts +3 -3
  119. package/dist/primitives/widgets/Select.svelte.test.d.ts +1 -0
  120. package/dist/primitives/widgets/Select.svelte.test.js +37 -0
  121. package/dist/primitives/widgets/Slider.svelte +4 -2
  122. package/dist/primitives/widgets/Slider.svelte.d.ts +2 -2
  123. package/dist/primitives/widgets/Slider.svelte.test.d.ts +1 -0
  124. package/dist/primitives/widgets/Slider.svelte.test.js +22 -0
  125. package/dist/primitives/widgets/SliderGroup.svelte +4 -2
  126. package/dist/primitives/widgets/SliderGroup.svelte.d.ts +2 -2
  127. package/dist/primitives/widgets/SliderGroup.svelte.test.d.ts +1 -0
  128. package/dist/primitives/widgets/SliderGroup.svelte.test.js +34 -0
  129. package/dist/primitives/widgets/Textarea.svelte +5 -2
  130. package/dist/primitives/widgets/Textarea.svelte.d.ts +2 -2
  131. package/dist/primitives/widgets/Textarea.svelte.test.d.ts +1 -0
  132. package/dist/primitives/widgets/Textarea.svelte.test.js +29 -0
  133. package/dist/primitives/widgets/UserPicker.svelte +53 -0
  134. package/dist/primitives/widgets/UserPicker.svelte.d.ts +9 -0
  135. package/dist/primitives/widgets/UserPicker.svelte.test.d.ts +1 -0
  136. package/dist/primitives/widgets/UserPicker.svelte.test.js +30 -0
  137. package/dist/primitives/widgets/UserPicker.test.d.ts +1 -0
  138. package/dist/primitives/widgets/UserPicker.test.js +115 -0
  139. package/dist/primitives/widgets/_contract.d.ts +27 -0
  140. package/dist/primitives/widgets/_contract.js +10 -0
  141. package/dist/projects/session-state.svelte.d.ts +17 -0
  142. package/dist/projects/session-state.svelte.js +39 -0
  143. package/dist/projects/session-state.test.d.ts +1 -0
  144. package/dist/projects/session-state.test.js +55 -0
  145. package/dist/projects-shard/DeleteProjectDialog.svelte +150 -0
  146. package/dist/projects-shard/DeleteProjectDialog.svelte.d.ts +12 -0
  147. package/dist/projects-shard/DeleteProjectDialog.test.d.ts +1 -0
  148. package/dist/projects-shard/DeleteProjectDialog.test.js +120 -0
  149. package/dist/projects-shard/ProjectManage.svelte +219 -0
  150. package/dist/projects-shard/ProjectManage.svelte.d.ts +8 -0
  151. package/dist/projects-shard/ProjectsSection.svelte +120 -0
  152. package/dist/projects-shard/ProjectsSection.svelte.d.ts +3 -0
  153. package/dist/projects-shard/index.d.ts +4 -0
  154. package/dist/projects-shard/index.js +4 -0
  155. package/dist/projects-shard/projectsApi.d.ts +20 -0
  156. package/dist/projects-shard/projectsApi.js +44 -0
  157. package/dist/projects-shard/projectsApi.test.d.ts +1 -0
  158. package/dist/projects-shard/projectsApi.test.js +71 -0
  159. package/dist/projects-shard/projectsShard.svelte.d.ts +10 -0
  160. package/dist/projects-shard/projectsShard.svelte.js +148 -0
  161. package/dist/sh3core-shard/ShellHome.svelte +83 -39
  162. package/dist/sh3core-shard/appActions.d.ts +13 -0
  163. package/dist/sh3core-shard/appActions.js +181 -0
  164. package/dist/sh3core-shard/appActions.test.d.ts +1 -0
  165. package/dist/sh3core-shard/appActions.test.js +25 -0
  166. package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -0
  167. package/dist/shards/activate-scopeid.test.d.ts +1 -0
  168. package/dist/shards/{activate-tenantid.test.js → activate-scopeid.test.js} +6 -6
  169. package/dist/version.d.ts +1 -1
  170. package/dist/version.js +1 -1
  171. package/package.json +2 -2
  172. /package/dist/{shards/activate-tenantid.test.d.ts → actions/resolveLabel.test.d.ts} +0 -0
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Confirmation modal for project deletion. Mirrors ConfirmDialog's
4
+ * shape (title + body + Cancel/Confirm) and adds a single checkbox
5
+ * that opts in to also wiping the on-disk docs directory.
6
+ *
7
+ * Mounted via modalManager.open(); `close` is injected by the modal
8
+ * manager. Cancel is the default-focused button so a stray Enter
9
+ * does not trigger the destructive path.
10
+ */
11
+
12
+ let {
13
+ projectName,
14
+ projectId,
15
+ onConfirm,
16
+ onCancel,
17
+ close,
18
+ }: {
19
+ projectName: string;
20
+ projectId: string;
21
+ onConfirm: (opts: { wipeData: boolean }) => void | Promise<void>;
22
+ onCancel?: () => void;
23
+ close: () => void;
24
+ } = $props();
25
+
26
+ let cancelBtn: HTMLButtonElement | undefined = $state();
27
+ let busy = $state(false);
28
+ let wipeData = $state(false);
29
+
30
+ $effect(() => {
31
+ cancelBtn?.focus();
32
+ });
33
+
34
+ async function handleConfirm(): Promise<void> {
35
+ if (busy) return;
36
+ busy = true;
37
+ try {
38
+ await onConfirm({ wipeData });
39
+ } finally {
40
+ close();
41
+ }
42
+ }
43
+
44
+ function handleCancel(): void {
45
+ if (busy) return;
46
+ onCancel?.();
47
+ close();
48
+ }
49
+ </script>
50
+
51
+ <div class="delete-project-dialog">
52
+ <div class="delete-project-dialog__title">Delete project '{projectName}'?</div>
53
+ <div class="delete-project-dialog__body">
54
+ The project record will be removed and any members will lose access.
55
+ Documents already written under the project's namespace remain on disk
56
+ unless you tick the option below.
57
+ </div>
58
+ <label class="delete-project-dialog__opt">
59
+ <input type="checkbox" bind:checked={wipeData} disabled={busy} />
60
+ <span>
61
+ Also delete project files
62
+ <code class="delete-project-dialog__path">&lt;dataDir&gt;/docs/{projectId}/</code>
63
+ </span>
64
+ </label>
65
+ <div class="delete-project-dialog__actions">
66
+ <button
67
+ type="button"
68
+ class="delete-project-dialog__btn"
69
+ data-delete-project-cancel
70
+ bind:this={cancelBtn}
71
+ onclick={handleCancel}
72
+ disabled={busy}
73
+ >
74
+ Cancel
75
+ </button>
76
+ <button
77
+ type="button"
78
+ class="delete-project-dialog__btn delete-project-dialog__btn--danger"
79
+ data-delete-project-confirm
80
+ onclick={handleConfirm}
81
+ disabled={busy}
82
+ >
83
+ Delete
84
+ </button>
85
+ </div>
86
+ </div>
87
+
88
+ <style>
89
+ .delete-project-dialog {
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: 16px;
93
+ padding: 20px 24px;
94
+ min-width: 400px;
95
+ max-width: 520px;
96
+ }
97
+ .delete-project-dialog__title {
98
+ font-size: 16px;
99
+ font-weight: 600;
100
+ color: var(--shell-fg);
101
+ }
102
+ .delete-project-dialog__body {
103
+ font-size: 13px;
104
+ color: var(--shell-fg-muted, var(--shell-fg));
105
+ line-height: 1.5;
106
+ }
107
+ .delete-project-dialog__opt {
108
+ display: flex;
109
+ align-items: flex-start;
110
+ gap: 8px;
111
+ font-size: 13px;
112
+ color: var(--shell-fg);
113
+ cursor: pointer;
114
+ }
115
+ .delete-project-dialog__opt input { margin-top: 3px; }
116
+ .delete-project-dialog__path {
117
+ display: block;
118
+ font-family: var(--shell-font-mono, monospace);
119
+ font-size: 11px;
120
+ color: var(--shell-fg-muted);
121
+ background: var(--shell-bg-elevated);
122
+ padding: 2px 6px;
123
+ border-radius: var(--shell-radius-sm, 3px);
124
+ margin-top: 2px;
125
+ word-break: break-all;
126
+ }
127
+ .delete-project-dialog__actions {
128
+ display: flex;
129
+ justify-content: flex-end;
130
+ gap: 8px;
131
+ margin-top: 4px;
132
+ }
133
+ .delete-project-dialog__btn {
134
+ font-size: 13px;
135
+ padding: 6px 14px;
136
+ border-radius: var(--shell-radius-sm, 4px);
137
+ border: 1px solid var(--shell-border-strong);
138
+ background: transparent;
139
+ color: var(--shell-fg);
140
+ cursor: pointer;
141
+ }
142
+ .delete-project-dialog__btn:disabled {
143
+ opacity: 0.6;
144
+ cursor: not-allowed;
145
+ }
146
+ .delete-project-dialog__btn--danger {
147
+ color: var(--shell-error, #d32f2f);
148
+ border-color: var(--shell-error, #d32f2f);
149
+ }
150
+ </style>
@@ -0,0 +1,12 @@
1
+ type $$ComponentProps = {
2
+ projectName: string;
3
+ projectId: string;
4
+ onConfirm: (opts: {
5
+ wipeData: boolean;
6
+ }) => void | Promise<void>;
7
+ onCancel?: () => void;
8
+ close: () => void;
9
+ };
10
+ declare const DeleteProjectDialog: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type DeleteProjectDialog = ReturnType<typeof DeleteProjectDialog>;
12
+ export default DeleteProjectDialog;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, unmount, tick } from 'svelte';
3
+ import DeleteProjectDialog from './DeleteProjectDialog.svelte';
4
+ let host;
5
+ let cmp = null;
6
+ beforeEach(() => {
7
+ host = document.createElement('div');
8
+ document.body.appendChild(host);
9
+ });
10
+ afterEach(() => {
11
+ if (cmp) {
12
+ unmount(cmp);
13
+ cmp = null;
14
+ }
15
+ host.remove();
16
+ });
17
+ describe('DeleteProjectDialog', () => {
18
+ it('renders the project name in the title and the docs path in the checkbox label', async () => {
19
+ cmp = mount(DeleteProjectDialog, {
20
+ target: host,
21
+ props: {
22
+ projectName: 'Acme Website',
23
+ projectId: 'acme-website-abcd',
24
+ onConfirm: () => { },
25
+ onCancel: () => { },
26
+ close: () => { },
27
+ },
28
+ });
29
+ await tick();
30
+ expect(host.textContent).toContain("Delete project 'Acme Website'");
31
+ expect(host.textContent).toContain('docs/acme-website-abcd');
32
+ });
33
+ it('the checkbox starts unchecked', async () => {
34
+ cmp = mount(DeleteProjectDialog, {
35
+ target: host,
36
+ props: {
37
+ projectName: 'A', projectId: 'a-1234',
38
+ onConfirm: () => { }, close: () => { },
39
+ },
40
+ });
41
+ await tick();
42
+ const cb = host.querySelector('input[type="checkbox"]');
43
+ expect(cb).not.toBeNull();
44
+ expect(cb.checked).toBe(false);
45
+ });
46
+ it('Confirm fires onConfirm({ wipeData: false }) when the box is unchecked', async () => {
47
+ let received = null;
48
+ let closed = false;
49
+ cmp = mount(DeleteProjectDialog, {
50
+ target: host,
51
+ props: {
52
+ projectName: 'A', projectId: 'a-1234',
53
+ onConfirm: (o) => { received = o; },
54
+ close: () => { closed = true; },
55
+ },
56
+ });
57
+ await tick();
58
+ const confirm = host.querySelector('[data-delete-project-confirm]');
59
+ confirm.click();
60
+ await tick();
61
+ await new Promise((r) => setTimeout(r, 0));
62
+ expect(received).toEqual({ wipeData: false });
63
+ expect(closed).toBe(true);
64
+ });
65
+ it('Confirm fires onConfirm({ wipeData: true }) when the box is checked', async () => {
66
+ let received = null;
67
+ cmp = mount(DeleteProjectDialog, {
68
+ target: host,
69
+ props: {
70
+ projectName: 'A', projectId: 'a-1234',
71
+ onConfirm: (o) => { received = o; },
72
+ close: () => { },
73
+ },
74
+ });
75
+ await tick();
76
+ const cb = host.querySelector('input[type="checkbox"]');
77
+ cb.click();
78
+ await tick();
79
+ const confirm = host.querySelector('[data-delete-project-confirm]');
80
+ confirm.click();
81
+ await tick();
82
+ await new Promise((r) => setTimeout(r, 0));
83
+ expect(received).toEqual({ wipeData: true });
84
+ });
85
+ it('Cancel does NOT fire onConfirm and does call close', async () => {
86
+ let confirmFired = false;
87
+ let closed = false;
88
+ cmp = mount(DeleteProjectDialog, {
89
+ target: host,
90
+ props: {
91
+ projectName: 'A', projectId: 'a-1234',
92
+ onConfirm: () => { confirmFired = true; },
93
+ close: () => { closed = true; },
94
+ },
95
+ });
96
+ await tick();
97
+ const cancel = host.querySelector('[data-delete-project-cancel]');
98
+ cancel.click();
99
+ await tick();
100
+ expect(confirmFired).toBe(false);
101
+ expect(closed).toBe(true);
102
+ });
103
+ it('Confirm button is disabled while onConfirm is in flight', async () => {
104
+ let release = null;
105
+ cmp = mount(DeleteProjectDialog, {
106
+ target: host,
107
+ props: {
108
+ projectName: 'A', projectId: 'a-1234',
109
+ onConfirm: () => new Promise((res) => { release = res; }),
110
+ close: () => { },
111
+ },
112
+ });
113
+ await tick();
114
+ const confirm = host.querySelector('[data-delete-project-confirm]');
115
+ confirm.click();
116
+ await tick();
117
+ expect(confirm.disabled).toBe(true);
118
+ release();
119
+ });
120
+ });
@@ -0,0 +1,219 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Admin-only float view for creating, editing, or deleting a project.
4
+ *
5
+ * Members and app allowlist are picked from live data via UserPicker
6
+ * (over /api/admin/users) and AppPicker (over the client-side app
7
+ * registry). The current user is pre-selected on the create path so
8
+ * "include myself" works without looking up the underlying user id.
9
+ */
10
+
11
+ import { untrack } from 'svelte';
12
+ import { projectsApi, type ProjectRecord } from './projectsApi';
13
+ import { refreshProjects } from './projectsShard.svelte';
14
+ import { getUser } from '../auth/auth.svelte';
15
+ import AppPicker from '../primitives/widgets/AppPicker.svelte';
16
+ import UserPicker from '../primitives/widgets/UserPicker.svelte';
17
+ import { modalManager } from '../overlays/modal';
18
+ import DeleteProjectDialog from './DeleteProjectDialog.svelte';
19
+
20
+ interface Props {
21
+ project?: ProjectRecord | null;
22
+ onClose: () => void;
23
+ }
24
+
25
+ let { project = null, onClose }: Props = $props();
26
+
27
+ const isEdit = $derived(project !== null);
28
+ const me = $derived(getUser());
29
+
30
+ // Form fields snapshot the project on mount; the modal is recreated per
31
+ // open, so prop changes after mount aren't expected. untrack defers the
32
+ // reactive read so Svelte 5 doesn't emit state_referenced_locally.
33
+ let name = $state(untrack(() => project?.name ?? ''));
34
+ let description = $state(untrack(() => project?.description ?? ''));
35
+ let members = $state<string[]>(
36
+ untrack(() => {
37
+ if (project) return [...project.members];
38
+ const userId = getUser()?.id;
39
+ return userId ? [userId] : [];
40
+ }),
41
+ );
42
+ let appAllowlist = $state<string[]>(
43
+ untrack(() => (project ? [...project.appAllowlist] : [])),
44
+ );
45
+ let saving = $state(false);
46
+ let error = $state<string | null>(null);
47
+
48
+ async function save() {
49
+ if (!name.trim()) {
50
+ error = 'Name is required';
51
+ return;
52
+ }
53
+ saving = true;
54
+ error = null;
55
+ const payload = {
56
+ name: name.trim(),
57
+ description: description.trim() || undefined,
58
+ members,
59
+ appAllowlist,
60
+ };
61
+ try {
62
+ if (isEdit && project) {
63
+ await projectsApi.update(project.id, payload);
64
+ } else {
65
+ await projectsApi.create(payload);
66
+ }
67
+ await refreshProjects();
68
+ onClose();
69
+ } catch (e) {
70
+ error = (e as Error).message;
71
+ } finally {
72
+ saving = false;
73
+ }
74
+ }
75
+
76
+ function remove() {
77
+ if (!isEdit || !project) return;
78
+ const target = project;
79
+ modalManager.open(DeleteProjectDialog, {
80
+ projectName: target.name,
81
+ projectId: target.id,
82
+ onConfirm: async ({ wipeData }: { wipeData: boolean }) => {
83
+ saving = true;
84
+ error = null;
85
+ try {
86
+ await projectsApi.delete(target.id, { wipeData });
87
+ await refreshProjects();
88
+ onClose();
89
+ } catch (e) {
90
+ error = (e as Error).message;
91
+ } finally {
92
+ saving = false;
93
+ }
94
+ },
95
+ });
96
+ }
97
+ </script>
98
+
99
+ <div class="project-manage">
100
+ <div class="body">
101
+ <h2>{isEdit ? `Edit ${project!.name}` : 'Create Project'}</h2>
102
+
103
+ {#if isEdit}
104
+ <p class="project-id">ID: <code>{project!.id}</code></p>
105
+ {/if}
106
+
107
+ <label class="field">
108
+ <span>Name</span>
109
+ <input type="text" bind:value={name} placeholder="Acme Website" disabled={saving} />
110
+ </label>
111
+
112
+ <label class="field">
113
+ <span>Description</span>
114
+ <textarea bind:value={description} rows={2} disabled={saving}></textarea>
115
+ </label>
116
+
117
+ <label class="field">
118
+ <span>Members</span>
119
+ <UserPicker bind:value={members} disabled={saving} />
120
+ {#if me}
121
+ <span class="hint">
122
+ Your id: <code>{me.id}</code> ({me.username ?? me.displayName})
123
+ </span>
124
+ {/if}
125
+ </label>
126
+
127
+ <label class="field">
128
+ <span>App allowlist</span>
129
+ <AppPicker bind:value={appAllowlist} disabled={saving} />
130
+ </label>
131
+
132
+ {#if error}
133
+ <p class="error">{error}</p>
134
+ {/if}
135
+ </div>
136
+
137
+ <div class="actions">
138
+ <button type="button" class="primary" onclick={save} disabled={saving}>
139
+ {isEdit ? 'Save' : 'Create'}
140
+ </button>
141
+ <button type="button" onclick={onClose} disabled={saving}>Cancel</button>
142
+ {#if isEdit}
143
+ <button type="button" class="danger" onclick={remove} disabled={saving}>Delete</button>
144
+ {/if}
145
+ </div>
146
+ </div>
147
+
148
+ <style>
149
+ .project-manage {
150
+ position: absolute;
151
+ inset: 0;
152
+ display: flex;
153
+ flex-direction: column;
154
+ font: inherit;
155
+ color: var(--shell-fg);
156
+ background: var(--shell-bg);
157
+ }
158
+ .body {
159
+ flex: 1;
160
+ overflow-y: auto;
161
+ padding: 16px 16px 8px;
162
+ }
163
+ h2 { margin: 0 0 8px; font-size: 16px; }
164
+ .project-id { font-size: 12px; color: var(--shell-fg-muted); margin: 0 0 16px; }
165
+ .project-id code { font-family: var(--shell-font-mono, monospace); }
166
+ .field {
167
+ display: flex;
168
+ flex-direction: column;
169
+ gap: 4px;
170
+ margin-bottom: 12px;
171
+ font-size: 13px;
172
+ }
173
+ .field span { color: var(--shell-fg-muted); }
174
+ .hint { font-size: 11px; color: var(--shell-fg-muted); }
175
+ .hint code {
176
+ font-family: var(--shell-font-mono, monospace);
177
+ color: var(--shell-fg);
178
+ background: var(--shell-bg-elevated);
179
+ padding: 0 4px;
180
+ border-radius: var(--shell-radius-sm, 3px);
181
+ }
182
+ .field input,
183
+ .field textarea {
184
+ background: var(--shell-bg-elevated);
185
+ color: var(--shell-fg);
186
+ border: 1px solid var(--shell-border);
187
+ border-radius: var(--shell-radius-sm, 3px);
188
+ padding: 6px 8px;
189
+ font: inherit;
190
+ font-size: 13px;
191
+ }
192
+ .field textarea { resize: vertical; min-height: 60px; }
193
+ .error { color: var(--shell-error, #c33); font-size: 13px; margin: 0 0 8px; }
194
+ .actions {
195
+ display: flex;
196
+ gap: 8px;
197
+ padding: 12px 16px;
198
+ border-top: 1px solid var(--shell-border);
199
+ background: var(--shell-bg);
200
+ flex: 0 0 auto;
201
+ }
202
+ .actions button {
203
+ background: var(--shell-bg-elevated);
204
+ color: var(--shell-fg);
205
+ border: 1px solid var(--shell-border);
206
+ border-radius: var(--shell-radius-sm, 3px);
207
+ padding: 6px 14px;
208
+ font: inherit;
209
+ cursor: pointer;
210
+ }
211
+ .actions button:hover { border-color: var(--shell-accent); }
212
+ .actions button.primary {
213
+ background: var(--shell-accent);
214
+ color: #fff;
215
+ border-color: var(--shell-accent);
216
+ }
217
+ .actions button.danger { margin-left: auto; color: var(--shell-error, #c33); }
218
+ .actions button:disabled { opacity: 0.5; cursor: not-allowed; }
219
+ </style>
@@ -0,0 +1,8 @@
1
+ import { type ProjectRecord } from './projectsApi';
2
+ interface Props {
3
+ project?: ProjectRecord | null;
4
+ onClose: () => void;
5
+ }
6
+ declare const ProjectManage: import("svelte").Component<Props, {}, "">;
7
+ type ProjectManage = ReturnType<typeof ProjectManage>;
8
+ export default ProjectManage;
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Projects section for ShellHome.
4
+ *
5
+ * Renders the list of projects the current user is a member of as
6
+ * selectable cards. Selecting a project sets sessionState.activeProjectId
7
+ * (which then filters the apps grid via the appAllowlist) and binds any
8
+ * subsequently launched app's documents to the project scope.
9
+ */
10
+
11
+ import { projectsState, openProjectManage } from './projectsShard.svelte';
12
+ import { sessionState, setActiveProjectId } from '../projects/session-state.svelte';
13
+ import { isAdmin } from '../auth/auth.svelte';
14
+
15
+ const visible = $derived(projectsState.projects.length > 0);
16
+ const activeId = $derived(sessionState.activeProjectId);
17
+ const elevated = $derived(isAdmin());
18
+
19
+ function selectProject(id: string) {
20
+ setActiveProjectId(activeId === id ? null : id);
21
+ }
22
+
23
+ function editProject(id: string, ev: MouseEvent) {
24
+ ev.stopPropagation();
25
+ const project = projectsState.projects.find((p) => p.id === id) ?? null;
26
+ if (project) openProjectManage(project);
27
+ }
28
+ </script>
29
+
30
+ {#if visible}
31
+ <section class="projects-section">
32
+ <h2 class="projects-heading">Projects</h2>
33
+ <div class="projects-grid">
34
+ {#each projectsState.projects as project (project.id)}
35
+ <div class="project-card-wrap">
36
+ <button
37
+ type="button"
38
+ class="project-card"
39
+ class:active={activeId === project.id}
40
+ onclick={() => selectProject(project.id)}
41
+ title={project.description ?? `${project.members.length} member${project.members.length === 1 ? '' : 's'}`}
42
+ >
43
+ <span class="project-name">{project.name}</span>
44
+ <span class="project-meta">{project.members.length} member{project.members.length === 1 ? '' : 's'}</span>
45
+ </button>
46
+ {#if elevated}
47
+ <button
48
+ type="button"
49
+ class="project-card-edit"
50
+ onclick={(ev) => editProject(project.id, ev)}
51
+ aria-label="Edit project"
52
+ title="Edit project"
53
+ >⚙</button>
54
+ {/if}
55
+ </div>
56
+ {/each}
57
+ </div>
58
+ </section>
59
+ {/if}
60
+
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(--shell-fg-subtle);
73
+ margin: 0 0 12px;
74
+ }
75
+ .projects-grid {
76
+ display: grid;
77
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
78
+ gap: 10px;
79
+ }
80
+ .project-card-wrap { position: relative; }
81
+ .project-card {
82
+ display: flex;
83
+ flex-direction: column;
84
+ align-items: flex-start;
85
+ gap: 4px;
86
+ padding: 12px 14px;
87
+ background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
88
+ border: 1px solid var(--shell-border);
89
+ border-radius: var(--shell-radius-md);
90
+ cursor: pointer;
91
+ text-align: left;
92
+ color: inherit;
93
+ font: inherit;
94
+ transition: border-color 120ms ease, transform 120ms ease;
95
+ width: 100%;
96
+ }
97
+ .project-card-edit {
98
+ position: absolute;
99
+ top: 6px;
100
+ right: 6px;
101
+ background: transparent;
102
+ border: 0;
103
+ color: var(--shell-fg-muted);
104
+ cursor: pointer;
105
+ padding: 2px 6px;
106
+ font-size: 14px;
107
+ border-radius: var(--shell-radius-sm, 3px);
108
+ }
109
+ .project-card-edit:hover { color: var(--shell-fg); background: var(--shell-bg-elevated); }
110
+ .project-card:hover {
111
+ border-color: var(--shell-accent);
112
+ transform: translateY(-1px);
113
+ }
114
+ .project-card.active {
115
+ border-color: var(--shell-accent);
116
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--shell-accent) 40%, transparent);
117
+ }
118
+ .project-name { font-weight: 600; font-size: 13px; }
119
+ .project-meta { font-size: 11px; color: var(--shell-fg-muted); }
120
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const ProjectsSection: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ProjectsSection = ReturnType<typeof ProjectsSection>;
3
+ export default ProjectsSection;
@@ -0,0 +1,4 @@
1
+ export { projectsShard, projectsState, refreshProjects, openProjectManage, } from './projectsShard.svelte';
2
+ export { projectsApi, type ProjectRecord } from './projectsApi';
3
+ export { default as ProjectsSection } from './ProjectsSection.svelte';
4
+ export { default as ProjectManage } from './ProjectManage.svelte';
@@ -0,0 +1,4 @@
1
+ export { projectsShard, projectsState, refreshProjects, openProjectManage, } from './projectsShard.svelte';
2
+ export { projectsApi } from './projectsApi';
3
+ export { default as ProjectsSection } from './ProjectsSection.svelte';
4
+ export { default as ProjectManage } from './ProjectManage.svelte';
@@ -0,0 +1,20 @@
1
+ export interface ProjectRecord {
2
+ id: string;
3
+ name: string;
4
+ description?: string;
5
+ members: string[];
6
+ appAllowlist: string[];
7
+ createdBy: string;
8
+ createdAt: number;
9
+ updatedAt: number;
10
+ }
11
+ export declare const projectsApi: {
12
+ list(): Promise<ProjectRecord[]>;
13
+ listAll(): Promise<ProjectRecord[]>;
14
+ get(id: string): Promise<ProjectRecord>;
15
+ create(input: Pick<ProjectRecord, "name" | "description" | "members" | "appAllowlist">): Promise<ProjectRecord>;
16
+ update(id: string, patch: Partial<Pick<ProjectRecord, "name" | "description" | "members" | "appAllowlist">>): Promise<ProjectRecord>;
17
+ delete(id: string, opts?: {
18
+ wipeData?: boolean;
19
+ }): Promise<void>;
20
+ };