mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39: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:
@@ -686,7 +686,7 @@
|
|||||||
<button class="btn-toolbar btn-shell" onclick="app.runShell()" title="Run Shell">
|
<button class="btn-toolbar btn-shell" onclick="app.runShell()" title="Run Shell">
|
||||||
Run Shell
|
Run Shell
|
||||||
</button>
|
</button>
|
||||||
<!-- Phone-only: replaces the Shell button on ≤430px (Shell moves into the Run
|
<!-- Phone-only: replaces the Shell button under 600px (Shell moves into the Run
|
||||||
dropdown there). Sends a bare Enter to the active session, the complement
|
dropdown there). Sends a bare Enter to the active session, the complement
|
||||||
to the accessory bar's Esc. Hidden everywhere else — see styles.css. -->
|
to the accessory bar's Esc. Hidden everywhere else — see styles.css. -->
|
||||||
<button class="btn-toolbar btn-enter" onclick="app.sendEnterKey()" title="Send Enter">
|
<button class="btn-toolbar btn-enter" onclick="app.sendEnterKey()" title="Send Enter">
|
||||||
|
|||||||
@@ -85,20 +85,20 @@ const MobileDetection = {
|
|||||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Check if screen is small (phone-sized, <430px) */
|
/** Check if screen is small (phone-sized, <600px) */
|
||||||
isSmallScreen() {
|
isSmallScreen() {
|
||||||
return window.innerWidth < 430;
|
return window.innerWidth < 600;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Check if screen is medium (tablet-sized, 430-768px) */
|
/** Check if screen is medium (tablet-sized, 600-768px) */
|
||||||
isMediumScreen() {
|
isMediumScreen() {
|
||||||
return window.innerWidth >= 430 && window.innerWidth < 768;
|
return window.innerWidth >= 600 && window.innerWidth < 768;
|
||||||
},
|
},
|
||||||
|
|
||||||
/** Get device type based on screen width */
|
/** Get device type based on screen width */
|
||||||
getDeviceType() {
|
getDeviceType() {
|
||||||
const width = window.innerWidth;
|
const width = window.innerWidth;
|
||||||
if (width < 430) return 'mobile';
|
if (width < 600) return 'mobile';
|
||||||
if (width < 768) return 'tablet';
|
if (width < 768) return 'tablet';
|
||||||
return 'desktop';
|
return 'desktop';
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
* the SAME comparator the desktop rail uses: blocked longest-first, then
|
* the SAME comparator the desktop rail uses: blocked longest-first, then
|
||||||
* running longest-first, then quiet most-recently-quiet first.
|
* running longest-first, then quiet most-recently-quiet first.
|
||||||
*
|
*
|
||||||
* PHONE ONLY. The gate is `shouldUseMobileOverview()` (viewport < 430px, not a
|
* PHONE ONLY. The gate is `shouldUseMobileOverview()` (viewport < 600px, not a
|
||||||
* popped-out solo window, per-device setting on). Tablet and desktop keep the
|
* popped-out solo window, per-device setting on). Tablet and desktop keep the
|
||||||
* welcome overlay untouched. The container ships with the `hidden` attribute and
|
* welcome overlay untouched. The container ships with the `hidden` attribute and
|
||||||
* only this module removes it, so desktop (which never loads mobile.css) cannot
|
* only this module removes it, so desktop (which never loads mobile.css) cannot
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** Viewport width that counts as a phone. Matches the mobile.css phone block. */
|
/** Viewport width that counts as a phone. Matches the mobile.css phone block. */
|
||||||
const MOBILE_OVERVIEW_PHONE_QUERY = '(max-width: 430px)';
|
const MOBILE_OVERVIEW_PHONE_QUERY = '(max-width: 599px)';
|
||||||
|
|
||||||
/** How many past conversations show before the "Show all" toggle. */
|
/** How many past conversations show before the "Show all" toggle. */
|
||||||
const MOBILE_OVERVIEW_PAST_LIMIT = 8;
|
const MOBILE_OVERVIEW_PAST_LIMIT = 8;
|
||||||
|
|||||||
+12
-12
@@ -46,9 +46,9 @@ html.mobile-init .file-browser-panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
Tablet Breakpoint (430px - 768px)
|
Tablet Breakpoint (600px - 768px)
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
@media (max-width: 768px) and (min-width: 430px) {
|
@media (max-width: 768px) and (min-width: 600px) {
|
||||||
/* Compact header for tablet - fixed at top, includes safe area padding */
|
/* Compact header for tablet - fixed at top, includes safe area padding */
|
||||||
.header {
|
.header {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -303,12 +303,12 @@ html.mobile-init .file-browser-panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Show desktop voice button on tablet (hidden by max-width:1023px in styles.css,
|
/* Show desktop voice button on tablet (hidden by max-width:1023px in styles.css,
|
||||||
mobile .btn-voice-mobile only shows at <430px) */
|
mobile .btn-voice-mobile only shows at <600px) */
|
||||||
.toolbar-center .btn-toolbar.btn-voice {
|
.toolbar-center .btn-toolbar.btn-voice {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Toolbar — use desktop-style sizing on tablet (plenty of room at 430-768px) */
|
/* Toolbar — use desktop-style sizing on tablet (plenty of room at 600-768px) */
|
||||||
.toolbar {
|
.toolbar {
|
||||||
padding: 0 0.5rem;
|
padding: 0 0.5rem;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -333,9 +333,9 @@ html.mobile-init .file-browser-panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
Phone Breakpoint (<430px)
|
Phone Breakpoint (<600px)
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
@media (max-width: 430px) {
|
@media (max-width: 599px) {
|
||||||
/* Phones get a 44px header, up from 36px. Every header control is a touch
|
/* Phones get a 44px header, up from 36px. Every header control is a touch
|
||||||
target and 44px is the floor for one; the brand "C" that gets you home is
|
target and 44px is the floor for one; the brand "C" that gets you home is
|
||||||
the one that matters most. Redefined as the TOKEN rather than a literal so
|
the one that matters most. Redefined as the TOKEN rather than a literal so
|
||||||
@@ -499,7 +499,7 @@ html.mobile-init .file-browser-panel {
|
|||||||
|
|
||||||
/* Exception to the 26px shrink above: in sidebar layout this button is the
|
/* Exception to the 26px shrink above: in sidebar layout this button is the
|
||||||
ONLY way to open the session list — the strip it replaced is gone. A 26px
|
ONLY way to open the session list — the strip it replaced is gone. A 26px
|
||||||
target is below --touch-target-min (44px), which the 430-768px block
|
target is below --touch-target-min (44px), which the 600-768px block
|
||||||
already enforces for every other header button. */
|
already enforces for every other header button. */
|
||||||
html[data-session-list='sidebar'] #sidebarToggleBtn {
|
html[data-session-list='sidebar'] #sidebarToggleBtn {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
@@ -1167,7 +1167,7 @@ html.mobile-init .file-browser-panel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 430px) {
|
@media (max-width: 599px) {
|
||||||
.btn-case-settings-mobile {
|
.btn-case-settings-mobile {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -3144,7 +3144,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
|||||||
|
|
||||||
/* Keyboard accessory bar + paste overlay base styles moved to styles.css
|
/* Keyboard accessory bar + paste overlay base styles moved to styles.css
|
||||||
(always loaded — covers iPad landscape where mobile.css doesn't load).
|
(always loaded — covers iPad landscape where mobile.css doesn't load).
|
||||||
Phone-specific overrides remain in @media (max-width: 430px) above. */
|
Phone-specific overrides remain in @media (max-width: 599px) above. */
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
iOS Safari Specific Fixes
|
iOS Safari Specific Fixes
|
||||||
@@ -3187,7 +3187,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 430px) {
|
@media (max-width: 599px) {
|
||||||
/* Attachment history (COD-18): full-screen sheet on phones */
|
/* Attachment history (COD-18): full-screen sheet on phones */
|
||||||
.attachment-history-drawer {
|
.attachment-history-drawer {
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -3237,7 +3237,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
|||||||
already reserves that space), so it needs the same safe-area padding as the
|
already reserves that space), so it needs the same safe-area padding as the
|
||||||
other banners. The overlay is fixed and handles its own insets.
|
other banners. The overlay is fixed and handles its own insets.
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
@media (max-width: 430px) {
|
@media (max-width: 599px) {
|
||||||
.offline-banner {
|
.offline-banner {
|
||||||
padding: 0.4rem 0.5rem;
|
padding: 0.4rem 0.5rem;
|
||||||
padding-left: calc(0.5rem + var(--safe-area-left));
|
padding-left: calc(0.5rem + var(--safe-area-left));
|
||||||
@@ -3727,7 +3727,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
|
|||||||
This whole file is served with media="(max-width: 1023px)", so these
|
This whole file is served with media="(max-width: 1023px)", so these
|
||||||
top-level rules cover the entire handheld range — deliberately NOT wrapped in
|
top-level rules cover the entire handheld range — deliberately NOT wrapped in
|
||||||
a nested @media, because the two compact `.session-tabs` blocks above live in
|
a nested @media, because the two compact `.session-tabs` blocks above live in
|
||||||
`max-width: 768px` and `max-width: 430px` and would leave 769-1023px
|
`max-width: 768px` and `max-width: 599px` and would leave 769-1023px
|
||||||
unhandled.
|
unhandled.
|
||||||
|
|
||||||
Placement at the END of the file is load-bearing: the compact strip blocks at
|
Placement at the END of the file is load-bearing: the compact strip blocks at
|
||||||
|
|||||||
@@ -2526,7 +2526,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
if (!input._mobileScrollWired) {
|
if (!input._mobileScrollWired) {
|
||||||
input._mobileScrollWired = true;
|
input._mobileScrollWired = true;
|
||||||
input.addEventListener('focus', () => {
|
input.addEventListener('focus', () => {
|
||||||
if (window.innerWidth <= 430) {
|
if (window.innerWidth < 600) {
|
||||||
setTimeout(() => input.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
|
setTimeout(() => input.scrollIntoView({ behavior: 'smooth', block: 'center' }), 300);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
document.getElementById('appSettingsShowMultiMonitorButton').checked = settings.showMultiMonitorButton ?? defaults.showMultiMonitorButton ?? false;
|
document.getElementById('appSettingsShowMultiMonitorButton').checked = settings.showMultiMonitorButton ?? defaults.showMultiMonitorButton ?? false;
|
||||||
document.getElementById('appSettingsShowPlanUsageLimits').checked = this.planUsageChipEnabled(settings);
|
document.getElementById('appSettingsShowPlanUsageLimits').checked = this.planUsageChipEnabled(settings);
|
||||||
document.getElementById('appSettingsShowRedrawButton').checked = settings.showRedrawButton ?? defaults.showRedrawButton ?? false;
|
document.getElementById('appSettingsShowRedrawButton').checked = settings.showRedrawButton ?? defaults.showRedrawButton ?? false;
|
||||||
// Phone overview home screen: only meaningful under 430px, so the row is
|
// Phone overview home screen: only meaningful under 600px, so the row is
|
||||||
// hidden elsewhere rather than offering a toggle that changes nothing.
|
// hidden elsewhere rather than offering a toggle that changes nothing.
|
||||||
// Spawn lineage lines: desktop-only (the overlay sits UNDER the fixed mobile
|
// Spawn lineage lines: desktop-only (the overlay sits UNDER the fixed mobile
|
||||||
// header), so the row is hidden elsewhere rather than offering a toggle that
|
// header), so the row is hidden elsewhere rather than offering a toggle that
|
||||||
|
|||||||
@@ -5156,7 +5156,7 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
|
|||||||
.run-mode-dot.shell { background: #94a3b8; }
|
.run-mode-dot.shell { background: #94a3b8; }
|
||||||
|
|
||||||
/* Phone-only Enter button (see index.html). Hidden by default at every width;
|
/* Phone-only Enter button (see index.html). Hidden by default at every width;
|
||||||
mobile.css turns it on inside @media (max-width: 430px), where it takes over
|
mobile.css turns it on inside @media (max-width: 599px), where it takes over
|
||||||
the slot the Shell button occupies on wider screens. */
|
the slot the Shell button occupies on wider screens. */
|
||||||
.btn-toolbar.btn-enter {
|
.btn-toolbar.btn-enter {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -11807,7 +11807,7 @@ kbd {
|
|||||||
}
|
}
|
||||||
/* Footer row: the buttons are btn-toolbar (display: flex, block-level), so
|
/* Footer row: the buttons are btn-toolbar (display: flex, block-level), so
|
||||||
without this rule the four of them stack vertically. Mirrors the
|
without this rule the four of them stack vertically. Mirrors the
|
||||||
runSummaryModal footer; the ≤430px block in mobile.css adds wrapping. */
|
runSummaryModal footer; the phone block (under 600px) in mobile.css adds wrapping. */
|
||||||
.readmymind-modal .modal-footer {
|
.readmymind-modal .modal-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -13184,7 +13184,7 @@ body.touch-device.cjk-input-visible .main {
|
|||||||
Keyboard Accessory Bar
|
Keyboard Accessory Bar
|
||||||
Base styles in styles.css (always loaded) so iPad landscape (≥1024px,
|
Base styles in styles.css (always loaded) so iPad landscape (≥1024px,
|
||||||
where mobile.css doesn't load) still gets dark styling. Phone overrides
|
where mobile.css doesn't load) still gets dark styling. Phone overrides
|
||||||
remain in mobile.css @media (max-width: 430px).
|
remain in mobile.css @media (max-width: 599px).
|
||||||
═══════════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
.keyboard-accessory-bar {
|
.keyboard-accessory-bar {
|
||||||
|
|||||||
@@ -4842,7 +4842,7 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
const viewportType =
|
const viewportType =
|
||||||
typeof MobileDetection !== 'undefined' && MobileDetection.getDeviceType
|
typeof MobileDetection !== 'undefined' && MobileDetection.getDeviceType
|
||||||
? MobileDetection.getDeviceType()
|
? MobileDetection.getDeviceType()
|
||||||
: window.innerWidth < 430
|
: window.innerWidth < 600
|
||||||
? 'mobile'
|
? 'mobile'
|
||||||
: window.innerWidth < 768
|
: window.innerWidth < 768
|
||||||
? 'tablet'
|
? 'tablet'
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function loadHomeSessionsApp(overrides: Record<string, any> = {}, innerWidth = 1
|
|||||||
createElement: () => fakeElement(),
|
createElement: () => fakeElement(),
|
||||||
createElementNS: () => 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']) {
|
for (const file of ['constants.js', 'mobile-overview.js', 'home-sessions.js']) {
|
||||||
vm.runInContext(readFileSync(resolve(PUBLIC, file), 'utf8'), context, { filename: file });
|
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
|
// 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:
|
// 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
|
// below with a reason. A new default-visible header button with neither fails this
|
||||||
// test, forcing the author to decide its mobile behavior.
|
// test, forcing the author to decide its mobile behavior.
|
||||||
//
|
//
|
||||||
@@ -123,7 +123,7 @@ describe('Mobile header button policy (static guard)', () => {
|
|||||||
hidden || allowed,
|
hidden || allowed,
|
||||||
`Header button .${btn.distinguishing.join('.')} (id=${btn.id || '?'}) is VISIBLE BY DEFAULT but has ` +
|
`Header button .${btn.distinguishing.join('.')} (id=${btn.id || '?'}) is VISIBLE BY DEFAULT but has ` +
|
||||||
`no mobile-visibility decision.\n` +
|
`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` +
|
`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 ` +
|
` → To keep it visible on phones: add '${btn.distinguishing[0]}' to MOBILE_VISIBLE_ALLOWLIST in ` +
|
||||||
`this test, with a reason.\n` +
|
`this test, with a reason.\n` +
|
||||||
@@ -137,7 +137,7 @@ describe('Mobile header button policy (static guard)', () => {
|
|||||||
for (const cls of KNOWN_PHONE_HIDDEN) {
|
for (const cls of KNOWN_PHONE_HIDDEN) {
|
||||||
expect(
|
expect(
|
||||||
phoneHidden.has(cls),
|
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.`
|
`display:none block in src/web/public/mobile.css.`
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Port: none (pure model + static markup assertions — no browser, no server).
|
// Port: none (pure model + static markup assertions — no browser, no server).
|
||||||
//
|
//
|
||||||
// The phone home screen (src/web/public/mobile-overview.js) replaces the welcome
|
// 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
|
// a session blocked on a permission prompt landing in "idle" is exactly the bug
|
||||||
// this surface exists to prevent. buildMobileOverviewModel() is pure for that
|
// this surface exists to prevent. buildMobileOverviewModel() is pure for that
|
||||||
// reason, so it can be exercised here against plain objects.
|
// 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 CSS = readFileSync(resolve(import.meta.dirname, '../src/web/public/mobile.css'), 'utf8');
|
||||||
const ROOT = postcss.parse(CSS);
|
const ROOT = postcss.parse(CSS);
|
||||||
/** The phone block. Tablets keep the roomier layout and are deliberately out of scope. */
|
/** 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`. */
|
/** `.session-tab` border, from styles.css: `border: 1px solid transparent`. */
|
||||||
const TAB_BORDER = 1;
|
const TAB_BORDER = 1;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ Matching `app.js MobileDetection` and `mobile.css` media queries:
|
|||||||
|
|
||||||
| Breakpoint | Width | CSS Class | Header | Toolbar |
|
| Breakpoint | Width | CSS Class | Header | Toolbar |
|
||||||
|------------|-------|-----------|--------|---------|
|
|------------|-------|-----------|--------|---------|
|
||||||
| **Phone** | ≤ 430px | `device-mobile` | Fixed at top | Fixed at bottom |
|
| **Phone** | ≤ 599px | `device-mobile` | Fixed at top | Fixed at bottom |
|
||||||
| **Tablet** | 431–768px | `device-tablet` | Fixed at top | Relative (in flow) |
|
| **Tablet** | 600–768px | `device-tablet` | Fixed at top | Relative (in flow) |
|
||||||
| **Desktop** | > 768px | `device-desktop` | Relative (in flow) | 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
|
## Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const BASE_URL = `http://localhost:${PORT}`;
|
|||||||
|
|
||||||
let server: WebServer;
|
let server: WebServer;
|
||||||
|
|
||||||
// Hidden on phones (< 430px width)
|
// Hidden on phones (< 600px width)
|
||||||
const PHONE_HIDDEN_SELECTORS = [
|
const PHONE_HIDDEN_SELECTORS = [
|
||||||
SELECTORS.HEADER_BRAND,
|
SELECTORS.HEADER_BRAND,
|
||||||
SELECTORS.CASE_SELECT_GROUP,
|
SELECTORS.CASE_SELECT_GROUP,
|
||||||
@@ -94,7 +94,7 @@ describe('Device Matrix', () => {
|
|||||||
const { width } = device.viewport;
|
const { width } = device.viewport;
|
||||||
const position = await getCSSProperty(page, SELECTORS.TOOLBAR, 'position');
|
const position = await getCSSProperty(page, SELECTORS.TOOLBAR, 'position');
|
||||||
if (width <= BREAKPOINTS.PHONE_MAX) {
|
if (width <= BREAKPOINTS.PHONE_MAX) {
|
||||||
// Phone (max-width: 430px includes 430): fixed toolbar
|
// Phone (max-width: 599px): fixed toolbar
|
||||||
expect(position).toBe('fixed');
|
expect(position).toBe('fixed');
|
||||||
} else {
|
} else {
|
||||||
// Tablet/desktop: relative toolbar
|
// Tablet/desktop: relative toolbar
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface DeviceEntry {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function breakpointFor(width: number): 'phone' | 'tablet' | 'desktop' {
|
function breakpointFor(width: number): 'phone' | 'tablet' | 'desktop' {
|
||||||
if (width < 430) return 'phone';
|
if (width < 600) return 'phone';
|
||||||
if (width < 768) return 'tablet';
|
if (width < 768) return 'tablet';
|
||||||
return 'desktop';
|
return 'desktop';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const SELECTORS = {
|
|||||||
|
|
||||||
// Device breakpoints (match app.js MobileDetection)
|
// Device breakpoints (match app.js MobileDetection)
|
||||||
export const BREAKPOINTS = {
|
export const BREAKPOINTS = {
|
||||||
PHONE_MAX: 430,
|
PHONE_MAX: 599,
|
||||||
TABLET_MAX: 768,
|
TABLET_MAX: 768,
|
||||||
} as const;
|
} 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 device = REPRESENTATIVE_DEVICES['large-phone'];
|
||||||
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
|
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
|
||||||
try {
|
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);
|
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
|
||||||
try {
|
try {
|
||||||
await assertDeviceClasses(page, iPhone14Pro.viewport.width);
|
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 tablet = REPRESENTATIVE_DEVICES['small-tablet'];
|
||||||
const { context, page } = await createDevicePage(tablet, BASE_URL);
|
const { context, page } = await createDevicePage(tablet, BASE_URL);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('Visual Regression', () => {
|
|||||||
isMobile: width < 768,
|
isMobile: width < 768,
|
||||||
hasTouch: width < 768,
|
hasTouch: width < 768,
|
||||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
|
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,
|
isIOS: true,
|
||||||
defaultBrowserType: 'chromium' as const,
|
defaultBrowserType: 'chromium' as const,
|
||||||
};
|
};
|
||||||
@@ -71,7 +71,7 @@ describe('Visual Regression', () => {
|
|||||||
isMobile: width < 768,
|
isMobile: width < 768,
|
||||||
hasTouch: width < 768,
|
hasTouch: width < 768,
|
||||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
|
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,
|
isIOS: true,
|
||||||
defaultBrowserType: 'chromium' as const,
|
defaultBrowserType: 'chromium' as const,
|
||||||
};
|
};
|
||||||
@@ -104,7 +104,7 @@ describe('Visual Regression', () => {
|
|||||||
isMobile: width < 768,
|
isMobile: width < 768,
|
||||||
hasTouch: width < 768,
|
hasTouch: width < 768,
|
||||||
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
|
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,
|
isIOS: true,
|
||||||
defaultBrowserType: 'chromium' as const,
|
defaultBrowserType: 'chromium' as const,
|
||||||
};
|
};
|
||||||
@@ -115,7 +115,7 @@ describe('Visual Regression', () => {
|
|||||||
|
|
||||||
// Open settings modal - try mobile button first, then desktop
|
// Open settings modal - try mobile button first, then desktop
|
||||||
const mobileBtn = page.locator(SELECTORS.SETTINGS_MOBILE);
|
const mobileBtn = page.locator(SELECTORS.SETTINGS_MOBILE);
|
||||||
const isPhone = width < 430;
|
const isPhone = width < 600;
|
||||||
if (isPhone && await mobileBtn.isVisible()) {
|
if (isPhone && await mobileBtn.isVisible()) {
|
||||||
await mobileBtn.click();
|
await mobileBtn.click();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ describe('read my mind phone key + alternates (static guards)', () => {
|
|||||||
const html = read('index.html');
|
const html = read('index.html');
|
||||||
const ui = read('readmymind-ui.js');
|
const ui = read('readmymind-ui.js');
|
||||||
const settingsUi = read('settings-ui.js');
|
const settingsUi = read('settings-ui.js');
|
||||||
// Everything phone-specific lives in the max-width 430px block of mobile.css.
|
// Everything phone-specific lives in the max-width 599px block of mobile.css.
|
||||||
const phoneBlock = mobile.slice(mobile.indexOf('@media (max-width: 430px)'));
|
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', () => {
|
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] ?? '';
|
const simple = accessory.match(/_simpleButtons\s*:\s*`([\s\S]*?)`/)?.[1] ?? '';
|
||||||
|
|||||||
Reference in New Issue
Block a user