Implementer BlockReceiver i alle trait-komponenter (oppgave 20.3)
Hver trait-komponent (Chat, Kanban, Kalender, Editor, Studio) har nå en BlockReceiver med canReceive() som sjekker kompatibilitetsmatrisen. Inkompatible drops viser forklaring og forslag til alternativ. Endringer: - transfer.ts: Per-verktøy compat-sjekker (checkChatCompat, checkKanbanCompat, checkCalendarCompat, checkEditorCompat, checkStudioCompat) + createBlockReceiver factory - types.ts: BlockReceiver utvidet med optional receive() + PlacementIntent type - BlockShell.svelte: Validerer payload på faktisk drop (ikke bare drag-over) - Alle 5 traits: Eksporterer BlockReceiver med canReceive + receive - workspace/+page.svelte: Kobler receivers til BlockShell i spatial canvas - Doc oppdatert til å reflektere faktisk implementasjon Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
90b5117a5f
commit
5b0881d5d9
11 changed files with 363 additions and 17 deletions
|
|
@ -142,17 +142,31 @@ for komplett oversikt over hva som kan dras til hva.
|
|||
```typescript
|
||||
interface BlockReceiver {
|
||||
/** Kan denne verktøy-panelen motta dette objektet? */
|
||||
canReceive(message: Message): boolean;
|
||||
canReceive(payload: DragPayload): CompatResult;
|
||||
|
||||
/** Opprett plassering for mottatt objekt */
|
||||
receive(message: Message, dropPosition?: { x: number, y: number }): Placement;
|
||||
|
||||
/** Visuell feedback for aktiv drag */
|
||||
renderDropZone(): void;
|
||||
/** 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 dette interfacet.
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -257,6 +257,20 @@
|
|||
if (dropZoneState === 'compatible' && e.dataTransfer) {
|
||||
const payload = getDragPayload(e.dataTransfer);
|
||||
if (payload) {
|
||||
// Validate on actual drop (we couldn't read payload during drag)
|
||||
if (receiver) {
|
||||
const result = receiver.canReceive(payload);
|
||||
if (!result.compatible) {
|
||||
// Show incompatible feedback briefly
|
||||
dropZoneState = 'incompatible';
|
||||
dropFeedback = result.reason ?? 'Kan ikke motta dette innholdet';
|
||||
setTimeout(() => {
|
||||
dropZoneState = 'idle';
|
||||
dropFeedback = '';
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
onDrop?.(payload);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,19 @@
|
|||
* Ref: docs/features/universell_overfoering.md § 8.1
|
||||
*/
|
||||
|
||||
import type { DragPayload, CompatResult } from '$lib/transfer.js';
|
||||
import type { DragPayload, CompatResult, ToolType } from '$lib/transfer.js';
|
||||
|
||||
/** Placement intent returned by BlockReceiver.receive() */
|
||||
export interface PlacementIntent {
|
||||
/** What happens on receive: new node or new edge/placement */
|
||||
mode: 'innholdstransfer' | 'lettvekts-triage';
|
||||
/** Target context ID (collection, board, calendar, channel) */
|
||||
contextId: string;
|
||||
/** Context type for placement table */
|
||||
contextType: string;
|
||||
/** Position data (column for kanban, date for calendar, etc.) */
|
||||
position?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** Size constraints for a panel */
|
||||
export interface SizeConstraints {
|
||||
|
|
@ -40,10 +52,16 @@ export type DropZoneState = 'idle' | 'compatible' | 'incompatible';
|
|||
/**
|
||||
* BlockReceiver interface — implemented by tool panels that accept drops.
|
||||
* Ref: docs/features/universell_overfoering.md § 4.3
|
||||
*
|
||||
* - canReceive(): compatibility check (called during drag-over)
|
||||
* - receive(): creates placement/edge (called on drop)
|
||||
* - renderDropZone is handled by BlockShell (visual feedback via CSS)
|
||||
*/
|
||||
export interface BlockReceiver {
|
||||
/** Can this panel receive this payload? */
|
||||
canReceive(payload: DragPayload): CompatResult;
|
||||
/** Process a received drop — returns placement intent for the transfer service */
|
||||
receive?(payload: DragPayload, dropPosition?: { x: number; y: number }): PlacementIntent;
|
||||
}
|
||||
|
||||
/** Events emitted by BlockShell */
|
||||
|
|
|
|||
|
|
@ -1,15 +1,43 @@
|
|||
<script lang="ts">
|
||||
import type { Node } from '$lib/spacetime';
|
||||
import { edgeStore, nodeStore } from '$lib/spacetime';
|
||||
import { checkCalendarCompat, type DragPayload } from '$lib/transfer';
|
||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||
import TraitPanel from './TraitPanel.svelte';
|
||||
|
||||
interface Props {
|
||||
collection: Node;
|
||||
config: Record<string, unknown>;
|
||||
userId?: string;
|
||||
/** Called when a drop is received on this panel */
|
||||
onReceiveDrop?: (payload: DragPayload, intent: PlacementIntent) => void;
|
||||
}
|
||||
|
||||
let { collection, config, userId }: Props = $props();
|
||||
let { collection, config, userId, onReceiveDrop }: Props = $props();
|
||||
|
||||
/**
|
||||
* BlockReceiver implementation for Calendar.
|
||||
* Accepts communication and content nodes as new events/scheduled items.
|
||||
* Kanban cards get a scheduled edge.
|
||||
* Ref: docs/retninger/arbeidsflaten.md § Kompatibilitetsmatrise
|
||||
*/
|
||||
export const receiver: BlockReceiver = {
|
||||
canReceive(payload: DragPayload) {
|
||||
return checkCalendarCompat(payload);
|
||||
},
|
||||
receive(payload: DragPayload) {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const mode = payload.sourcePanel === 'kanban' ? 'lettvekts-triage' as const : 'innholdstransfer' as const;
|
||||
const intent: PlacementIntent = {
|
||||
mode,
|
||||
contextId: collection?.id ?? '',
|
||||
contextType: 'calendar',
|
||||
position: { date: today, all_day: true },
|
||||
};
|
||||
onReceiveDrop?.(payload, intent);
|
||||
return intent;
|
||||
}
|
||||
};
|
||||
|
||||
/** Scheduled events connected to this collection */
|
||||
const events = $derived.by(() => {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,39 @@
|
|||
<script lang="ts">
|
||||
import type { Node } from '$lib/spacetime';
|
||||
import { edgeStore, nodeStore } from '$lib/spacetime';
|
||||
import { setDragPayload } from '$lib/transfer';
|
||||
import { setDragPayload, checkChatCompat, type DragPayload } from '$lib/transfer';
|
||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||
import TraitPanel from './TraitPanel.svelte';
|
||||
|
||||
interface Props {
|
||||
collection: Node;
|
||||
config: Record<string, unknown>;
|
||||
userId?: string;
|
||||
/** Called when a drop is received on this panel */
|
||||
onReceiveDrop?: (payload: DragPayload, intent: PlacementIntent) => void;
|
||||
}
|
||||
|
||||
let { collection, config, userId }: Props = $props();
|
||||
let { collection, config, userId, onReceiveDrop }: Props = $props();
|
||||
|
||||
/**
|
||||
* BlockReceiver implementation for Chat.
|
||||
* Accepts any node — wraps as message with mentions.
|
||||
* Ref: docs/retninger/arbeidsflaten.md § Kompatibilitetsmatrise
|
||||
*/
|
||||
export const receiver: BlockReceiver = {
|
||||
canReceive(payload: DragPayload) {
|
||||
return checkChatCompat(payload);
|
||||
},
|
||||
receive(payload: DragPayload) {
|
||||
const intent: PlacementIntent = {
|
||||
mode: 'lettvekts-triage',
|
||||
contextId: collection?.id ?? '',
|
||||
contextType: 'chat',
|
||||
};
|
||||
onReceiveDrop?.(payload, intent);
|
||||
return intent;
|
||||
}
|
||||
};
|
||||
|
||||
/** Communication nodes linked to this collection */
|
||||
const chatNodes = $derived.by(() => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
import { deleteEdge, aiProcess } from '$lib/api';
|
||||
import TraitPanel from './TraitPanel.svelte';
|
||||
import PublishDialog from '$lib/components/PublishDialog.svelte';
|
||||
import { getDragPayload, setDragPayload, checkToolToNodeCompat } from '$lib/transfer';
|
||||
import { getDragPayload, setDragPayload, checkToolToNodeCompat, checkEditorCompat, type DragPayload } from '$lib/transfer';
|
||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||
|
||||
interface Props {
|
||||
collection: Node;
|
||||
|
|
@ -13,9 +14,36 @@
|
|||
accessToken?: string;
|
||||
/** Full collection metadata (for reading traits) */
|
||||
collectionMetadata?: Record<string, unknown>;
|
||||
/** Called when a drop is received on this panel */
|
||||
onReceiveDrop?: (payload: DragPayload, intent: PlacementIntent) => void;
|
||||
}
|
||||
|
||||
let { collection, config, userId, accessToken, collectionMetadata }: Props = $props();
|
||||
let { collection, config, userId, accessToken, collectionMetadata, onReceiveDrop }: Props = $props();
|
||||
|
||||
/**
|
||||
* BlockReceiver implementation for Editor (Artikkelverktøy).
|
||||
* Accepts text nodes as source_material, media as embedded content.
|
||||
* Also handles AI preset drops (tool_to_node) separately via inline handlers.
|
||||
* Ref: docs/retninger/arbeidsflaten.md § Kompatibilitetsmatrise
|
||||
*/
|
||||
export const receiver: BlockReceiver = {
|
||||
canReceive(payload: DragPayload) {
|
||||
// AI preset drops are handled by the inline item-level handlers
|
||||
if (payload.nodeKind === 'ai_preset' && payload.presetId) {
|
||||
return { compatible: true };
|
||||
}
|
||||
return checkEditorCompat(payload);
|
||||
},
|
||||
receive(payload: DragPayload) {
|
||||
const intent: PlacementIntent = {
|
||||
mode: 'innholdstransfer',
|
||||
contextId: collection?.id ?? '',
|
||||
contextType: 'editor',
|
||||
};
|
||||
onReceiveDrop?.(payload, intent);
|
||||
return intent;
|
||||
}
|
||||
};
|
||||
|
||||
const preset = $derived((config.preset as string) ?? 'longform');
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,40 @@
|
|||
<script lang="ts">
|
||||
import type { Node } from '$lib/spacetime';
|
||||
import { checkKanbanCompat, type DragPayload } from '$lib/transfer';
|
||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||
import TraitPanel from './TraitPanel.svelte';
|
||||
|
||||
interface Props {
|
||||
collection: Node;
|
||||
config: Record<string, unknown>;
|
||||
userId?: string;
|
||||
/** Called when a drop is received on this panel */
|
||||
onReceiveDrop?: (payload: DragPayload, intent: PlacementIntent) => void;
|
||||
}
|
||||
|
||||
let { collection, config, userId }: Props = $props();
|
||||
let { collection, config, userId, onReceiveDrop }: Props = $props();
|
||||
|
||||
/**
|
||||
* BlockReceiver implementation for Kanban.
|
||||
* Accepts communication and content nodes as new tasks.
|
||||
* Creates source_material edge to the original.
|
||||
* Ref: docs/retninger/arbeidsflaten.md § Kompatibilitetsmatrise
|
||||
*/
|
||||
export const receiver: BlockReceiver = {
|
||||
canReceive(payload: DragPayload) {
|
||||
return checkKanbanCompat(payload);
|
||||
},
|
||||
receive(payload: DragPayload) {
|
||||
const intent: PlacementIntent = {
|
||||
mode: 'innholdstransfer',
|
||||
contextId: collection?.id ?? '',
|
||||
contextType: 'kanban',
|
||||
position: { column_id: 'inbox', position: 0 },
|
||||
};
|
||||
onReceiveDrop?.(payload, intent);
|
||||
return intent;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<TraitPanel name="kanban" label="Kanban-brett" icon="📋">
|
||||
|
|
|
|||
|
|
@ -1,15 +1,39 @@
|
|||
<script lang="ts">
|
||||
import type { Node } from '$lib/spacetime';
|
||||
import { edgeStore, nodeStore, nodeVisibility } from '$lib/spacetime';
|
||||
import { checkStudioCompat, type DragPayload } from '$lib/transfer';
|
||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||
import TraitPanel from './TraitPanel.svelte';
|
||||
|
||||
interface Props {
|
||||
collection: Node;
|
||||
config: Record<string, unknown>;
|
||||
userId?: string;
|
||||
/** Called when a drop is received on this panel */
|
||||
onReceiveDrop?: (payload: DragPayload, intent: PlacementIntent) => void;
|
||||
}
|
||||
|
||||
let { collection, config, userId }: Props = $props();
|
||||
let { collection, config, userId, onReceiveDrop }: Props = $props();
|
||||
|
||||
/**
|
||||
* BlockReceiver implementation for Studio (Lydstudio).
|
||||
* Only accepts audio files — opens them in the studio.
|
||||
* Ref: docs/retninger/arbeidsflaten.md § Kompatibilitetsmatrise
|
||||
*/
|
||||
export const receiver: BlockReceiver = {
|
||||
canReceive(payload: DragPayload) {
|
||||
return checkStudioCompat(payload);
|
||||
},
|
||||
receive(payload: DragPayload) {
|
||||
const intent: PlacementIntent = {
|
||||
mode: 'lettvekts-triage',
|
||||
contextId: collection?.id ?? '',
|
||||
contextType: 'studio',
|
||||
};
|
||||
onReceiveDrop?.(payload, intent);
|
||||
return intent;
|
||||
}
|
||||
};
|
||||
|
||||
/** Media nodes (audio files) belonging to this collection */
|
||||
const audioNodes = $derived.by(() => {
|
||||
|
|
|
|||
|
|
@ -118,3 +118,141 @@ export function checkToolToNodeCompat(nodeKind: string, hasContent: boolean): Co
|
|||
}
|
||||
return { compatible: true };
|
||||
}
|
||||
|
||||
// --- Per-tool compatibility checks ---
|
||||
// Ref: docs/retninger/arbeidsflaten.md § Kompatibilitetsmatrise
|
||||
|
||||
/**
|
||||
* Check compatibility for dropping a node onto the Chat panel.
|
||||
* Chat accepts practically anything — it wraps the node as a message with mentions.
|
||||
*/
|
||||
export function checkChatCompat(payload: DragPayload): CompatResult {
|
||||
if (payload.sourcePanel === 'chat') {
|
||||
return {
|
||||
compatible: false,
|
||||
reason: 'Noden er allerede i denne chatten.',
|
||||
suggestion: 'Dra til en annen kontekst for å dele.'
|
||||
};
|
||||
}
|
||||
return { compatible: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check compatibility for dropping a node onto the Kanban panel.
|
||||
* Kanban accepts text-based nodes (communication, content) as new tasks.
|
||||
* Media and images can't become tasks directly.
|
||||
*/
|
||||
export function checkKanbanCompat(payload: DragPayload): CompatResult {
|
||||
if (payload.sourcePanel === 'kanban') {
|
||||
// Same board — handled as column move, not a new drop
|
||||
return { compatible: true };
|
||||
}
|
||||
if (isMediaKind(payload.nodeKind)) {
|
||||
return {
|
||||
compatible: false,
|
||||
reason: 'Mediefiler kan ikke bli oppgaver direkte.',
|
||||
suggestion: 'Dra til Chat for å dele, eller til Artikkelverktøyet for å sette inn.'
|
||||
};
|
||||
}
|
||||
if (payload.nodeKind === 'communication' || payload.nodeKind === 'content') {
|
||||
return { compatible: true };
|
||||
}
|
||||
return {
|
||||
compatible: false,
|
||||
reason: `Nodetypen «${payload.nodeKind}» kan ikke legges til som oppgave.`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check compatibility for dropping a node onto the Calendar panel.
|
||||
* Calendar accepts text nodes as events, and kanban cards for scheduling.
|
||||
*/
|
||||
export function checkCalendarCompat(payload: DragPayload): CompatResult {
|
||||
if (isMediaKind(payload.nodeKind)) {
|
||||
return {
|
||||
compatible: false,
|
||||
reason: 'Mediefiler kan ikke planlegges i kalenderen.',
|
||||
suggestion: 'Opprett en oppgave først, og planlegg den.'
|
||||
};
|
||||
}
|
||||
if (payload.nodeKind === 'communication' || payload.nodeKind === 'content') {
|
||||
return { compatible: true };
|
||||
}
|
||||
return {
|
||||
compatible: false,
|
||||
reason: `Nodetypen «${payload.nodeKind}» kan ikke legges til i kalenderen.`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check compatibility for dropping a node onto the Editor (Artikkelverktøy) panel.
|
||||
* Editor accepts most node types — text as source_material, media as embedded content.
|
||||
*/
|
||||
export function checkEditorCompat(payload: DragPayload): CompatResult {
|
||||
if (payload.nodeKind === 'communication' || payload.nodeKind === 'content') {
|
||||
return { compatible: true };
|
||||
}
|
||||
if (isMediaKind(payload.nodeKind)) {
|
||||
// Media (audio, image) can be embedded
|
||||
return { compatible: true };
|
||||
}
|
||||
// Agent, person, collection etc. don't make sense in editor
|
||||
return {
|
||||
compatible: false,
|
||||
reason: `Nodetypen «${payload.nodeKind}» kan ikke settes inn i artikkelverktøyet.`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check compatibility for dropping a node onto the Studio panel.
|
||||
* Studio only accepts audio files.
|
||||
*/
|
||||
export function checkStudioCompat(payload: DragPayload): CompatResult {
|
||||
if (payload.sourcePanel === 'studio') {
|
||||
return {
|
||||
compatible: false,
|
||||
reason: 'Lydfiler kan ikke slås sammen direkte.',
|
||||
suggestion: 'Åpne i Studioet for å redigere.'
|
||||
};
|
||||
}
|
||||
if (isAudioKind(payload.nodeKind)) {
|
||||
return { compatible: true };
|
||||
}
|
||||
if (isMediaKind(payload.nodeKind)) {
|
||||
return {
|
||||
compatible: false,
|
||||
reason: 'Studioet behandler kun lydfiler.',
|
||||
suggestion: payload.nodeKind === 'image'
|
||||
? 'Bilder kan dras til Artikkelverktøyet.'
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
return {
|
||||
compatible: false,
|
||||
reason: 'Studioet behandler kun lydfiler.',
|
||||
suggestion: 'Dra lydfiler hit for å åpne dem i studioet.'
|
||||
};
|
||||
}
|
||||
|
||||
/** Map of tool type to its compatibility check function */
|
||||
const TOOL_COMPAT_CHECKS: Record<ToolType, (payload: DragPayload) => CompatResult> = {
|
||||
chat: checkChatCompat,
|
||||
kanban: checkKanbanCompat,
|
||||
calendar: checkCalendarCompat,
|
||||
editor: checkEditorCompat,
|
||||
studio: checkStudioCompat,
|
||||
ai_tool: (payload) => checkAiToolCompat(payload.nodeKind, true), // hasContent not knowable from payload alone
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a BlockReceiver for a given tool type.
|
||||
* Used by workspace/BlockShell to validate drops.
|
||||
*/
|
||||
export function createBlockReceiver(toolType: ToolType): { canReceive: (payload: DragPayload) => CompatResult } {
|
||||
const checkFn = TOOL_COMPAT_CHECKS[toolType];
|
||||
return {
|
||||
canReceive(payload: DragPayload): CompatResult {
|
||||
return checkFn(payload);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@
|
|||
import MixerTrait from '$lib/components/traits/MixerTrait.svelte';
|
||||
import GenericTrait from '$lib/components/traits/GenericTrait.svelte';
|
||||
import AiToolPanel from '$lib/components/AiToolPanel.svelte';
|
||||
import { createBlockReceiver, type DragPayload } from '$lib/transfer';
|
||||
import type { BlockReceiver } from '$lib/components/blockshell/types';
|
||||
|
||||
const session = $derived($page.data.session as Record<string, unknown> | undefined);
|
||||
const nodeId = $derived(session?.nodeId as string | undefined);
|
||||
|
|
@ -321,6 +323,36 @@
|
|||
'editor', 'chat', 'kanban', 'podcast', 'publishing',
|
||||
'rss', 'calendar', 'recording', 'transcription', 'studio', 'mixer'
|
||||
]);
|
||||
|
||||
// =========================================================================
|
||||
// BlockReceiver per tool type — wired into BlockShell for drop compatibility
|
||||
// Ref: docs/features/universell_overfoering.md § 4
|
||||
// =========================================================================
|
||||
|
||||
/** Traits that support BlockReceiver drops */
|
||||
const RECEIVER_TRAITS = new Set(['editor', 'chat', 'kanban', 'calendar', 'studio']);
|
||||
|
||||
/** Cache receivers by trait name — one per type */
|
||||
const receiverCache = new Map<string, BlockReceiver>();
|
||||
|
||||
function getReceiverForTrait(trait: string): BlockReceiver | undefined {
|
||||
if (!RECEIVER_TRAITS.has(trait)) return undefined;
|
||||
if (!receiverCache.has(trait)) {
|
||||
const toolType = trait as import('$lib/transfer').ToolType;
|
||||
receiverCache.set(trait, createBlockReceiver(toolType));
|
||||
}
|
||||
return receiverCache.get(trait);
|
||||
}
|
||||
|
||||
function handlePanelDrop(trait: string, payload: DragPayload) {
|
||||
// Drop handling will be fully implemented in task 20.4 (transfer service).
|
||||
// For now, log the intent for debugging.
|
||||
const receiver = getReceiverForTrait(trait);
|
||||
if (receiver?.receive) {
|
||||
const intent = receiver.receive(payload);
|
||||
console.log(`[universell-overføring] ${payload.sourcePanel} → ${trait}:`, intent);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
|
||||
|
|
@ -516,9 +548,11 @@
|
|||
width={panel?.width ?? obj.width}
|
||||
height={panel?.height ?? obj.height}
|
||||
minimized={panel?.minimized ?? false}
|
||||
receiver={getReceiverForTrait(trait)}
|
||||
onResize={(w, h) => handlePanelResize(trait, w, h)}
|
||||
onClose={() => handlePanelClose(trait)}
|
||||
onMinimizeChange={(m) => handlePanelMinimize(trait, m)}
|
||||
onDrop={(payload) => handlePanelDrop(trait, payload)}
|
||||
>
|
||||
{#if knownTraits.has(trait)}
|
||||
{#if trait === 'editor'}
|
||||
|
|
|
|||
3
tasks.md
3
tasks.md
|
|
@ -225,8 +225,7 @@ Ref: `docs/features/universell_overfoering.md`, `docs/retninger/arbeidsflaten.md
|
|||
|
||||
- [x] 20.1 message_placements tabell: PG-migrasjon + SpacetimeDB-modul med `place_message`, `remove_placement`, `move_on_canvas` reducers. Synk STDB→PG. Ref: `docs/features/universell_overfoering.md` § 2.
|
||||
- [x] 20.2 source_material edge-type: legg til i edge-skjema + maskinrommet-validering. Støtt kontekst-metadata (quoted, summarized, referenced) og excerpt-felt. Ref: `docs/retninger/arbeidsflaten.md` § "source_material-edge".
|
||||
- [~] 20.3 BlockReceiver interface: implementer `canReceive()`, `receive()`, `renderDropZone()` i alle trait-komponenter (Chat, Kanban, Kalender, Editor, Studio). Kompatibilitetsmatrise bestemmer godkjente drops. Ref: `docs/features/universell_overfoering.md` § 4–5.
|
||||
> Påbegynt: 2026-03-18T08:05
|
||||
- [x] 20.3 BlockReceiver interface: implementer `canReceive()`, `receive()`, `renderDropZone()` i alle trait-komponenter (Chat, Kanban, Kalender, Editor, Studio). Kompatibilitetsmatrise bestemmer godkjente drops. Ref: `docs/features/universell_overfoering.md` § 4–5.
|
||||
- [ ] 20.4 Transfer service: `innholdstransfer`-modus (ny node + source_material edge) og `lettvekts-triage` (eksisterende node + ny edge/placement). Bestem modus fra verktøy-par. Shift-modifier for override. Ref: `docs/features/universell_overfoering.md` § 1, 3.
|
||||
- [ ] 20.5 Panelrework — Chat: gjør ChatTrait til fullverdig BlockShell-panel med BlockReceiver, fullskjerm-toggle, og responsivt design innenfor begrenset container.
|
||||
- [ ] 20.6 Panelrework — Kanban: gjør KanbanTrait til BlockShell-panel med drag-and-drop aksept fra andre paneler, fullskjerm, responsivt.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue