@tangle-network/ui 11.11.8 → 11.13.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 (279) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/_virtual/_rolldown/runtime.js +13 -0
  3. package/dist/auth/auth.d.ts +53 -0
  4. package/dist/{auth-DOETfnd9.js → auth/auth.js} +7 -37
  5. package/dist/auth/login-layout.d.ts +20 -0
  6. package/dist/auth/login-layout.js +36 -0
  7. package/dist/auth.d.ts +2 -1
  8. package/dist/auth.js +2 -1
  9. package/dist/chat/agent-timeline.d.ts +83 -0
  10. package/dist/chat/agent-timeline.js +236 -0
  11. package/dist/chat/chat-container.d.ts +51 -0
  12. package/dist/chat/chat-container.js +309 -0
  13. package/dist/chat/chat-message.d.ts +22 -0
  14. package/dist/chat/chat-message.js +38 -0
  15. package/dist/chat/message-list.d.ts +32 -0
  16. package/dist/chat/message-list.js +37 -0
  17. package/dist/chat/thinking-indicator.d.ts +6 -0
  18. package/dist/chat/thinking-indicator.js +38 -0
  19. package/dist/chat/user-message.d.ts +21 -0
  20. package/dist/chat/user-message.js +42 -0
  21. package/dist/chat.d.ts +6 -1
  22. package/dist/chat.js +6 -1
  23. package/dist/editor/document-editor-pane.d.ts +37 -0
  24. package/dist/editor/document-editor-pane.js +199 -0
  25. package/dist/editor/editor-lazy.js +37 -0
  26. package/dist/editor/editor-loading.js +21 -0
  27. package/dist/editor/editor-peers.d.ts +6 -0
  28. package/dist/editor/editor-peers.js +155 -0
  29. package/dist/{document-editor-pane-DHM8DDhb.d.ts → editor/editor-provider.d.ts} +10 -47
  30. package/dist/editor/editor-provider.js +253 -0
  31. package/dist/editor/editor-toolbar.d.ts +11 -0
  32. package/dist/editor/editor-toolbar.js +106 -0
  33. package/dist/editor/markdown-conversion.js +23 -0
  34. package/dist/editor/markdown-document-editor.js +103 -0
  35. package/dist/editor/tiptap-editor.d.ts +45 -0
  36. package/dist/editor/tiptap-editor.js +271 -0
  37. package/dist/editor/use-editor.d.ts +67 -0
  38. package/dist/editor/use-editor.js +158 -0
  39. package/dist/editor.d.ts +6 -119
  40. package/dist/editor.js +5 -1
  41. package/dist/files/file-artifact-pane.d.ts +47 -0
  42. package/dist/files/file-artifact-pane.js +114 -0
  43. package/dist/files/file-format.d.ts +51 -0
  44. package/dist/{file-format-BgXbr-Ib.js → files/file-format.js} +1 -1
  45. package/dist/files/file-preview.d.ts +27 -0
  46. package/dist/files/file-preview.js +359 -0
  47. package/dist/files/file-tabs.d.ts +19 -0
  48. package/dist/files/file-tabs.js +48 -0
  49. package/dist/files/file-tree.d.ts +31 -0
  50. package/dist/files/file-tree.js +135 -0
  51. package/dist/files/rich-file-tree.d.ts +62 -0
  52. package/dist/files/rich-file-tree.js +122 -0
  53. package/dist/files.d.ts +6 -1
  54. package/dist/files.js +6 -2
  55. package/dist/hooks/use-auth.d.ts +40 -0
  56. package/dist/hooks/use-auth.js +91 -0
  57. package/dist/hooks/use-auto-scroll.d.ts +11 -0
  58. package/dist/hooks/use-auto-scroll.js +48 -0
  59. package/dist/hooks/use-dropdown-menu.d.ts +12 -0
  60. package/dist/hooks/use-dropdown-menu.js +31 -0
  61. package/dist/hooks/use-live-time.d.ts +7 -0
  62. package/dist/hooks/use-live-time.js +22 -0
  63. package/dist/hooks/use-realtime-session.d.ts +32 -0
  64. package/dist/hooks/use-realtime-session.js +235 -0
  65. package/dist/hooks/use-run-collapse-state.d.ts +13 -0
  66. package/dist/hooks/use-run-collapse-state.js +24 -0
  67. package/dist/hooks/use-run-groups.d.ts +11 -0
  68. package/dist/hooks/use-run-groups.js +85 -0
  69. package/dist/hooks/use-sdk-session.d.ts +59 -0
  70. package/dist/hooks/use-sdk-session.js +322 -0
  71. package/dist/hooks/use-sse-stream.d.ts +124 -0
  72. package/dist/hooks/use-sse-stream.js +229 -0
  73. package/dist/hooks/use-tool-call-stream.d.ts +23 -0
  74. package/dist/hooks/use-tool-call-stream.js +75 -0
  75. package/dist/hooks.d.ts +11 -49
  76. package/dist/hooks.js +10 -110
  77. package/dist/index.d.ts +99 -25
  78. package/dist/index.js +86 -36
  79. package/dist/lib/focus.d.ts +55 -0
  80. package/dist/{focus-BcE_SHqz.js → lib/focus.js} +1 -1
  81. package/dist/lib/utils.d.ts +4 -0
  82. package/dist/{utils-C_uf36nf.js → lib/utils.js} +1 -1
  83. package/dist/{code-block-B9miUPht.d.ts → markdown/code-block.d.ts} +5 -6
  84. package/dist/{code-block-B36KVUSN.js → markdown/code-block.js} +22 -47
  85. package/dist/{markdown-C0atSabr.d.ts → markdown/markdown.d.ts} +4 -5
  86. package/dist/{markdown-Db08KAYF.js → markdown/markdown.js} +3 -3
  87. package/dist/markdown.d.ts +2 -2
  88. package/dist/markdown.js +2 -2
  89. package/dist/{openui-artifact-renderer-fkBQaoLO.d.ts → openui/openui-artifact-renderer.d.ts} +20 -21
  90. package/dist/{openui-artifact-renderer-DG_i2lOR.js → openui/openui-artifact-renderer.js} +9 -7
  91. package/dist/{schema-BpW9rSSG.js → openui/schema-compiled.js} +2 -354
  92. package/dist/{schema-DAstk-J4.d.ts → openui/schema-data.d.ts} +3 -24
  93. package/dist/openui/schema-data.js +206 -0
  94. package/dist/openui-schema.d.ts +22 -3
  95. package/dist/openui-schema.js +138 -1
  96. package/dist/openui.d.ts +3 -2
  97. package/dist/openui.js +3 -2
  98. package/dist/{artifact-pane-BL_DFWNl.d.ts → primitives/artifact-pane.d.ts} +3 -4
  99. package/dist/{artifact-pane-DMXT9a8U.js → primitives/artifact-pane.js} +2 -2
  100. package/dist/primitives/avatar.d.ts +8 -0
  101. package/dist/primitives/avatar.js +25 -0
  102. package/dist/primitives/badge.d.ts +22 -0
  103. package/dist/primitives/badge.js +47 -0
  104. package/dist/{button-yoIv77e2.d.ts → primitives/button.d.ts} +2 -2
  105. package/dist/primitives/button.js +113 -0
  106. package/dist/primitives/card.d.ts +18 -0
  107. package/dist/primitives/card.js +52 -0
  108. package/dist/primitives/code-block.d.ts +6 -0
  109. package/dist/primitives/code-block.js +13 -0
  110. package/dist/primitives/dialog.d.ts +23 -0
  111. package/dist/primitives/dialog.js +59 -0
  112. package/dist/primitives/drop-zone.d.ts +23 -0
  113. package/dist/primitives/drop-zone.js +104 -0
  114. package/dist/primitives/dropdown-menu.d.ts +29 -0
  115. package/dist/{dropdown-menu-BpcLa5vF.js → primitives/dropdown-menu.js} +2 -23
  116. package/dist/primitives/empty-state.d.ts +11 -0
  117. package/dist/primitives/empty-state.js +32 -0
  118. package/dist/primitives/heading.d.ts +23 -0
  119. package/dist/primitives/heading.js +38 -0
  120. package/dist/primitives/input.d.ts +23 -0
  121. package/dist/primitives/input.js +110 -0
  122. package/dist/primitives/label.d.ts +6 -0
  123. package/dist/primitives/label.js +16 -0
  124. package/dist/primitives/metric-strip.d.ts +48 -0
  125. package/dist/primitives/metric-strip.js +52 -0
  126. package/dist/primitives/page-header.d.ts +25 -0
  127. package/dist/primitives/page-header.js +47 -0
  128. package/dist/primitives/page-shell.d.ts +14 -0
  129. package/dist/primitives/page-shell.js +17 -0
  130. package/dist/primitives/progress.d.ts +9 -0
  131. package/dist/primitives/progress.js +29 -0
  132. package/dist/primitives/segmented-control.d.ts +40 -0
  133. package/dist/primitives/segmented-control.js +57 -0
  134. package/dist/primitives/select.d.ts +15 -0
  135. package/dist/primitives/select.js +74 -0
  136. package/dist/primitives/sidebar-drop-zone.d.ts +21 -0
  137. package/dist/primitives/sidebar-drop-zone.js +88 -0
  138. package/dist/primitives/skeleton.d.ts +10 -0
  139. package/dist/primitives/skeleton.js +50 -0
  140. package/dist/primitives/stat-card.d.ts +16 -0
  141. package/dist/primitives/stat-card.js +60 -0
  142. package/dist/primitives/status-pill.d.ts +44 -0
  143. package/dist/primitives/status-pill.js +119 -0
  144. package/dist/primitives/switch.d.ts +6 -0
  145. package/dist/primitives/switch.js +16 -0
  146. package/dist/primitives/table.d.ts +22 -0
  147. package/dist/primitives/table.js +63 -0
  148. package/dist/primitives/tabs.d.ts +13 -0
  149. package/dist/{tabs-BVHR6NkO.js → primitives/tabs.js} +3 -3
  150. package/dist/primitives/terminal-display.d.ts +25 -0
  151. package/dist/primitives/terminal-display.js +109 -0
  152. package/dist/primitives/text-shimmer.d.ts +16 -0
  153. package/dist/{text-shimmer-BUUo6I-g.js → primitives/text-shimmer.js} +2 -2
  154. package/dist/primitives/theme-toggle.d.ts +11 -0
  155. package/dist/primitives/theme-toggle.js +154 -0
  156. package/dist/primitives/toast.d.ts +29 -0
  157. package/dist/primitives/toast.js +142 -0
  158. package/dist/primitives/toolbar.d.ts +55 -0
  159. package/dist/primitives/toolbar.js +37 -0
  160. package/dist/primitives/upload-progress.d.ts +23 -0
  161. package/dist/primitives/upload-progress.js +72 -0
  162. package/dist/primitives.d.ts +35 -5
  163. package/dist/primitives.js +35 -9
  164. package/dist/{index-COgqsFAQ.d.ts → redaction/redacted-document.d.ts} +6 -7
  165. package/dist/{redaction-CGGB2ENG.js → redaction/redacted-document.js} +2 -2
  166. package/dist/redaction.d.ts +1 -1
  167. package/dist/redaction.js +1 -1
  168. package/dist/run/expandable-content.js +41 -0
  169. package/dist/run/expanded-tool-detail.d.ts +12 -0
  170. package/dist/run/expanded-tool-detail.js +163 -0
  171. package/dist/run/inline-thinking-item.d.ts +11 -0
  172. package/dist/run/inline-thinking-item.js +60 -0
  173. package/dist/run/inline-tool-item.d.ts +25 -0
  174. package/dist/run/inline-tool-item.js +63 -0
  175. package/dist/run/run-group.d.ts +26 -0
  176. package/dist/{run-group-BWGeesgs.js → run/run-group.js} +9 -60
  177. package/dist/run/run-item-primitives.d.ts +5 -0
  178. package/dist/{run-ZsIKPA6J.js → run/run-item-primitives.js} +2 -2
  179. package/dist/run/run-row-shell.d.ts +53 -0
  180. package/dist/run/run-row-shell.js +114 -0
  181. package/dist/{tool-call-feed-BeB1_4pX.d.ts → run/tool-call-feed.d.ts} +7 -12
  182. package/dist/{tool-call-feed-BpmMLpPX.js → run/tool-call-feed.js} +4 -4
  183. package/dist/run/tool-call-step.d.ts +5 -0
  184. package/dist/run/tool-call-step.js +82 -0
  185. package/dist/run.d.ts +8 -2
  186. package/dist/run.js +7 -4
  187. package/dist/sdk-hooks.d.ts +8 -1
  188. package/dist/sdk-hooks.js +8 -2
  189. package/dist/stores/active-sessions-store.d.ts +83 -0
  190. package/dist/{active-sessions-store-Gt1R0rFG.js → stores/active-sessions-store.js} +1 -1
  191. package/dist/stores/chat-store.d.ts +14 -0
  192. package/dist/stores/chat-store.js +33 -0
  193. package/dist/stores.d.ts +3 -16
  194. package/dist/stores.js +2 -33
  195. package/dist/tool-previews/command-preview.d.ts +11 -0
  196. package/dist/tool-previews/command-preview.js +91 -0
  197. package/dist/tool-previews/diff-preview.d.ts +7 -0
  198. package/dist/tool-previews/diff-preview.js +76 -0
  199. package/dist/tool-previews/glob-results-preview.d.ts +7 -0
  200. package/dist/tool-previews/glob-results-preview.js +53 -0
  201. package/dist/tool-previews/grep-results-preview.d.ts +7 -0
  202. package/dist/tool-previews/grep-results-preview.js +99 -0
  203. package/dist/tool-previews/preview-primitives.js +58 -0
  204. package/dist/tool-previews/question-preview.d.ts +7 -0
  205. package/dist/tool-previews/question-preview.js +63 -0
  206. package/dist/tool-previews/web-search-preview.d.ts +7 -0
  207. package/dist/tool-previews/web-search-preview.js +78 -0
  208. package/dist/tool-previews/write-file-preview.d.ts +11 -0
  209. package/dist/tool-previews/write-file-preview.js +52 -0
  210. package/dist/tool-previews.d.ts +7 -1
  211. package/dist/tool-previews.js +7 -1
  212. package/dist/{branding-26d5ARIr.d.ts → types/branding.d.ts} +2 -3
  213. package/dist/{message-vhUI0c5G.d.ts → types/message.d.ts} +2 -3
  214. package/dist/{parts-5mhy9yJ3.d.ts → types/parts.d.ts} +8 -9
  215. package/dist/{run-OyWmIDqu.d.ts → types/run.d.ts} +9 -10
  216. package/dist/{tool-display-DgmL5rIU.d.ts → types/tool-display.d.ts} +5 -6
  217. package/dist/types.d.ts +5 -5
  218. package/dist/utils/copy-text.d.ts +7 -0
  219. package/dist/{utils-Bl_CZ0Qp.js → utils/copy-text.js} +1 -11
  220. package/dist/utils/format.d.ts +22 -0
  221. package/dist/{format-DfcLWqMa.js → utils/format.js} +1 -1
  222. package/dist/utils/time-ago.d.ts +3 -0
  223. package/dist/utils/time-ago.js +11 -0
  224. package/dist/utils/tool-display.d.ts +16 -0
  225. package/dist/{tool-display-BAuAVa43.js → utils/tool-display.js} +1 -1
  226. package/dist/utils.d.ts +6 -1
  227. package/dist/utils.js +6 -5
  228. package/package.json +2 -2
  229. package/src/editor/document-editor-pane.tsx +4 -1
  230. package/src/files/file-artifact-pane.test.tsx +23 -0
  231. package/src/files/file-artifact-pane.tsx +3 -0
  232. package/src/files/rich-file-tree.stories.tsx +1 -1
  233. package/src/files/rich-file-tree.tsx +12 -9
  234. package/src/markdown/code-block.test.tsx +46 -2
  235. package/src/markdown/code-block.tsx +23 -60
  236. package/src/markdown/markdown.test.tsx +3 -2
  237. package/src/primitives/button.stories.tsx +37 -1
  238. package/src/primitives/button.test.tsx +230 -0
  239. package/src/primitives/button.tsx +62 -14
  240. package/src/primitives/card-anatomy.stories.tsx +51 -0
  241. package/src/primitives/card.test.tsx +71 -0
  242. package/src/primitives/card.tsx +25 -13
  243. package/src/primitives/heading.stories.tsx +35 -0
  244. package/src/primitives/heading.test.tsx +56 -0
  245. package/src/primitives/heading.tsx +76 -0
  246. package/src/primitives/index.ts +8 -0
  247. package/src/primitives/input.stories.tsx +28 -0
  248. package/src/primitives/input.test.tsx +161 -0
  249. package/src/primitives/input.tsx +34 -28
  250. package/src/primitives/page-header.stories.tsx +51 -0
  251. package/src/primitives/page-header.test.tsx +48 -0
  252. package/src/primitives/page-header.tsx +35 -35
  253. package/src/primitives/page-shell.stories.tsx +89 -0
  254. package/src/primitives/page-shell.test.tsx +24 -0
  255. package/src/primitives/page-shell.tsx +26 -0
  256. package/src/primitives/table-wrapper.stories.tsx +54 -0
  257. package/src/primitives/table.test.tsx +72 -0
  258. package/src/primitives/table.tsx +33 -12
  259. package/src/primitives/theme-toggle.test.tsx +167 -0
  260. package/src/primitives/theme-toggle.tsx +111 -39
  261. package/dist/active-sessions-store-nl70ZnMC.d.ts +0 -84
  262. package/dist/button-C87nEe2R.js +0 -78
  263. package/dist/chat-DDvF2oGQ.js +0 -672
  264. package/dist/diff-preview-BcK3nRXb.js +0 -533
  265. package/dist/document-editor-pane-CsFbH2qo.js +0 -1279
  266. package/dist/files--Unn___t.js +0 -752
  267. package/dist/index-2UntyjBb.d.ts +0 -52
  268. package/dist/index-BgUHilZm.d.ts +0 -108
  269. package/dist/index-CPDET_s1.d.ts +0 -549
  270. package/dist/index-Cg6gYAqh.d.ts +0 -73
  271. package/dist/index-Cs7j8V7l.d.ts +0 -203
  272. package/dist/index-CyGSLGVF.d.ts +0 -234
  273. package/dist/index-DjKxO357.d.ts +0 -128
  274. package/dist/primitives-Bx59fT-U.js +0 -1239
  275. package/dist/table-Bss73DhB.js +0 -146
  276. package/dist/tool-call-step-DNebqhmX.js +0 -434
  277. package/dist/use-auto-scroll-C5Pjeqon.js +0 -153
  278. package/dist/use-sse-stream-DjqRuDft.js +0 -884
  279. package/dist/use-sse-stream-Dm6R5YWz.d.ts +0 -282
