diff --git a/src/app/features/scene/sceneButtons.css.ts b/src/app/features/scene/sceneButtons.css.ts index 5c96e71b..9fca44f3 100644 --- a/src/app/features/scene/sceneButtons.css.ts +++ b/src/app/features/scene/sceneButtons.css.ts @@ -2,9 +2,14 @@ import { globalStyle } from '@vanilla-extract/css'; // Meta, 2026-11-10: "buttons are big and ugly, too, they don't need chrome either". Over the scene a folds Button or // Chip is its label in the page's ink, with a faint wash on hover and focus. Menus and dialogs (#portalContainer) keep -// folds' look. The selectors are folds' hashed recipe base classes (Button epr39zd, Chip _1bv8u1w6). +// folds' look. The selectors are folds' hashed recipe base classes (Button, Chip; pinned below). +// sceneButtons.test.ts resolves both from the installed folds and fails when a folds upgrade rehashes them. +export const FOLDS_BUTTON_CLASS = 'epr39zd'; +export const FOLDS_CHIP_CLASS = '_1bv8u1w6'; const scene = ':root[data-scene] #root'; -const controls = [`${scene} .epr39zd`, `${scene} ._1bv8u1w6`].join(', '); +const btn = `${scene} .${FOLDS_BUTTON_CLASS}`; +const chip = `${scene} .${FOLDS_CHIP_CLASS}`; +const controls = [btn, chip].join(', '); globalStyle(controls, { backgroundColor: 'transparent', boxShadow: 'none', @@ -12,20 +17,15 @@ globalStyle(controls, { color: 'inherit', }); globalStyle( - [ - `${scene} .epr39zd:hover`, - `${scene} .epr39zd:focus-visible`, - `${scene} ._1bv8u1w6:hover`, - `${scene} ._1bv8u1w6:focus-visible`, - ].join(', '), + [`${btn}:hover`, `${btn}:focus-visible`, `${chip}:hover`, `${chip}:focus-visible`].join(', '), { backgroundColor: 'color-mix(in srgb, currentColor 12%, transparent)' } ); // A visible keyboard focus ring in the control's own ink, on top of the hover wash. -globalStyle([`${scene} .epr39zd:focus-visible`, `${scene} ._1bv8u1w6:focus-visible`].join(', '), { +globalStyle([`${btn}:focus-visible`, `${chip}:focus-visible`].join(', '), { outline: '2px solid currentColor', outlineOffset: '2px', }); -globalStyle(`${scene} .epr39zd`, { +globalStyle(btn, { fontWeight: 700, textDecoration: 'underline', textUnderlineOffset: '0.25em', diff --git a/src/app/features/scene/sceneButtons.test.ts b/src/app/features/scene/sceneButtons.test.ts new file mode 100644 index 00000000..f3a2ff91 --- /dev/null +++ b/src/app/features/scene/sceneButtons.test.ts @@ -0,0 +1,30 @@ +import { readFileSync } from 'fs'; +import { createRequire } from 'module'; +import { dirname, join } from 'path'; +import { describe, expect, it } from 'vitest'; + +// sceneButtons.css.ts restyles folds' Button and Chip by their hashed recipe base classes. A folds upgrade rehashes +// them and the chromeless buttons would silently revert, so resolve the classes from the installed folds. +const foldsDir = dirname(createRequire(import.meta.url).resolve('folds/package.json')); +const dist = readFileSync(join(foldsDir, 'dist/index.js'), 'utf8'); +const css = readFileSync(join(foldsDir, 'dist/style.css'), 'utf8'); +const sceneButtons = readFileSync( + `${process.cwd()}/src/app/features/scene/sceneButtons.css.ts`, + 'utf8' +); + +const recipeBase = (name: string) => + new RegExp(`\\b${name}\\$1 = createRuntimeFn\\(\\{ defaultClassName: "([^" ]+)`).exec(dist)?.[1]; +const pinned = (name: string) => new RegExp(`${name} = '([^']+)'`).exec(sceneButtons)?.[1]; + +describe('sceneButtons pins folds recipe base classes', () => { + it.each([ + ['Button', 'FOLDS_BUTTON_CLASS'], + ['Chip', 'FOLDS_CHIP_CLASS'], + ])('%s base class matches the installed folds', (recipe, constant) => { + const base = recipeBase(recipe); + expect(base, `folds no longer builds ${recipe} with createRuntimeFn`).toBeTruthy(); + expect(pinned(constant)).toBe(base); + expect(css).toContain(`.${base} {`); + }); +});