Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 161 additions & 4 deletions kits/slides/app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions kits/slides/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"reifyui": "^0.4.1",
"reifyui": "^0.4.2",
"echarts": "^5.5.1",
"mermaid": "^11.4.1",
"highlight.js": "^11.10.0",
"lucide-react": "^0.469.0",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0"
"remark-gfm": "^4.0.0",
"pptxgenjs": "^4.0.1"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
Expand Down
6 changes: 4 additions & 2 deletions kits/slides/app/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ function parseHash() {
const raw = window.location.hash.replace(/^#\/?/, '');
const [path, query = ''] = raw.split('?');
const params = new URLSearchParams(query);
if (path === 'print') return { page: 'print' };
if (path === 'print') {
return { page: 'print', handoff: params.get('h') || '', id: params.get('deck') || '' };
}
if (path.startsWith('d/')) {
return { page: 'deck', id: decodeURIComponent(path.slice(2)),
seed: params.get('seed') || '', template: params.get('tpl') || '' };
Expand All @@ -32,7 +34,7 @@ export default function App() {
return () => window.removeEventListener('hashchange', onHash);
}, []);

if (route.page === 'print') return <PrintPage />;
if (route.page === 'print') return <PrintPage handoff={route.handoff} id={route.id} />;
if (route.page === 'deck') {
return <DeckPage key={route.id} id={route.id} seed={route.seed} template={route.template} />;
}
Expand Down
15 changes: 15 additions & 0 deletions kits/slides/app/src/components/Dialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,18 @@ export function NameDialog({ title, body, defaultValue = '', confirmLabel = 'Cre
</Dialog>
);
}

/** A message the person must acknowledge. In-app, never window.alert — a browser popup is the
* clearest signal a product was not finished, and it cannot carry detail or styling. */
export function AlertDialog({ title, body, detail, onClose }) {
return (
<Dialog onClose={onClose}>
<h3>{title}</h3>
<div className="body">
{Array.isArray(body) ? body.map((line, i) => <p key={i}>{line}</p>) : body}
</div>
{detail && <pre className="dlg-detail">{detail}</pre>}
<div className="foot"><button className="btn primary" onClick={onClose}>OK</button></div>
</Dialog>
);
}
44 changes: 44 additions & 0 deletions kits/slides/app/src/lib/exportPdf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// PDF export: the browser's own print pipeline, pointed at a fixed print surface.
//
// There is no export service in this deployment, and this is not a compromise — printing #/print
// renders the deck through the SAME components the canvas uses, so text stays selectable vector
// and charts stay vector (the chart element already renders SVG). Any library that rasterises the
// slide would be larger, slower and worse.
//
// The honest cost, stated in the UI rather than hidden: this opens the system print dialog. We
// cannot name the file or choose the destination, so the menu says "Print to PDF…", not
// "download".
const KEY = 'slides.print.';
const STALE_MS = 5 * 60 * 1000;

/** Hand the LIVE deck to the print tab.
*
* Deliberately not "let the print tab load it by id": autosave is debounced and is refused with
* 409 while a turn is running, so the file on disk is not always what the person is looking at.
* Printing something other than what is on screen would be its own bug.
*
* Must run inside the click. Opening the tab after an await loses user activation and the
* browser blocks it as a popup.
*/
export function openPrintView(id, deck) {
for (const k of Object.keys(localStorage)) {
if (k.startsWith(KEY) && Date.now() - Number(k.slice(KEY.length)) > STALE_MS) {
localStorage.removeItem(k); // a handoff nobody collected (tab closed, print cancelled)
}
}

const key = KEY + Date.now();
try {
localStorage.setItem(key, JSON.stringify({ id, deck }));
} catch {
throw new Error('This deck is too large to hand to the print view.');
}

const url = `${window.location.pathname}#/print?h=${encodeURIComponent(key)}`
+ `&deck=${encodeURIComponent(id || '')}`;
const tab = window.open(url, '_blank');
if (!tab) {
localStorage.removeItem(key);
throw new Error('Your browser blocked the print tab. Allow pop-ups for this page, then try Export again.');
}
}
Loading
Loading