feat: add global server custom CSS (#6753)
* feat: add global server custom CSS * refactor: reuse CodeMirror for CSS editors
This commit is contained in:
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">
|
||||
|
||||
Reference in new issue
Block a user