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):

  • EmojiPicker.js wraps it with a toggle button and inserts unicode at cursor position
  • Added to all .comment-form textarea and .emoji-picker-target elements
  • 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_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.

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.

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.