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

@@ -20,9 +20,9 @@
<div
id="sentinel-{{ page }}"
class="h-4 opacity-0 pointer-events-none"
hx-get="{{ entries_url }}"
hx-trigger="intersect once delay:250ms"
hx-swap="outerHTML"
sx-get="{{ entries_url }}"
sx-trigger="intersect once delay:250ms"
sx-swap="outerHTML"
role="status"
aria-hidden="true"
>

View File

@@ -4,14 +4,14 @@
{% set tile_href = (current_local_href ~ {'view': 'tile'}|qs)|host %}
<a
href="{{ list_href }}"
hx-get="{{ list_href }}"
hx-target="#main-panel"
hx-select="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
sx-get="{{ list_href }}"
sx-target="#main-panel"
sx-select="{{hx_select_search}}"
sx-swap="outerHTML"
sx-push-url="true"
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600' }}"
title="List view"
_="on click js localStorage.removeItem('events_view') end"
onclick="localStorage.removeItem('events_view')"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
@@ -19,14 +19,14 @@
</a>
<a
href="{{ tile_href }}"
hx-get="{{ tile_href }}"
hx-target="#main-panel"
hx-select="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
sx-get="{{ tile_href }}"
sx-target="#main-panel"
sx-select="{{hx_select_search}}"
sx-swap="outerHTML"
sx-push-url="true"
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600' }}"
title="Tile view"
_="on click js localStorage.setItem('events_view','tile') end"
onclick="localStorage.setItem('events_view','tile')"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />

View File

@@ -2,7 +2,7 @@
<div
id="calendar-description-title"
{% if oob %}
hx-swap-oob="outerHTML"
sx-swap-oob="outerHTML"
{% endif %}
class="text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"
>

View File

@@ -10,14 +10,14 @@
calendar_slug=calendar.slug,
year=prev_year,
month=month) }}"
hx-get="{{ url_for('calendar.get',
sx-get="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=prev_year,
month=month) }}"
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"
>
&laquo;
</a>
@@ -29,14 +29,14 @@
calendar_slug=calendar.slug,
year=prev_month_year,
month=prev_month) }}"
hx-get="{{ url_for('calendar.get',
sx-get="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=prev_month_year,
month=prev_month) }}"
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"
>
&lsaquo;
</a>
@@ -52,14 +52,14 @@
calendar_slug=calendar.slug,
year=next_month_year,
month=next_month) }}"
hx-get="{{ url_for('calendar.get',
sx-get="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=next_month_year,
month=next_month) }}"
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"
>
&rsaquo;
</a>
@@ -71,14 +71,14 @@
calendar_slug=calendar.slug,
year=next_year,
month=month) }}"
hx-get="{{ url_for('calendar.get',
sx-get="{{ url_for('calendar.get',
calendar_slug=calendar.slug,
year=next_year,
month=month) }}"
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"
>
&raquo;
</a>
@@ -115,15 +115,15 @@
year=day.date.year,
month=day.date.month,
day=day.date.day) }}"
hx-get="{{ url_for('calendar.day.show_day',
sx-get="{{ url_for('calendar.day.show_day',
calendar_slug=calendar.slug,
year=day.date.year,
month=day.date.month,
day=day.date.day) }}"
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"
>
{{ day.date.day }}
</a>

View File

@@ -12,12 +12,12 @@
<button
type="button"
class="mt-2 text-xs underline"
hx-get="{{ url_for(
sx-get="{{ url_for(
'calendar.admin.calendar_description_edit',
calendar_slug=calendar.slug,
) }}"
hx-target="#calendar-description"
hx-swap="outerHTML"
sx-target="#calendar-description"
sx-swap="outerHTML"
>
<i class="fas fa-edit"></i>
</button>

View File

@@ -1,11 +1,11 @@
<div id="calendar-description">
<form
hx-post="{{ url_for(
sx-post="{{ url_for(
'calendar.admin.calendar_description_save',
calendar_slug=calendar.slug,
) }}"
hx-target="#calendar-description"
hx-swap="outerHTML"
sx-target="#calendar-description"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
@@ -27,12 +27,12 @@
<button
type="button"
class="px-3 py-1 rounded border"
hx-get="{{ url_for(
sx-get="{{ url_for(
'calendar.admin.calendar_description_view',
calendar_slug=calendar.slug,
) }}"
hx-target="#calendar-description"
hx-swap="outerHTML"
sx-target="#calendar-description"
sx-swap="outerHTML"
>
Cancel
</button>

View File

