docs(01): create phase 1 plans -- walking skeleton + portal shell
Three plans for Foundation & Portal Shell phase: - 01-01: Monorepo scaffold, Docker Compose with 3-network segmentation, NestJS API, Next.js app, PostgreSQL - 01-02: Design tokens (yellow #ffed00 primary), i18n (DE/EN), theme switching, responsive portal layout - 01-03: Visual verification checkpoint for human approval Includes SKELETON.md documenting architectural decisions for subsequent phases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
---
|
||||
phase: 01-foundation-portal-shell
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 01-01
|
||||
files_modified:
|
||||
- apps/web/package.json
|
||||
- apps/web/next.config.ts
|
||||
- apps/web/src/app/globals.css
|
||||
- apps/web/src/app/layout.tsx
|
||||
- apps/web/src/app/page.tsx
|
||||
- apps/web/src/i18n/request.ts
|
||||
- apps/web/src/messages/de.json
|
||||
- apps/web/src/messages/en.json
|
||||
- apps/web/src/lib/stores/sidebar-store.ts
|
||||
- apps/web/src/components/layout/app-shell.tsx
|
||||
- apps/web/src/components/layout/header.tsx
|
||||
- apps/web/src/components/layout/sidebar.tsx
|
||||
- apps/web/src/components/layout/sidebar-footer.tsx
|
||||
- apps/web/src/components/locale-switcher.tsx
|
||||
- apps/web/src/components/theme-toggle.tsx
|
||||
autonomous: true
|
||||
requirements:
|
||||
- PRTAL-01
|
||||
- PRTAL-04
|
||||
- UI-01
|
||||
- UI-02
|
||||
- UI-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "User sees a sticky header with logo/branding left, page title center, and user menu area right (D-07, D-08, D-09)"
|
||||
- "User sees a left sidebar that collapses to icon-width and expands to ~240px (D-01, D-02)"
|
||||
- "On mobile, sidebar is hidden with a hamburger button to open (D-03)"
|
||||
- "Sidebar shows Dashboard and Marketplace entries with accordion categories (D-05, D-15)"
|
||||
- "Sidebar has a footer area with settings icon and user info placeholder (D-06)"
|
||||
- "User can toggle between light and dark theme and the preference persists across refresh (UI-01)"
|
||||
- "User can switch between German and English, preference stored in cookie (UI-02)"
|
||||
- "All visible UI text is rendered via t() -- no hardcoded strings (UI-03)"
|
||||
- "Main content area shows empty dashboard state with placeholder text per D-16"
|
||||
- "Layout adapts responsively to different screen sizes (PRTAL-04)"
|
||||
artifacts:
|
||||
- path: "apps/web/src/components/layout/header.tsx"
|
||||
provides: "Sticky header with branding, title, user menu"
|
||||
min_lines: 30
|
||||
- path: "apps/web/src/components/layout/sidebar.tsx"
|
||||
provides: "Collapsible sidebar with accordion categories"
|
||||
min_lines: 50
|
||||
- path: "apps/web/src/components/layout/app-shell.tsx"
|
||||
provides: "Portal layout composition (header + sidebar + main)"
|
||||
min_lines: 20
|
||||
- path: "apps/web/src/components/theme-toggle.tsx"
|
||||
provides: "Light/dark theme toggle button"
|
||||
min_lines: 15
|
||||
- path: "apps/web/src/components/locale-switcher.tsx"
|
||||
provides: "DE/EN language switcher"
|
||||
min_lines: 15
|
||||
- path: "apps/web/src/messages/de.json"
|
||||
provides: "German translations"
|
||||
contains: "Dashboard"
|
||||
- path: "apps/web/src/messages/en.json"
|
||||
provides: "English translations"
|
||||
contains: "Dashboard"
|
||||
- path: "apps/web/src/app/globals.css"
|
||||
provides: "Design tokens with OKLCH colors and dark mode"
|
||||
contains: "oklch"
|
||||
- path: "apps/web/src/lib/stores/sidebar-store.ts"
|
||||
provides: "Sidebar collapse/expand state with persistence"
|
||||
exports: ["useSidebarStore"]
|
||||
- path: "apps/web/src/i18n/request.ts"
|
||||
provides: "Locale resolution from NEXT_LOCALE cookie"
|
||||
contains: "getRequestConfig"
|
||||
key_links:
|
||||
- from: "apps/web/src/app/layout.tsx"
|
||||
to: "apps/web/src/components/layout/app-shell.tsx"
|
||||
via: "Component composition"
|
||||
pattern: "AppShell"
|
||||
- from: "apps/web/src/app/layout.tsx"
|
||||
to: "next-themes"
|
||||
via: "ThemeProvider wrapper"
|
||||
pattern: "ThemeProvider"
|
||||
- from: "apps/web/src/app/layout.tsx"
|
||||
to: "next-intl"
|
||||
via: "NextIntlClientProvider wrapper"
|
||||
pattern: "NextIntlClientProvider"
|
||||
- from: "apps/web/src/components/layout/sidebar.tsx"
|
||||
to: "apps/web/src/lib/stores/sidebar-store.ts"
|
||||
via: "Zustand store consumption"
|
||||
pattern: "useSidebarStore"
|
||||
- from: "apps/web/src/components/locale-switcher.tsx"
|
||||
to: "NEXT_LOCALE cookie"
|
||||
via: "document.cookie set"
|
||||
pattern: "NEXT_LOCALE"
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
**As a** user, **I want to** access a running portal application with responsive layout, theme switching, and bilingual interface, **so that** I have the structural frame into which all workflow modules will be placed.
|
||||
|
||||
<objective>
|
||||
Portal Shell: Build the complete portal UI with design token system (yellow #ffed00 primary, dark gray-blue dark mode per D-10, D-14), i18n framework (DE/EN via next-intl cookie-based per UI-02, UI-03), theme switching (light/dark via next-themes per UI-01), and responsive layout (sticky header per D-07/D-08/D-09, collapsible sidebar per D-01 through D-06, main content area with empty dashboard state per D-15/D-16) -- implementing all 17 locked user decisions.
|
||||
|
||||
Purpose: After this plan, the user sees and interacts with the full portal frame. They can collapse the sidebar, toggle theme, switch language, and see the empty dashboard -- every visual and interactive element of Phase 1.
|
||||
|
||||
Output: Fully styled, responsive, bilingual portal shell with theme support, ready for module content in later phases.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
|
||||
@/home/vicolab/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-foundation-portal-shell/01-RESEARCH.md
|
||||
@.planning/phases/01-foundation-portal-shell/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation-portal-shell/01-01-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Design token system + i18n framework + theme provider setup</name>
|
||||
<files>
|
||||
apps/web/package.json
|
||||
apps/web/next.config.ts
|
||||
apps/web/src/app/globals.css
|
||||
apps/web/src/app/layout.tsx
|
||||
apps/web/src/i18n/request.ts
|
||||
apps/web/src/messages/de.json
|
||||
apps/web/src/messages/en.json
|
||||
</files>
|
||||
<read_first>
|
||||
apps/web/package.json (current dependencies from Plan 01)
|
||||
apps/web/next.config.ts (current config from Plan 01)
|
||||
apps/web/src/app/layout.tsx (current layout from Plan 01)
|
||||
apps/web/src/app/globals.css (current CSS from Plan 01)
|
||||
.planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 2: next-intl, Pattern 3: Design Tokens, Root Layout with Providers, Message JSON Structure, next.config.ts example)
|
||||
.planning/phases/01-foundation-portal-shell/01-CONTEXT.md (D-10 through D-14 for colors/style)
|
||||
</read_first>
|
||||
<action>
|
||||
Install additional dependencies in apps/web: `pnpm add next-intl next-themes zustand` (from the web directory or via --filter @tessera/web from root).
|
||||
|
||||
Update `apps/web/next.config.ts` to use the next-intl plugin: import createNextIntlPlugin from "next-intl/plugin", call withNextIntl("./src/i18n/request.ts"), wrap the nextConfig (output: "standalone") with it, export default.
|
||||
|
||||
Replace `apps/web/src/app/globals.css` with the full design token system from RESEARCH Pattern 3. Include:
|
||||
- `@import "tailwindcss";` at top
|
||||
- `@theme inline` block mapping CSS variable names to Tailwind utility classes (--color-primary, --color-primary-foreground, --color-accent, --color-accent-foreground, --color-background, --color-foreground, --color-card, --color-card-foreground, --color-muted, --color-muted-foreground, --color-border, --color-input, --color-ring, --color-sidebar-*, --radius-sm, --radius-md, --radius-lg)
|
||||
- `:root` block with light mode OKLCH values: --radius 0.5rem (8px per D-13), --primary oklch(0.91 0.19 102) (yellow #ffed00 per D-10), --primary-foreground oklch(0.20 0.02 90) dark text on yellow, --background oklch(0.99 0 0), --foreground oklch(0.15 0 0), plus all card/secondary/muted/accent/border/input/ring tokens
|
||||
- `.dark` block with dark mode values: --background oklch(0.17 0.01 260) dark gray-blue per D-14 discretion, --foreground oklch(0.95 0 0), --primary stays oklch(0.91 0.19 102) vibrant yellow, all other dark variants as in RESEARCH
|
||||
- Add --font-sans variable set to "Inter, system-ui, -apple-system, sans-serif" (discretion: Inter for clean modern style)
|
||||
- Add sidebar-specific tokens: --sidebar-width: 240px (D-02), --sidebar-width-collapsed: 64px, --header-height: 60px (D-08, middle of 56-64px range)
|
||||
- Add base body styles: font-family var(--font-sans), antialiased
|
||||
|
||||
Create `apps/web/src/i18n/request.ts` per RESEARCH Pattern 2: import getRequestConfig from "next-intl/server", import cookies from "next/headers". Export default getRequestConfig that reads NEXT_LOCALE cookie (fallback "de"), dynamically imports messages from ../messages/${locale}.json, returns { locale, messages }.
|
||||
|
||||
Create `apps/web/src/messages/de.json` with the full German translation structure from RESEARCH: common (appName, loading, save, cancel), header (search placeholder, userMenu), sidebar (dashboard, marketplace, settings, collapse, expand), dashboard (empty: "Keine Widgets aktiv", addWidget: "Widget hinzufuegen"), theme (light: "Hell", dark: "Dunkel", system: "System"), locale (de: "Deutsch", en: "English"). Add additional keys: sidebar.categories (label), header.breadcrumb (home: "Startseite").
|
||||
|
||||
Create `apps/web/src/messages/en.json` with matching English translations: common (appName, loading, save, cancel), header (search, userMenu), sidebar (dashboard, marketplace, settings, collapse, expand), dashboard (empty: "No active widgets", addWidget: "Add widget"), theme (light, dark, system), locale (de: "Deutsch", en: "English"). Mirror all keys from de.json exactly.
|
||||
|
||||
Update `apps/web/src/app/layout.tsx` to the full provider-wrapped layout: async function RootLayout, call getLocale() and getMessages() from "next-intl/server". Wrap children in html (lang={locale}, suppressHydrationWarning) > body (className with font-sans, antialiased, bg-background, text-foreground) > ThemeProvider (attribute="class", defaultTheme="system", enableSystem, disableTransitionOnChange) > NextIntlClientProvider (messages={messages}) > {children}. Import globals.css. Export metadata with title "Tessera", description from t or static.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check --filter=@tessera/web 2>&1 | tail -5 && node -e "const de = require('./apps/web/src/messages/de.json'); const en = require('./apps/web/src/messages/en.json'); const deKeys = JSON.stringify(Object.keys(de).sort()); const enKeys = JSON.stringify(Object.keys(en).sort()); if(deKeys !== enKeys) { console.error('MISMATCH:', deKeys, enKeys); process.exit(1); } console.log('i18n keys match')"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- apps/web/src/app/globals.css contains @import "tailwindcss" and @theme inline block
|
||||
- globals.css :root contains --primary with oklch(0.91 0.19 102) value (yellow #ffed00 per D-10)
|
||||
- globals.css .dark contains --background with oklch(0.17 0.01 260) value (dark gray-blue per D-14)
|
||||
- globals.css contains --radius: 0.5rem (8px per D-13)
|
||||
- globals.css contains --sidebar-width: 240px (per D-02)
|
||||
- globals.css contains --header-height: 60px (per D-08)
|
||||
- apps/web/src/i18n/request.ts imports getRequestConfig and reads NEXT_LOCALE cookie
|
||||
- de.json and en.json have identical top-level key structure
|
||||
- de.json contains "dashboard.empty": "Keine Widgets aktiv" (per D-16 German)
|
||||
- en.json contains "dashboard.empty": "No active widgets"
|
||||
- apps/web/src/app/layout.tsx wraps children in ThemeProvider and NextIntlClientProvider
|
||||
- layout.tsx calls getLocale() and getMessages() from next-intl/server
|
||||
- layout.tsx html tag has lang={locale} and suppressHydrationWarning
|
||||
- apps/web/next.config.ts uses createNextIntlPlugin wrapping the config
|
||||
- `pnpm turbo type-check --filter=@tessera/web` exits with code 0
|
||||
</acceptance_criteria>
|
||||
<done>Design token system with OKLCH colors (yellow primary, dark gray-blue dark mode), i18n framework with DE/EN message files and cookie-based locale, and provider-wrapped root layout are all in place and type-check.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Portal layout components -- header, sidebar, app shell, interactions</name>
|
||||
<files>
|
||||
apps/web/src/lib/stores/sidebar-store.ts
|
||||
apps/web/src/components/layout/app-shell.tsx
|
||||
apps/web/src/components/layout/header.tsx
|
||||
apps/web/src/components/layout/sidebar.tsx
|
||||
apps/web/src/components/layout/sidebar-footer.tsx
|
||||
apps/web/src/components/locale-switcher.tsx
|
||||
apps/web/src/components/theme-toggle.tsx
|
||||
apps/web/src/app/page.tsx
|
||||
</files>
|
||||
<read_first>
|
||||
apps/web/src/app/layout.tsx (provider structure from Task 1)
|
||||
apps/web/src/app/globals.css (design tokens and CSS variables from Task 1)
|
||||
apps/web/src/messages/de.json (translation keys available)
|
||||
apps/web/src/lib/stores/sidebar-store.ts (if exists, otherwise will create)
|
||||
.planning/phases/01-foundation-portal-shell/01-CONTEXT.md (ALL decisions D-01 through D-17)
|
||||
.planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Zustand Sidebar Store example)
|
||||
</read_first>
|
||||
<action>
|
||||
Create `apps/web/src/lib/stores/sidebar-store.ts` per RESEARCH example: Zustand store with persist middleware. Interface SidebarState with isCollapsed (boolean), isMobileOpen (boolean), toggle() (toggles isCollapsed), setMobileOpen(open: boolean). Persist to localStorage key "tessera-sidebar". Default isCollapsed: false, isMobileOpen: false.
|
||||
|
||||
Create `apps/web/src/components/theme-toggle.tsx` ("use client"): Import useTheme from "next-themes", useTranslations from "next-intl". Render a button that cycles through light/dark/system themes. Display current theme icon (sun for light, moon for dark, monitor for system) using inline SVG or unicode symbols. Use t("theme.light"), t("theme.dark"), t("theme.system") for aria-label/tooltip. Style with Tailwind: rounded-md (per D-13 border-radius), hover:bg-muted, transition-colors. Include mounted state check to avoid hydration mismatch (common next-themes pattern: const [mounted, setMounted] = useState(false), useEffect to set mounted true, render placeholder while not mounted).
|
||||
|
||||
Create `apps/web/src/components/locale-switcher.tsx` ("use client"): Per RESEARCH Pattern 2 locale-switcher example. Import useRouter from "next/navigation", useTranslations from "next-intl", useLocale from "next-intl". Button displays current locale label (t("locale.de") or t("locale.en")). On click: set document.cookie NEXT_LOCALE to opposite locale with path=/ max-age=31536000, then router.refresh() inside startTransition. Style with Tailwind matching the theme-toggle button style.
|
||||
|
||||
Create `apps/web/src/components/layout/header.tsx` ("use client"): Per D-07, D-08, D-09. Fixed/sticky header at top. Height var(--header-height) via h-[var(--header-height)] or h-15 (60px). CSS: sticky top-0 z-50 bg-background border-b border-border.
|
||||
|
||||
Layout is a flex row with three sections:
|
||||
- Left: Logo area. Render the Tessera logo text (use t("common.appName")) styled with primary color (text-primary font-bold). Include a mobile hamburger button (visible only on md:hidden) that calls useSidebarStore().setMobileOpen(true).
|
||||
- Center: Breadcrumb / page title. Render t("header.breadcrumb.home") or a simple "Dashboard" text. Use text-muted-foreground, truncate for overflow.
|
||||
- Right: Actions area with theme-toggle, locale-switcher, and a user avatar placeholder (circular div with bg-muted, will be wired to auth in Phase 2). Flex row with gap-2.
|
||||
|
||||
All strings via useTranslations(). No hardcoded text per UI-03.
|
||||
|
||||
Create `apps/web/src/components/layout/sidebar.tsx` ("use client"): Per D-01 through D-06.
|
||||
|
||||
Outer container: fixed left-0, top-[var(--header-height)], height calc(100vh - var(--header-height)), bg-background, border-r border-border, transition-all duration-200. Width toggles between var(--sidebar-width) when expanded and var(--sidebar-width-collapsed) when collapsed. Use useSidebarStore() for isCollapsed state.
|
||||
|
||||
Mobile behavior (D-03): On screens below md breakpoint, sidebar is position fixed, full height, translated off-screen by default (transform -translate-x-full). When isMobileOpen is true, translate-x-0 with a backdrop overlay (fixed inset-0 bg-black/50 z-40). Tapping overlay calls setMobileOpen(false).
|
||||
|
||||
Content structure:
|
||||
- Top section: Navigation items. Two fixed entries visible in empty state (discretion: Dashboard and Marketplace). Each entry is a flex row with icon (inline SVG or emoji placeholder) + label text. When collapsed (D-01), only icons show (label hidden via overflow-hidden or conditional render). Active item (Dashboard per D-15) has bg-primary/10 text-primary styling.
|
||||
- Categories section (D-05): An accordion-style expandable section labeled t("sidebar.categories"). When expanded shows "No modules" placeholder. Use a simple disclosure pattern (button toggles visibility of child div) -- no external accordion library needed.
|
||||
- Toggle button: At bottom of nav section, a button to collapse/expand. Calls useSidebarStore().toggle(). Shows collapse icon (chevron-left) when expanded, expand icon (chevron-right) when collapsed. Label: t("sidebar.collapse") / t("sidebar.expand"), hidden when collapsed.
|
||||
|
||||
All strings via useTranslations("sidebar"). No hardcoded text per UI-03.
|
||||
|
||||
Create `apps/web/src/components/layout/sidebar-footer.tsx` ("use client"): Per D-06. Footer area at bottom of sidebar. Contains:
|
||||
- Settings icon (gear/cog) as a button/link. When sidebar collapsed, just the icon. When expanded, icon + t("sidebar.settings") label.
|
||||
- User info placeholder: small avatar circle + username text (placeholder "User" for now -- wired in Phase 2). When collapsed, just avatar. When expanded, avatar + name.
|
||||
Flex column with gap-2, border-t border-border, p-3.
|
||||
|
||||
Create `apps/web/src/components/layout/app-shell.tsx`: Server component or client component composing the layout. Renders Header at top, Sidebar on left, main content area filling remaining space. Main area: ml-[var(--sidebar-width)] when sidebar expanded, ml-[var(--sidebar-width-collapsed)] when collapsed. On mobile (below md): ml-0 (sidebar overlays). Transition-all for smooth sidebar animation. Padding p-6 on main content. The children prop renders inside main.
|
||||
|
||||
Since app-shell needs to read sidebar state for margin calculation, make it "use client" and import useSidebarStore. Alternatively, use CSS-only approach with a data attribute on a parent. Prefer the Zustand approach for consistency.
|
||||
|
||||
Update `apps/web/src/app/page.tsx`: The dashboard page. Per D-15 (dashboard is start page) and D-16 (empty state). Render within the app-shell. Display a centered empty state: an icon (layout-grid or similar), t("dashboard.empty") text ("Keine Widgets aktiv" in German), and a button styled with bg-primary text-primary-foreground rounded-md (per D-13) showing t("dashboard.addWidget"). Button is non-functional for now (dashboard implementation is Phase 5). Use useTranslations("dashboard").
|
||||
|
||||
Wire everything together: In layout.tsx (from Task 1), the children are wrapped by providers. In page.tsx, render AppShell wrapping the dashboard content. AppShell renders Header, Sidebar (with SidebarFooter inside), and main area with {children}.
|
||||
|
||||
Responsive behavior (PRTAL-04): Test at Tailwind breakpoints. Below md (~768px): sidebar hidden, hamburger visible in header. Above md: sidebar visible, hamburger hidden. Above lg (~1024px): full sidebar width.
|
||||
|
||||
Visual style (D-12): Modern/clean with whitespace. Use shadow-sm on header (subtle shadow per D-12). Cards and containers use rounded-lg (per D-13). Spacing generous (p-4, gap-4 minimum).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm turbo type-check --filter=@tessera/web 2>&1 | tail -5 && grep -r "\"use client\"" apps/web/src/components/ --include="*.tsx" -l | wc -l && grep -rn ">[A-Z][a-z]" apps/web/src/components/ apps/web/src/app/page.tsx --include="*.tsx" | grep -v "import\|from\|//\|className\|type\|interface\|export\|const\|SVG\|path\|svg\|function\|return\|useState\|useEffect\|use client\|next" | head -20</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- apps/web/src/lib/stores/sidebar-store.ts exports useSidebarStore with isCollapsed, isMobileOpen, toggle, setMobileOpen
|
||||
- sidebar-store.ts uses persist middleware with key "tessera-sidebar"
|
||||
- apps/web/src/components/layout/header.tsx is a "use client" component
|
||||
- header.tsx has sticky positioning (className contains "sticky" and "top-0")
|
||||
- header.tsx contains three sections: logo/branding left, breadcrumb center, actions right
|
||||
- header.tsx includes hamburger button visible only on mobile (md:hidden class)
|
||||
- header.tsx renders ThemeToggle and LocaleSwitcher components
|
||||
- apps/web/src/components/layout/sidebar.tsx reads isCollapsed from useSidebarStore
|
||||
- sidebar.tsx implements width transition between expanded (~240px) and collapsed (~64px) states
|
||||
- sidebar.tsx shows Dashboard entry with active styling (bg-primary or similar)
|
||||
- sidebar.tsx shows Marketplace entry
|
||||
- sidebar.tsx has accordion-style categories section per D-05
|
||||
- sidebar.tsx on mobile uses translate-x transform with backdrop overlay
|
||||
- apps/web/src/components/layout/sidebar-footer.tsx renders settings icon and user placeholder
|
||||
- apps/web/src/components/layout/app-shell.tsx composes Header, Sidebar, and main content area
|
||||
- app-shell.tsx adjusts main content margin based on sidebar collapse state
|
||||
- apps/web/src/app/page.tsx shows empty dashboard state with t("dashboard.empty") and t("dashboard.addWidget")
|
||||
- apps/web/src/components/theme-toggle.tsx uses useTheme() from next-themes with mounted guard
|
||||
- apps/web/src/components/locale-switcher.tsx sets NEXT_LOCALE cookie and calls router.refresh()
|
||||
- All component text uses useTranslations() calls -- no hardcoded user-visible strings (UI-03)
|
||||
- `pnpm turbo type-check --filter=@tessera/web` exits with code 0
|
||||
</acceptance_criteria>
|
||||
<done>Complete portal shell with sticky header (logo, breadcrumb, theme/locale/user), collapsible sidebar (nav items, accordion categories, footer with settings), responsive layout (mobile hamburger + overlay), empty dashboard state, theme toggle persisting to localStorage, and locale switcher persisting to cookie -- all text via i18n framework.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
## Artifacts This Phase Produces
|
||||
|
||||
| Symbol/File | Type | Consumed By |
|
||||
|---|---|---|
|
||||
| `useSidebarStore` | Zustand store | sidebar.tsx, app-shell.tsx, header.tsx (hamburger) |
|
||||
| `apps/web/src/components/layout/header.tsx` | React component | app-shell.tsx |
|
||||
| `apps/web/src/components/layout/sidebar.tsx` | React component | app-shell.tsx |
|
||||
| `apps/web/src/components/layout/sidebar-footer.tsx` | React component | sidebar.tsx |
|
||||
| `apps/web/src/components/layout/app-shell.tsx` | React component | page.tsx (and all future pages) |
|
||||
| `apps/web/src/components/theme-toggle.tsx` | React component | header.tsx |
|
||||
| `apps/web/src/components/locale-switcher.tsx` | React component | header.tsx |
|
||||
| `apps/web/src/i18n/request.ts` | i18n config | next.config.ts plugin |
|
||||
| `apps/web/src/messages/de.json` | German translations | All components via t() |
|
||||
| `apps/web/src/messages/en.json` | English translations | All components via t() |
|
||||
| `apps/web/src/app/globals.css` | Design token system | All styled components |
|
||||
| CSS variables: `--primary`, `--sidebar-width`, `--header-height` | Design tokens | All layout components |
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Cookie (NEXT_LOCALE) | Client-set locale cookie consumed server-side |
|
||||
| localStorage (tessera-sidebar, theme) | Client-persisted UI state |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-02-01 | Tampering | NEXT_LOCALE cookie | accept | Cookie only controls UI language (de/en). Invalid values fall back to "de". No security impact. |
|
||||
| T-02-02 | Tampering | localStorage sidebar state | accept | Controls only UI collapse state. No data or auth implications. |
|
||||
| T-02-03 | Information Disclosure | SSR-rendered locale/theme | accept | No sensitive data in theme/locale preferences |
|
||||
| T-02-SC | Tampering | npm installs (next-intl, next-themes, zustand) | mitigate | All packages pass legitimacy audit in RESEARCH.md with OK verdict |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
1. `pnpm turbo type-check --filter=@tessera/web` passes
|
||||
2. `pnpm turbo build --filter=@tessera/web` completes successfully
|
||||
3. Docker rebuild and `docker compose up` serves the portal shell at localhost:80
|
||||
4. Clicking theme toggle cycles light/dark/system -- preference survives page refresh
|
||||
5. Clicking locale switcher changes all text between DE and EN -- preference survives page refresh
|
||||
6. Clicking sidebar collapse button animates sidebar to icon width
|
||||
7. On narrow viewport (< 768px), sidebar is hidden and hamburger button appears in header
|
||||
8. `grep -rn ">[A-Z]" apps/web/src/components/ --include="*.tsx"` returns no hardcoded user-visible strings
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Portal shell renders with header (sticky, ~60px, logo + breadcrumb + actions)
|
||||
- Sidebar collapses from 240px to 64px icon-width with smooth transition
|
||||
- Mobile sidebar overlays with backdrop on hamburger click
|
||||
- Theme toggle persists dark/light preference across browser refresh
|
||||
- Language switch changes all UI text between German and English
|
||||
- Empty dashboard shows "Keine Widgets aktiv" (DE) / "No active widgets" (EN) with add button
|
||||
- All user-visible text comes from i18n message files (no hardcoded strings)
|
||||
- Design uses yellow #ffed00 primary, dark gray-blue dark mode, 8px border-radius
|
||||
- Modern/clean visual style with whitespace, subtle shadows per D-12
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/01-foundation-portal-shell/01-02-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user