# Phase 3: Component System - Research

**Researched:** 2026-02-05
**Domain:** Reusable UI components with YAML-defined properties, lifecycle hooks, HTMX handlers, and Twig-compatible templating
**Confidence:** HIGH

## Summary

This phase implements the component system that enables reusable UI components responding to HTMX interactions. Components are defined with YAML property schemas (similar to WinterCMS Inspector configuration), registered per-plugin, and render via Twig-compatible templates. HTMX handlers (methods prefixed with `on`) respond to client-side triggers and return HTML fragments for swapping.

The recommended stack uses: ZIO HTTP 3.3.0+ with the official `zio-http-htmx` module for HTMX response headers, **Pebble 4.1.1** for Twig/Jinja2-compatible templating (chosen over ScalaTags per CONTEXT.md decision for Twig-compatible templates), circe-yaml for YAML property schema parsing (reusing Phase 2 infrastructure), and ZIO for component lifecycle management. The htmx4s library provides type-safe HTMX constants and ScalaTags integration for any programmatic HTML needs.

**Primary recommendation:** Define components as Scala traits with YAML property schemas parsed at plugin registration time. Use Pebble template engine for Twig-compatible rendering with `default.htm` convention. Implement HTMX handlers as `on*` methods returning `ZIO[ComponentEnv, ComponentError, Html]`. Integrate CSRF protection via request middleware injecting tokens, with validation on all HTMX POST requests.

## Standard Stack

The established libraries/tools for this domain:

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| zio-http-htmx | 3.3.0 | HTMX response headers | Official ZIO HTTP module for HTMX integration |
| Pebble | 4.1.1 | Twig-compatible templates | Java templating with Jinja2/Twig syntax, i18n, autoescaping |
| circe-yaml | 0.16.1 | YAML property parsing | Already in stack from Phase 2, proven for YAML parsing |
| htmx4s-constants | latest | HTMX vocabulary | Type-safe HTMX attribute/header constants |

### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| htmx4s-scalatags | latest | ScalaTags HTMX attrs | When generating HTML programmatically (not templates) |
| ScalaTags | 0.13.x | Programmatic HTML | Component helper methods, fragment generation |
| zio-http | 3.3.0+ | HTTP server + SSE | Base HTTP server, Server-Sent Events streaming |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Pebble | ScalaTags only | ScalaTags is type-safe but not Twig-compatible; Pebble enables WinterCMS migration |
| Pebble | Twirl | Twirl has Scala syntax, not Jinja2/Twig-compatible |
| Pebble | Jinjava | Jinjava is also Jinja2-compatible; Pebble has better Twig inheritance |
| htmx4s | Hand-roll constants | htmx4s provides IDE docs from htmx markdown, type safety |

**Installation (Mill build.mill):**
```scala
def mvnDeps = Seq(
  // Existing deps from Phase 1/2...

  // HTMX integration
  mvn"dev.zio::zio-http-htmx:3.3.0",

  // Twig-compatible templating
  mvn"io.pebbletemplates:pebble:4.1.1",

  // HTMX constants (optional, for type-safe attributes)
  mvn"com.github.eikek::htmx4s-constants:latest",

  // ScalaTags for programmatic HTML fragments
  mvn"com.lihaoyi::scalatags:0.13.1"
)
```

## Architecture Patterns

### Recommended Component Directory Structure
```
plugins/
├── golem15/
│   └── blog/
│       ├── plugin.yaml
│       ├── Plugin.scala              # registerComponents method
│       └── components/
│           ├── Posts.scala           # Component class
│           └── posts/
│               ├── component.yaml    # Property schema
│               ├── default.htm       # Main template (required)
│               ├── pagination.htm    # Partial template
│               └── item.htm          # Item template
```

