sh3-core 0.17.0 → 0.19.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 (154) hide show
  1. package/dist/Sh3.svelte +107 -39
  2. package/dist/__screenshots__/handheld.browser.test.ts/handheld-viewport-flip-e2e-viewport-override-flips-chrome-and-body-branches-1.png +0 -0
  3. package/dist/actions/CommandPalette.svelte +1 -2
  4. package/dist/actions/listActionsFromEntries.test.js +29 -0
  5. package/dist/actions/listActive.js +2 -0
  6. package/dist/actions/listeners.js +16 -1
  7. package/dist/actions/programmatic-dispatch.svelte.test.js +9 -2
  8. package/dist/actions/types.d.ts +8 -0
  9. package/dist/api.d.ts +8 -1
  10. package/dist/app/store/storeShard.svelte.js +1 -21
  11. package/dist/app/store/version.d.ts +11 -0
  12. package/dist/app/store/version.js +39 -0
  13. package/dist/app/store/version.test.d.ts +1 -0
  14. package/dist/app/store/version.test.js +44 -0
  15. package/dist/apps/lifecycle.d.ts +6 -0
  16. package/dist/apps/lifecycle.js +5 -2
  17. package/dist/apps/lifecycle.test.js +30 -0
  18. package/dist/apps/types.d.ts +12 -0
  19. package/dist/assets/iconIds.generated.d.ts +1 -1
  20. package/dist/assets/iconIds.generated.js +5 -0
  21. package/dist/assets/icons.svg +31 -0
  22. package/dist/auth/auth.svelte.js +18 -8
  23. package/dist/auth/types.d.ts +6 -0
  24. package/dist/chrome/CompactChrome.svelte +130 -0
  25. package/dist/chrome/CompactChrome.svelte.d.ts +3 -0
  26. package/dist/chrome/CompactChrome.svelte.test.d.ts +1 -0
  27. package/dist/chrome/CompactChrome.svelte.test.js +174 -0
  28. package/dist/chrome/MenuSheet.svelte +224 -0
  29. package/dist/chrome/MenuSheet.svelte.d.ts +7 -0
  30. package/dist/chrome/MenuSheet.svelte.test.d.ts +1 -0
  31. package/dist/chrome/MenuSheet.svelte.test.js +46 -0
  32. package/dist/createShell.d.ts +9 -0
  33. package/dist/createShell.js +20 -7
  34. package/dist/createShell.remoteAuth.test.d.ts +1 -0
  35. package/dist/createShell.remoteAuth.test.js +71 -0
  36. package/dist/documents/http-backend.js +12 -11
  37. package/dist/env/client.js +11 -5
  38. package/dist/files/types.d.ts +106 -0
  39. package/dist/files/types.js +1 -0
  40. package/dist/gestures/gestureRegistry.d.ts +6 -0
  41. package/dist/gestures/gestureRegistry.js +190 -0
  42. package/dist/gestures/gestureRegistry.test.d.ts +1 -0
  43. package/dist/gestures/gestureRegistry.test.js +119 -0
  44. package/dist/gestures/index.d.ts +6 -0
  45. package/dist/gestures/index.js +12 -0
  46. package/dist/gestures/pointerClaim.d.ts +7 -0
  47. package/dist/gestures/pointerClaim.js +36 -0
  48. package/dist/gestures/pointerClaim.test.d.ts +1 -0
  49. package/dist/gestures/pointerClaim.test.js +64 -0
  50. package/dist/gestures/types.d.ts +83 -0
  51. package/dist/gestures/types.js +1 -0
  52. package/dist/handheld.browser.test.d.ts +1 -0
  53. package/dist/handheld.browser.test.js +90 -0
  54. package/dist/host-entry.d.ts +1 -0
  55. package/dist/host-entry.js +1 -0
  56. package/dist/layout/LayoutRenderer.browser.test.js +15 -3
  57. package/dist/layout/LayoutRenderer.svelte +27 -3
  58. package/dist/layout/LayoutRenderer.svelte.d.ts +4 -1
  59. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-3-splitter-drag-updates-split-sizes-when-the-splitter-handle-is-dragged-1.png +0 -0
  60. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-5-splitter-collapse-toggle-toggles-collapsed-i--on-double-click-1.png +0 -0
  61. package/dist/layout/__screenshots__/LayoutRenderer.browser.test.ts/LayoutRenderer-browser---E-6-fixed-slots-hides-the-collapse-widget-on-a-fixed-pane-but-keeps-it-on-panes-with-a-non-fixed-neighbor-1.png +0 -0
  62. package/dist/layout/compact/CarouselTabs.svelte +361 -0
  63. package/dist/layout/compact/CarouselTabs.svelte.d.ts +10 -0
  64. package/dist/layout/compact/CarouselTabs.svelte.test.d.ts +1 -0
  65. package/dist/layout/compact/CarouselTabs.svelte.test.js +300 -0
  66. package/dist/layout/compact/CompactRenderer.svelte +53 -0
  67. package/dist/layout/compact/CompactRenderer.svelte.d.ts +3 -0
  68. package/dist/layout/compact/CompactRenderer.svelte.test.d.ts +1 -0
  69. package/dist/layout/compact/CompactRenderer.svelte.test.js +125 -0
  70. package/dist/layout/compact/derive.d.ts +3 -0
  71. package/dist/layout/compact/derive.js +157 -0
  72. package/dist/layout/compact/derive.test.d.ts +1 -0
  73. package/dist/layout/compact/derive.test.js +197 -0
  74. package/dist/layout/compact/drawerStore.svelte.d.ts +21 -0
  75. package/dist/layout/compact/drawerStore.svelte.js +75 -0
  76. package/dist/layout/compact/drawerStore.svelte.test.d.ts +1 -0
  77. package/dist/layout/compact/drawerStore.svelte.test.js +43 -0
  78. package/dist/layout/compact/enrichCarousels.d.ts +8 -0
  79. package/dist/layout/compact/enrichCarousels.js +44 -0
  80. package/dist/layout/compact/enrichCarousels.test.d.ts +1 -0
  81. package/dist/layout/compact/enrichCarousels.test.js +88 -0
  82. package/dist/layout/compact/resolveRole.d.ts +6 -0
  83. package/dist/layout/compact/resolveRole.js +13 -0
  84. package/dist/layout/compact/resolveRole.test.d.ts +1 -0
  85. package/dist/layout/compact/resolveRole.test.js +18 -0
  86. package/dist/layout/compact/types.d.ts +30 -0
  87. package/dist/layout/compact/types.js +15 -0
  88. package/dist/layout/drag.svelte.js +13 -0
  89. package/dist/layout/presets.compactVariant.test.d.ts +1 -0
  90. package/dist/layout/presets.compactVariant.test.js +27 -0
  91. package/dist/layout/presets.d.ts +12 -0
  92. package/dist/layout/presets.js +16 -0
  93. package/dist/layout/store.drawers.svelte.test.d.ts +1 -0
  94. package/dist/layout/store.drawers.svelte.test.js +49 -0
  95. package/dist/layout/store.schemaVersion.test.d.ts +1 -0
  96. package/dist/layout/store.schemaVersion.test.js +35 -0
  97. package/dist/layout/store.svelte.js +52 -2
  98. package/dist/layout/types.d.ts +51 -1
  99. package/dist/layout/types.js +1 -1
  100. package/dist/layout/types.test.d.ts +1 -0
  101. package/dist/layout/types.test.js +26 -0
  102. package/dist/overlays/DrawerSurface.svelte +141 -0
  103. package/dist/overlays/DrawerSurface.svelte.d.ts +12 -0
  104. package/dist/overlays/DrawerSurface.svelte.test.d.ts +1 -0
  105. package/dist/overlays/DrawerSurface.svelte.test.js +67 -0
  106. package/dist/overlays/ModalFrame.svelte +3 -1
  107. package/dist/overlays/ModalFrame.svelte.d.ts +1 -0
  108. package/dist/overlays/OverlayRoots.svelte +12 -9
  109. package/dist/overlays/floatDismiss.js +5 -0
  110. package/dist/overlays/focusTrap.d.ts +11 -1
  111. package/dist/overlays/focusTrap.js +11 -9
  112. package/dist/overlays/modal.js +1 -0
  113. package/dist/overlays/popup.js +4 -0
  114. package/dist/overlays/types.d.ts +10 -1
  115. package/dist/primitives/Button.svelte +18 -0
  116. package/dist/primitives/Button.svelte.d.ts +6 -0
  117. package/dist/primitives/ResizableSplitter.svelte +71 -11
  118. package/dist/primitives/ResizableSplitter.svelte.d.ts +8 -0
  119. package/dist/primitives/ResizableSplitter.svelte.test.d.ts +1 -0
  120. package/dist/primitives/ResizableSplitter.svelte.test.js +74 -0
  121. package/dist/server-shard/types.d.ts +2 -1
  122. package/dist/sh3Api/headless.js +9 -1
  123. package/dist/sh3Api/headless.svelte.test.js +45 -1
  124. package/dist/sh3Runtime.svelte.d.ts +36 -0
  125. package/dist/sh3Runtime.svelte.js +33 -0
  126. package/dist/shards/activate.svelte.js +10 -0
  127. package/dist/shards/ctx-fetch.test.d.ts +1 -0
  128. package/dist/shards/ctx-fetch.test.js +66 -0
  129. package/dist/shards/types.d.ts +22 -1
  130. package/dist/tokens.css +3 -2
  131. package/dist/transport/apiFetch.d.ts +1 -0
  132. package/dist/transport/apiFetch.js +65 -0
  133. package/dist/transport/apiFetch.test.d.ts +1 -0
  134. package/dist/transport/apiFetch.test.js +37 -0
  135. package/dist/transport/authToken.d.ts +2 -0
  136. package/dist/transport/authToken.js +53 -0
  137. package/dist/transport/authToken.test.d.ts +1 -0
  138. package/dist/transport/authToken.test.js +33 -0
  139. package/dist/verbs/types.d.ts +5 -2
  140. package/dist/version.d.ts +1 -1
  141. package/dist/version.js +1 -1
  142. package/dist/viewport/classify.d.ts +8 -0
  143. package/dist/viewport/classify.js +20 -0
  144. package/dist/viewport/classify.test.d.ts +1 -0
  145. package/dist/viewport/classify.test.js +32 -0
  146. package/dist/viewport/store.browser.test.d.ts +1 -0
  147. package/dist/viewport/store.browser.test.js +33 -0
  148. package/dist/viewport/store.svelte.d.ts +9 -0
  149. package/dist/viewport/store.svelte.js +71 -0
  150. package/dist/viewport/store.svelte.test.d.ts +1 -0
  151. package/dist/viewport/store.svelte.test.js +54 -0
  152. package/dist/viewport/types.d.ts +9 -0
  153. package/dist/viewport/types.js +6 -0
  154. package/package.json +1 -1
