@grove-dev/astro 0.5.1 → 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.
Files changed (78) hide show
  1. package/assets/icons/.grove-icons.json +47 -0
  2. package/assets/icons/platforms/android.svg +1 -0
  3. package/assets/icons/platforms/app.svg +4 -0
  4. package/assets/icons/platforms/chrome.svg +1 -0
  5. package/assets/icons/platforms/desktop.svg +4 -0
  6. package/assets/icons/platforms/embedded.svg +5 -0
  7. package/assets/icons/platforms/ios.svg +1 -0
  8. package/assets/icons/platforms/linux.svg +1 -0
  9. package/assets/icons/platforms/macos.svg +1 -0
  10. package/assets/icons/platforms/ubuntu.svg +1 -0
  11. package/assets/icons/platforms/web.svg +5 -0
  12. package/assets/icons/platforms/windows.svg +1 -0
  13. package/assets/icons/stacks/android.svg +1 -0
  14. package/assets/icons/stacks/apple.svg +1 -0
  15. package/assets/icons/stacks/bun.svg +1 -0
  16. package/assets/icons/stacks/capacitor.svg +1 -0
  17. package/assets/icons/stacks/clojure.svg +1 -0
  18. package/assets/icons/stacks/dart.svg +1 -0
  19. package/assets/icons/stacks/deno.svg +1 -0
  20. package/assets/icons/stacks/django.svg +1 -0
  21. package/assets/icons/stacks/docker.svg +1 -0
  22. package/assets/icons/stacks/firebase.svg +1 -0
  23. package/assets/icons/stacks/flutter.svg +1 -0
  24. package/assets/icons/stacks/go.svg +1 -0
  25. package/assets/icons/stacks/graphql.svg +1 -0
  26. package/assets/icons/stacks/ionic.svg +1 -0
  27. package/assets/icons/stacks/java.svg +1 -0
  28. package/assets/icons/stacks/javascript.svg +1 -0
  29. package/assets/icons/stacks/kotlin.svg +1 -0
  30. package/assets/icons/stacks/llm.svg +4 -0
  31. package/assets/icons/stacks/mongodb.svg +1 -0
  32. package/assets/icons/stacks/nodejs.svg +1 -0
  33. package/assets/icons/stacks/python.svg +1 -0
  34. package/assets/icons/stacks/rag.svg +6 -0
  35. package/assets/icons/stacks/react-native.svg +1 -0
  36. package/assets/icons/stacks/react.svg +1 -0
  37. package/assets/icons/stacks/rust.svg +1 -0
  38. package/assets/icons/stacks/solidity.svg +1 -0
  39. package/assets/icons/stacks/sveltekit.svg +1 -0
  40. package/assets/icons/stacks/swift.svg +1 -0
  41. package/assets/icons/stacks/tauri.svg +1 -0
  42. package/assets/icons/stacks/tensorflow.svg +1 -0
  43. package/assets/icons/stacks/typescript.svg +1 -0
  44. package/assets/icons/stacks/vue.svg +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +23 -4
  47. package/dist/index.js.map +1 -1
  48. package/dist/lib/icon-kinds.d.ts +13 -0
  49. package/dist/lib/icon-kinds.d.ts.map +1 -0
  50. package/dist/lib/icon-kinds.js +46 -0
  51. package/dist/lib/icon-kinds.js.map +1 -0
  52. package/dist/lib/icon-registry.d.ts +40 -0
  53. package/dist/lib/icon-registry.d.ts.map +1 -0
  54. package/dist/lib/icon-registry.js +85 -0
  55. package/dist/lib/icon-registry.js.map +1 -0
  56. package/dist/lib/index.d.ts +4 -0
  57. package/dist/lib/index.d.ts.map +1 -1
  58. package/dist/lib/index.js +4 -0
  59. package/dist/lib/index.js.map +1 -1
  60. package/dist/lib/packaged-icons.d.ts +9 -0
  61. package/dist/lib/packaged-icons.d.ts.map +1 -0
  62. package/dist/lib/packaged-icons.js +21 -0
  63. package/dist/lib/packaged-icons.js.map +1 -0
  64. package/package.json +3 -2
  65. package/src/components/CollectionRow.astro +12 -47
  66. package/src/components/Icon.astro +74 -152
  67. package/src/components/IndexRow.astro +7 -25
  68. package/src/components/ProjectCard.astro +76 -117
  69. package/src/components/RefinePanel.astro +10 -1
  70. package/src/components/SmartLensTabs.astro +11 -1
  71. package/src/index.ts +28 -4
  72. package/src/layouts/BaseLayout.astro +11 -1
  73. package/src/layouts/Header.astro +19 -4
  74. package/src/lib/icon-kinds.ts +59 -0
  75. package/src/lib/icon-registry.ts +111 -0
  76. package/src/lib/index.ts +4 -0
  77. package/src/lib/packaged-icons.ts +32 -0
  78. package/src/styles.css +61 -0
