diff --git a/.gitignore b/.gitignore
index d19d857..97ed884 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,8 +4,9 @@ package-lock.json
# Build outputs
/*.map
-/*.css
script.js
+script.css
+fonts/
coverage
dist
build
diff --git a/index.html b/index.html
index f5e8cd9..add8421 100644
--- a/index.html
+++ b/index.html
@@ -4,15 +4,8 @@
Screenwriter
-
-
-
-
-
-
-
-
-
+
+
diff --git a/package.json b/package.json
index a3b8b06..850eb71 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"description": "A simple copy of Final Draft that can be used online",
"main": "app.js",
"dependencies": {
- "bootstrap": "^5.3.8",
+ "bootstrap": "^3.4.1",
"firebase": "^12.9.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
@@ -13,7 +13,6 @@
"underscore": "^1.13.7"
},
"devDependencies": {
- "@babel/cli": "^7.28.6",
"@babel/core": "^7.29.0",
"@babel/preset-env": "^7.29.0",
"@babel/preset-react": "^7.28.5",
@@ -21,6 +20,8 @@
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"babel-jest": "^30.2.0",
+ "esbuild": "^0.27.4",
+ "esbuild-sass-plugin": "^3.7.0",
"gulp": "^5.0.1",
"gulp-plumber": "^1.2.1",
"gulp-sass": "^6.0.1",
@@ -30,9 +31,9 @@
"sass": "^1.97.3"
},
"scripts": {
- "build": "npm run build:jsx && npm run build:css",
- "build:jsx": "babel script.jsx --out-file script.js --presets=@babel/preset-react --no-babelrc",
- "build:css": "gulp sass",
+ "build": "node scripts/build.js",
+ "build:jsx": "node scripts/build.js",
+ "build:css": "node scripts/build.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
diff --git a/script.jsx b/script.jsx
index 556558d..e86f201 100644
--- a/script.jsx
+++ b/script.jsx
@@ -1,657 +1,10 @@
-var types = ['scene', 'action', 'character', 'dialogue', 'parenthetical', 'transition', 'shot', 'text'];
-var nextTypes = {
- scene: 'action',
- action: 'action',
- character: 'dialogue',
- dialogue: 'character',
- parenthetical: 'dialogue',
- transition: 'scene',
- shot: 'action',
- text: 'text'
-};
+// CSS entry – bundled by esbuild so Bootstrap and app styles are self-hosted
+import 'bootstrap/dist/css/bootstrap.min.css';
+import './styles.scss';
+import './print.scss';
-var StopPropagationMixin = {
- stopProp: function(event) {
- event.nativeEvent.stopImmediatePropagation();
- },
-};
-function cursorPos(element) {
- var caretOffset = 0;
- var doc = element.ownerDocument || element.document;
- var win = doc.defaultView || doc.parentWindow;
- var sel;
- if (typeof win.getSelection != "undefined") {
- sel = win.getSelection();
- if (sel.rangeCount > 0) {
- var range = win.getSelection().getRangeAt(0);
- var preCaretRange = range.cloneRange();
- preCaretRange.selectNodeContents(element);
- preCaretRange.setEnd(range.endContainer, range.endOffset);
- caretOffset = preCaretRange.toString().length;
- }
- } else if ( (sel = doc.selection) && sel.type != "Control") {
- var textRange = sel.createRange();
- var preCaretTextRange = doc.body.createTextRange();
- preCaretTextRange.moveToElementText(element);
- preCaretTextRange.setEndPoint("EndToEnd", textRange);
- caretOffset = preCaretTextRange.text.length;
- }
- return caretOffset;
-};
+import { createRoot } from 'react-dom/client';
+import App from './src/App.jsx';
-function placeCaretAtEnd(el) {
- el.focus();
- if (typeof window.getSelection != "undefined"
- && typeof document.createRange != "undefined") {
- var range = document.createRange();
- range.selectNodeContents(el);
- range.collapse(false);
- var sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- } else if (typeof document.body.createTextRange != "undefined") {
- var textRange = document.body.createTextRange();
- textRange.moveToElementText(el);
- textRange.collapse(false);
- textRange.select();
- }
-}
-
-function S4() {
- return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
-}
-function guid() {
- return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
-}
-
-
-var Script = React.createClass({
- mixins: [ReactFireMixin, ReactRouter.State],
- getInitialState: function() {
- highlight = '';
-
- return {
- scriptId: this.getParams().scriptId,
- action: this.getParams().action,
- script: {},
- editing: {}
- };
- },
- componentWillMount: function() {
- this.loadScript();
- },
- componentWillReceiveProps: function() {
- this.loadScript();
- },
- loadScript: function() {
- if (this.firebaseRefs.script) this.unbind('script');
- this.bindAsObject(new Firebase("https://screenwrite.firebaseio.com/"+this.getParams().scriptId), "script");
- // CLEANUP OLD DATA
- var fb = new Firebase("https://screenwrite.firebaseio.com/"+this.state.scriptId);
- fb.once('value', (function(snapshot){
- if (!snapshot.val()) {
- fb.set({});
- var newLine = fb.child('lines').push({ type: 'scene' });
- fb.update({ firstLine: newLine.key });
- return;
- }
- if (snapshot.val().firstLine) return;
- var previous, previousIndex;
- fb.update({firstLine: '0'});
- _.each(snapshot.val().lines, function(line, index) {
- if (previous) {
- fb.child('lines/'+previousIndex+'/next').set(index);
- }
- previous = line;
- previousIndex = index;
- });
- }).bind(this));
-
- window.onunload = (function(){
- if (_.keys(this.state.script.lines).length <= 2)
- fb.remove();
- }).bind(this);
- },
- editing: function(line) {
- this.setState({editing:line});
- },
- getSuggestion: function(lineIndex, fromValue) {
- if (!this.state.script.lines[lineIndex].text) return '';
- var type = this.state.script.lines[lineIndex].type;
- var text = fromValue && fromValue.toUpperCase() || this.state.script.lines[lineIndex].text.toUpperCase();
-
- var suggestions = [];
- var passed = false;
- var iterate = (function(index){
- var line = this.state.script.lines[index];
- if (!line) {
- console.warn('Line not found at index:', index);
- return;
- }
- if (line.type == type
- && line.text
- && line.text.length > text.length
- && line.text.toUpperCase().indexOf(text) === 0)
- suggestions.push(line.text.toUpperCase());
- if (index == lineIndex)
- passed = true;
- if (passed && suggestions.length) return;
- if (line.next && this.state.script.lines[line.next])
- iterate(line.next);
- }).bind(this);
- iterate(this.state.script.firstLine);
- return (suggestions.pop() || '').substr(text.length);
- },
- handleKey: function(event, line, index, prevIndex, prevPrevIndex) {
- switch (event.keyCode) {
- case 38: // up
- if (prevIndex) {
- if (event.metaKey || event.ctrlKey) {
- // [a, b, C, d] => [a, C, b, d]
- // A points to C
- if (prevPrevIndex)
- this.firebaseRefs.script.child('lines/'+prevPrevIndex).update({next: index});
- else
- this.firebaseRefs.script.update({firstLine:index});
- // C points to B
- var newNext = line.next;
- this.firebaseRefs.script.child('lines/'+index).update({next: prevIndex });
- // B points to D
- if (line.next)
- this.firebaseRefs.script.child('lines/'+prevIndex).update({next: newNext });
- else
- this.firebaseRefs.script.child('lines/'+prevIndex+'/next').remove();
- this.refs['line'+index].focus(true);
- event.preventDefault();
- } else if (!cursorPos(event.target)) {
- this.refs['line'+prevIndex].focus(true);
- event.preventDefault();
- }
- }
- break;
- case 40: // down
- if (line.next) {
- if (event.metaKey || event.ctrlKey) {
- // [a, b, c, d] => [a, c, b, d]
-
- // A points to C
- if (prevIndex)
- this.firebaseRefs.script.child('lines/'+prevIndex).update({next: line.next});
- else
- this.firebaseRefs.script.update({firstLine:line.next});
- var newNext = this.state.script.lines[line.next].next;
- // C points to B
- this.firebaseRefs.script.child('lines/'+line.next).update({next: index});
- // B points to D
- if (newNext)
- this.firebaseRefs.script.child('lines/'+index).update({ next: newNext });
- else
- this.firebaseRefs.script.child('lines/'+index+'/next').remove();
- this.refs['line'+index].focus();
- event.preventDefault();
- } else if (cursorPos(event.target) >= event.target.textContent.length ) {
- this.refs['line'+line.next].focus();
- event.preventDefault();
- }
- }
- break;
- case 8: // backspace
- if (!line.text && prevIndex) {
- // update previous line
- if (line.next)
- this.firebaseRefs.script.child('lines/'+prevIndex).update({next:line.next});
- else
- this.firebaseRefs.script.child('lines/'+prevIndex+'/next').remove();
-
- // remove line
- this.firebaseRefs.script.child('lines/'+index).remove();
- this.refs['line'+prevIndex].focus(true);
- event.preventDefault();
- }
- break;
- case 13: // enter
- if (line.text) {
- // create new line pointing to current line's `next`
- var newItem = { type: nextTypes[line.type] };
- if (line.next) newItem.next = line.next;
- var newRef = this.firebaseRefs.script.child('lines').push(newItem);
- // point current line to the new line
- this.firebaseRefs.script.child('lines/'+index+'/next').set(newRef.key);
- setTimeout((function(){
- this.refs['line'+newRef.key].focus();
- }).bind(this));
- }
- }
- },
- render: function() {
- var indexes = {};
- var lines = [];
- var previous = null, prevPrevious = null;
- var next = (function(line, index){
- if (!line) {
- console.warn('Line not found at index:', index);
- return;
- }
- lines.push(
-
- );
- prevPrevious = previous;
- previous = index;
- if (line.next && this.state.script.lines[line.next]) next(this.state.script.lines[line.next], line.next);
- }).bind(this);
-
- if (this.state.script && this.state.script.lines && this.state.script.firstLine) {
- next(this.state.script.lines[this.state.script.firstLine], this.state.script.firstLine);
- } else {
- lines = Loading Script...
- }
- return (
-
- );
- }
-});
-
-var highlight = '';
-
-var Line = React.createClass({
- mixins: [ReactFireMixin, StopPropagationMixin, ReactRouter.State],
- getInitialState: function() {
- return {
- comments: this.props.line.comments,
- commenting: false,
- scriptId: this.getParams().scriptId,
- focused: false,
- };
- },
- componentWillMount: function() {
- this.bindAsObject(new Firebase("https://screenwrite.firebaseio.com/"+this.state.scriptId+"/lines/" + this.props.index), "line");
- },
- handleChange: function(event) {
- this.firebaseRefs.line.update({'text':event.target.value});
- },
- handleComment: function(event) {
- this.firebaseRefs.line.update({'comment':event.target.value});
- },
- nextType: function(){
- var index = types.indexOf(this.props.line.type) + 1;
- index = (index < types.length) ? index : 0;
- this.setType(types[index]);
- },
- prevType: function() {
- var index = types.indexOf(this.props.line.type) - 1;
- index = (index >= 0) ? index : types.length - 1;
- this.setType(types[index]);
- },
- setType: function(type) {
- this.firebaseRefs.line.update({type:type});
- },
- handleKey: function(event) {
- switch (event.keyCode) {
- case 39: // right
- if (~['character', 'scene'].indexOf(this.props.line.type) && cursorPos(event.target) >= event.target.textContent.length) {
- var suggestion;
- if (suggestion = this.props.getSuggestion(this.props.index)) {
- this.firebaseRefs.line.update({ text: this.props.line.text + suggestion }, (function(){
- placeCaretAtEnd(this.refs.text.getDOMNode());
- }).bind(this));
- }
- }
- break;
- case 13: // enter
- event.preventDefault();
- if (this.props.line.text) {
- break;
- }
- case 9: // tab
- event.preventDefault();
- if (event.shiftKey) {
- this.prevType();
- } else {
- this.nextType();
- }
- }
-
- this.props.onKeyDown(event, this.props.line, this.props.index, this.props.previous, this.props.prevPrevious);
- },
- comment: function(event) {
- event.stopPropagation();
- this.setState({ commenting: !this.state.commenting }, function(){
- if (this.state.commenting) {
- var that = this;
- document.addEventListener('click', function listener(){
- that.setState({ commenting: false });
- document.removeEventListener('click', listener);
- });
- this.refs.commentBox.getDOMNode().focus();
- }
- });
- },
- focus: function(atEnd) {
- if (atEnd)
- placeCaretAtEnd(this.refs.text.getDOMNode());
- else
- this.refs.text.getDOMNode().focus();
- },
- onFocus: function(event) {
- this.setState({focused:true});
- this.props.onFocus(event);
- },
- onBlur: function(event) {
- this.setState({focused:false});
- },
- render: function() {
- var classes = {
- line: true,
- commented: this.props.line.comment,
- highlight: highlight && this.props.line.text && highlight.toUpperCase()==this.props.line.text.toUpperCase()
- };
- classes[this.props.line.type] = true;
- classes = React.addons.classSet(classes);
-
- var line, suggest;
- if (this.props.readonly) {
- line =
;
- } else {
- if (this.state.focused) {
- suggest = this.props.getSuggestion(this.props.index);
- }
-
- line =
- }
-
- return (
-
- {line}
-
-
-
-
- {this.state.commenting && }
-
- );
- }
-});
-
-var ContentEditable = React.createClass({
- shouldComponentUpdate: function(nextProps) {
- // Only update if the html prop has changed AND it's different from what we have in the DOM
- // This prevents cursor jumping when the user is typing (internal changes)
- // but allows updates from external sources (autocomplete, Firebase updates, etc.)
- var currentHTML = this.getDOMNode().innerHTML;
- return nextProps.html !== currentHTML;
- },
- stripPaste: function(e){
- // Strip formatting on paste
- var tempDiv = document.createElement("DIV");
- var item = _.findWhere(e.clipboardData.items, { type: 'text/plain' });
- item.getAsString(function (value) {
- tempDiv.innerHTML = value;
- document.execCommand('inserttext', false, tempDiv.innerText);
- });
- e.preventDefault();
- },
- emitChange: function(){
- var html = this.getDOMNode().innerHTML;
- if (this.props.onChange && html !== this.lastHtml) {
-
- this.props.onChange({
- target: {
- value: html
- }
- });
- }
- this.lastHtml = html;
- },
- render: function(){
- return
;
- }
-});
-
-var Nav = React.createClass({
- mixins: [ReactFireMixin, StopPropagationMixin, ReactRouter.State],
- getInitialState: function() {
- return {
- open: null,
- script: {},
- scriptId: this.getParams().scriptId,
- highlight: ''
- };
- },
- componentWillMount: function() {
- this.bindAsObject(new Firebase("https://screenwrite.firebaseio.com/"+this.state.scriptId), "script");
- },
- toggle: function(dropdown, event) {
- var that = this;
- if (this.state.open != dropdown) {
- setTimeout((function(){
- document.addEventListener('click', function listener(){
- that.setState({ open: false });
- document.removeEventListener('click', listener);
- });
- this.setState({ open: dropdown });
- }).bind(this));
- }
- },
- setType: function(type) {
- if (!this.props.editingIndex) return;
- this.firebaseRefs.script.child('lines/'+this.props.editingIndex+'/type').set(type);
- },
- print: function() {
- window.print();
- },
- highlight: function(event) {
- highlight = event.target.value;
- this.setState({highlight: event.target.value});
- },
- handleChange: function(input, event) {
- this.firebaseRefs.script.child(input).set(event.target.value);
- },
- newScript: function(){
- var fb = new Firebase("https://screenwrite.firebaseio.com/");
- var newRef = fb.push();
- window.location.hash = '#/' + newRef.key;
- window.location.reload(); // force firebase to reload
- },
- render: function() {
- if (!this.state.script) return
;
-
- if (this.state.script.title)
- document.title = 'Screenwriter: ' + this.state.script.title;
-
- var editing = this.state.script.lines && this.state.script.lines[this.props.editingIndex] || {};
- if (this.state.open=='print') {
- var characters = [];
- _.each(_.uniq(_.map(_.pluck(_.where(this.state.script.lines, {type:'character'}), 'text'), function(character){
- return character && character.toUpperCase();
- })), function(character){
- if (character)
- characters.push({character} )
- });
- }
- return (
-
-
-
-
-
-
- {this.state.open == 'print' &&
-
-
Print Script
-
-
-
-
-
-
-
-
-
-
-
-
- -- Highlighter --
- {characters}
-
-
-
-
}
-
- {this.props.readonly ||
-
-
-
- {editing.type || 'Line Type'}
-
-
- {this.state.open == 'line' &&
-
-
- {types.map(function(type){
- return
- {type}
-
- }, this)}
-
-
}
-
- }
-
-
-
-
- {this.props.script.title}
- {this.props.script.authors && by
}
- {this.props.script.authors}
- {this.state.highlight && Character: {this.state.highlight.toUpperCase()}
}
- {this.props.script.leftAddress}
- {this.props.script.rightAddress}
-
-
- );
- }
-});
-
-var Home = React.createClass({
- newScript: function(){
- var fb = new Firebase("https://screenwrite.firebaseio.com/");
- var newRef = fb.push();
- window.location.hash = '#/' + newRef.key;
- window.location.reload(); // force firebase to reload
- },
- render: function() {
- var commentStyles = {
- color: '#dd0',
- textShadow: '0 1px 1px #000',
- fontSize: '120%'
- };
- return (
-
-
-
-
-
Collaborate:
-
Share your custom URL with friends to collaborate or add /view to the end for readonly mode!
-
-
Shortcuts:
-
- Enter Insert new line
- (Shift+)Tab Cycle through line types
- Up/Down Move through lines
- Cmd/Ctrl+Up/Down Reorder lines
- Right Autocomplete the character or scene
-
-
-
Comments:
-
Hover over a line and click comment button
-
-
Notes:
-
Scripts are not secure, if someone can figure out your URL, they can edit it. Print to PDF if you want a permanent copy.
-
- );
- }
-
-});
-
-var App = React.createClass({
- render: function() {
- return ;
- }
-});
-
-Route = ReactRouter.Route;
-Link = ReactRouter.Link;
-RouteHandler = ReactRouter.RouteHandler;
-DefaultRoute = ReactRouter.DefaultRoute;
-var routes = (
-
-
-
-
-
-);
-
-
-ReactRouter.run(routes, function (Handler) {
- React.render( , document.getElementById('container'));
-});
\ No newline at end of file
+var root = createRoot(document.getElementById('container'));
+root.render( );
diff --git a/scripts/build.js b/scripts/build.js
new file mode 100644
index 0000000..9d8bfce
--- /dev/null
+++ b/scripts/build.js
@@ -0,0 +1,26 @@
+#!/usr/bin/env node
+// Unified build script: bundles JS/JSX + SCSS + Bootstrap CSS via esbuild.
+// Outputs:
+// script.js – JavaScript bundle
+// script.css – CSS bundle (Bootstrap + app styles + print styles)
+
+const esbuild = require('esbuild');
+const { sassPlugin } = require('esbuild-sass-plugin');
+
+esbuild.build({
+ entryPoints: ['script.jsx'],
+ bundle: true,
+ outfile: 'script.js',
+ platform: 'browser',
+ jsx: 'automatic',
+ plugins: [sassPlugin()],
+ loader: {
+ '.eot': 'file',
+ '.ttf': 'file',
+ '.woff': 'file',
+ '.woff2': 'file',
+ '.svg': 'file',
+ },
+ assetNames: 'fonts/[name]-[hash]',
+ logLevel: 'info',
+}).catch(function() { process.exit(1); });
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 0000000..0bbfb47
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,18 @@
+import { HashRouter, Routes, Route } from 'react-router';
+import Home from './components/Home.jsx';
+import ScriptPage from './components/ScriptPage.jsx';
+
+// Root App component with hash-based routing
+function App() {
+ return (
+
+
+ } />
+ } />
+ } />
+
+
+ );
+}
+
+export default App;
diff --git a/src/components/ContentEditable.jsx b/src/components/ContentEditable.jsx
new file mode 100644
index 0000000..2a6c8d0
--- /dev/null
+++ b/src/components/ContentEditable.jsx
@@ -0,0 +1,72 @@
+import { forwardRef, useRef, useLayoutEffect } from 'react';
+
+// ContentEditable – keeps cursor stable while the user types.
+// The html prop is written to the DOM only when it genuinely differs from the
+// current DOM content, preventing React from resetting the cursor position.
+var ContentEditable = forwardRef(function ContentEditable(props, ref) {
+ var html = props.html;
+ var onChange = props.onChange;
+ var onKeyDown = props.onKeyDown;
+ var onClick = props.onClick;
+ var className = props.className;
+ var onFocus = props.onFocus;
+ var onBlur = props.onBlur;
+ var suggest = props.suggest;
+
+ var domRef = useRef(null);
+ var lastHtml = useRef(html || '');
+
+ var setRef = function(el) {
+ domRef.current = el;
+ if (typeof ref === 'function') ref(el);
+ else if (ref) ref.current = el;
+ };
+
+ useLayoutEffect(function() {
+ if (!domRef.current) return;
+ if ((html || '') !== domRef.current.innerHTML) {
+ domRef.current.innerHTML = html || '';
+ lastHtml.current = html || '';
+ }
+ });
+
+ var emitChange = function() {
+ if (!domRef.current) return;
+ var currentHtml = domRef.current.innerHTML;
+ if (onChange && currentHtml !== lastHtml.current) {
+ onChange({ target: { value: currentHtml } });
+ }
+ lastHtml.current = currentHtml;
+ };
+
+ var stripPaste = function(e) {
+ var items = Array.from(e.clipboardData.items);
+ var textItem = items.find(function(i) { return i.type === 'text/plain'; });
+ if (textItem) {
+ var tempDiv = document.createElement('div');
+ textItem.getAsString(function(value) {
+ tempDiv.innerHTML = value;
+ document.execCommand('inserttext', false, tempDiv.innerText);
+ });
+ }
+ e.preventDefault();
+ };
+
+ return (
+
+ );
+});
+
+export default ContentEditable;
diff --git a/src/components/Home.jsx b/src/components/Home.jsx
new file mode 100644
index 0000000..0798621
--- /dev/null
+++ b/src/components/Home.jsx
@@ -0,0 +1,58 @@
+import { Link } from 'react-router';
+import { ref as dbRef, push } from 'firebase/database';
+import { db } from '../firebase.js';
+
+// Home component – landing page shown at '#/'.
+function Home() {
+ var commentStyles = {
+ color: '#dd0',
+ textShadow: '0 1px 1px #000',
+ fontSize: '120%'
+ };
+
+ var newScript = function() {
+ var newRef = push(dbRef(db));
+ window.location.hash = '#/' + newRef.key;
+ window.location.reload();
+ };
+
+ return (
+
+
+
+
Collaborate:
+
Share your custom URL with friends to collaborate or add /view to the end for readonly mode!
+
+
Shortcuts:
+
+ Enter Insert new line
+ (Shift+)Tab Cycle through line types
+ Up/Down Move through lines
+ Cmd/Ctrl+Up/Down Reorder lines
+ Right Autocomplete the character or scene
+
+
+
Comments:
+
Hover over a line and click comment button
+
+
Notes:
+
Scripts are not secure, if someone can figure out your URL, they can edit it. Print to PDF if you want a permanent copy.
+
+ );
+}
+
+export default Home;
diff --git a/src/components/Line.jsx b/src/components/Line.jsx
new file mode 100644
index 0000000..58068b0
--- /dev/null
+++ b/src/components/Line.jsx
@@ -0,0 +1,152 @@
+import { forwardRef, useRef, useState, useImperativeHandle } from 'react';
+import { ref as dbRef, update } from 'firebase/database';
+import { db } from '../firebase.js';
+import { types } from '../constants.js';
+import { cursorPos, placeCaretAtEnd } from '../utils.js';
+import ContentEditable from './ContentEditable.jsx';
+
+// Line component – renders a single screenplay line and exposes a focus() method.
+var Line = forwardRef(function Line(props, ref) {
+ var line = props.line;
+ var index = props.index;
+ var previous = props.previous;
+ var prevPrevious = props.prevPrevious;
+ var onFocusProp = props.onFocus;
+ var getSuggestion = props.getSuggestion;
+ var readonly = props.readonly;
+ var onKeyDown = props.onKeyDown;
+ var scriptId = props.scriptId;
+ var highlight = props.highlight;
+
+ var focused = useState(false);
+ var setFocused = focused[1];
+ focused = focused[0];
+
+ var commentingState = useState(false);
+ var setCommenting = commentingState[1];
+ var commenting = commentingState[0];
+
+ var textRef = useRef(null);
+ var commentBoxRef = useRef(null);
+ var firebaseLineRef = useRef(dbRef(db, scriptId + '/lines/' + index));
+
+ useImperativeHandle(ref, function() {
+ return {
+ focus: function(atEnd) {
+ if (textRef.current) {
+ if (atEnd) placeCaretAtEnd(textRef.current);
+ else textRef.current.focus();
+ }
+ }
+ };
+ });
+
+ var handleChange = function(event) {
+ update(firebaseLineRef.current, { text: event.target.value });
+ };
+
+ var handleComment = function(event) {
+ update(firebaseLineRef.current, { comment: event.target.value });
+ };
+
+ var nextTypeAction = function() {
+ var idx = types.indexOf(line.type) + 1;
+ update(firebaseLineRef.current, { type: idx < types.length ? types[idx] : types[0] });
+ };
+
+ var prevTypeAction = function() {
+ var idx = types.indexOf(line.type) - 1;
+ update(firebaseLineRef.current, { type: idx >= 0 ? types[idx] : types[types.length - 1] });
+ };
+
+ var handleKey = function(event) {
+ switch (event.keyCode) {
+ case 39: { // right – autocomplete
+ if (~['character', 'scene'].indexOf(line.type) && cursorPos(event.target) >= event.target.textContent.length) {
+ var suggestion = getSuggestion(index);
+ if (suggestion) {
+ update(firebaseLineRef.current, { text: line.text + suggestion }).then(function() {
+ if (textRef.current) placeCaretAtEnd(textRef.current);
+ });
+ }
+ }
+ break;
+ }
+ case 13: // enter
+ event.preventDefault();
+ if (line.text) break;
+ // fall through to tab
+ /* falls through */
+ case 9: // tab
+ event.preventDefault();
+ if (event.shiftKey) prevTypeAction();
+ else nextTypeAction();
+ }
+ onKeyDown(event, line, index, previous, prevPrevious);
+ };
+
+ var handleCommentToggle = function(event) {
+ event.stopPropagation();
+ setCommenting(function(c) {
+ var next = !c;
+ if (next) {
+ setTimeout(function() {
+ var listener = function() {
+ setCommenting(false);
+ document.removeEventListener('click', listener);
+ };
+ document.addEventListener('click', listener);
+ if (commentBoxRef.current) commentBoxRef.current.focus();
+ }, 0);
+ }
+ return next;
+ });
+ };
+
+ var classes = [
+ 'line',
+ line.type,
+ line.comment ? 'commented' : null,
+ highlight && line.text && highlight.toUpperCase() === line.text.toUpperCase() ? 'highlight' : null
+ ].filter(Boolean).join(' ');
+
+ var suggest;
+ var lineElement;
+ if (readonly) {
+ lineElement =
;
+ } else {
+ if (focused) suggest = getSuggestion(index);
+ lineElement = (
+
+ );
+ }
+
+ return (
+
+ {lineElement}
+
+
+
+ {commenting && (
+
+ )}
+
+ );
+});
+
+export default Line;
diff --git a/src/components/Nav.jsx b/src/components/Nav.jsx
new file mode 100644
index 0000000..fa19fa7
--- /dev/null
+++ b/src/components/Nav.jsx
@@ -0,0 +1,206 @@
+import { useState } from 'react';
+import { ref as dbRef, set, push } from 'firebase/database';
+import { db } from '../firebase.js';
+import { types } from '../constants.js';
+
+// Nav component – top navigation bar with title, print options and line-type picker.
+function Nav(props) {
+ var script = props.script;
+ var editingIndex = props.editingIndex;
+ var readonly = props.readonly;
+ var scriptId = props.scriptId;
+ var highlight = props.highlight;
+ var onHighlightChange = props.onHighlightChange;
+
+ var openState = useState(null);
+ var setOpen = openState[1];
+ var open = openState[0];
+
+ var toggle = function(dropdown) {
+ if (open !== dropdown) {
+ setTimeout(function() {
+ var listener = function() {
+ setOpen(null);
+ document.removeEventListener('click', listener);
+ };
+ document.addEventListener('click', listener);
+ }, 0);
+ setOpen(dropdown);
+ }
+ };
+
+ var setType = function(type) {
+ if (!editingIndex) return;
+ set(dbRef(db, scriptId + '/lines/' + editingIndex + '/type'), type);
+ };
+
+ var handleFieldChange = function(field, event) {
+ set(dbRef(db, scriptId + '/' + field), event.target.value);
+ };
+
+ var newScript = function() {
+ var newRef = push(dbRef(db));
+ window.location.hash = '#/' + newRef.key;
+ window.location.reload();
+ };
+
+ if (!script) return
;
+
+ if (script.title) document.title = 'Screenwriter: ' + script.title;
+
+ var editing = (script.lines && script.lines[editingIndex]) || {};
+
+ var characters = [];
+ if (open === 'print' && script.lines) {
+ var seen = {};
+ Object.values(script.lines).forEach(function(l) {
+ if (l && l.type === 'character' && l.text) {
+ var upper = l.text.toUpperCase();
+ if (!seen[upper]) {
+ seen[upper] = true;
+ characters.push({upper} );
+ }
+ }
+ });
+ }
+
+ return (
+
+
+
+
+
+
+ {open === 'print' && (
+
+
+
+ Print Script
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -- Highlighter --
+ {characters}
+
+
+
+
+ )}
+
+ {!readonly && (
+
+
+
+ {editing.type || 'Line Type'}
+
+
+ {open === 'line' && (
+
+
+
+ {types.map(function(type) {
+ return (
+
+ {type}
+
+ );
+ })}
+
+
+ )}
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default Nav;
diff --git a/src/components/ScriptPage.jsx b/src/components/ScriptPage.jsx
new file mode 100644
index 0000000..e3eb652
--- /dev/null
+++ b/src/components/ScriptPage.jsx
@@ -0,0 +1,193 @@
+import { useState, useEffect, useRef, useCallback } from 'react';
+import { useParams } from 'react-router';
+import { ref as dbRef, onValue, set, push, update, remove } from 'firebase/database';
+import { db } from '../firebase.js';
+import { nextTypes } from '../constants.js';
+import { cursorPos } from '../utils.js';
+import Nav from './Nav.jsx';
+import Line from './Line.jsx';
+
+// ScriptPage component – loads a script from Firebase and renders its lines.
+function ScriptPage() {
+ var params = useParams();
+ var scriptId = params.scriptId;
+ var action = params.action;
+
+ var scriptState = useState(null);
+ var setScript = scriptState[1];
+ var script = scriptState[0];
+
+ var editingState = useState(null);
+ var setEditingIndex = editingState[1];
+ var editingIndex = editingState[0];
+
+ var highlightState = useState('');
+ var setHighlight = highlightState[1];
+ var highlight = highlightState[0];
+
+ var lineRefs = useRef({});
+
+ useEffect(function() {
+ if (!scriptId) return;
+ var scriptRef = dbRef(db, scriptId);
+ var unsubscribe = onValue(scriptRef, function(snapshot) {
+ var val = snapshot.val();
+ if (!val) {
+ // Initialize an empty script with one blank scene line
+ var linesRef = dbRef(db, scriptId + '/lines');
+ var newLineRef = push(linesRef, { type: 'scene' });
+ set(dbRef(db, scriptId + '/firstLine'), newLineRef.key);
+ } else {
+ setScript(val);
+ }
+ });
+ return unsubscribe;
+ }, [scriptId]);
+
+ useEffect(function() {
+ if (script && script.title) {
+ document.title = 'Screenwriter: ' + script.title;
+ }
+ }, [script]);
+
+ var getSuggestion = useCallback(function(lineIndex, fromValue) {
+ if (!script || !script.lines || !script.lines[lineIndex]) return '';
+ if (!script.lines[lineIndex].text) return '';
+ var type = script.lines[lineIndex].type;
+ var text = (fromValue && fromValue.toUpperCase()) || script.lines[lineIndex].text.toUpperCase();
+ var suggestions = [];
+ var passed = false;
+ var iterate = function(index) {
+ var line = script.lines[index];
+ if (!line) return;
+ if (line.type === type && line.text && line.text.length > text.length && line.text.toUpperCase().indexOf(text) === 0) {
+ suggestions.push(line.text.toUpperCase());
+ }
+ if (index === lineIndex) passed = true;
+ if (passed && suggestions.length) return;
+ if (line.next && script.lines[line.next]) iterate(line.next);
+ };
+ if (script.firstLine) iterate(script.firstLine);
+ return (suggestions.pop() || '').substr(text.length);
+ }, [script]);
+
+ var focusLine = function(index, atEnd) {
+ var r = lineRefs.current[index];
+ if (r && r.current) r.current.focus(atEnd || false);
+ };
+
+ var handleKey = useCallback(function(event, line, index, prevIndex, prevPrevIndex) {
+ switch (event.keyCode) {
+ case 38: // up
+ if (prevIndex) {
+ if (event.metaKey || event.ctrlKey) {
+ // [a, b, C, d] => [a, C, b, d]: A→C, C→B, B→D
+ if (prevPrevIndex) update(dbRef(db, scriptId + '/lines/' + prevPrevIndex), { next: index });
+ else update(dbRef(db, scriptId), { firstLine: index });
+ var nextIndexBeforeSwap = line.next;
+ update(dbRef(db, scriptId + '/lines/' + index), { next: prevIndex });
+ if (line.next) update(dbRef(db, scriptId + '/lines/' + prevIndex), { next: nextIndexBeforeSwap });
+ else remove(dbRef(db, scriptId + '/lines/' + prevIndex + '/next'));
+ setTimeout(function() { focusLine(index, true); }, 0);
+ event.preventDefault();
+ } else if (!cursorPos(event.target)) {
+ focusLine(prevIndex, true);
+ event.preventDefault();
+ }
+ }
+ break;
+ case 40: // down
+ if (line.next) {
+ if (event.metaKey || event.ctrlKey) {
+ // [a, b, c, d] => [a, c, b, d]: A→C, C→B, B→D
+ if (prevIndex) update(dbRef(db, scriptId + '/lines/' + prevIndex), { next: line.next });
+ else update(dbRef(db, scriptId), { firstLine: line.next });
+ var nextLineData = script && script.lines && script.lines[line.next];
+ var nextIndexAfterCurrent = nextLineData && nextLineData.next;
+ update(dbRef(db, scriptId + '/lines/' + line.next), { next: index });
+ if (nextIndexAfterCurrent) update(dbRef(db, scriptId + '/lines/' + index), { next: nextIndexAfterCurrent });
+ else remove(dbRef(db, scriptId + '/lines/' + index + '/next'));
+ setTimeout(function() { focusLine(index, false); }, 0);
+ event.preventDefault();
+ } else if (cursorPos(event.target) >= event.target.textContent.length) {
+ focusLine(line.next, false);
+ event.preventDefault();
+ }
+ }
+ break;
+ case 8: // backspace
+ if (!line.text && prevIndex) {
+ if (line.next) update(dbRef(db, scriptId + '/lines/' + prevIndex), { next: line.next });
+ else remove(dbRef(db, scriptId + '/lines/' + prevIndex + '/next'));
+ remove(dbRef(db, scriptId + '/lines/' + index));
+ setTimeout(function() { focusLine(prevIndex, true); }, 0);
+ event.preventDefault();
+ }
+ break;
+ case 13: // enter
+ if (line.text) {
+ var newItem = { type: nextTypes[line.type] };
+ if (line.next) newItem.next = line.next;
+ var newLineRef = push(dbRef(db, scriptId + '/lines'), newItem);
+ set(dbRef(db, scriptId + '/lines/' + index + '/next'), newLineRef.key);
+ setTimeout(function() { focusLine(newLineRef.key, false); }, 0);
+ }
+ break;
+ }
+ }, [scriptId, script]);
+
+ // Walk the linked list to build the ordered array of line elements
+ var lineElements = [];
+ var previous = null;
+ var prevPrevious = null;
+
+ var iterateLines = function(line, index) {
+ if (!line) return;
+ if (!lineRefs.current[index]) lineRefs.current[index] = { current: null };
+ var lineRef = lineRefs.current[index];
+
+ lineElements.push(
+
+ );
+ prevPrevious = previous;
+ previous = index;
+ if (line.next && script.lines[line.next]) iterateLines(script.lines[line.next], line.next);
+ };
+
+ if (script && script.lines && script.firstLine) {
+ iterateLines(script.lines[script.firstLine], script.firstLine);
+ }
+
+ return (
+
+
+
+ {script
+ ? (lineElements.length > 0 ? lineElements : null)
+ : Loading Script... }
+
+
+ );
+}
+
+export default ScriptPage;
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 0000000..ca8bea8
--- /dev/null
+++ b/src/constants.js
@@ -0,0 +1,12 @@
+export var types = ['scene', 'action', 'character', 'dialogue', 'parenthetical', 'transition', 'shot', 'text'];
+
+export var nextTypes = {
+ scene: 'action',
+ action: 'action',
+ character: 'dialogue',
+ dialogue: 'character',
+ parenthetical: 'dialogue',
+ transition: 'scene',
+ shot: 'action',
+ text: 'text'
+};
diff --git a/src/firebase.js b/src/firebase.js
new file mode 100644
index 0000000..ee26e83
--- /dev/null
+++ b/src/firebase.js
@@ -0,0 +1,5 @@
+import { initializeApp } from 'firebase/app';
+import { getDatabase } from 'firebase/database';
+
+var firebaseApp = initializeApp({ databaseURL: 'https://screenwrite.firebaseio.com' });
+export var db = getDatabase(firebaseApp);
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..61aea4e
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,41 @@
+export function cursorPos(element) {
+ var caretOffset = 0;
+ var doc = element.ownerDocument || element.document;
+ var win = doc.defaultView || doc.parentWindow;
+ var sel;
+ if (typeof win.getSelection != "undefined") {
+ sel = win.getSelection();
+ if (sel.rangeCount > 0) {
+ var range = win.getSelection().getRangeAt(0);
+ var preCaretRange = range.cloneRange();
+ preCaretRange.selectNodeContents(element);
+ preCaretRange.setEnd(range.endContainer, range.endOffset);
+ caretOffset = preCaretRange.toString().length;
+ }
+ } else if ( (sel = doc.selection) && sel.type != "Control") {
+ var textRange = sel.createRange();
+ var preCaretTextRange = doc.body.createTextRange();
+ preCaretTextRange.moveToElementText(element);
+ preCaretTextRange.setEndPoint("EndToEnd", textRange);
+ caretOffset = preCaretTextRange.text.length;
+ }
+ return caretOffset;
+}
+
+export function placeCaretAtEnd(el) {
+ el.focus();
+ if (typeof window.getSelection != "undefined"
+ && typeof document.createRange != "undefined") {
+ var range = document.createRange();
+ range.selectNodeContents(el);
+ range.collapse(false);
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ } else if (typeof document.body.createTextRange != "undefined") {
+ var textRange = document.body.createTextRange();
+ textRange.moveToElementText(el);
+ textRange.collapse(false);
+ textRange.select();
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index adebdef..7fb9627 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -18,22 +18,6 @@
"@csstools/css-tokenizer" "^3.0.3"
lru-cache "^10.4.3"
-"@babel/cli@^7.28.6":
- version "7.28.6"
- resolved "https://registry.npmjs.org/@babel/cli/-/cli-7.28.6.tgz"
- integrity sha512-6EUNcuBbNkj08Oj4gAZ+BUU8yLCgKzgVX4gaTh09Ya2C8ICM4P+G30g4m3akRxSYAp3A/gnWchrNst7px4/nUQ==
- dependencies:
- "@jridgewell/trace-mapping" "^0.3.28"
- commander "^6.2.0"
- convert-source-map "^2.0.0"
- fs-readdir-recursive "^1.1.0"
- glob "^7.2.0"
- make-dir "^2.1.0"
- slash "^2.0.0"
- optionalDependencies:
- "@nicolo-ribaudo/chokidar-2" "2.1.8-no-fsevents.3"
- chokidar "^3.6.0"
-
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.27.1", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0":
version "7.29.0"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz"
@@ -1011,6 +995,11 @@
resolved "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz"
integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==
+"@esbuild/linux-x64@0.27.4":
+ version "0.27.4"
+ resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz"
+ integrity sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==
+
"@firebase/ai@2.8.0":
version "2.8.0"
resolved "https://registry.npmjs.org/@firebase/ai/-/ai-2.8.0.tgz"
@@ -1727,16 +1716,16 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
-"@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3":
- version "2.1.8-no-fsevents.3"
- resolved "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz"
- integrity sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==
-
"@parcel/watcher-linux-x64-glibc@2.5.6":
version "2.5.6"
resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz"
integrity sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==
+"@parcel/watcher-linux-x64-musl@2.5.6":
+ version "2.5.6"
+ resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz"
+ integrity sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==
+
"@parcel/watcher@^2.4.1":
version "2.5.6"
resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz"
@@ -1981,6 +1970,11 @@
resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz"
integrity sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==
+"@unrs/resolver-binding-linux-x64-musl@1.11.1":
+ version "1.11.1"
+ resolved "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz"
+ integrity sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==
+
accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz"
@@ -2328,10 +2322,10 @@ body-parser@~1.8.0:
raw-body "1.3.0"
type-is "~1.5.1"
-bootstrap@^5.3.8:
- version "5.3.8"
- resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz"
- integrity sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==
+bootstrap@^3.4.1:
+ version "3.4.1"
+ resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz"
+ integrity sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==
brace-expansion@^1.1.7:
version "1.1.12"
@@ -2459,7 +2453,7 @@ char-regex@^1.0.2:
resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
-chokidar@^3.5.3, chokidar@^3.6.0:
+chokidar@^3.5.3:
version "3.6.0"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
@@ -2546,11 +2540,6 @@ color-support@^1.1.3:
resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
-commander@^6.2.0:
- version "6.2.1"
- resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz"
- integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
-
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
@@ -2814,6 +2803,46 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
+esbuild-sass-plugin@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-3.7.0.tgz"
+ integrity sha512-vxNSXFx3/0ZFApKo9036ek2iRfsT+yVO99qIYqa+JaDSuJuId2/N4s1TY+xfK+5LRpAMQkfdBVUTxb/1r2bq1A==
+ dependencies:
+ resolve "^1.22.11"
+ sass "^1.97.3"
+
+esbuild@^0.27.4:
+ version "0.27.4"
+ resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz"
+ integrity sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.27.4"
+ "@esbuild/android-arm" "0.27.4"
+ "@esbuild/android-arm64" "0.27.4"
+ "@esbuild/android-x64" "0.27.4"
+ "@esbuild/darwin-arm64" "0.27.4"
+ "@esbuild/darwin-x64" "0.27.4"
+ "@esbuild/freebsd-arm64" "0.27.4"
+ "@esbuild/freebsd-x64" "0.27.4"
+ "@esbuild/linux-arm" "0.27.4"
+ "@esbuild/linux-arm64" "0.27.4"
+ "@esbuild/linux-ia32" "0.27.4"
+ "@esbuild/linux-loong64" "0.27.4"
+ "@esbuild/linux-mips64el" "0.27.4"
+ "@esbuild/linux-ppc64" "0.27.4"
+ "@esbuild/linux-riscv64" "0.27.4"
+ "@esbuild/linux-s390x" "0.27.4"
+ "@esbuild/linux-x64" "0.27.4"
+ "@esbuild/netbsd-arm64" "0.27.4"
+ "@esbuild/netbsd-x64" "0.27.4"
+ "@esbuild/openbsd-arm64" "0.27.4"
+ "@esbuild/openbsd-x64" "0.27.4"
+ "@esbuild/openharmony-arm64" "0.27.4"
+ "@esbuild/sunos-x64" "0.27.4"
+ "@esbuild/win32-arm64" "0.27.4"
+ "@esbuild/win32-ia32" "0.27.4"
+ "@esbuild/win32-x64" "0.27.4"
+
escalade@^3.1.1, escalade@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz"
@@ -3109,11 +3138,6 @@ fs-mkdirp-stream@^2.0.1:
graceful-fs "^4.2.8"
streamx "^2.12.0"
-fs-readdir-recursive@^1.1.0:
- version "1.1.0"
- resolved "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz"
- integrity sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==
-
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
@@ -3197,7 +3221,7 @@ glob@^10.3.10:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
-glob@^7.1.4, glob@^7.2.0:
+glob@^7.1.4:
version "7.2.3"
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -4348,14 +4372,6 @@ lz-string@^1.5.0:
resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz"
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
-make-dir@^2.1.0:
- version "2.1.0"
- resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz"
- integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==
- dependencies:
- pify "^4.0.1"
- semver "^5.6.0"
-
make-dir@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz"
@@ -4798,11 +4814,6 @@ pify@^2.0.0:
resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
-pify@^4.0.1:
- version "4.0.1"
- resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz"
- integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
-
pinkie-promise@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz"
@@ -5225,11 +5236,6 @@ semver-greatest-satisfied-range@^2.0.0:
dependencies:
sver "^1.8.3"
-semver@^5.6.0:
- version "5.7.2"
- resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz"
- integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
-
semver@^6.3.0, semver@^6.3.1:
version "6.3.1"
resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
@@ -5319,7 +5325,12 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-signal-exit@^3.0.0, signal-exit@^3.0.3:
+signal-exit@^3.0.0:
+ version "3.0.7"
+ resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
+ integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
+
+signal-exit@^3.0.3:
version "3.0.7"
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
@@ -5329,11 +5340,6 @@ signal-exit@^4.0.1:
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
-slash@^2.0.0:
- version "2.0.0"
- resolved "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz"
- integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
-
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
@@ -5481,7 +5487,16 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
-string-width@^5.0.1, string-width@^5.1.2:
+string-width@^5.0.1:
+ version "5.1.2"
+ resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz"
+ integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
+ dependencies:
+ eastasianwidth "^0.2.0"
+ emoji-regex "^9.2.2"
+ strip-ansi "^7.0.1"
+
+string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz"
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==