feat(scenes): rich scene descriptions and control viewer window

Add TipTap editing in the editor and an Electron window on the control panel to read the current scene description.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-15 09:49:40 +08:00
parent 35a6e979eb
commit cfa3959fb3
26 changed files with 1795 additions and 60 deletions
@@ -0,0 +1,28 @@
.page {
height: 100vh;
padding: 16px;
box-sizing: border-box;
display: grid;
background: var(--color-surface-elevated);
}
.body {
min-height: 0;
overflow: auto;
padding: 14px 16px;
border-radius: var(--radius-md);
border: 1px solid var(--stroke);
background: var(--color-overlay-dark-3);
}
.proseText {
color: var(--text0);
font-size: var(--text-md);
line-height: 1.55;
word-break: break-word;
}
.empty {
color: var(--text2);
font-size: var(--text-sm);
}
@@ -0,0 +1,50 @@
import React, { useEffect, useMemo, useState } from 'react';
import { ipcChannels } from '../../shared/ipc/contracts';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { isSceneDescriptionEmpty, sanitizeSceneDescriptionHtml } from '../editor/sceneDescriptionHtml';
import proseStyles from '../editor/SceneDescriptionModal.module.css';
import { getDndApi } from '../shared/dndApi';
import styles from './SceneDescriptionApp.module.css';
export function SceneDescriptionApp() {
const { t } = useEditorI18n();
const [html, setHtml] = useState('');
useEffect(() => {
const api = getDndApi();
void api.invoke(ipcChannels.windows.getSceneDescriptionContent, {}).then(({ html: next }) => {
setHtml(next);
});
return api.on(ipcChannels.windows.sceneDescriptionContent, ({ html: next }) => {
setHtml(next);
});
}, []);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') window.close();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
const safeHtml = useMemo(() => sanitizeSceneDescriptionHtml(html), [html]);
const empty = isSceneDescriptionEmpty(html);
return (
<div className={styles.page}>
<div className={styles.body}>
{empty ? (
<div className={styles.empty}>{t('scene.descriptionEmpty')}</div>
) : (
<div
className={[proseStyles.prose, styles.proseText].join(' ')}
dangerouslySetInnerHTML={{ __html: safeHtml }}
/>
)}
</div>
</div>
);
}
+20
View File
@@ -0,0 +1,20 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import '../shared/ui/globals.css';
import { EditorI18nProvider } from '../editor/i18n/EditorI18nContext';
import { SceneDescriptionApp } from './SceneDescriptionApp';
const rootEl = document.getElementById('root');
if (!rootEl) {
throw new Error('Missing #root element');
}
createRoot(rootEl).render(
<React.StrictMode>
<EditorI18nProvider>
<SceneDescriptionApp />
</EditorI18nProvider>
</React.StrictMode>,
);