Phase 03: Component System - 2 plan(s) in 2 wave(s) - Wave 1: 03-01 (registration and lifecycle) - parallel - Wave 2: 03-02 (HTMX routing and response) - sequential - Ready for execution
653 lines
21 KiB
Markdown
653 lines
21 KiB
Markdown
---
|
|
phase: 03-component-system
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- build.mill
|
|
- summercms/src/component/SummerComponent.scala
|
|
- summercms/src/component/ComponentDetails.scala
|
|
- summercms/src/component/ComponentError.scala
|
|
- summercms/src/component/PropertyDef.scala
|
|
- summercms/src/component/PropertyValue.scala
|
|
- summercms/src/component/ComponentSchema.scala
|
|
- summercms/src/component/ComponentFactory.scala
|
|
- summercms/src/component/ComponentManager.scala
|
|
- summercms/src/plugin/PluginRegistration.scala
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Developer can define component properties in YAML with types (string, dropdown, checkbox)"
|
|
- "Components have lifecycle hooks: init (once) and onRun (per request)"
|
|
- "Plugins can register components via registerComponents method"
|
|
- "ComponentManager collects all components from loaded plugins"
|
|
- "Property schemas parse from YAML with validation rules"
|
|
artifacts:
|
|
- path: "summercms/src/component/SummerComponent.scala"
|
|
provides: "Base trait for all components with lifecycle"
|
|
contains: "trait SummerComponent"
|
|
- path: "summercms/src/component/ComponentManager.scala"
|
|
provides: "Service managing all registered components"
|
|
exports: ["ComponentManager"]
|
|
- path: "summercms/src/component/PropertyDef.scala"
|
|
provides: "YAML property definition types"
|
|
contains: "case class PropertyDef"
|
|
- path: "summercms/src/component/ComponentSchema.scala"
|
|
provides: "YAML component schema parser"
|
|
contains: "object ComponentSchema"
|
|
key_links:
|
|
- from: "summercms/src/component/ComponentManager.scala"
|
|
to: "summercms/src/plugin/PluginManager.scala"
|
|
via: "collects components from plugin registrations"
|
|
pattern: "PluginManager\\.getRegistration"
|
|
- from: "summercms/src/component/ComponentSchema.scala"
|
|
to: "circe-yaml"
|
|
via: "YAML parsing"
|
|
pattern: "parser\\.parse"
|
|
---
|
|
|
|
<objective>
|
|
Create the component registration and lifecycle foundation for SummerCMS.
|
|
|
|
Purpose: Components are reusable UI pieces that plugins provide. This plan establishes the component trait, property schema parsing, and the manager that collects components from plugins. The HTMX routing (03-02) builds on this foundation.
|
|
|
|
Output: SummerComponent trait, PropertyDef/PropertyValue types, ComponentSchema parser, ComponentManager service, updated PluginRegistration
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
@.planning/phases/03-component-system/03-RESEARCH.md
|
|
@.planning/phases/03-component-system/03-CONTEXT.md
|
|
@build.mill
|
|
@summercms/src/plugin/PluginManager.scala
|
|
@summercms/src/plugin/PluginRegistration.scala
|
|
@summercms/src/plugin/SummerPlugin.scala
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add component system dependencies to build.mill</name>
|
|
<files>build.mill</files>
|
|
<action>
|
|
Add the following dependencies to build.mill mvnDeps after the existing Flyway/logging dependencies:
|
|
|
|
```scala
|
|
// Component system - Templating
|
|
mvn"io.pebbletemplates:pebble:4.1.1",
|
|
|
|
// Component system - HTMX integration
|
|
mvn"dev.zio::zio-http-htmx:3.0.1",
|
|
|
|
// Component system - ScalaTags for programmatic HTML
|
|
mvn"com.lihaoyi::scalatags:0.13.1",
|
|
```
|
|
|
|
Note:
|
|
- Use zio-http-htmx:3.0.1 to match zio-http:3.0.1 already in build.mill
|
|
- htmx4s-constants is optional (adds type-safe HTMX attribute constants) - skip for now, can add later if needed
|
|
- circe-yaml:0.16.1 will be added by Phase 2 plans - if not present yet, add it:
|
|
|
|
```scala
|
|
// YAML parsing (for component.yaml and plugin.yaml)
|
|
mvn"io.circe::circe-yaml:0.16.1",
|
|
mvn"io.circe::circe-generic:0.14.10",
|
|
mvn"io.circe::circe-parser:0.14.10",
|
|
```
|
|
</action>
|
|
<verify>Run `./mill summercms.compile` - dependencies resolve successfully</verify>
|
|
<done>build.mill contains Pebble 4.1.1, zio-http-htmx, ScalaTags dependencies</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create component property types and schema parser</name>
|
|
<files>
|
|
summercms/src/component/PropertyDef.scala
|
|
summercms/src/component/PropertyValue.scala
|
|
summercms/src/component/ComponentSchema.scala
|
|
summercms/src/component/ComponentError.scala
|
|
summercms/src/component/ComponentDetails.scala
|
|
</files>
|
|
<action>
|
|
Create `summercms/src/component/` directory with these files:
|
|
|
|
**ComponentError.scala:**
|
|
```scala
|
|
package component
|
|
|
|
import java.nio.file.Path
|
|
|
|
/** Component system error ADT */
|
|
enum ComponentError:
|
|
case SchemaNotFound(path: Path)
|
|
case SchemaParseError(path: Path, message: String)
|
|
case ComponentNotFound(alias: String)
|
|
case InvalidHandler(alias: String, handler: String, reason: String)
|
|
case RenderError(alias: String, template: String, cause: Throwable)
|
|
case PropertyValidationError(alias: String, property: String, message: String)
|
|
case CsrfViolation(message: String)
|
|
|
|
def message: String = this match
|
|
case SchemaNotFound(p) => s"Component schema not found: $p"
|
|
case SchemaParseError(p, m) => s"Failed to parse component schema at $p: $m"
|
|
case ComponentNotFound(a) => s"Component not found: $a"
|
|
case InvalidHandler(a, h, r) => s"Invalid handler '$h' on component '$a': $r"
|
|
case RenderError(a, t, c) => s"Failed to render template '$t' for component '$a': ${c.getMessage}"
|
|
case PropertyValidationError(a, p, m) => s"Property '$p' on component '$a' failed validation: $m"
|
|
case CsrfViolation(m) => s"CSRF violation: $m"
|
|
```
|
|
|
|
**ComponentDetails.scala:**
|
|
```scala
|
|
package component
|
|
|
|
/** Component metadata from YAML schema */
|
|
case class ComponentDetails(
|
|
name: String,
|
|
description: String,
|
|
icon: Option[String] = None
|
|
)
|
|
```
|
|
|
|
**PropertyValue.scala:**
|
|
```scala
|
|
package component
|
|
|
|
import io.circe.*
|
|
|
|
/** Runtime property values for components */
|
|
sealed trait PropertyValue:
|
|
def asString: Option[String]
|
|
def asBoolean: Option[Boolean]
|
|
def asInt: Option[Int]
|
|
def asList: Option[List[PropertyValue]]
|
|
def asMap: Option[Map[String, PropertyValue]]
|
|
|
|
object PropertyValue:
|
|
case class StringVal(value: String) extends PropertyValue:
|
|
def asString = Some(value)
|
|
def asBoolean = None
|
|
def asInt = value.toIntOption
|
|
def asList = None
|
|
def asMap = None
|
|
|
|
case class BoolVal(value: Boolean) extends PropertyValue:
|
|
def asString = Some(value.toString)
|
|
def asBoolean = Some(value)
|
|
def asInt = None
|
|
def asList = None
|
|
def asMap = None
|
|
|
|
case class IntVal(value: Int) extends PropertyValue:
|
|
def asString = Some(value.toString)
|
|
def asBoolean = None
|
|
def asInt = Some(value)
|
|
def asList = None
|
|
def asMap = None
|
|
|
|
case class ListVal(values: List[PropertyValue]) extends PropertyValue:
|
|
def asString = None
|
|
def asBoolean = None
|
|
def asInt = None
|
|
def asList = Some(values)
|
|
def asMap = None
|
|
|
|
case class ObjectVal(fields: Map[String, PropertyValue]) extends PropertyValue:
|
|
def asString = None
|
|
def asBoolean = None
|
|
def asInt = None
|
|
def asList = None
|
|
def asMap = Some(fields)
|
|
|
|
case object NullVal extends PropertyValue:
|
|
def asString = None
|
|
def asBoolean = None
|
|
def asInt = None
|
|
def asList = None
|
|
def asMap = None
|
|
|
|
/** Parse JSON value to PropertyValue */
|
|
def fromJson(json: Json): PropertyValue =
|
|
json.fold(
|
|
jsonNull = NullVal,
|
|
jsonBoolean = BoolVal(_),
|
|
jsonNumber = n => n.toInt.map(IntVal(_)).getOrElse(StringVal(n.toString)),
|
|
jsonString = StringVal(_),
|
|
jsonArray = arr => ListVal(arr.map(fromJson).toList),
|
|
jsonObject = obj => ObjectVal(obj.toMap.view.mapValues(fromJson).toMap)
|
|
)
|
|
|
|
/** Implicit decoder for circe */
|
|
given Decoder[PropertyValue] = Decoder.instance { cursor =>
|
|
Right(fromJson(cursor.value))
|
|
}
|
|
```
|
|
|
|
**PropertyDef.scala:**
|
|
```scala
|
|
package component
|
|
|
|
import io.circe.*
|
|
import io.circe.generic.semiauto.*
|
|
|
|
/** Property type enum matching WinterCMS Inspector types */
|
|
enum PropertyType:
|
|
case String, Text, Number, Checkbox, Dropdown, Set
|
|
case Dictionary, Object, ObjectList, StringList
|
|
case Autocomplete, ColorPicker, CodeEditor, FileUpload
|
|
|
|
object PropertyType:
|
|
/** Parse string to PropertyType (case-insensitive) */
|
|
def fromString(s: String): Option[PropertyType] =
|
|
PropertyType.values.find(_.toString.equalsIgnoreCase(s))
|
|
|
|
given Decoder[PropertyType] = Decoder.decodeString.emap { s =>
|
|
fromString(s).toRight(s"Unknown property type: $s")
|
|
}
|
|
|
|
/** Validation rules for a property */
|
|
case class ValidationRules(
|
|
required: Option[Boolean] = None,
|
|
requiredMessage: Option[String] = None,
|
|
min: Option[Int] = None,
|
|
minMessage: Option[String] = None,
|
|
max: Option[Int] = None,
|
|
maxMessage: Option[String] = None,
|
|
pattern: Option[String] = None,
|
|
patternMessage: Option[String] = None
|
|
)
|
|
|
|
object ValidationRules:
|
|
given Decoder[ValidationRules] = deriveDecoder[ValidationRules]
|
|
|
|
/** Property definition from component.yaml */
|
|
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: Option[Boolean] = None, // Allow {{ :param }} binding
|
|
validation: Option[ValidationRules] = None
|
|
):
|
|
def propertyType: PropertyType = `type`
|
|
def isRequired: Boolean = validation.flatMap(_.required).getOrElse(false)
|
|
|
|
object PropertyDef:
|
|
given Decoder[PropertyDef] = deriveDecoder[PropertyDef]
|
|
```
|
|
|
|
**ComponentSchema.scala:**
|
|
```scala
|
|
package component
|
|
|
|
import io.circe.*
|
|
import io.circe.generic.semiauto.*
|
|
import io.circe.yaml.parser
|
|
import zio.*
|
|
import java.nio.file.{Files, Path}
|
|
|
|
/** Component schema parsed from component.yaml */
|
|
case class ComponentSchema(
|
|
name: String,
|
|
description: String,
|
|
icon: Option[String] = None,
|
|
properties: Map[String, PropertyDef] = Map.empty
|
|
):
|
|
def toDetails: ComponentDetails = ComponentDetails(name, description, icon)
|
|
|
|
object ComponentSchema:
|
|
given Decoder[ComponentSchema] = deriveDecoder[ComponentSchema]
|
|
|
|
/** Parse YAML string into ComponentSchema */
|
|
def parse(yaml: String): Either[String, ComponentSchema] =
|
|
parser.parse(yaml)
|
|
.flatMap(_.as[ComponentSchema])
|
|
.left.map(_.getMessage)
|
|
|
|
/** Parse YAML file at path */
|
|
def parseFile(path: Path): IO[ComponentError, ComponentSchema] =
|
|
for
|
|
exists <- ZIO.attemptBlocking(Files.exists(path))
|
|
.orDie
|
|
_ <- ZIO.fail(ComponentError.SchemaNotFound(path)).when(!exists)
|
|
content <- ZIO.attemptBlocking(Files.readString(path))
|
|
.mapError(e => ComponentError.SchemaParseError(path, e.getMessage))
|
|
schema <- ZIO.fromEither(parse(content))
|
|
.mapError(msg => ComponentError.SchemaParseError(path, msg))
|
|
yield schema
|
|
```
|
|
</action>
|
|
<verify>Run `./mill summercms.compile` - all component types compile without errors</verify>
|
|
<done>PropertyValue ADT, PropertyDef with validation, PropertyType enum, ComponentSchema parser, ComponentError ADT</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Create SummerComponent trait and ComponentManager service</name>
|
|
<files>
|
|
summercms/src/component/SummerComponent.scala
|
|
summercms/src/component/ComponentFactory.scala
|
|
summercms/src/component/ComponentManager.scala
|
|
summercms/src/plugin/PluginRegistration.scala
|
|
</files>
|
|
<action>
|
|
**SummerComponent.scala:**
|
|
```scala
|
|
package component
|
|
|
|
import zio.*
|
|
import java.nio.file.Path
|
|
|
|
/** Page context available during component rendering */
|
|
case class PageContext(
|
|
url: String,
|
|
params: Map[String, String] = Map.empty,
|
|
isBackendEditor: Boolean = false
|
|
):
|
|
def param(name: String): Option[String] = params.get(name)
|
|
|
|
/** Base trait for all SummerCMS components */
|
|
trait SummerComponent:
|
|
/** Component alias (used in page templates) */
|
|
def alias: String
|
|
|
|
/** Component metadata from YAML schema */
|
|
def details: ComponentDetails
|
|
|
|
/** Property schema from component.yaml */
|
|
def propertySchema: Map[String, PropertyDef]
|
|
|
|
/** Current property values (set from page/layout configuration) */
|
|
def properties: Ref[Map[String, PropertyValue]]
|
|
|
|
/** Page context for this request */
|
|
def pageContext: PageContext
|
|
|
|
/** Path to component's template directory */
|
|
def templateDir: Path
|
|
|
|
/**
|
|
* Called once when component is initialized.
|
|
* Use for one-time setup before any handlers or rendering.
|
|
*/
|
|
def init: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit
|
|
|
|
/**
|
|
* Called when component is bound to page/layout, part of page lifecycle.
|
|
* Use for loading data needed for rendering.
|
|
*/
|
|
def onRun: ZIO[ComponentEnv, ComponentError, Unit] = ZIO.unit
|
|
|
|
/**
|
|
* Get a property value by name, with fallback to default from schema.
|
|
*/
|
|
def property(name: String): UIO[Option[PropertyValue]] =
|
|
properties.get.map { props =>
|
|
props.get(name).orElse(propertySchema.get(name).flatMap(_.default))
|
|
}
|
|
|
|
/**
|
|
* Get a property as string.
|
|
*/
|
|
def propertyString(name: String): UIO[Option[String]] =
|
|
property(name).map(_.flatMap(_.asString))
|
|
|
|
/**
|
|
* Get a property as int.
|
|
*/
|
|
def propertyInt(name: String): UIO[Option[Int]] =
|
|
property(name).map(_.flatMap(_.asInt))
|
|
|
|
/**
|
|
* Get a property as boolean.
|
|
*/
|
|
def propertyBool(name: String): UIO[Option[Boolean]] =
|
|
property(name).map(_.flatMap(_.asBoolean))
|
|
|
|
/** Environment available to components */
|
|
type ComponentEnv = PageContext
|
|
```
|
|
|
|
**ComponentFactory.scala:**
|
|
```scala
|
|
package component
|
|
|
|
import zio.*
|
|
import java.nio.file.Path
|
|
|
|
/** Factory for creating component instances */
|
|
trait ComponentFactory:
|
|
/** Component class name (fully qualified) */
|
|
def className: String
|
|
|
|
/** Schema parsed from component.yaml */
|
|
def schema: ComponentSchema
|
|
|
|
/** Path to component's directory (contains templates) */
|
|
def directory: Path
|
|
|
|
/** Create a component instance with given alias, properties, and context */
|
|
def create(
|
|
alias: String,
|
|
initialProperties: Map[String, PropertyValue],
|
|
ctx: PageContext
|
|
): UIO[SummerComponent]
|
|
|
|
object ComponentFactory:
|
|
/**
|
|
* Create a factory from a component class and schema.
|
|
* The factory will create instances with the given properties.
|
|
*/
|
|
def apply[C <: SummerComponent](
|
|
cls: Class[C],
|
|
schema: ComponentSchema,
|
|
directory: Path,
|
|
constructor: (String, Map[String, PropertyDef], Ref[Map[String, PropertyValue]], PageContext, Path) => C
|
|
): ComponentFactory = new ComponentFactory:
|
|
val className = cls.getName
|
|
val schema_ = schema
|
|
def schema = schema_
|
|
val directory_ = directory
|
|
def directory = directory_
|
|
|
|
def create(
|
|
alias: String,
|
|
initialProperties: Map[String, PropertyValue],
|
|
ctx: PageContext
|
|
): UIO[SummerComponent] =
|
|
Ref.make(initialProperties).map { propsRef =>
|
|
constructor(alias, schema.properties, propsRef, ctx, directory)
|
|
}
|
|
```
|
|
|
|
**ComponentManager.scala:**
|
|
```scala
|
|
package component
|
|
|
|
import zio.*
|
|
import plugin.{PluginId, PluginManager}
|
|
|
|
/** Registration info for a component */
|
|
case class ComponentRegistration(
|
|
pluginId: PluginId,
|
|
alias: String,
|
|
factory: ComponentFactory
|
|
)
|
|
|
|
/** Service for managing all registered components */
|
|
trait ComponentManager:
|
|
/** List all registered components */
|
|
def listComponents: UIO[List[ComponentRegistration]]
|
|
|
|
/** Find a component by alias */
|
|
def resolve(alias: String): UIO[Option[ComponentRegistration]]
|
|
|
|
/** Find a component by alias or class name */
|
|
def resolveByAliasOrClass(aliasOrClass: String): UIO[Option[ComponentRegistration]]
|
|
|
|
/** Create a component instance */
|
|
def create(
|
|
alias: String,
|
|
properties: Map[String, PropertyValue],
|
|
ctx: PageContext
|
|
): IO[ComponentError, SummerComponent]
|
|
|
|
/** Register a component from a plugin */
|
|
def register(pluginId: PluginId, alias: String, factory: ComponentFactory): UIO[Unit]
|
|
|
|
object ComponentManager:
|
|
/** List all components */
|
|
def listComponents: ZIO[ComponentManager, Nothing, List[ComponentRegistration]] =
|
|
ZIO.serviceWithZIO[ComponentManager](_.listComponents)
|
|
|
|
/** Resolve component by alias */
|
|
def resolve(alias: String): ZIO[ComponentManager, Nothing, Option[ComponentRegistration]] =
|
|
ZIO.serviceWithZIO[ComponentManager](_.resolve(alias))
|
|
|
|
/** Create component instance */
|
|
def create(
|
|
alias: String,
|
|
properties: Map[String, PropertyValue],
|
|
ctx: PageContext
|
|
): ZIO[ComponentManager, ComponentError, SummerComponent] =
|
|
ZIO.serviceWithZIO[ComponentManager](_.create(alias, properties, ctx))
|
|
|
|
/** Live implementation */
|
|
val live: ULayer[ComponentManager] = ZLayer.fromZIO {
|
|
Ref.make(Map.empty[String, ComponentRegistration]).map { registry =>
|
|
new ComponentManager:
|
|
def listComponents: UIO[List[ComponentRegistration]] =
|
|
registry.get.map(_.values.toList)
|
|
|
|
def resolve(alias: String): UIO[Option[ComponentRegistration]] =
|
|
registry.get.map(_.get(alias))
|
|
|
|
def resolveByAliasOrClass(aliasOrClass: String): UIO[Option[ComponentRegistration]] =
|
|
registry.get.map { regs =>
|
|
regs.get(aliasOrClass).orElse(
|
|
regs.values.find(_.factory.className == aliasOrClass)
|
|
)
|
|
}
|
|
|
|
def create(
|
|
alias: String,
|
|
properties: Map[String, PropertyValue],
|
|
ctx: PageContext
|
|
): IO[ComponentError, SummerComponent] =
|
|
resolve(alias).flatMap {
|
|
case Some(reg) => reg.factory.create(alias, properties, ctx)
|
|
case None => ZIO.fail(ComponentError.ComponentNotFound(alias))
|
|
}
|
|
|
|
def register(pluginId: PluginId, alias: String, factory: ComponentFactory): UIO[Unit] =
|
|
registry.update(_ + (alias -> ComponentRegistration(pluginId, alias, factory)))
|
|
}
|
|
}
|
|
```
|
|
|
|
**Update PluginRegistration.scala** - read existing file and replace ComponentDef with proper import:
|
|
|
|
Read the existing file from Phase 2 and update the ComponentDef to use the component package:
|
|
|
|
```scala
|
|
package plugin
|
|
|
|
import component.ComponentFactory
|
|
|
|
/** Data returned from plugin's register() method - declarative, no effects */
|
|
case class PluginRegistration(
|
|
// Components provided by this plugin (alias -> factory)
|
|
components: Map[String, ComponentFactory] = Map.empty,
|
|
// Permissions defined by this plugin (Phase 6)
|
|
permissions: List[PermissionDef] = List.empty,
|
|
// Navigation items for admin backend (Phase 8)
|
|
navigation: List[NavigationDef] = List.empty,
|
|
// Settings pages (Phase 8)
|
|
settings: List[SettingDef] = List.empty,
|
|
// Event subscriptions (Phase 2 extension API)
|
|
events: List[EventSubscription] = List.empty,
|
|
// Extensions to other plugins (Phase 2 extension API)
|
|
extensions: List[ExtensionDef] = List.empty,
|
|
// Form field definitions (for YAML-driven forms)
|
|
fields: List[FieldDef] = List.empty
|
|
)
|
|
|
|
object PluginRegistration:
|
|
val empty: PluginRegistration = PluginRegistration()
|
|
|
|
// Placeholder types - will be fleshed out in later phases
|
|
case class PermissionDef(code: String, label: String)
|
|
case class NavigationDef(id: String, label: String, url: String, icon: String = "", order: Int = 0)
|
|
case class SettingDef(key: String, label: String, icon: String = "")
|
|
case class EventSubscription(eventType: String, handler: String)
|
|
case class ExtensionDef(target: String, extensionClass: String)
|
|
case class FieldDef(name: String, fieldType: String)
|
|
```
|
|
|
|
Note: Changed `components: List[ComponentDef]` to `components: Map[String, ComponentFactory]` for proper registration pattern.
|
|
</action>
|
|
<verify>
|
|
Run `./mill summercms.compile` - all component and plugin files compile.
|
|
|
|
Verify the types work together by checking in Mill REPL:
|
|
```bash
|
|
./mill -i summercms.console
|
|
```
|
|
|
|
Then:
|
|
```scala
|
|
import component._
|
|
import zio._
|
|
|
|
// Test schema parsing
|
|
val yaml = """
|
|
name: Test Component
|
|
description: A test component
|
|
properties:
|
|
title:
|
|
type: String
|
|
title: Title
|
|
default: "Hello"
|
|
"""
|
|
println(ComponentSchema.parse(yaml))
|
|
// Should print Right(ComponentSchema(Test Component,A test component,None,Map(title -> PropertyDef(...))))
|
|
|
|
:quit
|
|
```
|
|
</verify>
|
|
<done>SummerComponent trait with lifecycle hooks, ComponentFactory, ComponentManager service, PluginRegistration updated to use Map[String, ComponentFactory]</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. `./mill summercms.compile` succeeds with all new dependencies and types
|
|
2. ComponentSchema.parse(validYaml) returns Right(schema)
|
|
3. PropertyDef parses type, title, default, validation from YAML
|
|
4. SummerComponent trait has init and onRun lifecycle hooks
|
|
5. ComponentManager.live provides working service
|
|
6. PluginRegistration.components is Map[String, ComponentFactory]
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- build.mill has Pebble, zio-http-htmx, ScalaTags dependencies
|
|
- YAML property schemas parse with all WinterCMS-compatible types
|
|
- SummerComponent trait defines alias, details, propertySchema, properties, init, onRun
|
|
- ComponentManager can register and resolve components by alias
|
|
- PluginRegistration supports component registration via Map
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/03-component-system/03-01-SUMMARY.md`
|
|
</output>
|