Purpose and Status
This is the living implementation guide for Ghostwriter’s UI refresh. Read it before changing the application shell, adding a page, modernizing an existing template, or auditing a page for consistency. The refresh is still in progress. The client, project, and report dashboards have received the most review, along with their related workflows and selected forms. The main dashboard, libraries, findings, observations, evidence, report templates, and administrative pages are at different stages of migration. A page using some new classes is not necessarily fully reviewed. Ghostwriter is an operator workspace for red teamers who need to move between engagement context, reporting content, infrastructure, and operational records quickly. The interface should feel like a focused operations console: dense enough to be useful, calm enough for long work sessions, and clear about the next action. The refresh is not an attempt to make Ghostwriter look like a generic SaaS application. Every visual element should help an operator answer one of these questions:- What engagement or object am I working on?
- What requires my attention?
- What can I do here?
- Where can I go next?
Non-Negotiable Constraints
Offline operation
Ghostwriter supports offline and air-gapped deployments. All fonts, icons, CSS, JavaScript, and component dependencies must be packaged in the repository. Do not add CDN references, remote font imports, analytics scripts, or runtime requests for UI assets. Bootstrap 5.3.8, Font Awesome, Poppins, Metrophobic, Roboto Mono, jQuery, Tiptap, and the other current dependencies are served from the repository. Vite packages Tiptap and its dependencies intojavascript/dist_frontend/; Django serves that output from
the local static asset path.
Progressive migration
The current UI is Django-rendered and still uses mature jQuery components. The direction is:- Establish a design that operators like in the server-rendered application.
- Standardize semantic tokens and reusable component patterns.
- Replace aging jQuery components incrementally.
- Make those patterns portable to a future React application or SPA.
Security
UI work must preserve the XSS hardening introduced around pull request 945.- Escape user-controlled values inserted into JavaScript with
escapejs. - Pass structured data to JavaScript with Django’s
json_scriptfilter. - Populate autocomplete tag lists through
get_tags_for_queryset(). - Do not interpolate untrusted values into executable script, inline event handlers, selectors, or raw HTML.
- Do not add
safemerely to make new markup render. - Keep Bootstrap popovers, editor output, and rich content sanitization enabled.
Behavior before appearance
Modernization must not regress existing workflows. Preserve permissions, form saving, collaborative editing, sorting, filtering, drag-and-drop ordering, hash navigation, keyboard behavior, and editor initialization.Source of Truth
The UI styles are currently split across migration layers:
New work should prefer semantic
--gw-* tokens and shared classes in
design_system.css or app_shell.css. Do not copy a legacy rule merely because it
already exists. --primary-color is still green and remains widely used by older
selectors; it is migration debt, not permission to use green for every interaction.
The older Form Layouts & Design guide remains useful for Django forms, Crispy helpers,
and formset architecture. This guide takes precedence where its visual advice differs:
refreshed forms normally keep useful labels visible, group fields by task, and use the
spacing and card patterns described below.
Use Bootstrap utilities for ordinary spacing and layout. Prefer mb-3, gap-3,
d-flex, or the grid utilities over a custom class that only adds the same margin or
display rule. Create a custom class when it identifies a reusable Ghostwriter
component or carries behavior that Bootstrap does not express.
Rich-Text Editing
Phase 1 of the editor migration uses the same Tiptap schema for standalone Django forms, Django admin extra-field defaults, and collaborative reporting editors. Standalone editors continue to post HTML through their original<textarea>, so
model fields and server-side form handling do not change.
Source binding and compatibility
- Do not replace a Django textarea with a new field name. The original textarea is the submission source and remains in the DOM as a visually hidden, validatable control.
- Loading a field must not rewrite its source HTML. Tiptap may normalize the editing DOM, but the textarea keeps the exact original value until the operator edits it.
- Existing TinyMCE-era HTML must retain headings and bookmarks, combined bold/italic/underline classes, text color, font family and size, alignment, nested lists, blockquotes, code blocks, horizontal rules, tables and captions, table cell color, page breaks, links, and Ghostwriter’s evidence or client-logo nodes.
- Ordinary HTML
<img>elements are not part of this compatibility contract. Operators upload image evidence through Ghostwriter’s evidence workflow; editors store references as structured evidence nodes. - Reject unsafe link protocols and sanitize retained style attributes. Compatibility is not a reason to permit arbitrary HTML or CSS.
- Run
npm run test:html-compatibilityafter changing the shared schema or HTML serialization behavior.
Editor profiles
Use field intent to choose the editing surface:data-tiptap-min-height may set a stable minimum for a whole form or modal. Use
no-auto-rich-text for JSON, command output, code-like text, or any textarea that
must remain plain.
Lifecycle
The shared base loads a small bootstrap module first; the React/Tiptap runtime is requested only when a usable textarea exists or is inserted later. Standalone editors then initialize only when usable:- Editors in inactive Bootstrap tabs wait for
shown.bs.tab. - Editors in closed
<details>collections wait for the card to open. - Hidden formset prototypes never initialize.
- Newly inserted forms are detected automatically. Code that deliberately opens a
stable collection can call
window.gwInitTiptapStableContainer(container). - Code replacing modal or formset markup should call
window.gwDestroyTiptapEditors(container)before removal andwindow.gwInitTiptapTextareas(container)after insertion. - Initialization must not focus an editor, change
scrollY, or add a document-level horizontal scrollbar.
npm run build-frontend-prod.
Phase 1 does not change collaborative persistence or solve API-versus-collaboration
write conflicts. Those require a later concurrency design; do not make standalone
form work silently overwrite an active Yjs document.
Visual Direction
Attention hierarchy
Ask one question before styling a page:What is the one thing an operator should notice first, and why?Spend visual emphasis there and quiet everything else. A page should not contain a bright title, saturated primary button, multiple colorful badges, and a highlighted instruction panel all competing for attention. Typical hierarchy:
- Object identity or the operator’s immediate work.
- Current status and engagement context.
- Primary task or next action.
- Supporting content and secondary actions.
Color system
Use color by meaning, not decoration.
Green was historically Ghostwriter’s general primary color. That made success
indistinguishable from a routine action. In refreshed components, reserve green for a
successful, healthy, available, or completed state. Use violet for current context and
selection, neutral surfaces for ordinary actions, and coral or amber only when the
content genuinely requires attention.
Legacy controls, focus rings, and checked inputs may still use
--primary-color.
Audit these over time rather than extending this assumption to new components.
Severity is domain data, not a fixed decorative color. Finding severity colors come
from configured Severity.color values. A severity glyph, dot, or restrained badge
should follow that configured color and fall back to information slate when the value
is invalid. Do not make every bug icon red.
Surfaces, depth, and shape
The foundation uses quiet layering rather than large color blocks:- Canvas:
--gw-canvas - Primary card:
--gw-surface - Subtle inset or header:
--gw-surface-muted - Boundary:
--gw-border - Strong text:
--gw-text-strong
--gw-radius-sm, --gw-radius-md,
--gw-radius-lg) and shadow scale (--gw-shadow-xs, --gw-shadow-sm,
--gw-shadow-md). Most cards should use a border and a small shadow. Large shadows
are reserved for floating menus, modals, or content that truly sits above the page.
Avoid nested cards whose only purpose is decoration. A card should establish a real
section, field group, object, or workflow boundary.
Typography
All fonts are packaged locally.
Use display typography with restraint. Most pages need one strong identity heading,
not oversized headings inside every card. Limit prose to a readable line length,
usually around 65–72 characters, without limiting the width of the entire tab or
dataset.
Default to left-aligned text. Justified prose may be appropriate for longer narrative
content. Center alignment is reserved for a small, intentional empty state or compact
summary—not instructions, form fields, finding titles, or operational data.
Application Shell and Navigation
Sidebar
The sidebar follows an operator-tool rail model:- The collapsed state is a persistent 4rem icon rail.
- The Ghostwriter logo is always visible.
- The expand control belongs inside the sidebar below the logo.
- When expanded, the collapse control shares the navigation toolbar with Customize.
- Clients, projects, and reports are core destinations.
- Optional areas can be pinned, hidden, and reordered through sidebar preferences.
- Working context and Pinned work are independent engagement panels. Users can show, hide, or reorder them; the saved layout applies to both the expanded sidebar and compact rail.
- Profile, theme, and logout remain available without consuming the primary tool area.
app_shell.css: the rail changes width with a
short ease-out, expanded navigation appears after there is enough room, and account
and theme controls appear last. Do not reveal wide controls while the sidebar is
still collapsed. Preserve prefers-reduced-motion behavior.
Working-context bar
The sticky working-context bar replaces the old breadcrumb bar and the sidebar’s Jump to Report shortcut. Some implementation classes still useengagement-context for compatibility. The visible language is Working on, not
the ambiguous Active project or Activate report.
It provides direct access to:
- Working report
- Client
- Project
- Activity logs belonging to the working report’s project
- Draft or complete state
- Delivered or not delivered state
Working reports and pinned work
Working state and pinned navigation are intentionally different:
Pinning an object must never silently make a report the working report. A pinned
report may offer a separate target control for explicitly choosing it. Pinned clients
and projects only navigate or reveal their reports.
The expanded sidebar shows named pinned work. The compact rail shows the same pins as
neutral icon shortcuts with full tooltips; the separate bullseye remains the only
working-report control. Dynamic pins are permission-filtered on every render.
Only the navigation section for the page being viewed receives the filled active
treatment. Working context uses a small violet target indicator, while pinned objects
remain neutral even when a pin points to the current page. Location, destination, and
saved shortcut must not compete as three active navigation states.
Anywhere a library item can be copied globally:
- Show the exact destination as
Adding to: <report>. - Send that displayed report ID in the request; do not rely only on session state.
- If no report is selected, open the chooser and complete the pending add after selection.
- Name the destination report in the success message.
- Use
Set as working report,Working report, andSwitch reportconsistently.
Page Patterns
Detail and dashboard headers
Usedetail-page-heading and the applicable page-specific modifier.
The standard order is:
- Small semantic eyebrow such as Client, Project, Report, or Finding.
- Object name in strong neutral text.
- Compact tags for status, type, ownership, or other high-value context.
- A neutral Actions menu aligned to the right.
CLIENT + PROJECT TYPE. Put supporting
identity in badges. Long names must wrap or truncate without pushing actions off the
viewport.
Do not use a hamburger menu for page actions. Use an Actions button with one clear
disclosure affordance. Avoid combining an ellipsis and a down arrow when both mean
“more.”
Library pages
Use the library pattern demonstrated byclient_list.html, finding_list.html, and
observation_list.html:
- One page heading and one concise description.
- A result count based on the accessible, filtered queryset—not a global total.
- Create/import actions on the right.
- Filters in their own toolbar or region.
- The table begins without a redundant second title and description.
- Empty states distinguish “no objects exist” from “filters matched nothing.”
Tabs
Use#tab-bar.nav-tabs.
- Tabs wrap to a second row.
- Tabs do not require horizontal scrolling.
- Every tab remains reachable at narrow widths.
- The selected tab uses a quiet surface and contextual color.
- Tab content starts with consistent top spacing.
- Hash navigation remains stable.
Cards and sections
Section actions such as Add a Project or Add & Edit Assignments belong inline with the section header when space permits. Do not leave a small button alone above a wide empty area. Tables and lists should meet the bottom of their containing card cleanly. Avoid an arbitrary gap between the last row and the card boundary. Keep internal spacing even and use Bootstrap spacing utilities where possible. General information, overview, planning, and status tabs should use the available viewport like the neighboring tabs. Constrain prose, not the entire tab. Let grids add columns and cards stretch at larger widths without creating excessively long text lines.Empty states, instructions, and alerts
Useempty-state, empty-state-title, and empty-state-description.
State what is absent and whether the user can act:
- “No assignments yet” identifies a real empty collection.
- “No matching findings” identifies a filter result.
- “Connections unavailable” explains why an action cannot happen.
Tables, Lists, and Repeated Work
Use a table only when the relationships between columns matter. Object details, configuration values, and narrative content usually work better as definition lists, cards, or structured rows. For genuine tables:- Wrap with
table-responsive data-table-frame. - Align titles and descriptive text left.
- Style record links with
table-primary-link: bold neutral text at rest, then engagement violet with a thin underline and right-pointing arrow on hover or keyboard focus. The shared component reserves the arrow’s space so the row does not shift. Do not use a persistent dotted green underline in tables; green remains reserved for success and healthy states. Addlibrary-primary-linkalongside the shared class only when the library-row arrow element is present. - Keep search/filter controls separated from the table with normal Bootstrap spacing.
- Use a flat header edge when a column chooser, filter summary, or other row sits above the column headers.
- Render empty cells deliberately so a row does not appear to end abruptly.
- Use one drag handle glyph. Rotate or animate the glyph, not the entire cell.
- Keep row actions consistent with
report-row-actions.
Calendars
Usegw-calendar on FullCalendar roots. Calendars are operational schedules, so their
grid, controls, and current-day treatment should remain quiet:
- Use neutral surfaces and boundaries for calendar chrome.
- Use engagement violet, context blue/orchid, and information slate to distinguish scheduling categories.
- Do not use signal green for an ordinary project or execution window.
- Keep the calendar frame, controls, dates, list view, and events on the shared radius scale.
- Prefer
listWeekon narrow screens rather than compressing a seven-day grid into unusable columns. Do not overwrite the user’s saved desktop view from a mobile session.
Forms and Editors
Field layout
- Put labels above their controls.
- Keep help text directly below the field it describes.
- Never allow the next field label or an Edit JSON button to share a help-text line.
- Use consistent vertical rhythm, normally Bootstrap
mb-3. - Align toggles with their labels and use Bootstrap
form-switch. - Keep Save and Cancel actions consistent and visually grouped.
- Place Cancel before the principal Save or Create action, with the principal action on the right.
Editing workspaces
Use the client, project, report, domain, and server forms as references for a task-oriented editing workspace. The following primitives are shared across object types:resource-form-page-headingfor the page eyebrow, task heading, and scope sentence.resource-form-shellfor a fluid, overflow-safe form workspace.tabbed-form-shellwhen the workspace contains task-based tabs or hidden editors.form-section-headingfor lightweight grouping inside a tab.resource-edit-form,resource-form-grid, andresource-form-cardfor dense, independently understandable tasks.collection-toolbarfor a repeated collection’s explanation and Add action.resource-form-actions,resource-form-actions-context, andresource-form-actions-buttonsfor the persistent Save/Cancel surface.resource-form-actions-compactfor smaller single-purpose forms that do not need contextual action copy.
- Start with
resource-form-page-heading: a semantic eyebrow, an action-oriented heading such as Create client or Edit Client Name, and one sentence that explains the scope. - Let the editing workspace use the available viewport so tabs, repeated collections, and action surfaces align with the surrounding page. Constrain explanatory copy and long prose where needed rather than imposing a fixed width on the entire form.
- Group fields by the operator’s task. Use small
form-section-headingelements to distinguish identity, profile, delivery, access, or other coherent concerns. - Put labels above controls and help immediately below them. A grid row is for related fields, not merely for filling horizontal space.
- Use a compact, sticky action surface when tabs or repeated items make the form long. Save and Cancel remain reachable without turning them into full-width desktop bars.
- Name submit actions for the task: Create Client, Save Changes, or another specific result. Do not rely on a generic Submit label.
- Use violet for the principal editing action. Reserve green for successful, completed, or healthy state.
- Clients: identity/profile, points of contact, access, and extra fields.
- Projects: engagement identity/schedule, assignments, access, integrations, and a separate component workspace.
- Reports: report identity/engagement and output templates.
- Domains: identity/lifecycle, health, and extra fields.
- Reusable servers: identity/context, alternate addresses, and extra fields.
- Project servers, evidence, templates, deconfliction records, checkouts, and connections: coherent resource cards with a compact or contextual action surface.
- Findings and observations: task-based collaborative tabs with automatic-save status. Do not add a manual Save surface to an automatic-save editor.
Form cards
Useresource-edit-form, resource-form-grid, and resource-form-card for dense
resource forms. A card should contain one coherent task and may use
resource-form-section-heading for an icon, heading, and short explanation.
Extra fields belong in a dedicated tab on long forms. Render each extra field in a
card so its label, value, help, and Edit JSON action have a clear boundary.
Formsets need explicit workflow review:
- Add and delete actions should occupy predictable locations.
- Repeated forms should not create unexplained vertical gaps.
- Delete controls should be consistent across every repeated item.
- Adding, deleting, and reordering must work before and after validation errors.
- Keep Django’s inline formset as the validation and submission layer.
- Present existing objects as collapsed
collection-form-cardsummaries. - Put the collection description and Add action together in
collection-toolbar. - Show a specific
collection-empty-statewhen no submitted objects exist. - Open new entries and entries with validation errors.
- Update the summary from form controls with
textContent/jQuery.text(), never raw HTML. - Do not count or initialize the hidden
empty_formprototype. - Use the hidden
DELETEfield for removal and retain the existing undo state. - Mount rich-text editors in a closed collection card only after the card opens.
- Show a neutral count badge on the tab and update it after add, remove, and undo.
File uploads
Use the resource upload-card/dropzone pattern. The file input may cover the dropzone, but it must not cover unrelated controls or the form action row. Display the current filename and accepted formats clearly. Template uploads should appear early because replacing the template file is a frequent edit task.Rich text
Tiptap must be usable in light and dark themes:- Editor canvas, toolbar, menus, headings, and icons use the active theme.
- Text has sufficient contrast.
- Editors do not render collapsed.
- Opening a form must not temporarily block scrolling.
- Switching tabs must not flicker or move the viewport.
- Hidden editors initialize lazily or off-screen without disturbing layout.
Do not place a full authoring toolbar on every repeated row. Compact and collection
editors should remain lazy so a form with many contacts or assignments does not mount
every editor during page load or tab selection.
Dark Theme
Every refreshed component must be checked in both themes. Do not assume a light surface will invert automatically. Common regressions:- White note or editor cards in dark mode
- Bright white tags
- Black narrative text on black backgrounds
- Light dropdown menus opened from dark controls
- Logos that disappear because one asset is used for both themes
- Muted text with insufficient contrast
- Empty table cells losing their dark surface
Motion and Interaction
Motion should confirm an interaction, not perform for its own sake.- Prefer color, underline, a 1–2 px translation, or a very small scale change.
- Do not enlarge linked badges or client names dramatically.
- Keep row hover states quiet.
- Animate only the glyph inside an expand control.
- Avoid layout-affecting transitions that produce scroll jumps.
- Respect
prefers-reduced-motion.
Reference Surfaces
Use these as current pattern references, while remembering the migration is incomplete:Page Audit Checklist
Use the installed Playwright MCP browser for UI review. Do not attempt to connect to a separate browser. Local administrator credentials are available in the workspace.env; do not print them in logs or documentation.
Review at representative widths such as 1440, 1024, 768, and 390 pixels.
Identity and hierarchy
- Is the page’s most important object or task obvious?
- Is there only one dominant visual target?
- Is the title neutral rather than bright green?
- Are supporting details tags rather than an oversized composite heading?
- Can long names wrap or truncate safely?
- Are redundant headings or breadcrumbs removed?
Navigation and actions
- Does the active-engagement bar remain usable?
- Are page actions neutral, consistent, and keyboard accessible?
- Is there one disclosure affordance rather than duplicated ellipsis/arrow cues?
- Do tabs wrap without horizontal scrolling?
- Do actions remain reachable at narrow widths?
Content and data
- Is text left-aligned unless there is a reason otherwise?
- Is a table used only for genuinely tabular data?
- Do search boxes and buttons have normal spacing before the dataset?
- Are empty cells and empty collections intentionally rendered?
- Do status tags communicate meaning without creating pill overload?
- Does repeated work condense before introducing a horizontal scrollbar?
Forms and editors
- Are labels, controls, help text, and actions clearly separated?
- Are fields evenly spaced with Bootstrap utilities where appropriate?
- Are checkboxes and switches aligned with their labels?
- Are extra fields in a dedicated tab and individual cards when the form is long?
- Are file inputs bounded to the visible upload area?
- Do formsets add and delete items without gaps or alignment changes?
- Does Tiptap render at a usable height without flicker or scroll jumps?
Theme, accessibility, and responsiveness
- Does the page work in light and dark themes?
- Is visible keyboard focus preserved?
- Are text and muted metadata readable?
- Does motion respect reduced-motion preferences?
- Is there no document-level horizontal overflow?
- Are dropdowns, tooltips, modals, and alerts within the viewport?
Security, behavior, and offline support
- Are JavaScript values escaped or delivered with
json_script? - Do autocomplete tags come from
get_tags_for_queryset()? - Are user-controlled values absent from raw HTML and executable strings?
- Do saving, sorting, filtering, drag-and-drop, and collaboration still work?
- Does the browser make no external asset requests?
- Are all new dependencies stored in the repository?