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
@@ -2,24 +2,32 @@
2
2
  * Document zone configuration — module-level singletons.
3
3
  *
4
4
  * Mirrors the __setBackend pattern in state/zones.svelte.ts. The host
5
- * calls __setTenantId and __setDocumentBackend before bootstrap() to
6
- * configure multi-tenancy and swap backends (e.g. Tauri FS).
5
+ * calls __setActiveScope and __setDocumentBackend before bootstrap() to
6
+ * configure multi-scope routing and swap backends (e.g. Tauri FS).
7
7
  *
8
- * Defaults: tenantId='local' (single-user self-hosted), backend=IndexedDB.
8
+ * Defaults: scopeId='local' (single-user self-hosted), backend=IndexedDB.
9
9
  */
10
10
  import { IndexedDBDocumentBackend } from './backends';
11
- const DEFAULT_TENANT = 'local';
12
- let tenantId = DEFAULT_TENANT;
11
+ const DEFAULT_SCOPE = 'local';
12
+ let scopeId = DEFAULT_SCOPE;
13
13
  let backend = new IndexedDBDocumentBackend();
14
+ export function getActiveScopeId() {
15
+ return scopeId;
16
+ }
17
+ /** @deprecated use getActiveScopeId — kept until callers migrate. */
14
18
  export function getTenantId() {
15
- return tenantId;
19
+ return scopeId;
16
20
  }
17
21
  export function getDocumentBackend() {
18
22
  return backend;
19
23
  }
20
- /** Host-only. Set the tenant id before bootstrap(). */
24
+ /** Host-only. Set the active scope id before bootstrap(). */
25
+ export function __setActiveScope(id) {
26
+ scopeId = id;
27
+ }
28
+ /** @deprecated use __setActiveScope — kept until callers migrate. */
21
29
  export function __setTenantId(id) {
22
- tenantId = id;
30
+ __setActiveScope(id);
23
31
  }
24
32
  /** Host-only. Swap the document backend before bootstrap(). */
25
33
  export function __setDocumentBackend(b) {
@@ -3,6 +3,6 @@ export { MemoryDocumentBackend, IndexedDBDocumentBackend } from './backends';
3
3
  export { HttpDocumentBackend } from './http-backend';
4
4
  export { createDocumentHandle } from './handle';
5
5
  export { documentChanges } from './notifications';
6
- export { getTenantId, getDocumentBackend, __setTenantId, __setDocumentBackend, } from './config';
6
+ export { getActiveScopeId, getTenantId, getDocumentBackend, __setActiveScope, __setTenantId, __setDocumentBackend, } from './config';
7
7
  export type { SyncPolicy, SyncPolicyRule, DocStatus, ConflictFile, ConflictBranch, } from './sync-types';
8
8
  export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY, } from './sync-types';
@@ -5,5 +5,5 @@ export { MemoryDocumentBackend, IndexedDBDocumentBackend } from './backends';
5
5
  export { HttpDocumentBackend } from './http-backend';
6
6
  export { createDocumentHandle } from './handle';
7
7
  export { documentChanges } from './notifications';
8
- export { getTenantId, getDocumentBackend, __setTenantId, __setDocumentBackend, } from './config';
8
+ export { getActiveScopeId, getTenantId, getDocumentBackend, __setActiveScope, __setTenantId, __setDocumentBackend, } from './config';
9
9
  export { PERMISSION_SYNC_PEER, PERMISSION_SYNC_POLICY, } from './sync-types';
@@ -1,6 +1,6 @@
1
1
  export { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner } from './host';
2
2
  export type { BootstrapConfig } from './host';
3
- export { __setTenantId, __setDocumentBackend } from './host';
3
+ export { __setActiveScope, __setTenantId, __setDocumentBackend } from './host';
4
4
  export type { Backend } from './state/types';
5
5
  export type { DocumentBackend } from './documents/types';
6
6
  export { HttpDocumentBackend } from './documents/http-backend';
@@ -6,7 +6,7 @@
6
6
  * should touch this path. Shards and apps must not import from here.
7
7
  */
