@lingjingai/script-editor 0.1.19 → 0.1.21

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
@@ -20,7 +20,7 @@
20
20
  ```ts
21
21
  import "@lingjingai/script-editor/style.css";
22
22
  ```
23
- - **构建产物**:消费方一律走 `dist`(`tsup` 编译好的 JS + d.ts + style.css + paper-fibers.png),无需编译包内 TS。`exports` 不带 `development` 条件(早期版本带过,会让 Vite **dev 模式**去找未发布的 `src/` 而报 `Failed to resolve import .../style.css` —— `0.1.1+` 已修)。
23
+ - **构建产物**:发布 / 外部消费走 `dist`(`tsup` 编译好的 JS + d.ts + style.css + paper-fibers.png),无需编译包内 TS。本仓 `apps/web` dev 通过 Vite `conditions:["development"]` `src/index.ts` + `src/styles.css`,这样改包内源码能 HMR 生效;外部没有该 condition 时自动落到 `dist`。
24
24
 
25
25
  ---
26
26
 
package/dist/index.d.ts CHANGED
@@ -163,7 +163,7 @@ type StudioSelectionFrom = {
163
163
  *
164
164
  * 不打 origin 时按 'sidebar' 等价处理(保守滚动)。
165
165
  */
166
- type StudioSelectionOrigin = "sidebar" | "scroll" | "reference" | "diff" | "chat" | "init" | "episode-toggle";
166
+ type StudioSelectionOrigin = "sidebar" | "scroll" | "reference" | "diff" | "search" | "chat" | "init" | "episode-toggle";
167
167
  declare function buildSelectionFrom(kind: StudioSelectionFrom["kind"], id: string): StudioSelectionFrom | undefined;
168
168
  declare function fromToSelection(from: StudioSelectionFrom): StudioSelection;
169
169
  /** 公共字段:所有 selection variant 都可携带(origin 防双向联动死循环;from 资产回链)。 */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createContext, memo, useState, useMemo, useCallback, Fragment, useRef, useContext, useEffect, useLayoutEffect, useDeferredValue } from 'react';
2
- import { Plus, Trash2, X, Check, Eraser, ArrowUp, ArrowDown, GripVertical, MessageCircle, Film, Brain, BookOpen, Scissors, ArrowUpToLine, BookText, ChevronRight, FileText, Users, MapPin, Package, Undo2, Redo2, MessageSquareQuote, ArrowLeft, ChevronLeft, Loader2, AlertCircle } from 'lucide-react';
2
+ import { Plus, Trash2, X, Check, ArrowUp, ArrowDown, GripVertical, MessageCircle, Film, Brain, BookOpen, Eraser, Scissors, ArrowUpToLine, BookText, ChevronRight, FileText, Users, MapPin, Package, Undo2, Redo2, MessageSquareQuote, ArrowLeft, ChevronLeft, Search, Loader2, AlertCircle } from 'lucide-react';
3
3
  import { jsxs, jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
4
4
  import { Virtuoso } from 'react-virtuoso';
5
5
  import { createPortal } from 'react-dom';
@@ -596,6 +596,30 @@ function EntityGroup({
596
596
  )) : null
597
597
  ] });
598
598
  }
599
+ var HighlightContext = createContext("");
600
+ function HighlightedText({ text }) {
601
+ const highlight = useContext(HighlightContext);
602
+ return /* @__PURE__ */ jsx(Fragment$1, { children: renderHighlightedText(text, highlight) });
603
+ }
604
+ function renderHighlightedText(value, query) {
605
+ const needle = query.trim();
606
+ if (!needle) return value;
607
+ const lowerValue = value.toLocaleLowerCase();
608
+ const lowerNeedle = needle.toLocaleLowerCase();
609
+ const out = [];
610
+ let from = 0;
611
+ let hit = lowerValue.indexOf(lowerNeedle);
612
+ while (hit >= 0) {
613
+ if (hit > from) out.push(value.slice(from, hit));
614
+ out.push(
615
+ /* @__PURE__ */ jsx("mark", { className: "lj-se-search-hit", children: value.slice(hit, hit + needle.length) }, `${hit}-${out.length}`)
616
+ );
617
+ from = hit + needle.length;
618
+ hit = lowerValue.indexOf(lowerNeedle, from);
619
+ }
620
+ if (from < value.length) out.push(value.slice(from));
621
+ return out.length > 0 ? out : value;
622
+ }
599
623
  var EditingSignalContext = createContext(null);
600
624
  var LIVE_COMMIT_MS = 400;