@@ -0,0 +1,197 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { derive } from './derive';
3
+ describe('derive', () => {
4
+ describe('body partition', () => {
5
+ it('all-body tree → bodyRoot identical, all drawers null', () => {
6
+ const tree = {
7
+ type: 'split',
8
+ direction: 'horizontal',
9
+ sizes: [1, 1],
10
+ children: [
11
+ { type: 'slot', slotId: 'a', viewId: 'view:a', role: 'body' },
12
+ { type: 'slot', slotId: 'b', viewId: 'view:b', role: 'body' },
13
+ ],
14
+ };
15
+ const result = derive(tree);
16
+ expect(result.bodyRoot).toEqual(tree);
17
+ expect(result.drawers.left).toBeNull();
18
+ expect(result.drawers.right).toBeNull();
19
+ expect(result.drawers.top).toBeNull();
20
+ });
21
+ it('zero-body tree → bodyRoot is empty placeholder', () => {
22
+ var _a, _b;
23
+ const tree = {
24
+ type: 'split', direction: 'horizontal', sizes: [1, 1],
25
+ children: [
26
+ { type: 'slot', slotId: 'a', viewId: 'view:a', role: 'sidebar' },
27
+ { type: 'slot', slotId: 'b', viewId: 'view:b', role: 'inspector' },
28
+ ],
29
+ };
30
+ const result = derive(tree);
31
+ expect(result.bodyRoot.type).toBe('slot');
32
+ expect(result.bodyRoot.slotId).toBe('__sh3core__:compact:empty');
33
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['a']);
34
+ expect((_b = result.drawers.right) === null || _b === void 0 ? void 0 : _b.slots.map((s) => s.slotId)).toEqual(['b']);
35
+ });
36
+ it('single body wrapped in split collapses to bare body slot', () => {
37
+ const tree = {
38
+ type: 'split', direction: 'horizontal', sizes: [0.2, 0.8],
39
+ children: [
40
+ { type: 'slot', slotId: 'side', viewId: 'view:side', role: 'sidebar' },
41
+ { type: 'slot', slotId: 'body', viewId: 'view:body', role: 'body' },
42
+ ],
43
+ };
44
+ const result = derive(tree);
45
+ expect(result.bodyRoot.type).toBe('slot');
46
+ expect(result.bodyRoot.slotId).toBe('body');
47
+ });
48
+ });
49
+ describe('anchor inference', () => {
50
+ it('first horizontal child → left, last → right', () => {
51
+ var _a, _b;
52
+ const tree = {
53
+ type: 'split', direction: 'horizontal', sizes: [0.2, 0.6, 0.2],
54
+ children: [
55
+ { type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
56
+ { type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
57
+ { type: 'slot', slotId: 'ins', viewId: 'v:ins', role: 'inspector' },
58
+ ],
59
+ };
60
+ const result = derive(tree);
61
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['sb']);
62
+ expect((_b = result.drawers.right) === null || _b === void 0 ? void 0 : _b.slots.map((s) => s.slotId)).toEqual(['ins']);
63
+ expect(result.drawers.top).toBeNull();
64
+ });
65
+ it('top of vertical split → top anchor', () => {
66
+ var _a;
67
+ const tree = {
68
+ type: 'split', direction: 'vertical', sizes: [0.3, 0.7],
69
+ children: [
70
+ { type: 'slot', slotId: 'tools', viewId: 'v:tools', role: 'sidebar' },
71
+ { type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
72
+ ],
73
+ };
74
+ const result = derive(tree);
75
+ expect((_a = result.drawers.top) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['tools']);
76
+ expect(result.drawers.left).toBeNull();
77
+ expect(result.drawers.right).toBeNull();
78
+ });
79
+ it('inspector with no horizontal anchor defaults to right', () => {
80
+ var _a;
81
+ const tree = {
82
+ type: 'slot', slotId: 'lone', viewId: 'v:lone', role: 'inspector',
83
+ };
84
+ const result = derive(tree);
85
+ expect((_a = result.drawers.right) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['lone']);
86
+ expect(result.drawers.left).toBeNull();
87
+ });
88
+ it('sidebar with no horizontal anchor defaults to left', () => {
89
+ var _a;
90
+ const tree = {
91
+ type: 'slot', slotId: 'lone', viewId: 'v:lone', role: 'sidebar',
92
+ };
93
+ const result = derive(tree);
94
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['lone']);
95
+ expect(result.drawers.right).toBeNull();
96
+ });
97
+ });
98
+ describe('multi-slot drawers', () => {
99
+ it('two sidebars on the left render as one drawer with two slots', () => {
100
+ var _a;
101
+ const tree = {
102
+ type: 'split', direction: 'horizontal', sizes: [0.3, 0.7],
103
+ children: [
104
+ {
105
+ type: 'split', direction: 'vertical', sizes: [0.5, 0.5],
106
+ children: [
107
+ { type: 'slot', slotId: 'sb-top', viewId: 'v:sb-top', role: 'sidebar' },
108
+ { type: 'slot', slotId: 'sb-bot', viewId: 'v:sb-bot', role: 'sidebar' },
109
+ ],
110
+ },
111
+ { type: 'slot', slotId: 'body', viewId: 'v:body', role: 'body' },
112
+ ],
113
+ };
114
+ const result = derive(tree);
115
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['sb-top', 'sb-bot']);
116
+ });
117
+ });
118
+ describe('default role', () => {
119
+ it('untagged slot defaults to body', () => {
120
+ const tree = { type: 'slot', slotId: 'x', viewId: 'v:x' };
121
+ const result = derive(tree);
122
+ expect(result.bodyRoot).toEqual(tree);
123
+ expect(result.drawers.left).toBeNull();
124
+ expect(result.drawers.right).toBeNull();
125
+ expect(result.drawers.top).toBeNull();
126
+ });
127
+ });
128
+ describe('tabs nodes', () => {
129
+ it('tabs of body slots stay in body root', () => {
130
+ const tree = {
131
+ type: 'tabs', activeTab: 0,
132
+ tabs: [
133
+ { slotId: 't1', viewId: 'v:t1', label: 'Tab 1', role: 'body' },
134
+ { slotId: 't2', viewId: 'v:t2', label: 'Tab 2', role: 'body' },
135
+ ],
136
+ };
137
+ const result = derive(tree);
138
+ expect(result.bodyRoot.type).toBe('tabs');
139
+ });
140
+ it('tabs with mixed roles split body tabs from sidebar slots', () => {
141
+ var _a;
142
+ const tree = {
143
+ type: 'split', direction: 'horizontal', sizes: [0.2, 0.8],
144
+ children: [
145
+ { type: 'slot', slotId: 'sb', viewId: 'v:sb', role: 'sidebar' },
146
+ {
147
+ type: 'tabs', activeTab: 0,
148
+ tabs: [
149
+ { slotId: 't1', viewId: 'v:t1', label: 'Tab 1', role: 'body' },
150
+ { slotId: 't2', viewId: 'v:t2', label: 'Tab 2', role: 'body' },
151
+ ],
152
+ },
153
+ ],
154
+ };
155
+ const result = derive(tree);
156
+ expect(result.bodyRoot.type).toBe('tabs');
157
+ expect((_a = result.drawers.left) === null || _a === void 0 ? void 0 : _a.slots.map((s) => s.slotId)).toEqual(['sb']);
158
+ });
159
+ });
160
+ describe('carousels', () => {
161
+ it('includes a carousels Map in the output', () => {
162
+ var _a;
163
+ const tree = {
164
+ type: 'tabs',
165
+ tabs: [{ slotId: 't0', viewId: null, label: 'Only', role: 'body' }],
166
+ activeTab: 0,
167
+ };
168
+ const out = derive(tree);
169
+ expect(out.carousels).toBeInstanceOf(Map);
170
+ expect(out.carousels.size).toBe(1);
171
+ expect((_a = out.carousels.get('')) === null || _a === void 0 ? void 0 : _a.activeLabel).toBe('Only');
172
+ });
173
+ it('carousels Map is empty when bodyRoot has no qualifying tabs', () => {
174
+ const tree = { type: 'slot', slotId: 's', viewId: null, role: 'body' };
175
+ const out = derive(tree);
176
+ expect(out.carousels.size).toBe(0);
177
+ });
178
+ it('detection runs against bodyRoot (post-stripNonBody), not the source tree', () => {
179
+ const tree = {
180
+ type: 'split',
181
+ direction: 'horizontal',
182
+ sizes: [0.3, 0.7],
183
+ children: [
184
+ { type: 'slot', slotId: 'sb', viewId: null, role: 'sidebar' },
185
+ {
186
+ type: 'tabs',
187
+ tabs: [{ slotId: 't0', viewId: null, label: 'Body Tab', role: 'body' }],
188
+ activeTab: 0,
189
+ },
190
+ ],
191
+ };
192
+ const out = derive(tree);
193
+ expect(out.carousels.size).toBe(1);
194
+ expect(out.carousels.get('')).toBeDefined();
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,21 @@
1
+ import type { DrawerAnchor, DrawerStateMap } from './types';
2
+ export declare const drawerStore: {
3
+ readonly state: DrawerStateMap;
4
+ open(anchor: DrawerAnchor): void;
5
+ close(anchor: DrawerAnchor): void;
6
+ toggle(anchor: DrawerAnchor): void;
7
+ activate(anchor: DrawerAnchor, slotId: string): void;
8
+ /**
9
+ * Bind a write-through callback. layoutStore calls this when the active
10
+ * preset/viewport changes; the callback persists state into the
11
+ * AppLayoutBlob.drawers field. Pass null to unbind.
12
+ */
13
+ __setWriteThrough(cb: ((next: DrawerStateMap) => void) | null): void;
14
+ /**
15
+ * Replace the current state from a persisted snapshot. Used by
16
+ * layoutStore on preset/viewport-class change.
17
+ */
18
+ __hydrate(snapshot: DrawerStateMap): void;
19
+ /** Test-only reset. */
20
+ __reset(): void;
21
+ };
@@ -0,0 +1,75 @@
1
+ /*
2
+ * drawerStore — backing store for Sh3.drawers.
3
+ *
4
+ * In-memory state mirror; persistence integration (read/write
5
+ * AppLayoutBlob.drawers) lives in layout/store.svelte.ts and is wired
6
+ * via __setWriteThrough(...). When no blob is bound (home layout, satellite
7
+ * mode, tests), mutations stay in-memory only.
8
+ *
9
+ * The state object is keyed by anchor only; the per-(preset, viewport)
10
+ * dimension lives in the blob, not here. The bind helper in layoutStore
11
+ * rehydrates this store when the active preset or viewport class changes.
12
+ *
13
+ * File is .svelte.ts so the Svelte compiler processes the runes.
14
+ */
15
+ function initial() {
16
+ return {
17
+ left: { open: false, activeSlotId: null },
18
+ right: { open: false, activeSlotId: null },
19
+ top: { open: false, activeSlotId: null },
20
+ };
21
+ }
22
+ const state = $state(initial());
23
+ let writeThrough = null;
24
+ function flush() {
25
+ if (writeThrough)
26
+ writeThrough(state);
27
+ }
28
+ export const drawerStore = {
29
+ get state() {
30
+ return state;
31
+ },
32
+ open(anchor) {
33
+ state[anchor].open = true;
34
+ flush();
35
+ },
36
+ close(anchor) {
37
+ state[anchor].open = false;
38
+ flush();
39
+ },
40
+ toggle(anchor) {
41
+ state[anchor].open = !state[anchor].open;
42
+ flush();
43
+ },
44
+ activate(anchor, slotId) {
45
+ state[anchor].activeSlotId = slotId;
46
+ flush();
47
+ },
48
+ /**
49
+ * Bind a write-through callback. layoutStore calls this when the active
50
+ * preset/viewport changes; the callback persists state into the
51
+ * AppLayoutBlob.drawers field. Pass null to unbind.
52
+ */
53
+ __setWriteThrough(cb) {
54
+ writeThrough = cb;
55
+ },
56
+ /**
57
+ * Replace the current state from a persisted snapshot. Used by
58
+ * layoutStore on preset/viewport-class change.
59
+ */
60
+ __hydrate(snapshot) {
61
+ for (const anchor of ['left', 'right', 'top']) {
62
+ state[anchor].open = snapshot[anchor].open;
63
+ state[anchor].activeSlotId = snapshot[anchor].activeSlotId;
64
+ }
65
+ },
66
+ /** Test-only reset. */
67
+ __reset() {
68
+ writeThrough = null;
69
+ const fresh = initial();
70
+ for (const anchor of ['left', 'right', 'top']) {
71
+ state[anchor].open = fresh[anchor].open;
72
+ state[anchor].activeSlotId = fresh[anchor].activeSlotId;
73
+ }
74
+ },
75
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ /*
2
+ * Drawer store unit tests. These exercise the in-memory state machine
3
+ * only; persistence integration is covered in Task 11's blob
4
+ * round-trip test.
5
+ */
6
+ import { describe, it, expect, beforeEach } from 'vitest';
7
+ import { drawerStore } from './drawerStore.svelte';
8
+ describe('drawerStore', () => {
9
+ beforeEach(() => {
10
+ drawerStore.__reset();
11
+ });
12
+ it('starts with all drawers closed', () => {
13
+ expect(drawerStore.state.left).toEqual({ open: false, activeSlotId: null });
14
+ expect(drawerStore.state.right).toEqual({ open: false, activeSlotId: null });
15
+ expect(drawerStore.state.top).toEqual({ open: false, activeSlotId: null });
16
+ });
17
+ it('open(anchor) sets open true', () => {
18
+ drawerStore.open('left');
19
+ expect(drawerStore.state.left.open).toBe(true);
20
+ });
21
+ it('close(anchor) sets open false', () => {
22
+ drawerStore.open('left');
23
+ drawerStore.close('left');
24
+ expect(drawerStore.state.left.open).toBe(false);
25
+ });
26
+ it('toggle(anchor) flips open state', () => {
27
+ drawerStore.toggle('right');
28
+ expect(drawerStore.state.right.open).toBe(true);
29
+ drawerStore.toggle('right');
30
+ expect(drawerStore.state.right.open).toBe(false);
31
+ });
32
+ it('activate(anchor, slotId) sets activeSlotId', () => {
33
+ drawerStore.activate('left', 'sidebar-1');
34
+ expect(drawerStore.state.left.activeSlotId).toBe('sidebar-1');
35
+ });
36
+ it('reset returns to initial state', () => {
37
+ drawerStore.open('left');
38
+ drawerStore.activate('right', 'x');
39
+ drawerStore.__reset();
40
+ expect(drawerStore.state.left.open).toBe(false);
41
+ expect(drawerStore.state.right.activeSlotId).toBeNull();
42
+ });
43
+ });
@@ -0,0 +1,8 @@
1
+ import type { LayoutNode } from '../types';
2
+ export type NodePath = string;
3
+ export interface CarouselInfo {
4
+ wrap: boolean;
5
+ activeLabel: string;
6
+ }
7
+ export declare function pathKey(path: number[]): NodePath;
8
+ export declare function enrichCarousels(root: LayoutNode): Map<NodePath, CarouselInfo>;
@@ -0,0 +1,44 @@
1
+ /*
2
+ * enrichCarousels — pure post-derive transform.
3
+ *
4
+ * Walks the compact bodyRoot (the body-only sub-tree returned by
5
+ * stripNonBody) and identifies every TabsNode whose horizontal extent
6
+ * equals the body container's full width. A TabsNode qualifies iff
7
+ * every ancestor split is vertical — any horizontal split anywhere
8
+ * above it disqualifies the node and everything beneath it.
9
+ *
10
+ * The output is a Map keyed by NodePath (dot-joined child indices)
11
+ * carrying { wrap, activeLabel } for the renderer + chrome breadcrumb.
12
+ *
13
+ * The walk does NOT descend into a TabsNode itself once it has been
14
+ * identified — tab children are slot/leaf content, not nested layout
15
+ * nodes that could themselves carouselize.
16
+ */
17
+ export function pathKey(path) {
18
+ return path.join('.');
19
+ }
20
+ export function enrichCarousels(root) {
21
+ const out = new Map();
22
+ function walk(node, ancestorAllVertical, path) {
23
+ var _a, _b;
24
+ if (node.type === 'tabs') {
25
+ if (ancestorAllVertical) {
26
+ const active = node.tabs[node.activeTab];
27
+ out.set(pathKey(path), {
28
+ wrap: (_a = node.wrap) !== null && _a !== void 0 ? _a : false,
29
+ activeLabel: (_b = active === null || active === void 0 ? void 0 : active.label) !== null && _b !== void 0 ? _b : '',
30
+ });
31
+ }
32
+ return;
33
+ }
34
+ if (node.type === 'split') {
35
+ const next = ancestorAllVertical && node.direction === 'vertical';
36
+ for (let i = 0; i < node.children.length; i++) {
37
+ walk(node.children[i], next, [...path, i]);
38
+ }
39
+ return;
40
+ }
41
+ }
42
+ walk(root, true, []);
43
+ return out;
44
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { enrichCarousels, pathKey } from './enrichCarousels';
3
+ function slot(id) {
4
+ return { type: 'slot', slotId: id, viewId: null };
5
+ }
6
+ function tabs(labels, wrap) {
7
+ return Object.assign({ type: 'tabs', tabs: labels.map((l, i) => ({ slotId: `t${i}`, viewId: null, label: l })), activeTab: 0 }, (wrap !== undefined ? { wrap } : {}));
8
+ }
9
+ function vsplit(...children) {
10
+ return {
11
+ type: 'split',
12
+ direction: 'vertical',
13
+ sizes: children.map(() => 1),
14
+ children,
15
+ };
16
+ }
17
+ function hsplit(...children) {
18
+ return {
19
+ type: 'split',
20
+ direction: 'horizontal',
21
+ sizes: children.map(() => 1),
22
+ children,
23
+ };
24
+ }
25
+ describe('pathKey', () => {
26
+ it('joins indices with dots; empty path → empty string', () => {
27
+ expect(pathKey([])).toBe('');
28
+ expect(pathKey([0])).toBe('0');
29
+ expect(pathKey([1, 0, 2])).toBe('1.0.2');
30
+ });
31
+ });
32
+ describe('enrichCarousels', () => {
33
+ it('bodyRoot IS a tabs node → 1 carousel at root', () => {
34
+ const root = tabs(['A', 'B']);
35
+ const out = enrichCarousels(root);
36
+ expect(out.size).toBe(1);
37
+ expect(out.get('')).toEqual({ wrap: false, activeLabel: 'A' });
38
+ });
39
+ it('vertical-split[slot, tabs] → 1 carousel at "1"', () => {
40
+ const root = vsplit(slot('s'), tabs(['X', 'Y']));
41
+ const out = enrichCarousels(root);
42
+ expect(out.size).toBe(1);
43
+ expect(out.get('1')).toEqual({ wrap: false, activeLabel: 'X' });
44
+ });
45
+ it('vertical-split[tabs, vertical-split[tabs, slot]] → 2 carousels (stacked)', () => {
46
+ const inner = vsplit(tabs(['I0', 'I1']), slot('s'));
47
+ const root = vsplit(tabs(['T0']), inner);
48
+ const out = enrichCarousels(root);
49
+ expect(out.size).toBe(2);
50
+ expect(out.get('0')).toEqual({ wrap: false, activeLabel: 'T0' });
51
+ expect(out.get('1.0')).toEqual({ wrap: false, activeLabel: 'I0' });
52
+ });
53
+ it('horizontal-split[tabs, slot] → 0 carousels (tabs is narrowed)', () => {
54
+ const root = hsplit(tabs(['A']), slot('s'));
55
+ const out = enrichCarousels(root);
56
+ expect(out.size).toBe(0);
57
+ });
58
+ it('horizontal-split[tabs] (single child) → 0 carousels (any horizontal ancestor narrows)', () => {
59
+ const root = hsplit(tabs(['A']));
60
+ const out = enrichCarousels(root);
61
+ expect(out.size).toBe(0);
62
+ });
63
+ it('vertical-split[slot, horizontal-split[tabs, slot]] → 0 (inner tabs has horizontal ancestor)', () => {
64
+ const root = vsplit(slot('s'), hsplit(tabs(['A']), slot('s2')));
65
+ const out = enrichCarousels(root);
66
+ expect(out.size).toBe(0);
67
+ });
68
+ it('flows wrap=true through into the map', () => {
69
+ const root = vsplit(slot('s'), tabs(['X'], true));
70
+ const out = enrichCarousels(root);
71
+ expect(out.get('1')).toEqual({ wrap: true, activeLabel: 'X' });
72
+ });
73
+ it('reads activeLabel from the currently-active tab, not always the first', () => {
74
+ const t = tabs(['First', 'Second', 'Third']);
75
+ t.activeTab = 2;
76
+ const out = enrichCarousels(t);
77
+ expect(out.get('')).toEqual({ wrap: false, activeLabel: 'Third' });
78
+ });
79
+ it('does not recurse into the carousel\'s own children (a tabs node\'s tabs are not walked)', () => {
80
+ const root = tabs(['A']);
81
+ const out = enrichCarousels(root);
82
+ expect(out.size).toBe(1);
83
+ });
84
+ it('bare slot → 0 carousels', () => {
85
+ const out = enrichCarousels(slot('s'));
86
+ expect(out.size).toBe(0);
87
+ });
88
+ });
@@ -0,0 +1,6 @@
1
+ import type { SlotRole } from '../types';
2
+ interface SlotLike {
3
+ role?: SlotRole;
4
+ }
5
+ export declare function resolveRole(slot: SlotLike, viewDefault: SlotRole | undefined): SlotRole;
6
+ export {};
@@ -0,0 +1,13 @@
1
+ /*
2
+ * Role resolution: slot-level wins, view-level fills in, default 'body'.
3
+ *
4
+ * Why this matters: authoring a layout shouldn't require knowing every
5
+ * view's natural role. The view's author knows best ('graphlive:hierarchy'
6
+ * is a sidebar by nature), so they declare `defaultRole: 'sidebar'` on
7
+ * the ViewHandle. The app retains override authority by writing `role`
8
+ * on the slot itself.
9
+ */
10
+ export function resolveRole(slot, viewDefault) {
11
+ var _a, _b;
12
+ return (_b = (_a = slot.role) !== null && _a !== void 0 ? _a : viewDefault) !== null && _b !== void 0 ? _b : 'body';
13
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveRole } from './resolveRole';
3
+ describe('resolveRole', () => {
4
+ it('slot-level role wins over view default', () => {
5
+ expect(resolveRole({ role: 'body' }, 'sidebar')).toBe('body');
6
+ expect(resolveRole({ role: 'inspector' }, 'sidebar')).toBe('inspector');
7
+ });
8
+ it('view default fills in when slot role is unset', () => {
9
+ expect(resolveRole({}, 'sidebar')).toBe('sidebar');
10
+ expect(resolveRole({}, 'inspector')).toBe('inspector');
11
+ });
12
+ it('defaults to body when both unset', () => {
13
+ expect(resolveRole({}, undefined)).toBe('body');
14
+ });
15
+ it('treats undefined slot role same as missing field', () => {
16
+ expect(resolveRole({ role: undefined }, 'sidebar')).toBe('sidebar');
17
+ });
18
+ });
@@ -0,0 +1,30 @@
1
+ import type { LayoutNode, SlotNode, SlotRole } from '../types';
2
+ import type { NodePath, CarouselInfo } from './enrichCarousels';
3
+ export type { NodePath, CarouselInfo };
4
+ export type DrawerAnchor = 'left' | 'right' | 'top';
5
+ export interface DrawerSpec {
6
+ slots: Array<{
7
+ slotId: string;
8
+ viewId: string | null;
9
+ label: string;
10
+ icon?: string;
11
+ role: SlotRole;
12
+ }>;
13
+ }
14
+ export interface CompactRendering {
15
+ bodyRoot: LayoutNode;
16
+ drawers: {
17
+ left: DrawerSpec | null;
18
+ right: DrawerSpec | null;
19
+ top: DrawerSpec | null;
20
+ };
21
+ carousels: Map<NodePath, CarouselInfo>;
22
+ }
23
+ export interface DrawerState {
24
+ open: boolean;
25
+ activeSlotId: string | null;
26
+ }
27
+ export type DrawerStateMap = {
28
+ [anchor in DrawerAnchor]: DrawerState;
29
+ };
30
+ export declare const EMPTY_BODY: SlotNode;
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Compact-rendering types. The derivation (./derive.ts) walks the
3
+ * canonical LayoutNode tree and emits a CompactRendering: a body root
4
+ * (the body-only sub-tree) plus per-anchor drawer specs.
5
+ *
6
+ * EMPTY_BODY is the placeholder used when an input tree has zero body
7
+ * slots — likely an authoring bug, but we render the chrome correctly
8
+ * rather than crashing.
9
+ */
10
+ export const EMPTY_BODY = {
11
+ type: 'slot',
12
+ slotId: '__sh3core__:compact:empty',
13
+ viewId: null,
14
+ role: 'body',
15
+ };
@@ -43,6 +43,8 @@
43
43
  import { cleanupTree, insertTabIntoTabs, moveTabWithinTabs, removeTabBySlotId, splitNodeAtPath, } from './ops';
44
44
  import { layoutStore } from './store.svelte';
45
45
  import { isEmptyContent } from './floats';
46
+ import { claim, revoke } from '../gestures/pointerClaim';
47
+ import { ancestorCount } from '../gestures';
46
48
  export const dragState = $state({
47
49
  phase: 'idle',
48
50
  source: null,
@@ -65,6 +67,7 @@ export function suppressNextClick() {
65
67
  const DRAG_THRESHOLD_PX = 4;
66
68
  let pendingStartX = 0;
67
69
  let pendingStartY = 0;
70
+ let activeDragPointerId = null;
68
71
  /**
69
72
  * Begin a potential tab drag. Call from pointerdown on a tab element.
70
73
  * This does not yet enter the dragging phase — movement past the
@@ -73,6 +76,11 @@ let pendingStartY = 0;
73
76
  export function beginTabDrag(slotId, entry, sourceRoot, event, tabElement) {
74
77
  if (dragState.phase !== 'idle')
75
78
  return;
79
+ const depth = ancestorCount(tabElement);
80
+ const claimGranted = claim(event.pointerId, { ownerId: 'sh3:tabdrag', axis: 'xy', priority: 'normal', depth });
81
+ if (!claimGranted)
82
+ return;
83
+ activeDragPointerId = event.pointerId;
76
84
  const rect = tabElement.getBoundingClientRect();
77
85
  dragState.phase = 'pending';
78
86
  dragState.source = {
@@ -205,6 +213,10 @@ function commit() {
205
213
  autoCloseEmptyFloat(source.sourceRoot);
206
214
  }
207
215
  function teardown() {
216
+ if (activeDragPointerId !== null) {
217
+ revoke(activeDragPointerId, 'sh3:tabdrag');
218
+ activeDragPointerId = null;
219
+ }
208
220
  dragState.phase = 'idle';
209
221
  dragState.source = null;
210
222
  dragState.target = null;
@@ -244,4 +256,5 @@ export function __resetDragStateForTest() {
244
256
  clickSuppressedUntil = 0;
245
257
  pendingStartX = 0;
246
258
  pendingStartY = 0;
259
+ activeDragPointerId = null;
247
260
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveActiveTree } from './presets';
3
+ describe('compact variant selection', () => {
4
+ const preset = {
5
+ name: 'main',
6
+ variants: {
7
+ default: { docked: { type: 'slot', slotId: 'a', viewId: 'v:a' }, floats: [] },
8
+ compact: { docked: { type: 'slot', slotId: 'b', viewId: 'v:b' }, floats: [] },
9
+ },
10
+ };
11
+ it('returns default variant when class is desktop', () => {
12
+ const tree = resolveActiveTree(preset, 'desktop');
13
+ expect(tree.docked.slotId).toBe('a');
14
+ });
15
+ it('returns compact variant when class is compact and present', () => {
16
+ const tree = resolveActiveTree(preset, 'compact');
17
+ expect(tree.docked.slotId).toBe('b');
18
+ });
19
+ it('falls back to default when compact variant is absent', () => {
20
+ const onlyDefault = {
21
+ name: 'main',
22
+ variants: { default: preset.variants.default },
23
+ };
24
+ const tree = resolveActiveTree(onlyDefault, 'compact');
25
+ expect(tree.docked.slotId).toBe('a');
26
+ });
27
+ });