0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-08-08 02:05:01 +00:00

Merge branch 'master' of https://github.com/naturalcrit/homebrewery into add-cm-features

This commit is contained in:
Víctor Losada Hernández
2026-07-26 11:20:38 +02:00
37 changed files with 1203 additions and 784 deletions
@@ -11,7 +11,7 @@ import ErrorBar from './errorBar/errorBar.jsx';
import ToolBar from './toolBar/toolBar.jsx';
//TODO: move to the brew renderer
import RenderWarnings from '../../components/renderWarnings/renderWarnings.jsx';
import RenderWarnings from '@components/renderWarnings/renderWarnings.jsx';
import NotificationPopup from './notificationPopup/notificationPopup.jsx';
import Frame from 'react-frame-component';
import dedent from 'dedent';
@@ -29,6 +29,7 @@ const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<title>Rendered Brew Content</title>
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
<link href="${brewRendererStylesUrl}" rel="stylesheet" />
<link href="${headerNavStylesUrl}" rel="stylesheet" />
@@ -210,7 +211,7 @@ const BrewRenderer = (props)=>{
classes = [classes, injectedTags.classes].join(' ').trim();
attributes = injectedTags.attributes;
if(global.enablev4) {
if (attributes && Object.hasOwn(attributes, 'hbtemplate')) {
if(attributes && Object.hasOwn(attributes, 'hbtemplate')) {
pageTemplates[index] = attributes['hbtemplate'];
}
}
@@ -220,7 +221,7 @@ const BrewRenderer = (props)=>{
if(!pageTemplates[index]) {
for (let i=index;i>=0; i--) {
// If one is found, add the template attribute
if (pageTemplates[i]) attributes['hbtemplate'] = pageTemplates[i];
if(pageTemplates[i]) attributes['hbtemplate'] = pageTemplates[i];
}
}
}
@@ -348,10 +349,11 @@ const BrewRenderer = (props)=>{
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/>
{/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
<Frame id='BrewRenderer' title="Rendered Brew Content" initialContent={INITIAL_CONTENT}
style={{ width: '100%', height: '100%', visibility: state.visibility }}
contentDidMount={frameDidMount}
onClick={()=>{emitClick();}}
sandbox="allow-same-origin allow-modals allow-top-navigation"
>
<div className='brewRenderer'
onKeyDown={handleControlKeys}
@@ -1,7 +1,7 @@
import './errorBar.less';
import React from 'react';
import Dialog from '../../../components/dialog.jsx';
import Dialog from '@components/dialog.jsx';
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
@@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from '@shared/markdown.js';
import Dialog from '../../../components/dialog.jsx';
import Dialog from '@components/dialog.jsx';
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
@@ -3,7 +3,7 @@ import './toolBar.less';
import React, { useState, useEffect } from 'react';
import _ from 'lodash';
import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx';
import { Anchored, AnchoredBox, AnchoredTrigger } from '@components/Anchored.jsx';
const MAX_ZOOM = 300;
const MIN_ZOOM = 10;
+1 -1
View File
@@ -5,7 +5,7 @@ import createReactClass from 'create-react-class';
import _ from 'lodash';
import dedent from 'dedent';
import CodeEditor from '../../components/codeEditor/codeEditor.jsx';
import CodeEditor from '@components/codeEditor/codeEditor.jsx';
import SnippetBar from './snippetbar/snippetbar.jsx';
import MetadataEditor from './metadataEditor/metadataEditor.jsx';
@@ -4,7 +4,7 @@ import React from 'react';
import createReactClass from 'create-react-class';
import _ from 'lodash';
import request from '../../utils/request-middleware.js';
import Combobox from '../../../components/combobox.jsx';
import Combobox from '@components/combobox.jsx';
import TagInput from '../tagInput/tagInput.jsx';
@@ -44,6 +44,7 @@ const MetadataEditor = createReactClass({
getInitialState : function(){
return {
isOwner : global.account?.username && global.account?.username === this.props.metadata?.authors[0],
showThumbnail : true
};
},
@@ -144,6 +145,15 @@ const MetadataEditor = createReactClass({
});
},
handleDeleteAuthor : function(author){
if(!confirm('Are you sure you want to remove this author? They will lose all edit access to this brew, and it will dissapear from their userpage.')) return;
if(!this.props.metadata.authors.includes(author)) return;
this.props.onChange({
...this.props.metadata,
authors : this.props.metadata.authors.filter((a)=>a !== author)
});
},
renderPublish : function(){
if(this.props.metadata.published){
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
@@ -170,16 +180,54 @@ const MetadataEditor = createReactClass({
},
renderAuthors : function(){
let text = 'None.';
if(this.props.metadata.authors && this.props.metadata.authors.length){
text = this.props.metadata.authors.join(', ');
}
return <div className='field authors'>
<label>authors</label>
<div className='value'>
{text}
const authors = this.props.metadata.authors;
if(!this.state.isOwner || authors.length < 2) return (
<div className='field authors'>
<label>authors</label>
<div className='value'>
{authors.length > 0 && (
<a href={`/user/${authors[0]}`} className='author-link' target="_blank" title={`Owner - Click to open ${authors[0]}'s profile in a new tab`}>
{authors[0]}{authors.length > 1 && ', '}
</a>
)}
{authors.length > 1 && authors.slice(1).map((author, i)=>(
<a href={`/user/${author}`} className='author-link' title={`Author - Click to open ${author}'s profile in a new tab`}>
{author}{i+2 < authors.length && ', '}
</a>
))}
</div>
</div>
</div>;
);
return (
<div className='field authors'>
<label>Authors</label>
<ul className='list'>
{authors.length > 0 && (
<li className='tag owner' title='Owner'>
<a href={`/user/${authors[0]}`} className='author-link' title={`Owner - Click to open ${authors[0]}'s profile in a new tab`}>
{authors[0]}
</a>
</li>
)}
{authors.length > 1 && authors.slice(1).map((author, i)=>(
<li className='tag author' key={i + 1} title='Author'>
<a href={`/user/${author}`} className='author-link' title={`Author - Click to open ${authors[0]}'s profile in a new tab`}>
{author}
</a>
<button
onClick={()=>this.handleDeleteAuthor(author)}
className='delete'
title={`Remove ${author} as an author`}
>
<i className='fa fa-times fa-fw' />
</button>
</li>
))}
</ul>
</div>
);
},
renderThemeDropdown : function(){
@@ -173,7 +173,51 @@
.colorButton(@red);
}
}
.authors.field .value { line-height : 1.5em; }
.authors.field {
.tag {
font-weight:300;
transition:background-color 0.2s;
&.owner {
position: relative;
background-color:@silverLight;
min-width:25px;
display:grid;
place-items:center;
font-weight: 900;
&::after {
content: "\f521";
font-family: "Font Awesome 6 Free";
color:gold;
position: absolute;
top: 0;
left: 0;
width:15px;
height:15px;
rotate:-25deg;
translate:-30% -50%;
transform: scaleY(0.7);
}
}
&:has(button) a {
padding-right:5px;
}
&:has(button:hover) {
background:#d97d7d;
}
button {
color:@red;
}
}
a {
color:black;
text-decoration:unset;
}
}
.themes.field {
& .dropdown-container {
@@ -275,13 +319,17 @@
}
.tag {
padding : 0.3em;
padding : 0.35em;
margin : 2px;
font-size : 0.9em;
font-size : 0.95em;
background-color : #DDDDDD;
border-radius : 0.5em;
.icon { #groupedIcon; }
button {
cursor : pointer;
}
}
.input-group {
@@ -2,6 +2,7 @@
import './snippetbar.less';
import React from 'react';
import createReactClass from 'create-react-class';
import { Dropdown } from '@components/dropdown/dropdown.jsx';
import _ from 'lodash';
import cx from 'classnames';
@@ -325,36 +326,33 @@ const SnippetGroup = createReactClass({
};
},
handleSnippetClick : function(e, snippet){
e.stopPropagation();
this.props.onSnippetClick(execute(snippet.gen, this.props));
},
renderSnippets : function(snippets){
return _.map(snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
<i className={snippet.icon} />
<span className={`name${snippet.disabled ? ' disabled' : ''}`} title={snippet.name}>{snippet.name}</span>
{snippet.experimental && <span className='beta'>beta</span>}
{snippet.disabled && <span className='beta' title='temporarily disabled due to large slowdown; under re-design'>disabled</span>}
{snippet.subsnippets && <>
<i className='fas fa-caret-right'></i>
<div className='dropdown side'>
if(!snippet.subsnippets){
return (
<button className='menu-item' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)} role='menuitem'>
<i className={snippet.icon} />
<span className={`name${snippet.disabled ? ' disabled' : ''}`} title={snippet.name}>{snippet.name}</span>
{snippet.experimental && <span className='beta'>beta</span>}
{snippet.disabled && <span className='beta' title='temporarily disabled due to large slowdown; under re-design'>disabled</span>}
</button>
);
} else if(snippet.subsnippets){
return (
<Dropdown groupName={snippet.name} icon={snippet.icon} key={snippet.name}>
{this.renderSnippets(snippet.subsnippets)}
</div></>}
</div>;
</Dropdown>
)
}
});
},
render : function(){
const snippetGroup = `snippetGroup snippetBarButton ${this.props.snippets.length === 0 ? 'disabledSnippets' : ''}`;
return <div className={snippetGroup}>
<div className='text'>
<i className={this.props.icon} />
<span className='groupName'>{this.props.groupName}</span>
</div>
<div className='dropdown'>
{this.renderSnippets(this.props.snippets)}
</div>
</div>;
return <Dropdown groupName={this.props.groupName} id={this.props.groupName} icon={this.props.icon}>
{this.renderSnippets(this.props.snippets)}
</Dropdown>;
},
});
@@ -11,18 +11,22 @@
height : auto;
color : black;
background-color : #DDDDDD;
font-size : .65rem;
font-family: 'Open Sans', sans-serif;
text-transform: uppercase;
font-weight: 800;
.snippets {
display : flex;
justify-content : flex-start;
min-width : 470px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
min-width : 565px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
}
.editors {
display : flex;
justify-content : flex-end;
min-width : 275px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
font-size: .85rem;
&:only-child {min-width : unset; margin-left : auto;}
>div {
@@ -57,25 +61,21 @@
}
&.undo {
.tooltipLeft('Undo');
font-size : 0.75em;
color : grey;
&.active { color : inherit; }
}
&.redo {
.tooltipLeft('Redo');
font-size : 0.75em;
color : grey;
&.active { color : inherit; }
}
&.foldAll {
.tooltipLeft('Fold All');
font-size : 0.75em;
color : grey;
&.active { color : inherit; }
}
&.unfoldAll {
.tooltipLeft('Unfold All');
font-size : 0.75em;
color : grey;
&.active { color : inherit; }
}
@@ -88,7 +88,6 @@
&.history {
.tooltipLeft('History');
position : relative;
font-size : 0.75em;
color : grey;
border : none;
&.active { color : inherit; }
@@ -99,7 +98,6 @@
}
&.editorTheme {
.tooltipLeft('Editor Themes');
font-size : 0.75em;
color : inherit;
&.active {
position : relative;
@@ -150,91 +148,97 @@
border-left : 1px solid black;
.tooltipLeft('Edit Brew Properties');
}
.snippetGroup {
&:hover {
& > .dropdown { visibility : visible; }
}
.dropdown {
position : absolute;
top : 100%;
z-index : 1000;
visibility : hidden;
padding : 0px;
margin-left : -5px;
background-color : #DDDDDD;
.snippet {
position : relative;
display : flex;
align-items : center;
min-width : max-content;
padding : 5px;
font-size : 10px;
cursor : pointer;
.animate(background-color);
i {
min-width : 25px;
height : 1.2em;
margin-right : 8px;
font-size : 1.2em;
text-align : center;
& ~ i {
margin-right : 0;
margin-left : 5px;
}
/* Fonts */
&.font {
height : auto;
&::before {
font-size : 1em;
content : 'ABC';
}
&.OpenSans {font-family : 'OpenSans';}
&.CodeBold {font-family : 'CodeBold';}
&.CodeLight {font-family : 'CodeLight';}
&.ScalySansRemake {font-family : 'ScalySansRemake';}
&.BookInsanityRemake {font-family : 'BookInsanityRemake';}
&.MrEavesRemake {font-family : 'MrEavesRemake';}
&.SolberaImitationRemake {font-family : 'SolberaImitationRemake';}
&.ScalySansSmallCapsRemake {font-family : 'ScalySansSmallCapsRemake';}
&.WalterTurncoat {font-family : 'WalterTurncoat';}
&.Lato {font-family : 'Lato';}
&.Courier {font-family : 'Courier';}
&.NodestoCapsCondensed {font-family : 'NodestoCapsCondensed';}
&.Overpass {font-family : 'Overpass';}
&.Davek {font-family : 'Davek';}
&.Iokharic {font-family : 'Iokharic';}
&.Rellanic {font-family : 'Rellanic';}
&.TimesNewRoman {font-family : 'Times New Roman';}
}
}
.name { margin-right : auto; }
.disabled { text-decoration : line-through; }
.beta {
align-self : center;
padding : 4px 6px;
margin-left : 5px;
font-family : monospace;
line-height : 1em;
color : white;
background : grey;
border-radius : 12px;
}
&:hover {
background-color : #999999;
& > .dropdown {
visibility : visible;
&.side {
top : 0%;
left : 100%;
margin-left : 0;
box-shadow : -1px 1px 2px 0px #999999;
}
}
}
.menu-wrapper {
.menu-item:is(.snippets > .menu-wrapper > .menu-item):first-child {
.caret {
display: none;
}
}
.menu-list {
padding : 0px;
background-color : #DDDDDD;
}
}
.menu-item {
position : relative;
display : flex;
justify-content: space-between;
align-items : center;
min-width : max-content;
padding : 5px;
cursor : pointer;
width: 100%;
.animate(background-color);
&:is(.menu-list .menu-item) [class*="name"] {
padding-inline: 8px;
}
.menu-name {
flex: 1;
text-align: left;
text-box-trim: trim-end;
}
i {
min-width : 25px;
height : .85rem;
font-size : 1.2em;
text-align : center;
&.caret {
margin-right: 0;
}
&.caret:is(.menu-wrapper .menu-wrapper * ) {
text-align: right;
}
/* Fonts */
&.font {
height : auto;
&::before {
font-size : 1em;
content : 'ABC';
}
&.OpenSans {font-family : 'OpenSans';}
&.CodeBold {font-family : 'CodeBold';}
&.CodeLight {font-family : 'CodeLight';}
&.ScalySansRemake {font-family : 'ScalySansRemake';}
&.BookInsanityRemake {font-family : 'BookInsanityRemake';}
&.MrEavesRemake {font-family : 'MrEavesRemake';}
&.SolberaImitationRemake {font-family : 'SolberaImitationRemake';}
&.ScalySansSmallCapsRemake {font-family : 'ScalySansSmallCapsRemake';}
&.WalterTurncoat {font-family : 'WalterTurncoat';}
&.Lato {font-family : 'Lato';}
&.Courier {font-family : 'Courier';}
&.NodestoCapsCondensed {font-family : 'NodestoCapsCondensed';}
&.Overpass {font-family : 'Overpass';}
&.Davek {font-family : 'Davek';}
&.Iokharic {font-family : 'Iokharic';}
&.Rellanic {font-family : 'Rellanic';}
&.TimesNewRoman {font-family : 'Times New Roman';}
}
}
.name { margin-right : auto; }
.disabled { text-decoration : line-through; }
.beta {
align-self : center;
padding : 4px 6px;
margin-left : 5px;
font-family : monospace;
line-height : 1em;
color : white;
background : grey;
border-radius : 12px;
}
&:hover {
background-color : #999999;
}
&:disabled {
color: gray;
cursor: not-allowed;
&:hover { background-color: unset; }
}
}
.disabledSnippets {
color: grey;
@@ -244,7 +248,7 @@
}
}
@container editor (width < 745px) {
@container editor (width < 841px) {
.snippetBar {
.editors {
flex : 1;
+1 -1
View File
@@ -1,6 +1,6 @@
import './tagInput.less';
import React, { useState, useEffect } from 'react';
import Combobox from '../../../components/combobox.jsx';
import Combobox from '@components/combobox.jsx';
import { tagSuggestionList, canonizationList } from './curatedTagSuggestionList.js';
+1 -1
View File
@@ -44,7 +44,7 @@ const Homebrew = (props)=>{
global.account = account;
global.version = version;
global.config = config;
global.enablev4 = enablev4;
global.enablev4 = enablev4;
const backgroundObject = ()=>{
if(config?.deployment || (config?.local && config?.development)) {
+2
View File
@@ -1,6 +1,8 @@
import { createRoot } from 'react-dom/client';
import Homebrew from './homebrew.jsx';
import { bootstrapAnchorPositioningPolyfill } from '@components/anchorPositioningPolyfill.js';
const props = window.__INITIAL_PROPS__ || {};
createRoot(document.getElementById('reactRoot')).render(<Homebrew {...props} />);
bootstrapAnchorPositioningPolyfill();
+1 -1
View File
@@ -4,7 +4,7 @@ import createReactClass from 'create-react-class';
import _ from 'lodash';
import cx from 'classnames';
import NaturalCritIcon from '../../components/svg/naturalcrit-d20.svg.jsx';
import NaturalCritIcon from '@components/svg/naturalcrit-d20.svg.jsx';
const Nav = {
base : createReactClass({
@@ -1,7 +1,7 @@
import React from 'react';
import moment from 'moment';
import UIPage from '../basePages/uiPage/uiPage.jsx';
import NaturalCritIcon from '../../../components/svg/naturalcrit-d20.svg.jsx';
import NaturalCritIcon from '@components/svg/naturalcrit-d20.svg.jsx';
let SAVEKEY = '';
+1 -1
View File
@@ -10,7 +10,7 @@ import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
@@ -1,7 +1,7 @@
import './lockNotification.less';
import * as React from 'react';
import request from '../../../utils/request-middleware.js';
import Dialog from '../../../../components/dialog.jsx';
import Dialog from '@components/dialog.jsx';
function LockNotification(props) {
props = {
+1 -1
View File
@@ -10,7 +10,7 @@ import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
+1 -1
View File
@@ -10,7 +10,7 @@ import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import SplitPane from '@components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
@@ -11,7 +11,7 @@ import Account from '@navbar/account.navitem.jsx';
import NewBrew from '@navbar/newbrew.navitem.jsx';
import HelpNavItem from '@navbar/help.navitem.jsx';
import BrewItem from '../basePages/listPage/brewItem/brewItem.jsx';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import SplitPane from '@components/splitPane/splitPane.jsx';
import ErrorIndex from '../errorPage/errors/errorIndex.js';
import request from '../../utils/request-middleware.js';