( parsed: ParsedDocument, ids: HfId[], styles: Record<string, string | null>, )
| 301 | // ─── Op handlers ──────────────────────────────────────────────────────────── |
| 302 | |
| 303 | function handleSetStyle( |
| 304 | parsed: ParsedDocument, |
| 305 | ids: HfId[], |
| 306 | styles: Record<string, string | null>, |
| 307 | ): MutationResult { |
| 308 | const result: MutationResult = { forward: [], inverse: [] }; |
| 309 | for (const id of ids) { |
| 310 | const el = resolveScoped(parsed.document, id); |
| 311 | if (!el) continue; |
| 312 | const old = getElementStyles(el); |
| 313 | setElementStyles(el, styles); |
| 314 | for (const [prop, value] of Object.entries(styles)) { |
| 315 | // Normalize to the camelCase key the style map + patch grammar use. A |
| 316 | // hyphenated op key ("transform-origin") otherwise misses the camelCase |
| 317 | // store, so oldValue is always null → undo deletes/loses the prior value, |
| 318 | // a removal skips its inverse patch entirely (DOM/patch-log desync), and |
| 319 | // the patch path/override-set key diverge from the camelCase grammar. |
| 320 | const key = toCamel(prop); |
| 321 | const path = stylePath(id, key); |
| 322 | const oldValue = old[key] ?? null; |
| 323 | if (value !== null) { |
| 324 | const p = scalarChange(path, oldValue, value); |
| 325 | result.forward.push(p.forward); |
| 326 | result.inverse.push(p.inverse); |
| 327 | } else if (oldValue !== null) { |
| 328 | const p = scalarDelete(path, oldValue); |
| 329 | result.forward.push(p.forward); |
| 330 | result.inverse.push(p.inverse); |
| 331 | } |
| 332 | } |
| 333 | } |
| 334 | return result; |
| 335 | } |
| 336 | |
| 337 | function handleMoveElement( |
| 338 | parsed: ParsedDocument, |
no test coverage detected