Phone glyphs: bigger icons in IconButtons and nav chevrons on phones #144
4 changed files with 101 additions and 0 deletions
|
|
@ -3,6 +3,7 @@ import { config } from 'folds';
|
|||
import { surfaceInk } from '../scene/sceneShell.css';
|
||||
import { skyLabelInk } from '../scene/skyLabelInk';
|
||||
import { NAV_ICON_REST_OPACITY } from '../../components/nav/styles.css';
|
||||
import { PHONE_GLYPHS } from '../../../phoneGlyph.css';
|
||||
|
||||
export const CategoryButton = style({
|
||||
flexGrow: 1,
|
||||
|
|
@ -32,4 +33,11 @@ export const CategoryButton = style({
|
|||
});
|
||||
export const CategoryButtonIcon = style({
|
||||
opacity: NAV_ICON_REST_OPACITY,
|
||||
// A phone draws the 16 px chevron as its IconButton neighbours are drawn (see phoneGlyph.css.ts): paint-time, so
|
||||
// the gutter and the row keep their layout.
|
||||
'@media': {
|
||||
'screen and (max-width: 750px)': {
|
||||
scale: `${PHONE_GLYPHS['50'].phonePx / PHONE_GLYPHS['50'].px}`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
64
src/app/phoneGlyph.test.ts
Normal file
64
src/app/phoneGlyph.test.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { readFileSync } from 'fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MOBILE_BREAKPOINT } from './hooks/useScreenSize';
|
||||
import { emitted } from './phoneHitTestKit';
|
||||
import { ICON_BUTTON_CLASS, PHONE_GLYPHS } from '../phoneGlyph.css';
|
||||
|
||||
const foldsJs = readFileSync(`${process.cwd()}/node_modules/folds/dist/index.js`, 'utf8');
|
||||
const foldsCss = readFileSync(`${process.cwd()}/node_modules/folds/dist/style.css`, 'utf8');
|
||||
|
||||
/** The px folds draws an Icon class at: its width variable's rem value, at the 16 px root. */
|
||||
const foldsPx = (cls: string) => {
|
||||
const v = foldsCss.match(new RegExp(`\\.${cls} \\{\\s*width: var\\((--[\\w-]+)\\)`))?.[1];
|
||||
const rem = v && foldsCss.match(new RegExp(`${v}: ([\\d.]+)rem`))?.[1];
|
||||
return rem ? Number(rem) * 16 : undefined;
|
||||
};
|
||||
|
||||
describe('phone glyphs inside IconButtons', () => {
|
||||
it("name folds' own classes: the IconButton base class and one Icon class per size", () => {
|
||||
expect(foldsJs).toContain(
|
||||
`var IconButton$1 = createRuntimeFn({ defaultClassName: "${ICON_BUTTON_CLASS} `
|
||||
);
|
||||
const iconSizes = foldsJs.match(
|
||||
/var Icon\$1 = createRuntimeFn\(\{[^\n]*variantClassNames: \{ size: \{([^}]*)\}/
|
||||
)?.[1];
|
||||
expect(iconSizes).toBeDefined();
|
||||
Object.entries(PHONE_GLYPHS).forEach(([size, { cls }]) =>
|
||||
expect(iconSizes).toContain(`"${size}": "${cls}"`)
|
||||
);
|
||||
});
|
||||
|
||||
it("start from the px folds draws each Icon size at (the scale's divisor is not a guess)", () =>
|
||||
Object.values(PHONE_GLYPHS).forEach(({ cls, px }) => expect(foldsPx(cls)).toBe(px)));
|
||||
|
||||
it('draw the folds Icon px (16 18 20 22 24) at 22..28 px, never smaller, never past the smallest button box', () => {
|
||||
// folds IconButton padding is 4 px a side at the smallest size, so its box is the glyph plus 8.
|
||||
Object.values(PHONE_GLYPHS).forEach(({ px, phonePx }) => {
|
||||
expect(phonePx).toBeGreaterThanOrEqual(22);
|
||||
expect(phonePx).toBeLessThanOrEqual(28);
|
||||
expect(phonePx).toBeGreaterThan(px);
|
||||
expect(phonePx).toBeLessThanOrEqual(px + 8);
|
||||
});
|
||||
});
|
||||
|
||||
it('are a paint-time scale on phones only: no size, margin or padding, and no rule outside the phone query', async () => {
|
||||
const css = await emitted('../phoneGlyph.css.ts');
|
||||
const phone = `@media screen and (max-width: ${MOBILE_BREAKPOINT}px)`;
|
||||
expect(
|
||||
css.replace(new RegExp(`${phone.replace(/[()]/g, '\\$&')} \\{\\n[\\s\\S]*?\\n\\}`, 'g'), '')
|
||||
).not.toMatch(/\.brs7ce5/);
|
||||
Object.entries(PHONE_GLYPHS).forEach(([size, { cls, px, phonePx }]) => {
|
||||
const rule = css.match(
|
||||
new RegExp(`\\.${ICON_BUTTON_CLASS} > \\.${cls} \\{\\n([^}]*)\\}`)
|
||||
)?.[1];
|
||||
expect(rule, `rule for Icon size ${size}`).toBeDefined();
|
||||
expect(
|
||||
rule!
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((l) => l.trim().split(':')[0])
|
||||
).toEqual(['scale']);
|
||||
expect(Number(rule!.match(/scale: ([\d.]+)/)![1])).toBeCloseTo(phonePx / px, 6);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -14,6 +14,7 @@ import '@fontsource/montserrat/200.css';
|
|||
import '@fontsource/montserrat/300.css';
|
||||
import '@fontsource/ibm-plex-mono/400.css';
|
||||
import 'folds/dist/style.css';
|
||||
import './phoneGlyph.css';
|
||||
import { configClass, varsClass } from 'folds';
|
||||
|
||||
enableMapSet();
|
||||
|
|
|
|||
28
src/phoneGlyph.css.ts
Normal file
28
src/phoneGlyph.css.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { globalStyle } from '@vanilla-extract/css';
|
||||
import { MOBILE_BREAKPOINT } from './app/hooks/useScreenSize';
|
||||
|
||||
// Phone glyph size. On a phone the glyph inside an IconButton is drawn larger, by `scale` (a paint-time transform:
|
||||
// the button's box, its PhoneHit area and every row of buttons keep the layout they have, so nothing reflows at
|
||||
// 320 px). folds draws a glyph at one of its Icon sizes and the IconButton's box is padding plus that glyph, so the
|
||||
// factor is chosen per Icon size. Desktop is not touched.
|
||||
//
|
||||
// The class names are folds 2.7.2's own (the Icon size classes and the IconButton base class); phoneGlyph.test.ts
|
||||
// reads them back from folds' dist, so a folds upgrade that renames them fails there instead of silently here.
|
||||
export const ICON_BUTTON_CLASS = 'brs7ce5';
|
||||
/** folds Icon size -> [its class, its px, the px it is drawn at on a phone]. The 24 px glyph is the folds default and
|
||||
* reads small in a 40 px button, so it grows too; 28 px and up are already large. */
|
||||
export const PHONE_GLYPHS = {
|
||||
'50': { cls: 'cpipac7', px: 16, phonePx: 22 },
|
||||
'100': { cls: 'cpipac8', px: 18, phonePx: 24 },
|
||||
'200': { cls: 'cpipac9', px: 20, phonePx: 24 },
|
||||
'300': { cls: 'cpipaca', px: 22, phonePx: 26 },
|
||||
'400': { cls: 'cpipacb', px: 24, phonePx: 28 },
|
||||
} as const;
|
||||
|
||||
const PHONE = `screen and (max-width: ${MOBILE_BREAKPOINT}px)`;
|
||||
|
||||
Object.values(PHONE_GLYPHS).forEach(({ cls, px, phonePx }) =>
|
||||
globalStyle(`.${ICON_BUTTON_CLASS} > .${cls}`, {
|
||||
'@media': { [PHONE]: { scale: `${phonePx / px}` } },
|
||||
})
|
||||
);
|
||||
Loading…
Reference in a new issue