feat(config): add system panel with reset and starship theme, add onboarding wizard
All checks were successful
Beta Release / beta (push) Successful in 41s
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:
224
web/src/components/OnboardingWizard.jsx
Normal file
224
web/src/components/OnboardingWizard.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user