--- phase: 17-ui-optimization-polish plan: 04 type: execute wave: 2 depends_on: ["17-01", "17-02"] files_modified: - apps/pwa/src/components/BrandSlot.tsx - apps/pwa/index.html - apps/pwa/vite.config.ts - apps/pwa/src/styles/tokens.css autonomous: true requirements: [D-04, D-05] must_haves: truths: - "The real logo renders in BrandSlot on /login with no LoginPage layout shift;

still carries the app name" - "index.html links favicon.svg + favicon.ico + apple-touch-icon, and theme-color matches the approved accent" - "The PWA manifest references the proper separate icon-maskable-512.png (not a reused icon-512.png)" - "The approved brand accent is applied consistently across tokens.css, index.html, and the manifest" artifacts: - path: "apps/pwa/src/components/BrandSlot.tsx" provides: "Logo img swapped in for the placeholder div, decorative (alt empty, aria-hidden)" contains: "logo.svg" - path: "apps/pwa/index.html" provides: "favicon link set + theme-color meta" contains: "favicon.svg" key_links: - from: "apps/pwa/vite.config.ts" to: "apps/pwa/public/icon-maskable-512.png" via: "manifest icons[] references the separate maskable file with purpose maskable" pattern: "icon-maskable-512.png" - from: "apps/pwa/src/components/BrandSlot.tsx" to: "apps/pwa/public/logo.svg" via: "img src /logo.svg" pattern: "logo.svg" --- Wire the approved branding assets (from plan 17-02) into the app (D-05, D-04): swap the BrandSlot placeholder div for a decorative logo img through the existing seam (no LoginPage layout change), add the missing favicon links to index.html, fix the vite.config.ts manifest to reference the proper separate `icon-maskable-512.png`, and apply the operator-selected brand accent plus `--brand-logo-border-radius` (from the 17-02 checkpoint) consistently across tokens.css, index.html, and the manifest. This plan runs AFTER 17-02 (assets exist + accent/border-radius approved at the checkpoint) and 17-01 (tokens.css restructured, `--brand-logo-*` live in the combined block). It does not touch logo art — only wiring + the approved token values. Purpose: replace placeholder stubs/missing favicons with the real, approved identity and fix the improper-maskable defect. Output: BrandSlot img, index.html favicon links + theme-color, manifest icons[] with separate maskable, and the accent/border-radius token values. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-ui-optimization-polish/17-UI-SPEC.md @.planning/phases/17-ui-optimization-polish/17-PATTERNS.md @.planning/phases/17-ui-optimization-polish/17-RESEARCH.md @.planning/phases/17-ui-optimization-polish/17-01-SUMMARY.md @.planning/phases/17-ui-optimization-polish/17-02-SUMMARY.md Task 1: Swap BrandSlot placeholder div for the logo img and set --brand-logo-border-radius apps/pwa/src/components/BrandSlot.tsx, apps/pwa/src/styles/tokens.css - apps/pwa/src/components/BrandSlot.tsx lines 29-50 (the placeholder div to replace; note the h1 FamilySync and tagline p must stay) - 17-UI-SPEC.md section "Workstream B" -> "BrandSlot swap contract (D-05)" and section "Surface Architecture" -> "Surface B-1" (the exact img style object) - 17-PATTERNS.md section "apps/pwa/src/components/BrandSlot.tsx" (before/after excerpt) - 17-RESEARCH.md section "Common Pitfalls" -> Pitfall 5 (--brand-logo-border-radius 50% clips an SVG into a circle) - 17-02-SUMMARY.md (the operator-approved --brand-logo-border-radius value from the checkpoint) - .planning/phases/19-local-auth-no-oidc-mode/19-UI-SPEC.md section "Brand Slot" (the Phase 19 seam contract — LoginPage layout MUST NOT change) In apps/pwa/src/components/BrandSlot.tsx, replace the placeholder div (currently rendering the "FS" initials with aria-hidden) with a decorative logo image element: src "/logo.svg", empty alt, aria-hidden true. Apply the existing brand-logo token surface to the image per UI-SPEC Surface B-1: width var(--brand-logo-size, 48px), height var(--brand-logo-size, 48px), borderRadius var(--brand-logo-border-radius), margin "0 auto var(--space-2, 8px)", display block, aspectRatio "1 / 1", objectFit contain, flexShrink 0. The --brand-logo-bg background is NOT applied (no background div now). Keep the h1 "FamilySync" and the tagline p exactly as-is — the logo is decorative; the h1 remains the accessible page name. Do NOT modify LoginPage.tsx (the Phase 19 seam contract forbids it). Do NOT use dangerouslySetInnerHTML. In apps/pwa/src/styles/tokens.css, update --brand-logo-border-radius from 50% to the value the operator approved at the 17-02 checkpoint (recorded in 17-02-SUMMARY — likely 0 if the SVG draws its own rounded shape, or 12px for a square mark; per RESEARCH Pitfall 5, leaving it at 50% would clip the logo into a circle). This is the only tokens.css edit in this task; it goes inside the combined :root, [data-theme="light"] block. grep -q 'src="/logo.svg"' apps/pwa/src/components/BrandSlot.tsx && grep -q 'alt=""' apps/pwa/src/components/BrandSlot.tsx && grep -q 'FamilySync' apps/pwa/src/components/BrandSlot.tsx && ! grep -q 'dangerouslySetInnerHTML' apps/pwa/src/components/BrandSlot.tsx && pnpm --filter @familysync/pwa build - BrandSlot renders a decorative logo image (src "/logo.svg", empty alt, aria-hidden); the h1 "FamilySync" and tagline p are unchanged. - --brand-logo-border-radius in tokens.css equals the operator-approved value from 17-02-SUMMARY (not the old 50%, unless the operator explicitly chose circular). - LoginPage.tsx is NOT in this plan's diff (seam contract honored). - playwright-cli on /login: the logo renders, no layout shift vs the placeholder (aspect-ratio 1/1 + explicit width), h1 "FamilySync" still present as text. Observation noted in SUMMARY. - pnpm --filter @familysync/pwa build exits 0. BrandSlot shows the real decorative logo via the seam with no LoginPage change, the border-radius token matches the approved shape, and the login view has no layout shift. Task 2: Wire index.html favicons + theme-color and fix the vite.config.ts manifest maskable + accent apps/pwa/index.html, apps/pwa/vite.config.ts, apps/pwa/src/styles/tokens.css - apps/pwa/index.html (current: one apple-touch-icon link, theme-color #4A90D9, no rel=icon links) - apps/pwa/vite.config.ts lines ~33 (theme_color) and ~38-42 (the icons array; the last entry reuses icon-512.png for maskable — the defect) - 17-UI-SPEC.md section "Workstream B" -> "index.html wiring contract" and "vite.config.ts manifest wiring contract" (exact link set + icons[] + theme_color rules) - 17-PATTERNS.md sections "apps/pwa/index.html" and "apps/pwa/vite.config.ts" (before/after excerpts) - 17-02-SUMMARY.md (the operator-selected accent hex from the checkpoint — applies to index.html theme-color + manifest theme_color; tokens.css --color-member-0 only if Variant B) - 17-UI-SPEC.md section "Color" -> "Brand accent checkpoint" (which files flip for the accent) In apps/pwa/index.html, add the favicon links in order (SVG first for modern browsers, ICO second for legacy): a rel=icon link to /favicon.svg with type image/svg+xml, then a rel=icon link to /favicon.ico with sizes "any". Update the existing apple-touch-icon link to include sizes "180x180". Add the apple-mobile-web-app meta tags per UI-SPEC (capable yes, status-bar-style default, title FamilySync). Set the theme-color meta content to the operator-approved accent hex from 17-02-SUMMARY (#4A90D9 if Variant A was kept). In apps/pwa/vite.config.ts, fix the manifest icons array so the maskable entry references the SEPARATE /icon-maskable-512.png file (src /icon-maskable-512.png, sizes 512x512, type image/png, purpose maskable) — remove the defective reuse of /icon-512.png for the maskable purpose. Keep the /icon-192.png and /icon-512.png (purpose any) entries. Update the manifest theme_color to the same approved accent hex so it matches index.html. If the operator selected the warm accent (Variant B) at the 17-02 checkpoint, ALSO update --color-member-0 in tokens.css to the chosen hex (this is the only additional tokens.css edit, inside the combined block; --sx-color-primary already maps to --color-member-0 and follows automatically). If Variant A was kept, no tokens.css color edit is needed. grep -q 'favicon.svg' apps/pwa/index.html && grep -q 'favicon.ico' apps/pwa/index.html && grep -q 'icon-maskable-512.png' apps/pwa/vite.config.ts && ! grep -E "/icon-512.png'.*maskable|maskable.*/icon-512.png'" apps/pwa/vite.config.ts && pnpm --filter @familysync/pwa build - index.html contains rel=icon links to /favicon.svg (svg first) and /favicon.ico (sizes any), plus the apple-touch-icon with sizes 180x180. - index.html theme-color content equals the approved accent hex; vite.config.ts manifest theme_color matches it. - vite.config.ts manifest maskable entry references /icon-maskable-512.png (the separate file), and no manifest entry uses /icon-512.png with purpose maskable. - If Variant B accent was chosen: tokens.css --color-member-0 updated to the chosen hex; otherwise tokens.css color unchanged. - pnpm --filter @familysync/pwa build exits 0; playwright-cli on / confirms manifest icon links resolve (no 404). Observation noted in SUMMARY. index.html links the full favicon set with the approved theme-color, the manifest references the proper separate maskable icon and matching theme_color, the accent is applied consistently, and the build passes. ## Trust Boundaries | Boundary | Description | |----------|-------------| | (none new) | Wiring static asset references + an img element + token values. No runtime data flow, no user input, no new endpoints. | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-17-04-01 | Tampering | BrandSlot img / index.html links | accept | The logo img is decorative with empty alt; no dangerouslySetInnerHTML (T-05-24 invariant maintained); favicon links and manifest entries point at committed static files. No new threat above LOW for Workstream B wiring. | No new high/medium-severity threats. Static asset wiring + token value edits; no executable content, no input surface. - grep favicon.svg / favicon.ico in apps/pwa/index.html — links present - grep icon-maskable-512.png in apps/pwa/vite.config.ts — maskable references the separate file - no manifest entry uses icon-512.png for maskable purpose - grep src="/logo.svg" + alt="" in BrandSlot.tsx; no dangerouslySetInnerHTML - pnpm --filter @familysync/pwa build — exits 0 - playwright-cli /login (logo, no layout shift, h1 intact) + / (manifest icons resolve) The real logo is wired through the BrandSlot seam with no LoginPage change, the full favicon set + theme-color are in index.html, the manifest references the proper separate maskable icon, the approved accent + logo border-radius are applied consistently, and the build is green. Create `.planning/phases/17-ui-optimization-polish/17-04-SUMMARY.md` when done.