feat: featured projects

This commit is contained in:
Ben Allfree
2026-04-17 02:53:44 -07:00
parent 30705f504c
commit 3235864063
13 changed files with 195 additions and 8 deletions
+12
View File
@@ -27,6 +27,7 @@
"@mdx-js/rollup": "^3.1.1",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-slot": "^1.2.4",
"@rollup/plugin-yaml": "^4.1.2",
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^24.10.1",
@@ -47,6 +48,7 @@
"typescript": "^5.9.3",
"vite": "^7.2.6",
"wrangler": "^4.51.0",
"yaml": "^2.8.3",
},
},
},
@@ -375,6 +377,8 @@
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="],
"@rollup/plugin-yaml": ["@rollup/plugin-yaml@4.1.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "js-yaml": "^4.1.0", "tosource": "^2.0.0-alpha.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-RpupciIeZMUqhgFE97ba0s98mOFS7CWzN3EJNhJkqSv9XLlWYtwVdtE6cDw6ASOF/sZVFS7kRJXftaqM2Vakdw=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="],
@@ -605,6 +609,8 @@
"acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
@@ -771,6 +777,8 @@
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
@@ -1045,6 +1053,8 @@
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tosource": ["tosource@2.0.0-alpha.3", "", {}, "sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
@@ -1099,6 +1109,8 @@
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
"youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
"youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
+68
View File
@@ -0,0 +1,68 @@
import { getFeaturedProjects } from "@/src/lib/featuredProjects"
import type { FeaturedProject } from "@/src/types/featured"
import { Check, Github } from "lucide-react"
import { useState } from "react"
function FeaturedAvatar({ src, title }: { src: string; title: string }) {
const [ok, setOk] = useState(true)
if (!ok) {
return (
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-xl bg-slate-800 ring-1 ring-white/10">
<Github className="h-8 w-8 text-slate-400" aria-hidden />
</div>
)
}
return (
<img
src={src}
alt=""
loading="lazy"
className="h-14 w-14 shrink-0 rounded-xl object-cover ring-1 ring-white/10"
onError={() => setOk(false)}
title={title}
/>
)
}
function FeaturedTile({ project, onOpenRepoUrl }: { project: FeaturedProject; onOpenRepoUrl: (url: string) => void }) {
return (
<button
type="button"
onClick={() => onOpenRepoUrl(project.url)}
aria-label={`Open ${project.title} in Mesh Forge`}
className="group flex w-full cursor-pointer items-center gap-3 rounded-xl border border-slate-700/80 bg-slate-900/60 p-3 text-left ring-1 ring-white/5 transition hover:border-cyan-700/60 hover:bg-slate-800/80 hover:ring-cyan-500/20"
>
<FeaturedAvatar src={project.logo} title={project.title} />
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="truncate font-semibold text-white group-hover:text-cyan-100">{project.title}</span>
{project.highlighted ? (
<span className="inline-flex shrink-0 items-center gap-0.5 rounded-full bg-emerald-500/15 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-emerald-300 ring-1 ring-emerald-500/30">
<Check className="h-3 w-3" aria-hidden />
pick
</span>
) : null}
</div>
{project.subtitle ? (
<p className="mt-1 line-clamp-2 text-xs leading-snug text-slate-400">{project.subtitle}</p>
) : null}
</div>
</button>
)
}
export function FeaturedProjects({ onOpenRepoUrl }: { onOpenRepoUrl: (url: string) => void }) {
const projects = getFeaturedProjects()
if (!projects.length) return null
return (
<div className="w-full space-y-3 text-left">
<h2 className="text-center text-sm font-semibold uppercase tracking-wide text-slate-400">Featured projects</h2>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{projects.map(p => (
<FeaturedTile key={p.url} project={p} onOpenRepoUrl={onOpenRepoUrl} />
))}
</div>
</div>
)
}
+4 -1
View File
@@ -12,6 +12,7 @@
"scripts": {
"generate:versions": "node scripts/generate-versions.js && biome format src/constants/versions.ts --write",
"generate:architecture": "node scripts/generate-architecture-hierarchy.js",
"sync:featured": "bun scripts/sync-featured-logos.ts",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
@@ -44,6 +45,7 @@
"@mdx-js/rollup": "^3.1.1",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-slot": "^1.2.4",
"@rollup/plugin-yaml": "^4.1.2",
"@tailwindcss/postcss": "^4.2.2",
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^24.10.1",
@@ -63,7 +65,8 @@
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.9.3",
"vite": "^7.2.6",
"wrangler": "^4.51.0"
"wrangler": "^4.51.0",
"yaml": "^2.8.3"
},
"type": "module"
}
+21
View File
@@ -0,0 +1,21 @@
projects:
- title: Lotato
subtitle: 100% on-device Potato Mesh ingestor for MeshCore.
url: https://github.com/MeshEnvy/MeshCore-lotato
highlighted: false
logo: https://avatars.githubusercontent.com/u/237130814?v=4&s=128
- title: Mesh64
subtitle: A 64-hop Meshtastic prototype by Baymesh.
url: https://github.com/RCGV1/firmware-Fork/tree/baymesh-refactor
highlighted: true
logo: https://avatars.githubusercontent.com/u/119711889?v=4&s=128
- title: MeshCore
subtitle: Open mesh stack and device firmware for LoRa-class radios—companion nodes, repeaters, and app-friendly transports.
url: https://github.com/meshcore-dev/MeshCore
highlighted: false
logo: https://avatars.githubusercontent.com/u/210668307?v=4&s=128
- title: Meshtastic
subtitle: Official open-source device firmware—encrypted text, position, and telemetry over a decentralized LoRa mesh.
url: https://github.com/meshtastic/firmware
highlighted: false
logo: https://avatars.githubusercontent.com/u/61627050?v=4&s=128
+45
View File
@@ -0,0 +1,45 @@
import { readFileSync, writeFileSync } from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { parse, stringify } from "yaml"
import { parseGithubUrl } from "../src/lib/parseGithubUrl"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const root = path.resolve(__dirname, "..")
const yamlPath = path.join(root, "projects.yaml")
type Row = {
title: string
url: string
logo?: string
subtitle?: string
highlighted?: boolean
}
const text = readFileSync(yamlPath, "utf8")
const doc = parse(text) as { projects: Row[] }
if (!doc?.projects || !Array.isArray(doc.projects)) {
console.error("projects.yaml: expected top-level `projects` array")
process.exit(1)
}
for (const p of doc.projects) {
const parsed = parseGithubUrl(p.url)
if (!parsed) {
console.error("Invalid url:", p.url)
process.exit(1)
}
const res = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`)
if (!res.ok) {
console.error(`${parsed.owner}/${parsed.repo}:`, res.status, await res.text())
process.exit(1)
}
const json = (await res.json()) as { owner: { avatar_url: string } }
const base = json.owner.avatar_url
const sep = base.includes("?") ? "&" : "?"
p.logo = `${base}${sep}s=128`
}
writeFileSync(yamlPath, stringify(doc, { lineWidth: 120, indent: 2 }) + "\n", "utf8")
console.log("Wrote logos to", yamlPath)
+6 -1
View File
@@ -75,9 +75,14 @@
}
.prose.prose-invert a {
color: oklch(0.488 0.243 264.376);
color: oklch(0.82 0.12 195);
font-weight: 500;
text-decoration: underline;
text-underline-offset: 0.15em;
}
.prose.prose-invert a:hover {
color: oklch(0.88 0.1 195);
}
.prose.prose-invert code {
+8
View File
@@ -0,0 +1,8 @@
import type { FeaturedProjectsFile } from "@/src/types/featured"
import rawFeatured from "../../projects.yaml"
const data = rawFeatured as FeaturedProjectsFile
export function getFeaturedProjects(): FeaturedProjectsFile["projects"] {
return data.projects
}
+10 -3
View File
@@ -1,4 +1,5 @@
import logo from "@/assets/logo.png"
import { FeaturedProjects } from "@/components/FeaturedProjects"
import { Button } from "@/components/ui/button"
import { useState } from "react"
import { useNavigate } from "react-router-dom"
@@ -13,8 +14,10 @@ export default function HomePage() {
const [input, setInput] = useState("")
const [error, setError] = useState<string | null>(null)
const go = () => {
const parsed = parseGithubUrl(input)
const openRepoUrl = (raw: string) => {
const trimmed = raw.trim()
setInput(trimmed)
const parsed = parseGithubUrl(trimmed)
if (!parsed) {
setError("Paste a GitHub URL like https://github.com/owner/repo or owner/repo")
return
@@ -30,8 +33,10 @@ export default function HomePage() {
}
}
const go = () => openRepoUrl(input)
return (
<div className="min-h-screen bg-gradient-to-b from-slate-950 via-slate-900 to-slate-950 text-white flex flex-col items-center justify-center px-6 py-16">
<div className="min-h-screen bg-gradient-to-b from-slate-950 via-slate-900 to-slate-950 text-white flex flex-col items-center justify-start px-6 py-12 md:py-16">
<div className="max-w-xl w-full text-center space-y-8">
<div className="space-y-5">
<div className="flex justify-center">
@@ -49,6 +54,8 @@ export default function HomePage() {
</div>
</div>
<FeaturedProjects onOpenRepoUrl={openRepoUrl} />
<div className="space-y-3 text-left">
<input
id="gh-url"
+1 -1
View File
@@ -728,7 +728,7 @@ export default function RepoPage() {
<div>
<p className="text-xs text-slate-500 mb-1">{owner}</p>
<h3 className="flex flex-wrap items-center gap-1.5 text-xl font-bold text-white leading-tight">
<a className="hover:text-cyan-400" href={ghRepoRoot} target="_blank" rel="noreferrer">
<a className="text-white hover:text-cyan-400" href={ghRepoRoot} target="_blank" rel="noreferrer">
{repo}
</a>
{!ghAboutHomepage ? (
+11
View File
@@ -0,0 +1,11 @@
export type FeaturedProject = {
title: string
subtitle?: string
url: string
highlighted?: boolean
logo: string
}
export type FeaturedProjectsFile = {
projects: FeaturedProject[]
}
+6 -1
View File
@@ -1,6 +1,11 @@
/// <reference types="vite/client" />
declare module '*.md?raw' {
declare module "*.md?raw" {
const src: string
export default src
}
declare module "*.yaml" {
const data: unknown
export default data
}
Vendored Submodule
+1
Submodule vendor/meshtastic-web added at 6535c96e81
+2 -1
View File
@@ -1,10 +1,11 @@
import yaml from "@rollup/plugin-yaml"
import react from "@vitejs/plugin-react"
import path from "node:path"
import { defineConfig } from "vite"
/** Tailwind runs via PostCSS (`postcss.config.mjs`), not `@tailwindcss/vite` — the Vite plugin was stalling builds (no stdout) on large workspaces. */
export default defineConfig({
plugins: [react()],
plugins: [react(), yaml()],
logLevel: "info",
resolve: {
alias: {