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:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user