feat(config): add system panel with reset and starship theme, add onboarding wizard
All checks were successful
Beta Release / beta (push) Successful in 41s

- Add PanelSystem with reset config and apply starship theme (charm/zerotwo/default)
- Add OnboardingWizard that activates when profile is empty on first run
- Fix <thing> tag parsing in Shell AI messages (wait for </thing> before rendering)
- Add /api/config/reset and /api/starship/apply-theme endpoints
- Wire wizard trigger in App.jsx based on profile completeness

💘 Generated with Crush

Assisted-by: MiniMax-M2.7 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-22 20:36:36 +02:00
parent 83d7a573c7
commit 5bbac499a7
6 changed files with 526 additions and 7 deletions

View File

@@ -0,0 +1,224 @@
import { useState } from 'react'
import { Sparkles, ArrowRight } from 'lucide-react'
import { useI18n, LANGUAGES } from '../i18n'
import { getLayoutList } from '../i18n/keyboards'
const STEPS = [
{ key: 'welcome', title: 'welcome', field: null },
{ key: 'name', title: 'name', field: 'name' },
{ key: 'language', title: 'language', field: 'language' },
{ key: 'keyboard', title: 'keyboard', field: 'keyboard' },
{ key: 'editor', title: 'editor', field: 'editor' },
{ key: 'done', title: 'done', field: null },
]
const EDITOR_SUGGESTIONS = ['vim', 'nvim', 'vscode', 'emacs', 'nano', 'helix']
export default function OnboardingWizard({ api, onComplete }) {
const { t, language, keyboard, setLanguage, setKeyboard } = useI18n()
const [step, setStep] = useState(0)
const [answers, setAnswers] = useState({
name: '',
language: 'fr',
keyboard: 'azerty',
editor: '',
})
const [saving, setSaving] = useState(false)
const current = STEPS[step]
const layouts = getLayoutList()
const goNext = () => {
if (step < STEPS.length - 1) setStep(step + 1)
}
const handleSave = async () => {
setSaving(true)
try {
await api.saveProfile({
name: answers.name,
pseudo: answers.name.split(' ')[0] || 'user',
editor: answers.editor,
})
await api.savePreferences({
language: answers.language,
keyboard_layout: answers.keyboard,
})
onComplete()
} catch (err) {
console.error(err)
}
setSaving(false)
}
return (
<div className="onboarding-overlay">
<div className="onboarding-card">
<div className="onboarding-header">
<Sparkles size={20} style={{ color: 'var(--accent)' }} />
<span> Muyue Setup</span>
</div>
<div className="onboarding-progress">
{STEPS.map((_, i) => (
<div key={i} className={`onboarding-dot ${i === step ? 'active' : ''} ${i < step ? 'done' : ''}`} />
))}
</div>
<div className="onboarding-body">
{current.key === 'welcome' && (
<div className="onboarding-step">
<div className="onboarding-title">Bienvenue ! 👋</div>
<div className="onboarding-desc">
Je suis votre assistant de configuration. Quelques questions rapides pour personnaliser votre expérience.
</div>
</div>
)}
{current.key === 'name' && (
<div className="onboarding-step">
<div className="onboarding-title">Comment vous appelez-vous ?</div>
<input
className="onboarding-input"
placeholder="Votre nom..."
value={answers.name}
onChange={e => setAnswers(a => ({ ...a, name: e.target.value }))}
autoFocus
/>
</div>
)}
{current.key === 'language' && (
<div className="onboarding-step">
<div className="onboarding-title">Quelle langue pr\u00e9f\u00e9rez-vous ?</div>
<div className="onboarding-chips">
{LANGUAGES.map(lang => (
<div
key={lang.id}
className={`chip ${answers.language === lang.id ? 'active' : ''}`}
onClick={() => setAnswers(a => ({ ...a, language: lang.id }))}
>
{lang.name}
</div>
))}
</div>
</div>
)}
{current.key === 'keyboard' && (
<div className="onboarding-step">
<div className="onboarding-title">Disposition du clavier ?</div>
<div className="onboarding-chips">
{layouts.map(l => (
<div
key={l.id}
className={`chip ${answers.keyboard === l.id ? 'active' : ''}`}
onClick={() => setAnswers(a => ({ ...a, keyboard: l.id }))}
>
{l.name}
</div>
))}
</div>
</div>
)}
{current.key === 'editor' && (
<div className="onboarding-step">
<div className="onboarding-title">Quel \u00e9diteur utilisez-vous ?</div>
<div className="onboarding-chips">
{EDITOR_SUGGESTIONS.map(ed => (
<div
key={ed}
className={`chip ${answers.editor === ed ? 'active' : ''}`}
onClick={() => setAnswers(a => ({ ...a, editor: ed }))}
>
{ed}
</div>
))}
</div>
<input
className="onboarding-input"
style={{ marginTop: 12 }}
placeholder="Autre (vim, nvim, vscode...)"
value={answers.editor}
onChange={e => setAnswers(a => ({ ...a, editor: e.target.value }))}
autoFocus
/>
</div>
)}
{current.key === 'done' && (
<div className="onboarding-step">
<div className="onboarding-title">C'est parti ! 🚀</div>
<div className="onboarding-desc">
Votre profil est configur\u00e9. Vous pouvez toujours ajuster les param\u00e8tres dans l'onglet Configuration.
</div>
</div>
)}
</div>
<div className="onboarding-footer">
{current.key === 'done' ? (
<button className="primary" onClick={handleSave} disabled={saving}>
{saving ? '...' : 'Commencer'}
</button>
) : (
<button className="primary" onClick={goNext}>
Suivant <ArrowRight size={14} />
</button>
)}
</div>
</div>
<style>{`
.onboarding-overlay {
position: fixed; inset: 0; z-index: 500;
background: rgba(10,10,12,0.85);
display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(8px);
}
.onboarding-card {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
width: 480px; max-width: 90vw;
box-shadow: 0 24px 64px rgba(0,0,0,0.5);
overflow: hidden;
}
.onboarding-header {
display: flex; align-items: center; gap: 8px;
padding: 16px 20px; font-size: 14px; font-weight: 700;
color: var(--accent); border-bottom: 1px solid var(--border);
background: var(--bg-surface);
}
.onboarding-progress {
display: flex; gap: 6px; padding: 14px 20px;
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
}
.onboarding-dot {
width: 32px; height: 4px; border-radius: 2px;
background: var(--bg-input); transition: all 0.3s;
}
.onboarding-dot.active { background: var(--accent); }
.onboarding-dot.done { background: var(--accent-dim); }
.onboarding-body { padding: 28px 24px; min-height: 200px; }
.onboarding-step { display: flex; flex-direction: column; gap: 16px; }
.onboarding-title { font-size: 18px; font-weight: 700; color: var(--text-primary); }
.onboarding-desc { font-size: 14px; color: var(--text-tertiary); line-height: 1.6; }
.onboarding-input {
width: 100%; background: var(--bg-input); border: 1px solid var(--border);
border-radius: var(--radius); padding: 10px 14px; color: var(--text-primary);
font-size: 14px; outline: none; transition: border-color 0.2s, box-shadow 0.2s;
}
.onboarding-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--border-accent); }
.onboarding-chips { display: flex; gap: 8px; flex-wrap: wrap; }
.onboarding-footer {
display: flex; justify-content: flex-end; gap: 8px;
padding: 16px 20px; border-top: 1px solid var(--border);
background: var(--bg-surface);
}
`}</style>
</div>
)
}