@nextclaw/ui 0.12.8 → 0.12.10

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 (227) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/assets/ChannelsList-M9FTK1Ak.js +8 -0
  3. package/dist/assets/DocBrowser-CH7-GxlL.js +1 -0
  4. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-B3HWP4oz.js} +1 -1
  9. package/dist/assets/ProvidersList-CHjMnRhX.js +1 -0
  10. package/dist/assets/RuntimeConfig-psp8nMSG.js +1 -0
  11. package/dist/assets/SearchConfig-CSoKip1f.js +1 -0
  12. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-DocgeQtR.js → book-open-DzdUViDm.js} +1 -1
  16. package/dist/assets/chat-page-CLp0UV0Y.js +58 -0
  17. package/dist/assets/chat-session-display-DsYHx0RZ.js +1 -0
  18. package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-CVqPF5ie.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-Bop2oB18.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-DmAlInRu.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-Da5Gm_pO.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/download-BD0ETkB-.js +1 -0
  27. package/dist/assets/{external-link-DFjw3x1B.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-DJtaCejM.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/i18n-CpTZLchQ.js +1 -0
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-DEFUIR12.js → logos-B7gRObP8.js} +1 -1
  35. package/dist/assets/marketplace-page-3qVMnF3d.js +1 -0
  36. package/dist/assets/marketplace-page-BhFIeQzI.js +49 -0
  37. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +40 -0
  38. package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-0UcO9H9Z.js} +1 -1
  39. package/dist/assets/play-CKDjSQFL.js +1 -0
  40. package/dist/assets/plus-CG0QrVY_.js +1 -0
  41. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-Bcv40SXy.js} +1 -1
  43. package/dist/assets/remote-access-page-CWHG-sug.js +1 -0
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-EqJPOF0G.js} +1 -1
  46. package/dist/assets/search-BCAlB8nz.js +1 -0
  47. package/dist/assets/security-config-Slh0Mayz.js +1 -0
  48. package/dist/assets/select-CVz0t7MF.js +41 -0
  49. package/dist/assets/setting-row-CbVHAuQt.js +1 -0
  50. package/dist/assets/skeleton-D5rdKvzy.js +1 -0
  51. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-0rmPBRKI.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +95 -21
  61. package/dist/manifest.webmanifest +30 -0
  62. package/dist/offline.html +102 -0
  63. package/dist/pwa-192.png +0 -0
  64. package/dist/pwa-512.png +0 -0
  65. package/dist/runtime-icons/claude.ico +0 -0
  66. package/dist/runtime-icons/codex-openai.svg +6 -0
  67. package/dist/runtime-icons/hermes-agent.png +0 -0
  68. package/dist/sw.js +80 -0
  69. package/index.html +73 -1
  70. package/package.json +5 -5
  71. package/public/manifest.webmanifest +30 -0
  72. package/public/offline.html +102 -0
  73. package/public/pwa-192.png +0 -0
  74. package/public/pwa-512.png +0 -0
  75. package/public/runtime-icons/claude.ico +0 -0
  76. package/public/runtime-icons/codex-openai.svg +6 -0
  77. package/public/runtime-icons/hermes-agent.png +0 -0
  78. package/public/sw.js +80 -0
  79. package/src/account/components/account-panel.tsx +217 -97
  80. package/src/account/managers/account.manager.ts +3 -2
  81. package/src/api/chat-session-type.types.ts +7 -0
  82. package/src/api/runtime-control.types.ts +8 -0
  83. package/src/api/server-path.ts +27 -4
  84. package/src/api/types.ts +25 -10
  85. package/src/app.tsx +227 -54
  86. package/src/components/agents/agent-dialogs.tsx +499 -0
  87. package/src/components/agents/agents-page.test.tsx +238 -0
  88. package/src/components/agents/agents-page.tsx +435 -0
  89. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  90. package/src/components/chat/ChatSidebar.tsx +35 -35
  91. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  92. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  93. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  94. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +127 -206
  95. package/src/components/chat/chat-conversation-panel.tsx +482 -0
  96. package/src/components/chat/chat-page-shell.tsx +19 -13
  97. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  98. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  99. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +178 -0
  100. package/src/components/chat/chat-session-workspace-file-preview.tsx +278 -0
  101. package/src/components/chat/chat-session-workspace-panel-nav.tsx +203 -0
  102. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  103. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  104. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  105. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  106. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  107. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  108. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  109. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  110. package/src/components/chat/ncp/ncp-chat-page.tsx +9 -7
  111. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  112. package/src/components/chat/ncp/ncp-session-adapter.test.ts +36 -1
  113. package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
  114. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +62 -13
  115. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  116. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  117. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  118. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  119. package/src/components/chat/stores/chat-input.store.ts +2 -1
  120. package/src/components/chat/stores/chat-thread.store.ts +27 -1
  121. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  122. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  123. package/src/components/common/BrandHeader.tsx +3 -1
  124. package/src/components/common/session-context-icon.tsx +15 -2
  125. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  126. package/src/components/config/ChannelForm.test.tsx +89 -3
  127. package/src/components/config/ChannelForm.tsx +157 -188
  128. package/src/components/config/ChannelsList.test.tsx +163 -119
  129. package/src/components/config/ChannelsList.tsx +90 -101
  130. package/src/components/config/ProviderForm.tsx +108 -146
  131. package/src/components/config/ProvidersList.tsx +100 -123
  132. package/src/components/config/RuntimeConfig.tsx +141 -2
  133. package/src/components/config/SearchConfig.tsx +423 -393
  134. package/src/components/config/channel-form-fields-section.tsx +70 -37
  135. package/src/components/config/config-split-page.tsx +109 -0
  136. package/src/components/config/provider-enabled-field.tsx +17 -10
  137. package/src/components/config/runtime-control-card.test.tsx +56 -0
  138. package/src/components/config/runtime-control-card.tsx +25 -0
  139. package/src/components/config/runtime-presence-card.tsx +93 -79
  140. package/src/components/layout/AppLayout.tsx +25 -37
  141. package/src/components/layout/app-layout.test.tsx +46 -14
  142. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  143. package/src/components/layout/runtime-status-entry.tsx +143 -0
  144. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  145. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  146. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  147. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  148. package/src/components/marketplace/marketplace-page.tsx +596 -0
  149. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  150. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  151. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  152. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  153. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  154. package/src/components/providers/ThemeProvider.tsx +5 -0
  155. package/src/components/remote/remote-access-page.test.tsx +105 -0
  156. package/src/components/remote/remote-access-page.tsx +248 -0
  157. package/src/components/ui/notice-card.tsx +129 -0
  158. package/src/components/ui/setting-row.tsx +51 -0
  159. package/src/components/ui/tag-chip.tsx +39 -0
  160. package/src/components/ui/textarea.tsx +19 -0
  161. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  162. package/src/hooks/useConfig.ts +2 -1
  163. package/src/index.css +24 -0
  164. package/src/lib/app-resource-uri.test.ts +20 -0
  165. package/src/lib/app-resource-uri.ts +29 -0
  166. package/src/lib/chat-message.ts +14 -3
  167. package/src/lib/i18n.chat.ts +12 -1
  168. package/src/lib/i18n.pwa.ts +62 -0
  169. package/src/lib/i18n.remote.ts +1 -1
  170. package/src/lib/i18n.runtime-control.ts +31 -0
  171. package/src/lib/i18n.ts +7 -10
  172. package/src/lib/session-context.utils.test.ts +71 -0
  173. package/src/lib/session-context.utils.ts +28 -3
  174. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  175. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  176. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  177. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  178. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  179. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  180. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  181. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  182. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  183. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  184. package/src/pwa/pwa.types.ts +22 -0
  185. package/src/pwa/register-pwa.ts +14 -0
  186. package/src/pwa/stores/pwa.store.ts +17 -0
  187. package/src/vite-env.d.ts +9 -0
  188. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  189. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  190. package/dist/assets/MarketplacePage-BySqkYDh.js +0 -49
  191. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  192. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  193. package/dist/assets/ModelConfig-IrmzoslW.js +0 -1
  194. package/dist/assets/ProvidersList-8_Kalfwl.js +0 -1
  195. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  196. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  197. package/dist/assets/SearchConfig-DNBR-UbE.js +0 -1
  198. package/dist/assets/SessionsConfig-Doqp5ghH.js +0 -2
  199. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  200. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  201. package/dist/assets/config-layout-DmlGaay2.js +0 -1
  202. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  203. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  204. package/dist/assets/index-DafCdM4F.css +0 -1
  205. package/dist/assets/index-DdksE6U3.js +0 -6
  206. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  207. package/dist/assets/play-DBQbBxTA.js +0 -1
  208. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  209. package/dist/assets/popover-C_mWOFzI.js +0 -1
  210. package/dist/assets/search-MChQRYR1.js +0 -1
  211. package/dist/assets/security-config-CbXfPZzr.js +0 -1
  212. package/dist/assets/select-Caud8QvU.js +0 -41
  213. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  214. package/dist/assets/x-DuMhMATD.js +0 -1
  215. package/src/components/agents/AgentDialogs.tsx +0 -400
  216. package/src/components/agents/AgentsPage.test.tsx +0 -217
  217. package/src/components/agents/AgentsPage.tsx +0 -352
  218. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  219. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  220. package/src/components/config/config-layout.ts +0 -10
  221. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  222. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  223. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  224. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  225. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  226. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  227. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
