@nextclaw/ui 0.15.1 → 0.15.3

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 (192) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/assets/{api-b1R9VdeH.js → api-iNBpppzj.js} +3 -3
  3. package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-DMffQFR3.js} +1 -1
  4. package/dist/assets/{book-open-B77lQ2Tg.js → book-open-C3mJGwiy.js} +1 -1
  5. package/dist/assets/channels-list-page-CCtp0BRS.js +8 -0
  6. package/dist/assets/chat-page-H5a0dWvO.js +1 -0
  7. package/dist/assets/config-split-page-CrOJgB-Y.js +1 -0
  8. package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-Chv50USy.js} +2 -2
  9. package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-KzTuXRf_.js} +1 -1
  10. package/dist/assets/{dist-jdGmbb6w.js → dist-BunzmC4B.js} +1 -1
  11. package/dist/assets/{dist-YtoqU-AT.js → dist-ChmGwvtK.js} +1 -1
  12. package/dist/assets/doc-browser-DNRL4Zvd.js +1 -0
  13. package/dist/assets/doc-browser-Dg0pmkOg.js +1 -0
  14. package/dist/assets/doc-browser-context-BHY-0FSD.js +1 -0
  15. package/dist/assets/{es2015-bNpL2LKy.js → es2015-DQZ5JWwK.js} +1 -1
  16. package/dist/assets/{external-link-BM6fk8q-.js → external-link-CU0kzzIP.js} +1 -1
  17. package/dist/assets/form-actions-BvCp_0lS.js +1 -0
  18. package/dist/assets/{i18n-provider-CntWolRA.js → i18n-provider-Cu8XyZb_.js} +1 -1
  19. package/dist/assets/index-CzfBdvK1.css +1 -0
  20. package/dist/assets/index-DGqHne1Q.js +104 -0
  21. package/dist/assets/loader-circle-CDSFuzmk.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +1 -0
  24. package/dist/assets/model-config-page-bNNo-XyG.js +1 -0
  25. package/dist/assets/plus-D4DsdLN-.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-Cm_a4GHf.js → provider-scoped-model-input-IlgQCsoT.js} +1 -1
  27. package/dist/assets/providers-config-page-wLo6CAJw.js +1 -0
  28. package/dist/assets/{react-BN8uzPnp.js → react-BPIsQIlO.js} +1 -1
  29. package/dist/assets/{refresh-cw-BpVcFPmJ.js → refresh-cw-CILd1vAP.js} +1 -1
  30. package/dist/assets/remote-uKEYp7AR.js +1 -0
  31. package/dist/assets/rotate-cw-wPFPg1kH.js +1 -0
  32. package/dist/assets/runtime-config-page-DDn1sP4G.js +1 -0
  33. package/dist/assets/{save-BpFW5fpk.js → save-ByJqcPTP.js} +1 -1
  34. package/dist/assets/{search-D0uO7enf.js → search-DVIV6pfD.js} +1 -1
  35. package/dist/assets/search-config-page-B-Cf4iiB.js +1 -0
  36. package/dist/assets/secrets-config-page-DPKsY8Iw.js +3 -0
  37. package/dist/assets/security-config-e9SSBlKr.js +1 -0
  38. package/dist/assets/{select-Bh-CrA4p.js → select-BdgczSNw.js} +1 -1
  39. package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-BkTVUTKx.js} +1 -1
  40. package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-CH5shGg_.js} +1 -1
  41. package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-BsBvcJ9i.js} +2 -2
  42. package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-DeX1FKw7.js} +1 -1
  43. package/dist/assets/switch-Cv8Ekb3x.js +1 -0
  44. package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-RB9uKcoC.js} +1 -1
  45. package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-oQlEN-l6.js} +1 -1
  46. package/dist/assets/{use-config-CVPgsBmU.js → use-config-l3W7y8Qz.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CSeBk9SF.js → use-confirm-dialog-BAxaVOJI.js} +1 -1
  48. package/dist/assets/x-BBMffEN0.js +1 -0
  49. package/dist/index.html +38 -30
  50. package/index.html +12 -4
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
  53. package/src/app/components/layout/sidebar-items.tsx +46 -7
  54. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  55. package/src/app/components/layout/sidebar.tsx +84 -62
  56. package/src/app/configs/app-navigation.config.ts +49 -0
  57. package/src/app/styles/design-system.css +99 -4
  58. package/src/features/channels/components/config/channel-form.tsx +14 -11
  59. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
  60. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
  61. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  63. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  64. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  65. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  66. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +30 -31
  67. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  68. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  69. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  70. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  71. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  72. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  73. package/src/features/chat/features/message/components/chat-message-list.container.tsx +47 -1
  74. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  75. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  76. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  77. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  78. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  79. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  80. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  81. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  82. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  83. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  84. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  85. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +2 -2
  86. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  87. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  88. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  89. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  90. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  91. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +226 -56
  92. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  93. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  94. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  95. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  96. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +4 -2
  97. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +232 -21
  98. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  101. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +4 -0
  102. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  103. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  104. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  105. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  106. package/src/features/chat/stores/chat-thread.store.ts +3 -0
  107. package/src/features/cron/components/cron-config.tsx +63 -80
  108. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  109. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  110. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  111. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  112. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  113. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  115. package/src/features/panel-apps/index.ts +1 -0
  116. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  117. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  118. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  119. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  120. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  121. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  122. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  123. package/src/features/settings/components/config/provider-form.tsx +11 -8
  124. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  125. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  126. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  127. package/src/features/settings/pages/model-config-page.tsx +26 -25
  128. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  129. package/src/features/settings/pages/search-config-page.tsx +8 -5
  130. package/src/features/side-dock/components/side-dock.tsx +3 -3
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  132. package/src/index.css +257 -13
  133. package/src/shared/components/actions/action-link.tsx +1 -1
  134. package/src/shared/components/config-split-page.tsx +6 -6
  135. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  136. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  137. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  138. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +6 -2
  139. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  140. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  141. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  142. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  143. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  144. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  145. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  146. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  147. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  148. package/src/shared/components/ui/button.tsx +1 -1
  149. package/src/shared/components/ui/card.tsx +12 -4
  150. package/src/shared/components/ui/dialog.tsx +1 -1
  151. package/src/shared/components/ui/input.tsx +1 -1
  152. package/src/shared/components/ui/select.tsx +1 -1
  153. package/src/shared/components/ui/switch.tsx +1 -1
  154. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  155. package/src/shared/components/ui/tabs.tsx +1 -1
  156. package/src/shared/components/ui/textarea.tsx +1 -1
  157. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  158. package/src/shared/hooks/use-server-path-read.ts +11 -1
  159. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  160. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  161. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  162. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  163. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  164. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  165. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  166. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  167. package/src/shared/lib/theme/index.test.ts +21 -0
  168. package/src/shared/lib/theme/index.ts +6 -4
  169. package/src/vite-env.d.ts +2 -0
  170. package/dist/assets/channels-list-page-DcsIQc6A.js +0 -8
  171. package/dist/assets/chat-page-CZ7WxPir.js +0 -1
  172. package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
  173. package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
  174. package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
  175. package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
  176. package/dist/assets/index-BIcEw90u.js +0 -104
  177. package/dist/assets/index-BiNSTKTU.css +0 -1
  178. package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +0 -9
  180. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
  181. package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
  182. package/dist/assets/plus-vDqW5Kxp.js +0 -1
  183. package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
  184. package/dist/assets/remote-CLQ0IBM8.js +0 -1
  185. package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
  186. package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
  187. package/dist/assets/search-config-page-D0klFUXd.js +0 -1
  188. package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
  189. package/dist/assets/security-config-C5gnlkuj.js +0 -1
  190. package/dist/assets/switch-CFLJFNSd.js +0 -1
  191. package/dist/assets/x-BF2KQ--V.js +0 -1
  192. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -1,7 +1,8 @@
