# Feature: Universell overføring — flytt innhold mellom verktøy
**Filsti:** `docs/features/universell_overfoering.md`

## 1. Konsept

Universell overføring er kjerneinteraksjonen på arbeidsflaten: drag-and-drop
mellom verktøy-paneler. Se [arbeidsflaten](../retninger/arbeidsflaten.md) for
den overordnede retningen.

### 1.1 To moduser

| Modus | Hva skjer | Eksempel |
|-------|-----------|---------|
| **Innholdstransfer** | Ny node opprettes med `source_material`-edge til kilden | Chatboble → artikkelverktøy: ny artikkelnode |
| **Lettvekts-triage** | Eksisterende node får ny edge/plassering | Chatboble → kanban: noden vises i kanban OG chat |

Default avhenger av verktøy-paret. Se kompatibilitetsmatrisen i
[arbeidsflaten](../retninger/arbeidsflaten.md). Bruker kan overstyre
med modifier-tast (Shift = alltid ny node).

### 1.2 Grunnprinsipp

```
Bruker drar kort fra Chat → slipper på Artikkelverktøyet
  → Ny artikkelnode opprettes med sitert tekst
  → source_material-edge kobler artikkel → chatboble
  → Chatboblen forblir uendret i chatten

Bruker drar kort fra Chat → slipper på Kanban-brettet
  → Noden får board-edge + status-edge (lettvekts)
  → Noden vises i kanban OG chat (samme node, to kontekster)
```

Bruker kan alltid flytte (fjerne fra kilde) 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

```sql
CREATE TABLE message_placements (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    message_id   UUID NOT NULL REFERENCES nodes(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 verktøy-panel. Når objektet forlater verktøy-panel-grensen:

1. Objektet blir en "ghost" (semi-transparent drag-representasjon)
2. Andre verktøy-paneler 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 verktøy-paneler brukeren har tilgang til 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 verktøy-panel-type definerer en **mottaker** som bestemmer hva som skjer når et objekt ankommer:

### 4.1 Mottaker per verktøy-panel-type

| Verktøy-panel | 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 verktøy-panel i bunnen av notatet | Tekst fades inn |

### 4.2 Mottakssone-rendering og inkompatibilitet

Hver verktøy-panel rendrer en visuell drop-target når en drag er aktiv:

- **Kompatibel:** Panelet lyser opp med subtil border/glow
- **Spesifikke soner** (f.eks. en kolonne i kanban) highlighter ved hover
- **Inkompatibel:** Drop-sonen er rød/grå. Tooltip forklarer *hvorfor*
  og foreslår alternativ der det gir mening.

Se kompatibilitetsmatrisen i [arbeidsflaten](../retninger/arbeidsflaten.md)
for komplett oversikt over hva som kan dras til hva.

### 4.3 Mottaker-interface

```typescript
interface BlockReceiver {
    /** Kan denne verktøy-panelen motta dette objektet? */
    canReceive(payload: DragPayload): CompatResult;

    /** Opprett plassering for mottatt objekt (optional — full impl i transfer service) */
    receive?(payload: DragPayload, dropPosition?: { x: number; y: number }): PlacementIntent;
}
```

Alle verktøy-panel-typer implementerer `canReceive()`. `receive()` er optional —
den fulle overføringslogikken (ny node + edges) håndteres av transfer service (§ 1).

**Drop-zone rendering** (`renderDropZone`) håndteres av `BlockShell`, ikke
av trait-komponentene selv. BlockShell bruker `receiver.canReceive()` for å
bestemme visuell tilstand (`compatible` / `incompatible`), og viser overlay
med forklaring ved inkompatibilitet.

**Kompatibilitetssjekker** per verktøy-type finnes i `$lib/transfer.ts`:
- `checkChatCompat()` — Chat aksepterer alt unntatt fra egen panel
- `checkKanbanCompat()` — Kanban aksepterer kommunikasjon og innhold
- `checkCalendarCompat()` — Kalender aksepterer kommunikasjon og innhold
- `checkEditorCompat()` — Artikkelverktøy aksepterer tekst og media
- `checkStudioCompat()` — Studio aksepterer kun lyd
- `checkAiToolCompat()` — AI-verktøy aksepterer kun tekst

Factory-funksjon `createBlockReceiver(toolType)` oppretter en `BlockReceiver`
for en gitt verktøy-type.

## 5. SpacetimeDB-integrasjon

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

```rust
#[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
}

#[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 verktøy-paneler fungerer naturlig
- **Tablet:** Drag-and-drop fungerer med touch, men "Send til..."-meny er primær
- **Mobil:** Kun "Send til..."-meny (verktøy-paneler 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: Verktøy-panel-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 innholdsverktøy-panelen
- 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. Implementasjonsstatus

### 9.1 Transfer service (`$lib/transfer.ts`)

Sentral `transferService` med:
- **`resolveTransferMode(source, target, shiftKey)`** — bestemmer modus fra verktøy-par.
  Bruker en matrise (`DEFAULT_TRANSFER_MODE`) som mapper source→target til default-modus.
  Shift-modifier overstyrer alltid til `innholdstransfer`.
- **`executeTransfer(accessToken, payload, intent, shiftKey)`** — utfører overføringen:
  - Innholdstransfer: `createNode()` → `createEdge(source_material)` → `createEdge(belongs_to)`
  - Lettvekts-triage: `createEdge(belongs_to)` med placement-metadata
- **Kompatibilitetssjekker** per verktøy-type (`checkChatCompat`, `checkKanbanCompat`, etc.)
- **`createBlockReceiver(toolType)`** — factory for `BlockReceiver`-implementasjoner

### 9.2 Shift-modifier

- `BlockShell.handleDropEvent()` sender `e.shiftKey` til `onDrop`-callback
- Workspace `handlePanelDrop()` sender shiftKey videre til `resolveTransferMode()`
- Shift = alltid ny node (innholdstransfer), uansett verktøy-par

### 9.3 Default-modus per verktøy-par

| Source → Target | Default-modus | Begrunnelse |
|----------------|---------------|-------------|
| chat → kanban | lettvekts-triage | Noden vises på brettet |
| chat → editor | innholdstransfer | Ny artikkelnode fra chatinnhold |
| chat → calendar | lettvekts-triage | Noden planlegges |
| kanban → calendar | lettvekts-triage | Oppgave planlegges |
| * → editor | innholdstransfer | Artikkelverktøyet skaper nytt innhold |
| * → ai_tool | innholdstransfer | AI skaper nytt innhold |
| * → chat/kanban/calendar/studio | lettvekts-triage | Noden vises i ny kontekst |

## 10. Instruks for Claude Code
- **Innholdstransfer** (ny node) brukes når innholdet transformeres (chat → artikkel)
- **Lettvekts-triage** (ny edge/plassering) brukes når kontekst legges til (chat → kanban)
- `source_material`-edge kobler ny node tilbake til kilden ved innholdstransfer
- `entered_at` er alltid `now()` ved plassering, aldri kopiert fra kilden
- En melding uten noen plasseringer er "løs" — UI advarer ved siste fjerning
- Inkompatible drops gir visuell feedback (rød/grå) + tooltip med forklaring
- Drag-and-drop bruker HTML5 API mellom paneler, pointer events intra-canvas
- Hold overføringslogikken i en sentral `transferService`
- Mottaker-interfacet er obligatorisk for alle verktøy-panel-typer
- Shift-modifier overstyrer alltid til innholdstransfer (ny node)
