@happyvertical/smrt-svelte 0.51.39 → 0.53.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 (143) hide show
  1. package/AGENTS.md +3 -0
  2. package/README.md +214 -1
  3. package/agents/components.md +8 -1
  4. package/agents/workspace.md +24 -0
  5. package/dist/Provider.svelte +19 -15
  6. package/dist/Provider.svelte.d.ts +2 -2
  7. package/dist/Provider.svelte.d.ts.map +1 -1
  8. package/dist/__tests__/provider-mode-harness.svelte +14 -0
  9. package/dist/__tests__/provider-mode-harness.svelte.d.ts +11 -0
  10. package/dist/__tests__/provider-mode-harness.svelte.d.ts.map +1 -0
  11. package/dist/__tests__/provider-mode-probe.svelte +13 -0
  12. package/dist/__tests__/provider-mode-probe.svelte.d.ts +8 -0
  13. package/dist/__tests__/provider-mode-probe.svelte.d.ts.map +1 -0
  14. package/dist/browser-ai/adapters/stt/browser-speech.d.ts +11 -0
  15. package/dist/browser-ai/adapters/stt/browser-speech.d.ts.map +1 -1
  16. package/dist/browser-ai/adapters/stt/browser-speech.js +20 -4
  17. package/dist/browser-ai/adapters/stt/browser-speech.test.js +113 -0
  18. package/dist/browser-ai/adapters/stt/dictation-source.d.ts +3 -0
  19. package/dist/browser-ai/adapters/stt/dictation-source.d.ts.map +1 -0
  20. package/dist/browser-ai/adapters/stt/dictation-source.js +38 -0
  21. package/dist/browser-ai/adapters/stt/dictation-source.test.js +52 -0
  22. package/dist/browser-ai/adapters/stt/index.d.ts +2 -1
  23. package/dist/browser-ai/adapters/stt/index.d.ts.map +1 -1
  24. package/dist/browser-ai/adapters/stt/index.js +2 -1
  25. package/dist/components/forms/FileUpload.svelte +120 -16
  26. package/dist/components/forms/FileUpload.svelte.d.ts +7 -1
  27. package/dist/components/forms/FileUpload.svelte.d.ts.map +1 -1
  28. package/dist/components/forms/Form.svelte +118 -8
  29. package/dist/components/forms/Form.svelte.d.ts +18 -2
  30. package/dist/components/forms/Form.svelte.d.ts.map +1 -1
  31. package/dist/components/forms/FormScope.svelte +202 -0
  32. package/dist/components/forms/FormScope.svelte.d.ts +22 -0
  33. package/dist/components/forms/FormScope.svelte.d.ts.map +1 -0
  34. package/dist/components/forms/__tests__/FileUpload.native-form.test.js +249 -0
  35. package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte +24 -0
  36. package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte.d.ts +10 -0
  37. package/dist/components/forms/__tests__/FileUploadReset.fixture.svelte.d.ts.map +1 -0
  38. package/dist/components/forms/__tests__/Form.ui-controls.test.js +119 -0
  39. package/dist/components/forms/__tests__/Form.webmcp.test.js +5 -3
  40. package/dist/components/forms/__tests__/FormScope.test.js +39 -0
  41. package/dist/components/forms/__tests__/form-scope.fixture.svelte +30 -0
  42. package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts +10 -0
  43. package/dist/components/forms/__tests__/form-scope.fixture.svelte.d.ts.map +1 -0
  44. package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte +84 -0
  45. package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts +14 -0
  46. package/dist/components/forms/__tests__/form-with-ui-controls.fixture.svelte.d.ts.map +1 -0
  47. package/dist/components/forms/index.d.ts +1 -0
  48. package/dist/components/forms/index.d.ts.map +1 -1
  49. package/dist/components/forms/index.js +1 -0
  50. package/dist/components/workspace/README.md +11 -0
  51. package/dist/components/workspace/__tests__/AdminShell.responsive.test.js +597 -0
  52. package/dist/components/workspace/__tests__/AdminShell.test.js +257 -1
  53. package/dist/components/workspace/__tests__/TenantNav.test.js +115 -0
  54. package/dist/components/workspace/__tests__/WorkspaceAccountMenu.test.js +6 -0
  55. package/dist/components/workspace/__tests__/admin-shell-header-overflow.test.js +25 -0
  56. package/dist/components/workspace/__tests__/admin-shell-mobile.test.js +217 -0
  57. package/dist/components/workspace/__tests__/admin-shell-page-contracts.test.js +30 -0
  58. package/dist/components/workspace/__tests__/admin-shell-responsive-state.test.js +184 -0
  59. package/dist/components/workspace/__tests__/admin-shell-state.test.js +25 -0
  60. package/dist/components/workspace/__tests__/admin-shell-theme-hooks.test.js +36 -0
  61. package/dist/components/workspace/__tests__/shell-chrome-components.test.js +108 -0
  62. package/dist/components/workspace/admin-shell/AdminShell.svelte +1099 -59
  63. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts +44 -1
  64. package/dist/components/workspace/admin-shell/AdminShell.svelte.d.ts.map +1 -1
  65. package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte +170 -0
  66. package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts +11 -0
  67. package/dist/components/workspace/admin-shell/PhoneBottomBar.svelte.d.ts.map +1 -0
  68. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte +93 -0
  69. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts +11 -0
  70. package/dist/components/workspace/admin-shell/PhoneTopBar.svelte.d.ts.map +1 -0
  71. package/dist/components/workspace/admin-shell/ShellNavToggle.svelte +88 -0
  72. package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts +14 -0
  73. package/dist/components/workspace/admin-shell/ShellNavToggle.svelte.d.ts.map +1 -0
  74. package/dist/components/workspace/admin-shell/ShellTitle.svelte +42 -0
  75. package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts +11 -0
  76. package/dist/components/workspace/admin-shell/ShellTitle.svelte.d.ts.map +1 -0
  77. package/dist/components/workspace/admin-shell/TenantNav.svelte +116 -13
  78. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts +8 -2
  79. package/dist/components/workspace/admin-shell/TenantNav.svelte.d.ts.map +1 -1
  80. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +16 -2
  81. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts +2 -0
  82. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte.d.ts.map +1 -1
  83. package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts +37 -0
  84. package/dist/components/workspace/admin-shell/mobile-shell-dom.d.ts.map +1 -0
  85. package/dist/components/workspace/admin-shell/mobile-shell-dom.js +150 -0
  86. package/dist/components/workspace/admin-shell/mobile-shell.d.ts +191 -0
  87. package/dist/components/workspace/admin-shell/mobile-shell.d.ts.map +1 -0
  88. package/dist/components/workspace/admin-shell/mobile-shell.js +165 -0
  89. package/dist/components/workspace/admin-shell/settings.d.ts +27 -2
  90. package/dist/components/workspace/admin-shell/settings.d.ts.map +1 -1
  91. package/dist/components/workspace/admin-shell/settings.js +84 -2
  92. package/dist/components/workspace/admin-shell/state.svelte.d.ts +57 -1
  93. package/dist/components/workspace/admin-shell/state.svelte.d.ts.map +1 -1
  94. package/dist/components/workspace/admin-shell/state.svelte.js +156 -6
  95. package/dist/components/workspace/admin-shell/types.d.ts +115 -1
  96. package/dist/components/workspace/admin-shell/types.d.ts.map +1 -1
  97. package/dist/components/workspace/admin-shell/types.js +13 -0
  98. package/dist/components/workspace/index.d.ts +11 -4
  99. package/dist/components/workspace/index.d.ts.map +1 -1
  100. package/dist/components/workspace/index.js +12 -3
  101. package/dist/i18n/strings.forms.d.ts +3 -0
  102. package/dist/i18n/strings.forms.d.ts.map +1 -1
  103. package/dist/i18n/strings.forms.js +3 -0
  104. package/dist/i18n/strings.workspace.d.ts +14 -0
  105. package/dist/i18n/strings.workspace.d.ts.map +1 -1
  106. package/dist/i18n/strings.workspace.js +14 -0
  107. package/dist/index.d.ts +1 -1
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +1 -1
  110. package/dist/provider-mode.test.js +86 -0
  111. package/dist/state/app-state.svelte.d.ts.map +1 -1
  112. package/dist/state/app-state.svelte.js +8 -8
  113. package/dist/web/__tests__/assistant-dock.integration.svelte.test.js +2 -2
  114. package/dist/web/__tests__/list-sort-surface.fixture.svelte +41 -0
  115. package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts +15 -0
  116. package/dist/web/__tests__/list-sort-surface.fixture.svelte.d.ts.map +1 -0
  117. package/dist/web/__tests__/list-sort-surface.svelte.test.js +59 -0
  118. package/dist/web/__tests__/surfaces.child.fixture.svelte +69 -0
  119. package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts +16 -0
  120. package/dist/web/__tests__/surfaces.child.fixture.svelte.d.ts.map +1 -0
  121. package/dist/web/__tests__/surfaces.fixture.svelte +26 -0
  122. package/dist/web/__tests__/surfaces.fixture.svelte.d.ts +18 -0
  123. package/dist/web/__tests__/surfaces.fixture.svelte.d.ts.map +1 -0
  124. package/dist/web/__tests__/surfaces.svelte.test.js +140 -0
  125. package/dist/web/__tests__/webmcp-ui.test.js +18 -0
  126. package/dist/web/index.d.ts +4 -1
  127. package/dist/web/index.d.ts.map +1 -1
  128. package/dist/web/index.js +4 -1
  129. package/dist/web/link-surface.svelte.d.ts +14 -0
  130. package/dist/web/link-surface.svelte.d.ts.map +1 -0
  131. package/dist/web/link-surface.svelte.js +49 -0
  132. package/dist/web/list-surface.svelte.d.ts +80 -0
  133. package/dist/web/list-surface.svelte.d.ts.map +1 -0
  134. package/dist/web/list-surface.svelte.js +309 -0
  135. package/dist/web/step-surface.svelte.d.ts +17 -0
  136. package/dist/web/step-surface.svelte.d.ts.map +1 -0
  137. package/dist/web/step-surface.svelte.js +56 -0
  138. package/dist/web/webmcp-ui-context.d.ts +7 -0
  139. package/dist/web/webmcp-ui-context.d.ts.map +1 -1
  140. package/dist/web/webmcp-ui-context.js +10 -0
  141. package/dist/web/webmcp-ui.d.ts.map +1 -1
  142. package/dist/web/webmcp-ui.js +16 -3
  143. package/package.json +17 -17
