Add floating canvas panels, theme settings, and SVG line-spacing analysis.

Canvas Studio now uses dockable floating panels, app settings/help navigation, and improved SVG export; the backend adds an SVG line-spacing analysis API with SciPy acceleration and new design templates.
This commit is contained in:
2026-07-13 22:01:01 +08:00
parent d5d8caef2f
commit f8a907e7c5
33 changed files with 4434 additions and 455 deletions
+45 -5
View File
@@ -1,12 +1,13 @@
import { useLayoutEffect, useState } from 'react';
import { useEffect, useLayoutEffect, useState } from 'react';
import type { ThemeMode } from './components/AppSettingsWindow';
import CanvasStudio from './pages/CanvasStudio';
import HelpPage from './pages/HelpPage';
import TemplateHome from './pages/TemplateHome';
import TestWorkbench from './pages/TestWorkbench';
import { CanvasDocument, WordcloudStickerPayload } from './types';
import { createDefaultDocument } from './lib/canvasDocument';
type AppPage = 'home' | 'canvas' | 'wordcloud';
type ThemeMode = 'light' | 'dark' | 'system';
type AppPage = 'home' | 'canvas' | 'wordcloud' | 'help';
const getStoredTheme = (): ThemeMode => {
const stored = window.localStorage.getItem('wordcloud-theme');
@@ -18,22 +19,40 @@ const getSystemTheme = () =>
export default function App() {
const [page, setPage] = useState<AppPage>('home');
const [themeMode] = useState<ThemeMode>(getStoredTheme);
const [systemTheme] = useState<'light' | 'dark'>(getSystemTheme);
const [themeMode, setThemeMode] = useState<ThemeMode>(getStoredTheme);
const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>(getSystemTheme);
const [initialDocument, setInitialDocument] = useState<CanvasDocument | null>(null);
const [pendingWordcloudSticker, setPendingWordcloudSticker] = useState<WordcloudStickerPayload | null>(null);
const openHelp = () => setPage('help');
useLayoutEffect(() => {
const resolvedTheme = themeMode === 'system' ? systemTheme : themeMode;
document.documentElement.dataset.theme = resolvedTheme;
document.documentElement.dataset.themeMode = themeMode;
document.documentElement.style.colorScheme = resolvedTheme;
window.localStorage.setItem('wordcloud-theme', themeMode);
}, [themeMode, systemTheme]);
useEffect(() => {
const media = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = (event: MediaQueryListEvent) => {
setSystemTheme(event.matches ? 'dark' : 'light');
};
setSystemTheme(media.matches ? 'dark' : 'light');
media.addEventListener('change', handleChange);
return () => media.removeEventListener('change', handleChange);
}, []);
if (page === 'wordcloud') {
return (
<TestWorkbench
themeMode={themeMode}
systemTheme={systemTheme}
onThemeModeChange={setThemeMode}
onOpenCanvas={() => setPage('canvas')}
onOpenHelp={openHelp}
onImportWordcloudSticker={(payload) => {
setPendingWordcloudSticker(payload);
setPage('canvas');
@@ -45,8 +64,12 @@ export default function App() {
if (page === 'canvas') {
return (
<CanvasStudio
themeMode={themeMode}
systemTheme={systemTheme}
onThemeModeChange={setThemeMode}
onOpenHome={() => setPage('home')}
onOpenWordcloud={() => setPage('wordcloud')}
onOpenHelp={openHelp}
initialDocument={initialDocument}
onConsumeInitialDocument={() => setInitialDocument(null)}
pendingWordcloudSticker={pendingWordcloudSticker}
@@ -55,8 +78,24 @@ export default function App() {
);
}
if (page === 'help') {
return (
<HelpPage
themeMode={themeMode}
systemTheme={systemTheme}
onThemeModeChange={setThemeMode}
onOpenHome={() => setPage('home')}
onOpenCanvas={() => setPage('canvas')}
onOpenWordcloud={() => setPage('wordcloud')}
/>
);
}
return (
<TemplateHome
themeMode={themeMode}
systemTheme={systemTheme}
onThemeModeChange={setThemeMode}
onCreateBlank={() => {
setInitialDocument(createDefaultDocument());
setPage('canvas');
@@ -67,6 +106,7 @@ export default function App() {
}}
onOpenCanvas={() => setPage('canvas')}
onOpenWordcloud={() => setPage('wordcloud')}
onOpenHelp={openHelp}
/>
);
}