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):
EmojiPickerElement.jsis the single wrapper around the vendored element:load()lazily imports the module on first open,create(onSelect)builds an<emoji-picker>wired to the vendoreddata.jsonand calls back with the chosen unicode. Never instantiate<emoji-picker>or hardcode its data source anywhere else.EmojiPicker.jsis the textarea consumer: it wraps the element in a toggle panel and inserts unicode at cursor position- Added to all
.comment-form textareaand.emoji-picker-targetelements ReactionBar.jsis the second consumer: the+button in_reaction_bar.htmlmounts the same element into.reaction-pickerso any emoji can be used as a reaction- 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_composer_form.html- the create-post form (topic selector, content/title, project select, attachments, poll builder, footer). Locals:_composer_topic(preselected topic, defaultrandom),_composer_project(preselected project uid or""). Wrapped in themodal()macro byfeed.html(Create New Post) andproject_detail.html(owner-only Post an update, preset todevlog+ 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(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._quiz_actions.html- the hub's New quiz / Create quiz with Devii card. Needsuser; the Devii button carriesdata-devii-prompt(the platform-wide prompt-seeding attribute read byDeviiTerminal.bindTriggers)._quiz_scoreboard.html- the cross-quiz scoreboard rail plus the Your progress card. Needsscoreboard,viewer_standing,viewer_progress,user._quiz_card.html- one quiz listing card. Needsitem(aQuizListItemOut-shaped dict) anduser._quiz_question.html- one question in builder-preview or player mode. Locals:_question,_mode(builder|player), plus_attempt_urlandanswer_max_charsin 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_quizseeds the current values._report_button.html- the Report control (and Block, when_owner_nameis 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 rootCLAUDE.mdrule)._report_dialog.html- the one report dialog, included once inbase.htmlfor signed-in users and driven byReportDialog.js. Reasons come from theREPORT_REASONSJinja 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 thematurity_hidden(level, user)global; the item'smaturityis attached byenrich_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.