refactor: better typing for tool icons

This commit is contained in:
2026-10-10 11:47:31 +05:00
parent 8b2172c39a
commit 2ddb348e54
20 changed files with 94 additions and 62 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ describe("dictionary completeness for the new registry", () => {
});
it("the pages section has no extra pages", () => {
const slugs = new Set(PAGES.map((page) => page.slug));
const slugs = new Set<string>(PAGES.map((page) => page.slug));
for (const [locale, dict] of [
["ru", ru],
["en", en],
+7 -2
View File
@@ -50,7 +50,9 @@ export const TOOLS: Tool[] = [
...textTools,
] as unknown as Tool[];
export const PAGES: Page[] = [
// Not annotated as Page[] on purpose: the literal slug types must survive
// for PageSlug below; every consumer treats PAGES as a readonly Page list.
export const PAGES = [
...geometryPages,
...alphaPages,
...convertPages,
@@ -59,7 +61,10 @@ export const PAGES: Page[] = [
...colorPages,
...generatePages,
...textPages,
] as unknown as Page[];
];
/** Union of existing page slugs (groups use `satisfies Page[]`, so literals survive). */
export type PageSlug = (typeof PAGES)[number]["slug"];
export function getTool(id: string): Tool | undefined {
return TOOLS.find((tool) => tool.id === id);
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Page } from "../types";
export const alphaPages: Page[] = [
export const alphaPages = [
{
slug: "add-stroke-png",
title: "Outline PNG",
@@ -157,4 +157,4 @@ export const alphaPages: Page[] = [
category: "alpha",
steps: [{ id: "close-holes" }],
},
];
] as const satisfies readonly Page[];
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Page } from "../types";
export const analyzePages: Page[] = [
export const analyzePages = [
{
slug: "extract-color-from-png",
title: "Extract Color from PNG",
@@ -93,4 +93,4 @@ export const analyzePages: Page[] = [
category: "analyze",
steps: [{ id: "orientation" }],
},
];
] as const satisfies readonly Page[];
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Page } from "../types";
export const colorPages: Page[] = [
export const colorPages = [
{
slug: "two-colors-png",
title: "Two colors PNG",
@@ -199,4 +199,4 @@ export const colorPages: Page[] = [
category: "color",
steps: [{ id: "to-lab" }],
},
];
] as const satisfies readonly Page[];
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Page } from "../types";
export const convertPages: Page[] = [
export const convertPages = [
{
slug: "convert-png-to-jpg",
title: "Convert PNG to JPG",
@@ -112,4 +112,4 @@ export const convertPages: Page[] = [
category: "convert",
steps: [{ id: "from-svg" }],
},
];
] as const satisfies readonly Page[];
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Page } from "../types";
export const filtersPages: Page[] = [
export const filtersPages = [
{
slug: "blur-png",
title: "Blur PNG",
@@ -65,4 +65,4 @@ export const filtersPages: Page[] = [
category: "filters",
steps: [{ id: "jpeg-artifacts" }],
},
];
] as const satisfies readonly Page[];
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Page } from "../types";
export const generatePages: Page[] = [
export const generatePages = [
{
slug: "create-empty-png",
title: "Create empty PNG",
@@ -164,4 +164,4 @@ export const generatePages: Page[] = [
category: "generate",
steps: [{ id: "from-text" }],
},
];
] as const satisfies readonly Page[];
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Page } from "../types";
export const geometryPages: Page[] = [
export const geometryPages = [
{
slug: "add-border-png",
title: "Add border to PNG",
@@ -149,4 +149,4 @@ export const geometryPages: Page[] = [
category: "geometry",
steps: [{ id: "shift" }],
},
];
] as const satisfies readonly Page[];
+2 -2
View File
@@ -1,6 +1,6 @@
import type { Page } from "../types";
export const textPages: Page[] = [
export const textPages = [
{
slug: "add-text-png",
title: "Add text to PNG",
@@ -25,4 +25,4 @@ export const textPages: Page[] = [
category: "text",
steps: [{ id: "watermark-tile" }],
},
];
] as const satisfies readonly Page[];
+3 -1
View File
@@ -48,7 +48,9 @@ describe("registry-new (migrated tools)", () => {
});
it("every tool is used by at least one page", () => {
const used = new Set(PAGES.flatMap((page) => page.steps.map((s) => s.id)));
const used = new Set<string>(
PAGES.flatMap((page) => page.steps.map((s) => s.id)),
);
const orphans = TOOLS.map((tool) => tool.id).filter((id) => !used.has(id));
expect(orphans, `tools without pages:\n${orphans.join("\n")}`).toEqual([]);
});
+1 -1
View File
@@ -104,7 +104,7 @@ describe("docs/tools-map.md vs registry pages", () => {
const markdown = readFileSync(MAP_PATH, "utf8");
const mentioned = mentionedIds(markdown);
const planned = plannedIds(markdown);
const registrySlugs = new Set(PAGES.map((page) => page.slug));
const registrySlugs = new Set<string>(PAGES.map((page) => page.slug));
it("parses a non-empty slug list (parser did not silently break)", () => {
expect(mentioned.length).toBeGreaterThan(0);
+5 -5
View File
@@ -80,12 +80,12 @@ export type PageStep = {
/** Page: unique address, strings, and the ordered tool list in `steps` (a page may be a ready-made chain run by `SchemaToolView`). */
export type Page = {
/** Address `/tools/<slug>`, downloaded file name, and icon key. */
slug: string;
readonly slug: string;
/** EN string; the translation lives in the dictionary by slug. */
title: string;
description: string;
category: CategoryId;
steps: PageStep[];
readonly title: string;
readonly description: string;
readonly category: CategoryId;
readonly steps: readonly PageStep[];
};
export function requireSource<P>(ctx: ToolContext<P>): PixelImage {
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { PAGES } from "./registry";
import { TOOL_ICONS } from "./tool-icons";
describe("tool-icons", () => {
it("every icon key is an existing page slug", () => {
const slugs = new Set<string>(PAGES.map((page) => page.slug));
const orphans = Object.keys(TOOL_ICONS).filter((key) => !slugs.has(key));
expect(orphans).toEqual([]);
});
it("every page has an icon", () => {
const missing = PAGES.map((page) => page.slug).filter(
(slug) => !(slug in TOOL_ICONS),
);
expect(missing).toEqual([]);
});
});
+12 -13
View File
@@ -1,5 +1,4 @@
import {
AppWindow,
Binary,
Blend,
CalendarDays,
@@ -15,7 +14,6 @@ import {
FileImage,
FileOutput,
FilePlus2,
Film,
FlipHorizontal2,
Focus,
Frame,
@@ -27,7 +25,6 @@ import {
Layers,
Link2,
Maximize2,
Minimize2,
Moon,
PaintBucket,
Palette,
@@ -49,8 +46,13 @@ import {
ZoomIn,
ZoomOut,
} from "@lucide/svelte";
import type { Component } from "svelte";
import type { PageSlug } from "./registry";
export const TOOL_ICONS: Record<string, typeof AppWindow> = {
type IconComponent = Component<{ size?: number; class?: string }>;
// Keyed by page slug: a renamed/missing page is a compile error here.
export const TOOL_ICONS: Record<PageSlug, IconComponent> = {
"resize-png": Scaling,
"crop-png": Crop,
"rotate-png": RotateCw,
@@ -61,12 +63,6 @@ export const TOOL_ICONS: Record<string, typeof AppWindow> = {
"convert-png-to-jpg": FileImage,
"convert-png-to-webp": FileImage,
"remove-color-from-png": Scissors,
"png-info": Info,
"jpg-to-png": FileImage,
"webp-to-png": FileImage,
"gif-to-png": Film,
"bmp-to-png": FileImage,
"ico-to-png": AppWindow,
"png-to-bmp": FileOutput,
"png-to-base64": Binary,
"base64-to-png": ClipboardPaste,
@@ -134,8 +130,6 @@ export const TOOL_ICONS: Record<string, typeof AppWindow> = {
"change-aspect-ratio-png": Scaling,
"swap-orientation-png": RotateCw,
"symmetric-copy-png": FlipHorizontal2,
"compress-png": Minimize2,
"reduce-to-size-png": Scaling,
"png-file-size": Info,
"png-to-bytes": Binary,
"bytes-to-png": Binary,
@@ -153,7 +147,6 @@ export const TOOL_ICONS: Record<string, typeof AppWindow> = {
"star-mask-png": Star,
"wavy-mask-png": WavesHorizontal,
"watermark-tile-png": Stamp,
"watermark-image-png": FileImage,
"color-wheel-png": Rainbow,
"complementary-png": Contrast,
"triadic-png": Hash,
@@ -183,3 +176,9 @@ export const TOOL_ICONS: Record<string, typeof AppWindow> = {
"tint-png": Droplet,
"svg-to-png": FileImage,
};
// Slugs come from the registry at runtime (Page.slug is string there), so the
// lookup widens; completeness is guaranteed by the Record<PageSlug, _> above.
export function toolIcon(slug: string): IconComponent {
return TOOL_ICONS[slug as PageSlug];
}
+3 -3
View File
@@ -13,7 +13,7 @@
} from "$lib/i18n/schema-tool-strings";
import { t } from "$lib/i18n/t";
import { PAGES } from "$lib/registry";
import { TOOL_ICONS } from "$lib/tool-icons";
import { toolIcon } from "$lib/tool-icons";
let query = $state("");
let category = $state<string>("all");
@@ -52,7 +52,7 @@
slug={page.slug}
description={pageDescription(page)}
index={i + 1}
icon={TOOL_ICONS[page.slug]}
icon={toolIcon(page.slug)}
/>
{/each}
</CatalogGroup>
@@ -65,7 +65,7 @@
slug={page.slug}
description={pageDescription(page)}
index={i + 1}
icon={TOOL_ICONS[page.slug]}
icon={toolIcon(page.slug)}
/>
{/each}
</CatalogGroup>
+3 -3
View File
@@ -13,7 +13,7 @@
} from "$lib/i18n/schema-tool-strings";
import { t } from "$lib/i18n/t";
import { PAGES } from "$lib/registry";
import { TOOL_ICONS } from "$lib/tool-icons";
import { toolIcon } from "$lib/tool-icons";
let query = $state("");
let category = $state<string>("all");
@@ -56,7 +56,7 @@
slug={page.slug}
description={pageDescription(page)}
index={i + 1}
icon={TOOL_ICONS[page.slug]}
icon={toolIcon(page.slug)}
/>
{/each}
</CatalogGroup>
@@ -69,7 +69,7 @@
slug={page.slug}
description={pageDescription(page)}
index={i + 1}
icon={TOOL_ICONS[page.slug]}
icon={toolIcon(page.slug)}
/>
{/each}
</CatalogGroup>