xinyu-pro 0.21.0

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/.env.example +21 -0
  2. package/README.md +36 -0
  3. package/app/api/chat/route.ts +84 -0
  4. package/app/api/generate-svg/route.ts +171 -0
  5. package/app/api/generate-theme/route.ts +137 -0
  6. package/app/api/plugins/bindings/route.ts +173 -0
  7. package/app/api/plugins/export/route.ts +122 -0
  8. package/app/api/plugins/export-xye/route.ts +156 -0
  9. package/app/api/plugins/files/route.ts +146 -0
  10. package/app/api/plugins/files-list/route.ts +168 -0
  11. package/app/api/plugins/files-upload/route.ts +101 -0
  12. package/app/api/plugins/files-write/route.ts +272 -0
  13. package/app/api/plugins/import/route.ts +140 -0
  14. package/app/api/plugins/import-package/route.ts +231 -0
  15. package/app/api/plugins/resources/route.ts +109 -0
  16. package/app/api/plugins/route.ts +308 -0
  17. package/app/api/plugins/scan/route.ts +280 -0
  18. package/app/api/plugins/storage/route.ts +146 -0
  19. package/app/api/sessions/route.ts +165 -0
  20. package/app/api/settings/route.ts +40 -0
  21. package/app/api/suggest-fields/route.ts +129 -0
  22. package/app/api/templates/route.ts +159 -0
  23. package/app/api/test-api/route.ts +63 -0
  24. package/app/editor/page.tsx +1466 -0
  25. package/app/extensions/create/page.tsx +1422 -0
  26. package/app/extensions/edit/[id]/page.tsx +2342 -0
  27. package/app/extensions/page.tsx +1572 -0
  28. package/app/extensions/tutorial/page.tsx +4258 -0
  29. package/app/favicon.ico +0 -0
  30. package/app/fonts/GeistMonoVF.woff +0 -0
  31. package/app/fonts/GeistVF.woff +0 -0
  32. package/app/game/[id]/page.tsx +996 -0
  33. package/app/globals.css +3 -0
  34. package/app/layout.tsx +26 -0
  35. package/app/loading.tsx +26 -0
  36. package/app/page.tsx +345 -0
  37. package/app/settings/page.tsx +1490 -0
  38. package/bin/cli.js +262 -0
  39. package/components/ChatInput.tsx +106 -0
  40. package/components/ChatWindow.tsx +52 -0
  41. package/components/FullPageLoader.tsx +107 -0
  42. package/components/LoadingDots.tsx +20 -0
  43. package/components/MathCurveLoader.tsx +173 -0
  44. package/components/MessageBubble.tsx +147 -0
  45. package/components/WorldCardPreview.tsx +98 -0
  46. package/components/WorldCardUploader.tsx +58 -0
  47. package/components/ui/ConfirmDialog.tsx +135 -0
  48. package/components/ui/PageHeader.tsx +99 -0
  49. package/components/ui/PermissionConflictDialog.tsx +206 -0
  50. package/components/ui/PluginConfigForm.tsx +192 -0
  51. package/components/ui/PluginFloatingLayer.tsx +52 -0
  52. package/components/ui/PluginIcon.tsx +53 -0
  53. package/components/ui/PluginModalRenderer.tsx +185 -0
  54. package/components/ui/PluginProvider.tsx +1038 -0
  55. package/components/ui/PluginSlotRenderer.tsx +76 -0
  56. package/components/ui/ThemeCustomizer.tsx +174 -0
  57. package/components/ui/ThemeProvider.tsx +125 -0
  58. package/components/ui/ThemeSwitcher.tsx +140 -0
  59. package/components/ui/ToastProvider.tsx +141 -0
  60. package/lib/builtin-plugins.ts +11 -0
  61. package/lib/db-init.ts +35 -0
  62. package/lib/db.ts +244 -0
  63. package/lib/manifest-parser.ts +185 -0
  64. package/lib/parseWorldCard.ts +110 -0
  65. package/lib/plugin-dom-sandbox.ts +327 -0
  66. package/lib/plugin-events.ts +88 -0
  67. package/lib/plugin-files.ts +186 -0
  68. package/lib/plugin-html-sanitizer.ts +79 -0
  69. package/lib/plugin-resource-tracker.ts +175 -0
  70. package/lib/plugin-runtime.ts +2287 -0
  71. package/lib/plugin-security.ts +151 -0
  72. package/lib/plugin-types.ts +416 -0
  73. package/lib/prompt-builder.ts +55 -0
  74. package/lib/router-history.ts +119 -0
  75. package/lib/storage.ts +381 -0
  76. package/lib/themes.ts +129 -0
  77. package/lib/types.ts +117 -0
  78. package/lib/version.ts +55 -0
  79. package/next.config.mjs +43 -0
  80. package/package.json +56 -0
  81. package/plugins/xinyu.bag-system.xye +0 -0
  82. package/plugins/xinyu.cache-optimizer.xye +0 -0
  83. package/plugins/xinyu.dice-arbiter.xye +0 -0
  84. package/plugins/xinyu.game-auto-start-choices.xye +0 -0
  85. package/plugins/xinyu.markdown-render.xye +0 -0
  86. package/plugins/xinyu.slot-ui-beautify.xye +0 -0
  87. package/plugins/xinyu.world-info.xye +0 -0
  88. package/postcss.config.mjs +8 -0
  89. package/public/templates/atlantis.svg +63 -0
  90. package/public/templates/cyber-city.svg +68 -0
  91. package/public/templates/jianghu.svg +69 -0
  92. package/public/templates//351/255/224/346/263/225/344/270/226/347/225/214.svg +137 -0
  93. package/styles/themes.css +111 -0
  94. package/tailwind.config.ts +18 -0
  95. package/tsconfig.json +26 -0
  96. package/version.json +6 -0
