commit b7601fe123a4b6bcd243dfcf83c5270be8740c3d
parent ce51f06be88e5591c891663e54ff1262ede0167f
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 28 Sep 2026 18:42:08 -0400
editor: "Show in header" on each social link, with its hint; a refused icon's save names why
The social-links editor (Settings, a site's form) gains one checkbox per row,
stored as `featured: true` only when checked, and beside it "With none checked,
the header shows the last four." The posted JSON is built by socialLinksJson
(pure, unit tested); the settings spec reads `featured` back. A refused SVG's
error names its reason class (an event handler attribute, a link to something
outside the icon, …) without echoing the markup. Existing labels, placeholders
and buttons are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
6 files changed, 78 insertions(+), 12 deletions(-)
diff --git a/editor/app/components/SocialLinksField.tsx b/editor/app/components/SocialLinksField.tsx
@@ -1,11 +1,17 @@
"use client";
import type { SocialLink } from "yt-dlp-transcript-common/lib/settings";
+import { socialLinksJson, type SocialRow } from "./socialLinksJson";
-export type SocialRow = { label: string; url: string; svg: string };
+export type { SocialRow };
export function toSocialRow(s: SocialLink): SocialRow {
- return { label: s.label, url: s.url, svg: s.svg };
+ return {
+ label: s.label,
+ url: s.url,
+ svg: s.svg,
+ ...(s.featured ? { featured: true } : {}),
+ };
}
type Props = {
@@ -32,13 +38,7 @@ export function SocialLinksField({ value, onChange, name }: Props) {
const update = (idx: number, patch: Partial<SocialRow>) =>
onChange(value.map((row, i) => (i === idx ? { ...row, ...patch } : row)));
- const json = JSON.stringify(
- value.map((s) => ({
- label: s.label.trim(),
- url: s.url.trim(),
- svg: s.svg.trim(),
- })),
- );
+ const json = socialLinksJson(value);
return (
<>
@@ -96,6 +96,22 @@ export function SocialLinksField({ value, onChange, name }: Props) {
placeholder='<svg viewBox="0 0 24 24"><path d="…"/></svg>'
className="rounded border border-border bg-card px-2 py-1 text-xs font-mono"
/>
+ {/* A header shows at most four links: the ones checked here, or,
+ with none checked, the last four (common/lib/socialLinks.ts). */}
+ <div className="flex flex-wrap items-center gap-x-3 gap-y-1">
+ <label className="flex items-center gap-2 text-sm">
+ <input
+ type="checkbox"
+ checked={s.featured === true}
+ onChange={(e) => update(idx, { featured: e.target.checked })}
+ className="accent-brand"
+ />
+ Show in header
+ </label>
+ <span className="text-xs text-muted-foreground">
+ With none checked, the header shows the last four.
+ </span>
+ </div>
</div>
))}
<div>
diff --git a/editor/app/components/socialLinksJson.test.ts b/editor/app/components/socialLinksJson.test.ts
@@ -0,0 +1,23 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { parseSocialLinks } from "yt-dlp-transcript-common/lib/settingsSchema";
+import { socialLinksJson } from "./socialLinksJson";
+
+const SVG = `<svg viewBox="0 0 24 24"><path d="M0 0"/></svg>`;
+
+test("Show in header: a checked row posts featured: true, an unchecked one no key", () => {
+ const posted = JSON.parse(
+ socialLinksJson([
+ { label: " A ", url: " https://a.example ", svg: ` ${SVG} `, featured: true },
+ { label: "B", url: "https://b.example", svg: SVG, featured: false },
+ { label: "C", url: "https://c.example", svg: SVG },
+ ]),
+ );
+ assert.deepEqual(posted, [
+ { label: "A", url: "https://a.example", svg: SVG, featured: true },
+ { label: "B", url: "https://b.example", svg: SVG },
+ { label: "C", url: "https://c.example", svg: SVG },
+ ]);
+ // …and the actions' parser keeps exactly that.
+ assert.deepEqual(parseSocialLinks(posted), posted);
+});
diff --git a/editor/app/components/socialLinksJson.ts b/editor/app/components/socialLinksJson.ts
@@ -0,0 +1,22 @@
+// What the social-links editor posts: its rows, trimmed, as the JSON both the
+// settings and the site actions parse with parseSocialLinks. Pure, so it has a
+// unit test (socialLinksJson.test.ts) without a DOM.
+
+export type SocialRow = {
+ label: string;
+ url: string;
+ svg: string;
+ // "Show in header". Posted only when checked, the way the schema stores it.
+ featured?: boolean;
+};
+
+export function socialLinksJson(rows: readonly SocialRow[]): string {
+ return JSON.stringify(
+ rows.map((s) => ({
+ label: s.label.trim(),
+ url: s.url.trim(),
+ svg: s.svg.trim(),
+ ...(s.featured ? { featured: true } : {}),
+ })),
+ );
+}
diff --git a/editor/app/settings/actions.ts b/editor/app/settings/actions.ts
@@ -11,6 +11,7 @@ import {
RESUME_MARGIN_GB_DEFAULT,
RESUME_MARGIN_GB_MAX,
normalizeSocialSvg,
+ socialSvgProblem,
parseSocialLinks,
PARALLEL_TRANSCRIPTIONS_DEFAULT,
SLEEP_BETWEEN_DOWNLOADS_MAX_SECONDS,
@@ -172,7 +173,7 @@ export async function saveSettingsAction(
if (svg === null) {
return {
ok: false,
- error: `Social link "${link.label}" has an invalid SVG.`,
+ error: `Social link "${link.label}" has an invalid SVG: ${socialSvgProblem(link.svg)}.`,
};
}
socialLinks.push({ ...link, svg });
diff --git a/editor/app/sites/actions.ts b/editor/app/sites/actions.ts
@@ -21,6 +21,7 @@ import {
} from "yt-dlp-transcript-common/lib/site";
import {
normalizeSocialSvg,
+ socialSvgProblem,
parseSocialLinks,
type SocialLink,
} from "yt-dlp-transcript-common/lib/settings";
@@ -182,7 +183,7 @@ export async function saveSiteAction(
if (svg === null) {
return {
ok: false,
- error: `Social link "${link.label}" has an invalid SVG.`,
+ error: `Social link "${link.label}" has an invalid SVG: ${socialSvgProblem(link.svg)}.`,
};
}
socialLinks.push({ ...link, svg });
diff --git a/editor/e2e/settings.spec.ts b/editor/e2e/settings.spec.ts
@@ -42,6 +42,7 @@ test("saves global default social links", async ({ page }) => {
await page
.getByPlaceholder(/<svg viewbox/i)
.fill('<svg viewBox="0 0 24 24"><path d="M0 0h24v24H0z"/></svg>');
+ await page.getByRole("checkbox", { name: "Show in header" }).check();
await page.getByRole("button", { name: /save settings/i }).click();
await expect(
@@ -49,10 +50,12 @@ test("saves global default social links", async ({ page }) => {
).toBeVisible();
const saved = await readJson<{
- socialLinks?: { label: string; url: string; svg: string }[];
+ socialLinks?: { label: string; url: string; svg: string; featured?: boolean }[];
}>("test-settings.json");
expect(saved.socialLinks).toHaveLength(1);
expect(saved.socialLinks?.[0].label).toBe("GitHub");
+ // "Show in header" is stored as featured: true.
+ expect(saved.socialLinks?.[0].featured).toBe(true);
// SVG was normalized on save (fill="currentColor" injected).
expect(saved.socialLinks?.[0].svg).toContain("currentColor");
});