@vishu1301/script-writing 1.0.4 → 1.0.5
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/README.md +8 -4
- package/dist/index.cjs +534 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +71 -5
- package/dist/index.d.ts +71 -5
- package/dist/index.js +533 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
# Script Writing
|
|
1
|
+
# Script Writing & Breakdown Library
|
|
2
2
|
|
|
3
|
-
An advanced, React and Next.js-based script and
|
|
3
|
+
An advanced, React and Next.js-based script writing and breakdown analysis library for the web.
|
|
4
4
|
|
|
5
|
-
This
|
|
5
|
+
This package provides developers with an intuitive suite of tools to draft, edit, and format scripts according to industry standards, alongside powerful script breakdown utilities to highlight, tag, and analyze scene elements.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
+
- **Screenplay Editor Component:** Fully-featured WYSIWYG editor for screenplays.
|
|
9
10
|
- **Industry-Standard Formatting:** Automatically formats text to screenplay guidelines including Scene Headings, Action, Character, Parenthetical, Dialogue, and Transitions.
|
|
10
11
|
- **Smart Auto-Suggestions:** Intelligently suggests previously used Characters, Locations, and Character Extensions (like V.O., O.S.) as you type.
|
|
11
12
|
- **PDF Import & Export:** Built-in tools to import and parse existing scripts from PDFs (`pdfjs-dist`) and export your drafts into perfectly formatted screenplay PDFs (`jsPDF`).
|
|
12
13
|
- **Scene Management:** Easily toggle Scene Types (INT./EXT.) and Time of Day (DAY/NIGHT) alongside Scene Numbering features.
|
|
14
|
+
- **Script Breakdown & Tagging:** Interactive scene viewer with auto-character tagging, text selection, and customizable element tagging (e.g., Cast, Props, Wardrobe).
|
|
13
15
|
- **Keyboard Shortcuts:** Fluid writing experience using keyboard shortcuts (e.g., `Ctrl + ↑/↓` to change block types, `Enter` to create new blocks).
|
|
14
|
-
- **Headless Architecture:** Core logic is exposed via
|
|
16
|
+
- **Headless Architecture:** Core logic is exposed via hooks (`useScreenplayEditor` and `useScriptBreakdownScene`), allowing complete customizability over the UI.
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
## Installation
|
|
18
20
|
|
|
21
|
+
Install the package via npm or yarn:
|
|
22
|
+
|
|
19
23
|
```bash
|
|
20
24
|
npm install @vishu1301/script-writing
|
package/dist/index.cjs
CHANGED
|
@@ -1656,7 +1656,540 @@ var handleSaveAsSbx = (blocks, sceneNumbers, onSaveAsSbx) => {
|
|
|
1656
1656
|
}
|
|
1657
1657
|
};
|
|
1658
1658
|
|
|
1659
|
+
// app/types/script-breakdown.types.tsx
|
|
1660
|
+
var CATEGORIES = [
|
|
1661
|
+
{ id: "CAST", label: "Cast", color: "#7c3aed", hex: "#8b5cf6" },
|
|
1662
|
+
{ id: "PROP", label: "Prop", color: "#ea580c", hex: "#f97316" },
|
|
1663
|
+
{ id: "COSTUME", label: "Costume", color: "#db2777", hex: "#ec4899" },
|
|
1664
|
+
{ id: "VEHICLE", label: "Vehicle", color: "#2563eb", hex: "#3b82f6" },
|
|
1665
|
+
{ id: "SET_PROP", label: "Set Prop", color: "#16a34a", hex: "#22c55e" },
|
|
1666
|
+
{ id: "EXTRA", label: "Extra", color: "#0d9488", hex: "#14b8a6" },
|
|
1667
|
+
{ id: "LOCATION", label: "Location", color: "#ca8a04", hex: "#eab308" }
|
|
1668
|
+
];
|
|
1669
|
+
function ScriptBreakdownSceneView({
|
|
1670
|
+
blocks,
|
|
1671
|
+
characters,
|
|
1672
|
+
isLoading,
|
|
1673
|
+
sceneNumber,
|
|
1674
|
+
tags,
|
|
1675
|
+
selectionMenu,
|
|
1676
|
+
handleMouseUp,
|
|
1677
|
+
addTag,
|
|
1678
|
+
removeTag,
|
|
1679
|
+
clearSelection,
|
|
1680
|
+
menuPlacement,
|
|
1681
|
+
menuRef
|
|
1682
|
+
}) {
|
|
1683
|
+
const COURIER_STACK = "'Courier Prime', 'Courier', monospace";
|
|
1684
|
+
react.useEffect(() => {
|
|
1685
|
+
const fontId = "google-font-courier-prime";
|
|
1686
|
+
const styleId = "screenplay-editor-force-v4";
|
|
1687
|
+
if (!document.getElementById(fontId)) {
|
|
1688
|
+
const link = document.createElement("link");
|
|
1689
|
+
link.id = fontId;
|
|
1690
|
+
link.rel = "stylesheet";
|
|
1691
|
+
link.href = "https://fonts.googleapis.com/css2?family=Courier+Prime:ital,wght@0,400;0,700;1,400;1,700&display=swap";
|
|
1692
|
+
document.head.appendChild(link);
|
|
1693
|
+
}
|
|
1694
|
+
if (!document.getElementById(styleId)) {
|
|
1695
|
+
const style = document.createElement("style");
|
|
1696
|
+
style.id = styleId;
|
|
1697
|
+
style.textContent = `
|
|
1698
|
+
/* We target by the data-attribute to ensure the highest specificity possible */
|
|
1699
|
+
[data-screenplay-editor] *,
|
|
1700
|
+
[data-screenplay-editor] div,
|
|
1701
|
+
[data-screenplay-editor] span,
|
|
1702
|
+
[data-screenplay-editor] [contenteditable="true"] {
|
|
1703
|
+
font-family: ${COURIER_STACK} !important;
|
|
1704
|
+
-webkit-font-smoothing: antialiased;
|
|
1705
|
+
}
|
|
1706
|
+
`;
|
|
1707
|
+
document.head.appendChild(style);
|
|
1708
|
+
}
|
|
1709
|
+
}, [COURIER_STACK]);
|
|
1710
|
+
if (isLoading) {
|
|
1711
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-32 gap-4", children: [
|
|
1712
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "w-8 h-8 animate-spin text-zinc-400" }),
|
|
1713
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium text-zinc-500 animate-pulse", children: "Loading scene details..." })
|
|
1714
|
+
] });
|
|
1715
|
+
}
|
|
1716
|
+
const hasLocationTag = tags.some((t) => t.categoryId === "LOCATION");
|
|
1717
|
+
const renderBlockText = (block) => {
|
|
1718
|
+
const blockTags = tags.filter((t) => t.blockId === block.id).sort((a, b) => a.startIndex - b.startIndex);
|
|
1719
|
+
if (blockTags.length === 0) return block.text;
|
|
1720
|
+
const nodes = [];
|
|
1721
|
+
let currentIndex = 0;
|
|
1722
|
+
blockTags.forEach((tag) => {
|
|
1723
|
+
if (tag.startIndex > currentIndex) {
|
|
1724
|
+
nodes.push(
|
|
1725
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: block.text.slice(currentIndex, tag.startIndex) }, `text-${currentIndex}`)
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
const category = CATEGORIES.find((c) => c.id === tag.categoryId);
|
|
1729
|
+
nodes.push(
|
|
1730
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1731
|
+
"span",
|
|
1732
|
+
{
|
|
1733
|
+
title: `${category == null ? void 0 : category.label} (Click to edit)`,
|
|
1734
|
+
onClick: (e) => {
|
|
1735
|
+
e.stopPropagation();
|
|
1736
|
+
const selection = window.getSelection();
|
|
1737
|
+
if (!selection) return;
|
|
1738
|
+
const range = document.createRange();
|
|
1739
|
+
const textNode = e.currentTarget.firstChild;
|
|
1740
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
1741
|
+
range.selectNodeContents(textNode);
|
|
1742
|
+
} else {
|
|
1743
|
+
range.selectNodeContents(e.currentTarget);
|
|
1744
|
+
}
|
|
1745
|
+
selection.removeAllRanges();
|
|
1746
|
+
selection.addRange(range);
|
|
1747
|
+
setTimeout(() => handleMouseUp(), 0);
|
|
1748
|
+
},
|
|
1749
|
+
className: "cursor-pointer font-bold transition-opacity hover:opacity-70",
|
|
1750
|
+
style: { color: category == null ? void 0 : category.color },
|
|
1751
|
+
children: block.text.slice(tag.startIndex, tag.endIndex)
|
|
1752
|
+
},
|
|
1753
|
+
tag.id
|
|
1754
|
+
)
|
|
1755
|
+
);
|
|
1756
|
+
currentIndex = tag.endIndex;
|
|
1757
|
+
});
|
|
1758
|
+
if (currentIndex < block.text.length) {
|
|
1759
|
+
nodes.push(
|
|
1760
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: block.text.slice(currentIndex) }, `text-${currentIndex}`)
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
return nodes;
|
|
1764
|
+
};
|
|
1765
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 md:p-12 mx-auto w-full min-h-screen flex flex-col gap-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col lg:flex-row gap-8 items-start", children: [
|
|
1766
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1767
|
+
"div",
|
|
1768
|
+
{
|
|
1769
|
+
className: "relative bg-[#fdfdfc] shadow-2xl shadow-zinc-300/60 border border-zinc-100 rounded-sm md:rounded-md pl-[1.5in] py-[1in] pr-[1in] flex flex-col w-[210mm] min-h-auto shrink-0",
|
|
1770
|
+
style: {
|
|
1771
|
+
fontFamily: COURIER_STACK,
|
|
1772
|
+
paddingLeft: "1.5in",
|
|
1773
|
+
paddingRight: "1in",
|
|
1774
|
+
paddingTop: "1in",
|
|
1775
|
+
paddingBottom: "1in",
|
|
1776
|
+
lineHeight: "1.2"
|
|
1777
|
+
},
|
|
1778
|
+
"data-screenplay-editor": "true",
|
|
1779
|
+
onMouseUp: handleMouseUp,
|
|
1780
|
+
children: blocks.map((block) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1781
|
+
"div",
|
|
1782
|
+
{
|
|
1783
|
+
"data-block-id": block.id,
|
|
1784
|
+
className: `relative break-words w-full px-4 py-2 ${blockStyles[block.type].className}`,
|
|
1785
|
+
style: __spreadProps(__spreadValues({}, blockStyles[block.type].inputStyle), {
|
|
1786
|
+
minHeight: "2.5rem"
|
|
1787
|
+
}),
|
|
1788
|
+
children: [
|
|
1789
|
+
renderBlockText(block),
|
|
1790
|
+
(selectionMenu == null ? void 0 : selectionMenu.blockId) === block.id && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1791
|
+
"div",
|
|
1792
|
+
{
|
|
1793
|
+
ref: menuRef,
|
|
1794
|
+
"data-screenplay-editor": "false",
|
|
1795
|
+
className: `tag-menu absolute z-50 bg-white/70 backdrop-blur-2xl shadow-[0_10px_40px_rgb(0,0,0,0.06)] border border-white rounded-[1.5rem] p-2 flex flex-col w-56 animate-in fade-in zoom-in-95 duration-300 ease-out ${menuPlacement === "top" ? "origin-bottom" : "origin-top"}`,
|
|
1796
|
+
style: {
|
|
1797
|
+
top: selectionMenu.top,
|
|
1798
|
+
left: selectionMenu.left,
|
|
1799
|
+
transform: menuPlacement === "top" ? "translate(-50%, calc(-100% - 12px))" : "translate(-50%, 32px)"
|
|
1800
|
+
},
|
|
1801
|
+
children: [
|
|
1802
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative z-10 px-3 py-2.5 border-b border-white/60 mb-1.5", children: [
|
|
1803
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[9px] font-extrabold tracking-[0.2em] text-slate-400 uppercase mb-1", children: "Tag Element" }),
|
|
1804
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1805
|
+
"p",
|
|
1806
|
+
{
|
|
1807
|
+
className: "text-xs font-bold text-slate-700 truncate drop-shadow-sm",
|
|
1808
|
+
title: selectionMenu.text,
|
|
1809
|
+
children: [
|
|
1810
|
+
'"',
|
|
1811
|
+
selectionMenu.text,
|
|
1812
|
+
'"'
|
|
1813
|
+
]
|
|
1814
|
+
}
|
|
1815
|
+
)
|
|
1816
|
+
] }),
|
|
1817
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative z-10 flex flex-col gap-1", children: [
|
|
1818
|
+
CATEGORIES.filter(
|
|
1819
|
+
(cat) => !(cat.id === "LOCATION" && hasLocationTag)
|
|
1820
|
+
).map((cat) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1821
|
+
"button",
|
|
1822
|
+
{
|
|
1823
|
+
onClick: () => addTag(cat.id),
|
|
1824
|
+
className: "group w-full text-[12px] font-bold px-3 py-2 rounded-xl transition-all duration-300 text-left flex items-center justify-between hover:bg-white/80 hover:shadow-[0_2px_10px_rgb(0,0,0,0.02)] active:scale-[0.98]",
|
|
1825
|
+
style: { color: cat.color },
|
|
1826
|
+
children: [
|
|
1827
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1828
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1829
|
+
"div",
|
|
1830
|
+
{
|
|
1831
|
+
className: "w-2 h-2 rounded-full shadow-sm group-hover:scale-125 transition-transform duration-300",
|
|
1832
|
+
style: { backgroundColor: cat.hex }
|
|
1833
|
+
}
|
|
1834
|
+
),
|
|
1835
|
+
cat.label
|
|
1836
|
+
] }),
|
|
1837
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-bold text-slate-400 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-x-1 group-hover:translate-x-0", children: "Select" })
|
|
1838
|
+
]
|
|
1839
|
+
},
|
|
1840
|
+
cat.id
|
|
1841
|
+
)),
|
|
1842
|
+
tags.some(
|
|
1843
|
+
(t) => t.blockId === block.id && t.startIndex === selectionMenu.startIndex && t.endIndex === selectionMenu.endIndex
|
|
1844
|
+
) && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 pt-1 border-t border-white/60", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1845
|
+
"button",
|
|
1846
|
+
{
|
|
1847
|
+
onClick: (e) => {
|
|
1848
|
+
const tagToRemove = tags.find(
|
|
1849
|
+
(t) => t.blockId === block.id && t.startIndex === selectionMenu.startIndex && t.endIndex === selectionMenu.endIndex
|
|
1850
|
+
);
|
|
1851
|
+
if (tagToRemove) {
|
|
1852
|
+
removeTag(e, tagToRemove.id);
|
|
1853
|
+
clearSelection();
|
|
1854
|
+
}
|
|
1855
|
+
},
|
|
1856
|
+
className: "group w-full text-[12px] font-bold px-3 py-2 rounded-xl transition-all duration-300 text-left flex items-center justify-between hover:bg-rose-50 hover:text-rose-600 hover:shadow-[0_2px_10px_rgb(225,29,72,0.04)] active:scale-[0.98] text-slate-500 border border-transparent hover:border-rose-100",
|
|
1857
|
+
children: [
|
|
1858
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
1859
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-2 h-2 rounded-full shadow-sm bg-rose-400 group-hover:scale-125 transition-transform duration-300" }),
|
|
1860
|
+
"Remove Tag"
|
|
1861
|
+
] }),
|
|
1862
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] font-bold text-rose-400 opacity-0 group-hover:opacity-100 transition-all duration-300 translate-x-1 group-hover:translate-x-0", children: "Remove" })
|
|
1863
|
+
]
|
|
1864
|
+
}
|
|
1865
|
+
) })
|
|
1866
|
+
] })
|
|
1867
|
+
]
|
|
1868
|
+
}
|
|
1869
|
+
)
|
|
1870
|
+
]
|
|
1871
|
+
},
|
|
1872
|
+
block.id
|
|
1873
|
+
))
|
|
1874
|
+
}
|
|
1875
|
+
),
|
|
1876
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full lg:w-80 flex-shrink-0 bg-white/50 backdrop-blur-2xl border border-white shadow-[0_8px_30px_rgb(0,0,0,0.04)] rounded-[2.5rem] p-8 sticky top-6", children: [
|
|
1877
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "text-xs font-extrabold text-slate-800 uppercase tracking-[0.25em] mb-8 flex items-center gap-3", children: [
|
|
1878
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex items-center justify-center w-8 h-8 rounded-full bg-white/80 shadow-[0_4px_15px_rgb(0,0,0,0.04)] border border-white", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Tags, { className: "w-3.5 h-3.5 text-slate-500" }) }),
|
|
1879
|
+
"Tags",
|
|
1880
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-auto bg-slate-100/80 text-slate-500 px-2.5 py-1 rounded-lg text-[10px] font-bold tracking-widest border border-slate-200/50 shadow-inner", children: tags.length })
|
|
1881
|
+
] }),
|
|
1882
|
+
tags.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-8", children: CATEGORIES.map((cat) => {
|
|
1883
|
+
const catTags = Array.from(
|
|
1884
|
+
new Map(
|
|
1885
|
+
tags.filter((t) => t.categoryId === cat.id).map((tag) => [tag.text.toLowerCase(), tag])
|
|
1886
|
+
).values()
|
|
1887
|
+
);
|
|
1888
|
+
if (catTags.length === 0) return null;
|
|
1889
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col gap-4", children: [
|
|
1890
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em] flex items-center gap-2 border-b border-white/60 pb-2", children: [
|
|
1891
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1892
|
+
"div",
|
|
1893
|
+
{
|
|
1894
|
+
className: "w-2 h-2 rounded-full shadow-sm",
|
|
1895
|
+
style: { backgroundColor: cat.hex }
|
|
1896
|
+
}
|
|
1897
|
+
),
|
|
1898
|
+
cat.label
|
|
1899
|
+
] }),
|
|
1900
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2", children: catTags.map((tag) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1901
|
+
"span",
|
|
1902
|
+
{
|
|
1903
|
+
className: "text-[11px] font-bold px-3 py-1.5 rounded-xl bg-white/80 backdrop-blur-md border border-white shadow-[0_4px_15px_rgb(0,0,0,0.03)] hover:shadow-[0_4px_20px_rgb(0,0,0,0.06)] hover:-translate-y-0.5 transition-all duration-300 cursor-default",
|
|
1904
|
+
style: { color: cat.color },
|
|
1905
|
+
children: tag.text
|
|
1906
|
+
},
|
|
1907
|
+
tag.id
|
|
1908
|
+
)) })
|
|
1909
|
+
] }, cat.id);
|
|
1910
|
+
}) }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium text-slate-400 italic bg-white/40 p-6 rounded-[2rem] border border-white border-dashed text-center shadow-[0_4px_20px_rgb(0,0,0,0.02)]", children: "Highlight text to tag elements." })
|
|
1911
|
+
] })
|
|
1912
|
+
] }) });
|
|
1913
|
+
}
|
|
1914
|
+
function useScriptBreakdown({
|
|
1915
|
+
scenes
|
|
1916
|
+
}) {
|
|
1917
|
+
const [parsedScenes, setParsedScenes] = react.useState(scenes || []);
|
|
1918
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
1919
|
+
const [error, setError] = react.useState(null);
|
|
1920
|
+
const [hasFetchedFallback, setHasFetchedFallback] = react.useState(false);
|
|
1921
|
+
react.useEffect(() => {
|
|
1922
|
+
if (scenes && scenes.length > 0) {
|
|
1923
|
+
setParsedScenes(scenes);
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
if (hasFetchedFallback) return;
|
|
1927
|
+
const fetchFallback = async () => {
|
|
1928
|
+
setIsLoading(true);
|
|
1929
|
+
setHasFetchedFallback(true);
|
|
1930
|
+
try {
|
|
1931
|
+
const response = await fetch(
|
|
1932
|
+
"https://pub-4c2073ce6f434c4e92ed33f8e1c7f9ea.r2.dev/screenplay%20(1).sbx"
|
|
1933
|
+
);
|
|
1934
|
+
if (!response.ok) {
|
|
1935
|
+
throw new Error(
|
|
1936
|
+
`Failed to fetch fallback script: ${response.status}`
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
let text = await response.text();
|
|
1940
|
+
if (text.includes("<div")) {
|
|
1941
|
+
const textarea = document.createElement("textarea");
|
|
1942
|
+
textarea.innerHTML = text;
|
|
1943
|
+
text = textarea.value;
|
|
1944
|
+
}
|
|
1945
|
+
const parser = new DOMParser();
|
|
1946
|
+
const doc = parser.parseFromString(text, "text/html");
|
|
1947
|
+
const divs = Array.from(doc.querySelectorAll("div"));
|
|
1948
|
+
const extractedScenes = [];
|
|
1949
|
+
let currentSceneNumber = "1";
|
|
1950
|
+
let currentContent = "";
|
|
1951
|
+
let hasSeenFirstSceneHeading = false;
|
|
1952
|
+
divs.forEach((div) => {
|
|
1953
|
+
var _a;
|
|
1954
|
+
const isSceneHeading = div.classList.contains("divtype0");
|
|
1955
|
+
const divText = ((_a = div.textContent) == null ? void 0 : _a.trim()) || "";
|
|
1956
|
+
if (!divText) return;
|
|
1957
|
+
if (isSceneHeading) {
|
|
1958
|
+
if (hasSeenFirstSceneHeading) {
|
|
1959
|
+
extractedScenes.push({
|
|
1960
|
+
scene_number: currentSceneNumber,
|
|
1961
|
+
content: currentContent.trim()
|
|
1962
|
+
});
|
|
1963
|
+
currentContent = "";
|
|
1964
|
+
}
|
|
1965
|
+
hasSeenFirstSceneHeading = true;
|
|
1966
|
+
currentSceneNumber = div.getAttribute("data-scene") || String(extractedScenes.length + 1);
|
|
1967
|
+
}
|
|
1968
|
+
currentContent += div.outerHTML + "\n";
|
|
1969
|
+
});
|
|
1970
|
+
if (currentContent.trim()) {
|
|
1971
|
+
extractedScenes.push({
|
|
1972
|
+
scene_number: currentSceneNumber,
|
|
1973
|
+
content: currentContent.trim()
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
setParsedScenes(extractedScenes);
|
|
1977
|
+
} catch (err) {
|
|
1978
|
+
console.error("Error fetching fallback script:", err);
|
|
1979
|
+
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
1980
|
+
} finally {
|
|
1981
|
+
setIsLoading(false);
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
fetchFallback();
|
|
1985
|
+
}, [scenes, hasFetchedFallback]);
|
|
1986
|
+
return { scenes: parsedScenes, isLoading, error };
|
|
1987
|
+
}
|
|
1988
|
+
var use_script_breakdown_default = useScriptBreakdown;
|
|
1989
|
+
|
|
1990
|
+
// app/hook/use-script-breakdown-scene.ts
|
|
1991
|
+
function useScriptBreakdownScene(sceneNumber) {
|
|
1992
|
+
const { scenes, isLoading, error } = use_script_breakdown_default({ scenes: [] });
|
|
1993
|
+
const scene = react.useMemo(() => {
|
|
1994
|
+
return scenes.find((s) => s.scene_number === sceneNumber);
|
|
1995
|
+
}, [scenes, sceneNumber]);
|
|
1996
|
+
const blocks = react.useMemo(() => {
|
|
1997
|
+
if (!scene || !scene.content) return [];
|
|
1998
|
+
const parser = new DOMParser();
|
|
1999
|
+
const doc = parser.parseFromString(scene.content, "text/html");
|
|
2000
|
+
const divs = Array.from(doc.querySelectorAll("div"));
|
|
2001
|
+
const parsedBlocks = [];
|
|
2002
|
+
const typeMap = {
|
|
2003
|
+
divtype0: "SCENE_HEADING",
|
|
2004
|
+
divtype2: "ACTION",
|
|
2005
|
+
divtype3: "CHARACTER",
|
|
2006
|
+
divtype4: "PARENTHETICAL",
|
|
2007
|
+
divtype5: "DIALOGUE",
|
|
2008
|
+
divtype6: "TRANSITION"
|
|
2009
|
+
};
|
|
2010
|
+
divs.forEach((div) => {
|
|
2011
|
+
var _a;
|
|
2012
|
+
const divText = ((_a = div.textContent) == null ? void 0 : _a.trim()) || "";
|
|
2013
|
+
if (!divText) return;
|
|
2014
|
+
let type = "ACTION";
|
|
2015
|
+
for (const className of Array.from(div.classList)) {
|
|
2016
|
+
if (typeMap[className]) {
|
|
2017
|
+
type = typeMap[className];
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
parsedBlocks.push({ id: uuid(), type, text: divText });
|
|
2022
|
+
});
|
|
2023
|
+
return parsedBlocks;
|
|
2024
|
+
}, [scene]);
|
|
2025
|
+
const characters = react.useMemo(() => {
|
|
2026
|
+
const chars = blocks.filter((b) => b.type === "CHARACTER").map((b) => {
|
|
2027
|
+
const text = b.text.trim().toUpperCase();
|
|
2028
|
+
const parenIndex = text.indexOf("(");
|
|
2029
|
+
return parenIndex > -1 ? text.substring(0, parenIndex).trim() : text;
|
|
2030
|
+
}).filter(Boolean);
|
|
2031
|
+
return [...new Set(chars)];
|
|
2032
|
+
}, [blocks]);
|
|
2033
|
+
const [tags, setTags] = react.useState([]);
|
|
2034
|
+
const [selectionMenu, setSelectionMenu] = react.useState(null);
|
|
2035
|
+
const autoTaggedSceneRef = react.useRef(null);
|
|
2036
|
+
const [menuPlacement, setMenuPlacement] = react.useState("top");
|
|
2037
|
+
const menuRef = react.useRef(null);
|
|
2038
|
+
react.useEffect(() => {
|
|
2039
|
+
setTags([]);
|
|
2040
|
+
autoTaggedSceneRef.current = null;
|
|
2041
|
+
}, [sceneNumber]);
|
|
2042
|
+
react.useEffect(() => {
|
|
2043
|
+
if (blocks.length > 0 && characters.length > 0 && autoTaggedSceneRef.current !== sceneNumber) {
|
|
2044
|
+
const autoTags = [];
|
|
2045
|
+
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2046
|
+
const sortedChars = [...characters].sort((a, b) => b.length - a.length);
|
|
2047
|
+
blocks.forEach((block) => {
|
|
2048
|
+
sortedChars.forEach((char) => {
|
|
2049
|
+
const escapedChar = escapeRegExp(char);
|
|
2050
|
+
const regex = new RegExp(`\\b${escapedChar}\\b`, "gi");
|
|
2051
|
+
let match;
|
|
2052
|
+
while ((match = regex.exec(block.text)) !== null) {
|
|
2053
|
+
const isOverlapping = autoTags.some(
|
|
2054
|
+
(t) => t.blockId === block.id && (match.index + char.length > t.startIndex && match.index < t.endIndex)
|
|
2055
|
+
);
|
|
2056
|
+
if (!isOverlapping) {
|
|
2057
|
+
autoTags.push({
|
|
2058
|
+
id: uuid(),
|
|
2059
|
+
blockId: block.id,
|
|
2060
|
+
categoryId: "CAST",
|
|
2061
|
+
text: block.text.substring(match.index, match.index + char.length),
|
|
2062
|
+
startIndex: match.index,
|
|
2063
|
+
endIndex: match.index + char.length
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
});
|
|
2069
|
+
setTags(autoTags);
|
|
2070
|
+
autoTaggedSceneRef.current = sceneNumber;
|
|
2071
|
+
}
|
|
2072
|
+
}, [blocks, characters, sceneNumber]);
|
|
2073
|
+
const clearSelection = react.useCallback(() => {
|
|
2074
|
+
var _a;
|
|
2075
|
+
setSelectionMenu(null);
|
|
2076
|
+
(_a = window.getSelection()) == null ? void 0 : _a.removeAllRanges();
|
|
2077
|
+
}, []);
|
|
2078
|
+
react.useEffect(() => {
|
|
2079
|
+
if (!selectionMenu) {
|
|
2080
|
+
setMenuPlacement("top");
|
|
2081
|
+
}
|
|
2082
|
+
}, [selectionMenu]);
|
|
2083
|
+
react.useEffect(() => {
|
|
2084
|
+
if (selectionMenu && menuRef.current) {
|
|
2085
|
+
const rect = menuRef.current.getBoundingClientRect();
|
|
2086
|
+
if (menuPlacement === "top" && rect.top < 100) {
|
|
2087
|
+
setMenuPlacement("bottom");
|
|
2088
|
+
} else if (menuPlacement === "bottom" && rect.bottom > window.innerHeight - 40 && rect.top > 300) {
|
|
2089
|
+
setMenuPlacement("top");
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
}, [selectionMenu, menuPlacement]);
|
|
2093
|
+
react.useEffect(() => {
|
|
2094
|
+
const handleClickOutside = (e) => {
|
|
2095
|
+
if (selectionMenu && !e.target.closest(".tag-menu")) {
|
|
2096
|
+
clearSelection();
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
2100
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
2101
|
+
}, [selectionMenu, clearSelection]);
|
|
2102
|
+
const getAbsoluteOffset = (container, targetNode, targetOffset) => {
|
|
2103
|
+
let absoluteOffset = 0;
|
|
2104
|
+
let found = false;
|
|
2105
|
+
const traverse = (node) => {
|
|
2106
|
+
var _a;
|
|
2107
|
+
if (found) return;
|
|
2108
|
+
if (node === targetNode) {
|
|
2109
|
+
absoluteOffset += targetOffset;
|
|
2110
|
+
found = true;
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
2114
|
+
absoluteOffset += ((_a = node.nodeValue) == null ? void 0 : _a.length) || 0;
|
|
2115
|
+
} else {
|
|
2116
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
2117
|
+
traverse(node.childNodes[i]);
|
|
2118
|
+
if (found) return;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
traverse(container);
|
|
2123
|
+
return found ? absoluteOffset : null;
|
|
2124
|
+
};
|
|
2125
|
+
const handleMouseUp = () => {
|
|
2126
|
+
const selection = window.getSelection();
|
|
2127
|
+
if (!selection || selection.isCollapsed || !selection.toString().trim()) return;
|
|
2128
|
+
const range = selection.getRangeAt(0);
|
|
2129
|
+
let container = range.commonAncestorContainer;
|
|
2130
|
+
if (container.nodeType === Node.TEXT_NODE) container = container.parentElement;
|
|
2131
|
+
const blockElem = container.closest("[data-block-id]");
|
|
2132
|
+
if (!blockElem) return;
|
|
2133
|
+
const blockId = blockElem.getAttribute("data-block-id");
|
|
2134
|
+
const startOffset = getAbsoluteOffset(blockElem, range.startContainer, range.startOffset);
|
|
2135
|
+
const endOffset = getAbsoluteOffset(blockElem, range.endContainer, range.endOffset);
|
|
2136
|
+
if (startOffset !== null && endOffset !== null) {
|
|
2137
|
+
const rect = range.getBoundingClientRect();
|
|
2138
|
+
const blockRect = blockElem.getBoundingClientRect();
|
|
2139
|
+
setSelectionMenu({
|
|
2140
|
+
blockId,
|
|
2141
|
+
startIndex: Math.min(startOffset, endOffset),
|
|
2142
|
+
endIndex: Math.max(startOffset, endOffset),
|
|
2143
|
+
text: selection.toString().trim(),
|
|
2144
|
+
top: rect.top - blockRect.top,
|
|
2145
|
+
left: rect.left - blockRect.left + rect.width / 2
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
const addTag = (categoryId) => {
|
|
2150
|
+
if (!selectionMenu) return;
|
|
2151
|
+
const newTag = {
|
|
2152
|
+
id: uuid(),
|
|
2153
|
+
blockId: selectionMenu.blockId,
|
|
2154
|
+
categoryId,
|
|
2155
|
+
text: selectionMenu.text,
|
|
2156
|
+
startIndex: selectionMenu.startIndex,
|
|
2157
|
+
endIndex: selectionMenu.endIndex
|
|
2158
|
+
};
|
|
2159
|
+
setTags((prev) => {
|
|
2160
|
+
const filtered = prev.filter(
|
|
2161
|
+
(t) => t.blockId !== newTag.blockId || !(newTag.endIndex > t.startIndex && newTag.startIndex < t.endIndex)
|
|
2162
|
+
);
|
|
2163
|
+
return [...filtered, newTag];
|
|
2164
|
+
});
|
|
2165
|
+
clearSelection();
|
|
2166
|
+
};
|
|
2167
|
+
const removeTag = (e, id) => {
|
|
2168
|
+
e.stopPropagation();
|
|
2169
|
+
e.preventDefault();
|
|
2170
|
+
setTags((prev) => prev.filter((t) => t.id !== id));
|
|
2171
|
+
clearSelection();
|
|
2172
|
+
};
|
|
2173
|
+
return {
|
|
2174
|
+
scene,
|
|
2175
|
+
blocks,
|
|
2176
|
+
characters,
|
|
2177
|
+
isLoading,
|
|
2178
|
+
error,
|
|
2179
|
+
tags,
|
|
2180
|
+
selectionMenu,
|
|
2181
|
+
handleMouseUp,
|
|
2182
|
+
addTag,
|
|
2183
|
+
removeTag,
|
|
2184
|
+
clearSelection,
|
|
2185
|
+
menuPlacement,
|
|
2186
|
+
menuRef
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
exports.CATEGORIES = CATEGORIES;
|
|
1659
2191
|
exports.ScreenplayEditorView = ScreenplayEditorView;
|
|
2192
|
+
exports.ScriptBreakdownSceneView = ScriptBreakdownSceneView;
|
|
1660
2193
|
exports.blockStyles = blockStyles;
|
|
1661
2194
|
exports.blockTypes = blockTypes;
|
|
1662
2195
|
exports.handleSaveAsPdf = handleSaveAsPdf;
|
|
@@ -1664,6 +2197,7 @@ exports.handleSaveAsSbx = handleSaveAsSbx;
|
|
|
1664
2197
|
exports.icons = icons;
|
|
1665
2198
|
exports.timeOfDayOptions = timeOfDayOptions;
|
|
1666
2199
|
exports.useScreenplayEditor = useScreenplayEditor;
|
|
2200
|
+
exports.useScriptBreakdownScene = useScriptBreakdownScene;
|
|
1667
2201
|
exports.uuid = uuid;
|
|
1668
2202
|
//# sourceMappingURL=index.cjs.map
|
|
1669
2203
|
//# sourceMappingURL=index.cjs.map
|