@lingjingai/script-editor 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INTEGRATION.md +59 -0
- package/dist/index.d.ts +66 -2
- package/dist/index.js +302 -37
- package/dist/index.js.map +1 -1
- package/dist/style.css +44 -0
- package/package.json +1 -1
package/INTEGRATION.md
CHANGED
|
@@ -64,6 +64,7 @@ function Host({ script, setScript }) {
|
|
|
64
64
|
- `selection` / `onSelectionChange`(`StudioSelection`,不传则内部自管)
|
|
65
65
|
- `format` / `onFormatChange`(`"standard" | "asian"`)
|
|
66
66
|
- `disabled`、`dark`、`className`
|
|
67
|
+
- `toc?: "sidebar" | "none"`:内置左侧目录。默认 `"sidebar"` 渲染自带 256px 目录列;`"none"` = **无目录模式**——只渲染顶栏 + 画布(占满宽度),宿主自建导航。见 §10。`0.1.18+`
|
|
67
68
|
|
|
68
69
|
**HUD(顶栏)**
|
|
69
70
|
- `onTitleChange?(next: string)`:顶栏左侧常驻**剧本标题**(取 `value.title`)。传了它,标题变**点击即改**;不传则只读展示(`value.title` 为空时不显示)。包**不碰存储**——拿到新标题宿主自己持久化(可 `onEdit(p => ({...p, title}))` 写回 `script.json`,或另调重命名接口)。`0.1.16+`
|
|
@@ -127,3 +128,61 @@ function Host({ script, setScript }) {
|
|
|
127
128
|
- `apps/web/src/features/script-editor/useScriptDocument.ts` —— 自动保存 + undo/redo + 8s 轮询探测
|
|
128
129
|
- `apps/web/src/features/script-editor/script-output-api.ts` —— 后端存取
|
|
129
130
|
- `apps/web/src/main.tsx` / `apps/web/vite.config.ts` —— CSS 引入 + `development` 解析条件
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 10. 无目录模式 / 自建导航(`toc="none"`)`0.1.18+`
|
|
135
|
+
|
|
136
|
+
把内置左侧目录关掉、导航**透出**给宿主自己渲染(嵌进已有全局导航、或做成面包屑下拉 / ⌘K 命令面板 / 移动端 sheet 等完全不同的形态)。包只给**纯数据模型**,不给 UI。
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
import {
|
|
140
|
+
ScriptEditor,
|
|
141
|
+
buildScriptNav,
|
|
142
|
+
selectionTargetKey,
|
|
143
|
+
withOrigin,
|
|
144
|
+
type StudioSelection,
|
|
145
|
+
} from "@lingjingai/script-editor";
|
|
146
|
+
|
|
147
|
+
function Host({ script, onEdit }) {
|
|
148
|
+
// 无目录模式下 selection 必须受控(宿主自己的导航要驱动它)
|
|
149
|
+
const [sel, setSel] = useState<StudioSelection>({ kind: "empty" });
|
|
150
|
+
const nav = buildScriptNav(script); // 派生导航树(纯函数)
|
|
151
|
+
const active = selectionTargetKey(sel); // 当前激活节点的 key
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
{/* 宿主自建的任意导航 UI —— 这里是最朴素的列表 */}
|
|
156
|
+
<MyNav>
|
|
157
|
+
{nav.episodes.flatMap((e) => e.scenes).map((s) => (
|
|
158
|
+
<button
|
|
159
|
+
key={s.key}
|
|
160
|
+
data-active={s.key === active}
|
|
161
|
+
onClick={() => setSel(withOrigin(s.selection, "sidebar"))}
|
|
162
|
+
>
|
|
163
|
+
{s.label} · {s.meta}
|
|
164
|
+
</button>
|
|
165
|
+
))}
|
|
166
|
+
</MyNav>
|
|
167
|
+
|
|
168
|
+
<ScriptEditor
|
|
169
|
+
value={script}
|
|
170
|
+
onEdit={onEdit}
|
|
171
|
+
toc="none" {/* ← 关掉内置目录,画布占满 */}
|
|
172
|
+
selection={sel}
|
|
173
|
+
onSelectionChange={setSel} {/* ← 编辑器内滚动 spy 也会回吐 selection */}
|
|
174
|
+
/>
|
|
175
|
+
</>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`buildScriptNav(value): ScriptNav` 返回:
|
|
181
|
+
- `worldview` / `episodes[]` / `actors[]` / `locations[]` / `props[]`,`episodes[].scenes[]` 内嵌。
|
|
182
|
+
- 每个节点都带 **`key`**(= `selectionTargetKey(node.selection)`,拿去和 `selectionTargetKey(当前selection)` 比对即判激活)和 **`selection`**(即用型 `StudioSelection`,**不带 origin**;点击时 `withOrigin(node.selection, "sidebar")` 再喂 `onSelectionChange`)。
|
|
183
|
+
- 标签字段(`label`/`title`/`meta`/`name`)与内置目录**同 helper 派生**,长相一致。
|
|
184
|
+
|
|
185
|
+
要点:
|
|
186
|
+
- selection 同时也是**回吐**通道:编辑器画布滚动时会上抛 `onSelectionChange`(origin `"scroll"`),宿主导航据此高亮当前位置。
|
|
187
|
+
- 无目录模式下内置「新增一集」按钮消失——宿主可自行调 `mutations.addEpisode`(已从包导出:`mutations.addEpisode(prev)`)接到自己的按钮上。
|
|
188
|
+
- 形态对照交互演示:`toc-modes-demo.html`(本包根目录,不随包发布)「形态 B」tab,演示用同一个 `buildScriptNav` 模型同时驱动面包屑下拉 + ⌘K 两种 UI。
|
package/dist/index.d.ts
CHANGED
|
@@ -488,6 +488,12 @@ interface ScriptEditorProps extends ScriptEditorAdapters {
|
|
|
488
488
|
disabled?: boolean;
|
|
489
489
|
className?: string;
|
|
490
490
|
dark?: boolean;
|
|
491
|
+
/** built-in left directory. Default "sidebar" renders the bundled 256px TOC
|
|
492
|
+
* column; "none" → directory-less mode: the editor renders top bar + canvas
|
|
493
|
+
* only (full width) and the host drives navigation via controlled
|
|
494
|
+
* selection/onSelectionChange + the exported buildScriptNav(value) model.
|
|
495
|
+
* 0.1.18+ */
|
|
496
|
+
toc?: "sidebar" | "none";
|
|
491
497
|
/** when provided, the script title (value.title) becomes click-to-edit in the
|
|
492
498
|
* top bar; the host persists the new value (the editor never touches storage,
|
|
493
499
|
* matching the value + onEdit contract — just one more delegated hook). Omit →
|
|
@@ -512,7 +518,65 @@ interface ScriptEditorProps extends ScriptEditorAdapters {
|
|
|
512
518
|
* scroll position across editor unmount/remount (tab switch / project nav). */
|
|
513
519
|
scrollScopeKey?: string;
|
|
514
520
|
}
|
|
515
|
-
declare function ScriptEditor({ value, onEdit, selection: controlledSelection, onSelectionChange, format: controlledFormat, onFormatChange, disabled, className, dark, onTitleChange, topBarActions, saveStatus, canUndo, canRedo, onUndo, onRedo, onEditingChange, referenceFocusRequest, scrollScopeKey, diff, asset, onAddChatReferences, onRequestAssetDelete, }: ScriptEditorProps): react.JSX.Element;
|
|
521
|
+
declare function ScriptEditor({ value, onEdit, selection: controlledSelection, onSelectionChange, format: controlledFormat, onFormatChange, disabled, className, dark, toc, onTitleChange, topBarActions, saveStatus, canUndo, canRedo, onUndo, onRedo, onEditingChange, referenceFocusRequest, scrollScopeKey, diff, asset, onAddChatReferences, onRequestAssetDelete, }: ScriptEditorProps): react.JSX.Element;
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Headless navigation model — surfaces the script's TOC tree (the same
|
|
525
|
+
* episodes/scenes/entity structure `StudioToc` renders) as plain data so a host
|
|
526
|
+
* in directory-less mode (`<ScriptEditor toc="none" />`) can build its OWN
|
|
527
|
+
* navigation UI: breadcrumb dropdown, ⌘K command palette, mobile sheet, etc.
|
|
528
|
+
*
|
|
529
|
+
* Each node carries:
|
|
530
|
+
* - `key` — `selectionTargetKey` of its selection; compare against
|
|
531
|
+
* `selectionTargetKey(currentSelection)` to mark the active node.
|
|
532
|
+
* - `selection` — a ready-to-use `StudioSelection` (no origin). On click do
|
|
533
|
+
* `onSelectionChange(withOrigin(node.selection, "sidebar"))`.
|
|
534
|
+
*
|
|
535
|
+
* Labels reuse the exact same helpers as `StudioToc`, so a custom nav stays
|
|
536
|
+
* label-identical to the built-in one. Pure function, zero side effects.
|
|
537
|
+
*/
|
|
538
|
+
|
|
539
|
+
interface ScriptNavScene {
|
|
540
|
+
key: string;
|
|
541
|
+
episodeIdx: number;
|
|
542
|
+
sceneIdx: number;
|
|
543
|
+
sceneId: string;
|
|
544
|
+
/** e.g. "场 7" */
|
|
545
|
+
label: string;
|
|
546
|
+
/** e.g. "scn_007 · 夜内" */
|
|
547
|
+
meta: string;
|
|
548
|
+
selection: StudioSelection;
|
|
549
|
+
}
|
|
550
|
+
interface ScriptNavEpisode {
|
|
551
|
+
key: string;
|
|
552
|
+
episodeIdx: number;
|
|
553
|
+
/** zero-padded episode number, e.g. "01" */
|
|
554
|
+
label: string;
|
|
555
|
+
/** human title (falls back to "第 NN 集") */
|
|
556
|
+
title: string;
|
|
557
|
+
sceneCount: number;
|
|
558
|
+
scenes: ScriptNavScene[];
|
|
559
|
+
selection: StudioSelection;
|
|
560
|
+
}
|
|
561
|
+
interface ScriptNavEntity {
|
|
562
|
+
key: string;
|
|
563
|
+
id: string;
|
|
564
|
+
name: string;
|
|
565
|
+
selection: StudioSelection;
|
|
566
|
+
}
|
|
567
|
+
interface ScriptNav {
|
|
568
|
+
worldview: {
|
|
569
|
+
key: string;
|
|
570
|
+
label: string;
|
|
571
|
+
selection: StudioSelection;
|
|
572
|
+
};
|
|
573
|
+
episodes: ScriptNavEpisode[];
|
|
574
|
+
actors: ScriptNavEntity[];
|
|
575
|
+
locations: ScriptNavEntity[];
|
|
576
|
+
props: ScriptNavEntity[];
|
|
577
|
+
}
|
|
578
|
+
/** Derive the navigation tree from a ScriptProject (see file header). */
|
|
579
|
+
declare function buildScriptNav(value: ScriptProject): ScriptNav;
|
|
516
580
|
|
|
517
581
|
/**
|
|
518
582
|
* Pure ScriptProject mutations.
|
|
@@ -739,4 +803,4 @@ declare function DeleteConfirmButton({ onConfirm, disabled, targetScope, title,
|
|
|
739
803
|
description?: string;
|
|
740
804
|
}): react.JSX.Element;
|
|
741
805
|
|
|
742
|
-
export { type AssetAdapter, type AssetDeleteRequest, type AssetState, type ChangeSet, type ChatReference, DeleteConfirmButton, type DiffActionMark, type DiffAdapter, type DiffSceneMark, EMPTY_SELECTION, EditableText, type EpisodeWorkspaceView, type FieldChange, Popover, type SaveStatus, type Scene, type SceneActionDiff, type SceneChangeKind, type SceneContext, type SceneEnvironment, type ScriptActor, ScriptContentType, type ScriptData, ScriptEditor, type ScriptEditorAdapters, type ScriptEditorProps, type ScriptEpisode, type ScriptFormat, type ScriptItem, type ScriptLocation, type ScriptProject, type ScriptProp, type ScriptReferenceFocusRequest, type ScriptReferenceLocator, type ScriptSpeaker, type ScriptTargetKind, SelectField, type StateChange, type StudioSelection, type StudioSelectionFrom, type StudioSelectionOrigin, type TransitionPrompt, acceptActionIntoBaseline, acceptSceneIntoBaseline, applyActionAccept, applyActionReject, buildActionDiff, buildDiffFromBaseline, buildSelectionFrom, changeSetToDiffScenes, computeChangeSet, computeSceneActionDiff, findBaselineScene, fromToSelection, getActorCueVar, getActorMap, getEpisodeTitle, getLocationMap, getPropMap, getSceneContext, getSelectionEpisodeIdx, getSpeakerMap, isClipSelected, isEpisodeOpen, isSameSelectionTarget, isSceneSelected, index as mutations, rejectActionToBaseline, rejectSceneToBaseline, sceneChangeKey, selectionFromScriptReference, selectionTargetKey, withOrigin };
|
|
806
|
+
export { type AssetAdapter, type AssetDeleteRequest, type AssetState, type ChangeSet, type ChatReference, DeleteConfirmButton, type DiffActionMark, type DiffAdapter, type DiffSceneMark, EMPTY_SELECTION, EditableText, type EpisodeWorkspaceView, type FieldChange, Popover, type SaveStatus, type Scene, type SceneActionDiff, type SceneChangeKind, type SceneContext, type SceneEnvironment, type ScriptActor, ScriptContentType, type ScriptData, ScriptEditor, type ScriptEditorAdapters, type ScriptEditorProps, type ScriptEpisode, type ScriptFormat, type ScriptItem, type ScriptLocation, type ScriptNav, type ScriptNavEntity, type ScriptNavEpisode, type ScriptNavScene, type ScriptProject, type ScriptProp, type ScriptReferenceFocusRequest, type ScriptReferenceLocator, type ScriptSpeaker, type ScriptTargetKind, SelectField, type StateChange, type StudioSelection, type StudioSelectionFrom, type StudioSelectionOrigin, type TransitionPrompt, acceptActionIntoBaseline, acceptSceneIntoBaseline, applyActionAccept, applyActionReject, buildActionDiff, buildDiffFromBaseline, buildScriptNav, buildSelectionFrom, changeSetToDiffScenes, computeChangeSet, computeSceneActionDiff, findBaselineScene, fromToSelection, getActorCueVar, getActorMap, getEpisodeTitle, getLocationMap, getPropMap, getSceneContext, getSelectionEpisodeIdx, getSpeakerMap, isClipSelected, isEpisodeOpen, isSameSelectionTarget, isSceneSelected, index as mutations, rejectActionToBaseline, rejectSceneToBaseline, sceneChangeKey, selectionFromScriptReference, selectionTargetKey, withOrigin };
|
package/dist/index.js
CHANGED
|
@@ -1360,11 +1360,12 @@ function buildLocationDistribution(data, locationId) {
|
|
|
1360
1360
|
(episode.scenes ?? []).forEach((scene, si) => {
|
|
1361
1361
|
const inIds = (scene.location_ids ?? []).some((i) => (i ?? "").trim() === locationId);
|
|
1362
1362
|
const ctx = getSceneContext(scene);
|
|
1363
|
-
const
|
|
1364
|
-
if (inIds ||
|
|
1363
|
+
const ctxRef = (ctx.locations ?? []).find((i) => (i.location_id ?? "").trim() === locationId);
|
|
1364
|
+
if (inIds || ctxRef) {
|
|
1365
1365
|
items.push({
|
|
1366
1366
|
key: `location-${locationId}-${ei}-${si}`,
|
|
1367
|
-
...refFields(episode, ei, scene, si)
|
|
1367
|
+
...refFields(episode, ei, scene, si),
|
|
1368
|
+
stateId: ctxRef?.state_id
|
|
1368
1369
|
});
|
|
1369
1370
|
}
|
|
1370
1371
|
});
|
|
@@ -1378,13 +1379,15 @@ function buildPropDistribution(data, propId, propName) {
|
|
|
1378
1379
|
(data.episodes ?? []).forEach((episode, ei) => {
|
|
1379
1380
|
(episode.scenes ?? []).forEach((scene, si) => {
|
|
1380
1381
|
const ctx = getSceneContext(scene);
|
|
1381
|
-
const
|
|
1382
|
+
const ctxRef = (ctx.props ?? []).find((i) => (i.prop_id ?? "").trim() === id);
|
|
1383
|
+
const byRef = !!ctxRef;
|
|
1382
1384
|
const byId = !byRef && id ? (scene.actions ?? []).some((i) => (i.content ?? "").includes(id)) : false;
|
|
1383
1385
|
const byName = !byRef && !byId && needle ? (scene.actions ?? []).some((i) => (i.content ?? "").toLowerCase().includes(needle)) : false;
|
|
1384
1386
|
if (byRef || byId || byName) {
|
|
1385
1387
|
items.push({
|
|
1386
1388
|
key: `prop-${id || needle}-${ei}-${si}`,
|
|
1387
|
-
...refFields(episode, ei, scene, si)
|
|
1389
|
+
...refFields(episode, ei, scene, si),
|
|
1390
|
+
stateId: ctxRef?.state_id
|
|
1388
1391
|
});
|
|
1389
1392
|
}
|
|
1390
1393
|
});
|
|
@@ -2893,33 +2896,238 @@ function DeleteConfirmButton({
|
|
|
2893
2896
|
}
|
|
2894
2897
|
);
|
|
2895
2898
|
}
|
|
2896
|
-
|
|
2899
|
+
var SPACE_ZH = { interior: "\u5185", exterior: "\u5916" };
|
|
2900
|
+
var TIME_ZH = { day: "\u65E5", night: "\u591C", dawn: "\u6668", dusk: "\u66AE" };
|
|
2901
|
+
function resolveCueId(item, actorMap, speakerMap) {
|
|
2902
|
+
const actorId = item.actor_id?.trim();
|
|
2903
|
+
if (actorId) return actorId;
|
|
2904
|
+
const spkId = item.speaker_id?.trim() || item.speakers?.[0]?.speaker_id?.trim() || "";
|
|
2905
|
+
if (!spkId) return "";
|
|
2906
|
+
const spk = speakerMap.get(spkId);
|
|
2907
|
+
const kind = spk?.source_kind?.trim();
|
|
2908
|
+
if ((kind === "actor" || kind === "prop") && spk?.source_id?.trim()) return spk.source_id.trim();
|
|
2909
|
+
if (spkId.startsWith("spk_") && actorMap.has(spkId.slice(4))) return spkId.slice(4);
|
|
2910
|
+
return "";
|
|
2911
|
+
}
|
|
2912
|
+
function ScenePreviewCard({
|
|
2913
|
+
data,
|
|
2914
|
+
anchor,
|
|
2915
|
+
highlightId,
|
|
2916
|
+
highlightKind,
|
|
2917
|
+
onEnter,
|
|
2918
|
+
onLeave
|
|
2919
|
+
}) {
|
|
2920
|
+
const actorMap = useMemo(() => getActorMap(data.actors), [data.actors]);
|
|
2921
|
+
const locationMap = useMemo(() => getLocationMap(data.locations), [data.locations]);
|
|
2922
|
+
const speakerMap = useMemo(() => getSpeakerMap(data.speakers), [data.speakers]);
|
|
2923
|
+
const episode = data.episodes?.[anchor.episodeIndex];
|
|
2924
|
+
const scene = episode?.scenes?.[anchor.sceneIndex];
|
|
2925
|
+
if (!scene) return null;
|
|
2926
|
+
const locId = getPrimarySceneLocationId(scene);
|
|
2927
|
+
const locName = (locId ? getLocationDisplayName(locationMap.get(locId)) : "") || "\u672A\u6307\u5B9A\u5730\u70B9";
|
|
2928
|
+
const env = scene.environment ?? {};
|
|
2929
|
+
const slug = `${SPACE_ZH[env.space ?? "interior"] ?? env.space ?? ""}\xB7${TIME_ZH[env.time ?? "day"] ?? env.time ?? ""}`;
|
|
2930
|
+
const actions = scene.actions ?? [];
|
|
2931
|
+
const W = 360;
|
|
2932
|
+
const vw = typeof window !== "undefined" ? window.innerWidth : 1280;
|
|
2933
|
+
const vh = typeof window !== "undefined" ? window.innerHeight : 800;
|
|
2934
|
+
const left = Math.min(Math.max(8, anchor.rect.left + anchor.rect.width / 2 - W / 2), vw - W - 8);
|
|
2935
|
+
const above = anchor.rect.top > vh * 0.55;
|
|
2936
|
+
const pos = above ? { left, bottom: vh - anchor.rect.top + 8, maxHeight: anchor.rect.top - 16 } : { left, top: anchor.rect.bottom + 8, maxHeight: vh - anchor.rect.bottom - 16 };
|
|
2937
|
+
const card = /* @__PURE__ */ jsxs("div", { className: "lj-se-spv", style: { width: W, ...pos }, onMouseEnter: onEnter, onMouseLeave: onLeave, children: [
|
|
2938
|
+
/* @__PURE__ */ jsxs("div", { className: "lj-se-spv-head", children: [
|
|
2939
|
+
/* @__PURE__ */ jsxs("span", { className: "lj-se-spv-num", children: [
|
|
2940
|
+
getEpisodeNumberLabel(episode, anchor.episodeIndex),
|
|
2941
|
+
" \xB7 ",
|
|
2942
|
+
getSceneNumberLabel(scene, anchor.sceneIndex)
|
|
2943
|
+
] }),
|
|
2944
|
+
/* @__PURE__ */ jsxs("span", { className: "lj-se-spv-slug", children: [
|
|
2945
|
+
locName,
|
|
2946
|
+
" ",
|
|
2947
|
+
/* @__PURE__ */ jsx("span", { className: "lj-se-spv-slug-env", children: slug })
|
|
2948
|
+
] })
|
|
2949
|
+
] }),
|
|
2950
|
+
/* @__PURE__ */ jsx("div", { className: "lj-se-spv-body", children: actions.length === 0 ? /* @__PURE__ */ jsx("p", { className: "lj-se-spv-empty", children: "\uFF08\u672C\u573A\u6682\u65E0\u5185\u5BB9\uFF09" }) : actions.map((item, i) => /* @__PURE__ */ jsx(
|
|
2951
|
+
PreviewLine,
|
|
2952
|
+
{
|
|
2953
|
+
item,
|
|
2954
|
+
actorMap,
|
|
2955
|
+
speakerMap,
|
|
2956
|
+
highlightId,
|
|
2957
|
+
highlightKind
|
|
2958
|
+
},
|
|
2959
|
+
i
|
|
2960
|
+
)) })
|
|
2961
|
+
] });
|
|
2962
|
+
return createPortal(card, anchor.host ?? document.body);
|
|
2963
|
+
}
|
|
2964
|
+
function PreviewLine({
|
|
2965
|
+
item,
|
|
2966
|
+
actorMap,
|
|
2967
|
+
speakerMap,
|
|
2968
|
+
highlightId,
|
|
2969
|
+
highlightKind
|
|
2970
|
+
}) {
|
|
2971
|
+
const type = item.type ?? "dialogue" /* Dialogue */;
|
|
2972
|
+
const content = item.content?.trim() ?? "";
|
|
2973
|
+
if (type === "action" /* Action */) {
|
|
2974
|
+
return /* @__PURE__ */ jsxs("p", { className: "lj-se-spv-action", children: [
|
|
2975
|
+
/* @__PURE__ */ jsx("span", { className: "lj-se-spv-mark", children: "\u25B3" }),
|
|
2976
|
+
content || "\u2026"
|
|
2977
|
+
] });
|
|
2978
|
+
}
|
|
2979
|
+
if (type === "narration" /* Narration */) {
|
|
2980
|
+
return /* @__PURE__ */ jsx("p", { className: "lj-se-spv-narr", children: content || "\u2026" });
|
|
2981
|
+
}
|
|
2982
|
+
const isInner = type === "inner_thought" /* InnerThought */;
|
|
2983
|
+
const tone = isInner ? "inner" : "dialogue";
|
|
2984
|
+
const cueId = resolveCueId(item, actorMap, speakerMap);
|
|
2985
|
+
const speaker = getDialogueSpeakerLabel(item, actorMap, speakerMap) || "\u672A\u77E5\u89D2\u8272";
|
|
2986
|
+
const emotion = translateEmotionLabel(item.emotion);
|
|
2987
|
+
const delivery = getDialogueDeliveryLabel(item.delivery);
|
|
2988
|
+
const tag = isInner ? emotion ? `OS\xB7${emotion}` : "OS" : emotion;
|
|
2989
|
+
const mine = !!highlightId && (highlightKind === "actor" || highlightKind === "prop") && cueId === highlightId;
|
|
2990
|
+
const mineStyle = mine ? { "--mine": getActorCueVar(cueId, tone) } : void 0;
|
|
2991
|
+
return /* @__PURE__ */ jsxs("p", { className: cn("lj-se-spv-cue", isInner && "is-inner", mine && "is-mine"), style: mineStyle, children: [
|
|
2992
|
+
/* @__PURE__ */ jsxs("strong", { className: "lj-se-spv-speaker", style: { color: getActorCueVar(cueId, tone) }, children: [
|
|
2993
|
+
speaker,
|
|
2994
|
+
delivery ? /* @__PURE__ */ jsxs("span", { className: "lj-se-spv-delivery", children: [
|
|
2995
|
+
"\uFF08",
|
|
2996
|
+
delivery,
|
|
2997
|
+
"\uFF09"
|
|
2998
|
+
] }) : null
|
|
2999
|
+
] }),
|
|
3000
|
+
tag ? /* @__PURE__ */ jsxs("span", { className: "lj-se-spv-tag", children: [
|
|
3001
|
+
"\uFF08",
|
|
3002
|
+
tag,
|
|
3003
|
+
"\uFF09"
|
|
3004
|
+
] }) : null,
|
|
3005
|
+
/* @__PURE__ */ jsx("span", { className: "lj-se-spv-colon", children: "\uFF1A" }),
|
|
3006
|
+
content || "\u2026"
|
|
3007
|
+
] });
|
|
3008
|
+
}
|
|
3009
|
+
var UNASSIGNED = "__unassigned__";
|
|
3010
|
+
var CUE_COUNT = 8;
|
|
3011
|
+
var COL = 32;
|
|
3012
|
+
var MIN_EP_WIDTH = 48;
|
|
3013
|
+
function AssetStateTimeline({
|
|
3014
|
+
data,
|
|
3015
|
+
assetId,
|
|
3016
|
+
assetKind,
|
|
2897
3017
|
refs,
|
|
2898
|
-
|
|
3018
|
+
states: states2,
|
|
2899
3019
|
onJump
|
|
2900
3020
|
}) {
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
3021
|
+
const [hover, setHover] = useState(null);
|
|
3022
|
+
const closeTimer = useRef(null);
|
|
3023
|
+
const cancelClose = () => {
|
|
3024
|
+
if (closeTimer.current) {
|
|
3025
|
+
clearTimeout(closeTimer.current);
|
|
3026
|
+
closeTimer.current = null;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
const scheduleClose = () => {
|
|
3030
|
+
cancelClose();
|
|
3031
|
+
closeTimer.current = setTimeout(() => setHover(null), 140);
|
|
3032
|
+
};
|
|
3033
|
+
const cols = [];
|
|
3034
|
+
for (const group of refs) for (const item of group.items) cols.push(item);
|
|
3035
|
+
const n = cols.length;
|
|
3036
|
+
if (n === 0) return null;
|
|
3037
|
+
const definedIds = states2.map((s) => (s.state_id ?? "").trim()).filter(Boolean);
|
|
3038
|
+
const lanes = states2.map((s, i) => ({ id: (s.state_id ?? "").trim(), name: getStateDisplayName(s) || "\u672A\u547D\u540D\u72B6\u6001", cueIndex: i % CUE_COUNT + 1 })).filter((l) => l.id);
|
|
3039
|
+
const isUnassigned = (c) => {
|
|
3040
|
+
const sid = (c.stateId ?? "").trim();
|
|
3041
|
+
return !sid || !definedIds.includes(sid);
|
|
3042
|
+
};
|
|
3043
|
+
if (cols.some(isUnassigned)) {
|
|
3044
|
+
lanes.push({ id: UNASSIGNED, name: lanes.length > 0 ? "\u672A\u6807\u6CE8" : "\u51FA\u73B0", cueIndex: 0 });
|
|
3045
|
+
}
|
|
3046
|
+
const colMetas = [];
|
|
3047
|
+
const epHeads = [];
|
|
3048
|
+
let x = 0;
|
|
3049
|
+
for (const group of refs) {
|
|
3050
|
+
const count = group.items.length;
|
|
3051
|
+
const w = Math.max(COL, Math.ceil(MIN_EP_WIDTH / count));
|
|
3052
|
+
epHeads.push({ episodeIndex: group.episodeIndex, episodeNumber: group.episodeNumber, width: count * w });
|
|
3053
|
+
for (const item of group.items) {
|
|
3054
|
+
colMetas.push({ item, left: x, width: w });
|
|
3055
|
+
x += w;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
const trackWidth = x;
|
|
3059
|
+
const center = (m) => m.left + m.width / 2;
|
|
3060
|
+
const cueVar = (cueIndex) => cueIndex === 0 ? "var(--lj-se-muted-fg)" : `var(--lj-se-actor-cue-${cueIndex})`;
|
|
3061
|
+
const laneMetas = (laneId) => colMetas.filter((m) => laneId === UNASSIGNED ? isUnassigned(m.item) : (m.item.stateId ?? "").trim() === laneId);
|
|
3062
|
+
return /* @__PURE__ */ jsxs("div", { className: "lj-se-sttl", children: [
|
|
3063
|
+
/* @__PURE__ */ jsxs("div", { className: "lj-se-sttl-axis", children: [
|
|
3064
|
+
/* @__PURE__ */ jsx("span", { className: "lj-se-sttl-axis-lab", children: "\u72B6\u6001 \uFF3C \u573A" }),
|
|
3065
|
+
/* @__PURE__ */ jsx("span", { className: "lj-se-sttl-axis-eps", style: { width: trackWidth }, children: epHeads.map((g) => /* @__PURE__ */ jsxs("span", { className: "lj-se-sttl-ep", style: { width: g.width }, title: `\u7B2C ${g.episodeNumber} \u96C6`, children: [
|
|
3066
|
+
"\u7B2C",
|
|
3067
|
+
String(g.episodeNumber).padStart(2, "0"),
|
|
3068
|
+
"\u96C6"
|
|
3069
|
+
] }, g.episodeIndex)) })
|
|
2907
3070
|
] }),
|
|
2908
|
-
|
|
2909
|
-
const
|
|
2910
|
-
return /* @__PURE__ */
|
|
2911
|
-
"
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
className:
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
3071
|
+
lanes.map((lane) => {
|
|
3072
|
+
const mine = laneMetas(lane.id);
|
|
3073
|
+
return /* @__PURE__ */ jsxs("div", { className: "lj-se-sttl-lane", children: [
|
|
3074
|
+
/* @__PURE__ */ jsxs("span", { className: "lj-se-sttl-lane-head", title: lane.name, children: [
|
|
3075
|
+
/* @__PURE__ */ jsx("span", { className: "lj-se-sttl-dot", style: { background: cueVar(lane.cueIndex) } }),
|
|
3076
|
+
/* @__PURE__ */ jsx("span", { className: "lj-se-sttl-lane-name", children: lane.name }),
|
|
3077
|
+
/* @__PURE__ */ jsx("span", { className: "lj-se-sttl-lane-count", children: mine.length })
|
|
3078
|
+
] }),
|
|
3079
|
+
/* @__PURE__ */ jsxs("span", { className: "lj-se-sttl-track", style: { width: trackWidth, "--cue": cueVar(lane.cueIndex) }, children: [
|
|
3080
|
+
colMetas.map((m, i) => /* @__PURE__ */ jsx("span", { className: "lj-se-sttl-col", style: { left: m.left } }, `col-${i}`)),
|
|
3081
|
+
mine.map((m, k) => {
|
|
3082
|
+
const next = mine[k + 1];
|
|
3083
|
+
if (!next) return null;
|
|
3084
|
+
return /* @__PURE__ */ jsx(
|
|
3085
|
+
"span",
|
|
3086
|
+
{
|
|
3087
|
+
className: "lj-se-sttl-flow",
|
|
3088
|
+
style: { left: center(m), width: center(next) - center(m) }
|
|
3089
|
+
},
|
|
3090
|
+
`flow-${m.item.key}`
|
|
3091
|
+
);
|
|
3092
|
+
}),
|
|
3093
|
+
mine.map((m) => /* @__PURE__ */ jsx(
|
|
3094
|
+
"button",
|
|
3095
|
+
{
|
|
3096
|
+
type: "button",
|
|
3097
|
+
className: "lj-se-sttl-node",
|
|
3098
|
+
style: { left: center(m) },
|
|
3099
|
+
title: m.item.label,
|
|
3100
|
+
onClick: () => onJump(m.item.episodeIndex, m.item.sceneIndex),
|
|
3101
|
+
onMouseEnter: (e) => {
|
|
3102
|
+
cancelClose();
|
|
3103
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
3104
|
+
setHover({
|
|
3105
|
+
episodeIndex: m.item.episodeIndex,
|
|
3106
|
+
sceneIndex: m.item.sceneIndex,
|
|
3107
|
+
rect: { top: r.top, bottom: r.bottom, left: r.left, width: r.width },
|
|
3108
|
+
host: e.currentTarget.closest(".lj-se-root")
|
|
3109
|
+
});
|
|
3110
|
+
},
|
|
3111
|
+
onMouseLeave: scheduleClose,
|
|
3112
|
+
children: m.item.sceneNumber
|
|
3113
|
+
},
|
|
3114
|
+
m.item.key
|
|
3115
|
+
))
|
|
3116
|
+
] })
|
|
3117
|
+
] }, lane.id);
|
|
3118
|
+
}),
|
|
3119
|
+
hover ? /* @__PURE__ */ jsx(
|
|
3120
|
+
ScenePreviewCard,
|
|
3121
|
+
{
|
|
3122
|
+
data,
|
|
3123
|
+
anchor: hover,
|
|
3124
|
+
highlightId: assetId,
|
|
3125
|
+
highlightKind: assetKind,
|
|
3126
|
+
onEnter: cancelClose,
|
|
3127
|
+
onLeave: scheduleClose
|
|
3128
|
+
}
|
|
3129
|
+
) : null
|
|
3130
|
+
] });
|
|
2923
3131
|
}
|
|
2924
3132
|
var META = {
|
|
2925
3133
|
actors: { kind: "actor" },
|
|
@@ -3051,12 +3259,20 @@ function AssetCardImpl({
|
|
|
3051
3259
|
] }, sid);
|
|
3052
3260
|
}) }) : null
|
|
3053
3261
|
] }),
|
|
3054
|
-
refs.length > 0 ?
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3262
|
+
refs.length > 0 ? (
|
|
3263
|
+
// state→scene reverse-lookup timeline: which state appears in which
|
|
3264
|
+
// scene, and how it flows. Stateless assets degrade to one "出现" lane.
|
|
3265
|
+
/* @__PURE__ */ jsx(
|
|
3266
|
+
AssetStateTimeline,
|
|
3267
|
+
{
|
|
3268
|
+
data,
|
|
3269
|
+
assetId,
|
|
3270
|
+
assetKind: kind,
|
|
3271
|
+
refs,
|
|
3272
|
+
states: states2,
|
|
3273
|
+
onJump: (episodeIdx, sceneIdx) => onSelect({ kind: "scene", episodeIdx, sceneIdx, from: fromRef, origin: "sidebar" })
|
|
3274
|
+
}
|
|
3275
|
+
)
|
|
3060
3276
|
) : null
|
|
3061
3277
|
] }) });
|
|
3062
3278
|
}
|
|
@@ -3773,6 +3989,7 @@ function ScriptEditor({
|
|
|
3773
3989
|
disabled = false,
|
|
3774
3990
|
className,
|
|
3775
3991
|
dark,
|
|
3992
|
+
toc = "sidebar",
|
|
3776
3993
|
onTitleChange,
|
|
3777
3994
|
topBarActions,
|
|
3778
3995
|
saveStatus = "idle",
|
|
@@ -3936,7 +4153,7 @@ function ScriptEditor({
|
|
|
3936
4153
|
}
|
|
3937
4154
|
);
|
|
3938
4155
|
return /* @__PURE__ */ jsx(EditingSignalContext.Provider, { value: editingSignal, children: /* @__PURE__ */ jsxs("div", { className: rootClass, children: [
|
|
3939
|
-
/* @__PURE__ */ jsx(
|
|
4156
|
+
toc === "sidebar" ? /* @__PURE__ */ jsx(
|
|
3940
4157
|
StudioToc,
|
|
3941
4158
|
{
|
|
3942
4159
|
data: value,
|
|
@@ -3944,7 +4161,7 @@ function ScriptEditor({
|
|
|
3944
4161
|
onSelect: setSelection,
|
|
3945
4162
|
onAddEpisode: disabled ? void 0 : () => onEdit((p) => addEpisode(p))
|
|
3946
4163
|
}
|
|
3947
|
-
),
|
|
4164
|
+
) : null,
|
|
3948
4165
|
/* @__PURE__ */ jsxs("div", { className: "lj-se-main", children: [
|
|
3949
4166
|
/* @__PURE__ */ jsx(
|
|
3950
4167
|
CanvasTopBar,
|
|
@@ -4042,6 +4259,54 @@ function resolveReturnTarget(value, selection) {
|
|
|
4042
4259
|
return { label, selection: withOrigin(fromToSelection(from), "reference") };
|
|
4043
4260
|
}
|
|
4044
4261
|
|
|
4262
|
+
// src/toc/nav-model.ts
|
|
4263
|
+
function nav(selection) {
|
|
4264
|
+
return { key: selectionTargetKey(selection), selection };
|
|
4265
|
+
}
|
|
4266
|
+
function buildScriptNav(value) {
|
|
4267
|
+
const episodes = (value.episodes ?? []).map((episode, episodeIdx) => {
|
|
4268
|
+
const scenes = (episode.scenes ?? []).map((scene, sceneIdx) => {
|
|
4269
|
+
const selection2 = { kind: "scene", episodeIdx, sceneIdx };
|
|
4270
|
+
return {
|
|
4271
|
+
...nav(selection2),
|
|
4272
|
+
episodeIdx,
|
|
4273
|
+
sceneIdx,
|
|
4274
|
+
sceneId: scene.scene_id ?? "",
|
|
4275
|
+
label: getSceneNumberLabel(scene, sceneIdx),
|
|
4276
|
+
meta: getSceneSidebarLabel(scene, sceneIdx)
|
|
4277
|
+
};
|
|
4278
|
+
});
|
|
4279
|
+
const selection = { kind: "episode", episodeIdx };
|
|
4280
|
+
return {
|
|
4281
|
+
...nav(selection),
|
|
4282
|
+
episodeIdx,
|
|
4283
|
+
label: getEpisodeNumberLabel(episode, episodeIdx),
|
|
4284
|
+
title: getEpisodeTitle(episode, episodeIdx),
|
|
4285
|
+
sceneCount: scenes.length,
|
|
4286
|
+
scenes
|
|
4287
|
+
};
|
|
4288
|
+
});
|
|
4289
|
+
const actors = (value.actors ?? []).map((a) => {
|
|
4290
|
+
const id = (a.actor_id ?? "").trim();
|
|
4291
|
+
return { ...nav({ kind: "actor", actorId: id }), id, name: getActorDisplayName(a) };
|
|
4292
|
+
}).filter((e) => e.id);
|
|
4293
|
+
const locations = (value.locations ?? []).map((l) => {
|
|
4294
|
+
const id = (l.location_id ?? "").trim();
|
|
4295
|
+
return { ...nav({ kind: "location", locationId: id }), id, name: getLocationDisplayName(l) };
|
|
4296
|
+
}).filter((e) => e.id);
|
|
4297
|
+
const props = (value.props ?? []).map((p) => {
|
|
4298
|
+
const id = (p.prop_id ?? "").trim();
|
|
4299
|
+
return { ...nav({ kind: "prop", propId: id }), id, name: getPropDisplayName(p) };
|
|
4300
|
+
}).filter((e) => e.id);
|
|
4301
|
+
return {
|
|
4302
|
+
worldview: { ...nav({ kind: "worldview" }), label: "\u4E16\u754C\u89C2 & \u98CE\u683C" },
|
|
4303
|
+
episodes,
|
|
4304
|
+
actors,
|
|
4305
|
+
locations,
|
|
4306
|
+
props
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
|
|
4045
4310
|
// src/mutations/index.ts
|
|
4046
4311
|
var mutations_exports = {};
|
|
4047
4312
|
__export(mutations_exports, {
|
|
@@ -4083,6 +4348,6 @@ __export(mutations_exports, {
|
|
|
4083
4348
|
updateScenePropState: () => updateScenePropState
|
|
4084
4349
|
});
|
|
4085
4350
|
|
|
4086
|
-
export { DeleteConfirmButton, EMPTY_SELECTION, EditableText, Popover, ScriptContentType, ScriptEditor, SelectField, acceptActionIntoBaseline, acceptSceneIntoBaseline, applyActionAccept, applyActionReject, buildActionDiff, buildDiffFromBaseline, buildSelectionFrom, changeSetToDiffScenes, computeChangeSet, computeSceneActionDiff, findBaselineScene, fromToSelection, getActorCueVar, getActorMap, getEpisodeTitle, getLocationMap, getPropMap, getSceneContext, getSelectionEpisodeIdx, getSpeakerMap, isClipSelected, isEpisodeOpen, isSameSelectionTarget, isSceneSelected, mutations_exports as mutations, rejectActionToBaseline, rejectSceneToBaseline, sceneChangeKey, selectionFromScriptReference, selectionTargetKey, withOrigin };
|
|
4351
|
+
export { DeleteConfirmButton, EMPTY_SELECTION, EditableText, Popover, ScriptContentType, ScriptEditor, SelectField, acceptActionIntoBaseline, acceptSceneIntoBaseline, applyActionAccept, applyActionReject, buildActionDiff, buildDiffFromBaseline, buildScriptNav, buildSelectionFrom, changeSetToDiffScenes, computeChangeSet, computeSceneActionDiff, findBaselineScene, fromToSelection, getActorCueVar, getActorMap, getEpisodeTitle, getLocationMap, getPropMap, getSceneContext, getSelectionEpisodeIdx, getSpeakerMap, isClipSelected, isEpisodeOpen, isSameSelectionTarget, isSceneSelected, mutations_exports as mutations, rejectActionToBaseline, rejectSceneToBaseline, sceneChangeKey, selectionFromScriptReference, selectionTargetKey, withOrigin };
|
|
4087
4352
|
//# sourceMappingURL=index.js.map
|
|
4088
4353
|
//# sourceMappingURL=index.js.map
|