@lingjingai/script-editor 0.1.15 → 0.1.17

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/dist/index.js CHANGED
@@ -608,6 +608,169 @@ function EntityGroup({
608
608
  )) : null
609
609
  ] });
610
610
  }
611
+ var EditingSignalContext = createContext(null);
612
+ var LIVE_COMMIT_MS = 400;
613
+ function caretOffsetFromPoint(x, y) {
614
+ const d = document;
615
+ if (typeof d.caretRangeFromPoint === "function") {
616
+ const r = d.caretRangeFromPoint(x, y);
617
+ return r ? r.startOffset : null;
618
+ }
619
+ if (typeof d.caretPositionFromPoint === "function") {
620
+ const p = d.caretPositionFromPoint(x, y);
621
+ return p ? p.offset : null;
622
+ }
623
+ return null;
624
+ }
625
+ function EditableText({
626
+ value,
627
+ onSave,
628
+ placeholder,
629
+ multiline = false,
630
+ disabled = false,
631
+ className,
632
+ autoEdit = false,
633
+ onEditEnd,
634
+ spanProps
635
+ }) {
636
+ const [editing, setEditing] = useState(autoEdit);
637
+ const [draft, setDraft] = useState(value);
638
+ const caretRef = useRef(null);
639
+ const inputRef = useRef(null);
640
+ const committedRef = useRef(value);
641
+ const editingSignal = useContext(EditingSignalContext);
642
+ const liveTimerRef = useRef(null);
643
+ const composingRef = useRef(false);
644
+ const draftRef = useRef(value);
645
+ const onSaveRef = useRef(onSave);
646
+ onSaveRef.current = onSave;
647
+ const clearLiveTimer = () => {
648
+ if (liveTimerRef.current) {
649
+ clearTimeout(liveTimerRef.current);
650
+ liveTimerRef.current = null;
651
+ }
652
+ };
653
+ const flushLive = () => {
654
+ clearLiveTimer();
655
+ const next = draftRef.current;
656
+ if (next !== committedRef.current) {
657
+ committedRef.current = next;
658
+ onSaveRef.current(next);
659
+ }
660
+ };
661
+ const scheduleLive = () => {
662
+ clearLiveTimer();
663
+ liveTimerRef.current = setTimeout(() => {
664
+ liveTimerRef.current = null;
665
+ if (composingRef.current) return;
666
+ flushLive();
667
+ }, LIVE_COMMIT_MS);
668
+ };
669
+ useEffect(() => () => flushLive(), []);
670
+ useEffect(() => {
671
+ if (!editing || !editingSignal) return;
672
+ editingSignal.inc();
673
+ return () => editingSignal.dec();
674
+ }, [editing, editingSignal]);
675
+ useEffect(() => {
676
+ if (value === committedRef.current) return;
677
+ const userDiverged = editing && draft !== committedRef.current;
678
+ if (userDiverged) return;
679
+ setDraft(value);
680
+ committedRef.current = value;
681
+ }, [value, editing, draft]);
682
+ useEffect(() => {
683
+ if (autoEdit) setEditing(true);
684
+ }, [autoEdit]);
685
+ useLayoutEffect(() => {
686
+ if (!editing) return;
687
+ const el = inputRef.current;
688
+ if (!el) return;
689
+ if (multiline) autoSize(el);
690
+ el.focus();
691
+ const caret = caretRef.current;
692
+ const pos = caret == null ? el.value.length : Math.min(caret, el.value.length);
693
+ try {
694
+ el.setSelectionRange(pos, pos);
695
+ } catch {
696
+ }
697
+ caretRef.current = null;
698
+ }, [editing, multiline]);
699
+ function beginEdit(e) {
700
+ if (disabled) return;
701
+ const sel = window.getSelection();
702
+ if (sel && !sel.isCollapsed) return;
703
+ caretRef.current = caretOffsetFromPoint(e.clientX, e.clientY);
704
+ setDraft(committedRef.current);
705
+ draftRef.current = committedRef.current;
706
+ setEditing(true);
707
+ }
708
+ function commit() {
709
+ clearLiveTimer();
710
+ setEditing(false);
711
+ onEditEnd?.();
712
+ if (draft !== committedRef.current) {
713
+ committedRef.current = draft;
714
+ onSave(draft);
715
+ }
716
+ }
717
+ function cancel() {
718
+ clearLiveTimer();
719
+ setDraft(committedRef.current);
720
+ draftRef.current = committedRef.current;
721
+ setEditing(false);
722
+ onEditEnd?.();
723
+ }
724
+ if (!editing) {
725
+ return /* @__PURE__ */ jsx(
726
+ "span",
727
+ {
728
+ ...spanProps,
729
+ className: "lj-se-editable" + (className ? " " + className : "") + (disabled ? " lj-se-editable--ro" : ""),
730
+ onClick: beginEdit,
731
+ children: value || /* @__PURE__ */ jsx("span", { className: "lj-se-ph", children: placeholder ?? "" })
732
+ }
733
+ );
734
+ }
735
+ const common = {
736
+ ref: inputRef,
737
+ value: draft,
738
+ className: "lj-se-editing" + (className ? " " + className : ""),
739
+ onChange: (e) => {
740
+ const v = e.target.value;
741
+ setDraft(v);
742
+ draftRef.current = v;
743
+ if (multiline) autoSize(e.target);
744
+ if (!composingRef.current) scheduleLive();
745
+ },
746
+ onCompositionStart: () => {
747
+ composingRef.current = true;
748
+ clearLiveTimer();
749
+ },
750
+ onCompositionEnd: (e) => {
751
+ composingRef.current = false;
752
+ draftRef.current = e.currentTarget.value;
753
+ scheduleLive();
754
+ },
755
+ onBlur: commit,
756
+ onKeyDown: (e) => {
757
+ if (e.key === "Escape") {
758
+ e.preventDefault();
759
+ cancel();
760
+ } else if (e.key === "Enter") {
761
+ if (!multiline || e.metaKey || e.ctrlKey) {
762
+ e.preventDefault();
763
+ commit();
764
+ }
765
+ }
766
+ }
767
+ };
768
+ return multiline ? /* @__PURE__ */ jsx("textarea", { ...common, rows: 1 }) : /* @__PURE__ */ jsx("input", { ...common, type: "text" });
769
+ }
770
+ function autoSize(el) {
771
+ el.style.height = "auto";
772
+ el.style.height = el.scrollHeight + "px";
773
+ }
611
774
  function FormatToggle({
612
775
  value,
613
776
  onChange
@@ -628,22 +791,43 @@ function FormatToggle({
628
791
  )) });
