Phase 6: design polish — accept confetti + distraction-free mode

- Accept confetti: CSS-only 4-dot burst spawned at the accepted card,
  trajectory via inline --dx/--dy, auto-cleared after 720ms.
- Distraction-free mode: editor focus collapses the doc-list sidebar
  (width→0 slide), canvas re-centers full-width; Escape or a click
  outside the canvas (header/gutter/status bar) restores it.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-25 21:21:46 -07:00
parent 0fa70979a0
commit 534f7262ab
5 changed files with 135 additions and 23 deletions

View File

@@ -12,6 +12,10 @@ export default function App() {
const [title, setTitle] = useState('')
const [wordCount, setWordCount] = useState(0)
const [ready, setReady] = useState(false)
// Distraction-free mode: entered on editor focus, collapses the doc-list
// sidebar. Escape or a click outside the editor canvas restores it.
const [focusMode, setFocusMode] = useState(false)
const canvasRef = useRef<HTMLDivElement>(null)
const { status, schedule, saveNow } = useAutoSave(currentDoc?.id ?? null)
const {
@@ -135,6 +139,22 @@ export default function App() {
[removeSuggestion],
)
// Escape always restores the sidebar while in distraction-free mode.
useEffect(() => {
if (!focusMode) return
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setFocusMode(false)
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [focusMode])
// A pointer-down outside the centered editor canvas (the cream gutters, the
// header, the status bar) also restores the sidebar.
const handleChromeDown = useCallback((e: React.MouseEvent) => {
if (!canvasRef.current?.contains(e.target as Node)) setFocusMode(false)
}, [])
const handleDismiss = useCallback(
async (s: Suggestion) => {
removeSuggestion(s.id)
@@ -150,6 +170,7 @@ export default function App() {
return (
<div className="flex h-full flex-col">
<header
onMouseDown={handleChromeDown}
className="flex h-12 shrink-0 items-center gap-2 px-5"
style={{ borderBottom: '1px solid var(--color-border)' }}
>
@@ -158,19 +179,24 @@ export default function App() {
</header>
<div className="flex min-h-0 flex-1">
<DocList
docs={docs}
selectedId={currentDoc?.id ?? null}
onSelect={openDoc}
onCreate={handleCreate}
onDelete={handleDelete}
/>
<div className={`petal-sidebar shrink-0${focusMode ? ' petal-sidebar-hidden' : ''}`}>
<DocList
docs={docs}
selectedId={currentDoc?.id ?? null}
onSelect={openDoc}
onCreate={handleCreate}
onDelete={handleDelete}
/>
</div>
<main className="flex min-w-0 flex-1 flex-col">
{currentDoc ? (
<>
<div className="flex flex-1 flex-col overflow-y-auto px-6 py-8">
<div className="mx-auto flex w-full max-w-[720px] flex-1 flex-col">
<div
onMouseDown={handleChromeDown}
className="flex flex-1 flex-col overflow-y-auto px-6 py-8"
>
<div ref={canvasRef} className="mx-auto flex w-full max-w-[720px] flex-1 flex-col">
<input
value={title}
onChange={(e) => handleTitleChange(e.target.value)}
@@ -189,15 +215,18 @@ export default function App() {
onDismiss={handleDismiss}
onVoiceCheck={runVoice}
voicing={voicing}
onFocusMode={() => setFocusMode(true)}
/>
</div>
</div>
<StatusBar
wordCount={wordCount}
saveStatus={status}
checking={checking}
voicing={voicing}
/>
<div onMouseDown={handleChromeDown}>
<StatusBar
wordCount={wordCount}
saveStatus={status}
checking={checking}
voicing={voicing}
/>
</div>
</>
) : (
<div