@@ -0,0 +1,85 @@
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
+ * Normalize: lowercase + replace spaces/underscores with dashes.
11
+ *
12
+ * Deliberately NOT `@grove-dev/core`'s `slugify`, which also strips
13
+ * dots and other punctuation. Icon names are file names, so they must
14
+ * survive verbatim apart from case and separator normalization.
15
+ */
16
+ export function slugifyIconName(s) {
17
+ return s
18
+ .toLowerCase()
19
+ .trim()
20
+ .replace(/[\s_]+/g, "-");
21
+ }
22
+ /**
23
+ * Built-in id remaps. Stack-only by design: `category="platform"`
24
+ * resolves straight to the file name so a consumer shipping their own
25
+ * `public/icons/platforms/ios.svg` is never silently bypassed.
26
+ */
27
+ const defaultAliases = {
28
+ stack: {
29
+ ios: "apple",
30
+ "native-ios": "apple",
31
+ ipados: "apple",
32
+ macos: "apple",
33
+ "objective-c": "apple",
34
+ spritekit: "apple",
35
+ swiftui: "apple",
36
+ kmp: "kotlin",
37
+ "kotlin-multiplatform": "kotlin",
38
+ // Names the shipped set covers under a different file.
39
+ "node.js": "nodejs",
40
+ node: "nodejs",
41
+ clojurescript: "clojure",
42
+ svelte: "sveltekit",
43
+ },
44
+ };
45
+ /** Stack marks that `category="platform"` should reuse rather than duplicate. */
46
+ const platformStackIcons = new Set(["docker"]);
47
+ /** Platforms with no mark at all — always render the initials chip. */
48
+ export const textOnlyPlatforms = new Set(["self-host", "self-hosted"]);
49
+ /**
50
+ * Resolve a registry id to the asset it points at.
51
+ *
52
+ * Lookup order: `aliases` → the built-in stack aliases → the raw
53
+ * slugified name. Any name not covered by the shipped set simply
54
+ * resolves to `{category-folder}/{slug}`, which is what lets consumers
55
+ * drop their own SVGs in without touching this file.
56
+ */
57
+ export function resolveIconAsset(name, category, aliases) {
58
+ const resolved = resolveName(name, category, aliases);
59
+ const folder = category === "stack" || (category === "platform" && platformStackIcons.has(resolved))
60
+ ? "stacks"
61
+ : category === "platform"
62
+ ? "platforms"
63
+ : "brands";
64
+ return {
65
+ folder,
66
+ name: resolved,
67
+ key: `${folder}/${resolved}`,
68
+ textOnly: category === "platform" && textOnlyPlatforms.has(resolved),
69
+ };
70
+ }
71
+ function resolveName(raw, category, aliases) {
72
+ if (!raw)
73
+ return "";
74
+ for (const registry of [aliases, defaultAliases[category]]) {
75
+ if (!registry)
76
+ continue;
77
+ const lower = raw.toLowerCase();
78
+ for (const [k, v] of Object.entries(registry)) {
79
+ if (k.toLowerCase() === lower)
80
+ return slugifyIconName(v);
81
+ }
82
+ }
83
+ return slugifyIconName(raw);
84
+ }
85
+ //# sourceMappingURL=icon-registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icon-registry.js","sourceRoot":"","sources":["../../src/lib/icon-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,CAAS;IACvC,OAAO,CAAC;SACL,WAAW,EAAE;SACb,IAAI,EAAE;SACN,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,MAAM,cAAc,GAA0D;IAC5E,KAAK,EAAE;QACL,GAAG,EAAE,OAAO;QACZ,YAAY,EAAE,OAAO;QACrB,MAAM,EAAE,OAAO;QACf,KAAK,EAAE,OAAO;QACd,aAAa,EAAE,OAAO;QACtB,SAAS,EAAE,OAAO;QAClB,OAAO,EAAE,OAAO;QAChB,GAAG,EAAE,QAAQ;QACb,sBAAsB,EAAE,QAAQ;QAChC,uDAAuD;QACvD,SAAS,EAAE,QAAQ;QACnB,IAAI,EAAE,QAAQ;QACd,aAAa,EAAE,SAAS;QACxB,MAAM,EAAE,WAAW;KACpB;CACF,CAAC;AAEF,iFAAiF;AACjF,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE/C,uEAAuE;AACvE,MAAM,CAAC,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC,CAAC;AAavE;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,QAAsB,EACtB,OAAgC;IAEhC,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACtD,MAAM,MAAM,GACV,QAAQ,KAAK,OAAO,IAAI,CAAC,QAAQ,KAAK,UAAU,IAAI,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACnF,CAAC,CAAC,QAAQ;QACV,CAAC,CAAC,QAAQ,KAAK,UAAU;YACvB,CAAC,CAAC,WAAW;YACb,CAAC,CAAC,QAAQ,CAAC;IAEjB,OAAO;QACL,MAAM;QACN,IAAI,EAAE,QAAQ;QACd,GAAG,EAAE,GAAG,MAAM,IAAI,QAAQ,EAAE;QAC5B,QAAQ,EAAE,QAAQ,KAAK,UAAU,IAAI,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC;KACrE,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAClB,GAAW,EACX,QAAsB,EACtB,OAAgC;IAEhC,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,KAAK,MAAM,QAAQ,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QAC3D,IAAI,CAAC,QAAQ;YAAE,SAAS;QACxB,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;QAChC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9C,IAAI,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK;gBAAE,OAAO,eAAe,CAAC,CAAC,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IACD,OAAO,eAAe,CAAC,GAAG,CAAC,CAAC;AAC9B,CAAC"}
@@ -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
@@ -22,4 +23,7 @@ export * from "./search.js";
22
23
  export * from "./taxonomy-counts.js";
