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 { EmbedCell } from './EmbedCell';
|
||||||
import { Loader2 } from 'lucide-react';
|
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 {
|
interface EmbedViewerProps {
|
||||||
notebookId: string;
|
notebookId: string;
|
||||||
initialTheme: string;
|
initialTheme: string;
|
||||||
@ -33,8 +27,6 @@ export default function EmbedViewer({ notebookId, initialTheme }: EmbedViewerPro
|
|||||||
// Listen for postMessage theme changes from parent iframe host
|
// Listen for postMessage theme changes from parent iframe host
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleMessage(event: MessageEvent) {
|
function handleMessage(event: MessageEvent) {
|
||||||
if (!ALLOWED_MESSAGE_ORIGINS.has(event.origin)) return;
|
|
||||||
|
|
||||||
if (event.data?.type === 'spicebook-theme') {
|
if (event.data?.type === 'spicebook-theme') {
|
||||||
const incoming = event.data.theme;
|
const incoming = event.data.theme;
|
||||||
if (incoming === 'dark' || incoming === 'light') {
|
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 { Button } from '../../ui/Button';
|
||||||
import { Badge } from '../../ui/Badge';
|
import { Badge } from '../../ui/Badge';
|
||||||
import { Dropdown } from '../../ui/Dropdown';
|
import { Dropdown } from '../../ui/Dropdown';
|
||||||
|
import { EmbedDialog } from './EmbedDialog';
|
||||||
import { useNotebookStore } from '../../../lib/notebook-store';
|
import { useNotebookStore } from '../../../lib/notebook-store';
|
||||||
import type { CellType } from '../../../lib/types';
|
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"
|
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="ngspice">ngspice</option>
|
||||||
<option value="ltspice" disabled>
|
<option value="ltspice">ltspice</option>
|
||||||
ltspice (soon)
|
|
||||||
</option>
|
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{/* Dirty indicator */}
|
{/* Dirty indicator */}
|
||||||
@ -161,6 +160,9 @@ export function NotebookToolbar() {
|
|||||||
Run All
|
Run All
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Embed snippet */}
|
||||||
|
<EmbedDialog />
|
||||||
|
|
||||||
{/* Save */}
|
{/* Save */}
|
||||||
<Button
|
<Button
|
||||||
variant={dirty ? 'primary' : 'ghost'}
|
variant={dirty ? 'primary' : 'ghost'}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { defineMiddleware } from 'astro:middleware';
|
import { defineMiddleware } from 'astro:middleware';
|
||||||
|
|
||||||
// CSP frame-ancestors: controls which origins can embed this site in an iframe.
|
// 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).
|
// /embed/* routes allow framing from any origin; the main app stays locked to 'self'.
|
||||||
const FRAME_ANCESTORS = "'self' https://forrest.warehack.ing";
|
const FRAME_ANCESTORS = '*';
|
||||||
|
|
||||||
export const onRequest = defineMiddleware(async ({ url }, next) => {
|
export const onRequest = defineMiddleware(async ({ url }, next) => {
|
||||||
const response = await next();
|
const response = await next();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user