@lingjingai/script-editor 0.1.18 → 0.1.20

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
 
@@ -178,7 +178,8 @@ function Host({ script, onEdit }) {
178
178
  ```
179
179
 
180
180
  `buildScriptNav(value): ScriptNav` 返回:
181
- - `worldview` / `episodes[]` / `actors[]` / `locations[]` / `props[]`,`episodes[].scenes[]` 内嵌。
181
+ - `episodes[]` / `actors[]` / `locations[]` / `props[]`,`episodes[].scenes[]` 内嵌。
182
+ (**0.1.19 起**:世界观 & 风格的显示与编辑暂时下线——不再有内联区块,也不再是导航节点;宿主无需在目录里渲染它。`WorldviewEditor` 组件保留但未接线,后续恢复时可直接复用。)
182
183
  - 每个节点都带 **`key`**(= `selectionTargetKey(node.selection)`,拿去和 `selectionTargetKey(当前selection)` 比对即判激活)和 **`selection`**(即用型 `StudioSelection`,**不带 origin**;点击时 `withOrigin(node.selection, "sidebar")` 再喂 `onSelectionChange`)。
183
184
  - 标签字段(`label`/`title`/`meta`/`name`)与内置目录**同 helper 派生**,长相一致。
184
185
 
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 资产回链)。 */
@@ -565,11 +565,6 @@ interface ScriptNavEntity {
565
565
  selection: StudioSelection;
566
566
  }
567
567
  interface ScriptNav {
568
- worldview: {
569
- key: string;
570
- label: string;
571
- selection: StudioSelection;
572
- };
573
568
  episodes: ScriptNavEpisode[];
574
569
  actors: ScriptNavEntity[];
575
570
  locations: ScriptNavEntity[];
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, Globe, BookText, ChevronRight, FileText, Users, MapPin, Package, Undo2, Redo2, MessageSquareQuote, ArrowLeft, ChevronLeft, Loader2, AlertCircle } from 'lucide-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, 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';
@@ -479,18 +479,6 @@ function StudioToc({
479
479
  const pick = (sel) => onSelect(withOrigin(sel, "sidebar"));
480
480
  const epHighlighted = selection.kind === "episode" || selection.kind === "scene" || selection.kind === "clip";
481
481
  return /* @__PURE__ */ jsxs("nav", { className: "lj-se-toc", children: [
482
- /* @__PURE__ */ jsxs(
483
- "button",
484
- {
485
- type: "button",
486
- className: cn("lj-se-tocitem lj-se-toc-l0", selection.kind === "worldview" && "is-active"),
487
- onClick: () => pick({ kind: "worldview" }),
488
- children: [
489
- /* @__PURE__ */ jsx(Globe, { className: "lj-se-tocitem-icon lj-se-ic-4", strokeWidth: 1.5 }),
490
- /* @__PURE__ */ jsx("span", { className: "lj-se-tocitem-label", children: "\u4E16\u754C\u89C2 & \u98CE\u683C" })
491
- ]
492
- }
493
- ),
494
482
  /* @__PURE__ */ jsx(
495
483
  GroupHeader,
496
484
  {
@@ -608,6 +596,30 @@ function EntityGroup({
608
596
  )) : null
609
597
  ] });
610
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
+ }
611
623
  var EditingSignalContext = createContext(null);
612
624
  var LIVE_COMMIT_MS = 400;
613
625
  function caretOffsetFromPoint(x, y) {
@@ -639,6 +651,7 @@ function EditableText({
639
651
  const inputRef = useRef(null);
640
652
  const committedRef = useRef(value);
641
653
  const editingSignal = useContext(EditingSignalContext);
654
+ const highlight = useContext(HighlightContext);
642
655
  const liveTimerRef = useRef(null);
643
656
  const composingRef = useRef(false);
644
657
  const draftRef = useRef(value);
@@ -722,13 +735,14 @@ function EditableText({
722
735
  onEditEnd?.();
723
736
  }
724
737
  if (!editing) {
738
+ const content = value ? renderHighlightedText(value, highlight) : /* @__PURE__ */ jsx("span", { className: "lj-se-ph", children: placeholder ?? "" });
725
739
  return /* @__PURE__ */ jsx(
726
740
  "span",
727
741
  {
728
742
  ...spanProps,
729
743
  className: "lj-se-editable" + (className ? " " + className : "") + (disabled ? " lj-se-editable--ro" : ""),
730
744
  onClick: beginEdit,
731
- children: value || /* @__PURE__ */ jsx("span", { className: "lj-se-ph", children: placeholder ?? "" })
745
+ children: content
732
746
  }
733
747
  );
734
748
  }
@@ -1758,6 +1772,9 @@ function SelectField({
1758
1772
  style,
1759
1773
  emptyOptionLabel
1760
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());
1761
1778
  return /* @__PURE__ */ jsxs(
1762
1779
  "select",
1763
1780
  {
@@ -1766,7 +1783,7 @@ function SelectField({
1766
1783
  title,
1767
1784
  onChange: (e) => onChange(e.target.value),
1768
1785
  style,
1769
- 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),
1770
1787
  children: [
1771
1788
  !value || !options.some((o) => o.value === value) ? /* @__PURE__ */ jsx("option", { value, children: placeholder ?? emptyOptionLabel ?? "\u2014" }) : null,
1772
1789
  options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label }, o.value))
@@ -1790,7 +1807,7 @@ function AssetStateChip({
1790
1807
  const opts = stateOptions(states2);
1791
1808
  const trimmed = (currentStateId ?? "").trim();
1792
1809
  return /* @__PURE__ */ jsxs("span", { className: cn("lj-se-chip", orphan && "is-orphan"), title: orphan ? `${name} \u4E0D\u5B58\u5728` : void 0, children: [
1793
- 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 }) }),
1794
1811
  /* @__PURE__ */ jsx("span", { className: "lj-se-chip-sep", children: "\xB7" }),
1795
1812
  /* @__PURE__ */ jsx(
1796
1813
  SelectField,
@@ -2279,7 +2296,7 @@ function SceneItemRowImpl({
2279
2296
  className: "lj-se-cue lj-se-cue--structured",
2280
2297
  style: resolvedActor ? { color: getActorCueVar(resolvedActorId, tone) } : void 0,
2281
2298
  children: [
2282
- getDialogueSpeakerLabel(item, actorMap, speakerMap) || "\u672A\u77E5\u53D1\u58F0\u6E90",
2299
+ /* @__PURE__ */ jsx(HighlightedText, { text: getDialogueSpeakerLabel(item, actorMap, speakerMap) || "\u672A\u77E5\u53D1\u58F0\u6E90" }),
2283
2300
  getDialogueDeliveryLabel(item.delivery) ? /* @__PURE__ */ jsxs("span", { className: "lj-se-cue-delivery", children: [
2284
2301
  "\uFF08",
2285
2302
  getDialogueDeliveryLabel(item.delivery),
@@ -2371,7 +2388,7 @@ function SceneItemRowImpl({
2371
2388
  const tone = isInner ? "inner" : "dialogue";
2372
2389
  const tag = isInner ? emotion ? `OS\xB7${emotion}` : "OS" : emotion;
2373
2390
  body = /* @__PURE__ */ jsxs("p", { className: cn("lj-se-asian-cueline", isInner && "is-inner"), children: [
2374
- 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(
2375
2392
  SelectField,
2376
2393
  {
2377
2394
  value: resolvedActorId,
@@ -2602,82 +2619,6 @@ function SceneEditorImpl({
2602
2619
  ] });
2603
2620
  }
2604
2621
  var SceneEditor = memo(SceneEditorImpl);
2605
- function Stat({ label, value }) {
2606
- return /* @__PURE__ */ jsxs("div", { className: "lj-se-stat", children: [
2607
- /* @__PURE__ */ jsx("span", { className: "lj-se-stat-label", children: label }),
2608
- /* @__PURE__ */ jsx("span", { className: "lj-se-stat-value", children: value })
2609
- ] });
2610
- }
2611
- function WorldviewEditor({
2612
- data,
2613
- disabled = false,
2614
- onEdit
2615
- }) {
2616
- const episodes = data.episodes ?? [];
2617
- const totalScenes = episodes.reduce((sum, ep) => sum + (ep.scenes?.length ?? 0), 0);
2618
- const totalLines = episodes.reduce(
2619
- (sum, ep) => sum + (ep.scenes ?? []).reduce((s, sc) => s + (sc.actions?.length ?? 0), 0),
2620
- 0
2621
- );
2622
- return /* @__PURE__ */ jsxs("div", { className: "lj-se-worldview lj-se-scroll", children: [
2623
- /* @__PURE__ */ jsxs("header", { className: "lj-se-wv-header", children: [
2624
- /* @__PURE__ */ jsx("div", { className: "lj-se-wv-kicker", children: "\u5267\u672C \xB7 \u9876\u5C42" }),
2625
- /* @__PURE__ */ jsx("h2", { className: "lj-se-wv-title", children: "\u4E16\u754C\u89C2 & \u98CE\u683C" }),
2626
- /* @__PURE__ */ jsx("p", { className: "lj-se-wv-sub", children: "\u6539\u52A8\u4F1A\u5F71\u54CD\u4E0B\u6E38\u8D44\u4EA7\u751F\u6210\u7684 prompt\uFF1B\u6539\u5B8C\u540E\u53EF\u8BA9 AI \u540C\u6B65\u66F4\u65B0\u4EBA\u8BBE / \u573A\u666F\u3002" })
2627
- ] }),
2628
- /* @__PURE__ */ jsxs("section", { className: "lj-se-stats", children: [
2629
- /* @__PURE__ */ jsx(Stat, { label: "\u96C6\u6570", value: episodes.length }),
2630
- /* @__PURE__ */ jsx(Stat, { label: "\u573A\u666F", value: totalScenes }),
2631
- /* @__PURE__ */ jsx(Stat, { label: "\u53F0\u8BCD", value: totalLines }),
2632
- /* @__PURE__ */ jsx(Stat, { label: "\u89D2\u8272", value: data.actors?.length ?? 0 }),
2633
- /* @__PURE__ */ jsx(Stat, { label: "\u9053\u5177", value: data.props?.length ?? 0 })
2634
- ] }),
2635
- /* @__PURE__ */ jsxs("div", { className: "lj-se-field", children: [
2636
- /* @__PURE__ */ jsx("div", { className: "lj-se-field-label", children: "\u6807\u9898" }),
2637
- /* @__PURE__ */ jsx("div", { className: "lj-se-field-box", children: /* @__PURE__ */ jsx(
2638
- EditableText,
2639
- {
2640
- value: data.title ?? "",
2641
- disabled,
2642
- placeholder: "\u672A\u547D\u540D\u5267\u672C",
2643
- onSave: (v) => onEdit((p) => ({ ...p, title: v }))
2644
- }
2645
- ) })
2646
- ] }),
2647
- /* @__PURE__ */ jsxs("div", { className: "lj-se-field", children: [
2648
- /* @__PURE__ */ jsxs("div", { className: "lj-se-field-label", children: [
2649
- "\u4E16\u754C\u89C2 ",
2650
- /* @__PURE__ */ jsx("span", { className: "lj-se-field-hint", children: "\u591A\u884C\uFF1BAI \u5199\u5267\u672C\u65F6\u4F1A\u5F15\u7528" })
2651
- ] }),
2652
- /* @__PURE__ */ jsx("div", { className: "lj-se-field-box", children: /* @__PURE__ */ jsx(
2653
- EditableText,
2654
- {
2655
- multiline: true,
2656
- value: data.worldview ?? "",
2657
- disabled,
2658
- placeholder: "\u63CF\u8FF0\u6545\u4E8B\u53D1\u751F\u7684\u4E16\u754C\u3001\u80CC\u666F\u8BBE\u5B9A\u2026",
2659
- onSave: (v) => onEdit((p) => ({ ...p, worldview: v }))
2660
- }
2661
- ) })
2662
- ] }),
2663
- /* @__PURE__ */ jsxs("div", { className: "lj-se-field", children: [
2664
- /* @__PURE__ */ jsxs("div", { className: "lj-se-field-label", children: [
2665
- "\u98CE\u683C ",
2666
- /* @__PURE__ */ jsx("span", { className: "lj-se-field-hint", children: "\u8BED\u8A00\u8C03\u6027 / \u955C\u5934\u8BED\u8A00\u503E\u5411" })
2667
- ] }),
2668
- /* @__PURE__ */ jsx("div", { className: "lj-se-field-box", children: /* @__PURE__ */ jsx(
2669
- EditableText,
2670
- {
2671
- multiline: true,
2672
- value: data.style ?? "",
2673
- disabled,
2674
- placeholder: "\u4F8B\u5982\uFF1A\u5199\u5B9E\u3001\u51B7\u5CFB\u3001\u957F\u955C\u5934\u2026",
2675
- onSave: (v) => onEdit((p) => ({ ...p, style: v }))
2676
- }
2677
- ) })
2678
- ] })
2679
- ] });
2680
- }
2681
2622
 
2682
2623
  // src/stream/flat-items.ts
2683
2624
  var SCENE_KEY = (e, s) => `ep:${e}:sc:${s}`;
@@ -2704,7 +2645,6 @@ function buildFlatStream(data) {
2704
2645
  if (item.kind === "asset-card") assetIndexLookup.set(ASSET_KEY(item.group, item.assetId), items.length);
2705
2646
  items.push(item);
2706
2647
  };
2707
- push({ key: "worldview", kind: "worldview" });
2708
2648
  if (!data) {
2709
2649
  return { items, indexByKey, sceneIndexLookup, sceneIdLookup, assetIndexLookup, paperStartKey: null, paperEndKey: null };
2710
2650
  }
@@ -2751,8 +2691,6 @@ function buildFlatStream(data) {
2751
2691
  }
2752
2692
  function selectionToFlatIndex(stream, sel) {
2753
2693
  switch (sel.kind) {
2754
- case "worldview":
2755
- return stream.indexByKey.get("worldview") ?? null;
2756
2694
  case "episode":
2757
2695
  return stream.indexByKey.get(`ep:${sel.episodeIdx}:header`) ?? null;
2758
2696
  case "scene":
@@ -2779,8 +2717,6 @@ function flatIndexToSelection(stream, idx) {
2779
2717
  const item = stream.items[idx];
2780
2718
  if (!item) return null;
2781
2719
  switch (item.kind) {
2782
- case "worldview":
2783
- return { kind: "worldview" };
2784
2720
  case "episode-header":
2785
2721
  return { kind: "episode", episodeIdx: item.episodeIdx };
2786
2722
  case "scene":
@@ -3168,7 +3104,7 @@ function AssetCardImpl({
3168
3104
  const description = rawDescription.trim();
3169
3105
  const states2 = entity.states ?? [];
3170
3106
  const fromRef = buildSelectionFrom(kind, assetId);
3171
- return /* @__PURE__ */ jsx("div", { className: "lj-se-assetcard-row", children: /* @__PURE__ */ jsxs("article", { className: "lj-se-assetcard", "data-no-quote-selection": true, children: [
3107
+ 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: [
3172
3108
  /* @__PURE__ */ jsxs("div", { className: "lj-se-assetcard-head", children: [
3173
3109
  /* @__PURE__ */ jsx("span", { className: "lj-se-assetcard-name", children: /* @__PURE__ */ jsx(
3174
3110
  EditableText,
@@ -3274,7 +3210,7 @@ function AssetCardImpl({
3274
3210
  }
3275
3211
  )
3276
3212
  ) : null
3277
- ] }) });
3213
+ ] }) }) });
3278
3214
  }
3279
3215
  function areEqual(prev, next) {
3280
3216
  if (prev.group !== next.group || prev.assetId !== next.assetId || prev.disabled !== next.disabled) return false;
@@ -3402,8 +3338,6 @@ function ScriptStream({
3402
3338
  itemContent: (_index, item) => {
3403
3339
  if (!item) return null;
3404
3340
  switch (item.kind) {
3405
- case "worldview":
3406
- return /* @__PURE__ */ jsx("div", { className: "lj-se-stream-worldview", children: /* @__PURE__ */ jsx(WorldviewEditor, { data: value, disabled, onEdit }) });
3407
3341
  case "episode-header": {
3408
3342
  const episode = value.episodes?.[item.episodeIdx];
3409
3343
  if (!episode) return null;
@@ -3979,6 +3913,250 @@ function QuoteLayer({
3979
3913
  }
3980
3914
  );
3981
3915
  }
3916
+ function ScriptSearchBox({
3917
+ query,
3918
+ current,
3919
+ total,
3920
+ inputRef,
3921
+ onQueryChange,
3922
+ onPrev,
3923
+ onNext,
3924
+ onClose
3925
+ }) {
3926
+ return /* @__PURE__ */ jsxs("div", { className: "lj-se-search", children: [
3927
+ /* @__PURE__ */ jsx(Search, { className: "lj-se-ic-3", strokeWidth: 2 }),
3928
+ /* @__PURE__ */ jsx(
3929
+ "input",
3930
+ {
3931
+ ref: inputRef,
3932
+ className: "lj-se-search-input",
3933
+ value: query,
3934
+ placeholder: "\u641C\u7D22",
3935
+ "aria-label": "\u641C\u7D22\u5267\u672C",
3936
+ onChange: (e) => onQueryChange(e.target.value),
3937
+ onKeyDown: (e) => {
3938
+ if (e.key === "Enter") {
3939
+ e.preventDefault();
3940
+ if (e.shiftKey) onPrev();
3941
+ else onNext();
3942
+ } else if (e.key === "Escape") {
3943
+ e.preventDefault();
3944
+ onClose();
3945
+ }
3946
+ }
3947
+ }
3948
+ ),
3949
+ /* @__PURE__ */ jsxs("span", { className: "lj-se-search-count", children: [
3950
+ total > 0 ? current + 1 : 0,
3951
+ "/",
3952
+ total
3953
+ ] }),
3954
+ /* @__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 }) }),
3955
+ /* @__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 }) }),
3956
+ /* @__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 }) })
3957
+ ] });
3958
+ }
3959
+
3960
+ // src/search/search-index.ts
3961
+ var SPACE_LABEL = {
3962
+ interior: "INT. \u5185",
3963
+ exterior: "EXT. \u5916"
3964
+ };
3965
+ var TIME_LABEL = {
3966
+ day: "DAY \u65E5",
3967
+ night: "NIGHT \u591C",
3968
+ dawn: "DAWN \u6668",
3969
+ dusk: "DUSK \u66AE"
3970
+ };
3971
+ function add(parts, value) {
3972
+ const text = value?.trim();
3973
+ if (text) parts.push(text);
3974
+ }
3975
+ function findStateName(states2, stateId) {
3976
+ const id = stateId?.trim();
3977
+ if (!id) return "";
3978
+ return getStateDisplayName(states2?.find((state) => state.state_id?.trim() === id));
3979
+ }
3980
+ function normalize(value) {
3981
+ return value.trim().toLocaleLowerCase();
3982
+ }
3983
+ function sceneSearchText(scene, sceneIdx, maps) {
3984
+ const parts = [];
3985
+ const context = getSceneContext(scene);
3986
+ const primaryLocationId = getPrimarySceneLocationId(scene);
3987
+ const location = primaryLocationId ? maps.locationMap.get(primaryLocationId) : void 0;
3988
+ add(parts, String(getSceneNumber(scene, sceneIdx)));
3989
+ add(parts, SPACE_LABEL[scene.environment?.space ?? ""] ?? scene.environment?.space);
3990
+ add(parts, location ? getLocationDisplayName(location) : primaryLocationId);
3991
+ add(parts, TIME_LABEL[scene.environment?.time ?? ""] ?? scene.environment?.time);
3992
+ for (const ref of context.actors ?? []) {
3993
+ const id = ref.actor_id?.trim();
3994
+ if (!id) continue;
3995
+ const actor = maps.actorMap.get(id);
3996
+ add(parts, actor ? getActorDisplayName(actor) : id);
3997
+ add(parts, findStateName(actor?.states, ref.state_id));
3998
+ }
3999
+ for (const ref of context.props ?? []) {
4000
+ const id = ref.prop_id?.trim();
4001
+ if (!id) continue;
4002
+ const prop = maps.propMap.get(id);
4003
+ add(parts, prop ? getPropDisplayName(prop) : id);
4004
+ add(parts, findStateName(prop?.states, ref.state_id));
4005
+ }
4006
+ for (const item of scene.actions ?? []) {
4007
+ addItemSearchText(parts, item, maps.actorMap, maps.speakerMap);
4008
+ }
4009
+ return parts.join("\n");
4010
+ }
4011
+ function addItemSearchText(parts, item, actorMap, speakerMap) {
4012
+ add(parts, item.content);
4013
+ add(parts, translateEmotionLabel(item.emotion));
4014
+ if (isActorBearingContentType(item.type)) {
4015
+ add(parts, getDialogueSpeakerLabel(item, actorMap, speakerMap));
4016
+ }
4017
+ for (const line of item.lines ?? []) {
4018
+ add(parts, getDialogueSpeakerLabel({ speaker_id: line.speaker_id }, actorMap, speakerMap));
4019
+ add(parts, line.content);
4020
+ }
4021
+ }
4022
+ function buildScriptSearchEntries(value) {
4023
+ const entries = [];
4024
+ const maps = {
4025
+ actorMap: getActorMap(value.actors ?? []),
4026
+ speakerMap: getSpeakerMap(value.speakers ?? []),
4027
+ locationMap: getLocationMap(value.locations ?? []),
4028
+ propMap: getPropMap(value.props ?? [])
4029
+ };
4030
+ const title = value.title?.trim();
4031
+ if (title && (value.episodes?.length ?? 0) > 0) {
4032
+ entries.push({
4033
+ key: "title",
4034
+ text: title,
4035
+ selection: { kind: "episode", episodeIdx: 0 }
4036
+ });
4037
+ }
4038
+ (value.episodes ?? []).forEach((episode, episodeIdx) => {
4039
+ entries.push({
4040
+ key: `episode:${episodeIdx}`,
4041
+ text: getEpisodeTitle(episode, episodeIdx),
4042
+ selection: { kind: "episode", episodeIdx }
4043
+ });
4044
+ (episode.scenes ?? []).forEach((scene, sceneIdx) => {
4045
+ entries.push({
4046
+ key: `scene:${episodeIdx}:${sceneIdx}`,
4047
+ text: sceneSearchText(scene, sceneIdx, maps),
4048
+ selection: { kind: "scene", episodeIdx, sceneIdx }
4049
+ });
4050
+ });
4051
+ });
4052
+ return entries;
4053
+ }
4054
+ function findScriptSearchMatches(entries, query) {
4055
+ const needle = normalize(query);
4056
+ if (!needle) return [];
4057
+ const matches = [];
4058
+ for (const entry of entries) {
4059
+ if (normalize(entry.text).includes(needle)) {
4060
+ matches.push({ entry, index: matches.length });
4061
+ }
4062
+ }
4063
+ return matches;
4064
+ }
4065
+ function ScriptSearchLayer({
4066
+ value,
4067
+ disabled,
4068
+ rootRef,
4069
+ selection,
4070
+ onSelectionChange,
4071
+ children
4072
+ }) {
4073
+ const editorActiveRef = useRef(false);
4074
+ const inputRef = useRef(null);
4075
+ const [open, setOpen] = useState(false);
4076
+ const [query, setQuery] = useState("");
4077
+ const [cursor, setCursor] = useState(0);
4078
+ const entries = useMemo(() => buildScriptSearchEntries(value), [value]);
4079
+ const matches = useMemo(() => findScriptSearchMatches(entries, query), [entries, query]);
4080
+ const total = matches.length;
4081
+ const current = total > 0 ? Math.min(cursor, total - 1) : 0;
4082
+ const selectMatch = useCallback(
4083
+ (index) => {
4084
+ const match = matches[index];
4085
+ if (!match) return;
4086
+ const next = withOrigin(match.entry.selection, "search");
4087
+ setCursor(index);
4088
+ if (!isSameSelectionTarget(next, selection)) onSelectionChange(next);
4089
+ },
4090
+ [matches, onSelectionChange, selection]
4091
+ );
4092
+ const openSearch = useCallback(() => {
4093
+ setOpen(true);
4094
+ requestAnimationFrame(() => {
4095
+ inputRef.current?.focus();
4096
+ inputRef.current?.select();
4097
+ });
4098
+ }, []);
4099
+ const closeSearch = useCallback(() => setOpen(false), []);
4100
+ const updateQuery = useCallback((next) => {
4101
+ setQuery(next);
4102
+ setCursor(0);
4103
+ }, []);
4104
+ const nextMatch = useCallback(() => {
4105
+ if (total > 0) selectMatch((current + 1) % total);
4106
+ }, [current, selectMatch, total]);
4107
+ const prevMatch = useCallback(() => {
4108
+ if (total > 0) selectMatch((current - 1 + total) % total);
4109
+ }, [current, selectMatch, total]);
4110
+ useEffect(() => {
4111
+ if (open && total > 0) selectMatch(current);
4112
+ }, [current, open, selectMatch, total]);
4113
+ useEffect(() => {
4114
+ if (!disabled && open) openSearch();
4115
+ }, [disabled, open, openSearch]);
4116
+ useEffect(() => {
4117
+ if (disabled) return;
4118
+ const rememberActive = (target) => {
4119
+ editorActiveRef.current = target instanceof Node && !!rootRef.current?.contains(target);
4120
+ };
4121
+ const onPointerDown = (e) => rememberActive(e.target);
4122
+ const onFocusIn = (e) => rememberActive(e.target);
4123
+ const onKeyDown = (e) => {
4124
+ if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== "f") return;
4125
+ const target = e.target;
4126
+ const targetInside = target instanceof Node && !!rootRef.current?.contains(target);
4127
+ if (!targetInside) {
4128
+ if (target instanceof HTMLElement && target !== document.body) return;
4129
+ if (!editorActiveRef.current) return;
4130
+ }
4131
+ e.preventDefault();
4132
+ openSearch();
4133
+ };
4134
+ document.addEventListener("pointerdown", onPointerDown, true);
4135
+ document.addEventListener("focusin", onFocusIn, true);
4136
+ document.addEventListener("keydown", onKeyDown, true);
4137
+ return () => {
4138
+ document.removeEventListener("pointerdown", onPointerDown, true);
4139
+ document.removeEventListener("focusin", onFocusIn, true);
4140
+ document.removeEventListener("keydown", onKeyDown, true);
4141
+ };
4142
+ }, [disabled, openSearch, rootRef]);
4143
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
4144
+ open ? /* @__PURE__ */ jsx(
4145
+ ScriptSearchBox,
4146
+ {
4147
+ query,
4148
+ current,
4149
+ total,
4150
+ inputRef,
4151
+ onQueryChange: updateQuery,
4152
+ onPrev: prevMatch,
4153
+ onNext: nextMatch,
4154
+ onClose: closeSearch
4155
+ }
4156
+ ) : null,
4157
+ /* @__PURE__ */ jsx(HighlightContext.Provider, { value: open ? query : "", children })
4158
+ ] });
4159
+ }
3982
4160
  function ScriptEditor({
3983
4161
  value,
3984
4162
  onEdit,
@@ -4063,6 +4241,7 @@ function ScriptEditor({
4063
4241
  },
4064
4242
  [onFormatChange]
4065
4243
  );
4244
+ const rootRef = useRef(null);
4066
4245
  const sceneKindByKey = useMemo(() => {
4067
4246
  const m = /* @__PURE__ */ new Map();
4068
4247
  for (const s of diff?.scenes ?? []) m.set(sceneChangeKey(s.episodeIdx, s.sceneId), s.kind);
@@ -4152,54 +4331,71 @@ function ScriptEditor({
4152
4331
  onRequestAssetDelete
4153
4332
  }
4154
4333
  );
4155
- return /* @__PURE__ */ jsx(EditingSignalContext.Provider, { value: editingSignal, children: /* @__PURE__ */ jsxs("div", { className: rootClass, children: [
4156
- toc === "sidebar" ? /* @__PURE__ */ jsx(
4157
- StudioToc,
4158
- {
4159
- data: value,
4160
- selection,
4161
- onSelect: setSelection,
4162
- onAddEpisode: disabled ? void 0 : () => onEdit((p) => addEpisode(p))
4163
- }
4164
- ) : null,
4165
- /* @__PURE__ */ jsxs("div", { className: "lj-se-main", children: [
4166
- /* @__PURE__ */ jsx(
4167
- CanvasTopBar,
4168
- {
4169
- title: value.title,
4170
- onTitleChange: disabled ? void 0 : onTitleChange,
4171
- titleDisabled: disabled,
4172
- contextLabel,
4173
- format,
4174
- onFormatChange: setFormat,
4175
- canUndo,
4176
- canRedo,
4177
- onUndo,
4178
- onRedo,
4179
- actions: topBarActions
4180
- }
4181
- ),
4182
- /* @__PURE__ */ jsxs("div", { className: "lj-se-canvas", ref: canvasRef, children: [
4183
- center,
4184
- onAddChatReferences ? /* @__PURE__ */ jsx(QuoteLayer, { containerRef: canvasRef, onAddChatReferences }) : null,
4185
- /* @__PURE__ */ jsx(
4186
- SaveCapsule,
4334
+ return /* @__PURE__ */ jsx(EditingSignalContext.Provider, { value: editingSignal, children: /* @__PURE__ */ jsxs(
4335
+ "div",
4336
+ {
4337
+ className: rootClass,
4338
+ ref: rootRef,
4339
+ children: [
4340
+ toc === "sidebar" ? /* @__PURE__ */ jsx(
4341
+ StudioToc,
4187
4342
  {
4188
- saveStatus,
4189
- diffCount,
4190
- baselineStatus: diff?.baselineStatus,
4191
- canApplyDiff: !!(diff?.onAcceptAll || diff?.onRejectAll),
4192
- onDiffPrev: changedSelections.length > 0 ? () => navDiff(-1) : void 0,
4193
- onDiffNext: changedSelections.length > 0 ? () => navDiff(1) : void 0,
4194
- onAcceptAll: diff?.onAcceptAll,
4195
- onRejectAll: diff?.onRejectAll,
4196
- returnLabel: returnTarget?.label,
4197
- onReturn: returnTarget ? () => setSelection(returnTarget.selection) : void 0
4343
+ data: value,
4344
+ selection,
4345
+ onSelect: setSelection,
4346
+ onAddEpisode: disabled ? void 0 : () => onEdit((p) => addEpisode(p))
4198
4347
  }
4199
- )
4200
- ] })
4201
- ] })
4202
- ] }) });
4348
+ ) : null,
4349
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-main", children: [
4350
+ /* @__PURE__ */ jsx(
4351
+ CanvasTopBar,
4352
+ {
4353
+ title: value.title,
4354
+ onTitleChange: disabled ? void 0 : onTitleChange,
4355
+ titleDisabled: disabled,
4356
+ contextLabel,
4357
+ format,
4358
+ onFormatChange: setFormat,
4359
+ canUndo,
4360
+ canRedo,
4361
+ onUndo,
4362
+ onRedo,
4363
+ actions: topBarActions
4364
+ }
4365
+ ),
4366
+ /* @__PURE__ */ jsxs("div", { className: "lj-se-canvas", ref: canvasRef, children: [
4367
+ /* @__PURE__ */ jsx(
4368
+ ScriptSearchLayer,
4369
+ {
4370
+ value,
4371
+ disabled,
4372
+ rootRef,
4373
+ selection,
4374
+ onSelectionChange: setSelection,
4375
+ children: center
4376
+ }
4377
+ ),
4378
+ onAddChatReferences ? /* @__PURE__ */ jsx(QuoteLayer, { containerRef: canvasRef, onAddChatReferences }) : null,
4379
+ /* @__PURE__ */ jsx(
4380
+ SaveCapsule,
4381
+ {
4382
+ saveStatus,
4383
+ diffCount,
4384
+ baselineStatus: diff?.baselineStatus,
4385
+ canApplyDiff: !!(diff?.onAcceptAll || diff?.onRejectAll),
4386
+ onDiffPrev: changedSelections.length > 0 ? () => navDiff(-1) : void 0,
4387
+ onDiffNext: changedSelections.length > 0 ? () => navDiff(1) : void 0,
4388
+ onAcceptAll: diff?.onAcceptAll,
4389
+ onRejectAll: diff?.onRejectAll,
4390
+ returnLabel: returnTarget?.label,
4391
+ onReturn: returnTarget ? () => setSelection(returnTarget.selection) : void 0
4392
+ }
4393
+ )
4394
+ ] })
4395
+ ] })
4396
+ ]
4397
+ }
4398
+ ) });
4203
4399
  }
4204
4400
  function buildContextLabel(value, selection) {
4205
4401
  if (selection.kind === "worldview") {
@@ -4299,7 +4495,6 @@ function buildScriptNav(value) {
4299
4495
  return { ...nav({ kind: "prop", propId: id }), id, name: getPropDisplayName(p) };
4300
4496
  }).filter((e) => e.id);
4301
4497
  return {
4302
- worldview: { ...nav({ kind: "worldview" }), label: "\u4E16\u754C\u89C2 & \u98CE\u683C" },
4303
4498
  episodes,
4304
4499
  actors,
4305
4500
  locations,