Skip to content

Commit 3dd9ab3

Browse files
committed
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)
1 parent 4609b8f commit 3dd9ab3

4 files changed

Lines changed: 171 additions & 85 deletions

File tree

plugins/repo-data-omit-list.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,21 @@
1717
"python3": "python",
1818
"hci": "human-computer-interaction",
1919
"games": "game",
20+
"garrysmod": "game",
21+
"gmod": "game",
22+
"slaythespire": "game",
23+
"slaythespire-mod": "game",
24+
"gameboyadvance": "game",
25+
"mgba": "game",
26+
"blazor-server": "blazor",
27+
"blazor-components": "blazor",
28+
"awesome": "list",
29+
"awesome-list": "list",
2030
"gpt": "ai",
2131
"openai": "ai",
2232
"chatgpt": "ai",
2333
"llm": "ai",
2434
"copilot": "ai"
25-
}
35+
},
36+
"matchNamesToExistingTags": true
2637
}

plugins/repo-data-plugin.js

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module.exports = function (context, options) {
1515
let omitRepos = [];
1616
let implicitTags = [];
1717
let tagAliases = {};
18+
let matchNamesToExistingTags = false;
1819

1920
try {
2021
const omitListFile = path.join(__dirname, "repo-data-omit-list.json");
@@ -35,6 +36,9 @@ module.exports = function (context, options) {
3536
) {
3637
tagAliases = omitConfig.tagAliases;
3738
}
39+
if (omitConfig.matchNamesToExistingTags === true) {
40+
matchNamesToExistingTags = true;
41+
}
3842
}
3943
} catch (error) {
4044
console.warn("Failed to load repo-data-omit-list.json:", error.message);
@@ -47,7 +51,64 @@ module.exports = function (context, options) {
4751
console.log(
4852
`Repository omit list loaded: ${omitRepos.length} repositories will be excluded`,
4953
);
50-
} // Determine if we should fetch fresh data
54+
}
55+
56+
// canonicalTags is built after repos are loaded, since we need
57+
// to include tags that actually exist across repos.
58+
let canonicalTags = null;
59+
60+
function buildCanonicalTags(repos) {
61+
const tags = new Set(Object.values(tagAliases));
62+
repos.forEach((repo) => {
63+
repo.topics.forEach((t) => {
64+
const canonical = tagAliases[t] || t;
65+
if (!implicitTags.includes(canonical)) {
66+
tags.add(canonical);
67+
}
68+
});
69+
});
70+
return tags;
71+
}
72+
73+
/**
74+
* Split a repo name into words, handling kebab-case, snake_case,
75+
* camelCase, PascalCase, and mixed conventions.
76+
* e.g. "VoiceLauncherBlazor" -> ["voice", "launcher", "blazor"]
77+
* "talon-mouse-rig" -> ["talon", "mouse", "rig"]
78+
* "talon_mgba_http" -> ["talon", "mgba", "http"]
79+
*/
80+
function splitRepoName(name) {
81+
return name
82+
// Insert boundary before uppercase runs: "VoiceLauncher" -> "Voice Launcher"
83+
.replace(/([a-z])([A-Z])/g, "$1 $2")
84+
// Split acronym from next word: "HTTPServer" -> "HTTP Server"
85+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
86+
.split(/[-_.\s]+/)
87+
.map((w) => w.toLowerCase())
88+
.filter((w) => w.length > 2);
89+
}
90+
91+
/**
92+
* Infer tags from a repo's name by matching words against known
93+
* canonical tags. Only adds tags the repo doesn't already have
94+
* (after alias resolution).
95+
*/
96+
function inferTags(repo) {
97+
if (!matchNamesToExistingTags || !canonicalTags) return;
98+
const words = splitRepoName(repo.name);
99+
const existingCanonical = new Set(
100+
repo.topics.map((t) => tagAliases[t] || t),
101+
);
102+
for (const word of words) {
103+
const canonical = tagAliases[word] || word;
104+
if (canonicalTags.has(canonical) && !existingCanonical.has(canonical) && !implicitTags.includes(canonical)) {
105+
repo.topics.push(word);
106+
existingCanonical.add(canonical);
107+
}
108+
}
109+
}
110+
111+
// Determine if we should fetch fresh data
51112
const isUpdateRepos =
52113
(process.env.npm_config_argv &&
53114
JSON.parse(process.env.npm_config_argv).original.includes(
@@ -88,6 +149,9 @@ module.exports = function (context, options) {
88149
);
89150
}
90151

152+
canonicalTags = buildCanonicalTags(filteredRepos);
153+
filteredRepos.forEach(inferTags);
154+
91155
return {
92156
...cachedData,
93157
repositories: filteredRepos,
@@ -173,6 +237,9 @@ module.exports = function (context, options) {
173237
`After filtering: ${filteredRepos.length} repositories (${allRepos.length - filteredRepos.length} omitted)`,
174238
);
175239

240+
canonicalTags = buildCanonicalTags(filteredRepos);
241+
filteredRepos.forEach(inferTags);
242+
176243
return {
177244
repositories: filteredRepos,
178245
total_count: totalCount,
@@ -197,6 +264,9 @@ module.exports = function (context, options) {
197264
return !omitRepos.includes(fullName);
198265
});
199266

267+
canonicalTags = buildCanonicalTags(filteredRepos);
268+
filteredRepos.forEach(inferTags);
269+
200270
return {
201271
...cachedData,
202272
repositories: filteredRepos,

src/components/RepoExplorer/index.tsx

Lines changed: 82 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,71 @@ interface RepoData {
2929
tagAliases?: Record<string, string>;
3030
}
3131

32+
interface CardTagsProps {
33+
topics: string[];
34+
implicitTags: string[];
35+
resolveTag: (tag: string) => string;
36+
selectedTags: string[];
37+
toggleTag: (tag: string) => void;
38+
maxTags: number;
39+
compact?: boolean;
40+
}
41+
42+
const CardTags: React.FC<CardTagsProps> = ({
43+
topics,
44+
implicitTags,
45+
resolveTag,
46+
selectedTags,
47+
toggleTag,
48+
maxTags,
49+
compact = false,
50+
}) => {
51+
// Keep original tag names but deduplicate by canonical form,
52+
// picking the first occurrence per canonical group.
53+
const seenCanonical = new Set<string>();
54+
const tags: { label: string; canonical: string }[] = [];
55+
for (const topic of topics) {
56+
if (implicitTags.includes(topic)) continue;
57+
const canonical = resolveTag(topic);
58+
if (!seenCanonical.has(canonical)) {
59+
seenCanonical.add(canonical);
60+
tags.push({ label: topic, canonical });
61+
}
62+
}
63+
64+
if (tags.length === 0) return null;
65+
66+
const containerClass = compact ? styles.topicsCompact : styles.topics;
67+
const tagClass = compact ? styles.topicCompact : styles.topic;
68+
const activeClass = compact ? styles.topicActiveCompact : styles.topicActive;
69+
const moreClass = compact ? styles.topicMoreCompact : styles.topicMore;
70+
71+
return (
72+
<div className={containerClass}>
73+
{tags.slice(0, maxTags).map(({ label, canonical }) => (
74+
<button
75+
key={label}
76+
className={clsx(
77+
tagClass,
78+
styles.tagGeneric,
79+
selectedTags.includes(canonical) && styles.tagGenericActive,
80+
selectedTags.includes(canonical) && activeClass,
81+
)}
82+
onClick={() => toggleTag(canonical)}
83+
title={`Filter by ${canonical}`}
84+
>
85+
{label}
86+
</button>
87+
))}
88+
{tags.length > maxTags && (
89+
<span className={moreClass}>
90+
+{tags.length - maxTags} more
91+
</span>
92+
)}
93+
</div>
94+
);
95+
};
96+
3297
const RepoExplorer: React.FC = () => {
3398
const [repos, setRepos] = useState<GitHubRepo[]>([]);
3499
const [loading, setLoading] = useState(true);
@@ -415,45 +480,14 @@ const RepoExplorer: React.FC = () => {
415480
Updated {formatDate(repo.updated_at)}
416481
</span>
417482
</div>{" "}
418-
{repo.topics.filter((topic) => !implicitTags.includes(topic))
419-
.length > 0 && (
420-
<div className={styles.topics}>
421-
{repo.topics
422-
.filter((topic) => !implicitTags.includes(topic))
423-
.slice(0, 4)
424-
.map((topic) => {
425-
const canonical = resolveTag(topic);
426-
return (
427-
<button
428-
key={topic}
429-
className={clsx(
430-
styles.topic,
431-
styles.tagGeneric,
432-
selectedTags.includes(canonical) &&
433-
styles.tagGenericActive,
434-
selectedTags.includes(canonical) &&
435-
styles.topicActive,
436-
)}
437-
onClick={() => toggleTag(canonical)}
438-
title={`Filter by ${canonical}`}
439-
>
440-
{topic}
441-
</button>
442-
);
443-
})}
444-
{repo.topics.filter(
445-
(topic) => !implicitTags.includes(topic),
446-
).length > 4 && (
447-
<span className={styles.topicMore}>
448-
+
449-
{repo.topics.filter(
450-
(topic) => !implicitTags.includes(topic),
451-
).length - 4}{" "}
452-
more
453-
</span>
454-
)}
455-
</div>
456-
)}
483+
<CardTags
484+
topics={repo.topics}
485+
implicitTags={implicitTags}
486+
resolveTag={resolveTag}
487+
selectedTags={selectedTags}
488+
toggleTag={toggleTag}
489+
maxTags={4}
490+
/>
457491
</div>
458492
</>
459493
) : (
@@ -510,44 +544,15 @@ const RepoExplorer: React.FC = () => {
510544
<span className={styles.updatedCompact}>
511545
{formatDate(repo.updated_at)}
512546
</span>{" "}
513-
{repo.topics.filter((topic) => !implicitTags.includes(topic))
514-
.length > 0 && (
515-
<div className={styles.topicsCompact}>
516-
{repo.topics
517-
.filter((topic) => !implicitTags.includes(topic))
518-
.slice(0, 2)
519-
.map((topic) => {
520-
const canonical = resolveTag(topic);
521-
return (
522-
<button
523-
key={topic}
524-
className={clsx(
525-
styles.topicCompact,
526-
styles.tagGeneric,
527-
selectedTags.includes(canonical) &&
528-
styles.tagGenericActive,
529-
selectedTags.includes(canonical) &&
530-
styles.topicActiveCompact,
531-
)}
532-
onClick={() => toggleTag(canonical)}
533-
title={`Filter by ${canonical}`}
534-
>
535-
{topic}
536-
</button>
537-
);
538-
})}
539-
{repo.topics.filter(
540-
(topic) => !implicitTags.includes(topic),
541-
).length > 2 && (
542-
<span className={styles.topicMoreCompact}>
543-
+
544-
{repo.topics.filter(
545-
(topic) => !implicitTags.includes(topic),
546-
).length - 2}
547-
</span>
548-
)}
549-
</div>
550-
)}
547+
<CardTags
548+
topics={repo.topics}
549+
implicitTags={implicitTags}
550+
resolveTag={resolveTag}
551+
selectedTags={selectedTags}
552+
toggleTag={toggleTag}
553+
maxTags={2}
554+
compact
555+
/>
551556
</div>
552557
</>
553558
)}

src/pages/explorer.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ export default function Explorer(): JSX.Element {
4242
<code className={styles.codeHighlight}>talonvoice</code>
4343
</p>
4444
<p className={styles.warning}>
45-
⚠️ <strong>Use at your own risk:</strong> These repositories may
46-
not be curated or tested. Do not download or run anything from
47-
unvetted repositories without careful review. For curated
48-
packages, visit{" "}
45+
⚠️ <strong>Use at your own risk:</strong> This compilation
46+
wasn't reviewed. For a curated list, visit{" "}
4947
<a href="/integrations/talon_user_file_sets">
50-
talon_user_file_sets
48+
Talon user file sets
5149
</a>
52-
. To report a suspicious repository, please{" "}
50+
. Do not download or run anything from unvetted repositories
51+
without careful review. To report a suspicious repository,
52+
please{" "}
5353
<a
5454
href="https://github.com/TalonCommunity/Wiki/issues/new?title=Suspicious+repository+report&labels=explorer"
5555
target="_blank"

0 commit comments

Comments
 (0)