@nextclaw/ui 0.5.1 → 0.5.3

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/dist/index.html CHANGED
@@ -6,8 +6,8 @@
6
6
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
  <title>NextClaw - 系统配置</title>
9
- <script type="module" crossorigin src="/assets/index-B_-o3-kG.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-B6sMhZ9q.css">
9
+ <script type="module" crossorigin src="/assets/index-CAFcPSll.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-BHGBLfqi.css">
11
11
  </head>
12
12
 
13
13
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,6 +11,7 @@
11
11
  "@radix-ui/react-slot": "^1.1.0",
12
12
  "@radix-ui/react-switch": "^1.1.1",
13
13
  "@radix-ui/react-tabs": "^1.1.1",
14
+ "@radix-ui/react-tooltip": "^1.1.11",
14
15
  "@tanstack/react-query": "^5.59.20",
15
16
  "class-variance-authority": "^0.7.1",
16
17
  "clsx": "^2.1.1",
@@ -2,6 +2,8 @@ import { api } from './client';
2
2
  import type {
3
3
  MarketplaceInstallRequest,
4
4
  MarketplaceInstallResult,
5
+ MarketplaceManageRequest,
6
+ MarketplaceManageResult,
5
7
  MarketplaceInstalledView,
6
8
  MarketplaceItemType,
7
9
  MarketplaceItemView,
@@ -108,3 +110,11 @@ export async function fetchMarketplaceInstalled(): Promise<MarketplaceInstalledV
108
110
  }
109
111
  return response.data;
110
112
  }
113
+
114
+ export async function manageMarketplaceItem(request: MarketplaceManageRequest): Promise<MarketplaceManageResult> {
115
+ const response = await api.post<MarketplaceManageResult>('/api/marketplace/manage', request);
116
+ if (!response.ok) {
117
+ throw new Error(response.error.message);
118
+ }
119
+ return response.data;
120
+ }
package/src/api/types.ts CHANGED
@@ -244,9 +244,9 @@ export type WsEvent =
244
244
 
245
245
  export type MarketplaceItemType = 'plugin' | 'skill';
246
246
 
247
- export type MarketplaceSort = 'relevance' | 'updated' | 'downloads';
247
+ export type MarketplaceSort = 'relevance' | 'updated';
248
248
 
249
- export type MarketplaceInstallKind = 'npm' | 'clawhub' | 'git';
249
+ export type MarketplaceInstallKind = 'npm' | 'clawhub' | 'git' | 'builtin';
250
250
 
