@nextclaw/ui 0.14.4 → 0.15.1

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 (222) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/assets/api-b1R9VdeH.js +13 -0
  3. package/dist/assets/appearance-settings-page-D2O9pch9.js +1 -0
  4. package/dist/assets/book-open-B77lQ2Tg.js +1 -0
  5. package/dist/assets/channels-list-page-DcsIQc6A.js +8 -0
  6. package/dist/assets/chat-page-CZ7WxPir.js +1 -0
  7. package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-BWQ6_Raq.js} +1 -1
  8. package/dist/assets/confirm-dialog-C-NXSioX.js +5 -0
  9. package/dist/assets/desktop-update-config-usHpXugP.js +1 -0
  10. package/dist/assets/{dist-CikkhOlq.js → dist-YtoqU-AT.js} +1 -1
  11. package/dist/assets/{dist-PVA_xL1D.js → dist-jdGmbb6w.js} +1 -1
  12. package/dist/assets/doc-browser-B_Tf9n70.js +1 -0
  13. package/dist/assets/doc-browser-BdZPFOVa.js +1 -0
  14. package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-DwokemUn.js} +1 -1
  15. package/dist/assets/es2015-bNpL2LKy.js +41 -0
  16. package/dist/assets/{external-link-DVJIQf_G.js → external-link-BM6fk8q-.js} +1 -1
  17. package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
  18. package/dist/assets/i18n-provider-CntWolRA.js +1 -0
  19. package/dist/assets/index-BIcEw90u.js +104 -0
  20. package/dist/assets/index-BiNSTKTU.css +1 -0
  21. package/dist/assets/loader-circle-MBgNSVIH.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +1 -0
  24. package/dist/assets/model-config-page-Di0ySJD2.js +1 -0
  25. package/dist/assets/plus-vDqW5Kxp.js +1 -0
  26. package/dist/assets/provider-scoped-model-input-Cm_a4GHf.js +1 -0
  27. package/dist/assets/providers-config-page-BAf2W0MV.js +1 -0
  28. package/dist/assets/{react-VNqT5Zp7.js → react-BN8uzPnp.js} +1 -1
  29. package/dist/assets/refresh-cw-BpVcFPmJ.js +1 -0
  30. package/dist/assets/remote-CLQ0IBM8.js +1 -0
  31. package/dist/assets/rotate-cw-CvNBv7_w.js +1 -0
  32. package/dist/assets/runtime-config-page-CflvvoDc.js +1 -0
  33. package/dist/assets/{save-BOJ477H-.js → save-BpFW5fpk.js} +1 -1
  34. package/dist/assets/{search-BEti5beA.js → search-D0uO7enf.js} +1 -1
  35. package/dist/assets/search-config-page-D0klFUXd.js +1 -0
  36. package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-dVZ631Wp.js} +2 -2
  37. package/dist/assets/security-config-C5gnlkuj.js +1 -0
  38. package/dist/assets/select-Bh-CrA4p.js +1 -0
  39. package/dist/assets/setting-row-BJfCHRcb.js +1 -0
  40. package/dist/assets/settings-2-Blzi1vr6.js +1 -0
  41. package/dist/assets/side-dock-ZdaSrQVB.js +3 -0
  42. package/dist/assets/skeleton-BLlEzebZ.js +1 -0
  43. package/dist/assets/switch-CFLJFNSd.js +1 -0
  44. package/dist/assets/tag-chip-CzGihiQx.js +1 -0
  45. package/dist/assets/tooltip-CdcrBjrC.js +1 -0
  46. package/dist/assets/use-config-CVPgsBmU.js +1 -0
  47. package/dist/assets/use-confirm-dialog-CSeBk9SF.js +1 -0
  48. package/dist/assets/x-BF2KQ--V.js +1 -0
  49. package/dist/index.html +53 -17
  50. package/index.html +25 -2
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
  53. package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
  54. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
  55. package/src/app/components/layout/app-layout.tsx +10 -3
  56. package/src/app/components/layout/sidebar-items.tsx +8 -8
  57. package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
  58. package/src/app/components/layout/sidebar.tsx +21 -28
  59. package/src/app/configs/app-navigation.config.ts +11 -5
  60. package/src/app/index.tsx +8 -0
  61. package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
  62. package/src/app/managers/viewport-layout.manager.ts +39 -0
  63. package/src/app/presenters/app.presenter.ts +25 -2
  64. package/src/app/styles/design-system.css +91 -4
  65. package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
  66. package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
  67. package/src/features/agents/components/agent-details-dialog.tsx +398 -0
  68. package/src/features/agents/components/agent-dialogs.tsx +20 -47
  69. package/src/features/agents/components/agents-page.tsx +76 -10
  70. package/src/features/agents/types/agent-form.types.ts +21 -0
  71. package/src/features/agents/utils/agent-form.utils.ts +39 -0
  72. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
  73. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +132 -1
  74. package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
  75. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +8 -0
  76. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
  77. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
  78. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
  79. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
  80. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +66 -2
  81. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
  82. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
  83. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
  84. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
  85. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
  86. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
  87. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
  88. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
  89. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
  90. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
  91. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
  92. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
  93. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +167 -1
  94. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
  95. package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
  96. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
  97. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
  98. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
  99. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
  100. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
  101. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
  102. package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
  103. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +62 -1
  104. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
  105. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
  106. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  107. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
  108. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
  109. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  110. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
  111. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
  112. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
  113. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
  114. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
  115. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +304 -115
  116. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
  117. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
  118. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +64 -5
  119. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
  120. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
  121. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +126 -1
  122. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
  123. package/src/features/chat/managers/chat-thread.manager.ts +15 -3
  124. package/src/features/chat/managers/chat-ui.manager.ts +5 -1
  125. package/src/features/chat/presenters/chat.presenter.ts +2 -1
  126. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  127. package/src/features/chat/stores/chat-thread.store.ts +6 -64
  128. package/src/features/chat/types/chat-message.types.ts +4 -0
  129. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
  130. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
  131. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  132. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
  133. package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
  134. package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
  135. package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
  136. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
  137. package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
  138. package/src/features/marketplace/index.ts +3 -0
  139. package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
  140. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
  141. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
  142. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
  143. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
  144. package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
  145. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
  146. package/src/features/side-dock/components/side-dock.tsx +100 -13
  147. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
  148. package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
  149. package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
  150. package/src/features/side-dock/stores/side-dock.store.ts +9 -0
  151. package/src/features/side-dock/types/side-dock.types.ts +10 -3
  152. package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
  153. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
  154. package/src/features/system-status/components/desktop-update-config.tsx +91 -2
  155. package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
  156. package/src/index.css +501 -27
  157. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  158. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
  159. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
  160. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
  161. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
  162. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
  163. package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
  164. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
  165. package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
  166. package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
  167. package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
  168. package/src/shared/components/ui/dialog.tsx +2 -2
  169. package/src/shared/components/ui/popover.tsx +1 -1
  170. package/src/shared/components/ui/select.tsx +1 -1
  171. package/src/shared/components/ui/switch.tsx +4 -3
  172. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
  173. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
  174. package/src/shared/components/ui/tooltip.tsx +1 -1
  175. package/src/shared/lib/api/ncp-session.types.ts +1 -1
  176. package/src/shared/lib/api/types.ts +12 -3
  177. package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
  178. package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
  179. package/src/shared/lib/i18n/locales/en-US/chat.json +18 -6
  180. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  181. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
  182. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
  183. package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
  184. package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
  185. package/src/shared/lib/i18n/locales/zh-CN/chat.json +18 -6
  186. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  187. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
  188. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
  189. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
  190. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
  191. package/src/shared/lib/session-project/index.ts +1 -1
  192. package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
  193. package/src/shared/lib/theme/index.ts +51 -13
  194. package/dist/assets/api-BP_8IaxB.js +0 -13
  195. package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
  196. package/dist/assets/chat-page-BRB0N5WW.js +0 -105
  197. package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
  198. package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
  199. package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
  200. package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
  201. package/dist/assets/ellipsis-y6vDtq4M.js +0 -1
  202. package/dist/assets/index-BiE-K8-q.css +0 -1
  203. package/dist/assets/index-D9BAE_L_.js +0 -105
  204. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
  205. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
  206. package/dist/assets/model-config-page-DotaTUHb.js +0 -1
  207. package/dist/assets/plus-BbZqquJe.js +0 -1
  208. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
  209. package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
  210. package/dist/assets/remote-B8TiUfwW.js +0 -1
  211. package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
  212. package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
  213. package/dist/assets/search-config-page-DGj5am03.js +0 -1
  214. package/dist/assets/select-hoj8dK4K.js +0 -41
  215. package/dist/assets/tag-chip-T8-h-die.js +0 -1
  216. package/dist/assets/use-config-CjRV2hwL.js +0 -1
  217. package/dist/assets/x-D9PXs_xP.js +0 -1
  218. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
  219. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
  220. package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
  221. /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
  222. /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
