From 391dc3fe97ffbb92032775d21d469ad1738f9839 Mon Sep 17 00:00:00 2001 From: Roku Date: Thu, 2 Apr 2026 20:31:07 -0500 Subject: [PATCH 1/6] Curate explorer: tag consolidation, spam exclusion, weekly rebuild - Config-driven tag aliases, implicit tag hiding, and repo exclusion - Stronger safety warning with link to report suspicious repos - Weekly CI rebuild + manual trigger for fast spam response --- .github/workflows/deploy-ghpages.yml | 4 ++ plugins/repo-data-omit-list.json | 18 ++++- plugins/repo-data-plugin.js | 18 ++++- src/components/RepoExplorer/index.tsx | 97 +++++++++++++++++++-------- src/pages/explorer.tsx | 12 +++- 5 files changed, 118 insertions(+), 31 deletions(-) diff --git a/.github/workflows/deploy-ghpages.yml b/.github/workflows/deploy-ghpages.yml index 13899a7c..ed22f155 100644 --- a/.github/workflows/deploy-ghpages.yml +++ b/.github/workflows/deploy-ghpages.yml @@ -5,6 +5,10 @@ on: branches: - main - emily/gh-pages-deploy + schedule: + # Rebuild weekly (Saturday night / Sunday 00:00 UTC) + - cron: "0 0 * * 0" + workflow_dispatch: # Allow manual trigger jobs: build_and_deploy: diff --git a/plugins/repo-data-omit-list.json b/plugins/repo-data-omit-list.json index eaf34965..e21964e4 100644 --- a/plugins/repo-data-omit-list.json +++ b/plugins/repo-data-omit-list.json @@ -1,4 +1,18 @@ { - "description": "List of repositories to exclude from the Repository Explorer. Add repository full names (e.g., 'owner/repo-name') to the omitRepos array.", - "omitRepos": ["voqal/voqal"] + "description": "Configuration for Repository Explorer exclusions and tag normalization.", + "omitRepos": ["voqal/voqal", "Jailsonfs/community"], + "implicitTags": ["talonvoice", "talon", "voice", "voice-recognition", "speech-recognition", "voice-commands", "voice-control", "voice-dictation", "dictation"], + "tagAliases": { + "a11y": "accessibility", + "macos-accessibility": "accessibility", + "maths": "math", + "python3": "python", + "hci": "human-computer-interaction", + "games": "game", + "gpt": "ai", + "openai": "ai", + "chatgpt": "ai", + "llm": "ai", + "copilot": "ai" + } } diff --git a/plugins/repo-data-plugin.js b/plugins/repo-data-plugin.js index e3f2800c..f4075c75 100644 --- a/plugins/repo-data-plugin.js +++ b/plugins/repo-data-plugin.js @@ -11,8 +11,10 @@ module.exports = function (context, options) { "../.docusaurus/repo-data-plugin/default/repos.json", ); - // Load omit configuration from repo-explorer-omit-list.json + // Load omit configuration from repo-data-omit-list.json let omitRepos = []; + let implicitTags = []; + let tagAliases = {}; try { const omitListFile = path.join(__dirname, "repo-data-omit-list.json"); @@ -21,6 +23,12 @@ module.exports = function (context, options) { if (omitConfig.omitRepos && Array.isArray(omitConfig.omitRepos)) { omitRepos = omitConfig.omitRepos; } + if (omitConfig.implicitTags && Array.isArray(omitConfig.implicitTags)) { + implicitTags = omitConfig.implicitTags; + } + if (omitConfig.tagAliases && typeof omitConfig.tagAliases === "object") { + tagAliases = omitConfig.tagAliases; + } } } catch (error) { console.warn("Failed to load repo-data-omit-list.json:", error.message); @@ -80,6 +88,8 @@ module.exports = function (context, options) { filtered_count: filteredRepos.length, omitted_count: (cachedData.repositories.length || 0) - filteredRepos.length, + implicitTags, + tagAliases, }; } } catch (error) { @@ -163,6 +173,8 @@ module.exports = function (context, options) { filtered_count: filteredRepos.length, omitted_count: allRepos.length - filteredRepos.length, generated_at: new Date().toISOString(), + implicitTags, + tagAliases, }; } catch (error) { console.error("Failed to fetch repository data:", error); @@ -186,6 +198,8 @@ module.exports = function (context, options) { omitted_count: (cachedData.repositories.length || 0) - filteredRepos.length, error: `Build-time fetch failed: ${error.message}. Using cached data.`, + implicitTags, + tagAliases, }; } } catch (cacheError) { @@ -198,6 +212,8 @@ module.exports = function (context, options) { total_count: 0, generated_at: new Date().toISOString(), error: error.message, + implicitTags, + tagAliases, }; } }, diff --git a/src/components/RepoExplorer/index.tsx b/src/components/RepoExplorer/index.tsx index a78fe5ea..7effaac6 100644 --- a/src/components/RepoExplorer/index.tsx +++ b/src/components/RepoExplorer/index.tsx @@ -25,6 +25,8 @@ interface RepoData { total_count: number; generated_at: string; error?: string; + implicitTags?: string[]; + tagAliases?: Record; } const RepoExplorer: React.FC = () => { @@ -37,6 +39,8 @@ const RepoExplorer: React.FC = () => { const [sortBy, setSortBy] = useState<"stars" | "updated" | "name">("stars"); const [viewMode, setViewMode] = useState<"compact" | "expanded">("compact"); const [showAllTags, setShowAllTags] = useState(false); + const [implicitTags, setOmitTags] = useState(["talonvoice", "talon"]); + const [tagAliases, setTagAliases] = useState>({}); useEffect(() => { // Load data from build-time generated JSON @@ -47,6 +51,8 @@ const RepoExplorer: React.FC = () => { setError(`Build-time data fetch failed: ${data.error}`); } else { setRepos(data.repositories); + if (data.implicitTags) setOmitTags(data.implicitTags); + if (data.tagAliases) setTagAliases(data.tagAliases); console.log( `Loaded ${data.repositories.length} repositories (generated at ${data.generated_at})`, ); @@ -59,6 +65,27 @@ const RepoExplorer: React.FC = () => { } }, []); + // Resolve a tag to its canonical form using tagAliases + const resolveTag = React.useCallback( + (tag: string): string => tagAliases[tag] || tag, + [tagAliases], + ); + + // Build a reverse map: canonical tag -> all original tags that map to it + const canonicalToOriginals = React.useMemo(() => { + const map = new Map>(); + repos.forEach((repo) => { + repo.topics + .filter((topic) => !implicitTags.includes(topic)) + .forEach((topic) => { + const canonical = resolveTag(topic); + if (!map.has(canonical)) map.set(canonical, new Set()); + map.get(canonical)!.add(topic); + }); + }); + return map; + }, [repos, implicitTags, resolveTag]); + const filteredAndSortedRepos = React.useMemo(() => { let filtered = repos.filter((repo) => { const matchesSearch = @@ -70,7 +97,12 @@ const RepoExplorer: React.FC = () => { selectedLanguage === "all" || repo.language === selectedLanguage; const matchesTags = selectedTags.length === 0 || - selectedTags.some((tag) => repo.topics.includes(tag)); + selectedTags.some((tag) => { + const originals = canonicalToOriginals.get(tag); + return originals + ? repo.topics.some((t) => originals.has(t)) + : repo.topics.includes(tag); + }); return matchesSearch && matchesLanguage && matchesTags; }); // Sort repositories filtered.sort((a, b) => { @@ -89,7 +121,7 @@ const RepoExplorer: React.FC = () => { }); return filtered; - }, [repos, searchTerm, selectedLanguage, selectedTags, sortBy]); + }, [repos, searchTerm, selectedLanguage, selectedTags, sortBy, canonicalToOriginals]); const languages = React.useMemo(() => { return Array.from( @@ -97,21 +129,26 @@ const RepoExplorer: React.FC = () => { ).sort(); }, [repos]); - // Get all unique tags with counts, excluding 'talonvoice' and 'talon' + // Get all unique tags with counts, using canonical names const tagStats = React.useMemo(() => { const tagCounts = new Map(); repos.forEach((repo) => { + const seen = new Set(); repo.topics - .filter((topic) => topic !== "talonvoice" && topic !== "talon") + .filter((topic) => !implicitTags.includes(topic)) .forEach((topic) => { - tagCounts.set(topic, (tagCounts.get(topic) || 0) + 1); + const canonical = resolveTag(topic); + if (!seen.has(canonical)) { + seen.add(canonical); + tagCounts.set(canonical, (tagCounts.get(canonical) || 0) + 1); + } }); }); return Array.from(tagCounts.entries()) .map(([tag, count]) => ({ tag, count })) - .sort((a, b) => b.count - a.count); // Sort by count descending - }, [repos]); + .sort((a, b) => b.count - a.count); + }, [repos, implicitTags, resolveTag]); const toggleTag = (tag: string) => { setSelectedTags((prev) => @@ -366,40 +403,43 @@ const RepoExplorer: React.FC = () => { {" "} {repo.topics.filter( - (topic) => topic !== "talonvoice" && topic !== "talon", + (topic) => !implicitTags.includes(topic), ).length > 0 && (
{repo.topics .filter( (topic) => - topic !== "talonvoice" && topic !== "talon", + !implicitTags.includes(topic), ) .slice(0, 4) - .map((topic) => ( + .map((topic) => { + const canonical = resolveTag(topic); + return ( - ))} + ); + })} {repo.topics.filter( - (topic) => topic !== "talonvoice" && topic !== "talon", + (topic) => !implicitTags.includes(topic), ).length > 4 && ( + {repo.topics.filter( (topic) => - topic !== "talonvoice" && topic !== "talon", + !implicitTags.includes(topic), ).length - 4}{" "} more @@ -463,40 +503,43 @@ const RepoExplorer: React.FC = () => { {formatDate(repo.updated_at)} {" "} {repo.topics.filter( - (topic) => topic !== "talonvoice" && topic !== "talon", + (topic) => !implicitTags.includes(topic), ).length > 0 && (
{repo.topics .filter( (topic) => - topic !== "talonvoice" && topic !== "talon", + !implicitTags.includes(topic), ) .slice(0, 2) - .map((topic) => ( + .map((topic) => { + const canonical = resolveTag(topic); + return ( - ))} + ); + })} {repo.topics.filter( - (topic) => topic !== "talonvoice" && topic !== "talon", + (topic) => !implicitTags.includes(topic), ).length > 2 && ( + {repo.topics.filter( (topic) => - topic !== "talonvoice" && topic !== "talon", + !implicitTags.includes(topic), ).length - 2} )} diff --git a/src/pages/explorer.tsx b/src/pages/explorer.tsx index edb4fa29..02aa2a6e 100644 --- a/src/pages/explorer.tsx +++ b/src/pages/explorer.tsx @@ -43,10 +43,20 @@ export default function Explorer(): JSX.Element {

⚠️ Use at your own risk: These repositories may - not be curated or tested. For curated packages, visit{" "} + not be curated or tested. Do not download or run anything + from unvetted repositories without careful review.{" "} + For curated packages, visit{" "} talon_user_file_sets + . To report a suspicious repository, please{" "} + + open an issue + .

From 1dad2d5be0ef5450774da9438c4629e9364be10b Mon Sep 17 00:00:00 2001 From: Roku Date: Thu, 2 Apr 2026 20:34:18 -0500 Subject: [PATCH 2/6] Show data last updated date on explorer --- src/components/RepoExplorer/index.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/RepoExplorer/index.tsx b/src/components/RepoExplorer/index.tsx index 7effaac6..9054210b 100644 --- a/src/components/RepoExplorer/index.tsx +++ b/src/components/RepoExplorer/index.tsx @@ -41,6 +41,7 @@ const RepoExplorer: React.FC = () => { const [showAllTags, setShowAllTags] = useState(false); const [implicitTags, setOmitTags] = useState(["talonvoice", "talon"]); const [tagAliases, setTagAliases] = useState>({}); + const [generatedAt, setGeneratedAt] = useState(null); useEffect(() => { // Load data from build-time generated JSON @@ -53,6 +54,7 @@ const RepoExplorer: React.FC = () => { setRepos(data.repositories); if (data.implicitTags) setOmitTags(data.implicitTags); if (data.tagAliases) setTagAliases(data.tagAliases); + if (data.generated_at) setGeneratedAt(data.generated_at); console.log( `Loaded ${data.repositories.length} repositories (generated at ${data.generated_at})`, ); @@ -331,6 +333,9 @@ const RepoExplorer: React.FC = () => { {searchTerm && ` matching "${searchTerm}"`} {selectedLanguage !== "all" && ` in ${selectedLanguage}`} {selectedTags.length > 0 && ` with tags: ${selectedTags.join(", ")}`} + {generatedAt && ( + <> · Data updated {formatDate(generatedAt)} + )}

{" "}
Date: Thu, 2 Apr 2026 21:18:22 -0500 Subject: [PATCH 3/6] update omit list --- plugins/repo-data-omit-list.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/repo-data-omit-list.json b/plugins/repo-data-omit-list.json index e21964e4..37e91523 100644 --- a/plugins/repo-data-omit-list.json +++ b/plugins/repo-data-omit-list.json @@ -1,7 +1,7 @@ { "description": "Configuration for Repository Explorer exclusions and tag normalization.", "omitRepos": ["voqal/voqal", "Jailsonfs/community"], - "implicitTags": ["talonvoice", "talon", "voice", "voice-recognition", "speech-recognition", "voice-commands", "voice-control", "voice-dictation", "dictation"], + "implicitTags": ["talonvoice", "talon", "voice", "voice-recognition", "speech-recognition", "voice-commands", "voice-control"], "tagAliases": { "a11y": "accessibility", "macos-accessibility": "accessibility", From 4609b8ffcf4bef78e0a5eedcefe8036f08bade1b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:21:29 +0000 Subject: [PATCH 4/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- plugins/repo-data-omit-list.json | 10 ++- plugins/repo-data-plugin.js | 10 ++- src/components/RepoExplorer/index.tsx | 104 +++++++++++++------------- src/pages/explorer.tsx | 6 +- 4 files changed, 71 insertions(+), 59 deletions(-) diff --git a/plugins/repo-data-omit-list.json b/plugins/repo-data-omit-list.json index 37e91523..37f18276 100644 --- a/plugins/repo-data-omit-list.json +++ b/plugins/repo-data-omit-list.json @@ -1,7 +1,15 @@ { "description": "Configuration for Repository Explorer exclusions and tag normalization.", "omitRepos": ["voqal/voqal", "Jailsonfs/community"], - "implicitTags": ["talonvoice", "talon", "voice", "voice-recognition", "speech-recognition", "voice-commands", "voice-control"], + "implicitTags": [ + "talonvoice", + "talon", + "voice", + "voice-recognition", + "speech-recognition", + "voice-commands", + "voice-control" + ], "tagAliases": { "a11y": "accessibility", "macos-accessibility": "accessibility", diff --git a/plugins/repo-data-plugin.js b/plugins/repo-data-plugin.js index f4075c75..369011cc 100644 --- a/plugins/repo-data-plugin.js +++ b/plugins/repo-data-plugin.js @@ -23,10 +23,16 @@ module.exports = function (context, options) { if (omitConfig.omitRepos && Array.isArray(omitConfig.omitRepos)) { omitRepos = omitConfig.omitRepos; } - if (omitConfig.implicitTags && Array.isArray(omitConfig.implicitTags)) { + if ( + omitConfig.implicitTags && + Array.isArray(omitConfig.implicitTags) + ) { implicitTags = omitConfig.implicitTags; } - if (omitConfig.tagAliases && typeof omitConfig.tagAliases === "object") { + if ( + omitConfig.tagAliases && + typeof omitConfig.tagAliases === "object" + ) { tagAliases = omitConfig.tagAliases; } } diff --git a/src/components/RepoExplorer/index.tsx b/src/components/RepoExplorer/index.tsx index 9054210b..e09e9c0d 100644 --- a/src/components/RepoExplorer/index.tsx +++ b/src/components/RepoExplorer/index.tsx @@ -39,7 +39,10 @@ const RepoExplorer: React.FC = () => { const [sortBy, setSortBy] = useState<"stars" | "updated" | "name">("stars"); const [viewMode, setViewMode] = useState<"compact" | "expanded">("compact"); const [showAllTags, setShowAllTags] = useState(false); - const [implicitTags, setOmitTags] = useState(["talonvoice", "talon"]); + const [implicitTags, setOmitTags] = useState([ + "talonvoice", + "talon", + ]); const [tagAliases, setTagAliases] = useState>({}); const [generatedAt, setGeneratedAt] = useState(null); @@ -123,7 +126,14 @@ const RepoExplorer: React.FC = () => { }); return filtered; - }, [repos, searchTerm, selectedLanguage, selectedTags, sortBy, canonicalToOriginals]); + }, [ + repos, + searchTerm, + selectedLanguage, + selectedTags, + sortBy, + canonicalToOriginals, + ]); const languages = React.useMemo(() => { return Array.from( @@ -333,9 +343,7 @@ const RepoExplorer: React.FC = () => { {searchTerm && ` matching "${searchTerm}"`} {selectedLanguage !== "all" && ` in ${selectedLanguage}`} {selectedTags.length > 0 && ` with tags: ${selectedTags.join(", ")}`} - {generatedAt && ( - <> · Data updated {formatDate(generatedAt)} - )} + {generatedAt && <> · Data updated {formatDate(generatedAt)}}

{" "}
{ Updated {formatDate(repo.updated_at)}
{" "} - {repo.topics.filter( - (topic) => !implicitTags.includes(topic), - ).length > 0 && ( + {repo.topics.filter((topic) => !implicitTags.includes(topic)) + .length > 0 && (
{repo.topics - .filter( - (topic) => - !implicitTags.includes(topic), - ) + .filter((topic) => !implicitTags.includes(topic)) .slice(0, 4) .map((topic) => { const canonical = resolveTag(topic); return ( - + ); })} {repo.topics.filter( @@ -443,8 +447,7 @@ const RepoExplorer: React.FC = () => { + {repo.topics.filter( - (topic) => - !implicitTags.includes(topic), + (topic) => !implicitTags.includes(topic), ).length - 4}{" "} more @@ -507,34 +510,30 @@ const RepoExplorer: React.FC = () => { {formatDate(repo.updated_at)} {" "} - {repo.topics.filter( - (topic) => !implicitTags.includes(topic), - ).length > 0 && ( + {repo.topics.filter((topic) => !implicitTags.includes(topic)) + .length > 0 && (
{repo.topics - .filter( - (topic) => - !implicitTags.includes(topic), - ) + .filter((topic) => !implicitTags.includes(topic)) .slice(0, 2) .map((topic) => { const canonical = resolveTag(topic); return ( - + ); })} {repo.topics.filter( @@ -543,8 +542,7 @@ const RepoExplorer: React.FC = () => { + {repo.topics.filter( - (topic) => - !implicitTags.includes(topic), + (topic) => !implicitTags.includes(topic), ).length - 2} )} diff --git a/src/pages/explorer.tsx b/src/pages/explorer.tsx index 02aa2a6e..ff4a016a 100644 --- a/src/pages/explorer.tsx +++ b/src/pages/explorer.tsx @@ -43,9 +43,9 @@ export default function Explorer(): JSX.Element {

⚠️ Use at your own risk: These repositories may - not be curated or tested. Do not download or run anything - from unvetted repositories without careful review.{" "} - For curated packages, visit{" "} + not be curated or tested. Do not download or run anything from + unvetted repositories without careful review. For curated + packages, visit{" "} talon_user_file_sets From 3dd9ab3f8b3301ea6c475839c9aeccae43e85cf0 Mon Sep 17 00:00:00 2001 From: Roku Date: Sun, 5 Apr 2026 00:42:40 -0500 Subject: [PATCH 5/6] Address explorer feedback: tag consolidation, warning text, name-based tag inference - Consolidate game tags (garrysmod, gmod, slaythespire, etc.) and add blazor and list aliases per reviewer feedback - Update warning message to clarify compilation is unreviewed - Extract CardTags component for card tag rendering, showing original tag labels while filtering by canonical form - Add matchNamesToExistingTags option to infer tags from repo names against existing tag categories (handles kebab, snake, camel, Pascal) --- plugins/repo-data-omit-list.json | 13 ++- plugins/repo-data-plugin.js | 72 +++++++++++- src/components/RepoExplorer/index.tsx | 159 +++++++++++++------------- src/pages/explorer.tsx | 12 +- 4 files changed, 171 insertions(+), 85 deletions(-) diff --git a/plugins/repo-data-omit-list.json b/plugins/repo-data-omit-list.json index 37f18276..f5c00a2e 100644 --- a/plugins/repo-data-omit-list.json +++ b/plugins/repo-data-omit-list.json @@ -17,10 +17,21 @@ "python3": "python", "hci": "human-computer-interaction", "games": "game", + "garrysmod": "game", + "gmod": "game", + "slaythespire": "game", + "slaythespire-mod": "game", + "gameboyadvance": "game", + "mgba": "game", + "blazor-server": "blazor", + "blazor-components": "blazor", + "awesome": "list", + "awesome-list": "list", "gpt": "ai", "openai": "ai", "chatgpt": "ai", "llm": "ai", "copilot": "ai" - } + }, + "matchNamesToExistingTags": true } diff --git a/plugins/repo-data-plugin.js b/plugins/repo-data-plugin.js index 369011cc..36fc3b61 100644 --- a/plugins/repo-data-plugin.js +++ b/plugins/repo-data-plugin.js @@ -15,6 +15,7 @@ module.exports = function (context, options) { let omitRepos = []; let implicitTags = []; let tagAliases = {}; + let matchNamesToExistingTags = false; try { const omitListFile = path.join(__dirname, "repo-data-omit-list.json"); @@ -35,6 +36,9 @@ module.exports = function (context, options) { ) { tagAliases = omitConfig.tagAliases; } + if (omitConfig.matchNamesToExistingTags === true) { + matchNamesToExistingTags = true; + } } } catch (error) { console.warn("Failed to load repo-data-omit-list.json:", error.message); @@ -47,7 +51,64 @@ module.exports = function (context, options) { console.log( `Repository omit list loaded: ${omitRepos.length} repositories will be excluded`, ); - } // Determine if we should fetch fresh data + } + + // canonicalTags is built after repos are loaded, since we need + // to include tags that actually exist across repos. + let canonicalTags = null; + + function buildCanonicalTags(repos) { + const tags = new Set(Object.values(tagAliases)); + repos.forEach((repo) => { + repo.topics.forEach((t) => { + const canonical = tagAliases[t] || t; + if (!implicitTags.includes(canonical)) { + tags.add(canonical); + } + }); + }); + return tags; + } + + /** + * Split a repo name into words, handling kebab-case, snake_case, + * camelCase, PascalCase, and mixed conventions. + * e.g. "VoiceLauncherBlazor" -> ["voice", "launcher", "blazor"] + * "talon-mouse-rig" -> ["talon", "mouse", "rig"] + * "talon_mgba_http" -> ["talon", "mgba", "http"] + */ + function splitRepoName(name) { + return name + // Insert boundary before uppercase runs: "VoiceLauncher" -> "Voice Launcher" + .replace(/([a-z])([A-Z])/g, "$1 $2") + // Split acronym from next word: "HTTPServer" -> "HTTP Server" + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") + .split(/[-_.\s]+/) + .map((w) => w.toLowerCase()) + .filter((w) => w.length > 2); + } + + /** + * Infer tags from a repo's name by matching words against known + * canonical tags. Only adds tags the repo doesn't already have + * (after alias resolution). + */ + function inferTags(repo) { + if (!matchNamesToExistingTags || !canonicalTags) return; + const words = splitRepoName(repo.name); + const existingCanonical = new Set( + repo.topics.map((t) => tagAliases[t] || t), + ); + for (const word of words) { + const canonical = tagAliases[word] || word; + if (canonicalTags.has(canonical) && !existingCanonical.has(canonical) && !implicitTags.includes(canonical)) { + repo.topics.push(word); + existingCanonical.add(canonical); + } + } + } + + // Determine if we should fetch fresh data const isUpdateRepos = (process.env.npm_config_argv && JSON.parse(process.env.npm_config_argv).original.includes( @@ -88,6 +149,9 @@ module.exports = function (context, options) { ); } + canonicalTags = buildCanonicalTags(filteredRepos); + filteredRepos.forEach(inferTags); + return { ...cachedData, repositories: filteredRepos, @@ -173,6 +237,9 @@ module.exports = function (context, options) { `After filtering: ${filteredRepos.length} repositories (${allRepos.length - filteredRepos.length} omitted)`, ); + canonicalTags = buildCanonicalTags(filteredRepos); + filteredRepos.forEach(inferTags); + return { repositories: filteredRepos, total_count: totalCount, @@ -197,6 +264,9 @@ module.exports = function (context, options) { return !omitRepos.includes(fullName); }); + canonicalTags = buildCanonicalTags(filteredRepos); + filteredRepos.forEach(inferTags); + return { ...cachedData, repositories: filteredRepos, diff --git a/src/components/RepoExplorer/index.tsx b/src/components/RepoExplorer/index.tsx index e09e9c0d..14b77e1d 100644 --- a/src/components/RepoExplorer/index.tsx +++ b/src/components/RepoExplorer/index.tsx @@ -29,6 +29,71 @@ interface RepoData { tagAliases?: Record; } +interface CardTagsProps { + topics: string[]; + implicitTags: string[]; + resolveTag: (tag: string) => string; + selectedTags: string[]; + toggleTag: (tag: string) => void; + maxTags: number; + compact?: boolean; +} + +const CardTags: React.FC = ({ + topics, + implicitTags, + resolveTag, + selectedTags, + toggleTag, + maxTags, + compact = false, +}) => { + // Keep original tag names but deduplicate by canonical form, + // picking the first occurrence per canonical group. + const seenCanonical = new Set(); + const tags: { label: string; canonical: string }[] = []; + for (const topic of topics) { + if (implicitTags.includes(topic)) continue; + const canonical = resolveTag(topic); + if (!seenCanonical.has(canonical)) { + seenCanonical.add(canonical); + tags.push({ label: topic, canonical }); + } + } + + if (tags.length === 0) return null; + + const containerClass = compact ? styles.topicsCompact : styles.topics; + const tagClass = compact ? styles.topicCompact : styles.topic; + const activeClass = compact ? styles.topicActiveCompact : styles.topicActive; + const moreClass = compact ? styles.topicMoreCompact : styles.topicMore; + + return ( +

+ {tags.slice(0, maxTags).map(({ label, canonical }) => ( + + ))} + {tags.length > maxTags && ( + + +{tags.length - maxTags} more + + )} +
+ ); +}; + const RepoExplorer: React.FC = () => { const [repos, setRepos] = useState([]); const [loading, setLoading] = useState(true); @@ -415,45 +480,14 @@ const RepoExplorer: React.FC = () => { Updated {formatDate(repo.updated_at)}
{" "} - {repo.topics.filter((topic) => !implicitTags.includes(topic)) - .length > 0 && ( -
- {repo.topics - .filter((topic) => !implicitTags.includes(topic)) - .slice(0, 4) - .map((topic) => { - const canonical = resolveTag(topic); - return ( - - ); - })} - {repo.topics.filter( - (topic) => !implicitTags.includes(topic), - ).length > 4 && ( - - + - {repo.topics.filter( - (topic) => !implicitTags.includes(topic), - ).length - 4}{" "} - more - - )} -
- )} +
) : ( @@ -510,44 +544,15 @@ const RepoExplorer: React.FC = () => { {formatDate(repo.updated_at)} {" "} - {repo.topics.filter((topic) => !implicitTags.includes(topic)) - .length > 0 && ( -
- {repo.topics - .filter((topic) => !implicitTags.includes(topic)) - .slice(0, 2) - .map((topic) => { - const canonical = resolveTag(topic); - return ( - - ); - })} - {repo.topics.filter( - (topic) => !implicitTags.includes(topic), - ).length > 2 && ( - - + - {repo.topics.filter( - (topic) => !implicitTags.includes(topic), - ).length - 2} - - )} -
- )} + )} diff --git a/src/pages/explorer.tsx b/src/pages/explorer.tsx index ff4a016a..059d1128 100644 --- a/src/pages/explorer.tsx +++ b/src/pages/explorer.tsx @@ -42,14 +42,14 @@ export default function Explorer(): JSX.Element { talonvoice

- ⚠️ Use at your own risk: These repositories may - not be curated or tested. Do not download or run anything from - unvetted repositories without careful review. For curated - packages, visit{" "} + ⚠️ Use at your own risk: This compilation + wasn't reviewed. For a curated list, visit{" "} - talon_user_file_sets + Talon user file sets - . To report a suspicious repository, please{" "} + . Do not download or run anything from unvetted repositories + without careful review. To report a suspicious repository, + please{" "} Date: Sun, 5 Apr 2026 05:54:12 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- plugins/repo-data-plugin.js | 24 +++++++++++++++--------- src/components/RepoExplorer/index.tsx | 4 +--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/plugins/repo-data-plugin.js b/plugins/repo-data-plugin.js index 36fc3b61..2e20b5ff 100644 --- a/plugins/repo-data-plugin.js +++ b/plugins/repo-data-plugin.js @@ -78,14 +78,16 @@ module.exports = function (context, options) { * "talon_mgba_http" -> ["talon", "mgba", "http"] */ function splitRepoName(name) { - return name - // Insert boundary before uppercase runs: "VoiceLauncher" -> "Voice Launcher" - .replace(/([a-z])([A-Z])/g, "$1 $2") - // Split acronym from next word: "HTTPServer" -> "HTTP Server" - .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") - .split(/[-_.\s]+/) - .map((w) => w.toLowerCase()) - .filter((w) => w.length > 2); + return ( + name + // Insert boundary before uppercase runs: "VoiceLauncher" -> "Voice Launcher" + .replace(/([a-z])([A-Z])/g, "$1 $2") + // Split acronym from next word: "HTTPServer" -> "HTTP Server" + .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2") + .split(/[-_.\s]+/) + .map((w) => w.toLowerCase()) + .filter((w) => w.length > 2) + ); } /** @@ -101,7 +103,11 @@ module.exports = function (context, options) { ); for (const word of words) { const canonical = tagAliases[word] || word; - if (canonicalTags.has(canonical) && !existingCanonical.has(canonical) && !implicitTags.includes(canonical)) { + if ( + canonicalTags.has(canonical) && + !existingCanonical.has(canonical) && + !implicitTags.includes(canonical) + ) { repo.topics.push(word); existingCanonical.add(canonical); } diff --git a/src/components/RepoExplorer/index.tsx b/src/components/RepoExplorer/index.tsx index 14b77e1d..3cc81978 100644 --- a/src/components/RepoExplorer/index.tsx +++ b/src/components/RepoExplorer/index.tsx @@ -86,9 +86,7 @@ const CardTags: React.FC = ({ ))} {tags.length > maxTags && ( - - +{tags.length - maxTags} more - + +{tags.length - maxTags} more )} );