@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.
@@ -0,0 +1,132 @@
1
+ # Pattern 015: No Inline Hover Styles
2
+
3
+ **Canon Version:** 1.0
4
+ **Status:** Stable
5
+ **Category:** Styling
6
+ **Enforcement:** Documentation
7
+
8
+ ## Decision
9
+
10
+ Use Tailwind hover/focus classes. Do not use inline styles for interactive states.
11
+
12
+ ## Rationale
13
+
14
+ 1. **CSS handles it** — Pseudo-classes work in stylesheets, not inline
15
+ 2. **No JavaScript** — Hover effects without React state
16
+ 3. **Performance** — No re-renders on hover
17
+ 4. **Consistency** — All hover states follow Tailwind patterns
18
+
19
+ ## Examples
20
+
21
+ ### Good: Tailwind Pseudo-Classes
22
+
23
+ ```tsx
24
+ <button className="
25
+ bg-accent text-white
26
+ hover:bg-accent2
27
+ focus:ring-2 focus:ring-accent
28
+ active:bg-accent3
29
+ transition-colors
30
+ ">
31
+ Click Me
32
+ </button>
33
+ ```
34
+
35
+ ### Bad: State-Based Styling
36
+
37
+ ```tsx
38
+ function Button() {
39
+ const [isHovered, setIsHovered] = useState(false)
40
+
41
+ return (
42
+ <button
43
+ style={{ backgroundColor: isHovered ? '#6b4a3a' : '#8b5a4a' }}
44
+ onMouseEnter={() => setIsHovered(true)}
45
+ onMouseLeave={() => setIsHovered(false)}
46
+ >
47
+ Click Me
48
+ </button>
49
+ )
50
+ }
51
+ ```
52
+
53
+ ## Common Hover Patterns
54
+
55
+ ### Color Changes
56
+
57
+ ```tsx
58
+ // Background color
59
+ <div className="bg-gray-100 hover:bg-gray-200">
60
+
61
+ // Text color
62
+ <a className="text-body hover:text-accent">
63
+
64
+ // Border color
65
+ <div className="border-gray-300 hover:border-accent">
66
+ ```
67
+
68
+ ### Transforms
69
+
70
+ ```tsx
71
+ // Scale up on hover
72
+ <div className="hover:scale-105 transition-transform">
73
+
74
+ // Lift effect
75
+ <div className="hover:-translate-y-1 hover:shadow-lg transition-all">
76
+ ```
77
+
78
+ ### Opacity
79
+
80
+ ```tsx
81
+ // Fade in overlay
82
+ <div className="opacity-0 hover:opacity-100 transition-opacity">
83
+
84
+ // Dim on hover
85
+ <button className="hover:opacity-80 transition-opacity">
86
+ ```
87
+
88
+ ### Group Hover
89
+
90
+ ```tsx
91
+ // Parent hover affects child
92
+ <div className="group">
93
+ <img className="group-hover:scale-110 transition-transform" />
94
+ <span className="group-hover:text-accent">Title</span>
95
+ </div>
96
+ ```
97
+
98
+ ## Focus States
99
+
100
+ Always include focus states for accessibility:
101
+
102
+ ```tsx
103
+ <button className="
104
+ hover:bg-accent2
105
+ focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2
106
+ ">
107
+ ```
108
+
109
+ ## Transitions
110
+
111
+ Add transitions for smooth effects:
112
+
113
+ ```tsx
114
+ // Single property
115
+ <div className="transition-colors duration-200">
116
+
117
+ // Multiple properties
118
+ <div className="transition-all duration-300">
119
+
120
+ // Specific properties
121
+ <div className="transition-[transform,opacity] duration-200">
122
+ ```
123
+
124
+ ## Enforcement
125
+
126
+ - **Method:** Code review / Documentation
127
+ - **Check:** No `onMouseEnter`/`onMouseLeave` for styling
128
+
129
+ ## References
130
+
131
+ - Tailwind hover/focus docs
132
+ - `src/components/button.tsx` — Button with hover states
@@ -0,0 +1,149 @@
1
+ # Pattern 016: Client Extraction
2
+
3
+ **Canon Version:** 1.0
4
+ **Status:** Stable
5
+ **Category:** Rendering
6
+ **Enforcement:** ESLint (via 001)
7
+
8
+ ## Decision
9
+
10
+ Extract client-side logic (hooks, event handlers) into dedicated client components. Blocks and most components should remain server components.
11
+
12
+ ## Rationale
13
+
14
+ 1. **Minimal client JavaScript** — Only interactive parts need client runtime
15
+ 2. **Better performance** — Server components don't add to bundle
16
+ 3. **Clear boundaries** — Easy to identify what runs where
17
+ 4. **Easier testing** — Server components are pure renders
18
+
19
+ ## Extraction Patterns
20
+
21
+ ### Pattern A: Init Components (Side Effects Only)
22
+
23
+ For effects that don't render UI, create "init" components that return null:
24
+
25
+ ```tsx
26
+ // src/hooks/use-circle-animation.tsx
27
+ 'use client'
28
+ import { useEffect } from 'react'
29
+ import { useInView } from 'react-intersection-observer'
30
+
31
+ export default function CircleAnimationInit({ targetId }: { targetId: string }) {
32
+ const { ref, inView } = useInView({ threshold: 0.1 })
33
+
34
+ useEffect(() => {
35
+ if (inView) {
36
+ // Animation logic
37
+ }
38
+ }, [inView, targetId])
39
+
40
+ return null // No UI, just side effects
41
+ }
42
+ ```
43
+
44
+ Usage in server block:
45
+
46
+ ```tsx
47
+ // src/blocks/hero-16.tsx (server component)
48
+ import CircleAnimationInit from '@/hooks/use-circle-animation'
49
+
50
+ export default function Hero16() {
51
+ return (
52
+ <Section>
53
+ <div id="circle-text">...</div>
54
+ <CircleAnimationInit targetId="circle-text" />
55
+ </Section>
56
+ )
57
+ }
58
+ ```
59
+
60
+ ### Pattern B: Interactive Components
61
+
62
+ For UI that needs interactivity, create client components:
63
+
64
+ ```tsx
65
+ // src/components/video-popup.tsx
66
+ 'use client'
67
+ import { useState } from 'react'
68
+ import { Dialog } from '@headlessui/react'
69
+
70
+ export function VideoPopup({ videoId }: { videoId: string }) {
71
+ const [isOpen, setIsOpen] = useState(false)
72
+
73
+ return (
74
+ <>
75
+ <button onClick={() => setIsOpen(true)}>Play</button>
76
+ <Dialog open={isOpen} onClose={() => setIsOpen(false)}>
77
+ {/* Video player */}
78
+ </Dialog>
79
+ </>
80
+ )
81
+ }
82
+ ```
83
+
84
+ ### Pattern C: Event Delegation
85
+
86
+ For many clickable items, use event delegation:
87
+
88
+ ```tsx
89
+ // src/components/sidebar-click-handler.tsx
90
+ 'use client'
91
+
92
+ export function SidebarClickHandler({ children }: { children: React.ReactNode }) {
93
+ const handleClick = (e: React.MouseEvent) => {
94
+ const link = (e.target as HTMLElement).closest('a[data-sidebar]')
95
+ if (link) {
96
+ e.preventDefault()
97
+ // Handle sidebar navigation
98
+ }
99
+ }
100
+
101
+ return <div onClick={handleClick}>{children}</div>
102
+ }
103
+ ```
104
+
105
+ ## What Stays Server-Side
106
+
107
+ - Static content rendering
108
+ - Data fetching
109
+ - Metadata generation
110
+ - Layout structure
111
+
112
+ ## What Moves to Client
113
+
114
+ - `useState`, `useEffect`, `useRef`
115
+ - Event handlers (`onClick`, `onChange`)
116
+ - Browser APIs (`window`, `document`)
117
+ - Animation libraries (Framer Motion)
118
+ - Third-party interactive widgets
119
+
120
+ ## Static IDs
121
+
122
+ When server components need to reference DOM elements for client scripts:
123
+
124
+ ```tsx
125
+ // Server component
126
+ const sliderId = 'hero-slider'
127
+ const circleId = 'hero-circle'
128
+
129
+ return (
130
+ <>
131
+ <div id={sliderId}>...</div>
132
+ <div id={circleId}>...</div>
133
+ <SwiperSliderInit swiperId={sliderId} />
134
+ <CircleAnimationInit targetId={circleId} />
135
+ </>
136
+ )
137
+ ```
138
+
139
+ ## Enforcement
140
+
141
+ - **ESLint rule:** `gallop/no-client-blocks` (Pattern 001)
142
+ - **Severity:** Warning
143
+
144
+ ## References
145
+
146
+ - `src/hooks/use-circle-animation.tsx` — Init component pattern
147
+ - `src/hooks/swiper-slider-init.tsx` — Init component pattern
148
+ - `src/components/video-popup.tsx` — Interactive component pattern
149
+ - `src/components/sidebar-stack/` — Event delegation pattern
@@ -0,0 +1,181 @@
1
+ # Pattern 017: SEO Metadata
2
+
3
+ **Canon Version:** 1.0
4
+ **Status:** Stable
5
+ **Category:** SEO
6
+ **Enforcement:** Documentation
7
+
8
+ ## Decision
9
+
10
+ Every page must have complete SEO metadata including Open Graph, Twitter cards, and structured data.
11
+
12
+ ## Rationale
13
+
14
+ 1. **Search visibility** — Proper metadata improves rankings
15
+ 2. **Social sharing** — OG/Twitter cards control previews
16
+ 3. **Rich results** — Structured data enables rich snippets
17
+ 4. **Consistency** — All pages follow the same pattern
18
+
19
+ ## Required Metadata
20
+
21
+ ### Core Properties
22
+
23
+ | Property | Required | Description |
24
+ |----------|----------|-------------|
25
+ | `title` | Yes | Page title with site name (50-60 chars) |
26
+ | `description` | Yes | SEO description (150-160 chars) |
27
+ | `keywords` | Yes | Array of relevant keywords |
28
+ | `focusKeyword` | Yes | Primary keyword for the page |
29
+ | `featuredImage` | Yes | OG image path |
30
+ | `alternates.canonical` | Yes | Canonical URL |
31
+
32
+ ### Open Graph
33
+
34
+ ```typescript
35
+ openGraph: {
36
+ type: 'website', // or 'article' for blog posts
37
+ locale: 'en_US',
38
+ url: 'https://example.com/page',
39
+ siteName: 'Site Name',
40
+ title: 'Page Title',
41
+ description: 'Page description',
42
+ image: {
43
+ url: '/images/og-image.jpg',
44
+ alt: 'Image description',
45
+ },
46
+ }
47
+ ```
48
+
49
+ ### Twitter Cards
50
+
51
+ ```typescript
52
+ twitter: {
53
+ card: 'summary_large_image',
54
+ site: '@sitehandle',
55
+ creator: '@authorhandle',
56
+ title: 'Page Title',
57
+ description: 'Page description',
58
+ image: '/images/twitter-image.jpg',
59
+ }
60
+ ```
61
+
62
+ ### Structured Data
63
+
64
+ ```typescript
65
+ structuredData: [
66
+ {
67
+ '@context': 'https://schema.org',
68
+ '@type': 'WebPage', // or Organization, Product, Article, etc.
69
+ name: 'Page Title',
70
+ description: 'Page description',
71
+ // Additional properties based on type
72
+ },
73
+ ]
74
+ ```
75
+
76
+ ## Common Structured Data Types
77
+
78
+ ### Organization (Homepage)
79
+
80
+ ```typescript
81
+ {
82
+ '@context': 'https://schema.org',
83
+ '@type': 'Organization',
84
+ name: 'Company Name',
85
+ url: 'https://example.com',
86
+ logo: 'https://example.com/logo.png',
87
+ contactPoint: {
88
+ '@type': 'ContactPoint',
89
+ telephone: '+1-555-555-5555',
90
+ contactType: 'customer service',
91
+ },
92
+ }
93
+ ```
94
+
95
+ ### LocalBusiness (Contact Page)
96
+
97
+ ```typescript
98
+ {
99
+ '@context': 'https://schema.org',
100
+ '@type': 'LocalBusiness',
101
+ name: 'Business Name',
102
+ address: {
103
+ '@type': 'PostalAddress',
104
+ streetAddress: '123 Main St',
105
+ addressLocality: 'City',
106
+ addressRegion: 'State',
107
+ postalCode: '12345',
108
+ },
109
+ telephone: '+1-555-555-5555',
110
+ }
111
+ ```
112
+
113
+ ### Article (Blog Post)
114
+
115
+ ```typescript
116
+ {
117
+ '@context': 'https://schema.org',
118
+ '@type': 'Article',
119
+ headline: 'Article Title',
120
+ author: {
121
+ '@type': 'Person',
122
+ name: 'Author Name',
123
+ },
124
+ datePublished: '2026-01-15',
125
+ dateModified: '2026-01-15',
126
+ image: 'https://example.com/article-image.jpg',
127
+ }
128
+ ```
129
+
130
+ ### Product (Product Page)
131
+
132
+ ```typescript
133
+ {
134
+ '@context': 'https://schema.org',
135
+ '@type': 'Product',
136
+ name: 'Product Name',
137
+ description: 'Product description',
138
+ brand: {
139
+ '@type': 'Brand',
140
+ name: 'Brand Name',
141
+ },
142
+ offers: {
143
+ '@type': 'Offer',
144
+ price: '99.00',
145
+ priceCurrency: 'USD',
146
+ availability: 'https://schema.org/InStock',
147
+ },
148
+ }
149
+ ```
150
+
151
+ ## Image Guidelines
152
+
153
+ ### Featured/OG Images
154
+
155
+ - Minimum size: 1200x630px
156
+ - Recommended: 1200x630px (1.91:1 ratio)
157
+ - Format: JPG or PNG
158
+ - Max file size: 5MB
159
+
160
+ ### Twitter Images
161
+
162
+ - Minimum: 300x157px
163
+ - Recommended: 1200x600px (2:1 ratio)
164
+ - Format: JPG, PNG, or GIF
165
+
166
+ ## Validation Tools
167
+
168
+ - [Google Rich Results Test](https://search.google.com/test/rich-results)
169
+ - [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/)
170
+ - [Twitter Card Validator](https://cards-dev.twitter.com/validator)
171
+
172
+ ## Enforcement
173
+
174
+ - **Method:** Code review / SEO audit
175
+ - **Future:** CI validation of metadata completeness
176
+
177
+ ## References
178
+
179
+ - `src/utils/page-helpers.ts` — Metadata generation utilities
180
+ - `src/app/(hero)/page.tsx` — Homepage metadata example
181
+ - Schema.org documentation: https://schema.org
package/schema.json ADDED
@@ -0,0 +1,245 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "name": "Gallop Canon",
4
+ "version": "1.0.0",
5
+ "description": "Gallop Enterprise Architecture Canon - Versioned, AI-compatible, auditable web architecture patterns",
6
+ "categories": [
7
+ {
8
+ "id": "rendering",
9
+ "name": "Rendering",
10
+ "description": "Server/client component boundaries and rendering strategies"
11
+ },
12
+ {
13
+ "id": "layout",
14
+ "name": "Layout",
15
+ "description": "Layout hierarchy, spacing, and responsive design"
16
+ },
17
+ {
18
+ "id": "typography",
19
+ "name": "Typography",
20
+ "description": "Text component usage and styling"
21
+ },
22
+ {
23
+ "id": "structure",
24
+ "name": "Structure",
25
+ "description": "File and folder organization patterns"
26
+ },
27
+ {
28
+ "id": "styling",
29
+ "name": "Styling",
30
+ "description": "CSS, Tailwind, and visual styling patterns"
31
+ },
32
+ {
33
+ "id": "components",
34
+ "name": "Components",
35
+ "description": "Component design and implementation patterns"
36
+ },
37
+ {
38
+ "id": "seo",
39
+ "name": "SEO",
40
+ "description": "Search engine optimization and metadata patterns"
41
+ }
42
+ ],
43
+ "patterns": [
44
+ {
45
+ "id": "001",
46
+ "title": "Server-First Blocks",
47
+ "file": "patterns/001-server-first-blocks.md",
48
+ "category": "rendering",
49
+ "status": "stable",
50
+ "enforcement": "eslint",
51
+ "rule": "gallop/no-client-blocks",
52
+ "summary": "Blocks must be server components"
53
+ },
54
+ {
55
+ "id": "002",
56
+ "title": "Layout Hierarchy",
57
+ "file": "patterns/002-layout-hierarchy.md",
58
+ "category": "layout",
59
+ "status": "stable",
60
+ "enforcement": "eslint",
61
+ "rule": "gallop/no-container-in-section",
62
+ "summary": "No Container inside Section"
63
+ },
64
+ {
65
+ "id": "003",
66
+ "title": "Typography Components",
67
+ "file": "patterns/003-typography-components.md",
68
+ "category": "typography",
69
+ "status": "stable",
70
+ "enforcement": "eslint",
71
+ "rule": "gallop/prefer-typography-components",
72
+ "summary": "Use Paragraph/Span, not raw tags"
73
+ },
74
+ {
75
+ "id": "004",
76
+ "title": "Component Props",
77
+ "file": "patterns/004-component-props.md",
78
+ "category": "typography",
79
+ "status": "stable",
80
+ "enforcement": "eslint",
81
+ "rule": "gallop/prefer-component-props",
82
+ "summary": "Use props over className for supported styles"
83
+ },
84
+ {
85
+ "id": "005",
86
+ "title": "Page Structure",
87
+ "file": "patterns/005-page-structure.md",
88
+ "category": "structure",
89
+ "status": "stable",
90
+ "enforcement": "documentation",
91
+ "rule": null,
92
+ "summary": "PageWrapper, generatePageMetadata pattern"
93
+ },
94
+ {
95
+ "id": "006",
96
+ "title": "Block Naming",
97
+ "file": "patterns/006-block-naming.md",
98
+ "category": "structure",
99
+ "status": "stable",
100
+ "enforcement": "documentation",
101
+ "rule": null,
102
+ "summary": "{type}-{n}.tsx naming, PascalCase exports"
103
+ },
104
+ {
105
+ "id": "007",
106
+ "title": "Import Paths",
107
+ "file": "patterns/007-import-paths.md",
108
+ "category": "structure",
109
+ "status": "stable",
110
+ "enforcement": "documentation",
111
+ "rule": null,
112
+ "summary": "@/ aliases, destructured imports"
113
+ },
114
+ {
115
+ "id": "008",
116
+ "title": "Tailwind Only",
117
+ "file": "patterns/008-tailwind-only.md",
118
+ "category": "styling",
119
+ "status": "stable",
120
+ "enforcement": "documentation",
121
+ "rule": null,
122
+ "summary": "No inline styles, use Tailwind exclusively"
123
+ },
124
+ {
125
+ "id": "009",
126
+ "title": "Color Tokens",
127
+ "file": "patterns/009-color-tokens.md",
128
+ "category": "styling",
129
+ "status": "stable",
130
+ "enforcement": "documentation",
131
+ "rule": null,
132
+ "summary": "Use semantic color tokens"
133
+ },
134
+ {
135
+ "id": "010",
136
+ "title": "Spacing System",
137
+ "file": "patterns/010-spacing-system.md",
138
+ "category": "layout",
139
+ "status": "stable",
140
+ "enforcement": "documentation",
141
+ "rule": null,
142
+ "summary": "Standard padding/margin values"
143
+ },
144
+ {
145
+ "id": "011",
146
+ "title": "Responsive Mobile-First",
147
+ "file": "patterns/011-responsive-mobile-first.md",
148
+ "category": "layout",
149
+ "status": "stable",
150
+ "enforcement": "documentation",
151
+ "rule": null,
152
+ "summary": "sm/md/lg/xl breakpoint usage"
153
+ },
154
+ {
155
+ "id": "012",
156
+ "title": "Icon System",
157
+ "file": "patterns/012-icon-system.md",
158
+ "category": "components",
159
+ "status": "stable",
160
+ "enforcement": "documentation",
161
+ "rule": null,
162
+ "summary": "Iconify with Icon component"
163
+ },
164
+ {
165
+ "id": "013",
166
+ "title": "New Component Pattern",
167
+ "file": "patterns/013-new-component-pattern.md",
168
+ "category": "components",
169
+ "status": "stable",
170
+ "enforcement": "documentation",
171
+ "rule": null,
172
+ "summary": "Props for margin/color/fontSize"
173
+ },
174
+ {
175
+ "id": "014",
176
+ "title": "clsx Not classnames",
177
+ "file": "patterns/014-clsx-not-classnames.md",
178
+ "category": "styling",
179
+ "status": "stable",
180
+ "enforcement": "documentation",
181
+ "rule": null,
182
+ "summary": "Use clsx, never classnames package"
183
+ },
184
+ {
185
+ "id": "015",
186
+ "title": "No Inline Hover Styles",
187
+ "file": "patterns/015-no-inline-hover-styles.md",
188
+ "category": "styling",
189
+ "status": "stable",
190
+ "enforcement": "documentation",
191
+ "rule": null,
192
+ "summary": "Tailwind for hover states"
193
+ },
194
+ {
195
+ "id": "016",
196
+ "title": "Client Extraction",
197
+ "file": "patterns/016-client-extraction.md",
198
+ "category": "rendering",
199
+ "status": "stable",
200
+ "enforcement": "documentation",
201
+ "rule": null,
202
+ "summary": "Extract hooks to components, not blocks (see Pattern 001 for enforcement)"
203
+ },
204
+ {
205
+ "id": "017",
206
+ "title": "SEO Metadata",
207
+ "file": "patterns/017-seo-metadata.md",
208
+ "category": "seo",
209
+ "status": "stable",
210
+ "enforcement": "documentation",
211
+ "rule": null,
212
+ "summary": "PageMetadata structure, structured data"
213
+ }
214
+ ],
215
+ "guarantees": [
216
+ {
217
+ "id": "SEO_STABLE",
218
+ "name": "SEO Stability",
219
+ "since": "1.0.0",
220
+ "status": "stable",
221
+ "patterns": ["001", "016", "017"]
222
+ },
223
+ {
224
+ "id": "PERF_BASELINE",
225
+ "name": "Performance Baseline",
226
+ "since": "1.0.0",
227
+ "status": "stable",
228
+ "patterns": ["001", "007", "010", "016"]
229
+ },
230
+ {
231
+ "id": "MAINTAIN",
232
+ "name": "Maintainability",
233
+ "since": "1.0.0",
234
+ "status": "stable",
235
+ "patterns": ["004", "005", "006", "007", "013"]
236
+ },
237
+ {
238
+ "id": "DESIGN_SYSTEM",
239
+ "name": "Design System Compliance",
240
+ "since": "1.0.0",
241
+ "status": "stable",
242
+ "patterns": ["003", "004", "009", "010", "011"]
243
+ }
244
+ ]
245
+ }