From 534f7262abd969c8f2fafdd5e1f3916a956a2ebc Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:21:46 -0700 Subject: [PATCH] =?UTF-8?q?Phase=206:=20design=20polish=20=E2=80=94=20acce?= =?UTF-8?q?pt=20confetti=20+=20distraction-free=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- BUILD_PLAN.md | 11 +++-- web/src/App.tsx | 59 ++++++++++++++++++------ web/src/components/DocList/DocList.tsx | 2 +- web/src/components/Editor/EditorCore.tsx | 47 ++++++++++++++++++- web/src/index.css | 39 ++++++++++++++++ 5 files changed, 135 insertions(+), 23 deletions(-) diff --git a/BUILD_PLAN.md b/BUILD_PLAN.md index 014458a..819a117 100644 --- a/BUILD_PLAN.md +++ b/BUILD_PLAN.md @@ -60,11 +60,11 @@ Multi-session build. **Source of truth for what's done and what's next.** Update - Tests: `TestVoicePassCoexists` (grammar+voice coexist, unified response, null→"" replacement, voice re-run scoped). go build/vet/test clean, tsc clean, vite build OK; live smoke vs a fake vLLM (voice anchored at 62, grammar preserved, unified list `[grammar, voice]`). - **Known limitation (carried from Phase 3)**: `findRange` anchors within a single textblock, so a voice passage spanning a paragraph break (`\n\n`) won't decorate. Model passages usually sit within one paragraph; multi-block anchoring is deferred. -### Phase 6 — Design system & polish -- [ ] Full pastel tokens, Nunito + Lora + JetBrains Mono -- [ ] Shape language, shadows, transitions -- [ ] Signature animations (suggestion fade-float, accept confetti, breathing checkpoint dot) -- [ ] Distraction-free mode +### Phase 6 — Design system & polish ✅ +- [x] Full pastel tokens, Nunito + Lora + JetBrains Mono — `@theme` tokens + Google Fonts (landed in Phase 0, in use throughout) +- [x] Shape language, shadows, transitions — `--radius-*`, `--shadow-soft`, global `200ms ease` on interactive elements +- [x] Signature animations (suggestion fade-float, accept confetti, breathing checkpoint dot) — fade-float + breathing dot already live; **accept confetti** added this phase: CSS-only 4-dot burst (`petal-confetti`/`@keyframes petal-confetti`, direction via `--dx`/`--dy` inline), spawned in `EditorCore.handleAccept` at the card position, auto-cleared after 720ms +- [x] Distraction-free mode — entered on editor focus (`EditorCore` `onFocus` → `App.setFocusMode`), the doc-list sidebar slides left + collapses to 0 width (`.petal-sidebar`/`.petal-sidebar-hidden`, 280ms), editor canvas re-centers full-width. Restored by Escape or a pointer-down outside the centered canvas (gutters, header, status bar via `handleChromeDown` + `canvasRef` containment check) ### Phase 7 — Spell check - [ ] nspell browser-side (en-US), vendor dictionaries @@ -81,4 +81,5 @@ Multi-session build. **Source of truth for what's done and what's next.** Update - 2026-06-25: **Phase 2 complete.** Backend `internal/docs`: chi sub-router (list/create/get/update/delete) mounted at `/api/docs`, local-user scoped, RETURNING on create, COALESCE partial-update (one PUT serves rename + full save), 404/400 JSON errors; `handlers_test.go` walks the full lifecycle. Frontend: `api/client.ts`, `useAutoSave` (1.5s debounce + `saveNow` flush), `EditorCore` (Tiptap StarterKit/Underline/TextAlign/Placeholder/CharacterCount) + `Toolbar`, `DocList`/`DocListItem`, `StatusBar`, rewritten `App.tsx` orchestrating load/select/create/delete with optimistic sidebar patching. `.petal-prose` styles (Lora body, Nunito headings). tsc clean, vite build OK, go build OK; smoke-tested full CRUD incl. CJK title round-trip + SPA serve. Next: **Phase 3 (LLM grammar checkpoint).** - 2026-06-25: **Phase 3 complete.** Backend `internal/llm`: `LLMClient` interface + factory (vLLM OpenAI-compat + Ollama native, both Complete/Stream), `prompts.go` (checkpoint + Ask Petal templates), `checkpoint.go` (brace-matched JSON salvage, per-doc 30s `RateLimiter`, doc/history truncation). `internal/suggestions`: `/api/docs/:id/check` + `:id/suggestions` + `/api/suggestions/:id/{accept,dismiss}`; each check replaces the pending set in a tx (accepted/rejected kept as history), throttled checks return the current set, positions located by `strings.Index` (advisory only). Frontend: `useCheckpoint` (4s debounce, loads existing on doc open, run-token guards stale responses), `SuggestionHighlight` Tiptap extension rendering ProseMirror **decorations** re-anchored by `original` string on every doc change (precise textblock offset→PM-pos mapping, handles inline atoms), `SuggestionCard` (type-colored tag, original→replacement diff, accept applies replacement in-editor + PATCHes, hover-bridge with close delay), breathing rose checkpoint dot in StatusBar, suggestion fade-float + breathe CSS. Tests: llm parse/rate-limit/truncate, suggestions full flow + rate-limit over httptest with a stub client. go build/vet/test clean, tsc clean, vite build OK; end-to-end smoke-tested against a fake vLLM endpoint (anchoring verified: `I has`→0:5, `two apple`→6:15) and 502 path when LLM unreachable. Next: **Phase 4 (Ask Petal SSE chat).** - 2026-06-25: **Phase 5 complete.** Tier-1 voice-consistency pass. Backend: `internal/llm/voice.go` (`RunVoice` — whole document, no `TruncateDoc`, MaxTokens 2048, `VoiceInterval` 20s per-doc floor), standalone `voiceSystemPrompt`/`VoiceMessages` (not bundled with the grammar checkpoint). `internal/suggestions`: `POST /api/docs/:id/voice` route; `check`/`voice` collapsed into a shared `runPass(limiter, pass, scope)`; `pendingScope` makes `replacePending` family-aware (grammar deletes `type != 'voice'`, voice deletes `type = 'voice'`), so the two passes never clobber each other's pending flags; both endpoints now return the **unified** pending set (also fixed a latent throttle-returns-full-set vs success-returns-batch inconsistency). Frontend: `api.voiceDoc`, `useCheckpoint` → `voicing`/`runVoice` (shared run-token guard, reset on doc switch), honey "Check my voice 🍯" pill in `Toolbar` (→ "Reading…" while in flight), breathing honey dot + "Reading your voice…" in `StatusBar`. Voice flags' `replacement: null` round-trips to `""`; `SuggestionCard` already hides the diff row + Accept for those. Tests: `TestVoicePassCoexists` (coexistence both directions, unified response, null→"" replacement). go build/vet/test clean, tsc clean, vite build OK. Live smoke vs a fake vLLM: grammar check → grammar flag; voice pass → unified `[grammar@0, voice@62 (empty replacement)]`, grammar preserved. Known limitation: `findRange` is single-textblock, so a voice passage crossing a `\n\n` paragraph break won't decorate (deferred). Next: **Phase 6 (design system & polish).** +- 2026-06-25: **Phase 6 complete.** Design system & polish. Tokens/fonts/shape/transitions were already in place from Phase 0; this phase added the two missing signature pieces. **Accept confetti**: CSS-only burst (`.petal-confetti-dot` + `@keyframes petal-confetti`, each dot's trajectory from inline `--dx`/`--dy`), a `Confetti` component in `EditorCore` spawned at the accepted card's position on `handleAccept` and cleared after 720ms (timer cleaned up on unmount). **Distraction-free mode**: `EditorCore` gains an `onFocus` → `App` `focusMode` state; the doc-list sidebar is wrapped in `.petal-sidebar` and collapses via `.petal-sidebar-hidden` (width→0 + translateX + fade, 280ms) while the centered editor canvas re-centers into the full pane; restored by Escape (window keydown) or a pointer-down outside the canvas (`handleChromeDown` checks `canvasRef` containment; wired on the header, the editor scroll-gutter, and the status bar). tsc clean, vite build OK, go build/vet/test clean; binary boots and serves the rebuilt SPA with the new CSS embedded (confetti + sidebar-collapse classes verified in the served bundle). Next: **Phase 7 (browser-side spell check, nspell en-US).** - 2026-06-25: **Phase 4 complete.** Backend: `internal/llm/chat.go` (`StreamAskPetal` — conversational sampling params, reuses `AskPetalSystemPrompt`/`TrimHistory`), `internal/suggestions/chat.go` (`POST /api/suggestions/:id/chat` — one user-scoped join loads the suggestion + parent `content_text`, `surroundingParagraph` extracts the `\n\n`-bounded paragraph at `from_pos` with whole-doc fallback, streams `event: token`/`event: done` SSE frames with JSON-encoded data, `X-Accel-Buffering: no`, real `http.Flusher` per chunk; LLM-down → 502 before SSE headers, unknown id → 404). Handler imports the interface only. Frontend: `streamSuggestionChat` (fetch + ReadableStream SSE parser, abortable), `AskPetal.tsx` (in-component history — no persistence, pre-seeded first bubble, rose/lavender bubbles, CJK font stack per Note #17, streaming caret), `SuggestionCard` "Ask Petal ✨" pill that pins the card open (hover-close suppressed, click-away closes) and widens it to 340px. Tests: `chat_test.go` (streamed-text concat + done event, server-side context injection asserted on the system message, sampling params, 404, `surroundingParagraph` unit). go build/vet/test clean, tsc clean, vite build OK. Live SSE smoke test against a fake streaming vLLM (fresh ports 8077/8088 — a pre-existing dev petal on :8099 left untouched): tokens flushed individually through the chi middleware stack, `done` terminator, 502 on LLM-down, 404 on unknown suggestion all verified. Next: **Phase 5 (voice consistency pass, Tier 1).** diff --git a/web/src/App.tsx b/web/src/App.tsx index f24140b..0f62b84 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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(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 (
@@ -158,19 +179,24 @@ export default function App() {
- +
+ +
{currentDoc ? ( <> -
-
+
+
handleTitleChange(e.target.value)} @@ -189,15 +215,18 @@ export default function App() { onDismiss={handleDismiss} onVoiceCheck={runVoice} voicing={voicing} + onFocusMode={() => setFocusMode(true)} />
- +
+ +
) : (