601
625
  function caretOffsetFromPoint(x, y) {
@@ -627,6 +651,7 @@ function EditableText({
627
651
  const inputRef = useRef(null);
628
652
  const committedRef = useRef(value);
629
653
  const editingSignal = useContext(EditingSignalContext);
654
+ const highlight = useContext(HighlightContext);
630
655
  const liveTimerRef = useRef(null);
631
656
  const composingRef = useRef(false);
632
657
  const draftRef = useRef(value);
@@ -710,13 +735,14 @@ function EditableText({
710
735
  onEditEnd?.();
711
736
  }
712
737
  if (!editing) {
738
+ const content = value ? renderHighlightedText(value, highlight) : /* @__PURE__ */ jsx("span", { className: "lj-se-ph", children: placeholder ?? "" });
713
739
  return /* @__PURE__ */ jsx(
714
740
  "span",
715
741
  {
716
742
  ...spanProps,
717
743
  className: "lj-se-editable" + (className ? " " + className : "") + (disabled ? " lj-se-editable--ro" : ""),
718
744
  onClick: beginEdit,
719
- children: value || /* @__PURE__ */ jsx("span", { className: "lj-se-ph", children: placeholder ?? "" })
745
+ children: content
720
746
  }
721
747
  );
722
748
  }
@@ -1746,6 +1772,9 @@ function SelectField({
1746
1772
  style,
1747
1773
  emptyOptionLabel
1748
1774
  }) {
1775
+ const highlight = useContext(HighlightContext).trim();
1776
+ const selectedLabel = options.find((o) => o.value === value)?.label ?? (value ? value : placeholder ?? emptyOptionLabel ?? "\u2014");
1777
+ const highlighted = !!highlight && selectedLabel.toLocaleLowerCase().includes(highlight.toLocaleLowerCase());
1749
1778
  return /* @__PURE__ */ jsxs(
1750
1779
  "select",
1751
1780
  {
@@ -1754,7 +1783,7 @@ function SelectField({
1754
1783
  title,
1755
1784
  onChange: (e) => onChange(e.target.value),
1756
1785
  style,
1757
- className: cn("lj-se-select", mono && "lj-se-mono", className),
1786
+ className: cn("lj-se-select", mono && "lj-se-mono", highlighted && "is-search-hit", className),
1758
1787
  children: [
1759
1788
  !value || !options.some((o) => o.value === value) ? /* @__PURE__ */ jsx("option", { value, children: placeholder ?? emptyOptionLabel ?? "\u2014" }) : null,
1760
1789
  options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
@@ -1778,7 +1807,7 @@ function AssetStateChip({
1778
1807
  const opts = stateOptions(states2);
1779
1808
  const trimmed = (currentStateId ?? "").trim();
1780
1809
  return /* @__PURE__ */ jsxs("span", { className: cn("lj-se-chip", orphan && "is-orphan"), title: orphan ? `${name} \u4E0D\u5B58\u5728` : void 0, children: [
1781
- onNavigate ? /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-chip-name lj-se-chip-name--link", title: `\u8DF3\u5230 ${name}`, onClick: onNavigate, children: name }) : /* @__PURE__ */ jsx("span", { className: "lj-se-chip-name", children: name }),
1810
+ onNavigate ? /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-chip-name lj-se-chip-name--link", title: `\u8DF3\u5230 ${name}`, onClick: onNavigate, children: /* @__PURE__ */ jsx(HighlightedText, { text: name }) }) : /* @__PURE__ */ jsx("span", { className: "lj-se-chip-name", children: /* @__PURE__ */ jsx(HighlightedText, { text: name }) }),
1782
1811
  /* @__PURE__ */ jsx("span", { className: "lj-se-chip-sep", children: "\xB7" }),
1783
1812
  /* @__PURE__ */ jsx(
1784
1813
  SelectField,
@@ -2267,7 +2296,7 @@ function SceneItemRowImpl({
2267
2296
  className: "lj-se-cue lj-se-cue--structured",
2268
2297
  style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0,
2269
2298
  children: [
2270
- getDialogueSpeakerLabel(item, actorMap, speakerMap) || "\u672A\u77E5\u53D1\u58F0\u6E90",
2299
+ /* @__PURE__ */ jsx(HighlightedText, { text: getDialogueSpeakerLabel(item, actorMap, speakerMap) || "\u672A\u77E5\u53D1\u58F0\u6E90" }),
2271
2300
  getDialogueDeliveryLabel(item.delivery) ? /* @__PURE__ */ jsxs("span", { className: "lj-se-cue-delivery", children: [
2272
2301
  "\uFF08",
2273
2302
  getDialogueDeliveryLabel(item.delivery),
@@ -2359,7 +2388,7 @@ function SceneItemRowImpl({
2359
2388
  const tone = isInner ? "inner" : "dialogue";
2360
2389
  const tag = isInner ? emotion ? `OS\xB7${emotion}` : "OS" : emotion;
2361
2390
  body = /* @__PURE__ */ jsxs("p", { className: cn("lj-se-asian-cueline", isInner && "is-inner"), children: [
2362
- multiSpeaker ? /* @__PURE__ */ jsx("strong", { className: "lj-se-asian-speaker", style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0, children: getDialogueSpeakerLabel(item, actorMap, speakerMap) }) : /* @__PURE__ */ jsx(
2391
+ multiSpeaker ? /* @__PURE__ */ jsx("strong", { className: "lj-se-asian-speaker", style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0, children: /* @__PURE__ */ jsx(HighlightedText, { text: getDialogueSpeakerLabel(item, actorMap, speakerMap) }) }) : /* @__PURE__ */ jsx(
2363
2392
  SelectField,
2364
2393
  {
2365
2394
  value: resolvedActorId,
@@ -2753,56 +2782,6 @@ function removeEntityState(data, kind, id, stateId) {
2753
2782
  states: (e.states ?? []).filter((s) => (s.state_id ?? "").trim() !== stateId)
2754
2783
  }));
2755
2784
  }
2756
- var CONFIRM_TTL_MS = 2500;
2757
- function DeleteConfirmButton({
2758
- onConfirm,
2759
- disabled,
2760
- targetScope,
2761
- title = "\u5220\u9664\uFF1F"
2762
- }) {
2763
- const [confirming, setConfirming] = useState(false);
2764
- const timerRef = useRef(null);
2765
- useEffect(
2766
- () => () => {
2767
- if (timerRef.current) clearTimeout(timerRef.current);
2768
- },
2769
- []
2770
- );
2771
- const cancel = () => {
2772
- if (timerRef.current) {
2773
- clearTimeout(timerRef.current);
2774
- timerRef.current = null;
2775
- }
2776
- setConfirming(false);
2777
- };
2778
- const handleClick = () => {
2779
- if (confirming) {
2780
- cancel();
2781
- void onConfirm();
2782
- return;
2783
- }
2784
- setConfirming(true);
2785
- if (timerRef.current) clearTimeout(timerRef.current);
2786
- timerRef.current = setTimeout(() => {
2787
- setConfirming(false);
2788
- timerRef.current = null;
2789
- }, CONFIRM_TTL_MS);
2790
- };
2791
- return /* @__PURE__ */ jsx(
2792
- "button",
2793
- {
2794
- type: "button",
2795
- disabled,
2796
- "data-delete-target": targetScope,
2797
- "data-confirming": confirming || void 0,
2798
- onClick: handleClick,
2799
- "aria-label": confirming ? `\u518D\u70B9\u4E00\u6B21\u786E\u8BA4\uFF1A${title}` : title,
2800
- title: confirming ? "\u518D\u70B9\u4E00\u6B21\u786E\u8BA4\uFF082.5 \u79D2\u540E\u81EA\u52A8\u53D6\u6D88\uFF09" : title,
2801
- className: cn("lj-se-del", confirming && "is-confirming"),
2802
- children: confirming ? /* @__PURE__ */ jsx(Check, { className: "lj-se-icon", strokeWidth: 2.2 }) : /* @__PURE__ */ jsx(Eraser, { className: "lj-se-icon", strokeWidth: 1.5 })
2803
- }
2804
- );
2805
- }
2806
2785
  var SPACE_ZH = { interior: "\u5185", exterior: "\u5916" };
2807
2786
  var TIME_ZH = { day: "\u65E5", night: "\u591C", dawn: "\u6668", dusk: "\u66AE" };
2808
2787
  function resolveCueId(item, actorMap, speakerMap) {
@@ -3044,6 +3023,38 @@ var META = {
3044
3023
  function newStateId(id) {
3045
3024
  return `st_${id}_${Date.now().toString(36)}`;
3046
3025
  }
3026
+ function StateDeleteButton({ stateName, onConfirm }) {
3027
+ const [open, setOpen] = useState(false);
3028
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3029
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-assetcard-state-del", title: "\u5220\u9664\u8BE5\u72B6\u6001", "aria-label": "\u5220\u9664\u8BE5\u72B6\u6001", onClick: () => setOpen(true), children: /* @__PURE__ */ jsx(X, { className: "lj-se-ic-3", strokeWidth: 1.8 }) }),
3030
+ open ? /* @__PURE__ */ jsx("div", { className: "lj-se-modal-backdrop", role: "presentation", onMouseDown: () => setOpen(false), children: /* @__PURE__ */ jsxs("div", { className: "lj-se-modal", role: "dialog", "aria-modal": "true", "aria-labelledby": "lj-se-delete-state-title", onMouseDown: (e) => e.stopPropagation(), children: [
3031
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-modal-head", children: [
3032
+ /* @__PURE__ */ jsx("h2", { id: "lj-se-delete-state-title", className: "lj-se-modal-title", children: "\u5220\u9664\u72B6\u6001" }),
3033
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-modal-x", "aria-label": "\u5173\u95ED", onClick: () => setOpen(false), children: /* @__PURE__ */ jsx(X, { className: "lj-se-ic-3", strokeWidth: 1.8 }) })
3034
+ ] }),
3035
+ /* @__PURE__ */ jsxs("p", { className: "lj-se-modal-body", children: [
3036
+ "\u786E\u8BA4\u5220\u9664\u300C",
3037
+ stateName || "\u672A\u547D\u540D\u72B6\u6001",
3038
+ "\u300D\uFF1F"
3039
+ ] }),
3040
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-modal-actions", children: [
3041
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-modal-btn", onClick: () => setOpen(false), children: "\u53D6\u6D88" }),
3042
+ /* @__PURE__ */ jsx(
3043
+ "button",
3044
+ {
3045
+ type: "button",
3046
+ className: "lj-se-modal-btn lj-se-modal-btn-danger",
3047
+ onClick: () => {
3048
+ setOpen(false);
3049
+ onConfirm();
3050
+ },
3051
+ children: "\u5220\u9664"
3052
+ }
3053
+ )
3054
+ ] })
3055
+ ] }) }) : null
3056
+ ] });
3057
+ }
3047
3058
  function AssetCardImpl({
3048
3059
  data,
3049
3060
  group,
@@ -3075,7 +3086,7 @@ function AssetCardImpl({
3075
3086
  const description = rawDescription.trim();
3076
3087
  const states2 = entity.states ?? [];
3077
3088
  const fromRef = buildSelectionFrom(kind, assetId);
3078
- return /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-row", children: /* @__PURE__ */ jsxs("article", { className: "lj-se-assetcard", "data-no-quote-selection": true, children: [
3089
+ return /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-row", children: /* @__PURE__ */ jsx(HighlightContext.Provider, { value: "", children: /* @__PURE__ */ jsxs("article", { className: "lj-se-assetcard", "data-no-quote-selection": true, children: [
3079
3090
  /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-head", children: [
3080
3091
  /* @__PURE__ */ jsx("span", { className: "lj-se-assetcard-name", children: /* @__PURE__ */ jsx(
3081
3092
  EditableText,
@@ -3150,7 +3161,13 @@ function AssetCardImpl({
3150
3161
  onSave: (v) => onEdit((p) => updateEntityStateField(p, kind, assetId, sid, "state_name", v))
3151
3162
  }
3152
3163
  ),
3153
- !disabled && (state.state_id ?? "").trim() ? /* @__PURE__ */ jsx(DeleteConfirmButton, { targetScope: "row", title: "\u5220\u9664\u8BE5\u72B6\u6001\uFF1F", onConfirm: () => onEdit((p) => removeEntityState(p, kind, assetId, sid)) }) : null
3164
+ !disabled && (state.state_id ?? "").trim() ? /* @__PURE__ */ jsx(
3165
+ StateDeleteButton,
3166
+ {
3167
+ stateName: state.state_name ?? state.name ?? "",
3168
+ onConfirm: () => onEdit((p) => removeEntityState(p, kind, assetId, sid))
3169
+ }
3170
+ ) : null
3154
3171
  ] }),
3155
3172
  /* @__PURE__ */ jsx(
3156
3173
  EditableText,
@@ -3181,7 +3198,7 @@ function AssetCardImpl({
3181
3198
  }
3182
3199
  )
3183
3200
  ) : null
3184
- ] }) });
3201
+ ] }) }) });
3185
3202
  }
3186
3203
  function areEqual(prev, next) {
3187
3204
  if (prev.group !== next.group || prev.assetId !== next.assetId || prev.disabled !== next.disabled) return false;
@@ -3884,6 +3901,250 @@ function QuoteLayer({
3884
3901
  }
3885
3902
  );
3886
3903
  }
3904
+ function ScriptSearchBox({
3905
+ query,
3906
+ current,
3907
+ total,
3908
+ inputRef,
3909
+ onQueryChange,
3910
+ onPrev,
3911
+ onNext,
3912
+ onClose
3913
+ }) {
3914
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-search", children: [
3915
+ /* @__PURE__ */ jsx(Search, { className: "lj-se-ic-3", strokeWidth: 2 }),
3916
+ /* @__PURE__ */ jsx(
3917
+ "input",
3918
+ {
3919
+ ref: inputRef,
3920
+ className: "lj-se-search-input",
3921
+ value: query,
3922
+ placeholder: "\u641C\u7D22",
3923
+ "aria-label": "\u641C\u7D22\u5267\u672C",
3924
+ onChange: (e) => onQueryChange(e.target.value),
3925
+ onKeyDown: (e) => {
3926
+ if (e.key === "Enter") {
3927
+ e.preventDefault();
3928
+ if (e.shiftKey) onPrev();
3929
+ else onNext();
3930
+ } else if (e.key === "Escape") {
3931
+ e.preventDefault();
3932
+ onClose();
3933
+ }
3934
+ }
3935
+ }
3936
+ ),
3937
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-search-count", children: [
3938
+ total > 0 ? current + 1 : 0,
3939
+ "/",
3940
+ total
3941
+ ] }),
3942
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-search-btn", title: "\u4E0A\u4E00\u5904", disabled: total === 0, onClick: onPrev, children: /* @__PURE__ */ jsx(ChevronLeft, { className: "lj-se-ic-3", strokeWidth: 2.25 }) }),
3943
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-search-btn", title: "\u4E0B\u4E00\u5904", disabled: total === 0, onClick: onNext, children: /* @__PURE__ */ jsx(ChevronRight, { className: "lj-se-ic-3", strokeWidth: 2.25 }) }),
3944
+ /* @__PURE__ */ jsx("button", { type: "button", className: "lj-se-search-btn", title: "\u5173\u95ED\u641C\u7D22", onClick: onClose, children: /* @__PURE__ */ jsx(X, { className: "lj-se-ic-3", strokeWidth: 2.25 }) })
3945
+ ] });
3946
+ }
3947
+
3948
+ // src/search/search-index.ts
3949
+ var SPACE_LABEL = {
3950
+ interior: "INT. \u5185",
3951
+ exterior: "EXT. \u5916"
3952
+ };
3953
+ var TIME_LABEL = {
3954
+ day: "DAY \u65E5",
3955
+ night: "NIGHT \u591C",
3956
+ dawn: "DAWN \u6668",
3957
+ dusk: "DUSK \u66AE"
3958
+ };
3959
+ function add(parts, value) {
3960
+ const text = value?.trim();
3961
+ if (text) parts.push(text);
3962
+ }
3963
+ function findStateName(states2, stateId) {
3964
+ const id = stateId?.trim();
3965
+ if (!id) return "";
3966
+ return getStateDisplayName(states2?.find((state) => state.state_id?.trim() === id));
3967
+ }
3968
+ function normalize(value) {
3969
+ return value.trim().toLocaleLowerCase();
3970
+ }
3971
+ function sceneSearchText(scene, sceneIdx, maps) {
3972
+ const parts = [];
3973
+ const context = getSceneContext(scene);
3974
+ const primaryLocationId = getPrimarySceneLocationId(scene);
3975
+ const location = primaryLocationId ? maps.locationMap.get(primaryLocationId) : void 0;
3976
+ add(parts, String(getSceneNumber(scene, sceneIdx)));
3977
+ add(parts, SPACE_LABEL[scene.environment?.space ?? ""] ?? scene.environment?.space);
3978
+ add(parts, location ? getLocationDisplayName(location) : primaryLocationId);
3979
+ add(parts, TIME_LABEL[scene.environment?.time ?? ""] ?? scene.environment?.time);
3980
+ for (const ref of context.actors ?? []) {
3981
+ const id = ref.actor_id?.trim();
3982
+ if (!id) continue;
3983
+ const actor = maps.actorMap.get(id);
3984
+ add(parts, actor ? getActorDisplayName(actor) : id);
3985
+ add(parts, findStateName(actor?.states, ref.state_id));
3986
+ }
3987
+ for (const ref of context.props ?? []) {
3988
+ const id = ref.prop_id?.trim();
3989
+ if (!id) continue;
3990
+ const prop = maps.propMap.get(id);
3991
+ add(parts, prop ? getPropDisplayName(prop) : id);
3992
+ add(parts, findStateName(prop?.states, ref.state_id));
3993
+ }
3994
+ for (const item of scene.actions ?? []) {
3995
+ addItemSearchText(parts, item, maps.actorMap, maps.speakerMap);
3996
+ }
3997
+ return parts.join("\n");
3998
+ }
3999
+ function addItemSearchText(parts, item, actorMap, speakerMap) {
4000
+ add(parts, item.content);
4001
+ add(parts, translateEmotionLabel(item.emotion));
4002
+ if (isActorBearingContentType(item.type)) {
4003
+ add(parts, getDialogueSpeakerLabel(item, actorMap, speakerMap));
4004
+ }
4005
+ for (const line of item.lines ?? []) {
4006
+ add(parts, getDialogueSpeakerLabel({ speaker_id: line.speaker_id }, actorMap, speakerMap));
4007
+ add(parts, line.content);
4008
+ }
4009
+ }
4010
+ function buildScriptSearchEntries(value) {
4011
+ const entries = [];
4012
+ const maps = {
4013
+ actorMap: getActorMap(value.actors ?? []),
4014
+ speakerMap: getSpeakerMap(value.speakers ?? []),
4015
+ locationMap: getLocationMap(value.locations ?? []),
4016
+ propMap: getPropMap(value.props ?? [])
4017
+ };
4018
+ const title = value.title?.trim();
4019
+ if (title && (value.episodes?.length ?? 0) > 0) {
4020
+ entries.push({
4021
+ key: "title",
4022
+ text: title,
4023
+ selection: { kind: "episode", episodeIdx: 0 }
4024
+ });
4025
+ }
4026
+ (value.episodes ?? []).forEach((episode, episodeIdx) => {
4027
+ entries.push({
4028
+ key: `episode:${episodeIdx}`,
4029
+ text: getEpisodeTitle(episode, episodeIdx),
4030
+ selection: { kind: "episode", episodeIdx }
4031
+ });
4032
+ (episode.scenes ?? []).forEach((scene, sceneIdx) => {
4033
+ entries.push({
4034
+ key: `scene:${episodeIdx}:${sceneIdx}`,
4035
+ text: sceneSearchText(scene, sceneIdx, maps),
4036
+ selection: { kind: "scene", episodeIdx, sceneIdx }
4037
+ });
4038
+ });
4039
+ });
4040
+ return entries;
4041
+ }
4042
+ function findScriptSearchMatches(entries, query) {
4043
+ const needle = normalize(query);
4044
+ if (!needle) return [];
4045
+ const matches = [];
4046
+ for (const entry of entries) {
4047
+ if (normalize(entry.text).includes(needle)) {
4048
+ matches.push({ entry, index: matches.length });
4049
+ }
4050
+ }
4051
+ return matches;
4052
+ }
4053
+ function ScriptSearchLayer({
4054
+ value,
4055
+ disabled,
4056
+ rootRef,
4057
+ selection,
4058
+ onSelectionChange,
4059
+ children
4060
+ }) {
4061
+ const editorActiveRef = useRef(false);
4062
+ const inputRef = useRef(null);
4063
+ const [open, setOpen] = useState(false);
4064
+ const [query, setQuery] = useState("");
4065
+ const [cursor, setCursor] = useState(0);
4066
+ const entries = useMemo(() => buildScriptSearchEntries(value), [value]);
4067
+ const matches = useMemo(() => findScriptSearchMatches(entries, query), [entries, query]);
4068
+ const total = matches.length;
4069
+ const current = total > 0 ? Math.min(cursor, total - 1) : 0;
4070
+ const selectMatch = useCallback(
4071
+ (index) => {
4072
+ const match = matches[index];
4073
+ if (!match) return;
4074
+ const next = withOrigin(match.entry.selection, "search");
4075
+ setCursor(index);
4076
+ if (!isSameSelectionTarget(next, selection)) onSelectionChange(next);
4077
+ },
4078
+ [matches, onSelectionChange, selection]
4079
+ );
4080
+ const openSearch = useCallback(() => {
4081
+ setOpen(true);
4082
+ requestAnimationFrame(() => {
4083
+ inputRef.current?.focus();
4084
+ inputRef.current?.select();
4085
+ });
4086
+ }, []);
4087
+ const closeSearch = useCallback(() => setOpen(false), []);
4088
+ const updateQuery = useCallback((next) => {
4089
+ setQuery(next);
4090
+ setCursor(0);
4091
+ }, []);
4092
+ const nextMatch = useCallback(() => {
4093
+ if (total > 0) selectMatch((current + 1) % total);
4094
+ }, [current, selectMatch, total]);
4095
+ const prevMatch = useCallback(() => {
4096
+ if (total > 0) selectMatch((current - 1 + total) % total);
4097
+ }, [current, selectMatch, total]);
4098
+ useEffect(() => {
4099
+ if (open && total > 0) selectMatch(current);
4100
+ }, [current, open, selectMatch, total]);
4101
+ useEffect(() => {
4102
+ if (!disabled && open) openSearch();
4103
+ }, [disabled, open, openSearch]);
4104
+ useEffect(() => {
4105
+ if (disabled) return;
4106
+ const rememberActive = (target) => {
4107
+ editorActiveRef.current = target instanceof Node && !!rootRef.current?.contains(target);
4108
+ };
4109
+ const onPointerDown = (e) => rememberActive(e.target);
4110
+ const onFocusIn = (e) => rememberActive(e.target);
4111
+ const onKeyDown = (e) => {
4112
+ if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== "f") return;
4113
+ const target = e.target;
4114
+ const targetInside = target instanceof Node && !!rootRef.current?.contains(target);
4115
+ if (!targetInside) {
4116
+ if (target instanceof HTMLElement && target !== document.body) return;
4117
+ if (!editorActiveRef.current) return;
4118
+ }
4119
+ e.preventDefault();
4120
+ openSearch();
4121
+ };
4122
+ document.addEventListener("pointerdown", onPointerDown, true);
4123
+ document.addEventListener("focusin", onFocusIn, true);
4124
+ document.addEventListener("keydown", onKeyDown, true);
4125
+ return () => {
4126
+ document.removeEventListener("pointerdown", onPointerDown, true);
4127
+ document.removeEventListener("focusin", onFocusIn, true);
4128
+ document.removeEventListener("keydown", onKeyDown, true);
4129
+ };
4130
+ }, [disabled, openSearch, rootRef]);
4131
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
4132
+ open ? /* @__PURE__ */ jsx(
4133
+ ScriptSearchBox,
4134
+ {
4135
+ query,
4136
+ current,
4137
+ total,
4138
+ inputRef,
4139
+ onQueryChange: updateQuery,
4140
+ onPrev: prevMatch,
4141
+ onNext: nextMatch,
4142
+ onClose: closeSearch
4143
+ }
4144
+ ) : null,
4145
+ /* @__PURE__ */ jsx(HighlightContext.Provider, { value: open ? query : "", children })
4146
+ ] });
4147
+ }
3887
4148
  function ScriptEditor({
3888
4149
  value,
3889
4150
  onEdit,
@@ -3968,6 +4229,7 @@ function ScriptEditor({
3968
4229
  },
3969
4230
  [onFormatChange]
3970
4231
  );
4232
+ const rootRef = useRef(null);
3971
4233
  const sceneKindByKey = useMemo(() => {
3972
4234
  const m = /* @__PURE__ */ new Map();
3973
4235
  for (const s of diff?.scenes ?? []) m.set(sceneChangeKey(s.episodeIdx, s.sceneId), s.kind);
@@ -4057,54 +4319,71 @@ function ScriptEditor({
4057
4319
  onRequestAssetDelete
4058
4320
  }
4059
4321
  );
4060
- return /* @__PURE__ */ jsx(EditingSignalContext.Provider, { value: editingSignal, children: /* @__PURE__ */ jsxs("div", { className: rootClass, children: [
4061
- toc === "sidebar" ? /* @__PURE__ */ jsx(
4062
- StudioToc,
4063
- {
4064
- data: value,
4065
- selection,
4066
- onSelect: setSelection,
4067
- onAddEpisode: disabled ? void 0 : () => onEdit((p) => addEpisode(p))
4068
- }
4069
- ) : null,
4070
- /* @__PURE__ */ jsxs("div", { className: "lj-se-main", children: [
4071
- /* @__PURE__ */ jsx(
4072
- CanvasTopBar,
4073
- {
4074
- title: value.title,
4075
- onTitleChange: disabled ? void 0 : onTitleChange,
4076
- titleDisabled: disabled,
4077
- contextLabel,
4078
- format,
4079
- onFormatChange: setFormat,
4080
- canUndo,
4081
- canRedo,
4082
- onUndo,
4083
- onRedo,
4084
- actions: topBarActions
4085
- }
4086
- ),
4087
- /* @__PURE__ */ jsxs("div", { className: "lj-se-canvas", ref: canvasRef, children: [
4088
- center,
4089
- onAddChatReferences ? /* @__PURE__ */ jsx(QuoteLayer, { containerRef: canvasRef, onAddChatReferences }) : null,
4090
- /* @__PURE__ */ jsx(
4091
- SaveCapsule,
4322
+ return /* @__PURE__ */ jsx(EditingSignalContext.Provider, { value: editingSignal, children: /* @__PURE__ */ jsxs(
4323
+ "div",
4324
+ {
4325
+ className: rootClass,
4326
+ ref: rootRef,
4327
+ children: [
4328
+ toc === "sidebar" ? /* @__PURE__ */ jsx(
4329
+ StudioToc,
4092
4330
  {
4093
- saveStatus,
4094
- diffCount,
4095
- baselineStatus: diff?.baselineStatus,
4096
- canApplyDiff: !!(diff?.onAcceptAll || diff?.onRejectAll),
4097
- onDiffPrev: changedSelections.length > 0 ? () => navDiff(-1) : void 0,
4098
- onDiffNext: changedSelections.length > 0 ? () => navDiff(1) : void 0,
4099
- onAcceptAll: diff?.onAcceptAll,
4100
- onRejectAll: diff?.onRejectAll,
4101
- returnLabel: returnTarget?.label,
4102
- onReturn: returnTarget ? () => setSelection(returnTarget.selection) : void 0
4331
+ data: value,
4332
+ selection,
4333
+ onSelect: setSelection,
4334
+ onAddEpisode: disabled ? void 0 : () => onEdit((p) => addEpisode(p))
4103
4335
  }
4104
- )
4105
- ] })
4106
- ] })
4107
- ] }) });
4336
+ ) : null,
4337
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-main", children: [
4338
+ /* @__PURE__ */ jsx(
4339
+ CanvasTopBar,
4340
+ {
4341
+ title: value.title,
4342
+ onTitleChange: disabled ? void 0 : onTitleChange,
4343
+ titleDisabled: disabled,
4344
+ contextLabel,
4345
+ format,
4346
+ onFormatChange: setFormat,
4347
+ canUndo,
4348
+ canRedo,
4349
+ onUndo,
4350
+ onRedo,
4351
+ actions: topBarActions
4352
+ }
4353
+ ),
4354
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-canvas", ref: canvasRef, children: [
4355
+ /* @__PURE__ */ jsx(
4356
+ ScriptSearchLayer,
4357
+ {
4358
+ value,
4359
+ disabled,
4360
+ rootRef,
4361
+ selection,
4362
+ onSelectionChange: setSelection,
4363
+ children: center
4364
+ }
4365
+ ),
4366
+ onAddChatReferences ? /* @__PURE__ */ jsx(QuoteLayer, { containerRef: canvasRef, onAddChatReferences }) : null,
4367
+ /* @__PURE__ */ jsx(
4368
+ SaveCapsule,
4369
+ {
4370
+ saveStatus,
4371
+ diffCount,
4372
+ baselineStatus: diff?.baselineStatus,
4373
+ canApplyDiff: !!(diff?.onAcceptAll || diff?.onRejectAll),
4374
+ onDiffPrev: changedSelections.length > 0 ? () => navDiff(-1) : void 0,
4375
+ onDiffNext: changedSelections.length > 0 ? () => navDiff(1) : void 0,
4376
+ onAcceptAll: diff?.onAcceptAll,
4377
+ onRejectAll: diff?.onRejectAll,
4378
+ returnLabel: returnTarget?.label,
4379
+ onReturn: returnTarget ? () => setSelection(returnTarget.selection) : void 0
4380
+ }
4381
+ )
4382
+ ] })
4383
+ ] })
4384
+ ]
4385
+ }
4386
+ ) });
4108
4387
  }
