@grove-dev/astro 0.1.5 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/dist/index.d.ts +4 -25
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +56 -22
  4. package/dist/index.js.map +1 -1
  5. package/dist/layouts/index.d.ts +25 -0
  6. package/dist/layouts/index.d.ts.map +1 -0
  7. package/dist/layouts/index.js +25 -0
  8. package/dist/layouts/index.js.map +1 -0
  9. package/dist/lib/display.d.ts +33 -0
  10. package/dist/lib/display.d.ts.map +1 -0
  11. package/dist/lib/display.js +104 -0
  12. package/dist/lib/display.js.map +1 -0
  13. package/dist/lib/format.d.ts +30 -0
  14. package/dist/lib/format.d.ts.map +1 -0
  15. package/dist/lib/format.js +73 -0
  16. package/dist/lib/format.js.map +1 -0
  17. package/dist/lib/index.d.ts +26 -0
  18. package/dist/lib/index.d.ts.map +1 -0
  19. package/dist/lib/index.js +26 -0
  20. package/dist/lib/index.js.map +1 -0
  21. package/dist/lib/lenses.d.ts +96 -0
  22. package/dist/lib/lenses.d.ts.map +1 -0
  23. package/dist/lib/lenses.js +181 -0
  24. package/dist/lib/lenses.js.map +1 -0
  25. package/dist/lib/repo.d.ts +30 -0
  26. package/dist/lib/repo.d.ts.map +1 -0
  27. package/dist/lib/repo.js +44 -0
  28. package/dist/lib/repo.js.map +1 -0
  29. package/dist/lib/scores.d.ts +38 -0
  30. package/dist/lib/scores.d.ts.map +1 -0
  31. package/dist/lib/scores.js +83 -0
  32. package/dist/lib/scores.js.map +1 -0
  33. package/dist/lib/search.d.ts +199 -0
  34. package/dist/lib/search.d.ts.map +1 -0
  35. package/dist/lib/search.js +357 -0
  36. package/dist/lib/search.js.map +1 -0
  37. package/dist/lib/taxonomy-counts.d.ts +25 -0
  38. package/dist/lib/taxonomy-counts.d.ts.map +1 -0
  39. package/dist/lib/taxonomy-counts.js +41 -0
  40. package/dist/lib/taxonomy-counts.js.map +1 -0
  41. package/package.json +7 -7
  42. package/src/components/AppSection.astro +83 -0
  43. package/src/components/AppsIndexRow.astro +204 -0
  44. package/src/components/AppsPagination.astro +126 -0
  45. package/src/components/CategoryGrid.astro +86 -17
  46. package/src/components/ContributorsGrid.astro +149 -0
  47. package/src/components/DecisionRow.astro +89 -0
  48. package/src/components/ExploreByCategory.astro +67 -0
  49. package/src/components/ExploreByStack.astro +72 -0
  50. package/src/components/Hero.astro +246 -0
  51. package/src/components/Icon.astro +223 -0
  52. package/src/components/ItemCard.astro +289 -30
  53. package/src/components/MinimalAbout.astro +82 -0
  54. package/src/components/OriginalCollection.astro +116 -0
  55. package/src/components/RefinePanel.astro +197 -0
  56. package/src/components/ScoreBars.astro +92 -22
  57. package/src/components/SmartLensTabs.astro +71 -0
  58. package/src/components/StackGrid.astro +132 -0
  59. package/src/components/WhyThisExists.astro +116 -0
  60. package/src/index.ts +61 -14
  61. package/src/layouts/BaseLayout.astro +227 -15
  62. package/src/layouts/Container.astro +57 -0
  63. package/src/layouts/Footer.astro +235 -0
  64. package/src/layouts/Header.astro +159 -0
  65. package/src/layouts/SectionHeader.astro +58 -0
  66. package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
  67. package/src/layouts/ThemeToggle.astro +110 -0
  68. package/src/layouts/astro-shim.d.ts +29 -0
  69. package/src/layouts/index.ts +24 -0
  70. package/src/lib/display.ts +118 -0
  71. package/src/lib/format.ts +65 -0
  72. package/src/lib/index.ts +26 -0
  73. package/src/lib/lenses.ts +241 -0
  74. package/src/lib/repo.ts +44 -0
  75. package/src/lib/scores.ts +94 -0
  76. package/src/lib/search.ts +481 -0
  77. package/src/lib/taxonomy-counts.ts +60 -0
  78. package/src/styles.css +366 -638
  79. package/templates/default/.astro/content.d.ts +155 -0
  80. package/templates/default/.astro/settings.json +5 -0
  81. package/templates/default/.astro/types.d.ts +2 -0
  82. package/templates/default/astro.config.mjs +8 -6
  83. package/templates/default/content/pages/about.md +66 -0
  84. package/templates/default/content/pages/methodology.md +103 -0
  85. package/templates/default/data/generated/records.full.json +8 -0
  86. package/templates/default/data/generated/records.index.json +7 -0
  87. package/templates/default/data/generated/records.json +8 -0
  88. package/templates/default/data/generated/site-config.json +31 -0
  89. package/templates/default/data/records/.gitkeep +0 -0
  90. package/templates/default/dist/_astro/BaseLayout.DfpwR3jc.css +1 -0
  91. package/templates/default/dist/about/index.html +165 -0
  92. package/templates/default/dist/icons/platforms/android.svg +1 -0
  93. package/templates/default/dist/icons/platforms/apple-dark.svg +1 -0
  94. package/templates/default/dist/icons/platforms/apple-light.svg +1 -0
  95. package/templates/default/dist/icons/platforms/chrome.svg +1 -0
  96. package/templates/default/dist/icons/platforms/chromeos.svg +5 -0
  97. package/templates/default/dist/icons/platforms/desktop.svg +4 -0
  98. package/templates/default/dist/icons/platforms/embedded.svg +5 -0
  99. package/templates/default/dist/icons/platforms/ios.svg +1 -0
  100. package/templates/default/dist/icons/platforms/linux.svg +1 -0
  101. package/templates/default/dist/icons/platforms/macos.svg +1 -0
  102. package/templates/default/dist/icons/platforms/ubuntu.svg +1 -0
  103. package/templates/default/dist/icons/platforms/web.svg +5 -0
  104. package/templates/default/dist/icons/platforms/windows.svg +6 -0
  105. package/templates/default/dist/icons/stacks/android.svg +1 -0
  106. package/templates/default/dist/icons/stacks/apple-dark.svg +1 -0
  107. package/templates/default/dist/icons/stacks/apple-light.svg +1 -0
  108. package/templates/default/dist/icons/stacks/capacitor.svg +14 -0
  109. package/templates/default/dist/icons/stacks/dart.svg +1 -0
  110. package/templates/default/dist/icons/stacks/firebase.svg +1 -0
  111. package/templates/default/dist/icons/stacks/flutter.svg +1 -0
  112. package/templates/default/dist/icons/stacks/graphql.svg +1 -0
  113. package/templates/default/dist/icons/stacks/ionic.svg +4 -0
  114. package/templates/default/dist/icons/stacks/java.svg +1 -0
  115. package/templates/default/dist/icons/stacks/javascript.svg +1 -0
  116. package/templates/default/dist/icons/stacks/kotlin.svg +1 -0
  117. package/templates/default/dist/icons/stacks/mongodb.svg +1 -0
  118. package/templates/default/dist/icons/stacks/nodejs.svg +1 -0
  119. package/templates/default/dist/icons/stacks/python.svg +1 -0
  120. package/templates/default/dist/icons/stacks/react-native.svg +1 -0
  121. package/templates/default/dist/icons/stacks/react.svg +1 -0
  122. package/templates/default/dist/icons/stacks/rust.svg +1 -0
  123. package/templates/default/dist/icons/stacks/solidity.svg +1 -0
  124. package/templates/default/dist/icons/stacks/swift.svg +1 -0
  125. package/templates/default/dist/icons/stacks/tensorflow.svg +1 -0
  126. package/templates/default/dist/icons/stacks/typescript.svg +1 -0
  127. package/templates/default/dist/index.html +55 -0
  128. package/templates/default/dist/llms-full.txt +14 -0
  129. package/templates/default/dist/llms.txt +11 -0
  130. package/templates/default/dist/og-image.svg +48 -0
  131. package/templates/default/dist/projects/index.html +61 -0
  132. package/templates/default/dist/robots.txt +53 -0
  133. package/templates/default/dist/sitemap.xml +21 -0
  134. package/templates/default/dist/submit/index.html +69 -0
  135. package/templates/default/grove.config.ts +36 -0
  136. package/templates/default/node_modules/.bin/astro +21 -0
  137. package/templates/default/node_modules/.bin/astro-check +21 -0
  138. package/templates/default/node_modules/.bin/grove +21 -0
  139. package/templates/default/node_modules/.bin/jiti +21 -0
  140. package/templates/default/node_modules/.bin/prettier +21 -0
  141. package/templates/default/node_modules/.bin/rollup +21 -0
  142. package/templates/default/node_modules/.bin/tsc +21 -0
  143. package/templates/default/node_modules/.bin/tsserver +21 -0
  144. package/templates/default/node_modules/.bin/tsx +21 -0
  145. package/templates/default/node_modules/.bin/vite +21 -0
  146. package/templates/default/node_modules/.bin/yaml +21 -0
  147. package/templates/default/node_modules/.vite/deps/_metadata.json +79 -0
  148. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js +433 -0
  149. package/templates/default/node_modules/.vite/deps/astro-KRVZHBC5.js.map +7 -0
  150. package/templates/default/node_modules/.vite/deps/astro___aria-query.js +5 -0
  151. package/templates/default/node_modules/.vite/deps/astro___aria-query.js.map +7 -0
  152. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js +5 -0
  153. package/templates/default/node_modules/.vite/deps/astro___axobject-query.js.map +7 -0
  154. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js +9 -0
  155. package/templates/default/node_modules/.vite/deps/astro___html-escaper.js.map +7 -0
  156. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +241 -0
  157. package/templates/default/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +7 -0
  158. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js +1592 -0
  159. package/templates/default/node_modules/.vite/deps/audit-GYHQ3ITH.js.map +7 -0
  160. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
  161. package/templates/default/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
  162. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js +186 -0
  163. package/templates/default/node_modules/.vite/deps/chunk-7P4KIK53.js.map +7 -0
  164. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js +48 -0
  165. package/templates/default/node_modules/.vite/deps/chunk-BQGHFQJH.js.map +7 -0
  166. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js +6779 -0
  167. package/templates/default/node_modules/.vite/deps/chunk-CJCDKWP2.js.map +7 -0
  168. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js +33 -0
  169. package/templates/default/node_modules/.vite/deps/chunk-FHJ3XBDE.js.map +7 -0
  170. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js +88 -0
  171. package/templates/default/node_modules/.vite/deps/chunk-FQEMNIUQ.js.map +7 -0
  172. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js +72 -0
  173. package/templates/default/node_modules/.vite/deps/chunk-HBSVDAQD.js.map +7 -0
  174. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js +3757 -0
  175. package/templates/default/node_modules/.vite/deps/chunk-HDOQ2U3U.js.map +7 -0
  176. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js +54 -0
  177. package/templates/default/node_modules/.vite/deps/chunk-XEBKRCNE.js.map +7 -0
  178. package/templates/default/node_modules/.vite/deps/package.json +3 -0
  179. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js +213 -0
  180. package/templates/default/node_modules/.vite/deps/settings-C5H4VHXU.js.map +7 -0
  181. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js +525 -0
  182. package/templates/default/node_modules/.vite/deps/toolbar-H5DB6HVP.js.map +7 -0
  183. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js +1020 -0
  184. package/templates/default/node_modules/.vite/deps/ui-library-JGHU6JE2.js.map +7 -0
  185. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js +147 -0
  186. package/templates/default/node_modules/.vite/deps/xray-EFCQOW4E.js.map +7 -0
  187. package/templates/default/package.json +6 -5
  188. package/templates/default/public/llms-full.txt +14 -0
  189. package/templates/default/public/llms.txt +11 -0
  190. package/templates/default/src/components/layout/BaseLayout.astro +10 -57
  191. package/templates/default/src/components/layout/Footer.astro +16 -134
  192. package/templates/default/src/components/layout/Header.astro +25 -126
  193. package/templates/default/src/data/records.ts +161 -0
  194. package/templates/default/src/lib/markdown.ts +171 -0
  195. package/templates/default/src/pages/about.astro +95 -429
  196. package/templates/default/src/pages/index.astro +258 -61
  197. package/templates/default/src/pages/projects/[slug].astro +480 -0
  198. package/templates/default/src/pages/projects/index.astro +193 -0
  199. package/templates/default/src/pages/sitemap.xml.ts +73 -61
  200. package/templates/default/src/pages/submit.astro +122 -411
  201. package/templates/default/src/styles/global.css +42 -68
  202. package/src/components/ActiveFilterChips.astro +0 -20
  203. package/src/components/DirectoryFilters.astro +0 -100
  204. package/src/components/DirectoryHero.astro +0 -32
  205. package/src/components/HealthBadge.astro +0 -12
  206. package/src/components/LensTabs.astro +0 -26
  207. package/src/components/MethodologyPanel.astro +0 -20
  208. package/src/components/Pagination.astro +0 -24
  209. package/src/components/ProjectDetail.astro +0 -166
  210. package/src/components/SubmitDraft.astro +0 -137
  211. package/templates/default/src/components/AppCard.astro +0 -248
  212. package/templates/default/src/components/AppSection.astro +0 -58
  213. package/templates/default/src/components/AppsIndexRow.astro +0 -230
  214. package/templates/default/src/components/AppsPagination.astro +0 -147
  215. package/templates/default/src/components/CategoryGrid.astro +0 -80
  216. package/templates/default/src/components/ContributorsGrid.astro +0 -105
  217. package/templates/default/src/components/DecisionRow.astro +0 -219
  218. package/templates/default/src/components/ExploreByCategory.astro +0 -74
  219. package/templates/default/src/components/ExploreByStack.astro +0 -102
  220. package/templates/default/src/components/Hero.astro +0 -176
  221. package/templates/default/src/components/MinimalAbout.astro +0 -57
  222. package/templates/default/src/components/OriginalCollection.astro +0 -144
  223. package/templates/default/src/components/RefinePanel.astro +0 -272
  224. package/templates/default/src/components/ScoreBars.astro +0 -72
  225. package/templates/default/src/components/SmartLensTabs.astro +0 -65
  226. package/templates/default/src/components/StackGrid.astro +0 -95
  227. package/templates/default/src/components/WhyThisExists.astro +0 -81
  228. package/templates/default/src/components/icons/Icon.astro +0 -148
  229. package/templates/default/src/components/layout/SectionHeader.astro +0 -42
  230. package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
  231. package/templates/default/src/data/apps.ts +0 -149
  232. package/templates/default/src/data/categories.ts +0 -27
  233. package/templates/default/src/data/config.ts +0 -28
  234. package/templates/default/src/data/contributors.ts +0 -96
  235. package/templates/default/src/data/stacks.ts +0 -47
  236. package/templates/default/src/data/stats.ts +0 -65
  237. package/templates/default/src/data/types.ts +0 -248
  238. package/templates/default/src/lib/apps-search.ts +0 -228
  239. package/templates/default/src/lib/format.ts +0 -36
  240. package/templates/default/src/lib/lenses.ts +0 -98
  241. package/templates/default/src/lib/repo.ts +0 -12
  242. package/templates/default/src/lib/scores.ts +0 -51
  243. package/templates/default/src/lib/taxonomy-counts.ts +0 -31
  244. package/templates/default/src/pages/apps/[slug].astro +0 -709
  245. package/templates/default/src/pages/apps/index.astro +0 -669
  246. package/templates/default/src/pages/contributors.astro +0 -142
