---
phase: 07-admin-forms-lists
plan: 03
type: execute
wave: 2
depends_on: ["07-01"]
files_modified:
  - summercms/src/admin/lists/ListColumn.scala
  - summercms/src/admin/lists/columns/TextColumn.scala
  - summercms/src/admin/lists/columns/DateColumn.scala
  - summercms/src/admin/lists/columns/RelationColumn.scala
  - summercms/src/admin/lists/columns/SwitchColumn.scala
  - summercms/src/admin/lists/Filter.scala
  - summercms/src/admin/lists/Pagination.scala
  - summercms/src/admin/lists/ListRenderer.scala
  - summercms/src/admin/lists/ColumnRegistry.scala
autonomous: true

must_haves:
  truths:
    - "List renders as table with columns from columns.yaml"
    - "Text columns display string values correctly"
    - "Date columns format dates according to format spec"
    - "Relation columns display related model's name attribute"
    - "Switch columns display toggle for boolean values"
    - "Filters narrow displayed records based on scope definitions"
    - "Pagination shows page numbers with navigation controls"
    - "Column headers are clickable for sorting (if sortable: true)"
    - "Bulk actions toolbar appears with checkbox selection"
  artifacts:
    - path: "summercms/src/admin/lists/ListColumn.scala"
      provides: "Processed column definition with rendering helpers"
      exports: ["ListColumn", "ColumnType"]
    - path: "summercms/src/admin/lists/columns/TextColumn.scala"
      provides: "Text column value rendering"
      exports: ["TextColumnRenderer"]
    - path: "summercms/src/admin/lists/columns/DateColumn.scala"
      provides: "Date column with format support"
      exports: ["DateColumnRenderer"]
    - path: "summercms/src/admin/lists/columns/RelationColumn.scala"
      provides: "Relation column showing related model attribute"
      exports: ["RelationColumnRenderer"]
    - path: "summercms/src/admin/lists/columns/SwitchColumn.scala"
      provides: "Boolean toggle column"
      exports: ["SwitchColumnRenderer"]
    - path: "summercms/src/admin/lists/Filter.scala"
      provides: "Filter scope definitions and condition types"
      exports: ["FilterScope", "FilterCondition"]
    - path: "summercms/src/admin/lists/Pagination.scala"
      provides: "Pagination state and rendering"
      exports: ["PaginationState", "PaginationRenderer"]
    - path: "summercms/src/admin/lists/ListRenderer.scala"
      provides: "Complete list view rendering"
      exports: ["ListRenderer"]
    - path: "summercms/src/admin/lists/ColumnRegistry.scala"
      provides: "Column type to renderer mapping"
      exports: ["ColumnRegistry", "ColumnRenderer"]
  key_links:
    - from: "summercms/src/admin/lists/ListRenderer.scala"
      to: "summercms/src/admin/lists/ColumnRegistry.scala"
      via: "resolves column types for rendering"
      pattern: "columnRegistry\\.resolve"
    - from: "summercms/src/admin/lists/ListRenderer.scala"
      to: "summercms/src/admin/yaml/ColumnsYamlSchema.scala"
      via: "uses parsed column config"
      pattern: "ColumnsYamlConfig"
    - from: "summercms/src/admin/lists/ListRenderer.scala"
      to: "summercms/src/admin/lists/Pagination.scala"
      via: "renders pagination controls"
      pattern: "PaginationRenderer\\.render"
---

<objective>
Implement list rendering with columns, filters, and pagination for admin backend.

Purpose: This plan creates the list view functionality that displays records in tabular format with sorting, filtering, and pagination. Lists are the primary way admins browse and manage records.

Output: Complete list rendering system with column types (text, date, relation, switch), filter scopes, pagination, bulk actions, and HTMX-powered sorting.
</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/07-admin-forms-lists/07-CONTEXT.md
@.planning/phases/07-admin-forms-lists/07-RESEARCH.md
@.planning/phases/07-admin-forms-lists/07-01-SUMMARY.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create ListColumn, ColumnRegistry, and column type renderers</name>
  <files>
    summercms/src/admin/lists/ListColumn.scala
    summercms/src/admin/lists/ColumnRegistry.scala
    summercms/src/admin/lists/columns/TextColumn.scala
    summercms/src/admin/lists/columns/DateColumn.scala
    summercms/src/admin/lists/columns/RelationColumn.scala
    summercms/src/admin/lists/columns/SwitchColumn.scala
  </files>
  <action>
