Workspace UI: AI/ressurs-paneler, innstillinger, kontekst-velger
- AI-verktøy og Ressursforbruk registrert som BlockShell-paneler i verktøymenyen (🤖 og 📊) - Innstillingsmeny (⚙️) lengst til høyre i header: tre hue-slidere (bakgrunn, overflate, aksent) + logg ut. Lagres i workspace-metadata. - Kontekst-velger: to grupper (Mine flater / Delte flater), inline rename (✏️), "+ Ny arbeidsflate"-knapp - Mørke overrides for manglende Tailwind bg-farger i app.css Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a1a1b8c460
commit
d82fab25df
4 changed files with 430 additions and 30 deletions
|
|
@ -4,20 +4,21 @@ Funnet ved manuell testing av frontend. Fikses som en samlet sesjon.
|
||||||
|
|
||||||
## Workspace
|
## Workspace
|
||||||
|
|
||||||
- [ ] AI-verktøy er hardkodet utenfor workspace (footer). Skal være et valgfritt BlockShell-panel som alle andre verktøy.
|
- [x] AI-verktøy er nå et valgfritt BlockShell-panel (🤖 i verktøymenyen).
|
||||||
- [ ] Ressursforbruk er hardkodet utenfor workspace. Skal være et valgfritt BlockShell-panel.
|
- [x] Ressursforbruk er nå et valgfritt BlockShell-panel (📊 i verktøymenyen).
|
||||||
- [x] BlockShell-knapper (minimer, maksimer, lukk) fikset:
|
- [x] BlockShell-knapper (minimer, maksimer, lukk) fikset:
|
||||||
- Minimer → kollapser til kompakt ikon/fane, bevarer posisjon
|
- Minimer → kollapser til kompakt ikon/fane, bevarer posisjon
|
||||||
- Maksimer → fullskjerm overlay (portalt til body), Escape for å gå tilbake
|
- Maksimer → fullskjerm overlay (portalt til body), Escape for å gå tilbake
|
||||||
- Lukk → fjern panel fra workspace
|
- Lukk → fjern panel fra workspace
|
||||||
- [x] Kanban-panel kan nå lukkes (samme fix)
|
- [x] Kanban-panel kan nå lukkes (samme fix)
|
||||||
- [ ] Fjern footer-feltet helt. Alt som var der (AI, ressurs) blir paneler i canvas. Canvas får full høyde.
|
- [x] Ingen footer — canvas har full høyde.
|
||||||
- [ ] Workspace-modifikatorer (zoom-knapper, fullskjerm, snap-to-grid, tilpass) er uvirksomme. Zoom via musehjul fungerer.
|
- [ ] Workspace-modifikatorer (zoom-knapper, fullskjerm, snap-to-grid, tilpass) er uvirksomme. Zoom via musehjul fungerer.
|
||||||
|
|
||||||
## Header / innstillinger
|
## Header / innstillinger
|
||||||
|
|
||||||
- [ ] Fargevelger i header-meny (⚙️): bakgrunn, overflate, accent-hue. Lagres i person-node metadata.preferences.theme. Tre slidere er nok.
|
- [x] Innstillingsmeny (⚙️-ikon lengst til høyre i header): tre hue-slidere (bakgrunn, overflate, aksent). Lagres i workspace-node metadata.preferences.theme.
|
||||||
- [ ] Innstillingsmeny (⚙️-ikon i header): tema, varsler, profil. Ikke et panel i workspace — det styrer *hele* brukeropplevelsen.
|
- [x] Logg ut-knapp i innstillingsmenyen.
|
||||||
|
- [ ] Gjenstående: varsler, profil-innstillinger.
|
||||||
|
|
||||||
## Stor refaktor: workspace er appen
|
## Stor refaktor: workspace er appen
|
||||||
|
|
||||||
|
|
@ -40,16 +41,15 @@ Funnet ved manuell testing av frontend. Fikses som en samlet sesjon.
|
||||||
|
|
||||||
## Kontekst-velger (arbeidsflate-dropdown i header)
|
## Kontekst-velger (arbeidsflate-dropdown i header)
|
||||||
|
|
||||||
- [ ] Vis "Mine flater" og "Delte flater" som to grupper i dropdown.
|
- [x] Vis "Mine flater" og "Delte flater" som to grupper i dropdown.
|
||||||
- [ ] ✏️-ikon på egne flater for inline rename (klikk, skriv, enter).
|
- [x] ✏️-ikon på egne flater for inline rename (klikk, skriv, enter).
|
||||||
- [ ] "+ Ny arbeidsflate"-knapp nederst i dropdown → opprett blank workspace-node.
|
- [x] "+ Ny arbeidsflate"-knapp nederst i dropdown → opprett blank workspace-node.
|
||||||
- [ ] "Del med..."-handling (høyreklikk eller ⚙️) → velg person/team, velg rolle → member_of-edge.
|
- [ ] "Del med..."-handling → velg person/team, velg rolle → member_of-edge. (v2)
|
||||||
- [ ] Flaten dukker opp under "Delte flater" hos mottaker.
|
- [x] Bruk begrepet "arbeidsflate" konsekvent, ikke "workspace".
|
||||||
- [ ] Bruk begrepet "arbeidsflate" konsekvent, ikke "workspace".
|
|
||||||
|
|
||||||
## Tema (pågår)
|
## Tema
|
||||||
|
|
||||||
- [x] Mørkt tema: arbeidsflaten (canvas + header)
|
- [x] Mørkt tema: arbeidsflaten (canvas + header)
|
||||||
- [x] Mørkt tema: canvas-bakgrunn + grid-linjer
|
- [x] Mørkt tema: canvas-bakgrunn + grid-linjer
|
||||||
|
- [x] Hue-slidere for bakgrunn, overflate, aksent (lagres i workspace-metadata)
|
||||||
- [ ] Gjenstående lyse elementer i chat, board, kalender, admin (CSS-override dekker noe, men hardkodede farger i style-blokker gjenstår)
|
- [ ] Gjenstående lyse elementer i chat, board, kalender, admin (CSS-override dekker noe, men hardkodede farger i style-blokker gjenstår)
|
||||||
- [ ] Lys/mørk-toggle i innstillingsmeny
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ div[style*="display: contents"] { background-color: var(--color-bg) !important;
|
||||||
.hover\:bg-indigo-600:hover, .hover\:bg-indigo-700:hover { background-color: #7577f5 !important; }
|
.hover\:bg-indigo-600:hover, .hover\:bg-indigo-700:hover { background-color: #7577f5 !important; }
|
||||||
.text-indigo-600, .text-indigo-500, .text-indigo-700 { color: #6366f1 !important; }
|
.text-indigo-600, .text-indigo-500, .text-indigo-700 { color: #6366f1 !important; }
|
||||||
.bg-indigo-50, .bg-indigo-100 { background-color: rgba(99, 102, 241, 0.15) !important; }
|
.bg-indigo-50, .bg-indigo-100 { background-color: rgba(99, 102, 241, 0.15) !important; }
|
||||||
|
.bg-purple-50 { background-color: rgba(139, 92, 246, 0.1) !important; }
|
||||||
|
.bg-green-50 { background-color: rgba(34, 197, 94, 0.1) !important; }
|
||||||
|
.bg-red-50 { background-color: rgba(239, 68, 68, 0.1) !important; }
|
||||||
|
.bg-blue-50 { background-color: rgba(59, 130, 246, 0.1) !important; }
|
||||||
|
.bg-yellow-50 { background-color: rgba(234, 179, 8, 0.1) !important; }
|
||||||
.border-indigo-300 { border-color: #6366f1 !important; }
|
.border-indigo-300 { border-color: #6366f1 !important; }
|
||||||
.hover\:border-indigo-300:hover { border-color: #6366f1 !important; }
|
.hover\:border-indigo-300:hover { border-color: #6366f1 !important; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ export const TRAIT_PANEL_INFO: Record<string, TraitPanelInfo> = {
|
||||||
mixer: { title: 'Mikser', icon: '🎚️', defaultWidth: 450, defaultHeight: 400 },
|
mixer: { title: 'Mikser', icon: '🎚️', defaultWidth: 450, defaultHeight: 400 },
|
||||||
orchestration: { title: 'Orkestrering', icon: '⚡', defaultWidth: 550, defaultHeight: 500 },
|
orchestration: { title: 'Orkestrering', icon: '⚡', defaultWidth: 550, defaultHeight: 500 },
|
||||||
mindmap: { title: 'Tankekart', icon: '🧠', defaultWidth: 600, defaultHeight: 500 },
|
mindmap: { title: 'Tankekart', icon: '🧠', defaultWidth: 600, defaultHeight: 500 },
|
||||||
|
ai: { title: 'AI-verktøy', icon: '🤖', defaultWidth: 420, defaultHeight: 500 },
|
||||||
|
usage: { title: 'Ressursforbruk', icon: '📊', defaultWidth: 380, defaultHeight: 350 },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Default info for unknown traits */
|
/** Default info for unknown traits */
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { connectionState, nodeStore, edgeStore, nodeAccessStore, nodeVisibility } from '$lib/realtime';
|
import { connectionState, nodeStore, edgeStore, nodeAccessStore, nodeVisibility } from '$lib/realtime';
|
||||||
import type { Node } from '$lib/realtime';
|
import type { Node } from '$lib/realtime';
|
||||||
import { fetchMyWorkspace, updateNode } from '$lib/api';
|
import { fetchMyWorkspace, updateNode, createNode, createEdge } from '$lib/api';
|
||||||
|
import { signOut } from '@auth/sveltekit/client';
|
||||||
|
|
||||||
// Canvas + BlockShell
|
// Canvas + BlockShell
|
||||||
import Canvas from '$lib/components/canvas/Canvas.svelte';
|
import Canvas from '$lib/components/canvas/Canvas.svelte';
|
||||||
|
|
@ -36,6 +37,8 @@
|
||||||
import MixerTrait from '$lib/components/traits/MixerTrait.svelte';
|
import MixerTrait from '$lib/components/traits/MixerTrait.svelte';
|
||||||
import MindMapTrait from '$lib/components/traits/MindMapTrait.svelte';
|
import MindMapTrait from '$lib/components/traits/MindMapTrait.svelte';
|
||||||
import GenericTrait from '$lib/components/traits/GenericTrait.svelte';
|
import GenericTrait from '$lib/components/traits/GenericTrait.svelte';
|
||||||
|
import AiToolPanel from '$lib/components/AiToolPanel.svelte';
|
||||||
|
import NodeUsage from '$lib/components/NodeUsage.svelte';
|
||||||
|
|
||||||
import { createBlockReceiver, executeTransfer, resolveTransferMode, type DragPayload } from '$lib/transfer';
|
import { createBlockReceiver, executeTransfer, resolveTransferMode, type DragPayload } from '$lib/transfer';
|
||||||
import type { BlockReceiver } from '$lib/components/blockshell/types';
|
import type { BlockReceiver } from '$lib/components/blockshell/types';
|
||||||
|
|
@ -70,6 +73,10 @@
|
||||||
} else if (!layoutInitialized) {
|
} else if (!layoutInitialized) {
|
||||||
layoutInitialized = true;
|
layoutInitialized = true;
|
||||||
}
|
}
|
||||||
|
// Load theme preferences
|
||||||
|
if (res.metadata) {
|
||||||
|
loadThemeFromMetadata(res.metadata as Record<string, unknown>);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
workspaceError = err.message;
|
workspaceError = err.message;
|
||||||
|
|
@ -120,12 +127,12 @@
|
||||||
|
|
||||||
let saveTimeout: ReturnType<typeof setTimeout> | undefined;
|
let saveTimeout: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
|
||||||
function persistLayout() {
|
/** Persist all workspace metadata (layout + preferences) */
|
||||||
|
function persistMetadata() {
|
||||||
if (!accessToken || !workspaceNodeId) return;
|
if (!accessToken || !workspaceNodeId) return;
|
||||||
clearTimeout(saveTimeout);
|
clearTimeout(saveTimeout);
|
||||||
saveTimeout = setTimeout(async () => {
|
saveTimeout = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
// Read current metadata from node store
|
|
||||||
const currentMeta = workspaceNode
|
const currentMeta = workspaceNode
|
||||||
? JSON.parse(workspaceNode.metadata ?? '{}')
|
? JSON.parse(workspaceNode.metadata ?? '{}')
|
||||||
: {};
|
: {};
|
||||||
|
|
@ -134,14 +141,21 @@
|
||||||
metadata: {
|
metadata: {
|
||||||
...currentMeta,
|
...currentMeta,
|
||||||
workspace_layout: layout,
|
workspace_layout: layout,
|
||||||
|
preferences: {
|
||||||
|
...(currentMeta.preferences ?? {}),
|
||||||
|
theme: { hueBg: themeHueBg, hueSurface: themeHueSurface, hueAccent: themeHueAccent },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('Failed to persist workspace layout:', err);
|
console.warn('Failed to persist workspace metadata:', err);
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep old name as alias for callers
|
||||||
|
function persistLayout() { persistMetadata(); }
|
||||||
|
|
||||||
function handleObjectMove(id: string, x: number, y: number) {
|
function handleObjectMove(id: string, x: number, y: number) {
|
||||||
const idx = layout.panels.findIndex(p => p.trait === id);
|
const idx = layout.panels.findIndex(p => p.trait === id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
|
|
@ -240,11 +254,67 @@
|
||||||
return collectionNodes.filter(n => (n.title ?? '').toLowerCase().includes(q));
|
return collectionNodes.filter(n => (n.title ?? '').toLowerCase().includes(q));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const myCollections = $derived(filteredCollections.filter(n => n.createdBy === nodeId));
|
||||||
|
const sharedCollections = $derived(filteredCollections.filter(n => n.createdBy !== nodeId));
|
||||||
|
|
||||||
|
let renamingId = $state<string | undefined>(undefined);
|
||||||
|
let renameValue = $state('');
|
||||||
|
|
||||||
|
function startRename(node: Node) {
|
||||||
|
renamingId = node.id;
|
||||||
|
renameValue = node.title || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function commitRename() {
|
||||||
|
if (!renamingId || !accessToken) return;
|
||||||
|
const id = renamingId;
|
||||||
|
renamingId = undefined;
|
||||||
|
if (renameValue.trim()) {
|
||||||
|
try {
|
||||||
|
await updateNode(accessToken, { node_id: id, title: renameValue.trim() });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Feil ved omdøping:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRenameKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); commitRename(); }
|
||||||
|
if (e.key === 'Escape') { renamingId = undefined; }
|
||||||
|
}
|
||||||
|
|
||||||
|
let isCreatingWorkspace = $state(false);
|
||||||
|
|
||||||
|
async function createNewWorkspace() {
|
||||||
|
if (!accessToken || !nodeId || isCreatingWorkspace) return;
|
||||||
|
isCreatingWorkspace = true;
|
||||||
|
selectorOpen = false;
|
||||||
|
try {
|
||||||
|
const { node_id } = await createNode(accessToken, {
|
||||||
|
node_kind: 'collection',
|
||||||
|
title: 'Ny arbeidsflate',
|
||||||
|
visibility: 'hidden',
|
||||||
|
});
|
||||||
|
await createEdge(accessToken, {
|
||||||
|
source_id: nodeId,
|
||||||
|
target_id: node_id,
|
||||||
|
edge_type: 'owner',
|
||||||
|
});
|
||||||
|
goto(`/collection/${node_id}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Feil ved oppretting av arbeidsflate:', e);
|
||||||
|
} finally {
|
||||||
|
isCreatingWorkspace = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSelector() {
|
function toggleSelector() {
|
||||||
selectorOpen = !selectorOpen;
|
selectorOpen = !selectorOpen;
|
||||||
toolMenuOpen = false;
|
toolMenuOpen = false;
|
||||||
|
settingsOpen = false;
|
||||||
if (selectorOpen) {
|
if (selectorOpen) {
|
||||||
searchQuery = '';
|
searchQuery = '';
|
||||||
|
renamingId = undefined;
|
||||||
requestAnimationFrame(() => searchInput?.focus());
|
requestAnimationFrame(() => searchInput?.focus());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -271,6 +341,7 @@
|
||||||
function toggleToolMenu() {
|
function toggleToolMenu() {
|
||||||
toolMenuOpen = !toolMenuOpen;
|
toolMenuOpen = !toolMenuOpen;
|
||||||
selectorOpen = false;
|
selectorOpen = false;
|
||||||
|
settingsOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTool(trait: string) {
|
function addTool(trait: string) {
|
||||||
|
|
@ -278,6 +349,48 @@
|
||||||
toolMenuOpen = false;
|
toolMenuOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Settings menu (theme + sign out)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
let settingsOpen = $state(false);
|
||||||
|
let themeHueBg = $state(0);
|
||||||
|
let themeHueSurface = $state(0);
|
||||||
|
let themeHueAccent = $state(240); // default indigo ≈ 240
|
||||||
|
|
||||||
|
function toggleSettings() {
|
||||||
|
settingsOpen = !settingsOpen;
|
||||||
|
selectorOpen = false;
|
||||||
|
toolMenuOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hslColor(hue: number, sat: number, light: number): string {
|
||||||
|
return `hsl(${hue}, ${sat}%, ${light}%)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme() {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty('--color-bg', hslColor(themeHueBg, themeHueBg ? 10 : 0, 4));
|
||||||
|
root.style.setProperty('--color-surface', hslColor(themeHueSurface, themeHueSurface ? 8 : 0, 12));
|
||||||
|
root.style.setProperty('--color-surface-hover', hslColor(themeHueSurface, themeHueSurface ? 6 : 0, 15));
|
||||||
|
root.style.setProperty('--color-border', hslColor(themeHueSurface, themeHueSurface ? 5 : 0, 18));
|
||||||
|
root.style.setProperty('--color-accent', hslColor(themeHueAccent, 70, 60));
|
||||||
|
root.style.setProperty('--color-accent-hover', hslColor(themeHueAccent, 70, 65));
|
||||||
|
root.style.setProperty('--color-accent-glow', `hsla(${themeHueAccent}, 70%, 60%, 0.15)`);
|
||||||
|
persistMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadThemeFromMetadata(meta: Record<string, unknown>) {
|
||||||
|
const prefs = meta.preferences as Record<string, unknown> | undefined;
|
||||||
|
const theme = prefs?.theme as Record<string, number> | undefined;
|
||||||
|
if (theme) {
|
||||||
|
themeHueBg = theme.hueBg ?? 0;
|
||||||
|
themeHueSurface = theme.hueSurface ?? 0;
|
||||||
|
themeHueAccent = theme.hueAccent ?? 240;
|
||||||
|
applyTheme();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Mobile detection
|
// Mobile detection
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -310,19 +423,24 @@
|
||||||
if (toolMenuOpen && !target.closest('.tool-menu')) {
|
if (toolMenuOpen && !target.closest('.tool-menu')) {
|
||||||
toolMenuOpen = false;
|
toolMenuOpen = false;
|
||||||
}
|
}
|
||||||
|
if (settingsOpen && !target.closest('.settings-menu')) {
|
||||||
|
settingsOpen = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
selectorOpen = false;
|
selectorOpen = false;
|
||||||
toolMenuOpen = false;
|
toolMenuOpen = false;
|
||||||
|
settingsOpen = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Trait components that have dedicated implementations */
|
/** Trait components that have dedicated implementations */
|
||||||
const knownTraits = new Set([
|
const knownTraits = new Set([
|
||||||
'editor', 'chat', 'kanban', 'podcast', 'publishing',
|
'editor', 'chat', 'kanban', 'podcast', 'publishing',
|
||||||
'rss', 'calendar', 'recording', 'transcription', 'studio', 'mixer', 'mindmap'
|
'rss', 'calendar', 'recording', 'transcription', 'studio', 'mixer', 'mindmap',
|
||||||
|
'ai', 'usage'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
@ -402,24 +520,65 @@
|
||||||
bind:this={searchInput}
|
bind:this={searchInput}
|
||||||
bind:value={searchQuery}
|
bind:value={searchQuery}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Søk samlinger..."
|
placeholder="Søk arbeidsflater..."
|
||||||
class="context-selector-search-input"
|
class="context-selector-search-input"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="context-selector-list">
|
<div class="context-selector-list">
|
||||||
{#each filteredCollections as node (node.id)}
|
{#if myCollections.length > 0}
|
||||||
|
<div class="context-selector-group-label">Mine flater</div>
|
||||||
|
{#each myCollections as node (node.id)}
|
||||||
|
<div class="context-selector-item-row">
|
||||||
|
{#if renamingId === node.id}
|
||||||
|
<input
|
||||||
|
class="context-selector-rename-input"
|
||||||
|
bind:value={renameValue}
|
||||||
|
onblur={commitRename}
|
||||||
|
onkeydown={handleRenameKeydown}
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
<button
|
<button
|
||||||
class="context-selector-item"
|
class="context-selector-item"
|
||||||
onclick={() => selectCollection(node.id)}
|
onclick={() => selectCollection(node.id)}
|
||||||
>
|
>
|
||||||
<span class="context-selector-item-title">{node.title || 'Uten tittel'}</span>
|
<span class="context-selector-item-title">{node.title || 'Uten tittel'}</span>
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
<button
|
||||||
<div class="context-selector-empty">
|
class="context-selector-rename-btn"
|
||||||
{searchQuery ? 'Ingen treff' : 'Ingen samlinger'}
|
onclick={(e) => { e.stopPropagation(); startRename(node); }}
|
||||||
|
title="Gi nytt navn"
|
||||||
|
>✏️</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if sharedCollections.length > 0}
|
||||||
|
<div class="context-selector-group-label">Delte flater</div>
|
||||||
|
{#each sharedCollections as node (node.id)}
|
||||||
|
<button
|
||||||
|
class="context-selector-item"
|
||||||
|
onclick={() => selectCollection(node.id)}
|
||||||
|
>
|
||||||
|
<span class="context-selector-item-title">{node.title || 'Uten tittel'}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if myCollections.length === 0 && sharedCollections.length === 0}
|
||||||
|
<div class="context-selector-empty">
|
||||||
|
{searchQuery ? 'Ingen treff' : 'Ingen arbeidsflater'}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="context-selector-footer">
|
||||||
|
<button
|
||||||
|
class="context-selector-new-btn"
|
||||||
|
onclick={createNewWorkspace}
|
||||||
|
disabled={isCreatingWorkspace}
|
||||||
|
>
|
||||||
|
+ Ny arbeidsflate
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -463,6 +622,44 @@
|
||||||
{:else}
|
{:else}
|
||||||
<span class="context-status" title="{connectionState.current}">●</span>
|
<span class="context-status" title="{connectionState.current}">●</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<div class="settings-menu">
|
||||||
|
<button
|
||||||
|
class="context-btn settings-trigger"
|
||||||
|
onclick={toggleSettings}
|
||||||
|
title="Innstillinger"
|
||||||
|
>
|
||||||
|
⚙
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if settingsOpen}
|
||||||
|
<div class="settings-dropdown">
|
||||||
|
<div class="settings-title">Tema</div>
|
||||||
|
<label class="settings-slider">
|
||||||
|
<span class="settings-slider-label">Bakgrunn</span>
|
||||||
|
<input type="range" min="0" max="360" bind:value={themeHueBg} oninput={applyTheme} />
|
||||||
|
</label>
|
||||||
|
<label class="settings-slider">
|
||||||
|
<span class="settings-slider-label">Overflate</span>
|
||||||
|
<input type="range" min="0" max="360" bind:value={themeHueSurface} oninput={applyTheme} />
|
||||||
|
</label>
|
||||||
|
<label class="settings-slider">
|
||||||
|
<span class="settings-slider-label">Aksent</span>
|
||||||
|
<input type="range" min="0" max="360" bind:value={themeHueAccent} oninput={applyTheme} />
|
||||||
|
</label>
|
||||||
|
<div class="settings-divider"></div>
|
||||||
|
{#if $page.data.session?.user}
|
||||||
|
<div class="settings-user">{$page.data.session.user.name}</div>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
class="settings-signout"
|
||||||
|
onclick={() => signOut()}
|
||||||
|
>
|
||||||
|
Logg ut
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -545,6 +742,12 @@
|
||||||
<MixerTrait collection={undefined} config={{}} {accessToken} />
|
<MixerTrait collection={undefined} config={{}} {accessToken} />
|
||||||
{:else if panel.trait === 'mindmap'}
|
{:else if panel.trait === 'mindmap'}
|
||||||
<MindMapTrait collection={undefined} config={{}} userId={nodeId} {accessToken} />
|
<MindMapTrait collection={undefined} config={{}} userId={nodeId} {accessToken} />
|
||||||
|
{:else if panel.trait === 'ai'}
|
||||||
|
<AiToolPanel {accessToken} userId={nodeId} />
|
||||||
|
{:else if panel.trait === 'usage'}
|
||||||
|
{#if nodeId && accessToken}
|
||||||
|
<NodeUsage nodeId={nodeId} {accessToken} />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<GenericTrait name={panel.trait} config={{}} />
|
<GenericTrait name={panel.trait} config={{}} />
|
||||||
|
|
@ -602,6 +805,12 @@
|
||||||
<MixerTrait collection={undefined} config={{}} {accessToken} />
|
<MixerTrait collection={undefined} config={{}} {accessToken} />
|
||||||
{:else if trait === 'mindmap'}
|
{:else if trait === 'mindmap'}
|
||||||
<MindMapTrait collection={undefined} config={{}} userId={nodeId} {accessToken} />
|
<MindMapTrait collection={undefined} config={{}} userId={nodeId} {accessToken} />
|
||||||
|
{:else if trait === 'ai'}
|
||||||
|
<AiToolPanel {accessToken} userId={nodeId} />
|
||||||
|
{:else if trait === 'usage'}
|
||||||
|
{#if nodeId && accessToken}
|
||||||
|
<NodeUsage nodeId={nodeId} {accessToken} />
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<GenericTrait name={trait} config={{}} />
|
<GenericTrait name={trait} config={{}} />
|
||||||
|
|
@ -1045,6 +1254,189 @@
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Context selector — groups, rename, new */
|
||||||
|
/* ================================================================= */
|
||||||
|
.context-selector-group-label {
|
||||||
|
padding: 8px 10px 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #5a5a66;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-item-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-item-row .context-selector-item {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-rename-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.1s;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-item-row:hover .context-selector-rename-btn {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-rename-btn:hover {
|
||||||
|
opacity: 1 !important;
|
||||||
|
background: #242428;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-rename-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid #6366f1 !important;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
background: #141416 !important;
|
||||||
|
color: #e8e8ec !important;
|
||||||
|
margin: 2px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-footer {
|
||||||
|
border-top: 1px solid #2a2a2e;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-new-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px dashed #2a2a2e;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #5a5a66;
|
||||||
|
transition: border-color 0.1s, color 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-new-btn:hover:not(:disabled) {
|
||||||
|
border-color: #6366f1;
|
||||||
|
color: #8a8a96;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-selector-new-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ================================================================= */
|
||||||
|
/* Settings menu */
|
||||||
|
/* ================================================================= */
|
||||||
|
.settings-menu {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-trigger {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
right: 0;
|
||||||
|
min-width: 220px;
|
||||||
|
background: #1c1c20;
|
||||||
|
background: var(--color-surface, #1c1c20);
|
||||||
|
border: 1px solid #2a2a2e;
|
||||||
|
border: 1px solid var(--color-border, #2a2a2e);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
|
||||||
|
z-index: 50;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #5a5a66;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-slider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-slider-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #8a8a96;
|
||||||
|
min-width: 65px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-slider input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: #2a2a2e !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: none;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-slider input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-accent, #6366f1);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #2a2a2e;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-user {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8a8a96;
|
||||||
|
padding: 4px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-signout {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #8a8a96;
|
||||||
|
text-align: left;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-signout:hover {
|
||||||
|
background: #242428;
|
||||||
|
color: #e8e8ec;
|
||||||
|
}
|
||||||
|
|
||||||
/* ================================================================= */
|
/* ================================================================= */
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
/* ================================================================= */
|
/* ================================================================= */
|
||||||
|
|
@ -1063,7 +1455,8 @@
|
||||||
max-width: calc(100vw - 24px);
|
max-width: calc(100vw - 24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-menu-dropdown {
|
.tool-menu-dropdown,
|
||||||
|
.settings-dropdown {
|
||||||
max-width: calc(100vw - 24px);
|
max-width: calc(100vw - 24px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue