From 41ccdce3e5a1ae308d188075e19536a40bcb68cb Mon Sep 17 00:00:00 2001 From: joshunrau Date: Tue, 9 Jun 2026 13:39:57 -0400 Subject: [PATCH] feat(playground): add preview mode option to share button Add a "Preview mode" toggle to the share popover. When enabled, the share URL includes a fullscreen=1 query param, and opening that link renders the instrument fullscreen as a read-only preview with no editor. Co-Authored-By: Claude Opus 4.8 --- .../Header/ShareButton/ShareButton.tsx | 48 +++++++++++++++++-- apps/playground/src/pages/IndexPage.tsx | 29 ++++++++++- apps/playground/src/utils/encode.ts | 13 ++++- 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/apps/playground/src/components/Header/ShareButton/ShareButton.tsx b/apps/playground/src/components/Header/ShareButton/ShareButton.tsx index 74b071114..1e21f3731 100644 --- a/apps/playground/src/components/Header/ShareButton/ShareButton.tsx +++ b/apps/playground/src/components/Header/ShareButton/ShareButton.tsx @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; import { formatByteSize } from '@douglasneuroinformatics/libjs'; -import { Heading, Input, Popover, Tooltip } from '@douglasneuroinformatics/libui/components'; +import { Heading, Input, Label, Popover, Tooltip } from '@douglasneuroinformatics/libui/components'; import { useTranslation } from '@douglasneuroinformatics/libui/hooks'; import { CopyButton } from '@opendatacapture/react-core'; -import { Share2Icon } from 'lucide-react'; +import { CircleHelpIcon, Share2Icon } from 'lucide-react'; import { useFilesRef } from '@/hooks/useFilesRef'; import { useAppStore } from '@/store'; @@ -13,6 +13,7 @@ import { encodeShareURL } from '@/utils/encode'; export const ShareButton = () => { const label = useAppStore((store) => store.selectedInstrument.label); const editorFilesRef = useFilesRef(); + const [isFullscreen, setIsFullscreen] = useState(false); const [shareURL, setShareURL] = useState(encodeShareURL({ files: editorFilesRef.current, label })); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isTooltipOpen, setIsTooltipOpen] = useState(false); @@ -21,9 +22,9 @@ export const ShareButton = () => { // The user cannot modify the editor without closing the popover useEffect(() => { if (isPopoverOpen) { - setShareURL(encodeShareURL({ files: editorFilesRef.current, label })); + setShareURL(encodeShareURL({ files: editorFilesRef.current, fullscreen: isFullscreen, label })); } - }, [isPopoverOpen, label]); + }, [isFullscreen, isPopoverOpen, label]); return ( { {formatByteSize(shareURL.size)}.

+
+
+ + + + + + +

+ {t({ + en: 'Anyone with the link opens the instrument fullscreen as a preview they can try, with no editor and no ability to make changes.', + fr: "Toute personne disposant du lien ouvre l'instrument en plein écran comme un aperçu qu'elle peut essayer, sans éditeur ni possibilité de le modifier." + })} +

+
+
+
+ +
diff --git a/apps/playground/src/pages/IndexPage.tsx b/apps/playground/src/pages/IndexPage.tsx index 2e61857d2..c8428ea64 100644 --- a/apps/playground/src/pages/IndexPage.tsx +++ b/apps/playground/src/pages/IndexPage.tsx @@ -1,13 +1,14 @@ import { useEffect } from 'react'; -import { Separator } from '@douglasneuroinformatics/libui/components'; +import { LanguageToggle, Separator, ThemeToggle } from '@douglasneuroinformatics/libui/components'; import esbuildWasmUrl from 'esbuild-wasm/esbuild.wasm?url'; import { Header } from '@/components/Header'; import { MainContent } from '@/components/MainContent'; +import { Viewer } from '@/components/Viewer'; import type { InstrumentRepository } from '@/models/instrument-repository.model'; import { useAppStore } from '@/store'; -import { decodeShareURL } from '@/utils/encode'; +import { decodeShareURL, isFullscreenShareURL } from '@/utils/encode'; const { initialize } = await import('esbuild-wasm'); await initialize({ @@ -77,6 +78,30 @@ const IndexPage = () => { }; }, [location.href]); + const isFullscreen = isFullscreenShareURL(new URL(location.href)); + + if (isFullscreen) { + return ( +
+
+ + +
+ +
+ +
+
+ ); + } + return (
diff --git a/apps/playground/src/utils/encode.ts b/apps/playground/src/utils/encode.ts index c7b60f173..4705155c5 100644 --- a/apps/playground/src/utils/encode.ts +++ b/apps/playground/src/utils/encode.ts @@ -17,14 +17,25 @@ function encodeFiles(files: EditorFile[]): string { return compressToEncodedURIComponent(JSON.stringify($EditorFiles.parse(files))); } -export function encodeShareURL({ files, label }: Pick): ShareURL { +export function encodeShareURL({ + files, + fullscreen, + label +}: Pick & { fullscreen?: boolean }): ShareURL { const url = new URL(location.origin) as ShareURL; url.searchParams.append('files', encodeFiles(files)); url.searchParams.append('label', compressToEncodedURIComponent(label)); + if (fullscreen) { + url.searchParams.append('fullscreen', '1'); + } url.size = new TextEncoder().encode(url.href).length; return url; } +export function isFullscreenShareURL(url: URL): boolean { + return url.searchParams.get('fullscreen') === '1'; +} + export function decodeShareURL(url: URL): null | Pick { const encodedFiles = url.searchParams.get('files'); const encodedLabel = url.searchParams.get('label');