@@ -0,0 +1,76 @@
1
+ // components/ui/PluginSlotRenderer.tsx - 插槽渲染器组件
2
+ // 负责渲染指定插槽中的所有插件注册内容
3
+ 'use client';
4
+
5
+ import React, { useCallback, useRef } from 'react';
6
+ import { SlotRegistration, UISlotId } from '@/lib/plugin-types';
7
+ import { getPluginRuntime } from '@/lib/plugin-runtime';
8
+
9
+ interface PluginSlotRendererProps {
10
+ slotId: UISlotId;
11
+ registrations: SlotRegistration[];
12
+ className?: string;
13
+ direction?: 'horizontal' | 'vertical';
14
+ gap?: string;
15
+ style?: React.CSSProperties;
16
+ }
17
+
18
+ export function PluginSlotRenderer({
19
+ slotId,
20
+ registrations,
21
+ className,
22
+ direction = 'horizontal',
23
+ gap = '8px',
24
+ style,
25
+ }: PluginSlotRendererProps) {
26
+ const containerRef = useRef<HTMLDivElement>(null);
27
+
28
+ // 事件委托:处理 data-action 属性的点击事件
29
+ const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
30
+ const target = e.target as HTMLElement;
31
+ const actionEl = target.closest<HTMLElement>('[data-action]');
32
+ if (!actionEl) return;
33
+
34
+ const actionName = actionEl.getAttribute('data-action');
35
+ const pluginId = actionEl.closest<HTMLElement>('[data-plugin-id]')?.getAttribute('data-plugin-id');
36
+ const payload = actionEl.getAttribute('data-payload') || undefined;
37
+
38
+ if (actionName && pluginId) {
39
+ e.preventDefault();
40
+ e.stopPropagation();
41
+ const runtime = getPluginRuntime();
42
+ runtime.dispatchAction(pluginId, actionName, actionEl, payload);
43
+ }
44
+ }, []);
45
+
46
+ // 始终渲染容器(即使没有注册内容),确保 getContainerElement 能找到它
47
+ // 空容器使用 display:contents 避免影响布局
48
+ const isEmpty = !registrations || registrations.length === 0;
49
+
50
+ return (
51
+ <div
52
+ ref={containerRef}
53
+ id={`xinyu-${slotId}`}
54
+ data-plugin-slot={slotId}
55
+ className={className}
56
+ onClick={handleClick}
57
+ style={{
58
+ display: isEmpty ? 'contents' : 'flex',
59
+ flexDirection: direction === 'vertical' ? 'column' : 'row',
60
+ gap,
61
+ alignItems: direction === 'vertical' ? 'stretch' : 'center',
62
+ flexWrap: direction === 'horizontal' ? 'wrap' : 'nowrap',
63
+ ...style,
64
+ }}
65
+ >
66
+ {registrations.map((reg) => (
67
+ <div
68
+ key={reg.id}
69
+ data-plugin-registration-id={reg.id}
70
+ data-plugin-id={reg.pluginId}
71
+ dangerouslySetInnerHTML={{ __html: reg.content }}
72
+ />
73
+ ))}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,174 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+ import { useTheme } from './ThemeProvider';
5
+ import { Theme } from '@/lib/types';
6
+
7
+ export function ThemeCustomizer() {
8
+ const { activeTheme, setCustomTheme, resetToPreset } = useTheme();
9
+ const [isOpen, setIsOpen] = useState(false);
10
+ const [localVars, setLocalVars] = useState<Record<string, string>>({});
11
+
12
+ useEffect(() => {
13
+ setLocalVars({ ...activeTheme.variables });
14
+ }, [activeTheme]);
15
+
16
+ const handleVarChange = (key: string, value: string) => {
17
+ setLocalVars((prev) => ({ ...prev, [key]: value }));
18
+ };
19
+
20
+ const handleApply = () => {
21
+ const customTheme: Theme = {
22
+ id: 'custom',
23
+ name: '自定义主题',
24
+ description: '用户自定义主题',
25
+ isDark: activeTheme.isDark,
26
+ variables: localVars,
27
+ };
28
+ setCustomTheme(customTheme);
29
+ setIsOpen(false);
30
+ };
31
+
32
+ const handleReset = () => {
33
+ setLocalVars({ ...activeTheme.variables });
34
+ };
35
+
36
+ // 可修改的变量配置
37
+ const editableVars = [
38
+ { key: '--color-bg-primary', label: '主背景色', type: 'color' },
39
+ { key: '--color-bg-secondary', label: '次级背景色', type: 'color' },
40
+ { key: '--color-text-primary', label: '主文字色', type: 'color' },
41
+ { key: '--color-accent', label: '强调色', type: 'color' },
42
+ { key: '--color-accent-hover', label: '强调色悬停', type: 'color' },
43
+ { key: '--color-user-bubble', label: '用户气泡色', type: 'color' },
44
+ { key: '--color-ai-bubble', label: 'AI 气泡色', type: 'color' },
45
+ { key: '--color-border', label: '边框色', type: 'color' },
46
+ { key: '--border-radius', label: '圆角大小', type: 'text' },
47
+ ];
48
+
49
+ return (
50
+ <div className="relative">
51
+ <button
52
+ onClick={() => setIsOpen(!isOpen)}
53
+ className="flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors"
54
+ style={{
55
+ backgroundColor: 'var(--color-bg-tertiary)',
56
+ borderColor: 'var(--color-border)',
57
+ color: 'var(--color-text-primary)',
58
+ }}
59
+ title="自定义主题"
60
+ >
61
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
62
+ <path d="M12 3a9 9 0 0 0 9 9" />
63
+ <path d="M12 3a9 9 0 0 1 9 9" />
64
+ <path d="M12 3v9h9" />
65
+ <circle cx="12" cy="12" r="2" />
66
+ </svg>
67
+ <span className="text-sm hidden sm:inline">自定义</span>
68
+ </button>
69
+
70
+ {isOpen && (
71
+ <>
72
+ <div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
73
+ <div
74
+ className="fixed right-4 top-20 w-100 max-h-[80vh] overflow-y-auto rounded-lg border shadow-xl z-50 p-4"
75
+ style={{
76
+ backgroundColor: 'var(--color-bg-secondary)',
77
+ borderColor: 'var(--color-border)',
78
+ boxShadow: '0 8px 32px var(--color-shadow)',
79
+ }}
80
+ >
81
+ <h3
82
+ className="text-lg font-bold mb-4"
83
+ style={{ color: 'var(--color-text-primary)' }}
84
+ >
85
+ 自定义主题
86
+ </h3>
87
+
88
+ <div className="space-y-3">
89
+ {editableVars.map(({ key, label, type }) => (
90
+ <div key={key} className="flex items-center gap-3">
91
+ <label
92
+ className="text-sm w-24 shrink-0"
93
+ style={{ color: 'var(--color-text-secondary)' }}
94
+ >
95
+ {label}
96
+ </label>
97
+ {type === 'color' ? (
98
+ <div className="flex items-center gap-2 flex-1">
99
+ <input
100
+ type="color"
101
+ value={localVars[key] || '#000000'}
102
+ onChange={(e) => handleVarChange(key, e.target.value)}
103
+ className="w-8 h-8 rounded cursor-pointer border-0 p-0"
104
+ />
105
+ <input
106
+ type="text"
107
+ value={localVars[key] || ''}
108
+ onChange={(e) => handleVarChange(key, e.target.value)}
109
+ className="flex-1 px-2 py-1 rounded text-sm border"
110
+ style={{
111
+ backgroundColor: 'var(--color-bg-tertiary)',
112
+ borderColor: 'var(--color-border)',
113
+ color: 'var(--color-text-primary)',
114
+ }}
115
+ />
116
+ </div>
117
+ ) : (
118
+ <input
119
+ type="text"
120
+ value={localVars[key] || ''}
121
+ onChange={(e) => handleVarChange(key, e.target.value)}
122
+ className="flex-1 px-2 py-1 rounded text-sm border"
123
+ style={{
124
+ backgroundColor: 'var(--color-bg-tertiary)',
125
+ borderColor: 'var(--color-border)',
126
+ color: 'var(--color-text-primary)',
127
+ }}
128
+ />
129
+ )}
130
+ </div>
131
+ ))}
132
+ </div>
133
+
134
+ <div className="flex gap-2 mt-4">
135
+ <button
136
+ onClick={handleApply}
137
+ className="flex-1 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
138
+ style={{
139
+ backgroundColor: 'var(--color-accent)',
140
+ color: activeTheme.isDark ? '#000' : '#fff',
141
+ }}
142
+ >
143
+ 应用
144
+ </button>
145
+ <button
146
+ onClick={handleReset}
147
+ className="px-4 py-2 rounded-lg text-sm border transition-colors"
148
+ style={{
149
+ borderColor: 'var(--color-border)',
150
+ color: 'var(--color-text-secondary)',
151
+ }}
152
+ >
153
+ 重置
154
+ </button>
155
+ <button
156
+ onClick={() => {
157
+ resetToPreset('deep-night');
158
+ setIsOpen(false);
159
+ }}
160
+ className="px-4 py-2 rounded-lg text-sm border transition-colors"
161
+ style={{
162
+ borderColor: 'var(--color-border)',
163
+ color: 'var(--color-text-secondary)',
164
+ }}
165
+ >
166
+ 恢复默认
167
+ </button>
168
+ </div>
169
+ </div>
170
+ </>
171
+ )}
172
+ </div>
173
+ );
174
+ }
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
4
+ import { Theme, ThemeConfig } from '@/lib/types';
5
+ import { presetThemes, findThemeById, getDefaultThemeConfig } from '@/lib/themes';
6
+ import { getThemeConfig, saveThemeConfig } from '@/lib/storage';
7
+
8
+ interface ThemeContextValue {
9
+ config: ThemeConfig;
10
+ activeTheme: Theme;
11
+ setActiveThemeId: (id: string) => void;
12
+ setCustomTheme: (theme: Theme) => void;
13
+ resetToPreset: (id: string) => void;
14
+ addImportedTheme: (theme: Theme) => void;
15
+ removeImportedTheme: (id: string) => void;
16
+ }
17
+
18
+ const ThemeContext = createContext<ThemeContextValue | null>(null);
19
+
20
+ export function useTheme(): ThemeContextValue {
21
+ const ctx = useContext(ThemeContext);
22
+ if (!ctx) {
23
+ throw new Error('useTheme must be used within a ThemeProvider');
24
+ }
25
+ return ctx;
26
+ }
27
+
28
+ /** 将主题变量应用到 document.documentElement */
29
+ function applyThemeVariables(variables: Record<string, string>) {
30
+ const root = document.documentElement;
31
+ Object.entries(variables).forEach(([key, value]) => {
32
+ root.style.setProperty(key, value);
33
+ });
34
+ }
35
+
36
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
37
+ const [config, setConfig] = useState<ThemeConfig>(getDefaultThemeConfig());
38
+ const [mounted, setMounted] = useState(false);
39
+
40
+ // 初始化:从 localStorage 读取主题
41
+ useEffect(() => {
42
+ const stored = getThemeConfig();
43
+ // 合并预设主题(防止旧版本缺少新主题)
44
+ const mergedConfig: ThemeConfig = {
45
+ ...stored,
46
+ presets: presetThemes,
47
+ importedThemes: stored.importedThemes || [],
48
+ };
49
+ setConfig(mergedConfig);
50
+ setMounted(true);
51
+ }, []);
52
+
53
+ // 应用主题变量到 DOM
54
+ useEffect(() => {
55
+ if (!mounted) return;
56
+ const theme = findThemeById(config, config.activeThemeId);
57
+ if (theme) {
58
+ applyThemeVariables(theme.variables);
59
+ }
60
+ }, [config, mounted]);
61
+
62
+ const setActiveThemeId = useCallback((id: string) => {
63
+ setConfig((prev) => {
64
+ const updated = { ...prev, activeThemeId: id };
65
+ saveThemeConfig(updated);
66
+ return updated;
67
+ });
68
+ }, []);
69
+
70
+ const setCustomTheme = useCallback((theme: Theme) => {
71
+ setConfig((prev) => {
72
+ const updated = { ...prev, customTheme: theme, activeThemeId: 'custom' };
73
+ saveThemeConfig(updated);
74
+ return updated;
75
+ });
76
+ }, []);
77
+
78
+ const resetToPreset = useCallback((id: string) => {
79
+ setConfig((prev) => {
80
+ const updated = { ...prev, activeThemeId: id };
81
+ saveThemeConfig(updated);
82
+ return updated;
83
+ });
84
+ }, []);
85
+
86
+ const addImportedTheme = useCallback((theme: Theme) => {
87
+ setConfig((prev) => {
88
+ // 去重:如果已有相同 id 则替换
89
+ const filtered = (prev.importedThemes || []).filter(t => t.id !== theme.id);
90
+ const updated = {
91
+ ...prev,
92
+ importedThemes: [...filtered, theme],
93
+ activeThemeId: theme.id,
94
+ };
95
+ saveThemeConfig(updated);
96
+ return updated;
97
+ });
98
+ }, []);
99
+
100
+ const removeImportedTheme = useCallback((id: string) => {
101
+ setConfig((prev) => {
102
+ const filtered = (prev.importedThemes || []).filter(t => t.id !== id);
103
+ const updated = {
104
+ ...prev,
105
+ importedThemes: filtered,
106
+ activeThemeId: prev.activeThemeId === id ? 'deep-night' : prev.activeThemeId,
107
+ };
108
+ saveThemeConfig(updated);
109
+ return updated;
110
+ });
111
+ }, []);
112
+
113
+ const activeTheme = findThemeById(config, config.activeThemeId) || presetThemes[0];
114
+
115
+ // 防止闪烁:未挂载时隐藏内容(保持 DOM 结构完整,避免子组件 remount)
116
+ return (
117
+ <ThemeContext.Provider
118
+ value={{ config, activeTheme, setActiveThemeId, setCustomTheme, resetToPreset, addImportedTheme, removeImportedTheme }}
119
+ >
120
+ <div style={mounted ? undefined : { visibility: 'hidden' }}>
121
+ {children}
122
+ </div>
123
+ </ThemeContext.Provider>
124
+ );
125
+ }
@@ -0,0 +1,140 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { useTheme } from './ThemeProvider';
5
+
6
+ export function ThemeSwitcher({ buttonStyle = {} }: { buttonStyle?: React.CSSProperties }) {
7
+ const { config, activeTheme, setActiveThemeId } = useTheme();
8
+ const [isOpen, setIsOpen] = useState(false);
9
+
10
+ return (
11
+ <div className="relative">
12
+ <button
13
+ onClick={() => setIsOpen(!isOpen)}
14
+ className="flex items-center gap-2 px-3 py-2 rounded-lg border transition-colors"
15
+ style={{
16
+ backgroundColor: 'var(--color-bg-tertiary)',
17
+ borderColor: 'var(--color-border)',
18
+ color: 'var(--color-text-primary)',
19
+ ...buttonStyle,
20
+ }}
21
+ title="切换主题"
22
+ >
23
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
24
+ <circle cx="12" cy="12" r="5" />
25
+ <path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
26
+ </svg>
27
+ <span className="text-sm hidden sm:inline">{activeTheme.name}</span>
28
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
29
+ <path d="M6 9l6 6 6-6" />
30
+ </svg>
31
+ </button>
32
+
33
+ {isOpen && (
34
+ <>
35
+ {/* 遮罩层 */}
36
+ <div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
37
+ {/* 下拉菜单 */}
38
+ <div
39
+ className="absolute right-0 top-full mt-2 w-56 rounded-lg border shadow-lg z-50 overflow-hidden"
40
+ style={{
41
+ backgroundColor: 'var(--color-bg-secondary)',
42
+ borderColor: 'var(--color-border)',
43
+ boxShadow: '0 4px 24px var(--color-shadow)',
44
+ }}
45
+ >
46
+ <div className="p-2">
47
+ <p
48
+ className="text-xs font-medium px-3 py-1 mb-1"
49
+ style={{ color: 'var(--color-text-muted)' }}
50
+ >
51
+ 预设主题
52
+ </p>
53
+ {config.presets.map((theme) => (
54
+ <button
55
+ key={theme.id}
56
+ onClick={() => {
57
+ setActiveThemeId(theme.id);
58
+ setIsOpen(false);
59
+ }}
60
+ className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors"
61
+ style={{
62
+ backgroundColor:
63
+ activeTheme.id === theme.id ? 'var(--color-bg-tertiary)' : 'transparent',
64
+ color: 'var(--color-text-primary)',
65
+ }}
66
+ >
67
+ {/* 颜色预览 */}
68
+ <div className="flex gap-1">
69
+ <span
70
+ className="w-4 h-4 rounded-full border"
71
+ style={{
72
+ backgroundColor: theme.variables['--color-bg-primary'],
73
+ borderColor: theme.variables['--color-border'],
74
+ }}
75
+ />
76
+ <span
77
+ className="w-4 h-4 rounded-full"
78
+ style={{ backgroundColor: theme.variables['--color-accent'] }}
79
+ />
80
+ </div>
81
+ <div>
82
+ <div className="text-sm font-medium">{theme.name}</div>
83
+ <div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
84
+ {theme.description}
85
+ </div>
86
+ </div>
87
+ {activeTheme.id === theme.id && (
88
+ <svg className="ml-auto" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
89
+ <path d="M20 6L9 17l-5-5" />
90
+ </svg>
91
+ )}
92
+ </button>
93
+ ))}
94
+
95
+ {config.customTheme && (
96
+ <>
97
+ <div className="my-1 border-t" style={{ borderColor: 'var(--color-border)' }} />
98
+ <button
99
+ onClick={() => {
100
+ setActiveThemeId('custom');
101
+ setIsOpen(false);
102
+ }}
103
+ className="w-full flex items-center gap-3 px-3 py-2 rounded-md text-left transition-colors"
104
+ style={{
105
+ backgroundColor:
106
+ activeTheme.id === 'custom' ? 'var(--color-bg-tertiary)' : 'transparent',
107
+ color: 'var(--color-text-primary)',
108
+ }}
109
+ >
110
+ <div className="flex gap-1">
111
+ <span
112
+ className="w-4 h-4 rounded-full border"
113
+ style={{
114
+ backgroundColor: config.customTheme.variables['--color-bg-primary'],
115
+ borderColor: config.customTheme.variables['--color-border'],
116
+ }}
117
+ />
118
+ <span
119
+ className="w-4 h-4 rounded-full"
120
+ style={{ backgroundColor: config.customTheme.variables['--color-accent'] }}
121
+ />
122
+ </div>
123
+ <div>
124
+ <div className="text-sm font-medium">自定义主题</div>
125
+ </div>
126
+ {activeTheme.id === 'custom' && (
127
+ <svg className="ml-auto" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
128
+ <path d="M20 6L9 17l-5-5" />
129
+ </svg>
130
+ )}
131
+ </button>
132
+ </>
133
+ )}
134
+ </div>
135
+ </div>
136
+ </>
137
+ )}
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useState, useCallback, useRef } from 'react';
4
+
5
+ // ==================== 类型定义 ====================
6
+
7
+ export type ToastType = 'info' | 'success' | 'error' | 'warning';
8
+
9
+ interface Toast {
10
+ id: string;
11
+ message: string;
12
+ type: ToastType;
13
+ duration: number;
14
+ }
15
+
16
+ interface ToastContextValue {
17
+ toast: (message: string, type?: ToastType, duration?: number) => void;
18
+ }
19
+
20
+ // ==================== Context ====================
21
+
22
+ const ToastContext = createContext<ToastContextValue>({ toast: () => {} });
23
+
24
+ export function useToast() {
25
+ return useContext(ToastContext);
26
+ }
27
+
28
+ // ==================== 图标 ====================
29
+
30
+ const ICONS: Record<ToastType, string> = {
31
+ info: 'fa-solid fa-circle-info',
32
+ success: 'fa-solid fa-circle-check',
33
+ error: 'fa-solid fa-circle-xmark',
34
+ warning: 'fa-solid fa-triangle-exclamation',
35
+ };
36
+
37
+ const COLORS: Record<ToastType, string> = {
38
+ info: 'var(--color-accent)',
39
+ success: '#22c55e',
40
+ error: '#ef4444',
41
+ warning: '#f59e0b',
42
+ };
43
+
44
+ // ==================== 单条 Toast ====================
45
+
46
+ function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: (id: string) => void }) {
47
+ const [isLeaving, setIsLeaving] = useState(false);
48
+ const timerRef = useRef<ReturnType<typeof setTimeout>>();
49
+
50
+ const handleDismiss = useCallback(() => {
51
+ setIsLeaving(true);
52
+ setTimeout(() => onDismiss(toast.id), 300);
53
+ }, [toast.id, onDismiss]);
54
+
55
+ // 自动消失
56
+ React.useEffect(() => {
57
+ timerRef.current = setTimeout(handleDismiss, toast.duration);
58
+ return () => clearTimeout(timerRef.current);
59
+ }, [toast.duration, handleDismiss]);
60
+
61
+ // info 类型使用 CSS 变量,需要用 rgba 方式添加透明度;其他类型直接拼接 hex 透明度
62
+ const color = COLORS[toast.type];
63
+ const isInfo = toast.type === 'info';
64
+ const borderColor = isInfo ? `color-mix(in srgb, ${color} 19%, transparent)` : `${color}30`;
65
+ const shadowColor = isInfo ? `color-mix(in srgb, ${color} 8%, transparent)` : `${color}15`;
66
+
67
+ return (
68
+ <div
69
+ onClick={handleDismiss}
70
+ className="flex items-center gap-2 px-4 py-2.5 rounded-lg shadow-lg cursor-pointer select-none"
71
+ style={{
72
+ backgroundColor: 'var(--color-bg-secondary)',
73
+ border: `1px solid ${borderColor}`,
74
+ boxShadow: `0 4px 16px var(--color-shadow), 0 0 0 1px ${shadowColor}`,
75
+ transform: isLeaving ? 'translateY(100%) scale(0.95)' : 'translateY(0) scale(1)',
76
+ opacity: isLeaving ? 0 : 1,
77
+ transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease',
78
+ maxWidth: '400px',
79
+ }}
80
+ >
81
+ <i
82
+ className={ICONS[toast.type]}
83
+ style={{ color: COLORS[toast.type], fontSize: '14px', flexShrink: 0 }}
84
+ />
85
+ <span
86
+ className="text-sm flex-1"
87
+ style={{ color: 'var(--color-text-primary)' }}
88
+ >
89
+ {toast.message}
90
+ </span>
91
+ <i
92
+ className="fa-solid fa-xmark"
93
+ style={{ color: 'var(--color-text-muted)', fontSize: '10px', flexShrink: 0, opacity: 0.5 }}
94
+ />
95
+ </div>
96
+ );
97
+ }
98
+
99
+ // ==================== Toast 容器 ====================
100
+
101
+ function ToastContainer({ toasts, onDismiss }: { toasts: Toast[]; onDismiss: (id: string) => void }) {
102
+ if (toasts.length === 0) return null;
103
+
104
+ return (
105
+ <div
106
+ className="fixed bottom-4 left-1/2 z-50 flex flex-col-reverse items-center gap-2"
107
+ style={{ transform: 'translateX(-50%)' }}
108
+ >
109
+ {toasts.map((t) => (
110
+ <ToastItem key={t.id} toast={t} onDismiss={onDismiss} />
111
+ ))}
112
+ </div>
113
+ );
114
+ }
115
+
116
+ // ==================== Provider ====================
117
+
118
+ let toastIdCounter = 0;
119
+
120
+ export function ToastProvider({ children }: { children: React.ReactNode }) {
121
+ const [toasts, setToasts] = useState<Toast[]>([]);
122
+
123
+ const dismiss = useCallback((id: string) => {
124
+ setToasts((prev) => prev.filter((t) => t.id !== id));
125
+ }, []);
126
+
127
+ const toast = useCallback(
128
+ (message: string, type: ToastType = 'info', duration: number = 3000) => {
129
+ const id = `toast_${++toastIdCounter}`;
130
+ setToasts((prev) => [...prev, { id, message, type, duration }]);
131
+ },
132
+ []
133
+ );
134
+
135
+ return (
136
+ <ToastContext.Provider value={{ toast }}>
137
+ {children}
138
+ <ToastContainer toasts={toasts} onDismiss={dismiss} />
139
+ </ToastContext.Provider>
140
+ );
141
+ }
@@ -0,0 +1,11 @@
1
+ // lib/builtin-plugins.ts - 内置插件定义(当前无内置插件)
2
+
3
+ import { PluginManifest } from './plugin-types';
4
+
5
+ /** 所有内置插件定义 */
6
+ export const builtinPlugins: PluginManifest[] = [];
7
+
8
+ /** 安装内置插件到数据库 */
9
+ export async function installBuiltinPlugins(): Promise<void> {
10
+ // 无内置插件
11
+ }