Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fit-listpage-into-vault

This commit is contained in:
Víctor Losada Hernández
2026-10-02 18:21:16 +02:00
71 changed files with 5567 additions and 8759 deletions
+3
View File
@@ -6,6 +6,9 @@ updates:
interval: daily
open-pull-requests-limit: 99
groups:
eslint-bundle:
patterns: ["*eslint*"]
update-types: ["patch", "minor", "major"]
dev-dependencies:
dependency-type: "development"
patterns: ["*"]
+52 -2
View File
@@ -93,6 +93,56 @@ pre {
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Tuesday 9/29/2026 - v3.24.1
{{taskList
##### calculuschild
* [x] Small hotfix for broken images thrashing the server
}}
### Tuesday 9/29/2026 - v3.24.0
{{taskList
##### calculuschild
* [x] Add a periodic warning to /new page if work has never been saved
* [x] Small fix to variable hoisting logic
Fixes issue [#31](https://github.com/naturalcrit/marked-variables/issues/31)
##### Ericscheid
* [x] Enable "fancy drop caps" on any paragraph, by placing `{drop-cap}` on the line below.
Fixes issue [#5024](https://github.com/naturalcrit/homebrewery/issues/5024)
* [x] Prevent crash if `<script>` tag in brew text
Fixes issue [#546](https://github.com/naturalcrit/homebrewery/issues/546)
##### G-Ambatte
* [x] Add compatibility with Markdeep Ascii art diagrams. See [example brew](https://homebrewery.naturalcrit.com/share/TsZ5Iak3bapb)
* [x] Fix stray hyphens in PDF output titles
Fixes issue [#5069](https://github.com/naturalcrit/homebrewery/issues/5069)
##### 5e-Cleric
* [x] Fix Google Drive files that can't be deleted
Fixes issue [#5020](https://github.com/naturalcrit/homebrewery/issues/5020)
* [x] Prevent co-authors moving brews between Google and Homebrewery storage.
Fixes issue [#4862](https://github.com/naturalcrit/homebrewery/issues/4862)
* [x] Fix cursor position for hotkeys, and other tweaks
Fixes issues [#4758](https://github.com/naturalcrit/homebrewery/issues/4758), [#4790](https://github.com/naturalcrit/homebrewery/issues/4790), [#4809](https://github.com/naturalcrit/homebrewery/issues/4809)
##### 5e-Cleric, G-Ambatte
* [x] Share pages update live when file contents change
##### Abquintic, Calculuschild, G-Ambatte, Gazook89, 5e-Cleric, Ericscheid
* [x] Multiple background fixes and code cleanups
}}
### Monday 9/07/2026 - v3.23.0
{{taskList
##### firstmatekidd (new contributor!)
@@ -140,6 +190,8 @@ Fixes issue [#4904](https://github.com/naturalcrit/homebrewery/issues/4904)
* [x] Multiple background fixes and code cleanups
}}
\page
### Wednesday 5/13/2026 - v3.22.1
{{taskList
##### Frederlk (new Contributor!)
@@ -157,8 +209,6 @@ Fixes issue [#4904](https://github.com/naturalcrit/homebrewery/issues/4904)
Fixes issues [#4771](https://github.com/naturalcrit/homebrewery/issues/4771), [#4783](https://github.com/naturalcrit/homebrewery/issues/4783)
}}
\page
### Saturday 4/04/2026 - v3.21.0
{{taskList
+4 -4
View File
@@ -107,14 +107,14 @@ body {
}
th, td {
max-width : 50ch;
padding : 5px 10px;
overflow : hidden;
text-overflow : ellipsis;
vertical-align : middle;
text-align : center;
white-space : nowrap;
border-right : 1px solid;
max-width:50ch;
overflow:hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:last-child { border-right : none; }
}
@@ -10,7 +10,7 @@ const BrewCleanup = ({})=>{
const [error, setError] = useState(null);
const find = async (type)=>{
if(type === 'junk') try {
setPendingJunk(true);
@@ -109,18 +109,18 @@ const BrewCleanup = ({})=>{
<td style={{ width: '200px' }}>{Moment(brew.updatedAt).fromNow()}</td>
<td>{brew.lastViewed ? Moment(brew.lastViewed).fromNow() : 'No last viewed date'}</td>
<td>{brew.googleId ? 'Google' : 'Homebrewery'}</td>
</tr>
</tr>;
})}
</tbody>
</table>
</>;
};
const renderFound = (type)=>{
const deleteButton = !(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0);
const deleteButton = !(type === 'junk' && junkBrewCollection.length === 0 || type === 'lost' && lostBrewCollection.length === 0);
return <div className='result'>
{deleteButton && <button onClick={()=>cleanup(type)} className='remove'>
{pendingLost && type === "lost" || pendingJunk && type === "junk"
{pendingLost && type === 'lost' || pendingJunk && type === 'junk'
? <i className='fas fa-spin fa-spinner' />
: <span><i className='fas fa-times' /> Remove</span>
}
+2 -2
View File
@@ -16,7 +16,7 @@
margin-left : 10px;
}
&.checkbox {
line-height: 1.5em;
line-height : 1.5em;
input {
width : 1.5em;
height : 1.5em;
@@ -26,9 +26,9 @@
}
.lockSuggestions {
columns : 2;
line-height : 1.2em;
break-inside : avoid;
columns : 2;
h2 { column-span : all; }
h3 { margin-top : 0px; }
b { font-weight : 600; }
+1 -1
View File
@@ -2,8 +2,8 @@
.anchored-box {
position : absolute;
inset-block-start : anchor(bottom);
visibility : hidden;
justify-self : anchor-center;
inset-block-start : anchor(bottom);
&.active { visibility : visible; }
}
@@ -1,6 +1,6 @@
/*
This file basically checks support for Anchor Positioning API in the browser,
and then loads the Oddbird polyfill if support is lacking.
and then loads the Oddbird polyfill if support is lacking.
*/
let polyfillPromise;
+1 -1
View File
@@ -46,7 +46,7 @@ const settingsCompartment = new Compartment();
import { generalKeymap, markdownKeymap, cssKeymap, formatCSS } from './extensions/customKeyMaps.js';
import foldOnPages from './extensions/customFolding.js';
import { customHighlightStyle , customHighlightPlugin } from './extensions/customHighlight.js';
import { customHighlightStyle, customHighlightPlugin } from './extensions/customHighlight.js';
import { legacyCustomHighlightStyle } from './extensions/legacyCustomHighlight.js';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
+39 -44
View File
@@ -16,15 +16,15 @@
}
@keyframes slideacross {
0% {
bottom: -200px;
left: -200px;
}
0% {
bottom : -200px;
left : -200px;
}
100% {
bottom:100%;
left:100%;
}
100% {
bottom : 100%;
left : 100%;
}
}
:where(.codeEditor) {
@@ -37,22 +37,22 @@
outline : none !important;
&.cm-flash {
position:relative;
position : relative;
&::after {
position:absolute;
content:'';
bottom:-200px;
left:-200px;
translate:-50%;
width:200%;
height:100px;
background:linear-gradient(0deg, #89eafc00 0px, #89ebfc8e 50px, #89eafc00 100px, transparent);
display:block;
position : absolute;
bottom : -200px;
left : -200px;
display : block;
width : 200%;
height : 100px;
content : '';
background : linear-gradient(0deg, #89EAFC00 0px, #89EBFC8E 50px, #89EAFC00 100px, transparent);
rotate:30deg;
rotate : 30deg;
translate : -50%;
@media screen and (prefers-reduced-motion: no-preference) {
animation: .5s linear 1 slideacross ;
@media screen and (prefers-reduced-motion : no-preference) {
animation : 0.5s linear 1 slideacross;
}
}
}
@@ -134,9 +134,7 @@
vertical-align : sub;
color : rgb(123, 123, 15);
}
.cm-strikethrough {
text-decoration: line-through;
}
.cm-strikethrough { text-decoration : line-through; }
.cm-definitionList {
.cm-definitionTerm { color : rgb(96, 117, 143); }
@@ -196,30 +194,27 @@
}
.cm-image {
position:relative;
position : relative;
.cm-preview {
object-fit: contain;
position:absolute;
bottom:0;
left:0;
height:200px;
width:200px;
height:200px;
padding:5px;
background-color: #fff;
border-radius:10px;
border:3px solid grey;
pointer-events: none;
opacity:0;
transition:0.2s opacity 0.5s;
translate:0 100%;
z-index:1000;
position : absolute;
bottom : 0;
left : 0;
z-index : 1000;
width : 200px;
height : 200px;
padding : 5px;
pointer-events : none;
object-fit : contain;
background-color : #FFFFFF;
border : 3px solid grey;
border-radius : 10px;
opacity : 0;
translate : 0 100%;
transition : 0.2s opacity 0.5s;
}
&:hover .cm-preview {
opacity:1;
}
&:hover .cm-preview { opacity : 1; }
}
/* Tab character visualization (optional) */
@@ -354,10 +354,7 @@ class ImageWidget extends WidgetType {
img.className = 'cm-preview';
img.src = this.url;
img.onerror = ()=>{
img.src = 'client/icons/broken-image.jpg';
};
if (img.onerror) return document.createElement('span');
return img;
}
@@ -1,7 +1,7 @@
/* eslint max-lines: ["error", { "max": 300 }] */
import { keymap } from '@codemirror/view';
import { undo, redo, indentMore, indentLess, deleteLine } from '@codemirror/commands';
import { EditorSelection } from '@codemirror/state';
import { EditorSelection, ChangeSet } from '@codemirror/state';
import { Prec } from '@codemirror/state';
import * as prettier from 'prettier/standalone';
import * as postcssPlugin from 'prettier/plugins/postcss';
@@ -14,33 +14,30 @@ export async function formatCSS(view) {
const code = empty ? fullDoc : selection;
let formatted = await prettier.format(code, {
parser: 'css',
plugins: [postcssPlugin],
parser : 'css',
plugins : [postcssPlugin],
// formatting options
tabWidth: 2,
useTabs: false,
printWidth: 100,
singleQuote: false,
trailingComma: 'all',
bracketSpacing: true,
endOfLine: 'lf'
tabWidth : 2,
useTabs : false,
printWidth : 100,
singleQuote : false,
trailingComma : 'all',
bracketSpacing : true,
endOfLine : 'lf'
});
//format manually single declaration rules to span one line.
//Prettier can't do it by default, this is crude but it works
formatted = formatted.replace(
/([^{]+)\{\s*\n\s*([^;\n]+:[^;\n]+;)\s*\n\s*\}(\s*)/g,
(_, selector, decl, whitespace) =>
`${selector} { ${decl.trim()} }${whitespace}`
/([^{]+)\{\s*\n\s*([^;\n]+:[^;\n]+;)\s*\n\s*\}(\s*)/g,
(_, selector, decl, whitespace)=>`${selector} { ${decl.trim()} }${whitespace}`
);
if(formatted === code) return true;
const dom = view.dom;
dom.classList.add('cm-flash');
view.dom.classList.add('cm-flash');
setTimeout(()=>{
dom.classList.remove('cm-flash');
view.dom.classList.remove('cm-flash');
view.dispatch({
changes : {
@@ -54,186 +51,172 @@ export async function formatCSS(view) {
} catch (err) {
console.error('Error formatting css: ', err);
}
return true;
}
const forEachSelection = (view, fn)=>{
view.dispatch(view.state.changeByRange((range)=>fn(view.state, range.from, range.to, view.state.doc.sliceString(range.from, range.to))));
return true;
};
const insertTab = (view)=>{
// If any selection spans multiple lines, delegates to CodeMirror's indentMore
// Otherwise inserts two spaces at each cursor/selection
// Otherwise inserts two spaces at each cursor/selection
const shouldIndent = view.state.selection.ranges.some((range)=>view.state.doc.lineAt(range.from).number !==
view.state.doc.lineAt(range.to).number
);
if(shouldIndent) return indentMore(view);
const changes = [];
for (const range of view.state.selection.ranges) {
changes.push({
from : range.from,
to : range.to,
insert : ' ' // Insert two spaces, not a tab char!
});
}
// Create a transaction so we can map old positions to
// their new positions after the edits are applied
const mappedChanges = view.state.update({ changes });
view.dispatch({
changes,
selection : EditorSelection.create(
view.state.selection.ranges.map((range)=>EditorSelection.cursor(
mappedChanges.changes.mapPos(range.from, -1) + 2
)
)
)
return forEachSelection(view, (state, from, to, selected)=>{
return {
changes : { from, to, insert: ' ' }, // Insert two spaces, not a tab char!
range : EditorSelection.cursor(from + 2)
};
});
return true;
};
const wrapSelection = (prefix, suffix)=>(view)=>{
const changes = [];
const wrapSelection = (prefix, suffix)=>(view)=>forEachSelection(view, (state, from, to, selected)=>{
const noSelection = from === to;
if(noSelection) {
const insert = prefix + suffix;
for (const range of view.state.selection.ranges) {
const { from, to } = range;
const selected = view.state.doc.sliceString(from, to);
let text;
if(from === to) { text = prefix + suffix; } else if(selected.startsWith(prefix) && selected.endsWith(suffix)) {
text = selected.slice(prefix.length, -suffix.length);
} else {text = `${prefix}${selected}${suffix}`;}
changes.push({ from, to, insert: text });
return {
changes : { from, to, insert },
range : EditorSelection.cursor(from + prefix.length)
};
}
view.dispatch({
changes
});
const hasWrapper = selected.startsWith(prefix) && selected.endsWith(suffix);
if(hasWrapper) {
return {
changes : [
{ from: to - suffix.length, to, insert: '' },
{ from, to: from + prefix.length, insert: '' }
],
range : EditorSelection.range(from, to - prefix.length - suffix.length)
};
}
return true;
};
const makeNbsp = (view)=>{
const { from } = view.state.selection.main;
const prev2 = from >= 2
? view.state.doc.sliceString(from - 2, from)
: '';
return {
changes : { from, to, insert: prefix + selected + suffix },
range : EditorSelection.range(from, to + prefix.length + suffix.length)
};
});
const makeNbsp = (view)=>forEachSelection(view, (state, from, to, selected)=>{
const prev2 = from >= 2 ? state.doc.sliceString(from - 2, from) : '';
const insert = (prev2 === ':>' || prev2 === '>>') ? '>' : ':>';
view.dispatch({
changes : { from, to: from, insert },
selection : { anchor: from + insert.length },
});
return {
changes : { from, to, insert },
range : EditorSelection.cursor(from + insert.length)
};
});
return true;
};
const makeSpace = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const makeSpace = (view)=>forEachSelection(view, (state, from, to, selected)=>{
let insert = '{{width:10% }}';
const match = selected.match(/^{{width:(\d+)% }}$/);
let newText = '{{width:10% }}';
if(match) {
const percent = Math.min(parseInt(match[1], 10) + 10, 100);
newText = `{{width:${percent}% }}`;
insert = `{{width:${percent}% }}`;
}
view.dispatch({ changes: { from, to, insert: newText } });
return true;
};
return {
changes : { from, to, insert },
range : EditorSelection.range(from, from + insert.length)
};
});
const removeSpace = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const removeSpace = (view)=>forEachSelection(view, (state, from, to, selected)=>{
const match = selected.match(/^{{width:(\d+)% }}$/);
if(match) {
const percent = parseInt(match[1], 10) - 10;
const newText = percent > 0 ? `{{width:${percent}% }}` : '';
view.dispatch({ changes: { from, to, insert: newText } });
const insert = percent > 0 ? `{{width:${percent}% }}` : '';
return { changes: { from, to, insert: insert }, range: EditorSelection.range(from, from + insert.length) };
}
return true;
};
});
const makeSpan = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('{{') && selected.endsWith('}}')
? selected.slice(2, -2)
: `{{${selected}}}`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
const makeDiv = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = selected.startsWith('{{') && selected.endsWith('}}')
? selected.slice(2, -2)
: `{{\n${selected}\n}}`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
const makeComment = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const isHtmlComment = selected.startsWith('<!--') && selected.endsWith('-->');
const text = isHtmlComment
? selected.slice(4, -3)
: `<!-- ${selected} -->`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
const makeLink = (view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to).trim();
const isLink = /^\[(.*)\]\((.*)\)$/.exec(selected);
const text = isLink ? `${isLink[1]} ${isLink[2]}` : `[${selected || 'alt text'}](url)`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
const makeList = (type)=>(view)=>{
const { from, to } = view.state.selection.main;
const lines = [];
for (let l = from; l <= to; l++) {
const lineText = view.state.doc.line(l + 1).text;
lines.push(lineText);
const makeLink = (view)=>forEachSelection(view, (state, from, to, selected)=>{
// If the selection is already a Markdown link, unwrap it
const existingLink = /^\[(.*)\]\((.*)\)$/.exec(selected);
if(existingLink) {
const [, text, url] = existingLink;
const insert = `${text} ${url}`;
return {
changes : { from, to, insert },
range : EditorSelection.range(
from,
from + insert.length
),
};
}
const joined = lines.join('\n');
let newText;
if(type === 'UL') newText = joined.replace(/^/gm, '- ');
else newText = joined.replace(/^/gm, (m, i)=>`${i + 1}. `);
view.dispatch({ changes: { from, to, insert: newText } });
return true;
};
const makeHeader = (level)=>(view)=>{
const { from, to } = view.state.selection.main;
const selected = view.state.doc.sliceString(from, to);
const text = `${'#'.repeat(level)} ${selected}`;
view.dispatch({ changes: { from, to, insert: text } });
return true;
};
// If the selection is a URL, use its domain as the link text.
const isUrl =
/^(https?:\/\/|www\.)\S+$/i.test(selected) ||
/^[a-z0-9-]+(\.[a-z0-9-]+)+([/?#]\S*)?$/i.test(selected);
if(isUrl) {
const domain = selected
.replace(/^https?:\/\//i, '')
.replace(/^www\./i, '')
.split(/[/?#]/)[0];
const newColumn = (view)=>{
const { from, to } = view.state.selection.main;
view.dispatch({ changes: { from, to, insert: '\n\\column\n\n' } });
return true;
};
const name = domain.split('.')[0];
const insert = `[${name}](${selected})`;
return {
changes : { from, to, insert },
range : EditorSelection.range(from, from + insert.length),
};
}
const newPage = (view)=>{
const { from, to } = view.state.selection.main;
view.dispatch({ changes: { from, to, insert: '\n\\page\n\n' } });
return true;
};
// If the selection is just text, use it as alt text and set url as placeholder
const insert = `[${selected || 'alt text'}](url)`;
return {
changes : { from, to, insert },
range : EditorSelection.range(from, from + insert.length),
};
});
const addPrefixAtLineStart = (prefix)=>(view)=>forEachSelection(view, (state, from, to, selected)=>{
const startLine = state.doc.lineAt(from);
const endLine = state.doc.lineAt(to);
const selectedLines = [];
for (let lineNumber = startLine.number; lineNumber <= endLine.number; lineNumber++) { selectedLines.push(state.doc.line(lineNumber)); }
const allLinesHavePrefix = selectedLines.every((line)=>line.text.startsWith(prefix));
const changes = selectedLines.map((line)=>{
const shouldRemovePrefix = allLinesHavePrefix && line.text.startsWith(prefix);
return {
from : line.from,
to : shouldRemovePrefix
? line.from + prefix.length
: line.from,
insert : shouldRemovePrefix ? '' : prefix,
};
});
const changeSet = ChangeSet.of(changes, state.doc.length);
return {
changes,
range : EditorSelection.range(
changeSet.mapPos(from, -1),
changeSet.mapPos(to, 1)
),
};
});
const newBreak = (type)=>(view)=>forEachSelection(view, (state, from, to, selected)=>{
const insert = `\n\\${type}\n\n`;
return {
changes : { from, to, insert },
range : EditorSelection.cursor(from + insert.length)
};
});
export const generalKeymap = Prec.high(keymap.of([
{ key: 'Tab', run: insertTab }, //runs indentMore if multiple lines selected in a single selection
{ key: 'Tab', run: insertTab },
{ key: 'Shift-Tab', run: indentLess },
{ key: 'Mod-z', run: undo }, //it may be unnecessary
{ key: 'Mod-Shift-z', run: redo },
@@ -247,7 +230,6 @@ export const cssKeymap = Prec.highest(keymap.of([
]));
export const markdownKeymap = Prec.highest(keymap.of([
{ key: 'Mod-b', run: wrapSelection('**', '**') }, // makeBold
{ key: 'Mod-i', run: wrapSelection('*', '*') }, // makeItalic
{ key: 'Mod-u', run: wrapSelection('<u>', '</u>') }, // makeUnderline
@@ -256,18 +238,18 @@ export const markdownKeymap = Prec.highest(keymap.of([
{ key: 'Mod-.', run: makeNbsp },
{ key: 'Shift-Mod-.', run: makeSpace },
{ key: 'Shift-Mod-,', run: removeSpace },
{ key: 'Mod-m', run: makeSpan },
{ key: 'Shift-Mod-m', run: makeDiv },
{ key: 'Mod-/', run: makeComment },
{ key: 'Mod-k', run: makeLink },
{ key: 'Mod-l', run: makeList('UL') },
{ key: 'Shift-Mod-l', run: makeList('OL') },
{ key: 'Shift-Mod-1', run: makeHeader(1) },
{ key: 'Shift-Mod-2', run: makeHeader(2) },
{ key: 'Shift-Mod-3', run: makeHeader(3) },
{ key: 'Shift-Mod-4', run: makeHeader(4) },
{ key: 'Shift-Mod-5', run: makeHeader(5) },
{ key: 'Shift-Mod-6', run: makeHeader(6) },
{ key: 'Mod-Enter', run: newPage },
{ key: 'Shift-Mod-Enter', run: newColumn },
{ key: 'Mod-m', run: wrapSelection('{{', '}}') },
{ key: 'Shift-Mod-m', run: wrapSelection('{{\n', '\n}}') },
{ key: 'Mod-/', run: wrapSelection('<!-- ', ' -->') },
{ key: 'Mod-Shift-k', run: makeLink },
{ key: 'Mod-Shift-u', run: addPrefixAtLineStart('- ') },
{ key: 'Mod-Shift-o', run: addPrefixAtLineStart('1. ') },
{ key: 'Shift-Mod-1', run: addPrefixAtLineStart('# ') },
{ key: 'Shift-Mod-2', run: addPrefixAtLineStart('## ') },
{ key: 'Shift-Mod-3', run: addPrefixAtLineStart('### ') },
{ key: 'Shift-Mod-4', run: addPrefixAtLineStart('#### ') },
{ key: 'Shift-Mod-5', run: addPrefixAtLineStart('##### ') },
{ key: 'Shift-Mod-6', run: addPrefixAtLineStart('###### ') },
{ key: 'Mod-Enter', run: newBreak('page') },
{ key: 'Shift-Mod-Enter', run: newBreak('column') },
]));
+1 -1
View File
@@ -7,7 +7,7 @@ const Combobox = createReactClass({
displayName : 'Combobox',
getDefaultProps : function() {
return {
id : '',
id : '',
className : '',
trigger : 'hover',
default : '',
+2 -4
View File
@@ -31,14 +31,12 @@
font-family : 'Open Sans';
font-size : 11px;
cursor : default;
&:hover {
background-color : #ddd;
}
&:hover { background-color : #DDDDDD; }
.detail {
width : 100%;
font-size : 9px;
font-style : italic;
color : #7c7c7c;
color : #7C7C7C;
text-align : left;
}
}
+2 -2
View File
@@ -1,11 +1,11 @@
/**
* A dropdown menu component that uses the Anchor Positioning API to position the elements. It supports nested submenus as well.
* Anchor Positioning is now supported in all major browsers. A polyfill is conditionally loaded for older browsers.
*
*
* As-is, the menus will always open down aligned on left to trigger, submenus open to the right initially.
* If no space, menus will still open down, but aligned to the right of the trigger. Submenus will flip to the other side of the top menu.
* This could be customized either in more specific CSS, or as a `direction` prop on the component (in future iterations).
*
*
* @param {string} props.groupName - Name of the menu. Appears as the trigger text.
* @param {string} [props.icon] - Icon to display in the trigger.
* @param {string} [props.color] - Color class to add to the trigger.
+18 -20
View File
@@ -1,37 +1,35 @@
@property --menuColor {
syntax: '<color>';
inherits: true;
initial-value: #DDD;
syntax : '<color>';
inherits : true;
initial-value : #DDDDDD;
}
@property --activeTriggerColor {
syntax: '<color>';
inherits: true;
initial-value: #999;
syntax : '<color>';
inherits : true;
initial-value : #999999;
}
:root{
--activeTriggerColor : var(--activeTriggerColor);
}
:root { --activeTriggerColor : var(--activeTriggerColor); }
.menu-list {
contain : content;
position : fixed;
top : anchor(bottom);
left : anchor(left);
position-try: flip-inline flip-block;
color: inherit; // [popover] gets a `canvastext` color value from useragent.
background: var(--menuColor);
position-try : flip-inline flip-block;
contain : content;
color : inherit; // [popover] gets a `canvastext` color value from useragent.
background : var(--menuColor);
li > .menu-list {
margin: 0 0px;
top : anchor(top);
left : anchor(right);
position-try: flip-inline;
}
top : anchor(top);
left : anchor(right);
margin : 0 0px;
position-try : flip-inline;
}
}
.menu-wrapper:has(:popover-open) > button { // if menu is open...
background-color: var(--activeTriggerColor, hsl(from var(--menuColor) h s calc(l * .85))); // tint menu triggers based on menu color
background-color : var(--activeTriggerColor, hsl(from var(--menuColor) h s calc(l * 0.85))); // tint menu triggers based on menu color
}
.darkMode .menu-wrapper:has(:popover-open) > button { // if menu is open...
--activeTriggerColor : #444; // tint menu triggers based on menu color
--activeTriggerColor : #444444; // tint menu triggers based on menu color
}
@@ -103,8 +103,8 @@ const BrewRenderer = (props)=>{
const pagesRef = useRef(null);
const [visiblePages, setVisiblePages] = useState([]);
const [centerPage , setCenterPage ] = useState(1);
const [headerState , setHeaderState ] = useState(false);
const [centerPage, setCenterPage] = useState(1);
const [headerState, setHeaderState] = useState(false);
const [state, setState] = useState({
isMounted : false,
@@ -158,10 +158,9 @@ const BrewRenderer = (props)=>{
return false;
};
const renderDummyPage = (index)=>
<div className='phb page' id={`p${index + 1}`} key={index}>
<i className='fas fa-spinner fa-spin' />
</div>;
const renderDummyPage = (index)=><div className='phb page' id={`p${index + 1}`} key={index}>
<i className='fas fa-spinner fa-spin' />
</div>;
const renderStyle = ()=>{
const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
@@ -61,9 +61,7 @@
}
@supports (break-after:always) {
.columnSplit {
margin-bottom: 100vh;
}
.columnSplit { margin-bottom : 100vh; }
}
}
@@ -76,13 +74,11 @@
height : 100%;
padding : unset;
overflow-y : unset;
&:has(.facing, .flow) {
padding : unset;
}
&:has(.facing, .flow) { padding : unset; }
.pages {
display : block;
margin : 0px;
zoom : 100% !important;
display : block;
& > .page { box-shadow : unset; }
}
}
@@ -99,13 +99,13 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
return (
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
<div className='toggleButton'>
<button data-tooltip-right={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`}
aria-label={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`}
<button data-tooltip-right={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`}
aria-label={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`}
onClick={()=>{ setToolsVisible(!toolsVisible); localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible); }}>
<i aria-hidden='true' className='fas fa-glasses' />
</button>
<button data-tooltip-right={`${headerState ? 'Hide' : 'Show'} Header Navigation`}
aria-label={`${headerState ? 'Hide' : 'Show'} Header Navigation`}
<button data-tooltip-right={`${headerState ? 'Hide' : 'Show'} Header Navigation`}
aria-label={`${headerState ? 'Hide' : 'Show'} Header Navigation`}
onClick={()=>{setHeaderState(!headerState);}}>
<i aria-hidden='true' className='fas fa-rectangle-list' />
</button>
@@ -1,4 +1,4 @@
@import (less) './client/icons/customIcons.less';
@import (less) '../../../icons/customIcons.less';
.toolBar {
position : absolute;
@@ -176,9 +176,7 @@
transition : all 0.2s ease;
}
.toggleButton button i {
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
}
.toggleButton button i { filter : drop-shadow(0 0 2px black) drop-shadow(0 0 1px black); }
}
}
+1 -1
View File
@@ -338,7 +338,7 @@ const Editor = forwardRef(
view={view}
style={{ display: 'none' }}
editorTheme={editorSettings.editorTheme}
onThemeChange={setIsDark}
onThemeChange={setIsDark}
settings={editorSettings}
/>
<SettingsEditor
@@ -131,7 +131,7 @@ const SettingsEditor = ({ settings, updateSettings = ()=>{}, EditorThemeNameList
onChange={(e)=>handleFieldChange('fontSize', e)}
/>
<datalist id='font-sizes'>
<option value='1' />
<option value='1' />
</datalist>
</div>
@@ -1,25 +1,25 @@
@import '@sharedStyles/core.less';
@import (less) './client/icons/customIcons.less';
@import (less) '../../../icons/customIcons.less';
@import (less) '@themes/fonts/5e/fonts.less';
.snippetBar {
--menuColor : #DDDDDD;
--textColor : black;
--hoverMenuColor : #999;
--hoverMenuColor : #999999;
@menuHeight : 25px;
position : relative;
display : flex;
flex-wrap : wrap-reverse;
reading-flow : flex-visual;
justify-content : space-between;
height : auto;
font-family : 'Open Sans', sans-serif;
font-size : 0.65rem;
font-weight : 800;
color : var(--textColor);
text-transform : uppercase;
background-color : var(--menuColor);
font-size : .65rem;
font-family: 'Open Sans', sans-serif;
text-transform: uppercase;
font-weight: 800;
reading-flow : flex-visual;
.editors {
display : flex;
@@ -46,7 +46,7 @@
&.editorTool:not(.active) { cursor : not-allowed; }
&:hover,&.selected { background-color : var(--hoverMenuColor) }
&:hover,&.selected { background-color : var(--hoverMenuColor); }
&.text {
.tooltipLeft('Brew Editor');
}
@@ -199,9 +199,7 @@
background : grey;
border-radius : 12px;
}
&:hover {
background-color : var(--hoverMenuColor);
}
&:hover { background-color : var(--hoverMenuColor); }
&:disabled {
color : gray;
cursor : not-allowed;
@@ -227,7 +225,7 @@
}
.editor.darkMode .snippetBar {
--menuColor : #666;
--textColor : #eee;
--hoverMenuColor : #444;
--menuColor : #666666;
--textColor : #EEEEEE;
--hoverMenuColor : #444444;
}
+8 -18
View File
@@ -1,30 +1,20 @@
.tags {
.tagInputWrap {
display:grid;
grid-template-columns: 200px 3fr;
gap:10px;
display : grid;
grid-template-columns : 200px 3fr;
gap : 10px;
}
.list input {
border-radius: 5px;
}
.list input { border-radius : 5px; }
.tagInput-dropdown {
.dropdown-options {
.item {
&.type {
background-color: #00800035;
}
&.group {
background-color: #50505035;
}
&.meta {
background-color: #00008035;
}
&.system {
background-color: #80000035;
}
&.type { background-color : #00800035; }
&.group { background-color : #50505035; }
&.meta { background-color : #00008035; }
&.system { background-color : #80000035; }
}
}
}
File diff suppressed because it is too large Load Diff
+3 -7
View File
@@ -105,9 +105,7 @@
&::placeholder { color : var(--input-placeholder-clr); }
}
input[type='range'] {
color: var(--input-text-clr);
}
input[type='range'] { color : var(--input-text-clr); }
&.description {
flex : 1;
@@ -151,9 +149,7 @@
max-width : 150px;
}
&.themes {
.value {
overflow : visible;
}
.value { overflow : visible; }
button {
padding-right : 5px;
padding-left : 5px;
@@ -165,7 +161,7 @@
color : dimgray;
background-color : darkgray;
}
.item.dropdown-input { border:none;}
.item.dropdown-input { border : none;}
.item {
position : relative;
overflow : visible;
+2 -2
View File
@@ -1,7 +1,7 @@
@import '@sharedStyles/core.less';
.homebrew {
height : 100%;
background-color:@steel;
height : 100%;
background-color : @steel;
&.deployment { background-color : darkred; }
.sitePage {
+1 -3
View File
@@ -39,9 +39,7 @@
flex-grow : 1;
min-width : 300px;
}
>.brewTitle {
cursor:auto;
}
>.brewTitle { cursor : auto; }
}
// "NaturalCrit" logo
.navLogo {
+1 -1
View File
@@ -12,7 +12,7 @@ export default function(){
return ()=>{
document.removeEventListener('print:startprep', handlePrintStartPrep);
document.removeEventListener('print:finishedprep', handlePrintPrepFinished);
}
};
}, []);
const handlePrintStartPrep = ()=>{ setPrinting(true); };
@@ -10,6 +10,7 @@
isolation : isolate;
break-inside : avoid;
-webkit-column-break-inside : avoid;
break-inside : avoid;
.thumbnail {
position : absolute;
top : 0;
@@ -28,9 +29,18 @@
}
}
.info {
font-size : 0.9em;
& > span { line-height : 1.5em; }
a { color : inherit; }
position : initial;
bottom : 2px;
font-size : 0.9em;
& > span {
margin-right : 12px;
line-height : 1.5em;
a { color : inherit; }
}
}
.brewTags span {
display : inline-block;
@@ -1,6 +1,22 @@
@import url('https://fonts.googleapis.com/css2?family=Alata&display=swap');
@import '@sharedStyles/colors.less';
.noColumns() {
-webkit-column-width : auto;
-moz-column-width : auto;
column-width : auto;
-webkit-column-count : auto;
-moz-column-count : auto;
column-count : auto;
column-fill : auto;
-webkit-column-gap : normal;
-moz-column-gap : normal;
column-gap : normal;
height : auto;
min-height : 279.4mm;
margin : 20px auto;
contain : unset;
}
.listPage {
.content {
padding : clamp(20px, 5vh, 200px) clamp(20px, 20vw, 200px);
+16 -119
View File
@@ -1,4 +1,3 @@
/* eslint-disable max-lines */
import './editPage.less';
// Common imports
@@ -9,32 +8,15 @@ import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx'
import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx';
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
// Page specific imports
import Headtags from '../../../../vitreum/headtags.js';
const Meta = Headtags.Meta;
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
import ShareNavItem from '@navbar/share.navitem.jsx';
import LockNotification from './lockNotification/lockNotification.jsx';
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
import googleDriveIcon from '../../googleDrive.svg';
@@ -43,8 +25,11 @@ const STYLEKEY = 'HB_newPage_style';
const SNIPKEY = 'HB_newPage_snippets';
const METAKEY = 'HB_newPage_meta';
const useLocalStorage = false;
const sandbox = false;
const useLocalStorage = false;
const sandbox = false;
const showFloatingButtons = false;
const showEditorButtons = true;
const pageName = "editPage";
const EditPage = (props)=>{
props = {
@@ -55,26 +40,13 @@ const EditPage = (props)=>{
const [currentBrew, setCurrentBrew] = useState(props.brew);
const [saveGoogle, setSaveGoogle] = useState(!!props.brew.googleId);
const [error, setError] = useState(null);
const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text));
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle, setThemeBundle] = useState({});
const [alertTrashedGoogleBrew, setAlertTrashedGoogleBrew] = useState(props.brew.trashed);
const [alertNoGoogleToTransfer, setAlertNoGoogleToTransfer] = useState(false);
const [alertOwnershipToTransfer, setAlertOwnershipToTransfer] = useState(false);
const [confirmGoogleTransfer, setConfirmGoogleTransfer] = useState(false);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
...prevBrew,
style : newData.style,
text : newData.text,
snippets : newData.snippets
}));
const handleGoogleClick = ()=>{
if(currentBrew.authors.length > 0 && global.account?.username !== currentBrew.authors[0]) {
setAlertOwnershipToTransfer(true);
@@ -106,8 +78,6 @@ const EditPage = (props)=>{
};
const save = async (brew, saveToGoogle)=>{
setHTMLErrors(hbfm.validate(brew.text));
await updateHistory(brew).catch(console.error);
await versionHistoryGarbageCollection().catch(console.error);
@@ -163,7 +133,7 @@ const EditPage = (props)=>{
{alertOwnershipToTransfer && (
<div className='errorContainer'>
You must be the Owner to transfer between the Homebrewery and Google Drive!
You must be the Owner to transfer between the Homebrewery and Google Drive!
The owner of this file is {currentBrew.authors[0]}.
<br></br>
<div className='confirm' onClick={closeAlerts}> Okay </div>
@@ -201,51 +171,13 @@ const EditPage = (props)=>{
</Nav.item>
);
const renderAutoSaveButton = ()=>(
<Nav.item onClick={toggleAutoSave}>
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
</Nav.item>
);
const renderNavbar = ()=>{
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
</Nav.section>
<Nav.section>
{renderGoogleDriveIcon()}
{error
? <ErrorNavItem error={error} clearError={clearError} />
: <Nav.dropdown className='save-menu'>
{renderSaveButton()}
{renderAutoSaveButton()}
</Nav.dropdown>}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<ShareNavItem brew={currentBrew} currentPage={currentBrewRendererPageNum} />
<RecentNavItem brew={currentBrew} storageKey='edit' />
<AccountNavItem/>
</Nav.section>
</Navbar>;
};
const {
handleSplitMove,
handleBrewChange,
toggleAutoSave,
clearError,
renderSaveButton,
autoSaveEnabled,
trySave
renderPanels,
trySave,
} = useCommonEditPageFunctions({
saveGoogle,
error,
setError,
setThemeBundle,
HTMLErrors,
setHTMLErrors,
currentBrew,
setCurrentBrew,
useLocalStorage,
@@ -255,51 +187,16 @@ const EditPage = (props)=>{
METAKEY,
hbfm,
sandbox,
showFloatingButtons,
lastSavedBrew,
editorRef,
save,
pageName,
showEditorButtons,
renderGoogleDriveIcon,
userThemes: props.userThemes
});
return (
<div className='editPage sitePage'>
<Meta name='robots' content='noindex, nofollow' />
{renderNavbar()}
{currentBrew.lock && <LockNotification shareId={currentBrew.shareId} message={currentBrew.lock.editMessage} reviewRequested={currentBrew.lock.reviewRequested}/>}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
reportError={setError}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
updateBrew={updateBrew}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
allowPrint={true}
/>
</SplitPane>
</div>
</div>
);
return renderPanels();
};
export default EditPage;
+13 -93
View File
@@ -1,4 +1,3 @@
import './homePage.less';
// Common imports
@@ -9,35 +8,19 @@ import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx'
import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx';
// Page specific imports
import Headtags from '@vitreum/headtags.js';
const Meta = Headtags.Meta;
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
const SNIPKEY = 'HB_newPage_snippets';
const METAKEY = 'HB_newPage_meta';
const useLocalStorage = false;
const sandbox = true;
const useLocalStorage = false;
const sandbox = true;
const showFloatingButtons = true;
const showEditorButtons = false;
const pageName = "homePage";
const HomePage =(props)=>{
props = {
@@ -48,13 +31,7 @@ const HomePage =(props)=>{
const [currentBrew, setCurrentBrew] = useState(props.brew);
const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false);
const [error, setError] = useState(undefined);
const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text));
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle, setThemeBundle] = useState({});
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const save = async (brew, saveToGoogle)=>{
@@ -72,35 +49,12 @@ const HomePage =(props)=>{
window.location = `/edit/${saved.editId}`;
};
const renderNavbar = ()=>{
return <Navbar ver={props.ver}>
<Nav.section>
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>;
};
const {
handleSplitMove,
handleBrewChange,
clearError,
renderSaveButton,
unsavedChanges,
trySave
renderPanels
} = useCommonEditPageFunctions({
saveGoogle,
error,
setError,
setThemeBundle,
HTMLErrors,
setHTMLErrors,
currentBrew,
setCurrentBrew,
useLocalStorage,
@@ -110,49 +64,15 @@ const HomePage =(props)=>{
METAKEY,
hbfm,
sandbox,
showFloatingButtons,
lastSavedBrew,
editorRef,
save,
pageName,
showEditorButtons,
userThemes: {}
});
return (
<div className='homePage sitePage'>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
{renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
showEditButtons={false}
themeBundle={themeBundle}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
themeBundle={themeBundle}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
/>
</SplitPane>
</div>
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={()=>trySave(true, true, saveGoogle)}>
Save current <i className='fas fa-save' />
</div>
<a href='/new' className='floatingNewButton'>
Create your own <i className='fas fa-magic' />
</a>
</div>
);
return renderPanels();
};
export default HomePage;
+5 -5
View File
@@ -37,14 +37,14 @@
.navItem.save {
background-color : @orange;
transition:all 0.2s;
transition : all 0.2s;
&:hover { background-color : @green; }
&.neverSaved {
translate:-100%;
opacity: 0;
background-color :#333;
cursor:auto;
cursor : auto;
background-color : #333333;
opacity : 0;
translate : -100%;
}
}
}
+13 -87
View File
@@ -1,4 +1,3 @@
/* eslint-disable max-lines */
import './newPage.less';
// Common imports
@@ -8,24 +7,8 @@ import { hbfm } from 'marked-hbfm';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx'
import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
import useCommonEditPageFunctions from '../../utils/commonEditPageFunctions.jsx';
// Page specific imports
const BREWKEY = 'HB_newPage_content';
@@ -35,8 +18,11 @@ const METAKEY = 'HB_newPage_meta';
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true;
const sandbox = true;
const useLocalStorage = true;
const sandbox = true;
const showFloatingButtons = false;
const showEditorButtons = true;
const pageName = "newPage";
const NewPage = (props)=>{
props = {
@@ -47,13 +33,7 @@ const NewPage = (props)=>{
const [currentBrew, setCurrentBrew] = useState(props.brew);
const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false);
const [error, setError] = useState(null);
const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(props.brew.text));
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle, setThemeBundle] = useState({});
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
useEffect(()=>{
@@ -116,37 +96,12 @@ const NewPage = (props)=>{
window.location = `/edit/${savedBrew.editId}`;
};
const renderNavbar = ()=>(
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
</Nav.section>
<Nav.section>
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>
);
const {
handleSplitMove,
handleBrewChange,
clearError,
renderSaveButton
renderPanels
} = useCommonEditPageFunctions({
saveGoogle,
error,
setError,
setThemeBundle,
HTMLErrors,
setHTMLErrors,
currentBrew,
setCurrentBrew,
useLocalStorage,
@@ -156,44 +111,15 @@ const NewPage = (props)=>{
METAKEY,
hbfm,
sandbox,
showFloatingButtons,
lastSavedBrew,
editorRef,
save,
pageName,
showEditorButtons,
userThemes: props.userThemes
});
return (
<div className='newPage sitePage'>
{renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
allowPrint={true}
/>
</SplitPane>
</div>
</div>
);
return renderPanels();
};
export default NewPage;
+5 -5
View File
@@ -3,14 +3,14 @@
.newPage {
.navItem.save {
background-color : @orange;
transition:all 0.2s;
transition : all 0.2s;
&:hover { background-color : @green; }
&.neverSaved {
translate:-100%;
opacity: 0;
background-color :#333;
cursor:auto;
cursor : auto;
background-color : #333333;
opacity : 0;
translate : -100%;
}
}
}
@@ -6,9 +6,7 @@
*:not(input) { user-select : none; }
.form {
background:white;
}
.form { background : white; }
:where(.content .dataGroup) {
width : 100%;
@@ -174,8 +172,8 @@
height : 100%;
max-height : 100%;
padding : 70px 50px;
overflow-y : scroll;
container-type : inline-size;
overflow-y : scroll;
h3 { font-size : 25px; }
+134 -24
View File
@@ -1,7 +1,26 @@
import React, { useState, useEffect, useEffectEvent, useRef } from 'react';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import _ from 'lodash';
import Nav from '@navbar/nav.jsx';
import Headtags from '@vitreum/headtags.js';
import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../editor/editor.jsx';
import BrewRenderer from '../brewRenderer/brewRenderer.jsx';
import LockNotification from '../pages/editPage/lockNotification/lockNotification.jsx';
const Meta = Headtags.Meta;
//===---- Navbar
import Nav from '@navbar/nav.jsx';
import Navbar from '@navbar/navbar.jsx';
import NewBrewItem from '@navbar/newbrew.navitem.jsx';
import AccountNavItem from '@navbar/account.navitem.jsx';
import ErrorNavItem from '@navbar/error-navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import VaultNavItem from '@navbar/vault.navitem.jsx';
import PrintNavItem from '@navbar/print.navitem.jsx';
import ShareNavItem from '@navbar/share.navitem.jsx';
import RecentNavItems from '@navbar/recent.navitem.jsx';
const { both: RecentNavItem } = RecentNavItems;
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
@@ -12,10 +31,8 @@ const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
export default function useCommonEditPageFunctions(dependencies) {
const {
saveGoogle,
error,
setError,
setThemeBundle,
HTMLErrors,
setHTMLErrors,
currentBrew,
setCurrentBrew,
useLocalStorage,
@@ -25,27 +42,37 @@ export default function useCommonEditPageFunctions(dependencies) {
METAKEY,
hbfm,
sandbox,
showFloatingButtons,
lastSavedBrew,
editorRef,
save,
pageName,
renderGoogleDriveIcon = ()=>{},
showEditorButtons,
userThemes = {}
} = dependencies;
const [isSaving , setIsSaving] = useState(false);
const [lastSavedTime , setLastSavedTime] = useState(new Date());
const [autoSaveEnabled , setAutoSaveEnabled] = useState(!sandbox);
const [isSaving, setIsSaving] = useState(false);
const [lastSavedTime, setLastSavedTime] = useState(new Date());
const [autoSaveEnabled, setAutoSaveEnabled] = useState(!sandbox);
const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true);
const [unsavedChanges , setUnsavedChanges] = useState(false);
const [unsavedChanges, setUnsavedChanges] = useState(false);
const [themeBundle, setThemeBundle] = useState({});
const [HTMLErrors, setHTMLErrors] = useState(hbfm.validate(currentBrew.text));
const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const unsavedChangesRef = useRef(unsavedChanges); // onBeforeUnload lives outside React and needs ref to unsavedChanges
const warnUnsavedTimeout = useRef(null); // timers live outside React and need ref to consistently track time
const saveTimeout = useRef(null);
const editorRef = useRef(null);
//==--------- Page setup ----------==//
useEffect(()=>{
const autoSavePref = !sandbox && JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
setAutoSaveEnabled(autoSavePref);
setWarnUnsavedChanges(!autoSavePref);
setHTMLErrors(hbfm.validate(currentBrew.text));
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
@@ -64,7 +91,7 @@ export default function useCommonEditPageFunctions(dependencies) {
};
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
window.onBeforeUnload = null;
window.onbeforeunload = null;
};
}, []);
@@ -105,7 +132,7 @@ export default function useCommonEditPageFunctions(dependencies) {
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
lang : value.lang
}));
}
};
@@ -132,10 +159,9 @@ export default function useCommonEditPageFunctions(dependencies) {
saveTimeout.current = setTimeout(async ()=>{
setIsSaving(true);
setError(null);
setHTMLErrors(hbfm.validate(currentBrew.text));
await save(currentBrew, saveToGoogle)
.catch((err)=>{
setError(err);
});
.catch((err)=>{setError(err);});
setIsSaving(false);
setLastSavedTime(new Date());
if(!autoSaveEnabled) resetWarnUnsavedTimer();
@@ -155,7 +181,7 @@ export default function useCommonEditPageFunctions(dependencies) {
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder...
<div className='errorContainer'>{text}</div>
<div className='errorContainer'>{text}</div>
</Nav.item>;
}
@@ -171,14 +197,98 @@ export default function useCommonEditPageFunctions(dependencies) {
return <Nav.item className='save saved'>saved</Nav.item>;
};
//TODO: Candidate for refactor/rename; used with history tool to load previous verion; may overlap with snippet injection or handleBrewChange
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
...prevBrew,
style : newData.style,
text : newData.text,
snippets : newData.snippets
}));
const renderNavbar = ()=>(
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
</Nav.section>
<Nav.section>
{renderGoogleDriveIcon()}
{error
? <ErrorNavItem error={error} clearError={clearError} />
: <Nav.dropdown className='save-menu'>
{renderSaveButton()}
{pageName == "editPage" && renderAutoSaveButton()}
</Nav.dropdown>}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
{(pageName == "editPage") && <ShareNavItem brew={currentBrew} currentPage={currentBrewRendererPageNum} />}
<RecentNavItem brew={currentBrew} storageKey={(pageName == "editPage") ? 'edit' : undefined} />
<AccountNavItem />
</Nav.section>
</Navbar>
);
const renderAutoSaveButton = ()=>(
<Nav.item onClick={toggleAutoSave}>
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
</Nav.item>
);
const renderFloatingSaveButtons = ()=>(
<>
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={()=>trySave(true, true, saveGoogle)}>
Save current <i className='fas fa-save' />
</div>
<a href='/new' className='floatingNewButton'>
Create your own <i className='fas fa-magic' />
</a>
</>
);
const renderPanels = ()=>(
<div className= {`${pageName} sitePage`}>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
{(pageName == 'editPage') && <Meta name='robots' content='noindex, nofollow' />}
{renderNavbar()}
{currentBrew.lock && <LockNotification shareId={currentBrew.shareId} message={currentBrew.lock.editMessage} reviewRequested={currentBrew.lock.reviewRequested}/>}
<div className='content'>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
reportError={setError}
renderer={currentBrew.renderer}
userThemes={userThemes}
showEditButtons={showEditorButtons}
themeBundle={themeBundle}
updateBrew={updateBrew}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
lang={currentBrew.lang}
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
themeBundle={themeBundle}
errors={HTMLErrors}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
allowPrint={true} //TODO: candidate for cleanup in brewRenderer. Homepage only place where allowPrint = false. Vote OK to allow print everywhere
/>
</SplitPane>
</div>
{showFloatingButtons && renderFloatingSaveButtons()}
</div>
);
return {
handleSplitMove,
handleBrewChange,
toggleAutoSave,
clearError,
trySave,
renderSaveButton,
autoSaveEnabled,
unsavedChanges,
}
renderPanels
};
}
+14 -4
View File
@@ -21,6 +21,15 @@
let prefix = '';
// Default page title
let result ='The Homebrewery';
// Add brew title, if it exists
if (title) {
result = `${title} - ${result}`;
}
// Add prefix for Stage/PR Deployment/Local install
if (url && url?.includes('://homebrewery-stage.')) {
prefix = `Stage `;
} else if (url?.includes('://homebrewery-pr-')) {
@@ -30,12 +39,13 @@
prefix = 'Local ';
}
if (title) {
document.title = `${prefix} - ${title} - The Homebrewery`;
} else if (prefix) {
document.title = `${prefix} - The Homebrewery`;
if (prefix) {
result = `${prefix} - ${result}`;
}
// Set page title
document.title = result;
if (window.location.pathname.startsWith('/admin')) {
import('/client/admin/main.jsx');
} else {
+1888 -354
View File
File diff suppressed because it is too large Load Diff
+15 -28
View File
@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.23.0",
"version": "3.24.1",
"type": "module",
"engines": {
"npm": ">=10.8 <12",
@@ -94,20 +94,18 @@
"@babel/runtime": "^8.0.5",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.11.1",
"@codemirror/highlight": "^0.19.8",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/lang-html": "^6.4.12",
"@codemirror/lang-markdown": "^6.5.2",
"@codemirror/language": "^6.12.2",
"@codemirror/language-data": "^6.5.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.7.5",
"@codemirror/view": "^6.43.12",
"@dmsnell/diff-match-patch": "^1.1.0",
"@googleapis/drive": "^26.0.0",
"@lezer/highlight": "^1.2.3",
"@codemirror/state": "^6.7.6",
"@codemirror/view": "^6.43.13",
"@googleapis/drive": "^26.0.1",
"@lezer/highlight": "^1.2.4",
"@oddbird/css-anchor-positioning": "^0.10.2",
"@sanity/diff-match-patch": "^3.2.0",
"@sanity/diff-match-patch": "^3.2.1",
"@vitejs/plugin-react": "^6.0.5",
"body-parser": "^2.3.0",
"classnames": "^2.5.1",
@@ -121,38 +119,27 @@
"express-async-handler": "^1.2.0",
"express-static-gzip": "3.0.2",
"fflate": "^0.8.3",
"fs-extra": "^11.3.5",
"fs-extra": "^11.4.1",
"hash-wasm": "^4.12.0",
"idb-keyval": "^6.2.5",
"js-yaml": "^5.4.2",
"jwt-simple": "^0.5.6",
"less": "^4.9.1",
"lodash": "^4.18.1",
"marked": "15.0.12",
"marked-alignment-paragraphs": "^1.0.0",
"marked-definition-lists": "^1.0.1",
"marked-diagrams-markdeep": "^1.0.1",
"marked-emoji": "^3.0.0",
"marked-extended-tables": "^2.0.1",
"marked-gfm-heading-id": "^4.1.4",
"marked-hbfm": "^1.0.1",
"marked-nonbreaking-spaces": "^1.0.1",
"marked-smartypants-lite": "^1.0.3",
"marked-subsuper-text": "^1.0.4",
"marked-variables": "^1.0.5",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.31.0",
"mongoose": "^9.10.1",
"mongoose": "^9.10.2",
"nanoid": "6.0.1",
"nconf": "^0.13.0",
"node": "^26.9.0",
"prettier": "^3.9.8",
"node": "^26.10.0",
"prettier": "^3.9.9",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"react-frame-component": "^5.3.2",
"react-router": "^8.4.0",
"sanitize-filename": "1.6.4",
"superagent": "^10.2.1"
"superagent": "^10.4.1"
},
"devDependencies": {
"@stylistic/stylelint-plugin": "^5.3.0",
@@ -164,13 +151,13 @@
"globals": "^16.4.0",
"jest": "^30.5.2",
"jest-expect-message": "^1.1.3",
"jsdom": "^30.1.0",
"jsdom": "^30.1.1",
"jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0",
"stylelint": "^17.15.0",
"stylelint-config-recess-order": "^7.7.0",
"stylelint-config-recommended": "^18.0.0",
"supertest": "^7.1.4",
"vite": "^8.3.0"
"supertest": "^7.3.0",
"vite": "^8.3.1"
}
}
+14
View File
@@ -2,3 +2,17 @@
User-agent: *
Disallow: /edit/
# AI / LLM training crawlers
User-agent: GPTBot
User-agent: ClaudeBot
User-agent: CCBot
User-agent: Google-Extended
User-agent: Bytespider
User-agent: Applebot-Extended
User-agent: Meta-ExternalAgent
User-agent: Amazonbot
Disallow: /share/
Disallow: /download/
Disallow: /print/
Disallow: /edit/
+9 -12
View File
@@ -13,7 +13,6 @@ const isProd = nodeEnv === 'production';
import HomebrewAPI from './homebrew.api.js';
import asyncHandler from 'express-async-handler';
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
@@ -38,14 +37,14 @@ export default function createAdminApi(vite) {
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
}
};
// Search for up to 300 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
const junkBrewsPipeline = [
{ $match : {
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
} },
{ $project: { _id: 1, textBinSize: { $binarySize: '$textBin' }, updatedAt: 1, lastViewed: 1} },
{ $project: { _id: 1, textBinSize: { $binarySize: '$textBin' }, updatedAt: 1, lastViewed: 1 } },
{ $match: { textBinSize: { $lt: 140 } } },
{ $limit: 300 }
];
@@ -53,14 +52,14 @@ export default function createAdminApi(vite) {
// Search for up to 500 unauthored brews that have not been viewed or updated in two years
const lostBrewsPipeline = [
{
$match: {
authors: [],
updatedAt: { $lt: Moment().subtract(2, 'years').toDate() },
lastViewed: { $lt: Moment().subtract(2, 'years').toDate() }
$match : {
authors : [],
updatedAt : { $lt: Moment().subtract(2, 'years').toDate() },
lastViewed : { $lt: Moment().subtract(2, 'years').toDate() }
}
},
{
$limit: 500
$limit : 500
}
];
@@ -71,7 +70,7 @@ export default function createAdminApi(vite) {
router.get('/admin/cleanupJunk', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewsPipeline).option({ maxTimeMS: 60000 })
.then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
.then((objs)=>res.json({ count: objs.length, brewCollection: objs }))
.catch((error)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
@@ -94,7 +93,7 @@ export default function createAdminApi(vite) {
router.get('/admin/cleanupLost', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(lostBrewsPipeline).option({ maxTimeMS: 60000 })
.then((objs)=>res.json({ count: objs.length, brewCollection : objs }))
.then((objs)=>res.json({ count: objs.length, brewCollection: objs }))
.catch((error)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
@@ -148,8 +147,6 @@ export default function createAdminApi(vite) {
brew[property] = cleanText(brew[property]);
});
splitTextStyleAndMetadata(brew);
req.body = brew;
// Remove Account from request to prevent Admin user from being added to brew as an Author
+9 -7
View File
@@ -137,7 +137,7 @@ export default async function createApp(vite) {
return res.sendFile(`robots.txt`, { root: process.cwd() });
});
//serve brew for sharepage rerender
app.get('/api/fetch/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res) => {
app.get('/api/fetch/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res)=>{
const { brew } = req;
brew.authors.includes(req.account?.username)
? sanitizeBrew(brew, 'shareAuthor')
@@ -204,7 +204,7 @@ export default async function createApp(vite) {
});
Stream.on('sendUpdate', (event, data)=>{
console.log('Event:', event, '\nData:', data);
if(isLocalEnvironment) { console.log('Event:', event, '\nData:', data); }
res.write(`data: ${JSON.stringify({ ...data, eventType: event })}\n\n`);
});
});
@@ -283,11 +283,13 @@ export default async function createApp(vite) {
const safeProps = JSON.stringify(props).replace(/<(?=\/?script)/ig, '\\u003c');
html = html.replace(
'<head>',
`<head>\n`
+ `<script id="props">`
+ `window.__INITIAL_PROPS__ = ` + safeProps
+ `</script>\n`
+ ogMetaTags
()=>{ //Replace must use a function instead of string, else brews with `$` in contents are treated as replacement patterns
return `<head>\n`
+ `<script id="props">`
+ `window.__INITIAL_PROPS__ = ${safeProps
}</script>\n${
ogMetaTags}`;
}
);
return html;
+1 -3
View File
@@ -193,13 +193,13 @@ const api = {
const fixedStub = migrateSystemsToTags(stub);
req.brew = fixedStub;
splitTextStyleAndMetadata(req.brew);
next();
};
},
getCSS : async (req, res)=>{
const { brew } = req;
if(!brew) return res.status(404).send('');
splitTextStyleAndMetadata(brew);
if(!brew.style) return res.status(404).send('');
res.set({
@@ -345,7 +345,6 @@ const api = {
});
currentTheme = req.brew;
splitTextStyleAndMetadata(currentTheme);
if(!currentTheme.tags.some((tag)=>tag === 'meta:theme' || tag === 'meta:Theme'))
throw { brewId: req.params.id, name: 'Invalid Theme Selected', message: 'Selected theme does not have the meta:theme tag', status: 422, HBErrorCode: '10' };
themeName ??= currentTheme.title;
@@ -384,7 +383,6 @@ const api = {
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
const brewFromClient = api.excludePropsFromUpdate(req.body);
const brewFromServer = req.brew;
splitTextStyleAndMetadata(brewFromServer);
if(brewFromServer?.version !== brewFromClient?.version){
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
+111 -111
View File
@@ -360,25 +360,25 @@ describe('Tests for api', ()=>{
trashed : false,
updatedAt : undefined,
views : 0,
bleedSize: {
top: '.125in',
bottom: '.125in',
inner: '.125in',
outer: '.125in',
bleedSize : {
top : '.125in',
bottom : '.125in',
inner : '.125in',
outer : '.125in',
},
columns: '2',
columnGutter: '.125in',
legalAuthors: '',
license: 'None',
safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
columns : '2',
columnGutter : '.125in',
legalAuthors : '',
license : 'None',
safetySpace : {
top : '.25in',
bottom : '.25in',
outer : '.25in',
inner : '.5in',
},
trimSize: {
width: '8.5in',
height: '11in',
trimSize : {
width : '8.5in',
height : '11in',
},
});
expect(next).toHaveBeenCalled();
@@ -471,25 +471,25 @@ describe('Tests for api', ()=>{
renderer : 'v3',
theme : 'phb',
googleId : '12345',
bleedSize: {
top: '.125in',
bottom: '.125in',
inner: '.125in',
outer: '.125in',
bleedSize : {
top : '.125in',
bottom : '.125in',
inner : '.125in',
outer : '.125in',
},
columns: '2',
columnGutter: '.125in',
legalAuthors: '',
license: 'None',
safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
columns : '2',
columnGutter : '.125in',
legalAuthors : '',
license : 'None',
safetySpace : {
top : '.25in',
bottom : '.25in',
outer : '.25in',
inner : '.5in',
},
trimSize: {
width: '8.5in',
height: '11in',
trimSize : {
width : '8.5in',
height : '11in',
},
});
@@ -534,25 +534,25 @@ brew`);
renderer : 'v3',
theme : 'phb',
googleId : '12345',
bleedSize: {
top: '.125in',
bottom: '.125in',
inner: '.125in',
outer: '.125in',
bleedSize : {
top : '.125in',
bottom : '.125in',
inner : '.125in',
outer : '.125in',
},
columns: '2',
columnGutter: '.125in',
legalAuthors: '',
license: 'None',
safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
columns : '2',
columnGutter : '.125in',
legalAuthors : '',
license : 'None',
safetySpace : {
top : '.25in',
bottom : '.25in',
outer : '.25in',
inner : '.5in',
},
trimSize: {
width: '8.5in',
height: '11in',
trimSize : {
width : '8.5in',
height : '11in',
},
});
@@ -687,16 +687,16 @@ brew`);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
_id : '1',
authors : ['test user'],
createdAt : undefined,
description : '',
editId : expect.any(String),
gDrive : false,
pageCount : 1,
published : false,
renderer : 'V3',
lang : 'en',
_id : '1',
authors : ['test user'],
createdAt : undefined,
description : '',
editId : expect.any(String),
gDrive : false,
pageCount : 1,
published : false,
renderer : 'V3',
lang : 'en',
shareId : expect.any(String),
style : undefined,
tags : [],
@@ -708,25 +708,25 @@ brew`);
trashed : false,
updatedAt : undefined,
views : 0,
bleedSize: {
top: '.125in',
bottom: '.125in',
inner: '.125in',
outer: '.125in',
bleedSize : {
top : '.125in',
bottom : '.125in',
inner : '.125in',
outer : '.125in',
},
columns: '2',
columnGutter: '.125in',
legalAuthors: '',
license: 'None',
safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
columns : '2',
columnGutter : '.125in',
legalAuthors : '',
license : 'None',
safetySpace : {
top : '.25in',
bottom : '.25in',
outer : '.25in',
inner : '.5in',
},
trimSize: {
width: '8.5in',
height: '11in',
trimSize : {
width : '8.5in',
height : '11in',
},
});
});
@@ -765,16 +765,16 @@ brew`);
expect(google.newGoogleBrew).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
_id : '1',
authors : ['test user'],
bleedSize: {
top: '.125in',
bottom: '.125in',
inner: '.125in',
outer: '.125in',
_id : '1',
authors : ['test user'],
bleedSize : {
top : '.125in',
bottom : '.125in',
inner : '.125in',
outer : '.125in',
},
columns: '2',
columnGutter: '.125in',
columns : '2',
columnGutter : '.125in',
createdAt : undefined,
description : '',
editId : expect.any(String),
@@ -784,29 +784,29 @@ brew`);
renderer : 'V3',
lang : 'en',
shareId : expect.any(String),
legalAuthors: '',
license: 'None',
safetySpace: {
top: '.25in',
bottom: '.25in',
outer: '.25in',
inner: '.5in',
legalAuthors : '',
license : 'None',
safetySpace : {
top : '.25in',
bottom : '.25in',
outer : '.25in',
inner : '.5in',
},
googleId : expect.any(String),
style : undefined,
tags : [],
text : undefined,
textBin : undefined,
theme : '5ePHB',
thumbnail : '',
title : 'asdf',
trashed : false,
trimSize: {
width: '8.5in',
height: '11in',
googleId : expect.any(String),
style : undefined,
tags : [],
text : undefined,
textBin : undefined,
theme : '5ePHB',
thumbnail : '',
title : 'asdf',
trashed : false,
trimSize : {
width : '8.5in',
height : '11in',
},
updatedAt : undefined,
views : 0
updatedAt : undefined,
views : 0
});
});
});
@@ -1031,7 +1031,7 @@ brew`);
);
});
});
describe('deleteGoogleBrew', ()=>{
it('should check auth and delete brew', async ()=>{
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
@@ -1261,7 +1261,7 @@ brew`);
expect(saved.googleId).toEqual(brew.googleId);
});
});
describe('Split Text, Style, and Metadata', ()=>{
it('basic splitting', async ()=>{
@@ -1372,5 +1372,5 @@ brew`);
expect(testBrew.text).toEqual('text\n');
});
});
});
+1 -4
View File
@@ -244,7 +244,6 @@ export default function pageRoutes({
};
sanitizeBrew(req.brew, 'edit');
splitTextStyleAndMetadata(req.brew);
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
return next();
}));
@@ -252,7 +251,6 @@ export default function pageRoutes({
//New Page from ID
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew);
const brew = {
shareId : req.brew.shareId,
title : `CLONE - ${req.brew.title}`,
@@ -303,14 +301,13 @@ export default function pageRoutes({
const googleId = brew.googleId;
const shareId = brew.shareId;
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
.catch((err)=>{next(err);});
} else {
await HomebrewModel.increaseView({ shareId: brew.shareId });
}
};
brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew);
return next();
}));
+12 -21
View File
@@ -42,7 +42,6 @@
transition-timing-function : @easing;
}
.transition (@prop, @duration: @defaultDuration) {
-webkit-transition : @prop @duration, -webkit-transform @duration;
-moz-transition : @prop @duration, -moz-transform @duration;
@@ -58,13 +57,13 @@
transform : @transform;
}
.delay(@delay) {
-webkit-transition-delay : @delay;
transition-delay : @delay;
-webkit-animation-delay : @delay;
animation-delay : @delay;
}
.keep() {
-webkit-animation-fill-mode : forwards;
-moz-animation-fill-mode : forwards;
@@ -73,7 +72,6 @@
animation-fill-mode : forwards;
}
.sequentialDelay(@delayInc : 0.2s, @initialDelay : 0s) {
&:nth-child(1) {.delay(0*@delayInc + @initialDelay); }
&:nth-child(2) {.delay(1*@delayInc + @initialDelay); }
@@ -97,14 +95,12 @@
&:nth-child(20) {.delay(19*@delayInc + @initialDelay); }
}
.createFrames(@name, @from, @to) {
@frames: {
from { @from(); }
to { @to(); }
};
@-webkit-keyframes @name {@frames();}
@-webkit-keyframes @name {@frames();}
@-moz-keyframes @name {@frames();}
@-ms-keyframes @name {@frames();}
@-o-keyframes @name {@frames();}
@@ -126,8 +122,6 @@
animation-timing-function : @easing;
}
/***************************
Standard Animations
****************************/
@@ -136,7 +130,7 @@
.createAnimation(fadeIn; @duration; @easing);
.createFrames(fadeIn,
{ opacity : 0; },
{ opacity : 1; }
{ opacity : 1; }
);
}
@@ -144,7 +138,7 @@
.createAnimation(fadeInDown; @duration; @easing);
.createFrames(fadeInDown,
{ opacity : 0; .transform(translateY(20px));},
{ opacity : 1; .transform(translateY(0px));}
{ opacity : 1; .transform(translateY(0px));}
);
}
@@ -152,7 +146,7 @@
.createAnimation(fadeInTop; @duration; @easing);
.createFrames(fadeInTop,
{ opacity : 0; .transform(translateY(-20px)); },
{ opacity : 1; .transform(translateY(0px));}
{ opacity : 1; .transform(translateY(0px));}
);
}
@@ -160,7 +154,7 @@
.createAnimation(fadeInLeft; @duration; @easing);
.createFrames(fadeInLeft,
{ opacity: 0; .transform(translateX(-20px));},
{ opacity: 1; .transform(translateX(0));}
{ opacity: 1; .transform(translateX(0));}
);
}
@@ -168,7 +162,7 @@
.createAnimation(fadeInRight; @duration; @easing);
.createFrames(fadeInRight,
{ opacity: 0; .transform(translateX(20px));},
{ opacity: 1; .transform(translateX(0));}
{ opacity: 1; .transform(translateX(0));}
);
}
@@ -176,7 +170,7 @@
.createAnimation(fadeOut; @duration; @easing);
.createFrames(fadeOut,
{ opacity : 1; },
{ opacity : 0; }
{ opacity : 0; }
);
}
@@ -184,7 +178,7 @@
.createAnimation(fadeOutDown; @duration; @easing);
.createFrames(fadeOutDown,
{ opacity : 1; .transform(translateY(0)); visibility: visible;},
{ opacity : 0; .transform(translateY(20px)); visibility: hidden;}
{ opacity : 0; .transform(translateY(20px)); visibility: hidden;}
);
}
@@ -192,7 +186,7 @@
.createAnimation(fadeOutTop; @duration; @easing);
.createFrames(fadeOutTop,
{ opacity : 1; .transform(translateY(0)); },
{ opacity : 0; .transform(translateY(-20px)); }
{ opacity : 0; .transform(translateY(-20px)); }
);
}
@@ -200,7 +194,7 @@
.createAnimation(fadeOutLeft; @duration; @easing);
.createFrames(fadeOutLeft,
{ opacity : 1; .transform(translateX(0));},
{ opacity : 0; .transform(translateX(-20px));}
{ opacity : 0; .transform(translateX(-20px));}
);
}
@@ -208,13 +202,10 @@
.createAnimation(fadeOutRight; @duration; @easing);
.createFrames(fadeOutRight,
{ opacity : 1; .transform(translateX(0));},
{ opacity : 0; .transform(translateX(20px));}
{ opacity : 0; .transform(translateX(20px));}
);
}
/***************************
Fun Animations
****************************/
+2 -2
View File
@@ -40,7 +40,7 @@ p {
color : @copyGrey;
}
code {
font-family : 'Courier', "mono";
font-family : 'Courier', 'mono';
color : black;
white-space : pre;
background-color : #F8F8F8;
@@ -54,7 +54,7 @@ button {
.animate(background-color);
display : inline-block;
padding : 0.6em 1.2em;
font-family : 'Lato', "Helvetica", "Arial", sans-serif;
font-family : 'Lato', 'Helvetica', 'Arial', sans-serif;
font-size : 15px;
color : white;
text-decoration : none;
+1 -1
View File
@@ -22,4 +22,4 @@
background-color : unset;
}
:where(i){ text-box-trim: trim-both }
:where(i) { text-box-trim : trim-both; }
+26 -26
View File
@@ -3,23 +3,23 @@
@arrowSize : 6px;
@arrowPosition : 18px;
[data-tooltip] {
position:relative;
position : relative;
.tooltip(attr(data-tooltip));
}
[data-tooltip-top] {
position:relative;
position : relative;
.tooltipTop(attr(data-tooltip-top));
}
[data-tooltip-bottom] {
position:relative;
position : relative;
.tooltipBottom(attr(data-tooltip-bottom));
}
[data-tooltip-left] {
position:relative;
position : relative;
.tooltipLeft(attr(data-tooltip-left));
}
[data-tooltip-right] {
position:relative;
position : relative;
.tooltipRight(attr(data-tooltip-right));
}
.tooltip(@content) {
@@ -33,9 +33,9 @@
}
&::after { margin-left : -18px; }
&::before, &::after {
bottom : 100%;
left : 50%;
translate: -50% 0;
bottom : 100%;
left : 50%;
translate : -50% 0;
}
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
.transform(translateY(-(@arrowSize + 2)));
@@ -49,9 +49,9 @@
}
&::after { margin-left : -18px; }
&::before, &::after {
top : 100%;
left : 50%;
translate: -50% 0;
top : 100%;
left : 50%;
translate : -50% 0;
}
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
.transform(translateY(@arrowSize + 2));
@@ -65,8 +65,8 @@
border-left-color : @tooltipColor;
}
&::after {
max-width : 50ch;
margin-bottom : -14px;
max-width : 50ch;
}
&::before, &::after {
right : 100%;
@@ -84,13 +84,13 @@
border-right-color : @tooltipColor;
}
&::after {
max-width : 50ch;
margin-bottom : -14px;
max-width : 50ch;
}
&::before, &::after {
top : 50%;
left : 100%;
translate:0 -50%;
top : 50%;
left : 100%;
translate : 0 -50%;
}
&:hover::after, &:hover::before, &:focus::after, &:focus::before {
.transform(translateX(@arrowSize + 2));
@@ -115,17 +115,17 @@
}
//Box
&::after {
visibility : hidden;
padding : 8px 10px;
font-size : 12px;
line-height : 12px;
color : white;
content : @content;
background : @tooltipColor;
max-width : 60ch;
width :max-content;
word-break : break-word;
visibility : hidden;
width : max-content;
max-width : 60ch;
padding : 8px 10px;
font-size : 12px;
line-height : 12px;
color : white;
word-break : break-word;
overflow-wrap : break-word;
content : @content;
background : @tooltipColor;
}
&:hover::before, &:hover::after {
visibility : visible;
+21 -21
View File
@@ -47,19 +47,19 @@ describe(`brewSnippetsToJSON`, ()=>{
gen : 'I am the first brew snippet!\n',
name : 'First Brew Snippet'
}, {
gen : 'I am the second brew Snippet!',
name: 'Second Brew Snippet'
gen : 'I am the second brew Snippet!',
name : 'Second Brew Snippet'
}
]}]
] }]
};
const rendered = brewSnippetsToJSON(`Test Snippets JSON without theme snippets`, brewSnippetsBrewTest, null, true);
expect(rendered).toStrictEqual(testMenuObject);
});
it('converts raw brew snippets with theme snippets to JSON', function() {
it('converts raw brew snippets with theme snippets to JSON', function() {
const testMenuObject = {
groupName : 'Brew Snippets',
icon : 'fas fa-th-list',
icon : 'fas fa-th-list',
view : 'text',
snippets : [{
gen : '',
@@ -76,20 +76,20 @@ describe(`brewSnippetsToJSON`, ()=>{
icon : '',
name : 'Second Theme Snippet',
},
]},
{
name : 'Test Snippets JSON with theme snippets',
subsnippets : [
{
gen : 'I am the first brew snippet!\n',
name : 'First Brew Snippet'
},
{
gen : 'I am the second brew Snippet!',
name: 'Second Brew Snippet'
}
]
}]};
] },
{
name : 'Test Snippets JSON with theme snippets',
subsnippets : [
{
gen : 'I am the first brew snippet!\n',
name : 'First Brew Snippet'
},
{
gen : 'I am the second brew Snippet!',
name : 'Second Brew Snippet'
}
]
}] };
const rendered = brewSnippetsToJSON(`Test Snippets JSON with theme snippets`, brewSnippetsBrewTest, brewSnippetsThemeTest, true);
expect(rendered).toStrictEqual(testMenuObject);
});
@@ -103,8 +103,8 @@ describe(`YAMLSnippetsToText`, ()=>{
gen : 'I am the first brew snippet!\n',
name : 'First Brew Snippet'
}, {
gen : 'I am the second brew Snippet!',
name: 'Second Brew Snippet'
gen : 'I am the second brew Snippet!',
name : 'Second Brew Snippet'
}
]
}];
+1 -1
View File
@@ -1,6 +1,6 @@
import {hbfm} from 'marked-hbfm';
import { hbfm } from 'marked-hbfm';
describe('Non-Breaking Spaces Interactions', ()=>{
test('I am actually a single-line definition list!', function() {
+16 -16
View File
@@ -25,16 +25,16 @@ body { counter-reset : phb-page-numbers; }
}
}
.useColumns(@multiplier : 1) {
column-count : 2;
column-fill : auto;
column-gap : 1cm;
-webkit-column-width : 8cm * @multiplier;
-moz-column-width : 8cm * @multiplier;
column-width : 8cm * @multiplier;
-webkit-column-count : 2;
-moz-column-count : 2;
-webkit-column-width : 8cm * @multiplier;
-moz-column-width : 8cm * @multiplier;
column-count : 2;
column-fill : auto;
-webkit-column-gap : 1cm;
-moz-column-gap : 1cm;
column-gap : 1cm;
}
.phb, .page {
.useColumns();
@@ -43,8 +43,11 @@ body { counter-reset : phb-page-numbers; }
box-sizing : border-box;
width : 215.9mm;
height : 279.4mm;
contain-intrinsic-size : auto none;
padding : 1.0cm 1.7cm;
padding-bottom : 1.5cm;
contain : strict;
content-visibility : auto;
overflow : hidden;
font-family : 'BookSanity';
font-size : 0.317cm;
@@ -52,11 +55,8 @@ body { counter-reset : phb-page-numbers; }
background-color : @background;
background-image : @backgroundImage;
text-rendering : optimizeLegibility;
page-break-before : always;
page-break-after : always;
contain : strict;
content-visibility : auto;
contain-intrinsic-size : auto none;
break-before : page;
break-after : page;
}
.phb {
@@ -111,10 +111,10 @@ body { counter-reset : phb-page-numbers; }
color : @headerText;
}
h1 {
column-span : all;
font-size : 0.987cm;
-webkit-column-span : all;
-moz-column-span : all;
column-span : all;
font-size : 0.987cm;
& + p::first-letter {
float : left;
font-family : 'Solberry';
@@ -308,7 +308,7 @@ body { counter-reset : phb-page-numbers; }
p,blockquote,table {
z-index : 15;
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
break-inside : avoid;
}
//Better spacing for spell blocks
@@ -322,7 +322,7 @@ body { counter-reset : phb-page-numbers; }
}
li {
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
break-inside : avoid;
}
}
@@ -347,7 +347,7 @@ body { counter-reset : phb-page-numbers; }
list-style-type : none;
break-inside : auto;
-webkit-column-break-inside : auto;
page-break-inside : auto;
break-inside : auto;
}
}
//*****************************
@@ -436,7 +436,7 @@ body { counter-reset : phb-page-numbers; }
// *****************************/
.phb .toc {
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
break-inside : avoid;
a {
color : black;
+26 -26
View File
@@ -27,16 +27,16 @@
h5 + * { margin-top : 0.1cm; }
}
.useColumns(@multiplier : 1, @fillMode: auto) {
column-count : 2;
column-fill : @fillMode;
column-gap : 0.9cm;
-webkit-column-width : 8cm * @multiplier;
-moz-column-width : 8cm * @multiplier;
column-width : 8cm * @multiplier;
-webkit-column-count : 2;
-moz-column-count : 2;
-webkit-column-width : 8cm * @multiplier;
-moz-column-width : 8cm * @multiplier;
column-count : 2;
column-fill : @fillMode;
-webkit-column-gap : 0.9cm;
-moz-column-gap : 0.9cm;
column-gap : 0.9cm;
}
.page {
.useColumns();
@@ -79,12 +79,12 @@
color : var(--HB_Color_HeaderText);
}
h1 {
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
column-span : all;
font-size : 0.89cm;
line-height : 1em;
-webkit-column-span : all;
-moz-column-span : all;
column-span : all;
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
font-size : 0.89cm;
line-height : 1em;
& + p::first-line { font-variant : small-caps; }
}
p.first-letter::first-letter,
@@ -363,13 +363,13 @@
// Monster Ability table
hr + table:first-of-type {
margin : 0;
-webkit-column-span : none;
column-span : none;
margin : 0;
color : var(--HB_Color_HeaderText);
background-color : transparent;
border-style : none;
border-image : none;
-webkit-column-span : none;
tr { background-color : transparent; }
td,th { padding : 0px; }
}
@@ -420,15 +420,15 @@
color : var(--HB_Color_Footnotes);
}
.footnote {
text-transform: uppercase;
position : absolute;
right : 80px;
bottom : 32px;
z-index : 150;
width : 200px;
font-size : 0.8em;
color : var(--HB_Color_Footnotes);
text-align : right;
position : absolute;
right : 80px;
bottom : 32px;
z-index : 150;
width : 200px;
font-size : 0.8em;
color : var(--HB_Color_Footnotes);
text-align : right;
text-transform : uppercase;
}
// ************************************
// * CODE BLOCKS
@@ -488,7 +488,7 @@
text-indent : -1em;
list-style-type : none;
-webkit-column-break-inside : auto;
page-break-inside : auto;
break-inside : auto;
break-inside : auto;
}
&.wide { column-count : 4; }
@@ -615,9 +615,9 @@
font-size : 0.496cm;
color : white;
text-align : center;
-webkit-text-stroke : 0.1cm black;
text-transform : none;
paint-order : stroke;
text-transform : none;
-webkit-text-stroke : 0.1cm black;
}
.logo {
position : absolute;
@@ -675,10 +675,10 @@
// * BACK COVER
// *****************************/
.page:has(.backCover) {
columns : 1;
padding : 2.25cm 1.3cm 2cm 1.3cm;
line-height : 1.4em;
color : #FFFFFF;
columns : 1;
&::after { display : none; }
.columnWrapper { width : 7.6cm; }
.backCover {
@@ -756,9 +756,9 @@
// * PART COVER
// *****************************/
.page:has(.partCover) {
columns : 1;
padding-top : 0;
text-align : center;
columns : 1;
.partCover {
position : absolute;
@@ -807,7 +807,7 @@
&:has(.toc)::after { display : none; }
.toc {
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
break-inside : avoid;
h1 {
margin-bottom : 0.3cm;
+1 -1
View File
@@ -329,7 +329,7 @@ export default [
},
{
name : 'DTRPG Community Content',
icon : null,
icon : null,
subsnippets : [
{
name : 'Chronicle System Guild Colophon',
+45 -57
View File
@@ -34,15 +34,15 @@ body { counter-reset : page-numbers 0; }
}
.useColumns(@multiplier : 1, @fillMode: auto) {
column-fill : @fillMode;
column-count : 2;
column-fill : @fillMode;
}
.columnWrapper {
columns : inherit;
column-span : all;
column-fill : inherit;
column-gap : inherit;
max-height : 100%;
column-span : all;
columns : inherit;
column-fill : inherit;
}
.page {
.useColumns();
@@ -51,13 +51,13 @@ body { counter-reset : page-numbers 0; }
box-sizing : border-box;
width : 215.9mm;
height : 279.4mm;
contain-intrinsic-size : auto none;
padding : 1.4cm 1.9cm 1.7cm;
contain : strict;
content-visibility : auto;
overflow : clip;
background-color : var(--HB_Color_Background);
text-rendering : optimizeLegibility;
contain : strict;
content-visibility : auto;
contain-intrinsic-size : auto none;
}
//*****************************
// * BASE
@@ -145,7 +145,7 @@ body { counter-reset : page-numbers 0; }
blockquote,table {
z-index : 15;
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
break-inside : avoid;
}
// Nested lists
@@ -155,20 +155,20 @@ body { counter-reset : page-numbers 0; }
}
li {
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
break-inside : avoid;
}
/* Wrap Text */
.wrapLeft {
shape-outside : var(--HB_src);
float : right;
shape-outside : var(--HB_src);
shape-margin : 0.2cm;
}
.wrapRight {
shape-outside : var(--HB_src);
float : left;
shape-outside : var(--HB_src);
shape-margin : 0.2cm;
}
@@ -433,8 +433,8 @@ body { counter-reset : page-numbers 0; }
// *****************************/
.page {
.wide {
column-span : all;
display : block;
column-span : all;
margin-bottom : 1em;
& + * { margin-top : 0; }
}
@@ -502,66 +502,54 @@ body { counter-reset : page-numbers 0; }
// * Ordered List Type Overrides
// *****************************/
@counter-style lower-alpha-trailing-paren {
system: extends lower-alpha;
prefix: "";
suffix: ") ";
}
system : extends lower-alpha;
prefix : '';
suffix : ') ';
}
.page{
.ol-ccby ol {
list-style-type: lower-alpha;
}
.page {
.ol-ccby ol { list-style-type : lower-alpha; }
.ol-ccby ol ol{
padding-left: 0px !important;
list-style-type: greek;
.ol-ccby ol ol {
padding-left : 0px !important;
list-style-type : greek;
}
.ol-ccby ol ol ol {
padding-left: 0px !important;
list-style-type: upper-alpha;
padding-left : 0px !important;
list-style-type : upper-alpha;
}
.ol-ccby ol ol ol ol {
padding-left: 0px !important;
list-style-type: lower-roman;
padding-left : 0px !important;
list-style-type : lower-roman;
}
.ol-ccby p {
padding-left: 25px;
text-indent: 0em !important;
padding-left : 25px;
text-indent : 0em !important;
}
// Non-indented, numerical lists with slight gap between paragraphs.
.ol-lowerLetters ol { list-style-type : lower-alpha-trailing-paren; }
.ol-lowerLetters ol li p { display : inline; }
.ol-lowerLetters ol {
list-style-type: lower-alpha-trailing-paren;
padding : 0px;
list-style-position : inside;
}
.ol-lowerLetters ol li p {
display: inline;
}
.ol-lowerLetters ol {
list-style-position: inside;
padding: 0px;
}
.ol-lowerLetters ol li:not(:last-child) {
margin-bottom: 0.5em;
}
.ol-lowerLetters ol li:not(:last-child) { margin-bottom : 0.5em; }
.ol-noIndent ol {
list-style-position: inside;
padding: 0px;
padding : 0px;
list-style-position : inside;
}
.ol-noIndent ol li p {
display: inline;
}
.ol-noIndent ol li p { display : inline; }
.ol-noIndent ol li:not(:last-child) {
margin-bottom: 0.5em;
}
.ol-noIndent ol li:not(:last-child) { margin-bottom : 0.5em; }
}
// *****************************
@@ -617,7 +605,7 @@ h6,
&:has(.toc)::after { display : none; }
.toc {
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
break-inside : avoid;
h1 {
margin-bottom : 0.3cm;
@@ -684,10 +672,10 @@ Firefox endruns
**********************************/
@supports (-moz-user-select: none) { // This section will only apply to Firefox; it's the only browser that supports `-mos-xyz...`
.page {
blockquote, table {
page-break-inside: auto;
break-inside: auto;
}
}
.page {
blockquote, table {
break-inside : auto;
break-inside : auto;
}
}
}
+4 -4
View File
@@ -275,12 +275,12 @@
// Monster Ability table
hr + table:first-of-type {
column-span : none;
margin : 0;
color : inherit;
background-image : none;
border-style : none;
border-image : none;
column-span : none;
tr { background-image : none; }
td,th { padding : 0px; }
}
@@ -372,8 +372,8 @@
.page .spellList {
.useSansSerif();
font-family : 'PermanentMarker';
column-count : 2;
font-family : 'PermanentMarker';
ul + h5 { margin-top : 15px; }
ul {
padding-left : 1em;
@@ -382,7 +382,7 @@
list-style-type : none;
break-inside : auto;
-webkit-column-break-inside : auto;
page-break-inside : auto;
break-inside : auto;
}
&.wide { column-count : 4; }
}
@@ -399,7 +399,7 @@
// *****************************/
.page .toc {
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
break-inside : avoid;
h1 {
margin-bottom : 0.3cm;
+13 -21
View File
@@ -8,31 +8,23 @@
}
.page {
font-family: Cambria,Georgia,serif;
font-size: 14px;
font-family : "Cambria","Georgia",serif;
font-size : 14px;
h1, h2, h3, h4 {
font-variant: small-caps;
font-weight: normal;
font-weight : normal;
font-variant : small-caps;
}
h1 {
column-span: all;
-webkit-column-span: all;
font-size: 40px;
}
h2 {
font-size: 26px;
-webkit-column-span : all;
column-span : all;
font-size : 40px;
}
h2 { font-size : 26px; }
h3 {
font-size: 20px;
border-bottom: 2px solid black;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 16px;
}
h6 {
font-size: 14px;
font-size : 20px;
border-bottom : 2px solid black;
}
h4 { font-size : 18px; }
h5 { font-size : 16px; }
h6 { font-size : 14px; }
}
+9 -9
View File
@@ -1,24 +1,24 @@
/* Main Font, serif */
@font-face {
font-family : "BookSanity";
font-family : 'BookSanity';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e legacy/Bookinsanity.woff2');
}
@font-face {
font-family : "BookSanity";
font-family : 'BookSanity';
font-style : normal;
font-weight : bold;
src : url('../../../fonts/5e legacy/Bookinsanity Bold.woff2');
}
@font-face {
font-family : "BookSanity";
font-family : 'BookSanity';
font-style : italic;
font-weight : normal;
src : url('../../../fonts/5e legacy/Bookinsanity Italic.woff2');
}
@font-face {
font-family : "BookSanity";
font-family : 'BookSanity';
font-style : italic;
font-weight : bold;
src : url('../../../fonts/5e legacy/Bookinsanity Bold Italic.woff2');
@@ -26,19 +26,19 @@
/* Notes and Tables, sans-serif */
@font-face {
font-family : "ScalySans";
font-family : 'ScalySans';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e legacy/Scaly Sans.woff2');
}
@font-face {
font-family : "ScalySansSmallCaps";
font-family : 'ScalySansSmallCaps';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e legacy/Scaly Sans Caps.woff2');
}
@font-face {
font-family : "WalterTurncoat";
font-family : 'WalterTurncoat';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e legacy/WalterTurncoat-Regular.woff2');
@@ -46,7 +46,7 @@
/* Headers */
@font-face {
font-family : "MrJeeves";
font-family : 'MrJeeves';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e legacy/Mr Eaves Small Caps.woff2');
@@ -54,7 +54,7 @@
/* Fancy Drop Cap */
@font-face {
font-family : "Solberry";
font-family : 'Solberry';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e legacy/Solbera Imitation.woff2');
+21 -21
View File
@@ -1,24 +1,24 @@
/* Main Font, serif */
@font-face {
font-family : "BookInsanityRemake";
font-family : 'BookInsanityRemake';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e/Bookinsanity.woff2');
}
@font-face {
font-family : "BookInsanityRemake";
font-family : 'BookInsanityRemake';
font-style : normal;
font-weight : bold;
src : url('../../../fonts/5e/Bookinsanity Bold.woff2');
}
@font-face {
font-family : "BookInsanityRemake";
font-family : 'BookInsanityRemake';
font-style : italic;
font-weight : normal;
src : url('../../../fonts/5e/Bookinsanity Italic.woff2');
}
@font-face {
font-family : "BookInsanityRemake";
font-family : 'BookInsanityRemake';
font-style : italic;
font-weight : bold;
src : url('../../../fonts/5e/Bookinsanity Bold Italic.woff2');
@@ -26,37 +26,37 @@
/* Notes and Tables, sans-serif */
@font-face {
font-family : "ScalySansRemake";
font-family : 'ScalySansRemake';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e/Scaly Sans.woff2');
}
@font-face {
font-family : "ScalySansRemake";
font-family : 'ScalySansRemake';
font-style : normal;
font-weight : bold;
src : url('../../../fonts/5e/Scaly Sans Bold.woff2');
}
@font-face {
font-family : "ScalySansRemake";
font-family : 'ScalySansRemake';
font-style : italic;
font-weight : normal;
src : url('../../../fonts/5e/Scaly Sans Italic.woff2');
}
@font-face {
font-family : "ScalySansRemake";
font-family : 'ScalySansRemake';
font-style : italic;
font-weight : bold;
src : url('../../../fonts/5e/Scaly Sans Bold Italic.woff2');
}
@font-face {
font-family : "ScalySansSmallCapsRemake";
font-family : 'ScalySansSmallCapsRemake';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e/Scaly Sans Caps.woff2');
}
@font-face {
font-family : "WalterTurncoat";
font-family : 'WalterTurncoat';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e/WalterTurncoat-Regular.woff2');
@@ -64,7 +64,7 @@
/* Headers */
@font-face {
font-family : "MrEavesRemake";
font-family : 'MrEavesRemake';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e/Mr Eaves Small Caps.woff2');
@@ -72,7 +72,7 @@
/* Fancy Drop Cap */
@font-face {
font-family : "SolberaImitationRemake"; //Tweaked 5e version
font-family : 'SolberaImitationRemake'; //Tweaked 5e version
font-style : normal;
font-style : italic;
font-weight : 100 1000;
@@ -81,62 +81,62 @@
/* Cover Page */
@font-face {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e/Nodesto Caps Condensed.woff2');
}
@font-face {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-style : normal;
font-weight : bold;
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold.woff2');
}
@font-face {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-style : italic;
font-weight : normal;
src : url('../../../fonts/5e/Nodesto Caps Condensed Italic.woff2');
}
@font-face {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-style : italic;
font-weight : bold;
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold Italic.woff2');
}
@font-face {
font-family : "NodestoCapsWide";
font-family : 'NodestoCapsWide';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e/Nodesto Caps Wide.woff2');
}
@font-face {
font-family : "Overpass";
font-family : 'Overpass';
font-style : normal;
font-weight : 500;
src : url('../../../fonts/5e/Overpass Medium.woff2');
}
@font-face {
font-family : "Davek";
font-family : 'Davek';
font-style : normal;
font-weight : 500;
src : url('../../../fonts/5e/Davek.woff2');
}
@font-face {
font-family : "Iokharic";
font-family : 'Iokharic';
font-style : normal;
font-weight : 500;
src : url('../../../fonts/5e/Iokharic.woff2');
}
@font-face {
font-family : "Rellanic";
font-family : 'Rellanic';
font-style : normal;
font-weight : 500;
src : url('../../../fonts/5e/Rellanic.woff2');
+4 -4
View File
@@ -18,28 +18,28 @@ License:
*/
@font-face {
font-family : "Pagella";
font-family : 'Pagella';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/Blank/texgyrepagella-regular.woff2');
}
@font-face {
font-family : "Pagella";
font-family : 'Pagella';
font-style : normal;
font-weight : bold;
src : url('../../../fonts/Blank/texgyrepagella-bold.woff2');
}
@font-face {
font-family : "Pagella";
font-family : 'Pagella';
font-style : italic;
font-weight : normal;
src : url('../../../fonts/Blank/texgyrepagella-italic.woff2');
}
@font-face {
font-family : "Pagella";
font-family : 'Pagella';
font-style : italic;
font-weight : bold;
src : url('../../../fonts/Blank/texgyrepagella-bolditalic.woff2');
+8 -8
View File
@@ -1,6 +1,6 @@
/* Main Font, serif */
@font-face {
font-family : "ReenieBeanie";
font-family : 'ReenieBeanie';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/Journal/ReenieBeanie-Regular.woff2');
@@ -8,13 +8,13 @@
/* Notes and Tables, sans-serif */
@font-face {
font-family : "PermanentMarker";
font-family : 'PermanentMarker';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/Journal/PermanentMarker-Regular.woff2');
}
@font-face {
font-family : "WalterTurncoat";
font-family : 'WalterTurncoat';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e/WalterTurncoat-Regular.woff2');
@@ -22,7 +22,7 @@
/* Headers */
@font-face {
font-family : "FrederickaTheGreat";
font-family : 'FrederickaTheGreat';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/Journal/FrederickaTheGreat-Regular.woff2');
@@ -30,28 +30,28 @@
/* Cover Page */
@font-face {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-style : normal;
font-weight : normal;
src : url('../../../fonts/5e/Nodesto Caps Condensed.woff2');
}
@font-face {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-style : normal;
font-weight : bold;
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold.woff2');
}
@font-face {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-style : italic;
font-weight : normal;
src : url('../../../fonts/5e/Nodesto Caps Condensed Italic.woff2');
}
@font-face {
font-family : "NodestoCapsCondensed";
font-family : 'NodestoCapsCondensed';
font-style : italic;
font-weight : bold;
src : url('../../../fonts/5e/Nodesto Caps Condensed Bold Italic.woff2');
+12 -12
View File
@@ -25,19 +25,19 @@
&.F-minus::before { content : '\f191'; }
&.F-plus::before { content : '\f192'; }
&.F-zero::before { content : '\f193'; }
&.d10::before { content : '\f194'; }
&.d10-0::before { content : '\f100'; }
&.d10-1:before { content : '\f101'; }
&.d10::before { content : '\f194'; }
&.d10-0::before { content : '\f100'; }
&.d10-1::before { content : '\f101'; }
&.d10-10::before { content : '\f102'; }
&.d10-2:before { content : '\f103'; }
&.d10-3::before { content : '\f104'; }
&.d10-4::before { content : '\f105'; }
&.d10-5::before { content : '\f106'; }
&.d10-6::before { content : '\f107'; }
&.d10-7::before { content : '\f108'; }
&.d10-8::before { content : '\f109'; }
&.d10-9::before { content : '\f10A'; }
&.d12::before { content : '\f195'; }
&.d10-2::before { content : '\f103'; }
&.d10-3::before { content : '\f104'; }
&.d10-4::before { content : '\f105'; }
&.d10-5::before { content : '\f106'; }
&.d10-6::before { content : '\f107'; }
&.d10-7::before { content : '\f108'; }
&.d10-8::before { content : '\f109'; }
&.d10-9::before { content : '\f10A'; }
&.d12::before { content : '\f195'; }
&.d12-1::before { content : '\f10b'; }
&.d12-10::before { content : '\f10c'; }
&.d12-11::before { content : '\f10d'; }
+4 -4
View File
@@ -1,7 +1,7 @@
@import (less) "./themes/fonts/iconFonts/fontawesome-free.less";
@import (less) "./themes/fonts/iconFonts/fontawesome-solid.less";
@import (less) "./themes/fonts/iconFonts/fontawesome-brands.less";
@import (less) "./themes/fonts/iconFonts/fontawesome-regular.less";
@import (less) './themes/fonts/iconFonts/fontawesome-free.less';
@import (less) './themes/fonts/iconFonts/fontawesome-solid.less';
@import (less) './themes/fonts/iconFonts/fontawesome-brands.less';
@import (less) './themes/fonts/iconFonts/fontawesome-regular.less';
/* Icon Font: Font Awesome */
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -4,16 +4,15 @@
* Copyright 2024 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; }
--fa-style-family-classic : 'Font Awesome 6 Free';
--fa-font-regular : normal 400 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("/fonts/iconFonts/fa-regular-400.woff2") format("woff2"), url("/fonts/iconFonts/fa-regular-400.ttf") format("truetype"); }
font-family : 'Font Awesome 6 Free';
font-style : normal;
font-weight : normal;
src : url('/fonts/iconFonts/fa-regular-400.woff2') format('woff2'), url('/fonts/iconFonts/fa-regular-400.ttf') format('truetype');
font-display : block; }
.far,
.fa-regular {
font-weight: 400; }
.fa-regular { font-weight : normal; }
@@ -4,16 +4,15 @@
* Copyright 2024 Fonticons, Inc.
*/
:root, :host {
--fa-style-family-classic: 'Font Awesome 6 Free';
--fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; }
--fa-style-family-classic : 'Font Awesome 6 Free';
--fa-font-solid : normal 900 1em/1 'Font Awesome 6 Free'; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("/fonts/iconFonts/fa-solid-900.woff2") format("woff2"), url("/fonts/iconFonts/fa-solid-900.ttf") format("truetype"); }
font-family : 'Font Awesome 6 Free';
font-style : normal;
font-weight : 900;
src : url('/fonts/iconFonts/fa-solid-900.woff2') format('woff2'), url('/fonts/iconFonts/fa-solid-900.ttf') format('truetype');
font-display : block; }
.fas,
.fa-solid {
font-weight: 900; }
.fa-solid { font-weight : 900; }
+6 -6
View File
@@ -8,12 +8,12 @@ export default defineConfig({
plugins : [react(), generateAssetsPlugin()],
resolve : {
alias : {
'@vitreum' : path.resolve(__dirname, './vitreum'),
'@shared' : path.resolve(__dirname, './shared'),
'@sharedStyles' : path.resolve(__dirname, './shared/naturalcrit/styles'),
'@navbar' : path.resolve(__dirname, './client/homebrew/navbar'),
'@themes' : path.resolve(__dirname, './themes'),
'@components' : path.resolve(__dirname, './client/components')
'@vitreum' : path.resolve(import.meta.dirname, './vitreum'),
'@shared' : path.resolve(import.meta.dirname, './shared'),
'@sharedStyles' : path.resolve(import.meta.dirname, './shared/naturalcrit/styles'),
'@navbar' : path.resolve(import.meta.dirname, './client/homebrew/navbar'),
'@themes' : path.resolve(import.meta.dirname, './themes'),
'@components' : path.resolve(import.meta.dirname, './client/components')
},
},
build : {
+8
View File
@@ -12,6 +12,14 @@ export function generateAssetsPlugin(isDev = false) {
// Copy favicon
await fs.copy('./client/homebrew/favicon.ico', `${buildDir}/assets/favicon.ico`);
//Copy this image into multiple paths to avoid server spam where browser requests an image
//at the wrong path, gets a 406, then in response requests broken-image which also fails. Error handler
//used the wrong path for this image causing an infinite request loop. Should be able to remove
//in some time once all users haver refreshed their browser
await fs.copy('./client/icons/broken-image.jpg', `${buildDir}/edit/client/icons/broken-image.jpg`);
await fs.copy('./client/icons/broken-image.jpg', `${buildDir}/new/client/icons/broken-image.jpg`);
await fs.copy('./client/icons/broken-image.jpg', `${buildDir}/client/icons/broken-image.jpg`);
// Copy shared styles/fonts
const assets = fs.readdirSync('./shared/naturalcrit/styles');
for (const file of assets) {
+12 -75
View File
@@ -1,91 +1,28 @@
import React, { useEffect } from 'react';
//old vitreum file, still imported in some pages
const injectTag = (tag, props, children)=>{
const injectNode = document.createElement(tag);
Object.entries(props).forEach(([key, val])=>injectNode[key] = val);
if(children) injectNode.appendChild(document.createTextNode(children));
document.getElementsByTagName('head')[0].appendChild(injectNode);
};
const obj2props = (obj)=>Object.entries(obj)
.map(([k, v])=>`${k}="${v}"`)
const obj2props = (obj) =>
Object.entries(obj)
.map(([k, v]) => `${k}="${v}"`)
.join(' ');
const toStr = (chld)=>(Array.isArray(chld) ? chld.join('') : chld);
const onServer = typeof window === 'undefined';
let NamedTags = {};
let UnnamedTags = [];
export const HeadComponents = {
Title({ children }) {
if(onServer) NamedTags.title = `<title>${toStr(children)}</title>`;
useEffect(()=>{
document.title = toStr(children);
}, [children]);
return null;
},
Favicon({ type = 'image/png', href = '', rel = 'icon', id = 'favicon' }) {
if(onServer) NamedTags.favicon = `<link rel='shortcut icon' type="${type}" id="${id}" href="${href}" />`;
useEffect(()=>{
document.getElementById(id).href = href;
}, [id, href]);
return null;
},
Description({ children }) {
if(onServer) NamedTags.description = `<meta name='description' content='${toStr(children)}' />`;
return null;
},
Noscript({ children }) {
if(onServer) UnnamedTags.push(`<noscript>${toStr(children)}</noscript>`);
return null;
},
Script({ children = [], ...props }) {
if(onServer) {
UnnamedTags.push(
children.length
? `<script ${obj2props(props)}>${toStr(children)}</script>`
: `<script ${obj2props(props)} />`,
);
}
return null;
},
const HeadComponents = {
Meta(props) {
const tag = `<meta ${obj2props(props)} />`;
props.property || props.name ? (NamedTags[props.property || props.name] = tag) : UnnamedTags.push(tag);
useEffect(()=>{
props.property || props.name
? (NamedTags[props.property || props.name] = tag)
: null;
useEffect(() => {
document
.getElementsByTagName('head')[0]
.insertAdjacentHTML('beforeend', Object.values(NamedTags).join('\n'));
}, [NamedTags]);
return null;
},
Style({ children, type = 'text/css' }) {
if(onServer) UnnamedTags.push(`<style type="${type}">${toStr(children)}</style>`);
return null;
},
};
export const Inject = ({ tag, children, ...props })=>{
useEffect(()=>{
injectTag(tag, props, children);
}, []);
return null;
};
export const generate = ()=>Object.values(NamedTags).concat(UnnamedTags).join('\n');
export const flush = ()=>{
NamedTags = {};
UnnamedTags = [];
};
export const Meta = HeadComponents.Meta;
export default {
Inject,
...HeadComponents,
generate,
flush,
};
export default HeadComponents;