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:
@@ -29,12 +29,15 @@ import {
|
|||||||
import {
|
import {
|
||||||
applyDockIconIfNeeded,
|
applyDockIconIfNeeded,
|
||||||
closeMultiWindow,
|
closeMultiWindow,
|
||||||
|
closeSceneDescriptionWindow,
|
||||||
createEditorWindowDeferred,
|
createEditorWindowDeferred,
|
||||||
createWindows,
|
createWindows,
|
||||||
focusEditorWindow,
|
focusEditorWindow,
|
||||||
|
getSceneDescriptionContent,
|
||||||
isMultiWindowOpen,
|
isMultiWindowOpen,
|
||||||
markAppQuitting,
|
markAppQuitting,
|
||||||
openMultiWindow,
|
openMultiWindow,
|
||||||
|
openSceneDescriptionWindow,
|
||||||
togglePresentationFullscreen,
|
togglePresentationFullscreen,
|
||||||
waitForEditorWindowReady,
|
waitForEditorWindowReady,
|
||||||
} from './windows/createWindows';
|
} from './windows/createWindows';
|
||||||
@@ -308,6 +311,17 @@ async function main() {
|
|||||||
registerHandler(ipcChannels.windows.getMultiWindowState, () => {
|
registerHandler(ipcChannels.windows.getMultiWindowState, () => {
|
||||||
return { open: isMultiWindowOpen() };
|
return { open: isMultiWindowOpen() };
|
||||||
});
|
});
|
||||||
|
registerHandler(ipcChannels.windows.openSceneDescription, ({ html }) => {
|
||||||
|
openSceneDescriptionWindow(html);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
registerHandler(ipcChannels.windows.closeSceneDescription, () => {
|
||||||
|
closeSceneDescriptionWindow();
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
registerHandler(ipcChannels.windows.getSceneDescriptionContent, () => {
|
||||||
|
return { html: getSceneDescriptionContent() };
|
||||||
|
});
|
||||||
|
|
||||||
registerHandler(ipcChannels.project.list, async () => {
|
registerHandler(ipcChannels.project.list, async () => {
|
||||||
const projects = await projectStore.listProjects();
|
const projects = await projectStore.listProjects();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ function channelRequiresLicense(channel: string): boolean {
|
|||||||
if (channel.startsWith('license.')) return false;
|
if (channel.startsWith('license.')) return false;
|
||||||
if (channel.startsWith('app.')) return false;
|
if (channel.startsWith('app.')) return false;
|
||||||
if (channel === ipcChannels.windows.closeMultiWindow) return false;
|
if (channel === ipcChannels.windows.closeMultiWindow) return false;
|
||||||
|
if (channel === ipcChannels.windows.closeSceneDescription) return false;
|
||||||
if (channel === ipcChannels.windows.togglePresentationFullscreen) return false;
|
if (channel === ipcChannels.windows.togglePresentationFullscreen) return false;
|
||||||
// Список файлов в %userData%/projects — только чтение; без лицензии список не должен «пропадать».
|
// Список файлов в %userData%/projects — только чтение; без лицензии список не должен «пропадать».
|
||||||
if (channel === ipcChannels.project.list) return false;
|
if (channel === ipcChannels.project.list) return false;
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ void test('createWindows: пульт поверх экрана просмотр
|
|||||||
assert.ok(src.includes("createWindow('control'"));
|
assert.ok(src.includes("createWindow('control'"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void test('createWindows: окно описания сцены закрывается с multi-window и отдельно', () => {
|
||||||
|
const src = readCreateWindows();
|
||||||
|
assert.ok(src.includes('openSceneDescriptionWindow'));
|
||||||
|
assert.ok(src.includes('closeSceneDescriptionWindow'));
|
||||||
|
assert.ok(src.includes("createWindow('sceneDescription')"));
|
||||||
|
assert.match(src, /export function closeMultiWindow[\s\S]*closeSceneDescriptionWindow/);
|
||||||
|
assert.match(src, /kind !== 'presentation' && kind !== 'control'[\s\S]*closeSceneDescriptionWindow/);
|
||||||
|
});
|
||||||
|
|
||||||
void test('createWindows: production — loadFile для HTML (не только file://)', () => {
|
void test('createWindows: production — loadFile для HTML (не только file://)', () => {
|
||||||
const src = readCreateWindows();
|
const src = readCreateWindows();
|
||||||
assert.ok(src.includes('loadFile'));
|
assert.ok(src.includes('loadFile'));
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import { ipcChannels } from '../../shared/ipc/contracts';
|
|||||||
import { getBootSplashWindow } from './bootWindow';
|
import { getBootSplashWindow } from './bootWindow';
|
||||||
import { loadBrandingWindowIcon } from './brandingIcon';
|
import { loadBrandingWindowIcon } from './brandingIcon';
|
||||||
|
|
||||||
type WindowKind = 'editor' | 'presentation' | 'control';
|
type WindowKind = 'editor' | 'presentation' | 'control' | 'sceneDescription';
|
||||||
|
|
||||||
const windows = new Map<WindowKind, BrowserWindow>();
|
const windows = new Map<WindowKind, BrowserWindow>();
|
||||||
|
|
||||||
let appQuitting = false;
|
let appQuitting = false;
|
||||||
|
let pendingSceneDescriptionHtml = '';
|
||||||
|
|
||||||
/** Учитываем окна, которые уже уничтожены при каскадном закрытии (родитель → дочернее). */
|
/** Учитываем окна, которые уже уничтожены при каскадном закрытии (родитель → дочернее). */
|
||||||
function broadcastMultiWindowStateChanged(open: boolean): void {
|
function broadcastMultiWindowStateChanged(open: boolean): void {
|
||||||
@@ -27,6 +28,15 @@ function broadcastMultiWindowStateChanged(open: boolean): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendSceneDescriptionContent(win: BrowserWindow, html: string): void {
|
||||||
|
if (win.isDestroyed() || win.webContents.isDestroyed()) return;
|
||||||
|
try {
|
||||||
|
win.webContents.send(ipcChannels.windows.sceneDescriptionContent, { html });
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Разрешает реальное закрытие окна редактора (выход из приложения). */
|
/** Разрешает реальное закрытие окна редактора (выход из приложения). */
|
||||||
export function markAppQuitting(): void {
|
export function markAppQuitting(): void {
|
||||||
appQuitting = true;
|
appQuitting = true;
|
||||||
@@ -51,6 +61,19 @@ function getRendererHtmlPath(kind: WindowKind): string {
|
|||||||
return path.join(app.getAppPath(), 'dist', 'renderer', `${kind}.html`);
|
return path.join(app.getAppPath(), 'dist', 'renderer', `${kind}.html`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pageNameForKind(kind: WindowKind): string {
|
||||||
|
switch (kind) {
|
||||||
|
case 'editor':
|
||||||
|
return 'editor.html';
|
||||||
|
case 'presentation':
|
||||||
|
return 'presentation.html';
|
||||||
|
case 'control':
|
||||||
|
return 'control.html';
|
||||||
|
case 'sceneDescription':
|
||||||
|
return 'sceneDescription.html';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* В production `loadURL(file://…)` на Windows с asar иногда даёт чёрный экран;
|
* В production `loadURL(file://…)` на Windows с asar иногда даёт чёрный экран;
|
||||||
* `loadFile` корректно открывает HTML из asar и на Windows, и на macOS.
|
* `loadFile` корректно открывает HTML из asar и на Windows, и на macOS.
|
||||||
@@ -58,9 +81,7 @@ function getRendererHtmlPath(kind: WindowKind): string {
|
|||||||
function loadWindowPage(win: BrowserWindow, kind: WindowKind): void {
|
function loadWindowPage(win: BrowserWindow, kind: WindowKind): void {
|
||||||
const dev = process.env.VITE_DEV_SERVER_URL;
|
const dev = process.env.VITE_DEV_SERVER_URL;
|
||||||
if (dev) {
|
if (dev) {
|
||||||
const page =
|
void win.loadURL(new URL(pageNameForKind(kind), dev).toString());
|
||||||
kind === 'editor' ? 'editor.html' : kind === 'presentation' ? 'presentation.html' : 'control.html';
|
|
||||||
void win.loadURL(new URL(page, dev).toString());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void win.loadFile(getRendererHtmlPath(kind));
|
void win.loadFile(getRendererHtmlPath(kind));
|
||||||
@@ -112,12 +133,27 @@ function ensureWindowBecomesVisible(win: BrowserWindow): void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function windowSizeForKind(kind: WindowKind): { width: number; height: number } {
|
||||||
|
if (kind === 'editor') return { width: 1280, height: 800 };
|
||||||
|
if (kind === 'control') return { width: 1200, height: 800 };
|
||||||
|
if (kind === 'sceneDescription') return { width: 720, height: 640 };
|
||||||
|
return { width: 1280, height: 800 };
|
||||||
|
}
|
||||||
|
|
||||||
function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow {
|
function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow {
|
||||||
const deferEditor = kind === 'editor' && opts?.deferVisibility === true;
|
const deferEditor = kind === 'editor' && opts?.deferVisibility === true;
|
||||||
const icon = loadBrandingWindowIcon();
|
const icon = loadBrandingWindowIcon();
|
||||||
|
const size = windowSizeForKind(kind);
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
width: kind === 'editor' ? 1280 : kind === 'control' ? 1200 : 1280,
|
width: size.width,
|
||||||
height: 800,
|
height: size.height,
|
||||||
|
...(kind === 'sceneDescription'
|
||||||
|
? {
|
||||||
|
minWidth: 520,
|
||||||
|
minHeight: 420,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
show: false,
|
show: false,
|
||||||
backgroundColor: '#09090B',
|
backgroundColor: '#09090B',
|
||||||
...(icon ? { icon } : {}),
|
...(icon ? { icon } : {}),
|
||||||
@@ -142,6 +178,9 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
|
|||||||
}
|
}
|
||||||
|
|
||||||
win.setTitle(windowChromeTitle(kind, app.getLocale()));
|
win.setTitle(windowChromeTitle(kind, app.getLocale()));
|
||||||
|
if (kind === 'sceneDescription') {
|
||||||
|
win.setMenuBarVisibility(false);
|
||||||
|
}
|
||||||
|
|
||||||
win.webContents.on('preload-error', (_event, preloadPath, error) => {
|
win.webContents.on('preload-error', (_event, preloadPath, error) => {
|
||||||
console.error(`[preload-error] ${preloadPath}:`, error);
|
console.error(`[preload-error] ${preloadPath}:`, error);
|
||||||
@@ -168,6 +207,9 @@ function createWindow(kind: WindowKind, opts?: CreateWindowOpts): BrowserWindow
|
|||||||
win.on('closed', () => {
|
win.on('closed', () => {
|
||||||
if (kind !== 'presentation' && kind !== 'control') return;
|
if (kind !== 'presentation' && kind !== 'control') return;
|
||||||
const open = windows.has('presentation') || windows.has('control');
|
const open = windows.has('presentation') || windows.has('control');
|
||||||
|
if (!open) {
|
||||||
|
closeSceneDescriptionWindow();
|
||||||
|
}
|
||||||
broadcastMultiWindowStateChanged(open);
|
broadcastMultiWindowStateChanged(open);
|
||||||
});
|
});
|
||||||
windows.set(kind, win);
|
windows.set(kind, win);
|
||||||
@@ -250,6 +292,7 @@ export function openMultiWindow() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function closeMultiWindow(): void {
|
export function closeMultiWindow(): void {
|
||||||
|
closeSceneDescriptionWindow();
|
||||||
const pres = windows.get('presentation');
|
const pres = windows.get('presentation');
|
||||||
const ctrl = windows.get('control');
|
const ctrl = windows.get('control');
|
||||||
if (pres) pres.close();
|
if (pres) pres.close();
|
||||||
@@ -260,6 +303,52 @@ export function isMultiWindowOpen(): boolean {
|
|||||||
return windows.has('presentation') || windows.has('control');
|
return windows.has('presentation') || windows.has('control');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function closeSceneDescriptionWindow(): void {
|
||||||
|
const win = windows.get('sceneDescription');
|
||||||
|
if (win && !win.isDestroyed()) {
|
||||||
|
win.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSceneDescriptionContent(): string {
|
||||||
|
return pendingSceneDescriptionHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Одно окно описания: переиспользовать существующее или создать новое. */
|
||||||
|
export function openSceneDescriptionWindow(html: string): void {
|
||||||
|
pendingSceneDescriptionHtml = html;
|
||||||
|
const existing = windows.get('sceneDescription');
|
||||||
|
if (existing && !existing.isDestroyed()) {
|
||||||
|
if (existing.isMinimized()) existing.restore();
|
||||||
|
existing.show();
|
||||||
|
existing.focus();
|
||||||
|
existing.moveTop();
|
||||||
|
sendSceneDescriptionContent(existing, html);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Держим поверх пульта/презентации, иначе окно уходит под полноэкранный экран просмотра.
|
||||||
|
const parent = windows.get('control') ?? windows.get('presentation');
|
||||||
|
const win = createWindow('sceneDescription', parent ? { parent } : undefined);
|
||||||
|
const { width, height } = win.getBounds();
|
||||||
|
const display = screen.getDisplayMatching(parent?.getBounds() ?? win.getBounds());
|
||||||
|
const { x, y, width: dw, height: dh } = display.workArea;
|
||||||
|
win.setBounds({
|
||||||
|
x: Math.round(x + (dw - width) / 2),
|
||||||
|
y: Math.round(y + (dh - height) / 2),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
win.webContents.once('did-finish-load', () => {
|
||||||
|
sendSceneDescriptionContent(win, pendingSceneDescriptionHtml);
|
||||||
|
if (!win.isDestroyed()) {
|
||||||
|
win.show();
|
||||||
|
win.focus();
|
||||||
|
win.moveTop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function togglePresentationFullscreen(): boolean {
|
export function togglePresentationFullscreen(): boolean {
|
||||||
const pres = windows.get('presentation');
|
const pres = windows.get('presentation');
|
||||||
if (!pres) return false;
|
if (!pres) return false;
|
||||||
|
|||||||
@@ -79,6 +79,98 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bookIcon {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-muted-on-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBackdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: var(--z-modal-backdrop);
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background: var(--color-scrim);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalDialog {
|
||||||
|
position: fixed;
|
||||||
|
z-index: var(--z-modal);
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 520px;
|
||||||
|
max-width: calc(100vw - 32px);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
background: var(--color-surface-elevated);
|
||||||
|
box-shadow: var(--shadow-xl);
|
||||||
|
padding: 16px;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionViewDialog {
|
||||||
|
width: min(720px, calc(100vw - 32px));
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalTitle {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalClose {
|
||||||
|
border: none;
|
||||||
|
background: var(--panel2);
|
||||||
|
color: var(--text2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionViewBody {
|
||||||
|
min-height: 360px;
|
||||||
|
max-height: min(560px, calc(100vh - 200px));
|
||||||
|
overflow: auto;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
background: var(--color-overlay-dark-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionViewProse {
|
||||||
|
color: var(--text0);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
line-height: 1.55;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionViewEmpty {
|
||||||
|
color: var(--text2);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
.radiusRow {
|
.radiusRow {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 100px 1fr 44px;
|
grid-template-columns: 100px 1fr 44px;
|
||||||
|
|||||||
@@ -3,9 +3,14 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 're
|
|||||||
import { pickEraseTargetId } from '../../shared/effectEraserHitTest';
|
import { pickEraseTargetId } from '../../shared/effectEraserHitTest';
|
||||||
import { ipcChannels } from '../../shared/ipc/contracts';
|
import { ipcChannels } from '../../shared/ipc/contracts';
|
||||||
import type { SessionState } from '../../shared/ipc/contracts';
|
import type { SessionState } from '../../shared/ipc/contracts';
|
||||||
import { isNodeInMainStoryline, isNodeInSideStoryline, listSideStoryStarts } from '../../shared/graph/sceneGraphLineage';
|
import {
|
||||||
|
isNodeInMainStoryline,
|
||||||
|
isNodeInSideStoryline,
|
||||||
|
listSideStoryStarts,
|
||||||
|
} from '../../shared/graph/sceneGraphLineage';
|
||||||
import type { GraphNodeId, Scene, SceneId } from '../../shared/types';
|
import type { GraphNodeId, Scene, SceneId } from '../../shared/types';
|
||||||
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
||||||
|
import { isSceneDescriptionEmpty } from '../editor/sceneDescriptionHtml';
|
||||||
import { getDndApi } from '../shared/dndApi';
|
import { getDndApi } from '../shared/dndApi';
|
||||||
import { RotatedImage } from '../shared/RotatedImage';
|
import { RotatedImage } from '../shared/RotatedImage';
|
||||||
import { PixiEffectsOverlay } from '../shared/effects/PxiEffectsOverlay';
|
import { PixiEffectsOverlay } from '../shared/effects/PxiEffectsOverlay';
|
||||||
@@ -48,15 +53,7 @@ function playLightningEffectSound(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function SideStoryTile({
|
function SideStoryTile({ scene, title, onClick }: { scene: Scene; title: string; onClick: () => void }) {
|
||||||
scene,
|
|
||||||
title,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
scene: Scene;
|
|
||||||
title: string;
|
|
||||||
onClick: () => void;
|
|
||||||
}) {
|
|
||||||
const thumbUrl = useAssetUrl(scene.previewThumbAssetId ?? scene.previewAssetId);
|
const thumbUrl = useAssetUrl(scene.previewThumbAssetId ?? scene.previewAssetId);
|
||||||
const previewUrl = useAssetUrl(scene.previewAssetId);
|
const previewUrl = useAssetUrl(scene.previewAssetId);
|
||||||
const imageUrl = thumbUrl ?? (scene.previewAssetType === 'image' ? previewUrl : null);
|
const imageUrl = thumbUrl ?? (scene.previewAssetType === 'image' ? previewUrl : null);
|
||||||
@@ -213,6 +210,8 @@ export function ControlApp() {
|
|||||||
project && session?.currentSceneId ? project.scenes[session.currentSceneId] : undefined;
|
project && session?.currentSceneId ? project.scenes[session.currentSceneId] : undefined;
|
||||||
const isVideoPreviewScene = currentScene?.previewAssetType === 'video';
|
const isVideoPreviewScene = currentScene?.previewAssetType === 'video';
|
||||||
const isDarkenScene = Boolean(currentScene?.darkenScene) && !isVideoPreviewScene;
|
const isDarkenScene = Boolean(currentScene?.darkenScene) && !isVideoPreviewScene;
|
||||||
|
const sceneDescription = currentScene?.description ?? '';
|
||||||
|
const hasSceneDescription = !isSceneDescriptionEmpty(sceneDescription);
|
||||||
const sceneAudioRefs = useMemo(() => currentScene?.media.audios ?? [], [currentScene]);
|
const sceneAudioRefs = useMemo(() => currentScene?.media.audios ?? [], [currentScene]);
|
||||||
// Keep this memo as narrow as possible: project changes on scene switch,
|
// Keep this memo as narrow as possible: project changes on scene switch,
|
||||||
// but campaign audio list/config often does not.
|
// but campaign audio list/config often does not.
|
||||||
@@ -1149,6 +1148,27 @@ export function ControlApp() {
|
|||||||
<Surface className={styles.remote}>
|
<Surface className={styles.remote}>
|
||||||
<div className={styles.remoteTitle}>{t('control.remoteTitle')}</div>
|
<div className={styles.remoteTitle}>{t('control.remoteTitle')}</div>
|
||||||
<div className={styles.spacer12} />
|
<div className={styles.spacer12} />
|
||||||
|
<div className={styles.sectionLabel}>{t('control.instruments')}</div>
|
||||||
|
<div className={styles.spacer8} />
|
||||||
|
<div className={styles.iconRow}>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
iconOnly
|
||||||
|
disabled={!hasSceneDescription}
|
||||||
|
title={hasSceneDescription ? t('control.descriptionTool') : t('control.descriptionMissing')}
|
||||||
|
ariaLabel={hasSceneDescription ? t('control.descriptionTool') : t('control.descriptionMissing')}
|
||||||
|
onClick={() => {
|
||||||
|
void api.invoke(ipcChannels.windows.openSceneDescription, { html: sceneDescription }).catch((err) => {
|
||||||
|
console.error('[control] openSceneDescription failed', err);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={styles.iconGlyph} aria-hidden>
|
||||||
|
📖
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.spacer12} />
|
||||||
{!isVideoPreviewScene ? (
|
{!isVideoPreviewScene ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles.sectionLabel}>{t('control.effects')}</div>
|
<div className={styles.sectionLabel}>{t('control.effects')}</div>
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ void test('ControlApp: звук облака яда (public/oblako-yada.mp3)', (
|
|||||||
|
|
||||||
void test('ControlApp: эффекты в пульте, иконки с тултипами и подписью для a11y', () => {
|
void test('ControlApp: эффекты в пульте, иконки с тултипами и подписью для a11y', () => {
|
||||||
const src = readControlApp();
|
const src = readControlApp();
|
||||||
|
assert.ok(src.includes("t('control.instruments')"));
|
||||||
|
assert.ok(src.includes("t('control.descriptionTool')"));
|
||||||
|
assert.ok(src.includes("t('control.descriptionMissing')"));
|
||||||
|
assert.ok(src.includes('openSceneDescription'));
|
||||||
|
assert.ok(!src.includes('SceneDescriptionViewModal'));
|
||||||
assert.ok(src.includes("t('control.effects')"));
|
assert.ok(src.includes("t('control.effects')"));
|
||||||
assert.ok(src.includes("t('control.tools')"));
|
assert.ok(src.includes("t('control.tools')"));
|
||||||
assert.ok(src.includes("t('control.fieldEffects')"));
|
assert.ok(src.includes("t('control.fieldEffects')"));
|
||||||
@@ -68,8 +73,13 @@ void test('ControlApp: эффекты в пульте, иконки с тулт
|
|||||||
assert.ok(src.includes("title={t('control.clearEffects')}"));
|
assert.ok(src.includes("title={t('control.clearEffects')}"));
|
||||||
assert.ok(src.includes("ariaLabel={t('control.clearEffects')}"));
|
assert.ok(src.includes("ariaLabel={t('control.clearEffects')}"));
|
||||||
assert.ok(src.includes('#e5484d'));
|
assert.ok(src.includes('#e5484d'));
|
||||||
|
const instruments = src.indexOf("t('control.instruments')");
|
||||||
const fx = src.indexOf("t('control.effects')");
|
const fx = src.indexOf("t('control.effects')");
|
||||||
const story = src.indexOf("t('control.storyLine')");
|
const story = src.indexOf("t('control.storyLine')");
|
||||||
|
assert.ok(
|
||||||
|
instruments !== -1 && fx !== -1 && instruments < fx,
|
||||||
|
'Блок инструментов должен быть выше эффектов',
|
||||||
|
);
|
||||||
assert.ok(fx !== -1 && story !== -1 && fx < story, 'Блок эффектов должен быть выше сюжетной линии');
|
assert.ok(fx !== -1 && story !== -1 && fx < story, 'Блок эффектов должен быть выше сюжетной линии');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -645,6 +645,19 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.labelRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelRow .labelSm {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.spacer8 {
|
.spacer8 {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
@@ -660,6 +673,75 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.descriptionEmpty {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px dashed var(--stroke-2);
|
||||||
|
background: var(--color-overlay-dark-2);
|
||||||
|
color: var(--text2);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionPreview {
|
||||||
|
max-height: 4.6em;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
background: var(--color-overlay-dark-3);
|
||||||
|
color: var(--text1);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: 1.45;
|
||||||
|
word-break: break-word;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionPreview :global(p),
|
||||||
|
.descriptionPreview :global(h2),
|
||||||
|
.descriptionPreview :global(h3),
|
||||||
|
.descriptionPreview :global(ul),
|
||||||
|
.descriptionPreview :global(ol),
|
||||||
|
.descriptionPreview :global(blockquote),
|
||||||
|
.descriptionPreview :global(pre) {
|
||||||
|
margin: 0 0 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionPreview :global(p:last-child),
|
||||||
|
.descriptionPreview :global(h2:last-child),
|
||||||
|
.descriptionPreview :global(h3:last-child),
|
||||||
|
.descriptionPreview :global(ul:last-child),
|
||||||
|
.descriptionPreview :global(ol:last-child),
|
||||||
|
.descriptionPreview :global(blockquote:last-child),
|
||||||
|
.descriptionPreview :global(pre:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionPreview :global(h2),
|
||||||
|
.descriptionPreview :global(h3) {
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionPreview :global(ul),
|
||||||
|
.descriptionPreview :global(ol) {
|
||||||
|
padding-left: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionPreview :global(strong),
|
||||||
|
.descriptionPreview :global(b) {
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionPreview :global(a) {
|
||||||
|
color: var(--accent2);
|
||||||
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
color: var(--text2);
|
color: var(--text2);
|
||||||
font-size: var(--text-xs);
|
font-size: var(--text-xs);
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import React, { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { startTransition, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { moveSceneInListOrder, reconcileSceneListOrder } from '../../shared/graph/sceneListOrder';
|
||||||
|
import type {
|
||||||
|
SceneImportResolution,
|
||||||
|
StorylineImportMergeReport,
|
||||||
|
StorylineSelection,
|
||||||
|
} from '../../shared/graph/storylineExportImport';
|
||||||
import {
|
import {
|
||||||
ipcChannels,
|
ipcChannels,
|
||||||
type UpdaterCheckResponse,
|
type UpdaterCheckResponse,
|
||||||
@@ -25,6 +31,8 @@ import { Button, Input } from '../shared/ui/controls';
|
|||||||
import { LayoutShell } from '../shared/ui/LayoutShell';
|
import { LayoutShell } from '../shared/ui/LayoutShell';
|
||||||
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
||||||
|
|
||||||
|
import { AppAboutModal, InstructionsModal } from './about/EditorAboutModals';
|
||||||
|
import styles from './EditorApp.module.css';
|
||||||
import {
|
import {
|
||||||
filterAudioFilePaths,
|
filterAudioFilePaths,
|
||||||
partitionSceneMediaDrops,
|
partitionSceneMediaDrops,
|
||||||
@@ -32,9 +40,20 @@ import {
|
|||||||
sceneTitleFromMediaPath,
|
sceneTitleFromMediaPath,
|
||||||
useFileDropZone,
|
useFileDropZone,
|
||||||
} from './fileDrop';
|
} from './fileDrop';
|
||||||
|
import { buildNextSceneCardById } from './graph/sceneCardById';
|
||||||
import { moveSceneInListOrder, reconcileSceneListOrder } from '../../shared/graph/sceneListOrder';
|
import {
|
||||||
import type { SceneImportResolution, StorylineImportMergeReport, StorylineSelection } from '../../shared/graph/storylineExportImport';
|
DND_SCENE_ID_MIME,
|
||||||
|
SceneGraph,
|
||||||
|
type SceneGraphSceneCard,
|
||||||
|
type SceneGraphUiStrings,
|
||||||
|
} from './graph/SceneGraph';
|
||||||
|
import type { HelpSectionId } from './help/helpSections';
|
||||||
|
import { useEditorI18n } from './i18n/EditorI18nContext';
|
||||||
|
import { EulaModal, LicenseAboutModal, LicenseTokenModal } from './license/EditorLicenseModals';
|
||||||
|
import { isSceneDescriptionEmpty, sanitizeSceneDescriptionHtml } from './sceneDescriptionHtml';
|
||||||
|
import { SceneDescriptionModal } from './SceneDescriptionModal';
|
||||||
|
import type { ProjectNoticeCode } from './state/projectState';
|
||||||
|
import { useProjectState } from './state/projectState';
|
||||||
import {
|
import {
|
||||||
buildSceneResolutionsForImport,
|
buildSceneResolutionsForImport,
|
||||||
computeImportConflicts,
|
computeImportConflicts,
|
||||||
@@ -47,20 +66,6 @@ import {
|
|||||||
type ImportPeekResult,
|
type ImportPeekResult,
|
||||||
type ImportSourceSelection,
|
type ImportSourceSelection,
|
||||||
} from './StorylineTransferModals';
|
} from './StorylineTransferModals';
|
||||||
import { AppAboutModal, InstructionsModal } from './about/EditorAboutModals';
|
|
||||||
import styles from './EditorApp.module.css';
|
|
||||||
import { buildNextSceneCardById } from './graph/sceneCardById';
|
|
||||||
import {
|
|
||||||
DND_SCENE_ID_MIME,
|
|
||||||
SceneGraph,
|
|
||||||
type SceneGraphSceneCard,
|
|
||||||
type SceneGraphUiStrings,
|
|
||||||
} from './graph/SceneGraph';
|
|
||||||
import type { HelpSectionId } from './help/helpSections';
|
|
||||||
import { useEditorI18n } from './i18n/EditorI18nContext';
|
|
||||||
import { EulaModal, LicenseAboutModal, LicenseTokenModal } from './license/EditorLicenseModals';
|
|
||||||
import type { ProjectNoticeCode } from './state/projectState';
|
|
||||||
import { useProjectState } from './state/projectState';
|
|
||||||
|
|
||||||
type SceneCard = {
|
type SceneCard = {
|
||||||
id: SceneId;
|
id: SceneId;
|
||||||
@@ -2238,8 +2243,14 @@ function SceneInspector({
|
|||||||
onSideStoryLineTitleChange,
|
onSideStoryLineTitleChange,
|
||||||
}: SceneInspectorProps) {
|
}: SceneInspectorProps) {
|
||||||
const { t } = useEditorI18n();
|
const { t } = useEditorI18n();
|
||||||
|
const [descriptionModalOpen, setDescriptionModalOpen] = useState(false);
|
||||||
const previewUrl = useAssetUrl(previewAssetId);
|
const previewUrl = useAssetUrl(previewAssetId);
|
||||||
const audioById = useMemo(() => new Map(audioRefs.map((a) => [a.assetId, a])), [audioRefs]);
|
const audioById = useMemo(() => new Map(audioRefs.map((a) => [a.assetId, a])), [audioRefs]);
|
||||||
|
const descriptionEmpty = isSceneDescriptionEmpty(description);
|
||||||
|
const descriptionPreviewHtml = useMemo(
|
||||||
|
() => (descriptionEmpty ? '' : sanitizeSceneDescriptionHtml(description)),
|
||||||
|
[description, descriptionEmpty],
|
||||||
|
);
|
||||||
const previewDrop = useFileDropZone({
|
const previewDrop = useFileDropZone({
|
||||||
disabled: previewBusy,
|
disabled: previewBusy,
|
||||||
onDropPaths: (paths) => {
|
onDropPaths: (paths) => {
|
||||||
@@ -2256,12 +2267,40 @@ function SceneInspector({
|
|||||||
<div className={styles.labelSm}>{t('scene.title')}</div>
|
<div className={styles.labelSm}>{t('scene.title')}</div>
|
||||||
<Input value={title} onChange={onTitleChange} />
|
<Input value={title} onChange={onTitleChange} />
|
||||||
<div className={styles.spacer8} />
|
<div className={styles.spacer8} />
|
||||||
|
<div className={styles.labelRow}>
|
||||||
<div className={styles.labelSm}>{t('scene.description')}</div>
|
<div className={styles.labelSm}>{t('scene.description')}</div>
|
||||||
<textarea
|
<Button
|
||||||
className={styles.textarea}
|
iconOnly
|
||||||
value={description}
|
title={t('common.edit')}
|
||||||
onChange={(e) => onDescriptionChange(e.target.value)}
|
ariaLabel={t('common.edit')}
|
||||||
|
onClick={() => setDescriptionModalOpen(true)}
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width={14} height={14} aria-hidden>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"
|
||||||
/>
|
/>
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{descriptionEmpty ? (
|
||||||
|
<div className={styles.descriptionEmpty}>{t('scene.descriptionEmpty')}</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className={styles.descriptionPreview}
|
||||||
|
dangerouslySetInnerHTML={{ __html: descriptionPreviewHtml }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{descriptionModalOpen ? (
|
||||||
|
<SceneDescriptionModal
|
||||||
|
initialHtml={description}
|
||||||
|
onClose={() => setDescriptionModalOpen(false)}
|
||||||
|
onSave={(html) => {
|
||||||
|
onDescriptionChange(html);
|
||||||
|
setDescriptionModalOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{sideStoryStartNodes.length > 0 ? (
|
{sideStoryStartNodes.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className={styles.spacer8} />
|
<div className={styles.spacer8} />
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
.dialog {
|
||||||
|
width: min(720px, calc(100vw - 32px));
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorShell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
min-height: 360px;
|
||||||
|
max-height: min(560px, calc(100vh - 200px));
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
background: var(--color-overlay-dark-3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--stroke);
|
||||||
|
background: var(--color-panel-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarGroup {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbarSep {
|
||||||
|
width: 1px;
|
||||||
|
align-self: stretch;
|
||||||
|
margin: 2px 4px;
|
||||||
|
background: var(--stroke-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolBtn {
|
||||||
|
min-width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text1);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolBtn:hover:not(:disabled) {
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolBtnActive {
|
||||||
|
border-color: var(--accent-border);
|
||||||
|
background: var(--accent-fill-soft);
|
||||||
|
color: var(--accent2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolBtn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolIcon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContent {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContent :global(.tiptap) {
|
||||||
|
min-height: 280px;
|
||||||
|
outline: none;
|
||||||
|
color: var(--text0);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
line-height: 1.55;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContent :global(.tiptap p.is-editor-empty:first-child::before) {
|
||||||
|
color: var(--text2);
|
||||||
|
content: attr(data-placeholder);
|
||||||
|
float: left;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shared rich-text look (editor + inspector preview) */
|
||||||
|
.prose :global(p) {
|
||||||
|
margin: 0 0 0.65em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(p:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(h2),
|
||||||
|
.prose :global(h3) {
|
||||||
|
margin: 0.85em 0 0.4em;
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text0);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(h2) {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(h3) {
|
||||||
|
font-size: 1.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(ul),
|
||||||
|
.prose :global(ol) {
|
||||||
|
margin: 0 0 0.65em;
|
||||||
|
padding-left: 1.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(li) {
|
||||||
|
margin: 0.15em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(strong) {
|
||||||
|
font-weight: 800;
|
||||||
|
color: var(--text0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(em) {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(u) {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(a) {
|
||||||
|
color: var(--accent2);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(blockquote) {
|
||||||
|
margin: 0 0 0.65em;
|
||||||
|
padding: 0.35em 0 0.35em 0.85em;
|
||||||
|
border-left: 3px solid var(--accent-border);
|
||||||
|
color: var(--text1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(code) {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.92em;
|
||||||
|
padding: 0.1em 0.35em;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--color-overlay-dark-4);
|
||||||
|
color: var(--text0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(pre) {
|
||||||
|
margin: 0 0 0.65em;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
border: 1px solid var(--stroke);
|
||||||
|
background: var(--color-overlay-dark-5);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :global(pre code) {
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import Placeholder from '@tiptap/extension-placeholder';
|
||||||
|
import { EditorContent, useEditor, useEditorState } from '@tiptap/react';
|
||||||
|
import StarterKit from '@tiptap/starter-kit';
|
||||||
|
import React, { useEffect, useMemo } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { Button } from '../shared/ui/controls';
|
||||||
|
|
||||||
|
import styles from './EditorApp.module.css';
|
||||||
|
import { useEditorI18n } from './i18n/EditorI18nContext';
|
||||||
|
import { normalizeSceneDescriptionHtml } from './sceneDescriptionHtml';
|
||||||
|
import modalStyles from './SceneDescriptionModal.module.css';
|
||||||
|
|
||||||
|
type SceneDescriptionModalProps = {
|
||||||
|
initialHtml: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onSave: (html: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ToolbarIcon({ path, size = 14 }: { path: string; size?: number }) {
|
||||||
|
return (
|
||||||
|
<svg className={modalStyles.toolIcon} viewBox="0 0 24 24" width={size} height={size} aria-hidden>
|
||||||
|
<path fill="currentColor" d={path} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolButton({
|
||||||
|
active = false,
|
||||||
|
disabled = false,
|
||||||
|
title,
|
||||||
|
onClick,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
active?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
title: string;
|
||||||
|
onClick: () => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
aria-pressed={active}
|
||||||
|
disabled={disabled}
|
||||||
|
className={[modalStyles.toolBtn, active ? modalStyles.toolBtnActive : ''].filter(Boolean).join(' ')}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SceneDescriptionModal({ initialHtml, onClose, onSave }: SceneDescriptionModalProps) {
|
||||||
|
const { t } = useEditorI18n();
|
||||||
|
|
||||||
|
const extensions = useMemo(
|
||||||
|
() => [
|
||||||
|
StarterKit.configure({
|
||||||
|
heading: { levels: [2, 3] },
|
||||||
|
codeBlock: false,
|
||||||
|
link: {
|
||||||
|
openOnClick: false,
|
||||||
|
autolink: true,
|
||||||
|
HTMLAttributes: {
|
||||||
|
rel: 'noopener noreferrer',
|
||||||
|
target: '_blank',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Placeholder.configure({
|
||||||
|
placeholder: t('scene.descriptionPlaceholder'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions,
|
||||||
|
content: initialHtml || '',
|
||||||
|
immediatelyRender: true,
|
||||||
|
shouldRerenderOnTransaction: true,
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: [modalStyles.prose, 'tiptap'].join(' '),
|
||||||
|
'aria-label': t('scene.descriptionModalTitle'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const toolbarState = useEditorState({
|
||||||
|
editor,
|
||||||
|
selector: ({ editor: ed }) => ({
|
||||||
|
bold: ed.isActive('bold'),
|
||||||
|
italic: ed.isActive('italic'),
|
||||||
|
underline: ed.isActive('underline'),
|
||||||
|
bulletList: ed.isActive('bulletList'),
|
||||||
|
orderedList: ed.isActive('orderedList'),
|
||||||
|
h2: ed.isActive('heading', { level: 2 }),
|
||||||
|
h3: ed.isActive('heading', { level: 3 }),
|
||||||
|
blockquote: ed.isActive('blockquote'),
|
||||||
|
canLink: ed.isEditable,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onSave(normalizeSceneDescriptionHtml(editor.getHTML()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLink = () => {
|
||||||
|
const prev = editor.getAttributes('link').href as string | undefined;
|
||||||
|
const next = window.prompt(t('scene.descriptionLinkPrompt'), prev ?? 'https://');
|
||||||
|
if (next === null) return;
|
||||||
|
const trimmed = next.trim();
|
||||||
|
if (trimmed === '') {
|
||||||
|
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor.chain().focus().extendMarkRange('link').setLink({ href: trimmed }).run();
|
||||||
|
};
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t('common.close')}
|
||||||
|
onClick={onClose}
|
||||||
|
className={styles.modalBackdrop}
|
||||||
|
/>
|
||||||
|
<div role="dialog" aria-modal="true" className={[styles.modalDialog, modalStyles.dialog].join(' ')}>
|
||||||
|
<div className={styles.modalHeader}>
|
||||||
|
<div className={styles.modalTitle}>{t('scene.descriptionModalTitle')}</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t('common.close')}
|
||||||
|
onClick={onClose}
|
||||||
|
className={styles.modalClose}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={modalStyles.editorShell}>
|
||||||
|
<div className={modalStyles.toolbar} role="toolbar" aria-label={t('scene.descriptionToolbar')}>
|
||||||
|
<div className={modalStyles.toolbarGroup}>
|
||||||
|
<ToolButton
|
||||||
|
title={t('scene.descriptionBold')}
|
||||||
|
active={toolbarState.bold}
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
>
|
||||||
|
<strong>B</strong>
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
title={t('scene.descriptionItalic')}
|
||||||
|
active={toolbarState.italic}
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
>
|
||||||
|
<em>I</em>
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
title={t('scene.descriptionUnderline')}
|
||||||
|
active={toolbarState.underline}
|
||||||
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
|
>
|
||||||
|
<span style={{ textDecoration: 'underline' }}>U</span>
|
||||||
|
</ToolButton>
|
||||||
|
</div>
|
||||||
|
<div className={modalStyles.toolbarSep} />
|
||||||
|
<div className={modalStyles.toolbarGroup}>
|
||||||
|
<ToolButton
|
||||||
|
title={t('scene.descriptionHeading2')}
|
||||||
|
active={toolbarState.h2}
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
>
|
||||||
|
H2
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
title={t('scene.descriptionHeading3')}
|
||||||
|
active={toolbarState.h3}
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||||
|
>
|
||||||
|
H3
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
title={t('scene.descriptionQuote')}
|
||||||
|
active={toolbarState.blockquote}
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
>
|
||||||
|
<ToolbarIcon path="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z" />
|
||||||
|
</ToolButton>
|
||||||
|
</div>
|
||||||
|
<div className={modalStyles.toolbarSep} />
|
||||||
|
<div className={modalStyles.toolbarGroup}>
|
||||||
|
<ToolButton
|
||||||
|
title={t('scene.descriptionBulletList')}
|
||||||
|
active={toolbarState.bulletList}
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
>
|
||||||
|
<ToolbarIcon path="M4 6h2v2H4V6zm0 5h2v2H4v-2zm0 5h2v2H4v-2zm4-10h12v2H8V6zm0 5h12v2H8v-2zm0 5h12v2H8v-2z" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
title={t('scene.descriptionOrderedList')}
|
||||||
|
active={toolbarState.orderedList}
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
>
|
||||||
|
<ToolbarIcon path="M2 17h2v.5H3v1h1v.5H2v1h3v-4H2v1zm1-9h1V4H2v1h1v3zm-1 3h1.8L2 13.1V14h3v-1H3.2L5 10.9V10H2v1zm5-6v2h14V5H7zm0 14h14v-2H7v2zm0-6h14v-2H7v2z" />
|
||||||
|
</ToolButton>
|
||||||
|
<ToolButton
|
||||||
|
title={t('scene.descriptionLink')}
|
||||||
|
disabled={!toolbarState.canLink}
|
||||||
|
onClick={setLink}
|
||||||
|
>
|
||||||
|
<ToolbarIcon path="M3.9 12a5 5 0 0 1 5-5h4v2h-4a3 3 0 1 0 0 6h4v2h-4a5 5 0 0 1-5-5zm7-1h6v2h-6v-2zm5-4h-4v2h4a3 3 0 1 1 0 6h-4v2h4a5 5 0 0 0 0-10z" />
|
||||||
|
</ToolButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={modalStyles.editorContent}>
|
||||||
|
<EditorContent editor={editor} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.modalFooter}>
|
||||||
|
<Button onClick={onClose}>{t('common.cancel')}</Button>
|
||||||
|
<Button variant="primary" onClick={handleSave}>
|
||||||
|
{t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
'common.close': 'Закрыть',
|
'common.close': 'Закрыть',
|
||||||
'common.cancel': 'Отмена',
|
'common.cancel': 'Отмена',
|
||||||
'common.save': 'Сохранить',
|
'common.save': 'Сохранить',
|
||||||
|
'common.edit': 'Редактировать',
|
||||||
'common.understood': 'Понятно',
|
'common.understood': 'Понятно',
|
||||||
'common.message': 'Сообщение',
|
'common.message': 'Сообщение',
|
||||||
'common.error': 'Ошибка',
|
'common.error': 'Ошибка',
|
||||||
@@ -325,6 +326,20 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
|
|
||||||
'scene.title': 'НАЗВАНИЕ СЦЕНЫ',
|
'scene.title': 'НАЗВАНИЕ СЦЕНЫ',
|
||||||
'scene.description': 'ОПИСАНИЕ',
|
'scene.description': 'ОПИСАНИЕ',
|
||||||
|
'scene.descriptionEmpty': 'описание отсутствует',
|
||||||
|
'scene.descriptionModalTitle': 'Описание сцены',
|
||||||
|
'scene.descriptionPlaceholder': 'Введите описание сцены…',
|
||||||
|
'scene.descriptionToolbar': 'Форматирование',
|
||||||
|
'scene.descriptionBold': 'Жирный',
|
||||||
|
'scene.descriptionItalic': 'Курсив',
|
||||||
|
'scene.descriptionUnderline': 'Подчёркнутый',
|
||||||
|
'scene.descriptionHeading2': 'Заголовок',
|
||||||
|
'scene.descriptionHeading3': 'Подзаголовок',
|
||||||
|
'scene.descriptionQuote': 'Цитата',
|
||||||
|
'scene.descriptionBulletList': 'Маркированный список',
|
||||||
|
'scene.descriptionOrderedList': 'Нумерованный список',
|
||||||
|
'scene.descriptionLink': 'Ссылка',
|
||||||
|
'scene.descriptionLinkPrompt': 'URL ссылки',
|
||||||
'scene.preview': 'ПРЕВЬЮ СЦЕНЫ',
|
'scene.preview': 'ПРЕВЬЮ СЦЕНЫ',
|
||||||
'scene.previewHint': 'Файл изображения (PNG, JPG, WebP, GIF и т.д.).',
|
'scene.previewHint': 'Файл изображения (PNG, JPG, WebP, GIF и т.д.).',
|
||||||
'scene.previewEmpty': 'Превью не задано',
|
'scene.previewEmpty': 'Превью не задано',
|
||||||
@@ -369,6 +384,9 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
'scene.sideStoryLineTitle': 'Название побочной линии',
|
'scene.sideStoryLineTitle': 'Название побочной линии',
|
||||||
|
|
||||||
'control.remoteTitle': 'ПУЛЬТ УПРАВЛЕНИЯ',
|
'control.remoteTitle': 'ПУЛЬТ УПРАВЛЕНИЯ',
|
||||||
|
'control.instruments': 'ИНСТРУМЕНТЫ',
|
||||||
|
'control.descriptionTool': 'Описание',
|
||||||
|
'control.descriptionMissing': 'Описание отсутствует',
|
||||||
'control.effects': 'ЭФФЕКТЫ',
|
'control.effects': 'ЭФФЕКТЫ',
|
||||||
'control.tools': 'Инструменты',
|
'control.tools': 'Инструменты',
|
||||||
'control.fieldEffects': 'Эффекты поля',
|
'control.fieldEffects': 'Эффекты поля',
|
||||||
@@ -439,6 +457,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
'common.close': 'Close',
|
'common.close': 'Close',
|
||||||
'common.cancel': 'Cancel',
|
'common.cancel': 'Cancel',
|
||||||
'common.save': 'Save',
|
'common.save': 'Save',
|
||||||
|
'common.edit': 'Edit',
|
||||||
'common.understood': 'OK',
|
'common.understood': 'OK',
|
||||||
'common.message': 'Message',
|
'common.message': 'Message',
|
||||||
'common.error': 'Error',
|
'common.error': 'Error',
|
||||||
@@ -710,6 +729,20 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
|
|
||||||
'scene.title': 'SCENE TITLE',
|
'scene.title': 'SCENE TITLE',
|
||||||
'scene.description': 'DESCRIPTION',
|
'scene.description': 'DESCRIPTION',
|
||||||
|
'scene.descriptionEmpty': 'no description',
|
||||||
|
'scene.descriptionModalTitle': 'Scene description',
|
||||||
|
'scene.descriptionPlaceholder': 'Enter scene description…',
|
||||||
|
'scene.descriptionToolbar': 'Formatting',
|
||||||
|
'scene.descriptionBold': 'Bold',
|
||||||
|
'scene.descriptionItalic': 'Italic',
|
||||||
|
'scene.descriptionUnderline': 'Underline',
|
||||||
|
'scene.descriptionHeading2': 'Heading',
|
||||||
|
'scene.descriptionHeading3': 'Subheading',
|
||||||
|
'scene.descriptionQuote': 'Quote',
|
||||||
|
'scene.descriptionBulletList': 'Bullet list',
|
||||||
|
'scene.descriptionOrderedList': 'Numbered list',
|
||||||
|
'scene.descriptionLink': 'Link',
|
||||||
|
'scene.descriptionLinkPrompt': 'Link URL',
|
||||||
'scene.preview': 'SCENE PREVIEW',
|
'scene.preview': 'SCENE PREVIEW',
|
||||||
'scene.previewHint': 'Image file (PNG, JPG, WebP, GIF, etc.).',
|
'scene.previewHint': 'Image file (PNG, JPG, WebP, GIF, etc.).',
|
||||||
'scene.previewEmpty': 'No preview',
|
'scene.previewEmpty': 'No preview',
|
||||||
@@ -753,6 +786,9 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
|||||||
'graph.runFromScene': 'Start from this scene',
|
'graph.runFromScene': 'Start from this scene',
|
||||||
|
|
||||||
'control.remoteTitle': 'CONTROL PANEL',
|
'control.remoteTitle': 'CONTROL PANEL',
|
||||||
|
'control.instruments': 'TOOLS',
|
||||||
|
'control.descriptionTool': 'Description',
|
||||||
|
'control.descriptionMissing': 'No description',
|
||||||
'control.effects': 'EFFECTS',
|
'control.effects': 'EFFECTS',
|
||||||
'control.tools': 'Tools',
|
'control.tools': 'Tools',
|
||||||
'control.fieldEffects': 'Field effects',
|
'control.fieldEffects': 'Field effects',
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { isSceneDescriptionEmpty, normalizeSceneDescriptionHtml } from './sceneDescriptionHtml';
|
||||||
|
|
||||||
|
void test('isSceneDescriptionEmpty treats blank and empty paragraphs as empty', () => {
|
||||||
|
assert.equal(isSceneDescriptionEmpty(''), true);
|
||||||
|
assert.equal(isSceneDescriptionEmpty(' '), true);
|
||||||
|
assert.equal(isSceneDescriptionEmpty('<p></p>'), true);
|
||||||
|
assert.equal(isSceneDescriptionEmpty('<p><br></p>'), true);
|
||||||
|
assert.equal(isSceneDescriptionEmpty('<p> </p>'), true);
|
||||||
|
assert.equal(isSceneDescriptionEmpty('<p>Hi</p>'), false);
|
||||||
|
assert.equal(isSceneDescriptionEmpty('plain'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('normalizeSceneDescriptionHtml clears empty markup', () => {
|
||||||
|
assert.equal(normalizeSceneDescriptionHtml('<p></p>'), '');
|
||||||
|
assert.equal(normalizeSceneDescriptionHtml('<p>Ok</p>'), '<p>Ok</p>');
|
||||||
|
});
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/** Detect empty TipTap / legacy plain description values. */
|
||||||
|
export function isSceneDescriptionEmpty(html: string | null | undefined): boolean {
|
||||||
|
if (html == null) return true;
|
||||||
|
const trimmed = html.trim();
|
||||||
|
if (trimmed === '') return true;
|
||||||
|
const text = trimmed
|
||||||
|
.replace(/<br\s*\/?>/gi, ' ')
|
||||||
|
.replace(/<\/(p|div|h[1-6]|li|blockquote)>/gi, ' ')
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/ /gi, ' ')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
return text.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Persist empty editor as '' instead of empty paragraph markup. */
|
||||||
|
export function normalizeSceneDescriptionHtml(html: string): string {
|
||||||
|
return isSceneDescriptionEmpty(html) ? '' : html.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALLOWED_TAGS = new Set([
|
||||||
|
'P',
|
||||||
|
'BR',
|
||||||
|
'STRONG',
|
||||||
|
'B',
|
||||||
|
'EM',
|
||||||
|
'I',
|
||||||
|
'U',
|
||||||
|
'S',
|
||||||
|
'H2',
|
||||||
|
'H3',
|
||||||
|
'UL',
|
||||||
|
'OL',
|
||||||
|
'LI',
|
||||||
|
'BLOCKQUOTE',
|
||||||
|
'A',
|
||||||
|
'CODE',
|
||||||
|
'SPAN',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** Sanitize TipTap HTML for safe preview rendering. */
|
||||||
|
export function sanitizeSceneDescriptionHtml(html: string): string {
|
||||||
|
if (typeof document === 'undefined') return html;
|
||||||
|
if (isSceneDescriptionEmpty(html)) return '';
|
||||||
|
if (!/<[a-z][\s\S]*>/i.test(html)) {
|
||||||
|
const esc = document.createElement('div');
|
||||||
|
esc.textContent = html;
|
||||||
|
return esc.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = document.createElement('template');
|
||||||
|
template.innerHTML = html.trim();
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.appendChild(template.content.cloneNode(true));
|
||||||
|
|
||||||
|
const walk = (node: Node) => {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
const el = node as HTMLElement;
|
||||||
|
const tag = el.tagName;
|
||||||
|
if (!ALLOWED_TAGS.has(tag)) {
|
||||||
|
const parent = el.parentNode;
|
||||||
|
if (parent) {
|
||||||
|
while (el.firstChild) parent.insertBefore(el.firstChild, el);
|
||||||
|
parent.removeChild(el);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const attr of [...el.attributes]) {
|
||||||
|
const name = attr.name.toLowerCase();
|
||||||
|
if (tag === 'A' && (name === 'href' || name === 'target' || name === 'rel')) {
|
||||||
|
if (name === 'href') {
|
||||||
|
const href = attr.value.trim();
|
||||||
|
if (!/^(https?:|mailto:)/i.test(href)) {
|
||||||
|
el.removeAttribute(attr.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
el.removeAttribute(attr.name);
|
||||||
|
}
|
||||||
|
if (tag === 'A') {
|
||||||
|
el.setAttribute('rel', 'noopener noreferrer');
|
||||||
|
el.setAttribute('target', '_blank');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const child of [...node.childNodes]) walk(child);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Walk children only — never treat the container as a removable tag
|
||||||
|
// (that would unwrap it and leave container.innerHTML empty).
|
||||||
|
for (const child of [...container.childNodes]) walk(child);
|
||||||
|
return container.innerHTML;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" href="/app-window-icon.png" type="image/png" />
|
||||||
|
<title>TTRPG - Scene description</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/sceneDescription/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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>,
|
||||||
|
);
|
||||||
@@ -13,6 +13,12 @@
|
|||||||
opacity: 0.45;
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabledTipHost {
|
||||||
|
display: inline-flex;
|
||||||
|
vertical-align: middle;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.buttonPrimary {
|
.buttonPrimary {
|
||||||
border: 1px solid var(--accent-border);
|
border: 1px solid var(--accent-border);
|
||||||
background: var(--accent-fill-solid);
|
background: var(--accent-fill-solid);
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ void test('Button: тултип через портал (title), не тольк
|
|||||||
assert.ok(src.includes('role="tooltip"'));
|
assert.ok(src.includes('role="tooltip"'));
|
||||||
assert.ok(src.includes('onMouseEnter={showTip}'));
|
assert.ok(src.includes('onMouseEnter={showTip}'));
|
||||||
assert.ok(src.includes('document.body'));
|
assert.ok(src.includes('document.body'));
|
||||||
|
assert.ok(src.includes('disabledTipHost'), 'disabled + title → хост для hover-тултипа');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ export function Button({
|
|||||||
tooltipPlacement = 'top',
|
tooltipPlacement = 'top',
|
||||||
}: ButtonProps) {
|
}: ButtonProps) {
|
||||||
const btnRef = useRef<HTMLButtonElement | null>(null);
|
const btnRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
const hostRef = useRef<HTMLSpanElement | null>(null);
|
||||||
const [tipPos, setTipPos] = useState<{ x: number; y: number } | null>(null);
|
const [tipPos, setTipPos] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
const showTip = useCallback(() => {
|
const showTip = useCallback(() => {
|
||||||
if (disabled || !title) return;
|
if (!title) return;
|
||||||
const el = btnRef.current;
|
const el = disabled ? hostRef.current : btnRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
if (tooltipPlacement === 'bottom-left') {
|
if (tooltipPlacement === 'bottom-left') {
|
||||||
@@ -66,8 +67,7 @@ export function Button({
|
|||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
const button = (
|
||||||
<>
|
|
||||||
<button
|
<button
|
||||||
ref={btnRef}
|
ref={btnRef}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -75,13 +75,30 @@ export function Button({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
onClick={disabled ? undefined : onClick}
|
onClick={disabled ? undefined : onClick}
|
||||||
onMouseEnter={showTip}
|
onMouseEnter={disabled ? undefined : showTip}
|
||||||
onMouseLeave={hideTip}
|
onMouseLeave={disabled ? undefined : hideTip}
|
||||||
onFocus={showTip}
|
onFocus={showTip}
|
||||||
onBlur={hideTip}
|
onBlur={hideTip}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Disabled buttons don't receive mouse events — host span keeps tooltip usable.
|
||||||
|
if (disabled && title) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span ref={hostRef} className={styles.disabledTipHost} onMouseEnter={showTip} onMouseLeave={hideTip}>
|
||||||
|
{button}
|
||||||
|
</span>
|
||||||
|
{tip}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{button}
|
||||||
{tip}
|
{tip}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ export function appDisplayNameForLocale(localeTag: string): string {
|
|||||||
/** Префикс заголовка окон: `TTRPG - Редактор`. */
|
/** Префикс заголовка окон: `TTRPG - Редактор`. */
|
||||||
export const APP_WINDOW_BRAND = 'TTRPG';
|
export const APP_WINDOW_BRAND = 'TTRPG';
|
||||||
|
|
||||||
export type AppWindowKind = 'editor' | 'presentation' | 'control' | 'boot';
|
export type AppWindowKind = 'editor' | 'presentation' | 'control' | 'boot' | 'sceneDescription';
|
||||||
|
|
||||||
const WINDOW_SUFFIX: Record<AppWindowKind, { ru: string; en: string }> = {
|
const WINDOW_SUFFIX: Record<AppWindowKind, { ru: string; en: string }> = {
|
||||||
editor: { ru: 'Редактор', en: 'Editor' },
|
editor: { ru: 'Редактор', en: 'Editor' },
|
||||||
presentation: { ru: 'Презентация', en: 'Presentation' },
|
presentation: { ru: 'Презентация', en: 'Presentation' },
|
||||||
control: { ru: 'Пульт', en: 'Control' },
|
control: { ru: 'Пульт', en: 'Control' },
|
||||||
boot: { ru: 'Загрузка', en: 'Loading' },
|
boot: { ru: 'Загрузка', en: 'Loading' },
|
||||||
|
sceneDescription: { ru: 'Описание сцены', en: 'Scene description' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export function windowChromeTitle(kind: AppWindowKind, localeTag: string): string {
|
export function windowChromeTitle(kind: AppWindowKind, localeTag: string): string {
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ export const ipcChannels = {
|
|||||||
togglePresentationFullscreen: 'windows.togglePresentationFullscreen',
|
togglePresentationFullscreen: 'windows.togglePresentationFullscreen',
|
||||||
getMultiWindowState: 'windows.getMultiWindowState',
|
getMultiWindowState: 'windows.getMultiWindowState',
|
||||||
multiWindowStateChanged: 'windows.multiWindowStateChanged',
|
multiWindowStateChanged: 'windows.multiWindowStateChanged',
|
||||||
|
openSceneDescription: 'windows.openSceneDescription',
|
||||||
|
closeSceneDescription: 'windows.closeSceneDescription',
|
||||||
|
getSceneDescriptionContent: 'windows.getSceneDescriptionContent',
|
||||||
|
sceneDescriptionContent: 'windows.sceneDescriptionContent',
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
stateChanged: 'session.stateChanged',
|
stateChanged: 'session.stateChanged',
|
||||||
@@ -156,6 +160,7 @@ export type IpcEventMap = {
|
|||||||
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
|
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
|
||||||
[ipcChannels.license.statusChanged]: { snapshot: LicenseSnapshot };
|
[ipcChannels.license.statusChanged]: { snapshot: LicenseSnapshot };
|
||||||
[ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
|
[ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
|
||||||
|
[ipcChannels.windows.sceneDescriptionContent]: { html: string };
|
||||||
[ipcChannels.project.importZipProgress]: ZipProgressEvent;
|
[ipcChannels.project.importZipProgress]: ZipProgressEvent;
|
||||||
[ipcChannels.project.exportZipProgress]: ZipProgressEvent;
|
[ipcChannels.project.exportZipProgress]: ZipProgressEvent;
|
||||||
[ipcChannels.project.scenePreviewImportProgress]: ScenePreviewImportEvent;
|
[ipcChannels.project.scenePreviewImportProgress]: ScenePreviewImportEvent;
|
||||||
@@ -373,6 +378,18 @@ export type IpcInvokeMap = {
|
|||||||
req: Record<string, never>;
|
req: Record<string, never>;
|
||||||
res: { open: boolean };
|
res: { open: boolean };
|
||||||
};
|
};
|
||||||
|
[ipcChannels.windows.openSceneDescription]: {
|
||||||
|
req: { html: string };
|
||||||
|
res: { ok: true };
|
||||||
|
};
|
||||||
|
[ipcChannels.windows.closeSceneDescription]: {
|
||||||
|
req: Record<string, never>;
|
||||||
|
res: { ok: true };
|
||||||
|
};
|
||||||
|
[ipcChannels.windows.getSceneDescriptionContent]: {
|
||||||
|
req: Record<string, never>;
|
||||||
|
res: { html: string };
|
||||||
|
};
|
||||||
[ipcChannels.effects.getState]: {
|
[ipcChannels.effects.getState]: {
|
||||||
req: Record<string, never>;
|
req: Record<string, never>;
|
||||||
res: { state: EffectsState };
|
res: { state: EffectsState };
|
||||||
|
|||||||
Generated
+652
-3
@@ -11,6 +11,9 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
"@tiptap/extension-placeholder": "^3.27.4",
|
||||||
|
"@tiptap/react": "^3.27.4",
|
||||||
|
"@tiptap/starter-kit": "^3.27.4",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"ffmpeg-static": "^5.3.0",
|
"ffmpeg-static": "^5.3.0",
|
||||||
"pixi.js": "^8.18.1",
|
"pixi.js": "^8.18.1",
|
||||||
@@ -1457,6 +1460,34 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.8.0",
|
||||||
|
"@floating-ui/utils": "^0.2.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||||
|
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@fontsource/inter": {
|
"node_modules/@fontsource/inter": {
|
||||||
"version": "5.2.8",
|
"version": "5.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz",
|
||||||
@@ -3136,6 +3167,449 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-8W/GwlEn0JwNdpyVfTWcXwHYUpj9BWwO++YxtizmgjJzlwigSh7/xLVJMwVykuQHQ2fCq5rkUvmBRtpHOMLUQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-d1tOHgP3R5cOE+Ot8qL/dkLXRByajgn+j6cCXHqDtmJO2wsK9knmbKQ0SEjbKrU6OgHrTnY/EotNxBEBW9HGoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4",
|
||||||
|
"@tiptap/pm": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-wTtJUUAxCAZ01ICH2DNlOBzzHKRQ1ZST8aRYtIhBPzqEUhnJaKGcjnDB4X49fqPi48iXaPxzhsInDl+rVUujWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bubble-menu": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-Poy7xwcD3POG5ew/TW7mYXv7m++vCchvHxPUqIfnTxBxvvvqDZkPYFWZS1lvPrSBtm1DcfUTQAgVutM5NDZ99Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4",
|
||||||
|
"@tiptap/pm": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-rvja0N1RnwGJAVwDdbUfDIJ4NoT+KjPFaZudKiPuEMfMHfbqe4xcbbC2hsfs61JNcl2xmx+ohV6lzD9YxxJl1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-aPc7opCR1ylK4m4c2lsjLsGpEBD1fLQQKWd5PbZiJvrTF8gkdGZlYLt9A6VukpxeJyHhb22Jaj4fxgKmGMeTtw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-a5caWfWN6Z6usy48vzJDDOhWoA6+rFFCHGpQM7jXn/7rRzYPcvBzTZUGptjEbltj4YqtrQ2tVwTJcCtbb+mknA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4",
|
||||||
|
"@tiptap/pm": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-7nAqgfkgb9HADBeCTnOHuTiyZuxfxvMPT3nH4OZeY+cmtkI1On3QffqlmtcUPvNbkhT3o9ehA1hVfCnQ1Ye4LQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-RiZasQJuUTUO3aME16Bn8eJH7cYnvhT5JCFDFq0ya/1iFI9wUQA2NJC5tb5TrZ74+sQwkYU9VzexnchM481Y9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-tnZywwoNDuEcUZmYYIztXl3PpIKUq+gKeaYPuZhpYEVTThU44tzK3ZuFOmd+qf2aAa1MQwxKWqUuLpNK77bwNw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "3.27.4",
|
||||||
|
"@tiptap/pm": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-svLwSKcFhzpcJeXvxxKkRFuQpykmXrQefVhEsaXq0L95yJIIAGKMRmQC3mxKdzL2j0P9cY7V41bNVSyOAyvclw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-W+Z9pmDgqjbdu3NeZOQrzA15iM4w60Yd8l2CYzxcdApPVIfYzb2S3a7+u1RqW9wnTYb6xyZjASmFNfxXS4P4cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-RgvpxzuYk6QEK+az+eiXpWvGlUso42zNcGnjyUrvskoZjS47MbhSg8ylRYQSRtXE0ETlXhAx4J7iGlGr72kyIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-2eQU/55nE5mhMJHALtLMuBL3dcVJUDVVT7n+uZYMaYE63BtCvC4VS08YLFSR7JZSVJIlgVAmdt5nAw0B+rEPNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4",
|
||||||
|
"@tiptap/pm": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-PeZT4XbyxAp7Lqo/hfA1k5LI27g1RlgS+YgXp2CeHXIrUfSpO5HlZXh02Bvb0pOdl3RFw2tEKtlHzjt8Y1+Nwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-6K/FkNwMLWWQbNWKlycrUPTN7YcyVFdFwZncoBXe5WyarRjLTGw7ywafnCI9PDIWSq7ttzVL4NgjN2IN8kBXww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4",
|
||||||
|
"@tiptap/pm": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-A0BgmRO1RE0yLCx9w7GQITtKfS9wLE5cdngSYDiSpwulcXJhJjKm5mZ4OUZmks2VN4HO5jMl2BWCGt2NSDhA+w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4",
|
||||||
|
"@tiptap/pm": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-z5TVuPw2mkK0B/x+gFg3uUV7tBdaElDFg0zVgnXZCqlSVTLfIyInOOnG5LTWoAd9BdzBjGrzE3PohDcLVDDGBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-on7JNDi7Eqz7UdZeZdiO83bQHo0flVDHzjmtR+v/nrCGW9H15D3CHs5+4ozLDiCvTK8tbkBuut/l9AWNxcCE/Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-bHwLiof0FqJfWzB0act7oEKMTZatEKQ4IYCvmyF5EktjMs4kxEatkPp4Yx/1LSYSjLy1MMT7oLELyaz2FFYyXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-8Dnr1J5s/s4XYYuEF3b784NnCxLjXOlQpmGyXRxTAzW7JaOP08tIUJWVNvSMekfXc2vXa33HUbqjxyyWZEQ6LQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-placeholder": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-7hBoFLeddCv1WzkqB0x3coZ1Hp9WZ9wLoRXIUtUhRKMpzFq2IlTtW1iw88g9pTJnL98bCCElN4DZ4mYtaQvmgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-8OXwcPKuV3ToBBgyvDxH1jQdObK5FIKCGiyIim6qNWiOpi9BhM3XYD+aO1khjv8qIjtoI/DYbizF4ewj09fX2g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-lKQH/hP4FBXsziHypd6Ywj8JFvMLM5GVkK1xsH6yApNuXbHq95rd42ZOYWpYILIBib7tlaz93z61d74UrJiuiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-nRJGvRyEXDtINlHTW+C2oWcL3vmX1URVxAPpkD3Zwn5Rb/vEeOU/pk/w97I0iid816MR4iVbvl1XhbUVegK9gQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-d8opkg2iGtVwJmNGIqv0blfRxnvWOJp1brz+Z8CsP4ojSS2ZtaE46d6JSQ5OeJ7nMpjhT+9wh4UQcA7OSEO59w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4",
|
||||||
|
"@tiptap/pm": "3.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-UB8lcyomfWk7YGI2PZKNqcYXfyRA+PFj+QntlsUXyrsiA5JJIaE8SHKYjxKlGG/xtW3EtPm1b0p38T9Mk4xiFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-changeset": "^2.4.1",
|
||||||
|
"prosemirror-commands": "^1.7.1",
|
||||||
|
"prosemirror-dropcursor": "^1.8.2",
|
||||||
|
"prosemirror-gapcursor": "^1.4.1",
|
||||||
|
"prosemirror-history": "^1.5.0",
|
||||||
|
"prosemirror-inputrules": "^1.5.1",
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.9",
|
||||||
|
"prosemirror-schema-list": "^1.5.1",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-tables": "^1.8.5",
|
||||||
|
"prosemirror-transform": "^1.12.0",
|
||||||
|
"prosemirror-view": "^1.41.9"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/react": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-rTY1V9Y1jzwmo5ItRi3v2Og/mbcYsr9AjUvGoqpXzR9Z31WhXYphw0y05aYzryh0MHXYzkiE+gbGvrbg+cjwEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"fast-equals": "^5.3.3",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.27.4",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.27.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.27.4",
|
||||||
|
"@tiptap/pm": "3.27.4",
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/starter-kit": {
|
||||||
|
"version": "3.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.27.4.tgz",
|
||||||
|
"integrity": "sha512-/sb6rFxNt5BO4hWpUwvHh+Yh1kNyCQuuz3oDpGef5HUUjSdu9p9rfNiHWIUKBadK8VXuw5es7N+UlZ4hma+gvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "^3.27.4",
|
||||||
|
"@tiptap/extension-blockquote": "^3.27.4",
|
||||||
|
"@tiptap/extension-bold": "^3.27.4",
|
||||||
|
"@tiptap/extension-bullet-list": "^3.27.4",
|
||||||
|
"@tiptap/extension-code": "^3.27.4",
|
||||||
|
"@tiptap/extension-code-block": "^3.27.4",
|
||||||
|
"@tiptap/extension-document": "^3.27.4",
|
||||||
|
"@tiptap/extension-dropcursor": "^3.27.4",
|
||||||
|
"@tiptap/extension-gapcursor": "^3.27.4",
|
||||||
|
"@tiptap/extension-hard-break": "^3.27.4",
|
||||||
|
"@tiptap/extension-heading": "^3.27.4",
|
||||||
|
"@tiptap/extension-horizontal-rule": "^3.27.4",
|
||||||
|
"@tiptap/extension-italic": "^3.27.4",
|
||||||
|
"@tiptap/extension-link": "^3.27.4",
|
||||||
|
"@tiptap/extension-list": "^3.27.4",
|
||||||
|
"@tiptap/extension-list-item": "^3.27.4",
|
||||||
|
"@tiptap/extension-list-keymap": "^3.27.4",
|
||||||
|
"@tiptap/extension-ordered-list": "^3.27.4",
|
||||||
|
"@tiptap/extension-paragraph": "^3.27.4",
|
||||||
|
"@tiptap/extension-strike": "^3.27.4",
|
||||||
|
"@tiptap/extension-text": "^3.27.4",
|
||||||
|
"@tiptap/extension-underline": "^3.27.4",
|
||||||
|
"@tiptap/extensions": "^3.27.4",
|
||||||
|
"@tiptap/pm": "^3.27.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.10.1",
|
"version": "0.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||||
@@ -3523,7 +3997,6 @@
|
|||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -3533,7 +4006,6 @@
|
|||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
@@ -3549,6 +4021,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/validator": {
|
"node_modules/@types/validator": {
|
||||||
"version": "13.15.10",
|
"version": "13.15.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
|
||||||
@@ -5849,7 +6327,6 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/d3-color": {
|
"node_modules/d3-color": {
|
||||||
@@ -7748,6 +8225,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-equals": {
|
||||||
|
"version": "5.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz",
|
||||||
|
"integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fast-json-stable-stringify": {
|
"node_modules/fast-json-stable-stringify": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||||
@@ -10263,6 +10749,12 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
|
||||||
|
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/load-bmfont": {
|
"node_modules/load-bmfont": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz",
|
||||||
@@ -11248,6 +11740,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/orderedmap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/own-keys": {
|
"node_modules/own-keys": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
|
||||||
@@ -11890,6 +12388,145 @@
|
|||||||
"signal-exit": "^3.0.2"
|
"signal-exit": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-changeset": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-commands": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-dropcursor": {
|
||||||
|
"version": "1.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz",
|
||||||
|
"integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0",
|
||||||
|
"prosemirror-view": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-gapcursor": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.0.0",
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-history": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.2.2",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.31.0",
|
||||||
|
"rope-sequence": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-inputrules": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-keymap": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"w3c-keyname": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-model": {
|
||||||
|
"version": "1.25.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz",
|
||||||
|
"integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"orderedmap": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-list": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-state": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-tables": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.4",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-transform": "^1.10.5",
|
||||||
|
"prosemirror-view": "^1.41.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-transform": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-view": {
|
||||||
|
"version": "1.42.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.1.tgz",
|
||||||
|
"integrity": "sha512-rRqzZnRgkyh69XoOMrfFJHwauHscLBmHbq772kwbic1ymQAM8gXjzEbJse5j1ep2UO2HRIAQL0bY3kZ/RoqjVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.25.8",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/psl": {
|
"node_modules/psl": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||||
@@ -12385,6 +13022,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/rope-sequence": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-array-concat": {
|
"node_modules/safe-array-concat": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||||
@@ -14566,6 +15209,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/wcwidth": {
|
"node_modules/wcwidth": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
|
||||||
|
|||||||
+4
-1
@@ -10,7 +10,7 @@
|
|||||||
"build:obfuscate": "node scripts/build.mjs --production --obfuscate",
|
"build:obfuscate": "node scripts/build.mjs --production --obfuscate",
|
||||||
"lint": "eslint . --max-warnings 0",
|
"lint": "eslint . --max-warnings 0",
|
||||||
"typecheck": "tsc -p tsconfig.eslint.json --noEmit",
|
"typecheck": "tsc -p tsconfig.eslint.json --noEmit",
|
||||||
"test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/fileDrop.test.ts app/shared/graph/sceneListOrder.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/shared/graph/storylineExportImport.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/shared/fieldEffectEraser.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs",
|
"test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/fileDrop.test.ts app/shared/graph/sceneListOrder.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/renderer/editor/sceneDescriptionHtml.test.ts app/shared/graph/storylineExportImport.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/shared/fieldEffectEraser.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs",
|
||||||
"format": "prettier . --check",
|
"format": "prettier . --check",
|
||||||
"format:write": "prettier . --write",
|
"format:write": "prettier . --write",
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
@@ -31,6 +31,9 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
"@tiptap/extension-placeholder": "^3.27.4",
|
||||||
|
"@tiptap/react": "^3.27.4",
|
||||||
|
"@tiptap/starter-kit": "^3.27.4",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"ffmpeg-static": "^5.3.0",
|
"ffmpeg-static": "^5.3.0",
|
||||||
"pixi.js": "^8.18.1",
|
"pixi.js": "^8.18.1",
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
editor: path.resolve(__dirname, 'app/renderer/editor.html'),
|
editor: path.resolve(__dirname, 'app/renderer/editor.html'),
|
||||||
presentation: path.resolve(__dirname, 'app/renderer/presentation.html'),
|
presentation: path.resolve(__dirname, 'app/renderer/presentation.html'),
|
||||||
control: path.resolve(__dirname, 'app/renderer/control.html'),
|
control: path.resolve(__dirname, 'app/renderer/control.html'),
|
||||||
|
sceneDescription: path.resolve(__dirname, 'app/renderer/sceneDescription.html'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user