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
templatesfromdevplacepy.templating. Do NOT instantiateJinja2Templatesper 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_activeJinja global, never an inline path check.nav_active(request, *prefixes, exact=False)(templating.py) returns"active"(or"") whenrequest.url.pathequals a prefix or is a sub-path of it (exact=Truematches 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.jswraps it with a toggle button and inserts unicode at cursor position- Added to all
.comment-form textareaand.emoji-picker-targetelements - The old
😀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(addsdata-stop-propagation). The star glyph (☆→★when.voted) comes from thevote-starCSS 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.