diff --git a/src/app/components/editor/Toolbar.tsx b/src/app/components/editor/Toolbar.tsx index 7d701c42..3721b909 100644 --- a/src/app/components/editor/Toolbar.tsx +++ b/src/app/components/editor/Toolbar.tsx @@ -36,6 +36,7 @@ import { KeySymbol } from '../../utils/key-symbol'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { stopPropagation } from '../../utils/keyboard'; +import { PhoneHit, PhoneHitRoom36, PhoneHitSpread36 } from '../../styles/PhoneHit.css'; function BtnTooltip({ text, shortCode }: { text: string; shortCode?: string }) { return ( @@ -72,6 +73,7 @@ export function MarkButton({ format, icon, tooltip }: MarkButtonProps) { {(triggerRef) => ( {(triggerRef) => ( {(triggerRef) => ( handleMenuSelect(1)} size="400" @@ -178,6 +182,7 @@ export function HeadingBlockButton() { > {(triggerRef) => ( handleMenuSelect(2)} size="400" @@ -193,6 +198,7 @@ export function HeadingBlockButton() { > {(triggerRef) => ( handleMenuSelect(3)} size="400" @@ -208,6 +214,7 @@ export function HeadingBlockButton() { } > {(triggerRef) => ( {(triggerRef) => ( setIsMarkdown(!isMarkdown)} diff --git a/src/app/components/emoji-board/components/Sidebar.tsx b/src/app/components/emoji-board/components/Sidebar.tsx index de22b483..d2d5f197 100644 --- a/src/app/components/emoji-board/components/Sidebar.tsx +++ b/src/app/components/emoji-board/components/Sidebar.tsx @@ -14,6 +14,7 @@ import { } from 'folds'; import classNames from 'classnames'; import * as css from './styles.css'; +import { PhoneHit } from '../../../styles/PhoneHit.css'; export function Sidebar({ children }: { children: ReactNode }) { return ( @@ -68,6 +69,7 @@ function SidebarBtn({ > {(ref) => ( )} )} { + // The class each IconButton must carry, in source order. + const expected: Record = { + 'components/editor/Toolbar.tsx': [ + 'PhoneHitSpread36', // mark button: size 400 + 20 px icon, in a row 4 px apart + 'PhoneHitSpread36', // block button, same row + 'PhoneHitRoom36', // heading 1, inside the heading Menu (overflow hidden) + 'PhoneHitRoom36', // heading 2 + 'PhoneHitRoom36', // heading 3 + 'PhoneHit', // heading opener: two 20 px icons, 56 px wide, only its height is short + 'PhoneHit', // Exit chip: text label, wider than 44 px + 'PhoneHit', // markdown toggle, alone at the end of the toolbar + ], + // A column of 40 px buttons 4 px apart (the stack's gap="100", pinned below): there is no vertical variant, so only + // PhoneHit; the 44 px areas then touch with no overlap. + 'components/emoji-board/components/Sidebar.tsx': ['PhoneHit'], + // Both cards put a Retry chip beside the cancel button when the upload failed. + 'components/upload-card/UploadCardRenderer.tsx': ['PhoneHitSpread28'], + 'components/upload-card/CompactUploadCardRenderer.tsx': ['PhoneHitSpread28'], + 'pages/client/ClientRoot.tsx': ['PhoneHit'], // alone, absolute in the corner + }; + + const sizeAllow: SizeAllow = {}; + + it.each(Object.entries(expected))( + '%s: each IconButton carries its own PhoneHit class', + (file, want) => expectPhoneHitClasses(file, want) + ); + + it.each(Object.keys(expected))('%s: Spread and Room variants name the size they draw', (file) => + expectSizeTie(file, sizeAllow) + ); +}); + +/** The JSX element opened at `open` (index of its ``: nesting is counted, not guessed (no self-closing Box here). */ +const boxSpan = (src: string, open: number) => { + const marks = [...src.slice(open).matchAll(/])|<\/Box>/g)]; + const depths = marks.map((m, i) => + marks.slice(0, i + 1).reduce((d, x) => d + (x[0] === '' ? -1 : 1), 0) + ); + const end = depths.indexOf(0); + if (end === -1) throw new Error('unbalanced '); + return [open, open + (marks[end].index ?? 0) + marks[end][0].length] as const; +}; + +describe('the estate navigator find button', () => { + // The only IconButton there needs no PhoneHit: every button in the navigator header is 44 px on a phone because + // the Targets class sets a min size, and the button sits inside the Box that carries that class. + it('is 44 px by the Targets class instead', async () => { + const src = readSource('features/estate/EstateNavigator.tsx'); + const tags = iconButtonTags(src); + expect(tags).toHaveLength(1); + expect(tags[0]).not.toMatch(/PhoneHit/); + + const header = src.indexOf(''); + const boxStart = src.indexOf('', boxStart))).toMatch(/className=\{css\.Targets\}/); + const button = src.indexOf(' { + // PhoneHit makes the button position: relative on a phone; the inline style wins over a class, so the corner + // button stays absolute and its ::after anchors to it. + it('ClientRoot keeps its corner button absolutely positioned inline', () => { + const [tag] = iconButtonTags(readSource('pages/client/ClientRoot.tsx')); + expect(tag).toMatch(/style=\{\{[^}]*position:\s*'absolute'/); + }); + + // A 40 px button with a 44 px area, in a column 4 px apart: the areas touch and do not overlap. A smaller gap + // overlaps them, a larger one leaves a dead strip. + it('the emoji sidebar stack keeps its 4 px gap', () => { + const src = readSource('components/emoji-board/components/Sidebar.tsx'); + const stack = src.slice( + src.indexOf('export const SidebarStack'), + src.indexOf('export function SidebarDivider') + ); + expect(stack).toMatch(/direction="Column"/); + expect(stack).toMatch(/gap="100"/); + }); +});