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>
This commit is contained in:
vegard 2026-03-16 18:10:31 +01:00
parent 65b395082e
commit 7babafc65f
12 changed files with 909 additions and 1 deletions

View file

@ -35,6 +35,8 @@ CLAUDE.md er eneste startdokument. Alt annet ligger under `docs/`:
- `kanban.md` — Drag-and-drop planlegging
- `kalender.md` — Redaksjonell kalender med abonnementsmodell og ICS-eksport
- `notater.md` — Scratchpad/notatblokk med auto-save og debounce
- `canvas_primitiv.md` — Felles fritt-canvas underlag (pan, zoom, drag — brukes av whiteboard og storyboard)
- `universell_overfoering.md` — Flytt objekter mellom blokker (drag-and-drop + "Send til...")
- `whiteboard.md` — Sanntids frihåndstavle (møterom, chat, solo)
- `live_transkripsjon.md` — Whisper-pipeline (felles motor for studio/møter/fabrikk)
- `live_ai.md` — Live AI: faktoid-oppslag (studio) + referent (møter)

View file

@ -0,0 +1,213 @@
# Feature: Canvas-primitiv — felles fritt-canvas underlag
**Filsti:** `docs/features/canvas_primitiv.md`
## 1. Konsept
Canvas-primitivet er den felles underliggende komponenten for alle friform-views i Sidelinja: whiteboard (tegning), storyboard (kort-canvas), og fremtidige canvas-baserte visninger. Det håndterer kamera (pan, zoom), viewport-styring, objekt-plassering og interaksjon — men vet ingenting om *hva* som rendres.
### 1.1 Hvorfor et felles primitiv?
Whiteboard og storyboard har identisk infrastruktur-behov:
- Uendelig canvas med pan og zoom
- Objekter med `(x, y)`-posisjon
- Drag-and-drop av objekter
- Viewport culling (ikke render det som er utenfor synsfeltet)
- Touch-støtte (pinch-zoom, to-finger-pan)
- Responsivt design (fungerer på mobil, tablet, desktop)
Forskjellen er *innholdet*: whiteboard rendrer streker/figurer, storyboard rendrer meldingsboks-kort. Primitivet abstraherer det felles, slik at begge views gjenbruker 100 % av canvas-logikken.
### 1.2 Arkitekturprinsipp
```
Canvas-primitiv (felles)
├── Kamera: pan, zoom, transform matrix
├── Viewport: culling, synlige objekter
├── Interaksjon: pointer events, touch, drag
├── Grid: valgfri snap, hjelpelinje
└── Render-delegering: slot/callback for innhold
Whiteboard (consumer)
├── Tegneverktøy: penn, linje, rektangel, tekst
├── Strøk-modell: SVG paths / canvas paths
└── SpacetimeDB: strøk-synkronisering
Storyboard (consumer)
├── Kort-rendering: <MessageBox> i kompakt modus
├── Status-modell: Klar / Tatt opp / Droppet / Arkivert
├── Portal-soner: overføringsmekanikk til andre blokker
└── SpacetimeDB: kort-posisjon + status-synkronisering
```
## 2. Kamera-modell
### 2.1 Transform
Kameraet representeres som en 2D affin transformasjon:
```typescript
interface Camera {
x: number; // pan offset X (world coords)
y: number; // pan offset Y (world coords)
zoom: number; // scale factor (1.0 = 100%)
}
```
Rendring via CSS `transform` på en wrapper-div:
```css
.canvas-world {
transform: translate(calc(var(--cam-x) * 1px), calc(var(--cam-y) * 1px))
scale(var(--cam-zoom));
transform-origin: 0 0;
}
```
### 2.2 Zoom-begrensning
- Min zoom: `0.1` (10 % — fugleperspektiv, brukes av Pinboard Mode)
- Max zoom: `3.0` (300 % — detalj)
- Default: `1.0`
- Zoom pivoterer rundt musepeker/finger-midtpunkt
### 2.3 Pan
- **Desktop:** Hold mellomknapp eller mellomrom + dra. Alternativt: to-finger-drag på trackpad.
- **Touch:** To-finger-pan (én finger = dra objekter, to fingre = pan).
- **Edge-pan:** Når man drar et objekt nær kanten av viewport, scroller canvaset automatisk i den retningen.
## 3. Viewport Culling
Bare objekter som overlapper med det synlige viewport-rektangelet rendres i DOM. For storyboard med 50200 kort er dette en optimalisering som holder DOM-et lett.
```typescript
function visibleObjects(objects: CanvasObject[], camera: Camera, viewportSize: { w: number, h: number }): CanvasObject[] {
const worldRect = screenToWorld(camera, viewportSize);
return objects.filter(obj => intersects(obj.bounds, worldRect));
}
```
En margin (f.eks. 200px i world-space) legges til for å unngå pop-in ved pan.
## 4. Objektmodell
Canvas-primitivet opererer på generiske objekter:
```typescript
interface CanvasObject {
id: string;
x: number;
y: number;
width: number;
height: number;
// Consumer-spesifikk data håndteres via generics/props
}
```
Consumer (whiteboard, storyboard) bestemmer *hva* som rendres for hvert objekt via en render-callback eller Svelte snippet:
```svelte
<Canvas objects={cards} let:object>
<!-- Consumer bestemmer innholdet -->
<StoryboardCard card={object} />
</Canvas>
```
## 5. Interaksjon
### 5.1 Pointer events
All interaksjon håndteres via pointer events (unified mouse + touch):
| Gest | Desktop | Touch | Handling |
|------|---------|-------|----------|
| Pan | Mellomknapp-drag / Space+drag | To-finger-drag | Flytt kamera |
| Zoom | Scroll wheel | Pinch | Zoom inn/ut |
| Velg | Klikk | Tap | Velg objekt |
| Flytt | Venstreklikk-drag på objekt | Én-finger-drag på objekt | Flytt objekt |
| Multi-select | Shift+klikk / lasso | Lang-trykk + drag | Velg flere |
### 5.2 Snap-to-grid (valgfri)
Når aktivert, snapper objekter til et rutenett ved drag-slipp:
```typescript
function snap(value: number, gridSize: number): number {
return Math.round(value / gridSize) * gridSize;
}
```
Default: av. Kan toggles via hurtigtast eller toolbar.
### 5.3 Seleksjon
- Klikk på tom flate: deselect alle
- Klikk på objekt: velg det (deselect andre)
- Shift+klikk: toggle seleksjon
- Lasso: dra på tom flate uten Space = tegn seleksjonsboks
## 6. Responsivt design
Canvas-primitivet skal fungere på alle skjermstørrelser:
| Skjerm | Tilpasning |
|--------|-----------|
| Desktop (>1024px) | Full interaksjon, alle hurtigtaster |
| Tablet (7681024px) | Touch-gester, toolbar i bunn |
| Mobil (<768px) | Forenklet toolbar, større treffområder for objekter, ingen lasso |
Touch-treffområder skal være minimum 44x44px (WCAG 2.5.5).
## 7. Fullskjerm-modus (BlockShell-feature)
Enhver blokk i `BlockShell` kan gå i fullskjerm. Dette er en generell feature, ikke spesifikk for canvas:
- **Toggle:** Dobbeltklikk på blokk-headeren, eller knapp i header
- **Implementering:** Blokken settes til `position: fixed; inset: 0; z-index: 50`
- **Escape:** Trykk Esc eller klikk "minimer"-knapp for å gå tilbake
- **URL-state:** Fullskjerm-tilstand lagres ikke i URL — det er en visuell modus, ikke en side
## 8. SpacetimeDB-integrasjon
Canvas-primitivet selv har ingen SpacetimeDB-kobling — det er consumer-ens ansvar. Men primitivet eksponerer events som consumeren kan koble til SpacetimeDB:
```typescript
interface CanvasEvents {
onObjectMove: (id: string, x: number, y: number) => void;
onObjectResize: (id: string, w: number, h: number) => void;
onCameraChange: (camera: Camera) => void;
onSelectionChange: (ids: string[]) => void;
}
```
Storyboard-consumeren bruker `onObjectMove` til å kalle en SpacetimeDB-reducer for å synkronisere posisjon til andre klienter.
## 9. Bygger på
- **SvelteKit:** Svelte 5 `$state`/`$derived` for reaktiv kamera- og objekt-state
- **CSS transforms:** Ingen Canvas2D eller WebGL — DOM-basert rendering for å beholde Svelte-komponent-rendering inne i objektene
- **Pointer Events API:** Unified input for mus og touch
## 10. Implementeringsstrategi
### Fase 1: Kjerne-primitiv
- `<Canvas>` Svelte-komponent med kamera (pan/zoom), viewport culling, og objekt-drag
- Touch-støtte (pinch-zoom, to-finger-pan)
- BlockShell fullskjerm-toggle
### Fase 2: Storyboard som første consumer
- `<StoryboardCard>` rendrer meldingsboks-kort på canvaset
- SpacetimeDB-synk for posisjon og status
- Portal-soner for overføring
### Fase 3: Whiteboard-migrering
- Migrere eksisterende whiteboard-spec til å bruke canvas-primitivet
- Tegneverktøy som overlay oppå primitivet
## 11. Instruks for Claude Code
- Canvas-primitivet er en ren Svelte-komponent uten backend-avhengigheter
- Bruk CSS transforms, ikke Canvas2D — innholdet inne i objekter er vanlige Svelte-komponenter
- All state styres via Svelte 5 `$state` og `$derived` — ingen external state management
- Pointer events, ikke mouse events — unified input
- Test med touch-emulering i DevTools for responsivitet
- Viewport culling er påkrevd fra dag 1 — ikke optimaliser bort

