0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-25 16:12:37 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into quote-snippet

This commit is contained in:
Víctor Losada Hernández
2023-08-08 19:12:34 +02:00
62 changed files with 19303 additions and 16831 deletions

View File

@@ -15,7 +15,7 @@ module.exports = {
rules : {
/** Errors **/
'camelcase' : ['error', { properties: 'never' }],
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
'no-array-constructor' : 'error',
'no-iterator' : 'error',
'no-nested-ternary' : 'error',

48
.stylelintrc.json Normal file
View File

@@ -0,0 +1,48 @@
{
"extends": [
"stylelint-config-recess-order",
"stylelint-config-recommended"],
"plugins": [
"stylelint-stylistic",
"./stylelint_plugins/declaration-colon-align.js",
"./stylelint_plugins/declaration-colon-min-space-before",
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
],
"customSyntax": "postcss-less",
"rules": {
"no-descending-specificity" : null,
"at-rule-no-unknown" : null,
"function-no-unknown" : null,
"font-family-no-missing-generic-family-keyword" : null,
"font-weight-notation" : "named-where-possible",
"font-family-name-quotes" : "always-unless-keyword",
"stylistic/indentation" : "tab",
"no-duplicate-selectors" : true,
"stylistic/color-hex-case" : "upper",
"color-hex-length" : "long",
"stylistic/selector-combinator-space-after" : "always",
"stylistic/selector-combinator-space-before" : "always",
"stylistic/selector-attribute-operator-space-before" : "never",
"stylistic/selector-attribute-operator-space-after" : "never",
"stylistic/selector-attribute-brackets-space-inside" : "never",
"selector-attribute-quotes" : "always",
"selector-pseudo-element-colon-notation" : "double",
"stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
"stylistic/block-opening-brace-space-before" : "always",
"naturalcrit/declaration-colon-min-space-before" : 1,
"stylistic/declaration-block-trailing-semicolon" : "always",
"stylistic/declaration-colon-space-after" : "always",
"stylistic/number-leading-zero" : "always",
"function-url-quotes" : ["always", { "except": ["empty"] }],
"function-url-scheme-disallowed-list" : ["data","http"],
"comment-whitespace-inside" : "always",
"stylistic/string-quotes" : "single",
"stylistic/media-feature-range-operator-space-before" : "always",
"stylistic/media-feature-range-operator-space-after" : "always",
"stylistic/media-feature-parentheses-space-inside" : "never",
"stylistic/media-feature-colon-space-before" : "always",
"stylistic/media-feature-colon-space-after" : "always",
"naturalcrit/declaration-colon-align" : true,
"naturalcrit/declaration-block-multi-line-min-declarations": 1
}
}

View File

@@ -1,4 +1,4 @@
FROM node:16.13-alpine
FROM node:18-alpine
RUN apk --no-cache add git
ENV NODE_ENV=docker
@@ -10,11 +10,11 @@ WORKDIR /usr/src/app
# This improves caching so we don't have to download the dependencies every time the code changes
COPY package.json ./
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
RUN yarn install --ignore-scripts
RUN npm install --ignore-scripts
# Bundle app source and build application
COPY . .
RUN yarn build
RUN npm run build
EXPOSE 8000
CMD [ "yarn", "start" ]
CMD [ "npm", "start" ]

View File

@@ -80,6 +80,50 @@ pre {
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Wednesday 28/06/2023 - v3.9.1
{{taskList
##### G-Ambatte
* [x] Better error pages with more useful information
Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
}}
### Friday 02/06/2023 - v3.9.0
{{taskList
##### Calculuschild
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
Fixes issue [#2408](https://github.com/naturalcrit/homebrewery/issues/2408)
* [x] Pressing tab now indents with spaces instead of tab character; fixes several issues with Markdown lists
Fixes issues [#2092](https://github.com/naturalcrit/homebrewery/issues/2092), [#1556](https://github.com/naturalcrit/homebrewery/issues/1556)
* [x] Rename `naturalCritLogo.svg` to `naturalCritLogoRed.svg`. Those using the {{beta BETA}} coverPage snippet may need to update that text to make the NaturalCrit logo appear again.
##### G-Ambatte
* [x] Fix strange animation of image masks
Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
##### 5e-Cleric
* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
* [x] New {{openSans **PHB → {{fac,book-back-cover}} BACK COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
* [x] New {{openSans **TEXT EDITOR → {{fas,fa-bars}} INDEX** }} snippet for V3!
* [x] Fix highlighting of curly braces inside comments
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
}}
### Wednesday 12/04/2023 - v3.8.0
{{taskList
@@ -101,7 +145,7 @@ Fixes issue [#2595](https://github.com/naturalcrit/homebrewery/issues/2595)
Fixes issues [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
* [x] Fix internal links inside `<div>` blocks not receiving the `target=_self` attribute
* [x] Fix internal links inside `<\div>` blocks not receiving the `target=_self` attribute
Fixes issues [#2680](https://github.com/naturalcrit/homebrewery/issues/2680)
@@ -111,7 +155,7 @@ Fixes issues [#1679](https://github.com/naturalcrit/homebrewery/issues/1679)
* [x] Add local Windows install script via Chocolatey
##### 5e-Clerc
##### 5e-Cleric
* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.

View File

@@ -108,6 +108,12 @@ const BrewRenderer = createClass({
return false;
},
sanitizeScriptTags : function(content) {
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
},
renderPageInfo : function(){
return <div className='pageInfo' ref='main'>
<div>
@@ -135,18 +141,20 @@ const BrewRenderer = createClass({
renderStyle : function() {
if(!this.props.style) return;
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.props.style}\n} </style>` }} />;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.props.style}\n</style>` }} />;
const cleanStyle = this.sanitizeScriptTags(this.props.style);
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.sanitizeScriptTags(this.props.style)}\n} </style>` }} />;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
},
renderPage : function(pageText, index){
const cleanPageText = this.sanitizeScriptTags(pageText);
if(this.props.renderer == 'legacy')
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />;
else {
pageText += `\n\n&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
return (
<div className='page' id={`p${index + 1}`} key={index} >
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(cleanPageText) }} />
</div>
);
}
@@ -185,6 +193,12 @@ const BrewRenderer = createClass({
}, 100);
},
emitClick : function(){
// console.log('iFrame clicked');
if(!window || !document) return;
document.dispatchEvent(new MouseEvent('click'));
},
render : function(){
//render in iFrame so broken code doesn't crash the site.
//Also render dummy page while iframe is mounting.
@@ -203,7 +217,9 @@ const BrewRenderer = createClass({
<Frame id='BrewRenderer' initialContent={this.state.initialContent}
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
contentDidMount={this.frameDidMount}>
contentDidMount={this.frameDidMount}
onClick={()=>{this.emitClick();}}
>
<div className={'brewRenderer'}
onScroll={this.handleScroll}
style={{ height: this.state.height }}>

View File

@@ -25,14 +25,13 @@ const NotificationPopup = createClass({
return (
<>
<li key='psa'>
<em>Broken <b>CoverPage</b> snippet</em> <br />
Those of you who have been trying out our Cover Page snippet may
notice that the cover page no longer displays correctly. Due to some
small tweaks of this BETA feature, the CSS class has been renamed
from "coverPage" to "frontCover". Simply change the text to "frontCover"
and it should again function as before. Remember that any snippet
marked "beta" may have a similar change in the future as we
encounter any bugs or reworks.
<em>Broken default logo on <b>CoverPage</b> </em> <br />
If you have used the Cover Page snippet and notice the Naturalcrit
logo is showing as a broken image, this is due to some small tweaks
of this BETA feature. To fix the logo in your cover page, rename
the image link <b>"/assets/naturalCritLogoRed.svg"</b>. Remember
that any snippet marked "BETA" may have a similar change in the
future as we encounter any bugs or reworks.
</li>
<li key='googleDriveFolder'>

View File

@@ -323,7 +323,8 @@ const Editor = createClass({
theme={this.props.brew.theme}
undo={this.undo}
redo={this.redo}
historySize={this.historySize()} />
historySize={this.historySize()}
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
{this.renderEditor()}
</div>

View File

@@ -19,17 +19,17 @@
background-color : fade(#299, 15%);
border-bottom : #299 solid 1px;
}
.block{
.block:not(.cm-comment){
color : purple;
font-weight : bold;
//font-style: italic;
}
.inline-block{
.inline-block:not(.cm-comment){
color : red;
font-weight : bold;
//font-style: italic;
}
.injection{
.injection:not(.cm-comment){
color : green;
font-weight : bold;
}

View File

@@ -15,8 +15,8 @@ ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
const execute = function(val, brew){
if(_.isFunction(val)) return val(brew);
const execute = function(val, props){
if(_.isFunction(val)) return val(props);
return val;
};
@@ -33,7 +33,8 @@ const Snippetbar = createClass({
renderer : 'legacy',
undo : ()=>{},
redo : ()=>{},
historySize : ()=>{}
historySize : ()=>{},
cursorPos : {}
};
},
@@ -105,6 +106,7 @@ const Snippetbar = createClass({
snippets={snippetGroup.snippets}
key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick}
cursorPos={this.props.cursorPos}
/>;
});
},
@@ -165,7 +167,7 @@ const SnippetGroup = createClass({
},
handleSnippetClick : function(e, snippet){
e.stopPropagation();
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
this.props.onSnippetClick(execute(snippet.gen, this.props));
},
renderSnippets : function(snippets){
return _.map(snippets, (snippet)=>{

View File

@@ -140,7 +140,7 @@ const StringArrayEditor = createClass({
</div>
</div>
{this.props.notes ? this.props.notes.map((n)=><p><small>{n}</small></p>) : null}
{this.props.notes ? this.props.notes.map((n, index)=><p key={index}><small>{n}</small></p>) : null}
</div>
</div>;
}

View File

@@ -9,7 +9,7 @@ const EditPage = require('./pages/editPage/editPage.jsx');
const UserPage = require('./pages/userPage/userPage.jsx');
const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx');
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx');
const AccountPage = require('./pages/accountPage/accountPage.jsx');
@@ -78,6 +78,7 @@ const Homebrew = createClass({
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
</Routes>

View File

@@ -1,277 +1,272 @@
@import 'naturalcrit/styles/colors.less';
@navbarHeight : 28px;
@keyframes pinkColoring {
//from {color: white;}
//to {color: red;}
0% {color: pink;}
50% {color: pink;}
75% {color: red;}
100% {color: pink;}
}
.homebrew nav{
.homebrewLogo{
.animate(color);
font-family : CodeBold;
font-size : 12px;
color : white;
div{
margin-top : 2px;
margin-bottom : -2px;
}
&:hover{
color : @blue;
}
}
.editTitle.navItem{
padding : 2px 12px;
input{
width : 250px;
margin : 0;
padding : 2px;
background-color : transparent;
font-family : 'Open Sans', sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
border : 1px solid @blue;
outline : none;
}
.charCount{
display : inline-block;
vertical-align : bottom;
margin-left : 8px;
color : #666;
text-align : right;
&.max{
color : @red;
}
}
}
.brewTitle.navItem{
height: 100%;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
text-transform : initial;
flex-grow : 1;
background-color: transparent;
}
.save-menu {
.dropdown {
z-index: 1000;
}
.navItem i.fa-power-off {
color : red;
&.active {
color : rgb(0, 182, 52);
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765))
}
}
}
.patreon.navItem{
border-left : 1px solid #666;
border-right : 1px solid #666;
&:hover i {
color: red;
}
i{
.animate(color);
animation-name: pinkColoring;
animation-duration: 2s;
color: pink;
}
}
.recent.navItem {
position : relative;
.dropdown{
position : absolute;
top : 28px;
left : 0px;
z-index : 10000;
width : 100%;
overflow : hidden auto;
max-height : ~"calc(100vh - 28px)";
scrollbar-color : #666 #333;
scrollbar-width : thin;
h4{
display : block;
box-sizing : border-box;
padding : 5px 0px;
background-color : #333;
font-size : 0.8em;
color : #bbb;
text-align : center;
border-top : 1px solid #888;
&:nth-of-type(1){ background-color: darken(@teal, 20%); }
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
}
.item{
#backgroundColorsHover;
.animate(background-color);
position : relative;
display : block;
box-sizing : border-box;
padding : 8px 5px 13px;
background-color : #333;
color : white;
text-decoration : none;
border-top : 1px solid #888;
overflow : clip;
.clear{
display : none;
position : absolute;
top : 50%;
transform : translateY(-50%);
right : 0px;
width : 20px;
height : 100%;
background-color : #333;
opacity : 70%;
border-radius : 3px;
&:hover {
opacity : 100%;
}
i {
text-align : center;
font-size : 10px;
margin : 0;
height :100%;
width :100%;
}
}
&:hover{
background-color : @blue;
.clear{
display : grid;
place-content : center;
}
}
.title{
display : inline-block;
overflow : hidden;
width : 100%;
text-overflow : ellipsis;
white-space : nowrap;
}
.time{
position : absolute;
right : 2px;
bottom : 2px;
font-size : 0.7em;
color : #888;
}
}
}
}
.metadata.navItem {
position : relative;
padding: 0;
align-items: center;
display : flex;
flex-grow: 1;
height: 100%;
i{
margin-right: 10px;
}
.window{
position : absolute;
bottom : 0;
width : 440px;
left : 50%;
max-height : ~"calc(100vh - 28px)";
background-color : #333;
border : 3px solid #444;
border-top : unset;
border-radius : 0 0 5px 5px;
box-shadow : inset 0 7px 9px -7px #111;
display : flex;
flex-flow : row wrap;
justify-content : flex-start;
align-content : baseline;
padding : 0px 10px 5px;
margin : 0 auto;
z-index : -1;
transition : transform 0.4s, opacity 0.4s;
&.active{
transform: translateX(-50%) translateY(100%);
opacity: 1;
}
&.inactive{
transform: translateX(-50%) translateY(0%);
opacity: 0;
}
.row{
display : flex;
flex-flow : row wrap;
width : 100%;
h4{
display : block;
box-sizing : border-box;
padding : 5px 0px;
color : #bbb;
text-align : center;
flex-basis : 20%;
flex-grow : 1;
min-width : 76px;
}
p{
font-family : 'Open Sans', sans-serif;
font-size : 10px;
font-weight : normal;
text-transform : initial;
padding : 5px 0;
flex-basis : 80%;
flex-grow : 1;
.tag{
border : 2px solid grey;
padding : 2px;
margin : 2px 2px;
display : inline-block;
border-radius : 5px;
background-color : #444;
}
a.userPageLink{
text-decoration: none;
color: white;
&:hover{
text-decoration: underline;
}
}
}
&:nth-of-type(even){
background-color: #555;
}
}
}
}
.warning.navItem{
position : relative;
background-color : @orange;
color : white;
&:hover>.dropdown{
visibility : visible;
}
.dropdown{
position : absolute;
display : block;
top : 28px;
left : 0px;
visibility : hidden;
z-index : 10000;
box-sizing : border-box;
width : 100%;
padding : 13px 5px;
background-color : #333;
text-align : center;
}
}
.account.navItem{
min-width: 100px;
}
.account.username.navItem{
text-transform: none;
}
}
@import "naturalcrit/styles/colors.less";
@navbarHeight : 28px;
@keyframes pinkColoring {
0% {color : pink;}
50% {color : pink;}
75% {color : red;}
100% {color : pink;}
}
.homebrew nav {
.homebrewLogo {
.animate(color);
font-family : CodeBold;
font-size : 12px;
color : white;
div {
margin-top : 2px;
margin-bottom : -2px;
}
&:hover {
color : @blue;
}
}
.editTitle.navItem {
padding : 2px 12px;
input {
font-family : "Open Sans", sans-serif;
font-size : 12px;
font-weight : 800;
width : 250px;
margin : 0;
padding : 2px;
text-align : center;
color : white;
border : 1px solid @blue;
outline : none;
background-color : transparent;
}
.charCount {
display : inline-block;
margin-left : 8px;
text-align : right;
vertical-align : bottom;
color : #666;
&.max {
color : @red;
}
}
}
.brewTitle.navItem {
font-size : 12px;
font-weight : 800;
height : 100%;
text-align : center;
text-transform : initial;
color : white;
background-color : transparent;
flex-grow : 1;
}
.save-menu {
.dropdown {
z-index : 1000;
}
.navItem i.fa-power-off {
color : red;
&.active {
color : rgb(0, 182, 52);
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
}
}
}
.patreon.navItem {
border-right : 1px solid #666;
border-left : 1px solid #666;
&:hover i {
color : red;
}
i {
.animate(color);
animation-name : pinkColoring;
animation-duration : 2s;
color : pink;
}
}
.recent.navDropdownContainer {
position : relative;
.navDropdown .navItem {
overflow : hidden auto;
max-height : ~"calc(100vh - 28px)";
scrollbar-color : #666 #333;
scrollbar-width : thin;
#backgroundColorsHover;
.animate(background-color);
position : relative;
display : block;
overflow : clip;
box-sizing : border-box;
padding : 8px 5px 13px;
text-decoration : none;
color : white;
border-top : 1px solid #888;
background-color : #333;
.clear {
position : absolute;
top : 50%;
right : 0;
display : none;
width : 20px;
height : 100%;
transform : translateY(-50%);
opacity : 70%;
border-radius : 3px;
background-color : #333;
&:hover {
opacity : 100%;
}
i {
font-size : 10px;
width : 100%;
height : 100%;
margin : 0;
text-align : center;
}
}
&:hover {
background-color : @blue;
.clear {
display : grid;
place-content : center;
}
}
.title {
display : inline-block;
overflow : hidden;
width : 100%;
white-space : nowrap;
text-overflow : ellipsis;
}
.time {
font-size : 0.7em;
position : absolute;
right : 2px;
bottom : 2px;
color : #888;
}
&.header {
display : block;
box-sizing : border-box;
padding : 5px 0;
text-align : center;
color : #BBB;
border-top : 1px solid #888;
background-color : #333;
&:nth-of-type(1) {
background-color : darken(@teal, 20%);
}
&:nth-of-type(2) {
background-color : darken(@purple, 30%);
}
}
}
}
.metadata.navItem {
position : relative;
display : flex;
align-items : center;
height : 100%;
padding : 0;
flex-grow : 1;
i {
margin-right : 10px;
}
.window {
position : absolute;
z-index : -1;
bottom : 0;
left : 50%;
display : flex;
justify-content : flex-start;
width : 440px;
max-height : ~"calc(100vh - 28px)";
margin : 0 auto;
padding : 0 10px 5px;
transition : transform 0.4s, opacity 0.4s;
border : 3px solid #444;
border-top : unset;
border-radius : 0 0 5px 5px;
background-color : #333;
box-shadow : inset 0 7px 9px -7px #111;
flex-flow : row wrap;
align-content : baseline;
&.active {
transform : translateX(-50%) translateY(100%);
opacity : 1;
}
&.inactive {
transform : translateX(-50%) translateY(0%);
opacity : 0;
}
.row {
display : flex;
width : 100%;
flex-flow : row wrap;
h4 {
display : block;
box-sizing : border-box;
min-width : 76px;
padding : 5px 0;
text-align : center;
color : #BBB;
flex-basis : 20%;
flex-grow : 1;
}
p {
font-family : "Open Sans", sans-serif;
font-size : 10px;
font-weight : normal;
padding : 5px 0;
text-transform : initial;
flex-basis : 80%;
flex-grow : 1;
.tag {
display : inline-block;
margin : 2px 2px;
padding : 2px;
border : 2px solid grey;
border-radius : 5px;
background-color : #444;
}
a.userPageLink {
text-decoration : none;
color : white;
&:hover {
text-decoration : underline;
}
}
}
&:nth-of-type(even) {
background-color : #555;
}
}
}
}
.warning.navItem {
position : relative;
color : white;
background-color : @orange;
&:hover > .dropdown {
visibility : visible;
}
.dropdown {
position : absolute;
z-index : 10000;
top : 28px;
left : 0;
display : block;
visibility : hidden;
box-sizing : border-box;
width : 100%;
padding : 13px 5px;
text-align : center;
background-color : #333;
}
}
.account.navItem {
min-width : 100px;
}
.account.username.navItem {
text-transform : none;
}
}

View File

@@ -121,6 +121,7 @@ const RecentItems = createClass({
removeItem : function(url, evt){
evt.preventDefault();
evt.stopPropagation();
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
@@ -139,11 +140,11 @@ const RecentItems = createClass({
},
renderDropdown : function(){
if(!this.state.showDropdown) return null;
// if(!this.state.showDropdown) return null;
const makeItems = (brews)=>{
return _.map(brews, (brew, i)=>{
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
return <a className='navItem' href={brew.url} key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
<span className='title'>{brew.title || '[ no title ]'}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</span>
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
@@ -151,25 +152,25 @@ const RecentItems = createClass({
});
};
return <div className='dropdown'>
return <>
{(this.props.showEdit && this.props.showView) ?
<h4>edited</h4> : null }
<Nav.item className='header'>edited</Nav.item> : null }
{this.props.showEdit ?
makeItems(this.state.edit) : null }
{(this.props.showEdit && this.props.showView) ?
<h4>viewed</h4> : null }
<Nav.item className='header'>viewed</Nav.item> : null }
{this.props.showView ?
makeItems(this.state.view) : null }
</div>;
</>;
},
render : function(){
return <Nav.item icon='fas fa-history' color='grey' className='recent'
onMouseEnter={()=>this.handleDropdown(true)}
onMouseLeave={()=>this.handleDropdown(false)}>
{this.props.text}
return <Nav.dropdown className='recent'>
<Nav.item icon='fas fa-history' color='grey' >
{this.props.text}
</Nav.item>
{this.renderDropdown()}
</Nav.item>;
</Nav.dropdown>;
}
});

View File

@@ -1,47 +1,52 @@
.uiPage{
.content{
overflow-y : hidden;
width : 90vw;
background-color: #f0f0f0;
font-family: 'Open Sans';
margin-left: auto;
margin-right: auto;
margin-top: 25px;
padding: 2% 4%;
font-size: 0.8em;
line-height: 1.8em;
.dataGroup{
padding: 6px 20px 15px;
border: 2px solid black;
border-radius: 5px;
margin: 5px 0px;
}
h1, h2, h3, h4{
font-weight: 900;
text-transform: uppercase;
margin: 0.5em 30% 0.25em 0;
border-bottom: 2px solid slategrey;
}
h1 {
font-size: 2em;
border-bottom: 2px solid darkslategrey;
margin-bottom: 0.5em;
margin-right: 0;
}
h2 {
font-size: 1.75em;
}
h3 {
font-size: 1.5em;
svg {
width: 19px;
.homebrew {
.uiPage.sitePage {
.content {
width : ~"min(90vw, 1000px)";
padding : 2% 4%;
margin-top : 25px;
margin-right : auto;
margin-left : auto;
overflow-y : scroll;
font-family : 'Open Sans';
font-size : 0.8em;
line-height : 1.8em;
background-color : #F0F0F0;
.dataGroup {
padding : 6px 20px 15px;
margin : 5px 0px;
border : 2px solid black;
border-radius : 5px;
}
h1, h2, h3, h4 {
width : 100%;
margin : 0.5em 30% 0.25em 0;
font-weight : 900;
text-transform : uppercase;
border-bottom : 2px solid slategrey;
}
h1 {
margin-right : 0;
margin-bottom : 0.5em;
font-size : 2em;
border-bottom : 2px solid darkslategrey;
}
h2 { font-size : 1.75em; }
h3 {
font-size : 1.5em;
svg { width : 19px; }
}
h4 { font-size : 1.25em; }
strong { font-weight : bold; }
em { font-style : italic; }
ul {
padding-left : 1.25em;
list-style : square;
}
.blank {
height : 1em;
margin-top : 0;
& + * { margin-top : 0; }
}
}
h4 {
font-size: 1.25em;
}
strong {
font-weight: bold;
}
}
}
}

View File

@@ -4,44 +4,37 @@ const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const UIPage = require('../basePages/uiPage/uiPage.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
const ErrorIndex = require('./errors/errorIndex.js');
const ErrorPage = createClass({
displayName : 'ErrorPage',
getDefaultProps : function() {
return {
ver : '0.0.0',
errorId : ''
errorId : '',
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
error : {}
};
},
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
render : function(){
return <div className='errorPage sitePage'>
<Navbar ver={this.props.ver}>
<Nav.section>
<Nav.item className='errorTitle'>
Crit Fail!
</Nav.item>
</Nav.section>
const errorText = ErrorIndex(this.props)[this.props.brew.HBErrorCode.toString()] || '';
<Nav.section>
<PatreonNavItem />
<HelpNavItem />
<RecentNavItem />
</Nav.section>
</Navbar>
<div className='content'>
<BrewRenderer text={this.text} />
return <UIPage brew={{ title: 'Crit Fail!' }}>
<div className='dataGroup'>
<div className='errorTitle'>
<h1>{`Error ${this.props.brew.status || '000'}`}</h1>
<h4>{this.props.brew.text || 'No error text'}</h4>
</div>
<hr />
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
</div>
</div>;
</UIPage>;
}
});

View File

@@ -1,5 +1,13 @@
.errorPage{
.errorTitle{
background-color: @orange;
.homebrew {
.uiPage.sitePage {
.errorTitle {
//background-color: @orange;
color : #D02727;
text-align : center;
}
.content {
h1, h2, h3, h4 { border-bottom : none; }
hr { border-bottom : 2px solid slategrey; }
}
}
}

View File

@@ -0,0 +1,126 @@
const dedent = require('dedent-tabs').default;
const loginUrl = 'https://www.naturalcrit.com/login';
const errorIndex = (props)=>{
return {
// Default catch all
'00' : dedent`
## An unknown error occurred!
We aren't sure what happened, but our server wasn't able to find what you
were looking for.`,
// General Google load error
'01' : dedent`
## An error occurred while retrieving this brew from Google Drive!
Google reported an error while attempting to retrieve a brew from this link.`,
// Google Drive - 404 : brew deleted or access denied
'02' : dedent`
## We can't find this brew in Google Drive!
This file was saved on Google Drive, but this link doesn't work anymore.
${ props.brew.authors?.length > 0
? `Note that this brew belongs to the Homebrewery account **${ props.brew.authors[0] }**,
${ props.brew.account
? `which is
${props.brew.authors[0] == props.brew.account
? `your account.`
: `not your account (you are currently signed in as **${props.brew.account}**).`
}`
: 'and you are not currently signed in to any account.'
}`
: ''
}
The Homebrewery cannot delete files from Google Drive on its own, so there
are three most likely possibilities:
:
- **The Google Drive files may have been accidentally deleted.** Look in
the Google Drive account that owns this brew (or ask the owner to do so),
and make sure the Homebrewery folder is still there, and that it holds your brews
as text files.
- **You may have changed the sharing settings for your files.** If the files
are still on Google Drive, change all of them to be shared *with everyone who has
the link* so the Homebrewery can access them.
- **The Google Account may be closed.** Google may have removed the account
due to inactivity or violating a Google policy. Make sure the owner can
still access Google Drive normally and upload/download files to it.
:
If the file isn't found, Google Drive usually puts your file in your Trash folder for
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
You can also find the Activity tab on the right side of the Google Drive page, which
shows the recent activity on Google Drive. This can help you pin down the exact date
the brew was deleted or moved, and by whom.
:
If the brew still isn't found, some people have had success asking Google to recover
accidentally deleted files at this link:
https://support.google.com/drive/answer/1716222?hl=en&ref_topic=7000946.
At the bottom of the page there is a button that says *Send yourself an Email*
and you will receive instructions on how to request the files be restored.
:
Also note, if you prefer not to use your Google Drive for storage, you can always
change the storage location of a brew by clicking the Google drive icon by the
brew title and choosing *transfer my brew to/from Google Drive*.`,
// User is not Authors list
'03' : dedent`
## Current signed-in user does not have editor access to this brew.
If you believe you should have access to this brew, ask one of its authors to invite you
as an author by opening the Edit page for the brew, viewing the {{fa,fa-info-circle}}
**Properties** tab, and adding your username to the "invited authors" list. You can
then try to access this document again.
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
// User is not signed in; must be a user on the Authors List
'04' : dedent`
## Sign-in required to edit this brew.
You must be logged in to one of the accounts listed as an author of this brew.
User is not logged in. Please log in [here](${loginUrl}).
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
// Brew load error
'05' : dedent`
## No Homebrewery document could be found.
The server could not locate the Homebrewery document. It was likely deleted by
its owner.
**Requested access:** ${props.brew.accessType}
**Brew ID:** ${props.brew.brewId}`,
// Brew save error
'06' : dedent`
## Unable to save Homebrewery document.
An error occurred wil attempting to save the Homebrewery document.`,
// Brew delete error
'07' : dedent`
## Unable to delete Homebrewery document.
An error occurred while attempting to remove the Homebrewery document.
**Brew ID:** ${props.brew.brewId}`,
// Author delete error
'08' : dedent`
## Unable to remove user from Homebrewery document.
An error occurred while attempting to remove the user from the Homebrewery document author list!
**Brew ID:** ${props.brew.brewId}`,
};
};
module.exports = errorIndex;

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 541.53217 512"
version="1.1"
id="back-cover-icon"
sodipodi:docname="book-front-cover.svg"
width="541.53217"
height="512"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs22131" />
<sodipodi:namedview
id="namedview22129"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.39257813"
inkscape:cx="-263.64179"
inkscape:cy="444.49751"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg22127" />
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<g id="g20308" transform="matrix(3.7795276,0,0,3.7795276,-201.76367,-251.58203)">
<path id="rect20232" d="M95.1,66.6h-8.5c-4.7,0-8.5,3.8-8.5,8.5v21.4c3.5-0.4,7.4-0.5,12-0.5c0.7,0,0.6,0,1.2,0
c0-2.4,0-4.2,0.3-6.2c0.3-2.2,2.2-5.8,3.5-7c0.9-0.9,3-3.2,7-3.7c1-0.1,2-0.1,2.8,0c2.6,0.3,4.6,1.6,6.1,2.6
c3.9,2.7,7.4,6.4,14.8,13.8c6.3,6.3,9.8,9.8,12,12.4c1.1,1.3,2.1,2.4,2.9,4c0.9,1.7,1.4,4.2,1.4,5.6c0,1.4-0.5,4-1.4,5.6
c-0.9,1.6-1.8,2.7-2.9,4c-2.2,2.6-5.6,6-11.8,12.2c-3.8,3.8-7.4,7.3-10.2,9.9c-1.4,1.3-2.6,2.4-3.6,3.3c-0.5,0.4-1,0.8-1.5,1.2
c-0.3,0.2-0.5,0.4-1,0.7s-0.7,0.7-2.8,1.2c-4.3,1.1-6.3,0.4-9.4-1.3c-0.5-0.3-1.9-0.9-3.3-2.6c-1.4-1.7-2.1-3.7-2.4-5.1
c-0.5-2.4-0.5-4.3-0.6-7.2c-3.9,0-6,0.1-6.5,0.1c-0.5,0.1,0.2-0.2-1.2,0.5c-1.7,0.8-3.6,2.8-4.4,4.5c-0.3,0.8-0.5,1-0.6,6.6
c-0.1,2.2-0.2,4.3-0.4,6c0,0.3-0.1,0.6-0.1,0.8v1.9c0,4.7,3.8,8.5,8.5,8.5v16.9c-4.7,0-8.5,3.8-8.5,8.5c0,4.7,3.8,8.5,8.5,8.5h8.5
h76.2c14,0,25.4-11.4,25.4-25.4V92c0-14-11.4-25.4-25.4-25.4L95.1,66.6z M171.3,168.2c4.7,0,8.5,3.8,8.5,8.5c0,4.7-3.8,8.5-8.5,8.5
h-67.7v-16.9L171.3,168.2L171.3,168.2z"/>
<path id="path20297" d="M63.4,158c1.8,1.6,4.5,1.9,5.5,0.7c0.3-0.4,0.7-4,0.8-8.1c0.2-5.9,0.5-7.9,1.4-10c1.7-3.7,4.9-7,8.6-8.9
c3.1-1.5,3.6-1.6,11.7-1.6h8.5l0.3,7.6c0.3,7.5,0.3,7.7,1.7,8.5c0.8,0.5,2.1,0.7,2.8,0.5c0.8-0.2,7.4-6.4,14.9-13.9
c12.4-12.4,13.5-13.7,13.5-15.5c0-1.8-1.1-3.1-13.8-15.7c-14.7-14.7-15.4-15.2-18-12.7c-1,1-1.1,1.9-1.1,7.6c0,3.6-0.2,6.9-0.3,7.4
c-0.3,0.8-1.7,0.9-9.8,0.9c-15.6,0-21.1,1.7-27.9,8.5c-6.5,6.5-8.8,12-8.8,21.1c0,4.7,0.3,6.8,1.3,9.8
C56.2,148.6,60.7,155.7,63.4,158L63.4,158z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -2,7 +2,7 @@
<svg
viewBox="0 0 541.53217 512"
version="1.1"
id="svg22127"
id="front-cover-icon"
sodipodi:docname="book-front-cover.svg"
width="541.53217"
height="512"

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 704.00001 512"
version="1.1"
id="svg22127"
sodipodi:docname="book-part-cover.svg"
width="704"
height="512"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
inkscape:export-filename="InsideCover3.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs22131" />
<sodipodi:namedview
id="namedview22129"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.6685671"
inkscape:cx="299.8951"
inkscape:cy="80.021886"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg22127" />
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
id="path2161-6"
style="color:#000000;fill:#000000;stroke-width:1;-inkscape-stroke:none;paint-order:stroke fill markers"
d="M 208,0 C 147.0078,0 94.429433,14.25071 60.367188,26.66992 23.520854,39.96036 0,76.16076 0,112.95896 v 317.8321 c 0,59.8499 56.949847,92.6546 107.47266,76.6035 l -0.1543,0.049 c 26.46715,-8.335 74.84649,-18.3965 100.68164,-18.3965 17.25807,0 61.31688,10.6183 85.14453,18.8438 l 0.0508,0.018 0.0527,0.018 c 19.82627,6.5858 40.84117,4.9222 58.99804,-3.0762 18.04267,7.8799 38.84257,9.6126 58.33594,3.1328 l 0.13672,-0.045 0.13672,-0.047 c 23.88445,-8.0588 67.88646,-18.8437 85.14453,-18.8437 25.83515,0 74.22549,10.0266 100.68164,18.3964 l 0.1543,0.049 0.15625,0.049 C 647.13371,523.05316 704,490.64216 704,430.79226 v -317.8321 c 0,-36.8274 -23.49583,-72.8235 -60.00977,-86.25583 l -0.16015,-0.0606 -0.16211,-0.0566 C 609.79193,14.33005 557.11269,0.0012 496,0.0012 434.5671,0.0012 387.12553,14.01354 352,34.94261 316.87446,14.01344 269.4331,0.0012 208,0.0012 Z m 0,32.00977 c 58.3999,0 103.40004,18.89469 123,33.63279 3.3,2.4564 5,6.4246 5,10.3926 v 356.5508 c 0,10.7702 -11.70041,18.2326 -22.40039,14.6426 -26.59996,-8.9751 -71.69966,-22.2012 -105.59961,-22.2012 -38.49993,0 -88.40045,11.4317 -119.900391,21.3516 C 76.799621,449.96896 64,442.03166 64,430.78906 V 80.94726 C 64,69.51586 70.799631,58.93546 82.099609,54.87306 110.29956,44.57516 157.50009,32.00977 208,32.00977 Z m 288,0 c 50.49991,0 97.70044,12.56619 125.90039,22.76949 C 633.20037,58.93616 640,69.51586 640,80.94726 v 349.8418 c 0,11.2426 -12.79963,19.0854 -24.09961,15.5899 -31.49995,-9.9199 -81.40046,-21.3516 -119.90039,-21.3516 -33.89995,0 -78.99966,13.2261 -105.59961,22.2012 C 379.60041,450.81856 368,443.35616 368,432.58596 V 76.03516 c 0,-3.968 1.60001,-7.9362 5,-10.3926 19.59997,-14.7381 64.6001,-33.63279 123,-33.63279 z M 335.52734,45.75386 c -0.1289,0.093 -0.23137,0.2032 -0.35937,0.2969 -0.198,0.1477 -0.428,0.2796 -0.625,0.4278 z m 33.67969,0.5372 0.24805,0.1875 c -0.0427,-0.033 -0.0937,-0.061 -0.13672,-0.094 -0.0393,-0.03 -0.0713,-0.064 -0.11133,-0.094 z" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 417.64553,213.53304 c 88.71546,-18.9285 95.50522,-18.6158 172.79707,0.054"
id="path2371-8"
sodipodi:nodetypes="cc" />
<path
id="path2315"
style="stroke-width:67.6532;stroke-linejoin:bevel;paint-order:stroke markers fill;stop-color:#000000"
inkscape:transform-center-x="-3.4164388e-06"
inkscape:transform-center-y="-8.443352"
d="m 505.27489,52.89544 25.98603,52.6535 58.10652,8.4434 -42.04628,40.985 9.92578,57.8717 -51.97205,-27.3234 -51.97204,27.3234 9.92578,-57.8717 -42.04627,-40.985 58.10651,-8.4434 z" />
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -34,15 +34,18 @@
.mask-center {
content: url('../icons/mask-center.svg');
}
.fa-file-c {
content: url('../icons/fa-file-c.svg');
}
.book-front-cover {
content: url('../icons/book-front-cover.svg');
}
.book-back-cover {
content: url('../icons/book-back-cover.svg');
}
.book-inside-cover {
content: url('../icons/book-inside-cover.svg');
}
.book-part-cover {
content: url('../icons/book-part-cover.svg');
}
.davek {
content: url('../icons/Davek.svg');
}

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 610.4 816.5" style="enable-background:new 0 0 610.4 816.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#FFFFFF;stroke:#FFFFFF;stroke-width:20;stroke-miterlimit:10;}
</style>
<title>fa-file-c</title>
<g id="Layer_2_1_">
<g id="Layer_1-2">
<g id="page">
<path id="page-2" d="M610.3,468.3c0,77.3,0.2,154.5,0,231.8s-39.8,116.5-116.8,116.4c-127.6,0-255.1,0-382.7,0
c-68.1,0-110.5-41.7-110.6-109.8c-0.2-197.7-0.2-395.5,0-593.2c0-68.4,43.2-110.9,112.1-111c90-0.1,180,0.2,270-0.2
c12.8,0,21.5,0.6,32.9,4c17.1,5,152.7,150.7,190.7,188.8c-0.7,18-6,5.7,1.4,35.1c0,6.8,3.1,11.2,3.1,18.1
C610.2,320.8,610.3,395.7,610.3,468.3z"/>
<path id="white_corner" class="st0" d="M364.1,0v200c0,9.3,1.7,25.6,13.1,36.8c12,11.7,28.8,12.1,37.5,12.2
c119.8,1.3,195.6,0.4,195.6,0.4l0,0l-0.3-54.3l-197,1l3-192L364.1,0z"/>
</g>
<path class="st1" d="M317.7,719.8c-38.3,0-71-8.1-98.3-24.3c-27.2-16.2-48.1-39.2-62.7-69C142.3,596.8,135,561.2,135,520
c0-30.9,4.1-58.6,12.4-83.1c8.3-24.5,20.2-45.3,35.9-62.4c15.6-17.1,34.9-30.4,57.7-39.8s48.4-14.1,76.7-14.1
c22.1-0.1,44,3.1,65.1,9.7c20.6,6.4,38.4,15.9,53.5,28.4c4.8,3.7,8,7.8,9.7,12.4c1.6,4.2,1.8,8.9,0.6,13.2
c-1.2,4.1-3.5,7.7-6.6,10.5c-3.1,2.8-7.2,4.2-11.3,4.1c-4.4,0-9.4-1.8-14.9-5.5c-13-10.5-27.7-18.6-43.6-23.7
c-16.6-5.3-33.9-7.9-51.3-7.7c-29.1,0-53.7,6.2-74,18.5s-35.5,30.3-45.8,53.8c-10.3,23.6-15.4,52.1-15.4,85.5s5.1,62.1,15.4,85.9
c10.3,23.7,25.6,41.8,45.8,54.1c20.2,12.3,44.9,18.5,74,18.5c17.4,0.1,34.8-2.6,51.3-8c16.2-5.3,31.3-13.5,44.7-24
c5.5-3.7,10.5-5.4,14.9-5.3c4,0.1,7.9,1.5,11,4.1c3,2.7,5.2,6.1,6.4,9.9c1.3,4.1,1.3,8.6,0,12.7c-1.3,4.4-4.1,8.3-8.6,11.6
c-15.5,13.3-33.6,23.3-54.4,30.1C362.7,716.6,340.3,720,317.7,719.8z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -11,6 +11,7 @@ const template = async function(name, title='', props = {}){
return `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href=${`/${name}/bundle.css`} rel='stylesheet' />

33583
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.8.0",
"version": "3.9.1",
"engines": {
"node": "16.13.x"
"node": ">=18.16.x"
},
"repository": {
"type": "git",
@@ -16,6 +16,8 @@
"builddev": "node scripts/buildHomebrew.js --dev",
"lint": "eslint --fix **/*.{js,jsx}",
"lint:dry": "eslint **/*.{js,jsx}",
"stylelint": "stylelint --fix **/*.{less}",
"stylelint:dry": "stylelint **/*.less",
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
"verify": "npm run lint && npm test",
"test": "jest --runInBand",
@@ -76,11 +78,11 @@
]
},
"dependencies": {
"@babel/core": "^7.21.4",
"@babel/plugin-transform-runtime": "^7.21.4",
"@babel/preset-env": "^7.21.4",
"@babel/preset-react": "^7.18.6",
"@googleapis/drive": "^5.0.2",
"@babel/core": "^7.22.9",
"@babel/plugin-transform-runtime": "^7.22.9",
"@babel/preset-env": "^7.22.9",
"@babel/preset-react": "^7.22.5",
"@googleapis/drive": "^5.1.0",
"body-parser": "^1.20.2",
"classnames": "^2.3.2",
"codemirror": "^5.65.6",
@@ -95,28 +97,35 @@
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "4.3.0",
"marked-extended-tables": "^1.0.5",
"marked": "5.1.1",
"marked-extended-tables": "^1.0.6",
"marked-gfm-heading-id": "^3.0.4",
"marked-smartypants-lite": "^1.0.0",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.4",
"mongoose": "^7.0.3",
"mongoose": "^7.4.2",
"nanoid": "3.3.4",
"nconf": "^0.12.0",
"npm": "^9.6.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"npm": "^9.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-frame-component": "^4.1.3",
"react-router-dom": "6.10.0",
"react-router-dom": "6.14.2",
"sanitize-filename": "1.6.3",
"superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^8.38.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-jest": "^27.2.1",
"jest": "^29.5.0",
"eslint": "^8.46.0",
"eslint-plugin-jest": "^27.2.3",
"eslint-plugin-react": "^7.33.1",
"jest": "^29.6.2",
"jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0",
"stylelint": "^15.10.2",
"stylelint-config-recess-order": "^4.3.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.3",
"supertest": "^6.3.3"
}
}

View File

@@ -135,10 +135,12 @@ fs.emptyDirSync('./build');
})().catch(console.error);
//In development set up a watch server and livereload
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
if(isDev){
livereload('./build');
watchFile('./server.js', {
watch : ['./client', './server', './themes'] // Watch additional folders if you want
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'js json' // Extensions to watch (only .js/.json by default)
//watch : ['./server', './themes'], // Watch additional folders if needed
});
}

View File

@@ -1,4 +1,4 @@
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
// Set working directory to project root
process.chdir(`${__dirname}/..`);
@@ -324,8 +324,8 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
};
if(req.params.id.length > 12 && !brew._id) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
const googleId = brew.googleId;
const shareId = brew.shareId;
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
} else {
@@ -397,7 +397,6 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
return next();
}));
const nodeEnv = config.get('node_env');
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
// Local only
@@ -414,8 +413,7 @@ if(isLocalEnvironment){
//Render the page
const templateFn = require('./../client/template.js');
app.use(asyncHandler(async (req, res, next)=>{
const renderPage = async (req, res)=>{
// Create configuration object
const configuration = {
local : isLocalEnvironment,
@@ -424,7 +422,7 @@ app.use(asyncHandler(async (req, res, next)=>{
};
const props = {
version : require('./../package.json').version,
url : req.originalUrl,
url : req.customUrl || req.originalUrl,
brew : req.brew,
brews : req.brews,
googleBrews : req.googleBrews,
@@ -438,15 +436,20 @@ app.use(asyncHandler(async (req, res, next)=>{
const page = await templateFn('homebrew', title, props)
.catch((err)=>{
console.log(err);
return res.sendStatus(500);
});
return page;
};
//Send rendered page
app.use(asyncHandler(async (req, res, next)=>{
const page = await renderPage(req, res);
if(!page) return;
res.send(page);
}));
//v=====----- Error-Handling Middleware -----=====v//
//Format Errors so all fields will be sent
const replaceErrors = (key, value)=>{
//Format Errors as plain objects so all fields will appear in the string sent
const formatErrors = (key, value)=>{
if(value instanceof Error) {
const error = {};
Object.getOwnPropertyNames(value).forEach(function (key) {
@@ -458,13 +461,30 @@ const replaceErrors = (key, value)=>{
};
const getPureError = (error)=>{
return JSON.parse(JSON.stringify(error, replaceErrors));
return JSON.parse(JSON.stringify(error, formatErrors));
};
app.use((err, req, res, next)=>{
const status = err.status || 500;
app.use(async (err, req, res, next)=>{
const status = err.status || err.code || 500;
console.error(err);
res.status(status).send(getPureError(err));
req.ogMeta = { ...defaultMetaTags,
title : 'Error Page',
description : 'Something went wrong!'
};
req.brew = {
...err,
title : 'Error - Something went wrong!',
text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
status : status,
HBErrorCode : err.HBErrorCode ?? '00',
pureError : getPureError(err)
};
req.customUrl= '/error';
const page = await renderPage(req, res);
if(!page) return;
res.send(page);
});
app.use((req, res)=>{

View File

@@ -99,23 +99,31 @@ const GoogleActions = {
listGoogleBrews : async (auth)=>{
const drive = googleDrive.drive({ version: 'v3', auth });
const obj = await drive.files.list({
pageSize : 1000,
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
})
.catch((err)=>{
console.log(`Error Listing Google Brews`);
console.error(err);
throw (err);
//TODO: Should break out here, but continues on for some reason.
});
const fileList = [];
let NextPageToken = '';
if(!obj.data.files.length) {
do {
const obj = await drive.files.list({
pageSize : 1000,
pageToken : NextPageToken || '',
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
})
.catch((err)=>{
console.log(`Error Listing Google Brews`);
console.error(err);
throw (err);
//TODO: Should break out here, but continues on for some reason.
});
fileList.push(...obj.data.files);
NextPageToken = obj.data.nextPageToken;
} while (NextPageToken);
if(!fileList.length) {
console.log('No files found.');
}
const brews = obj.data.files.map((file)=>{
const brews = fileList.map((file)=>{
return {
text : '',
shareId : file.properties.shareId,
@@ -235,9 +243,9 @@ const GoogleActions = {
if(obj) {
if(accessType == 'edit' && obj.data.properties.editId != accessId){
throw ('Edit ID does not match');
throw ({ message: 'Edit ID does not match' });
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
throw ('Share ID does not match');
throw ({ message: 'Share ID does not match' });
}
const file = await drive.files.get({

View File

@@ -27,8 +27,13 @@ const api = {
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
if(id.length > 12) {
googleId = id.slice(0, -12);
id = id.slice(-12);
if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
googleId = id.slice(0, -12); // current editId is 12 chars
} else { // old editIds used to be 10 chars;
googleId = id.slice(0, -10); // if total string is too short, must be old brew
console.log('Old brew, using 10-char Id');
}
id = id.slice(googleId.length);
}
return { id, googleId };
},
@@ -57,7 +62,14 @@ const api = {
googleError = err;
});
// Throw any error caught while attempting to retrieve Google brew.
if(googleError) throw googleError;
if(googleError) {
const reason = googleError.errors?.[0].reason;
if(reason == 'notFound') {
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
} else {
throw { ...googleError, HBErrorCode: '01' };
}
}
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
}
@@ -65,14 +77,16 @@ const api = {
const isAuthor = stub?.authors?.includes(req.account?.username);
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
throw `The current logged in user does not have editor access to this brew.
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
const accessError = { name: 'Access Error', status: 401 };
if(req.account){
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title };
}
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title };
}
// If after all of that we still don't have a brew, throw an exception
if(!stub && !stubOnly) {
throw 'Brew not found in Homebrewery database or Google Drive';
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
}
// Clean up brew: fill in missing fields with defaults / fix old invalid values
@@ -181,7 +195,7 @@ If you believe you should have access to this brew, ask the file owner to invite
saved = await newHomebrew.save()
.catch((err)=>{
console.error(err, err.toString(), err.stack);
throw `Error while creating new brew, ${err.toString()}`;
throw { name: 'BrewSave Error', message: `Error while creating new brew, ${err.toString()}`, status: 500, HBErrorCode: '06' };
});
if(!saved) return;
saved = saved.toObject();
@@ -283,10 +297,13 @@ If you believe you should have access to this brew, ask the file owner to invite
try {
await api.getBrew('edit')(req, res, ()=>{});
} catch (err) {
const { id, googleId } = api.getId(req);
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
await HomebrewModel.deleteOne({ editId: id });
return next();
// Only if the error code is HBErrorCode '02', that is, Google returned "404 - Not Found"
if(err.HBErrorCode == '02') {
const { id, googleId } = api.getId(req);
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
await HomebrewModel.deleteOne({ editId: id });
return next();
}
}
let brew = req.brew;
@@ -308,7 +325,7 @@ If you believe you should have access to this brew, ask the file owner to invite
await HomebrewModel.deleteOne({ _id: brew._id })
.catch((err)=>{
console.error(err);
throw { status: 500, message: 'Error while removing' };
throw { name: 'BrewDelete Error', message: 'Error while removing', status: 500, HBErrorCode: '07', brewId: brew._id };
});
} else {
if(shouldDeleteGoogleBrew) {
@@ -320,7 +337,7 @@ If you believe you should have access to this brew, ask the file owner to invite
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
await brew.save()
.catch((err)=>{
throw { status: 500, message: err };
throw { name: 'BrewAuthorDelete Error', message: err, status: 500, HBErrorCode: '08', brewId: brew._id };
});
}
}

View File

@@ -111,21 +111,32 @@ describe('Tests for api', ()=>{
expect(googleId).toEqual('12345');
});
it('should return id and google id from params', ()=>{
it('should return 12-char id and google id from params', ()=>{
const { id, googleId } = api.getId({
params : {
id : '123456789012abcdefghijkl'
id : '123456789012345678901234567890123abcdefghijkl'
}
});
expect(googleId).toEqual('123456789012345678901234567890123');
expect(id).toEqual('abcdefghijkl');
expect(googleId).toEqual('123456789012');
});
it('should return 10-char id and google id from params', ()=>{
const { id, googleId } = api.getId({
params : {
id : '123456789012345678901234567890123abcdefghij'
}
});
expect(googleId).toEqual('123456789012345678901234567890123');
expect(id).toEqual('abcdefghij');
});
});
describe('getBrew', ()=>{
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
const notFoundError = 'Brew not found in Homebrewery database or Google Drive';
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
it('returns middleware', ()=>{
const getFn = api.getBrew('share');
@@ -183,7 +194,7 @@ describe('Tests for api', ()=>{
expect(next).toHaveBeenCalled();
});
it('throws if invalid author', async ()=>{
it('throws if not logged in as author', async ()=>{
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
@@ -197,9 +208,24 @@ describe('Tests for api', ()=>{
err = e;
}
expect(err).toEqual(`The current logged in user does not have editor access to this brew.
expect(err).toEqual({ HBErrorCode: '04', message: 'User is not logged in', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
});
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`);
it('throws if logged in as invalid author', async ()=>{
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
const fn = api.getBrew('edit', true);
const req = { brew: {}, account: { username: 'b' } };
let err;
try {
await fn(req, null, null);
} catch (e) {
err = e;
}
expect(err).toEqual({ HBErrorCode: '03', message: 'User is not an Author', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
});
it('does not throw if no authors', async ()=>{
@@ -545,7 +571,7 @@ brew`);
describe('deleteBrew', ()=>{
it('should handle case where fetching the brew returns an error', async ()=>{
api.getBrew = jest.fn(()=>async ()=>{ throw 'err'; });
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
model.deleteOne = jest.fn(async ()=>{});
const next = jest.fn(()=>{});

View File

@@ -97,11 +97,14 @@ const CodeEditor = createClass({
this.codeMirror = CodeMirror(this.refs.editor, {
lineNumbers : true,
lineWrapping : this.props.wrap,
indentWithTabs : true,
indentWithTabs : false,
tabSize : 2,
smartIndent : false,
historyEventDelay : 250,
scrollPastEnd : true,
extraKeys : {
'Tab' : this.indent,
'Shift-Tab' : this.dedent,
'Ctrl-B' : this.makeBold,
'Cmd-B' : this.makeBold,
'Ctrl-I' : this.makeItalic,
@@ -171,6 +174,19 @@ const CodeEditor = createClass({
this.updateSize();
},
indent : function () {
const cm = this.codeMirror;
if(cm.somethingSelected()) {
cm.execCommand('indentMore');
} else {
cm.execCommand('insertSoftTab');
}
},
dedent : function () {
this.codeMirror.execCommand('indentLess');
},
makeHeader : function (number) {
const selection = this.codeMirror.getSelection();
const header = Array(number).fill('#').join('');

View File

@@ -9,6 +9,9 @@
}
.codeEditor{
@media screen and (pointer : coarse) {
font-size : 16px;
}
.CodeMirror-foldmarker {
font-family: inherit;
text-shadow: none;

View File

@@ -2,6 +2,8 @@
const _ = require('lodash');
const Marked = require('marked');
const MarkedExtendedTables = require('marked-extended-tables');
const { markedSmartypantsLite: MarkedSmartypantsLite } = require('marked-smartypants-lite');
const { gfmHeadingId: MarkedGFMHeadingId } = require('marked-gfm-heading-id');
const renderer = new Marked.Renderer();
//Processes the markdown within an HTML block if it's just a class-wrapper
@@ -237,9 +239,9 @@ const definitionLists = {
};
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
Marked.use(MarkedExtendedTables());
Marked.use(mustacheInjectBlock);
Marked.use({ renderer: renderer, smartypants: true });
Marked.use({ renderer: renderer, mangle: false });
Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite());
//Fix local links in the Preview iFrame to link inside the frame
renderer.link = function (href, title, text) {
@@ -311,12 +313,6 @@ const escape = function (html, encode) {
return html;
};
const sanatizeScriptTags = (content)=>{
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
};
const tagTypes = ['div', 'span', 'a'];
const tagRegex = new RegExp(`(${
_.map(tagTypes, (type)=>{
@@ -347,7 +343,7 @@ module.exports = {
render : (rawBrewText)=>{
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
return Marked.parse(sanatizeScriptTags(rawBrewText));
return Marked.parse(rawBrewText);
},
validate : (rawBrewText)=>{

View File

@@ -90,12 +90,6 @@ const escape = function (html, encode) {
return html;
};
const sanatizeScriptTags = (content)=>{
return content
.replace(/<script/ig, '&lt;script')
.replace(/<\/script>/ig, '&lt;/script&gt;');
};
const tagTypes = ['div', 'span', 'a'];
const tagRegex = new RegExp(`(${
_.map(tagTypes, (type)=>{
@@ -113,7 +107,7 @@ module.exports = {
marked : Markdown,
render : (rawBrewText)=>{
return Markdown(
sanatizeScriptTags(rawBrewText),
rawBrewText,
{ renderer: renderer }
);
},

View File

@@ -1,5 +1,6 @@
require('./nav.less');
const React = require('react');
const { useState, useRef, useEffect } = React;
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
@@ -71,64 +72,49 @@ const Nav = {
}
}),
dropdown : createClass({
displayName : 'Nav.dropdown',
getDefaultProps : function() {
return {
trigger : 'hover'
};
},
getInitialState : function() {
return {
showDropdown : false
};
},
componentDidMount : function() {
if(this.props.trigger == 'click')
document.addEventListener('click', this.handleClickOutside);
},
componentWillUnmount : function() {
if(this.props.trigger == 'click')
document.removeEventListener('click', this.handleClickOutside);
},
handleClickOutside : function(e){
// Close dropdown when clicked outside
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
this.handleDropdown(false);
}
},
handleDropdown : function(show){
this.setState({
showDropdown : show
});
},
renderDropdown : function(dropdownChildren){
if(!this.state.showDropdown) return null;
dropdown : function dropdown(props) {
props = Object.assign({}, props, {
trigger : 'hover click'
});
return (
<div className='navDropdown'>
{dropdownChildren}
</div>
);
},
render : function () {
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{
// Ignore the first child
if(i < 1) return;
return child;
});
return (
<div className={`navDropdownContainer ${this.props.className}`}
ref='dropdown'
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
{this.props.children[0] || this.props.children /*children is not an array when only one child*/}
{this.renderDropdown(dropdownChildren)}
</div>
);
const myRef = useRef(null);
const [showDropdown, setShowDropdown] = useState(false);
useEffect(()=>{
document.addEventListener('click', handleClickOutside);
return ()=>{
document.removeEventListener('click', handleClickOutside);
};
}, []);
function handleClickOutside(e) {
// Close dropdown when clicked outside
if(!myRef.current?.contains(e.target)) {
handleDropdown(false);
}
}
})
function handleDropdown(show) {
setShowDropdown(show ?? !showDropdown);
}
const dropdownChildren = React.Children.map(props.children, (child, i)=>{
if(i < 1) return;
return child;
});
return (
<div className={`navDropdownContainer ${props.className}`}
ref={myRef}
onMouseEnter = { props.trigger.includes('hover') ? ()=>handleDropdown(true) : undefined }
onMouseLeave = { props.trigger.includes('hover') ? ()=>handleDropdown(false) : undefined }
onClick = { props.trigger.includes('click') ? ()=>handleDropdown(true) : undefined }
>
{props.children[0] || props.children /*children is not an array when only one child*/}
{showDropdown && <div className='navDropdown'>{dropdownChildren}</div>}
</div>
);
}
};

View File

@@ -79,6 +79,8 @@ nav{
left : 0px;
z-index : 10000;
width : 100%;
overflow : hidden auto;
max-height : calc(100vh - 28px);
.navItem{
animation-name: glideDropDown;
animation-duration: 0.4s;

View File

@@ -61,7 +61,8 @@ const SplitPane = createClass({
return result;
},
handleUp : function(){
handleUp : function(e){
e.preventDefault();
if(this.state.isDragging){
this.props.onDragFinish(this.state.currentDividerPos);
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
@@ -76,6 +77,7 @@ const SplitPane = createClass({
},
handleMove : function(e){
e.preventDefault();
if(!this.state.isDragging) return;
const newSize = this.limitPosition(e.pageX);
@@ -122,7 +124,7 @@ const SplitPane = createClass({
renderDivider : function(){
return <>
{this.renderMoveArrows()}
<div className='divider' onMouseDown={this.handleDown} >
<div className='divider' onPointerDown={this.handleDown} >
<div className='dots'>
<i className='fas fa-circle' />
<i className='fas fa-circle' />
@@ -133,7 +135,7 @@ const SplitPane = createClass({
},
render : function(){
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
return <div className='splitPane' onPointerMove={this.handleMove} onPointerUp={this.handleUp}>
<Pane
ref='pane1'
width={this.state.currentDividerPos}

View File

@@ -11,6 +11,7 @@
flex : 1;
}
.divider{
touch-action : none;
display : table;
height : 100%;
width : 15px;

View File

@@ -0,0 +1,58 @@
const stylelint = require('stylelint');
const { isNumber } = require('stylelint/lib/utils/validateTypes');
const { report, ruleMessages, validateOptions } = stylelint.utils;
const ruleName = 'naturalcrit/declaration-block-multi-line-min-declarations';
const messages = ruleMessages(ruleName, {
expected : (decls)=>`Rule with ${decls} declaration${decls == 1 ? '' : 's'} should be single line`,
});
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) {
return function lint(postcssRoot, postcssResult) {
const validOptions = validateOptions(
postcssResult,
ruleName,
{
actual : primaryOption,
possible : [isNumber],
}
);
if(!validOptions) { //If the options are invalid, don't lint
return;
}
const isAutoFixing = Boolean(context.fix);
postcssRoot.walkRules((rule)=>{ //Iterate CSS rules
//Apply rule only if all children are decls (no further nested rules)
if(rule.nodes.length > primaryOption || !rule.nodes.every((node)=>node.type === 'decl')) {
return;
}
//Ignore if already one line
if(!rule.nodes.some((node)=>node.raws.before.includes('\n')) && !rule.raws.after.includes('\n'))
return;
if(isAutoFixing) { //We are in “fix” mode
rule.each((decl)=>{
decl.raws.before = ' ';
});
rule.raws.after = ' ';
} else {
report({
ruleName,
result : postcssResult,
message : messages.expected(rule.nodes.length), // Build the reported message
node : rule, // Specify the reported node
word : rule.selector, // Which exact word caused the error? This positions the error properly
});
}
});
};
});
module.exports.ruleName = ruleName;
module.exports.messages = messages;

View File

@@ -0,0 +1,68 @@
const stylelint = require('stylelint');
const { report, ruleMessages, validateOptions } = stylelint.utils;
const ruleName = 'naturalcrit/declaration-colon-align';
const messages = ruleMessages(ruleName, {
expected : (rule)=>`Expected colons aligned within rule "${rule}"`,
});
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) {
return function lint(postcssRoot, postcssResult) {
const validOptions = validateOptions(
postcssResult,
ruleName,
{
actual : primaryOption,
possible : [
true,
false
]
}
);
if(!validOptions) { //If the options are invalid, don't lint
return;
}
const isAutoFixing = Boolean(context.fix);
postcssRoot.walkRules((rule)=>{ //Iterate CSS rules
let maxColonPos = 0;
let misaligned = false;
rule.each((declaration)=>{
if(declaration.type != 'decl')
return;
const colonPos = declaration.prop.length + declaration.raws.between.indexOf(':');
if(maxColonPos > 0 && colonPos != maxColonPos) {
misaligned = true;
}
maxColonPos = Math.max(maxColonPos, colonPos);
});
if(misaligned) {
if(isAutoFixing) { //We are in “fix” mode
rule.each((declaration)=>{
if(declaration.type != 'decl')
return;
declaration.raws.between = `${' '.repeat(maxColonPos - declaration.prop.length)}:${declaration.raws.between.split(':')[1]}`;
});
} else { //We are in “report only” mode
report({
ruleName,
result : postcssResult,
message : messages.expected(rule.selector), // Build the reported message
node : rule, // Specify the reported node
word : rule.selector, // Which exact word caused the error? This positions the error properly
});
}
}
});
};
});
module.exports.ruleName = ruleName;
module.exports.messages = messages;

View File

@@ -0,0 +1,52 @@
const stylelint = require('stylelint');
const { isNumber } = require('stylelint/lib/utils/validateTypes');
const { report, ruleMessages, validateOptions } = stylelint.utils;
const ruleName = 'naturalcrit/declaration-colon-min-space-before';
const messages = ruleMessages(ruleName, {
expected : (num)=>`Expected at least ${num} space${num == 1 ? '' : 's'} before ":"`
});
module.exports = stylelint.createPlugin(ruleName, function getPlugin(primaryOption, secondaryOptionObject, context) {
return function lint(postcssRoot, postcssResult) {
const validOptions = validateOptions(
postcssResult,
ruleName,
{
actual : primaryOption,
possible : [isNumber],
}
);
if(!validOptions) { //If the options are invalid, don't lint
return;
}
const isAutoFixing = Boolean(context.fix);
postcssRoot.walkDecls((decl)=>{ //Iterate CSS declarations
const between = decl.raws.between;
const colonIndex = between.indexOf(':');
if(between.slice(0, colonIndex).length >= primaryOption) {
return;
}
if(isAutoFixing) { //We are in “fix” mode
decl.raws.between = between.slice(0, colonIndex).replace(/\s*$/, ' '.repeat(primaryOption)) + between.slice(colonIndex);
} else {
report({
ruleName,
result : postcssResult,
message : messages.expected(primaryOption), // Build the reported message
node : decl, // Specify the reported node
word : ':', // Which exact word caused the error? This positions the error properly
});
}
});
};
});
module.exports.ruleName = ruleName;
module.exports.messages = messages;

View File

@@ -2,12 +2,6 @@
const Markdown = require('naturalcrit/markdown.js');
test('Escapes <script> tag', function() {
const source = '<script></script>';
const rendered = Markdown.render(source);
expect(rendered).toMatch('&lt;script&gt;&lt;/script&gt;');
});
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
const source = '<div>*Bold text*</div>';
const rendered = Markdown.render(source);

View File

@@ -47,8 +47,8 @@ const getTOC = (pages)=>{
return res;
};
module.exports = function(brew){
const pages = brew.text.split('\\page');
module.exports = function(props){
const pages = props.brew.text.split('\\page');
const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);

View File

@@ -1,3 +1,5 @@
@import (less) './themes/assets/assets.less';
:root {
//Colors
--HB_Color_Accent : #EBCEC3; // Salmon
@@ -22,3 +24,10 @@
bottom : 40px;
}
}
.page:has(.partCover) {
.partCover {
background-image: @partCoverHeaderDMG;
}
}

View File

@@ -7,6 +7,7 @@ const scriptGen = require('./snippets/script.gen.js');
const ClassFeatureGen = require('./snippets/classfeature.gen.js');
const CoverPageGen = require('./snippets/coverpage.gen.js');
const TableOfContentsGen = require('./snippets/tableOfContents.gen.js');
const indexGen = require('./snippets/index.gen.js');
const QuoteGen = require('./snippets/quote.gen.js');
const dedent = require('dedent-tabs').default;
@@ -19,20 +20,16 @@ module.exports = [
icon : 'fas fa-pencil-alt',
view : 'text',
snippets : [
{
name : 'Page Number',
icon : 'fas fa-bookmark',
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
},
{
name : 'Auto-incrementing Page Number',
icon : 'fas fa-sort-numeric-down',
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
},
{
name : 'Table of Contents',
icon : 'fas fa-book',
gen : TableOfContentsGen
},
{
name : 'Index',
icon : 'fas fa-bars',
gen : indexGen,
experimental : true
}
]
},
@@ -187,6 +184,18 @@ module.exports = [
gen : CoverPageGen.inside,
experimental : true
},
{
name : 'Part Cover Page',
icon : 'fac book-part-cover',
gen : CoverPageGen.part,
experimental : true
},
{
name : 'Back Cover Page',
icon : 'fac book-back-cover',
gen : CoverPageGen.back,
experimental : true
},
{
name : 'Magic Item',
icon : 'fas fa-hat-wizard',
@@ -203,7 +212,7 @@ module.exports = [
}}
\n`;
},
},
}
]
},
@@ -217,34 +226,51 @@ module.exports = [
view : 'text',
snippets : [
{
name : 'Class Table',
icon : 'fas fa-table',
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
},
{
name : 'Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.full('classTable,wide'),
},
{
name : '1/2 Class Table',
icon : 'fas fa-list-alt',
gen : ClassTableGen.half('classTable,decoration,frame'),
},
{
name : '1/2 Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.half('classTable'),
},
{
name : '1/3 Class Table',
icon : 'fas fa-border-all',
gen : ClassTableGen.third('classTable,frame'),
},
{
name : '1/3 Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.third('classTable'),
name : 'Class Tables',
icon : 'fas fa-table',
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
subsnippets : [
{
name : 'Martial Class Table',
icon : 'fas fa-table',
gen : ClassTableGen.non('classTable,frame,decoration'),
},
{
name : 'Martial Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.non('classTable'),
},
{
name : 'Full Caster Class Table',
icon : 'fas fa-table',
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
},
{
name : 'Full Caster Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.full('classTable,wide'),
},
{
name : 'Half Caster Class Table',
icon : 'fas fa-list-alt',
gen : ClassTableGen.half('classTable,frame,decoration,wide'),
},
{
name : 'Half Caster Class Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.half('classTable,wide'),
},
{
name : 'Third Caster Spell Table',
icon : 'fas fa-border-all',
gen : ClassTableGen.third('classTable,frame,decoration'),
},
{
name : 'Third Caster Spell Table (unframed)',
icon : 'fas fa-border-none',
gen : ClassTableGen.third('classTable'),
}
]
},
{
name : 'Rune Table',

View File

@@ -1,132 +1,138 @@
const _ = require('lodash');
const dedent = require('dedent-tabs').default;
const features = [
'Astrological Botany',
'Biochemical Sorcery',
'Civil Divination',
'Consecrated Augury',
'Demonic Anthropology',
'Divinatory Mineralogy',
'Exo Interfacer',
'Genetic Banishing',
'Gunpowder Torturer',
'Gunslinger Corruptor',
'Hermetic Geography',
'Immunological Cultist',
'Malefic Chemist',
'Mathematical Pharmacy',
'Nuclear Biochemistry',
'Orbital Gravedigger',
'Pharmaceutical Outlaw',
'Phased Linguist',
'Plasma Gunslinger',
'Police Necromancer',
'Ritual Astronomy',
'Sixgun Poisoner',
'Seismological Alchemy',
'Spiritual Illusionism',
'Statistical Occultism',
'Spell Analyst',
'Torque Interfacer'
'Astrological Botany', 'Biochemical Sorcery', 'Civil Divination',
'Consecrated Augury', 'Demonic Anthropology', 'Divinatory Mineralogy',
'Exo Interfacer', 'Genetic Banishing', 'Gunpowder Torturer',
'Gunslinger Corruptor', 'Hermetic Geography', 'Immunological Cultist',
'Malefic Chemist', 'Mathematical Pharmacy', 'Nuclear Biochemistry',
'Orbital Gravedigger', 'Pharmaceutical Outlaw', 'Phased Linguist',
'Plasma Gunslinger', 'Police Necromancer', 'Ritual Astronomy',
'Sixgun Poisoner', 'Seismological Alchemy', 'Spiritual Illusionism',
'Statistical Occultism', 'Spell Analyst', 'Torque Interfacer'
].map((f)=>_.padEnd(f, 21)); // Pad to equal length of 21 chars long
const classnames = [
'Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'
];
const classnames = ['Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'];
const levels = ['1st', '2nd', '3rd', '4th', '5th',
'6th', '7th', '8th', '9th', '10th',
'11th', '12th', '13th', '14th', '15th',
'16th', '17th', '18th', '19th', '20th'];
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
const drawSlots = function(Slots, rows, padding){
let slots = Number(Slots);
return _.times(rows, function(i){
const max = maxes[i];
if(slots < 1) return _.pad('—', padding);
const res = _.min([max, slots]);
slots -= res;
return _.pad(res.toString(), padding);
}).join(' | ');
};
module.exports = {
full : function(classes){
const classname = _.sample(classnames);
let cantrips = 3;
let spells = 1;
let slots = 2;
return `{{${classes}\n##### The ${classname}\n` +
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
_.map(levels, function(levelName, level){
const res = [
_.pad(levelName, 5),
_.pad(`+${profBonus[level]}`, 2),
_.padEnd(_.sample(features), 21),
_.pad(cantrips.toString(), 8),
_.pad(spells.toString(), 6),
drawSlots(slots, 9, 2),
].join(' | ');
cantrips += _.random(0, 1);
spells += _.random(0, 1);
slots += _.random(0, 2);
return `| ${res} |`;
}).join('\n')}\n}}\n\n`;
non : function(snippetClasses){
return dedent`
{{${snippetClasses}
##### The ${_.sample(classnames)}
| Level | Proficiency Bonus | Features | ${_.sample(features)} |
|:-----:|:-----------------:|:---------|:---------------------:|
| 1st | +2 | ${_.sample(features)} | 2 |
| 2nd | +2 | ${_.sample(features)} | 2 |
| 3rd | +2 | ${_.sample(features)} | 3 |
| 4th | +2 | ${_.sample(features)} | 3 |
| 5th | +3 | ${_.sample(features)} | 3 |
| 6th | +3 | ${_.sample(features)} | 4 |
| 7th | +3 | ${_.sample(features)} | 4 |
| 8th | +3 | ${_.sample(features)} | 4 |
| 9th | +4 | ${_.sample(features)} | 4 |
| 10th | +4 | ${_.sample(features)} | 4 |
| 11th | +4 | ${_.sample(features)} | 4 |
| 12th | +4 | ${_.sample(features)} | 5 |
| 13th | +5 | ${_.sample(features)} | 5 |
| 14th | +5 | ${_.sample(features)} | 5 |
| 15th | +5 | ${_.sample(features)} | 5 |
| 16th | +5 | ${_.sample(features)} | 5 |
| 17th | +6 | ${_.sample(features)} | 6 |
| 18th | +6 | ${_.sample(features)} | 6 |
| 19th | +6 | ${_.sample(features)} | 6 |
| 20th | +6 | ${_.sample(features)} | unlimited |
}}\n\n`;
},
half : function(classes){
const classname = _.sample(classnames);
let featureScore = 1;
return `{{${classes}\n##### The ${classname}\n` +
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
_.map(levels, function(levelName, level){
const res = [
_.pad(levelName, 5),
_.pad(`+${profBonus[level]}`, 2),
_.padEnd(_.sample(features), 23),
_.pad(`+${featureScore}`, 21),
].join(' | ');
featureScore += _.random(0, 1);
return `| ${res} |`;
}).join('\n')}\n}}\n\n`;
full : function(snippetClasses){
return dedent`
{{${snippetClasses}
##### The ${_.sample(classnames)}
| Level | Proficiency | Features | Cantrips | --- Spell Slots Per Spell Level ---|||||||||
| ^| Bonus ^| ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |
|:-----:|:-----------:|:-------------|:--------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| 1st | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — | — | — | — | — |
| 2nd | +2 | ${_.sample(features)} | 2 | 3 | — | — | — | — | — | — | — | — |
| 3rd | +2 | ${_.sample(features)} | 2 | 4 | 2 | — | — | — | — | — | — | — |
| 4th | +2 | ${_.sample(features)} | 3 | 4 | 3 | — | — | — | — | — | — | — |
| 5th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 2 | — | — | — | — | — | — |
| 6th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | — | — | — | — | — | — |
| 7th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 1 | — | — | — | — | — |
| 8th | +3 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | — | — | — | — | — |
| 9th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
| 10th | +4 | ${_.sample(features)} | 3 | 4 | 3 | 3 | 2 | 1 | — | — | — | — |
| 11th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
| 12th | +4 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | — | — | — |
| 13th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
| 14th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | — | — |
| 15th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
| 16th | +5 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | — |
| 17th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 2 | 1 | 1 | 1 | 1 | 1 |
| 18th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 1 | 1 | 1 | 1 | 1 |
| 19th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 1 | 1 | 1 |
| 20th | +6 | ${_.sample(features)} | 4 | 4 | 3 | 3 | 3 | 2 | 2 | 2 | 1 | 1 |
}}\n\n`;
},
third : function(classes){
const classname = _.sample(classnames);
half : function(snippetClasses){
return dedent`
{{${snippetClasses}
##### The ${_.sample(classnames)}
| Level | Proficiency | Features | Spells |--- Spell Slots Per Spell Level ---|||||
| ^| Bonus ^| ^| Known ^| 1st | 2nd | 3rd | 4th | 5th |
|:-----:|:-----------:|:-------------|:------:|:-----:|:-----:|:-----:|:-----:|:-----:|
| 1st | +2 | ${_.sample(features)} | — | — | — | — | — | — |
| 2nd | +2 | ${_.sample(features)} | 2 | 2 | — | — | — | — |
| 3rd | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
| 4th | +2 | ${_.sample(features)} | 3 | 3 | — | — | — | — |
| 5th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
| 6th | +3 | ${_.sample(features)} | 4 | 4 | 2 | — | — | — |
| 7th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
| 8th | +3 | ${_.sample(features)} | 5 | 4 | 3 | — | — | — |
| 9th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
| 10th | +4 | ${_.sample(features)} | 6 | 4 | 3 | 2 | — | — |
| 11th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
| 12th | +4 | ${_.sample(features)} | 7 | 4 | 3 | 3 | — | — |
| 13th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
| 14th | +5 | ${_.sample(features)} | 8 | 4 | 3 | 3 | 1 | — |
| 15th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
| 16th | +5 | ${_.sample(features)} | 9 | 4 | 3 | 3 | 2 | — |
| 17th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
| 18th | +6 | ${_.sample(features)} | 10 | 4 | 3 | 3 | 3 | 1 |
| 19th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
| 20th | +6 | ${_.sample(features)} | 11 | 4 | 3 | 3 | 3 | 2 |
}}\n\n`;
},
let cantrips = 3;
let spells = 1;
let slots = 2;
return `{{${classes}\n##### ${classname} Spellcasting\n` +
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
_.map(levels, function(levelName, level){
const res = [
_.pad(levelName, 6),
_.pad(cantrips.toString(), 8),
_.pad(spells.toString(), 7),
drawSlots(slots, 4, 7),
].join(' | ');
cantrips += _.random(0, 1);
spells += _.random(0, 1);
slots += _.random(0, 1);
return `| ${res} |`;
}).join('\n')}\n}}\n\n`;
third : function(snippetClasses){
return dedent`
{{${snippetClasses}
##### ${_.sample(classnames)} Spellcasting
| Level | Cantrips | Spells |--- Spells Slots per Spell Level ---||||
| ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |
|:-----:|:--------:|:------:|:-------:|:-------:|:-------:|:-------:|
| 3rd | 2 | 3 | 2 | — | — | — |
| 4th | 2 | 4 | 3 | — | — | — |
| 5th | 2 | 4 | 3 | — | — | — |
| 6th | 2 | 4 | 3 | — | — | — |
| 7th | 2 | 5 | 4 | 2 | — | — |
| 8th | 2 | 6 | 4 | 2 | — | — |
| 9th | 2 | 6 | 4 | 2 | — | — |
| 10th | 3 | 7 | 4 | 3 | — | — |
| 11th | 3 | 8 | 4 | 3 | — | — |
| 12th | 3 | 8 | 4 | 3 | — | — |
| 13th | 3 | 9 | 4 | 3 | 2 | — |
| 14th | 3 | 10 | 4 | 3 | 2 | — |
| 15th | 3 | 10 | 4 | 3 | 2 | — |
| 16th | 3 | 11 | 4 | 3 | 3 | — |
| 17th | 3 | 11 | 4 | 3 | 3 | — |
| 18th | 3 | 11 | 4 | 3 | 3 | — |
| 19th | 3 | 12 | 4 | 3 | 3 | 1 |
| 20th | 3 | 13 | 4 | 3 | 3 | 1 |
}}\n\n`;
}
};

View File

@@ -68,13 +68,23 @@ const footnote = [
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.'
];
const coverText = [
'Embark on a thrilling journey across a vast and varied world, where magic and mystery await you at every turn. Encounter strange creatures and ancient secrets, and forge your own destiny with your choices. The world is yours to shape and explore.',
'Join a band of brave adventurers and set out to explore the unknown lands beyond the horizon. Along the way, youll face perilous challenges, make new friends and enemies, and uncover a plot that threatens to destroy everything you hold dear. The fate of the world rests in your hands.',
'Create your own character and enter a realm of endless possibilities, where you can be whoever you want to be. Whether you prefer to fight, sneak, charm, or craft your way through the game, youll find a style that suits you. The only limit is your imagination.',
'Experience a rich and immersive story that adapts to your actions and decisions. Every choice you make has consequences, for good or ill. Will you be a hero or a villain? A leader or a follower? A friend or a foe? The choice is yours.',
'Dive into a world of epic fantasy and adventure, where you can explore ancient civilizations, dark dungeons, and hidden secrets. Along the way, youll meet colorful characters, collect powerful items, and learn new skills. The more you play, the more youll discover.',
'Explore a vast and dynamic world that changes according to your actions. You can shape the environment, influence the politics, and alter the history of the game world. But be careful, as every change has a ripple effect that may have unforeseen consequences.',
'Enter a world of wonder and danger, where you can find allies and enemies among the various races and factions that inhabit it. You can choose to join or oppose any of them, or forge your own path. The game world is alive and responsive to your actions.'
];
module.exports = {
front : function() {
return dedent`
{{frontCover}}
{{logo ![](/assets/naturalCritLogo.svg)}}
{{logo ![](/assets/naturalCritLogoRed.svg)}}
# ${_.sample(titles)}
## ${_.sample(subtitles)}
@@ -100,10 +110,46 @@ module.exports = {
___
{{imageMaskCenter${_.random(1, 16)},--offsetX:0%,--offsetY:0%,--rotation:0
![](https://i.imgur.com/IsfUnFR.jpg){height:100%}
![background image](https://i.imgur.com/IsfUnFR.jpg){position:absolute,bottom:0,left:0,height:100%}
}}
{{logo ![](/assets/naturalCritLogo.svg)}}
{{logo ![](/assets/naturalCritLogoRed.svg)}}
\page`;
},
part : function() {
return dedent`
{{partCover}}
# PART X
## ${_.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%}
}}
\page`;
},
back : function() {
return dedent`
{{backCover}}
# ${_.sample(subtitles)}
${_.sampleSize(coverText, 3).join('\n:\n')}
___
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%}
{{logo
![](/assets/naturalCritLogoWhite.svg)
Homebrewery.Naturalcrit.com
}}
\page`;
}

View File

@@ -0,0 +1,85 @@
const dedent = require('dedent-tabs').default;
module.exports = ()=>{
return dedent`
{{index,wide,columns:5;
##### Index
- Ankhesh-Bort
- city map, 7
- city watch, 12
- guilds, 19
- Cheese
- types of cheese, 8
- cheese-related magic, 14
- cheese-related quests, 26-27
- Death
- appearance, 10
- personality, 13
- hobbies, 23
- Elves
- types of elves, 15
- elvish magic, 24
- elvish curses, 28
- Footnotes
- types of footnotes, 16-17
- footnote rules, 20-21
- footnote humor, 29-30
- Gods
- types of gods, 12
- godly interventions, 25
- godly conflicts, 31
- Heroes
- class features, 11-12
- heroic deeds, 26-27
- Inns
- types of inns, 9
- inn amenities, 18
- Jokes
- types of jokes, 11-12
- joke delivery, 25
- Knives
- types of knives, 16-17
- knife skills, 22-23
- knife fights, 28-29
- Luggage
- appearance, 10
- personality, 13
- abilities, 23
- Magic
- types of magic, 15
- magic rules, 24
- magic mishaps, 28
- Socks
- types of socks, 9
- sock-related magic (yes, really), 15
- sock-related quests (no, really), 26
- Trolls
- appearance and biology, 11
- culture and language, 18
- troll rights and activism, 31
- Unknown University
- history and architecture, 12
- faculty and staff, 20
- courses and exams, 33
- Vampires
- types and origins, 13
- vampiric powers and weaknesses, 21
- vampiric etiquette and politics, 34
- Witches
- types and traditions, 14
- witchcraft and headology, 22
- witch trials and tribulations, 35
- Xylophones
- musical instruments or weapons?, 15
- xylophone-related magic and lore, 23
- xylophone-related quests and puzzles, 36
- Yetis
- appearance and behavior, 16
- yeti philosophy and religion, 24
- yeti encounters and stories, 37
- Zombies
- types and causes, 17
- zombie rights and duties, 25
- zombie survival and prevention, 38
}}`;
};

View File

@@ -29,27 +29,29 @@ const getTOC = (pages)=>{
const res = [];
_.each(pages, (page, pageNum)=>{
const lines = page.split('\n');
_.each(lines, (line)=>{
if(_.startsWith(line, '# ')){
const title = line.replace('# ', '');
add1(title, pageNum);
}
if(_.startsWith(line, '## ')){
const title = line.replace('## ', '');
add2(title, pageNum);
}
if(_.startsWith(line, '### ')){
const title = line.replace('### ', '');
add3(title, pageNum);
}
});
if(!page.includes("{{frontCover}}") && !page.includes("{{insideCover}}") && !page.includes("{{partCover}}") && !page.includes("{{backCover}}")) {
const lines = page.split('\n');
_.each(lines, (line)=>{
if(_.startsWith(line, '# ')){
const title = line.replace('# ', '');
add1(title, pageNum);
}
if(_.startsWith(line, '## ')){
const title = line.replace('## ', '');
add2(title, pageNum);
}
if(_.startsWith(line, '### ')){
const title = line.replace('### ', '');
add3(title, pageNum);
}
});
}
});
return res;
};
module.exports = function(brew){
const pages = brew.text.split('\\page');
module.exports = function(props){
const pages = props.brew.text.split('\\page');
const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
if(g1.title !== null) {

View File

@@ -851,6 +851,134 @@ h5 + table{
}
}
}
//*****************************
// * BACK COVER
// *****************************/
.page:has(.backCover) {
color: #fff;
columns: 1;
padding: 2.25cm 1.3cm 2cm 1.3cm;
&:after {
all: unset;
}
.columnWrapper {
width: 7.6cm;
}
.backCover {
position: absolute;
inset: 0;
width: 11cm;
background-image: @backCover;
background-repeat: no-repeat;
background-size: contain;
z-index: -1;
}
.blank {
height: 1.4em;
}
h1 {
margin-bottom: .3cm;
font-size: 1.35cm;
line-height: 0.95em;
font-family: NodestoCapsCondensed;
text-align: center;
color: #ED1C24;
}
h1+p::first-line,
h1+p::first-letter {
all: unset;
}
img {
position: absolute;
top: 0px;
height: 100%;
z-index: -2;
}
hr {
margin-left: auto;
margin-right: auto;
margin-top: 1.1cm;
height: .53cm;
width: 4.5cm;
visibility: visible;
background-image: @horizontalRule;
background-size: 100% 100%;
border: none;
}
p {
font-family: Overpass;
line-height: 1.5em;
font-size: 0.332cm;
}
hr+p {
text-align: center;
margin-top: .6cm;
}
.logo {
position: absolute;
z-index: 0;
height: 1.5cm;
left: 1.2cm;
bottom: 2cm;
width: 7.6cm;
img {
position: relative;
height : 1.5cm;
width : 100%;
z-index : 0;
}
p {
position: relative;
color: #fff;
font-family: NodestoCapsWide;
font-size: .4cm;
letter-spacing: 0.08em;
line-height: 1em;
text-align: center;
text-indent: 0;
width: 100%;
}
}
}
//*****************************
// * PART COVER
// *****************************/
.page:has(.partCover) {
columns : 1;
text-align : center;
padding-top: 0;
.partCover {
background-image: @partCoverHeaderPHB;
background-repeat: no-repeat;
position: absolute;
background-size: 100%;
top: 0;
left: 0;
height: 6cm;
width: 100%;
}
h1 {
position: relative;
text-align: center;
text-transform: uppercase;
font-size: 2.3cm;
font-family: NodestoCapsCondensed;
margin-top: .4cm;
}
h2 {
font-family: Overpass;
font-size: 0.45cm;
position: relative;
margin-top: -0.7em;
line-height: 1.1em;
margin-left : auto;
margin-right : auto;
}
}
//*****************************
// * TABLE OF CONTENTS
@@ -1009,3 +1137,26 @@ break-inside : avoid;
}
}
}
//*****************************
// * INDEX
// *****************************/
.page {
.index {
font-size : 0.218cm;
ul ul {
margin : 0;
}
ul {
padding-left : 0;
text-indent : 0;
list-style-type : none;
}
& > ul > li {
text-indent : -1.5em;
padding-left : 1.5em;
}
}
}

View File

@@ -2,6 +2,7 @@
const WatercolorGen = require('./snippets/watercolor.gen.js');
const ImageMaskGen = require('./snippets/imageMask.gen.js');
const FooterGen = require('./snippets/footer.gen.js');
const dedent = require('dedent-tabs').default;
module.exports = [
@@ -21,6 +22,53 @@ module.exports = [
icon : 'fas fa-file-alt',
gen : '\n\\page\n'
},
{
name : 'Page Number',
icon : 'fas fa-bookmark',
gen : '{{pageNumber 1}}\n'
},
{
name : 'Auto-incrementing Page Number',
icon : 'fas fa-sort-numeric-down',
gen : '{{pageNumber,auto}}\n'
},
{
name : 'Footer',
icon : 'fas fa-shoe-prints',
gen : FooterGen.createFooterFunc(),
subsnippets : [
{
name : 'Footer from H1',
icon : 'fas fa-dice-one',
gen : FooterGen.createFooterFunc(1)
},
{
name : 'Footer from H2',
icon : 'fas fa-dice-two',
gen : FooterGen.createFooterFunc(2)
},
{
name : 'Footer from H3',
icon : 'fas fa-dice-three',
gen : FooterGen.createFooterFunc(3)
},
{
name : 'Footer from H4',
icon : 'fas fa-dice-four',
gen : FooterGen.createFooterFunc(4)
},
{
name : 'Footer from H5',
icon : 'fas fa-dice-five',
gen : FooterGen.createFooterFunc(5)
},
{
name : 'Footer from H6',
icon : 'fas fa-dice-six',
gen : FooterGen.createFooterFunc(6)
}
]
},
{
name : 'Vertical Spacing',
icon : 'fas fa-arrows-alt-v',

View File

@@ -0,0 +1,17 @@
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
module.exports = {
createFooterFunc : function(headerSize=1){
return (props)=>{
const cursorPos = props.cursorPos;
const markdownText = props.brew.text.split('\n').slice(0, cursorPos.line).join('\n');
const markdownTokens = Markdown.marked.lexer(markdownText);
const headerToken = markdownTokens.findLast((lexerToken)=>{ return lexerToken.type === 'heading' && lexerToken.depth === headerSize; });
const headerText = headerToken?.tokens.map((token)=>{ return token.text; }).join('');
const outputText = headerText || 'PART 1 | SECTION NAME';
return `\n{{footnote ${outputText}}}\n`;
};
}
};

View File

@@ -23,6 +23,9 @@ body {
break-inside : avoid;
display : inline-block;
width : 100%;
img {
z-index : 0;
}
}
.inline-block {
display : inline-block;
@@ -251,7 +254,6 @@ body {
background-size : 20px;
z-index : -1;
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
transition : transform 2s;
& > p:has(img) {
position : absolute;
width : 50%;
@@ -259,7 +261,6 @@ body {
bottom : 50%;
left : 50%;
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
transition : transform 2s;
}
& img {
position : absolute;

View File

@@ -13,7 +13,10 @@
@naturalCritLogo : url('/assets/naturalCritLogo.svg');
@coverPageBanner : url('/assets/coverPageBanner.svg');
@horizontalRule : url('/assets/horizontalRule.svg');
@partCoverHeaderPHB : url('/assets/partCoverHeaderPHB.png');
@partCoverHeaderDMG : url('/assets/partCoverHeaderDMG.svg');
@insideCoverMask : url('/assets/insideCoverMask.png');
@backCover : url('/assets/backCover.png');
@scriptBorder : url('/assets/scriptBorder.png');
// Watercolor Images

BIN
themes/assets/backCover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 94.65 94.6"
version="1.1"
id="svg11"
sodipodi:docname="naturalCritLogoWhite.svg"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview13"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="8.4989431"
inkscape:cx="38.887188"
inkscape:cy="47.35883"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg11" />
<defs
id="defs4">
<style
id="style2">.cls-1{fill:#ed1f24;}</style>
</defs>
<title
id="title6">NaturalCritLogo</title>
<g
id="Layer_2"
data-name="Layer 2">
<g
id="base">
<path
id="D20"
class="cls-1"
d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"
style="fill:#ffffff;fill-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 611.99 154.72"><defs><style>.cls-1{fill:#f4f0f1;}.cls-2{fill:#d0d4d0;stroke:#888;stroke-linecap:round;stroke-miterlimit:35;stroke-width:0.5px;}</style></defs><title>Asset 2</title><g id="Layer_2" data-name="Layer 2"><g id="svg"><polygon id="background" class="cls-1" points="221.58 0 221.58 86.65 305.82 136.18 389.8 86.91 389.8 0 221.58 0"/><path id="right_triangle_compound_path" data-name="right triangle compound path" class="cls-2" d="M526.31.25c0,14.18-.08,26.67-.12,40.85,0,4.47-2.72,11-11.77,11-3.25.16-13.9-1.11-23.74-6.57L410.79.25h-2.53L527.82,69.38V.25Zm-.06,66.27L498.44,50.67c1,.39,14.21,4,19.29,2.81a13.9,13.9,0,0,0,8.52-6Z"/><path id="main_shape" data-name="main shape" class="cls-2" d="M598.88,123.48l-.13-65.27a81.69,81.69,0,0,1-24,20.31,79.73,79.73,0,0,1-26.8,9L544.82,86a78.57,78.57,0,0,0,42.52-18.09c15.33-13,22.12-30.25,24.4-37.06v-7a80.45,80.45,0,0,1-15.39,31.7c.63-1,16.49-26.4-2.42-55.2h-2.37c3.37,5,12.23,19.65,9.21,37.64-4,24-27.74,43.84-58.54,46.25L471.67,43.29a3.24,3.24,0,0,0-1.37-3,3.2,3.2,0,0,0-3.55,0L447.21,29a7,7,0,0,0-2.25-2.7,7,7,0,0,0-4.73-1.19l-16.59-9.57C414.47,10.29,404,4.25,397.06.25h-3.32L436.1,25.46s-36.92.06-40.94.11c-2.79-1.71-2.7,3.24,0,1.62h41.71l-46.4.37L390.3.26h-1.57L388.79,86l-41.1,24.5c-6.35,3.73-24.5,13.91-42.86,14.1s-36.47-11.3-44-15.49C248,101.8,235.15,93.85,222.33,86.52L222.46.25h-2V28l-46.09-.37h41.7c2.71,1.62,2.79-3.33,0-1.62-4-.05-40.93-.11-40.93-.11L218.32.25H215c-7,4-18.25,10.52-27.42,15.8L171,25.62a7,7,0,0,0-4.73,1.19,7,7,0,0,0-2.25,2.7L144.46,40.72a3.19,3.19,0,0,0-3.54.05,3.24,3.24,0,0,0-1.37,3L69,84.62c-30.8-2.41-54.53-22.3-58.54-46.25-3-18,5.84-32.62,9.2-37.64H17.29c-18.91,28.8-3,54.19-2.42,55.2C2.75,41,.27,26.28.27,26.28l0,7.08a78.31,78.31,0,0,0,23.63,35A78.57,78.57,0,0,0,66.4,86.43L63.22,88a79.66,79.66,0,0,1-26.8-9,81.76,81.76,0,0,1-24-20.31L12.34,124C12.46,141,.25,147.51.25,147.51v1.72a27.5,27.5,0,0,0,8.43-8A29,29,0,0,0,13,131.51v10.41L.25,152v2.1L26,133.61v-22l20.48-12c62.61,1.71,93.58-40,99.08-46.72,21.86-20.39,70.66-22.25,74.93-22.37V87.7l85.25,49.11,85.12-49.4-.33-57.36s52.35,1.05,75.23,22.38c5.49,6.67,36.47,48.43,99.08,46.72l20.48,12v22l26.47,21.07v-2.1l-13.55-10.66V131a29,29,0,0,0,4.35,9.72,32.12,32.12,0,0,0,9.2,8.6v-1.72A27.69,27.69,0,0,1,598.88,123.48ZM139.64,46.06c-.25.47-8.48,15.47-26.11,27.94-17.91,12.67-40,10.81-40,10.81ZM24.06,110.34l.1,22.47-7.09,6.1V93.77a99.79,99.79,0,0,0,12.3,3.52,99.74,99.74,0,0,0,13.8,2ZM48.19,97A102.43,102.43,0,0,1,30,95.38a103,103,0,0,1-15-3.87l-.09,48.35L13.76,141q.22-39,.47-77.9A83.48,83.48,0,0,0,36.08,80.34a82.84,82.84,0,0,0,25.15,8.85Zm30.54-3a121.63,121.63,0,0,1-26.57,3l12-7.14a96,96,0,0,0,15.51.29A97,97,0,0,0,102.19,86,122.56,122.56,0,0,1,78.73,94ZM140,55.86C135.3,61.42,126.49,71.79,114.11,79,96,89.52,77.09,89.38,66.63,88.34l2.65-1.62A77.74,77.74,0,0,0,112,77.14c17.82-9.9,26.42-24.69,29.83-30.8.2,0,2.07.45,3.05-.86a3.75,3.75,0,0,0,.64-2.86l6.71-3.76A128.27,128.27,0,0,1,140,55.86ZM164.8,39.57a119.38,119.38,0,0,0-15.51,8.29l7-11.33,11-6.29,36.42-.38C190.52,32.15,179.34,33.15,164.8,39.57Zm140.83,95.27-23.08-13.27c3.87,1.5,15.25,4.45,22.63,4.45s18.72-2.93,23.34-4.45Zm232.1-50.51s-22.14,1.87-40-10.81c-17.63-12.47-25.85-27.47-26.1-27.94ZM446.43,39.1c-14.55-6.43-25.73-7.43-38.88-9.72l36.41.38,11,6.29,7,11.33A118,118,0,0,0,446.43,39.1ZM471.2,55.38a128.27,128.27,0,0,1-12.2-17l6.71,3.76a3.73,3.73,0,0,0,.65,2.86c1,1.31,2.84.85,3.05.86,3.4,6.11,12,20.9,29.83,30.8A77.73,77.73,0,0,0,542,86.24l2.64,1.62c-10.47,1-29.4,1.18-47.48-9.34C484.73,71.32,475.92,60.94,471.2,55.38Zm61.29,38.1A123,123,0,0,1,509,85.54a96.54,96.54,0,0,0,22.51,4.13,96,96,0,0,0,15.51-.29l12,7.14A121.63,121.63,0,0,1,532.49,93.48Zm61.67,45-7.1-6.1.09-22.47-19.1-11a99.66,99.66,0,0,0,13.81-2,100.3,100.3,0,0,0,12.3-3.52Zm2.17.95L596.23,91a102.68,102.68,0,0,1-15,3.87A102.43,102.43,0,0,1,563,96.52l-13-7.81a82.61,82.61,0,0,0,25.16-8.85A83.14,83.14,0,0,0,597,62.62q.22,39,.47,77.9Z"/><path id="left_triangle_compound_path" data-name="left triangle compound path" class="cls-2" d="M83.4.73V69.86L203.77.25h-2.53l-80.7,45.8c-9.84,5.46-20.49,6.73-23.74,6.57-9,0-11.77-6.57-11.77-11C85,27.4,85,14.91,84.91.73ZM85,48a13.87,13.87,0,0,0,8.51,6c5.08,1.18,18.26-2.42,19.29-2.81L85,67Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -107,6 +107,13 @@
font-style: italic;
}
@font-face {
font-family: NodestoCapsWide;
src: url('../../../fonts/5e/Nodesto Caps Wide.woff2');
font-weight: normal;
font-style: normal
}
@font-face {
font-family: Overpass;
src: url('../../../fonts/5e/Overpass Medium.woff2');