629
792
  }
630
793
  function CanvasTopBar({
794
+ title,
795
+ onTitleChange,
796
+ titleDisabled,
631
797
  contextLabel,
632
798
  format,
633
799
  onFormatChange,
634
800
  canUndo,
635
801
  canRedo,
636
802
  onUndo,
637
- onRedo
803
+ onRedo,
804
+ actions
638
805
  }) {
806
+ const hasTitle = onTitleChange != null || (title ?? "").length > 0;
639
807
  return /* @__PURE__ */ jsxs("header", { className: "lj-se-topbar", children: [
640
- /* @__PURE__ */ jsx("div", { className: "lj-se-topbar-left", children: contextLabel }),
808
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-topbar-left", children: [
809
+ hasTitle ? onTitleChange ? /* @__PURE__ */ jsx(
810
+ EditableText,
811
+ {
812
+ className: "lj-se-topbar-title",
813
+ value: title ?? "",
814
+ placeholder: "\u672A\u547D\u540D\u5267\u672C",
815
+ disabled: titleDisabled,
816
+ onSave: onTitleChange
817
+ }
818
+ ) : /* @__PURE__ */ jsx("span", { className: "lj-se-topbar-title lj-se-topbar-title--ro", children: title }) : null,
819
+ contextLabel ? /* @__PURE__ */ jsxs("span", { className: "lj-se-topbar-ctx", children: [
820
+ hasTitle ? /* @__PURE__ */ jsx("span", { className: "lj-se-topbar-sep", "aria-hidden": true, children: "/" }) : null,
821
+ contextLabel
822
+ ] }) : null
823
+ ] }),
641
824
  /* @__PURE__ */ jsxs("div", { className: "lj-se-topbar-right", children: [
642
825
  /* @__PURE__ */ jsx(FormatToggle, { value: format, onChange: onFormatChange }),
643
826
  onUndo || onRedo ? /* @__PURE__ */ jsxs("div", { className: "lj-se-topbar-undo", children: [
644
827
  /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-rowbtn", disabled: !canUndo, title: "\u64A4\u9500", onClick: onUndo, children: /* @__PURE__ */ jsx(Undo2, { className: "lj-se-ic-4", strokeWidth: 1.75 }) }),
645
828
  /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-rowbtn", disabled: !canRedo, title: "\u91CD\u505A", onClick: onRedo, children: /* @__PURE__ */ jsx(Redo2, { className: "lj-se-ic-4", strokeWidth: 1.75 }) })
646
- ] }) : null
829
+ ] }) : null,
830
+ actions ? /* @__PURE__ */ jsx("div", { className: "lj-se-topbar-actions", children: actions }) : null
647
831
  ] })
