# Empty State


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

## Where this lives, and why

Per the three-layer architecture (see `cjm-fasthtml-design-system`
bootstrap plan, §8.6, and the multi-workflow design-system doc’s Library
Architecture section), the empty-state convention spans two libraries:

- **`cjm-fasthtml-design-system/nbs/empty_states.ipynb`** owns the
  *anatomy recipe* — CSS class strings only. The `empty_states`
  frozen-dataclass instance carries four slots: `wrapper` (flex column,
  centered, padded, fills available flex space), `icon` (V13 `subtle`
  opacity), `title` (V13 `secondary` emphasis at `font_size.lg`,
  centered), `detail` (V13 `tertiary` emphasis at `font_size.sm`,
  centered).
- **This notebook** owns the *rendering helper* — an FT-returning
  `render_empty_state(message, detail, icon_name, cta, id)` that
  composes V8 anatomy slots into a `Div` with the canonical child
  ordering (icon → title → detail → CTA).

The split is load-bearing: recipes return strings (design-system layer);
layout primitives return FT (app-core layer). V12
(`render_confirm_modal`) established this pattern; V8 follows it.

**Why this helper is the recommended consumer entry point.** Consumers
who want fine-grained DOM control can compose V8 anatomy slots directly
from `cjm_fasthtml_design_system.empty_states`. Consumers who want the
canonical default — centered icon-above-title-above-detail, optional
CTA, all V8/V11/V13 composition baked in — call this helper. Both paths
consume the same anatomy; the helper is the recommended starting point
because it locks in the canonical child ordering (icon-first, CTA-last)
which is the load-bearing visual contract V8 documents but doesn’t
enforce structurally.

**Why the parameters look the way they do.**

- `message` is required; everything else is optional. The minimal empty
  state is `render_empty_state("No items available")` — just the title
  slot inside the wrapper.
- `icon_name` is a string (Lucide icon name like `"inbox"` or
  `"file-plus"`), not a pre-built FT. The helper internally calls
  `lucide_icon(icon_name, size=icons.empty_state, cls=empty_states.icon)`
  so V8’s icon-size + opacity contract holds. Consumers needing
  alternate sizes or non-Lucide icons compose against V8 anatomy
  directly.
- `cta` accepts any FT, not just a Button. Most CTAs are
  `Button(label, cls=buttons.primary_action)` but the helper doesn’t
  constrain the shape — an `<A>` link styled as a button, a small button
  cluster, etc., all work.
- `id` is the outer Div id, exposed for HTMX targeting (`hx_swap_oob`,
  etc.) and consumer ID-schema integration (e.g., card-stack passing
  `ids.card_stack_empty`).

For the convention-level rationale (why V13 `secondary` for title, why
V13 `tertiary` for detail, why `subtle` for icon, why `gap(3)` on the
wrapper), see `cjm-fasthtml-design-system/nbs/empty_states.ipynb`. This
helper deliberately doesn’t restate that rationale — the catalog
notebook is canonical.

## `render_empty_state`

Render a canonical empty-state component per V8 anatomy. The minimal
call is `render_empty_state("No items available")` — just the title
inside the wrapper. Adding `icon_name`, `detail`, or `cta` populates the
corresponding V8 slots in canonical order (icon → title → detail → CTA).

------------------------------------------------------------------------

### render_empty_state

``` python

def render_empty_state(
    message:str, # Primary empty-state message (single sentence in the title slot)
    detail:Optional=None, # Optional supporting line rendered below the title in the detail slot
    icon_name:Optional=None, # Optional Lucide icon name; rendered at V11 icons.empty_state size with V8 icon opacity
    cta:Optional=None, # Optional call-to-action element appended as the last wrapper child (typically Button with V1 buttons.primary_action)
    id:Optional=None, # Optional outer Div id (for HTMX targeting or consumer ID-schema integration)
)->FT: # Div implementing V8 empty-state anatomy

```

*Render a canonical empty-state component per V8 anatomy (icon → title →
detail → CTA child ordering, all slots optional except message).*

## Example

``` python
from fasthtml.common import Button
from cjm_fasthtml_design_system.buttons import buttons

# Canonical full empty state: icon + title + detail + CTA
render_empty_state(
    message="No transcription sources selected",
    detail="Add a source from the browser to begin.",
    icon_name="inbox",
    cta=Button("Add a source", cls=buttons.primary_action),
    id="example-empty-state",
)
```

``` html
<div id="example-empty-state" class="flex flex-col items-center justify-center gap-3 p-16 grow-1">
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-12 h-12 text-base-content/40"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path></svg>  <p class="text-lg text-center text-base-content/70">No transcription sources selected</p>
  <p class="text-sm text-center text-base-content/60">Add a source from the browser to begin.</p>
<button class="btn btn-primary btn-sm">Add a source</button></div>
```

## Tests — V8 anatomy + child-ordering regression guards

