mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
feat(mobile): let the text-input census read a literal already clear of the floor (OTA phase C, C7.2, ruling 12)
The floor is the rule and the seam is the mechanism. A binding rule alone made the custom-key capture field an offender at 22, where nothing can zoom, and the only way to satisfy it was to lower a one-character field to 16 — the tail wagging the dog. The seam's web half now exports the floor it already computed `Math.max` against, and the census reads that number out of that file rather than carrying a second copy of 16. The rule becomes "the seam's binding, or a literal at or above the floor", with no per-site exemption: a literal under the floor is still reported, which is the case the seam exists for. A tree whose seam declares no floor is refused rather than judged against a number the census invented. So the capture field goes back to 22 on both platforms and its split, its override entry and its parity test go with it. The chat's two fields stay split, because 15 is under the floor however it is spelled. Red-first: with the rule removed, a planted literal 16 and a literal 22 are both reported and the refusal case does not throw; a literal 15 is reported either way. All three route closures that run this census — session, source-control, review — report 0 offenders and 0 unresolved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
@@ -30,7 +30,7 @@ const describeClosure = mobileWebAppDependenciesPresent() ? describe : describe.
|
||||
const SESSION = 'app/h/[hostId]/session/[worktreeId].tsx'
|
||||
|
||||
/**
|
||||
* The two style modules this screen splits, as the page bundle resolves them.
|
||||
* The style module this screen splits, as the page bundle resolves it.
|
||||
*
|
||||
* Named rather than left to the offender list because a split is the one fix that can be undone
|
||||
* without reopening the offence: delete the `.web.ts` and the native sibling's size is what the
|
||||
@@ -38,10 +38,7 @@ const SESSION = 'app/h/[hostId]/session/[worktreeId].tsx'
|
||||
* so — but only the next time someone reads it. Listed here, the closure says which file the page
|
||||
* loads.
|
||||
*/
|
||||
const SPLIT_WEB_STYLES = [
|
||||
'src/components/custom-key-input-styles.web.ts',
|
||||
'src/session/mobile-native-chat-input-styles.web.ts'
|
||||
]
|
||||
const SPLIT_WEB_STYLES = ['src/session/mobile-native-chat-input-styles.web.ts']
|
||||
|
||||
describeClosure(
|
||||
'the text inputs the session screen reaches',
|
||||
|
||||
@@ -20,6 +20,7 @@ import { mobileWebAppRouteClosure } from './build-mobile-web-app-bundle.mjs'
|
||||
import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs'
|
||||
import {
|
||||
TEXT_INPUT_FONT_SIZE_SEAM,
|
||||
textInputFontSizeFloor,
|
||||
textInputFontSizeOffenders,
|
||||
unresolvedTextInputStyles
|
||||
} from './mobile-web-app-text-input-font-size-seam.mjs'
|
||||
@@ -34,12 +35,26 @@ const REVIEW = 'app/h/[hostId]/review/[worktreeId].tsx'
|
||||
const SEAM_SOURCE = {
|
||||
'src/platform/text-input-font-size.ts': 'export const TEXT_INPUT_FONT_SIZE = 14'
|
||||
}
|
||||
|
||||
/**
|
||||
* The seam's web half, seeded into every scratch tree below.
|
||||
*
|
||||
* Not a fixture detail: the floor is declared here and the census reads it here, so a tree without
|
||||
* this file is one the rule refuses to judge at all. Seeding it makes every case a tree with a
|
||||
* seam, which is what a real one is; the case that checks the refusal writes its own over the top.
|
||||
*/
|
||||
const FLOOR_SOURCE = {
|
||||
'src/platform/text-input-font-size.web.ts': [
|
||||
'export const TEXT_INPUT_FONT_SIZE_FLOOR = 16',
|
||||
'export const TEXT_INPUT_FONT_SIZE = 16'
|
||||
].join('\n')
|
||||
}
|
||||
const SEAM_IMPORT = "import { TEXT_INPUT_FONT_SIZE } from '../platform/text-input-font-size'"
|
||||
|
||||
/** A scratch module tree, so a planted offender never lands in the tree other censuses walk. */
|
||||
function plant(files) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-text-input-census-'))
|
||||
for (const [path, source] of Object.entries(files)) {
|
||||
for (const [path, source] of Object.entries({ ...FLOOR_SOURCE, ...files })) {
|
||||
mkdirSync(join(root, path.slice(0, path.lastIndexOf('/'))), { recursive: true })
|
||||
writeFileSync(join(root, path), source)
|
||||
}
|
||||
@@ -191,6 +206,58 @@ describe('the size a text input declares, as the census reads it', () => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* The floor is the rule and the seam is the mechanism, so a literal already clear of the floor
|
||||
* satisfies it without binding to anything.
|
||||
*
|
||||
* Written as three sizes rather than one: a rule that only proved 22 passes would also be
|
||||
* satisfied by a census that stopped reading literals at all, and the 15 is the case the whole
|
||||
* seam exists for. The boundary is included because "at or above" is where an off-by-one lives.
|
||||
*/
|
||||
it.each([
|
||||
['under the floor, which is the offence the seam exists for', 15, ['src/ui/Sized.tsx:1']],
|
||||
['exactly the floor', 16, []],
|
||||
['well above the floor, which no binding could keep', 22, []]
|
||||
])('reads a literal %s', (_label, size, expected) => {
|
||||
const root = plant({
|
||||
'src/ui/Sized.tsx': `export const Sized = () => <TextInput style={{ fontSize: ${size} }} />`,
|
||||
...SEAM_SOURCE
|
||||
})
|
||||
try {
|
||||
const closure = { local: ['src/ui/Sized.tsx'] }
|
||||
expect(textInputFontSizeOffenders(root, closure)).toEqual(expected)
|
||||
expect(unresolvedTextInputStyles(root, closure)).toEqual([])
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('reads the floor out of the seam rather than carrying its own copy of 16', () => {
|
||||
// The half that keeps the rule honest: a census with its own number would go on passing after
|
||||
// the seam's moved, and the two copies would disagree in the direction nobody reads again.
|
||||
const root = plant({})
|
||||
try {
|
||||
expect(textInputFontSizeFloor(root)).toBe(16)
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('refuses to judge a tree whose seam declares no floor, rather than assuming one', () => {
|
||||
const root = plant({
|
||||
'src/ui/Sized.tsx': 'export const Sized = () => <TextInput style={{ fontSize: 22 }} />',
|
||||
'src/platform/text-input-font-size.ts': 'export const TEXT_INPUT_FONT_SIZE = 14',
|
||||
'src/platform/text-input-font-size.web.ts': 'export const TEXT_INPUT_FONT_SIZE = 16'
|
||||
})
|
||||
try {
|
||||
expect(() => textInputFontSizeOffenders(root, { local: ['src/ui/Sized.tsx'] })).toThrow(
|
||||
/declares no numeric/
|
||||
)
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('names a style shape it cannot follow rather than dropping it', () => {
|
||||
const root = plant({
|
||||
'src/ui/Called.tsx': 'export const Called = () => <TextInput style={makeStyle()} />'
|
||||
|
||||
@@ -18,8 +18,46 @@ export const TEXT_INPUT_FONT_SIZE_SEAM = 'src/platform/text-input-font-size.web.
|
||||
const SEAM_EXPORT = 'TEXT_INPUT_FONT_SIZE'
|
||||
const SEAM_MODULE = 'src/platform/text-input-font-size.ts'
|
||||
|
||||
/** The floor's name in the seam's web half, which is the only place the number is written. */
|
||||
const FLOOR_EXPORT = 'TEXT_INPUT_FONT_SIZE_FLOOR'
|
||||
|
||||
const parse = (file, source) => ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true)
|
||||
|
||||
/** One read per tree: the file does not change under a run, and every style asks for it. */
|
||||
const floorByRoot = new Map()
|
||||
|
||||
/**
|
||||
* The size at or above which an input cannot make iOS zoom the page, read from the seam itself.
|
||||
*
|
||||
* Read rather than restated, and that is the whole reason this rule can exist: the seam's web half
|
||||
* already computes `Math.max(bodySize, floor)`, so a census that wrote `16` beside it would be a
|
||||
* second copy of the one number the seam is for, and the two would drift in the direction nobody
|
||||
* reads again.
|
||||
*
|
||||
* Absent is a throw rather than a default. A census that silently fell back to a number of its own
|
||||
* would go on passing while the thing it measures against had moved or gone.
|
||||
*/
|
||||
export function textInputFontSizeFloor(mobileDir) {
|
||||
const cached = floorByRoot.get(mobileDir)
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
const source = readOrNull(join(mobileDir, TEXT_INPUT_FONT_SIZE_SEAM))
|
||||
if (source === null) {
|
||||
throw new Error(`[text-input-font-size-seam] no seam at ${TEXT_INPUT_FONT_SIZE_SEAM}`)
|
||||
}
|
||||
const parsed = parse(TEXT_INPUT_FONT_SIZE_SEAM, source)
|
||||
const declared = declarationOf(parsed, FLOOR_EXPORT)
|
||||
if (declared === null || !ts.isNumericLiteral(declared)) {
|
||||
throw new Error(
|
||||
`[text-input-font-size-seam] ${TEXT_INPUT_FONT_SIZE_SEAM} declares no numeric ${FLOOR_EXPORT}`
|
||||
)
|
||||
}
|
||||
const floor = Number(declared.text)
|
||||
floorByRoot.set(mobileDir, floor)
|
||||
return floor
|
||||
}
|
||||
|
||||
function readOrNull(path) {
|
||||
try {
|
||||
return readFileSync(path, 'utf8')
|
||||
@@ -371,7 +409,25 @@ function resolveStyleKey(mobileDir, file, exportName, key, seen = new Set()) {
|
||||
return null
|
||||
}
|
||||
|
||||
/** The `fontSize` a style object literal declares, with whether it came through the seam. */
|
||||
/**
|
||||
* Whether a size is a literal that already clears the floor.
|
||||
*
|
||||
* The floor is the rule and the seam is the mechanism, so a style that declares a number at or
|
||||
* above it satisfies the rule without binding to anything: 22 on a capture field cannot zoom a
|
||||
* page, and making it read the seam would have lowered it to 16 to satisfy a census. A literal
|
||||
* under the floor is still an offence, which is the case the rule was written for.
|
||||
*
|
||||
* Literals only. `typography.bodySize + 1` is 15 today and whatever the theme says tomorrow, and a
|
||||
* census that evaluated expressions would be a second renderer.
|
||||
*/
|
||||
function isLiteralAtOrAboveFloor(mobileDir, initializer) {
|
||||
return (
|
||||
ts.isNumericLiteral(initializer) &&
|
||||
Number(initializer.text) >= textInputFontSizeFloor(mobileDir)
|
||||
)
|
||||
}
|
||||
|
||||
/** The `fontSize` a style object literal declares, with whether the rule is satisfied. */
|
||||
function fontSizeIn(mobileDir, parsed, file, object) {
|
||||
for (const entry of object.properties) {
|
||||
if (ts.isPropertyAssignment(entry) && entry.name.getText() === 'fontSize') {
|
||||
@@ -379,7 +435,9 @@ function fontSizeIn(mobileDir, parsed, file, object) {
|
||||
file,
|
||||
text: entry.initializer.getText(),
|
||||
line: parsed.getLineAndCharacterOfPosition(entry.getStart(parsed)).line + 1,
|
||||
onSeam: isSeamBinding(mobileDir, parsed, file, entry.initializer)
|
||||
onSeam:
|
||||
isSeamBinding(mobileDir, parsed, file, entry.initializer) ||
|
||||
isLiteralAtOrAboveFloor(mobileDir, entry.initializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,22 @@ export const customKeyModalStyles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
paddingBottom: spacing.sm
|
||||
},
|
||||
keyInput: {
|
||||
width: '100%',
|
||||
height: 56,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.bgPanel,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderSubtle,
|
||||
color: colors.textPrimary,
|
||||
fontFamily: typography.monoFamily,
|
||||
// 22 on both platforms, and no binding to the seam: the floor is the rule and this clears it,
|
||||
// so reading the seam here would have lowered a one-character capture field to 16 to satisfy
|
||||
// a census. C7 ruling 12.
|
||||
fontSize: 22,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center'
|
||||
},
|
||||
backButton: {
|
||||
width: 30,
|
||||
height: 30,
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
type TerminalShortcutSpecialKey
|
||||
} from '../terminal/terminal-accessory-keys'
|
||||
import { customKeyModalStyles as styles } from './CustomKeyModal.styles'
|
||||
import { customKeyInputStyles } from './custom-key-input-styles'
|
||||
|
||||
const CUSTOM_ACCESSORY_KEYS_STORAGE_KEY = 'orca:custom-accessory-keys'
|
||||
|
||||
@@ -294,7 +293,7 @@ export function CustomKeyModal({ visible, onClose, onKeysChanged, onManageShortc
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionLabel}>Key</Text>
|
||||
<TextInput
|
||||
style={customKeyInputStyles.keyInput}
|
||||
style={styles.keyInput}
|
||||
value={shortcutKey.length === 1 ? shortcutKey.toUpperCase() : ''}
|
||||
onChangeText={handleShortcutKeyInput}
|
||||
placeholder={SPECIAL_KEY_BY_ID[shortcutKey]?.label ?? 'C'}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { colors, typography } from '../theme/mobile-theme'
|
||||
|
||||
/**
|
||||
* The custom-key capture field, minus the one thing that is a platform answer.
|
||||
*
|
||||
* Shared because a `.web.ts` cannot import a value from the file it shadows, and two copies of a
|
||||
* style object is how the two platforms drift apart on everything except the difference that was
|
||||
* meant to be between them.
|
||||
*/
|
||||
export const customKeyInputBase = {
|
||||
width: '100%',
|
||||
height: 56,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.bgPanel,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderSubtle,
|
||||
color: colors.textPrimary,
|
||||
fontFamily: typography.monoFamily,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center'
|
||||
} as const
|
||||
@@ -1,15 +0,0 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { customKeyInputBase } from './custom-key-input-base-styles'
|
||||
|
||||
/**
|
||||
* Native: the single character this field captures is shown large, which is what it has rendered
|
||||
* at since the modal existed.
|
||||
*
|
||||
* The `.web.ts` sibling puts it on the text-input seam instead. That is a reduction rather than
|
||||
* the raise every other input in this closure gets — 22 is already clear of the focus-zoom floor —
|
||||
* and it is the price of the seam being a binding rule rather than a number: a size the census
|
||||
* cannot follow to the seam module is one nobody can tell from a 14 that was left behind.
|
||||
*/
|
||||
export const customKeyInputStyles = StyleSheet.create({
|
||||
keyInput: { ...customKeyInputBase, fontSize: 22 }
|
||||
})
|
||||
@@ -1,78 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// StyleSheet.create is identity in React Native and on RN Web alike, and every other export of the
|
||||
// module reaches the native runtime this test does not have.
|
||||
vi.mock('react-native', () => ({
|
||||
StyleSheet: { create: (styles: unknown) => styles }
|
||||
}))
|
||||
|
||||
// The seam as the page bundle resolves it. Without this the `.web.ts` style below would read the
|
||||
// native seam and the test would pass on a size that no browser ever renders.
|
||||
vi.mock(
|
||||
'../platform/text-input-font-size',
|
||||
async () => await import('../platform/text-input-font-size.web')
|
||||
)
|
||||
|
||||
import { TEXT_INPUT_FONT_SIZE } from '../platform/text-input-font-size'
|
||||
import { colors, typography } from '../theme/mobile-theme'
|
||||
import { customKeyInputBase } from './custom-key-input-base-styles'
|
||||
import { customKeyInputStyles } from './custom-key-input-styles'
|
||||
import { customKeyInputStyles as customKeyInputStylesOnWeb } from './custom-key-input-styles.web'
|
||||
|
||||
/** Below this an iOS browser zooms the page when an input takes focus, and does not zoom back. */
|
||||
const IOS_FOCUS_ZOOM_FLOOR = 16
|
||||
|
||||
/** Every property the capture field carried before it was split, read off the commit that split it. */
|
||||
const BEFORE_THE_SPLIT = {
|
||||
width: '100%',
|
||||
height: 56,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.bgPanel,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.borderSubtle,
|
||||
color: colors.textPrimary,
|
||||
fontFamily: typography.monoFamily,
|
||||
fontSize: 22,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center'
|
||||
}
|
||||
|
||||
describe('the custom-key capture field natively', () => {
|
||||
it('renders exactly what it rendered before the split, property for property', () => {
|
||||
expect(customKeyInputStyles.keyInput).toEqual(BEFORE_THE_SPLIT)
|
||||
// Key for key as well as value for value: `toEqual` would pass over an extra undefined.
|
||||
expect(Object.keys(customKeyInputStyles.keyInput).sort()).toEqual(
|
||||
Object.keys(BEFORE_THE_SPLIT).sort()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the custom-key capture field on the web', () => {
|
||||
it('takes its size from the seam, which clears the focus-zoom floor', () => {
|
||||
expect(customKeyInputStylesOnWeb.keyInput.fontSize).toBe(TEXT_INPUT_FONT_SIZE)
|
||||
expect(customKeyInputStylesOnWeb.keyInput.fontSize).toBeGreaterThanOrEqual(IOS_FOCUS_ZOOM_FLOOR)
|
||||
})
|
||||
|
||||
/**
|
||||
* The one input on this screen the seam lowers rather than raises, recorded rather than implied.
|
||||
*
|
||||
* 22 already clears the floor, so this move buys nothing for the keyboard seam; what it buys is
|
||||
* that the census reads every size on the screen as a binding to one module. Written as a
|
||||
* comparison rather than as the number 16, so a theme that raised the body size past 22 would
|
||||
* make this fail and be read rather than silently reverse the direction.
|
||||
*/
|
||||
it('is the one field the move shrinks, and says so', () => {
|
||||
expect(customKeyInputStylesOnWeb.keyInput.fontSize).toBeLessThan(BEFORE_THE_SPLIT.fontSize)
|
||||
})
|
||||
|
||||
// The split is one value, not a second style: everything the siblings do not differ on comes from
|
||||
// the same object, so a padding or a colour cannot drift between the platforms.
|
||||
it('differs from the native style in nothing but the size', () => {
|
||||
expect(customKeyInputBase).not.toHaveProperty('fontSize')
|
||||
expect(customKeyInputStyles.keyInput).toMatchObject(customKeyInputBase)
|
||||
expect(customKeyInputStylesOnWeb.keyInput).toMatchObject(customKeyInputBase)
|
||||
expect(Object.keys(customKeyInputStylesOnWeb.keyInput).sort()).toEqual(
|
||||
Object.keys(customKeyInputStyles.keyInput).sort()
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,17 +0,0 @@
|
||||
import { StyleSheet } from 'react-native'
|
||||
import { TEXT_INPUT_FONT_SIZE } from '../platform/text-input-font-size'
|
||||
import { customKeyInputBase } from './custom-key-input-base-styles'
|
||||
|
||||
/**
|
||||
* Web sibling: the capture field goes on the text-input seam.
|
||||
*
|
||||
* Every other input on this screen is raised by that move; this one is lowered, from 22 to the
|
||||
* seam's 16. Both sizes clear the floor below which iOS zooms the page on focus, so nothing about
|
||||
* the keyboard seam turns on which of them renders — what turns on it is that the census reads the
|
||||
* seam as a binding and not as a number, so there is no expression that keeps 22 here and still
|
||||
* says where the size came from. A 56px box holding one capitalised character carries 16 legibly,
|
||||
* and the alternative is a per-site exemption the next 14px input would inherit.
|
||||
*/
|
||||
export const customKeyInputStyles = StyleSheet.create({
|
||||
keyInput: { ...customKeyInputBase, fontSize: TEXT_INPUT_FONT_SIZE }
|
||||
})
|
||||
@@ -19,7 +19,10 @@ import { customKeyModalStyles } from '../components/CustomKeyModal.styles'
|
||||
import { mobileSessionCommandInputStyles } from '../session/mobile-session-command-input-styles'
|
||||
import { typography } from '../theme/mobile-theme'
|
||||
import { TEXT_INPUT_FONT_SIZE } from './text-input-font-size'
|
||||
import { TEXT_INPUT_FONT_SIZE as WEB_TEXT_INPUT_FONT_SIZE } from './text-input-font-size.web'
|
||||
import {
|
||||
TEXT_INPUT_FONT_SIZE_FLOOR,
|
||||
TEXT_INPUT_FONT_SIZE as WEB_TEXT_INPUT_FONT_SIZE
|
||||
} from './text-input-font-size.web'
|
||||
|
||||
/**
|
||||
* The size every page-served text input carries, on each platform.
|
||||
@@ -53,9 +56,9 @@ const STYLE_MODULES = [
|
||||
*/
|
||||
const SPLIT_STYLE_MODULES = [
|
||||
'src/browser/browser-address-field-styles.web.ts',
|
||||
// The session screen's two: a capture field at 22 and the chat's two fields at 15, none of
|
||||
// which is the body size, so each keeps its own native sibling.
|
||||
'src/components/custom-key-input-styles.web.ts',
|
||||
// The session screen's one: the chat's two fields sit at 15, under the floor and not the body
|
||||
// size, so they keep a native sibling. The custom-key capture field needed no split — 22 clears
|
||||
// the floor, and the census reads a literal that does as satisfying the rule (C7 ruling 12).
|
||||
'src/session/mobile-native-chat-input-styles.web.ts'
|
||||
]
|
||||
|
||||
@@ -64,9 +67,11 @@ const SEAM_EXPORT_NAME = 'TEXT_INPUT_FONT_SIZE'
|
||||
|
||||
describe('the font size the page-served text inputs carry', () => {
|
||||
it('clears the size iOS zooms the page for, on the web', () => {
|
||||
// 16 is the floor; below it a focus zooms the document and the keyboard seam, which reads a
|
||||
// scale other than 1 as no keyboard, stops lifting for the rest of the session.
|
||||
expect(WEB_TEXT_INPUT_FONT_SIZE).toBeGreaterThanOrEqual(16)
|
||||
// Below the floor a focus zooms the document, and the keyboard seam reads a scale other than
|
||||
// 1 as no keyboard and stops lifting for the rest of the session. The number is the seam's
|
||||
// own, read rather than restated, because the census over every page route reads it too.
|
||||
expect(WEB_TEXT_INPUT_FONT_SIZE).toBeGreaterThanOrEqual(TEXT_INPUT_FONT_SIZE_FLOOR)
|
||||
expect(TEXT_INPUT_FONT_SIZE_FLOOR).toBe(16)
|
||||
})
|
||||
|
||||
it('leaves a phone rendering exactly what it rendered before', () => {
|
||||
@@ -75,6 +80,11 @@ describe('the font size the page-served text inputs carry', () => {
|
||||
expect(mobileDiffReviewControlStyles.composerInput.fontSize).toBe(typography.bodySize)
|
||||
expect(mobileBrowserPaneStyles.keyboardInput.fontSize).toBe(typography.bodySize)
|
||||
expect(customKeyModalStyles.fieldInput.fontSize).toBe(typography.bodySize)
|
||||
// The capture field beside it, which is the one input on this screen no seam touches.
|
||||
expect(customKeyModalStyles.keyInput.fontSize).toBe(22)
|
||||
expect(customKeyModalStyles.keyInput.fontSize).toBeGreaterThanOrEqual(
|
||||
TEXT_INPUT_FONT_SIZE_FLOOR
|
||||
)
|
||||
expect(mobileSessionCommandInputStyles.textInput.fontSize).toBe(typography.bodySize)
|
||||
// The pane's address bar is the one that is split: it keeps the compact size natively, so the
|
||||
// seam reaches it through the `.web.ts` sibling rather than through this constant.
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { typography } from '../theme/mobile-theme'
|
||||
|
||||
/** Below this, iOS Safari and every iOS WebView zoom the page when an input takes focus. */
|
||||
const IOS_FOCUS_ZOOM_FLOOR = 16
|
||||
/**
|
||||
* Below this, iOS Safari and every iOS WebView zoom the page when an input takes focus.
|
||||
*
|
||||
* Exported because the floor is the rule and this constant is the only statement of it. The
|
||||
* census over a page route's closure reads this number out of this file rather than restating it,
|
||||
* so an input that declares a literal already at or above it is on the floor by construction and
|
||||
* needs no binding — and a floor that moved would move both halves together.
|
||||
*/
|
||||
export const TEXT_INPUT_FONT_SIZE_FLOOR = 16
|
||||
|
||||
/**
|
||||
* Web sibling: the app's body size, raised to the size that stops the page being zoomed.
|
||||
@@ -18,4 +25,4 @@ const IOS_FOCUS_ZOOM_FLOOR = 16
|
||||
*
|
||||
* `Math.max` rather than the constant, so a theme that raises the body size past 16 keeps it.
|
||||
*/
|
||||
export const TEXT_INPUT_FONT_SIZE = Math.max(typography.bodySize, IOS_FOCUS_ZOOM_FLOOR)
|
||||
export const TEXT_INPUT_FONT_SIZE = Math.max(typography.bodySize, TEXT_INPUT_FONT_SIZE_FLOOR)
|
||||
|
||||
@@ -14,14 +14,12 @@ vi.mock(
|
||||
)
|
||||
|
||||
import { TEXT_INPUT_FONT_SIZE } from '../platform/text-input-font-size'
|
||||
import { TEXT_INPUT_FONT_SIZE_FLOOR } from '../platform/text-input-font-size.web'
|
||||
import { colors, radii, spacing, typography } from '../theme/mobile-theme'
|
||||
import { mobileNativeChatInputBase } from './mobile-native-chat-input-base-styles'
|
||||
import { mobileNativeChatInputStyles } from './mobile-native-chat-input-styles'
|
||||
import { mobileNativeChatInputStyles as onWeb } from './mobile-native-chat-input-styles.web'
|
||||
|
||||
/** Below this an iOS browser zooms the page when an input takes focus, and does not zoom back. */
|
||||
const IOS_FOCUS_ZOOM_FLOOR = 16
|
||||
|
||||
/** Every property the two fields carried before the split, read off the commit that split them. */
|
||||
const BEFORE_THE_SPLIT = {
|
||||
input: {
|
||||
@@ -64,14 +62,14 @@ describe('the chat composer and question fields natively', () => {
|
||||
it('sits one point under the floor, which is why the split exists', () => {
|
||||
// The premise, not a restatement: if the body size ever rose to 15 this whole pair collapses
|
||||
// into an in-place move and someone should be told rather than left maintaining three files.
|
||||
expect(BEFORE_THE_SPLIT.input.fontSize).toBeLessThan(IOS_FOCUS_ZOOM_FLOOR)
|
||||
expect(BEFORE_THE_SPLIT.input.fontSize).toBeLessThan(TEXT_INPUT_FONT_SIZE_FLOOR)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the chat composer and question fields on the web', () => {
|
||||
it.each(KEYS)('takes its size from the seam, clear of the focus-zoom floor: %s', (key) => {
|
||||
expect(onWeb[key].fontSize).toBe(TEXT_INPUT_FONT_SIZE)
|
||||
expect(onWeb[key].fontSize).toBeGreaterThanOrEqual(IOS_FOCUS_ZOOM_FLOOR)
|
||||
expect(onWeb[key].fontSize).toBeGreaterThanOrEqual(TEXT_INPUT_FONT_SIZE_FLOOR)
|
||||
expect(onWeb[key].fontSize).toBeGreaterThan(BEFORE_THE_SPLIT[key].fontSize)
|
||||
})
|
||||
|
||||
|
||||
@@ -85,10 +85,6 @@
|
||||
"file": "src/browser/browser-screencast-request.web.ts",
|
||||
"reason": "Not an RN Web API gap but a transport one that exists only in the page: a screencast frame crosses the bridge as one message under BRIDGE_MAX_MESSAGE_BYTES, and a phone's mobile view at the native device scale factor produces a worst-case JPEG larger than that. This file budgets the mobile view's area against the cap, the envelope it measures rather than names, and one worst-case bytes-per-pixel constant. Web view mode is untouched, because a letterboxed desktop viewport is not an area the page can predict."
|
||||
},
|
||||
{
|
||||
"file": "src/components/custom-key-input-styles.web.ts",
|
||||
"reason": "The custom-key capture field renders one character at 22px, which already clears the 16px floor below which iOS zooms the page on focus \u2014 so unlike every other input on the session screen this sibling lowers the size rather than raising it. The text-input census reads the seam as a binding and not as a number, on purpose: a literal that happens to be large enough and a 14 somebody left behind are the same thing to a reader, and a per-site exemption is what the next 14 would inherit. Native keeps 22, where no page can zoom."
|
||||
},
|
||||
{
|
||||
"file": "src/session/mobile-native-chat-input-styles.web.ts",
|
||||
"reason": "The chat's composer and its question field render one point above the app's body size, which is 15 and under the floor below which iOS zooms the page on focus. keyboard-occlusion.web.ts reads a scale other than 1 as 'no keyboard' and answers 0, and on this screen that lift is the terminal's only feedback that its hidden input has focus, so one focus of the composer would cost the rest of the session. This file puts both fields on TEXT_INPUT_FONT_SIZE; the native sibling keeps 15."
|
||||
|
||||
Reference in New Issue
Block a user