251
251
  export type MarketplaceInstallSpec = {
252
252
  kind: MarketplaceInstallKind;
@@ -254,11 +254,6 @@ export type MarketplaceInstallSpec = {
254
254
  command: string;
255
255
  };
256
256
 
257
- export type MarketplaceItemMetrics = {
258
- downloads30d?: number;
259
- stars?: number;
260
- };
261
-
262
257
  export type MarketplaceItemSummary = {
263
258
  id: string;
264
259
  slug: string;
@@ -268,7 +263,6 @@ export type MarketplaceItemSummary = {
268
263
  tags: string[];
269
264
  author: string;
270
265
  install: MarketplaceInstallSpec;
271
- metrics?: MarketplaceItemMetrics;
272
266
  updatedAt: string;
273
267
  };
274
268
 
@@ -299,10 +293,15 @@ export type MarketplaceRecommendationView = {
299
293
 
300
294
  export type MarketplaceInstalledRecord = {
301
295
  type: MarketplaceItemType;
296
+ id?: string;
302
297
  spec: string;
303
298
  label?: string;
304
299
  source?: string;
305
300
  installedAt?: string;
301
+ enabled?: boolean;
302
+ runtimeStatus?: string;
303
+ origin?: string;
304
+ installPath?: string;
306
305
  };
307
306
 
308
307
  export type MarketplaceInstalledView = {
@@ -315,6 +314,7 @@ export type MarketplaceInstalledView = {
315
314
  export type MarketplaceInstallRequest = {
316
315
  type: MarketplaceItemType;
317
316
  spec: string;
317
+ kind?: MarketplaceInstallKind;
318
318
  version?: string;
319
319
  registry?: string;
320
320
  force?: boolean;
@@ -326,3 +326,20 @@ export type MarketplaceInstallResult = {
326
326
  message: string;
327
327
  output?: string;
328
328
  };
329
+
330
+ export type MarketplaceManageAction = 'enable' | 'disable' | 'uninstall';
331
+
332
+ export type MarketplaceManageRequest = {
333
+ type: MarketplaceItemType;
334
+ action: MarketplaceManageAction;
335
+ id?: string;
336
+ spec?: string;
337
+ };
338
+
339
+ export type MarketplaceManageResult = {
340
+ type: MarketplaceItemType;
341
+ action: MarketplaceManageAction;
342
+ id: string;
343
+ message: string;
344
+ output?: string;
345
+ };
@@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button';
13
13
  import { Input } from '@/components/ui/input';
14
14
  import { Label } from '@/components/ui/label';
15
15
  import { Switch } from '@/components/ui/switch';
16
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
16
17
  import { TagInput } from '@/components/common/TagInput';
17
18
  import { t } from '@/lib/i18n';
18
19
  import { hintForPath } from '@/lib/config-hints';
@@ -410,18 +411,21 @@ export function ChannelForm() {
410
411
  )}
411
412
 
412
413
  {field.type === 'select' && (
413
- <select
414
- id={field.name}
414
+ <Select
415
415
  value={(formData[field.name] as string) || ''}
416
- onChange={(e) => updateField(field.name, e.target.value)}
417
- className="flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm"
416
+ onValueChange={(v) => updateField(field.name, v)}
418
417
  >
419
- {(field.options ?? []).map((option) => (
420
- <option key={option.value} value={option.value}>
421
- {option.label}
422
- </option>
423
- ))}
424
- </select>
418
+ <SelectTrigger className="rounded-xl">
419
+ <SelectValue />
420
+ </SelectTrigger>
421
+ <SelectContent>
422
+ {(field.options ?? []).map((option) => (
423
+ <SelectItem key={option.value} value={option.value}>
424
+ {option.label}
425
+ </SelectItem>
426
+ ))}
427
+ </SelectContent>
428
+ </Select>
425
429
  )}
426
430
 
427
431
  {field.type === 'json' && (
@@ -12,6 +12,7 @@ import {
12
12
  import { Button } from '@/components/ui/button';
13
13
  import { Input } from '@/components/ui/input';
14
14
  import { Label } from '@/components/ui/label';
15
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
15
16
  import { MaskedInput } from '@/components/common/MaskedInput';
16
17
  import { KeyValueEditor } from '@/components/common/KeyValueEditor';
17
18
  import { t } from '@/lib/i18n';
@@ -149,22 +150,22 @@ export function ProviderForm() {
149
150
  <Hash className="h-3.5 w-3.5 text-gray-500" />
150
151
  {wireApiHint?.label ?? t('wireApi')}
151
152
  </Label>
152
- <select
153
- id="wireApi"
154
- value={wireApi}
155
- onChange={(e) => setWireApi(e.target.value as 'auto' | 'chat' | 'responses')}
156
- className="flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
157
- >
158
- {(providerSpec.wireApiOptions || ['auto', 'chat', 'responses']).map((option) => (
159
- <option key={option} value={option}>
160
- {option === 'chat'
161
- ? t('wireApiChat')
162
- : option === 'responses'
163
- ? t('wireApiResponses')
164
- : t('wireApiAuto')}
165
- </option>
166
- ))}
167
- </select>
153
+ <Select value={wireApi} onValueChange={(v) => setWireApi(v as 'auto' | 'chat' | 'responses')}>
154
+ <SelectTrigger className="rounded-xl">
155
+ <SelectValue />
156
+ </SelectTrigger>
157
+ <SelectContent>
158
+ {(providerSpec.wireApiOptions || ['auto', 'chat', 'responses']).map((option) => (
159
+ <SelectItem key={option} value={option}>
160
+ {option === 'chat'
161
+ ? t('wireApiChat')
162
+ : option === 'responses'
163
+ ? t('wireApiResponses')
164
+ : t('wireApiAuto')}
165
+ </SelectItem>
166
+ ))}
167
+ </SelectContent>
168
+ </Select>
168
169
  </div>
169
170
  )}
170
171
 
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
5
5
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
6
6
  import { Input } from '@/components/ui/input';
7
7
  import { Switch } from '@/components/ui/switch';
8
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
8
9
  import { hintForPath } from '@/lib/config-hints';
9
10
  import { Plus, Save, Trash2 } from 'lucide-react';
10
11
  import { toast } from 'sonner';
@@ -84,9 +85,9 @@ export function RuntimeConfig() {
84
85
  accountId: binding.match?.accountId ?? '',
85
86
  peer: binding.match?.peer
86
87
  ? {
87
- kind: binding.match.peer.kind,
88
- id: binding.match.peer.id
89
- }
88
+ kind: binding.match.peer.kind,
89
+ id: binding.match.peer.id
90
+ }
90
91
  : undefined
91
92
  }
92
93
  }))
@@ -260,17 +261,18 @@ export function RuntimeConfig() {
260
261
  </div>
261
262
  <div className="space-y-2">
262
263
  <label className="text-sm font-medium text-gray-800">{dmScopeHint?.label ?? 'DM Scope'}</label>
263
- <select
264
- value={dmScope}
265
- onChange={(event) => setDmScope(event.target.value as DmScope)}
266
- className="flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm"
267
- >
268
- {DM_SCOPE_OPTIONS.map((option) => (
269
- <option key={option.value} value={option.value}>
270
- {option.label}
271
- </option>
272
- ))}
273
- </select>
264
+ <Select value={dmScope} onValueChange={(v) => setDmScope(v as DmScope)}>
265
+ <SelectTrigger>
266
+ <SelectValue />
267
+ </SelectTrigger>
268
+ <SelectContent>
269
+ {DM_SCOPE_OPTIONS.map((option) => (
270
+ <SelectItem key={option.value} value={option.value}>
271
+ {option.label}
272
+ </SelectItem>
273
+ ))}
274
+ </SelectContent>
275
+ </Select>
274
276
  </div>
275
277
  <div className="space-y-2">
276
278
  <label className="text-sm font-medium text-gray-800">
@@ -429,10 +431,10 @@ export function RuntimeConfig() {
429
431
  }
430
432
  placeholder="Account ID (optional)"
431
433
  />
432
- <select
433
- value={peerKind}
434
- onChange={(event) => {
435
- const nextKind = event.target.value as PeerKind;
434
+ <Select
435
+ value={peerKind || '__none__'}
436
+ onValueChange={(v) => {
437
+ const nextKind = v === '__none__' ? '' : v as PeerKind;
436
438
  if (!nextKind) {
437
439
  updateBinding(index, {
438
440
  ...binding,
@@ -454,13 +456,17 @@ export function RuntimeConfig() {
454
456
  }
455
457
  });
456
458
  }}
457
- className="flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm"
458
459
  >
459
- <option value="">Peer kind (optional)</option>
460
- <option value="direct">direct</option>
461
- <option value="group">group</option>
462
- <option value="channel">channel</option>
463
- </select>
460
+ <SelectTrigger>
461
+ <SelectValue />
462
+ </SelectTrigger>
463
+ <SelectContent>
464
+ <SelectItem value="__none__">Peer kind (optional)</SelectItem>
465
+ <SelectItem value="direct">direct</SelectItem>
466
+ <SelectItem value="group">group</SelectItem>
467
+ <SelectItem value="channel">channel</SelectItem>
468
+ </SelectContent>
469
+ </Select>
464
470
  <Input
465
471
  value={binding.match.peer?.id ?? ''}
466
472
  onChange={(event) =>
@@ -470,9 +476,9 @@ export function RuntimeConfig() {
470
476
  ...binding.match,
471
477
  peer: peerKind
472
478
  ? {
473
- kind: peerKind,
474
- id: event.target.value
475
- }
479
+ kind: peerKind,
480
+ id: event.target.value
481
+ }
476
482
  : undefined
477
483
  }
478
484
  })
@@ -23,10 +23,10 @@ import {
23
23
  */
24
24
  export function DocBrowser() {
25
25
  const {
26
- isOpen, mode, currentUrl,
26
+ isOpen, mode, currentUrl, navVersion,
27
27
  close, toggleMode,
28
28
  goBack, goForward, canGoBack, canGoForward,
29
- navigate,
29
+ navigate, syncUrl,
30
30
  } = useDocBrowser();
31
31
 
32
32
  const [urlInput, setUrlInput] = useState('');
@@ -41,6 +41,8 @@ export function DocBrowser() {
41
41
  const dragRef = useRef<{ startX: number; startY: number; startPosX: number; startPosY: number } | null>(null);
42
42
  const resizeRef = useRef<{ startX: number; startY: number; startW: number; startH: number } | null>(null);
43
43
  const dockResizeRef = useRef<{ startX: number; startW: number } | null>(null);
44
+ const iframeRef = useRef<HTMLIFrameElement>(null);
45
+ const prevNavVersionRef = useRef(navVersion);
44
46
 
45
47
  // Sync URL input with current URL
46
48
  useEffect(() => {
@@ -52,6 +54,23 @@ export function DocBrowser() {
52
54
  }
53
55
  }, [currentUrl]);
54
56
 
57
+ // When currentUrl changes without navVersion bump (goBack/goForward),
58
+ // use postMessage to SPA-navigate inside the iframe instead of remounting
59
+ useEffect(() => {
60
+ if (navVersion !== prevNavVersionRef.current) {
61
+ // navVersion changed — iframe will remount via key, no need to postMessage
62
+ prevNavVersionRef.current = navVersion;
63
+ return;
64
+ }
65
+ // Same navVersion — means goBack/goForward triggered
66
+ if (iframeRef.current?.contentWindow) {
67
+ try {
68
+ const path = new URL(currentUrl).pathname;
69
+ iframeRef.current.contentWindow.postMessage({ type: 'docs-navigate', path }, '*');
70
+ } catch { /* ignore */ }
71
+ }
72
+ }, [currentUrl, navVersion]);
73
+
55
74
  // Reposition floating window near right edge when switching from docked
56
75
  useEffect(() => {
57
76
  if (mode === 'floating') {
@@ -66,12 +85,12 @@ export function DocBrowser() {
66
85
  useEffect(() => {
67
86
  const handler = (e: MessageEvent) => {
68
87
  if (e.data?.type === 'docs-route-change' && typeof e.data.url === 'string') {
69
- navigate(e.data.url);
88
+ syncUrl(e.data.url);
70
89
  }
71
90
  };
72
91
  window.addEventListener('message', handler);
73
92
  return () => window.removeEventListener('message', handler);
74
- }, [navigate]);
93
+ }, [syncUrl]);
75
94
 
76
95
  const handleUrlSubmit = useCallback((e: React.FormEvent) => {
77
96
  e.preventDefault();
@@ -169,6 +188,29 @@ export function DocBrowser() {
169
188
  window.addEventListener('mouseup', onUp);
170
189
  }, [dockedWidth]);
171
190
 
191
+ // --- Left-edge resize logic (floating mode — adjusts both position and width) ---
192
+ const onLeftResizeStart = useCallback((e: React.MouseEvent) => {
193
+ e.preventDefault();
194
+ e.stopPropagation();
195
+ setIsResizing(true);
196
+ const startX = e.clientX;
197
+ const startW = floatSize.w;
198
+ const startPosX = floatPos.x;
199
+ const onMove = (ev: MouseEvent) => {
200
+ const delta = startX - ev.clientX;
201
+ const newW = Math.max(360, startW + delta);
202
+ setFloatSize(prev => ({ ...prev, w: newW }));
203
+ setFloatPos(prev => ({ ...prev, x: startPosX - (newW - startW) }));
204
+ };
205
+ const onUp = () => {
206
+ setIsResizing(false);
207
+ window.removeEventListener('mousemove', onMove);
208
+ window.removeEventListener('mouseup', onUp);
209
+ };
210
+ window.addEventListener('mousemove', onMove);
211
+ window.addEventListener('mouseup', onUp);
212
+ }, [floatSize.w, floatPos.x]);
213
+
172
214
  if (!isOpen) return null;
173
215
 
174
216
  const isDocked = mode === 'docked';
@@ -264,7 +306,8 @@ export function DocBrowser() {
264
306
  {/* Iframe Content */}
265
307
  <div className="flex-1 relative overflow-hidden">
266
308
  <iframe
267
- key={currentUrl}
309
+ ref={iframeRef}
310
+ key={navVersion}
268
311
  src={currentUrl}
269
312
  className="absolute inset-0 w-full h-full border-0"
270
313
  title="NextClaw Documentation"
@@ -283,6 +326,7 @@ export function DocBrowser() {
283
326
  href={currentUrl}
284
327
  target="_blank"
285
328
  rel="noopener noreferrer"
329
+ data-doc-external
286
330
  className="flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover font-medium transition-colors"
287
331
  >
288
332
  {t('docBrowserOpenExternal')}
@@ -293,6 +337,8 @@ export function DocBrowser() {
293
337
  {/* Resize Handles (floating only) */}
294
338
  {!isDocked && (
295
339
  <>
340
+ {/* Left edge */}
341
+ <div className="absolute top-0 left-0 w-1.5 h-full cursor-ew-resize z-20 hover:bg-primary/10 transition-colors" onMouseDown={onLeftResizeStart} />
296
342
  {/* Right edge */}
297
343
  <div className="absolute top-0 right-0 w-1.5 h-full cursor-ew-resize z-20 hover:bg-primary/10 transition-colors" onMouseDown={onResizeStart} data-axis="x" />
298
344
  {/* Bottom edge */}
@@ -1,24 +1,31 @@
1
1
  import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from 'react';
2
2
 
3
3
  const DOCS_PRIMARY_DOMAIN = 'docs.nextclaw.io';
4
- const DOCS_FALLBACK_DOMAIN = 'nextclaw-docs.pages.dev';
4
+ const DOCS_PAGES_DEV = 'nextclaw-docs.pages.dev';
5
5
  const DOCS_HOSTS = new Set([
6
6
  DOCS_PRIMARY_DOMAIN,
7
7
  `www.${DOCS_PRIMARY_DOMAIN}`,
8
- DOCS_FALLBACK_DOMAIN,
9
- `www.${DOCS_FALLBACK_DOMAIN}`,
8
+ DOCS_PAGES_DEV,
9
+ `www.${DOCS_PAGES_DEV}`,
10
10
  ]);
11
11
 
12
- export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_FALLBACK_DOMAIN}`;
12
+ export const DOCS_DEFAULT_BASE_URL = `https://${DOCS_PRIMARY_DOMAIN}`;
13
13
 
14
14
  export type DocBrowserMode = 'floating' | 'docked';
15
15
 
16
+ /** Normalize URL for comparison: strip .html and trailing slash */
17
+ function normalizeDocUrl(u: string): string {
18
+ try { return new URL(u).pathname.replace(/\.html$/, '').replace(/\/$/, ''); } catch { return u; }
19
+ }
20
+
16
21
  interface DocBrowserState {
17
22
  isOpen: boolean;
18
23
  mode: DocBrowserMode;
19
24
  currentUrl: string;
20
25
  history: string[];
21
26
  historyIndex: number;
27
+ /** Increments on parent-initiated navigation (navigate/goBack/goForward) */
28
+ navVersion: number;
22
29
  }
23
30
 
24
31
  interface DocBrowserActions {
@@ -26,7 +33,10 @@ interface DocBrowserActions {
26
33
  close: () => void;
27
34
  toggleMode: () => void;
28
35
  setMode: (mode: DocBrowserMode) => void;
36
+ /** Parent-initiated navigation — will cause iframe to reload to this URL */
29
37
  navigate: (url: string) => void;
38
+ /** Iframe-initiated sync — records URL to history without reloading iframe */
39
+ syncUrl: (url: string) => void;
30
40
  goBack: () => void;
31
41
  goForward: () => void;
32
42
  canGoBack: boolean;
@@ -60,6 +70,7 @@ export function DocBrowserProvider({ children }: { children: ReactNode }) {
60
70
  currentUrl: `${DOCS_DEFAULT_BASE_URL}/guide/getting-started`,
61
71
  history: [`${DOCS_DEFAULT_BASE_URL}/guide/getting-started`],
62
72
  historyIndex: 0,
73
+ navVersion: 0,
63
74
  });
64
75
 
65
76
  const open = useCallback((url?: string) => {
@@ -70,6 +81,7 @@ export function DocBrowserProvider({ children }: { children: ReactNode }) {
70
81
  currentUrl: targetUrl,
71
82
  history: [...prev.history.slice(0, prev.historyIndex + 1), targetUrl],
72
83
  historyIndex: prev.historyIndex + 1,
84
+ navVersion: prev.navVersion + 1,
73
85
  }));
74
86
  }, [state.currentUrl]);
75
87
 
@@ -85,14 +97,24 @@ export function DocBrowserProvider({ children }: { children: ReactNode }) {
85
97
  setState(prev => ({ ...prev, mode }));
86
98
  }, []);
87
99
 
100
+ /** Parent-initiated: push to history AND bump navVersion so iframe reloads */
88
101
  const navigate = useCallback((url: string) => {
89
102
  setState(prev => {
90
- // Normalize URLs for comparison (strip trailing slash, .html suffix)
91
- const normalize = (u: string) => {
92
- try { return new URL(u).pathname.replace(/\.html$/, '').replace(/\/$/, ''); } catch { return u; }
103
+ if (normalizeDocUrl(url) === normalizeDocUrl(prev.currentUrl)) return prev;
104
+ return {
105
+ ...prev,
106
+ currentUrl: url,
107
+ history: [...prev.history.slice(0, prev.historyIndex + 1), url],
108
+ historyIndex: prev.historyIndex + 1,
109
+ navVersion: prev.navVersion + 1,
93
110
  };
94
- // Skip if same as current URL (prevents loop from postMessage echo)
95
- if (normalize(url) === normalize(prev.currentUrl)) return prev;
111
+ });
112
+ }, []);
113
+
114
+ /** Iframe-initiated: push to history but do NOT bump navVersion (no iframe reload) */
115
+ const syncUrl = useCallback((url: string) => {
116
+ setState(prev => {
117
+ if (normalizeDocUrl(url) === normalizeDocUrl(prev.currentUrl)) return prev;
96
118
  return {
97
119
  ...prev,
98
120
  currentUrl: url,
@@ -128,11 +150,12 @@ export function DocBrowserProvider({ children }: { children: ReactNode }) {
128
150
  toggleMode,
129
151
  setMode,
130
152
  navigate,
153
+ syncUrl,
131
154
  goBack,
132
155
  goForward,
133
156
  canGoBack,
134
157
  canGoForward,
135
- }), [state, open, close, toggleMode, setMode, navigate, goBack, goForward, canGoBack, canGoForward]);
158
+ }), [state, open, close, toggleMode, setMode, navigate, syncUrl, goBack, goForward, canGoBack, canGoForward]);
136
159
 
137
160
  return (
138
161
  <DocBrowserContext.Provider value={value}>