Help Page House Style - edmozley/freeitsm GitHub Wiki

Every module in FreeITSM has an in-app guide โ€” 26 module pages, from tickets/help.php down to workflow/help-ssl.php, plus the 19 admin-area guides and the landing page under system/help/. They all look the same because they all use one stylesheet: assets/css/help.css.

This page is the rule they follow. Read it before writing a new guide, and before adding anything to an existing one.

โš ๏ธ The System help pages were missed by the original sweep, and only came onto the house style in 1.8.0 โ€” five weeks later. They were built on 26 June 2026, six weeks before help.css existed, so the August sweep of "every guide" never saw them, and they quietly carried the 27th private copy of the layout. Everything the next section describes as a solved problem was still live in them: a 118-line block under a syshelp- prefix, hardcoded indigo instead of theme tokens, a decorative .highlight band with margin: 0 -48px bleeding 48px wider than its neighbours โ€” the identical CMDB bug โ€” a contents rail display:noned below 900px, and no indent model at all, so headings and body copy sat at the same level on every page.

๐Ÿ”‘ The lesson is about the sweep, not the pages. "Every help page" was resolved in practice as */help.php, which structurally cannot match system/help/<topic>.php. A pattern that answers a slightly different question than the one you asked reports complete, and looks complete. When you sweep a category, enumerate the category and count it โ€” do not assume one glob is the same shape as the idea.


Why this exists

Until August 2026 there was no house style. There were 26 copies of one.

Each guide carried its own <style> block โ€” the same ~280 lines of layout under its own prefix: cal-help-*, ct-help-*, kb-help-*, tk-help-*, wt-help-*, and a dozen more. Every copy was individually reasonable. The problem was that changing one never changed the others, so they drifted:

  • The CMDB guide tinted sections 2, 4 and 8 with a grey band, for no rule anyone could state โ€” and those sections bled 48px wider than their neighbours, so "level 1" appeared to be a different width depending where you were on the page.
  • .cmdb-help-section > p had no indent at all, so three paragraphs sat at level 1 while the steps and cards beside them were indented. Three others had been patched by hand with an inline style="margin-left:46px".
  • Twelve pages hand-coloured their feature icons blue / green / orange / purple down a single grid.
  • Seven different class names โ€” tip, callout, warn, gotcha, danger, consequence, example โ€” did the job of one.
  • Most pages simply display: noned their contents list below 900px.

7,657 lines of duplicated styling became 600. The rest of this page is what replaced it.


1. The heading model

This is the rule. Everything else is detail.

level 1   .help-section              full width of the pane, NEVER indented
          .help-section-header         (number) + <h3> + lead <p>
level 2   everything else in it      indented to --help-indent
level 3   .help-subsection           its <h4> sits at level 2,
                                     its content one step further in

A guide reads as a numbered list of sections. Each section spans the full width; everything belonging to that section lines up underneath its heading. That holds at every screen size โ€” on a phone the indent shrinks from 46px to 32px along with the badge that sets it, rather than collapsing to the left edge.

๐Ÿ”‘ The indent is on the parent, not the children

The old pages put margin-left: 46px on each individual component, which is exactly why the occasional paragraph fell out of line โ€” somebody added a <p> and there was nothing to remind them. The house style does it the other way round:

.help-section {
    padding-left: var(--help-indent);      /* everything inside starts here */
}
.help-section-header {
    margin-left: calc(var(--help-indent) * -1);   /* except the heading, which backs out */
}

Anything you drop into a section is aligned correctly by default, and there is no way to forget. Never add margin-left to a help page to line something up by hand โ€” if it isn't aligned, the markup is in the wrong place.

๐Ÿšซ No decorative section bands

Every section looks identical: number badge, heading, indented body, a hairline rule to the next one. There is no "highlighted section" variant, deliberately. A variant that means nothing in particular gets applied inconsistently, which is how this whole mess started. If a section needs to stand out, that is a job for its words.


2. Colour

A page declares its accent once โ€” and that is all it declares

body {
    --accent:       var(--cmdb-accent);
    --accent-hover: var(--cmdb-accent-hover);
    --accent-soft:  var(--cmdb-accent-soft);
    --on-accent:    var(--cmdb-on-accent);
}

Four lines. assets/css/help.css draws everything โ€” the hero gradient, the badges, the note callouts, the card icons, the flow steps โ€” from those four variables. Every module already defines its own set in assets/css/theme.css, in both light and dark palettes, so nothing else needs writing. See Theming & Dark Mode for where those tokens come from.