Create ListColumn.scala with processed column definition:
```scala
enum ColumnType:
  case Text, Date, DateTime, TimeTense, Switch, Partial, Relation, Number

case class ListColumn(
  name: String,
  label: String,
  columnType: ColumnType,
  sortable: Boolean = true,
  searchable: Boolean = false,
  invisible: Boolean = false,
  width: Option[String] = None,
  format: Option[String] = None,      // Date format string
  relation: Option[String] = None,    // Relation name for relation columns
  select: Option[String] = None,      // Attribute to select from relation
  valueFrom: Option[String] = None,   // Alternative value source
  path: Option[String] = None,        // Path for partial columns
  cssClass: Option[String] = None
):
  def effectiveLabel: String = label
  def isVisible: Boolean = !invisible

object ListColumn:
  def fromConfig(name: String, config: ListColumnConfig): ListColumn =
    val columnType = config.`type`.getOrElse("text") match
      case "date" => ColumnType.Date
      case "datetime" => ColumnType.DateTime
      case "timetense" => ColumnType.TimeTense
      case "switch" => ColumnType.Switch
      case "partial" => ColumnType.Partial
      case "relation" => ColumnType.Relation
      case "number" => ColumnType.Number
      case _ => ColumnType.Text

    ListColumn(
      name = name,
      label = config.label,
      columnType = columnType,
      sortable = config.sortable.getOrElse(true),
      searchable = config.searchable.getOrElse(false),
      invisible = config.invisible.getOrElse(false),
      width = config.width,
      format = config.format,
      relation = config.relation,
      select = config.select,
      valueFrom = config.valueFrom,
      path = config.path,
      cssClass = config.cssClass
    )
```

Create ColumnRegistry.scala similar to WidgetRegistry:
```scala
trait ColumnRenderer:
  def render(column: ListColumn, record: Map[String, Any]): scalatags.Text.Frag

trait ColumnRegistry:
  def register(typeName: ColumnType, renderer: ColumnRenderer): UIO[Unit]
  def resolve(typeName: ColumnType): UIO[ColumnRenderer]

object ColumnRegistry:
  val live: ZLayer[Any, Nothing, ColumnRegistry] =
    ZLayer.fromZIO {
      Ref.make(Map.empty[ColumnType, ColumnRenderer]).map { registry =>
        new ColumnRegistry:
          def register(typeName: ColumnType, renderer: ColumnRenderer): UIO[Unit] =
            registry.update(_ + (typeName -> renderer))

          def resolve(typeName: ColumnType): UIO[ColumnRenderer] =
            registry.get.map(_.getOrElse(typeName, TextColumnRenderer))
      }
    }
```

Create TextColumn.scala:
```scala
object TextColumnRenderer extends ColumnRenderer:
  def render(column: ListColumn, record: Map[String, Any]): Frag =
    val value = extractValue(column, record)
    span(cls := s"column-text ${column.cssClass.getOrElse("")}",
      Option(value).map(_.toString).getOrElse("")
    )

  private def extractValue(column: ListColumn, record: Map[String, Any]): Any =
    column.valueFrom match
      case Some(path) => getNestedValue(record, path.split('.').toList)
      case None => record.getOrElse(column.name, null)

  private def getNestedValue(data: Map[String, Any], path: List[String]): Any =
    path match
      case Nil => null
      case head :: Nil => data.getOrElse(head, null)
      case head :: tail =>
        data.get(head) match
          case Some(nested: Map[_, _]) => getNestedValue(nested.asInstanceOf[Map[String, Any]], tail)
          case _ => null
```

