@nextclaw/ui 0.3.16 → 0.3.17
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 +10 -0
- package/dist/assets/index-CPXV1dWr.js +337 -0
- package/dist/assets/index-Wn63frSd.css +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/App.tsx +2 -0
- package/src/api/marketplace.ts +110 -0
- package/src/api/types.ts +85 -0
- package/src/components/config/ChannelsList.tsx +2 -2
- package/src/components/config/ModelConfig.tsx +2 -2
- package/src/components/config/ProvidersList.tsx +3 -3
- package/src/components/config/SessionsConfig.tsx +93 -81
- package/src/components/doc-browser/DocBrowser.tsx +272 -0
- package/src/components/doc-browser/DocBrowserContext.tsx +134 -0
- package/src/components/doc-browser/index.ts +3 -0
- package/src/components/doc-browser/useDocLinkInterceptor.ts +33 -0
- package/src/components/layout/AppLayout.tsx +25 -8
- package/src/components/layout/Sidebar.tsx +32 -5
- package/src/components/marketplace/MarketplacePage.tsx +408 -0
- package/src/hooks/useMarketplace.ts +59 -0
- package/src/index.css +11 -4
- package/src/lib/i18n.ts +10 -1
- package/src/styles/design-system.css +256 -214
- package/dist/assets/index-DuW0OWcM.js +0 -298
- package/dist/assets/index-xwCviEXg.css +0 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
const DOCS_PRIMARY_DOMAIN = 'docs.nextclaw.io';
|
|
4
|
+
const DOCS_FALLBACK_DOMAIN = 'nextclaw-docs.pages.dev';
|
|
5
|
+
const DOCS_HOSTS = new Set([
|
|
6
|
+
DOCS_PRIMARY_DOMAIN,
|
|
7
|
+
`www.${DOCS_PRIMARY_DOMAIN}`,
|
|
8
|
+
DOCS_FALLBACK_DOMAIN,
|
|
9
|
+
`www.${DOCS_FALLBACK_DOMAIN}`,
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_FALLBACK_DOMAIN}`;
|
|
13
|
+
|
|
14
|
+
export type DocBrowserMode = 'floating' | 'docked';
|
|
15
|
+
|
|
16
|
+
interface DocBrowserState {
|
|
17
|
+
isOpen: boolean;
|
|
18
|
+
mode: DocBrowserMode;
|
|
19
|
+
currentUrl: string;
|
|
20
|
+
history: string[];
|
|
21
|
+
historyIndex: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface DocBrowserActions {
|
|
25
|
+
open: (url?: string) => void;
|
|
26
|
+
close: () => void;
|
|
27
|
+
toggleMode: () => void;
|
|
28
|
+
setMode: (mode: DocBrowserMode) => void;
|
|
29
|
+
navigate: (url: string) => void;
|
|
30
|
+
goBack: () => void;
|
|
31
|
+
goForward: () => void;
|
|
32
|
+
canGoBack: boolean;
|
|
33
|
+
canGoForward: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type DocBrowserContextValue = DocBrowserState & DocBrowserActions;
|
|
37
|
+
|
|
38
|
+
const DocBrowserContext = createContext<DocBrowserContextValue | null>(null);
|
|
39
|
+
|
|
40
|
+
export function useDocBrowser(): DocBrowserContextValue {
|
|
41
|
+
const ctx = useContext(DocBrowserContext);
|
|
42
|
+
if (!ctx) throw new Error('useDocBrowser must be used within DocBrowserProvider');
|
|
43
|
+
return ctx;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Check if a URL belongs to the docs domain */
|
|
47
|
+
export function isDocsUrl(url: string): boolean {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(url, window.location.origin);
|
|
50
|
+
return DOCS_HOSTS.has(parsed.hostname);
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function DocBrowserProvider({ children }: { children: ReactNode }) {
|
|
57
|
+
const [state, setState] = useState<DocBrowserState>({
|
|
58
|
+
isOpen: false,
|
|
59
|
+
mode: 'docked',
|
|
60
|
+
currentUrl: `${DOCS_DEFAULT_BASE_URL}/guide/getting-started`,
|
|
61
|
+
history: [`${DOCS_DEFAULT_BASE_URL}/guide/getting-started`],
|
|
62
|
+
historyIndex: 0,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const open = useCallback((url?: string) => {
|
|
66
|
+
const targetUrl = url || state.currentUrl || `${DOCS_DEFAULT_BASE_URL}/guide/getting-started`;
|
|
67
|
+
setState(prev => ({
|
|
68
|
+
...prev,
|
|
69
|
+
isOpen: true,
|
|
70
|
+
currentUrl: targetUrl,
|
|
71
|
+
history: [...prev.history.slice(0, prev.historyIndex + 1), targetUrl],
|
|
72
|
+
historyIndex: prev.historyIndex + 1,
|
|
73
|
+
}));
|
|
74
|
+
}, [state.currentUrl]);
|
|
75
|
+
|
|
76
|
+
const close = useCallback(() => {
|
|
77
|
+
setState(prev => ({ ...prev, isOpen: false }));
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
const toggleMode = useCallback(() => {
|
|
81
|
+
setState(prev => ({ ...prev, mode: prev.mode === 'floating' ? 'docked' : 'floating' }));
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const setMode = useCallback((mode: DocBrowserMode) => {
|
|
85
|
+
setState(prev => ({ ...prev, mode }));
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const navigate = useCallback((url: string) => {
|
|
89
|
+
setState(prev => ({
|
|
90
|
+
...prev,
|
|
91
|
+
currentUrl: url,
|
|
92
|
+
history: [...prev.history.slice(0, prev.historyIndex + 1), url],
|
|
93
|
+
historyIndex: prev.historyIndex + 1,
|
|
94
|
+
}));
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const goBack = useCallback(() => {
|
|
98
|
+
setState(prev => {
|
|
99
|
+
if (prev.historyIndex <= 0) return prev;
|
|
100
|
+
const newIndex = prev.historyIndex - 1;
|
|
101
|
+
return { ...prev, historyIndex: newIndex, currentUrl: prev.history[newIndex] };
|
|
102
|
+
});
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
const goForward = useCallback(() => {
|
|
106
|
+
setState(prev => {
|
|
107
|
+
if (prev.historyIndex >= prev.history.length - 1) return prev;
|
|
108
|
+
const newIndex = prev.historyIndex + 1;
|
|
109
|
+
return { ...prev, historyIndex: newIndex, currentUrl: prev.history[newIndex] };
|
|
110
|
+
});
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const canGoBack = state.historyIndex > 0;
|
|
114
|
+
const canGoForward = state.historyIndex < state.history.length - 1;
|
|
115
|
+
|
|
116
|
+
const value = useMemo<DocBrowserContextValue>(() => ({
|
|
117
|
+
...state,
|
|
118
|
+
open,
|
|
119
|
+
close,
|
|
120
|
+
toggleMode,
|
|
121
|
+
setMode,
|
|
122
|
+
navigate,
|
|
123
|
+
goBack,
|
|
124
|
+
goForward,
|
|
125
|
+
canGoBack,
|
|
126
|
+
canGoForward,
|
|
127
|
+
}), [state, open, close, toggleMode, setMode, navigate, goBack, goForward, canGoBack, canGoForward]);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<DocBrowserContext.Provider value={value}>
|
|
131
|
+
{children}
|
|
132
|
+
</DocBrowserContext.Provider>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { isDocsUrl, useDocBrowser } from './DocBrowserContext';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Global click interceptor for docs links.
|
|
6
|
+
* Captures clicks on <a> tags pointing to the docs domain
|
|
7
|
+
* and opens them in the in-app micro-browser instead.
|
|
8
|
+
*/
|
|
9
|
+
export function useDocLinkInterceptor() {
|
|
10
|
+
const docBrowser = useDocBrowser();
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const handler = (e: MouseEvent) => {
|
|
14
|
+
// Walk up from the click target to find an anchor
|
|
15
|
+
const anchor = (e.target as HTMLElement).closest<HTMLAnchorElement>('a[href]');
|
|
16
|
+
if (!anchor) return;
|
|
17
|
+
|
|
18
|
+
const href = anchor.getAttribute('href') || '';
|
|
19
|
+
if (!isDocsUrl(href)) return;
|
|
20
|
+
|
|
21
|
+
// Don't intercept if modifier keys are held (user wants new tab behavior)
|
|
22
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey) return;
|
|
23
|
+
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
e.stopPropagation();
|
|
26
|
+
docBrowser.open(href);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Use capture phase to intercept before React's synthetic events
|
|
30
|
+
document.addEventListener('click', handler, true);
|
|
31
|
+
return () => document.removeEventListener('click', handler, true);
|
|
32
|
+
}, [docBrowser]);
|
|
33
|
+
}
|
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import { Sidebar } from './Sidebar';
|
|
2
|
+
import { DocBrowserProvider, DocBrowser, useDocBrowser, useDocLinkInterceptor } from '@/components/doc-browser';
|
|
2
3
|
|
|
3
4
|
interface AppLayoutProps {
|
|
4
5
|
children: React.ReactNode;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
function AppLayoutInner({ children }: AppLayoutProps) {
|
|
9
|
+
const { isOpen, mode } = useDocBrowser();
|
|
10
|
+
useDocLinkInterceptor();
|
|
11
|
+
|
|
8
12
|
return (
|
|
9
|
-
<div className="h-screen flex bg-
|
|
13
|
+
<div className="h-screen flex bg-white font-sans text-foreground">
|
|
10
14
|
<Sidebar />
|
|
11
|
-
<div className="flex-1 flex
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
<div className="flex-1 flex min-w-0 overflow-hidden relative">
|
|
16
|
+
<div className="flex-1 flex flex-col min-w-0 bg-gray-50 overflow-hidden">
|
|
17
|
+
<main className="flex-1 overflow-auto custom-scrollbar p-10">
|
|
18
|
+
<div className="max-w-6xl mx-auto animate-fade-in h-full">
|
|
19
|
+
{children}
|
|
20
|
+
</div>
|
|
21
|
+
</main>
|
|
22
|
+
</div>
|
|
23
|
+
{/* Doc Browser: docked mode renders inline, floating mode renders as overlay */}
|
|
24
|
+
{isOpen && mode === 'docked' && <DocBrowser />}
|
|
17
25
|
</div>
|
|
26
|
+
{isOpen && mode === 'floating' && <DocBrowser />}
|
|
18
27
|
</div>
|
|
19
28
|
);
|
|
20
29
|
}
|
|
30
|
+
|
|
31
|
+
export function AppLayout({ children }: AppLayoutProps) {
|
|
32
|
+
return (
|
|
33
|
+
<DocBrowserProvider>
|
|
34
|
+
<AppLayoutInner>{children}</AppLayoutInner>
|
|
35
|
+
</DocBrowserProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
2
|
import { t } from '@/lib/i18n';
|
|
3
|
-
import { Cpu, GitBranch, History, MessageSquare, Sparkles } from 'lucide-react';
|
|
3
|
+
import { Cpu, GitBranch, History, MessageSquare, Sparkles, BookOpen, Store } from 'lucide-react';
|
|
4
4
|
import { NavLink } from 'react-router-dom';
|
|
5
|
+
import { useDocBrowser } from '@/components/doc-browser';
|
|
5
6
|
|
|
6
7
|
const navItems = [
|
|
7
8
|
{
|
|
@@ -28,12 +29,19 @@ const navItems = [
|
|
|
28
29
|
target: '/sessions',
|
|
29
30
|
label: t('sessions'),
|
|
30
31
|
icon: History,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
target: '/marketplace',
|
|
35
|
+
label: 'Marketplace',
|
|
36
|
+
icon: Store,
|
|
31
37
|
}
|
|
32
38
|
];
|
|
33
39
|
|
|
34
40
|
export function Sidebar() {
|
|
41
|
+
const docBrowser = useDocBrowser();
|
|
42
|
+
|
|
35
43
|
return (
|
|
36
|
-
<aside className="w-[240px] bg-
|
|
44
|
+
<aside className="w-[240px] bg-white border-r border-gray-200 flex flex-col h-full py-6 px-4">
|
|
37
45
|
{/* Logo Area */}
|
|
38
46
|
<div className="px-3 mb-8">
|
|
39
47
|
<div className="flex items-center gap-2.5 group cursor-pointer">
|
|
@@ -55,10 +63,10 @@ export function Sidebar() {
|
|
|
55
63
|
<NavLink
|
|
56
64
|
to={item.target}
|
|
57
65
|
className={({ isActive }) => cn(
|
|
58
|
-
'group w-full flex items-center gap-3 px-
|
|
66
|
+
'group w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-base',
|
|
59
67
|
isActive
|
|
60
|
-
? 'bg-
|
|
61
|
-
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
|
|
68
|
+
? 'bg-brand-50 text-brand-700'
|
|
69
|
+
: 'text-gray-600 hover:bg-gray-100/80 hover:text-gray-900'
|
|
62
70
|
)}
|
|
63
71
|
>
|
|
64
72
|
{({ isActive }) => (
|
|
@@ -76,6 +84,25 @@ export function Sidebar() {
|
|
|
76
84
|
})}
|
|
77
85
|
</ul>
|
|
78
86
|
</nav>
|
|
87
|
+
|
|
88
|
+
{/* Help Button */}
|
|
89
|
+
<div className="pt-2 border-t border-gray-100 mt-2">
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => docBrowser.open()}
|
|
92
|
+
className={cn(
|
|
93
|
+
'w-full flex items-center gap-3 px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-base',
|
|
94
|
+
docBrowser.isOpen
|
|
95
|
+
? 'bg-brand-50 text-brand-700'
|
|
96
|
+
: 'text-gray-600 hover:bg-gray-100/80 hover:text-gray-900'
|
|
97
|
+
)}
|
|
98
|
+
>
|
|
99
|
+
<BookOpen className={cn(
|
|
100
|
+
'h-4 w-4',
|
|
101
|
+
docBrowser.isOpen ? 'text-primary' : 'text-gray-500'
|
|
102
|
+
)} />
|
|
103
|
+
<span className="flex-1 text-left">{t('docBrowserHelp')}</span>
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
79
106
|
</aside>
|
|
80
107
|
);
|
|
81
108
|
}
|