Source code for duck.html.components.footer

"""
Footer component module.
"""
from datetime import datetime
from typing import Any

from duck.meta import Meta
from duck.html.components import (
    Component,
    InnerComponent,
    to_component,
)
from duck.html.components.theme import Theme
from duck.html.components.duck import MadeWithDuck
from duck.html.components.container import GridContainer, FlexContainer
from duck.html.components.style import Style
from duck.html.components.link import Link
from duck.html.components.span import Span
from duck.html.components.heading import Heading


# Duck project links used by the "made with Duck" badge
GITHUB_URL = "https://github.com/duckframework/duck"
DUCK_URL = "https://duckframework.com"

# Default inline svg icons, keyed by platform name, used when a social
# link entry does not supply its own icon
DEFAULT_SOCIAL_ICONS = {
    "twitter": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">'
        '<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1'
        'A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5'
        'a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"/></svg>'
    ),
    "instagram": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" '
        'stroke="currentColor" stroke-width="1.8">'
        '<rect x="2" y="2" width="20" height="20" rx="5"/>'
        '<circle cx="12" cy="12" r="4"/>'
        '<circle cx="17.5" cy="6.5" r="1" fill="currentColor" stroke="none"/>'
        '</svg>'
    ),
    "facebook": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">'
        '<path d="M13 22v-9h3l.5-4H13V6.5A1.5 1.5 0 0 1 14.5 5H17V1h-3'
        'a5 5 0 0 0-5 5v3H6v4h3v9z"/></svg>'
    ),
    "whatsapp": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">'
        '<g transform="translate(-1, 0.6)">'
        '<path d="M12 2a10 10 0 0 0-8.6 15.1L2 22l5.1-1.3A10 10 0 1 0 12 2z'
        'm0 18.2a8.2 8.2 0 0 1-4.2-1.1l-.3-.2-3 .8.8-2.9-.2-.3A8.2 8.2 0 1 1 12 20.2z'
        'm4.5-6.1c-.2-.1-1.5-.7-1.7-.8s-.4-.1-.6.1-.6.8-.8 1-.3.2-.6.1'
        'a6.6 6.6 0 0 1-2-1.2 7.4 7.4 0 0 1-1.4-1.7c-.1-.2 0-.4.1-.5s.3-.3.4-.5'
        'a.5.5 0 0 0 .1-.5c-.1-.1-.6-1.4-.8-1.9s-.4-.4-.6-.4h-.5a1 1 0 0 0-.7.3'
        'A3 3 0 0 0 6.5 9c0 1.6 1.2 3.2 1.4 3.4s2.3 3.5 5.6 4.9c.8.3 1.4.5 1.9.7'
        'a4.5 4.5 0 0 0 2 .1c.6-.1 1.5-.6 1.7-1.2s.2-1.1.2-1.2-.2-.2-.4-.3z"/>'
        '</g></svg>'
    ),
    "github": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">'
        '<path d="M12 1.5a10.5 10.5 0 0 0-3.32 20.47c.53.1.72-.23.72-.5v-1.96'
        'c-2.93.64-3.55-1.24-3.55-1.24-.48-1.22-1.17-1.55-1.17-1.55-.96-.66.07-.64'
        '.07-.64 1.06.07 1.62 1.09 1.62 1.09.94 1.61 2.46 1.15 3.06.87.1-.68.37'
        '-1.15.67-1.41-2.34-.27-4.8-1.17-4.8-5.21 0-1.15.41-2.09 1.09-2.83'
        '-.11-.27-.47-1.35.1-2.81 0 0 .89-.29 2.9 1.08a10 10 0 0 1 5.28 0'
        'c2.01-1.37 2.9-1.08 2.9-1.08.57 1.46.21 2.54.1 2.81.68.74 1.09 1.68 1.09'
        ' 2.83 0 4.05-2.47 4.94-4.82 5.2.38.33.71.97.71 1.96v2.9c0 .27.19.61.73.5'
        'A10.5 10.5 0 0 0 12 1.5z"/></svg>'
    ),
    "youtube": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">'
        '<path d="M22.5 7.2a3 3 0 0 0-2.1-2.1C18.6 4.5 12 4.5 12 4.5s-6.6 0-8.4.6'
        'a3 3 0 0 0-2.1 2.1A31 31 0 0 0 1 12a31 31 0 0 0 .5 4.8 3 3 0 0 0 2.1 2.1'
        'c1.8.6 8.4.6 8.4.6s6.6 0 8.4-.6a3 3 0 0 0 2.1-2.1A31 31 0 0 0 23 12'
        'a31 31 0 0 0-.5-4.8zM9.8 15.3V8.7l6 3.3-6 3.3z"/></svg>'
    ),
    "email": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" '
        'stroke="currentColor" stroke-width="1.8">'
        '<rect x="2" y="4" width="20" height="16" rx="2"/>'
        '<path d="M2 6l10 7 10-7"/></svg>'
    ),
    "patreon": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">'
        '<circle cx="15" cy="9.5" r="6.5"/>'
        '<rect x="2" y="2" width="3.5" height="20"/></svg>'
    ),
    "tiktok": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">'
        '<path d="M16.5 2h-3v13.7a3.2 3.2 0 1 1-2.3-3.07V9.5a6.3 6.3 0 1 0 5.3'
        ' 6.2V8.4a8.2 8.2 0 0 0 4.8 1.5V6.8a5 5 0 0 1-4.8-4.8z"/></svg>'
    ),
    "linkedin": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">'
        '<path d="M4.98 3.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5zM3 9h4v12H3z'
        'M9 9h3.8v1.7h.05c.53-1 1.83-2.06 3.77-2.06 4.03 0 4.78 2.65 4.78 6.1'
        'V21h-4v-5.6c0-1.34-.02-3.06-1.87-3.06-1.87 0-2.16 1.46-2.16 2.96V21H9z"/>'
        '</svg>'
    ),
    "discord": (
        '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">'
        '<path d="M20 4.5A18 18 0 0 0 15.6 3l-.35.7a15 15 0 0 1 3.65 1.4'
        'A16.6 16.6 0 0 0 12 3.6a16.6 16.6 0 0 0-6.9 1.5A15 15 0 0 1 8.75 3.7'
        'L8.4 3A18 18 0 0 0 4 4.5C1.6 8 1 11.4 1.2 14.7a18 18 0 0 0 5.4 2.7'
        'l.75-1.2a11.6 11.6 0 0 1-1.85-.9c.16-.1.3-.23.45-.34a12.8 12.8 0 0 0'
        '10.1 0c.15.11.29.24.45.34a11.6 11.6 0 0 1-1.85.9l.75 1.2a18 18 0 0 0'
        '5.4-2.7c.24-3.9-.7-7.3-2.8-10.2zM8.7 13.1c-.85 0-1.55-.78-1.55-1.75'
        's.68-1.75 1.55-1.75 1.57.79 1.55 1.75c0 .97-.68 1.75-1.55 1.75z'
        'm6.6 0c-.85 0-1.55-.78-1.55-1.75s.68-1.75 1.55-1.75 1.57.79 1.55 1.75'
        'c0 .97-.67 1.75-1.55 1.75z"/></svg>'
    ),
}


