Gjør StudioTrait til fullverdig BlockShell-panel med lydstudio-funksjonalitet (oppgave 20.9)
StudioTrait var en minimal liste med TraitPanel-wrapper. Nå er den et fullverdig BlockShell-panel som følger mønsteret fra CalendarTrait (20.7) og EditorTrait (20.8): - Fjernet TraitPanel-wrapper, bruker egen flex-layout som fyller BlockShell - Viser lydfiler med metadata: format-tag, varighet, størrelse, dato - Drag-and-drop ut: lydfiler kan dras til andre paneler (editor, kalender) - Drop-aksept: mottar lydfiler fra andre paneler via BlockReceiver - Viser prosesserte versjoner (derived_from edges) per fil - Responsivt med @container queries (360px, 500px) og @media fallback - Lagt til `studio` trait i trait-katalogen i docs/primitiver/traits.md - accessToken sendes nå fra collection page til StudioTrait Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
204d46866f
commit
5babad9e59
4 changed files with 435 additions and 30 deletions
|
|
@ -104,6 +104,8 @@ Fravær av en trait betyr at funksjonaliteten er deaktivert. Ingen boolean
|
||||||
| `tts` | "Les opp"-knapp, lydversjon av artikler | Tekst-til-tale via jobbkø |
|
| `tts` | "Les opp"-knapp, lydversjon av artikler | Tekst-til-tale via jobbkø |
|
||||||
| `clips` | Klipp-editor, segment-markering | Segmentering, CAS-lagring av klipp |
|
| `clips` | Klipp-editor, segment-markering | Segmentering, CAS-lagring av klipp |
|
||||||
| `playlist` | Ordnet avspillingsliste, drag-and-drop rekkefølge | Sekvensiell avspilling |
|
| `playlist` | Ordnet avspillingsliste, drag-and-drop rekkefølge | Sekvensiell avspilling |
|
||||||
|
| `mixer` | Lydmixer: volumslidere, mute, VU-meter, sound pads, stemme-/EQ-effekter | Pad-konfig i metadata (se `docs/features/lydmixer.md`) |
|
||||||
|
| `studio` | Lydstudio-panel: lydfilliste med metadata, drag-ut, drop-aksept for lyd, lenke til fullverdig editor | Validering av lydformat, FFmpeg-pipeline via jobbkø (se `docs/features/lydstudio.md`) |
|
||||||
|
|
||||||
### Kommunikasjon
|
### Kommunikasjon
|
||||||
|
|
||||||
|
|
@ -148,6 +150,7 @@ Fravær av en trait betyr at funksjonaliteten er deaktivert. Ingen boolean
|
||||||
| `digest` | Periodisk oppsummering i UI | AI-sammendrag av aktivitet på intervall |
|
| `digest` | Periodisk oppsummering i UI | AI-sammendrag av aktivitet på intervall |
|
||||||
| `bridge` | "Også funnet i..."-forslag | pgvector-embedding, krysskontekst-søk |
|
| `bridge` | "Også funnet i..."-forslag | pgvector-embedding, krysskontekst-søk |
|
||||||
| `moderation` | Moderasjonskø, flagging | AI-assistert innholdsvurdering |
|
| `moderation` | Moderasjonskø, flagging | AI-assistert innholdsvurdering |
|
||||||
|
| `ai_tool` | AI-verktøy-panel med prompt-velger, drag-and-drop tekstbehandling | Modellprofil-mapping, AI Gateway, ai_usage_log (se `docs/features/ai_verktoy.md`) |
|
||||||
|
|
||||||
### Tilgang & fellesskap
|
### Tilgang & fellesskap
|
||||||
|
|
||||||
|
|
@ -177,16 +180,16 @@ Brukeren kan legge til eller fjerne traits etterpå.
|
||||||
| Pakke | Traits |
|
| Pakke | Traits |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Nettmagasin** | editor(longform), publishing, rss, comments, analytics, custom_domain, newsletter |
|
| **Nettmagasin** | editor(longform), publishing, rss, comments, analytics, custom_domain, newsletter |
|
||||||
| **Podcaststudio** | podcast, recording, transcription, editor(shownotes), rss, analytics, clips, knowledge_graph |
|
| **Podcaststudio** | podcast, recording, transcription, mixer, editor(shownotes), rss, analytics, clips, knowledge_graph |
|
||||||
| **Nyhetsbrev** | editor(longform), newsletter, analytics, versioning |
|
| **Nyhetsbrev** | editor(longform), newsletter, analytics, versioning |
|
||||||
| **Wiki** | wiki, editor(longform), collaboration, versioning, knowledge_graph, glossary |
|
| **Wiki** | wiki, editor(longform), collaboration, versioning, knowledge_graph, glossary |
|
||||||
| **Diskusjonsklubb** | forum, chat, polls, membership, roles, directory |
|
| **Diskusjonsklubb** | forum, chat, polls, membership, roles, directory |
|
||||||
| **Kursplattform** | editor(longform), playlist, qa, membership, paywall, templates |
|
| **Kursplattform** | editor(longform), playlist, qa, membership, paywall, templates |
|
||||||
| **Møteplass** | recording, chat, kanban, calendar, auto_summarize, guest_input |
|
| **Møteplass** | recording, mixer, chat, kanban, calendar, auto_summarize, guest_input |
|
||||||
| **Fotoblogg** | gallery, publishing, comments, custom_domain, rss |
|
| **Fotoblogg** | gallery, publishing, comments, custom_domain, rss |
|
||||||
| **Prosjektstyring** | kanban, calendar, chat, table, tags, roles |
|
| **Prosjektstyring** | kanban, calendar, chat, table, tags, roles |
|
||||||
| **Åpen forskning** | editor(longform), versioning, bibliography, publishing, comments, collaboration, api |
|
| **Åpen forskning** | editor(longform), versioning, bibliography, publishing, comments, collaboration, api |
|
||||||
| **Community radio** | recording, podcast, chat, polls, membership, clips, playlist |
|
| **Community radio** | recording, mixer, podcast, chat, polls, membership, clips, playlist |
|
||||||
| **Bokmerke-vegg** | bookmarks, tags, publishing, rss, comments |
|
| **Bokmerke-vegg** | bookmarks, tags, publishing, rss, comments |
|
||||||
| **Redaksjon** | chat, kanban, calendar, editor(longform), knowledge_graph, guest_input |
|
| **Redaksjon** | chat, kanban, calendar, editor(longform), knowledge_graph, guest_input |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Node } from '$lib/spacetime';
|
import type { Node } from '$lib/spacetime';
|
||||||
import { edgeStore, nodeStore, nodeVisibility } from '$lib/spacetime';
|
import { edgeStore, nodeStore, nodeVisibility } from '$lib/spacetime';
|
||||||
import { checkStudioCompat, type DragPayload } from '$lib/transfer';
|
import { setDragPayload, checkStudioCompat, type DragPayload } from '$lib/transfer';
|
||||||
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
import type { BlockReceiver, PlacementIntent } from '$lib/components/blockshell/types';
|
||||||
import TraitPanel from './TraitPanel.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
collection: Node;
|
collection: Node;
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
accessToken?: string;
|
||||||
/** Called when a drop is received on this panel */
|
/** Called when a drop is received on this panel */
|
||||||
onReceiveDrop?: (payload: DragPayload, intent: PlacementIntent) => void;
|
onReceiveDrop?: (payload: DragPayload, intent: PlacementIntent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { collection, config, userId, onReceiveDrop }: Props = $props();
|
let { collection, config, userId, accessToken, onReceiveDrop }: Props = $props();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BlockReceiver implementation for Studio (Lydstudio).
|
* BlockReceiver implementation for Studio (Lydstudio).
|
||||||
|
|
@ -35,7 +35,10 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Media nodes (audio files) belonging to this collection */
|
// =========================================================================
|
||||||
|
// Audio nodes belonging to this collection
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
const audioNodes = $derived.by(() => {
|
const audioNodes = $derived.by(() => {
|
||||||
const nodes: Node[] = [];
|
const nodes: Node[] = [];
|
||||||
for (const edge of edgeStore.byTarget(collection.id)) {
|
for (const edge of edgeStore.byTarget(collection.id)) {
|
||||||
|
|
@ -59,6 +62,10 @@
|
||||||
return nodes;
|
return nodes;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helpers
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
/** Check if a node has processed versions (derived_from edges) */
|
/** Check if a node has processed versions (derived_from edges) */
|
||||||
function hasVersions(nodeId: string): boolean {
|
function hasVersions(nodeId: string): boolean {
|
||||||
for (const edge of edgeStore.byTarget(nodeId)) {
|
for (const edge of edgeStore.byTarget(nodeId)) {
|
||||||
|
|
@ -66,29 +73,425 @@
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get version count for a node */
|
||||||
|
function versionCount(nodeId: string): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const edge of edgeStore.byTarget(nodeId)) {
|
||||||
|
if (edge.edgeType === 'derived_from') count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format duration from metadata (seconds → mm:ss) */
|
||||||
|
function formatDuration(node: Node): string {
|
||||||
|
try {
|
||||||
|
const meta = JSON.parse(node.metadata ?? '{}');
|
||||||
|
if (typeof meta.duration === 'number') {
|
||||||
|
const m = Math.floor(meta.duration / 60);
|
||||||
|
const s = Math.floor(meta.duration % 60);
|
||||||
|
return `${m}:${String(s).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format file size from metadata */
|
||||||
|
function formatSize(node: Node): string {
|
||||||
|
try {
|
||||||
|
const meta = JSON.parse(node.metadata ?? '{}');
|
||||||
|
if (typeof meta.size === 'number') {
|
||||||
|
const kb = meta.size / 1024;
|
||||||
|
if (kb < 1024) return `${Math.round(kb)} KB`;
|
||||||
|
return `${(kb / 1024).toFixed(1)} MB`;
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get MIME subtype (e.g. 'wav' from 'audio/wav') */
|
||||||
|
function formatType(node: Node): string {
|
||||||
|
try {
|
||||||
|
const meta = JSON.parse(node.metadata ?? '{}');
|
||||||
|
if (typeof meta.mime === 'string') {
|
||||||
|
return meta.mime.replace('audio/', '').toUpperCase();
|
||||||
|
}
|
||||||
|
} catch { /* skip */ }
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(node: Node): string {
|
||||||
|
if (!node.createdAt?.microsSinceUnixEpoch) return '';
|
||||||
|
const ms = Number(node.createdAt.microsSinceUnixEpoch / 1000n);
|
||||||
|
const date = new Date(ms);
|
||||||
|
return date.toLocaleDateString('nb-NO', { day: 'numeric', month: 'short' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Drag-and-drop: audio items as drag source
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function handleDragStart(e: DragEvent, node: Node) {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
setDragPayload(e.dataTransfer, {
|
||||||
|
nodeId: node.id,
|
||||||
|
nodeKind: node.nodeKind,
|
||||||
|
sourcePanel: 'studio'
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TraitPanel title="Lydstudio" icon="studio">
|
<!--
|
||||||
|
StudioTrait — fullverdig BlockShell-panel for lydstudio.
|
||||||
|
Viser lydfiler med metadata, drag-and-drop ut til andre paneler,
|
||||||
|
drop-aksept for lydfiler fra andre paneler, og lenke til studioet.
|
||||||
|
Forelder (collection page) wrapper dette i BlockShell.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="studio-trait">
|
||||||
{#if audioNodes.length === 0}
|
{#if audioNodes.length === 0}
|
||||||
<p class="text-sm text-gray-400">Ingen lydfiler i denne samlingen enna.</p>
|
<div class="studio-empty">
|
||||||
|
<span class="studio-empty-icon">🎙️</span>
|
||||||
|
<p>Ingen lydfiler i denne samlingen.</p>
|
||||||
|
<p class="studio-empty-hint">Dra lydfiler hit fra andre paneler, eller last opp via studio.</p>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="space-y-2">
|
<div class="studio-header">
|
||||||
{#each audioNodes as node}
|
<span class="studio-header-label">Lydfiler</span>
|
||||||
<li class="flex items-center justify-between rounded border border-gray-100 p-2">
|
<span class="studio-header-count">{audioNodes.length}</span>
|
||||||
<div class="min-w-0 flex-1">
|
</div>
|
||||||
<p class="truncate text-sm text-gray-700">{node.title ?? 'Uten tittel'}</p>
|
|
||||||
{#if hasVersions(node.id)}
|
<div class="studio-items">
|
||||||
<span class="text-[10px] text-green-600">Har prosesserte versjoner</span>
|
{#each audioNodes as node (node.id)}
|
||||||
{/if}
|
{@const duration = formatDuration(node)}
|
||||||
|
{@const size = formatSize(node)}
|
||||||
|
{@const type = formatType(node)}
|
||||||
|
{@const versions = versionCount(node.id)}
|
||||||
|
{@const date = formatTime(node)}
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="studio-item"
|
||||||
|
draggable="true"
|
||||||
|
ondragstart={(e) => handleDragStart(e, node)}
|
||||||
|
>
|
||||||
|
<div class="studio-item-main">
|
||||||
|
<div class="studio-item-icon">🎵</div>
|
||||||
|
<div class="studio-item-text">
|
||||||
|
<span class="studio-item-title">{node.title ?? 'Uten tittel'}</span>
|
||||||
|
<div class="studio-item-meta">
|
||||||
|
{#if type}
|
||||||
|
<span class="studio-item-tag">{type}</span>
|
||||||
|
{/if}
|
||||||
|
{#if duration}
|
||||||
|
<span class="studio-item-duration">{duration}</span>
|
||||||
|
{/if}
|
||||||
|
{#if size}
|
||||||
|
<span class="studio-item-size">{size}</span>
|
||||||
|
{/if}
|
||||||
|
{#if date}
|
||||||
|
<span class="studio-item-date">{date}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="studio-item-actions">
|
||||||
|
{#if versions > 0}
|
||||||
|
<span class="studio-item-versions" title="{versions} prosesserte versjon(er)">
|
||||||
|
{versions} ver.
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
<a
|
||||||
|
href="/studio/{node.id}"
|
||||||
|
class="studio-edit-btn"
|
||||||
|
title="Åpne i lydstudio"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
Rediger
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a
|
</div>
|
||||||
href="/studio/{node.id}"
|
|
||||||
class="ml-2 shrink-0 rounded bg-blue-50 px-3 py-1 text-xs text-blue-600 transition-colors hover:bg-blue-100"
|
|
||||||
>
|
|
||||||
Rediger
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</TraitPanel>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Root — fills BlockShell content area */
|
||||||
|
/* ================================================================= */
|
||||||
|
.studio-trait {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Empty state */
|
||||||
|
/* ================================================================= */
|
||||||
|
.studio-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
padding: 24px;
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-empty-icon {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-empty-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #d1d5db;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Header */
|
||||||
|
/* ================================================================= */
|
||||||
|
.studio-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-header-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9ca3af;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-header-count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #9ca3af;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 9999px;
|
||||||
|
padding: 0 6px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Items list */
|
||||||
|
/* ================================================================= */
|
||||||
|
.studio-items {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
cursor: grab;
|
||||||
|
transition: box-shadow 0.1s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item:hover {
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
border-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1f2937;
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-tag {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #7c3aed;
|
||||||
|
background: #ede9fe;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-duration {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-size {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-date {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-versions {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #059669;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-edit-btn {
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #ede9fe;
|
||||||
|
color: #7c3aed;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.1s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-edit-btn:hover {
|
||||||
|
background: #ddd6fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Responsive within bounded container */
|
||||||
|
/* ================================================================= */
|
||||||
|
|
||||||
|
/* Small panels: compact layout */
|
||||||
|
@container (max-width: 360px) {
|
||||||
|
.studio-header {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-items {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-title {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-meta {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-versions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-edit-btn {
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-empty {
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-empty-icon {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Medium panels: slightly tighter */
|
||||||
|
@container (max-width: 500px) {
|
||||||
|
.studio-item-size {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-date {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile viewport fallback */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.studio-header {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-items {
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item {
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-title {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-item-size {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-empty {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -350,7 +350,7 @@
|
||||||
{:else if trait === 'transcription'}
|
{:else if trait === 'transcription'}
|
||||||
<TranscriptionTrait collection={collectionNode} config={traits[trait]} />
|
<TranscriptionTrait collection={collectionNode} config={traits[trait]} />
|
||||||
{:else if trait === 'studio'}
|
{:else if trait === 'studio'}
|
||||||
<StudioTrait collection={collectionNode} config={traits[trait]} userId={nodeId} />
|
<StudioTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||||
{:else if trait === 'mixer'}
|
{:else if trait === 'mixer'}
|
||||||
<MixerTrait collection={collectionNode} config={traits[trait]} {accessToken} />
|
<MixerTrait collection={collectionNode} config={traits[trait]} {accessToken} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -405,7 +405,7 @@
|
||||||
{:else if trait === 'transcription'}
|
{:else if trait === 'transcription'}
|
||||||
<TranscriptionTrait collection={collectionNode} config={traits[trait]} />
|
<TranscriptionTrait collection={collectionNode} config={traits[trait]} />
|
||||||
{:else if trait === 'studio'}
|
{:else if trait === 'studio'}
|
||||||
<StudioTrait collection={collectionNode} config={traits[trait]} userId={nodeId} />
|
<StudioTrait collection={collectionNode} config={traits[trait]} userId={nodeId} {accessToken} />
|
||||||
{:else if trait === 'mixer'}
|
{:else if trait === 'mixer'}
|
||||||
<MixerTrait collection={collectionNode} config={traits[trait]} {accessToken} />
|
<MixerTrait collection={collectionNode} config={traits[trait]} {accessToken} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -231,8 +231,7 @@ Ref: `docs/features/universell_overfoering.md`, `docs/retninger/arbeidsflaten.md
|
||||||
- [x] 20.6 Panelrework — Kanban: gjør KanbanTrait til BlockShell-panel med drag-and-drop aksept fra andre paneler, fullskjerm, responsivt.
|
- [x] 20.6 Panelrework — Kanban: gjør KanbanTrait til BlockShell-panel med drag-and-drop aksept fra andre paneler, fullskjerm, responsivt.
|
||||||
- [x] 20.7 Panelrework — Kalender: gjør CalendarTrait til BlockShell-panel med drop-aksept for scheduling, fullskjerm, responsivt.
|
- [x] 20.7 Panelrework — Kalender: gjør CalendarTrait til BlockShell-panel med drop-aksept for scheduling, fullskjerm, responsivt.
|
||||||
- [x] 20.8 Panelrework — Editor/Artikkelverktøy: gjør artikkelverktøy til BlockShell-panel med source_material mottak fra andre paneler. Ref: `docs/features/artikkelverktoy.md`.
|
- [x] 20.8 Panelrework — Editor/Artikkelverktøy: gjør artikkelverktøy til BlockShell-panel med source_material mottak fra andre paneler. Ref: `docs/features/artikkelverktoy.md`.
|
||||||
- [~] 20.9 Panelrework — Studio: gjør StudioTrait til BlockShell-panel med drop-aksept for lydfiler, fullskjerm, responsivt.
|
- [x] 20.9 Panelrework — Studio: gjør StudioTrait til BlockShell-panel med drop-aksept for lydfiler, fullskjerm, responsivt.
|
||||||
> Påbegynt: 2026-03-18T08:48
|
|
||||||
|
|
||||||
## Fase 21: CLI-verktøy — Unix-filosofi
|
## Fase 21: CLI-verktøy — Unix-filosofi
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue