- Server sends sexp source text, client (sexp.js) renders everything - SexpExpr marker class for nested sexp composition in serialize() - sexp_page() HTML shell with data-mount="body" for full page loads - sexp_response() returns text/sexp for OOB/partial responses - ~app-body layout component replaces ~app-layout (no raw!) - ~rich-text is the only component using raw! (for CMS HTML content) - Fragment endpoints return text/sexp, auto-wrapped in SexpExpr - All _*_html() helpers converted to _*_sexp() returning sexp source - Head auto-hoist: sexp.js moves meta/title/link/script[ld+json] from rendered body to document.head automatically - Unknown components render warning box instead of crashing page - Component kwargs preserve AST for lazy rendering (fixes <> in kwargs) - Fix unterminated paren in events/sexp/tickets.sexpr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
60 lines
2.5 KiB
HTML
60 lines
2.5 KiB
HTML
{#
|
|
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"
|
|
onclick="document.getElementById('{{ 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;"
|
|
data-scroll-arrows="scrolling-menu-arrow-{{ container_id }}"
|
|
onscroll="(function(el){var cls='scrolling-menu-arrow-{{ container_id }}';var arrows=document.getElementsByClassName(cls);var show=window.innerWidth>=640&&el.scrollWidth>el.clientWidth;for(var i=0;i<arrows.length;i++){if(show){arrows[i].classList.remove('hidden');arrows[i].classList.add('flex')}else{arrows[i].classList.add('hidden');arrows[i].classList.remove('flex')}}})(this)">
|
|
<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"
|
|
onclick="document.getElementById('{{ container_id }}').scrollLeft += 200">
|
|
<i class="fa fa-chevron-right"></i>
|
|
</button>
|
|
{% endif %}
|
|
{% endmacro %}
|