@@ -1,709 +0,0 @@
1
- ---
2
- import { siteConfig } from "../../data/config";
3
- import BaseLayout from "../../components/layout/BaseLayout.astro";
4
- import type { OpenSourceApp } from "../../data/types";
5
- import { apps } from "../../data/apps";
6
- import Container from "../../components/layout/Container.astro";
7
- import Icon from "../../components/icons/Icon.astro";
8
- import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "../../lib/repo";
9
- import { formatDate, formatStars } from "../../lib/format";
10
- import { SCORE_DIMENSIONS, SCORE_LABELS, scoreTier, scoreLabel, SCORE_REASONING } from "../../lib/scores";
11
- import { stats } from "../../data/stats";
12
-
13
- export function getStaticPaths() {
14
- return apps.map((app) => ({
15
- params: { slug: app.slug },
16
- props: { app },
17
- }));
18
- }
19
-
20
- interface Props {
21
- app: OpenSourceApp;
22
- }
23
-
24
- const { app } = Astro.props;
25
-
26
- const { owner, repo } = getOwnerAndRepoFromRepoUrl(app.repoUrl);
27
-
28
- // Initials placeholder fallback
29
- const initials = app.name
30
- .split(/\s+/)
31
- .map((w) => w[0])
32
- .filter(Boolean)
33
- .slice(0, 2)
34
- .join("")
35
- .toUpperCase();
36
-
37
- // Owner avatar URL — prefer the curated logoUrl if set, fall back to
38
- // the GitHub owner avatar (matches the AppCard pattern on the home page).
39
- const ownerAvatar = app.logoUrl ?? getOwnerAvatarUrl(owner, 160);
40
-
41
- // Label for the primary label badge (new / hot / mature / stale).
42
- // Stale apps override the "mature" label — see AppCard.astro for
43
- // the same logic; both call sites must stay in sync.
44
- const isStale = app.status === "stale";
45
- const primaryLabel = isStale
46
- ? "stale"
47
- : app.labels?.find((l) => l === "new" || l === "hot" || l === "mature") ??
48
- app.labels?.[0];
49
-
50
- const labelClass = primaryLabel === "new"
51
- ? "badge-new"
52
- : primaryLabel === "hot"
53
- ? "badge-hot"
54
- : primaryLabel === "mature"
55
- ? "badge-mature"
56
- : primaryLabel === "stale"
57
- ? "badge-stale"
58
- : "";
59
-
60
- // All stacks (primary + secondary) for chips
61
- const allStacks = [app.stack, ...(app.stacks ?? [])].filter(Boolean);
62
-
63
- // Curation display state
64
- const hasScores = app.scores && Object.values(app.scores).some((v) => typeof v === "number");
65
- const hasCuration =
66
- (app.bestFor && app.bestFor.length > 0) ||
67
- (app.whyListed && app.whyListed.length > 0) ||
68
- (app.caveats && app.caveats.length > 0);
69
-
70
- const starsLabel = formatStars(app.stars);
71
-
72
- const repoMeta = app.github?.repository;
73
- const repoHealth = app.health;
74
- const rawMonthly = app.github?.activity?.monthlyCommits ?? [];
75
- const monthlyCommits = rawMonthly.map((item) =>
76
- typeof item === "number" ? { month: "", commits: item } : item,
77
- );
78
- const maxMonthlyCommits = Math.max(1, ...monthlyCommits.map((item) => item.commits ?? 0));
79
- const languages = app.github?.languages
80
- ? Object.entries(app.github.languages)
81
- .sort((a, b) => b[1] - a[1])
82
- .slice(0, 5)
83
- : [];
84
- const totalLanguageBytes = Math.max(1, languages.reduce((sum, [, bytes]) => sum + bytes, 0));
85
- const files = app.github?.files ?? {};
86
- const contributionSignals = [
87
- { label: "README", ok: files.readme },
88
- { label: "Contributing", ok: files.contributing },
89
- { label: "Security", ok: files.security },
90
- { label: "Issue templates", ok: files.issueTemplates },
91
- { label: "PR template", ok: files.pullRequestTemplate },
92
- ].filter((signal) => signal.ok !== undefined);
93
- const latestRelease = app.github?.latestRelease;
94
- const latestReleaseAt =
95
- typeof latestRelease?.published_at === "string"
96
- ? latestRelease.published_at
97
- : undefined;
98
- const contributorsKnown = app.github?.activity?.contributorsKnown;
99
- const totalCommitsKnown = app.github?.activity?.totalCommitsKnown;
100
- const distributionChannels = app.distribution?.channels ?? [];
101
- const distributionLabel = (type: string) => ({
102
- "app-store": "App Store",
103
- "play-store": "Play Store",
104
- fdroid: "F-Droid",
105
- "github-releases": "GitHub Releases",
106
- apk: "APK",
107
- testflight: "TestFlight",
108
- website: "Website",
109
- snapcraft: "Snapcraft",
110
- flathub: "Flathub",
111
- "microsoft-store": "Microsoft Store",
112
- other: "Download",
113
- }[type] ?? "Download");
114
-
115
- const title = `${app.name} — ${siteConfig.name}`;
116
- const description = `${app.name} — ${app.description}`;
117
-
118
- // JSON-LD structured data for this app. Uses schema.org's
119
- // SoftwareSourceCode type — that's the right fit for a source
120
- // repository, more so than the generic SoftwareApplication. Helps
121
- // Google show rich results (programming language, license, last
122
- // commit date) on app detail pages.
123
- const jsonLd: Record<string, unknown> = {
124
- "@context": "https://schema.org",
125
- "@type": "SoftwareSourceCode",
126
- name: app.name,
127
- headline: app.name,
128
- description: app.description,
129
- url: app.repoUrl,
130
- codeRepository: app.repoUrl,
131
- ...(app.homepageUrl ? { sameAs: [app.repoUrl, app.homepageUrl] } : { sameAs: [app.repoUrl] }),
132
- ...(app.stack ? { programmingLanguage: app.stack } : {}),
133
- ...(app.license ? { license: app.license } : {}),
134
- ...(app.category ? { applicationCategory: app.category } : {}),
135
- ...(app.platforms && app.platforms.length > 0
136
- ? { operatingSystem: app.platforms.join(", ") }
137
- : {}),
138
- ...(app.tags && app.tags.length > 0 ? { keywords: app.tags.join(", ") } : {}),
139
- ...(app.addedAt ? { dateCreated: app.addedAt } : {}),
140
- ...(app.lastCommitAt ? { dateModified: app.lastCommitAt } : {}),
141
- ...(app.github?.repository?.stargazers_count !== undefined
142
- ? { interactionStatistic: {
143
- "@type": "InteractionCounter",
144
- interactionType: { "@type": "LikeAction" },
145
- userInteractionCount: app.github.repository.stargazers_count,
146
- } }
147
- : {}),
148
- author: { "@type": "Organization", name: owner ?? app.name, url: app.repoUrl },
149
- isAccessibleForFree: true,
150
- };
151
- ---
152
-
153
- <BaseLayout title={title} description={description} jsonLd={jsonLd}>
154
- <Container>
155
- <article class="pt-10 sm:pt-14">
156
- {/* Breadcrumb */}
157
- <nav class="text-2xs text-ink-500 dark:text-ink-400" aria-label="Breadcrumb">
158
- <a href="/apps" class="hover:text-ink-900 dark:hover:text-ink-100">Apps</a>
159
- <span class="mx-1.5 text-ink-300 dark:text-ink-700">/</span>
160
- <span class="text-ink-700 dark:text-ink-300">{app.name}</span>
161
- </nav>
162
-
163
- {/* Header */}
164
- <header class="mt-5 flex flex-col gap-5 sm:flex-row sm:items-start sm:justify-between">
165
- <div class="flex items-start gap-4">
166
- {ownerAvatar ? (
167
- <img
168
- src={ownerAvatar}
169
- alt={`${owner} avatar`}
170
- width="64"
171
- height="64"
172
- class="h-16 w-16 shrink-0 rounded-lg border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
173
- />
174
- ) : (
175
- <span
176
- aria-hidden="true"
177
- class="flex h-16 w-16 shrink-0 items-center justify-center rounded-lg border border-ink-200 bg-ink-50 text-base font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300"
178
- >
179
- {initials}
180
- </span>
181
- )}
182
- <div class="flex min-w-0 flex-col gap-1">
183
- <h1 class="m-0 text-[1.75rem] sm:text-[2rem] font-semibold tracking-[-0.02em] leading-[1.1] text-ink-900 dark:text-ink-100">
184
- {app.name}
185
- </h1>
186
- <div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-2xs text-ink-500 dark:text-ink-400">
187
- {owner && (
188
- <a
189
- href={app.repoUrl}
190
- target="_blank"
191
- rel="noopener noreferrer"
192
- class="font-mono hover:text-ink-900 dark:hover:text-ink-100"
193
- >
194
- @{owner}{repo ? <span class="text-ink-400 dark:text-ink-500">/{repo}</span> : ""}
195
- </a>
196
- )}
197
- <span class="text-ink-300 dark:text-ink-700">·</span>
198
- <span>{app.category}</span>
199
- {app.status && (
200
- <>
201
- <span class="text-ink-300 dark:text-ink-700">·</span>
202
- <span class="capitalize">{app.status}</span>
203
- </>
204
- )}
205
- </div>
206
- </div>
207
- </div>
208
-
209
- <div class="flex shrink-0 flex-wrap items-center gap-2 sm:flex-col sm:items-end sm:gap-2">
210
- {primaryLabel && (
211
- <span class={`badge ${labelClass} text-[10px] leading-none`}>
212
- {primaryLabel}
213
- </span>
214
- )}
215
- <a
216
- href={app.repoUrl}
217
- target="_blank"
218
- rel="noopener noreferrer"
219
- class="btn-primary"
220
- >
221
- <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true" fill="currentColor">
222
- <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38v-1.33c-2.22.48-2.69-1.07-2.69-1.07-.36-.92-.89-1.17-.89-1.17-.73-.5.05-.49.05-.49.8.06 1.23.83 1.23.83.72 1.22 1.87.87 2.33.67.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48v2.19c0 .21.15.46.55.38A8 8 0 0 0 8 0Z" />
223
- </svg>
224
- View on GitHub
225
- </a>
226
- {app.homepageUrl && (
227
- <a
228
- href={app.homepageUrl}
229
- target="_blank"
230
- rel="noopener noreferrer"
231
- class="btn-outline"
232
- >
233
- Homepage
234
- <span aria-hidden="true">↗</span>
235
- </a>
236
- )}
237
- </div>
238
- </header>
239
-
240
- {distributionChannels.length > 0 && (
241
- <section class="mt-6 flex flex-wrap items-center gap-2">
242
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
243
- Download
244
- </span>
245
- {distributionChannels.map((channel) => (
246
- <a
247
- href={channel.url}
248
- target="_blank"
249
- rel="noopener noreferrer"
250
- class="btn-outline"
251
- title={channel.notes}
252
- >
253
- {channel.label ?? distributionLabel(channel.type)}
254
- {channel.platform && (
255
- <span class="text-ink-400 dark:text-ink-500">· {channel.platform}</span>
256
- )}
257
- </a>
258
- ))}
259
- </section>
260
- )}
261
-
262
- {/* Description */}
263
- <p class="mt-6 max-w-[68ch] text-[0.9375rem] sm:text-base leading-relaxed text-ink-700 dark:text-ink-200 m-0">
264
- {app.description}
265
- </p>
266
-
267
- {/* Metadata grid */}
268
- <dl class="mt-8 grid grid-cols-1 gap-px overflow-hidden rounded-lg border border-ink-200 bg-ink-200 text-2xs sm:grid-cols-2 lg:grid-cols-4 dark:border-ink-800 dark:bg-ink-800">
269
- <div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
270
- <dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Primary stack</dt>
271
- <dd class="text-ink-900 dark:text-ink-100">
272
- <span class="chip chip-stack">
273
- <Icon name={app.stack} category="stack" size={11} />
274
- <span>{app.stack}</span>
275
- </span>
276
- </dd>
277
- </div>
278
- <div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
279
- <dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Platforms</dt>
280
- <dd class="flex flex-wrap gap-1 text-ink-900 dark:text-ink-100">
281
- {app.platforms.map((p) => (
282
- <span class="chip">
283
- <Icon name={p} category="platform" size={11} />
284
- <span>{p}</span>
285
- </span>
286
- ))}
287
- </dd>
288
- </div>
289
- <div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
290
- <dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">License</dt>
291
- <dd class="font-mono text-ink-900 dark:text-ink-100">{app.license ?? "—"}</dd>
292
- </div>
293
- <div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
294
- <dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Stars</dt>
295
- <dd class="font-mono text-ink-900 dark:text-ink-100">
296
- {starsLabel ?? "—"}
297
- </dd>
298
- </div>
299
- {allStacks.length > 1 && (
300
- <div class="flex flex-col gap-1 bg-white p-4 sm:col-span-2 dark:bg-ink-950">
301
- <dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Also uses</dt>
302
- <dd class="flex flex-wrap gap-1 text-ink-900 dark:text-ink-100">
303
- {app.stacks?.map((s) => (
304
- <span class="chip chip-stack">
305
- <Icon name={s} category="stack" size={11} />
306
- <span>{s}</span>
307
- </span>
308
- ))}
309
- </dd>
310
- </div>
311
- )}
312
- {app.addedAt && (
313
- <div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
314
- <dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Added</dt>
315
- <dd class="font-mono text-ink-900 dark:text-ink-100">{formatDate(app.addedAt)}</dd>
316
- </div>
317
- )}
318
- {app.lastCommitAt && (
319
- <div class="flex flex-col gap-1 bg-white p-4 dark:bg-ink-950">
320
- <dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Last commit</dt>
321
- <dd class="font-mono text-ink-900 dark:text-ink-100">{formatDate(app.lastCommitAt)}</dd>
322
- </div>
323
- )}
324
- </dl>
325
-
326
- {/* Decision signals */}
327
- <section class="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-3">
328
- <div class="card p-5">
329
- <h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
330
- Repository health
331
- </h2>
332
- <dl class="mt-4 grid gap-3 text-sm">
333
- <div class="flex items-center justify-between gap-3">
334
- <dt class="text-ink-500 dark:text-ink-400">Status</dt>
335
- <dd class="font-medium capitalize text-ink-900 dark:text-ink-100">
336
- {repoHealth?.status ?? app.status ?? "unknown"}
337
- </dd>
338
- </div>
339
- <div class="flex items-center justify-between gap-3">
340
- <dt class="text-ink-500 dark:text-ink-400">Tier</dt>
341
- <dd class="font-medium capitalize text-ink-900 dark:text-ink-100">
342
- {repoHealth?.tier ?? app.tier ?? "listed"}
343
- </dd>
344
- </div>
345
- <div class="flex items-center justify-between gap-3">
346
- <dt class="text-ink-500 dark:text-ink-400">Forks</dt>
347
- <dd class="font-mono text-ink-900 dark:text-ink-100">
348
- {repoMeta?.forks_count ?? "—"}
349
- </dd>
350
- </div>
351
- <div class="flex items-center justify-between gap-3">
352
- <dt class="text-ink-500 dark:text-ink-400">Contributors</dt>
353
- <dd class="font-mono text-ink-900 dark:text-ink-100">
354
- {contributorsKnown ?? "—"}
355
- </dd>
356
- </div>
357
- <div class="flex items-center justify-between gap-3">
358
- <dt class="text-ink-500 dark:text-ink-400">Commits known</dt>
359
- <dd class="font-mono text-ink-900 dark:text-ink-100">
360
- {totalCommitsKnown ?? "—"}
361
- </dd>
362
- </div>
363
- <div class="flex items-center justify-between gap-3">
364
- <dt class="text-ink-500 dark:text-ink-400">Open issues</dt>
365
- <dd class="font-mono text-ink-900 dark:text-ink-100">
366
- {repoMeta?.open_issues_count ?? "—"}
367
- </dd>
368
- </div>
369
- {latestReleaseAt && (
370
- <div class="flex items-center justify-between gap-3">
371
- <dt class="text-ink-500 dark:text-ink-400">Latest release</dt>
372
- <dd class="font-mono text-ink-900 dark:text-ink-100">
373
- {formatDate(latestReleaseAt)}
374
- </dd>
375
- </div>
376
- )}
377
- </dl>
378
- </div>
379
-
380
- <div class="card p-5">
381
- <h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
382
- Activity
383
- </h2>
384
- {monthlyCommits.length > 0 ? (
385
- <figure class="mt-4 m-0">
386
- <div
387
- class="flex h-28 items-end gap-1.5"
388
- role="img"
389
- aria-label={`Monthly commits over the last ${monthlyCommits.length} months. Peak: ${maxMonthlyCommits} commits.`}
390
- >
391
- {monthlyCommits.map((item) => (
392
- <div class="flex min-w-0 flex-1 flex-col items-center gap-1">
393
- <div
394
- class="w-full rounded-sm bg-ink-700 dark:bg-ink-200"
395
- style={`height: ${Math.max(8, Math.round(((item.commits ?? 0) / maxMonthlyCommits) * 96))}px`}
396
- title={`${item.month || "month"}: ${item.commits ?? 0} commits`}
397
- />
398
- <span class="max-w-full truncate text-[10px] text-ink-500 dark:text-ink-400">
399
- {item.month ? item.month.slice(5) : item.commits}
400
- </span>
401
- </div>
402
- ))}
403
- </div>
404
- {/* Visually-hidden data table so screen readers can read
405
- the same values sighted users see in the bars. */}
406
- <figcaption class="sr-only">
407
- <table>
408
- <caption>Monthly commit count over the last {monthlyCommits.length} months</caption>
409
- <thead>
410
- <tr><th scope="col">Month</th><th scope="col">Commits</th></tr>
411
- </thead>
412
- <tbody>
413
- {monthlyCommits.map((item) => (
414
- <tr>
415
- <th scope="row">{item.month || "Unknown"}</th>
416
- <td>{item.commits ?? 0}</td>
417
- </tr>
418
- ))}
419
- </tbody>
420
- </table>
421
- </figcaption>
422
- </figure>
423
- ) : (
424
- <div class="mt-4 space-y-1.5">
425
- <p class="m-0 text-sm text-ink-500 dark:text-ink-400">
426
- Monthly commit history is not yet populated for this app.
427
- </p>
428
- <p class="m-0 text-2xs text-ink-400 dark:text-ink-500">
429
- Activity data refreshes daily via
430
- <a
431
- href={`${stats.originalRepo}/blob/main/.github/workflows/sync-github-metadata.yml`}
432
- target="_blank"
433
- rel="noopener noreferrer"
434
- class="text-ink-700 hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
435
- >sync-github-metadata</a
436
- >. Last commit and stars are already in sync.
437
- </p>
438
- </div>
439
- )}
440
- </div>
441
-
442
- <div class="card p-5">
443
- <h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
444
- Contribution readiness
445
- </h2>
446
- {contributionSignals.length > 0 ? (
447
- <ul class="mt-4 m-0 grid list-none gap-2 p-0 text-sm">
448
- {contributionSignals.map((signal) => (
449
- <li class="flex items-center justify-between gap-3">
450
- <span class="text-ink-700 dark:text-ink-200">{signal.label}</span>
451
- <span class={signal.ok ? "text-emerald-700 dark:text-emerald-400" : "text-ink-400 dark:text-ink-600"}>
452
- {signal.ok ? "Found" : "Missing"}
453
- </span>
454
- </li>
455
- ))}
456
- </ul>
457
- ) : (
458
- <p class="mt-4 m-0 text-sm text-ink-500 dark:text-ink-400">
459
- Contribution files have not been synced yet.
460
- </p>
461
- )}
462
- </div>
463
- </section>
464
-
465
- {languages.length > 0 && (
466
- <section class="mt-4 card p-5">
467
- <h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
468
- Language mix
469
- </h2>
470
- <dl class="mt-4 grid gap-2 text-sm">
471
- {languages.map(([language, bytes]) => {
472
- const pct = Math.round((bytes / totalLanguageBytes) * 100);
473
- return (
474
- <div class="grid grid-cols-[8rem_minmax(0,1fr)_3rem] items-center gap-3">
475
- <dt class="truncate text-ink-700 dark:text-ink-200">{language}</dt>
476
- <dd class="h-2 overflow-hidden rounded-sm bg-ink-200 dark:bg-ink-800">
477
- <div class="h-full bg-ink-700 dark:bg-ink-200" style={`width: ${pct}%`} />
478
- </dd>
479
- <dd class="text-right font-mono text-2xs text-ink-500 dark:text-ink-400">{pct}%</dd>
480
- </div>
481
- );
482
- })}
483
- </dl>
484
- </section>
485
- )}
486
-
487
- {/* Curation block — scores + Best for / Why listed / Caveats */}
488
- {(hasScores || hasCuration) && (
489
- <section class="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-5">
490
- {hasScores && (
491
- <div class="card p-5 lg:col-span-3">
492
- <header class="mb-4 flex items-baseline justify-between gap-2">
493
- <h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
494
- Scores
495
- </h2>
496
- {app.curation?.reviewed ? (
497
- <span class="inline-flex items-center gap-1 rounded border border-emerald-300/60 bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300">
498
- Curator reviewed
499
- </span>
500
- ) : app.curation?.by ? (
501
- <span class="text-2xs text-ink-500 dark:text-ink-400">
502
- Auto-curated · {app.curation.by}
503
- </span>
504
- ) : (
505
- <span class="text-2xs text-ink-500 dark:text-ink-400">
506
- Auto-curated
507
- </span>
508
- )}
509
- </header>
510
- <dl class="flex flex-col gap-2.5 text-sm">
511
- {SCORE_DIMENSIONS.map((dim) => {
512
- const v = app.scores![dim];
513
- const tier = typeof v === "number" ? scoreTier(v) : 0;
514
- const isOverall = dim === "overall";
515
- return (
516
- <div class="flex items-center gap-3">
517
- <dt
518
- class={`w-24 shrink-0 ${
519
- isOverall
520
- ? "font-semibold text-ink-900 dark:text-ink-100"
521
- : "text-ink-700 dark:text-ink-200"
522
- }`}
523
- >
524
- {SCORE_LABELS[dim]}
525
- </dt>
526
- <dd
527
- class="flex h-2 flex-1 gap-px"
528
- aria-label={`${SCORE_LABELS[dim]} ${scoreLabel(v)} of 100`}
529
- >
530
- {[0, 1, 2, 3].map((i) => (
531
- <div
532
- class={`flex-1 rounded-sm ${
533
- i < tier
534
- ? isOverall
535
- ? "bg-ink-700 dark:bg-ink-200"
536
- : "bg-ink-500 dark:bg-ink-400"
537
- : "bg-ink-200 dark:bg-ink-800"
538
- }`}
539
- />
540
- ))}
541
- </dd>
542
- <dd
543
- class={`w-7 shrink-0 text-right font-mono tabular-nums ${
544
- isOverall
545
- ? "font-semibold text-ink-900 dark:text-ink-100"
546
- : "text-ink-700 dark:text-ink-300"
547
- }`}
548
- >
549
- {scoreLabel(v)}
550
- </dd>
551
- </div>
552
- );
553
- })}
554
- </dl>
555
- <dl class="mt-5 flex flex-col gap-1.5 border-t border-ink-200 pt-4 text-2xs dark:border-ink-800">
556
- {SCORE_DIMENSIONS.map((dim) => (
557
- <div class="flex items-baseline gap-2">
558
- <dt class="w-24 shrink-0 font-medium text-ink-500 dark:text-ink-400">
559
- {SCORE_LABELS[dim]}
560
- </dt>
561
- <dd class="text-ink-600 dark:text-ink-300">
562
- {SCORE_REASONING[dim]}
563
- </dd>
564
- </div>
565
- ))}
566
- </dl>
567
- </div>
568
- )}
569
-
570
- {hasCuration && (
571
- <div class="card p-5 lg:col-span-2">
572
- <h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
573
- Curation
574
- </h2>
575
- <div class="mt-4 flex flex-col gap-4 text-sm">
576
- {app.bestFor && app.bestFor.length > 0 && (
577
- <div>
578
- <div class="text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
579
- Best for
580
- </div>
581
- <ul class="mt-1.5 m-0 list-none space-y-1 p-0 text-ink-700 dark:text-ink-200">
582
- {app.bestFor.map((s) => (
583
- <li class="flex items-baseline gap-2">
584
- <span class="text-ink-400 dark:text-ink-500" aria-hidden="true">·</span>
585
- <span>{s}</span>
586
- </li>
587
- ))}
588
- </ul>
589
- </div>
590
- )}
591
- {app.whyListed && app.whyListed.length > 0 && (
592
- <div>
593
- <div class="text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
594
- Why listed
595
- </div>
596
- <ul class="mt-1.5 m-0 list-none space-y-1 p-0 text-ink-700 dark:text-ink-200">
597
- {app.whyListed.map((s) => (
598
- <li class="flex items-baseline gap-2">
599
- <span class="text-ink-400 dark:text-ink-500" aria-hidden="true">·</span>
600
- <span>{s}</span>
601
- </li>
602
- ))}
603
- </ul>
604
- </div>
605
- )}
606
- {app.caveats && app.caveats.length > 0 && (
607
- <div>
608
- <div class="text-2xs font-semibold uppercase tracking-wider text-amber-700 dark:text-amber-400">
609
- Caveats
610
- </div>
611
- <ul class="mt-1.5 m-0 list-none space-y-1 p-0 text-ink-700 dark:text-ink-200">
612
- {app.caveats.map((s) => (
613
- <li class="flex items-baseline gap-2">
614
- <span class="text-amber-500 dark:text-amber-400" aria-hidden="true">·</span>
615
- <span>{s}</span>
616
- </li>
617
- ))}
618
- </ul>
619
- </div>
620
- )}
621
- </div>
622
- </div>
623
- )}
624
- </section>
625
- )}
626
-
627
- {/* Project shape — secondary curation metadata */}
628
- {(app.projectType || app.stateManagement || app.backend || app.architecture || app.difficulty || app.codebaseSize) && (
629
- <section class="mt-4 card p-5">
630
- <h2 class="m-0 text-[1rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
631
- Project shape
632
- </h2>
633
- <dl class="mt-4 grid grid-cols-1 gap-x-6 gap-y-3 text-sm sm:grid-cols-2 lg:grid-cols-3">
634
- {app.projectType && (
635
- <div>
636
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Type</dt>
637
- <dd class="mt-0.5 text-ink-700 dark:text-ink-200">
638
- {app.projectType === "real-app" ? "Real app" :
639
- app.projectType === "production" ? "Production" :
640
- app.projectType === "reference" ? "Reference" :
641
- app.projectType === "demo" ? "Demo" :
642
- app.projectType === "template" ? "Template" : app.projectType}
643
- </dd>
644
- </div>
645
- )}
646
- {app.stateManagement && (
647
- <div>
648
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">State management</dt>
649
- <dd class="mt-0.5 text-ink-700 dark:text-ink-200">{app.stateManagement}</dd>
650
- </div>
651
- )}
652
- {app.backend && (
653
- <div>
654
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Backend</dt>
655
- <dd class="mt-0.5 text-ink-700 dark:text-ink-200">{app.backend}</dd>
656
- </div>
657
- )}
658
- {app.architecture && (
659
- <div>
660
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Architecture</dt>
661
- <dd class="mt-0.5 text-ink-700 dark:text-ink-200">{app.architecture}</dd>
662
- </div>
663
- )}
664
- {app.difficulty && (
665
- <div>
666
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Difficulty</dt>
667
- <dd class="mt-0.5 capitalize text-ink-700 dark:text-ink-200">{app.difficulty}</dd>
668
- </div>
669
- )}
670
- {app.codebaseSize && (
671
- <div>
672
- <dt class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Codebase size</dt>
673
- <dd class="mt-0.5 capitalize text-ink-700 dark:text-ink-200">{app.codebaseSize}</dd>
674
- </div>
675
- )}
676
- </dl>
677
- </section>
678
- )}
679
-
680
- {/* Tags */}
681
- {app.tags && app.tags.length > 0 && (
682
- <div class="mt-4 flex flex-wrap items-center gap-1.5">
683
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Tags</span>
684
- {app.tags.map((t) => (
685
- <a
686
- href={`/apps?q=${encodeURIComponent(t)}`}
687
- class="chip hover:border-ink-300 dark:hover:border-ink-700"
688
- >
689
- {t}
690
- </a>
691
- ))}
692
- </div>
693
- )}
694
-
695
- {/* Related section removed — this is a code reference, not an
696
- e-commerce page. Use the Apps list to find more by stack. */}
697
- </article>
698
- </Container>
699
- <style>
700
- @reference "../../styles/global.css";
701
-
702
- .chip {
703
- @apply inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-1.5 py-0.5 text-2xs font-medium text-ink-700 transition-colors dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300;
704
- }
705
- .chip-stack {
706
- @apply bg-ink-50 dark:bg-ink-900/60;
707
- }
708
- </style>
709
- </BaseLayout>