@pheem49/mint 1.7.0 → 1.8.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 (55) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/crates/mint-cli/src/agent.rs +1 -1
  5. package/crates/mint-cli/src/main.rs +47 -24
  6. package/crates/mint-cli/src/setup.rs +1 -1
  7. package/crates/mint-core/src/api_server.rs +64 -10
  8. package/crates/mint-core/src/chat.rs +79 -0
  9. package/crates/mint-core/src/lib.rs +1 -1
  10. package/crates/mint-core/src/plugins.rs +4 -1
  11. package/crates/mint-core/src/workflows.rs +8 -0
  12. package/package.json +13 -2
  13. package/src/renderer/src/App.tsx +1 -1
  14. package/src/renderer/src/components/ChatPanel.tsx +27 -14
  15. package/src/renderer/src/components/DashboardSidebar.tsx +71 -12
  16. package/src/renderer/src/components/ImageStudioPanel.tsx +7 -3
  17. package/src/renderer/src/components/MintDashboard.tsx +199 -5
  18. package/src/renderer/src/components/PicturesLibrary.tsx +1 -3
  19. package/src/renderer/src/components/Settings/AudioTab.tsx +97 -0
  20. package/src/renderer/src/components/Settings/AutomationTab.tsx +195 -0
  21. package/src/renderer/src/components/Settings/GeneralTab.tsx +950 -0
  22. package/src/renderer/src/components/Settings/MemoryTab.tsx +60 -0
  23. package/src/renderer/src/components/Settings/PluginsTab.tsx +452 -0
  24. package/src/renderer/src/components/Settings/ShortcutsTab.tsx +30 -0
  25. package/src/renderer/src/components/Settings/ThemeTab.tsx +129 -0
  26. package/src/renderer/src/components/SettingsWindow.tsx +175 -962
  27. package/src/renderer/src/components/WorkflowBuilderPanel.tsx +536 -0
  28. package/src/renderer/src/components/WorkflowsWindow.tsx +46 -0
  29. package/src/renderer/src/css/image-studio.css +9 -4
  30. package/src/renderer/src/css/settings.css +75 -6
  31. package/src/renderer/src/css/styles.css +355 -38
  32. package/src/renderer/src/css/workflows.css +542 -0
  33. package/src/renderer/src/env.d.ts +2 -0
  34. package/src/renderer/src/index.css +1 -0
  35. package/src/renderer/src/tauri.ts +97 -0
  36. package/src/renderer/src-web/components/ChatPanel.tsx +27 -14
  37. package/src/renderer/src-web/components/DashboardSidebar.tsx +17 -1
  38. package/src/renderer/src-web/components/ImageStudioPanel.tsx +7 -3
  39. package/src/renderer/src-web/components/MintDashboard.tsx +189 -0
  40. package/src/renderer/src-web/components/PicturesLibrary.tsx +1 -3
  41. package/src/renderer/src-web/components/Settings/AudioTab.tsx +97 -0
  42. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +196 -0
  43. package/src/renderer/src-web/components/Settings/GeneralTab.tsx +948 -0
  44. package/src/renderer/src-web/components/Settings/MemoryTab.tsx +60 -0
  45. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +452 -0
  46. package/src/renderer/src-web/components/Settings/ShortcutsTab.tsx +30 -0
  47. package/src/renderer/src-web/components/Settings/ThemeTab.tsx +129 -0
  48. package/src/renderer/src-web/components/SettingsWindow.tsx +187 -968
  49. package/src/renderer/src-web/css/image-studio.css +9 -4
  50. package/src/renderer/src-web/css/settings.css +75 -6
  51. package/src/renderer/src-web/css/styles.css +294 -38
  52. package/src/renderer/src-web/env.d.ts +2 -0
  53. package/src/renderer/src-web/tauri.ts +36 -0
  54. package/src-tauri/src/lib.rs +84 -92
  55. package/src-tauri/tauri.conf.json +1 -1