@@ -0,0 +1,30 @@
1
+ /**
2
+ * AdminShell's page-header contracts on phones. jsdom doesn't apply the
3
+ * component's scoped styles, so these read the rules themselves: the phone
4
+ * top bar replaces the page's crumbs and title, and tabs inside a page header
5
+ * keep sticking under the bar.
6
+ */
7
+ import { describe, expect, it } from 'vitest';
8
+ import source from '../admin-shell/AdminShell.svelte?raw';
9
+ const css = source.slice(source.indexOf('<style>'));
10
+ function rule(selector) {
11
+ const start = css.indexOf(selector);
12
+ expect(start, `missing rule for ${selector}`).toBeGreaterThan(-1);
13
+ return css.slice(start, css.indexOf('}', start));
14
+ }
15
+ describe('AdminShell page-header contracts', () => {
16
+ it('hides the breadcrumb row on phones', () => {
17
+ expect(rule(".smrt-admin-shell[data-viewport='phone'] :global([data-shell-breadcrumbs])")).toContain('display: none');
18
+ });
19
+ it('visually hides the page title while the phone top bar shows it', () => {
20
+ const titleRule = rule('.smrt-admin-shell[data-phone-top] :global([data-shell-page-title])');
21
+ // Visually hidden, not display:none: screen readers and the document
22
+ // outline keep the heading.
23
+ expect(titleRule).toContain('position: absolute');
24
+ expect(titleRule).toContain('clip-path: inset(50%)');
25
+ expect(titleRule).not.toContain('display: none');
26
+ });
27
+ it("lets a page header's tabs stick under the top bar", () => {
28
+ expect(rule('.smrt-admin-shell[data-phone-top]\n :global([data-page-header-extra]:has([data-shell-tabs]))')).toContain('display: contents');
29
+ });
30
+ });
@@ -0,0 +1,184 @@
1
+ /**
2
+ * ShellState's viewport defaults, resizable-edge sizes, and per-edge
3
+ * persistence opt-outs.
4
+ */
5
+ import { describe, expect, it, vi } from 'vitest';
6
+ import { panelPersists, pruneShellSettingsDelta, resolvePanelResize, resolveShellConfig, stripUnpersistedSettings, } from '../admin-shell/settings.js';
7
+ import { createShellState } from '../admin-shell/state.svelte.js';
8
+ function adapter(initial = null) {
9
+ const writes = [];
10
+ return {
11
+ writes,
12
+ read: vi.fn(() => initial),
13
+ write: vi.fn((delta) => {
14
+ writes.push(JSON.parse(JSON.stringify(delta)));
15
+ }),
16
+ };
17
+ }
18
+ const navDefaults = {
19
+ phone: 'collapsed',
20
+ tablet: 'collapsed',
21
+ desktop: 'expanded',
22
+ };
23
+ describe('viewport defaults', () => {
24
+ it('uses the default for the initial viewport', () => {
25
+ const phone = createShellState({
26
+ viewport: 'phone',
27
+ config: { left: { viewportDefaults: navDefaults } },
28
+ });
29
+ expect(phone.panels.left).toBe('collapsed');
30
+ const desktop = createShellState({
31
+ viewport: 'desktop',
32
+ config: { left: { viewportDefaults: navDefaults } },
33
+ });
34
+ expect(desktop.panels.left).toBe('expanded');
35
+ });
36
+ it('applies the new default only when the viewport class changes', () => {
37
+ const shell = createShellState({
38
+ viewport: 'desktop',
39
+ config: { left: { viewportDefaults: navDefaults } },
40
+ });
41
+ shell.collapsePanel('left');
42
+ shell.setViewport('desktop');
43
+ expect(shell.panels.left).toBe('collapsed');
44
+ shell.setViewport('tablet');
45
+ expect(shell.viewport).toBe('tablet');
46
+ expect(shell.panels.left).toBe('collapsed');
47
+ shell.expandPanel('left');
48
+ shell.setViewport('desktop');
49
+ expect(shell.panels.left).toBe('expanded');
50
+ // A later settings merge does not resurrect the old class's toggle.
51
+ shell.applySettings({ activeFocusToolId: 'x' });
52
+ expect(shell.panels.left).toBe('expanded');
53
+ });
54
+ it('never persists the state of an edge that follows the viewport', async () => {
55
+ const store = adapter({ panels: { left: 'expanded', right: 'expanded' } });
56
+ const shell = createShellState({
57
+ viewport: 'phone',
58
+ config: { left: { viewportDefaults: navDefaults } },
59
+ settingsAdapter: store,
60
+ });
61
+ await shell.hydrate();
62
+ expect(shell.panels.left).toBe('collapsed');
63
+ expect(shell.panels.right).toBe('expanded');
64
+ shell.expandPanel('left');
65
+ expect(store.writes.at(-1)?.panels).toEqual({ right: 'expanded' });
66
+ });
67
+ it('leaves edges without viewport defaults alone (backward compatible)', () => {
68
+ const shell = createShellState({ viewport: 'desktop' });
69
+ shell.expandPanel('left');
70
+ shell.setViewport('phone');
71
+ expect(shell.panels.left).toBe('expanded');
72
+ expect(shell.snapshot().viewport).toBe('phone');
73
+ });
74
+ });
75
+ describe('phone presentation', () => {
76
+ it('hides a phone-hidden edge only on phones, and Escape skips it there', () => {
77
+ const shell = createShellState({
78
+ viewport: 'phone',
79
+ config: { right: { phone: 'hidden', initial: 'expanded' } },
80
+ });
81
+ expect(shell.phonePresentation('right')).toBe('hidden');
82
+ expect(shell.phonePresentation('left')).toBe('drawer');
83
+ expect(shell.isEdgeShown('right')).toBe(false);
84
+ expect(shell.closeTopmostExpanded()).toBe(false);
85
+ expect(shell.panels.right).toBe('expanded');
86
+ shell.setViewport('desktop');
87
+ expect(shell.isEdgeShown('right')).toBe(true);
88
+ expect(shell.closeTopmostExpanded()).toBe(true);
89
+ expect(shell.panels.right).toBe('collapsed');
90
+ });
91
+ });
92
+ describe('resizable edges', () => {
93
+ it('resolves limits for side edges only', () => {
94
+ const config = resolveShellConfig({
95
+ right: { resizable: { min: 300, max: 600 } },
96
+ left: { resizable: true },
97
+ top: { resizable: true },
98
+ });
99
+ expect(resolvePanelResize('right', config.panels.right)).toEqual({
100
+ min: 300,
101
+ max: 600,
102
+ step: 16,
103
+ });
104
+ expect(resolvePanelResize('left', config.panels.left)).toEqual({
105
+ min: 240,
106
+ max: 720,
107
+ step: 16,
108
+ });
109
+ expect(resolvePanelResize('top', config.panels.top)).toBeNull();
110
+ expect(resolvePanelResize('bottom', config.panels.bottom)).toBeNull();
111
+ });
112
+ it('clamps, persists, and resets sizes', () => {
113
+ const store = adapter();
114
+ const shell = createShellState({
115
+ config: { right: { resizable: { min: 300, max: 600 } } },
116
+ settingsAdapter: store,
117
+ });
118
+ expect(shell.panelSize('right')).toBeNull();
119
+ shell.setPanelSize('right', 900);
120
+ expect(shell.panelSize('right')).toBe(600);
121
+ expect(store.writes.at(-1)?.sizes).toEqual({ right: 600 });
122
+ shell.setPanelSize('right', 120.4, { persist: false });
123
+ expect(shell.panelSize('right')).toBe(300);
124
+ expect(store.write).toHaveBeenCalledTimes(1);
125
+ shell.setPanelSize('right', null);
126
+ expect(shell.panelSize('right')).toBeNull();
127
+ expect(pruneShellSettingsDelta(shell.settings).sizes).toBeUndefined();
128
+ });
129
+ it('ignores sizes for edges that are not resizable', () => {
130
+ const shell = createShellState({ settings: { sizes: { left: 500 } } });
131
+ expect(shell.panelSize('left')).toBeNull();
132
+ shell.setPanelSize('left', 400);
133
+ expect(shell.settings.sizes).toEqual({ left: 500 });
134
+ expect(shell.panelSize('left')).toBeNull();
135
+ });
136
+ it('keeps a size in memory but not in storage when size persistence is off', () => {
137
+ const store = adapter({ sizes: { right: 480 } });
138
+ const shell = createShellState({
139
+ config: { right: { resizable: true, persist: { size: false } } },
140
+ settingsAdapter: store,
141
+ });
142
+ void shell.hydrate();
143
+ expect(shell.panelSize('right')).toBeNull();
144
+ shell.setPanelSize('right', 500);
145
+ expect(shell.panelSize('right')).toBe(500);
146
+ expect(store.writes.at(-1)?.sizes).toBeUndefined();
147
+ });
148
+ });
149
+ describe('persistence opt-out', () => {
150
+ it('reports what an edge persists', () => {
151
+ const config = resolveShellConfig({
152
+ left: { persist: false },
153
+ right: { persist: { state: false } },
154
+ bottom: { viewportDefaults: { phone: 'collapsed' } },
155
+ });
156
+ expect(panelPersists(config.panels.left, 'state')).toBe(false);
157
+ expect(panelPersists(config.panels.left, 'size')).toBe(false);
158
+ expect(panelPersists(config.panels.right, 'state')).toBe(false);
159
+ expect(panelPersists(config.panels.right, 'size')).toBe(true);
160
+ expect(panelPersists(config.panels.bottom, 'state')).toBe(false);
161
+ expect(panelPersists(config.panels.top, 'state')).toBe(true);
162
+ });
163
+ it('strips unpersisted panels and sizes on write and on read', async () => {
164
+ const config = resolveShellConfig({ left: { persist: false } });
165
+ expect(stripUnpersistedSettings({
166
+ panels: { left: 'expanded', right: 'collapsed' },
167
+ sizes: { left: 300 },
168
+ }, config)).toEqual({ panels: { right: 'collapsed' } });
169
+ const store = adapter({
170
+ panels: { left: 'expanded' },
171
+ hotkeysEnabled: false,
172
+ });
173
+ const shell = createShellState({
174
+ config: { left: { persist: false, initial: 'collapsed' } },
175
+ settingsAdapter: store,
176
+ });
177
+ await shell.hydrate();
178
+ expect(shell.panels.left).toBe('collapsed');
179
+ expect(shell.settings.hotkeysEnabled).toBe(false);
180
+ shell.expandPanel('left');
181
+ expect(shell.panels.left).toBe('expanded');
182
+ expect(store.writes.at(-1)?.panels).toBeUndefined();
183
+ });
184
+ });
@@ -28,6 +28,31 @@ function mountMutationEffect(shell, run) {
28
28
  };
