@nextclaw/ui 0.13.4 → 0.13.6

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 (142) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/assets/api-BCDb13Uv.js +8 -0
  3. package/dist/assets/app-presenter-provider-C-QMjK7m.js +3 -0
  4. package/dist/assets/arrow-left-rIXFA2Zy.js +1 -0
  5. package/dist/assets/channels-list-page-DnqwsKGe.js +8 -0
  6. package/dist/assets/chat-page-CsQx20xL.js +105 -0
  7. package/dist/assets/config-split-page-BwhGqmUS.js +1 -0
  8. package/dist/assets/confirm-dialog-DBkKcQ1q.js +5 -0
  9. package/dist/assets/desktop-update-config-BE9KjiXi.js +1 -0
  10. package/dist/assets/{dist-CpwDZDtv.js → dist-BI0bX8jv.js} +1 -1
  11. package/dist/assets/dist-C8JRt-tf.js +1 -0
  12. package/dist/assets/doc-browser-context-VHUOIUXL.js +1 -0
  13. package/dist/assets/doc-browser-eqdNEerh.js +1 -0
  14. package/dist/assets/doc-browser-lpF_pyx4.js +1 -0
  15. package/dist/assets/ellipsis-C9VB2CxT.js +1 -0
  16. package/dist/assets/es2015-BCw3Q7nY.js +41 -0
  17. package/dist/assets/external-link-Bp7_4s9t.js +1 -0
  18. package/dist/assets/index-BCq7kbC8.css +1 -0
  19. package/dist/assets/index-DTdPDqto.js +101 -0
  20. package/dist/assets/key-round-Ccy6oND3.js +1 -0
  21. package/dist/assets/loader-circle-OpH4EErP.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-Dvi8mCBd.js → mcp-marketplace-page-B-GxKOqS.js} +2 -2
  23. package/dist/assets/mcp-marketplace-page-Dm-0-w51.js +1 -0
  24. package/dist/assets/model-config-BXPnV_7n.js +1 -0
  25. package/dist/assets/notice-card-Dsbkl7u7.js +1 -0
  26. package/dist/assets/play-CvgYhNbo.js +1 -0
  27. package/dist/assets/plus-DHcP-K1U.js +1 -0
  28. package/dist/assets/popover-BTLZE5Iz.js +1 -0
  29. package/dist/assets/provider-models-C_yOh6DE.js +1 -0
  30. package/dist/assets/provider-scoped-model-input-DKI9KN7N.js +1 -0
  31. package/dist/assets/providers-list-Bko6B7dE.js +1 -0
  32. package/dist/assets/react-BqS8oMyM.js +8 -0
  33. package/dist/assets/refresh-cw-BIaj7txb.js +1 -0
  34. package/dist/assets/remote-DHn_b8QU.js +1 -0
  35. package/dist/assets/rotate-cw-DMqSkRJQ.js +1 -0
  36. package/dist/assets/runtime-config-page-DX_wSK1J.js +1 -0
  37. package/dist/assets/save-BEoORten.js +1 -0
  38. package/dist/assets/search-COFh_SFI.js +1 -0
  39. package/dist/assets/{search-config-G2O1fcRb.js → search-config-DA5By-tE.js} +1 -1
  40. package/dist/assets/{secrets-config-TenzxYbE.js → secrets-config-eDxG7iN4.js} +2 -2
  41. package/dist/assets/{status-dot-BSMG7ZaC.js → status-dot-oWBjTovo.js} +1 -1
  42. package/dist/assets/{tabs-D1CjpWNX.js → tabs-CGaY0BVO.js} +1 -1
  43. package/dist/assets/{tabs-custom-CRSN1ekg.js → tabs-custom-D2fPYb4G.js} +1 -1
  44. package/dist/assets/{tag-chip-DEashiw6.js → tag-chip-TVkgofS1.js} +1 -1
  45. package/dist/assets/tooltip-BwHPKBv9.js +1 -0
  46. package/dist/assets/trash-2-D3psAGrz.js +1 -0
  47. package/dist/assets/x-CHAxzyp2.js +1 -0
  48. package/dist/index.html +25 -23
  49. package/dist/logos/mimo.svg +9 -0
  50. package/package.json +8 -8
  51. package/public/logos/mimo.svg +9 -0
  52. package/src/app/components/layout/app-layout.tsx +10 -2
  53. package/src/features/agents/components/agents-page.tsx +6 -4
  54. package/src/features/chat/components/chat-welcome.test.tsx +15 -0
  55. package/src/features/chat/components/chat-welcome.tsx +4 -7
  56. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +17 -10
  57. package/src/features/marketplace/components/marketplace-list-card.tsx +22 -1
  58. package/src/features/marketplace/components/marketplace-page.tsx +3 -1
  59. package/src/features/marketplace/hooks/use-marketplace.ts +3 -1
  60. package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +8 -0
  61. package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +70 -6
  62. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts +25 -1
  63. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts +1 -1
  64. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +42 -0
  65. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +22 -0
  66. package/src/features/service-apps/components/service-apps-panel.test.tsx +88 -0
  67. package/src/features/service-apps/components/service-apps-panel.tsx +246 -56
  68. package/src/features/service-apps/hooks/use-service-apps.ts +14 -0
  69. package/src/features/side-dock/components/side-dock.test.tsx +58 -3
  70. package/src/features/side-dock/components/side-dock.tsx +77 -24
  71. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +37 -34
  72. package/src/features/side-dock/index.ts +2 -0
  73. package/src/features/side-dock/managers/side-dock.manager.test.ts +84 -3
  74. package/src/features/side-dock/managers/side-dock.manager.ts +135 -12
  75. package/src/features/side-dock/types/side-dock.types.ts +11 -4
  76. package/src/features/side-dock/utils/side-dock-item.utils.test.ts +8 -8
  77. package/src/features/side-dock/utils/side-dock-item.utils.ts +13 -5
  78. package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -2
  79. package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -1
  80. package/src/shared/components/config/provider-form-support.test.ts +21 -0
  81. package/src/shared/components/config/provider-form-support.ts +38 -78
  82. package/src/shared/components/config/provider-form.tsx +54 -67
  83. package/src/shared/components/config/providers-list.tsx +211 -73
  84. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +73 -0
  85. package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
  86. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +184 -98
  87. package/src/shared/components/doc-browser/doc-browser.test.tsx +168 -43
  88. package/src/shared/components/doc-browser/doc-browser.tsx +38 -7
  89. package/src/shared/components/doc-browser/index.ts +1 -0
  90. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +30 -6
  91. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +29 -0
  92. package/src/shared/components/doc-browser/types/doc-browser.types.ts +23 -0
  93. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +7 -0
  94. package/src/shared/components/model-config.tsx +5 -4
  95. package/src/shared/components/provider-auth-section.tsx +2 -2
  96. package/src/shared/components/ui/switch.tsx +5 -2
  97. package/src/shared/hooks/use-config.ts +27 -6
  98. package/src/shared/lib/api/marketplace.types.ts +193 -0
  99. package/src/shared/lib/api/types.ts +23 -200
  100. package/src/shared/lib/api/utils/config.utils.ts +17 -7
  101. package/src/shared/lib/i18n/index.ts +10 -12
  102. package/src/shared/lib/i18n/{marketplace.ts → marketplace-labels.utils.ts} +3 -0
  103. package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +46 -2
  104. package/src/shared/lib/provider-models/index.test.ts +93 -15
  105. package/src/shared/lib/provider-models/index.ts +35 -20
  106. package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
  107. package/dist/assets/api-DruAcori.js +0 -15
  108. package/dist/assets/app-presenter-provider-DIcnNB9s.js +0 -3
  109. package/dist/assets/book-open-CRotBOF2.js +0 -1
  110. package/dist/assets/boxes-BvA-I63l.js +0 -1
  111. package/dist/assets/channels-list-page-wK3l7p5R.js +0 -8
  112. package/dist/assets/chat-page-DgehvdJT.js +0 -105
  113. package/dist/assets/config-split-page-BnTO1mAM.js +0 -1
  114. package/dist/assets/confirm-dialog-CGrKHbAW.js +0 -5
  115. package/dist/assets/createLucideIcon-Br1EFMEr.js +0 -1
  116. package/dist/assets/desktop-update-config-CehWxSSg.js +0 -1
  117. package/dist/assets/dist-rzakR_bu.js +0 -41
  118. package/dist/assets/doc-browser-BgxmVsTx.js +0 -1
  119. package/dist/assets/doc-browser-CZn5-Qbu.js +0 -1
  120. package/dist/assets/doc-browser-context-B8gk7HMr.js +0 -1
  121. package/dist/assets/external-link-DHjFdCpy.js +0 -1
  122. package/dist/assets/index-DORmh0Cy.css +0 -1
  123. package/dist/assets/index-DcFpvnge.js +0 -101
  124. package/dist/assets/key-round-D41Oe35x.js +0 -1
  125. package/dist/assets/mcp-marketplace-page-DBLhJWOJ.js +0 -1
  126. package/dist/assets/model-config-B-EJXGtO.js +0 -1
  127. package/dist/assets/play-C-0vo7hi.js +0 -1
  128. package/dist/assets/plus-Cr_akLp2.js +0 -1
  129. package/dist/assets/popover-YieElO8c.js +0 -1
  130. package/dist/assets/provider-scoped-model-input-BPRNGqYn.js +0 -1
  131. package/dist/assets/providers-list-CtBpL1ZY.js +0 -1
  132. package/dist/assets/react-B2X_ph0a.js +0 -1
  133. package/dist/assets/remote-BGHbHoHf.js +0 -1
  134. package/dist/assets/rotate-cw-jueVCNJQ.js +0 -1
  135. package/dist/assets/runtime-config-page-C1Hv7mad.js +0 -1
  136. package/dist/assets/save-aYdSdK97.js +0 -1
  137. package/dist/assets/search-CdhgFNwf.js +0 -1
  138. package/dist/assets/setting-row-BNu0qhOM.js +0 -1
  139. package/dist/assets/trash-2-DSWTsARW.js +0 -1
  140. package/dist/assets/x-D1v_vb54.js +0 -1
  141. package/src/shared/components/provider-enabled-field.tsx +0 -23
  142. /package/dist/assets/{config-hints-A7G_jMTU.js → config-hints-Ceiol9x4.js} +0 -0
