Reusable server-rendered UI components live in internal/server/templates/components.html. Prefer these before adding new markup to feature templates.
breadcrumb: subtle entity hierarchy navigation backed byuiBreadcrumbData. Project pages showProjects / Project name / Current view; issue pages append the current issue key to the project, with a parent issue key between them for sub-issues. The issue Context manager links the issue key and appendsContextas the current item.tab-bar: single-line sibling-view navigation with optional Lucide icons, backed byuiTabBarData. Set an item'sMobileOverflowflag when its owning page provides an equivalent constrained-screen overflow-menu link; those tabs return atlg.sidebar-favorites: shell sidebar favorite-project shortcuts backed byuiSidebarFavoritesData, under a small uppercaseFavoritesheading (hidden with the other wide-only labels when the sidebar is collapsed). Keep it directly below theProjectsnav item with a subtle divider from standard navigation, and refresh it with OOB HTMX swaps when favorite state changes.sidebar-recents: the sidebar'sRecentslist below Favorites, backed byuiSidebarRecentsData. It shows the up to ten issues the signed-in user opened most recently, newest first; each row is anissue-keyand a one-line title.renderUIShellloads it (store.ListRecentIssues, which drops issues the user can no longer read). The issue page and panel record each view (store.RecordIssueView); an htmx panel also swaps the list in out of band. The whole section is wide-only, so the collapsed sidebar hides it. On an issue page the issue's Recents entry, not its project, is the one active destination: the panel'sdata-sidebar-viewelement carriesdata-sidebar-issue-id, andapp.jsfalls back to the project while the entry is hidden.- Truncation tooltips: a control with visible text can add
data-tooltipplusdata-tooltip-when-truncated, and mark the clipped elementdata-tooltip-truncates. The shared app tooltip then shows only while that text is cut off, as Recents does for long titles. - Account menu: the menu inside the sidebar's account footer in
shell_sidebar.htmlranges overaccountPages(uiAccountPages, a list ofuiAccountPagewithView,Label,Path, and LucideIcon) and ends with theSign outform below a divider. Each account panel setsdata-sidebar-viewto its page'sView, and its handler renders throughrenderUIAccountPageso the matchingdata-account-menu-linkis marked current;app.jskeeps that mark in step after htmx navigation and closes the menu when an item is chosen. legal-links: compact links to the canonical Terms, Privacy, and Security pages. Reuse it on signed-out pages; authenticated account pages get it throughaccount-footer, so the published documents remain consistently reachable.account-footer: bordered page footer wrappinglegal-links. Every account page (Profile, Login, Notifications, Tokens) ends with it; it takes no data.issue-list-controls: collapsible shared status, priority, tag, assignee, sort, and direction controls for issue list views. Closed by default; summary shows active filter count plus current sort/direction. Sort uses dropdown options including due date; direction uses Asc/Desc dropdown options with arrow icons. ExpectsuiIssueControlsData; omit tag fields for cross-project lists and omit assignee fields for current-user scoped lists.
Brand building blocks live in internal/server/templates/brand.html; see "Brand" in DESIGN_CONTEXT.md.
brand-head: the icon, apple-touch icon, manifest, theme colour, and stylesheet links. Every full-page document includes it inside<head>, so no page ships without the icon or the stylesheet.brand-backdrop: the brand scene, a fixed, clipped, pointer-events-free layer atz-index: -1placed directly inside<body>. Build its data withbrandBackdrop <variant> <preset> <oob>: variant""is the full, animated scene behind a single centered card (auth and OAuth pages) and"ambient"the still, calmer version behind the app shell and legal pages. Only the app shell passes the user's background preset (.Background); every other page passes""for the default.oobre-renders it as an htmx out-of-band swap, which the Appearance card uses after a save. Any surface that sits straight on the page must be opaque.brand-mark: the 28px icon andtrackslashwordmark lockup for page chrome (sidebar head, mobile app bar, legal header). Wrap it in a link where the chrome needs one; it takes no data.- Navigation progress:
shell.htmlrenders one<div data-nav-progress class="nav-progress">under<body>.app.jssetsdata-nav-busyon<html>while any htmx request targeting#mainis in flight, and the stylesheet fades the bar in after a short delay. statusSurface/statusCard: status tints for cards that sit straight on the page (the issue header, board cards). They lay the translucentstatusRowtint over an opaque page-colour base;statusCardadds the row hover tint for linked cards. UsestatusRowonly for rows inside an opaque list card.
auth-page-openandauth-page-closeininternal/server/templates/login.html: the shared document shell forlogin,signup,oauth-consent, andoauth-error. Pass the page's CSRF token (or""when the page has no form) toauth-page-open. It rendersbrand-head, the fullbrand-backdrop, and opens the centered card with the icon andtrackslashwordmark, so page headings inside the card areh2. Put the page body between the two templates.auth-page-closecloses the card, addslegal-links, and loadsauth.js, which also renders the page's Lucide icons.- Login password disclosure:
<details data-password-login>holds the username/password form below the primary passkey button. The server renders itopenwhen the page carries a password-login error;auth.jsopens it when WebAuthn is unavailable and focuses the username field whenever it opens.
- App tooltip: one body-level tooltip in
shell_scripts.htmlautomatically labels interactive controls that have anaria-labelbut no visible text. It appears on pointer hover and keyboard focus, follows the control through the shared CSS anchor infrontend/tailwind.css, and stays outside card overflow. Keep labels concise and action-oriented; do not add a tooltip to controls whose text is already visible. local-time: semantic timestamp backed byuiLocalTimeDatafromtokenTime. It keeps canonical UTC indatetime, renders an explicit UTC fallback, and is enhanced byapp.jsinto the browser's local date, time, and timezone on initial load and after HTMX swaps.
issue-key: compact monospace issue identifier badge. Use it for ticket numbers wherever possible; if a generic data-driven badge must show an issue identifier, mirror this component's monospace, uppercase, compact bordered treatment.project-key: compact project key badge.access-badge: icon-and-label badge for a project access setting, from auiAccessBadge. The open or on state is tinted emerald and the restricted or off state is neutral. Build it withprojectVisibilityBadge(Publicglobe /Privatelock),projectIssueCreationBadge(Any signed-in userusers /Members onlyuser-round-check) orprojectSprintModeBadge(Enabledperson-standing /Disabledlist-checks). Each carries its setting's data hook (data-project-visibility,data-project-issue-creation,data-project-sprint-mode) so tests and scripts can read the state without parsing copy.sprint-ref: compact monospace canonical sprint-reference badge. Keep thesprint-Nvalue lowercase and pair it with sprint titles on current, planned, and historical cards.count-badge: small numeric count badge.sprint-issue-count-badge: compact sprint-total badge that usesIssuefor one andIssuesfor zero or multiple while reusingcount-badgestyling.status-badge: issue status badge usingstatusClass.close-reason-badge: close reason badge for closed issues.missing-close-reason-badge: dashed placeholder for invalid or incomplete closed issue detail states.priority-badge: circular P0-P4 priority marker.tag-badge: compact hashtag badge formodel.IssueTag, usingDisplayNameandtagClass .Color.issue-due-badge: due-date badge with overdue/today/future styling.issue-worker-badge: 20px square with a Lucide icon, from an issue'sWorker(*model.IssueWorker) throughworkerBadge. It says who is meant to complete the issue: a neutralbotfor an agent, an amberuser-roundfor a human, because a human mark means an agent could not finish and someone is waiting on a person. An issue nobody has marked renders nothing. The icon isaria-hidden; the shared app tooltip (data-tooltip) and a screen-reader label readFor an agentorNeeds a human. It carriesdata-issue-worker-badge="agent|human"and isn't interactive, so it can sit inside a row link. Rows and cards put it first in their right-hand cluster, before the due badge; the issue header puts it after the priority.issue-worker-label: the same mark as an icon-and-label badge (For an agent/Needs a human) for the issue Details sidebar, carryingdata-issue-worker-label.issue-private-badge: 20px square with a Lucidelockand the tooltip "Private", marking a private issue on rows (issue-summary-row), sprint board cards, Me and the issue header. It takes no data; render it inside{{if .Issue.Private}}. Only people who can see the issue ever get it.issue-private-field: the "Keep this issue private" checkbox with its muted hint, on every form that files an issue: new issue, the help desk and the sub-issue dialog. Build it withissuePrivateField "<id>" <checked> <helpDesk>; the help-desk variant explains that the issue stays private if the project is later made public. It postsprivate=true.issue-sprint-badge: 20px indigo square with a visibleS, from amodel.Sprint. It marks an issue that is in a sprint. TheSisaria-hidden; the shared app tooltip (data-tooltip) and a screen-reader label both readIn sprint-N · Name (status). It carriesdata-issue-sprint-badge="sprint-N". It isn't interactive, so it can sit inside a row link.
user-avatar: circular user avatar with thumbnail-or-initials fallback. PassuserAvatar <user-like value> <class>where the value ismodel.User,model.ProjectMember,model.ProjectAssignee,model.ProjectAssigneeIssueStats,model.ProjectChangelogActor, oruiIssueCommentItem. The shared component owns the circular crop and clipping; callers own dimensions, colors, and borders through the class string. The helper adds cache-busting thumbnail URLs with?v={thumbnail_object_id}and falls back to initials from display name, username, or email.project-icon: square project image with a small corner radius and project-initial fallback, backed byuiProjectIconData. PassprojectIcon <project> <class>; the helper adds a cache-busting thumbnail URL and the component owns the square crop and radius.
csrf-field: hiddencsrf_tokeninput. Every form withmethod="post"must include it as{{template "csrf-field" $.CSRFToken}}, which means the data passed to that template needs aCSRFTokenfield populated fromuiSessionCSRFToken(r).TestEveryPostFormRendersACSRFFieldfails the build if a posting form omits it, andTestRenderedPagesCarryAPopulatedCSRFTokenfails if a construction site leaves it empty.option-dropdown: expanded dropdown/listbox for choosing one option and submitting immediately. Backed byuiOptionDropdownData; use for compact enum-like changes such as issue status, close reason and worker. Options and the current value take an optional LucideIcon/CurrentIcon.autocomplete-inputandautocomplete-options: shared search/autofill building blocks backed byuiAutocompleteEditDataanduiAutocompleteOption. Supports local option filtering, optional hidden target values, addressable option containers, collapsible suggestions, and optional debounced HTMX refresh on input for server-filtered suggestions.autocomplete-edit: search-style edit row with suggestions and save/cancel actions. Use for member, sprint, and similar lookup fields.
modal-openandmodal-close: reusable modal shell with title, optional description, badges, and cancel action. Wrap workflow-specific body content between the two templates. Client-controlled modals may setOpenwhen a validation response must keep the modal visible; the shared script focuses the first form control after open or an HTMX swap.- Add/create controls on management pages open this shared modal instead of permanently rendering a second form beside existing content. Dedicated creation pages remain full-page workflows; Context remains the documented integrated-manager exception.
image-picker: shared client-controlled profile/project image modal backed byuiImagePickerData. Keep only the current avatar/icon and its Add/Change trigger in the owning panel; file selection, upload, and removal live inside this modal. Profile previews remain circular and project previews remain square with a small corner radius.- Issue-scoped relationship edits should prefer modals when the user is making a small local change from issue detail. Issue Context is the deliberate exception because browsing and editing multiple documents benefits from its integrated manager. Other modal workflows should keep the surrounding issue visible, avoid URL pushes for open/submit/close, support repeated HTMX updates, and link out when the task expands.
- Issue tag modal convention: show attached tags first, then a searchable list of available project tags. Attach/detach existing tags only; create/edit/delete project tags stays in the project tag manager.
issue-summary-row: responsive issue list row content accepting auiIssueItem. It stacks key/priority, title/tags, and due/status metadata on mobile, then restores the compact four-column row fromsmupward. WhenSubIssueProgress.Totalis non-zero, it also shows the shared compact completed/total ring used on sprint cards. It showsissue-worker-badgebefore the due badge when the issue is marked. WhenSprintBadgeis set, it showsissue-sprint-badgeafter the due badge. Only lists that are not already grouped by sprint set it.issue-delete-notice: restore notice shown after deleting an issue.- Shell responses:
renderUIShellrenders the whole document for a navigation and onlyshell-main-contentfor an htmx request. Every htmx control targets a sub-element, and#mainis a sibling of the sidebar inside.app-shell, so answering htmx with a document swaps a second header, sidebar, and#maininto the existing#main. Add new whole-page panels toshell-main-content, never to theshell-mainwrapper. error-panel: shell-hosted error page backed byuiErrorPanelData(status, title, message), with copy fromuiErrorPageFor. Rendered throughrenderUIShellso signed-in visitors keep their sidebar. Unmatched URLs get it fromuiNotFound. In the signed-in route group, theuiErrorPagesmiddleware turns any plain-text 4xx/5xx (writeUIStoreError,http.Error) answered to a browser page navigation (aGETthat acceptstext/htmland isn't htmx) into this page with the same status; htmx, fetch, image, and websocket requests keep the plain-text body. Add new whole-page error states here rather than hand-rolling error markup.- Context detail row: issue detail uses a Details-sidebar row labeled
Context, acount-badge, and a compact book-open action that opens the integrated issue Context manager and pushes its URL. Project About does not render context; project context is a top-level tab.
project-favorite-action: project header star toggle backed byuiProjectFavoriteData/uiProjectPanelData. Keep it adjacent to the project title and update only the action wrapper plussidebar-favorites.project-panel-context: integrated project Context tab ininternal/server/templates/project_panel_context.html; expectsuiContextManagerDatathroughuiProjectPanelData.ContextManager. It owns the ordered page list, selected Markdown page, and complete linked-issue list in a separate section below the document card.- Tokens page: API tokens keep a per-row list with individual revoke; web sessions collapse to a live count and one Revoke all web sessions action. Sessions are numerous and their names carry no information, so a row each buried the tokens people actually manage. The action revokes the caller's own session too and signs them out.
- Tokens page, Connectors section: registered OAuth clients keep a per-row list with individual revoke, between API tokens and web sessions. Connector access tokens collapse to a live count for the same reason sessions do — they are reissued hourly by the connector itself. Registration uses the shared
modal-open/modal-closemodal throughoauthClientCreateModal, which follows the API token modal's create-then-reveal-once convention:ClientControlledwithOpenset when there is an error to show or a secret to reveal. The client secret is hashed at rest, so the modal is the only place it is ever displayed. oauth-consentandoauth-errorininternal/server/templates/oauth_consent.html: standalone documents backed byuiOAuthConsentDataanduiOAuthErrorData, built on theauth-page-open/auth-page-closesign-in card rather than rendered throughrenderUIShell. They are interstitials shown on behalf of a third party, not pages of the product, so they deliberately carry no sidebar or shell chrome. SeeOAUTH.md.project-delete-modal: destructive project deletion dialog ininternal/server/templates/project_panel.html, backed byprojectDeleteModaland theDeleteProject*fields onuiProjectPanelData. The project actions menu links to/{owner}/projects/{key}/delete, which re-renders the panel with the dialog open; the same URL performs the deletion on POST. It states the consequences, requires the project key to be typed, and is only rendered whenCanDeleteProjectis set. Destructive actions this broad get a typed confirmation rather thanhx-confirm, which stays right for single-row deletes.range-controlincomponents.html: a segmented row of links that reloads the current project view over a different window, backed byuiRangeControl/uiRangeOptionand built in templates withrangeControl "<aria label>" .Options. The active option carriesaria-current="true". Insights uses it for its date range and In progress for its completion window; reuse it rather than drawing another segmented control.project-panel-progressininternal/server/templates/project_panel_progress.html: the In progress view shown instead of Planned when sprints are off, backed byuiProjectProgressData(built inui_project_progress.gofrom the sharedprojectProgresshelper that also serves the API and MCP). It renders theIn progressandRecently completedsections with the sharedissue-list; recently completed items setuiIssueItem.CompletedAt, whichissue-summary-rowshows before the status badge.ProgressNoticeon the panel shows a one-line explanation above both sections.project-panel-insightsandinsight-chartininternal/server/templates/project_panel_insights.html: the project Insights view and its reusable chart card, backed byuiProjectInsightsDataanduiInsightChart(built inui_insight_charts.go). A chart card owns its title, description, stat row, legend toggles, plot, notes, andData tabledisclosure. Kinds areline,area(stacked),bars(grouped, one column per period), andscatter. Lines and areas draw in a stretched0 0 1000 1000viewBox with non-scaling strokes; text, dots, bars, and gridlines use percentage coordinates so they keep their shape at any width. The server embeds tooltip data as JSON indata-insight-chart, andapp.jsadds the crosshair, per-series hover markers, tooltips (from thedata-insight-tooltip-*templates, filled withtextContent), arrow-key stepping, tap-to-inspect, and legend toggles. Nothing writes inline scripts or styles, which the CSP forbids: the tooltip lives in an SVGforeignObjectthat moves by itsx/yattributes. SetEmptyfor an empty state instead of drawing an empty plot.helpdesk-panel: a help-desk reporter's whole view of a project ininternal/server/templates/helpdesk.html, fromuiHelpDeskPanelData(Viewisnew,issuesorissue). It must never carry anything a reporter may not see: build it frommodel.ReporterIssueand shared comments only, render Markdown without attachment targets, and name comment authors by display name, never email. The new-issue form ends with the one sanctioned trackslash promotion line (data-helpdesk-promo); keep it a muted footer note, not a banner or card.project-member-page: full project access manager ininternal/server/templates/project_member_page.html; expects member, access-mode (AccessSettings.AccessMode), sprint-mode (Project.SprintsEnabled,SprintModeLocked,SprintModeError), and blocked-user fields onuiProjectPanelData. Keep the owner fixed, use avatar/name/username rows with inline role selectors, offer the access modes as one radio group fromprojectAccessModeOptions(never separate switches that can combine into a mode that does not exist), keep theSprintssetting in its own form besideAccessand lock it while a sprint is active, require an exact username when blocking, and re-render the page after each mutation.project-panel-whiteboard: integrated project Whiteboard tab ininternal/server/templates/project_panel_whiteboard.html; expectsuiWhiteboardDatathroughuiProjectPanelData.Whiteboard, built byuiBuildProjectPanelso the project header keeps every permitted action. It owns the most-recently-updated-first title list and one selected, created, or edited Markdown page, reusingdescription-editor(without upload configuration) anddescription-body. It never renders issue-linking or attachment UI.context-manager-panel: routes issue mode to the integrated list/document manager ininternal/server/templates/issue_context_manager.html; project mode remains a compatibility fallback because project Context normally renders throughproject-panel-context.description-body: shared safe Markdown display backed byuiDescriptionBodyData. Project, issue, and sprint adapters pass attachment-scoped rendered HTML.description-editor: shared Markdown textarea backed byuiDescriptionEditorData, with optional upload and attachment-list URLs. Creation forms omit upload configuration until a parent ref exists.description-attachment-list: shared project/issue/sprint attachment rows backed byuiAttachmentListData, including previews, metadata, Markdown copy, download, delete, pagination notice, and editing state.sprint-description: shared active/planned/completed-history sprint cropped-Markdown preview backed byuiSprintDescriptionDataor the matching fields onuiPlannedSprint. It lazily swaps full Markdown and attachment rows throughSee morewithout affecting the sprint-issues disclosure.
- Project tab route:
/{owner}/projects/{key}/context; selected pages use/context/{contextRef}. Issue manager route:/{owner}/issues/{issueRef}/context, with the same selected-page suffix. - Project pages support create/import/edit/delete, page-scoped attachments, ordering, and linked-issue management. The page list stays compact without per-page issue counts; only the selected page renders content, followed by its complete linked-issue list and count.
- Markdown pages use the shared safe Markdown renderer and attachment components. Plain-text imports remain escaped and pre-wrapped.
- Issue manager mode supports creating and editing issue-scoped context, attaching and viewing project pages read-only, and removing links in the same responsive list/document pattern as project Context.
- User-facing attach/search controls use context titles. Do not present refs such as
context-1as visible identifiers, badges, placeholders, or option labels.
- Project tab route:
/{owner}/projects/{key}/whiteboard; selected pages use/whiteboard/{whiteboard-N}, with/new,/{ref}/edit, and/{ref}/deletefor writers. - Pages are title plus Markdown only. They render through the shared safe Markdown pipeline with no attachment store, so external images stay inert links and
object-Nrefs stay text. - Delete uses a single
hx-confirmaction. Refs such aswhiteboard-1stay in URLs and API/MCP mechanics, never as visible row labels.
When adding a reusable component, document its template name, purpose, and expected data shape here.