@grove-dev/astro 0.6.1 → 0.8.0
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.
- package/README.md +77 -5
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -33
- package/dist/index.js.map +1 -1
- package/dist/lib/display.d.ts +1 -1
- package/dist/lib/display.js +1 -1
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/icon-kinds.d.ts +1 -1
- package/dist/lib/icon-kinds.js +43 -43
- package/dist/lib/icon-registry.d.ts +2 -2
- package/dist/lib/icon-registry.js +23 -23
- package/dist/lib/index.d.ts +9 -9
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +9 -9
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/lenses.d.ts +1 -1
- package/dist/lib/lenses.js +1 -1
- package/dist/lib/packaged-icons.d.ts +1 -1
- package/dist/lib/packaged-icons.js +4 -4
- package/dist/lib/repo.d.ts +1 -1
- package/dist/lib/repo.js +1 -1
- package/dist/lib/search.d.ts +1 -1
- package/dist/lib/search.js +1 -1
- package/dist/lib/taxonomy-counts.d.ts +1 -1
- package/dist/lib/taxonomy-counts.js +1 -1
- package/package.json +7 -17
- package/src/index.ts +23 -38
- package/src/lib/display.ts +1 -1
- package/src/lib/format.ts +1 -1
- package/src/lib/icon-kinds.ts +44 -44
- package/src/lib/icon-registry.ts +25 -25
- package/src/lib/index.ts +9 -9
- package/src/lib/lenses.ts +1 -1
- package/src/lib/packaged-icons.ts +8 -8
- package/src/lib/repo.ts +1 -1
- package/src/lib/search.ts +1 -1
- package/src/lib/taxonomy-counts.ts +1 -1
- package/src/server/collections.ts +53 -42
- package/src/server/contrast.ts +14 -9
- package/src/server/directory.ts +22 -17
- package/src/server/index.ts +3 -2
- package/src/server/models.ts +404 -213
- package/src/server/seo.ts +34 -31
- package/src/styles.css +235 -91
- package/dist/layouts/index.d.ts +0 -25
- package/dist/layouts/index.d.ts.map +0 -1
- package/dist/layouts/index.js +0 -25
- package/dist/layouts/index.js.map +0 -1
- package/dist/ui/button.d.ts +0 -53
- package/dist/ui/button.d.ts.map +0 -1
- package/dist/ui/button.js +0 -90
- package/dist/ui/button.js.map +0 -1
- package/src/components/CardGrid.astro +0 -41
- package/src/components/CardIcon.astro +0 -67
- package/src/components/CategoryGrid.astro +0 -114
- package/src/components/CollectionCard.astro +0 -56
- package/src/components/CollectionIndex.astro +0 -102
- package/src/components/CollectionPage.astro +0 -128
- package/src/components/CollectionRow.astro +0 -81
- package/src/components/CollectionTeaser.astro +0 -47
- package/src/components/ContributorsGrid.astro +0 -143
- package/src/components/DirectoryIndexClient.astro +0 -510
- package/src/components/EditorialSummary.astro +0 -58
- package/src/components/FilterGroupMenu.astro +0 -69
- package/src/components/FilterOptions.astro +0 -49
- package/src/components/FinalCta.astro +0 -43
- package/src/components/Hero.astro +0 -243
- package/src/components/Icon.astro +0 -191
- package/src/components/IndexRow.astro +0 -37
- package/src/components/LanguageBreakdown.astro +0 -67
- package/src/components/MarkdownBody.astro +0 -37
- package/src/components/OriginalCollection.astro +0 -117
- package/src/components/Pagination.astro +0 -50
- package/src/components/PoweredBy.astro +0 -60
- package/src/components/ProjectCard.astro +0 -321
- package/src/components/RecordHeader.astro +0 -221
- package/src/components/RecordSection.astro +0 -128
- package/src/components/RecordSidebar.astro +0 -387
- package/src/components/RefinePanel.astro +0 -168
- package/src/components/SmartLensTabs.astro +0 -81
- package/src/components/StackGrid.astro +0 -151
- package/src/components/StackPlatformChips.astro +0 -73
- package/src/components/SubmissionClient.astro +0 -254
- package/src/components/TableOfContents.astro +0 -140
- package/src/components/WhyThisExists.astro +0 -116
- package/src/layouts/BaseLayout.astro +0 -415
- package/src/layouts/Container.astro +0 -18
- package/src/layouts/Footer.astro +0 -231
- package/src/layouts/Header.astro +0 -214
- package/src/layouts/SectionHeader.astro +0 -43
- package/src/layouts/Seo.astro +0 -171
- package/src/layouts/ThemeToggle.astro +0 -118
- package/src/layouts/astro-shim.d.ts +0 -29
- package/src/layouts/index.ts +0 -24
- package/src/server/collections.test.ts +0 -212
- package/src/server/contrast.test.ts +0 -82
- package/src/server/directory.test.ts +0 -177
- package/src/server/github-repo.test.ts +0 -88
- package/src/server/models-home.test.ts +0 -101
- package/src/server/models.test.ts +0 -135
- package/src/server/seo.test.ts +0 -123
- package/src/ui/Badge.astro +0 -38
- package/src/ui/Button.astro +0 -42
- package/src/ui/EmptyState.astro +0 -38
- package/src/ui/FilterDrawer.astro +0 -124
- package/src/ui/PageHeader.astro +0 -39
- package/src/ui/SearchField.astro +0 -106
- package/src/ui/button.test.ts +0 -54
- package/src/ui/button.ts +0 -107
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
import { buttonClass } from "../ui/button.js";
|
|
3
|
-
|
|
4
|
-
interface Props {
|
|
5
|
-
itemLabel?: string;
|
|
6
|
-
itemsLabel?: string;
|
|
7
|
-
browseHref?: string;
|
|
8
|
-
submitHref?: string;
|
|
9
|
-
class?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const {
|
|
13
|
-
itemLabel = "item",
|
|
14
|
-
itemsLabel = "items",
|
|
15
|
-
browseHref = "/items",
|
|
16
|
-
submitHref = "/submit",
|
|
17
|
-
class: className = "",
|
|
18
|
-
} = Astro.props;
|
|
19
|
-
---
|
|
20
|
-
|
|
21
|
-
<section class:list={["border-t border-ink-200 py-10 sm:py-12 dark:border-ink-800", className]} aria-labelledby="grove-final-cta-heading">
|
|
22
|
-
<div class="mx-auto w-full max-w-container px-5 sm:px-7">
|
|
23
|
-
<div class="rounded-[calc(var(--radius)+0.25rem)] border border-border bg-card px-6 py-7 sm:flex sm:items-center sm:justify-between sm:gap-10 sm:px-8 sm:py-8 dark:border-ink-800">
|
|
24
|
-
<div class="max-w-2xl">
|
|
25
|
-
<p class="m-0 text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">Keep the directory useful</p>
|
|
26
|
-
<h2 id="grove-final-cta-heading" class="m-0 mt-1.5 text-[1.5rem] font-semibold tracking-tight text-ink-900 sm:text-[1.75rem] dark:text-ink-100">
|
|
27
|
-
Know {itemLabel === "app" ? "an" : "a"} {itemLabel} that belongs here?
|
|
28
|
-
</h2>
|
|
29
|
-
<p class="m-0 mt-2 max-w-xl text-sm leading-relaxed text-ink-500 dark:text-ink-400">
|
|
30
|
-
Add a real, inspectable project or improve the metadata behind an existing listing.
|
|
31
|
-
</p>
|
|
32
|
-
</div>
|
|
33
|
-
<div class="mt-5 flex shrink-0 flex-wrap items-center gap-2 sm:mt-0 sm:justify-end">
|
|
34
|
-
<a href={submitHref} class={buttonClass("primary", "md", "px-4")}>
|
|
35
|
-
Submit {itemLabel}
|
|
36
|
-
</a>
|
|
37
|
-
<a href={browseHref} class={buttonClass("outline", "md", "px-4")}>
|
|
38
|
-
Explore {itemsLabel}
|
|
39
|
-
</a>
|
|
40
|
-
</div>
|
|
41
|
-
</div>
|
|
42
|
-
</div>
|
|
43
|
-
</section>
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Hero — the home page banner.
|
|
4
|
-
*
|
|
5
|
-
* Mirrors the openapps layout: trust-stats row across the top,
|
|
6
|
-
* big two-line headline with a gradient accent on the second
|
|
7
|
-
* line, descriptive paragraph, search form with a Filters
|
|
8
|
-
* shortcut, quick-filter chips, and up to 3 CTAs.
|
|
9
|
-
*
|
|
10
|
-
* Generic: every value comes through props (or `siteConfig` for
|
|
11
|
-
* the trust stats). The label next to the count is blueprint-
|
|
12
|
-
* driven via `itemsLabel` — so a project-directory renders
|
|
13
|
-
* "apps indexed", a resource-hub renders "resources indexed",
|
|
14
|
-
* an ecosystem-map renders "entities indexed".
|
|
15
|
-
*/
|
|
16
|
-
import Container from "../layouts/Container.astro";
|
|
17
|
-
import { compact } from "../lib/format";
|
|
18
|
-
|
|
19
|
-
interface CTA {
|
|
20
|
-
label: string;
|
|
21
|
-
href: string;
|
|
22
|
-
variant?: "primary" | "outline" | "ghost";
|
|
23
|
-
external?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface Props {
|
|
27
|
-
/** Site/brand name. */
|
|
28
|
-
name: string;
|
|
29
|
-
/** Short tagline — usually pulled from siteConfig.tagline. */
|
|
30
|
-
tagline: string;
|
|
31
|
-
/** Longer descriptive paragraph. */
|
|
32
|
-
description?: string;
|
|
33
|
-
/** Trust stats: items count, categories, stacks, contributors, stars. */
|
|
34
|
-
stats?: {
|
|
35
|
-
totalApps?: number;
|
|
36
|
-
totalCategories?: number;
|
|
37
|
-
totalStacks?: number;
|
|
38
|
-
totalOwners?: number;
|
|
39
|
-
totalStars?: number;
|
|
40
|
-
repositoryContributors?: number;
|
|
41
|
-
originalRepo?: string;
|
|
42
|
-
};
|
|
43
|
-
/** Search form target — the list page. */
|
|
44
|
-
searchHref?: string;
|
|
45
|
-
/** Search input placeholder. */
|
|
46
|
-
searchPlaceholder?: string;
|
|
47
|
-
/**
|
|
48
|
-
* Plural noun for the primary count (e.g. "apps", "resources",
|
|
49
|
-
* "entities"). Used in the trust-stats label and CTA copy.
|
|
50
|
-
*/
|
|
51
|
-
itemsLabel?: string;
|
|
52
|
-
/** Singular noun for the Submit / Browse copy. */
|
|
53
|
-
itemLabel?: string;
|
|
54
|
-
/** Quick filter chips under the search bar. */
|
|
55
|
-
quickFilters?: Array<{ label: string; href: string }>;
|
|
56
|
-
/** CTAs rendered below the form. */
|
|
57
|
-
ctas?: CTA[];
|
|
58
|
-
/** Extra class. */
|
|
59
|
-
class?: string;
|
|
60
|
-
/** Eyebrow text rendered above the title. */
|
|
61
|
-
eyebrow?: string;
|
|
62
|
-
/** Optional badge content rendered next to the eyebrow. */
|
|
63
|
-
eyebrowBadge?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const {
|
|
67
|
-
name,
|
|
68
|
-
tagline,
|
|
69
|
-
description,
|
|
70
|
-
stats = {},
|
|
71
|
-
searchHref = "/",
|
|
72
|
-
searchPlaceholder = "Search the directory...",
|
|
73
|
-
itemsLabel = "items",
|
|
74
|
-
itemLabel,
|
|
75
|
-
quickFilters = [],
|
|
76
|
-
ctas = [],
|
|
77
|
-
class: className = "",
|
|
78
|
-
eyebrow = "A directory of curated knowledge",
|
|
79
|
-
eyebrowBadge,
|
|
80
|
-
} = Astro.props;
|
|
81
|
-
|
|
82
|
-
// Singular for the Submit-style CTA copy. Defaults to the lowercased
|
|
83
|
-
// items label so "apps" → "app" without an extra prop.
|
|
84
|
-
const singular = (itemLabel ?? itemsLabel.replace(/s$/, "")).toLowerCase();
|
|
85
|
-
|
|
86
|
-
// One compact stats line: "6 projects indexed · 5 categories · 3 stacks".
|
|
87
|
-
// Every entry is guarded so a fresh directory never renders a zero-value.
|
|
88
|
-
const statParts: string[] = [
|
|
89
|
-
...(stats.totalApps !== undefined && stats.totalApps > 0
|
|
90
|
-
? [`${compact(stats.totalApps)} ${itemsLabel} indexed`]
|
|
91
|
-
: []),
|
|
92
|
-
...(stats.totalCategories !== undefined && stats.totalCategories > 0
|
|
93
|
-
? [`${stats.totalCategories} categories`]
|
|
94
|
-
: []),
|
|
95
|
-
...(stats.totalStacks !== undefined && stats.totalStacks > 0
|
|
96
|
-
? [`${stats.totalStacks} stacks`]
|
|
97
|
-
: []),
|
|
98
|
-
...(stats.totalOwners !== undefined && stats.totalOwners > 0
|
|
99
|
-
? [`${compact(stats.totalOwners)} owners`]
|
|
100
|
-
: []),
|
|
101
|
-
...(stats.totalStars !== undefined && stats.totalStars > 0
|
|
102
|
-
? [`${compact(stats.totalStars)} stars`]
|
|
103
|
-
: []),
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
// One primary CTA only — Submit already lives in the navbar
|
|
107
|
-
// (BaseLayout → Header submitHref), so the hero never duplicates it.
|
|
108
|
-
const primaryCta: CTA | undefined =
|
|
109
|
-
ctas.find((c) => c.variant === "primary") ??
|
|
110
|
-
ctas[0] ??
|
|
111
|
-
{ label: `Browse ${itemsLabel}`, href: searchHref, variant: "primary" };
|
|
112
|
-
---
|
|
113
|
-
|
|
114
|
-
<section
|
|
115
|
-
class:list={[
|
|
116
|
-
"relative overflow-hidden border-b border-ink-200 dark:border-ink-800",
|
|
117
|
-
className,
|
|
118
|
-
]}
|
|
119
|
-
>
|
|
120
|
-
{/* Dark mode only: a barely-there radial lift behind the headline. */}
|
|
121
|
-
<div
|
|
122
|
-
aria-hidden="true"
|
|
123
|
-
class="pointer-events-none absolute inset-0 hidden dark:block"
|
|
124
|
-
style="background: radial-gradient(60% 55% at 50% 0%, color-mix(in oklab, var(--grove-foreground) 5%, transparent) 0%, transparent 70%);"
|
|
125
|
-
></div>
|
|
126
|
-
|
|
127
|
-
<Container>
|
|
128
|
-
<div class="relative mx-auto flex max-w-[900px] flex-col items-center py-[88px] text-center lg:py-[104px]">
|
|
129
|
-
{/* Eyebrow. The `eyebrow` slot replaces the whole line — dot
|
|
130
|
-
included — so a consumer can put richer content here (a
|
|
131
|
-
`PoweredBy` link, a release badge) without forking the Hero. */}
|
|
132
|
-
<span class="inline-flex animate-fade-in items-center gap-2 text-[13px] font-medium text-ink-500 dark:text-ink-400">
|
|
133
|
-
<slot name="eyebrow">
|
|
134
|
-
<span aria-hidden="true" class="inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-emerald-500"></span>
|
|
135
|
-
{eyebrow}
|
|
136
|
-
{eyebrowBadge && (
|
|
137
|
-
<span
|
|
138
|
-
class="inline-flex items-center rounded-full border border-amber-300 bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-800 dark:border-amber-700/50 dark:bg-amber-900/20 dark:text-amber-300"
|
|
139
|
-
aria-label={eyebrowBadge}
|
|
140
|
-
>
|
|
141
|
-
{eyebrowBadge}
|
|
142
|
-
</span>
|
|
143
|
-
)}
|
|
144
|
-
</slot>
|
|
145
|
-
</span>
|
|
146
|
-
|
|
147
|
-
{/* Headline */}
|
|
148
|
-
<h1
|
|
149
|
-
class="m-0 mt-5 max-w-[820px] animate-fade-in-up text-balance text-[clamp(44px,5vw,64px)] font-[650] leading-[1.08] tracking-[-0.035em] text-ink-900 dark:text-ink-100"
|
|
150
|
-
style="animation-delay: 0.05s;"
|
|
151
|
-
>
|
|
152
|
-
<span class="block">{name}</span>
|
|
153
|
-
<span class="block">{tagline}</span>
|
|
154
|
-
</h1>
|
|
155
|
-
|
|
156
|
-
{description && (
|
|
157
|
-
<p
|
|
158
|
-
class="m-0 mt-5 max-w-[640px] animate-fade-in-up text-[16px] leading-[1.6] text-ink-500 dark:text-ink-400"
|
|
159
|
-
style="animation-delay: 0.1s;"
|
|
160
|
-
>
|
|
161
|
-
{description}
|
|
162
|
-
</p>
|
|
163
|
-
)}
|
|
164
|
-
|
|
165
|
-
{/* Search — primary interaction. The FORM is the visual control:
|
|
166
|
-
it owns the border, radius, and the single focus treatment
|
|
167
|
-
(focus-within border + soft ring). The inner input opts out
|
|
168
|
-
of the global input styling (`grove-input-unstyled`) so it
|
|
169
|
-
stays a transparent, chrome-free text slot — no second box
|
|
170
|
-
or ring appearing inside the pill on focus. */}
|
|
171
|
-
<form
|
|
172
|
-
class="mt-8 flex h-[58px] w-full max-w-[840px] animate-fade-in-up items-center gap-3 rounded-xl border border-ink-200 bg-background p-1.5 pl-4 transition-[border-color,box-shadow] focus-within:border-ink-400 focus-within:ring-3 focus-within:ring-ring/15 dark:border-ink-800 dark:focus-within:border-ink-600"
|
|
173
|
-
style="animation-delay: 0.15s;"
|
|
174
|
-
role="search"
|
|
175
|
-
action={searchHref}
|
|
176
|
-
method="get"
|
|
177
|
-
>
|
|
178
|
-
<label class="flex min-w-0 flex-1 items-center gap-3">
|
|
179
|
-
<span class="sr-only">Search {itemsLabel}, stacks, and categories</span>
|
|
180
|
-
<svg viewBox="0 0 16 16" width="20" height="20" aria-hidden="true" class="shrink-0 text-ink-400 dark:text-ink-500" fill="currentColor">
|
|
181
|
-
<path d="M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215ZM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7Z"></path>
|
|
182
|
-
</svg>
|
|
183
|
-
<input
|
|
184
|
-
type="search"
|
|
185
|
-
name="q"
|
|
186
|
-
placeholder={searchPlaceholder}
|
|
187
|
-
class="grove-input-unstyled h-full w-full min-w-0 rounded-none border-0 bg-transparent px-0 text-[16px] text-ink-900 outline-none placeholder:text-ink-400 dark:text-ink-100 dark:placeholder:text-ink-500"
|
|
188
|
-
/>
|
|
189
|
-
</label>
|
|
190
|
-
<button
|
|
191
|
-
type="submit"
|
|
192
|
-
class="inline-flex h-[46px] shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-primary px-5 text-[15px] font-medium leading-none text-primary-foreground outline-none transition-colors hover:bg-primary/90 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35"
|
|
193
|
-
>
|
|
194
|
-
Search
|
|
195
|
-
</button>
|
|
196
|
-
</form>
|
|
197
|
-
|
|
198
|
-
{quickFilters.length > 0 && (
|
|
199
|
-
<p class="m-0 mt-4 text-[13px] text-ink-500 dark:text-ink-400">
|
|
200
|
-
Try{" "}
|
|
201
|
-
{quickFilters.map((f, i) => (
|
|
202
|
-
<>
|
|
203
|
-
<a href={f.href} class="font-mono text-ink-700 underline-offset-2 hover:underline dark:text-ink-300">{f.label}</a>
|
|
204
|
-
{i < quickFilters.length - 1 && <span aria-hidden="true" class="mx-1">·</span>}
|
|
205
|
-
</>
|
|
206
|
-
))}
|
|
207
|
-
</p>
|
|
208
|
-
)}
|
|
209
|
-
|
|
210
|
-
{/* Inline stats — single compact line, middle-dot separators */}
|
|
211
|
-
{statParts.length > 0 && (
|
|
212
|
-
<p class="m-0 mt-5 text-[13px] text-ink-500 dark:text-ink-400">
|
|
213
|
-
{statParts.map((part, i) => (
|
|
214
|
-
<>
|
|
215
|
-
{i > 0 && <span aria-hidden="true" class="mx-2 opacity-60">·</span>}
|
|
216
|
-
<span>{part}</span>
|
|
217
|
-
</>
|
|
218
|
-
))}
|
|
219
|
-
</p>
|
|
220
|
-
)}
|
|
221
|
-
|
|
222
|
-
{/* Primary CTA only */}
|
|
223
|
-
{primaryCta && (
|
|
224
|
-
<div class="mt-8">
|
|
225
|
-
<a
|
|
226
|
-
href={primaryCta.href}
|
|
227
|
-
class="inline-flex h-11 cursor-pointer items-center justify-center gap-2 rounded-[var(--radius)] border border-transparent bg-primary px-5 text-[15px] font-medium leading-none text-primary-foreground outline-none transition-colors hover:bg-primary/90 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35"
|
|
228
|
-
{...(primaryCta.external ? { target: "_blank", rel: "noopener noreferrer" } : {})}
|
|
229
|
-
data-event="hero_cta_click"
|
|
230
|
-
data-event-source="hero"
|
|
231
|
-
>
|
|
232
|
-
{primaryCta.label}
|
|
233
|
-
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
234
|
-
<path d="M5 12h14"></path>
|
|
235
|
-
<path d="m12 5 7 7-7 7"></path>
|
|
236
|
-
</svg>
|
|
237
|
-
</a>
|
|
238
|
-
</div>
|
|
239
|
-
)}
|
|
240
|
-
{singular /* suppress unused warning on the singular variable */ ? null : null}
|
|
241
|
-
</div>
|
|
242
|
-
</Container>
|
|
243
|
-
</section>
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* Icon — brand icon registry.
|
|
4
|
-
*
|
|
5
|
-
* Resolves a registry name to `/icons/{stacks|platforms|brands}/{name}.svg`
|
|
6
|
-
* and renders it. The Astro integration syncs the packaged set into the
|
|
7
|
-
* consumer's `public/icons/` on every build; consumers can ship their own
|
|
8
|
-
* SVGs under the same paths (or override the `base` prop) without forking
|
|
9
|
-
* this component.
|
|
10
|
-
*
|
|
11
|
-
* The component is **generic** — it does not import any consumer-side
|
|
12
|
-
* `siteConfig`, `item`, or blueprint-specific module.
|
|
13
|
-
*
|
|
14
|
-
* `category` selects the subfolder:
|
|
15
|
-
*
|
|
16
|
-
* - "stack" → `/icons/stacks/{name}.svg`
|
|
17
|
-
* - "platform" → `/icons/platforms/{name}.svg`
|
|
18
|
-
* - "brand" → `/icons/brands/{name}.svg` (consumer-supplied)
|
|
19
|
-
*
|
|
20
|
-
* ## Theming
|
|
21
|
-
*
|
|
22
|
-
* `ICON_KINDS` (generated by `scripts/sync-icons.mjs`) classifies each
|
|
23
|
-
* packaged icon:
|
|
24
|
-
*
|
|
25
|
-
* - **color** — the default. Any brand with a color mark keeps it,
|
|
26
|
-
* rendered as `<img>` with an `onerror` fallback to an initials
|
|
27
|
-
* chip.
|
|
28
|
-
* - **mono** — marks with no color to lose (Apple, Rust, Tauri,
|
|
29
|
-
* Solidity, Deno) plus the concept glyphs. Rendered as a CSS-masked
|
|
30
|
-
* `<span>` painted from `--grove-foreground`, which reproduces how
|
|
31
|
-
* those brands are actually used: solid black on light, solid white
|
|
32
|
-
* on dark, with no script and no second file. See
|
|
33
|
-
* `.grove-icon-mask` in `styles.css`.
|
|
34
|
-
*
|
|
35
|
-
* The map CLASSIFIES; it does not gate availability. A name it has never
|
|
36
|
-
* heard of — a consumer's own SVG — takes the `<img>` path, exactly as
|
|
37
|
-
* before. Pass `kind="mono"` to opt a custom mark into `currentColor`
|
|
38
|
-
* tinting.
|
|
39
|
-
*
|
|
40
|
-
* Aliases: a tiny `aliases` map (e.g. `rn → react-native`) lets the
|
|
41
|
-
* registry accept multiple ids for the same asset. Lookup falls through
|
|
42
|
-
* `aliases` → the built-in stack aliases → the raw slug, then fails over
|
|
43
|
-
* to the initials placeholder.
|
|
44
|
-
*/
|
|
45
|
-
import { ICON_KINDS } from "../lib/icon-kinds";
|
|
46
|
-
import type { IconKind } from "../lib/icon-kinds";
|
|
47
|
-
import { resolveIconAsset, slugifyIconName } from "../lib/icon-registry";
|
|
48
|
-
import type { IconCategory } from "../lib/icon-registry";
|
|
49
|
-
|
|
50
|
-
export type { IconCategory, IconKind };
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @deprecated Theme variants are no longer file-based — `mono` icons
|
|
54
|
-
* inherit `currentColor`. Accepted and ignored so consumers passing
|
|
55
|
-
* `mode="auto"` do not hit a type error.
|
|
56
|
-
*/
|
|
57
|
-
export type IconMode = "static" | "auto";
|
|
58
|
-
|
|
59
|
-
interface Props {
|
|
60
|
-
/**
|
|
61
|
-
* Registry id. Resolved through `aliases` first, then matched
|
|
62
|
-
* against the SVG file name (case-insensitive, slug-normalized).
|
|
63
|
-
*/
|
|
64
|
-
name: string;
|
|
65
|
-
/** Subfolder under `/icons/`. */
|
|
66
|
-
category: IconCategory;
|
|
67
|
-
/** Pixel size (square). Defaults to 16. */
|
|
68
|
-
size?: number;
|
|
69
|
-
/** Extra class for the outer wrapper. */
|
|
70
|
-
class?: string;
|
|
71
|
-
/** Tooltip / aria-label. */
|
|
72
|
-
title?: string;
|
|
73
|
-
/** @deprecated No-op. Kept for one minor release. */
|
|
74
|
-
mode?: IconMode;
|
|
75
|
-
/**
|
|
76
|
-
* Override the packaged kind classification. Pass `"mono"` for a
|
|
77
|
-
* consumer-supplied single-color mark so it is painted black on
|
|
78
|
-
* light and white on dark instead of shipping two files.
|
|
79
|
-
*/
|
|
80
|
-
kind?: IconKind;
|
|
81
|
-
/**
|
|
82
|
-
* Optional id → id remap. Lookup goes `name → aliases[name] →
|
|
83
|
-
* {name}.svg` (case-insensitive).
|
|
84
|
-
*/
|
|
85
|
-
aliases?: Record<string, string>;
|
|
86
|
-
/**
|
|
87
|
-
* Optional complete asset registry supplied by a consumer. When omitted,
|
|
88
|
-
* Grove attempts the configured asset path and falls back on image error.
|
|
89
|
-
*/
|
|
90
|
-
available?: string[];
|
|
91
|
-
/**
|
|
92
|
-
* Initials to render when the icon file is missing. Defaults to
|
|
93
|
-
* the first 1–2 characters of `name`.
|
|
94
|
-
*/
|
|
95
|
-
fallbackInitials?: string;
|
|
96
|
-
/**
|
|
97
|
-
* Base path for icon files. Defaults to `/icons` (matches the
|
|
98
|
-
* synced `public/icons/`).
|
|
99
|
-
*/
|
|
100
|
-
base?: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const {
|
|
104
|
-
name,
|
|
105
|
-
category,
|
|
106
|
-
size = 16,
|
|
107
|
-
class: className = "",
|
|
108
|
-
title,
|
|
109
|
-
kind,
|
|
110
|
-
aliases,
|
|
111
|
-
available,
|
|
112
|
-
fallbackInitials,
|
|
113
|
-
base = "/icons",
|
|
114
|
-
} = Astro.props;
|
|
115
|
-
|
|
116
|
-
const asset = resolveIconAsset(name, category, aliases);
|
|
117
|
-
const resolved = asset.name;
|
|
118
|
-
const availableIcons = available?.map(slugifyIconName);
|
|
119
|
-
const hasAsset =
|
|
120
|
-
category === "brand" ||
|
|
121
|
-
availableIcons === undefined ||
|
|
122
|
-
availableIcons.includes(resolved);
|
|
123
|
-
const src = `${base.replace(/\/$/, "")}/${asset.folder}/${resolved}.svg`;
|
|
124
|
-
|
|
125
|
-
// Initials fallback: first 1–2 alphanumerics, uppercased.
|
|
126
|
-
const init = (fallbackInitials ?? name ?? "")
|
|
127
|
-
.replace(/[^a-z0-9]/gi, "")
|
|
128
|
-
.slice(0, 2)
|
|
129
|
-
.toUpperCase();
|
|
130
|
-
const showFallback = !resolved || !hasAsset || asset.textOnly;
|
|
131
|
-
|
|
132
|
-
// Unknown names fall through to `<img>`, never to `mono` — a mask has
|
|
133
|
-
// no `onerror`, so a missing file would render an invisible blank.
|
|
134
|
-
const effectiveKind: IconKind = kind ?? ICON_KINDS[asset.key] ?? "color";
|
|
135
|
-
|
|
136
|
-
// `name` reaches the mask branch as part of a CSS `url()` token, and
|
|
137
|
-
// slugifying does not remove the characters that would close it. Astro
|
|
138
|
-
// escapes the attribute; this escapes the CSS inside it.
|
|
139
|
-
const cssUrl = src.replace(/['"()\\\s]/g, encodeURIComponent);
|
|
140
|
-
|
|
141
|
-
// Build aria-label.
|
|
142
|
-
const ariaLabel = title ?? name;
|
|
143
|
-
const a11y: Record<string, string> = title
|
|
144
|
-
? { "aria-label": ariaLabel, role: "img" }
|
|
145
|
-
: { "aria-hidden": "true", "focusable": "false" };
|
|
146
|
-
|
|
147
|
-
const fallbackClass = [
|
|
148
|
-
"inline-flex shrink-0 select-none items-center justify-center rounded-sm bg-ink-100 align-middle font-semibold uppercase leading-none text-ink-700 dark:bg-ink-800 dark:text-ink-200",
|
|
149
|
-
className,
|
|
150
|
-
];
|
|
151
|
-
const fallbackStyle = `width:${size}px;height:${size}px;font-size:${Math.max(10, size - 6)}px;`;
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
showFallback ? (
|
|
156
|
-
<span class:list={fallbackClass} style={fallbackStyle} {...a11y}>
|
|
157
|
-
{init || "·"}
|
|
158
|
-
</span>
|
|
159
|
-
) : effectiveKind === "mono" ? (
|
|
160
|
-
<span
|
|
161
|
-
class:list={["grove-icon-mask", className]}
|
|
162
|
-
style={`width:${size}px;height:${size}px;--grove-icon-url:url('${cssUrl}');`}
|
|
163
|
-
{...a11y}
|
|
164
|
-
/>
|
|
165
|
-
) : (
|
|
166
|
-
<>
|
|
167
|
-
<img
|
|
168
|
-
src={src}
|
|
169
|
-
width={size}
|
|
170
|
-
height={size}
|
|
171
|
-
{/* The `width`/`height` attributes are only presentational
|
|
172
|
-
hints. These SVGs carry a `viewBox` and no intrinsic size,
|
|
173
|
-
so once one loads the browser re-derives the box and the
|
|
174
|
-
row reflows — Lighthouse reports it as "media element
|
|
175
|
-
lacking an explicit size" and it cost the browse page
|
|
176
|
-
0.115 CLS. An explicit CSS size pins the box before the
|
|
177
|
-
first byte arrives. */}
|
|
178
|
-
style={`width:${size}px;height:${size}px`}
|
|
179
|
-
alt=""
|
|
180
|
-
class:list={["inline-block shrink-0 align-middle", className]}
|
|
181
|
-
loading="lazy"
|
|
182
|
-
decoding="async"
|
|
183
|
-
onerror="this.hidden=true;this.nextElementSibling.hidden=false"
|
|
184
|
-
{...a11y}
|
|
185
|
-
/>
|
|
186
|
-
<span hidden data-grove-icon-fallback-initials class:list={fallbackClass} style={fallbackStyle} {...a11y}>
|
|
187
|
-
{init || "·"}
|
|
188
|
-
</span>
|
|
189
|
-
</>
|
|
190
|
-
)
|
|
191
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* IndexRow — browse-page adapter around the canonical `ProjectCard`.
|
|
4
|
-
*
|
|
5
|
-
* Keeps the historical public props (`record`, `detailHref`,
|
|
6
|
-
* `showChips`, `eager`) so consumer pages copied from earlier versions
|
|
7
|
-
* keep working. The card itself is the same on every surface: a
|
|
8
|
-
* record must not look like a different record depending on whether
|
|
9
|
-
* you found it on the home page or in the browse list. The repository
|
|
10
|
-
* is reached through the card's `owner/repo` line, so this adapter
|
|
11
|
-
* adds nothing to the card at all.
|
|
12
|
-
*/
|
|
13
|
-
import type { IndexRecord } from "@grove-dev/core";
|
|
14
|
-
import ProjectCard from "./ProjectCard.astro";
|
|
15
|
-
|
|
16
|
-
interface Props {
|
|
17
|
-
record: IndexRecord;
|
|
18
|
-
detailHref: string;
|
|
19
|
-
/** @deprecated Ignored — the card renders one fixed layout. */
|
|
20
|
-
showChips?: boolean;
|
|
21
|
-
/** Set true on the first 1-2 rows of a list to bypass lazy
|
|
22
|
-
* loading for the above-the-fold avatars. Pair with
|
|
23
|
-
* `fetchpriority="high"`. */
|
|
24
|
-
eager?: boolean;
|
|
25
|
-
class?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const { record, detailHref, eager = false, class: className = "" } = Astro.props;
|
|
29
|
-
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
<ProjectCard
|
|
33
|
-
record={record}
|
|
34
|
-
href={detailHref}
|
|
35
|
-
eager={eager}
|
|
36
|
-
class={className}
|
|
37
|
-
/>
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* LanguageBreakdown.astro
|
|
4
|
-
*
|
|
5
|
-
* Visual GitHub-Linguist-style code-composition bar plus a legend
|
|
6
|
-
* showing each language's percentage. Reads from the record's
|
|
7
|
-
* pre-computed `languages` array (top-5 by bytes) and `langColor`
|
|
8
|
-
* (in @grove-dev/core).
|
|
9
|
-
*
|
|
10
|
-
* Used inside the Activity sidebar card.
|
|
11
|
-
*/
|
|
12
|
-
import { langColor } from "@grove-dev/core";
|
|
13
|
-
|
|
14
|
-
interface LangEntry {
|
|
15
|
-
name: string;
|
|
16
|
-
bytes: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
interface Props {
|
|
20
|
-
/** Top-N languages as `[name, bytes]` tuples (already sorted). */
|
|
21
|
-
languages: ReadonlyArray<readonly [string, number]>;
|
|
22
|
-
/** Total bytes across all languages (denominator). */
|
|
23
|
-
totalBytes: number;
|
|
24
|
-
/** Optional max segments to show. */
|
|
25
|
-
maxSegments?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const { languages, totalBytes, maxSegments = 5 } = Astro.props;
|
|
29
|
-
|
|
30
|
-
const segments = languages.slice(0, maxSegments).map(([name, bytes]) => {
|
|
31
|
-
const pct = Math.round((bytes / Math.max(1, totalBytes)) * 100);
|
|
32
|
-
return { name, pct, color: langColor(name) };
|
|
33
|
-
});
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
{segments.length > 0 && (
|
|
37
|
-
<div class="grove-langbreak mt-4 border-t border-ink-100 pt-4 dark:border-ink-800">
|
|
38
|
-
<h3 class="m-0 text-2xs font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
39
|
-
Code composition
|
|
40
|
-
</h3>
|
|
41
|
-
<div
|
|
42
|
-
class="grove-langbreak-bar mt-2 flex h-2 w-full overflow-hidden rounded-full bg-ink-100 dark:bg-ink-800"
|
|
43
|
-
role="img"
|
|
44
|
-
aria-label={`Code composition: ${segments.map((s) => `${s.name} ${s.pct}%`).join(', ')}`}
|
|
45
|
-
>
|
|
46
|
-
{segments.map((s) => (
|
|
47
|
-
<span
|
|
48
|
-
class="grove-langbreak-seg h-full"
|
|
49
|
-
style={`width:${s.pct}%;background-color:${s.color}`}
|
|
50
|
-
title={`${s.name}: ${s.pct}%`}
|
|
51
|
-
/>
|
|
52
|
-
))}
|
|
53
|
-
</div>
|
|
54
|
-
<ul class="grove-langbreak-list mt-2 flex flex-wrap gap-x-3 gap-y-1 text-2xs text-ink-500 dark:text-ink-400">
|
|
55
|
-
{segments.map((s) => (
|
|
56
|
-
<li class="inline-flex items-center gap-1">
|
|
57
|
-
<span
|
|
58
|
-
class="inline-block h-2 w-2 rounded-sm"
|
|
59
|
-
style={`background-color:${s.color}`}
|
|
60
|
-
/>
|
|
61
|
-
<span class="font-mono">{s.name}</span>
|
|
62
|
-
<span>{s.pct}%</span>
|
|
63
|
-
</li>
|
|
64
|
-
))}
|
|
65
|
-
</ul>
|
|
66
|
-
</div>
|
|
67
|
-
)}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* MarkdownBody.astro
|
|
4
|
-
*
|
|
5
|
-
* Renders a record's pre-sanitized Markdown body (`getContentHtml`)
|
|
6
|
-
* inside the package's `.grove-prose` stylesheet.
|
|
7
|
-
*
|
|
8
|
-
* The renderer itself lives in `@grove-dev/astro/server/directory.ts`
|
|
9
|
-
* (`renderMarkdownToSafeHtml`) and runs once per record at module
|
|
10
|
-
* load; this component is the presentation half of that pipeline.
|
|
11
|
-
*
|
|
12
|
-
* Props:
|
|
13
|
-
* - `recordSlug`: the **record's** slug (not the route prefix).
|
|
14
|
-
* `getContentHtml` is keyed by record slug — `detail.slug` is the
|
|
15
|
-
* route prefix (e.g. "projects"), `detail.record.slug` is the
|
|
16
|
-
* record (e.g. "crewai").
|
|
17
|
-
* - `fallback`: optional content to render when the record has no
|
|
18
|
-
* body (e.g. an empty-state message).
|
|
19
|
-
*/
|
|
20
|
-
import { getContentHtml } from "../server/directory.js";
|
|
21
|
-
|
|
22
|
-
interface Props {
|
|
23
|
-
/** Record slug — looks up the pre-rendered HTML in the module map. */
|
|
24
|
-
recordSlug: string;
|
|
25
|
-
/** Optional fallback content for records without a body. */
|
|
26
|
-
fallback?: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const { recordSlug, fallback } = Astro.props;
|
|
30
|
-
const html = getContentHtml(recordSlug);
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
{html ? (
|
|
34
|
-
<div class="grove-prose" set:html={html} />
|
|
35
|
-
) : fallback ? (
|
|
36
|
-
<p class="grove-prose text-ink-500 dark:text-ink-400">{fallback}</p>
|
|
37
|
-
) : null}
|