Phase 10: organization & polish — cross-doc search, tags, touch, warm failures

Cross-document FTS5 search (trigram tokenizer for EN + space-free CJK, kept in
sync by triggers, back-filled from existing docs). GET /api/search uses the FTS
index for queries >=3 runes and a LIKE fallback for 1-2 (so 2-char Chinese words
resolve); snippets are built in Go with rune-aware boundaries and sentinel
highlights.

Tags: user-scoped tags + document_tags join (both cascade), idempotent
create/assign, per-tag doc counts. Doc list and search carry each doc's tags
(one tagsByDoc query). Frontend: useTags, TagChip/TagPicker/SearchBox, rewritten
DocList with chips + filter bar + search.

Tablet/touch: responsive sidebar drawer (hamburger + scrim <768px), coarse-
pointer tap targets, tap-to-open + outside-pointerdown-close for suggestion
cards.

Warm LLM-down state: useCheckpoint llmDown flag drives a gentle bilingual
StatusBar note (writing still saves locally).

Migration 0004 (tags + FTS). Tests: tags lifecycle, search EN/CJK/LIKE/update-
reindex. go build/vet/test, tsc, vite all clean; verified live on deployment host.

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-26 06:29:56 -07:00
parent 60eba25fee
commit 9e141e4169
21 changed files with 1841 additions and 51 deletions

View File

@@ -11,6 +11,9 @@ interface Props {
checking: boolean
// True while a whole-document voice pass runs — shows a breathing honey dot.
voicing: boolean
// True when Petal can't reach its LLM helper — shows a gentle, reassuring note
// (the writing still saves locally, so this is awareness, not an error).
llmDown: boolean
}
const SAVE_LABEL: Record<SaveStatus, string> = {
@@ -24,7 +27,7 @@ const SAVE_LABEL: Record<SaveStatus, string> = {
// StatusBar is the slim footer: word count on the left, save state and the
// grammar-checkpoint indicator on the right. The checkpoint dot is a soft rose
// circle that breathes while a check is in flight (spec → Signature animations).
export function StatusBar({ wordCount, text, saveStatus, checking, voicing }: Props) {
export function StatusBar({ wordCount, text, saveStatus, checking, voicing, llmDown }: Props) {
const label = SAVE_LABEL[saveStatus]
// The expanded stats panel toggles open when the word count is clicked.
const [statsOpen, setStatsOpen] = useState(false)
@@ -86,6 +89,20 @@ export function StatusBar({ wordCount, text, saveStatus, checking, voicing }: Pr
</span>
</>
)}
{llmDown && !checking && !voicing && (
<>
<span aria-hidden>·</span>
<span
className="inline-flex items-center gap-1.5"
title="Petal can't reach its writing helper right now — your text is still saved."
style={{ color: 'var(--color-honey)' }}
>
<span aria-hidden>🌙</span>
<span></span>
<span style={{ opacity: 0.75 }}>· Petal's helper is resting · 文字已保存</span>
</span>
</>
)}
{label && (
<>
<span aria-hidden>·</span>