diff --git a/changelog.md b/changelog.md index 7cfbfde8a..1e1ac70e2 100644 --- a/changelog.md +++ b/changelog.md @@ -83,12 +83,40 @@ pre { .page .exampleTable td,th { border:1px dashed #00000030; } + +.page .df { + font-size: 2em; + vertical-align: middle; +} ``` ## changelog For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery). -### Friday 11/14/2025 - v13.20.0 +### Friday 1/11/2026 - v3.20.1 + +{{taskList +##### calculuschild +* [x] Add D100 "ball" dice icons `:d100:` :df_d100_05: + +##### G-Ambatte +* [x] Fix transparent edge on back cover image + +Fixes issue [#4551](https://github.com/naturalcrit/homebrewery/issues/4551) + +* [x] Fix "Out of sync" error when document contains extended unicode characters + +Fixes issue [#4583](https://github.com/naturalcrit/homebrewery/issues/4583) + +##### 5e-Cleric +* [x] Fix page count error on Vault + +* [x] Fix cover page footnote set to all-caps + +Fixes issue [#4559](https://github.com/naturalcrit/homebrewery/issues/4559) +}} + +### Friday 11/14/2025 - v3.20.0 {{taskList ##### calculuschild diff --git a/client/admin/admin.jsx b/client/admin/admin.jsx index 787c2a3eb..a8ee10562 100644 --- a/client/admin/admin.jsx +++ b/client/admin/admin.jsx @@ -1,7 +1,7 @@ import './admin.less'; import React, { useEffect, useState } from 'react'; -const BrewUtils = require('./brewUtils/brewUtils.jsx'); -const NotificationUtils = require('./notificationUtils/notificationUtils.jsx'); +import BrewUtils from './brewUtils/brewUtils.jsx'; +import NotificationUtils from './notificationUtils/notificationUtils.jsx'; import AuthorUtils from './authorUtils/authorUtils.jsx'; import LockTools from './lockTools/lockTools.jsx'; @@ -49,4 +49,4 @@ const Admin = ()=>{ ); }; -module.exports = Admin; +export default Admin; diff --git a/client/admin/admin.less b/client/admin/admin.less index 0fc353194..432f92e8b 100644 --- a/client/admin/admin.less +++ b/client/admin/admin.less @@ -1,11 +1,9 @@ -@import 'naturalcrit/styles/reset.less'; -@import 'naturalcrit/styles/elements.less'; -@import 'naturalcrit/styles/animations.less'; -@import 'naturalcrit/styles/colors.less'; -@import 'naturalcrit/styles/tooltip.less'; -@import './themes/fonts/iconFonts/fontAwesome.less'; - -@import 'font-awesome/css/font-awesome.css'; +@import '@sharedStyles/reset.less'; +@import '@sharedStyles/elements.less'; +@import '@sharedStyles/animations.less'; +@import '@sharedStyles/colors.less'; +@import '@sharedStyles/tooltip.less'; +@import '@themes/fonts/iconFonts/fontAwesome.less'; html,body, #reactContainer, .naturalCrit { min-height : 100%; } diff --git a/client/admin/authorUtils/authorLookup/authorLookup.jsx b/client/admin/authorUtils/authorLookup/authorLookup.jsx index abdece6f7..cdfe6274b 100644 --- a/client/admin/authorUtils/authorLookup/authorLookup.jsx +++ b/client/admin/authorUtils/authorLookup/authorLookup.jsx @@ -84,4 +84,4 @@ const authorLookup = ()=>{ ); }; -module.exports = authorLookup; +export default authorLookup; diff --git a/client/admin/authorUtils/authorUtils.jsx b/client/admin/authorUtils/authorUtils.jsx index a96eea528..ef717a1ec 100644 --- a/client/admin/authorUtils/authorUtils.jsx +++ b/client/admin/authorUtils/authorUtils.jsx @@ -10,4 +10,4 @@ const authorUtils = ()=>{ ); }; -module.exports = authorUtils; \ No newline at end of file +export default authorUtils; \ No newline at end of file diff --git a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx index d4b17c570..6cec01178 100644 --- a/client/admin/brewUtils/brewCleanup/brewCleanup.jsx +++ b/client/admin/brewUtils/brewCleanup/brewCleanup.jsx @@ -1,9 +1,8 @@ -const React = require('react'); -const createClass = require('create-react-class'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; -const request = require('superagent'); - -const BrewCleanup = createClass({ +const BrewCleanup = createReactClass({ displayName : 'BrewCleanup', getDefaultProps(){ return {}; @@ -69,4 +68,4 @@ const BrewCleanup = createClass({ } }); -module.exports = BrewCleanup; +export default BrewCleanup; diff --git a/client/admin/brewUtils/brewCompress/brewCompress.jsx b/client/admin/brewUtils/brewCompress/brewCompress.jsx index ccb59e027..181315fce 100644 --- a/client/admin/brewUtils/brewCompress/brewCompress.jsx +++ b/client/admin/brewUtils/brewCompress/brewCompress.jsx @@ -1,8 +1,8 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const request = require('superagent'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; -const BrewCompress = createClass({ +const BrewCompress = createReactClass({ displayName : 'BrewCompress', getDefaultProps(){ return {}; @@ -85,4 +85,4 @@ const BrewCompress = createClass({ } }); -module.exports = BrewCompress; +export default BrewCompress; diff --git a/client/admin/brewUtils/brewLookup/brewLookup.jsx b/client/admin/brewUtils/brewLookup/brewLookup.jsx index fb780f29e..6b725198c 100644 --- a/client/admin/brewUtils/brewLookup/brewLookup.jsx +++ b/client/admin/brewUtils/brewLookup/brewLookup.jsx @@ -1,12 +1,11 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const cx = require('classnames'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; +import cx from 'classnames'; -const request = require('superagent'); -const Moment = require('moment'); +import Moment from 'moment'; - -const BrewLookup = createClass({ +const BrewLookup = createReactClass({ getDefaultProps() { return {}; }, @@ -110,4 +109,4 @@ const BrewLookup = createClass({ } }); -module.exports = BrewLookup; +export default BrewLookup; diff --git a/client/admin/brewUtils/brewUtils.jsx b/client/admin/brewUtils/brewUtils.jsx index bab2cb82f..dd663ddaa 100644 --- a/client/admin/brewUtils/brewUtils.jsx +++ b/client/admin/brewUtils/brewUtils.jsx @@ -1,15 +1,14 @@ -const React = require('react'); -const createClass = require('create-react-class'); -require('./brewUtils.less'); +import React from 'react'; +import './brewUtils.less'; -const BrewCleanup = require('./brewCleanup/brewCleanup.jsx'); -const BrewLookup = require('./brewLookup/brewLookup.jsx'); -const BrewCompress = require ('./brewCompress/brewCompress.jsx'); -const Stats = require('./stats/stats.jsx'); +import BrewCleanup from './brewCleanup/brewCleanup.jsx'; +import BrewLookup from './brewLookup/brewLookup.jsx'; +import BrewCompress from './brewCompress/brewCompress.jsx'; +import Stats from './stats/stats.jsx'; -const BrewUtils = createClass({ - render : function(){ - return <> +const BrewUtils = ()=>{ + return ( + <>
@@ -17,8 +16,7 @@ const BrewUtils = createClass({
- ; - } -}); - -module.exports = BrewUtils; + + ); +}; +export default BrewUtils; diff --git a/client/admin/brewUtils/brewUtils.less b/client/admin/brewUtils/brewUtils.less index 5bbbc3f69..cfa7dbbdf 100644 --- a/client/admin/brewUtils/brewUtils.less +++ b/client/admin/brewUtils/brewUtils.less @@ -1,3 +1,5 @@ +@import '@sharedStyles/colors.less'; + .brewUtil { .result { margin-top : 20px; diff --git a/client/admin/brewUtils/stats/stats.jsx b/client/admin/brewUtils/stats/stats.jsx index 7f96618f9..b05d3afd5 100644 --- a/client/admin/brewUtils/stats/stats.jsx +++ b/client/admin/brewUtils/stats/stats.jsx @@ -1,9 +1,8 @@ -const React = require('react'); -const createClass = require('create-react-class'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; -const request = require('superagent'); - -const Stats = createClass({ +const Stats = createReactClass({ displayName : 'Stats', getDefaultProps(){ return {}; @@ -43,4 +42,4 @@ const Stats = createClass({ } }); -module.exports = Stats; +export default Stats; diff --git a/client/admin/lockTools/lockTools.jsx b/client/admin/lockTools/lockTools.jsx index 9a28d330f..8144d5d11 100644 --- a/client/admin/lockTools/lockTools.jsx +++ b/client/admin/lockTools/lockTools.jsx @@ -1,11 +1,11 @@ /*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/ -require('./lockTools.less'); -const React = require('react'); -const createClass = require('create-react-class'); +import './lockTools.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; import request from '../../homebrew/utils/request-middleware.js'; -const LockTools = createClass({ +const LockTools = createReactClass({ displayName : 'LockTools', getInitialState : function() { return { @@ -55,7 +55,7 @@ const LockTools = createClass({ } }); -const LockBrew = createClass({ +const LockBrew = createReactClass({ displayName : 'LockBrew', getInitialState : function() { // Default values @@ -183,7 +183,7 @@ const LockBrew = createClass({ } }); -const LockTable = createClass({ +const LockTable = createReactClass({ displayName : 'LockTable', getDefaultProps : function() { return { @@ -273,7 +273,7 @@ const LockTable = createClass({ } }); -const LockLookup = createClass({ +const LockLookup = createReactClass({ displayName : 'LockLookup', getDefaultProps : function() { return { @@ -339,4 +339,4 @@ const LockLookup = createClass({ } }); -module.exports = LockTools; \ No newline at end of file +export default LockTools; \ No newline at end of file diff --git a/client/admin/main.jsx b/client/admin/main.jsx new file mode 100644 index 000000000..a5b7c84ad --- /dev/null +++ b/client/admin/main.jsx @@ -0,0 +1,6 @@ +import { createRoot } from 'react-dom/client'; +import Admin from './admin.jsx'; + +const props = window.__INITIAL_PROPS__ || {}; + +createRoot(document.getElementById('reactRoot')).render(); diff --git a/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx b/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx index 0cca1047e..3a64d4bf7 100644 --- a/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx +++ b/client/admin/notificationUtils/notificationAdd/notificationAdd.jsx @@ -1,7 +1,6 @@ -require('./notificationAdd.less'); -const React = require('react'); -const { useState, useRef } = require('react'); -const request = require('superagent'); +import './notificationAdd.less'; +import React, { useState, useRef } from 'react'; +import request from 'superagent'; const NotificationAdd = ()=>{ const [notificationResult, setNotificationResult] = useState(null); @@ -106,4 +105,4 @@ const NotificationAdd = ()=>{ ); }; -module.exports = NotificationAdd; +export default NotificationAdd; diff --git a/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx b/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx index 05f81b776..063b3e908 100644 --- a/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx +++ b/client/admin/notificationUtils/notificationLookup/notificationLookup.jsx @@ -1,9 +1,7 @@ -require('./notificationLookup.less'); - -const React = require('react'); -const { useState } = require('react'); -const request = require('superagent'); -const Moment = require('moment'); +import './notificationLookup.less'; +import React, { useState } from 'react'; +import request from 'superagent'; +import Moment from 'moment'; const NotificationDetail = ({ notification, onDelete })=>( <> @@ -102,4 +100,4 @@ const NotificationLookup = ()=>{ ); }; -module.exports = NotificationLookup; +export default NotificationLookup; diff --git a/client/admin/notificationUtils/notificationUtils.jsx b/client/admin/notificationUtils/notificationUtils.jsx index 22ea21328..00db44a37 100644 --- a/client/admin/notificationUtils/notificationUtils.jsx +++ b/client/admin/notificationUtils/notificationUtils.jsx @@ -1,7 +1,6 @@ -const React = require('react'); - -const NotificationLookup = require('./notificationLookup/notificationLookup.jsx'); -const NotificationAdd = require('./notificationAdd/notificationAdd.jsx'); +import React from 'react'; +import NotificationLookup from './notificationLookup/notificationLookup.jsx'; +import NotificationAdd from './notificationAdd/notificationAdd.jsx'; const NotificationUtils = ()=>{ return ( @@ -12,4 +11,4 @@ const NotificationUtils = ()=>{ ); }; -module.exports = NotificationUtils; +export default NotificationUtils; diff --git a/client/components/codeEditor/autocompleteEmoji.js b/client/components/codeEditor/autocompleteEmoji.js index ae7f2628f..fc64e7bbd 100644 --- a/client/components/codeEditor/autocompleteEmoji.js +++ b/client/components/codeEditor/autocompleteEmoji.js @@ -1,7 +1,7 @@ -import diceFont from 'themes/fonts/iconFonts/diceFont.js'; -import elderberryInn from 'themes/fonts/iconFonts/elderberryInn.js'; -import fontAwesome from 'themes/fonts/iconFonts/fontAwesome.js'; -import gameIcons from 'themes/fonts/iconFonts/gameIcons.js'; +import diceFont from '@themes/fonts/iconFonts/diceFont.js'; +import elderberryInn from '@themes/fonts/iconFonts/elderberryInn.js'; +import fontAwesome from '@themes/fonts/iconFonts/fontAwesome.js'; +import gameIcons from '@themes/fonts/iconFonts/gameIcons.js'; const emojis = { ...diceFont, @@ -79,6 +79,6 @@ const showAutocompleteEmoji = function(CodeMirror, editor) { }); }; -module.exports = { +export default { showAutocompleteEmoji }; \ No newline at end of file diff --git a/client/components/codeEditor/close-tag.js b/client/components/codeEditor/close-tag.js index 728b63a5c..84cf62169 100644 --- a/client/components/codeEditor/close-tag.js +++ b/client/components/codeEditor/close-tag.js @@ -38,11 +38,11 @@ const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) { } }; -module.exports = { +export default { autoCloseCurlyBraces : function(CodeMirror, codeMirror) { const map = { name: 'autoCloseCurlyBraces' }; map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); }; map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); }; - codeMirror.addKeyMap(map); + codeMirror?.addKeyMap(map); } }; \ No newline at end of file diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx index fb69b6dcf..cd140ad07 100644 --- a/client/components/codeEditor/codeEditor.jsx +++ b/client/components/codeEditor/codeEditor.jsx @@ -1,55 +1,18 @@ /* eslint-disable max-lines */ -require('./codeEditor.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); -const closeTag = require('./close-tag'); -const autoCompleteEmoji = require('./autocompleteEmoji'); - +import './codeEditor.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; +import closeTag from './close-tag'; +import autoCompleteEmoji from './autocompleteEmoji'; let CodeMirror; -if(typeof window !== 'undefined'){ - CodeMirror = require('codemirror'); - //Language Modes - require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown - require('codemirror/mode/css/css.js'); - require('codemirror/mode/javascript/javascript.js'); - - //Addons - //Code folding - require('codemirror/addon/fold/foldcode.js'); - require('codemirror/addon/fold/foldgutter.js'); - //Search and replace - require('codemirror/addon/search/search.js'); - require('codemirror/addon/search/searchcursor.js'); - require('codemirror/addon/search/jump-to-line.js'); - require('codemirror/addon/search/match-highlighter.js'); - require('codemirror/addon/search/matchesonscrollbar.js'); - require('codemirror/addon/dialog/dialog.js'); - //Trailing space highlighting - // require('codemirror/addon/edit/trailingspace.js'); - //Active line highlighting - // require('codemirror/addon/selection/active-line.js'); - //Scroll past last line - require('codemirror/addon/scroll/scrollpastend.js'); - //Auto-closing - //XML code folding is a requirement of the auto-closing tag feature and is not enabled - require('codemirror/addon/fold/xml-fold.js'); - require('codemirror/addon/edit/closetag.js'); - //Autocompletion - require('codemirror/addon/hint/show-hint.js'); - - const foldPagesCode = require('./fold-pages'); - foldPagesCode.registerHomebreweryHelper(CodeMirror); - const foldCSSCode = require('./fold-css'); - foldCSSCode.registerHomebreweryHelper(CodeMirror); -} - -const CodeEditor = createClass({ +const CodeEditor = createReactClass({ displayName : 'CodeEditor', getDefaultProps : function() { return { language : '', + tab : 'brewText', value : '', wrap : true, onChange : ()=>{}, @@ -66,23 +29,54 @@ const CodeEditor = createClass({ editor : React.createRef(null), - componentDidMount : function() { + async componentDidMount() { + CodeMirror = (await import('codemirror')).default; + this.CodeMirror = CodeMirror; + + await import('codemirror/mode/gfm/gfm.js'); + await import('codemirror/mode/css/css.js'); + await import('codemirror/mode/javascript/javascript.js'); + + // addons + await import('codemirror/addon/fold/foldcode.js'); + await import('codemirror/addon/fold/foldgutter.js'); + await import('codemirror/addon/fold/xml-fold.js'); + await import('codemirror/addon/search/search.js'); + await import('codemirror/addon/search/searchcursor.js'); + await import('codemirror/addon/search/jump-to-line.js'); + await import('codemirror/addon/search/match-highlighter.js'); + await import('codemirror/addon/search/matchesonscrollbar.js'); + await import('codemirror/addon/dialog/dialog.js'); + await import('codemirror/addon/scroll/scrollpastend.js'); + await import('codemirror/addon/edit/closetag.js'); + await import('codemirror/addon/hint/show-hint.js'); + // import 'codemirror/addon/selection/active-line.js'; + // import 'codemirror/addon/edit/trailingspace.js'; + + + // register helpers dynamically as well + const foldPagesCode = (await import('./fold-pages')).default; + const foldCSSCode = (await import('./fold-css')).default; + foldPagesCode.registerHomebreweryHelper(CodeMirror); + foldCSSCode.registerHomebreweryHelper(CodeMirror); + this.buildEditor(); - const newDoc = CodeMirror.Doc(this.props.value, this.props.language); - this.codeMirror.swapDoc(newDoc); + const newDoc = CodeMirror?.Doc(this.props.value, this.props.language); + this.codeMirror?.swapDoc(newDoc); }, + componentDidUpdate : function(prevProps) { if(prevProps.view !== this.props.view){ //view changed; swap documents let newDoc; if(!this.state.docs[this.props.view]) { - newDoc = CodeMirror.Doc(this.props.value, this.props.language); + newDoc = CodeMirror?.Doc(this.props.value, this.props.language); } else { newDoc = this.state.docs[this.props.view]; } - const oldDoc = { [prevProps.view]: this.codeMirror.swapDoc(newDoc) }; + const oldDoc = { [prevProps.view]: this.codeMirror?.swapDoc(newDoc) }; this.setState((prevState)=>({ docs : _.merge({}, prevState.docs, oldDoc) @@ -90,17 +84,17 @@ const CodeEditor = createClass({ this.props.rerenderParent(); } else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside - this.codeMirror.setValue(this.props.value); + this.codeMirror?.setValue(this.props.value); } if(this.props.enableFolding) { - this.codeMirror.setOption('foldOptions', this.foldOptions(this.codeMirror)); + this.codeMirror?.setOption('foldOptions', this.foldOptions(this.codeMirror)); } else { - this.codeMirror.setOption('foldOptions', false); + this.codeMirror?.setOption('foldOptions', false); } if(prevProps.editorTheme !== this.props.editorTheme){ - this.codeMirror.setOption('theme', this.props.editorTheme); + this.codeMirror?.setOption('theme', this.props.editorTheme); } }, @@ -188,11 +182,27 @@ const CodeEditor = createClass({ closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror); autoCompleteEmoji.showAutocompleteEmoji(CodeMirror, this.codeMirror); - // Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works. - this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());}); + // Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror?. Either one works. + this.codeMirror?.on('change', (cm)=>{this.props.onChange(cm.getValue());}); this.updateSize(); }, + // Use for GFM tabs that use common hot-keys + isGFM : function() { + if((this.isGFM()) || (this.props.tab === 'brewSnippets')) return true; + return false; + }, + + isBrewText : function() { + if(this.isGFM()) return true; + return false; + }, + + isBrewSnippets : function() { + if(this.props.tab === 'brewSnippets') return true; + return false; + }, + indent : function () { const cm = this.codeMirror; if(cm.somethingSelected()) { @@ -203,84 +213,94 @@ const CodeEditor = createClass({ }, dedent : function () { - this.codeMirror.execCommand('indentLess'); + this.codeMirror?.execCommand('indentLess'); }, makeHeader : function (number) { - const selection = this.codeMirror.getSelection(); + if(!this.isGFM()) return; + const selection = this.codeMirror?.getSelection(); const header = Array(number).fill('#').join(''); - this.codeMirror.replaceSelection(`${header} ${selection}`, 'around'); - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 }); + this.codeMirror?.replaceSelection(`${header} ${selection}`, 'around'); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 }); }, makeBold : function() { - const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**'; - this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around'); + if(!this.isGFM()) return; + const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**'; + this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); } }, makeItalic : function() { + if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*'; - this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); } }, makeSuper : function() { + if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^'; - this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(1, -1) : `^${selection}^`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 1 }); } }, makeSub : function() { + if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^'; - this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `^^${selection}^^`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); } }, makeNbsp : function() { - this.codeMirror.replaceSelection(' ', 'end'); + if(!this.isGFM()) return; + this.codeMirror?.replaceSelection(' ', 'end'); }, makeSpace : function() { - const selection = this.codeMirror.getSelection(); + if(!this.isGFM()) return; + const selection = this.codeMirror?.getSelection(); const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; if(t){ const percent = parseInt(selection.slice(8, -4)) + 10; - this.codeMirror.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around'); + this.codeMirror?.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around'); } else { - this.codeMirror.replaceSelection(`{{width:10% }}`, 'around'); + this.codeMirror?.replaceSelection(`{{width:10% }}`, 'around'); } }, removeSpace : function() { - const selection = this.codeMirror.getSelection(); + if(!this.isGFM()) return; + const selection = this.codeMirror?.getSelection(); const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; if(t){ const percent = parseInt(selection.slice(8, -4)) - 10; - this.codeMirror.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around'); + this.codeMirror?.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around'); } }, newColumn : function() { - this.codeMirror.replaceSelection('\n\\column\n\n', 'end'); + if(!this.isGFM()) return; + this.codeMirror?.replaceSelection('\n\\column\n\n', 'end'); }, newPage : function() { - this.codeMirror.replaceSelection('\n\\page\n\n', 'end'); + if(!this.isGFM()) return; + this.codeMirror?.replaceSelection('\n\\page\n\n', 'end'); }, injectText : function(injectText, overwrite=true) { @@ -293,29 +313,32 @@ const CodeEditor = createClass({ }, makeUnderline : function() { + if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '' && selection.slice(-4) === ''; - this.codeMirror.replaceSelection(t ? selection.slice(3, -4) : `${selection}`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(3, -4) : `${selection}`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 4 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 4 }); } }, makeSpan : function() { + if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; - this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - 2 }); } }, makeDiv : function() { + if(!this.isGFM()) return; const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; - this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around'); + this.codeMirror?.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection } }, @@ -323,8 +346,8 @@ const CodeEditor = createClass({ let regex; let cursorPos; let newComment; - const selection = this.codeMirror.getSelection(); - if(this.props.language === 'gfm'){ + const selection = this.codeMirror?.getSelection(); + if(this.isGFM()){ regex = /^\s*()\s*$/gs; cursorPos = 4; newComment = ``; @@ -333,44 +356,46 @@ const CodeEditor = createClass({ cursorPos = 3; newComment = `/* ${selection} */`; } - this.codeMirror.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around'); + this.codeMirror?.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around'); if(selection.length === 0){ - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos }); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos }); }; }, makeLink : function() { + if(!this.isGFM()) return; const isLink = /^\[(.*)\]\((.*)\)$/; - const selection = this.codeMirror.getSelection().trim(); + const selection = this.codeMirror?.getSelection().trim(); let match; if(match = isLink.exec(selection)){ const altText = match[1]; const url = match[2]; - this.codeMirror.replaceSelection(`${altText} ${url}`); - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch }); + this.codeMirror?.replaceSelection(`${altText} ${url}`); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch }); } else { - this.codeMirror.replaceSelection(`[${selection || 'alt text'}](url)`); - const cursor = this.codeMirror.getCursor(); - this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 }); + this.codeMirror?.replaceSelection(`[${selection || 'alt text'}](url)`); + const cursor = this.codeMirror?.getCursor(); + this.codeMirror?.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 }); } }, makeList : function(listType) { + if(!this.isGFM()) return; const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to'); - this.codeMirror.setSelection( + this.codeMirror?.setSelection( { line: selectionStart.line, ch: 0 }, - { line: selectionEnd.line, ch: this.codeMirror.getLine(selectionEnd.line).length } + { line: selectionEnd.line, ch: this.codeMirror?.getLine(selectionEnd.line).length } ); - const newSelection = this.codeMirror.getSelection(); + const newSelection = this.codeMirror?.getSelection(); const regex = /^\d+\.\s|^-\s/gm; if(newSelection.match(regex) != null){ // if selection IS A LIST - this.codeMirror.replaceSelection(newSelection.replace(regex, ''), 'around'); + this.codeMirror?.replaceSelection(newSelection.replace(regex, ''), 'around'); } else { // if selection IS NOT A LIST - listType == 'UL' ? this.codeMirror.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') : - this.codeMirror.replaceSelection(newSelection.replace(/^/gm, (()=>{ + listType == 'UL' ? this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') : + this.codeMirror?.replaceSelection(newSelection.replace(/^/gm, (()=>{ let n = 1; return ()=>{ return `${n++}. `; @@ -380,39 +405,39 @@ const CodeEditor = createClass({ }, foldAllCode : function() { - this.codeMirror.execCommand('foldAll'); + this.codeMirror?.execCommand('foldAll'); }, unfoldAllCode : function() { - this.codeMirror.execCommand('unfoldAll'); + this.codeMirror?.execCommand('unfoldAll'); }, //=-- Externally used -==// setCursorPosition : function(line, char){ setTimeout(()=>{ - this.codeMirror.focus(); - this.codeMirror.doc.setCursor(line, char); + this.codeMirror?.focus(); + this.codeMirror?.doc.setCursor(line, char); }, 10); }, getCursorPosition : function(){ - return this.codeMirror.getCursor(); + return this.codeMirror?.getCursor(); }, getTopVisibleLine : function(){ - const rect = this.codeMirror.getWrapperElement().getBoundingClientRect(); - const topVisibleLine = this.codeMirror.lineAtHeight(rect.top, 'window'); + const rect = this.codeMirror?.getWrapperElement().getBoundingClientRect(); + const topVisibleLine = this.codeMirror?.lineAtHeight(rect.top, 'window'); return topVisibleLine; }, updateSize : function(){ - this.codeMirror.refresh(); + this.codeMirror?.refresh(); }, redo : function(){ - return this.codeMirror.redo(); + return this.codeMirror?.redo(); }, undo : function(){ - return this.codeMirror.undo(); + return this.codeMirror?.undo(); }, historySize : function(){ - return this.codeMirror.doc.historySize(); + return this.codeMirror?.doc.historySize(); }, foldOptions : function(cm){ @@ -426,7 +451,7 @@ const CodeEditor = createClass({ let foldPreviewText = ''; while (currentLine <= to.line && text.length <= maxLength) { - const currentText = this.codeMirror.getLine(currentLine); + const currentText = this.codeMirror?.getLine(currentLine); currentLine++; if(currentText[0] == '#'){ foldPreviewText = currentText; @@ -461,5 +486,5 @@ const CodeEditor = createClass({ } }); -module.exports = CodeEditor; +export default CodeEditor; diff --git a/client/components/codeEditor/codeEditor.less b/client/components/codeEditor/codeEditor.less index c8e60974b..89d0c9497 100644 --- a/client/components/codeEditor/codeEditor.less +++ b/client/components/codeEditor/codeEditor.less @@ -5,10 +5,10 @@ @import (less) 'codemirror/addon/hint/show-hint.css'; //Icon fonts included so they can appear in emoji autosuggest dropdown -@import (less) './themes/fonts/iconFonts/diceFont.less'; -@import (less) './themes/fonts/iconFonts/elderberryInn.less'; -@import (less) './themes/fonts/iconFonts/gameIcons.less'; -@import (less) './themes/fonts/iconFonts/fontAwesome.less'; +@import (less) '@themes/fonts/iconFonts/diceFont.less'; +@import (less) '@themes/fonts/iconFonts/elderberryInn.less'; +@import (less) '@themes/fonts/iconFonts/gameIcons.less'; +@import (less) '@themes/fonts/iconFonts/fontAwesome.less'; @keyframes sourceMoveAnimation { 50% { color : white;background-color : red;} diff --git a/client/components/codeEditor/fold-css.js b/client/components/codeEditor/fold-css.js index 338cab176..06bfd96a4 100644 --- a/client/components/codeEditor/fold-css.js +++ b/client/components/codeEditor/fold-css.js @@ -1,4 +1,4 @@ -module.exports = { +export default { registerHomebreweryHelper : function(CodeMirror) { CodeMirror.registerHelper('fold', 'homebrewerycss', function(cm, start) { diff --git a/client/components/codeEditor/fold-pages.js b/client/components/codeEditor/fold-pages.js index cff1c64e0..1d8d19f6b 100644 --- a/client/components/codeEditor/fold-pages.js +++ b/client/components/codeEditor/fold-pages.js @@ -1,4 +1,4 @@ -module.exports = { +export default { registerHomebreweryHelper : function(CodeMirror) { CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) { const matcher = /^\\page.*/; diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index ae9f1d7f8..7c015d3d6 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -1,9 +1,9 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); -require('./combobox.less'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; +import './combobox.less'; -const Combobox = createClass({ +const Combobox = createReactClass({ displayName : 'Combobox', getDefaultProps : function() { return { @@ -11,14 +11,17 @@ const Combobox = createClass({ trigger : 'hover', default : '', placeholder : '', + tooltip : '', autoSuggest : { clearAutoSuggestOnClick : true, suggestMethod : 'includes', filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter }, + valuePatterns : /.+/ }; }, getInitialState : function() { + this.dropdownRef = React.createRef(); return { showDropdown : false, value : '', @@ -39,7 +42,7 @@ const Combobox = createClass({ }, handleClickOutside : function(e){ // Close dropdown when clicked outside - if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) { + if(this.dropdownRef.current && !this.dropdownRef.current.contains(e.target)) { this.handleDropdown(false); } }, @@ -69,11 +72,14 @@ const Combobox = createClass({ return (
{this.handleDropdown(true);} : undefined} - onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}> + onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined} + {...(this.props.tooltip ? { 'data-tooltip-right': this.props.tooltip } : {})}> this.handleInput(e)} value={this.state.value || ''} + title='' + pattern={this.props.valuePatterns} placeholder={this.props.placeholder} onBlur={(e)=>{ if(!e.target.checkValidity()){ @@ -82,6 +88,12 @@ const Combobox = createClass({ }); } }} + onKeyDown={(e)=>{ + if(e.key === 'Enter') { + e.preventDefault(); + this.props.onEntry(e); + } + }} />
@@ -117,7 +129,7 @@ const Combobox = createClass({ }); return (
{this.handleDropdown(false);} : undefined}> {this.renderTextInput()} {this.renderDropdown(dropdownChildren)} @@ -126,4 +138,4 @@ const Combobox = createClass({ } }); -module.exports = Combobox; +export default Combobox; diff --git a/client/components/combobox.less b/client/components/combobox.less index 27f78356b..a3c7527f2 100644 --- a/client/components/combobox.less +++ b/client/components/combobox.less @@ -10,6 +10,7 @@ position : absolute; z-index : 100; width : 100%; + height : max-content; max-height : 200px; overflow-y : auto; background-color : white; diff --git a/client/components/renderWarnings/renderWarnings.jsx b/client/components/renderWarnings/renderWarnings.jsx index 6b8ea8184..75fdb8ff4 100644 --- a/client/components/renderWarnings/renderWarnings.jsx +++ b/client/components/renderWarnings/renderWarnings.jsx @@ -1,11 +1,11 @@ -require('./renderWarnings.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); +import './renderWarnings.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; import Dialog from '../dialog.jsx'; -const RenderWarnings = createClass({ +const RenderWarnings = createReactClass({ displayName : 'RenderWarnings', getInitialState : function() { return { @@ -57,4 +57,4 @@ const RenderWarnings = createClass({ } }); -module.exports = RenderWarnings; +export default RenderWarnings; diff --git a/client/components/renderWarnings/renderWarnings.less b/client/components/renderWarnings/renderWarnings.less index 70799092a..a1413709b 100644 --- a/client/components/renderWarnings/renderWarnings.less +++ b/client/components/renderWarnings/renderWarnings.less @@ -1,3 +1,5 @@ +@import '@sharedStyles/colors.less'; + .renderWarnings { position : relative; float : right; diff --git a/client/components/splitPane/splitPane.jsx b/client/components/splitPane/splitPane.jsx index 78ba59ed3..7cbfe2066 100644 --- a/client/components/splitPane/splitPane.jsx +++ b/client/components/splitPane/splitPane.jsx @@ -1,6 +1,5 @@ -require('./splitPane.less'); -const React = require('react'); -const { useState, useEffect } = React; +import './splitPane.less'; +import React, { useEffect, useState } from 'react'; const PANE_WIDTH_KEY = 'HB_editor_splitWidth'; const LIVE_SCROLL_KEY = 'HB_editor_liveScroll'; @@ -108,4 +107,4 @@ const Pane = ({ width, children, isDragging, moveBrew, moveSource, liveScroll, s ); }; -module.exports = SplitPane; +export default SplitPane; diff --git a/client/components/splitPane/splitPane.less b/client/components/splitPane/splitPane.less index 80a8695af..779d0c222 100644 --- a/client/components/splitPane/splitPane.less +++ b/client/components/splitPane/splitPane.less @@ -1,3 +1,4 @@ +@import '@sharedStyles/core.less'; .splitPane { position : relative; diff --git a/client/components/svg/cauldron.svg.jsx b/client/components/svg/cauldron.svg.jsx index 49405783b..c396dd05e 100644 --- a/client/components/svg/cauldron.svg.jsx +++ b/client/components/svg/cauldron.svg.jsx @@ -1,7 +1,6 @@ -const React = require('react'); -const createClass = require('create-react-class'); +import React from 'react'; -module.exports = function(props){ +export default function(props){ return diff --git a/client/components/svg/naturalcrit-d20.svg.jsx b/client/components/svg/naturalcrit-d20.svg.jsx index 1482b5559..9109d6e91 100644 --- a/client/components/svg/naturalcrit-d20.svg.jsx +++ b/client/components/svg/naturalcrit-d20.svg.jsx @@ -1,6 +1,5 @@ -const React = require('react'); -const createClass = require('create-react-class'); +import React from 'react'; -module.exports = function(props){ +export default function(props){ return ; }; \ No newline at end of file diff --git a/client/homebrew/brewRenderer/brewRenderer.jsx b/client/homebrew/brewRenderer/brewRenderer.jsx index 7e6681c57..8e74473b3 100644 --- a/client/homebrew/brewRenderer/brewRenderer.jsx +++ b/client/homebrew/brewRenderer/brewRenderer.jsx @@ -1,23 +1,24 @@ /*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/ -require('./brewRenderer.less'); -const React = require('react'); -const { useState, useRef, useMemo, useEffect } = React; -const _ = require('lodash'); +import brewRendererStylesUrl from './brewRenderer.less?url'; +import headerNavStylesUrl from './headerNav/headerNav.less?url'; +import './brewRenderer.less'; +import React, { useState, useRef, useMemo, useEffect } from 'react'; +import _ from 'lodash'; -const MarkdownLegacy = require('markdownLegacy.js'); -import Markdown from 'markdown.js'; -const ErrorBar = require('./errorBar/errorBar.jsx'); -const ToolBar = require('./toolBar/toolBar.jsx'); +import MarkdownLegacy from '@shared/markdownLegacy.js'; +import Markdown from '@shared/markdown.js'; +import ErrorBar from './errorBar/errorBar.jsx'; +import ToolBar from './toolBar/toolBar.jsx'; //TODO: move to the brew renderer -const RenderWarnings = require('client/components/renderWarnings/renderWarnings.jsx'); -const NotificationPopup = require('./notificationPopup/notificationPopup.jsx'); -const Frame = require('react-frame-component').default; -const dedent = require('dedent-tabs').default; -const { printCurrentBrew } = require('../../../shared/helpers.js'); +import RenderWarnings from '../../components/renderWarnings/renderWarnings.jsx'; +import NotificationPopup from './notificationPopup/notificationPopup.jsx'; +import Frame from 'react-frame-component'; +import dedent from 'dedent'; +import { printCurrentBrew } from '@shared/helpers.js'; import HeaderNav from './headerNav/headerNav.jsx'; -import { safeHTML } from './safeHTML.js'; +import safeHTML from './safeHTML.js'; const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m; const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m; @@ -30,6 +31,8 @@ const INITIAL_CONTENT = dedent` + +
`; @@ -345,4 +348,4 @@ const BrewRenderer = (props)=>{ ); }; -module.exports = BrewRenderer; +export default BrewRenderer; diff --git a/client/homebrew/brewRenderer/brewRenderer.less b/client/homebrew/brewRenderer/brewRenderer.less index bb4fe69c5..ea9b43d75 100644 --- a/client/homebrew/brewRenderer/brewRenderer.less +++ b/client/homebrew/brewRenderer/brewRenderer.less @@ -1,4 +1,4 @@ -@import (multiple, less) 'shared/naturalcrit/styles/reset.less'; +@import '@sharedStyles/core.less'; .brewRenderer { height : 100vh; @@ -59,6 +59,12 @@ } &-corner { visibility : hidden; } } + + @supports (break-after:always) { + .columnSplit { + margin-bottom: 100vh; + } + } } .pane { position : relative; } @@ -81,4 +87,5 @@ } } .headerNav { visibility : hidden; } -} \ No newline at end of file + +} diff --git a/client/homebrew/brewRenderer/errorBar/errorBar.jsx b/client/homebrew/brewRenderer/errorBar/errorBar.jsx index 78b36d70c..b4522d759 100644 --- a/client/homebrew/brewRenderer/errorBar/errorBar.jsx +++ b/client/homebrew/brewRenderer/errorBar/errorBar.jsx @@ -1,5 +1,5 @@ -require('./errorBar.less'); -const React = require('react'); +import './errorBar.less'; +import React from 'react'; import Dialog from '../../../components/dialog.jsx'; @@ -50,4 +50,4 @@ const ErrorBar = (props)=>{ ); }; -module.exports = ErrorBar; +export default ErrorBar; diff --git a/client/homebrew/brewRenderer/errorBar/errorBar.less b/client/homebrew/brewRenderer/errorBar/errorBar.less index 163648533..528521a07 100644 --- a/client/homebrew/brewRenderer/errorBar/errorBar.less +++ b/client/homebrew/brewRenderer/errorBar/errorBar.less @@ -1,3 +1,4 @@ +@import '@sharedStyles/colors.less'; .errorBar { position : absolute; diff --git a/client/homebrew/brewRenderer/headerNav/headerNav.jsx b/client/homebrew/brewRenderer/headerNav/headerNav.jsx index 04ced2585..3b184aff0 100644 --- a/client/homebrew/brewRenderer/headerNav/headerNav.jsx +++ b/client/homebrew/brewRenderer/headerNav/headerNav.jsx @@ -1,7 +1,7 @@ -require('./headerNav.less'); +import './headerNav.less'; -import * as React from 'react'; -import * as _ from 'lodash'; +import React from 'react'; +import _ from 'lodash'; const MAX_TEXT_LENGTH = 40; diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx index ad8f9140a..5f4fc5608 100644 --- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx +++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.jsx @@ -1,7 +1,7 @@ -require('./notificationPopup.less'); +import './notificationPopup.less'; import React, { useEffect, useState } from 'react'; import request from '../../utils/request-middleware.js'; -import Markdown from 'markdown.js'; +import Markdown from '@shared/markdown.js'; import Dialog from '../../../components/dialog.jsx'; @@ -62,4 +62,4 @@ const NotificationPopup = ()=>{ ; }; -module.exports = NotificationPopup; +export default NotificationPopup; diff --git a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less index 85d4c8365..4f34bad8d 100644 --- a/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less +++ b/client/homebrew/brewRenderer/notificationPopup/notificationPopup.less @@ -1,3 +1,5 @@ +@import './client/homebrew/navbar/navbar.less'; + .popups { position : fixed; top : calc(@navbarHeight + @viewerToolsHeight); diff --git a/client/homebrew/brewRenderer/safeHTML.js b/client/homebrew/brewRenderer/safeHTML.js index 2574f4cfe..d9438b663 100644 --- a/client/homebrew/brewRenderer/safeHTML.js +++ b/client/homebrew/brewRenderer/safeHTML.js @@ -43,4 +43,4 @@ function safeHTML(htmlString) { return div.innerHTML; }; -module.exports.safeHTML = safeHTML; \ No newline at end of file +export default safeHTML; \ No newline at end of file diff --git a/client/homebrew/brewRenderer/toolBar/toolBar.jsx b/client/homebrew/brewRenderer/toolBar/toolBar.jsx index 4aee3b6bd..97d996633 100644 --- a/client/homebrew/brewRenderer/toolBar/toolBar.jsx +++ b/client/homebrew/brewRenderer/toolBar/toolBar.jsx @@ -1,8 +1,7 @@ /* eslint-disable max-lines */ -require('./toolBar.less'); -const React = require('react'); -const { useState, useEffect } = React; -const _ = require('lodash'); +import './toolBar.less'; +import React, { useState, useEffect } from 'react'; +import _ from 'lodash'; import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx'; @@ -100,18 +99,18 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa return (
; } else { dropdown = -
+
- Select from the list below (built-in themes and brews you have tagged "meta:theme"), or paste in the Share URL or Share ID of any brew.
; } @@ -281,7 +256,7 @@ const MetadataEditor = createClass({ return
-
+
- Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.
; }, renderRenderOptions : function(){ - return
+ return
- this.handleFieldChange('tags', e)} - /> - -
- -
- {this.renderSystems()} +
+ +
+ this.handleFieldChange('tags', e)} + tooltip='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.' + />
+ {this.renderLanguageDropdown()} {this.renderThemeDropdown()} @@ -387,13 +363,22 @@ const MetadataEditor = createClass({ {this.renderAuthors()} - !this.props.metadata.authors?.includes(v)]} - placeholder='invite author' unique={true} - values={this.props.metadata.invitedAuthors} - notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']} - onChange={(e)=>this.handleFieldChange('invitedAuthors', e)} - /> +
+ +
+ !this.props.metadata.authors?.includes(v)]} + placeholder='invite author' unique={true} + tooltip={`Invited author usernames are case sensitive. + After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.`} + values={this.props.metadata.invitedAuthors} + onChange={(e)=>this.handleFieldChange('invitedAuthors', e)} + /> +
+
+

Privacy

@@ -411,4 +396,4 @@ const MetadataEditor = createClass({ } }); -module.exports = MetadataEditor; +export default MetadataEditor; diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/metadataEditor/metadataEditor.less index fd04f07d9..6a4df1c46 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.less +++ b/client/homebrew/editor/metadataEditor/metadataEditor.less @@ -1,4 +1,4 @@ -@import 'naturalcrit/styles/colors.less'; +@import '@sharedStyles/core.less'; .userThemeName { padding-right : 10px; @@ -44,8 +44,6 @@ gap : 10px; } - - .field { position : relative; display : flex; @@ -62,6 +60,7 @@ & > .value { flex : 1 1 auto; width : 50px; + &[data-tooltip-right] { max-width : 380px; } &:invalid { background : #FFB9B9; } small { display : block; @@ -74,6 +73,16 @@ border : 1px solid gray; &:focus { outline : 1px solid #444444; } } + + &.description { + flex : 1; + textarea.value { + height : auto; + font-family : 'Open Sans', sans-serif; + resize : none; + } + } + &.thumbnail, &.themes { label { line-height : 2.0em; } .value { @@ -90,6 +99,15 @@ } } + &.tags .tagInput-dropdown { + z-index : 400; + max-width : 200px; + } + &.language .value { + z-index : 300; + max-width : 150px; + } + &.themes { .value { overflow : visible; @@ -101,22 +119,13 @@ } } - &.description { - flex : 1; - textarea.value { - height : auto; - font-family : 'Open Sans', sans-serif; - resize : none; - } - } - - &.language .language-dropdown { - z-index : 200; - max-width : 150px; + &.invitedAuthors .value { + z-index : 100; + + .tagInput-dropdown { max-width : 200px; } } } - .thumbnail-preview { position : relative; flex : 1 1; @@ -129,7 +138,7 @@ background-color : #AAAAAA; } - .systems.field .value { + .renderers.field .value { label { display : inline-flex; align-items : center; @@ -169,7 +178,7 @@ .themes.field { & .dropdown-container { position : relative; - z-index : 100; + z-index : 200; background-color : white; } & .dropdown-options { overflow-y : visible; } diff --git a/client/homebrew/editor/metadataEditor/validations.js b/client/homebrew/editor/metadataEditor/validations.js index d0e052b07..2430534dc 100644 --- a/client/homebrew/editor/metadataEditor/validations.js +++ b/client/homebrew/editor/metadataEditor/validations.js @@ -1,4 +1,4 @@ -module.exports = { +export default { title : [ (value)=>{ return value?.length > 100 ? 'Max title length of 100 characters' : null; diff --git a/client/homebrew/editor/snippetbar/snippetbar.jsx b/client/homebrew/editor/snippetbar/snippetbar.jsx index 5e2051a86..304664ff5 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.jsx +++ b/client/homebrew/editor/snippetbar/snippetbar.jsx @@ -1,29 +1,36 @@ /*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/ -require('./snippetbar.less'); -const React = require('react'); -const createClass = require('create-react-class'); -const _ = require('lodash'); -const cx = require('classnames'); +import './snippetbar.less'; +import React from 'react'; +import createReactClass from 'create-react-class'; + +import _ from 'lodash'; +import cx from 'classnames'; import { loadHistory } from '../../utils/versionHistory.js'; -import { brewSnippetsToJSON } from '../../../../shared/helpers.js'; +import { brewSnippetsToJSON } from '@shared/helpers.js'; -//Import all themes -const ThemeSnippets = {}; -ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js'); -ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js'); -ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js'); -ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js'); -ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js'); +import Legacy5ePHB from '@themes/Legacy/5ePHB/snippets.js'; +import V3_5ePHB from '@themes/V3/5ePHB/snippets.js'; +import V3_5eDMG from '@themes/V3/5eDMG/snippets.js'; +import V3_Journal from '@themes/V3/Journal/snippets.js'; +import V3_Blank from '@themes/V3/Blank/snippets.js'; -const EditorThemes = require('build/homebrew/codeMirror/editorThemes.json'); +const ThemeSnippets = { + Legacy_5ePHB : Legacy5ePHB, + V3_5ePHB : V3_5ePHB, + V3_5eDMG : V3_5eDMG, + V3_Journal : V3_Journal, + V3_Blank : V3_Blank, +}; + +import EditorThemes from '../../../../build/homebrew/codeMirror/editorThemes.json'; const execute = function(val, props){ if(_.isFunction(val)) return val(props); return val; }; -const Snippetbar = createClass({ +const Snippetbar = createReactClass({ displayName : 'SnippetBar', getDefaultProps : function() { return { @@ -281,9 +288,9 @@ const Snippetbar = createClass({ } }); -module.exports = Snippetbar; +export default Snippetbar; -const SnippetGroup = createClass({ +const SnippetGroup = createReactClass({ displayName : 'SnippetGroup', getDefaultProps : function() { return { diff --git a/client/homebrew/editor/snippetbar/snippetbar.less b/client/homebrew/editor/snippetbar/snippetbar.less index 8e50aa764..37853ca75 100644 --- a/client/homebrew/editor/snippetbar/snippetbar.less +++ b/client/homebrew/editor/snippetbar/snippetbar.less @@ -1,5 +1,6 @@ +@import '@sharedStyles/core.less'; @import (less) './client/icons/customIcons.less'; -@import (less) '././././themes/fonts/5e/fonts.less'; +@import (less) '@themes/fonts/5e/fonts.less'; .snippetBar { @menuHeight : 25px; diff --git a/client/homebrew/editor/tagInput/curatedTagSuggestionList.js b/client/homebrew/editor/tagInput/curatedTagSuggestionList.js new file mode 100644 index 000000000..7d692ecb0 --- /dev/null +++ b/client/homebrew/editor/tagInput/curatedTagSuggestionList.js @@ -0,0 +1,219 @@ +export const tagSuggestionList = [ + // ############################## Systems + // D&D + 'system:D&D Original', + 'system:D&D Basic', + 'system:AD&D 1e', + 'system:AD&D 2e', + 'system:D&D 3e', + 'system:D&D 3.5e', + 'system:D&D 4e', + 'system:D&D 5e', + 'system:D&D 5e 2024', + 'system:BD&D (B/X)', + 'system:D&D Essentials', + + // Other Famous RPGs + 'system:Pathfinder 1e', + 'system:Pathfinder 2e', + 'system:Vampire: The Masquerade', + 'system:Werewolf: The Apocalypse', + 'system:Mage: The Ascension', + 'system:Call of Cthulhu', + 'system:Shadowrun', + 'system:Star Wars RPG (D6/D20/Edge of the Empire)', + 'system:Warhammer Fantasy Roleplay', + 'system:Cyberpunk 2020', + 'system:Blades in the Dark', + 'system:Daggerheart', + 'system:Draw Steel', + 'system:Mutants and Masterminds', + + // Meta + 'meta:V3', + 'meta:Legacy', + 'meta:Template', + 'meta:Theme', + 'meta:free', + 'meta:Character Sheet', + 'meta:Documentation', + 'meta:NPC', + 'meta:Guide', + 'meta:Resource', + 'meta:Notes', + 'meta:Example', + + // Book type + 'type:Campaign', + 'type:Campaign Setting', + 'type:Adventure', + 'type:One-Shot', + 'type:Setting', + 'type:World', + 'type:Lore', + 'type:History', + 'type:Dungeon Master', + 'type:Encounter Pack', + 'type:Encounter', + 'type:Session Notes', + 'type:reference', + 'type:Handbook', + 'type:Manual', + 'type:Manuals', + 'type:Compendium', + 'type:Bestiary', + + // ###################################### RPG Keywords + + // Classes / Subclasses / Archetypes + 'Class', + 'Subclass', + 'Archetype', + 'Martial', + 'Half-Caster', + 'Full Caster', + 'Artificer', + 'Barbarian', + 'Bard', + 'Cleric', + 'Druid', + 'Fighter', + 'Monk', + 'Paladin', + 'Rogue', + 'Sorcerer', + 'Warlock', + 'Wizard', + + // Races / Species / Lineages + 'Race', + 'Ancestry', + 'Lineage', + 'Aasimar', + 'Beastfolk', + 'Dragonborn', + 'Dwarf', + 'Elf', + 'Goblin', + 'Half-Elf', + 'Half-Orc', + 'Human', + 'Kobold', + 'Lizardfolk', + 'Lycan', + 'Orc', + 'Tiefling', + 'Vampire', + 'Yuan-Ti', + + // Magic / Spells / Items + 'Magic', + 'Magic Item', + 'Magic Items', + 'Wondrous Item', + 'Magic Weapon', + 'Artifact', + 'Spell', + 'Spells', + 'Cantrip', + 'Cantrips', + 'Eldritch', + 'Eldritch Invocation', + 'Invocation', + 'Invocations', + 'Pact boon', + 'Pact Boon', + 'Spellcaster', + 'Spellblade', + 'Magical Tattoos', + 'Enchantment', + 'Enchanted', + 'Attunement', + 'Requires Attunement', + 'Rune', + 'Runes', + 'Wand', + 'Rod', + 'Scroll', + 'Potion', + 'Potions', + 'Item', + 'Items', + 'Bag of Holding', + + // Monsters / Creatures / Enemies + 'Monster', + 'Creatures', + 'Creature', + 'Beast', + 'Beasts', + 'Humanoid', + 'Undead', + 'Fiend', + 'Aberration', + 'Ooze', + 'Giant', + 'Dragon', + 'Monstrosity', + 'Demon', + 'Devil', + 'Elemental', + 'Construct', + 'Constructs', + 'Boss', + 'BBEG', + + // ############################# Media / Pop Culture + 'One Piece', + 'Dragon Ball', + 'Dragon Ball Z', + 'Naruto', + 'Jujutsu Kaisen', + 'Fairy Tail', + 'Final Fantasy', + 'Kingdom Hearts', + 'Elder Scrolls', + 'Skyrim', + 'WoW', + 'World of Warcraft', + 'Marvel Comics', + 'DC Comics', + 'Pokemon', + 'League of Legends', + 'Runeterra', + 'Arcane', + 'Yu-Gi-Oh', + 'Minecraft', + 'Don\'t Starve', + 'Witcher', + 'Witcher 3', + 'Cyberpunk', + 'Cyberpunk 2077', + 'Fallout', + 'Divinity Original Sin 2', + 'Fullmetal Alchemist', + 'Fullmetal Alchemist Brotherhood', + 'Lobotomy Corporation', + 'Bloodborne', + 'Dragonlance', + 'Shackled City Adventure Path', + 'Baldurs Gate 3', + 'Library of Ruina', + 'Radiant Citadel', + 'Ravenloft', + 'Forgotten Realms', + 'Exandria', + 'Critical Role', + 'Star Wars', + 'SW5e', + 'Star Wars 5e', +]; + +// substrings to be normalized to the first value on the array +export const canonizationList = [ + ['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'], + ['5e', '5th Edition'], + ['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'], + ['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'], + ['P2e', 'p2e', 'P2E', 'Pathfinder 2e'], +]; \ No newline at end of file diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index d60e23b1b..b43797424 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -1,105 +1,198 @@ -require('./tagInput.less'); -const React = require('react'); -const { useState, useEffect } = React; -const _ = require('lodash'); +import './tagInput.less'; +import React, { useState, useEffect } from 'react'; +import Combobox from '../../../components/combobox.jsx'; -const TagInput = ({ unique = true, values = [], ...props })=>{ - const [tempInputText, setTempInputText] = useState(''); - const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false }))); +import { tagSuggestionList, canonizationList } from './curatedTagSuggestionList.js'; + +const TagInput = ({ tooltip, label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{ + const [tagList, setTagList] = useState( + values.map((value)=>({ + value, + editing : false, + draft : '', + })), + ); useEffect(()=>{ - handleChange(tagList.map((context)=>context.value)); + const incoming = values || []; + const current = tagList.map((t)=>t.value); + + const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]); + + if(changed) { + setTagList( + incoming.map((value)=>({ + value, + editing : false, + })), + ); + } + }, [values]); + + useEffect(()=>{ + onChange?.({ + target : { value: tagList.map((t)=>t.value) }, + }); }, [tagList]); - const handleChange = (value)=>{ - props.onChange({ - target : { value } - }); - }; + const normalizeValue = (input)=>{ + const lowerInput = input.toLowerCase(); + let normalizedTag = input; - const handleInputKeyDown = ({ evt, value, index, options = {} })=>{ - if(_.includes(['Enter', ','], evt.key)) { - evt.preventDefault(); - submitTag(evt.target.value, value, index); - if(options.clear) { - setTempInputText(''); + for (const group of canonizationList) { + for (const tag of group) { + if(!tag) continue; + + const index = lowerInput.indexOf(tag.toLowerCase()); + if(index !== -1) { + normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length); + break; + } } } + + if(normalizedTag.includes(':')) { + const [rawType, rawValue = ''] = normalizedTag.split(':'); + const tagType = rawType.trim().toLowerCase(); + const tagValue = rawValue.trim(); + + if(tagValue.length > 0) { + normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`; + } + //trims spaces around colon and capitalizes the first word after the colon + //this is preferred to users not understanding they can't put spaces in + } + + return normalizedTag; }; - const submitTag = (newValue, originalValue, index)=>{ - setTagList((prevContext)=>{ - // remove existing tag - if(newValue === null){ - return [...prevContext].filter((context, i)=>i !== index); + const submitTag = (newValue, index = null)=>{ + const trimmed = newValue?.trim(); + if(!trimmed) return; + if(!valuePatterns.test(trimmed)) return; + + const normalizedTag = normalizeValue(trimmed); + + setTagList((prev)=>{ + const existsIndex = prev.findIndex((t)=>t.value.toLowerCase() === normalizedTag.toLowerCase()); + if(unique && existsIndex !== -1) return prev; + if(index !== null) { + return prev.map((t, i)=>(i === index ? { ...t, value: normalizedTag, editing: false } : t)); } - // add new tag - if(originalValue === null){ - return [...prevContext, { value: newValue, editing: false }]; - } - // update existing tag - return prevContext.map((context, i)=>{ - if(i === index) { - return { ...context, value: newValue, editing: false }; - } - return context; - }); + + return [...prev, { value: normalizedTag, editing: false }]; }); }; + const removeTag = (index)=>{ + setTagList((prev)=>prev.filter((_, i)=>i !== index)); + }; + const editTag = (index)=>{ - setTagList((prevContext)=>{ - return prevContext.map((context, i)=>{ - if(i === index) { - return { ...context, editing: true }; - } - return { ...context, editing: false }; - }); - }); + setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t))); }; - const renderReadTag = (context, index)=>{ - return ( -
  • editTag(index)}> - {context.value} - -
  • - ); + const stopEditing = (index)=>{ + setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t))); }; - const renderWriteTag = (context, index)=>{ + const suggestionOptions = tagSuggestionList.map((tag)=>{ + const tagType = tag.split(':'); + + let classes = 'item'; + switch (tagType[0]) { + case 'type': + classes = 'item type'; + break; + case 'group': + classes = 'item group'; + break; + case 'meta': + classes = 'item meta'; + break; + case 'system': + classes = 'item system'; + break; + default: + classes = 'item'; + break; + } + return ( - handleInputKeyDown({ evt, value: context.value, index: index })} - autoFocus - /> +
    + {tag} +
    ); - }; + }); return ( -
    - -
    -
      - {tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} -
    - - setTempInputText(e.target.value)} - onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })} - /> -
    +
    + submitTag(value)} + onEntry={(e)=>{ + if(e.key === 'Enter') { + e.preventDefault(); + submitTag(e.target.value); + } + }} + /> +
      + {tagList.map((t, i)=>t.editing ? ( + setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: e.target.value } : tag)), + ) + } + onKeyDown={(e)=>{ + if(e.key === 'Enter') { + e.preventDefault(); + submitTag(t.draft, i); // submit draft + setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: '' } : tag)), + ); + } + if(e.key === 'Escape') { + stopEditing(i); + e.target.blur(); + } + }} + autoFocus + /> + ) : ( +
    • editTag(i)}> + {t.value} + +
    • + ), + )} +
    ); }; -module.exports = TagInput; +export default TagInput; diff --git a/client/homebrew/editor/tagInput/tagInput.less b/client/homebrew/editor/tagInput/tagInput.less index e69de29bb..e986c6cc7 100644 --- a/client/homebrew/editor/tagInput/tagInput.less +++ b/client/homebrew/editor/tagInput/tagInput.less @@ -0,0 +1,31 @@ +.tags { + + .tagInputWrap { + display:grid; + grid-template-columns: 200px 3fr; + gap:10px; + } + + .list input { + border-radius: 5px; + } + + .tagInput-dropdown { + .dropdown-options { + .item { + &.type { + background-color: #00800035; + } + &.group { + background-color: #50505035; + } + &.meta { + background-color: #00008035; + } + &.system { + background-color: #80000035; + } + } + } + } +} diff --git a/client/homebrew/editor/tagInput/tagSuggestionList.js b/client/homebrew/editor/tagInput/tagSuggestionList.js new file mode 100644 index 000000000..eb4891616 --- /dev/null +++ b/client/homebrew/editor/tagInput/tagSuggestionList.js @@ -0,0 +1,1980 @@ +export default [ + 'meta:Theme', + '5e', + 'Subclass', + 'meta:theme', + 'subclass', + 'Class', + 'Homebrew', + 'Race', + 'Dungeons and Dragons', + 'theme', + 'Daggerheart', + '2024', + 'One Piece', + 'One Piece DND', + 'Luffy', + 'Dungeons and Devil Fruits', + 'Strawhats', + 'Template', + 'Campaign Frame', + 'class', + 'Players Handbook', + 'dnd', + 'osr', + 'Dungeon Masters Guide', + 'shadowdark', + 'dragonbane', + 'PHB', + 'example', + 'Devil Fruits', + 'system:pf2e', + 'DnD', + 'DMG', + 'system:dnd5.5', + 'Monster', + 'homebrew', + 'race', + 'template', + 'Warlock', + 'monster', + 'Fighter', + 'warlock', + 'druid', + 'sorcerer', + 'D&D', + 'Magic Item', + 'Barbarian', + 'Artificer', + '2014', + 'system:descent into avernus', + 'Sorcerer', + 'Adventure', + 'Paladin', + 'Ranger', + 'user help', + 'fighter', + '5th Edition', + 'Spells', + 'Monk', + 'Spell', + 'NPC', + 'Cleric', + 'spell', + 'Rogue', + 'css', + 'Item', + 'artificer', + 'magic item', + 'Rules', + 'barbarian', + 'wizard', + 'russian', + 'DnD5e', + 'Wizard', + 'paladin', + 'bastionland', + 'spells', + 'Devil Fruit', + 'Bard', + '5.5e', + 'rogue', + 'Tabletop System', + 'Haki', + 'Druid', + 'mystic bastionland', + 'item', + 'Lore', + 'bard', + 'monk', + 'system:dnd5e', + 'world', + 'ranger', + 'WIP', + 'cleric', + 'Dragon', + 'Naruto', + 'Creature', + 'snippet', + 'npc', + 'DeS', + 'Magic', + 'guide', + 'v3', + 'Beast', + 'Classe', + 'onering', + 'Monsters', + 'Races', + 'Weapon', + 'adventure', + 'Subclasses', + 'stat block', + 'weapon', + 'Species', + 'DONE', + 'archetype', + 'RPG', + 'Hollow Knight', + '5e\'24', + 'Martial', + 'DND', + 'Classe Nova', + 'Curse of Strahd', + 'Boss', + 'Hollowed Kingdoms', + 'baldurs mouth', + '5.24', + 'Homewbrew', + 'Encyclopedia', + 'Revised', + 'OneWorldHD', + 'knight', + 'DPS', + 'srd', + 'Undead', + 'items', + 'DnD 5e', + 'Guide', + 'Compendium', + 'Feat', + 'newspaper', + 'magic', + 'TTRPG', + 'descent into avernus', + 'reference', + 'system:D&D 5e24', + 'feat', + 'Magic Items', + 'Campaign', + 'resource', + 'Feats', + 'Anime', + 'dd5', + 'races', + 'Monstrosity', + 'DM Screen', + '2024 Rules', + 'Rework', + 'Character Build', + 'Done', + '5e 2024', + 'Construct', + 'myth', + 'magic items', + 'creature', + 'Legendary', + 'Strahd', + 'background', + 'Player', + 'style', + 'Legacy', + 'Player Handbook', + 'martial', + 'Character', + 'Dungeons & Dragons', + 'Table', + 'reddit', + 'monsters', + 'OneDND', + 'dragon', + 'Suporte', + 'Soulbound', + 'Expanded Handbook', + 'bestiary', + 'Humanoid', + 'system:dnd', + 'Oredell', + 'system:class', + 'V3', + 'system:5e', + '5e\'14', + 'Age of Sigmar', + 'Items', + 'Eberron', + 'horror', + 'dnd5e', + 'star wars', + 'Background', + 'compendium', + 'revision', + 'Elemental', + 'character', + 'Marcial', + 'SW5e', + 'notes', + 'DM', + 'Fey', + 'one-shot', + 'resources', + 'NEW', + 'campaign', + 'wondrous item', + 'Setting', + 'Archive', + 'NIMRE', + 'Tanque', + 'Jogavel', + 'Dnd 5e', + 'LotM', + 'setting', + 'revised', + 'Warhammer', + 'rework', + 'Conjurador', + 'GMBinder', + 'ItemSet', + 'NPCs', + 'classes', + 'CR 2', + 'AetherSail', + 'undead', + 'Artifact', + 'cards', + 'Example', + 'Guides', + 'Theme', + 'Swamp', + 'CR 1', + 'patron', + 'Extra', + 'concept', + 'final fantasy', + 'Aberration', + 'Elder Scrolls', + 'rules', + 'meta:gratis', + 'Dice Pool', + 'Cue', + 'dark', + 'type:Style', + 'rpg', + 'meta:free', + 'summoner', + 'OC', + 'rare', + 'Fleshing Out', + 'francais', + 'Dnd', + 'Creatures', + 'Sims 4', + 'sous-classe', + 'ffxiv', + 'Underdark', + 'add-on', + 'weapons', + 'Subrace', + 'dungeons and dragons', + 'Naruto 5e', + 'Lafari', + 'Very Rare', + 'd6', + 'red', + 'Equipment', + 'CR 3', + 'Patron', + '5E', + 'objet magique', + 'spellcaster', + 'feats', + '5.24e', + 'system:d&d5e', + 'Rare', + 'Thudnfer', + 'daggerheart', + 'CR 1/2', + 'Archetype', + 'dnd 5e', + 'Pathfinder', + 'lineage', + 'Celestial', + 'Support', + 'species', + 'ARCHIVED', + 'Horror', + 'humanoid', + 'Subclase', + 'book:PHB E&E', + 'final fantasy xiv', + 'Jungle', + 'Feywild', + 'Fiend', + 'subclasses', + 'HB', + 'Legacy Challenge', + 'Project Horizon', + 'vampire', + 'WoW', + 'DND 5e', + 'Weapons', + 'system:book clone', + 'CoS', + 'N5e', + 'Summon', + 'Spellcaster', + 'Koretra', + 'Voidborn', + 'one shot', + 'Templates', + 'tables', + 'Iphexar', + 'Shattered Obelisk', + 'Sword Coast', + 'elemental', + 'lore', + 'character sheet', + 'discord', + 'BetterMonsters', + 'players', + 'group:simple skans', + 'Coastal', + 'Forest', + 'Unearthed Arcana', + 'Old', + 'Collection', + 'Ancestry', + 'Caevash', + 'Strixhaven', + 'Limbus Company', + 'Daydreams & Deviants', + 'Statblocks', + 'Urban', + 'Classes', + 'familiar', + 'Blood', + 'oneshot', + 'n5e', + 'wip', + 'masks', + 'Planeshifted', + 'Carrioss', + 'avernus', + 'object', + '1', + 'Ruins', + 'Anime Character', + 'wild shape', + 'mask', + 'dj9 game', + 'Handbook', + 'Curse', + 'oath', + 'Midralis', + 'Appendix', + '5.5', + 'tales of the valiant', + 'player classes', + 'Caster', + 'Large', + 'Fateforge', + 'Cursed', + 'beast', + 'Pathfinder 2e', + 'Design', + 'Uncommon', + 'Endeur', + 'Elemental Water', + 'SCC', + 'sci-fi', + 'Dragons', + 'human', + 'Gods', + 'Medium', + 'System', + 'Help', + 'Handout', + 'Skyrim', + 'BnB', + 'draft', + 'PC', + 'Variant', + 'syntax', + 'OneShot', + 'Clase', + 'UESTRPG', + 'Savannah', + 'Reef', + 'CR 5', + 'Forgotten Realms', + 'collection', + 'Combat', + 'Historia', + 'artifact', + 'Expansion', + 'Attunement', + 'Variant Rules', + 'd&d', + 'Party Build', + 'Evocation', + 'homerule', + 'Forbidden West', + 'how-to', + 'tov', + 'Mystical Item', + 'CR 4', + 'Necromancer', + 'General Rules', + 'Needs Update', + 'stat blocks', + 'DC Comics', + 'Sword', + 'Armor', + 'blood hunter', + 'Blood Hunter', + 'Meio Conjurador', + 'Mydia', + '3rd Party', + 'N5E', + 'Delvebound', + 'Ocean', + 'Subterranean', + 'half-caster', + 'Demon', + 'Fire', + 'meta:Template', + 'Character Sheet', + 'PF2e', + 'png', + 'fantasy', + 'Setting Guide', + 'Style', + 'Cor', + 'legacy', + 'Minion', + 'meta:khaoz age', + 'Book', + 'magical item', + 'immersion', + 'reminder cards', + 'Regras', + 'Durnovar', + '2025', + 'Camp1', + 'Abyss', + 'armor', + 'construct', + 'firearms', + 'Backgrounds', + 'Companion', + 'Melee', + 'fey', + 'Incomplete', + 'Vampire', + 'Bestiary', + 'Worldbuilding', + 'History', + 'Conjuration', + 'necromancy', + 'dragons', + 'ancestry', + 'Mech', + 'PbtA', + 'COS', + 'One Shot', + 'Factions', + 'Transmutation', + 'Spellcasting', + 'card', + 'Ardh', + 'redveil', + 'conditions', + 'elturel', + 'rhye', + 'group:James Haeck', + 'CaelYuu', + 'system:5.24e', + 'system:GM Binder', + 'Grassland', + 'melee', + 'Potions', + 'anime', + 'D&D 5e', + 'Healer', + 'Gambling', + 'Lightning', + 'fighting style', + 'support', + 'Style Template', + 'mtg', + 'NSFW', + 'aventura', + 'Manuals', + 'plant', + 'Incarnate', + 'Crafting', + 'DnDBeyond', + 'Monster Girl', + 'Monster Girl Encyclopedia', + 'pet', + 'npcs', + 'Stat Block', + 'Styleguide', + 'mitologia', + 'Objeto maravilloso', + 'vaesen rpg', + 'dnd-2024', + 'Class Handbook', + 'Space', + 'Taiga', + 'CR 6', + 'Pact Boon', + 'race/ancestry', + 'Necromancy', + 'cantrip', + 'LoL', + 'Raza', + 'handout', + 'Mechanic', + 'Conversion', + 'Wondrous Item', + 'Familiar', + 'necromancer', + 'uncommon', + 'curse', + 'Campaign Setting', + 'Tetra', + '1e', + 'module', + 'Evolving', + 'boiling sea', + 'deck', + 'OSR', + 'RU', + 'VL', + 'Underdeep', + 'Deep Ocean', + 'Giant', + 'statblock', + 'combat', + 'Time', + '5E24', + 'session zero', + 'elf', + 'tank', + 'sorcerous origin', + 'Drakkenheim', + 'Tavern', + 'Domain', + 'healer', + 'Valenor', + 'blood', + 'Oath', + 'spooky', + 'fire', + 'meta:5e24 Style', + 'Notes', + 'City', + 'Conjurador Completo', + 'prop', + 'Dotherys', + 'Rietuma 3.0', + '5e24', + 'Library of Ruina', + 'español', + 'Project Echo', + 'battle of Japan', + 'Plant', + 'Badlands', + 'Neverwinter', + 'Fantasy', + 'Beastfolk', + 'Unarmed', + 'Cold', + 'Damage', + 'attunement', + 'Hurthud', + '3rd Level', + 'spelljammer', + 'mostro', + 'Custom', + 'PT-BR', + 'Alternative Realms', + 'The Foot', + 'boss', + 'demo', + 'Supplement', + 'FitD', + 'classe', + '5.14', + 'Copy', + 'DnD5e24', + 'X-Men', + 'TNA', + 'CR 8', + 'Desert', + 'CR 7', + 'arme', + 'random', + 'spellcasting', + 'Deprecated', + 'Cards', + 'finished', + 'Ben 10', + 'equipment', + 'Geography', + 'Games', + 'For Players', + 'Faerun', + 'scroll', + 'Faction', + 'Alchemist', + 'drow', + 'Lineage', + 'mix-blend-mode', + 'columns', + 'User Help', + 'Reami Dimenticati', + 'Класс', + 'D100', + 'nsfw', + 'hucaen', + 'v1.0', + 'Cortex', + 'Fallout', + 'ww5e', + 'MAGIC', + 'DnD2024', + 'ToV', + 'D&D2024', + 'The Backrooms', + 'Freshwater', + 'D20', + 'Dragonborn', + 'custom', + 'sword', + 'Dungeons and Dragons 5e', + 'Water', + 'legendary', + 'Dungeon', + 'Ravenloft', + 'aberration', + 'Longsword', + 'transmutation', + 'Fairy Tail', + 'Character background', + 'Exandria', + 'Updated', + 'pitch', + 'Half-Caster', + 'Complete', + 'Money', + 'player', + 'forgotten-realms', + 'Festival', + 'Casino', + 'SCAG', + 'Currency', + 'North', + 'Toril', + 'Scourged Land of Valenor', + 'Oota', + 'parchment', + 'Literature', + 'Serrith', + 'PNJ', + 'Divinity Original Sin 2', + 'Wild', + 'videogame', + 'magic the gathering', + 'sweetblossom', + 'GMscreen', + 'MandM', + 'D&D 5.24', + 'Camp2', + 'Remaster', + 'riassunti', + 'type:Resource', + 'system:D&D', + 'tag:Class', + 'Excelsior', + 'Stat Blocks', + 'Sci-Fi', + 'Ooze', + 'CR 1/8', + 'sublclass', + 'chart', + 'Mountains', + 'guns', + 'Nature', + 'Orc', + 'Poison', + 'Devil', + 'fiend', + 'DC', + 'pt-br', + 'ABnB', + 'One-Shot', + 'strahd', + 'Ring', + 'Theme song', + 'orc', + 'summon', + 'Psion', + 'Psionics', + 'Dungeon Master', + 'vehicle', + 'DM only', + 'Demigod', + 'Antica Energia', + 'Pirates', + 'Sourcebook', + 'devil', + 'Cantrip', + 'mystery', + 'MtG', + 'conversion', + 'Festivals', + 'Casinos', + 'Taverns', + 'Betting', + 'Drinking', + 'phandelver', + 'Warhammer 40k', + 'mutant', + 'styling', + 'FATE', + 'Lone Wolf', + 'icon', + 'New Dawn', + 'Magic Set', + 'Paladin Subclass', + 'Alter Class', + 'difficulty classses', + 'combat tables', + 'phb', + 'Project Moon', + 'Undertomes', + 'EGO', + 'Campagne 1', + 'Constelação', + 'Arvore I', + 'Fim da Jornada', + 'greek god', + 'dwarf', + 'Firearms', + '3.5e', + 'generator', + 'Elf', + 'meta: Scenario', + 'enchantment', + 'buff', + 'ITW', + 'Tank', + 'Archived', + 'Martial Archetype', + 'caster', + 'BR', + 'Knight', + 'Utility', + 'SWADE', + 'Star Wars', + 'pc', + 'Mystic', + 'Useful', + 'Netherdeep', + 'crafting', + 'Sapient Undead', + 'Maverick', + 'Revision', + 'Resource', + 'Humblewood', + 'one piece', + 'Bag of Holding', + 'medium', + 'lightning', + 'backgrounds', + '4th Level', + 'path', + 'BREAK-RPG', + 'dark fantasy', + 'Players', + 'poison', + 'psionic', + 'gazook89', + 'homebrew subclass', + 'wild-wasteland', + 'CWD', + 'Paid', + 'Tales of the Valiant', + 'Dreadhold', + 'arma', + 'system:Mutants and Masterminds', + '#Tiefschlaf', + 'Brew', + 'Myra', + 'Swashbuckler', + 'dead by daylight', + 'Exceptional', + 'COD Zombies', + 'Hills', + 'Tundra', + 'type:Campaign', + 'wild magic', + 'Food', + 'Death', + 'homebrew rules', + 'Remake', + 'Witcher', + 'water', + 'Pet', + 'book', + 'AAH', + 'pact', + 'Ice', + 'Character Creation', + 'animal', + 'Pokemon', + 'clase', + '5e14', + 'DBZ', + 'CLONE', + 'Evil', + 'Tarsere', + 'Mythology', + 'pf2e', + 'Magical', + 'type:race', + 'Sorcerous Origin', + 'Information', + 'styles', + 'Module', + 'gish', + 'frames', + 'DeltaGreen', + 'Magic item', + 'food', + 'chef', + 'basics', + 'giant', + 'Brew Creation', + 'One-shot', + 'ttrpg', + 'Path', + 'Don\'t Starve', + 'MGE', + 'firearm', + 'DnDBehindTheScreen', + 'store', + 'The Artisan', + 'timeline', + 'college', + 'dev', + 'dungeon of the dead three', + 'Cradle', + 'Dnd5e', + 'dungeon', + 'Amaranthine', + 'Regno di Oltremare', + 'bestia', + 'rewrite', + 'WiP', + 'Subclasse', + 'mutants and masterminds', + 'The Embrace', + 'meta:documentation', + 'Mutants And Masterminds', + 'khedoria', + 'Encounter Pack', + 'giorni', + 'Statblock', + 'Enemies', + 'Goblinoids', + 'Heavens', + 'system:2e', + 'Vaalbara', + 'Dwarf', + 'airos', + 'table', + 'Artificer Specialization', + 'Buff', + 'Book 1', + 'Ranged', + 'cypher', + 'utilities', + '40k', + 'Psychic', + 'Fear', + 'steampunk', + 'shadow', + 'subclase', + 'Barbarian Subclass', + 'Elements', + 'pact boon', + 'Clan', + 'Fly', + 'solo', + 'sourcebook', + 'Marvel Comics', + 'compilation', + 'Firearm', + 'sidekick', + 'infusions', + 'Mechanics', + 'Summoner', + 'Aasimar', + 'Human', + 'Vehicle', + 'Shadow', + 'Clone', + 'custom css', + 'ocean', + 'sotdl', + 'bandit', + 'Wind', + 'Printer Friendly', + 'Obsolete', + 'mechanics', + 'illusion', + '5th edition', + 'League of Legends', + 'Vestige', + 'dungeons', + 'Dungeons', + 'and', + 'Elden Ring', + 'L5R', + 'd20', + 'Poisons', + 'd15', + 'Dungeons And Dragons', + 'MTG', + 'divine', + 'characters', + 'witch', + 'Anime Homebrew', + 'Zombie', + 'thunder', + 'Jujutsu Kaisen', + 'campagne', + 'Deadlands', + 'spell list', + '1 Person', + 'Ritual', + 'screen', + 'nature', + 'Divination', + 'Compattare', + 'dtrpg', + 'quick ref', + 'Mago', + 'Illivia', + 'Shonen', + 'Core Deities', + 'green ronin', + 'Bless', + 'D&D5e', + 'version:0.1.0', + 'curato', + 'system:Ord', + 'Images', + 'Sealed Artifact', + 'Giants', + 'CR 9', + 'CR 11', + 'CR 0', + 'CR 14', + 'Shadowfell', + 'Tier 1', + 'd100', + 'Elemental Air', + 'artificiel', + 'Cultist', + 'Cyberpunk', + 'Huge', + 'Warrior', + 'Gun', + 'quest', + 'LYRA', + 'Music', + 'Tiefling', + 'Master', + 'Witch', + 'Linnorm', + '1st-level', + 'Mount', + 'Animal', + 'Comics', + 'Superhero', + 'creatures', + 'Hunter', + 'Control', + 'Dragon Ball', + 'Dragon Ball Z', + 'Dagger', + 'questingforamonster', + 'ICRPG', + 'Booklet', + 'f and t', + 'common', + 'Chaos', + 'spellblade', + 'Constitution', + 'artisan', + 'arcane', + 'Released', + 'ring', + 'runes', + 'gun', + 'Supportive Material', + 'The Witcher', + 'Desarmado', + 'Monster Monday', + 'Bleach', + 'Demon Slayer', + 'mice', + 'worldbuilding', + 'Necrotic', + 'ability score', + 'demon', + 'Armybook Shivatiano', + 'warrior', + 'Fighter Subclass', + 'system', + 'whisperveil', + 'psychic', + 'warhammer', + 'Aventura', + 'Culture', + 'Material', + 'meta:npc', + 'shops', + 'magic weapon', + 'nhera', + 'Dark Fantasy', + 'Regles', + 'Wonderous Item', + 'Features', + 'pokemon', + 'Ghosts of Saltmarsh', + 'monstrosity', + 'DL TWW', + 'companion', + 'alternate layout', + 'Tutorials', + 'Kitsune', + 'don', + 'heroique', + 'mini campaign', + 'drago', + 'Aquatic', + 'tool', + 'handmade', + 'released', + 'Spellblade', + 'pregen', + 'level 2', + 'Baldurs gate 3', + 'My Hero', + 'Technically a subclass', + '5.24e Remastered Subclasses', + 'dinosaurs', + '5E.2024', + 'Razas', + 'Horizon', + 'Clothing', + '+2', + 'castellano', + 'pentacle prophecy', + 'tag:Spells', + 'gruppo A', + 'Rpg', + 'razze', + 'type:Adventure', + 'unfinished', + '3.5', + 'gunslinger', + 'BBEG', + 'Arcane', + 'component', + 'Bow', + 'backstory', + 'phandalin', + 'Skills', + 'Pact', + 'Elemental Earth', + 'Joke', + 'invocation', + 'martial class', + 'Super Villain', + 'Eldritch', + 'Elemental Fire', + 'Homebrew Class', + 'eldritch', + 'cyberpunk', + 'Player Race', + 'Class Mod', + 'Heatcoast', + 'meta:Guide', + 'Yemao', + 'evil', + 'Named NPC', + 'CLASS', + 'Angel', + 'vecna', + 'PT', + 'PTBR', + 'Ancient', + 'Small', + 'WotC Style', + '5e Homebrew', + '1st Level', + 'dagger', + 'Brancalonia', + 'encounter', + 'cat', + 'primal path', + 'Ambientazione', + 'Magie', + 'candlekeep', + 'Ongoing', + 'Oneshot', + 'Wondrous', + 'Janbrewery', + 'Tattoos', + '5e (2014)', + 'concentration', + 'very rare', + 'Set', + 'Kobold', + 'martial archetype', + 'God', + 'blog', + 'New Gate', + 'Healing', + 'OneDnD', + 'Incantesimi', + 'Player Options', + 'contest', + 'pirate', + 'Manuel', + 'Alchimie', + 'Herboristerie', + 'Ingredients', + 'starlost', + 'Campaign 1', + 'Abandoned', + 'Previous Editions', + 'Enchantment', + 'Tools', + 'Oblivion', + 'domain', + '5th Level', + 'DnD Beyond', + 'Reference', + 'Sorcerer Subclass', + 'Dragon Magazine', + 'feature', + 'german', + 'conjuration', + 'strixhaven', + 'Sentient', + 'JJK', + '10 Generations', + 'character creation', + 'LevelUp', + 'pallid grove', + 'primer', + 'Requires Attunement', + 'College', + 'Aesthetic', + 'critter', + 'home game', + 'spanish', + 'stats', + 'Lairon', + 'Hunters Guild', + 'original setting', + 'Bosses', + 'Radiant Citadel', + 'actions', + 'Reworked', + 'Elystera', + 'Wyvern', + 'vikings', + 'thief', + 'enemies', + 'Obsession', + 'Yi Sang', + 'aberrazione', + 'Limbus', + 'animals', + 'minecraft', + 'mice of legend', + 'osric', + '20 Minutes Till Dawn', + 'campaign frame', + 'latigo', + 'DH', + 'Eldritch Invocation', + 'system:daggerheart', + '100ni', + 'meta:Sheet', + 'fa-solid fa-sheet-plastic:Ficha', + 'tag:Berean', + 'AD&D', + 'B/X', + 'The Codex Of Anomalous Entities', + 'monster manual', + 'Polar Waters', + 'CR 12', + 'CR 10', + 'blood magic', + 'Gunslinger', + 'grimoire', + 'Drakes', + 'Japanese', + 'subrace', + 'ooze', + 'Stats', + 'Half Caster', + 'Sea', + 'time', + 'Brawler', + 'Session 0', + 'Halloween', + 'Runeterra', + 'Divine', + 'Random', + 'Lifestar', + 'arcane trickster', + 'Paddy4530', + 'evocation', + 'light', + 'Steampunk', + 'shaman', + 'Primal Path', + 'monk subclass', + 'Full Caster', + 'World', + 'Planning', + 'spirit', + 'Nova Era', + 'abjuration', + 'Christmas', + 'Critical Role', + 'Gish', + 'Bandit', + 'Monster Manual', + 'party member', + 'mgazt', + 'Playable Race', + 'Donjon.bin.sh', + 'Final Fantasy', + 'Roleplay', + 'monstre', + 'fairy', + 'frame', + 'Minecraft', + 'Stealth', + 'Manual', + 'half caster', + 'Storm', + 'Sorcery', + 'format work', + 'Kingdom Hearts', + 'hexblade', + 'block', + 'page layouts', + 'Monk Subclass', + 'FinyaFluKaiKolja', + 'Radiant', + 'group:playtest', + 'Korrahir', + 'noble', + 'exorcist', + 'xapien', + 'Raven Queen', + 'markdown', + 'damage', + 'Alchemy', + 'morrigan', + 'genasi', + 'ZNH', + 'folklore', + 'Fate', + 'hechicero', + 'Air', + 'Magic Weapon', + 'Anime DND 5e', + 'Dragon Ball Z TTRPG', + 'Dragon Ball Z RPG', + 'Dragon Ball Z DND', + 'Dragon Ball Z 5e', + 'samurai', + 'Goblin', + 'Base Sheet', + 'Shackled City Adventure Path', + 'Natureza', + 'control', + 'Normarch', + 'Reddit', + 'Genshin Impact', + 'Abjuration', + 'Myr', + 'Flight', + 'Vampyre', + 'nightmare', + 'Lycan', + 'Occult', + 'circle', + 'Christmas Special', + 'DoDD', + 'Character Options', + 'traduction', + 'Characters', + 'Gear', + 'system:sf2e', + 'drakkenheim', + 'downtime', + 'amulet', + 'Feiticeiros e Maldicoes', + 'Tecnica amaldicoada', + 'prorpg', + 'enemy', + 'No Mercy', + 'rain world', + 'slugcat', + 'fly', + 'meta:User Guide', + 'Fallout TTRPG', + 'regles', + 'Ill Tides', + 'Light-hearted', + 'Vastria', + 'school', + 'Fillible Online', + 'Mezgarr', + 'Berserk', + 'invocations', + 'Classe Refeita', + 'Auroboros', + 'bosses', + 'fabula ultima', + 'Shagya', + 'wild', + 'DnD 2024', + 'KaiburrKathHound', + 'Barbarian Path', + 'fauna', + '5E.2014', + 'system:curse of strahd', + 'Unofficial', + 'how to', + 'Glaive', + 'A5E', + 'pt', + 'Consumible', + 'Realmers\'', + 'Versatile Lineage', + 'Shichibukai', + '2024e', + 'Rencontre', + 'tag:Spell List', + 'elementalist', + 'noncaster', + 'blasphemous', + 'Mordhiem', + 'Wildfrost', + '#Regelwerk', + 'Rewrite', + 'Maldición de Strahd', + 'Scion', + 'Entities', + 'Hoarwyrm', + 'Player utility', + 'CR 1/4', + 'Temperate Forest', + 'Demons', + 'Drow', + 'type:rules', + 'fay', + '2e', + 'familier', + 'supplement', + 'Amberwar', + 'slime', + 'Lycanthropy', + 'meta: Terres de Leyt', + 'Strong', + 'AAH Vol. 1', + 'Force', + 'Jump', + 'Aboleths', + 'lol', + 'location', + 'small', + 'customizable', + 'Modern', + 'Sky', + 'portugues', + 'Hero', + 'Villain', + 'element', + 'Tyranny of Dragons', + 'Adventure Guide', + 'New Class', + 'Witchlight', + 'Shardblade', + 'Plateaux', + 'WOTC', + 'Snippet', + 'Terra', + 'Otherworldly Patron', + 'ritual', + 'hag', + 'Cyberpunk 2077', + 'tavern', + 'Artificer Specialist', + 'Werewolf', + 'Boesia', + 'vampiric', + 'monastic tradition', + 'Gothic', + 'celestial', + 'Unfinished', + 'Core', + 'Arcane Tradition', + 'Troll', + 'Origin', + 'Draconic', + 'dj9 member', + 'test', + 'Hag', + 'gem', + 'Invocations', + 'Dark Sun', + 'aarkhen', + 'How to', + 'ravenloft', + 'faerie', + 'Playtest', + 'Shaman', + 'dead', + 'Tomba Aniquilacio', + 'Pacto', + 'fullcaster', + 'Electric', + 'Ability Score', + '4D', + 'pathfinder', + 'insect', + 'hook', + 'page layout', + 'healing', + 'Lineages', + 'Flying', + 'Martial Arts', + 'journal', + 'Aide de jeu', + 'hunter', + 'headers', + 'Dark Souls', + 'courtyard', + 'crossroads', + 'Quest', + 'CotF', + 'defense', + 'Semryss', + 'invoked class', + 'Session Notes', + 'goblin', + 'infernal', + 'fate', + 'oni', + 'spellbook', + 'Summoning', + 'slut', + 'whore', + 'Greyhawk', + 'Mobility', + 'Reddit Remake', + 'Guild', + 'Cosmic Mart', + '7th Level', + 'dragonborn', + 'curse of strahd', + 'Ranger Subclass', + 'dossier', + 'dossie', + 'de', + 'pnpde', + 'Plane Shift', + 'halloween', + 'group:aventura', + '9th Level', + 'tome', + 'cold', + 'acid', + 'deprecated', + 'mind flayer', + 'MECHA', + 'EssentialsKit', + '2d6', + 'ToD', + 'Work In Progress', + 'Bond', + 'Versatile', + 'Dead', + 'SYWTBAGM', + 'summoning', + 'english', + 'Eilistraee', + 'Draft', + 'DoD', + 'map', + 'Frightened', + 'Psychic Damage', + 'eberron', + 'recompensa', + 'wizard subclass', + 'teiran', + 'Saltmarsh', + 'jp setting', + 'Illithid', + 'Longbow', + 'hell', + 'Monarch', + 'type:feat', + 'reglas', + 'cooking', + 'Abenteuer', + 'reloaded', + 'incompleto', + 'mecanica', + 'Location', + 'Grimlores Grimoire', + '2024 Subclass', + 'Chiesa di Toleno', + 'finalfantasy', + 'The Undertomes', + 'Lobotomy Corporation', + 'SDHTA', + 'D&D 2024', + 'other', + 'ally', + 'images', + 'Player\'s Guide', + 'Avalon Sword', + 'Cael\'Yuu', + 'dnd-2014', + 'Regelwerk', + 'Español', + 'br', + 'dnd 5.0', + 'monstro', + 'grand cemetery', + 'Phoenix', + 'dnd 2024', + 'Bloodhunter', + 'Sintonizacion', + 'dungeons & dragons', + 'Fix', + 'Rulebook', + 'Shadowdark', + 'heroic', + 'HFW', + 'Earthdawn', + '24e', + 'cormyr', + 'suzail', + 'dc20', + 'tag:Rules', + 'The Griffon\'s Saddlebag', + 'LOTM', + 'tag:Adventure', + 'drunken master', + 'eldritch invocation', + 'Персонаж', + 'Orcs', + 'Lizardfolk', + 'Frostfell', + 'CR 17', + 'Shapechanger', + 'Farmland', + 'Mages', + 'Any', + 'CR 13', + 'Earth', + 'Mountain', + 'Drake', + 'transformation', + 'GM', + 'Lich', + 'lovecraft', + 'unique', + 'Optional Rules', + 'int', + 'creator', + 'Primal', + 'simple', + 'golem', + 'Void', + 'Armour', + 'spellsword', + 'General', + 'Asian', + 'Bringers of chaos', + 'Optional Feature', + 'subraces', + 'Galanoth', + 'barbarian subclass', + 'felhearth', + 'modular', + 'Vampires', + 'wysteria', + 'adaptation', + 'beasts', + 'naruto', + 'ninja', + 'Psionic', + 'Guns', + 'Crystal', + 'Guardian', + 'NonProfit', + 'Mimic', + 'languages', + 'Epic Boons', + 'Primer', + 'Icewind Dale', + 'joke', + 'lycan', + 'CR3', + 'Armors', + 'ff7', + 'materia', + 'final fantasy 7 remake', + 'esper', + 'ff7 remake', + 'gargantuan', + 'Frog', + 'CR5', + 'blank', + 'monster hunter', + 'league of legends', + 'french', + 'Pokémon', + 'kobold', + 'soul', + 'ffxi', + 'd10', + 'Roman', + 'Cute', + 'DD5', + 'variant', + 'tree', + 'fr', + 'Scenario', + 'lycanthrope', + 'druide', + 'staff', + 'eios', + 'arkheneios', + 'Runic', + 'Work', + 'Ukrainian', + 'cover-page', + 'mage', + 'deities', + 'gods', + 'Boss Fight', + 'Lair', + 'WBTW', + 'roguish archetype', + 'Character Option', + 'Shortsword', + 'Illrigger', + 'Bloodborne', + 'cr6', + 'Priest', + 'Hamon', + 'Toonkind', + 'rol', + 'Strength', + 'forgotten realms', + 'Spanish', + 'Conclave', + 'Electro', + 'Magical Tattoos', + 'Matt Mercer', + 'Wildemount', + 'Mighty Nien', + 'Campaign 2', + 'Resistances', + 'Bug', + 'impression', + 'PF', + 'Magnus Archives', + 'ice', + 'speed', + 'Generic NPC', + 'Titanic', + 'Ink Friendly', + 'bleed', + 'elder scrolls', + 'Immortal', + 'LMOP', + 'Travel', + 'Olphus', + '3d6', + 'heist', + 'World History', + 'ghost', + 'genie', + 'kids on bikes', + 'Russia', + 'conclave', + 'overhaul', + 'manual', + 'Adventures In Eden', + 'Downtime', + 'hamon', + 'cloak', + 'shadowfell', + 'Hellfire', + 'Paladin Oath', + 'Genshin', + 'Nation', + 'air', + 'Magical Item', + 'War', + 'Original', + 'Monstrous Compendium', + 'Calamity', + 'Warden', + 'Apocalypse', + 'shield', + 'AC', + 'expansion', + 'Concentration', + 'charm', + 'Weave', + 'lycanthropy', + 'raza', + 'far realm', + 'fighter subclass', + 'ita', + 'Pirate', + 'Laranja', + 'Grapple', + 'EastByForce', + 'hobgoblin', + 'oneshot-notes', + 'Holy', + 'optional', + 'type:cenario', + 'group:core', + 'The Brewery', + 'Alcance', + 'Morrowind', + 'Indigo', + 'Divino', + '2nd Level', + 'Sub-Class', + 'cantrips', + 'Cloak', + 'battle master', + 'Dark', + 'Puzzle', + 'Lucky', + 'consumable', + 'rebalance', + 'Shove', + 'Area Control', + 'Vanguard', + 'funny', + 'e5', + 'Dragonlance', + 'psion', + 'initiative', + 'Tactician', + 'Inspiration', + 'artificier', + 'way', + 'inspired', + 'historia', + 'Medusa', + '2 part', + 'holy', + 'gift', + 'Nimble', + 'mostri', + 'phoenix', + 'travel', + 'Class Template', + 'Intimidation', + 'constructs', + 'P666', + 'Formatting', + 'Divinity', + 'Rod', + 'Language', + 'yokai', + 'rune', + 'western', + 'vampires', + 'flying', + 'cute', + 'Enemy', + 'boon', + 'Tables', + 'ShadowFight', + 'meta: Theme', + 'SCS', + 'vanthampur villa', + 'CoA', + 'shop', + 'destiny', + 'magical weapon', + 'Arcane Arcade', + 'XP to Level 3', + 'Dice Average RPG', + 'Pip-Boy', + 'Dragon Heist', + 'session notes', + 'tattoo', + 'flick', + 'P6:66', + 'Comic Character', + 'experiment', + 'Minerva', + 'type:Spellbook', + 'Realmfall', + 'Wand', + 'halfling', + 'sw5e', + 'implementar AP', + 'Mask', + 'Gazook89', + 'Weltenrauch-Chroniken', + 'MiA', + 'Made in Abyss', + 'français', + 'fae', + 'Lemuria', + 'Mork Borg', + 'guerrier', + 'prunus', + 'condition', + 'pf2', + 'tr', + 'costrutto', + 'German', + 'project moon', + '5r', + 'galles', + 'Project moon', + 'Yisang', + 'Spicebush', + 'player-accessible', + 'Especie', + 'Westmarch', + 'a', + 'Cart', + 'Magus', + 'group:Mchael Galvis', + 'tip', + 'werewolf', + 'mundane', + 'garrett', + 'unarmed', + 'Arcane Odyssey', + 'Tomb of Divinity', + 'pets', + 'Video Game', + '4 part', + 'pbta', + 'Druids', + 'multiclass', + 'manuale', + 'mimic', + 'plane shift', + 'Dotes', + 'Hechizos', + 'Infernal', + 'Enhanced', + 'done', + 'Mission report', + 'Blanks', + 'Masks', + 'Ultimate Ability', + 'shadow-slave', + 'Advertising', + 'transform', + 'Fullmetal Alchemist', + 'Fullmetal Alchemist Brotherhood', + 'tag:TAoF&F', + 'Dwarves', + 'Humans', + 'Nine Hells', + 'Devils', + 'Archons', + 'CR 15', + 'Troglodytes', + 'Goliath', + 'retired', + 'boots', + 'ranged', + 'shields', + 'Zhentarim', + 'World of Warcraft', + 'Frontline', + 'Guildmaster\'s Guide to Ravnica', + 'Dungeons & Dragons 5e', + 'beholder', + 'NEEDS FIXING', + 'mechanic', + 'Loot', + 'champion', + 'Runes', + 'Shield', + 'Punch', + 'Sniper', + 'Magical Girl', + 'NotDND', + 'story', + 'Sleep', + 'Bard College', + 'Illusion', + 'Thunder', + 'Defender', + 'Genasi', + 'troll', + 'Gehenna', + 'Yugoloth', + 'social', + 'Player Class', + 'homebrew class', + 'CR 16', + 'Ghost', + 'Kobolds', + 'Trolls', + 'Yuan-Ti', + 'Elder Scrolls Offline', + 'armure', + 'Mage', + 'CR 18', + 'Technology', + 'Mystery', + 'darkness', + 'Airship', + 'New Campaign', + 'Warframe', + 'Wizard Subclass', + 'Gold', + 'Candor', + 'Overhaul', + 'Dragon Knight', + 'Enoreth', + 'Artifacts', + 'New', + 'AMMO', + 'Campagne', + 'Valbise', + 'Subclasseptember', + 'Mecha', + 'Yu-Gi-Oh', + 'Goblinoid', + 'underwater', + 'SW5E', + 'bardo' +]; \ No newline at end of file diff --git a/client/homebrew/homebrew.jsx b/client/homebrew/homebrew.jsx index e38293bde..252b5ac11 100644 --- a/client/homebrew/homebrew.jsx +++ b/client/homebrew/homebrew.jsx @@ -1,8 +1,7 @@ -/* eslint-disable camelcase */ -import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers +import 'core-js/es/string/to-well-formed.js'; // Polyfill for older browsers import './homebrew.less'; import React from 'react'; -import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router'; +import { BrowserRouter as Router, Routes, Route, useParams, useSearchParams } from 'react-router'; import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js'; @@ -41,24 +40,34 @@ const Homebrew = (props)=>{ brews } = props; - global.account = account; - global.version = version; - global.config = config; - const backgroundObject = ()=>{ - if(global.config.deployment || (config.local && config.development)){ - const bgText = global.config.deployment || 'Local'; - return { - backgroundImage : `url("data:image/svg+xml;utf8,${bgText}")` - }; + if(config?.deployment || (config?.local && config?.development)) { + const bgText = config?.deployment || 'Local'; + return { + backgroundImage : `url("data:image/svg+xml;utf8,${bgText}")` + }; } return null; }; + updateLocalStorage(); + if(brew.pureError) { + return ( + +
    + + } /> + +
    +
    + ); + } + + return ( - -
    + +
    } /> } /> @@ -80,4 +89,4 @@ const Homebrew = (props)=>{ ); }; -module.exports = Homebrew; \ No newline at end of file +export default Homebrew; diff --git a/client/homebrew/homebrew.less b/client/homebrew/homebrew.less index 2cbc35857..9bafadffe 100644 --- a/client/homebrew/homebrew.less +++ b/client/homebrew/homebrew.less @@ -1,4 +1,4 @@ -@import 'naturalcrit/styles/core.less'; +@import '@sharedStyles/core.less'; .homebrew { height : 100%; background-color:@steel; diff --git a/client/homebrew/main.jsx b/client/homebrew/main.jsx new file mode 100644 index 000000000..635729d49 --- /dev/null +++ b/client/homebrew/main.jsx @@ -0,0 +1,6 @@ +import { createRoot } from 'react-dom/client'; +import Homebrew from './homebrew.jsx'; + +const props = window.__INITIAL_PROPS__ || {}; + +createRoot(document.getElementById('reactRoot')).render(); diff --git a/client/homebrew/navbar/account.navitem.jsx b/client/homebrew/navbar/account.navitem.jsx index bdc6d386a..0be06c471 100644 --- a/client/homebrew/navbar/account.navitem.jsx +++ b/client/homebrew/navbar/account.navitem.jsx @@ -1,9 +1,9 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const Nav = require('client/homebrew/navbar/nav.jsx'); -const request = require('superagent'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import request from 'superagent'; +import Nav from './nav.jsx'; -const Account = createClass({ +const Account = createReactClass({ displayName : 'AccountNavItem', getInitialState : function() { return { @@ -97,7 +97,7 @@ const Account = createClass({ // Logged out // LOCAL ONLY - if(global.config.local) { + if(global.config?.local) { return login ; @@ -111,4 +111,4 @@ const Account = createClass({ } }); -module.exports = Account; +export default Account; diff --git a/client/homebrew/navbar/error-navitem.jsx b/client/homebrew/navbar/error-navitem.jsx index 6b739379b..e8802e184 100644 --- a/client/homebrew/navbar/error-navitem.jsx +++ b/client/homebrew/navbar/error-navitem.jsx @@ -1,6 +1,6 @@ -require('./error-navitem.less'); -const React = require('react'); -const Nav = require('client/homebrew/navbar/nav.jsx'); +import './error-navitem.less'; +import React from 'react'; +import Nav from './nav.jsx'; const ErrorNavItem = ({ error = '', clearError })=>{ const response = error.response; @@ -144,4 +144,4 @@ const ErrorNavItem = ({ error = '', clearError })=>{ ; }; -module.exports = ErrorNavItem; +export default ErrorNavItem; diff --git a/client/homebrew/navbar/error-navitem.less b/client/homebrew/navbar/error-navitem.less index 637ddac95..4da665915 100644 --- a/client/homebrew/navbar/error-navitem.less +++ b/client/homebrew/navbar/error-navitem.less @@ -1,3 +1,5 @@ +@import '@sharedStyles/core.less'; + .navItem.error { position : relative; background-color : @red; diff --git a/client/homebrew/navbar/help.navitem.jsx b/client/homebrew/navbar/help.navitem.jsx index 0b1ff6c3a..4e012d9a7 100644 --- a/client/homebrew/navbar/help.navitem.jsx +++ b/client/homebrew/navbar/help.navitem.jsx @@ -1,9 +1,9 @@ -const React = require('react'); -const dedent = require('dedent-tabs').default; +import React from 'react'; +import dedent from 'dedent'; -const Nav = require('client/homebrew/navbar/nav.jsx'); +import Nav from './nav.jsx'; -module.exports = function(props){ +export default function(props){ return need help? diff --git a/client/homebrew/navbar/metadata.navitem.jsx b/client/homebrew/navbar/metadata.navitem.jsx index 6eadac367..8ee6b72c0 100644 --- a/client/homebrew/navbar/metadata.navitem.jsx +++ b/client/homebrew/navbar/metadata.navitem.jsx @@ -1,11 +1,11 @@ -const React = require('react'); -const createClass = require('create-react-class'); -const Moment = require('moment'); +import React from 'react'; +import createReactClass from 'create-react-class'; +import Moment from 'moment'; -const Nav = require('client/homebrew/navbar/nav.jsx'); +import Nav from './nav.jsx'; -const MetadataNav = createClass({ +const MetadataNav = createReactClass({ displayName : 'MetadataNav', getDefaultProps : function() { return { @@ -46,11 +46,6 @@ const MetadataNav = createClass({ ; }, - getSystems : function(){ - if(!this.props.brew.systems || this.props.brew.systems.length == 0) return 'No systems'; - return this.props.brew.systems.join(', '); - }, - renderMetaWindow : function(){ return
    @@ -65,10 +60,6 @@ const MetadataNav = createClass({

    Tags

    {this.getTags()}

    -
    -

    Systems

    -

    {this.getSystems()}

    -

    Updated

    {Moment(this.props.brew.updatedAt).fromNow()}

    @@ -86,4 +77,4 @@ const MetadataNav = createClass({ }); -module.exports = MetadataNav; +export default MetadataNav; diff --git a/client/homebrew/navbar/nav.jsx b/client/homebrew/navbar/nav.jsx index 16f194716..9e065e0ee 100644 --- a/client/homebrew/navbar/nav.jsx +++ b/client/homebrew/navbar/nav.jsx @@ -1,14 +1,13 @@ -require('client/homebrew/navbar/navbar.less'); -const React = require('react'); -const { useState, useRef, useEffect } = React; -const createClass = require('create-react-class'); -const _ = require('lodash'); -const cx = require('classnames'); +import './navbar.less'; +import React, { useState, useRef, useEffect } from 'react'; +import createReactClass from 'create-react-class'; +import _ from 'lodash'; +import cx from 'classnames'; -const NaturalCritIcon = require('client/components/svg/naturalcrit-d20.svg.jsx'); +import NaturalCritIcon from '../../components/svg/naturalcrit-d20.svg.jsx'; const Nav = { - base : createClass({ + base : createReactClass({ displayName : 'Nav.base', render : function(){ return