Tickets is the one exception: it has no accent of its own and uses the application's brand colour, which assets/css/help.css already reads from the global --accent. Its style block says exactly that and nothing else.

๐Ÿ”‘ The rule for everything else: colour that carries meaning stays

The sweep had to decide, a dozen times, whether a splash of colour was information or decoration. The rule that settled it:

Colour that carries the meaning stays. Colour that only repeats the label sitting next to it goes.

Kept โ€” the colour is the information Removed โ€” the label already said it
Service-status dots (operational / degraded / maintenance / outage) Feature-card icons in four unrelated colours down one grid
The morning-checks red/amber/green trend bars and its three RAG cards Flow-chain steps in a per-module rainbow
Change types (standard / normal / emergency) and the change lifecycle badges Knowledge's pending / approved / rejected card borders
The change risk scale, green through to deep red Contracts' four coloured dots beside settings headings
Watchtower's status dots; the LMS progress pills; the mailbox-auth badges

Everything in the "kept" column was moved onto the theme's semantic tokens (--success-*, --warning-*, --danger-*) on the way through, because the originals were hardcoded pastels that only worked in light mode.

โš ๏ธ The dark-mode hero trap

Don't build the hero banner from accent โ†’ accent-hover. On dark palettes the module accents are deliberately lifted so they read on dark surfaces, so a full-bleed banner made from them glares. assets/css/help.css handles this already:

.help-hero { background: linear-gradient(135deg, var(--accent) 0%, var(--accent-hover) 100%); }
[data-theme-mode="dark"] .help-hero {
    background: linear-gradient(135deg, var(--accent-soft) 0%, var(--surface-2) 100%);
    color: var(--text);
}

3. The components

Colour key: ๐Ÿงฑ page skeleton ยท ๐Ÿ“ body copy ยท ๐Ÿ—‚๏ธ grouped content ยท ๐Ÿท๏ธ inline ยท ๐Ÿ’ป code & tables

๐ŸŽจ Class What it is
๐Ÿงฑ .help-container the flex shell; also where the --help-indent / --help-gutter variables live
๐Ÿงฑ .help-sidebar + h3 the fixed contents rail; h3 is a group heading
๐Ÿงฑ .help-nav-link + .help-nav-num one entry; add .active for the current section
๐Ÿงฑ .help-main the scrolling column (the scroll-spy listens on this, not window)
๐Ÿงฑ .help-hero the banner; takes <h1> or <h2>
๐Ÿงฑ .help-content the padded column the sections sit in
๐Ÿงฑ .help-section + -header + -num level 1 โ€” see the heading model above
๐Ÿ“ .help-subsection level 3 โ€” a titled block within a section
๐Ÿ“ plain <p>, <ul>, <ol>, <h4>, <h5> styled as direct children of .help-section; no class needed
๐Ÿ“ .help-lede the one sentence that sets up a whole guide
๐Ÿ“ .help-note a callout. Modifiers: .warn, .bad, .ok. Plain = the module accent
๐Ÿ“ .help-back "โ† Back to โ€ฆ" on a deep-dive page hanging off another guide
๐Ÿ“ .help-muted an aside within a paragraph
๐Ÿ—‚๏ธ .help-cards + .help-card the card grid. Modifiers .cols-1 / .cols-3 / .cols-4; 2 is the default
๐Ÿ—‚๏ธ .help-card-icon the tinted icon tile. .plain for an emoji (no tile)
๐Ÿ—‚๏ธ .help-card.row the one sanctioned card variant: icon left rather than above
๐Ÿ—‚๏ธ .help-card-eg the italic "e.g. โ€ฆ" line at the foot of a card
๐Ÿ—‚๏ธ .help-steps + .help-step + .help-step-num a numbered procedure
๐Ÿ—‚๏ธ .help-defs + .help-def + -term + -desc term-and-definition rows
๐Ÿ—‚๏ธ .help-list a plain stack of tinted rows
๐Ÿ—‚๏ธ .help-flow + .help-flow-step + .help-flow-arrow A โ†’ B โ†’ C. Steps are neutral; .accent marks one
๐Ÿ—‚๏ธ .help-diagram + -node + -line a monospace tree drawing
๐Ÿ—‚๏ธ .help-timeline a worked example told in order
๐Ÿท๏ธ .help-pill a status tag. Modifiers .ok .warn .bad .info; plain = neutral
๐Ÿท๏ธ .help-dot a small coloured status dot
๐Ÿ’ป .help-code, <pre> a code block (same dark scheme in both themes โ€” a terminal should look like a terminal)
๐Ÿ’ป <code>, .help-kbd inline identifier; a keyboard key
๐Ÿ’ป .help-table a wrapper <div> around the <table>, so a wide table scrolls inside its section instead of stretching the page
๐Ÿ—‚๏ธ .help-tabs + .help-tab-panel 3.5.0. One guide per product inside a section: buttons with data-tab, panels with the same data-tab and hidden on all but one. The page's own script switches them. Without JavaScript all panels show stacked, so nothing is lost
๐Ÿ—‚๏ธ .help-checklist + .help-check + .help-check-title 3.5.0. A tick-off step: <label class="help-check"><input type="checkbox" data-check="id"> + .help-check-body holding .help-check-title and the description. .done strikes the title. Ticks are kept in localStorage by the page (never anything that matters)
๐Ÿ—‚๏ธ .help-progress + .help-progress-bar > span 3.5.0. "3 of 5 done" and a bar, for a checklist
๐Ÿท๏ธ .help-ext 3.5.0. A link that opens in a new tab (target="_blank" rel="noopener"); adds โ†—. Use it for System โ†’ X links in a step-by-step guide, so the guide stays open beside the screen being set up