@@ -1,18 +1,39 @@
1
1
  import * as React from "react";
2
2
  import { cn } from "../lib/utils";
3
3
 
4
- const Table = React.forwardRef<
5
- HTMLTableElement,
6
- React.HTMLAttributes<HTMLTableElement>
7
- >(({ className, ...props }, ref) => (
8
- <div className="relative w-full overflow-auto">
9
- <table
10
- ref={ref}
11
- className={cn("w-full caption-bottom text-sm", className)}
12
- {...props}
13
- />
14
- </div>
15
- ));
4
+ export interface TableProps extends React.TableHTMLAttributes<HTMLTableElement> {
5
+ /** Keep the existing scroll wrapper by default; false returns only the native table. */
6
+ wrapper?: boolean;
7
+ /**
8
+ * Attributes for the wrapper, not the table (ignored when wrapper is false).
9
+ * For an overflowing table, supply tabIndex and an accessible region name.
10
+ * No extra tab stop or unnamed landmark is imposed on every small table.
11
+ */
12
+ wrapperProps?: Omit<React.HTMLAttributes<HTMLDivElement>, "children" | "dangerouslySetInnerHTML">;
13
+ }
14
+
15
+ const Table = React.forwardRef<HTMLTableElement, TableProps>(
16
+ ({ className, wrapper = true, wrapperProps, ...props }, ref) => {
17
+ const table = (
18
+ <table
19
+ ref={ref}
20
+ className={cn("w-full caption-bottom text-sm", className)}
21
+ {...props}
22
+ />
23
+ );
24
+ if (!wrapper) return table;
25
+
26
+ const { className: wrapperClassName, ...attributes } = wrapperProps ?? {};
27
+ return (
28
+ <div
29
+ {...attributes}
30
+ className={cn("relative w-full overflow-auto", wrapperClassName)}
31
+ >
32
+ {table}
33
+ </div>
34
+ );
35
+ },
36
+ );
16
37
  Table.displayName = "Table";
