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 (
toggleTag(topic)}
- title={`Filter by ${topic}`}
+ onClick={() => toggleTag(canonical)}
+ title={`Filter by ${canonical}`}
>
{topic}
- ))}
+ );
+ })}
{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 (
toggleTag(topic)}
- title={`Filter by ${topic}`}
+ onClick={() => toggleTag(canonical)}
+ title={`Filter by ${canonical}`}
>
{topic}
- ))}
+ );
+ })}
{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 (
-
toggleTag(canonical)}
- title={`Filter by ${canonical}`}
- >
- {topic}
-
+
toggleTag(canonical)}
+ title={`Filter by ${canonical}`}
+ >
+ {topic}
+
);
})}
{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 (
-
toggleTag(canonical)}
- title={`Filter by ${canonical}`}
- >
- {topic}
-
+
toggleTag(canonical)}
+ title={`Filter by ${canonical}`}
+ >
+ {topic}
+
);
})}
{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 }) => (
+ toggleTag(canonical)}
+ title={`Filter by ${canonical}`}
+ >
+ {label}
+
+ ))}
+ {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 (
- toggleTag(canonical)}
- title={`Filter by ${canonical}`}
- >
- {topic}
-
- );
- })}
- {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 (
- toggleTag(canonical)}
- title={`Filter by ${canonical}`}
- >
- {topic}
-
- );
- })}
- {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
)}
);