@@ -14,11 +14,11 @@
<form
id="calendar-form"
method="post"
hx-target="#main-panel"
hx-select="{{ hx_select_search }}"
hx-on::before-request="document.querySelector('#cal-put-errors').textContent='';"
hx-on::response-error="document.querySelector('#cal-put-errors').innerHTML = event.detail.xhr.responseText;"
hx-on::after-request="if (event.detail.successful) this.reset()"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-on:beforeRequest="document.querySelector('#cal-put-errors').textContent='';"
sx-on:responseError="if(event.detail.response){event.detail.response.clone().text().then(function(t){document.querySelector('#cal-put-errors').innerHTML=t})}"
sx-on:afterRequest="if (event.detail.successful) this.reset()"
class="hidden space-y-4 mt-4"
autocomplete="off"

View File

@@ -7,11 +7,11 @@
<a
class="flex items-baseline gap-3"
href="{{ calendar_href }}"
hx-get="{{ calendar_href }}"
hx-target="#main-panel"
hx-select ="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
sx-get="{{ calendar_href }}"
sx-target="#main-panel"
sx-select ="{{hx_select_search}}"
sx-swap="outerHTML"
sx-push-url="true"
>
<h3 class="font-semibold">{{ cal.name }}</h3>
<h4 class="text-gray-500">/{{ cal.slug }}/</h4>
@@ -27,12 +27,12 @@
data-confirm-confirm-text="Yes, delete it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
hx-delete="{{ url_for('calendar.delete', calendar_slug=cal.slug) }}"
hx-trigger="confirmed"
hx-target="#calendars-list"
hx-select="#calendars-list"
hx-swap="outerHTML"
hx-headers='{"X-CSRFToken":"{{ csrf_token() }}"}'
sx-delete="{{ url_for('calendar.delete', calendar_slug=cal.slug) }}"
sx-trigger="confirmed"
sx-target="#calendars-list"
sx-select="#calendars-list"
sx-swap="outerHTML"
sx-headers='{"X-CSRFToken":"{{ csrf_token() }}"}'
>
<i class="fa-solid fa-trash"></i>
</button>

View File

@@ -5,12 +5,12 @@
<form
class="mt-4 flex gap-2 items-end"
hx-post="{{ url_for('calendars.create_calendar') }}"
hx-target="#calendars-list"
hx-select="#calendars-list"
hx-swap="outerHTML"
hx-on::before-request="document.querySelector('#cal-create-errors').textContent='';"
hx-on::response-error="document.querySelector('#cal-create-errors').innerHTML = event.detail.xhr.responseText;"
sx-post="{{ url_for('calendars.create_calendar') }}"
sx-target="#calendars-list"
sx-select="#calendars-list"
sx-swap="outerHTML"
sx-on:beforeRequest="document.querySelector('#cal-create-errors').textContent='';"
sx-on:responseError="if(event.detail.response){event.detail.response.clone().text().then(function(t){document.querySelector('#cal-create-errors').innerHTML=t})}"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="flex-1">

View File

@@ -2,16 +2,16 @@
<form
class="mt-4 grid grid-cols-1 md:grid-cols-4 gap-2"
hx-post="{{ url_for(
sx-post="{{ url_for(
'calendar.day.calendar_entries.add_entry',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
hx-target="#day-entries"
hx-on::after-request="if (event.detail.successful) this.reset()"
hx-swap="innerHTML"
sx-target="#day-entries"
sx-on:afterRequest="if (event.detail.successful) this.reset()"
sx-swap="innerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
@@ -123,14 +123,14 @@
<button
type="button"
class="{{styles.cancel_button}}"
hx-get="{{ url_for('calendar.day.calendar_entries.add_button',
sx-get="{{ url_for('calendar.day.calendar_entries.add_button',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
hx-target="#entry-add-container"
hx-swap="innerHTML"
sx-target="#entry-add-container"
sx-swap="innerHTML"
>
Cancel
</button>

View File

@@ -2,15 +2,15 @@
<button
type="button"
class="{{styles.pre_action_button}}"
hx-get="{{ url_for(
sx-get="{{ url_for(
'calendar.day.calendar_entries.add_form',
calendar_slug=calendar.slug,
day=day,
month=month,
year=year,
) }}"
hx-target="#entry-add-container"
hx-swap="innerHTML"
sx-target="#entry-add-container"
sx-swap="innerHTML"
>
+ Add entry
</button>

View File

