{# Shared stack card macro — visual parity with marketplace.html .mp-card. Vertical tile (mirrors mp-card pixel-for-pixel): ┌──────────────────────────────────────┐ │ │ │ SA │ ← 120px photo banner, │ (26px bold initials) │ gradient bg, centered │ │ ├──────────────────────────────────────┤ │ Sales bundle │ ← title (15px bold, clamp 2) │ 12 tables │ ← meta (11px secondary) │ Orders + line items … │ ← description (clamp 2) │ [keboola] [bigquery] │ ← optional tags ├──────────────────────────────────────┤ │ Open → [+ Add] │ ← footer └──────────────────────────────────────┘ The 120px coloured banner with 2-letter initials replaces the old tiny inline icon-box. This matches marketplace.html .mp-card .photo exactly so /catalog, /memory and /marketplace read as one product. Initials default to the first letter of the first two whitespace- separated words ("Sales bundle" → "SB"); single-word names fall back to name[:2]. When `entry.icon` is provided AND is a single emoji glyph the admin set explicitly, we render it instead — at 30px so it doesn't dwarf the rest of the card. States (dual-encoded: color border + text badge): .stack-card.is-required → amber border + "Required" badge top-right .stack-card.is-in-stack → amber border + "In stack" badge top-right default → neutral border + "+ Add to stack" button `entry` is a dict-like with keys: id, name, description, icon, color, requirement ("available"|"required"), in_stack (bool), meta (str), meta_html (str — server-built, trusted, wins over `meta` for inline CTAs), tags (list[str]), drilldown_url (str), footer_left (str). #} {% macro card(entry) %} {% set _cls_required = ' is-required' if entry.requirement == 'required' else '' %} {% set _cls_instack = ' is-in-stack' if entry.in_stack else '' %} {% set _name = (entry.name or '?')|string %} {% set _name_parts = _name.split() %} {% if _name_parts|length >= 2 %} {% set _initials = (_name_parts[0][0] ~ _name_parts[1][0])|upper %} {% else %} {% set _initials = _name[:2]|upper %} {% endif %} {# Per design review (2 independent assessments): emoji glyphs on cards feel childish in a B2B context. Always render the 2-letter initials — matches marketplace.html PL/SK/AG fallback exactly. The admin's `entry.icon` is preserved on disk (admin/* edit screens still show it) but not rendered on cards. Future polish: swap initials for Lucide SVG icons keyed by `entry.icon`. #} {% set _has_glyph = False %} {# v51 status pill — only the non-default values surface as a visible pill. 'prod' is the assumed-good default and would just be noise on every card. Map drives both the visible label and a per-status CSS class for tint. #} {% set _status = entry.status|default('prod') %} {% set _status_meta = { 'poc': {'label': 'POC', 'cls': 'stack-card__status-pill--poc'}, 'coming-soon': {'label': 'Coming soon', 'cls': 'stack-card__status-pill--coming'}, 'draft': {'label': 'Draft', 'cls': 'stack-card__status-pill--draft'} }.get(_status) %}
{% if entry.requirement == 'required' %}
Required
{% elif entry.in_stack %}
In stack
{% endif %} {# v56 derived badges (curated / new). Render top-left of the card when present; multiple stack horizontally. Class hooks ``data-badge=""`` let the visual audit test pin them. #} {% if entry.badges %}
{% for b in entry.badges %} {{ b|capitalize }} {% endfor %}
{% endif %} {# Photo banner — 120px tall. - If `entry.cover_image_url` is set (v50 admin-uploaded cover), render it as an filling the banner. Mirrors marketplace.html's cover_photo path (templates/marketplace.html:702-707) so /catalog and /memory cards visually match the marketplace cards. - If the 404s (file deleted, mid-deploy file-server lag, etc.), the `onerror` swap replaces the broken-image icon with the same FLAT-color + initials fallback used when no cover is set at all. - No cover → flat solid color from `entry.color` (was: pastel→pink gradient, user feedback "vošklivé, divné") + 2-letter initials. #} {# v53 cover-corner emoji glyph — when entry.icon is a single emoji (not a Lucide name like "chart-line"), render it semi-transparent in the lower-right of the cover for the "abstract glyph" feel the mockup uses, without shipping a Lucide bundle. Heuristic: short string + first char not an ASCII identifier char ⇒ treat as glyph. Lucide-name strings are silently suppressed (no CDN dep). #} {% set _icon_str = (entry.icon or '')|string %} {% set _icon_first = _icon_str[:1] %} {% set _cover_glyph = _icon_str if ( _icon_str and _icon_str|length <= 4 and _icon_first not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' ) else '' %}
{% if _status_meta %} {{ _status_meta.label }} {% endif %} {% if entry.cover_image_url %} {% elif _has_glyph %}{{ entry.icon }}{% else %}{{ _initials }}{% endif %} {% if _cover_glyph and not entry.cover_image_url %} {% endif %}
{% if entry.category %}
{{ entry.category }}
{% endif %}

{{ entry.name }}

{# `meta_html` (server-built, trusted) wins over `meta` (plain text) so callers can render an inline action link on the meta line — e.g. empty packages that want a "0 tables — assign some →" CTA. #} {% if entry.meta_html %}
{{ entry.meta_html|safe }}
{% elif entry.meta %}
{{ entry.meta }}
{% endif %} {% if entry.description %}
{{ entry.description }}
{% endif %} {% if entry.tags %}
{% for tag in entry.tags %}{{ tag }}{% endfor %}
{% endif %}
{# v56 owner chip — small attribution line below tags. Only rendered when the package has an owner; class hook `data-card-owner` pins the contract for the visual-audit test. #} {% if entry.owner_name %}
Owned by {{ entry.owner_name }}{% if entry.owner_team %} · {{ entry.owner_team }}{% endif %}
{% endif %}
{% endmacro %}