{# macros/display.html - Display and content component macros for APEP. Import with: {% from "apep/macros/display.html" import image, logo_scroller, text_block, block_text, faq, faq_list, image_gallery, sidescroller, review_card, review_gallery %} Macros: image(src, alt, url, on_click, protected, width, height, downloadable) logo_scroller(logos, speed, logo_height, width, height) text_block(text, min_width, max_width, min_height, max_height, scroll, variant) block_text(title, text, variant) faq(question, answer, open) faq_list(items, open_all) image_gallery(images, downloadable) sidescroller(items, overlay, protected) review_card(text, author, role, rating, avatar, logo, date, expandable, expand_text, collapse_text) review_gallery(reviews, expand_text, collapse_text) Author: sora7672 #} {% from "apep/macros/helper.html" import alert %} {# Renders an image or inline SVG with optional link, click handler, drag protection overlay, and a download button shown on hover. src accepts a file path/URL, a raw string, or a raw tag string. url and on_click are mutually exclusive - on_click takes priority when both are set. The protected flag adds an overlay div that blocks drag on the img or svg element but does NOT prevent right-click context menus on the page level. When url is set without protected and without on_click, and src is not an SVG, the image renders as an tag. In all other cases it renders as a
. For SVG input: loading and draggable are ignored. width/height are applied directly to the svg element via inline style. downloadable saves the SVG as a .svg file. If alt is provided with an SVG that already contains aria-label, a warning is shown and the existing aria-label is overwritten. @param src {string} - image path/URL, raw string, or raw tag string @param alt {string} - alt text for img, or aria-label for svg. Default "". @param url {string} - opens in a new tab on click. Without protected: renders the image as an tag (img only). With protected: overlay handles the click and shows a link hint. Optional. @param on_click {string} - name of a global JS function called on click. Works with or without protected. Takes priority over url. Optional. @param protected {bool} - adds an overlay div to block drag. Default false. @param width {string} - CSS width value applied to the wrapper and img/svg. Optional. @param height {string} - CSS height value applied to the img/svg. Optional. @param downloadable {bool} - shows a download icon button on hover. Default false. SVG input saves as .svg file. External URLs trigger a confirm dialog. #} {% macro image(src, alt="", url=None, on_click=None, protected=True, width=None, height=None, downloadable=False, same_origin=False) %} {% if not src %} {{ alert("image: src is required", "Pass a valid image path, svg string, or img tag as the first argument.") }} {% else %} {# Detect input type #} {% set _is_svg = src.strip().lower().startswith(" {% else %}
{% endif %} {% if _is_svg %} {# Inject width/height directly into svg via style attribute #} {% if width or height %} {% set _svg_style = "" %} {% if width %}{% set _svg_style = _svg_style ~ "width:" ~ width ~ ";" %}{% endif %} {% if height %}{% set _svg_style = _svg_style ~ "height:" ~ height ~ ";" %}{% endif %} {{ src | replace("
{% endif %} {% if downloadable %} {% if _is_svg %} {% else %} {% endif %} {% endif %} {% if _linked %}
{% else %}
{% endif %} {% endif %} {% endif %} {% endmacro %} {# Renders an infinitely scrolling horizontal strip of logos with prev/next arrow buttons. The scroll speed and logo height are controlled via CSS custom properties set inline. Each logo's src accepts a file path/URL, a raw string, or a raw tag string. Logos with a url render as anchor tags. Invalid or unresolvable src values are skipped. Maximum 50 logos per instance. @param logos {list} - list of {src, alt, url} dicts. src accepts a path/URL, string, or tag string. alt is used when src is a path/URL. Ignored for svg/img tags. url makes the logo a link opening in a new tab. Optional per logo. @param speed {int} - scroll animation duration in seconds. Lower = faster. Default 30. @param logo_height {int} - logo height in px applied via CSS custom property. Default 40. @param width {string} - CSS width of the scroller strip. Default "100%". Optional. @param height {string} - CSS height of the outer container. Optional. #} {% macro logo_scroller(logos, speed=30, logo_height=40, width=None, height=None) %} {% if not logos %} {{ alert("logo_scroller: logos is required", "Pass a non-empty list of {src, alt, url} dicts.") }} {% elif logos | length > 50 %} {{ alert("logo_scroller: too many logos", "Maximum is 50 logos per scroller instance.") }} {% else %}
{% for logo in logos %} {% set _resolved = logo.src | helper_to_image %} {% if _resolved %}
{% if logo.url is defined and logo.url %} {{ _resolved | safe }} {% else %} {{ _resolved | safe }} {% endif %}
{% endif %} {% endfor %}
{% endif %} {% endmacro %} {# Renders a styled content block around arbitrary HTML text. When scroll is set, the block gets a wrapper div with scroll hint overlays that fade in/out based on scroll position, managed by text-block.js. Width constraints behave differently per scroll direction: for vertical scroll they apply to the outer wrapper; for horizontal scroll they apply to the inner block. @param text {string} - HTML content to render inside the block @param min_width {string} - CSS min-width, e.g. "200px". Optional. @param max_width {string} - CSS max-width, e.g. "600px". Optional. @param min_height {string} - CSS min-height, e.g. "100px". Optional. @param max_height {string} - CSS max-height, e.g. "300px". Optional. @param scroll {string} - "vertical" | "horizontal" | None, default None. vertical - overflow-y auto, content wraps at block width. horizontal - overflow-x auto, content does not wrap. None - no scroll, fluid sizing. @param variant {string} - "default" | "plain", default "default" #} {% macro text_block(text, min_width=None, max_width=None, min_height=None, max_height=None, scroll=None, variant="default") %} {% if not text %} {{ alert("text_block: text is required", "Pass a non-empty string.") }} {% elif variant not in ["default", "plain"] %} {{ alert("text_block: invalid variant \"" ~ variant ~ "\"", "Allowed: \"default\", \"plain\".") }} {% elif scroll is not none and scroll not in ["vertical", "horizontal"] %} {{ alert("text_block: invalid scroll \"" ~ scroll ~ "\"", "Allowed: \"vertical\", \"horizontal\", or omit for no scroll.") }} {% else %} {% set _scroll_class = "" %} {% if scroll == "vertical" %}{% set _scroll_class = "text-block-scroll-v" %}{% endif %} {% if scroll == "horizontal" %}{% set _scroll_class = "text-block-scroll-h" %}{% endif %} {% set _base_class = "text-block" %} {% if variant == "plain" %}{% set _base_class = _base_class ~ " text-block-plain" %}{% endif %} {% if _scroll_class %}{% set _base_class = _base_class ~ " " ~ _scroll_class %}{% endif %} {% if scroll %} {% set _wrapper_parts = [] %} {% if scroll == "vertical" %} {% if min_width %}{% set _ = _wrapper_parts.append("min-width: " ~ min_width) %}{% endif %} {% if max_width %}{% set _ = _wrapper_parts.append("max-width: " ~ max_width) %}{% endif %} {% endif %} {% set _wrapper_style = _wrapper_parts | join(";") %} {% if scroll == "horizontal" %} {% set _block_parts = ["box-sizing: border-box"] %} {% if min_width %}{% set _ = _block_parts.append("min-width: " ~ min_width) %}{% endif %} {% if max_width %}{% set _ = _block_parts.append("max-width: " ~ max_width) %}{% endif %} {% else %} {% set _block_parts = ["width: 100%", "box-sizing: border-box"] %} {% endif %} {% if min_height %}{% set _ = _block_parts.append("min-height: " ~ min_height) %}{% endif %} {% if max_height %}{% set _ = _block_parts.append("max-height: " ~ max_height) %}{% endif %} {% set _block_style = _block_parts | join(";") %}
{{ text }}
{% else %} {% set _style_parts = [] %} {% if min_width %}{% set _ = _style_parts.append("min-width: " ~ min_width) %}{% endif %} {% if max_width %}{% set _ = _style_parts.append("max-width: " ~ max_width) %}{% endif %} {% if min_height %}{% set _ = _style_parts.append("min-height: " ~ min_height) %}{% endif %} {% if max_height %}{% set _ = _style_parts.append("max-height: " ~ max_height) %}{% endif %} {% set _style = _style_parts | join(";") %}
{{ text }}
{% endif %} {% endif %} {% endmacro %} {# Renders a titled content block with a heading and a body section accepting HTML. @param title {string} - heading text rendered as

@param text {string} - body content, HTML allowed @param variant {string} - "default" | "centered" | "highlight", default "default" #} {% macro block_text(title, text, variant="default") %} {% if not title %} {{ alert("block_text: title is required", "Pass a non-empty string as the first argument.") }} {% elif not text %} {{ alert("block_text: text is required", "Pass a non-empty string as the second argument.") }} {% elif variant not in ["default", "centered", "highlight"] %} {{ alert("block_text: invalid variant \"" ~ variant ~ "\"", "Allowed values: \"default\", \"centered\", \"highlight\".") }} {% else %}

{{ title }}

{{ text }}
{% endif %} {% endmacro %} {# Renders a single collapsible FAQ item with an accessible button toggle. Open/close state is toggled by faq.js. aria-expanded is kept in sync. @param question {string} - question text displayed in the toggle button @param answer {string} - answer content, HTML allowed, shown when expanded @param open {bool} - start in the expanded state, default false #} {% macro faq(question, answer, open=False) %} {% if not question %} {{ alert("faq: question is required", "Pass a non-empty string as the first argument.") }} {% elif not answer %} {{ alert("faq: answer is required", "Pass a non-empty string as the second argument.") }} {% else %}
{{ answer }}
{% endif %} {% endmacro %} {# Renders a list of collapsible FAQ items with a global open/close all toggle button. The open_all param overrides individual item open states - when true, all items start expanded regardless of their own open field. @param items {list} - list of {question, answer, open} dicts. open defaults to false when omitted. @param open_all {bool} - start with all items expanded. Default false. #} {% macro faq_list(items, open_all=False) %} {% if not items %} {{ alert("faq_list: items is required", "Pass a non-empty list of {question, answer} dicts.") }} {% else %}
{% for item in items %} {% set is_open = open_all or (item.open is defined and item.open) %}
{{ item.answer }}
{% endfor %}
{% endif %} {% endmacro %} {# Renders a responsive image grid with a fullscreen lightbox overlay. Clicking any image opens it via the global apep-overlay system. src accepts a file path/URL, a raw tag string, or a raw string. For SVG input: rendered inline as thumbnail and shown as SVG in overlay. For img/path input: rendered as a normal tag. @param images {list} - list of {src, alt, caption} dicts. src accepts a path/URL, tag string, or string. alt is used when src is a path/URL. caption is shown below the image in the overlay. Optional. @param downloadable {bool} - shows a download button on hover and in overlay. Default false. SVG downloads save as .svg. External URLs show a confirm dialog. #} {% macro image_gallery(images, downloadable=False) %} {% if not images %} {{ alert("image_gallery: images is required", "Pass a non-empty list of {src, alt, caption} dicts.") }} {% else %} {% endfor %} {% endif %} {% endmacro %} {# Renders a horizontally scrollable item strip with prev/next arrow buttons. Each item is rendered via the image macro, so all image options apply per item. url and on_click are mutually exclusive per item - on_click takes priority. @param items {list} - list of dicts, one per item. Supported keys per item: src {string} - image path/URL, , or tag. Required if no content. content {string} - raw HTML string. Used when src is not set. alt {string} - alt text. Optional. label {string} - text shown below the image. Optional. caption {string} - caption shown in overlay. Optional. url {string} - opens in new tab on click. Optional. on_click {string} - global JS function name called on click. Optional. same_origin {bool} - when true, url opens in same tab. Optional. overlay {bool} - when true, click opens image fullscreen. Optional. protected {bool} - overrides image macro default per item. Optional. downloadable {bool} - shows download button on this item. Optional. #} {% macro sidescroller(items) %} {% if not items %} {{ alert("sidescroller: items is required", "Pass a non-empty list of item dicts.") }} {% else %}
{% for item in items %} {% set _src = item.src if item.src is defined else "" %} {% set _alt = item.alt if item.alt is defined else "" %} {% set _url = item.url if item.url is defined else None %} {% set _onclick = item.on_click if item.on_click is defined else None %} {% set _overlay = item.overlay if item.overlay is defined else False %} {% set _downloadable = item.downloadable if item.downloadable is defined else False %} {% set _same_origin = item.same_origin if item.same_origin is defined else False %} {# on_click takes priority over url - warn if both are set #} {% if _url and _onclick %} {{ alert("sidescroller: url and on_click are mutually exclusive", "on_click takes priority. Remove url or on_click from item: \"" ~ (_alt or _src) ~ "\".") }} {% endif %} {% set _clickable = _url or _onclick or _overlay %}
{% if _src %} {{ image(_src, alt=_alt, url=_url if not _onclick and not _overlay else None, on_click=_onclick, same_origin=_same_origin, downloadable=_downloadable, protected=item.protected if item.protected is defined else True ) }} {% elif item.content is defined %} {{ item.content | safe }} {% endif %}
{% if item.label is defined and item.label %} {{ item.label }} {% endif %}
{% endfor %}
{% endif %} {% endmacro %} {# Renders a single review card with optional star rating, logo, avatar, author meta, and an expand/collapse toggle for long review texts. logo and avatar accept a file path/URL, a raw string, or a raw tag string. When used inside review_gallery, expand_text and collapse_text are passed down automatically. Only override them here when a standalone card needs different labels. rating must be an integer between 1 and 5 when provided. @param text {string} - review text content, displayed as a blockquote @param author {string} - reviewer name. Optional. @param role {string} - reviewer role or position. Optional. @param rating {int} - star rating, 1-5. Renders filled and empty stars. Optional. @param avatar {string} - image path/URL, , or tag for the reviewer avatar. Optional. @param logo {string} - image path/URL, , or tag for a company or brand logo. Optional. @param date {string} - display date string. Optional. @param expandable {bool} - when true, fades the review text and shows an expand button if content overflows. Managed by review-cards.js. Default false. @param expand_text {string} - label for the expand button. Default "Expand". @param collapse_text {string} - label for the collapse button. Default "Collapse". #} {% macro review_card(text, author=None, role=None, rating=None, avatar=None, logo=None, date=None, expandable=False, expand_text="Expand", collapse_text="Collapse") %} {% if not text %} {{ alert("review_card: text is required", "Pass a non-empty string as the first argument.") }} {% elif rating is not none and rating not in [1, 2, 3, 4, 5] %} {{ alert("review_card: invalid rating \"" ~ rating ~ "\"", "Allowed values: 1, 2, 3, 4, 5.") }} {% else %} {# Resolve logo and avatar to proper html #} {% if logo %}{% set _logo_html = logo | helper_to_image %}{% else %}{% set _logo_html = None %}{% endif %} {% if avatar %}{% set _avatar_html = avatar | helper_to_image %}{% else %}{% set _avatar_html = None %}{% endif %}
{% if rating %}
{% for i in range(1, 6) %} {% endfor %}
{% endif %} {% if _logo_html %} {% endif %}
{{ text }}
{% if expandable %} {% endif %} {% if author or _avatar_html or role or date %} {% endif %}
{% endif %} {% endmacro %} {# Renders a paginated review carousel with prev/next arrows and dot indicators. Wraps review_card calls and passes expand_text and collapse_text down to every card. Per-card overrides of expand_text and collapse_text are not supported inside this macro. @param reviews {list} - list of review dicts matching the review_card params. Required fields per item: text. Optional fields: author, role, rating, avatar, logo, date, expandable. @param expand_text {string} - expand button label applied to all cards. Default "Expand". @param collapse_text {string} - collapse button label applied to all cards. Default "Collapse". #} {% macro review_gallery(reviews, expand_text="Expand", collapse_text="Collapse") %} {% if not reviews %} {{ alert("review_gallery: reviews is required", "Pass a non-empty list of review dicts.") }} {% else %} {% endif %} {% endmacro %}