[docs] class FooterHeading(Heading): """ Small-caps section heading used inside a footer block. Args: heading_color (str): Optional color override for the heading text. """
[docs] def on_create(self) -> None: super().on_create() # Base heading styling self.klass = "footer-heading" self.style.update({ "font-size": "0.95rem", "letter-spacing": "0.04em", "text-transform": "uppercase", "margin-bottom": "6px", "color": self.kwargs.get("heading_color") or Theme.current.text_color, })
[docs] class FooterBlock(FlexContainer): """ A single footer column: a heading plus a list of link/text elements. Args: heading (str): The heading for the footer block. elements (list[str | Component]): Block elements, either raw html strings (e.g. produced by a `{% Link %}` template tag) or already-built Component instances. heading_color (str): Optional override for the heading color. Notes: A footer may render several blocks side by side, each with its own heading and items. """
[docs] def on_create(self) -> None: super().on_create() # Base column layout self.klass = "footer-block" self.style.update({ "flex-direction": "column", "gap": "10px", "min-width": "100px", "justify-self": "start", }) # Build the heading and items together, in construction order self.add_children(self.build_children())
[docs] def build_children(self) -> list[Component]: """ Builds the heading (if any) followed by the block's elements. Returns: List of Component instances ready to be added as children. """ children = [] # Optional block heading heading = self.kwargs.get("heading") if heading: children.append( FooterHeading( type="h3", text=heading, heading_color=self.kwargs.get("heading_color"), ) ) # Block items, converting raw html where needed for element in self.kwargs.get("elements", []): if isinstance(element, Component): children.append(element) else: children.append(to_component(element, tag="span")) return children
[docs] class FooterItems(GridContainer): """ Main container laying out footer blocks in a responsive grid. Args: footer_items (dict[str, list[str | Component]]): Mapping of footer block headings to their list of elements. """
[docs] def on_create(self) -> None: super().on_create() # Base grid layout self.id = self.kwargs.get("id", "footer-items") self.klass = "footer-items" self.style.update({ "gap": "32px", "grid-template-columns": "repeat(auto-fit, minmax(100px, max-content))", "justify-content": "space-around", "align-content": "start", }) # Build one block per configured heading self.add_children(self.build_blocks())
[docs] def build_blocks(self) -> list[FooterBlock]: """ Builds one FooterBlock per configured heading. Returns: List of FooterBlock components. """ blocks = [] for heading, elements in self.kwargs.get("footer_items", {}).items(): block_id = f"footer-block-{heading.lower().replace(' ', '-')}" blocks.append( FooterBlock(id=block_id, heading=heading, elements=elements) ) return blocks
[docs] class PlainFooter(InnerComponent): """ Just plain simple footer without anything. """
[docs] def get_element(self): return "footer"