The first page to use the tabs and checklist is system/help/sso-groups.php, a step-by-step guide. Its sgCheck() / sgLink() helpers are the pattern to copy. โš ๏ธ Only .help-check-title is a block: a <strong> in a step's description stays inline (an early version styled every strong as a block and broke the sentences).

Note the pill and note modifiers are named for what they mean, not for a colour โ€” .help-note.bad, not .help-note.red. That is what lets them follow the theme.


4. Writing a new guide

<link rel="stylesheet" href="../assets/css/theme.css?v=22">
<link rel="stylesheet" href="../assets/css/inbox.css">
<link rel="stylesheet" href="../assets/css/help.css?v=1">
<style>
    /* The only thing a help page should need to say for itself: its colour. */
    body {
        --accent:       var(--mymod-accent);
        --accent-hover: var(--mymod-accent-hover);
        --accent-soft:  var(--mymod-accent-soft);
        --on-accent:    var(--mymod-on-accent);
    }
</style>
<div class="help-container">
    <div class="help-sidebar">
        <h3>Guide</h3>
        <a href="#overview" class="help-nav-link active" data-section="overview">
            <span class="help-nav-num">1</span> Overview
        </a>
    </div>

    <div class="help-main" id="helpMain">
        <div class="help-hero">
            <h2>My module guide</h2>
            <p>One sentence on what the module is for.</p>
        </div>

        <div class="help-content">
            <div class="help-section" id="overview">
                <div class="help-section-header">
                    <span class="help-section-num">1</span>
                    <div>
                        <h3>Overview</h3>
                        <p>The lead paragraph.</p>
                    </div>
                </div>

                <!-- everything from here is level 2 and indents itself -->
                <p>Body copy.</p>
                <div class="help-cards">โ€ฆ</div>
                <div class="help-note">A tip.</div>
            </div>
        </div>
    </div>
</div>

Copy the scroll-spy script from any existing guide โ€” it listens on #helpMain and filters on [data-section].

Writing a System help page โ€” the registry does most of it

The guides under system/help/ use the same classes, but they are assembled differently: one registry drives everything, so a page cannot disagree with its own navigation.

<?php require __DIR__ . '/_init.php';
      $helpSlug = 'carddav';          // the key in system/help/_registry.php
      require __DIR__ . '/_top.php'; ?>

<div class="help-section" id="setup">
    <div class="help-section-header"><?php echo helpSectionNum('setup'); ?>
        <div><h3>Setting one up</h3><p>The standfirst.</p></div>
    </div>
    <p>Body copy.</p>
</div>

<?php require __DIR__ . '/_bottom.php'; ?>

_registry.php owns the hero, the standfirst, the ordered section list and a terms synonym string. From that one entry you get the <h1>, the sidebar rail, the section badges, and the page's deep-linkable rows in the landing-page search. The card's icon, title and description are not repeated there โ€” they come from getSystemAreas(), so a card and its help page cannot drift apart, and an area with no registry entry simply gets no card instead of a link to a page that does not exist.

Two things to know:

  • ๐Ÿ”‘ A section on the page but not in the registry gets no badge, no nav entry and no search row. That is the design, not a bug โ€” but it means adding a section means editing two files. Count them against each other before you ship.
  • โš ๏ธ terms is a SECOND search haystack, separate from the System landing card's *_keywords in lang/en/system.php. A new feature needs its vocabulary in both, or it is half-findable. CardDAV shipped complete, tested and documented and still could not be found by searching for "address book", because only one of the two had been updated.

