feat: add global server custom CSS (#6753)

* feat: add global server custom CSS

* refactor: reuse CodeMirror for CSS editors
This commit is contained in:
Thomas authored and GitHub committed 2026-08-31 14:26:58 +02:00
1 parent ee30aa13cf
commit f57a660088
18 files changed
+152 -359

No files matched your search

@@ -0,0 +1,38 @@
"use client";
import { CodeEditor } from "~/components/custom-widgets/code-editor";
interface CustomCssEditorProps {
id: string;
label: string;
description: string;
value?: string;
onChange: (value: string) => void;
error?: React.ReactNode;
actions?: React.ReactNode;
}
export const CustomCssEditor = ({
id,
label,
description,
value = "",
onChange,
error,
actions,
}: CustomCssEditorProps) => {
return (
<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,6 +1,7 @@
"use client";
import { useState } from "react";
import type { ButtonProps } from "@mantine/core";
import { ActionIcon, Button, Group, Loader, Modal, Pagination, Stack, Text, TextInput, Tooltip } from "@mantine/core";
import { useDebouncedValue, useDisclosure } from "@mantine/hooks";
import { IconDownload, IconExternalLink, IconSearch } from "@tabler/icons-react";
@@ -21,15 +22,27 @@ import { WorkshopVoteControl } from "./workshop-vote-control";
/**
* Picks a community CSS theme in a single surface: browse, confirm, done.
* The imported rules land in the editor below and are only persisted when the
* board settings themselves are saved, so there is nothing to preview twice.
* settings themselves are saved, so there is nothing to preview twice.
*/
export function WorkshopCssImportButton({ onImport }: { onImport(css: string): void }) {
export function WorkshopCssImportButton({
onImport,
size,
}: {
onImport(css: string): void;
size?: ButtonProps["size"];
}) {
const t = useI18n("workshop");
const [opened, controls] = useDisclosure(false);
return (
<>
<Button type="button" variant="light" leftSection={<IconDownload size={16} />} onClick={controls.open}>
<Button
type="button"
size={size}
variant="light"
leftSection={<IconDownload size={16} />}
onClick={controls.open}
>
{t("importCss")}
</Button>
<Modal opened={opened} onClose={controls.close} title={t("importCss")} size={modalSizeSelect} radius="md">