@@ -1,8 +1,23 @@
1
- import type { PointerEvent } from 'react';
2
- import { ArrowLeft, ArrowRight, Maximize2, PanelRightOpen, Plus, X } from 'lucide-react';
3
- import type { DocBrowserTab } from './doc-browser-context';
4
- import { cn } from '@/shared/lib/utils';
5
- import { t } from '@/shared/lib/i18n';
1
+ import type { ButtonHTMLAttributes, PointerEvent, ReactElement } from "react";
2
+ import {
3
+ ArrowLeft,
4
+ ArrowRight,
5
+ Maximize2,
6
+ PanelRightOpen,
7
+ Pin,
8
+ PinOff,
9
+ Plus,
10
+ X,
11
+ } from "lucide-react";
12
+ import type { DocBrowserDockState, DocBrowserTab } from "./doc-browser-context";
13
+ import { cn } from "@/shared/lib/utils";
14
+ import { t } from "@/shared/lib/i18n";
15
+ import {
16
+ Tooltip,
17
+ TooltipContent,
18
+ TooltipProvider,
19
+ TooltipTrigger,
20
+ } from "@/shared/components/ui/tooltip";
6
21
 
7
22
  type DocBrowserTabStripProps = {
8
23
  tabs: DocBrowserTab[];
@@ -11,9 +26,11 @@ type DocBrowserTabStripProps = {
11
26
  canGoForward: boolean;
12
27
  isDocked: boolean;
13
28
  isFullscreen: boolean;
29
+ dockState?: DocBrowserDockState;
14
30
  onGoBack: () => void;
15
31
  onGoForward: () => void;
16
32
  onOpenNewTab: () => void;
33
+ onToggleDock?: () => void;
17
34
  onSetActiveTab: (tabId: string) => void;
18
35
  onCloseTab: (tabId: string) => void;
19
36
  onClose: () => void;
@@ -21,6 +38,53 @@ type DocBrowserTabStripProps = {
21
38
  onToggleMode: () => void;
22
39
  };
23
40
 
41
+ type DocBrowserIconActionButtonProps = Omit<
42
+ ButtonHTMLAttributes<HTMLButtonElement>,
43
+ "children" | "type"
44
+ > & {
45
+ disabled?: boolean;
46
+ icon: ReactElement;
47
+ label: string;
48
+ variant?: "tab" | "toolbar";
49
+ };
50
+
51
+ function DocBrowserIconActionButton({
52
+ className,
53
+ disabled = false,
54
+ icon,
55
+ label,
56
+ variant = "toolbar",
57
+ ...buttonProps
58
+ }: DocBrowserIconActionButtonProps) {
59
+ const button = (
60
+ <button
61
+ {...buttonProps}
62
+ type="button"
63
+ disabled={disabled}
64
+ className={cn(
65
+ variant === "toolbar"
66
+ ? "rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:cursor-not-allowed disabled:text-gray-300 disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-gray-300"
67
+ : "rounded p-0.5 transition-colors hover:bg-black/10",
68
+ className,
69
+ )}
70
+ aria-label={label}
71
+ >
72
+ {icon}
73
+ </button>
74
+ );
75
+
76
+ return (
77
+ <Tooltip>
78
+ <TooltipTrigger asChild>
79
+ {disabled ? <span className="inline-flex">{button}</span> : button}
80
+ </TooltipTrigger>
81
+ <TooltipContent side="bottom" className="text-xs">
82
+ {label}
83
+ </TooltipContent>
84
+ </Tooltip>
85
+ );
86
+ }
87
+
24
88
  export function DocBrowserTabStrip({
25
89
  tabs,
26
90
  activeTabId,
@@ -28,113 +92,135 @@ export function DocBrowserTabStrip({
28
92
  canGoForward,
29
93
  isDocked,
30
94
  isFullscreen,
95
+ dockState,
31
96
  onGoBack,
32
97
  onGoForward,
33
98
  onOpenNewTab,
99
+ onToggleDock,
34
100
  onSetActiveTab,
35
101
  onCloseTab,
36
102
  onClose,
37
103
  onDragStart,
38
104
  onToggleMode,
39
105
  }: DocBrowserTabStripProps) {
106
+ const backLabel = t("docBrowserBack");
107
+ const closeLabel = t("docBrowserClose");
108
+ const closeTabLabel = t("docBrowserCloseTab");
109
+ const dockLabel = dockState?.isDocked
110
+ ? dockState.removable
111
+ ? t("sideDockUnpinCurrent")
112
+ : t("sideDockBuiltInDocked")
113
+ : t("sideDockPinCurrent");
114
+ const forwardLabel = t("docBrowserForward");
115
+ const modeLabel = isDocked
116
+ ? t("docBrowserFloatMode")
117
+ : t("docBrowserDockMode");
118
+ const newTabLabel = t("docBrowserNewTab");
119
+
40
120
  return (
41
- <div
42
- data-testid="doc-browser-tab-strip"
43
- className={cn(
44
- 'flex h-11 items-stretch gap-2 px-2.5 bg-background border-b border-[#f1e7d4] shrink-0 select-none',
45
- isFullscreen && 'h-[calc(env(safe-area-inset-top,0px)+2.75rem)] pt-[env(safe-area-inset-top,0px)]',
46
- )}
47
- onPointerDown={!isDocked && !isFullscreen ? onDragStart : undefined}
48
- >
121
+ <TooltipProvider delayDuration={250}>
49
122
  <div
50
- className="doc-browser-tab-scrollbar flex h-full min-w-0 flex-1 items-center gap-1.5 overflow-x-auto"
51
- onPointerDown={(event) => event.stopPropagation()}
123
+ data-testid="doc-browser-tab-strip"
124
+ className={cn(
125
+ "flex h-11 items-stretch gap-2 px-2.5 bg-background border-b border-[#f1e7d4] shrink-0 select-none",
126
+ isFullscreen &&
127
+ "h-[calc(env(safe-area-inset-top,0px)+2.75rem)] pt-[env(safe-area-inset-top,0px)]",
128
+ )}
129
+ onPointerDown={!isDocked && !isFullscreen ? onDragStart : undefined}
52
130
  >
53
- {tabs.map((tab) => {
54
- const isActive = tab.id === activeTabId;
55
- return (
56
- <div
57
- key={tab.id}
58
- className={cn(
59
- 'inline-flex items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors',
60
- isActive
61
- ? 'bg-amber-50/80 border-amber-200 text-amber-900 shadow-[0_1px_2px_rgba(30,20,10,0.04)]'
62
- : 'bg-[#f9f8f5] border-[#eee3d1] text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212]',
63
- )}
64
- >
65
- <button
66
- type="button"
67
- onClick={() => onSetActiveTab(tab.id)}
68
- className="truncate text-left px-1"
69
- title={tab.title}
70
- >
71
- {tab.title || t('docBrowserTabUntitled')}
72
- </button>
73
- <button
74
- type="button"
75
- onClick={(event) => {
76
- event.stopPropagation();
77
- onCloseTab(tab.id);
78
- }}
79
- className="rounded p-0.5 hover:bg-black/10"
80
- aria-label={t('docBrowserCloseTab')}
81
- >
82
- <X className="w-3 h-3" />
83
- </button>
84
- </div>
85
- );
86
- })}
87
- </div>
88
- <div
89
- className="flex h-full items-center gap-1 shrink-0"
90
- data-testid="doc-browser-tab-actions"
91
- onPointerDown={(event) => event.stopPropagation()}
92
- >
93
- <button
94
- type="button"
95
- onClick={onGoBack}
96
- disabled={!canGoBack}
97
- className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:cursor-not-allowed disabled:text-gray-300 disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-gray-300"
98
- title={t('docBrowserBack')}
99
- >
100
- <ArrowLeft className="w-3.5 h-3.5" />
101
- </button>
102
- <button
103
- type="button"
104
- onClick={onGoForward}
105
- disabled={!canGoForward}
106
- className="rounded-md p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 disabled:cursor-not-allowed disabled:text-gray-300 disabled:opacity-60 disabled:hover:bg-transparent disabled:hover:text-gray-300"
107
- title={t('docBrowserForward')}
131
+ <div
132
+ className="doc-browser-tab-scrollbar flex h-full min-w-0 flex-1 items-center gap-1.5 overflow-x-auto"
133
+ onPointerDown={(event) => event.stopPropagation()}
108
134
  >
109
- <ArrowRight className="w-3.5 h-3.5" />
110
- </button>
111
- <button
112
- type="button"
113
- onClick={onOpenNewTab}
114
- className="hover:bg-gray-100 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
115
- title={t('docBrowserNewTab')}
116
- >
117
- <Plus className="w-3.5 h-3.5" />
118
- </button>
119
- {!isFullscreen ? (
120
- <button
121
- type="button"
122
- onClick={onToggleMode}
123
- className="hover:bg-gray-100 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
124
- title={isDocked ? t('docBrowserFloatMode') : t('docBrowserDockMode')}
125
- >
126
- {isDocked ? <Maximize2 className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
127
- </button>
128
- ) : null}
129
- <button
130
- type="button"
131
- onClick={onClose}
132
- className="hover:bg-gray-100 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
133
- title={t('docBrowserClose')}
135
+ {tabs.map((tab) => {
136
+ const isActive = tab.id === activeTabId;
137
+ return (
138
+ <div
139
+ key={tab.id}
140
+ className={cn(
141
+ "inline-flex items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors",
142
+ isActive
143
+ ? "bg-amber-50/80 border-amber-200 text-amber-900 shadow-[0_1px_2px_rgba(30,20,10,0.04)]"
144
+ : "bg-[#f9f8f5] border-[#eee3d1] text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212]",
145
+ )}
146
+ >
147
+ <button
148
+ type="button"
149
+ onClick={() => onSetActiveTab(tab.id)}
150
+ className="truncate text-left px-1"
151
+ title={tab.title}
152
+ >
153
+ {tab.title || t("docBrowserTabUntitled")}
154
+ </button>
155
+ <DocBrowserIconActionButton
156
+ icon={<X className="w-3 h-3" />}
157
+ label={closeTabLabel}
158
+ variant="tab"
159
+ onClick={(event) => {
160
+ event.stopPropagation();
161
+ onCloseTab(tab.id);
162
+ }}
163
+ />
164
+ </div>
165
+ );
166
+ })}
167
+ </div>
168
+ <div
169
+ className="flex h-full items-center gap-1 shrink-0"
170
+ data-testid="doc-browser-tab-actions"
171
+ onPointerDown={(event) => event.stopPropagation()}
134
172
  >
135
- <X className="w-3.5 h-3.5" />
136
- </button>
173
+ <DocBrowserIconActionButton
174
+ disabled={!canGoBack}
175
+ icon={<ArrowLeft className="w-3.5 h-3.5" />}
176
+ label={backLabel}
177
+ onClick={onGoBack}
178
+ />
179
+ <DocBrowserIconActionButton
180
+ disabled={!canGoForward}
181
+ icon={<ArrowRight className="w-3.5 h-3.5" />}
182
+ label={forwardLabel}
183
+ onClick={onGoForward}
184
+ />
185
+ <DocBrowserIconActionButton
186
+ icon={<Plus className="w-3.5 h-3.5" />}
187
+ label={newTabLabel}
188
+ onClick={onOpenNewTab}
189
+ />
190
+ {dockState?.canDock ? (
191
+ <DocBrowserIconActionButton
192
+ disabled={dockState.isDocked && !dockState.removable}
193
+ icon={
194
+ dockState.isDocked && dockState.removable ? (
195
+ <PinOff className="w-3.5 h-3.5" />
196
+ ) : (
197
+ <Pin className="w-3.5 h-3.5" />
198
+ )
199
+ }
200
+ label={dockLabel}
201
+ onClick={onToggleDock}
202
+ />
203
+ ) : null}
204
+ {!isFullscreen ? (
205
+ <DocBrowserIconActionButton
206
+ icon={
207
+ isDocked ? (
208
+ <Maximize2 className="w-3.5 h-3.5" />
209
+ ) : (
210
+ <PanelRightOpen className="w-3.5 h-3.5" />
211
+ )
212
+ }
213
+ label={modeLabel}
214
+ onClick={onToggleMode}
215
+ />
216
+ ) : null}
217
+ <DocBrowserIconActionButton
218
+ icon={<X className="w-3.5 h-3.5" />}
219
+ label={closeLabel}
220
+ onClick={onClose}
221
+ />
222
+ </div>
137
223
  </div>
138
- </div>
224
+ </TooltipProvider>
139
225
  );
140
226
  }
@@ -1,7 +1,11 @@
1
1
  import { fireEvent, render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
2
3
  import { beforeEach, describe, expect, it, vi } from "vitest";
3
4
  import { DocBrowser } from "@/shared/components/doc-browser/doc-browser";
4
- import type { DocBrowserContextValue, DocBrowserTab } from "@/shared/components/doc-browser/doc-browser-context";
5
+ import type {
6
+ DocBrowserContextValue,
7
+ DocBrowserTab,
8
+ } from "@/shared/components/doc-browser/doc-browser-context";
5
9
  import { PANEL_APPS_DOC_BROWSER_RENDERERS } from "@/features/panel-apps";
6
10
 
7
11
  const { navigateMock } = vi.hoisted(() => ({
@@ -9,11 +13,13 @@ const { navigateMock } = vi.hoisted(() => ({
9
13
  }));
10
14
 
11
15
  vi.mock("react-router-dom", async () => ({
12
- ...(await vi.importActual("react-router-dom") as object),
16
+ ...((await vi.importActual("react-router-dom")) as object),
13
17
  useNavigate: () => navigateMock,
14
18
  }));
15
19
 
16
- const { docBrowserState } = vi.hoisted<{ docBrowserState: DocBrowserContextValue }>(() => ({
20
+ const { docBrowserState } = vi.hoisted<{
21
+ docBrowserState: DocBrowserContextValue;
22
+ }>(() => ({
17
23
  docBrowserState: {
18
24
  isOpen: true,
19
25
  mode: "docked" as "docked" | "floating",
@@ -29,11 +35,13 @@ const { docBrowserState } = vi.hoisted<{ docBrowserState: DocBrowserContextValue
29
35
  },
30
36
  ],
31
37
  activeTabId: "docs",
32
- activeHistory: [{
33
- kind: "docs" as const,
34
- tabId: "docs",
35
- url: "https://docs.nextclaw.io/en/guide/getting-started",
36
- }],
38
+ activeHistory: [
39
+ {
40
+ kind: "docs" as const,
41
+ tabId: "docs",
42
+ url: "https://docs.nextclaw.io/en/guide/getting-started",
43
+ },
44
+ ],
37
45
  activeHistoryIndex: 0,
38
46
  currentTab: {
39
47
  id: "docs",
@@ -99,11 +107,13 @@ describe("DocBrowser", () => {
99
107
  },
100
108
  ];
101
109
  docBrowserState.activeTabId = "docs";
102
- docBrowserState.activeHistory = [{
103
- kind: "docs" as const,
104
- tabId: "docs",
105
- url: "https://docs.nextclaw.io/en/guide/getting-started",
106
- }];
110
+ docBrowserState.activeHistory = [
111
+ {
112
+ kind: "docs" as const,
113
+ tabId: "docs",
114
+ url: "https://docs.nextclaw.io/en/guide/getting-started",
115
+ },
116
+ ];
107
117
  docBrowserState.activeHistoryIndex = 0;
108
118
  docBrowserState.currentTab = docBrowserState.tabs[0];
109
119
  Object.defineProperty(window, "innerWidth", {
@@ -133,24 +143,41 @@ describe("DocBrowser", () => {
133
143
  expect(panel.className).toContain("inset-0");
134
144
  expect(panel.className).toContain("w-screen");
135
145
  expect(
136
- screen.queryByTitle(/float/i) ?? screen.queryByTitle(/dock/i),
146
+ screen.queryByRole("button", { name: /float/i }) ??
147
+ screen.queryByRole("button", { name: /dock/i }),
137
148
  ).toBeNull();
138
149
  expect(panel.querySelector(".cursor-ew-resize")).toBeNull();
139
150
  expect(panel.querySelector(".cursor-se-resize")).toBeNull();
140
151
  });
141
152
 
142
- it("keeps browser window controls on the tab strip", () => {
143
- render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
153
+ it("keeps browser window controls on the tab strip", async () => {
154
+ const user = userEvent.setup();
155
+ render(
156
+ <DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />,
157
+ );
144
158
 
145
159
  const tabStrip = screen.getByTestId("doc-browser-tab-strip");
146
160
  const tabActions = screen.getByTestId("doc-browser-tab-actions");
161
+ const newTabButton = screen.getByRole("button", { name: "New Tab" });
147
162
 
148
- expect(tabActions.contains(screen.getByTitle("Back"))).toBe(true);
149
- expect(tabActions.contains(screen.getByTitle("Forward"))).toBe(true);
150
- expect(tabStrip.contains(screen.getByTitle("Float Window"))).toBe(true);
151
- expect(tabStrip.contains(screen.getByTitle("Close"))).toBe(true);
152
- expect(tabActions.contains(screen.getByTitle("New Tab"))).toBe(true);
163
+ expect(
164
+ tabActions.contains(screen.getByRole("button", { name: "Back" })),
165
+ ).toBe(true);
166
+ expect(
167
+ tabActions.contains(screen.getByRole("button", { name: "Forward" })),
168
+ ).toBe(true);
169
+ expect(
170
+ tabStrip.contains(screen.getByRole("button", { name: "Float Window" })),
171
+ ).toBe(true);
172
+ expect(
173
+ tabStrip.contains(screen.getByRole("button", { name: "Close" })),
174
+ ).toBe(true);
175
+ expect(tabActions.contains(newTabButton)).toBe(true);
153
176
  expect(screen.queryByText("Embedded Browser")).toBeNull();
177
+
178
+ await user.hover(newTabButton);
179
+
180
+ expect((await screen.findAllByText("New Tab")).length).toBeGreaterThan(0);
154
181
  });
155
182
 
156
183
  it("uses active history to enable browser back and forward actions", () => {
@@ -178,10 +205,12 @@ describe("DocBrowser", () => {
178
205
  docBrowserState.activeHistoryIndex = 1;
179
206
  docBrowserState.currentTab = historyTab;
180
207
 
181
- render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
208
+ render(
209
+ <DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />,
210
+ );
182
211
 
183
- fireEvent.click(screen.getByTitle("Back"));
184
- fireEvent.click(screen.getByTitle("Forward"));
212
+ fireEvent.click(screen.getByRole("button", { name: "Back" }));
213
+ fireEvent.click(screen.getByRole("button", { name: "Forward" }));
185
214
 
186
215
  expect(docBrowserState.goBack).toHaveBeenCalled();
187
216
  expect(docBrowserState.goForward).toHaveBeenCalled();
@@ -212,8 +241,14 @@ describe("DocBrowser", () => {
212
241
 
213
242
  const { rerender } = render(<DocBrowser />);
214
243
 
215
- expect(screen.getByTitle("Back")).toHaveProperty("disabled", false);
216
- expect(screen.getByTitle("Forward")).toHaveProperty("disabled", true);
244
+ expect(screen.getByRole("button", { name: "Back" })).toHaveProperty(
245
+ "disabled",
246
+ false,
247
+ );
248
+ expect(screen.getByRole("button", { name: "Forward" })).toHaveProperty(
249
+ "disabled",
250
+ true,
251
+ );
217
252
 
218
253
  const oldestTab: DocBrowserTab = {
219
254
  ...latestTab,
@@ -225,18 +260,94 @@ describe("DocBrowser", () => {
225
260
  docBrowserState.activeHistoryIndex = 0;
226
261
  rerender(<DocBrowser />);
227
262
 
228
- expect(screen.getByTitle("Back")).toHaveProperty("disabled", true);
229
- expect(screen.getByTitle("Forward")).toHaveProperty("disabled", false);
263
+ expect(screen.getByRole("button", { name: "Back" })).toHaveProperty(
264
+ "disabled",
265
+ true,
266
+ );
267
+ expect(screen.getByRole("button", { name: "Forward" })).toHaveProperty(
268
+ "disabled",
269
+ false,
270
+ );
230
271
  });
231
272
 
232
273
  it("opens the start page from the fixed new tab action", () => {
233
274
  render(<DocBrowser />);
234
275
 
235
- fireEvent.click(screen.getByTitle("New Tab"));
276
+ fireEvent.click(screen.getByRole("button", { name: "New Tab" }));
236
277
 
237
278
  expect(docBrowserState.openNewTab).toHaveBeenCalled();
238
279
  });
239
280
 
281
+ it("pins the current tab through injected dock controls", () => {
282
+ const pinTab = vi.fn();
283
+ const unpinTab = vi.fn();
284
+
285
+ render(
286
+ <DocBrowser
287
+ dockControls={{
288
+ getDockState: () => ({
289
+ canDock: true,
290
+ isDocked: false,
291
+ removable: false,
292
+ }),
293
+ pinTab,
294
+ unpinTab,
295
+ }}
296
+ />,
297
+ );
298
+
299
+ fireEvent.click(screen.getByRole("button", { name: "Pin to SideDock" }));
300
+
301
+ expect(pinTab).toHaveBeenCalledWith(docBrowserState.currentTab);
302
+ expect(unpinTab).not.toHaveBeenCalled();
303
+ });
304
+
305
+ it("unpins a removable docked tab through injected dock controls", () => {
306
+ const pinTab = vi.fn();
307
+ const unpinTab = vi.fn();
308
+
309
+ render(
310
+ <DocBrowser
311
+ dockControls={{
312
+ getDockState: () => ({
313
+ canDock: true,
314
+ isDocked: true,
315
+ removable: true,
316
+ }),
317
+ pinTab,
318
+ unpinTab,
319
+ }}
320
+ />,
321
+ );
322
+
323
+ fireEvent.click(
324
+ screen.getByRole("button", { name: "Remove from SideDock" }),
325
+ );
326
+
327
+ expect(unpinTab).toHaveBeenCalledWith(docBrowserState.currentTab);
328
+ expect(pinTab).not.toHaveBeenCalled();
329
+ });
330
+
331
+ it("disables dock controls for built-in shortcuts", () => {
332
+ render(
333
+ <DocBrowser
334
+ dockControls={{
335
+ getDockState: () => ({
336
+ canDock: true,
337
+ isDocked: true,
338
+ removable: false,
339
+ }),
340
+ pinTab: vi.fn(),
341
+ unpinTab: vi.fn(),
342
+ }}
343
+ />,
344
+ );
345
+
346
+ expect(
347
+ screen.getByRole("button", { name: "Built-in shortcut" }),
348
+ ).toHaveProperty("disabled", true);
349
+ });
350
+
240
351
  it("renders a start page for home tabs", () => {
241
352
  const homeTab: DocBrowserTab = {
242
353
  id: "home",
@@ -251,7 +362,9 @@ describe("DocBrowser", () => {
251
362
  docBrowserState.activeTabId = homeTab.id;
252
363
  docBrowserState.currentTab = homeTab;
253
364
 
254
- render(<DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />);
365
+ render(
366
+ <DocBrowser customTabRenderers={PANEL_APPS_DOC_BROWSER_RENDERERS} />,
367
+ );
255
368
 
256
369
  expect(screen.getAllByText("Start Page").length).toBeGreaterThan(0);
257
370
  expect(screen.getByText("Apps")).toBeTruthy();
@@ -264,10 +377,14 @@ describe("DocBrowser", () => {
264
377
 
265
378
  render(<DocBrowser />);
266
379
 
267
- firePointerEvent(screen.getByTestId("doc-browser-resize-right"), "pointerdown", {
268
- clientX: 1160,
269
- pointerId: 1,
270
- });
380
+ firePointerEvent(
381
+ screen.getByTestId("doc-browser-resize-right"),
382
+ "pointerdown",
383
+ {
384
+ clientX: 1160,
385
+ pointerId: 1,
386
+ },
387
+ );
271
388
  firePointerEvent(window, "pointermove", { clientX: 1120, pointerId: 1 });
272
389
 
273
390
  const panel = screen.getByTestId("doc-browser-panel");
@@ -280,10 +397,14 @@ describe("DocBrowser", () => {
280
397
 
281
398
  render(<DocBrowser />);
282
399
 
283
- firePointerEvent(screen.getByTestId("doc-browser-resize-left"), "pointerdown", {
284
- clientX: 680,
285
- pointerId: 1,
286
- });
400
+ firePointerEvent(
401
+ screen.getByTestId("doc-browser-resize-left"),
402
+ "pointerdown",
403
+ {
404
+ clientX: 680,
405
+ pointerId: 1,
406
+ },
407
+ );
287
408
  firePointerEvent(window, "pointermove", { clientX: 620, pointerId: 1 });
288
409
 
289
410
  const panel = screen.getByTestId("doc-browser-panel");
@@ -296,11 +417,15 @@ describe("DocBrowser", () => {
296
417
 
297
418
  render(<DocBrowser />);
298
419
 
299
- firePointerEvent(screen.getByTestId("doc-browser-resize-top"), "pointerdown", {
300
- clientX: 900,
301
- clientY: 80,
302
- pointerId: 1,
303
- });
420
+ firePointerEvent(
421
+ screen.getByTestId("doc-browser-resize-top"),
422
+ "pointerdown",
423
+ {
424
+ clientX: 900,
425
+ clientY: 80,
426
+ pointerId: 1,
427
+ },
428
+ );
304
429
  firePointerEvent(window, "pointermove", {
305
430
  clientX: 900,
306
431
  clientY: 120,