0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-05-09 22:48:39 +00:00

Merge branch 'master' into HTMLDownload

This commit is contained in:
David Bolack
2026-02-27 09:59:13 -06:00
27 changed files with 373 additions and 295 deletions
+40 -8
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 }
+4 -1
View File
@@ -11,6 +11,7 @@ const Combobox = createReactClass({
trigger : 'hover', trigger : 'hover',
default : '', default : '',
placeholder : '', placeholder : '',
tooltip: '',
autoSuggest : { autoSuggest : {
clearAutoSuggestOnClick : true, clearAutoSuggestOnClick : true,
suggestMethod : 'includes', suggestMethod : 'includes',
@@ -70,11 +71,13 @@ 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} pattern={this.props.valuePatterns}
placeholder={this.props.placeholder} placeholder={this.props.placeholder}
onBlur={(e)=>{ onBlur={(e)=>{
+1
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;
@@ -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)}
> >
@@ -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;
+8 -5
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')}
+1
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%; }
@@ -213,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}
@@ -231,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>;
} }
@@ -256,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 || ''}
@@ -273,7 +272,6 @@ 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>;
@@ -339,14 +337,20 @@ const MetadataEditor = createReactClass({
{this.renderThumbnail()} {this.renderThumbnail()}
</div> </div>
<TagInput <div className="field tags">
label='tags' <label>Tags</label>
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/} <div className="value" >
placeholder='add tag' unique={true} <TagInput
values={this.props.metadata.tags} label='tags'
smallText='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.' valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
onChange={(e)=>this.handleFieldChange('tags', e)} placeholder='add tag' unique={true}
/> values={this.props.metadata.tags}
onChange={(e)=>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.'
/>
</div>
</div>
{this.renderLanguageDropdown()} {this.renderLanguageDropdown()}
@@ -358,15 +362,22 @@ const MetadataEditor = createReactClass({
{this.renderAuthors()} {this.renderAuthors()}
<TagInput <div className="field invitedAuthors">
label='invited authors' <label>Invited authors</label>
valuePatterns={/.+/} <div className="value">
validators={[(v)=>!this.props.metadata.authors?.includes(v)]} <TagInput
placeholder='invite author' unique={true} label='invited authors'
values={this.props.metadata.invitedAuthors} valuePatterns={/.+/}
smallText='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.' validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)} 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>
@@ -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,27 +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;
}
&.tags .tagInput-dropdown {
z-index : 201;
max-width : 200px;
} }
} }
.thumbnail-preview { .thumbnail-preview {
position : relative; position : relative;
flex : 1 1; flex : 1 1;
@@ -174,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; }
+105 -107
View File
@@ -1,71 +1,71 @@
import './tagInput.less'; import "./tagInput.less";
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import Combobox from '../../../components/combobox.jsx'; import Combobox from "../../../components/combobox.jsx";
import tagSuggestionList from './curatedTagSuggestionList.js'; import tagSuggestionList from "./curatedTagSuggestionList.js";
const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{ const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, placeholder = "", smallText = "", onChange }) => {
const [tagList, setTagList] = useState( const [tagList, setTagList] = useState(
values.map((value)=>({ values.map((value) => ({
value, value,
editing : false, editing: false,
draft : '', draft: "",
})), })),
); );
useEffect(()=>{ useEffect(() => {
const incoming = values || []; const incoming = values || [];
const current = tagList.map((t)=>t.value); const current = tagList.map((t) => t.value);
const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]); const changed = incoming.length !== current.length || incoming.some((v, i) => v !== current[i]);
if(changed) { if (changed) {
setTagList( setTagList(
incoming.map((value)=>({ incoming.map((value) => ({
value, value,
editing : false, editing: false,
})), })),
); );
} }
}, [values]); }, [values]);
useEffect(()=>{ useEffect(() => {
onChange?.({ onChange?.({
target : { value: tagList.map((t)=>t.value) }, target: { value: tagList.map((t) => t.value) },
}); });
}, [tagList]); }, [tagList]);
// substrings to be normalized to the first value on the array // substrings to be normalized to the first value on the array
const duplicateGroups = [ const duplicateGroups = [
['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'], ["5e 2024", "5.5e", "5e'24", "5.24", "5e24", "5.5"],
['5e', '5th Edition'], ["5e", "5th Edition"],
['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'], ["Dungeons & Dragons", "Dungeons and Dragons", "Dungeons n dragons"],
['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'], ["D&D", "DnD", "dnd", "Dnd", "dnD", "d&d", "d&D", "D&d"],
['P2e', 'p2e', 'P2E', 'Pathfinder 2e'], ["P2e", "p2e", "P2E", "Pathfinder 2e"],
]; ];
const normalizeValue = (input)=>{ const normalizeValue = (input) => {
const lowerInput = input.toLowerCase(); const lowerInput = input.toLowerCase();
let normalizedTag = input; let normalizedTag = input;
for (const group of duplicateGroups) { for (const group of duplicateGroups) {
for (const tag of group) { for (const tag of group) {
if(!tag) continue; if (!tag) continue;
const index = lowerInput.indexOf(tag.toLowerCase()); const index = lowerInput.indexOf(tag.toLowerCase());
if(index !== -1) { if (index !== -1) {
normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length); normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length);
break; break;
} }
} }
} }
if(normalizedTag.includes(':')) { if (normalizedTag.includes(":")) {
const [rawType, rawValue = ''] = normalizedTag.split(':'); const [rawType, rawValue = ""] = normalizedTag.split(":");
const tagType = rawType.trim().toLowerCase(); const tagType = rawType.trim().toLowerCase();
const tagValue = rawValue.trim(); const tagValue = rawValue.trim();
if(tagValue.length > 0) { if (tagValue.length > 0) {
normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`; normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`;
} }
//trims spaces around colon and capitalizes the first word after the colon //trims spaces around colon and capitalizes the first word after the colon
@@ -75,88 +75,114 @@ const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholde
return normalizedTag; return normalizedTag;
}; };
const submitTag = (newValue, index = null)=>{ const submitTag = (newValue, index = null) => {
const trimmed = newValue?.trim(); const trimmed = newValue?.trim();
if(!trimmed) return; if (!trimmed) return;
if(!valuePatterns.test(trimmed)) return; if (!valuePatterns.test(trimmed)) return;
const normalizedTag = normalizeValue(trimmed); const normalizedTag = normalizeValue(trimmed);
setTagList((prev)=>{ setTagList((prev) => {
const existsIndex = prev.findIndex((t)=>t.value.toLowerCase() === normalizedTag.toLowerCase()); const existsIndex = prev.findIndex((t) => t.value.toLowerCase() === normalizedTag.toLowerCase());
if(unique && existsIndex !== -1) return prev; if (unique && existsIndex !== -1) return prev;
if(index !== null) { if (index !== null) {
return prev.map((t, i)=>(i === index ? { ...t, value: normalizedTag, editing: false } : t)); return prev.map((t, i) => (i === index ? { ...t, value: normalizedTag, editing: false } : t));
} }
return [...prev, { value: normalizedTag, editing: false }]; return [...prev, { value: normalizedTag, editing: false }];
}); });
}; };
const removeTag = (index)=>{ const removeTag = (index) => {
setTagList((prev)=>prev.filter((_, i)=>i !== index)); setTagList((prev) => prev.filter((_, i) => i !== index));
}; };
const editTag = (index)=>{ const editTag = (index) => {
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t))); setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: true, draft: t.value } : t)));
}; };
const stopEditing = (index)=>{ const stopEditing = (index) => {
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t))); setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: false, draft: "" } : t)));
}; };
const suggestionOptions = tagSuggestionList.map((tag)=>{ const suggestionOptions = tagSuggestionList.map((tag) => {
const tagType = tag.split(':'); const tagType = tag.split(":");
let classes = 'item'; let classes = "item";
switch (tagType[0]) { switch (tagType[0]) {
case 'type': case "type":
classes = 'item type'; classes = "item type";
break; break;
case 'group': case "group":
classes = 'item group'; classes = "item group";
break; break;
case 'meta': case "meta":
classes = 'item meta'; classes = "item meta";
break; break;
case 'system': case "system":
classes = 'item system'; classes = "item system";
break; break;
default: default:
classes = 'item'; classes = "item";
break; break;
} }
return ( return (
<div className={classes} key={`tag-${tag}`} value={tag} data={tag} title={tag}> <div className={classes} key={`tag-${tag}`} value={tag} data={tag}>
{tag} {tag}
</div> </div>
); );
}); });
return ( return (
<div className='field tags'> <div className="tagInputWrap">
{label && <label>{label}</label>} <Combobox
trigger="click"
<div className='value'> className="tagInput-dropdown"
<ul className='list'> default=""
{tagList.map((t, i)=>t.editing ? ( placeholder={placeholder}
options={label === "tags" ? suggestionOptions : []}
tooltip={tooltip}
autoSuggest={
label === "tags"
? {
suggestMethod: "startsWith",
clearAutoSuggestOnClick: true,
filterOn: ["value", "title"],
}
: { 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 <input
key={i} key={i}
type='text' type="text"
value={t.draft} // always use draft value={t.draft} // always use draft
pattern={valuePatterns.source} pattern={valuePatterns.source}
onChange={(e)=>setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: e.target.value } : tag)), onChange={(e) =>
) setTagList((prev) =>
prev.map((tag, idx) => (idx === i ? { ...tag, draft: e.target.value } : tag)),
)
} }
onKeyDown={(e)=>{ onKeyDown={(e) => {
if(e.key === 'Enter') { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
submitTag(t.draft, i); // submit draft submitTag(t.draft, i); // submit draft
setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: '' } : tag)), setTagList((prev) =>
prev.map((tag, idx) => (idx === i ? { ...tag, draft: "" } : tag)),
); );
} }
if(e.key === 'Escape') { if (e.key === "Escape") {
stopEditing(i); stopEditing(i);
e.target.blur(); e.target.blur();
} }
@@ -164,48 +190,20 @@ const TagInput = ({ label, valuePatterns, values = [], unique = true, placeholde
autoFocus autoFocus
/> />
) : ( ) : (
<li key={i} className='tag' onClick={()=>editTag(i)}> <li key={i} className="tag" onClick={() => editTag(i)}>
{t.value} {t.value}
<button <button
type='button' type="button"
onClick={(e)=>{ onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
removeTag(i); removeTag(i);
}}> }}>
<i className='fa fa-times fa-fw' /> <i className="fa fa-times fa-fw" />
</button> </button>
</li> </li>
), ),
)} )}
</ul> </ul>
<Combobox
trigger='click'
className='tagInput-dropdown'
default=''
placeholder={placeholder}
options={label === 'tags' ? suggestionOptions : []}
autoSuggest={
label === 'tags'
? {
suggestMethod : 'startsWith',
clearAutoSuggestOnClick : true,
filterOn : ['value', 'title'],
}
: { suggestMethod: 'includes', clearAutoSuggestOnClick: true, filterOn: [] }
}
valuePatterns={valuePatterns.source}
onSelect={(value)=>submitTag(value)}
onEntry={(e)=>{
if(e.key === 'Enter') {
console.log('submit');
e.preventDefault();
submitTag(e.target.value);
}
}}
/>
{smallText.length !== 0 && <small>{smallText}</small>}
</div>
</div> </div>
); );
}; };
+26 -17
View File
@@ -1,21 +1,30 @@
.list input { .tags {
border-radius: 5px;
}
.tagInput-dropdown { .tagInputWrap {
.dropdown-options { display:grid;
.item { grid-template-columns: 200px 3fr;
&.type { gap:10px;
background-color: #00800035; }
}
&.group { .list input {
background-color: #50505035; border-radius: 5px;
} }
&.meta {
background-color: #00008035; .tagInput-dropdown {
} .dropdown-options {
&.system { .item {
background-color: #80000035; &.type {
background-color: #00800035;
}
&.group {
background-color: #50505035;
}
&.meta {
background-color: #00008035;
}
&.system {
background-color: #80000035;
}
} }
} }
} }
@@ -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.*
+39 -39
View File
@@ -73,7 +73,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",
@@ -3245,13 +3245,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "25.2.3", "version": "25.3.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~7.16.0" "undici-types": "~7.18.0"
} }
}, },
"node_modules/@types/stack-utils": { "node_modules/@types/stack-utils": {
@@ -4121,9 +4121,9 @@
} }
}, },
"node_modules/asn1.js/node_modules/bn.js": { "node_modules/asn1.js/node_modules/bn.js": {
"version": "4.12.2", "version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/assert": { "node_modules/assert": {
@@ -4497,9 +4497,9 @@
} }
}, },
"node_modules/bn.js": { "node_modules/bn.js": {
"version": "5.2.2", "version": "5.2.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
"integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
@@ -5511,9 +5511,9 @@
} }
}, },
"node_modules/create-ecdh/node_modules/bn.js": { "node_modules/create-ecdh/node_modules/bn.js": {
"version": "4.12.2", "version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/create-hash": { "node_modules/create-hash": {
@@ -5960,9 +5960,9 @@
} }
}, },
"node_modules/diffie-hellman/node_modules/bn.js": { "node_modules/diffie-hellman/node_modules/bn.js": {
"version": "4.12.2", "version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/dir-glob": { "node_modules/dir-glob": {
@@ -6067,9 +6067,9 @@
} }
}, },
"node_modules/elliptic/node_modules/bn.js": { "node_modules/elliptic/node_modules/bn.js": {
"version": "4.12.2", "version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/emittery": { "node_modules/emittery": {
@@ -7913,9 +7913,9 @@
} }
}, },
"node_modules/hashery": { "node_modules/hashery": {
"version": "1.4.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.4.0.tgz", "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.0.tgz",
"integrity": "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==", "integrity": "sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -10326,9 +10326,9 @@
} }
}, },
"node_modules/miller-rabin/node_modules/bn.js": { "node_modules/miller-rabin/node_modules/bn.js": {
"version": "4.12.2", "version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/mime": { "node_modules/mime": {
@@ -10413,10 +10413,10 @@
} }
}, },
"node_modules/minipass": { "node_modules/minipass": {
"version": "7.1.2", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
"license": "ISC", "license": "BlueOak-1.0.0",
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
@@ -11911,9 +11911,9 @@
} }
}, },
"node_modules/public-encrypt/node_modules/bn.js": { "node_modules/public-encrypt/node_modules/bn.js": {
"version": "4.12.2", "version": "4.12.3",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
"integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/punycode": { "node_modules/punycode": {
@@ -14636,9 +14636,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.16.0", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -15360,9 +15360,9 @@
} }
}, },
"node_modules/whatwg-url": { "node_modules/whatwg-url": {
"version": "16.0.0", "version": "16.0.1",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
"integrity": "sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==", "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+21 -4
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;
+2 -2
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')
}, },
+7 -7
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
}}`; }}`;
+4 -4
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',
+3 -3
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
+19 -19
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.',
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB