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:
vegard 2026-03-19 05:11:09 +00:00
parent a1a1b8c460
commit d82fab25df
4 changed files with 430 additions and 30 deletions

View file

@ -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

View file

@ -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; }

View file

@ -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 */

View file

@ -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}">&#9679;</span> <span class="context-status" title="{connectionState.current}">&#9679;</span>
{/if} {/if}
<div class="settings-menu">
<button
class="context-btn settings-trigger"
onclick={toggleSettings}
title="Innstillinger"
>
&#9881;
</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);
} }