mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
refactor(studio): update layout, config, and remove agent activity tracking (#64)
## Summary - **NLELayout**: Add toolbar slot, composition breadcrumb navigation, improved responsive layout - **Vite config**: Add full project API (preview, thumbnail, render, file CRUD) for standalone dev mode - Remove AgentActivityTrack component (replaced by timeline clips) - Add HTML editor utilities for composition source editing - Guard setInterval cleanup to dev-only to prevent `vite build` from hanging in CI 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* HTML Editor — Utility functions for parsing and manipulating HyperFrame HTML source.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse a CSS inline style string into a key-value map.
|
||||
* e.g. "opacity: 0.5; transform: matrix(1,0,0,1,0,0)" →
|
||||
* { opacity: "0.5", transform: "matrix(1,0,0,1,0,0)" }
|
||||
*/
|
||||
export function parseStyleString(style: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
for (const decl of style.split(";")) {
|
||||
const colonIdx = decl.indexOf(":");
|
||||
if (colonIdx < 0) continue;
|
||||
const key = decl.slice(0, colonIdx).trim();
|
||||
const value = decl.slice(colonIdx + 1).trim();
|
||||
if (key && value) result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge `newStyles` into an opening tag string's `style` attribute.
|
||||
* - New values win over existing ones.
|
||||
* - If no `style` attribute is present, one is added before the closing `>`.
|
||||
*/
|
||||
export function mergeStyleIntoTag(tag: string, newStyles: string): string {
|
||||
if (!newStyles.trim()) return tag;
|
||||
|
||||
const incoming = parseStyleString(newStyles);
|
||||
|
||||
// Match style="..." or style='...' — handle multi-line attrs via dotall-like trick
|
||||
const styleAttrRe = /style=(["'])([\s\S]*?)\1/;
|
||||
const match = tag.match(styleAttrRe);
|
||||
|
||||
if (match) {
|
||||
const quote = match[1];
|
||||
const existing = parseStyleString(match[2]);
|
||||
const merged = { ...existing, ...incoming };
|
||||
const serialized = Object.entries(merged)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join("; ");
|
||||
return tag.replace(styleAttrRe, `style=${quote}${serialized}${quote}`);
|
||||
}
|
||||
|
||||
// No style attribute — insert one before the closing `>`
|
||||
const serialized = Object.entries(incoming)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join("; ");
|
||||
// Handle self-closing tags (`/>`) and regular closing (`>`)
|
||||
return tag.replace(/(\/?>)$/, ` style="${serialized}"$1`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the full element block (opening tag through closing tag) in the source.
|
||||
* Uses quote-aware scanning to handle attributes containing >.
|
||||
* Uses depth counting to handle nested same-name tags.
|
||||
*/
|
||||
export function findElementBlock(
|
||||
html: string,
|
||||
elementId: string,
|
||||
): {
|
||||
start: number;
|
||||
end: number;
|
||||
openTag: string;
|
||||
tagName: string;
|
||||
indent: string;
|
||||
innerContent: string;
|
||||
isSelfClosing: boolean;
|
||||
} | null {
|
||||
let idIdx = html.indexOf(`id="${elementId}"`);
|
||||
if (idIdx < 0) idIdx = html.indexOf(`id='${elementId}'`);
|
||||
if (idIdx < 0) return null;
|
||||
|
||||
// Walk backward to find < and capture indent
|
||||
let tagStart = idIdx;
|
||||
while (tagStart > 0 && html[tagStart] !== "<") tagStart--;
|
||||
|
||||
let indentStart = tagStart;
|
||||
while (indentStart > 0 && html[indentStart - 1] !== "\n") indentStart--;
|
||||
const indent = html.slice(indentStart, tagStart);
|
||||
|
||||
// Walk forward from id to find the closing > of the opening tag
|
||||
let tagEnd = idIdx;
|
||||
let inQuote: string | null = null;
|
||||
while (tagEnd < html.length) {
|
||||
const ch = html[tagEnd];
|
||||
if (inQuote) {
|
||||
if (ch === inQuote) inQuote = null;
|
||||
} else {
|
||||
if (ch === '"' || ch === "'") inQuote = ch;
|
||||
if (ch === ">") {
|
||||
tagEnd++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
tagEnd++;
|
||||
}
|
||||
|
||||
const openTag = html.slice(tagStart, tagEnd);
|
||||
const tagNameMatch = openTag.match(/^<([a-z][a-z0-9]*)/i);
|
||||
if (!tagNameMatch) return null;
|
||||
|
||||
const tagName = tagNameMatch[1];
|
||||
const isSelfClosing =
|
||||
openTag.trimEnd().endsWith("/>") ||
|
||||
["img", "br", "hr", "input", "meta", "link", "source"].includes(tagName.toLowerCase());
|
||||
|
||||
if (isSelfClosing) {
|
||||
return {
|
||||
start: tagStart,
|
||||
end: tagStart + openTag.length,
|
||||
openTag,
|
||||
tagName,
|
||||
indent: /^[\t ]*$/.test(indent) ? indent : "",
|
||||
innerContent: "",
|
||||
isSelfClosing: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Find matching closing tag using depth counting
|
||||
const closeTag = `</${tagName.toLowerCase()}>`;
|
||||
const openPattern = `<${tagName.toLowerCase()}`;
|
||||
let depth = 0;
|
||||
let pos = tagStart;
|
||||
const lower = html.toLowerCase();
|
||||
|
||||
while (pos < html.length) {
|
||||
if (lower.startsWith("<!--", pos)) {
|
||||
const commentEnd = lower.indexOf("-->", pos + 4);
|
||||
pos = commentEnd < 0 ? html.length : commentEnd + 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lower.startsWith(openPattern, pos) && /[\s>/]/.test(html[pos + openPattern.length] || "")) {
|
||||
depth++;
|
||||
pos += openPattern.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lower.startsWith(closeTag, pos)) {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
const end = pos + closeTag.length;
|
||||
const innerContent = html.slice(tagStart + openTag.length, pos);
|
||||
return {
|
||||
start: tagStart,
|
||||
end,
|
||||
openTag,
|
||||
tagName,
|
||||
indent: /^[\t ]*$/.test(indent) ? indent : "",
|
||||
innerContent,
|
||||
isSelfClosing: false,
|
||||
};
|
||||
}
|
||||
pos += closeTag.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
pos++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user