17
38
 
18
39
  const TableHeader = React.forwardRef<
@@ -0,0 +1,167 @@
1
+ import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
2
+ import { hydrateRoot } from "react-dom/client";
3
+ import { renderToString } from "react-dom/server";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ let ThemeToggle: typeof import("./theme-toggle").ThemeToggle;
7
+ let useTheme: typeof import("./theme-toggle").useTheme;
8
+ let dark = false;
9
+ let listeners: Set<() => void>;
10
+
11
+ beforeEach(async () => {
12
+ vi.resetModules();
13
+ for (const attr of ["class", "data-theme", "data-sandbox-theme"]) document.documentElement.removeAttribute(attr);
14
+ window.localStorage.clear();
15
+ dark = false;
16
+ listeners = new Set();
17
+ vi.stubGlobal("matchMedia", vi.fn(() => ({
18
+ get matches() { return dark; },
19
+ media: "(prefers-color-scheme: dark)",
20
+ addEventListener: (_: string, handler: () => void) => listeners.add(handler),
21
+ removeEventListener: (_: string, handler: () => void) => listeners.delete(handler),
22
+ })));
23
+ ({ ThemeToggle, useTheme } = await import("./theme-toggle"));
24
+ });
25
+
26
+ afterEach(() => {
27
+ cleanup();
28
+ vi.restoreAllMocks();
29
+ vi.unstubAllGlobals();
30
+ for (const attr of ["class", "data-theme", "data-sandbox-theme"]) document.documentElement.removeAttribute(attr);
31
+ });
32
+
33
+ function systemMode(next: boolean) {
34
+ act(() => { dark = next; listeners.forEach((notify) => notify()); });
35
+ }
36
+
37
+ function PreferenceControl() {
38
+ const { theme, resolvedTheme, setTheme } = useTheme();
39
+ return <button onClick={() => setTheme("system")}>{theme}:{resolvedTheme}</button>;
40
+ }
41
+
42
+ describe("root mode preference", () => {
43
+ it("tracks system changes in the DOM, icon label and click behavior", () => {
44
+ render(<ThemeToggle />);
45
+ expect(screen.getByRole("button", { name: "Switch to dark mode" })).toBeTruthy();
46
+ expect(document.documentElement.classList.contains("light")).toBe(true);
47
+ systemMode(true);
48
+ fireEvent.click(screen.getByRole("button", { name: "Switch to light mode" }));
49
+ expect(window.localStorage.getItem("theme")).toBe("light");
50
+ expect(document.documentElement.className).toBe("light");
51
+ systemMode(false); systemMode(true);
52
+ expect(screen.getByRole("button", { name: "Switch to dark mode" })).toBeTruthy();
53
+ });
54
+
55
+ it("shares explicit choices and a return to system across mounted consumers", () => {
56
+ window.localStorage.setItem("theme", "dark");
57
+ render(<><ThemeToggle /><PreferenceControl /></>);
58
+ fireEvent.click(screen.getByRole("button", { name: "Switch to light mode" }));
59
+ expect(screen.getByRole("button", { name: "light:light" })).toBeTruthy();
60
+ fireEvent.click(screen.getByRole("button", { name: "light:light" }));
61
+ expect(window.localStorage.getItem("theme")).toBeNull();
62
+ systemMode(true);
63
+ expect(screen.getByRole("button", { name: "system:dark" })).toBeTruthy();
64
+ });
65
+
66
+ it("remains usable when storage reads, writes and removals throw", () => {
67
+ for (const method of ["getItem", "setItem", "removeItem"] as const) {
68
+ vi.spyOn(Storage.prototype, method).mockImplementation(() => { throw new Error("denied"); });
69
+ }
70
+ render(<><ThemeToggle /><PreferenceControl /></>);
71
+ fireEvent.click(screen.getByRole("button", { name: "Switch to dark mode" }));
72
+ expect(screen.getByRole("button", { name: "dark:dark" })).toBeTruthy();
73
+ fireEvent.click(screen.getByRole("button", { name: "dark:dark" }));
74
+ expect(screen.getByRole("button", { name: "system:light" })).toBeTruthy();
75
+ });
76
+
77
+ it("ignores invalid stored values and handles cross-tab preference changes", () => {
78
+ window.localStorage.setItem("theme", "not-a-theme");
79
+ render(<ThemeToggle />);
80
+ expect(document.documentElement.className).toBe("light");
81
+ act(() => window.dispatchEvent(new StorageEvent("storage", { key: "theme", newValue: "dark" })));
82
+ expect(screen.getByRole("button", { name: "Switch to light mode" })).toBeTruthy();
83
+ act(() => window.dispatchEvent(new StorageEvent("storage", { key: "theme", newValue: null })));
84
+ expect(document.documentElement.className).toBe("light");
85
+ });
86
+
87
+ it.each(["light", "dark"])("honors explicit data-theme=%s and keeps it in sync", (mode) => {
88
+ document.documentElement.dataset.theme = mode;
89
+ dark = mode !== "dark";
90
+ render(<ThemeToggle />);
91
+ expect(document.documentElement.className).toBe(mode);
92
+ fireEvent.click(screen.getByRole("button"));
93
+ expect(document.documentElement.dataset.theme).toBe(mode === "light" ? "dark" : "light");
94
+ });
95
+
96
+ it.each(["light", "dark"])("honors an explicit .%s class without a saved preference", (mode) => {
97
+ document.documentElement.className = mode;
98
+ dark = mode !== "dark";
99
+ render(<ThemeToggle />);
100
+ expect(document.documentElement.className).toBe(mode);
101
+ });
102
+
103
+ it.each([
104
+ ["aubergine", "aubergine-light"], ["arena", "arena-light"], ["tangle-dark", "tangle-light"],
105
+ ])("keeps the %s named family in both directions", (name, lightName) => {
106
+ document.documentElement.dataset.theme = name;
107
+ render(<ThemeToggle />);
108
+ fireEvent.click(screen.getByRole("button", { name: "Switch to light mode" }));
109
+ expect(document.documentElement.dataset.theme).toBe(lightName);
110
+ fireEvent.click(screen.getByRole("button", { name: "Switch to dark mode" }));
111
+ expect(document.documentElement.dataset.theme).toBe(name);
112
+ });
113
+
114
+ it.each(["intelligence", "custom-product"])("does not overwrite unpaired identity %s", (name) => {
115
+ document.documentElement.dataset.theme = name;
116
+ document.documentElement.className = "dark";
117
+ render(<ThemeToggle />);
118
+ fireEvent.click(screen.getByRole("button"));
119
+ expect(document.documentElement.dataset.theme).toBe(name);
120
+ expect(document.documentElement.className).toBe("light");
121
+ });
122
+
123
+ it.each(["vault", "dawn"])("preserves legacy %s identity while changing mode", (name) => {
124
+ document.documentElement.dataset.sandboxTheme = name;
125
+ render(<ThemeToggle />);
126
+ fireEvent.click(screen.getByRole("button", { name: "Switch to dark mode" }));
127
+ expect(document.documentElement.dataset.sandboxTheme).toBe(name);
128
+ expect(document.documentElement.className).toBe("dark");
129
+ });
130
+
131
+ it("uses the dark fallback without matchMedia and cleans subscriptions on unmount", () => {
132
+ const view = render(<ThemeToggle />);
133
+ expect(listeners.size).toBe(1);
134
+ view.unmount();
135
+ expect(listeners.size).toBe(0);
136
+ vi.stubGlobal("matchMedia", undefined);
137
+ render(<ThemeToggle />);
138
+ expect(screen.getByRole("button", { name: "Switch to light mode" })).toBeTruthy();
139
+ });
140
+ });
141
+
142
+ describe("server rendering and hydration", () => {
143
+ it("renders without window, document, matchMedia or storage", () => {
144
+ vi.stubGlobal("window", undefined);
145
+ vi.stubGlobal("document", undefined);
146
+ expect(renderToString(<ThemeToggle />)).toContain("Switch to light mode");
147
+ vi.unstubAllGlobals();
148
+ });
149
+
150
+ it("hydrates the server snapshot then resolves saved light without a dark write", async () => {
151
+ const html = renderToString(<ThemeToggle />);
152
+ window.localStorage.setItem("theme", "light");
153
+ document.documentElement.className = "light";
154
+ const host = document.createElement("div");
155
+ host.innerHTML = html;
156
+ document.body.append(host);
157
+ const recoverable = vi.fn();
158
+ const toggle = vi.spyOn(document.documentElement.classList, "toggle");
159
+ let root: ReturnType<typeof hydrateRoot>;
160
+ await act(async () => { root = hydrateRoot(host, <ThemeToggle />, { onRecoverableError: recoverable }); });
161
+ expect(host.querySelector("button")?.getAttribute("aria-label")).toBe("Switch to dark mode");
162
+ expect(recoverable).not.toHaveBeenCalled();
163
+ expect(toggle).not.toHaveBeenCalledWith("dark", true);
164
+ await act(async () => root.unmount());
165
+ host.remove();
166
+ });
167
+ });
@@ -1,44 +1,117 @@
1
- import { useCallback, useEffect, useState } from "react";
1
+ import { useEffect, useSyncExternalStore } from "react";
2
2
 
