@nextclaw/ui 0.13.0 → 0.13.2

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 (96) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/dist/assets/app-presenter-provider-DIcnNB9s.js +3 -0
  3. package/dist/assets/{wrench-qVIwShTF.js → boxes-BvA-I63l.js} +1 -1
  4. package/dist/assets/channels-list-page-wK3l7p5R.js +8 -0
  5. package/dist/assets/chat-page-DgehvdJT.js +105 -0
  6. package/dist/assets/{confirm-dialog-B0TP-vPd.js → confirm-dialog-CGrKHbAW.js} +1 -1
  7. package/dist/assets/desktop-update-config-CehWxSSg.js +1 -0
  8. package/dist/assets/doc-browser-BgxmVsTx.js +1 -0
  9. package/dist/assets/doc-browser-CZn5-Qbu.js +1 -0
  10. package/dist/assets/doc-browser-context-B8gk7HMr.js +1 -0
  11. package/dist/assets/index-DORmh0Cy.css +1 -0
  12. package/dist/assets/index-DcFpvnge.js +101 -0
  13. package/dist/assets/key-round-D41Oe35x.js +1 -0
  14. package/dist/assets/mcp-marketplace-page-DBLhJWOJ.js +1 -0
  15. package/dist/assets/{mcp-marketplace-page-DDjeHd1I.js → mcp-marketplace-page-Dvi8mCBd.js} +2 -2
  16. package/dist/assets/model-config-B-EJXGtO.js +1 -0
  17. package/dist/assets/popover-YieElO8c.js +1 -0
  18. package/dist/assets/{provider-scoped-model-input-t1tCP5y5.js → provider-scoped-model-input-BPRNGqYn.js} +1 -1
  19. package/dist/assets/providers-list-CtBpL1ZY.js +1 -0
  20. package/dist/assets/remote-BGHbHoHf.js +1 -0
  21. package/dist/assets/rotate-cw-jueVCNJQ.js +1 -0
  22. package/dist/assets/runtime-config-page-C1Hv7mad.js +1 -0
  23. package/dist/assets/search-config-G2O1fcRb.js +1 -0
  24. package/dist/assets/{secrets-config-D1AUSEXX.js → secrets-config-TenzxYbE.js} +2 -2
  25. package/dist/assets/status-dot-BSMG7ZaC.js +1 -0
  26. package/dist/assets/tabs-D1CjpWNX.js +1 -0
  27. package/dist/assets/trash-2-DSWTsARW.js +1 -0
  28. package/dist/index.html +19 -13
  29. package/package.json +8 -8
  30. package/src/app/components/layout/app-layout.test.tsx +2 -1
  31. package/src/app/components/layout/app-layout.tsx +18 -4
  32. package/src/app/components/layout/sidebar.tsx +1 -1
  33. package/src/app/presenters/app.presenter.ts +6 -0
  34. package/src/features/chat/components/layout/chat-sidebar.test.tsx +1 -2
  35. package/src/features/chat/components/layout/chat-sidebar.tsx +1 -1
  36. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +17 -13
  37. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +96 -0
  38. package/src/features/right-panel-resources/configs/right-panel-resource-routes.config.ts +171 -0
  39. package/src/features/right-panel-resources/index.ts +18 -0
  40. package/src/features/right-panel-resources/types/right-panel-resource.types.ts +36 -0
  41. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.test.ts +38 -0
  42. package/src/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils.ts +89 -0
  43. package/src/features/right-panel-resources/utils/right-panel-resource-uri.utils.ts +48 -0
  44. package/src/features/side-dock/components/side-dock.test.tsx +58 -0
  45. package/src/features/side-dock/components/side-dock.tsx +117 -0
  46. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +43 -0
  47. package/src/features/side-dock/index.ts +10 -0
  48. package/src/features/side-dock/managers/side-dock.manager.test.ts +62 -0
  49. package/src/features/side-dock/managers/side-dock.manager.ts +58 -0
  50. package/src/features/side-dock/stores/side-dock.store.ts +42 -0
  51. package/src/features/side-dock/types/side-dock.types.ts +29 -0
  52. package/src/features/side-dock/utils/side-dock-item.utils.test.ts +77 -0
  53. package/src/features/side-dock/utils/side-dock-item.utils.ts +126 -0
  54. package/src/platforms/desktop/components/desktop-app-shell.tsx +3 -0
  55. package/src/shared/components/doc-browser/doc-browser-context.test.tsx +52 -5
  56. package/src/shared/components/doc-browser/doc-browser-context.tsx +22 -14
  57. package/src/shared/components/doc-browser/doc-browser-home-page.tsx +9 -89
  58. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -0
  59. package/src/shared/components/doc-browser/doc-browser.test.tsx +5 -3
  60. package/src/shared/components/doc-browser/doc-browser.tsx +3 -1
  61. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +92 -43
  62. package/src/shared/components/doc-browser/types/doc-browser.types.ts +24 -0
  63. package/src/shared/components/doc-browser/utils/doc-browser-route-resolver.utils.ts +65 -0
  64. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +5 -4
  65. package/src/shared/lib/resource-uri/index.ts +11 -0
  66. package/src/shared/lib/resource-uri/types/resource-uri.types.ts +19 -0
  67. package/src/shared/lib/resource-uri/utils/resource-uri-resolver.utils.test.ts +49 -0
  68. package/src/shared/lib/resource-uri/utils/resource-uri-resolver.utils.ts +35 -0
  69. package/src/shared/lib/resource-uri/utils/resource-uri.utils.ts +48 -0
  70. package/dist/assets/channels-list-page-DXH1qYLs.js +0 -8
  71. package/dist/assets/chat-page-Cpo0AFHb.js +0 -105
  72. package/dist/assets/desktop-update-config-Be8x1jNy.js +0 -1
  73. package/dist/assets/doc-browser-BI7HSaMK.js +0 -3
  74. package/dist/assets/doc-browser-context-DB6TTHeE.js +0 -1
  75. package/dist/assets/doc-browser-route-registry.utils-x5ijvk0u.js +0 -1
  76. package/dist/assets/doc-browser-yTfN7UBH.js +0 -1
  77. package/dist/assets/index-GVTiCf1s.js +0 -101
  78. package/dist/assets/index-v0fAGXQW.css +0 -1
  79. package/dist/assets/mcp-marketplace-page-WUkc7n94.js +0 -1
  80. package/dist/assets/model-config-BjXMpU9U.js +0 -1
  81. package/dist/assets/providers-list-C1D5p9kK.js +0 -1
  82. package/dist/assets/remote-BHeENKxL.js +0 -1
  83. package/dist/assets/runtime-config-page--O0Wq01S.js +0 -1
  84. package/dist/assets/search-config-Dzpizo-_.js +0 -1
  85. package/src/shared/components/doc-browser/utils/doc-browser-route-registry.utils.ts +0 -232
  86. /package/dist/assets/{book-open-cPX0Za7Z.js → book-open-CRotBOF2.js} +0 -0
  87. /package/dist/assets/{config-hints-CY1lg9IE.js → config-hints-A7G_jMTU.js} +0 -0
  88. /package/dist/assets/{config-split-page-BFh_Qixx.js → config-split-page-BnTO1mAM.js} +0 -0
  89. /package/dist/assets/{external-link-C3usm3se.js → external-link-DHjFdCpy.js} +0 -0
  90. /package/dist/assets/{play-CqNi0u02.js → play-C-0vo7hi.js} +0 -0
  91. /package/dist/assets/{plus-BcIC8dG1.js → plus-Cr_akLp2.js} +0 -0
  92. /package/dist/assets/{save-D10M5vQg.js → save-aYdSdK97.js} +0 -0
  93. /package/dist/assets/{setting-row-8VkukJoH.js → setting-row-BNu0qhOM.js} +0 -0
  94. /package/dist/assets/{tabs-custom-BBXu1OGs.js → tabs-custom-CRSN1ekg.js} +0 -0
  95. /package/dist/assets/{tag-chip-ix15P-r4.js → tag-chip-DEashiw6.js} +0 -0
  96. /package/dist/assets/{x-Chb07O-Y.js → x-D1v_vb54.js} +0 -0
