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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user