Mobile mode: same bar on a phone as on a desktop, list title back, board reflows.

Reported with two screenshots of the same app: on an iPhone 12 mini the second header
bar held a handful of icons and no board title, while a desktop Firefox window in
mobile mode showed the title and the full set of buttons. Same mode, two bars.

Mobile layout is driven from three switches that did not agree: `body.mobile-mode`
(the toggle), `Utils.isMiniScreen()` (which template branch renders) and
`@media (max-width: 800px)` (the viewport - true on a phone, false on a wide desktop
window even with mobile mode on). The first two were made to agree in a5246fbf5; the
rules that assumed the third are fixed here.

`.mobile-mode .board-header-btns.left { display: none }` is removed. The left group
holds edit title / visibility / watch / star / sort, and boardHeader.jade renders them
in EXACTLY ONE of the two groups - left `unless isMiniScreen`, right `if isMiniScreen`.
So in mobile mode the left group is already empty and hiding it changes nothing,
EXCEPT when the switches disagree, and then it deletes those five buttons from the bar
with no replacement. Gone, so that if the switches ever drift again the buttons survive
instead of vanishing.

The phone block gave the bar a fixed `height: 48px` for #6419's touch targets. A fixed
height CLIPS the second row the bar wraps to when title + buttons do not fit on one -
part of the bar was simply not there. Now `min-height`, so the targets keep their size
and the bar can grow; it still sits on one row whenever the content fits, which is the
normal case.

The list title is visible in mobile mode again, on both. listHeader.jade has two
shapes: on a mini screen the `h2.list-header-name` is a direct child of `.list-header`,
otherwise it is wrapped in a div (the one that rotates when a list is collapsed).
Mobile mode lays the header out as a grid and places the title by grid row/column,
which only applies to a direct grid ITEM - inside the wrapper the placement was ignored
and the title landed in the 30px first column, squeezed to nothing. `display: contents`
drops the wrapper from the layout so the title is a grid item in both shapes. Not for
`.list-rotated`, which needs a real box to rotate, nor for the screen-reader move
buttons.

The board reflows sideways again instead of overflowing by a sliver. The mobile board
rules sized the canvas, swimlanes, lists and list headers with `100vw` - the viewport
width INCLUDING the vertical scrollbar. The canvas scrolls vertically, so every one of
those was about a scrollbar's width wider than the box it sat in, at each nesting
level, and `min-width` pinned to the same value meant it could not shrink. All 18 are
now `100%`, which resolves against the parent's content box and already excludes the
scrollbar.

Tests: 1321 passing.

Thanks to xet7 !
This commit is contained in:
Lauri Ojansivu committed 2026-07-25 22:03:07 +03:00
1 parent 72cd786c77
commit 72315d8f96
4 files changed
+159 -26

No files matched your search

