synops/frontend/src/lib/components/ContextHeader.svelte
vegard bf744639c1 Forenklet fargevelger: én aksentfarge + lys/mørk
Erstattet 9 slidere (3 per farge × 3 farger) med 3 intuitive kontroller:
- Farge: hue-stripe for aksentfarge
- Intensitet: saturation
- Lys/mørk: brightness slider (0=svart, 100=hvit)

Systemet utleder bg, surface, border, text automatisk fra disse.
Canvas-bakgrunn styres nå av --color-bg (var ikke det før).
Presets med emoji-ikoner. Rosa og lys preset lagt til.
Bakoverkompatibel med alle tidligere tema-formater.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 06:50:16 +00:00

847 lines
27 KiB
Svelte

<script lang="ts">
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { signOut } from '@auth/sveltekit/client';
import { connectionState, nodeStore, edgeStore, nodeAccessStore, nodeVisibility } from '$lib/realtime';
import type { Node } from '$lib/realtime';
import { getRankedNodeIds, recordVisit } from '$lib/workspace/recency.js';
import { TRAIT_PANEL_INFO } from '$lib/workspace/types.js';
import { updateNode, createNode, createEdge, deleteNode } from '$lib/api';
import {
type ThemeConfig,
DEFAULT_THEME,
THEME_PRESETS,
applyTheme,
presetAccentCSS,
} from '$lib/workspace/theme.js';
interface Props {
/** Collection ID (undefined = personal workspace) */
collectionId?: string;
/** Collection node (if loaded) */
collectionNode?: Node | undefined;
/** Current user's node ID */
userId: string | undefined;
/** Access token for API calls */
accessToken?: string | undefined;
/** Whether WebSocket is connected */
connected: boolean;
/** Active trait names on this collection */
traitNames?: string[];
/** Callback to toggle trait admin panel */
onToggleTraitAdmin?: () => void;
/** Whether trait admin is currently shown */
showTraitAdmin?: boolean;
/** Callback to add a panel/trait to the workspace */
onAddPanel: (trait: string) => void;
/** Traits already visible on the workspace */
activeTraits: string[];
/** Current theme config */
theme?: ThemeConfig;
/** Callback when theme changes */
onThemeChange?: (theme: ThemeConfig) => void;
}
let {
collectionId,
collectionNode,
userId,
accessToken,
connected,
traitNames = [],
onToggleTraitAdmin,
showTraitAdmin = false,
onAddPanel,
activeTraits,
theme = DEFAULT_THEME,
onThemeChange,
}: Props = $props();
const isPersonalWorkspace = $derived(!collectionId);
// =========================================================================
// Context selector
// =========================================================================
let selectorOpen = $state(false);
let searchQuery = $state('');
let searchInput = $state<HTMLInputElement | undefined>(undefined);
$effect(() => {
if (collectionId) recordVisit(collectionId);
});
const collectionNodes = $derived.by((): Node[] => {
if (!connected || !userId) return [];
const accessibleIds = new Set<string>();
for (const edge of edgeStore.bySource(userId)) {
if (!edge.system) accessibleIds.add(edge.targetId);
}
for (const id of nodeAccessStore.objectsForSubject(userId)) {
accessibleIds.add(id);
}
const collections: Node[] = [];
for (const id of accessibleIds) {
const node = nodeStore.get(id);
if (node && node.nodeKind === 'collection' && nodeVisibility(node, userId) !== 'hidden') {
collections.push(node);
}
}
const ranked = getRankedNodeIds();
const rankMap = new Map(ranked.map((id, i) => [id, i]));
collections.sort((a, b) => {
const ra = rankMap.get(a.id) ?? 999;
const rb = rankMap.get(b.id) ?? 999;
if (ra !== rb) return ra - rb;
return (a.title ?? '').localeCompare(b.title ?? '');
});
return collections;
});
const filteredCollections = $derived.by(() => {
if (!searchQuery.trim()) return collectionNodes;
const q = searchQuery.toLowerCase().trim();
return collectionNodes.filter(n => (n.title ?? '').toLowerCase().includes(q));
});
const myCollections = $derived(filteredCollections.filter(n => n.createdBy === userId));
const sharedCollections = $derived(filteredCollections.filter(n => n.createdBy !== userId));
// --- Rename ---
let renamingId = $state<string | undefined>(undefined);
let renameValue = $state('');
function startRename(node: Node) {
renamingId = node.id;
renameValue = node.title || '';
requestAnimationFrame(() => {
const input = document.querySelector('.context-selector-rename-input') as HTMLInputElement;
input?.focus();
input?.select();
});
}
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; }
}
// --- Create new ---
let isCreatingWorkspace = $state(false);
async function createNewWorkspace() {
if (!accessToken || !userId || isCreatingWorkspace) return;
isCreatingWorkspace = true;
selectorOpen = false;
try {
const { node_id } = await createNode(accessToken, {
node_kind: 'collection',
title: 'Ny arbeidsflate',
visibility: 'hidden',
metadata: {
traits: { chat: {}, editor: {}, kanban: {}, calendar: {} },
},
});
await createEdge(accessToken, {
source_id: userId,
target_id: node_id,
edge_type: 'owner',
});
goto(`/collection/${node_id}`);
} catch (e) {
console.error('Feil ved oppretting av arbeidsflate:', e);
} finally {
isCreatingWorkspace = false;
}
}
// --- Delete ---
let deletingId = $state<string | undefined>(undefined);
let deleteChildCount = $state(0);
function startDelete(node: Node) {
deletingId = node.id;
// Count child nodes
let count = 0;
for (const edge of edgeStore.byTarget(node.id)) {
if (edge.edgeType === 'belongs_to') count++;
}
deleteChildCount = count;
}
async function confirmDelete() {
if (!deletingId || !accessToken) return;
const id = deletingId;
deletingId = undefined;
selectorOpen = false;
try {
await deleteNode(accessToken, id);
if (collectionId === id) goto('/');
} catch (e) {
console.error('Feil ved sletting:', e);
}
}
function toggleSelector() {
selectorOpen = !selectorOpen;
toolMenuOpen = false;
settingsOpen = false;
if (selectorOpen) {
searchQuery = '';
renamingId = undefined;
deletingId = undefined;
requestAnimationFrame(() => searchInput?.focus());
}
}
function selectCollection(id: string) {
selectorOpen = false;
if (id !== collectionId) goto(`/collection/${id}`);
}
// =========================================================================
// Tool menu
// =========================================================================
let toolMenuOpen = $state(false);
const availableTools = $derived.by(() => {
return Object.entries(TRAIT_PANEL_INFO).map(([key, info]) => ({
key,
...info,
active: activeTraits.includes(key),
}));
});
function toggleToolMenu() {
toolMenuOpen = !toolMenuOpen;
selectorOpen = false;
settingsOpen = false;
}
function addTool(trait: string) {
onAddPanel(trait);
toolMenuOpen = false;
}
// =========================================================================
// Settings menu (theme + sign out)
// =========================================================================
let settingsOpen = $state(false);
function toggleSettings() {
settingsOpen = !settingsOpen;
selectorOpen = false;
toolMenuOpen = false;
}
function updateTheme(partial: Partial<ThemeConfig>) {
const newTheme = { ...theme, ...partial };
applyTheme(newTheme);
onThemeChange?.(newTheme);
}
function applyPreset(preset: ThemeConfig) {
applyTheme(preset);
onThemeChange?.(preset);
}
// =========================================================================
// Click outside
// =========================================================================
function handleClickOutside(e: MouseEvent) {
const target = e.target as HTMLElement;
if (selectorOpen && !target.closest('.context-selector')) selectorOpen = false;
if (toolMenuOpen && !target.closest('.tool-menu')) toolMenuOpen = false;
if (settingsOpen && !target.closest('.settings-menu')) settingsOpen = false;
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
selectorOpen = false;
toolMenuOpen = false;
settingsOpen = false;
}
}
</script>
<svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
<header class="context-header">
<div class="context-header-inner">
<!-- Left: Back + Context selector -->
<div class="context-header-left">
{#if !isPersonalWorkspace}
<a href="/" class="context-back" title="Tilbake til arbeidsflaten">&larr;</a>
{/if}
<div class="context-selector">
<button
class="context-selector-trigger"
onclick={toggleSelector}
title="Bytt kontekst"
>
<span class="context-selector-title">
{isPersonalWorkspace ? 'Hjem' : (collectionNode?.title || 'Arbeidsflate')}
</span>
<span class="context-selector-chevron" class:open={selectorOpen}>&#9662;</span>
</button>
{#if selectorOpen}
<div class="context-selector-dropdown">
<div class="context-selector-search">
<input
bind:this={searchInput}
bind:value={searchQuery}
type="text"
placeholder="Søk arbeidsflater..."
class="context-selector-search-input"
onclick={(e) => e.stopPropagation()}
/>
</div>
<div class="context-selector-list">
<!-- Hjem (alltid øverst når ikke søker) -->
{#if !searchQuery.trim()}
<button
class="context-selector-item"
class:current={isPersonalWorkspace}
onclick={() => { selectorOpen = false; goto('/'); }}
>
<span class="context-selector-item-title">Hjem</span>
{#if isPersonalWorkspace}
<span class="context-selector-item-check">&#10003;</span>
{/if}
</button>
{/if}
<!-- Mine flater -->
{#if myCollections.length > 0}
<div class="context-selector-group-label">Mine flater</div>
{#each myCollections as node (node.id)}
{#if deletingId === node.id}
<div class="context-selector-delete-confirm">
<p class="delete-confirm-text">
Slett «{node.title || 'Uten tittel'}»?
{#if deleteChildCount > 0}
<br/><span class="delete-confirm-count">{deleteChildCount} noder tilhører denne flaten.</span>
{/if}
</p>
<div class="delete-confirm-actions">
<button class="delete-confirm-btn delete-confirm-cancel" onclick={(e) => { e.stopPropagation(); deletingId = undefined; }}>Avbryt</button>
<button class="delete-confirm-btn delete-confirm-yes" onclick={(e) => { e.stopPropagation(); confirmDelete(); }}>Slett</button>
</div>
</div>
{:else}
<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
class="context-selector-item"
class:current={node.id === collectionId}
onclick={() => selectCollection(node.id)}
>
<span class="context-selector-item-title">{node.title || 'Uten tittel'}</span>
{#if node.id === collectionId}
<span class="context-selector-item-check">&#10003;</span>
{/if}
</button>
<button
class="context-selector-action-btn"
onclick={(e) => { e.stopPropagation(); startRename(node); }}
title="Gi nytt navn"
>✏️</button>
<button
class="context-selector-action-btn context-selector-delete-btn"
onclick={(e) => { e.stopPropagation(); startDelete(node); }}
title="Slett"
>🗑️</button>
{/if}
</div>
{/if}
{/each}
{/if}
<!-- Delte flater -->
{#if sharedCollections.length > 0}
<div class="context-selector-group-label">Delte flater</div>
{#each sharedCollections as node (node.id)}
<button
class="context-selector-item"
class:current={node.id === collectionId}
onclick={() => selectCollection(node.id)}
>
<span class="context-selector-item-title">{node.title || 'Uten tittel'}</span>
{#if node.id === collectionId}
<span class="context-selector-item-check">&#10003;</span>
{/if}
</button>
{/each}
{/if}
{#if myCollections.length === 0 && sharedCollections.length === 0 && searchQuery.trim()}
<div class="context-selector-empty">Ingen treff</div>
{/if}
</div>
<div class="context-selector-footer">
<button
class="context-selector-new-btn"
onclick={createNewWorkspace}
disabled={isCreatingWorkspace}
>
+ Ny arbeidsflate
</button>
</div>
</div>
{/if}
</div>
</div>
<!-- Right: Tool menu + status + settings -->
<div class="context-header-right">
{#if traitNames.length > 0 && onToggleTraitAdmin}
<button
onclick={onToggleTraitAdmin}
class="context-btn {showTraitAdmin ? 'context-btn-active' : ''}"
title="Administrer egenskaper"
>
Egenskaper
</button>
{/if}
<div class="tool-menu">
<button
class="context-btn tool-menu-trigger"
onclick={toggleToolMenu}
title="Legg til verktøy-panel"
>
+ Verktøy
</button>
{#if toolMenuOpen}
<div class="tool-menu-dropdown">
<div class="tool-menu-title">Legg til panel</div>
{#each availableTools as tool (tool.key)}
<button
class="tool-menu-item"
class:tool-menu-item-active={tool.active}
onclick={() => addTool(tool.key)}
disabled={tool.active}
title={tool.active ? 'Allerede på flaten' : `Legg til ${tool.title}`}
>
<span class="tool-menu-item-icon">{tool.icon}</span>
<span class="tool-menu-item-label">{tool.title}</span>
{#if tool.active}
<span class="tool-menu-item-badge">aktiv</span>
{/if}
</button>
{/each}
</div>
{/if}
</div>
{#if connected}
<span class="context-status context-status-ok" title="Tilkoblet sanntid">&#9679;</span>
{:else}
<span class="context-status" title="{connectionState.current}">&#9679;</span>
{/if}
<div class="settings-menu">
<button
class="context-btn settings-trigger"
onclick={toggleSettings}
title="Innstillinger"
>&#9881;</button>
{#if settingsOpen}
<div class="settings-dropdown">
<!-- Presets -->
<div class="settings-title">Tema</div>
<div class="settings-presets">
{#each THEME_PRESETS as preset (preset.name)}
<button
class="settings-preset"
title={preset.name}
onclick={() => applyPreset(preset.theme)}
>
<span class="settings-preset-swatch"
style:background={presetAccentCSS(preset.theme)}
>{preset.emoji}</span>
</button>
{/each}
</div>
<!-- Accent color -->
<div class="settings-color-group">
<div class="settings-color-label">Farge</div>
<input type="range" class="settings-hue-slider" min="0" max="360"
value={theme.accentHue}
oninput={(e) => updateTheme({ accentHue: +e.currentTarget.value })} />
</div>
<div class="settings-color-group">
<div class="settings-color-label">Intensitet</div>
<input type="range" class="settings-sat-slider" min="0" max="100"
value={theme.accentSat}
oninput={(e) => updateTheme({ accentSat: +e.currentTarget.value })} />
</div>
<div class="settings-color-group">
<div class="settings-color-label">Lys / mørk</div>
<input type="range" class="settings-light-slider" min="0" max="100"
value={theme.brightness}
oninput={(e) => updateTheme({ brightness: +e.currentTarget.value })} />
</div>
<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>
</header>
<style>
.context-header {
border-bottom: 1px solid var(--color-border, #2a2a2e);
background: var(--color-surface, #1c1c20);
flex-shrink: 0;
z-index: 30;
position: relative;
}
.context-header-inner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 16px;
max-width: 100%;
min-height: 44px;
}
.context-header-left {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
}
.context-header-right {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
/* Back button */
.context-back {
font-size: 16px;
color: var(--color-text-dim, #5a5a66);
text-decoration: none;
flex-shrink: 0;
padding: 4px;
line-height: 1;
}
.context-back:hover { color: var(--color-text-muted, #8a8a96); }
/* Context selector */
.context-selector { position: relative; min-width: 0; }
.context-selector-trigger {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border: 1px solid transparent;
border-radius: 6px;
background: transparent;
cursor: pointer;
font-size: 15px;
font-weight: 600;
color: var(--color-text, #e8e8ec);
max-width: 300px;
transition: background 0.12s, border-color 0.12s;
}
.context-selector-trigger:hover {
background: var(--color-surface-hover, #242428);
border-color: var(--color-border-hover, #3a3a3e);
}
.context-selector-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.context-selector-chevron { font-size: 10px; color: var(--color-text-dim, #5a5a66); transition: transform 0.15s; flex-shrink: 0; }
.context-selector-chevron.open { transform: rotate(180deg); }
/* Dropdown */
.context-selector-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
min-width: 280px;
max-width: 380px;
background: var(--color-surface, #1c1c20);
border: 1px solid var(--color-border, #2a2a2e);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
z-index: 50;
overflow: hidden;
}
.context-selector-search { padding: 8px; border-bottom: 1px solid var(--color-border, #2a2a2e); }
.context-selector-search-input {
width: 100%;
padding: 6px 10px;
border: 1px solid var(--color-border, #2a2a2e) !important;
border-radius: 6px;
font-size: 13px;
outline: none;
background: var(--color-bg, #141416) !important;
color: var(--color-text, #e8e8ec) !important;
}
.context-selector-search-input:focus { border-color: var(--color-accent, #6366f1) !important; }
.context-selector-list { max-height: 320px; overflow-y: auto; padding: 4px; }
.context-selector-item {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 8px 10px;
border: none;
border-radius: 6px;
background: transparent;
cursor: pointer;
font-size: 13px;
color: var(--color-text-muted, #8a8a96);
text-align: left;
transition: background 0.1s;
}
.context-selector-item:hover { background: var(--color-surface-hover, #242428); }
.context-selector-item.current { background: rgba(99, 102, 241, 0.1); color: var(--color-accent, #6366f1); font-weight: 500; }
.context-selector-item-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.context-selector-item-check { color: var(--color-accent, #6366f1); font-size: 12px; flex-shrink: 0; margin-left: 8px; }
.context-selector-empty { padding: 16px; text-align: center; font-size: 12px; color: var(--color-text-dim, #5a5a66); }
/* Groups */
.context-selector-group-label {
padding: 8px 10px 4px;
font-size: 10px;
font-weight: 600;
color: var(--color-text-dim, #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; }
/* Action buttons (rename, delete) */
.context-selector-action-btn {
flex-shrink: 0;
padding: 4px 4px;
border: none;
background: transparent;
cursor: pointer;
font-size: 11px;
opacity: 0;
transition: opacity 0.1s;
border-radius: 4px;
}
.context-selector-item-row:hover .context-selector-action-btn { opacity: 0.5; }
.context-selector-action-btn:hover { opacity: 1 !important; background: var(--color-surface-hover, #242428); }
.context-selector-delete-btn:hover { background: rgba(239, 68, 68, 0.15); }
/* Rename input */
.context-selector-rename-input {
flex: 1;
padding: 6px 10px;
border: 1px solid var(--color-accent, #6366f1) !important;
border-radius: 6px;
font-size: 13px;
outline: none;
background: var(--color-bg, #141416) !important;
color: var(--color-text, #e8e8ec) !important;
margin: 2px 4px;
}
/* Delete confirmation */
.context-selector-delete-confirm {
padding: 8px 10px;
border-radius: 6px;
background: rgba(239, 68, 68, 0.08);
margin: 2px 0;
}
.delete-confirm-text { font-size: 12px; color: var(--color-text-muted, #8a8a96); line-height: 1.4; }
.delete-confirm-count { color: var(--color-warning, #f59e0b); }
.delete-confirm-actions { display: flex; gap: 6px; margin-top: 6px; }
.delete-confirm-btn {
padding: 4px 10px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.delete-confirm-cancel { background: var(--color-surface-hover, #242428); color: var(--color-text-muted, #8a8a96); }
.delete-confirm-cancel:hover { background: var(--color-border, #2a2a2e); }
.delete-confirm-yes { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.delete-confirm-yes:hover { background: rgba(239, 68, 68, 0.35); }
/* Footer */
.context-selector-footer { border-top: 1px solid var(--color-border, #2a2a2e); padding: 6px; }
.context-selector-new-btn {
width: 100%;
padding: 8px 10px;
border: 1px dashed var(--color-border, #2a2a2e);
border-radius: 6px;
background: transparent;
cursor: pointer;
font-size: 13px;
color: var(--color-text-dim, #5a5a66);
transition: border-color 0.1s, color 0.1s;
}
.context-selector-new-btn:hover:not(:disabled) { border-color: var(--color-accent, #6366f1); color: var(--color-text-muted, #8a8a96); }
.context-selector-new-btn:disabled { opacity: 0.5; cursor: default; }
/* Tool Menu */
.tool-menu { position: relative; }
.tool-menu-trigger { font-weight: 500; }
.tool-menu-dropdown {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 200px;
background: var(--color-surface, #1c1c20);
border: 1px solid var(--color-border, #2a2a2e);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
z-index: 50;
overflow: hidden;
padding: 4px;
}
.tool-menu-title { padding: 6px 10px 4px; font-size: 11px; font-weight: 600; color: var(--color-text-dim, #5a5a66); text-transform: uppercase; letter-spacing: 0.05em; }
.tool-menu-item {
display: flex; align-items: center; gap: 8px; width: 100%; padding: 7px 10px;
border: none; border-radius: 6px; background: transparent; cursor: pointer;
font-size: 13px; color: var(--color-text-muted, #8a8a96); text-align: left; transition: background 0.1s;
}
.tool-menu-item:hover:not(:disabled) { background: var(--color-surface-hover, #242428); }
.tool-menu-item:disabled { cursor: default; opacity: 0.5; }
.tool-menu-item-active { color: var(--color-text-dim, #5a5a66); }
.tool-menu-item-icon { font-size: 15px; flex-shrink: 0; }
.tool-menu-item-label { flex: 1; }
.tool-menu-item-badge { font-size: 10px; color: var(--color-text-dim, #5a5a66); background: var(--color-surface-hover, #242428); padding: 1px 6px; border-radius: 4px; }
/* Status */
.context-status { font-size: 8px; color: #d1d5db; }
.context-status-ok { color: #16a34a; }
/* Shared button */
.context-btn {
padding: 4px 10px; border: none; border-radius: 6px; font-size: 12px; font-weight: 500;
cursor: pointer; background: var(--color-surface-hover, #242428); color: var(--color-text-muted, #8a8a96); transition: background 0.12s;
}
.context-btn:hover { background: var(--color-border, #3a3a3e); color: var(--color-text, #e8e8ec); }
.context-btn-active { background: var(--color-accent, #6366f1); color: white; }
.context-btn-active:hover { background: var(--color-accent-hover, #7577f5); }
/* Settings menu */
.settings-menu { position: relative; }
.settings-trigger { font-size: 14px; }
.settings-dropdown {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 260px;
background: var(--color-surface, #1c1c20);
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: var(--color-text-dim, #5a5a66); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
/* Presets */
.settings-presets { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 10px; }
.settings-preset {
padding: 2px; border: 2px solid transparent; border-radius: 8px; background: transparent;
cursor: pointer; transition: border-color 0.15s;
}
.settings-preset:hover { border-color: var(--color-accent, #6366f1); }
.settings-preset-swatch { width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; }
/* Per-color controls */
.settings-color-group { margin-bottom: 8px; }
.settings-color-label { font-size: 11px; color: var(--color-text-dim, #5a5a66); margin-bottom: 3px; }
.settings-hue-slider, .settings-sat-slider, .settings-light-slider {
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
border: none !important;
border-radius: 3px;
outline: none;
padding: 0 !important;
cursor: pointer;
}
.settings-hue-slider {
background: linear-gradient(to right,
hsl(0,80%,50%), hsl(60,80%,50%), hsl(120,80%,50%),
hsl(180,80%,50%), hsl(240,80%,50%), hsl(300,80%,50%), hsl(360,80%,50%)
) !important;
}
.settings-sat-slider {
background: linear-gradient(to right,
hsl(0,0%,40%), hsl(0,100%,50%)
) !important;
}
.settings-light-slider {
background: linear-gradient(to right,
hsl(0,0%,0%), hsl(0,0%,50%), hsl(0,0%,100%)
) !important;
}
.settings-hue-slider::-webkit-slider-thumb,
.settings-sat-slider::-webkit-slider-thumb,
.settings-light-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px; height: 14px; border-radius: 50%;
background: white; border: 2px solid rgba(0,0,0,0.3); cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.settings-divider { height: 1px; background: var(--color-border, #2a2a2e); margin: 10px 0; }
.settings-user { font-size: 13px; color: var(--color-text-muted, #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: var(--color-text-muted, #8a8a96); text-align: left; transition: background 0.1s;
}
.settings-signout:hover { background: var(--color-surface-hover, #242428); color: var(--color-text, #e8e8ec); }
/* Responsive */
@media (max-width: 768px) {
.context-header-inner { padding: 6px 12px; }
.context-selector-trigger { font-size: 14px; max-width: 200px; }
.context-selector-dropdown { min-width: 240px; max-width: calc(100vw - 24px); }
.tool-menu-dropdown, .settings-dropdown { max-width: calc(100vw - 24px); }
.context-header-right { gap: 6px; }
}
</style>