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

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into vitreum-to-vite

This commit is contained in:
Víctor Losada Hernández
2026-02-24 20:52:48 +01:00
28 changed files with 2618 additions and 232 deletions

View File

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

View File

@@ -11,11 +11,13 @@ const Combobox = createReactClass({
trigger : 'hover',
default : '',
placeholder : '',
tooltip: '',
autoSuggest : {
clearAutoSuggestOnClick : true,
suggestMethod : 'includes',
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
},
valuePatterns: /.+/
};
},
getInitialState : function() {
@@ -69,11 +71,14 @@ const Combobox = createReactClass({
return (
<div className='dropdown-input item'
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
type='text'
onChange={(e)=>this.handleInput(e)}
value={this.state.value || ''}
title=''
pattern={this.props.valuePatterns}
placeholder={this.props.placeholder}
onBlur={(e)=>{
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'/>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,8 +11,6 @@ import TagInput from '../tagInput/tagInput.jsx';
import Themes from '../../../../themes/themes.json';
import validations from './validations.js';
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
import homebreweryThumbnail from '../../thumbnail.png';
const callIfExists = (val, fn, ...args)=>{
@@ -34,7 +32,6 @@ const MetadataEditor = createReactClass({
tags : [],
published : false,
authors : [],
systems : [],
renderer : 'legacy',
theme : '5ePHB',
lang : 'en'
@@ -92,15 +89,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){
if(e.target.checked){
this.props.metadata.renderer = renderer;
@@ -156,18 +144,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(){
if(this.props.metadata.published){
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
@@ -238,7 +214,7 @@ const MetadataEditor = createReactClass({
</div>;
} else {
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'
className='themes-dropdown'
default={currentThemeDisplay}
@@ -256,7 +232,6 @@ const MetadataEditor = createReactClass({
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>;
}
@@ -281,7 +256,7 @@ const MetadataEditor = createReactClass({
return <div className='field language'>
<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'
className='language-dropdown'
default={this.props.metadata.lang || ''}
@@ -298,14 +273,13 @@ const MetadataEditor = createReactClass({
filterOn : ['value', 'detail', 'title']
}}
/>
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
</div>
</div>;
},
renderRenderOptions : function(){
return <div className='field systems'>
return <div className='field renderers'>
<label>Renderer</label>
<div className='value'>
<label key='legacy'>
@@ -364,18 +338,20 @@ const MetadataEditor = createReactClass({
{this.renderThumbnail()}
</div>
<TagInput label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
placeholder='add tag' unique={true}
values={this.props.metadata.tags}
onChange={(e)=>this.handleFieldChange('tags', e)}
/>
<div className='field systems'>
<label>systems</label>
<div className='value'>
{this.renderSystems()}
<div className="field tags">
<label>Tags</label>
<div className="value" >
<TagInput
label='tags'
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
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()}
@@ -387,13 +363,22 @@ const MetadataEditor = createReactClass({
{this.renderAuthors()}
<TagInput label='invited authors' valuePatterns={[/.+/]}
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
placeholder='invite author' unique={true}
values={this.props.metadata.invitedAuthors}
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
/>
<div className="field invitedAuthors">
<label>Invited authors</label>
<div className="value">
<TagInput
label='invited authors'
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>

View File

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

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 React, { useState, useEffect } from 'react';
import _ from 'lodash';
import "./tagInput.less";
import React, { useState, useEffect } from "react";
import Combobox from "../../../components/combobox.jsx";
const TagInput = ({ unique = true, values = [], ...props })=>{
const [tempInputText, setTempInputText] = useState('');
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false })));
import tagSuggestionList from "./curatedTagSuggestionList.js";
useEffect(()=>{
handleChange(tagList.map((context)=>context.value));
const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, placeholder = "", smallText = "", onChange }) => {
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]);
const handleChange = (value)=>{
props.onChange({
target : { value }
});
};
// substrings to be normalized to the first value on the array
const duplicateGroups = [
["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 = {} })=>{
if(_.includes(['Enter', ','], evt.key)) {
evt.preventDefault();
submitTag(evt.target.value, value, index);
if(options.clear) {
setTempInputText('');
const normalizeValue = (input) => {
const lowerInput = input.toLowerCase();
let normalizedTag = input;
for (const group of duplicateGroups) {
for (const tag of group) {
if (!tag) continue;
const index = lowerInput.indexOf(tag.toLowerCase());
if (index !== -1) {
normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length);
break;
}
}
}
if (normalizedTag.includes(":")) {
const [rawType, rawValue = ""] = normalizedTag.split(":");
const tagType = rawType.trim().toLowerCase();
const tagValue = rawValue.trim();
if (tagValue.length > 0) {
normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`;
}
//trims spaces around colon and capitalizes the first word after the colon
//this is preferred to users not understanding they can't put spaces in
}
return normalizedTag;
};
const submitTag = (newValue, originalValue, index)=>{
setTagList((prevContext)=>{
// remove existing tag
if(newValue === null){
return [...prevContext].filter((context, i)=>i !== index);
const submitTag = (newValue, index = null) => {
const trimmed = newValue?.trim();
if (!trimmed) return;
if (!valuePatterns.test(trimmed)) return;
const normalizedTag = normalizeValue(trimmed);
setTagList((prev) => {
const existsIndex = prev.findIndex((t) => t.value.toLowerCase() === normalizedTag.toLowerCase());
if (unique && existsIndex !== -1) return prev;
if (index !== null) {
return prev.map((t, i) => (i === index ? { ...t, value: normalizedTag, editing: false } : t));
}
// add new tag
if(originalValue === null){
return [...prevContext, { value: newValue, editing: false }];
}
// update existing tag
return prevContext.map((context, i)=>{
if(i === index) {
return { ...context, value: newValue, editing: false };
}
return context;
});
return [...prev, { value: normalizedTag, editing: false }];
});
};
const editTag = (index)=>{
setTagList((prevContext)=>{
return prevContext.map((context, i)=>{
if(i === index) {
return { ...context, editing: true };
}
return { ...context, editing: false };
});
});
const removeTag = (index) => {
setTagList((prev) => prev.filter((_, i) => i !== index));
};
const renderReadTag = (context, index)=>{
return (
<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 editTag = (index) => {
setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: true, draft: t.value } : t)));
};
const renderWriteTag = (context, index)=>{
return (
<input type='text'
key={index}
defaultValue={context.value}
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index: index })}
autoFocus
/>
);
const stopEditing = (index) => {
setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: false, draft: "" } : t)));
};
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 (
<div className='field'>
<label>{props.label}</label>
<div className='value'>
<ul className='list'>
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
</ul>
<input
type='text'
className='value'
placeholder={props.placeholder}
value={tempInputText}
onChange={(e)=>setTempInputText(e.target.value)}
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })}
/>
</div>
<div className="tagInputWrap">
<Combobox
trigger="click"
className="tagInput-dropdown"
default=""
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
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>
);
};

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!
}}
![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
##### 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.
### 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.
{{position:absolute;top:20px;right:20px;width:auto
[![Discord](/assets/discord.png){height:30px}](https://discord.gg/by3deKx)
[![Github](/assets/github.png){height:30px}](https://github.com/naturalcrit/homebrewery)
[![Patreon](/assets/patreon.png){height:30px}](https://patreon.com/NaturalCrit)
[![Reddit](/assets/reddit.png){height:30px}](https://www.reddit.com/r/homebrewery/)
[![Discord](https://homebrewery.naturalcrit.com/assets/discord.png){height:30px}](https://discord.gg/by3deKx)
[![Github](https://homebrewery.naturalcrit.com/assets/github.png){height:30px}](https://github.com/naturalcrit/homebrewery)
[![Patreon](https://homebrewery.naturalcrit.com/assets/patreon.png){height:30px}](https://patreon.com/NaturalCrit)
[![Reddit](https://homebrewery.naturalcrit.com/assets/reddit.png){height:30px}](https://www.reddit.com/r/homebrewery/)
}}
\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.
![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.*

View File

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

View File

@@ -40,7 +40,7 @@ export default [
icon : 'fas fa-image',
gen : [
'<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\' />',
'Credit: Kyounghwan Kim'
].join('\n')
@@ -50,7 +50,7 @@ export default [
icon : 'fas fa-tree',
gen : [
'<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\' />'
].join('\n')
},

View File

@@ -84,7 +84,7 @@ export default {
return dedent`
{{frontCover}}
{{logo ![](/assets/naturalCritLogoRed.svg)}}
{{logo ![](https://homebrewery.naturalcrit.com/assets/naturalCritLogoRed.svg)}}
# ${_.sample(titles)}
## ${_.sample(subtitles)}
@@ -96,7 +96,7 @@ export default {
${_.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`;
},
@@ -110,10 +110,10 @@ export default {
___
{{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`;
},
@@ -126,7 +126,7 @@ export default {
## ${_.sample(subtitles)}
{{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`;
@@ -143,10 +143,10 @@ export default {
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
![](/assets/naturalCritLogoWhite.svg)
![](https://homebrewery.naturalcrit.com/assets/naturalCritLogoWhite.svg)
Homebrewery.Naturalcrit.com
}}`;

View File

@@ -645,25 +645,25 @@ export default [
name : 'Image',
icon : 'fas fa-image',
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',
icon : 'fac image-wrap-left',
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',
icon : 'fac image-wrap-right',
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',
icon : 'fas fa-tree',
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',

View File

@@ -5,7 +5,7 @@ export default {
center : ()=>{
return dedent`
{{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 --offsetY to shift the mask up or down
@@ -21,7 +21,7 @@ export default {
}[side];
return dedent`
{{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 --rotation to set rotation angle in degrees. -->\n\n`;
@@ -32,7 +32,7 @@ export default {
const offsetY = (y == 'top' ? '50%' : '-50%');
return dedent`
{{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 --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`,
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`,
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`,
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`,
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)`,
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)`,
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)`,
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`,
shadowDarkBlack : `![Shadowdark Black Logo](/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}`,
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)`,
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`,
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`,
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`,
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)`,
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)`,
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)`,
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)`,
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](https://homebrewery.naturalcrit.com/assets/license_logos/The-Arcane-Library_Third-Party-License_Black.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`,
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.*`,
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.',

View File

@@ -101,10 +101,10 @@ export default {
},
// Verify Logo redistribution
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}`,
greenRoninAgeCreatorsAllianceBlueRoseLogo : `![Age Creators Alliance](/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}`,
greenRoninAgeCreatorsAllianceModernAGECompatible : `![Modern AGE Compatible](/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_Modern-AGE-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](https://homebrewery.naturalcrit.com/assets/license_logos/Green-Ronin_AGE-Creators-Alliance_Blue-Rose-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](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
greenRoninChronicleSystemGuildColophon : function() {
return dedent`
@@ -179,10 +179,10 @@ export default {
`;
},
// Verify Logo redistribution
monteCookLogoDarkLarge : `![Cypher System Compatible](/assets/license_logos/CSCDarkLarge.png)`,
monteCookLogoDarkSmall : `![Cypher System Compatible](/assets/license_logos/CSCDarkSmall.png)`,
monteCookLogoLightLarge : `![Cypher System Compatible](/assets/license_logos/CSCLightLarge.png)`,
monteCookLogoLightSmall : `![Cypher System Compatible](/assets/license_logos/CSCLightSmall.png)`,
monteCookLogoDarkLarge : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCDarkLarge.png)`,
monteCookLogoDarkSmall : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCDarkSmall.png)`,
monteCookLogoLightLarge : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCLightLarge.png)`,
monteCookLogoLightSmall : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCLightSmall.png)`,
// Onyx Path Canis Minor - Verify logos and access
onyxPathCanisMinorColophon : function () {
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