|
| 1 | +# Component Development Guide (AI Operations Manual) |
| 2 | + |
| 3 | +This guide provides a technical walkthrough for an AI agent on how to create and update the structural components of the website. Components are the foundational building blocks of the site, written as Astro (`.astro`) files. |
| 4 | + |
| 5 | +The core philosophy is a strict **separation of concerns**: |
| 6 | + |
| 7 | +* **Structure (Components)**: Defined in `.astro` files. This is your focus when following this guide. Components are responsible for the HTML structure and accepting data via props. They should be completely agnostic of the content they display and the specific styles they wear. |
| 8 | +* **Content (Data)**: Defined in `.yaml` files in the `/content` directory. This is managed according to the [Content Management Guide](content_management.rst). |
| 9 | +* **Style (Theme)**: Defined in the `/src/themes/[theme-name]/` directory. This is managed according to the [AI Theming Engine Guide](theme_design.rst). |
| 10 | + |
| 11 | +Your role in component development is to act as an architect, creating robust and flexible structures that can be filled with any content and styled by any theme. |
| 12 | + |
| 13 | +## Component Categories & Templates |
| 14 | + |
| 15 | +When tasked with creating a "component", first determine its type, then strictly adhere to the corresponding template. Don't improvise on the architecture; use these proven patterns. |
| 16 | + |
| 17 | +### 1. UI Components (Atoms) |
| 18 | + |
| 19 | +Small custom elements (e.g., buttons, badges, icons). Use this for reusable UI bits. |
| 20 | + |
| 21 | +**Template Path**: `src/core/dev/templates/ui/GenericElement.astro` |
| 22 | + |
| 23 | +**Style Template**: `src/core/dev/templates/ui/GenericElement.style.yaml` |
| 24 | +**Style Spec Template**: `src/core/dev/templates/ui/GenericElement.style.spec.yaml` |
| 25 | + |
| 26 | +```astro |
| 27 | +--- |
| 28 | +/** |
| 29 | + * GenericElement.astro |
| 30 | + */ |
| 31 | +import type { HTMLAttributes } from 'astro/types'; |
| 32 | +import { getClasses } from '~/utils/theme'; |
| 33 | +import { twMerge } from 'tailwind-merge'; |
| 34 | +
|
| 35 | +interface Props extends HTMLAttributes<'div'> { |
| 36 | + variant?: 'default' | 'alternative'; |
| 37 | +} |
| 38 | +
|
| 39 | +const { |
| 40 | + variant = 'default', |
| 41 | + class: className = '', |
| 42 | + ...rest |
| 43 | +} = Astro.props; |
| 44 | +
|
| 45 | +// Retrieve CSS classes |
| 46 | +const classes = getClasses('Component+GenericElement'); |
| 47 | +
|
| 48 | +const variants = { |
| 49 | + default: classes.default, |
| 50 | + alternative: classes.alternative, |
| 51 | +}; |
| 52 | +--- |
| 53 | +
|
| 54 | +<div class={twMerge(variants[variant], className)} {...rest}> |
| 55 | + <slot /> |
| 56 | +</div> |
| 57 | +``` |
| 58 | + |
| 59 | +```yaml |
| 60 | +# GenericElement.style.yaml |
| 61 | +Component+GenericElement: |
| 62 | + default: 'rounded p-4 bg-surface text-default' |
| 63 | + alternative: 'rounded p-4 bg-primary text-white' |
| 64 | +``` |
| 65 | +
|
| 66 | +```yaml |
| 67 | +# GenericElement.style.spec.yaml |
| 68 | +Component+GenericElement: |
| 69 | + type: object |
| 70 | + properties: |
| 71 | + default: { type: string } |
| 72 | + alternative: { type: string } |
| 73 | +``` |
| 74 | +
|
| 75 | +### 2. Standard Widgets (Molecules) |
| 76 | +
|
| 77 | +Self-contained content blocks (e.g., Forms, Charts, Simple Cards). |
| 78 | +
|
| 79 | +**Template Path**: `src/core/dev/templates/widgets/GenericWidget.astro` |
| 80 | +**Spec Path**: `src/core/dev/templates/widgets/GenericWidget.spec.yaml` |
| 81 | +**Style Template**: `src/core/dev/templates/widgets/GenericWidget.style.yaml` |
| 82 | +**Style Spec Template**: `src/core/dev/templates/widgets/GenericWidget.style.spec.yaml` |
| 83 | + |
| 84 | +**Key Pattern**: Inherits `WidgetProps`, uses `WidgetWrapper`. |
| 85 | + |
| 86 | +```astro |
| 87 | +--- |
| 88 | +import type { Widget as WidgetProps } from '~/types'; |
| 89 | +import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; |
| 90 | +import { getClasses } from '~/utils/theme'; |
| 91 | +
|
| 92 | +export interface Props extends WidgetProps { |
| 93 | + customProp?: string; |
| 94 | +} |
| 95 | +
|
| 96 | +const { id, customProp, classes: rawClasses = {}, bg = '' } = Astro.props; |
| 97 | +const classes = getClasses('Component+GenericWidget', rawClasses); |
| 98 | +--- |
| 99 | +
|
| 100 | +<WidgetWrapper type="generic-widget" id={id} classes={classes.wrapper} bg={bg}> |
| 101 | + <div class={classes.container}> |
| 102 | + {customProp && <p class={classes.text}>{customProp}</p>} |
| 103 | + <slot /> |
| 104 | + </div> |
| 105 | +</WidgetWrapper> |
| 106 | +``` |
| 107 | + |
| 108 | +```yaml |
| 109 | +# GenericWidget.style.yaml |
| 110 | +Component+GenericWidget: |
| 111 | + wrapper: |
| 112 | + container: '' |
| 113 | + container: 'flex flex-col gap-4' |
| 114 | + text: 'text-lg font-medium' |
| 115 | +``` |
| 116 | + |
| 117 | +### 3. Titled Widgets (Complex Molecules) |
| 118 | + |
| 119 | +Widgets that need a header (Title, Subtitle, Tagline) like Hero, Features, FAQs. This is the **most common** type of widget you will build. |
| 120 | + |
| 121 | +**Template Path**: `src/core/dev/templates/widgets/GenericTitledWidget.astro` |
| 122 | +**Spec Path**: `src/core/dev/templates/widgets/GenericTitledWidget.spec.yaml` |
| 123 | +**Style Template**: `src/core/dev/templates/widgets/GenericTitledWidget.style.yaml` |
| 124 | +**Style Spec Template**: `src/core/dev/templates/widgets/GenericTitledWidget.style.spec.yaml` |
| 125 | + |
| 126 | +**Key Pattern**: Inherits `TitledWidget`, uses `TitledWidgetWrapper`. |
| 127 | + |
| 128 | +```astro |
| 129 | +--- |
| 130 | +import type { TitledWidget } from '~/types'; |
| 131 | +import TitledWidgetWrapper from '~/components/ui/TitledWidgetWrapper.astro'; |
| 132 | +import { getClasses } from '~/utils/theme'; |
| 133 | +
|
| 134 | +export interface Props extends TitledWidget { |
| 135 | + content?: string; |
| 136 | +} |
| 137 | +
|
| 138 | +const { |
| 139 | + id, |
| 140 | + title = await Astro.slots.render('title'), |
| 141 | + subtitle = await Astro.slots.render('subtitle'), |
| 142 | + tagline = await Astro.slots.render('tagline'), |
| 143 | + content, |
| 144 | + classes: rawClasses = {}, |
| 145 | + bg = '', |
| 146 | +} = Astro.props; |
| 147 | +
|
| 148 | +const classes = getClasses('Component+GenericTitledWidget', rawClasses); |
| 149 | +--- |
| 150 | +
|
| 151 | +<TitledWidgetWrapper |
| 152 | + type="generic-titled-widget" |
| 153 | + id={id} |
| 154 | + classes={classes} |
| 155 | + bg={bg} |
| 156 | + title={title} |
| 157 | + subtitle={subtitle} |
| 158 | + tagline={tagline} |
| 159 | +> |
| 160 | + <div class={classes.content_container}> |
| 161 | + {content && <p class={classes.content}>{content}</p>} |
| 162 | + <slot /> |
| 163 | + </div> |
| 164 | +</TitledWidgetWrapper> |
| 165 | +``` |
| 166 | + |
| 167 | +```yaml |
| 168 | +# GenericTitledWidget.style.yaml |
| 169 | +Component+GenericTitledWidget: |
| 170 | + wrapper: |
| 171 | + container: '' |
| 172 | + headline: |
| 173 | + title: '' |
| 174 | + content_container: 'mt-8 flex flex-col gap-6' |
| 175 | + content: 'text-muted' |
| 176 | +``` |
| 177 | + |
| 178 | +### 4. Section Components (Organisms) |
| 179 | + |
| 180 | +Layout containers that hold Widgets. |
| 181 | + |
| 182 | +**Template Path**: `src/core/dev/templates/sections/GenericSection.astro` |
| 183 | +**Spec Path**: `src/core/dev/templates/sections/GenericSection.spec.yaml` |
| 184 | +**Style Template**: `src/core/dev/templates/sections/GenericSection.style.yaml` |
| 185 | +**Style Spec Template**: `src/core/dev/templates/sections/GenericSection.style.spec.yaml` |
| 186 | + |
| 187 | +**Key Pattern**: Uses `SectionWrapper`, iterates over `components.main` using `generateSection`. |
| 188 | + |
| 189 | +```astro |
| 190 | +--- |
| 191 | +import type { Section as Props } from '~/types'; |
| 192 | +import SectionWrapper from '~/components/ui/SectionWrapper.astro'; |
| 193 | +import { generateSection } from '~/utils/generator'; |
| 194 | +import { getClasses } from '~/utils/theme'; |
| 195 | +
|
| 196 | +// Destructure section properties |
| 197 | +const { id, components, classes: rawClasses = {}, bg = '' } = Astro.props; |
| 198 | +const classes = getClasses('Section+GenericSection', rawClasses); |
| 199 | +--- |
| 200 | +
|
| 201 | +<SectionWrapper type="generic-section" id={id} classes={classes.wrapper} bg={bg}> |
| 202 | + <div class={classes.container} data-name="section-generic-container"> |
| 203 | + <div class={classes.content} data-name="section-generic-content"> |
| 204 | + { |
| 205 | + generateSection(components?.main).map(({ component: Component, props }) => ( |
| 206 | + <Component {...props} /> |
| 207 | + )) |
| 208 | + } |
| 209 | + </div> |
| 210 | + </div> |
| 211 | +</SectionWrapper> |
| 212 | +``` |
| 213 | + |
| 214 | +```yaml |
| 215 | +# GenericSection.style.yaml |
| 216 | +Section+GenericSection: |
| 217 | + wrapper: |
| 218 | + container: 'py-10 md:py-16' |
| 219 | + main: '' |
| 220 | +``` |
| 221 | + |
| 222 | +## Development Workflow |
| 223 | + |
| 224 | +Follow this exact process to create a new component. |
| 225 | + |
| 226 | +### Step 1: Select Type & Template |
| 227 | +Choose strictly from the 4 types above. Do not invent new structures. |
| 228 | +* Need a button or label? -> **UI Component** |
| 229 | +* Need a content block with a title? -> **Titled Widget** (Most common) |
| 230 | +* Need a raw content block? -> **Standard Widget** |
| 231 | +* Need a new page layout? -> **Section Component** |
| 232 | + |
| 233 | +### Step 2: Create the Files (The "4-File Rule") |
| 234 | +Every functional component requires **exactly four files** to be complete. This ensures the component is structurally sound, spec-compliant, and fully themeable. |
| 235 | + |
| 236 | +1. **Component File** (`.astro`): The HTML structure and logic. |
| 237 | +2. **Component Spec** (`.spec.yaml`): The content schema (props validation). |
| 238 | +3. **Style Definition** (`.style.yaml`): The default default styles (Tailwind classes). |
| 239 | +4. **Style Spec** (`.style.spec.yaml`): The style schema (validating the theme). |
| 240 | + |
| 241 | +**Copy all four templates** for your chosen type into the component's directory. |
| 242 | +* **Location**: `src/[module]/src/components/[type]/` |
| 243 | +* **Naming**: |
| 244 | + - `MyComponent.astro` |
| 245 | + - `MyComponent.spec.yaml` |
| 246 | + - `MyComponent.style.yaml` (Note: In the final module structure, these are consolidated, but start by creating them individually or adding to the module's main `style.yaml` and `style.spec.yaml`). |
| 247 | + |
| 248 | +### Step 3: Define "The API" (Props) |
| 249 | +In the `.astro` file, define the `Props` interface. |
| 250 | +* **Strings**: For text content (`title`, `description`). |
| 251 | +* **Booleans**: For toggles (`isReversed`). |
| 252 | +* **Objects**: For complex data (`image: { src, alt }`). |
| 253 | +* **Arrays**: For lists (`items: []`). |
| 254 | + |
| 255 | +**Crucial**: Update the `.spec.yaml` to Match! |
| 256 | +Every prop in the interface must have a corresponding entry in the spec file. This allows the AI System to validate content against your component. |
| 257 | + |
| 258 | +### Step 4: Implement Structure & Styling |
| 259 | +* **Semantic HTML**: Use proper tags (`<article>`, `<figure>`, `<header>`). |
| 260 | +* **Theme Integration**: |
| 261 | + 1. Call `getClasses('Component+[Name]')`. |
| 262 | + 2. For every styled element, look for a class in the `classes` object (`class={classes.container}`). |
| 263 | + 3. **NEVER hardcode Tailwind classes** (e.g., `class="p-4 bg-blue-500"`). Always use the theme system. This is non-negotiable. |
| 264 | + |
| 265 | +### Step 5: Register Component |
| 266 | +Run `npm run generate-spec` to register your new component in the global schema. |
| 267 | + |
| 268 | +### Step 6: Default Theme (Consolidated) |
| 269 | +Once you have defined your `.style.yaml` and `.style.spec.yaml`, you must merge them into the module's main theme files (or the site's default theme if working in core). |
| 270 | + |
| 271 | +1. **Merge Styles**: Append the content of your `.style.yaml` to `src/[module]/theme/style.yaml`. |
| 272 | +2. **Merge Specs**: Append the content of your `.style.spec.yaml` to `src/[module]/theme/style.spec.yaml`. |
| 273 | + |
| 274 | +## Engineering Standards & Best Practices |
| 275 | + |
| 276 | +### Defensive Rendering Patterns |
| 277 | +AI-generated components must be robust. Never assume data exists. |
| 278 | + |
| 279 | +1. **Conditional Rendering**: Do not render empty containers. |
| 280 | + * **Bad**: `<div class="subtitle">{subtitle}</div>` |
| 281 | + * **Good**: `{subtitle && <p class={classes.subtitle}>{subtitle}</p>}` |
| 282 | + |
| 283 | +2. **Default Values**: Always provide fallback values for visual props. |
| 284 | + * **Example**: `const { icon = 'tabler:star', ... } = Astro.props;` |
| 285 | + |
| 286 | +### Accessibility (A11y) |
| 287 | +1. **Semantic HTML**: Use `<header>`, `<article>`, `<figure>`. |
| 288 | +2. **Interactive Elements**: must have `aria-expanded` if they toggle content. |
| 289 | +3. **Images**: `alt` text is mandatory. |
| 290 | + |
| 291 | +### Client-Side Interactivity |
| 292 | +1. **Scoped Selection**: use `getElementById(id)` using the component's unique `id` prop. |
| 293 | + * **Bad**: `document.querySelector('.my-button')` |
| 294 | + * **Good**: `const container = document.getElementById(id); container.querySelector('.my-button');` |
| 295 | + |
| 296 | +2. **Nano Stores**: For shared state management, prefer Nano Stores over passing callbacks. |
0 commit comments