0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-03-22 08:58:11 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into update-dependencies

This commit is contained in:
Víctor Losada Hernández
2026-02-24 20:45:59 +01:00
30 changed files with 2636 additions and 237 deletions

View File

@@ -12,6 +12,7 @@ const CodeEditor = createReactClass({
getDefaultProps : function() { getDefaultProps : function() {
return { return {
language : '', language : '',
tab : 'brewText',
value : '', value : '',
wrap : true, wrap : true,
onChange : ()=>{}, onChange : ()=>{},
@@ -186,6 +187,22 @@ const CodeEditor = createReactClass({
this.updateSize(); 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 () { indent : function () {
const cm = this.codeMirror; const cm = this.codeMirror;
if(cm.somethingSelected()) { if(cm.somethingSelected()) {
@@ -200,6 +217,7 @@ const CodeEditor = createReactClass({
}, },
makeHeader : function (number) { makeHeader : function (number) {
if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection(); const selection = this.codeMirror?.getSelection();
const header = Array(number).fill('#').join(''); const header = Array(number).fill('#').join('');
this.codeMirror?.replaceSelection(`${header} ${selection}`, 'around'); this.codeMirror?.replaceSelection(`${header} ${selection}`, 'around');
@@ -208,6 +226,7 @@ const CodeEditor = createReactClass({
}, },
makeBold : function() { makeBold : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**'; 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){ if(selection.length === 0){
@@ -217,7 +236,8 @@ const CodeEditor = createReactClass({
}, },
makeItalic : function() { makeItalic : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*'; 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){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror?.getCursor();
@@ -226,7 +246,8 @@ const CodeEditor = createReactClass({
}, },
makeSuper : function() { makeSuper : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 1) === '^' && selection.slice(-1) === '^'; 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){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror?.getCursor();
@@ -235,7 +256,8 @@ const CodeEditor = createReactClass({
}, },
makeSub : function() { makeSub : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '^^' && selection.slice(-2) === '^^'; 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){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror?.getCursor();
@@ -245,10 +267,12 @@ const CodeEditor = createReactClass({
makeNbsp : function() { makeNbsp : function() {
if(!this.isGFM()) return;
this.codeMirror?.replaceSelection(' ', 'end'); this.codeMirror?.replaceSelection(' ', 'end');
}, },
makeSpace : function() { makeSpace : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection(); const selection = this.codeMirror?.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){ if(t){
@@ -260,6 +284,7 @@ const CodeEditor = createReactClass({
}, },
removeSpace : function() { removeSpace : function() {
if(!this.isGFM()) return;
const selection = this.codeMirror?.getSelection(); const selection = this.codeMirror?.getSelection();
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}'; const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
if(t){ if(t){
@@ -269,10 +294,12 @@ const CodeEditor = createReactClass({
}, },
newColumn : function() { newColumn : function() {
if(!this.isGFM()) return;
this.codeMirror?.replaceSelection('\n\\column\n\n', 'end'); this.codeMirror?.replaceSelection('\n\\column\n\n', 'end');
}, },
newPage : function() { newPage : function() {
if(!this.isGFM()) return;
this.codeMirror?.replaceSelection('\n\\page\n\n', 'end'); this.codeMirror?.replaceSelection('\n\\page\n\n', 'end');
}, },
@@ -286,7 +313,8 @@ const CodeEditor = createReactClass({
}, },
makeUnderline : function() { makeUnderline : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>'; if(!this.isGFM()) return;
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 3) === '<u>' && selection.slice(-4) === '</u>';
this.codeMirror?.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around'); this.codeMirror?.replaceSelection(t ? selection.slice(3, -4) : `<u>${selection}</u>`, 'around');
if(selection.length === 0){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror?.getCursor();
@@ -295,7 +323,8 @@ const CodeEditor = createReactClass({
}, },
makeSpan : function() { makeSpan : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; 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){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror?.getCursor();
@@ -304,7 +333,8 @@ const CodeEditor = createReactClass({
}, },
makeDiv : function() { makeDiv : function() {
const selection = this.codeMirror?.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}'; 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){ if(selection.length === 0){
const cursor = this.codeMirror?.getCursor(); const cursor = this.codeMirror?.getCursor();
@@ -317,7 +347,7 @@ const CodeEditor = createReactClass({
let cursorPos; let cursorPos;
let newComment; let newComment;
const selection = this.codeMirror?.getSelection(); const selection = this.codeMirror?.getSelection();
if(this.props.language === 'gfm'){ if(this.isGFM()){
regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs; regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs;
cursorPos = 4; cursorPos = 4;
newComment = `<!-- ${selection} -->`; newComment = `<!-- ${selection} -->`;
@@ -334,6 +364,7 @@ const CodeEditor = createReactClass({
}, },
makeLink : function() { makeLink : function() {
if(!this.isGFM()) return;
const isLink = /^\[(.*)\]\((.*)\)$/; const isLink = /^\[(.*)\]\((.*)\)$/;
const selection = this.codeMirror?.getSelection().trim(); const selection = this.codeMirror?.getSelection().trim();
let match; let match;
@@ -351,7 +382,8 @@ const CodeEditor = createReactClass({
}, },
makeList : function(listType) { makeList : function(listType) {
const selectionStart = this.codeMirror?.getCursor('from'), selectionEnd = this.codeMirror?.getCursor('to'); 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: selectionStart.line, ch: 0 },
{ line: selectionEnd.line, ch: this.codeMirror?.getLine(selectionEnd.line).length } { line: selectionEnd.line, ch: this.codeMirror?.getLine(selectionEnd.line).length }

View File

@@ -11,11 +11,13 @@ const Combobox = createReactClass({
trigger : 'hover', trigger : 'hover',
default : '', default : '',
placeholder : '', placeholder : '',
tooltip: '',
autoSuggest : { autoSuggest : {
clearAutoSuggestOnClick : true, clearAutoSuggestOnClick : true,
suggestMethod : 'includes', suggestMethod : 'includes',
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
}, },
valuePatterns: /.+/
}; };
}, },
getInitialState : function() { getInitialState : function() {
@@ -69,11 +71,14 @@ const Combobox = createReactClass({
return ( return (
<div className='dropdown-input item' <div className='dropdown-input item'
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined} onMouseEnter={this.props.trigger == 'hover' ? ()=>{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 } : {})}>
<input <input
type='text' type='text'
onChange={(e)=>this.handleInput(e)} onChange={(e)=>this.handleInput(e)}
value={this.state.value || ''} value={this.state.value || ''}
title=''
pattern={this.props.valuePatterns}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
onBlur={(e)=>{ onBlur={(e)=>{
if(!e.target.checkValidity()){ if(!e.target.checkValidity()){
@@ -82,6 +87,12 @@ const Combobox = createReactClass({
}); });
} }
}} }}
onKeyDown={(e)=>{
if (e.key === "Enter") {
e.preventDefault();
this.props.onEntry(e);
}
}}
/> />
<i className='fas fa-caret-down'/> <i className='fas fa-caret-down'/>
</div> </div>

View File

@@ -10,6 +10,7 @@
position : absolute; position : absolute;
z-index : 100; z-index : 100;
width : 100%; width : 100%;
height : max-content;
max-height : 200px; max-height : 200px;
overflow-y : auto; overflow-y : auto;
background-color : white; background-color : white;

View File

@@ -99,18 +99,18 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
return ( return (
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'> <div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
<div className='toggleButton'> <div className='toggleButton'>
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{ <button data-tooltip-right={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
setToolsVisible(!toolsVisible); setToolsVisible(!toolsVisible);
localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible); localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible);
}}><i className='fas fa-glasses' /></button> }}><i className='fas fa-glasses' /></button>
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button> <button data-tooltip-right={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
</div> </div>
{/*v=====----------------------< Zoom Controls >---------------------=====v*/} {/*v=====----------------------< Zoom Controls >---------------------=====v*/}
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}> <div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
<button <button
id='fill-width' id='fill-width'
className='tool' className='tool'
title='Set zoom to fill preview with one page' data-tooltip-bottom='Set zoom to fill preview with one page'
onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fill'))} onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fill'))}
> >
<i className='fac fit-width' /> <i className='fac fit-width' />
@@ -118,7 +118,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
<button <button
id='zoom-to-fit' id='zoom-to-fit'
className='tool' className='tool'
title='Set zoom to fit entire page in preview' data-tooltip-bottom='Set zoom to fit entire page in preview'
onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fit'))} onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fit'))}
> >
<i className='fac zoom-to-fit' /> <i className='fac zoom-to-fit' />
@@ -128,7 +128,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
className='tool' className='tool'
onClick={()=>handleZoomButton(displayOptions.zoomLevel - 20)} onClick={()=>handleZoomButton(displayOptions.zoomLevel - 20)}
disabled={displayOptions.zoomLevel <= MIN_ZOOM} disabled={displayOptions.zoomLevel <= MIN_ZOOM}
title='Zoom Out' data-tooltip-bottom='Zoom Out'
> >
<i className='fas fa-magnifying-glass-minus' /> <i className='fas fa-magnifying-glass-minus' />
</button> </button>
@@ -137,7 +137,6 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
className='range-input tool hover-tooltip' className='range-input tool hover-tooltip'
type='range' type='range'
name='zoom' name='zoom'
title='Set Zoom'
list='zoomLevels' list='zoomLevels'
min={MIN_ZOOM} min={MIN_ZOOM}
max={MAX_ZOOM} max={MAX_ZOOM}
@@ -154,7 +153,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
className='tool' className='tool'
onClick={()=>handleZoomButton(displayOptions.zoomLevel + 20)} onClick={()=>handleZoomButton(displayOptions.zoomLevel + 20)}
disabled={displayOptions.zoomLevel >= MAX_ZOOM} disabled={displayOptions.zoomLevel >= MAX_ZOOM}
title='Zoom In' data-tooltip-bottom='Zoom In'
> >
<i className='fas fa-magnifying-glass-plus' /> <i className='fas fa-magnifying-glass-plus' />
</button> </button>
@@ -166,44 +165,44 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
<button role='radio' <button role='radio'
id='single-spread' id='single-spread'
className='tool' className='tool'
title='Single Page' data-tooltip-bottom='Single Page'
onClick={()=>{handleOptionChange('spread', 'single');}} onClick={()=>{handleOptionChange('spread', 'single');}}
aria-checked={displayOptions.spread === 'single'} aria-checked={displayOptions.spread === 'single'}
><i className='fac single-spread' /></button> ><i className='fac single-spread' /></button>
<button role='radio' <button role='radio'
id='facing-spread' id='facing-spread'
className='tool' className='tool'
title='Facing Pages' data-tooltip-bottom='Facing Pages'
onClick={()=>{handleOptionChange('spread', 'facing');}} onClick={()=>{handleOptionChange('spread', 'facing');}}
aria-checked={displayOptions.spread === 'facing'} aria-checked={displayOptions.spread === 'facing'}
><i className='fac facing-spread' /></button> ><i className='fac facing-spread' /></button>
<button role='radio' <button role='radio'
id='flow-spread' id='flow-spread'
className='tool' className='tool'
title='Flow Pages' data-tooltip-bottom='Flow Pages'
onClick={()=>{handleOptionChange('spread', 'flow');}} onClick={()=>{handleOptionChange('spread', 'flow');}}
aria-checked={displayOptions.spread === 'flow'} aria-checked={displayOptions.spread === 'flow'}
><i className='fac flow-spread' /></button> ><i className='fac flow-spread' /></button>
</div> </div>
<Anchored> <Anchored>
<AnchoredTrigger id='spread-settings' className='tool' title='Spread options'><i className='fas fa-gear' /></AnchoredTrigger> <AnchoredTrigger id='spread-settings' className='tool' data-tooltip-bottom='Spread options'><i className='fas fa-gear' /></AnchoredTrigger>
<AnchoredBox title='Options'> <AnchoredBox>
<h1>Options</h1> <h1>Options</h1>
<label title='Modify the horizontal space between pages.'> <label data-tooltip-left='Modify the horizontal space between pages.'>
Column gap Column gap
<input type='range' min={0} max={200} defaultValue={displayOptions.columnGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} /> <input type='range' min={0} max={200} defaultValue={displayOptions.columnGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
</label> </label>
<label title='Modify the vertical space between rows of pages.'> <label data-tooltip-left='Modify the vertical space between rows of pages.'>
Row gap Row gap
<input type='range' min={0} max={200} defaultValue={displayOptions.rowGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} /> <input type='range' min={0} max={200} defaultValue={displayOptions.rowGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
</label> </label>
<label title='Start 1st page on the right side, such as if you have cover page.'> <label data-tooltip-left='Start 1st page on the right side, such as if you have cover page.'>
Start on right Start on right
<input type='checkbox' checked={displayOptions.startOnRight} onChange={()=>{handleOptionChange('startOnRight', !displayOptions.startOnRight);}} <input type='checkbox' checked={displayOptions.startOnRight} onChange={()=>{handleOptionChange('startOnRight', !displayOptions.startOnRight);}}
title={displayOptions.spread !== 'facing' ? 'Switch to Facing to enable toggle.' : null} /> data-tooltip-right={displayOptions.spread !== 'facing' ? 'Switch to Facing to enable toggle.' : null} />
</label> </label>
<label title='Toggle the page shadow on every page.'> <label data-tooltip-left='Toggle the page shadow on every page.'>
Page shadows Page shadows
<input type='checkbox' checked={displayOptions.pageShadows} onChange={()=>{handleOptionChange('pageShadows', !displayOptions.pageShadows);}} /> <input type='checkbox' checked={displayOptions.pageShadows} onChange={()=>{handleOptionChange('pageShadows', !displayOptions.pageShadows);}} />
</label> </label>
@@ -217,7 +216,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
id='previous-page' id='previous-page'
className='previousPage tool' className='previousPage tool'
type='button' type='button'
title='Previous Page(s)' data-tooltip-bottom='Previous Page(s)'
onClick={()=>scrollToPage(_.min(visiblePages) - visiblePages.length)} onClick={()=>scrollToPage(_.min(visiblePages) - visiblePages.length)}
disabled={visiblePages.includes(1)} disabled={visiblePages.includes(1)}
> >
@@ -230,7 +229,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
className='text-input' className='text-input'
type='text' type='text'
name='page' name='page'
title='Current page(s) in view' data-tooltip-bottom='Current page(s) in view'
inputMode='numeric' inputMode='numeric'
pattern='[0-9]' pattern='[0-9]'
value={pageNum} value={pageNum}
@@ -240,14 +239,14 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)} onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
style={{ width: `${pageNum.length}ch` }} style={{ width: `${pageNum.length}ch` }}
/> />
<span id='page-count' title='Total Page Count'>/ {totalPages}</span> <span id='page-count' data-tooltip-bottom='Total Page Count'>/ {totalPages}</span>
</div> </div>
<button <button
id='next-page' id='next-page'
className='tool' className='tool'
type='button' type='button'
title='Next Page(s)' data-tooltip-bottom='Next Page(s)'
onClick={()=>scrollToPage(_.max(visiblePages) + 1)} onClick={()=>scrollToPage(_.max(visiblePages) + 1)}
disabled={visiblePages.includes(totalPages)} disabled={visiblePages.includes(totalPages)}
> >

View File

@@ -166,7 +166,7 @@
&.hidden { &.hidden {
flex-wrap : nowrap; flex-wrap : nowrap;
width : 92px; width : 50%;
overflow : hidden; overflow : hidden;
background-color : unset; background-color : unset;
opacity : 0.7; opacity : 0.7;

View File

@@ -86,9 +86,9 @@ const Editor = createReactClass({
}); });
} }
const snippetBar = document.querySelector('.editor > .snippetBar'); const snippetBar = document.querySelector('.editor > .snippetBar');
if (!snippetBar) return; if(!snippetBar) return;
this.resizeObserver = new ResizeObserver(entries => { this.resizeObserver = new ResizeObserver(entries=>{
const height = document.querySelector('.editor > .snippetBar').offsetHeight; const height = document.querySelector('.editor > .snippetBar').offsetHeight;
this.setState({ snippetBarHeight: height }); this.setState({ snippetBarHeight: height });
}); });
@@ -117,7 +117,7 @@ const Editor = createReactClass({
}, },
componentWillUnmount() { componentWillUnmount() {
if (this.resizeObserver) this.resizeObserver.disconnect(); if(this.resizeObserver) this.resizeObserver.disconnect();
}, },
handleControlKeys : function(e){ handleControlKeys : function(e){
@@ -337,7 +337,7 @@ const Editor = createReactClass({
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0]; const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
const currentPos = brewRenderer.scrollTop; const currentPos = brewRenderer.scrollTop;
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top; const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
let scrollingTimeout; let scrollingTimeout;
const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times const checkIfScrollComplete = ()=>{ // Prevent interrupting a scroll in progress if user clicks multiple times
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
@@ -392,7 +392,7 @@ const Editor = createReactClass({
isJumping = true; isJumping = true;
checkIfScrollComplete(); checkIfScrollComplete();
if (this.codeEditor.current?.codeMirror) { if(this.codeEditor.current?.codeMirror) {
this.codeEditor.current.codeMirror?.on('scroll', checkIfScrollComplete); this.codeEditor.current.codeMirror?.on('scroll', checkIfScrollComplete);
} }
@@ -442,6 +442,7 @@ const Editor = createReactClass({
<CodeEditor key='codeEditor' <CodeEditor key='codeEditor'
ref={this.codeEditor} ref={this.codeEditor}
language='gfm' language='gfm'
tab='brewText'
view={this.state.view} view={this.state.view}
value={this.props.brew.text} value={this.props.brew.text}
onChange={this.props.onBrewChange('text')} onChange={this.props.onBrewChange('text')}
@@ -455,6 +456,7 @@ const Editor = createReactClass({
<CodeEditor key='codeEditor' <CodeEditor key='codeEditor'
ref={this.codeEditor} ref={this.codeEditor}
language='css' language='css'
tab='brewStyles'
view={this.state.view} view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT} value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onBrewChange('style')} onChange={this.props.onBrewChange('style')}
@@ -484,6 +486,7 @@ const Editor = createReactClass({
<CodeEditor key='codeEditor' <CodeEditor key='codeEditor'
ref={this.codeEditor} ref={this.codeEditor}
language='gfm' language='gfm'
tab='brewSnippets'
view={this.state.view} view={this.state.view}
value={this.props.brew.snippets} value={this.props.brew.snippets}
onChange={this.props.onBrewChange('snippets')} onChange={this.props.onBrewChange('snippets')}

View File

@@ -4,6 +4,7 @@
width : 100%; width : 100%;
height : 100%; height : 100%;
container : editor / inline-size; container : editor / inline-size;
background:white;
.codeEditor { .codeEditor {
height : calc(100% - 25px); height : calc(100% - 25px);
.CodeMirror { height : 100%; } .CodeMirror { height : 100%; }

View File

@@ -10,8 +10,6 @@ import TagInput from '../tagInput/tagInput.jsx';
import Themes from 'themes/themes.json'; import Themes from 'themes/themes.json';
import validations from './validations.js'; import validations from './validations.js';
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
import homebreweryThumbnail from '../../thumbnail.png'; import homebreweryThumbnail from '../../thumbnail.png';
const callIfExists = (val, fn, ...args)=>{ const callIfExists = (val, fn, ...args)=>{
@@ -33,7 +31,6 @@ const MetadataEditor = createReactClass({
tags : [], tags : [],
published : false, published : false,
authors : [], authors : [],
systems : [],
renderer : 'legacy', renderer : 'legacy',
theme : '5ePHB', theme : '5ePHB',
lang : 'en' lang : 'en'
@@ -91,15 +88,6 @@ const MetadataEditor = createReactClass({
} }
}, },
handleSystem : function(system, e){
if(e.target.checked){
this.props.metadata.systems.push(system);
} else {
this.props.metadata.systems = _.without(this.props.metadata.systems, system);
}
this.props.onChange(this.props.metadata);
},
handleRenderer : function(renderer, e){ handleRenderer : function(renderer, e){
if(e.target.checked){ if(e.target.checked){
this.props.metadata.renderer = renderer; this.props.metadata.renderer = renderer;
@@ -155,18 +143,6 @@ const MetadataEditor = createReactClass({
}); });
}, },
renderSystems : function(){
return _.map(SYSTEMS, (val)=>{
return <label key={val}>
<input
type='checkbox'
checked={_.includes(this.props.metadata.systems, val)}
onChange={(e)=>this.handleSystem(val, e)} />
{val}
</label>;
});
},
renderPublish : function(){ renderPublish : function(){
if(this.props.metadata.published){ if(this.props.metadata.published){
return <button className='unpublish' onClick={()=>this.handlePublish(false)}> return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
@@ -237,7 +213,7 @@ const MetadataEditor = createReactClass({
</div>; </div>;
} else { } else {
dropdown = dropdown =
<div className='value'> <div className='value' data-tooltip-top='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.'>
<Combobox trigger='click' <Combobox trigger='click'
className='themes-dropdown' className='themes-dropdown'
default={currentThemeDisplay} default={currentThemeDisplay}
@@ -255,7 +231,6 @@ const MetadataEditor = createReactClass({
filterOn : ['value', 'title'] filterOn : ['value', 'title']
}} }}
/> />
<small>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.</small>
</div>; </div>;
} }
@@ -280,7 +255,7 @@ const MetadataEditor = createReactClass({
return <div className='field language'> return <div className='field language'>
<label>language</label> <label>language</label>
<div className='value'> <div className='value' data-tooltip-right='Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.'>
<Combobox trigger='click' <Combobox trigger='click'
className='language-dropdown' className='language-dropdown'
default={this.props.metadata.lang || ''} default={this.props.metadata.lang || ''}
@@ -297,14 +272,13 @@ const MetadataEditor = createReactClass({
filterOn : ['value', 'detail', 'title'] filterOn : ['value', 'detail', 'title']
}} }}
/> />
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
</div> </div>
</div>; </div>;
}, },
renderRenderOptions : function(){ renderRenderOptions : function(){
return <div className='field systems'> return <div className='field renderers'>
<label>Renderer</label> <label>Renderer</label>
<div className='value'> <div className='value'>
<label key='legacy'> <label key='legacy'>
@@ -363,18 +337,20 @@ const MetadataEditor = createReactClass({
{this.renderThumbnail()} {this.renderThumbnail()}
</div> </div>
<TagInput label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]} <div className="field tags">
placeholder='add tag' unique={true} <label>Tags</label>
values={this.props.metadata.tags} <div className="value" >
onChange={(e)=>this.handleFieldChange('tags', e)} <TagInput
/> label='tags'
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
<div className='field systems'> placeholder='add tag' unique={true}
<label>systems</label> values={this.props.metadata.tags}
<div className='value'> onChange={(e)=>this.handleFieldChange('tags', e)}
{this.renderSystems()} tooltip='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
/>
</div> </div>
</div> </div>
{this.renderLanguageDropdown()} {this.renderLanguageDropdown()}
@@ -386,13 +362,22 @@ const MetadataEditor = createReactClass({
{this.renderAuthors()} {this.renderAuthors()}
<TagInput label='invited authors' valuePatterns={[/.+/]} <div className="field invitedAuthors">
validators={[(v)=>!this.props.metadata.authors?.includes(v)]} <label>Invited authors</label>
placeholder='invite author' unique={true} <div className="value">
values={this.props.metadata.invitedAuthors} <TagInput
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.']} label='invited authors'
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)} valuePatterns={/.+/}
/> validators={[(v)=>!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)}
/>
</div>
</div>
<h2>Privacy</h2> <h2>Privacy</h2>

View File

@@ -44,8 +44,6 @@
gap : 10px; gap : 10px;
} }
.field { .field {
position : relative; position : relative;
display : flex; display : flex;
@@ -62,6 +60,7 @@
& > .value { & > .value {
flex : 1 1 auto; flex : 1 1 auto;
width : 50px; width : 50px;
&[data-tooltip-right] { max-width : 380px; }
&:invalid { background : #FFB9B9; } &:invalid { background : #FFB9B9; }
small { small {
display : block; display : block;
@@ -74,6 +73,16 @@
border : 1px solid gray; border : 1px solid gray;
&:focus { outline : 1px solid #444444; } &:focus { outline : 1px solid #444444; }
} }
&.description {
flex : 1;
textarea.value {
height : auto;
font-family : 'Open Sans', sans-serif;
resize : none;
}
}
&.thumbnail, &.themes { &.thumbnail, &.themes {
label { line-height : 2.0em; } label { line-height : 2.0em; }
.value { .value {
@@ -90,6 +99,15 @@
} }
} }
&.tags .tagInput-dropdown {
z-index : 400;
max-width : 200px;
}
&.language .value {
z-index : 300;
max-width : 150px;
}
&.themes { &.themes {
.value { .value {
overflow : visible; overflow : visible;
@@ -101,22 +119,13 @@
} }
} }
&.description { &.invitedAuthors .value {
flex : 1; z-index : 100;
textarea.value {
height : auto; .tagInput-dropdown { max-width : 200px; }
font-family : 'Open Sans', sans-serif;
resize : none;
}
}
&.language .language-dropdown {
z-index : 200;
max-width : 150px;
} }
} }
.thumbnail-preview { .thumbnail-preview {
position : relative; position : relative;
flex : 1 1; flex : 1 1;
@@ -129,7 +138,7 @@
background-color : #AAAAAA; background-color : #AAAAAA;
} }
.systems.field .value { .renderers.field .value {
label { label {
display : inline-flex; display : inline-flex;
align-items : center; align-items : center;
@@ -169,7 +178,7 @@
.themes.field { .themes.field {
& .dropdown-container { & .dropdown-container {
position : relative; position : relative;
z-index : 100; z-index : 200;
background-color : white; background-color : white;
} }
& .dropdown-options { overflow-y : visible; } & .dropdown-options { overflow-y : visible; }

View File

@@ -0,0 +1,210 @@
export default [
// ############################## 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",
];

View File

@@ -1,102 +1,209 @@
import './tagInput.less'; import "./tagInput.less";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import _ from 'lodash'; import Combobox from "../../../components/combobox.jsx";
const TagInput = ({ unique = true, values = [], ...props })=>{ import tagSuggestionList from "./curatedTagSuggestionList.js";
const [tempInputText, setTempInputText] = useState('');
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false })));
useEffect(()=>{ const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, placeholder = "", smallText = "", onChange }) => {
handleChange(tagList.map((context)=>context.value)); const [tagList, setTagList] = useState(
values.map((value) => ({
value,
editing: false,
draft: "",
})),
);
useEffect(() => {
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]); }, [tagList]);
const handleChange = (value)=>{ // substrings to be normalized to the first value on the array
props.onChange({ const duplicateGroups = [
target : { value } ["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"],
];
const handleInputKeyDown = ({ evt, value, index, options = {} })=>{ const normalizeValue = (input) => {
if(_.includes(['Enter', ','], evt.key)) { const lowerInput = input.toLowerCase();
evt.preventDefault(); let normalizedTag = input;
submitTag(evt.target.value, value, index);
if(options.clear) { for (const group of duplicateGroups) {
setTempInputText(''); 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)=>{ const submitTag = (newValue, index = null) => {
setTagList((prevContext)=>{ const trimmed = newValue?.trim();
// remove existing tag if (!trimmed) return;
if(newValue === null){ if (!valuePatterns.test(trimmed)) return;
return [...prevContext].filter((context, i)=>i !== index);
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 [...prev, { value: normalizedTag, editing: false }];
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;
});
}); });
}; };
const editTag = (index)=>{ const removeTag = (index) => {
setTagList((prevContext)=>{ setTagList((prev) => prev.filter((_, i) => i !== index));
return prevContext.map((context, i)=>{
if(i === index) {
return { ...context, editing: true };
}
return { ...context, editing: false };
});
});
}; };
const renderReadTag = (context, index)=>{ const editTag = (index) => {
return ( setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: true, draft: t.value } : t)));
<li key={index}
data-value={context.value}
className='tag'
onClick={()=>editTag(index)}>
{context.value}
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index);}}><i className='fa fa-times fa-fw'/></button>
</li>
);
}; };
const renderWriteTag = (context, index)=>{ const stopEditing = (index) => {
return ( setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: false, draft: "" } : t)));
<input type='text'
key={index}
defaultValue={context.value}
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index: index })}
autoFocus
/>
);
}; };
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 (
<div className={classes} key={`tag-${tag}`} value={tag} data={tag}>
{tag}
</div>
);
});
return ( return (
<div className='field'> <div className="tagInputWrap">
<label>{props.label}</label> <Combobox
<div className='value'> trigger="click"
<ul className='list'> className="tagInput-dropdown"
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })} default=""
</ul> placeholder={placeholder}
options={label === "tags" ? suggestionOptions : []}
<input tooltip={tooltip}
type='text' autoSuggest={
className='value' label === "tags"
placeholder={props.placeholder} ? {
value={tempInputText} suggestMethod: "startsWith",
onChange={(e)=>setTempInputText(e.target.value)} clearAutoSuggestOnClick: true,
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })} filterOn: ["value", "title"],
/> }
</div> : { suggestMethod: "includes", clearAutoSuggestOnClick: true, filterOn: [] }
}
valuePatterns={valuePatterns.source}
onSelect={(value) => submitTag(value)}
onEntry={(e) => {
if (e.key === "Enter") {
e.preventDefault();
submitTag(e.target.value);
}
}}
/>
<ul className="list">
{tagList.map((t, i) =>
t.editing ? (
<input
key={i}
type="text"
value={t.draft} // always use draft
pattern={valuePatterns.source}
onChange={(e) =>
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
/>
) : (
<li key={i} className="tag" onClick={() => editTag(i)}>
{t.value}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeTag(i);
}}>
<i className="fa fa-times fa-fw" />
</button>
</li>
),
)}
</ul>
</div> </div>
); );
}; };

View File

@@ -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;
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -36,7 +36,7 @@ After clicking the "Print" item in the navbar a new page will open and a print d
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew! If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
}} }}
![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px} ![homebrew mug](https://homebrewery.naturalcrit.com/assets/homebrewerymug.png) {position:absolute,bottom:20px,left:130px,width:220px}
{{artist,bottom:160px,left:100px {{artist,bottom:160px,left:100px
##### Homebrew Mug ##### Homebrew Mug
@@ -77,16 +77,16 @@ If you wish to sell or in some way gain profit for what's created on this site,
If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery. If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery.
### More Homebrew Resources ### More Homebrew Resources
[![Discord](/assets/discordOfManyThings.svg){width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx) [![Discord](https://homebrewery.naturalcrit.com/assets/discordOfManyThings.svg){width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback. If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback.
{{position:absolute;top:20px;right:20px;width:auto {{position:absolute;top:20px;right:20px;width:auto
[![Discord](/assets/discord.png){height:30px}](https://discord.gg/by3deKx) [![Discord](https://homebrewery.naturalcrit.com/assets/discord.png){height:30px}](https://discord.gg/by3deKx)
[![Github](/assets/github.png){height:30px}](https://github.com/naturalcrit/homebrewery) [![Github](https://homebrewery.naturalcrit.com/assets/github.png){height:30px}](https://github.com/naturalcrit/homebrewery)
[![Patreon](/assets/patreon.png){height:30px}](https://patreon.com/NaturalCrit) [![Patreon](https://homebrewery.naturalcrit.com/assets/patreon.png){height:30px}](https://patreon.com/NaturalCrit)
[![Reddit](/assets/reddit.png){height:30px}](https://www.reddit.com/r/homebrewery/) [![Reddit](https://homebrewery.naturalcrit.com/assets/reddit.png){height:30px}](https://www.reddit.com/r/homebrewery/)
}} }}
\page \page
@@ -162,7 +162,7 @@ Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax. Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
![alt-text](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:100px,border:"2px solid",border-radius:10px} ![alt-text](https://homebrewery.naturalcrit.com/assets/catwarrior.jpg) {width:100px,border:"2px solid",border-radius:10px}
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interface.* \* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interface.*

View File

@@ -61,10 +61,11 @@
"server" "server"
], ],
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"node_modules/(?!(nanoid|@exodus/bytes|parse5)/)" "node_modules/(?!(nanoid|@exodus/bytes|parse5|@asamuzakjp|@csstools)/)"
], ],
"transform": { "transform": {
"^.+\\.js$": "babel-jest" "^.+\\.[jt]s$": "babel-jest",
"^.+\\.mjs$": "babel-jest"
}, },
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
"build/*" "build/*"
@@ -94,7 +95,7 @@
"@babel/preset-react": "^7.28.5", "@babel/preset-react": "^7.28.5",
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"@dmsnell/diff-match-patch": "^1.1.0", "@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^19.2.0", "@googleapis/drive": "^20.1.0",
"@sanity/diff-match-patch": "^3.2.0", "@sanity/diff-match-patch": "^3.2.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -128,7 +129,7 @@
"marked-variables": "^1.0.5", "marked-variables": "^1.0.5",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1", "moment": "^2.30.1",
"mongoose": "^9.1.5", "mongoose": "^9.2.1",
"nanoid": "5.1.6", "nanoid": "5.1.6",
"nconf": "^0.13.0", "nconf": "^0.13.0",
"react": "^18.3.1", "react": "^18.3.1",
@@ -151,7 +152,7 @@
"globals": "^16.4.0", "globals": "^16.4.0",
"jest": "^30.2.0", "jest": "^30.2.0",
"jest-expect-message": "^1.1.3", "jest-expect-message": "^1.1.3",
"jsdom": "^28.0.0", "jsdom": "^28.1.0",
"jsdom-global": "^3.0.2", "jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0", "postcss-less": "^6.0.0",
"stylelint": "^16.25.0", "stylelint": "^16.25.0",

View File

@@ -1,4 +1,5 @@
/*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/ /*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/
import mongoose from 'mongoose';
import supertest from 'supertest'; import supertest from 'supertest';
import HBApp from './app.js'; import HBApp from './app.js';
import { model as NotificationModel } from './notifications.model.js'; import { model as NotificationModel } from './notifications.model.js';
@@ -8,8 +9,19 @@ import { model as HomebrewModel } from './homebrew.model.js';
// Mimic https responses to avoid being redirected all the time // Mimic https responses to avoid being redirected all the time
const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https'); const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
let dbState;
describe('Tests for admin api', ()=>{ describe('Tests for admin api', ()=>{
beforeEach(()=>{
// Mock DB ready (for dbCheck middleware)
dbState = mongoose.connection.readyState;
mongoose.connection.readyState = 1;
});
afterEach(()=>{ afterEach(()=>{
// Restore DB ready state
mongoose.connection.readyState = dbState;
jest.resetAllMocks(); jest.resetAllMocks();
}); });

View File

@@ -3,18 +3,23 @@
@arrowSize : 6px; @arrowSize : 6px;
@arrowPosition : 18px; @arrowPosition : 18px;
[data-tooltip] { [data-tooltip] {
position:relative;
.tooltip(attr(data-tooltip)); .tooltip(attr(data-tooltip));
} }
[data-tooltip-top] { [data-tooltip-top] {
position:relative;
.tooltipTop(attr(data-tooltip-top)); .tooltipTop(attr(data-tooltip-top));
} }
[data-tooltip-bottom] { [data-tooltip-bottom] {
position:relative;
.tooltipBottom(attr(data-tooltip-bottom)); .tooltipBottom(attr(data-tooltip-bottom));
} }
[data-tooltip-left] { [data-tooltip-left] {
position:relative;
.tooltipLeft(attr(data-tooltip-left)); .tooltipLeft(attr(data-tooltip-left));
} }
[data-tooltip-right] { [data-tooltip-right] {
position:relative;
.tooltipRight(attr(data-tooltip-right)); .tooltipRight(attr(data-tooltip-right));
} }
.tooltip(@content) { .tooltip(@content) {
@@ -30,6 +35,7 @@
&::before, &::after { &::before, &::after {
bottom : 100%; bottom : 100%;
left : 50%; left : 50%;
translate: -50% 0;
} }
&:hover::after, &:hover::before, &:focus::after, &:focus::before { &:hover::after, &:hover::before, &:focus::after, &:focus::before {
.transform(translateY(-(@arrowSize + 2))); .transform(translateY(-(@arrowSize + 2)));
@@ -45,6 +51,7 @@
&::before, &::after { &::before, &::after {
top : 100%; top : 100%;
left : 50%; left : 50%;
translate: -50% 0;
} }
&:hover::after, &:hover::before, &:focus::after, &:focus::before { &:hover::after, &:hover::before, &:focus::after, &:focus::before {
.transform(translateY(@arrowSize + 2)); .transform(translateY(@arrowSize + 2));
@@ -57,7 +64,10 @@
margin-bottom : -@arrowSize; margin-bottom : -@arrowSize;
border-left-color : @tooltipColor; border-left-color : @tooltipColor;
} }
&::after { margin-bottom : -14px;} &::after {
margin-bottom : -14px;
max-width : 50ch;
}
&::before, &::after { &::before, &::after {
right : 100%; right : 100%;
bottom : 50%; bottom : 50%;
@@ -73,10 +83,14 @@
margin-left : -@arrowSize * 2; margin-left : -@arrowSize * 2;
border-right-color : @tooltipColor; border-right-color : @tooltipColor;
} }
&::after { margin-bottom : -14px;} &::after {
margin-bottom : -14px;
max-width : 50ch;
}
&::before, &::after { &::before, &::after {
bottom : 50%; top : 50%;
left : 100%; left : 100%;
translate:0 -50%;
} }
&:hover::after, &:hover::before, &:focus::after, &:focus::before { &:hover::after, &:hover::before, &:focus::after, &:focus::before {
.transform(translateX(@arrowSize + 2)); .transform(translateX(@arrowSize + 2));
@@ -106,9 +120,12 @@
font-size : 12px; font-size : 12px;
line-height : 12px; line-height : 12px;
color : white; color : white;
white-space : nowrap;
content : @content; content : @content;
background : @tooltipColor; background : @tooltipColor;
max-width : 60ch;
width :max-content;
word-break : break-word;
overflow-wrap : break-word;
} }
&:hover::before, &:hover::after { &:hover::before, &:hover::after {
visibility : visible; visibility : visible;

View File

@@ -40,7 +40,7 @@ export default [
icon : 'fas fa-image', icon : 'fas fa-image',
gen : [ gen : [
'<img ', '<img ',
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ', ' src=\'https://homebrewery.naturalcrit.com/assets/catwarrior.jpg\' ',
' style=\'width:325px\' />', ' style=\'width:325px\' />',
'Credit: Kyounghwan Kim' 'Credit: Kyounghwan Kim'
].join('\n') ].join('\n')
@@ -50,7 +50,7 @@ export default [
icon : 'fas fa-tree', icon : 'fas fa-tree',
gen : [ gen : [
'<img ', '<img ',
' src=\'http://i.imgur.com/hMna6G0.png\' ', ' src=\'https://homebrewery.naturalcrit.com/assets/homebrewerymug.png\' ',
' style=\'position:absolute; top:50px; right:30px; width:280px\' />' ' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
].join('\n') ].join('\n')
}, },

View File

@@ -84,7 +84,7 @@ export default {
return dedent` return dedent`
{{frontCover}} {{frontCover}}
{{logo ![](/assets/naturalCritLogoRed.svg)}} {{logo ![](https://homebrewery.naturalcrit.com/assets/naturalCritLogoRed.svg)}}
# ${_.sample(titles)} # ${_.sample(titles)}
## ${_.sample(subtitles)} ## ${_.sample(subtitles)}
@@ -96,7 +96,7 @@ export default {
${_.sample(footnote)} ${_.sample(footnote)}
}} }}
![background image](https://i.imgur.com/IwHRrbF.jpg){position:absolute,bottom:0,left:0,height:100%} ![background image](https://homebrewery.naturalcrit.com/assets/demontemple.jpg){position:absolute,bottom:0,left:0,height:100%}
\page`; \page`;
}, },
@@ -110,10 +110,10 @@ export default {
___ ___
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0 {{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
![background image](https://i.imgur.com/IsfUnFR.jpg){position:absolute,bottom:0,left:0,height:100%} ![background image](https://homebrewery.naturalcrit.com/assets/mountaincottage.jpg){position:absolute,bottom:0,left:0,height:100%}
}} }}
{{logo ![](/assets/naturalCritLogoRed.svg)}} {{logo ![](https://homebrewery.naturalcrit.com/assets/naturalCritLogoRed.svg)}}
\page`; \page`;
}, },
@@ -126,7 +126,7 @@ export default {
## ${_.sample(subtitles)} ## ${_.sample(subtitles)}
{{imageMaskEdge${_.random(1, 8)},--offset:10cm,--rotation:180 {{imageMaskEdge${_.random(1, 8)},--offset:10cm,--rotation:180
![Background image](https://i.imgur.com/9TU96xY.jpg){position:absolute,bottom:0,left:0,height:100%} ![Background image](https://homebrewery.naturalcrit.com/assets/nightchapel.jpg){position:absolute,bottom:0,left:0,height:100%}
}} }}
\page`; \page`;
@@ -143,10 +143,10 @@ export default {
For use with any fantasy roleplaying ruleset. Play the best game of your life! For use with any fantasy roleplaying ruleset. Play the best game of your life!
![background image](https://i.imgur.com/MJ4YHu7.jpg){position:absolute,bottom:0,left:0,height:100%} ![background image](https://homebrewery.naturalcrit.com/assets/shopvials.jpg){position:absolute,bottom:0,left:0,height:100%}
{{logo {{logo
![](/assets/naturalCritLogoWhite.svg) ![](https://homebrewery.naturalcrit.com/assets/naturalCritLogoWhite.svg)
Homebrewery.Naturalcrit.com Homebrewery.Naturalcrit.com
}}`; }}`;

View File

@@ -645,25 +645,25 @@ export default [
name : 'Image', name : 'Image',
icon : 'fas fa-image', icon : 'fas fa-image',
gen : dedent` gen : dedent`
![cat warrior](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:325px,mix-blend-mode:multiply}` ![cat warrior](https://homebrewery.naturalcrit.com/assets/catwarrior.jpg) {width:325px,mix-blend-mode:multiply}`
}, },
{ {
name : 'Image Wrap Left', name : 'Image Wrap Left',
icon : 'fac image-wrap-left', icon : 'fac image-wrap-left',
gen : dedent` gen : dedent`
![homebrewery_mug](http://i.imgur.com/hMna6G0.png) {width:280px,margin-right:-3cm,wrapLeft}` ![homebrewery_mug](https://homebrewery.naturalcrit.com/assets/homebrewerymug.png) {width:280px,margin-right:-3cm,wrapLeft}`
}, },
{ {
name : 'Image Wrap Right', name : 'Image Wrap Right',
icon : 'fac image-wrap-right', icon : 'fac image-wrap-right',
gen : dedent` gen : dedent`
![homebrewery_mug](http://i.imgur.com/hMna6G0.png) {width:280px,margin-left:-3cm,wrapRight}` ![homebrewery_mug](https://homebrewery.naturalcrit.com/assets/homebrewerymug.png) {width:280px,margin-left:-3cm,wrapRight}`
}, },
{ {
name : 'Background Image', name : 'Background Image',
icon : 'fas fa-tree', icon : 'fas fa-tree',
gen : dedent` gen : dedent`
![homebrew mug](http://i.imgur.com/hMna6G0.png) {position:absolute,top:50px,right:30px,width:280px}` ![homebrew mug](https://homebrewery.naturalcrit.com/assets/homebrewerymug.png) {position:absolute,top:50px,right:30px,width:280px}`
}, },
{ {
name : 'Watercolor Splatter', name : 'Watercolor Splatter',

View File

@@ -5,7 +5,7 @@ export default {
center : ()=>{ center : ()=>{
return dedent` return dedent`
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0 {{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
![](https://i.imgur.com/GZfjDWV.png){height:100%} ![](https://homebrewery.naturalcrit.com/assets/dragoninflight.jpg){height:100%}
}} }}
<!-- Use --offsetX to shift the mask left or right (can use cm instead of %) <!-- Use --offsetX to shift the mask left or right (can use cm instead of %)
Use --offsetY to shift the mask up or down Use --offsetY to shift the mask up or down
@@ -21,7 +21,7 @@ export default {
}[side]; }[side];
return dedent` return dedent`
{{imageMaskEdge${_.random(1, 8)},--offset:0%,--rotation:${rotation} {{imageMaskEdge${_.random(1, 8)},--offset:0%,--rotation:${rotation}
![](https://i.imgur.com/GZfjDWV.png){height:100%} ![](https://homebrewery.naturalcrit.com/assets/dragoninflight.jpg){height:100%}
}} }}
<!-- Use --offset to shift the mask away from page center (can use cm instead of %) <!-- Use --offset to shift the mask away from page center (can use cm instead of %)
Use --rotation to set rotation angle in degrees. -->\n\n`; Use --rotation to set rotation angle in degrees. -->\n\n`;
@@ -32,7 +32,7 @@ export default {
const offsetY = (y == 'top' ? '50%' : '-50%'); const offsetY = (y == 'top' ? '50%' : '-50%');
return dedent` return dedent`
{{imageMaskCorner${_.random(1, 37)},--offsetX:${offsetX},--offsetY:${offsetY},--rotation:0 {{imageMaskCorner${_.random(1, 37)},--offsetX:${offsetX},--offsetY:${offsetY},--rotation:0
![](https://i.imgur.com/GZfjDWV.png){height:100%} ![](https://homebrewery.naturalcrit.com/assets/dragoninflight.jpg){height:100%}
}} }}
<!-- Use --offsetX to shift the mask left or right (can use cm instead of %) <!-- Use --offsetX to shift the mask left or right (can use cm instead of %)
Use --offsetY to shift the mask up or down Use --offsetY to shift the mask up or down

View File

@@ -35,25 +35,25 @@ export default {
}} }}
`; `;
}, },
cczero : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)\n\n`, cczero : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)\n\n`,
ccby : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY 4.0](https://creativecommons.org/publicdomain/by/4.0/)\n\n`, ccby : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY 4.0](https://creativecommons.org/publicdomain/by/4.0/)\n\n`,
ccbysa : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY-SA 4.0](https://creativecommons.org/publicdomain/by-sa/4.0/)\n\n`, ccbysa : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY-SA 4.0](https://creativecommons.org/publicdomain/by-sa/4.0/)\n\n`,
ccbync : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY-NC 4.0](https://creativecommons.org/publicdomain/by-nc/4.0/)\n\n`, ccbync : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY-NC 4.0](https://creativecommons.org/publicdomain/by-nc/4.0/)\n\n`,
ccbyncsa : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY-NC-SA](https://creativecommons.org/publicdomain/by-nc-sa/4.0/)\n\n`, ccbyncsa : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY-NC-SA](https://creativecommons.org/publicdomain/by-nc-sa/4.0/)\n\n`,
ccbynd : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY-ND 4.0](https://creativecommons.org/publicdomain/by-nd/4.0/)\n\n`, ccbynd : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC BY-ND 4.0](https://creativecommons.org/publicdomain/by-nd/4.0/)\n\n`,
ccbyncnd : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC NY-NC-ND 4.0](https://creativecommons.org/publicdomain/by-nc-nd/4.0/)\n\n`, ccbyncnd : `<i class="far fa-copyright"></i> \<year\> This work is openly licensed via [CC NY-NC-ND 4.0](https://creativecommons.org/publicdomain/by-nc-nd/4.0/)\n\n`,
cczeroBadge : `![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)`, cczeroBadge : `![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)`,
ccbyBadge : `![CC BY](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by.svg)`, ccbyBadge : `![CC BY](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by.svg)`,
ccbysaBadge : `![CC BY-SA](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-sa.svg)`, ccbysaBadge : `![CC BY-SA](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-sa.svg)`,
ccbyncBadge : `![CC BY-NC](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc.svg)`, ccbyncBadge : `![CC BY-NC](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc.svg)`,
ccbyncsaBadge : `![CC BY-NC-SA](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc-sa.svg)`, ccbyncsaBadge : `![CC BY-NC-SA](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc-sa.svg)`,
ccbyndBadge : `![CC BY-ND](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nd.svg)`, ccbyndBadge : `![CC BY-ND](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nd.svg)`,
ccbyncndBadge : `![CC BY-NC-ND](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc-nd.svg)`, ccbyncndBadge : `![CC BY-NC-ND](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc-nd.svg)`,
shadowDarkNotice : `\[Product Name]\ is an independent product published under the Shadowdark RPG Third-Party License and is not affiliated with The Arcane Library, LLC. Shadowdark RPG © 2023 The Arcane Library, LLC.\n`, shadowDarkNotice : `\[Product Name]\ is an independent product published under the Shadowdark RPG Third-Party License and is not affiliated with The Arcane Library, LLC. Shadowdark RPG © 2023 The Arcane Library, LLC.\n`,
shadowDarkBlack : `![Shadowdark Black Logo](/assets/license_logos/The-Arcane-Library_Third-Party-License_Black.png){width:200px}`, shadowDarkBlack : `![Shadowdark Black Logo](https://homebrewery.naturalcrit.com/assets/license_logos/The-Arcane-Library_Third-Party-License_Black.png){width:200px}`,
shadowDarkWhite : `![Shadowdark White Logo](/assets/license_logos/The-Arcane-Library_Third-Party-License_White.png){width:200px}`, shadowDarkWhite : `![Shadowdark White Logo](https://homebrewery.naturalcrit.com/assets/license_logos/The-Arcane-Library_Third-Party-License_White.png){width:200px}`,
bladesDarkNotice : `This work is based on Blades in the Dark \(found at (http://www.bladesinthedark.com/)\), product of One Seven Design, developed and authored by John Harper, and licensed for our use under the Creative Commons Attribution 3.0 Unported license \(http://creativecommons.org/licenses/by/3.0/\).\n`, bladesDarkNotice : `This work is based on Blades in the Dark \(found at (http://www.bladesinthedark.com/)\), product of One Seven Design, developed and authored by John Harper, and licensed for our use under the Creative Commons Attribution 3.0 Unported license \(http://creativecommons.org/licenses/by/3.0/\).\n`,
bladesDarkLogo : `![Forged in the Dark](/assets/license_logos/Evil-Hat_Forged-In-The-Dark_Logo-V2.png)`, bladesDarkLogo : `![Forged in the Dark](https://homebrewery.naturalcrit.com/assets/license_logos/Evil-Hat_Forged-In-The-Dark_Logo-V2.png)`,
bladesDarkLogoAttribution : `*Blades in the Dark^tm^ is a trademark of One Seven Design. The Forged in the Dark Logo is © One Seven Design, and is used with permission.*`, bladesDarkLogoAttribution : `*Blades in the Dark^tm^ is a trademark of One Seven Design. The Forged in the Dark Logo is © One Seven Design, and is used with permission.*`,
iconsCompatibility : 'Compatibility with Icons requires Icons Superpowered Roleplaying from Ad Infinitum Adventures. Ad Infinitum Adventures does not guarantee compatibility, and does not endorse this product.', iconsCompatibility : 'Compatibility with Icons requires Icons Superpowered Roleplaying from Ad Infinitum Adventures. Ad Infinitum Adventures does not guarantee compatibility, and does not endorse this product.',
iconsTrademark : 'Icons Superpowered Roleplaying is a trademark of Steve Kenson, published exclusively by Ad Infinitum Adventures. The Icons Superpowered Roleplaying Compatibility Logo is a trademark of Ad Infinitum Adventures and is used under the Icons Superpowered Roleplaying Compatibility License.', iconsTrademark : 'Icons Superpowered Roleplaying is a trademark of Steve Kenson, published exclusively by Ad Infinitum Adventures. The Icons Superpowered Roleplaying Compatibility Logo is a trademark of Ad Infinitum Adventures and is used under the Icons Superpowered Roleplaying Compatibility License.',

View File

@@ -101,10 +101,10 @@ export default {
}, },
// Verify Logo redistribution // Verify Logo redistribution
greenRoninAgeCreatorsAllianceCover : `Requires the \[Game Title\] Rulebook from Green Ronin Publishing for use.`, greenRoninAgeCreatorsAllianceCover : `Requires the \[Game Title\] Rulebook from Green Ronin Publishing for use.`,
greenRoninAgeCreatorsAllianceLogo : `![Age Creators Alliance](/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_General-Compatibility-Logo.png){width:200px}`, greenRoninAgeCreatorsAllianceLogo : `![Age Creators Alliance](https://homebrewery.naturalcrit.com/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_General-Compatibility-Logo.png){width:200px}`,
greenRoninAgeCreatorsAllianceBlueRoseLogo : `![Age Creators Alliance](/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_Blue-Rose-Compatibility-Logo.png){width:200px}`, greenRoninAgeCreatorsAllianceBlueRoseLogo : `![Age Creators Alliance](https://homebrewery.naturalcrit.com/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_Blue-Rose-Compatibility-Logo.png){width:200px}`,
greenRoninAgeCreatorsAllianceFantasyAgeCompatible : `![Fantasy AGE Compatible](/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_Fantasy-AGE-Compatibility-Logo.png){width:200px}`, greenRoninAgeCreatorsAllianceFantasyAgeCompatible : `![Fantasy AGE Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_Fantasy-AGE-Compatibility-Logo.png){width:200px}`,
greenRoninAgeCreatorsAllianceModernAGECompatible : `![Modern AGE Compatible](/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_Modern-AGE-Compatibility-Logo.png){width:200px}`, greenRoninAgeCreatorsAllianceModernAGECompatible : `![Modern AGE Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_Modern-AGE-Compatibility-Logo.png){width:200px}`,
// Green Ronin's Chronicle - Verify Art and Access // Green Ronin's Chronicle - Verify Art and Access
greenRoninChronicleSystemGuildColophon : function() { greenRoninChronicleSystemGuildColophon : function() {
return dedent` return dedent`
@@ -179,10 +179,10 @@ export default {
`; `;
}, },
// Verify Logo redistribution // Verify Logo redistribution
monteCookLogoDarkLarge : `![Cypher System Compatible](/assets/license_logos/CSCDarkLarge.png)`, monteCookLogoDarkLarge : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCDarkLarge.png)`,
monteCookLogoDarkSmall : `![Cypher System Compatible](/assets/license_logos/CSCDarkSmall.png)`, monteCookLogoDarkSmall : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCDarkSmall.png)`,
monteCookLogoLightLarge : `![Cypher System Compatible](/assets/license_logos/CSCLightLarge.png)`, monteCookLogoLightLarge : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCLightLarge.png)`,
monteCookLogoLightSmall : `![Cypher System Compatible](/assets/license_logos/CSCLightSmall.png)`, monteCookLogoLightSmall : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCLightSmall.png)`,
// Onyx Path Canis Minor - Verify logos and access // Onyx Path Canis Minor - Verify logos and access
onyxPathCanisMinorColophon : function () { onyxPathCanisMinorColophon : function () {
return dedent` return dedent`

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
themes/assets/shopvials.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB