Skip to content

Mobile Friendly

Ed Mozley edited this page Jul 7, 2026 · 8 revisions

Mobile‑Friendly FreeITSM

How FreeITSM is being made usable on a phone β€” the strategy, the one hard rule that keeps it safe, the reusable patterns, and how to bring a new page along. Like Theming & Dark Mode, this is a gradual, non‑breaking rollout layered on top of the existing desktop app rather than a rewrite or a separate mobile site.

Status: early. Tickets (the inbox) is the first module and the reference implementation. It works and is genuinely usable on a phone, but there is a long tail of polish β€” squeezing a full ITSM ticket onto a small screen is a lot to ask.


Who is this for?

The target is the on‑the‑go analyst β€” someone away from their desk who needs to triage the inbox, read a ticket, reply, change its properties, and link/record things. It is deliberately not an attempt to make every one of the ~20 modules fully usable on a phone. Some surfaces (the Network Mapper canvas, the Process Mapper, the Gantt timeline, drag‑between‑columns Kanban) are inherently desktop interactions and are out of scope for mobile β€” they stay desktop‑first.

So the scope is a focused mobile experience for a handful of high‑value journeys, done well, starting with Tickets.


The one hard rule: never break desktop

This is the north star, and it's what makes the rollout safe to do incrementally:

Every mobile style lives inside a @media (max-width: 768px) block, and every mobile behaviour is gated on matchMedia('(max-width: 768px)').

The whole feature is two shared files:

File What it is Safety
assets/css/mobile.css Only @media (max-width: 768px) rules β€” nothing at the top level. Above 768px the rules don't exist, so the desktop render is byte‑identical β€” provable, not by inspection.
assets/js/mobile.js A single IIFE whose every branch checks mq.matches first. On a wide screen every handler no‑ops; injected elements are hidden.