@@ -5,14 +5,18 @@ import { getPresenter } from '@/app/presenters/app.presenter';
5
5
  import { AppsPanel, type AppsPanelTab } from '@/features/apps';
6
6
  import { PanelAppToolbar } from '@/features/panel-apps/components/panel-app-toolbar';
7
7
  import {
8
- DOC_BROWSER_APPS_TAB_KIND,
9
- DOC_BROWSER_APPS_URL,
10
- DOC_BROWSER_SERVICE_APPS_URL,
11
- } from '@/shared/components/doc-browser/utils/doc-browser-route-registry.utils';
8
+ createPanelAppRightPanelResourceTarget,
9
+ RIGHT_PANEL_APPS_TAB_KIND,
10
+ RIGHT_PANEL_APPS_URL,
11
+ RIGHT_PANEL_HOME_TAB_KIND,
12
+ RIGHT_PANEL_PANEL_APP_TAB_KIND,
13
+ RIGHT_PANEL_SERVICE_APPS_URL,
14
+ RightPanelResourceHomePage,
15
+ } from '@/features/right-panel-resources';
12
16
  import { t } from '@/shared/lib/i18n';
13
17
 
14
- export const APPS_TAB_KIND = DOC_BROWSER_APPS_TAB_KIND;
15
- export const PANEL_APP_TAB_KIND = 'panel-app';
18
+ export const APPS_TAB_KIND = RIGHT_PANEL_APPS_TAB_KIND;
19
+ export const PANEL_APP_TAB_KIND = RIGHT_PANEL_PANEL_APP_TAB_KIND;
16
20
  const DEFAULT_APPS_PANEL_TAB: AppsPanelTab = 'panel-apps';