+49 -23
View File
@@ -226,6 +226,19 @@
position: relative !important;
}
/* The list title must be visible in mobile mode whichever markup rendered it.
listHeader.jade has TWO shapes: on a mini screen the `h2.list-header-name` is a
direct child of `.list-header`, and otherwise it is wrapped in a `div` (the one
that rotates when a list is collapsed). The rules below place the title by grid
row/column, which only works on a direct grid ITEM - inside the wrapper the title
was not one, so the placement was ignored and the wrapper landed in the 30px first
column, squeezing the title to nothing. `display: contents` drops the wrapper from
the layout so its children become grid items in both shapes.
Not when collapsed: `.list-rotated` needs the wrapper to be a real box to rotate. */
.mobile-mode .list-header > div:not(.list-rotated):not(.list-move-buttons) {
display: contents;
}
.mobile-mode .list-header .list-header-name {
font-size: clamp(18px, 2.5vw, 32px) !important;
font-weight: bold !important;
@@ -498,25 +511,33 @@
min-width: 100% !important;
}
/* Force mobile mode list styling on all screen sizes - override desktop CSS */
/* Force mobile mode list styling on all screen sizes - override desktop CSS.
These are `100%`, NOT `100vw`. `100vw` is the width of the viewport INCLUDING the
vertical scrollbar, and the board canvas scrolls vertically - so every element
forced to 100vw was about a scrollbar's width wider than the box it sat in, at each
nesting level (canvas > swimlane > lists > list). The result was a board that
always overflowed sideways by a sliver and, with `min-width` pinned to the same
value, could not reflow narrower. `100%` resolves against the parent's content box,
which already excludes the scrollbar. */
.mobile-mode .board-canvas {
display: block !important;
flex-direction: column !important;
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
.mobile-mode .swimlane {
display: block !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
@@ -525,9 +546,9 @@
.mobile-mode .swimlane .swimlane-header {
display: block !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 1rem 0 !important;
padding: 1rem !important;
font-size: clamp(18px, 2.5vw, 32px) !important;
@@ -537,9 +558,9 @@
.mobile-mode .swimlane .lists {
display: block !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 !important;
padding: 0 !important;
flex-direction: column !important;
@@ -550,9 +571,9 @@
.mobile-mode .list {
display: block !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
margin: 0 0 2rem 0 !important;
padding: 0 !important;
float: none !important;
@@ -667,9 +688,9 @@
flex-wrap: nowrap !important;
align-items: stretch !important;
justify-content: flex-start !important;
width: 100vw !important;
max-width: 100vw !important;
min-width: 100vw !important;
width: 100% !important;
max-width: 100% !important;
min-width: 100% !important;
overflow-x: hidden !important;
overflow-y: auto !important;
}
@@ -768,10 +789,15 @@
display: none !important;
}
/* Hide desktop-specific board header buttons in mobile mode */
.mobile-mode .board-header-btns.left {
display: none !important;
}
/* The left group is NOT hidden in mobile mode.
It holds edit title / visibility / watch / star / sort, and boardHeader.jade renders
them in EXACTLY ONE of the two groups: the left one `unless isMiniScreen`, the right
one `if isMiniScreen`. So in mobile mode the left group is already empty and hiding
it changes nothing - EXCEPT when the two mobile switches disagree, and then it
deletes those five buttons from the bar with no replacement. That is what made the
second header bar on a phone hold fewer buttons than the same bar in mobile mode on
a desktop window. They now agree (see Utils.isMiniScreen), and this rule is gone so
that if they ever drift again the buttons survive instead of vanishing. */
.mobile-mode .board-header-btns.center {
display: none !important;
+6 -2
View File
@@ -573,8 +573,12 @@ body.mobile-mode #header-quick-access .allBoards {
@media screen and (max-width: 800px),
screen and (max-device-width: 932px) and (-webkit-min-device-pixel-ratio: 3) {
#header #header-main-bar {
/* #6419: taller bar so the action icons can use ~44px touch targets. */
height: 48px;
/* #6419: taller bar so the action icons can use ~44px touch targets.
min-height, NOT height: the bar wraps to a second row when the board title and
the buttons do not fit on one, and a fixed height CLIPPED that second row - on
a phone the title or the icons simply were not there. It still sits on one row
whenever they fit, which is the normal case. */
min-height: 48px;
}
#header #header-main-bar .board-header-btns {
margin-top: 0px;
+1 -1
View File
@@ -25,7 +25,7 @@
},
"scripts": {
"test": "meteor test --once --driver-package meteortesting:mocha",
"test:unit:node": "node tests/filenameSanitizer.test.cjs && node tests/cardBoardConsistency.test.cjs && node tests/cardUrl.test.cjs && node tests/defaultSwimlane.test.cjs && node tests/cardMoveModifier.test.cjs && node tests/authenticationMethod.test.cjs && node tests/linkedCardTarget.test.cjs && node tests/staleWhileRevalidate.test.cjs && node tests/restArrayParam.test.cjs && node tests/minicardLabelHeight.test.cjs && node tests/dateUtils.normalizeDigits.test.cjs && node tests/reconcileBoardTeamMembers.test.cjs && node tests/rulesBoardSelector.test.cjs && node tests/ruleActionResolve.test.cjs && node tests/boardBackground.test.cjs && node tests/weekStart.test.cjs && node tests/ldapUsernameSlug.test.cjs && node tests/ldapGroupAllowlist.test.cjs && node tests/ldapConnectionRelease.test.cjs && node tests/htmljsArrayContent.test.cjs && node tests/cardDescriptionDraft.test.cjs && node tests/commentDraft.test.cjs && node tests/attachmentDeleteGuard.test.cjs && node tests/ruleMoveAction.test.cjs && node tests/snapMigrationRecovery.test.cjs && node tests/migrationAttachmentExtraction.test.cjs && node tests/attachmentRepair.test.cjs && node tests/attachmentStoragePath.test.cjs && node tests/schemaUpgradeSteps.test.cjs && node tests/cpuExec.test.cjs && node tests/cpuExecWiring.test.cjs && node tests/ferretdbHasData.test.cjs && node tests/ferretdbPolling.test.cjs && node tests/ferretdbWaitTimeout.test.cjs && node tests/ferretdbOplog.test.cjs && node tests/ferretdbOplogReset.test.cjs && node tests/ferretdbTextDataBackup.test.cjs && node tests/recoveryPlan.test.cjs && node tests/recoveryReportQuery.test.cjs && node tests/recoveryEventsJsonl.test.cjs && node tests/recoveryReportWiring.test.cjs && node tests/recoveryMaintenance.test.cjs && node tests/importMemberMapping.test.cjs && node tests/boardMemberMapPlan.test.cjs && node tests/withDeadline.test.cjs && node tests/importVirtualMembers.test.cjs && node tests/sandstormMigrationBridge.test.cjs && node tests/fileNameDisplay.test.cjs && node tests/uploadFileName.test.cjs && node tests/diskSpace.test.cjs && node tests/fileTypeCorrection.test.cjs && node tests/fileHardeningGuards.test.cjs && node tests/cpuHighTracker.test.cjs && node tests/cpuMonitorWiring.test.cjs && node tests/buttonThemeColors.test.cjs && node tests/uiDensity.test.cjs && node tests/editorSubmitKey.test.cjs && node tests/isoWeek.test.cjs && node tests/boardCopyDefaults.test.cjs && node tests/linkedCardTitle.test.cjs && node tests/commentActivity.test.cjs && node tests/swimlaneSort.test.cjs && node tests/subtaskCopy.test.cjs && node tests/subtasksDefaultBoard.test.cjs && node tests/scrollbarCss.test.cjs && node tests/minicardBidiIsolation.test.cjs && node tests/notificationCleanup.test.cjs && node tests/cardSearch.test.cjs && node tests/linkedCardMembers.test.cjs && node tests/verificationEmail.test.cjs && node tests/boardSortReorder.test.cjs && node tests/checklistItemTitles.test.cjs && node tests/swimlaneFilter.test.cjs && node tests/popupOffset.test.cjs && node tests/popupAfterConfirm.test.cjs && node tests/defaultBoard.test.cjs && node tests/upcomingUiGuards.test.cjs && node tests/linkedCardCover.test.cjs && node tests/inviteEmailLanguage.test.cjs && node tests/domainTablePage.test.cjs && node tests/mongoSelectorSafety.test.cjs && node tests/filterDateNull.test.cjs && node tests/listMoveSwimlane.test.cjs && node tests/cardDragSwimlaneScrollRefresh.test.cjs && node tests/undoRedoSelection.test.cjs && node tests/softDelete.test.cjs && node tests/globalThemeColor.test.cjs && node tests/themeCategories.test.cjs && node tests/themeColorPicker.test.cjs && node tests/uiFonts.test.cjs && node tests/sandstormAutoOpen.test.cjs && node tests/createListParams.test.cjs && node tests/addBoardTileHeight.test.cjs && node tests/multiSelectionButton.test.cjs && node tests/headerHamburger.test.cjs && node tests/changeLanguageColumns.test.cjs && node tests/cardsLoading.test.cjs && node tests/base64Chunk.test.cjs && node tests/backupPaths.test.cjs && node tests/backupCron.test.cjs && node tests/avatarUrlSafety.test.cjs && node tests/changeAvatarPopup.test.cjs && node tests/devOverlayRuntimeErrors.test.cjs && node tests/boardHeaderOneLine.test.cjs && node tests/headerDragscroll.test.cjs && node tests/jadeCompiles.test.cjs && node tests/mobileModeFullWidth.test.cjs && node tests/minicardDragHandle.test.cjs && node tests/securityReportScript.test.cjs && node tests/tablePage.test.cjs && node tests/leftMenu.test.cjs && node tests/templateHandlersExist.test.cjs && node tests/noViewportSpacing.test.cjs && node tests/boardsVisibilitySettings.test.cjs && node tests/snapMigrationSwitchOrder.test.cjs && node tests/importedUserReconciliationPlan.test.cjs && node tests/ldapPhoto.test.cjs && node tests/importedBoardMemberPlan.test.cjs && node tests/migrateNonFinite.test.cjs && node tests/addListComposerOptions.test.cjs && node tests/sidebarWidth.test.cjs && node tests/sidebarResize.test.cjs && node tests/securityMeifukun.test.cjs Line truncated
"test:unit:node": "node tests/filenameSanitizer.test.cjs && node tests/cardBoardConsistency.test.cjs && node tests/cardUrl.test.cjs && node tests/defaultSwimlane.test.cjs && node tests/cardMoveModifier.test.cjs && node tests/authenticationMethod.test.cjs && node tests/linkedCardTarget.test.cjs && node tests/staleWhileRevalidate.test.cjs && node tests/restArrayParam.test.cjs && node tests/minicardLabelHeight.test.cjs && node tests/dateUtils.normalizeDigits.test.cjs && node tests/reconcileBoardTeamMembers.test.cjs && node tests/rulesBoardSelector.test.cjs && node tests/ruleActionResolve.test.cjs && node tests/boardBackground.test.cjs && node tests/weekStart.test.cjs && node tests/ldapUsernameSlug.test.cjs && node tests/ldapGroupAllowlist.test.cjs && node tests/ldapConnectionRelease.test.cjs && node tests/htmljsArrayContent.test.cjs && node tests/cardDescriptionDraft.test.cjs && node tests/commentDraft.test.cjs && node tests/attachmentDeleteGuard.test.cjs && node tests/ruleMoveAction.test.cjs && node tests/snapMigrationRecovery.test.cjs && node tests/migrationAttachmentExtraction.test.cjs && node tests/attachmentRepair.test.cjs && node tests/attachmentStoragePath.test.cjs && node tests/schemaUpgradeSteps.test.cjs && node tests/cpuExec.test.cjs && node tests/cpuExecWiring.test.cjs && node tests/ferretdbHasData.test.cjs && node tests/ferretdbPolling.test.cjs && node tests/ferretdbWaitTimeout.test.cjs && node tests/ferretdbOplog.test.cjs && node tests/ferretdbOplogReset.test.cjs && node tests/ferretdbTextDataBackup.test.cjs && node tests/recoveryPlan.test.cjs && node tests/recoveryReportQuery.test.cjs && node tests/recoveryEventsJsonl.test.cjs && node tests/recoveryReportWiring.test.cjs && node tests/recoveryMaintenance.test.cjs && node tests/importMemberMapping.test.cjs && node tests/boardMemberMapPlan.test.cjs && node tests/withDeadline.test.cjs && node tests/importVirtualMembers.test.cjs && node tests/sandstormMigrationBridge.test.cjs && node tests/fileNameDisplay.test.cjs && node tests/uploadFileName.test.cjs && node tests/diskSpace.test.cjs && node tests/fileTypeCorrection.test.cjs && node tests/fileHardeningGuards.test.cjs && node tests/cpuHighTracker.test.cjs && node tests/cpuMonitorWiring.test.cjs && node tests/buttonThemeColors.test.cjs && node tests/uiDensity.test.cjs && node tests/editorSubmitKey.test.cjs && node tests/isoWeek.test.cjs && node tests/boardCopyDefaults.test.cjs && node tests/linkedCardTitle.test.cjs && node tests/commentActivity.test.cjs && node tests/swimlaneSort.test.cjs && node tests/subtaskCopy.test.cjs && node tests/subtasksDefaultBoard.test.cjs && node tests/scrollbarCss.test.cjs && node tests/minicardBidiIsolation.test.cjs && node tests/notificationCleanup.test.cjs && node tests/cardSearch.test.cjs && node tests/linkedCardMembers.test.cjs && node tests/verificationEmail.test.cjs && node tests/boardSortReorder.test.cjs && node tests/checklistItemTitles.test.cjs && node tests/swimlaneFilter.test.cjs && node tests/popupOffset.test.cjs && node tests/popupAfterConfirm.test.cjs && node tests/defaultBoard.test.cjs && node tests/upcomingUiGuards.test.cjs && node tests/linkedCardCover.test.cjs && node tests/inviteEmailLanguage.test.cjs && node tests/domainTablePage.test.cjs && node tests/mongoSelectorSafety.test.cjs && node tests/filterDateNull.test.cjs && node tests/listMoveSwimlane.test.cjs && node tests/cardDragSwimlaneScrollRefresh.test.cjs && node tests/undoRedoSelection.test.cjs && node tests/softDelete.test.cjs && node tests/globalThemeColor.test.cjs && node tests/themeCategories.test.cjs && node tests/themeColorPicker.test.cjs && node tests/uiFonts.test.cjs && node tests/sandstormAutoOpen.test.cjs && node tests/createListParams.test.cjs && node tests/addBoardTileHeight.test.cjs && node tests/multiSelectionButton.test.cjs && node tests/headerHamburger.test.cjs && node tests/changeLanguageColumns.test.cjs && node tests/cardsLoading.test.cjs && node tests/base64Chunk.test.cjs && node tests/backupPaths.test.cjs && node tests/backupCron.test.cjs && node tests/avatarUrlSafety.test.cjs && node tests/changeAvatarPopup.test.cjs && node tests/devOverlayRuntimeErrors.test.cjs && node tests/boardHeaderOneLine.test.cjs && node tests/headerDragscroll.test.cjs && node tests/jadeCompiles.test.cjs && node tests/mobileModeFullWidth.test.cjs && node tests/minicardDragHandle.test.cjs && node tests/mobileModeConsistency.test.cjs && node tests/securityReportScript.test.cjs && node tests/tablePage.test.cjs && node tests/leftMenu.test.cjs && node tests/templateHandlersExist.test.cjs && node tests/noViewportSpacing.test.cjs && node tests/boardsVisibilitySettings.test.cjs && node tests/snapMigrationSwitchOrder.test.cjs && node tests/importedUserReconciliationPlan.test.cjs && node tests/ldapPhoto.test.cjs && node tests/importedBoardMemberPlan.test.cjs && node tests/migrateNonFinite.test.cjs && node tests/addListComposerOptions.test.cjs && node tests/sidebarWidth.test.cjs && node tests/sidebarResize.testLine truncated
"test:unit:all": "npm run test:unit:node && node tests/cards.stickers.test.js && node tests/stickers.test.js && node tests/stickers.packs.test.js && node tests/linkCardPopup.test.js && node tests/trelloCreator.import.test.js && node tests/trelloImport.maplink.test.js && node tests/trelloImport.zip.test.js && node tests/wekanCreator.import.test.js && node tests/wekanCreator.inconsistent.test.js && node tests/unit/oauth2-admin-groups.test.js",
"test:watch": "meteor test --driver-package meteortesting:mocha",
"test:e2e": "node tests/e2e/list-regressions.js",
+103
View File
@@ -0,0 +1,103 @@
'use strict';
// Mobile mode must look the same on a phone and in mobile mode on a desktop window.
//
// Reported with two screenshots of the same app: on an iPhone 12 mini the second
// header bar held a handful of icons and no board title, while a desktop Firefox
// window in mobile mode showed the title and the full set of buttons. Same mode, two
// different bars.
//
// Mobile layout is driven from three switches that did not agree:
// 1. `body.mobile-mode` - the explicit toggle
// 2. `Utils.isMiniScreen()` - which branch of the templates renders (fixed in
// tests/mobileModeFullWidth.test.cjs, where an explicit choice now wins)
// 3. `@media (max-width: 800px)` - the viewport, true on a phone and false on a
// wide desktop window even when mobile mode is on
//
// A rule keyed off one of them that assumes another cannot hold on both devices.
//
// Run: node tests/mobileModeConsistency.test.cjs
const assert = require('assert');
const fs = require('fs');
const path = require('path');
let passed = 0;
function test(name, fn) { fn(); passed += 1; console.log(' ok -', name); }
const root = path.join(__dirname, '..');
const read = rel => fs.readFileSync(path.join(root, rel), 'utf8');
const boardCss = read('client/components/boards/boardHeader.css');
const headerCss = read('client/components/main/header.css');
const boardJade = read('client/components/boards/boardHeader.jade');
const listJade = read('client/components/lists/listHeader.jade');
console.log('mobileModeConsistency:');
test('the header buttons are never hidden without a replacement', () => {
// edit title / visibility / watch / star / sort are rendered in EXACTLY ONE of the
// two groups - left `unless isMiniScreen`, right `if isMiniScreen`. Hiding the left
// group in mobile mode is a no-op while the switches agree, and deletes those five
// buttons when they do not.
assert.ok(!/\.mobile-mode \.board-header-btns\.left \{[^}]*display:\s*none/.test(boardCss),
'the left button group must not be hidden in mobile mode');
// Both branches must still exist in the markup, or one device loses the buttons
// outright.
assert.ok(/unless isMiniScreen/.test(boardJade), 'the wide-screen branch');
assert.ok(/if isMiniScreen/.test(boardJade), 'and the mini-screen branch');
});
test('the second header bar can grow to a second row on a phone', () => {
// The phone block gives the bar a bigger touch target (#6419). As a fixed `height`
// that CLIPPED the second row the bar wraps to when title + buttons do not fit, so
// part of the bar was simply not there.
const phone = headerCss.slice(headerCss.indexOf('@media screen and (max-width: 800px),'));
const bar = /#header #header-main-bar \{([^}]*)\}/.exec(phone);
assert.ok(bar, 'the phone block must still size the bar');
assert.ok(/min-height:\s*48px/.test(bar[1]), 'a floor, so the touch targets stay');
assert.ok(!/[^-]height:\s*48px/.test(bar[1]), 'but not a fixed height, which clips');
});
test('the list title shows whichever markup rendered it', () => {
// listHeader.jade has two shapes: on a mini screen the h2 is a direct child of
// .list-header, otherwise it is wrapped in a div. Mobile mode places the title by
// grid row/column, which only applies to a direct grid ITEM - inside the wrapper
// the placement was ignored and the title was squeezed into the 30px first column.
const mini = listJade.indexOf('if isMiniScreen');
assert.ok(mini > -1, 'the mini-screen branch exists');
assert.ok(/div\(class="\{\{#if collapsed\}\}list-rotated/.test(listJade),
'and the other branch wraps the title in a div');
const rule = /\.mobile-mode \.list-header > div:not\(\.list-rotated\)[^{]*\{([^}]*)\}/
.exec(boardCss);
assert.ok(rule, 'the wrapper must be dropped from the layout in mobile mode');
assert.ok(/display:\s*contents/.test(rule[1]),
'display:contents makes its children grid items in both shapes');
// The title is still placed by grid, which is what needed the wrapper gone.
const name = /\.mobile-mode \.list-header \.list-header-name \{([^}]*)\}/.exec(boardCss);
assert.ok(name && /grid-column:\s*2/.test(name[1]), 'the title is grid-placed');
});
test('a collapsed list keeps its rotation wrapper (negative)', () => {
// display:contents on .list-rotated would remove the box that does the rotating.
const rule = /\.mobile-mode \.list-header > div:not\(\.list-rotated\)([^{]*)\{/.exec(boardCss);
assert.ok(rule, 'the rule must exist');
assert.ok(/:not\(\.list-move-buttons\)/.test(rule[1]),
'the screen-reader move buttons keep their box too');
});
test('the board reflows instead of overflowing by a scrollbar width', () => {
// 100vw includes the vertical scrollbar. The board canvas scrolls vertically, so
// every child forced to 100vw was a scrollbar wider than the box it sat in - at
// each nesting level - and min-width pinned it there, so it could not reflow.
// Comments blanked first: the note explaining this fix names the unit it replaced.
const code = boardCss.replace(/\/\*[\s\S]*?\*\//g, '');
assert.ok(!/100vw/.test(code),
'mobile board sizing must use 100%, which excludes the scrollbar');
// The elements that were 100vw must still be full width, just measured correctly.
const canvas = /\.mobile-mode \.board-canvas \{([^}]*overflow-x[^}]*)\}/.exec(boardCss);
assert.ok(canvas && /width:\s*100% !important/.test(canvas[1]),
'the canvas is still full width');
});
console.log(`\nmobileModeConsistency: ${passed} tests passed`);