Merge pull request #390 from JDProfresh/fix/phone-breakpoint-480

fix(mobile): raise the phone breakpoint from 430px to 600px
This commit is contained in:
Ark0N
2026-09-14 15:52:10 +02:00
committed by GitHub
20 changed files with 51 additions and 51 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ function loadHomeSessionsApp(overrides: Record<string, any> = {}, innerWidth = 1
createElement: () => fakeElement(),
createElementNS: () => fakeElement(),
},
MobileDetection: { getDeviceType: () => (innerWidth < 430 ? 'mobile' : 'desktop') },
MobileDetection: { getDeviceType: () => (innerWidth < 600 ? 'mobile' : 'desktop') },
});
for (const file of ['constants.js', 'mobile-overview.js', 'home-sessions.js']) {
vm.runInContext(readFileSync(resolve(PUBLIC, file), 'utf8'), context, { filename: file });
+3 -3
View File
@@ -10,7 +10,7 @@
//
// Policy: every header button that is VISIBLE BY DEFAULT on desktop must have an
// explicit decision for phones — either it's hidden via an @media (max-width:
// 430px) display:none rule in mobile.css, or it's added to MOBILE_VISIBLE_ALLOWLIST
// 600px) display:none rule in mobile.css, or it's added to MOBILE_VISIBLE_ALLOWLIST
// below with a reason. A new default-visible header button with neither fails this
// test, forcing the author to decide its mobile behavior.
//
@@ -123,7 +123,7 @@ describe('Mobile header button policy (static guard)', () => {
hidden || allowed,
`Header button .${btn.distinguishing.join('.')} (id=${btn.id || '?'}) is VISIBLE BY DEFAULT but has ` +
`no mobile-visibility decision.\n` +
` → To hide it on phones: add it to the @media (max-width: 430px) "display: none" block in ` +
` → To hide it on phones: add it to the @media (max-width: 599px) "display: none" block in ` +
`src/web/public/mobile.css (next to .btn-settings / .btn-lifecycle-log).\n` +
` → To keep it visible on phones: add '${btn.distinguishing[0]}' to MOBILE_VISIBLE_ALLOWLIST in ` +
`this test, with a reason.\n` +
@@ -137,7 +137,7 @@ describe('Mobile header button policy (static guard)', () => {
for (const cls of KNOWN_PHONE_HIDDEN) {
expect(
phoneHidden.has(cls),
`${cls} must stay hidden on phones — restore its rule in the @media (max-width: 430px) ` +
`${cls} must stay hidden on phones — restore its rule in the @media (max-width: 599px) ` +
`display:none block in src/web/public/mobile.css.`
).toBe(true);
}
+1 -1
View File
@@ -1,7 +1,7 @@
// Port: none (pure model + static markup assertions — no browser, no server).
//
// The phone home screen (src/web/public/mobile-overview.js) replaces the welcome
// overlay under 430px. Its grouping logic is the part that can silently go wrong:
// overlay under 600px. Its grouping logic is the part that can silently go wrong:
// a session blocked on a permission prompt landing in "idle" is exactly the bug
// this surface exists to prevent. buildMobileOverviewModel() is pure for that
// reason, so it can be exercised here against plain objects.
+1 -1
View File
@@ -43,7 +43,7 @@ import { describe, expect, it } from 'vitest';
const CSS = readFileSync(resolve(import.meta.dirname, '../src/web/public/mobile.css'), 'utf8');
const ROOT = postcss.parse(CSS);
/** The phone block. Tablets keep the roomier layout and are deliberately out of scope. */
const PHONE_QUERY = '(max-width: 430px)';
const PHONE_QUERY = '(max-width: 599px)';
/** `.session-tab` border, from styles.css: `border: 1px solid transparent`. */
const TAB_BORDER = 1;
/**
+3 -3
View File
@@ -89,11 +89,11 @@ Matching `app.js MobileDetection` and `mobile.css` media queries:
| Breakpoint | Width | CSS Class | Header | Toolbar |
|------------|-------|-----------|--------|---------|
| **Phone** | ≤ 430px | `device-mobile` | Fixed at top | Fixed at bottom |
| **Tablet** | 431–768px | `device-tablet` | Fixed at top | Relative (in flow) |
| **Phone** | ≤ 599px | `device-mobile` | Fixed at top | Fixed at bottom |
| **Tablet** | 600–768px | `device-tablet` | Fixed at top | Relative (in flow) |
| **Desktop** | > 768px | `device-desktop` | Relative (in flow) | Relative (in flow) |
Breakpoint boundaries (430px, 768px) use `max-width` which is **inclusive** — a 430px device is phone, a 768px device is tablet.
The phone block is `max-width: 599px` and the tablet block starts at `min-width: 600px`, so a 599px device is phone and a 600px device (Nexus 7) is a small tablet in both CSS and the JS `getDeviceType()` cutoff (`< 600`). The tablet/desktop boundary (768px) is `max-width` inclusive: a 768px device is tablet.
## Architecture
+2 -2
View File
@@ -27,7 +27,7 @@ const BASE_URL = `http://localhost:${PORT}`;
let server: WebServer;
// Hidden on phones (< 430px width)
// Hidden on phones (< 600px width)
const PHONE_HIDDEN_SELECTORS = [
SELECTORS.HEADER_BRAND,
SELECTORS.CASE_SELECT_GROUP,
@@ -94,7 +94,7 @@ describe('Device Matrix', () => {
const { width } = device.viewport;
const position = await getCSSProperty(page, SELECTORS.TOOLBAR, 'position');
if (width <= BREAKPOINTS.PHONE_MAX) {
// Phone (max-width: 430px includes 430): fixed toolbar
// Phone (max-width: 599px): fixed toolbar
expect(position).toBe('fixed');
} else {
// Tablet/desktop: relative toolbar
+1 -1
View File
@@ -26,7 +26,7 @@ export interface DeviceEntry {
// ---------------------------------------------------------------------------
function breakpointFor(width: number): 'phone' | 'tablet' | 'desktop' {
if (width < 430) return 'phone';
if (width < 600) return 'phone';
if (width < 768) return 'tablet';
return 'desktop';
}
+1 -1
View File
@@ -59,7 +59,7 @@ export const SELECTORS = {
// Device breakpoints (match app.js MobileDetection)
export const BREAKPOINTS = {
PHONE_MAX: 430,
PHONE_MAX: 599,
TABLET_MAX: 768,
} as const;
+3 -3
View File
@@ -187,7 +187,7 @@ describe('Mobile Layout', () => {
}
});
it('does not render the desktop voice button at the 430px phone/tablet boundary', async () => {
it('does not render the desktop voice button on a 430px large phone', async () => {
const device = REPRESENTATIVE_DEVICES['large-phone'];
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
try {
@@ -331,7 +331,7 @@ describe('Mobile Layout', () => {
}
});
it('width < 430 adds device-mobile', async () => {
it('width < 600 adds device-mobile', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
try {
await assertDeviceClasses(page, iPhone14Pro.viewport.width);
@@ -340,7 +340,7 @@ describe('Mobile Layout', () => {
}
});
it('width 430-768 adds device-tablet', async () => {
it('width 600-768 adds device-tablet', async () => {
const tablet = REPRESENTATIVE_DEVICES['small-tablet'];
const { context, page } = await createDevicePage(tablet, BASE_URL);
try {
+4 -4
View File
@@ -44,7 +44,7 @@ describe('Visual Regression', () => {
isMobile: width < 768,
hasTouch: width < 768,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
expectedBreakpoint: (width < 430 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
expectedBreakpoint: (width < 600 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
isIOS: true,
defaultBrowserType: 'chromium' as const,
};
@@ -71,7 +71,7 @@ describe('Visual Regression', () => {
isMobile: width < 768,
hasTouch: width < 768,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
expectedBreakpoint: (width < 430 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
expectedBreakpoint: (width < 600 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
isIOS: true,
defaultBrowserType: 'chromium' as const,
};
@@ -104,7 +104,7 @@ describe('Visual Regression', () => {
isMobile: width < 768,
hasTouch: width < 768,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
expectedBreakpoint: (width < 430 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
expectedBreakpoint: (width < 600 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
isIOS: true,
defaultBrowserType: 'chromium' as const,
};
@@ -115,7 +115,7 @@ describe('Visual Regression', () => {
// Open settings modal - try mobile button first, then desktop
const mobileBtn = page.locator(SELECTORS.SETTINGS_MOBILE);
const isPhone = width < 430;
const isPhone = width < 600;
if (isPhone && await mobileBtn.isVisible()) {
await mobileBtn.click();
} else {
+2 -2
View File
@@ -35,8 +35,8 @@ describe('read my mind phone key + alternates (static guards)', () => {
const html = read('index.html');
const ui = read('readmymind-ui.js');
const settingsUi = read('settings-ui.js');
// Everything phone-specific lives in the max-width 430px block of mobile.css.
const phoneBlock = mobile.slice(mobile.indexOf('@media (max-width: 430px)'));
// Everything phone-specific lives in the max-width 599px block of mobile.css.
const phoneBlock = mobile.slice(mobile.indexOf('@media (max-width: 599px)'));
it('ships the 🧠 key in BOTH accessory bar templates and routes it to the modal', () => {
const simple = accessory.match(/_simpleButtons\s*:\s*`([\s\S]*?)`/)?.[1] ?? '';