### Pattern 1: Component Trait with Lifecycle Hooks
**What:** Define components as Scala traits with `init`, `onRun`, and HTMX handler methods
**When to use:** All components must implement this trait
**Example:**
```scala
// Source: WinterCMS ComponentBase pattern adapted to ZIO
trait SummerComponent:
  /** Component identifier (alias used in pages) */
  def alias: String

  /** Component metadata from YAML */
  def details: ComponentDetails

  /** Property schema from component.yaml */
  def propertySchema: List[PropertyDef]

  /** Current property values (set from page/layout) */
  def properties: Ref[Map[String, PropertyValue]]

  /** Access to page context for rendering */
  def pageContext: PageContext

  /** Called once when component is initialized (before HTMX handlers) */
  def init: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit

  /** Called when component is bound to page/layout, part of page lifecycle */
  def onRun: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit

  /** Render the default template with current context */
  def render: ZIO[ComponentEnv, ComponentError, Html]

  /** Render a specific partial template */
  def renderPartial(name: String): ZIO[ComponentEnv, ComponentError, Html]

case class ComponentDetails(
  name: String,
  description: String,
  icon: Option[String] = None
)
```

### Pattern 2: YAML Property Schema (WinterCMS Inspector Compatible)
**What:** Define component properties in YAML with explicit types and validation
**When to use:** Every component with configurable properties
**Example:**
```yaml
# plugins/golem15/blog/components/posts/component.yaml
# Source: WinterCMS Inspector configuration format
name: Blog Posts
description: Displays a list of blog posts with pagination
icon: pencil

properties:
  postsPerPage:
    type: string
    title: Posts per page
    description: Number of posts to display per page
    default: "10"
    validation:
      integer:
        message: Posts per page must be a positive integer
        min:
          value: 1
          message: Must show at least 1 post
        max:
          value: 100
          message: Maximum 100 posts per page

  categoryFilter:
    type: dropdown
    title: Category Filter
    description: Filter posts by category
    default: ""
    showExternalParam: true    # Allow {{ :category }} binding
    # Options loaded dynamically from getCategoryFilterOptions()

  sortOrder:
    type: dropdown
    title: Sort Order
    options:
      published_at desc: Published date (newest first)
      published_at asc: Published date (oldest first)
      title asc: Title (A-Z)
    default: published_at desc

  noPostsMessage:
    type: string
    title: No posts message
    default: No posts found.
    group: Messages

  throwNotFound:
    type: checkbox
    title: Throw 404 on empty
    description: Show 404 page when no posts match filter
    default: false
    group: Advanced
```

```scala
// Source: circe-yaml parsing + WinterCMS property types
import io.circe.generic.auto.*
import io.circe.yaml.parser

case class ComponentSchema(
  name: String,
  description: String,
  icon: Option[String],
  properties: Map[String, PropertyDef]
)

case class PropertyDef(
  `type`: PropertyType,
  title: String,
  description: Option[String] = None,
  default: Option[PropertyValue] = None,
  options: Option[Map[String, String]] = None,  // For dropdown/set
  group: Option[String] = None,
  showExternalParam: Boolean = false,
  validation: Option[ValidationRules] = None
)

enum PropertyType:
  case String, Text, Checkbox, Dropdown, Set, Dictionary
  case Object, ObjectList, StringList, Autocomplete

sealed trait PropertyValue
object PropertyValue:
  case class StringVal(value: String) extends PropertyValue
  case class BoolVal(value: Boolean) extends PropertyValue
  case class IntVal(value: Int) extends PropertyValue
  case class ListVal(values: List[PropertyValue]) extends PropertyValue
  case class ObjectVal(fields: Map[String, PropertyValue]) extends PropertyValue
```

