@grove-dev/astro 0.1.5 → 0.2.5

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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/dist/index.d.ts +4 -25
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +56 -22
  5. package/dist/index.js.map +1 -1
  6. package/dist/layouts/index.d.ts +25 -0
  7. package/dist/layouts/index.d.ts.map +1 -0
  8. package/dist/layouts/index.js +25 -0
  9. package/dist/layouts/index.js.map +1 -0
  10. package/dist/lib/display.d.ts +33 -0
  11. package/dist/lib/display.d.ts.map +1 -0
  12. package/dist/lib/display.js +104 -0
  13. package/dist/lib/display.js.map +1 -0
  14. package/dist/lib/format.d.ts +30 -0
  15. package/dist/lib/format.d.ts.map +1 -0
  16. package/dist/lib/format.js +73 -0
  17. package/dist/lib/format.js.map +1 -0
  18. package/dist/lib/index.d.ts +26 -0
  19. package/dist/lib/index.d.ts.map +1 -0
  20. package/dist/lib/index.js +26 -0
  21. package/dist/lib/index.js.map +1 -0
  22. package/dist/lib/lenses.d.ts +96 -0
  23. package/dist/lib/lenses.d.ts.map +1 -0
  24. package/dist/lib/lenses.js +181 -0
  25. package/dist/lib/lenses.js.map +1 -0
  26. package/dist/lib/repo.d.ts +30 -0
  27. package/dist/lib/repo.d.ts.map +1 -0
  28. package/dist/lib/repo.js +44 -0
  29. package/dist/lib/repo.js.map +1 -0
  30. package/dist/lib/scores.d.ts +38 -0
  31. package/dist/lib/scores.d.ts.map +1 -0
  32. package/dist/lib/scores.js +83 -0
  33. package/dist/lib/scores.js.map +1 -0
  34. package/dist/lib/search.d.ts +199 -0
  35. package/dist/lib/search.d.ts.map +1 -0
  36. package/dist/lib/search.js +357 -0
  37. package/dist/lib/search.js.map +1 -0
  38. package/dist/lib/taxonomy-counts.d.ts +25 -0
  39. package/dist/lib/taxonomy-counts.d.ts.map +1 -0
  40. package/dist/lib/taxonomy-counts.js +41 -0
  41. package/dist/lib/taxonomy-counts.js.map +1 -0
  42. package/package.json +7 -7
  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 +306 -30
  53. package/src/components/MinimalAbout.astro +82 -0
  54. package/src/components/OriginalCollection.astro +116 -0
  55. package/src/components/RecordSection.astro +88 -0
  56. package/src/components/RefinePanel.astro +197 -0
  57. package/src/components/ScoreBars.astro +92 -22
  58. package/src/components/SmartLensTabs.astro +71 -0
  59. package/src/components/StackGrid.astro +132 -0
  60. package/src/components/WhyThisExists.astro +116 -0
  61. package/src/index.ts +61 -14
  62. package/src/layouts/BaseLayout.astro +227 -15
  63. package/src/layouts/Container.astro +57 -0
  64. package/src/layouts/Footer.astro +235 -0
  65. package/src/layouts/Header.astro +159 -0
  66. package/src/layouts/SectionHeader.astro +58 -0
  67. package/{templates/default/src/components/layout → src/layouts}/Seo.astro +62 -38
  68. package/src/layouts/ThemeToggle.astro +110 -0
  69. package/src/layouts/astro-shim.d.ts +29 -0
  70. package/src/layouts/index.ts +24 -0
  71. package/src/lib/display.ts +118 -0
  72. package/src/lib/format.ts +65 -0
  73. package/src/lib/index.ts +26 -0
  74. package/src/lib/lenses.ts +241 -0
  75. package/src/lib/repo.ts +44 -0
  76. package/src/lib/scores.ts +94 -0
  77. package/src/lib/search.ts +481 -0
  78. package/src/lib/taxonomy-counts.ts +60 -0
  79. package/src/styles.css +366 -638
  80. package/templates/default/astro.config.mjs +8 -6
  81. package/templates/default/content/pages/about.md +66 -0
  82. package/templates/default/content/pages/methodology.md +103 -0
  83. package/templates/default/data/generated/records.full.json +8 -0
  84. package/templates/default/data/generated/records.index.json +7 -0
  85. package/templates/default/data/generated/records.json +8 -0
  86. package/templates/default/data/generated/site-config.json +31 -0
  87. package/templates/default/data/records/.gitkeep +0 -0
  88. package/templates/default/grove.config.ts +36 -0
  89. package/templates/default/package.json +6 -5
  90. package/templates/default/public/llms-full.txt +14 -0
  91. package/templates/default/public/llms.txt +11 -0
  92. package/templates/default/src/components/layout/BaseLayout.astro +10 -57
  93. package/templates/default/src/components/layout/Footer.astro +16 -134
  94. package/templates/default/src/components/layout/Header.astro +25 -126
  95. package/templates/default/src/data/records.ts +193 -0
  96. package/templates/default/src/lib/markdown.ts +171 -0
  97. package/templates/default/src/pages/about.astro +95 -429
  98. package/templates/default/src/pages/index.astro +263 -61
  99. package/templates/default/src/pages/projects/[slug].astro +480 -0
  100. package/templates/default/src/pages/projects/index.astro +193 -0
  101. package/templates/default/src/pages/sitemap.xml.ts +73 -61
  102. package/templates/default/src/pages/submit.astro +122 -411
  103. package/templates/default/src/styles/global.css +42 -68
  104. package/src/components/ActiveFilterChips.astro +0 -20
  105. package/src/components/DirectoryFilters.astro +0 -100
  106. package/src/components/DirectoryHero.astro +0 -32
  107. package/src/components/HealthBadge.astro +0 -12
  108. package/src/components/LensTabs.astro +0 -26
  109. package/src/components/MethodologyPanel.astro +0 -20
  110. package/src/components/Pagination.astro +0 -24
  111. package/src/components/ProjectDetail.astro +0 -166
  112. package/src/components/SubmitDraft.astro +0 -137
  113. package/templates/default/src/components/AppCard.astro +0 -248
  114. package/templates/default/src/components/AppSection.astro +0 -58
  115. package/templates/default/src/components/AppsIndexRow.astro +0 -230
  116. package/templates/default/src/components/AppsPagination.astro +0 -147
  117. package/templates/default/src/components/CategoryGrid.astro +0 -80
  118. package/templates/default/src/components/ContributorsGrid.astro +0 -105
  119. package/templates/default/src/components/DecisionRow.astro +0 -219
  120. package/templates/default/src/components/ExploreByCategory.astro +0 -74
  121. package/templates/default/src/components/ExploreByStack.astro +0 -102
  122. package/templates/default/src/components/Hero.astro +0 -176
  123. package/templates/default/src/components/MinimalAbout.astro +0 -57
  124. package/templates/default/src/components/OriginalCollection.astro +0 -144
  125. package/templates/default/src/components/RefinePanel.astro +0 -272
  126. package/templates/default/src/components/ScoreBars.astro +0 -72
  127. package/templates/default/src/components/SmartLensTabs.astro +0 -65
  128. package/templates/default/src/components/StackGrid.astro +0 -95
  129. package/templates/default/src/components/WhyThisExists.astro +0 -81
  130. package/templates/default/src/components/icons/Icon.astro +0 -148
  131. package/templates/default/src/components/layout/SectionHeader.astro +0 -42
  132. package/templates/default/src/components/layout/ThemeToggle.astro +0 -97
  133. package/templates/default/src/data/apps.ts +0 -149
  134. package/templates/default/src/data/categories.ts +0 -27
  135. package/templates/default/src/data/config.ts +0 -28
  136. package/templates/default/src/data/contributors.ts +0 -96
  137. package/templates/default/src/data/stacks.ts +0 -47
  138. package/templates/default/src/data/stats.ts +0 -65
  139. package/templates/default/src/data/types.ts +0 -248
  140. package/templates/default/src/lib/apps-search.ts +0 -228
  141. package/templates/default/src/lib/format.ts +0 -36
  142. package/templates/default/src/lib/lenses.ts +0 -98
  143. package/templates/default/src/lib/repo.ts +0 -12
  144. package/templates/default/src/lib/scores.ts +0 -51
  145. package/templates/default/src/lib/taxonomy-counts.ts +0 -31
  146. package/templates/default/src/pages/apps/[slug].astro +0 -709
  147. package/templates/default/src/pages/apps/index.astro +0 -669
  148. package/templates/default/src/pages/contributors.astro +0 -142