Each assertion below maps to one load-bearing decision in the V8 + V12
architecture: V8 anatomy slots composed correctly, optional slots
omitted when not supplied, child ordering invariant (icon-first,
CTA-last) holds across all combinations.

``` python
from fasthtml.common import to_xml, Button
from cjm_fasthtml_design_system.buttons import buttons
from cjm_fasthtml_design_system.empty_states import empty_states

# ── Slot composition guards ───────────────────────────────────────────
# Render the canonical full empty state and verify each V8 slot's class
# composition lands in the output. We assert against V8 anatomy via the
# `empty_states` instance directly (rather than hardcoded class strings)
# so future V8 value-tuning sessions (per CD3 two-evidence discipline)
# don't break these tests &mdash; the wire is V8-anatomy-as-contract.

full = render_empty_state(
    message="No transcription sources selected",
    detail="Add a source from the browser to begin.",
    icon_name="inbox",
    cta=Button("Add a source", cls=buttons.primary_action),
    id="test-empty",
)
full_html = to_xml(full)

# V8 wrapper applied to outer Div (flex column + grow + center)
assert empty_states.wrapper in full_html, (
    f"V8 contract: outer Div must carry empty_states.wrapper class. "
    f"Got HTML: {full_html[:200]!r}"
)

# V8 title slot applied to title P
assert empty_states.title in full_html, (
    f"V8 contract: title P must carry empty_states.title class. "
    f"Got HTML: {full_html[:200]!r}"
)

# V8 detail slot applied to detail P (when provided)
assert empty_states.detail in full_html, (
    f"V8 contract: detail P must carry empty_states.detail class when detail supplied."
)

# V8 icon slot applied to icon element (when icon_name provided)
# icon slot = V13 text_tiers.subtle; pair with V11 icons.empty_state size
assert empty_states.icon in full_html, (
    f"V8 contract: icon element must carry empty_states.icon class when icon_name supplied."
)

# Message text rendered
assert "No transcription sources selected" in full_html

# Detail text rendered
assert "Add a source from the browser to begin." in full_html

# CTA forwarded into the output verbatim
assert "Add a source" in full_html
assert "btn-primary" in full_html

# Outer Div id forwarded
assert 'id="test-empty"' in full_html

# ── Child-ordering invariant (icon &rarr; title &rarr; detail &rarr; CTA) ─
# Load-bearing visual contract: icon always first if present, CTA always
# last if present. A refactor that reordered children would silently break
# the canonical empty-state appearance at every consumer site.

# Use V8 slot class strings as positional markers in the serialized HTML.
icon_pos   = full_html.find(empty_states.icon)
title_pos  = full_html.find(empty_states.title)
detail_pos = full_html.find(empty_states.detail)
cta_pos    = full_html.find("btn-primary")

assert 0 <= icon_pos < title_pos < detail_pos < cta_pos, (
    f"V8 child-ordering contract: icon &rarr; title &rarr; detail &rarr; CTA. "
    f"Got positions: icon={icon_pos}, title={title_pos}, detail={detail_pos}, "
    f"cta={cta_pos}."
)

# ── Optional-slot omission guards ─────────────────────────────────────
# When a slot is not requested, the helper must not emit its class string.
# Catches accidental "always render an empty Span" or "default to empty
# detail line" regressions.

title_only_html = to_xml(render_empty_state(message="No items available"))

# Title is always present
assert empty_states.title in title_only_html
# Other slots are NOT present
assert empty_states.detail not in title_only_html, (
    "Detail slot must be omitted from output when detail param is None."
)
assert empty_states.icon not in title_only_html, (
    "Icon slot must be omitted from output when icon_name param is None."
)

# ── Title-and-icon-only (no detail, no CTA) ───────────────────────────

icon_title_html = to_xml(render_empty_state(
    message="No items available",
    icon_name="inbox",
))
assert empty_states.icon in icon_title_html
assert empty_states.title in icon_title_html
assert empty_states.detail not in icon_title_html

# Ordering still holds: icon before title.
assert icon_title_html.find(empty_states.icon) < icon_title_html.find(empty_states.title)

# ── id omission guard ─────────────────────────────────────────────────
# When id is not supplied, the outer Div has no id attribute.

no_id_html = to_xml(render_empty_state(message="x"))
assert "id=" not in no_id_html, (
    "Outer Div must not carry an id attribute when id param is None. "
    f"Got HTML: {no_id_html!r}"
)

# ── CTA accepts arbitrary FT, not just Button ─────────────────────────
# V8 doesn't codify a CTA role; the helper forwards whatever element the
# caller passes. A regression that constrained cta to Button-only would
# break legitimate use cases (anchor-styled-as-button, button clusters).

from fasthtml.common import A
cta_anchor_html = to_xml(render_empty_state(
    message="No items",
    cta=A("Browse all", href="/all", cls=buttons.primary_action),
))
assert "Browse all" in cta_anchor_html
assert 'href="/all"' in cta_anchor_html

print("render_empty_state tests passed!")
```

    render_empty_state tests passed!
