feat(electrobun): redesign layout — numbered groups, right sidebar, notification drawer
- Fix group click locking UI (fire-and-forget RPC) - Group icons: numbered circles with red badge for new items - AGOR title: larger (900 weight, 1.25rem) - Removed top title bar - Right sidebar: vertical window controls + notification bell - NotifDrawer.svelte: slide-in notification history
This commit is contained in:
parent
a020f59cb4
commit
d95cf122f0
5 changed files with 435 additions and 295 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,8 +4,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Svelte App</title>
|
<title>Svelte App</title>
|
||||||
<script type="module" crossorigin src="/assets/index-CgAt0V08.js"></script>
|
<script type="module" crossorigin src="/assets/index-CcWCxrMp.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-rAZ5LabM.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-B2TjuJxL.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
import SettingsDrawer from './SettingsDrawer.svelte';
|
import SettingsDrawer from './SettingsDrawer.svelte';
|
||||||
import CommandPalette from './CommandPalette.svelte';
|
import CommandPalette from './CommandPalette.svelte';
|
||||||
import ToastContainer from './ToastContainer.svelte';
|
import ToastContainer from './ToastContainer.svelte';
|
||||||
|
import NotifDrawer, { type Notification } from './NotifDrawer.svelte';
|
||||||
import { themeStore } from './theme-store.svelte.ts';
|
import { themeStore } from './theme-store.svelte.ts';
|
||||||
import { fontStore } from './font-store.svelte.ts';
|
import { fontStore } from './font-store.svelte.ts';
|
||||||
import { keybindingStore } from './keybinding-store.svelte.ts';
|
import { keybindingStore } from './keybinding-store.svelte.ts';
|
||||||
|
|
@ -45,6 +46,7 @@
|
||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
position: number;
|
position: number;
|
||||||
|
hasNew?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Demo data ──────────────────────────────────────────────────
|
// ── Demo data ──────────────────────────────────────────────────
|
||||||
|
|
@ -97,7 +99,7 @@
|
||||||
// ── Groups state ───────────────────────────────────────────────
|
// ── Groups state ───────────────────────────────────────────────
|
||||||
let groups = $state<Group[]>([
|
let groups = $state<Group[]>([
|
||||||
{ id: 'dev', name: 'Development', icon: '🔧', position: 0 },
|
{ id: 'dev', name: 'Development', icon: '🔧', position: 0 },
|
||||||
{ id: 'test', name: 'Testing', icon: '🧪', position: 1 },
|
{ id: 'test', name: 'Testing', icon: '🧪', position: 1, hasNew: true },
|
||||||
{ id: 'ops', name: 'DevOps', icon: '🚀', position: 2 },
|
{ id: 'ops', name: 'DevOps', icon: '🚀', position: 2 },
|
||||||
{ id: 'research', name: 'Research', icon: '🔬', position: 3 },
|
{ id: 'research', name: 'Research', icon: '🔬', position: 3 },
|
||||||
]);
|
]);
|
||||||
|
|
@ -110,16 +112,8 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
// Group projects into: top-level cards + clone groups
|
// Group projects into: top-level cards + clone groups
|
||||||
// A "clone group" is a parent + its clones rendered side by side
|
interface ProjectRow { type: 'standalone'; project: Project; }
|
||||||
interface ProjectRow {
|
interface CloneGroupRow { type: 'clone-group'; parent: Project; clones: Project[]; }
|
||||||
type: 'standalone';
|
|
||||||
project: Project;
|
|
||||||
}
|
|
||||||
interface CloneGroupRow {
|
|
||||||
type: 'clone-group';
|
|
||||||
parent: Project;
|
|
||||||
clones: Project[];
|
|
||||||
}
|
|
||||||
type GridRow = ProjectRow | CloneGroupRow;
|
type GridRow = ProjectRow | CloneGroupRow;
|
||||||
|
|
||||||
let gridRows = $derived((): GridRow[] => {
|
let gridRows = $derived((): GridRow[] => {
|
||||||
|
|
@ -127,11 +121,9 @@
|
||||||
const cloneParentIds = new Set(
|
const cloneParentIds = new Set(
|
||||||
filteredProjects.filter(p => p.cloneOf).map(p => p.cloneOf!)
|
filteredProjects.filter(p => p.cloneOf).map(p => p.cloneOf!)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const p of filteredProjects) {
|
for (const p of filteredProjects) {
|
||||||
if (p.cloneOf) continue; // Skip clones — they go into clone groups
|
if (p.cloneOf) continue;
|
||||||
if (cloneParentIds.has(p.id)) {
|
if (cloneParentIds.has(p.id)) {
|
||||||
// This parent has clones
|
|
||||||
const clones = filteredProjects
|
const clones = filteredProjects
|
||||||
.filter(c => c.cloneOf === p.id)
|
.filter(c => c.cloneOf === p.id)
|
||||||
.sort((a, b) => (a.cloneIndex ?? 0) - (b.cloneIndex ?? 0));
|
.sort((a, b) => (a.cloneIndex ?? 0) - (b.cloneIndex ?? 0));
|
||||||
|
|
@ -143,83 +135,65 @@
|
||||||
return standalone;
|
return standalone;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Clone count helpers ────────────────────────────────────────
|
// ── Clone helpers ──────────────────────────────────────────────
|
||||||
function cloneCountForProject(projectId: string): number {
|
function cloneCountForProject(projectId: string): number {
|
||||||
return PROJECTS.filter(p => p.cloneOf === projectId).length;
|
return PROJECTS.filter(p => p.cloneOf === projectId).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClone(projectId: string) {
|
function handleClone(projectId: string) {
|
||||||
// In a real app: open the clone dialog and call RPC.
|
|
||||||
// Here we demonstrate the flow by calling the RPC directly with a demo branch.
|
|
||||||
const source = PROJECTS.find(p => p.id === projectId);
|
const source = PROJECTS.find(p => p.id === projectId);
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
const branchName = `feature/clone-${Date.now()}`;
|
const branchName = `feature/clone-${Date.now()}`;
|
||||||
appRpc.request["project.clone"]({ projectId, branchName }).then((result) => {
|
appRpc.request["project.clone"]({ projectId, branchName }).then((result) => {
|
||||||
if (result.ok && result.project) {
|
if (result.ok && result.project) {
|
||||||
const cloneConfig = JSON.parse(result.project.config) as Project;
|
const cloneConfig = JSON.parse(result.project.config) as Project;
|
||||||
PROJECTS = [...PROJECTS, {
|
PROJECTS = [...PROJECTS, { ...cloneConfig, status: 'idle', costUsd: 0, tokens: 0, messages: [] }];
|
||||||
...cloneConfig,
|
|
||||||
status: 'idle',
|
|
||||||
costUsd: 0,
|
|
||||||
tokens: 0,
|
|
||||||
messages: [],
|
|
||||||
}];
|
|
||||||
} else {
|
} else {
|
||||||
console.error('[clone]', result.error);
|
console.error('[clone]', result.error);
|
||||||
}
|
}
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Init on mount ──────────────────────────────────────────────
|
|
||||||
onMount(() => {
|
|
||||||
themeStore.initTheme(appRpc).catch(console.error);
|
|
||||||
fontStore.initFonts(appRpc).catch(console.error);
|
|
||||||
keybindingStore.init(appRpc).catch(console.error);
|
|
||||||
|
|
||||||
// Load groups from DB
|
|
||||||
appRpc.request["groups.list"]({}).then(({ groups: dbGroups }) => {
|
|
||||||
if (dbGroups.length > 0) groups = dbGroups;
|
|
||||||
}).catch(console.error);
|
|
||||||
|
|
||||||
// Restore active group from settings
|
|
||||||
appRpc.request["settings.get"]({ key: 'active_group' }).then(({ value }) => {
|
|
||||||
if (value && groups.some(g => g.id === value)) activeGroupId = value;
|
|
||||||
}).catch(console.error);
|
|
||||||
|
|
||||||
// Register keybinding command handlers
|
|
||||||
keybindingStore.on('palette', () => { paletteOpen = !paletteOpen; });
|
|
||||||
keybindingStore.on('settings', () => { settingsOpen = !settingsOpen; });
|
|
||||||
keybindingStore.on('group1', () => setActiveGroup(groups[0]?.id));
|
|
||||||
keybindingStore.on('group2', () => setActiveGroup(groups[1]?.id));
|
|
||||||
keybindingStore.on('group3', () => setActiveGroup(groups[2]?.id));
|
|
||||||
keybindingStore.on('group4', () => setActiveGroup(groups[3]?.id));
|
|
||||||
keybindingStore.on('minimize', () => appRpc.request["window.minimize"]({}).catch(console.error));
|
|
||||||
|
|
||||||
// Install listener (returns cleanup fn, but onMount handles lifecycle)
|
|
||||||
const cleanup = keybindingStore.installListener();
|
|
||||||
return cleanup;
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Reactive state ─────────────────────────────────────────────
|
// ── Reactive state ─────────────────────────────────────────────
|
||||||
let settingsOpen = $state(false);
|
let settingsOpen = $state(false);
|
||||||
let paletteOpen = $state(false);
|
let paletteOpen = $state(false);
|
||||||
let notifCount = $state(2);
|
let drawerOpen = $state(false);
|
||||||
let sessionStart = $state(Date.now());
|
let sessionStart = $state(Date.now());
|
||||||
|
|
||||||
|
let notifications = $state<Notification[]>([
|
||||||
|
{ id: 1, message: 'Agent completed: wake scheduler implemented', type: 'success', time: '2m ago' },
|
||||||
|
{ id: 2, message: 'Context pressure: 78% on agent-orchestrator', type: 'warning', time: '5m ago' },
|
||||||
|
{ id: 3, message: 'PTY daemon connected', type: 'info', time: '12m ago' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
let notifCount = $derived(notifications.length);
|
||||||
|
|
||||||
|
function clearNotifications() {
|
||||||
|
notifications = [];
|
||||||
|
drawerOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── setActiveGroup: fire-and-forget RPC ───────────────────────
|
||||||
function setActiveGroup(id: string | undefined) {
|
function setActiveGroup(id: string | undefined) {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
activeGroupId = id;
|
activeGroupId = id;
|
||||||
appRpc.request["settings.set"]({ key: 'active_group', value: id }).catch(console.error);
|
// Fire and forget — don't let RPC failures block UI
|
||||||
|
appRpc?.request["settings.set"]?.({ key: 'active_group', value: id })?.catch?.(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blink state
|
// ── Window controls ────────────────────────────────────────────
|
||||||
|
function handleClose() { appRpc.request["window.close"]({}).catch(console.error); }
|
||||||
|
function handleMaximize() { appRpc.request["window.maximize"]({}).catch(console.error); }
|
||||||
|
function handleMinimize() { appRpc.request["window.minimize"]({}).catch(console.error); }
|
||||||
|
|
||||||
|
// ── Blink ──────────────────────────────────────────────────────
|
||||||
let blinkVisible = $state(true);
|
let blinkVisible = $state(true);
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const id = setInterval(() => { blinkVisible = !blinkVisible; }, 500);
|
const id = setInterval(() => { blinkVisible = !blinkVisible; }, 500);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Session duration
|
// ── Session duration ───────────────────────────────────────────
|
||||||
let sessionDuration = $state('0m');
|
let sessionDuration = $state('0m');
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
function update() {
|
function update() {
|
||||||
|
|
@ -231,7 +205,7 @@
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Window frame persistence (debounced 500ms)
|
// ── Window frame persistence (debounced 500ms) ─────────────────
|
||||||
let frameSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
let frameSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
function saveWindowFrame() {
|
function saveWindowFrame() {
|
||||||
if (frameSaveTimer) clearTimeout(frameSaveTimer);
|
if (frameSaveTimer) clearTimeout(frameSaveTimer);
|
||||||
|
|
@ -246,63 +220,77 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Status bar aggregates ──────────────────────────────────────
|
// ── Status bar aggregates ──────────────────────────────────────
|
||||||
let runningCount = $derived(PROJECTS.filter(p => p.status === 'running').length);
|
let runningCount = $derived(PROJECTS.filter(p => p.status === 'running').length);
|
||||||
let idleCount = $derived(PROJECTS.filter(p => p.status === 'idle').length);
|
let idleCount = $derived(PROJECTS.filter(p => p.status === 'idle').length);
|
||||||
let stalledCount = $derived(PROJECTS.filter(p => p.status === 'stalled').length);
|
let stalledCount = $derived(PROJECTS.filter(p => p.status === 'stalled').length);
|
||||||
let totalCost = $derived(PROJECTS.reduce((s, p) => s + p.costUsd, 0));
|
let totalCost = $derived(PROJECTS.reduce((s, p) => s + p.costUsd, 0));
|
||||||
let totalTokens = $derived(PROJECTS.reduce((s, p) => s + p.tokens, 0));
|
let totalTokens = $derived(PROJECTS.reduce((s, p) => s + p.tokens, 0));
|
||||||
let attentionItems = $derived(
|
let attentionItems = $derived(PROJECTS.filter(p => p.status === 'stalled' || (p.contextPct ?? 0) >= 75));
|
||||||
PROJECTS.filter(p => p.status === 'stalled' || (p.contextPct ?? 0) >= 75)
|
|
||||||
);
|
|
||||||
|
|
||||||
function fmtTokens(n: number): string {
|
function fmtTokens(n: number): string { return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); }
|
||||||
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
function fmtCost(n: number): string { return `$${n.toFixed(3)}`; }
|
||||||
}
|
|
||||||
|
|
||||||
function fmtCost(n: number): string {
|
// ── Init ───────────────────────────────────────────────────────
|
||||||
return `$${n.toFixed(3)}`;
|
onMount(() => {
|
||||||
}
|
themeStore.initTheme(appRpc).catch(console.error);
|
||||||
|
fontStore.initFonts(appRpc).catch(console.error);
|
||||||
|
keybindingStore.init(appRpc).catch(console.error);
|
||||||
|
|
||||||
// ── Window control helpers ─────────────────────────────────────
|
appRpc.request["groups.list"]({}).then(({ groups: dbGroups }) => {
|
||||||
function windowMinimize() {
|
if (dbGroups.length > 0) groups = dbGroups;
|
||||||
appRpc.request["window.minimize"]({}).catch(console.error);
|
}).catch(console.error);
|
||||||
}
|
|
||||||
|
|
||||||
function windowMaximize() {
|
appRpc.request["settings.get"]({ key: 'active_group' }).then(({ value }) => {
|
||||||
appRpc.request["window.maximize"]({}).catch(console.error);
|
if (value && groups.some(g => g.id === value)) activeGroupId = value;
|
||||||
}
|
}).catch(console.error);
|
||||||
|
|
||||||
function windowClose() {
|
keybindingStore.on('palette', () => { paletteOpen = !paletteOpen; });
|
||||||
appRpc.request["window.close"]({}).catch(console.error);
|
keybindingStore.on('settings', () => { settingsOpen = !settingsOpen; });
|
||||||
}
|
keybindingStore.on('group1', () => setActiveGroup(groups[0]?.id));
|
||||||
|
keybindingStore.on('group2', () => setActiveGroup(groups[1]?.id));
|
||||||
|
keybindingStore.on('group3', () => setActiveGroup(groups[2]?.id));
|
||||||
|
keybindingStore.on('group4', () => setActiveGroup(groups[3]?.id));
|
||||||
|
keybindingStore.on('minimize', () => handleMinimize());
|
||||||
|
|
||||||
|
const cleanup = keybindingStore.installListener();
|
||||||
|
return cleanup;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsDrawer open={settingsOpen} onClose={() => settingsOpen = false} />
|
<SettingsDrawer open={settingsOpen} onClose={() => settingsOpen = false} />
|
||||||
<CommandPalette open={paletteOpen} onClose={() => paletteOpen = false} />
|
<CommandPalette open={paletteOpen} onClose={() => paletteOpen = false} />
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
<NotifDrawer
|
||||||
|
open={drawerOpen}
|
||||||
|
{notifications}
|
||||||
|
onClear={clearNotifications}
|
||||||
|
onClose={() => drawerOpen = false}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="app-shell"
|
class="app-shell"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
onresize={saveWindowFrame}
|
onresize={saveWindowFrame}
|
||||||
>
|
>
|
||||||
<!-- Sidebar icon rail -->
|
<!-- Left sidebar icon rail -->
|
||||||
<aside class="sidebar" role="navigation" aria-label="Primary navigation">
|
<aside class="sidebar" role="navigation" aria-label="Primary navigation">
|
||||||
<!-- AGOR vertical title -->
|
<!-- AGOR vertical title -->
|
||||||
<div class="agor-title" aria-hidden="true">AGOR</div>
|
<div class="agor-title" aria-hidden="true">AGOR</div>
|
||||||
|
|
||||||
<!-- Group icons -->
|
<!-- Group icons — numbered circles -->
|
||||||
<div class="sidebar-groups" role="list" aria-label="Project groups">
|
<div class="sidebar-groups" role="list" aria-label="Project groups">
|
||||||
{#each groups as group, i}
|
{#each groups as group, i}
|
||||||
<button
|
<button
|
||||||
class="sidebar-icon group-icon"
|
class="group-btn"
|
||||||
class:active={activeGroupId === group.id}
|
class:active={activeGroupId === group.id}
|
||||||
onclick={() => setActiveGroup(group.id)}
|
onclick={() => setActiveGroup(group.id)}
|
||||||
aria-label="{group.name} (Ctrl+{i + 1})"
|
aria-label="{group.name} (Ctrl+{i + 1})"
|
||||||
title="{group.name} (Ctrl+{i + 1})"
|
title="{group.name} (Ctrl+{i + 1})"
|
||||||
role="listitem"
|
|
||||||
>
|
>
|
||||||
<span class="group-emoji" aria-hidden="true">{group.icon}</span>
|
<span class="group-circle" aria-hidden="true">{i + 1}</span>
|
||||||
|
{#if group.hasNew}
|
||||||
|
<span class="group-badge" aria-label="New activity"></span>
|
||||||
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -326,32 +314,8 @@
|
||||||
|
|
||||||
<!-- Main workspace -->
|
<!-- Main workspace -->
|
||||||
<main class="workspace">
|
<main class="workspace">
|
||||||
<!-- Draggable title bar area with window controls -->
|
<!-- Draggable region at top of workspace (no title bar chrome) -->
|
||||||
<div class="title-bar" aria-label="Window title bar">
|
<div class="drag-region" aria-hidden="true"></div>
|
||||||
<div class="title-bar-drag" aria-hidden="true">
|
|
||||||
<span class="title-bar-text">{activeGroup?.name ?? 'Agent Orchestrator'}</span>
|
|
||||||
</div>
|
|
||||||
<div class="window-controls" role="toolbar" aria-label="Window controls">
|
|
||||||
<button
|
|
||||||
class="wc-btn"
|
|
||||||
onclick={windowMinimize}
|
|
||||||
aria-label="Minimize window"
|
|
||||||
title="Minimize"
|
|
||||||
>─</button>
|
|
||||||
<button
|
|
||||||
class="wc-btn"
|
|
||||||
onclick={windowMaximize}
|
|
||||||
aria-label="Maximize window"
|
|
||||||
title="Maximize"
|
|
||||||
>□</button>
|
|
||||||
<button
|
|
||||||
class="wc-btn close-btn"
|
|
||||||
onclick={windowClose}
|
|
||||||
aria-label="Close window"
|
|
||||||
title="Close"
|
|
||||||
>✕</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Project grid -->
|
<!-- Project grid -->
|
||||||
<div class="project-grid" role="list" aria-label="{activeGroup?.name ?? 'Projects'} projects">
|
<div class="project-grid" role="list" aria-label="{activeGroup?.name ?? 'Projects'} projects">
|
||||||
|
|
@ -378,7 +342,6 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Clone group: parent + clones in a flex row spanning full width -->
|
|
||||||
<div class="clone-group-row" role="listitem" aria-label="Project group: {row.parent.name}">
|
<div class="clone-group-row" role="listitem" aria-label="Project group: {row.parent.name}">
|
||||||
<ProjectCard
|
<ProjectCard
|
||||||
id={row.parent.id}
|
id={row.parent.id}
|
||||||
|
|
@ -399,7 +362,6 @@
|
||||||
onClone={handleClone}
|
onClone={handleClone}
|
||||||
/>
|
/>
|
||||||
{#each row.clones as clone (clone.id)}
|
{#each row.clones as clone (clone.id)}
|
||||||
<!-- Chain link icon between cards -->
|
|
||||||
<div class="chain-icon" aria-hidden="true">
|
<div class="chain-icon" aria-hidden="true">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||||
|
|
@ -431,12 +393,37 @@
|
||||||
|
|
||||||
{#if filteredProjects.length === 0}
|
{#if filteredProjects.length === 0}
|
||||||
<div class="empty-group" role="listitem">
|
<div class="empty-group" role="listitem">
|
||||||
<span class="empty-group-icon" aria-hidden="true">{activeGroup?.icon ?? '📁'}</span>
|
|
||||||
<p class="empty-group-text">No projects in {activeGroup?.name ?? 'this group'}</p>
|
<p class="empty-group-text">No projects in {activeGroup?.name ?? 'this group'}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<!-- Right sidebar: window controls + notification bell -->
|
||||||
|
<aside class="right-bar" aria-label="Window controls and notifications">
|
||||||
|
<div class="window-controls-vertical" role="toolbar" aria-label="Window controls">
|
||||||
|
<button class="wc-btn close-btn" onclick={handleClose} aria-label="Close window" title="Close">✕</button>
|
||||||
|
<button class="wc-btn" onclick={handleMaximize} aria-label="Maximize window" title="Maximize">□</button>
|
||||||
|
<button class="wc-btn" onclick={handleMinimize} aria-label="Minimize window" title="Minimize">─</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-spacer"></div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="right-icon notif-btn"
|
||||||
|
class:active={drawerOpen}
|
||||||
|
onclick={() => drawerOpen = !drawerOpen}
|
||||||
|
aria-label="{notifCount > 0 ? `${notifCount} notifications` : 'Notifications'}"
|
||||||
|
title="Notifications"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
||||||
|
</svg>
|
||||||
|
{#if notifCount > 0}
|
||||||
|
<span class="right-badge" aria-hidden="true">{notifCount}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status bar -->
|
<!-- Status bar -->
|
||||||
|
|
@ -462,9 +449,8 @@
|
||||||
<span>stalled</span>
|
<span>stalled</span>
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if attentionItems.length > 0}
|
{#if attentionItems.length > 0}
|
||||||
<span class="status-segment attn-badge" title="Needs attention: {attentionItems.map(p=>p.name).join(', ')}">
|
<span class="status-segment attn-badge" title="Needs attention: {attentionItems.map(p => p.name).join(', ')}">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" class="attn-icon">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" class="attn-icon">
|
||||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
@ -475,17 +461,13 @@
|
||||||
|
|
||||||
<span class="status-bar-spacer"></span>
|
<span class="status-bar-spacer"></span>
|
||||||
|
|
||||||
<!-- Group indicator -->
|
|
||||||
<span class="status-segment" title="Active group">
|
<span class="status-segment" title="Active group">
|
||||||
<span aria-hidden="true">{activeGroup?.icon}</span>
|
|
||||||
<span class="status-value">{activeGroup?.name}</span>
|
<span class="status-value">{activeGroup?.name}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="status-segment" title="Session duration">
|
<span class="status-segment" title="Session duration">
|
||||||
<span>session</span>
|
<span>session</span>
|
||||||
<span class="status-value">{sessionDuration}</span>
|
<span class="status-value">{sessionDuration}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="status-segment" title="Total tokens used">
|
<span class="status-segment" title="Total tokens used">
|
||||||
<span>tokens</span>
|
<span>tokens</span>
|
||||||
<span class="status-value">{fmtTokens(totalTokens)}</span>
|
<span class="status-value">{fmtTokens(totalTokens)}</span>
|
||||||
|
|
@ -495,20 +477,6 @@
|
||||||
<span class="status-value">{fmtCost(totalCost)}</span>
|
<span class="status-value">{fmtCost(totalCost)}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<button
|
|
||||||
class="notif-btn"
|
|
||||||
onclick={() => notifCount = 0}
|
|
||||||
aria-label="{notifCount > 0 ? `${notifCount} unread notifications` : 'Notifications'}"
|
|
||||||
title="Notifications"
|
|
||||||
>
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
|
||||||
</svg>
|
|
||||||
{#if notifCount > 0}
|
|
||||||
<span class="notif-badge" aria-hidden="true">{notifCount}</span>
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<kbd class="palette-hint" title="Open command palette">Ctrl+K</kbd>
|
<kbd class="palette-hint" title="Open command palette">Ctrl+K</kbd>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|
@ -523,7 +491,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Sidebar ──────────────────────────────────────────────── */
|
/* ── Left sidebar ─────────────────────────────────────────── */
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: var(--sidebar-width);
|
width: var(--sidebar-width);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
@ -536,26 +504,24 @@
|
||||||
gap: 0.125rem;
|
gap: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Vertical AGOR title */
|
|
||||||
.agor-title {
|
.agor-title {
|
||||||
writing-mode: vertical-rl;
|
writing-mode: vertical-rl;
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
font-family: var(--ui-font-family);
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
font-weight: 800;
|
font-weight: 900;
|
||||||
font-size: 0.6875rem;
|
font-size: 1.25rem;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.2em;
|
||||||
color: var(--ctp-overlay1);
|
color: var(--ctp-overlay0);
|
||||||
padding: 0.625rem 0;
|
padding: 1rem 0;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Group icons section */
|
|
||||||
.sidebar-groups {
|
.sidebar-groups {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.125rem;
|
gap: 0.25rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.25rem 0;
|
padding: 0.25rem 0;
|
||||||
border-bottom: 1px solid var(--ctp-surface0);
|
border-bottom: 1px solid var(--ctp-surface0);
|
||||||
|
|
@ -564,6 +530,57 @@
|
||||||
|
|
||||||
.sidebar-spacer { flex: 1; }
|
.sidebar-spacer { flex: 1; }
|
||||||
|
|
||||||
|
/* Numbered group button */
|
||||||
|
.group-btn {
|
||||||
|
position: relative;
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-circle {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1.5px solid var(--ctp-surface1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ctp-subtext0);
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-btn:hover .group-circle {
|
||||||
|
border-color: var(--ctp-overlay1);
|
||||||
|
color: var(--ctp-text);
|
||||||
|
background: var(--ctp-surface0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-btn.active .group-circle {
|
||||||
|
border-color: var(--accent, var(--ctp-mauve));
|
||||||
|
color: var(--accent, var(--ctp-mauve));
|
||||||
|
background: color-mix(in srgb, var(--accent, var(--ctp-mauve)) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.125rem;
|
||||||
|
right: 0.125rem;
|
||||||
|
width: 0.375rem;
|
||||||
|
height: 0.375rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--ctp-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings icon button */
|
||||||
.sidebar-icon {
|
.sidebar-icon {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
|
@ -577,31 +594,13 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: background 0.15s, color 0.15s;
|
transition: background 0.15s, color 0.15s;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-icon:hover { background: var(--ctp-surface0); color: var(--ctp-text); }
|
.sidebar-icon:hover { background: var(--ctp-surface0); color: var(--ctp-text); }
|
||||||
.sidebar-icon.active { background: var(--ctp-surface1); color: var(--ctp-text); }
|
.sidebar-icon.active { background: var(--ctp-surface1); color: var(--ctp-text); }
|
||||||
.sidebar-icon svg { width: 1rem; height: 1rem; }
|
.sidebar-icon svg { width: 1rem; height: 1rem; }
|
||||||
|
|
||||||
/* Active group: accent border-left indicator */
|
/* ── Main workspace ───────────────────────────────────────── */
|
||||||
.group-icon.active::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: -0.375rem;
|
|
||||||
top: 25%;
|
|
||||||
bottom: 25%;
|
|
||||||
width: 2px;
|
|
||||||
background: var(--ctp-mauve);
|
|
||||||
border-radius: 0 1px 1px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-emoji {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Workspace ────────────────────────────────────────────── */
|
|
||||||
.workspace {
|
.workspace {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
@ -610,64 +609,13 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Title bar (custom chrome) ────────────────────────────── */
|
/* Invisible draggable region at top of workspace */
|
||||||
.title-bar {
|
.drag-region {
|
||||||
height: 2rem;
|
height: 2.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
-webkit-app-region: drag;
|
||||||
background: var(--ctp-crust);
|
background: var(--ctp-crust);
|
||||||
border-bottom: 1px solid var(--ctp-surface0);
|
border-bottom: 1px solid var(--ctp-surface0);
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
flex-shrink: 0;
|
|
||||||
/* Make entire title bar draggable; window controls override this */
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-drag {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-text {
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--ctp-overlay1);
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.window-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wc-btn {
|
|
||||||
width: 2.75rem;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--ctp-overlay1);
|
|
||||||
font-size: 0.6875rem;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: background 0.12s, color 0.12s;
|
|
||||||
padding: 0;
|
|
||||||
font-family: var(--ui-font-family);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wc-btn:hover {
|
|
||||||
background: var(--ctp-surface0);
|
|
||||||
color: var(--ctp-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn:hover {
|
|
||||||
background: var(--ctp-red);
|
|
||||||
color: var(--ctp-base);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Project grid ─────────────────────────────────────────── */
|
/* ── Project grid ─────────────────────────────────────────── */
|
||||||
|
|
@ -687,7 +635,6 @@
|
||||||
.project-grid::-webkit-scrollbar-track { background: transparent; }
|
.project-grid::-webkit-scrollbar-track { background: transparent; }
|
||||||
.project-grid::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
|
.project-grid::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
|
||||||
|
|
||||||
/* Clone group: spans both grid columns, flex row */
|
|
||||||
.clone-group-row {
|
.clone-group-row {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -697,13 +644,8 @@
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Each ProjectCard inside a clone group gets flex: 1 */
|
.clone-group-row :global(.project-card) { flex: 1; min-width: 0; }
|
||||||
.clone-group-row :global(.project-card) {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Chain icon between linked cards */
|
|
||||||
.chain-icon {
|
.chain-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
|
|
@ -713,12 +655,8 @@
|
||||||
color: var(--ctp-surface1);
|
color: var(--ctp-surface1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chain-icon svg {
|
.chain-icon svg { width: 1rem; height: 1rem; }
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty group placeholder */
|
|
||||||
.empty-group {
|
.empty-group {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -730,9 +668,88 @@
|
||||||
color: var(--ctp-overlay0);
|
color: var(--ctp-overlay0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-group-icon { font-size: 2rem; }
|
|
||||||
.empty-group-text { font-size: 0.875rem; font-style: italic; }
|
.empty-group-text { font-size: 0.875rem; font-style: italic; }
|
||||||
|
|
||||||
|
/* ── Right sidebar ────────────────────────────────────────── */
|
||||||
|
.right-bar {
|
||||||
|
width: 2.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--ctp-mantle);
|
||||||
|
border-left: 1px solid var(--ctp-surface0);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.375rem 0;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls-vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-btn {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ctp-overlay1);
|
||||||
|
font-size: 0.625rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.12s, color 0.12s;
|
||||||
|
padding: 0;
|
||||||
|
font-family: var(--ui-font-family);
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wc-btn:hover { background: var(--ctp-surface0); color: var(--ctp-text); }
|
||||||
|
.close-btn:hover { background: var(--ctp-red); color: var(--ctp-base); }
|
||||||
|
|
||||||
|
.right-spacer { flex: 1; }
|
||||||
|
|
||||||
|
.right-icon {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--ctp-overlay1);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
position: relative;
|
||||||
|
transition: color 0.12s, background 0.12s;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-icon:hover { background: var(--ctp-surface0); color: var(--ctp-text); }
|
||||||
|
.right-icon.active { background: var(--ctp-surface1); color: var(--ctp-text); }
|
||||||
|
.right-icon svg { width: 0.875rem; height: 0.875rem; }
|
||||||
|
|
||||||
|
.right-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.125rem;
|
||||||
|
right: 0.125rem;
|
||||||
|
min-width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
background: var(--ctp-red);
|
||||||
|
color: var(--ctp-base);
|
||||||
|
border-radius: 0.4375rem;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 0.125rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Status bar ───────────────────────────────────────────── */
|
/* ── Status bar ───────────────────────────────────────────── */
|
||||||
.status-bar {
|
.status-bar {
|
||||||
height: var(--status-bar-height);
|
height: var(--status-bar-height);
|
||||||
|
|
@ -765,50 +782,12 @@
|
||||||
.status-dot-sm.gray { background: var(--ctp-overlay0); }
|
.status-dot-sm.gray { background: var(--ctp-overlay0); }
|
||||||
.status-dot-sm.orange { background: var(--ctp-peach); }
|
.status-dot-sm.orange { background: var(--ctp-peach); }
|
||||||
|
|
||||||
.status-value { color: var(--ctp-text); font-weight: 500; }
|
.status-value { color: var(--ctp-text); font-weight: 500; }
|
||||||
.status-bar-spacer { flex: 1; }
|
.status-bar-spacer { flex: 1; }
|
||||||
|
|
||||||
.attn-badge { color: var(--ctp-yellow); }
|
.attn-badge { color: var(--ctp-yellow); }
|
||||||
.attn-icon { width: 0.75rem; height: 0.75rem; stroke: var(--ctp-yellow); }
|
.attn-icon { width: 0.75rem; height: 0.75rem; stroke: var(--ctp-yellow); }
|
||||||
|
|
||||||
.notif-btn {
|
|
||||||
position: relative;
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--ctp-subtext0);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
transition: color 0.12s;
|
|
||||||
padding: 0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notif-btn:hover { color: var(--ctp-text); }
|
|
||||||
.notif-btn svg { width: 0.875rem; height: 0.875rem; }
|
|
||||||
|
|
||||||
.notif-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.125rem;
|
|
||||||
right: 0.125rem;
|
|
||||||
min-width: 0.875rem;
|
|
||||||
height: 0.875rem;
|
|
||||||
background: var(--ctp-red);
|
|
||||||
color: var(--ctp-base);
|
|
||||||
border-radius: 0.4375rem;
|
|
||||||
font-size: 0.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 0.2rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.palette-hint {
|
.palette-hint {
|
||||||
padding: 0.1rem 0.3rem;
|
padding: 0.1rem 0.3rem;
|
||||||
background: var(--ctp-surface0);
|
background: var(--ctp-surface0);
|
||||||
|
|
|
||||||
161
ui-electrobun/src/mainview/NotifDrawer.svelte
Normal file
161
ui-electrobun/src/mainview/NotifDrawer.svelte
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export interface Notification {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
type: 'success' | 'warning' | 'info' | 'error';
|
||||||
|
time: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
notifications: Notification[];
|
||||||
|
onClear: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { open, notifications, onClear, onClose }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<!-- Backdrop to close on outside click -->
|
||||||
|
<div class="notif-backdrop" role="presentation" onclick={onClose}></div>
|
||||||
|
|
||||||
|
<div class="notif-drawer" role="complementary" aria-label="Notification history">
|
||||||
|
<div class="drawer-header">
|
||||||
|
<span class="drawer-title">Notifications</span>
|
||||||
|
<button class="clear-btn" onclick={onClear} aria-label="Clear all notifications">
|
||||||
|
Clear all
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-body">
|
||||||
|
{#each notifications as notif (notif.id)}
|
||||||
|
<div class="notif-item" class:success={notif.type === 'success'} class:warning={notif.type === 'warning'} class:error={notif.type === 'error'}>
|
||||||
|
<span class="notif-dot" class:success={notif.type === 'success'} class:warning={notif.type === 'warning'} class:error={notif.type === 'error'} aria-hidden="true"></span>
|
||||||
|
<div class="notif-content">
|
||||||
|
<span class="notif-text">{notif.message}</span>
|
||||||
|
<span class="notif-time">{notif.time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if notifications.length === 0}
|
||||||
|
<div class="notif-empty">No notifications</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.notif-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-drawer {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 2.25rem; /* right-bar width */
|
||||||
|
bottom: var(--status-bar-height, 1.5rem);
|
||||||
|
width: 18rem;
|
||||||
|
background: var(--ctp-mantle);
|
||||||
|
border-left: 1px solid var(--ctp-surface0);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 91;
|
||||||
|
box-shadow: -0.25rem 0 1rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
border-bottom: 1px solid var(--ctp-surface0);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ctp-text);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
color: var(--ctp-overlay1);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: var(--ui-font-family);
|
||||||
|
transition: color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn:hover { color: var(--ctp-text); }
|
||||||
|
|
||||||
|
.drawer-body {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.375rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-body::-webkit-scrollbar { width: 0.25rem; }
|
||||||
|
.drawer-body::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
.drawer-body::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
|
||||||
|
|
||||||
|
.notif-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-item:hover { background: var(--ctp-surface0); }
|
||||||
|
|
||||||
|
.notif-dot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 0.4375rem;
|
||||||
|
height: 0.4375rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
background: var(--ctp-overlay1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-dot.success { background: var(--ctp-green); }
|
||||||
|
.notif-dot.warning { background: var(--ctp-yellow); }
|
||||||
|
.notif-dot.error { background: var(--ctp-red); }
|
||||||
|
|
||||||
|
.notif-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--ctp-text);
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-time {
|
||||||
|
font-size: 0.625rem;
|
||||||
|
color: var(--ctp-overlay0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notif-empty {
|
||||||
|
padding: 2rem 0.75rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--ctp-overlay0);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue