"""Material Design components for FastHTML using BeerCSS"""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/02_components.ipynb.

# %% auto #0
__all__ = ['BUTTON_SPECIALS', 'ButtonT', 'ANCHOR_SPECIALS', 'AT', 'NavToggleButton', 'SpaceT', 'GridSpanT', 'GridCell', 'Grid',
           'DivHStacked', 'DivLAligned', 'DivVStacked', 'DivRAligned', 'DivCentered', 'DivFullySpaced', 'Icon',
           'NavBar', 'Modal', 'ModalButton', 'ModalCancel', 'ModalConfirm', 'ModalTitle', 'ModalBody', 'ModalFooter',
           'Field', 'LabelInput', 'FormLabel', 'CheckboxX', 'Radio', 'Switch', 'TextArea', 'Range', 'SelectMenu',
           'Select', 'FormGrid', 'Progress', 'LoadingIndicator', 'Table', 'Td', 'Th', 'Thead', 'Tbody', 'Tfoot',
           'TableFromLists', 'TableFromDicts', 'TableControls', 'Pagination', 'Card', 'Toolbar', 'Toast', 'Snackbar',
           'ContainerT', 'FormField', 'FormModal', 'NavContainer', 'NavHeaderLi', 'NavDividerLi', 'NavCloseLi',
           'NavSubtitle', 'BottomNav', 'NavSideBarHeader', 'NavSideBarLinks', 'NavSideBarContainer', 'Page', 'Layout',
           'TextT', 'TextPresets', 'CodeSpan', 'CodeBlock', 'Blockquote', 'Q', 'Em', 'Strong', 'Small', 'Mark', 'Abbr',
           'Sub', 'Sup', 'FAQItem', 'CookiesBanner']

# %% ../nbs/02_components.ipynb #32bd23ad
from fastcore.utils import *
from fasthtml.common import *
from fasthtml.jupyter import *
from fastlite import *
import fasthtml.components as fc
from fasthtml.common import A, Button as FhButton, I, Span
from .foundations import normalize_tokens, stringify, VEnum, dedupe_preserve_order
from .core import *
from nbdev.showdoc import show_doc


# %% ../nbs/02_components.ipynb #31957c2d
#| code-fold: true
def NavToggleButton(target, icon='menu', **kwargs):
    """Create a navigation toggle button that toggles the 'max' class on the target element.
    
    Also toggles icon between 'menu' and 'menu_open' based on nav state.
    """
    cls = kwargs.get('cls', 'circle transparent')
    # Inline JS: toggle nav's max class AND change this button's icon
    onclick = f"var nav=document.querySelector('{target}');if(nav){{nav.classList.toggle('max');var i=this.querySelector('i');if(i)i.textContent=nav.classList.contains('max')?'menu_open':'menu'}};return false;"
    kwargs.update({'onclick': onclick, 'cls': cls})
    return Button(I(icon), **kwargs)

# %% ../nbs/02_components.ipynb #aea20daf
#| code-fold: true
BUTTON_SPECIALS = {
    'primary': 'primary',
    'secondary': 'secondary', 
    'destructive': 'tertiary',
    'ghost': 'transparent border',
    'text': 'transparent',
    'link': '__link__',
    'default': 'primary'
}

def _make_button_property(tokens):
    return property(lambda self: _ButtonChain(self._tokens + tokens))

class _ButtonChain(BeerCssChain):
    """Chainable button style helper"""
    pass

for name, css in BUTTON_SPECIALS.items():
    tokens = css.split()
    setattr(_ButtonChain, name, _make_button_property(tokens))

ButtonT = _ButtonChain()

# %% ../nbs/02_components.ipynb #ba073cd1
#| code-fold: true
class _AnchorChain(BeerCssChain):
    """Chainable anchor style helper"""
    pass

ANCHOR_SPECIALS = {
    'muted': 'grey-text',
    'text': '',
    'reset': 'no-underline',
    'primary': 'primary-text link',
    'classic': 'link',
    'inverse': 'inverse-link'
}

def _make_anchor_property(tokens):
    return property(lambda self: _AnchorChain(self._tokens + tokens))

for name, css in ANCHOR_SPECIALS.items():
    tokens = css.split() if css else []
    setattr(_AnchorChain, name, _make_anchor_property(tokens))

AT = _AnchorChain()

# %% ../nbs/02_components.ipynb #e55766c2
class SpaceT(VEnum):
    """Space types using BeerCSS spacing classes"""
    no_space = 'no-space'
    small_space = 'small-space'
    medium_space = 'medium-space'
    large_space = 'large-space'


class GridSpanT(VEnum):
    """Grid span classes for responsive layouts (BeerCSS)"""
    s1 = 's1'
    s2 = 's2'
    s3 = 's3'
    s4 = 's4'
    s5 = 's5'
    s6 = 's6'
    s7 = 's7'
    s8 = 's8'
    s9 = 's9'
    s10 = 's10'
    s11 = 's11'
    s12 = 's12'
    m1 = 'm1'
    m2 = 'm2'
    m3 = 'm3'
    m4 = 'm4'
    m5 = 'm5'
    m6 = 'm6'
    m7 = 'm7'
    m8 = 'm8'
    m9 = 'm9'
    m10 = 'm10'
    m11 = 'm11'
    m12 = 'm12'
    l1 = 'l1'
    l2 = 'l2'
    l3 = 'l3'
    l4 = 'l4'
    l5 = 'l5'
    l6 = 'l6'
    l7 = 'l7'
    l8 = 'l8'
    l9 = 'l9'
    l10 = 'l10'
    l11 = 'l11'
    l12 = 'l12'


def _has_space_token(tokens):
    space_tokens = {'space', 'no-space', 'small-space', 'medium-space', 'large-space'}
    return any(t in space_tokens for t in tokens)


def GridCell(*c, span=(), cls='', **kwargs):
    """Wrap content as a BeerCSS grid cell with responsive span control."""
    cell_cls = []
    cell_cls.extend(normalize_tokens(span))
    cell_cls.extend(normalize_tokens(cls))
    cell_cls = [t for t in cell_cls if t]
    return Div(*c, cls=stringify(dedupe_preserve_order(cell_cls)), **kwargs)


# Valid BeerCSS 12-column grid divisors (cols that divide evenly into 12)
_VALID_GRID_COLS = (1, 2, 3, 4, 6, 12)

def _snap_to_valid_cols(n: int) -> int:
    """Snap column count to nearest valid 12-column grid divisor."""
    if n <= 0: return 1
    if n >= 12: return 12
    for valid in _VALID_GRID_COLS:
        if n <= valid: return valid
    return 12


def _wrap_grid_children(cells, cols_sm=None, cols_md=None, cols_lg=None):
    """Wrap grid children with span classes based on column counts."""
    # If no column counts specified, return cells as-is
    if not any([cols_sm, cols_md, cols_lg]):
        return cells
    
    # Build span string from column counts
    spans = []
    if cols_sm:
        spans.append(f's{12 // cols_sm}')
    if cols_md:
        spans.append(f'm{12 // cols_md}')
    if cols_lg:
        spans.append(f'l{12 // cols_lg}')
    
    span_str = ' '.join(spans) if spans else 's12'
    
    # Wrap each child in a GridCell with the calculated spans
    return [GridCell(cell, span=span_str) for cell in cells]


