From f09a84ac8e233c6dfa5b9bedf9b573e16958b09e Mon Sep 17 00:00:00 2001
From: Till JS
Date: Sat, 18 Apr 2026 16:52:14 +0200
Subject: [PATCH] feat(workbench): scope-aware empty state for scoped modules
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Phase 1 of the scene-scope empty state plan (docs/plans/scene-scope-
empty-state.md). When the active scene's scope tags filter a module
down to zero results, the ListView now shows a dedicated empty state
with a one-click "Bereich zurücksetzen" button instead of the generic
"Keine Aufgaben"/"Keine Treffer" message. Previously the user couldn't
tell whether the list was empty because of missing data or because of
the scope filter.
- New `ScopeEmptyState.svelte` shared component.
- New `hasActiveSceneScope()` reactive helper on the scene-scope store.
- Wired into todo, notes, calendar, contacts ListViews — the four
modules that currently use `filterBySceneScopeBatch`.
- 4 unit tests for the scope primitives.
Phase 2 (per-module hidden count) and Phase 3 (persistent scope badge)
remain optional follow-ups.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../workbench/ScopeEmptyState.svelte | 82 +++++++++++++++++++
.../src/lib/modules/calendar/ListView.svelte | 8 +-
.../src/lib/modules/contacts/ListView.svelte | 8 +-
.../web/src/lib/modules/notes/ListView.svelte | 8 +-
.../web/src/lib/modules/todo/ListView.svelte | 8 +-
.../web/src/lib/stores/scene-scope.svelte.ts | 8 ++
.../web/src/lib/stores/scene-scope.test.ts | 44 ++++++++++
7 files changed, 162 insertions(+), 4 deletions(-)
create mode 100644 apps/mana/apps/web/src/lib/components/workbench/ScopeEmptyState.svelte
create mode 100644 apps/mana/apps/web/src/lib/stores/scene-scope.test.ts
diff --git a/apps/mana/apps/web/src/lib/components/workbench/ScopeEmptyState.svelte b/apps/mana/apps/web/src/lib/components/workbench/ScopeEmptyState.svelte
new file mode 100644
index 000000000..1c5fc72ce
--- /dev/null
+++ b/apps/mana/apps/web/src/lib/components/workbench/ScopeEmptyState.svelte
@@ -0,0 +1,82 @@
+
+
+
+
+
+
{primaryLine}
+
+
+
+
diff --git a/apps/mana/apps/web/src/lib/modules/calendar/ListView.svelte b/apps/mana/apps/web/src/lib/modules/calendar/ListView.svelte
index f1e3fbc62..5c5885b72 100644
--- a/apps/mana/apps/web/src/lib/modules/calendar/ListView.svelte
+++ b/apps/mana/apps/web/src/lib/modules/calendar/ListView.svelte
@@ -17,6 +17,8 @@
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
import { transcribeAudio } from '$lib/voice/transcribe';
import FloatingInputBar from '$lib/components/FloatingInputBar.svelte';
+ import ScopeEmptyState from '$lib/components/workbench/ScopeEmptyState.svelte';
+ import { hasActiveSceneScope } from '$lib/stores/scene-scope.svelte';
let { navigate, goBack, params }: ViewProps = $props();
@@ -219,7 +221,11 @@
{/each}
{#if upcomingEvents.length === 0}
-
Keine Termine
+ {#if hasActiveSceneScope()}
+
+ {:else}
+
Keine Termine
+ {/if}
{/if}
diff --git a/apps/mana/apps/web/src/lib/modules/contacts/ListView.svelte b/apps/mana/apps/web/src/lib/modules/contacts/ListView.svelte
index 6eb025950..ec8223060 100644
--- a/apps/mana/apps/web/src/lib/modules/contacts/ListView.svelte
+++ b/apps/mana/apps/web/src/lib/modules/contacts/ListView.svelte
@@ -17,6 +17,8 @@
import { useAllTags, getTagsByIds } from '@mana/shared-stores';
import { addTagId } from '$lib/data/tag-mutations';
import { useItemContextMenu } from '$lib/data/item-context-menu.svelte';
+ import ScopeEmptyState from '$lib/components/workbench/ScopeEmptyState.svelte';
+ import { hasActiveSceneScope } from '$lib/stores/scene-scope.svelte';
let { navigate, goBack, params }: ViewProps = $props();
@@ -175,7 +177,11 @@
{/each}
{#if filtered().length === 0}
-
Keine Kontakte gefunden
+ {#if hasActiveSceneScope()}
+
+ {:else}
+
Keine Kontakte gefunden
+ {/if}
{/if}
diff --git a/apps/mana/apps/web/src/lib/modules/notes/ListView.svelte b/apps/mana/apps/web/src/lib/modules/notes/ListView.svelte
index 5594fcfbf..ff5f209ee 100644
--- a/apps/mana/apps/web/src/lib/modules/notes/ListView.svelte
+++ b/apps/mana/apps/web/src/lib/modules/notes/ListView.svelte
@@ -13,6 +13,8 @@
import { PencilSimple, Trash, PushPin } from '@mana/shared-icons';
import FloatingInputBar from '$lib/components/FloatingInputBar.svelte';
import AgentDot from '$lib/components/ai/AgentDot.svelte';
+ import ScopeEmptyState from '$lib/components/workbench/ScopeEmptyState.svelte';
+ import { hasActiveSceneScope } from '$lib/stores/scene-scope.svelte';
let { navigate, goBack, params }: ViewProps = $props();
@@ -212,7 +214,11 @@
{#if notes.length === 0}
-
Erstelle deine erste Notiz.
+ {#if hasActiveSceneScope()}
+
+ {:else}
+
Erstelle deine erste Notiz.
+ {/if}
{/if}
Keine Aufgaben
+ {#if hasActiveSceneScope()}
+
+ {:else}
+
Keine Aufgaben
+ {/if}
{/if}
diff --git a/apps/mana/apps/web/src/lib/stores/scene-scope.svelte.ts b/apps/mana/apps/web/src/lib/stores/scene-scope.svelte.ts
index dd986d469..f80c5d3a7 100644
--- a/apps/mana/apps/web/src/lib/stores/scene-scope.svelte.ts
+++ b/apps/mana/apps/web/src/lib/stores/scene-scope.svelte.ts
@@ -20,6 +20,14 @@ export function getSceneScopeTagIds(): readonly string[] | undefined {
return _scopeTagIds;
}
+/**
+ * True when the active scene is filtering by at least one scope tag.
+ * Reactive — safe to read inside a $derived or template.
+ */
+export function hasActiveSceneScope(): boolean {
+ return !!_scopeTagIds?.length;
+}
+
/**
* Batch filter using a pre-fetched tag map. Preferred for list queries
* (1 Dexie call instead of N).
diff --git a/apps/mana/apps/web/src/lib/stores/scene-scope.test.ts b/apps/mana/apps/web/src/lib/stores/scene-scope.test.ts
new file mode 100644
index 000000000..d9c4745be
--- /dev/null
+++ b/apps/mana/apps/web/src/lib/stores/scene-scope.test.ts
@@ -0,0 +1,44 @@
+/**
+ * Unit tests for the reactive scope-tag primitives backing the workbench
+ * scene scope. These live in the module scope of scene-scope.svelte.ts
+ * and are shared between AI scope-context, ListView empty states, and
+ * module queries via filterBySceneScopeBatch.
+ */
+
+import { afterEach, describe, expect, it } from 'vitest';
+import {
+ setSceneScopeTagIds,
+ getSceneScopeTagIds,
+ hasActiveSceneScope,
+} from './scene-scope.svelte';
+
+afterEach(() => {
+ setSceneScopeTagIds(undefined);
+});
+
+describe('scene scope state', () => {
+ it('starts empty', () => {
+ expect(getSceneScopeTagIds()).toBeUndefined();
+ expect(hasActiveSceneScope()).toBe(false);
+ });
+
+ it('setSceneScopeTagIds populates the state', () => {
+ setSceneScopeTagIds(['tag-1', 'tag-2']);
+ expect(getSceneScopeTagIds()).toEqual(['tag-1', 'tag-2']);
+ expect(hasActiveSceneScope()).toBe(true);
+ });
+
+ it('empty array is normalized to undefined', () => {
+ setSceneScopeTagIds([]);
+ expect(getSceneScopeTagIds()).toBeUndefined();
+ expect(hasActiveSceneScope()).toBe(false);
+ });
+
+ it('explicit undefined clears any active scope', () => {
+ setSceneScopeTagIds(['tag-1']);
+ expect(hasActiveSceneScope()).toBe(true);
+ setSceneScopeTagIds(undefined);
+ expect(hasActiveSceneScope()).toBe(false);
+ expect(getSceneScopeTagIds()).toBeUndefined();
+ });
+});