648
832
  ] });
649
833
  }
@@ -1176,11 +1360,12 @@ function buildLocationDistribution(data, locationId) {
1176
1360
  (episode.scenes ?? []).forEach((scene, si) => {
1177
1361
  const inIds = (scene.location_ids ?? []).some((i) => (i ?? "").trim() === locationId);
1178
1362
  const ctx = getSceneContext(scene);
1179
- const inCtx = (ctx.locations ?? []).some((i) => (i.location_id ?? "").trim() === locationId);
1180
- if (inIds || inCtx) {
1363
+ const ctxRef = (ctx.locations ?? []).find((i) => (i.location_id ?? "").trim() === locationId);
1364
+ if (inIds || ctxRef) {
1181
1365
  items.push({
1182
1366
  key: `location-${locationId}-${ei}-${si}`,
1183
- ...refFields(episode, ei, scene, si)
1367
+ ...refFields(episode, ei, scene, si),
1368
+ stateId: ctxRef?.state_id
1184
1369
  });
1185
1370
  }
1186
1371
  });
@@ -1194,13 +1379,15 @@ function buildPropDistribution(data, propId, propName) {
1194
1379
  (data.episodes ?? []).forEach((episode, ei) => {
1195
1380
  (episode.scenes ?? []).forEach((scene, si) => {
1196
1381
  const ctx = getSceneContext(scene);
1197
- const byRef = (ctx.props ?? []).some((i) => (i.prop_id ?? "").trim() === id);
1382
+ const ctxRef = (ctx.props ?? []).find((i) => (i.prop_id ?? "").trim() === id);
1383
+ const byRef = !!ctxRef;
1198
1384
  const byId = !byRef && id ? (scene.actions ?? []).some((i) => (i.content ?? "").includes(id)) : false;
1199
1385
  const byName = !byRef && !byId && needle ? (scene.actions ?? []).some((i) => (i.content ?? "").toLowerCase().includes(needle)) : false;
1200
1386
  if (byRef || byId || byName) {
1201
1387
  items.push({
1202
1388
  key: `prop-${id || needle}-${ei}-${si}`,
1203
- ...refFields(episode, ei, scene, si)
1389
+ ...refFields(episode, ei, scene, si),
1390
+ stateId: ctxRef?.state_id
1204
1391
  });
1205
1392
  }
1206
1393
  });
@@ -2014,169 +2201,6 @@ function getActorCueVar(actorId, tone = "dialogue") {
2014
2201
  const idx = hashActorId(id) % PALETTE_SIZE + 1;
2015
2202
  return `var(--lj-se-actor-cue-${idx})`;
2016
2203
  }
2017
- var EditingSignalContext = createContext(null);
2018
- var LIVE_COMMIT_MS = 400;
2019
- function caretOffsetFromPoint(x, y) {
2020
- const d = document;
2021
- if (typeof d.caretRangeFromPoint === "function") {
2022
- const r = d.caretRangeFromPoint(x, y);
2023
- return r ? r.startOffset : null;
2024
- }
2025
- if (typeof d.caretPositionFromPoint === "function") {
2026
- const p = d.caretPositionFromPoint(x, y);
2027
- return p ? p.offset : null;
2028
- }
2029
- return null;
2030
- }
2031
- function EditableText({
2032
- value,
2033
- onSave,
2034
- placeholder,
2035
- multiline = false,
2036
- disabled = false,
2037
- className,
2038
- autoEdit = false,
2039
- onEditEnd,
2040
- spanProps
2041
- }) {
2042
- const [editing, setEditing] = useState(autoEdit);
2043
- const [draft, setDraft] = useState(value);
2044
- const caretRef = useRef(null);
2045
- const inputRef = useRef(null);
2046
- const committedRef = useRef(value);
2047
- const editingSignal = useContext(EditingSignalContext);
2048
- const liveTimerRef = useRef(null);
2049
- const composingRef = useRef(false);
2050
- const draftRef = useRef(value);
2051
- const onSaveRef = useRef(onSave);
2052
- onSaveRef.current = onSave;
2053
- const clearLiveTimer = () => {
2054
- if (liveTimerRef.current) {
2055
- clearTimeout(liveTimerRef.current);
2056
- liveTimerRef.current = null;
2057
- }
2058
- };
2059
- const flushLive = () => {
2060
- clearLiveTimer();
2061
- const next = draftRef.current;
2062
- if (next !== committedRef.current) {
2063
- committedRef.current = next;
2064
- onSaveRef.current(next);
2065
- }
2066
- };
2067
- const scheduleLive = () => {
2068
- clearLiveTimer();
2069
- liveTimerRef.current = setTimeout(() => {
2070
- liveTimerRef.current = null;
2071
- if (composingRef.current) return;
2072
- flushLive();
2073
- }, LIVE_COMMIT_MS);
2074
- };
2075
- useEffect(() => () => flushLive(), []);
2076
- useEffect(() => {
2077
- if (!editing || !editingSignal) return;
2078
- editingSignal.inc();
2079
- return () => editingSignal.dec();
2080
- }, [editing, editingSignal]);
2081
- useEffect(() => {
2082
- if (value === committedRef.current) return;
2083
- const userDiverged = editing && draft !== committedRef.current;
2084
- if (userDiverged) return;
2085
- setDraft(value);
2086
- committedRef.current = value;
2087
- }, [value, editing, draft]);
2088
- useEffect(() => {
2089
- if (autoEdit) setEditing(true);
2090
- }, [autoEdit]);
2091
- useLayoutEffect(() => {
2092
- if (!editing) return;
2093
- const el = inputRef.current;
2094
- if (!el) return;
2095
- if (multiline) autoSize(el);
2096
- el.focus();
2097
- const caret = caretRef.current;
2098
- const pos = caret == null ? el.value.length : Math.min(caret, el.value.length);
2099
- try {
2100
- el.setSelectionRange(pos, pos);
2101
- } catch {
2102
- }
2103
- caretRef.current = null;
2104
- }, [editing, multiline]);
2105
- function beginEdit(e) {
2106
- if (disabled) return;
2107
- const sel = window.getSelection();
2108
- if (sel && !sel.isCollapsed) return;
2109
- caretRef.current = caretOffsetFromPoint(e.clientX, e.clientY);
2110
- setDraft(committedRef.current);
2111
- draftRef.current = committedRef.current;
2112
- setEditing(true);
2113
- }
2114
- function commit() {
2115
- clearLiveTimer();
2116
- setEditing(false);
2117
- onEditEnd?.();
2118
- if (draft !== committedRef.current) {
2119
- committedRef.current = draft;
2120
- onSave(draft);
2121
- }
2122
- }
2123
- function cancel() {
2124
- clearLiveTimer();
2125
- setDraft(committedRef.current);
2126
- draftRef.current = committedRef.current;
2127
- setEditing(false);
2128
- onEditEnd?.();
2129
- }
2130
- if (!editing) {
2131
- return /* @__PURE__ */ jsx(
2132
- "span",
2133
- {
2134
- ...spanProps,
2135
- className: "lj-se-editable" + (className ? " " + className : "") + (disabled ? " lj-se-editable--ro" : ""),
2136
- onClick: beginEdit,
2137
- children: value || /* @__PURE__ */ jsx("span", { className: "lj-se-ph", children: placeholder ?? "" })
2138
- }
2139
- );
2140
- }
2141
- const common = {
2142
- ref: inputRef,
2143
- value: draft,
2144
- className: "lj-se-editing" + (className ? " " + className : ""),
2145
- onChange: (e) => {
2146
- const v = e.target.value;
2147
- setDraft(v);
2148
- draftRef.current = v;
2149
- if (multiline) autoSize(e.target);
2150
- if (!composingRef.current) scheduleLive();
2151
- },
2152
- onCompositionStart: () => {
2153
- composingRef.current = true;
2154
- clearLiveTimer();
2155
- },
2156
- onCompositionEnd: (e) => {
2157
- composingRef.current = false;
2158
- draftRef.current = e.currentTarget.value;
2159
- scheduleLive();
2160
- },
2161
- onBlur: commit,
2162
- onKeyDown: (e) => {
2163
- if (e.key === "Escape") {
2164
- e.preventDefault();
2165
- cancel();
2166
- } else if (e.key === "Enter") {
2167
- if (!multiline || e.metaKey || e.ctrlKey) {
2168
- e.preventDefault();
2169
- commit();
2170
- }
2171
- }
2172
- }
2173
- };
2174
- return multiline ? /* @__PURE__ */ jsx("textarea", { ...common, rows: 1 }) : /* @__PURE__ */ jsx("input", { ...common, type: "text" });
2175
- }
2176
- function autoSize(el) {
2177
- el.style.height = "auto";
2178
- el.style.height = el.scrollHeight + "px";
2179
- }
2180
2204
  function formatStateChangeLabels(item, actorMap, locationMap, propMap) {
2181
2205
  const labels = (item.state_changes ?? []).map((change) => {
2182
2206
  const kind = change.target_kind?.trim() || "actor";
@@ -2872,33 +2896,238 @@ function DeleteConfirmButton({
2872
2896
  }
2873
2897
  );
2874
2898
  }
2875
- function AssetReferenceGrid({
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,
2876
3017
  refs,
2877
- highlight,
3018
+ states: states2,
2878
3019
  onJump
2879
3020
  }) {
2880
- if (refs.length === 0) return null;
2881
- return /* @__PURE__ */ jsx("div", { className: "lj-se-refgrid", children: refs.map(({ episodeIndex, episodeNumber, items }) => /* @__PURE__ */ jsxs("span", { className: "lj-se-refgrid-chunk", children: [
2882
- /* @__PURE__ */ jsxs("span", { className: "lj-se-refgrid-ep", children: [
2883
- "\u7B2C ",
2884
- String(episodeNumber).padStart(2, "0"),
2885
- " \u96C6"
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)) })
2886
3070
  ] }),
2887
- /* @__PURE__ */ jsx("span", { className: "lj-se-refgrid-scenes", children: items.map((ref) => {
2888
- const isCurrent = !!highlight && highlight.episodeIdx === ref.episodeIndex && highlight.sceneIdx === ref.sceneIndex;
2889
- return /* @__PURE__ */ jsx(
2890
- "button",
2891
- {
2892
- type: "button",
2893
- className: cn("lj-se-refgrid-scene", isCurrent && "is-current"),
2894
- title: ref.label,
2895
- onClick: () => onJump(ref.episodeIndex, ref.sceneIndex),
2896
- children: ref.sceneNumber
2897
- },
2898
- ref.key
2899
- );
2900
- }) })
2901
- ] }, episodeIndex)) });
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
+ ] });
2902
3131
  }
