+**ComponentRoutes.scala:**
+```scala
+package component
+
+import zio.*
+import zio.http.*
+import scala.reflect.Selectable.reflectiveSelectable
+
+/** Routes for component HTMX handlers */
+object ComponentRoutes:
+
+ /** Component handler route: POST /summer/component/{alias}/{handler} */
+ val routes: Routes[ComponentManager & TemplateService & CsrfService, Response] =
+ Routes(
+ // HTMX handler endpoint
+ Method.POST / "summer" / "component" / string("alias") / string("handler") ->
+ handler { (alias: String, handlerName: String, req: Request) =>
+ handleComponentRequest(alias, handlerName, req)
+ },
+
+ // Component render endpoint (for initial page load)
+ Method.GET / "summer" / "component" / string("alias") / "render" ->
+ handler { (alias: String, req: Request) =>
+ renderComponent(alias, req)
+ }
+ )
+
+ private def handleComponentRequest(
+ alias: String,
+ handlerName: String,
+ req: Request
+ ): ZIO[ComponentManager & TemplateService & CsrfService, Nothing, Response] =
+ (for
+ // Validate CSRF token
+ _ <- CsrfService.validateRequest(req)
+
+ // Validate handler name starts with "on"
+ _ <- ZIO.fail(ComponentError.InvalidHandler(alias, handlerName, "Handler must start with 'on'"))
+ .when(!handlerName.startsWith("on"))
+
+ // Create page context from request
+ ctx = pageContextFromRequest(req)
+
+ // Extract properties from request body
+ props <- extractPropertiesFromRequest(req)
+
+ // Create component instance
+ manager <- ZIO.service[ComponentManager]
+ component <- manager.create(alias, props, ctx)
+
+ // Initialize component
+ _ <- component.init.provideEnvironment(ZEnvironment(ctx))
+
+ // Invoke the handler using reflection
+ result <- invokeHandler(component, handlerName, req, ctx)
+ yield result.toResponse).catchAll { error =>
+ // Return error as HTML fragment with error styling
+ ZIO.succeed(errorResponse(error))
+ }
+
+ private def renderComponent(
+ alias: String,
+ req: Request
+ ): ZIO[ComponentManager & TemplateService, Nothing, Response] =
+ (for
+ ctx = pageContextFromRequest(req)
+ props <- extractPropertiesFromRequest(req)
+ manager <- ZIO.service[ComponentManager]
+ component <- manager.create(alias, props, ctx)
+ _ <- component.init.provideEnvironment(ZEnvironment(ctx))
+ _ <- component.onRun.provideEnvironment(ZEnvironment(ctx))
+
+ // Render default template
+ templateService <- ZIO.service[TemplateService]
+ templatePath = component.templateDir.resolve("default.htm")
+ propsMap <- component.properties.get
+ html <- templateService.render(templatePath, buildTemplateContext(component, propsMap))
+ yield Response.html(html.content)).catchAll { error =>
+ ZIO.succeed(errorResponse(error))
+ }
+
+ private def pageContextFromRequest(req: Request): PageContext =
+ PageContext(
+ url = req.url.path.toString,
+ params = req.url.queryParams.map.view.mapValues(_.headOption.getOrElse("")).toMap,
+ isBackendEditor = req.header("X-Summer-Editor").isDefined
+ )
+
+ private def extractPropertiesFromRequest(req: Request): UIO[Map[String, PropertyValue]] =
+ // For now, return empty map - full form parsing in later enhancement
+ // Properties typically come from page/layout configuration, not request
+ ZIO.succeed(Map.empty)
+
+ /**
+ * Invoke a handler method on the component.
+ * Handler methods must:
+ * - Start with "on" (e.g., onRefresh, onLoadMore)
+ * - Return ZIO[ComponentEnv, ComponentError, HtmxResponse]
+ *
+ * Note: Uses Scala 3 reflection. For production, consider compile-time macro.
+ */
+ private def invokeHandler(
+ component: SummerComponent,
+ handlerName: String,
+ req: Request,
+ ctx: PageContext
+ ): ZIO[TemplateService, ComponentError, HtmxResponse] =
+ ZIO.attemptBlocking {
+ // Use reflection to find and invoke the handler method
+ val cls = component.getClass
+ val method = cls.getMethod(handlerName)
+ method.invoke(component)
+ }.flatMap {
+ case zio: ZIO[?, ?, ?] =>
+ // The handler returns a ZIO - we need to run it
+ zio.asInstanceOf[ZIO[ComponentEnv, ComponentError, HtmxResponse]]
+ .provideEnvironment(ZEnvironment(ctx))
+ case response: HtmxResponse =>
+ // Handler returned a direct response
+ ZIO.succeed(response)
+ case other =>
+ ZIO.fail(ComponentError.InvalidHandler(
+ component.alias,
+ handlerName,
+ s"Handler must return ZIO[ComponentEnv, ComponentError, HtmxResponse], got ${other.getClass}"
+ ))
+ }.catchAll {
+ case ce: ComponentError => ZIO.fail(ce)
+ case e: NoSuchMethodException =>
+ ZIO.fail(ComponentError.InvalidHandler(component.alias, handlerName, "Handler method not found"))
+ case e: Throwable =>
+ ZIO.fail(ComponentError.InvalidHandler(component.alias, handlerName, e.getMessage))
+ }
+
+ private def buildTemplateContext(
+ component: SummerComponent,
+ properties: Map[String, PropertyValue]
+ ): Map[String, Any] =
+ Map(
+ "__SELF__" -> component.alias,
+ "_component" -> component.details
+ ) ++ properties.view.mapValues(pvToAny).toMap
+
+ private def pvToAny(pv: PropertyValue): Any = pv match
+ case PropertyValue.StringVal(s) => s
+ case PropertyValue.BoolVal(b) => b
+ case PropertyValue.IntVal(i) => i
+ case PropertyValue.ListVal(l) => l.map(pvToAny)
+ case PropertyValue.ObjectVal(m) => m.view.mapValues(pvToAny).toMap
+ case PropertyValue.NullVal => null
+
+ private def errorResponse(error: ComponentError): Response =
+ val html = s"""
+ Component Error: ${error.message}
+
"""
+ Response.html(html).status(Status.InternalServerError)
+```
+
+**package.scala:**
+```scala
+package object component:
+ import zio.*
+
+ /** Combined layer for all component services */
+ type ComponentServices = ComponentManager & TemplateService & CsrfService
+
+ /** Create all component service layers */
+ val componentLayer: ZLayer[Any, Nothing, ComponentServices] =
+ ComponentManager.live ++ TemplateService.live ++ CsrfService.live
+```
+
+**Update api/Routes.scala** to include component routes:
+
+Read existing Routes.scala and add component routes:
+
+```scala
+import zio.http.*
+import component.{ComponentRoutes, componentLayer}
+
+object Routes:
+ /** Combined routes for the application */
+ val routes: Routes[Any, Response] =
+ HealthRoutes.routes ++ LandingRoutes.routes ++
+ ComponentRoutes.routes.provideLayer(componentLayer)
+```
+
+Note: The exact integration depends on existing Routes.scala structure. The key is to combine ComponentRoutes.routes with existing routes and provide the componentLayer.
+
+**Update Main.scala** to provide component services:
+
+Read existing Main.scala and update the layer composition. The key changes:
+
+1. Import component package: `import component.componentLayer`
+2. Add componentLayer to the provide chain for routes
+
+The updated provide for startupLogic should include:
+
+```scala
+// In the routes Server.serve line:
+res <- Server.serve(Routes.routes).provide(
+ Server.defaultWithPort(cfg.server.port),
+ QuillContext.dataSourceLayer,
+ componentLayer
+)
+```
+
+However, if routes are already using the layer internally (as shown above), Main.scala may not need changes.
+
+Full Main.scala update:
+```scala
+import zio.*
+import zio.http.*
+import zio.config.typesafe.TypesafeConfigProvider
+
+import api.Routes
+import _root_.config.{AppConfig as SummerConfig}
+import db.QuillContext
+import plugin.{PluginManager, PluginDiscovery, pluginLayer}
+import component.{ComponentManager, componentLayer}
+
+object Main extends ZIOAppDefault {
+
+ private val banner: String =
+ """
+ |
+ | | .
+ | `. * | .'
+ | `. ._|_* .' .
+ | . * .' `. *
+ | -------| |-------
+ | . *`.___.' * .
+ | .' |* `. *
+ | .' * | . `.
+ | . |
+ |
+ | S U M M E R C M S
+ |""".stripMargin
+
+ override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] =
+ Runtime.setConfigProvider(TypesafeConfigProvider.fromResourcePath())
+
+ private val startupLogic: ZIO[PluginManager & PluginDiscovery & ComponentManager, Any, Nothing] =
+ for {
+ cfg <- ZIO.config[SummerConfig](SummerConfig.config)
+ _ <- Console.printLine(banner)
+ _ <- Console.printLine(s"Starting on port ${cfg.server.port}...")
+ _ <- Console.printLine("")
+ // Initialize plugin system
+ _ <- Console.printLine("Loading plugins...")
+ _ <- PluginManager.loadPlugins(PluginDiscovery.defaultPluginsDir)
+ .catchAll(e => Console.printLine(s"Plugin loading warning: ${e.message}").as(()))
+ _ <- PluginManager.bootAll
+ .catchAll(e => Console.printLine(s"Plugin boot warning: ${e.message}").as(()))
+ plugins <- PluginManager.listPlugins
+ _ <- Console.printLine(s"Loaded ${plugins.count(_._2.isActive)} plugin(s)")
+ // Initialize component system
+ components <- ComponentManager.listComponents
+ _ <- Console.printLine(s"Registered ${components.size} component(s)")
+ _ <- Console.printLine("")
+ // Note: Migrations are NOT auto-run. Use CLI to run migrations (Phase 5).
+ res <- Server.serve(Routes.routes).provide(
+ Server.defaultWithPort(cfg.server.port),
+ QuillContext.dataSourceLayer
+ )
+ } yield res
+
+ override def run: ZIO[Any, Any, Any] =
+ startupLogic.provide(pluginLayer ++ componentLayer)
+}
+```
+
+