Compare commits

..
Author SHA1 Message Date
ajnart dc7a8579dc refactor: reuse CodeMirror for CSS editors 2026-08-31 14:01:43 +02:00
ajnart e617a47395 feat: add global server custom CSS 2026-08-31 13:47:37 +02:00
Thomas ee30aa13cf fix(workshop): clamp submission titles (#6752) 2026-08-31 12:18:43 +02:00
Bergasha feea3512d0 fix(board): fix container void/overflow gap across scrollable and collapse states (#6750)
A container's outer grid cell and its own inner content grid are sized by
two independent calculations that only agree when the container is
scrollable and expanded. When collapsed, or when non-scrollable content
needs more rows than declared, the inner grid keeps rendering at its old/
content-driven size while the outer cell shrinks or stays fixed, leaving
an invisible-but-space-occupying box that inflates the board's scroll
height into a visible empty gap below the container.

Pass the outer instance's collapsed row count down as a
viewportRowCountOverride so the inner grid matches it exactly when
collapsed, and grow the outer placement's height to the already-computed
content minimum (getContainerMinimumSizes) for non-scrollable containers
so it stays in sync with the inner grid's own growth.
2026-08-31 15:51:17 +10:00
Thomas 5e23659d99 feat(assistant): optimize MCP and custom widget authoring (#6743)
* feat(assistant): optimize MCP and custom widget authoring

* fix(assistant): isolate server-safe JSX analyzer

* refactor(assistant): generate MCP tool staging

* fix(assistant): preserve widget placement follow-up

* fix(ci): allow fast gate to finish

* fix(custom-widgets): address review findings

* fix(custom-widgets): split preview evidence module

* style: format assistant custom widget files

* fix(custom-widgets): sync formatted skill schema
2026-08-30 23:43:29 +02:00
21 changed files with 170 additions and 370 deletions

No files matched your search

+5 -2
View File
@@ -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)
@@ -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.
@@ -359,7 +359,7 @@ const SubmissionCard = ({ submission, backend, userVote, onVote }: SubmissionCar
<CardHeader className="flex flex-col gap-2 sm:grid">
<div className="flex items-center gap-2">
<a href={`/workshop/${submission.id}/`} className="min-w-0 hover:underline" title={submission.title}>
<CardTitle className="truncate">{submission.title}</CardTitle>
<CardTitle className="line-clamp-2">{submission.title}</CardTitle>
</a>
{!hasScreenshots && (
<Badge variant="secondary" className="shrink-0 gap-1.5 px-2">
-3
View File
@@ -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,21 +1,16 @@
"use client";
import { Alert, Input } from "@mantine/core";
import { highlight, languages } from "prismjs";
import Editor from "react-simple-code-editor";
import "~/styles/prismjs.scss";
import { Alert } from "@mantine/core";
import { IconInfoCircle } from "@tabler/icons-react";
import { useSession } from "@homarr/auth/client";
import type { UseFormReturnType } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { CustomCssEditor } from "~/components/custom-css/custom-css-editor";
import { SectionCard } from "~/components/manage/section-card";
import { WorkshopCssImportButton } from "~/components/workshop/workshop-css-import-button";
import type { FormValues } from "./_settings-form";
import classes from "./customcss.module.css";
interface Props {
form: UseFormReturnType<FormValues>;
@@ -29,7 +24,12 @@ export const CustomCssSettingsContent = ({ form }: Props) => {
return (
<SectionCard title={t("setting.section.customCss.title")}>
<CustomCssInput {...form.getInputProps("customCss")} />
<CustomCssEditor
id="board-custom-css"
label={customCssT("label")}
description={customCssT("description")}
{...form.getInputProps("customCss")}
/>
<Alert variant="light" color="cyan" title={customCssT("customClassesAlert.title")} icon={<IconInfoCircle />}>
{customCssT("customClassesAlert.description")}
@@ -38,40 +38,3 @@ export const CustomCssSettingsContent = ({ form }: Props) => {
</SectionCard>
);
};
interface CustomCssInputProps {
value?: string;
onChange: (value: string) => void;
error?: React.ReactNode;
}
const CustomCssInput = ({ value, onChange, error }: CustomCssInputProps) => {
const customCssT = useI18n("board.field.customCss");
return (
<Input.Wrapper
label={customCssT("label")}
labelProps={{
htmlFor: "custom-css",
}}
description={customCssT("description")}
error={error}
inputWrapperOrder={["label", "description", "input", "error"]}
>
<div className={classes.codeEditorRoot}>
<Editor
textareaId="custom-css"
onValueChange={onChange}
value={value ?? ""}
highlight={(code) => highlight(code, languages.extend("css", {}), "css")}
padding={10}
style={{
fontFamily: '"Fira code", "Fira Mono", monospace',
fontSize: 12,
minHeight: 250,
}}
/>
</div>
</Input.Wrapper>
);
};
@@ -1,22 +0,0 @@
.codeEditorFooter {
border-bottom-left-radius: var(--mantine-radius-sm);
border-bottom-right-radius: var(--mantine-radius-sm);
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
}
.codeEditorRoot {
margin-top: 4px;
border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
border-width: 1px;
border-style: solid;
border-radius: var(--mantine-radius-sm);
}
.codeEditor {
background-color: light-dark(white, var(--mantine-color-dark-6));
font-size: var(--mantine-font-size-xs);
}
.codeEditor ::placeholder {
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
}
+1
View File
@@ -199,6 +199,7 @@ export default async function Layout(props: {
) : null}
<SearchEngineOptimization />
<CrowdinLiveTranslation locale={locale} />
<style data-homarr-global-custom-css>{serverSettings.branding.customCss}</style>
</head>
<body className={[fontSans.className, fontSans.variable].join(" ")} suppressHydrationWarning>
<Analytics enabled={serverSettings.analytics.enableGeneral} />
@@ -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";
@@ -20,7 +20,9 @@ import type { UseFormReturnType } from "@homarr/form";
import { useI18n } from "@homarr/translation/client";
import { BoardColorInput, CornerStylePicker, cornerStyleValues } from "@homarr/ui";
import { CustomCssEditor } from "~/components/custom-css/custom-css-editor";
import { SectionCard } from "~/components/manage/section-card";
import { WorkshopCssImportButton } from "~/components/workshop/workshop-css-import-button";
import type { FormValues } from "./settings-form";
interface BrandingSettingsFormProps {
@@ -101,6 +103,21 @@ export const BrandingSettingsForm = ({ form }: BrandingSettingsFormProps) => {
</SimpleGrid>
</Fieldset>
<Fieldset legend={t("groups.customCss")} p="sm">
<CustomCssEditor
id="global-custom-css"
label={t("customCss.label")}
description={t("customCss.description")}
actions={
<WorkshopCssImportButton
size="compact-xs"
onImport={(css) => form.setFieldValue("branding.customCss", css)}
/>
}
{...form.getInputProps("branding.customCss")}
/>
</Fieldset>
<Fieldset legend={t("groups.authentication")} p="sm">
<Stack gap="sm">
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="sm" verticalSpacing="sm">
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Button, Stack } from "@mantine/core";
import { z } from "zod/v4";
@@ -70,6 +71,7 @@ interface SettingsFormProps {
export const SettingsForm = ({ initialData, selectableBoards, selectableSearchEngines }: SettingsFormProps) => {
const tCommon = useI18n("common");
const tSettings = useI18n("management.page.settings");
const router = useRouter();
const initialValues = buildInitialValues(initialData);
const initialValuesRef = useRef(initialValues);
@@ -164,6 +166,7 @@ export const SettingsForm = ({ initialData, selectableBoards, selectableSearchEn
form.setInitialValues(values);
form.resetDirty();
await revalidatePathActionAsync("/manage/settings");
router.refresh();
showSuccessNotification({
title: tCommon("notification.update.success"),
message: tSettings("notification.success.message"),
@@ -8,6 +8,7 @@ import { useEditMode } from "@homarr/boards/edit-mode";
import { useI18n } from "@homarr/translation/client";
import type { ContainerSectionItem } from "~/app/[locale]/boards/_types";
import { COLLAPSED_SECTION_ROW_COUNT } from "~/components/board/layout";
import { SectionGrid } from "./grid/section-grid";
import { useSectionCollapse } from "./section-collapse";
import { useOpenSectionApps } from "./use-open-section-apps";
@@ -149,7 +150,13 @@ export const BoardContainerSection = ({ section }: Props) => {
aria-hidden={isVisuallyCollapsed}
inert={isVisuallyCollapsed}
>
<SectionGrid section={section} columnCount={section.width} requestedRowCount={section.height} label={label} />
<SectionGrid
section={section}
columnCount={section.width}
requestedRowCount={section.height}
viewportRowCountOverride={isVisuallyCollapsed ? COLLAPSED_SECTION_ROW_COUNT : undefined}
label={label}
/>
</Box>
</Card>
{isEditMode && <BoardContainerMenu section={section} />}
@@ -33,6 +33,7 @@ interface SectionGridProps {
section: Exclude<Section, { kind: "container" }> | ContainerSectionItem;
columnCount: number;
requestedRowCount?: number;
viewportRowCountOverride?: number;
label: string;
railPlacement?: "main" | "left" | "right";
className?: string;
@@ -42,6 +43,7 @@ export const SectionGrid = ({
section,
columnCount,
requestedRowCount = 0,
viewportRowCountOverride,
label,
railPlacement = "main",
className,
@@ -67,6 +69,8 @@ export const SectionGrid = ({
() =>
[...items, ...innerSections].map((item): SectionGridPlacement => {
const minimum = item.type === "section" ? minimumBySectionId.get(item.id) : undefined;
const isNonScrollableContainer = item.type === "section" && !item.options.scrollable;
const height = isNonScrollableContainer && minimum ? Math.max(item.height, minimum.height) : item.height;
return normalizeGridPlacement(
{
id: item.id,
@@ -74,7 +78,7 @@ export const SectionGrid = ({
x: item.xOffset,
y: item.yOffset,
w: item.width,
h: item.height,
h: height,
minW: minimum?.width,
minH: minimum?.height,
},
@@ -137,7 +141,8 @@ export const SectionGrid = ({
// A scrollable container isn't forced to grow with its content - it scrolls internally instead
// of expanding to fit every widget, so its viewport height is capped independently of rowCount.
const isScrollableContainer = section.kind === "container" && section.options.scrollable;
const viewportRowCount = isScrollableContainer ? Math.max(requestedRowCount, 1) : rowCount;
const viewportRowCount =
viewportRowCountOverride ?? (isScrollableContainer ? Math.max(requestedRowCount, 1) : rowCount);
// A container's own visible card is inset from its allocated board cell by the board's
// standard per-item gap (see the base, non-container `.staticItem[data-type="item"] >
// .contentMount` rule in section-grid.module.css - a container is placed as an "item" like
@@ -159,14 +164,9 @@ export const SectionGrid = ({
// all, the header bar covers the first row of the container's content instead of sitting
// above it. Only the vertical axis is affected - the toggle spans the full width already.
const collapsibleHeaderInset =
section.kind === "container" && section.options.collapsible
? CONTAINER_HEADER_HEIGHT / effectiveCanvasScale
: 0;
section.kind === "container" && section.options.collapsible ? CONTAINER_HEADER_HEIGHT / effectiveCanvasScale : 0;
const logicalWidth = getLogicalGridSize(columnCount) - outerCardInset;
const viewportHeight = Math.max(
1,
getLogicalGridSize(viewportRowCount) - outerCardInset - collapsibleHeaderInset,
);
const viewportHeight = Math.max(1, getLogicalGridSize(viewportRowCount) - outerCardInset - collapsibleHeaderInset);
// Items are positioned in a coordinate space sized to the *un-inset* column/row count
// (fullGridWidth/Height below - see getLogicalItemStyle), but logicalWidth/Height above are
// deliberately smaller by outerCardInset to match the container's actual visible card size.
@@ -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">
-225
View File
@@ -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;
}
+2
View File
@@ -51,6 +51,7 @@ export const brandingServerSettingsSchema = z.object({
signInBackgroundOverlay: z.number().min(0).max(0.9),
authBranding: authBrandingSchema,
defaultRadius: z.enum(brandingRadiusOptions),
customCss: z.string().max(16_384),
});
export type BrandingSettings = z.infer<typeof brandingServerSettingsSchema>;
@@ -71,6 +72,7 @@ export const defaultBrandingSettings: BrandingSettings = {
showGreeting: true,
},
defaultRadius: "md",
customCss: "",
};
const hasDefaultBrandingColors = (branding: Pick<BrandingSettings, "primaryColor" | "secondaryColor">) =>
+5
View File
@@ -5972,6 +5972,7 @@
"groups": {
"identity": "Identity",
"appearance": "Appearance",
"customCss": "Custom CSS",
"authentication": "Authentication"
},
"identity": {
@@ -6018,6 +6019,10 @@
"xl": "Soft"
}
},
"customCss": {
"label": "Global custom CSS",
"description": "Applied to every page. Board custom CSS can override it."
},
"authVisibility": {
"description": "Choose what appears on every authentication page.",
"appName": "Application name",
-9
View File
@@ -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