@nextclaw/ui 0.5.35 → 0.5.37

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 (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/assets/ChannelsList-DtvhbEV9.js +1 -0
  3. package/dist/assets/{ChatPage-C_wANEY9.js → ChatPage-Bw_aXB4R.js} +2 -2
  4. package/dist/assets/CronConfig-BZLXcDbm.js +1 -0
  5. package/dist/assets/{DocBrowser-CdX5oDgu.js → DocBrowser-BY0TiFOc.js} +1 -1
  6. package/dist/assets/MarketplacePage-BDlAw7fO.js +1 -0
  7. package/dist/assets/{ModelConfig-DagIPD4R.js → ModelConfig-Bi8Q4_NG.js} +1 -1
  8. package/dist/assets/ProvidersList-D2OB0siE.js +1 -0
  9. package/dist/assets/{RuntimeConfig-BDyqfVSA.js → RuntimeConfig-Bz9aUkwu.js} +1 -1
  10. package/dist/assets/{SecretsConfig-De2IZ7GX.js → SecretsConfig-Bqi-biOL.js} +1 -1
  11. package/dist/assets/SessionsConfig-DcWT2QvI.js +2 -0
  12. package/dist/assets/{card-CWG4Tz0Y.js → card-DwZkVl7S.js} +1 -1
  13. package/dist/assets/index-C1NAfZSm.js +2 -0
  14. package/dist/assets/index-DWgSvrx4.css +1 -0
  15. package/dist/assets/{label-LbWa2Yzc.js → label-BBDuC6Nm.js} +1 -1
  16. package/dist/assets/logos-DMFt4YDI.js +1 -0
  17. package/dist/assets/{page-layout-Bz8CAEiD.js → page-layout-hPFzCUTQ.js} +1 -1
  18. package/dist/assets/{switch-BwbfObfA.js → switch-CwkcbkEs.js} +1 -1
  19. package/dist/assets/{tabs-custom-VeX6BYro.js → tabs-custom-TUrWRyYy.js} +1 -1
  20. package/dist/assets/{useConfig-CFFZ66EV.js → useConfig-DZVUrqQz.js} +1 -1
  21. package/dist/assets/useConfirmDialog-D5X0Iqid.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 +170 -180
  27. package/src/components/config/ChannelsList.tsx +131 -93
  28. package/src/components/config/ProviderForm.tsx +3 -2
  29. package/src/components/config/ProvidersList.tsx +3 -2
  30. package/src/components/config/config-layout.ts +10 -0
  31. package/src/lib/i18n.ts +6 -0
  32. package/dist/assets/ChannelsList-DGoIQT1t.js +0 -1
  33. package/dist/assets/CronConfig-Q7faThLl.js +0 -1
  34. package/dist/assets/MarketplacePage-DXoPkFYk.js +0 -1
  35. package/dist/assets/ProvidersList-2aHarRNe.js +0 -1
  36. package/dist/assets/SessionsConfig-BK0xx6EF.js +0 -2
  37. package/dist/assets/dialog-ssdjbutm.js +0 -5
  38. package/dist/assets/index-B8Wh_FvS.css +0 -1
  39. package/dist/assets/index-q2B1bssI.js +0 -2
  40. package/dist/assets/logos-DncMldHC.js +0 -1
  41. 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-C1NAfZSm.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-DN_iJQc4.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-DWgSvrx4.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.37",
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,31 @@
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';
19
+ import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
24
20
 
25
21
  type ChannelFieldType = 'boolean' | 'text' | 'email' | 'password' | 'number' | 'tags' | 'select' | 'json';
26
22
  type ChannelOption = { value: string; label: string };
27
23
  type ChannelField = { name: string; type: ChannelFieldType; label: string; options?: ChannelOption[] };
28
24
 
25
+ type ChannelFormProps = {
26
+ channelName?: string;
27
+ };
28
+
29
29
  const DM_POLICY_OPTIONS: ChannelOption[] = [
30
30
  { value: 'pairing', label: 'pairing' },
31
31
  { value: 'allowlist', label: 'allowlist' },
@@ -46,7 +46,6 @@ const STREAMING_MODE_OPTIONS: ChannelOption[] = [
46
46
  { value: 'progress', label: 'progress' }
47
47
  ];
48
48
 
49
- // Field icon mapping
50
49
  const getFieldIcon = (fieldName: string) => {
51
50
  if (fieldName.includes('token') || fieldName.includes('secret') || fieldName.includes('password')) {
52
51
  return <KeyRound className="h-3.5 w-3.5 text-gray-500" />;
@@ -164,20 +163,6 @@ function buildChannelFields(): Record<string, ChannelField[]> {
164
163
  };
165
164
  }
166
165
 
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
166
  function isRecord(value: unknown): value is Record<string, unknown> {
182
167
  return typeof value === 'object' && value !== null && !Array.isArray(value);
183
168
  }
@@ -208,8 +193,7 @@ function buildScopeDraft(scope: string, value: Record<string, unknown>): Record<
208
193
  return output;
209
194
  }
210
195
 
211
- export function ChannelForm() {
212
- const { channelModal, closeChannelModal } = useUiStore();
196
+ export function ChannelForm({ channelName }: ChannelFormProps) {
213
197
  const { data: config } = useConfig();
214
198
  const { data: meta } = useConfigMeta();
215
199
  const { data: schema } = useConfigSchema();
@@ -220,7 +204,6 @@ export function ChannelForm() {
220
204
  const [jsonDrafts, setJsonDrafts] = useState<Record<string, string>>({});
221
205
  const [runningActionId, setRunningActionId] = useState<string | null>(null);
222
206
 
223
- const channelName = channelModal.channel;
224
207
  const channelConfig = channelName ? config?.channels[channelName] : null;
225
208
  const fields = channelName ? buildChannelFields()[channelName] ?? [] : [];
226
209
  const uiHints = schema?.uiHints;
@@ -282,10 +265,7 @@ export function ChannelForm() {
282
265
  }
283
266
  }
284
267
 
285
- updateChannel.mutate(
286
- { channel: channelName, data: payload },
287
- { onSuccess: () => closeChannelModal() }
288
- );
268
+ updateChannel.mutate({ channel: channelName, data: payload });
289
269
  };
290
270
 
291
271
  const applyActionPatchToForm = (patch?: Record<string, unknown>) => {
@@ -344,156 +324,163 @@ export function ChannelForm() {
344
324
  }
345
325
  };
346
326
 
347
- const Icon = channelIcons[channelName || ''] || channelIcons.default;
348
- const gradientClass = channelColors[channelName || ''] || channelColors.default;
327
+ if (!channelName || !channelMeta || !channelConfig) {
328
+ return (
329
+ <div className={CONFIG_EMPTY_DETAIL_CARD_CLASS}>
330
+ <div>
331
+ <h3 className="text-base font-semibold text-gray-900">{t('channelsSelectTitle')}</h3>
332
+ <p className="mt-2 text-sm text-gray-500">{t('channelsSelectDescription')}</p>
333
+ </div>
334
+ </div>
335
+ );
336
+ }
337
+
338
+ const enabled = Boolean(channelConfig.enabled);
349
339
 
350
340
  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
- )}
341
+ <div className={CONFIG_DETAIL_CARD_CLASS}>
342
+ <div className="border-b border-gray-100 px-6 py-5">
343
+ <div className="flex flex-wrap items-center justify-between gap-3">
344
+ <div className="min-w-0">
345
+ <div className="flex items-center gap-3">
346
+ <LogoBadge
347
+ name={channelName}
348
+ src={getChannelLogo(channelName)}
349
+ className={cn(
350
+ 'h-9 w-9 rounded-lg border',
351
+ enabled ? 'border-primary/30 bg-white' : 'border-gray-200/70 bg-white'
352
+ )}
353
+ imgClassName="h-5 w-5 object-contain"
354
+ fallback={<span className="text-sm font-semibold uppercase text-gray-500">{channelName[0]}</span>}
355
+ />
356
+ <h3 className="truncate text-lg font-semibold text-gray-900 capitalize">{channelLabel}</h3>
370
357
  </div>
358
+ <p className="mt-2 text-sm text-gray-500">{t('channelsFormDescription')}</p>
359
+ {tutorialUrl && (
360
+ <a
361
+ href={tutorialUrl}
362
+ className="mt-2 inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover"
363
+ >
364
+ <BookOpen className="h-3.5 w-3.5" />
365
+ {t('channelsGuideTitle')}
366
+ </a>
367
+ )}
371
368
  </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
369
+ <StatusDot status={enabled ? 'active' : 'inactive'} label={enabled ? t('statusActive') : t('statusInactive')} />
370
+ </div>
371
+ </div>
372
+
373
+ <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
374
+ <div className="min-h-0 flex-1 space-y-6 overflow-y-auto px-6 py-5">
375
+ {fields.map((field) => {
376
+ const hint = channelName
377
+ ? hintForPath(`channels.${channelName}.${field.name}`, uiHints)
378
+ : undefined;
379
+ const label = hint?.label ?? field.label;
380
+ const placeholder = hint?.placeholder;
381
+
382
+ return (
383
+ <div key={field.name} className="space-y-2.5">
384
+ <Label
385
+ htmlFor={field.name}
386
+ className="flex items-center gap-2 text-sm font-medium text-gray-900"
387
+ >
388
+ {getFieldIcon(field.name)}
389
+ {label}
390
+ </Label>
391
+
392
+ {field.type === 'boolean' && (
393
+ <div className="flex items-center justify-between rounded-xl bg-gray-50 p-3">
394
+ <span className="text-sm text-gray-500">
395
+ {(formData[field.name] as boolean) ? t('enabled') : t('disabled')}
396
+ </span>
397
+ <Switch
467
398
  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"
399
+ checked={(formData[field.name] as boolean) || false}
400
+ onCheckedChange={(checked) => updateField(field.name, checked)}
401
+ className="data-[state=checked]:bg-emerald-500"
476
402
  />
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>
403
+ </div>
404
+ )}
405
+
406
+ {(field.type === 'text' || field.type === 'email') && (
407
+ <Input
408
+ id={field.name}
409
+ type={field.type}
410
+ value={(formData[field.name] as string) || ''}
411
+ onChange={(e) => updateField(field.name, e.target.value)}
412
+ placeholder={placeholder}
413
+ className="rounded-xl"
414
+ />
415
+ )}
416
+
417
+ {field.type === 'password' && (
418
+ <Input
419
+ id={field.name}
420
+ type="password"
421
+ value={(formData[field.name] as string) || ''}
422
+ onChange={(e) => updateField(field.name, e.target.value)}
423
+ placeholder={placeholder ?? t('leaveBlankToKeepUnchanged')}
424
+ className="rounded-xl"
425
+ />
426
+ )}
427
+
428
+ {field.type === 'number' && (
429
+ <Input
430
+ id={field.name}
431
+ type="number"
432
+ value={(formData[field.name] as number) || 0}
433
+ onChange={(e) => updateField(field.name, parseInt(e.target.value, 10) || 0)}
434
+ placeholder={placeholder}
435
+ className="rounded-xl"
436
+ />
437
+ )}
438
+
439
+ {field.type === 'tags' && (
440
+ <TagInput
441
+ value={(formData[field.name] as string[]) || []}
442
+ onChange={(tags) => updateField(field.name, tags)}
443
+ />
444
+ )}
445
+
446
+ {field.type === 'select' && (
447
+ <Select
448
+ value={(formData[field.name] as string) || ''}
449
+ onValueChange={(v) => updateField(field.name, v)}
450
+ >
451
+ <SelectTrigger className="rounded-xl">
452
+ <SelectValue />
453
+ </SelectTrigger>
454
+ <SelectContent>
455
+ {(field.options ?? []).map((option) => (
456
+ <SelectItem key={option.value} value={option.value}>
457
+ {option.label}
458
+ </SelectItem>
459
+ ))}
460
+ </SelectContent>
461
+ </Select>
462
+ )}
463
+
464
+ {field.type === 'json' && (
465
+ <textarea
466
+ id={field.name}
467
+ value={jsonDrafts[field.name] ?? '{}'}
468
+ onChange={(event) =>
469
+ setJsonDrafts((prev) => ({
470
+ ...prev,
471
+ [field.name]: event.target.value
472
+ }))
473
+ }
474
+ className="min-h-[120px] w-full resize-none rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
475
+ />
476
+ )}
477
+ </div>
478
+ );
479
+ })}
480
+ </div>
481
+
482
+ <div className="flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4">
483
+ <div className="flex flex-wrap items-center gap-2">
497
484
  {actions
498
485
  .filter((action) => action.trigger === 'manual')
499
486
  .map((action) => (
@@ -507,9 +494,12 @@ export function ChannelForm() {
507
494
  {runningActionId === action.id ? t('connecting') : action.title}
508
495
  </Button>
509
496
  ))}
510
- </DialogFooter>
511
- </form>
512
- </DialogContent>
513
- </Dialog>
497
+ </div>
498
+ <Button type="submit" disabled={updateChannel.isPending || Boolean(runningActionId)}>
499
+ {updateChannel.isPending ? t('saving') : t('save')}
500
+ </Button>
501
+ </div>
502
+ </form>
503
+ </div>
514
504
  );
515
505
  }