@@ -1,43 +1,48 @@
1
- import { useConfig, useConfigMeta, useConfigSchema } from '@/hooks/useConfig';
2
- import { MessageSquare, ExternalLink, Search } from 'lucide-react';
3
- import { useEffect, useMemo, useState } from 'react';
4
- import { ChannelForm } from './ChannelForm';
5
- import { Tabs } from '@/components/ui/tabs-custom';
6
- import { LogoBadge } from '@/components/common/LogoBadge';
7
- import { getChannelLogo } from '@/lib/logos';
8
- import { hintForPath } from '@/lib/config-hints';
9
- import { StatusDot } from '@/components/ui/status-dot';
10
- import { cn } from '@/lib/utils';
11
- import { t } from '@/lib/i18n';
12
- import { PageLayout, PageHeader } from '@/components/layout/page-layout';
13
- import { resolveChannelTutorialUrl } from '@/lib/channel-tutorials';
14
- import { Input } from '@/components/ui/input';
15
- import { CONFIG_SIDEBAR_CARD_CLASS, CONFIG_SPLIT_GRID_CLASS } from './config-layout';
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { ExternalLink, MessageSquare, Search } from "lucide-react";
3
+ import { useConfig, useConfigMeta, useConfigSchema } from "@/hooks/useConfig";
4
+ import { LogoBadge } from "@/components/common/LogoBadge";
5
+ import { PageHeader, PageLayout } from "@/components/layout/page-layout";
6
+ import { Input } from "@/components/ui/input";
7
+ import { StatusDot } from "@/components/ui/status-dot";
8
+ import { Tabs } from "@/components/ui/tabs-custom";
9
+ import { hintForPath } from "@/lib/config-hints";
10
+ import { resolveChannelTutorialUrl } from "@/lib/channel-tutorials";
11
+ import { t } from "@/lib/i18n";
12
+ import { getChannelLogo } from "@/lib/logos";
13
+ import { cn } from "@/lib/utils";
14
+ import { ChannelForm } from "./ChannelForm";
15
+ import {
16
+ ConfigSelectionCard,
17
+ ConfigSplitEmptyState,
18
+ ConfigSplitPage,
19
+ ConfigSplitPaneBody,
20
+ ConfigSplitPaneHeader,
21
+ ConfigSplitSidebar,
22
+ } from "./config-split-page";
16
23
 
