server/docs/features/universell_overfoering.md
vegard 7babafc65f Storyboard-spec, canvas-primitiv og universell overføring
Tre nye/omskrevne dokumenter som definerer fritt-canvas arkitekturen:
- Canvas-primitiv: felles underlag for whiteboard og storyboard (pan, zoom, drag, viewport culling)
- Universell overføring: message_placements-tabell og blokk-til-blokk drag-and-drop
- Storyboard: full spec med episode-sekvens, LiveKit-kobling, inter-board overføring

Inkluderer også storyboard-relaterte mini-proposals (ghost cards, pinboard mode,
flow meter, emotion tags, card chaining, collaborative cursors, card heat map).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 18:10:31 +01:00

8.7 KiB

Feature: Universell overføring — flytt objekter mellom blokker

Filsti: docs/features/universell_overfoering.md

1. Konsept

Universell overføring er mekanikken som lar brukere flytte meldingsboks-objekter mellom vilkårlige blokker: fra storyboard til chat, fra kanban til kalender, fra chat til storyboard. Enhver blokk kan sende og motta objekter. Meldingsboksen er alltid det underliggende objektet — overføringen endrer kun view-config, ikke selve meldingen.

1.1 Grunnprinsipp

En meldingsboks (node i kunnskapsgrafen) kan ha flere samtidige roller via view-configs (se meldingsboks.md §4). Universell overføring gjør dette til en førsteklasses brukerinteraksjon:

Bruker drar kort fra Storyboard → slipper på Chat-blokken
  → Meldingen får en ny plassering i chatten (placement-record)
  → Meldingen beholder sin posisjon på storyboardet
  → Diskusjonstråden er synlig begge steder (samme objekt)

Alternativt kan brukeren flytte (fjerne fra kilde, legge til i mål) i stedet for å kopiere (beholde begge). Kontekstmeny gir valget.

2. Plasseringsrelasjon (Placement)

Når en melding vises i en kontekst (chat, kanban, storyboard, kalender), trenger vi metadata om hvordan den vises der. Dette er plasseringsrelasjonen — en edge i grafen mellom meldingen og konteksten, med metadata.

2.1 Datamodell

CREATE TABLE message_placements (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    message_id   UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
    context_type TEXT NOT NULL,  -- 'chat', 'kanban', 'storyboard', 'calendar', 'notes'
    context_id   UUID NOT NULL,  -- channel_id, board_id, episode_id, calendar_id, note_id
    entered_at   TIMESTAMPTZ NOT NULL DEFAULT now(),  -- når objektet ankom denne konteksten
    position     JSONB,          -- kontekst-spesifikk posisjon (se §2.2)
    UNIQUE (message_id, context_type, context_id)
);

CREATE INDEX idx_placements_context ON message_placements(context_type, context_id, entered_at);
CREATE INDEX idx_placements_message ON message_placements(message_id);

2.2 Posisjonsdata per kontekst

position-feltet er JSONB og inneholder kontekst-spesifikk plassering:

Kontekst position-innhold Eksempel
Chat null (sorteres etter entered_at) null
Kanban { "column_id": "...", "position": 1.5 } Kolonne + rekkefølge
Storyboard { "x": 340, "y": 120 } Fritt canvas-posisjon
Kalender { "date": "2026-03-20", "all_day": true } Dato + tidspunkt
Notes { "position": 3 } Rekkefølge i notatet

2.3 Forhold til eksisterende view-configs

message_placements erstatter ikke de eksisterende view-config-tabellene (kanban_card_view, calendar_event_view) umiddelbart. Strategien er:

  1. Fase 1: message_placements brukes for nye kontekster (storyboard, notes) og for overføringsmekanikken
  2. Fase 2: Eksisterende view-configs migreres gradvis til message_placements (kanban-posisjon, kalender-dato)
  3. Fase 3: kanban_card_view og calendar_event_view kan fjernes når all logikk bruker placements

2.4 entered_at vs created_at

  • messages.created_at = når meldingen ble skrevet
  • message_placements.entered_at = når meldingen ankom denne konteksten

En melding opprettet mandag i chatten som dras til storyboardet onsdag har created_at = mandag og storyboard-plassering med entered_at = onsdag. I chatten sorteres den etter sin chat-plasserings entered_at — som er mandag (opprinnelig plassering). I storyboardet vises den på canvas-posisjonen, uavhengig av tid.

3. Sende-mekanikk (source)

3.1 Drag-and-drop

Brukeren drar et objekt ut av en blokk. Når objektet forlater blokk-grensen:

  1. Objektet blir en "ghost" (semi-transparent drag-representasjon)
  2. Andre blokker som kan motta objektet highlighter sin mottakssone
  3. Slipp på en mottakssone → overføring

3.2 Kontekstmeny: "Send til..."

For situasjoner der drag-and-drop er upraktisk (mobil, lang avstand):