System help is English-only, consistent with the System module, so its content is written inline rather than through lang/ โ€” which is also why the class-names-in-translation-strings trap below does not apply to it.

If you find yourself adding layout CSS to a help page, it belongs in assets/css/help.css so every module gets it. What legitimately stays in a page is a diagram of that module and nothing else: the knowledge guide's mock AI conversation, morning checks' trend chart, watchtower's drawing of a real module card, contracts' tab strip, the change lifecycle, the RFP phase strip. Everything else has a 5-to-9-line style block.


5. Traps the sweep found

Worth knowing before you rename anything.

โš ๏ธ Class names live inside translation strings

lang/en/tickets.php contained <span class="tk-help-code">. lang/*/process-mapper.php had pm-help-kbd. lang/*/lms.php had lh-pill โ€” across all 21 locales. A grep of the .php pages reports "clean" while the rendered page is broken.

After any class rename in a help page, grep lang/ as well.

โš ๏ธ Don't type a section's number into its heading

Two failures, both from the same cause:

  • self-service numbered two of its nine sections with an empty badge. The numbers were hardcoded in the headings, two sections had been inserted later, and JavaScript patched the numbers at runtime to paper over it. It now derives the number from the $helpNav array in PHP, so the heading and the sidebar cannot disagree โ€” and it no longer needs JS to show a number at all.

  • workflow rendered "1." twice, once in the badge and once in the heading text, because the number was baked into the translation string. Stripped from the ten headings in en, pt-BR and uk โ€” and note that only 3 of 21 locales had ever carried them, which is its own argument for keeping numbers out of prose.

  • System help went further and numbered its sections entirely in JavaScript: _bottom.php counted .syshelp-section nodes and inserted a badge into each one. That numbers whatever is in the DOM rather than what the sidebar lists, so a section present on the page but absent from _registry.php silently shifted every number after it โ€” and no number appeared at all without JS. Fixed in 1.8.0 by helpSectionNum() in system/help/_init.php:

/** The badge for a section, resolved from the same array that builds the sidebar. */
function helpSectionNum(string $id): string
{
    global $helpNav;
    foreach (($helpNav ?? []) as $i => $s) {
        if (($s['id'] ?? '') === $id) {
            return '<span class="help-section-num">' . ($i + 1) . '</span>';
        }
    }
    return '';          // unregistered id: no badge, rather than a wrong number
}
<div class="help-section" id="passwords">
    <div class="help-section-header"><?php echo helpSectionNum('passwords'); ?>
        <div>
            <h3>Passwords</h3>
            <p>The section's standfirst.</p>
        </div>
    </div>

๐Ÿ”‘ Returning nothing for an unregistered id is the point, not a shortcoming. A missing badge is visible and honest; a number that is confidently wrong is neither. It also gives you a check worth running โ€” count sections against badges against sidebar links, and prove the check can fail by injecting an unregistered section before you trust a pass.

โš ๏ธ .help-table is a wrapper, not a table class

Two pages had <table class="help-table">. It needs to be <div class="help-table"><table>โ€ฆ</table></div> or a wide table stretches the whole page instead of scrolling inside its section.


6. Verifying a change

Two throwaway scripts earned their keep and are worth rebuilding if you do this again:

  • An orphan-class check โ€” extract every class="โ€ฆ" from the markup, and confirm each name has a rule in some stylesheet the page actually links (parse the <link> tags rather than assuming). This is what catches a rename that dropped a rule on the floor: a modifier like .tag.green whose CSS went with the deleted style block, so the badge silently renders plain.
  • A fetch-and-read check โ€” pull every help page over HTTP with a forged session and read the body, not the status code. A PHP fatal is served as HTTP 200, so a status-only check reports a broken page as a pass. While you have the HTML, grep it for unresolved *.help.* keys โ€” a missing translation falls back to printing the key.

Then a console-error pass in headless Chrome across every page โ€” with a negative control, so you know the check can actually fail. Injecting a call to an undefined function into one copy and confirming the check catches it takes ten seconds and is the difference between "no errors" and "no detection".

Two things to know when doing this locally:

  • The portal page needs an ss_user_id session, not analyst_id.
  • system/integrations/help.php redirects without a ?provider= parameter.

Related

โš ๏ธ **GitHub.com Fallback** โš ๏ธ