Monorepo: consolidate 7 repos into one
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m5s

Combines shared, blog, market, cart, events, federation, and account
into a single repository. Eliminates submodule sync, sibling model
copying at build time, and per-app CI orchestration.

Changes:
- Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs
- Remove stale sibling model copies from each app
- Update all 6 Dockerfiles for monorepo build context (root = .)
- Add build directives to docker-compose.yml
- Add single .gitea/workflows/ci.yml with change detection
- Add .dockerignore for monorepo build context
- Create __init__.py for federation and account (cross-app imports)
This commit is contained in:
giles
2026-02-24 19:44:17 +00:00
commit f42042ccb7
895 changed files with 61147 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
{% extends oob.oob_extends %}
{# OOB elements for HTMX navigation - all elements that need updating #}
{# Import shared OOB macros #}
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
{% block oobs %}
{% from '_types/root/_n/macros.html' import oob_header with context %}
{{oob_header(
oob.parent_id,
oob.child_id,
oob.header,
)}}
{% from oob.parent_header import header_row with context %}
{{ header_row(oob=True) }}
{% endblock %}
{# Mobile menu - from market/index.html _main_mobile_menu block #}
{% set mobile_nav %}
{% include oob.nav %}
{% endset %}
{{ mobile_menu(mobile_nav) }}
{% block content %}
{% include oob.main %}
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% set href=account_url('/') %}
<a
href="{{ href }}"
class="justify-center cursor-pointer flex flex-row items-center p-3 gap-2 rounded bg-stone-200 text-black {{select_colours}}"
data-close-details
>
<i class="fa-solid fa-user"></i>
<span>{{g.user.email}}</span>
</a>

View File

@@ -0,0 +1,13 @@
<div class="md:hidden bg-stone-200 rounded">
<svg class="h-12 w-12 transition-transform group-open/root:hidden block self-start" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16" />
</svg>
<svg aria-hidden="true" viewBox="0 0 24 24"
class="w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start">
<path d="M6 9l6 6 6-6" fill="currentColor"/>
</svg>
</div>
<!-- Desktop nav -->

View File

@@ -0,0 +1,67 @@
<style>
@media (min-width: 768px) { .js-mobile-sentinel { display:none !important; } }
</style>
<link rel="stylesheet" type="text/css" href="{{asset_url('styles/basics.css')}}">
<link rel="stylesheet" type="text/css" href="{{asset_url('styles/cards.css')}}">
<link rel="stylesheet" type="text/css" href="{{asset_url('styles/blog-content.css')}}">
<script src="https://unpkg.com/htmx.org@2.0.8"></script>
<script src="https://unpkg.com/hyperscript.org@0.9.12"></script>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="{{asset_url('fontawesome/css/all.min.css')}}">
<link rel="stylesheet" href="{{asset_url('fontawesome/css/v4-shims.min.css')}}">
<link href="https://unpkg.com/prismjs/themes/prism.css" rel="stylesheet" />
<script src="https://unpkg.com/prismjs/prism.js"></script>
<script src="https://unpkg.com/prismjs/components/prism-javascript.min.js"></script>
<script src="https://unpkg.com/prismjs/components/prism-python.min.js"></script>
<script src="https://unpkg.com/prismjs/components/prism-bash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
if (matchMedia('(hover: hover) and (pointer: fine)').matches) {
document.documentElement.classList.add('hover-capable');
}
</script>
<script>
document.addEventListener('click', function (e) {
const closeTarget = e.target.closest('[data-close-details]');
if (!closeTarget) return;
const details = closeTarget.closest('details');
if (details) {
details.removeAttribute('open');
}
});
</script>
<style>
/* hide disclosure glyph */
details[data-toggle-group="mobile-panels"] > summary {
list-style: none;
}
details[data-toggle-group="mobile-panels"] > summary::-webkit-details-marker {
display: none;
}
/* Desktop hover/focus dropdowns */
@media (min-width: 768px) {
.nav-group:focus-within .submenu,
.nav-group:hover .submenu { display:block }
}
img { max-width: 100%; height: auto; }
.clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
details.group { overflow: hidden; }
details.group > summary { list-style: none; }
details.group > summary::-webkit-details-marker { display:none; }
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: inline-flex; }
</style>
<style>
.js-wrap.open .js-pop { display:block; }
.js-wrap.open .js-backdrop { display:block; }
</style>

View File

@@ -0,0 +1,13 @@
{% extends '_types/root/index.html' %}
{% from 'macros/glyphs.html' import opener %}
{% from 'macros/title.html' import title with context %}
{% block main_mobile_menu %}
<div class="flex flex-col gap-2 md:hidden z-40">
{% block _main_mobile_menu %}
{% include '_types/root/_nav.html' %}
{% include '_types/root/_nav_panel.html' %}
{% endblock %}
</div>
{% endblock %}

View File

@@ -0,0 +1,35 @@
{% macro header(id=False, oob=False) %}
<div
{% if id %}id="{{id}}"{% endif %}
{% if oob %}hx-swap-oob="outerHTML"{% endif %}
class="w-full"
>
{{ caller() }}
</div>
{% endmacro %}
{% macro oob_header(id, child_id, row_macro) %}
{% call header(id=id, oob=True) %}
{% call header() %}
{% from row_macro import header_row with context %}
{{header_row()}}
<div id="{{child_id}}">
</div>
{% endcall %}
{% endcall %}
{% endmacro %}
{% macro index_row(id, row_macro) %}
{% from '_types/root/_n/macros.html' import header with context %}
{% set _caller = caller %}
{% call header() %}
{% from row_macro import header_row with context %}
{{ header_row() }}
<div id="{{id}}">
{{_caller()}}
</div>
{% endcall %}
{% endmacro %}

View File

@@ -0,0 +1,29 @@
{% set _app_slugs = {
'cart': cart_url('/'),
'market': market_url('/'),
'events': events_url('/'),
'federation': federation_url('/'),
'account': account_url('/'),
} %}
{% set _first_seg = request.path.strip('/').split('/')[0] %}
<div class="flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
id="menu-items-nav-wrapper">
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
{% call(item) scrolling_menu('menu-items-container', menu_items) %}
{% set _href = _app_slugs.get(item.slug, blog_url('/' + item.slug + '/')) %}
<a
href="{{ _href }}"
aria-selected="{{ 'true' if (item.slug == _first_seg or item.slug == app_name) else 'false' }}"
class="{{styles.nav_button_less_pad}}"
>
{% if item.feature_image %}
<img src="{{ item.feature_image }}"
alt="{{ item.label }}"
class="w-8 h-8 rounded-full object-cover flex-shrink-0" />
{% else %}
<div class="w-8 h-8 rounded-full bg-stone-200 flex-shrink-0"></div>
{% endif %}
<span>{{ item.label }}</span>
</a>
{% endcall %}
</div>

View File

@@ -0,0 +1,7 @@
{% import 'macros/links.html' as links %}
{% if g.rights.admin %}
<a href="{{ blog_url('/settings/') }}" class="{{styles.nav_button}}">
<i class="fa fa-cog" aria-hidden="true"></i>
</a>
{% endif %}

View File

@@ -0,0 +1,46 @@
{#
Shared mobile menu for both base templates and OOB updates
This macro can be used in two modes:
- oob=true: Outputs full wrapper with hx-swap-oob attribute (for OOB updates)
- oob=false: Outputs just content, assumes wrapper exists (for base templates)
The caller can pass section-specific nav items via section_nav parameter.
#}
{% macro mobile_menu(section_nav='', oob=true) %}
{% if oob %}
<div id="root-menu" hx-swap-oob="outerHTML" class="md:hidden">
{% endif %}
<nav id="nav-panel" {% if oob %}hx-swap-oob="true"{% endif %} class="flex flex-col gap-2 mt-2 px-2 pb-2" role="listbox">
{% if not g.user %}
{% include '_types/root/_sign_in.html' %}
{% endif %}
{% include '_types/root/_nav.html' %}
{# Section-specific mobile nav #}
{% if section_nav %}
{{ section_nav }}
{% else %}
{% include "_types/root/_nav_panel.html"%}
{% endif %}
</nav>
{% if oob %}
</div>
{% endif %}
{% endmacro %}
{% macro oob_mobile_menu() %}
<div id="root-menu" hx-swap-oob="outerHTML" class="md:hidden">
<nav id="nav-panel" class="flex flex-col gap-2 mt-2 px-2 pb-2" role="listbox">
{{caller()}}
</nav>
</div>
{% endmacro %}

View File

@@ -0,0 +1,10 @@
<a
href="{{ account_url('/') }}"
aria-selected="{{ 'true' if '/auth/login' in request.path else 'false' }}"
class="justify-center cursor-pointer flex flex-row items-center p-3 gap-2 rounded bg-stone-200 text-black {{select_colours}}"
data-close-details
>
<i class="fa-solid fa-key"></i>
<span>sign in or register</span>
</a>

View File

@@ -0,0 +1 @@
{{asset_url('errors/403.gif')}}

View File

@@ -0,0 +1 @@
YOU CAN'T DO THAT

View File

@@ -0,0 +1 @@
{{asset_url('errors/404.gif')}}

View File

@@ -0,0 +1 @@
NOT FOUND

View File

@@ -0,0 +1,12 @@
{% extends '_types/root/exceptions/base.html' %}
{% block error_summary %}
<div>
{% include '_types/root/exceptions/' + errnum + '/message.html' %}
</div>
{% endblock %}
{% block error_content %}
<img src="{% include '_types/root/exceptions/' + errnum + '/img.html' %}" width="300px" height="300px"/>
{% endblock %}

View File

@@ -0,0 +1,42 @@
{% extends '_types/root/_index.html' %}
{% block content %}
<div class="flex flex-col items-center justify-center min-h-[50vh] p-8">
<div class="max-w-md w-full bg-white rounded-lg shadow-lg p-6">
<div class="flex items-center justify-center w-12 h-12 mx-auto mb-4 rounded-full bg-red-100">
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
</div>
<h1 class="text-xl font-semibold text-center text-stone-800 mb-4">
Something went wrong
</h1>
{% if messages %}
<div class="space-y-2 mb-6">
{% for message in messages %}
<div class="p-3 bg-red-50 border border-red-200 rounded text-sm text-red-700">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<button
onclick="history.back()"
class="px-4 py-2 border border-stone-300 text-stone-700 rounded hover:bg-stone-50 transition-colors"
>
← Go Back
</button>
<a
href="{{ blog_url('/') }}"
class="px-4 py-2 bg-stone-800 text-white rounded hover:bg-stone-700 transition-colors text-center"
>
Home
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,17 @@
{% extends '_types/root/index.html' %}
{% block content %}
<div
class="w-full flex justify-center font-bold text-2xl md:text-4xl px-2 flex-1 text-red-500"
>
{% block error_summary %}
{% endblock %}
</div>
<div
class="w-full flex justify-center"
>
{% block error_content %}
{% endblock %}
</div>
{% endblock %}

View File

@@ -0,0 +1,12 @@
{% extends '_types/root/exceptions/base.html' %}
{% block error_summary %}
<div>
WELL THIS IS EMBARASSING...
</div>
{% endblock %}
{% block error_content %}
<img src="{{asset_url('errors/error.gif')}}" width="300px" height="300px"/>
{% endblock %}

View File

@@ -0,0 +1,8 @@
<div class="flex flex-col gap-2 items-center">
<div>
{% include '_types/root/exceptions/' + errnum + '/message.html' %}
</div>
<img src="{% include '_types/root/exceptions/' + errnum + '/img.html' %}" width="300px" height="300px"/>
</div>

View File

@@ -0,0 +1,41 @@
{% set select_colours = "
[.hover-capable_&]:hover:bg-yellow-300
aria-selected:bg-stone-500 aria-selected:text-white
[.hover-capable_&[aria-selected=true]:hover]:bg-orange-500
"%}
{% import 'macros/links.html' as links %}
{% macro header_row(oob=False) %}
{% call links.menu_row(id='root-row', oob=oob) %}
<div class="w-full flex flex-row items-top">
{# Cart mini — fetched from cart app as fragment #}
{% if cart_mini_html %}
{{ cart_mini_html | safe }}
{% endif %}
{# Site title #}
<div class="font-bold text-5xl flex-1">
{% from 'macros/title.html' import title with context %}
{{ title('flex justify-center md:justify-start')}}
</div>
{# Desktop nav #}
<nav class="hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0">
{% if nav_tree_html %}
{{ nav_tree_html | safe }}
{% endif %}
{% if auth_menu_html %}
{{ auth_menu_html | safe }}
{% endif %}
{% include "_types/root/_nav_panel.html"%}
</nav>
{% include '_types/root/_hamburger.html' %}
</div>
{% endcall %}
{# Mobile user info #}
<div class="block md:hidden text-md font-bold">
{% if auth_menu_html %}
{{ auth_menu_html | safe }}
{% endif %}
</div>
{% endmacro %}

View File

@@ -0,0 +1,67 @@
{#
Shared root header for both base templates and OOB updates
This macro can be used in two modes:
- oob=true: Outputs full div with hx-swap-oob attribute (for OOB updates)
- oob=false: Outputs just content, assumes wrapper div exists (for base templates)
Usage:
1. Call root_header_start(oob=true/false)
2. Add any section-specific headers
3. Call root_header_end(oob=true/false)
#}
{% macro root_header_start(oob=true) %}
{% set select_colours = "
[.hover-capable_&]:hover:bg-yellow-300
aria-selected:bg-stone-500 aria-selected:text-white
[.hover-capable_&[aria-selected=true]:hover]:bg-orange-500
"%}
{% if oob %}
<div id="root-header" hx-swap-oob="outerHTML" class="flex items-start gap-2 p-1 bg-{{ menu_colour }}-{{ (500-(level()*100))|string }}">
{% endif %}
<div class="flex flex-col items-center flex-1">
<div class="flex w-full justify-center md:justify-start">
{# Cart mini — rendered via fragment #}
{% if cart_mini_html %}
{{ cart_mini_html | safe }}
{% endif %}
{# Site title #}
<div class="font-bold text-5xl flex-1">
{% from 'macros/title.html' import title with context %}
{{ title('flex justify-center md:justify-start')}}
</div>
{# Desktop nav #}
<nav class="hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0">
{% include '_types/root/_nav.html' %}
{% if not g.user %}
{% include '_types/root/_sign_in.html' %}
{% else %}
{% include '_types/root/_full_user.html' %}
{% endif %}
{% include "_types/root/_nav_panel.html"%}
</nav>
{% include '_types/root/_hamburger.html' %}
</div>
{# Mobile user info #}
<div class="block md:hidden text-md font-bold">
{% if g.user %}
{% include '_types/root/mobile/_full_user.html' %}
{% else %}
{% include '_types/root/mobile/_sign_in.html' %}
{% endif %}
</div>
{# Section-specific headers go here (caller adds them between start and end) #}
{% endmacro %}
{% macro root_header_end(oob=true) %}
</div>
{% if oob %}
</div>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,38 @@
{#
Shared root header for both base templates and OOB updates
This macro can be used in two modes:
- oob=true: Outputs full div with hx-swap-oob attribute (for OOB updates)
- oob=false: Outputs just content, assumes wrapper div exists (for base templates)
Usage:
1. Call root_header_start(oob=true/false)
2. Add any section-specific headers
3. Call root_header_end(oob=true/false)
#}
{% macro root_header(oob=true) %}
{% set select_colours = "
[.hover-capable_&]:hover:bg-yellow-300
aria-selected:bg-stone-500 aria-selected:text-white
[.hover-capable_&[aria-selected=true]:hover]:bg-orange-500
"%}
{% if oob %}
<div id="root-header"
hx-swap-oob="outerHTML"
class="flex items-start gap-2 p-1 bg-{{ menu_colour }}-{{ (500-(level()*100))|string }}">
{% endif %}
<div class="flex flex-col items-center flex-1">
{% from '_types/root/header/_header.html' import header_row with context %}
{{ header_row() }}
{{ caller() }}
</div>
{% if oob %}
</div>
{% endif %}
{% endmacro %}

View File

@@ -0,0 +1,84 @@
{% import 'macros/layout.html' as layout %}
{% from '_types/root/header/_oob.html' import root_header_start, root_header_end with context %}
{% from '_types/root/_oob_menu.html' import mobile_menu with context %}
<!doctype html>
<html lang="en">
<head>
{% block meta %}
{% include 'social/meta_site.html' %}
{% endblock %}
{% include '_types/root/_head.html' %}
</head>
<body class="bg-stone-50 text-stone-900">
<div class="max-w-screen-2xl mx-auto py-1 px-1">
{% block header %}
{% from '_types/root/_n/macros.html' import header with context %}
{% call header() %}
{% call layout.details('/root-header') %}
{% call layout.summary(
'root-header-summary',
_class='flex items-start gap-2 p-1 + bg-' + menu_colour + '-' + (500-(level()*100))|string,
)
%}
<div class="flex flex-col w-full items-center">
{% from '_types/root/header/_header.html' import header_row with context %}
{{ header_row() }}
<div id="root-header-child" class="flex flex-col w-full items-center">
{% block root_header_child %}
{% endblock %}
</div>
</div>
{% endcall %}
{% call layout.menu('root-menu', 'md:hidden bg-yellow-100') %}
{% block main_mobile_menu %}
{% endblock %}
{% endcall %}
{% endcall %}
{% endcall %}
{% endblock %}
<div
id="filter"
>
{% block filter %}
{% endblock %}
</div>
<main
id="root-panel"
class="max-w-full">
<div class="md:min-h-0">
<div class="flex flex-row md:h-full md:min-h-0">
<aside
id="aside"
class="hidden md:flex md:flex-col max-w-xs md:h-full md:min-h-0 mr-3"
>
{% block aside %}
{% endblock %}
</aside>
<section
id="main-panel"
class="flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
>
{% block content %}
{% endblock %}
<div class="pb-8"></div>
</section>
</div>
</div>
</main>
</div>
<script src="{{asset_url('scripts/body.js')}}"></script>
</body>
</html>

View File

@@ -0,0 +1,10 @@
{% set href=account_url('/') %}
<a
href="{{ href }}"
data-close-details
>
<i class="fa-solid fa-user"></i>
<span>{{g.user.email}}</span>
</a>

View File

@@ -0,0 +1,8 @@
<a
href="{{ account_url('/') }}"
aria-selected="{{ 'true' if '/auth/login' in request.path else 'false' }}"
>
<i class="fa-solid fa-key"></i>
<span>sign in or register</span>
</a>

View 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 %}

View File

@@ -0,0 +1,31 @@
{# Cart icon/badge — shows logo when empty, cart icon with count when items present #}
{% macro cart_icon(count=0, oob=False) %}
<div id="cart-mini" {% if oob %}hx-swap-oob="{{oob}}"{% endif %}>
{% if count == 0 %}
<div class="h-12 w-12 rounded-full overflow-hidden border border-stone-300 flex-shrink-0">
<a
href="{{ blog_url('/') }}"
class="h-full w-full font-bold text-5xl flex-shrink-0 flex flex-row items-center gap-1"
>
<img
src="{{ site().logo }}"
class="h-full w-full rounded-full object-cover border border-stone-300 flex-shrink-0"
>
</a>
</div>
{% else %}
<a
href="{{ cart_url('/') }}"
class="relative inline-flex items-center justify-center text-stone-700 hover:text-emerald-700"
>
<i class="fa fa-shopping-cart text-5xl" aria-hidden="true"></i>
<span
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full bg-emerald-600 text-white text-sm w-5 h-5"
>
{{ count }}
</span>
</a>
{% endif %}
</div>
{% endmacro %}

View 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 %}

View File

@@ -0,0 +1,61 @@
{# 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>
<div class="md:hidden mx-2 bg-stone-200 rounded">
<span class="flex items-center justify-center text-stone-600 text-lg h-12 w-12 transition-transform group-open/filter:hidden self-start">
<i class="fa-solid fa-filter"></i>
</span>
<span>
<svg aria-hidden="true" viewBox="0 0 24 24"
class="w-12 h-12 rotate-180 transition-transform group-open/filter:block hidden self-start">
<path d="M6 9l6 6 6-6" fill="currentColor"/>
</svg>
</span>
</div>
</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>
{% from 'macros/search.html' import search_mobile %}
{{ search_mobile(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 %}

View 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 %}

View 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 %}

View File

@@ -0,0 +1,83 @@
{# Shared search input macros for filter UIs #}
{% macro search_mobile(current_local_href, search, search_count, hx_select) -%}
<div
id="search-mobile-wrapper"
class="flex flex-row gap-2 items-center flex-1 min-w-0 pr-2"
>
<input
id="search-mobile"
type="text"
name="search"
aria-label="search"
class="text-base md:text-sm col-span-5 rounded-md px-3 py-2 mb-2 w-full min-w-0 max-w-full border-2 border-stone-200 placeholder-shown:border-stone-200 [&:not(:placeholder-shown)]:border-yellow-200"
hx-preserve
value="{{ search|default('', true) }}"
placeholder="search"
hx-trigger="input changed delay:300ms"
hx-target="#main-panel"
hx-select="{{hx_select}}, #search-mobile-wrapper, #search-desktop-wrapper"
hx-get="{{ (current_local_href ~ {'search': None}|qs)|host }}"
hx-swap="outerHTML"
hx-push-url="true"
hx-headers='{"X-Origin":"search-mobile", "X-Search":"true"}'
hx-sync="this:replace"
autocomplete="off"
>
<div
id="search-count-mobile"
aria-label="search count"
{% if not search_count %}
class="text-xl text-red-500"
{% endif %}
>
{% if search %}
{{search_count}}
{% endif %}
</div>
</div>
{%- endmacro %}
{% macro search_desktop(current_local_href, search, search_count, hx_select) -%}
<div
id="search-desktop-wrapper"
class="flex flex-row gap-2 items-center"
>
<input
id="search-desktop"
type="text"
name="search"
aria-label="search"
class="w-full mx-1 my-3 px-3 py-2 text-md rounded-xl border-2 shadow-sm border-white placeholder-shown:border-white [&:not(:placeholder-shown)]:border-yellow-200"
hx-preserve
value="{{ search|default('', true) }}"
placeholder="search"
hx-trigger="input changed delay:300ms"
hx-target="#main-panel"
hx-select="{{hx_select}}, #search-mobile-wrapper, #search-desktop-wrapper"
hx-get="{{ (current_local_href ~ {'search': None}|qs)|host}}"
hx-swap="outerHTML"
hx-push-url="true"
hx-headers='{"X-Origin":"search-desktop", "X-Search":"true"}'
hx-sync="this:replace"
autocomplete="off"
>
<div
id="search-count-desktop"
aria-label="search count"
{% if not search_count %}
class="text-xl text-red-500"
{% endif %}
>
{% if search %}
{{search_count}}
{% endif %}
{{zap_filter}}
</div>
</div>
{%- endmacro %}

View 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 -%}

View File

@@ -0,0 +1,10 @@
{% macro title(_class='') %}
<a
href="{{ blog_url('/') }}"
class="{{_class}}"
>
<h1>
{{ site().title }}
</h1>
</a>
{% endmacro %}

View File

@@ -0,0 +1,5 @@
<div class="md:hidden z-40">
{% block menu %}
{% endblock %}
</div>

View File

@@ -0,0 +1,38 @@
{% block oobs %}
{% endblock %}
<div
id="filter"
hx-swap-oob="outerHTML"
>
{% block filter %}
{% endblock %}
</div>
<aside
id="aside"
hx-swap-oob="outerHTML"
class="hidden md:flex md:flex-col max-w-xs md:h-full md:min-h-0 mr-3"
>
{% block aside %}
{% endblock %}
</aside>
<div id="root-menu" hx-swap-oob="outerHTML" class="md:hidden">
{% block mobile_menu %}
{% endblock %}
</div>
<section
id="main-panel"
class="flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
>
{% block content %}
{% endblock %}
</section>

View File

@@ -0,0 +1,9 @@
<div class="js-loading text-center text-xs text-stone-400">
loading… {{ page }} / {{ total_pages }}
</div>
<div class="js-neterr hidden inset-0 grid place-items-center p-4">
<div class="w-full max-w-[360px]">
{% include "sentinel/wireless_error.svg" %}
</div>
</div>

View File

@@ -0,0 +1,11 @@
<!-- tiny loading text (default) -->
<div class="js-loading text-center text-xs text-stone-400">
loading… {{ page }} / {{ total_pages }}
</div>
<!-- BIG error panel (hidden by default) -->
<div class="js-neterr hidden flex h-full items-center justify-center">
<!-- Funky SVG: unplugged cable + pulse -->
{% include "sentinel/wireless_error.svg" %}
</div>

View File

@@ -0,0 +1,20 @@
<svg fill="#f5a40cff" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
width="800px" height="800px" viewBox="0 0 862.899 862.9"
xml:space="preserve"
class="block w-full h-auto max-h-full" preserveAspectRatio="xMidYMid meet"
>
<g>
<g>
<circle cx="385.6" cy="656.1" r="79.8"/>
<path d="M561.7,401c-15.801-10.3-32.601-19.2-50.2-26.6c-39.9-16.9-82.3-25.5-126-25.5c-44.601,0-87.9,8.9-128.6,26.6
c-39.3,17-74.3,41.3-104.1,72.2L253.5,545c34.899-36.1,81.8-56,132-56c49,0,95.1,19.1,129.8,53.8l25.4-25.399L493,469.7L561.7,401
z"/>
<path d="M385.6,267.1c107.601,0,208.9,41.7,285.3,117.4l98.5-99.5c-50-49.5-108.1-88.4-172.699-115.6
c-66.9-28.1-138-42.4-211.101-42.4c-73.6,0-145,14.4-212.3,42.9c-65,27.5-123.3,66.8-173.3,116.9l99,99
C175.5,309.299,277.3,267.1,385.6,267.1z"/>
<polygon points="616.8,402.5 549.7,469.599 639.2,559.099 549.7,648.599 616.8,715.7 706.3,626.2 795.8,715.7 862.899,648.599
773.399,559.099 862.899,469.599 795.8,402.5 706.3,492 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1009 B

View File

@@ -0,0 +1,54 @@
{# social/meta_base.html — common, non-conflicting head tags #}
{# Expected context:
site: { title, url, logo, default_image, twitter_site, fb_app_id, description? }
request: Quart request (for canonical derivation)
robots_override: optional string ("index,follow" / "noindex,nofollow")
#}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{# Canonical #}
{% set _site_url = site().url.rstrip('/') if site and site().url else '' %}
{% set canonical = (
request.url if request and request.url
else (_site_url ~ request.path if request and _site_url else _site_url or None)
) %}
{# Robots: allow override; default to index,follow #}
<meta name="robots" content="{{ robots_override if robots_override is defined else 'index,follow' }}">
{# Theme & RSS #}
<meta name="theme-color" content="#ffffff">
{% if _site_url %}
<link rel="alternate" type="application/rss+xml"
title="{{ site().title if site and site().title else 'RSS' }}"
href="{{ _site_url }}/rss.xml">
{% endif %}
{# JSON-LD: Organization & WebSite are safe on all pages (don't conflict with BlogPosting) #}
{% set org_jsonld = {
"@context": "https://schema.org",
"@type": "Organization",
"name": site().title if site and site().title else "",
"url": _site_url if _site_url else None,
"logo": site().logo if site and site().logo else None
} %}
<script type="application/ld+json">
{{ org_jsonld | tojson }}
</script>
{% set website_jsonld = {
"@context": "https://schema.org",
"@type": "WebSite",
"name": site().title if site and site().title else "",
"url": _site_url if _site_url else canonical,
"potentialAction": {
"@type": "SearchAction",
"target": (_site_url ~ "/search?q={query}") if _site_url else None,
"query-input": "required name=query"
}
} %}
<script type="application/ld+json">
{{ website_jsonld | tojson }}
</script>

View File

@@ -0,0 +1,25 @@
{# social/meta_site.html — generic site/page meta #}
{% include 'social/meta_base.html' %}
{# Title/description (site-level) #}
{% set description = site().description or '' %}
<title>{{ base_title }}</title>
{% if description %}<meta name="description" content="{{ description }}">{% endif %}
{% if canonical %}<link rel="canonical" href="{{ canonical }}">{% endif %}
{# Open Graph (website) #}
<meta property="og:site_name" content="{{ site().title if site and site().title else '' }}">
<meta property="og:type" content="website">
<meta property="og:title" content="{{ base_title }}">
{% if description %}<meta property="og:description" content="{{ description }}">{% endif %}
{% if canonical %}<meta property="og:url" content="{{ canonical }}">{% endif %}
{% if site and site().default_image %}<meta property="og:image" content="{{ site().default_image }}">{% endif %}
{% if site and site().fb_app_id %}<meta property="fb:app_id" content="{{ site().fb_app_id }}">{% endif %}
{# Twitter (website) #}
<meta name="twitter:card" content="{{ 'summary_large_image' if site and site().default_image else 'summary' }}">
{% if site and site().twitter_site %}<meta name="twitter:site" content="{{ site().twitter_site }}">{% endif %}
<meta name="twitter:title" content="{{ base_title }}">
{% if description %}<meta name="twitter:description" content="{{ description }}">{% endif %}
{% if site and site().default_image %}<meta name="twitter:image" content="{{ site().default_image }}">{% endif %}