diff --git a/apps/docs/docs/advanced/styling/index.mdx b/apps/docs/docs/advanced/styling/index.mdx index ed2faac99..705a3eccd 100644 --- a/apps/docs/docs/advanced/styling/index.mdx +++ b/apps/docs/docs/advanced/styling/index.mdx @@ -85,8 +85,11 @@ To do this, enter the board settings: ![](./img/enter-board-settings.png) -Scroll to the **Custom CSS** section on the board settings page. It is part of the unified settings form; enter your CSS -there and save the board settings to apply it to that board. +Administrators can add instance-wide CSS under **Management → Settings → Branding → Custom CSS**. Enter CSS directly +or import a community theme from the Workshop, then save the server settings to apply it to every page. + +For board-specific styling, scroll to the **Custom CSS** section on the board settings page. Enter your CSS there and +save the board settings to apply it to that board. Board CSS is loaded after the global stylesheet and can override it. ![](./img/custom-css-setting.png) diff --git a/apps/docs/docs/management/settings/index.mdx b/apps/docs/docs/management/settings/index.mdx index f45fabdc5..e531d04f2 100644 --- a/apps/docs/docs/management/settings/index.mdx +++ b/apps/docs/docs/management/settings/index.mdx @@ -142,6 +142,10 @@ Branding settings let administrators adapt Homarr to the identity and visual sty Choose the primary and secondary colors used by the theme. **Lock primary color** prevents users from overriding the configured primary color. **Default corner style** controls the global radius used by cards, controls, and other interface elements. +### Global custom CSS + +The branding settings include a CSS editor for styles that should apply across the entire instance. You can write CSS directly or import a community theme from the Workshop. CSS configured on an individual board takes precedence over the global stylesheet. + ### Sign-in page You can set a greeting and a background image for authentication pages. The overlay slider adjusts the contrast between the background and the sign-in content. The visibility controls independently show or hide the application name, logo, and greeting. diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index f5c4794b7..3a8feec1d 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -106,12 +106,10 @@ "next": "catalog:", "postcss-preset-mantine": "catalog:", "posthog-js": "catalog:", - "prismjs": "catalog:", "react": "catalog:", "react-dom": "catalog:", "react-error-boundary": "catalog:", "react-shiki": "catalog:", - "react-simple-code-editor": "catalog:", "remark-breaks": "catalog:", "remark-directive": "catalog:", "remark-gfm": "catalog:", @@ -128,7 +126,6 @@ "@types/better-sqlite3": "catalog:", "@types/chroma-js": "catalog:", "@types/node": "catalog:", - "@types/prismjs": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", "concurrently": "catalog:", diff --git a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_code-editor.tsx b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_code-editor.tsx index b8c6630f3..bc8c28a0b 100644 --- a/apps/nextjs/src/app/[locale]/manage/custom-widgets/_code-editor.tsx +++ b/apps/nextjs/src/app/[locale]/manage/custom-widgets/_code-editor.tsx @@ -1,50 +1 @@ -"use client"; - -import { useMemo } from "react"; -import type { ComponentProps } from "react"; - -import { CustomWidgetCodeEditor } from "@homarr/custom-widgets/workbench"; -import { invariantTechnicalLabels } from "@homarr/definitions"; -import { useI18n } from "@homarr/translation/client"; - -type SharedEditorProps = ComponentProps; -type CodeEditorProps = Omit; - -export function CodeEditor(props: CodeEditorProps) { - const t = useI18n("customWidget.editor"); - const tCommon = useI18n("common"); - const messages = useMemo( - () => ({ - languageJsx: invariantTechnicalLabels.jsx, - languageJson: invariantTechnicalLabels.json, - undo: tCommon("action.undo"), - redo: t("action.redo"), - components: t("action.components"), - componentSearch: t("componentReference.search"), - componentEmpty: t("componentReference.empty"), - componentCount: (count) => t("componentReference.count", { count }), - insertStarter: t("action.insertStarter"), - format: t("action.format"), - copy: tCommon("action.copy"), - copied: t("action.copied"), - schema: t("action.schema"), - schemaTab: invariantTechnicalLabels.jsonSchema, - minimalTab: t("reference.minimal"), - fullTab: t("reference.full"), - errors: (count) => t("status.errors", { count }), - warnings: (count) => t("status.warnings", { count }), - ready: t("status.ready"), - position: (cursor) => t("status.position", cursor), - characters: (count, limit) => - limit ? t("status.charactersWithLimit", { count, limit }) : t("status.characters", { count }), - diagnosticsTitle: t("diagnostics.title"), - diagnostic: (diagnostic) => - `${diagnostic.line ? `${t("diagnostics.line", { line: diagnostic.line })}: ` : ""}${t( - `diagnostics.${diagnostic.code}` as never, - { value: diagnostic.value ?? "" } as never, - )}`, - }), - [t, tCommon], - ); - return ; -} +export { CodeEditor } from "~/components/custom-widgets/code-editor"; diff --git a/apps/nextjs/src/app/[locale]/manage/settings/_components/branding-settings-form.tsx b/apps/nextjs/src/app/[locale]/manage/settings/_components/branding-settings-form.tsx index f9b75c9d1..e1b196f5b 100644 --- a/apps/nextjs/src/app/[locale]/manage/settings/_components/branding-settings-form.tsx +++ b/apps/nextjs/src/app/[locale]/manage/settings/_components/branding-settings-form.tsx @@ -110,7 +110,7 @@ export const BrandingSettingsForm = ({ form }: BrandingSettingsFormProps) => { description={t("customCss.description")} actions={ form.setFieldValue("branding.customCss", css)} /> } diff --git a/apps/nextjs/src/components/custom-css/custom-css-editor.module.css b/apps/nextjs/src/components/custom-css/custom-css-editor.module.css deleted file mode 100644 index f098b0fa1..000000000 --- a/apps/nextjs/src/components/custom-css/custom-css-editor.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.root { - overflow: auto; - margin-top: 4px; - border: 1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4)); - border-radius: var(--mantine-radius-sm); -} - -.editor { - min-height: 15.625rem; - background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); - font-family: "Fira code", "Fira Mono", monospace; - font-size: var(--mantine-font-size-xs); -} diff --git a/apps/nextjs/src/components/custom-css/custom-css-editor.tsx b/apps/nextjs/src/components/custom-css/custom-css-editor.tsx index 9f986f480..d46db8d74 100644 --- a/apps/nextjs/src/components/custom-css/custom-css-editor.tsx +++ b/apps/nextjs/src/components/custom-css/custom-css-editor.tsx @@ -1,12 +1,6 @@ "use client"; -import { Group, Input, Stack } from "@mantine/core"; -import { highlight, languages } from "prismjs"; -import Editor from "react-simple-code-editor"; - -import "~/styles/prismjs.scss"; - -import classes from "./custom-css-editor.module.css"; +import { CodeEditor } from "~/components/custom-widgets/code-editor"; interface CustomCssEditorProps { id: string; @@ -28,26 +22,17 @@ export const CustomCssEditor = ({ actions, }: CustomCssEditorProps) => { return ( - - -
- {label} - {description} -
- {actions} -
- -
- highlight(code, languages.extend("css", {}), "css")} - padding={10} - className={classes.editor} - /> -
-
-
+ ); }; diff --git a/apps/nextjs/src/components/custom-widgets/code-editor.tsx b/apps/nextjs/src/components/custom-widgets/code-editor.tsx new file mode 100644 index 000000000..b8c6630f3 --- /dev/null +++ b/apps/nextjs/src/components/custom-widgets/code-editor.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { useMemo } from "react"; +import type { ComponentProps } from "react"; + +import { CustomWidgetCodeEditor } from "@homarr/custom-widgets/workbench"; +import { invariantTechnicalLabels } from "@homarr/definitions"; +import { useI18n } from "@homarr/translation/client"; + +type SharedEditorProps = ComponentProps; +type CodeEditorProps = Omit; + +export function CodeEditor(props: CodeEditorProps) { + const t = useI18n("customWidget.editor"); + const tCommon = useI18n("common"); + const messages = useMemo( + () => ({ + languageJsx: invariantTechnicalLabels.jsx, + languageJson: invariantTechnicalLabels.json, + undo: tCommon("action.undo"), + redo: t("action.redo"), + components: t("action.components"), + componentSearch: t("componentReference.search"), + componentEmpty: t("componentReference.empty"), + componentCount: (count) => t("componentReference.count", { count }), + insertStarter: t("action.insertStarter"), + format: t("action.format"), + copy: tCommon("action.copy"), + copied: t("action.copied"), + schema: t("action.schema"), + schemaTab: invariantTechnicalLabels.jsonSchema, + minimalTab: t("reference.minimal"), + fullTab: t("reference.full"), + errors: (count) => t("status.errors", { count }), + warnings: (count) => t("status.warnings", { count }), + ready: t("status.ready"), + position: (cursor) => t("status.position", cursor), + characters: (count, limit) => + limit ? t("status.charactersWithLimit", { count, limit }) : t("status.characters", { count }), + diagnosticsTitle: t("diagnostics.title"), + diagnostic: (diagnostic) => + `${diagnostic.line ? `${t("diagnostics.line", { line: diagnostic.line })}: ` : ""}${t( + `diagnostics.${diagnostic.code}` as never, + { value: diagnostic.value ?? "" } as never, + )}`, + }), + [t, tCommon], + ); + return ; +} diff --git a/apps/nextjs/src/styles/prismjs.scss b/apps/nextjs/src/styles/prismjs.scss deleted file mode 100644 index d9f1d6327..000000000 --- a/apps/nextjs/src/styles/prismjs.scss +++ /dev/null @@ -1,225 +0,0 @@ -[data-mantine-color-scheme="light"] { - code[class*="language-"], - pre[class*="language-"] { - color: #000; - background: 0 0; - text-shadow: 0 1px #fff; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; - font-size: 1em; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; - } - code[class*="language-"] ::-moz-selection, - code[class*="language-"]::-moz-selection, - pre[class*="language-"] ::-moz-selection, - pre[class*="language-"]::-moz-selection { - text-shadow: none; - background: #b3d4fc; - } - code[class*="language-"] ::selection, - code[class*="language-"]::selection, - pre[class*="language-"] ::selection, - pre[class*="language-"]::selection { - text-shadow: none; - background: #b3d4fc; - } - @media print { - code[class*="language-"], - pre[class*="language-"] { - text-shadow: none; - } - } - pre[class*="language-"] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - } - :not(pre) > code[class*="language-"], - pre[class*="language-"] { - background: #f5f2f0; - } - :not(pre) > code[class*="language-"] { - padding: 0.1em; - border-radius: 0.3em; - white-space: normal; - } - .token.cdata, - .token.comment, - .token.doctype, - .token.prolog { - color: #708090; - } - .token.punctuation { - color: #999; - } - .token.namespace { - opacity: 0.7; - } - .token.boolean, - .token.constant, - .token.deleted, - .token.number, - .token.property, - .token.symbol, - .token.tag { - color: #905; - } - .token.attr-name, - .token.builtin, - .token.char, - .token.inserted, - .token.selector, - .token.string { - color: #690; - } - .language-css .token.string, - .style .token.string, - .token.entity, - .token.operator, - .token.url { - color: #9a6e3a; - background: hsla(0, 0%, 100%, 0.5); - } - .token.atrule, - .token.attr-value, - .token.keyword { - color: #07a; - } - .token.class-name, - .token.function { - color: #dd4a68; - } - .token.important, - .token.regex, - .token.variable { - color: #e90; - } - .token.bold, - .token.important { - font-weight: 700; - } - .token.italic { - font-style: italic; - } - .token.entity { - cursor: help; - } -} - -[data-mantine-color-scheme="dark"] { - code[class*="language-"], - pre[class*="language-"] { - color: #fff; - background: 0 0; - text-shadow: 0 -0.1em 0.2em #000; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; - font-size: 1em; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; - } - @media print { - code[class*="language-"], - pre[class*="language-"] { - text-shadow: none; - } - } - :not(pre) > code[class*="language-"], - pre[class*="language-"] { - background: #4c3f33; - } - pre[class*="language-"] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - border: 0.3em solid #7a6651; - border-radius: 0.5em; - box-shadow: 1px 1px 0.5em #000 inset; - } - :not(pre) > code[class*="language-"] { - padding: 0.15em 0.2em 0.05em; - border-radius: 0.3em; - border: 0.13em solid #7a6651; - box-shadow: 1px 1px 0.3em -0.1em #000 inset; - white-space: normal; - } - .token.cdata, - .token.comment, - .token.doctype, - .token.prolog { - color: #997f66; - } - .token.punctuation { - opacity: 0.7; - } - .token.namespace { - opacity: 0.7; - } - .token.boolean, - .token.constant, - .token.number, - .token.property, - .token.symbol, - .token.tag { - color: #d1939e; - } - .token.attr-name, - .token.builtin, - .token.char, - .token.inserted, - .token.selector, - .token.string { - color: #bce051; - } - .language-css .token.string, - .style .token.string, - .token.entity, - .token.operator, - .token.url, - .token.variable { - color: #f4b73d; - } - .token.atrule, - .token.attr-value, - .token.keyword { - color: #d1939e; - } - .token.important, - .token.regex { - color: #e90; - } - .token.bold, - .token.important { - font-weight: 700; - } - .token.italic { - font-style: italic; - } - .token.entity { - cursor: help; - } - .token.deleted { - color: red; - } -} diff --git a/packages/custom-widgets/src/workbench/code-editor-toolbar.tsx b/packages/custom-widgets/src/workbench/code-editor-toolbar.tsx index 3b3f246c0..010bf5eee 100644 --- a/packages/custom-widgets/src/workbench/code-editor-toolbar.tsx +++ b/packages/custom-widgets/src/workbench/code-editor-toolbar.tsx @@ -54,6 +54,7 @@ export function CodeEditorToolbar({ + {props.actions} {!props.readOnly && ( <>