View file

@ -0,0 +1,202 @@
# 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

View file

@ -39,10 +39,20 @@ Når en idé modnes nok til å bli implementert, skrives en full spec i `docs/fe
| [Podcasting 2.0](podcasting_2_0.md) | Lav | Høy | Podcastfabrikken, kunnskapsgraf, RSS |
| [Web Clipper](web_clipper.md) | LavMiddels | Høy | Jobbkø, AI Gateway, meldingsboks, kunnskapsgraf |
| [Visuelle Waveforms](waveforms.md) | LavMiddels | Høy | Podcastfabrikken, jobbkø, editor |
| **Innspilling & Storyboard** | | | |
| [Storyboard](storyboard.md) | MiddelsStor | Høy | Canvas-primitiv, meldingsboks, universell overføring, Studioet, Podcastfabrikken |
| [Card Chaining](card_chaining.md) | Lav | Middels | Kunnskapsgraf, Storyboard, AI Gateway |
| [Ghost Cards](ghost_cards.md) | LavMiddels | Høy | Storyboard, meldingsboks, kunnskapsgraf |
| [Pinboard Mode](pinboard_mode.md) | Lav | Høy | Storyboard, kanban |
| [Flow Meter](flow_meter.md) | Lav | Middels | Storyboard |
| [Emotion Tags](emotion_tags.md) | Lav | Middels | Meldingsboks, kanban, storyboard |
| **Samarbeid** | | | |
| [Collaborative Cursors](collaborative_cursors.md) | Lav | Middels | SpacetimeDB, Svelte |
| [Card Heat Map](card_heat_map.md) | Lav | Middels | Meldingsboks, kanban/storyboard |
**Forfremmet til feature:** [Meldingsboks](../features/meldingsboks.md) — universell diskusjonsprimitiv (erstatter separate modeller for chat, kanban-kort, kalenderhendelser, faktoider, notater).
**Lavthengende frukter** (lav innsats, høy wow): Serendipity Roulette, Podcast Time Machine, Meme Generator, Audience Voice Memo.
**Lavthengende frukter** (lav innsats, høy wow): Serendipity Roulette, Podcast Time Machine, Meme Generator, Audience Voice Memo, Pinboard Mode, Ghost Cards.
## Format
Forslagsfiler er lette — ingen streng mal. Minimum:

View file

@ -0,0 +1,26 @@
# Card Chaining — Automatisk kobling av relaterte kort
## Idé
Når to kort plasseres ved siden av hverandre (i kanban, storyboard eller kalender), opprettes automatisk en graf-edge mellom dem. Systemet kan også foreslå overganger: "Og apropos drømmer..."
## Hvorfor interessant?
Podcast-segmenter henger ofte sammen tematisk, men koblingen er implisitt. Card chaining gjør den eksplisitt — uten manuelt arbeid. Gir bedre flyt under innspilling og bedre metadata for kunnskapsgrafen.
## Fungerer slik
1. Dra kort A ved siden av kort B i storyboard
2. System oppretter `graph_edge` med `relation_type: 'sequence'` og `origin: 'proximity'`
3. Valgfritt: AI foreslår overgangssetning basert på begge kortenes innhold
4. Ved eksport/arkivering: sekvensen bevares som episode-struktur
## Bygger på
- Kunnskapsgraf (graph_edges)
- Storyboard (proximity detection)
- AI Gateway (overgangsforslag)
## Innsats
Lav — graph_edges finnes, bare UI for proximity + auto-edge.
## Wow-faktor
Middels — subtilt, men forbedrer metadata-kvaliteten dramatisk over tid.

View file

@ -0,0 +1,25 @@
# Card Heat Map — Visuell indikator for engasjement
## Idé
Kort på storyboard/kanban gløder basert på hvor mye oppmerksomhet de har fått: hover-tid, antall redigeringer, diskusjonstråd-lengde, og tid brukt i innspilling.
## Hvorfor interessant?
Hjelper med å se hva teamet faktisk er engasjert i — uten å lese alt. Under innspilling: "det kortet gløder mest, kanskje vi bør ta det først."
## Fungerer slik
1. Klient tracker hover-tid per kort (lokal state)
2. Server aggregerer: antall edits, tråd-lengde, reaksjoner
3. Kombinert score → CSS-variabel (`--heat: 0.01.0`) → glow-effekt
4. Valgfritt: "Hot topics"-filter som sorterer kort etter heat
## Bygger på
- Meldingsboks (reaksjoner, tråd-lengde)
- Kanban/Storyboard (visuell rendering)
## Innsats
Lav — ren frontend-logikk med enkel server-aggregering.
## Wow-faktor
Middels — subtilt men nyttig for redaksjonell prioritering.

View file

@ -0,0 +1,29 @@
# Collaborative Cursors — Sanntids-pekere for flerbrukermiljø
## Idé
Alle brukere som er på samme side ser hverandres musepekere som fargede prikker med navn. Fungerer på storyboard, kanban, whiteboard og kalender.
## Hvorfor interessant?
Gir "jamming together"-følelse under innspilling og planlegging. Produsent og host ser hverandre jobbe i sanntid uten å snakke om det.
## Fungerer slik
1. Klient sender `{ user_id, x, y, page }` til SpacetimeDB ved musebevegelse (throttlet til ~10 Hz)
2. Andre klienter renderer fargede SVG-sirkler med brukernavn
3. Prikken fader ut etter 5 sekunder uten bevegelse
4. Valgfritt: kort "trail" som viser bevegelsesretning
## Bygger på
- SpacetimeDB (pub/sub for posisjoner)
- Svelte ($state store for cursor-map)
## Innsats
Lav — under 50 linjer Svelte + en SpacetimeDB-reducer.
## Wow-faktor
Middels — visuelt tiltalende, men ikke kritisk funksjonalitet.
## Åpne spørsmål
- Bør pekere vises i chat-visning også, eller bare canvas-baserte views?
- Throttling-strategi: SpacetimeDB-reducer eller klient-side debounce?

View file

@ -0,0 +1,26 @@
# Emotion Tags — Hurtigkategorisering av segmenter
## Idé
Kort i storyboard/kanban kan tagges med stemnings-ikoner — morsom, seriøs, kontroversiell, personlig. Taggen farger kortets ramme og fungerer som filter i etterarbeid.
## Hvorfor interessant?
Under redigering er det nyttig å filtrere på stemning: "vis alle kontroversielle segmenter" eller "vi trenger en morsom bit mellom to tunge tema." Raskere enn å lese alle kortene.
## Fungerer slik
1. Predefinerte tags med ikoner og farger (konfigurerbart per workspace)
2. Klikk-basert tagging — ett klikk per tag, toggle on/off
3. Visuelt: farget border + ikon-badge på kortet
4. Filter: "vis kun 🔥-kort" i storyboard og kanban
5. Lagres som `message.metadata.emotion_tags: string[]`
## Passer inn i eksisterende
- **Meldingsboks**: tags i metadata-feltet, ingen ny tabell
- **Reaksjoner**: kan gjenbruke reaksjons-mekanismen (emoji = emotion tag)
## Innsats
Lav — ren frontend + metadata-felt.
## Wow-faktor
Middels — nyttig for store episoder med mange segmenter.

View file

@ -0,0 +1,24 @@
# Flow Meter — Visuell episodeprogresjon
## Idé
En tynn progresjonslinje langs toppen av storyboardet som fylles etter hvert som kort dras til "Tatt opp". Grønn = solid episode, gul = trenger mer, rød = for kort. Basert på antall segmenter og total opptakstid.
## Hvorfor interessant?
Under innspilling er det lett å miste oversikten over om man har nok materiale. Flow meter gir et intuitivt "mage-sjekk" uten å telle manuelt.
## Fungerer slik
1. Konfigurerbar målvarighet per workspace (f.eks. 45 min)
2. Summerer varighet for alle "Tatt opp"-kort
3. Fargeovergang: rød (030%) → gul (3070%) → grønn (70100%)
4. Valgfritt: pulserer sakte når man nærmer seg mål
## Bygger på
- Storyboard (episode-sekvens med tidsstempler)
## Innsats
Lav — én beregning + CSS gradient.
## Wow-faktor
Middels — liten ting, men fjerner mental overhead.

View file

@ -0,0 +1,27 @@
# Ghost Cards — Visuelle spor fra tidligere episoder
## Idé
Når en episode er ferdig og arkivert, etterlater kort som ble "Tatt opp" svake, semi-transparente spøkelseskort på storyboardet. De fungerer som påminnelser: "vi snakket om dette forrige uke — har vi oppfølging?"
## Hvorfor interessant?
Podcast-temaer henger sammen over tid. Ghost cards gir visuell kontinuitet mellom episoder uten manuell sporing. Perfekt for serier og løpende historier.
## Fungerer slik
1. Ved arkivering: kort som var "Tatt opp" får `ghost_episode_id` i metadata
2. Ved neste episode: ghost cards vises med `opacity: 0.3` og episode-nummer
3. Klikk på ghost → se original diskusjonstråd og tidsstempel
4. Dra ghost → promoter til nytt aktivt kort for oppfølging (beholder graf-edge til originalen)
5. Konfigurerbart: vis ghosts fra siste N episoder (default: 3)
## Bygger på
- Storyboard (visuell rendering)
- Meldingsboks (message med view-config)
- Kunnskapsgraf (edge mellom original og oppfølger)
## Innsats
LavMiddels — mest metadata-design og UI for ghost-rendering.
## Wow-faktor
Høy — gir podcasten "hukommelse" som oppleves magisk for brukeren.

View file

@ -0,0 +1,26 @@
# Pinboard Mode — Fugleperspektiv over episode-arc
## Idé
Hurtigtast (f.eks. `Ctrl+0`) zoomer ut storyboardet til fugleperspektiv. Kort krymper, titler blir store. Du ser hele episodens arc som en visuell flyt: intro → morsom bit → dypt spørsmål → outro. Dra kort som Lego-klosser for å endre rekkefølge.
## Hvorfor interessant?
Under innspilling er man "for nær" — man ser enkelt-kort, ikke helheten. Pinboard mode gir 3-sekunders overblikk: "vi har tre morsomme segmenter på rad, vi trenger noe tungt i midten."
## Fungerer slik
1. Toggle via hurtigtast eller knapp
2. CSS transform: `scale(0.4)` + økt font-size på titler
3. Kort viser kun: tittel, status-farge, varighet (hvis tatt opp)
4. Drag-and-drop endrer rekkefølge i episode-sekvensen
5. Klikk på kort = zoom tilbake til normalvisning på det kortet
## Bygger på
- Storyboard (episode-sekvens)
- Kanban (drag-and-drop)
## Innsats
Lav — ren CSS/UI-jobb.
## Wow-faktor
Høy — visuelt imponerende og genuint nyttig under innspilling.

View file

@ -0,0 +1,298 @@
# Storyboard — Fritt canvas for innspillingsplanlegging og live-produksjon
## 1. Konsept
Storyboardet er et fritt canvas der meldingsboks-kort plasseres, flyttes og grupperes visuelt. Det brukes **før, under og etter innspilling** — fra idémyldring til ferdig episodestruktur. Ikke et erstatningsverktøy for kanban (som er for langsiktig planlegging), men et taktilt arbeidsflate for å *se* en episode ta form.
Storyboardet er en consumer av Canvas-primitivet (`docs/features/canvas_primitiv.md`) og deltar i universell overføring (`docs/features/universell_overfoering.md`).
## 2. Kjernemodell
### 2.1 Kort = Meldingsboks med storyboard-plassering
Alle kort på storyboardet er vanlige `messages`-noder. Plasseringen på canvaset styres av `message_placements`-tabellen:
```
message_placements:
message_id → messages.id
context_type = 'storyboard'
context_id → episode_id (eller standalone storyboard-id)
entered_at = når kortet ble lagt på boardet
position = { "x": 340, "y": 120, "status": "klar" }
```
Kortet kan samtidig leve i en chat, på et kanban-brett, eller i en kalender. Endringer i chatten (nye svar, redigeringer) propagerer til storyboard-visningen fordi det er *samme melding*.
### 2.2 Episode-objekt
Et storyboard er knyttet til en episode (node av type `episode`) eller eksisterer som frittstående canvas (for idémyldring uten episode-tilknytning).
Episode-noden eier:
- **Sekvensliste:** En ordnet liste over kort som er "Tatt opp", med tidsstempel for når de ble markert. Lagres som `episode_sequence` i SpacetimeDB.
- **Målvarighet:** Konfigurerbar per workspace (f.eks. 45 min) — brukes av Flow Meter.
### 2.3 Statuser
Kort har status (lagret i `position`-JSONB på plasseringen):
| Status | Visuelt | Betydning |
|--------|---------|-----------|
| **Klar** | Solid, hvit/lys border | Planlagt, venter på tur |
| **Tatt opp** | Grønn border, tidsstempel-badge | Snakket om under innspilling |
| **Droppet** | Dimmet (opacity 0.4), rød stripe | Ikke brukt denne gang |
| **Arkivert** | Skjult (filter) | Ferdig behandlet |
Status-endring skjer via:
- Drag til en status-sone (valgfri — konfigurer per workspace)
- Hurtigtast: `R` = Tatt opp, `D` = Droppet (når kort er valgt)
- Kontekstmeny
- Flytende toolbar ved seleksjon
### 2.4 Flere storyboards
Et workspace kan ha mange storyboards — ett per episode, pluss frittstående for idémyldring. Hver er en `StoryboardBlock` med `props.episodeId` (eller `props.boardId` for frittstående).
Tre episoder under planlegging = tre storyboard-blokker, enten:
- På samme side (splittet grid-layout)
- På ulike sider i workspace-navigasjonen
## 3. Fritt canvas
Storyboardet bruker Canvas-primitivet for all canvas-interaksjon:
- **Pan/zoom:** Se `canvas_primitiv.md` §2
- **Objekt-plassering:** Kort har `(x, y)` i world-space, ingen kolonner eller rader
- **Snap-to-grid:** Av som default, toggle med `G`
- **Ingen akse-begrensning:** Brukeren plasserer kort fritt. Ingen antatt retning eller tidslinje
### 3.1 Kort-rendering
Hvert kort rendres som en `<StoryboardCard>` inne i canvas-primitivets objekt-slot:
```
┌─────────────────────────┐
│ 🔵 Tittel │ ← Status-farge som border
│ │
│ Kort sammendrag av body │ ← Avkortet tekst
│ │
│ 💬 3 ⏱ 04:32 │ ← Svar-count + varighet (hvis tatt opp)
└─────────────────────────┘
```
Kort-størrelse er fast bredde (variabel ved zoom), høyde tilpasser seg innholdet opp til en max.
### 3.2 Kort-interaksjon
- **Klikk:** Velg kort, vis flytende toolbar
- **Dobbeltklikk:** Åpne meldingsboksen i utvidet modus (full diskusjonstråd)
- **Drag:** Flytt kort på canvaset
- **Høyreklikk:** Kontekstmeny (status, send til, fjern, slett)
## 4. Overføring mellom blokker
Storyboardet deltar fullt i universell overføring (`universell_overfoering.md`):
### 4.1 Som sender
Dra et kort ut av storyboard-blokken → ghost følger musepekeren → slipp på annen blokk:
- **→ Chat:** Melding ankommer som ny chatmelding med `entered_at = now()`
- **→ Kanban:** Melding blir kort i første kolonne
- **→ Kalender:** Melding blir hendelse på dagens dato
- **→ Annet storyboard:** Melding får ny canvas-posisjon i mål-boardet
### 4.2 Som mottaker
Storyboardet kan motta fra alle blokk-typer:
- **Default-plassering:** Senter av viewport
- **Drag-plassering:** Der brukeren slipper objektet på canvaset
- **Status:** Nye kort ankommer som "Klar"
### 4.3 Inter-storyboard overføring
For å flytte kort mellom episoder (f.eks. "dette passer bedre i episode 48"):
- Dra kortet til en annen storyboard-blokk
- Eller bruk "Send til..." → velg annet storyboard
- Plasseringen i kilde-boardet fjernes, ny plassering opprettes i mål-boardet
## 5. Kort som oppstår under innspilling
Under live innspilling dukker nye idéer opp. Flere veier inn:
| Metode | Flyt |
|--------|------|
| **Hurtigtast (`N`)** | Popup for tittel + body → kort plasseres nær senter |
| **Fra chat** | Skriv melding i studio-chat → "Send til Storyboard" |
| **AI-forslag** | Live AI foreslår kort basert på transkripsjon (se §8) |
Alle veier oppretter en meldingsboks + plassering med status "Klar".
## 6. Kobling til LiveKit / Studioet
### 6.1 Tidsstempel ved "Tatt opp"
Når et kort settes til "Tatt opp" under en aktiv innspilling:
1. **Klient spør LiveKit** om nåværende innspillings-tidspunkt (offset fra oppstart)
2. Tidspunktet lagres i plasserings-metadata: `position.recorded_at_offset = 1823` (sekunder)
3. Episode-sekvensen oppdateres med kortet i riktig posisjon
Etter innspilling, når Whisper har prosessert lyden, kan `recorded_at_offset` matches mot Whisper-segmenter for å koble kort til eksakte transkripsjonsavsnitt.
### 6.2 Episode-sekvens
```rust
#[table(name = episode_sequence_entry, public)]
pub struct EpisodeSequenceEntry {
#[primary_key]
pub id: String,
pub episode_id: String,
pub message_id: String,
pub sequence_position: f32, // REAL for midpoint-innsetting
pub recorded_at_offset: Option<i64>, // sekunder fra innspillingsstart
pub workspace_id: String,
}
```
Sekvensen er den ordnede listen over "Tatt opp"-kort og blir grunnlaget for episode-strukturen i Podcastfabrikken.
## 7. Etter innspilling
### 7.1 Episode-oppsummering
Automatisk generert visning etter innspilling:
```
Episode 47 — Oppsummering
━━━━━━━━━━━━━━━━━━━━━━━━
1. [00:00] Intro — Kommunevalg 2027 💬 2 svar
2. [04:32] Listekandidatene i Oslo 💬 5 svar
3. [12:15] Valgomaten — første resultater 💬 1 svar
4. [18:40] Debatten om bompenger 💬 3 svar
━━━━━━━━━━━━━━━━━━━━━━━━
Totalt: 23:10 av 45:00 mål (51%)
Droppet: 3 kort (bevart for neste episode)
```
### 7.2 Arkivering
Ett-klikks arkivering:
1. Alle "Tatt opp"-kort settes til "Arkivert"
2. "Droppet"-kort beholder status (synlige for neste episode som Ghost Cards)
3. "Klar"-kort beholder status (ubrukte idéer)
4. Episode-sekvensen fryses (immutable etter arkivering)
## 8. AI-integrasjon (fremtidig)
Under innspilling kan Live AI foreslå nye kort:
1. Whisper transkriberer i sanntid
2. AI analyserer transkripsjon: "det ble nevnt et nytt tema som ikke er på boardet"
3. AI oppretter et foreslått kort (status: "Foreslått", visuelt distinkt — stiplet border)
4. Bruker aksepterer → status endres til "Klar"
Dette bygger på Live AI (`docs/features/live_ai.md`) og er ikke del av MVP.
## 9. SpacetimeDB-modell
### 9.1 Tabeller
```rust
// Kort-posisjon og status (via universell overføring)
// Bruker message_placement-tabellen — se universell_overfoering.md
// Episode-sekvens (ordnet liste over "Tatt opp"-kort)
#[table(name = episode_sequence_entry, public)]
pub struct EpisodeSequenceEntry {
#[primary_key]
pub id: String,
pub episode_id: String,
pub message_id: String,
pub sequence_position: f32,
pub recorded_at_offset: Option<i64>,
pub workspace_id: String,
}
```
### 9.2 Reducers
```rust
#[reducer]
pub fn set_card_status(ctx: &ReducerContext, placement_id: String, status: String) { ... }
#[reducer]
pub fn record_card(ctx: &ReducerContext, episode_id: String, message_id: String, offset: Option<i64>) {
// Sett status til "Tatt opp" + legg til i episode-sekvens
}
#[reducer]
pub fn reorder_sequence(ctx: &ReducerContext, episode_id: String, entries: Vec<(String, f32)>) { ... }
#[reducer]
pub fn archive_episode(ctx: &ReducerContext, episode_id: String) { ... }
```
## 10. Responsivt design
| Skjerm | Tilpasning |
|--------|-----------|
| Desktop | Full canvas med zoom/pan, hurtigtaster, drag-and-drop |
| Tablet | Touch-gester, flytende toolbar i bunn, "Send til"-meny |
| Mobil | Listevisning av kort (gruppert etter status), "Send til"-meny, ingen canvas |
Mobil får en **alternativ listevisning** fordi fritt canvas på liten skjerm er upraktisk. Listen grupperer kort etter status og lar brukeren endre status via swipe.
## 11. Bygger på
| Feature / Infra | Rolle |
|-----------------|-------|
| Canvas-primitiv | Fritt canvas med zoom/pan/drag |
| Meldingsboks | Kort = meldinger med view-config |
| Universell overføring | Portal-mekanikk mellom blokker |
| Kunnskapsgraf | Kort er noder, relasjoner mellom kort |
| SpacetimeDB | Sanntidssynk av posisjon og status |
| Studioet / LiveKit | Tidsstempel ved "Tatt opp" under innspilling |
| Podcastfabrikken | Episode-sekvens → redigeringsgrunnlag |
## 12. Innsats
MiddelsStor — canvas-primitivet er det tyngste løftet, men gjenbrukes av whiteboard. Selve storyboard-logikken er middels (status, sekvens, arkivering).
## 13. Wow-faktor
Høy — dette er "limen" mellom redaksjonelt arbeid og faktisk innspilling. Visuelt imponerende og genuint nyttig.
## 14. Implementeringsfaser
### Fase 1: Fundament
1. Canvas-primitiv (`<Canvas>`) med pan, zoom, drag, viewport culling
2. BlockShell fullskjerm-toggle
3. `message_placements`-tabell (PG + SpacetimeDB)
4. `StoryboardBlock` registrert i block registry
### Fase 2: Kjerne-storyboard
5. `<StoryboardCard>` med status-visning og hurtigtaster
6. Episode-sekvens med SpacetimeDB-synk
7. Universell overføring: "Send til..." kontekstmeny
8. Drag-and-drop mellom blokker
### Fase 3: Innspillingsintegrasjon
9. LiveKit-kobling for tidsstempler ved "Tatt opp"
10. Episode-oppsummering og arkivering
11. Kort opprettet under innspilling (hurtigtast + fra chat)
### Fase 4: Polish og utvidelser
12. Ghost Cards (forrige episodes kort)
13. Pinboard Mode (zoom-ut til fugleperspektiv)
14. Flow Meter (visuell progresjon)
15. Mobil listevisning
16. AI-foreslåtte kort under innspilling
## 15. Åpne spørsmål (parkert)
- Skal frittstående storyboards (uten episode) ha en egen node-type, eller er de bare episoder uten publiseringsdato?
- Bør canvaset ha et bakgrunnsmønster (dots/grid) for orientering, eller blank?
- Hvor mange kort tåler canvaset før ytelsen lider? Sannsynlig grense: 200500 DOM-noder med viewport culling.
## 16. Instruks for Claude Code
- Storyboard er en blokk-type (`StoryboardBlock.svelte`) i block registry
- Kort-posisjon eies av SpacetimeDB via `message_placements` — aldri direkte PG fra frontend
- Episode-sekvens er en egen SpacetimeDB-tabell, ikke metadata på episode-noden
- Bruk Canvas-primitivet for all canvas-logikk — ikke reimplementer pan/zoom
- Mobil-fallback (listevisning) er en egen komponent, ikke en "responsiv" versjon av canvaset
- Status-endring skal alltid gå via SpacetimeDB reducer, aldri direkte state-mutasjon