This mirrors the dark‑mode safety net: just as var(--token, #fallback) made tokenising a stylesheet non‑breaking, the @media/matchMedia gate makes adding mobile behaviour non‑breaking. You can convert a page and know with certainty you changed nothing for desktop users.

Corollary β€” injected chrome must be hidden off‑mobile. mobile.js injects DOM (a back/folders sub‑bar, a views hamburger, section sheets). Because mobile.css is @media‑only it cannot set a desktop default of display:none. So injected elements are either created with an inline style="display:none" (sheets) or toggled by a syncBar() function that runs on load and on every matchMedia change:

function syncBar() {
    var on = mq.matches;
    bar.style.display = on ? 'flex' : 'none';        // Back/Folders sub-bar
    var vb = document.querySelector('.mobile-views-btn');
    if (vb) vb.style.display = on ? '' : 'none';      // views hamburger
    if (!on) document.body.classList.remove('mobile-views-open');
}

Rollout model

Exactly like theming: per‑page opt‑in. A page joins the mobile experience by linking mobile.css (and, where it needs behaviour, mobile.js) in its <head>, after its own stylesheet so the @media rules win on ties:

<link rel="stylesheet" href="../assets/css/theme.css?v=18">
<link rel="stylesheet" href="../assets/css/inbox.css?v=37">
<link rel="stylesheet" href="../assets/css/mobile.css?v=6">   <!-- opt in -->
...
<script src="../assets/js/inbox.js?v=52"></script>
<script src="../assets/js/mobile.js?v=4"></script>            <!-- after the page's JS -->

mobile.js loads after the page's own script so it can wrap the globals that page already exposes (see below) rather than editing them.


The three navigation layers

The hardest conceptual problem on mobile is navigation: how do you move between modules, between views inside a module, and between panes that normally sit side‑by‑side? FreeITSM answers each with a distinct affordance.

Layer Desktop Mobile
Between modules the waffle app‑launcher (top‑left) the same waffle panel, restyled into a full‑height left slide‑in drawer
Between views in a module (e.g. Inbox / Dashboard / Calendar) the .header-nav button row a ☰ hamburger at the top‑right opening a right slide‑in drawer
Between panes in a view (e.g. folders / list / conversation) side‑by‑side split panes an Outlook‑style master‑detail stack β€” one pane on screen, slide between them

The two drawers are symmetric β€” modules left, views right β€” which keeps them easy to tell apart. The waffle drawer needed almost no new code: the app‑launcher panel already existed with open/close JS, so mobile.css just turns the absolute dropdown into a fixed, full‑height, slide‑in drawer.


Reusable patterns

These are the building blocks. Tickets uses all of them; future modules should reach for the same vocabulary.

1. Master‑detail pane stack

A multi‑pane split becomes a one‑pane‑at‑a‑time slide stack. State lives in a single attribute on <body> (data-mobile-pane), and the panes are absolutely positioned and slid with transform: translateX(...). Wiring it to history.pushState makes the device back button pop the stack, which is what makes it feel native rather than like a resized website.

2. Modal β†’ full‑screen sheet

The canonical .modal-content (used app‑wide) fills the screen on mobile β€” so the reply composer, pickers and forms get room instead of a cramped centred box. One rule, every modal benefits.

3. Section β†’ its own sheet

When a section crowds a small screen, relocate its DOM node into a full‑screen sheet opened by a button. Tickets does this for Links, Properties, Time entries and Affected objects. It's config‑driven, so adding another panel is one line.

4. Bottom action bar

Action buttons drop their text labels to icons only and move to a bottom bar (via flex order) below the scrolling content β€” the mobile‑app convention.

5. Collapsible sections

Verbose blocks (e.g. a ticket's From/To/Date/Cc) start collapsed behind a tappable summary + chevron, Gmail‑app style.


How to make a page mobile‑friendly

  1. Link mobile.css (and mobile.js if it needs behaviour) in the page <head>, after the page's own CSS/JS. Bump the version query when you change them.
  2. Fix the height model. Desktop shells often use height: calc(100vh - 48px), which assumes a 48px header β€” wrong once the header wraps. Switch the body to a flex column on mobile so the header takes its natural height and the content area flexes to fill (flex: 1; min-height: 0). Use 100dvh, not 100vh, so mobile browser chrome is accounted for.
  3. Collapse side‑by‑side layouts into the master‑detail stack (or stack them vertically if there are only two).
  4. Relocate crowded sections into sheets; tighten padding; icon‑only dense button rows; collapse verbose blocks.
  5. Wrap, don't edit. If you need behaviour, wrap the page's existing global handlers from mobile.js rather than editing the page's large JS file.
  6. Keep every rule inside the @media block and every behaviour behind mq.matches. Verify: grep -nE "^[^[:space:]/}].*\{" mobile.css | grep -v "@media" should print nothing.
  7. Test on a real device / DevTools device mode. There is no substitute β€” see the note below.

Challenges & solutions (general)

Challenge Solution
The whole app is a fixed‑height desktop shell (100vh, overflow:hidden, split panes). Per‑pattern collapse to single‑column / master‑detail; replace fragile calc() heights with flexbox + 100dvh.
Drag‑based surfaces (canvas editors, Gantt, Kanban) don't map to touch. Out of scope for mobile; they stay desktop‑first (documented per module).
Data tables overflow narrow screens. Become card lists or horizontal‑scroll containers (per module, as they're reached).
Can't self‑verify visually β€” pages need auth and there's no headless browser in the dev box, so screenshots aren't possible. Build to the spec + prove desktop safety statically (the @media/matchMedia gate), then a tight test loop with a human on a real device. Expect iteration.
Injected mobile chrome leaking onto desktop. syncBar() mq‑toggle + inline display:none defaults on injected nodes.

Module status

Module State Page
Tickets (inbox) In progress β€” usable, polishing Mobile: Tickets
Everything else Not started β€”

When another module is brought along, add a Mobile-Friendly-<Module>.md page here and a row above.


Reference

  • CSS: assets/css/mobile.css β€” one @media (max-width: 768px) block, organised into numbered LAYERS.
  • JS: assets/js/mobile.js β€” one matchMedia‑gated IIFE.
  • Related: Theming & Dark Mode (the same gradual, non‑breaking philosophy), Architecture.

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally