fix(web): restore in-app browser / Reference Board styles dropped by #3358 (#3748)

* fix(web): restore in-app browser / Reference Board styles dropped by #3358

PR #3358 ("redesign Design Files panel", commit 640893756) rewrote
design-files.css and deleted the entire `.design-browser` / `.db-*` block
without touching DesignBrowserPanel.tsx, which still renders 65 of those
class names. The in-app browser tab (新建浏览器 → Reference Board) shipped
completely unstyled in the v0.10.0 beta: the chrome bar overlaps the page
heading and the reference cards lose all layout.

Nothing in CI caught it — there is no stylelint, jsdom unit tests never
apply CSS, and the browser panel is not in the visual baseline suite — so
deleting a global stylesheet block while leaving the component's className
strings was invisible to every existing check.

Restore the block verbatim from 640893756^ into its own
styles/workspace/design-browser.css (so a future design-files refactor
cannot silently drop it again) and wire it into index.css. Add a
tests/styles regression guard that fails when the module's structural
classes are missing or unimported — red on main, green here.

Verified: the standalone production build (the path the packaged beta uses)
now compiles 76 `.db-*` selectors into the CSS chunk; before this change it
emitted zero.

* test(web): track the whole browser panel in the design-browser style guard

Address review feedback on #3748: the coverage assertion hard-coded a few
`db-*` families, so a future stylesheet refactor could still drop rules for
the viewport switcher, browser-use menu, suggestions, or comment layer and
keep the guard green.

Now derive the used set from every `design-browser`/`db-*` token the
component renders, skipping only dynamic template-literal stems
(`db-viewport-${mode}`) and an explicit, documented allowlist of style-less
JS hook / value-span / modifier classes that never had a dedicated rule
(verified against the pre-#3358 stylesheet this file restores). Removing any
painted class's rule now turns the guard red.

---------

Co-authored-by: audit <a@b.c>
This commit is contained in:
lefarcen
2026-06-05 14:17:31 +00:00
committed by GitHub
co-authored by audit
parent 0582749df5
commit 4c85cacdf6
3 changed files with 1330 additions and 0 deletions
+1
View File
@@ -14,6 +14,7 @@
@import './styles/workspace/connectors.css';
@import './styles/workspace/drawer.css';
@import './styles/workspace/design-files.css';
@import './styles/workspace/design-browser.css';
@import './styles/viewer/core.css';
@import './styles/viewer/code.css';
@import './styles/viewer/tools.css';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,118 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
// Regression guard for the in-app browser / Reference Board panel
// (`DesignBrowserPanel.tsx`). PR #3358 ("redesign Design Files panel",
// commit 640893756) rewrote design-files.css and dropped the entire
// `.design-browser` / `.db-*` block without touching the component, which still
// references those class names. The panel shipped completely unstyled in the
// v0.10.0 beta. Nothing in CI caught it: there is no stylelint, jsdom unit
// tests never apply CSS, and the panel is not in the visual baseline suite.
// This test wires the component's class usage to its CSS so a future refactor
// cannot silently delete the module again.
const indexCss = readFileSync(new URL('../../src/index.css', import.meta.url), 'utf8');
const designBrowserCss = readFileSync(
new URL('../../src/styles/workspace/design-browser.css', import.meta.url),
'utf8',
);
const component = readFileSync(
new URL('../../src/components/DesignBrowserPanel.tsx', import.meta.url),
'utf8',
);
function definedClasses(css: string): Set<string> {
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
const defined = new Set<string>();
const rulePattern = /([^{}]+)\{[^}]*\}/g;
let match: RegExpExecArray | null;
while ((match = rulePattern.exec(withoutComments)) !== null) {
const selectorList = match[1] ?? '';
for (const token of selectorList.matchAll(/\.((?:design-browser|db)[\w-]*)/g)) {
defined.add(token[1]!);
}
}
return defined;
}
// Classes the panel renders purely as JS state hooks / value spans / modifiers
// that never had a dedicated CSS rule — verified against the pre-#3358
// stylesheet (640893756^), which design-browser.css restores verbatim. They are
// intentionally style-less, so the coverage check exempts them rather than
// demanding a rule that never existed. A class only belongs here if it carries
// no layout of its own; anything that paints or positions must stay covered.
const STYLELESS_HOOKS = new Set([
'db-action-browser-use',
'db-action-menu',
'db-action-primary',
'db-action-save',
'db-action-screenshot',
'db-inspect-bg',
'db-inspect-color',
'db-inspect-font-size',
'db-inspect-padding',
'db-inspect-radius',
'db-inspect-weight',
'db-viewport-height',
'db-viewport-icon',
'db-viewport-menu-label',
'db-viewport-width',
]);
describe('design browser panel styles', () => {
it('is imported into the global stylesheet so the panel ships styled', () => {
expect(indexCss).toContain("@import './styles/workspace/design-browser.css';");
});
it('defines every structural class the panel relies on for layout', () => {
// Layout-critical classes — their absence is what collapsed the panel in
// the beta (overlapping chrome, unstyled reference cards).
const structural = [
'design-browser',
'db-chrome',
'db-nav',
'db-actions',
'db-address-form',
'db-address-field',
'db-content',
'db-start',
'db-reference-board',
'db-reference-chip',
'db-reference-list',
'db-reference-card',
];
const defined = definedClasses(designBrowserCss);
const missing = structural.filter((cls) => !defined.has(cls));
expect(missing).toEqual([]);
});
it('gives the chrome bar and reference board real box rules', () => {
// Spot-check a couple of declarations so an empty stub file cannot satisfy
// the presence check above.
const chrome = /\.db-chrome\s*\{([^}]*)\}/.exec(designBrowserCss)?.[1] ?? '';
expect(chrome).toMatch(/grid-template-columns/);
const board = /\.db-reference-board\s*\{([^}]*)\}/.exec(designBrowserCss)?.[1] ?? '';
expect(board).toMatch(/(grid|flex|padding)/);
});
it('keeps every styled browser-panel class the component renders defined in the stylesheet', () => {
// The component is the source of truth: collect EVERY `design-browser`/`db-*`
// token it renders (not a hard-coded family subset) so dropping the rules for
// any part of the panel — viewport switcher, browser-use menu, suggestions,
// comment layer, etc. — turns this guard red.
const used = new Set<string>();
for (const token of component.matchAll(/\b(?:design-browser|db-[a-z][a-z0-9-]*)\b/g)) {
const cls = token[0];
// Dynamic class built from a template literal (e.g. `db-viewport-${mode}`):
// the word boundary truncates the stem just before the interpolated `-`,
// so the very next source char is `-`. Such classes can't be matched
// statically — skip the stem (its static siblings are still asserted).
if (cls.endsWith('-') || component[token.index + cls.length] === '-') continue;
if (STYLELESS_HOOKS.has(cls)) continue;
used.add(cls);
}
const defined = definedClasses(designBrowserCss);
const orphans = [...used].filter((cls) => !defined.has(cls)).sort();
expect(orphans).toEqual([]);
});
});