Create DateColumn.scala:
```scala
import java.time.format.DateTimeFormatter
import java.time.{LocalDate, LocalDateTime, Instant, ZoneId}

object DateColumnRenderer extends ColumnRenderer:
  private val defaultDateFormat = "MMM d, yyyy"
  private val defaultDateTimeFormat = "MMM d, yyyy HH:mm"

  def render(column: ListColumn, record: Map[String, Any]): Frag =
    val value = record.getOrElse(column.name, null)
    val formatted = formatDate(value, column.format, column.columnType)
    span(cls := s"column-date ${column.cssClass.getOrElse("")}", formatted)

  private def formatDate(value: Any, format: Option[String], colType: ColumnType): String =
    if value == null then return ""

    val formatter = DateTimeFormatter.ofPattern(
      format.getOrElse(if colType == ColumnType.DateTime then defaultDateTimeFormat else defaultDateFormat)
    )

    value match
      case ld: LocalDate => ld.format(formatter)
      case ldt: LocalDateTime => ldt.format(formatter)
      case inst: Instant => inst.atZone(ZoneId.systemDefault()).format(formatter)
      case ts: java.sql.Timestamp => ts.toLocalDateTime.format(formatter)
      case s: String => s  // Already formatted string
      case _ => value.toString

object TimeTenseColumnRenderer extends ColumnRenderer:
  def render(column: ListColumn, record: Map[String, Any]): Frag =
    val value = record.getOrElse(column.name, null)
    val tense = formatTimeTense(value)
    span(cls := "column-timetense", attr("title") := Option(value).map(_.toString).getOrElse(""), tense)

  private def formatTimeTense(value: Any): String =
    // Simplified implementation - "2 hours ago", "3 days ago", etc.
    if value == null then return ""
    // ... time ago calculation
    "Recently"  // Placeholder - implement proper time-ago logic
```

Create RelationColumn.scala:
```scala
object RelationColumnRenderer extends ColumnRenderer:
  def render(column: ListColumn, record: Map[String, Any]): Frag =
    val relationData = record.get(column.relation.getOrElse(column.name))
    val displayValue = relationData match
      case Some(rel: Map[_, _]) =>
        val relMap = rel.asInstanceOf[Map[String, Any]]
        relMap.getOrElse(column.select.getOrElse("name"), "").toString
      case Some(list: List[_]) if list.nonEmpty =>
        // hasMany - show count or first few items
        list.take(3).map {
          case m: Map[_, _] => m.asInstanceOf[Map[String, Any]].getOrElse(column.select.getOrElse("name"), "")
          case v => v.toString
        }.mkString(", ") + (if list.size > 3 then s" (+${list.size - 3})" else "")
      case _ => ""

    span(cls := s"column-relation ${column.cssClass.getOrElse("")}", displayValue)
```

Create SwitchColumn.scala:
```scala
object SwitchColumnRenderer extends ColumnRenderer:
  def render(column: ListColumn, record: Map[String, Any]): Frag =
    val value = record.getOrElse(column.name, false)
    val isActive = value match
      case b: Boolean => b
      case n: Number => n.intValue() != 0
      case s: String => Set("true", "1", "yes", "on").contains(s.toLowerCase)
      case _ => false

    // Read-only switch display (clickable would need HTMX for toggle)
    div(cls := s"switch-display ${if isActive then "active" else ""} ${column.cssClass.getOrElse("")}",
      span(cls := "switch-indicator")
    )
```
  </action>
  <verify>
Run `mill summercms.compile` - should compile without errors.
Verify each column renderer produces appropriate HTML for its type.
  </verify>
  <done>
ListColumn case class with fromConfig conversion. ColumnRegistry with registration and resolution. Column renderers for Text, Date, DateTime, TimeTense, Relation, and Switch types.
  </done>
</task>

<task type="auto">
  <name>Task 2: Create Filter and Pagination components</name>
  <files>
    summercms/src/admin/lists/Filter.scala
    summercms/src/admin/lists/Pagination.scala
  </files>
  <action>