@@ -11,13 +11,21 @@ import { normalizeDocUrl } from './utils/doc-browser-url.utils';
11
11
  import { DocBrowserHomePage } from './doc-browser-home-page';
12
12
  import type { DocBrowserCustomTabRenderers } from './doc-browser-renderer.types';
13
13
  import {
14
- DocBrowserDocsToolbar,
14
+ DocBrowserAddressToolbar,
15
15
  DocBrowserExternalLink,
16
16
  DocBrowserFrameContent,
17
17
  } from './doc-browser-panel-parts';
18
18
  import { DocBrowserTabStrip } from './doc-browser-tab-strip';
19
+ import {
20
+ createInitialFloatingPanelRect,
21
+ moveFloatingPanelRect,
22
+ resizeFloatingPanelRect,
23
+ type FloatingPanelRect,
24
+ type FloatingPanelResizeEdge,
25
+ } from './utils/doc-browser-floating-panel.utils';
19
26
  import { ResizableRightPanel } from '@/shared/components/resizable-right-panel/resizable-right-panel';
20
27
  import { cn } from '@/shared/lib/utils';
28
+ import { t } from '@/shared/lib/i18n';
21
29
  import { GripVertical } from 'lucide-react';
22
30
 
23
31
  type DocBrowserProps = {
@@ -26,26 +34,46 @@ type DocBrowserProps = {
26
34
  dockControls?: DocBrowserDockControls;
27
35
  };
28
36
 
29
- type FloatingPanelRect = { x: number; y: number; w: number; h: number };
30
- type FloatingPanelResizeEdge = 'left' | 'right' | 'top' | 'bottom' | 'bottom-right';
31
37
  type FloatingPanelInteraction = {
32
- kind: 'drag' | 'resize';
33
- edge?: FloatingPanelResizeEdge;
34
38
  startX: number;
35
39
  startY: number;
36
40
  startRect: FloatingPanelRect;
37
- };
41
+ } & (
42
+ | { kind: 'drag' }
43
+ | { kind: 'resize'; edge: FloatingPanelResizeEdge }
44
+ );
38
45
 
