chore: establish YCode replication baseline and TODO
This commit is contained in:
commit
6dc59f89ba
1118 files changed
+196002
No files matched your search
@@ -0,0 +1,307 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { dirname, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import postcss from 'postcss'
|
||||
|
||||
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..')
|
||||
const exportDir = resolve(rootDir, '.tmp/rea-css-recovered')
|
||||
const outputPath = resolve(rootDir, 'src/renderer/src/workbuddy/composer/composer-source.css')
|
||||
const evidenceId = 'ev_377c8790468b3c67dad8577aabb327f98d3140659f0fffdbabfcff5a40cc9e94'
|
||||
|
||||
const sourceSpecs = [
|
||||
{
|
||||
file: 'src-CyXHA0IC.css',
|
||||
selectors: [
|
||||
'.cr-input-box',
|
||||
'.cr-input-box__main',
|
||||
'.cr-input-container',
|
||||
'.cr-input-editor-host',
|
||||
'.cr-input-toolbar',
|
||||
'.cr-input-toolbar__left',
|
||||
'.cr-input-toolbar__right',
|
||||
'.cr-input-toolbar__send',
|
||||
'.cr-input-footer-item',
|
||||
'.cr-input-footer-item:hover:not(:disabled)',
|
||||
'.cr-input-footer-item.cr-input-footer-item--open:not(:disabled)',
|
||||
'.cr-input-footer-item.cr-input-footer-item--open:not(:disabled) .cr-input-footer-item__label',
|
||||
'.cr-input-footer-item.cr-input-footer-item--open:not(:disabled) .cr-input-footer-item__chevron',
|
||||
'.cr-input-footer-item:focus-visible',
|
||||
'.cr-input-footer-item__icon',
|
||||
'.cr-input-footer-item__label',
|
||||
'.cr-input-footer-item__label:after',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item .cr-input-footer-item__icon',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item .cr-input-footer-item__label',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item .cr-input-footer-item__chevron',
|
||||
'.cr-permission-setting--danger:hover.cr-input-footer-item',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item',
|
||||
'.cr-permission-setting--danger:hover.cr-input-footer-item .cr-input-footer-item__icon',
|
||||
'.cr-permission-setting--danger:hover.cr-input-footer-item .cr-input-footer-item__label',
|
||||
'.cr-permission-setting--danger:hover.cr-input-footer-item .cr-input-footer-item__chevron',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item .cr-input-footer-item__icon',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item .cr-input-footer-item__label',
|
||||
'.cr-permission-setting--danger.cr-input-footer-item--open.cr-input-footer-item .cr-input-footer-item__chevron',
|
||||
'.cr-input-toolbar__left .cr-permission-setting.cr-input-footer-item',
|
||||
'.cr-input-toolbar__left .cr-permission-setting.cr-input-footer-item:hover:not(:disabled)',
|
||||
'.cr-input-toolbar__left .cr-permission-setting.cr-input-footer-item.cr-input-footer-item--open',
|
||||
'.cr-send-button',
|
||||
'.cr-send-button:hover:not(:disabled)',
|
||||
'.cr-send-button:active:not(:disabled)',
|
||||
'.cr-send-button:disabled',
|
||||
'.cr-send-button--sending',
|
||||
'.cr-send-button--stop',
|
||||
'.cr-send-button--custom',
|
||||
'.cr-send-button--custom:hover:not(:disabled)',
|
||||
'.cr-send-button__icon',
|
||||
'.cr-send-button__icon>svg',
|
||||
'.cr-voice-trigger',
|
||||
'.cr-voice-trigger:hover:not(:disabled)',
|
||||
'.cr-voice-trigger--recording',
|
||||
'.cr-voice-trigger--recording:hover:not(:disabled)',
|
||||
'.cr-voice-trigger--loading',
|
||||
'.cr-voice-trigger>svg'
|
||||
]
|
||||
},
|
||||
{
|
||||
file: 'contexts-BkjwYSZT.css',
|
||||
selectors: [
|
||||
'.conversation-input-area',
|
||||
'.conversation-input-area__custom-floating',
|
||||
'.conversation-input-area__input-stack',
|
||||
'.conversation-input .cr-input-box',
|
||||
'.conversation-input__box .cr-input-box__main',
|
||||
'.conversation-input__box .cr-input-container',
|
||||
'.conversation-input__box .cr-permission-setting:not(.cr-permission-setting--danger):not(:disabled)',
|
||||
'.conversation-input__box .cr-permission-setting:not(.cr-permission-setting--danger):not(:disabled) .cr-input-footer-item__label'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
function normalizeSelector(selector) {
|
||||
return selector.trim().replace(/\s*([>+~])\s*/g, '$1').replace(/\s+/g, ' ')
|
||||
}
|
||||
|
||||
function sourceHash(bytes) {
|
||||
return createHash('sha256').update(bytes).digest('hex')
|
||||
}
|
||||
|
||||
function formatRule(selector, declarations, source, sha256, note) {
|
||||
const comment = `/* REA ${source.file}:1:${source.column}; sha256 ${sha256}${note ? `; ${note}` : ''} */`
|
||||
const body = declarations.map((declaration) => ` ${declaration}`).join('\n')
|
||||
return `${comment}\n${selector} {\n${body}\n}`
|
||||
}
|
||||
|
||||
function declarationText(rule) {
|
||||
return rule.nodes
|
||||
.filter((node) => node.type === 'decl')
|
||||
.map((node) => `${node.prop}: ${node.value}${node.important ? ' !important' : ''};`)
|
||||
}
|
||||
|
||||
function collectRules(root, source, selectors) {
|
||||
const allowed = new Set(selectors.map(normalizeSelector))
|
||||
const records = []
|
||||
root.walkRules((rule) => {
|
||||
const selected = postcss.list.comma(rule.selector)
|
||||
.map(normalizeSelector)
|
||||
.filter((selector) => allowed.has(selector))
|
||||
if (selected.length === 0) return
|
||||
|
||||
const declarations = declarationText(rule)
|
||||
if (declarations.length === 0) return
|
||||
records.push({
|
||||
source,
|
||||
column: rule.source?.start?.column ?? 1,
|
||||
selectors: selected,
|
||||
declarations
|
||||
})
|
||||
})
|
||||
return records
|
||||
}
|
||||
|
||||
function findRule(root, selector) {
|
||||
const expected = normalizeSelector(selector)
|
||||
let match
|
||||
root.walkRules((rule) => {
|
||||
if (match) return
|
||||
if (postcss.list.comma(rule.selector).some((item) => normalizeSelector(item) === expected)) match = rule
|
||||
})
|
||||
if (!match) throw new Error(`REA stylesheet is missing required selector: ${selector}`)
|
||||
return match
|
||||
}
|
||||
|
||||
function declaration(rule, property) {
|
||||
const match = rule.nodes.find((node) => node.type === 'decl' && node.prop === property)
|
||||
if (!match) throw new Error(`REA rule ${rule.selector} is missing ${property}`)
|
||||
return match.value
|
||||
}
|
||||
|
||||
function resolveToken(token, tokens, seen = new Set()) {
|
||||
if (seen.has(token)) throw new Error(`Cyclic REA token reference: ${token}`)
|
||||
const value = tokens.get(token)
|
||||
if (!value) throw new Error(`REA token is missing: ${token}`)
|
||||
const simpleVar = /^var\((--[\w-]+)\)$/.exec(value.trim())
|
||||
if (!simpleVar) return value.trim()
|
||||
seen.add(token)
|
||||
return resolveToken(simpleVar[1], tokens, seen)
|
||||
}
|
||||
|
||||
const manifestBytes = await readFile(resolve(exportDir, 'manifest.json'))
|
||||
const manifest = JSON.parse(manifestBytes.toString('utf8'))
|
||||
if (manifest.sourceEvidenceId !== evidenceId) {
|
||||
throw new Error(`REA CSS evidence changed: expected ${evidenceId}, found ${manifest.sourceEvidenceId ?? 'none'}`)
|
||||
}
|
||||
|
||||
const sources = new Map()
|
||||
for (const spec of sourceSpecs) {
|
||||
const bytes = await readFile(resolve(exportDir, spec.file))
|
||||
const record = manifest.files?.find((file) => file.file === spec.file)
|
||||
if (!record?.sha256) throw new Error(`REA manifest has no SHA-256 for ${spec.file}`)
|
||||
const actualHash = sourceHash(bytes)
|
||||
if (actualHash !== record.sha256) {
|
||||
throw new Error(`REA CSS digest mismatch for ${spec.file}: manifest ${record.sha256}, actual ${actualHash}`)
|
||||
}
|
||||
sources.set(spec.file, {
|
||||
...spec,
|
||||
bytes,
|
||||
sha256: actualHash,
|
||||
root: postcss.parse(bytes.toString('utf8'), { from: spec.file })
|
||||
})
|
||||
}
|
||||
|
||||
const extraFiles = ['src-Bh3YZ35H.css', 'tokens-Df-i-zsO.css']
|
||||
for (const file of extraFiles) {
|
||||
const bytes = await readFile(resolve(exportDir, file))
|
||||
const record = manifest.files?.find((item) => item.file === file)
|
||||
if (!record?.sha256) throw new Error(`REA manifest has no SHA-256 for ${file}`)
|
||||
const actualHash = sourceHash(bytes)
|
||||
if (actualHash !== record.sha256) {
|
||||
throw new Error(`REA CSS digest mismatch for ${file}: manifest ${record.sha256}, actual ${actualHash}`)
|
||||
}
|
||||
sources.set(file, {
|
||||
file,
|
||||
bytes,
|
||||
sha256: actualHash,
|
||||
root: postcss.parse(bytes.toString('utf8'), { from: file })
|
||||
})
|
||||
}
|
||||
|
||||
const cy = sources.get('src-CyXHA0IC.css')
|
||||
const contexts = sources.get('contexts-BkjwYSZT.css')
|
||||
const button = sources.get('src-Bh3YZ35H.css')
|
||||
const tokensRoot = sources.get('tokens-Df-i-zsO.css').root
|
||||
const tokens = new Map()
|
||||
tokensRoot.walkDecls((decl) => {
|
||||
if (decl.prop.startsWith('--wb-') && !tokens.has(decl.prop)) tokens.set(decl.prop, decl.value)
|
||||
})
|
||||
|
||||
const sourceRules = sourceSpecs.flatMap((spec) => {
|
||||
const source = sources.get(spec.file)
|
||||
return collectRules(source.root, source, spec.selectors)
|
||||
})
|
||||
for (const spec of sourceSpecs) {
|
||||
const found = new Set(sourceRules
|
||||
.filter((record) => record.source.file === spec.file)
|
||||
.flatMap((record) => record.selectors))
|
||||
const missing = spec.selectors.map(normalizeSelector).filter((selector) => !found.has(selector))
|
||||
if (missing.length) throw new Error(`REA stylesheet ${spec.file} is missing selected rules: ${missing.join(', ')}`)
|
||||
}
|
||||
|
||||
const slateEditorRule = findRule(cy.root, '.cr-input-editor-host [data-slate-editor]')
|
||||
const focusedContainerRule = findRule(cy.root, '.cr-input-container--focused')
|
||||
const sendButtonRule = findRule(cy.root, '.cr-send-button')
|
||||
const sendIconRule = findRule(cy.root, '.cr-send-button__icon')
|
||||
const nativeButtonRule = findRule(button.root, '.wb-button')
|
||||
const iconOnlyRule = findRule(button.root, '.wb-button--icon-only')
|
||||
const iconOnlyMediumRule = findRule(button.root, '.wb-button--icon-only.wb-button--medium')
|
||||
const plainButtonRule = findRule(button.root, '.wb-button--plain')
|
||||
const circleButtonRule = findRule(button.root, '.wb-button--circle')
|
||||
const footerItemRule = findRule(cy.root, '.cr-input-footer-item')
|
||||
const sendSize = resolveToken('--wb-button-height-medium', tokens)
|
||||
const sendRadius = declaration(circleButtonRule, 'border-radius')
|
||||
const iconOnlyWidth = declaration(iconOnlyMediumRule, 'width')
|
||||
const iconOnlyHeight = declaration(iconOnlyMediumRule, 'height')
|
||||
const footerItemHeight = declaration(footerItemRule, 'height')
|
||||
if (sendSize !== '32px' || footerItemHeight !== sendSize || iconOnlyWidth !== 'var(--wb-button-height-medium)' || iconOnlyHeight !== iconOnlyWidth || sendRadius !== '50%') {
|
||||
throw new Error(`Unexpected REA send-button sizing: height ${sendSize}, button width ${iconOnlyWidth}, height ${iconOnlyHeight}`)
|
||||
}
|
||||
if (declaration(sendButtonRule, '--wb-button-icon-size-medium') !== '32px' || declaration(sendIconRule, 'width') !== '32px') {
|
||||
throw new Error('REA send-button icon sizing changed; review the native-button projection before regenerating')
|
||||
}
|
||||
|
||||
const sourceNotes = [
|
||||
`CSS Evidence ID ${evidenceId}`,
|
||||
`capture target ${manifest.target?.title ?? 'unknown'}; provider ${manifest.provider?.id ?? 'unknown'}`,
|
||||
...[...sources.values()].map((source) => `${source.file} sha256=${source.sha256}`),
|
||||
'Only selected input-area rules are recovered; source sheets are not imported wholesale.',
|
||||
'WorkBuddy input-wrapper wb-color tokens use the recovered scoped theme declarations.',
|
||||
'The native textarea receives declarations selected from .cr-input-editor-host [data-slate-editor]; no Slate data attributes are added.',
|
||||
'The native send button receives the required .wb-button base, icon-only, medium, and plain declarations as a local projection.'
|
||||
]
|
||||
|
||||
const cssSections = [
|
||||
`/*\n * Generated by scripts/rea/recover-composer-styles.mjs.\n${sourceNotes.map((note) => ` * ${note}`).join('\n')}\n */`,
|
||||
...sourceRules.map((record) => formatRule(
|
||||
record.selectors.join(', '),
|
||||
record.declarations,
|
||||
{ file: record.source.file, column: record.column },
|
||||
record.source.sha256
|
||||
)),
|
||||
formatRule(
|
||||
'.cr-input-container:focus-within',
|
||||
declarationText(focusedContainerRule),
|
||||
{ file: cy.file, column: focusedContainerRule.source?.start?.column ?? 1 },
|
||||
cy.sha256,
|
||||
'projected from the original focus state class for a native focusable container'
|
||||
),
|
||||
formatRule(
|
||||
'.conversation-input__box .cr-input-container:focus-within',
|
||||
declarationText(findRule(contexts.root, '.conversation-input__box .cr-input-container--focused')),
|
||||
{ file: contexts.file, column: findRule(contexts.root, '.conversation-input__box .cr-input-container--focused').source?.start?.column ?? 1 },
|
||||
contexts.sha256,
|
||||
'selector projection of WorkBuddy focus-border class'
|
||||
),
|
||||
formatRule(
|
||||
'.chat-pane[data-cr-theme=dark] .conversation-input__box .cr-input-container:focus-within',
|
||||
declarationText(findRule(contexts.root, '.vscode-dark .conversation-input__box .cr-input-container--focused')),
|
||||
{ file: contexts.file, column: findRule(contexts.root, '.vscode-dark .conversation-input__box .cr-input-container--focused').source?.start?.column ?? 1 },
|
||||
contexts.sha256,
|
||||
'dark theme selector projection'
|
||||
),
|
||||
formatRule(
|
||||
'.cr-input-editor-host > .wb-composer__input',
|
||||
declarationText(slateEditorRule),
|
||||
{ file: cy.file, column: slateEditorRule.source?.start?.column ?? 1 },
|
||||
cy.sha256,
|
||||
'selector projection from [data-slate-editor] to the native textarea; attribute intentionally omitted'
|
||||
),
|
||||
formatRule(
|
||||
'.wb-composer__add-button.cr-input-footer-item',
|
||||
[
|
||||
`width: ${footerItemHeight};`,
|
||||
`min-width: ${footerItemHeight};`,
|
||||
'padding: 0;'
|
||||
],
|
||||
{ file: cy.file, column: footerItemRule.source?.start?.column ?? 1 },
|
||||
cy.sha256,
|
||||
'icon-only projection retains the original 32px footer-item height'
|
||||
),
|
||||
`/* Native button adapter from ${button.file} and ${sources.get('tokens-Df-i-zsO.css').file}; .wb-button base/icon-only/plain/circle declarations, resolved medium height ${sendSize}, border radius ${sendRadius}. */\n.cr-send-button {\n${[
|
||||
'box-sizing',
|
||||
'cursor',
|
||||
'white-space',
|
||||
'user-select',
|
||||
'border',
|
||||
'justify-content',
|
||||
'align-items',
|
||||
'font-family',
|
||||
'font-weight',
|
||||
'text-decoration',
|
||||
'display'
|
||||
].map((property) => ` ${property}: ${declaration(nativeButtonRule, property)};`).join('\n')}\n width: ${sendSize};\n min-width: ${declaration(iconOnlyRule, 'min-width')};\n height: ${sendSize};\n padding: ${declaration(iconOnlyRule, 'padding')};\n border-radius: ${sendRadius};\n color: ${declaration(plainButtonRule, 'color')};\n background-color: ${declaration(plainButtonRule, 'background-color')};\n}`
|
||||
]
|
||||
|
||||
const output = `${cssSections.join('\n\n')}\n`
|
||||
postcss.parse(output, { from: relative(rootDir, outputPath) })
|
||||
await writeFile(outputPath, output, 'utf8')
|
||||
console.log(`Wrote ${relative(rootDir, outputPath)} from ${sources.size} REA CSS exports (${evidenceId}).`)
|
||||
Reference in new issue
Block a user