@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,103 @@
|
|
|
1
|
+
# Pattern 010: Spacing System
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Styling
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use consistent spacing values from the Tailwind scale. Follow established defaults for sections, typography, and layout.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Visual rhythm** — Consistent spacing creates harmony
|
|
15
|
+
2. **Predictable layouts** — Developers know what to expect
|
|
16
|
+
3. **Easier maintenance** — Change defaults in one place
|
|
17
|
+
4. **Design system compliance** — Spacing follows the scale
|
|
18
|
+
|
|
19
|
+
## Default Spacing Values
|
|
20
|
+
|
|
21
|
+
### Sections
|
|
22
|
+
|
|
23
|
+
| Element | Default | Description |
|
|
24
|
+
|---------|---------|-------------|
|
|
25
|
+
| Section padding (vertical) | `py-20 md:py-30` | Top and bottom padding |
|
|
26
|
+
| Section padding (horizontal) | `px-6 lg:px-8` | Left and right padding |
|
|
27
|
+
| Max width | `max-w-[1600px]` | Content container width |
|
|
28
|
+
|
|
29
|
+
### Typography
|
|
30
|
+
|
|
31
|
+
| Element | Default | Description |
|
|
32
|
+
|---------|---------|-------------|
|
|
33
|
+
| Heading margin | `mb-8` | All heading levels |
|
|
34
|
+
| Paragraph margin | `mb-8` | Body text blocks |
|
|
35
|
+
| Label margin | `mb-0` | Inline labels |
|
|
36
|
+
| Accent margin | `mb-4` | When above headings |
|
|
37
|
+
| Accent margin (decorative) | `mb-0` | When rotated/decorative |
|
|
38
|
+
|
|
39
|
+
### Layout
|
|
40
|
+
|
|
41
|
+
| Element | Default | Description |
|
|
42
|
+
|---------|---------|-------------|
|
|
43
|
+
| Column gap | `gap-8 lg:gap-16` | Between columns |
|
|
44
|
+
| Card padding | `p-6` | Internal card padding |
|
|
45
|
+
| Button spacing | `gap-4` | Between buttons |
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
### Section Structure
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
<Section className="py-20 md:py-30">
|
|
53
|
+
<div className="mx-auto max-w-[1600px] px-6 lg:px-8">
|
|
54
|
+
<Heading margin="mb-8">Section Title</Heading>
|
|
55
|
+
<Paragraph margin="mb-8">Content paragraph.</Paragraph>
|
|
56
|
+
</div>
|
|
57
|
+
</Section>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Typography Stack
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<Accent margin="mb-4">Featured</Accent>
|
|
64
|
+
<Heading margin="mb-8">Main Heading</Heading>
|
|
65
|
+
<Paragraph margin="mb-8">First paragraph of content.</Paragraph>
|
|
66
|
+
<Paragraph margin="mb-0">Final paragraph, no margin.</Paragraph>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Columns Layout
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<Columns
|
|
73
|
+
cols="grid-cols-1 lg:grid-cols-2"
|
|
74
|
+
gap="gap-8 lg:gap-16"
|
|
75
|
+
>
|
|
76
|
+
<Column>Left content</Column>
|
|
77
|
+
<Column>Right content</Column>
|
|
78
|
+
</Columns>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Spacing Scale Reference
|
|
82
|
+
|
|
83
|
+
| Class | Value |
|
|
84
|
+
|-------|-------|
|
|
85
|
+
| `*-4` | 1rem (16px) |
|
|
86
|
+
| `*-6` | 1.5rem (24px) |
|
|
87
|
+
| `*-8` | 2rem (32px) |
|
|
88
|
+
| `*-10` | 2.5rem (40px) |
|
|
89
|
+
| `*-12` | 3rem (48px) |
|
|
90
|
+
| `*-16` | 4rem (64px) |
|
|
91
|
+
| `*-20` | 5rem (80px) |
|
|
92
|
+
| `*-30` | 7.5rem (120px) |
|
|
93
|
+
|
|
94
|
+
## Enforcement
|
|
95
|
+
|
|
96
|
+
- **Method:** Code review / Documentation
|
|
97
|
+
- **Component defaults:** Typography components have built-in margins
|
|
98
|
+
|
|
99
|
+
## References
|
|
100
|
+
|
|
101
|
+
- `src/components/section.tsx` — Section with default padding
|
|
102
|
+
- `src/components/heading.tsx` — Heading with mb-8 default
|
|
103
|
+
- `src/components/paragraph.tsx` — Paragraph with mb-8 default
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Pattern 011: Responsive Mobile-First
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Styling
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Design mobile-first. Use Tailwind breakpoint prefixes to progressively enhance for larger screens.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Performance** — Mobile styles load first, enhancements are additive
|
|
15
|
+
2. **Accessibility** — Core experience works on all devices
|
|
16
|
+
3. **Maintainability** — Base styles are simple, complexity added at breakpoints
|
|
17
|
+
4. **SEO** — Google uses mobile-first indexing
|
|
18
|
+
|
|
19
|
+
## Breakpoint Scale
|
|
20
|
+
|
|
21
|
+
| Prefix | Min Width | Target |
|
|
22
|
+
|--------|-----------|--------|
|
|
23
|
+
| (none) | 0px | Mobile phones |
|
|
24
|
+
| `sm:` | 640px | Large phones, small tablets |
|
|
25
|
+
| `md:` | 768px | Tablets |
|
|
26
|
+
| `lg:` | 1024px | Laptops, small desktops |
|
|
27
|
+
| `xl:` | 1280px | Desktops |
|
|
28
|
+
| `2xl:` | 1536px | Large desktops |
|
|
29
|
+
|
|
30
|
+
## Common Patterns
|
|
31
|
+
|
|
32
|
+
### Layout Direction
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
// Stack on mobile, row on desktop
|
|
36
|
+
<div className="flex flex-col lg:flex-row">
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Grid Columns
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// 1 column mobile, 2 columns tablet, 3 columns desktop
|
|
43
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Responsive Typography
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
// Smaller on mobile, larger on desktop
|
|
50
|
+
<Heading fontSize="text-3xl md:text-4xl lg:text-5xl">
|
|
51
|
+
Responsive Heading
|
|
52
|
+
</Heading>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Responsive Spacing
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// Less padding on mobile, more on desktop
|
|
59
|
+
<Section className="py-12 md:py-20 lg:py-30">
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Responsive Heights
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
// Shorter on mobile, taller on desktop
|
|
66
|
+
<div className="h-[300px] sm:h-[450px] lg:h-[600px]">
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Show/Hide Elements
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
// Hide on mobile, show on desktop
|
|
73
|
+
<nav className="hidden lg:flex">
|
|
74
|
+
|
|
75
|
+
// Show on mobile, hide on desktop
|
|
76
|
+
<button className="lg:hidden">Menu</button>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
### Good: Mobile-First
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
<div className="
|
|
85
|
+
flex flex-col gap-4
|
|
86
|
+
md:flex-row md:gap-8
|
|
87
|
+
lg:gap-16
|
|
88
|
+
">
|
|
89
|
+
<div className="w-full md:w-1/2">Content</div>
|
|
90
|
+
<div className="w-full md:w-1/2">Content</div>
|
|
91
|
+
</div>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Bad: Desktop-First (Anti-Pattern)
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
// Don't do this - starts with desktop, removes at mobile
|
|
98
|
+
<div className="
|
|
99
|
+
flex flex-row gap-16
|
|
100
|
+
max-md:flex-col max-md:gap-4
|
|
101
|
+
">
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Testing Checklist
|
|
105
|
+
|
|
106
|
+
- [ ] Works on 320px width (small phones)
|
|
107
|
+
- [ ] Works on 375px width (iPhone)
|
|
108
|
+
- [ ] Works on 768px width (tablet)
|
|
109
|
+
- [ ] Works on 1024px width (laptop)
|
|
110
|
+
- [ ] Works on 1440px width (desktop)
|
|
111
|
+
|
|
112
|
+
## Enforcement
|
|
113
|
+
|
|
114
|
+
- **Method:** Code review / Visual testing
|
|
115
|
+
- **Tools:** Browser DevTools responsive mode
|
|
116
|
+
|
|
117
|
+
## References
|
|
118
|
+
|
|
119
|
+
- Tailwind CSS responsive design docs
|
|
120
|
+
- All blocks are built mobile-first
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Pattern 012: Icon System
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Components
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use Iconify icon packages with the `Icon` component. Do not use inline SVGs or other icon libraries.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Consistent sizing** — Icon component handles dimensions
|
|
15
|
+
2. **Tree shaking** — Only imported icons are bundled
|
|
16
|
+
3. **Large library** — Access to thousands of icons
|
|
17
|
+
4. **Type safety** — Icon imports are typed
|
|
18
|
+
|
|
19
|
+
## Icon Packages
|
|
20
|
+
|
|
21
|
+
| Package | Usage |
|
|
22
|
+
|---------|-------|
|
|
23
|
+
| `@iconify/icons-heroicons` | UI icons (arrows, actions) |
|
|
24
|
+
| `@iconify/icons-lucide` | General purpose icons |
|
|
25
|
+
| `@iconify/icons-mdi` | Material Design icons |
|
|
26
|
+
| `@iconify/icons-simple-icons` | Brand/logo icons |
|
|
27
|
+
|
|
28
|
+
## Usage Pattern
|
|
29
|
+
|
|
30
|
+
### Import Icons
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import arrowRightIcon from '@iconify/icons-heroicons/arrow-right-20-solid'
|
|
34
|
+
import playCircleIcon from '@iconify/icons-lucide/play-circle'
|
|
35
|
+
import twitterIcon from '@iconify/icons-simple-icons/twitter'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Render with Icon Component
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { Icon } from '@/components'
|
|
42
|
+
|
|
43
|
+
<Icon icon={arrowRightIcon} className="w-5 h-5" />
|
|
44
|
+
<Icon icon={playCircleIcon} className="w-6 h-6 text-accent" />
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### With Buttons
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<Button
|
|
51
|
+
href="/contact"
|
|
52
|
+
icon={arrowRightIcon}
|
|
53
|
+
iconPlacement="after"
|
|
54
|
+
>
|
|
55
|
+
Get Started
|
|
56
|
+
</Button>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Common Icons
|
|
60
|
+
|
|
61
|
+
| Icon | Import |
|
|
62
|
+
|------|--------|
|
|
63
|
+
| Arrow right | `@iconify/icons-heroicons/arrow-right-20-solid` |
|
|
64
|
+
| Arrow down | `@iconify/icons-heroicons/arrow-down-20-solid` |
|
|
65
|
+
| Play | `@iconify/icons-heroicons/play-solid` |
|
|
66
|
+
| Play circle | `@iconify/icons-lucide/play-circle` |
|
|
67
|
+
| Check | `@iconify/icons-heroicons/check-20-solid` |
|
|
68
|
+
| X/Close | `@iconify/icons-heroicons/x-mark-20-solid` |
|
|
69
|
+
| Menu | `@iconify/icons-heroicons/bars-3-20-solid` |
|
|
70
|
+
| Sparkles | `@iconify/icons-heroicons/sparkles-20-solid` |
|
|
71
|
+
|
|
72
|
+
## Examples
|
|
73
|
+
|
|
74
|
+
### Good
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import arrowRightIcon from '@iconify/icons-heroicons/arrow-right-20-solid'
|
|
78
|
+
import { Icon } from '@/components'
|
|
79
|
+
|
|
80
|
+
<button className="flex items-center gap-2">
|
|
81
|
+
Next
|
|
82
|
+
<Icon icon={arrowRightIcon} className="w-5 h-5" />
|
|
83
|
+
</button>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Bad
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
// Inline SVG
|
|
90
|
+
<button>
|
|
91
|
+
Next
|
|
92
|
+
<svg className="w-5 h-5" viewBox="0 0 20 20">
|
|
93
|
+
<path d="M..." />
|
|
94
|
+
</svg>
|
|
95
|
+
</button>
|
|
96
|
+
|
|
97
|
+
// Other icon library
|
|
98
|
+
import { ArrowRight } from 'react-icons/hi'
|
|
99
|
+
<ArrowRight />
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Sizing Guidelines
|
|
103
|
+
|
|
104
|
+
| Context | Size |
|
|
105
|
+
|---------|------|
|
|
106
|
+
| Inline with text | `w-4 h-4` or `w-5 h-5` |
|
|
107
|
+
| Button icons | `w-5 h-5` |
|
|
108
|
+
| Standalone icons | `w-6 h-6` |
|
|
109
|
+
| Large decorative | `w-8 h-8` or larger |
|
|
110
|
+
|
|
111
|
+
## Enforcement
|
|
112
|
+
|
|
113
|
+
- **Method:** Code review / Documentation
|
|
114
|
+
- **Future:** ESLint rule to detect inline SVGs
|
|
115
|
+
|
|
116
|
+
## References
|
|
117
|
+
|
|
118
|
+
- `src/components/icon.tsx` — Icon component
|
|
119
|
+
- `src/components/button.tsx` — Button with icon support
|
|
120
|
+
- Iconify icon explorer: https://icon-sets.iconify.design/
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Pattern 013: New Component Pattern
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Components
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
New components expose props for commonly overridden styles instead of relying solely on `className`.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Discoverable API** — Props appear in autocomplete
|
|
15
|
+
2. **Consistent defaults** — Sensible defaults built-in
|
|
16
|
+
3. **Type safety** — Props can be typed and documented
|
|
17
|
+
4. **Easier refactoring** — Change defaults in one place
|
|
18
|
+
5. **Lint compatibility** — Works with `prefer-component-props` rule
|
|
19
|
+
|
|
20
|
+
## Component Template
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { clsx } from 'clsx'
|
|
24
|
+
|
|
25
|
+
interface MyComponentProps {
|
|
26
|
+
margin?: string // e.g., "mb-8", "mb-4"
|
|
27
|
+
color?: string // e.g., "text-accent", "text-contrast"
|
|
28
|
+
fontSize?: string // e.g., "text-lg", "text-sm"
|
|
29
|
+
textAlign?: string // e.g., "text-center", "text-left"
|
|
30
|
+
fontWeight?: string // e.g., "font-bold", "font-medium"
|
|
31
|
+
className?: string // Additional styling
|
|
32
|
+
children: React.ReactNode
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function MyComponent({
|
|
36
|
+
margin = 'mb-4', // Sensible default
|
|
37
|
+
color = 'text-contrast', // Sensible default
|
|
38
|
+
fontSize = 'text-base',
|
|
39
|
+
textAlign = '',
|
|
40
|
+
fontWeight = '',
|
|
41
|
+
className = '',
|
|
42
|
+
children,
|
|
43
|
+
}: MyComponentProps) {
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={clsx(
|
|
47
|
+
margin,
|
|
48
|
+
color,
|
|
49
|
+
fontSize,
|
|
50
|
+
textAlign,
|
|
51
|
+
fontWeight,
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Standard Props
|
|
62
|
+
|
|
63
|
+
| Prop | Type | Common Values |
|
|
64
|
+
|------|------|---------------|
|
|
65
|
+
| `margin` | string | `mb-0`, `mb-4`, `mb-8` |
|
|
66
|
+
| `color` | string | `text-body`, `text-contrast`, `text-accent` |
|
|
67
|
+
| `fontSize` | string | `text-sm`, `text-base`, `text-lg`, `text-xl` |
|
|
68
|
+
| `textAlign` | string | `text-left`, `text-center`, `text-right` |
|
|
69
|
+
| `fontWeight` | string | `font-normal`, `font-medium`, `font-bold` |
|
|
70
|
+
| `lineHeight` | string | `leading-tight`, `leading-relaxed` |
|
|
71
|
+
| `className` | string | Any additional Tailwind classes |
|
|
72
|
+
|
|
73
|
+
## File Structure
|
|
74
|
+
|
|
75
|
+
1. Create file in `src/components/` with lowercase hyphenated name
|
|
76
|
+
2. Export from `src/components/index.ts`
|
|
77
|
+
3. Use `clsx` for combining classes
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
src/components/
|
|
81
|
+
├── my-component.tsx ← Component file
|
|
82
|
+
├── index.ts ← Add export here
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
|
|
87
|
+
### Good: Props for Common Overrides
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
// Definition
|
|
91
|
+
export function Badge({
|
|
92
|
+
margin = 'mb-0',
|
|
93
|
+
color = 'text-white',
|
|
94
|
+
bgColor = 'bg-accent',
|
|
95
|
+
fontSize = 'text-xs',
|
|
96
|
+
className = '',
|
|
97
|
+
children,
|
|
98
|
+
}: BadgeProps) {
|
|
99
|
+
return (
|
|
100
|
+
<span className={clsx(margin, color, bgColor, fontSize, 'px-2 py-1 rounded', className)}>
|
|
101
|
+
{children}
|
|
102
|
+
</span>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Usage
|
|
107
|
+
<Badge color="text-black" bgColor="bg-yellow-400">New</Badge>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Bad: className-Only Styling
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
// Definition - no props for common overrides
|
|
114
|
+
export function Badge({ className, children }: { className?: string; children: React.ReactNode }) {
|
|
115
|
+
return (
|
|
116
|
+
<span className={clsx('text-xs px-2 py-1 rounded', className)}>
|
|
117
|
+
{children}
|
|
118
|
+
</span>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Usage - must override via className
|
|
123
|
+
<Badge className="text-black bg-yellow-400 mb-4">New</Badge>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Enforcement
|
|
127
|
+
|
|
128
|
+
- **Method:** Code review / Documentation
|
|
129
|
+
- **Lint:** `gallop/prefer-component-props` encourages prop usage
|
|
130
|
+
|
|
131
|
+
## References
|
|
132
|
+
|
|
133
|
+
- `src/components/paragraph.tsx` — Example with full prop support
|
|
134
|
+
- `src/components/heading.tsx` — Example with full prop support
|
|
135
|
+
- `src/components/label.tsx` — Example with full prop support
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Pattern 014: clsx Not classnames
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Styling
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use `clsx` for conditional class names. Do not use the `classnames` package.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Smaller bundle** — clsx is ~234B vs classnames ~454B
|
|
15
|
+
2. **Faster runtime** — clsx is optimized for performance
|
|
16
|
+
3. **Same API** — Drop-in replacement, familiar syntax
|
|
17
|
+
4. **Project standard** — Consistency across codebase
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Import
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { clsx } from 'clsx'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Basic Conditionals
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<div className={clsx(
|
|
31
|
+
'base-class',
|
|
32
|
+
isActive && 'active-class',
|
|
33
|
+
isDisabled && 'disabled-class'
|
|
34
|
+
)}>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Object Syntax
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<div className={clsx({
|
|
41
|
+
'base-class': true,
|
|
42
|
+
'active-class': isActive,
|
|
43
|
+
'disabled-class': isDisabled,
|
|
44
|
+
})}>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Array Syntax
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<div className={clsx([
|
|
51
|
+
'base-class',
|
|
52
|
+
isActive ? 'active-class' : 'inactive-class',
|
|
53
|
+
])}>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Combining Props and Conditions
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
function Button({ className, isLoading }: Props) {
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
className={clsx(
|
|
63
|
+
'px-4 py-2 rounded font-medium',
|
|
64
|
+
'bg-accent text-accent-contrast',
|
|
65
|
+
'hover:bg-accent2 transition-colors',
|
|
66
|
+
isLoading && 'opacity-50 cursor-not-allowed',
|
|
67
|
+
className // Allow additional classes from parent
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{isLoading ? 'Loading...' : children}
|
|
71
|
+
</button>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Examples
|
|
77
|
+
|
|
78
|
+
### Good
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { clsx } from 'clsx'
|
|
82
|
+
|
|
83
|
+
<div className={clsx(
|
|
84
|
+
'flex items-center gap-2',
|
|
85
|
+
variant === 'primary' && 'bg-accent text-white',
|
|
86
|
+
variant === 'secondary' && 'bg-gray-100 text-gray-900',
|
|
87
|
+
disabled && 'opacity-50 pointer-events-none'
|
|
88
|
+
)}>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Bad
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
// Using classnames package
|
|
95
|
+
import classNames from 'classnames'
|
|
96
|
+
|
|
97
|
+
<div className={classNames('flex', { 'bg-accent': isPrimary })}>
|
|
98
|
+
|
|
99
|
+
// Template literals (harder to read)
|
|
100
|
+
<div className={`flex ${isPrimary ? 'bg-accent' : ''}`}>
|
|
101
|
+
|
|
102
|
+
// Array join (non-standard)
|
|
103
|
+
<div className={['flex', isPrimary && 'bg-accent'].filter(Boolean).join(' ')}>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Migration from classnames
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
// Before
|
|
110
|
+
import classNames from 'classnames'
|
|
111
|
+
classNames('foo', { bar: true })
|
|
112
|
+
|
|
113
|
+
// After
|
|
114
|
+
import { clsx } from 'clsx'
|
|
115
|
+
clsx('foo', { bar: true })
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The API is identical — just change the import.
|
|
119
|
+
|
|
120
|
+
## Enforcement
|
|
121
|
+
|
|
122
|
+
- **Method:** Code review / package.json audit
|
|
123
|
+
- **Check:** `classnames` should not appear in dependencies
|
|
124
|
+
|
|
125
|
+
## References
|
|
126
|
+
|
|
127
|
+
- clsx GitHub: https://github.com/lukeed/clsx
|
|
128
|
+
- All components use clsx for conditional classes
|