39
- const FLOATING_PANEL_MARGIN = 40;
40
- const FLOATING_PANEL_MIN_WIDTH = 360;
41
- const FLOATING_PANEL_MIN_HEIGHT = 400;
42
- const DEFAULT_IFRAME_SANDBOX = 'allow-same-origin allow-scripts allow-popups allow-forms';
46
+ const DEFAULT_DOCS_IFRAME_SANDBOX = 'allow-same-origin allow-scripts allow-popups allow-forms';
47
+
48
+ function resolveContentUrlInput(input: string, currentUrl: string): string {
49
+ if (input.startsWith('/')) {
50
+ try {
51
+ return new URL(input, currentUrl).toString();
52
+ } catch {
53
+ return input;
54
+ }
55
+ }
56
+ if (/^[a-z][a-z\d+.-]*:\/\//i.test(input) || input === 'about:blank') {
57
+ return input;
58
+ }
59
+ if (/^(localhost|127\.|0\.0\.0\.0|\[::1\])(?::|\/|$)/i.test(input)) {
60
+ return `http://${input}`;
61
+ }
62
+ return `https://${input}`;
63
+ }
43
64
 
44
65
  function resolveIframeSandbox(
45
66
  customRenderer: DocBrowserCustomTabRenderers[string] | undefined,
46
67
  currentTab: DocBrowserTab | undefined,
47
- ): string {
48
- return currentTab ? customRenderer?.getIframeSandbox?.(currentTab) ?? DEFAULT_IFRAME_SANDBOX : DEFAULT_IFRAME_SANDBOX;
68
+ ): string | undefined {
69
+ if (!currentTab) {
70
+ return DEFAULT_DOCS_IFRAME_SANDBOX;
71
+ }
72
+ const customSandbox = customRenderer?.getIframeSandbox?.(currentTab);
73
+ if (customSandbox !== undefined) {
74
+ return customSandbox;
75
+ }
76
+ return currentTab.kind === 'content' ? undefined : DEFAULT_DOCS_IFRAME_SANDBOX;
49
77
  }
50
78
 
51
79
  function useDocBrowserDockAction(
@@ -67,16 +95,62 @@ function useDocBrowserDockAction(
67
95
  }, [currentTab, dockControls, dockState]);
68
96
  }
69
97
 
70
- function clamp(value: number, min: number, max: number): number {
71
- return Math.min(max, Math.max(min, value));
72
- }
98
+ function useDocBrowserAddressBar({
99
+ activeTabId,
100
+ currentUrl,
101
+ isAddressToolbarTab,
102
+ isContentTab,
103
+ isDocsTab,
104
+ navigate,
105
+ }: {
106
+ activeTabId: string;
107
+ currentUrl: string;
108
+ isAddressToolbarTab: boolean;
109
+ isContentTab: boolean;
110
+ isDocsTab: boolean;
111
+ navigate: (url: string) => void;
112
+ }) {
113
+ const addressInputKey = `${activeTabId}:${currentUrl}:${isContentTab ? 'content' : 'docs'}`;
114
+ const [draftInput, setDraftInput] = useState<{ key: string; value: string } | null>(null);
115
+ const currentInput = (() => {
116
+ if (!isAddressToolbarTab) {
117
+ return '';
118
+ }
119
+ if (isContentTab) {
120
+ return currentUrl;
121
+ }
122
+ try {
123
+ return new URL(currentUrl).pathname;
124
+ } catch {
125
+ return currentUrl;
126
+ }
127
+ })();
128
+ const urlInput = draftInput?.key === addressInputKey ? draftInput.value : currentInput;
129
+ const setUrlInput = useCallback(
130
+ (value: string) => setDraftInput({ key: addressInputKey, value }),
131
+ [addressInputKey],
132
+ );
133
+
134
+ const handleUrlSubmit = useCallback((e: React.FormEvent) => {
135
+ e.preventDefault();
136
+ if (!isAddressToolbarTab) return;
137
+ const input = urlInput.trim();
138
+ if (!input) return;
139
+ if (isDocsTab && input.startsWith('/')) {
140
+ navigate(`${DOCS_DEFAULT_BASE_URL}${input}`);
141
+ } else if (isDocsTab && input.startsWith('http')) {
142
+ navigate(input);
143
+ } else if (isDocsTab) {
144
+ navigate(`${DOCS_DEFAULT_BASE_URL}/${input}`);
145
+ } else {
146
+ navigate(resolveContentUrlInput(input, currentUrl));
147
+ }
148
+ }, [currentUrl, isAddressToolbarTab, isDocsTab, navigate, urlInput]);
73
149
 
74
- function createInitialFloatingPanelRect(): FloatingPanelRect {
75
150
  return {
76
- x: Math.max(FLOATING_PANEL_MARGIN, window.innerWidth - 520),
77
- y: 80,
78
- w: 480,
79
- h: 600,
151
+ handleUrlSubmit,
152
+ setUrlInput,
153
+ urlInput,
80
154
  };
81
155
  }
82
156
 
@@ -102,7 +176,6 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
102
176
  closeTab,
103
177
  } = useDocBrowser();
