From 0f4535c48c8d0540145f8f434cc1ab98a24ace6e Mon Sep 17 00:00:00 2001 From: Till JS Date: Sat, 18 Apr 2026 17:05:05 +0200 Subject: [PATCH] =?UTF-8?q?feat(workbench):=20interactive=20scope=20badge?= =?UTF-8?q?=20=E2=80=94=20click=20to=20clear,=20tooltip=20lists=20tags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Funnel badge on a scoped scene pill is now a real button: - Click clears the scene's scopeTagIds in one interaction instead of sending the user through the TagSelector in SceneHeader. - Tooltip shows the active scope tag names ("Bereich: Deep Work, Urlaub — klicken zum Aufheben") so users see which filter is on without opening the scene header. - Keyboard-accessible via Enter/Space; stopPropagation prevents the surrounding scene-pill button from also firing scene-select. Tag names are resolved via the existing useAllTags liveQuery — no extra Dexie round-trip per render. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/workbench/SceneAppBar.svelte | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/apps/mana/apps/web/src/lib/components/workbench/SceneAppBar.svelte b/apps/mana/apps/web/src/lib/components/workbench/SceneAppBar.svelte index b070a1926..520acc108 100644 --- a/apps/mana/apps/web/src/lib/components/workbench/SceneAppBar.svelte +++ b/apps/mana/apps/web/src/lib/components/workbench/SceneAppBar.svelte @@ -8,6 +8,8 @@ import type { CarouselPage } from '$lib/components/page-carousel/types'; import type { WorkbenchScene } from '$lib/types/workbench-scenes'; import { useAgents } from '$lib/data/ai/agents/queries'; + import { useAllTags } from '@mana/shared-stores'; + import { workbenchScenesStore } from '$lib/stores/workbench-scenes.svelte'; // Resolve each scene's bound agent → avatar + name. Cheap lookup // since all active agents are already in memory from the live- @@ -15,6 +17,30 @@ const agents = $derived(useAgents({ state: 'active' })); const agentById = $derived(new Map(agents.value.map((a) => [a.id, a]))); + // Tag name lookup for the scope-badge tooltip. Same liveQuery that + // SceneHeader already uses — no extra Dexie round-trip. + const allTags = $derived(useAllTags()); + const tagNameById = $derived(new Map((allTags.value ?? []).map((t) => [t.id, t.name] as const))); + + function scopeTitle(scopeTagIds: readonly string[] | undefined): string { + const names = (scopeTagIds ?? []) + .map((id) => tagNameById.get(id)) + .filter((n): n is string => !!n); + return names.length > 0 + ? `Bereich: ${names.join(', ')} — klicken zum Aufheben` + : 'Bereichsfilter aktiv — klicken zum Aufheben'; + } + + async function handleClearScope(e: MouseEvent | KeyboardEvent, sceneId: string) { + e.stopPropagation(); + if ('key' in e) e.preventDefault(); + try { + await workbenchScenesStore.setSceneScopeTags(sceneId, undefined); + } catch (err) { + console.error('[workbench] setSceneScopeTags failed:', err); + } + } + interface Props { scenes: WorkbenchScene[]; activeSceneId: string | null; @@ -141,7 +167,18 @@ {/if} {scene.name} {#if hasScope} - + + handleClearScope(e, scene.id)} + onkeydown={(e) => { + if (e.key === 'Enter' || e.key === ' ') handleClearScope(e, scene.id); + }} + > {/if} @@ -264,6 +301,19 @@ color: hsl(var(--color-primary)); opacity: 0.8; flex-shrink: 0; + cursor: pointer; + padding: 0.125rem; + margin: -0.125rem; + border-radius: 9999px; + transition: + opacity 0.15s, + background 0.15s; + } + .scope-badge:hover, + .scope-badge:focus-visible { + opacity: 1; + background: color-mix(in oklab, hsl(var(--color-primary)) 18%, transparent); + outline: none; } .group-sep { width: 1px;