feat(fase-12): nav gating + redirect guard + realtime publication

UI side:
  * DesktopSidebar / BottomTabBar filter their entries by $enabledSections,
    tag each entry with data-section, and BottomTabBar exposes a
    data-section-count attribute so the grid is observable from tests.
  * "lists" is force-shown unconditionally — the §12.3.4 always-one-
    landing rule covers the edge case where every layer says OFF.
  * (app)/+layout.svelte adds a $effect that, when the URL matches a
    disabled section (/tasks, /notes, /search), goto's /lists and shows
    a transient `section_disabled_for_collective` toast.
  * Root +layout.svelte exposes the supabase singleton on window.__sb
    in dev so the new Playwright spec can patch rows without a parallel
    client; dead-code-eliminated in production builds.

DB side:
  * Migration 023 grows by ALTER PUBLICATION supabase_realtime ADD TABLE
    public.users + public.collectives + REPLICA IDENTITY FULL on both.
    Without this membership the features.ts subscriptions get zero
    events and SV-02 (collective toggle → realtime → member's nav
    updates) silently fails. Caught while running the spec.
  * pgTAP 015 grows from 16 to 20 assertions to cover publication
    membership + REPLICA IDENTITY for both new realtime tables.

Paraglide messages: section_disabled_for_collective, the visibility
section titles + blurbs, section_label_* per SectionKey. Both en + es.

Playwright tests/e2e/section-visibility.test.ts (SV-01..SV-03):
  SV-01 user toggle → nav reflects → /tasks redirects to /lists
  SV-02 admin collective toggle → member sees it disappear in realtime
  SV-03 collective ON beats user OFF — per-collective resolution

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 04:10:19 +02:00
parent 59c425f6f6
commit 286f59225f
9 changed files with 369 additions and 15 deletions

View File

@@ -2,18 +2,29 @@
import { page } from '$app/stores';
import { displayName } from '$lib/stores/auth';
import { currentCollective, userCollectives } from '$lib/stores/collective';
import { enabledSections } from '$lib/stores/features';
import type { SectionKey } from '@colectivo/types';
import Avatar from '$lib/components/Avatar.svelte';
import CreateCollectiveModal from '$lib/components/CreateCollectiveModal.svelte';
import * as m from '$lib/paraglide/messages';
import { ShoppingCart, CheckSquare, FileText, Search, Settings, Plus } from 'lucide-svelte';
const navItems = [
{ href: '/lists', label: () => m.nav_lists(), icon: ShoppingCart },
{ href: '/tasks', label: () => m.nav_tasks(), icon: CheckSquare },
{ href: '/notes', label: () => m.nav_notes(), icon: FileText },
{ href: '/search', label: () => m.nav_search(), icon: Search }
// Fase 12.3.1: nav items declared with their section key so we can filter
// by $enabledSections (precedence resolved client-side; see features.ts).
// Force "lists" through unconditionally in case every flag layer says OFF
// — matches the BottomTabBar / MobileDrawer "always one safe landing"
// rule documented in §12.3.4.
const allNavItems: Array<{ section: SectionKey; href: string; label: () => string; icon: typeof ShoppingCart }> = [
{ section: 'lists', href: '/lists', label: () => m.nav_lists(), icon: ShoppingCart },
{ section: 'tasks', href: '/tasks', label: () => m.nav_tasks(), icon: CheckSquare },
{ section: 'notes', href: '/notes', label: () => m.nav_notes(), icon: FileText },
{ section: 'search', href: '/search', label: () => m.nav_search(), icon: Search }
];
const navItems = $derived(
allNavItems.filter((item) => item.section === 'lists' || $enabledSections[item.section])
);
let switcherOpen = $state(false);
let showCreate = $state(false);
@@ -76,9 +87,10 @@
</div>
<nav class="flex-1 overflow-y-auto px-2 py-3">
{#each navItems as { href, label, icon: Icon }}
{#each navItems as { section, href, label, icon: Icon } (section)}
<a
{href}
data-section={section}
class="mb-0.5 flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors {activePath.startsWith(href) ? 'bg-black/10 text-slate-900 dark:bg-white/10 dark:text-slate-50' : 'text-slate-500 hover:bg-black/5 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-white/5 dark:hover:text-slate-50'}"
>
<Icon size={16} strokeWidth={1.5} />