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 && (