@vishu1301/script-writing 0.3.8 → 0.4.0

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
@@ -46,6 +46,7 @@ var blockStyles = {
46
46
  inputStyle: {
47
47
  textTransform: "uppercase",
48
48
  fontWeight: 700,
49
+ maxWidth: "6.0in",
49
50
  outline: "none",
50
51
  whiteSpace: "pre-wrap",
51
52
  overflowWrap: "break-word",
@@ -54,21 +55,24 @@ var blockStyles = {
54
55
  },
55
56
  ACTION: {
56
57
  label: "Action",
57
- className: "text-zinc-800 leading-relaxed",
58
+ className: "text-zinc-800",
58
59
  inputStyle: {
60
+ maxWidth: "6.0in",
59
61
  outline: "none",
60
62
  whiteSpace: "pre-wrap",
61
63
  overflowWrap: "break-word",
62
64
  wordBreak: "break-word",
63
- lineHeight: 1.7
65
+ lineHeight: "12pt"
64
66
  }
65
67
  },
66
68
  CHARACTER: {
67
69
  label: "Character",
68
- className: "uppercase font-bold text-center text-zinc-900 tracking-widest",
70
+ className: "uppercase font-bold text-zinc-900 tracking-widest",
69
71
  inputStyle: {
70
72
  textTransform: "uppercase",
71
- textAlign: "center",
73
+ textAlign: "left",
74
+ marginLeft: "2.0in",
75
+ maxWidth: "4.0in",
72
76
  fontWeight: 700,
73
77
  letterSpacing: "0.1em",
74
78
  outline: "none",
@@ -79,31 +83,32 @@ var blockStyles = {
79
83
  },
80
84
  PARENTHETICAL: {
81
85
  label: "Parenthetical",
82
- className: "text-center text-zinc-600",
86
+ className: "text-zinc-600",
83
87
  inputStyle: {
84
88
  fontStyle: "normal",
85
- maxWidth: "20rem",
86
- margin: "0 auto",
89
+ maxWidth: "3.0in",
90
+ marginLeft: "1.5in",
91
+ textTransform: "lowercase",
87
92
  outline: "none",
88
93
  whiteSpace: "pre-wrap",
89
94
  overflowWrap: "break-word",
90
95
  wordBreak: "break-word",
91
- textAlign: "center"
96
+ textAlign: "left"
92
97
  }
93
98
  },
94
99
  DIALOGUE: {
95
100
  label: "Dialogue",
96
- className: "text-zinc-900 leading-relaxed max-w-[30rem] mx-auto",
101
+ className: "text-zinc-900",
97
102
  inputStyle: {
98
- marginLeft: "auto",
99
- marginRight: "auto",
103
+ marginLeft: "1.0in",
104
+ maxWidth: "3.5in",
100
105
  outline: "none",
101
106
  whiteSpace: "pre-wrap",
102
107
  overflowWrap: "break-word",
103
108
  wordBreak: "break-word",
104
109
  fontSize: "1.05rem",
105
110
  textAlign: "left",
106
- lineHeight: 1.7
111
+ lineHeight: "12pt"
107
112
  }
108
113
  },
109
114
  TRANSITION: {
@@ -128,6 +133,8 @@ function ScreenplayEditorView({
128
133
  refs,
129
134
  focusedBlockId,
130
135
  showSuggestions,
136
+ showExtensionSuggestions,
137
+ characterExtensions,
131
138
  locations,
132
139
  characters,
133
140
  sceneNumbers,
@@ -135,6 +142,7 @@ function ScreenplayEditorView({
135
142
  handleSceneTypeChange,
136
143
  handleTimeOfDayChange,
137
144
  handleBlockTypeChange,
145
+ handleSelectCharacterExtension,
138
146
  handleKeyDown,
139
147
  handleFocus,
140
148
  handleBlur,
@@ -185,7 +193,7 @@ function ScreenplayEditorView({
185
193
  /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-12 w-full items-center pb-24", children: pages.map((pageBlocks, pageIndex) => /* @__PURE__ */ jsxs(
186
194
  "div",
187
195
  {
188
- className: "relative bg-[#fdfdfc] shadow-2xl shadow-zinc-300/60 ring-1 ring-zinc-200/50 rounded-sm md:rounded-md p-16 md:p-20 flex flex-col w-[210mm] min-h-[297mm] shrink-0",
196
+ className: "relative bg-[#fdfdfc] shadow-2xl shadow-zinc-300/60 ring-1 ring-zinc-200/50 rounded-sm md:rounded-md pl-[1.5in] py-[1in] pr-[1in] flex flex-col w-[210mm] min-h-[297mm] shrink-0",
189
197
  style: {
190
198
  fontFamily: "var(--font-courier-prime, 'Courier New', Courier, monospace)"
191
199
  },
@@ -227,7 +235,7 @@ function ScreenplayEditorView({
227
235
  "aria-haspopup": "listbox",
228
236
  "aria-expanded": focusedBlockId === block.id && showSuggestions && locations.length > 0,
229
237
  spellCheck: false,
230
- className: "min-w-[5rem] py-1 outline-none text-base font-bold uppercase tracking-widest break-all bg-transparent",
238
+ className: "min-w-[3rem] py-1 outline-none text-base font-bold uppercase tracking-widest break-all bg-transparent",
231
239
  onInput: (e) => handleBlockTextChange(
232
240
  block.id,
233
241
  e.target.innerText
@@ -349,6 +357,34 @@ function ScreenplayEditorView({
349
357
  char
350
358
  )) })
351
359
  }
360
+ ),
361
+ focusedBlockId === block.id && block.type === "CHARACTER" && showExtensionSuggestions && characterExtensions && /* @__PURE__ */ jsx(
362
+ "div",
363
+ {
364
+ role: "listbox",
365
+ id: `extension-suggestions-${block.id}`,
366
+ className: "absolute top-[calc(100%+8px)] left-1/2 -translate-x-1/2 w-72 z-50 bg-white border border-slate-200 shadow-2xl shadow-slate-200/60 rounded-xl py-2 overflow-hidden animate-in fade-in zoom-in-95 duration-200",
367
+ children: /* @__PURE__ */ jsx("div", { className: "max-h-56 overflow-y-auto custom-scrollbar", children: characterExtensions.filter((ext) => {
368
+ const openParenIndex = block.text.lastIndexOf("(");
369
+ const query = openParenIndex > -1 ? block.text.substring(openParenIndex + 1).toUpperCase() : "";
370
+ return ext.toUpperCase().includes(query);
371
+ }).map((ext) => /* @__PURE__ */ jsxs(
372
+ "div",
373
+ {
374
+ role: "option",
375
+ className: "group flex items-center px-4 py-2.5 cursor-pointer transition-colors duration-150 hover:bg-slate-50 active:bg-slate-100",
376
+ onMouseDown: (e) => {
377
+ e.preventDefault();
378
+ handleSelectCharacterExtension(ext);
379
+ },
380
+ children: [
381
+ /* @__PURE__ */ jsx("span", { className: "flex-1 text-[11px] font-bold tracking-[0.1em] text-slate-600 uppercase text-left", children: ext }),
382
+ /* @__PURE__ */ jsx(ChevronRight, { className: "w-3 h-3 text-slate-200 opacity-0 group-hover:opacity-100 transition-all -translate-x-1 group-hover:translate-x-0" })
383
+ ]
384
+ },
385
+ ext
386
+ )) })
387
+ }
352
388
  )
353
389
  ] })
354
390
  },
@@ -466,6 +502,8 @@ function createNewBlock(type) {
466
502
  if (type === "SCENE_HEADING") {
467
503
  newBlock.sceneType = "INT.";
468
504
  newBlock.timeOfDay = "DAY";
505
+ } else if (type === "PARENTHETICAL") {
506
+ newBlock.text = "()";
469
507
  }
470
508
  return newBlock;
471
509
  }
@@ -571,6 +609,7 @@ function useScreenplayEditor() {
571
609
  );
572
610
  const [newBlockId, setNewBlockId] = useState(null);
573
611
  const [showSuggestions, setShowSuggestions] = useState(false);
612
+ const [showExtensionSuggestions, setShowExtensionSuggestions] = useState(false);
574
613
  const blurTimeout = useRef(null);
575
614
  const [isPageSplitEnabled, setIsPageSplitEnabled] = useState(false);
576
615
  const [pageBreaks, setPageBreaks] = useState([]);
@@ -585,12 +624,23 @@ function useScreenplayEditor() {
585
624
  }
586
625
  setIsPageSplitEnabled((prev) => !prev);
587
626
  }, [focusedBlockId]);
627
+ const characterExtensions = useMemo(
628
+ () => ["(V.O.)", "(O.S.)", "(O.C.)", "(SUBTITLE)", "(CONT'D)"],
629
+ []
630
+ );
588
631
  const locations = useMemo(() => {
589
632
  const locs = blocks.filter((b) => b.type === "SCENE_HEADING" && b.text.trim() !== "").map((b) => b.text.trim().toUpperCase());
590
633
  return [...new Set(locs)];
591
634
  }, [blocks]);
592
635
  const characters = useMemo(() => {
593
- const chars = blocks.filter((b) => b.type === "CHARACTER" && b.text.trim() !== "").map((b) => b.text.trim().toUpperCase());
636
+ const chars = blocks.filter((b) => b.type === "CHARACTER" && b.text.trim() !== "").map((b) => {
637
+ const text = b.text.trim().toUpperCase();
638
+ const parenIndex = text.indexOf("(");
639
+ if (parenIndex > -1) {
640
+ return text.substring(0, parenIndex).trim();
641
+ }
642
+ return text;
643
+ }).filter(Boolean);
594
644
  return [...new Set(chars)];
595
645
  }, [blocks]);
596
646
  const sceneNumbers = useMemo(() => {
@@ -605,13 +655,22 @@ function useScreenplayEditor() {
605
655
  return map;
606
656
  }, [blocks]);
607
657
  useEffect(() => {
608
- var _a;
609
658
  if (newBlockId && refs.current[newBlockId]) {
610
- (_a = refs.current[newBlockId]) == null ? void 0 : _a.focus();
659
+ const block = blocks.find((b) => b.id === newBlockId);
660
+ const el = refs.current[newBlockId];
661
+ if (el && block) {
662
+ el.focus();
663
+ el.innerText = block.text;
664
+ if (block.type === "PARENTHETICAL") {
665
+ setTimeout(() => setCaretPosition(el, 1), 0);
666
+ } else {
667
+ setTimeout(() => setCaretPosition(el, block.text.length), 0);
668
+ }
669
+ }
611
670
  setFocusedBlockId(newBlockId);
612
671
  setNewBlockId(null);
613
672
  }
614
- }, [newBlockId]);
673
+ }, [newBlockId, blocks]);
615
674
  useEffect(() => {
616
675
  blocks.forEach((block) => {
617
676
  const element = refs.current[block.id];
@@ -620,6 +679,23 @@ function useScreenplayEditor() {
620
679
  }
621
680
  });
622
681
  }, [blocks, isPageSplitEnabled, pageBreaks]);
682
+ useEffect(() => {
683
+ const handleClickOutside = (e) => {
684
+ const target = e.target;
685
+ const isInsideBlock = target.closest("[data-block-id]");
686
+ const isInsideToolbar = target.closest(".sticky");
687
+ const isInsideSuggestions = target.closest('[role="listbox"]');
688
+ if (!isInsideBlock && !isInsideToolbar && !isInsideSuggestions) {
689
+ setFocusedBlockId("");
690
+ setShowSuggestions(false);
691
+ setShowExtensionSuggestions(false);
692
+ }
693
+ };
694
+ document.addEventListener("mousedown", handleClickOutside);
695
+ return () => {
696
+ document.removeEventListener("mousedown", handleClickOutside);
697
+ };
698
+ }, []);
623
699
  useEffect(() => {
624
700
  if (!isPageSplitEnabled) {
625
701
  setPageBreaks([]);
@@ -712,9 +788,45 @@ function useScreenplayEditor() {
712
788
  focusStateRef.current = null;
713
789
  }
714
790
  }, [pages]);
715
- const handleBlockTextChange = useCallback((id, text) => {
716
- setBlocks((bs) => updateBlock(bs, id, "text", text));
717
- }, []);
791
+ const handleBlockTextChange = useCallback(
792
+ (id, text) => {
793
+ const block = blocks.find((b) => b.id === id);
794
+ if (!block) return;
795
+ if (block.type === "CHARACTER") {
796
+ const trimmedText = text.trim();
797
+ const openParenIndex = trimmedText.lastIndexOf("(");
798
+ const closeParenIndex = trimmedText.lastIndexOf(")");
799
+ if (openParenIndex !== -1 && openParenIndex > closeParenIndex) {
800
+ setShowExtensionSuggestions(true);
801
+ setShowSuggestions(false);
802
+ } else {
803
+ setShowExtensionSuggestions(false);
804
+ setShowSuggestions(openParenIndex === -1);
805
+ }
806
+ } else if (showExtensionSuggestions) {
807
+ setShowExtensionSuggestions(false);
808
+ }
809
+ let processedText = text;
810
+ if (block.type === "PARENTHETICAL") {
811
+ const clean = text.replace(/[()]/g, "");
812
+ processedText = `(${clean})`;
813
+ }
814
+ setBlocks(
815
+ (bs) => updateBlock(bs, id, "text", processedText)
816
+ );
817
+ if (text !== processedText) {
818
+ const el = refs.current[id];
819
+ if (el) {
820
+ const offset = getCaretCharacterOffsetWithin(el);
821
+ const charsBeforeCaret = text.substring(0, offset).replace(/[()]/g, "").length;
822
+ const newOffset = 1 + charsBeforeCaret;
823
+ el.innerText = processedText;
824
+ setCaretPosition(el, newOffset);
825
+ }
826
+ }
827
+ },
828
+ [blocks, showExtensionSuggestions]
829
+ );
718
830
  const handleSceneTypeChange = useCallback(
719
831
  (id, sceneType) => {
720
832
  setBlocks(
@@ -745,13 +857,48 @@ function useScreenplayEditor() {
745
857
  setTimeout(() => {
746
858
  const el = refs.current[focusedBlockId];
747
859
  if (el) {
748
- el.innerText = "";
749
860
  el.focus();
861
+ const newBlock = createNewBlock(newType);
862
+ el.innerText = newBlock.text;
863
+ if (newType === "PARENTHETICAL") {
864
+ setCaretPosition(el, 1);
865
+ } else {
866
+ setCaretPosition(el, newBlock.text.length);
867
+ }
750
868
  }
751
869
  }, 0);
752
870
  },
753
871
  [focusedBlockId]
754
872
  );
873
+ const handleSelectCharacterExtension = useCallback(
874
+ (extension) => {
875
+ if (!focusedBlockId) return;
876
+ setBlocks((currentBlocks) => {
877
+ const block = currentBlocks.find((b) => b.id === focusedBlockId);
878
+ if (!block || block.type !== "CHARACTER") return currentBlocks;
879
+ const parenIndex = block.text.indexOf("(");
880
+ const baseText = (parenIndex > -1 ? block.text.substring(0, parenIndex) : block.text).trim();
881
+ const newText = `${baseText} ${extension}`;
882
+ const newBlocks = updateBlock(
883
+ currentBlocks,
884
+ focusedBlockId,
885
+ "text",
886
+ newText
887
+ );
888
+ setTimeout(() => {
889
+ const el = refs.current[focusedBlockId];
890
+ if (el) {
891
+ el.innerText = newText;
892
+ el.focus();
893
+ setCaretPosition(el, newText.length);
894
+ }
895
+ }, 0);
896
+ return newBlocks;
897
+ });
898
+ setShowExtensionSuggestions(false);
899
+ },
900
+ [focusedBlockId]
901
+ );
755
902
  const focusBlock = (id, position = "start") => {
756
903
  const el = refs.current[id];
757
904
  if (!el) return;
@@ -765,19 +912,44 @@ function useScreenplayEditor() {
765
912
  sel.addRange(range);
766
913
  };
767
914
  const cycleBlockType = (id, direction) => {
768
- setBlocks((bs) => {
769
- const block = bs.find((b) => b.id === id);
770
- if (!block) return bs;
771
- const idx = blockTypes.indexOf(block.type);
772
- let newIdx = direction === "up" ? idx - 1 : idx + 1;
773
- if (newIdx < 0) newIdx = blockTypes.length - 1;
774
- if (newIdx >= blockTypes.length) newIdx = 0;
775
- return updateBlock(bs, id, "type", blockTypes[newIdx]);
776
- });
915
+ const block = blocks.find((b) => b.id === id);
916
+ if (!block) return;
917
+ const idx = blockTypes.indexOf(block.type);
918
+ let newIdx = direction === "up" ? idx - 1 : idx + 1;
919
+ if (newIdx < 0) newIdx = blockTypes.length - 1;
920
+ if (newIdx >= blockTypes.length) newIdx = 0;
921
+ const newType = blockTypes[newIdx];
922
+ setBlocks((bs) => changeBlockType(bs, id, newType));
923
+ setTimeout(() => {
924
+ const el = refs.current[id];
925
+ if (el) {
926
+ el.focus();
927
+ const newBlock = createNewBlock(newType);
928
+ el.innerText = newBlock.text;
929
+ if (newType === "PARENTHETICAL") {
930
+ setCaretPosition(el, 1);
931
+ } else {
932
+ setCaretPosition(el, el.innerText.length);
933
+ }
934
+ }
935
+ }, 10);
777
936
  };
778
937
  const handleKeyDown = useCallback(
779
938
  (e, id, text) => {
780
939
  var _a;
940
+ const block = blocks.find((b) => b.id === id);
941
+ if ((block == null ? void 0 : block.type) === "PARENTHETICAL") {
942
+ const offset = getCaretCharacterOffsetWithin(e.currentTarget);
943
+ if (e.key === "Backspace" && (offset <= 1 || text === "()")) {
944
+ e.preventDefault();
945
+ cycleBlockType(id, "up");
946
+ return;
947
+ }
948
+ if (e.key === "Delete" && (offset >= text.length - 1 || text === "()")) {
949
+ e.preventDefault();
950
+ return;
951
+ }
952
+ }
781
953
  if ((e.key === "Backspace" || e.key === "Delete") && text.length <= 1) {
782
954
  e.preventDefault();
783
955
  const { newBlocks, nextFocusedId } = deleteBlock(
@@ -803,11 +975,9 @@ function useScreenplayEditor() {
803
975
  } else if (e.key === "ArrowUp" && e.ctrlKey) {
804
976
  e.preventDefault();
805
977
  cycleBlockType(id, "up");
806
- requestAnimationFrame(() => focusBlock(id));
807
978
  } else if (e.key === "ArrowDown" && e.ctrlKey) {
808
979
  e.preventDefault();
809
980
  cycleBlockType(id, "down");
810
- requestAnimationFrame(() => focusBlock(id));
811
981
  } else if (e.key === "ArrowUp" && !e.ctrlKey) {
812
982
  const selection = window.getSelection();
813
983
  if (!selection || !selection.isCollapsed || selection.rangeCount === 0) {
@@ -884,18 +1054,36 @@ function useScreenplayEditor() {
884
1054
  }
885
1055
  }
886
1056
  },
887
- [blocks]
1057
+ [blocks, handleBlockTextChange]
888
1058
  );
889
1059
  const handleFocus = useCallback((id) => {
890
1060
  if (blurTimeout.current) {
891
1061
  clearTimeout(blurTimeout.current);
892
1062
  }
893
1063
  setFocusedBlockId(id);
894
- setShowSuggestions(true);
895
- }, []);
1064
+ const block = blocks.find((b) => b.id === id);
1065
+ if ((block == null ? void 0 : block.type) === "CHARACTER") {
1066
+ const trimmedText = block.text.trim();
1067
+ const openParenIndex = trimmedText.lastIndexOf("(");
1068
+ const closeParenIndex = trimmedText.lastIndexOf(")");
1069
+ if (openParenIndex !== -1 && openParenIndex > closeParenIndex) {
1070
+ setShowExtensionSuggestions(true);
1071
+ setShowSuggestions(false);
1072
+ } else {
1073
+ setShowExtensionSuggestions(false);
1074
+ setShowSuggestions(openParenIndex === -1);
1075
+ }
1076
+ } else {
1077
+ setShowSuggestions(true);
1078
+ setShowExtensionSuggestions(false);
1079
+ }
1080
+ }, [blocks]);
896
1081
  const handleBlur = useCallback((id) => {
897
1082
  if (document.activeElement === refs.current[id]) return;
898
- blurTimeout.current = setTimeout(() => setShowSuggestions(false), 200);
1083
+ blurTimeout.current = setTimeout(() => {
1084
+ setShowSuggestions(false);
1085
+ setShowExtensionSuggestions(false);
1086
+ }, 200);
899
1087
  }, []);
900
1088
  return {
901
1089
  blocks,
@@ -905,6 +1093,8 @@ function useScreenplayEditor() {
905
1093
  refs,
906
1094
  focusedBlockId,
907
1095
  showSuggestions,
1096
+ showExtensionSuggestions,
1097
+ characterExtensions,
908
1098
  locations,
909
1099
  characters,
910
1100
  sceneNumbers,
@@ -912,6 +1102,7 @@ function useScreenplayEditor() {
912
1102
  handleSceneTypeChange,
913
1103
  handleTimeOfDayChange,
914
1104
  handleBlockTypeChange,
1105
+ handleSelectCharacterExtension,
915
1106
  handleKeyDown,
916
1107
  handleFocus,
917
1108
  handleBlur