### Pattern 3: HTMX Handler Convention (on* Methods)
**What:** Methods starting with `on` become HTMX handlers, routed via component alias
**When to use:** All interactive component behaviors
**Example:**
```scala
// Source: WinterCMS AJAX handler pattern + CONTEXT.md decisions
trait BlogPostsComponent extends SummerComponent:

  // HTMX handler: POST /summer/component/blogPosts/onLoadMore
  def onLoadMore: ZIO[ComponentEnv, ComponentError, HtmxResponse] =
    for
      page    <- getFormValue("page").map(_.toIntOption.getOrElse(1))
      posts   <- loadPosts(page)
      html    <- renderPartial("items", Map("posts" -> posts))
      hasMore <- ZIO.succeed(posts.nonEmpty)
    yield HtmxResponse(
      html = html,
      triggers = if hasMore then Map.empty else Map("postsExhausted" -> "true"),
      retarget = None  // Use original hx-target
    )

  // HTMX handler: POST /summer/component/blogPosts/onFilter
  def onFilter: ZIO[ComponentEnv, ComponentError, HtmxResponse] =
    for
      category <- getFormValue("category")
      posts    <- loadPosts(1, category)
      html     <- renderPartial("default")
    yield HtmxResponse(html)

case class HtmxResponse(
  html: Html,
  triggers: Map[String, String] = Map.empty,      // HX-Trigger header
  retarget: Option[String] = None,                // HX-Retarget header
  reswap: Option[String] = None,                  // HX-Reswap header
  oobSwaps: List[OobSwap] = List.empty            // Out-of-band swaps
)

case class OobSwap(
  targetId: String,
  html: Html,
  swapStrategy: String = "outerHTML"
)
```

### Pattern 4: Component Registration in Plugin
**What:** Plugins register components via `registerComponents` method returning alias -> class mapping
**When to use:** Plugin boot phase, collected by ComponentManager
**Example:**
```scala
// Source: WinterCMS Plugin.registerComponents pattern
class BlogPlugin extends SummerPlugin:
  override def id = PluginId("golem15", "blog")

  override def registerComponents: Map[String, ComponentFactory] =
    Map(
      "blogPost"       -> ComponentFactory[BlogPostComponent],
      "blogPosts"      -> ComponentFactory[BlogPostsComponent],
      "blogCategories" -> ComponentFactory[BlogCategoriesComponent],
      "blogRssFeed"    -> ComponentFactory[BlogRssFeedComponent]
    )

trait ComponentFactory[C <: SummerComponent]:
  def create(alias: String, properties: Map[String, PropertyValue], ctx: PageContext): ZIO[Any, Nothing, C]

// ComponentManager collects from all plugins
trait ComponentManager:
  def listComponents: UIO[Map[String, ComponentFactory[?]]]
  def resolve(aliasOrClass: String): UIO[Option[ComponentFactory[?]]]
  def make[C <: SummerComponent](
    alias: String,
    properties: Map[String, PropertyValue],
    ctx: PageContext
  ): IO[ComponentError, C]
```

### Pattern 5: Pebble Template Integration
**What:** Use Pebble engine for Twig-compatible templates with component context
**When to use:** All component template rendering
**Example:**
```java
// Source: Pebble Templates documentation
// Pebble template syntax (Twig-compatible)
// plugins/golem15/blog/components/posts/default.htm
{% extends "layout" %}

{% block content %}
<div class="blog-posts" id="posts-{{ __SELF__ }}">
  {% if posts is empty %}
    <p class="no-posts">{{ noPostsMessage }}</p>
  {% else %}
    {% for post in posts %}
      {% include "posts/item" %}
    {% endfor %}

    {% if hasMorePosts %}
    <button
      hx-post="{{ componentHandler('onLoadMore') }}"
      hx-target="#posts-{{ __SELF__ }}"
      hx-swap="beforeend"
      hx-vals='{"page": {{ currentPage + 1 }}}'
      class="btn btn-load-more">
      {{ 'Load More'|_ }}
    </button>
    {% endif %}
  {% endif %}
</div>
{% endblock %}
```

