feat(ST-805): wire useSoloRealtime + live indicator + column-move animatie
- SoloBoard roept useSoloRealtime(productId) aan, zo komt elke task/
story-mutatie uit web/REST/MCP binnen via SSE en wordt door de
store-dispatcher (ST-804) verwerkt
- markPending/clearPending rond de drag-drop optimistic write zodat
de echo van de eigen Server Action de store niet dubbel beweegt
- RealtimeIndicator: kleine status-dot in de header
- groen ('Live') wanneer SSE-stream open OF tijdens de eerste 2s
grace-period (animatie B in de hook — voorkomt micro-flikker)
- grijs ('Verbinden…') na 2s in connecting-state
- rood ('Verbroken') na 2s in disconnected-state
- Animatie A (kolom-move): bij task UPDATE-events wikkelt de hook de
store-dispatch in document.startViewTransition + flushSync. SoloTask-
cards krijgen view-transition-name `solo-task-<id>` (alleen wanneer
niet aan het draggen) zodat de browser de positie-shift soepel
animeert van bezig naar klaar (en omgekeerd)
Bestaande 89 tests blijven groen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
562735b98b
commit
ccaae75468
3 changed files with 105 additions and 10 deletions
|
|
@ -14,6 +14,7 @@
|
|||
'use client'
|
||||
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { flushSync } from 'react-dom'
|
||||
import { useSoloStore } from '@/stores/solo-store'
|
||||
import type { RealtimeEvent } from '@/stores/solo-store'
|
||||
|
||||
|
|
@ -81,7 +82,34 @@ export function useSoloRealtime(productId: string) {
|
|||
if (!e.data) return
|
||||
try {
|
||||
const payload = JSON.parse(e.data) as RealtimeEvent
|
||||
handleEvent(payload)
|
||||
// ST-805 animatie A: kanban-move animeren via View Transitions API.
|
||||
// Voor task UPDATE-events wrap'en we de store-update in een view
|
||||
// transition. De browser snapshot't de DOM voor en na, en animeert
|
||||
// het verschil — vereist `view-transition-name` op de cards.
|
||||
// Andere events (task INSERT/DELETE, story-events) krijgen geen
|
||||
// animatie; die zijn niet zichtbaar als positie-shift in de
|
||||
// kanban-kolommen.
|
||||
const animate =
|
||||
payload.entity === 'task' &&
|
||||
payload.op === 'U' &&
|
||||
typeof document !== 'undefined' &&
|
||||
typeof (document as Document & { startViewTransition?: unknown }).startViewTransition ===
|
||||
'function'
|
||||
if (animate) {
|
||||
// flushSync forceert React om de re-render synchroon te doen
|
||||
// tijdens de view-transition callback, zodat de nieuwe DOM-state
|
||||
// wordt gesnapshot voor de animatie. Zonder flushSync rendert
|
||||
// React asynchroon en captured de browser nog de oude state.
|
||||
;(
|
||||
document as Document & {
|
||||
startViewTransition: (cb: () => void) => unknown
|
||||
}
|
||||
).startViewTransition(() => {
|
||||
flushSync(() => handleEvent(payload))
|
||||
})
|
||||
} else {
|
||||
handleEvent(payload)
|
||||
}
|
||||
} catch (err) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.error('[realtime] failed to parse event', err, e.data)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue