@gallop.software/canon 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +116 -0
- package/guarantees.md +136 -0
- package/package.json +41 -0
- package/patterns/001-server-first-blocks.md +84 -0
- package/patterns/002-layout-hierarchy.md +84 -0
- package/patterns/003-typography-components.md +78 -0
- package/patterns/004-component-props.md +102 -0
- package/patterns/005-page-structure.md +110 -0
- package/patterns/006-block-naming.md +87 -0
- package/patterns/007-import-paths.md +113 -0
- package/patterns/008-tailwind-only.md +97 -0
- package/patterns/009-color-tokens.md +114 -0
- package/patterns/010-spacing-system.md +103 -0
- package/patterns/011-responsive-mobile-first.md +120 -0
- package/patterns/012-icon-system.md +120 -0
- package/patterns/013-new-component-pattern.md +135 -0
- package/patterns/014-clsx-not-classnames.md +128 -0
- package/patterns/015-no-inline-hover-styles.md +132 -0
- package/patterns/016-client-extraction.md +149 -0
- package/patterns/017-seo-metadata.md +181 -0
- package/schema.json +245 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Pattern 003: Typography Components
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Typography
|
|
6
|
+
**Enforcement:** ESLint
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use `Paragraph` and `Span` components instead of raw `<p>` and `<span>` elements in blocks and components.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Consistent styling** — Typography components apply default margins, colors, and fonts
|
|
15
|
+
2. **Prop-based customization** — Override styles via props, not className hunting
|
|
16
|
+
3. **Design system compliance** — All text follows the same patterns
|
|
17
|
+
4. **Easier refactoring** — Change defaults in one place
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
### Bad
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<p className="text-sm text-gray-500 mb-4">Some descriptive text</p>
|
|
25
|
+
<span className="text-green-500 font-medium">+28%</span>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Good
|
|
29
|
+
|
|
30
|
+
```tsx
|
|
31
|
+
<Paragraph fontSize="text-sm" color="text-gray-500">
|
|
32
|
+
Some descriptive text
|
|
33
|
+
</Paragraph>
|
|
34
|
+
<Span color="text-green-500" fontWeight="font-medium">+28%</Span>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Available Typography Components
|
|
38
|
+
|
|
39
|
+
| Component | Default Tag | Default Margin | Use Case |
|
|
40
|
+
|-----------|-------------|----------------|----------|
|
|
41
|
+
| `Heading` | `h2` | `mb-8` | All headings |
|
|
42
|
+
| `Paragraph` | `p` | `mb-8` | Body text blocks |
|
|
43
|
+
| `Span` | `span` | `mb-0` | Inline text |
|
|
44
|
+
| `Label` | `p` | `mb-0` | Labels, captions |
|
|
45
|
+
| `Quote` | `blockquote` | `mb-8` | Quotations |
|
|
46
|
+
| `Accent` | `span` | `mb-4` | Decorative accent text |
|
|
47
|
+
|
|
48
|
+
## Exceptions
|
|
49
|
+
|
|
50
|
+
Raw `<span>` is allowed when:
|
|
51
|
+
|
|
52
|
+
1. **Inside typography components** — For inline styling within Heading, Paragraph, etc.
|
|
53
|
+
2. **Gradient text effects** — When using `bg-clip-text` for gradient text
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
{/* Allowed: span inside Heading for gradient effect */}
|
|
57
|
+
<Heading>
|
|
58
|
+
Transform your space into something{' '}
|
|
59
|
+
<span className="bg-gradient-to-r from-purple-600 to-pink-500 bg-clip-text text-transparent">
|
|
60
|
+
extraordinary
|
|
61
|
+
</span>
|
|
62
|
+
</Heading>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Enforcement
|
|
66
|
+
|
|
67
|
+
- **ESLint rule:** `gallop/prefer-typography-components`
|
|
68
|
+
- **Severity:** Warning
|
|
69
|
+
|
|
70
|
+
## Escape Hatch
|
|
71
|
+
|
|
72
|
+
For gradient text or complex inline styling, raw `<span>` with `bg-clip-text` is allowed.
|
|
73
|
+
|
|
74
|
+
## References
|
|
75
|
+
|
|
76
|
+
- `src/components/paragraph.tsx` — Paragraph component
|
|
77
|
+
- `src/components/span.tsx` — Span component
|
|
78
|
+
- `src/components/heading.tsx` — Heading component
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Pattern 004: Component Props Over className
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Typography
|
|
6
|
+
**Enforcement:** ESLint
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use dedicated props instead of `className` for styling that components support.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Discoverable API** — Props are documented and autocompleted
|
|
15
|
+
2. **Consistent defaults** — Components define sensible defaults
|
|
16
|
+
3. **Easier auditing** — Can search for prop usage, not className patterns
|
|
17
|
+
4. **Type safety** — Props can be typed, className cannot
|
|
18
|
+
|
|
19
|
+
## Examples
|
|
20
|
+
|
|
21
|
+
### Bad
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<Heading className="text-center mb-6 text-accent">Title</Heading>
|
|
25
|
+
<Paragraph className="text-lg mb-4">Text content</Paragraph>
|
|
26
|
+
<Label className="text-sm font-semibold">Category</Label>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Good
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<Heading textAlign="text-center" margin="mb-6" color="text-accent">
|
|
33
|
+
Title
|
|
34
|
+
</Heading>
|
|
35
|
+
<Paragraph fontSize="text-lg" margin="mb-4">
|
|
36
|
+
Text content
|
|
37
|
+
</Paragraph>
|
|
38
|
+
<Label fontSize="text-sm" fontWeight="font-semibold">
|
|
39
|
+
Category
|
|
40
|
+
</Label>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Supported Props by Component
|
|
44
|
+
|
|
45
|
+
### Heading
|
|
46
|
+
- `margin` — Bottom margin (e.g., `mb-8`, `mb-4`)
|
|
47
|
+
- `color` — Text color (e.g., `text-contrast`, `text-accent`)
|
|
48
|
+
- `textAlign` — Alignment (e.g., `text-center`, `text-left`)
|
|
49
|
+
- `fontSize` — Size (e.g., `text-4xl`, `text-2xl`)
|
|
50
|
+
- `fontWeight` — Weight (e.g., `font-bold`, `font-semibold`)
|
|
51
|
+
|
|
52
|
+
### Paragraph
|
|
53
|
+
- `margin` — Bottom margin
|
|
54
|
+
- `color` — Text color
|
|
55
|
+
- `textAlign` — Alignment
|
|
56
|
+
- `fontSize` — Size
|
|
57
|
+
- `lineHeight` — Line height
|
|
58
|
+
|
|
59
|
+
### Label
|
|
60
|
+
- `margin` — Bottom margin
|
|
61
|
+
- `color` — Text color
|
|
62
|
+
- `textAlign` — Alignment
|
|
63
|
+
- `fontSize` — Size
|
|
64
|
+
- `fontWeight` — Weight
|
|
65
|
+
|
|
66
|
+
### Accent
|
|
67
|
+
- `margin` — Bottom margin
|
|
68
|
+
- `color` — Text color
|
|
69
|
+
- `textAlign` — Alignment
|
|
70
|
+
|
|
71
|
+
### Button
|
|
72
|
+
- `margin` — Bottom margin
|
|
73
|
+
|
|
74
|
+
## When to Use className
|
|
75
|
+
|
|
76
|
+
Use `className` for styles that are NOT covered by props:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
{/* max-w is not a prop, so className is appropriate */}
|
|
80
|
+
<Paragraph className="max-w-lg">
|
|
81
|
+
Content with constrained width
|
|
82
|
+
</Paragraph>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Enforcement
|
|
86
|
+
|
|
87
|
+
- **ESLint rule:** `gallop/prefer-component-props`
|
|
88
|
+
- **Severity:** Warning
|
|
89
|
+
- **Detected patterns:** `mb-*`, `my-*`, `m-*`, `text-center`, `text-left`, `text-right`, `font-*`, `text-{color}` in className
|
|
90
|
+
|
|
91
|
+
## Escape Hatch
|
|
92
|
+
|
|
93
|
+
If a component doesn't support a prop you need:
|
|
94
|
+
|
|
95
|
+
1. Consider adding the prop to the component
|
|
96
|
+
2. Use className as a fallback with a comment explaining why
|
|
97
|
+
|
|
98
|
+
## References
|
|
99
|
+
|
|
100
|
+
- `src/components/heading.tsx` — Heading with prop overrides
|
|
101
|
+
- `src/components/paragraph.tsx` — Paragraph with prop overrides
|
|
102
|
+
- `src/components/label.tsx` — Label with prop overrides
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Pattern 005: Page Structure
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Structure
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Page files follow a consistent structure using `PageWrapper` and `generatePageMetadata`.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Consistent metadata** — All pages have proper SEO metadata
|
|
15
|
+
2. **Structured data** — Schema.org markup applied uniformly
|
|
16
|
+
3. **Predictable layout** — Pages follow the same wrapper pattern
|
|
17
|
+
4. **Easy auditing** — Can verify metadata completeness across all pages
|
|
18
|
+
|
|
19
|
+
## Template
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { PageWrapper } from '@/components/page-wrapper'
|
|
23
|
+
import { generatePageMetadata, type PageMetadata } from '@/utils/page-helpers'
|
|
24
|
+
|
|
25
|
+
import Hero1 from '@/blocks/hero-1'
|
|
26
|
+
import Section1 from '@/blocks/section-1'
|
|
27
|
+
|
|
28
|
+
function Content() {
|
|
29
|
+
return (
|
|
30
|
+
<>
|
|
31
|
+
<Hero1 />
|
|
32
|
+
<Section1 />
|
|
33
|
+
</>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const metadata: PageMetadata = {
|
|
38
|
+
title: 'Page Title | Site Name',
|
|
39
|
+
description: 'Page description for SEO (150-160 characters)',
|
|
40
|
+
keywords: ['keyword1', 'keyword2', 'keyword3'],
|
|
41
|
+
focusKeyword: 'primary keyword',
|
|
42
|
+
readingTimeMinutes: 5,
|
|
43
|
+
publishDate: '2026-01-15T00:00:00Z',
|
|
44
|
+
modifiedDate: '2026-01-15T00:00:00Z',
|
|
45
|
+
featuredImage: '/images/page-featured.jpg',
|
|
46
|
+
alternates: {
|
|
47
|
+
canonical: 'https://example.com/page-slug',
|
|
48
|
+
},
|
|
49
|
+
authors: [{ name: 'Author Name' }],
|
|
50
|
+
openGraph: {
|
|
51
|
+
type: 'website',
|
|
52
|
+
locale: 'en_US',
|
|
53
|
+
url: 'https://example.com/page-slug',
|
|
54
|
+
siteName: 'Site Name',
|
|
55
|
+
title: 'Page Title | Site Name',
|
|
56
|
+
description: 'Page description for SEO',
|
|
57
|
+
image: {
|
|
58
|
+
url: '/images/page-featured.jpg',
|
|
59
|
+
alt: 'Featured image description',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
twitter: {
|
|
63
|
+
card: 'summary_large_image',
|
|
64
|
+
site: '@sitehandle',
|
|
65
|
+
creator: '@creatorhandle',
|
|
66
|
+
title: 'Page Title',
|
|
67
|
+
description: 'Page description for Twitter',
|
|
68
|
+
image: '/images/page-featured.jpg',
|
|
69
|
+
},
|
|
70
|
+
structuredData: [
|
|
71
|
+
{
|
|
72
|
+
'@context': 'https://schema.org',
|
|
73
|
+
'@type': 'WebPage',
|
|
74
|
+
name: 'Page Title',
|
|
75
|
+
description: 'Page description',
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const generateMetadata = () => generatePageMetadata(metadata)
|
|
81
|
+
export default function Page() {
|
|
82
|
+
return (
|
|
83
|
+
<PageWrapper metadata={metadata}>
|
|
84
|
+
<Content />
|
|
85
|
+
</PageWrapper>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Required Metadata Properties
|
|
91
|
+
|
|
92
|
+
| Property | Required | Description |
|
|
93
|
+
|----------|----------|-------------|
|
|
94
|
+
| `title` | Yes | Page title with site name |
|
|
95
|
+
| `description` | Yes | SEO description (150-160 chars) |
|
|
96
|
+
| `keywords` | Yes | Array of relevant keywords |
|
|
97
|
+
| `focusKeyword` | Yes | Primary SEO keyword |
|
|
98
|
+
| `featuredImage` | Yes | OG image path |
|
|
99
|
+
| `alternates.canonical` | Yes | Canonical URL |
|
|
100
|
+
|
|
101
|
+
## Enforcement
|
|
102
|
+
|
|
103
|
+
- **Method:** Code review / Documentation
|
|
104
|
+
- **Future:** CI validation of metadata completeness
|
|
105
|
+
|
|
106
|
+
## References
|
|
107
|
+
|
|
108
|
+
- `src/app/(hero)/page.tsx` — Homepage example
|
|
109
|
+
- `src/components/page-wrapper.tsx` — PageWrapper component
|
|
110
|
+
- `src/utils/page-helpers.ts` — Metadata utilities
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Pattern 006: Block Naming
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Structure
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Block files use `{type}-{n}.tsx` naming with PascalCase function exports.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Sequential organization** — Numbers indicate variants, not priority
|
|
15
|
+
2. **Easy discovery** — Group by type in file explorer
|
|
16
|
+
3. **No conflicts** — Numbers prevent naming collisions
|
|
17
|
+
4. **Consistent exports** — PascalCase matches React conventions
|
|
18
|
+
|
|
19
|
+
## Naming Rules
|
|
20
|
+
|
|
21
|
+
### File Names
|
|
22
|
+
|
|
23
|
+
- Lowercase with hyphens: `hero-1.tsx`, `section-15.tsx`
|
|
24
|
+
- Type prefix followed by number: `{type}-{n}.tsx`
|
|
25
|
+
- Check existing files to find next available number
|
|
26
|
+
|
|
27
|
+
### Function Exports
|
|
28
|
+
|
|
29
|
+
- PascalCase matching file name
|
|
30
|
+
- `hero-16.tsx` → `function Hero16()`
|
|
31
|
+
- `call-to-action-3.tsx` → `function CallToAction3()`
|
|
32
|
+
|
|
33
|
+
## Block Types
|
|
34
|
+
|
|
35
|
+
| Type | Description |
|
|
36
|
+
|------|-------------|
|
|
37
|
+
| `hero-{n}` | Hero/banner sections |
|
|
38
|
+
| `section-{n}` | General content sections |
|
|
39
|
+
| `content-{n}` | Content/feature sections |
|
|
40
|
+
| `showcase-{n}` | Portfolio/gallery showcases |
|
|
41
|
+
| `cover-{n}` | Full-width image/video covers |
|
|
42
|
+
| `contact-{n}` | Contact forms and info |
|
|
43
|
+
| `testimonial-{n}` | Testimonials |
|
|
44
|
+
| `blog-{n}` | Blog-related sections |
|
|
45
|
+
| `pricing-{n}` | Pricing tables |
|
|
46
|
+
| `process-{n}` | Process/steps sections |
|
|
47
|
+
| `about-{n}` | About sections |
|
|
48
|
+
| `services-{n}` | Services sections |
|
|
49
|
+
| `call-to-action-{n}` | CTA sections |
|
|
50
|
+
| `partners-{n}` | Partner/logo clouds |
|
|
51
|
+
| `accordion-{n}` | FAQ/accordion sections |
|
|
52
|
+
| `archive-{n}` | Archive/listing sections |
|
|
53
|
+
| `application-{n}` | Job application forms |
|
|
54
|
+
| `business-info-{n}` | Business information |
|
|
55
|
+
| `sidebar-{n}` | Sidebar panels |
|
|
56
|
+
| `portfolio-{n}` | Portfolio grids |
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
### Good
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
src/blocks/
|
|
64
|
+
├── hero-1.tsx → export default function Hero1()
|
|
65
|
+
├── hero-2.tsx → export default function Hero2()
|
|
66
|
+
├── section-1.tsx → export default function Section1()
|
|
67
|
+
├── call-to-action-1.tsx → export default function CallToAction1()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Bad
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
src/blocks/
|
|
74
|
+
├── Hero.tsx ❌ No number
|
|
75
|
+
├── hero_1.tsx ❌ Underscore instead of hyphen
|
|
76
|
+
├── HeroSection.tsx ❌ Wrong naming pattern
|
|
77
|
+
├── cta-1.tsx ❌ Abbreviation not in type list
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Enforcement
|
|
81
|
+
|
|
82
|
+
- **Method:** Code review / Documentation
|
|
83
|
+
- **Future:** CI validation of naming patterns
|
|
84
|
+
|
|
85
|
+
## References
|
|
86
|
+
|
|
87
|
+
- `src/blocks/` — All block files follow this pattern
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Pattern 007: Import Paths
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Structure
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use `@/` path aliases for all internal imports. Use destructured imports for components.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Consistent paths** — No relative path gymnastics (`../../..`)
|
|
15
|
+
2. **Refactoring safe** — Moving files doesn't break imports
|
|
16
|
+
3. **Clear origin** — `@/` indicates project source
|
|
17
|
+
4. **Smaller bundles** — Destructured imports enable tree shaking
|
|
18
|
+
|
|
19
|
+
## Path Aliases
|
|
20
|
+
|
|
21
|
+
| Alias | Resolves To | Example |
|
|
22
|
+
|-------|-------------|---------|
|
|
23
|
+
| `@/components` | `src/components` | `import { Heading } from '@/components'` |
|
|
24
|
+
| `@/blocks` | `src/blocks` | `import Hero1 from '@/blocks/hero-1'` |
|
|
25
|
+
| `@/hooks` | `src/hooks` | `import { useInView } from '@/hooks/use-in-view'` |
|
|
26
|
+
| `@/template` | `src/template` | `import PageFooter from '@/template/page-footer'` |
|
|
27
|
+
| `@/utils` | `src/utils` | `import { cn } from '@/utils/cn'` |
|
|
28
|
+
|
|
29
|
+
## Import Patterns
|
|
30
|
+
|
|
31
|
+
### Components (Destructured)
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Good: Destructured import from barrel
|
|
35
|
+
import { Heading, Paragraph, Button, Section, Columns, Column } from '@/components'
|
|
36
|
+
|
|
37
|
+
// Bad: Individual file imports
|
|
38
|
+
import { Heading } from '@/components/heading'
|
|
39
|
+
import { Paragraph } from '@/components/paragraph'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Blocks (Default Export)
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// Good: Default import with descriptive name
|
|
46
|
+
import Hero1 from '@/blocks/hero-1'
|
|
47
|
+
import Section3 from '@/blocks/section-3'
|
|
48
|
+
|
|
49
|
+
// Bad: Named import (blocks use default exports)
|
|
50
|
+
import { Hero1 } from '@/blocks/hero-1'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Hooks (Named or Default)
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
// Init components (return null, used for side effects)
|
|
57
|
+
import CircleAnimationInit from '@/hooks/use-circle-animation'
|
|
58
|
+
import SwiperSliderInit from '@/hooks/swiper-slider-init'
|
|
59
|
+
|
|
60
|
+
// Traditional hooks
|
|
61
|
+
import { useInView } from '@/hooks/use-in-view'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Icons
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Good: Import from Iconify packages
|
|
68
|
+
import arrowRightIcon from '@iconify/icons-heroicons/arrow-right-20-solid'
|
|
69
|
+
import playCircleIcon from '@iconify/icons-lucide/play-circle'
|
|
70
|
+
|
|
71
|
+
// Use with Icon component
|
|
72
|
+
<Icon icon={arrowRightIcon} className="w-5 h-5" />
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Examples
|
|
76
|
+
|
|
77
|
+
### Good
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import {
|
|
81
|
+
Heading,
|
|
82
|
+
Paragraph,
|
|
83
|
+
Button,
|
|
84
|
+
Section,
|
|
85
|
+
Columns,
|
|
86
|
+
Column,
|
|
87
|
+
Image,
|
|
88
|
+
} from '@/components'
|
|
89
|
+
import Hero1 from '@/blocks/hero-1'
|
|
90
|
+
import arrowDownIcon from '@iconify/icons-heroicons/arrow-down-20-solid'
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Bad
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
// Relative imports
|
|
97
|
+
import { Heading } from '../../components/heading'
|
|
98
|
+
import Hero1 from '../blocks/hero-1'
|
|
99
|
+
|
|
100
|
+
// Wrong alias format
|
|
101
|
+
import { Heading } from 'components/heading'
|
|
102
|
+
import { Heading } from '~/components/heading'
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Enforcement
|
|
106
|
+
|
|
107
|
+
- **Method:** Code review / ESLint import rules
|
|
108
|
+
- **Future:** Custom ESLint rule for path validation
|
|
109
|
+
|
|
110
|
+
## References
|
|
111
|
+
|
|
112
|
+
- `tsconfig.json` — Path alias configuration
|
|
113
|
+
- `src/components/index.ts` — Component barrel file
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Pattern 008: Tailwind Only
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Styling
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use Tailwind CSS classes exclusively. Do not use inline styles or CSS-in-JS.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Consistent styling** — All styles come from the same system
|
|
15
|
+
2. **Design tokens** — Tailwind enforces spacing/color scales
|
|
16
|
+
3. **Performance** — No runtime CSS generation
|
|
17
|
+
4. **Scannable code** — Styles are visible in the JSX
|
|
18
|
+
5. **Tooling support** — Tailwind IntelliSense, Prettier sorting
|
|
19
|
+
|
|
20
|
+
## Examples
|
|
21
|
+
|
|
22
|
+
### Good
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
<div className="flex items-center gap-4 p-6 bg-white rounded-lg shadow-lg">
|
|
26
|
+
<Heading className="text-2xl">Title</Heading>
|
|
27
|
+
</div>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Bad
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// Inline styles
|
|
34
|
+
<div style={{ display: 'flex', padding: '24px', backgroundColor: 'white' }}>
|
|
35
|
+
<Heading>Title</Heading>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
// CSS-in-JS
|
|
39
|
+
const StyledDiv = styled.div`
|
|
40
|
+
display: flex;
|
|
41
|
+
padding: 24px;
|
|
42
|
+
`;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Allowed Exceptions
|
|
46
|
+
|
|
47
|
+
### Dynamic Values
|
|
48
|
+
|
|
49
|
+
When a style value comes from data or calculations:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
// Allowed: Dynamic positioning
|
|
53
|
+
<div style={{ left: `${position}px` }}>
|
|
54
|
+
|
|
55
|
+
// Allowed: CSS custom properties for theming
|
|
56
|
+
<div style={{ '--progress': `${percent}%` } as React.CSSProperties}>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### CSS Custom Properties
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
// Allowed: Using CSS variables
|
|
63
|
+
<div className="bg-[var(--color-accent)]">
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Complex Animations
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
// Allowed: Framer Motion
|
|
70
|
+
<motion.div animate={{ opacity: 1 }}>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Conditional Classes
|
|
74
|
+
|
|
75
|
+
Use `clsx` for conditional classes:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { clsx } from 'clsx'
|
|
79
|
+
|
|
80
|
+
<button
|
|
81
|
+
className={clsx(
|
|
82
|
+
'px-4 py-2 rounded',
|
|
83
|
+
isActive && 'bg-accent text-white',
|
|
84
|
+
!isActive && 'bg-gray-100 text-gray-700'
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Enforcement
|
|
90
|
+
|
|
91
|
+
- **Method:** Code review / Documentation
|
|
92
|
+
- **Future:** ESLint rule to detect inline styles
|
|
93
|
+
|
|
94
|
+
## References
|
|
95
|
+
|
|
96
|
+
- `src/styles/tailwind.css` — Tailwind configuration
|
|
97
|
+
- All blocks and components use Tailwind exclusively
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Pattern 009: Color Tokens
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Styling
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use semantic color tokens instead of raw color values. Colors are defined as CSS custom properties.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Theme consistency** — Colors change in one place
|
|
15
|
+
2. **Semantic meaning** — `text-contrast` is clearer than `text-gray-800`
|
|
16
|
+
3. **Dark mode ready** — Token values can change per theme
|
|
17
|
+
4. **Accessibility** — Contrast ratios are pre-validated
|
|
18
|
+
|
|
19
|
+
## Color Token System
|
|
20
|
+
|
|
21
|
+
### Text Colors
|
|
22
|
+
|
|
23
|
+
| Token | Usage |
|
|
24
|
+
|-------|-------|
|
|
25
|
+
| `text-body` | Default body text |
|
|
26
|
+
| `text-contrast` | High-contrast text (headings, emphasis) |
|
|
27
|
+
| `text-accent` | Accent/brand color text |
|
|
28
|
+
| `text-accent-contrast` | Text on accent backgrounds |
|
|
29
|
+
| `text-white` | White text |
|
|
30
|
+
|
|
31
|
+
### Background Colors
|
|
32
|
+
|
|
33
|
+
| Token | Usage |
|
|
34
|
+
|-------|-------|
|
|
35
|
+
| `bg-body` | Main page background |
|
|
36
|
+
| `bg-body2` | Secondary/alternate background |
|
|
37
|
+
| `bg-contrast` | High-contrast background |
|
|
38
|
+
| `bg-accent` | Primary accent background |
|
|
39
|
+
| `bg-accent2` | Secondary accent background |
|
|
40
|
+
| `bg-accent3` | Tertiary accent background |
|
|
41
|
+
|
|
42
|
+
### Pairing Rules
|
|
43
|
+
|
|
44
|
+
Accent backgrounds pair with contrast variants:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// Good: Proper pairing
|
|
48
|
+
<div className="bg-accent text-accent-contrast">
|
|
49
|
+
Readable text on accent background
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
// Bad: May have contrast issues
|
|
53
|
+
<div className="bg-accent text-body">
|
|
54
|
+
Potentially unreadable
|
|
55
|
+
</div>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
### Good
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<Section className="bg-body2">
|
|
64
|
+
<Heading color="text-contrast">Title</Heading>
|
|
65
|
+
<Paragraph color="text-body">Body content</Paragraph>
|
|
66
|
+
<Button className="bg-accent text-accent-contrast">
|
|
67
|
+
Call to Action
|
|
68
|
+
</Button>
|
|
69
|
+
</Section>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Bad
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<Section className="bg-gray-100">
|
|
76
|
+
<Heading className="text-gray-900">Title</Heading>
|
|
77
|
+
<Paragraph className="text-gray-600">Body content</Paragraph>
|
|
78
|
+
<Button className="bg-purple-600 text-white">
|
|
79
|
+
Call to Action
|
|
80
|
+
</Button>
|
|
81
|
+
</Section>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## CSS Variable Definitions
|
|
85
|
+
|
|
86
|
+
```css
|
|
87
|
+
:root {
|
|
88
|
+
--color-body: #171412;
|
|
89
|
+
--color-body2: #f2eae7;
|
|
90
|
+
--color-contrast: #1a1a1a;
|
|
91
|
+
--color-accent: #8b5a4a;
|
|
92
|
+
--color-accent2: #6b4a3a;
|
|
93
|
+
--color-accent3: #f2ebe1;
|
|
94
|
+
--color-accent-contrast: #ffffff;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## When to Use Raw Colors
|
|
99
|
+
|
|
100
|
+
Raw Tailwind colors are acceptable for:
|
|
101
|
+
|
|
102
|
+
1. **Third-party content** — Colors from external data
|
|
103
|
+
2. **One-off decorative elements** — Gradients, shadows
|
|
104
|
+
3. **Status indicators** — `text-green-500` for success
|
|
105
|
+
|
|
106
|
+
## Enforcement
|
|
107
|
+
|
|
108
|
+
- **Method:** Code review / Documentation
|
|
109
|
+
- **Future:** ESLint rule to prefer tokens
|
|
110
|
+
|
|
111
|
+
## References
|
|
112
|
+
|
|
113
|
+
- `src/styles/tailwind.css` — Color token definitions
|
|
114
|
+
- `src/components/button.tsx` — Button using accent tokens
|