8
8
  export { registerShard, registerApp, bootstrap, __setBackend, setLocalOwner } from './host';
9
- export { __setTenantId, __setDocumentBackend } from './host';
9
+ export { __setActiveScope, __setTenantId, __setDocumentBackend } from './host';
10
10
  export { HttpDocumentBackend } from './documents/http-backend';
11
11
  export { __setEnvServerUrl } from './env/index';
12
12
  // Install API (host-only).
package/dist/host.d.ts CHANGED
@@ -4,7 +4,7 @@ import { __setBackend } from './state/zones.svelte';
4
4
  import { setLocalOwner } from './auth/index';
5
5
  export { __setBackend };
6
6
  export { setLocalOwner };
7
- export { __setTenantId, __setDocumentBackend } from './documents/config';
7
+ export { __setActiveScope, __setTenantId, __setDocumentBackend } from './documents/config';
8
8
  export declare function registerShard(shard: Parameters<typeof registerShardInternal>[0]): void;
9
9
  export { registerApp };
10
10
  export interface BootstrapConfig {
package/dist/host.js CHANGED
@@ -22,6 +22,8 @@ import { launchApp, readLastApp, clearLastApp } from './apps/lifecycle';
22
22
  import { sh3coreShard } from './sh3core-shard/sh3coreShard.svelte';
23
23
  import { shellShard } from './shell-shard/shellShard.svelte';
24
24
  import { storeShard } from './app/store/storeShard.svelte';
25
+ import { projectsShard } from './projects-shard/projectsShard.svelte';
26
+ import { appearanceShard } from './app-appearance';
25
27
  import { __setBackend, backends } from './state/zones.svelte';
26
28
  import { loadInstalledPackages } from './registry/installer';
27
29
  import { setLocalOwner } from './auth/index';
@@ -34,7 +36,7 @@ import { installWebEmitter } from './navigation/platform-web';
34
36
  import { returnToHome } from './apps/lifecycle';
35
37
  export { __setBackend };
36
38
  export { setLocalOwner };
37
- export { __setTenantId, __setDocumentBackend } from './documents/config';
39
+ export { __setActiveScope, __setTenantId, __setDocumentBackend } from './documents/config';
38
40
  export function registerShard(shard) {
39
41
  registerShardInternal(shard);
40
42
  }
@@ -58,10 +60,15 @@ export async function bootstrap(config) {
58
60
  // already in place when shards activate.
59
61
  if (typeof globalThis.localStorage !== 'undefined') {
60
62
  runShellRenameMigration(createWorkspaceZoneAdapter(), globalThis.localStorage);
63
+ // Per ADR-002 amendment, app workspace state is keyed by (scopeId, appId).
64
+ // Rewrite legacy unkeyed entries to the personal scope namespace.
65
+ const { migrateLegacyWorkspaceKeys } = await import('./apps/workspace-rekey');
66
+ const { getActiveScopeId } = await import('./documents/config');
67
+ migrateLegacyWorkspaceKeys(getActiveScopeId());
61
68
  }
62
69
  const exShards = new Set(config === null || config === void 0 ? void 0 : config.excludeShards);
63
70
  // 1. Framework-owned shards
64
- const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard];
71
+ const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard, projectsShard, appearanceShard];
65
72
  for (const shard of frameworkShards) {
66
73
  if (!exShards.has(shard.manifest.id)) {
67
74
  registerShardInternal(shard);
@@ -6,6 +6,9 @@
6
6
  * `icon` accepts either a sprite symbol id ("save") or a direct URL
7
7
  * ending in .svg / containing a slash ("./foo.svg"); the URL form
8
8
  * bypasses the active sprite entirely.
9
+ *
10
+ * `loading` flips the button into a pending state: spinner takes the
11
+ * icon slot, label stays put, button is disabled, aria-busy is set.
9
12
  */
10
13
 
11
14
  import type { Snippet } from 'svelte';
@@ -17,6 +20,7 @@
17
20
  icon,
18
21
  sprite,
19
22
  disabled = false,
23
+ loading,
20
24
  type = 'button',
21
25
  title,
22
26
  ariaLabel,
@@ -29,10 +33,12 @@
29
33
  /** Override the sprite sheet URL for this button only. */
30
34
  sprite?: string;
31
35
  disabled?: boolean;
36
+ /** Controlled pending state. When true, spinner + disabled + aria-busy. */
37
+ loading?: boolean;
32
38
  type?: 'button' | 'submit' | 'reset';
33
39
  title?: string;
34
40
  ariaLabel?: string;
35
- onclick?: (event: MouseEvent) => void;
41
+ onclick?: (event: MouseEvent) => void | Promise<unknown>;
36
42
  children?: Snippet;
37
43
  } = $props();
38
44
 
@@ -49,19 +55,48 @@
49
55
  return `${base}#${icon}`;
50
56
  });