@@ -0,0 +1,60 @@
1
+ import React from 'react'
2
+
3
+ interface MemoryTabProps {
4
+ userName: string
5
+ setUserName: (val: string) => void
6
+ userPreferences: string
7
+ setUserPreferences: (val: string) => void
8
+ }
9
+
10
+ export default function MemoryTab({
11
+ userName,
12
+ setUserName,
13
+ userPreferences,
14
+ setUserPreferences
15
+ }: MemoryTabProps) {
16
+ return (
17
+ <div className="tab-pane active animate-fade-in" style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
18
+ <section className="setting-section">
19
+ <div className="section-heading">
20
+ <div>
21
+ <p className="section-kicker">Cross-session Memory</p>
22
+ <h2 className="section-title">User Profile & Preferences</h2>
23
+ </div>
24
+ <p className="section-description">
25
+ Information and preferences stored here will be remembered across all conversations. Mint automatically learns and updates your profile details from your chats in the background, but you can also edit them manually.
26
+ </p>
27
+ </div>
28
+
29
+ <div className="form-grid single">
30
+ <div className="setting-row">
31
+ <label>Your Name / Nickname</label>
32
+ <div className="memory-field-container">
33
+ <input
34
+ type="text"
35
+ value={userName}
36
+ onChange={(e) => setUserName(e.target.value)}
37
+ placeholder="e.g. Pheem, Jane"
38
+ />
39
+ <p className="hint">Used by the assistant to address you in conversation.</p>
40
+ </div>
41
+ </div>
42
+
43
+ <div className="setting-row">
44
+ <label>Custom Instructions & Preferences</label>
45
+ <div className="memory-field-container">
46
+ <textarea
47
+ value={userPreferences}
48
+ onChange={(e) => setUserPreferences(e.target.value)}
49
+ placeholder="e.g. Explain coding concepts step-by-step. Prefer TypeScript. Talk in Thai. Keep explanations concise."
50
+ style={{ minHeight: '120px', resize: 'vertical' }}
51
+ />
52
+ <p className="hint">Preferences, guidelines, or persona instructions for the assistant.</p>
53
+ </div>
54
+ </div>
55
+ </div>
56
+ </section>
57
+ </div>
58
+ )
59
+ }
60
+
@@ -0,0 +1,452 @@
1
+ import React, { useState, useEffect } from 'react'
2
+ import { DEFAULT_CONFIG } from '../../components/SettingsWindow'
3
+ import { listLearnedSkills, addLearnedSkill, deleteLearnedSkill, LearnedSkill } from '../../../src/tauri'
4
+
5
+ interface PluginsTabProps {
6
+ config: typeof DEFAULT_CONFIG
7
+ updateField: (field: keyof typeof DEFAULT_CONFIG, value: any) => void
8
+ mcpName: string
9
+ setMcpName: (val: string) => void
10
+ mcpCmd: string
11
+ setMcpCmd: (val: string) => void
12
+ mcpArgs: string
13
+ setMcpArgs: (val: string) => void
14
+ mcpEnv: string
15
+ setMcpEnv: (val: string) => void
16
+ handleAddMcpServer: () => void
17
+ handleRemoveMcpServer: (name: string) => void
18
+ handleConnectPlugin: (plugin: string) => void
19
+ }
20
+
21
+ export default function PluginsTab({
22
+ config,
23
+ updateField,
24
+ mcpName,
25
+ setMcpName,
26
+ mcpCmd,
27
+ setMcpCmd,
28
+ mcpArgs,
29
+ setMcpArgs,
30
+ mcpEnv,
31
+ setMcpEnv,
32
+ handleAddMcpServer,
33
+ handleRemoveMcpServer,
34
+ handleConnectPlugin
35
+ }: PluginsTabProps) {
36
+ // Local state for learned skills
37
+ const [skills, setSkills] = useState<LearnedSkill[]>([])
38
+ const [newSkillName, setNewSkillName] = useState('')
39
+ const [newSkillContent, setNewSkillContent] = useState('')
40
+ const [skillsLoading, setSkillsLoading] = useState(false)
41
+ const [skillsError, setSkillsError] = useState('')
42
+
43
+ // Load skills on mount
44
+ useEffect(() => {
45
+ fetchSkills()
46
+ }, [])
47
+
48
+ const fetchSkills = async () => {
49
+ try {
50
+ const list = await listLearnedSkills()
51
+ setSkills(list)
52
+ } catch (e) {
53
+ console.error("Failed to load skills:", e)
54
+ }
55
+ }
56
+
57
+ const handleAddSkill = async (e: React.FormEvent) => {
58
+ e.preventDefault()
59
+ if (!newSkillName.trim() || !newSkillContent.trim()) {
60
+ setSkillsError('Please fill in both name and content.')
61
+ return
62
+ }
63
+
64
+ const cleanName = newSkillName.trim().replace(/\s+/g, '-').toLowerCase()
65
+ setSkillsLoading(true)
66
+ setSkillsError('')
67
+
68
+ try {
69
+ await addLearnedSkill(cleanName, newSkillContent)
70
+ setNewSkillName('')
71
+ setNewSkillContent('')
72
+ await fetchSkills()
73
+ } catch (err: any) {
74
+ setSkillsError(err.message || String(err))
75
+ } finally {
76
+ setSkillsLoading(false)
77
+ }
78
+ }
79
+
80
+ const handleDeleteSkill = async (name: string) => {
81
+ if (confirm(`Forget skill "${name}"?`)) {
82
+ try {
83
+ await deleteLearnedSkill(name)
84
+ await fetchSkills()
85
+ } catch (e) {
86
+ console.error("Failed to delete skill:", e)
87
+ }
88
+ }
89
+ }
90
+
91
+ const [expandedPlugin, setExpandedPlugin] = useState<string | null>(null)
92
+
93
+ const toggleExpand = (pluginKey: string) => {
94
+ setExpandedPlugin(expandedPlugin === pluginKey ? null : pluginKey)
95
+ }
96
+
97
+ const pluginsList: Array<{
98
+ key: string
99
+ name: string
100
+ desc: string
101
+ icon: string
102
+ enabledField: 'pluginSpotifyEnabled' | 'pluginDiscordEnabled'
103
+ hasCredentials: boolean
104
+ fields?: Array<{ label: string; field: any; type: string; placeholder: string }>
105
+ }> = [
106
+ {
107
+ key: 'spotify',
108
+ name: 'Spotify',
109
+ desc: 'Control playback with AI. Requires playerctl locally.',
110
+ icon: '🎵',
111
+ enabledField: 'pluginSpotifyEnabled' as const,
112
+ hasCredentials: false
113
+ },
114
+ {
115
+ key: 'discord',
116
+ name: 'Discord RPC',
117
+ desc: 'Show "Using Mint Assistant" status in your local Discord client.',
118
+ icon: '💬',
119
+ enabledField: 'pluginDiscordEnabled' as const,
120
+ hasCredentials: false
121
+ }
122
+ ]
123
+
124
+ return (
125
+ <div className="tab-pane active" style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
126
+ {/* ── Learned AI Skills ── */}
127
+ <section className="setting-section">
128
+ <div className="section-heading">
129
+ <div>
130
+ <p className="section-kicker">Knowledge Base</p>
131
+ <h2 className="section-title">Learned AI Skills</h2>
132
+ </div>
133
+ <p className="section-description">
134
+ Skills are special instructions or guides taught to Mint (equivalent to <code>mint learn</code> in CLI). The AI reads active skills before every prompt to align with your guidelines.
135
+ </p>
136
+ </div>
137
+
138
+ {skills.length === 0 ? (
139
+ <div className="empty-skills-notice" style={{
140
+ padding: '24px',
141
+ border: '1px dashed var(--border)',
142
+ borderRadius: '12px',
143
+ textAlign: 'center',
144
+ color: 'var(--text-muted)',
145
+ background: 'rgba(0,0,0,0.06)',
146
+ fontSize: '0.9rem',
147
+ marginBottom: '20px'
148
+ }}>
149
+ No learned skills found. Teach Mint a skill below or run <code>/learn &lt;path&gt;</code> in chat!
150
+ </div>
151
+ ) : (
152
+ <div className="skills-list" style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '20px' }}>
153
+ {skills.map((s) => (
154
+ <div className="skill-card" key={s.id} style={{
155
+ display: 'flex',
156
+ alignItems: 'flex-start',
157
+ justifyContent: 'space-between',
158
+ padding: '16px 20px',
159
+ border: '1px solid var(--border)',
160
+ borderRadius: '12px',
161
+ background: 'var(--surface-bg)',
162
+ gap: '16px'
163
+ }}>
164
+ <div style={{ flex: 1, minWidth: 0 }}>
165
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
166
+ <span className="badge" style={{
167
+ background: 'var(--accent)',
168
+ color: '#fff',
169
+ fontSize: '0.75rem',
170
+ fontWeight: '600',
171
+ padding: '2px 8px',
172
+ borderRadius: '4px'
173
+ }}>{s.name}</span>
174
+ <span style={{ fontSize: '0.8rem', color: 'var(--text-muted)', textOverflow: 'ellipsis', overflow: 'hidden', whiteSpace: 'nowrap' }}>
175
+ Source: {s.sourcePath}
176
+ </span>
177
+ </div>
178
+ <div style={{
179
+ fontSize: '0.85rem',
180
+ color: 'var(--text-main)',
181
+ whiteSpace: 'pre-wrap',
182
+ maxHeight: '120px',
183
+ overflowY: 'auto',
184
+ background: 'rgba(0,0,0,0.12)',
185
+ padding: '10px 14px',
186
+ borderRadius: '8px',
187
+ fontFamily: 'monospace',
188
+ lineHeight: '1.4'
189
+ }}>
190
+ {s.content}
191
+ </div>
192
+ </div>
193
+ <button
194
+ className="btn btn-danger"
195
+ onClick={() => handleDeleteSkill(s.name)}
196
+ style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px', flexShrink: 0 }}
197
+ >
198
+ Forget
199
+ </button>
200
+ </div>
201
+ ))}
202
+ </div>
203
+ )}
204
+
205
+ <form className="add-skill-box" onSubmit={handleAddSkill} style={{
206
+ padding: '20px',
207
+ border: '1px solid var(--border)',
208
+ borderRadius: '12px',
209
+ background: 'var(--surface-bg)'
210
+ }}>
211
+ <h3 style={{ fontSize: '1rem', fontWeight: '600', color: 'var(--text-main)', marginBottom: '14px' }}>Teach New Skill</h3>
212
+ {skillsError && <div style={{ color: '#ef4444', fontSize: '0.85rem', marginBottom: '12px', fontWeight: '500' }}>{skillsError}</div>}
213
+
214
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '12px' }}>
215
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Skill Name (e.g. coding-guidelines)</label>
216
+ <input
217
+ type="text"
218
+ placeholder="e.g. angular-standard"
219
+ value={newSkillName}
220
+ onChange={(e) => setNewSkillName(e.target.value)}
221
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem' }}
222
+ />
223
+ </div>
224
+
225
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '14px' }}>
226
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Skill Instructions / Content</label>
227
+ <textarea
228
+ placeholder="# Instructions&#10;Write only clean TypeScript. Use async/await. Avoid let where const is possible."
229
+ value={newSkillContent}
230
+ onChange={(e) => setNewSkillContent(e.target.value)}
231
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%', height: '100px', resize: 'vertical' }}
232
+ />
233
+ </div>
234
+
235
+ <button
236
+ type="submit"
237
+ className="btn-primary"
238
+ disabled={skillsLoading}
239
+ style={{ width: '100%', padding: '10px 16px', borderRadius: '8px', border: 'none', background: 'var(--accent)', color: '#fff', fontWeight: '600', cursor: 'pointer' }}
240
+ >
241
+ {skillsLoading ? 'Learning...' : 'Teach Skill'}
242
+ </button>
243
+ </form>
244
+ </section>
245
+
246
+ {/* ── External tools (MCP Servers) ── */}
247
+ <section className="setting-section" style={{ borderTop: '1px solid var(--border)', paddingTop: '24px' }}>
248
+ <div className="section-heading">
249
+ <div>
250
+ <p className="section-kicker">External tools</p>
251
+ <h2 className="section-title">MCP Servers</h2>
252
+ </div>
253
+ <p className="section-description">Connect Mint to tools like search, GitHub, or filesystem servers.</p>
254
+ </div>
255
+
256
+ <div className="mcp-list">
257
+ {Object.entries(config.mcpServers || {}).map(([name, srv]: [string, any]) => (
258
+ <div className="plugin-card" key={name} style={{
259
+ display: 'flex',
260
+ alignItems: 'center',
261
+ justifyContent: 'space-between',
262
+ padding: '16px 20px',
263
+ border: '1px solid var(--border)',
264
+ borderRadius: '12px',
265
+ background: 'var(--surface-bg)',
266
+ marginBottom: '10px'
267
+ }}>
268
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
269
+ <div className="plugin-icon" style={{ fontSize: '1.5rem', width: '40px', height: '40px', display: 'grid', placeItems: 'center', background: 'var(--surface-strong)', borderRadius: '10px' }}>⚙</div>
270
+ <div className="plugin-info">
271
+ <div className="plugin-name" style={{ fontWeight: '600', color: 'var(--text-main)' }}>{name}</div>
272
+ <div className="plugin-desc" style={{ fontFamily: 'monospace', fontSize: '0.8rem', color: 'var(--text-muted)' }}>
273
+ Command: {srv.command} {srv.args?.join(' ')}
274
+ </div>
275
+ </div>
276
+ </div>
277
+ <div className="plugin-actions">
278
+ <button className="btn btn-danger" onClick={() => handleRemoveMcpServer(name)} style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px' }}>Remove</button>
279
+ </div>
280
+ </div>
281
+ ))}
282
+ </div>
283
+
284
+ <div className="add-mcp-box" style={{
285
+ marginTop: '20px',
286
+ padding: '20px',
287
+ border: '1px solid var(--border)',
288
+ borderRadius: '12px',
289
+ background: 'var(--surface-bg)'
290
+ }}>
291
+ <h3 style={{ fontSize: '1rem', fontWeight: '600', color: 'var(--text-main)', marginBottom: '14px' }}>Add MCP Server</h3>
292
+ <div className="form-grid" style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '12px' }}>
293
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
294
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Server Name</label>
295
+ <input
296
+ type="text"
297
+ placeholder="e.g. google-search"
298
+ value={mcpName}
299
+ onChange={(e) => setMcpName(e.target.value)}
300
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem' }}
301
+ />
302
+ </div>
303
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
304
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Command</label>
305
+ <input
306
+ type="text"
307
+ placeholder="e.g. npx"
308
+ value={mcpCmd}
309
+ onChange={(e) => setMcpCmd(e.target.value)}
310
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem' }}
311
+ />
312
+ </div>
313
+ </div>
314
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '12px' }}>
315
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Arguments</label>
316
+ <input
317
+ type="text"
318
+ placeholder="e.g. -y @modelcontextprotocol/server-brave-search"
319
+ value={mcpArgs}
320
+ onChange={(e) => setMcpArgs(e.target.value)}
321
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%' }}
322
+ />
323
+ </div>
324
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '14px' }}>
325
+ <label style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>Environment Variables (JSON)</label>
326
+ <textarea
327
+ placeholder='e.g. {"BRAVE_API_KEY": "your_key_here"}'
328
+ value={mcpEnv}
329
+ onChange={(e) => setMcpEnv(e.target.value)}
330
+ style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--input-bg)', color: 'var(--text-main)', fontSize: '0.9rem', width: '100%', height: '70px', resize: 'vertical' }}
331
+ />
332
+ </div>
333
+ <button className="btn-primary" onClick={handleAddMcpServer} style={{ width: '100%', padding: '10px 16px', borderRadius: '8px', border: 'none', background: 'var(--accent)', color: '#fff', fontWeight: '600', cursor: 'pointer' }}>Add MCP Server</button>
334
+ </div>
335
+ </section>
336
+
337
+ {/* ── Built-in Plugins ── */}
338
+ <section className="setting-section" style={{ borderTop: '1px solid var(--border)', paddingTop: '24px' }}>
339
+ <div className="section-heading">
340
+ <div>
341
+ <p className="section-kicker">Integrations</p>
342
+ <h2 className="section-title">Built-in Plugins</h2>
343
+ </div>
344
+ <p className="section-description">Enable and configure credentials for native Mint plugins.</p>
345
+ </div>
346
+ <div className="plugin-list" style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
347
+ {pluginsList.map(p => {
348
+ const isEnabled = config[p.enabledField]
349
+ const isExpanded = expandedPlugin === p.key
350
+
351
+ return (
352
+ <div className={`plugin-card-wrapper ${isEnabled ? 'active-plugin-card' : ''}`} key={p.key} style={{
353
+ border: '1px solid var(--border)',
354
+ borderRadius: '12px',
355
+ background: 'var(--surface-bg)',
356
+ transition: 'all 0.2s ease',
357
+ overflow: 'hidden'
358
+ }}>
359
+ <div className="plugin-card" style={{
360
+ display: 'flex',
361
+ alignItems: 'center',
362
+ justifyContent: 'space-between',
363
+ padding: '16px 20px',
364
+ background: isEnabled ? 'color-mix(in srgb, var(--accent) 3%, var(--surface-bg))' : 'transparent'
365
+ }}>
366
+ <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
367
+ <div className="plugin-icon" style={{ fontSize: '1.5rem', width: '40px', height: '40px', display: 'grid', placeItems: 'center', background: 'var(--surface-strong)', borderRadius: '10px' }}>
368
+ {p.icon}
369
+ </div>
370
+ <div className="plugin-info">
371
+ <div className="plugin-name" style={{ fontWeight: '600', color: 'var(--text-main)' }}>{p.name}</div>
372
+ <div className="plugin-desc" style={{ fontSize: '0.85rem', color: 'var(--text-muted)' }}>{p.desc}</div>
373
+ </div>
374
+ </div>
375
+ <div className="plugin-actions" style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
376
+ {p.hasCredentials && (
377
+ <button
378
+ className="btn-secondary"
379
+ onClick={() => toggleExpand(p.key)}
380
+ style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px' }}
381
+ >
382
+ {isExpanded ? 'Hide Config' : 'Configure'}
383
+ </button>
384
+ )}
385
+ {p.key === 'discord' && isEnabled && (
386
+ <button
387
+ className="btn-secondary"
388
+ onClick={() => handleConnectPlugin('discord')}
389
+ style={{ padding: '6px 12px', fontSize: '0.8rem', height: '34px' }}
390
+ >
391
+ Update RPC
392
+ </button>
393
+ )}
394
+ <label className="settings-toggle-switch">
395
+ <input
396
+ type="checkbox"
397
+ checked={isEnabled}
398
+ onChange={(e) => {
399
+ updateField(p.enabledField, e.target.checked)
400
+ if (!e.target.checked && expandedPlugin === p.key) {
401
+ // Don't auto-collapse config on disable so user can still edit
402
+ }
403
+ }}
404
+ />
405
+ <span className="settings-toggle-slider" />
406
+ </label>
407
+ </div>
408
+ </div>
409
+
410
+ {p.hasCredentials && isExpanded && (
411
+ <div className="plugin-config-panel" style={{
412
+ padding: '20px',
413
+ borderTop: '1px solid var(--border)',
414
+ background: 'rgba(0,0,0,0.12)',
415
+ display: 'flex',
416
+ flexDirection: 'column',
417
+ gap: '12px'
418
+ }}>
419
+ <h4 style={{ fontSize: '0.9rem', fontWeight: '600', color: 'var(--text-soft)', marginBottom: '4px' }}>Credentials Configuration</h4>
420
+ <div className="form-grid compact" style={{ display: 'grid', gridTemplateColumns: '1fr', gap: '12px' }}>
421
+ {p.fields?.map(f => (
422
+ <div className="setting-row" key={f.field} style={{ display: 'flex', flexDirection: 'column', gap: '6px', border: 'none', padding: 0 }}>
423
+ <label style={{ fontSize: '0.8rem', fontWeight: '500', color: 'var(--text-muted)' }}>{f.label}</label>
424
+ <input
425
+ type={f.type}
426
+ placeholder={f.placeholder}
427
+ value={(config as any)[f.field] || ''}
428
+ onChange={(e) => updateField(f.field, e.target.value)}
429
+ style={{
430
+ width: '100%',
431
+ padding: '10px 14px',
432
+ borderRadius: '8px',
433
+ border: '1px solid var(--border)',
434
+ background: 'var(--input-bg)',
435
+ color: 'var(--text-main)',
436
+ fontSize: '0.9rem'
437
+ }}
438
+ />
439
+ </div>
440
+ ))}
441
+ </div>
442
+ </div>
443
+ )}
444
+ </div>
445
+ );
446
+ })}
447
+ </div>
448
+ </section>
449
+ </div>
450
+ )
451
+ }
452
+
@@ -0,0 +1,30 @@
1
+ import React from 'react'
2
+
3
+ export default function ShortcutsTab() {
4
+ return (
5
+ <div className="tab-pane active">
6
+ <section className="setting-section">
7
+ <div className="section-heading">
8
+ <div>
9
+ <p className="section-kicker">Keyboard</p>
10
+ <h2 className="section-title">Shortcuts</h2>
11
+ </div>
12
+ </div>
13
+ <div className="shortcut-list">
14
+ <div className="shortcut-item">
15
+ <span>Show / Hide Mint Window</span>
16
+ <div className="keys"><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>Space</kbd></div>
17
+ </div>
18
+ <div className="shortcut-item">
19
+ <span>Open Spotlight</span>
20
+ <div className="keys"><kbd>Alt</kbd><kbd>Space</kbd></div>
21
+ </div>
22
+ <div className="shortcut-item">
23
+ <span>Close / Dismiss</span>
24
+ <div className="keys"><kbd>Esc</kbd></div>
25
+ </div>
26
+ </div>
27
+ </section>
28
+ </div>
29
+ )
30
+ }
@@ -0,0 +1,129 @@
1
+ import React from 'react'
2
+ import { DEFAULT_CONFIG } from '../SettingsWindow'
3
+
4
+ interface ThemeTabProps {
5
+ config: typeof DEFAULT_CONFIG
6
+ updateField: (field: keyof typeof DEFAULT_CONFIG, value: any) => void
7
+ }
8
+
9
+ export default function ThemeTab({ config, updateField }: ThemeTabProps) {
10
+ return (
11
+ <div className="tab-pane active">
12
+ <section className="setting-section">
13
+ <div className="section-heading">
14
+ <div>
15
+ <p className="section-kicker">Appearance</p>
16
+ <h2 className="section-title">Theme</h2>
17
+ </div>
18
+ </div>
19
+ <div className="theme-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '12px' }}>
20
+ {(['dark', 'light', 'midnight', 'custom'] as const).map(t => (
21
+ <button
22
+ key={t}
23
+ className={`theme-card ${config.theme === t ? 'active' : ''}`}
24
+ onClick={() => updateField('theme', t)}
25
+ >
26
+ <div className={`theme-preview ${t}-preview`}></div>
27
+ <span style={{ textTransform: 'capitalize' }}>{t}</span>
28
+ </button>
29
+ ))}
30
+ </div>
31
+
32
+ {config.theme === 'custom' && (
33
+ <div className="custom-theme-panel" style={{ marginTop: '15px' }}>
34
+ <div className="setting-row">
35
+ <label>Background Gradient</label>
36
+ <div className="color-range" style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
37
+ <input type="color" value={config.customBgStart} onChange={(e) => updateField('customBgStart', e.target.value)} />
38
+ <span style={{ display: 'inline-flex', alignItems: 'center', color: 'var(--text-soft)' }}>
39
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
40
+ <line x1="5" y1="12" x2="19" y2="12"></line>
41
+ <polyline points="12 5 19 12 12 19"></polyline>
42
+ </svg>
43
+ </span>
44
+ <input type="color" value={config.customBgEnd} onChange={(e) => updateField('customBgEnd', e.target.value)} />
45
+ </div>
46
+ </div>
47
+ <div className="setting-row">
48
+ <label>Panel Background</label>
49
+ <input type="color" value={config.customPanelBg} onChange={(e) => updateField('customPanelBg', e.target.value)} />
50
+ </div>
51
+ </div>
52
+ )}
53
+ </section>
54
+
55
+ <section className="setting-section">
56
+ <div className="section-heading">
57
+ <div>
58
+ <p className="section-kicker">Color</p>
59
+ <h2 className="section-title">Accent & Text</h2>
60
+ </div>
61
+ </div>
62
+ <div className="color-section">
63
+ <div>
64
+ <label>Accent Color</label>
65
+ <div className="color-presets">
66
+ {['#8b5cf6', '#06b6d4', '#10b981', '#f59e0b', '#ef4444', '#ec4899'].map(c => (
67
+ <button
68
+ key={c}
69
+ className="color-dot"
70
+ style={{ backgroundColor: c, border: config.accentColor === c ? '2px solid white' : 'none' }}
71
+ onClick={() => updateField('accentColor', c)}
72
+ />
73
+ ))}
74
+ </div>
75
+ </div>
76
+ <div className="color-inputs" style={{ display: 'flex', gap: '15px', marginTop: '12px' }}>
77
+ <div>
78
+ <label>Custom Accent</label>
79
+ <input type="color" value={config.accentColor} onChange={(e) => updateField('accentColor', e.target.value)} style={{ display: 'block', marginTop: '4px' }} />
80
+ </div>
81
+ <div>
82
+ <label>System Text</label>
83
+ <input type="color" value={config.systemTextColor} onChange={(e) => updateField('systemTextColor', e.target.value)} style={{ display: 'block', marginTop: '4px' }} />
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </section>
88
+
89
+ <section className="setting-section">
90
+ <div className="section-heading">
91
+ <div>
92
+ <p className="section-kicker">Surface</p>
93
+ <h2 className="section-title">Interface Style</h2>
94
+ </div>
95
+ </div>
96
+ <div className="form-grid">
97
+ <div className="setting-row">
98
+ <label>Glass Blur</label>
99
+ <select value={config.glassBlur} onChange={(e) => updateField('glassBlur', e.target.value)}>
100
+ <option value="blur(4px)">Low (4px)</option>
101
+ <option value="blur(16px)">Medium (16px) - Default</option>
102
+ <option value="blur(32px)">High (32px)</option>
103
+ <option value="none">Off (Solid)</option>
104
+ </select>
105
+ </div>
106
+ <div className="setting-row">
107
+ <label>Font Family</label>
108
+ <select value={config.fontFamily} onChange={(e) => updateField('fontFamily', e.target.value)}>
109
+ <option value="'Outfit', sans-serif">Outfit (Default)</option>
110
+ <option value="'Mali', cursive">Mali (Cute Thai Font)</option>
111
+ <option value="'Prompt', sans-serif">Prompt (Modern Thai)</option>
112
+ <option value="'Sarabun', sans-serif">Sarabun (Formal Thai)</option>
113
+ </select>
114
+ </div>
115
+ <div className="setting-row">
116
+ <label>Font Size</label>
117
+ <select value={config.fontSize} onChange={(e) => updateField('fontSize', e.target.value)}>
118
+ <option value="16px">Small</option>
119
+ <option value="18px">Medium (Default)</option>
120
+ <option value="22px">Large</option>
121
+ <option value="26px">Extra Large</option>
122
+ <option value="30px">Extra Extra Large</option>
123
+ </select>
124
+ </div>
125
+ </div>
126
+ </section>
127
+ </div>
128
+ )
129
+ }