23
24
  export * from "./display.js";
24
25
  export * from "./load-collections.js";
26
+ export * from "./icon-registry.js";
27
+ export * from "./icon-kinds.js";
28
+ export * from "./packaged-icons.js";
25
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
package/dist/lib/index.js 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
@@ -22,4 +23,7 @@ export * from "./search.js";
22
23
  export * from "./taxonomy-counts.js";
23
24
  export * from "./display.js";
24
25
  export * from "./load-collections.js";
26
+ export * from "./icon-registry.js";
27
+ export * from "./icon-kinds.js";
28
+ export * from "./packaged-icons.js";
25
29
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import type { IconSyncOptions, IconSyncResult } from "@grove-dev/core";
2
+ /**
3
+ * `packages/astro/assets/icons` — two levels up, which holds for both
4
+ * `src/lib/` (tests, Vite) and `dist/lib/` (published).
5
+ */
6
+ export declare const packagedIconsDir: string;
7
+ /** Copy the packaged icon set into a site's `public/`. */
8
+ export declare function syncPackagedIcons(publicDir: string, options?: IconSyncOptions): Promise<IconSyncResult>;
9
+ //# sourceMappingURL=packaged-icons.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packaged-icons.d.ts","sourceRoot":"","sources":["../../src/lib/packaged-icons.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGvE;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAM5B,CAAC;AAEF,0DAA0D;AAC1D,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,eAAe,GACxB,OAAO,CAAC,cAAc,CAAC,CAEzB"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Locate the icon set that ships inside `@grove-dev/astro`.
3
+ *
4
+ * The component and the SVGs it points at live in the same package on
5
+ * purpose: an upgrade that changes how `Icon.astro` resolves a name
6
+ * carries the matching artwork with it, so a consumer can never end up
7
+ * with a component asking for files their site does not have.
8
+ */
9
+ import { dirname, resolve } from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { syncIconAssets } from "@grove-dev/core";
12
+ /**
13
+ * `packages/astro/assets/icons` — two levels up, which holds for both
14
+ * `src/lib/` (tests, Vite) and `dist/lib/` (published).
15
+ */
16
+ export const packagedIconsDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "icons");
17
+ /** Copy the packaged icon set into a site's `public/`. */
18
+ export function syncPackagedIcons(publicDir, options) {
19
+ return syncIconAssets(packagedIconsDir, publicDir, options);
20
+ }
21
+ //# sourceMappingURL=packaged-icons.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packaged-icons.js","sourceRoot":"","sources":["../../src/lib/packaged-icons.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CACrC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EACvC,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,OAAO,CACR,CAAC;AAEF,0DAA0D;AAC1D,MAAM,UAAU,iBAAiB,CAC/B,SAAiB,EACjB,OAAyB;IAEzB,OAAO,cAAc,CAAC,gBAAgB,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;AAC9D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grove-dev/astro",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
5
  "description": "Composable Astro UI, data adapters, and integration for Grove directories.",
