mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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:
@@ -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;
|
||||||
@@ -1170,7 +1170,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;
|
||||||
}
|
}
|
||||||
@@ -3147,7 +3147,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
|
||||||
@@ -3190,7 +3190,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;
|
||||||
@@ -3240,7 +3240,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));
|
||||||
@@ -3730,7 +3730,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -402,7 +402,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
|
||||||
|
|||||||
@@ -5270,7 +5270,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;
|
||||||
@@ -11930,7 +11930,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;
|
||||||
@@ -13307,7 +13307,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 {
|
||||||
|
|||||||
@@ -5082,7 +5082,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