@@ -1,248 +0,0 @@
1
- ---
2
- import type { OpenSourceApp } from "../data/types";
3
- import Icon from "./icons/Icon.astro";
4
- import { getOwnerAndRepoFromRepoUrl, getOwnerAvatarUrl } from "../lib/repo";
5
- import { formatStars, formatRelative } from "../lib/format";
6
-
7
- interface Props {
8
- app: OpenSourceApp;
9
- }
10
-
11
- const { app } = Astro.props;
12
-
13
- // Stale apps: a repo with no commits in 365+ days is not really
14
- // "mature" — it's a snapshot. Override the label so we don't show
15
- // "Mature" on a 6-year-old fork. (See healthFromSignals in
16
- // scripts/app-schema.mjs for how status is computed.)
17
- const isStale = app.status === "stale";
18
-
19
- const label = isStale
20
- ? "stale"
21
- : app.labels?.find((l) => l === "new" || l === "hot" || l === "mature") ??
22
- app.labels?.[0];
23
-
24
- const labelClass = label === "new"
25
- ? "badge-new"
26
- : label === "hot"
27
- ? "badge-hot"
28
- : label === "mature"
29
- ? "badge-mature"
30
- : label === "stale"
31
- ? "badge-stale"
32
- : "";
33
-
34
- // Curated = at least one of the hand-written fields is filled in. These
35
- // are the apps a human actually looked at. Shown as a "verified" mark
36
- // next to the label badge so it doesn't dominate the card.
37
- const isCurated = Boolean(
38
- app.bestFor?.length || app.whyListed?.length || app.caveats?.length || app.scores,
39
- );
40
-
41
- // Initials for placeholder logo (first 2 letters of any word in name).
42
- const initials = app.name
43
- .split(/\s+/)
44
- .map((w) => w[0])
45
- .filter(Boolean)
46
- .slice(0, 2)
47
- .join("")
48
- .toUpperCase();
49
-
50
- // Resolve a logo URL for this app:
51
- // 1. Explicit `logoUrl` on the app
52
- // 2. GitHub owner avatar (extracted from `repoUrl`)
53
- // 3. Initials placeholder.
54
- const { owner, repo } = getOwnerAndRepoFromRepoUrl(app.repoUrl);
55
- const logoUrl = app.logoUrl ?? getOwnerAvatarUrl(owner, 80);
56
-
57
- // Show up to 4 platforms; overflow as +N.
58
- const visiblePlatforms = app.platforms.slice(0, 4);
59
- const platformOverflow = app.platforms.length - visiblePlatforms.length;
60
-
61
- // Show up to 3 stack chips (primary + first 2 secondary).
62
- const allStacks = [app.stack, ...(app.stacks ?? [])].filter(Boolean);
63
- const visibleStacks = allStacks.slice(0, 3);
64
- const stackOverflow = allStacks.length - visibleStacks.length;
65
-
66
- const starsLabel = formatStars(app.stars);
67
- const updatedLabel = formatRelative(app.lastCommitAt);
68
-
69
- const detailHref = `/apps/${app.slug}`;
70
- const repoHref = app.repoUrl;
71
- ---
72
-
73
- <article
74
- class="card card-hover group relative flex h-full flex-col gap-3 p-4"
75
- >
76
- {/* Top row: logo + name + label badge. */}
77
- <header class="relative z-10 flex items-start gap-3">
78
- <a
79
- href={detailHref}
80
- aria-label={`Open ${app.name} details`}
81
- class="absolute inset-0 z-0 rounded-lg"
82
- ></a>
83
-
84
- {logoUrl ? (
85
- <img
86
- src={logoUrl}
87
- alt={`${app.name} avatar`}
88
- width="40"
89
- height="40"
90
- loading="lazy"
91
- decoding="async"
92
- class="h-10 w-10 shrink-0 rounded-md border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
93
- />
94
- ) : (
95
- <span
96
- aria-hidden="true"
97
- class="flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-2xs font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300"
98
- >
99
- {initials}
100
- </span>
101
- )}
102
-
103
- <div class="flex min-w-0 flex-1 flex-col gap-0.5">
104
- <h3
105
- class="m-0 truncate text-[0.9375rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100"
106
- >
107
- {/* Name → detail page (not GitHub repo). Stacks above the
108
- stretched link via z-index. */}
109
- <a
110
- href={detailHref}
111
- class="relative z-10 text-inherit no-underline"
112
- >
113
- {app.name}
114
- </a>
115
- </h3>
116
- <span class="truncate text-2xs text-ink-500 dark:text-ink-400">
117
- {owner ? (
118
- <>
119
- <a
120
- href={repoHref}
121
- target="_blank"
122
- rel="noopener noreferrer"
123
- class="relative z-10 font-mono text-ink-500 hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
124
- >
125
- <span class="text-ink-400 dark:text-ink-500">@</span>{owner}{repo ? <span class="text-ink-400 dark:text-ink-500">/{repo}</span> : ""}
126
- </a>
127
- <span class="mx-1 text-ink-300 dark:text-ink-700">·</span>
128
- {app.category}
129
- </>
130
- ) : (
131
- app.category
132
- )}
133
- </span>
134
- </div>
135
-
136
- <div class="flex shrink-0 flex-col items-end gap-1">
137
- {label && (
138
- <span class={`badge ${labelClass} text-[10px] leading-none`}>
139
- {label}
140
- </span>
141
- )}
142
- {isCurated && (
143
- <span
144
- class="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-400"
145
- title="Hand-curated entry: bestFor, whyListed, caveats, or scores are filled in"
146
- >
147
- <svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
148
- <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm3.41 5.09a.75.75 0 0 0-1.06 0L6.75 8.69 5.65 7.59a.75.75 0 1 0-1.06 1.06l1.65 1.65a.75.75 0 0 0 1.06 0l4.11-4.11a.75.75 0 0 0 0-1.06Z"/>
149
- </svg>
150
- curated
151
- </span>
152
- )}
153
- </div>
154
- </header>
155
-
156
- {/* Description */}
157
- <p
158
- class="relative z-10 m-0 line-clamp-2 text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400"
159
- >
160
- {app.description}
161
- </p>
162
-
163
- {/* Metadata row: stars · updated · license — the three signals
164
- that answer "is this alive and usable?" at a glance. */}
165
- <div class="relative z-10 flex flex-wrap items-center gap-x-3 gap-y-1 text-2xs text-ink-500 dark:text-ink-400">
166
- {starsLabel !== null && (
167
- <span class="inline-flex items-center gap-1" title={`${app.stars} stars on GitHub`}>
168
- <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-amber-500">
169
- <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/>
170
- </svg>
171
- <span class="font-mono">{starsLabel}</span>
172
- </span>
173
- )}
174
- {app.lastCommitAt && (
175
- <span class="inline-flex items-center gap-1" title={`Last commit ${app.lastCommitAt}`}>
176
- <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor" class="text-ink-400">
177
- <path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Zm0 1.5a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm-.5 1.5a.5.5 0 0 0-1 0v3.25c0 .15.07.29.18.39l1.95 1.65a.5.5 0 0 0 .66-.76L7.5 7.55V5Z"/>
178
- </svg>
179
- <span class="font-mono">Updated {updatedLabel}</span>
180
- </span>
181
- )}
182
- {app.license && (
183
- <span class="inline-flex items-center gap-1" title={`License: ${app.license}`}>
184
- <span class="font-mono uppercase tracking-wide text-[10px]">{app.license}</span>
185
- </span>
186
- )}
187
- </div>
188
-
189
- {/* Platform + stack chips, pushed to the bottom. */}
190
- <div class="relative z-10 mt-auto flex flex-col gap-1.5">
191
- <div class="flex flex-wrap items-center gap-1.5">
192
- {visiblePlatforms.map((p) => (
193
- <span class="chip">
194
- <Icon name={p} category="platform" size={11} />
195
- <span>{p}</span>
196
- </span>
197
- ))}
198
- {platformOverflow > 0 && (
199
- <span class="chip text-ink-400 dark:text-ink-500">+{platformOverflow}</span>
200
- )}
201
- </div>
202
- <div class="flex flex-wrap items-center gap-1.5">
203
- {visibleStacks.map((s) => (
204
- <span class="chip chip-stack">
205
- <Icon name={s} category="stack" size={11} />
206
- <span>{s}</span>
207
- </span>
208
- ))}
209
- {stackOverflow > 0 && (
210
- <span class="chip chip-stack text-ink-400 dark:text-ink-500">+{stackOverflow}</span>
211
- )}
212
- </div>
213
- </div>
214
-
215
- {/* Footer row: View repo + Details links, separated from chips.
216
- Stacks above the stretched link via z-index. */}
217
- <footer class="relative z-10 mt-1 flex items-center justify-between gap-2 border-t border-ink-100 pt-2 text-2xs dark:border-ink-900">
218
- <a
219
- href={repoHref}
220
- target="_blank"
221
- rel="noopener noreferrer"
222
- class="relative z-10 inline-flex items-center gap-1 font-medium text-ink-700 transition-colors hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
223
- >
224
- View repo
225
- <svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
226
- <path d="M3.75 2A1.75 1.75 0 0 0 2 3.75v8.5C2 13.216 2.784 14 3.75 14h8.5A1.75 1.75 0 0 0 14 12.25v-3.5a.75.75 0 0 0-1.5 0v3.5a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-8.5a.25.25 0 0 1 .25-.25h3.5a.75.75 0 0 0 0-1.5h-3.5Z"/>
227
- <path d="M10 0a.75.75 0 0 0-.75.75v.75h-3a.75.75 0 0 0 0 1.5h3v.75A.75.75 0 0 0 10.75 4.5h2.5A.75.75 0 0 0 14 3.75v-2.5A.75.75 0 0 0 13.25.5H10Z"/>
228
- </svg>
229
- </a>
230
- <a
231
- href={detailHref}
232
- class="relative z-10 inline-flex items-center gap-1 font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
233
- >
234
- Details →
235
- </a>
236
- </footer>
237
- </article>
238
-
239
- <style>
240
- @reference "../styles/global.css";
241
-
242
- .chip {
243
- @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;
244
- }
245
- .chip-stack {
246
- @apply bg-ink-50 dark:bg-ink-900/60;
247
- }
248
- </style>
@@ -1,58 +0,0 @@
1
- ---
2
- import type { OpenSourceApp } from "../data/types";
3
- import AppCard from "./AppCard.astro";
4
- import SectionHeader from "./layout/SectionHeader.astro";
5
- import Container from "./layout/Container.astro";
6
-
7
- interface Props {
8
- id: string;
9
- title: string;
10
- /** Plain-language subtitle explaining the lens. */
11
- description: string;
12
- apps: OpenSourceApp[];
13
- /** Where the "View all" link points (typically the apps page with a
14
- * filter applied). */
15
- viewAllHref?: string;
16
- /** Custom label for the "View all" link. Lets us say
17
- * "View all new" / "View hot apps" / "View mature apps"
18
- * instead of the generic "View all". */
19
- viewAllLabel?: string;
20
- }
21
-
22
- const {
23
- id,
24
- title,
25
- description,
26
- apps,
27
- viewAllHref = "#",
28
- viewAllLabel = "View all",
29
- } = Astro.props;
30
- ---
31
-
32
- <section id={id} class="section border-b border-ink-200 dark:border-ink-800 scroll-mt-20">
33
- <Container>
34
- <div class="flex items-end justify-between gap-4">
35
- <SectionHeader title={title} description={description} />
36
- <a
37
- href={viewAllHref}
38
- class="hidden shrink-0 pb-2 text-2xs font-medium text-ink-500 transition-colors hover:text-ink-900 sm:inline dark:text-ink-400 dark:hover:text-ink-100"
39
- >
40
- {viewAllLabel} →
41
- </a>
42
- </div>
43
-
44
- <div
45
- class="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"
46
- >
47
- {apps.map((app) => <AppCard app={app} />)}
48
- </div>
49
-
50
- <div class="mt-4 sm:hidden">
51
- <a
52
- href={viewAllHref}
53
- class="text-2xs font-medium text-ink-500 transition-colors hover:text-ink-900 dark:text-ink-400 dark:hover:text-ink-100"
54
- >{viewAllLabel} →</a
55
- >
56
- </div>
57
- </Container>
58
- </section>
@@ -1,230 +0,0 @@
1
- ---
2
- import type { OpenSourceApp } from "../data/types";
3
- import Icon from "./icons/Icon.astro";
4
- import {
5
- getOwnerAndRepoFromRepoUrl,
6
- getOwnerAvatarUrl,
7
- } from "../lib/repo";
8
- import { formatStars, formatRelative } from "../lib/format";
9
-
10
- interface Props {
11
- app: OpenSourceApp;
12
- }
13
-
14
- const { app } = Astro.props;
15
-
16
- const { owner, repo } = getOwnerAndRepoFromRepoUrl(app.repoUrl);
17
- const avatar = app.logoUrl ?? getOwnerAvatarUrl(owner, 80);
18
-
19
- // "Mature" wins over "Hot" wins over "New" wins over anything else.
20
- const label =
21
- app.labels?.find((l) => l === "mature" || l === "hot" || l === "new") ??
22
- app.labels?.[0];
23
-
24
- const labelClass = label === "new"
25
- ? "border-emerald-300/60 bg-emerald-50 text-emerald-800 dark:border-emerald-800/60 dark:bg-emerald-950/40 dark:text-emerald-300"
26
- : label === "hot"
27
- ? "border-orange-300/60 bg-orange-50 text-orange-800 dark:border-orange-800/60 dark:bg-orange-950/40 dark:text-orange-300"
28
- : label === "mature"
29
- ? "border-blue-300/60 bg-blue-50 text-blue-800 dark:border-blue-800/60 dark:bg-blue-950/40 dark:text-blue-300"
30
- : "border-ink-200 bg-ink-50 text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300";
31
-
32
- const isCurated = Boolean(
33
- app.bestFor?.length || app.whyListed?.length || app.caveats?.length || app.scores,
34
- );
35
-
36
- // Initials for placeholder logo.
37
- const initials = app.name
38
- .split(/\s+/)
39
- .map((w) => w[0])
40
- .filter(Boolean)
41
- .slice(0, 2)
42
- .join("")
43
- .toUpperCase();
44
-
45
- const allStacks = [app.stack, ...(app.stacks ?? [])].filter(Boolean);
46
- const visibleStacks = allStacks.slice(0, 4);
47
- const stackOverflow = allStacks.length - visibleStacks.length;
48
-
49
- const visiblePlatforms = app.platforms.slice(0, 4);
50
- const platformOverflow = app.platforms.length - visiblePlatforms.length;
51
-
52
- const starsLabel = formatStars(app.stars);
53
- const updatedLabel = formatRelative(app.lastCommitAt);
54
-
55
- const detailHref = `/apps/${app.slug}`;
56
- const repoHref = app.repoUrl;
57
- ---
58
-
59
- <article
60
- class="apps-row group relative flex flex-col gap-3 rounded-lg border border-ink-200 bg-white p-4 transition-colors hover:border-ink-300 hover:shadow-[0_1px_2px_rgba(0,0,0,0.04),0_4px_12px_-2px_rgba(0,0,0,0.06)] dark:border-ink-800 dark:bg-ink-950 dark:hover:border-ink-700 dark:hover:shadow-[0_1px_2px_rgba(0,0,0,0.4),0_4px_12px_-2px_rgba(0,0,0,0.3)]"
61
- data-slug={app.slug}
62
- data-stack={app.stack}
63
- data-stacks={allStacks.join("|")}
64
- data-category={app.category ?? ""}
65
- data-platforms={(app.platforms ?? []).join("|")}
66
- data-labels={(app.labels ?? []).join("|")}
67
- data-status={app.status ?? ""}
68
- data-license={app.license ?? ""}
69
- data-stars={app.stars ?? 0}
70
- data-updated={app.lastCommitAt ?? ""}
71
- data-name={app.name.toLowerCase()}
72
- data-owner={(owner ?? "").toLowerCase()}
73
- data-q={(app.description ?? "").toLowerCase()}
74
- >
75
- {/* Header: avatar + name + label */}
76
- <header class="flex items-start gap-3">
77
- <a
78
- href={detailHref}
79
- class="relative z-10 flex shrink-0"
80
- aria-label={`Open ${app.name} details`}
81
- >
82
- {avatar ? (
83
- <img
84
- src={avatar}
85
- alt={`${app.name} avatar`}
86
- width="44"
87
- height="44"
88
- loading="lazy"
89
- decoding="async"
90
- class="h-11 w-11 rounded-md border border-ink-200 bg-white object-cover dark:border-ink-800 dark:bg-ink-900"
91
- />
92
- ) : (
93
- <span
94
- aria-hidden="true"
95
- class="flex h-11 w-11 items-center justify-center rounded-md border border-ink-200 bg-ink-50 text-sm font-semibold text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300"
96
- >
97
- {initials}
98
- </span>
99
- )}
100
- </a>
101
-
102
- <div class="flex min-w-0 flex-1 flex-col gap-0.5">
103
- <div class="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
104
- <h3 class="m-0 text-[1.0625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
105
- <a
106
- href={detailHref}
107
- class="relative z-10 text-inherit no-underline hover:underline"
108
- >
109
- {app.name}
110
- </a>
111
- </h3>
112
- {label && (
113
- <span
114
- class={`inline-flex items-center rounded border px-1.5 py-0 text-[10px] font-medium uppercase tracking-wider ${labelClass}`}
115
- >
116
- {label}
117
- </span>
118
- )}
119
- {isCurated && (
120
- <span
121
- class="inline-flex items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-emerald-700 dark:text-emerald-400"
122
- title="Hand-curated entry"
123
- >
124
- <svg viewBox="0 0 16 16" width="9" height="9" aria-hidden="true" fill="currentColor">
125
- <path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>
126
- </svg>
127
- curated
128
- </span>
129
- )}
130
- </div>
131
- <div class="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-2xs text-ink-500 dark:text-ink-400">
132
- {owner && (
133
- <a
134
- href={repoHref}
135
- target="_blank"
136
- rel="noopener noreferrer"
137
- class="relative z-10 font-mono hover:text-ink-900 dark:hover:text-ink-100"
138
- >
139
- <span class="text-ink-400 dark:text-ink-500">@</span>{owner}
140
- {repo && <span class="text-ink-400 dark:text-ink-500">/{repo}</span>}
141
- </a>
142
- )}
143
- {owner && <span class="text-ink-300 dark:text-ink-700">·</span>}
144
- <span>{app.category}</span>
145
- </div>
146
- </div>
147
- </header>
148
-
149
- {/* Description */}
150
- <p class="apps-row-desc m-0 line-clamp-2 text-[0.8125rem] leading-relaxed text-ink-600 dark:text-ink-300">
151
- {app.description}
152
- </p>
153
-
154
- {/* High-priority metadata row: stars · updated · license — the
155
- "is this alive and usable?" answers at a glance. */}
156
- <dl class="apps-row-meta grid grid-cols-3 gap-x-3 text-2xs">
157
- {starsLabel !== null && (
158
- <div class="flex items-baseline gap-1.5">
159
- <dt class="sr-only">Stars</dt>
160
- <dd class="inline-flex items-center gap-1 font-mono text-ink-700 dark:text-ink-300">
161
- <svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor" class="text-amber-500">
162
- <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/>
163
- </svg>
164
- {starsLabel}
165
- </dd>
166
- </div>
167
- )}
168
- <div class="flex items-baseline gap-1.5">
169
- <dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">Updated</dt>
170
- <dd class="font-mono text-ink-700 dark:text-ink-300" title={app.lastCommitAt ?? ""}>{updatedLabel}</dd>
171
- </div>
172
- <div class="flex items-baseline gap-1.5">
173
- <dt class="font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">License</dt>
174
- <dd class="font-mono text-ink-700 dark:text-ink-300">{app.license ?? "—"}</dd>
175
- </div>
176
- </dl>
177
-
178
- {/* Medium-priority metadata: platforms + stacks. Hidden in compact mode
179
- (controlled by parent via the `data-density` attribute on the
180
- results container). */}
181
- <div class="apps-row-chips flex flex-col gap-1.5">
182
- <div class="flex flex-wrap items-center gap-1.5">
183
- {visiblePlatforms.map((p) => (
184
- <span class="apps-chip 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 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300">
185
- <Icon name={p} category="platform" size={11} />
186
- <span>{p}</span>
187
- </span>
188
- ))}
189
- {platformOverflow > 0 && (
190
- <span class="apps-chip text-2xs text-ink-400 dark:text-ink-500">+{platformOverflow}</span>
191
- )}
192
- </div>
193
- <div class="flex flex-wrap items-center gap-1.5">
194
- {visibleStacks.map((s) => (
195
- <span class="apps-chip inline-flex items-center gap-1 rounded-md border border-ink-200 bg-ink-50 px-1.5 py-0.5 text-2xs font-medium text-ink-700 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-300">
196
- <Icon name={s} category="stack" size={11} />
197
- <span>{s}</span>
198
- </span>
199
- ))}
200
- {stackOverflow > 0 && (
201
- <span class="apps-chip text-2xs text-ink-400 dark:text-ink-500">+{stackOverflow}</span>
202
- )}
203
- </div>
204
- </div>
205
-
206
- {/* Footer actions: View details + GitHub. Always visible (even in
207
- compact mode) so the two primary actions never disappear. */}
208
- <footer class="relative z-10 mt-1 flex items-center gap-2 border-t border-ink-100 pt-2 text-2xs dark:border-ink-900">
209
- <a
210
- href={detailHref}
211
- class="relative z-10 inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-2 py-1 font-medium text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60"
212
- >
213
- View details
214
- <svg viewBox="0 0 16 16" width="10" height="10" aria-hidden="true" fill="currentColor">
215
- <path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"/>
216
- </svg>
217
- </a>
218
- <a
219
- href={repoHref}
220
- target="_blank"
221
- rel="noopener noreferrer"
222
- class="relative z-10 inline-flex items-center gap-1 rounded-md border border-ink-200 bg-white px-2 py-1 font-medium text-ink-700 transition-colors hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60"
223
- >
224
- <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
225
- <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"/>
226
- </svg>
227
- GitHub
228
- </a>
229
- </footer>
230
- </article>
@@ -1,147 +0,0 @@
1
- ---
2
- /**
3
- * Pagination for the /apps list. Server-rendered; the page index in
4
- * the URL is the source of truth. "Load more" is not used here — the
5
- * full result set is small (a few hundred at most) so a real page
6
- * index is simpler and more shareable.
7
- *
8
- * The component renders Prev / numbered pages / Next, plus a small
9
- * "Page X of Y" caption. If there's only one page, returns nothing.
10
- */
11
- interface Props {
12
- /** 1-based current page. */
13
- current: number;
14
- /** Total page count. */
15
- total: number;
16
- /**
17
- * Build the URL for a target page. Caller passes the function so
18
- * we can preserve any active filter query string. The function
19
- * receives the target page and should return a href with that
20
- * page query param updated.
21
- */
22
- hrefFor: (page: number) => string;
23
- class?: string;
24
- }
25
-
26
- const { current, total, hrefFor, class: className = "" } = Astro.props;
27
-
28
- // Windowed page list: always show first, last, current ± 1. Hides
29
- // distant pages with "…". Keeps the bar compact for large total.
30
- function pageList(current: number, total: number): (number | "ellipsis")[] {
31
- if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
32
- const out: (number | "ellipsis")[] = [];
33
- const add = (n: number) => {
34
- if (out[out.length - 1] !== n) out.push(n);
35
- };
36
- add(1);
37
- if (current > 3) out.push("ellipsis");
38
- for (let p = Math.max(2, current - 1); p <= Math.min(total - 1, current + 1); p++) add(p);
39
- if (current < total - 2) out.push("ellipsis");
40
- add(total);
41
- return out;
42
- }
43
-
44
- const pages = pageList(current, total);
45
- const hasPrev = current > 1;
46
- const hasNext = current < total;
47
- const prevHref = hasPrev ? hrefFor(current - 1) : null;
48
- const nextHref = hasNext ? hrefFor(current + 1) : null;
49
-
50
- const baseBtn =
51
- "inline-flex h-8 min-w-8 items-center justify-center rounded-md border px-2 text-2xs font-medium transition-colors";
52
- const inactiveBtn =
53
- "border-ink-200 bg-white text-ink-700 hover:border-ink-300 hover:bg-ink-50 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300 dark:hover:border-ink-700 dark:hover:bg-ink-900/60";
54
- const activeBtn =
55
- "border-ink-900 bg-ink-900 text-white dark:border-ink-100 dark:bg-ink-100 dark:text-ink-900";
56
- const disabledBtn =
57
- "border-ink-200 bg-white text-ink-300 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-700";
58
- ---
59
-
60
- {
61
- total > 1 && (
62
- <nav
63
- class={`mt-8 flex flex-col items-center gap-3 sm:flex-row sm:justify-between ${className}`}
64
- aria-label="Pagination"
65
- >
66
- <p class="m-0 text-2xs text-ink-500 dark:text-ink-400">
67
- Page <span class="font-mono text-ink-700 dark:text-ink-300">{current}</span> of{" "}
68
- <span class="font-mono text-ink-700 dark:text-ink-300">{total}</span>
69
- </p>
70
- <ul class="m-0 flex list-none flex-wrap items-center justify-center gap-1 p-0">
71
- <li>
72
- {hasPrev && prevHref ? (
73
- <a
74
- href={prevHref}
75
- rel="prev"
76
- class={`${baseBtn} ${inactiveBtn}`}
77
- aria-label="Previous page"
78
- >
79
- <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
80
- <path d="M9.78 3.22a.75.75 0 0 1 0 1.06L6.56 7.5h6.69a.75.75 0 0 1 0 1.5H6.56l3.22 3.22a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 0Z"/>
81
- </svg>
82
- <span class="ml-1">Prev</span>
83
- </a>
84
- ) : (
85
- <span class={`${baseBtn} ${disabledBtn} cursor-not-allowed`} aria-disabled="true">
86
- <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
87
- <path d="M9.78 3.22a.75.75 0 0 1 0 1.06L6.56 7.5h6.69a.75.75 0 0 1 0 1.5H6.56l3.22 3.22a.75.75 0 1 1-1.06 1.06l-4.5-4.5a.75.75 0 0 1 0-1.06l4.5-4.5a.75.75 0 0 1 1.06 0Z"/>
88
- </svg>
89
- <span class="ml-1">Prev</span>
90
- </span>
91
- )}
92
- </li>
93
- {pages.map((p) =>
94
- p === "ellipsis" ? (
95
- <li>
96
- <span class={`${baseBtn} border-transparent text-ink-400 dark:text-ink-500`} aria-hidden="true">
97
-
98
- </span>
99
- </li>
100
- ) : p === current ? (
101
- <li>
102
- <span
103
- class={`${baseBtn} ${activeBtn}`}
104
- aria-current="page"
105
- aria-label={`Page ${p}, current`}
106
- >
107
- {p}
108
- </span>
109
- </li>
110
- ) : (
111
- <li>
112
- <a
113
- href={hrefFor(p)}
114
- class={`${baseBtn} ${inactiveBtn}`}
115
- aria-label={`Go to page ${p}`}
116
- >
117
- {p}
118
- </a>
119
- </li>
120
- ),
121
- )}
122
- <li>
123
- {hasNext && nextHref ? (
124
- <a
125
- href={nextHref}
126
- rel="next"
127
- class={`${baseBtn} ${inactiveBtn}`}
128
- aria-label="Next page"
129
- >
130
- <span class="mr-1">Next</span>
131
- <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
132
- <path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06L9.44 8.5H2.75a.75.75 0 0 1 0-1.5h6.69L6.22 4.28a.75.75 0 0 1 0-1.06Z"/>
133
- </svg>
134
- </a>
135
- ) : (
136
- <span class={`${baseBtn} ${disabledBtn} cursor-not-allowed`} aria-disabled="true">
137
- <span class="mr-1">Next</span>
138
- <svg viewBox="0 0 16 16" width="11" height="11" aria-hidden="true" fill="currentColor">
139
- <path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1 0 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06L9.44 8.5H2.75a.75.75 0 0 1 0-1.5h6.69L6.22 4.28a.75.75 0 0 1 0-1.06Z"/>
140
- </svg>
141
- </span>
142
- )}
143
- </li>
144
- </ul>
145
- </nav>
146
- )
147
- }