6
6
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  "registry": "https://registry.npmjs.org/"
30
30
  },
31
31
  "files": [
32
+ "assets",
32
33
  "dist",
33
34
  "src/components",
34
35
  "src/layouts",
@@ -60,7 +61,7 @@
60
61
  "sanitize-html": "^2.17.5",
61
62
  "shiki": "^1.29.2",
62
63
  "yaml": "^2.9.0",
63
- "@grove-dev/core": "0.5.1"
64
+ "@grove-dev/core": "0.5.3"
64
65
  },
65
66
  "peerDependencies": {
66
67
  "astro": "^6.0.0 || ^7.0.0"
@@ -12,19 +12,20 @@
12
12
  * - `href` — full detail URL; wins over `routeSlug`
13
13
  * - `routeSlug` — blueprint slug (e.g. "projects"); details derived
14
14
  * as `/${routeSlug}/${slug}` when `href` is not provided
15
- * - `showChips` — render platform + stack chip row (default true)
16
- * - `showMeta` render stars/updated/license metadata (default true)
17
- * - `showLabel` render the curated/category label (default true)
15
+ * - `showChips`, `showLabel`deprecated no-ops. The card renders
16
+ * one fixed layout everywhere, so a record cannot look like a
17
+ * different record depending on the page it appears on.
18
+ * - `showMeta` — still honoured: it controls whether the *data*
19
+ * (stars, last-updated) is passed in at all, not how it is drawn
18
20
  * - `class` — extra class on the card root
19
21
  * - `eager` — bypass lazy loading for above-the-fold avatars
20
22
  *
21
- * Footer links:
22
- * - "View repo" when `entry.repoHref` is set
23
- * - "Visit site" — when `entry.homepageHref` is set
23
+ * The repository is reached through the card's `owner/repo` line;
24
+ * there is no separate footer link button.
24
25
  *
25
26
  * Analytics: emits `data-event="collection_item_click"` /
26
- * `data-event-source="collection_row"` on the card root and per-link
27
- * events on the footer links, so sites wire one handler in BaseLayout.
27
+ * `data-event-source="collection_row"` on the card root, so sites wire
28
+ * one handler in BaseLayout.
28
29
  */
29
30
  import type { CollectionEntry } from "@grove-dev/core";
30
31
  import ProjectCard from "./ProjectCard.astro";
@@ -35,8 +36,11 @@ interface Props {
35
36
  href?: string;
36
37
  /** Blueprint route slug; detail URL = `/${routeSlug}/${slug}`. */
37
38
  routeSlug?: string;
39
+ /** @deprecated Ignored — the card renders one fixed layout. */
38
40
  showChips?: boolean;
41
+ /** Pass stars / last-updated through to the card. Default true. */
39
42
  showMeta?: boolean;
43
+ /** @deprecated Ignored — see `showChips`. */
40
44
  showLabel?: boolean;
41
45
  eager?: boolean;
42
46
  class?: string;
@@ -46,9 +50,7 @@ const {
46
50
  entry,
47
51
  href: hrefProp,
48
52
  routeSlug,
49
- showChips = true,
50
53
  showMeta = true,
51
- showLabel = true,
52
54
  eager = false,
53
55
  class: className = "",
54
56
  } = Astro.props;
@@ -61,33 +63,6 @@ const detailHref =
61
63
  : entry.url || (base ? `/${base}/${slug}` : `/${slug}`);
62
64
 
63
65
  const repoHref = entry.repoHref;
64
- const homepageHref = entry.homepageHref;
65
- const curated = Boolean(entry.curationScore || (entry.activityScore && entry.activityScore > 0.5));
66
-
67
- const secondaryLinks = [
68
- ...(repoHref
69
- ? [
70
- {
71
- label: "View repo",
72
- href: repoHref,
73
- external: true,
74
- event: "external_repo_click",
75
- eventSource: "collection_row_view_repo",
76
- },
77
- ]
78
- : []),
79
- ...(homepageHref
80
- ? [
81
- {
82
- label: "Visit site",
83
- href: homepageHref,
84
- external: true,
85
- event: "external_link_click",
86
- eventSource: "collection_row_visit_site",
87
- },
88
- ]
89
- : []),
90
- ];
91
66
  ---
92
67
 
93
68
  <ProjectCard
@@ -98,17 +73,7 @@ const secondaryLinks = [
98
73
  repoUrl={repoHref}
99
74
  stars={showMeta ? entry.stars : undefined}
100
75
  pushedAt={showMeta ? entry.pushedAt : undefined}
101
- license={showMeta ? (entry.license ?? null) : null}
102
- category={entry.categories?.[0]}
103
- status={entry.status}
104
- curated={curated}
105
76
  stack={entry.stack}
106
- platforms={entry.platform ?? []}
107
- showCurated={showLabel}
108
- showLicense={showMeta}
109
- showCategoryStatus={showLabel}
110
- labelledChips={showChips}
111
- secondaryLinks={secondaryLinks}
112
77
  eager={eager}
113
78
  event="collection_item_click"
114
79
  eventSource="collection_row"
@@ -2,18 +2,14 @@
2
2
  /**
3
3
  * Icon — brand icon registry.
4
4
  *
5
- * Renders an `<img src="/icons/{stacks|platforms|brands}/{name}.svg" />`
6
- * for a given registry name. The Astro integration ships the default
7
- * `public/icons/{stacks,platforms}/` assets inside the `default`
8
- * template's `public/` directory; consumers can ship their own SVGs
9
- * under the same path (or override the `base` prop) without forking
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
10
9
  * this component.
11
10
  *
12
11
  * The component is **generic** — it does not import any consumer-side
13
- * `siteConfig`, `item`, or blueprint-specific module. Every consumer
14
- * supplies its own icon list (and optionally an `aliases` map for
15
- * friendly ids) via props, and any `name` not in the registry
16
- * falls back to the initials placeholder.
12
+ * `siteConfig`, `item`, or blueprint-specific module.
17
13
  *
18
14
  * `category` selects the subfolder:
19
15
  *
@@ -21,21 +17,43 @@
21
17
  * - "platform" → `/icons/platforms/{name}.svg`
22
18
  * - "brand" → `/icons/brands/{name}.svg` (consumer-supplied)
23
19
  *
24
- * Theme-aware: when the consumer sets `mode: "auto"` the component
25
- * looks for `{name}-light.svg` and `{name}-dark.svg` siblings in
26
- * addition to the default `{name}.svg` and switches the `src` based
27
- * on the resolved theme via the inline `data-grove-icon` script.
28
- * For static (non-themed) icons this is a no-op — set `mode: "static"`
29
- * (the default) to keep the markup simple and avoid hydration cost.
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.
30
39
  *
31
40
  * Aliases: a tiny `aliases` map (e.g. `rn → react-native`) lets the
32
- * registry accept multiple ids for the same asset without a renames
33
- * pass. The lookup falls through the aliases map before failing over
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
34
43
  * to the initials placeholder.
35
44
  */
36
- import { getOwnerAndRepoFromRepoUrl } from "../lib/repo";
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";
37
49
 
38
- export type IconCategory = "stack" | "platform" | "brand";
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
+ */
39
57
  export type IconMode = "static" | "auto";
40
58
 
41
59
  interface Props {
@@ -52,11 +70,14 @@ interface Props {
52
70
  class?: string;
53
71
  /** Tooltip / aria-label. */
54
72
  title?: string;
73
+ /** @deprecated No-op. Kept for one minor release. */
74
+ mode?: IconMode;
55
75
  /**
56
- * Static (single SVG) or auto (light/dark variants via inline
57
- * script). Default "static".
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.
58
79
  */
59
- mode?: IconMode;
80
+ kind?: IconKind;
60
81
  /**
61
82
  * Optional id → id remap. Lookup goes `name → aliases[name] →
62
83
  * {name}.svg` (case-insensitive).
@@ -74,7 +95,7 @@ interface Props {
74
95
  fallbackInitials?: string;
75
96
  /**
76
97
  * Base path for icon files. Defaults to `/icons` (matches the
77
- * default template's `public/icons/`).
98
+ * synced `public/icons/`).
78
99
  */
79
100
  base?: string;
80
101
  }
@@ -85,125 +106,66 @@ const {
85
106
  size = 16,
86
107
  class: className = "",
87
108
  title,
88
- mode = "static",
109
+ kind,
89
110
  aliases,
90
111
  available,
91
112
  fallbackInitials,
92
113
  base = "/icons",
93
114
  } = Astro.props;
94
115
 
95
- // Normalize: lowercase + replace spaces/underscores with dashes.
96
- function slugify(s: string): string {
97
- return s.toLowerCase().trim().replace(/[\s_]+/g, "-");
98
- }
99
-
100
- // Resolve `name` through the aliases map.
101
- function resolveName(raw: string): string {
102
- if (!raw) return "";
103
- const defaultAliases: Partial<Record<IconCategory, Record<string, string>>> = {
104
- stack: {
105
- ios: "apple",
106
- "native-ios": "apple",
107
- ipados: "apple",
108
- macos: "apple",
109
- "objective-c": "apple",
110
- spritekit: "apple",
111
- swiftui: "apple",
112
- kmp: "kotlin",
113
- "kotlin-multiplatform": "kotlin",
114
- },
115
- };
116
- for (const registry of [aliases, defaultAliases[category]]) {
117
- if (!registry) continue;
118
- const lower = raw.toLowerCase();
119
- for (const [k, v] of Object.entries(registry)) {
120
- if (k.toLowerCase() === lower) return slugify(v);
121
- }
122
- }
123
- return slugify(raw);
124
- }
125
-
126
- const resolved = resolveName(name);
127
- const availableIcons = available?.map(slugify);
116
+ const asset = resolveIconAsset(name, category, aliases);
117
+ const resolved = asset.name;
118
+ const availableIcons = available?.map(slugifyIconName);
128
119
  const hasAsset =
129
120
  category === "brand" ||
130
121
  availableIcons === undefined ||
131
122
  availableIcons.includes(resolved);
132
- const effectiveMode: IconMode = resolved === "apple" ? "auto" : mode;
133
- const platformStackIcons = new Set(["docker"]);
134
- const textOnlyPlatforms = new Set(["self-host", "self-hosted"]);
135
- const folder =
136
- category === "stack" || (category === "platform" && platformStackIcons.has(resolved))
137
- ? "stacks"
138
- : category === "platform"
139
- ? "platforms"
140
- : "brands";
141
- const baseUrl = `${base.replace(/\/$/, "")}/${folder}/${resolved}`;
142
-
143
- // Static: just point at the canonical svg.
144
- const staticSrc = `${baseUrl}.svg`;
123
+ const src = `${base.replace(/\/$/, "")}/${asset.folder}/${resolved}.svg`;
145
124
 
146
125
  // Initials fallback: first 1–2 alphanumerics, uppercased.
147
126
  const init = (fallbackInitials ?? name ?? "")
148
127
  .replace(/[^a-z0-9]/gi, "")
149
128
  .slice(0, 2)
150
129
  .toUpperCase();
151
- const showFallback =
152
- !resolved ||
153
- !hasAsset ||
154
- (category === "platform" && textOnlyPlatforms.has(resolved));
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);
155
140
 
156
141
  // Build aria-label.
157
142
  const ariaLabel = title ?? name;
158
143
  const a11y: Record<string, string> = title
159
144
  ? { "aria-label": ariaLabel, role: "img" }
160
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;`;
161
152
  ---
162
153
 
163
154
  {
164
155
  showFallback ? (
165
- <span
166
- class:list={[
167
- "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",
168
- className,
169
- ]}
170
- style={`width:${size}px;height:${size}px;font-size:${Math.max(10, size - 6)}px;`}
171
- {...a11y}
172
- >
156
+ <span class:list={fallbackClass} style={fallbackStyle} {...a11y}>
173
157
  {init || "·"}
174
158
  </span>
175
- ) : effectiveMode === "auto" ? (
176
- <>
177
- <img
178
- src={`${baseUrl}-light.svg`}
179
- data-grove-icon
180
- data-grove-icon-light={`${baseUrl}-light.svg`}
181
- data-grove-icon-dark={`${baseUrl}-dark.svg`}
182
- data-grove-icon-fallback={`${baseUrl}-light.svg`}
183
- width={size}
184
- height={size}
185
- alt=""
186
- class:list={["inline-block shrink-0 align-middle", className]}
187
- onerror="this.hidden=true;this.nextElementSibling.hidden=false"
188
- {...a11y}
189
- />
190
- <span
191
- hidden
192
- data-grove-icon-fallback-initials
193
- class:list={[
194
- "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",
195
- className,
196
- ]}
197
- style={`width:${size}px;height:${size}px;font-size:${Math.max(10, size - 6)}px;`}
198
- {...a11y}
199
- >
200
- {init || "·"}
201
- </span>
202
- </>
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
+ />
203
165
  ) : (
204
166
  <>
205
167
  <img
206
- src={staticSrc}
168
+ src={src}
207
169
  width={size}
208
170
  height={size}
209
171
  alt=""
@@ -213,49 +175,9 @@ const a11y: Record<string, string> = title
213
175
  onerror="this.hidden=true;this.nextElementSibling.hidden=false"
214
176
  {...a11y}
215
177
  />
216
- <span
217
- hidden
218
- data-grove-icon-fallback-initials
219
- class:list={[
220
- "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",
221
- className,
222
- ]}
223
- style={`width:${size}px;height:${size}px;font-size:${Math.max(10, size - 6)}px;`}
224
- {...a11y}
225
- >
178
+ <span hidden data-grove-icon-fallback-initials class:list={fallbackClass} style={fallbackStyle} {...a11y}>
226
179
  {init || "·"}
227
180
  </span>
228
181
  </>
229
182
  )
230
183
  }
231
-
232
- {effectiveMode === "auto" && (
233
- <script is:inline>
234
- // Theme-aware icon swap. Runs after THEME_INIT (BaseLayout) so
235
- // `data-resolved-theme` is already populated. Idempotent — the
236
- // MutationObserver picks up theme changes from ThemeToggle.
237
- (function () {
238
- function swap() {
239
- var theme =
240
- document.documentElement.dataset.resolvedTheme || "light";
241
- var nodes = document.querySelectorAll(
242
- "img[data-grove-icon]:not([data-grove-icon-bound])",
243
- );
244
- for (var i = 0; i < nodes.length; i++) {
245
- var img = nodes[i];
246
- img.setAttribute("data-grove-icon-bound", "1");
247
- var next =
248
- theme === "dark"
249
- ? img.getAttribute("data-grove-icon-dark")
250
- : img.getAttribute("data-grove-icon-light");
251
- if (next) img.src = next;
252
- }
253
- }
254
- swap();
255
- new MutationObserver(swap).observe(document.documentElement, {
256
- attributes: true,
257
- attributeFilter: ["data-resolved-theme"],
258
- });
259
- })();
260
- </script>
261
- )}