diff --git a/changelog.md b/changelog.md
index 698cbb128..08b9314f9 100644
--- a/changelog.md
+++ b/changelog.md
@@ -90,9 +90,30 @@ pre {
}
```
+
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
+### Tuesday 10/20/2026 - v3.24.2
+{{taskList
+##### G-Ambatte
+* [x] Hotfix for memory leak on server
+
+Fixes issue [#5090](https://github.com/naturalcrit/homebrewery/issues/5090)
+
+##### Calculuschild
+* [x] Fix brew variables not working across pages
+
+* [x] Allow brew variable definitions to be on adjacent lines (no blank line required anymore)
+
+##### 5e-Cleric
+* [x] Partial rollout of some site UI changes (includes alternate views and darkmode in {{openSans **BREWS** :fas_beer_mug_empty:}} page, and changes to {{openSans **VAULT** :fas_dungeon:}})
+
+##### Abquintic, Calculuschild, 5e-Cleric
+* [x] Multiple background fixes and code cleanups
+}}
+
+
### Tuesday 9/29/2026 - v3.24.1
{{taskList
##### calculuschild
@@ -169,7 +190,7 @@ Fixes issue [#3840](https://github.com/naturalcrit/homebrewery/issues/3840)
Fixes issue [#2037](https://github.com/naturalcrit/homebrewery/issues/2037)
-* [x] Remember editor theme across tabs
+* [x] Remember editor theme across tabs, and new {{openSans **SETTINGS :fas_gear:**}} tab with editor settings
Fixes issue [#4858](https://github.com/naturalcrit/homebrewery/issues/4858)
diff --git a/client/components/codeEditor/codeEditor.jsx b/client/components/codeEditor/codeEditor.jsx
index bda7e3b73..230a30e1f 100644
--- a/client/components/codeEditor/codeEditor.jsx
+++ b/client/components/codeEditor/codeEditor.jsx
@@ -156,6 +156,31 @@ const CodeEditor = forwardRef(
return folds;
};
+ const foldAll = (view = viewRef.current)=>{
+ if(!view) return false;
+
+ const doc = view.state.doc;
+ const pages = pageMap.current;
+
+ const effects = pages.map((start, i)=>{
+ const next = pages[i + 1] || doc.length;
+ const from = i ? doc.line(doc.lineAt(start).number + 1).from : 0;
+ const to = doc.line(doc.lineAt(next).number).from - 1;
+
+ return to > from ? foldEffect.of({ from, to }) : null;
+ }).filter(Boolean);
+
+ view.dispatch({ effects });
+ return true;
+ };
+
+ const unfoldAll = (view = viewRef.current)=>{
+ if(!view) return false;
+
+ view.dispatch(unfoldAllCmd(view));
+ return true;
+ };
+
const createExtensions = ({ onChange, language, editorTheme })=>{
const setEventListeners = EditorView.updateListener.of((update)=>{
if(update.docChanged) {
@@ -199,7 +224,7 @@ const CodeEditor = forwardRef(
//keyboard shortcut
keymap.of([...defaultKeymap, foldKeymap, ...searchKeymap]),
- generalKeymap,
+ generalKeymap(foldAll, unfoldAll),
...(tab === 'brewStyles' ? [cssKeymap] : [markdownKeymap]),
//multiple cursors and selections
@@ -404,29 +429,8 @@ const CodeEditor = forwardRef(
undone : redoDepth(view.state),
};
},
-
- foldAll : ()=>{
- const view = viewRef.current;
- if(!view) return;
-
- const doc = view.state.doc;
- const pages = pageMap.current;
-
- const effects = pages.map((start, i)=>{
- const next = pages[i + 1] || doc.length;
- const from = i ? doc.line(doc.lineAt(start).number + 1).from : 0;
- const to = doc.line(doc.lineAt(next).number).from - 1;
-
- return to > from ? foldEffect.of({ from, to }) : null;
- }).filter(Boolean);
-
- view.dispatch({ effects });
- },
- unfoldAll : ()=>{
- const view = viewRef.current;
- if(!view) return;
- view.dispatch(unfoldAllCmd(view));
- },
+ foldAll : ()=>foldAll(),
+ unfoldAll : ()=>unfoldAll(),
focus : ()=>viewRef.current.focus(),
}));
diff --git a/client/components/codeEditor/extensions/customKeyMaps.js b/client/components/codeEditor/extensions/customKeyMaps.js
index af7fa7131..e6adec71e 100644
--- a/client/components/codeEditor/extensions/customKeyMaps.js
+++ b/client/components/codeEditor/extensions/customKeyMaps.js
@@ -215,13 +215,15 @@ const newBreak = (type)=>(view)=>forEachSelection(view, (state, from, to, select
};
});
-export const generalKeymap = Prec.high(keymap.of([
+export const generalKeymap = (foldAll, unfoldAll) => Prec.high(keymap.of([
{ key: 'Tab', run: insertTab },
{ key: 'Shift-Tab', run: indentLess },
{ key: 'Mod-z', run: undo }, //it may be unnecessary
{ key: 'Mod-Shift-z', run: redo },
{ key: 'Mod-y', run: redo }, //user asked, so double keybind
{ key: 'Mod-d', run: deleteLine }, //annoyingly overrides "selectNextOccurrence" because users asked
+ { key: 'Mod-Shift-[', run: foldAll },
+ { key: 'Mod-Shift-]', run: unfoldAll },
]));
export const cssKeymap = Prec.highest(keymap.of([
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
index d713c1cc9..d5c4387e9 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
@@ -2,13 +2,14 @@
import '../uiEditor.less';
import React, { useState } from 'react';
import _ from 'lodash';
-import request from '../../utils/request-middleware.js';
-import Combobox from '@components/combobox.jsx';
-import TagInput from '../tagInput/tagInput.jsx';
+import request from '../../utils/request-middleware.js';
import Themes from '@themes/themes.json';
import validations from './validations.js';
+import Combobox from '@components/combobox.jsx';
+import TagInput from '../tagInput/tagInput.jsx';
+
import homebreweryThumbnail from '../../thumbnail.png';
const callIfExists = (val, fn, ...args)=>{
@@ -19,32 +20,25 @@ const callIfExists = (val, fn, ...args)=>{
const MetadataEditor = ({
metadata = {
- editId : null,
shareId : null,
+ editId : null,
title : '',
description : '',
- thumbnail : '',
tags : [],
- published : false,
- authors : [],
- renderer : 'legacy',
- theme : '5ePHB',
lang : 'en',
+ renderer : 'legacy',
+ authors : [],
+ published : false,
+ thumbnail : '',
+ theme : '5ePHB',
},
-
onChange = ()=>{},
reportError = ()=>{},
- ...props
+ userThemes = {},
+ themeBundle = {}
})=>{
const [showThumbnail, setShowThumbnail] = useState(true);
- const toggleThumbnailDisplay = ()=>{
- setShowThumbnail(!showThumbnail);
- };
- const renderThumbnail = ()=>{
- if(!showThumbnail) return;
- return ;
- };
const handleFieldChange = (name, e)=>{
// load validation rules, and check input value against them
const inputRules = validations[name] ?? [];
@@ -90,13 +84,11 @@ const MetadataEditor = ({
const handleTheme = (theme)=>{
metadata.renderer = theme.renderer;
metadata.theme = theme.path;
-
onChange(metadata, 'theme');
};
const handleThemeWritein = (e)=>{
const shareId = e.target.value.split('/').pop(); //Extract just the ID if a URL was pasted in
metadata.theme = shareId;
-
onChange(metadata, 'theme');
};
const handleLanguage = (languageCode)=>{
@@ -141,18 +133,6 @@ const MetadataEditor = ({
;
}
};
- const renderDelete = ()=>{
- if(!metadata.editId) return;
-
- return