Create Filter.scala with filter scope definitions:
```scala
enum FilterCondition:
  case Equals(value: String)
  case NotEquals(value: String)
  case Like(pattern: String)
  case GreaterThan(value: String)
  case LessThan(value: String)
  case Between(from: String, to: String)
  case In(values: List[String])
  case IsNull
  case IsNotNull

enum FilterType:
  case Dropdown   // Select from predefined options
  case Switch     // Boolean toggle
  case Date       // Date range filter
  case DateRange  // From/to date
  case Text       // Free text search
  case Number     // Numeric comparison

case class FilterScope(
  name: String,
  label: String,
  filterType: FilterType,
  conditions: Map[String, FilterCondition],  // key -> condition
  default: Option[String] = None,
  options: Map[String, String] = Map.empty   // For dropdown type
)

object FilterScope:
  def fromConfig(name: String, config: FilterConfig): FilterScope =
    val filterType = config.`type`.getOrElse("dropdown") match
      case "switch" => FilterType.Switch
      case "date" => FilterType.Date
      case "daterange" => FilterType.DateRange
      case "text" => FilterType.Text
      case "number" => FilterType.Number
      case _ => FilterType.Dropdown

    FilterScope(
      name = name,
      label = config.label,
      filterType = filterType,
      conditions = parseConditions(config.conditions),
      default = config.default,
      options = config.options.getOrElse(Map.empty)
    )

trait FilterRenderer:
  def render(filter: FilterScope, activeValue: Option[String], baseUrl: String): scalatags.Text.Frag

object FilterRenderer:
  def render(filter: FilterScope, activeValue: Option[String], baseUrl: String): Frag =
    filter.filterType match
      case FilterType.Dropdown => renderDropdownFilter(filter, activeValue, baseUrl)
      case FilterType.Switch => renderSwitchFilter(filter, activeValue, baseUrl)
      case FilterType.Date => renderDateFilter(filter, activeValue, baseUrl)
      case FilterType.Text => renderTextFilter(filter, activeValue, baseUrl)
      case _ => renderDropdownFilter(filter, activeValue, baseUrl)

  private def renderDropdownFilter(filter: FilterScope, activeValue: Option[String], baseUrl: String): Frag =
    div(cls := "filter-scope filter-dropdown",
      label(filter.label),
      select(
        cls := "form-control filter-select",
        attr("hx-get") := baseUrl,
        attr("hx-trigger") := "change",
        attr("hx-target") := ".list-widget",
        attr("name") := s"filter[${filter.name}]",

        option(attr("value") := "", "-- All --"),
        filter.options.map { case (value, label) =>
          option(
            attr("value") := value,
            if activeValue.contains(value) then Some(attr("selected") := "selected") else None,
            label
          )
        }
      )
    )

  private def renderSwitchFilter(filter: FilterScope, activeValue: Option[String], baseUrl: String): Frag =
    div(cls := "filter-scope filter-switch",
      label(cls := "filter-switch-label",
        input(
          tpe := "checkbox",
          attr("name") := s"filter[${filter.name}]",
          attr("hx-get") := baseUrl,
          attr("hx-trigger") := "change",
          attr("hx-target") := ".list-widget",
          if activeValue.contains("1") then Some(attr("checked") := "checked") else None
        ),
        span(filter.label)
      )
    )

  private def renderDateFilter(filter: FilterScope, activeValue: Option[String], baseUrl: String): Frag =
    div(cls := "filter-scope filter-date",
      label(filter.label),
      input(
        tpe := "text",
        cls := "form-control flatpickr-input",
        attr("name") := s"filter[${filter.name}]",
        attr("value") := activeValue.getOrElse(""),
        data("flatpickr") := "true",
        attr("hx-get") := baseUrl,
        attr("hx-trigger") := "change",
        attr("hx-target") := ".list-widget"
      )
    )

  private def renderTextFilter(filter: FilterScope, activeValue: Option[String], baseUrl: String): Frag =
    div(cls := "filter-scope filter-text",
      label(filter.label),
      input(
        tpe := "text",
        cls := "form-control",
        attr("name") := s"filter[${filter.name}]",
        attr("value") := activeValue.getOrElse(""),
        attr("hx-get") := baseUrl,
        attr("hx-trigger") := "keyup changed delay:500ms",
        attr("hx-target") := ".list-widget"
      )
    )
```