```scala
// Source: Pebble Java API wrapped for ZIO
import io.pebbletemplates.pebble.PebbleEngine
import io.pebbletemplates.pebble.loader.FileLoader

trait TemplateService:
  def render(template: String, context: Map[String, Any]): Task[Html]
  def renderString(templateContent: String, context: Map[String, Any]): Task[Html]

object TemplateService:
  val live: ZLayer[PluginManager, Nothing, TemplateService] =
    ZLayer.fromFunction { (plugins: PluginManager) =>
      new TemplateService:
        private val engine = new PebbleEngine.Builder()
          .loader(new ComponentTemplateLoader(plugins))
          .autoEscaping(true)
          .defaultLocale(Locale.ENGLISH)
          .build()

        def render(template: String, context: Map[String, Any]): Task[Html] =
          ZIO.attemptBlocking {
            val writer = new StringWriter()
            val ctx = context.asJava
            engine.getTemplate(template).evaluate(writer, ctx)
            Html(writer.toString)
          }
    }

// Custom Twig functions/filters
// {{ asset('img/logo.png') }} -> ThemeService.asset()
// {{ componentHandler('onRefresh') }} -> component URL builder
// {{ 'text'|_ }} -> Translation filter
```

### Pattern 6: CSRF Protection for HTMX Requests
**What:** Inject CSRF token via middleware, validate on all HTMX POST/PUT/DELETE
**When to use:** All HTMX handlers (per CONTEXT.md: CSRF always required)
**Example:**
```scala
// Source: HTMX CSRF pattern + OWASP recommendations
// Middleware injects CSRF token into response headers and validates requests
val csrfMiddleware: HandlerAspect[Any, Unit] = HandlerAspect.interceptHandler { handler =>
  Handler.fromFunctionZIO[Request] { request =>
    for
      session <- SessionService.get
      token   <- session.csrfToken
      _       <- ZIO.when(request.method.isUnsafe) {
        val headerToken = request.header("X-CSRF-Token").map(_.value)
        val formToken = request.bodyAsMultipartForm.flatMap(_.get("_token"))
        val requestToken = headerToken.orElse(formToken)
        ZIO.when(requestToken != Some(token)) {
          ZIO.fail(CsrfViolation("Invalid CSRF token"))
        }
      }
      response <- handler(request)
    yield response.addHeader("X-CSRF-Token", token)
  }
}

// Template helper for forms
// <input type="hidden" name="_token" value="{{ csrfToken() }}">

// HTMX global header injection (in base layout)
// <body hx-headers='{"X-CSRF-Token": "{{ csrfToken() }}"}'>
```

### Pattern 7: Server-Sent Events for Real-Time Updates
**What:** Components can push updates to clients via SSE stream
**When to use:** Live updates (chat, notifications, progress bars)
**Example:**
```scala
// Source: ZIO HTTP SSE documentation
// Component declares SSE capability
trait LiveNotifications extends SummerComponent:

  def onSubscribe: ZIO[ComponentEnv, ComponentError, Response] =
    for
      userId  <- getCurrentUserId
      stream  <- NotificationService.subscribeUser(userId)
      sse     <- ZIO.succeed(
        stream.map { notification =>
          ServerSentEvent(
            data = renderNotification(notification),
            event = Some("notification"),
            id = Some(notification.id.toString)
          )
        }
      )
    yield Response(
      status = Status.Ok,
      headers = Headers(
        Header.ContentType(MediaType.text.`event-stream`),
        Header.CacheControl.NoCache
      ),
      body = Body.fromStreamSSE(sse)
    )

// Template usage
// <div hx-ext="sse" sse-connect="{{ componentHandler('onSubscribe') }}">
//   <div sse-swap="notification" hx-swap="beforeend"></div>
// </div>
```

### Pattern 8: Out-of-Band Swaps for Multiple Updates
**What:** Single HTMX response updates multiple DOM elements
**When to use:** Handler needs to update content outside its target
**Example:**
```scala
// Source: htmx hx-swap-oob documentation
def onAddToCart: ZIO[ComponentEnv, ComponentError, HtmxResponse] =
  for
    productId <- getFormValue("product_id")
    _         <- CartService.addItem(productId)
    cart      <- CartService.getCart
    itemHtml  <- renderPartial("cart-item", Map("item" -> cart.lastItem))
    countHtml <- renderFragment(s"""<span id="cart-count" hx-swap-oob="true">${cart.itemCount}</span>""")
  yield HtmxResponse(
    html = itemHtml,
    oobSwaps = List(
      OobSwap("cart-count", countHtml),
      OobSwap("cart-total", renderCartTotal(cart))
    )
  )

// Response includes primary HTML plus OOB fragments:
// <div>... item HTML ...</div>
// <span id="cart-count" hx-swap-oob="true">5</span>
// <span id="cart-total" hx-swap-oob="true">$99.00</span>
```

