Files
novalon-website/CLAUDE.md
T
zhangxiang 0bd1e9a7e2 feat(theme): 主题三态偏好模型,默认跟随系统自动切换暗黑模式
- 新增 src/lib/theme.ts:三态偏好单一真源
  (system/light/dark,无存储值默认跟随系统)
- ThemeToggle 改三态循环(Monitor→Sun→Moon):
  实时监听 prefers-color-scheme(仅 system 档生效)
  + storage 跨标签页同步;尺寸位置不变
- layout.tsx FOUC 内联脚本支持 system 档,
  首帧前解析避免闪烁
- e2e 视觉回归 L2 改用 test.use({ colorScheme }) 驱动
  (事后 setAttribute 存在被挂载效果覆盖的竞态)
- header.test.tsx lucide 白名单补 Monitor
- CLAUDE.md 同步三态语义与 variant 踩坑
- 新增 scripts/audit/theme-system-verify.mjs
  (55 断言:首帧无闪烁/循环/实时跟随/旧值兼容/
  污染回退/6 路由冒烟)

验证:单测 131 套件 1662 通过 0 失败;
tsc 0 错;eslint 0 错;next build 通过;
运行时 55/55 PASS(dogfood-output/theme-system-verify/)
2026-09-20 11:50:58 +08:00

