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>
847 lines
27 KiB
Svelte
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">←</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}>▾</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">✓</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">✓</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">✓</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">●</span>
|
|
{:else}
|
|
<span class="context-status" title="{connectionState.current}">●</span>
|
|
{/if}
|
|
|
|
<div class="settings-menu">
|
|
<button
|
|
class="context-btn settings-trigger"
|
|
onclick={toggleSettings}
|
|
title="Innstillinger"
|
|
>⚙</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>
|