@@ -5,7 +5,7 @@
{% if confirmed_entries %}
<div class="flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
id="day-entries-nav-wrapper"
hx-swap-oob="true">
sx-swap-oob="true">
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
{% call(entry) scrolling_menu('day-entries-container', confirmed_entries) %}
<a
@@ -29,5 +29,5 @@
</div>
{% else %}
{# Empty placeholder to remove nav entries when none are confirmed #}
<div id="day-entries-nav-wrapper" hx-swap-oob="true"></div>
<div id="day-entries-nav-wrapper" sx-swap-oob="true"></div>
{% endif %}

View File

@@ -6,14 +6,14 @@
<form
class="space-y-3 mt-4"
hx-put="{{ url_for(
sx-put="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.put',
calendar_slug=calendar.slug,
day=day, month=month, year=year,
entry_id=entry.id
) }}"
hx-target="#entry-{{ entry.id }}"
hx-swap="outerHTML"
sx-target="#entry-{{ entry.id }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
@@ -161,14 +161,14 @@
<button
type="button"
class="{{ styles.cancel_button }}"
hx-get="{{ url_for(
sx-get="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.get',
calendar_slug=calendar.slug,
day=day, month=month, year=year,
entry_id=entry.id
) }}"
hx-target="#entry-{{ entry.id }}"
hx-swap="outerHTML"
sx-target="#entry-{{ entry.id }}"
sx-swap="outerHTML"
>
Cancel
</button>

View File

@@ -110,7 +110,7 @@
<button
type="button"
class="{{styles.pre_action_button}}"
hx-get="{{ url_for(
sx-get="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.get_edit',
entry_id=entry.id,
calendar_slug=calendar.slug,
@@ -118,8 +118,8 @@
month=month,
year=year,
) }}"
hx-target="#entry-{{entry.id}}"
hx-swap="outerHTML"
sx-target="#entry-{{entry.id}}"
sx-swap="outerHTML"
>
Edit
</button>

View File

@@ -1,9 +1,9 @@
{% include '_types/entry/_options.html' %}
<div id="entry-title-{{entry.id}}" hx-swap-oob="innerHTML">
<div id="entry-title-{{entry.id}}" sx-swap-oob="innerHTML">
{% include '_types/entry/_title.html' %}
</div>
<div id="entry-state-{{entry.id}}" hx-swap-oob="innerHTML">
<div id="entry-state-{{entry.id}}" sx-swap-oob="innerHTML">
{% include '_types/entry/_state.html' %}
</div>

View File

@@ -1,7 +1,7 @@
<div id="calendar_entry_options_{{ entry.id }}" class="flex flex-col md:flex-row gap-1">
{% if entry.state == 'provisional' %}
<form
hx-post="{{ url_for(
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.confirm_entry',
calendar_slug=calendar.slug,
day=day,
@@ -9,9 +9,9 @@
year=year,
entry_id=entry.id
) }}"
hx-select="#calendar_entry_options_{{ entry.id }}"
hx-target="#calendar_entry_options_{{entry.id}}"
hx-swap="outerHTML"
sx-select="#calendar_entry_options_{{ entry.id }}"
sx-target="#calendar_entry_options_{{entry.id}}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button
@@ -30,7 +30,7 @@
</button>
</form>
<form
hx-post="{{ url_for(
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.decline_entry',
calendar_slug=calendar.slug,
day=day,
@@ -38,9 +38,9 @@
year=year,
entry_id=entry.id
) }}"
hx-select="#calendar_entry_options_{{ entry.id }}"
hx-target="#calendar_entry_options_{{entry.id}}"
hx-swap="outerHTML"
sx-select="#calendar_entry_options_{{ entry.id }}"
sx-target="#calendar_entry_options_{{entry.id}}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button
@@ -61,7 +61,7 @@
{% endif %}
{% if entry.state == 'confirmed' %}
<form
hx-post="{{ url_for(
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.provisional_entry',
calendar_slug=calendar.slug,
day=day,
@@ -69,10 +69,10 @@
year=year,
entry_id=entry.id
) }}"
hx-target="#calendar_entry_options_{{ entry.id }}"
hx-select="#calendar_entry_options_{{ entry.id }}"
hx-swap="outerHTML"
hx-trigger="confirmed"
sx-target="#calendar_entry_options_{{ entry.id }}"
sx-select="#calendar_entry_options_{{ entry.id }}"
sx-swap="outerHTML"
sx-trigger="confirmed"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

View File

