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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>,
|
||||
);
|
||||
Reference in New Issue
Block a user