@nextclaw/ui 0.26.1 → 0.26.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 (141) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/.vite/manifest.json +366 -365
  3. package/dist/assets/{account-panel-ZM2qzKF4.js → account-panel-DIdHjKG-.js} +1 -1
  4. package/dist/assets/{api-Cjq-o1VN.js → api-DkLJrSCd.js} +2 -2
  5. package/dist/assets/app-presenter-provider-Co8uKOyW.js +124 -0
  6. package/dist/assets/{appearance-settings-page-B4zouKzg.js → appearance-settings-page-DI8aDY2G.js} +1 -1
  7. package/dist/assets/{book-open-AteA89HX.js → book-open-BPMkKSci.js} +1 -1
  8. package/dist/assets/{card-BLXvBstA.js → card-Bqu8zWLq.js} +1 -1
  9. package/dist/assets/{channels-list-page-fj8kMo-T.js → channels-list-page-CCIaTQu7.js} +1 -1
  10. package/dist/assets/{circle-dashed-LSYZTzM7.js → circle-dashed-B48a0t6p.js} +1 -1
  11. package/dist/assets/{client-DudCRJw4.js → client-imrVphQb.js} +1 -1
  12. package/dist/assets/{config-split-page-G7Kt2BnV.js → config-split-page-Bd-bGKsw.js} +1 -1
  13. package/dist/assets/{confirm-dialog-DShT64F6.js → confirm-dialog-10Qb3dpZ.js} +1 -1
  14. package/dist/assets/context-menu-CRqH2SEk.js +1 -0
  15. package/dist/assets/{createLucideIcon-D7hvIHV-.js → createLucideIcon-B4xj2I2g.js} +1 -1
  16. package/dist/assets/{desktop-capabilities-page-CzqzFZUY.js → desktop-capabilities-page-CFcT2g8-.js} +1 -1
  17. package/dist/assets/{desktop-update-config-CgeQ0Ffo.js → desktop-update-config-CRJd_An1.js} +1 -1
  18. package/dist/assets/{dialog-C7glmAQJ.js → dialog-BgL3vKJQ.js} +2 -2
  19. package/dist/assets/doc-browser-52t2_rSF.js +1 -0
  20. package/dist/assets/{doc-browser-context-BlhBAOLH.js → doc-browser-context-DH4aWvkS.js} +1 -1
  21. package/dist/assets/{download-BYgECE0n.js → download-Cr337h4W.js} +1 -1
  22. package/dist/assets/{es2015-CsjAAQ6X.js → es2015-D-CRf3_Z.js} +1 -1
  23. package/dist/assets/{extensions-qYL-NpL3.js → extensions-CsmJPz7g.js} +1 -1
  24. package/dist/assets/{eye-BBuTzJ53.js → eye-DrQRSAfJ.js} +1 -1
  25. package/dist/assets/{form-actions-CLGWO52v.js → form-actions-BEeWTGUZ.js} +1 -1
  26. package/dist/assets/{i18n-qBGEbcW6.js → i18n-OGmF0Ws_.js} +2 -2
  27. package/dist/assets/{keyboard-shortcuts-settings-page-B-wK_6zG.js → keyboard-shortcuts-settings-page-DQU56SrW.js} +1 -1
  28. package/dist/assets/loader-circle-DbOQWT9C.js +1 -0
  29. package/dist/assets/login-page-DW1AZXqX.js +1 -0
  30. package/dist/assets/{login-page-Cd84lpVV.js → login-page-sgivRYG1.js} +1 -1
  31. package/dist/assets/main-B-oxUq-o.js +2 -0
  32. package/dist/assets/main-BpL6OjwM.css +1 -0
  33. package/dist/assets/main-i18n-catalog.config-Bpu5GcwF.js +1 -0
  34. package/dist/assets/mcp-marketplace-page-DSLRpJEB.js +1 -0
  35. package/dist/assets/{model-config-page-D0DQlSDa.js → model-config-page-CDQBhmVr.js} +1 -1
  36. package/dist/assets/{navigation-link-32iM1-e6.js → navigation-link-D3pzNHeA.js} +1 -1
  37. package/dist/assets/panel-app-BYfQu_TS.js +1 -0
  38. package/dist/assets/panel-app-i18n-catalog.config-D44qmtfk.js +1 -0
  39. package/dist/assets/{panelStandalone-C9EKaApN.js → panelStandalone-CGkYeMK5.js} +2 -2
  40. package/dist/assets/panelStandalone-D-mhAGNa.css +1 -0
  41. package/dist/assets/{privacy-settings-page-Bxxx12OO.js → privacy-settings-page-CjoAkoDt.js} +1 -1
  42. package/dist/assets/projects-C7Zxpcoi.js +1 -0
  43. package/dist/assets/{provider-models-C3frdXeW.js → provider-models-YW7rIeOD.js} +1 -1
  44. package/dist/assets/{provider-scoped-model-input-CYetrPYZ.js → provider-scoped-model-input-axJ-pTEu.js} +1 -1
  45. package/dist/assets/{providers-config-page-DESB1h8g.js → providers-config-page-_QhBXQ4M.js} +1 -1
  46. package/dist/assets/{puzzle-CWHfFyYN.js → puzzle--RLCeYML.js} +1 -1
  47. package/dist/assets/{pwa-CiGUjpcd.js → pwa-DcizGowz.js} +1 -1
  48. package/dist/assets/{refresh-cw-y3L6PyLt.js → refresh-cw-syCVj_nw.js} +1 -1
  49. package/dist/assets/remote-BLQU7aY5.js +1 -0
  50. package/dist/assets/{rotate-cw-CNE9qSHi.js → rotate-cw-BlH9El3x.js} +1 -1
  51. package/dist/assets/{runtime-config-page-5o98T-TD.js → runtime-config-page-Cco9dS73.js} +1 -1
  52. package/dist/assets/{save-B0p5a0ET.js → save-BWynuRQ2.js} +1 -1
  53. package/dist/assets/{search-config-page-CpGcnIzi.js → search-config-page-Cc74_SAU.js} +1 -1
  54. package/dist/assets/{secrets-config-page-C4VTiW3r.js → secrets-config-page-raYDzlFC.js} +1 -1
  55. package/dist/assets/security-config-aE_jOwy-.js +1 -0
  56. package/dist/assets/{select-CctTN205.js → select-6Uq4chZW.js} +1 -1
  57. package/dist/assets/{service-apps-panel-CKE5HKNY.js → service-apps-panel-jBWAa4qZ.js} +1 -1
  58. package/dist/assets/{settings-2-EPflrNk6.js → settings-2-COvVf1XX.js} +1 -1
  59. package/dist/assets/{status-dot-CensgJZC.js → status-dot-DZlxa5zW.js} +1 -1
  60. package/dist/assets/{tag-chip-Bjzb4-qW.js → tag-chip-DXHtZ8AS.js} +1 -1
  61. package/dist/assets/{theme-provider-hlle7Skk.js → theme-provider-C5PCDBPB.js} +1 -1
  62. package/dist/assets/{trash-2-C5KH5Rw9.js → trash-2-DazgBvmJ.js} +1 -1
  63. package/dist/assets/{use-config-CPl_F79t.js → use-config-DRRGF9Zb.js} +1 -1
  64. package/dist/assets/{use-desktop-capabilities-CEg9-DrQ.js → use-desktop-capabilities-CdfOUjdE.js} +1 -1
  65. package/dist/assets/{use-viewport-layout-CPZUmTPi.js → use-viewport-layout-DdxVnCYc.js} +1 -1
  66. package/dist/assets/x-CQIp_nJT.js +1 -0
  67. package/dist/index.html +34 -34
  68. package/dist/panel-standalone.html +11 -11
  69. package/package.json +8 -8
  70. package/src/app/components/layout/page-layout.tsx +5 -2
  71. package/src/app/configs/__tests__/app-navigation.config.test.ts +10 -0
  72. package/src/app/configs/app-navigation.config.ts +6 -2
  73. package/src/app/styles/charcoal-theme.css +1 -1
  74. package/src/app/styles/design-system.css +8 -2
  75. package/src/app/styles/island-theme.css +1 -1
  76. package/src/features/agents/components/agent-details-dialog.tsx +1 -1
  77. package/src/features/agents/components/agents-page.tsx +5 -3
  78. package/src/features/apps/components/app-package-card.tsx +7 -6
  79. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -3
  80. package/src/features/chat/components/conversation/chat-conversation-header.tsx +4 -4
  81. package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
  82. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +12 -5
  83. package/src/features/chat/features/session/components/chat-mobile-session-row.tsx +1 -1
  84. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +1 -1
  85. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +3 -5
  86. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +4 -3
  87. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
  88. package/src/features/chat/features/workspace/components/project-files/workspace-project-files-toolbar.tsx +3 -1
  89. package/src/features/cron/components/cron-config.tsx +1 -0
  90. package/src/features/cron/components/cron-job-row.tsx +4 -4
  91. package/src/features/inbox/components/inbox-delivery-content.tsx +1 -1
  92. package/src/features/inbox/pages/inbox-page.tsx +24 -28
  93. package/src/features/marketplace/components/marketplace-page-parts.tsx +4 -3
  94. package/src/features/marketplace/components/marketplace-page.tsx +2 -2
  95. package/src/features/panel-apps/components/panel-app-open-standalone-menu-item.tsx +3 -1
  96. package/src/features/projects/components/project-overview.tsx +1 -1
  97. package/src/features/projects/pages/project-home-page.tsx +11 -2
  98. package/src/features/projects/pages/projects-page.test.tsx +4 -2
  99. package/src/platforms/mobile/components/mobile-app-shell.tsx +1 -1
  100. package/src/platforms/mobile/components/mobile-bottom-nav.tsx +8 -7
  101. package/src/platforms/mobile/components/mobile-settings-shell.tsx +24 -23
  102. package/src/platforms/mobile/components/mobile-topbar.tsx +10 -11
  103. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +34 -2
  104. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +36 -5
  105. package/src/shared/components/doc-browser/doc-browser.tsx +5 -1
  106. package/src/shared/components/settings/settings-page.tsx +1 -1
  107. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +5 -5
  108. package/src/shared/components/ui/actions/action-feedback.ts +8 -0
  109. package/src/shared/components/ui/actions/icon-action-button.tsx +13 -12
  110. package/src/shared/components/ui/button.tsx +1 -1
  111. package/src/shared/components/ui/context-menu/context-menu-items.tsx +4 -3
  112. package/src/shared/components/ui/dialog.tsx +4 -3
  113. package/src/shared/components/ui/input.tsx +1 -0
  114. package/src/shared/components/ui/select.tsx +2 -1
  115. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +5 -3
  116. package/src/shared/components/ui/tabs.tsx +2 -2
  117. package/src/shared/components/ui/textarea.tsx +1 -0
  118. package/src/shared/components/workbench/workbench-surface-toolbar.tsx +3 -4
  119. package/src/shared/components/workbench/workbench-surface.tsx +2 -2
  120. package/src/shared/lib/i18n/locales/en-US/agents.json +2 -0
  121. package/src/shared/lib/i18n/locales/en-US/core.json +1 -0
  122. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +1 -1
  123. package/src/shared/lib/i18n/locales/zh-CN/agents.json +2 -0
  124. package/src/shared/lib/i18n/locales/zh-CN/core.json +1 -0
  125. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +1 -1
  126. package/dist/assets/app-presenter-provider-C02YPoMr.js +0 -124
  127. package/dist/assets/context-menu-rd7b_kVD.js +0 -1
  128. package/dist/assets/doc-browser-dBiR9Qi0.js +0 -1
  129. package/dist/assets/loader-circle-BA7EMkih.js +0 -1
  130. package/dist/assets/login-page-VdMyNkFf.js +0 -1
  131. package/dist/assets/main-CR0SrTBf.js +0 -2
  132. package/dist/assets/main-DXH6EdXI.css +0 -1
  133. package/dist/assets/main-i18n-catalog.config-Btp_OowF.js +0 -1
  134. package/dist/assets/mcp-marketplace-page-jacRwiI-.js +0 -1
  135. package/dist/assets/panel-app-CC0bVSu4.js +0 -1
  136. package/dist/assets/panel-app-i18n-catalog.config-LUSBp5Ce.js +0 -1
  137. package/dist/assets/panelStandalone-Y91OJkgV.css +0 -1
  138. package/dist/assets/projects-D56yAPNT.js +0 -1
  139. package/dist/assets/remote-DiGFV8nP.js +0 -1
  140. package/dist/assets/security-config-C5-RQVpS.js +0 -1
  141. package/dist/assets/x--xxUvHKW.js +0 -1