211 lines
14 KiB
Markdown

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Common Commands
```bash
# Development
npm run dev # Start dev server on port 3000
npm run dev:clean # Clean .next/dist then start dev server
# Build & Preview
npm run build # Build production files to dist/
npm run build:clean # Clean then build
npm run preview # Serve dist/ on port 3000 (npx serve)
# Deploy (统一发布脚本)
./scripts/deploy.sh build # 构建静态产物
./scripts/deploy.sh deploy # 构建并发布到生产服务器
./scripts/deploy.sh deploy --skip-build # 使用现有 dist/ 直接发布
./scripts/deploy.sh rollback # 回滚到最近一次远程备份
./scripts/deploy.sh status # 查看生产环境发布状态
# Linting & Type Checking
npm run lint # ESLint (configured in config/lint/.eslintrc.json)
npm run type-check # tsc --noEmit
# E2E Testing (Playwright)
npm run test # Run all E2E tests
npm run test:e2e # Same as above
npm run test:smoke # Only @smoke-tagged tests
npm run test:visual # Visual regression (Desktop Chrome)
npm run test:visual:all # Visual regression (all projects)
npm run test:visual:update # Update visual snapshots
npx playwright test --grep "test name" # Run a single E2E test by name
# Unit Testing (Jest)
npm run test:unit # Run all unit tests
npm run test:coverage # Coverage report (thresholds: 80% branches/functions/lines)
npx jest --testPathPattern="button" # Run a single test file matching pattern
# Quality Checks
npm run check:contrast # Color contrast audit
npm run check:headings # Heading hierarchy audit
npm run lighthouse # Lighthouse CI performance audit
# Database (Prisma + SQLite)
npm run db:seed # Seed the dev database
npm run db:reset # Reset database with migrations
```
## Architecture
### Tech Stack
- **Next.js 14** (App Router) with **hybrid rendering** — static pages + API routes (Note: `output: 'export'` was recently removed; the project is transitioning away from pure static export)
- **React 18**, **TypeScript 5** (strict mode with `noUncheckedIndexedAccess`)
- **Tailwind CSS 3** with design tokens exposed as CSS custom properties (all tokenized via `var()` references in `tailwind.config.js`)
- **Framer Motion** for animations, **Lucide React** for icons, **Zod** for validation
- **shadcn/ui** pattern (Radix UI + class-variance-authority + tailwind-merge)
- **Prisma** with SQLite for backend data (admin, auth, CMS)
### Path Alias
`@/` maps to `src/` — configured in both `tsconfig.json` (`paths`) and `jest.config.js` (`moduleNameMapper`).
### TypeScript Strictness
Beyond `strict: true`, the project enables:
- `noUncheckedIndexedAccess: true` — array/object index access returns `T | undefined`, requiring null checks. This is a significant constraint to be aware of when writing code.
- `noImplicitReturns`, `noFallthroughCasesInSwitch`, `noUnusedLocals`, `noUnusedParameters`
### Route Structure (App Router)
```
src/app/
├── layout.tsx # Root layout: fonts, metadata, theme, analytics, SEO schemas
├── (marketing)/ # Route group — all public marketing pages
│ ├── layout.tsx # Shared: Header + Footer + PageTransition + ErrorBoundary
│ ├── page.tsx # Home (delegates to home-content-cms.tsx)
│ ├── about/ # About page (client.tsx)
│ ├── news/ # News list + [slug] detail
│ ├── contact/ # Contact form
│ ├── products/ # Products hub (HSI model)
│ │ ├── page.tsx # Product listing (enterprise suites + standalone)
│ │ ├── [id]/ # Product detail (four-layer narrative)
│ │ ├── standalone/[id]/ # Standalone product detail
│ │ ├── erp-upgrade/ # Specific product landing pages
│ │ └── erp-upgrade-v3/
│ ├── services/ # Services list + [id] detail
│ ├── solutions/ # Solutions list + [id] detail (cross-references products)
│ ├── cases/ # Case studies
│ └── team/ # Team page
├── api/
│ ├── admin/ # Admin API
│ ├── auth/ # Authentication API
│ ├── cms/ # CMS API routes (draft mode, revalidation)
│ └── contact/ # Contact form submission
├── privacy/, terms/ # Legal pages
└── fonts/ # Local font files (Geist Sans/Mono)
```
### Archiving Convention
When replacing a page or component with a new version, move the old one to an `_archive/` subdirectory (e.g., `src/app/(marketing)/_archive/` for old homepage iterations). The archive is excluded from TypeScript compilation via `tsconfig.json`.
### Dark Mode
Dark mode uses the `data-theme="dark"` HTML attribute (not Tailwind's `dark:` class). Tailwind is configured with `darkMode: ['variant', '[data-theme="dark"] &']` — use the **variant** form, not `['selector', ...]`, because the selector form is accepted by the config but JIT emits zero `dark:` rules under Turbopack + Next.js 16.
**Three-state preference model** (`src/lib/theme.ts` is the single source of truth):
| Stored value (`localStorage['novalon-theme']`) | Meaning |
|---|---|
| `'light'` / `'dark'` | Explicit user choice — always wins over the OS |
| `'system'` / absent | Follow OS `prefers-color-scheme` (**default for new visitors**) |
- `src/app/layout.tsx` has an inline `<script>` in `<head>` that resolves the preference and sets `data-theme` **before first paint** (FOUC prevention). It must stay semantically in sync with `src/lib/theme.ts` — change both together.
- `src/components/theme/theme-toggle.tsx` cycles `跟随系统 → 浅色 → 深色`. It listens to `matchMedia('(prefers-color-scheme: dark)')` for live OS changes (only effective in the `system` state) and to `storage` events for cross-tab sync.
- `html[data-theme]` is an **output** of preference resolution — never read it back as the user's preference, or the `system` state collapses into `dark` on dark-OS machines and the cycle deadlocks.
### HSI Information Architecture
The site follows a **Hub-Spoke-Independent** model (see `CONTEXT.md` and ADR-0002):
- **Hub**: `/products` — product catalog, split into "企业套装" (6 enterprise products) and "专业产品" (standalone)
- **Spoke**: `/solutions` — industry scenarios, each recommending product combinations from the Hub
- **Independent**: Standalone products in the specialized zone have their own narrative path
### Four-Layer Narrative Model
Every detail page (product/service/solution/standalone) follows the same four-layer structure:
1. **L1 Hero**: Emotional entry with visual, title, value prop, status badge
2. **L2 Value Rationale**: Product-specific — features/benefits for products, pain-points→architecture for solutions, challenges→results for services
3. **L3 Trust Proof**: Case studies, data proofs, certifications, testimonials (currently stubbed — company is pre-launch)
4. **L4 CTA Conversion**: Primary action + secondary action + cross-recommendations
The detail components implementing this live in `src/components/detail/`:
- `detail-hero.tsx`, `detail-product-value.tsx`, `detail-trust-section.tsx`, `detail-cta-section.tsx`
- `solution-value.tsx`, `service-value.tsx` (type-specific L2 variants)
- `detail-cross-recommend.tsx` (cross-links between products↔solutions↔services)
### CMS Content Architecture
The app has a **mock CMS layer** at `src/lib/cms/` that simulates a headless CMS (no real backend — designed to be swapped with real API calls later):
- `types.ts` — ContentModel, ContentItem, ContentZone, ThemeConfig definitions
- `ContentZoneRenderer.tsx` — Renders a zone's items in grid/list/carousel layouts, delegating each item to `getItemRenderer(modelCode)` from the component registry
- `mock-home.ts` — Mock data for the homepage zones
- `component-registry.ts` — Maps model codes to React renderer components
The homepage (`home-content-cms.tsx`) initializes the CMS and fetches mock content zones (hero, stats, services, solutions, cases, news).
### Design Token System
All visual tokens are defined as **CSS custom properties** in `src/app/globals.css` (`:root` block) and mapped into Tailwind's config via `var()` references:
- **Colors**: `--color-ink` (deep charcoal #0A0E14), `--color-brand` (vermilion #C41E3A), accent colors for service-coding (blue, teal, amber, purple), functional colors (success, warning, error, info), dark-section overrides
- **Typography**: `--font-size-*`, `--line-height-*`, `--letter-spacing-*` all mapped to Tailwind's `fontSize`/`lineHeight`/`letterSpacing` scales
- **Spacing, radius, shadows** all tokenized through CSS variables
- **Transitions**: `--transition-fast/normal/slow`, `--ease-ink` (cubic-bezier), `--ease-sharp`, `--ease-spring-soft`
- **Dark mode**: `data-theme="dark"` attribute (set via inline script before paint to prevent flash); flipped by the `[data-theme='dark']` override block in `globals.css`, which only re-maps CSS variables — components do not restyle per element
### Design DNA Framework
The site follows a three-dimensional design system (see `CONTEXT.md`):
- **Dimension 1 — Design System**: Quantifiable tokens (colors, typography, spacing, radius, shadows, motion, components)
- **Dimension 2 — Design Style**: Qualitative perception (atmosphere, visual language, composition, imagery, brand tone)
- **Dimension 3 — Visual Effects**: Scroll animations, micro-interactions, parallax, SVG effects
The **Consulting Professional** aesthetic (inspired by Accenture + Bain) is the primary skeleton. **Ink cultural elements** (水墨) are secondary decorations limited to ≤6 locations (logo, dividers, transition animations, footer texture).
**Brand red (#C41E3A) usage rule**: Every page must have ≥3 brand-red touchpoints. It must never be used as paragraph text color, large backgrounds, or alongside accent colors in the same card. Area ≤10%.
**Motion design**: Animations are purposeful (not decorative), fast (150-300ms), use `ease-ink` [0.22, 1, 0.36, 1] as default easing, and stagger children by 30-60ms. No continuous looping animations except pulse-soft for skeletons. No spring easings for content entry. No animations >700ms.
### Data Layer
All structured content data is in `src/lib/constants/` as TypeScript constants — no API/database for marketing content:
- `products.ts` — 6 enterprise products (ERP, CRM, CMS, BI, SDS, OA) + standalone products (NovaVis)
- `services.ts`, `solutions.ts` — Service and solution definitions
- `cases.ts` — Case studies with industry filtering
- `navigation.ts` — Nav structure + mega dropdown data
- `company.ts`, `stats.ts`, `team.ts`, `news.ts`, `methodology.ts`
- `hero-themes.ts` — Per-product hero visual theme variants
- `cross-references.ts` — Cross-links between products/solutions/services
Each product/solution/service implements the `Product`/`Solution`/`Service` interface which includes `caseStudies[]`, `dataProofs[]`, `certifications[]`, etc. for the four-layer narrative.
### Backend Layer (Prisma + API Routes)
The project has a backend layer for admin/auth/CMS functionality:
- **Prisma** with SQLite (`prisma/dev.db`) for admin user data, auth, and CMS content management
- API routes at `src/app/api/admin/`, `auth/`, `cms/`, `contact/`
- The marketing pages use mock data from `src/lib/cms/mock-home.ts`, but the CMS API routes suggest a real CMS backend is planned
### Component Organization
```
src/components/
├── ui/ # Base: Button, Card, Input, Badge, ScrollReveal, AnimatedCounter, etc.
├── layout/ # Header, Footer, MobileTabBar, Breadcrumb, MegaDropdown, MobileMenu
├── sections/ # Page section components: HeroSectionV2, ServiceGrid, CTASection, etc.
├── detail/ # Four-layer narrative: DetailHero, ProductValueSection, DetailTrustSection, etc.
├── seo/ # Structured data (OrganizationSchema, WebsiteSchema, etc.)
├── analytics/ # GA4, error tracking, cookie consent, scroll depth, outbound links
├── cms/ # CMS renderers (ContentRenderer, SectionRenderer, FieldRenderer)
├── content/ # sections.tsx, testimonials.tsx
└── providers/ # (currently empty)
```
### Testing Setup
- **Jest** (unit): Tests alongside source in `src/**/*.test.{ts,tsx}`, coverage threshold 80%. Config in `config/test/jest.config.js`. Uses `@/` path alias and ts-jest with `jsx: 'react-jsx'` transform (since tsconfig uses `'preserve'`). Run single tests with `npx jest --testPathPattern="component-name"`.
- **Playwright** (E2E): Tests in `e2e/`, config in `e2e/playwright.config.ts`. Auto-starts `npm run preview` as web server. Visual regression snapshots in `e2e/visual-snapshots/`. Three browser projects (chromium, firefox, webkit) plus dedicated visual regression projects at desktop/tablet/mobile breakpoints. Run single tests with `npx playwright test --grep "test name"`.
### Build Output
The project builds to `dist/` (configured via `distDir` in `next.config.mjs`). It is served via Nginx (see `nginx-static-production.conf`) with CDN support (`assetPrefix` respects `CDN_DOMAIN` env var). Images are unoptimized (static export limitation). Note: `output: 'export'` was recently removed from the config — the project may be moving toward a hybrid SSR + static model.
### Commit Convention
Uses Conventional Commits with commitlint (`@commitlint/config-conventional`). Husky + lint-staged enforces linting on pre-commit.
### Component Versioning History
The project has gone through multiple design iterations. Older component versions:
- `components/detail-v2/` — previous iteration (deleted per git status, migration to `detail/` completed)
- `components/detail-v3/` — another iteration (deleted, same reason)
- `home-content-v2.tsx` through `home-content-v11.tsx` — archived homepage iterations in `(marketing)/_archive/`
- The current canonical components are in `components/detail/` and `home-content-cms.tsx`