@nextclaw/ui 0.12.35-beta.6 → 0.12.35-beta.7
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.
- package/CHANGELOG.md +63 -0
- package/dist/assets/api-DruAcori.js +15 -0
- package/dist/assets/{book-open-qc0UVn6m.js → book-open-cPX0Za7Z.js} +1 -1
- package/dist/assets/channels-list-page-D-tv11dc.js +8 -0
- package/dist/assets/chat-page-MyXEZI8R.js +105 -0
- package/dist/assets/{config-split-page-DW8tP9Bc.js → config-split-page-BFh_Qixx.js} +1 -1
- package/dist/assets/confirm-dialog-B0TP-vPd.js +5 -0
- package/dist/assets/{createLucideIcon-CA0sgoMJ.js → createLucideIcon-Br1EFMEr.js} +1 -1
- package/dist/assets/{desktop-update-config-DARSwQG8.js → desktop-update-config-oZpIKKtS.js} +1 -1
- package/dist/assets/{dist-D5zsy7pX.js → dist-CpwDZDtv.js} +1 -1
- package/dist/assets/{dist-DNxIa3pe.js → dist-rzakR_bu.js} +1 -1
- package/dist/assets/doc-browser-B2OFjScU.js +3 -0
- package/dist/assets/doc-browser-BrUQEZ1j.js +1 -0
- package/dist/assets/doc-browser-context-DB6TTHeE.js +1 -0
- package/dist/assets/doc-browser-route-registry.utils-x5ijvk0u.js +1 -0
- package/dist/assets/{external-link-C9fbyi6k.js → external-link-C3usm3se.js} +1 -1
- package/dist/assets/index-D2H2CKYR.js +101 -0
- package/dist/assets/index-v0fAGXQW.css +1 -0
- package/dist/assets/{mcp-marketplace-page-BiExW5wq.js → mcp-marketplace-page-CegP14Hl.js} +2 -2
- package/dist/assets/mcp-marketplace-page-MjpFEEkY.js +1 -0
- package/dist/assets/model-config-fIE_Asps.js +1 -0
- package/dist/assets/play-CqNi0u02.js +1 -0
- package/dist/assets/plus-BcIC8dG1.js +1 -0
- package/dist/assets/{provider-scoped-model-input-Bk8jlpRm.js → provider-scoped-model-input-D4zRsGZ6.js} +1 -1
- package/dist/assets/providers-list-CqsOk2hr.js +1 -0
- package/dist/assets/react-B2X_ph0a.js +1 -0
- package/dist/assets/remote-BMWQUPDW.js +1 -0
- package/dist/assets/runtime-config-page-BJ86cC8a.js +1 -0
- package/dist/assets/{save-CG4Hb1u8.js → save-D10M5vQg.js} +1 -1
- package/dist/assets/{search-lWpOIHEn.js → search-CdhgFNwf.js} +1 -1
- package/dist/assets/search-config-TbCPlRE7.js +1 -0
- package/dist/assets/{secrets-config-ZbLzjIRx.js → secrets-config-DBIEKP_V.js} +1 -1
- package/dist/assets/{setting-row-CNfTKDjc.js → setting-row-8VkukJoH.js} +1 -1
- package/dist/assets/{tabs-custom-BbZoysAl.js → tabs-custom-BBXu1OGs.js} +1 -1
- package/dist/assets/{tag-chip-Dl8JbhC7.js → tag-chip-ix15P-r4.js} +1 -1
- package/dist/assets/wrench-qVIwShTF.js +1 -0
- package/dist/assets/x-Chb07O-Y.js +1 -0
- package/dist/index.html +17 -15
- package/package.json +8 -8
- package/src/features/panel-apps/components/panel-app-list-item.test.tsx +60 -0
- package/src/features/panel-apps/components/panel-app-list-item.tsx +120 -30
- package/src/features/panel-apps/components/panel-apps-list.tsx +8 -1
- package/src/features/panel-apps/hooks/use-panel-apps.ts +10 -0
- package/src/features/panel-apps/managers/panel-app-bridge.manager.test.ts +85 -1
- package/src/features/panel-apps/managers/panel-app-bridge.manager.ts +123 -6
- package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +7 -3
- package/src/features/panel-apps/utils/panel-app-view.utils.test.ts +66 -0
- package/src/features/panel-apps/utils/panel-app-view.utils.ts +13 -2
- package/src/index.css +23 -0
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +170 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
- package/src/shared/components/doc-browser/doc-browser-home-page.tsx +95 -0
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +0 -90
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +117 -44
- package/src/shared/components/doc-browser/doc-browser.test.tsx +159 -1
- package/src/shared/components/doc-browser/doc-browser.tsx +42 -23
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +183 -71
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +52 -1
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +9 -0
- package/src/shared/components/doc-browser/utils/doc-browser-route-registry.utils.ts +232 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
- package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +2 -0
- package/src/shared/lib/api/types.ts +1 -0
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +10 -0
- package/dist/assets/api-BTZui7nj.js +0 -15
- package/dist/assets/channels-list-page-DBtU_M1Y.js +0 -8
- package/dist/assets/chat-page-BcoJYo91.js +0 -105
- package/dist/assets/doc-browser-B9skxzVo.js +0 -1
- package/dist/assets/doc-browser-CwyITRXx.js +0 -1
- package/dist/assets/doc-browser-context-Do2qSrB8.js +0 -1
- package/dist/assets/index-BYRP_BMj.css +0 -1
- package/dist/assets/index-DckPGTdM.js +0 -103
- package/dist/assets/mcp-marketplace-page-Cj6MDSkE.js +0 -1
- package/dist/assets/model-config-DbdY7fF1.js +0 -1
- package/dist/assets/notice-card-CyiVcu_Z.js +0 -5
- package/dist/assets/play-D-s0C4MN.js +0 -1
- package/dist/assets/plus-DOwjM_kF.js +0 -1
- package/dist/assets/providers-list-CJjNStDV.js +0 -1
- package/dist/assets/react-BaRotFd6.js +0 -1
- package/dist/assets/remote-gl8QckNc.js +0 -1
- package/dist/assets/runtime-config-page-BMFc1yzf.js +0 -1
- package/dist/assets/search-config-DUOR26if.js +0 -1
- package/dist/assets/x-D6GwT_8k.js +0 -1
- /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-CY1lg9IE.js} +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { PanelAppEntryView } from '@/shared/lib/api';
|
|
3
|
+
import { getPanelAppViewEntries } from './panel-app-view.utils';
|
|
4
|
+
|
|
5
|
+
function createPanelAppEntry(overrides: Partial<PanelAppEntryView> = {}): PanelAppEntryView {
|
|
6
|
+
const createdAt = overrides.createdAt ?? '2026-05-01T00:00:00.000Z';
|
|
7
|
+
const updatedAt = overrides.updatedAt ?? createdAt;
|
|
8
|
+
return {
|
|
9
|
+
id: overrides.id ?? overrides.fileName ?? 'demo.panel.html',
|
|
10
|
+
fileName: overrides.fileName ?? 'demo.panel.html',
|
|
11
|
+
title: overrides.title ?? 'Demo',
|
|
12
|
+
contentPath: overrides.contentPath ?? '/api/panel-apps/demo/content',
|
|
13
|
+
createdAt,
|
|
14
|
+
updatedAt,
|
|
15
|
+
sizeBytes: overrides.sizeBytes ?? 12,
|
|
16
|
+
favorite: overrides.favorite ?? false,
|
|
17
|
+
openCount: overrides.openCount ?? 0,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('panel app view sorting', () => {
|
|
23
|
+
it('orders smart view by the latest panel app activity before favorite state', () => {
|
|
24
|
+
const entries = [
|
|
25
|
+
createPanelAppEntry({
|
|
26
|
+
fileName: 'old-favorite.panel.html',
|
|
27
|
+
title: 'Old Favorite',
|
|
28
|
+
favorite: true,
|
|
29
|
+
}),
|
|
30
|
+
createPanelAppEntry({
|
|
31
|
+
fileName: 'newly-created.panel.html',
|
|
32
|
+
title: 'Newly Created',
|
|
33
|
+
createdAt: '2026-05-27T12:00:00.000Z',
|
|
34
|
+
updatedAt: '2026-05-01T00:00:00.000Z',
|
|
35
|
+
}),
|
|
36
|
+
createPanelAppEntry({
|
|
37
|
+
fileName: 'just-opened.panel.html',
|
|
38
|
+
title: 'Just Opened',
|
|
39
|
+
lastOpenedAt: '2026-05-28T08:00:00.000Z',
|
|
40
|
+
}),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
expect(getPanelAppViewEntries(entries, 'smart').map((entry) => entry.fileName)).toEqual([
|
|
44
|
+
'just-opened.panel.html',
|
|
45
|
+
'newly-created.panel.html',
|
|
46
|
+
'old-favorite.panel.html',
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('keeps the dedicated recent-open view scoped to apps that were opened', () => {
|
|
51
|
+
const entries = [
|
|
52
|
+
createPanelAppEntry({
|
|
53
|
+
fileName: 'newly-created.panel.html',
|
|
54
|
+
createdAt: '2026-05-27T12:00:00.000Z',
|
|
55
|
+
}),
|
|
56
|
+
createPanelAppEntry({
|
|
57
|
+
fileName: 'just-opened.panel.html',
|
|
58
|
+
lastOpenedAt: '2026-05-28T08:00:00.000Z',
|
|
59
|
+
}),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
expect(getPanelAppViewEntries(entries, 'recent-open').map((entry) => entry.fileName)).toEqual([
|
|
63
|
+
'just-opened.panel.html',
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -27,9 +27,8 @@ function comparePanelApps(left: PanelAppEntryView, right: PanelAppEntryView, mod
|
|
|
27
27
|
return compareByName(left, right);
|
|
28
28
|
}
|
|
29
29
|
return (
|
|
30
|
+
comparePanelAppActivityDesc(left, right) ||
|
|
30
31
|
Number(right.favorite) - Number(left.favorite) ||
|
|
31
|
-
compareIsoDesc(left.lastOpenedAt, right.lastOpenedAt) ||
|
|
32
|
-
compareIsoDesc(left.updatedAt, right.updatedAt) ||
|
|
33
32
|
compareByName(left, right)
|
|
34
33
|
);
|
|
35
34
|
}
|
|
@@ -38,6 +37,18 @@ function compareByName(left: PanelAppEntryView, right: PanelAppEntryView): numbe
|
|
|
38
37
|
return left.title.localeCompare(right.title);
|
|
39
38
|
}
|
|
40
39
|
|
|
40
|
+
function comparePanelAppActivityDesc(left: PanelAppEntryView, right: PanelAppEntryView): number {
|
|
41
|
+
return resolvePanelAppActivityMs(right) - resolvePanelAppActivityMs(left);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolvePanelAppActivityMs(entry: PanelAppEntryView): number {
|
|
45
|
+
return Math.max(
|
|
46
|
+
new Date(entry.lastOpenedAt ?? 0).getTime(),
|
|
47
|
+
new Date(entry.createdAt).getTime(),
|
|
48
|
+
new Date(entry.updatedAt).getTime(),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
function compareIsoDesc(left?: string, right?: string): number {
|
|
42
53
|
return new Date(right ?? 0).getTime() - new Date(left ?? 0).getTime();
|
|
43
54
|
}
|
package/src/index.css
CHANGED
|
@@ -91,6 +91,29 @@
|
|
|
91
91
|
background: hsl(var(--gray-400) / 0.48);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
.doc-browser-tab-scrollbar {
|
|
95
|
+
scrollbar-width: thin;
|
|
96
|
+
scrollbar-color: hsl(var(--gray-300) / 0.34) transparent;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.doc-browser-tab-scrollbar::-webkit-scrollbar {
|
|
100
|
+
width: 2px;
|
|
101
|
+
height: 2px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.doc-browser-tab-scrollbar::-webkit-scrollbar-track {
|
|
105
|
+
background: transparent;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.doc-browser-tab-scrollbar::-webkit-scrollbar-thumb {
|
|
109
|
+
background: hsl(var(--gray-300) / 0.34);
|
|
110
|
+
border-radius: 999px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.doc-browser-tab-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
114
|
+
background: hsl(var(--gray-400) / 0.44);
|
|
115
|
+
}
|
|
116
|
+
|
|
94
117
|
/* ========================================
|
|
95
118
|
GLASSMORPHISM
|
|
96
119
|
======================================== */
|
|
@@ -160,6 +160,176 @@ describe('DocBrowserProvider dedupe keys', () => {
|
|
|
160
160
|
kind: 'apps',
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
|
+
|
|
164
|
+
it('opens the built-in start page without falling back to docs', () => {
|
|
165
|
+
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
166
|
+
|
|
167
|
+
act(() => {
|
|
168
|
+
result.current.openNewTab();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(result.current.currentTab).toMatchObject({
|
|
172
|
+
currentUrl: 'nextclaw://new-tab',
|
|
173
|
+
kind: 'home',
|
|
174
|
+
title: 'Start Page',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('infers built-in apps routes without component-level options', () => {
|
|
179
|
+
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
180
|
+
|
|
181
|
+
act(() => {
|
|
182
|
+
result.current.open('nextclaw://apps?tab=service-apps');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(result.current.currentTab).toMatchObject({
|
|
186
|
+
currentUrl: 'nextclaw://apps?tab=service-apps',
|
|
187
|
+
dedupeKey: 'apps',
|
|
188
|
+
kind: 'apps',
|
|
189
|
+
title: 'Service Apps',
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('uses managed history for non-doc routes', () => {
|
|
194
|
+
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
195
|
+
|
|
196
|
+
act(() => {
|
|
197
|
+
result.current.open('data:text/html,A', {
|
|
198
|
+
kind: 'content',
|
|
199
|
+
title: 'A',
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
act(() => {
|
|
203
|
+
result.current.navigate('data:text/html,B');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(result.current.currentTab).toMatchObject({
|
|
207
|
+
currentUrl: 'data:text/html,B',
|
|
208
|
+
historyIndex: 1,
|
|
209
|
+
kind: 'content',
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
act(() => {
|
|
213
|
+
result.current.goBack();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(result.current.currentTab).toMatchObject({
|
|
217
|
+
currentUrl: 'data:text/html,A',
|
|
218
|
+
historyIndex: 0,
|
|
219
|
+
kind: 'content',
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('stops back and forward navigation at browser history boundaries', () => {
|
|
224
|
+
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
225
|
+
|
|
226
|
+
act(() => {
|
|
227
|
+
result.current.open('data:text/html,A', {
|
|
228
|
+
kind: 'content',
|
|
229
|
+
title: 'A',
|
|
230
|
+
});
|
|
231
|
+
result.current.navigate('data:text/html,B');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
act(() => {
|
|
235
|
+
result.current.goBack();
|
|
236
|
+
result.current.goBack();
|
|
237
|
+
result.current.goBack();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(result.current.currentTab).toMatchObject({
|
|
241
|
+
kind: 'docs',
|
|
242
|
+
});
|
|
243
|
+
expect(result.current.activeHistoryIndex).toBe(0);
|
|
244
|
+
|
|
245
|
+
act(() => {
|
|
246
|
+
result.current.goForward();
|
|
247
|
+
result.current.goForward();
|
|
248
|
+
result.current.goForward();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(result.current.currentTab).toMatchObject({
|
|
252
|
+
currentUrl: 'data:text/html,B',
|
|
253
|
+
historyIndex: 1,
|
|
254
|
+
history: ['data:text/html,A', 'data:text/html,B'],
|
|
255
|
+
});
|
|
256
|
+
expect(result.current.activeHistoryIndex).toBe(2);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('records active tab switching in the top-level browser history', () => {
|
|
260
|
+
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
261
|
+
|
|
262
|
+
act(() => {
|
|
263
|
+
result.current.open('data:text/html,A', {
|
|
264
|
+
kind: 'content',
|
|
265
|
+
title: 'A',
|
|
266
|
+
});
|
|
267
|
+
result.current.openNewTab();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(result.current.currentTab).toMatchObject({
|
|
271
|
+
currentUrl: 'nextclaw://new-tab',
|
|
272
|
+
kind: 'home',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
act(() => {
|
|
276
|
+
result.current.goBack();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(result.current.currentTab).toMatchObject({
|
|
280
|
+
currentUrl: 'data:text/html,A',
|
|
281
|
+
kind: 'content',
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
act(() => {
|
|
285
|
+
result.current.goForward();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
expect(result.current.currentTab).toMatchObject({
|
|
289
|
+
currentUrl: 'nextclaw://new-tab',
|
|
290
|
+
kind: 'home',
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('truncates forward history before appending manual navigation after going back', () => {
|
|
295
|
+
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
296
|
+
|
|
297
|
+
act(() => {
|
|
298
|
+
result.current.open('data:text/html,A', {
|
|
299
|
+
kind: 'content',
|
|
300
|
+
title: 'A',
|
|
301
|
+
});
|
|
302
|
+
result.current.navigate('data:text/html,B');
|
|
303
|
+
result.current.navigate('data:text/html,C');
|
|
304
|
+
result.current.goBack();
|
|
305
|
+
result.current.navigate('data:text/html,D');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(result.current.currentTab).toMatchObject({
|
|
309
|
+
currentUrl: 'data:text/html,D',
|
|
310
|
+
historyIndex: 2,
|
|
311
|
+
history: ['data:text/html,A', 'data:text/html,B', 'data:text/html,D'],
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('deduplicates consecutive manual navigation to the current URL', () => {
|
|
316
|
+
const { result } = renderHook(() => useDocBrowser(), { wrapper });
|
|
317
|
+
|
|
318
|
+
act(() => {
|
|
319
|
+
result.current.open('data:text/html,A', {
|
|
320
|
+
kind: 'content',
|
|
321
|
+
title: 'A',
|
|
322
|
+
});
|
|
323
|
+
result.current.navigate('data:text/html,B');
|
|
324
|
+
result.current.navigate('data:text/html,B');
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(result.current.currentTab).toMatchObject({
|
|
328
|
+
currentUrl: 'data:text/html,B',
|
|
329
|
+
historyIndex: 1,
|
|
330
|
+
history: ['data:text/html,A', 'data:text/html,B'],
|
|
331
|
+
});
|
|
332
|
+
});
|
|
163
333
|
});
|
|
164
334
|
|
|
165
335
|
describe('DocBrowserProvider persistence', () => {
|
|
@@ -14,6 +14,8 @@ export type {
|
|
|
14
14
|
DocBrowserTabKind,
|
|
15
15
|
} from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
16
16
|
export {
|
|
17
|
+
DOC_BROWSER_HOME_TAB_KIND,
|
|
18
|
+
DOC_BROWSER_HOME_URL,
|
|
17
19
|
DOCS_DEFAULT_BASE_URL,
|
|
18
20
|
isDocsUrl,
|
|
19
21
|
} from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
|
|
@@ -37,6 +39,7 @@ export function DocBrowserProvider({ children }: { children: ReactNode }) {
|
|
|
37
39
|
...snapshot,
|
|
38
40
|
currentTab,
|
|
39
41
|
open: docBrowserManager.open,
|
|
42
|
+
openNewTab: docBrowserManager.openNewTab,
|
|
40
43
|
close: docBrowserManager.close,
|
|
41
44
|
toggleMode: docBrowserManager.toggleMode,
|
|
42
45
|
navigate: docBrowserManager.navigate,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
BookOpen,
|
|
4
|
+
Boxes,
|
|
5
|
+
BrainCircuit,
|
|
6
|
+
Grid3X3,
|
|
7
|
+
Wrench,
|
|
8
|
+
} from 'lucide-react';
|
|
9
|
+
import { useNavigate } from 'react-router-dom';
|
|
10
|
+
import type { DocBrowserContextValue } from './doc-browser-context';
|
|
11
|
+
import {
|
|
12
|
+
docBrowserRouteRegistry,
|
|
13
|
+
type DocBrowserHomeNavigationItem,
|
|
14
|
+
type DocBrowserNavigationTarget,
|
|
15
|
+
} from './utils/doc-browser-route-registry.utils';
|
|
16
|
+
import { cn } from '@/shared/lib/utils';
|
|
17
|
+
|
|
18
|
+
type DocBrowserHomePageProps = {
|
|
19
|
+
open: DocBrowserContextValue['open'];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type DocBrowserHomeItem = {
|
|
23
|
+
accentClassName: string;
|
|
24
|
+
icon: ReactNode;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const HOME_ITEM_VIEW_BY_ID: Record<string, DocBrowserHomeItem> = {
|
|
28
|
+
apps: {
|
|
29
|
+
accentClassName: 'bg-emerald-50 text-emerald-700 border-emerald-100',
|
|
30
|
+
icon: <Boxes className="h-5 w-5" />,
|
|
31
|
+
},
|
|
32
|
+
'service-apps': {
|
|
33
|
+
accentClassName: 'bg-sky-50 text-sky-700 border-sky-100',
|
|
34
|
+
icon: <Grid3X3 className="h-5 w-5" />,
|
|
35
|
+
},
|
|
36
|
+
docs: {
|
|
37
|
+
accentClassName: 'bg-amber-50 text-amber-700 border-amber-100',
|
|
38
|
+
icon: <BookOpen className="h-5 w-5" />,
|
|
39
|
+
},
|
|
40
|
+
'skill-marketplace': {
|
|
41
|
+
accentClassName: 'bg-rose-50 text-rose-700 border-rose-100',
|
|
42
|
+
icon: <BrainCircuit className="h-5 w-5" />,
|
|
43
|
+
},
|
|
44
|
+
'mcp-marketplace': {
|
|
45
|
+
accentClassName: 'bg-indigo-50 text-indigo-700 border-indigo-100',
|
|
46
|
+
icon: <Wrench className="h-5 w-5" />,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function resolveHomeItemView(item: DocBrowserHomeNavigationItem): DocBrowserHomeItem {
|
|
51
|
+
return HOME_ITEM_VIEW_BY_ID[item.id] ?? {
|
|
52
|
+
accentClassName: 'bg-gray-50 text-gray-700 border-gray-100',
|
|
53
|
+
icon: <Boxes className="h-5 w-5" />,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function DocBrowserHomePage({ open }: DocBrowserHomePageProps) {
|
|
58
|
+
const navigate = useNavigate();
|
|
59
|
+
const items = docBrowserRouteRegistry.getHomeNavigationItems();
|
|
60
|
+
|
|
61
|
+
const openTarget = (target: DocBrowserNavigationTarget) => {
|
|
62
|
+
if (target.type === 'app-route') {
|
|
63
|
+
navigate(target.path);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
open(target.url, target.options);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="h-full overflow-auto bg-[#fbfaf7] px-5 py-6">
|
|
71
|
+
<div className="mx-auto w-full max-w-[640px]">
|
|
72
|
+
<div className="grid grid-cols-[repeat(auto-fit,minmax(88px,1fr))] gap-x-3 gap-y-5">
|
|
73
|
+
{items.map((item) => {
|
|
74
|
+
const view = resolveHomeItemView(item);
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
key={item.id}
|
|
78
|
+
type="button"
|
|
79
|
+
onClick={() => openTarget(item.target)}
|
|
80
|
+
className="group flex min-h-[82px] flex-col items-center gap-2 rounded-lg px-2 py-2 text-center transition-colors hover:bg-white/80"
|
|
81
|
+
>
|
|
82
|
+
<span className={cn('flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl border shadow-[0_1px_2px_rgba(30,20,10,0.04)] transition-transform group-hover:scale-[1.03]', view.accentClassName)}>
|
|
83
|
+
{view.icon}
|
|
84
|
+
</span>
|
|
85
|
+
<span className="min-w-0 max-w-full">
|
|
86
|
+
<span className="line-clamp-2 block text-xs font-medium leading-snug text-gray-800">{item.label}</span>
|
|
87
|
+
</span>
|
|
88
|
+
</button>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -1,37 +1,16 @@
|
|
|
1
1
|
import type { ReactNode, Ref } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
ArrowLeft,
|
|
4
|
-
ArrowRight,
|
|
5
|
-
BookOpen,
|
|
6
3
|
ExternalLink,
|
|
7
|
-
Maximize2,
|
|
8
|
-
PanelRightOpen,
|
|
9
4
|
Search,
|
|
10
|
-
X,
|
|
11
5
|
} from 'lucide-react';
|
|
12
6
|
import {
|
|
13
7
|
isDocsUrl,
|
|
14
8
|
type DocBrowserTab,
|
|
15
9
|
} from './doc-browser-context';
|
|
16
|
-
import type { DocBrowserCustomTabRenderer } from './doc-browser-renderer.types';
|
|
17
|
-
import { cn } from '@/shared/lib/utils';
|
|
18
10
|
import { t } from '@/shared/lib/i18n';
|
|
19
11
|
|
|
20
|
-
type DocBrowserPanelHeaderProps = {
|
|
21
|
-
currentTab?: DocBrowserTab;
|
|
22
|
-
customRenderer?: DocBrowserCustomTabRenderer;
|
|
23
|
-
isDocked: boolean;
|
|
24
|
-
isFullscreen: boolean;
|
|
25
|
-
onClose: () => void;
|
|
26
|
-
onDragStart: (e: React.PointerEvent<HTMLElement>) => void;
|
|
27
|
-
onToggleMode: () => void;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
12
|
type DocBrowserDocsToolbarProps = {
|
|
31
|
-
currentTab?: DocBrowserTab;
|
|
32
13
|
isDocsTab: boolean;
|
|
33
|
-
onBack: () => void;
|
|
34
|
-
onForward: () => void;
|
|
35
14
|
onSubmit: (e: React.FormEvent) => void;
|
|
36
15
|
onUrlInputChange: (value: string) => void;
|
|
37
16
|
urlInput: string;
|
|
@@ -50,62 +29,8 @@ type DocBrowserFrameContentProps = {
|
|
|
50
29
|
navVersion: number;
|
|
51
30
|
};
|
|
52
31
|
|
|
53
|
-
export function DocBrowserPanelHeader({
|
|
54
|
-
currentTab,
|
|
55
|
-
customRenderer,
|
|
56
|
-
isDocked,
|
|
57
|
-
isFullscreen,
|
|
58
|
-
onClose,
|
|
59
|
-
onDragStart,
|
|
60
|
-
onToggleMode,
|
|
61
|
-
}: DocBrowserPanelHeaderProps) {
|
|
62
|
-
const title = currentTab && customRenderer?.getTitle
|
|
63
|
-
? customRenderer.getTitle(currentTab)
|
|
64
|
-
: t('docBrowserTitle');
|
|
65
|
-
const icon = currentTab && customRenderer?.renderIcon
|
|
66
|
-
? customRenderer.renderIcon(currentTab)
|
|
67
|
-
: <BookOpen className="w-4 h-4 text-primary shrink-0" />;
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div
|
|
71
|
-
className={cn(
|
|
72
|
-
'flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b border-gray-200 shrink-0 select-none',
|
|
73
|
-
!isDocked && !isFullscreen && 'cursor-grab active:cursor-grabbing',
|
|
74
|
-
isFullscreen && 'pt-[calc(env(safe-area-inset-top,0px)+0.625rem)]',
|
|
75
|
-
)}
|
|
76
|
-
onPointerDown={!isDocked && !isFullscreen ? onDragStart : undefined}
|
|
77
|
-
>
|
|
78
|
-
<div className="flex items-center gap-2.5 min-w-0">
|
|
79
|
-
{icon}
|
|
80
|
-
<span className="text-sm font-semibold text-gray-900 truncate">{title}</span>
|
|
81
|
-
</div>
|
|
82
|
-
<div className="flex items-center gap-1">
|
|
83
|
-
{!isFullscreen ? (
|
|
84
|
-
<button
|
|
85
|
-
onClick={onToggleMode}
|
|
86
|
-
className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
|
|
87
|
-
title={isDocked ? t('docBrowserFloatMode') : t('docBrowserDockMode')}
|
|
88
|
-
>
|
|
89
|
-
{isDocked ? <Maximize2 className="w-3.5 h-3.5" /> : <PanelRightOpen className="w-3.5 h-3.5" />}
|
|
90
|
-
</button>
|
|
91
|
-
) : null}
|
|
92
|
-
<button
|
|
93
|
-
onClick={onClose}
|
|
94
|
-
className="hover:bg-gray-200 rounded-md p-1.5 text-gray-500 hover:text-gray-700 transition-colors"
|
|
95
|
-
title={t('docBrowserClose')}
|
|
96
|
-
>
|
|
97
|
-
<X className="w-3.5 h-3.5" />
|
|
98
|
-
</button>
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
32
|
export function DocBrowserDocsToolbar({
|
|
105
|
-
currentTab,
|
|
106
33
|
isDocsTab,
|
|
107
|
-
onBack,
|
|
108
|
-
onForward,
|
|
109
34
|
onSubmit,
|
|
110
35
|
onUrlInputChange,
|
|
111
36
|
urlInput,
|
|
@@ -116,21 +41,6 @@ export function DocBrowserDocsToolbar({
|
|
|
116
41
|
|
|
117
42
|
return (
|
|
118
43
|
<div className="flex items-center gap-2 px-3.5 py-2 bg-white border-b border-gray-100 shrink-0">
|
|
119
|
-
<button
|
|
120
|
-
onClick={onBack}
|
|
121
|
-
disabled={!isDocsTab || (currentTab?.historyIndex ?? 0) <= 0}
|
|
122
|
-
className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
|
|
123
|
-
>
|
|
124
|
-
<ArrowLeft className="w-4 h-4" />
|
|
125
|
-
</button>
|
|
126
|
-
<button
|
|
127
|
-
onClick={onForward}
|
|
128
|
-
disabled={!isDocsTab || (currentTab?.historyIndex ?? 0) >= (currentTab?.history.length ?? 0) - 1}
|
|
129
|
-
className="p-1.5 rounded-md hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed text-gray-600 transition-colors"
|
|
130
|
-
>
|
|
131
|
-
<ArrowRight className="w-4 h-4" />
|
|
132
|
-
</button>
|
|
133
|
-
|
|
134
44
|
<form onSubmit={onSubmit} className="flex-1 relative">
|
|
135
45
|
<Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
|
136
46
|
<input
|