@@ -1,6 +1,6 @@
{% for search_post in search_posts %}
<form
hx-post="{{ url_for(
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.add_post',
calendar_slug=calendar.slug,
day=day,
@@ -8,8 +8,8 @@
year=year,
entry_id=entry.id
) }}"
hx-target="#entry-posts-{{entry.id}}"
hx-swap="innerHTML"
sx-target="#entry-posts-{{entry.id}}"
sx-swap="innerHTML"
class="p-2 hover:bg-stone-50 cursor-pointer rounded text-sm border-b"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
@@ -40,7 +40,7 @@
{% if page < total_pages|int %}
<div
id="post-search-sentinel-{{ page }}"
hx-get="{{ url_for(
sx-get="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.search_posts',
calendar_slug=calendar.slug,
day=day,
@@ -50,42 +50,9 @@
q=search_query,
page=page + 1
) }}"
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 sentinel:retry
remove .hidden from .js-loading in me
add .hidden to .js-neterr in me
set me.style.pointerEvents to 'none'
set me.style.opacity to '0'
trigger htmx:consume on me
call htmx.trigger(me, 'intersect')
end
def backoff()
add .hidden to .js-loading in me
remove .hidden from .js-neterr in me
set myMs to Number(me.dataset.retryMs)
if myMs < 10000 then set me.dataset.retryMs to myMs * 2 end
js setTimeout(() => htmx.trigger(me, 'sentinel:retry'), myMs)
end
on htmx:beforeRequest
set me.style.pointerEvents to 'none'
set me.style.opacity to '0'
end
on htmx:afterSwap
set me.dataset.retryMs to 1000
end
on htmx:sendError call backoff()
on htmx:responseError call backoff()
on htmx:timeout call backoff()
"
sx-trigger="intersect once delay:250ms"
sx-swap="outerHTML"
sx-retry="exponential:1000:30000"
role="status"
aria-live="polite"
aria-hidden="true"

View File

@@ -22,7 +22,7 @@
data-confirm-confirm-text="Yes, remove it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
hx-delete="{{ url_for(
sx-delete="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.remove_post',
calendar_slug=calendar.slug,
day=day,
@@ -31,10 +31,10 @@
entry_id=entry.id,
post_id=entry_post.id
) }}"
hx-trigger="confirmed"
hx-target="#entry-posts-{{entry.id}}"
hx-swap="innerHTML"
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
sx-trigger="confirmed"
sx-target="#entry-posts-{{entry.id}}"
sx-swap="innerHTML"
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
>
<i class="fa fa-times"></i> Remove
</button>
@@ -54,7 +54,7 @@
type="text"
placeholder="Search posts..."
class="w-full px-3 py-2 border rounded text-sm"
hx-get="{{ url_for(
sx-get="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.search_posts',
calendar_slug=calendar.slug,
day=day,
@@ -62,9 +62,9 @@
year=year,
entry_id=entry.id
) }}"
hx-trigger="keyup changed delay:300ms, load"
hx-target="#post-search-results-{{entry.id}}"
hx-swap="innerHTML"
sx-trigger="keyup changed delay:300ms, load"
sx-target="#post-search-results-{{entry.id}}"
sx-swap="innerHTML"
name="q"
/>
<div id="post-search-results-{{entry.id}}" class="mt-2 max-h-96 overflow-y-auto border rounded"></div>

View File

@@ -43,7 +43,7 @@
<form
id="ticket-form-{{entry.id}}"
class="{% if entry.ticket_price is not none %}hidden{% endif %} space-y-3 mt-2 p-3 border rounded bg-stone-50"
hx-post="{{ url_for(
sx-post="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.update_tickets',
entry_id=entry.id,
calendar_slug=calendar.slug,
@@ -51,8 +51,8 @@
month=month,
year=year,
) }}"
hx-target="#entry-tickets-{{entry.id}}"
hx-swap="innerHTML"
sx-target="#entry-tickets-{{entry.id}}"
sx-swap="innerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<div>

View File