3
3
  type Theme = "light" | "dark" | "system";
4
+ type Mode = Exclude<Theme, "system">;
4
5
 
5
- function getSystemTheme(): "light" | "dark" {
6
- return window.matchMedia("(prefers-color-scheme: dark)").matches
7
- ? "dark"
8
- : "light";
6
+ const preferenceEvent = "tangle:theme-preference";
7
+ const systemQuery = "(prefers-color-scheme: dark)";
8
+ // Only existing, paired Brand themes. Never turn an arbitrary product identity
9
+ // (notably Intelligence) into data-theme="light" or data-theme="dark".
10
+ const namedModes = [
11
+ { dark: "aubergine", light: "aubergine-light" },
12
+ { dark: "arena", light: "arena-light" },
13
+ { dark: "tangle-dark", light: "tangle-light" },
14
+ ] as const;
15
+
16
+ // Client-only preference cache also keeps the control usable when storage is
17
+ // denied, and lets multiple useTheme consumers share the same preference.
18
+ // Server renders ONLY read getServerTheme; no request state is stored here.
19
+ let preference: Theme | undefined;
20
+
21
+ function isTheme(value: unknown): value is Theme {
22
+ return value === "light" || value === "dark" || value === "system";
9
23
  }
10
24
 
11
- function applyTheme(theme: Theme) {
12
- const resolved = theme === "system" ? getSystemTheme() : theme;
13
- document.documentElement.classList.toggle("dark", resolved === "dark");
25
+ function getSystemTheme(): Mode {
26
+ if (typeof window === "undefined" || !window.matchMedia) return "dark";
27
+ return window.matchMedia(systemQuery).matches ? "dark" : "light";
14
28
  }
15
29
 
16
- export function useTheme() {
17
- const [theme, setThemeState] = useState<Theme>(() => {
18
- if (typeof window === "undefined") return "system";
19
- return (localStorage.getItem("theme") as Theme) ?? "system";
20
- });
21
-
22
- const setTheme = useCallback((next: Theme) => {
23
- setThemeState(next);
24
- if (next === "system") {
25
- localStorage.removeItem("theme");
26
- } else {
27
- localStorage.setItem("theme", next);
28
- }
29
- applyTheme(next);
30
- }, []);
30
+ function getServerTheme(): Theme { return "system"; }
31
+ function getServerMode(): Mode { return "dark"; }
32
+
33
+ function readInitialTheme(): Theme {
34
+ try {
35
+ const stored = window.localStorage.getItem("theme");
36
+ if (isTheme(stored)) return stored;
37
+ } catch { /* Storage is optional, not a prerequisite for changing mode. */ }
38
+
39
+ const root = document.documentElement;
40
+ const named = root.getAttribute("data-theme");
41
+ if (named === "light" || named === "dark") return named;
42
+ for (const pair of namedModes) {
43
+ if (named === pair.light) return "light";
44
+ if (named === pair.dark) return "dark";
45
+ }
46
+ if (root.classList.contains("light")) return "light";
47
+ if (root.classList.contains("dark")) return "dark";
48
+ const legacy = root.getAttribute("data-sandbox-theme");
49
+ if (legacy === "vault" || legacy === "dawn") return "light";
50
+ return "system";
51
+ }
52
+
53
+ function getTheme(): Theme {
54
+ if (typeof window === "undefined") return "system";
55
+ return preference ??= readInitialTheme();
56
+ }
57
+
58
+ function applyMode(mode: Mode) {
59
+ const root = document.documentElement;
60
+ root.classList.toggle("dark", mode === "dark");
61
+ root.classList.toggle("light", mode === "light");
62
+ const named = root.getAttribute("data-theme");
63
+ if (named === "dark" || named === "light") {
64
+ root.setAttribute("data-theme", mode);
65
+ } else {
66
+ const pair = namedModes.find((pair) => named === pair.dark || named === pair.light);
67
+ if (pair) root.setAttribute("data-theme", pair[mode]);
68
+ }
69
+ }
70
+
71
+ function setTheme(next: Theme) {
72
+ if (!isTheme(next) || typeof window === "undefined") return;
73
+ preference = next;
74
+ try {
75
+ if (next === "system") window.localStorage.removeItem("theme");
76
+ else window.localStorage.setItem("theme", next);
77
+ } catch { /* Keep the in-memory preference when storage is unavailable. */ }
78
+ applyMode(next === "system" ? getSystemTheme() : next);
79
+ window.dispatchEvent(new Event(preferenceEvent));
80
+ }
81
+
82
+ function subscribeTheme(notify: () => void) {
83
+ const onStorage = (event: StorageEvent) => {
84
+ if (event.key !== "theme" && event.key !== null) return;
85
+ preference = isTheme(event.newValue) ? event.newValue : "system";
86
+ notify();
87
+ };
88
+ window.addEventListener(preferenceEvent, notify);
89
+ window.addEventListener("storage", onStorage);
90
+ return () => {
91
+ window.removeEventListener(preferenceEvent, notify);
92
+ window.removeEventListener("storage", onStorage);
93
+ };
94
+ }
31
95
 
96
+ function subscribeSystem(notify: () => void) {
97
+ if (!window.matchMedia) return () => {};
98
+ const media = window.matchMedia(systemQuery);
99
+ media.addEventListener("change", notify);
100
+ return () => media.removeEventListener("change", notify);
101
+ }
102
+
103
+ export function useTheme() {
104
+ const theme = useSyncExternalStore(subscribeTheme, getTheme, getServerTheme);
105
+ const system = useSyncExternalStore(subscribeSystem, getSystemTheme, getServerMode);
106
+ const resolvedTheme = theme === "system" ? system : theme;
32
107
  useEffect(() => {
33
- applyTheme(theme);
34
- if (theme !== "system") return;
35
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
36
- const handler = () => applyTheme("system");
37
- mq.addEventListener("change", handler);
38
- return () => mq.removeEventListener("change", handler);
39
- }, [theme]);
40
-
41
- return { theme, setTheme };
108
+ // Hydration first renders the stable server snapshot. Apply the real client
109
+ // preference, not that placeholder, so we never repaint a persisted light
110
+ // page dark while React is catching up.
111
+ const selected = getTheme();
112
+ applyMode(selected === "system" ? getSystemTheme() : selected);
113
+ }, [resolvedTheme]);
114
+ return { theme, setTheme, resolvedTheme };
42
115
  }
43
116
 
44
117
  const iconClass = "h-4 w-4";
@@ -54,6 +127,7 @@ function SunIcon() {
54
127
  strokeLinecap="round"
55
128
  strokeLinejoin="round"
56
129
  className={iconClass}
130
+ aria-hidden="true"
57
131
  >
58
132
  <circle cx={12} cy={12} r={5} />
59
133
  <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
@@ -72,6 +146,7 @@ function MoonIcon() {
72
146
  strokeLinecap="round"
73
147
  strokeLinejoin="round"
74
148
  className={iconClass}
149
+ aria-hidden="true"
75
150
  >
76
151
  <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
77
152
  </svg>
@@ -79,18 +154,15 @@ function MoonIcon() {
79
154
  }
80
155
 
81
156
  export function ThemeToggle() {
82
- const { theme, setTheme } = useTheme();
83
- const resolved =
84
- theme === "system" ? getSystemTheme() : theme;
85
-
157
+ const { resolvedTheme, setTheme } = useTheme();
86
158
  return (
87
159
  <button
88
160
  type="button"
89
- onClick={() => setTheme(resolved === "dark" ? "light" : "dark")}
161
+ onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
90
162
  className="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
91
- aria-label={`Switch to ${resolved === "dark" ? "light" : "dark"} mode`}
163
+ aria-label={`Switch to ${resolvedTheme === "dark" ? "light" : "dark"} mode`}
92
164
  >
93
- {resolved === "dark" ? <SunIcon /> : <MoonIcon />}
165
+ {resolvedTheme === "dark" ? <SunIcon /> : <MoonIcon />}
94
166
  </button>
95
167
  );
96
168
  }
@@ -1,84 +0,0 @@
1
- //#region src/stores/active-sessions-store.d.ts
2
- type SessionProjectKey = string | number;
3
- type ActiveSessionStatus = "idle" | "running" | "attention-needed" | "error";
4
- type ActiveSessionReconnectState = "idle" | "reconnecting" | "failed";
5
- type ActiveSessionConnectionState = "disconnected" | "connecting" | "connected" | "reconnecting" | "error";
6
- type ActiveSessionTransportMode = "websocket" | "sse" | "polling" | "custom";
7
- interface ActiveSessionRecord {
8
- sessionId: string;
9
- projectId: SessionProjectKey | null;
10
- projectLabel?: string;
11
- title?: string;
12
- href?: string;
13
- registeredAt: number;
14
- lastActivityAt: number;
15
- lastEventAt: number | null;
16
- status: ActiveSessionStatus;
17
- isRunning: boolean;
18
- isForeground: boolean;
19
- needsAttention: boolean;
20
- connectionState: ActiveSessionConnectionState;
21
- reconnectState: ActiveSessionReconnectState;
22
- transportMode: ActiveSessionTransportMode | null;
23
- lastError: string | null;
24
- metadata?: Record<string, unknown>;
25
- }
26
- interface ActiveSessionsState {
27
- sessions: Record<string, ActiveSessionRecord>;
28
- lastUpdatedAt: number;
29
- }
30
- interface RegisterActiveSessionOptions {
31
- sessionId: string;
32
- projectId?: SessionProjectKey | null;
33
- projectLabel?: string;
34
- title?: string;
35
- href?: string;
36
- metadata?: Record<string, unknown>;
37
- }
38
- interface ActiveSessionConnectionOptions {
39
- connectionState: ActiveSessionConnectionState;
40
- reconnectState?: ActiveSessionReconnectState;
41
- transportMode?: ActiveSessionTransportMode | null;
42
- lastError?: string | null;
43
- lastEventAt?: number | null;
44
- }
45
- interface ActiveSessionActivityOptions {
46
- lastEventAt?: number | null;
47
- }
48
- interface ActiveProjectActivity {
49
- projectId: SessionProjectKey;
50
- projectLabel?: string;
51
- activeSessionCount: number;
52
- runningSessionIds: string[];
53
- lastActivityAt: number;
54
- }
55
- declare const activeSessionsAtom: import("nanostores").PreinitializedWritableAtom<ActiveSessionsState> & object;
56
- declare function registerActiveSession(options: RegisterActiveSessionOptions): void;
57
- declare function unregisterActiveSession(sessionId: string): void;
58
- declare function setForegroundActiveSession(sessionId: string | null): void;
59
- declare function updateActiveSessionMeta(sessionId: string, meta: Partial<Pick<ActiveSessionRecord, "title" | "href" | "projectId" | "projectLabel" | "metadata">>): void;
60
- declare function setActiveSessionConnection(sessionId: string, options: ActiveSessionConnectionOptions): void;
61
- declare function setActiveSessionRunning(sessionId: string, isRunning: boolean, options?: ActiveSessionActivityOptions): void;
62
- declare function setActiveSessionAttention(sessionId: string, needsAttention: boolean, options?: ActiveSessionActivityOptions): void;
63
- declare function setActiveSessionError(sessionId: string, error: string | null): void;
64
- declare function bumpActiveSessionActivity(sessionId: string, options?: ActiveSessionActivityOptions): void;
65
- declare function resetActiveSessions(): void;
66
- declare function getAllActiveSessions(state: ActiveSessionsState): ActiveSessionRecord[];
67
- declare function getActiveSession(state: ActiveSessionsState, sessionId: string): ActiveSessionRecord | null;
68
- declare function getSessionsForProject(state: ActiveSessionsState, projectId: SessionProjectKey): ActiveSessionRecord[];
69
- declare function getSessionsForNavbar(state: ActiveSessionsState, projectId?: SessionProjectKey | null): ActiveSessionRecord[];
70
- declare function getSessionsByActivity(state: ActiveSessionsState): ActiveSessionRecord[];
71
- declare function getTotalRunningSessionCount(state: ActiveSessionsState): number;
72
- declare function hasBackgroundRunningSessions(state: ActiveSessionsState): boolean;
73
- declare function getAllProjectActivity(state: ActiveSessionsState): ActiveProjectActivity[];
74
- declare function useActiveSessionsState(): ActiveSessionsState;
75
- declare function useActiveSessions(): ActiveSessionRecord[];
76
- declare function useActiveSession(sessionId: string | null): ActiveSessionRecord | null;
77
- declare function useProjectSessions(projectId: SessionProjectKey | null): ActiveSessionRecord[];
78
- declare function useNavbarSessions(projectId?: SessionProjectKey | null): ActiveSessionRecord[];
79
- declare function useSessionsByActivity(): ActiveSessionRecord[];
80
- declare function useProjectActivity(): ActiveProjectActivity[];
81
- declare function useTotalRunningSessions(): number;
82
- declare function useHasBackgroundRunningSessions(): boolean;
83
- //#endregion
84
- export { updateActiveSessionMeta as A, resetActiveSessions as C, setActiveSessionRunning as D, setActiveSessionError as E, useNavbarSessions as F, useProjectActivity as I, useProjectSessions as L, useActiveSessions as M, useActiveSessionsState as N, setForegroundActiveSession as O, useHasBackgroundRunningSessions as P, useSessionsByActivity as R, registerActiveSession as S, setActiveSessionConnection as T, getSessionsByActivity as _, ActiveSessionReconnectState as a, getTotalRunningSessionCount as b, ActiveSessionTransportMode as c, SessionProjectKey as d, activeSessionsAtom as f, getAllProjectActivity as g, getAllActiveSessions as h, ActiveSessionConnectionState as i, useActiveSession as j, unregisterActiveSession as k, ActiveSessionsState as l, getActiveSession as m, ActiveSessionActivityOptions as n, ActiveSessionRecord as o, bumpActiveSessionActivity as p, ActiveSessionConnectionOptions as r, ActiveSessionStatus as s, ActiveProjectActivity as t, RegisterActiveSessionOptions as u, getSessionsForNavbar as v, setActiveSessionAttention as w, hasBackgroundRunningSessions as x, getSessionsForProject as y, useTotalRunningSessions as z };
@@ -1,78 +0,0 @@
1
- import { i as focusRing } from "./focus-BcE_SHqz.js";
2
- import { t as cn } from "./utils-C_uf36nf.js";
3
- import { Slot } from "@radix-ui/react-slot";
4
- import { cva } from "class-variance-authority";
5
- import * as React from "react";
6
- import { jsx, jsxs } from "react/jsx-runtime";
7
- //#region src/primitives/button.tsx
8
- const buttonVariants = cva(`inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg font-medium text-sm transition-all ${focusRing} disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`, {
9
- variants: {
10
- variant: {
11
- default: "bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.97] duration-[var(--transition-fast)]",
12
- destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90 active:scale-[0.97] duration-[var(--transition-fast)]",
13
- outline: "border border-border bg-card hover:bg-muted active:scale-[0.97] duration-[var(--transition-fast)] text-foreground",
14
- secondary: "bg-muted border border-border text-foreground hover:bg-muted/80 active:scale-[0.97] duration-[var(--transition-fast)]",
15
- ghost: "hover:bg-muted hover:text-foreground duration-[var(--transition-fast)] text-muted-foreground border border-transparent",
16
- link: "text-primary underline-offset-4 hover:underline",
17
- sandbox: "bg-[var(--btn-primary-bg)] text-[var(--btn-primary-text)] border border-[var(--border-accent)] hover:bg-[var(--btn-primary-hover)] active:scale-[0.97] duration-[var(--transition-fast)]"
18
- },
19
- size: {
20
- default: "h-[var(--control-height)] px-4 py-2",
21
- sm: "h-8 rounded-md px-3 text-xs",
22
- lg: "h-11 rounded-lg px-7 text-sm",
23
- xl: "h-13 rounded-xl px-9 text-base",
24
- icon: "h-[var(--control-height)] w-[var(--control-height)]"
25
- }
26
- },
27
- defaultVariants: {
28
- variant: "default",
29
- size: "default"
30
- }
31
- });
32
- const Button = React.forwardRef(({ className, variant, size, asChild = false, loading = false, disabled, children, ...props }, ref) => {
33
- const Comp = asChild ? Slot : "button";
34
- if (asChild) return /* @__PURE__ */ jsx(Comp, {
35
- className: cn(buttonVariants({
36
- variant,
37
- size,
38
- className
39
- })),
40
- ref,
41
- ...props,
42
- children
43
- });
44
- return /* @__PURE__ */ jsxs(Comp, {
45
- className: cn(buttonVariants({
46
- variant,
47
- size,
48
- className
49
- })),
50
- ref,
51
- disabled: disabled || loading,
52
- ...props,
53
- children: [loading && /* @__PURE__ */ jsxs("svg", {
54
- className: "mr-2 -ml-1 h-4 w-4 animate-spin",
55
- fill: "none",
56
- viewBox: "0 0 24 24",
57
- children: [
58
- /* @__PURE__ */ jsx("title", { children: "Loading spinner" }),
59
- /* @__PURE__ */ jsx("circle", {
60
- className: "opacity-25",
61
- cx: "12",
62
- cy: "12",
63
- r: "10",
64
- stroke: "currentColor",
65
- strokeWidth: "4"
66
- }),
67
- /* @__PURE__ */ jsx("path", {
68
- className: "opacity-75",
69
- fill: "currentColor",
70
- d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
71
- })
72
- ]
73
- }), children]
74
- });
75
- });
76
- Button.displayName = "Button";
77
- //#endregion
78
- export { buttonVariants as n, Button as t };