@grove-dev/astro 0.5.2 → 0.5.3
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/assets/icons/.grove-icons.json +47 -0
- package/assets/icons/platforms/android.svg +1 -0
- package/assets/icons/platforms/app.svg +4 -0
- package/assets/icons/platforms/chrome.svg +1 -0
- package/assets/icons/platforms/desktop.svg +4 -0
- package/assets/icons/platforms/embedded.svg +5 -0
- package/assets/icons/platforms/ios.svg +1 -0
- package/assets/icons/platforms/linux.svg +1 -0
- package/assets/icons/platforms/macos.svg +1 -0
- package/assets/icons/platforms/ubuntu.svg +1 -0
- package/assets/icons/platforms/web.svg +5 -0
- package/assets/icons/platforms/windows.svg +1 -0
- package/assets/icons/stacks/android.svg +1 -0
- package/assets/icons/stacks/apple.svg +1 -0
- package/assets/icons/stacks/bun.svg +1 -0
- package/assets/icons/stacks/capacitor.svg +1 -0
- package/assets/icons/stacks/clojure.svg +1 -0
- package/assets/icons/stacks/dart.svg +1 -0
- package/assets/icons/stacks/deno.svg +1 -0
- package/assets/icons/stacks/django.svg +1 -0
- package/assets/icons/stacks/docker.svg +1 -0
- package/assets/icons/stacks/firebase.svg +1 -0
- package/assets/icons/stacks/flutter.svg +1 -0
- package/assets/icons/stacks/go.svg +1 -0
- package/assets/icons/stacks/graphql.svg +1 -0
- package/assets/icons/stacks/ionic.svg +1 -0
- package/assets/icons/stacks/java.svg +1 -0
- package/assets/icons/stacks/javascript.svg +1 -0
- package/assets/icons/stacks/kotlin.svg +1 -0
- package/assets/icons/stacks/llm.svg +4 -0
- package/assets/icons/stacks/mongodb.svg +1 -0
- package/assets/icons/stacks/nodejs.svg +1 -0
- package/assets/icons/stacks/python.svg +1 -0
- package/assets/icons/stacks/rag.svg +6 -0
- package/assets/icons/stacks/react-native.svg +1 -0
- package/assets/icons/stacks/react.svg +1 -0
- package/assets/icons/stacks/rust.svg +1 -0
- package/assets/icons/stacks/solidity.svg +1 -0
- package/assets/icons/stacks/sveltekit.svg +1 -0
- package/assets/icons/stacks/swift.svg +1 -0
- package/assets/icons/stacks/tauri.svg +1 -0
- package/assets/icons/stacks/tensorflow.svg +1 -0
- package/assets/icons/stacks/typescript.svg +1 -0
- package/assets/icons/stacks/vue.svg +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/lib/icon-kinds.d.ts +13 -0
- package/dist/lib/icon-kinds.d.ts.map +1 -0
- package/dist/lib/icon-kinds.js +46 -0
- package/dist/lib/icon-kinds.js.map +1 -0
- package/dist/lib/icon-registry.d.ts +40 -0
- package/dist/lib/icon-registry.d.ts.map +1 -0
- package/dist/lib/icon-registry.js +85 -0
- package/dist/lib/icon-registry.js.map +1 -0
- package/dist/lib/index.d.ts +4 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +4 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/packaged-icons.d.ts +9 -0
- package/dist/lib/packaged-icons.d.ts.map +1 -0
- package/dist/lib/packaged-icons.js +21 -0
- package/dist/lib/packaged-icons.js.map +1 -0
- package/package.json +3 -2
- package/src/components/CollectionRow.astro +12 -47
- package/src/components/Icon.astro +74 -152
- package/src/components/IndexRow.astro +7 -25
- package/src/components/ProjectCard.astro +76 -117
- package/src/index.ts +28 -4
- package/src/layouts/BaseLayout.astro +11 -1
- package/src/layouts/Header.astro +19 -4
- package/src/lib/icon-kinds.ts +59 -0
- package/src/lib/icon-registry.ts +111 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/packaged-icons.ts +32 -0
- package/src/styles.css +61 -0
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Keeps the historical public props (`record`, `detailHref`,
|
|
6
6
|
* `showChips`, `eager`) so consumer pages copied from earlier versions
|
|
7
|
-
* keep working
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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.
|
|
11
12
|
*/
|
|
12
13
|
import type { IndexRecord } from "@grove-dev/core";
|
|
13
14
|
import ProjectCard from "./ProjectCard.astro";
|
|
@@ -15,7 +16,7 @@ import ProjectCard from "./ProjectCard.astro";
|
|
|
15
16
|
interface Props {
|
|
16
17
|
record: IndexRecord;
|
|
17
18
|
detailHref: string;
|
|
18
|
-
/**
|
|
19
|
+
/** @deprecated Ignored — the card renders one fixed layout. */
|
|
19
20
|
showChips?: boolean;
|
|
20
21
|
/** Set true on the first 1-2 rows of a list to bypass lazy
|
|
21
22
|
* loading for the above-the-fold avatars. Pair with
|
|
@@ -24,32 +25,13 @@ interface Props {
|
|
|
24
25
|
class?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const { record, detailHref,
|
|
28
|
+
const { record, detailHref, eager = false, class: className = "" } = Astro.props;
|
|
28
29
|
|
|
29
|
-
const repoHref =
|
|
30
|
-
record.kind === "project" ? (record.repoUrl ?? record.links.github) : record.links.github;
|
|
31
|
-
|
|
32
|
-
const secondaryLinks = repoHref
|
|
33
|
-
? [
|
|
34
|
-
{
|
|
35
|
-
label: "GitHub",
|
|
36
|
-
href: repoHref,
|
|
37
|
-
external: true,
|
|
38
|
-
event: "external_repo_click",
|
|
39
|
-
eventSource: "index_row_github",
|
|
40
|
-
},
|
|
41
|
-
]
|
|
42
|
-
: [];
|
|
43
30
|
---
|
|
44
31
|
|
|
45
32
|
<ProjectCard
|
|
46
33
|
record={record}
|
|
47
34
|
href={detailHref}
|
|
48
|
-
showCurated
|
|
49
|
-
showLicense
|
|
50
|
-
showCategoryStatus
|
|
51
|
-
labelledChips={showChips}
|
|
52
|
-
secondaryLinks={secondaryLinks}
|
|
53
35
|
eager={eager}
|
|
54
36
|
class={className}
|
|
55
37
|
/>
|
|
@@ -10,18 +10,24 @@
|
|
|
10
10
|
* - Default mode: the whole card is one clickable `<a>` (cursor:
|
|
11
11
|
* pointer, subtle border + surface lift on hover, no more than
|
|
12
12
|
* 1–2px vertical motion).
|
|
13
|
-
* - When
|
|
14
|
-
* with a stretched title link, because nesting `<a>`
|
|
15
|
-
* is invalid HTML.
|
|
13
|
+
* - When the record has a repository the root becomes an
|
|
14
|
+
* `<article>` with a stretched title link, because nesting `<a>`
|
|
15
|
+
* inside `<a>` is invalid HTML. The `owner/repo` line is a real
|
|
16
|
+
* link to the repository and sits above the stretched overlay
|
|
16
17
|
* (`relative z-10`).
|
|
17
|
-
* - App icon top-left (48px), app name +
|
|
18
|
+
* - App icon top-left (48px), app name + clickable owner/repo,
|
|
18
19
|
* two-line description with reserved vertical space.
|
|
19
|
-
* - Footer row:
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* - Footer row: stack logos on the left, stars and last-updated on
|
|
21
|
+
* the right, navigation arrow anchored bottom-right.
|
|
22
|
+
*
|
|
23
|
+
* **The layout is fixed.** Earlier versions let list pages opt into a
|
|
24
|
+
* curated badge, a license tag, a `category · status` line and
|
|
25
|
+
* labelled chip rows, so the same record rendered differently
|
|
26
|
+
* depending on where a visitor found it. Those flags are now
|
|
27
|
+
* deprecated no-ops, as is `secondaryLinks` — the repository is
|
|
28
|
+
* reached through the `owner/repo` line rather than a separate footer
|
|
29
|
+
* button. Platform support is likewise off the card: it is reference
|
|
30
|
+
* detail and belongs on the record page (`RecordSidebar`).
|
|
25
31
|
*
|
|
26
32
|
* Data can come from a full `record` or from explicit props (adapters
|
|
27
33
|
* for shapes like `CollectionEntry` pass fields directly); explicit
|
|
@@ -37,14 +43,11 @@ import {
|
|
|
37
43
|
getOwnerAvatarUrl,
|
|
38
44
|
formatRelative,
|
|
39
45
|
formatStars,
|
|
40
|
-
licenseDisplay,
|
|
41
46
|
nameInitials,
|
|
42
47
|
projectStackIds,
|
|
43
|
-
statusDisplay,
|
|
44
48
|
} from "@grove-dev/astro";
|
|
45
49
|
import CardIcon from "./CardIcon.astro";
|
|
46
50
|
import Icon from "./Icon.astro";
|
|
47
|
-
import StackPlatformChips from "./StackPlatformChips.astro";
|
|
48
51
|
|
|
49
52
|
type CardRecord = IndexRecord | Resource | ProjectRecord | IndexProjectRecord;
|
|
50
53
|
|
|
@@ -78,14 +81,31 @@ interface Props {
|
|
|
78
81
|
status?: string;
|
|
79
82
|
curated?: boolean;
|
|
80
83
|
stack?: string;
|
|
84
|
+
/**
|
|
85
|
+
* @deprecated Cards render the stack, not platform support — that
|
|
86
|
+
* belongs on the record page. Accepted and ignored.
|
|
87
|
+
*/
|
|
81
88
|
platforms?: string[];
|
|
82
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* @deprecated The card renders one fixed metadata set on every
|
|
91
|
+
* surface. These flags used to add a curated badge, a license tag,
|
|
92
|
+
* a `category · status` line, and labelled chip rows on list pages
|
|
93
|
+
* only, which made the same record look like a different record
|
|
94
|
+
* depending on where you found it. Accepted and ignored.
|
|
95
|
+
*/
|
|
83
96
|
showCurated?: boolean;
|
|
97
|
+
/** @deprecated Ignored — see `showCurated`. */
|
|
84
98
|
showLicense?: boolean;
|
|
99
|
+
/** @deprecated Ignored — see `showCurated`. */
|
|
85
100
|
showCategoryStatus?: boolean;
|
|
86
|
-
/**
|
|
101
|
+
/** @deprecated Ignored — see `showCurated`. */
|
|
87
102
|
labelledChips?: boolean;
|
|
88
103
|
/** Footer links; presence switches the root to `<article>` mode. */
|
|
104
|
+
/**
|
|
105
|
+
* @deprecated Ignored. The repository is reached by clicking the
|
|
106
|
+
* `owner/repo` line under the title, so cards no longer carry a
|
|
107
|
+
* separate footer link button.
|
|
108
|
+
*/
|
|
89
109
|
secondaryLinks?: SecondaryLink[];
|
|
90
110
|
/** Eager-load the avatar (above-the-fold rows). */
|
|
91
111
|
eager?: boolean;
|
|
@@ -105,17 +125,7 @@ const {
|
|
|
105
125
|
repoUrl: repoUrlProp,
|
|
106
126
|
stars: starsProp,
|
|
107
127
|
pushedAt: pushedAtProp,
|
|
108
|
-
license: licenseProp,
|
|
109
|
-
category: categoryProp,
|
|
110
|
-
status: statusProp,
|
|
111
|
-
curated: curatedProp,
|
|
112
128
|
stack: stackProp,
|
|
113
|
-
platforms: platformsProp,
|
|
114
|
-
showCurated = false,
|
|
115
|
-
showLicense = false,
|
|
116
|
-
showCategoryStatus = false,
|
|
117
|
-
labelledChips = false,
|
|
118
|
-
secondaryLinks = [],
|
|
119
129
|
eager = false,
|
|
120
130
|
event,
|
|
121
131
|
eventSource,
|
|
@@ -148,8 +158,6 @@ const initials = nameInitials(name);
|
|
|
148
158
|
const description =
|
|
149
159
|
descriptionProp ?? (record as { description?: string } | undefined)?.description ?? "";
|
|
150
160
|
|
|
151
|
-
const platforms =
|
|
152
|
-
platformsProp ?? (isProject ? ((record as { platforms?: string[] }).platforms ?? []) : []);
|
|
153
161
|
const stars =
|
|
154
162
|
starsProp ?? (isProject ? (record as { github?: { stars?: number } }).github?.stars : undefined);
|
|
155
163
|
const pushedAt =
|
|
@@ -157,53 +165,34 @@ const pushedAt =
|
|
|
157
165
|
(isProject
|
|
158
166
|
? (record as { github?: { pushedAt?: string | null } }).github?.pushedAt
|
|
159
167
|
: (record as { publishedAt?: string | null } | undefined)?.publishedAt);
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
statusProp ?? (isProject ? (record as { health?: { status?: string } }).health?.status : undefined);
|
|
166
|
-
const curated =
|
|
167
|
-
curatedProp ??
|
|
168
|
-
Boolean(
|
|
169
|
-
(record as { curation?: { reviewed?: boolean } } | undefined)?.curation?.reviewed ||
|
|
170
|
-
(isProject &&
|
|
171
|
-
((record as { bestFor?: string[] }).bestFor?.length ||
|
|
172
|
-
(record as { whyListed?: string[] }).whyListed?.length ||
|
|
173
|
-
(record as { caveats?: string[] }).caveats?.length)),
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
const stackId =
|
|
177
|
-
stackProp ??
|
|
178
|
-
(isProject
|
|
179
|
-
? projectStackIds(record as { stack?: string; stacks?: string[] })[0]
|
|
180
|
-
: undefined);
|
|
168
|
+
const stackIds = stackProp
|
|
169
|
+
? [stackProp]
|
|
170
|
+
: isProject
|
|
171
|
+
? projectStackIds(record as { stack?: string; stacks?: string[] })
|
|
172
|
+
: [];
|
|
181
173
|
|
|
182
174
|
const starsLabel = formatStars(stars);
|
|
183
175
|
const updatedLabel = formatRelative(pushedAt);
|
|
184
|
-
// "Unknown" is an internal placeholder, not information — suppress it
|
|
185
|
-
// so card metadata never reads "Category · Unknown" to visitors.
|
|
186
|
-
const rawStatusText = status ? statusDisplay(status as Parameters<typeof statusDisplay>[0]) : "";
|
|
187
|
-
const statusText = rawStatusText === "Unknown" ? "" : rawStatusText;
|
|
188
|
-
const licenseLabel = license ? licenseDisplay(license) : null;
|
|
189
176
|
|
|
190
177
|
const hasStars = starsLabel !== null && typeof stars === "number" && stars > 0;
|
|
191
178
|
const hasUpdated = Boolean(pushedAt);
|
|
192
179
|
|
|
193
|
-
// `<a>`-in-`<a>` is invalid HTML,
|
|
194
|
-
//
|
|
195
|
-
|
|
180
|
+
// `<a>`-in-`<a>` is invalid HTML, and the `owner/repo` line is its own
|
|
181
|
+
// link to the repository — so any card that shows one renders as an
|
|
182
|
+
// `<article>` whose title link is stretched over the whole card.
|
|
183
|
+
const isArticle = Boolean(repoHref && owner && repo);
|
|
196
184
|
const Root = isArticle ? "article" : "a";
|
|
197
185
|
const rootAttrs = isArticle ? {} : { href, "aria-label": `Open ${name}` };
|
|
198
186
|
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
// `
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
const
|
|
187
|
+
// The card's brand strip shows the record's STACK, not its platforms.
|
|
188
|
+
// Stack is what distinguishes one card from its neighbours in a list;
|
|
189
|
+
// platform support is reference detail, and it belongs on the record
|
|
190
|
+
// page (`RecordSidebar`) where there is room to label it. Marks come
|
|
191
|
+
// from the shared `Icon` component, which falls back to initials when
|
|
192
|
+
// an asset is missing.
|
|
193
|
+
const visibleStacks = stackIds.slice(0, 4);
|
|
194
|
+
const stackOverflow = stackIds.length - visibleStacks.length;
|
|
195
|
+
const hasStacks = visibleStacks.length > 0;
|
|
207
196
|
---
|
|
208
197
|
|
|
209
198
|
<Root
|
|
@@ -251,27 +240,25 @@ const hasPlatforms = visiblePlatforms.length > 0;
|
|
|
251
240
|
</h3>
|
|
252
241
|
{owner && repo && (
|
|
253
242
|
<p class="m-0 truncate font-mono text-[13px] text-ink-500 dark:text-ink-400">
|
|
254
|
-
{
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
243
|
+
{repoHref ? (
|
|
244
|
+
<a
|
|
245
|
+
href={repoHref}
|
|
246
|
+
target="_blank"
|
|
247
|
+
rel="noopener noreferrer"
|
|
248
|
+
title={`${owner}/${repo} on GitHub`}
|
|
249
|
+
data-event="external_repo_click"
|
|
250
|
+
data-event-source="project_card_repo"
|
|
251
|
+
data-event-item={slug}
|
|
252
|
+
class="relative z-10 text-inherit no-underline transition-colors hover:text-ink-900 hover:underline dark:hover:text-ink-100"
|
|
253
|
+
>
|
|
254
|
+
{owner}<span class="opacity-60">/</span>{repo}
|
|
255
|
+
</a>
|
|
256
|
+
) : (
|
|
257
|
+
<Fragment>{owner}<span class="opacity-60">/</span>{repo}</Fragment>
|
|
258
|
+
)}
|
|
262
259
|
</p>
|
|
263
260
|
)}
|
|
264
261
|
</div>
|
|
265
|
-
|
|
266
|
-
{showCurated && curated && (
|
|
267
|
-
<span
|
|
268
|
-
class="inline-flex shrink-0 items-center gap-1 text-[10px] font-medium uppercase tracking-wider text-muted-foreground"
|
|
269
|
-
title="Reviewed by a curator"
|
|
270
|
-
>
|
|
271
|
-
<CardIcon name="curated" />
|
|
272
|
-
curated
|
|
273
|
-
</span>
|
|
274
|
-
)}
|
|
275
262
|
</header>
|
|
276
263
|
|
|
277
264
|
{/* Description — two-line clamp with reserved vertical space */}
|
|
@@ -281,39 +268,16 @@ const hasPlatforms = visiblePlatforms.length > 0;
|
|
|
281
268
|
{description}
|
|
282
269
|
</p>
|
|
283
270
|
|
|
284
|
-
{/*
|
|
285
|
-
{labelledChips && (
|
|
286
|
-
<StackPlatformChips stack={stackId} platform={platforms} />
|
|
287
|
-
)}
|
|
288
|
-
|
|
289
|
-
{/* Footer: links / platform logos · meta · arrow */}
|
|
271
|
+
{/* Footer: stack logos · stars/updated · arrow */}
|
|
290
272
|
<footer class="mt-auto flex items-center justify-between gap-3 text-[12px] sm:text-[13px]">
|
|
291
273
|
<div class="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1 text-ink-500 dark:text-ink-400">
|
|
292
|
-
{
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
data-event={link.event}
|
|
297
|
-
data-event-source={link.eventSource}
|
|
298
|
-
data-event-item={link.event ? slug : undefined}
|
|
299
|
-
class="relative z-10 inline-flex items-center gap-1 font-medium text-ink-700 no-underline transition-colors hover:text-ink-900 dark:text-ink-300 dark:hover:text-ink-100"
|
|
300
|
-
>
|
|
301
|
-
{link.label}
|
|
302
|
-
{link.external && <CardIcon name="external" />}
|
|
303
|
-
</a>
|
|
304
|
-
))}
|
|
305
|
-
{!labelledChips && hasPlatforms && (
|
|
306
|
-
<div class="flex items-center gap-2.5">
|
|
307
|
-
{visiblePlatforms.map((platform) => (
|
|
308
|
-
<Icon
|
|
309
|
-
name={platform}
|
|
310
|
-
category="platform"
|
|
311
|
-
size={22}
|
|
312
|
-
title={platform}
|
|
313
|
-
/>
|
|
274
|
+
{hasStacks && (
|
|
275
|
+
<div class="flex items-center gap-2">
|
|
276
|
+
{visibleStacks.map((stack) => (
|
|
277
|
+
<Icon name={stack} category="stack" size={16} title={stack} />
|
|
314
278
|
))}
|
|
315
|
-
{
|
|
316
|
-
<span class="text-[12px] text-ink-500 dark:text-ink-400">+{
|
|
279
|
+
{stackOverflow > 0 && (
|
|
280
|
+
<span class="text-[12px] text-ink-500 dark:text-ink-400">+{stackOverflow}</span>
|
|
317
281
|
)}
|
|
318
282
|
</div>
|
|
319
283
|
)}
|
|
@@ -331,11 +295,6 @@ const hasPlatforms = visiblePlatforms.length > 0;
|
|
|
331
295
|
Updated {updatedLabel}
|
|
332
296
|
</span>
|
|
333
297
|
)}
|
|
334
|
-
{showLicense && licenseLabel && (
|
|
335
|
-
<span class="font-mono text-[10px] uppercase tracking-wide" title={`License: ${licenseLabel}`}>
|
|
336
|
-
{licenseLabel}
|
|
337
|
-
</span>
|
|
338
|
-
)}
|
|
339
298
|
<CardIcon
|
|
340
299
|
name="arrow"
|
|
341
300
|
class="shrink-0 text-ink-400 transition-colors group-hover:text-ink-700 dark:text-ink-500 dark:group-hover:text-ink-100"
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from "node:url";
|
|
|
29
29
|
import { dirname, resolve } from "node:path";
|
|
30
30
|
import type { AstroIntegration } from "astro";
|
|
31
31
|
import { loadConfig, prepareDirectory } from "@grove-dev/core";
|
|
32
|
+
import { syncPackagedIcons } from "./lib/packaged-icons.js";
|
|
32
33
|
|
|
33
34
|
export * from "@grove-dev/core";
|
|
34
35
|
|
|
@@ -58,7 +59,7 @@ export default function groveAstro(): AstroIntegration {
|
|
|
58
59
|
return {
|
|
59
60
|
name: "@grove-dev/astro",
|
|
60
61
|
hooks: {
|
|
61
|
-
"astro:config:setup": async ({ config, updateConfig, injectScript }) => {
|
|
62
|
+
"astro:config:setup": async ({ config, updateConfig, injectScript, logger }) => {
|
|
62
63
|
// Alias the components/layouts source directories so
|
|
63
64
|
// consumer builds can import from
|
|
64
65
|
// @grove-dev/astro/components/ProjectCard.astro
|
|
@@ -68,10 +69,33 @@ export default function groveAstro(): AstroIntegration {
|
|
|
68
69
|
const groveConfigPath = resolve(consumerRoot, "grove.config.ts");
|
|
69
70
|
if (existsSync(groveConfigPath)) {
|
|
70
71
|
await prepareDirectory(consumerRoot);
|
|
71
|
-
//
|
|
72
|
-
// Routes belong to the consumer's
|
|
73
|
-
// visible, editable, and safe from
|
|
72
|
+
// Data preparation and the icon sync below are the integration's
|
|
73
|
+
// only site-level side effects. Routes belong to the consumer's
|
|
74
|
+
// `src/pages`, where they remain visible, editable, and safe from
|
|
75
|
+
// future Grove syncs.
|
|
74
76
|
await loadConfig(consumerRoot);
|
|
77
|
+
|
|
78
|
+
// `Icon.astro` resolves every mark to `/icons/**` in the
|
|
79
|
+
// consumer's `public/`, so the packaged SVGs have to land there
|
|
80
|
+
// before the build asks for them. Files the consumer edited are
|
|
81
|
+
// left alone and reported.
|
|
82
|
+
const icons = await syncPackagedIcons(fileURLToPath(config.publicDir));
|
|
83
|
+
if (icons.adopted && icons.written.length > 0) {
|
|
84
|
+
// One-time migration for a site that predates the sidecar.
|
|
85
|
+
// Say so: the overwrite is safe for the scaffold set, but a
|
|
86
|
+
// pre-sidecar hand edit would show up here, and version
|
|
87
|
+
// control is what makes it recoverable.
|
|
88
|
+
logger.info(
|
|
89
|
+
`Adopted ${icons.written.length} icon(s) into public/icons/ and recorded them in ` +
|
|
90
|
+
".grove-icons.json. Review with `git diff public/icons` if you had edited any.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (icons.skipped.length > 0) {
|
|
94
|
+
logger.warn(
|
|
95
|
+
`Kept ${icons.skipped.length} locally modified icon(s): ${icons.skipped.join(", ")}. ` +
|
|
96
|
+
"Run `grove icons sync --force` to restore the packaged versions.",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
75
99
|
}
|
|
76
100
|
const globalCssPath = resolve(consumerRoot, "src/styles/global.css");
|
|
77
101
|
const globalCssExists = existsSync(globalCssPath);
|
|
@@ -62,6 +62,10 @@ interface Site {
|
|
|
62
62
|
};
|
|
63
63
|
repoUrl?: string;
|
|
64
64
|
url?: string;
|
|
65
|
+
/** Header logo path under `public/` (e.g. `/logo.svg`). */
|
|
66
|
+
logo?: string;
|
|
67
|
+
/** Favicon path under `public/` (e.g. `/favicon.svg`). */
|
|
68
|
+
favicon?: string;
|
|
65
69
|
analytics?: {
|
|
66
70
|
googleAnalyticsId?: string;
|
|
67
71
|
};
|
|
@@ -172,8 +176,14 @@ if (themeConfig) {
|
|
|
172
176
|
}
|
|
173
177
|
const themeStyle = themeStyleParts.length > 0 ? `${themeStyleParts.join("; ")};` : undefined;
|
|
174
178
|
|
|
179
|
+
// `site.favicon` points at a file the consumer owns under `public/`.
|
|
180
|
+
// The fallback is a generated square — tinted with the configured
|
|
181
|
+
// brand color rather than a hardcoded near-black, so an unconfigured
|
|
182
|
+
// site still gets a tab mark that is recognisably its own.
|
|
183
|
+
const faviconFill = (site.theme?.primaryColor ?? "#18181b").trim();
|
|
175
184
|
const favicon =
|
|
176
|
-
|
|
185
|
+
site.favicon ??
|
|
186
|
+
`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='3' fill='${encodeURIComponent(faviconFill)}'/%3E%3C/svg%3E`;
|
|
177
187
|
---
|
|
178
188
|
|
|
179
189
|
<!doctype html>
|
package/src/layouts/Header.astro
CHANGED
|
@@ -28,6 +28,8 @@ interface Site {
|
|
|
28
28
|
description?: string;
|
|
29
29
|
repoUrl?: string;
|
|
30
30
|
url?: string;
|
|
31
|
+
/** Logo path under `public/` (e.g. `/logo.svg`). */
|
|
32
|
+
logo?: string;
|
|
31
33
|
stats?: {
|
|
32
34
|
/** Stars on this site's own source repository. */
|
|
33
35
|
repositoryStars?: number;
|
|
@@ -87,10 +89,23 @@ const starsLabel =
|
|
|
87
89
|
href="/"
|
|
88
90
|
class="inline-flex min-w-0 shrink items-center gap-2 text-[0.95rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
89
91
|
>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
{site.logo ? (
|
|
93
|
+
<img
|
|
94
|
+
src={site.logo}
|
|
95
|
+
alt=""
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
width="18"
|
|
98
|
+
height="18"
|
|
99
|
+
class="inline-block h-[18px] w-[18px] shrink-0 object-contain"
|
|
100
|
+
/>
|
|
101
|
+
) : (
|
|
102
|
+
/* No logo configured — a neutral mark is the honest default.
|
|
103
|
+
Set `site.logo` in grove.config.ts to replace it. */
|
|
104
|
+
<span
|
|
105
|
+
aria-hidden="true"
|
|
106
|
+
class="inline-block h-[18px] w-[18px] rounded bg-ink-900 dark:bg-ink-100"
|
|
107
|
+
></span>
|
|
108
|
+
)}
|
|
94
109
|
<span class="truncate">{site.name}</span>
|
|
95
110
|
</a>
|
|
96
111
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// GENERATED by scripts/sync-icons.mjs — do not edit by hand.
|
|
2
|
+
// Run `pnpm icons:sync` to regenerate.
|
|
3
|
+
/**
|
|
4
|
+
* How each packaged icon is painted.
|
|
5
|
+
*
|
|
6
|
+
* This map CLASSIFIES; it does not gate availability. A name that is
|
|
7
|
+
* absent falls through to the `<img>` + initials path, which is what
|
|
8
|
+
* keeps consumer-supplied SVGs under `public/icons/` working.
|
|
9
|
+
*
|
|
10
|
+
* Keyed by `${folder}/${resolvedName}` — the folder matters because
|
|
11
|
+
* `apple` exists under both `stacks/` and `platforms/`.
|
|
12
|
+
*/
|
|
13
|
+
export type IconKind = "color" | "mono";
|
|
14
|
+
|
|
15
|
+
export const ICON_KINDS: Readonly<Record<string, IconKind>> = {
|
|
16
|
+
"platforms/android": "color",
|
|
17
|
+
"platforms/app": "mono",
|
|
18
|
+
"platforms/chrome": "color",
|
|
19
|
+
"platforms/desktop": "mono",
|
|
20
|
+
"platforms/embedded": "mono",
|
|
21
|
+
"platforms/ios": "mono",
|
|
22
|
+
"platforms/linux": "color",
|
|
23
|
+
"platforms/macos": "mono",
|
|
24
|
+
"platforms/ubuntu": "color",
|
|
25
|
+
"platforms/web": "mono",
|
|
26
|
+
"platforms/windows": "color",
|
|
27
|
+
"stacks/android": "color",
|
|
28
|
+
"stacks/apple": "mono",
|
|
29
|
+
"stacks/bun": "color",
|
|
30
|
+
"stacks/capacitor": "color",
|
|
31
|
+
"stacks/clojure": "color",
|
|
32
|
+
"stacks/dart": "color",
|
|
33
|
+
"stacks/deno": "mono",
|
|
34
|
+
"stacks/django": "color",
|
|
35
|
+
"stacks/docker": "color",
|
|
36
|
+
"stacks/firebase": "color",
|
|
37
|
+
"stacks/flutter": "color",
|
|
38
|
+
"stacks/go": "color",
|
|
39
|
+
"stacks/graphql": "color",
|
|
40
|
+
"stacks/ionic": "color",
|
|
41
|
+
"stacks/java": "color",
|
|
42
|
+
"stacks/javascript": "color",
|
|
43
|
+
"stacks/kotlin": "color",
|
|
44
|
+
"stacks/llm": "mono",
|
|
45
|
+
"stacks/mongodb": "color",
|
|
46
|
+
"stacks/nodejs": "color",
|
|
47
|
+
"stacks/python": "color",
|
|
48
|
+
"stacks/rag": "mono",
|
|
49
|
+
"stacks/react": "color",
|
|
50
|
+
"stacks/react-native": "color",
|
|
51
|
+
"stacks/rust": "mono",
|
|
52
|
+
"stacks/solidity": "mono",
|
|
53
|
+
"stacks/sveltekit": "color",
|
|
54
|
+
"stacks/swift": "color",
|
|
55
|
+
"stacks/tauri": "mono",
|
|
56
|
+
"stacks/tensorflow": "color",
|
|
57
|
+
"stacks/typescript": "color",
|
|
58
|
+
"stacks/vue": "color",
|
|
59
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon registry resolution — the name → asset lookup shared by
|
|
3
|
+
* `components/Icon.astro` and the icon test suite.
|
|
4
|
+
*
|
|
5
|
+
* Kept out of the component's frontmatter so tests can assert on the
|
|
6
|
+
* exact resolution the component performs instead of reimplementing
|
|
7
|
+
* (and drifting from) the alias and folder rules.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type IconCategory = "stack" | "platform" | "brand";
|
|
11
|
+
export type IconFolder = "stacks" | "platforms" | "brands";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalize: lowercase + replace spaces/underscores with dashes.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately NOT `@grove-dev/core`'s `slugify`, which also strips
|
|
17
|
+
* dots and other punctuation. Icon names are file names, so they must
|
|
18
|
+
* survive verbatim apart from case and separator normalization.
|
|
19
|
+
*/
|
|
20
|
+
export function slugifyIconName(s: string): string {
|
|
21
|
+
return s
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/[\s_]+/g, "-");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Built-in id remaps. Stack-only by design: `category="platform"`
|
|
29
|
+
* resolves straight to the file name so a consumer shipping their own
|
|
30
|
+
* `public/icons/platforms/ios.svg` is never silently bypassed.
|
|
31
|
+
*/
|
|
32
|
+
const defaultAliases: Partial<Record<IconCategory, Record<string, string>>> = {
|
|
33
|
+
stack: {
|
|
34
|
+
ios: "apple",
|
|
35
|
+
"native-ios": "apple",
|
|
36
|
+
ipados: "apple",
|
|
37
|
+
macos: "apple",
|
|
38
|
+
"objective-c": "apple",
|
|
39
|
+
spritekit: "apple",
|
|
40
|
+
swiftui: "apple",
|
|
41
|
+
kmp: "kotlin",
|
|
42
|
+
"kotlin-multiplatform": "kotlin",
|
|
43
|
+
// Names the shipped set covers under a different file.
|
|
44
|
+
"node.js": "nodejs",
|
|
45
|
+
node: "nodejs",
|
|
46
|
+
clojurescript: "clojure",
|
|
47
|
+
svelte: "sveltekit",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Stack marks that `category="platform"` should reuse rather than duplicate. */
|
|
52
|
+
const platformStackIcons = new Set(["docker"]);
|
|
53
|
+
|
|
54
|
+
/** Platforms with no mark at all — always render the initials chip. */
|
|
55
|
+
export const textOnlyPlatforms = new Set(["self-host", "self-hosted"]);
|
|
56
|
+
|
|
57
|
+
export interface ResolvedIconAsset {
|
|
58
|
+
/** Subfolder under the icon base path. */
|
|
59
|
+
folder: IconFolder;
|
|
60
|
+
/** Alias-resolved, slugified file name (no extension). */
|
|
61
|
+
name: string;
|
|
62
|
+
/** `${folder}/${name}` — the key used by the generated kind map. */
|
|
63
|
+
key: string;
|
|
64
|
+
/** True when this name is text-only and must fall back to initials. */
|
|
65
|
+
textOnly: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a registry id to the asset it points at.
|
|
70
|
+
*
|
|
71
|
+
* Lookup order: `aliases` → the built-in stack aliases → the raw
|
|
72
|
+
* slugified name. Any name not covered by the shipped set simply
|
|
73
|
+
* resolves to `{category-folder}/{slug}`, which is what lets consumers
|
|
74
|
+
* drop their own SVGs in without touching this file.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveIconAsset(
|
|
77
|
+
name: string,
|
|
78
|
+
category: IconCategory,
|
|
79
|
+
aliases?: Record<string, string>,
|
|
80
|
+
): ResolvedIconAsset {
|
|
81
|
+
const resolved = resolveName(name, category, aliases);
|
|
82
|
+
const folder: IconFolder =
|
|
83
|
+
category === "stack" || (category === "platform" && platformStackIcons.has(resolved))
|
|
84
|
+
? "stacks"
|
|
85
|
+
: category === "platform"
|
|
86
|
+
? "platforms"
|
|
87
|
+
: "brands";
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
folder,
|
|
91
|
+
name: resolved,
|
|
92
|
+
key: `${folder}/${resolved}`,
|
|
93
|
+
textOnly: category === "platform" && textOnlyPlatforms.has(resolved),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveName(
|
|
98
|
+
raw: string,
|
|
99
|
+
category: IconCategory,
|
|
100
|
+
aliases?: Record<string, string>,
|
|
101
|
+
): string {
|
|
102
|
+
if (!raw) return "";
|
|
103
|
+
for (const registry of [aliases, defaultAliases[category]]) {
|
|
104
|
+
if (!registry) continue;
|
|
105
|
+
const lower = raw.toLowerCase();
|
|
106
|
+
for (const [k, v] of Object.entries(registry)) {
|
|
107
|
+
if (k.toLowerCase() === lower) return slugifyIconName(v);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return slugifyIconName(raw);
|
|
111
|
+
}
|
package/src/lib/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* `lib/search`)
|
|
11
11
|
* - compute taxonomy counts at build time (`lib/taxonomy-counts`)
|
|
12
12
|
* - pretty-print slugs and label/lens/sort/status ids (`lib/display`)
|
|
13
|
+
* - resolve icon registry ids to asset paths (`lib/icon-registry`)
|
|
13
14
|
*
|
|
14
15
|
* All helpers are pure, dependency-free, and typed against
|
|
15
16
|
* `@grove-dev/core` (specifically `ProjectRecord` for the index
|
|
@@ -23,3 +24,6 @@ export * from "./search.js";
|
|
|
23
24
|
export * from "./taxonomy-counts.js";
|
|
24
25
|
export * from "./display.js";
|
|
25
26
|
export * from "./load-collections.js";
|
|
27
|
+
export * from "./icon-registry.js";
|
|
28
|
+
export * from "./icon-kinds.js";
|
|
29
|
+
export * from "./packaged-icons.js";
|