Create Pagination.scala:
```scala
case class PaginationState(
  currentPage: Int,
  totalPages: Int,
  totalRecords: Int,
  perPage: Int,
  sortColumn: Option[String] = None,
  sortDirection: SortDirection = SortDirection.Asc
):
  def hasNextPage: Boolean = currentPage < totalPages
  def hasPreviousPage: Boolean = currentPage > 1
  def pageRange: Range =
    val start = math.max(1, currentPage - 2)
    val end = math.min(totalPages, currentPage + 2)
    start to end
  def offset: Int = (currentPage - 1) * perPage

enum SortDirection:
  case Asc, Desc
  def toggle: SortDirection = this match
    case Asc => Desc
    case Desc => Asc
  override def toString: String = this match
    case Asc => "asc"
    case Desc => "desc"

object SortDirection:
  def fromString(s: String): SortDirection = s.toLowerCase match
    case "desc" => Desc
    case _ => Asc

object PaginationRenderer:
  def render(state: PaginationState, baseUrl: String): Frag =
    if state.totalPages <= 1 then return frag()  // No pagination needed

    nav(cls := "pagination-nav",
      ul(cls := "pagination",
        // Previous button
        li(cls := s"page-item ${if !state.hasPreviousPage then "disabled" else ""}",
          if state.hasPreviousPage then
            a(cls := "page-link",
              attr("hx-get") := s"$baseUrl?page=${state.currentPage - 1}",
              attr("hx-target") := ".list-widget",
              "Previous"
            )
          else
            span(cls := "page-link", "Previous")
        ),

        // Page numbers
        state.pageRange.map { page =>
          li(cls := s"page-item ${if page == state.currentPage then "active" else ""}",
            if page == state.currentPage then
              span(cls := "page-link", page.toString)
            else
              a(cls := "page-link",
                attr("hx-get") := s"$baseUrl?page=$page",
                attr("hx-target") := ".list-widget",
                page.toString
              )
          )
        },

        // Next button
        li(cls := s"page-item ${if !state.hasNextPage then "disabled" else ""}",
          if state.hasNextPage then
            a(cls := "page-link",
              attr("hx-get") := s"$baseUrl?page=${state.currentPage + 1}",
              attr("hx-target") := ".list-widget",
              "Next"
            )
          else
            span(cls := "page-link", "Next")
        )
      ),

      // Record count info
      div(cls := "pagination-info",
        s"Showing ${state.offset + 1} to ${math.min(state.offset + state.perPage, state.totalRecords)} of ${state.totalRecords} records"
      )
    )
```
  </action>
  <verify>
Run `mill summercms.compile` - should compile without errors.
Verify FilterRenderer produces filter controls with HTMX attributes.
Verify PaginationRenderer produces pagination navigation.
  </verify>
  <done>
FilterScope and FilterCondition enums defined. FilterRenderer generates filter controls (dropdown, switch, date, text) with HTMX triggers. PaginationState tracks page state. PaginationRenderer generates page navigation with HTMX.
  </done>
</task>

<task type="auto">
  <name>Task 3: Create ListRenderer with complete list view</name>
  <files>
    summercms/src/admin/lists/ListRenderer.scala
  </files>
  <action>
Create ListRenderer.scala as a ZIO service for complete list view rendering:

