- On a gallery booth the marks panel moves into a sticky verdict aside beside the set. The aside comes first in the document, so a narrow screen stacks the question above the work; grid areas place it on the right when wide. Nothing in an ordered collection moves. Boards are unchanged. - The flag tray lists flagged items by tile number: the declared change from the panel list's (created, id). The standalone marks page keeps the list. - Inline group headers are divs, never figure.item. - Every mark-dependent element is a data-region: the verdict, each tile, the rail's filter counts. There is also a server-rendered status line. - The in-place script (base.html) POSTs with an explicit JSON Accept, then on 204 swaps every region from a fresh GET. Live media and per-viewer view state are carried across the swap, so there is no layout jolt and no stopped track. It never re-POSTs: on failure it says so and reloads. Tile controls re-bind after a swap, and the grid cursor survives it. - The `n` key opens the tile's closed note disclosure before focusing it. - test_embed_browser's keyboard-flag test expected a navigation, which is the defect R2 removes. It is updated as declared in the contract, and tightened: a window marker must survive, proving no reload. Browser tests: flag in place, with no reload and no scroll jump, and the tile, tray and rail count all updated; and a failed save that reloads without re-POSTing. Two mutations turn them red (no carry, no rail region). 700 passed.
180 lines
9.9 KiB
HTML
180 lines
9.9 KiB
HTML
{# Shared MARKS panel — included by booth.html (auto-gallery view) and by
|
||
marks.html (the standalone page a VERBATIM index.html booth links to, since
|
||
a verbatim page is served as-is and can never render this inline).
|
||
|
||
One primitive, three shapes, one slot:
|
||
pick — a session declared N options; the operator chooses. Renders as a
|
||
radio form; answering POSTs to /answer and rewrites .marks.json.
|
||
note — free text the operator volunteered, in either direction.
|
||
flag — the operator pointing at one item. Rendered on the item's tile
|
||
rather than here, so the judgment sits beside the artifact; the
|
||
count below is the way back to them.
|
||
|
||
Works with JS off — plain form POST, every shape. An answered pick shows the
|
||
recorded judgment and a collapsed "change" form, because the mark is the
|
||
CURRENT judgment and not a log. #}
|
||
{# A mark carrying `error` is sorted out FIRST, whatever shape it claims. A
|
||
pick keeps its own ⚠ broken rendering below (richer — it has a declaration to
|
||
show); a broken note would otherwise render as an empty <pre> with a withdraw
|
||
button, indistinguishable from a note the operator wrote and then cleared,
|
||
and a broken flag would link to a target that is not there. Unreadable state
|
||
is visible state — the rule `_hydrate` states for picks, applied to all
|
||
three. #}
|
||
{% set broken = marks | selectattr('error') | rejectattr('shape', 'equalto', 'pick') | list %}
|
||
{% set picks = marks | selectattr('shape', 'equalto', 'pick') | list %}
|
||
{% set notes = marks | selectattr('shape', 'equalto', 'note') | rejectattr('error') | list %}
|
||
{% set flags = marks | selectattr('shape', 'equalto', 'flag') | rejectattr('error') | list %}
|
||
<section class="marks">
|
||
|
||
{% for a in broken %}
|
||
<article class="mark mark-note is-broken" id="mark-{{ a.id }}">
|
||
<header class="mark-head">
|
||
<span class="mark-state">⚠ broken</span>
|
||
<span class="mark-id"><code>{{ a.id }}</code></span>
|
||
<span class="board-spacer"></span>
|
||
<form class="mark-undo" method="post" action="/b/{{ name_url }}/unmark" data-inplace>
|
||
<input type="hidden" name="mark" value="{{ a.id }}">
|
||
{% if marks_page %}<input type="hidden" name="back" value="marks">{% endif %}
|
||
<button type="submit" class="mark-x" title="withdraw this mark">×</button>
|
||
</form>
|
||
</header>
|
||
<p class="mark-error">This mark could not be read: {{ a.error }}</p>
|
||
</article>
|
||
{% endfor %}
|
||
|
||
{% for a in picks %}
|
||
<article class="mark mark-pick{% if a.answer and a.answer.complete %} is-answered{% elif a.answer %} is-partial{% elif a.error %} is-broken{% endif %}" id="mark-{{ a.id }}">
|
||
<header class="mark-head">
|
||
<span class="mark-state">{% if a.error %}⚠ broken{% elif a.answer and a.answer.complete %}✓ answered{% elif a.answer %}◐ partial{% else %}? open{% endif %}</span>
|
||
<span class="mark-id"><code>{{ a.id }}</code>{% if a.multi %} · {{ a.questions|length }} questions{% endif %}</span>
|
||
{% if a.target %}<span class="mark-target">on <a href="view?f={{ a.target|urlencode }}">{{ a.target }}</a></span>{% endif %}
|
||
<span class="board-spacer"></span>
|
||
{% if a.answer and not a.answer.complete %}<span class="mark-part">{{ (a.questions|length) - (a.answer.unanswered|length) }}/{{ a.questions|length }}</span>{% endif %}
|
||
{% if a.answer %}<span class="mark-when">{{ a.answer.answered_at }}{% if a.answer.answered_by %} · {{ a.answer.answered_by }}{% endif %}</span>{% endif %}
|
||
</header>
|
||
{% if a.error %}
|
||
<p class="mark-error">This question could not be read: {{ a.error }}</p>
|
||
{% else %}
|
||
{% if a.title and not a.multi %}<p class="mark-title">{{ a.title }}</p>{% endif %}
|
||
<p class="mark-prompt">{{ a.prompt }}</p>
|
||
{% if a.answer %}
|
||
<div class="mark-answer">
|
||
{% if a.multi %}
|
||
{% for q in a.questions %}{% set qa = a.answer.answers.get(q.key) %}
|
||
<div class="mark-answer-q">
|
||
<span class="mark-answer-qprompt">{{ q.prompt }}</span>
|
||
<div class="mark-answer-choice{% if not (qa and qa.choice is not none) %} is-skipped{% endif %}">{{ qa.label if (qa and qa.choice is not none) else 'left blank' }}</div>
|
||
{% if qa and qa.notes %}<pre class="mark-answer-notes">{{ qa.notes }}</pre>{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
<div class="mark-answer-choice">{{ a.answer.label }}</div>
|
||
{% endif %}
|
||
{% if a.answer.notes %}<pre class="mark-answer-notes">{{ a.answer.notes }}</pre>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
<details class="mark-formwrap"{% if not a.answer %} open{% endif %}>
|
||
<summary class="mark-change">{% if a.answer %}change answer{% else %}answer{% endif %}</summary>
|
||
<form class="mark-form" method="post" action="/b/{{ name_url }}/answer" data-inplace>
|
||
{# The field is still `ask`: inline fragments in reports the operator
|
||
has already published POST that name, and breaking every landed
|
||
verbatim report to tidy a form field is not a trade worth making. #}
|
||
<input type="hidden" name="ask" value="{{ a.id }}">
|
||
{# On the standalone page, come back HERE — the booth's own page is a
|
||
verbatim report that cannot show the recorded judgment. #}
|
||
{% if marks_page %}<input type="hidden" name="back" value="marks">{% endif %}
|
||
{% for q in a.questions %}
|
||
{% set field = 'choice.' ~ q.key if a.multi else 'choice' %}
|
||
{% set qa = a.answer.answers.get(q.key) if (a.answer and a.multi) else a.answer %}
|
||
<fieldset class="mark-q">
|
||
{% if a.multi %}<legend class="mark-q-prompt">{{ loop.index }}. {{ q.prompt }}</legend>{% endif %}
|
||
<div class="mark-options">
|
||
{% for o in q.options %}
|
||
<label class="mark-opt{% if qa and qa.choice == o.id %} is-current{% endif %}">
|
||
<input type="radio" name="{{ field }}" value="{{ o.id }}"
|
||
{% if qa and qa.choice == o.id %}checked{% endif %}>
|
||
<span class="mark-opt-main">
|
||
<span class="mark-opt-label">{{ o.label }}</span>
|
||
{% if o.detail %}<span class="mark-opt-detail">{{ o.detail }}</span>{% endif %}
|
||
</span>
|
||
</label>
|
||
{% endfor %}
|
||
</div>
|
||
{% if q.notes %}
|
||
<textarea class="mark-notes mark-qnotes" name="notes.{{ q.key }}" rows="2" placeholder="notes on this one (optional)">{{ qa.notes if qa else '' }}</textarea>
|
||
{% endif %}
|
||
</fieldset>
|
||
{% endfor %}
|
||
{% if a.notes_enabled %}
|
||
<textarea class="mark-notes" name="notes" rows="3" placeholder="{{ a.notes_label }} (optional)">{{ a.answer.notes if a.answer else '' }}</textarea>
|
||
{% endif %}
|
||
<div class="mark-actions">
|
||
<button type="submit" class="mark-submit">{% if a.answer %}Update answer{% else %}Submit answer{% endif %}</button>
|
||
</div>
|
||
</form>
|
||
</details>
|
||
{% endif %}
|
||
</article>
|
||
{% endfor %}
|
||
|
||
{# FLAGS come right after the picks. On the lightbox (`tray` defined) they
|
||
render as the TRAY: the flagged items in SET order — by tile number, the
|
||
declared R2 change from the click order below — each the original shown
|
||
small, blurred if the item is. The standalone marks page has no item
|
||
records, so it keeps the list, in `(created, id)` order. #}
|
||
{% if tray is defined and tray %}
|
||
<article class="mark mark-flags" id="mark-flags">
|
||
<header class="mark-head">
|
||
<span class="mark-state mark-state-flag">✔ flagged</span>
|
||
<span class="mark-id">{{ tray|length }} item{{ '' if tray|length == 1 else 's' }} · in set order</span>
|
||
</header>
|
||
<div class="tray">
|
||
{% for it in tray %}
|
||
<a class="tray-item{% if it.blurred %} is-blurred{% endif %}" href="view?f={{ it.url }}" title="{{ it.name }}">
|
||
{%- if it.kind == 'image' %}<img loading="lazy" src="{{ it.url }}" alt="">{% else %}<span class="tray-kind">{{ it.kind }}</span>{% endif -%}
|
||
<span class="tray-ord">#{{ "%0*d"|format(ord_width, it.ordinal) }}</span></a>
|
||
{% endfor %}
|
||
</div>
|
||
</article>
|
||
{% elif tray is not defined and flags %}
|
||
<article class="mark mark-flags" id="mark-flags">
|
||
<header class="mark-head">
|
||
<span class="mark-state mark-state-flag">✔ flagged</span>
|
||
<span class="mark-id">{{ flags|length }} item{{ '' if flags|length == 1 else 's' }}</span>
|
||
</header>
|
||
<ul class="mark-flaglist">
|
||
{% for a in flags %}
|
||
<li><a href="view?f={{ a.target|urlencode }}">{{ a.target }}</a></li>
|
||
{% endfor %}
|
||
</ul>
|
||
</article>
|
||
{% endif %}
|
||
|
||
{% for a in notes %}
|
||
<article class="mark mark-note" id="mark-{{ a.id }}">
|
||
<header class="mark-head">
|
||
<span class="mark-state mark-state-note">note</span>
|
||
{% if a.target %}<span class="mark-target">on <a href="view?f={{ a.target|urlencode }}">{{ a.target }}</a></span>
|
||
{% else %}<span class="mark-target">on this booth</span>{% endif %}
|
||
<span class="board-spacer"></span>
|
||
<span class="mark-when">{{ a.created }}{% if a.by %} · {{ a.by }}{% endif %}</span>
|
||
<form class="mark-undo" method="post" action="/b/{{ name_url }}/unmark" data-inplace>
|
||
<input type="hidden" name="mark" value="{{ a.id }}">
|
||
{% if marks_page %}<input type="hidden" name="back" value="marks">{% endif %}
|
||
<button type="submit" class="mark-x" title="withdraw this note">×</button>
|
||
</form>
|
||
</header>
|
||
<pre class="mark-text">{{ a.text }}</pre>
|
||
</article>
|
||
{% endfor %}
|
||
|
||
|
||
{# The operator volunteering a remark, which before marks had no mechanism at
|
||
all — this is the direction that was running through chat. #}
|
||
<form class="mark-add" method="post" action="/b/{{ name_url }}/note" data-inplace>
|
||
{% if marks_page %}<input type="hidden" name="back" value="marks">{% endif %}
|
||
<textarea name="text" rows="2" placeholder="a note on this booth, for the session that posted it"></textarea>
|
||
<button type="submit">Add note</button>
|
||
</form>
|
||
</section>
|