Send all responses as sexp wire format with client-side rendering
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
- 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>
This commit is contained in:
@@ -7,9 +7,9 @@
|
||||
<div
|
||||
id="sentinel-{{ page }}"
|
||||
class="h-4 opacity-0 pointer-events-none"
|
||||
hx-get="{{ next_url }}"
|
||||
hx-trigger="intersect once delay:250ms"
|
||||
hx-swap="outerHTML"
|
||||
sx-get="{{ next_url }}"
|
||||
sx-trigger="intersect once delay:250ms"
|
||||
sx-swap="outerHTML"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
|
||||
<a
|
||||
href="{{ item_href }}"
|
||||
hx-get="{{ item_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select ="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ item_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select ="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class=""
|
||||
>
|
||||
|
||||
@@ -80,11 +80,11 @@
|
||||
|
||||
<a
|
||||
href="{{ item_href }}"
|
||||
hx-get="{{ item_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select ="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ item_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select ="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
<div class="flex flex-row justify-center gap-2 p-2">
|
||||
{% for s in p.stickers %}
|
||||
|
||||
@@ -7,43 +7,11 @@
|
||||
<div
|
||||
id="sentinel-{{ page }}-m"
|
||||
class="block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel"
|
||||
hx-get="{{ (current_local_href ~ {'page': page + 1}|qs)|host }}"
|
||||
hx-trigger="intersect once delay:250ms, sentinelmobile:retry"
|
||||
hx-swap="outerHTML"
|
||||
_="
|
||||
init
|
||||
if not me.dataset.retryMs then set me.dataset.retryMs to 1000 end
|
||||
if window.matchMedia('(min-width: 768px)').matches then set @hx-disabled to '' end
|
||||
|
||||
on resize from window
|
||||
if window.matchMedia('(min-width: 768px)').matches then set @hx-disabled to '' else remove @hx-disabled end
|
||||
|
||||
on htmx:beforeRequest
|
||||
if window.matchMedia('(min-width: 768px)').matches then halt end
|
||||
add .hidden to .js-neterr in me
|
||||
remove .hidden from .js-loading in me
|
||||
remove .opacity-100 from me
|
||||
add .opacity-0 to me
|
||||
|
||||
def backoff()
|
||||
set ms to me.dataset.retryMs
|
||||
if ms > 30000 then set ms to 30000 end
|
||||
-- show big SVG panel & make sentinel visible
|
||||
add .hidden to .js-loading in me
|
||||
remove .hidden from .js-neterr in me
|
||||
remove .opacity-0 from me
|
||||
add .opacity-100 to me
|
||||
wait ms ms
|
||||
trigger sentinelmobile:retry
|
||||
set ms to ms * 2
|
||||
if ms > 30000 then set ms to 30000 end
|
||||
set me.dataset.retryMs to ms
|
||||
end
|
||||
|
||||
on htmx:sendError call backoff()
|
||||
on htmx:responseError call backoff()
|
||||
on htmx:timeout call backoff()
|
||||
"
|
||||
sx-get="{{ (current_local_href ~ {'page': page + 1}|qs)|host }}"
|
||||
sx-trigger="intersect once delay:250ms, sentinelmobile:retry"
|
||||
sx-swap="outerHTML"
|
||||
sx-media="(max-width: 767px)"
|
||||
sx-retry="exponential:1000:30000"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-hidden="true"
|
||||
@@ -54,47 +22,10 @@
|
||||
<div
|
||||
id="sentinel-{{ page }}-d"
|
||||
class="hidden md:block h-4 opacity-0 pointer-events-none"
|
||||
hx-get="{{ (current_local_href ~ {'page': page + 1}|qs)|host}}"
|
||||
hx-trigger="intersect once delay:250ms, sentinel:retry"
|
||||
hx-swap="outerHTML"
|
||||
_="
|
||||
init
|
||||
if not me.dataset.retryMs then set me.dataset.retryMs to 1000 end
|
||||
|
||||
on htmx:beforeRequest(event)
|
||||
add .hidden to .js-neterr in me
|
||||
remove .hidden from .js-loading in me
|
||||
remove .opacity-100 from me
|
||||
add .opacity-0 to me
|
||||
|
||||
set trig to null
|
||||
if event.detail and event.detail.triggeringEvent then
|
||||
set trig to event.detail.triggeringEvent
|
||||
end
|
||||
if trig and trig.type is 'intersect'
|
||||
set scroller to the closest .js-grid-viewport
|
||||
if scroller is null then halt end
|
||||
if scroller.scrollTop < 20 then halt end
|
||||
end
|
||||
|
||||
def backoff()
|
||||
set ms to me.dataset.retryMs
|
||||
if ms > 30000 then set ms to 30000 end
|
||||
add .hidden to .js-loading in me
|
||||
remove .hidden from .js-neterr in me
|
||||
remove .opacity-0 from me
|
||||
add .opacity-100 to me
|
||||
wait ms ms
|
||||
trigger sentinel:retry
|
||||
set ms to ms * 2
|
||||
if ms > 30000 then set ms to 30000 end
|
||||
set me.dataset.retryMs to ms
|
||||
end
|
||||
|
||||
on htmx:sendError call backoff()
|
||||
on htmx:responseError call backoff()
|
||||
on htmx:timeout call backoff()
|
||||
"
|
||||
sx-get="{{ (current_local_href ~ {'page': page + 1}|qs)|host}}"
|
||||
sx-trigger="intersect once delay:250ms, sentinel:retry"
|
||||
sx-swap="outerHTML"
|
||||
sx-retry="exponential:1000:30000"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-hidden="true"
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if top_active else 'false' }}"
|
||||
class="block px-4 py-3 text-[15px] transition {{select_colours}}">
|
||||
<div class="prose prose-stone max-w-none">All products</div>
|
||||
@@ -24,11 +24,11 @@
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if active else 'false' }}"
|
||||
class="block px-4 py-3 text-[15px] border-l-4 transition {{select_colours}}"
|
||||
>
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
<li>
|
||||
<a
|
||||
href="{{ brand_href }}"
|
||||
hx-get="{{ brand_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML" hx-push-url="true" hx-on:htmx:afterSwap="this.closest('details')?.removeAttribute('open')"
|
||||
sx-get="{{ brand_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML" sx-push-url="true" hx-on:htmx:afterSwap="this.closest('details')?.removeAttribute('open')"
|
||||
class="flex items-center gap-2 px-2 py-2 rounded transition {% if is_selected %} bg-stone-900 text-white {% else %} hover:bg-stone-50 {% endif %}">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded border {% if is_selected %} border-stone-900 bg-stone-900 text-white {% else %} border-stone-300 {% endif %}">
|
||||
{% if is_selected %}
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ s.name }}" aria-label="{{ s.name }}"
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if liked else 'false' }}"
|
||||
title="liked" aria-label="liked"
|
||||
|
||||
@@ -12,18 +12,18 @@
|
||||
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
|
||||
sx-preserve
|
||||
value="{{ search|default('', true) }}"
|
||||
placeholder="search"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
hx-target="#main-panel"
|
||||
sx-trigger="input changed delay:300ms"
|
||||
sx-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"
|
||||
sx-select="{{hx_select}}, #search-mobile-wrapper, #search-desktop-wrapper"
|
||||
sx-get="{{ (current_local_href ~ {'search': None}|qs)|host}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
sx-headers='{"X-Origin":"search-desktop", "X-Search":"true"}'
|
||||
sx-sync="this:replace"
|
||||
|
||||
autocomplete="off"
|
||||
>
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
class="flex flex-col items-center justify-center w-full h-full py-2 m-0"
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
<li>
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ s.name }}" aria-label="{{ s.name }}"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<button
|
||||
class="flex items-center gap-1 {% if liked %} text-red-600 {% else %} text-stone-300 {% endif %} hover:text-red-600 transition-colors w-[1em] h-[1em]"
|
||||
hx-post="{{ like_url if like_url else url_for('market.browse.product.like_toggle', product_slug=slug)|host }}"
|
||||
hx-target="this"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="false"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
hx-swap-settle="0ms"
|
||||
sx-post="{{ like_url if like_url else url_for('market.browse.product.like_toggle', product_slug=slug)|host }}"
|
||||
sx-target="this"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="false"
|
||||
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
|
||||
sx-swap-settle="0ms"
|
||||
{% if liked %}
|
||||
aria-label="Unlike this {{ item_type if item_type else 'product' }}"
|
||||
{% else %}
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
{% set href = (current_local_href ~ {"add_brand": b.name, "page": None}|qs)|host %}
|
||||
{%endif%}
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
sx-get="{{ href }}"
|
||||
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
|
||||
class="flex items-center gap-2 my-3 px-2 py-2 rounded transition {% if is_selected %} bg-stone-900 text-white {% else %} hover:bg-stone-50 {% endif %}">
|
||||
<span class="inline-flex items-center justify-center w-5 h-5 rounded border {% if is_selected %} border-stone-900 bg-stone-900 text-white {% else %} border-stone-300 {% endif %}">
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<div class = "flex flex-row justify-center">
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
title="clear filters"
|
||||
aria-label="clear filters"
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
<li class="list-none shrink-0">
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ s.name }}" aria-label="{{ s.name }}"
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
{% set href = (current_local_href ~ qs)|host %}
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if liked else 'false' }}"
|
||||
title="liked" aria-label="liked"
|
||||
|
||||
@@ -10,18 +10,18 @@
|
||||
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
|
||||
sx-preserve
|
||||
value="{{ search|default('', true) }}"
|
||||
placeholder="search"
|
||||
hx-trigger="input changed delay:300ms"
|
||||
hx-target="#main-panel"
|
||||
sx-trigger="input changed delay:300ms"
|
||||
sx-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"
|
||||
sx-select="{{hx_select}}, #search-mobile-wrapper, #search-desktop-wrapper"
|
||||
sx-get="{{ (current_local_href ~ {'search': None}|qs)|host }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
sx-headers='{"X-Origin":"search-mobile", "X-Search":"true"}'
|
||||
sx-sync="this:replace"
|
||||
autocomplete="off"
|
||||
>
|
||||
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
{% set href= (current_local_href ~ {"sort": key, "page": None}|qs )|host %}
|
||||
{% endif %}
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
{{ stick.sticker(asset_url(icon), label, sort==key) }}
|
||||
</a>
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
<li class="list-none shrink-0">
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ s.name }}" aria-label="{{ s.name }}"
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<div class="relative nav-group">
|
||||
<a
|
||||
href="{{ all_href }}"
|
||||
hx-get="{{ all_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ all_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if all_active else 'false' }}"
|
||||
class="block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black {{select_colours}}">
|
||||
All
|
||||
@@ -22,11 +22,11 @@
|
||||
<div class="relative nav-group">
|
||||
<a
|
||||
href="{{ cat_href }}"
|
||||
hx-get="{{ cat_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ cat_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if cat_active else 'false' }}"
|
||||
class="block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black {{select_colours}}"
|
||||
>
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
{% set all_active = (category_label == 'All Products') %}
|
||||
<a role="option"
|
||||
href="{{ all_href }}"
|
||||
hx-get="{{ all_href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ all_href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if all_active else 'false' }}"
|
||||
class="block rounded-lg px-3 py-3 text-base hover:bg-stone-50 {{select_colours}}">
|
||||
<div class="prose prose-stone max-w-none">
|
||||
@@ -27,11 +27,11 @@
|
||||
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
aria-selected="{{ 'true' if top_slug==(data.slug | lower) else 'false' }}"
|
||||
class="font-medium {{ select_colours }} flex flex-row gap-2"
|
||||
>
|
||||
@@ -60,11 +60,11 @@
|
||||
class="snap-start px-2 py-3 rounded {{select_colours}} flex flex-row gap-2"
|
||||
aria-selected="{{ 'true' if top_slug==(data.slug | lower) and sub_slug == sub.slug else 'false' }}"
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
<div>{{ sub.html_label or sub.name }}</div>
|
||||
<div aria-label="{{ sub.count }} products">{{ sub.count }}</div>
|
||||
@@ -78,11 +78,11 @@
|
||||
class="snap-start px-2 py-3 rounded {{select_colours}} flex flex-row gap-2"
|
||||
aria-selected="{{ 'true' if top_slug==(data.slug | lower) and sub_slug == sub.slug else 'false' }}"
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>
|
||||
<div>{{ sub.name }}</div>
|
||||
<div aria-label="{{ sub.count }} products">{{ sub.count }}</div>
|
||||
@@ -95,11 +95,11 @@
|
||||
{% set href = (url_for('market.browse.browse_top', top_slug=data.slug) ~ qs)|host%}
|
||||
<a class="px-2 py-1 rounded hover:bg-stone-100 block"
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
>View all</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
<div
|
||||
id="sentinel-{{ page }}"
|
||||
class="h-4 opacity-0 pointer-events-none"
|
||||
hx-get="{{ next_url }}"
|
||||
hx-trigger="intersect once delay:250ms"
|
||||
hx-swap="outerHTML"
|
||||
sx-get="{{ next_url }}"
|
||||
sx-trigger="intersect once delay:250ms"
|
||||
sx-swap="outerHTML"
|
||||
role="status"
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<button
|
||||
class="entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
aria-label="Scroll left"
|
||||
_="on click
|
||||
set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft - 200">
|
||||
onclick="document.getElementById('associated-items-container').scrollLeft -= 200">
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
|
||||
@@ -12,15 +11,8 @@
|
||||
<div id="associated-items-container"
|
||||
class="overflow-y-auto sm:overflow-x-auto sm:overflow-y-visible scrollbar-hide max-h-[50vh] sm:max-h-none"
|
||||
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 .entries-nav-arrow
|
||||
add .flex to .entries-nav-arrow
|
||||
else
|
||||
add .hidden to .entries-nav-arrow
|
||||
remove .flex from .entries-nav-arrow
|
||||
end">
|
||||
data-scroll-arrows="entries-nav-arrow"
|
||||
onscroll="(function(el){var arrows=document.getElementsByClassName('entries-nav-arrow');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">
|
||||
{% for wdata in container_nav_widgets %}
|
||||
{% with ctx=wdata.ctx %}
|
||||
@@ -44,7 +36,6 @@
|
||||
<button
|
||||
class="entries-nav-arrow hidden flex-shrink-0 p-2 hover:bg-stone-200 rounded"
|
||||
aria-label="Scroll right"
|
||||
_="on click
|
||||
set #associated-items-container.scrollLeft to #associated-items-container.scrollLeft + 200">
|
||||
onclick="document.getElementById('associated-items-container').scrollLeft += 200">
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
| selectattr('product.slug', 'equalto', slug)
|
||||
| sum(attribute='quantity') %}
|
||||
|
||||
<div id="cart-{{ slug }}" {% if oob=='true' %} hx-swap-oob="{{oob}}" {% endif %}>
|
||||
<div id="cart-{{ slug }}" {% if oob=='true' %} sx-swap-oob="{{oob}}" {% endif %}>
|
||||
|
||||
{% if not quantity %}
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
method="post"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
class="rounded flex items-center"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
@@ -40,9 +40,9 @@
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
method="post"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input
|
||||
@@ -82,9 +82,9 @@
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
method="post"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input
|
||||
@@ -113,7 +113,7 @@
|
||||
<article
|
||||
id="cart-item-{{p.slug}}"
|
||||
{% if oob %}
|
||||
hx-swap-oob="{{oob}}"
|
||||
sx-swap-oob="{{oob}}"
|
||||
{% endif %}
|
||||
class="flex flex-col sm:flex-row gap-3 sm:gap-4 rounded-2xl bg-white shadow-sm border border-stone-200 p-3 sm:p-4 md:p-5"
|
||||
>
|
||||
@@ -143,10 +143,10 @@
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx_get="{{href}}"
|
||||
hx-target="#main-panel"
|
||||
hx-select ="{{hx_select_search}}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-target="#main-panel"
|
||||
sx-select ="{{hx_select_search}}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
class="hover:text-emerald-700"
|
||||
>
|
||||
{{ p.title }}
|
||||
@@ -191,9 +191,9 @@
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
method="post"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input
|
||||
@@ -214,9 +214,9 @@
|
||||
<form
|
||||
action="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
method="post"
|
||||
hx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
hx-target="#cart-mini"
|
||||
hx-swap="outerHTML"
|
||||
sx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
|
||||
sx-target="#cart-mini"
|
||||
sx-swap="outerHTML"
|
||||
>
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<input
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<aside
|
||||
id="aside"
|
||||
hx-swap-oob="outerHTML"
|
||||
sx-swap-oob="outerHTML"
|
||||
class="hidden"
|
||||
>
|
||||
</aside>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
id="filter"
|
||||
hx-swap-oob="outerHTML"
|
||||
sx-swap-oob="outerHTML"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
<a
|
||||
href="{{ href }}"
|
||||
hx-get="{{ href }}"
|
||||
hx-target="#main-panel"
|
||||
hx-select="{{ hx_select_search }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-push-url="true"
|
||||
sx-get="{{ href }}"
|
||||
sx-target="#main-panel"
|
||||
sx-select="{{ hx_select_search }}"
|
||||
sx-swap="outerHTML"
|
||||
sx-push-url="true"
|
||||
role="button"
|
||||
aria-pressed="{{ 'true' if is_on else 'false' }}"
|
||||
title="{{ title }}"
|
||||
|
||||
Reference in New Issue
Block a user