4109
4388
  function buildContextLabel(value, selection) {
4110
4389
  if (selection.kind === "worldview") {
@@ -4251,6 +4530,56 @@ __export(mutations_exports, {
4251
4530
  updateSceneLocationState: () => updateSceneLocationState,
4252
4531
  updateScenePropState: () => updateScenePropState
4253
4532
  });
4533
+ var CONFIRM_TTL_MS = 2500;
4534
+ function DeleteConfirmButton({
4535
+ onConfirm,
4536
+ disabled,
4537
+ targetScope,
4538
+ title = "\u5220\u9664\uFF1F"
4539
+ }) {
4540
+ const [confirming, setConfirming] = useState(false);
4541
+ const timerRef = useRef(null);
4542
+ useEffect(
4543
+ () => () => {
4544
+ if (timerRef.current) clearTimeout(timerRef.current);
4545
+ },
4546
+ []
4547
+ );
4548
+ const cancel = () => {
4549
+ if (timerRef.current) {
4550
+ clearTimeout(timerRef.current);
4551
+ timerRef.current = null;
4552
+ }
4553
+ setConfirming(false);
4554
+ };
4555
+ const handleClick = () => {
4556
+ if (confirming) {
4557
+ cancel();
4558
+ void onConfirm();
4559
+ return;
4560
+ }
4561
+ setConfirming(true);
4562
+ if (timerRef.current) clearTimeout(timerRef.current);
4563
+ timerRef.current = setTimeout(() => {
4564
+ setConfirming(false);
4565
+ timerRef.current = null;
4566
+ }, CONFIRM_TTL_MS);
4567
+ };
4568
+ return /* @__PURE__ */ jsx(
4569
+ "button",
4570
+ {
4571
+ type: "button",
4572
+ disabled,
4573
+ "data-delete-target": targetScope,
4574
+ "data-confirming": confirming || void 0,
4575
+ onClick: handleClick,
4576
+ "aria-label": confirming ? `\u518D\u70B9\u4E00\u6B21\u786E\u8BA4\uFF1A${title}` : title,
4577
+ title: confirming ? "\u518D\u70B9\u4E00\u6B21\u786E\u8BA4\uFF082.5 \u79D2\u540E\u81EA\u52A8\u53D6\u6D88\uFF09" : title,
4578
+ className: cn("lj-se-del", confirming && "is-confirming"),
4579
+ children: confirming ? /* @__PURE__ */ jsx(Check, { className: "lj-se-icon", strokeWidth: 2.2 }) : /* @__PURE__ */ jsx(Eraser, { className: "lj-se-icon", strokeWidth: 1.5 })
4580
+ }
4581
+ );
4582
+ }
4254
4583
 
4255
4584
  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 };
4256
4585
  //# sourceMappingURL=index.js.map