### Anti-Patterns to Avoid
- **Blocking in handlers:** Always return `ZIO`, never block threads
- **Direct database access in components:** Use services, not repositories directly
- **Mutable component state:** Use `Ref` for any state, properties are immutable per request
- **Missing CSRF on unsafe methods:** All POST/PUT/DELETE require validation
- **String concatenation for HTML:** Use templates or ScalaTags, never raw strings
- **Synchronous template rendering:** Wrap Pebble in `ZIO.attemptBlocking`
- **Hard-coded component aliases:** Use `__SELF__` macro in templates for uniqueness

## Don't Hand-Roll

Problems that look simple but have existing solutions:

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Twig-compatible templates | Custom parser | Pebble | Complex inheritance, filters, autoescaping |
| HTMX response headers | Manual string headers | zio-http-htmx | Type-safe headers, correct formatting |
| HTMX attribute constants | String literals | htmx4s-constants | IDE docs, typo prevention, refactoring |
| HTML escaping | Manual escaping | Pebble autoescaping | XSS vulnerabilities, edge cases |
| CSRF token generation | Random strings | Secure random + HMAC | Timing attacks, entropy issues |
| Property validation | Custom validators | Use WinterCMS patterns | Complex rules (regex, ranges, dependencies) |
| Component discovery | Manual registration | Reflective scan or explicit registration | Miss components, ordering issues |

**Key insight:** Component systems appear simple but involve complex concerns: property validation, template inheritance, security (XSS, CSRF), lifecycle management, and state isolation. Use proven patterns from WinterCMS adapted to ZIO.

## Common Pitfalls

### Pitfall 1: Template Escaping Disabled Accidentally
**What goes wrong:** XSS vulnerabilities when rendering user content
**Why it happens:** Using `|raw` filter or disabling autoescaping for convenience
**How to avoid:** Keep autoescaping enabled, use `|raw` only for known-safe HTML fragments
**Warning signs:** HTML entities appearing in output, or HTML rendering unexpectedly

### Pitfall 2: Component State Leaking Between Requests
**What goes wrong:** User A sees User B's data
**Why it happens:** Storing state in class variables instead of request-scoped Ref
**How to avoid:** All component state via `Ref` created per-request, never `var`
**Warning signs:** Inconsistent behavior under load, data appearing randomly

### Pitfall 3: HTMX Handler Not Found (404)
**What goes wrong:** HTMX POST returns 404, component method exists
**Why it happens:** Handler method doesn't start with `on`, or component not registered
**How to avoid:** Naming convention enforcement, handler discovery at boot time
**Warning signs:** Console shows POST to correct URL, but 404 returned

### Pitfall 4: CSRF Token Mismatch After Cache
**What goes wrong:** Form submissions fail with CSRF error after page was cached
**Why it happens:** CDN/browser cached page with old CSRF token
**How to avoid:** Use `Vary: Cookie` header, or refresh token via HTMX header on each response
**Warning signs:** Works in development, fails in production with caching

### Pitfall 5: SSE Connection Exhaustion
**What goes wrong:** Server runs out of connections, new SSE subscriptions fail
**Why it happens:** Clients not closing SSE connections, no timeout
**How to avoid:** Set connection timeout, use heartbeat to detect dead connections
**Warning signs:** Gradual slowdown, connection refused errors

### Pitfall 6: Template Path Traversal
**What goes wrong:** Attacker reads arbitrary files via template include
**Why it happens:** User input in template path without validation
**How to avoid:** Whitelist allowed templates, never use user input directly in paths
**Warning signs:** Error messages revealing file paths, unexpected template content

