refactor: reuse CodeMirror for CSS editors
This commit is contained in:
1 parent
e617a47395
commit
dc7a8579dc
12 files changed
+76
-331
No files matched your search
@@ -85,8 +85,11 @@ To do this, enter the board settings:
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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<typeof CustomWidgetCodeEditor>;
|
||||
type CodeEditorProps = Omit<SharedEditorProps, "messages">;
|
||||
|
||||
export function CodeEditor(props: CodeEditorProps) {
|
||||
const t = useI18n("customWidget.editor");
|
||||
const tCommon = useI18n("common");
|
||||
const messages = useMemo<SharedEditorProps["messages"]>(
|
||||
() => ({
|
||||
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 <CustomWidgetCodeEditor {...props} messages={messages} />;
|
||||
}
|
||||
export { CodeEditor } from "~/components/custom-widgets/code-editor";
|
||||
@@ -110,7 +110,7 @@ export const BrandingSettingsForm = ({ form }: BrandingSettingsFormProps) => {
|
||||
description={t("customCss.description")}
|
||||
actions={
|
||||
<WorkshopCssImportButton
|
||||
size="compact-sm"
|
||||
size="compact-xs"
|
||||
onImport={(css) => form.setFieldValue("branding.customCss", css)}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 (
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between" align="end">
|
||||
<div>
|
||||
<Input.Label htmlFor={id}>{label}</Input.Label>
|
||||
<Input.Description>{description}</Input.Description>
|
||||
</div>
|
||||
{actions}
|
||||
</Group>
|
||||
<Input.Wrapper error={error} inputWrapperOrder={["input", "error"]}>
|
||||
<div className={classes.root}>
|
||||
<Editor
|
||||
textareaId={id}
|
||||
onValueChange={onChange}
|
||||
value={value}
|
||||
highlight={(code) => highlight(code, languages.extend("css", {}), "css")}
|
||||
padding={10}
|
||||
className={classes.editor}
|
||||
/>
|
||||
</div>
|
||||
</Input.Wrapper>
|
||||
</Stack>
|
||||
<CodeEditor
|
||||
id={id}
|
||||
label={label}
|
||||
description={description}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error}
|
||||
language="css"
|
||||
height="250px"
|
||||
maxLength={16_384}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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<typeof CustomWidgetCodeEditor>;
|
||||
type CodeEditorProps = Omit<SharedEditorProps, "messages">;
|
||||
|
||||
export function CodeEditor(props: CodeEditorProps) {
|
||||
const t = useI18n("customWidget.editor");
|
||||
const tCommon = useI18n("common");
|
||||
const messages = useMemo<SharedEditorProps["messages"]>(
|
||||
() => ({
|
||||
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 <CustomWidgetCodeEditor {...props} messages={messages} />;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ export function CodeEditorToolbar({
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap={6}>
|
||||
{props.actions}
|
||||
{!props.readOnly && (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -57,4 +57,5 @@ export interface CustomWidgetCodeEditorProps {
|
||||
readOnly?: boolean;
|
||||
reference?: CustomWidgetSchemaReferenceData;
|
||||
height?: string;
|
||||
actions?: ReactNode;
|
||||
}
|
||||
Generated
-9
@@ -1238,9 +1238,6 @@ importers:
|
||||
posthog-js:
|
||||
specifier: 'catalog:'
|
||||
version: 1.399.2(preact-render-to-string@6.5.11)
|
||||
prismjs:
|
||||
specifier: 'catalog:'
|
||||
version: 1.30.0
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.7
|
||||
@@ -1253,9 +1250,6 @@ importers:
|
||||
react-shiki:
|
||||
specifier: 'catalog:'
|
||||
version: 0.11.0(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2)
|
||||
react-simple-code-editor:
|
||||
specifier: 'catalog:'
|
||||
version: 0.14.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
remark-breaks:
|
||||
specifier: 'catalog:'
|
||||
version: 4.0.0
|
||||
@@ -1299,9 +1293,6 @@ importers:
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 24.13.3
|
||||
'@types/prismjs':
|
||||
specifier: 'catalog:'
|
||||
version: 1.26.6
|
||||
'@types/react':
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.17
|
||||
|
||||
Reference in new issue
Block a user