feat: extract shared infrastructure from shared_lib
Phase 1-3 of decoupling plan: - Shared DB, models, infrastructure, browser, config, utils - Event infrastructure (domain_events outbox, bus, processor) - Structured logging - Generic container concept (container_type/container_id) - Alembic migrations for all schema changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
21
browser/templates/macros/admin_nav.html
Normal file
21
browser/templates/macros/admin_nav.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{#
|
||||
Shared admin navigation macro
|
||||
Use this instead of duplicate _nav.html files
|
||||
#}
|
||||
|
||||
{% macro admin_nav_item(href, icon='cog', label='', select_colours='', aclass=styles.nav_button) %}
|
||||
{% import 'macros/links.html' as links %}
|
||||
{% call links.link(href, hx_select_search, select_colours, True, aclass=aclass) %}
|
||||
<i class="fa fa-{{ icon }}" aria-hidden="true"></i>
|
||||
{{ label }}
|
||||
{% endcall %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro placeholder_nav() %}
|
||||
{# Placeholder for admin sections without specific nav items #}
|
||||
<div class="relative nav-group">
|
||||
<span class="block px-3 py-2 text-stone-400 text-sm italic">
|
||||
Admin options
|
||||
</span>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
7
browser/templates/macros/date.html
Normal file
7
browser/templates/macros/date.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% macro dt(d) -%}
|
||||
{{ d.astimezone().strftime('%-d %b %Y, %H:%M') if d.tzinfo else d.strftime('%-d %b %Y, %H:%M') }}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro t(d) -%}
|
||||
{{ d.astimezone().strftime('%H:%M') if d.tzinfo else d.strftime('%H:%M') }}
|
||||
{%- endmacro %}
|
||||
117
browser/templates/macros/filters.html
Normal file
117
browser/templates/macros/filters.html
Normal file
@@ -0,0 +1,117 @@
|
||||
{#
|
||||
Unified filter macros for browse/shop pages
|
||||
Consolidates duplicate mobile/desktop filter components
|
||||
#}
|
||||
|
||||
{% macro filter_item(href, is_on, title, icon_html, count=none, variant='desktop') %}
|
||||
{#
|
||||
Generic filter item (works for labels, stickers, etc.)
|
||||
variant: 'desktop' or 'mobile'
|
||||
#}
|
||||
{% set base_class = "flex flex-col items-center justify-center" %}
|
||||
{% if variant == 'mobile' %}
|
||||
{% set item_class = base_class ~ " p-1 rounded hover:bg-stone-50" %}
|
||||
{% set count_class = "text-[10px] text-stone-500 mt-1 leading-none tabular-nums" if count != 0 else "text-md text-red-500 font-bold mt-1 leading-none tabular-nums" %}
|
||||
{% else %}
|
||||
{% set item_class = base_class ~ " py-2 w-full h-full" %}
|
||||
{% set count_class = "text-xs text-stone-500 leading-none justify-self-end tabular-nums" if count != 0 else "text-md text-red-500 font-bold leading-none justify-self-end tabular-nums" %}
|
||||
{% endif %}
|
||||
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ title }}"
|
||||
aria-label="{{ title }}"
|
||||
class="{{ item_class }}"
|
||||
>
|
||||
{{ icon_html | safe }}
|
||||
{% if count is not none %}
|
||||
<span class="{{ count_class }}">{{ count }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro labels_list(labels, selected_labels, current_local_href, variant='desktop') %}
|
||||
{#
|
||||
Unified labels filter list
|
||||
variant: 'desktop' or 'mobile'
|
||||
#}
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
|
||||
{% if variant == 'mobile' %}
|
||||
<nav aria-label="labels" class="px-4 pb-3">
|
||||
<ul class="flex w-full items-start justify-center gap-3 overflow-x-auto overflow-y-visible pr-1 no-scrollbar">
|
||||
{% else %}
|
||||
<ul id="labels-details-desktop" class="flex justify-center p-0 m-0 gap-2" >
|
||||
{% endif %}
|
||||
|
||||
{% for s in labels %}
|
||||
{% set is_on = (selected_labels and (s.name|lower in selected_labels)) %}
|
||||
{% set qs = {"remove_label": s.name, "page": None}|qs if is_on else {"add_label": s.name, "page": None}|qs %}
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
|
||||
<li class="{{ 'list-none shrink-0' if variant == 'mobile' else '' }}">
|
||||
{{ filter_item(
|
||||
href, is_on, s.name,
|
||||
stick.sticker(asset_url('nav-labels/' ~ s.name ~ '.svg'), s.name, is_on),
|
||||
s.count, variant
|
||||
) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
{% if variant == 'mobile' %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro stickers_list(stickers, selected_stickers, current_local_href, variant='desktop') %}
|
||||
{#
|
||||
Unified stickers filter list
|
||||
variant: 'desktop' or 'mobile'
|
||||
#}
|
||||
{% import 'macros/stickers.html' as stick %}
|
||||
|
||||
{% if variant == 'mobile' %}
|
||||
<nav aria-label="stickers" class="px-4 pb-3">
|
||||
<ul class="flex w-full items-start justify-center gap-3 overflow-x-auto overflow-y-visible pr-1 no-scrollbar">
|
||||
{% else %}
|
||||
<ul id="stickers-details-desktop"
|
||||
class="flex flex-wrap justify-center w-full p-0 m-0 border bg-white shadow-sm rounded-xl gap-1 [&>li]:list-none [&>li]:basis-[20%] [&>li]:max-w-[20%] [&>li]:grow-0"
|
||||
>
|
||||
{% endif %}
|
||||
|
||||
{% for s in stickers %}
|
||||
{% set is_on = (selected_stickers and (s.name|lower in selected_stickers)) %}
|
||||
{% set qs = {"remove_sticker": s.name, "page": None}|qs if is_on else {"add_sticker": s.name, "page": None}|qs %}
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
{% set display_name = s.name|capitalize if s.name|lower != 'sugarfree' else 'Sugar' %}
|
||||
|
||||
<li class="{{ 'list-none shrink-0' if variant == 'mobile' else '' }}">
|
||||
{% set icon_html %}
|
||||
<span class="{{ 'text-sm' if variant == 'mobile' else 'text-[11px]' }}">{{ display_name }}</span>
|
||||
{{ stick.sticker(asset_url('stickers/' ~ s.name ~ '.svg'), s.name, is_on) }}
|
||||
{% endset %}
|
||||
{{ filter_item(href, is_on, s.name, icon_html, s.count, variant) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
{% if variant == 'mobile' %}
|
||||
</nav>
|
||||
<style>
|
||||
.no-scrollbar::-webkit-scrollbar { display: none; }
|
||||
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
</style>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
17
browser/templates/macros/glyphs.html
Normal file
17
browser/templates/macros/glyphs.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% macro opener(group=False) %}
|
||||
<svg
|
||||
class="h-4 w-4 transition-transform group-open{{ '/' + group if group else ''}}:rotate-180"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 9l6 6 6-6"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
{% endmacro %}
|
||||
51
browser/templates/macros/layout.html
Normal file
51
browser/templates/macros/layout.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{# templates/macros/layout.html #}
|
||||
|
||||
{% macro details(group = '', _class='') %}
|
||||
<details
|
||||
class="group{{group}} p-2 {{_class}}" data-toggle-group="mobile-panels">
|
||||
{{ caller() }}
|
||||
</details>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro summary(id, _class=None, oob=False) %}
|
||||
<summary>
|
||||
<header class="z-50">
|
||||
<div
|
||||
id="{{id}}"
|
||||
{% if oob %}
|
||||
hx-swap-oob="true"
|
||||
{% endif %}
|
||||
class="{{'flex justify-between items-start gap-2' if not _class else _class}}">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
</header>
|
||||
</summary>
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro filter_summary(id, current_local_href, search, search_count, hx_select, oob=True) %}
|
||||
<summary class="bg-white/90">
|
||||
<div class="flex flex-row items-start">
|
||||
<div>
|
||||
{% include '_types/blog/mobile/_filter/_hamburger.html' %}
|
||||
</div>
|
||||
<div
|
||||
id="{{id}}"
|
||||
class="flex-1 md:hidden grid grid-cols-12 items-center gap-3"
|
||||
>
|
||||
<div class="flex flex-col items-start gap-2">
|
||||
{{ caller() }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% import '_types/browse/mobile/_filter/search.html' as s %}
|
||||
{{ s.search(current_local_href, search, search_count, hx_select) }}
|
||||
</div>
|
||||
</summary>
|
||||
{%- endmacro %}
|
||||
|
||||
|
||||
{% macro menu(id, _class="") %}
|
||||
<div id="{{id}}" hx-swap-oob="outerHTML" class="{{_class}}">
|
||||
{{ caller() }}
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
59
browser/templates/macros/links.html
Normal file
59
browser/templates/macros/links.html
Normal file
@@ -0,0 +1,59 @@
|
||||
|
||||
|
||||
{% macro link(url, select, select_colours='', highlight=True, _class='', aclass='') %}
|
||||
{% set href=url|host%}
|
||||
<div class="relative nav-group {{_class}}">
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{select}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
aria-selected="{{ 'true' if (request.path|host).startswith(href) else 'false' }}"
|
||||
{% if aclass %}
|
||||
class="{{aclass}}"
|
||||
{% elif select_colours %}
|
||||
class="whitespace-normal flex gap-2 px-3 py-2 rounded
|
||||
text-center break-words leading-snug
|
||||
bg-stone-200 text-black
|
||||
{{select_colours if highlight else ''}}
|
||||
"
|
||||
{% else %}
|
||||
class="w-full whitespace-normal flex items-center gap-2 font-bold text-2xl px-3 py-2"
|
||||
{% endif %}
|
||||
>
|
||||
{{ caller() }}
|
||||
</a>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro menu_row(id=False, oob=False) %}
|
||||
<div
|
||||
{% if id %}
|
||||
id="{{id}}"
|
||||
{% endif %}
|
||||
{% if oob %}
|
||||
hx-swap-oob="outerHTML"
|
||||
{% endif %}
|
||||
class="flex flex-col items-center md:flex-row justify-center md:justify-between w-full p-1 bg-{{menu_colour}}-{{(500-(level()*100))|string}}"
|
||||
>
|
||||
{{ caller() }}
|
||||
</div>
|
||||
{{level_up()}}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro desktop_nav() %}
|
||||
<nav class="hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0">
|
||||
{{ caller() }}
|
||||
</nav>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin() %}
|
||||
<i class="fa fa-cog" aria-hidden="true"></i>
|
||||
<div>
|
||||
settings
|
||||
</div>
|
||||
|
||||
{% endmacro %}
|
||||
68
browser/templates/macros/scrolling_menu.html
Normal file
68
browser/templates/macros/scrolling_menu.html
Normal file
@@ -0,0 +1,68 @@
|
||||
{#
|
||||
Scrolling menu macro with arrow navigation
|
||||
|
||||
Creates a horizontally scrollable menu (desktop) or vertically scrollable (mobile)
|
||||
with arrow buttons that appear/hide based on content overflow.
|
||||
|
||||
Parameters:
|
||||
- container_id: Unique ID for the scroll container
|
||||
- items: List of items to iterate over
|
||||
- item_content: Caller block that renders each item (receives 'item' variable)
|
||||
- wrapper_class: Optional additional classes for outer wrapper
|
||||
- container_class: Optional additional classes for scroll container
|
||||
- item_class: Optional additional classes for each item wrapper
|
||||
#}
|
||||
|
||||
{% macro scrolling_menu(container_id, items, wrapper_class='', container_class='', item_class='') %}
|
||||
{% if items %}
|
||||
{# Left scroll arrow - desktop only #}
|
||||
<button
|
||||
class="scrolling-menu-arrow-{{ container_id }} hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
aria-label="Scroll left"
|
||||
_="on click
|
||||
set #{{ container_id }}.scrollLeft to #{{ container_id }}.scrollLeft - 200">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
{# Scrollable container #}
|
||||
<div id="{{ container_id }}"
|
||||
class="overflow-y-auto sm:overflow-x-auto sm:overflow-y-visible scrollbar-hide max-h-[50vh] sm:max-h-none {{ container_class }}"
|
||||
style="scroll-behavior: smooth;"
|
||||
_="on load or scroll
|
||||
-- Show arrows if content overflows (desktop only)
|
||||
if window.innerWidth >= 640 and my.scrollWidth > my.clientWidth
|
||||
remove .hidden from .scrolling-menu-arrow-{{ container_id }}
|
||||
add .flex to .scrolling-menu-arrow-{{ container_id }}
|
||||
else
|
||||
add .hidden to .scrolling-menu-arrow-{{ container_id }}
|
||||
remove .flex from .scrolling-menu-arrow-{{ container_id }}
|
||||
end">
|
||||
<div class="flex flex-col sm:flex-row gap-1 {{ wrapper_class }}">
|
||||
{% for item in items %}
|
||||
<div class="{{ item_class }}">
|
||||
{{ caller(item) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
{# Right scroll arrow - desktop only #}
|
||||
<button
|
||||
class="scrolling-menu-arrow-{{ container_id }} hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
aria-label="Scroll right"
|
||||
_="on click
|
||||
set #{{ container_id }}.scrollLeft to #{{ container_id }}.scrollLeft + 200">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
24
browser/templates/macros/stickers.html
Normal file
24
browser/templates/macros/stickers.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% macro sticker(src, title, enabled, size=40, found=false) -%}
|
||||
|
||||
<span class="relative inline-flex items-center justify-center group"
|
||||
tabindex="0" aria-label="{{ title|capitalize }}">
|
||||
<!-- sticker icon -->
|
||||
<img
|
||||
src="{{ src }}"
|
||||
width="{{size}}" height="{{size}}"
|
||||
alt="{{ title|capitalize }}"
|
||||
title="{{ title|capitalize }}"
|
||||
class="{% if found %}border-2 border-yellow-200 bg-yellow-300{% endif %} {%if enabled %} opacity-100 {% else %} opacity-40 saturate-0 {% endif %}"
|
||||
/>
|
||||
|
||||
<!-- tooltip -->
|
||||
<span role="tooltip"
|
||||
class="pointer-events-none absolute z-50 bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover/tt:block group-focus-visible/tt:block whitespace-nowrap rounded-md bg-stone-900 text-white text-xs px-2 py-1 shadow-lg">
|
||||
{{ title|capitalize if title|lower != 'sugarfree' else 'Sugar' }}
|
||||
<!-- little arrow -->
|
||||
<span class="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-stone-900"></span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{%- endmacro -%}
|
||||
|
||||
10
browser/templates/macros/title.html
Normal file
10
browser/templates/macros/title.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% macro title(_class='') %}
|
||||
<a
|
||||
href="{{ coop_url('/') }}"
|
||||
class="{{_class}}"
|
||||
>
|
||||
<h1>
|
||||
{{ site().title }}
|
||||
</h1>
|
||||
</a>
|
||||
{% endmacro %}
|
||||
Reference in New Issue
Block a user