The macOS window's draggable area was a hand-written band: a 52px strip, deepened to 76px by a marker the Conversation header had to keep in step with its own height. It drifted from the layout — blank runs inside a chrome row did not drag, and a control a few pixels away dragged the window instead. Chrome rows now mark their own element (`data-window-drag`) and the shell declares the surface once (`html[data-platform='darwin'] [data-window-drag]`), so a row's own box is its draggable geometry and no fixed band has to match any row's height. The frame band, its 52/76 figures, the marker, and the per-sheet drag rules are gone; interactive controls and overlays portal beside `#root` where one rule subtracts them. Recollection becomes one shell watcher (`window-drag/recall.ts`): Electron rebuilds the window's drag rects only when a computed app-region value changes (electron#32341), so the watcher measures every marked row each frame while the surface moves, sets `data-window-drag-recall` for the frames whose geometry changed, and clears it on the frame that finds the geometry settled. The right panel's hand-written pulse is deleted with it. Coverage is a contract with executable parts. `window-drag/regions.ts` publishes the composition rule and the interactive selector that both the sheet and its spec compare against; the ui-theme gate asserts the package tree declares drag in exactly two places and pairs every row's mark with its sheet, selector, and authored height; and the browser lane boots the shipped composition in Chromium with the darwin marker to assert per-row geometry, a content body under a marked head row, a covering overlay, and the watcher's pulse. The plugin manager's detail views mark the head row they share rather than the pane they render in, so their text and form labels stay content. Both real machines agree: the macOS matrix ran on a real `hiddenInset` window with HID-level pointer input and window-frame readback, and the Windows matrix on a Windows 11 ARM64 VM — chrome rows drag, content and the covering overlay do not, and the controls keep their clicks. Refs #4619
102 lines
3.8 KiB
TypeScript
102 lines
3.8 KiB
TypeScript
/**
|
|
* Shared helpers for stylesheet-contract specs: flatten CSS text on disk into
|
|
* rules and enumerate the package stylesheets those contracts range over.
|
|
*/
|
|
import { readdirSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
/** One flattened CSS rule: its comma-separated selector parts and its declarations in source order. */
|
|
export interface CssRule {
|
|
selectors: string[]
|
|
declarations: [property: string, value: string][]
|
|
}
|
|
|
|
/** Root the package-wide stylesheet scans walk. */
|
|
const PACKAGES_DIR = fileURLToPath(new URL('../../../', import.meta.url))
|
|
|
|
/**
|
|
* Flatten a stylesheet into rules. Whitespace, declaration order, and trailing
|
|
* semicolons are normalized away; nesting is not handled, which no sheet under
|
|
* test uses, and at-rule preludes surface as selector-less rule boundaries.
|
|
* @param css - stylesheet text.
|
|
* @returns one entry per rule, in source order.
|
|
*/
|
|
export function parseRules(css: string): CssRule[] {
|
|
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, ' ')
|
|
const rules: CssRule[] = []
|
|
// Destructuring defaults only satisfy noUncheckedIndexedAccess; both groups
|
|
// are unconditional in the pattern.
|
|
for (const [, selector = '', body = ''] of withoutComments.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
|
|
const declarations = body
|
|
.split(';')
|
|
.map(part => part.trim())
|
|
.filter(part => part.includes(':'))
|
|
.map((part): [string, string] => {
|
|
const colon = part.indexOf(':')
|
|
return [part.slice(0, colon).trim(), part.slice(colon + 1).trim()]
|
|
})
|
|
rules.push({ selectors: selector.split(',').map(part => part.trim()), declarations })
|
|
}
|
|
return rules
|
|
}
|
|
|
|
/**
|
|
* Half-open source span of one at-rule's block, excluding its prelude.
|
|
* @param css - stylesheet text.
|
|
* @param prelude - exact at-rule prelude to locate, without the opening brace.
|
|
* @returns the block's brace offsets, or undefined when the prelude is absent.
|
|
*/
|
|
export function atRuleBlock(css: string, prelude: string): { start: number; end: number } | undefined {
|
|
const opening = css.indexOf(`${prelude} {`)
|
|
if (opening === -1) return undefined
|
|
const start = css.indexOf('{', opening)
|
|
let depth = 0
|
|
for (let index = start; index < css.length; index += 1) {
|
|
if (css[index] === '{') depth += 1
|
|
else if (css[index] === '}') {
|
|
depth -= 1
|
|
if (depth === 0) return { start, end: index }
|
|
}
|
|
}
|
|
throw new Error(`unbalanced braces after ${prelude}`)
|
|
}
|
|
|
|
/**
|
|
* Custom-property names a value reads.
|
|
* @param value - declaration value, possibly with nested var() calls.
|
|
* @returns every referenced custom-property name, in source order.
|
|
*/
|
|
export function varReferences(value: string): string[] {
|
|
return [...value.matchAll(/var\(\s*(--[\w-]+)/g)].map(([, name = '']) => name)
|
|
}
|
|
|
|
/**
|
|
* Every file shipped as package source whose name the predicate accepts,
|
|
* excluding build output and installed dependencies.
|
|
* @param accepts - filename predicate.
|
|
* @returns absolute paths under packages/, `/`-separated on every platform so
|
|
* specs can match repo-relative suffixes verbatim.
|
|
*/
|
|
export function packageFiles(accepts: (name: string) => boolean): string[] {
|
|
const found: string[] = []
|
|
const walk = (dir: string): void => {
|
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
const path = join(dir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
if (entry.name !== 'node_modules' && entry.name !== 'lib' && entry.name !== 'dist') walk(path)
|
|
} else if (accepts(entry.name)) found.push(path.replaceAll('\\', '/'))
|
|
}
|
|
}
|
|
walk(PACKAGES_DIR)
|
|
return found
|
|
}
|
|
|
|
/**
|
|
* Every CSS file shipped as package source.
|
|
* @returns absolute stylesheet paths, `/`-separated on every platform.
|
|
*/
|
|
export function packageStylesheets(): string[] {
|
|
return packageFiles(name => name.endsWith('.css'))
|
|
}
|