51
57
 
58
+ let autoPending = $state(false);
59
+ const pending = $derived(loading ?? autoPending);
52
60
  const iconOnly = $derived(variant === 'icon' || (!!icon && !children));
61
+
62
+ async function handleClick(event: MouseEvent) {
63
+ if (!onclick) return;
64
+ const result = onclick(event);
65
+ if (result && typeof (result as PromiseLike<unknown>).then === 'function') {
66
+ autoPending = true;
67
+ try {
68
+ await result;
69
+ } finally {
70
+ autoPending = false;
71
+ }
72
+ }
73
+ }
53
74
  </script>
54
75
 
55
76
  <button
56
77
  {type}
57
78
  class="sh3-btn sh3-btn--{variant}"
58
79
  class:sh3-btn--icon-only={iconOnly}
59
- {disabled}
80
+ disabled={disabled || pending}
81
+ aria-busy={pending || undefined}
60
82
  {title}
61
83
  aria-label={ariaLabel ?? (iconOnly ? title : undefined)}
62
- {onclick}
84
+ onclick={handleClick}
63
85
  >
64
- {#if iconHref}
86
+ {#if pending}
87
+ <svg class="sh3-btn__spinner" aria-hidden="true" viewBox="0 0 16 16">
88
+ <circle
89
+ cx="8"
90
+ cy="8"
91
+ r="6"
92
+ fill="none"
93
+ stroke="currentColor"
94
+ stroke-width="2"
95
+ stroke-linecap="round"
96
+ stroke-dasharray="30 10"
97
+ />
98
+ </svg>
99
+ {:else if iconHref}
65
100
  <svg class="sh3-btn__icon" aria-hidden="true">
66
101
  <use href={iconHref} />
67
102
  </svg>
@@ -141,4 +176,15 @@
141
176
  display: inline-flex;
142
177
  white-space: nowrap;
143
178
  }
179
+
180
+ .sh3-btn__spinner {
181
+ width: 16px;
182
+ height: 16px;
183
+ flex-shrink: 0;
184
+ animation: sh3-btn-spin 0.8s linear infinite;
185
+ }
186
+
187
+ @keyframes sh3-btn-spin {
188
+ to { transform: rotate(360deg); }
189
+ }
144
190
  </style>
@@ -7,10 +7,12 @@ type $$ComponentProps = {
7
7
  /** Override the sprite sheet URL for this button only. */
8
8
  sprite?: string;
9
9
  disabled?: boolean;
10
+ /** Controlled pending state. When true, spinner + disabled + aria-busy. */
11
+ loading?: boolean;
10
12
  type?: 'button' | 'submit' | 'reset';
11
13
  title?: string;
12
14
  ariaLabel?: string;
13
- onclick?: (event: MouseEvent) => void;
15
+ onclick?: (event: MouseEvent) => void | Promise<unknown>;
14
16
  children?: Snippet;
15
17
  };
16
18
  declare const Button: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -0,0 +1,110 @@
1
+ <script lang="ts">
2
+ /*
3
+ * Collapsible — structural primitive for show/hide sections.
4
+ *
5
+ * Body is conditionally rendered (not just hidden) so heavy children
6
+ * don't stay live when collapsed. Defaults to closed. No collapse/expand
7
+ * animation in v1 — caret rotates 90° via CSS, but no transitions
8
+ * (waiting on the motion RFC).
9
+ *
10
+ * `header` snippet, when provided, replaces the `title` string.
11
+ */
12
+
13
+ import type { Snippet } from 'svelte';
14
+
15
+ let {
16
+ open = $bindable(false),
17
+ title,
18
+ header,
19
+ children,
20
+ onopenchange,
21
+ }: {
22
+ /** Bindable; defaults to false. */
23
+ open?: boolean;
24
+ /** Plain text header. Ignored if `header` snippet is provided. */
25
+ title?: string;
26
+ /** Snippet alternative to `title` for richer headers. */
27
+ header?: Snippet;
28
+ children: Snippet;
29
+ onopenchange?: (open: boolean) => void;
30
+ } = $props();
31
+
32
+ function toggle() {
33
+ open = !open;
34
+ onopenchange?.(open);
35
+ }
36
+ </script>
37
+
38
+ <div class="sh3-collapsible" class:sh3-collapsible--open={open}>
39
+ <button
40
+ class="sh3-collapsible__head"
41
+ type="button"
42
+ aria-expanded={open}
43
+ onclick={toggle}
44
+ >
45
+ <svg class="sh3-collapsible__caret" aria-hidden="true" viewBox="0 0 16 16">
46
+ <path
47
+ d="M5 3l6 5-6 5"
48
+ fill="none"
49
+ stroke="currentColor"
50
+ stroke-width="2"
51
+ stroke-linecap="round"
52
+ stroke-linejoin="round"
53
+ />
54
+ </svg>
55
+ {#if header}
56
+ {@render header()}
57
+ {:else if title}
58
+ <span class="sh3-collapsible__title">{title}</span>
59
+ {/if}
60
+ </button>
61
+ {#if open}
62
+ <div class="sh3-collapsible__body">{@render children()}</div>
63
+ {/if}
64
+ </div>
65
+
66
+ <style>
67
+ .sh3-collapsible {
68
+ border: 1px solid var(--shell-border);
69
+ border-radius: var(--shell-radius);
70
+ background: transparent;
71
+ }
72
+ .sh3-collapsible__head {
73
+ appearance: none;
74
+ width: 100%;
75
+ display: inline-flex;
76
+ align-items: center;
77
+ gap: var(--shell-pad-sm);
78
+ padding: var(--shell-pad-sm) 12px;
79
+ background: transparent;
80
+ color: var(--shell-fg);
81
+ border: none;
82
+ border-radius: inherit;
83
+ cursor: pointer;
84
+ font-family: inherit;
85
+ font-size: 0.875rem;
86
+ line-height: var(--shell-line);
87
+ text-align: left;
88
+ }
89
+ .sh3-collapsible__head:hover { background: var(--shell-bg-elevated); }
90
+ .sh3-collapsible__head:focus-visible {
91
+ box-shadow: var(--shell-focus-ring);
92
+ outline: none;
93
+ }
94
+ .sh3-collapsible__caret {
95
+ width: 12px;
96
+ height: 12px;
97
+ flex-shrink: 0;
98
+ color: var(--shell-fg-muted);
99
+ }
100
+ .sh3-collapsible--open .sh3-collapsible__caret {
101
+ transform: rotate(90deg);
102
+ }
103
+ .sh3-collapsible__title {
104
+ flex: 1;
105
+ }
106
+ .sh3-collapsible__body {
107
+ padding: var(--shell-pad-sm) 12px;
108
+ border-top: 1px solid var(--shell-border);
109
+ }
110
+ </style>
@@ -0,0 +1,14 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Bindable; defaults to false. */
4
+ open?: boolean;
5
+ /** Plain text header. Ignored if `header` snippet is provided. */
6
+ title?: string;
7
+ /** Snippet alternative to `title` for richer headers. */
8
+ header?: Snippet;
9
+ children: Snippet;
10
+ onopenchange?: (open: boolean) => void;
11
+ };
12
+ declare const Collapsible: import("svelte").Component<$$ComponentProps, {}, "open">;
13
+ type Collapsible = ReturnType<typeof Collapsible>;
14
+ export default Collapsible;
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import type { CommitOnlyEvents } from './_contract';
3
+ import PickerList from './PickerList.svelte';
4
+ import type { PickerItem } from './PickerList';
5
+ import { listRegisteredApps } from '../../apps/registry.svelte';
6
+
7
+ /** Apps the framework ships and that bypass the project allowlist server-side. */
8
+ const FRAMEWORK_APP_IDS = new Set(['sh3-admin-app', 'sh3-store-app']);
9
+
10
+ let {
11
+ value = $bindable<string[]>([]),
12
+ onchange,
13
+ disabled = false,
14
+ size = 'md',
15
+ }: {
16
+ value?: string[];
17
+ disabled?: boolean;
18
+ size?: 'sm' | 'md';
19
+ } & CommitOnlyEvents<string[]> = $props();
20
+
21
+ const items = $derived<PickerItem[]>(
22
+ listRegisteredApps()
23
+ .filter((m) => !FRAMEWORK_APP_IDS.has(m.id) && !m.admin)
24
+ .map((m) => ({ id: m.id, label: m.label, sublabel: m.id }))
25
+ .sort((a, b) => a.label.localeCompare(b.label)),
26
+ );
27
+
28
+ function handleChange(next: string[]) {
29
+ value = next;
30
+ onchange?.(next);
31
+ }
32
+ </script>
33
+
34
+ <PickerList
35
+ {items}
36
+ {value}
37
+ onchange={handleChange}
38
+ {disabled}
39
+ {size}
40
+ emptyText="No apps installed."
41
+ />
@@ -0,0 +1,9 @@
1
+ import type { CommitOnlyEvents } from './_contract';
2
+ type $$ComponentProps = {
3
+ value?: string[];
4
+ disabled?: boolean;
5
+ size?: 'sm' | 'md';
6
+ } & CommitOnlyEvents<string[]>;
7
+ declare const AppPicker: import("svelte").Component<$$ComponentProps, {}, "value">;
8
+ type AppPicker = ReturnType<typeof AppPicker>;
9
+ export default AppPicker;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import AppPicker from './AppPicker.svelte';
4
+ vi.mock('../../apps/registry.svelte', () => ({
5
+ listRegisteredApps: () => [
6
+ { id: 'sh3-store-app', label: 'Store' },
7
+ { id: 'app.alpha', label: 'Alpha' },
8
+ { id: 'app.bravo', label: 'Bravo', admin: true },
9
+ { id: 'app.charlie', label: 'Charlie' },
10
+ ],
11
+ }));
12
+ describe('AppPicker event contract', () => {
13
+ it('renders only non-framework, non-admin apps', () => {
14
+ const { container } = render(AppPicker, { props: { value: [] } });
15
+ const labels = Array.from(container.querySelectorAll('.sh3-picker__row-label')).map((el) => el.textContent);
16
+ expect(labels).toEqual(['Alpha', 'Charlie']);
17
+ });
18
+ it('fires onchange when an app row is toggled', async () => {
19
+ const onchange = vi.fn();
20
+ const { container } = render(AppPicker, { props: { value: [], onchange } });
21
+ const checkboxes = container.querySelectorAll('input[type=checkbox]');
22
+ await fireEvent.click(checkboxes[0]);
23
+ expect(onchange).toHaveBeenCalledTimes(1);
24
+ expect(onchange).toHaveBeenCalledWith(['app.alpha']);
25
+ });
26
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mount, unmount, tick } from 'svelte';
3
+ import AppPicker from './AppPicker.svelte';
4
+ import { registerApp, __resetAppRegistryForTest } from '../../apps/registry.svelte';
5
+ let host;
6
+ let cmp = null;
7
+ function makeApp(id, label, opts = {}) {
8
+ return {
9
+ manifest: Object.assign({ id, label, version: '0.0.0', requiredShards: [], layoutVersion: 1 }, (opts.admin ? { admin: true } : {})),
10
+ initialLayout: { type: 'leaf', viewId: 'x:v' },
11
+ };
12
+ }
13
+ beforeEach(() => {
14
+ host = document.createElement('div');
15
+ document.body.appendChild(host);
16
+ __resetAppRegistryForTest();
17
+ });
18
+ afterEach(() => {
19
+ if (cmp) {
20
+ unmount(cmp);
21
+ cmp = null;
22
+ }
23
+ host.remove();
24
+ __resetAppRegistryForTest();
25
+ });
26
+ describe('AppPicker', () => {
27
+ it('renders one row per non-framework, non-admin app', async () => {
28
+ registerApp(makeApp('notes', 'Notes'));
29
+ registerApp(makeApp('files', 'Files'));
30
+ registerApp(makeApp('sh3-admin-app', 'Admin')); // framework — excluded
31
+ registerApp(makeApp('sh3-store-app', 'Store')); // framework — excluded
32
+ registerApp(makeApp('shadow', 'Shadow', { admin: true })); // admin flag — excluded
33
+ cmp = mount(AppPicker, { target: host, props: { value: [] } });
34
+ await tick();
35
+ const rows = host.querySelectorAll('.sh3-picker__row');
36
+ expect(rows.length).toBe(2);
37
+ const labels = Array.from(rows).map((r) => { var _a; return (_a = r.querySelector('.sh3-picker__row-label')) === null || _a === void 0 ? void 0 : _a.textContent; }).sort();
38
+ expect(labels).toEqual(['Files', 'Notes']);
39
+ });
40
+ it('passes selected ids through as checked rows', async () => {
41
+ registerApp(makeApp('notes', 'Notes'));
42
+ registerApp(makeApp('files', 'Files'));
43
+ cmp = mount(AppPicker, { target: host, props: { value: ['files'] } });
44
+ await tick();
45
+ const checks = host.querySelectorAll('input[type="checkbox"]');
46
+ const byLabel = (text) => Array.from(checks).find((c) => { var _a, _b; return (_b = (_a = c.closest('.sh3-picker__row')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.includes(text); });
47
+ expect(byLabel('Files').checked).toBe(true);
48
+ expect(byLabel('Notes').checked).toBe(false);
49
+ });
50
+ it('fires onchange with the new array on row click', async () => {
51
+ registerApp(makeApp('notes', 'Notes'));
52
+ registerApp(makeApp('files', 'Files'));
53
+ let received = null;
54
+ cmp = mount(AppPicker, {
55
+ target: host,
56
+ props: {
57
+ value: [],
58
+ onchange: (next) => { received = next; },
59
+ },
60
+ });
61
+ await tick();
62
+ const filesRow = Array.from(host.querySelectorAll('.sh3-picker__row'))
63
+ .find((r) => { var _a; return (_a = r.textContent) === null || _a === void 0 ? void 0 : _a.includes('Files'); });
64
+ filesRow.querySelector('input[type="checkbox"]').click();
65
+ await tick();
66
+ expect(received).toEqual(['files']);
67
+ });
68
+ it('shows emptyText when there are no installable apps', async () => {
69
+ registerApp(makeApp('sh3-admin-app', 'Admin')); // framework — excluded
70
+ cmp = mount(AppPicker, { target: host, props: { value: [] } });
71
+ await tick();
72
+ expect(host.textContent).toMatch(/No apps installed\./);
73
+ });
74
+ });
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { CommitOnlyEvents } from './_contract';
2
3
  import { shell } from '../../shellRuntime.svelte';
3
4
 
4
5
  let {
@@ -6,19 +7,23 @@
6
7
  label,
7
8
  disabled = false,
8
9
  size = 'md',
10
+ onchange,
9
11
  }: {
10
12
  value?: string;
11
13
  label?: string;
12
14
  disabled?: boolean;
13
15
  size?: 'sm' | 'md';
14
- } = $props();
16
+ } & CommitOnlyEvents<string> = $props();
15
17
 
16
18
  let trigger: HTMLButtonElement | undefined;
17
19
 
18
20
  async function open() {
19
21
  if (disabled) return;
20
22
  const result = await shell.color.pick({ initial: value, anchor: trigger });
21
- if (result !== null && result !== undefined) value = result;
23
+ if (result !== null && result !== undefined) {
24
+ value = result;
25
+ onchange?.(result);
26
+ }
22
27
  }
23
28
  </script>
24
29
 
@@ -1,9 +1,10 @@
1
+ import type { CommitOnlyEvents } from './_contract';
1
2
  type $$ComponentProps = {
2
3
  value?: string;
3
4
  label?: string;
4
5
  disabled?: boolean;
5
6
  size?: 'sm' | 'md';
6
- };
7
+ } & CommitOnlyEvents<string>;
7
8
  declare const ColorSwatch: import("svelte").Component<$$ComponentProps, {}, "value">;
8
9
  type ColorSwatch = ReturnType<typeof ColorSwatch>;
9
10
  export default ColorSwatch;
@@ -0,0 +1,31 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import ColorSwatch from './ColorSwatch.svelte';
4
+ vi.mock('../../shellRuntime.svelte', () => ({
5
+ shell: {
6
+ color: {
7
+ pick: vi.fn(async () => '#abcdef'),
8
+ },
9
+ },
10
+ }));
11
+ describe('ColorSwatch event contract', () => {
12
+ it('fires onchange with new hex when picker resolves', async () => {
13
+ const onchange = vi.fn();
14
+ const { container } = render(ColorSwatch, { props: { value: '#000000', onchange } });
15
+ const trigger = container.querySelector('.sh3-swatch__btn');
16
+ await fireEvent.click(trigger);
17
+ await new Promise((r) => setTimeout(r, 0));
18
+ expect(onchange).toHaveBeenCalledTimes(1);
19
+ expect(onchange).toHaveBeenCalledWith('#abcdef');
20
+ });
21
+ it('does not fire onchange when picker is cancelled (returns null)', async () => {
22
+ const { shell } = await import('../../shellRuntime.svelte');
23
+ shell.color.pick.mockResolvedValueOnce(null);
24
+ const onchange = vi.fn();
25
+ const { container } = render(ColorSwatch, { props: { value: '#000000', onchange } });
26
+ const trigger = container.querySelector('.sh3-swatch__btn');
27
+ await fireEvent.click(trigger);
28
+ await new Promise((r) => setTimeout(r, 0));
29
+ expect(onchange).not.toHaveBeenCalled();
30
+ });
31
+ });
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import type { LiveInputEvents } from './_contract';
3
4
 
4
5
  let {
5
6
  value = $bindable(''),
@@ -15,6 +16,7 @@
15
16
  size = 'md',
16
17
  required = false,
17
18
  autocomplete,
19
+ oninput,
18
20
  onchange,
19
21
  }: {
20
22
  value?: string;
@@ -30,8 +32,7 @@
30
32
  size?: 'sm' | 'md';
31
33
  required?: boolean;
32
34
  autocomplete?: AutoFill;
33
- onchange?: (next: string) => void;
34
- } = $props();
35
+ } & LiveInputEvents<string> = $props();
35
36
 
36
37
  const showError = $derived(invalid && !!error);
37
38
  const helperText = $derived(showError ? error : helper);
@@ -50,6 +51,7 @@
50
51
  {autocomplete}
51
52
  aria-invalid={invalid || undefined}
52
53
  bind:value
54
+ oninput={() => oninput?.(value)}
53
55
  onblur={() => onchange?.(value)}
54
56
  />
55
57
  {#if suffix}<span class="sh3-field__affix">{@render suffix()}</span>{/if}
@@ -1,4 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { LiveInputEvents } from './_contract';
2
3
  type $$ComponentProps = {
3
4
  value?: string;
4
5
  type?: 'text' | 'email' | 'password' | 'search' | 'url' | 'tel';
@@ -13,8 +14,7 @@ type $$ComponentProps = {
13
14
  size?: 'sm' | 'md';
14
15
  required?: boolean;
15
16
  autocomplete?: AutoFill;
16
- onchange?: (next: string) => void;
17
- };
17
+ } & LiveInputEvents<string>;
18
18
  declare const Field: import("svelte").Component<$$ComponentProps, {}, "value">;
19
19
  type Field = ReturnType<typeof Field>;
20
20
  export default Field;
@@ -0,0 +1 @@
1
+ export {};