This file documents CDN libraries, the emoji picker, the modal system, and shared template partials under templates/. Claude Code auto-loads it whenever a file under this directory is read or edited.

Templates and frontend (general rules)

  • All routers MUST import the shared templates from devplacepy.templating. Do NOT instantiate Jinja2Templates per router - globals (avatar_url, get_unread_count, get_user_projects, format_date) are registered on that single instance.
  • Templates extend base.html, add page CSS via {% block extra_head %}, page JS via {% block extra_js %}.
  • Nav active state is the nav_active Jinja global, never an inline path check. nav_active(request, *prefixes, exact=False) (templating.py) returns "active" (or "") when request.url.path equals a prefix or is a sub-path of it (exact=True matches only the exact path, e.g. the admin Dashboard root). Every topnav/sidebar link uses it (class="topnav-link {{ nav_active(request, '/projects') }}"); do NOT hand-write {% if request.url.path == ... or request.url.path.startswith(...) %}active{% endif %}.

CDN Libraries

Loaded via <script> tags in base.html. ALL must use defer to avoid blocking DOMContentLoaded:

<script defer src="https://cdn.jsdelivr.net/npm/marked/lib/marked.umd.js"></script>
<script defer src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js"></script>
<script defer src="/static/vendor/purify.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/emoji-picker-element@^1/index.js"></script>
<script defer src="/static/js/ContentRenderer.js"></script>
<script defer src="/static/js/EmojiPicker.js"></script>
<script type="module" src="/static/js/Application.js"></script>

Never use <script> without defer for CDN libraries - they block HTML parsing and cause wait_until="domcontentloaded" to timeout in Playwright tests.

Emoji Picker

Uses emoji-picker-element web component (Discord-style, searchable, skin tones):

  • EmojiPickerElement.js is the single wrapper around the vendored element: load() lazily imports the module on first open, create(onSelect) builds an <emoji-picker> wired to the vendored data.json and calls back with the chosen unicode. Never instantiate <emoji-picker> or hardcode its data source anywhere else.
  • EmojiPicker.js is the textarea consumer: it wraps the element in a toggle panel and inserts unicode at cursor position
  • Added to all .comment-form textarea and .emoji-picker-target elements
  • ReactionBar.js is the second consumer: the + button in _reaction_bar.html mounts the same element into .reaction-picker so any emoji can be used as a reaction
  • The old &#x1F600; emoji button has been removed from all templates

Modal System

Application.js initModals() toggles a .visible CSS class on .modal-overlay; the CSS rule .modal-overlay.visible { display: flex; } handles visibility:

// CORRECT - toggle the .visible class on the modal:
modal.classList.add("visible");    // show
modal.classList.remove("visible"); // hide

Triggers usually have href="#", so call e.preventDefault() in click handlers. Always call e.preventDefault() on [data-modal] click handlers since trigger elements often have href="#":

trigger.addEventListener("click", (e) => {
    e.preventDefault();
    modal.style.display = "flex";
});

.modal-close is wired generically by Application.js - no inline JS needed in templates for basic modals.

Do NOT hand-write the overlay/header markup. Use the shared macro in templates/_macros.html:

{% from "_macros.html" import modal %}
{% call modal('create-post-modal', 'Create New Post') %}{# wide=true for modal-card-wide #}
    <form ...> ... <div class="modal-footer">...</div> </form>
{% endcall %}

Shared template partials

Reuse these via {% set _x = ... %}{% include %} (the _avatar_link.html convention) instead of copy-pasting markup:

  • _post_composer_form.html - the create-post form (topic selector, content/title, project select, attachments, poll builder, footer). Locals: _composer_topic (preselected topic, default random), _composer_project (preselected project uid or ""). Wrapped in the modal() macro by feed.html (Create New Post) and project_detail.html (owner-only Post an update, preset to devlog + the project). Never fork a second copy of this form.
  • _post_votes.html - post +/- vote bar. Locals: _uid, _my_vote, _count.
  • _star_vote.html - project/gist star button. Locals: _type (project|gist), _uid, _my_vote, _count, _btn_class, optional _stop (adds data-stop-propagation). The star glyph ( when .voted) comes from the vote-star CSS class via ::before (base.css) - do not put a literal star in markup.
  • _post_header.html - post author/avatar/time header (.post-header). Locals: _author, _time.
  • _topic_selector.html - topic radio group. Locals: _topics, _selected.
  • _quiz_actions.html - the hub's New quiz / Create quiz with Devii card. Needs user; the Devii button carries data-devii-prompt (the platform-wide prompt-seeding attribute read by DeviiTerminal.bindTriggers).
  • _quiz_scoreboard.html - the cross-quiz scoreboard rail plus the Your progress card. Needs scoreboard, viewer_standing, viewer_progress, user.
  • _quiz_card.html - one quiz listing card. Needs item (a QuizListItemOut-shaped dict) and user.
  • _quiz_question.html - one question in builder-preview or player mode. Locals: _question, _mode (builder|player), plus _attempt_url and answer_max_chars in player mode.
  • _quiz_answer_review.html - one reviewed answer on the results screen. Local: _question.
  • _quiz_settings_fields.html - the shared quiz settings fieldset. Optional local _quiz seeds the current values.
  • _report_button.html - the Report control (and Block, when _owner_name is given) for any content action bar. Locals: _type, _uid, _owner (owner uid - the control hides on your own content), _owner_name (optional), _class (the surrounding surface's button class). Included at fifteen sites; a new content surface MUST include it (see the root CLAUDE.md rule).
  • _report_dialog.html - the one report dialog, included once in base.html for signed-in users and driven by ReportDialog.js. Reasons come from the REPORT_REASONS Jinja global; never hand-roll a second dialog.
  • _maturity_gate.html - the interstitial rendered in place of a maturity-labelled item. Local: _level. Guard the include with the maturity_hidden(level, user) global; the item's maturity is attached by enrich_items/load_detail.

Vote button styles live ONCE in feed.css (.post-action-btn) - never redefine them in post.css. Page-specific CSS goes in a static/css/*.css file referenced from {% block extra_head %}, never an inline <style> block. All CSS conventions (tokens, --z-* stacking bands, breakpoints, reduced motion) are in devplacepy/static/css/CLAUDE.md.

For clickable avatars/usernames, reuse templates/_avatar_link.html and templates/_user_link.html via {% include %} with _user, _size, _size_class locals.

The three public listings (/feed, /gists, /projects) share one search box: the templates/_sidebar_search.html partial (included at the top of .sidebar-card with _action/_placeholder/_hidden locals) plus the database.text_search_clause(table, search, fields, author_field) data helper. Each listing matches its text fields PLUS the author username: pass author_field="user_uid" and the helper resolves matching usernames to uids via database.get_uids_by_username_match(search) and OR-includes user_uid IN (...) (no JOIN, no separate ilike). Any new listing with a left filter panel reuses both - never re-inline an ilike search clause, never add a JOIN for author matching, and never hand-roll another search form. See devplacepy/routers/CLAUDE.md -> Listing search.

Modal pattern

Application.js initModals() toggles a .visible CSS class on .modal-overlay; the CSS rule .modal-overlay.visible { display: flex; } handles visibility. Triggers usually have href="#", so call e.preventDefault() in click handlers. .modal-close is wired generically - no inline JS needed.