```scala
import scalatags.Text.all._

case class ListContext(
  baseUrl: String,
  editUrl: String => String,        // Function: recordId -> edit URL
  pagination: PaginationState,
  activeFilters: Map[String, String],
  selectedIds: Set[String] = Set.empty
)

trait ListRenderer:
  def render(
    config: ColumnsYamlConfig,
    records: Seq[Map[String, Any]],
    ctx: ListContext
  ): Task[scalatags.Text.Frag]

object ListRenderer:
  val live: ZLayer[ColumnRegistry, Nothing, ListRenderer] =
    ZLayer.fromFunction { (columnRegistry: ColumnRegistry) =>
      new ListRenderer:
        def render(
          config: ColumnsYamlConfig,
          records: Seq[Map[String, Any]],
          ctx: ListContext
        ): Task[Frag] = ZIO.succeed {
          val columns = config.columns.map { case (name, cfg) =>
            ListColumn.fromConfig(name, cfg)
          }.filter(_.isVisible).toSeq

          val filters = config.filters.map { case (name, cfg) =>
            FilterScope.fromConfig(name, cfg)
          }.toSeq

          div(cls := "list-widget",
            // Toolbar with bulk actions
            renderToolbar(ctx),

            // Filters
            if filters.nonEmpty then renderFilters(filters, ctx) else frag(),

            // Table
            renderTable(columns, records, ctx),

            // Pagination
            PaginationRenderer.render(ctx.pagination, ctx.baseUrl)
          )
        }

        private def renderToolbar(ctx: ListContext): Frag =
          div(cls := "list-toolbar",
            // Create button
            a(
              cls := "btn btn-primary",
              href := s"${ctx.baseUrl}/create",
              "Create"
            ),

            // Bulk actions (shown when items selected)
            div(cls := "bulk-actions",
              button(
                cls := "btn btn-danger",
                attr("hx-post") := s"${ctx.baseUrl}/bulk-delete",
                attr("hx-include") := "[name='checked[]']",
                attr("hx-confirm") := "Delete selected records?",
                attr("hx-target") := ".list-widget",
                "Delete Selected"
              )
            )
          )

        private def renderFilters(filters: Seq[FilterScope], ctx: ListContext): Frag =
          div(cls := "list-filters",
            filters.map { filter =>
              FilterRenderer.render(filter, ctx.activeFilters.get(filter.name), ctx.baseUrl)
            }
          )

        private def renderTable(columns: Seq[ListColumn], records: Seq[Map[String, Any]], ctx: ListContext): Frag =
          table(cls := "table list-table",
            renderTableHead(columns, ctx),
            renderTableBody(columns, records, ctx)
          )

        private def renderTableHead(columns: Seq[ListColumn], ctx: ListContext): Frag =
          thead(
            tr(
              // Checkbox column
              th(cls := "list-checkbox",
                input(tpe := "checkbox", cls := "select-all",
                  attr("data-action") := "select-all"
                )
              ),

              // Data columns with sorting
              columns.map { col =>
                val isSorted = ctx.pagination.sortColumn.contains(col.name)
                val sortDir = if isSorted then ctx.pagination.sortDirection else SortDirection.Asc
                val newDir = if isSorted then sortDir.toggle else SortDirection.Asc

                th(
                  cls := s"list-header ${if col.sortable then "sortable" else ""} ${if isSorted then s"sorted-$sortDir" else ""}",
                  col.width.map(w => attr("style") := s"width: $w"),

                  if col.sortable then
                    a(
                      attr("hx-get") := s"${ctx.baseUrl}?sort=${col.name}&dir=$newDir",
                      attr("hx-target") := ".list-widget",
                      col.effectiveLabel,
                      if isSorted then span(cls := s"sort-icon $sortDir") else frag()
                    )
                  else
                    span(col.effectiveLabel)
                )
              }
            )
          )

        private def renderTableBody(columns: Seq[ListColumn], records: Seq[Map[String, Any]], ctx: ListContext): Frag =
          tbody(
            records.map { record =>
              val recordId = record.getOrElse("id", "").toString
              val isSelected = ctx.selectedIds.contains(recordId)

              tr(
                cls := s"list-row ${if isSelected then "selected" else ""}",
                // Row click to edit (per CONTEXT.md)
                attr("hx-get") := ctx.editUrl(recordId),
                attr("hx-target") := "#content-area",
                attr("hx-push-url") := "true",

                // Checkbox cell
                td(cls := "list-checkbox",
                  attr("hx-trigger") := "click consume",  // Prevent row click
                  input(
                    tpe := "checkbox",
                    attr("name") := "checked[]",
                    attr("value") := recordId,
                    if isSelected then Some(attr("checked") := "checked") else None
                  )
                ),

                // Data cells
                columns.map { col =>
                  td(cls := s"list-cell column-${col.columnType.toString.toLowerCase}",
                    renderColumnValue(col, record)
                  )
                }
              )
            }
          )

        private def renderColumnValue(column: ListColumn, record: Map[String, Any]): Frag =
          // Would use ColumnRegistry in full implementation
          column.columnType match
            case ColumnType.Text => TextColumnRenderer.render(column, record)
            case ColumnType.Date | ColumnType.DateTime => DateColumnRenderer.render(column, record)
            case ColumnType.TimeTense => TimeTenseColumnRenderer.render(column, record)
            case ColumnType.Relation => RelationColumnRenderer.render(column, record)
            case ColumnType.Switch => SwitchColumnRenderer.render(column, record)
            case _ => TextColumnRenderer.render(column, record)
    }
```