104
178
 
105
- const [urlInput, setUrlInput] = useState('');
106
179
  const [iframeReloadVersion, setIframeReloadVersion] = useState(0);
107
180
  const [floatRect, setFloatRect] = useState<FloatingPanelRect>(createInitialFloatingPanelRect);
108
181
  const [floatInteraction, setFloatInteraction] = useState<FloatingPanelInteraction | null>(null);
@@ -118,19 +191,16 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
118
191
  const canGoForward = activeHistoryIndex < activeHistory.length - 1;
119
192
  const customRenderer = currentTab ? customTabRenderers[currentTab.kind] : undefined;
120
193
  const dockState = dockControls?.getDockState(currentTab);
121
-
122
- useEffect(() => {
123
- if (!isDocsTab) {
124
- setUrlInput('');
125
- return;
126
- }
127
- try {
128
- const parsed = new URL(currentUrl);
129
- setUrlInput(parsed.pathname);
130
- } catch {
131
- setUrlInput(currentUrl);
132
- }
133
- }, [currentUrl, activeTabId, isDocsTab]);
194
+ const isContentTab = currentTab?.kind === 'content';
195
+ const isAddressToolbarTab = isDocsTab || isContentTab;
196
+ const { handleUrlSubmit, setUrlInput, urlInput } = useDocBrowserAddressBar({
197
+ activeTabId,
198
+ currentUrl,
199
+ isAddressToolbarTab,
200
+ isContentTab,
201
+ isDocsTab,
202
+ navigate,
203
+ });
134
204
 
135
205
  // When currentUrl changes without navVersion bump (goBack/goForward),
136
206
  // use postMessage to SPA-navigate inside the iframe instead of remounting.
@@ -221,31 +291,11 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
221
291
  const dy = event.clientY - startY;
222
292
 
223
293
  if (floatInteraction.kind === 'drag') {
224
- setFloatRect({
225
- ...startRect,
226
- x: clamp(startRect.x + dx, FLOATING_PANEL_MARGIN, window.innerWidth - FLOATING_PANEL_MARGIN - startRect.w),
227
- y: clamp(startRect.y + dy, FLOATING_PANEL_MARGIN, window.innerHeight - FLOATING_PANEL_MARGIN - startRect.h),
228
- });
294
+ setFloatRect(moveFloatingPanelRect(startRect, dx, dy));
229
295
  return;
230
296
  }
231
297
 
232
- if (floatInteraction.edge === 'left' || floatInteraction.edge === 'top') {
233
- const isLeftEdge = floatInteraction.edge === 'left';
234
- const fixedEdge = isLeftEdge ? startRect.x + startRect.w : startRect.y + startRect.h;
235
- const movingEdge = isLeftEdge ? startRect.x + dx : startRect.y + dy;
236
- const minSize = isLeftEdge ? FLOATING_PANEL_MIN_WIDTH : FLOATING_PANEL_MIN_HEIGHT;
237
- const nextEdge = clamp(movingEdge, FLOATING_PANEL_MARGIN, fixedEdge - minSize);
238
- setFloatRect(isLeftEdge ? { ...startRect, x: nextEdge, w: fixedEdge - nextEdge } : { ...startRect, y: nextEdge, h: fixedEdge - nextEdge });
239
- return;
240
- }
241
-
242
- const right = clamp(startRect.x + startRect.w + dx, startRect.x + FLOATING_PANEL_MIN_WIDTH, window.innerWidth - FLOATING_PANEL_MARGIN);
243
- const bottom = clamp(startRect.y + startRect.h + dy, startRect.y + FLOATING_PANEL_MIN_HEIGHT, window.innerHeight - FLOATING_PANEL_MARGIN);
244
- setFloatRect({
245
- ...startRect,
246
- w: floatInteraction.edge === 'bottom' ? startRect.w : right - startRect.x,
247
- h: floatInteraction.edge === 'right' ? startRect.h : bottom - startRect.y,
248
- });
298
+ setFloatRect(resizeFloatingPanelRect(floatInteraction.edge, startRect, dx, dy));
249
299
  };