@@ -5,7 +5,7 @@
{% if entry_posts %}
<div class="flex flex-col sm:flex-row sm:items-center gap-2 border-r border-stone-200 mr-2 sm:max-w-2xl"
id="entry-posts-nav-wrapper"
hx-swap-oob="true">
sx-swap-oob="true">
{% from 'macros/scrolling_menu.html' import scrolling_menu with context %}
{% call(entry_post) scrolling_menu('entry-posts-container', entry_posts) %}
<a
@@ -27,5 +27,5 @@
</div>
{% else %}
{# Empty placeholder to remove nav posts when all are disassociated #}
<div id="entry-posts-nav-wrapper" hx-swap-oob="true"></div>
<div id="entry-posts-nav-wrapper" sx-swap-oob="true"></div>
{% endif %}

View File

@@ -4,12 +4,12 @@
<form
class="mt-4 flex gap-2 items-end"
hx-post="{{ url_for('markets.create_market') }}"
hx-target="#markets-list"
hx-select="#markets-list"
hx-swap="outerHTML"
hx-on::before-request="document.querySelector('#market-create-errors').textContent='';"
hx-on::response-error="document.querySelector('#market-create-errors').innerHTML = event.detail.xhr.responseText;"
sx-post="{{ url_for('markets.create_market') }}"
sx-target="#markets-list"
sx-select="#markets-list"
sx-swap="outerHTML"
sx-on:beforeRequest="document.querySelector('#market-create-errors').textContent='';"
sx-on:responseError="if(event.detail.response){event.detail.response.clone().text().then(function(t){document.querySelector('#market-create-errors').innerHTML=t})}"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="flex-1">

View File

@@ -20,12 +20,12 @@
data-confirm-confirm-text="Yes, delete it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
hx-delete="{{ url_for('markets.delete_market', market_slug=m.slug) }}"
hx-trigger="confirmed"
hx-target="#markets-list"
hx-select="#markets-list"
hx-swap="outerHTML"
hx-headers='{"X-CSRFToken":"{{ csrf_token() }}"}'
sx-delete="{{ url_for('markets.delete_market', market_slug=m.slug) }}"
sx-trigger="confirmed"
sx-target="#markets-list"
sx-select="#markets-list"
sx-swap="outerHTML"
sx-headers='{"X-CSRFToken":"{{ csrf_token() }}"}'
>
<i class="fa-solid fa-trash"></i>
</button>

View File

@@ -20,9 +20,9 @@
<div
id="sentinel-{{ page }}"
class="h-4 opacity-0 pointer-events-none"
hx-get="{{ entries_url }}"
hx-trigger="intersect once delay:250ms"
hx-swap="outerHTML"
sx-get="{{ entries_url }}"
sx-trigger="intersect once delay:250ms"
sx-swap="outerHTML"
role="status"
aria-hidden="true"
>

View File

@@ -4,14 +4,14 @@
{% set tile_href = (current_local_href ~ {'view': 'tile'}|qs)|host %}
<a
href="{{ list_href }}"
hx-get="{{ list_href }}"
hx-target="#main-panel"
hx-select="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
sx-get="{{ list_href }}"
sx-target="#main-panel"
sx-select="{{hx_select_search}}"
sx-swap="outerHTML"
sx-push-url="true"
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view != 'tile' else 'text-stone-400 hover:text-stone-600' }}"
title="List view"
_="on click js localStorage.removeItem('events_view') end"
onclick="localStorage.removeItem('events_view')"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
@@ -19,14 +19,14 @@
</a>
<a
href="{{ tile_href }}"
hx-get="{{ tile_href }}"
hx-target="#main-panel"
hx-select="{{hx_select_search}}"
hx-swap="outerHTML"
hx-push-url="true"
sx-get="{{ tile_href }}"
sx-target="#main-panel"
sx-select="{{hx_select_search}}"
sx-swap="outerHTML"
sx-push-url="true"
class="p-1.5 rounded {{ 'bg-stone-200 text-stone-800' if view == 'tile' else 'text-stone-400 hover:text-stone-600' }}"
title="Tile view"
_="on click js localStorage.setItem('events_view','tile') end"
onclick="localStorage.setItem('events_view','tile')"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />

View File

@@ -8,9 +8,9 @@
<form
action="{{ ticket_url }}"
method="post"
hx-post="{{ ticket_url }}"
hx-target="#page-ticket-{{ entry.id }}"
hx-swap="outerHTML"
sx-post="{{ ticket_url }}"
sx-target="#page-ticket-{{ entry.id }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="entry_id" value="{{ entry.id }}">
@@ -26,9 +26,9 @@
<form
action="{{ ticket_url }}"
method="post"
hx-post="{{ ticket_url }}"
hx-target="#page-ticket-{{ entry.id }}"
hx-swap="outerHTML"
sx-post="{{ ticket_url }}"
sx-target="#page-ticket-{{ entry.id }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="entry_id" value="{{ entry.id }}">
@@ -49,9 +49,9 @@
<form
action="{{ ticket_url }}"
method="post"
hx-post="{{ ticket_url }}"
hx-target="#page-ticket-{{ entry.id }}"
hx-swap="outerHTML"
sx-post="{{ ticket_url }}"
sx-target="#page-ticket-{{ entry.id }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="entry_id" value="{{ entry.id }}">

View File

@@ -9,10 +9,10 @@
</p>
<form
hx-put="{{ url_for('payments.update_sumup') }}"
hx-target="#payments-panel"
hx-swap="outerHTML"
hx-select="#payments-panel"
sx-put="{{ url_for('payments.update_sumup') }}"
sx-target="#payments-panel"
sx-swap="outerHTML"
sx-select="#payments-panel"
class="space-y-3"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

View File

@@ -15,12 +15,12 @@
data-confirm-confirm-text="Yes, remove it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
hx-post="{{ url_for('blog.post.admin.toggle_entry', slug=post.slug, entry_id=entry.id) }}"
hx-trigger="confirmed"
hx-target="#associated-entries-list"
hx-swap="outerHTML"
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
_="on htmx:afterRequest trigger entryToggled on body"
sx-post="{{ url_for('blog.post.admin.toggle_entry', slug=post.slug, entry_id=entry.id) }}"
sx-trigger="confirmed"
sx-target="#associated-entries-list"
sx-swap="outerHTML"
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
sx-on:afterSwap="document.body.dispatchEvent(new CustomEvent('entryToggled'))"
>
<div class="flex items-center justify-between gap-3">
{% if calendar.post.feature_image %}