### Pitfall 7: Blocking Pebble Rendering on ZIO Runtime
**What goes wrong:** Thread pool exhaustion, timeout errors under load
**Why it happens:** Pebble is synchronous, blocking ZIO fibers
**How to avoid:** Wrap Pebble calls in `ZIO.attemptBlocking`, use blocking thread pool
**Warning signs:** High latency spikes, fiber count explosion

## Code Examples

Verified patterns from official sources:

### Complete Component Implementation
```scala
// Source: WinterCMS Account component pattern adapted to Scala/ZIO
package golem15.blog.components

import zio.*
import io.circe.yaml.parser
import io.pebbletemplates.pebble.PebbleEngine

class BlogPostsComponent(
  val alias: String,
  val propertySchema: List[PropertyDef],
  val properties: Ref[Map[String, PropertyValue]],
  val pageContext: PageContext,
  templateService: TemplateService,
  postService: PostService
) extends SummerComponent:

  override val details = ComponentDetails(
    name = "Blog Posts",
    description = "Displays a list of blog posts",
    icon = Some("pencil")
  )

  // Page variables set in onRun, available in template
  private var posts: List[Post] = List.empty
  private var category: Option[Category] = None
  private var pageParam: String = "page"

  override def onRun: ZIO[ComponentEnv, ComponentError, Unit] =
    for
      props <- properties.get
      catSlug = props.get("categoryFilter").collect { case StringVal(s) => s }
      cat <- catSlug.fold(ZIO.none)(slug =>
        categoryService.findBySlug(slug).map(Some(_))
      )
      perPage = props.get("postsPerPage")
        .collect { case StringVal(s) => s.toIntOption }
        .flatten.getOrElse(10)
      page = pageContext.param("page").flatMap(_.toIntOption).getOrElse(1)
      result <- postService.listFrontend(
        page = page,
        perPage = perPage,
        category = cat.map(_.id),
        published = !pageContext.isBackendEditor
      )
    yield
      this.posts = result.items
      this.category = cat
      this.pageParam = paramName("pageNumber").getOrElse("page")

  override def render: ZIO[ComponentEnv, ComponentError, Html] =
    for
      props <- properties.get
      noPostsMsg = props.get("noPostsMessage")
        .collect { case StringVal(s) => s }
        .getOrElse("No posts found.")
      html <- templateService.render(
        s"$alias/default",
        Map(
          "__SELF__" -> alias,
          "posts" -> posts.asJava,
          "category" -> category.orNull,
          "noPostsMessage" -> noPostsMsg,
          "pageParam" -> pageParam
        )
      )
    yield html

  // HTMX Handler: Load more posts
  def onLoadMore: ZIO[ComponentEnv, ComponentError, HtmxResponse] =
    for
      page <- ZIO.serviceWithZIO[Request](_.formField("page"))
        .map(_.flatMap(_.toIntOption).getOrElse(1))
      props <- properties.get
      perPage = props.get("postsPerPage")
        .collect { case StringVal(s) => s.toIntOption }
        .flatten.getOrElse(10)
      result <- postService.listFrontend(page = page, perPage = perPage)
      html <- templateService.render(s"$alias/items", Map("posts" -> result.items.asJava))
    yield HtmxResponse(
      html = html,
      triggers = if result.hasMore then Map.empty else Map("postsExhausted" -> "true")
    )

  // Dynamic options for categoryFilter dropdown
  def getCategoryFilterOptions: UIO[Map[String, String]] =
    categoryService.listAll.map { cats =>
      Map("" -> "- All Categories -") ++ cats.map(c => c.slug -> c.name).toMap
    }
```

