@fatdoge/wtree 0.1.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.
- package/README.en.md +113 -0
- package/README.md +136 -0
- package/api/app.ts +19 -0
- package/api/cli/wtree.ts +809 -0
- package/api/core/config.ts +26 -0
- package/api/core/exec.ts +55 -0
- package/api/core/git.ts +35 -0
- package/api/core/id.ts +8 -0
- package/api/core/open.ts +58 -0
- package/api/core/worktree.test.ts +33 -0
- package/api/core/worktree.ts +72 -0
- package/api/createApiApp.ts +33 -0
- package/api/index.ts +9 -0
- package/api/routes/worktrees.ts +255 -0
- package/api/server.ts +34 -0
- package/api/ui/startUiDev.ts +82 -0
- package/dist/assets/index-D9inyPb3.js +179 -0
- package/dist/assets/index-W34LSHWF.css +1 -0
- package/dist/favicon.svg +4 -0
- package/dist/index.html +354 -0
- package/dist-node/api/app.js +17 -0
- package/dist-node/api/cli/wtree.js +722 -0
- package/dist-node/api/cli/wtui.js +722 -0
- package/dist-node/api/core/config.js +21 -0
- package/dist-node/api/core/exec.js +24 -0
- package/dist-node/api/core/git.js +24 -0
- package/dist-node/api/core/id.js +6 -0
- package/dist-node/api/core/open.js +51 -0
- package/dist-node/api/core/worktree.js +58 -0
- package/dist-node/api/core/worktree.test.js +30 -0
- package/dist-node/api/createApiApp.js +26 -0
- package/dist-node/api/routes/worktrees.js +213 -0
- package/dist-node/api/server.js +29 -0
- package/dist-node/api/ui/startUiDev.js +65 -0
- package/dist-node/shared/wtui-types.js +1 -0
- package/index.html +24 -0
- package/package.json +89 -0
- package/postcss.config.js +10 -0
- package/shared/wtui-types.ts +36 -0
- package/src/App.tsx +28 -0
- package/src/assets/react.svg +1 -0
- package/src/components/Button.tsx +34 -0
- package/src/components/Empty.tsx +8 -0
- package/src/components/Input.tsx +16 -0
- package/src/components/Modal.tsx +33 -0
- package/src/components/ToastHost.tsx +42 -0
- package/src/hooks/useTheme.ts +29 -0
- package/src/i18n/index.ts +22 -0
- package/src/i18n/locales/en.json +145 -0
- package/src/i18n/locales/zh.json +145 -0
- package/src/index.css +24 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +11 -0
- package/src/pages/CreateWorktree.tsx +181 -0
- package/src/pages/HelpPage.tsx +67 -0
- package/src/pages/Home.tsx +3 -0
- package/src/pages/SettingsPage.tsx +218 -0
- package/src/pages/Worktrees.tsx +354 -0
- package/src/stores/themeStore.ts +44 -0
- package/src/stores/toastStore.ts +29 -0
- package/src/stores/worktreeStore.ts +93 -0
- package/src/utils/api.ts +36 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +13 -0
- package/vite.config.ts +46 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { ArrowLeft, CheckCircle2, Loader2 } from 'lucide-react'
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
3
|
+
import { Link, useNavigate } from 'react-router-dom'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
5
|
+
import Button from '@/components/Button'
|
|
6
|
+
import Input from '@/components/Input'
|
|
7
|
+
import { useToastStore } from '@/stores/toastStore'
|
|
8
|
+
import { useWorktreeStore } from '@/stores/worktreeStore'
|
|
9
|
+
import type { CreateWorktreeRequest } from '../../shared/wtui-types'
|
|
10
|
+
|
|
11
|
+
type Mode = 'existing' | 'new'
|
|
12
|
+
|
|
13
|
+
export default function CreateWorktree() {
|
|
14
|
+
const { t } = useTranslation()
|
|
15
|
+
const navigate = useNavigate()
|
|
16
|
+
const create = useWorktreeStore((s) => s.create)
|
|
17
|
+
const branches = useWorktreeStore((s) => s.branches)
|
|
18
|
+
const fetchBranches = useWorktreeStore((s) => s.fetchBranches)
|
|
19
|
+
const toast = useToastStore((s) => s.push)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
fetchBranches()
|
|
23
|
+
}, [fetchBranches])
|
|
24
|
+
|
|
25
|
+
const [mode, setMode] = useState<Mode>('existing')
|
|
26
|
+
const [ref, setRef] = useState('')
|
|
27
|
+
const [newBranch, setNewBranch] = useState('')
|
|
28
|
+
const [p, setP] = useState('')
|
|
29
|
+
const [submitting, setSubmitting] = useState(false)
|
|
30
|
+
const [resultPath, setResultPath] = useState<string | null>(null)
|
|
31
|
+
|
|
32
|
+
const preview = useMemo(() => {
|
|
33
|
+
const target = p.trim() ? p.trim() : '<path>'
|
|
34
|
+
if (mode === 'new' && newBranch.trim()) {
|
|
35
|
+
return `git branch ${newBranch.trim()} ${ref.trim() || '<ref>'} && git worktree add ${target} ${newBranch.trim()}`
|
|
36
|
+
}
|
|
37
|
+
return `git worktree add ${target} ${ref.trim() || '<ref>'}`
|
|
38
|
+
}, [mode, newBranch, p, ref])
|
|
39
|
+
|
|
40
|
+
const canSubmit = ref.trim().length > 0 && p.trim().length > 0 && (!mode || mode === 'existing' || newBranch.trim().length > 0)
|
|
41
|
+
|
|
42
|
+
const onSubmit = async () => {
|
|
43
|
+
if (!canSubmit) return
|
|
44
|
+
setSubmitting(true)
|
|
45
|
+
setResultPath(null)
|
|
46
|
+
try {
|
|
47
|
+
const payload: CreateWorktreeRequest = {
|
|
48
|
+
ref: ref.trim(),
|
|
49
|
+
path: p.trim(),
|
|
50
|
+
...(mode === 'new' ? { newBranch: newBranch.trim() } : {}),
|
|
51
|
+
}
|
|
52
|
+
const created = await create(payload)
|
|
53
|
+
if (!created) {
|
|
54
|
+
toast({ type: 'error', title: '创建失败' })
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
setResultPath(created.path)
|
|
58
|
+
toast({ type: 'success', title: '创建成功' })
|
|
59
|
+
} catch (e: unknown) {
|
|
60
|
+
const msg = e instanceof Error ? e.message : String(e)
|
|
61
|
+
toast({ type: 'error', title: '创建失败', detail: msg })
|
|
62
|
+
} finally {
|
|
63
|
+
setSubmitting(false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="mx-auto w-full max-w-screen-xl px-4 py-6">
|
|
69
|
+
<div className="flex items-center justify-between gap-4">
|
|
70
|
+
<div className="flex items-center gap-2">
|
|
71
|
+
<Link to="/">
|
|
72
|
+
<Button variant="ghost" size="sm">
|
|
73
|
+
<ArrowLeft className="h-4 w-4" />
|
|
74
|
+
{t('create.back')}
|
|
75
|
+
</Button>
|
|
76
|
+
</Link>
|
|
77
|
+
<div className="text-lg font-semibold text-slate-900 dark:text-slate-100">{t('create.title')}</div>
|
|
78
|
+
</div>
|
|
79
|
+
<Button variant="secondary" size="sm" onClick={() => navigate('/help')}>{t('create.help')}</Button>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className="mt-5 grid grid-cols-12 gap-4">
|
|
83
|
+
<div className="col-span-12 lg:col-span-7">
|
|
84
|
+
<div className="rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
|
|
85
|
+
<div className="flex gap-2">
|
|
86
|
+
<Button
|
|
87
|
+
size="sm"
|
|
88
|
+
variant={mode === 'existing' ? 'primary' : 'secondary'}
|
|
89
|
+
onClick={() => setMode('existing')}
|
|
90
|
+
>
|
|
91
|
+
{t('create.tabs.existing')}
|
|
92
|
+
</Button>
|
|
93
|
+
<Button size="sm" variant={mode === 'new' ? 'primary' : 'secondary'} onClick={() => setMode('new')}>
|
|
94
|
+
{t('create.tabs.new')}
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="mt-4 grid grid-cols-12 gap-3">
|
|
99
|
+
<div className="col-span-12">
|
|
100
|
+
<div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('create.fields.ref')}</div>
|
|
101
|
+
<div className="mt-1">
|
|
102
|
+
<Input
|
|
103
|
+
value={ref}
|
|
104
|
+
onChange={(e) => setRef(e.target.value)}
|
|
105
|
+
placeholder={t('create.fields.refPlaceholder')}
|
|
106
|
+
list="branch-list"
|
|
107
|
+
/>
|
|
108
|
+
<datalist id="branch-list">
|
|
109
|
+
{branches.map((b) => (
|
|
110
|
+
<option key={b} value={b} />
|
|
111
|
+
))}
|
|
112
|
+
</datalist>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
{mode === 'new' ? (
|
|
117
|
+
<div className="col-span-12">
|
|
118
|
+
<div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('create.fields.newBranch')}</div>
|
|
119
|
+
<div className="mt-1">
|
|
120
|
+
<Input value={newBranch} onChange={(e) => setNewBranch(e.target.value)} placeholder={t('create.fields.newBranchPlaceholder')} />
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
) : null}
|
|
124
|
+
|
|
125
|
+
<div className="col-span-12">
|
|
126
|
+
<div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('create.fields.path')}</div>
|
|
127
|
+
<div className="mt-1">
|
|
128
|
+
<Input value={p} onChange={(e) => setP(e.target.value)} placeholder={t('create.fields.pathPlaceholder')} />
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="mt-4 flex flex-col sm:flex-row items-center justify-end gap-2">
|
|
134
|
+
<Button className="w-full sm:w-auto" variant="primary" size="sm" disabled={!canSubmit || submitting} onClick={onSubmit}>
|
|
135
|
+
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
|
|
136
|
+
{t('create.submit')}
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div className="col-span-12 lg:col-span-5">
|
|
143
|
+
<div className="rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
|
|
144
|
+
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('create.preview.title')}</div>
|
|
145
|
+
<div className="mt-2 rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2 font-mono text-xs text-slate-900 dark:text-slate-100">
|
|
146
|
+
{preview}
|
|
147
|
+
</div>
|
|
148
|
+
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">{t('create.preview.desc')}</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div className="mt-3 rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
|
|
152
|
+
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('create.result.title')}</div>
|
|
153
|
+
{submitting ? (
|
|
154
|
+
<div className="mt-2 flex items-center gap-2 text-xs text-slate-600 dark:text-slate-300">
|
|
155
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
156
|
+
{t('create.result.creating')}
|
|
157
|
+
</div>
|
|
158
|
+
) : resultPath ? (
|
|
159
|
+
<div className="mt-2">
|
|
160
|
+
<div className="flex items-center gap-2 text-xs text-emerald-600 dark:text-emerald-400">
|
|
161
|
+
<CheckCircle2 className="h-4 w-4" />
|
|
162
|
+
{t('create.result.success')}
|
|
163
|
+
</div>
|
|
164
|
+
<div className="mt-2 rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2 font-mono text-xs text-slate-900 dark:text-slate-100">
|
|
165
|
+
{resultPath}
|
|
166
|
+
</div>
|
|
167
|
+
<div className="mt-3">
|
|
168
|
+
<Link to="/">
|
|
169
|
+
<Button variant="secondary" size="sm">{t('create.result.backToList')}</Button>
|
|
170
|
+
</Link>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
) : (
|
|
174
|
+
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">{t('create.result.empty')}</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ArrowLeft } from 'lucide-react'
|
|
2
|
+
import { Link } from 'react-router-dom'
|
|
3
|
+
import { useTranslation } from 'react-i18next'
|
|
4
|
+
import Button from '@/components/Button'
|
|
5
|
+
|
|
6
|
+
export default function HelpPage() {
|
|
7
|
+
const { t } = useTranslation()
|
|
8
|
+
return (
|
|
9
|
+
<div className="mx-auto w-full max-w-screen-xl px-4 py-6">
|
|
10
|
+
<div className="flex items-center gap-2">
|
|
11
|
+
<Link to="/">
|
|
12
|
+
<Button variant="ghost" size="sm">
|
|
13
|
+
<ArrowLeft className="h-4 w-4" />
|
|
14
|
+
{t('create.back')}
|
|
15
|
+
</Button>
|
|
16
|
+
</Link>
|
|
17
|
+
<div className="text-lg font-semibold text-slate-900 dark:text-slate-100">{t('helpPage.title')}</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div className="mt-5 grid grid-cols-12 gap-4">
|
|
21
|
+
<div className="col-span-12 lg:col-span-7">
|
|
22
|
+
<div className="rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
|
|
23
|
+
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('helpPage.commands')}</div>
|
|
24
|
+
<div className="mt-3 space-y-2 font-mono text-xs text-slate-700 dark:text-slate-200">
|
|
25
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree</div>
|
|
26
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree list</div>
|
|
27
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree create <branch></div>
|
|
28
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree delete</div>
|
|
29
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree open <path|branch></div>
|
|
30
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree lock <path|branch></div>
|
|
31
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree unlock <path|branch></div>
|
|
32
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree prune</div>
|
|
33
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree config</div>
|
|
34
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree config get <key></div>
|
|
35
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree config set <key> <value></div>
|
|
36
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree --ui</div>
|
|
37
|
+
<div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree --ui --no-open --port 5173</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="mt-3 text-xs text-slate-500 dark:text-slate-400">
|
|
40
|
+
{t('helpPage.commandsDesc')}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="col-span-12 lg:col-span-5">
|
|
46
|
+
<div className="rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
|
|
47
|
+
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('helpPage.faq.title')}</div>
|
|
48
|
+
<div className="mt-2 space-y-2 text-xs text-slate-500 dark:text-slate-400">
|
|
49
|
+
<div>
|
|
50
|
+
<div className="text-slate-700 dark:text-slate-200">{t('helpPage.faq.q1')}</div>
|
|
51
|
+
<div className="mt-1">{t('helpPage.faq.a1')}</div>
|
|
52
|
+
</div>
|
|
53
|
+
<div>
|
|
54
|
+
<div className="text-slate-700 dark:text-slate-200">{t('helpPage.faq.q2')}</div>
|
|
55
|
+
<div className="mt-1">{t('helpPage.faq.a2')}</div>
|
|
56
|
+
</div>
|
|
57
|
+
<div>
|
|
58
|
+
<div className="text-slate-700 dark:text-slate-200">{t('helpPage.faq.q3')}</div>
|
|
59
|
+
<div className="mt-1">{t('helpPage.faq.a3')}</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { ArrowLeft, Save, RotateCcw, Eraser, Moon, Sun, Laptop, Languages } from 'lucide-react'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
3
|
+
import { Link } from 'react-router-dom'
|
|
4
|
+
import { useTranslation } from 'react-i18next'
|
|
5
|
+
import Button from '@/components/Button'
|
|
6
|
+
import Input from '@/components/Input'
|
|
7
|
+
import { useToastStore } from '@/stores/toastStore'
|
|
8
|
+
import { useWorktreeStore } from '@/stores/worktreeStore'
|
|
9
|
+
import { useThemeStore } from '@/stores/themeStore'
|
|
10
|
+
import { apiGet, apiPut } from '@/utils/api'
|
|
11
|
+
import type { WtuiConfig } from '../../shared/wtui-types'
|
|
12
|
+
|
|
13
|
+
export default function SettingsPage() {
|
|
14
|
+
const { t, i18n } = useTranslation()
|
|
15
|
+
const toast = useToastStore((s) => s.push)
|
|
16
|
+
const prune = useWorktreeStore((s) => s.prune)
|
|
17
|
+
const { theme, setTheme } = useThemeStore()
|
|
18
|
+
const [cfg, setCfg] = useState<WtuiConfig>({})
|
|
19
|
+
const [loading, setLoading] = useState(false)
|
|
20
|
+
const [pruning, setPruning] = useState(false)
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setLoading(true)
|
|
24
|
+
apiGet<WtuiConfig>('/api/config')
|
|
25
|
+
.then((r) => {
|
|
26
|
+
if (r.ok) setCfg(r.data)
|
|
27
|
+
})
|
|
28
|
+
.finally(() => setLoading(false))
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
const onSave = async () => {
|
|
32
|
+
setLoading(true)
|
|
33
|
+
try {
|
|
34
|
+
const r = await apiPut<WtuiConfig, WtuiConfig>('/api/config', cfg)
|
|
35
|
+
if (!r.ok) {
|
|
36
|
+
const msg = (r as { ok: false; error: { message: string } }).error.message
|
|
37
|
+
toast({ type: 'error', title: t('settings.toast.saveFailed'), detail: msg })
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
setCfg(r.data)
|
|
41
|
+
toast({ type: 'success', title: t('settings.toast.saveSuccess') })
|
|
42
|
+
} finally {
|
|
43
|
+
setLoading(false)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const onReset = async () => {
|
|
48
|
+
setCfg({})
|
|
49
|
+
setLoading(true)
|
|
50
|
+
try {
|
|
51
|
+
const r = await apiPut<WtuiConfig, WtuiConfig>('/api/config', {})
|
|
52
|
+
if (!r.ok) {
|
|
53
|
+
const msg = (r as { ok: false; error: { message: string } }).error.message
|
|
54
|
+
toast({ type: 'error', title: t('settings.toast.resetFailed', '重置失败'), detail: msg })
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
setCfg(r.data)
|
|
58
|
+
toast({ type: 'info', title: t('settings.toast.resetSuccess', '已重置') })
|
|
59
|
+
} finally {
|
|
60
|
+
setLoading(false)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const onPrune = async () => {
|
|
65
|
+
setPruning(true)
|
|
66
|
+
try {
|
|
67
|
+
const ok = await prune()
|
|
68
|
+
toast({ type: ok ? 'success' : 'error', title: ok ? t('settings.toast.pruneSuccess') : t('settings.toast.pruneFailed') })
|
|
69
|
+
} finally {
|
|
70
|
+
setPruning(false)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div className="mx-auto w-full max-w-screen-xl px-4 py-6">
|
|
76
|
+
<div className="flex items-center justify-between gap-4">
|
|
77
|
+
<div className="flex items-center gap-2">
|
|
78
|
+
<Link to="/">
|
|
79
|
+
<Button variant="ghost" size="sm">
|
|
80
|
+
<ArrowLeft className="h-4 w-4" />
|
|
81
|
+
{t('create.back')}
|
|
82
|
+
</Button>
|
|
83
|
+
</Link>
|
|
84
|
+
<div className="text-lg font-semibold text-slate-900 dark:text-slate-100">{t('settings.title')}</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="flex flex-col sm:flex-row items-center sm:justify-end gap-2">
|
|
87
|
+
<Button className="w-full sm:w-auto" variant="secondary" size="sm" onClick={onReset} disabled={loading}>
|
|
88
|
+
<RotateCcw className="h-4 w-4" />
|
|
89
|
+
{t('settings.cli.reset')}
|
|
90
|
+
</Button>
|
|
91
|
+
<Button className="w-full sm:w-auto" variant="primary" size="sm" onClick={onSave} disabled={loading}>
|
|
92
|
+
<Save className="h-4 w-4" />
|
|
93
|
+
{t('settings.cli.save')}
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className="mt-5 grid grid-cols-12 gap-4">
|
|
99
|
+
<div className="col-span-12 lg:col-span-7">
|
|
100
|
+
<div className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
|
|
101
|
+
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('settings.preferences.title')}</div>
|
|
102
|
+
<div className="mt-4 grid grid-cols-12 gap-4">
|
|
103
|
+
<div className="col-span-12 lg:col-span-6">
|
|
104
|
+
<div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.preferences.theme')}</div>
|
|
105
|
+
<div className="mt-2 flex gap-2">
|
|
106
|
+
<Button
|
|
107
|
+
size="sm"
|
|
108
|
+
variant={theme === 'light' ? 'primary' : 'secondary'}
|
|
109
|
+
onClick={() => setTheme('light')}
|
|
110
|
+
>
|
|
111
|
+
<Sun className="h-4 w-4" />
|
|
112
|
+
{t('settings.preferences.light')}
|
|
113
|
+
</Button>
|
|
114
|
+
<Button
|
|
115
|
+
size="sm"
|
|
116
|
+
variant={theme === 'dark' ? 'primary' : 'secondary'}
|
|
117
|
+
onClick={() => setTheme('dark')}
|
|
118
|
+
>
|
|
119
|
+
<Moon className="h-4 w-4" />
|
|
120
|
+
{t('settings.preferences.dark')}
|
|
121
|
+
</Button>
|
|
122
|
+
<Button
|
|
123
|
+
size="sm"
|
|
124
|
+
variant={theme === 'system' ? 'primary' : 'secondary'}
|
|
125
|
+
onClick={() => setTheme('system')}
|
|
126
|
+
>
|
|
127
|
+
<Laptop className="h-4 w-4" />
|
|
128
|
+
{t('settings.preferences.system')}
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div className="col-span-12 lg:col-span-6">
|
|
134
|
+
<div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.preferences.language')}</div>
|
|
135
|
+
<div className="mt-2 flex gap-2">
|
|
136
|
+
<Button
|
|
137
|
+
size="sm"
|
|
138
|
+
variant={i18n.language.startsWith('en') ? 'primary' : 'secondary'}
|
|
139
|
+
onClick={() => i18n.changeLanguage('en')}
|
|
140
|
+
>
|
|
141
|
+
<Languages className="h-4 w-4" />
|
|
142
|
+
{t('settings.preferences.en')}
|
|
143
|
+
</Button>
|
|
144
|
+
<Button
|
|
145
|
+
size="sm"
|
|
146
|
+
variant={i18n.language.startsWith('zh') ? 'primary' : 'secondary'}
|
|
147
|
+
onClick={() => i18n.changeLanguage('zh')}
|
|
148
|
+
>
|
|
149
|
+
<Languages className="h-4 w-4" />
|
|
150
|
+
{t('settings.preferences.zh')}
|
|
151
|
+
</Button>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div className="mt-4 rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
|
|
158
|
+
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('settings.cli.title')}</div>
|
|
159
|
+
<div className="mt-4 grid grid-cols-12 gap-3">
|
|
160
|
+
<div className="col-span-12">
|
|
161
|
+
<div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.cli.baseDir')}</div>
|
|
162
|
+
<div className="mt-1">
|
|
163
|
+
<Input
|
|
164
|
+
value={cfg.baseDir || ''}
|
|
165
|
+
onChange={(e) => setCfg((c) => ({ ...c, baseDir: e.target.value }))}
|
|
166
|
+
placeholder={t('settings.cli.baseDirPlaceholder')}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="col-span-12">
|
|
171
|
+
<div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.cli.openCommand')}</div>
|
|
172
|
+
<div className="mt-1">
|
|
173
|
+
<Input
|
|
174
|
+
value={cfg.openCommand || ''}
|
|
175
|
+
onChange={(e) => setCfg((c) => ({ ...c, openCommand: e.target.value }))}
|
|
176
|
+
placeholder={t('settings.cli.openCommandPlaceholder')}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div className="col-span-12">
|
|
181
|
+
<div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.cli.editorCommand')}</div>
|
|
182
|
+
<div className="mt-1">
|
|
183
|
+
<Input
|
|
184
|
+
value={cfg.editorCommand || ''}
|
|
185
|
+
onChange={(e) => setCfg((c) => ({ ...c, editorCommand: e.target.value }))}
|
|
186
|
+
placeholder={t('settings.cli.editorCommandPlaceholder')}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div className="mt-4 rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
|
|
194
|
+
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('settings.maintenance.title')}</div>
|
|
195
|
+
<div className="mt-3">
|
|
196
|
+
<Button className="w-full sm:w-auto" variant="secondary" size="sm" onClick={onPrune} disabled={pruning}>
|
|
197
|
+
<Eraser className="h-4 w-4" />
|
|
198
|
+
{t('settings.maintenance.prune')}
|
|
199
|
+
</Button>
|
|
200
|
+
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
|
|
201
|
+
{t('settings.maintenance.pruneDesc')}
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div className="col-span-12 lg:col-span-5">
|
|
208
|
+
<div className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
|
|
209
|
+
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('settings.note.title')}</div>
|
|
210
|
+
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
|
|
211
|
+
{t('settings.note.desc')}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
)
|
|
218
|
+
}
|