17
24
  const channelDescriptionKeys: Record<string, string> = {
18
- telegram: 'channelDescTelegram',
19
- slack: 'channelDescSlack',
20
- email: 'channelDescEmail',
21
- webhook: 'channelDescWebhook',
22
- discord: 'channelDescDiscord',
23
- feishu: 'channelDescFeishu',
24
- weixin: 'channelDescWeixin'
25
+ telegram: "channelDescTelegram",
26
+ slack: "channelDescSlack",
27
+ email: "channelDescEmail",
28
+ webhook: "channelDescWebhook",
29
+ discord: "channelDescDiscord",
30
+ feishu: "channelDescFeishu",
31
+ weixin: "channelDescWeixin",
25
32
  };
26
33
 
27
- const prioritizedChannelNames = ['weixin', 'feishu', 'discord', 'qq'] as const;
28
-
29
- function sortChannelsForDisplay<T extends { name: string }>(channels: T[]): T[] {
30
- const priorityByName = new Map<string, number>(prioritizedChannelNames.map((name, index) => [name, index]));
34
+ const prioritizedChannelNames = ["weixin", "feishu", "discord", "qq"] as const;
31
35
 
36
+ function sortChannelsForDisplay<T extends { name: string }>(channels: T[]) {
37
+ const priorityByName = new Map<string, number>(
38
+ prioritizedChannelNames.map((name, index) => [name, index]),
39
+ );
32
40
  return channels
33
41
  .map((channel, index) => ({ channel, index }))
34
42
  .sort((left, right) => {
35
43
  const leftPriority = priorityByName.get(left.channel.name) ?? Number.POSITIVE_INFINITY;
36
44
  const rightPriority = priorityByName.get(right.channel.name) ?? Number.POSITIVE_INFINITY;
37
- if (leftPriority !== rightPriority) {
38
- return leftPriority - rightPriority;
39
- }
40
- return left.index - right.index;
45
+ return leftPriority !== rightPriority ? leftPriority - rightPriority : left.index - right.index;
41
46
  })
42
47
  .map(({ channel }) => channel);
43
48
  }
@@ -46,28 +51,16 @@ export function ChannelsList() {
46
51
  const { data: config } = useConfig();
47
52
  const { data: meta } = useConfigMeta();
48
53
  const { data: schema } = useConfigSchema();
49
- const [activeTab, setActiveTab] = useState('enabled');
50
- const [selectedChannel, setSelectedChannel] = useState<string | undefined>();
51
- const [query, setQuery] = useState('');
52
- const uiHints = schema?.uiHints;
54
+ const [activeTab, setActiveTab] = useState("enabled");
55
+ const [selectedChannel, setSelectedChannel] = useState<string>();
56
+ const [query, setQuery] = useState("");
53
57
  const channels = useMemo(() => sortChannelsForDisplay(meta?.channels ?? []), [meta?.channels]);
54
58
  const channelConfigs = config?.channels;
55
59
 
56
- const tabs = [
57
- { id: 'enabled', label: t('channelsTabEnabled'), count: channels.filter((c) => channelConfigs?.[c.name]?.enabled).length },
58
- { id: 'all', label: t('channelsTabAll'), count: channels.length }
59
- ];
60
-
61
60
  const filteredChannels = useMemo(() => {
62
61
  const keyword = query.trim().toLowerCase();
63
62
  return channels
64
- .filter((channel) => {
65
- const enabled = channelConfigs?.[channel.name]?.enabled || false;
66
- if (activeTab === 'enabled') {
67
- return enabled;
68
- }
69
- return true;
70
- })
63
+ .filter((channel) => activeTab !== "enabled" || Boolean(channelConfigs?.[channel.name]?.enabled))
71
64
  .filter((channel) => {
72
65
  if (!keyword) {
73
66
  return true;
@@ -78,64 +71,63 @@ export function ChannelsList() {
78
71
  }, [activeTab, channelConfigs, channels, query]);
79
72
 
80
73
  useEffect(() => {
81
- if (filteredChannels.length === 0) {
82
- setSelectedChannel(undefined);
83
- return;
84
- }
85
- const exists = filteredChannels.some((channel) => channel.name === selectedChannel);
86
- if (!exists) {
87
- setSelectedChannel(filteredChannels[0].name);
88
- }
74
+ setSelectedChannel(
75
+ filteredChannels.some((channel) => channel.name === selectedChannel)
76
+ ? selectedChannel
77
+ : filteredChannels[0]?.name,
78
+ );
89
79
  }, [filteredChannels, selectedChannel]);
90
80
 
91
81
  if (!config || !meta) {
92
- return <div className="p-8 text-gray-400">{t('channelsLoading')}</div>;
82
+ return <div className="p-8 text-gray-400">{t("channelsLoading")}</div>;
93
83
  }
94
84
 
95
85
  return (
96
- <PageLayout className="xl:flex xl:h-full xl:min-h-0 xl:flex-col xl:pb-0">
97
- <PageHeader title={t('channelsPageTitle')} description={t('channelsPageDescription')} />
98
-
99
- <div className={cn(CONFIG_SPLIT_GRID_CLASS, 'xl:min-h-0 xl:flex-1')}>
100
- <section className={CONFIG_SIDEBAR_CARD_CLASS}>
101
- <div className="border-b border-gray-100 px-4 pt-4">
102
- <Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} className="mb-0" />
103
- </div>
86
+ <PageLayout className="pb-0 xl:flex xl:h-full xl:min-h-0 xl:flex-col">
87
+ <PageHeader title={t("channelsPageTitle")} description={t("channelsPageDescription")} />
88
+ <ConfigSplitPage className="xl:min-h-0">
89
+ <ConfigSplitSidebar>
90
+ <ConfigSplitPaneHeader className="px-4 pt-4">
91
+ <Tabs
92
+ tabs={[
93
+ {
94
+ id: "enabled",
95
+ label: t("channelsTabEnabled"),
96
+ count: channels.filter((channel) => channelConfigs?.[channel.name]?.enabled).length,
97
+ },
98
+ { id: "all", label: t("channelsTabAll"), count: channels.length },
99
+ ]}
100
+ activeTab={activeTab}
101
+ onChange={setActiveTab}
102
+ className="mb-0"
103
+ />
104
+ </ConfigSplitPaneHeader>
104
105
 
105
106
  <div className="border-b border-gray-100 px-4 py-3">
106
107
  <div className="relative">
107
108
  <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
108
109
  <Input
109
110
  value={query}
110
- onChange={(e) => setQuery(e.target.value)}
111
- placeholder={t('channelsFilterPlaceholder')}
111
+ onChange={(event) => setQuery(event.target.value)}
112
+ placeholder={t("channelsFilterPlaceholder")}
112
113
  className="h-10 rounded-xl pl-9"
113
114
  />
114
115
  </div>
115
116
  </div>
116
117
 
117
- <div className="min-h-0 flex-1 space-y-2 overflow-y-auto overscroll-contain p-3">
118
+ <ConfigSplitPaneBody className="space-y-2 p-3">
118
119
  {filteredChannels.map((channel) => {
119
- const channelConfig = config.channels[channel.name];
120
- const enabled = channelConfig?.enabled || false;
121
- const channelHint = hintForPath(`channels.${channel.name}`, uiHints);
120
+ const enabled = Boolean(config.channels[channel.name]?.enabled);
122
121
  const tutorialUrl = resolveChannelTutorialUrl(channel);
123
122
  const description =
124
- channelHint?.help ||
125
- t(channelDescriptionKeys[channel.name] || 'channelDescriptionDefault');
126
- const isActive = selectedChannel === channel.name;
123
+ hintForPath(`channels.${channel.name}`, schema?.uiHints)?.help ||
124
+ t(channelDescriptionKeys[channel.name] || "channelDescriptionDefault");
127
125
 
128
126
  return (
129
- <button
127
+ <ConfigSelectionCard
130
128
  key={channel.name}
131
- type="button"
132
129
  onClick={() => setSelectedChannel(channel.name)}
133
- className={cn(
134
- 'w-full rounded-xl border p-2.5 text-left transition-all',
135
- isActive
136
- ? 'border-primary/30 bg-primary-50/40 shadow-sm'
137
- : 'border-gray-200/70 bg-white hover:border-gray-300 hover:bg-gray-50/70'
138
- )}
130
+ active={selectedChannel === channel.name}
139
131
  >
140
132
  <div className="flex items-start justify-between gap-3">
141
133
  <div className="flex min-w-0 items-center gap-3">
@@ -143,52 +135,49 @@ export function ChannelsList() {
143
135
  name={channel.name}
144
136
  src={getChannelLogo(channel.name)}
145
137
  className={cn(
146
- 'h-10 w-10 rounded-lg border',
147
- enabled ? 'border-primary/30 bg-white' : 'border-gray-200/70 bg-white'
138
+ "h-10 w-10 rounded-lg border",
139
+ enabled ? "border-primary/30 bg-white" : "border-gray-200/70 bg-white",
148
140
  )}
149
141
  imgClassName="h-5 w-5 object-contain"
150
142
  fallback={<span className="text-sm font-semibold uppercase text-gray-500">{channel.name[0]}</span>}
151
143
  />
152
144
  <div className="min-w-0">
153
- <p className="truncate text-sm font-semibold text-gray-900">{channel.displayName || channel.name}</p>
145
+ <p className="truncate text-sm font-semibold text-gray-900">
146
+ {channel.displayName || channel.name}
147
+ </p>
154
148
  <p className="line-clamp-1 text-[11px] text-gray-500">{description}</p>
155
149
  </div>
156
150
  </div>
157
151
  <div className="flex items-center gap-2">
158
- {tutorialUrl && (
152
+ {tutorialUrl ? (
159
153
  <a
160
154
  href={tutorialUrl}
161
- onClick={(e) => e.stopPropagation()}
155
+ onClick={(event) => event.stopPropagation()}
162
156
  className="inline-flex h-7 w-7 items-center justify-center rounded-md text-gray-300 transition-colors hover:bg-gray-100/70 hover:text-gray-500"
163
- title={t('channelsGuideTitle')}
157
+ title={t("channelsGuideTitle")}
164
158
  >
165
159
  <ExternalLink className="h-3.5 w-3.5" />
166
160
  </a>
167
- )}
161
+ ) : null}
168
162
  <StatusDot
169
- status={enabled ? 'active' : 'inactive'}
170
- label={enabled ? t('statusActive') : t('statusInactive')}
163
+ status={enabled ? "active" : "inactive"}
164
+ label={enabled ? t("statusActive") : t("statusInactive")}
171
165
  className="min-w-[56px] justify-center"
172
166
  />
173
167
  </div>
174
168
  </div>
175
- </button>
169
+ </ConfigSelectionCard>
176
170
  );
177
171
  })}
178
172
 
179
- {filteredChannels.length === 0 && (
180
- <div className="flex h-full min-h-[220px] flex-col items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50/70 py-10 text-center">
181
- <div className="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-white">
182
- <MessageSquare className="h-5 w-5 text-gray-300" />
183
- </div>
184
- <p className="text-sm font-medium text-gray-700">{t('channelsNoMatch')}</p>
185
- </div>
186
- )}
187
- </div>
188
- </section>
173
+ {filteredChannels.length === 0 ? (
174
+ <ConfigSplitEmptyState icon={MessageSquare} title={t("channelsNoMatch")} />
175
+ ) : null}
176
+ </ConfigSplitPaneBody>
177
+ </ConfigSplitSidebar>
189
178
 
190
179
  <ChannelForm channelName={selectedChannel} />
191
- </div>
180
+ </ConfigSplitPage>
192
181
  </PageLayout>
193
182
  );
194
183
  }