### HTMX Handler Router
```scala
// Source: ZIO HTTP routing + WinterCMS component handler pattern
object ComponentRoutes:

  val routes: Routes[ComponentManager & TemplateService & SessionService, Response] =
    Routes(
      // Component HTMX handler endpoint
      Method.POST / "summer" / "component" / string("alias") / string("handler") ->
        handler { (alias: String, handlerName: String, req: Request) =>
          for
            _ <- CsrfService.validate(req)
            manager <- ZIO.service[ComponentManager]
            factory <- manager.resolve(alias).someOrFail(ComponentNotFound(alias))
            ctx <- PageContext.fromRequest(req)
            props <- extractProperties(req)
            component <- factory.create(alias, props, ctx)
            result <- invokeHandler(component, handlerName)
            response <- buildHtmxResponse(result)
          yield response
        }
    )

  private def invokeHandler(
    component: SummerComponent,
    handlerName: String
  ): IO[ComponentError, HtmxResponse] =
    // Validate handler name starts with "on"
    if !handlerName.startsWith("on") then
      ZIO.fail(InvalidHandler(s"Handler must start with 'on': $handlerName"))
    else
      // Use reflection or macro to invoke handler method
      component.invokeHandler(handlerName)
```

### YAML Property Schema Parser
```scala
// Source: circe-yaml + WinterCMS Inspector format
import io.circe.*
import io.circe.generic.auto.*
import io.circe.yaml.parser

case class ComponentYaml(
  name: String,
  description: String,
  icon: Option[String] = None,
  properties: Map[String, PropertyDefYaml] = Map.empty
)

case class PropertyDefYaml(
  `type`: String,
  title: String,
  description: Option[String] = None,
  default: Option[Json] = None,
  options: Option[Map[String, String]] = None,
  group: Option[String] = None,
  showExternalParam: Option[Boolean] = None,
  validation: Option[ValidationYaml] = None
)

def parseComponentYaml(path: Path): IO[YamlError, ComponentSchema] =
  for
    content <- ZIO.attemptBlocking(Files.readString(path))
      .mapError(e => YamlError.ReadError(path, e))
    yaml <- ZIO.fromEither(parser.parse(content))
      .mapError(e => YamlError.ParseError(path, e))
    schema <- ZIO.fromEither(yaml.as[ComponentYaml])
      .mapError(e => YamlError.DecodeError(path, e))
  yield toComponentSchema(schema)

def toComponentSchema(yaml: ComponentYaml): ComponentSchema =
  ComponentSchema(
    name = yaml.name,
    description = yaml.description,
    icon = yaml.icon,
    properties = yaml.properties.map { case (name, prop) =>
      name -> PropertyDef(
        `type` = PropertyType.valueOf(prop.`type`.capitalize),
        title = prop.title,
        description = prop.description,
        default = prop.default.flatMap(parsePropertyValue),
        options = prop.options,
        group = prop.group,
        showExternalParam = prop.showExternalParam.getOrElse(false),
        validation = prop.validation.map(parseValidation)
      )
    }
  )
```