1
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
2
  import {
3
- DOCS_DEFAULT_BASE_URL,
4
3
  DOC_BROWSER_HOME_TAB_KIND,
4
+ getDefaultDocsUrl,
5
+ getDocsUrl,
5
6
  type DocBrowserDockControls,
6
7
  type DocBrowserDockState,
7
8
  type DocBrowserTab,
@@ -137,11 +138,11 @@ function useDocBrowserAddressBar({
137
138
  const input = urlInput.trim();
138
139
  if (!input) return;
139
140
  if (isDocsTab && input.startsWith('/')) {
140
- navigate(`${DOCS_DEFAULT_BASE_URL}${input}`);
141
+ navigate(getDocsUrl(input));
141
142
  } else if (isDocsTab && input.startsWith('http')) {
142
143
  navigate(input);
143
144
  } else if (isDocsTab) {
144
- navigate(`${DOCS_DEFAULT_BASE_URL}/${input}`);
145
+ navigate(getDocsUrl(`/${input}`));
145
146
  } else {
146
147
  navigate(resolveContentUrlInput(input, currentUrl));
147
148
  }
@@ -180,7 +181,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
180
181
  const [floatRect, setFloatRect] = useState<FloatingPanelRect>(createInitialFloatingPanelRect);
181
182
  const [floatInteraction, setFloatInteraction] = useState<FloatingPanelInteraction | null>(null);
182
183
  const iframeRef = useRef<HTMLIFrameElement>(null);
183
- const currentUrl = currentTab?.currentUrl ?? DOCS_DEFAULT_BASE_URL;
184
+ const currentUrl = currentTab?.currentUrl ?? getDefaultDocsUrl();
184
185
  const navVersion = currentTab?.navVersion ?? 0;
185
186
  const iframeInstanceId = `${activeTabId}:${navVersion}:${iframeReloadVersion}`;
186
187
  const pendingParentDocsUrlRef = useRef<string | null>(null);
@@ -374,6 +375,7 @@ export function DocBrowser({ customTabRenderers = {}, displayMode = 'desktop', d
374
375
  iframeSandbox={iframeSandbox}
375
376
  isDragging={floatInteraction?.kind === 'drag'}
376
377
  isResizing={floatInteraction?.kind === 'resize'}
378
+ onIframePointerOver={customRenderer?.onIframePointerOver}
377
379
  />
378
380
 
379
381
  <DocBrowserExternalLink currentUrl={currentUrl} isVisible={isDocsTab || isContentTab} />
@@ -2,15 +2,19 @@ import { getLanguage, type I18nLanguage } from '@/shared/lib/i18n';
2
2
  import type { DocBrowserTabKind } from '@/shared/components/doc-browser/types/doc-browser.types';
3
3
 
4
4
  const DOCS_PRIMARY_DOMAIN = 'docs.nextclaw.io';
5
+ const DOCS_MAINLAND_DOMAIN = 'docs.nextclaw.net';
5
6
  const DOCS_PAGES_DEV = 'nextclaw-docs.pages.dev';
6
7
  const DOCS_HOSTS = new Set([
7
8
  DOCS_PRIMARY_DOMAIN,
8
9
  `www.${DOCS_PRIMARY_DOMAIN}`,
10
+ DOCS_MAINLAND_DOMAIN,
11
+ `www.${DOCS_MAINLAND_DOMAIN}`,
9
12
  DOCS_PAGES_DEV,
10
13
  `www.${DOCS_PAGES_DEV}`,
11
14
  ]);
12
15
 
13
16
  export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_PRIMARY_DOMAIN}`;
17
+ export const DOCS_MAINLAND_BASE_URL = `https://${DOCS_MAINLAND_DOMAIN}`;
14
18
  export const DOC_BROWSER_HOME_TAB_KIND = 'home';
15
19
  export const DOC_BROWSER_HOME_URL = 'nextclaw://new-tab';
16
20
  const DOCS_DEFAULT_GUIDE_PATH = '/guide/getting-started';
@@ -33,19 +37,68 @@ function ensureLocalizedDocsPath(pathname: string, locale: 'en' | 'zh'): string
33
37
  return `/${locale}${normalized}`;
34
38
  }
35
39
 
40
+ function normalizeDocsBaseUrl(value?: string): string | undefined {
41
+ const raw = value?.trim();
42
+ if (!raw) {
43
+ return undefined;
44
+ }
45
+
46
+ try {
47
+ return new URL(raw).origin;
48
+ } catch {
49
+ return undefined;
50
+ }
51
+ }
52
+
53
+ function getBrowserTimeZone(): string {
54
+ try {
55
+ return Intl.DateTimeFormat().resolvedOptions().timeZone ?? '';
56
+ } catch {
57
+ return '';
58
+ }
59
+ }
60
+
61
+ function isAbsoluteUrl(value: string): boolean {
62
+ return /^[a-z][a-z\d+.-]*:/i.test(value.trim());
63
+ }
64
+
65
+ function isLikelyMainlandDocsAudience(): boolean {
66
+ return getLanguage() === 'zh' || getBrowserTimeZone() === 'Asia/Shanghai';
67
+ }
68
+
69
+ export function getDocsBaseUrl(): string {
70
+ const configuredBaseUrl = normalizeDocsBaseUrl(import.meta.env.VITE_NEXTCLAW_DOCS_BASE_URL);
71
+ if (configuredBaseUrl) {
72
+ return configuredBaseUrl;
73
+ }
74
+
75
+ if (isLikelyMainlandDocsAudience()) {
76
+ return normalizeDocsBaseUrl(import.meta.env.VITE_NEXTCLAW_DOCS_CN_BASE_URL) ?? DOCS_MAINLAND_BASE_URL;
77
+ }
78
+
79
+ return DOCS_DEFAULT_BASE_URL;
80
+ }
81
+
36
82
  function resolveLocalizedDocsUrl(url: string): string {
37
83
  const locale = toDocsLocale(getLanguage());
84
+ const docsBaseUrl = getDocsBaseUrl();
38
85
 
39
86
  try {
40
- const parsed = new URL(url, DOCS_DEFAULT_BASE_URL);
41
- if (!DOCS_HOSTS.has(parsed.hostname)) {
87
+ const parsed = new URL(url, docsBaseUrl);
88
+ const docsBase = new URL(docsBaseUrl);
89
+ const shouldResolveAsDocs = !isAbsoluteUrl(url)
90
+ || DOCS_HOSTS.has(parsed.hostname)
91
+ || parsed.origin === docsBase.origin;
92
+ if (!shouldResolveAsDocs) {
42
93
  return parsed.toString();
43
94
  }
44
95
 
96
+ parsed.protocol = docsBase.protocol;
97
+ parsed.host = docsBase.host;
45
98
  parsed.pathname = ensureLocalizedDocsPath(parsed.pathname, locale);
46
99
  return parsed.toString();
47
100
  } catch {
48
- return new URL(`/${locale}${DOCS_DEFAULT_GUIDE_PATH}`, DOCS_DEFAULT_BASE_URL).toString();
101
+ return new URL(`/${locale}${DOCS_DEFAULT_GUIDE_PATH}`, docsBaseUrl).toString();
49
102
  }
50
103
  }
51
104
 
@@ -53,6 +106,10 @@ export function getDefaultDocsUrl(): string {
53
106
  return resolveLocalizedDocsUrl(DOCS_DEFAULT_GUIDE_PATH);
54
107
  }
55
108
 
109
+ export function getDocsUrl(pathOrUrl: string): string {
110
+ return resolveLocalizedDocsUrl(pathOrUrl);
111
+ }
112
+
56
113
  export function normalizeDocUrl(url: string): string {
57
114
  try {
58
115
  return new URL(url).pathname.replace(/\.html$/, '').replace(/\/$/, '');
@@ -78,7 +135,8 @@ export function inferTabTitle(url: string, kind: DocBrowserTabKind, fallback = '
78
135
 
79
136
  export function isDocsUrl(url: string): boolean {
80
137
  try {
81
- const parsed = new URL(url, window.location.origin);
138
+ const fallbackOrigin = typeof window !== 'undefined' ? window.location.origin : DOCS_DEFAULT_BASE_URL;
139
+ const parsed = new URL(url, fallbackOrigin);
82
140
  return DOCS_HOSTS.has(parsed.hostname);
83
141
  } catch {
84
142
  return false;
@@ -63,4 +63,22 @@ describe("ResizableRightPanel", () => {
63
63
  ).toBeNull();
64
64
  expect(screen.getByTestId("right-panel").className).toContain("fixed");
65
65
  });
66
+
67
+ it("can scope overlay positioning to its container", () => {
68
+ render(
69
+ <ResizableRightPanel
70
+ data-testid="right-panel"
71
+ overlay
72
+ overlayScope="container"
73
+ >
74
+ Content
75
+ </ResizableRightPanel>,
76
+ );
77
+
78
+ expect(
79
+ screen.queryByTestId("resizable-right-panel-handle"),
80
+ ).toBeNull();
81
+ expect(screen.getByTestId("right-panel").className).toContain("absolute");
82
+ expect(screen.getByTestId("right-panel").className).not.toContain("fixed");
83
+ });
66
84
  });
@@ -1,12 +1,33 @@
1
1
  import { useRef, useState, type ComponentPropsWithoutRef } from "react";
2
2
  import { cn } from "@/shared/lib/utils";
3
3
 
4
- type ResizableRightPanelProps = ComponentPropsWithoutRef<"aside"> & { defaultWidth?: number; minWidth?: number; maxWidth?: number; overlay?: boolean };
4
+ type ResizableRightPanelProps = ComponentPropsWithoutRef<"aside"> & {
5
+ defaultWidth?: number;
6
+ minWidth?: number;
7
+ maxWidth?: number;
8
+ overlay?: boolean;
9
+ overlayScope?: "viewport" | "container";
10
+ };
5
11
 
6
- export function ResizableRightPanel({ children, className, style, defaultWidth = 420, minWidth = 320, maxWidth = 860, overlay = false, ...props }: ResizableRightPanelProps) {
12
+ export function ResizableRightPanel({
13
+ children,
14
+ className,
15
+ style,
16
+ defaultWidth = 420,
17
+ minWidth = 320,
18
+ maxWidth = 860,
19
+ overlay = false,
20
+ overlayScope = "viewport",
21
+ ...props
22
+ }: ResizableRightPanelProps) {
7
23
  const resizeRef = useRef<{ startX: number; startWidth: number } | null>(null);
8
24
  const [isResizing, setIsResizing] = useState(false);
9
25
  const [width, setWidth] = useState(defaultWidth);
26
+ const overlayClassName = overlay
27
+ ? overlayScope === "container"
28
+ ? "absolute inset-0 z-30"
29
+ : "fixed inset-0 z-40"
30
+ : "border-l border-border";
10
31
 
11
32
  const onResizeStart = (event: React.PointerEvent<HTMLDivElement>) => {
12
33
  if (overlay) return;
@@ -36,7 +57,11 @@ export function ResizableRightPanel({ children, className, style, defaultWidth =
36
57
  return (
37
58
  <aside
38
59
  {...props}
39
- className={cn("relative flex h-full min-h-0 shrink-0 overflow-hidden bg-card text-card-foreground", overlay ? "fixed inset-0 z-40" : "border-l border-border", className)}
60
+ className={cn(
61
+ "relative flex h-full min-h-0 shrink-0 overflow-hidden bg-card text-card-foreground",
62
+ overlayClassName,
63
+ className,
64
+ )}
40
65
  style={overlay ? style : { ...style, width }}
41
66
  >
42
67
  {!overlay ? (
@@ -0,0 +1,36 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
4
+
5
+ describe('IconActionButton', () => {
6
+ it('applies compact size and strong tone classes for nested hover surfaces', () => {
7
+ render(
8
+ <IconActionButton
9
+ size="sm"
10
+ tone="strong"
11
+ icon={<span data-testid="icon" />}
12
+ label="Pin"
13
+ />,
14
+ );
15
+
16
+ const button = screen.getByRole('button', { name: 'Pin' });
17
+ expect(button.className).toContain('h-6');
18
+ expect(button.className).toContain('w-6');
19
+ expect(button.className).toContain('p-1');
20
+ expect(button.className).toContain('hover:bg-black/10');
21
+ });
22
+
23
+ it('defaults to medium size and soft accent hover', () => {
24
+ render(
25
+ <IconActionButton
26
+ icon={<span data-testid="icon" />}
27
+ label="More"
28
+ />,
29
+ );
30
+
31
+ const button = screen.getByRole('button', { name: 'More' });
32
+ expect(button.className).toContain('h-7');
33
+ expect(button.className).toContain('w-7');
34
+ expect(button.className).toContain('hover:bg-accent');
35
+ });
36
+ });
@@ -0,0 +1,25 @@
1
+ import type { HTMLAttributes } from 'react';
2
+ import { cn } from '@/shared/lib/utils';
3
+
4
+ type FormActionsProps = HTMLAttributes<HTMLDivElement> & {
5
+ align?: 'start' | 'end' | 'between';
6
+ };
7
+
8
+ export function FormActions({
9
+ align = 'end',
10
+ className,
11
+ ...props
12
+ }: FormActionsProps) {
13
+ return (
14
+ <div
15
+ className={cn(
16
+ 'flex flex-wrap items-center gap-2',
17
+ align === 'start' && 'justify-start',
18
+ align === 'end' && 'justify-end',
19
+ align === 'between' && 'justify-between',
20
+ className,
21
+ )}
22
+ {...props}
23
+ />
24
+ );
25
+ }
@@ -7,16 +7,35 @@ import {
7
7
  } from '@/shared/components/ui/tooltip';
8
8
  import { cn } from '@/shared/lib/utils';
9
9
 
10
+ type IconActionButtonSize = 'sm' | 'md' | 'lg';
11
+ type IconActionButtonTone = 'default' | 'strong';
12
+
10
13
  type IconActionButtonProps = Omit<
11
14
  React.ButtonHTMLAttributes<HTMLButtonElement>,
12
15
  'children' | 'type'
13
16
  > & {
14
17
  icon: React.ReactNode;
15
18
  label: string;
19
+ size?: IconActionButtonSize;
20
+ /** default: soft accent hover. strong: denser gray hover for nested hover surfaces. */
21
+ tone?: IconActionButtonTone;
16
22
  tooltip?: string | false | null;
17
23
  tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
18
24
  };
19
25
 
26
+ const SIZE_CLASS: Record<IconActionButtonSize, string> = {
27
+ sm: 'h-6 w-6 rounded-md p-1',
28
+ md: 'h-7 w-7 rounded-md p-1.5',
29
+ lg: 'h-8 w-8 rounded-lg p-1.5',
30
+ };
31
+
32
+ const TONE_CLASS: Record<IconActionButtonTone, string> = {
33
+ default:
34
+ 'text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
35
+ strong:
36
+ 'text-muted-foreground hover:bg-black/10 hover:text-foreground disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
37
+ };
38
+
20
39
  const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonProps>(
21
40
  (
22
41
  {
@@ -24,6 +43,8 @@ const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonPro
24
43
  disabled = false,
25
44
  icon,
26
45
  label,
46
+ size = 'md',
47
+ tone = 'default',
27
48
  tooltip,
28
49
  tooltipSide = 'bottom',
29
50
  ...buttonProps
@@ -38,8 +59,9 @@ const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonPro
38
59
  disabled={disabled}
39
60
  aria-label={label}
40
61
  className={cn(
41
- 'inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 disabled:cursor-not-allowed',
42
- 'text-muted-foreground hover:bg-accent hover:text-accent-foreground disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
62
+ 'inline-flex shrink-0 items-center justify-center transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed',
63
+ SIZE_CLASS[size],
64
+ TONE_CLASS[tone],
43
65
  className
44
66
  )}
45
67
  >
@@ -63,3 +85,4 @@ const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonPro
63
85
  IconActionButton.displayName = 'IconActionButton';
64
86
 
65
87
  export { IconActionButton };
88
+ export type { IconActionButtonProps, IconActionButtonSize, IconActionButtonTone };
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from 'class-variance-authority';
3
3
  import { cn } from '@/shared/lib/utils';
4
4
 
5
5
  const buttonVariants = cva(
6
- 'inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-colors duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
6
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium ring-offset-background transition-colors duration-fast focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:pointer-events-none disabled:opacity-50',
7
7
  {
8
8
  variants: {
9
9
  variant: {
@@ -3,13 +3,21 @@ import { cn } from '@/shared/lib/utils';
3
3
 
4
4
  const Card = React.forwardRef<
5
5
  HTMLDivElement,
6
- React.HTMLAttributes<HTMLDivElement> & { hover?: boolean }
7
- >(({ className, hover = true, ...props }, ref) => (
6
+ React.HTMLAttributes<HTMLDivElement> & {
7
+ hover?: boolean;
8
+ /** Flat settings-style surface: light border, no shadow. */
9
+ surface?: 'raised' | 'flat';
10
+ }
11
+ >(({ className, hover = true, surface = 'raised', ...props }, ref) => (
8
12
  <div
9
13
  ref={ref}
10
14
  className={cn(
11
- 'rounded-2xl border border-border bg-card text-card-foreground shadow-card transition-all duration-base',
12
- hover && 'hover:border-primary/25 hover:shadow-card-hover',
15
+ 'rounded-2xl border bg-card text-card-foreground transition-colors duration-base',
16
+ surface === 'raised'
17
+ ? 'border-border shadow-card'
18
+ : 'border-border/75 shadow-none',
19
+ hover && surface === 'raised' && 'hover:border-primary/25 hover:shadow-card-hover',
20
+ hover && surface === 'flat' && 'hover:bg-muted/40',
13
21
  className
14
22
  )}
15
23
  {...props}
@@ -42,7 +42,7 @@ const DialogContent = React.forwardRef<
42
42
  {...props}
43
43
  >
44
44
  {children}
45
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg p-1 opacity-70 ring-offset-background transition-all duration-fast hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none">
45
+ <DialogPrimitive.Close className="absolute right-4 top-4 rounded-lg p-1 opacity-70 transition-all duration-fast hover:bg-accent hover:text-accent-foreground hover:opacity-100 focus:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:pointer-events-none">
46
46
  <X className="h-4 w-4 text-muted-foreground" />
47
47
  <span className="sr-only">Close</span>
48
48
  </DialogPrimitive.Close>
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
9
9
  <input
10
10
  type={type}
11
11
  className={cn(
12
- 'flex h-9 w-full rounded-xl border border-border bg-card px-3.5 py-2 text-sm text-foreground file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/55 placeholder:font-normal focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary/50 transition-colors disabled:cursor-not-allowed disabled:opacity-50',
12
+ 'flex h-9 w-full rounded-xl border border-border/75 bg-card px-3.5 py-2 text-sm text-foreground file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/55 placeholder:font-normal transition-colors focus:outline-none focus:ring-0 focus:border-border disabled:cursor-not-allowed disabled:opacity-50',
13
13
  className
14
14
  )}
15
15
  ref={ref}
@@ -25,7 +25,7 @@ const SelectTrigger = React.forwardRef<
25
25
  <SelectPrimitive.Trigger
26
26
  ref={ref}
27
27
  className={cn(
28
- "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-xl border border-border bg-card px-3 py-2 text-sm text-foreground shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/40 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
28
+ "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-xl border border-border/75 bg-card px-3 py-2 text-sm text-foreground ring-offset-background placeholder:text-muted-foreground transition-colors focus:outline-none focus:ring-0 focus:border-border disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
29
29
  className
30
30
  )}
31
31
  {...props}
@@ -17,7 +17,7 @@ const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
17
17
  data-state={checked ? 'checked' : 'unchecked'}
18
18
  ref={ref}
19
19
  className={cn(
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',
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-1 focus-visible:ring-border disabled:cursor-not-allowed disabled:opacity-50',
21
21
  checked ? 'border-primary/70 bg-primary' : 'border-border/60 bg-muted hover:bg-accent',
22
22
  className
23
23
  )}
@@ -147,11 +147,12 @@ function CompactTabItem({
147
147
  <CompactTabLabelButton tab={tab} labelClassName={labelClassName} />
148
148
  {tab.onClose && tab.closePlacement !== "leading-hover" ? (
149
149
  <IconActionButton
150
+ size="sm"
151
+ tone="strong"
150
152
  icon={<X className="h-3 w-3" />}
151
153
  label={tab.closeLabel ?? ""}
152
154
  tooltip={tab.closeLabel ?? null}
153
155
  onClick={(event) => closeCompactTab(event, tab.onClose!)}
154
- className="h-5 w-5 rounded p-0.5 hover:bg-black/10"
155
156
  />
156
157
  ) : null}
157
158
  </div>
@@ -64,7 +64,7 @@ export function TabsTrigger({ value, children, className }: TabsTriggerProps) {
64
64
  aria-pressed={isActive}
65
65
  data-state={isActive ? 'active' : 'inactive'}
66
66
  className={cn(
67
- 'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-[13px] font-medium ring-offset-background transition-all duration-fast focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
67
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-[13px] font-medium transition-all duration-fast focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:pointer-events-none disabled:opacity-50',
68
68
  isActive
69
69
  ? 'bg-card text-foreground shadow-sm'
70
70
  : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground',
@@ -7,7 +7,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
7
7
  ({ className, ...props }, ref) => (
8
8
  <textarea
9
9
  className={cn(
10
- "flex min-h-28 w-full rounded-xl border border-border bg-card px-3.5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/55 placeholder:font-normal focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary/50 transition-colors disabled:cursor-not-allowed disabled:opacity-50",
10
+ "flex min-h-28 w-full rounded-xl border border-border/75 bg-card px-3.5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/55 placeholder:font-normal transition-colors focus:outline-none focus:ring-0 focus:border-border disabled:cursor-not-allowed disabled:opacity-50",
11
11
  className,
12
12
  )}
13
13
  ref={ref}
@@ -1,19 +1,41 @@
1
1
  import { useQuery } from '@tanstack/react-query';
2
2
  import { fetchServerPathBrowse } from '@/shared/lib/api';
3
3
 
4
+ export function buildServerPathBrowseQueryKey(params: {
5
+ path?: string | null;
6
+ basePath?: string | null;
7
+ includeFiles?: boolean;
8
+ }) {
9
+ return [
10
+ 'server-path-browse',
11
+ params.path?.trim() ?? '',
12
+ params.basePath?.trim() ?? '',
13
+ params.includeFiles ?? false,
14
+ ] as const;
15
+ }
16
+
4
17
  export function useServerPathBrowse(params: {
5
18
  path?: string | null;
19
+ basePath?: string | null;
6
20
  includeFiles?: boolean;
7
21
  enabled?: boolean;
8
22
  }) {
23
+ const { basePath, enabled, includeFiles, path } = params;
24
+ const normalizedPath = path?.trim() ?? '';
25
+ const normalizedBasePath = basePath?.trim() ?? '';
9
26
  return useQuery({
10
- queryKey: ['server-path-browse', params.path ?? null, params.includeFiles ?? false],
27
+ queryKey: buildServerPathBrowseQueryKey({
28
+ path: normalizedPath,
29
+ basePath: normalizedBasePath,
30
+ includeFiles,
31
+ }),
11
32
  queryFn: () =>
12
33
  fetchServerPathBrowse({
13
- path: params.path,
14
- includeFiles: params.includeFiles,
34
+ path: normalizedPath,
35
+ basePath: normalizedBasePath,
36
+ includeFiles,
15
37
  }),
16
- enabled: params.enabled ?? true,
38
+ enabled: enabled ?? true,
17
39
  staleTime: 0,
18
40
  });
19
41
  }
@@ -1,6 +1,13 @@
1
1
  import { useQuery } from '@tanstack/react-query';
2
2
  import { fetchServerPathRead } from '@/shared/lib/api';
3
3
 
4
+ export function buildServerPathReadQueryKey(params: {
5
+ path?: string | null;
6
+ basePath?: string | null;
7
+ }) {
8
+ return ['server-path-read', params.path?.trim() ?? '', params.basePath ?? null] as const;
9
+ }
10
+
4
11
  export function useServerPathRead(params: {
5
12
  path?: string | null;
6
13
  basePath?: string | null;
@@ -8,7 +15,10 @@ export function useServerPathRead(params: {
8
15
  }) {
9
16
  const normalizedPath = params.path?.trim() ?? '';
10
17
  return useQuery({
11
- queryKey: ['server-path-read', normalizedPath, params.basePath ?? null],
18
+ queryKey: buildServerPathReadQueryKey({
19
+ path: normalizedPath,
20
+ basePath: params.basePath,
21
+ }),
12
22
  queryFn: () =>
13
23
  fetchServerPathRead({
14
24
  path: normalizedPath,
@@ -17,6 +17,7 @@ function encodeServerPathSegments(path: string): string {
17
17
 
18
18
  export async function fetchServerPathBrowse(params?: {
19
19
  path?: string | null;
20
+ basePath?: string | null;
20
21
  includeFiles?: boolean;
21
22
  }): Promise<ServerPathBrowseView> {
22
23
  return await nextclawClient.serverPaths.browse(params);