@@ -7,6 +7,7 @@ import {
7
7
  SelectValue,
8
8
  } from "@/shared/components/ui/select";
9
9
  import { Skeleton } from "@/shared/components/ui/skeleton";
10
+ import { Input } from "@/shared/components/ui/input";
10
11
  import { t } from "@/shared/lib/i18n";
11
12
  import { Loader2, PackageSearch } from "lucide-react";
12
13
  import type { Ref } from "react";
@@ -30,10 +31,10 @@ export function FilterPanel({
30
31
  }) {
31
32
  return (
32
33
  <div className="mb-4">
33
- <div className="flex items-center gap-3">
34
+ <div className="flex flex-wrap items-center gap-3">
34
35
  <div className="relative min-w-0 flex-1">
35
36
  <PackageSearch className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
36
- <input
37
+ <Input
37
38
  value={searchText}
38
39
  onChange={(event) => onSearchTextChange(event.target.value)}
39
40
  placeholder={searchPlaceholder}
@@ -52,7 +53,7 @@ export function FilterPanel({
52
53
  value={sort}
53
54
  onValueChange={(value) => onSortChange(value as MarketplaceSort)}
54
55
  >
55
- <SelectTrigger className="h-9 w-[150px] shrink-0 rounded-lg">
56
+ <SelectTrigger className="h-9 w-[110px] sm:w-[150px] shrink-0 rounded-lg">
56
57
  <SelectValue />
57
58
  </SelectTrigger>
58
59
  <SelectContent>
@@ -142,12 +142,12 @@ export function MarketplacePage({
142
142
  <PageLayout className="flex h-full min-h-0 flex-col pb-0">
143
143
  {!curatedSceneRoute.isSceneRoute && (
144
144
  <>
145
- <div className="mb-3 flex items-start justify-between gap-3 border-b border-gray-200/60">
145
+ <div className="mb-3 flex flex-wrap items-start justify-between gap-3 border-b border-border/60">
146
146
  <Tabs
147
147
  value={scope}
148
148
  onValueChange={(value) => setScope(value as ScopeType)}
149
149
  >
150
- <TabsList className="mb-0 h-auto flex-1 justify-start gap-6 rounded-none border-b-0 bg-transparent p-0 text-gray-500">
150
+ <TabsList className="mb-0 h-auto flex-1 justify-start gap-4 sm:gap-6 rounded-none border-b-0 bg-transparent p-0 text-muted-foreground">
151
151
  <TabsTrigger
152
152
  value="all"
153
153
  className="rounded-none border-b-2 border-transparent bg-transparent px-0 pb-3 pt-0 shadow-none hover:bg-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent data-[state=active]:text-gray-950 data-[state=active]:shadow-none"
@@ -1,4 +1,6 @@
1
1
  import { ExternalLink } from "lucide-react";
2
+ import { ACTION_FEEDBACK, ACTION_MENU_ITEM_CLASS } from '@/shared/components/ui/actions/action-feedback';
3
+ import { cn } from '@/shared/lib/utils';
2
4
  import type { PanelAppEntryView } from "@/shared/lib/api";
3
5
  import { t } from "@/shared/lib/i18n";
4
6
 
@@ -22,7 +24,7 @@ export function PanelAppOpenStandaloneMenuItem({
22
24
  // `opener` keeps this trusted same-origin target auxiliary, so the current browser owns the new tab.
23
25
  rel="opener"
24
26
  referrerPolicy="no-referrer"
25
- className="flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-left text-sm text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground"
27
+ className={cn(ACTION_MENU_ITEM_CLASS, ACTION_FEEDBACK.item)}
26
28
  onClick={onSelect}
27
29
  >
28
30
  <ExternalLink className="h-4 w-4 shrink-0" />
@@ -32,7 +32,7 @@ export function ProjectOverview({
32
32
  return (
33
33
  <div className="space-y-5">
34
34
  <section
35
- className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"
35
+ className="grid grid-cols-2 gap-3 lg:grid-cols-4"
36
36
  aria-label={t("projectsWorkSummary")}
37
37
  >
38
38
  {summary.data ? (
@@ -81,8 +81,17 @@ export function ProjectsPage() {
81
81
  );
82
82
  if (!selectedProject || !tab)
83
83
  return (
84
- <main className="p-6 text-sm text-muted-foreground">
85
- {t("projectsChoose")}
84
+ <main className="space-y-3 p-4 text-sm text-muted-foreground sm:p-6">
85
+ <label htmlFor="project-home-selection" className="block font-medium text-foreground">{t("projectsChoose")}</label>
86
+ <select
87
+ id="project-home-selection"
88
+ value=""
89
+ onChange={(event) => navigate(`/projects/${encodeURIComponent(event.target.value)}/overview`)}
90
+ className="min-h-11 w-full rounded-xl border border-border bg-card px-3 text-base text-foreground"
91
+ >
92
+ <option value="" disabled>{t("projectsChoose")}</option>
93
+ {registered.map((project) => <option key={project.id} value={project.id}>{project.name}</option>)}
94
+ </select>
86
95
  </main>
87
96
  );
88
97
 
@@ -135,11 +135,13 @@ describe("ProjectsPage", () => {
135
135
  ).toBeTruthy();
136
136
  });
137
137
 
138
- it("asks the user to choose from the sidebar when no project route is selected", () => {
138
+ it("lets users enter a project without a desktop sidebar", () => {
139
139
  renderPage("/projects");
140
140
  expect(mocks.useProjectAgreement).toHaveBeenCalledWith(null);
141
141
  expect(mocks.useProjectSkills).toHaveBeenCalledWith(null);
142
- expect(screen.getByText(/Choose a project|选择项目/)).toBeTruthy();
142
+ const picker = screen.getByRole("combobox", { name: /Choose a project|选择项目/ });
143
+ fireEvent.change(picker, { target: { value: "project-1" } });
144
+ expect(screen.getByRole("tab", { name: /Overview|概览/ }).getAttribute("aria-selected")).toBe("true");
143
145
  });
144
146
 
145
147
  it("loads each fixed project material only on its own tab", () => {
@@ -42,7 +42,7 @@ export function MobileAppShell({
42
42
  {isMainRoute ? (
43
43
  <div className="h-full min-h-0 overflow-hidden">{children}</div>
44
44
  ) : (
45
- <main className="h-full overflow-auto px-4 py-4 custom-scrollbar">
45
+ <main className="h-full overflow-auto overscroll-contain px-4 py-4 custom-scrollbar">
46
46
  <div className="mx-auto max-w-3xl animate-fade-in">{children}</div>
47
47
  </main>
48
48
  )}
@@ -19,13 +19,13 @@ export function MobileBottomNav() {
19
19
  return (
20
20
  <nav
21
21
  data-testid="mobile-bottom-nav"
22
- aria-label={t("settings")}
22
+ aria-label={t("mobileNavigation")}
23
23
  className="shrink-0 border-t border-border/60 bg-background text-foreground"
24
24
  style={{
25
25
  paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 0.25rem)",
26
26
  }}
27
27
  >
28
- <ul className="grid grid-cols-6 gap-1 px-2 pt-1">
28
+ <ul className="grid grid-cols-6 gap-0.5 px-1 pt-1">
29
29
  {getMobileBottomNavItems(t).map((item) => {
30
30
  const active = item.target === "/settings"
31
31
  ? isSettingsRoute(pathname)
@@ -34,10 +34,11 @@ export function MobileBottomNav() {
34
34
  <li key={item.target}>
35
35
  <Link
36
36
  to={item.target}
37
+ aria-label={item.label}
37
38
  aria-current={active ? "page" : undefined}
38
39
  data-testid={active ? "mobile-nav-active-indicator" : undefined}
39
40
  className={cn(
40
- "group flex min-h-[2.875rem] flex-col items-center justify-center gap-0.5 rounded-xl px-2 py-1 text-[10px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
41
+ "group flex min-h-12 flex-col items-center justify-center gap-1 rounded-xl px-0.5 py-1 text-[11px] font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border",
41
42
  active
42
43
  ? "bg-muted text-foreground"
43
44
  : "text-muted-foreground hover:bg-muted hover:text-foreground",
@@ -46,7 +47,7 @@ export function MobileBottomNav() {
46
47
  <span className="relative">
47
48
  <item.icon
48
49
  className={cn(
49
- "h-3.5 w-3.5",
50
+ "h-5 w-5",
50
51
  active ? "text-foreground" : "text-muted-foreground",
51
52
  )}
52
53
  />
@@ -54,7 +55,7 @@ export function MobileBottomNav() {
54
55
  <span className="absolute -right-1 -top-1 h-1.5 w-1.5 rounded-full bg-primary ring-1 ring-background" />
55
56
  ) : null}
56
57
  </span>
57
- <span className="max-w-full truncate">{item.label}</span>
58
+ <span className="max-w-full truncate">{item.target === "/agents" ? t("mobileAgents") : item.label}</span>
58
59
  </Link>
59
60
  </li>
60
61
  );
@@ -63,9 +64,9 @@ export function MobileBottomNav() {
63
64
  <button
64
65
  type="button"
65
66
  onClick={() => openApps(docBrowser)}
66
- className="group flex min-h-[2.875rem] w-full flex-col items-center justify-center gap-0.5 rounded-xl px-2 py-1 text-[10px] font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
67
+ className="group flex min-h-12 w-full flex-col items-center justify-center gap-1 rounded-xl px-0.5 py-1 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
67
68
  >
68
- <Boxes className="h-3.5 w-3.5 text-muted-foreground" />
69
+ <Boxes className="h-5 w-5 text-muted-foreground" />
69
70
  <span className="max-w-full truncate">{t("appsTitle")}</span>
70
71
  </button>
71
72
  </li>
@@ -1,8 +1,10 @@
1
1
  import { NavLink } from "react-router-dom";
2
+ import { ChevronRight } from "lucide-react";
2
3
  import { getSettingsNavSections } from "@/app/configs/app-navigation.config";
3
4
  import { t } from "@/shared/lib/i18n";
4
5
  import { cn } from "@/shared/lib/utils";
5
6
  import { useDesktopCapabilityAvailability } from "@/features/desktop-capabilities";
7
+ import { SettingsGroup } from "@/shared/components/settings/setting-row";
6
8
 
7
9
  export function MobileSettingsShell() {
8
10
  const desktopCapabilityAvailable = useDesktopCapabilityAvailability();
@@ -19,29 +21,28 @@ export function MobileSettingsShell() {
19
21
  {settingsNavSections.map((section) => (
20
22
  <section key={section.label} className="space-y-2">
21
23
  <h2 className="px-1 pt-2 text-xs font-medium text-muted-foreground">{section.label}</h2>
22
- {section.items.map((item) => (
23
- <NavLink
24
- key={item.target}
25
- to={item.target}
26
- className={({ isActive }) =>
27
- cn(
28
- "flex items-center gap-3 rounded-2xl border border-gray-200/80 bg-white px-4 py-4 shadow-card transition-colors",
29
- isActive
30
- ? "border-gray-300 bg-gray-50"
31
- : "hover:border-gray-300 hover:bg-gray-50/70",
32
- )
33
- }
34
- >
35
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-gray-100 text-gray-600">
36
- <item.icon className="h-4 w-4" />
37
- </div>
38
- <div className="min-w-0">
39
- <p className="truncate text-sm font-semibold text-gray-900">
40
- {item.label}
41
- </p>
42
- </div>
43
- </NavLink>
44
- ))}
24
+ <SettingsGroup>
25
+ {section.items.map((item) => (
26
+ <NavLink
27
+ key={item.target}
28
+ to={item.target}
29
+ className={({ isActive }) => cn(
30
+ "flex min-h-14 items-center gap-3 px-4 py-3 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-border",
31
+ isActive
32
+ ? "bg-[var(--interaction-selection)]"
33
+ : "hover:bg-[var(--interaction-hover)] active:bg-[var(--interaction-hover)]",
34
+ )}
35
+ >
36
+ <div className="flex h-8 w-8 shrink-0 items-center justify-center text-muted-foreground">
37
+ <item.icon className="h-5 w-5" />
38
+ </div>
39
+ <div className="min-w-0 flex-1">
40
+ <p className="text-[15px] font-medium text-foreground">{item.label}</p>
41
+ </div>
42
+ <ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground/60" aria-hidden="true" />
43
+ </NavLink>
44
+ ))}
45
+ </SettingsGroup>
45
46
  </section>
46
47
  ))}
47
48
  </div>
@@ -2,6 +2,7 @@ import { ArrowLeft } from "lucide-react";
2
2
  import { useNavigate, useLocation } from "react-router-dom";
3
3
  import { resolveMobileRouteMeta } from "@/app/configs/app-navigation.config";
4
4
  import { t } from "@/shared/lib/i18n";
5
+ import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
5
6
 
6
7
  export function MobileTopbar({ leadingInset }: { leadingInset?: string }) {
7
8
  const navigate = useNavigate();
@@ -11,31 +12,29 @@ export function MobileTopbar({ leadingInset }: { leadingInset?: string }) {
11
12
  return (
12
13
  <header
13
14
  data-testid="mobile-topbar"
14
- className="shrink-0 border-b border-border/60 bg-secondary text-foreground"
15
+ className="shrink-0 border-b border-border/60 bg-background text-foreground"
15
16
  style={{
16
- paddingTop: "calc(env(safe-area-inset-top, 0px) + 0.25rem)",
17
+ paddingTop: "env(safe-area-inset-top, 0px)",
17
18
  }}
18
19
  >
19
20
  <div
20
- className="flex min-h-[2.75rem] items-center gap-2 py-1.5 pl-3 pr-3"
21
+ className="flex h-[var(--mobile-header-height)] items-center gap-2 pl-3 pr-3"
21
22
  style={{ paddingLeft: leadingInset ? `calc(${leadingInset} + 0.75rem)` : undefined }}
22
23
  >
23
24
  {routeMeta.backTarget ? (
24
- <button
25
- type="button"
25
+ <IconActionButton
26
+ size="lg"
26
27
  onClick={() => navigate(routeMeta.backTarget as string)}
27
- className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
28
- aria-label={routeMeta.backLabel ?? t("backToMain")}
29
- >
30
- <ArrowLeft className="h-4 w-4" />
31
- </button>
28
+ label={routeMeta.backLabel ?? t("backToMain")}
29
+ icon={<ArrowLeft className="h-4 w-4" />}
30
+ />
32
31
  ) : null}
33
32
  <div className="min-w-0 flex-1">
34
33
  <h1 className="truncate text-center text-[17px] font-semibold text-foreground">
35
34
  {routeMeta.title}
36
35
  </h1>
37
36
  </div>
38
- {routeMeta.backTarget ? <div className="h-11 w-11 shrink-0" aria-hidden="true" /> : null}
37
+ {routeMeta.backTarget ? <div className="w-[var(--icon-control-size)] shrink-0" aria-hidden="true" /> : null}
39
38
  </div>
40
39
  </header>
41
40
  );
@@ -545,7 +545,8 @@ describe("DocBrowser panel app navigation", () => {
545
545
 
546
546
  expect(screen.queryByRole("button", { name: "Pin to left sidebar" })).toBeNull();
547
547
  await user.click(screen.getByRole("button", { name: "More panel app actions" }));
548
- await user.click(screen.getByRole("button", { name: "Pin to left sidebar" }));
548
+ await user.click(screen.getByRole("menuitem", { name: "Layout and position" }));
549
+ await user.click(screen.getByRole("menuitem", { name: "Pin to left sidebar" }));
549
550
  expect(panelAppHooks.mutate).toHaveBeenCalledWith({
550
551
  id: "piano",
551
552
  preferences: { mainSidebar: true },
@@ -571,7 +572,8 @@ describe("DocBrowser panel app navigation", () => {
571
572
  render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
572
573
 
573
574
  await user.click(screen.getByRole("button", { name: "More panel app actions" }));
574
- await user.click(screen.getByRole("button", { name: "Pin to left sidebar" }));
575
+ await user.click(screen.getByRole("menuitem", { name: "Layout and position" }));
576
+ await user.click(screen.getByRole("menuitem", { name: "Pin to left sidebar" }));
575
577
  expect(panelAppHooks.mutate).toHaveBeenCalledWith({
576
578
  id: "piano",
577
579
  preferences: { mainSidebar: true },
@@ -651,6 +653,36 @@ describe("DocBrowser floating interactions", () => {
651
653
  installDocBrowserPointerTestEnvironment();
652
654
  });
653
655
 
656
+ it("keeps compact navigation, tab selection, resource and app actions reachable", () => {
657
+ const onBack = vi.fn();
658
+ const onSelect = vi.fn();
659
+ const onClose = vi.fn();
660
+ const onAdd = vi.fn();
661
+ const onRefresh = vi.fn();
662
+ render(<DocBrowserTabStrip compact tabs={docBrowserState.tabs} activeTabId="docs"
663
+ canGoBack canGoForward={false} onGoBack={onBack} onGoForward={vi.fn()}
664
+ onOpenNewTab={vi.fn()} onSetActiveTab={onSelect} onCloseTab={onClose}
665
+ mobileToolbar={<button onClick={onRefresh}>Refresh app</button>}
666
+ getTabMenuGroups={() => [{ key: 'chat', items: [{ key: 'add', label: 'Add to Chat', onSelect: onAdd }] }]}
667
+ />);
668
+ const open = () => fireEvent.click(screen.getByRole('button', { name: 'More tab actions' }));
669
+ expect(screen.queryByRole('button', { name: 'Back' })).toBeNull();
670
+ open();
671
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Back' }));
672
+ expect(onBack).toHaveBeenCalledOnce();
673
+ open();
674
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Docs' }));
675
+ expect(onSelect).toHaveBeenCalledWith('docs');
676
+ open();
677
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Add to Chat' }));
678
+ expect(onAdd).toHaveBeenCalledOnce();
679
+ open();
680
+ fireEvent.click(screen.getByRole('button', { name: 'Refresh app' }));
681
+ expect(onRefresh).toHaveBeenCalledOnce();
682
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Close tab' }));
683
+ expect(onClose).toHaveBeenCalledWith('docs');
684
+ });
685
+
654
686
  it("drags the shared header without stealing tab actions", () => {
655
687
  new WorkbenchSurfaceManager().place('global-resources', 'floating');
656
688
  render(<DocBrowser />);
@@ -1,4 +1,8 @@
1
1
  import { PageResourceIcon } from '@/features/right-panel-resources';
2
+ import { useState, type ReactNode } from 'react';
3
+ import { ChevronDown } from 'lucide-react';
4
+ import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
+ import { ContextMenuItems } from '@/shared/components/ui/context-menu/context-menu';
2
6
  import {
3
7
  ArrowLeft,
4
8
  ArrowRight,
@@ -16,6 +20,8 @@ import {
16
20
  import { t } from "@/shared/lib/i18n";
17
21
 
18
22
  type DocBrowserTabStripProps = {
23
+ compact?: boolean;
24
+ mobileToolbar?: ReactNode;
19
25
  tabs: DocBrowserTab[];
20
26
  activeTabId: string;
21
27
  canGoBack: boolean;
@@ -47,7 +53,10 @@ export function DocBrowserTabStrip({
47
53
  onSetActiveTab,
48
54
  onCloseTab,
49
55
  getTabMenuGroups,
56
+ mobileToolbar,
57
+ compact = false,
50
58
  }: DocBrowserTabStripProps) {
59
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
51
60
  const backLabel = t("docBrowserBack");
52
61
  const closeTabLabel = t("workbenchCloseTab");
53
62
  const dockLabel = dockState?.isDocked
@@ -87,18 +96,40 @@ export function DocBrowserTabStrip({
87
96
  : []),
88
97
  ];
89
98
 
99
+ const current = tabs.find(tab => tab.id === activeTabId);
100
+ const mobileGroups: ContextMenuGroup[] = [
101
+ { key: 'tabs', items: compactTabs.map(tab => ({ key: tab.key, label: tab.label, icon: tab.leadingIcon, pressed: tab.active, onSelect: tab.onSelect })) },
102
+ { key: 'navigation', items: actions.map(action => ({ key: action.key, label: action.label, icon: action.icon, disabled: action.disabled, onSelect: action.onClick })) },
103
+ ...(current ? getTabMenuGroups?.(current) ?? [] : []),
104
+ ...(current ? [{ key: 'close-tab', items: [{ key: 'close-tab', label: closeTabLabel, onSelect: () => onCloseTab(current.id) }] }] : []),
105
+ ];
106
+ if (compact) return (
107
+ <div className="flex h-12 min-w-0 flex-1 items-center">
108
+ <Popover open={mobileMenuOpen} onOpenChange={setMobileMenuOpen}>
109
+ <PopoverTrigger asChild>
110
+ <button type="button" className="flex h-8 min-w-0 items-center gap-2 rounded-lg px-2 text-sm font-medium hover:bg-[var(--interaction-hover)]" aria-label={t('docBrowserTabMoreActions')}>
111
+ {current ? <DocBrowserTabIcon tab={current} /> : null}
112
+ <span className="truncate">{current?.title || t('docBrowserTabUntitled')}</span><ChevronDown className="h-4 w-4 shrink-0" />
113
+ </button>
114
+ </PopoverTrigger>
115
+ <PopoverContent align="start" className="max-h-[70dvh] w-72 overflow-y-auto p-1">
116
+ <ContextMenuItems groups={mobileGroups} onClose={() => setMobileMenuOpen(false)} />
117
+ {mobileToolbar}
118
+ </PopoverContent>
119
+ </Popover>
120
+ </div>
121
+ );
90
122
  return (
91
123
  <CompactTabStrip
92
124
  testId="doc-browser-tab-strip"
93
125
  actionsTestId="doc-browser-tab-actions"
94
126
  tabs={compactTabs}
95
127
  actions={actions}
96
- className="h-10 min-w-0 flex-1 gap-1 border-0 bg-transparent px-0 select-none"
97
- scrollClassName="doc-browser-tab-scrollbar flex h-full items-center gap-0.5"
128
+ className="flex h-10 min-w-0 flex-1 gap-1 border-0 bg-transparent px-0 select-none"
129
+ scrollClassName="doc-browser-tab-scrollbar flex h-full max-md:h-11 max-md:basis-full items-center gap-0.5"
98
130
  tabsClassName="items-center gap-0.5"
99
- actionsClassName="h-full items-center gap-0.5"
100
- actionButtonClassName="rounded-md p-1.5 text-muted-foreground hover:bg-muted/60 hover:text-foreground disabled:opacity-50"
101
- tabBaseClassName="inline-flex cursor-pointer items-center gap-0.5 h-7 px-2 rounded-md text-xs max-w-[220px] shrink-0 transition-colors"
131
+ actionsClassName="h-full max-md:h-11 max-md:w-full items-center gap-0.5"
132
+ tabBaseClassName="inline-flex cursor-pointer items-center gap-0.5 h-7 max-md:min-h-11 px-2 rounded-md text-xs max-w-[220px] shrink-0 transition-colors"
102
133
  activeTabClassName="bg-muted/80 text-foreground"
103
134
  inactiveTabClassName="text-muted-foreground hover:bg-muted/45 hover:text-foreground"
104
135
  labelClassName="px-0.5 text-xs font-normal"
@@ -1,4 +1,5 @@
1
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { useViewportLayout } from '@/app/hooks/use-viewport-layout';
2
3
  import {
3
4
  DOC_BROWSER_HOME_TAB_KIND,
4
5
  getDefaultDocsUrl,
@@ -258,6 +259,7 @@ export function DocBrowser({
258
259
 
259
260
 
260
261
  const isFullscreen = displayMode === 'fullscreen';
262
+ const { isMobile } = useViewportLayout();
261
263
  const customRenderParams = currentTab ? {
262
264
  currentUrl,
263
265
  open,
@@ -284,7 +286,7 @@ export function DocBrowser({
284
286
  urlInput={urlInput}
285
287
  />
286
288
 
287
- {customToolbar}
289
+ {!isMobile ? customToolbar : null}
288
290
 
289
291
  <DocBrowserFrameContent
290
292
  currentTab={currentTab}
@@ -310,6 +312,8 @@ export function DocBrowser({
310
312
  onWidthCommit={setDockedWidth} onClose={close} closeLabel={t('workbenchHideGroup')}
311
313
  hidden={!isOpen} fullscreen={isFullscreen} navigation={
312
314
  <DocBrowserTabStrip
315
+ mobileToolbar={customToolbar}
316
+ compact={isMobile}
313
317
  tabs={tabs}
314
318
  activeTabId={activeTabId}
315
319
  canGoBack={canGoBack}
@@ -27,7 +27,7 @@ export function SettingsPage({
27
27
  className
28
28
  )}
29
29
  >
30
- <PageHeader title={title} description={description} actions={actions} />
30
+ <PageHeader title={title} description={description} actions={actions} mobileTitleInShell />
31
31
  {children}
32
32
  </PageLayout>
33
33
  );
@@ -17,7 +17,7 @@ describe('IconActionButton', () => {
17
17
  expect(button.className).toContain('h-6');
18
18
  expect(button.className).toContain('w-6');
19
19
  expect(button.className).toContain('p-1');
20
- expect(button.className).toContain('hover:bg-black/10');
20
+ expect(button.className).toContain('hover:before:bg-[var(--interaction-hover)]');
21
21
  });
22
22
 
23
23
  it('defaults to medium size and soft accent hover', () => {
@@ -31,7 +31,7 @@ describe('IconActionButton', () => {
31
31
  const button = screen.getByRole('button', { name: 'More' });
32
32
  expect(button.className).toContain('h-7');
33
33
  expect(button.className).toContain('w-7');
34
- expect(button.className).toContain('hover:bg-[var(--interaction-hover)]');
34
+ expect(button.className).toContain('hover:before:bg-[var(--interaction-hover)]');
35
35
  });
36
36
 
37
37
  it('uses surface-relative feedback without introducing a new surface', () => {
@@ -44,9 +44,9 @@ describe('IconActionButton', () => {
44
44
  );
45
45
 
46
46
  const button = screen.getByRole('button', { name: 'Add project' });
47
- expect(button.className).toContain('hover:bg-gray-200/60');
48
- expect(button.className).toContain('hover:text-gray-900');
49
- expect(button.className).toContain('active:bg-gray-200/80');
47
+ expect(button.className).toContain('hover:before:bg-[var(--interaction-hover)]');
48
+ expect(button.className).toContain('hover:text-accent-foreground');
49
+ expect(button.className).toContain('active:before:bg-[var(--interaction-hover)]');
50
50
  });
51
51
 
52
52
  it('ignores restored pointer focus while preserving keyboard tooltip focus', () => {
@@ -0,0 +1,8 @@
1
+ /** Neutral actions share one theme color across icon and menu surfaces. */
2
+ export const ACTION_MENU_ITEM_CLASS = 'flex min-h-8 w-full items-center gap-2 rounded-lg px-2.5 py-1 text-left text-[13px] leading-5 font-medium outline-none transition-colors disabled:pointer-events-none disabled:opacity-45';
3
+
4
+ export const ACTION_FEEDBACK = {
5
+ icon: 'hover:before:bg-[var(--interaction-hover)] active:before:bg-[var(--interaction-hover)] disabled:hover:before:bg-transparent disabled:active:before:bg-transparent',
6
+ item: 'hover:bg-[var(--interaction-hover)] focus-visible:bg-[var(--interaction-hover)] active:bg-[var(--interaction-hover)] disabled:hover:bg-transparent disabled:active:bg-transparent',
7
+ destructive: 'text-destructive hover:bg-destructive/10 focus-visible:bg-destructive/10 disabled:hover:bg-transparent',
8
+ } as const;
@@ -6,6 +6,7 @@ import {
6
6
  TooltipTrigger,
7
7
  } from '@/shared/components/ui/tooltip';
8
8
  import { cn } from '@/shared/lib/utils';
9
+ import { ACTION_FEEDBACK } from './action-feedback';
9
10
 
10
11
  type IconActionButtonSize = 'sm' | 'md' | 'lg';
11
12
  type IconActionButtonTone = 'default' | 'surface' | 'strong';
@@ -17,7 +18,7 @@ type IconActionButtonProps = Omit<
17
18
  icon: React.ReactNode;
18
19
  label: string;
19
20
  size?: IconActionButtonSize;
20
- /** default: global interaction hover. surface: relative feedback on the current surface. strong: denser feedback for nested hover surfaces. */
21
+ /** @deprecated All neutral icon actions now share the theme feedback color. */
21
22
  tone?: IconActionButtonTone;
22
23
  tooltip?: string | false | null;
23
24
  tooltipSide?: 'top' | 'right' | 'bottom' | 'left';
@@ -29,14 +30,9 @@ const SIZE_CLASS: Record<IconActionButtonSize, string> = {
29
30
  lg: 'h-8 w-8 rounded-lg p-1.5',
30
31
  };
31
32
 
32
- const TONE_CLASS: Record<IconActionButtonTone, string> = {
33
- default:
34
- 'text-muted-foreground hover:bg-[var(--interaction-hover)] hover:text-accent-foreground disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
35
- surface:
36
- 'text-muted-foreground hover:bg-gray-200/60 hover:text-gray-900 active:bg-gray-200/80 disabled:text-muted-foreground/45 disabled:hover:bg-transparent disabled:hover:text-muted-foreground/45',
37
- strong:
38
- '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',
39
- };
33
+ export function IconActionGroup({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
34
+ return <div {...props} className={cn('flex shrink-0 items-center gap-1 max-md:gap-0', className)}>{children}</div>;
35
+ }
40
36
 
41
37
  const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonProps>(
42
38
  (
@@ -60,14 +56,19 @@ const IconActionButton = React.forwardRef<HTMLButtonElement, IconActionButtonPro
60
56
  type="button"
61
57
  disabled={disabled}
62
58
  aria-label={label}
59
+ data-tone={tone}
63
60
  className={cn(
64
- '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',
61
+ 'relative 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',
65
62
  SIZE_CLASS[size],
66
- TONE_CLASS[tone],
63
+ 'text-muted-foreground hover:text-accent-foreground disabled:text-muted-foreground/45 disabled:hover:text-muted-foreground/45',
64
+ ACTION_FEEDBACK.icon,
65
+ size === 'sm' ? 'max-md:h-8 max-md:w-8' : 'max-md:h-[var(--icon-control-size)] max-md:w-[var(--icon-control-size)]',
66
+ "before:pointer-events-none before:absolute before:inset-0 before:rounded-[inherit] before:transition-colors before:content-[''] max-md:before:inset-auto max-md:before:h-[var(--icon-feedback-size)] max-md:before:w-[var(--icon-feedback-size)]",
67
+ size === 'sm' && 'max-md:before:h-7 max-md:before:w-7',
67
68
  className
68
69
  )}
69
70
  >
70
- {icon}
71
+ <span className="relative inline-flex items-center justify-center">{icon}</span>
71
72
  </button>
72
73
  );
73
74
  const content = tooltip === false ? null : tooltip ?? label;
@@ -42,7 +42,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
42
42
  ({ className, variant, size, ...props }, ref) => {
43
43
  return (
44
44
  <button
45
- className={cn(buttonVariants({ variant, size, className }))}
45
+ className={cn(buttonVariants({ variant, size, className }), size !== 'icon' && 'max-md:min-h-[var(--control-touch-size)]')}
46
46
  ref={ref}
47
47
  {...props}
48
48
  />
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"
2
2
  import * as Popover from "@radix-ui/react-popover";
3
3
  import { ChevronRight } from "lucide-react";
4
4
  import { cn } from "@/shared/lib/utils";
5
+ import { ACTION_FEEDBACK, ACTION_MENU_ITEM_CLASS } from '../actions/action-feedback';
5
6
  import type { ContextMenuGroup, ContextMenuItem } from "./context-menu";
6
7
 
7
8
  export const CONTEXT_MENU_SURFACE_CLASS =
@@ -59,7 +60,7 @@ function ContextSubmenu({ item, open, onOpenChange, onClose, className, children
59
60
  aria-expanded={open}
60
61
  disabled={item.disabled}
61
62
  data-menu-key={item.key}
62
- className={cn(className, open && "bg-muted")}
63
+ className={cn(className, open && "bg-[var(--interaction-hover)]")}
63
64
  onPointerEnter={(event) => {
64
65
  if (event.pointerType === "touch") return;
65
66
  keyboardOpen.current = false;
@@ -154,8 +155,8 @@ export function ContextMenuItems({ groups, onClose }: {
154
155
  {index > 0 ? <div className="my-1 h-px bg-border" /> : null}
155
156
  {group.items.map((item) => {
156
157
  const className = cn(
157
- "flex h-8 w-full items-center gap-2 rounded-lg px-2.5 text-left text-[13px] font-medium outline-none transition-colors disabled:pointer-events-none disabled:opacity-45",
158
- item.destructive ? "text-destructive hover:bg-destructive/10 focus-visible:bg-destructive/10" : "text-foreground hover:bg-muted focus-visible:bg-muted",
158
+ ACTION_MENU_ITEM_CLASS,
159
+ item.destructive ? ACTION_FEEDBACK.destructive : `text-foreground ${ACTION_FEEDBACK.item}`,
159
160
  );
160
161
  const content = <MenuItemContent item={item} />;
161
162
  if (item.children) return <ContextSubmenu key={item.key} item={item} open={openKey === item.key}