From 47cd7dda1fa4db3427a52add2630e0d450c07aa8 Mon Sep 17 00:00:00 2001
From: Thomas Camlong
Loading comments…
} + {loading && ( +Failed to load comments
} {!loading && !fetchError && rows.length === 0 && (No comments yet
@@ -165,6 +174,7 @@ export const CommentsSection = ({ {canManage && !isEditing && (Check the Workshop service and try again.
+
{
Changelog Changelog {submission.changelog}
- The community has reported this submission {reports.length} time{reports.length === 1 ? "" : "s"}.
- Install in Homarr
+ {submission.type === "customWidget"
+ ? "Download the JSON, then open Manage → Custom Widgets → Import in your Homarr instance. Homarr asks for any required credentials during import."
+ : "Copy the CSS, then open your board settings and paste it into Custom CSS."}{" "}
+
+ Read the installation guide
+
+ Community custom widgets and CSS themes for Homarr.
+ Discover community-made widgets and CSS. Review the source, then import it into Homarr.
+ Vote and share your creations Vote, comment, report, and publish
+ Check the Workshop service and your connection, then try again.
+ {empty.title} {empty.hint}
+ Reload the page to try again. Your Homarr installation is not affected.
+ Hi there,
+ {{commenterName}} left a comment on your submission
+ {{submissionTitle}}:
+ ${escapeHtml(commenterName)} commented on ${escapeHtml(submissionTitle)}:
- {reports.map((report) => (
-
-
Workshop
- Workshop listings could not be loaded
+ Workshop could not be displayed
+
+
+
+
diff --git a/apps/workshop/pb_hooks/workshop.pb.js b/apps/workshop/pb_hooks/workshop.pb.js
index 4b54fe0fc..4e266a048 100644
--- a/apps/workshop/pb_hooks/workshop.pb.js
+++ b/apps/workshop/pb_hooks/workshop.pb.js
@@ -40,12 +40,20 @@ onRecordAfterCreateSuccess((event) => {
const rawExcerpt = event.record.getString("content").slice(0, 280);
const submissionUrl = `${publicOrigin}/workshop/${submission.id}/`;
const sender = event.app.settings().meta;
+ const templatePath = $os.getenv("WORKSHOP_PB_HOOKS_DIR") || $filepath.dirname(__filepath);
+ const template = $os
+ .readFile($filepath.join(templatePath, "comment-email.html"))
+ .replaceAll("{{commenterName}}", escapeHtml(commenterName))
+ .replaceAll("{{submissionTitle}}", escapeHtml(submissionTitle))
+ .replaceAll("{{commentExcerpt}}", escapeHtml(rawExcerpt))
+ .replaceAll("{{submissionUrl}}", escapeHtml(submissionUrl));
+
const message = new MailerMessage({
from: { address: sender.senderAddress, name: sender.senderName },
to: [{ address: recipientEmail }],
subject: `${commenterName} commented on ${submissionTitle}`,
text: `${commenterName} commented on “${submissionTitle}”:\n\n${rawExcerpt}\n\n${submissionUrl}`,
- html: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{commentExcerpt}}
+
+
+
+
+
+
+
+
+
+ VIEW THE COMMENT
+
+
+
+
+
+
+
+
+
+
+ homarr.dev
+ · Homarr — 2026
+
+ ${escapeHtml(rawExcerpt)}
`,
+ html: template,
});
event.app.newMailClient().send(message);
} catch (error) {
diff --git a/apps/workshop/pb_migrations/1784240000_workshop_widgets.js b/apps/workshop/pb_migrations/1784240000_workshop_widgets.js
index e008bcd25..9c8f51f72 100644
--- a/apps/workshop/pb_migrations/1784240000_workshop_widgets.js
+++ b/apps/workshop/pb_migrations/1784240000_workshop_widgets.js
@@ -135,8 +135,8 @@ migrate(
const reports = new Collection({
type: "base",
name: "reports",
- listRule: "",
- viewRule: "",
+ listRule: adminRule,
+ viewRule: adminRule,
createRule: "@request.auth.id != '' && @request.body.reporter = @request.auth.id",
updateRule: `${adminRule} && @request.body.reporter:changed = false && @request.body.submission:changed = false`,
deleteRule: adminRule,
diff --git a/packages/custom-widgets/src/workbench/code-editor.module.css b/packages/custom-widgets/src/workbench/code-editor.module.css
index 565976940..f1569bf88 100644
--- a/packages/custom-widgets/src/workbench/code-editor.module.css
+++ b/packages/custom-widgets/src/workbench/code-editor.module.css
@@ -1,8 +1,8 @@
.root {
margin-top: 4px;
- border: 1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4));
+ border: 1px solid var(--mantine-color-default-border);
border-radius: var(--mantine-radius-sm);
- background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
+ background: var(--mantine-color-body);
overflow: hidden;
}
@@ -15,7 +15,7 @@
.toolbar,
.footer {
padding: 7px 10px;
- background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
+ background: var(--mantine-color-default-hover);
}
.toolbar {
@@ -29,8 +29,8 @@
overflow: auto;
}
.viewport :global(.cm-editor) {
- background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
- color: light-dark(var(--mantine-color-black), var(--mantine-color-gray-0));
+ background: var(--mantine-color-body);
+ color: var(--mantine-color-text);
font-family: "Fira Code", "SFMono-Regular", Consolas, monospace;
font-size: 12px;
}
@@ -42,7 +42,7 @@
overflow: auto;
}
.viewport :global(.cm-gutters) {
- background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
+ background: var(--mantine-color-default-hover);
border-color: light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
}
.diagnosticButton {
diff --git a/packages/workshop/src/schema.spec.ts b/packages/workshop/src/schema.spec.ts
index 7d2671b7c..68cb85cd0 100644
--- a/packages/workshop/src/schema.spec.ts
+++ b/packages/workshop/src/schema.spec.ts
@@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest";
import { CUSTOM_WIDGET_STARTER } from "@homarr/custom-widgets/core";
-import { validateWorkshopWidget, workshopSubmissionInputSchema } from "./schema";
+import { validateWorkshopWidget, workshopSubmissionInputSchema, workshopSubmissionSummarySchema } from "./schema";
describe("Workshop widget validation", () => {
test("accepts a canonical credential-free widget", () => {
@@ -55,6 +55,25 @@ describe("Workshop widget validation", () => {
).toBe(false);
});
+ test("normalizes PocketBase records before rendering", () => {
+ const result = workshopSubmissionSummarySchema.parse({
+ id: "jzyxskmimxn42io",
+ title: "Tautulli Activity",
+ description: "Shows active Plex streams from Tautulli",
+ widgetSchema: "homarr-custom-widget-v2",
+ screenshots: "[]",
+ author: "wsqylmotymtqpox",
+ revision: 1,
+ created: "2026-07-21 14:19:25.790Z",
+ updated: "2026-07-21 14:19:25.790Z",
+ });
+
+ expect(result.type).toBe("customWidget");
+ expect(result.screenshots).toEqual([]);
+ expect(result.authorName).toBe("Community member");
+ expect(result.upvotes).toBe(0);
+ });
+
test("rejects literal credentials in templates and icon URLs", () => {
expect(
validateWorkshopWidget(
diff --git a/packages/workshop/src/schema.ts b/packages/workshop/src/schema.ts
index e1ff627ec..0aea69d41 100644
--- a/packages/workshop/src/schema.ts
+++ b/packages/workshop/src/schema.ts
@@ -36,7 +36,17 @@ export const workshopUserSchema = z.object({
});
export type WorkshopUser = z.infer