View File

@@ -8,14 +8,7 @@
<h3 class="text-lg font-semibold">Browse Calendars</h3>
{% for calendar in all_calendars %}
<details class="border rounded-lg bg-white"
_="on toggle
if my.open
for other in <details[open]/>
if other is not me
set other.open to false
end
end
end">
data-toggle-group="calendar-browser">
<summary class="p-4 cursor-pointer hover:bg-stone-50 flex items-center gap-3">
{% if calendar.post.feature_image %}
<img src="{{ calendar.post.feature_image }}"
@@ -35,8 +28,8 @@
</div>
</summary>
<div class="p-4 border-t"
hx-trigger="intersect once"
hx-swap="innerHTML">
sx-trigger="intersect once"
sx-swap="innerHTML">
<div class="text-sm text-stone-400">Loading calendar...</div>
</div>
</details>

View File

@@ -2,7 +2,7 @@
<div
id="slot-description-title"
{% if oob %}
hx-swap-oob="outerHTML"
sx-swap-oob="outerHTML"
{% endif %}
class="text-base font-normal break-words whitespace-normal min-w-0 break-all w-full text-center block"

View File

@@ -3,12 +3,12 @@
<div id="slot-errors" class="mt-2 text-sm text-red-600"></div>
<form
class="space-y-3 mt-4"
hx-put="{{ url_for('calendar.slots.slot.put',
sx-put="{{ url_for('calendar.slots.slot.put',
calendar_slug=calendar.slug,
slot_id=slot.id) }}"
hx-target="#slot-{{ slot.id }}"
hx-swap="outerHTML"
hx-on::after-request="if (event.detail.successful) this.reset()"
sx-target="#slot-{{ slot.id }}"
sx-swap="outerHTML"
sx-on:afterRequest="if (event.detail.successful) this.reset()"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
@@ -153,11 +153,11 @@
<button
type="button"
class="{{styles.cancel_button}}"
hx-get="{{ url_for('calendar.slots.slot.get_view',
sx-get="{{ url_for('calendar.slots.slot.get_view',
calendar_slug=calendar.slug,
slot_id=slot.id) }}"
hx-target="#slot-{{ slot.id }}"
hx-swap="outerHTML"
sx-target="#slot-{{ slot.id }}"
sx-swap="outerHTML"
>
Cancel
</button>

View File

@@ -53,13 +53,13 @@
<button
type="button"
class="{{styles.pre_action_button}}"
hx-get="{{ url_for(
sx-get="{{ url_for(
'calendar.slots.slot.get_edit',
slot_id=slot.id,
calendar_slug=calendar.slug,
) }}"
hx-target="#slot-{{slot.id}}"
hx-swap="outerHTML"
sx-target="#slot-{{slot.id}}"
sx-swap="outerHTML"
>
Edit
</button>

View File

@@ -1,11 +1,11 @@
<form
hx-post="{{ url_for('calendar.slots.post',
sx-post="{{ url_for('calendar.slots.post',
calendar_slug=calendar.slug) }}"
hx-target="#slots-table"
hx-select="#slots-table"
hx-disinherit="hx-select"
hx-swap="outerHTML"
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
sx-target="#slots-table"
sx-select="#slots-table"
sx-disinherit="sx-select"
sx-swap="outerHTML"
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
class="space-y-3"
>
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
@@ -98,10 +98,10 @@
<button
type="button"
class="{{styles.cancel_button}}"
hx-get="{{ url_for('calendar.slots.add_button',
sx-get="{{ url_for('calendar.slots.add_button',
calendar_slug=calendar.slug) }}"
hx-target="#slot-add-container"
hx-swap="innerHTML"
sx-target="#slot-add-container"
sx-swap="innerHTML"
>
Cancel
</button>

View File

@@ -2,10 +2,10 @@
<button
type="button"
class="{{styles.pre_action_button}}"
hx-get="{{ url_for('calendar.slots.add_form',
sx-get="{{ url_for('calendar.slots.add_form',
calendar_slug=calendar.slug) }}"
hx-target="#slot-add-container"
hx-swap="innerHTML"
sx-target="#slot-add-container"
sx-swap="innerHTML"
>
+ Add slot
</button>

View File

@@ -45,14 +45,14 @@
data-confirm-confirm-text="Yes, delete it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
hx-delete="{{ url_for('calendar.slots.slot.slot_delete',
sx-delete="{{ url_for('calendar.slots.slot.slot_delete',
calendar_slug=calendar.slug,
slot_id=s.id) }}"
hx-target="#slots-table"
hx-select="#slots-table"
hx-swap="outerHTML"
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
hx-trigger="confirmed"
sx-target="#slots-table"
sx-select="#slots-table"
sx-swap="outerHTML"
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
sx-trigger="confirmed"
type="button"
>
<i class="fa-solid fa-trash"></i>

