From b16ab3c294cddcd0e39cc2da17eb65d81ad27b59 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:50:19 +0200 Subject: [PATCH] refactor(chrome): one SSOT per chrome shape (Panel, BottomNav, nav-state) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Header/footer/nav/panels now follow one design + code pattern instead of ~30 hand-rolled variants. Establishes three primitives and migrates the highest-value duplications onto them. New SSOT primitives: - lib/design/nav.ts — NAV_STATE + navLinkClass(): the ONE definition of active/inactive per nav shape (sidebar/bottomTab/pill). Kills the ring-action/20-vs-/30-vs-inline fragmentation. admin-ui.navActive re-exports from it. - components/ui/Panel.tsx — titled card-shell surface (title/subtitle/ icon/action header slot). Replaces hand-rolled "bg-surface-base rounded-lg border p-5" + bespoke

. - components/layout/BottomNav.tsx — the byte-identical admin+dashboard mobile tab bar, in a neutral home (dashboard can't import app/admin). Consolidations: - Fold redundant ResponsiveHero into PageHero (1 consumer migrated; drops its inline-style dot-pattern, from-black gradient, color-ladder). Deleted. - 6 panels -> ; RequestSidebar/TimecardHistorySidebar -> . - Both bottom navs -> ; dashboard sheet pills -> navLinkClass('pill'). - AdminSidebar: 4x-repeated item -> navLinkClass('sidebar'); harmonize Hirn drift. - NavItem: consume designPrimitive.focus (drop divergent inline ring). - Fix duplicate-class "border border" typos in Contributions/Needs panels. Docs: CLAUDE.md "Chrome & Navigation SSOT" section + audit greps. Net -174 lines. verify green (typecheck, lint 0 err, i18n 16/16). Hero migration visually confirmed light+dark. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 49 ++++++ src/app/admin/AdminSidebar.tsx | 25 +-- src/app/admin/decisions/[id]/ResultsPanel.tsx | 7 +- .../services/open-source-solutions/page.tsx | 8 +- src/components/admin/MobileBottomNav.tsx | 61 ++----- .../admin/projects/ContributionsPanel.tsx | 18 +- src/components/admin/projects/NeedsPanel.tsx | 23 ++- src/components/admin/teams/TeamWorkPanels.tsx | 35 ++-- .../dashboard/DashboardMobileNav.tsx | 55 ++---- .../timecards/TimecardHistorySidebar.tsx | 9 +- src/components/decisions/VotingPanel.tsx | 10 +- .../it-hilfe/detail/RequestSidebar.tsx | 21 +-- src/components/layout/BottomNav.tsx | 73 ++++++++ src/components/layout/ResponsiveHero.tsx | 162 ------------------ src/components/layout/header/NavItem.tsx | 7 +- src/components/ui/Panel.tsx | 72 ++++++++ src/lib/admin-ui.ts | 5 +- src/lib/design-system.ts | 3 + src/lib/design/nav.ts | 54 ++++++ 19 files changed, 361 insertions(+), 336 deletions(-) create mode 100644 src/components/layout/BottomNav.tsx delete mode 100644 src/components/layout/ResponsiveHero.tsx create mode 100644 src/components/ui/Panel.tsx create mode 100644 src/lib/design/nav.ts diff --git a/CLAUDE.md b/CLAUDE.md index e89460701..3d2642c4f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,6 +137,46 @@ brand.*: social (mastodon, linkedin, facebook) — bg-brand-mastodon etc. | Section BG white | `bg-white` (dark auto-handled) | `bg-white dark:bg-neutral-950` (unless deeper black needed) | | Card hover | `hover:border-neutral-300` | `hover:shadow-xl` | +### Chrome & Navigation — one primitive per shape (SSOT) + +"Chrome" = the repeated structural shells: headers, footers, nav bars, panels, +sidebars, bottom nav. Each SHAPE has exactly ONE primitive. Never hand-roll a +card shell (`bg-surface-base rounded-lg border p-…`) or an active-nav class +(`bg-action/10 … ring-action/20`, `text-action`, `border-action bg-action-muted`) +inline — those are defined once and consumed everywhere. Retheming chrome = edit +the primitive, not N components. + +| Need | Use | File | Never hand-roll | +|---|---|---|---| +| Titled bordered surface (title + subtitle + right action + body) | `` | `components/ui/Panel.tsx` | `
` + own `

` | +| Untitled surface | `` | `components/ui/card.tsx` | `
` / `rounded-xl border bg-surface-base` | +| Header row *inside* a card | `` | `components/admin/AdminSectionHeader.tsx` | bespoke `flex justify-between` + divider | +| Full-bleed page hero (centered) | `` | `components/layout/PageHero.tsx` | a one-off `
` with a bespoke `

` type string | +| Page-section vertical rhythm / tinted band | `
` | `components/layout/Section.tsx` | hardcoded `py-16 sm:py-20 …` | +| Mobile bottom tab bar (`` | `components/layout/BottomNav.tsx` | a `fixed bottom-0 … flex` `

+ ); } diff --git a/src/app/services/open-source-solutions/page.tsx b/src/app/services/open-source-solutions/page.tsx index c9ccabc56..c121765a2 100644 --- a/src/app/services/open-source-solutions/page.tsx +++ b/src/app/services/open-source-solutions/page.tsx @@ -1,5 +1,5 @@ import { Metadata } from 'next' -import { ResponsiveHero } from '@/components/layout/ResponsiveHero' +import { PageHero } from '@/components/layout/PageHero' import { BenefitsSection } from './components/BenefitsSection' import { ServicesSection } from './components/ServicesSection' import { ComparisonSection } from './components/ComparisonSection' @@ -24,10 +24,10 @@ export default function OpenSourceSolutionsPage() { return (
{/* Hero Section */} - {/* Benefits Section */} diff --git a/src/components/admin/MobileBottomNav.tsx b/src/components/admin/MobileBottomNav.tsx index f27e1aaec..43e5e4236 100644 --- a/src/components/admin/MobileBottomNav.tsx +++ b/src/components/admin/MobileBottomNav.tsx @@ -1,9 +1,8 @@ 'use client' import { useTranslations } from 'next-intl' -import { Link, usePathname } from '@/i18n/navigation' -import { Menu } from 'lucide-react' -import { Button } from '@/components/ui/button' +import { usePathname } from '@/i18n/navigation' +import { BottomNav } from '@/components/layout/BottomNav' import { getMobileBottomNavSections } from '@/config/sections' import { sectionShortLabel } from '@/lib/section-labels' @@ -24,49 +23,25 @@ interface MobileBottomNavProps { */ export function MobileBottomNav({ accessibleSections, onMenuClick }: MobileBottomNavProps) { const pathname = usePathname() - const items = getMobileBottomNavSections(accessibleSections) + const sections = getMobileBottomNavSections(accessibleSections) const t = useTranslations('admin.sidebar') const tSections = useTranslations('admin.sections') - return ( - + return ( + ) } diff --git a/src/components/admin/projects/ContributionsPanel.tsx b/src/components/admin/projects/ContributionsPanel.tsx index 8a83e9f2f..9f920ca53 100644 --- a/src/components/admin/projects/ContributionsPanel.tsx +++ b/src/components/admin/projects/ContributionsPanel.tsx @@ -12,6 +12,7 @@ import { toast } from 'sonner' import { apiFetch } from '@/lib/api/client' import { cn } from '@/lib/utils' import { designPrimitive } from '@/lib/design-system' +import { Panel } from '@/components/ui/Panel' import { Button } from '@/components/ui/button' import { Select } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' @@ -82,12 +83,10 @@ export function ContributionsPanel({ slug, initialContributions, needs }: Props) } return ( -
-
-
-

{t('contributions.title')}

-

{t('contributions.subtitle')}

-
+ setFilter(e.target.value as ContributionStatus | 'all')} @@ -98,7 +97,8 @@ export function ContributionsPanel({ slug, initialContributions, needs }: Props) ))} -
+ } + > {filtered.length === 0 ? (

@@ -111,7 +111,7 @@ export function ContributionsPanel({ slug, initialContributions, needs }: Props) {filtered.map(c => { const need = c.needId ? needById[c.needId] : null return ( -

+
@@ -187,6 +187,6 @@ export function ContributionsPanel({ slug, initialContributions, needs }: Props) })}
)} -
+ ) } diff --git a/src/components/admin/projects/NeedsPanel.tsx b/src/components/admin/projects/NeedsPanel.tsx index b0278c507..d89ae7005 100644 --- a/src/components/admin/projects/NeedsPanel.tsx +++ b/src/components/admin/projects/NeedsPanel.tsx @@ -11,8 +11,7 @@ import { useState, useTransition, type FormEvent } from 'react' import { useTranslations } from 'next-intl' import { toast } from 'sonner' import { apiFetch } from '@/lib/api/client' -import { cn } from '@/lib/utils' -import { designPrimitive } from '@/lib/design-system' +import { Panel } from '@/components/ui/Panel' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Select } from '@/components/ui/select' @@ -108,12 +107,10 @@ export function NeedsPanel({ slug, initialNeeds }: Props) { } return ( -
-
-
-

{t('needs.title')}

-

{t('needs.subtitle')}

-
+ {t('needs.new')} -
+ } + className="mb-5" + > {adding && (