diff --git a/.claude/rules/charts.md b/.claude/rules/charts.md new file mode 100644 index 000000000..6d34a3187 --- /dev/null +++ b/.claude/rules/charts.md @@ -0,0 +1,85 @@ +# Charts + +## Visual Standards + +**Axis Lines:** + +- **ALWAYS** render both y-axis and x-axis lines unless specifically not needed +- Set `axisLine: { show: true }` for both xAxis and yAxis + +**Grid Lines:** + +- **NEVER** render grid lines +- Set `splitLine: { show: false }` for both xAxis and yAxis + +**Axis Ranges & Intervals:** + +- yMax/xMax must work with consistent, evenly-spaced intervals +- BAD: `0, 200, 400, 600, 800, 1000, 1100` (inconsistent final interval) +- GOOD: `0, 200, 400, 600, 800, 1000` OR `0, 250, 500, 750, 1000, 1250` +- Prefer setting yMax/xMax with `splitNumber` and let ECharts calculate intervals automatically +- Example: `yAxis: { max: 1000, splitNumber: 5 }` → generates [0, 200, 400, 600, 800, 1000] + +**Grid Padding:** + +- Minimize whitespace while ensuring labels are visible +- **Use simple explicit padding values** and let ECharts handle label positioning: + - Set `left`, `right`, `top`, `bottom` values + - No need for `containLabel`, `align`, `inside`, `margin` - let ECharts use defaults +- **Standard padding values:** + - **Left**: 60px (for y-axis labels and name) + - **Right**: 24px (minimal, 80px if dual y-axes present) + - **Top**: 16px (40px if ECharts title is used) + - **Bottom**: 50px (90px if native legend at bottom) +- Override only when absolutely necessary (e.g., dual y-axes, legends, special layouts) +- Wrappers should never override grid config unless they have very specific layout needs + +**Slot Time Formatting:** + +- When plotting slot time on xAxis: + - **Title**: Must be "Slot Time (s)" + - **Units**: Use seconds (not milliseconds) + - **Precision**: Round to the nearest second + - **Ticks**: Default ticks at 0, 4s, 8s, 12s (unless otherwise specified) + - **Format**: Display as "0", "4", "8", "12" (numbers only, no "s" suffix in tick labels) + +## Component Architecture + +**Core Chart Components** (`src/components/Charts/`): + +- Define sensible defaults for all visual standards above +- Provide consistent `gridConfig`, axis styling, and formatting +- Should be production-ready without overrides + +**Wrapper Components** (page-scoped chart components): + +- **Defer to core components** for most settings +- Only override when directly needed for page-specific requirements +- **Do NOT override** `gridConfig` unless absolutely necessary +- Pass through props to core components rather than recreating configuration + +**Example Pattern:** + +```tsx +// BAD: Wrapper recreates all config + + +// GOOD: Wrapper defers to core component + +``` + +## Shared Crosshairs + +Charts sharing the same x-axis can synchronize tooltips and crosshairs using the `syncGroup` prop: + +- Use `syncGroup="slot-time"` for charts with slot time x-axis (0-12s) +- Use `syncGroup="slot-number"` for charts with slot number x-axis +- Omit `syncGroup` for independent charts diff --git a/.claude/rules/forms.md b/.claude/rules/forms.md new file mode 100644 index 000000000..8b77f592a --- /dev/null +++ b/.claude/rules/forms.md @@ -0,0 +1,36 @@ +# Form Management + +## Zod & URL Search Params + +Zod schemas are used for **TanStack Router search param validation**, not react-hook-form resolvers: + +```tsx +// In route files (e.g., src/routes/ethereum/slots/$slot.tsx) +const slotSearchSchema = z.object({ + network: z.string().optional(), + tab: z.enum(['overview', 'attestations']).optional(), +}); + +export const Route = createFileRoute('/ethereum/slots/$slot')({ + validateSearch: slotSearchSchema, +}); +``` + +Type the search params in page components: + +```tsx +type SearchParams = z.infer; +``` + +## react-hook-form + +- `useForm` is used for simple form state management (filters, inputs) +- One `FormProvider` per page when sharing form state across child components +- Child components access form state via `useFormContext()` +- No Zod resolvers are used with react-hook-form — Zod is for route validation only + +## Placement + +- Generic filter components → `src/components/Forms/` +- Page-specific form components → `src/pages/[section]/[page-name]/components/` +- Search param types → `src/pages/[section]/[page-name]/IndexPage.types.ts` diff --git a/.claude/rules/seo.md b/.claude/rules/seo.md new file mode 100644 index 000000000..d0f991b55 --- /dev/null +++ b/.claude/rules/seo.md @@ -0,0 +1,35 @@ +# Head Meta & SEO + +## Meta Hierarchy + +- Base meta tags defined in `src/routes/__root.tsx` +- Routes override with `head: () => ({ meta: [...] })` +- Child routes inherit and can extend parent meta +- **No variables in head**: Only use literals and `import.meta.env.VITE_*` (processed by build plugin) + +## Page Feature Images + +Each page should have a feature image at `public/images/[section]/[page-name].png`. + +**Route implementation:** + +```tsx +// In routes/[section]/[page-name].tsx +head: () => ({ + meta: [ + { title: `Page Name | ${import.meta.env.VITE_BASE_TITLE}` }, + { name: 'description', content: 'Unique description of what this page does' }, + { property: 'og:image', content: '/images/[section]/[page-name].png' }, + { property: 'og:description', content: 'Unique description of what this page does' }, + { name: 'twitter:image', content: '/images/[section]/[page-name].png' }, + { name: 'twitter:description', content: 'Unique description of what this page does' }, + ], +}) +``` + +## Checklist + +- Always include page-specific title +- Write unique description for each page +- Update all three descriptions (meta, og:description, twitter:description) +- Provide unique feature image per page diff --git a/.claude/rules/storybook.md b/.claude/rules/storybook.md new file mode 100644 index 000000000..de236dba8 --- /dev/null +++ b/.claude/rules/storybook.md @@ -0,0 +1,15 @@ +# Storybook + +When creating a new story, add the following decorators: + +```tsx +decorators: [ + Story => ( +
+ +
+ ), +], +``` + +When choosing a title, use the full nested path to the story, e.g. `Components/Layout/Container`. diff --git a/.claude/rules/theming.md b/.claude/rules/theming.md new file mode 100644 index 000000000..b18aff66b --- /dev/null +++ b/.claude/rules/theming.md @@ -0,0 +1,28 @@ +# Theming + +Theme uses a **two-tier color architecture** defined in `src/index.css`: + +**Tier 1:** Primitive scales (terracotta, sand, neutral) with 50-950 shades +**Tier 2:** Semantic tokens that reference Tier 1 + +## Semantic Tokens + +- `primary`, `secondary`, `accent` - Brand colors +- `background`, `surface`, `foreground`, `muted`, `border` - UI colors +- `success`, `warning`, `danger` - State colors + +## Usage + +```tsx +// Always use semantic tokens +className="bg-primary text-foreground border-border" +className="hover:bg-accent text-muted" + +// Programmatic access +import { useThemeColors } from '@/hooks/useThemeColors'; +const colors = useThemeColors(); // { primary, background, ... } +``` + +**Never use primitive scales directly** (`bg-terracotta-500`, `bg-sand-100`) - only semantic tokens. + +**Modify theme:** Edit semantic mappings in `src/index.css` at `@layer base` (`:root` for light, `html.dark` for dark). diff --git a/CLAUDE.md b/CLAUDE.md index c281ec924..e4547be3a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,180 +15,98 @@ The `BACKEND` env var controls the API proxy target. Values: `local` (default), ## Libraries -- pnpm v10 -- node v24 -- vite v7 -- react v19 -- typescript v5 -- tailwindcss v4 -- @tanstack/react-query v5 -- @tanstack/router-plugin v1 -- zod v4 -- heroicons v2 -- headlessui v2 -- storybook v9 -- vitest v4 -- react-hook-form v7 -- echarts v6 -- echarts-for-react v3 -- echarts-gl v2 +- pnpm v10, node v24, vite v7, react v19, typescript v5 +- tailwindcss v4, headlessui v2, heroicons v2 +- @tanstack/react-query v5, @tanstack/router-plugin v1 +- zod v4, react-hook-form v7, clsx +- echarts v6, echarts-for-react v3, echarts-gl v2 +- storybook v10, vitest v4 ## Project Structure ```bash -vite.config.ts # Vite configuration -package.json # Dependencies and scripts -public/ # Public assets - images/ - [section]/ # Section-specific images - [page-name].png # Feature images for social sharing -.storybook/ # Storybook configuration src/ - routes/ # Route definitions using TanStack Router + routes/ # Thin TanStack Router definitions __root.tsx # Root layout with sidebar, providers, navigation index.tsx # "/" - Landing page route - [section].tsx # Layout routes for sections + [section].tsx # Layout routes (ethereum, xatu, experiments) [section]/ # Section-specific routes [page-name].tsx # Page route - [page-name]/ # Nested page routes + [page-name]/ # Nested/sub-section routes index.tsx # Default nested route $param.tsx # Dynamic parameter route pages/ # Page components (actual UI implementation) - IndexPage.tsx # Landing page component + home/ # Landing page [section]/ # Section-specific pages [page-name]/ # Individual page folder IndexPage.tsx # Main page component - [OtherPage].tsx # Additional page components (e.g., DetailPage) + IndexPage.types.ts # Search param types, page-specific types + [OtherPage].tsx # Additional pages (e.g., DetailPage) index.ts # Barrel export components/ # Page-specific components - [ComponentName]/ - [ComponentName].tsx - [ComponentName].test.tsx - index.ts - hooks/ # Page-specific hooks (optional) - use[HookName].ts - contexts/ # Page-specific contexts (optional) - [ContextName].ts - providers/ # Page-specific providers (optional) - [ProviderName].tsx + hooks/ # Page-specific hooks + contexts/ # Page-specific contexts + providers/ # Page-specific providers + utils/ # Page-specific utilities + constants.ts # Page-specific constants components/ # Core, app-wide reusable UI components - [Category]/ # Component category folder - [ComponentName]/ # Individual component folder - [ComponentName].tsx # Component implementation - [ComponentName].test.tsx # Vitest tests - [ComponentName].types.ts # TypeScript types (optional) - [ComponentName].stories.tsx # Storybook stories - index.ts # Barrel export + [Category]/[ComponentName]/ # Category → Component folder + [ComponentName].tsx # Implementation + [ComponentName].test.tsx # Vitest tests + [ComponentName].types.ts # TypeScript types (optional) + [ComponentName].stories.tsx # Storybook stories + index.ts # Barrel export providers/ # React Context Providers - [ProviderName]/ # e.g., NetworkProvider, ThemeProvider - [ProviderName].tsx - index.ts - - contexts/ # React Context definitions - [ContextName]/ # e.g., NetworkContext, ThemeContext - [ContextName].ts - [ContextName].types.ts - index.ts - - hooks/ # Custom React hooks - use[HookName]/ # e.g., useNetwork, useConfig, useBeaconClock - use[HookName].ts - use[HookName].test.ts # Vitest tests - use[HookName].types.ts # Optional: for complex hooks - index.ts - + contexts/ # React Context definitions (with .types.ts) + hooks/ # Custom React hooks (with tests) api/ # Generated API client (do not edit) - @tanstack/ - react-query.gen.ts # TanStack Query hooks - USE THIS for all API calls - [other generated files] # Auto-generated client, types, schemas, etc. - + @tanstack/react-query.gen.ts # TanStack Query hooks - USE THIS for all API calls utils/ # Utility functions and helpers - [util-name].ts # Utility functions - [util-name].test.ts # Vitest tests - index.ts # Barrel export - - main.tsx # Application entry point - index.css # Global styles and Tailwind config - routeTree.gen.ts # Generated route tree (auto-generated) - vite-env.d.ts # Vite environment types + index.css # Global styles and Tailwind theme config ``` -## Architecture Patterns +## Architecture ### Component & State Philosophy **Core/Shared** (`src/components/`, `src/hooks/`, `src/contexts/`): - -- App-wide reusable building blocks -- Generic and configurable +- App-wide reusable building blocks — generic and configurable - Work in any context without page logic -- Compose from other core items when logical - Core components include Storybook stories -**Page-Scoped** (`src/pages/[section]/components|hooks|contexts|providers/`): - +**Page-Scoped** (`src/pages/[section]/[page-name]/components|hooks|contexts|providers|utils/`): - Used within specific page/section only - Compose/extend core items for page needs - Contain page-specific business logic -- Specialized for page requirements -- Keep complex page state isolated -### Core Component Categories +**Placement rule:** Used across pages → Core. Page-specific → Page-scoped. -Current categories in `src/components/`: +### Core Component Categories -- **Charts**: Data visualization (Globe, Line, Map) -- **DataDisplay**: Data presentation (Stats) -- **Elements**: Basic UI building blocks (Badge, Button, ButtonGroup) -- **Ethereum**: Blockchain-specific (ClientLogo, NetworkIcon, NetworkSelect) -- **Feedback**: User feedback (Alert) -- **Forms**: Form controls (Checkbox, CheckboxGroup, InputGroup, RadioGroup, SelectMenu, Toggle) -- **Layout**: Structure and layout (Card, Container, Divider, Header, LoadingContainer, Sidebar, ThemeToggle) -- **Lists**: Tables and lists (Table) -- **Navigation**: Navigation elements (ProgressBar, ProgressSteps) -- **Overlays**: Modals and overlays (ConfigGate, FatalError) +- **Charts**: Bar, BoxPlot, Donut, FlameGraph, GasFlowDiagram, Gauge, Globe, GridHeatmap, Heatmap, Line, Map, Map2D, MultiLine, Radar, ScatterAndLine, Sparkline, StackedBar +- **DataDisplay**: CardChain, GasTooltip, MiniStat, Stats, Timestamp +- **DataTable**: DataTable (compound component with sub-components) +- **DateTimePickers**: DatePicker +- **Elements**: Avatar, Badge, Button, ButtonGroup, CopyToClipboard, Dropdown, Icons, TimezoneToggle +- **Ethereum**: ~32 blockchain-specific components (ClientLogo, Entity, Epoch, ForkLabel, NetworkIcon, NetworkSelect, Slot, SlotTimeline, etc.) +- **Feedback**: Alert, InfoBox +- **Forms**: Checkbox, CheckboxGroup, Input, RadioGroup, RangeInput, SelectMenu, TagInput, Toggle +- **Layout**: Card, Container, Disclosure, Divider, Header, ListContainer, LoadingContainer, PopoutCard, ScrollArea, Sidebar, ThemeToggle +- **Lists**: ScrollingTimeline, Table +- **Navigation**: Breadcrumb, ProgressBar, ProgressSteps, ScrollableTabs, ScrollAnchor, Tab +- **Overlays**: ConfigGate, Dialog, FatalError, FeatureGate, NotFound, Notification, Popover ### Page Sections -Page sections in `src/pages/`: - -- **ethereum**: Ethereum-focused blockchain visualizations and data -- **xatu**: Xatu-specific data, metrics, and contributor insights -- **experiments**: Legacy experiments index page (shows all experiments with links to new structure) - -### Standard Component Structure - -``` -ComponentName/ - ComponentName.tsx # Main implementation - ComponentName.test.tsx # Vitest tests - ComponentName.types.ts # TypeScript types (when needed) - ComponentName.stories.tsx # Storybook stories - index.ts # Barrel export -``` - -### Routing & Pages +- **home**: Landing page +- **ethereum**: Blockchain visualizations (sub-sections: consensus, contracts, data-availability, entities, epochs, execution, forks, slots, validators) +- **xatu**: Xatu data, metrics, contributor insights +- **experiments**: Legacy experiments index -- Route files in `src/routes/` - thin TanStack Router definitions -- Page components in `src/pages/` - UI implementations -- Page-scoped components in `pages/[section]/components/` -- Dynamic routes use `$param.tsx` syntax -- Always barrel export via `index.tsx` - -### State Management - -- Contexts in `src/contexts/` with types -- Providers in `src/providers/` wrap app/features -- Hooks in `src/hooks/` for context access and shared logic - -### API Integration - -- Auto-generated code in `src/api/` - do not edit -- Use hooks from `@/api/@tanstack/react-query.gen.ts` -- Hooks handle fetching, caching, state management +Route directories also include `beacon/` and `xatu-data/` alongside the sections above. ## Development Guidelines @@ -197,323 +115,60 @@ ComponentName/ - **New page**: Route in `src/routes/[section]/`, page in `src/pages/[section]/[page-name]/` - **Feature image**: `public/images/[section]/[page-name].png` for social sharing - **Skeleton component**: `src/pages/[section]/[page-name]/components/[PageName]Skeleton/` using `LoadingContainer` -- **Core component**: `src/components/[Category]/[ComponentName]/` - reusable, generic -- **Page-scoped component**: `src/pages/[section]/[page-name]/components/[ComponentName]/` - page-specific -- **Core hook**: `src/hooks/use[HookName]/` - app-wide logic -- **Page-scoped hook**: `src/pages/[section]/[page-name]/hooks/use[HookName].ts` - page-specific logic -- **Utility function**: `src/utils/[util-name].ts` - helper functions +- **Core component**: `src/components/[Category]/[ComponentName]/` +- **Page-scoped component**: `src/pages/[section]/[page-name]/components/[ComponentName]/` +- **Core hook**: `src/hooks/use[HookName]/` +- **Page-scoped hook**: `src/pages/[section]/[page-name]/hooks/use[HookName].ts` +- **Utility function**: `src/utils/[util-name].ts` - **Core context**: `src/contexts/[ContextName]/` with provider in `src/providers/[ProviderName]/` - **Page-scoped context**: `src/pages/[section]/[page-name]/contexts/[ContextName].ts` with local provider -### Placement Decision - -**Components/Hooks/Contexts:** - -- Used across pages → Core (`src/components/`, `src/hooks/`, `src/contexts/`) -- Page-specific logic → Page-scoped (`src/pages/[section]/components|hooks|contexts/`) -- Complex page state → Page-scoped providers - ### Best Practices - Storybook stories for all core components -- Keep core components generic and reusable -- Compose core components in page-scoped components -- Use `pnpm storybook` with Playwright MCP for iteration - Write Vitest tests for all components, hooks, and utilities - Use JSDoc style comments for functions, components, hooks, and complex types -- Use Tailwind v4 classes -- Use semantic color tokens from `src/index.css` theme -- Use TanStack Router for navigation +- Use Tailwind v4 classes with semantic color tokens from `src/index.css` theme - Use `@/api/@tanstack/react-query.gen.ts` hooks for API calls -- Use path aliases over relative imports -- Run `pnpm lint` and `pnpm build` before committing +- Use path aliases (`@/`) over relative imports - Use `clsx` for conditional classes +- Run `pnpm lint` and `pnpm build` before committing - React `useMemo`/`memo` should only be used for genuinely expensive calculations ### Naming Conventions -- **Routes** (`.tsx`): PascalCase - `index.tsx`, `users.tsx`, `users/$userId.tsx` -- **Pages** (`.tsx`, `.ts`): PascalCase - `UsersPage.tsx`, `UserTable.tsx` -- **Components** (`.tsx`, `.ts`): PascalCase - `NetworkSelector.tsx`, `SelectMenu.tsx` -- **Providers** (`.tsx`): PascalCase - `NetworkProvider.tsx` (in `src/providers/`) -- **Context** (`.ts`): PascalCase - `NetworkContext.ts` (in `src/contexts/`) -- **Hooks** (`.ts`): camelCase starting with `use` - `useNetwork.ts`, `useConfig.ts` -- **Utils** (`.ts`): kebab-case - `api-config.ts`, `auth-service.ts` -- **Tests** (`.test.ts(x)`): Match source file name - `useNetwork.test.ts`, `colour.test.ts` +- **Routes** (`.tsx`): lowercase — `index.tsx`, `slots.tsx`, `slots/$slot.tsx` +- **Pages** (`.tsx`): PascalCase — `IndexPage.tsx`, `DetailPage.tsx` +- **Components** (`.tsx`): PascalCase — `NetworkSelector.tsx`, `SelectMenu.tsx` +- **Providers/Contexts** (`.tsx`/`.ts`): PascalCase — `NetworkProvider.tsx`, `NetworkContext.ts` +- **Hooks** (`.ts`): camelCase with `use` prefix — `useNetwork.ts`, `useConfig.ts` +- **Utils** (`.ts`): kebab-case — `api-config.ts`, `format-time.ts` +- **Tests**: Match source file — `useNetwork.test.ts`, `Button.test.tsx` +- **Barrel exports**: Always `.ts` — `index.ts` ### Testing - **Required for**: All hooks, utilities, and core components -- **Framework**: - - Vitest for hooks and utilities - - Storybook interactions for components -- **Location**: Co-located with source files (`.test.ts` or `.test.tsx` for Vitest, `.stories.tsx` for Storybook) -- **Coverage**: Aim for high coverage of business logic and edge cases -- **Naming**: Test files match source files with `.test.ts(x)` or `.stories.tsx` extension +- **Vitest** for hooks and utilities, **Storybook interactions** for components +- **Location**: Co-located with source files (`.test.ts(x)` for Vitest, `.stories.tsx` for Storybook) -## Loading States - -### Shimmer/Skeleton Loading Pattern +### Loading States - Use `LoadingContainer` from `src/components/Layout/LoadingContainer/` as base - Create page-specific skeletons: `[PageName]Skeleton` or `[ComponentName]Skeleton` -- Place in `pages/[section]/components/` alongside other page components +- Place in `pages/[section]/[page-name]/components/` alongside other page components - Show skeleton UI that matches actual content structure -### Best Practices - -- Match loading skeleton to actual content layout -- Don't overuse - only for significant data fetches -- Keep loading states brief and informative -- Consider error states alongside loading - -## Form Management - -### Form Architecture - -- One FormProvider per page -- Reusable filter components across pages -- Form state and handlers in page component -- **Always use Zod for validation** - provides type-safe schemas and runtime validation - -### Implementation - -```tsx -// In page component (e.g., pages/[section]/[page-name]/IndexPage.tsx) -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; - -// Define Zod schema -const formSchema = z.object({ - email: z.string().email('Invalid email'), - username: z.string().min(3, 'Min 3 characters'), -}); - -type FormData = z.infer; - -// Manual Zod resolver (no need for @hookform/resolvers) -const methods = useForm({ - defaultValues, - resolver: async (data) => { - try { - const validData = formSchema.parse(data); - return { values: validData, errors: {} }; - } catch (error) { - if (error instanceof z.ZodError) { - return { - values: {}, - errors: error.issues.reduce((acc, issue) => { - const path = issue.path[0] as string; - acc[path] = { type: 'validation', message: issue.message }; - return acc; - }, {} as Record), - }; - } - return { values: {}, errors: {} }; - } - }, -}); - - - // Reusable core component - // Page-specific fields - - -// In reusable filter component (e.g., components/Forms/[FilterForm]/) -const { register, watch } = useFormContext(); // Access parent form -``` - -### Best Practices - -- **Always use Zod with react-hook-form** - type-safe validation -- Use manual Zod resolver (shown above) - no need for `@hookform/resolvers` package -- Define schema with `z.object()` and infer types with `z.infer` -- Single `useForm()` per page via FormProvider -- Child components use `useFormContext()` -- Validation schemas with types in `FormData.types.ts` -- Generic filters in `components/Forms/`, page-specific in `pages/[section]/[page-name]/components/` - -## Head Meta & SEO - -### Meta Hierarchy - -- Base meta tags defined in `src/routes/__root.tsx` -- Routes override with `head: () => ({ meta: [...] })` -- Child routes inherit and can extend parent meta -- **No variables in head**: Only use literals and `import.meta.env.VITE_*` (processed by build plugin) - -### Page Feature Images - -**Standard**: Each page should have a feature image at: - -``` -public/images/[section]/[page-name].png -``` - -**Route Implementation**: - -```tsx -// In routes/[section]/[page-name].tsx -head: () => ({ - meta: [ - { title: `Page Name | ${import.meta.env.VITE_BASE_TITLE}` }, - { name: 'description', content: 'Unique description of what this page does' }, - { property: 'og:image', content: '/images/[section]/[page-name].png' }, - { property: 'og:description', content: 'Unique description of what this page does' }, - { name: 'twitter:image', content: '/images/[section]/[page-name].png' }, - { name: 'twitter:description', content: 'Unique description of what this page does' }, - ], -}) -``` - -### Best Practices - -- Always include page-specific title -- Write unique description for each page -- Update all three descriptions (meta, og:description, twitter:description) -- Provide unique feature image per page -- Set og:image and twitter:image for social sharing - -## Theming - -Theme uses a **two-tier color architecture** defined in `src/index.css`: - -**Tier 1:** Primitive scales (terracotta, sand, neutral) with 50-950 shades **Tier 2:** Semantic tokens that reference Tier 1 - -**Available semantic tokens:** - -- `primary`, `secondary`, `accent` - Brand colors -- `background`, `surface`, `foreground`, `muted`, `border` - UI colors -- `success`, `warning`, `danger` - State colors - -**Usage in components:** - -```tsx -// ✅ Always use semantic tokens -className="bg-primary text-foreground border-border" -className="hover:bg-accent text-muted" - -// ✅ Programmatic access -import { useThemeColors } from '@/hooks/useThemeColors'; -const colors = useThemeColors(); // { primary, background, ... } -``` - -**Never use primitive scales directly** (`bg-terracotta-500`, `bg-sand-100`) - only semantic tokens. - -**Modify theme:** Edit semantic mappings in `src/index.css` at `@layer base` (`:root` for light, `html.dark` for dark). - -## Storybook - -- when creating a new story, add the following decorators: - -```tsx -decorators: [ - Story => ( -
- -
- ), -], -``` - -- When choosing a title, use the full nested path to the story, e.g. `Components/Layout/Container` - -## Charts - -### Visual Standards - -**Axis Lines:** - -- **ALWAYS** render both y-axis and x-axis lines unless specifically not needed -- Set `axisLine: { show: true }` for both xAxis and yAxis - -**Grid Lines:** - -- **NEVER** render grid lines -- Set `splitLine: { show: false }` for both xAxis and yAxis - -**Axis Ranges & Intervals:** - -- yMax/xMax must work with consistent, evenly-spaced intervals -- ❌ BAD: `0, 200, 400, 600, 800, 1000, 1100` (inconsistent final interval) -- ✅ GOOD: `0, 200, 400, 600, 800, 1000` OR `0, 250, 500, 750, 1000, 1250` -- Prefer setting yMax/xMax with `splitNumber` and let ECharts calculate intervals automatically -- Example: `yAxis: { max: 1000, splitNumber: 5 }` → generates \[0, 200, 400, 600, 800, 1000\] - -**Grid Padding:** - -- Minimize whitespace while ensuring labels are visible -- **Use simple explicit padding values** and let ECharts handle label positioning: - - Set `left`, `right`, `top`, `bottom` values - - No need for `containLabel`, `align`, `inside`, `margin` - let ECharts use defaults -- **Standard padding values:** - - **Left**: 60px (for y-axis labels and name) - - **Right**: 24px (minimal, 80px if dual y-axes present) - - **Top**: 16px (40px if ECharts title is used) - - **Bottom**: 50px (90px if native legend at bottom) -- Keep it simple - ECharts defaults handle axis label positioning correctly -- Override only when absolutely necessary (e.g., dual y-axes, legends, special layouts) -- Wrappers should never override grid config unless they have very specific layout needs - -**Slot Time Formatting:** - -- When plotting slot time on xAxis: - - **Title**: Must be "Slot Time (s)" - - **Units**: Use seconds (not milliseconds) - - **Precision**: Round to the nearest second - - **Ticks**: Default ticks at 0, 4s, 8s, 12s (unless otherwise specified) - - **Format**: Display as "0", "4", "8", "12" (numbers only, no "s" suffix in tick labels) - -### Component Architecture - -**Core Chart Components** (`src/components/Charts/`): - -- Define sensible defaults for all visual standards above -- Provide consistent `gridConfig`, axis styling, and formatting -- Should be production-ready without overrides - -**Wrapper Components** (page-scoped chart components): - -- **Defer to core components** for most settings -- Only override when directly needed for page-specific requirements -- **Do NOT override** `gridConfig` unless absolutely necessary -- Pass through props to core components rather than recreating configuration - -**Example Pattern:** - -```tsx -// ❌ BAD: Wrapper recreates all config - - -// ✅ GOOD: Wrapper defers to core component - -``` - -### Shared Crosshairs - -Charts sharing the same x-axis can synchronize tooltips and crosshairs using the `syncGroup` prop: - -- Use `syncGroup="slot-time"` for charts with slot time x-axis (0-12s) -- Use `syncGroup="slot-number"` for charts with slot number x-axis -- Omit `syncGroup` for independent charts - ## Slot & Epoch Display -**Never use locale formatting** (no commas) - slots/epochs are blockchain identifiers. +**Never use locale formatting** (no commas) — slots/epochs are blockchain identifiers. **UI displays** (tables, cards): ```tsx -import { Slot, Epoch } from '@/components/Ethereum'; +import { Slot } from '@/components/Ethereum/Slot'; +import { Epoch } from '@/components/Ethereum/Epoch'; + // Linked to detail page // Plain text ``` @@ -524,4 +179,14 @@ import { Slot, Epoch } from '@/components/Ethereum'; import { formatSlot, formatEpoch } from '@/utils'; {formatSlot(slot)} // "1234567" {formatEpoch(epoch)} // "12345" -``` \ No newline at end of file +``` + +## Additional Rules + +Detailed standards for specific topics are in `.claude/rules/`: + +- [Charts](.claude/rules/charts.md) — Visual standards, axis formatting, grid padding, component architecture +- [Forms](.claude/rules/forms.md) — Zod search param validation, react-hook-form patterns +- [SEO](.claude/rules/seo.md) — Head meta hierarchy, feature images, route implementation +- [Theming](.claude/rules/theming.md) — Two-tier color architecture, semantic tokens +- [Storybook](.claude/rules/storybook.md) — Decorator pattern, story title convention