Høyreklikk kort → "Send til..." →
  ├── 📋 Kanban: Episodeplanlegging
  ├── 💬 Chat: #studio-diskusjon
  ├── 📅 Kalender: Redaksjonskalender
  └── 🎬 Storyboard: Episode 47

Listen viser alle blokker i det aktive workspacet som kan motta meldinger.

3.3 Flytt vs. kopier

  • Kopier (default): Meldingen får en ny plassering i mål-konteksten, beholder plasseringen i kilde-konteksten
  • Flytt (hold Shift ved drag, eller velg i kontekstmeny): Plasseringen i kilde-konteksten fjernes, ny plassering opprettes i mål

4. Mottak-mekanikk (target)

Hver blokk-type definerer en mottaker som bestemmer hva som skjer når et objekt ankommer:

4.1 Mottaker per blokk-type

Blokk Default-plassering Visuell feedback
Chat Ny melding i bunnen, entered_at = now() Kort blinker inn i chatflyten
Kanban Første kolonne (eller "Innboks"), posisjon øverst Kort glir inn i kolonnen
Storyboard Senter av viewport (eller slipp-posisjon) Kort fader inn
Kalender Dagens dato, heldagshendelse Dato highlighter
Notes Ny blokk i bunnen av notatet Tekst fades inn

4.2 Mottakssone-rendering

Hver blokk rendrer en visuell drop-target når en drag er aktiv:

  • Hele blokken lyser opp med en subtil border/glow
  • Spesifikke soner (f.eks. en kolonne i kanban, en dato i kalender) highlighter ved hover
  • Avviste drops (feil type objekt) viser en dempet tilstand

4.3 Mottaker-interface

interface BlockReceiver {
    /** Kan denne blokken motta dette objektet? */
    canReceive(message: Message): boolean;

    /** Opprett plassering for mottatt objekt */
    receive(message: Message, dropPosition?: { x: number, y: number }): Placement;

    /** Visuell feedback for aktiv drag */
    renderDropZone(): void;
}

Alle blokk-typer implementerer dette interfacet.

5. SpacetimeDB-integrasjon

Plasseringsdata for sanntidskontekster (storyboard, kanban) eies av SpacetimeDB:

#[table(name = message_placement, public)]
pub struct MessagePlacement {
    #[primary_key]
    pub id: String,
    pub message_id: String,
    pub context_type: String,
    pub context_id: String,
    pub entered_at: Timestamp,
    pub position_json: String,  // JSON-serialisert posisjon
    pub workspace_id: String,
}

#[reducer]
pub fn place_message(ctx: &ReducerContext, placement: MessagePlacement) { ... }

#[reducer]
pub fn remove_placement(ctx: &ReducerContext, message_id: String, context_type: String, context_id: String) { ... }

#[reducer]
pub fn move_on_canvas(ctx: &ReducerContext, placement_id: String, new_position_json: String) { ... }

Sync-workeren persisterer til PG message_placements-tabellen.

6. Responsivt design

  • Desktop: Drag-and-drop mellom blokker fungerer naturlig
  • Tablet: Drag-and-drop fungerer med touch, men "Send til..."-meny er primær
  • Mobil: Kun "Send til..."-meny (blokker er stacked i én kolonne, drag mellom dem er upraktisk)

7. Bygger på

  • Meldingsboks (meldingsboks.md): Alle overførte objekter er meldingsbokser
  • Kunnskapsgraf (kunnskapsgraf_og_relasjoner.md): Plasseringer er relasjoner i grafen
  • BlockShell / PageGrid: Blokk-rammen som rendrer mottakssoner
  • SpacetimeDB (synkronisering.md): Sanntidssynk av plasseringer

8. Konsekvenser for eksisterende kode

8.1 BlockShell utvidelse

BlockShell trenger:

  • onDragEnter/onDragLeave/onDrop handlers for visuell feedback
  • Prop for receiver: BlockReceiver fra innholdsblokken
  • Fullskjerm-toggle (knapp i header + Esc for å lukke)

8.2 Ny plasserings-tabell

message_placements er ny. Eksisterende kanban_card_view og calendar_event_view lever parallelt inntil migrering.

8.3 SpacetimeDB-modul

Ny tabell message_placement med reducers for place/remove/move.

9. Instruks for Claude Code

  • Overføring oppretter aldri en kopi av meldingen — kun en ny plassering (view-config)
  • entered_at er alltid now() ved overføring, aldri kopiert fra kilden
  • En melding uten noen plasseringer er "løs" — den eksisterer i grafen men vises ikke noe sted. UI skal advare om dette ved siste fjerning
  • Drag-and-drop bruker HTML5 Drag and Drop API for blokk-til-blokk, og pointer events for intra-canvas (storyboard/whiteboard)
  • Hold overføringslogikken i en sentral transferService — ikke spread ut i hver blokk-type
  • Mottaker-interfacet er obligatorisk for alle blokk-typer