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

- 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:
2026-03-01 09:45:07 +00:00
parent 0d48fd22ee
commit 22802bd36b
270 changed files with 7153 additions and 5382 deletions

View File

@@ -18,7 +18,7 @@ async def market_context() -> dict:
"""
Market app context processor.
- nav_tree_html: fetched from blog as fragment
- nav_tree: fetched from blog as fragment
- cart_count/cart_total: via cart service (includes calendar entries)
- cart: direct ORM query (templates need .product relationship)
"""
@@ -54,14 +54,14 @@ async def market_context() -> dict:
if ident["session_id"] is not None:
cart_params["session_id"] = ident["session_id"]
cart_mini_html, auth_menu_html, nav_tree_html = await fetch_fragments([
cart_mini, auth_menu, nav_tree = await fetch_fragments([
("cart", "cart-mini", cart_params or None),
("account", "auth-menu", {"email": user.email} if user else None),
("blog", "nav-tree", {"app_name": "market", "path": request.path}),
])
ctx["cart_mini_html"] = cart_mini_html
ctx["auth_menu_html"] = auth_menu_html
ctx["nav_tree_html"] = nav_tree_html
ctx["cart_mini"] = cart_mini
ctx["auth_menu"] = auth_menu
ctx["nav_tree"] = nav_tree
# Cart items for product templates — fetched via internal data endpoint
# (cart_items table lives in db_cart, not db_market)
@@ -221,7 +221,7 @@ def create_app() -> "Quart":
("events", "container-nav", nav_params),
("market", "container-nav", nav_params),
], required=False)
ctx["container_nav_html"] = events_nav + market_nav
ctx["container_nav"] = events_nav + market_nav
return ctx
# --- oEmbed endpoint ---

View File

@@ -12,6 +12,7 @@ from __future__ import annotations
from quart import Blueprint, g, request, render_template, make_response
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response
from shared.infrastructure.data_client import fetch_data
from shared.contracts.dtos import PostDTO, dto_from_dict
from shared.services.registry import services
@@ -60,11 +61,11 @@ def register() -> Blueprint:
tctx = await get_template_context()
if is_htmx_request():
html = await render_all_markets_oob(tctx, markets, has_more, page_info, page)
sexp_src = await render_all_markets_oob(tctx, markets, has_more, page_info, page)
return sexp_response(sexp_src)
else:
html = await render_all_markets_page(tctx, markets, has_more, page_info, page)
return await make_response(html, 200)
return await make_response(html, 200)
@bp.get("/all-markets")
async def markets_fragment():
@@ -72,7 +73,7 @@ def register() -> Blueprint:
markets, has_more, page_info = await _load_markets(page)
from sexp.sexp_components import render_all_markets_cards
html = await render_all_markets_cards(markets, has_more, page_info, page)
return await make_response(html, 200)
sexp_src = await render_all_markets_cards(markets, has_more, page_info, page)
return sexp_response(sexp_src)
return bp

View File

