@lingjingai/script-editor 0.1.15 → 0.1.16

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 CHANGED
@@ -66,6 +66,8 @@ function Host({ script, setScript }) {
66
66
  - `disabled`、`dark`、`className`
67
67
 
68
68
  **HUD(顶栏)**
69
+ - `onTitleChange?(next: string)`:顶栏左侧常驻**剧本标题**(取 `value.title`)。传了它,标题变**点击即改**;不传则只读展示(`value.title` 为空时不显示)。包**不碰存储**——拿到新标题宿主自己持久化(可 `onEdit(p => ({...p, title}))` 写回 `script.json`,或另调重命名接口)。`0.1.16+`
70
+ - `topBarActions?: ReactNode`:顶栏**右侧**(格式切换/撤销重做旁)的**宿主注入槽**,放 stage 级业务按钮(如「删除剧本」「查看中间产物」)。包只渲染传入节点、零业务。`0.1.16+`
69
71
  - `saveStatus`:`"idle" | "saving" | "saved" | "error"`
70
72
  - `canUndo` / `canRedo` / `onUndo` / `onRedo` —— **Cmd/Ctrl+Z 撤销、Cmd/Ctrl+Shift+Z(或 Ctrl+Y)重做 已内建**(输入框内放行原生撤回),宿主**不要再自己接键盘监听**,否则会双触发(撤销两步)。`0.1.2+`
71
73
 
package/dist/index.d.ts CHANGED
@@ -488,6 +488,14 @@ interface ScriptEditorProps extends ScriptEditorAdapters {
488
488
  disabled?: boolean;
489
489
  className?: string;
490
490
  dark?: boolean;
491
+ /** when provided, the script title (value.title) becomes click-to-edit in the
492
+ * top bar; the host persists the new value (the editor never touches storage,
493
+ * matching the value + onEdit contract — just one more delegated hook). Omit →
494
+ * the title shows read-only (or nothing when value.title is empty). */
495
+ onTitleChange?: (next: string) => void;
496
+ /** host-injected actions for the top bar's right edge (e.g. 删除剧本 /
497
+ * 查看中间产物). The package only renders the node — all business stays host-side. */
498
+ topBarActions?: React.ReactNode;
491
499
  saveStatus?: SaveStatus;
492
500
  canUndo?: boolean;
493
501
  canRedo?: boolean;
@@ -504,7 +512,7 @@ interface ScriptEditorProps extends ScriptEditorAdapters {
504
512
  * scroll position across editor unmount/remount (tab switch / project nav). */
505
513
  scrollScopeKey?: string;
506
514
  }
507
- declare function ScriptEditor({ value, onEdit, selection: controlledSelection, onSelectionChange, format: controlledFormat, onFormatChange, disabled, className, dark, saveStatus, canUndo, canRedo, onUndo, onRedo, onEditingChange, referenceFocusRequest, scrollScopeKey, diff, asset, onAddChatReferences, onRequestAssetDelete, }: ScriptEditorProps): react.JSX.Element;
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;
508
516
 
509
517
  /**
510
518
  * Pure ScriptProject mutations.
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
  }
@@ -2014,169 +2198,6 @@ function getActorCueVar(actorId, tone = "dialogue") {
2014
2198
  const idx = hashActorId(id) % PALETTE_SIZE + 1;
2015
2199
  return `var(--lj-se-actor-cue-${idx})`;
2016
2200
  }
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
2201
  function formatStateChangeLabels(item, actorMap, locationMap, propMap) {
2181
2202
  const labels = (item.state_changes ?? []).map((change) => {
2182
2203
  const kind = change.target_kind?.trim() || "actor";
@@ -3752,6 +3773,8 @@ function ScriptEditor({
3752
3773
  disabled = false,
3753
3774
  className,
3754
3775
  dark,
3776
+ onTitleChange,
3777
+ topBarActions,
3755
3778
  saveStatus = "idle",
3756
3779
  canUndo,
3757
3780
  canRedo,
@@ -3926,13 +3949,17 @@ function ScriptEditor({
3926
3949
  /* @__PURE__ */ jsx(
3927
3950
  CanvasTopBar,
3928
3951
  {
3952
+ title: value.title,
3953
+ onTitleChange: disabled ? void 0 : onTitleChange,
3954
+ titleDisabled: disabled,
3929
3955
  contextLabel,
3930
3956
  format,
3931
3957
  onFormatChange: setFormat,
3932
3958
  canUndo,
3933
3959
  canRedo,
3934
3960
  onUndo,
3935
- onRedo
3961
+ onRedo,
3962
+ actions: topBarActions
3936
3963
  }
3937
3964
  ),
3938
3965
  /* @__PURE__ */ jsxs("div", { className: "lj-se-canvas", ref: canvasRef, children: [
@@ -3990,7 +4017,7 @@ function buildContextLabel(value, selection) {
3990
4017
  ] });
3991
4018
  }
3992
4019
  }
3993
- return /* @__PURE__ */ jsx("span", { className: "lj-se-ctx-kicker", children: "\u5267\u672C" });
4020
+ return null;
3994
4021
  }
3995
4022
  function Ctx({ kicker, name }) {
3996
4023
  return /* @__PURE__ */ jsxs("span", { className: "lj-se-ctx", children: [