@nextclaw/ui 0.5.35 → 0.5.36

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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/ChannelsList-BTQcN7OQ.js +1 -0
  3. package/dist/assets/{ChatPage-C_wANEY9.js → ChatPage-B5OG3EW3.js} +2 -2
  4. package/dist/assets/CronConfig-MXdvM9gu.js +1 -0
  5. package/dist/assets/{DocBrowser-CdX5oDgu.js → DocBrowser-CJDon901.js} +1 -1
  6. package/dist/assets/MarketplacePage-BwaTwPfP.js +1 -0
  7. package/dist/assets/{ModelConfig-DagIPD4R.js → ModelConfig-qJyJ1XS-.js} +1 -1
  8. package/dist/assets/{ProvidersList-2aHarRNe.js → ProvidersList-DinfLIyS.js} +1 -1
  9. package/dist/assets/{RuntimeConfig-BDyqfVSA.js → RuntimeConfig-DoKy3o8n.js} +1 -1
  10. package/dist/assets/{SecretsConfig-De2IZ7GX.js → SecretsConfig-S_jppujG.js} +1 -1
  11. package/dist/assets/SessionsConfig-C5VnCiw_.js +2 -0
  12. package/dist/assets/{card-CWG4Tz0Y.js → card-Bsb-eVmY.js} +1 -1
  13. package/dist/assets/index-BzQBLXUW.js +2 -0
  14. package/dist/assets/index-DcyOd66N.css +1 -0
  15. package/dist/assets/{label-LbWa2Yzc.js → label-CXP5KktX.js} +1 -1
  16. package/dist/assets/logos-DqE_6ErA.js +1 -0
  17. package/dist/assets/{page-layout-Bz8CAEiD.js → page-layout-DMWzimj9.js} +1 -1
  18. package/dist/assets/{switch-BwbfObfA.js → switch-oEZ0AFmj.js} +1 -1
  19. package/dist/assets/{tabs-custom-VeX6BYro.js → tabs-custom-CQP93tp3.js} +1 -1
  20. package/dist/assets/{useConfig-CFFZ66EV.js → useConfig-C0nxJgik.js} +1 -1
  21. package/dist/assets/useConfirmDialog-d6TTs8io.js +5 -0
  22. package/dist/assets/{vendor-CmqkRoMs.js → vendor-DN_iJQc4.js} +75 -85
  23. package/dist/index.html +3 -3
  24. package/package.json +1 -1
  25. package/src/components/common/LogoBadge.tsx +2 -2
  26. package/src/components/config/ChannelForm.tsx +180 -193
  27. package/src/components/config/ChannelsList.tsx +130 -93
  28. package/src/lib/i18n.ts +6 -0
  29. package/dist/assets/ChannelsList-DGoIQT1t.js +0 -1
  30. package/dist/assets/CronConfig-Q7faThLl.js +0 -1
  31. package/dist/assets/MarketplacePage-DXoPkFYk.js +0 -1
  32. package/dist/assets/SessionsConfig-BK0xx6EF.js +0 -2
  33. package/dist/assets/dialog-ssdjbutm.js +0 -5
  34. package/dist/assets/index-B8Wh_FvS.css +0 -1
  35. package/dist/assets/index-q2B1bssI.js +0 -2
  36. package/dist/assets/logos-DncMldHC.js +0 -1
  37. package/dist/assets/useConfirmDialog-ClpvgpHh.js +0 -1
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
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-q2B1bssI.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-CmqkRoMs.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-B8Wh_FvS.css">
9
+ <script type="module" crossorigin src="/assets/index-BzQBLXUW.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-DN_iJQc4.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-DcyOd66N.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.5.35",
3
+ "version": "0.5.36",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -14,12 +14,12 @@ export function LogoBadge({ name, src, className, imgClassName, fallback }: Logo
14
14
  const showImage = Boolean(src) && !failed;
15
15
 
16
16
  return (
17
- <div className={cn('flex items-center justify-center', className)}>
17
+ <div className={cn('inline-flex shrink-0 items-center justify-center overflow-hidden', className)}>
18
18
  {showImage ? (
19
19
  <img
20
20
  src={src as string}
21
21
  alt={`${name} logo`}
22
- className={cn('h-6 w-6 object-contain', imgClassName)}
22
+ className={cn('block h-6 w-6 object-contain', imgClassName)}
23
23
  onError={() => setFailed(true)}
24
24
  draggable={false}
25
25
  />
@@ -1,31 +1,30 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useConfig, useConfigMeta, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
3
- import { useUiStore } from '@/stores/ui.store';
4
- import {
5
- Dialog,
6
- DialogContent,
7
- DialogHeader,
8
- DialogTitle,
9
- DialogDescription,
10
- DialogFooter,
11
- } from '@/components/ui/dialog';
12
3
  import { Button } from '@/components/ui/button';
13
4
  import { Input } from '@/components/ui/input';
14
5
  import { Label } from '@/components/ui/label';
15
6
  import { Switch } from '@/components/ui/switch';
16
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
17
8
  import { TagInput } from '@/components/common/TagInput';
9
+ import { StatusDot } from '@/components/ui/status-dot';
10
+ import { LogoBadge } from '@/components/common/LogoBadge';
18
11
  import { t } from '@/lib/i18n';
19
12
  import { hintForPath } from '@/lib/config-hints';
13
+ import { cn } from '@/lib/utils';
20
14
  import { toast } from 'sonner';
21
- import { MessageCircle, Settings, ToggleLeft, Hash, Mail, Globe, KeyRound, BookOpen } from 'lucide-react';
15
+ import { Settings, ToggleLeft, Hash, Mail, Globe, KeyRound, BookOpen } from 'lucide-react';
22
16
  import type { ConfigActionManifest } from '@/api/types';
23
17
  import { resolveChannelTutorialUrl } from '@/lib/channel-tutorials';
18
+ import { getChannelLogo } from '@/lib/logos';
24
19
 
25
20
  type ChannelFieldType = 'boolean' | 'text' | 'email' | 'password' | 'number' | 'tags' | 'select' | 'json';
26
21
  type ChannelOption = { value: string; label: string };
27
22
  type ChannelField = { name: string; type: ChannelFieldType; label: string; options?: ChannelOption[] };
28
23
 
24
+ type ChannelFormProps = {
25
+ channelName?: string;
26
+ };
27
+
29
28
  const DM_POLICY_OPTIONS: ChannelOption[] = [
30
29
  { value: 'pairing', label: 'pairing' },
31
30
  { value: 'allowlist', label: 'allowlist' },
@@ -46,7 +45,6 @@ const STREAMING_MODE_OPTIONS: ChannelOption[] = [
46
45
  { value: 'progress', label: 'progress' }
47
46
  ];
48
47
 
49
- // Field icon mapping
50
48
  const getFieldIcon = (fieldName: string) => {
51
49
  if (fieldName.includes('token') || fieldName.includes('secret') || fieldName.includes('password')) {
52
50
  return <KeyRound className="h-3.5 w-3.5 text-gray-500" />;
@@ -164,20 +162,6 @@ function buildChannelFields(): Record<string, ChannelField[]> {
164
162
  };
165
163
  }
166
164
 
167
- const channelIcons: Record<string, typeof MessageCircle> = {
168
- telegram: MessageCircle,
169
- slack: MessageCircle,
170
- email: Mail,
171
- default: MessageCircle
172
- };
173
-
174
- const channelColors: Record<string, string> = {
175
- telegram: 'from-primary-300 to-primary-600',
176
- slack: 'from-primary-200 to-primary-500',
177
- email: 'from-primary-100 to-primary-400',
178
- default: 'from-gray-300 to-gray-500'
179
- };
180
-
181
165
  function isRecord(value: unknown): value is Record<string, unknown> {
182
166
  return typeof value === 'object' && value !== null && !Array.isArray(value);
183
167
  }
@@ -208,8 +192,7 @@ function buildScopeDraft(scope: string, value: Record<string, unknown>): Record<
208
192
  return output;
209
193
  }
210
194
 
211
- export function ChannelForm() {
212
- const { channelModal, closeChannelModal } = useUiStore();
195
+ export function ChannelForm({ channelName }: ChannelFormProps) {
213
196
  const { data: config } = useConfig();
214
197
  const { data: meta } = useConfigMeta();
215
198
  const { data: schema } = useConfigSchema();
@@ -220,7 +203,6 @@ export function ChannelForm() {
220
203
  const [jsonDrafts, setJsonDrafts] = useState<Record<string, string>>({});
221
204
  const [runningActionId, setRunningActionId] = useState<string | null>(null);
222
205
 
223
- const channelName = channelModal.channel;
224
206
  const channelConfig = channelName ? config?.channels[channelName] : null;
225
207
  const fields = channelName ? buildChannelFields()[channelName] ?? [] : [];
226
208
  const uiHints = schema?.uiHints;
@@ -282,10 +264,7 @@ export function ChannelForm() {
282
264
  }
283
265
  }
284
266
 
285
- updateChannel.mutate(
286
- { channel: channelName, data: payload },
287
- { onSuccess: () => closeChannelModal() }
288
- );
267
+ updateChannel.mutate({ channel: channelName, data: payload });
289
268
  };
290
269
 
291
270
  const applyActionPatchToForm = (patch?: Record<string, unknown>) => {
@@ -344,172 +323,180 @@ export function ChannelForm() {
344
323
  }
345
324
  };
346
325
 
347
- const Icon = channelIcons[channelName || ''] || channelIcons.default;
348
- const gradientClass = channelColors[channelName || ''] || channelColors.default;
326
+ if (!channelName || !channelMeta || !channelConfig) {
327
+ return (
328
+ <div className="flex min-h-[520px] items-center justify-center rounded-2xl border border-gray-200/70 bg-white px-6 text-center xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]">
329
+ <div>
330
+ <h3 className="text-base font-semibold text-gray-900">{t('channelsSelectTitle')}</h3>
331
+ <p className="mt-2 text-sm text-gray-500">{t('channelsSelectDescription')}</p>
332
+ </div>
333
+ </div>
334
+ );
335
+ }
336
+
337
+ const enabled = Boolean(channelConfig.enabled);
349
338
 
350
339
  return (
351
- <Dialog open={channelModal.open} onOpenChange={closeChannelModal}>
352
- <DialogContent className="sm:max-w-[550px] max-h-[85vh] overflow-hidden flex flex-col">
353
- <DialogHeader>
354
- <div className="flex items-center gap-3">
355
- <div className={`h-10 w-10 rounded-xl bg-gradient-to-br ${gradientClass} flex items-center justify-center`}>
356
- <Icon className="h-5 w-5 text-white" />
357
- </div>
358
- <div>
359
- <DialogTitle className="capitalize">{channelLabel}</DialogTitle>
360
- <DialogDescription>{t('configureMessageChannelParameters')}</DialogDescription>
361
- {tutorialUrl && (
362
- <a
363
- href={tutorialUrl}
364
- className="mt-2 inline-flex items-center gap-1.5 text-xs text-primary hover:text-primary-hover transition-colors"
365
- >
366
- <BookOpen className="h-3.5 w-3.5" />
367
- {t('channelsGuideTitle')}
368
- </a>
369
- )}
340
+ <div className="flex min-h-[520px] flex-col rounded-2xl border border-gray-200/70 bg-white shadow-card xl:h-[calc(100vh-180px)] xl:min-h-[600px] xl:max-h-[860px]">
341
+ <div className="border-b border-gray-100 px-6 py-5">
342
+ <div className="flex flex-wrap items-start justify-between gap-3">
343
+ <div className="min-w-0">
344
+ <div className="flex items-center gap-3">
345
+ <LogoBadge
346
+ name={channelName}
347
+ src={getChannelLogo(channelName)}
348
+ className={cn(
349
+ 'h-9 w-9 rounded-lg border',
350
+ enabled ? 'border-primary/30 bg-white' : 'border-gray-200/70 bg-white'
351
+ )}
352
+ imgClassName="h-5 w-5 object-contain"
353
+ fallback={<span className="text-sm font-semibold uppercase text-gray-500">{channelName[0]}</span>}
354
+ />
355
+ <h3 className="truncate text-lg font-semibold text-gray-900 capitalize">{channelLabel}</h3>
370
356
  </div>
357
+ <p className="mt-2 text-sm text-gray-500">{t('channelsFormDescription')}</p>
358
+ {tutorialUrl && (
359
+ <a
360
+ href={tutorialUrl}
361
+ className="mt-2 inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover"
362
+ >
363
+ <BookOpen className="h-3.5 w-3.5" />
364
+ {t('channelsGuideTitle')}
365
+ </a>
366
+ )}
371
367
  </div>
372
- </DialogHeader>
373
-
374
- <form onSubmit={handleSubmit} className="flex flex-col flex-1 overflow-hidden">
375
- <div className="flex-1 overflow-y-auto custom-scrollbar py-2 pr-2 space-y-5">
376
- {fields.map((field) => {
377
- const hint = channelName
378
- ? hintForPath(`channels.${channelName}.${field.name}`, uiHints)
379
- : undefined;
380
- const label = hint?.label ?? field.label;
381
- const placeholder = hint?.placeholder;
382
-
383
- return (
384
- <div key={field.name} className="space-y-2.5">
385
- <Label
386
- htmlFor={field.name}
387
- className="text-sm font-medium text-gray-900 flex items-center gap-2"
388
- >
389
- {getFieldIcon(field.name)}
390
- {label}
391
- </Label>
392
-
393
- {field.type === 'boolean' && (
394
- <div className="flex items-center justify-between p-3 rounded-xl bg-gray-50">
395
- <span className="text-sm text-gray-500">
396
- {(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
397
- </span>
398
- <Switch
399
- id={field.name}
400
- checked={(formData[field.name] as boolean) || false}
401
- onCheckedChange={(checked) => updateField(field.name, checked)}
402
- className="data-[state=checked]:bg-emerald-500"
403
- />
404
- </div>
405
- )}
406
-
407
- {(field.type === 'text' || field.type === 'email') && (
408
- <Input
409
- id={field.name}
410
- type={field.type}
411
- value={(formData[field.name] as string) || ''}
412
- onChange={(e) => updateField(field.name, e.target.value)}
413
- placeholder={placeholder}
414
- className="rounded-xl"
415
- />
416
- )}
417
-
418
- {field.type === 'password' && (
419
- <Input
420
- id={field.name}
421
- type="password"
422
- value={(formData[field.name] as string) || ''}
423
- onChange={(e) => updateField(field.name, e.target.value)}
424
- placeholder={placeholder ?? t('leaveBlankToKeepUnchanged')}
425
- className="rounded-xl"
426
- />
427
- )}
428
-
429
- {field.type === 'number' && (
430
- <Input
431
- id={field.name}
432
- type="number"
433
- value={(formData[field.name] as number) || 0}
434
- onChange={(e) => updateField(field.name, parseInt(e.target.value) || 0)}
435
- placeholder={placeholder}
436
- className="rounded-xl"
437
- />
438
- )}
439
-
440
- {field.type === 'tags' && (
441
- <TagInput
442
- value={(formData[field.name] as string[]) || []}
443
- onChange={(tags) => updateField(field.name, tags)}
444
- />
445
- )}
446
-
447
- {field.type === 'select' && (
448
- <Select
449
- value={(formData[field.name] as string) || ''}
450
- onValueChange={(v) => updateField(field.name, v)}
451
- >
452
- <SelectTrigger className="rounded-xl">
453
- <SelectValue />
454
- </SelectTrigger>
455
- <SelectContent>
456
- {(field.options ?? []).map((option) => (
457
- <SelectItem key={option.value} value={option.value}>
458
- {option.label}
459
- </SelectItem>
460
- ))}
461
- </SelectContent>
462
- </Select>
463
- )}
464
-
465
- {field.type === 'json' && (
466
- <textarea
368
+ <StatusDot status={enabled ? 'active' : 'inactive'} label={enabled ? t('statusActive') : t('statusInactive')} />
369
+ </div>
370
+ </div>
371
+
372
+ <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
373
+ <div className="min-h-0 flex-1 space-y-5 overflow-y-auto px-6 py-5">
374
+ {fields.map((field) => {
375
+ const hint = channelName
376
+ ? hintForPath(`channels.${channelName}.${field.name}`, uiHints)
377
+ : undefined;
378
+ const label = hint?.label ?? field.label;
379
+ const placeholder = hint?.placeholder;
380
+
381
+ return (
382
+ <div key={field.name} className="space-y-2.5">
383
+ <Label
384
+ htmlFor={field.name}
385
+ className="flex items-center gap-2 text-sm font-medium text-gray-900"
386
+ >
387
+ {getFieldIcon(field.name)}
388
+ {label}
389
+ </Label>
390
+
391
+ {field.type === 'boolean' && (
392
+ <div className="flex items-center justify-between rounded-xl bg-gray-50 p-3">
393
+ <span className="text-sm text-gray-500">
394
+ {(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
395
+ </span>
396
+ <Switch
467
397
  id={field.name}
468
- value={jsonDrafts[field.name] ?? '{}'}
469
- onChange={(event) =>
470
- setJsonDrafts((prev) => ({
471
- ...prev,
472
- [field.name]: event.target.value
473
- }))
474
- }
475
- className="min-h-[120px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
398
+ checked={(formData[field.name] as boolean) || false}
399
+ onCheckedChange={(checked) => updateField(field.name, checked)}
400
+ className="data-[state=checked]:bg-emerald-500"
476
401
  />
477
- )}
478
- </div>
479
- );
480
- })}
481
- </div>
482
-
483
- <DialogFooter className="pt-4 flex-shrink-0">
484
- <Button
485
- type="button"
486
- variant="outline"
487
- onClick={closeChannelModal}
488
- >
489
- {t('cancel')}
490
- </Button>
491
- <Button
492
- type="submit"
493
- disabled={updateChannel.isPending || Boolean(runningActionId)}
494
- >
495
- {updateChannel.isPending ? t('saving') : t('save')}
496
- </Button>
497
- {actions
498
- .filter((action) => action.trigger === 'manual')
499
- .map((action) => (
500
- <Button
501
- key={action.id}
502
- type="button"
503
- onClick={() => handleManualAction(action)}
504
- disabled={updateChannel.isPending || Boolean(runningActionId)}
505
- variant="secondary"
506
- >
507
- {runningActionId === action.id ? t('connecting') : action.title}
508
- </Button>
509
- ))}
510
- </DialogFooter>
511
- </form>
512
- </DialogContent>
513
- </Dialog>
402
+ </div>
403
+ )}
404
+
405
+ {(field.type === 'text' || field.type === 'email') && (
406
+ <Input
407
+ id={field.name}
408
+ type={field.type}
409
+ value={(formData[field.name] as string) || ''}
410
+ onChange={(e) => updateField(field.name, e.target.value)}
411
+ placeholder={placeholder}
412
+ className="rounded-xl"
413
+ />
414
+ )}
415
+
416
+ {field.type === 'password' && (
417
+ <Input
418
+ id={field.name}
419
+ type="password"
420
+ value={(formData[field.name] as string) || ''}
421
+ onChange={(e) => updateField(field.name, e.target.value)}
422
+ placeholder={placeholder ?? t('leaveBlankToKeepUnchanged')}
423
+ className="rounded-xl"
424
+ />
425
+ )}
426
+
427
+ {field.type === 'number' && (
428
+ <Input
429
+ id={field.name}
430
+ type="number"
431
+ value={(formData[field.name] as number) || 0}
432
+ onChange={(e) => updateField(field.name, parseInt(e.target.value, 10) || 0)}
433
+ placeholder={placeholder}
434
+ className="rounded-xl"
435
+ />
436
+ )}
437
+
438
+ {field.type === 'tags' && (
439
+ <TagInput
440
+ value={(formData[field.name] as string[]) || []}
441
+ onChange={(tags) => updateField(field.name, tags)}
442
+ />
443
+ )}
444
+
445
+ {field.type === 'select' && (
446
+ <Select
447
+ value={(formData[field.name] as string) || ''}
448
+ onValueChange={(v) => updateField(field.name, v)}
449
+ >
450
+ <SelectTrigger className="rounded-xl">
451
+ <SelectValue />
452
+ </SelectTrigger>
453
+ <SelectContent>
454
+ {(field.options ?? []).map((option) => (
455
+ <SelectItem key={option.value} value={option.value}>
456
+ {option.label}
457
+ </SelectItem>
458
+ ))}
459
+ </SelectContent>
460
+ </Select>
461
+ )}
462
+
463
+ {field.type === 'json' && (
464
+ <textarea
465
+ id={field.name}
466
+ value={jsonDrafts[field.name] ?? '{}'}
467
+ onChange={(event) =>
468
+ setJsonDrafts((prev) => ({
469
+ ...prev,
470
+ [field.name]: event.target.value
471
+ }))
472
+ }
473
+ className="min-h-[120px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
474
+ />
475
+ )}
476
+ </div>
477
+ );
478
+ })}
479
+ </div>
480
+
481
+ <div className="flex flex-wrap items-center justify-end gap-2 border-t border-gray-100 px-6 py-4">
482
+ {actions
483
+ .filter((action) => action.trigger === 'manual')
484
+ .map((action) => (
485
+ <Button
486
+ key={action.id}
487
+ type="button"
488
+ onClick={() => handleManualAction(action)}
489
+ disabled={updateChannel.isPending || Boolean(runningActionId)}
490
+ variant="secondary"
491
+ >
492
+ {runningActionId === action.id ? t('connecting') : action.title}
493
+ </Button>
494
+ ))}
495
+ <Button type="submit" disabled={updateChannel.isPending || Boolean(runningActionId)}>
496
+ {updateChannel.isPending ? t('saving') : t('save')}
497
+ </Button>
498
+ </div>
499
+ </form>
500
+ </div>
514
501
  );
515
502
  }