250
300
 
251
301
  const onEnd = () => setFloatInteraction(null);
@@ -260,20 +310,6 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
260
310
  };
261
311
  }, [floatInteraction]);
262
312
 
263
- const handleUrlSubmit = useCallback((e: React.FormEvent) => {
264
- e.preventDefault();
265
- if (!isDocsTab) return;
266
- const input = urlInput.trim();
267
- if (!input) return;
268
- if (input.startsWith('/')) {
269
- navigate(`${DOCS_DEFAULT_BASE_URL}${input}`);
270
- } else if (input.startsWith('http')) {
271
- navigate(input);
272
- } else {
273
- navigate(`${DOCS_DEFAULT_BASE_URL}/${input}`);
274
- }
275
- }, [urlInput, navigate, isDocsTab]);
276
-
277
313
  const refreshIframe = useCallback(() => {
278
314
  setIframeReloadVersion((version) => version + 1);
279
315
  }, []);
@@ -318,10 +354,12 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
318
354
  onToggleMode={toggleMode}
319
355
  />
320
356
 
321
- <DocBrowserDocsToolbar
322
- isDocsTab={isDocsTab}
357
+ <DocBrowserAddressToolbar
358
+ isVisible={isAddressToolbarTab}
359
+ onRefresh={refreshIframe}
323
360
  onSubmit={handleUrlSubmit}
324
361
  onUrlInputChange={setUrlInput}
362
+ placeholder={isDocsTab ? t('docBrowserSearchPlaceholder') : t('docBrowserAddressPlaceholder')}
325
363
  urlInput={urlInput}
326
364
  />
327
365
 
@@ -338,7 +376,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
338
376
  isResizing={floatInteraction?.kind === 'resize'}
339
377
  />
340
378
 
341
- <DocBrowserExternalLink currentUrl={currentUrl} isDocsTab={isDocsTab} />
379
+ <DocBrowserExternalLink currentUrl={currentUrl} isVisible={isDocsTab || isContentTab} />
342
380
  </>
343
381
  );
344
382
 
@@ -361,7 +399,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
361
399
  className={cn(
362
400
  'relative flex flex-col overflow-hidden bg-card text-card-foreground',
363
401
  isFullscreen
364
- ? 'fixed inset-0 z-[9999] h-[100dvh] w-screen rounded-none border-0 shadow-2xl'
402
+ ? 'fixed inset-0 z-[var(--z-floating-panel)] h-[100dvh] w-screen rounded-none border-0 shadow-2xl'
365
403
  : 'rounded-2xl border border-border shadow-2xl',
366
404
  )}
367
405
  style={
@@ -373,7 +411,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
373
411
  top: floatRect.y,
374
412
  width: floatRect.w,
375
413
  height: floatRect.h,
376
- zIndex: 9999,
414
+ zIndex: 'var(--z-floating-panel)',
377
415
  }
378
416
  }
379
417
  >
@@ -397,6 +435,21 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
397
435
  data-testid="doc-browser-resize-bottom"
398
436
  onPointerDown={startFloatResize('bottom')}
399
437
  />
438
+ <div
439
+ className="absolute top-0 left-0 z-30 h-4 w-4 cursor-nw-resize hover:bg-primary/10 transition-colors"
440
+ data-testid="doc-browser-resize-top-left"
441
+ onPointerDown={startFloatResize('top-left')}
442
+ />
443
+ <div
444
+ className="absolute top-0 right-0 z-30 h-4 w-4 cursor-ne-resize hover:bg-primary/10 transition-colors"
445
+ data-testid="doc-browser-resize-top-right"
446
+ onPointerDown={startFloatResize('top-right')}
447
+ />
448
+ <div
449
+ className="absolute bottom-0 left-0 z-30 h-4 w-4 cursor-sw-resize hover:bg-primary/10 transition-colors"
450
+ data-testid="doc-browser-resize-bottom-left"
451
+ onPointerDown={startFloatResize('bottom-left')}
452
+ />
400
453
  <div
401
454
  className="absolute bottom-0 right-0 z-30 flex h-4 w-4 cursor-se-resize items-center justify-center text-muted-foreground/45 transition-colors hover:text-muted-foreground"
402
455
  data-testid="doc-browser-resize-bottom-right"
