Phase 14: bedtime nag + night mode (dark theme + falling stars)

Companion warmth:
- ENCOURAGEMENTS grown 5->10; new BEDTIME lines (warm/playful, zh-first)
- useCompanion heartbeat gains a bedtime branch (>=11pm, while actively
  writing) gated by a 30min cooldown; new 'bedtime' BubbleTone paces it

Night mode (auto at ~11pm, local clock):
- lib/night.ts centralizes isBedtime() + the window, shared with the nag
- useNightMode toggles a `petal-night` class on <html>; index.css re-points
  only the palette tokens, so every Tailwind color utility flips via var()
  with no component edits (600ms dusk fade; print stays white)
- PetalFall gains a `night` prop: chunky cartoon power stars (makeCartoonStar,
  5 candy colors) mixed ~70/30 with twinkle sparkles; each star spins at its
  own rate (~0.5-2.2 rad/s, random direction), falls straight down, shimmers

Verified: go vet/test, tsc, vitest 51/51, vite build; real-browser
screenshots/video (clock mocked to 23:30).

Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
prosolis
2026-06-26 15:10:36 -07:00
parent aebdc2679a
commit 9576340391
8 changed files with 303 additions and 20 deletions

View File

@@ -0,0 +1,25 @@
import { useEffect, useState } from 'react'
import { isBedtime } from '../lib/night'
// Tracks whether it's late enough to shift Petal into its calm night mode (dark
// theme + falling stars instead of petals). Re-checks once a minute so the swap
// happens on its own as the clock rolls past 11pm — no reload needed. Returns
// the boolean and also toggles a `petal-night` class on <html> so the CSS token
// overrides cascade to everything (body background, panels, portals included).
export function useNightMode(): boolean {
const [night, setNight] = useState(() => isBedtime())
useEffect(() => {
const tick = () => setNight(isBedtime())
tick()
const id = setInterval(tick, 60_000)
return () => clearInterval(id)
}, [])
useEffect(() => {
document.documentElement.classList.toggle('petal-night', night)
return () => document.documentElement.classList.remove('petal-night')
}, [night])
return night
}