Skip to content

Mobile Friendly System Wiki

Ed Mozley edited this page Sep 7, 2026 · 2 revisions

Mobile‑Friendly: System Wiki

The twenty‑first module and the last one in the waffle menu: seven pages β€” browse, search, database tables, scan management, and the file, function and table detail pages. Shipped in #1487–#1490, mobile.css v135 / mobile.js v57, LAYER 36.

Read Mobile‑Friendly first for the strategy and the one hard rule, and Techniques & Tricks for the catalogue this round draws on.


πŸ”΄πŸ”΄ The whole module rendered an empty state, and the first pass passed everything

Every wiki_* table on the development install is empty β€” the codebase has never been scanned β€” so all seven pages draw "no scan data" and there is nothing anywhere to measure. The first probe run came back clean on every page, and it was worthless.

πŸ”‘ An assertion that cannot fail is not passing, it is absent β€” Β§28's rule and Β§30's in a third form. Β§28's version is a document pinned to 100dvh so it can never be taller than the screen; Β§30's is a body that clips so it can never be wider. This one is a page with no rows, and it is the easiest of the three to walk past, because the page looks finished.

Every page takes its data from api/wiki/*.php, so the fix was to stub fetch in the harness and re‑run each page's own loaders against fixtures shaped like this codebase's worst realistic case β€” long file names, long function names, descriptions that are whole sentences. The real renderers do the rendering; only the transport is fake, and not one row is written to the database.

⚠️ Two things that cost a round each:

  • The fixtures have to match the real vocabulary. table.php groups its references by typeOrder = ['SELECT', 'INSERT', …] and my fixture used lowercase, so nothing matched and the reference lists silently never rendered. And a screenshot showed a css file type with no badge β€” which turned out to be my fixture inventing a type: the scanner only ever emits PHP or JS ($IncludeExtensions = @("*.php", "*.js")) and the page styles exactly those two. Reporting that as a bug would have been reporting a fault in my own test data.
  • The loaders are async even against a stub. Tapping straight after calling one taps a tree that has not rendered, and the scenario reports "no leaf found" β€” which looks exactly like a leaf that cannot be tapped.

⭐ Six shells, one shape

.wiki-container, .wiki-detail (Γ—3), .wiki-tables, .wiki-scan and .wiki-search are all height: calc(100vh - 48px), so 36a is one rule for all seven pages. The System round's find the shared shells before estimating β€” except that here the shell is a shape rather than an include, which grepping the container class per page still finds.

Five of the seven could not be scrolled at all before β€” search and the file‑detail page were the two that already could β€” because 100vh is the large viewport on iOS and LAYER 2 already makes <body> a 100dvh flex column; the shell only has to take what is left. This is the seventh module built that way.

⚠️ Only the browse page had a lot of unreachable content (950px in a 740px screen). The other four were 26px over with fixtures β€” which is exactly the point of the empty‑module problem above: on an installation that has actually run a scan, those tables grow with the codebase and every one of them would have been badly stuck.


πŸ”΄πŸ”΄ 36c β€” The pane that was 80 pixels

.wiki-sidebar   280px fixed
.wiki-main       80px          <- what is left of 360
.file-table    1110px          <- clipped inside those 80

A table thirteen times wider than the pane holding it, and the page passed every containment check while doing it because .wiki-container and .wiki-main both clip (Β§30). The second‑narrowest pane in the rollout after Workflow's 20px, and like that one it is arithmetic rather than a bug: a 280px fixed sidebar beside anything, on a 360px screen, leaves 80.

The tree goes to a sheet, and the reasoning is Ed's own

A stacked tree would spend ~200px of every screen on navigation you have finished with, which is exactly what Ed objected to on the contracts screen (#1375, "a contract gets the whole phone screen"). And a tree cannot become the chip strip CMDB's flat class list became, because the hierarchy is the information β€” api and api/tickets are different places only because the indentation says so.

So it goes where the Calendar sidebar went: a .mobile-sheet behind one button, with the real node relocated rather than cloned (a clone would give the page two #folderTrees and loadFolderTree() would render into whichever came first), moved lazily on first open, and moved back if the viewport goes wide again.

⭐ Zero new translation keys. The button's label is read out of the sidebar's own .sidebar-title, which the page already prints translated; the sheet's Close is common.close.

πŸ”΄πŸ”΄ And the condition on closing it is the whole design

The first version closed the sheet whenever a folder was tapped. Driving it showed why that is wrong, and it is not a small wrong:

selectFolder() expands the tapped folder's children as well as selecting it β€” that is the page's own behaviour, and it is why the tree opens collapsed. So closing on every tap would shut the sheet at the exact moment it had just revealed the next level down, and api/tickets would have been unreachable on a phone, however many times you tried. Every tap on api would filter the list and throw away the thing you were navigating towards.

πŸ”‘ A parent is a step; a leaf is an arrival. Tapping a parent expands it and the sheet stays; tapping a leaf closes it. renderTree only puts a chevron glyph in .tree-toggle when a node has children, so the markup already says which is which and nothing has to be inferred.

⚠️ This is the class of fault that only driving the real gesture finds. Every measurement passed: the sheet opened, the tree was on screen, the rows were tap‑sized, the list reloaded. The thing that was broken was the second tap.


πŸ”΄ 36b β€” The hover rail, and the first !important the rollout has needed for one

includes/header.php carries a per‑analyst left‑panel mode (system_wiki_sidebar_mode). Set to hover, it collapses the sidebar to a 16px hot‑zone that expands on hover β€” and a phone has nothing to hover with. Measured with the mode forced on: position: absolute, min-width: 16px. This is the Contracts fault exactly, and the third module to ship the pattern.

⚠️ And this one needed !important, which the Contracts fix did not. Β§24: header.php emits its <style> inside <body>, so it loads after mobile.css however the head is ordered. Its rule is .wiki-container.sidebar-hover .wiki-sidebar:hover β€” (0,4,0) β€” and the module marker takes mine to (0,4,0) too, a tie the later sheet wins. The specificity trick that has worked all rollout is simply not available here.


πŸ”΄ 36g β€” Another module's card feed was already on it

.history-table  w=320  rowH=170  every <th> measuring 0

Contained, and crushed into a 170px row β€” but the why is Β§15 in its most exact form yet. LAYER 15b's .history-table card feed is completely unscoped, written for the Assets history and custody trails, and this page happens to use the same class name. The moment it opted in it inherited half a card feed from a module it has nothing to do with: display: block on rows and cells, thead { display: none } β€” which is why every column measured zero β€” and no labels, no card, and seven values stacked one per line with four of them bare numbers.

πŸ”‘ LAYER 35's headline was that a shared class means a fix is shared. This is the same coin's other face: it means a design is shared, and a design is a decision about content this module never made. The tell is a <th> measuring 0.

⬜ 15b is left alone β€” it is correct for Assets, its own comment explains that it avoids column indices deliberately because Assets has two variants of the table, and re‑scoping a shipped rule to fix a page it was never aimed at is the larger risk. LAYER 36g takes what 15b already did right and adds the card and the labels. And the hidden head is still in the DOM, which is all the Β§21 harvester needs β€” hiding a head is not the same as removing it.


36h β€” Six tables, three answers

Measured with fixtures, at 360Γ—740:

section before row height answer
Functions 824px 83 card feed β€” the description column is a sentence
Classes 460px 116 card feed β€” a sentence wrapping inside 104px
Dependencies 444px 33 left a table, just allowed to wrap
Dependents 320px 32 left completely alone
Database Tables 320px 33 left completely alone
Session Variables 324px 32 left completely alone

The best Β§11 example since LMS's four‑tables‑three‑answers. Dependencies is 84px over and the whole of that is one long file path, so it only needs permission to wrap β€” a three‑column row reading require Β· includes/db.php Β· L6 is a good table row and a card would be worse.

πŸ”΄ The inline nowrap that would have broken the feed

The functions description cell carries an inline max-width:300px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap. An inline style beats any stylesheet rule that is not !important, and Β§14 says a flex item's automatic minimum size is its content width β€” so one nowrap cell would have pinned every card wider than the screen while the document stayed perfectly contained. The same thing as .log-datetime in Reporting, with no way to out‑specify it.

πŸ”΄πŸ”΄ And the selector was the other half of the lesson

The first draft keyed the two feeds on .section:nth-of-type(1) and (2), which matched nothing: :nth-of-type counts among siblings of the same tag, and every one of these is a <div> sitting after .breadcrumb and .file-header, which are also divs. Measured: every cell still order: 0, six tables still tables, the layer with no effect at all β€” Β§9's symptom with a completely different cause.

⭐ And :nth-child(3)/(4) would have been worse than wrong rather than merely wrong: the Classes section is rendered only when the file has classes, so any positional selector points at a different table depending on which file you opened.

πŸ”‘ So it keys on the thing that decides the treatment: how many columns the table has. :has(thead th:nth-child(5)) is Functions, four‑but‑not‑five is Classes, three is left alone β€” which is also exactly how Β§11's decision was made, so the selector now says the same thing the comment does. Robust to the conditional section, to reordering, and to a seventh section being added later.

⚠️ Setting table-layout: fixed on every .detail-table to make that path wrap made the four small tables three equal 112px columns β€” Type and Line given as much room as a file path. overflow-wrap: anywhere is enough on its own: it drops the cell's min‑content width to one character, which is all the auto layout needed in order to be allowed to shrink it.


36f β€” Seven labels on one card, and it does not read as a spreadsheet

.tables-table  w=634  rowH=35
cols  Table Name 151 Β· Files 58 Β· Total Refs 61 Β·
      SELECT 74 Β· INSERT 75 Β· UPDATE 80 Β· DELETE 75 Β· JOIN 61

tickets / 96 / 412 / 268 / 21 / 94 / 6 / 23 is Β§21's own case and needs all seven labelled β€” the most this list has ever taken, and right for the same reason LAYER 33b needed six of nine.

⭐ But not seven identical lines, which is the spreadsheet effect Β§21 warns about. The five operation counts are a set you read together β€” that is the whole point of the page β€” so they stay on one wrapping line as labelled badges, and only Files and Total Refs take a line of their own. The colour on each badge already distinguishes them for anyone who can see it; the label is what makes that true for everyone else.


⭐ 36l β€” The one list in twenty‑one modules that arrived in the right shape

Search needed nothing for its results. All three panels render .result-item β€” a bordered block with a title, a meta line and a description β€” which is already a card, and already the form every other list in this rollout had to be converted into. Only the search bar and the three counting tabs needed anything.


Verification

check result
width containment, 7 pages @360Γ—740 docScrollW = 360 on every one
reachability (Β§28) a real scroller or a genuine fit on all seven; five had neither before
the folder sheet, driven button 336Γ—42 and a tap reaches it; sheet 360Γ—740 on screen; the real sidebar moved in and #folderTree still unique; rows 360Γ—50; a parent expands and the sheet stays (1 β†’ 5 visible rows); a leaf closes it and the list retitles
the chevron never closes the sheet
harvested labels 10 / 28 / 12 cells stamped, counted not eyeballed
the hover rail forced on position: static, width: auto, min-width: 0 β€” the !important beats the in‑body <style>
desktop control @1100Γ—900 every element's box identical with mobile.css disabled in place, all 7 pages
and mobile.js at desktop width sub‑bar hidden, sheet hidden, sidebar back in the container, .wiki-container back to its own height: 852px; flex: 0 1 auto; overflow: hidden β€” because the desktop control cannot prove this: both its snapshots are taken after mobile.js has run, so an injected node is in each and diffs to nothing
nested CSS comments (Β§17) clean, braces balanced
other pages diff contains nothing but the two version numbers

πŸ”‘ The desktop control has a blind spot and it is worth naming. It proves mobile.css moves nothing. It cannot prove the same of mobile.js, which injects nodes β€” so anything that adds chrome needs its own desktop assertion, separately.


Reference

  • 36a the six shells, in one rule
  • 36b the hover rail, with !important β€” Β§24, and the first time the specificity route was closed
  • 36c the folder tree as a sheet; .wiki-main from 80px to full width
  • 36d the stats bar two‑up, search box promoted above the figures
  • 36e the file list as a card feed with Lines and Functions harvested
  • 36f eight columns, seven labels, five of them kept on one line as a set
  • 36g the scan history β€” and another module's feed already on it
  • 36h six tables, three answers, one inline nowrap, and a selector keyed on column count
  • 36i the breadcrumb, on all three detail pages
  • 36j/36k a table's references and a function's signature: overflow-wrap, not table-layout: fixed
  • 36l search, which needed nothing but its chrome

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally