From 7b366c870907e8ebb06f94211803f3da87eb2a0f Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Sun, 23 Nov 2025 19:28:15 +0100 Subject: [PATCH 1/2] test: expand coverage and add a11y linting --- package-lock.json | 130 +++++++- package.json | 4 +- src/types/IconType.ts | 2 +- .../PodcastView.integration.test.ts | 143 ++++++++ src/ui/PodcastView/PodcastView.svelte | 52 +-- src/ui/PodcastView/TopBar.svelte | 4 +- src/ui/PodcastView/TopBar.test.ts | 70 ++++ src/ui/common/Image.test.ts | 60 ++++ src/ui/common/Progressbar.test.ts | 62 +++- .../__tests__/ImageFallbackHarness.svelte | 14 + src/ui/obsidian/Button.svelte | 7 + src/ui/obsidian/Icon.svelte | 20 +- src/ui/obsidian/Text.svelte | 4 + src/ui/settings/PodcastResultCard.svelte | 4 +- tests/mocks/EpisodePlayerStub.svelte | 1 + tests/mocks/obsidian.ts | 313 ++++++++++++++++++ tsconfig.json | 1 + vitest.config.ts | 8 +- vitest.setup.ts | 126 +++++++ 19 files changed, 971 insertions(+), 54 deletions(-) create mode 100644 src/ui/PodcastView/PodcastView.integration.test.ts create mode 100644 src/ui/PodcastView/TopBar.test.ts create mode 100644 src/ui/common/Image.test.ts create mode 100644 src/ui/common/__tests__/ImageFallbackHarness.svelte create mode 100644 tests/mocks/EpisodePlayerStub.svelte create mode 100644 tests/mocks/obsidian.ts create mode 100644 vitest.setup.ts diff --git a/package-lock.json b/package-lock.json index 068515c5..0a57e17b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "obsidian": "1.10.3", "semantic-release": "^25.0.2", "svelte": "^5.43.14", + "svelte-check": "^4.3.4", "svelte-preprocess": "^6.0.3", "tslib": "2.8.1", "typescript": "5.9.3", @@ -574,6 +575,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -617,6 +619,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1393,8 +1396,7 @@ "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -1450,6 +1452,7 @@ "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", @@ -2577,6 +2580,7 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -2759,6 +2763,7 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2816,6 +2821,7 @@ "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.47.0", "@typescript-eslint/types": "8.47.0", @@ -3132,6 +3138,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3485,6 +3492,22 @@ "dev": true, "license": "MIT" }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -3988,6 +4011,7 @@ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -4033,8 +4057,7 @@ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.6", @@ -4561,6 +4584,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4639,6 +4663,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6231,6 +6256,7 @@ "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@acemir/cssom": "^0.9.23", "@asamuzakjp/dom-selector": "^6.7.4", @@ -6657,6 +6683,7 @@ "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "dev": true, "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -6858,6 +6885,16 @@ "node": "*" } }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -9226,6 +9263,7 @@ "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -10050,6 +10088,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -10314,6 +10353,20 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -10496,6 +10549,19 @@ "tslib": "^2.1.0" } }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10543,6 +10609,7 @@ "integrity": "sha512-6qGjWccl5yoyugHt3jTgztJ9Y0JVzyH8/Voc/D8PlLat9pwxQYXz7W1Dpnq5h0/G5GCYGUaDSlYcyk3AMh5A6g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/error": "^4.0.0", @@ -11156,8 +11223,7 @@ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/super-regex": { "version": "1.1.0", @@ -11236,6 +11302,7 @@ "integrity": "sha512-pHeUrp1A5S6RGaXhJB7PtYjL1VVjbVrJ2EfuAoPu9/1LeoMaJa/pcdCsCSb0gS4eUHAHnhCbUDxORZyvGK6kOQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -11256,6 +11323,48 @@ "node": ">=18" } }, + "node_modules/svelte-check": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", + "integrity": "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-check/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/svelte-preprocess": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz", @@ -11532,6 +11641,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11696,6 +11806,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11835,6 +11946,7 @@ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -12412,6 +12524,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -12445,6 +12558,7 @@ "integrity": "sha512-QSD4I0fN6uZQfftryIXuqvqgBxTvJ3ZNkF6RWECd82YGAYAfhcppBLFXzXJHQAAhVFyYEuFTrq6h0hQqjB7jIQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.13", "@vitest/mocker": "4.0.13", @@ -12539,8 +12653,7 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", @@ -12749,6 +12862,7 @@ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "devOptional": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 1d0823a3..e6b9718d 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json", "semantic-release": "semantic-release", - "test": "vitest" + "test": "npm run check:a11y && vitest", + "check:a11y": "svelte-check --fail-on-warnings" }, "keywords": [], "author": "Christian Bager Bach Houmann", @@ -34,6 +35,7 @@ "obsidian": "1.10.3", "semantic-release": "^25.0.2", "svelte": "^5.43.14", + "svelte-check": "^4.3.4", "svelte-preprocess": "^6.0.3", "tslib": "2.8.1", "typescript": "5.9.3", diff --git a/src/types/IconType.ts b/src/types/IconType.ts index 6590be6b..66bcaa76 100644 --- a/src/types/IconType.ts +++ b/src/types/IconType.ts @@ -1,4 +1,4 @@ const ICON_LIST_UNIQUE = new Set(["activity", "airplay", "alarm-check", "alarm-clock-off", "alarm-clock", "alarm-minus", "alarm-plus", "album", "alert-circle", "alert-octagon", "alert-triangle", "align-center-horizontal", "align-center-vertical", "align-center", "align-end-horizontal", "align-end-vertical", "align-horizontal-distribute-center", "align-horizontal-distribute-end", "align-horizontal-distribute-start", "align-horizontal-justify-center", "align-horizontal-justify-end", "align-horizontal-justify-start", "align-horizontal-space-around", "align-horizontal-space-between", "align-justify", "align-left", "align-right", "align-start-horizontal", "align-start-vertical", "align-vertical-distribute-center", "align-vertical-distribute-end", "align-vertical-distribute-start", "align-vertical-justify-center", "align-vertical-justify-end", "align-vertical-justify-start", "align-vertical-space-around", "align-vertical-space-between", "anchor", "aperture", "archive", "arrow-big-down", "arrow-big-left", "arrow-big-right", "arrow-big-up", "arrow-down-circle", "arrow-down-left", "arrow-down-right", "arrow-down", "arrow-left-circle", "arrow-left-right", "arrow-left", "arrow-right-circle", "arrow-right", "arrow-up-circle", "arrow-up-left", "arrow-up-right", "arrow-up", "asterisk", "at-sign", "award", "axe", "banknote", "bar-chart-2", "bar-chart", "baseline", "battery-charging", "battery-full", "battery-low", "battery-medium", "battery", "beaker", "bell-minus", "bell-off", "bell-plus", "bell-ring", "bell", "bike", "binary", "bitcoin", "bluetooth-connected", "bluetooth-off", "bluetooth-searching", "bluetooth", "bold", "book-open", "book", "bookmark-minus", "bookmark-plus", "bookmark", "bot", "box-select", "box", "briefcase", "brush", "bug", "building-2", "building", "bus", "calculator", "calendar", "camera-off", "camera", "car", "carrot", "cast", "check-circle-2", "check-circle", "check-square", "check", "chevron-down", "chevron-first", "chevron-last", "chevron-left", "chevron-right", "chevron-up", "chevrons-down-up", "chevrons-down", "chevrons-left", "chevrons-right", "chevrons-up-down", "chevrons-up", "chrome", "circle-slashed", "circle", "clipboard-check", "clipboard-copy", "clipboard-list", "clipboard-x", "clipboard", "clock-1", "clock-10", "clock-11", "clock-12", "clock-2", "clock-3", "clock-4", "clock-5", "clock-6", "clock-7", "clock-8", "clock-9", "clock", "cloud-drizzle", "cloud-fog", "cloud-hail", "cloud-lightning", "cloud-moon", "cloud-off", "cloud-rain-wind", "cloud-rain", "cloud-snow", "cloud-sun", "cloud", "cloudy", "clover", "code-2", "code", "codepen", "codesandbox", "coffee", "coins", "columns", "command", "compass", "contact", "contrast", "cookie", "copy", "copyleft", "copyright", "corner-down-left", "corner-down-right", "corner-left-down", "corner-left-up", "corner-right-down", "corner-right-up", "corner-up-left", "corner-up-right", "cpu", "credit-card", "crop", "cross", "crosshair", "crown", "currency", "database", "delete", "dice-1", "dice-2", "dice-3", "dice-4", "dice-5", "dice-6", "disc", "divide-circle", "divide-square", "divide", "dollar-sign", "download-cloud", "download", "dribbble", "droplet", "droplets", "drumstick", "edit-2", "edit-3", "edit", "egg", "equal-not", "equal", "eraser", "euro", "expand", "external-link", "eye-off", "eye", "facebook", "fast-forward", "feather", "figma", "file-check-2", "file-check", "file-code", "file-digit", "file-input", "file-minus-2", "file-minus", "file-output", "file-plus-2", "file-plus", "file-search", "file-text", "file-x-2", "file-x", "file", "files", "film", "filter", "flag-off", "flag-triangle-left", "flag-triangle-right", "flag", "flame", "flashlight-off", "flashlight", "flask-conical", "flask-round", "folder-minus", "folder-open", "folder-plus", "folder", "form-input", "forward", "frame", "framer", "frown", "function-square", "gamepad-2", "gamepad", "gauge", "gavel", "gem", "ghost", "gift", "git-branch-plus", "git-branch", "git-commit", "git-fork", "git-merge", "git-pull-request", "github", "gitlab", "glasses", "globe-2", "globe", "grab", "graduation-cap", "grid", "grip-horizontal", "grip-vertical", "hammer", "hand-metal", "hand", "hard-drive", "hard-hat", "hash", "haze", "headphones", "heart", "help-circle", "hexagon", "highlighter", "history", "home", "image-minus", "image-off", "image-plus", "image", "import", "inbox", "indent", "indian-rupee", "infinity", "info", "inspect", "instagram", "italic", "japanese-yen", "key", "keyboard", "landmark", "languages", "laptop-2", "laptop", "lasso-select", "lasso", "layers", "layout-dashboard", "layout-grid", "layout-list", "layout-template", "layout", "library", "life-buoy", "lightbulb-off", "lightbulb", "link-2-off", "link-2", "link", "linkedin", "list-checks", "list-minus", "list-ordered", "list-plus", "list-x", "list", "loader-2", "loader", "locate-fixed", "locate-off", "locate", "lock", "log-in", "log-out", "mail", "map-pin", "map", "maximize-2", "maximize", "megaphone", "meh", "menu", "message-circle", "message-square", "mic-off", "mic", "minimize-2", "minimize", "minus-circle", "minus-square", "minus", "monitor-off", "monitor-speaker", "monitor", "moon", "more-horizontal", "more-vertical", "mountain-snow", "mountain", "mouse-pointer-2", "mouse-pointer-click", "mouse-pointer", "mouse", "move-diagonal-2", "move-diagonal", "move-horizontal", "move-vertical", "move", "music", "navigation-2", "navigation", "network", "octagon", "option", "outdent", "package-check", "package-minus", "package-plus", "package-search", "package-x", "package", "palette", "palmtree", "paperclip", "pause-circle", "pause-octagon", "pause", "pen-tool", "pencil", "percent", "person-standing", "phone-call", "phone-forwarded", "phone-incoming", "phone-missed", "phone-off", "phone-outgoing", "phone", "pie-chart", "piggy-bank", "pin", "pipette", "plane", "play-circle", "play", "plug-zap", "plus-circle", "plus-square", "plus", "pocket", "podcast", "pointer", "pound-sterling", "power-off", "power", "printer", "qr-code", "quote", "radio-receiver", "radio", "redo", "refresh-ccw", "refresh-cw", "regex", "repeat-1", "repeat", "reply-all", "reply", "rewind", "rocket", "rocking-chair", "rotate-ccw", "rotate-cw", "rss", "ruler", "russian-ruble", "save", "scale", "scan-line", "scan", "scissors", "screen-share-off", "screen-share", "search", "send", "separator-horizontal", "separator-vertical", "server-crash", "server-off", "server", "settings-2", "settings", "share-2", "share", "sheet", "shield-alert", "shield-check", "shield-close", "shield-off", "shield", "shirt", "shopping-bag", "shopping-cart", "shovel", "shrink", "shuffle", "sidebar-close", "sidebar-open", "sidebar", "sigma", "signal-high", "signal-low", "signal-medium", "signal-zero", "signal", "skip-back", "skip-forward", "skull", "slack", "slash", "sliders", "smartphone-charging", "smartphone", "smile", "snowflake", "sort-asc", "sort-desc", "speaker", "sprout", "square", "star-half", "star", "stop-circle", "stretch-horizontal", "stretch-vertical", "strikethrough", "subscript", "sun", "sunrise", "sunset", "superscript", "swiss-franc", "switch-camera", "table", "tablet", "tag", "target", "tent", "terminal-square", "terminal", "text-cursor-input", "text-cursor", "thermometer-snowflake", "thermometer-sun", "thermometer", "thumbs-down", "thumbs-up", "ticket", "timer-off", "timer-reset", "timer", "toggle-left", "toggle-right", "tornado", "trash-2", "trash", "trello", "trending-down", "trending-up", "triangle", "truck", "tv-2", "tv", "twitch", "twitter", "type", "umbrella", "underline", "undo", "unlink-2", "unlink", "unlock", "upload-cloud", "upload", "user-check", "user-minus", "user-plus", "user-x", "user", "users", "verified", "vibrate", "video-off", "video", "view", "voicemail", "volume-1", "volume-2", "volume-x", "volume", "wallet", "wand", "watch", "waves", "webcam", "wifi-off", "wifi", "wind", "wrap-text", "wrench", "x-circle", "x-octagon", "x-square", "x", "youtube", "zap-off", "zap", "zoom-in", "zoom-out", "search-large", "search", "activity", "airplay", "alarm-check", "alarm-clock-off", "alarm-clock", "alarm-minus", "alarm-plus", "album", "alert-circle", "alert-octagon", "alert-triangle", "align-center-horizontal", "align-center-vertical", "align-center", "align-end-horizontal", "align-end-vertical", "align-horizontal-distribute-center", "align-horizontal-distribute-end", "align-horizontal-distribute-start", "align-horizontal-justify-center", "align-horizontal-justify-end", "align-horizontal-justify-start", "align-horizontal-space-around", "align-horizontal-space-between", "align-justify", "align-left", "align-right", "align-start-horizontal", "align-start-vertical", "align-vertical-distribute-center", "align-vertical-distribute-end", "align-vertical-distribute-start", "align-vertical-justify-center", "align-vertical-justify-end", "align-vertical-justify-start", "align-vertical-space-around", "align-vertical-space-between", "anchor", "aperture", "archive", "arrow-big-down", "arrow-big-left", "arrow-big-right", "arrow-big-up", "arrow-down-circle", "arrow-down-left", "arrow-down-right", "arrow-down", "arrow-left-circle", "arrow-left-right", "arrow-left", "arrow-right-circle", "arrow-right", "arrow-up-circle", "arrow-up-left", "arrow-up-right", "arrow-up", "asterisk", "at-sign", "award", "axe", "banknote", "bar-chart-2", "bar-chart", "baseline", "battery-charging", "battery-full", "battery-low", "battery-medium", "battery", "beaker", "bell-minus", "bell-off", "bell-plus", "bell-ring", "bell", "bike", "binary", "bitcoin", "bluetooth-connected", "bluetooth-off", "bluetooth-searching", "bluetooth", "bold", "book-open", "book", "bookmark-minus", "bookmark-plus", "bookmark", "bot", "box-select", "box", "briefcase", "brush", "bug", "building-2", "building", "bus", "calculator", "calendar", "camera-off", "camera", "car", "carrot", "cast", "check-circle-2", "check-circle", "check-square", "check", "chevron-down", "chevron-first", "chevron-last", "chevron-left", "chevron-right", "chevron-up", "chevrons-down-up", "chevrons-down", "chevrons-left", "chevrons-right", "chevrons-up-down", "chevrons-up", "chrome", "circle-slashed", "circle", "clipboard-check", "clipboard-copy", "clipboard-list", "clipboard-x", "clipboard", "clock-1", "clock-10", "clock-11", "clock-12", "clock-2", "clock-3", "clock-4", "clock-5", "clock-6", "clock-7", "clock-8", "clock-9", "lucide-clock", "cloud-drizzle", "cloud-fog", "cloud-hail", "cloud-lightning", "cloud-moon", "cloud-off", "cloud-rain-wind", "cloud-rain", "cloud-snow", "cloud-sun", "lucide-cloud", "cloudy", "clover", "code-2", "code", "codepen", "codesandbox", "coffee", "coins", "columns", "command", "compass", "contact", "contrast", "cookie", "copy", "copyleft", "copyright", "corner-down-left", "corner-down-right", "corner-left-down", "corner-left-up", "corner-right-down", "corner-right-up", "corner-up-left", "corner-up-right", "cpu", "credit-card", "crop", "lucide-cross", "crosshair", "crown", "currency", "database", "delete", "dice-1", "dice-2", "dice-3", "dice-4", "dice-5", "dice-6", "disc", "divide-circle", "divide-square", "divide", "dollar-sign", "download-cloud", "download", "dribbble", "droplet", "droplets", "drumstick", "edit-2", "edit-3", "edit", "egg", "equal-not", "equal", "eraser", "euro", "expand", "external-link", "eye-off", "eye", "facebook", "fast-forward", "feather", "figma", "file-check-2", "file-check", "file-code", "file-digit", "file-input", "file-minus-2", "file-minus", "file-output", "file-plus-2", "file-plus", "file-search", "file-text", "file-x-2", "file-x", "file", "files", "film", "filter", "flag-off", "flag-triangle-left", "flag-triangle-right", "flag", "flame", "flashlight-off", "flashlight", "flask-conical", "flask-round", "folder-minus", "folder-open", "folder-plus", "lucide-folder", "form-input", "forward", "frame", "framer", "frown", "function-square", "gamepad-2", "gamepad", "gauge", "gavel", "gem", "ghost", "gift", "git-branch-plus", "git-branch", "git-commit", "git-fork", "git-merge", "git-pull-request", "github", "gitlab", "glasses", "globe-2", "globe", "grab", "graduation-cap", "grid", "grip-horizontal", "grip-vertical", "hammer", "hand-metal", "hand", "hard-drive", "hard-hat", "hash", "haze", "headphones", "heart", "help-circle", "hexagon", "highlighter", "history", "home", "image-minus", "image-off", "image-plus", "image", "import", "inbox", "indent", "indian-rupee", "infinity", "lucide-info", "inspect", "instagram", "italic", "japanese-yen", "key", "keyboard", "landmark", "lucide-languages", "laptop-2", "laptop", "lasso-select", "lasso", "layers", "layout-dashboard", "layout-grid", "layout-list", "layout-template", "layout", "library", "life-buoy", "lightbulb-off", "lightbulb", "link-2-off", "link-2", "lucide-link", "linkedin", "list-checks", "list-minus", "list-ordered", "list-plus", "list-x", "list", "loader-2", "loader", "locate-fixed", "locate-off", "locate", "lock", "log-in", "log-out", "mail", "map-pin", "map", "maximize-2", "maximize", "megaphone", "meh", "menu", "message-circle", "message-square", "mic-off", "mic", "minimize-2", "minimize", "minus-circle", "minus-square", "minus", "monitor-off", "monitor-speaker", "monitor", "moon", "more-horizontal", "more-vertical", "mountain-snow", "mountain", "mouse-pointer-2", "mouse-pointer-click", "mouse-pointer", "mouse", "move-diagonal-2", "move-diagonal", "move-horizontal", "move-vertical", "move", "music", "navigation-2", "navigation", "network", "octagon", "option", "outdent", "package-check", "package-minus", "package-plus", "package-search", "package-x", "package", "palette", "palmtree", "paperclip", "pause-circle", "pause-octagon", "pause", "pen-tool", "lucide-pencil", "percent", "person-standing", "phone-call", "phone-forwarded", "phone-incoming", "phone-missed", "phone-off", "phone-outgoing", "phone", "pie-chart", "piggy-bank", "lucide-pin", "pipette", "plane", "play-circle", "play", "plug-zap", "plus-circle", "plus-square", "plus", "pocket", "podcast", "pointer", "pound-sterling", "power-off", "power", "printer", "qr-code", "quote", "radio-receiver", "radio", "redo", "refresh-ccw", "refresh-cw", "regex", "repeat-1", "repeat", "reply-all", "reply", "rewind", "rocket", "rocking-chair", "rotate-ccw", "rotate-cw", "rss", "ruler", "russian-ruble", "save", "scale", "scan-line", "scan", "scissors", "screen-share-off", "screen-share", "lucide-search", "send", "separator-horizontal", "separator-vertical", "server-crash", "server-off", "server", "settings-2", "settings", "share-2", "share", "sheet", "shield-alert", "shield-check", "shield-close", "shield-off", "shield", "shirt", "shopping-bag", "shopping-cart", "shovel", "shrink", "shuffle", "sidebar-close", "sidebar-open", "sidebar", "sigma", "signal-high", "signal-low", "signal-medium", "signal-zero", "signal", "skip-back", "skip-forward", "skull", "slack", "slash", "sliders", "smartphone-charging", "smartphone", "smile", "snowflake", "sort-asc", "sort-desc", "speaker", "sprout", "square", "star-half", "lucide-star", "stop-circle", "stretch-horizontal", "stretch-vertical", "strikethrough", "subscript", "sun", "sunrise", "sunset", "superscript", "swiss-franc", "switch-camera", "table", "tablet", "tag", "target", "tent", "terminal-square", "terminal", "text-cursor-input", "text-cursor", "thermometer-snowflake", "thermometer-sun", "thermometer", "thumbs-down", "thumbs-up", "ticket", "timer-off", "timer-reset", "timer", "toggle-left", "toggle-right", "tornado", "trash-2", "lucide-trash", "trello", "trending-down", "trending-up", "triangle", "truck", "tv-2", "tv", "twitch", "twitter", "type", "umbrella", "underline", "undo", "unlink-2", "unlink", "unlock", "upload-cloud", "upload", "user-check", "user-minus", "user-plus", "user-x", "user", "users", "verified", "vibrate", "video-off", "video", "view", "voicemail", "volume-1", "volume-2", "volume-x", "volume", "wallet", "wand", "watch", "waves", "webcam", "wifi-off", "wifi", "wind", "wrap-text", "wrench", "x-circle", "x-octagon", "x-square", "x", "youtube", "zap-off", "zap", "zoom-in", "zoom-out", "search-large", "lucide-search"]); -export const ICON_LIST = Array.from(ICON_LIST_UNIQUE); +export const ICON_LIST = Array.from(ICON_LIST_UNIQUE) as IconType[]; export type IconType = "activity" | "airplay" | "alarm-check" | "alarm-clock-off" | "alarm-clock" | "alarm-minus" | "alarm-plus" | "album" | "alert-circle" | "alert-octagon" | "alert-triangle" | "align-center-horizontal" | "align-center-vertical" | "align-center" | "align-end-horizontal" | "align-end-vertical" | "align-horizontal-distribute-center" | "align-horizontal-distribute-end" | "align-horizontal-distribute-start" | "align-horizontal-justify-center" | "align-horizontal-justify-end" | "align-horizontal-justify-start" | "align-horizontal-space-around" | "align-horizontal-space-between" | "align-justify" | "align-left" | "align-right" | "align-start-horizontal" | "align-start-vertical" | "align-vertical-distribute-center" | "align-vertical-distribute-end" | "align-vertical-distribute-start" | "align-vertical-justify-center" | "align-vertical-justify-end" | "align-vertical-justify-start" | "align-vertical-space-around" | "align-vertical-space-between" | "anchor" | "aperture" | "archive" | "arrow-big-down" | "arrow-big-left" | "arrow-big-right" | "arrow-big-up" | "arrow-down-circle" | "arrow-down-left" | "arrow-down-right" | "arrow-down" | "arrow-left-circle" | "arrow-left-right" | "arrow-left" | "arrow-right-circle" | "arrow-right" | "arrow-up-circle" | "arrow-up-left" | "arrow-up-right" | "arrow-up" | "asterisk" | "at-sign" | "award" | "axe" | "banknote" | "bar-chart-2" | "bar-chart" | "baseline" | "battery-charging" | "battery-full" | "battery-low" | "battery-medium" | "battery" | "beaker" | "bell-minus" | "bell-off" | "bell-plus" | "bell-ring" | "bell" | "bike" | "binary" | "bitcoin" | "bluetooth-connected" | "bluetooth-off" | "bluetooth-searching" | "bluetooth" | "bold" | "book-open" | "book" | "bookmark-minus" | "bookmark-plus" | "bookmark" | "bot" | "box-select" | "box" | "briefcase" | "brush" | "bug" | "building-2" | "building" | "bus" | "calculator" | "calendar" | "camera-off" | "camera" | "car" | "carrot" | "cast" | "check-circle-2" | "check-circle" | "check-square" | "check" | "chevron-down" | "chevron-first" | "chevron-last" | "chevron-left" | "chevron-right" | "chevron-up" | "chevrons-down-up" | "chevrons-down" | "chevrons-left" | "chevrons-right" | "chevrons-up-down" | "chevrons-up" | "chrome" | "circle-slashed" | "circle" | "clipboard-check" | "clipboard-copy" | "clipboard-list" | "clipboard-x" | "clipboard" | "clock-1" | "clock-10" | "clock-11" | "clock-12" | "clock-2" | "clock-3" | "clock-4" | "clock-5" | "clock-6" | "clock-7" | "clock-8" | "clock-9" | "clock" | "cloud-drizzle" | "cloud-fog" | "cloud-hail" | "cloud-lightning" | "cloud-moon" | "cloud-off" | "cloud-rain-wind" | "cloud-rain" | "cloud-snow" | "cloud-sun" | "cloud" | "cloudy" | "clover" | "code-2" | "code" | "codepen" | "codesandbox" | "coffee" | "coins" | "columns" | "command" | "compass" | "contact" | "contrast" | "cookie" | "copy" | "copyleft" | "copyright" | "corner-down-left" | "corner-down-right" | "corner-left-down" | "corner-left-up" | "corner-right-down" | "corner-right-up" | "corner-up-left" | "corner-up-right" | "cpu" | "credit-card" | "crop" | "cross" | "crosshair" | "crown" | "currency" | "database" | "delete" | "dice-1" | "dice-2" | "dice-3" | "dice-4" | "dice-5" | "dice-6" | "disc" | "divide-circle" | "divide-square" | "divide" | "dollar-sign" | "download-cloud" | "download" | "dribbble" | "droplet" | "droplets" | "drumstick" | "edit-2" | "edit-3" | "edit" | "egg" | "equal-not" | "equal" | "eraser" | "euro" | "expand" | "external-link" | "eye-off" | "eye" | "facebook" | "fast-forward" | "feather" | "figma" | "file-check-2" | "file-check" | "file-code" | "file-digit" | "file-input" | "file-minus-2" | "file-minus" | "file-output" | "file-plus-2" | "file-plus" | "file-search" | "file-text" | "file-x-2" | "file-x" | "file" | "files" | "film" | "filter" | "flag-off" | "flag-triangle-left" | "flag-triangle-right" | "flag" | "flame" | "flashlight-off" | "flashlight" | "flask-conical" | "flask-round" | "folder-minus" | "folder-open" | "folder-plus" | "folder" | "form-input" | "forward" | "frame" | "framer" | "frown" | "function-square" | "gamepad-2" | "gamepad" | "gauge" | "gavel" | "gem" | "ghost" | "gift" | "git-branch-plus" | "git-branch" | "git-commit" | "git-fork" | "git-merge" | "git-pull-request" | "github" | "gitlab" | "glasses" | "globe-2" | "globe" | "grab" | "graduation-cap" | "grid" | "grip-horizontal" | "grip-vertical" | "hammer" | "hand-metal" | "hand" | "hard-drive" | "hard-hat" | "hash" | "haze" | "headphones" | "heart" | "help-circle" | "hexagon" | "highlighter" | "history" | "home" | "image-minus" | "image-off" | "image-plus" | "image" | "import" | "inbox" | "indent" | "indian-rupee" | "infinity" | "info" | "inspect" | "instagram" | "italic" | "japanese-yen" | "key" | "keyboard" | "landmark" | "languages" | "laptop-2" | "laptop" | "lasso-select" | "lasso" | "layers" | "layout-dashboard" | "layout-grid" | "layout-list" | "layout-template" | "layout" | "library" | "life-buoy" | "lightbulb-off" | "lightbulb" | "link-2-off" | "link-2" | "link" | "linkedin" | "list-checks" | "list-minus" | "list-ordered" | "list-plus" | "list-x" | "list" | "loader-2" | "loader" | "locate-fixed" | "locate-off" | "locate" | "lock" | "log-in" | "log-out" | "mail" | "map-pin" | "map" | "maximize-2" | "maximize" | "megaphone" | "meh" | "menu" | "message-circle" | "message-square" | "mic-off" | "mic" | "minimize-2" | "minimize" | "minus-circle" | "minus-square" | "minus" | "monitor-off" | "monitor-speaker" | "monitor" | "moon" | "more-horizontal" | "more-vertical" | "mountain-snow" | "mountain" | "mouse-pointer-2" | "mouse-pointer-click" | "mouse-pointer" | "mouse" | "move-diagonal-2" | "move-diagonal" | "move-horizontal" | "move-vertical" | "move" | "music" | "navigation-2" | "navigation" | "network" | "octagon" | "option" | "outdent" | "package-check" | "package-minus" | "package-plus" | "package-search" | "package-x" | "package" | "palette" | "palmtree" | "paperclip" | "pause-circle" | "pause-octagon" | "pause" | "pen-tool" | "pencil" | "percent" | "person-standing" | "phone-call" | "phone-forwarded" | "phone-incoming" | "phone-missed" | "phone-off" | "phone-outgoing" | "phone" | "pie-chart" | "piggy-bank" | "pin" | "pipette" | "plane" | "play-circle" | "play" | "plug-zap" | "plus-circle" | "plus-square" | "plus" | "pocket" | "podcast" | "pointer" | "pound-sterling" | "power-off" | "power" | "printer" | "qr-code" | "quote" | "radio-receiver" | "radio" | "redo" | "refresh-ccw" | "refresh-cw" | "regex" | "repeat-1" | "repeat" | "reply-all" | "reply" | "rewind" | "rocket" | "rocking-chair" | "rotate-ccw" | "rotate-cw" | "rss" | "ruler" | "russian-ruble" | "save" | "scale" | "scan-line" | "scan" | "scissors" | "screen-share-off" | "screen-share" | "search" | "send" | "separator-horizontal" | "separator-vertical" | "server-crash" | "server-off" | "server" | "settings-2" | "settings" | "share-2" | "share" | "sheet" | "shield-alert" | "shield-check" | "shield-close" | "shield-off" | "shield" | "shirt" | "shopping-bag" | "shopping-cart" | "shovel" | "shrink" | "shuffle" | "sidebar-close" | "sidebar-open" | "sidebar" | "sigma" | "signal-high" | "signal-low" | "signal-medium" | "signal-zero" | "signal" | "skip-back" | "skip-forward" | "skull" | "slack" | "slash" | "sliders" | "smartphone-charging" | "smartphone" | "smile" | "snowflake" | "sort-asc" | "sort-desc" | "speaker" | "sprout" | "square" | "star-half" | "star" | "stop-circle" | "stretch-horizontal" | "stretch-vertical" | "strikethrough" | "subscript" | "sun" | "sunrise" | "sunset" | "superscript" | "swiss-franc" | "switch-camera" | "table" | "tablet" | "tag" | "target" | "tent" | "terminal-square" | "terminal" | "text-cursor-input" | "text-cursor" | "thermometer-snowflake" | "thermometer-sun" | "thermometer" | "thumbs-down" | "thumbs-up" | "ticket" | "timer-off" | "timer-reset" | "timer" | "toggle-left" | "toggle-right" | "tornado" | "trash-2" | "trash" | "trello" | "trending-down" | "trending-up" | "triangle" | "truck" | "tv-2" | "tv" | "twitch" | "twitter" | "type" | "umbrella" | "underline" | "undo" | "unlink-2" | "unlink" | "unlock" | "upload-cloud" | "upload" | "user-check" | "user-minus" | "user-plus" | "user-x" | "user" | "users" | "verified" | "vibrate" | "video-off" | "video" | "view" | "voicemail" | "volume-1" | "volume-2" | "volume-x" | "volume" | "wallet" | "wand" | "watch" | "waves" | "webcam" | "wifi-off" | "wifi" | "wind" | "wrap-text" | "wrench" | "x-circle" | "x-octagon" | "x-square" | "x" | "youtube" | "zap-off" | "zap" | "zoom-in" | "zoom-out" | "search-large" | "search" | "activity" | "airplay" | "alarm-check" | "alarm-clock-off" | "alarm-clock" | "alarm-minus" | "alarm-plus" | "album" | "alert-circle" | "alert-octagon" | "alert-triangle" | "align-center-horizontal" | "align-center-vertical" | "align-center" | "align-end-horizontal" | "align-end-vertical" | "align-horizontal-distribute-center" | "align-horizontal-distribute-end" | "align-horizontal-distribute-start" | "align-horizontal-justify-center" | "align-horizontal-justify-end" | "align-horizontal-justify-start" | "align-horizontal-space-around" | "align-horizontal-space-between" | "align-justify" | "align-left" | "align-right" | "align-start-horizontal" | "align-start-vertical" | "align-vertical-distribute-center" | "align-vertical-distribute-end" | "align-vertical-distribute-start" | "align-vertical-justify-center" | "align-vertical-justify-end" | "align-vertical-justify-start" | "align-vertical-space-around" | "align-vertical-space-between" | "anchor" | "aperture" | "archive" | "arrow-big-down" | "arrow-big-left" | "arrow-big-right" | "arrow-big-up" | "arrow-down-circle" | "arrow-down-left" | "arrow-down-right" | "arrow-down" | "arrow-left-circle" | "arrow-left-right" | "arrow-left" | "arrow-right-circle" | "arrow-right" | "arrow-up-circle" | "arrow-up-left" | "arrow-up-right" | "arrow-up" | "asterisk" | "at-sign" | "award" | "axe" | "banknote" | "bar-chart-2" | "bar-chart" | "baseline" | "battery-charging" | "battery-full" | "battery-low" | "battery-medium" | "battery" | "beaker" | "bell-minus" | "bell-off" | "bell-plus" | "bell-ring" | "bell" | "bike" | "binary" | "bitcoin" | "bluetooth-connected" | "bluetooth-off" | "bluetooth-searching" | "bluetooth" | "bold" | "book-open" | "book" | "bookmark-minus" | "bookmark-plus" | "bookmark" | "bot" | "box-select" | "box" | "briefcase" | "brush" | "bug" | "building-2" | "building" | "bus" | "calculator" | "calendar" | "camera-off" | "camera" | "car" | "carrot" | "cast" | "check-circle-2" | "check-circle" | "check-square" | "check" | "chevron-down" | "chevron-first" | "chevron-last" | "chevron-left" | "chevron-right" | "chevron-up" | "chevrons-down-up" | "chevrons-down" | "chevrons-left" | "chevrons-right" | "chevrons-up-down" | "chevrons-up" | "chrome" | "circle-slashed" | "circle" | "clipboard-check" | "clipboard-copy" | "clipboard-list" | "clipboard-x" | "clipboard" | "clock-1" | "clock-10" | "clock-11" | "clock-12" | "clock-2" | "clock-3" | "clock-4" | "clock-5" | "clock-6" | "clock-7" | "clock-8" | "clock-9" | "lucide-clock" | "cloud-drizzle" | "cloud-fog" | "cloud-hail" | "cloud-lightning" | "cloud-moon" | "cloud-off" | "cloud-rain-wind" | "cloud-rain" | "cloud-snow" | "cloud-sun" | "lucide-cloud" | "cloudy" | "clover" | "code-2" | "code" | "codepen" | "codesandbox" | "coffee" | "coins" | "columns" | "command" | "compass" | "contact" | "contrast" | "cookie" | "copy" | "copyleft" | "copyright" | "corner-down-left" | "corner-down-right" | "corner-left-down" | "corner-left-up" | "corner-right-down" | "corner-right-up" | "corner-up-left" | "corner-up-right" | "cpu" | "credit-card" | "crop" | "lucide-cross" | "crosshair" | "crown" | "currency" | "database" | "delete" | "dice-1" | "dice-2" | "dice-3" | "dice-4" | "dice-5" | "dice-6" | "disc" | "divide-circle" | "divide-square" | "divide" | "dollar-sign" | "download-cloud" | "download" | "dribbble" | "droplet" | "droplets" | "drumstick" | "edit-2" | "edit-3" | "edit" | "egg" | "equal-not" | "equal" | "eraser" | "euro" | "expand" | "external-link" | "eye-off" | "eye" | "facebook" | "fast-forward" | "feather" | "figma" | "file-check-2" | "file-check" | "file-code" | "file-digit" | "file-input" | "file-minus-2" | "file-minus" | "file-output" | "file-plus-2" | "file-plus" | "file-search" | "file-text" | "file-x-2" | "file-x" | "file" | "files" | "film" | "filter" | "flag-off" | "flag-triangle-left" | "flag-triangle-right" | "flag" | "flame" | "flashlight-off" | "flashlight" | "flask-conical" | "flask-round" | "folder-minus" | "folder-open" | "folder-plus" | "lucide-folder" | "form-input" | "forward" | "frame" | "framer" | "frown" | "function-square" | "gamepad-2" | "gamepad" | "gauge" | "gavel" | "gem" | "ghost" | "gift" | "git-branch-plus" | "git-branch" | "git-commit" | "git-fork" | "git-merge" | "git-pull-request" | "github" | "gitlab" | "glasses" | "globe-2" | "globe" | "grab" | "graduation-cap" | "grid" | "grip-horizontal" | "grip-vertical" | "hammer" | "hand-metal" | "hand" | "hard-drive" | "hard-hat" | "hash" | "haze" | "headphones" | "heart" | "help-circle" | "hexagon" | "highlighter" | "history" | "home" | "image-minus" | "image-off" | "image-plus" | "image" | "import" | "inbox" | "indent" | "indian-rupee" | "infinity" | "lucide-info" | "inspect" | "instagram" | "italic" | "japanese-yen" | "key" | "keyboard" | "landmark" | "lucide-languages" | "laptop-2" | "laptop" | "lasso-select" | "lasso" | "layers" | "layout-dashboard" | "layout-grid" | "layout-list" | "layout-template" | "layout" | "library" | "life-buoy" | "lightbulb-off" | "lightbulb" | "link-2-off" | "link-2" | "lucide-link" | "linkedin" | "list-checks" | "list-minus" | "list-ordered" | "list-plus" | "list-x" | "list" | "loader-2" | "loader" | "locate-fixed" | "locate-off" | "locate" | "lock" | "log-in" | "log-out" | "mail" | "map-pin" | "map" | "maximize-2" | "maximize" | "megaphone" | "meh" | "menu" | "message-circle" | "message-square" | "mic-off" | "mic" | "minimize-2" | "minimize" | "minus-circle" | "minus-square" | "minus" | "monitor-off" | "monitor-speaker" | "monitor" | "moon" | "more-horizontal" | "more-vertical" | "mountain-snow" | "mountain" | "mouse-pointer-2" | "mouse-pointer-click" | "mouse-pointer" | "mouse" | "move-diagonal-2" | "move-diagonal" | "move-horizontal" | "move-vertical" | "move" | "music" | "navigation-2" | "navigation" | "network" | "octagon" | "option" | "outdent" | "package-check" | "package-minus" | "package-plus" | "package-search" | "package-x" | "package" | "palette" | "palmtree" | "paperclip" | "pause-circle" | "pause-octagon" | "pause" | "pen-tool" | "lucide-pencil" | "percent" | "person-standing" | "phone-call" | "phone-forwarded" | "phone-incoming" | "phone-missed" | "phone-off" | "phone-outgoing" | "phone" | "pie-chart" | "piggy-bank" | "lucide-pin" | "pipette" | "plane" | "play-circle" | "play" | "plug-zap" | "plus-circle" | "plus-square" | "plus" | "pocket" | "podcast" | "pointer" | "pound-sterling" | "power-off" | "power" | "printer" | "qr-code" | "quote" | "radio-receiver" | "radio" | "redo" | "refresh-ccw" | "refresh-cw" | "regex" | "repeat-1" | "repeat" | "reply-all" | "reply" | "rewind" | "rocket" | "rocking-chair" | "rotate-ccw" | "rotate-cw" | "rss" | "ruler" | "russian-ruble" | "save" | "scale" | "scan-line" | "scan" | "scissors" | "screen-share-off" | "screen-share" | "lucide-search" | "send" | "separator-horizontal" | "separator-vertical" | "server-crash" | "server-off" | "server" | "settings-2" | "settings" | "share-2" | "share" | "sheet" | "shield-alert" | "shield-check" | "shield-close" | "shield-off" | "shield" | "shirt" | "shopping-bag" | "shopping-cart" | "shovel" | "shrink" | "shuffle" | "sidebar-close" | "sidebar-open" | "sidebar" | "sigma" | "signal-high" | "signal-low" | "signal-medium" | "signal-zero" | "signal" | "skip-back" | "skip-forward" | "skull" | "slack" | "slash" | "sliders" | "smartphone-charging" | "smartphone" | "smile" | "snowflake" | "sort-asc" | "sort-desc" | "speaker" | "sprout" | "square" | "star-half" | "lucide-star" | "stop-circle" | "stretch-horizontal" | "stretch-vertical" | "strikethrough" | "subscript" | "sun" | "sunrise" | "sunset" | "superscript" | "swiss-franc" | "switch-camera" | "table" | "tablet" | "tag" | "target" | "tent" | "terminal-square" | "terminal" | "text-cursor-input" | "text-cursor" | "thermometer-snowflake" | "thermometer-sun" | "thermometer" | "thumbs-down" | "thumbs-up" | "ticket" | "timer-off" | "timer-reset" | "timer" | "toggle-left" | "toggle-right" | "tornado" | "trash-2" | "lucide-trash" | "trello" | "trending-down" | "trending-up" | "triangle" | "truck" | "tv-2" | "tv" | "twitch" | "twitter" | "type" | "umbrella" | "underline" | "undo" | "unlink-2" | "unlink" | "unlock" | "upload-cloud" | "upload" | "user-check" | "user-minus" | "user-plus" | "user-x" | "user" | "users" | "verified" | "vibrate" | "video-off" | "video" | "view" | "voicemail" | "volume-1" | "volume-2" | "volume-x" | "volume" | "wallet" | "wand" | "watch" | "waves" | "webcam" | "wifi-off" | "wifi" | "wind" | "wrap-text" | "wrench" | "x-circle" | "x-octagon" | "x-square" | "x" | "youtube" | "zap-off" | "zap" | "zoom-in" | "zoom-out" | "search-large" | "lucide-search"; diff --git a/src/ui/PodcastView/PodcastView.integration.test.ts b/src/ui/PodcastView/PodcastView.integration.test.ts new file mode 100644 index 00000000..229f9ab2 --- /dev/null +++ b/src/ui/PodcastView/PodcastView.integration.test.ts @@ -0,0 +1,143 @@ +import { fireEvent, render, screen } from "@testing-library/svelte"; +import { get } from "svelte/store"; +import { + afterEach, + beforeEach, + describe, + expect, + test, + vi, +} from "vitest"; + +import createPodcastNote from "src/createPodcastNote"; +import { + currentEpisode, + episodeCache, + plugin, + savedFeeds, + viewState, +} from "src/store"; +import type { Episode } from "src/types/Episode"; +import type { PodcastFeed } from "src/types/PodcastFeed"; +import { ViewState } from "src/types/ViewState"; +import EpisodePlayerStub from "../../../tests/mocks/EpisodePlayerStub.svelte"; + +vi.mock("./EpisodePlayer.svelte", () => ({ + default: EpisodePlayerStub, +})); + +import PodcastView from "./PodcastView.svelte"; + +const mockGetEpisodes = vi.fn<() => Promise>(); + +vi.mock("src/parser/feedParser", () => { + return { + default: class { + async getEpisodes() { + return mockGetEpisodes(); + } + }, + }; +}); + +const testFeed: PodcastFeed = { + title: "Test Podcast", + url: "https://pod.example.com/feed.xml", + artworkUrl: "https://pod.example.com/art.jpg", +}; + +const testEpisode: Episode = { + title: "Episode 1: Launch", + streamUrl: "https://pod.example.com/audio.mp3", + url: "https://pod.example.com/episode-1", + description: "Episode description", + content: "

Episode content

", + podcastName: testFeed.title, + artworkUrl: testFeed.artworkUrl, + episodeDate: new Date("2024-01-15T00:00:00.000Z"), +}; + +function resetStores() { + savedFeeds.set({}); + episodeCache.set({}); + viewState.set(ViewState.PodcastGrid); + currentEpisode.update(() => undefined as unknown as Episode); + plugin.set(undefined as never); +} + +beforeEach(() => { + resetStores(); + savedFeeds.set({ [testFeed.title]: testFeed }); + mockGetEpisodes.mockResolvedValue([testEpisode]); +}); + +afterEach(() => { + resetStores(); + mockGetEpisodes.mockClear(); + delete (globalThis as Record).app; +}); + +function bootstrapAppMock() { + const createdFiles: Array<{ path: string; data: string }> = []; + const leaf = { openFile: vi.fn() }; + const appMock = { + vault: { + getAbstractFileByPath: vi.fn(() => null), + createFolder: vi.fn(async () => {}), + create: vi.fn(async (path: string, data: string) => { + const file = { path, data }; + createdFiles.push(file); + return file; + }), + }, + workspace: { + getLeaf: vi.fn(() => leaf), + }, + }; + + (globalThis as { app?: typeof appMock }).app = appMock; + + return { appMock, createdFiles, leaf }; +} + +describe("PodcastView integration flow", () => { + test("select feed → play episode → create note with resolved template", async () => { + const { createdFiles, leaf } = bootstrapAppMock(); + + const pluginSettings = { + settings: { + note: { + path: "Podcasts/{{podcast}}/{{date}}/{{title}}", + template: "# {{title}}\n{{description}}\nStream: {{stream}}", + }, + }, + }; + plugin.set(pluginSettings as never); + + render(PodcastView); + + const feedImage = await screen.findByAltText(testFeed.title); + await fireEvent.click(feedImage); + + const episodeNode = await screen.findByText(testEpisode.title); + await fireEvent.click(episodeNode); + + expect(get(currentEpisode)).toMatchObject({ title: testEpisode.title }); + expect(get(viewState)).toBe(ViewState.Player); + + const current = get(currentEpisode); + expect(current).toBeDefined(); + await createPodcastNote(current as Episode); + + const expectedPath = + "Podcasts/Test Podcast/2024-01-15/Episode 1 Launch.md"; + + expect(createdFiles[0].path).toBe(expectedPath); + expect(createdFiles[0].data).toBe( + "# Episode 1: Launch\nEpisode description\nStream: https://pod.example.com/audio.mp3", + ); + expect(leaf.openFile).toHaveBeenCalledWith( + expect.objectContaining({ path: expectedPath }), + ); + }); +}); diff --git a/src/ui/PodcastView/PodcastView.svelte b/src/ui/PodcastView/PodcastView.svelte index 9c403a57..379c4c39 100644 --- a/src/ui/PodcastView/PodcastView.svelte +++ b/src/ui/PodcastView/PodcastView.svelte @@ -34,39 +34,41 @@ let displayedPlaylists: Playlist[] = []; let latestEpisodes: Episode[] = []; - onMount(async () => { - const unsubscribePlaylists = playlists.subscribe((pl) => { - displayedPlaylists = [$queue, $favorites, $localFiles, ...Object.values(pl)]; - }); +onMount(() => { + const unsubscribePlaylists = playlists.subscribe((pl) => { + displayedPlaylists = [$queue, $favorites, $localFiles, ...Object.values(pl)]; + }); - const unsubscribeSavedFeeds = savedFeeds.subscribe((storeValue) => { - feeds = Object.values(storeValue); - }); + const unsubscribeSavedFeeds = savedFeeds.subscribe((storeValue) => { + feeds = Object.values(storeValue); + }); - await fetchEpisodesInAllFeeds(feeds); + const unsubscribeEpisodeCache = episodeCache.subscribe((cache) => { + latestEpisodes = Object.entries(cache) + .map(([_, episodes]) => episodes.slice(0, 10)) + .flat() + .sort((a, b) => { + if (a.episodeDate && b.episodeDate) + return Number(b.episodeDate) - Number(a.episodeDate); - const unsubscribeEpisodeCache = episodeCache.subscribe((cache) => { - latestEpisodes = Object.entries(cache) - .map(([_, episodes]) => episodes.slice(0, 10)) - .flat() - .sort((a, b) => { - if (a.episodeDate && b.episodeDate) - return Number(b.episodeDate) - Number(a.episodeDate); + return 0; + }); + }); - return 0; - }); - }); + (async () => { + await fetchEpisodesInAllFeeds(feeds); if (!selectedFeed) { displayedEpisodes = latestEpisodes; } - - return () => { - unsubscribeEpisodeCache(); - unsubscribeSavedFeeds(); - unsubscribePlaylists(); - }; - }); + })(); + + return () => { + unsubscribeEpisodeCache(); + unsubscribeSavedFeeds(); + unsubscribePlaylists(); + }; +}); async function fetchEpisodes( feed: PodcastFeed, diff --git a/src/ui/PodcastView/TopBar.svelte b/src/ui/PodcastView/TopBar.svelte index 0b89d59f..d43061a9 100644 --- a/src/ui/PodcastView/TopBar.svelte +++ b/src/ui/PodcastView/TopBar.svelte @@ -47,7 +47,7 @@ ${canShowEpisodeList ? "topbar-selectable" : ""} `} role="button" - tabindex={canShowEpisodeList ? "0" : "-1"} + tabindex={canShowEpisodeList ? 0 : -1} aria-label="Episode list" > @@ -61,7 +61,7 @@ ${canShowPlayer ? "topbar-selectable" : ""} `} role="button" - tabindex={canShowPlayer ? "0" : "-1"} + tabindex={canShowPlayer ? 0 : -1} aria-label="Player" > diff --git a/src/ui/PodcastView/TopBar.test.ts b/src/ui/PodcastView/TopBar.test.ts new file mode 100644 index 00000000..fa94777c --- /dev/null +++ b/src/ui/PodcastView/TopBar.test.ts @@ -0,0 +1,70 @@ +import { fireEvent, render } from "@testing-library/svelte"; +import { describe, expect, test } from "vitest"; + +import { ViewState } from "src/types/ViewState"; +import TopBar from "./TopBar.svelte"; + +describe("TopBar", () => { + test("matches snapshot for default state", () => { + const { container } = render(TopBar, { + props: { + viewState: ViewState.PodcastGrid, + canShowEpisodeList: true, + canShowPlayer: false, + }, + }); + + expect(container.firstChild).toMatchSnapshot(); + }); + + test("activates episode list when clicked", async () => { + const { getByLabelText } = render(TopBar, { + props: { + viewState: ViewState.PodcastGrid, + canShowEpisodeList: true, + canShowPlayer: false, + }, + }); + + const episodeButton = getByLabelText("Episode list"); + const playerButton = getByLabelText("Player"); + + await fireEvent.click(episodeButton); + + expect(episodeButton.className).toContain("topbar-selected"); + expect(playerButton.className).not.toContain("topbar-selected"); + }); + + test("keeps controls unfocusable when view is unavailable", () => { + const { getByLabelText } = render(TopBar, { + props: { + viewState: ViewState.PodcastGrid, + canShowEpisodeList: false, + canShowPlayer: false, + }, + }); + + const episodeButton = getByLabelText("Episode list"); + const playerButton = getByLabelText("Player"); + + expect(episodeButton).toHaveAttribute("tabindex", "-1"); + expect(playerButton).toHaveAttribute("tabindex", "-1"); + expect(episodeButton.className).not.toContain("topbar-selectable"); + }); + + test("responds to keyboard activation for player control", async () => { + const { getByLabelText } = render(TopBar, { + props: { + viewState: ViewState.EpisodeList, + canShowEpisodeList: true, + canShowPlayer: true, + }, + }); + + const playerButton = getByLabelText("Player"); + + await fireEvent.keyDown(playerButton, { key: "Enter" }); + + expect(playerButton.className).toContain("topbar-selected"); + }); +}); diff --git a/src/ui/common/Image.test.ts b/src/ui/common/Image.test.ts new file mode 100644 index 00000000..120309f7 --- /dev/null +++ b/src/ui/common/Image.test.ts @@ -0,0 +1,60 @@ +import { fireEvent, render, screen } from "@testing-library/svelte"; +import { tick } from "svelte"; +import { describe, expect, test, vi } from "vitest"; + +import Image from "./Image.svelte"; +import ImageFallbackHarness from "./__tests__/ImageFallbackHarness.svelte"; + +const baseProps = { + src: "https://example.com/art.jpg", + alt: "Example artwork", + fadeIn: true, +}; + +describe("Image component", () => { + test("renders snapshot of the image button", () => { + const { container } = render(Image, { props: baseProps }); + + expect(container.firstChild).toMatchSnapshot(); + }); + + test("bubbles click events through the dispatcher", async () => { + const handler = vi.fn(); + const { getByRole } = render(Image, { + props: baseProps, + events: { + click: handler, + }, + }); + await fireEvent.click(getByRole("button")); + + expect(handler).toHaveBeenCalled(); + expect(handler.mock.calls[0][0].detail.event).toBeInstanceOf(MouseEvent); + }); + + test("fades image in once it has loaded", async () => { + const { getByRole } = render(Image, { props: baseProps }); + const img = getByRole("img") as HTMLImageElement; + + expect(img.style.opacity).toBe("0"); + + await fireEvent.load(img); + await tick(); + + expect(img.style.opacity).toBe("1"); + }); + + test("shows fallback slot when loading fails", async () => { + const { getByRole } = render(ImageFallbackHarness, { + props: { + props: baseProps, + }, + }); + const img = getByRole("img"); + + await fireEvent.error(img); + await tick(); + + expect(screen.getByText("placeholder")).toBeVisible(); + }); +}); diff --git a/src/ui/common/Progressbar.test.ts b/src/ui/common/Progressbar.test.ts index 2e4d88b8..e90fd44c 100644 --- a/src/ui/common/Progressbar.test.ts +++ b/src/ui/common/Progressbar.test.ts @@ -1,10 +1,58 @@ -import '@testing-library/jest-dom'; -import { render } from '@testing-library/svelte'; +import { fireEvent, render } from "@testing-library/svelte"; +import { describe, expect, test, vi } from "vitest"; -import Progressbar from './Progressbar.svelte'; +import Progressbar from "./Progressbar.svelte"; -test('should render', () => { - const { container } = render(Progressbar, { props: { value: 0, max: 100}}); +describe("Progressbar", () => { + test("renders an accessible slider snapshot", () => { + const { container, getByRole } = render(Progressbar, { + props: { value: 25, max: 200 }, + }); - expect(container).toBeVisible(); -}); \ No newline at end of file + const slider = getByRole("slider"); + + expect(slider).toHaveAttribute("aria-valuenow", "25"); + expect(slider).toHaveAttribute("aria-valuemax", "200"); + expect(container.firstChild).toMatchSnapshot(); + }); + + test("announces percent changes from keyboard interactions", async () => { + const handler = vi.fn(); + const { getByRole } = render(Progressbar, { + props: { value: 40, max: 200 }, + events: { + click: handler, + }, + }); + const slider = getByRole("slider"); + + await fireEvent.keyDown(slider, { key: "ArrowRight" }); + await fireEvent.keyDown(slider, { key: "Home" }); + + const [incrementEvent, homeEvent] = handler.mock.calls.map( + (call) => call[0].detail, + ); + + expect(incrementEvent.percent).toBeCloseTo(0.25, 5); + expect(homeEvent.percent).toBe(0); + }); + + test("fires drag events while mouse is down", async () => { + const handler = vi.fn(); + const { getByRole } = render(Progressbar, { + props: { value: 30, max: 100 }, + events: { + click: handler, + }, + }); + const slider = getByRole("slider"); + + await fireEvent.mouseDown(slider); + await fireEvent.mouseMove(slider); + await fireEvent.mouseUp(slider); + + expect(handler).toHaveBeenCalled(); + expect(handler.mock.calls[0][0].detail.percent).toBeUndefined(); + expect(handler.mock.calls[0][0].detail.event).toBeInstanceOf(MouseEvent); + }); +}); diff --git a/src/ui/common/__tests__/ImageFallbackHarness.svelte b/src/ui/common/__tests__/ImageFallbackHarness.svelte new file mode 100644 index 00000000..d2be7434 --- /dev/null +++ b/src/ui/common/__tests__/ImageFallbackHarness.svelte @@ -0,0 +1,14 @@ + + + + placeholder + diff --git a/src/ui/obsidian/Button.svelte b/src/ui/obsidian/Button.svelte index 14b951c1..2fc86067 100644 --- a/src/ui/obsidian/Button.svelte +++ b/src/ui/obsidian/Button.svelte @@ -7,6 +7,7 @@ export let text: string = ""; export let tooltip: string = ""; + export let ariaLabel: string = ""; export let icon: IconType | undefined = undefined; export let disabled: boolean = false; export let warning: boolean = false; @@ -47,6 +48,12 @@ if (styles) { btn.buttonEl.setAttr('style', extractStylesFromObj(styles)); } + + if (ariaLabel) { + btn.buttonEl.setAttr("aria-label", ariaLabel); + } else { + btn.buttonEl.removeAttribute("aria-label"); + } } diff --git a/src/ui/obsidian/Icon.svelte b/src/ui/obsidian/Icon.svelte index 96b3941f..f2448d00 100644 --- a/src/ui/obsidian/Icon.svelte +++ b/src/ui/obsidian/Icon.svelte @@ -20,15 +20,25 @@ const dispatch = createEventDispatcher(); onMount(() => { - setIcon(ref, icon, size); - ref.style.cssText = stylesStr; + setIcon(ref, icon); + applyIconStyles(); }); afterUpdate(() => { - setIcon(ref, icon, size); - ref.style.cssText = stylesStr; + setIcon(ref, icon); + applyIconStyles(); }); + function applyIconStyles() { + if (!ref) return; + ref.style.cssText = stylesStr; + + if (size) { + ref.style.width = `${size}px`; + ref.style.height = `${size}px`; + } + } + function forwardClick(event: MouseEvent) { if (!clickable) return; dispatch("click", { event }); @@ -40,7 +50,7 @@ class={clickable ? "icon-clickable" : ""} aria-label={label} role="button" - tabindex={clickable ? "0" : "-1"} + tabindex={clickable ? 0 : -1} on:keydown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); diff --git a/src/ui/obsidian/Text.svelte b/src/ui/obsidian/Text.svelte index 82a50359..ecd4bbb3 100644 --- a/src/ui/obsidian/Text.svelte +++ b/src/ui/obsidian/Text.svelte @@ -8,6 +8,7 @@ export let disabled: boolean = false; export let placeholder: string = ""; export let type: "text" | "password" | "email" | "number" | "tel" | "url" = "text"; + export let el: HTMLInputElement | null = null; export { styles as style }; let textRef: HTMLSpanElement; @@ -35,6 +36,9 @@ if (styles) { text.inputEl.setAttr("style", extractStylesFromObj(styles)); } + if (component?.inputEl) { + el = component.inputEl; + } component.onChange((newValue: string) => { value = newValue; diff --git a/src/ui/settings/PodcastResultCard.svelte b/src/ui/settings/PodcastResultCard.svelte index b2b2b36a..e00d1ead 100644 --- a/src/ui/settings/PodcastResultCard.svelte +++ b/src/ui/settings/PodcastResultCard.svelte @@ -25,13 +25,13 @@ {#if isSaved}