diff --git a/helpers/frontend/views/frontend-form.njk b/helpers/frontend/views/frontend-form.njk
index 0d6d19d79..2292fafa0 100644
--- a/helpers/frontend/views/frontend-form.njk
+++ b/helpers/frontend/views/frontend-form.njk
@@ -65,6 +65,18 @@
hint: "Textarea hint text"
}) }}
+{{ textarea({
+ errorMessage: { text: "Richtext input error" } if errors,
+ name: "richtext",
+ label: "Richtext",
+ hint: "Richtext hint text",
+ field: {
+ attributes: {
+ editor: true
+ }
+ }
+}) }}
+
{{ textarea({
errorMessage: { text: "Readonly textarea input error" } if errors,
name: "textarea-readonly",
diff --git a/indiekit.config.js b/indiekit.config.js
index 76340aab5..652db334c 100644
--- a/indiekit.config.js
+++ b/indiekit.config.js
@@ -6,6 +6,7 @@ dotenv.config();
const config = {
application: {
_devMode: process.env.NODE_ENV === "development",
+ locale: "en-GB",
mongodbUrl: process.env.MONGO_URL,
...(process.env.RAILWAY_ENVIRONMENT && {
url: `https://${process.env.RAILWAY_STATIC_URL}`,
diff --git a/package-lock.json b/package-lock.json
index bd2cb4712..a8f9e0fb8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5875,6 +5875,14 @@
"@types/node": "*"
}
},
+ "node_modules/@types/codemirror": {
+ "version": "5.60.15",
+ "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.15.tgz",
+ "integrity": "sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA==",
+ "dependencies": {
+ "@types/tern": "*"
+ }
+ },
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
@@ -5977,6 +5985,11 @@
"@types/mdurl": "*"
}
},
+ "node_modules/@types/marked": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.2.tgz",
+ "integrity": "sha512-a79Yc3TOk6dGdituy8hmTTJXjOkZ7zsFYV10L337ttq/rec8lRMDBpV7fL3uLx6TgbFCa5DU/h8FmIBQPSbU0w=="
+ },
"node_modules/@types/mdurl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz",
@@ -6065,6 +6078,14 @@
"integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==",
"dev": true
},
+ "node_modules/@types/tern": {
+ "version": "0.23.9",
+ "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
+ "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
"node_modules/@types/web-bluetooth": {
"version": "0.0.20",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
@@ -7751,6 +7772,19 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/codemirror": {
+ "version": "5.65.16",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.16.tgz",
+ "integrity": "sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg=="
+ },
+ "node_modules/codemirror-spell-checker": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz",
+ "integrity": "sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==",
+ "dependencies": {
+ "typo-js": "*"
+ }
+ },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -9317,6 +9351,18 @@
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
+ "node_modules/easymde": {
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/easymde/-/easymde-2.18.0.tgz",
+ "integrity": "sha512-IxVVUxNWIoXLeqtBU4BLc+eS/ScYhT1Dcb6yF5Wchoj1iXAV+TIIDWx+NCaZhY7RcSHqDPKllbYq7nwGKILnoA==",
+ "dependencies": {
+ "@types/codemirror": "^5.60.4",
+ "@types/marked": "^4.0.7",
+ "codemirror": "^5.63.1",
+ "codemirror-spell-checker": "1.1.2",
+ "marked": "^4.1.0"
+ }
+ },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -14266,6 +14312,17 @@
"integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==",
"dev": true
},
+ "node_modules/marked": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
+ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/masto": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/masto/-/masto-6.7.1.tgz",
@@ -19944,6 +20001,11 @@
"node": ">=14.17"
}
},
+ "node_modules/typo-js": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/typo-js/-/typo-js-1.2.4.tgz",
+ "integrity": "sha512-Oy/k+tFle5NAA3J/yrrYGfvEnPVrDZ8s8/WCwjUE75k331QyKIsFss7byQ/PzBmXLY6h1moRnZbnaxWBe3I3CA=="
+ },
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
@@ -21055,6 +21117,7 @@
"@rollup/plugin-commonjs": "^25.0.3",
"@rollup/plugin-node-resolve": "^15.0.0",
"color": "^4.0.1",
+ "easymde": "^2.18.0",
"iso-639-1": "^3.0.0",
"lightningcss": "^1.22.0",
"lodash": "^4.17.21",
diff --git a/packages/endpoint-posts/includes/post-types/content-field.njk b/packages/endpoint-posts/includes/post-types/content-field.njk
index 7bc0fcb4b..8d67103fa 100644
--- a/packages/endpoint-posts/includes/post-types/content-field.njk
+++ b/packages/endpoint-posts/includes/post-types/content-field.njk
@@ -1,7 +1,17 @@
-{{ characterCount({
+{{ textarea({
name: "content",
value: properties.content.text or fieldData("content").value,
label: __("posts.form.content.label"),
optional: not field.required,
- errorMessage: fieldData("content").errorMessage
+ errorMessage: fieldData("content").errorMessage,
+ field: {
+ attributes: {
+ editor: true,
+ "editor-id": (properties.uid or ("new-" + postType)) + "-content",
+ "editor-height": "50vh" if field.required else "6rem",
+ "editor-locale": application.locale,
+ "editor-image-upload": "false" if postType == "note",
+ "editor-status": "false" if not field.required
+ }
+ }
}) }}
\ No newline at end of file
diff --git a/packages/endpoint-posts/includes/post-types/summary-field.njk b/packages/endpoint-posts/includes/post-types/summary-field.njk
index 08a8fa7b5..89221db5e 100644
--- a/packages/endpoint-posts/includes/post-types/summary-field.njk
+++ b/packages/endpoint-posts/includes/post-types/summary-field.njk
@@ -3,5 +3,15 @@
value: properties.summary,
label: __("posts.form.summary.label"),
optional: not field.required,
- rows: 1
+ rows: 1,
+ field: {
+ attributes: {
+ editor: true,
+ "editor-id": (properties.uid or ("new-" + postType)) + "-summary",
+ "editor-height": "60vh" if field.required else "2rem",
+ "editor-locale": application.locale,
+ "editor-status": "false",
+ "editor-toolbar": "false"
+ }
+ }
}) }}
\ No newline at end of file
diff --git a/packages/frontend/assets/mona-sans.woff2 b/packages/frontend/assets/mona-sans.woff2
deleted file mode 100644
index 8208a5000..000000000
Binary files a/packages/frontend/assets/mona-sans.woff2 and /dev/null differ
diff --git a/packages/frontend/components/textarea/index.js b/packages/frontend/components/textarea/index.js
index 3d9c2c8ad..d5c493eb1 100644
--- a/packages/frontend/components/textarea/index.js
+++ b/packages/frontend/components/textarea/index.js
@@ -1,31 +1,168 @@
-import { debounce } from "../../lib/utils/debounce.js";
+import EasyMDE from "easymde";
+
+const paths = {
+ bold: "M17 30c6.1 0 10-3 10-8 0-3.5-2.7-6.3-6.5-6.5V15c3-.4 5-3 5-6 0-4.5-3.5-7-9-7H5v28h12ZM12 7h2c2.5 0 4 1 4 3 0 1.5-1.5 3-4 3h-2V7Zm0 18v-7h2.3c3.1 0 4.7 1.1 4.7 3.4 0 2.5-1.4 3.6-4.8 3.6H12Z",
+ code: "m13.5 8.5-3-3L2 14C.5 15.5.5 16.5 2 18l8.5 8.5 3-3L6 16l7.5-7.5Zm5 0 3-3L30 14c1.5 1.5 1.5 2.5 0 4l-8.5 8.5-3-3L26 16l-7.5-7.5Z",
+ fullscreen:
+ "M4 20H0v10c0 1.1.9 2 2 2h10v-4H4v-8Zm-4-8h4V4h8V0H2a2 2 0 0 0-2 2v10Zm28 16h-8v4h10a2 2 0 0 0 2-2V20h-4v8ZM20 0v4h8v8h4V2a2 2 0 0 0-2-2H20Z",
+ heading: "M27 30h-6V18H11v12H5V2h6v11h10V2h6z",
+ italic: "m21.5 30 .8-4h-6l4-20h6l.7-4H10l-.7 4h6l-4 20h-6l-.8 4z",
+ link: "M14 8v4H8a4 4 0 1 0 0 8h6v4H8A8 8 0 1 1 8 8h6Zm10 0a8 8 0 1 1 0 16h-6v-4h6a4 4 0 1 0 0-8h-6V8h6Zm-2 6v4H10v-4h12Z",
+ "ordered-list":
+ "M10 2h22v4H10Zm0 12h22v4H10Zm0 12h22v4H10ZM0 0h4v8H2V2H0V0Zm5 12h1v2l-3 4h3v2H0v-2l3.3-4H0v-2h5ZM0 26v-2h6v8H0v-2h4v-1H2v-2h2v-1H0Z",
+ quote:
+ "M4 6h7a3 3 0 0 1 3 3v6.6a4 4 0 0 1-1 2.5L6.6 26h-3l3-8H4a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3Zm17 0h7a3 3 0 0 1 3 3v6.6a4 4 0 0 1-1 2.5L23.6 26h-3l3-8H21a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3Z",
+ "side-by-side":
+ "M16 6c7 0 13.5 4 16 10-2.5 6-9 10-16 10S2.5 22 0 16C2.5 10 9 6 16 6Zm0 3a7 7 0 1 0 0 14 7 7 0 0 0 0-14Zm0 3a4 4 0 1 1 0 8 4 4 0 0 1 0-8Z",
+ table:
+ "M32 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2C0 .9.9 0 2 0h28a2 2 0 0 1 2 2Zm-4 2H4v6h24V4ZM14 14H4v5h10v-5Zm0 9H4v5h10v-5Zm14-9H18v5h10v-5Zm0 9H18v5h10v-5Z",
+ undo: "M2 24h-.1A2 2 0 0 1 0 22V9h4v7.5A15 15 0 0 1 32 24h-4a11 11 0 0 0-21.2-4H15v4H2Z",
+ "unordered-list":
+ "M9 2h22v4H9Zm0 12h22v4H9Zm0 12h22v4H9ZM3 7a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm0 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm0 12a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z",
+ "upload-image":
+ "M2 0h28a2 2 0 0 1 2 2v28a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2C0 .9.9 0 2 0Zm26 24-9-14-7 11-3-4.8L4 24h24Z",
+};
+
+/**
+ * Get SVG icon
+ * @param {string} name - Icon name
+ * @returns {string} SVG
+ */
+const getButtonSvg = (name) => {
+ return ``;
+};
export const TextareaFieldComponent = class extends HTMLElement {
constructor() {
super();
- this.adjustHeight = this.adjustHeight.bind(this);
+ this.editor = this.getAttribute("editor");
+ this.editorId = this.getAttribute("editor-id");
+ this.editorHeight = this.getAttribute("editor-height");
+ this.editorImageUpload = this.getAttribute("editor-image-upload");
+ this.editorLocale = this.getAttribute("editor-locale");
+ this.editorStatus = this.getAttribute("editor-status");
+ this.editorToolbar = this.getAttribute("editor-toolbar");
+ this.$label = this.querySelector("label");
this.$textarea = this.querySelector("textarea");
}
connectedCallback() {
- const delay = 100;
+ if (this.editor !== "") {
+ return;
+ }
- this.$textarea.style.overflow = "hidden";
- this.onResize =
- delay > 0 ? debounce(this.adjustHeight, delay) : this.adjustHeight;
- this.adjustHeight();
+ const status =
+ this?.editorStatus === "false"
+ ? false
+ : [
+ ...(this.editorImageUpload === "false" ? [] : ["upload-image"]),
+ "words",
+ "characters",
+ "autosave",
+ ];
- this.$textarea.addEventListener("input", this.adjustHeight);
- window.addEventListener("resize", this.onResize);
- }
+ const toolbar =
+ this?.editorToolbar === "false"
+ ? false
+ : [
+ "bold",
+ "italic",
+ "heading",
+ "quote",
+ "ordered-list",
+ "unordered-list",
+ "table",
+ "code",
+ "link",
+ ...(this.editorImageUpload === "false" ? [] : ["upload-image"]),
+ "|",
+ "undo",
+ "side-by-side",
+ "fullscreen",
+ ];
+
+ const editor = new EasyMDE({
+ autoDownloadFontAwesome: false,
+ autosave: {
+ enabled: true,
+ uniqueId: this.editorId || this.$textarea.id,
+ timeFormat: { locale: this.editorLocale || "en" },
+ },
+ blockStyles: {
+ bold: "**",
+ italic: "_",
+ },
+ element: this.$textarea,
+ imageUploadFunction: this.uploadFile,
+ maxHeight: this.editorHeight,
+ previewClass: ["editor-preview", "s-flow"],
+ status,
+ // @ts-ignore
+ toolbar,
+ unorderedListStyle: "-",
+ });
- disconnectedCallback() {
- window.removeEventListener("resize", this.onResize);
+ // Restore label behaviour
+ /** @type {HTMLTextAreaElement} */
+ const $codeMirrorTextarea = this.querySelector(".CodeMirror textarea");
+ this.$label.addEventListener("click", () => {
+ $codeMirrorTextarea.focus();
+ });
+
+ // Update character count
+ /** @type {HTMLElement} */
+ const $characters = this.querySelector(".editor-statusbar .characters");
+ editor.codemirror.on("update", () => {
+ $characters.innerHTML = String(editor.value().length);
+ });
+
+ const $editorToolbar = this.querySelector(".editor-toolbar");
+ if ($editorToolbar) {
+ // Use custom SVG icons
+ const buttons = $editorToolbar.querySelectorAll("button");
+ for (const button of buttons) {
+ button.innerHTML = getButtonSvg(button.classList[0]);
+ }
+
+ // Get toolbar height to offset editor and preview in fullscreen mode
+ const resizeObserver = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ this.style.setProperty(
+ "--toolbar-height",
+ `${entry.contentRect.height}px`,
+ );
+ }
+ });
+
+ resizeObserver.observe($editorToolbar);
+ }
}
- adjustHeight() {
- this.$textarea.style.height = "auto";
- this.$textarea.style.height = `${this.$textarea.scrollHeight + 4}px`;
+ /**
+ * Upload file
+ * @param {object} file - File
+ * @param {Function} onSuccess - Success callback
+ * @param {Function} onError - Error callback
+ * @returns {Promise} - File URL or error message
+ */
+ async uploadFile(file, onSuccess, onError) {
+ const formData = new FormData();
+ formData.append("file", file);
+
+ try {
+ const endpointResponse = await fetch("http://localhost:3000/media", {
+ method: "POST",
+ body: formData,
+ });
+
+ return endpointResponse.ok
+ ? onSuccess(endpointResponse.headers.get("location"))
+ : onError(endpointResponse.statusText);
+ } catch (error) {
+ onError(error.message);
+ }
}
};
diff --git a/packages/frontend/components/textarea/styles.css b/packages/frontend/components/textarea/styles.css
index fa45af5de..8c8116031 100644
--- a/packages/frontend/components/textarea/styles.css
+++ b/packages/frontend/components/textarea/styles.css
@@ -17,6 +17,7 @@ textarea-field {
border-color: var(--color-on-background);
border-width: var(--input-border-width-focus);
inset-block-start: calc(var(--input-border-width-focus-offset) * -1);
+ margin-block-end: calc(var(--input-border-width-focus-offset) * -2);
padding-inline-start: calc(
var(--space-s) - var(--input-border-width-focus-offset)
);
diff --git a/packages/frontend/layouts/default.njk b/packages/frontend/layouts/default.njk
index b6b39de74..3118619b4 100644
--- a/packages/frontend/layouts/default.njk
+++ b/packages/frontend/layouts/default.njk
@@ -5,7 +5,6 @@
{% from "button/macro.njk" import button with context %}
{% from "card/macro.njk" import card with context %}
{% from "card-grid/macro.njk" import cardGrid with context %}
-{% from "character-count/macro.njk" import characterCount with context %}
{% from "checkboxes/macro.njk" import checkboxes with context %}
{% from "details/macro.njk" import details with context %}
{% from "error-summary/macro.njk" import errorSummary with context %}
@@ -49,7 +48,6 @@
-
diff --git a/packages/frontend/lib/utils/debounce.js b/packages/frontend/lib/utils/debounce.js
deleted file mode 100644
index 226c56870..000000000
--- a/packages/frontend/lib/utils/debounce.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Delay a function
- * @param {Function} callback - Function to delay
- * @param {number} delay - Delay, in milliseconds
- * @returns {Function} Debounced function
- * @see {@link https://www.joshwcomeau.com/snippets/javascript/debounce/}
- */
-export const debounce = (callback, delay) => {
- let timeoutId;
-
- return (...arguments_) => {
- window.clearTimeout(timeoutId);
-
- timeoutId = window.setTimeout(() => {
- callback(...arguments_);
- }, delay);
- };
-};
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 47693101f..942b8dea9 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -41,6 +41,7 @@
"@rollup/plugin-commonjs": "^25.0.3",
"@rollup/plugin-node-resolve": "^15.0.0",
"color": "^4.0.1",
+ "easymde": "^2.18.0",
"iso-639-1": "^3.0.0",
"lightningcss": "^1.22.0",
"lodash": "^4.17.21",
diff --git a/packages/frontend/scripts/app.js b/packages/frontend/scripts/app.js
index 62a1abe08..5d4fd551e 100644
--- a/packages/frontend/scripts/app.js
+++ b/packages/frontend/scripts/app.js
@@ -1,5 +1,4 @@
import { AddAnotherComponent } from "../components/add-another/index.js";
-import { CharacterCountComponent } from "../components/character-count/index.js";
import { CheckboxesFieldComponent } from "../components/checkboxes/index.js";
import { ErrorSummaryComponent } from "../components/error-summary/index.js";
import { EventDurationComponent } from "../components/event-duration/index.js";
@@ -12,7 +11,6 @@ import { TagInputFieldComponent } from "../components/tag-input/index.js";
import { TextareaFieldComponent } from "../components/textarea/index.js";
customElements.define("add-another", AddAnotherComponent);
-customElements.define("character-count", CharacterCountComponent);
customElements.define("checkboxes-field", CheckboxesFieldComponent);
customElements.define("error-summary", ErrorSummaryComponent);
customElements.define("file-input-field", FileInputFieldController);
diff --git a/packages/frontend/styles/app.css b/packages/frontend/styles/app.css
index aa75f57af..5d00f4fd9 100644
--- a/packages/frontend/styles/app.css
+++ b/packages/frontend/styles/app.css
@@ -1,6 +1,6 @@
/* Config */
@import url("./config/custom-properties.css");
-@import url("./config/font-face.css");
+/* @import url("./config/font-face.css"); */
/* Base */
@import url("./base/embedded.css");
@@ -65,4 +65,6 @@
@import url("./utilities/visually-hidden.css");
/* Vendor */
+@import url("./vendor/codemirror.css");
+@import url("./vendor/easy-markdown-editor.css");
@import url("./vendor/markdown-it-prism.css");
diff --git a/packages/frontend/styles/base/sections.css b/packages/frontend/styles/base/sections.css
index 4df1c8cf8..7abe9687a 100644
--- a/packages/frontend/styles/base/sections.css
+++ b/packages/frontend/styles/base/sections.css
@@ -45,7 +45,7 @@ legend {
text-rendering: optimizelegibility;
}
-[tabindex="-1"]:focus,
+[tabindex="-1"]:focus:not(button),
:focus-visible {
border-radius: var(--border-radius-small);
box-shadow: 0 0 0 var(--focus-width) var(--color-focus);
diff --git a/packages/frontend/styles/config/custom-properties.css b/packages/frontend/styles/config/custom-properties.css
index 326cdd0dd..949fdce6c 100644
--- a/packages/frontend/styles/config/custom-properties.css
+++ b/packages/frontend/styles/config/custom-properties.css
@@ -64,6 +64,15 @@ html {
--color-selection: hsl(var(--tint-yellow) 50% / 30%);
--color-shadow: hsl(var(--tint-neutral) 10% / 0.2);
+ /* Syntax highlighting */
+ --color-token-comment: var(--color-on-offset);
+ --color-token-operator: var(--color-on-background);
+ --color-token-function: #900;
+ --color-token-keyword: #069;
+ --color-token-selector: #009;
+ --color-token-string: #c06;
+ --color-token-variable: #399;
+
/* Fluid type scale */
/* clamp(min=320, preferred, max=960) */
--font-size-xs: clamp(0.75rem, 0.6875rem + 0.3125vw, 0.875rem); /* 12 → 14 */
@@ -75,7 +84,7 @@ html {
/* Typography */
--font-family-system: system-ui;
- --font-family-sans: mona-sans, sans-serif;
+ --font-family-sans: system-ui, sans-serif;
--font-family-monospace: ui-monospace, sfmono-regular, sf mono, menlo,
consolas, liberation mono, monospace;
@@ -152,6 +161,13 @@ html {
--color-error: var(--color-red80);
--color-error-variant: var(--color-red90);
--color-on-error: var(--color-neutral10);
+
+ /* Syntax highlighting */
+ --color-token-function: #f99;
+ --color-token-keyword: #0cf;
+ --color-token-selector: #99f;
+ --color-token-string: #f6f;
+ --color-token-variable: #9ff;
}
}
@@ -170,4 +186,11 @@ html {
--color-error: var(--color-red80);
--color-error-variant: var(--color-red90);
--color-on-error: var(--color-neutral10);
+
+ /* Syntax highlighting */
+ --color-token-function: #f99;
+ --color-token-keyword: #0cf;
+ --color-token-selector: #99f;
+ --color-token-string: #f6f;
+ --color-token-variable: #9ff;
}
diff --git a/packages/frontend/styles/config/font-face.css b/packages/frontend/styles/config/font-face.css
deleted file mode 100644
index 025295017..000000000
--- a/packages/frontend/styles/config/font-face.css
+++ /dev/null
@@ -1,9 +0,0 @@
-@font-face {
- font-display: swap;
- font-family: mona-sans;
- font-stretch: 75% 125%;
- font-weight: 200 900;
- src:
- url("mona-sans.woff2") format("woff2 supports variations"),
- url("mona-sans.woff2") format("woff2-variations");
-}
diff --git a/packages/frontend/styles/vendor/codemirror.css b/packages/frontend/styles/vendor/codemirror.css
new file mode 100644
index 000000000..f0e6d7145
--- /dev/null
+++ b/packages/frontend/styles/vendor/codemirror.css
@@ -0,0 +1,107 @@
+@import url("../../../../node_modules/codemirror/lib/codemirror.css");
+
+.CodeMirror {
+ --fieldset-flow-space: 0;
+
+ background: var(--color-background);
+ block-size: auto;
+ border: var(--input-border-width) solid transparent;
+ border-radius: var(--border-radius-small);
+ color: var(--color-on-backround);
+ font-family: var(--font-family-monospace);
+ margin-block-start: 0;
+ padding: var(--space-s);
+
+ :focus-visible {
+ box-shadow: none;
+ }
+
+ .cm-attribute {
+ color: var(--color-token-keyword);
+ }
+
+ .cm-comment {
+ color: var(--color-token-comment);
+ }
+
+ .cm-string {
+ color: var(--color-token-string);
+ }
+
+ .cm-tag {
+ color: var(--color-token-selector);
+ }
+
+ .cm-header-1 {
+ font-size: var(--font-size-2xl);
+ }
+
+ .cm-header-2 {
+ font-size: var(--font-size-xl);
+ }
+
+ .cm-header-3 {
+ font-size: var(--font-size-l);
+ }
+
+ .cm-header-4 {
+ font-size: var(--font-size-m);
+ }
+
+ .cm-header-5 {
+ font-size: var(--font-size-s);
+ }
+
+ .cm-header-6 {
+ font-size: var(--font-size-xs);
+ }
+
+ .cm-link {
+ color: var(--anchor-color);
+ }
+
+ .cm-quote {
+ color: var(--color-on-offset);
+ font-style: italic;
+ }
+
+ .cm-url {
+ color: var(--anchor-color-hover);
+ }
+}
+
+.CodeMirror-focused {
+ &:not(.CodeMirror-fullscreen) {
+ border-color: var(--color-on-background);
+ border-width: var(--input-border-width-focus);
+ margin-inline: calc(var(--input-border-width-focus) * -1);
+ outline: var(--input-border-width-focus) solid var(--color-focus);
+ padding-block: calc(
+ var(--space-s) - var(--input-border-width-focus-offset)
+ );
+ padding-inline: calc(
+ var(--space-s) + var(--input-border-width-focus-offset)
+ );
+ }
+
+ .CodeMirror-selected {
+ background: var(--color-selection);
+ }
+}
+
+.CodeMirror-fullscreen {
+ block-size: auto;
+ inset: 0;
+ inset-block-start: var(--toolbar-height);
+ position: fixed;
+ z-index: 8;
+}
+
+.CodeMirror-placeholder {
+ color: var(--color-on-background);
+ opacity: 0.5;
+}
+
+.CodeMirror-sided {
+ inline-size: 50%;
+}
diff --git a/packages/frontend/styles/vendor/easy-markdown-editor.css b/packages/frontend/styles/vendor/easy-markdown-editor.css
new file mode 100644
index 000000000..c2f1c21b3
--- /dev/null
+++ b/packages/frontend/styles/vendor/easy-markdown-editor.css
@@ -0,0 +1,151 @@
+.EasyMDEContainer {
+ --toolbar-button-size: 44px;
+ --toolbar-padding: var(--space-2xs);
+
+ background-color: var(--color-background);
+ border: var(--input-border-width) solid var(--color-outline-variant);
+ border-radius: var(--border-radius-small);
+}
+
+.editor-statusbar,
+.editor-toolbar {
+ --fieldset-flow-space: 0;
+
+ background-color: var(--color-offset);
+ border: var(--border-width-thickest) solid var(--color-background);
+ border-radius: var(--border-radius-small);
+ color: var(--color-on-offset);
+ display: flex;
+ flex-wrap: wrap;
+ -webkit-user-select: none;
+ user-select: none;
+}
+
+.editor-statusbar {
+ font: var(--font-caption);
+ gap: var(--space-xs) var(--space-l);
+ justify-content: space-between;
+ margin-block-start: 0;
+ padding-block: var(--space-s);
+ padding-inline: var(--space-m);
+
+ & > * {
+ font-variant-numeric: tabular-nums;
+ min-inline-size: max-content;
+ }
+
+ & .autosave {
+ font-variant-numeric: normal;
+ text-align: end;
+ }
+
+ & .characters {
+ margin-inline-end: auto;
+ }
+
+ & .characters::after {
+ content: " characters";
+ }
+
+ & .words::after {
+ content: " words";
+ }
+
+ & .upload-image {
+ min-inline-size: 100%;
+ }
+}
+
+.editor-toolbar {
+ display: grid;
+ gap: var(--toolbar-padding);
+ grid-template-columns: repeat(
+ auto-fit,
+ minmax(var(--toolbar-button-size), 1fr)
+ );
+ padding: var(--toolbar-padding);
+ place-items: center;
+ position: relative;
+
+ &.fullscreen {
+ border: 0;
+ border-radius: 0;
+ inset-block-start: 0;
+ inset-inline: 0;
+ position: fixed;
+ z-index: 10;
+ }
+
+ & button {
+ background: none;
+ block-size: var(--toolbar-button-size);
+ border-radius: var(--border-radius-small);
+ display: flex;
+ inline-size: 100%;
+ min-inline-size: var(--toolbar-button-size);
+ place-content: center;
+
+ & svg {
+ place-self: center;
+ }
+
+ &:hover,
+ &.active {
+ background: var(--color-offset-variant);
+ }
+
+ &:active {
+ background: var(--color-offset-variant-darker);
+ }
+ }
+
+ & .separator {
+ border-left: var(--border-width-thin) solid var(--color-outline);
+ color: transparent;
+ inline-size: 0;
+
+ @media (width < 48rem) {
+ display: none;
+ }
+ }
+
+ &.disabled-for-preview button:not(.no-disable) {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+}
+
+.editor-preview {
+ --fieldset-flow-space: 1em;
+
+ background-color: var(--color-offset);
+ font: var(--font-body);
+ margin-block-start: 0;
+ padding: var(--space-m);
+
+ &:not(.editor-preview-active):not(.editor-preview-active-side) {
+ display: none;
+ }
+}
+
+.editor-preview-full {
+ inset-block: 0;
+ inset-inline: 2px;
+ overflow: auto;
+ position: absolute;
+}
+
+.editor-preview-side {
+ border: var(--border-width-thickest) solid var(--color-background);
+ inline-size: 50%;
+ inset-block: 0;
+ inset-block-start: calc(
+ var(--toolbar-height) + calc(var(--toolbar-padding) * 2)
+ );
+ inset-inline-end: 0;
+ overflow: auto;
+ padding: var(--space-l);
+ position: fixed;
+ word-wrap: break-word;
+ z-index: 9;
+}
diff --git a/packages/frontend/styles/vendor/markdown-it-prism.css b/packages/frontend/styles/vendor/markdown-it-prism.css
index fdbc36559..37421db7e 100644
--- a/packages/frontend/styles/vendor/markdown-it-prism.css
+++ b/packages/frontend/styles/vendor/markdown-it-prism.css
@@ -1,31 +1,3 @@
-:root {
- --color-token-comment: var(--color-on-offset);
- --color-token-operator: var(--color-on-background);
- --color-token-function: #900;
- --color-token-keyword: #069;
- --color-token-selector: #009;
- --color-token-string: #c06;
- --color-token-variable: #399;
-}
-
-@media (prefers-color-scheme: dark) {
- :root:not([data-color-scheme]) {
- --color-token-function: #f99;
- --color-token-keyword: #0cf;
- --color-token-selector: #99f;
- --color-token-string: #f6f;
- --color-token-variable: #9ff;
- }
-}
-
-[data-color-scheme="dark"] {
- --color-token-function: #f99;
- --color-token-keyword: #0cf;
- --color-token-selector: #99f;
- --color-token-string: #f6f;
- --color-token-variable: #9ff;
-}
-
.token.namespace {
opacity: 0.7;
}