Open embed route to all origins, add embed snippet UI, enable LTspice
frame-ancestors * for /embed/* routes so any site can iframe notebooks. Remove postMessage origin allowlist (theme toggle is cosmetic-only). Add EmbedDialog popover with copy-paste iframe snippet and theme picker. Enable ltspice in the engine dropdown now that the backend supports it.
This commit is contained in:
parent
896a8535cf
commit
fb70b39173
@ -4,12 +4,6 @@ import * as api from '../../lib/api';
|
||||
import { EmbedCell } from './EmbedCell';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
// Origins allowed to send postMessage theme changes to the embed.
|
||||
// Must stay in sync with CSP_FRAME_ANCESTORS / CORS_EXTRA_ORIGINS.
|
||||
const ALLOWED_MESSAGE_ORIGINS = new Set([
|
||||
'https://forrest.warehack.ing',
|
||||
]);
|
||||
|
||||
interface EmbedViewerProps {
|
||||
notebookId: string;
|
||||
initialTheme: string;
|
||||
@ -33,8 +27,6 @@ export default function EmbedViewer({ notebookId, initialTheme }: EmbedViewerPro
|
||||
// Listen for postMessage theme changes from parent iframe host
|
||||
useEffect(() => {
|
||||
function handleMessage(event: MessageEvent) {
|
||||
if (!ALLOWED_MESSAGE_ORIGINS.has(event.origin)) return;
|
||||
|
||||
if (event.data?.type === 'spicebook-theme') {
|
||||
const incoming = event.data.theme;
|
||||
if (incoming === 'dark' || incoming === 'light') {
|
||||
|
||||
97
frontend/src/components/notebook/toolbar/EmbedDialog.tsx
Normal file
97
frontend/src/components/notebook/toolbar/EmbedDialog.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { Code, Check, Copy } from 'lucide-react';
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '../../ui/popover';
|
||||
import { Button } from '../../ui/Button';
|
||||
import { useNotebookStore } from '../../../lib/notebook-store';
|
||||
|
||||
export function EmbedDialog() {
|
||||
const { notebookId } = useNotebookStore();
|
||||
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
if (!notebookId) return null;
|
||||
|
||||
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
|
||||
const snippet = `<iframe
|
||||
src="${origin}/embed/${notebookId}?theme=${theme}"
|
||||
width="100%" height="600"
|
||||
style="border: 1px solid #334155; border-radius: 8px;"
|
||||
allow="clipboard-write"
|
||||
></iframe>`;
|
||||
|
||||
function handleCopy() {
|
||||
navigator.clipboard.writeText(snippet).then(() => {
|
||||
setCopied(true);
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = setTimeout(() => setCopied(false), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="sm" title="Embed this notebook">
|
||||
<Code className="w-3.5 h-3.5" />
|
||||
Embed
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent align="end" className="w-80">
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-medium text-slate-300">
|
||||
Embed this notebook on any page
|
||||
</p>
|
||||
|
||||
{/* Theme toggle */}
|
||||
<div className="flex gap-1 rounded-md bg-slate-800 p-0.5">
|
||||
<button
|
||||
onClick={() => setTheme('dark')}
|
||||
className={`flex-1 rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
theme === 'dark'
|
||||
? 'bg-slate-600 text-slate-100'
|
||||
: 'text-slate-400 hover:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
Dark
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setTheme('light')}
|
||||
className={`flex-1 rounded px-2 py-1 text-xs font-medium transition-colors ${
|
||||
theme === 'light'
|
||||
? 'bg-slate-600 text-slate-100'
|
||||
: 'text-slate-400 hover:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
Light
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Snippet */}
|
||||
<textarea
|
||||
readOnly
|
||||
value={snippet}
|
||||
rows={5}
|
||||
className="w-full resize-none rounded-md border border-slate-600 bg-slate-800 p-2 font-mono text-[11px] text-slate-300 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
|
||||
{/* Copy button */}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
className="w-full"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
)}
|
||||
{copied ? 'Copied!' : 'Copy snippet'}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@ -12,6 +12,7 @@ import {
|
||||
import { Button } from '../../ui/Button';
|
||||
import { Badge } from '../../ui/Badge';
|
||||
import { Dropdown } from '../../ui/Dropdown';
|
||||
import { EmbedDialog } from './EmbedDialog';
|
||||
import { useNotebookStore } from '../../../lib/notebook-store';
|
||||
import type { CellType } from '../../../lib/types';
|
||||
|
||||
@ -124,9 +125,7 @@ export function NotebookToolbar() {
|
||||
className="bg-slate-800 border border-slate-600 rounded text-xs px-2 py-1 text-slate-300 focus:outline-none focus:border-blue-500 cursor-pointer"
|
||||
>
|
||||
<option value="ngspice">ngspice</option>
|
||||
<option value="ltspice" disabled>
|
||||
ltspice (soon)
|
||||
</option>
|
||||
<option value="ltspice">ltspice</option>
|
||||
</select>
|
||||
|
||||
{/* Dirty indicator */}
|
||||
@ -161,6 +160,9 @@ export function NotebookToolbar() {
|
||||
Run All
|
||||
</Button>
|
||||
|
||||
{/* Embed snippet */}
|
||||
<EmbedDialog />
|
||||
|
||||
{/* Save */}
|
||||
<Button
|
||||
variant={dirty ? 'primary' : 'ghost'}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { defineMiddleware } from 'astro:middleware';
|
||||
|
||||
// CSP frame-ancestors: controls which origins can embed this site in an iframe.
|
||||
// Only applied to /embed/* routes (the main app doesn't need to be framed).
|
||||
const FRAME_ANCESTORS = "'self' https://forrest.warehack.ing";
|
||||
// /embed/* routes allow framing from any origin; the main app stays locked to 'self'.
|
||||
const FRAME_ANCESTORS = '*';
|
||||
|
||||
export const onRequest = defineMiddleware(async ({ url }, next) => {
|
||||
const response = await next();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user