@grove-dev/astro 0.7.0 → 0.9.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.
Files changed (110) hide show
  1. package/dist/index.d.ts +3 -3
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +22 -33
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/display.d.ts +1 -1
  6. package/dist/lib/display.js +1 -1
  7. package/dist/lib/format.d.ts +1 -1
  8. package/dist/lib/format.js +1 -1
  9. package/dist/lib/icon-kinds.d.ts +1 -1
  10. package/dist/lib/icon-kinds.js +43 -43
  11. package/dist/lib/icon-registry.d.ts +2 -2
  12. package/dist/lib/icon-registry.js +23 -23
  13. package/dist/lib/index.d.ts +9 -9
  14. package/dist/lib/index.d.ts.map +1 -1
  15. package/dist/lib/index.js +9 -9
  16. package/dist/lib/index.js.map +1 -1
  17. package/dist/lib/lenses.d.ts +1 -1
  18. package/dist/lib/lenses.js +1 -1
  19. package/dist/lib/packaged-icons.d.ts +1 -1
  20. package/dist/lib/packaged-icons.js +4 -4
  21. package/dist/lib/repo.d.ts +1 -1
  22. package/dist/lib/repo.js +1 -1
  23. package/dist/lib/search.d.ts +1 -1
  24. package/dist/lib/search.js +1 -1
  25. package/dist/lib/taxonomy-counts.d.ts +1 -1
  26. package/dist/lib/taxonomy-counts.js +1 -1
  27. package/package.json +7 -17
  28. package/src/index.ts +23 -38
  29. package/src/lib/display.ts +1 -1
  30. package/src/lib/format.ts +1 -1
  31. package/src/lib/icon-kinds.ts +44 -44
  32. package/src/lib/icon-registry.ts +25 -25
  33. package/src/lib/index.ts +9 -9
  34. package/src/lib/lenses.ts +1 -1
  35. package/src/lib/packaged-icons.ts +8 -8
  36. package/src/lib/repo.ts +1 -1
  37. package/src/lib/search.ts +1 -1
  38. package/src/lib/taxonomy-counts.ts +1 -1
  39. package/src/server/collections.ts +53 -42
  40. package/src/server/contrast.ts +14 -9
  41. package/src/server/directory.ts +22 -17
  42. package/src/server/index.ts +3 -2
  43. package/src/server/models.ts +425 -218
  44. package/src/server/seo.ts +34 -31
  45. package/src/styles.css +235 -91
  46. package/dist/layouts/index.d.ts +0 -25
  47. package/dist/layouts/index.d.ts.map +0 -1
  48. package/dist/layouts/index.js +0 -25
  49. package/dist/layouts/index.js.map +0 -1
  50. package/dist/ui/button.d.ts +0 -53
  51. package/dist/ui/button.d.ts.map +0 -1
  52. package/dist/ui/button.js +0 -90
  53. package/dist/ui/button.js.map +0 -1
  54. package/src/components/CardGrid.astro +0 -41
  55. package/src/components/CardIcon.astro +0 -67
  56. package/src/components/CategoryGrid.astro +0 -114
  57. package/src/components/CollectionCard.astro +0 -56
  58. package/src/components/CollectionIndex.astro +0 -102
  59. package/src/components/CollectionPage.astro +0 -128
  60. package/src/components/CollectionRow.astro +0 -81
  61. package/src/components/CollectionTeaser.astro +0 -47
  62. package/src/components/ContributorsGrid.astro +0 -143
  63. package/src/components/DirectoryIndexClient.astro +0 -510
  64. package/src/components/EditorialSummary.astro +0 -58
  65. package/src/components/FilterGroupMenu.astro +0 -69
  66. package/src/components/FilterOptions.astro +0 -49
  67. package/src/components/FinalCta.astro +0 -43
  68. package/src/components/Hero.astro +0 -243
  69. package/src/components/Icon.astro +0 -191
  70. package/src/components/IndexRow.astro +0 -37
  71. package/src/components/LanguageBreakdown.astro +0 -67
  72. package/src/components/MarkdownBody.astro +0 -37
  73. package/src/components/OriginalCollection.astro +0 -117
  74. package/src/components/Pagination.astro +0 -50
  75. package/src/components/PoweredBy.astro +0 -60
  76. package/src/components/ProjectCard.astro +0 -321
  77. package/src/components/RecordHeader.astro +0 -221
  78. package/src/components/RecordSection.astro +0 -128
  79. package/src/components/RecordSidebar.astro +0 -387
  80. package/src/components/RefinePanel.astro +0 -168
  81. package/src/components/SmartLensTabs.astro +0 -81
  82. package/src/components/StackGrid.astro +0 -151
  83. package/src/components/StackPlatformChips.astro +0 -73
  84. package/src/components/SubmissionClient.astro +0 -254
  85. package/src/components/TableOfContents.astro +0 -140
  86. package/src/components/WhyThisExists.astro +0 -116
  87. package/src/layouts/BaseLayout.astro +0 -415
  88. package/src/layouts/Container.astro +0 -18
  89. package/src/layouts/Footer.astro +0 -231
  90. package/src/layouts/Header.astro +0 -214
  91. package/src/layouts/SectionHeader.astro +0 -43
  92. package/src/layouts/Seo.astro +0 -171
  93. package/src/layouts/ThemeToggle.astro +0 -118
  94. package/src/layouts/astro-shim.d.ts +0 -29
  95. package/src/layouts/index.ts +0 -24
  96. package/src/server/collections.test.ts +0 -212
  97. package/src/server/contrast.test.ts +0 -82
  98. package/src/server/directory.test.ts +0 -177
  99. package/src/server/github-repo.test.ts +0 -88
  100. package/src/server/models-home.test.ts +0 -101
  101. package/src/server/models.test.ts +0 -135
  102. package/src/server/seo.test.ts +0 -123
  103. package/src/ui/Badge.astro +0 -38
  104. package/src/ui/Button.astro +0 -42
  105. package/src/ui/EmptyState.astro +0 -38
  106. package/src/ui/FilterDrawer.astro +0 -124
  107. package/src/ui/PageHeader.astro +0 -39
  108. package/src/ui/SearchField.astro +0 -106
  109. package/src/ui/button.test.ts +0 -54
  110. 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}