Key features implemented per CONTEXT.md:
1. **Bulk actions toolbar** above list with Create, Delete Selected
2. **Row click navigates** to edit screen (configurable via ctx.editUrl)
3. **Checkbox selection** for bulk operations (hx-trigger="click consume" prevents row click)
4. **Column sorting** via clickable headers with HTMX
5. **Filters** render above table with HTMX-triggered updates
6. **Pagination** with page numbers (traditional, not infinite scroll)
7. **Column width** from YAML config or auto-size

Additional CSS classes for density toggle (comfortable/compact) would be applied via JavaScript toggle button that adds a class to .list-widget.
  </action>
  <verify>
Run `mill summercms.compile` - should compile without errors.
Verify ListRenderer produces complete list HTML with:
- Toolbar with Create and Delete Selected buttons
- Filter controls if filters defined
- Table with sortable headers
- Row checkboxes and click-to-edit behavior
- Pagination navigation
  </verify>
  <done>
ListRenderer generates complete admin list view with toolbar, filters, sortable table, bulk actions, row click navigation, and pagination. Uses ColumnRegistry for column value rendering.
  </done>
</task>

</tasks>

<verification>
After completing all tasks:

1. **Compile check:**
   ```bash
   mill summercms.compile
   ```
   Must succeed with no errors.

2. **Column rendering verification:**
   Create test records and verify:
   - TextColumnRenderer shows string value
   - DateColumnRenderer formats date according to format string
   - RelationColumnRenderer extracts and displays related model attribute
   - SwitchColumnRenderer shows toggle in correct state

3. **Filter rendering verification:**
   Create FilterScope instances and verify:
   - Dropdown filter produces select with options
   - Switch filter produces checkbox
   - All filters have hx-get and hx-target attributes

4. **Pagination verification:**
   Create PaginationState with 10 pages, current page 5, verify:
   - Previous/Next buttons present and enabled
   - Page range shows 3-7 (current +/- 2)
   - Current page is marked active
   - All links have hx-get for HTMX

5. **ListRenderer integration verification:**
   Call ListRenderer.render with sample config and records, verify:
   - Output contains table with thead and tbody
   - Toolbar has Create and Delete Selected buttons
   - Headers are clickable with sort URLs
   - Rows have checkbox and hx-get for edit navigation
   - Pagination appears at bottom
</verification>

<success_criteria>
- [ ] ListColumn case class with fromConfig conversion from ListColumnConfig
- [ ] ColumnRegistry provides column type to renderer mapping
- [ ] TextColumnRenderer handles text/string values with nested path support
- [ ] DateColumnRenderer formats dates with configurable format strings
- [ ] RelationColumnRenderer extracts and displays related model attributes
- [ ] SwitchColumnRenderer displays boolean toggle state
- [ ] FilterScope and FilterCondition enums defined for all filter types
- [ ] FilterRenderer generates dropdown, switch, date, text filter controls
- [ ] All filters have HTMX attributes for live filtering
- [ ] PaginationState tracks page, total, sort information
- [ ] PaginationRenderer generates page navigation with HTMX
- [ ] ListRenderer produces complete list view with toolbar, table, pagination
- [ ] Table headers sortable via HTMX when sortable: true
- [ ] Row checkboxes support bulk selection
- [ ] Row click triggers edit navigation via HTMX
- [ ] Project compiles successfully with `mill summercms.compile`
</success_criteria>

<output>
After completion, create `.planning/phases/07-admin-forms-lists/07-03-SUMMARY.md`
</output>