@@ -23,6 +23,7 @@ from .services import (
from shared.browser.app.redis_cacher import cache_page
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response
def register():
browse_bp = Blueprint("browse", __name__)
@@ -49,10 +50,10 @@ def register():
ctx.update(p_data)
if not is_htmx_request():
html = await render_market_home_page(ctx)
return await make_response(html)
else:
html = await render_market_home_oob(ctx)
return await make_response(html)
sexp_src = await render_market_home_oob(ctx)
return sexp_response(sexp_src)
@browse_bp.get("/all/")
@cache_page(tag="browse")
@@ -80,13 +81,15 @@ def register():
tctx.update(full_context)
if not is_htmx_request():
html = await render_browse_page(tctx)
resp = await make_response(html)
elif product_info["page"] > 1:
tctx.update(product_info)
html = await render_browse_cards(tctx)
sexp_src = await render_browse_cards(tctx)
resp = sexp_response(sexp_src)
else:
html = await render_browse_oob(tctx)
sexp_src = await render_browse_oob(tctx)
resp = sexp_response(sexp_src)
resp = await make_response(html)
resp.headers["Hx-Push-Url"] = _current_url_without_page()
return _vary(resp)
@@ -119,13 +122,15 @@ def register():
tctx.update(full_context)
if not is_htmx_request():
html = await render_browse_page(tctx)
resp = await make_response(html)
elif product_info["page"] > 1:
tctx.update(product_info)
html = await render_browse_cards(tctx)
sexp_src = await render_browse_cards(tctx)
resp = sexp_response(sexp_src)
else:
html = await render_browse_oob(tctx)
sexp_src = await render_browse_oob(tctx)
resp = sexp_response(sexp_src)
resp = await make_response(html)
resp.headers["Hx-Push-Url"] = _current_url_without_page()
return _vary(resp)
@@ -158,16 +163,18 @@ def register():
tctx.update(full_context)
if not is_htmx_request():
html = await render_browse_page(tctx)
resp = await make_response(html)
elif product_info["page"] > 1:
tctx.update(product_info)
html = await render_browse_cards(tctx)
sexp_src = await render_browse_cards(tctx)
resp = sexp_response(sexp_src)
else:
html = await render_browse_oob(tctx)
sexp_src = await render_browse_oob(tctx)
resp = sexp_response(sexp_src)
resp = await make_response(html)
resp.headers["Hx-Push-Url"] = _current_url_without_page()
return _vary(resp)
return browse_bp

View File

@@ -18,7 +18,10 @@ from ...market.filters.qs import decode
def _hx_fragment_request() -> bool:
return request.headers.get("HX-Request", "").lower() == "true"
return (
request.headers.get("SX-Request", "").lower() == "true"
or request.headers.get("HX-Request", "").lower() == "true"
)
async def _productInfo(top_slug=None, sub_slug=None):
"""

View File

@@ -1,6 +1,6 @@
"""Market app fragment endpoints.
Exposes HTML fragments at ``/internal/fragments/<type>`` for consumption
Exposes sexp fragments at ``/internal/fragments/<type>`` for consumption
by other coop apps via the fragment client.
"""
@@ -26,16 +26,16 @@ def register():
async def get_fragment(fragment_type: str):
handler = _handlers.get(fragment_type)
if handler is None:
return Response("", status=200, content_type="text/html")
html = await handler()
return Response(html, status=200, content_type="text/html")
return Response("", status=200, content_type="text/sexp")
src = await handler()
return Response(src, status=200, content_type="text/sexp")
# --- container-nav fragment: market links --------------------------------
async def _container_nav_handler():
from quart import current_app
from shared.infrastructure.urls import market_url
from shared.sexp.jinja_bridge import render as render_comp
from shared.sexp.helpers import sexp_call
container_type = request.args.get("container_type", "page")
container_id = int(request.args.get("container_id", 0))
@@ -51,21 +51,31 @@ def register():
parts = []
for m in markets:
href = market_url(f"/{post_slug}/{m.slug}/")
parts.append(render_comp(
"market-link-nav",
href=href, name=m.name, nav_class=nav_class,
))
return "\n".join(parts)
parts.append(sexp_call("market-link-nav",
href=href, name=m.name, nav_class=nav_class))
return "(<> " + " ".join(parts) + ")"
_handlers["container-nav"] = _container_nav_handler
# --- link-card fragment: product preview card --------------------------------
def _product_link_card_sexp(product, link: str) -> str:
from shared.sexp.helpers import sexp_call
subtitle = product.brand or ""
detail = ""
if product.special_price:
detail = f"{product.regular_price}{product.special_price}"
elif product.regular_price:
detail = str(product.regular_price)
return sexp_call("link-card",
title=product.title, image=product.image,
subtitle=subtitle, detail=detail,
link=link)
async def _link_card_handler():
from sqlalchemy import select
from shared.models.market import Product
from shared.infrastructure.urls import market_url
from shared.sexp.jinja_bridge import render as render_comp
slug = request.args.get("slug", "")
keys_raw = request.args.get("keys", "")
@@ -80,18 +90,8 @@ def register():
await g.s.execute(select(Product).where(Product.slug == s))
).scalar_one_or_none()
if product:
subtitle = product.brand or ""
detail = ""
if product.special_price:
detail = f"<s>{product.regular_price}</s> {product.special_price}"
elif product.regular_price:
detail = str(product.regular_price)
parts.append(render_comp(
"link-card",
title=product.title, image=product.image,
subtitle=subtitle, detail=detail,
link=market_url(f"/product/{product.slug}/"),
))
parts.append(_product_link_card_sexp(
product, market_url(f"/product/{product.slug}/")))
return "\n".join(parts)
# Single mode
@@ -102,18 +102,7 @@ def register():
).scalar_one_or_none()
if not product:
return ""
subtitle = product.brand or ""
detail = ""
if product.special_price:
detail = f"<s>{product.regular_price}</s> {product.special_price}"
elif product.regular_price:
detail = str(product.regular_price)
return render_comp(
"link-card",
title=product.title, image=product.image,
subtitle=subtitle, detail=detail,
link=market_url(f"/product/{product.slug}/"),
)
return _product_link_card_sexp(product, market_url(f"/product/{product.slug}/"))
_handlers["link-card"] = _link_card_handler

View File

@@ -23,8 +23,9 @@ def register():
tctx = await get_template_context()
if not is_htmx_request():
html = await render_market_admin_page(tctx)
return await make_response(html)
else:
html = await render_market_admin_oob(tctx)
return await make_response(html)
from shared.sexp.helpers import sexp_response
sexp_src = await render_market_admin_oob(tctx)
return sexp_response(sexp_src)
return bp

View File

@@ -18,8 +18,10 @@ def register():
tctx = await get_template_context()
if not is_htmx_request():
html = await render_page_admin_page(tctx)
return await make_response(html)
else:
html = await render_page_admin_oob(tctx)
return await make_response(html)
from shared.sexp.helpers import sexp_response
sexp_src = await render_page_admin_oob(tctx)
return sexp_response(sexp_src)
return bp

View File

@@ -12,6 +12,7 @@ from __future__ import annotations
from quart import Blueprint, g, request, render_template, make_response
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sexp.helpers import sexp_response
from shared.services.registry import services
@@ -45,11 +46,11 @@ def register() -> Blueprint:
tctx = await get_template_context()
tctx["post"] = post
if is_htmx_request():
html = await render_page_markets_oob(tctx, markets, has_more, page)
sexp_src = await render_page_markets_oob(tctx, markets, has_more, page)
return sexp_response(sexp_src)
else:
html = await render_page_markets_page(tctx, markets, has_more, page)
return await make_response(html, 200)
return await make_response(html, 200)
@bp.get("/page-markets")
async def markets_fragment():
@@ -60,7 +61,7 @@ def register() -> Blueprint:
from sexp.sexp_components import render_page_markets_cards
post_slug = post.get("slug", "")
html = await render_page_markets_cards(markets, has_more, page, post_slug)
return await make_response(html, 200)
sexp_src = await render_page_markets_cards(markets, has_more, page, post_slug)
return sexp_response(sexp_src)
return bp

View File

@@ -19,6 +19,7 @@ from shared.browser.app.redis_cacher import cache_page, clear_cache
from ..cart.services import total
from shared.infrastructure.actions import call_action
from .services.product_operations import massage_full_product
from shared.sexp.helpers import sexp_response
def register():
@@ -115,10 +116,10 @@ def register():
tctx["liked_by_current_user"] = item_data.get("liked", False)
if not is_htmx_request():
html = await render_product_page(tctx, d)
return html
else:
html = await render_product_oob(tctx, d)
return html
sexp_src = await render_product_oob(tctx, d)
return sexp_response(sexp_src)
@bp.post("/like/toggle/")
@clear_cache(tag="browse", tag_scope="user")
@@ -128,9 +129,7 @@ def register():
from sexp.sexp_components import render_like_toggle_button
if not g.user:
html = render_like_toggle_button(product_slug, False)
resp = make_response(html, 403)
return resp
return sexp_response(render_like_toggle_button(product_slug, False), status=403)
user_id = g.user.id
@@ -139,7 +138,7 @@ def register():
})
liked = result["liked"]
return render_like_toggle_button(product_slug, liked)
return sexp_response(render_like_toggle_button(product_slug, liked))
@@ -156,10 +155,10 @@ def register():
tctx["liked_by_current_user"] = item_data.get("liked", False)
if not is_htmx_request():
html = await render_product_admin_page(tctx, d)
return await make_response(html)
else:
html = await render_product_admin_oob(tctx, d)
return await make_response(html)
sexp_src = await render_product_admin_oob(tctx, d)
return sexp_response(sexp_src)
from bp.cart.services.identity import current_cart_identity
@@ -254,11 +253,11 @@ def register():
)
# htmx response: OOB-swap mini cart + product buttons
if request.headers.get("HX-Request") == "true":
if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true":
from sexp.sexp_components import render_cart_added_response
item_data = getattr(g, "item_data", {})
d = item_data.get("d", {})
return render_cart_added_response(g.cart, ci_ns, d)
return sexp_response(render_cart_added_response(g.cart, ci_ns, d))
# normal POST: go to cart page
from shared.infrastructure.urls import cart_url

View File

@@ -1,22 +1,22 @@
;; Market card components
;; Market card components — pure data, no raw! HTML injection
(defcomp ~market-label-overlay (&key src)
(img :src src :alt ""
:class "pointer-events-none absolute inset-0 w-full h-full object-contain object-top"))
(defcomp ~market-card-image (&key image labels-html brand-highlight brand)
(defcomp ~market-card-image (&key image labels brand brand-highlight)
(div :class "w-full aspect-square bg-stone-100 relative"
(figure :class "inline-block w-full h-full"
(div :class "relative w-full h-full"
(img :src image :alt "no image" :class "absolute inset-0 w-full h-full object-contain object-top" :loading "lazy" :decoding "async" :fetchpriority "low")
(raw! labels-html))
(when labels (map (lambda (src) (~market-label-overlay :src src)) labels)))
(figcaption :class (str "mt-2 text-sm text-center" brand-highlight " text-stone-600") brand))))
(defcomp ~market-card-no-image (&key labels-html brand)
(defcomp ~market-card-no-image (&key labels brand)
(div :class "w-full aspect-square bg-stone-100 relative"
(div :class "p-2 flex flex-col items-center justify-center gap-2 text-red-500 h-full relative"
(div :class "text-stone-400 text-xs" "No image")
(ul :class "flex flex-row gap-1" (raw! labels-html))
(when labels (ul :class "flex flex-row gap-1" (map (lambda (l) (li l)) labels)))
(div :class "text-stone-900 text-center line-clamp-3 break-words [overflow-wrap:anywhere]" brand))))
(defcomp ~market-card-label-item (&key label)
@@ -25,8 +25,9 @@
(defcomp ~market-card-sticker (&key src name ring-cls)
(img :src src :alt name :class (str "w-6 h-6" ring-cls)))
(defcomp ~market-card-stickers (&key items-html)
(div :class "flex flex-row justify-center gap-2 p-2" (raw! items-html)))
(defcomp ~market-card-stickers (&key stickers)
(div :class "flex flex-row justify-center gap-2 p-2"
(map (lambda (s) (~market-card-sticker :src (get s "src") :name (get s "name") :ring-cls (get s "ring-cls"))) stickers)))
(defcomp ~market-card-highlight (&key pre mid post)
(<> pre (mark mid) post))
@@ -34,23 +35,49 @@
(defcomp ~market-card-text (&key text)
(<> text))
(defcomp ~market-product-card (&key like-html href hx-select image-html price-html add-html stickers-html title-html)
;; Price — single component accepts both prices, renders correctly
(defcomp ~market-card-price (&key special-price regular-price)
(div :class "mt-1 flex items-baseline gap-2 justify-center"
(when special-price (div :class "text-lg font-semibold text-emerald-700" special-price))
(when (and special-price regular-price) (div :class "text-sm line-through text-stone-500" regular-price))
(when (and (not special-price) regular-price) (div :class "mt-1 text-lg font-semibold" regular-price))))
;; Main product card — accepts pure data, composes sub-components
(defcomp ~market-product-card (&key href hx-select
has-like liked slug csrf like-action
image labels brand brand-highlight
special-price regular-price
cart-action quantity cart-href
stickers
title has-highlight search-pre search-mid search-post)
(div :class "flex flex-col rounded-xl bg-white shadow hover:shadow-md transition overflow-hidden relative"
(raw! like-html)
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(raw! image-html) (raw! price-html))
(div :class "flex justify-center" (raw! add-html))
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(raw! stickers-html)
(when has-like
(~market-like-button :form-id (str "like-" slug) :action like-action :slug slug :csrf csrf
:icon-cls (if liked "fa-solid fa-heart text-red-500" "fa-regular fa-heart text-stone-400")))
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
(if image
(~market-card-image :image image :labels labels :brand brand :brand-highlight brand-highlight)
(~market-card-no-image :labels labels :brand brand))
(~market-card-price :special-price special-price :regular-price regular-price))
(div :class "flex justify-center"
(if quantity
(~market-cart-add-quantity :cart-id (str "cart-" slug) :action cart-action :csrf csrf
:minus-val (str (- quantity 1)) :plus-val (str (+ quantity 1))
:quantity (str quantity) :cart-href cart-href)
(~market-cart-add-empty :cart-id (str "cart-" slug) :action cart-action :csrf csrf)))
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
(when stickers (~market-card-stickers :stickers stickers))
(div :class "text-sm font-medium text-stone-800 text-center line-clamp-3 break-words [overflow-wrap:anywhere]"
(raw! title-html)))))
(if has-highlight
(~market-card-highlight :pre search-pre :mid search-mid :post search-post)
title)))))
(defcomp ~market-like-button (&key form-id action slug csrf icon-cls)
(div :class "absolute top-2 right-2 z-10 text-6xl md:text-xl"
(form :id form-id :action action :method "post"
:hx-post action :hx-target (str "#like-" slug) :hx-swap "outerHTML"
:sx-post action :sx-target (str "#like-" slug) :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf)
(button :type "submit" :class "cursor-pointer"
(i :class icon-cls :aria-hidden "true")))))
@@ -70,16 +97,18 @@
(a :href href :class "inline-block px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 hover:bg-amber-200"
title)))
(defcomp ~market-market-card (&key title-html desc-html badge-html)
(defcomp ~market-market-card (&key title-content desc-content badge-content title desc badge)
(article :class "rounded-xl bg-white shadow-sm border border-stone-200 p-5 flex flex-col justify-between hover:border-stone-400 transition-colors"
(div (raw! title-html) (raw! desc-html))
(raw! badge-html)))
(div
(if title-content title-content (when title title))
(if desc-content desc-content (when desc desc)))
(if badge-content badge-content (when badge badge))))
(defcomp ~market-sentinel-mobile (&key id next-url hyperscript)
(div :id id
:class "block md:hidden h-[60vh] opacity-0 pointer-events-none js-mobile-sentinel"
:hx-get next-url :hx-trigger "intersect once delay:250ms, sentinelmobile:retry"
:hx-swap "outerHTML"
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinelmobile:retry"
:sx-swap "outerHTML"
:_ hyperscript
:role "status" :aria-live "polite" :aria-hidden "true"
(div :class "js-loading text-center text-xs text-stone-400" "loading...")
@@ -88,8 +117,8 @@
(defcomp ~market-sentinel-desktop (&key id next-url hyperscript)
(div :id id
:class "hidden md:block h-4 opacity-0 pointer-events-none"
:hx-get next-url :hx-trigger "intersect once delay:250ms, sentinel:retry"
:hx-swap "outerHTML"
:sx-get next-url :sx-trigger "intersect once delay:250ms, sentinel:retry"
:sx-swap "outerHTML"
:_ hyperscript
:role "status" :aria-live "polite" :aria-hidden "true"
(div :class "js-loading text-center text-xs text-stone-400" "loading...")
@@ -100,6 +129,6 @@
(defcomp ~market-market-sentinel (&key id next-url)
(div :id id :class "h-4 opacity-0 pointer-events-none"
:hx-get next-url :hx-trigger "intersect once delay:250ms"
:hx-swap "outerHTML" :role "status" :aria-hidden "true"
:sx-get next-url :sx-trigger "intersect once delay:250ms"
:sx-swap "outerHTML" :role "status" :aria-hidden "true"
(div :class "text-center text-xs text-stone-400" "loading...")))

View File

@@ -2,7 +2,7 @@
(defcomp ~market-cart-add-empty (&key cart-id action csrf)
(div :id cart-id
(form :action action :method "post" :hx-post action :hx-target "#cart-mini" :hx-swap "outerHTML" :class "rounded flex items-center"
(form :action action :method "post" :sx-post action :sx-target "#cart-mini" :sx-swap "outerHTML" :class "rounded flex items-center"
(input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "count" :value "1")
(button :type "submit" :class "relative inline-flex items-center justify-center text-sm font-medium text-stone-500 hover:bg-emerald-50"
@@ -12,7 +12,7 @@
(defcomp ~market-cart-add-quantity (&key cart-id action csrf minus-val plus-val quantity cart-href)
(div :id cart-id
(div :class "rounded flex items-center gap-2"
(form :action action :method "post" :hx-post action :hx-target "#cart-mini" :hx-swap "outerHTML"
(form :action action :method "post" :sx-post action :sx-target "#cart-mini" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "count" :value minus-val)
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "-"))
@@ -21,13 +21,13 @@
(i :class "fa-solid fa-shopping-cart text-2xl" :aria-hidden "true")
(span :class "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none"
(span :class "flex items-center justify-center bg-black text-white rounded-full w-4 h-4 text-xs font-bold" quantity))))
(form :action action :method "post" :hx-post action :hx-target "#cart-mini" :hx-swap "outerHTML"
(form :action action :method "post" :sx-post action :sx-target "#cart-mini" :sx-swap "outerHTML"
(input :type "hidden" :name "csrf_token" :value csrf)
(input :type "hidden" :name "count" :value plus-val)
(button :type "submit" :class "inline-flex items-center justify-center w-8 h-8 text-sm font-medium rounded-full border border-emerald-600 text-emerald-700 hover:bg-emerald-50 text-xl" "+")))))
(defcomp ~market-cart-mini-count (&key href count)
(div :id "cart-mini" :hx-swap-oob "outerHTML"
(div :id "cart-mini" :sx-swap-oob "outerHTML"
(a :href href :class "relative inline-flex items-center justify-center"
(span :class "relative inline-flex items-center justify-center"
(i :class "fa-solid fa-shopping-cart text-xl" :aria-hidden "true")
@@ -36,9 +36,10 @@
count))))))
(defcomp ~market-cart-mini-empty (&key href logo)
(div :id "cart-mini" :hx-swap-oob "outerHTML"
(div :id "cart-mini" :sx-swap-oob "outerHTML"
(a :href href :class "relative inline-flex items-center justify-center"
(img :src logo :class "h-8 w-8 rounded-full object-cover border border-stone-300" :alt ""))))
(defcomp ~market-cart-add-oob (&key id inner-html)
(div :id id :hx-swap-oob "outerHTML" (raw! inner-html)))
(defcomp ~market-cart-add-oob (&key id content inner)
(div :id id :sx-swap-oob "outerHTML"
(if content content (when inner inner))))

View File

@@ -1,12 +1,12 @@
;; Market product detail components
(defcomp ~market-detail-gallery-inner (&key like-html image alt labels-html brand)
(<> (raw! like-html)
(defcomp ~market-detail-gallery-inner (&key like image alt labels brand)
(<> like
(figure :class "inline-block"
(div :class "relative w-full aspect-square"
(img :data-main-img "" :src image :alt alt
:class "w-full h-full object-contain object-top" :loading "eager" :decoding "async")
(raw! labels-html))
labels)
(figcaption :class "mt-2 text-sm text-stone-600 text-center" brand))))
(defcomp ~market-detail-nav-buttons ()
@@ -18,9 +18,9 @@
:class "absolute right-2 top-1/2 -translate-y-1/2 z-10 grid place-items-center w-12 h-12 md:w-14 md:h-14 rounded-full bg-white/90 hover:bg-white shadow-lg text-3xl md:text-4xl"
:title "Next" "\u203a")))
(defcomp ~market-detail-gallery (&key inner-html nav-html)
(defcomp ~market-detail-gallery (&key inner nav)
(div :class "relative rounded-xl overflow-hidden bg-stone-100"
(raw! inner-html) (raw! nav-html)))
inner nav))
(defcomp ~market-detail-thumb (&key title src alt)
(<> (button :type "button" :data-thumb ""
@@ -29,19 +29,19 @@
(img :src src :class "h-16 w-16 object-contain" :alt alt :loading "lazy" :decoding "async"))
(span :data-image-src src :class "hidden")))
(defcomp ~market-detail-thumbs (&key thumbs-html)
(defcomp ~market-detail-thumbs (&key thumbs)
(div :class "flex flex-row justify-center"
(div :class "mt-3 flex gap-2 overflow-x-auto no-scrollbar" (raw! thumbs-html))))
(div :class "mt-3 flex gap-2 overflow-x-auto no-scrollbar" thumbs)))
(defcomp ~market-detail-no-image (&key like-html)
(defcomp ~market-detail-no-image (&key like)
(div :class "relative aspect-square bg-stone-100 rounded-xl flex items-center justify-center text-stone-400"
(raw! like-html) "No image"))
like "No image"))
(defcomp ~market-detail-sticker (&key src name)
(img :src src :alt name :class "w-10 h-10"))
(defcomp ~market-detail-stickers (&key items-html)
(div :class "p-2 flex flex-row justify-center gap-2" (raw! items-html)))
(defcomp ~market-detail-stickers (&key items)
(div :class "p-2 flex flex-row justify-center gap-2" items))
(defcomp ~market-detail-unit-price (&key price)
(div (str "Unit price: " price)))
@@ -49,35 +49,35 @@
(defcomp ~market-detail-case-size (&key size)
(div (str "Case size: " size)))
(defcomp ~market-detail-extras (&key inner-html)
(div :class "mt-2 space-y-1 text-sm text-stone-600" (raw! inner-html)))
(defcomp ~market-detail-extras (&key inner)
(div :class "mt-2 space-y-1 text-sm text-stone-600" inner))
(defcomp ~market-detail-desc-short (&key text)
(p :class "leading-relaxed text-lg" text))
(defcomp ~market-detail-desc-html (&key html)
(div :class "max-w-none text-sm leading-relaxed" (raw! html)))
(div :class "max-w-none text-sm leading-relaxed" (~rich-text :html html)))
(defcomp ~market-detail-desc-wrapper (&key inner-html)
(div :class "mt-4 text-stone-800 space-y-3" (raw! inner-html)))
(defcomp ~market-detail-desc-wrapper (&key inner)
(div :class "mt-4 text-stone-800 space-y-3" inner))
(defcomp ~market-detail-section (&key title html)
(details :class "group rounded-xl border bg-white shadow-sm open:shadow p-0"
(summary :class "cursor-pointer select-none px-4 py-3 flex items-center justify-between"
(span :class "font-medium" title)
(span :class "ml-2 text-xl transition-transform group-open:rotate-180" "\u2304"))
(div :class "px-4 pb-4 max-w-none text-sm leading-relaxed" (raw! html))))
(div :class "px-4 pb-4 max-w-none text-sm leading-relaxed" (~rich-text :html html))))
(defcomp ~market-detail-sections (&key items-html)
(div :class "mt-8 space-y-3" (raw! items-html)))
(defcomp ~market-detail-sections (&key items)
(div :class "mt-8 space-y-3" items))
(defcomp ~market-detail-right-col (&key inner-html)
(div :class "md:col-span-3" (raw! inner-html)))
(defcomp ~market-detail-right-col (&key inner)
(div :class "md:col-span-3" inner))
(defcomp ~market-detail-layout (&key gallery-html stickers-html details-html)
(defcomp ~market-detail-layout (&key gallery stickers details)
(<> (div :class "mt-3 grid grid-cols-1 md:grid-cols-5 gap-6" :data-gallery-root ""
(div :class "md:col-span-2" (raw! gallery-html) (raw! stickers-html))
(raw! details-html))
(div :class "md:col-span-2" gallery stickers)
details)
(div :class "pb-8")))
(defcomp ~market-landing-excerpt (&key text)
@@ -88,7 +88,7 @@
(img :src src :alt "" :class "rounded-lg w-full md:w-3/4 object-cover")))
(defcomp ~market-landing-html (&key html)
(div :class "blog-content p-2" (raw! html)))
(div :class "blog-content p-2" (~rich-text :html html)))
(defcomp ~market-landing-content (&key inner-html)
(<> (article :class "relative w-full" (raw! inner-html)) (div :class "pb-8")))
(defcomp ~market-landing-content (&key inner)
(<> (article :class "relative w-full" inner) (div :class "pb-8")))

View File

@@ -1,90 +1,94 @@
;; Market filter components
(defcomp ~market-filter-sort-item (&key href hx-select ring-cls src label)
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls)
(img :src src :alt label :class "w-10 h-10")
(span :class "text-xs" label)))
(defcomp ~market-filter-sort-row (&key items-html)
(div :class "flex flex-row gap-2 justify-center p-1" (raw! items-html)))
(defcomp ~market-filter-sort-row (&key items)
(div :class "flex flex-row gap-2 justify-center p-1"
items))
(defcomp ~market-filter-like (&key href hx-select icon-cls size-cls)
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class "flex flex-col items-center gap-1 p-1 cursor-pointer"
(i :aria-hidden "true" :class (str icon-cls " " size-cls " leading-none"))))
(defcomp ~market-filter-label-item (&key href hx-select ring-cls src name)
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls)
(img :src src :alt name :class "w-10 h-10")))
(defcomp ~market-filter-sticker-item (&key href hx-select ring-cls src name count-cls count)
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "flex flex-col items-center gap-1 p-1 cursor-pointer" ring-cls)
(img :src src :alt name :class "w-6 h-6")
(span :class count-cls count)))
(defcomp ~market-filter-stickers-row (&key items-html)
(div :class "flex flex-wrap gap-2 justify-center p-1" (raw! items-html)))
(defcomp ~market-filter-stickers-row (&key items)
(div :class "flex flex-wrap gap-2 justify-center p-1"
items))
(defcomp ~market-filter-brand-item (&key href hx-select bg-cls name-cls name count)
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "flex flex-row items-center gap-2 px-2 py-1 rounded hover:bg-stone-100" bg-cls)
(div :class name-cls name) (div :class name-cls count)))
(defcomp ~market-filter-brands-panel (&key items-html)
(div :class "space-y-1 p-2" (raw! items-html)))
(defcomp ~market-filter-brands-panel (&key items)
(div :class "space-y-1 p-2"
items))
(defcomp ~market-filter-category-label (&key label)
(div :class "mb-4" (div :class "text-2xl uppercase tracking-wide text-black-500" label)))
(defcomp ~market-filter-like-labels-nav (&key inner-html)
(defcomp ~market-filter-like-labels-nav (&key content inner)
(nav :aria-label "like" :class "flex flex-row justify-center w-full p-0 m-0 border bg-white shadow-sm rounded-xl gap-1"
(raw! inner-html)))
(if content content (when inner inner))))
(defcomp ~market-desktop-category-summary (&key inner-html)
(div :id "category-summary-desktop" :hxx-swap-oob "outerHTML" (raw! inner-html)))
(defcomp ~market-desktop-category-summary (&key content inner)
(div :id "category-summary-desktop" :hxx-swap-oob "outerHTML"
(if content content (when inner inner))))
(defcomp ~market-desktop-brand-summary (&key inner-html)
(div :id "filter-summary-desktop" :hxx-swap-oob "outerHTML" (raw! inner-html)))
(defcomp ~market-desktop-brand-summary (&key inner)
(div :id "filter-summary-desktop" :hxx-swap-oob "outerHTML" inner))
(defcomp ~market-filter-subcategory-item (&key href hx-select active-cls name)
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "block px-2 py-1 rounded hover:bg-stone-100" active-cls)
name))
(defcomp ~market-filter-subcategory-panel (&key items-html)
(div :class "mt-4 space-y-1" (raw! items-html)))
(defcomp ~market-filter-subcategory-panel (&key items)
(div :class "mt-4 space-y-1" items))
(defcomp ~market-mobile-clear-filters (&key href hx-select)
(div :class "flex flex-row justify-center"
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:role "button" :title "clear filters" :aria-label "clear filters"
:class "flex flex-col items-center justify-start p-1 rounded bg-stone-200 text-black cursor-pointer"
(span :class "mt-1 leading-none tabular-nums" "clear filters"))))
(defcomp ~market-mobile-like-labels-row (&key inner-html)
(div :class "flex flex-row gap-2 justify-center items-center" (raw! inner-html)))
(defcomp ~market-mobile-like-labels-row (&key inner)
(div :class "flex flex-row gap-2 justify-center items-center" inner))
(defcomp ~market-mobile-filter-summary (&key search-bar chips-html filter-html)
(defcomp ~market-mobile-filter-summary (&key search-bar chips filter)
(details :class "md:hidden group" :id "/filter"
(summary :class "cursor-pointer select-none" :id "filter-summary-mobile"
(raw! search-bar)
search-bar
(div :class "col-span-12 min-w-0 grid grid-cols-1 gap-1 bg-gray-100 px-2" :role "list"
(raw! chips-html)))
chips))
(div :id "filter-details-mobile" :style "display:contents"
(raw! filter-html))))
filter)))
(defcomp ~market-mobile-chips-row (&key inner-html)
(div :class "flex flex-row items-start gap-2" (raw! inner-html)))
(defcomp ~market-mobile-chips-row (&key inner)
(div :class "flex flex-row items-start gap-2" inner))
(defcomp ~market-mobile-chip-sort (&key src label)
(ul :class "relative inline-flex items-center justify-center gap-2"
@@ -96,17 +100,17 @@
(defcomp ~market-mobile-chip-count (&key cls count)
(div :class (str cls " mt-1 leading-none tabular-nums") count))
(defcomp ~market-mobile-chip-liked (&key inner-html)
(div :class "flex flex-col items-center gap-1 pb-1" (raw! inner-html)))
(defcomp ~market-mobile-chip-liked (&key inner)
(div :class "flex flex-col items-center gap-1 pb-1" inner))
(defcomp ~market-mobile-chip-image (&key src name)
(img :src src :alt name :class "w-10 h-10"))
(defcomp ~market-mobile-chip-item (&key inner-html)
(li :role "listitem" :class "flex flex-col items-center gap-1 pb-1" (raw! inner-html)))
(defcomp ~market-mobile-chip-item (&key inner)
(li :role "listitem" :class "flex flex-col items-center gap-1 pb-1" inner))
(defcomp ~market-mobile-chip-list (&key items-html)
(ul :class "relative inline-flex items-center justify-center gap-2" (raw! items-html)))
(defcomp ~market-mobile-chip-list (&key items)
(ul :class "relative inline-flex items-center justify-center gap-2" items))
(defcomp ~market-mobile-chip-brand (&key name count)
(li :role "listitem" :class "flex flex-row items-center gap-2"
@@ -116,5 +120,5 @@
(li :role "listitem" :class "flex flex-row items-center gap-2"
(div :class "text-md text-red-500" name) (div :class "text-xl text-red-500" "0")))
(defcomp ~market-mobile-chip-brand-list (&key items-html)
(ul (raw! items-html)))
(defcomp ~market-mobile-chip-brand-list (&key items)
(ul items))

View File

@@ -1,22 +1,22 @@
;; Market grid and layout components
(defcomp ~market-markets-grid (&key cards-html)
(div :class "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" (raw! cards-html)))
(defcomp ~market-markets-grid (&key cards)
(div :class "max-w-full px-3 py-3 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4" cards))
(defcomp ~market-no-markets (&key message)
(div :class "px-3 py-12 text-center text-stone-400"
(i :class "fa fa-store text-4xl mb-3" :aria-hidden "true")
(p :class "text-lg" message)))
(defcomp ~market-product-grid (&key cards-html)
(<> (div :class "grid grid-cols-1 sm:grid-cols-3 md:grid-cols-6 gap-3" (raw! cards-html)) (div :class "pb-8")))
(defcomp ~market-product-grid (&key cards)
(<> (div :class "grid grid-cols-1 sm:grid-cols-3 md:grid-cols-6 gap-3" cards) (div :class "pb-8")))
(defcomp ~market-bottom-spacer ()
(div :class "pb-8"))
(defcomp ~market-like-toggle-button (&key colour action hx-headers label icon-cls)
(button :class (str "flex items-center gap-1 " colour " hover:text-red-600 transition-colors w-[1em] h-[1em]")
:hx-post action :hx-target "this" :hx-swap "outerHTML" :hx-push-url "false"
:hx-headers hx-headers
:hx-swap-settle "0ms" :aria-label label
:sx-post action :sx-target "this" :sx-swap "outerHTML" :sx-push-url "false"
:sx-headers hx-headers
:sx-swap-settle "0ms" :aria-label label
(i :aria-hidden "true" :class icon-cls)))

View File

@@ -1,10 +1,10 @@
;; Market header components
(defcomp ~market-shop-label (&key title top-slug sub-div-html)
(defcomp ~market-shop-label (&key title top-slug sub-div)
(div :class "font-bold text-xl flex-shrink-0 flex gap-2 items-center"
(div (i :class "fa fa-shop") " " title)
(div :class "flex flex-col md:flex-row md:gap-2 text-xs"
(div top-slug) (raw! sub-div-html))))
(div top-slug) sub-div)))
(defcomp ~market-sub-slug (&key sub)
(div sub))
@@ -13,8 +13,8 @@
(<> (i :class "fa fa-shopping-bag" :aria-hidden "true") (div title)))
(defcomp ~market-admin-link (&key href hx-select)
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class "px-2 py-1 text-stone-500 hover:text-stone-700"
(i :class "fa fa-cog" :aria-hidden "true")))

View File

@@ -16,4 +16,4 @@
(meta :name name :content content))
(defcomp ~market-meta-jsonld (&key json)
(script :type "application/ld+json" (raw! json)))
(script :type "application/ld+json" (~rich-text :html json)))

View File

@@ -2,22 +2,22 @@
(defcomp ~market-category-link (&key href hx-select active select-colours label)
(div :class "relative nav-group"
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:aria-selected (if active "true" "false")
:class (str "block px-2 py-1 rounded text-center whitespace-normal break-words leading-snug bg-stone-200 text-black " select-colours)
label)))
(defcomp ~market-desktop-category-nav (&key links-html admin-html)
(defcomp ~market-desktop-category-nav (&key links admin)
(nav :class "hidden md:flex gap-4 text-sm ml-2 w-full justify-end items-center"
(raw! links-html) (raw! admin-html)))
links admin))
(defcomp ~market-mobile-nav-wrapper (&key items-html)
(div :class "px-4 py-2" (div :class "divide-y" (raw! items-html))))
(defcomp ~market-mobile-nav-wrapper (&key items)
(div :class "px-4 py-2" (div :class "divide-y" items)))
(defcomp ~market-mobile-all-link (&key href hx-select active select-colours)
(a :role "option" :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :role "option" :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:aria-selected (if active "true" "false")
:class (str "block rounded-lg px-3 py-3 text-base hover:bg-stone-50 " select-colours)
(div :class "prose prose-stone max-w-none" "All")))
@@ -28,36 +28,36 @@
(path :fill-rule "evenodd" :clip-rule "evenodd"
:d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z")))
(defcomp ~market-mobile-cat-summary (&key bg-cls href hx-select select-colours cat-name count-label count-str chevron-html)
(defcomp ~market-mobile-cat-summary (&key bg-cls href hx-select select-colours cat-name count-label count-str chevron)
(summary :class (str "flex items-center justify-between cursor-pointer select-none block rounded-lg px-3 py-3 text-base hover:bg-stone-50" bg-cls)
(a :href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
(a :href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
:class (str "font-medium " select-colours " flex flex-row gap-2")
(div cat-name)
(div :aria-label count-label count-str))
(raw! chevron-html)))
chevron))
(defcomp ~market-mobile-sub-link (&key select-colours active href hx-select label count-label count-str)
(a :class (str "snap-start px-2 py-3 rounded " select-colours " flex flex-row gap-2")
:aria-selected (if active "true" "false")
:href href :hx-get href :hx-target "#main-panel"
:hx-select hx-select :hx-swap "outerHTML" :hx-push-url "true"
:href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
(div label)
(div :aria-label count-label count-str)))
(defcomp ~market-mobile-subs-panel (&key links-html)
(defcomp ~market-mobile-subs-panel (&key links)
(div :class "pb-3 pl-2"
(div :data-peek-viewport "" :data-peek-size-px "18" :data-peek-edge "bottom" :data-peek-mask "true" :class "m-2 bg-stone-100"
(div :data-peek-inner "" :class "grid grid-cols-1 gap-1 snap-y snap-mandatory pr-1" :aria-label "Subcategories"
(raw! links-html)))))
links))))
(defcomp ~market-mobile-view-all (&key href hx-select)
(div :class "pb-3 pl-2"
(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 :hx-swap "outerHTML" :hx-push-url "true"
:href href :sx-get href :sx-target "#main-panel"
:sx-select hx-select :sx-swap "outerHTML" :sx-push-url "true"
"View all")))
(defcomp ~market-mobile-cat-details (&key open summary-html subs-html)
(defcomp ~market-mobile-cat-details (&key open summary subs)
(details :class "group/cat py-1" :open open
(raw! summary-html) (raw! subs-html)))
summary subs))

View File

@@ -9,8 +9,8 @@
(defcomp ~market-price-regular (&key price)
(div :class "mt-1 text-lg font-semibold" price))
(defcomp ~market-price-line (&key inner-html)
(div :class "mt-1 flex items-baseline gap-2 justify-center" (raw! inner-html)))
(defcomp ~market-price-line (&key inner)
(div :class "mt-1 flex items-baseline gap-2 justify-center" inner))
(defcomp ~market-header-price-special-label ()
(div :class "text-md font-bold text-emerald-700" "Special price"))
@@ -30,5 +30,5 @@
(defcomp ~market-header-rrp (&key rrp)
(div :class "text-base text-stone-400" (span "rrp:") " " (span rrp)))
(defcomp ~market-prices-row (&key inner-html)
(div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" (raw! inner-html)))
(defcomp ~market-prices-row (&key inner)
(div :class "flex flex-row items-center justify-between md:gap-2 md:px-2" inner))

File diff suppressed because it is too large Load Diff

View File

@@ -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"
>

View File

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

View File

@@ -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"

View File

@@ -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}}"
>

View File

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

View File

@@ -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 }}"

View File

@@ -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"

View File

@@ -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"
>

View File

@@ -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"

View File

@@ -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 }}"

View File

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

View File

@@ -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 %}">

View File

@@ -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"

View File

@@ -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 }}"

View File

@@ -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"

View File

@@ -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"
>

View File

@@ -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>

View File

@@ -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 }}"

View File

@@ -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}}"
>

View File

@@ -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>

View File

@@ -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"
>

View File

@@ -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>

View File

@@ -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

View File

@@ -1,6 +1,6 @@
<aside
id="aside"
hx-swap-oob="outerHTML"
sx-swap-oob="outerHTML"
class="hidden"
>
</aside>

View File

@@ -1,5 +1,5 @@
<div
id="filter"
hx-swap-oob="outerHTML"
sx-swap-oob="outerHTML"
>
</div>

View File

@@ -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 }}"