### Pebble Custom Functions for Components
```scala
// Source: Pebble extension API
import io.pebbletemplates.pebble.extension.*
import io.pebbletemplates.pebble.template.EvaluationContext

class SummerPebbleExtension(
  themeService: ThemeService,
  componentContext: ComponentContext
) extends AbstractExtension:

  override def getFunctions: java.util.Map[String, Function] =
    Map(
      "asset" -> new AssetFunction(themeService),
      "componentHandler" -> new ComponentHandlerFunction(componentContext),
      "csrfToken" -> new CsrfTokenFunction(componentContext)
    ).asJava

  override def getFilters: java.util.Map[String, Filter] =
    Map(
      "_" -> new TranslateFilter(componentContext)  // {{ 'text'|_ }}
    ).asJava

class ComponentHandlerFunction(ctx: ComponentContext) extends Function:
  override def getArgumentNames = List("handler").asJava

  override def execute(
    args: java.util.Map[String, Object],
    self: PebbleTemplate,
    context: EvaluationContext,
    lineNumber: Int
  ): Object =
    val handler = args.get("handler").toString
    val alias = context.getVariable("__SELF__").toString
    s"/summer/component/$alias/$handler"
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| AJAX with JSON responses | HTMX with HTML fragments | 2020+ | Simpler, less JS, server-rendered |
| SPA with React/Vue | Server-side + HTMX | 2023+ | Lower complexity for CMS-style apps |
| Custom templating | Pebble (Twig-compatible) | Established | Migration from WinterCMS themes |
| Callback-based handlers | ZIO effect-based handlers | ZIO 2.0 | Type-safe async, resource safety |
| Session-based CSRF | Per-request tokens + SameSite | 2024+ | Better security, stateless option |
| Polling for updates | Server-Sent Events | 2020+ | Efficient real-time, browser-native |

**Deprecated/outdated:**
- jQuery AJAX: Use HTMX data attributes instead
- PHP component classes: Scala traits with ZIO
- `defineProperties()` method: YAML file at `component.yaml`
- Synchronous handlers: All handlers return `ZIO`

## Open Questions

Things that couldn't be fully resolved:

1. **Template Hot-Reload in Development**
   - What we know: Pebble caches compiled templates
   - What's unclear: Best pattern for dev-mode reloading without restart
   - Recommendation: Use Pebble's cache.invalidate() on file change, integrate with Mill -w

2. **Component Inheritance Depth**
   - What we know: CONTEXT.md allows component inheritance
   - What's unclear: How deep inheritance should go, multiple inheritance
   - Recommendation: Single-level inheritance only (extend one base), use composition for reuse

3. **Property Type Extensibility**
   - What we know: WinterCMS has fixed property types (string, dropdown, etc.)
   - What's unclear: How plugins can define custom property types
   - Recommendation: Start with WinterCMS types, add extension mechanism in v2 if needed

4. **Pebble vs ScalaTags for Fragments**
   - What we know: Pebble for templates, ScalaTags for programmatic HTML
   - What's unclear: When to use which, can they interop?
   - Recommendation: Pebble for component templates, ScalaTags for helper methods returning `Html`

5. **HTMX Extension Loading**
   - What we know: HTMX has extensions (sse, json-enc, etc.)
   - What's unclear: How to manage extension loading per-component
   - Recommendation: Load common extensions in base layout, document per-component needs

## Sources

### Primary (HIGH confidence)
- [ZIO HTTP HTMX Module](https://index.scala-lang.org/zio/zio-http/artifacts/zio-http-htmx/3.3.0) - Official module, version 3.3.0
- [ZIO HTTP SSE Examples](https://ziohttp.com/examples/server-sent-events-in-endpoints/) - Server-Sent Events pattern
- [Pebble Templates](https://pebbletemplates.io/) - Twig-compatible templating, version 4.1.1
- [htmx4s Library](https://github.com/eikek/htmx4s) - Type-safe HTMX constants for Scala
- [ScalaTags Documentation](https://com-lihaoyi.github.io/scalatags/) - Programmatic HTML generation
- [HTMX OOB Swaps](https://htmx.org/attributes/hx-swap-oob/) - Out-of-band swap patterns
- [HTMX CSRF Patterns](https://htmx.org/essays/web-security-basics-with-htmx/) - Security best practices

### Secondary (MEDIUM confidence)
- [rockthejvm/scalatags-htmx-demo](https://github.com/rockthejvm/scalatags-htmx-demo) - ZIO + ScalaTags + HTMX architecture
- WinterCMS ComponentBase.php - Reference implementation (local file)
- WinterCMS Inspector documentation - Property schema format (local file)
- [OWASP CSRF Prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) - Token patterns

### Tertiary (LOW confidence)
- Phase 2 Research - circe-yaml patterns (reuse)
- Phase 1 Research - ZIO service patterns (reuse)

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH - zio-http-htmx and Pebble verified via official sources
- Architecture patterns: HIGH - Based on WinterCMS reference + ZIO official patterns
- HTMX integration: HIGH - Official HTMX docs and zio-http module
- Template engine: MEDIUM - Pebble is established but interop with ZIO needs validation
- Property system: MEDIUM - Adapted from WinterCMS, Scala-specific validation untested

**Research date:** 2026-02-05
**Valid until:** 2026-03-05 (30 days - stable patterns, Pebble/HTMX established)
