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>
202 lines
8.7 KiB
Markdown
202 lines
8.7 KiB
Markdown
# 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
|
|
|
|
```sql
|
|
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
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```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
|
|
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
|