mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
refactor: better typing for tool icons
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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([]);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user