29
29
  }
30
30
  describe('ShellState', () => {
31
+ it('persists runtime hide/show and restores hidden preferences on hydration', async () => {
32
+ const write = vi.fn();
33
+ const shell = createShellState({
34
+ settingsAdapter: { read: () => null, write },
35
+ });
36
+ shell.setPanelState('left', 'hidden');
37
+ expect(shell.panels.left).toBe('hidden');
38
+ expect(write).toHaveBeenLastCalledWith(expect.objectContaining({ panels: { left: 'hidden' } }));
39
+ const restored = createShellState({
40
+ settingsAdapter: { read: () => shell.snapshot().settings, write },
41
+ });
42
+ await restored.hydrate();
43
+ expect(restored.panels.left).toBe('hidden');
44
+ restored.setPanelState('left', 'expanded');
45
+ expect(restored.panels.left).toBe('expanded');
46
+ restored.setPanelState('left', 'collapsed');
47
+ expect(restored.panels.left).toBe('collapsed');
48
+ });
49
+ it('keeps app-disabled edges hidden and rejects unknown runtime panel states', () => {
50
+ const shell = createShellState({ config: { right: false } });
51
+ shell.setPanelState('right', 'expanded');
52
+ expect(shell.panels.right).toBe('hidden');
53
+ shell.setPanelState('left', 'unknown');
54
+ expect(shell.panels.left).toBe('collapsed');
55
+ });
31
56
  it('allows panels to expand independently by default', () => {
32
57
  const shell = createShellState();
33
58
  shell.expandPanel('top');
@@ -0,0 +1,36 @@
1
+ /**
2
+ * An app themes the shell through custom properties instead of restyling its
3
+ * internal classes: the page background, themed thin scrollbars, the shell
4
+ * title's font family, and a right rail that never pads wider than its track.
5
+ */
6
+ import { readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { describe, expect, it } from 'vitest';
9
+ const packageRoot = process.cwd().endsWith('packages/smrt-svelte')
10
+ ? process.cwd()
11
+ : join(process.cwd(), 'packages/smrt-svelte');
12
+ const shellDir = join(packageRoot, 'src/components/workspace/admin-shell');
13
+ const shell = readFileSync(join(shellDir, 'AdminShell.svelte'), 'utf8');
14
+ const title = readFileSync(join(shellDir, 'ShellTitle.svelte'), 'utf8');
15
+ function ruleBody(source, selector) {
16
+ const start = source.indexOf(`${selector} {`);
17
+ expect(start).toBeGreaterThan(-1);
18
+ return source.slice(start, source.indexOf('\n }', start));
19
+ }
20
+ describe('AdminShell theme hooks', () => {
21
+ it('reads its page background from --smrt-admin-shell-background', () => {
22
+ expect(ruleBody(shell, '.smrt-admin-shell')).toMatch(/background:\s*var\(--smrt-admin-shell-background,\s*var\(--smrt-color-surface\)\)/);
23
+ });
24
+ it('themes thin scrollbars from tokens, overridable per app', () => {
25
+ const root = ruleBody(shell, '.smrt-admin-shell');
26
+ expect(root).toMatch(/scrollbar-color:[^;]*--smrt-admin-shell-scrollbar-thumb[^;]*--smrt-admin-shell-scrollbar-track/s);
27
+ expect(ruleBody(shell, '.smrt-admin-shell :global(*)')).toMatch(/scrollbar-width:\s*thin/);
28
+ expect(shell).toContain('--smrt-admin-shell-scrollbar-thumb-hover');
29
+ });
30
+ it('keeps a right rail collapsed to 0 from painting padding', () => {
31
+ expect(ruleBody(shell, '.smrt-admin-shell__edge--right .smrt-admin-shell__rail')).toMatch(/padding-inline:\s*min\(\s*var\(--smrt-spacing-3\),\s*calc\(var\(--smrt-admin-shell-right-collapsed\) \/ 4\)/);
32
+ });
33
+ it('lets an app set the shell title font family', () => {
34
+ expect(ruleBody(title, '.smrt-shell-title')).toMatch(/font-family:\s*var\(--smrt-shell-title-font-family/);
35
+ });
36
+ });
@@ -0,0 +1,108 @@
1
+ /** ShellNavToggle, ShellTitle, PhoneTopBar and PhoneBottomBar. */
2
+ import { fireEvent, render, screen } from '@testing-library/svelte';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import PhoneBottomBar from '../admin-shell/PhoneBottomBar.svelte';
5
+ import PhoneTopBar from '../admin-shell/PhoneTopBar.svelte';
6
+ import ShellNavToggle from '../admin-shell/ShellNavToggle.svelte';
7
+ import TestIcon from './test-icon.svelte';
8
+ describe('ShellNavToggle', () => {
9
+ it('collapses an open nav and expands a closed one', async () => {
10
+ const onclick = vi.fn();
11
+ const { rerender } = render(ShellNavToggle, {
12
+ props: { expanded: true, onclick, label: 'site navigation' },
13
+ });
14
+ const toggle = screen.getByRole('button', {
15
+ name: 'Collapse site navigation',
16
+ });
17
+ expect(toggle).toHaveAttribute('aria-expanded', 'true');
18
+ expect(toggle).toHaveAttribute('aria-controls', 'smrt-admin-shell-left-panel');
19
+ await fireEvent.click(toggle);
20
+ expect(onclick).toHaveBeenCalledTimes(1);
21
+ await rerender({ expanded: false, onclick });
22
+ expect(screen.getByRole('button', { name: 'Expand site navigation' })).toHaveAttribute('aria-expanded', 'false');
23
+ });
24
+ it('defaults its label', () => {
25
+ render(ShellNavToggle, { props: { expanded: false, onclick: () => { } } });
26
+ expect(screen.getByRole('button', { name: 'Expand navigation' })).toBeInTheDocument();
27
+ });
28
+ });
29
+ describe('PhoneTopBar', () => {
30
+ it('links the workspace name home on section homes', () => {
31
+ render(PhoneTopBar, {
32
+ props: {
33
+ model: { kind: 'home', title: 'Alpha Times' },
34
+ homeHref: '/sites/alpha',
35
+ },
36
+ });
37
+ const link = screen.getByRole('link', { name: 'Alpha Times' });
38
+ expect(link).toHaveAttribute('href', '/sites/alpha');
39
+ });
40
+ it('shows a named back link and the page title on detail pages', () => {
41
+ render(PhoneTopBar, {
42
+ props: {
43
+ model: {
44
+ kind: 'detail',
45
+ title: 'Fair opens Friday',
46
+ backHref: '/sites/alpha/articles',
47
+ backLabel: 'Articles',
48
+ },
49
+ homeHref: '/sites/alpha',
50
+ },
51
+ });
52
+ expect(screen.getByRole('link', { name: 'Back to Articles' })).toHaveAttribute('href', '/sites/alpha/articles');
53
+ expect(screen.getByTestId('phone-top-bar').textContent).toContain('Fair opens Friday');
54
+ });
55
+ });
56
+ describe('PhoneBottomBar', () => {
57
+ it('renders links and buttons with states, badges and dots', async () => {
58
+ const onMenu = vi.fn();
59
+ render(PhoneBottomBar, {
60
+ props: {
61
+ items: [
62
+ {
63
+ id: 'menu',
64
+ label: 'Menu',
65
+ icon: TestIcon,
66
+ onclick: onMenu,
67
+ expanded: false,
68
+ controls: 'nav',
69
+ },
70
+ {
71
+ id: 'home',
72
+ label: 'Home',
73
+ icon: TestIcon,
74
+ href: '/',
75
+ active: true,
76
+ },
77
+ {
78
+ id: 'notifications',
79
+ label: 'Notifications',
80
+ icon: TestIcon,
81
+ onclick: () => { },
82
+ badge: 12,
83
+ },
84
+ {
85
+ id: 'assistant',
86
+ label: 'Assistant',
87
+ icon: TestIcon,
88
+ onclick: () => { },
89
+ dot: true,
90
+ dotLabel: 'new reply',
91
+ },
92
+ ],
93
+ },
94
+ });
95
+ expect(screen.getByRole('navigation', { name: 'Main' })).toBeInTheDocument();
96
+ const menu = screen.getByRole('button', { name: 'Menu' });
97
+ expect(menu).toHaveAttribute('aria-expanded', 'false');
98
+ expect(menu).toHaveAttribute('aria-controls', 'nav');
99
+ await fireEvent.click(menu);
100
+ expect(onMenu).toHaveBeenCalledTimes(1);
101
+ expect(screen.getByRole('link', { name: 'Home' })).toHaveAttribute('aria-current', 'page');
102
+ const notifications = screen.getByRole('button', {
103
+ name: 'Notifications, 12 unread',
104
+ });
105
+ expect(notifications.textContent).toContain('9+');
106
+ expect(screen.getByRole('button', { name: 'Assistant, new reply' })).toBeInTheDocument();
107
+ });
108
+ });