@iloveagents/foundry-web-ui 0.3.0 → 0.4.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 (223) hide show
  1. package/README.md +14 -5
  2. package/dist/components/ag-ui-runtime-provider.d.ts +64 -0
  3. package/dist/components/ag-ui-runtime-provider.js +71 -0
  4. package/dist/components/app-brand.d.ts +6 -0
  5. package/dist/components/app-brand.js +13 -0
  6. package/dist/components/assistant-chat.d.ts +26 -0
  7. package/dist/components/assistant-chat.js +93 -0
  8. package/dist/components/chat-attachments.d.ts +25 -0
  9. package/dist/components/chat-attachments.js +20 -0
  10. package/dist/components/chat-bubble.d.ts +1 -0
  11. package/dist/components/chat-bubble.js +103 -0
  12. package/dist/components/chat-header.d.ts +2 -0
  13. package/dist/components/chat-header.js +16 -0
  14. package/dist/components/client-tool-executor.d.ts +12 -0
  15. package/dist/components/client-tool-executor.js +108 -0
  16. package/dist/components/collection-empty-state.d.ts +9 -0
  17. package/dist/components/collection-empty-state.js +6 -0
  18. package/dist/components/collection-filter-bar.d.ts +19 -0
  19. package/dist/components/collection-filter-bar.js +28 -0
  20. package/dist/components/collection-search-input.d.ts +6 -0
  21. package/dist/components/collection-search-input.js +8 -0
  22. package/dist/components/collection-skeleton.d.ts +6 -0
  23. package/dist/components/collection-skeleton.js +10 -0
  24. package/dist/components/collection-sort-menu.d.ts +10 -0
  25. package/dist/components/collection-sort-menu.js +9 -0
  26. package/dist/components/collection-surface.d.ts +8 -0
  27. package/dist/components/collection-surface.js +5 -0
  28. package/dist/components/collection-toolbar.d.ts +9 -0
  29. package/dist/components/collection-toolbar.js +5 -0
  30. package/dist/components/collection-view-toggle.d.ts +7 -0
  31. package/dist/components/collection-view-toggle.js +17 -0
  32. package/dist/components/composer-submit-bridge.d.ts +1 -0
  33. package/dist/components/composer-submit-bridge.js +15 -0
  34. package/dist/components/confirm-dialog.d.ts +11 -0
  35. package/dist/components/confirm-dialog.js +6 -0
  36. package/dist/components/confirmation-card.d.ts +14 -0
  37. package/dist/components/confirmation-card.js +12 -0
  38. package/dist/components/context-badges.d.ts +16 -0
  39. package/dist/components/context-badges.js +56 -0
  40. package/dist/components/context-bar.d.ts +41 -0
  41. package/dist/components/context-bar.js +123 -0
  42. package/dist/components/form-dialog.d.ts +12 -0
  43. package/dist/components/form-dialog.js +13 -0
  44. package/dist/components/global-selection-popover.d.ts +7 -0
  45. package/dist/components/global-selection-popover.js +167 -0
  46. package/dist/components/infinite-scroll-sentinel.d.ts +6 -0
  47. package/dist/components/infinite-scroll-sentinel.js +25 -0
  48. package/dist/components/json-viewer.d.ts +10 -0
  49. package/dist/components/json-viewer.js +80 -0
  50. package/dist/components/loading-indicator.d.ts +4 -0
  51. package/dist/components/loading-indicator.js +22 -0
  52. package/dist/components/markdown-text.d.ts +13 -0
  53. package/dist/components/markdown-text.js +221 -0
  54. package/dist/components/name-dialog.d.ts +16 -0
  55. package/dist/components/name-dialog.js +23 -0
  56. package/dist/components/selection-popover.d.ts +12 -0
  57. package/dist/components/selection-popover.js +111 -0
  58. package/dist/components/show-document-tool-ui.d.ts +1 -0
  59. package/dist/components/show-document-tool-ui.js +53 -0
  60. package/dist/components/sidebar.d.ts +4 -0
  61. package/dist/components/sidebar.js +904 -0
  62. package/dist/components/surface-card.d.ts +9 -0
  63. package/dist/components/surface-card.js +33 -0
  64. package/dist/components/theme-runtime-provider.d.ts +15 -0
  65. package/dist/components/theme-runtime-provider.js +48 -0
  66. package/dist/components/theme-toggle.d.ts +2 -0
  67. package/dist/components/theme-toggle.js +19 -0
  68. package/dist/components/tool-call-card.d.ts +27 -0
  69. package/dist/components/tool-call-card.js +13 -0
  70. package/dist/components/tool-fallback.d.ts +26 -0
  71. package/dist/components/tool-fallback.js +129 -0
  72. package/dist/components/tool-panel-layout.d.ts +10 -0
  73. package/dist/components/tool-panel-layout.js +12 -0
  74. package/dist/components/tool-panel.d.ts +1 -0
  75. package/dist/components/tool-panel.js +89 -0
  76. package/dist/components/tooltip-icon-button.d.ts +7 -0
  77. package/dist/components/tooltip-icon-button.js +9 -0
  78. package/dist/components/user-menu.d.ts +2 -0
  79. package/dist/components/user-menu.js +14 -0
  80. package/dist/index.d.ts +68 -0
  81. package/dist/index.js +83 -0
  82. package/dist/lib/ag-ui-adapter.d.ts +29 -0
  83. package/dist/lib/ag-ui-adapter.js +373 -0
  84. package/dist/lib/app-store.d.ts +115 -0
  85. package/dist/lib/app-store.js +199 -0
  86. package/dist/lib/attachment-adapter.d.ts +20 -0
  87. package/dist/lib/attachment-adapter.js +99 -0
  88. package/dist/lib/auth-provider.d.ts +16 -0
  89. package/dist/lib/auth-provider.js +88 -0
  90. package/dist/lib/chat-bubble-store.d.ts +30 -0
  91. package/dist/lib/chat-bubble-store.js +23 -0
  92. package/dist/lib/chat-lifecycle-store.d.ts +10 -0
  93. package/{src/lib/chat-lifecycle-store.ts → dist/lib/chat-lifecycle-store.js} +3 -13
  94. package/dist/lib/client-tools.d.ts +11 -0
  95. package/dist/lib/client-tools.js +274 -0
  96. package/dist/lib/composer-submit-store.d.ts +12 -0
  97. package/dist/lib/composer-submit-store.js +17 -0
  98. package/dist/lib/dev-store.d.ts +38 -0
  99. package/dist/lib/dev-store.js +32 -0
  100. package/dist/lib/nav-config.d.ts +159 -0
  101. package/dist/lib/nav-config.js +162 -0
  102. package/dist/lib/nav-dnd.d.ts +8 -0
  103. package/dist/lib/nav-dnd.js +9 -0
  104. package/dist/lib/nav-store.d.ts +45 -0
  105. package/dist/lib/nav-store.js +49 -0
  106. package/dist/lib/selection-context.d.ts +21 -0
  107. package/dist/lib/selection-context.js +34 -0
  108. package/dist/lib/sidebar-store.d.ts +23 -0
  109. package/dist/lib/sidebar-store.js +98 -0
  110. package/dist/lib/theme-runtime.d.ts +130 -0
  111. package/dist/lib/theme-runtime.js +479 -0
  112. package/dist/lib/theme-store.d.ts +15 -0
  113. package/dist/lib/theme-store.js +53 -0
  114. package/dist/lib/tool-panel-store.d.ts +48 -0
  115. package/dist/lib/tool-panel-store.js +69 -0
  116. package/dist/lib/use-new-conversation.d.ts +8 -0
  117. package/dist/lib/use-new-conversation.js +62 -0
  118. package/dist/lib/use-page-tools.d.ts +27 -0
  119. package/{src/lib/use-page-tools.ts → dist/lib/use-page-tools.js} +11 -13
  120. package/dist/styles.css +142 -0
  121. package/dist/ui/collapsible.d.ts +5 -0
  122. package/{src/ui/collapsible.tsx → dist/ui/collapsible.js} +0 -2
  123. package/dist/ui/dialog.d.ts +15 -0
  124. package/dist/ui/dialog.js +25 -0
  125. package/dist/ui/dropdown-menu.d.ts +7 -0
  126. package/dist/ui/dropdown-menu.js +19 -0
  127. package/dist/ui/popover.d.ts +7 -0
  128. package/dist/ui/popover.js +10 -0
  129. package/dist/ui/select.d.ts +4 -0
  130. package/dist/ui/select.js +9 -0
  131. package/dist/ui/tooltip.d.ts +7 -0
  132. package/dist/ui/tooltip.js +10 -0
  133. package/package.json +31 -12
  134. package/AGENTS.md +0 -59
  135. package/CHANGELOG.md +0 -203
  136. package/CLAUDE.md +0 -1
  137. package/src/__tests__/no-spaces-imports.test.ts +0 -59
  138. package/src/__tests__/open-core-guards.test.ts +0 -307
  139. package/src/__tests__/theme-runtime.test.ts +0 -81
  140. package/src/__tests__/tool-fallback.test.tsx +0 -109
  141. package/src/components/__tests__/context-bar.test.ts +0 -112
  142. package/src/components/ag-ui-runtime-provider.tsx +0 -170
  143. package/src/components/app-brand.tsx +0 -38
  144. package/src/components/assistant-chat.tsx +0 -461
  145. package/src/components/chat-attachments.tsx +0 -68
  146. package/src/components/chat-bubble.tsx +0 -373
  147. package/src/components/chat-header.tsx +0 -44
  148. package/src/components/client-tool-executor.tsx +0 -230
  149. package/src/components/collection-empty-state.tsx +0 -37
  150. package/src/components/collection-filter-bar.tsx +0 -168
  151. package/src/components/collection-search-input.tsx +0 -41
  152. package/src/components/collection-skeleton.tsx +0 -55
  153. package/src/components/collection-sort-menu.tsx +0 -62
  154. package/src/components/collection-surface.tsx +0 -46
  155. package/src/components/collection-toolbar.tsx +0 -33
  156. package/src/components/collection-view-toggle.tsx +0 -61
  157. package/src/components/composer-submit-bridge.tsx +0 -19
  158. package/src/components/confirm-dialog.tsx +0 -49
  159. package/src/components/confirmation-card.tsx +0 -32
  160. package/src/components/context-badges.tsx +0 -136
  161. package/src/components/context-bar.tsx +0 -238
  162. package/src/components/form-dialog.tsx +0 -56
  163. package/src/components/global-selection-popover.tsx +0 -266
  164. package/src/components/infinite-scroll-sentinel.tsx +0 -48
  165. package/src/components/json-viewer.tsx +0 -232
  166. package/src/components/loading-indicator.tsx +0 -40
  167. package/src/components/markdown-text-citations.test.tsx +0 -108
  168. package/src/components/markdown-text.tsx +0 -508
  169. package/src/components/name-dialog.tsx +0 -87
  170. package/src/components/selection-popover.tsx +0 -156
  171. package/src/components/show-document-tool-ui.tsx +0 -90
  172. package/src/components/sidebar.test.tsx +0 -611
  173. package/src/components/sidebar.tsx +0 -1689
  174. package/src/components/surface-card.tsx +0 -45
  175. package/src/components/theme-runtime-provider.tsx +0 -93
  176. package/src/components/theme-toggle.tsx +0 -27
  177. package/src/components/tool-call-card.tsx +0 -126
  178. package/src/components/tool-fallback.tsx +0 -169
  179. package/src/components/tool-panel-layout.tsx +0 -36
  180. package/src/components/tool-panel.tsx +0 -233
  181. package/src/components/tooltip-icon-button.tsx +0 -28
  182. package/src/components/user-menu.tsx +0 -60
  183. package/src/index.ts +0 -185
  184. package/src/lib/__tests__/ag-ui-adapter.test.ts +0 -273
  185. package/src/lib/__tests__/app-store.test.ts +0 -405
  186. package/src/lib/__tests__/attachment-adapter.test.ts +0 -48
  187. package/src/lib/__tests__/chat-bubble-store.test.ts +0 -67
  188. package/src/lib/__tests__/client-tools.test.ts +0 -124
  189. package/src/lib/__tests__/dev-store.test.ts +0 -78
  190. package/src/lib/__tests__/nav-config.test.ts +0 -135
  191. package/src/lib/__tests__/nav-dnd.test.ts +0 -24
  192. package/src/lib/__tests__/nav-store.test.ts +0 -59
  193. package/src/lib/__tests__/selection-context.test.ts +0 -114
  194. package/src/lib/__tests__/sidebar-store.test.ts +0 -144
  195. package/src/lib/__tests__/theme-store.test.ts +0 -83
  196. package/src/lib/__tests__/tool-panel-store.test.ts +0 -61
  197. package/src/lib/__tests__/use-page-context.test.ts +0 -58
  198. package/src/lib/ag-ui-adapter.ts +0 -412
  199. package/src/lib/app-store.ts +0 -361
  200. package/src/lib/attachment-adapter.ts +0 -92
  201. package/src/lib/auth-provider.tsx +0 -144
  202. package/src/lib/chat-bubble-store.ts +0 -46
  203. package/src/lib/client-tools.ts +0 -293
  204. package/src/lib/composer-submit-store.ts +0 -32
  205. package/src/lib/dev-store.ts +0 -69
  206. package/src/lib/nav-config.ts +0 -329
  207. package/src/lib/nav-dnd.ts +0 -14
  208. package/src/lib/nav-store.ts +0 -89
  209. package/src/lib/selection-context.ts +0 -65
  210. package/src/lib/sidebar-store.ts +0 -113
  211. package/src/lib/theme-runtime.ts +0 -660
  212. package/src/lib/theme-store.ts +0 -67
  213. package/src/lib/tool-panel-store.ts +0 -121
  214. package/src/lib/use-new-conversation.test.tsx +0 -168
  215. package/src/lib/use-new-conversation.ts +0 -64
  216. package/src/ui/dialog.tsx +0 -109
  217. package/src/ui/dropdown-menu.tsx +0 -75
  218. package/src/ui/popover.tsx +0 -34
  219. package/src/ui/select.tsx +0 -15
  220. package/src/ui/tooltip.tsx +0 -30
  221. package/src/vite-env.d.ts +0 -1
  222. package/tsconfig.json +0 -16
  223. package/vitest.config.ts +0 -8
