From 59ecf04d224b370edafecb0263eb48b97a874868 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 21 Jul 2026 12:12:30 +0200 Subject: [PATCH] Improve Workshop submission editor --- apps/docs/docs/management/workshop/index.mdx | 3 +- .../components/custom-widget-code-client.tsx | 17 +- .../src/components/custom-widget-code.tsx | 22 +++ .../src/components/workshop/SubmitForm.tsx | 165 +++++++++--------- packages/custom-widgets/package.json | 1 + .../src/workbench/code-editor-toolbar.tsx | 8 +- .../src/workbench/code-editor-types.ts | 2 +- .../src/workbench/code-editor.tsx | 2 +- .../src/workbench/direct-code-mirror.tsx | 7 +- pnpm-lock.yaml | 6 + pnpm-workspace.yaml | 1 + 11 files changed, 145 insertions(+), 89 deletions(-) diff --git a/apps/docs/docs/management/workshop/index.mdx b/apps/docs/docs/management/workshop/index.mdx index 8330b0572..84a2bd07b 100644 --- a/apps/docs/docs/management/workshop/index.mdx +++ b/apps/docs/docs/management/workshop/index.mdx @@ -42,7 +42,8 @@ Comments are publicly readable. Signed-in users can post; authors can edit or de - **Custom widget** — paste the JSON exported from **Manage → Custom Widgets → Export**. It must match the `homarr-custom-widget-v2` schema (validated before submission). - **Custom CSS** — paste your stylesheet (max 16384 characters). -3. Add a title, description, optional screenshots, and changelog, then submit. +3. Review the widget JSON or CSS in the syntax-aware editor, then add a title, description, optional screenshots, and changelog. +4. Submit the entry. Submissions are published immediately. Reported entries are reviewed and may be removed. diff --git a/apps/docs/src/components/custom-widget-code-client.tsx b/apps/docs/src/components/custom-widget-code-client.tsx index 90712bb81..f057400dc 100644 --- a/apps/docs/src/components/custom-widget-code-client.tsx +++ b/apps/docs/src/components/custom-widget-code-client.tsx @@ -22,9 +22,13 @@ import { CUSTOM_WIDGET_REQUEST_EXAMPLES, } from "@homarr/custom-widgets/core"; import type { CustomWidgetEditorMessages } from "@homarr/custom-widgets/workbench"; -import { ReadOnlyCustomWidgetCode } from "@homarr/custom-widgets/workbench"; +import { CustomWidgetCodeEditor, ReadOnlyCustomWidgetCode } from "@homarr/custom-widgets/workbench"; -import type { CanonicalCustomWidgetExampleProps, CustomWidgetCodeExampleProps } from "./custom-widget-code"; +import type { + CanonicalCustomWidgetExampleProps, + CustomWidgetCodeExampleProps, + CustomWidgetCodeInputProps, +} from "./custom-widget-code"; const messages: CustomWidgetEditorMessages = { languageJsx: "JSX", @@ -51,6 +55,7 @@ const messages: CustomWidgetEditorMessages = { diagnosticsTitle: "Diagnostics", diagnostic: (diagnostic) => diagnostic.value ?? diagnostic.code, }; +const editableMessages: CustomWidgetEditorMessages = { ...messages, ready: "Ready" }; export function CanonicalCustomWidgetExampleClient({ id, label, example, height }: CanonicalCustomWidgetExampleProps) { const value = example === "requests" ? CUSTOM_WIDGET_REQUEST_EXAMPLES.full : CUSTOM_WIDGET_OPTIONS_EXAMPLES.full; @@ -73,6 +78,14 @@ export function CustomWidgetCodeExampleClient(props: CustomWidgetCodeExampleProp ); } +export function CustomWidgetCodeInputClient(props: CustomWidgetCodeInputProps) { + return ( + + + + ); +} + export function BundledCustomWidgetGalleryClient() { const [selectedId, setSelectedId] = useState(BUNDLED_CUSTOM_WIDGETS[0]?.id ?? null); const selected = BUNDLED_CUSTOM_WIDGETS.find(({ id }) => id === selectedId) ?? BUNDLED_CUSTOM_WIDGETS[0]; diff --git a/apps/docs/src/components/custom-widget-code.tsx b/apps/docs/src/components/custom-widget-code.tsx index fc7dbfef4..cdf819517 100644 --- a/apps/docs/src/components/custom-widget-code.tsx +++ b/apps/docs/src/components/custom-widget-code.tsx @@ -3,6 +3,7 @@ import BrowserOnly from "@docusaurus/BrowserOnly"; import { BundledCustomWidgetGalleryClient, CanonicalCustomWidgetExampleClient, + CustomWidgetCodeInputClient, CustomWidgetCodeExampleClient, } from "./custom-widget-code-client"; @@ -14,6 +15,19 @@ export interface CustomWidgetCodeExampleProps { height?: string; } +export interface CustomWidgetCodeInputProps { + id: string; + label: string; + value: string; + onChange(value: string): void; + language: "json" | "css"; + description?: string; + placeholder?: string; + height?: string; + required?: boolean; + maxLength?: number; +} + export interface CanonicalCustomWidgetExampleProps { id: string; label: string; @@ -37,6 +51,14 @@ export function CustomWidgetCodeExample(props: CustomWidgetCodeExampleProps) { ); } +export function CustomWidgetCodeInput(props: CustomWidgetCodeInputProps) { + return ( + }> + {() => } + + ); +} + export function BundledCustomWidgetGallery() { return ( }> diff --git a/apps/docs/src/components/workshop/SubmitForm.tsx b/apps/docs/src/components/workshop/SubmitForm.tsx index a6d85c6fd..94e282626 100644 --- a/apps/docs/src/components/workshop/SubmitForm.tsx +++ b/apps/docs/src/components/workshop/SubmitForm.tsx @@ -15,6 +15,8 @@ import { import type { SubmissionType } from "@site/src/lib/workshop-schema"; import { validateSubmissionContent } from "@site/src/lib/workshop-schema"; +import { CustomWidgetCodeInput } from "@/components/custom-widget-code"; + import { Button } from "@/components/ui/button"; import { Dialog, @@ -79,10 +81,13 @@ export const SubmitForm = ({ onClose, onSubmit }: Props) => { const canAdvance = [type !== null, title.trim().length >= 3 && content.trim().length > 0, true]; - const goTo = (next: number) => { - setDirection(navDirection[Number(next > step)]); - setStep(next); - }; + const goTo = useCallback( + (next: number) => { + setDirection(navDirection[Number(next > step)]); + setStep(next); + }, + [step], + ); const setContentAndAutofill = useCallback((json: string, currentTitle: string, currentDesc: string) => { setContent(json); @@ -97,17 +102,17 @@ export const SubmitForm = ({ onClose, onSubmit }: Props) => { const jsonFile = Array.from(files).find((f) => f.name.endsWith(".json") || f.type === "application/json"); if (!jsonFile) return false; const reader = new FileReader(); - reader.onload = (e) => { + reader.addEventListener("load", (e) => { const text = e.target?.result as string; if (!type) setType("customWidget"); setContentAndAutofill(text, title, description); if (step === 0) goTo(1); - }; - reader.onerror = () => setError("Could not read the file"); + }); + reader.addEventListener("error", () => setError("Could not read the file")); reader.readAsText(jsonFile); return true; }, - [type, step, title, description, setContentAndAutofill], + [type, step, title, description, setContentAndAutofill, goTo], ); const addImageFiles = useCallback( @@ -187,7 +192,7 @@ export const SubmitForm = ({ onClose, onSubmit }: Props) => { > {/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} { e.preventDefault(); @@ -328,8 +333,9 @@ const StepType = ({