@@ -27,6 +27,8 @@ import {
27
27
 
28
28
  const defaultDocBrowserRouteResolver = new DefaultDocBrowserRouteResolver();
29
29
 
30
+ type RightPanelOpenedHandler = () => void;
31
+
30
32
  function updateTab(
31
33
  state: DocBrowserState,
32
34
  tabId: string,
@@ -258,7 +260,10 @@ function openDocBrowserState(
258
260
  }
259
261
 
260
262
  export class DocBrowserManager {
261
- constructor(private readonly routeResolver: DocBrowserRouteResolver = defaultDocBrowserRouteResolver) {}
263
+ constructor(
264
+ private readonly routeResolver: DocBrowserRouteResolver = defaultDocBrowserRouteResolver,
265
+ private readonly onRightPanelOpened?: RightPanelOpenedHandler,
266
+ ) {}
262
267
 
263
268
  private readonly setSnapshot = (next: DocBrowserStateUpdate): void => {
264
269
  useDocBrowserStore.getState().setSnapshot(next);
@@ -266,6 +271,7 @@ export class DocBrowserManager {
266
271
 
267
272
  readonly open = (url?: string, options?: DocBrowserOpenOptions): void => {
268
273
  this.setSnapshot((prev) => openDocBrowserState(this.routeResolver, prev, url, options));
274
+ this.onRightPanelOpened?.();
269
275
  };
270
276
 
271
277
  readonly openTarget = (target: DocBrowserRouteTarget, options?: DocBrowserOpenOptions): void => {
@@ -275,6 +281,7 @@ export class DocBrowserManager {
275
281
  kind: options?.kind ?? target.kind,
276
282
  title: options?.title ?? target.title,
277
283
  }));
284
+ this.onRightPanelOpened?.();
278
285
  };
279
286
 
280
287
  readonly openNewTab = (): void => {
@@ -287,6 +294,7 @@ export class DocBrowserManager {
287
294
 
288
295
  readonly toggleMode = (): void => {
289
296
  this.setSnapshot((prev) => ({ ...prev, mode: prev.mode === 'floating' ? 'docked' : 'floating' }));
297
+ this.onRightPanelOpened?.();
290
298
  };
291
299
 
292
300
  readonly navigate = (url: string): void => {
@@ -0,0 +1,83 @@
1
+ export type FloatingPanelRect = { x: number; y: number; w: number; h: number };
2
+ export type FloatingPanelResizeEdge =
3
+ | 'left'
4
+ | 'right'
5
+ | 'top'
6
+ | 'bottom'
7
+ | 'top-left'
8
+ | 'top-right'
9
+ | 'bottom-left'
10
+ | 'bottom-right';
11
+
12
+ const FLOATING_PANEL_MARGIN = 40;
13
+ const FLOATING_PANEL_MIN_WIDTH = 360;
14
+ const FLOATING_PANEL_MIN_HEIGHT = 400;
15
+
16
+ function clamp(value: number, min: number, max: number): number {
17
+ return Math.min(max, Math.max(min, value));
18
+ }
19
+
20
+ function resizeEdgeIncludes(
21
+ edge: FloatingPanelResizeEdge,
22
+ side: 'left' | 'right' | 'top' | 'bottom',
23
+ ): boolean {
24
+ return edge.split('-').includes(side);
25
+ }
26
+
27
+ export function createInitialFloatingPanelRect(): FloatingPanelRect {
28
+ return {
29
+ x: Math.max(FLOATING_PANEL_MARGIN, window.innerWidth - 520),
30
+ y: 80,
31
+ w: 480,
32
+ h: 600,
33
+ };
34
+ }
35
+
36
+ export function moveFloatingPanelRect(
37
+ startRect: FloatingPanelRect,
38
+ dx: number,
39
+ dy: number,
40
+ ): FloatingPanelRect {
41
+ return {
42
+ ...startRect,
43
+ x: clamp(
44
+ startRect.x + dx,
45
+ FLOATING_PANEL_MARGIN,
46
+ window.innerWidth - FLOATING_PANEL_MARGIN - startRect.w,
47
+ ),
48
+ y: clamp(
49
+ startRect.y + dy,
50
+ FLOATING_PANEL_MARGIN,
51
+ window.innerHeight - FLOATING_PANEL_MARGIN - startRect.h,
52
+ ),
53
+ };
54
+ }
55
+
56
+ export function resizeFloatingPanelRect(
57
+ edge: FloatingPanelResizeEdge,
58
+ startRect: FloatingPanelRect,
59
+ dx: number,
60
+ dy: number,
61
+ ): FloatingPanelRect {
62
+ const startRight = startRect.x + startRect.w;
63
+ const startBottom = startRect.y + startRect.h;
64
+ const nextLeft = resizeEdgeIncludes(edge, 'left')
65
+ ? clamp(startRect.x + dx, FLOATING_PANEL_MARGIN, startRight - FLOATING_PANEL_MIN_WIDTH)
66
+ : startRect.x;
67
+ const nextRight = resizeEdgeIncludes(edge, 'right')
68
+ ? clamp(startRight + dx, nextLeft + FLOATING_PANEL_MIN_WIDTH, window.innerWidth - FLOATING_PANEL_MARGIN)
69
+ : startRight;
70
+ const nextTop = resizeEdgeIncludes(edge, 'top')
71
+ ? clamp(startRect.y + dy, FLOATING_PANEL_MARGIN, startBottom - FLOATING_PANEL_MIN_HEIGHT)
72
+ : startRect.y;
73
+ const nextBottom = resizeEdgeIncludes(edge, 'bottom')
74
+ ? clamp(startBottom + dy, nextTop + FLOATING_PANEL_MIN_HEIGHT, window.innerHeight - FLOATING_PANEL_MARGIN)
75
+ : startBottom;
76
+
77
+ return {
78
+ x: nextLeft,
79
+ y: nextTop,
80
+ w: nextRight - nextLeft,
81
+ h: nextBottom - nextTop,
82
+ };
83
+ }
@@ -0,0 +1,27 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogTitle,
7
+ } from '@/shared/components/ui/dialog';
8
+
9
+ describe('Dialog', () => {
10
+ it('renders modal layers above floating panels', () => {
11
+ render(
12
+ <Dialog open>
13
+ <DialogContent>
14
+ <DialogTitle>Authorize action</DialogTitle>
15
+ </DialogContent>
16
+ </Dialog>,
17
+ );
18
+
19
+ const dialog = screen.getByRole('dialog');
20
+ const overlay = Array.from(document.body.querySelectorAll('[data-state="open"]'))
21
+ .find((element) => element instanceof HTMLElement && element.className.includes('--z-modal-backdrop'));
22
+
23
+ expect(dialog.className).toContain('z-[var(--z-modal,10050)]');
24
+ expect(overlay).toBeInstanceOf(HTMLElement);
25
+ expect((overlay as HTMLElement).className).toContain('z-[var(--z-modal-backdrop,10000)]');
26
+ });
27
+ });
@@ -14,10 +14,22 @@ type IconActionButtonProps = Omit<
14
14
  icon: React.ReactNode;
15
15
  label: string;
16
16
  tooltip?: string | false | null;
17
+ tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
17
18
  };
18
19
 
19
20
  const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonProps>(
20
- ({ className, disabled = false, icon, label, tooltip, ...buttonProps }, ref) => {
21
+ (
22
+ {
23
+ className,
24
+ disabled = false,
25
+ icon,
26
+ label,
27
+ tooltip,
28
+ tooltipSide = 'bottom',
29
+ ...buttonProps
30
+ },
31
+ ref
32
+ ) => {
21
33
  const button = (
22
34
  <button
23
35
  {...buttonProps}
@@ -42,7 +54,7 @@ const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonPro
42
54
  <TooltipTrigger asChild>
43
55
  {disabled ? <span className="inline-flex">{button}</span> : button}
44
56
  </TooltipTrigger>
45
- <TooltipContent side="bottom" className="text-xs">{content}</TooltipContent>
57
+ <TooltipContent side={tooltipSide} className="text-xs">{content}</TooltipContent>
46
58
  </Tooltip>
47
59
  </TooltipProvider>
48
60
  );
@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
19
19
  <DialogPrimitive.Overlay
20
20
  ref={ref}
21
21
  className={cn(
22
- "fixed inset-0 z-50 bg-black/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
+ "fixed inset-0 z-[var(--z-modal-backdrop,10000)] bg-black/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
23
23
  className
24
24
  )}
25
25
  {...props}
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
36
36
  <DialogPrimitive.Content
37
37
  ref={ref}
38
38
  className={cn(
39
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl border border-border bg-popover p-6 text-popover-foreground shadow-xl duration-base data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
39
+ "fixed left-[50%] top-[50%] z-[var(--z-modal,10050)] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl border border-border bg-popover p-6 text-popover-foreground shadow-xl duration-base data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
40
40
  className
41
41
  )}
42
42
  {...props}
@@ -28,7 +28,7 @@ const PopoverContent = React.forwardRef<
28
28
  align={align}
29
29
  collisionPadding={collisionPadding}
30
30
  className={cn(
31
- 'z-[var(--z-popover,50)] w-72 overflow-x-hidden overflow-y-auto rounded-2xl border border-border bg-popover p-4 text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
31
+ 'z-[var(--z-popover,10100)] w-72 overflow-x-hidden overflow-y-auto rounded-2xl border border-border bg-popover p-4 text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
32
32
  className
33
33
  )}
34
34
  style={{ maxHeight: POPOVER_CONTENT_MAX_HEIGHT, ...style }}
@@ -78,7 +78,7 @@ const SelectContent = React.forwardRef<
78
78
  <SelectPrimitive.Content
79
79
  ref={ref}
80
80
  className={cn(
81
- "relative z-[var(--z-popover,50)] flex max-h-96 min-w-[8rem] flex-col overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
81
+ "relative z-[var(--z-popover,10100)] flex max-h-96 min-w-[8rem] flex-col overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
82
82
  position === "popper" &&
83
83
  "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
84
84
  className
@@ -14,10 +14,11 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
14
14
  type="button"
15
15
  role="switch"
16
16
  aria-checked={checked}
17
+ data-state={checked ? 'checked' : 'unchecked'}
17
18
  ref={ref}
18
19
  className={cn(
19
- 'peer inline-flex h-[22px] w-10 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50',
20
- checked ? 'bg-primary' : 'bg-muted hover:bg-accent',
20
+ 'switch-track peer inline-flex h-[22px] w-10 shrink-0 cursor-pointer items-center rounded-full border-2 transition-colors duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50',
21
+ checked ? 'border-primary/70 bg-primary' : 'border-border/60 bg-muted hover:bg-accent',
21
22
  className
22
23
  )}
23
24
  onClick={() => onCheckedChange?.(!checked)}
@@ -26,7 +27,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
26
27
  <span
27
28
  data-state={checked ? 'checked' : 'unchecked'}
28
29
  className={cn(
29
- 'pointer-events-none block h-5 w-5 rounded-full bg-card shadow-md ring-0 transition-transform duration-fast',
30
+ 'switch-thumb pointer-events-none block h-5 w-5 rounded-full bg-card shadow-md ring-1 ring-border/50 transition-transform duration-fast',
30
31
  checked ? 'translate-x-5' : 'translate-x-0',
31
32
  thumbClassName
32
33
  )}
@@ -72,4 +72,58 @@ describe("CompactTabStrip", () => {
72
72
 
73
73
  expect(onSelect).toHaveBeenCalledTimes(1);
74
74
  });
75
+
76
+ it("selects a tab when clicking the tab item outside the label button", () => {
77
+ const onSelect = vi.fn();
78
+
79
+ render(
80
+ <CompactTabStrip
81
+ tabs={[
82
+ {
83
+ key: "preview-tab",
84
+ label: "Preview",
85
+ active: false,
86
+ onSelect,
87
+ },
88
+ ]}
89
+ actions={[]}
90
+ />,
91
+ );
92
+
93
+ const labelButton = screen.getByRole("button", { name: "Preview" });
94
+ const tabItem = labelButton.parentElement;
95
+
96
+ expect(tabItem).not.toBeNull();
97
+ fireEvent.click(tabItem!);
98
+
99
+ expect(onSelect).toHaveBeenCalledTimes(1);
100
+ });
101
+
102
+ it("does not select a tab when clicking its close button", () => {
103
+ const onClose = vi.fn();
104
+ const onSelect = vi.fn();
105
+
106
+ render(
107
+ <CompactTabStrip
108
+ tabs={[
109
+ {
110
+ key: "preview-tab",
111
+ label: "Preview",
112
+ active: false,
113
+ closeLabel: "Close preview",
114
+ closePlacement: "leading-hover",
115
+ leadingIcon: <span />,
116
+ onClose,
117
+ onSelect,
118
+ },
119
+ ]}
120
+ actions={[]}
121
+ />,
122
+ );
123
+
124
+ fireEvent.click(screen.getByRole("button", { name: "Close preview" }));
125
+
126
+ expect(onClose).toHaveBeenCalledTimes(1);
127
+ expect(onSelect).not.toHaveBeenCalled();
128
+ });
75
129
  });
@@ -50,10 +50,7 @@ type CompactTabStripProps = {
50
50
  onScrollPointerDown?: (event: React.PointerEvent<HTMLDivElement>) => void;
51
51
  };
52
52
 
53
- function closeCompactTab(
54
- event: React.MouseEvent<HTMLButtonElement>,
55
- onClose: () => void,
56
- ) {
53
+ function closeCompactTab(event: React.MouseEvent<HTMLButtonElement>, onClose: () => void) {
57
54
  event.stopPropagation();
58
55
  onClose();
59
56
  }
@@ -123,9 +120,11 @@ function CompactTabItem({
123
120
  return (
124
121
  <div
125
122
  ref={itemRef}
123
+ data-compact-tab-item=""
124
+ onClick={(event) => event.target === event.currentTarget && tab.onSelect()}
126
125
  className={cn(
127
126
  tabBaseClassName ??
128
- "group flex max-w-[180px] min-w-0 items-center gap-1.5 border-r border-gray-200/70 border-b-2 px-2.5 py-2 transition-colors",
127
+ "group flex max-w-[180px] min-w-0 cursor-pointer items-center gap-1.5 border-r border-gray-200/70 border-b-2 px-2.5 py-2 transition-colors",
129
128
  tab.active
130
129
  ? (activeTabClassName ?? "border-b-primary bg-white text-gray-900")
131
130
  : (inactiveTabClassName ??
@@ -18,7 +18,7 @@ const TooltipContent = React.forwardRef<
18
18
  ref={ref}
19
19
  sideOffset={sideOffset}
20
20
  className={cn(
21
- 'z-[var(--z-tooltip)] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
21
+ 'z-[var(--z-tooltip,10150)] overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
22
22
  className
23
23
  )}
24
24
  {...props}
@@ -32,7 +32,7 @@ export type SessionContextWindowView = {
32
32
  };
33
33
 
34
34
  export type SessionActivityPreviewView = {
35
- state: "running" | "completed" | "failed" | "idle";
35
+ state: "running" | "completed" | "failed" | "cancelled" | "idle";
36
36
  timestamp: string;
37
37
  statusText?: string;
38
38
  replyText?: string;