@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
@@ -0,0 +1,84 @@
1
+ import { useEffect } from 'react';
2
+ import { fetchMarketplaceSkillContent } from '@/shared/lib/api';
3
+ import { t } from '@/shared/lib/i18n';
4
+ import {
5
+ setMarketplaceDetailDocEntry,
6
+ useMarketplaceDetailDocStore,
7
+ } from '@/features/marketplace/stores/marketplace-detail-doc.store';
8
+
9
+ function parseMarketplaceDetailScopeKey(
10
+ detailId: string,
11
+ ): { scope: string; key: string } | null {
12
+ const separator = detailId.indexOf(':');
13
+ if (separator <= 0 || separator >= detailId.length - 1) {
14
+ return null;
15
+ }
16
+ return {
17
+ scope: detailId.slice(0, separator),
18
+ key: detailId.slice(separator + 1),
19
+ };
20
+ }
21
+
22
+ function isRehydratableSkillScope(scope: string): boolean {
23
+ return scope === 'skill' || scope === 'skill-preview';
24
+ }
25
+
26
+ export function useMarketplaceDetailDocEntry(detailId: string | null) {
27
+ const entry = useMarketplaceDetailDocStore((state) =>
28
+ detailId ? state.entries[detailId] : undefined,
29
+ );
30
+
31
+ useEffect(() => {
32
+ if (!detailId || useMarketplaceDetailDocStore.getState().entries[detailId]) {
33
+ return;
34
+ }
35
+ const parsed = parseMarketplaceDetailScopeKey(detailId);
36
+ if (!parsed || !isRehydratableSkillScope(parsed.scope)) {
37
+ return;
38
+ }
39
+
40
+ let cancelled = false;
41
+ const title = parsed.key;
42
+ const typeLabel = t('marketplaceTypeSkill');
43
+
44
+ void fetchMarketplaceSkillContent(parsed.key)
45
+ .then((content) => {
46
+ if (cancelled) {
47
+ return;
48
+ }
49
+ setMarketplaceDetailDocEntry({
50
+ id: detailId,
51
+ title,
52
+ typeLabel,
53
+ spec: parsed.key,
54
+ status: 'ready',
55
+ summary: t('marketplaceInstalledLocalSummary'),
56
+ metadataRaw: content.metadataRaw,
57
+ contentRaw: content.bodyRaw || content.raw,
58
+ sourceUrl: content.sourceUrl,
59
+ sourceLabel: content.source ? `Source (${content.source})` : undefined,
60
+ });
61
+ })
62
+ .catch(() => {
63
+ if (cancelled) {
64
+ return;
65
+ }
66
+ setMarketplaceDetailDocEntry({
67
+ id: detailId,
68
+ title,
69
+ typeLabel,
70
+ spec: parsed.key,
71
+ status: 'error',
72
+ summary: t('marketplaceInstalledLocalSummary'),
73
+ metadataRaw: JSON.stringify({ skill: parsed.key }, null, 2),
74
+ contentRaw: t('marketplaceDetailUnavailableDescription'),
75
+ });
76
+ });
77
+
78
+ return () => {
79
+ cancelled = true;
80
+ };
81
+ }, [detailId]);
82
+
83
+ return entry;
84
+ }
@@ -38,11 +38,11 @@ export function PanelAppListItem({
38
38
  };
39
39
 
40
40
  return (
41
- <div className="group w-full min-w-0 rounded-lg border border-border bg-card px-2.5 py-2.5 transition-colors hover:border-primary/35 hover:bg-accent/60">
41
+ <div className="group w-full min-w-0 rounded-lg border border-border/60 bg-card px-2.5 py-2.5 transition-colors hover:bg-muted/40">
42
42
  <div className="flex min-w-0 items-start gap-2">
43
43
  <button type="button" onClick={onOpen} className="min-w-0 flex-1 text-left">
44
44
  <span className="flex min-w-0 items-center gap-2">
45
- <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-accent text-base text-accent-foreground">
45
+ <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted text-base text-muted-foreground">
46
46
  <PanelAppIcon icon={entry.icon} title={entry.title} />
47
47
  </span>
48
48
  <span className="block min-w-0 flex-1 truncate text-sm font-medium text-foreground">{entry.title}</span>
@@ -52,22 +52,22 @@ export function PanelAppListItem({
52
52
  ) : null}
53
53
  <span className="mt-0.5 block truncate text-[11px] leading-4 text-muted-foreground/70">{secondary}</span>
54
54
  </button>
55
- <div className="flex shrink-0 items-center gap-1">
55
+ <div className="flex shrink-0 items-center gap-0.5 opacity-70 transition-opacity group-hover:opacity-100">
56
56
  <button
57
57
  type="button"
58
58
  onClick={handleFavorite}
59
59
  disabled={favoritePending}
60
- className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-background hover:text-amber-500 disabled:opacity-50"
60
+ className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
61
61
  title={favoriteLabel}
62
62
  aria-label={favoriteLabel}
63
63
  >
64
- <Star className={entry.favorite ? 'h-4 w-4 fill-amber-400 text-amber-500' : 'h-4 w-4'} />
64
+ <Star className={entry.favorite ? 'h-4 w-4 fill-current text-foreground' : 'h-4 w-4'} />
65
65
  </button>
66
66
  <Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
67
67
  <PopoverTrigger asChild>
68
68
  <button
69
69
  type="button"
70
- className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-background hover:text-foreground"
70
+ className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
71
71
  aria-label={t('panelAppsMoreActions')}
72
72
  disabled={deletePending}
73
73
  >
@@ -204,7 +204,7 @@ function PanelAppsEmptyGuide({
204
204
  <button
205
205
  type="button"
206
206
  onClick={onRefresh}
207
- className="mt-4 inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
207
+ className="mt-4 inline-flex items-center justify-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
208
208
  >
209
209
  <RefreshCw className="h-3.5 w-3.5" />
210
210
  {t('panelAppsRefresh')}
@@ -245,7 +245,7 @@ function PanelAppsEmptyGuideStep({
245
245
  <button
246
246
  type="button"
247
247
  onClick={onAction}
248
- className="flex w-full gap-2 rounded-md bg-muted/60 px-3 py-2.5 text-left transition-colors hover:bg-accent/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
248
+ className="flex w-full gap-2 rounded-md bg-muted/60 px-3 py-2.5 text-left transition-colors hover:bg-accent/70 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
249
249
  >
250
250
  {content}
251
251
  </button>
@@ -2,6 +2,7 @@ export { PanelAppsList } from './components/panel-apps-list';
2
2
  export { usePanelApps } from './hooks/use-panel-apps';
3
3
  export { PanelAppBridgeManager } from './managers/panel-app-bridge.manager';
4
4
  export { findPanelAppEntryByDisplayId } from './utils/panel-app-entry-match.utils';
5
+ export { PANEL_APP_IFRAME_SANDBOX, focusPanelAppIframe } from './utils/panel-app-iframe.utils';
5
6
  export {
6
7
  openApps,
7
8
  PANEL_APPS_DOC_BROWSER_RENDERERS,
@@ -14,20 +14,11 @@ import {
14
14
  RightPanelResourceHomePage,
15
15
  } from '@/features/right-panel-resources';
16
16
  import { t } from '@/shared/lib/i18n';
17
+ import { PANEL_APP_IFRAME_SANDBOX, focusPanelAppIframe } from './panel-app-iframe.utils';
17
18
 
18
19
  export const APPS_TAB_KIND = RIGHT_PANEL_APPS_TAB_KIND;
19
20
  export const PANEL_APP_TAB_KIND = RIGHT_PANEL_PANEL_APP_TAB_KIND;
20
21
  const DEFAULT_APPS_PANEL_TAB: AppsPanelTab = 'panel-apps';
21
- const PANEL_APP_IFRAME_SANDBOX = [
22
- 'allow-scripts',
23
- 'allow-forms',
24
- 'allow-modals',
25
- 'allow-popups',
26
- 'allow-popups-to-escape-sandbox',
27
- 'allow-downloads',
28
- 'allow-pointer-lock',
29
- 'allow-presentation',
30
- ].join(' ');
31
22
 
32
23
  function isAppsPanelTab(value: unknown): value is AppsPanelTab {
33
24
  return value === 'panel-apps' || value === 'service-apps';
@@ -79,6 +70,7 @@ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
79
70
  getIframeSandbox: () => PANEL_APP_IFRAME_SANDBOX,
80
71
  getTitle: (tab) => tab.title || t('panelAppsTitle'),
81
72
  onIframeMessage: (params) => getPresenter().panelAppBridgeManager.handleIframeMessage(params),
73
+ onIframePointerOver: (event) => focusPanelAppIframe(event.currentTarget),
82
74
  renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
83
75
  renderToolbar: ({ open, refreshIframe, tab }) => (
84
76
  <PanelAppToolbar
@@ -0,0 +1,6 @@
1
+ export const PANEL_APP_IFRAME_SANDBOX = 'allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-downloads allow-pointer-lock allow-presentation';
2
+
3
+ export function focusPanelAppIframe(iframe: HTMLIFrameElement | null): void {
4
+ iframe?.focus({ preventScroll: true });
5
+ iframe?.contentWindow?.focus();
6
+ }
@@ -12,12 +12,12 @@ describe('PwaShellThemeManager', () => {
12
12
  document.body.style.backgroundColor = '';
13
13
  });
14
14
 
15
- it('applies natural shell colors by default', () => {
15
+ it('applies default work shell colors when no theme is selected', () => {
16
16
  pwaShellThemeManager.syncCurrentTheme();
17
17
 
18
18
  const meta = document.querySelector('meta[name="theme-color"]');
19
- expect(meta?.getAttribute('content')).toBe('#FAF9F7');
20
- expect(document.body.style.backgroundColor).toBe('rgb(250, 249, 247)');
19
+ expect(meta?.getAttribute('content')).toBe('#FFFFFF');
20
+ expect(document.body.style.backgroundColor).toBe('rgb(255, 255, 255)');
21
21
  });
22
22
 
23
23
  it('applies natural shell colors explicitly', () => {
@@ -36,6 +36,14 @@ describe('PwaShellThemeManager', () => {
36
36
  expect(document.body.style.backgroundColor).toBe('rgb(255, 255, 255)');
37
37
  });
38
38
 
39
+ it('applies work shell colors explicitly', () => {
40
+ pwaShellThemeManager.syncTheme('work');
41
+
42
+ const meta = document.querySelector('meta[name="theme-color"]');
43
+ expect(meta?.getAttribute('content')).toBe('#FFFFFF');
44
+ expect(document.body.style.backgroundColor).toBe('rgb(255, 255, 255)');
45
+ });
46
+
39
47
  it('applies warm shell colors', () => {
40
48
  pwaShellThemeManager.syncTheme('warm');
41
49
 
@@ -1,4 +1,5 @@
1
1
  import {
2
+ DEFAULT_UI_THEME,
2
3
  getThemeAppearance,
3
4
  normalizeTheme,
4
5
  type UiTheme,
@@ -6,6 +7,7 @@ import {
6
7
 
7
8
  const PWA_SHELL_THEME_COLORS: Record<UiTheme, string> = {
8
9
  natural: '#FAF9F7',
10
+ work: '#FFFFFF',
9
11
  minimal: '#FFFFFF',
10
12
  warm: '#FAF8F4',
11
13
  cool: '#F8FAFC',
@@ -33,7 +35,7 @@ export class PwaShellThemeManager {
33
35
  }
34
36
 
35
37
  const currentTheme = document.documentElement.getAttribute('data-theme');
36
- this.syncTheme(normalizeTheme(currentTheme) ?? 'natural');
38
+ this.syncTheme(normalizeTheme(currentTheme) ?? DEFAULT_UI_THEME);
37
39
  };
38
40
 
39
41
  private updateThemeMeta = (themeColor: string) => {
@@ -80,7 +80,7 @@ export function RightPanelResourceHomePage({ open }: RightPanelResourceHomePageP
80
80
  key={item.id}
81
81
  type="button"
82
82
  onClick={() => openTarget(item.target)}
83
- className="group flex min-h-[68px] flex-col items-center gap-1.5 rounded-md px-1.5 py-1.5 text-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
83
+ className="group flex min-h-[68px] flex-col items-center gap-1.5 rounded-md px-1.5 py-1.5 text-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
84
84
  >
85
85
  <span className={cn('flex h-10 w-10 shrink-0 items-center justify-center rounded-md transition-colors', view.accentClassName, view.hoverAccentClassName)}>
86
86
  {view.icon}
@@ -7,6 +7,7 @@ import type {
7
7
  import {
8
8
  AlertTriangle,
9
9
  CheckCircle2,
10
+ ChevronDown,
10
11
  CircleDashed,
11
12
  LoaderCircle,
12
13
  MoreVertical,
@@ -53,6 +54,7 @@ export function ServiceAppsPanel({
53
54
  const [discoveredActionsByApp, setDiscoveredActionsByApp] = useState<
54
55
  Record<string, ServiceActionView[]>
55
56
  >({});
57
+ const [expandedActionsByApp, setExpandedActionsByApp] = useState<Record<string, boolean>>({});
56
58
 
57
59
  const refetch = () => {
58
60
  void serviceApps.refetch();
@@ -86,12 +88,16 @@ export function ServiceAppsPanel({
86
88
  ...current,
87
89
  [appId]: result.actions,
88
90
  }));
91
+ setExpandedActionsByApp((current) => ({
92
+ ...current,
93
+ [appId]: true,
94
+ }));
89
95
  });
90
96
  };
91
97
 
92
98
  return (
93
99
  <div className="flex h-full min-h-0 flex-col bg-card text-card-foreground">
94
- <div className="flex items-center justify-between gap-2 border-b border-border/70 px-4 py-3">
100
+ <div className="flex items-center justify-between gap-2 border-b border-border/60 px-4 py-3">
95
101
  <div className="flex min-w-0 items-center gap-2">
96
102
  {headerContent ?? (
97
103
  <>
@@ -110,20 +116,31 @@ export function ServiceAppsPanel({
110
116
  </div>
111
117
 
112
118
  {apps.length === 0 ? (
113
- <div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-muted-foreground">
114
- {t('serviceAppsEmpty')}
119
+ <div className="flex flex-1 items-center justify-center px-6 py-8 text-center">
120
+ <div className="w-full max-w-xs">
121
+ <div className="mx-auto flex h-10 w-10 items-center justify-center rounded-xl bg-muted text-muted-foreground">
122
+ <Server className="h-5 w-5" />
123
+ </div>
124
+ <h2 className="mt-3 text-sm font-semibold text-foreground">{t('serviceAppsEmptyTitle')}</h2>
125
+ <p className="mt-1 text-xs leading-5 text-muted-foreground">{t('serviceAppsEmptyDescription')}</p>
126
+ </div>
115
127
  </div>
116
128
  ) : (
117
- <div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto p-3">
118
- <div className="space-y-3">
129
+ <div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto bg-muted/25 p-3">
130
+ <div className="space-y-2.5">
119
131
  {apps.map((app) => (
120
132
  <ServiceAppCard
121
133
  key={app.id}
122
134
  app={app}
123
135
  actions={discoveredActionsByApp[app.id] ?? actions.filter((action) => action.appId === app.id)}
136
+ actionsOpen={Boolean(expandedActionsByApp[app.id])}
124
137
  grants={grants}
125
138
  deletePending={deleteServiceApp.isPending}
126
139
  isDiscovering={discoverServiceAppActions.isPending}
140
+ onActionsOpenChange={(open) => setExpandedActionsByApp((current) => ({
141
+ ...current,
142
+ [app.id]: open,
143
+ }))}
127
144
  onDiscover={discover}
128
145
  onDelete={(appId) => void deleteServiceApp.mutate(appId)}
129
146
  onRestart={(appId) => void restartServiceApp.mutate(appId)}
@@ -143,9 +160,11 @@ export function ServiceAppsPanel({
143
160
  function ServiceAppCard({
144
161
  app,
145
162
  actions,
163
+ actionsOpen,
146
164
  grants,
147
165
  deletePending,
148
166
  isDiscovering,
167
+ onActionsOpenChange,
149
168
  onDiscover,
150
169
  onDelete,
151
170
  onRestart,
@@ -153,9 +172,11 @@ function ServiceAppCard({
153
172
  }: {
154
173
  app: ServiceAppRecordView;
155
174
  actions: ServiceActionView[];
175
+ actionsOpen: boolean;
156
176
  grants: ServiceActionGrantView[];
157
177
  deletePending: boolean;
158
178
  isDiscovering: boolean;
179
+ onActionsOpenChange: (open: boolean) => void;
159
180
  onDiscover: (appId: string) => void;
160
181
  onDelete: (appId: string) => void;
161
182
  onRestart: (appId: string) => void;
@@ -172,21 +193,21 @@ function ServiceAppCard({
172
193
 
173
194
  return (
174
195
  <TooltipProvider delayDuration={250}>
175
- <section className="rounded-lg border border-border bg-card p-3">
176
- <div className="flex items-start justify-between gap-3">
177
- <div className="min-w-0">
196
+ <section className="overflow-hidden rounded-xl border border-border/70 bg-card shadow-[0_1px_2px_rgba(30,20,10,0.03)]">
197
+ <div className="flex items-start gap-3 px-3 pt-3">
198
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
199
+ <Server className="h-4 w-4" />
200
+ </div>
201
+ <div className="min-w-0 flex-1">
178
202
  <div className="flex items-center gap-2">
179
203
  <div className="truncate text-sm font-semibold text-foreground">{app.title}</div>
180
204
  <ServiceAppStatusBadge status={app.status} />
181
205
  </div>
182
206
  {app.description ? (
183
- <div className="mt-1 line-clamp-2 text-xs text-muted-foreground">{app.description}</div>
184
- ) : null}
185
- {app.lastError ? (
186
- <div className="mt-2 rounded bg-rose-50 px-2 py-1 text-xs text-rose-700">{app.lastError}</div>
207
+ <div className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">{app.description}</div>
187
208
  ) : null}
188
209
  </div>
189
- <div className="flex shrink-0 items-center gap-1">
210
+ <div className="flex shrink-0 items-center gap-0.5">
190
211
  <ServiceAppIconButton
191
212
  disabled={isDiscovering || !canConnectAndDiscover}
192
213
  icon={Radar}
@@ -207,7 +228,7 @@ function ServiceAppCard({
207
228
  <PopoverTrigger asChild>
208
229
  <button
209
230
  type="button"
210
- className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
231
+ className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
211
232
  aria-label={t('serviceAppsMoreActions')}
212
233
  disabled={deletePending}
213
234
  >
@@ -229,17 +250,85 @@ function ServiceAppCard({
229
250
  </Popover>
230
251
  </div>
231
252
  </div>
253
+
254
+ {app.lastError ? (
255
+ <div className="mx-3 mt-3 rounded-lg border border-rose-200/70 bg-rose-50 px-2.5 py-1.5 text-xs text-rose-700">
256
+ {app.lastError}
257
+ </div>
258
+ ) : null}
259
+
232
260
  <ServiceAppDiagnostics app={app} />
233
- <div className="mt-3 space-y-1">
234
- {actions.map((action) => (
235
- <ServiceActionRow
236
- key={action.id}
237
- action={action}
238
- grants={grants.filter((grant) => grant.actionId === action.id)}
239
- onRevoke={onRevoke}
261
+
262
+ <div className="mt-3 border-t border-border/50 bg-muted/30">
263
+ <button
264
+ type="button"
265
+ onClick={() => onActionsOpenChange(!actionsOpen)}
266
+ className="flex w-full items-start justify-between gap-2 px-3 py-2 text-left transition-colors hover:bg-muted/40"
267
+ aria-expanded={actionsOpen}
268
+ >
269
+ <div className="min-w-0 flex-1">
270
+ <div className="flex items-center gap-2">
271
+ <span className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground/80">
272
+ {t('serviceAppsActionsSection')}
273
+ </span>
274
+ <span className="rounded-full bg-card px-1.5 py-0.5 text-[11px] tabular-nums text-muted-foreground/70">
275
+ {actions.length}
276
+ </span>
277
+ </div>
278
+ {!actionsOpen ? (
279
+ actions.length === 0 ? (
280
+ <div className="mt-1 text-[11px] text-muted-foreground">
281
+ {t('serviceAppsActionsEmpty')}
282
+ </div>
283
+ ) : (
284
+ <div className="mt-1.5 flex flex-wrap gap-1">
285
+ {actions.slice(0, 4).map((action) => (
286
+ <span
287
+ key={action.id}
288
+ className="max-w-[9.5rem] truncate rounded-md border border-border/50 bg-card px-1.5 py-0.5 text-[11px] text-muted-foreground"
289
+ title={action.title ?? action.name}
290
+ >
291
+ {action.title ?? action.name}
292
+ </span>
293
+ ))}
294
+ {actions.length > 4 ? (
295
+ <span className="rounded-md bg-card px-1.5 py-0.5 text-[11px] text-muted-foreground/70">
296
+ +{actions.length - 4}
297
+ </span>
298
+ ) : null}
299
+ </div>
300
+ )
301
+ ) : null}
302
+ </div>
303
+ <ChevronDown
304
+ className={cn(
305
+ 'mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground/70 transition-transform',
306
+ actionsOpen && 'rotate-180',
307
+ )}
240
308
  />
241
- ))}
309
+ </button>
310
+ {actionsOpen ? (
311
+ <div className="px-3 pb-2.5">
312
+ {actions.length === 0 ? (
313
+ <div className="rounded-lg border border-dashed border-border/70 bg-card/70 px-2.5 py-2 text-[11px] text-muted-foreground">
314
+ {t('serviceAppsActionsEmpty')}
315
+ </div>
316
+ ) : (
317
+ <div className="space-y-1.5">
318
+ {actions.map((action) => (
319
+ <ServiceActionRow
320
+ key={action.id}
321
+ action={action}
322
+ grants={grants.filter((grant) => grant.actionId === action.id)}
323
+ onRevoke={onRevoke}
324
+ />
325
+ ))}
326
+ </div>
327
+ )}
328
+ </div>
329
+ ) : null}
242
330
  </div>
331
+
243
332
  <ConfirmDialog
244
333
  open={isDeleteDialogOpen}
245
334
  onOpenChange={setIsDeleteDialogOpen}
@@ -263,7 +352,7 @@ function ServiceAppStatusBadge({ status }: { status: ServiceAppStatus }) {
263
352
  <TooltipTrigger asChild>
264
353
  <span
265
354
  className={cn(
266
- 'inline-flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-medium',
355
+ 'inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-[11px] font-medium',
267
356
  view.className,
268
357
  )}
269
358
  aria-label={view.label}
@@ -345,7 +434,7 @@ function ServiceAppIconButton({
345
434
  type="button"
346
435
  onClick={onClick}
347
436
  disabled={disabled}
348
- className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:text-muted-foreground/40 disabled:opacity-60 disabled:hover:bg-transparent"
437
+ className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:text-muted-foreground/40 disabled:opacity-60 disabled:hover:bg-transparent"
349
438
  aria-label={label}
350
439
  >
351
440
  <Icon className="h-3.5 w-3.5" />
@@ -386,6 +475,7 @@ function ServiceAppMenuItem({
386
475
  }
387
476
 
388
477
  function ServiceAppDiagnostics({ app }: { app: ServiceAppRecordView }) {
478
+ const [open, setOpen] = useState(false);
389
479
  const command = [app.command, ...(app.args ?? [])]
390
480
  .filter((value): value is string => Boolean(value?.trim()))
391
481
  .join(' ');
@@ -403,11 +493,29 @@ function ServiceAppDiagnostics({ app }: { app: ServiceAppRecordView }) {
403
493
  }
404
494
 
405
495
  return (
406
- <dl className="mt-3 grid grid-cols-[4.5rem_minmax(0,1fr)] gap-x-2 gap-y-1 border-t border-border/70 pt-2 text-[11px]">
407
- {rows.map((row) => (
408
- <ServiceAppDiagnosticRow key={row.label} label={row.label} value={row.value} />
409
- ))}
410
- </dl>
496
+ <div className="mx-3 mt-3 overflow-hidden rounded-lg border border-border/50 bg-muted/35">
497
+ <button
498
+ type="button"
499
+ onClick={() => setOpen((value) => !value)}
500
+ className="flex w-full items-center justify-between gap-2 px-2.5 py-1.5 text-left transition-colors hover:bg-muted/50"
501
+ aria-expanded={open}
502
+ >
503
+ <span className="text-[11px] font-medium text-muted-foreground">{t('serviceAppsDetails')}</span>
504
+ <ChevronDown
505
+ className={cn(
506
+ 'h-3.5 w-3.5 shrink-0 text-muted-foreground/70 transition-transform',
507
+ open && 'rotate-180',
508
+ )}
509
+ />
510
+ </button>
511
+ {open ? (
512
+ <dl className="grid grid-cols-[4.75rem_minmax(0,1fr)] gap-x-2 gap-y-1 border-t border-border/50 px-2.5 py-2 text-[11px]">
513
+ {rows.map((row) => (
514
+ <ServiceAppDiagnosticRow key={row.label} label={row.label} value={row.value} />
515
+ ))}
516
+ </dl>
517
+ ) : null}
518
+ </div>
411
519
  );
412
520
  }
413
521
 
@@ -438,10 +546,12 @@ function ServiceActionRow({
438
546
  onRevoke: (grant: ServiceActionGrantView) => void;
439
547
  }) {
440
548
  return (
441
- <div className="rounded bg-muted/60 px-2 py-1.5">
549
+ <div className="rounded-lg border border-border/50 bg-card px-2.5 py-2">
442
550
  <div className="flex items-center justify-between gap-2">
443
551
  <div className="flex min-w-0 items-center gap-2">
444
- <Wrench className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" />
552
+ <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
553
+ <Wrench className="h-3.5 w-3.5" />
554
+ </span>
445
555
  <div className="min-w-0">
446
556
  <div className="truncate text-xs font-medium text-foreground">{action.title ?? action.name}</div>
447
557
  {action.description ? (
@@ -451,15 +561,15 @@ function ServiceActionRow({
451
561
  </div>
452
562
  <div className="flex shrink-0 items-center gap-1">
453
563
  {action.runtimeState ? (
454
- <span className="rounded bg-card px-1.5 py-0.5 text-[11px] text-muted-foreground">
564
+ <span className="rounded-full bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">
455
565
  {t(`serviceAppsRuntimeState_${action.runtimeState}`)}
456
566
  </span>
457
567
  ) : null}
458
- <span className="rounded bg-card px-1.5 py-0.5 text-[11px] text-muted-foreground">{action.risk}</span>
568
+ <span className="rounded-full bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">{action.risk}</span>
459
569
  </div>
460
570
  </div>
461
571
  {grants.map((grant) => (
462
- <div key={`${grant.caller.surface}:${grant.caller.appId}:${grant.actionId}`} className="mt-1 flex items-center justify-between gap-2 pl-5">
572
+ <div key={`${grant.caller.surface}:${grant.caller.appId}:${grant.actionId}`} className="mt-1.5 flex items-center justify-between gap-2 pl-8">
463
573
  <div className="flex min-w-0 items-center gap-1.5 text-[11px] text-muted-foreground">
464
574
  <ShieldCheck className="h-3 w-3 shrink-0 text-emerald-500" />
465
575
  <span className="truncate">{t('serviceAppsGrantedTo')} {grant.caller.appId}</span>
@@ -467,7 +577,7 @@ function ServiceActionRow({
467
577
  <button
468
578
  type="button"
469
579
  onClick={() => onRevoke(grant)}
470
- className="rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-background hover:text-rose-600"
580
+ className="rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-rose-600"
471
581
  title={t('serviceAppsRevokeGrant')}
472
582
  aria-label={t('serviceAppsRevokeGrant')}
473
583
  >
@@ -28,7 +28,7 @@ export function ProviderAuthSection(props: ProviderAuthSectionProps) {
28
28
  if (providerAuth?.kind !== 'device_code') return null;
29
29
 
30
30
  return (
31
- <div className="space-y-2 rounded-xl border border-primary/20 bg-primary-50/50 p-3">
31
+ <div className="space-y-2 rounded-xl border border-border/60 bg-muted/35 p-3">
32
32
  <Label className="text-sm font-medium text-foreground">
33
33
  {providerAuth.displayName || t('providerAuthSectionTitle')}
34
34
  </Label>
@@ -13,6 +13,7 @@ import { MaskedInput } from '@/shared/components/common/masked-input';
13
13
  import { Button } from '@/shared/components/ui/button';
14
14
  import { Input } from '@/shared/components/ui/input';
15
15
  import { Label } from '@/shared/components/ui/label';
16
+ import { FormActions } from '@/shared/components/ui/actions/form-actions';
16
17
  import { getLanguage, t } from '@/shared/lib/i18n';
17
18
  import type { ThinkingLevel } from '@/shared/lib/api';
18
19
  import {
@@ -548,14 +549,16 @@ function ProviderFormDetailPane(props: ProviderFormDetailPaneProps) {
548
549
  />
549
550
  </ConfigSplitPaneBody>
550
551
 
551
- <ConfigSplitPaneFooter className='flex items-center justify-between px-6 py-4'>
552
- <Button type='button' variant='outline' size='sm' onClick={onTestConnection} disabled={isTestPending}>
553
- <CircleDotDashed className='mr-1.5 h-4 w-4' />
554
- {isTestPending ? t('providerTestingConnection') : t('providerTestConnection')}
555
- </Button>
556
- <Button type='submit' disabled={isUpdatePending || !hasChanges}>
557
- {isUpdatePending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
558
- </Button>
552
+ <ConfigSplitPaneFooter>
553
+ <FormActions align="between">
554
+ <Button type='button' variant='outline' size='sm' onClick={onTestConnection} disabled={isTestPending}>
555
+ <CircleDotDashed className='mr-1.5 h-3.5 w-3.5' />
556
+ {isTestPending ? t('providerTestingConnection') : t('providerTestConnection')}
557
+ </Button>
558
+ <Button type='submit' size='sm' disabled={isUpdatePending || !hasChanges}>
559
+ {isUpdatePending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
560
+ </Button>
561
+ </FormActions>
559
562
  </ConfigSplitPaneFooter>
560
563
  </form>
561
564
  </ConfigSplitDetailPane>