feat(renderer): add FirstRunWizard with theme/channel/template steps

This commit is contained in:
2026-06-08 06:34:40 +05:30
parent 59fd9b8646
commit cc33e6c7a8
2 changed files with 125 additions and 0 deletions
@@ -0,0 +1,81 @@
import { useState } from 'react';
import { useAppStore } from '@/stores/app-store';
import { useSettingsStore } from '@/stores/settings-store';
const TEMPLATES = {
blank: '',
readme: '# Project\n\nDescription.\n\n## Usage\n\n```\nnpm install\n```\n',
meeting: '# Meeting Notes — YYYY-MM-DD\n\n## Attendees\n\n- \n## Agenda\n\n1. \n\n## Action items\n\n- [ ] \n',
blog: '# Title\n\n*Subtitle*\n\nLorem ipsum.\n\n---\n\n## Section 1\n',
};
export function FirstRunWizard() {
const firstRun = useAppStore((s) => s.firstRun);
const setFirstRun = useAppStore((s) => s.setFirstRun);
const theme = useSettingsStore((s) => s.theme);
const setSetting = useSettingsStore((s) => s.setSetting);
const updateChannel = useSettingsStore((s) => s.updateChannel);
const [step, setStep] = useState(0);
const [template, setTemplate] = useState<keyof typeof TEMPLATES>('blank');
if (!firstRun) return null;
const close = () => setFirstRun(false);
return (
<div data-testid="first-run-wizard" role="dialog" aria-modal="true" className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center">
<div className="bg-white dark:bg-neutral-900 rounded-lg p-6 w-[28rem] shadow-xl">
{step === 0 && (
<div>
<h2 className="text-lg font-semibold mb-2">Pick a theme</h2>
<div className="flex gap-2 mb-4">
{(['light', 'dark', 'system'] as const).map((t) => (
<label key={t} className="flex items-center gap-1">
<input type="radio" name="theme" checked={theme === t} onChange={() => setSetting('theme', t)} />
{t}
</label>
))}
</div>
</div>
)}
{step === 1 && (
<div>
<h2 className="text-lg font-semibold mb-2">Update channel</h2>
<div className="flex flex-col gap-2 mb-4">
<label className="flex items-center gap-2">
<input type="radio" name="channel" checked={updateChannel === 'github'} onChange={() => setSetting('updateChannel', 'github')} />
GitHub Releases (public)
</label>
<label className="flex items-center gap-2">
<input type="radio" name="channel" checked={updateChannel === 'concreteinfo'} onChange={() => setSetting('updateChannel', 'concreteinfo')} />
ConcreteInfo self-hosted
</label>
</div>
</div>
)}
{step === 2 && (
<div>
<h2 className="text-lg font-semibold mb-2">Starter template</h2>
<select value={template} onChange={(e) => setTemplate(e.target.value as any)} className="border rounded px-2 py-1 w-full mb-4">
<option value="blank">Blank</option>
<option value="readme">README</option>
<option value="meeting">Meeting notes</option>
<option value="blog">Blog post</option>
</select>
</div>
)}
<div className="flex justify-between items-center mt-4">
<button onClick={close} className="text-sm text-neutral-500">Skip</button>
<div className="flex gap-2">
{step > 0 && <button onClick={() => setStep(step - 1)}>Back</button>}
{step < 2 ? (
<button onClick={() => setStep(step + 1)} className="px-3 py-1 rounded bg-brand text-white">Next</button>
) : (
<button onClick={() => { useAppStore.getState().newBuffer(TEMPLATES[template]); close(); }} className="px-3 py-1 rounded bg-brand text-white">Done</button>
)}
</div>
</div>
</div>
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, test, beforeEach, vi } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { useAppStore } from '@/stores/app-store';
import { useSettingsStore } from '@/stores/settings-store';
import { FirstRunWizard } from '@/components/FirstRunWizard';
beforeEach(() => {
useAppStore.setState({ firstRun: true, modal: { kind: null } } as any);
useSettingsStore.setState({ theme: 'system', updateChannel: 'github' } as any);
});
describe('FirstRunWizard', () => {
test('renders nothing when firstRun is false', () => {
useAppStore.setState({ firstRun: false } as any);
const { container } = render(<FirstRunWizard />);
expect(container.firstChild).toBeNull();
});
test('renders step 1 by default', () => {
render(<FirstRunWizard />);
expect(screen.getByText(/theme/i)).toBeInTheDocument();
});
test('skip link closes the wizard and sets firstRun false', () => {
render(<FirstRunWizard />);
fireEvent.click(screen.getByText(/skip/i));
expect(useAppStore.getState().firstRun).toBe(false);
});
test('selecting a theme and clicking Next moves to step 2', () => {
render(<FirstRunWizard />);
fireEvent.click(screen.getByLabelText(/dark/i));
fireEvent.click(screen.getByText(/next/i));
expect(useSettingsStore.getState().theme).toBe('dark');
});
test('selecting concreteinfo channel persists it', () => {
render(<FirstRunWizard />);
fireEvent.click(screen.getByText(/next/i));
fireEvent.click(screen.getByLabelText(/concreteinfo/i));
fireEvent.click(screen.getByText(/next/i));
expect(useSettingsStore.getState().updateChannel).toBe('concreteinfo');
});
});