140 lines
11 KiB
JavaScript
140 lines
11 KiB
JavaScript
// Recover pure panel geometry and source motion CSS from fixed REA artifacts only.
|
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
import { createHash } from 'node:crypto'
|
|
import { resolve } from 'node:path'
|
|
import { parse } from '@babel/parser'
|
|
import postcss from 'postcss'
|
|
|
|
const runtimeEvidenceId = 'ev_717fa5e846e64d67ac19c8925d121bff632c0d3aefb3603b10858559cabb2dcb'
|
|
const cssEvidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
|
|
const runtimeFile = 'contexts-BMeWSUHk.js'
|
|
const runtimeSha256 = '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b'
|
|
const motionFile = 'desktop-sdk-setup-Bh0rg_xP.css'
|
|
const motionSha256 = 'b2ee58c0caf560eeddc4eb8c91d86cabddbec634b8d37ed5d6b85d0644c0e142'
|
|
const tokenFile = 'tokens-Df-i-zsO.css'
|
|
const tokenSha256 = '13f2bf82c96e5167a7d7098e36950c6c22c475b3337dbb8bb29ad2fe79cde696'
|
|
const hash = text => createHash('sha256').update(text).digest('hex')
|
|
const runtime = JSON.parse(await readFile(resolve('.tmp/rea-runtime-scripts.json'), 'utf8'))
|
|
if (runtime.evidence_id !== runtimeEvidenceId) throw new Error('Fixed runtime Evidence changed')
|
|
const matches = runtime.items.filter(item => item.file_path?.replaceAll('\\', '/').endsWith(`/renderer/assets/${runtimeFile}`))
|
|
if (matches.length !== 1) throw new Error('Expected exactly one panel layout runtime source')
|
|
const artifact = matches[0].source?.artifact
|
|
if (!matches[0].source.included || !artifact?.text || hash(artifact.text) !== runtimeSha256 || artifact.sha256 !== runtimeSha256) throw new Error('Fixed panel runtime SHA-256 mismatch')
|
|
const source = artifact.text
|
|
const ast = parse(source, { sourceType: 'module' })
|
|
const constantNames = ['Rw', 'zw', 'Bw', 'Vw', 'JQe', 'f$e', 'p$e']
|
|
const definitions = {
|
|
normalizeInset: { parameters: 'e: number', result: 'number' },
|
|
clampSidebarWidth: { parameters: 'e: number', result: 'number' },
|
|
resolveSidebarDragAction: { parameters: 'e: number, t: boolean', result: "'expand' | 'keep-collapsed' | 'collapse' | 'resize'" },
|
|
resolveChatAreaAvailableWidth: { parameters: 'e: PanelAvailableWidthInput', result: 'number' },
|
|
resolveDetailDrawerWidth: { parameters: 'e: DetailDrawerWidthInput', result: 'number' },
|
|
resolveLeadingInsetRebalanceWidth: { parameters: 'e: LeadingInsetRebalanceInput', result: 'number | undefined' },
|
|
resolveDetailPanelLayoutWidths: { parameters: 'e: DetailPanelLayoutInput', result: 'DetailPanelLayoutWidths | undefined' },
|
|
resolveRememberedDetailWidth: { parameters: 'e: readonly (number | null | undefined)[], t: number', implementationParameters: 'e: readonly any[], t: number', result: 'number' },
|
|
resolveChatAreaWidth: { parameters: 'e: number, t: number', result: 'number' },
|
|
getNarrowState: { parameters: 'e: number, t: number = 0', result: 'NarrowLayoutState' },
|
|
stickyNarrow: { parameters: 'e: boolean, t: number, n: number', result: 'boolean' },
|
|
resolveNarrowState: { parameters: 'e: NarrowLayoutState | null | undefined, t: number, n: number = 0', result: 'NarrowLayoutState' }
|
|
}
|
|
const functions = new Map(), constants = new Map()
|
|
function walk(node) {
|
|
if (!node || typeof node !== 'object') return
|
|
if (node.type === 'FunctionDeclaration' && definitions[node.id?.name]) {
|
|
if (functions.has(node.id.name)) throw new Error(`Duplicate source function ${node.id.name}`)
|
|
functions.set(node.id.name, node)
|
|
}
|
|
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' && constantNames.includes(node.left.name)
|
|
&& (node.right.type === 'ObjectExpression' || (['f$e', 'p$e'].includes(node.left.name) && node.right.type === 'BinaryExpression'))) {
|
|
if (constants.has(node.left.name)) throw new Error(`Duplicate panel constant ${node.left.name}`)
|
|
constants.set(node.left.name, node.right)
|
|
}
|
|
for (const [key, value] of Object.entries(node)) {
|
|
if (key === 'loc' || key === 'extra') continue
|
|
if (Array.isArray(value)) value.forEach(walk)
|
|
else if (value && typeof value === 'object') walk(value)
|
|
}
|
|
}
|
|
walk(ast)
|
|
for (const name of Object.keys(definitions)) if (!functions.has(name)) throw new Error(`Missing source function ${name}`)
|
|
for (const name of constantNames) if (!constants.has(name)) throw new Error(`Missing source constant ${name}`)
|
|
|
|
const cssDirectory = resolve('.tmp/rea-css-recovered')
|
|
const manifest = JSON.parse(await readFile(resolve(cssDirectory, 'manifest.json'), 'utf8'))
|
|
if (manifest.sourceEvidenceId !== cssEvidenceId) throw new Error('Fixed CSS Evidence changed')
|
|
async function stylesheet(file, expected) {
|
|
const content = await readFile(resolve(cssDirectory, file), 'utf8')
|
|
if (manifest.files.find(item => item.file === file)?.sha256 !== expected || hash(content) !== expected) throw new Error(`Fixed ${file} SHA-256 mismatch`)
|
|
return postcss.parse(content, { from: file })
|
|
}
|
|
const motion = await stylesheet(motionFile, motionSha256)
|
|
const tokens = await stylesheet(tokenFile, tokenSha256)
|
|
const tokenNames = ['--wb-motion-duration-medium', '--wb-motion-duration-fast', '--wb-motion-easing-decelerate', '--wb-motion-easing-accelerate']
|
|
const tokenValues = new Map()
|
|
tokens.walkDecls(decl => { if (tokenNames.includes(decl.prop) && decl.parent.selectors?.includes(':root')) tokenValues.set(decl.prop, decl.value) })
|
|
for (const name of tokenNames) if (!tokenValues.has(name)) throw new Error(`Missing motion token ${name}`)
|
|
const outputCss = postcss.root()
|
|
outputCss.append(postcss.rule({ selector: '.source-detail-layout', nodes: tokenNames.map(name => postcss.decl({ prop: name, value: tokenValues.get(name) })) }))
|
|
let rules = 0
|
|
const selected = selector => /\.detail-panel-motion-[\w-]+|\.detail-panel-container(?:[\s.:#[]|$)/.test(selector)
|
|
function scope(selector) {
|
|
if (selector.startsWith('.detail-panel-motion-')) return `.source-detail-layout${selector}`
|
|
return `.source-detail-layout ${selector}`
|
|
}
|
|
function project(node) {
|
|
if (node.type === 'rule') {
|
|
const selectors = node.selectors.filter(selected)
|
|
if (!selectors.length) return null
|
|
rules++
|
|
return node.clone({ selector: selectors.map(scope).join(',') })
|
|
}
|
|
if (node.type === 'atrule' && node.nodes && !/keyframes$/.test(node.name)) {
|
|
const children = node.nodes.map(project).filter(Boolean)
|
|
return children.length ? node.clone({ nodes: children }) : null
|
|
}
|
|
return null
|
|
}
|
|
for (const node of motion.nodes) { const projected = project(node); if (projected) outputCss.append(projected) }
|
|
|
|
const bodies = Object.fromEntries([...functions].map(([name, fn]) => [name, { offset: fn.start, bodySha256: hash(source.slice(fn.body.start, fn.body.end)) }]))
|
|
const identities = {
|
|
version: '5.8.2', runtimeEvidenceId, runtimeFile, runtimeSha256, cssEvidenceId, motionFile, motionSha256, tokenFile, tokenSha256,
|
|
functions: bodies, constants: Object.fromEntries([...constants].map(([name, node]) => [name, hash(source.slice(node.start, node.end))]))
|
|
}
|
|
const interfaces = `export interface PanelAvailableWidthInput { containerWidth: number; sidebarWidth: number; leadingInset?: number }
|
|
export interface DetailDrawerWidthInput extends PanelAvailableWidthInput { isMobile?: boolean; preferredWidth?: number }
|
|
export interface DetailPanelLayoutInput extends PanelAvailableWidthInput { requestedDetailWidth: number }
|
|
export interface DetailPanelLayoutWidths { mainWidth: number; detailWidth: number }
|
|
export interface LeadingInsetRebalanceInput { previousInset: number; nextInset: number; isNarrowForDetail: boolean; isPanelVisible: boolean; rememberedDetailWidth: number | null; chatAreaWidth: number }
|
|
export interface NarrowLayoutState { isMobile: boolean; isNarrowForDetail: boolean; isNarrowForSidebar: boolean; screenWidth: number }
|
|
`
|
|
const constantText = constantNames.map(name => `export const ${name} = ${source.slice(constants.get(name).start, constants.get(name).end)}${constants.get(name).type === 'ObjectExpression' ? ' as const' : ''}`).join('\n')
|
|
const functionText = Object.entries(definitions).map(([name, spec]) => {
|
|
const fn = functions.get(name)
|
|
const overload = spec.implementationParameters ? `/** Preserve source JavaScript null-to-number comparisons behind the typed public overload. */\nexport function ${name}(${spec.parameters}): ${spec.result}\n` : ''
|
|
return `${overload}export function ${name}(${spec.implementationParameters ?? spec.parameters}): ${spec.result} ${source.slice(fn.body.start, fn.body.end)}`
|
|
}).join('\n\n')
|
|
const toMilliseconds = value => value.endsWith('ms') ? Number.parseFloat(value) : Number.parseFloat(value) * 1000
|
|
const motionConfig = { openMs: toMilliseconds(tokenValues.get('--wb-motion-duration-medium')), closeMs: toMilliseconds(tokenValues.get('--wb-motion-duration-fast')),
|
|
openEasing: tokenValues.get('--wb-motion-easing-decelerate'), closeEasing: tokenValues.get('--wb-motion-easing-accelerate') }
|
|
const typescript = `/* Generated by scripts/rea/recover-panel-layout.mjs from fixed REA Evidence.
|
|
* Source function bodies and constant object expressions are unchanged; only exports/types are added.
|
|
* The source runtime leading-inset store is replaced by the explicit zero-default adapter below.
|
|
* Callers with a registered leading inset must pass leadingInset. Inputs otherwise retain source behavior.
|
|
*/\n${interfaces}\nexport const SOURCE_PANEL_LAYOUT_EVIDENCE = ${JSON.stringify(identities, null, 2)} as const\n\n${constantText}\n\nexport const SIDEBAR_LAYOUT = Rw\nexport const CHAT_LAYOUT = zw\nexport const DETAIL_BREAKPOINT = Bw\nexport const DETAIL_PANEL_LAYOUT = Vw\nexport const DETAIL_NARROW_WIDTH = f$e\nexport const SIDEBAR_NARROW_WIDTH = p$e\nexport const DETAIL_PANEL_MOTION = ${JSON.stringify(motionConfig, null, 2)} as const\n\n/** Host adapter: the source getter returns the currently registered inset; local default is zero. */\nfunction getMainContentLeadingInset(): number { return 0 }\n\n${functionText}\n`
|
|
const css = `/* Generated by scripts/rea/recover-panel-layout.mjs; WorkBuddy 5.8.2.
|
|
* REA CSS ${cssEvidenceId}; ${motionFile} sha256=${motionSha256}.
|
|
* Motion tokens ${tokenFile} sha256=${tokenSha256}.
|
|
* ${rules} selected rules, original bodies/order and all enclosing media conditions preserved.
|
|
* .source-detail-layout carries motion phase classes on the same element.
|
|
*/\n${outputCss.toString()}\n`
|
|
postcss.parse(css)
|
|
const targetDirectory = resolve('src/renderer/src/workbuddy/workbench')
|
|
for (const [file, contents] of [['source-panel-layout.ts', typescript], ['panel-motion-source.css', css]]) {
|
|
const target = resolve(targetDirectory, file)
|
|
if (process.argv.includes('--check')) {
|
|
if (await readFile(target, 'utf8') !== contents) throw new Error(`${file} differs from the fixed source recovery`)
|
|
} else await writeFile(target, contents)
|
|
}
|
|
console.log(JSON.stringify({ runtimeEvidenceId, cssEvidenceId, functions: functions.size, constants: constants.size, rules, motion: motionConfig, check: process.argv.includes('--check') }))
|