@vishu1301/script-writing 0.1.1
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 +36 -0
- package/dist/index.d.mts +50 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.js +970 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +960 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +41 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defProps = Object.defineProperties;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
11
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
12
|
+
var __spreadValues = (a, b) => {
|
|
13
|
+
for (var prop in b || (b = {}))
|
|
14
|
+
if (__hasOwnProp.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
if (__getOwnPropSymbols)
|
|
17
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
18
|
+
if (__propIsEnum.call(b, prop))
|
|
19
|
+
__defNormalProp(a, prop, b[prop]);
|
|
20
|
+
}
|
|
21
|
+
return a;
|
|
22
|
+
};
|
|
23
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
24
|
+
var __export = (target, all) => {
|
|
25
|
+
for (var name in all)
|
|
26
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
27
|
+
};
|
|
28
|
+
var __copyProps = (to, from, except, desc) => {
|
|
29
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
30
|
+
for (let key of __getOwnPropNames(from))
|
|
31
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
32
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
33
|
+
}
|
|
34
|
+
return to;
|
|
35
|
+
};
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
|
|
38
|
+
// app/index.ts
|
|
39
|
+
var index_exports = {};
|
|
40
|
+
__export(index_exports, {
|
|
41
|
+
ScreenplayEditorView: () => ScreenplayEditorView,
|
|
42
|
+
blockStyles: () => blockStyles,
|
|
43
|
+
blockTypes: () => blockTypes,
|
|
44
|
+
icons: () => icons,
|
|
45
|
+
timeOfDayOptions: () => timeOfDayOptions,
|
|
46
|
+
useScreenplayEditor: () => useScreenplayEditor,
|
|
47
|
+
uuid: () => uuid
|
|
48
|
+
});
|
|
49
|
+
module.exports = __toCommonJS(index_exports);
|
|
50
|
+
|
|
51
|
+
// app/view/screenplay-editor.view.tsx
|
|
52
|
+
var import_react = require("react");
|
|
53
|
+
|
|
54
|
+
// app/types/screenplay-editor.types.tsx
|
|
55
|
+
var import_lucide_react = require("lucide-react");
|
|
56
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
57
|
+
var timeOfDayOptions = ["DAY", "NIGHT"];
|
|
58
|
+
var blockTypes = [
|
|
59
|
+
"SCENE_HEADING",
|
|
60
|
+
"ACTION",
|
|
61
|
+
"CHARACTER",
|
|
62
|
+
"PARENTHETICAL",
|
|
63
|
+
"DIALOGUE",
|
|
64
|
+
"TRANSITION"
|
|
65
|
+
];
|
|
66
|
+
var uuid = () => Math.random().toString(36).slice(2, 9);
|
|
67
|
+
var icons = {
|
|
68
|
+
SCENE_HEADING: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Clapperboard, { className: "w-5 h-5" }),
|
|
69
|
+
ACTION: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Sparkles, { className: "w-5 h-5" }),
|
|
70
|
+
CHARACTER: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.UserRound, { className: "w-5 h-5" }),
|
|
71
|
+
PARENTHETICAL: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Brackets, { className: "w-5 h-5" }),
|
|
72
|
+
DIALOGUE: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.MessageCircle, { className: "w-5 h-5" }),
|
|
73
|
+
TRANSITION: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowRightLeft, { className: "w-5 h-5" })
|
|
74
|
+
};
|
|
75
|
+
var blockStyles = {
|
|
76
|
+
SCENE_HEADING: {
|
|
77
|
+
label: "Scene Heading",
|
|
78
|
+
className: "uppercase font-bold text-zinc-900 flex items-center gap-3",
|
|
79
|
+
inputStyle: {
|
|
80
|
+
textTransform: "uppercase",
|
|
81
|
+
fontWeight: 700,
|
|
82
|
+
outline: "none",
|
|
83
|
+
whiteSpace: "pre-wrap",
|
|
84
|
+
overflowWrap: "break-word",
|
|
85
|
+
wordBreak: "break-word"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
ACTION: {
|
|
89
|
+
label: "Action",
|
|
90
|
+
className: "text-zinc-800 leading-relaxed",
|
|
91
|
+
inputStyle: {
|
|
92
|
+
outline: "none",
|
|
93
|
+
whiteSpace: "pre-wrap",
|
|
94
|
+
overflowWrap: "break-word",
|
|
95
|
+
wordBreak: "break-word",
|
|
96
|
+
lineHeight: 1.7
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
CHARACTER: {
|
|
100
|
+
label: "Character",
|
|
101
|
+
className: "uppercase font-bold text-center text-zinc-900 tracking-widest",
|
|
102
|
+
inputStyle: {
|
|
103
|
+
textTransform: "uppercase",
|
|
104
|
+
textAlign: "center",
|
|
105
|
+
fontWeight: 700,
|
|
106
|
+
letterSpacing: "0.1em",
|
|
107
|
+
outline: "none",
|
|
108
|
+
whiteSpace: "pre-wrap",
|
|
109
|
+
overflowWrap: "break-word",
|
|
110
|
+
wordBreak: "break-word"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
PARENTHETICAL: {
|
|
114
|
+
label: "Parenthetical",
|
|
115
|
+
className: "text-center text-zinc-600",
|
|
116
|
+
inputStyle: {
|
|
117
|
+
fontStyle: "normal",
|
|
118
|
+
maxWidth: "20rem",
|
|
119
|
+
margin: "0 auto",
|
|
120
|
+
outline: "none",
|
|
121
|
+
whiteSpace: "pre-wrap",
|
|
122
|
+
overflowWrap: "break-word",
|
|
123
|
+
wordBreak: "break-word",
|
|
124
|
+
textAlign: "center"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
DIALOGUE: {
|
|
128
|
+
label: "Dialogue",
|
|
129
|
+
className: "text-zinc-900 leading-relaxed max-w-[30rem] mx-auto",
|
|
130
|
+
inputStyle: {
|
|
131
|
+
marginLeft: "auto",
|
|
132
|
+
marginRight: "auto",
|
|
133
|
+
outline: "none",
|
|
134
|
+
whiteSpace: "pre-wrap",
|
|
135
|
+
overflowWrap: "break-word",
|
|
136
|
+
wordBreak: "break-word",
|
|
137
|
+
fontSize: "1.05rem",
|
|
138
|
+
textAlign: "left",
|
|
139
|
+
lineHeight: 1.7
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
TRANSITION: {
|
|
143
|
+
label: "Transition",
|
|
144
|
+
className: "uppercase font-bold text-right text-zinc-900",
|
|
145
|
+
inputStyle: {
|
|
146
|
+
textTransform: "uppercase",
|
|
147
|
+
fontWeight: 600,
|
|
148
|
+
textAlign: "right",
|
|
149
|
+
outline: "none",
|
|
150
|
+
whiteSpace: "pre-wrap",
|
|
151
|
+
overflowWrap: "break-word",
|
|
152
|
+
wordBreak: "break-word"
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// app/view/screenplay-editor.view.tsx
|
|
158
|
+
var import_lucide_react2 = require("lucide-react");
|
|
159
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
160
|
+
function ScreenplayEditorView({
|
|
161
|
+
blocks,
|
|
162
|
+
pages,
|
|
163
|
+
isPageSplitEnabled,
|
|
164
|
+
togglePageSplit,
|
|
165
|
+
refs,
|
|
166
|
+
focusedBlockId,
|
|
167
|
+
showSuggestions,
|
|
168
|
+
locations,
|
|
169
|
+
characters,
|
|
170
|
+
sceneNumbers,
|
|
171
|
+
handleBlockTextChange,
|
|
172
|
+
handleSceneTypeChange,
|
|
173
|
+
handleTimeOfDayChange,
|
|
174
|
+
handleBlockTypeChange,
|
|
175
|
+
handleKeyDown,
|
|
176
|
+
handleFocus,
|
|
177
|
+
handleBlur,
|
|
178
|
+
onSave,
|
|
179
|
+
onSaveAsPdf,
|
|
180
|
+
onSyncWithCloud
|
|
181
|
+
}) {
|
|
182
|
+
const [isRulesOpen, setIsRulesOpen] = (0, import_react.useState)(false);
|
|
183
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
184
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "sticky top-6 z-50 bg-white backdrop-blur-xl border border-white/10 rounded-full shadow-2xl flex gap-1 max-w-fit p-1.5 mb-12 select-none overflow-x-auto custom-scrollbar", children: blockTypes.map((type) => {
|
|
185
|
+
var _a;
|
|
186
|
+
const selected = ((_a = blocks.find((b) => b.id === focusedBlockId)) == null ? void 0 : _a.type) === type;
|
|
187
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
188
|
+
"button",
|
|
189
|
+
{
|
|
190
|
+
type: "button",
|
|
191
|
+
className: `flex items-center gap-2 px-4 py-2.5 rounded-full font-medium text-sm transition-all duration-300 ${selected ? "bg-zinc-900 text-white shadow-sm" : "text-zinc-400 hover:bg-zinc-800/10 hover:text-zinc-800"}`,
|
|
192
|
+
onClick: () => handleBlockTypeChange(type),
|
|
193
|
+
children: [
|
|
194
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
195
|
+
"input",
|
|
196
|
+
{
|
|
197
|
+
type: "radio",
|
|
198
|
+
name: "blockType",
|
|
199
|
+
id: `block-type-${type}`,
|
|
200
|
+
className: "sr-only",
|
|
201
|
+
"aria-label": blockStyles[type].label,
|
|
202
|
+
checked: selected,
|
|
203
|
+
readOnly: true
|
|
204
|
+
}
|
|
205
|
+
),
|
|
206
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
207
|
+
"label",
|
|
208
|
+
{
|
|
209
|
+
htmlFor: `block-type-${type}`,
|
|
210
|
+
className: "flex items-center gap-2 cursor-pointer",
|
|
211
|
+
children: [
|
|
212
|
+
icons[type],
|
|
213
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "whitespace-nowrap hidden sm:inline", children: blockStyles[type].label })
|
|
214
|
+
]
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
type
|
|
220
|
+
);
|
|
221
|
+
}) }),
|
|
222
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex flex-col gap-12 w-full items-center pb-24", children: pages.map((pageBlocks, pageIndex) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
223
|
+
"div",
|
|
224
|
+
{
|
|
225
|
+
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",
|
|
226
|
+
style: {
|
|
227
|
+
fontFamily: "var(--font-courier-prime, 'Courier New', Courier, monospace)"
|
|
228
|
+
},
|
|
229
|
+
children: [
|
|
230
|
+
pageBlocks.map((block) => {
|
|
231
|
+
var _a, _b;
|
|
232
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
233
|
+
"div",
|
|
234
|
+
{
|
|
235
|
+
"data-block-id": block.id,
|
|
236
|
+
className: `relative rounded-sm transition-all duration-200 outline-none ${focusedBlockId === block.id ? "bg-zinc-100/50" : "bg-transparent"}`,
|
|
237
|
+
children: block.type === "SCENE_HEADING" ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
238
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-2 px-4 py-1 bg-transparent", children: [
|
|
239
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "absolute -left-16 top-2 w-12 text-right text-zinc-400 font-semibold select-none", children: String(sceneNumbers[block.id] || 0) }),
|
|
240
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
241
|
+
"select",
|
|
242
|
+
{
|
|
243
|
+
className: "rounded-md text-zinc-800 font-bold px-1.5 py-1 appearance-none bg-transparent hover:bg-zinc-200/50 outline-none cursor-pointer w-fit transition-colors",
|
|
244
|
+
"aria-label": "Scene Type",
|
|
245
|
+
value: (_a = block.sceneType) != null ? _a : "INT.",
|
|
246
|
+
onChange: (e) => handleSceneTypeChange(block.id, e.target.value),
|
|
247
|
+
children: [
|
|
248
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { children: "INT." }),
|
|
249
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { children: "EXT." }),
|
|
250
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { children: "INT/EXT." })
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
),
|
|
254
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
255
|
+
"div",
|
|
256
|
+
{
|
|
257
|
+
ref: (el) => {
|
|
258
|
+
if (!el) return;
|
|
259
|
+
refs.current[block.id] = el;
|
|
260
|
+
},
|
|
261
|
+
contentEditable: true,
|
|
262
|
+
suppressContentEditableWarning: true,
|
|
263
|
+
"aria-label": `Scene Heading: ${block.text}`,
|
|
264
|
+
"aria-haspopup": "listbox",
|
|
265
|
+
"aria-expanded": focusedBlockId === block.id && showSuggestions && locations.length > 0,
|
|
266
|
+
spellCheck: false,
|
|
267
|
+
className: "min-w-[5rem] py-1 outline-none text-base font-bold uppercase tracking-widest break-all bg-transparent",
|
|
268
|
+
onInput: (e) => handleBlockTextChange(
|
|
269
|
+
block.id,
|
|
270
|
+
e.target.innerText
|
|
271
|
+
),
|
|
272
|
+
onKeyDown: (e) => handleKeyDown(e, block.id, block.text),
|
|
273
|
+
onFocus: () => handleFocus(block.id),
|
|
274
|
+
onBlur: () => handleBlur(block.id)
|
|
275
|
+
}
|
|
276
|
+
),
|
|
277
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-zinc-400/80 font-bold", children: "-" }),
|
|
278
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
279
|
+
"select",
|
|
280
|
+
{
|
|
281
|
+
className: "rounded-md text-zinc-800 font-bold px-1.5 py-1 appearance-none bg-transparent hover:bg-zinc-200/50 outline-none cursor-pointer transition-colors",
|
|
282
|
+
"aria-label": "Time of Day",
|
|
283
|
+
value: (_b = block.timeOfDay) != null ? _b : "DAY",
|
|
284
|
+
onChange: (e) => handleTimeOfDayChange(block.id, e.target.value),
|
|
285
|
+
children: timeOfDayOptions.map((t) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("option", { children: t }, t))
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
] }),
|
|
289
|
+
focusedBlockId === block.id && showSuggestions && locations.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
290
|
+
"div",
|
|
291
|
+
{
|
|
292
|
+
role: "listbox",
|
|
293
|
+
id: `suggestions-${block.id}`,
|
|
294
|
+
className: "absolute top-[calc(100%+6px)] left-0 min-w-[240px] z-50 bg-white border border-slate-200/80 shadow-xl shadow-slate-200/40 rounded-xl py-1 overflow-hidden animate-in fade-in zoom-in-95 duration-150",
|
|
295
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "max-h-60 overflow-y-auto custom-scrollbar", children: locations.filter(
|
|
296
|
+
(loc) => loc.startsWith(block.text.toUpperCase()) && loc !== block.text.toUpperCase()
|
|
297
|
+
).map((loc) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
298
|
+
"div",
|
|
299
|
+
{
|
|
300
|
+
role: "option",
|
|
301
|
+
className: "group flex items-center justify-between px-4 py-2.5 cursor-pointer transition-all duration-150 hover:bg-slate-50 active:bg-slate-100",
|
|
302
|
+
onMouseDown: (e) => {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
const element = refs.current[block.id];
|
|
305
|
+
if (element) {
|
|
306
|
+
element.innerText = loc;
|
|
307
|
+
handleBlockTextChange(block.id, loc);
|
|
308
|
+
element.focus();
|
|
309
|
+
const range = document.createRange();
|
|
310
|
+
const sel = window.getSelection();
|
|
311
|
+
range.selectNodeContents(element);
|
|
312
|
+
range.collapse(false);
|
|
313
|
+
sel == null ? void 0 : sel.removeAllRanges();
|
|
314
|
+
sel == null ? void 0 : sel.addRange(range);
|
|
315
|
+
}
|
|
316
|
+
handleBlur(block.id);
|
|
317
|
+
},
|
|
318
|
+
children: [
|
|
319
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-[12px] font-semibold tracking-wide text-slate-600 uppercase line-clamp-1", children: loc }),
|
|
320
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.ArrowRight, { className: "w-3.5 h-3.5 text-slate-300 opacity-0 -translate-x-2 group-hover:opacity-100 group-hover:translate-x-0 transition-all duration-200" })
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
loc
|
|
324
|
+
)) })
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
328
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
329
|
+
"div",
|
|
330
|
+
{
|
|
331
|
+
ref: (el) => {
|
|
332
|
+
if (!el) return;
|
|
333
|
+
refs.current[block.id] = el;
|
|
334
|
+
},
|
|
335
|
+
contentEditable: true,
|
|
336
|
+
suppressContentEditableWarning: true,
|
|
337
|
+
"aria-label": `${blockStyles[block.type].label} text`,
|
|
338
|
+
"aria-multiline": block.type === "ACTION" || block.type === "DIALOGUE",
|
|
339
|
+
spellCheck: false,
|
|
340
|
+
className: `block outline-none w-full min-h-[2.5rem] px-4 py-2 break-words ${blockStyles[block.type].className}`,
|
|
341
|
+
onInput: (e) => handleBlockTextChange(
|
|
342
|
+
block.id,
|
|
343
|
+
e.target.innerText
|
|
344
|
+
),
|
|
345
|
+
onKeyDown: (e) => handleKeyDown(e, block.id, block.text),
|
|
346
|
+
onFocus: () => handleFocus(block.id),
|
|
347
|
+
onBlur: () => handleBlur(block.id),
|
|
348
|
+
style: blockStyles[block.type].inputStyle
|
|
349
|
+
}
|
|
350
|
+
),
|
|
351
|
+
focusedBlockId === block.id && block.type === "CHARACTER" && showSuggestions && characters.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
352
|
+
"div",
|
|
353
|
+
{
|
|
354
|
+
role: "listbox",
|
|
355
|
+
id: `suggestions-${block.id}`,
|
|
356
|
+
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",
|
|
357
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "max-h-56 overflow-y-auto custom-scrollbar", children: characters.filter(
|
|
358
|
+
(char) => char.startsWith(block.text.toUpperCase()) && char !== block.text.toUpperCase()
|
|
359
|
+
).map((char) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
360
|
+
"div",
|
|
361
|
+
{
|
|
362
|
+
role: "option",
|
|
363
|
+
className: "group flex items-center px-4 py-2.5 cursor-pointer transition-colors duration-150 hover:bg-slate-50 active:bg-slate-100",
|
|
364
|
+
onMouseDown: (e) => {
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
const element = refs.current[block.id];
|
|
367
|
+
if (element) {
|
|
368
|
+
element.innerText = char;
|
|
369
|
+
handleBlockTextChange(block.id, char);
|
|
370
|
+
element.focus();
|
|
371
|
+
const range = document.createRange();
|
|
372
|
+
const sel = window.getSelection();
|
|
373
|
+
range.selectNodeContents(element);
|
|
374
|
+
range.collapse(false);
|
|
375
|
+
sel == null ? void 0 : sel.removeAllRanges();
|
|
376
|
+
sel == null ? void 0 : sel.addRange(range);
|
|
377
|
+
}
|
|
378
|
+
handleBlur(block.id);
|
|
379
|
+
},
|
|
380
|
+
children: [
|
|
381
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.User, { className: "w-3.5 h-3.5 text-slate-300 group-hover:text-sky-500 transition-colors mr-3" }),
|
|
382
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "flex-1 text-[11px] font-bold tracking-[0.1em] text-slate-600 uppercase text-left", children: char }),
|
|
383
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.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" })
|
|
384
|
+
]
|
|
385
|
+
},
|
|
386
|
+
char
|
|
387
|
+
)) })
|
|
388
|
+
}
|
|
389
|
+
)
|
|
390
|
+
] })
|
|
391
|
+
},
|
|
392
|
+
block.id + "-" + block.type
|
|
393
|
+
);
|
|
394
|
+
}),
|
|
395
|
+
isPageSplitEnabled && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "absolute bottom-10 right-16 md:right-20 text-zinc-400 font-semibold text-sm select-none pointer-events-none", children: [
|
|
396
|
+
pageIndex + 1,
|
|
397
|
+
"."
|
|
398
|
+
] })
|
|
399
|
+
]
|
|
400
|
+
},
|
|
401
|
+
pageIndex
|
|
402
|
+
)) }),
|
|
403
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "fixed bottom-6 right-6 flex flex-col items-end gap-4 z-50", children: [
|
|
404
|
+
onSave && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
405
|
+
"button",
|
|
406
|
+
{
|
|
407
|
+
onClick: onSave,
|
|
408
|
+
className: "flex items-center justify-center gap-2 w-auto px-4 h-12 rounded-full bg-zinc-950 text-white shadow-xl shadow-zinc-900/20 border border-white/10 hover:bg-zinc-800 hover:scale-105 active:scale-95 transition-all duration-300",
|
|
409
|
+
"aria-label": "Save Script",
|
|
410
|
+
children: [
|
|
411
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.Save, { className: "w-5 h-5" }),
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-sm font-semibold", children: "Save" })
|
|
413
|
+
]
|
|
414
|
+
}
|
|
415
|
+
),
|
|
416
|
+
onSaveAsPdf && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
417
|
+
"button",
|
|
418
|
+
{
|
|
419
|
+
onClick: onSaveAsPdf,
|
|
420
|
+
className: "flex items-center justify-center gap-2 w-auto px-4 h-12 rounded-full bg-zinc-950 text-white shadow-xl shadow-zinc-900/20 border border-white/10 hover:bg-zinc-800 hover:scale-105 active:scale-95 transition-all duration-300",
|
|
421
|
+
"aria-label": "Save Script as PDF",
|
|
422
|
+
children: [
|
|
423
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.FileDown, { className: "w-5 h-5" }),
|
|
424
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-sm font-semibold", children: "Save as PDF" })
|
|
425
|
+
]
|
|
426
|
+
}
|
|
427
|
+
),
|
|
428
|
+
isRulesOpen && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "bg-white/80 backdrop-blur-md rounded-xl shadow-lg border border-zinc-200/50 p-4 text-xs text-zinc-700 select-none font-sans overflow-hidden transition-all duration-300 w-64 origin-bottom-right animate-in fade-in zoom-in-95", children: [
|
|
429
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h4", { className: "font-bold text-zinc-800 mb-3 text-sm", children: "Settings & Shortcuts" }),
|
|
430
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "space-y-4", children: [
|
|
431
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between gap-6", children: [
|
|
432
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-semibold text-zinc-800", children: "A4 Page Split" }),
|
|
433
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
434
|
+
"button",
|
|
435
|
+
{
|
|
436
|
+
type: "button",
|
|
437
|
+
role: "switch",
|
|
438
|
+
"aria-checked": isPageSplitEnabled,
|
|
439
|
+
onClick: togglePageSplit,
|
|
440
|
+
className: `${isPageSplitEnabled ? "bg-zinc-900" : "bg-zinc-300"} relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none`,
|
|
441
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
442
|
+
"span",
|
|
443
|
+
{
|
|
444
|
+
"aria-hidden": "true",
|
|
445
|
+
className: `${isPageSplitEnabled ? "translate-x-4" : "translate-x-0"} pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`
|
|
446
|
+
}
|
|
447
|
+
)
|
|
448
|
+
}
|
|
449
|
+
)
|
|
450
|
+
] }),
|
|
451
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "space-y-1.5 pt-3 border-t border-zinc-200/50", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("ul", { className: "space-y-1.5", children: [
|
|
452
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: "flex items-center justify-between gap-6", children: [
|
|
453
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "New Block" }),
|
|
454
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("kbd", { className: "px-2 py-1 text-xs font-semibold text-zinc-800 bg-zinc-200/70 border border-zinc-300/70 rounded-md", children: "Enter" })
|
|
455
|
+
] }),
|
|
456
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: "flex items-center justify-between gap-6", children: [
|
|
457
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Delete Block" }),
|
|
458
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("kbd", { className: "px-2 py-1 text-xs font-semibold text-zinc-800 bg-zinc-200/70 border border-zinc-300/70 rounded-md", children: "Backspace" })
|
|
459
|
+
] }),
|
|
460
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: "flex items-center justify-between gap-6", children: [
|
|
461
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Change Type" }),
|
|
462
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center gap-1", children: [
|
|
463
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("kbd", { className: "px-2 py-1 text-xs font-semibold text-zinc-800 bg-zinc-200/70 border border-zinc-300/70 rounded-md", children: "Ctrl" }),
|
|
464
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "+" }),
|
|
465
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("kbd", { className: "px-2 py-1 text-xs font-semibold text-zinc-800 bg-zinc-200/70 border border-zinc-300/70 rounded-md", children: "\u2191/\u2193" })
|
|
466
|
+
] })
|
|
467
|
+
] })
|
|
468
|
+
] }) })
|
|
469
|
+
] })
|
|
470
|
+
] }),
|
|
471
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
472
|
+
"button",
|
|
473
|
+
{
|
|
474
|
+
onClick: () => setIsRulesOpen(!isRulesOpen),
|
|
475
|
+
className: `flex items-center justify-center w-12 h-12 rounded-full bg-zinc-950 text-white shadow-xl shadow-zinc-900/20 border border-white/10 hover:bg-zinc-800 hover:scale-105 active:scale-95 transition-all duration-300 ${isRulesOpen ? "rotate-90" : "rotate-0"}`,
|
|
476
|
+
"aria-label": "Toggle Settings",
|
|
477
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react2.Cog, { className: "w-5 h-5" })
|
|
478
|
+
}
|
|
479
|
+
)
|
|
480
|
+
] })
|
|
481
|
+
] });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// app/hook/use-screenplay-editor.ts
|
|
485
|
+
var import_react2 = require("react");
|
|
486
|
+
|
|
487
|
+
// app/service/screenplay-editor.service.ts
|
|
488
|
+
function getNextBlockType(currentType) {
|
|
489
|
+
switch (currentType) {
|
|
490
|
+
case "SCENE_HEADING":
|
|
491
|
+
return "ACTION";
|
|
492
|
+
case "CHARACTER":
|
|
493
|
+
return "PARENTHETICAL";
|
|
494
|
+
case "PARENTHETICAL":
|
|
495
|
+
return "DIALOGUE";
|
|
496
|
+
case "DIALOGUE":
|
|
497
|
+
return "CHARACTER";
|
|
498
|
+
case "TRANSITION":
|
|
499
|
+
return "SCENE_HEADING";
|
|
500
|
+
default:
|
|
501
|
+
return "ACTION";
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function createNewBlock(type) {
|
|
505
|
+
const newBlock = { id: uuid(), type, text: "" };
|
|
506
|
+
if (type === "SCENE_HEADING") {
|
|
507
|
+
newBlock.sceneType = "INT.";
|
|
508
|
+
newBlock.timeOfDay = "DAY";
|
|
509
|
+
}
|
|
510
|
+
return newBlock;
|
|
511
|
+
}
|
|
512
|
+
function addBlockAfter(blocks, currentBlockId) {
|
|
513
|
+
const currentIndex = blocks.findIndex((b) => b.id === currentBlockId);
|
|
514
|
+
if (currentIndex === -1) {
|
|
515
|
+
return { newBlocks: blocks, newBlockId: "" };
|
|
516
|
+
}
|
|
517
|
+
const currentBlock = blocks[currentIndex];
|
|
518
|
+
const nextType = getNextBlockType(currentBlock.type);
|
|
519
|
+
const newBlock = createNewBlock(nextType);
|
|
520
|
+
const newBlocks = [
|
|
521
|
+
...blocks.slice(0, currentIndex + 1),
|
|
522
|
+
newBlock,
|
|
523
|
+
...blocks.slice(currentIndex + 1)
|
|
524
|
+
];
|
|
525
|
+
return { newBlocks, newBlockId: newBlock.id };
|
|
526
|
+
}
|
|
527
|
+
function deleteBlock(blocks, blockIdToDelete) {
|
|
528
|
+
var _a, _b, _c;
|
|
529
|
+
if (blocks.length <= 1) {
|
|
530
|
+
return { newBlocks: blocks, nextFocusedId: ((_a = blocks[0]) == null ? void 0 : _a.id) || null };
|
|
531
|
+
}
|
|
532
|
+
const index = blocks.findIndex((b) => b.id === blockIdToDelete);
|
|
533
|
+
if (index === -1) {
|
|
534
|
+
return { newBlocks: blocks, nextFocusedId: null };
|
|
535
|
+
}
|
|
536
|
+
const newBlocks = [...blocks];
|
|
537
|
+
newBlocks.splice(index, 1);
|
|
538
|
+
const nextFocusedId = ((_b = newBlocks[index]) == null ? void 0 : _b.id) || ((_c = newBlocks[index - 1]) == null ? void 0 : _c.id) || null;
|
|
539
|
+
return { newBlocks, nextFocusedId };
|
|
540
|
+
}
|
|
541
|
+
function updateBlock(blocks, id, key, value) {
|
|
542
|
+
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, b), { [key]: value }) : b);
|
|
543
|
+
}
|
|
544
|
+
function changeBlockType(blocks, id, newType) {
|
|
545
|
+
const newBlock = createNewBlock(newType);
|
|
546
|
+
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, newBlock), { id: b.id }) : b);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// app/hook/use-screenplay-editor.ts
|
|
550
|
+
var initialBlocks = [
|
|
551
|
+
{
|
|
552
|
+
id: uuid(),
|
|
553
|
+
type: "SCENE_HEADING",
|
|
554
|
+
text: "",
|
|
555
|
+
sceneType: "INT.",
|
|
556
|
+
timeOfDay: "DAY"
|
|
557
|
+
}
|
|
558
|
+
];
|
|
559
|
+
function getCaretCharacterOffsetWithin(element) {
|
|
560
|
+
let caretOffset = 0;
|
|
561
|
+
const doc = element.ownerDocument;
|
|
562
|
+
const win = doc.defaultView;
|
|
563
|
+
const sel = win == null ? void 0 : win.getSelection();
|
|
564
|
+
if (sel && sel.rangeCount > 0) {
|
|
565
|
+
const range = sel.getRangeAt(0);
|
|
566
|
+
const preCaretRange = range.cloneRange();
|
|
567
|
+
preCaretRange.selectNodeContents(element);
|
|
568
|
+
preCaretRange.setEnd(range.endContainer, range.endOffset);
|
|
569
|
+
caretOffset = preCaretRange.toString().length;
|
|
570
|
+
}
|
|
571
|
+
return caretOffset;
|
|
572
|
+
}
|
|
573
|
+
function setCaretPosition(element, offset) {
|
|
574
|
+
const sel = window.getSelection();
|
|
575
|
+
if (!sel) return;
|
|
576
|
+
const range = document.createRange();
|
|
577
|
+
let currentOffset = 0;
|
|
578
|
+
let found = false;
|
|
579
|
+
function traverseNodes(node) {
|
|
580
|
+
var _a;
|
|
581
|
+
if (found) return;
|
|
582
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
583
|
+
const length = ((_a = node.nodeValue) == null ? void 0 : _a.length) || 0;
|
|
584
|
+
if (currentOffset + length >= offset) {
|
|
585
|
+
range.setStart(node, offset - currentOffset);
|
|
586
|
+
range.collapse(true);
|
|
587
|
+
found = true;
|
|
588
|
+
} else {
|
|
589
|
+
currentOffset += length;
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
593
|
+
traverseNodes(node.childNodes[i]);
|
|
594
|
+
if (found) break;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
traverseNodes(element);
|
|
599
|
+
if (!found) {
|
|
600
|
+
range.setStart(element, 0);
|
|
601
|
+
range.collapse(true);
|
|
602
|
+
}
|
|
603
|
+
sel.removeAllRanges();
|
|
604
|
+
sel.addRange(range);
|
|
605
|
+
}
|
|
606
|
+
function useScreenplayEditor() {
|
|
607
|
+
const [blocks, setBlocks] = (0, import_react2.useState)(initialBlocks);
|
|
608
|
+
const refs = (0, import_react2.useRef)({});
|
|
609
|
+
const [focusedBlockId, setFocusedBlockId] = (0, import_react2.useState)(
|
|
610
|
+
initialBlocks[0].id
|
|
611
|
+
);
|
|
612
|
+
const [newBlockId, setNewBlockId] = (0, import_react2.useState)(null);
|
|
613
|
+
const [showSuggestions, setShowSuggestions] = (0, import_react2.useState)(false);
|
|
614
|
+
const blurTimeout = (0, import_react2.useRef)(null);
|
|
615
|
+
const [isPageSplitEnabled, setIsPageSplitEnabled] = (0, import_react2.useState)(false);
|
|
616
|
+
const [pageBreaks, setPageBreaks] = (0, import_react2.useState)([]);
|
|
617
|
+
const focusStateRef = (0, import_react2.useRef)(null);
|
|
618
|
+
const togglePageSplit = (0, import_react2.useCallback)(() => {
|
|
619
|
+
if (focusedBlockId && document.activeElement && document.activeElement.hasAttribute("contenteditable")) {
|
|
620
|
+
const el = refs.current[focusedBlockId];
|
|
621
|
+
if (el) {
|
|
622
|
+
const offset = getCaretCharacterOffsetWithin(el);
|
|
623
|
+
focusStateRef.current = { id: focusedBlockId, offset };
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
setIsPageSplitEnabled((prev) => !prev);
|
|
627
|
+
}, [focusedBlockId]);
|
|
628
|
+
const locations = (0, import_react2.useMemo)(() => {
|
|
629
|
+
const locs = blocks.filter((b) => b.type === "SCENE_HEADING" && b.text.trim() !== "").map((b) => b.text.trim().toUpperCase());
|
|
630
|
+
return [...new Set(locs)];
|
|
631
|
+
}, [blocks]);
|
|
632
|
+
const characters = (0, import_react2.useMemo)(() => {
|
|
633
|
+
const chars = blocks.filter((b) => b.type === "CHARACTER" && b.text.trim() !== "").map((b) => b.text.trim().toUpperCase());
|
|
634
|
+
return [...new Set(chars)];
|
|
635
|
+
}, [blocks]);
|
|
636
|
+
const sceneNumbers = (0, import_react2.useMemo)(() => {
|
|
637
|
+
const map = {};
|
|
638
|
+
let count = 0;
|
|
639
|
+
blocks.forEach((block) => {
|
|
640
|
+
if (block.type === "SCENE_HEADING") {
|
|
641
|
+
count++;
|
|
642
|
+
map[block.id] = count;
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
return map;
|
|
646
|
+
}, [blocks]);
|
|
647
|
+
(0, import_react2.useEffect)(() => {
|
|
648
|
+
var _a;
|
|
649
|
+
if (newBlockId && refs.current[newBlockId]) {
|
|
650
|
+
(_a = refs.current[newBlockId]) == null ? void 0 : _a.focus();
|
|
651
|
+
setFocusedBlockId(newBlockId);
|
|
652
|
+
setNewBlockId(null);
|
|
653
|
+
}
|
|
654
|
+
}, [newBlockId]);
|
|
655
|
+
(0, import_react2.useEffect)(() => {
|
|
656
|
+
blocks.forEach((block) => {
|
|
657
|
+
const element = refs.current[block.id];
|
|
658
|
+
if (element && element.innerText !== block.text && document.activeElement !== element) {
|
|
659
|
+
element.innerText = block.text;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
}, [blocks, isPageSplitEnabled, pageBreaks]);
|
|
663
|
+
(0, import_react2.useEffect)(() => {
|
|
664
|
+
if (!isPageSplitEnabled) {
|
|
665
|
+
setPageBreaks([]);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const saveFocus = () => {
|
|
669
|
+
if (focusedBlockId && document.activeElement && document.activeElement.hasAttribute("contenteditable")) {
|
|
670
|
+
const el = refs.current[focusedBlockId];
|
|
671
|
+
if (el) {
|
|
672
|
+
const offset = getCaretCharacterOffsetWithin(el);
|
|
673
|
+
focusStateRef.current = { id: focusedBlockId, offset };
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
focusStateRef.current = null;
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
const timeoutId = setTimeout(() => {
|
|
680
|
+
let currentHeight = 0;
|
|
681
|
+
let hasSceneOnCurrentPage = false;
|
|
682
|
+
const A4_HEIGHT = 960;
|
|
683
|
+
const newPageBreaks = [];
|
|
684
|
+
blocks.forEach((block) => {
|
|
685
|
+
const el = refs.current[block.id];
|
|
686
|
+
if (el) {
|
|
687
|
+
const measureEl = document.querySelector(`[data-block-id="${block.id}"]`) || el;
|
|
688
|
+
const style = window.getComputedStyle(measureEl);
|
|
689
|
+
const marginTop = parseFloat(style.marginTop) || 0;
|
|
690
|
+
const marginBottom = parseFloat(style.marginBottom) || 0;
|
|
691
|
+
const height = measureEl.getBoundingClientRect().height + marginTop + marginBottom;
|
|
692
|
+
let breakPage = false;
|
|
693
|
+
if (currentHeight + height > A4_HEIGHT) {
|
|
694
|
+
breakPage = true;
|
|
695
|
+
} else if (block.type === "SCENE_HEADING") {
|
|
696
|
+
if (hasSceneOnCurrentPage) {
|
|
697
|
+
breakPage = true;
|
|
698
|
+
} else if (currentHeight > A4_HEIGHT - 120) {
|
|
699
|
+
breakPage = true;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (breakPage && currentHeight > 0) {
|
|
703
|
+
newPageBreaks.push(block.id);
|
|
704
|
+
currentHeight = height;
|
|
705
|
+
hasSceneOnCurrentPage = block.type === "SCENE_HEADING";
|
|
706
|
+
} else {
|
|
707
|
+
currentHeight += height;
|
|
708
|
+
if (block.type === "SCENE_HEADING") {
|
|
709
|
+
hasSceneOnCurrentPage = true;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
setPageBreaks((prev) => {
|
|
715
|
+
if (prev.length !== newPageBreaks.length) {
|
|
716
|
+
saveFocus();
|
|
717
|
+
return newPageBreaks;
|
|
718
|
+
}
|
|
719
|
+
for (let i = 0; i < prev.length; i++) {
|
|
720
|
+
if (prev[i] !== newPageBreaks[i]) {
|
|
721
|
+
saveFocus();
|
|
722
|
+
return newPageBreaks;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return prev;
|
|
726
|
+
});
|
|
727
|
+
}, 300);
|
|
728
|
+
return () => clearTimeout(timeoutId);
|
|
729
|
+
}, [blocks, isPageSplitEnabled, focusedBlockId]);
|
|
730
|
+
const pages = (0, import_react2.useMemo)(() => {
|
|
731
|
+
if (!isPageSplitEnabled || pageBreaks.length === 0) return [blocks];
|
|
732
|
+
const result = [];
|
|
733
|
+
let currentPage = [];
|
|
734
|
+
for (const block of blocks) {
|
|
735
|
+
if (pageBreaks.includes(block.id) && currentPage.length > 0) {
|
|
736
|
+
result.push(currentPage);
|
|
737
|
+
currentPage = [];
|
|
738
|
+
}
|
|
739
|
+
currentPage.push(block);
|
|
740
|
+
}
|
|
741
|
+
if (currentPage.length > 0) result.push(currentPage);
|
|
742
|
+
return result;
|
|
743
|
+
}, [blocks, isPageSplitEnabled, pageBreaks]);
|
|
744
|
+
(0, import_react2.useEffect)(() => {
|
|
745
|
+
if (focusStateRef.current) {
|
|
746
|
+
const { id, offset } = focusStateRef.current;
|
|
747
|
+
const el = refs.current[id];
|
|
748
|
+
if (el && document.activeElement !== el) {
|
|
749
|
+
el.focus();
|
|
750
|
+
setCaretPosition(el, offset);
|
|
751
|
+
}
|
|
752
|
+
focusStateRef.current = null;
|
|
753
|
+
}
|
|
754
|
+
}, [pages]);
|
|
755
|
+
const handleBlockTextChange = (0, import_react2.useCallback)((id, text) => {
|
|
756
|
+
setBlocks((bs) => updateBlock(bs, id, "text", text));
|
|
757
|
+
}, []);
|
|
758
|
+
const handleSceneTypeChange = (0, import_react2.useCallback)(
|
|
759
|
+
(id, sceneType) => {
|
|
760
|
+
setBlocks(
|
|
761
|
+
(bs) => updateBlock(bs, id, "sceneType", sceneType)
|
|
762
|
+
);
|
|
763
|
+
setTimeout(() => {
|
|
764
|
+
var _a;
|
|
765
|
+
(_a = refs.current[id]) == null ? void 0 : _a.focus();
|
|
766
|
+
setFocusedBlockId(id);
|
|
767
|
+
}, 10);
|
|
768
|
+
},
|
|
769
|
+
[]
|
|
770
|
+
);
|
|
771
|
+
const handleTimeOfDayChange = (0, import_react2.useCallback)((id, time) => {
|
|
772
|
+
setBlocks((bs) => updateBlock(bs, id, "timeOfDay", time));
|
|
773
|
+
setTimeout(() => {
|
|
774
|
+
var _a;
|
|
775
|
+
(_a = refs.current[id]) == null ? void 0 : _a.focus();
|
|
776
|
+
setFocusedBlockId(id);
|
|
777
|
+
}, 10);
|
|
778
|
+
}, []);
|
|
779
|
+
const handleBlockTypeChange = (0, import_react2.useCallback)(
|
|
780
|
+
(newType) => {
|
|
781
|
+
if (!focusedBlockId) return;
|
|
782
|
+
setBlocks(
|
|
783
|
+
(bs) => changeBlockType(bs, focusedBlockId, newType)
|
|
784
|
+
);
|
|
785
|
+
setTimeout(() => {
|
|
786
|
+
const el = refs.current[focusedBlockId];
|
|
787
|
+
if (el) {
|
|
788
|
+
el.innerText = "";
|
|
789
|
+
el.focus();
|
|
790
|
+
}
|
|
791
|
+
}, 0);
|
|
792
|
+
},
|
|
793
|
+
[focusedBlockId]
|
|
794
|
+
);
|
|
795
|
+
const focusBlock = (id, position = "start") => {
|
|
796
|
+
const el = refs.current[id];
|
|
797
|
+
if (!el) return;
|
|
798
|
+
el.focus();
|
|
799
|
+
const range = document.createRange();
|
|
800
|
+
const sel = window.getSelection();
|
|
801
|
+
if (!sel) return;
|
|
802
|
+
range.selectNodeContents(el);
|
|
803
|
+
range.collapse(position === "start");
|
|
804
|
+
sel.removeAllRanges();
|
|
805
|
+
sel.addRange(range);
|
|
806
|
+
};
|
|
807
|
+
const cycleBlockType = (id, direction) => {
|
|
808
|
+
setBlocks((bs) => {
|
|
809
|
+
const block = bs.find((b) => b.id === id);
|
|
810
|
+
if (!block) return bs;
|
|
811
|
+
const idx = blockTypes.indexOf(block.type);
|
|
812
|
+
let newIdx = direction === "up" ? idx - 1 : idx + 1;
|
|
813
|
+
if (newIdx < 0) newIdx = blockTypes.length - 1;
|
|
814
|
+
if (newIdx >= blockTypes.length) newIdx = 0;
|
|
815
|
+
return updateBlock(bs, id, "type", blockTypes[newIdx]);
|
|
816
|
+
});
|
|
817
|
+
};
|
|
818
|
+
const handleKeyDown = (0, import_react2.useCallback)(
|
|
819
|
+
(e, id, text) => {
|
|
820
|
+
var _a;
|
|
821
|
+
if ((e.key === "Backspace" || e.key === "Delete") && text.length <= 1) {
|
|
822
|
+
e.preventDefault();
|
|
823
|
+
const { newBlocks, nextFocusedId } = deleteBlock(
|
|
824
|
+
blocks,
|
|
825
|
+
id
|
|
826
|
+
);
|
|
827
|
+
setBlocks(newBlocks);
|
|
828
|
+
if (nextFocusedId) {
|
|
829
|
+
setTimeout(() => {
|
|
830
|
+
focusBlock(nextFocusedId, "end");
|
|
831
|
+
}, 0);
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
836
|
+
e.preventDefault();
|
|
837
|
+
const { newBlocks, newBlockId: newBlockId2 } = addBlockAfter(
|
|
838
|
+
blocks,
|
|
839
|
+
id
|
|
840
|
+
);
|
|
841
|
+
setBlocks(newBlocks);
|
|
842
|
+
setNewBlockId(newBlockId2);
|
|
843
|
+
} else if (e.key === "ArrowUp" && e.ctrlKey) {
|
|
844
|
+
e.preventDefault();
|
|
845
|
+
cycleBlockType(id, "up");
|
|
846
|
+
requestAnimationFrame(() => focusBlock(id));
|
|
847
|
+
} else if (e.key === "ArrowDown" && e.ctrlKey) {
|
|
848
|
+
e.preventDefault();
|
|
849
|
+
cycleBlockType(id, "down");
|
|
850
|
+
requestAnimationFrame(() => focusBlock(id));
|
|
851
|
+
} else if (e.key === "ArrowUp" && !e.ctrlKey) {
|
|
852
|
+
const selection = window.getSelection();
|
|
853
|
+
if (!selection || !selection.isCollapsed || selection.rangeCount === 0) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const range = selection.getRangeAt(0);
|
|
857
|
+
const contentEditable = e.currentTarget;
|
|
858
|
+
let atStart = false;
|
|
859
|
+
if (range.startOffset === 0) {
|
|
860
|
+
let node = range.startContainer;
|
|
861
|
+
if (node === contentEditable) {
|
|
862
|
+
atStart = true;
|
|
863
|
+
} else {
|
|
864
|
+
let atStartOfParents = true;
|
|
865
|
+
while (node !== contentEditable) {
|
|
866
|
+
if (node.previousSibling) {
|
|
867
|
+
atStartOfParents = false;
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
node = node.parentNode;
|
|
871
|
+
if (!node) {
|
|
872
|
+
atStartOfParents = false;
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
atStart = atStartOfParents;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (atStart) {
|
|
880
|
+
e.preventDefault();
|
|
881
|
+
const currentIndex = blocks.findIndex((b) => b.id === id);
|
|
882
|
+
if (currentIndex > 0) {
|
|
883
|
+
const prevBlockId = blocks[currentIndex - 1].id;
|
|
884
|
+
focusBlock(prevBlockId, "end");
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
} else if (e.key === "ArrowDown" && !e.ctrlKey) {
|
|
888
|
+
const selection = window.getSelection();
|
|
889
|
+
if (!selection || !selection.isCollapsed || selection.rangeCount === 0) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const range = selection.getRangeAt(0);
|
|
893
|
+
const contentEditable = e.currentTarget;
|
|
894
|
+
const container = range.endContainer;
|
|
895
|
+
const isAtEndOffset = range.endOffset === (container.nodeType === Node.TEXT_NODE ? (_a = container.textContent) == null ? void 0 : _a.length : container.childNodes.length);
|
|
896
|
+
if (isAtEndOffset) {
|
|
897
|
+
let atEnd = false;
|
|
898
|
+
let node = container;
|
|
899
|
+
if (node === contentEditable) {
|
|
900
|
+
atEnd = true;
|
|
901
|
+
} else {
|
|
902
|
+
let atEndOfParents = true;
|
|
903
|
+
while (node !== contentEditable) {
|
|
904
|
+
if (node.nextSibling) {
|
|
905
|
+
atEndOfParents = false;
|
|
906
|
+
break;
|
|
907
|
+
}
|
|
908
|
+
node = node.parentNode;
|
|
909
|
+
if (!node) {
|
|
910
|
+
atEndOfParents = false;
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
atEnd = atEndOfParents;
|
|
915
|
+
}
|
|
916
|
+
if (atEnd) {
|
|
917
|
+
e.preventDefault();
|
|
918
|
+
const currentIndex = blocks.findIndex((b) => b.id === id);
|
|
919
|
+
if (currentIndex < blocks.length - 1) {
|
|
920
|
+
const nextBlockId = blocks[currentIndex + 1].id;
|
|
921
|
+
focusBlock(nextBlockId, "start");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
[blocks]
|
|
928
|
+
);
|
|
929
|
+
const handleFocus = (0, import_react2.useCallback)((id) => {
|
|
930
|
+
if (blurTimeout.current) {
|
|
931
|
+
clearTimeout(blurTimeout.current);
|
|
932
|
+
}
|
|
933
|
+
setFocusedBlockId(id);
|
|
934
|
+
setShowSuggestions(true);
|
|
935
|
+
}, []);
|
|
936
|
+
const handleBlur = (0, import_react2.useCallback)((id) => {
|
|
937
|
+
if (document.activeElement === refs.current[id]) return;
|
|
938
|
+
blurTimeout.current = setTimeout(() => setShowSuggestions(false), 200);
|
|
939
|
+
}, []);
|
|
940
|
+
return {
|
|
941
|
+
blocks,
|
|
942
|
+
pages,
|
|
943
|
+
isPageSplitEnabled,
|
|
944
|
+
togglePageSplit,
|
|
945
|
+
refs,
|
|
946
|
+
focusedBlockId,
|
|
947
|
+
showSuggestions,
|
|
948
|
+
locations,
|
|
949
|
+
characters,
|
|
950
|
+
sceneNumbers,
|
|
951
|
+
handleBlockTextChange,
|
|
952
|
+
handleSceneTypeChange,
|
|
953
|
+
handleTimeOfDayChange,
|
|
954
|
+
handleBlockTypeChange,
|
|
955
|
+
handleKeyDown,
|
|
956
|
+
handleFocus,
|
|
957
|
+
handleBlur
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
961
|
+
0 && (module.exports = {
|
|
962
|
+
ScreenplayEditorView,
|
|
963
|
+
blockStyles,
|
|
964
|
+
blockTypes,
|
|
965
|
+
icons,
|
|
966
|
+
timeOfDayOptions,
|
|
967
|
+
useScreenplayEditor,
|
|
968
|
+
uuid
|
|
969
|
+
});
|
|
970
|
+
//# sourceMappingURL=index.js.map
|