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