mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): add flat identity header for the inspector
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader";
|
||||
|
||||
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function renderHeader(overrides: Partial<Parameters<typeof PropertyPanelFlatHeader>[0]> = {}) {
|
||||
const host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
const root = createRoot(host);
|
||||
const props = {
|
||||
name: "Mono Label",
|
||||
meta: ".mono-label · div",
|
||||
elementKind: "text" as const,
|
||||
hidden: false,
|
||||
copied: false,
|
||||
onCopy: vi.fn(),
|
||||
onClear: vi.fn(),
|
||||
showUngroup: false,
|
||||
...overrides,
|
||||
};
|
||||
act(() => {
|
||||
root.render(<PropertyPanelFlatHeader {...props} />);
|
||||
});
|
||||
return { host, root, props };
|
||||
}
|
||||
|
||||
describe("PropertyPanelFlatHeader", () => {
|
||||
it("renders name, meta, and the mint text-type icon", () => {
|
||||
const { host, root } = renderHeader();
|
||||
expect(host.textContent).toContain("Mono Label");
|
||||
expect(host.textContent).toContain(".mono-label · div");
|
||||
const icon = host.querySelector('[data-flat-header-icon="true"]');
|
||||
expect(icon?.className).toContain("text-panel-accent");
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("colors the media icon cyan and the other icon amber", () => {
|
||||
const { host: mediaHost, root: mediaRoot } = renderHeader({ elementKind: "media" });
|
||||
expect(mediaHost.querySelector('[data-flat-header-icon="true"]')?.className).toContain(
|
||||
"text-panel-media",
|
||||
);
|
||||
act(() => mediaRoot.unmount());
|
||||
|
||||
const { host: otherHost, root: otherRoot } = renderHeader({ elementKind: "other" });
|
||||
expect(otherHost.querySelector('[data-flat-header-icon="true"]')?.className).toContain(
|
||||
"text-panel-container",
|
||||
);
|
||||
act(() => otherRoot.unmount());
|
||||
});
|
||||
|
||||
it("fires onCopy and onClear from their action buttons", () => {
|
||||
const { host, root, props } = renderHeader();
|
||||
const copy = host.querySelector<HTMLButtonElement>(
|
||||
'[aria-label="Copy element info to clipboard"]',
|
||||
);
|
||||
const clear = host.querySelector<HTMLButtonElement>('[aria-label="Clear selection"]');
|
||||
act(() => copy?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
||||
act(() => clear?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
||||
expect(props.onCopy).toHaveBeenCalledTimes(1);
|
||||
expect(props.onClear).toHaveBeenCalledTimes(1);
|
||||
act(() => root.unmount());
|
||||
});
|
||||
|
||||
it("only renders Ungroup when showUngroup is true", () => {
|
||||
const { host: without } = renderHeader({ showUngroup: false });
|
||||
expect(without.querySelector('[aria-label="Ungroup"]')).toBeNull();
|
||||
|
||||
const { host: withUngroup } = renderHeader({ showUngroup: true, onUngroup: vi.fn() });
|
||||
expect(withUngroup.querySelector('[aria-label="Ungroup"]')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Eye, EyeSlash } from "@phosphor-icons/react";
|
||||
import { ClipboardList, Film, Square, Type, X } from "../../icons/SystemIcons";
|
||||
|
||||
const ICON_BY_KIND = { text: Type, media: Film, other: Square } as const;
|
||||
const ICON_COLOR_BY_KIND = {
|
||||
text: "text-panel-accent",
|
||||
media: "text-panel-media",
|
||||
other: "text-panel-container",
|
||||
} as const;
|
||||
|
||||
export function PropertyPanelFlatHeader({
|
||||
name,
|
||||
meta,
|
||||
elementKind,
|
||||
hidden,
|
||||
onToggleHidden,
|
||||
copied,
|
||||
onCopy,
|
||||
onClear,
|
||||
onUngroup,
|
||||
showUngroup,
|
||||
}: {
|
||||
name: string;
|
||||
meta: string;
|
||||
elementKind: "text" | "media" | "other";
|
||||
hidden: boolean;
|
||||
onToggleHidden?: () => void;
|
||||
copied: boolean;
|
||||
onCopy: () => void;
|
||||
onClear: () => void;
|
||||
onUngroup?: () => void;
|
||||
showUngroup: boolean;
|
||||
}) {
|
||||
const Icon = ICON_BY_KIND[elementKind];
|
||||
const visibilityLabel = hidden ? "Show element" : "Hide element";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2.5 border-b border-panel-hairline px-4 py-3">
|
||||
<Icon
|
||||
size={15}
|
||||
data-flat-header-icon="true"
|
||||
className={`flex-shrink-0 ${ICON_COLOR_BY_KIND[elementKind]}`}
|
||||
/>
|
||||
<div className="flex min-w-0 flex-1 items-baseline gap-2">
|
||||
<span className="truncate text-[13px] font-semibold text-panel-text-0">{name}</span>
|
||||
<span className="truncate font-mono text-[10px] text-panel-text-4">{meta}</span>
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center gap-2.5 text-panel-text-3">
|
||||
{showUngroup && (
|
||||
<button type="button" aria-label="Ungroup" title="Ungroup (⌘⇧G)" onClick={onUngroup}>
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
>
|
||||
<rect x="1.5" y="1.5" width="7" height="7" rx="1" />
|
||||
<rect x="7.5" y="7.5" width="7" height="7" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
{onToggleHidden && (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={visibilityLabel}
|
||||
title={visibilityLabel}
|
||||
onClick={onToggleHidden}
|
||||
>
|
||||
{hidden ? <EyeSlash size={13} weight="bold" /> : <Eye size={13} weight="bold" />}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Copy element info to clipboard"
|
||||
title={copied ? "Copied!" : "Copy element info for any AI agent"}
|
||||
onClick={onCopy}
|
||||
className={copied ? "text-panel-accent" : undefined}
|
||||
>
|
||||
<ClipboardList size={13} />
|
||||
</button>
|
||||
<button type="button" aria-label="Clear selection" onClick={onClear}>
|
||||
<X size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user