mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 08:29:42 +02:00
fix(mobile): raise the phone breakpoint from 430px to 600px
The phone tier stopped at innerWidth < 430 and @media (max-width: 430px), so every current large phone landed in the tablet layout: the 430pt iPhone 14 Pro Max, 15 Plus, 15 Pro Max and 16 Plus, the 440pt iPhone 16 Pro Max and 17 Pro Max, Pixel 6 Pro, 7 Pro and OnePlus 12 Pro, the 448pt Pixel 8 Pro and 9 Pro XL, and the Galaxy Z Fold 5 cover screen at 460. On those devices the header icon row replaced the session pill, the toolbar kept the desktop Run Shell button instead of Enter and the mic, the keyboard accessory bar could never become visible because its .visible rule lives inside the phone block, and the toolbar jumped to the top of the page when the keyboard opened. The new cutoff is 600, the line test/mobile/devices.ts already draws between large phones (430-599) and small tablets (600-767). No physical device sits between 480 and 600, but a phone zoomed out one or two steps in Safari does: a 440pt iPhone at 85% or 75% page zoom reports 518px or 587px and still needs the phone controls, which a 480 cutoff would have taken away. The phone block is max-width: 599px and the tablet block starts at min-width: 600px, so a 600px device is a tablet in CSS and in getDeviceType() alike instead of straddling the boundary the way 430pt phones did. The number changes everywhere it is encoded: JS, CSS, comments, CLAUDE.md, the CI tests that pin the phone block, and the test:mobile helpers. Measurement history that names 430px stays as written.
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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] ?? '';
|
||||
|
||||
Reference in New Issue
Block a user