2903
3132
  var META = {
2904
3133
  actors: { kind: "actor" },
@@ -3030,12 +3259,20 @@ function AssetCardImpl({
3030
3259
  ] }, sid);
3031
3260
  }) }) : null
3032
3261
  ] }),
3033
- refs.length > 0 ? /* @__PURE__ */ jsx(
3034
- AssetReferenceGrid,
3035
- {
3036
- refs,
3037
- onJump: (episodeIdx, sceneIdx) => onSelect({ kind: "scene", episodeIdx, sceneIdx, from: fromRef, origin: "sidebar" })
3038
- }
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
+ )
3039
3276
  ) : null
3040
3277
  ] }) });
3041
3278
  }
@@ -3752,6 +3989,8 @@ function ScriptEditor({
3752
3989
  disabled = false,
3753
3990
  className,
3754
3991
  dark,
3992
+ onTitleChange,
3993
+ topBarActions,
3755
3994
  saveStatus = "idle",
3756
3995
  canUndo,
3757
3996
  canRedo,
@@ -3926,13 +4165,17 @@ function ScriptEditor({
3926
4165
  /* @__PURE__ */ jsx(
3927
4166
  CanvasTopBar,
3928
4167
  {
4168
+ title: value.title,
4169
+ onTitleChange: disabled ? void 0 : onTitleChange,
4170
+ titleDisabled: disabled,
3929
4171
  contextLabel,
3930
4172
  format,
3931
4173
  onFormatChange: setFormat,
3932
4174
  canUndo,
3933
4175
  canRedo,
3934
4176
  onUndo,
3935
- onRedo
4177
+ onRedo,
4178
+ actions: topBarActions
3936
4179
  }
3937
4180
  ),
3938
4181
  /* @__PURE__ */ jsxs("div", { className: "lj-se-canvas", ref: canvasRef, children: [
@@ -3990,7 +4233,7 @@ function buildContextLabel(value, selection) {
3990
4233
  ] });
3991
4234
  }
3992
4235
  }
3993
- return /* @__PURE__ */ jsx("span", { className: "lj-se-ctx-kicker", children: "\u5267\u672C" });
4236
+ return null;
3994
4237
  }
3995
4238
  function Ctx({ kicker, name }) {
3996
4239
  return /* @__PURE__ */ jsxs("span", { className: "lj-se-ctx", children: [