@nextclaw/ui 0.13.3 → 0.13.5
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 +128 -0
- package/dist/assets/api-BAyyGaoz.js +15 -0
- package/dist/assets/app-presenter-provider-DqHHfC1K.js +3 -0
- package/dist/assets/arrow-left-Bn-3Pj3p.js +1 -0
- package/dist/assets/{boxes-BvA-I63l.js → boxes-ClVuotcz.js} +1 -1
- package/dist/assets/channels-list-page-BnGxv6ma.js +8 -0
- package/dist/assets/chat-page-DInyWLb4.js +105 -0
- package/dist/assets/config-split-page-CWN2YG9U.js +1 -0
- package/dist/assets/confirm-dialog-CVbgKuHp.js +5 -0
- package/dist/assets/{createLucideIcon-Br1EFMEr.js → createLucideIcon-CjPUPAAK.js} +1 -1
- package/dist/assets/desktop-update-config-abmYzHSy.js +1 -0
- package/dist/assets/{dist-rzakR_bu.js → dist-hkrb6Ynt.js} +1 -1
- package/dist/assets/doc-browser-BlrpUKVJ.js +1 -0
- package/dist/assets/doc-browser-context-Cb1sEUT0.js +1 -0
- package/dist/assets/doc-browser-kJ2ClG0H.js +1 -0
- package/dist/assets/ellipsis-CdkxFkVZ.js +1 -0
- package/dist/assets/{external-link-DHjFdCpy.js → external-link-CT9_qFwN.js} +1 -1
- package/dist/assets/index-BGXTR0F_.css +1 -0
- package/dist/assets/index-Ccbswzt9.js +101 -0
- package/dist/assets/{key-round-D41Oe35x.js → key-round-xJfR4iRe.js} +1 -1
- package/dist/assets/{book-open-CRotBOF2.js → loader-circle-BI6HJDg5.js} +1 -1
- package/dist/assets/{mcp-marketplace-page-Dvi8mCBd.js → mcp-marketplace-page-CmUKUeno.js} +2 -2
- package/dist/assets/mcp-marketplace-page-DH11bSVx.js +1 -0
- package/dist/assets/model-config-Dbo3zWDl.js +1 -0
- package/dist/assets/notice-card-CFTFtJky.js +1 -0
- package/dist/assets/play-DyBWW2j4.js +1 -0
- package/dist/assets/plus-BlnBmcKE.js +1 -0
- package/dist/assets/popover-DPqa_Mfb.js +1 -0
- package/dist/assets/provider-models-CXUgDxGR.js +1 -0
- package/dist/assets/provider-scoped-model-input-C6xL7zSJ.js +1 -0
- package/dist/assets/providers-list-DOj_7GL8.js +1 -0
- package/dist/assets/react-oAHNoVya.js +1 -0
- package/dist/assets/refresh-cw-o8NSmIYV.js +1 -0
- package/dist/assets/remote-fCE5fLsj.js +1 -0
- package/dist/assets/rotate-cw-DA8EqNAH.js +1 -0
- package/dist/assets/runtime-config-page-Dg8aEgWx.js +1 -0
- package/dist/assets/{save-aYdSdK97.js → save-D4-g0EAq.js} +1 -1
- package/dist/assets/search-G2cMn3M5.js +1 -0
- package/dist/assets/{search-config-G2O1fcRb.js → search-config-DAibVFGR.js} +1 -1
- package/dist/assets/{secrets-config-TenzxYbE.js → secrets-config-CoJzy13_.js} +2 -2
- package/dist/assets/{status-dot-BSMG7ZaC.js → status-dot-D8xx5ZR8.js} +1 -1
- package/dist/assets/{tabs-D1CjpWNX.js → tabs-DxmND60Q.js} +1 -1
- package/dist/assets/{tabs-custom-CRSN1ekg.js → tabs-custom-Cjo0bHw8.js} +1 -1
- package/dist/assets/{tag-chip-DEashiw6.js → tag-chip-DnNWFRMu.js} +1 -1
- package/dist/assets/{trash-2-DSWTsARW.js → trash-2-C9tc-9sQ.js} +1 -1
- package/dist/assets/x-DmLl2yG6.js +1 -0
- package/dist/index.html +24 -23
- package/dist/logos/mimo.svg +9 -0
- package/package.json +8 -8
- package/public/logos/mimo.svg +9 -0
- package/src/app/components/layout/app-layout.tsx +10 -2
- package/src/features/agents/components/agents-page.tsx +6 -4
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +17 -10
- package/src/features/marketplace/components/marketplace-list-card.tsx +22 -1
- package/src/features/marketplace/components/marketplace-page.tsx +3 -1
- package/src/features/marketplace/hooks/use-marketplace.ts +3 -1
- package/src/features/marketplace/utils/marketplace-installed-cache.utils.ts +8 -0
- package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +70 -6
- package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts +25 -1
- package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts +1 -1
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.test.ts +42 -0
- package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +22 -0
- package/src/features/service-apps/components/service-apps-panel.test.tsx +88 -0
- package/src/features/service-apps/components/service-apps-panel.tsx +246 -56
- package/src/features/service-apps/hooks/use-service-apps.ts +14 -0
- package/src/features/side-dock/components/side-dock.test.tsx +58 -3
- package/src/features/side-dock/components/side-dock.tsx +77 -24
- package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +37 -34
- package/src/features/side-dock/index.ts +2 -0
- package/src/features/side-dock/managers/side-dock.manager.test.ts +84 -3
- package/src/features/side-dock/managers/side-dock.manager.ts +135 -12
- package/src/features/side-dock/types/side-dock.types.ts +11 -4
- package/src/features/side-dock/utils/side-dock-item.utils.test.ts +8 -8
- package/src/features/side-dock/utils/side-dock-item.utils.ts +13 -5
- package/src/platforms/desktop/components/desktop-app-shell.tsx +5 -2
- package/src/platforms/mobile/components/mobile-app-shell.tsx +4 -1
- package/src/shared/components/config/provider-form-support.test.ts +21 -0
- package/src/shared/components/config/provider-form-support.ts +38 -78
- package/src/shared/components/config/provider-form.tsx +54 -67
- package/src/shared/components/config/providers-list.tsx +211 -73
- package/src/shared/components/doc-browser/doc-browser-context.test.tsx +73 -0
- package/src/shared/components/doc-browser/doc-browser-context.tsx +3 -0
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +25 -2
- package/src/shared/components/doc-browser/doc-browser.test.tsx +42 -0
- package/src/shared/components/doc-browser/doc-browser.tsx +38 -7
- package/src/shared/components/doc-browser/index.ts +1 -0
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +30 -6
- package/src/shared/components/doc-browser/stores/doc-browser.store.ts +29 -0
- package/src/shared/components/doc-browser/types/doc-browser.types.ts +23 -0
- package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +7 -0
- package/src/shared/components/model-config.tsx +5 -4
- package/src/shared/components/provider-auth-section.tsx +2 -2
- package/src/shared/components/ui/switch.tsx +5 -2
- package/src/shared/hooks/use-config.ts +27 -6
- package/src/shared/lib/api/marketplace.types.ts +193 -0
- package/src/shared/lib/api/types.ts +23 -200
- package/src/shared/lib/api/utils/config.utils.ts +17 -7
- package/src/shared/lib/i18n/index.ts +10 -12
- package/src/shared/lib/i18n/{marketplace.ts → marketplace-labels.utils.ts} +3 -0
- package/src/shared/lib/i18n/runtime/doc-browser-labels.utils.ts +46 -2
- package/src/shared/lib/provider-models/index.test.ts +93 -15
- package/src/shared/lib/provider-models/index.ts +35 -20
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +1 -1
- package/dist/assets/api-DruAcori.js +0 -15
- package/dist/assets/app-presenter-provider-DIcnNB9s.js +0 -3
- package/dist/assets/channels-list-page-wK3l7p5R.js +0 -8
- package/dist/assets/chat-page-DgehvdJT.js +0 -105
- package/dist/assets/config-split-page-BnTO1mAM.js +0 -1
- package/dist/assets/confirm-dialog-CGrKHbAW.js +0 -5
- package/dist/assets/desktop-update-config-CehWxSSg.js +0 -1
- package/dist/assets/dist-CpwDZDtv.js +0 -1
- package/dist/assets/doc-browser-BgxmVsTx.js +0 -1
- package/dist/assets/doc-browser-CZn5-Qbu.js +0 -1
- package/dist/assets/doc-browser-context-B8gk7HMr.js +0 -1
- package/dist/assets/index-DORmh0Cy.css +0 -1
- package/dist/assets/index-DcFpvnge.js +0 -101
- package/dist/assets/mcp-marketplace-page-DBLhJWOJ.js +0 -1
- package/dist/assets/model-config-B-EJXGtO.js +0 -1
- package/dist/assets/play-C-0vo7hi.js +0 -1
- package/dist/assets/plus-Cr_akLp2.js +0 -1
- package/dist/assets/popover-YieElO8c.js +0 -1
- package/dist/assets/provider-scoped-model-input-BPRNGqYn.js +0 -1
- package/dist/assets/providers-list-CtBpL1ZY.js +0 -1
- package/dist/assets/react-B2X_ph0a.js +0 -1
- package/dist/assets/remote-BGHbHoHf.js +0 -1
- package/dist/assets/rotate-cw-jueVCNJQ.js +0 -1
- package/dist/assets/runtime-config-page-C1Hv7mad.js +0 -1
- package/dist/assets/search-CdhgFNwf.js +0 -1
- package/dist/assets/setting-row-BNu0qhOM.js +0 -1
- package/dist/assets/x-D1v_vb54.js +0 -1
- package/src/shared/components/provider-enabled-field.tsx +0 -23
- /package/dist/assets/{config-hints-A7G_jMTU.js → config-hints-TmNgpFHv.js} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { AppWindow, BookOpen, Boxes, Plus } from 'lucide-react';
|
|
1
|
+
import { AppWindow, BookOpen, Boxes, Plus, X } from 'lucide-react';
|
|
2
2
|
import type { LucideIcon } from 'lucide-react';
|
|
3
3
|
import type { SideDockManager } from '@/features/side-dock/managers/side-dock.manager';
|
|
4
|
-
import {
|
|
4
|
+
import { getSideDockBuiltInItems } from '@/features/side-dock/configs/side-dock-built-in-items.config';
|
|
5
5
|
import { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
|
|
6
6
|
import type {
|
|
7
7
|
SideDockIconName,
|
|
@@ -30,6 +30,7 @@ const SIDE_DOCK_ICON_COMPONENTS: Record<SideDockIconName, LucideIcon> = {
|
|
|
30
30
|
apps: Boxes,
|
|
31
31
|
docs: BookOpen,
|
|
32
32
|
'new-tab': Plus,
|
|
33
|
+
'panel-app': AppWindow,
|
|
33
34
|
'service-apps': AppWindow,
|
|
34
35
|
};
|
|
35
36
|
|
|
@@ -37,6 +38,13 @@ function SideDockItemIconView({ icon }: { icon: SideDockItemIcon }) {
|
|
|
37
38
|
if (icon.type === 'url') {
|
|
38
39
|
return <img src={icon.url} alt="" className="h-5 w-5 rounded object-cover" />;
|
|
39
40
|
}
|
|
41
|
+
if (icon.type === 'text') {
|
|
42
|
+
return (
|
|
43
|
+
<span className="max-w-7 truncate text-center text-[13px] font-semibold leading-none" aria-hidden="true">
|
|
44
|
+
{icon.value}
|
|
45
|
+
</span>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
40
48
|
|
|
41
49
|
const Icon = SIDE_DOCK_ICON_COMPONENTS[icon.name];
|
|
42
50
|
return <Icon className="h-5 w-5" aria-hidden="true" />;
|
|
@@ -46,56 +54,100 @@ function SideDockButton({
|
|
|
46
54
|
active,
|
|
47
55
|
item,
|
|
48
56
|
onOpen,
|
|
57
|
+
onUnpin,
|
|
49
58
|
}: {
|
|
50
59
|
active: boolean;
|
|
51
60
|
item: SideDockItem;
|
|
52
61
|
onOpen: (item: SideDockItem) => void;
|
|
62
|
+
onUnpin: (item: SideDockItem) => void;
|
|
53
63
|
}) {
|
|
54
|
-
const label =
|
|
64
|
+
const { label } = item;
|
|
55
65
|
|
|
56
66
|
return (
|
|
57
67
|
<Tooltip>
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
<div className="group relative">
|
|
69
|
+
<TooltipTrigger asChild>
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
aria-label={label}
|
|
73
|
+
title={label}
|
|
74
|
+
data-side-dock-item-id={item.id}
|
|
75
|
+
onClick={() => onOpen(item)}
|
|
76
|
+
className={cn(
|
|
77
|
+
'flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors',
|
|
78
|
+
'hover:bg-primary/10 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
|
79
|
+
active
|
|
80
|
+
? 'bg-primary/10 text-primary'
|
|
81
|
+
: 'bg-transparent',
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<SideDockItemIconView icon={item.icon} />
|
|
85
|
+
</button>
|
|
86
|
+
</TooltipTrigger>
|
|
87
|
+
{item.removable ? (
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
aria-label={t('sideDockUnpinItem')}
|
|
91
|
+
title={t('sideDockUnpinItem')}
|
|
92
|
+
onClick={(event) => {
|
|
93
|
+
event.stopPropagation();
|
|
94
|
+
onUnpin(item);
|
|
95
|
+
}}
|
|
96
|
+
className="absolute -right-1 -top-1 hidden h-4 w-4 items-center justify-center rounded-full bg-background text-muted-foreground shadow-sm ring-1 ring-border/70 transition-colors hover:bg-muted hover:text-foreground group-hover:flex focus-visible:flex focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
97
|
+
>
|
|
98
|
+
<X className="h-3 w-3" aria-hidden="true" />
|
|
99
|
+
</button>
|
|
100
|
+
) : null}
|
|
101
|
+
</div>
|
|
76
102
|
<TooltipContent side="left">{label}</TooltipContent>
|
|
77
103
|
</Tooltip>
|
|
78
104
|
);
|
|
79
105
|
}
|
|
80
106
|
|
|
107
|
+
function normalizeSideDockUri(uri?: string): string {
|
|
108
|
+
return uri?.trim() ?? '';
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
function isSideDockItemActive(
|
|
82
112
|
item: SideDockItem,
|
|
83
113
|
isDocBrowserOpen: boolean,
|
|
84
114
|
currentTab?: DocBrowserTab,
|
|
115
|
+
hasExactActiveItem = false,
|
|
85
116
|
): boolean {
|
|
86
117
|
if (!isDocBrowserOpen || !currentTab) {
|
|
87
118
|
return false;
|
|
88
119
|
}
|
|
89
|
-
|
|
120
|
+
const itemUri = normalizeSideDockUri(item.target.uri);
|
|
121
|
+
const currentResourceUri = normalizeSideDockUri(currentTab.resourceUri);
|
|
122
|
+
if (
|
|
123
|
+
(currentResourceUri && currentResourceUri === itemUri)
|
|
124
|
+
|| normalizeSideDockUri(currentTab.currentUrl) === itemUri
|
|
125
|
+
) {
|
|
90
126
|
return true;
|
|
91
127
|
}
|
|
128
|
+
if (hasExactActiveItem) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
92
131
|
return item.id === 'docs' && currentTab.kind === 'docs';
|
|
93
132
|
}
|
|
94
133
|
|
|
134
|
+
function hasExactActiveSideDockItem(items: SideDockItem[], currentTab?: DocBrowserTab): boolean {
|
|
135
|
+
if (!currentTab) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const currentResourceUri = normalizeSideDockUri(currentTab.resourceUri);
|
|
139
|
+
const currentUrl = normalizeSideDockUri(currentTab.currentUrl);
|
|
140
|
+
return items.some((item) => {
|
|
141
|
+
const itemUri = normalizeSideDockUri(item.target.uri);
|
|
142
|
+
return itemUri === currentResourceUri || itemUri === currentUrl;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
95
146
|
export function SideDock({ manager }: SideDockProps) {
|
|
96
147
|
const pinnedItems = useSideDockStore((state) => state.pinnedItems);
|
|
97
148
|
const { currentTab, isOpen } = useDocBrowser();
|
|
98
|
-
const items = mergeSideDockItems(
|
|
149
|
+
const items = mergeSideDockItems(getSideDockBuiltInItems(), pinnedItems);
|
|
150
|
+
const hasExactActiveItem = hasExactActiveSideDockItem(items, currentTab);
|
|
99
151
|
|
|
100
152
|
return (
|
|
101
153
|
<TooltipProvider delayDuration={250}>
|
|
@@ -106,9 +158,10 @@ export function SideDock({ manager }: SideDockProps) {
|
|
|
106
158
|
{items.map((item) => (
|
|
107
159
|
<SideDockButton
|
|
108
160
|
key={item.id}
|
|
109
|
-
active={isSideDockItemActive(item, isOpen, currentTab)}
|
|
161
|
+
active={isSideDockItemActive(item, isOpen, currentTab, hasExactActiveItem)}
|
|
110
162
|
item={item}
|
|
111
163
|
onOpen={manager.openItem}
|
|
164
|
+
onUnpin={(dockItem) => manager.unpinItem(dockItem.id)}
|
|
112
165
|
/>
|
|
113
166
|
))}
|
|
114
167
|
</aside>
|
|
@@ -4,40 +4,43 @@ import {
|
|
|
4
4
|
RIGHT_PANEL_SERVICE_APPS_URL,
|
|
5
5
|
} from '@/features/right-panel-resources';
|
|
6
6
|
import type { SideDockItem } from '@/features/side-dock/types/side-dock.types';
|
|
7
|
+
import { t } from '@/shared/lib/i18n';
|
|
7
8
|
|
|
8
9
|
export const SIDE_DOCK_DOCS_URL = 'nextclaw://docs';
|
|
9
10
|
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
11
|
+
export function getSideDockBuiltInItems(): SideDockItem[] {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
builtIn: true,
|
|
15
|
+
icon: { type: 'builtin', name: 'apps' },
|
|
16
|
+
id: 'apps',
|
|
17
|
+
label: t('appsTitle'),
|
|
18
|
+
removable: false,
|
|
19
|
+
target: { type: 'right-panel-resource', uri: RIGHT_PANEL_APPS_URL },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
builtIn: true,
|
|
23
|
+
icon: { type: 'builtin', name: 'service-apps' },
|
|
24
|
+
id: 'service-apps',
|
|
25
|
+
label: t('serviceAppsTitle'),
|
|
26
|
+
removable: false,
|
|
27
|
+
target: { type: 'right-panel-resource', uri: RIGHT_PANEL_SERVICE_APPS_URL },
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
builtIn: true,
|
|
31
|
+
icon: { type: 'builtin', name: 'docs' },
|
|
32
|
+
id: 'docs',
|
|
33
|
+
label: t('docBrowserHelp'),
|
|
34
|
+
removable: false,
|
|
35
|
+
target: { type: 'right-panel-resource', uri: SIDE_DOCK_DOCS_URL },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
builtIn: true,
|
|
39
|
+
icon: { type: 'builtin', name: 'new-tab' },
|
|
40
|
+
id: 'new-tab',
|
|
41
|
+
label: t('docBrowserHomeTitle'),
|
|
42
|
+
removable: false,
|
|
43
|
+
target: { type: 'right-panel-resource', uri: RIGHT_PANEL_HOME_URL },
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { SideDock } from '@/features/side-dock/components/side-dock';
|
|
2
2
|
export { SideDockManager } from '@/features/side-dock/managers/side-dock.manager';
|
|
3
|
+
export { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
|
|
3
4
|
export type {
|
|
4
5
|
SideDockIconName,
|
|
5
6
|
SideDockItem,
|
|
@@ -7,4 +8,5 @@ export type {
|
|
|
7
8
|
SideDockItemId,
|
|
8
9
|
SideDockItemTarget,
|
|
9
10
|
SideDockPinnedItem,
|
|
11
|
+
SideDockResourceDockState,
|
|
10
12
|
} from '@/features/side-dock/types/side-dock.types';
|
|
@@ -8,7 +8,7 @@ const customItem: SideDockItem = {
|
|
|
8
8
|
builtIn: false,
|
|
9
9
|
icon: { type: 'builtin', name: 'docs' },
|
|
10
10
|
id: 'custom-docs',
|
|
11
|
-
|
|
11
|
+
label: 'Help Docs',
|
|
12
12
|
removable: true,
|
|
13
13
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs/custom' },
|
|
14
14
|
};
|
|
@@ -25,7 +25,10 @@ describe('SideDockManager', () => {
|
|
|
25
25
|
|
|
26
26
|
manager.openItem(customItem);
|
|
27
27
|
|
|
28
|
-
expect(open).toHaveBeenCalledWith('nextclaw://docs/custom'
|
|
28
|
+
expect(open).toHaveBeenCalledWith('nextclaw://docs/custom', {
|
|
29
|
+
dockIcon: { type: 'builtin', name: 'docs' },
|
|
30
|
+
title: 'Help Docs',
|
|
31
|
+
});
|
|
29
32
|
});
|
|
30
33
|
|
|
31
34
|
it('pins removable items and keeps built-in items immutable', () => {
|
|
@@ -39,12 +42,90 @@ describe('SideDockManager', () => {
|
|
|
39
42
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
40
43
|
icon: { type: 'builtin', name: 'docs' },
|
|
41
44
|
id: 'custom-docs',
|
|
42
|
-
|
|
45
|
+
label: 'Help Docs',
|
|
43
46
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs/custom' },
|
|
44
47
|
},
|
|
45
48
|
]);
|
|
46
49
|
});
|
|
47
50
|
|
|
51
|
+
it('pins and unpins the current doc browser tab by resource URI', () => {
|
|
52
|
+
const manager = new SideDockManager({ open: vi.fn() } as unknown as DocBrowserManager);
|
|
53
|
+
|
|
54
|
+
manager.pinTab({
|
|
55
|
+
currentUrl: 'https://docs.nextclaw.ai/guide/getting-started',
|
|
56
|
+
history: ['https://docs.nextclaw.ai/guide/getting-started'],
|
|
57
|
+
historyIndex: 0,
|
|
58
|
+
id: 'tab-1',
|
|
59
|
+
kind: 'docs',
|
|
60
|
+
navVersion: 0,
|
|
61
|
+
resourceUri: 'nextclaw://docs/guide/getting-started',
|
|
62
|
+
title: 'Getting Started',
|
|
63
|
+
}, '2026-06-02T00:00:00.000Z');
|
|
64
|
+
|
|
65
|
+
expect(useSideDockStore.getState().pinnedItems).toMatchObject([
|
|
66
|
+
{
|
|
67
|
+
createdAt: '2026-06-02T00:00:00.000Z',
|
|
68
|
+
label: 'Getting Started',
|
|
69
|
+
target: { type: 'right-panel-resource', uri: 'nextclaw://docs/guide/getting-started' },
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
manager.unpinTab({
|
|
74
|
+
currentUrl: 'https://docs.nextclaw.ai/guide/getting-started',
|
|
75
|
+
history: ['https://docs.nextclaw.ai/guide/getting-started'],
|
|
76
|
+
historyIndex: 0,
|
|
77
|
+
id: 'tab-1',
|
|
78
|
+
kind: 'docs',
|
|
79
|
+
navVersion: 0,
|
|
80
|
+
resourceUri: 'nextclaw://docs/guide/getting-started',
|
|
81
|
+
title: 'Getting Started',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(useSideDockStore.getState().pinnedItems).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('pins panel app tabs with their own dock icon metadata', () => {
|
|
88
|
+
const manager = new SideDockManager({ open: vi.fn() } as unknown as DocBrowserManager);
|
|
89
|
+
|
|
90
|
+
manager.pinTab({
|
|
91
|
+
currentUrl: '/api/panel-apps/demo/content',
|
|
92
|
+
dockIcon: { type: 'url', url: '/api/panel-apps/demo/assets/icon.png' },
|
|
93
|
+
history: ['/api/panel-apps/demo/content'],
|
|
94
|
+
historyIndex: 0,
|
|
95
|
+
id: 'tab-1',
|
|
96
|
+
kind: 'panel-app',
|
|
97
|
+
navVersion: 0,
|
|
98
|
+
resourceUri: 'nextclaw://panel-app/demo',
|
|
99
|
+
title: 'Demo App',
|
|
100
|
+
}, '2026-06-02T00:00:00.000Z');
|
|
101
|
+
|
|
102
|
+
expect(useSideDockStore.getState().pinnedItems).toMatchObject([
|
|
103
|
+
{
|
|
104
|
+
icon: { type: 'url', url: '/api/panel-apps/demo/assets/icon.png' },
|
|
105
|
+
label: 'Demo App',
|
|
106
|
+
target: { type: 'right-panel-resource', uri: 'nextclaw://panel-app/demo' },
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('pins panel app text icons instead of falling back to a generic icon', () => {
|
|
112
|
+
const manager = new SideDockManager({ open: vi.fn() } as unknown as DocBrowserManager);
|
|
113
|
+
|
|
114
|
+
manager.pinTab({
|
|
115
|
+
currentUrl: '/api/panel-apps/compass/content',
|
|
116
|
+
dockIcon: { type: 'text', value: 'C' },
|
|
117
|
+
history: ['/api/panel-apps/compass/content'],
|
|
118
|
+
historyIndex: 0,
|
|
119
|
+
id: 'tab-1',
|
|
120
|
+
kind: 'panel-app',
|
|
121
|
+
navVersion: 0,
|
|
122
|
+
resourceUri: 'nextclaw://panel-app/compass',
|
|
123
|
+
title: 'Compass',
|
|
124
|
+
}, '2026-06-02T00:00:00.000Z');
|
|
125
|
+
|
|
126
|
+
expect(useSideDockStore.getState().pinnedItems[0]?.icon).toEqual({ type: 'text', value: 'C' });
|
|
127
|
+
});
|
|
128
|
+
|
|
48
129
|
it('unpins and reorders pinned items', () => {
|
|
49
130
|
const manager = new SideDockManager({ open: vi.fn() } as unknown as DocBrowserManager);
|
|
50
131
|
|
|
@@ -1,43 +1,155 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getSideDockBuiltInItems } from '@/features/side-dock/configs/side-dock-built-in-items.config';
|
|
2
2
|
import { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
SideDockIconName,
|
|
5
|
+
SideDockItem,
|
|
6
|
+
SideDockPinnedItem,
|
|
7
|
+
SideDockResourceDockState,
|
|
8
|
+
} from '@/features/side-dock/types/side-dock.types';
|
|
4
9
|
import {
|
|
5
10
|
createPinnedSideDockItem,
|
|
6
11
|
mergeSideDockItems,
|
|
7
12
|
} from '@/features/side-dock/utils/side-dock-item.utils';
|
|
8
13
|
import type { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
|
|
14
|
+
import type {
|
|
15
|
+
DocBrowserDockIcon,
|
|
16
|
+
DocBrowserTab,
|
|
17
|
+
} from '@/shared/components/doc-browser/types/doc-browser.types';
|
|
18
|
+
|
|
19
|
+
const SIDE_DOCK_PINNED_ITEM_ID_PREFIX = 'pinned';
|
|
20
|
+
const SIDE_DOCK_ICON_NAMES = new Set<SideDockIconName>([
|
|
21
|
+
'apps',
|
|
22
|
+
'docs',
|
|
23
|
+
'new-tab',
|
|
24
|
+
'panel-app',
|
|
25
|
+
'service-apps',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
function normalizeSideDockResourceUri(uri: string): string {
|
|
29
|
+
return uri.trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createPinnedItemId(uri: string): string {
|
|
33
|
+
let hash = 0;
|
|
34
|
+
Array.from(normalizeSideDockResourceUri(uri)).forEach((char) => {
|
|
35
|
+
hash = ((hash * 31) + char.charCodeAt(0)) >>> 0;
|
|
36
|
+
});
|
|
37
|
+
const slug = normalizeSideDockResourceUri(uri)
|
|
38
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
39
|
+
.replace(/^-+|-+$/g, '')
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
return `${SIDE_DOCK_PINNED_ITEM_ID_PREFIX}-${slug || 'resource'}-${hash.toString(36)}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveSideDockIcon(icon?: DocBrowserDockIcon): SideDockItem['icon'] | null {
|
|
45
|
+
if (icon?.type === 'builtin' && SIDE_DOCK_ICON_NAMES.has(icon.name as SideDockIconName)) {
|
|
46
|
+
return { type: 'builtin', name: icon.name as SideDockIconName };
|
|
47
|
+
}
|
|
48
|
+
if (icon?.type === 'url' && icon.url.trim().length > 0) {
|
|
49
|
+
return { type: 'url', url: icon.url.trim() };
|
|
50
|
+
}
|
|
51
|
+
if (icon?.type === 'text' && icon.value.trim().length > 0) {
|
|
52
|
+
return { type: 'text', value: icon.value.trim() };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getFallbackIconNameForTab(tab: DocBrowserTab): SideDockIconName {
|
|
58
|
+
if (tab.kind === 'docs') {
|
|
59
|
+
return 'docs';
|
|
60
|
+
}
|
|
61
|
+
if (tab.kind === 'home') {
|
|
62
|
+
return 'new-tab';
|
|
63
|
+
}
|
|
64
|
+
if (tab.kind === 'apps') {
|
|
65
|
+
return tab.resourceUri?.includes('service-apps') ? 'service-apps' : 'apps';
|
|
66
|
+
}
|
|
67
|
+
if (tab.kind === 'panel-app') {
|
|
68
|
+
return 'panel-app';
|
|
69
|
+
}
|
|
70
|
+
return 'apps';
|
|
71
|
+
}
|
|
9
72
|
|
|
10
73
|
export class SideDockManager {
|
|
11
74
|
constructor(private readonly docBrowserManager: DocBrowserManager) {}
|
|
12
75
|
|
|
13
|
-
|
|
14
|
-
|
|
76
|
+
getItems = (): SideDockItem[] => mergeSideDockItems(
|
|
77
|
+
getSideDockBuiltInItems(),
|
|
15
78
|
useSideDockStore.getState().pinnedItems,
|
|
16
79
|
);
|
|
17
80
|
|
|
18
|
-
|
|
81
|
+
openItem = (item: Pick<SideDockItem, 'icon' | 'label' | 'target'>): void => {
|
|
19
82
|
if (item.target.type === 'right-panel-resource') {
|
|
20
|
-
this.docBrowserManager.open(item.target.uri
|
|
83
|
+
this.docBrowserManager.open(item.target.uri, {
|
|
84
|
+
dockIcon: item.icon,
|
|
85
|
+
title: item.label,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
getDockState = (tab?: DocBrowserTab): SideDockResourceDockState => {
|
|
91
|
+
const uri = this.getTabResourceUri(tab);
|
|
92
|
+
if (!uri) {
|
|
93
|
+
return {
|
|
94
|
+
canDock: false,
|
|
95
|
+
isDocked: false,
|
|
96
|
+
removable: false,
|
|
97
|
+
};
|
|
21
98
|
}
|
|
99
|
+
|
|
100
|
+
const item = this.findItemByUri(uri);
|
|
101
|
+
return {
|
|
102
|
+
canDock: true,
|
|
103
|
+
isDocked: !!item,
|
|
104
|
+
removable: item?.removable === true,
|
|
105
|
+
};
|
|
22
106
|
};
|
|
23
107
|
|
|
24
|
-
|
|
108
|
+
pinTab = (tab?: DocBrowserTab, createdAt: string = new Date().toISOString()): void => {
|
|
109
|
+
const uri = this.getTabResourceUri(tab);
|
|
110
|
+
if (!tab || !uri || this.findItemByUri(uri)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
this.pinItem({
|
|
114
|
+
builtIn: false,
|
|
115
|
+
icon: resolveSideDockIcon(tab.dockIcon) ?? { type: 'builtin', name: getFallbackIconNameForTab(tab) },
|
|
116
|
+
id: createPinnedItemId(uri),
|
|
117
|
+
label: tab.title.trim() || uri,
|
|
118
|
+
removable: true,
|
|
119
|
+
target: { type: 'right-panel-resource', uri },
|
|
120
|
+
}, createdAt);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
unpinTab = (tab?: DocBrowserTab): void => {
|
|
124
|
+
const uri = this.getTabResourceUri(tab);
|
|
125
|
+
const item = uri ? this.findItemByUri(uri) : undefined;
|
|
126
|
+
if (!item?.removable) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.unpinItem(item.id);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
pinItem = (item: SideDockItem, createdAt: string = new Date().toISOString()): void => {
|
|
25
133
|
if (!item.removable) {
|
|
26
134
|
return;
|
|
27
135
|
}
|
|
136
|
+
const uri = normalizeSideDockResourceUri(item.target.uri);
|
|
28
137
|
this.setPinnedItems((items) => {
|
|
29
|
-
if (items.some((pinnedItem) => pinnedItem.id === item.id || pinnedItem.target.uri ===
|
|
138
|
+
if (items.some((pinnedItem) => pinnedItem.id === item.id || normalizeSideDockResourceUri(pinnedItem.target.uri) === uri)) {
|
|
30
139
|
return items;
|
|
31
140
|
}
|
|
32
|
-
return [...items, createPinnedSideDockItem(
|
|
141
|
+
return [...items, createPinnedSideDockItem({
|
|
142
|
+
...item,
|
|
143
|
+
target: { ...item.target, uri },
|
|
144
|
+
}, createdAt)];
|
|
33
145
|
});
|
|
34
146
|
};
|
|
35
147
|
|
|
36
|
-
|
|
148
|
+
unpinItem = (itemId: string): void => {
|
|
37
149
|
this.setPinnedItems((items) => items.filter((item) => item.id !== itemId));
|
|
38
150
|
};
|
|
39
151
|
|
|
40
|
-
|
|
152
|
+
reorderPinnedItems = (itemIds: string[]): void => {
|
|
41
153
|
this.setPinnedItems((items) => {
|
|
42
154
|
const itemsById = new Map(items.map((item) => [item.id, item]));
|
|
43
155
|
const orderedItems = itemIds
|
|
@@ -49,10 +161,21 @@ export class SideDockManager {
|
|
|
49
161
|
});
|
|
50
162
|
};
|
|
51
163
|
|
|
52
|
-
private
|
|
164
|
+
private setPinnedItems = (
|
|
53
165
|
updater: (items: SideDockPinnedItem[]) => SideDockPinnedItem[],
|
|
54
166
|
): void => {
|
|
55
167
|
const { pinnedItems, setPinnedItems } = useSideDockStore.getState();
|
|
56
168
|
setPinnedItems(updater(pinnedItems));
|
|
57
169
|
};
|
|
170
|
+
|
|
171
|
+
private getTabResourceUri = (tab?: DocBrowserTab): string | null => {
|
|
172
|
+
const uri = tab?.resourceUri ?? tab?.currentUrl;
|
|
173
|
+
const normalized = uri ? normalizeSideDockResourceUri(uri) : '';
|
|
174
|
+
return normalized.length > 0 ? normalized : null;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
private findItemByUri = (uri: string): SideDockItem | undefined => {
|
|
178
|
+
const normalized = normalizeSideDockResourceUri(uri);
|
|
179
|
+
return this.getItems().find((item) => normalizeSideDockResourceUri(item.target.uri) === normalized);
|
|
180
|
+
};
|
|
58
181
|
}
|
|
@@ -5,17 +5,18 @@ export type SideDockItemTarget = {
|
|
|
5
5
|
uri: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export type SideDockIconName = 'apps' | 'docs' | 'new-tab' | 'service-apps';
|
|
8
|
+
export type SideDockIconName = 'apps' | 'docs' | 'new-tab' | 'panel-app' | 'service-apps';
|
|
9
9
|
|
|
10
10
|
export type SideDockItemIcon =
|
|
11
11
|
| { type: 'builtin'; name: SideDockIconName }
|
|
12
|
-
| { type: 'url'; url: string }
|
|
12
|
+
| { type: 'url'; url: string }
|
|
13
|
+
| { type: 'text'; value: string };
|
|
13
14
|
|
|
14
15
|
export type SideDockItem = {
|
|
15
16
|
builtIn: boolean;
|
|
16
17
|
icon: SideDockItemIcon;
|
|
17
18
|
id: SideDockItemId;
|
|
18
|
-
|
|
19
|
+
label: string;
|
|
19
20
|
removable: boolean;
|
|
20
21
|
target: SideDockItemTarget;
|
|
21
22
|
};
|
|
@@ -24,6 +25,12 @@ export type SideDockPinnedItem = {
|
|
|
24
25
|
createdAt: string;
|
|
25
26
|
icon: SideDockItemIcon;
|
|
26
27
|
id: SideDockItemId;
|
|
27
|
-
|
|
28
|
+
label: string;
|
|
28
29
|
target: SideDockItemTarget;
|
|
29
30
|
};
|
|
31
|
+
|
|
32
|
+
export type SideDockResourceDockState = {
|
|
33
|
+
canDock: boolean;
|
|
34
|
+
isDocked: boolean;
|
|
35
|
+
removable: boolean;
|
|
36
|
+
};
|
|
@@ -10,7 +10,7 @@ const builtInItem: SideDockItem = {
|
|
|
10
10
|
builtIn: true,
|
|
11
11
|
icon: { type: 'builtin', name: 'apps' },
|
|
12
12
|
id: 'apps',
|
|
13
|
-
|
|
13
|
+
label: 'Apps',
|
|
14
14
|
removable: false,
|
|
15
15
|
target: { type: 'right-panel-resource', uri: 'nextclaw://apps' },
|
|
16
16
|
};
|
|
@@ -20,16 +20,16 @@ describe('side-dock-item utils', () => {
|
|
|
20
20
|
const items = normalizeSideDockPinnedItems([
|
|
21
21
|
{
|
|
22
22
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
23
|
-
icon: { type: '
|
|
23
|
+
icon: { type: 'text', value: 'D' },
|
|
24
24
|
id: 'docs-custom',
|
|
25
|
-
|
|
25
|
+
label: 'Help Docs',
|
|
26
26
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs' },
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
30
30
|
icon: { type: 'builtin', name: 'unknown' },
|
|
31
31
|
id: 'bad-icon',
|
|
32
|
-
|
|
32
|
+
label: 'Bad',
|
|
33
33
|
target: { type: 'right-panel-resource', uri: 'nextclaw://bad' },
|
|
34
34
|
},
|
|
35
35
|
]);
|
|
@@ -37,9 +37,9 @@ describe('side-dock-item utils', () => {
|
|
|
37
37
|
expect(items).toEqual([
|
|
38
38
|
{
|
|
39
39
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
40
|
-
icon: { type: '
|
|
40
|
+
icon: { type: 'text', value: 'D' },
|
|
41
41
|
id: 'docs-custom',
|
|
42
|
-
|
|
42
|
+
label: 'Help Docs',
|
|
43
43
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs' },
|
|
44
44
|
},
|
|
45
45
|
]);
|
|
@@ -58,14 +58,14 @@ describe('side-dock-item utils', () => {
|
|
|
58
58
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
59
59
|
icon: { type: 'builtin', name: 'docs' },
|
|
60
60
|
id: 'docs-custom',
|
|
61
|
-
|
|
61
|
+
label: 'Help Docs',
|
|
62
62
|
target: { type: 'right-panel-resource', uri: 'nextclaw://docs' },
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
createdAt: '2026-06-02T00:00:00.000Z',
|
|
66
66
|
icon: { type: 'builtin', name: 'apps' },
|
|
67
67
|
id: 'apps-copy',
|
|
68
|
-
|
|
68
|
+
label: 'Apps',
|
|
69
69
|
target: { type: 'right-panel-resource', uri: 'nextclaw://apps' },
|
|
70
70
|
},
|
|
71
71
|
],
|