85 lines
2.5 KiB
TypeScript
85 lines
2.5 KiB
TypeScript
/**
|
|
* extractArtifactToCanvas — Pull a generated artifact out of a generator shape
|
|
* and place it as a standalone canvas object (folk-image, folk-embed, or folk-bookmark).
|
|
*/
|
|
|
|
import type { FolkShape } from "./folk-shape";
|
|
|
|
export type ArtifactMediaType = "image" | "video" | "pdf" | "download";
|
|
|
|
interface ExtractOptions {
|
|
url: string;
|
|
mediaType: ArtifactMediaType;
|
|
title?: string;
|
|
sourceShape: FolkShape;
|
|
}
|
|
|
|
interface CanvasApi {
|
|
newShape: (tagName: string, props?: Record<string, any>, atPosition?: { x: number; y: number }) => any;
|
|
findFreePosition: (w: number, h: number, px?: number, py?: number, exclude?: any) => { x: number; y: number };
|
|
SHAPE_DEFAULTS: Record<string, { width: number; height: number }>;
|
|
}
|
|
|
|
const TAG_MAP: Record<ArtifactMediaType, string> = {
|
|
image: "folk-image",
|
|
video: "folk-embed",
|
|
pdf: "folk-embed",
|
|
download: "folk-bookmark",
|
|
};
|
|
|
|
export function extractArtifactToCanvas({ url, mediaType, title, sourceShape }: ExtractOptions): boolean {
|
|
const api = (window as any).__canvasApi as CanvasApi | undefined;
|
|
if (!api) {
|
|
console.warn("[extract-artifact] Canvas API not available");
|
|
return false;
|
|
}
|
|
|
|
const tagName = TAG_MAP[mediaType];
|
|
const defaults = api.SHAPE_DEFAULTS[tagName] || { width: 400, height: 300 };
|
|
|
|
// Position to the right of the source shape
|
|
const preferX = sourceShape.x + sourceShape.width + 40 + defaults.width / 2;
|
|
const preferY = sourceShape.y + sourceShape.height / 2;
|
|
const pos = api.findFreePosition(defaults.width, defaults.height, preferX, preferY, sourceShape);
|
|
|
|
const props: Record<string, any> = {};
|
|
if (mediaType === "image") {
|
|
props.src = url;
|
|
if (title) props.alt = title;
|
|
} else if (mediaType === "video" || mediaType === "pdf") {
|
|
props.url = url;
|
|
} else {
|
|
props.url = url;
|
|
if (title) props.title = title;
|
|
}
|
|
|
|
api.newShape(tagName, props, { x: pos.x + defaults.width / 2, y: pos.y + defaults.height / 2 });
|
|
return true;
|
|
}
|
|
|
|
/** CSS for the extract button — inject into each component's stylesheet */
|
|
export const extractBtnCss = `
|
|
.extract-btn {
|
|
position: absolute;
|
|
top: 6px;
|
|
right: 6px;
|
|
padding: 3px 8px;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
z-index: 1;
|
|
line-height: 1;
|
|
}
|
|
.image-item:hover .extract-btn,
|
|
.video-item:hover .extract-btn,
|
|
.section:hover .extract-btn,
|
|
.render-preview:hover .extract-btn,
|
|
.preview-area:hover > .extract-btn { opacity: 1; }
|
|
.extract-btn:hover { background: rgba(0, 0, 0, 0.8); }
|
|
`;
|