View File

@@ -43,9 +43,9 @@
<td class="px-4 py-2">
{% if ticket.state in ('confirmed', 'reserved') %}
<form
hx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
hx-target="#entry-ticket-row-{{ ticket.code }}"
hx-swap="outerHTML"
sx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
sx-target="#entry-ticket-row-{{ ticket.code }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button

View File

@@ -52,9 +52,9 @@
<div id="checkin-action-{{ ticket.code }}">
{% if ticket.state in ('confirmed', 'reserved') %}
<form
hx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
hx-target="#checkin-action-{{ ticket.code }}"
hx-swap="innerHTML"
sx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
sx-target="#checkin-action-{{ ticket.code }}"
sx-swap="innerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button

View File

@@ -35,10 +35,10 @@
name="code"
placeholder="Enter or scan ticket code..."
class="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
hx-get="{{ url_for('ticket_admin.lookup') }}"
hx-trigger="keyup changed delay:300ms"
hx-target="#lookup-result"
hx-include="this"
sx-get="{{ url_for('ticket_admin.lookup') }}"
sx-trigger="keyup changed delay:300ms"
sx-target="#lookup-result"
sx-include="this"
autofocus
/>
<button
@@ -112,9 +112,9 @@
<td class="px-4 py-3">
{% if ticket.state in ('confirmed', 'reserved') %}
<form
hx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
hx-target="#ticket-row-{{ ticket.code }}"
hx-swap="outerHTML"
sx-post="{{ url_for('ticket_admin.do_checkin', code=ticket.code) }}"
sx-target="#ticket-row-{{ ticket.code }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<button

View File

@@ -3,16 +3,16 @@
<div id="ticket-errors" class="mt-2 text-sm text-red-600"></div>
<form
class="space-y-3 mt-4"
hx-put="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.put',
sx-put="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.put',
calendar_slug=calendar.slug,
year=year,
month=month,
day=day,
entry_id=entry.id,
ticket_type_id=ticket_type.id) }}"
hx-target="#ticket-{{ ticket_type.id }}"
hx-swap="outerHTML"
hx-on::after-request="if (event.detail.successful) this.reset()"
sx-target="#ticket-{{ ticket_type.id }}"
sx-swap="outerHTML"
sx-on:afterRequest="if (event.detail.successful) this.reset()"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
@@ -70,15 +70,15 @@
<button
type="button"
class="{{styles.cancel_button}}"
hx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_view',
sx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_view',
calendar_slug=calendar.slug,
entry_id=entry.id,
year=year,
month=month,
day=day,
ticket_type_id=ticket_type.id) }}"
hx-target="#ticket-{{ ticket_type.id }}"
hx-swap="outerHTML"
sx-target="#ticket-{{ ticket_type.id }}"
sx-swap="outerHTML"
>
Cancel
</button>

View File

@@ -32,7 +32,7 @@
<button
type="button"
class="{{styles.pre_action_button}}"
hx-get="{{ url_for(
sx-get="{{ url_for(
'calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.get_edit',
ticket_type_id=ticket_type.id,
calendar_slug=calendar.slug,
@@ -41,8 +41,8 @@
day=day,
entry_id=entry.id,
) }}"
hx-target="#ticket-{{ticket_type.id}}"
hx-swap="outerHTML"
sx-target="#ticket-{{ticket_type.id}}"
sx-swap="outerHTML"
>
Edit
</button>

View File

@@ -1,16 +1,16 @@
<form
hx-post="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.post',
sx-post="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.post',
calendar_slug=calendar.slug,
entry_id=entry.id,
year=year,
month=month,
day=day,
) }}"
hx-target="#tickets-table"
hx-select="#tickets-table"
hx-disinherit="hx-select"
hx-swap="outerHTML"
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
sx-target="#tickets-table"
sx-select="#tickets-table"
sx-disinherit="sx-select"
sx-swap="outerHTML"
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
class="space-y-3"
>
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
@@ -55,15 +55,15 @@
<button
type="button"
class="{{styles.cancel_button}}"
hx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.add_button',
sx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.add_button',
calendar_slug=calendar.slug,
entry_id=entry.id,
year=year,
month=month,
day=day,
) }}"
hx-target="#ticket-add-container"
hx-swap="innerHTML"
sx-target="#ticket-add-container"
sx-swap="innerHTML"
>
Cancel
</button>

View File

@@ -1,14 +1,14 @@
<button
class="{{styles.action_button}}"
hx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.add_form',
sx-get="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.add_form',
calendar_slug=calendar.slug,
entry_id=entry.id,
year=year,
month=month,
day=day,
) }}"
hx-target="#ticket-add-container"
hx-swap="innerHTML"
sx-target="#ticket-add-container"
sx-swap="innerHTML"
>
<i class="fa fa-plus"></i>
Add ticket type