def Grid(*cells, space=SpaceT.medium_space, 
         cols_min: int = 1, cols_max: int = 4,
         cols: int = None, cols_sm: int = None, cols_md: int = None, cols_lg: int = None, 
         responsive: bool = True, padding: bool = True, cls: str = '', **kwargs):
    """BeerCSS responsive grid with smart column defaults and mobile-first design.
    
    MonsterUI-compatible API: `cols` parameter auto-derives responsive breakpoints:
        cols=4 -> cols_sm=1, cols_md=2, cols_lg=4 (mobile stacks, tablet halves, desktop full)
    
    For explicit control, use cols_sm, cols_md, cols_lg directly.
    """
    # Smart defaults based on content count (MonsterUI pattern)
    if cols:
        # Auto-derive responsive breakpoints from cols (mobile-friendly by default)
        # cols=4 -> stack on mobile (1), half on tablet (2), full on desktop (4)
        snapped = _snap_to_valid_cols(cols)
        cols_lg = cols_lg or snapped
        cols_md = cols_md or _snap_to_valid_cols(max(1, snapped // 2))
        cols_sm = cols_sm or 1  # Always stack on mobile for readability
    elif not any([cols_sm, cols_md, cols_lg]):
        # Auto-calculate responsive columns based on item count
        n = len(cells)
        cols_max = min(n, cols_max)
        cols_sm = _snap_to_valid_cols(min(n, cols_min, cols_max))
        cols_md = _snap_to_valid_cols(min(n, cols_min + 1, cols_max))
        cols_lg = _snap_to_valid_cols(min(n, cols_max))
    else:
        # Explicit breakpoint values - snap to valid grid cols
        if cols_sm: cols_sm = _snap_to_valid_cols(cols_sm)
        if cols_md: cols_md = _snap_to_valid_cols(cols_md)
        if cols_lg: cols_lg = _snap_to_valid_cols(cols_lg)
    
    wrapped_cells = _wrap_grid_children(cells, cols_sm, cols_md, cols_lg)
    
    cls_tokens = normalize_tokens(cls)
    grid_cls = ['grid']
    
    if padding and 'padding' not in cls_tokens and 'no-padding' not in cls_tokens:
        grid_cls.append('padding')
    
    if space and not _has_space_token(cls_tokens):
        grid_cls.extend(normalize_tokens(space))
    
    grid_cls.extend(cls_tokens)
    grid_cls = [t for t in grid_cls if t]
    
    return Div(*wrapped_cells, cls=stringify(dedupe_preserve_order(grid_cls)), **kwargs)

# %% ../nbs/02_components.ipynb #1264260c
def DivHStacked(*c, responsive=True, padding=True, cls='', **kwargs):
    """Responsive horizontal stack with padding and mobile compatibility."""
    cls_tokens = normalize_tokens(cls)
    tokens = []
    if responsive and 'responsive' not in cls_tokens:
        tokens.append('responsive')
    if padding and 'padding' not in cls_tokens and 'no-padding' not in cls_tokens:
        tokens.append('padding')
    if 'grid' not in cls_tokens:
        tokens.extend(['row', 'middle-align'])
    if not _has_space_token(cls_tokens):
        tokens.append(SpaceT.medium_space)
    tokens.extend(cls_tokens)
    tokens = [t for t in tokens if t]
    return Div(*c, cls=stringify(dedupe_preserve_order(tokens)), **kwargs)


# %% ../nbs/02_components.ipynb #16cae24c
def DivLAligned(*c, cls='', **kwargs):
    """MonsterUI-compatible left-aligned row using BeerCSS tokens."""
    cls_tokens = normalize_tokens(cls)
    tokens = ['left-align']
    tokens.extend(cls_tokens)
    tokens = [t for t in tokens if t]
    return DivHStacked(*c, cls=stringify(dedupe_preserve_order(tokens)), **kwargs)



# %% ../nbs/02_components.ipynb #2435d500
def DivVStacked(*c, responsive=True, padding=True, cls='', **kwargs):
    """Responsive vertical stack with padding and mobile compatibility."""
    cls_tokens = normalize_tokens(cls)
    tokens = []
    if responsive and 'responsive' not in cls_tokens:
        tokens.append('responsive')
    if padding and 'padding' not in cls_tokens and 'no-padding' not in cls_tokens:
        tokens.append('padding')
    if 'grid' not in cls_tokens:
        tokens.extend(['row', 'vertical'])
    if not _has_space_token(cls_tokens):
        tokens.append(SpaceT.medium_space)
    tokens.extend(cls_tokens)
    tokens = [t for t in tokens if t]
    return Div(*c, cls=stringify(dedupe_preserve_order(tokens)), **kwargs)


# %% ../nbs/02_components.ipynb #1c19a699
def DivRAligned(*c, cls='', **kwargs):
    """MonsterUI-compatible right-aligned row using BeerCSS tokens."""
    cls_tokens = normalize_tokens(cls)
    tokens = ['right-align']
    tokens.extend(cls_tokens)
    tokens = [t for t in tokens if t]
    return DivHStacked(*c, cls=stringify(dedupe_preserve_order(tokens)), **kwargs)

# %% ../nbs/02_components.ipynb #0a028d84
def DivCentered(*c, cls='', **kwargs):
    """Center-aligned container using BeerCSS tokens."""
    cls_tokens = normalize_tokens(cls)
    tokens = ['center-align']
    tokens.extend(cls_tokens)
    tokens = [t for t in tokens if t]
    return Div(*c, cls=stringify(dedupe_preserve_order(tokens)), **kwargs)

# %% ../nbs/02_components.ipynb #7b232371
def DivFullySpaced(*c, cls='', **kwargs):
    """Row with children stretched to far ends using BeerCSS `max` spacers."""
    cls_tokens = normalize_tokens(cls)
    tokens = []
    if 'grid' not in cls_tokens:
        tokens.extend(['row', 'middle-align'])
    if not _has_space_token(cls_tokens):
        tokens.append(SpaceT.no_space)
    tokens.extend(cls_tokens)
    tokens = [t for t in tokens if t]
    base = list(c)
    if 'grid' not in cls_tokens and len(base) > 1:
        spaced_children = []
        for i, child in enumerate(base):
            spaced_children.append(child)
            if i != len(base) - 1:
                spaced_children.append(Div(cls='max'))
        base = spaced_children
    return Div(*base, cls=stringify(dedupe_preserve_order(tokens)), **kwargs)

# %% ../nbs/02_components.ipynb #0e0bbaca
#| code-fold: true
def Icon(icon: str, size: str = None, fill: bool = False, cls = (), **kwargs):
    """Material Design icon with optional size and fill"""
    icon_cls = []
    if size: icon_cls.append(size)
    if fill: icon_cls.append('fill')
    if cls: icon_cls.extend(normalize_tokens(cls))
    cls_str = ' '.join(icon_cls) if icon_cls else None
    return I(icon, cls=cls_str, **kwargs) if cls_str else I(icon, **kwargs)

# %% ../nbs/02_components.ipynb #f1e25f09
#| code-fold: true
def NavBar(*children, brand=None, sticky=False, blur=None, cls='', size='small',
           center=False, end=None,
           hx_boost=True, hx_target='#main-content', hx_swap=None, **kwargs):
    """Horizontal navigation bar with HTMX SPA navigation defaults.
    
    Args:
        brand: Brand element (logo/title) positioned on the left
        sticky: Whether navbar sticks to top on scroll
        blur: Blur effect class ('small-blur', 'medium-blur', 'large-blur') for glass effect
        size: Navbar size - 'small' (default), 'medium', 'large', or None
        center: Center the navigation links (default False = links on right)
        end: Optional end slot content (shown at right when center=True)
        hx_boost: Auto-enhance all <a> links for HTMX navigation (default True)
        hx_target: Target element for boosted links (default '#main-content')
        hx_swap: HTMX swap method (e.g., 'outerHTML' for full main swap)
    
    Layout modes:
        center=False: [brand] [spacer] [links...]
        center=True:  [brand] [spacer] [links centered] [spacer] [end?]
    """
    size_cls = size if size else ''
    blur_cls = blur if blur else ''
    nav_cls = f"{'sticky top' if sticky else ''} surface {size_cls} {blur_cls} {cls}".strip()
    
    # HTMX SPA optimizations
    if hx_boost: kwargs['hx_boost'] = 'true'
    if hx_target: kwargs['hx_target'] = hx_target
    if hx_swap: kwargs['hx_swap'] = hx_swap
    kwargs.setdefault('hx_push_url', 'true')
    
    # Use small-padding for compact navbar
    padding_cls = 'small-padding' if size == 'small' else 'padding'
    
    if brand:
        if center:
            # Centered layout: brand | spacer | links(centered) | spacer | end?
            parts = [brand, Div(cls='max'), Div(*children, cls='center-align'), Div(cls='max')]
            if end:
                parts.append(end)
            return Nav(*parts, cls=f"row middle-align {padding_cls} {nav_cls}", **kwargs)
        # Default: brand | spacer | links (right-aligned)
        return Nav(brand, Div(cls='max'), *children, cls=f"row middle-align {padding_cls} {nav_cls}", **kwargs)
    return Nav(*children, cls=f"{padding_cls} {nav_cls}", **kwargs)

# %% ../nbs/02_components.ipynb #6d92c66b
def Modal(*c, id=None, footer=None, active=False, overlay='default', position=None, cls=(), **kwargs):
    """BeerCSS modal dialog with position and overlay options.
    
    Always returns a list for consistent unpacking with *Modal(...).
    When overlay is enabled, clicking the overlay closes the modal.
    
    Args:
        *c: Modal content (title, body, etc.)
        id: Modal ID for data-ui targeting
        footer: Footer content (auto-wrapped in Nav if not already)
        active: Whether modal starts active/visible
        overlay: Overlay style - 'default' (plain), 'blur', 'small-blur', 
                 'medium-blur', 'large-blur', or False/None (no overlay)
        position: Position - None (center), 'left', 'right', 'top', 'bottom'
        cls: Additional CSS classes
    
    Returns:
        List of elements (overlay + dialog, or just [dialog] if no overlay)
    """
    modal_cls = normalize_tokens(cls)
    
    # Add position class if specified
    if position in ['left', 'right', 'top', 'bottom']:
        modal_cls.append(position)
    
    if active: 
        modal_cls.append('active')
    
    children = list(c)
    if footer:
        if hasattr(footer, 'tag') and footer.tag == 'nav': 
            children.append(footer)
        else: 
            children.append(Nav(*(footer if is_listy(footer) else [footer])))
    
    cls_str = ' '.join(modal_cls) if modal_cls else None
    
    # Create the dialog
    dialog = Dialog(*children, id=id, cls=cls_str, **kwargs)
    
    # Handle overlay - always return a list for consistent *Modal(...) unpacking
    if overlay and overlay not in [False, None]:
        overlay_classes = ['overlay']
        
        # Map overlay parameter to BeerCSS classes
        if overlay == 'blur':
            overlay_classes.append('blur')
        elif overlay == 'small-blur':
            overlay_classes.append('small-blur')
        elif overlay == 'medium-blur':
            overlay_classes.append('medium-blur')
        elif overlay == 'large-blur':
            overlay_classes.append('large-blur')
        # 'default' means plain overlay with no blur class
        
        overlay_cls = ' '.join(overlay_classes)
        if active:
            overlay_cls += " active"
        
        # Overlay with data-ui to close modal on click (click-outside-to-close)
        overlay_el = Div(cls=overlay_cls, data_ui=f"#{id}" if id else None)
        
        return [overlay_el, dialog]
    
    # No overlay - still return as list for consistent unpacking
    return [dialog]

def ModalButton(text: str, id: str, icon: str = None, cls=(), **kwargs):
    """Button that opens a modal via data-ui attribute."""
    kwargs["data_ui"] = f"#{id}"
    return Button(text, icon=icon, cls=cls, **kwargs)

def ModalCancel(text="Cancel", modal_id=None, cls=(), **kwargs):
    """Cancel button that closes modal via data-ui."""
    cancel_cls = normalize_tokens(cls)
    cancel_cls.extend(['transparent', 'link'])
    if modal_id:
        kwargs["data_ui"] = f"#{modal_id}"
    return Button(text, cls=' '.join(cancel_cls), **kwargs)

def ModalConfirm(text="Confirm", modal_id=None, cls=(), **kwargs):
    """Confirm button that closes modal via data-ui."""
    confirm_cls = normalize_tokens(cls)
    confirm_cls.extend(['transparent', 'link'])
    if modal_id:
        kwargs["data_ui"] = f"#{modal_id}"
    return Button(text, cls=' '.join(confirm_cls), **kwargs)

def ModalTitle(*c, cls=(), **kwargs):
    """Modal title using H5 element."""
    return H5(*c, cls=stringify(cls), **kwargs)

def ModalBody(*c, cls=(), **kwargs):
    """Modal body wrapper for content layout."""
    return Div(*c, cls=stringify(cls), **kwargs)

def ModalFooter(*c, cls=(), **kwargs):
    """Modal footer with right-aligned action buttons."""
    footer_cls = normalize_tokens(cls)
    footer_cls.extend(['right-align', 'no-space'])
    return Nav(*c, cls=' '.join(footer_cls), **kwargs)

# %% ../nbs/02_components.ipynb #3a16954d
#| code-fold: true
def Field(*c, label: bool = False, prefix: bool = False, suffix: bool = False, cls = '', **kwargs):
    """BeerCSS field wrapper for inputs with border/label/prefix/suffix styling."""
    field_cls = ['field', 'small', 'round', 'border']
    if label: field_cls.append('label')
    if prefix: field_cls.append('prefix')
    if suffix: field_cls.append('suffix')
    cls_str = f"{' '.join(field_cls)} {cls}".strip()
    return Div(*c, cls=cls_str, **kwargs)

def LabelInput(label: str, id: str = None, placeholder: str = None, input_type: str = 'text',
               prefix_icon: str = None, suffix_icon: str = None, value: str = None, cls = '', **kwargs):
    """Labeled input field with floating label and optional icons."""
    if not id: id = label.lower().replace(' ', '-')
    children = []
    if prefix_icon: children.append(I(prefix_icon))
    input_attrs = {'type': input_type, 'id': id, 'name': id, 'placeholder': placeholder if placeholder is not None else " "}
    if value is not None: input_attrs['value'] = value
    input_attrs.update(kwargs)
    children.append(Input(**input_attrs))
    children.append(Label(label, fr=id))
    if suffix_icon: children.append(I(suffix_icon))
    return Field(*children, label=True, prefix=bool(prefix_icon), suffix=bool(suffix_icon), cls=cls)

# %% ../nbs/02_components.ipynb #d6668f1b
#| code-fold: true
def LabelInput(label: str, id: str = None, placeholder: str = None, input_type: str = 'text',
               prefix_icon: str = None, suffix_icon: str = None, value: str = None, cls = '', **kwargs):
    """Labeled input field with floating label and optional icons."""
    if not id: id = label.lower().replace(' ', '-')
    children = []
    if prefix_icon: children.append(I(prefix_icon))
    input_attrs = {'type': input_type, 'id': id, 'name': id, 'placeholder': placeholder if placeholder is not None else " "}
    if value is not None: input_attrs['value'] = value
    input_attrs.update(kwargs)
    children.append(Input(**input_attrs))
    children.append(Label(label, fr=id))
    if suffix_icon: children.append(I(suffix_icon))
    return Field(*children, label=True, prefix=bool(prefix_icon), suffix=bool(suffix_icon), cls=cls)

def FormLabel(*c, cls=(), **kwargs):
    """Standalone form label element with MonsterUI-compatible signature."""
    cls_str = stringify(cls) if cls else None
    if cls_str: return Label(*c, cls=cls_str, **kwargs)
    return Label(*c, **kwargs)

# %% ../nbs/02_components.ipynb #b9cf5341
#| code-fold: true
def CheckboxX(*c, cls=(), **kwargs):
    """BeerCSS checkbox with label support."""
    label_text = stringify(c) if c else ""
    checkbox_cls = ['checkbox']
    if cls: checkbox_cls.extend(normalize_tokens(cls))
    cls_str = stringify(checkbox_cls)
    return Label(Input(type='checkbox', **kwargs), Span(label_text) if label_text else Span(), cls=cls_str)

# %% ../nbs/02_components.ipynb #784de047
#| code-fold: true
def Radio(*c, cls=(), **kwargs):
    """BeerCSS radio button with label."""
    label_text = stringify(c) if c else ""
    radio_cls = ['radio']
    if cls: radio_cls.extend(normalize_tokens(cls))
    cls_str = stringify(radio_cls)
    return Label(Input(type='radio', **kwargs), Span(label_text) if label_text else Span(), cls=cls_str)

# %% ../nbs/02_components.ipynb #128c2b3c
#| code-fold: true
def Switch(*c, cls=(), **kwargs):
    """BeerCSS toggle switch for on/off states.
    
    If label text is provided, wraps in a nav with the label on the left
    and the switch on the right (standard BeerCSS pattern).
    """
    label_text = stringify(c) if c else ""
    switch_cls = ['switch']
    if cls: switch_cls.extend(normalize_tokens(cls))
    cls_str = stringify(switch_cls)
    
    # Core switch element: input + span (for the toggle visual)
    switch_label = Label(Input(type='checkbox', **kwargs), Span(), cls=cls_str)
    
    # If no label text, return just the switch
    if not label_text:
        return switch_label
    
    # With label text, use BeerCSS nav pattern for proper layout
    return Nav(
        Div(Span(label_text), cls='max'),
        switch_label,
        cls='middle-align'
    )

# %% ../nbs/02_components.ipynb #0c1c2718
#| code-fold: true
def TextArea(*c, cls=(), **kwargs):
    """BeerCSS textarea with field wrapper for consistent styling."""
    content = stringify(c) if c else ""
    textarea = Textarea(content, **kwargs) if content else Textarea(**kwargs)
    return Field(textarea, cls=cls)

# %% ../nbs/02_components.ipynb #ddbbd284
#| code-fold: true
def Range(*c, min=None, max=None, step=None, cls=(), **kwargs):
    """BeerCSS range slider with two-tone fill effect."""
    min_val = min if min is not None else 0
    max_val = max if max is not None else 100
    value = kwargs.get('value', min_val)
    percentage = ((float(value) - float(min_val)) / (float(max_val) - float(min_val))) * 100 if max_val != min_val else 0
    
    input_attrs = {'type': 'range'}
    if min is not None: input_attrs['min'] = min
    if max is not None: input_attrs['max'] = max
    if step is not None: input_attrs['step'] = step
    input_attrs['oninput'] = """
        const val = this.value;
        const min = this.min || 0;
        const max = this.max || 100;
        const percentage = ((val - min) / (max - min)) * 100;
        this.parentElement.style.setProperty('--_start', '0%');
        this.parentElement.style.setProperty('--_end', percentage + '%');
    """.strip()
    input_attrs.update(kwargs)
    
    slider_cls = ['slider']
    if cls: slider_cls.extend(normalize_tokens(cls))
    cls_str = stringify(slider_cls)
    style = f"--_start: 0%; --_end: {percentage:.1f}%;"
    return Label(Input(**input_attrs), Span(), cls=cls_str, style=style)

# %% ../nbs/02_components.ipynb #6262a7d8
#| code-fold: true
def SelectMenu(*items, value='', placeholder='Select...', prefix_icon=None, id=None, cls=(), **kwargs):
    """BeerCSS menu-based dropdown for display menus (profile, actions, navigation).
    
    Not intended for form input - use Select for forms with native change events.
    This component uses BeerCSS's menu element which opens/closes on click.
    
    Args:
        *items: Menu items - strings are auto-wrapped in Li, or pass Li elements directly
        value: Currently displayed value text
        placeholder: Placeholder text when no value selected
        prefix_icon: Optional icon to show before the input
        id: Optional wrapper ID
        cls: Additional CSS classes
    """
    menu_items = []
    for item in items:
        if isinstance(item, str): menu_items.append(Li(item))
        else: menu_items.append(item)
    
    children = []
    if prefix_icon: children.append(I(prefix_icon))
    
    # Display input (readonly, shows selected value)
    input_attrs = {'value': value, 'readonly': True, 'placeholder': placeholder if placeholder else ' '}
    children.append(Input(**input_attrs))
    
    children.append(I('arrow_drop_down'))
    children.append(Menu(*menu_items))
    
    field_cls = ['field', 'fill', 'round']
    if prefix_icon: field_cls.append('prefix')
    field_cls.append('suffix')
    if cls: field_cls.extend(normalize_tokens(cls))
    cls_str = stringify(field_cls)
    
    wrapper_attrs = {'cls': cls_str}
    if id:
        wrapper_attrs['id'] = id
    wrapper_attrs.update(kwargs)
    
    return Div(*children, **wrapper_attrs)


def Select(*options, label=None, value='', name='', id=None, placeholder=None, cls=(), **kwargs):
    """BeerCSS native HTML select with floating label support.
    
    Uses the native <select> element for full browser support including:
    - Standard 'change' event for HTMX (hx-trigger="change")
    - Keyboard navigation
    - Mobile-friendly native picker
    - Works seamlessly with cascading dropdowns
    
    Args:
        *options: Option items - strings, Option elements, or dicts with 'value'/'label' keys
        label: Floating label text (optional)
        value: Currently selected value
        name: Form field name
        id: Element ID (auto-generated from name if not provided)
        placeholder: Placeholder option text (shown as disabled first option)
        cls: Additional CSS classes for the field wrapper
    """
    # Auto-generate ID from name if not provided
    select_id = id or (f"select-{name}" if name else None)
    
    # Build options list
    opt_elements = []
    
    # Add placeholder as disabled first option if provided
    if placeholder:
        opt_elements.append(Option(placeholder, value='', disabled=True, selected=(not value)))
    
    for opt in options:
        if isinstance(opt, str):
            # String option: value and label are the same
            opt_elements.append(Option(opt, value=opt, selected=(opt == value)))
        elif hasattr(opt, 'tag') and opt.tag == 'option':
            # Already an Option element, pass through
            opt_elements.append(opt)
        elif isinstance(opt, dict):
            # Dict with value/label keys
            opt_val = opt.get('value', opt.get('label', ''))
            opt_label = opt.get('label', opt.get('value', ''))
            opt_elements.append(Option(opt_label, value=opt_val, selected=(opt_val == value)))
    
    # Build select element
    select_attrs = {}
    if name: select_attrs['name'] = name
    if select_id: select_attrs['id'] = select_id
    select_attrs.update(kwargs)
    
    select_el = fc.Select(*opt_elements, **select_attrs)
    
    # Build field wrapper - 'max' ensures full width like LabelInput
    field_cls = ['field', 'border', 'round', 'max']
    if label: field_cls.append('label')
    if cls: field_cls.extend(normalize_tokens(cls))
    cls_str = stringify(field_cls)
    
    children = [select_el]
    if label:
        children.append(Label(label, fr=select_id))
    
    return Div(*children, cls=cls_str)

# %% ../nbs/02_components.ipynb #e6576c75
#| code-fold: true
def FormGrid(*c, cols: int = 1):
    """Responsive grid layout for form fields that stacks on mobile."""
    col_width = 12 // max(1, min(cols, 4))
    col_cls = f"s12 m{col_width}"
    wrapped = [Div(child, cls=col_cls) for child in c]
    return Div(*wrapped, cls="grid")

# %% ../nbs/02_components.ipynb #5a159345
#| code-fold: true
def Progress(*c, value='', max='100', cls=(), **kwargs):
    """Linear progress bar with value/max support."""
    progress_attrs = {}
    if value: progress_attrs['value'] = value
    if max: progress_attrs['max'] = max
    progress_attrs.update(kwargs)
    cls_str = stringify(cls) if cls else None
    if cls_str: return fc.Progress(*c, cls=cls_str, **progress_attrs)
    return fc.Progress(*c, **progress_attrs)

# %% ../nbs/02_components.ipynb #11d87dd6
#| code-fold: true
def LoadingIndicator(size='medium', cls='', **kwargs):
    """BeerCSS circular spinner for async operations."""
    size_cls = size if size else 'medium'
    progress_cls = f"circle {size_cls} {cls}".strip()
    return fc.Progress(cls=progress_cls, **kwargs)

# %% ../nbs/02_components.ipynb #396ebe54
#| code-fold: true
def Table(*c, cls = 'border', **kwargs):
    """BeerCSS table with optional border/stripes classes."""
    cls_str = stringify(cls) if cls else 'border'
    return fc.Table(*c, cls=cls_str, **kwargs)

def Td(*c, shrink = False, expand = False, cls = (), **kwargs):
    """Table data cell with shrink/expand width options."""
    cls_str = stringify(cls) if cls else ''
    if shrink:
        cls_str += ' no-wrap'
        existing_style = kwargs.get('style', '').strip()
        kwargs['style'] = f"{existing_style}; width: 1%;".strip().lstrip(';')
    if expand:
        cls_str += ' no-wrap'
        existing_style = kwargs.get('style', '').strip()
        kwargs['style'] = f"{existing_style}; width: 99%;".strip().lstrip(';')
    return fc.Td(*c, cls=cls_str.strip(), **kwargs) if cls_str.strip() else fc.Td(*c, **kwargs)

def Th(*c, shrink = False, expand = False, cls = (), **kwargs):
    """Table header cell with shrink/expand width options."""
    cls_str = stringify(cls) if cls else ''
    if shrink:
        cls_str += ' no-wrap'
        existing_style = kwargs.get('style', '').strip()
        kwargs['style'] = f"{existing_style}; width: 1%;".strip().lstrip(';')
    if expand:
        cls_str += ' no-wrap'
        existing_style = kwargs.get('style', '').strip()
        kwargs['style'] = f"{existing_style}; width: 99%;".strip().lstrip(';')
    return fc.Th(*c, cls=cls_str.strip(), **kwargs) if cls_str.strip() else fc.Th(*c, **kwargs)

def Thead(*c, cls=(), **kwargs):
    """Table header section."""
    cls_str = stringify(cls) if cls else None
    return fc.Thead(*c, cls=cls_str, **kwargs) if cls_str else fc.Thead(*c, **kwargs)

def Tbody(*c, cls=(), sortable=False, **kwargs):
    """Table body section with optional SortableJS support."""
    cls_str = stringify(cls) if cls else ''
    if sortable: cls_str = f"{cls_str} sortable".strip()
    return fc.Tbody(*c, cls=cls_str, **kwargs) if cls_str else fc.Tbody(*c, **kwargs)

def Tfoot(*c, cls=(), **kwargs):
    """Table footer section."""
    cls_str = stringify(cls) if cls else None
    return fc.Tfoot(*c, cls=cls_str, **kwargs) if cls_str else fc.Tfoot(*c, **kwargs)

def TableFromLists(header_data, body_data, footer_data = None, header_cell_render = Th,
                   body_cell_render = Td, footer_cell_render = Td, cls = 'border', sortable = False, **kwargs):
    """Create table from header list and body list of lists."""
    return Table(
        Thead(Tr(*map(header_cell_render, header_data))),
        Tbody(*[Tr(*map(body_cell_render, row)) for row in body_data], sortable=sortable),
        Tfoot(Tr(*map(footer_cell_render, footer_data))) if footer_data else None,
        cls=cls, **kwargs)

def TableFromDicts(header_data, body_data, footer_data = None, header_cell_render = Th,
                   body_cell_render = lambda k, v: Td(v), footer_cell_render = lambda k, v: Td(v),
                   cls = 'border', sortable = False, **kwargs):
    """Create table from header keys and body list of dicts."""
    return Table(
        Thead(Tr(*[header_cell_render(h) for h in header_data])),
        Tbody(*[Tr(*[body_cell_render(k, row.get(k, '')) for k in header_data]) for row in body_data], sortable=sortable),
        Tfoot(Tr(*[footer_cell_render(k, footer_data.get(k, '')) for k in header_data])) if footer_data else None,
        cls=cls, **kwargs)

# %% ../nbs/02_components.ipynb #0cf9871f
#| code-fold: true
def TableControls(*controls, cls='', **kwargs):
    """Toolbar container for table filters, search, and actions."""
    controls_cls = f"padding middle-align space {cls}".strip()
    return Div(*controls, cls=controls_cls, **kwargs)

# %% ../nbs/02_components.ipynb #93a6e771
#| code-fold: true
def Pagination(current_page: int, total_pages: int, hx_get: str, hx_target: str = '#table-container',
               show_first_last: bool = True, cls='', **kwargs):
    """HTMX-integrated pagination controls with first/prev/next/last buttons."""
    buttons = []
    separator = '&' if '?' in hx_get else '?'
    
    if show_first_last:
        first_disabled = current_page == 1
        buttons.append(FhButton(Icon('first_page'), cls='circle transparent' if first_disabled else 'circle',
                       disabled=first_disabled, hx_get=f"{hx_get}{separator}page=1" if not first_disabled else None,
                       hx_target=hx_target, hx_push_url='true' if not first_disabled else None))
    
    prev_disabled = current_page == 1
    buttons.append(FhButton(Icon('chevron_left'), cls='circle transparent' if prev_disabled else 'circle',
                   disabled=prev_disabled, hx_get=f"{hx_get}{separator}page={current_page - 1}" if not prev_disabled else None,
                   hx_target=hx_target, hx_push_url='true' if not prev_disabled else None))
    
    buttons.append(Span(f"Page {current_page} of {total_pages}", cls='small-text'))
    
    next_disabled = current_page >= total_pages
    buttons.append(FhButton(Icon('chevron_right'), cls='circle transparent' if next_disabled else 'circle',
                   disabled=next_disabled, hx_get=f"{hx_get}{separator}page={current_page + 1}" if not next_disabled else None,
                   hx_target=hx_target, hx_push_url='true' if not next_disabled else None))
    
    if show_first_last:
        last_disabled = current_page >= total_pages
        buttons.append(FhButton(Icon('last_page'), cls='circle transparent' if last_disabled else 'circle',
                       disabled=last_disabled, hx_get=f"{hx_get}{separator}page={total_pages}" if not last_disabled else None,
                       hx_target=hx_target, hx_push_url='true' if not last_disabled else None))
    
    nav_cls = f"center-align middle-align {cls}".strip()
    return Nav(*buttons, cls=nav_cls, **kwargs)

# %% ../nbs/02_components.ipynb #c573ee87
#| code-fold: true
def Card(*c, header = None, footer = None, body_cls = 'padding', header_cls = (), footer_cls = (), cls = (), **kwargs):
    """BeerCSS card with optional header/footer sections."""
    cls = normalize_tokens(cls)
    header_cls = normalize_tokens(header_cls)
    footer_cls = normalize_tokens(footer_cls)
    body_cls = normalize_tokens(body_cls)
    sections = []
    if header is not None: sections.append(Header(header, cls=header_cls) if header_cls else Header(header))
    if c: sections.append(Div(*c, cls=body_cls) if body_cls else Div(*c))
    if footer is not None: sections.append(Nav(footer, cls=footer_cls) if footer_cls else Nav(footer))
    return Article(*sections, cls=cls, **kwargs)

# %% ../nbs/02_components.ipynb #f0c71209
#| code-fold: true
def Toolbar(*items, cls='', elevate='large', fill=True, **kwargs):
    """BeerCSS toolbar for action bars with elevation options."""
    classes = ['toolbar']
    if elevate: classes.append(f'{elevate}-elevate')
    if fill: classes.append('fill')
    if cls: classes.append(cls)
    return Nav(*items, cls=' '.join(classes), **kwargs)

# %% ../nbs/02_components.ipynb #7019d70e
#| code-fold: true
def Toast(*c, cls='', position='top', variant='', action=None, active=False, dur=None, **kwargs):
    """BeerCSS snackbar/toast notification with position ('top' or 'bottom') and variant options."""
    classes = ['snackbar']
    if variant: classes.append(variant)
    if position in ['top', 'bottom']:
        classes.append(position)
    if active: classes.append('active')
    if cls: classes.append(cls)
    
    content = []
    if action:
        if c: content.append(Div(*c, cls='max'))
        if isinstance(action, str): content.append(A(action, cls='inverse-link'))
        else: content.append(action)
    else: content.extend(c)
    
    snackbar = Div(*content, cls=' '.join(classes), **kwargs)
    
    # If duration is specified and there's an id, generate auto-hide script
    if dur and 'id' in kwargs:
        timeout_ms = int(dur * 1000)
        toast_id = kwargs['id']
        # Wait for Beer CSS module to load before calling ui()
        script = Script(f"""
window.addEventListener('load', function() {{
    function showToast() {{
        if (typeof ui !== 'undefined') {{
            try {{ ui('#{toast_id}', {timeout_ms}); }}
            catch (e) {{ console.error('Toast error:', e.message); }}
        }}
    }}
    setTimeout(showToast, 50);
}});
""")
        return (snackbar, script)
    
    return snackbar

def Snackbar(*c, **kwargs):
    """Alias for Toast component."""
    return Toast(*c, **kwargs)

# %% ../nbs/02_components.ipynb #fe1b0be7
#| code-fold: true
class ContainerT(VEnum):
    """Container size options (BeerCSS). Most alias to 'responsive'; use 'expand' for full-width."""
    xs = 'responsive'
    sm = 'responsive'
    medium = 'responsive'
    lg = 'responsive'
    xl = 'responsive'
    expand = 'responsive max'

# %% ../nbs/02_components.ipynb #f53893ec
#| code-fold: true
def _get_form_config(col: dict) -> dict:
    """Extract form config from column, with sensible defaults."""
    form = col.get("form", {})
    if form is None:
        form = {}
    
    return {
        "type": form.get("type", "text"),
        "required": form.get("required", False),
        "placeholder": form.get("placeholder", ""),
        "options": form.get("options", []),
        "disabled": form.get("disabled", False),
        "hidden": form.get("hidden", False),
    }


def FormField(
    col: dict,
    value: Any = None,
    mode: str = "edit"
) -> Any:
    """Render a single form field based on column config.
    
    Note: Currently uses DataTable column config schema. 
    Could be expanded to accept simpler field specs in the future.
    """
    key = col["key"]
    label = col.get("label", key.replace("_", " ").title())
    form_cfg = _get_form_config(col)
    
    # Hidden fields
    if form_cfg["hidden"]:
        return None
    
    # In view mode, all fields are disabled
    is_disabled = mode == "view" or form_cfg["disabled"]
    field_type = form_cfg["type"]
    
    # Handle value for create mode (empty)
    if mode == "create":
        value = ""
    
    # Build field based on type
    if field_type == "select":
        # Use menu-based Select from fh_matui.components
        options = form_cfg["options"]
        opt_items = []
        for opt in options:
            if isinstance(opt, dict):
                opt_items.append(opt.get("label", opt.get("value", "")))
            else:
                opt_items.append(str(opt))
        
        return Select(
            *opt_items,
            value=str(value) if value else "",
            name=key,
            placeholder=label
        )
    
    else:
        # Use LabelInput for text, email, number, date, etc.
        attrs = {}
        if is_disabled:
            attrs["disabled"] = True
        
        return LabelInput(
            label,
            id=key,
            input_type=field_type,
            value=str(value) if value is not None else "",
            **attrs
        )

# %% ../nbs/02_components.ipynb #aff2ee40
#| code-fold: true
from typing import Callable, Any

def FormModal(
    columns: list[dict],
    mode: str = "view",
    record: dict = None,
    modal_id: str = "form-modal",
    title: str = None,
    save_url: str = None,
    save_target: str = None,
    cancel_url: str = None,
    cancel_target: str = None,
    form_layout: Callable = None,
    row_id_field: str = "id",
    indicator_id: str = None
) -> Any:
    """Generate a modal dialog with form fields driven by column config.
    
    Uses FormGrid (single column by default) for field layout.
    Integrates with HTMX for form submission.
    """
    record = record or {}
    
    # Auto-generate title
    if title is None:
        title = {
            "view": "View Record",
            "edit": "Edit Record", 
            "create": "New Record"
        }.get(mode, "Record")
    
    # Generate form fields
    fields = {}
    field_elements = []
    
    for col in columns:
        field = FormField(col, value=record.get(col["key"]), mode=mode)
        if field is not None:
            fields[col["key"]] = field
            field_elements.append(field)
    
    # Use custom layout or default single-column FormGrid
    if form_layout and callable(form_layout):
        form_content = form_layout(fields, mode, record)
    else:
        form_content = FormGrid(*field_elements, cols=1)
    
    # Build form with hidden ID field for edit mode
    form_children = [form_content]
    
    if mode == "edit" and row_id_field in record:
        form_children.insert(0, Input(
            type="hidden",
            name=row_id_field,
            value=str(record[row_id_field])
        ))
    
    # Build footer buttons
    footer_buttons = []
    
    # Cancel button (always present)
    cancel_attrs = {"cls": "border"}
    if cancel_url:
        cancel_attrs["hx_get"] = cancel_url
        if cancel_target:
            cancel_attrs["hx_target"] = cancel_target
            cancel_attrs["hx_swap"] = "outerHTML"
    else:
        # Default: close modal via data-ui
        cancel_attrs["data_ui"] = f"#{modal_id}"
    
    footer_buttons.append(Button("Cancel", **cancel_attrs))
    
    # Save button (only for edit/create modes)
    if mode in ("edit", "create"):
        save_attrs = {"cls": ButtonT.primary}
        
        if save_url:
            save_attrs["hx_post"] = save_url
            if save_target:
                save_attrs["hx_target"] = save_target
                save_attrs["hx_swap"] = "outerHTML"
            if indicator_id:
                save_attrs["hx_indicator"] = f"#{indicator_id}"
        
        save_label = "Create" if mode == "create" else "Save"
        footer_buttons.append(Button(
            save_label,
            type="submit",
            **save_attrs
        ))
    
    # Center buttons horizontally using flex justify-center
    footer = Div(*footer_buttons, cls="row center small-space")
    
    # Build form element
    form_attrs = {"cls": "padding"}
    if save_url and mode in ("edit", "create"):
        form_attrs["hx_post"] = save_url
        if save_target:
            form_attrs["hx_target"] = save_target
            form_attrs["hx_swap"] = "outerHTML"
        if indicator_id:
            form_attrs["hx_indicator"] = f"#{indicator_id}"
    
    form = Form(*form_children, **form_attrs)
    
    # Build modal with title and form
    return Modal(
        ModalTitle(title),
        form,
        id=modal_id,
        footer=footer,
        active=True,
        cls="large-width"
    )

# %% ../nbs/02_components.ipynb #945684a7
#| code-fold: true
def NavContainer(*li, title=None, brand=None, position='left', close_button=True, cls='active', id=None, **kwargs):
    """Slide-out navigation drawer with header and close button."""
    children = []
    header_nav_content = []
    
    if brand:
        if isinstance(brand, str): header_nav_content.append(Img(src=brand, cls='circle large'))
        else: header_nav_content.append(brand)
    if title: header_nav_content.append(H6(title, cls='max'))
    if close_button and id: header_nav_content.append(FhButton(I('close'), cls='transparent circle large', data_ui=f'#{id}'))
    if header_nav_content: children.append(Header(Nav(*header_nav_content)))
    
    children.append(Div(cls='space'))
    
    processed_items = []
    for item in li:
        if hasattr(item, 'tag') and item.tag == 'li':
            item_attrs = getattr(item, 'attrs', {})
            item_cls = item_attrs.get('cls', '')
            if not item_cls:
                new_attrs = dict(item_attrs)
                new_attrs['cls'] = 'wave round'
                processed_items.append(Li(*item.children, **new_attrs))
            else: processed_items.append(item)
        else: processed_items.append(item)
    
    if processed_items: children.append(Ul(*processed_items, cls='list'))
    dialog_cls = [position]
    if cls: dialog_cls.append(cls)
    cls_str = ' '.join(dialog_cls)
    if id: return Dialog(*children, id=id, cls=cls_str, **kwargs)
    return Dialog(*children, cls=cls_str, **kwargs)

def NavHeaderLi(*c, cls='horizontal-padding', **kwargs):
    """Navigation header element."""
    return Header(*c, cls=cls, **kwargs)

def NavDividerLi(cls='', **kwargs):
    """Navigation divider (horizontal rule)."""
    return Hr(cls=cls, **kwargs)

def NavCloseLi(dialog_id: str, icon='close', cls='circle transparent', **kwargs):
    """Navigation close button."""
    return FhButton(I(icon), cls=cls, onclick=f"closeDialog('{dialog_id}')", **kwargs)

def NavSubtitle(*c, cls='small-text gray-text padding', **kwargs):
    """Navigation section subtitle label."""
    return Label(*c, cls=cls, **kwargs)

def BottomNav(*c, cls='bottom', size='s', **kwargs):
    """Mobile bottom navigation bar."""
    size_cls = {'s': 'small', 'm': 'medium', 'l': 'large'}.get(size, 'small')
    final_cls = f"{cls} {size_cls}".strip()
    return Nav(*c, cls=final_cls, **kwargs)

# %% ../nbs/02_components.ipynb #359f9555
#| code-fold: true
def NavSideBarHeader(*c, cls='', **kwargs):
    """Sidebar header section for menu buttons and branding."""
    return Header(*c, cls=cls, **kwargs)

def NavSideBarLinks(*children, as_list=False, cls='', **kwargs):
    """Container for navigation links (optional list wrapper)."""
    if as_list:
        list_cls = f"list {cls}".strip()
        return Ul(*children, cls=list_cls, **kwargs)
    return Group(*children) if len(children) > 1 else (children[0] if children else Group())

def NavSideBarContainer(*children, position='left', size='m', cls='', active=False,
                        hx_boost=True, hx_target='#main-content', **kwargs):
    """BeerCSS navigation sidebar with HTMX SPA navigation defaults.
    
    Args:
        position: Sidebar position ('left' or 'right')
        size: Sidebar size ('s', 'm', 'l')
        active: Whether sidebar starts visible
        hx_boost: Auto-enhance all <a> links for HTMX navigation (default True)
        hx_target: Target element for boosted links (default '#main-content')
    
    Usage:
        Routes should check `req.headers` for HX-Request and return content only for HTMX requests:
        
        @rt("/dashboard")
        def dashboard(req):
            content = dashboard_content()
            if 'HX-Request' in req.headers: return content  # HTMX swap
            return Layout(content)  # Full page load
    """
    base_cls = f"{size} {position} surface"
    if active: base_cls += " active"
    nav_cls = f"{base_cls} {cls}".strip()
    
    # HTMX SPA optimizations
    if hx_boost: kwargs['hx_boost'] = 'true'
    if hx_target: kwargs['hx_target'] = hx_target
    kwargs.setdefault('hx_push_url', 'true')
    
    return Nav(*children, cls=nav_cls, **kwargs)

# %% ../nbs/02_components.ipynb #541b2d99
#| code-fold: true
def Page(*c, active=True, position=None, cls='', **kwargs):
    """BeerCSS animated page container.
    
    Pages are containers that can be a main page, multiple pages, or animated elements.
    
    Args:
        active: Show page (default True)
        position: Animation direction - 'left', 'right', 'top', 'bottom' (optional)
        cls: Additional classes
    
    Example:
        Page(H1("Dashboard"), P("Content here"))  # Default active page
        Page(content, position='right')  # Slide from right animation
    """
    page_cls = ['page']
    if active: page_cls.append('active')
    if position: page_cls.append(position)
    if cls: page_cls.extend(normalize_tokens(cls))
    return Div(*c, cls=stringify(page_cls), **kwargs)

def Layout(*content, sidebar=None, sidebar_links=None, nav_bar=None, container_size=ContainerT.expand,
           main_bg='surface', sidebar_id='app-sidebar', main_id='main-content', cls='', **kwargs):
    """App layout with HTMX SPA navigation and responsive mobile support.
    
    Args:
        main_id: ID for main content area (default 'main-content') - use as hx-target
        
    HTMX SPA features:
        - hx-boost on sidebar automatically enhances all <a> links
        - hx-history-elt for back/forward button caching
        - Routes should check `req.headers` for HX-Request and return content only for HTMX requests
        
    Mobile Responsive:
        - Desktop (>992px): Left sidebar visible, bottom nav hidden
        - Mobile (≤992px): Bottom nav visible, sidebar hidden
        - Both navigations share the same sidebar_links for consistent behavior
        
    Usage:
        @rt("/dashboard")
        def dashboard(req):
            content = dashboard_content()
            if 'HX-Request' in req.headers: return content  # HTMX swap
            return Layout(content)  # Full page load
    """
    # Build content wrapper with history caching
    content_wrapper = None
    if content:
        content_wrapper = Div(*content, hx_history_elt='true')
    
    # No sidebar - simple layout
    if not sidebar and not sidebar_links:
        result = []
        if nav_bar:
            if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
                if 'sticky' not in nav_bar.attrs['cls']: nav_bar.attrs['cls'] += ' sticky top'
            result.append(nav_bar)
        if content_wrapper:
            container_cls = stringify((container_size, 'padding', main_bg))
            page_content = Page(content_wrapper, id=main_id)
            result.append(Main(page_content, cls=container_cls))
        return Div(*result, cls=cls, **kwargs) if result else Div(cls=cls, **kwargs)
    
    # Sidebar layout with hx-boost
    sidebar_children = [NavSideBarHeader(NavToggleButton(f"#{sidebar_id}"))]
    
    if sidebar_links: sidebar_children.extend(sidebar_links)
    elif sidebar:
        if is_listy(sidebar): sidebar_children.extend(sidebar)
        else: sidebar_children.append(sidebar)
    
    nav_rail = NavSideBarContainer(*sidebar_children, position='left', size='l', id=sidebar_id, cls='desktop-nav')
    
    # Mobile bottom navigation with same links
    bottom_nav_children = sidebar_links if sidebar_links else (list(sidebar) if is_listy(sidebar) else [sidebar] if sidebar else [])
    nav_bottom = BottomNav(*bottom_nav_children, cls='bottom mobile-nav', 
                           hx_boost='true', hx_target=f'#{main_id}', hx_push_url='true')
    
    # Responsive CSS for desktop/mobile navigation switching
    # Only hide elements when needed - don't override Beer CSS flex layout on desktop
    responsive_nav_css = Style("""
        .mobile-nav { display: none !important; }
        @media (max-width: 992px) {
            .mobile-nav { display: flex !important; }
            .desktop-nav { display: none !important; }
        }
    """)
    
    layout_children = [responsive_nav_css]
    
    if nav_bar:
        if hasattr(nav_bar, 'attrs') and 'cls' in nav_bar.attrs:
            if 'sticky' not in nav_bar.attrs['cls']: nav_bar.attrs['cls'] += ' sticky top'
        layout_children.append(nav_bar)
    
    layout_children.append(nav_rail)
    layout_children.append(nav_bottom)
    
    if content_wrapper:
        container_cls = stringify((container_size, 'surface-container', 'round', 'padding', 'bottom-margin', 'horizontal-margin'))
        page_content = Page(content_wrapper, id=main_id)
        layout_children.append(Main(page_content, cls=container_cls))
    
    final_cls = f"surface {cls}".strip() if cls else "surface"
    return Div(*layout_children, cls=final_cls, **kwargs)


# %% ../nbs/02_components.ipynb #4329c780
#| code-fold: true
class TextT(VEnum):
    """Text styles using BeerCSS typography classes."""
    italic = 'italic'
    bold = 'bold'
    underline = 'underline'
    overline = 'overline'
    upper = 'upper'
    lower = 'lower'
    capitalize = 'capitalize'
    small_text = 'small-text'
    medium_text = 'medium-text'
    large_text = 'large-text'
    left_align = 'left-align'
    right_align = 'right-align'
    center_align = 'center-align'
    primary_text = 'primary-text'
    secondary_text = 'secondary-text'
    tertiary_text = 'tertiary-text'

class TextPresets(VEnum):
    """Common typography presets combining multiple TextT values."""
    muted_sm = 'small-text secondary-text'
    muted_lg = 'large-text secondary-text'
    bold_sm = 'bold small-text'
    bold_lg = 'bold large-text'
    medium_sm = 'medium small-text'
    medium_muted = 'medium secondary-text'
    primary_link = 'link primary-text'
    muted_link = 'link secondary-text'

# %% ../nbs/02_components.ipynb #c62d0db5
#| code-fold: true
def CodeSpan(*c, cls=(), **kwargs):
    """Inline code snippet."""
    cls_str = stringify(cls) if cls else None
    return Code(*c, cls=cls_str, **kwargs) if cls_str else Code(*c, **kwargs)

def CodeBlock(*c, cls=(), code_cls=(), **kwargs):
    """Block code with pre wrapper."""
    code_cls_str = stringify(code_cls) if code_cls else None
    pre_cls_str = stringify(cls) if cls else None
    code_elem = Code(*c, cls=code_cls_str, **kwargs) if code_cls_str else Code(*c, **kwargs)
    return Pre(code_elem, cls=pre_cls_str) if pre_cls_str else Pre(code_elem)

def Blockquote(*c, cls=(), **kwargs):
    """Blockquote element for quotes."""
    cls_str = stringify(cls) if cls else None
    return fc.Blockquote(*c, cls=cls_str, **kwargs) if cls_str else fc.Blockquote(*c, **kwargs)

def Q(*c, cls='italic large-text', **kwargs):
    """Styled inline quotation."""
    return fc.Q(*c, cls=cls, **kwargs)

def Em(*c, cls=(), **kwargs):
    """Emphasized text."""
    cls_str = stringify(cls) if cls else None
    return fc.Em(*c, cls=cls_str, **kwargs) if cls_str else fc.Em(*c, **kwargs)

def Strong(*c, cls='bold', **kwargs):
    """Strong (bold) text."""
    return fc.Strong(*c, cls=cls, **kwargs)

def Small(*c, cls='small-text', **kwargs):
    """Small text element."""
    return fc.Small(*c, cls=cls, **kwargs)

def Mark(*c, cls=(), **kwargs):
    """Highlighted/marked text."""
    cls_str = stringify(cls) if cls else None
    return fc.Mark(*c, cls=cls_str, **kwargs) if cls_str else fc.Mark(*c, **kwargs)

def Abbr(*c, title='', cls=(), **kwargs):
    """Abbreviation with title tooltip."""
    cls_str = stringify(cls) if cls else None
    return fc.Abbr(*c, title=title, cls=cls_str, **kwargs) if cls_str else fc.Abbr(*c, title=title, **kwargs)

def Sub(*c, cls=(), **kwargs):
    """Subscript text."""
    cls_str = stringify(cls) if cls else None
    return fc.Sub(*c, cls=cls_str, **kwargs) if cls_str else fc.Sub(*c, **kwargs)

def Sup(*c, cls=(), **kwargs):
    """Superscript text."""
    cls_str = stringify(cls) if cls else None
    return fc.Sup(*c, cls=cls_str, **kwargs) if cls_str else fc.Sup(*c, **kwargs)

# %% ../nbs/02_components.ipynb #15d01ef3
#| code-fold: true
def FAQItem(question: str, answer: str, question_cls: str = '', answer_cls: str = ''):
    """Collapsible FAQ item using details/summary.
    
    Atomic component for a single collapsible Q&A item.
    Use FAQSection from web_pages for a full FAQ section with title.
    
    Args:
        question: The question text
        answer: The answer text  
        question_cls: Additional classes for question styling
        answer_cls: Additional classes for answer styling
    """
    return Details(
        Summary(Article(Nav(Div(question, cls=f"max bold {question_cls}".strip()), I("expand_more")), cls="round surface-variant border no-elevate")),
        Article(P(answer, cls=f"secondary-text {answer_cls}".strip()), cls="round border padding"))

# %% ../nbs/02_components.ipynb #287c86c3
#| code-fold: true
def CookiesBanner(message='We use cookies to enhance your experience. By continuing to visit this site you agree to our use of cookies.',
                  accept_text='Accept', decline_text='Decline', settings_text=None, policy_link='/cookies', policy_text='Learn more',
                  position='bottom', on_accept='console.log("Accepted")', on_decline='console.log("Declined")',
                  on_settings='console.log("Settings")', cls='', **kwargs):
    """GDPR-compliant cookie consent banner with accept/decline actions."""
    message_content = Div(Span(message, cls='small-text'), ' ', A(policy_text, href=policy_link, cls=AT.primary), cls='max')
    
    buttons = []
    if decline_text: buttons.append(Button(decline_text, type='button', cls=ButtonT.secondary,
                                           onclick=f"{on_decline}; this.closest('.cookie-banner').remove();"))
    if settings_text: buttons.append(Button(settings_text, type='button', cls=ButtonT.secondary, onclick=on_settings))
    buttons.append(Button(accept_text, type='button', cls=ButtonT.primary,
                          onclick=f"{on_accept}; this.closest('.cookie-banner').remove();"))
    
    banner_content = Div(Icon('cookie', cls='medium'), message_content, Div(*buttons, cls='row'), cls='row middle-align')
    
    position_style = {'top': 'position: fixed; top: 0; left: 0; right: 0; z-index: 9999;',
                      'bottom': 'position: fixed; bottom: 0; left: 0; right: 0; z-index: 9999;'}
    style = position_style.get(position, position_style['bottom'])
    banner_cls = f'cookie-banner surface-container padding shadow {cls}'.strip()
    return Div(banner_content, cls=banner_cls, style=style, **kwargs)