@@ -1,611 +0,0 @@
1
- import { act } from "react";
2
- import { createRoot, type Root } from "react-dom/client";
3
- import { MemoryRouter } from "react-router";
4
- import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
5
- import { AlertTriangle, FolderOpen, MessageSquare } from "lucide-react";
6
- import { useNavStore } from "../lib/nav-store.ts";
7
- import { useAppStore } from "../lib/app-store.ts";
8
- import { SIDEBAR_WIDTH, useSidebarStore } from "../lib/sidebar-store.ts";
9
- import type { NavGroup } from "../lib/nav-config.ts";
10
- import { TooltipProvider } from "../ui/tooltip.tsx";
11
-
12
- vi.mock("../lib/use-new-conversation.ts", () => ({
13
- useNewConversation: () => () => {},
14
- }));
15
-
16
- describe("Sidebar active state", () => {
17
- let container: HTMLDivElement;
18
- let root: Root;
19
- let originalActEnvironment: boolean | undefined;
20
- let originalInnerWidth: number;
21
- let originalMatchMedia: typeof window.matchMedia | undefined;
22
-
23
- beforeEach(() => {
24
- originalActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
25
- originalInnerWidth = window.innerWidth;
26
- originalMatchMedia = window.matchMedia;
27
- globalThis.IS_REACT_ACT_ENVIRONMENT = true;
28
- localStorage.clear();
29
- window.innerWidth = 1280;
30
- Object.defineProperty(window, "matchMedia", {
31
- configurable: true,
32
- writable: true,
33
- value:
34
- originalMatchMedia ||
35
- ((query: string) =>
36
- ({
37
- matches: false,
38
- media: query,
39
- onchange: null,
40
- addListener: () => {},
41
- removeListener: () => {},
42
- addEventListener: () => {},
43
- removeEventListener: () => {},
44
- dispatchEvent: () => false,
45
- }) as MediaQueryList),
46
- });
47
-
48
- const navConfig: NavGroup[] = [
49
- {
50
- label: "Pages",
51
- hideLabel: true,
52
- items: [
53
- { to: "/", label: "Chat", icon: MessageSquare },
54
- { to: "/spaces/demo", label: "Demo Workspace", icon: FolderOpen },
55
- ],
56
- },
57
- ];
58
-
59
- useNavStore.getState().setConfig(navConfig);
60
- useNavStore.setState({ footerActions: [] });
61
- useAppStore.setState({
62
- currentPage: "/spaces/demo",
63
- threadActive: false,
64
- contextItems: [],
65
- sentContext: {},
66
- highlights: [],
67
- selectedItemId: null,
68
- content: {},
69
- });
70
- useSidebarStore.setState({ isOpen: true, isMobileOpen: false, width: SIDEBAR_WIDTH });
71
-
72
- container = document.createElement("div");
73
- document.body.appendChild(container);
74
- root = createRoot(container);
75
- });
76
-
77
- afterEach(() => {
78
- act(() => {
79
- root.unmount();
80
- });
81
- container.remove();
82
- globalThis.IS_REACT_ACT_ENVIRONMENT = originalActEnvironment;
83
- window.innerWidth = originalInnerWidth;
84
- if (originalMatchMedia) {
85
- Object.defineProperty(window, "matchMedia", {
86
- configurable: true,
87
- writable: true,
88
- value: originalMatchMedia,
89
- });
90
- } else {
91
- // jsdom doesn't define this by default; remove our temporary override.
92
- // @ts-expect-error deleting configurable test override
93
- delete window.matchMedia;
94
- }
95
- });
96
-
97
- it("uses the neutral selected-row styling for the current nav item", async () => {
98
- const { Sidebar } = await import("./sidebar.tsx");
99
-
100
- await act(async () => {
101
- root.render(
102
- <MemoryRouter initialEntries={["/spaces/demo"]}>
103
- <TooltipProvider>
104
- <Sidebar />
105
- </TooltipProvider>
106
- </MemoryRouter>,
107
- );
108
- });
109
-
110
- const activeRow = Array.from(container.querySelectorAll<HTMLElement>("[class]")).find(
111
- (element) =>
112
- element.textContent?.includes("Demo Workspace") &&
113
- element.className.includes("bg-sidebar-selected"),
114
- );
115
-
116
- expect(activeRow).toBeTruthy();
117
- expect(activeRow?.className).toContain("bg-sidebar-selected");
118
- expect(activeRow?.className).not.toContain("bg-sidebar-primary/10");
119
- });
120
-
121
- it("renders first-level child leaves and folders at the same indent depth", async () => {
122
- const { Sidebar } = await import("./sidebar.tsx");
123
-
124
- const navConfig: NavGroup[] = [
125
- {
126
- label: "Workspaces",
127
- items: [
128
- {
129
- to: "/spaces/demo",
130
- label: "Demo Workspace",
131
- icon: FolderOpen,
132
- children: [
133
- { to: "/spaces/demo/documents", label: "Documents", icon: FolderOpen },
134
- {
135
- to: "/spaces/demo/views",
136
- label: "Views",
137
- icon: FolderOpen,
138
- children: [
139
- { to: "/spaces/demo/views/current", label: "Current", icon: FolderOpen },
140
- ],
141
- },
142
- ],
143
- },
144
- ],
145
- },
146
- ];
147
-
148
- useNavStore.getState().setConfig(navConfig);
149
- useAppStore.setState({
150
- currentPage: "/spaces/demo/documents",
151
- threadActive: false,
152
- contextItems: [],
153
- sentContext: {},
154
- highlights: [],
155
- selectedItemId: null,
156
- content: {},
157
- });
158
-
159
- await act(async () => {
160
- root.render(
161
- <MemoryRouter initialEntries={["/spaces/demo/documents"]}>
162
- <TooltipProvider>
163
- <Sidebar />
164
- </TooltipProvider>
165
- </MemoryRouter>,
166
- );
167
- });
168
-
169
- const documentsButton = Array.from(container.querySelectorAll("button")).find((element) =>
170
- element.textContent?.includes("Documents"),
171
- );
172
- const viewsButton = Array.from(container.querySelectorAll("button")).find((element) =>
173
- element.textContent?.includes("Views"),
174
- );
175
-
176
- expect(documentsButton).toBeTruthy();
177
- expect(viewsButton).toBeTruthy();
178
- expect(documentsButton?.className).toContain("pl-7");
179
- expect(viewsButton?.className).toContain("pl-7");
180
- expect(documentsButton?.className).not.toContain("pl-5");
181
- });
182
-
183
- it("collapsed rail wraps each nav group and adds a top border between groups", async () => {
184
- const { Sidebar } = await import("./sidebar.tsx");
185
-
186
- const navConfig: NavGroup[] = [
187
- {
188
- label: "Pages",
189
- hideLabel: true,
190
- items: [{ to: "/", label: "Chat", icon: MessageSquare }],
191
- },
192
- {
193
- label: "Workspaces",
194
- items: [{ to: "/spaces/demo", label: "Demo Workspace", icon: FolderOpen }],
195
- },
196
- {
197
- label: "Admin",
198
- items: [{ to: "/spaces/admin/workspaces", label: "Workspaces", icon: FolderOpen }],
199
- },
200
- ];
201
-
202
- useNavStore.getState().setConfig(navConfig);
203
- useAppStore.setState({
204
- currentPage: "/",
205
- threadActive: false,
206
- contextItems: [],
207
- sentContext: {},
208
- highlights: [],
209
- selectedItemId: null,
210
- content: {},
211
- });
212
- // Collapse the sidebar so the rail renders.
213
- useSidebarStore.setState({ isOpen: false, isMobileOpen: false, width: SIDEBAR_WIDTH });
214
-
215
- await act(async () => {
216
- root.render(
217
- <MemoryRouter initialEntries={["/"]}>
218
- <TooltipProvider>
219
- <Sidebar />
220
- </TooltipProvider>
221
- </MemoryRouter>,
222
- );
223
- });
224
-
225
- // Sidebar renders twice (mobile drawer + desktop rail) but only the
226
- // desktop rail uses ``collapsed={true}`` — the mobile drawer always
227
- // renders ``collapsed={false}``, so it doesn't emit collapsed-rail
228
- // group wrappers. Grouping the matched divs by owning <aside> handles
229
- // the (unlikely) case where multiple instances do contribute.
230
- // Filter out the outer rail container itself (which has these classes
231
- // but additionally carries ``py-3``).
232
- const wrappersByAside = new Map<Element, HTMLDivElement[]>();
233
- container
234
- .querySelectorAll<HTMLDivElement>("div.flex.flex-col.items-center.gap-1")
235
- .forEach((el) => {
236
- if (el.className.includes("py-3")) return;
237
- const aside = el.closest("aside");
238
- if (!aside) return;
239
- const list = wrappersByAside.get(aside) ?? [];
240
- list.push(el);
241
- wrappersByAside.set(aside, list);
242
- });
243
-
244
- // Each rendered sidebar instance must have one wrapper per nav group —
245
- // first plain, second + third with the separator border.
246
- const perAside = Array.from(wrappersByAside.values());
247
- expect(perAside.length).toBeGreaterThan(0);
248
- for (const wrappers of perAside) {
249
- expect(wrappers.length).toBe(3);
250
- expect(wrappers[0].className).not.toContain("border-t");
251
- expect(wrappers[1].className).toContain("border-t");
252
- expect(wrappers[1].className).toContain("mt-2");
253
- expect(wrappers[1].className).toContain("pt-2");
254
- expect(wrappers[2].className).toContain("border-t");
255
- }
256
- });
257
-
258
- it("collapsed rail keeps the first VISIBLE group plain when an earlier group renders nothing", async () => {
259
- const { Sidebar } = await import("./sidebar.tsx");
260
-
261
- // The first group supplies a `collapsedRail` that returns no items —
262
- // simulates an early-load state where a module hasn't populated yet.
263
- // The rendering must skip it entirely AND must not push a border-top
264
- // onto whatever group renders first.
265
- const navConfig: NavGroup[] = [
266
- {
267
- label: "EmptyFirst",
268
- items: [{ to: "/empty", label: "Empty", icon: MessageSquare }],
269
- collapsedRail: () => [],
270
- },
271
- {
272
- label: "Workspaces",
273
- items: [{ to: "/spaces/demo", label: "Demo", icon: FolderOpen }],
274
- },
275
- {
276
- label: "Admin",
277
- items: [{ to: "/spaces/admin", label: "Admin", icon: FolderOpen }],
278
- },
279
- ];
280
-
281
- useNavStore.getState().setConfig(navConfig);
282
- useAppStore.setState({
283
- currentPage: "/",
284
- threadActive: false,
285
- contextItems: [],
286
- sentContext: {},
287
- highlights: [],
288
- selectedItemId: null,
289
- content: {},
290
- });
291
- useSidebarStore.setState({ isOpen: false, isMobileOpen: false, width: SIDEBAR_WIDTH });
292
-
293
- await act(async () => {
294
- root.render(
295
- <MemoryRouter initialEntries={["/"]}>
296
- <TooltipProvider>
297
- <Sidebar />
298
- </TooltipProvider>
299
- </MemoryRouter>,
300
- );
301
- });
302
-
303
- const wrappersByAside = new Map<Element, HTMLDivElement[]>();
304
- container
305
- .querySelectorAll<HTMLDivElement>("div.flex.flex-col.items-center.gap-1")
306
- .forEach((el) => {
307
- if (el.className.includes("py-3")) return;
308
- const aside = el.closest("aside");
309
- if (!aside) return;
310
- const list = wrappersByAside.get(aside) ?? [];
311
- list.push(el);
312
- wrappersByAside.set(aside, list);
313
- });
314
-
315
- const perAside = Array.from(wrappersByAside.values());
316
- expect(perAside.length).toBeGreaterThan(0);
317
- for (const wrappers of perAside) {
318
- // Empty group skipped → 2 visible wrappers
319
- expect(wrappers.length).toBe(2);
320
- // First VISIBLE wrapper has no separator (despite being array-idx 1)
321
- expect(wrappers[0].className).not.toContain("border-t");
322
- // Second visible wrapper carries the divider
323
- expect(wrappers[1].className).toContain("border-t");
324
- }
325
- });
326
-
327
- it("renders NavItem.statusIcon with tone class + tooltip label", async () => {
328
- // The statusIcon slot is consumer-driven (e.g. ``spaces-web-ui``
329
- // stamps a warning triangle on workspaces with failed indexing).
330
- // This test pins three things the consumer relies on:
331
- // 1. The supplied LucideIcon component is rendered.
332
- // 2. The tone maps to the right Tailwind class (``warning`` →
333
- // ``text-warning``).
334
- // 3. The ``label`` lands on both ``aria-label`` and the
335
- // ``title`` attribute so the icon gets a hover tooltip and
336
- // a screen-reader announcement without the consumer having
337
- // to wire anything else.
338
- const { Sidebar } = await import("./sidebar.tsx");
339
-
340
- const navConfig: NavGroup[] = [
341
- {
342
- label: "Workspaces",
343
- items: [
344
- {
345
- to: "/spaces/wsA",
346
- label: "Workspace With Failures",
347
- icon: FolderOpen,
348
- statusIcon: {
349
- icon: AlertTriangle,
350
- tone: "warning",
351
- label: "3 documents failed to index",
352
- },
353
- },
354
- {
355
- to: "/spaces/wsB",
356
- label: "Healthy Workspace",
357
- icon: FolderOpen,
358
- },
359
- ],
360
- },
361
- ];
362
- useNavStore.getState().setConfig(navConfig);
363
- useAppStore.setState({
364
- currentPage: "/spaces/wsA",
365
- threadActive: false,
366
- contextItems: [],
367
- sentContext: {},
368
- highlights: [],
369
- selectedItemId: null,
370
- content: {},
371
- });
372
-
373
- await act(async () => {
374
- root.render(
375
- <MemoryRouter initialEntries={["/spaces/wsA"]}>
376
- <TooltipProvider>
377
- <Sidebar />
378
- </TooltipProvider>
379
- </MemoryRouter>,
380
- );
381
- });
382
-
383
- // The warning row carries the icon wrapped in a span. The label
384
- // lands on both ``aria-label`` (screen reader) and ``title``
385
- // (hover tooltip) on the wrapping span; the inner svg carries
386
- // the tone class.
387
- const warningWrappers = container.querySelectorAll<HTMLElement>(
388
- 'span[aria-label="3 documents failed to index"]',
389
- );
390
- expect(warningWrappers.length).toBeGreaterThan(0);
391
- const wrapper = warningWrappers[0];
392
- expect(wrapper.getAttribute("title")).toBe("3 documents failed to index");
393
- expect(wrapper.querySelector("svg.text-warning")).not.toBeNull();
394
-
395
- // No statusIcon on the healthy workspace ⇒ its leaf row
396
- // contains no warning svg. Workspaces are rendered as NavLink
397
- // anchors (button is for folders), so scope to ``a, button``
398
- // and reject ancestor containers that happen to enclose BOTH
399
- // workspaces.
400
- const healthyRowLeaves = Array.from(
401
- container.querySelectorAll<HTMLElement>("a, button"),
402
- ).filter((el) => el.textContent?.includes("Healthy Workspace"));
403
- expect(healthyRowLeaves.length).toBeGreaterThan(0);
404
- for (const leaf of healthyRowLeaves) {
405
- expect(leaf.querySelectorAll("svg.text-warning").length).toBe(0);
406
- }
407
- });
408
- });
409
-
410
- describe("Sidebar group priority + collapse + status bubble-up", () => {
411
- let container: HTMLDivElement;
412
- let root: Root;
413
- let originalActEnvironment: boolean | undefined;
414
- let originalInnerWidth: number;
415
- let originalMatchMedia: typeof window.matchMedia | undefined;
416
-
417
- beforeEach(() => {
418
- originalActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
419
- originalInnerWidth = window.innerWidth;
420
- originalMatchMedia = window.matchMedia;
421
- globalThis.IS_REACT_ACT_ENVIRONMENT = true;
422
- localStorage.clear();
423
- window.innerWidth = 1280;
424
- Object.defineProperty(window, "matchMedia", {
425
- configurable: true,
426
- writable: true,
427
- value:
428
- originalMatchMedia ||
429
- ((query: string) =>
430
- ({
431
- matches: false,
432
- media: query,
433
- onchange: null,
434
- addListener: () => {},
435
- removeListener: () => {},
436
- addEventListener: () => {},
437
- removeEventListener: () => {},
438
- dispatchEvent: () => false,
439
- })),
440
- });
441
- container = document.createElement("div");
442
- document.body.appendChild(container);
443
- act(() => {
444
- root = createRoot(container);
445
- });
446
- useSidebarStore.setState({ collapsed: false, width: SIDEBAR_WIDTH });
447
- useNavStore.setState({ groups: [], config: [] } as Parameters<
448
- typeof useNavStore.setState
449
- >[0]);
450
- });
451
-
452
- afterEach(() => {
453
- act(() => {
454
- root.unmount();
455
- });
456
- container.remove();
457
- globalThis.IS_REACT_ACT_ENVIRONMENT = originalActEnvironment;
458
- window.innerWidth = originalInnerWidth;
459
- if (originalMatchMedia) {
460
- Object.defineProperty(window, "matchMedia", {
461
- configurable: true,
462
- writable: true,
463
- value: originalMatchMedia,
464
- });
465
- } else {
466
- // @ts-expect-error deleting configurable test override
467
- delete window.matchMedia;
468
- }
469
- });
470
-
471
- function indexOfLabel(label: string): number {
472
- const headers = Array.from(
473
- container.querySelectorAll<HTMLElement>("[data-nav-group-label]"),
474
- );
475
- return headers.findIndex(
476
- (el) => el.getAttribute("data-nav-group-label") === label,
477
- );
478
- }
479
-
480
- it("sorts groups by NavGroup.priority desc (higher priority first)", async () => {
481
- const { Sidebar } = await import("./sidebar.tsx");
482
- const navConfig: NavGroup[] = [
483
- {
484
- label: "Low",
485
- priority: 10,
486
- items: [{ to: "/low", label: "Low item", icon: FolderOpen }],
487
- },
488
- {
489
- label: "High",
490
- priority: 100,
491
- items: [{ to: "/high", label: "High item", icon: FolderOpen }],
492
- },
493
- {
494
- label: "Mid",
495
- priority: 50,
496
- items: [{ to: "/mid", label: "Mid item", icon: FolderOpen }],
497
- },
498
- ];
499
- useNavStore.getState().setConfig(navConfig);
500
-
501
- await act(async () => {
502
- root.render(
503
- <MemoryRouter initialEntries={["/"]}>
504
- <TooltipProvider>
505
- <Sidebar />
506
- </TooltipProvider>
507
- </MemoryRouter>,
508
- );
509
- });
510
-
511
- const idxHigh = indexOfLabel("High");
512
- const idxMid = indexOfLabel("Mid");
513
- const idxLow = indexOfLabel("Low");
514
- expect(idxHigh).toBeGreaterThanOrEqual(0);
515
- expect(idxMid).toBeGreaterThan(idxHigh);
516
- expect(idxLow).toBeGreaterThan(idxMid);
517
- });
518
-
519
- it("collapsing a group hides its children and persists to localStorage", async () => {
520
- const { Sidebar } = await import("./sidebar.tsx");
521
- const navConfig: NavGroup[] = [
522
- {
523
- label: "Recents",
524
- collapsible: true,
525
- items: [
526
- { to: "/chat/1", label: "First chat", icon: MessageSquare },
527
- { to: "/chat/2", label: "Second chat", icon: MessageSquare },
528
- ],
529
- },
530
- ];
531
- useNavStore.getState().setConfig(navConfig);
532
-
533
- await act(async () => {
534
- root.render(
535
- <MemoryRouter initialEntries={["/"]}>
536
- <TooltipProvider>
537
- <Sidebar />
538
- </TooltipProvider>
539
- </MemoryRouter>,
540
- );
541
- });
542
-
543
- // Children visible initially (count varies — the sidebar may
544
- // also render an icon-only tooltip mirror in jsdom; we only care
545
- // that "First chat" appears at least once now and disappears
546
- // after collapse).
547
- const childTextBefore = container.textContent ?? "";
548
- expect(childTextBefore).toContain("First chat");
549
- expect(childTextBefore).toContain("Second chat");
550
-
551
- // Click the group header toggle button.
552
- const headerToggle = container.querySelector<HTMLElement>(
553
- '[data-nav-group-label="Recents"] [data-nav-group-toggle]',
554
- );
555
- expect(headerToggle).toBeTruthy();
556
- await act(async () => {
557
- headerToggle!.dispatchEvent(new MouseEvent("click", { bubbles: true }));
558
- });
559
-
560
- // Children gone after collapse.
561
- const childTextAfter = container.textContent ?? "";
562
- expect(childTextAfter).not.toContain("First chat");
563
- expect(childTextAfter).not.toContain("Second chat");
564
-
565
- // Persisted to localStorage under the generic package-scoped key.
566
- const persisted = JSON.parse(
567
- localStorage.getItem("sidebar-nav-collapsed") ?? "{}",
568
- );
569
- expect(persisted.Recents).toBe(true);
570
- });
571
-
572
- it("child statusDot bubbles up to the collapsed group header", async () => {
573
- const { Sidebar } = await import("./sidebar.tsx");
574
- const navConfig: NavGroup[] = [
575
- {
576
- label: "Recents",
577
- collapsible: true,
578
- defaultCollapsed: true,
579
- items: [
580
- {
581
- to: "/chat/active",
582
- label: "Active chat",
583
- icon: MessageSquare,
584
- statusDot: { tone: "primary", srLabel: "active" },
585
- },
586
- { to: "/chat/idle", label: "Idle chat", icon: MessageSquare },
587
- ],
588
- },
589
- ];
590
- useNavStore.getState().setConfig(navConfig);
591
-
592
- await act(async () => {
593
- root.render(
594
- <MemoryRouter initialEntries={["/"]}>
595
- <TooltipProvider>
596
- <Sidebar />
597
- </TooltipProvider>
598
- </MemoryRouter>,
599
- );
600
- });
601
-
602
- const header = container.querySelector<HTMLElement>(
603
- '[data-nav-group-label="Recents"]',
604
- );
605
- expect(header).toBeTruthy();
606
- // The bubbled-up dot is rendered with the same status-dot marker
607
- // as a child status dot; scope the lookup to the header.
608
- const bubble = header!.querySelector<HTMLElement>("[data-nav-status-dot]");
609
- expect(bubble).toBeTruthy();
610
- });
611
- });