17
21
  const PANEL_APP_IFRAME_SANDBOX = [
18
22
  'allow-scripts',
@@ -30,7 +34,7 @@ function isAppsPanelTab(value: unknown): value is AppsPanelTab {
30
34
  }
31
35
 
32
36
  export function createAppsPanelUrl(tab: AppsPanelTab = DEFAULT_APPS_PANEL_TAB): string {
33
- return tab === DEFAULT_APPS_PANEL_TAB ? DOC_BROWSER_APPS_URL : DOC_BROWSER_SERVICE_APPS_URL;
37
+ return tab === DEFAULT_APPS_PANEL_TAB ? RIGHT_PANEL_APPS_URL : RIGHT_PANEL_SERVICE_APPS_URL;
34
38
  }
35
39
 
36
40
  export function getAppsPanelTabFromUrl(url: string): AppsPanelTab {
@@ -51,10 +55,14 @@ export function openApps(docBrowser: Pick<DocBrowserContextValue, 'open'>): void
51
55
  }
52
56
 
53
57
  export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
58
+ [RIGHT_PANEL_HOME_TAB_KIND]: {
59
+ getTitle: () => t('docBrowserHomeTitle'),
60
+ renderContent: ({ open }) => <RightPanelResourceHomePage open={open} />,
61
+ },
54
62
  [APPS_TAB_KIND]: {
55
63
  getTitle: () => t('appsTitle'),
56
64
  renderIcon: () => <Boxes className="w-4 h-4 text-primary shrink-0" />,
57
- renderContent: ({ currentUrl, open }) => (
65
+ renderContent: ({ currentUrl, open, openTarget }) => (
58
66
  <AppsPanel
59
67
  activeTab={getAppsPanelTabFromUrl(currentUrl)}
60
68
  onActiveTabChange={(tab) => open(createAppsPanelUrl(tab), {
@@ -63,11 +71,7 @@ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
63
71
  title: t('appsTitle'),
64
72
  dedupeKey: 'apps',
65
73
  })}
66
- onOpenPanelApp={(entry) => open(entry.contentPath, {
67
- kind: PANEL_APP_TAB_KIND,
68
- title: entry.title,
69
- dedupeKey: `panel-app:${entry.id}`,
70
- })}
74
+ onOpenPanelApp={(entry) => openTarget(createPanelAppRightPanelResourceTarget(entry))}
71
75
  />
72
76
  ),
73
77
  },
@@ -0,0 +1,96 @@
1
+ import type { ReactNode } from 'react';
2
+ import {
3
+ BookOpen,
4
+ Boxes,
5
+ BrainCircuit,
6
+ Grid3X3,
7
+ } from 'lucide-react';
8
+ import { useNavigate } from 'react-router-dom';
9
+ import type { DocBrowserContextValue } from '@/shared/components/doc-browser/doc-browser-context';
10
+ import { cn } from '@/shared/lib/utils';
11
+ import {
12
+ getRightPanelResourceHomeNavigationItems,
13
+ } from '@/features/right-panel-resources/configs/right-panel-resource-routes.config';
14
+ import type {
15
+ RightPanelResourceHomeNavigationItem,
16
+ RightPanelResourceNavigationTarget,
17
+ } from '@/features/right-panel-resources/types/right-panel-resource.types';
18
+
19
+ type RightPanelResourceHomePageProps = {
20
+ open: DocBrowserContextValue['open'];
21
+ };
22
+
23
+ type RightPanelResourceHomeItem = {
24
+ accentClassName: string;
25
+ hoverAccentClassName: string;
26
+ icon: ReactNode;
27
+ };
28
+
29
+ const HOME_ITEM_VIEW_BY_ID: Record<string, RightPanelResourceHomeItem> = {
30
+ apps: {
31
+ accentClassName: 'bg-emerald-50 text-emerald-700',
32
+ hoverAccentClassName: 'group-hover:bg-emerald-100',
33
+ icon: <Boxes className="h-5 w-5" />,
34
+ },
35
+ 'service-apps': {
36
+ accentClassName: 'bg-sky-50 text-sky-700',
37
+ hoverAccentClassName: 'group-hover:bg-sky-100',
38
+ icon: <Grid3X3 className="h-5 w-5" />,
39
+ },
40
+ docs: {
41
+ accentClassName: 'bg-amber-50 text-amber-700',
42
+ hoverAccentClassName: 'group-hover:bg-amber-100',
43
+ icon: <BookOpen className="h-5 w-5" />,
44
+ },
45
+ 'skill-marketplace': {
46
+ accentClassName: 'bg-rose-50 text-rose-700',
47
+ hoverAccentClassName: 'group-hover:bg-rose-100',
48
+ icon: <BrainCircuit className="h-5 w-5" />,
49
+ },
50
+ };
51
+
52
+ function resolveHomeItemView(item: RightPanelResourceHomeNavigationItem): RightPanelResourceHomeItem {
53
+ return HOME_ITEM_VIEW_BY_ID[item.id] ?? {
54
+ accentClassName: 'bg-gray-50 text-gray-700',
55
+ hoverAccentClassName: 'group-hover:bg-gray-100',
56
+ icon: <Boxes className="h-5 w-5" />,
57
+ };
58
+ }
59
+
60
+ export function RightPanelResourceHomePage({ open }: RightPanelResourceHomePageProps) {
61
+ const navigate = useNavigate();
62
+ const items = getRightPanelResourceHomeNavigationItems();
63
+
64
+ const openTarget = (target: RightPanelResourceNavigationTarget) => {
65
+ if (target.type === 'app-route') {
66
+ navigate(target.path);
67
+ return;
68
+ }
69
+ open(target.uri, target.options);
70
+ };
71
+
72
+ return (
73
+ <div className="h-full overflow-auto bg-background px-4 py-5">
74
+ <div className="mx-auto w-full max-w-[520px]">
75
+ <div className="grid grid-cols-[repeat(auto-fit,minmax(76px,1fr))] gap-x-2 gap-y-4">
76
+ {items.map((item) => {
77
+ const view = resolveHomeItemView(item);
78
+ return (
79
+ <button
80
+ key={item.id}
81
+ type="button"
82
+ onClick={() => openTarget(item.target)}
83
+ className="group flex min-h-[68px] flex-col items-center gap-1.5 rounded-md px-1.5 py-1.5 text-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
84
+ >
85
+ <span className={cn('flex h-10 w-10 shrink-0 items-center justify-center rounded-md transition-colors', view.accentClassName, view.hoverAccentClassName)}>
86
+ {view.icon}
87
+ </span>
88
+ <span className="line-clamp-2 min-w-0 max-w-full text-xs font-medium leading-snug text-gray-700 transition-colors group-hover:text-gray-900">{item.label}</span>
89
+ </button>
90
+ );
91
+ })}
92
+ </div>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,171 @@
1
+ import { t } from '@/shared/lib/i18n';
2
+ import type { ParsedResourceUri, ResourceUriRouteDefinition } from '@/shared/lib/resource-uri';
3
+ import {
4
+ getDefaultDocsUrl,
5
+ inferTabTitle,
6
+ isDocsUrl,
7
+ normalizeDocUrl,
8
+ normalizeUrlByKind,
9
+ } from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
10
+ import type {
11
+ RightPanelResourceHomeNavigationItem,
12
+ RightPanelResourceRouteDefinition,
13
+ RightPanelResourceTarget,
14
+ } from '@/features/right-panel-resources/types/right-panel-resource.types';
15
+ import {
16
+ RIGHT_PANEL_APPS_URL,
17
+ RIGHT_PANEL_APPS_TAB_KIND,
18
+ RIGHT_PANEL_HOME_TAB_KIND,
19
+ RIGHT_PANEL_HOME_URL,
20
+ RIGHT_PANEL_PANEL_APP_TAB_KIND,
21
+ RIGHT_PANEL_SERVICE_APPS_URL,
22
+ normalizeRightPanelAppsUrl,
23
+ } from '@/features/right-panel-resources/utils/right-panel-resource-uri.utils';
24
+
25
+ function isUrlWithPrefix(url: string, prefix: string): boolean {
26
+ return url === prefix || url.startsWith(`${prefix}?`) || url.startsWith(`${prefix}/`);
27
+ }
28
+
29
+ function resolveNextclawDocsUrl(uri: ParsedResourceUri): string {
30
+ const docsPath = uri.pathSegments.join('/');
31
+ const rawPath = docsPath ? `/${docsPath}` : '/';
32
+ return normalizeUrlByKind(rawPath, 'docs');
33
+ }
34
+
35
+ function resolvePanelAppPlaceholderUrl(uri: ParsedResourceUri): string {
36
+ const appId = uri.pathSegments[0] ?? '';
37
+ return appId ? `nextclaw://panel-app/${encodeURIComponent(appId)}` : 'nextclaw://panel-app';
38
+ }
39
+
40
+ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS: RightPanelResourceRouteDefinition[] = [
41
+ {
42
+ defaultUrl: () => RIGHT_PANEL_HOME_URL,
43
+ id: 'home',
44
+ kind: RIGHT_PANEL_HOME_TAB_KIND,
45
+ match: (uri) => uri.raw === RIGHT_PANEL_HOME_URL,
46
+ resolve: () => ({
47
+ historyPolicy: 'managed',
48
+ kind: RIGHT_PANEL_HOME_TAB_KIND,
49
+ title: t('docBrowserHomeTitle'),
50
+ url: RIGHT_PANEL_HOME_URL,
51
+ }),
52
+ areEquivalent: (left, right) => left === right,
53
+ },
54
+ {
55
+ defaultUrl: getDefaultDocsUrl,
56
+ id: 'docs-url',
57
+ kind: 'docs',
58
+ match: (uri) => isDocsUrl(uri.raw),
59
+ resolve: (uri) => {
60
+ const url = normalizeUrlByKind(uri.raw, 'docs');
61
+ return {
62
+ historyPolicy: 'managed',
63
+ kind: 'docs',
64
+ title: t('docBrowserHelp'),
65
+ url,
66
+ };
67
+ },
68
+ areEquivalent: (left, right) => normalizeDocUrl(left) === normalizeDocUrl(right),
69
+ },
70
+ {
71
+ defaultUrl: getDefaultDocsUrl,
72
+ id: 'nextclaw-docs',
73
+ kind: 'docs',
74
+ match: (uri) => uri.scheme === 'nextclaw' && uri.authority === 'docs',
75
+ resolve: (uri) => {
76
+ const url = resolveNextclawDocsUrl(uri);
77
+ return {
78
+ historyPolicy: 'managed',
79
+ kind: 'docs',
80
+ title: t('docBrowserHelp'),
81
+ url,
82
+ };
83
+ },
84
+ areEquivalent: (left, right) => normalizeDocUrl(left) === normalizeDocUrl(right),
85
+ },
86
+ {
87
+ defaultUrl: () => RIGHT_PANEL_APPS_URL,
88
+ id: 'apps',
89
+ kind: RIGHT_PANEL_APPS_TAB_KIND,
90
+ match: (uri) => isUrlWithPrefix(uri.raw, RIGHT_PANEL_APPS_URL),
91
+ resolve: (uri) => {
92
+ const url = normalizeRightPanelAppsUrl(uri.raw);
93
+ return {
94
+ dedupeKey: 'apps',
95
+ historyPolicy: 'managed',
96
+ kind: RIGHT_PANEL_APPS_TAB_KIND,
97
+ title: url === RIGHT_PANEL_APPS_URL ? t('appsTitle') : t('serviceAppsTitle'),
98
+ url,
99
+ };
100
+ },
101
+ areEquivalent: (left, right) => normalizeRightPanelAppsUrl(left) === normalizeRightPanelAppsUrl(right),
102
+ },
103
+ {
104
+ defaultUrl: () => 'nextclaw://panel-app',
105
+ id: 'panel-app',
106
+ kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
107
+ match: (uri) => uri.scheme === 'nextclaw' && uri.authority === 'panel-app',
108
+ resolve: (uri) => {
109
+ const url = resolvePanelAppPlaceholderUrl(uri);
110
+ const appId = uri.pathSegments[0] ?? '';
111
+ return {
112
+ dedupeKey: appId ? `panel-app:${decodeURIComponent(appId)}` : undefined,
113
+ historyPolicy: 'managed',
114
+ kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
115
+ title: t('panelAppsTitle'),
116
+ url,
117
+ };
118
+ },
119
+ areEquivalent: (left, right) => left === right,
120
+ },
121
+ {
122
+ defaultUrl: () => 'about:blank',
123
+ id: 'content',
124
+ kind: 'content',
125
+ match: () => true,
126
+ resolve: (uri) => ({
127
+ historyPolicy: 'managed',
128
+ kind: 'content',
129
+ title: inferTabTitle(uri.raw, 'content', 'Detail'),
130
+ url: uri.raw || 'about:blank',
131
+ }),
132
+ areEquivalent: (left, right) => left === right,
133
+ },
134
+ ];
135
+
136
+ export const RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS_FOR_RESOURCE_URI: ResourceUriRouteDefinition<RightPanelResourceTarget>[] =
137
+ RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS.map((definition) => ({
138
+ areEquivalent: definition.areEquivalent,
139
+ id: definition.id,
140
+ match: definition.match,
141
+ resolve: definition.resolve,
142
+ }));
143
+
144
+ export function getRightPanelResourceHomeNavigationItems(): RightPanelResourceHomeNavigationItem[] {
145
+ return [
146
+ {
147
+ id: 'apps',
148
+ label: t('appsTitle'),
149
+ target: { type: 'right-panel-resource', uri: RIGHT_PANEL_APPS_URL },
150
+ },
151
+ {
152
+ id: 'service-apps',
153
+ label: t('serviceAppsTitle'),
154
+ target: { type: 'right-panel-resource', uri: RIGHT_PANEL_SERVICE_APPS_URL },
155
+ },
156
+ {
157
+ id: 'docs',
158
+ label: t('docBrowserHelp'),
159
+ target: {
160
+ options: { newTab: true },
161
+ type: 'right-panel-resource',
162
+ uri: getDefaultDocsUrl(),
163
+ },
164
+ },
165
+ {
166
+ id: 'skill-marketplace',
167
+ label: t('marketplaceSkillsPageTitle'),
168
+ target: { path: '/marketplace/skills', type: 'app-route' },
169
+ },
170
+ ];
171
+ }
@@ -0,0 +1,18 @@
1
+ export { RightPanelResourceHomePage } from '@/features/right-panel-resources/components/right-panel-resource-home-page';
2
+ export { RightPanelResourceRouteResolver } from '@/features/right-panel-resources/utils/right-panel-resource-route-resolver.utils';
3
+ export type {
4
+ RightPanelResourceHomeNavigationItem,
5
+ RightPanelResourceKind,
6
+ RightPanelResourceNavigationTarget,
7
+ RightPanelResourceTarget,
8
+ } from '@/features/right-panel-resources/types/right-panel-resource.types';
9
+ export {
10
+ createPanelAppResourceUri,
11
+ createPanelAppRightPanelResourceTarget,
12
+ RIGHT_PANEL_APPS_TAB_KIND,
13
+ RIGHT_PANEL_APPS_URL,
14
+ RIGHT_PANEL_HOME_TAB_KIND,
15
+ RIGHT_PANEL_HOME_URL,
16
+ RIGHT_PANEL_PANEL_APP_TAB_KIND,
17
+ RIGHT_PANEL_SERVICE_APPS_URL,
18
+ } from '@/features/right-panel-resources/utils/right-panel-resource-uri.utils';
@@ -0,0 +1,36 @@
1
+ import type {
2
+ DocBrowserOpenOptions,
3
+ DocBrowserRouteTarget,
4
+ DocBrowserTabKind,
5
+ } from '@/shared/components/doc-browser/types/doc-browser.types';
6
+ import type { ParsedResourceUri } from '@/shared/lib/resource-uri';
7
+
8
+ export type RightPanelResourceKind = DocBrowserTabKind;
9
+
10
+ export type RightPanelResourceTarget = DocBrowserRouteTarget;
11
+
12
+ export type RightPanelResourceRouteDefinition = {
13
+ areEquivalent?: (left: string, right: string) => boolean;
14
+ defaultUrl: () => string;
15
+ id: string;
16
+ kind: RightPanelResourceKind;
17
+ match: (uri: ParsedResourceUri) => boolean;
18
+ resolve: (uri: ParsedResourceUri) => RightPanelResourceTarget;
19
+ };
20
+
21
+ export type RightPanelResourceNavigationTarget =
22
+ | {
23
+ options?: DocBrowserOpenOptions;
24
+ type: 'right-panel-resource';
25
+ uri?: string;
26
+ }
27
+ | {
28
+ path: string;
29
+ type: 'app-route';
30
+ };
31
+
32
+ export type RightPanelResourceHomeNavigationItem = {
33
+ id: string;
34
+ label: string;
35
+ target: RightPanelResourceNavigationTarget;
36
+ };
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { RightPanelResourceRouteResolver } from '@/features/right-panel-resources';
3
+
4
+ describe('RightPanelResourceRouteResolver', () => {
5
+ it('normalizes apps resources and keeps service apps distinct', () => {
6
+ const resolver = new RightPanelResourceRouteResolver();
7
+
8
+ expect(resolver.resolve('nextclaw://apps')).toMatchObject({
9
+ dedupeKey: 'apps',
10
+ kind: 'apps',
11
+ url: 'nextclaw://apps',
12
+ });
13
+ expect(resolver.resolve('nextclaw://apps?tab=service-apps')).toMatchObject({
14
+ dedupeKey: 'apps',
15
+ kind: 'apps',
16
+ url: 'nextclaw://apps?tab=service-apps',
17
+ });
18
+ });
19
+
20
+ it('resolves nextclaw docs resources into docs targets', () => {
21
+ const resolver = new RightPanelResourceRouteResolver();
22
+ const target = resolver.resolve('nextclaw://docs/guide/getting-started');
23
+
24
+ expect(target.kind).toBe('docs');
25
+ expect(target.title).toBe('Help Docs');
26
+ expect(target.url).toContain('/guide/getting-started');
27
+ });
28
+
29
+ it('resolves panel app resources to a stable target identity', () => {
30
+ const resolver = new RightPanelResourceRouteResolver();
31
+
32
+ expect(resolver.resolve('nextclaw://panel-app/timer')).toMatchObject({
33
+ dedupeKey: 'panel-app:timer',
34
+ kind: 'panel-app',
35
+ url: 'nextclaw://panel-app/timer',
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,89 @@
1
+ import type {
2
+ DocBrowserRouteResolver,
3
+ DocBrowserTab,
4
+ DocBrowserTabKind,
5
+ } from '@/shared/components/doc-browser/types/doc-browser.types';
6
+ import { isDocsUrl } from '@/shared/components/doc-browser/utils/doc-browser-url.utils';
7
+ import { ResourceUriResolver, parseResourceUri } from '@/shared/lib/resource-uri';
8
+ import {
9
+ RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS,
10
+ RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS_FOR_RESOURCE_URI,
11
+ } from '@/features/right-panel-resources/configs/right-panel-resource-routes.config';
12
+ import type {
13
+ RightPanelResourceRouteDefinition,
14
+ RightPanelResourceTarget,
15
+ } from '@/features/right-panel-resources/types/right-panel-resource.types';
16
+
17
+ function inferFallbackKind(url?: string): DocBrowserTabKind {
18
+ return url && isDocsUrl(url) ? 'docs' : 'content';
19
+ }
20
+
21
+ export class RightPanelResourceRouteResolver implements DocBrowserRouteResolver {
22
+ private readonly resourceUriResolver = new ResourceUriResolver<RightPanelResourceTarget>(
23
+ RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS_FOR_RESOURCE_URI,
24
+ { getNormalizedUri: (target) => target.url },
25
+ );
26
+
27
+ resolve = (uri: string): RightPanelResourceTarget => this.resourceUriResolver.resolve(uri);
28
+
29
+ normalize = (uri: string): string => this.resourceUriResolver.normalize(uri);
30
+
31
+ areEquivalent = (left: string, right: string): boolean => this.resourceUriResolver.areEquivalent(left, right);
32
+
33
+ resolveOpenTarget = (params: {
34
+ activeTab?: DocBrowserTab;
35
+ kind?: DocBrowserTabKind;
36
+ url?: string;
37
+ }): RightPanelResourceTarget => {
38
+ const { activeTab, kind, url: requestedUrl } = params;
39
+ const route = this.resolveRouteDefinition(requestedUrl, kind, activeTab);
40
+ const rawUrl = requestedUrl?.trim()
41
+ || (!kind && activeTab && activeTab.kind !== 'docs'
42
+ ? activeTab.currentUrl
43
+ : route.defaultUrl());
44
+ return route.resolve(parseResourceUri(rawUrl));
45
+ };
46
+
47
+ areUrlsEquivalent = (
48
+ currentUrl: string,
49
+ nextUrl: string,
50
+ currentKind: DocBrowserTabKind,
51
+ nextKind: DocBrowserTabKind,
52
+ ): boolean => {
53
+ if (currentKind !== nextKind) {
54
+ return false;
55
+ }
56
+ const route = this.resolveRouteDefinition(currentUrl, currentKind);
57
+ return route.areEquivalent?.(currentUrl, nextUrl) ?? this.areEquivalent(currentUrl, nextUrl);
58
+ };
59
+
60
+ usesManagedHistory = (tab: DocBrowserTab): boolean => {
61
+ return this.resolveRouteDefinition(tab.currentUrl, tab.kind).resolve(parseResourceUri(tab.currentUrl)).historyPolicy === 'managed';
62
+ };
63
+
64
+ private resolveRouteDefinition = (
65
+ url?: string,
66
+ kind?: DocBrowserTabKind,
67
+ activeTab?: DocBrowserTab,
68
+ ): RightPanelResourceRouteDefinition => {
69
+ const targetKind = kind ?? (url ? undefined : activeTab?.kind);
70
+ const routeByKind = targetKind
71
+ ? RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS.find((route) => route.kind === targetKind)
72
+ : undefined;
73
+ if (routeByKind) {
74
+ return routeByKind;
75
+ }
76
+
77
+ const parsed = url ? parseResourceUri(url) : null;
78
+ const routeByUrl = parsed
79
+ ? RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS.find((route) => route.match(parsed))
80
+ : undefined;
81
+ if (routeByUrl) {
82
+ return routeByUrl;
83
+ }
84
+
85
+ const fallbackKind = kind ?? activeTab?.kind ?? inferFallbackKind(url);
86
+ return RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS.find((route) => route.kind === fallbackKind)
87
+ ?? RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS[RIGHT_PANEL_RESOURCE_ROUTE_DEFINITIONS.length - 1];
88
+ };
89
+ }
@@ -0,0 +1,48 @@
1
+ import type { PanelAppEntryView } from '@/shared/lib/api';
2
+ import type { RightPanelResourceTarget } from '@/features/right-panel-resources/types/right-panel-resource.types';
3
+
4
+ export const RIGHT_PANEL_HOME_TAB_KIND = 'home';
5
+ export const RIGHT_PANEL_HOME_URL = 'nextclaw://new-tab';
6
+ export const RIGHT_PANEL_APPS_TAB_KIND = 'apps';
7
+ export const RIGHT_PANEL_APPS_URL = 'nextclaw://apps';
8
+ export const RIGHT_PANEL_SERVICE_APPS_URL = `${RIGHT_PANEL_APPS_URL}?tab=service-apps`;
9
+ export const RIGHT_PANEL_PANEL_APP_TAB_KIND = 'panel-app';
10
+
11
+ export type RightPanelAppsTab = 'panel-apps' | 'service-apps';
12
+
13
+ const DEFAULT_RIGHT_PANEL_APPS_TAB: RightPanelAppsTab = 'panel-apps';
14
+
15
+ function isRightPanelAppsTab(value: unknown): value is RightPanelAppsTab {
16
+ return value === 'panel-apps' || value === 'service-apps';
17
+ }
18
+
19
+ export function createRightPanelAppsUrl(tab: RightPanelAppsTab = DEFAULT_RIGHT_PANEL_APPS_TAB): string {
20
+ return tab === DEFAULT_RIGHT_PANEL_APPS_TAB ? RIGHT_PANEL_APPS_URL : RIGHT_PANEL_SERVICE_APPS_URL;
21
+ }
22
+
23
+ export function getRightPanelAppsTabFromUrl(url: string): RightPanelAppsTab {
24
+ try {
25
+ const tab = new URL(url).searchParams.get('tab');
26
+ return isRightPanelAppsTab(tab) ? tab : DEFAULT_RIGHT_PANEL_APPS_TAB;
27
+ } catch {
28
+ return DEFAULT_RIGHT_PANEL_APPS_TAB;
29
+ }
30
+ }
31
+
32
+ export function normalizeRightPanelAppsUrl(url: string): string {
33
+ return createRightPanelAppsUrl(getRightPanelAppsTabFromUrl(url));
34
+ }
35
+
36
+ export function createPanelAppResourceUri(appId: string): string {
37
+ return `nextclaw://panel-app/${encodeURIComponent(appId)}`;
38
+ }
39
+
40
+ export function createPanelAppRightPanelResourceTarget(entry: PanelAppEntryView): RightPanelResourceTarget {
41
+ return {
42
+ dedupeKey: `panel-app:${entry.id}`,
43
+ historyPolicy: 'managed',
44
+ kind: RIGHT_PANEL_PANEL_APP_TAB_KIND,
45
+ title: entry.title,
46
+ url: entry.contentPath,
47
+ };
48
+ }
@@ -0,0 +1,58 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { SIDE_DOCK_BUILT_IN_ITEMS } from '@/features/side-dock/configs/side-dock-built-in-items.config';
4
+ import { SideDock } from '@/features/side-dock/components/side-dock';
5
+ import { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
6
+ import type { SideDockManager } from '@/features/side-dock/managers/side-dock.manager';
7
+ import { DocBrowserProvider } from '@/shared/components/doc-browser/doc-browser-context';
8
+ import { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
9
+ import { useDocBrowserStore } from '@/shared/components/doc-browser/stores/doc-browser.store';
10
+ import { createDefaultDocBrowserState } from '@/shared/components/doc-browser/utils/doc-browser-state.utils';
11
+
12
+ describe('SideDock', () => {
13
+ beforeEach(() => {
14
+ window.localStorage.clear();
15
+ useSideDockStore.getState().setPinnedItems([]);
16
+ useDocBrowserStore.getState().setSnapshot(createDefaultDocBrowserState());
17
+ });
18
+
19
+ it('renders built-in entries and opens the selected resource', () => {
20
+ const openItem = vi.fn();
21
+ const manager = { openItem } as unknown as SideDockManager;
22
+ const docBrowserManager = new DocBrowserManager();
23
+
24
+ render(
25
+ <DocBrowserProvider manager={docBrowserManager}>
26
+ <SideDock manager={manager} />
27
+ </DocBrowserProvider>,
28
+ );
29
+
30
+ expect(screen.getByTestId('side-dock')).toBeTruthy();
31
+ SIDE_DOCK_BUILT_IN_ITEMS.forEach((item) => {
32
+ expect(document.querySelector(`[data-side-dock-item-id="${item.id}"]`)).toBeTruthy();
33
+ });
34
+
35
+ const appsButton = document.querySelector('[data-side-dock-item-id="apps"]');
36
+ expect(appsButton).toBeTruthy();
37
+ fireEvent.click(appsButton as Element);
38
+
39
+ expect(openItem).toHaveBeenCalledWith(SIDE_DOCK_BUILT_IN_ITEMS[0]);
40
+ });
41
+
42
+ it('does not highlight the default docs tab while DocBrowser is closed', () => {
43
+ const manager = { openItem: vi.fn() } as unknown as SideDockManager;
44
+ const docBrowserManager = new DocBrowserManager();
45
+
46
+ render(
47
+ <DocBrowserProvider manager={docBrowserManager}>
48
+ <SideDock manager={manager} />
49
+ </DocBrowserProvider>,
50
+ );
51
+
52
+ const docsButton = document.querySelector('[data-side-dock-item-id="docs"]');
53
+
54
+ expect(docsButton).toBeTruthy();
55
+ expect(docsButton?.className).toContain('bg-transparent');
56
+ expect(docsButton?.className).not.toContain('shadow-sm');
57
+ });
58
+ });