View File

@@ -35,18 +35,18 @@
data-confirm-confirm-text="Yes, delete it"
data-confirm-cancel-text="Cancel"
data-confirm-event="confirmed"
hx-delete="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete',
sx-delete="{{ url_for('calendar.day.calendar_entries.calendar_entry.ticket_types.ticket_type.delete',
calendar_slug=calendar.slug,
year=year,
month=month,
day=day,
entry_id=entry.id,
ticket_type_id=tt.id) }}"
hx-target="#tickets-table"
hx-select="#tickets-table"
hx-swap="outerHTML"
hx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
hx-trigger="confirmed"
sx-target="#tickets-table"
sx-select="#tickets-table"
sx-swap="outerHTML"
sx-headers='{"X-CSRFToken": "{{ csrf_token() }}"}'
sx-trigger="confirmed"
type="button"
>
<i class="fa-solid fa-trash"></i>

View File

@@ -39,9 +39,9 @@
{% if type_count == 0 %}
{# Add to basket button #}
<form
hx-post="{{ url_for('tickets.adjust_quantity') }}"
hx-target="#ticket-buy-{{ entry.id }}"
hx-swap="outerHTML"
sx-post="{{ url_for('tickets.adjust_quantity') }}"
sx-target="#ticket-buy-{{ entry.id }}"
sx-swap="outerHTML"
class="flex items-center"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
@@ -59,9 +59,9 @@
{# +/- controls #}
<div class="flex items-center gap-2">
<form
hx-post="{{ url_for('tickets.adjust_quantity') }}"
hx-target="#ticket-buy-{{ entry.id }}"
hx-swap="outerHTML"
sx-post="{{ url_for('tickets.adjust_quantity') }}"
sx-target="#ticket-buy-{{ entry.id }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="entry_id" value="{{ entry.id }}" />
@@ -90,9 +90,9 @@
</a>
<form
hx-post="{{ url_for('tickets.adjust_quantity') }}"
hx-target="#ticket-buy-{{ entry.id }}"
hx-swap="outerHTML"
sx-post="{{ url_for('tickets.adjust_quantity') }}"
sx-target="#ticket-buy-{{ entry.id }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="entry_id" value="{{ entry.id }}" />
@@ -128,9 +128,9 @@
{% if qty == 0 %}
{# Add to basket button #}
<form
hx-post="{{ url_for('tickets.adjust_quantity') }}"
hx-target="#ticket-buy-{{ entry.id }}"
hx-swap="outerHTML"
sx-post="{{ url_for('tickets.adjust_quantity') }}"
sx-target="#ticket-buy-{{ entry.id }}"
sx-swap="outerHTML"
class="flex items-center"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
@@ -149,9 +149,9 @@
{# +/- controls #}
<div class="flex items-center gap-2">
<form
hx-post="{{ url_for('tickets.adjust_quantity') }}"
hx-target="#ticket-buy-{{ entry.id }}"
hx-swap="outerHTML"
sx-post="{{ url_for('tickets.adjust_quantity') }}"
sx-target="#ticket-buy-{{ entry.id }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="entry_id" value="{{ entry.id }}" />
@@ -179,9 +179,9 @@
</a>
<form
hx-post="{{ url_for('tickets.adjust_quantity') }}"
hx-target="#ticket-buy-{{ entry.id }}"
hx-swap="outerHTML"
sx-post="{{ url_for('tickets.adjust_quantity') }}"
sx-target="#ticket-buy-{{ entry.id }}"
sx-swap="outerHTML"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<input type="hidden" name="entry_id" value="{{ entry.id }}" />

View File

@@ -1,22 +1,22 @@
{# Account nav items: tickets + bookings links for the account dashboard #}
<div class="relative nav-group">
<a href="{{ account_url('/tickets/') }}"
hx-get="{{ account_url('/tickets/') }}"
hx-target="#main-panel"
hx-select="{{ hx_select_search }}"
hx-swap="outerHTML"
hx-push-url="true"
sx-get="{{ account_url('/tickets/') }}"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-swap="outerHTML"
sx-push-url="true"
class="{{styles.nav_button}}">
tickets
</a>
</div>
<div class="relative nav-group">
<a href="{{ account_url('/bookings/') }}"
hx-get="{{ account_url('/bookings/') }}"
hx-target="#main-panel"
hx-select="{{ hx_select_search }}"
hx-swap="outerHTML"
hx-push-url="true"
sx-get="{{ account_url('/bookings/') }}"
sx-target="#main-panel"
sx-select="{{ hx_select_search }}"
sx-swap="outerHTML"
sx-push-url="true"
class="{{styles.nav_button}}">
bookings
</a>