The project detail page becomes a full project showcase built entirely
from existing platform mechanisms. One encompassing dark card wraps the
page; inner panels (tab bar, sidebar cards, devlog entries, comments)
sit one elevation lighter. The hero opens with a cover banner and an
optional logo tile, both plain attachment references
(cover_attachment_uid/logo_attachment_uid) uploaded through the
standard dp-upload attachment widget and linked via the existing
link_attachments choke point - the route validates each uid belongs to
the actor and is an image, and an empty value on edit keeps the current
one. The title block, type/platform chips and author row overlay the
banner behind a scrim with a dark text shadow, next to an owner-set
Visit Website CTA; website_url and repo_url are normalized in models
and render with rel noopener nofollow.
An anchor tab bar (Overview, Devlog, Screenshots when present,
Comments, Files) navigates the page. The main column keeps About, the
devlog timeline (with devlog_count and an owner Post update button
opening the shared composer preset to the devlog topic + project - the
form now lives once in _post_composer_form.html, included by feed.html
and project_detail.html), a Screenshots gallery built from image
attachments minus the cover/logo (thumbnails, lightbox, 12 rendered),
and the comment thread; the sidebar holds Links, Stats and the Author
card. Owners add gallery images from the More menu via
POST /projects/{slug}/screenshots (owner-only, audit
project.screenshots.add, Devii action project_add_screenshots, docs id
projects-screenshots). comment_count/devlog_count ride
ProjectDetailOut, the new fields ride ProjectOut, and the create/edit
faces (modals, Devii actions, API docs) carry them. The project
comment/files e2e tests scope their locators per the documented
dual-control idiom, and new unit/api/e2e tests cover URL normalization,
the counts, the hero attachment guard, the screenshots flow and the
preset composer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8.2 KiB
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.