- {fields.map((field) => {
- const value = values[field.key] ?? "";
- const inputId = `${formId}-${field.key}`;
- return (
-
{
- clearFeedback();
- setValues((current) => ({ ...current, [field.key]: nextValue }));
- }}
- />
- );
- })}
+
+ {fieldGroups.map((group, groupIndex) => (
+
0 ? " mt-5 border-t border-slate-100 pt-5" : ""}`
+ : groupIndex > 0 ? "border-t border-slate-100" : ""}
+ >
+ {group.fields.map((field) => {
+ const value = values[field.key] ?? "";
+ const inputId = `${formId}-${field.key}`;
+ return (
+
{
+ clearFeedback();
+ setValues((current) => ({ ...current, [field.key]: nextValue }));
+ }}
+ />
+ );
+ })}
+
+ ))}
{message ? {message} : null}
diff --git a/apps/web/src/components/plugins/plugin-settings-layout.test.mjs b/apps/web/src/components/plugins/plugin-settings-layout.test.mjs
new file mode 100644
index 00000000..3e1430f0
--- /dev/null
+++ b/apps/web/src/components/plugins/plugin-settings-layout.test.mjs
@@ -0,0 +1,32 @@
+import { describe, expect, test } from "bun:test";
+import { groupPluginSettingFields } from "./plugin-settings-layout.ts";
+
+describe("plugin settings layout", () => {
+ test("groups consecutive topic switches without absorbing unrelated switches", () => {
+ const fields = [
+ { key: "topics.ai", type: "boolean", label: "AI" },
+ { key: "topics.engineering", type: "boolean", label: "Engineering" },
+ { key: "topics.science", type: "boolean", label: "Science" },
+ { key: "translation.auto-enabled", type: "boolean", label: "Translate" },
+ { key: "translation.target-language", type: "select", label: "Language", options: [{ value: "en", label: "English" }] },
+ { key: "digest.auto-enabled", type: "boolean", label: "Digest" },
+ ];
+
+ const groups = groupPluginSettingFields(fields);
+ expect(groups.map((group) => ({ compact: group.compact, keys: group.fields.map((field) => field.key) }))).toEqual([
+ { compact: true, keys: ["topics.ai", "topics.engineering", "topics.science"] },
+ { compact: false, keys: ["translation.auto-enabled"] },
+ { compact: false, keys: ["translation.target-language"] },
+ { compact: false, keys: ["digest.auto-enabled"] },
+ ]);
+ });
+
+ test("keeps short boolean runs in the standard full-width layout", () => {
+ const fields = [
+ { key: "alerts.email", type: "boolean", label: "Email" },
+ { key: "alerts.push", type: "boolean", label: "Push" },
+ ];
+
+ expect(groupPluginSettingFields(fields).every((group) => !group.compact)).toBe(true);
+ });
+});
diff --git a/apps/web/src/components/plugins/plugin-settings-layout.ts b/apps/web/src/components/plugins/plugin-settings-layout.ts
new file mode 100644
index 00000000..99db918c
--- /dev/null
+++ b/apps/web/src/components/plugins/plugin-settings-layout.ts
@@ -0,0 +1,30 @@
+import type { PluginSettingField } from "@edgeever/plugin-api";
+
+export interface PluginSettingFieldGroup {
+ id: string;
+ compact: boolean;
+ fields: PluginSettingField[];
+}
+
+const fieldNamespace = (field: PluginSettingField) => field.key.split(".", 1)[0] ?? field.key;
+
+export const groupPluginSettingFields = (fields: PluginSettingField[]): PluginSettingFieldGroup[] => {
+ const groups: PluginSettingFieldGroup[] = [];
+ for (let index = 0; index < fields.length;) {
+ const field = fields[index]!;
+ if (field.type !== "boolean") {
+ groups.push({ id: field.key, compact: false, fields: [field] });
+ index += 1;
+ continue;
+ }
+
+ const namespace = fieldNamespace(field);
+ let end = index + 1;
+ while (end < fields.length && fields[end]!.type === "boolean" && fieldNamespace(fields[end]!) === namespace) end += 1;
+ const run = fields.slice(index, end);
+ if (run.length >= 3) groups.push({ id: `${namespace}:${field.key}`, compact: true, fields: run });
+ else for (const item of run) groups.push({ id: item.key, compact: false, fields: [item] });
+ index = end;
+ }
+ return groups;
+};