@grove-dev/astro 0.6.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +77 -5
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +22 -33
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/display.d.ts +1 -1
  7. package/dist/lib/display.js +1 -1
  8. package/dist/lib/format.d.ts +1 -1
  9. package/dist/lib/format.js +1 -1
  10. package/dist/lib/icon-kinds.d.ts +1 -1
  11. package/dist/lib/icon-kinds.js +43 -43
  12. package/dist/lib/icon-registry.d.ts +2 -2
  13. package/dist/lib/icon-registry.js +23 -23
  14. package/dist/lib/index.d.ts +9 -9
  15. package/dist/lib/index.d.ts.map +1 -1
  16. package/dist/lib/index.js +9 -9
  17. package/dist/lib/index.js.map +1 -1
  18. package/dist/lib/lenses.d.ts +1 -1
  19. package/dist/lib/lenses.js +1 -1
  20. package/dist/lib/packaged-icons.d.ts +1 -1
  21. package/dist/lib/packaged-icons.js +4 -4
  22. package/dist/lib/repo.d.ts +1 -1
  23. package/dist/lib/repo.js +1 -1
  24. package/dist/lib/search.d.ts +1 -1
  25. package/dist/lib/search.js +1 -1
  26. package/dist/lib/taxonomy-counts.d.ts +1 -1
  27. package/dist/lib/taxonomy-counts.js +1 -1
  28. package/package.json +7 -17
  29. package/src/index.ts +23 -38
  30. package/src/lib/display.ts +1 -1
  31. package/src/lib/format.ts +1 -1
  32. package/src/lib/icon-kinds.ts +44 -44
  33. package/src/lib/icon-registry.ts +25 -25
  34. package/src/lib/index.ts +9 -9
  35. package/src/lib/lenses.ts +1 -1
  36. package/src/lib/packaged-icons.ts +8 -8
  37. package/src/lib/repo.ts +1 -1
  38. package/src/lib/search.ts +1 -1
  39. package/src/lib/taxonomy-counts.ts +1 -1
  40. package/src/server/collections.ts +53 -42
  41. package/src/server/contrast.ts +14 -9
  42. package/src/server/directory.ts +22 -17
  43. package/src/server/index.ts +3 -2
  44. package/src/server/models.ts +404 -213
  45. package/src/server/seo.ts +34 -31
  46. package/src/styles.css +235 -91
  47. package/dist/layouts/index.d.ts +0 -25
  48. package/dist/layouts/index.d.ts.map +0 -1
  49. package/dist/layouts/index.js +0 -25
  50. package/dist/layouts/index.js.map +0 -1
  51. package/dist/ui/button.d.ts +0 -53
  52. package/dist/ui/button.d.ts.map +0 -1
  53. package/dist/ui/button.js +0 -90
  54. package/dist/ui/button.js.map +0 -1
  55. package/src/components/CardGrid.astro +0 -41
  56. package/src/components/CardIcon.astro +0 -67
  57. package/src/components/CategoryGrid.astro +0 -114
  58. package/src/components/CollectionCard.astro +0 -56
  59. package/src/components/CollectionIndex.astro +0 -102
  60. package/src/components/CollectionPage.astro +0 -128
  61. package/src/components/CollectionRow.astro +0 -81
  62. package/src/components/CollectionTeaser.astro +0 -47
  63. package/src/components/ContributorsGrid.astro +0 -143
  64. package/src/components/DirectoryIndexClient.astro +0 -510
  65. package/src/components/EditorialSummary.astro +0 -58
  66. package/src/components/FilterGroupMenu.astro +0 -69
  67. package/src/components/FilterOptions.astro +0 -49
  68. package/src/components/FinalCta.astro +0 -43
  69. package/src/components/Hero.astro +0 -243
  70. package/src/components/Icon.astro +0 -191
  71. package/src/components/IndexRow.astro +0 -37
  72. package/src/components/LanguageBreakdown.astro +0 -67
  73. package/src/components/MarkdownBody.astro +0 -37
  74. package/src/components/OriginalCollection.astro +0 -117
  75. package/src/components/Pagination.astro +0 -50
  76. package/src/components/PoweredBy.astro +0 -60
  77. package/src/components/ProjectCard.astro +0 -321
  78. package/src/components/RecordHeader.astro +0 -221
  79. package/src/components/RecordSection.astro +0 -128
  80. package/src/components/RecordSidebar.astro +0 -387
  81. package/src/components/RefinePanel.astro +0 -168
  82. package/src/components/SmartLensTabs.astro +0 -81
  83. package/src/components/StackGrid.astro +0 -151
  84. package/src/components/StackPlatformChips.astro +0 -73
  85. package/src/components/SubmissionClient.astro +0 -254
  86. package/src/components/TableOfContents.astro +0 -140
  87. package/src/components/WhyThisExists.astro +0 -116
  88. package/src/layouts/BaseLayout.astro +0 -415
  89. package/src/layouts/Container.astro +0 -18
  90. package/src/layouts/Footer.astro +0 -231
  91. package/src/layouts/Header.astro +0 -214
  92. package/src/layouts/SectionHeader.astro +0 -43
  93. package/src/layouts/Seo.astro +0 -171
  94. package/src/layouts/ThemeToggle.astro +0 -118
  95. package/src/layouts/astro-shim.d.ts +0 -29
  96. package/src/layouts/index.ts +0 -24
  97. package/src/server/collections.test.ts +0 -212
  98. package/src/server/contrast.test.ts +0 -82
  99. package/src/server/directory.test.ts +0 -177
  100. package/src/server/github-repo.test.ts +0 -88
  101. package/src/server/models-home.test.ts +0 -101
  102. package/src/server/models.test.ts +0 -135
  103. package/src/server/seo.test.ts +0 -123
  104. package/src/ui/Badge.astro +0 -38
  105. package/src/ui/Button.astro +0 -42
  106. package/src/ui/EmptyState.astro +0 -38
  107. package/src/ui/FilterDrawer.astro +0 -124
  108. package/src/ui/PageHeader.astro +0 -39
  109. package/src/ui/SearchField.astro +0 -106
  110. package/src/ui/button.test.ts +0 -54
  111. package/src/ui/button.ts +0 -107
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/layouts/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,qBAAqB,CAAC"}
@@ -1,25 +0,0 @@
1
- /**
2
- * @grove-dev/astro — generic layout components.
3
- *
4
- * These layouts are the public, framework-agnostic shell used by
5
- * every consumer page. They:
6
- *
7
- * - take a `site` prop (name, tagline, nav, repoUrl) instead of
8
- * importing a `siteConfig` from a project-local path;
9
- * - compose the standard Header / Footer / Seo / Container /
10
- * SectionHeader / ThemeToggle components;
11
- * - share the `grove-theme` localStorage key for theme state.
12
- *
13
- * Re-exported as ESM module names. Astro resolves the
14
- * `@grove-dev/astro/layouts/*` subpath import from `src/layouts/`
15
- * (the package's `dist/` only contains TypeScript helpers, not the
16
- * `.astro` files themselves).
17
- */
18
- export { default as BaseLayout } from "./BaseLayout.astro";
19
- export { default as Container } from "./Container.astro";
20
- export { default as Footer } from "./Footer.astro";
21
- export { default as Header } from "./Header.astro";
22
- export { default as SectionHeader } from "./SectionHeader.astro";
23
- export { default as Seo } from "./Seo.astro";
24
- export { default as ThemeToggle } from "./ThemeToggle.astro";
25
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/layouts/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,GAAG,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,qBAAqB,CAAC"}
@@ -1,53 +0,0 @@
1
- /**
2
- * Button / chip class builders — the single source for interactive
3
- * control styling.
4
- *
5
- * Plain TS (not an .astro component) on purpose: `DirectoryIndexClient`
6
- * rebuilds pagination links and active-filter chips in CLIENT-side JS,
7
- * and those class strings must stay byte-identical with the server
8
- * render. Both sides import these builders, so they cannot drift.
9
- *
10
- * Variant semantics (see styles.css):
11
- * - `primary` — the brand action (config `theme.primaryColor`).
12
- * - `secondary` — default chrome buttons.
13
- * - `outline` / `ghost` — quieter chrome.
14
- * - `selected` — "you are here" state (active filter, current lens,
15
- * current page). Neutral inversion via the `selected` tokens —
16
- * deliberately NOT the brand primary, so green never doubles as a
17
- * selection indicator.
18
- */
19
- export type ButtonVariant = "primary" | "secondary" | "outline" | "ghost" | "selected";
20
- /** `bare` emits no size classes — pass custom geometry via `extra`. */
21
- export type ButtonSize = "sm" | "md" | "lg" | "bare";
22
- export declare function buttonClass(variant?: ButtonVariant, size?: ButtonSize, extra?: string): string;
23
- /**
24
- * Layout extras for pagination controls — shared by `Pagination.astro`
25
- * and the client-side pagination rebuild in `DirectoryIndexClient`.
26
- */
27
- export declare const PAGINATION_EXTRA = "min-w-9 px-2 py-1.5 text-2xs";
28
- /**
29
- * The dead end of a pagination row — "Previous" on page 1.
30
- *
31
- * Dimming to `opacity-40` put it at 3.37:1 against the dark surface,
32
- * which is a contrast failure, not a disabled state. 60% still reads
33
- * as unavailable and clears 4.5:1.
34
- */
35
- export declare const PAGINATION_DISABLED = "pointer-events-none opacity-60";
36
- /**
37
- * Facet filter trigger (the "Stack: Any" dropdown buttons). Includes
38
- * the `grove-filter-trigger` hook class because the client script
39
- * reassigns the whole className when the active state changes — the
40
- * hook must survive that reassignment.
41
- */
42
- export declare function filterTriggerClass(active: boolean): string;
43
- /**
44
- * Lens tab (the All / Hot / Mature row). Same geometry as the filter
45
- * trigger; `data-lens-id` targeting keeps its own hook attribute.
46
- */
47
- export declare function lensTabClass(active: boolean): string;
48
- /**
49
- * Active-filter chip (the removable "Stack: Python ×" pills). One
50
- * definition for the server render and the client rebuild.
51
- */
52
- export declare function chipClass(extra?: string): string;
53
- //# sourceMappingURL=button.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"button.d.ts","sourceRoot":"","sources":["../../src/ui/button.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,CAAC;AACvF,uEAAuE;AACvE,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;AAwBrD,wBAAgB,WAAW,CACzB,OAAO,GAAE,aAA2B,EACpC,IAAI,GAAE,UAAiB,EACvB,KAAK,SAAK,GACT,MAAM,CAER;AAED;;;GAGG;AACH,eAAO,MAAM,gBAAgB,iCAAiC,CAAC;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,mCAAmC,CAAC;AAEpE;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAO1D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAMpD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,SAAK,GAAG,MAAM,CAO5C"}
package/dist/ui/button.js DELETED
@@ -1,90 +0,0 @@
1
- /**
2
- * Button / chip class builders — the single source for interactive
3
- * control styling.
4
- *
5
- * Plain TS (not an .astro component) on purpose: `DirectoryIndexClient`
6
- * rebuilds pagination links and active-filter chips in CLIENT-side JS,
7
- * and those class strings must stay byte-identical with the server
8
- * render. Both sides import these builders, so they cannot drift.
9
- *
10
- * Variant semantics (see styles.css):
11
- * - `primary` — the brand action (config `theme.primaryColor`).
12
- * - `secondary` — default chrome buttons.
13
- * - `outline` / `ghost` — quieter chrome.
14
- * - `selected` — "you are here" state (active filter, current lens,
15
- * current page). Neutral inversion via the `selected` tokens —
16
- * deliberately NOT the brand primary, so green never doubles as a
17
- * selection indicator.
18
- */
19
- const BASE = "inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-[var(--radius)] border border-transparent font-medium leading-none no-underline outline-none transition-colors focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/35";
20
- /**
21
- * `md`/`lg` guarantee a ≥44px hit target on small viewports
22
- * (`max-sm:min-h-11`) while keeping the compact desktop height.
23
- */
24
- const SIZES = {
25
- sm: "h-8 px-3 text-sm",
26
- md: "h-9 px-3 text-sm max-sm:min-h-11",
27
- lg: "h-11 px-4 text-sm",
28
- bare: "",
29
- };
30
- const VARIANTS = {
31
- primary: "bg-primary text-primary-foreground hover:bg-primary/90",
32
- secondary: "border-border bg-secondary text-foreground hover:bg-accent",
33
- outline: "border-border bg-transparent text-foreground hover:bg-accent",
34
- ghost: "text-foreground hover:bg-accent",
35
- selected: "bg-selected text-selected-foreground hover:bg-selected/90",
36
- };
37
- export function buttonClass(variant = "secondary", size = "sm", extra = "") {
38
- return [BASE, SIZES[size], VARIANTS[variant], extra].filter(Boolean).join(" ");
39
- }
40
- /**
41
- * Layout extras for pagination controls — shared by `Pagination.astro`
42
- * and the client-side pagination rebuild in `DirectoryIndexClient`.
43
- */
44
- export const PAGINATION_EXTRA = "min-w-9 px-2 py-1.5 text-2xs";
45
- /**
46
- * The dead end of a pagination row — "Previous" on page 1.
47
- *
48
- * Dimming to `opacity-40` put it at 3.37:1 against the dark surface,
49
- * which is a contrast failure, not a disabled state. 60% still reads
50
- * as unavailable and clears 4.5:1.
51
- */
52
- export const PAGINATION_DISABLED = "pointer-events-none opacity-60";
53
- /**
54
- * Facet filter trigger (the "Stack: Any" dropdown buttons). Includes
55
- * the `grove-filter-trigger` hook class because the client script
56
- * reassigns the whole className when the active state changes — the
57
- * hook must survive that reassignment.
58
- */
59
- export function filterTriggerClass(active) {
60
- return [
61
- "grove-filter-trigger",
62
- BASE,
63
- "h-9 px-3 text-[0.8125rem] max-sm:min-h-11",
64
- active ? `${VARIANTS.selected} hover:text-selected-foreground` : VARIANTS.secondary,
65
- ].join(" ");
66
- }
67
- /**
68
- * Lens tab (the All / Hot / Mature row). Same geometry as the filter
69
- * trigger; `data-lens-id` targeting keeps its own hook attribute.
70
- */
71
- export function lensTabClass(active) {
72
- return [
73
- BASE,
74
- "h-9 px-3 text-[0.8125rem] max-sm:min-h-11",
75
- active ? `${VARIANTS.selected} hover:text-selected-foreground` : VARIANTS.secondary,
76
- ].join(" ");
77
- }
78
- /**
79
- * Active-filter chip (the removable "Stack: Python ×" pills). One
80
- * definition for the server render and the client rebuild.
81
- */
82
- export function chipClass(extra = "") {
83
- return [
84
- "inline-flex min-h-5 items-center justify-center gap-1 rounded-full border border-transparent bg-secondary px-2 py-0.5 align-middle text-xs font-medium leading-none text-secondary-foreground transition-colors hover:border-ink-400",
85
- extra,
86
- ]
87
- .filter(Boolean)
88
- .join(" ");
89
- }
90
- //# sourceMappingURL=button.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"button.js","sourceRoot":"","sources":["../../src/ui/button.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAMH,MAAM,IAAI,GACR,iQAAiQ,CAAC;AAEpQ;;;GAGG;AACH,MAAM,KAAK,GAA+B;IACxC,EAAE,EAAE,kBAAkB;IACtB,EAAE,EAAE,kCAAkC;IACtC,EAAE,EAAE,mBAAmB;IACvB,IAAI,EAAE,EAAE;CACT,CAAC;AAEF,MAAM,QAAQ,GAAkC;IAC9C,OAAO,EAAE,wDAAwD;IACjE,SAAS,EAAE,4DAA4D;IACvE,OAAO,EAAE,8DAA8D;IACvE,KAAK,EAAE,iCAAiC;IACxC,QAAQ,EAAE,2DAA2D;CACtE,CAAC;AAEF,MAAM,UAAU,WAAW,CACzB,UAAyB,WAAW,EACpC,OAAmB,IAAI,EACvB,KAAK,GAAG,EAAE;IAEV,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACjF,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,8BAA8B,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,gCAAgC,CAAC;AAEpE;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAe;IAChD,OAAO;QACL,sBAAsB;QACtB,IAAI;QACJ,2CAA2C;QAC3C,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,iCAAiC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS;KACpF,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,MAAe;IAC1C,OAAO;QACL,IAAI;QACJ,2CAA2C;QAC3C,MAAM,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,QAAQ,iCAAiC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS;KACpF,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,KAAK,GAAG,EAAE;IAClC,OAAO;QACL,sOAAsO;QACtO,KAAK;KACN;SACE,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC"}
@@ -1,41 +0,0 @@
1
- ---
2
- /**
3
- * CardGrid — the one responsive grid host for card-shaped children.
4
- *
5
- * Owns the three-column layout every listing surface shares (home
6
- * record sections, browse results, collection entries, taxonomy
7
- * terms) so pages never re-declare grid classes. Cards are passed
8
- * through the default slot, which keeps the grid card-agnostic: any
9
- * component that follows the sizing contract (`h-full flex flex-col`
10
- * root, package radius/padding tokens) drops in — including new card
11
- * types added by consumers.
12
- *
13
- * Children may be bare cards or `<li>` wrappers; bare children are
14
- * valid because the grid renders `role="list"` semantics regardless
15
- * (Safari drops list semantics on `list-none` either way).
16
- *
17
- * `empty` swaps the grid for the `empty` slot so pages keep their
18
- * empty-state markup next to the list they describe.
19
- */
20
- interface Props {
21
- /** id forwarded to the `<ul>` (client scripts key off it). */
22
- id?: string;
23
- /** Render the `empty` slot instead of the grid. */
24
- empty?: boolean;
25
- class?: string;
26
- }
27
-
28
- const { id, empty = false, class: className = "" } = Astro.props;
29
- ---
30
-
31
- {empty ? (
32
- <slot name="empty" />
33
- ) : (
34
- <ul
35
- id={id}
36
- role="list"
37
- class={`m-0 grid list-none grid-cols-1 items-stretch gap-5 p-0 sm:grid-cols-2 lg:grid-cols-3 ${className}`}
38
- >
39
- <slot />
40
- </ul>
41
- )}
@@ -1,67 +0,0 @@
1
- ---
2
- /**
3
- * CardIcon — the small metadata glyphs shared by every card component
4
- * (star, clock, curated check, navigation arrow, external link).
5
- *
6
- * Extracted so a visual tweak (size, stroke, path) is one edit instead
7
- * of a copy-paste hunt across ProjectCard/IndexRow/CollectionRow.
8
- * Color comes from `currentColor`; wrap in a colored span (e.g.
9
- * `text-amber-500` for the star) to tint.
10
- */
11
- interface Props {
12
- name: "star" | "clock" | "curated" | "arrow" | "external";
13
- /** Square size in px. Defaults per-glyph to match card typography. */
14
- size?: number;
15
- class?: string;
16
- }
17
-
18
- const { name, size, class: className = "" } = Astro.props;
19
-
20
- const defaults: Record<Props["name"], number> = {
21
- star: 13,
22
- clock: 11,
23
- curated: 10,
24
- arrow: 16,
25
- external: 10,
26
- };
27
- const px = size ?? defaults[name];
28
- ---
29
-
30
- {name === "star" && (
31
- <svg viewBox="0 0 16 16" width={px} height={px} aria-hidden="true" fill="currentColor" class={className}>
32
- <path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.751.751 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z" />
33
- </svg>
34
- )}
35
- {name === "clock" && (
36
- <svg viewBox="0 0 16 16" width={px} height={px} aria-hidden="true" fill="currentColor" class={className}>
37
- <path d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2Zm0 1.5a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm-.5 1.5a.5.5 0 0 0-1 0v3.25c0 .15.07.29.18.39l1.95 1.65a.5.5 0 0 0 .66-.76L7.5 7.55V5Z" />
38
- </svg>
39
- )}
40
- {name === "curated" && (
41
- <svg viewBox="0 0 16 16" width={px} height={px} aria-hidden="true" fill="currentColor" class={className}>
42
- <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0Zm3.41 5.09a.75.75 0 0 0-1.06 0L6.75 8.69 5.65 7.59a.75.75 0 1 0-1.06 1.06l1.65 1.65a.75.75 0 0 0 1.06 0l4.11-4.11a.75.75 0 0 0 0-1.06Z" />
43
- </svg>
44
- )}
45
- {name === "arrow" && (
46
- <svg
47
- viewBox="0 0 24 24"
48
- width={px}
49
- height={px}
50
- fill="none"
51
- stroke="currentColor"
52
- stroke-width="2"
53
- stroke-linecap="round"
54
- stroke-linejoin="round"
55
- aria-hidden="true"
56
- class={className}
57
- >
58
- <path d="M5 12h14"></path>
59
- <path d="m12 5 7 7-7 7"></path>
60
- </svg>
61
- )}
62
- {name === "external" && (
63
- <svg viewBox="0 0 16 16" width={px} height={px} aria-hidden="true" fill="currentColor" class={className}>
64
- <path d="M3.75 2A1.75 1.75 0 0 0 2 3.75v8.5C2 13.216 2.784 14 3.75 14h8.5A1.75 1.75 0 0 0 14 12.25v-3.5a.75.75 0 0 0-1.5 0v3.5a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-8.5a.25.25 0 0 1 .25-.25h3.5a.75.75 0 0 0 0-1.5h-3.5Z" />
65
- <path d="M10 0a.75.75 0 0 0-.75.75v.75h-3a.75.75 0 0 0 0 1.5h3v.75A.75.75 0 0 0 10.75 4.5h2.5A.75.75 0 0 0 14 3.75v-2.5A.75.75 0 0 0 13.25.5H10Z" />
66
- </svg>
67
- )}
@@ -1,114 +0,0 @@
1
- ---
2
- import { formatCount } from "@grove-dev/core";
3
- /**
4
- * CategoryGrid — browse-by-category grid for the home page.
5
- *
6
- * The lighter sibling of StackGrid. Renders a card per category with
7
- * name, blurb, and count. Generic: takes a list of plain
8
- * `{ name, slug, blurb, count }` entries.
9
- */
10
-
11
- interface CategoryEntry {
12
- name: string;
13
- slug: string;
14
- blurb?: string;
15
- count?: number;
16
- }
17
-
18
- interface Props {
19
- categories: CategoryEntry[];
20
- /** List page base. */
21
- pathPrefix?: string;
22
- /** Extra class. */
23
- class?: string;
24
- /** Hide the section header. */
25
- bare?: boolean;
26
- itemLabelPlural?: string;
27
- /** Noun pair for count chips — singular fixes "1 projects". */
28
- itemNoun?: { singular: string; plural: string };
29
- /** Pass 1 when this grid IS the page (`/categories` index). */
30
- headingLevel?: 1 | 2;
31
- /** Card link builder (defaults to browse filtered by category). */
32
- hrefFor?: (slug: string) => string;
33
- }
34
-
35
- const {
36
- categories,
37
- pathPrefix = "/",
38
- class: className = "",
39
- bare = false,
40
- itemLabelPlural = "items",
41
- itemNoun = { singular: "item", plural: "items" },
42
- headingLevel = 2,
43
- hrefFor,
44
- } = Astro.props;
45
-
46
- const Heading = `h${headingLevel}` as "h1" | "h2";
47
-
48
- function buildHref(slug: string): string {
49
- if (hrefFor) return hrefFor(slug);
50
- const base = pathPrefix.replace(/\/$/, "");
51
- return `${base}?category=${encodeURIComponent(slug)}`;
52
- }
53
- ---
54
-
55
- <section
56
- id="categories"
57
- class:list={["py-12 sm:py-16 scroll-mt-20 border-b border-ink-200 dark:border-ink-800", className]}
58
- aria-labelledby={bare ? undefined : "grove-categorygrid-heading"}
59
- >
60
- <div class="mx-auto w-full max-w-container px-5 sm:px-7">
61
- {!bare && (
62
- <header class="mb-6">
63
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
64
- Categories
65
- </span>
66
- <Heading
67
- id="grove-categorygrid-heading"
68
- class="m-0 mt-1 text-[1.5rem] sm:text-[1.625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
69
- >
70
- Browse by category
71
- </Heading>
72
- <p class="m-0 max-w-[60ch] text-[0.9375rem] text-ink-500 dark:text-ink-400">
73
- Find {itemLabelPlural} by what they do, not by which stack they use.
74
- </p>
75
- </header>
76
- )}
77
-
78
- <div class="overflow-hidden rounded-lg border border-ink-200 dark:border-ink-800">
79
- <ul class="m-0 grid list-none grid-cols-1 divide-y divide-ink-200 p-0 sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-3 dark:divide-ink-800">
80
- {categories.map((c) => (
81
- <li class="divide-x divide-ink-200 dark:divide-ink-800">
82
- <a
83
- href={buildHref(c.slug)}
84
- class="group flex h-full items-start gap-3 p-4 no-underline transition-colors hover:bg-ink-50/60 dark:hover:bg-ink-900/40"
85
- >
86
- <div class="flex min-w-0 flex-1 flex-col gap-0.5">
87
- <div class="flex items-center gap-2">
88
- {/* Not a heading — see StackGrid: link tiles under a
89
- page h1 must not introduce h3 skips. */}
90
- <span class="m-0 text-[0.9375rem] font-semibold tracking-tight text-ink-900 group-hover:underline dark:text-ink-100">
91
- {c.name}
92
- </span>
93
- {typeof c.count === "number" && (
94
- <span class="inline-flex items-center rounded-md border border-ink-200 bg-ink-50 px-1.5 py-0.5 font-mono text-[10px] font-medium text-ink-600 dark:border-ink-800 dark:bg-ink-900/60 dark:text-ink-400">
95
- {formatCount(c.count, itemNoun)}
96
- </span>
97
- )}
98
- </div>
99
- {c.blurb && (
100
- <p class="m-0 text-2xs leading-relaxed text-ink-500 dark:text-ink-400">
101
- {c.blurb}
102
- </p>
103
- )}
104
- </div>
105
- <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true" class="mt-1 shrink-0 text-ink-300 transition-colors group-hover:text-ink-500 dark:text-ink-700 dark:group-hover:text-ink-400" fill="currentColor">
106
- <path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z" />
107
- </svg>
108
- </a>
109
- </li>
110
- ))}
111
- </ul>
112
- </div>
113
- </div>
114
- </section>
@@ -1,56 +0,0 @@
1
- ---
2
- /**
3
- * CollectionCard — card for one collection (not a record): kind
4
- * eyebrow, title, description, entry count.
5
- *
6
- * Extracted from `CollectionIndex` so the collection tile shares the
7
- * `ProjectCard` visual language (border, radius, hover lift, tokens)
8
- * and stays in sync with the record cards as the design evolves.
9
- * Follows the `CardGrid` sizing contract (`h-full flex flex-col`).
10
- */
11
- import { formatCount } from "@grove-dev/core";
12
-
13
- interface Props {
14
- collection: {
15
- slug: string;
16
- url: string;
17
- kind: string;
18
- title: string;
19
- description?: string;
20
- count: number;
21
- };
22
- /** Analytics source emitted with `collection_card_click`. */
23
- eventSource?: string;
24
- /** Noun pair for the entry count (blueprint labels). */
25
- countNoun?: { singular: string; plural: string };
26
- class?: string;
27
- }
28
-
29
- const {
30
- collection: c,
31
- eventSource = "collection_index",
32
- countNoun = { singular: "item", plural: "items" },
33
- class: className = "",
34
- } = Astro.props;
35
- ---
36
-
37
- <a
38
- href={c.url}
39
- data-event="collection_card_click"
40
- data-event-source={eventSource}
41
- data-event-item={c.slug}
42
- class={`group relative flex h-full min-w-0 flex-col gap-2 rounded-xl border border-ink-200 bg-card p-5 no-underline outline-none transition-[border-color,background-color,transform] duration-150 hover:-translate-y-0.5 hover:border-ink-300 hover:bg-ink-50 focus-visible:border-ink-500 focus-visible:ring-2 focus-visible:ring-ink-900/15 dark:border-ink-800 dark:hover:border-ink-700 dark:hover:bg-ink-900 dark:focus-visible:border-ink-500 dark:focus-visible:ring-ink-100/15 ${className}`}
43
- >
44
- <span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
45
- {c.kind === "curated" ? "Curated" : "Generated"}
46
- </span>
47
- <span class="text-[1.0625rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100">
48
- {c.title}
49
- </span>
50
- <span class="line-clamp-2 text-[0.875rem] text-ink-500 dark:text-ink-400">
51
- {c.description}
52
- </span>
53
- <span class="mt-auto pt-2 text-2xs font-medium text-ink-500 transition-colors group-hover:text-ink-900 dark:text-ink-400 dark:group-hover:text-ink-100">
54
- {formatCount(c.count, countNoun)} →
55
- </span>
56
- </a>
@@ -1,102 +0,0 @@
1
- ---
2
- /**
3
- * CollectionIndex — renders a grid of every collection defined in
4
- * `data/collections/*.yml`. Each card links to the collection's
5
- * detail page and shows the entry count.
6
- *
7
- * Consumes a `CollectionIndexModel` produced by
8
- * `getCollectionIndexModel`. Pass `limit` (e.g. 3) for a teaser-style
9
- * subset; the framework also re-exports this component as
10
- * `CollectionTeaser` for the homepage use case.
11
- *
12
- * API parity with `RecordSection`:
13
- * - `id`, `eyebrow`, `title`, `description`
14
- * - `viewAllHref`, `viewAllLabel` — header CTA
15
- * - `emptyLabel` — copy when there are no collections
16
- * - `<slot name="header-actions">` — custom buttons in header
17
- * - `class` — extra class on the outer <section>
18
- */
19
- import CardGrid from "./CardGrid.astro";
20
- import CollectionCard from "./CollectionCard.astro";
21
- import Container from "../layouts/Container.astro";
22
- import SectionHeader from "../layouts/SectionHeader.astro";
23
- import EmptyState from "../ui/EmptyState.astro";
24
- import type { CollectionIndexModel } from "../server/collections.js";
25
-
26
- interface Props {
27
- model: CollectionIndexModel;
28
- id?: string;
29
- eyebrow?: string;
30
- title?: string;
31
- description?: string;
32
- /** Header "View all" link. Hides when not provided. */
33
- viewAllHref?: string;
34
- viewAllLabel?: string;
35
- emptyLabel?: string;
36
- /** Cap the number of cards rendered. Omit for "all". */
37
- limit?: number;
38
- /**
39
- * Heading level for the section title. Pass 1 when this component
40
- * renders the page's title (the `/collections` index); the default
41
- * 2 fits embedded use (homepage teaser).
42
- */
43
- headingLevel?: 1 | 2;
44
- /** Noun pair for the per-card entry counts (blueprint labels). */
45
- countNoun?: { singular: string; plural: string };
46
- class?: string;
47
- }
48
-
49
- const {
50
- model,
51
- id,
52
- eyebrow = "Collections",
53
- title = "Collections",
54
- description = "Curated and generated directories of related items.",
55
- viewAllHref,
56
- viewAllLabel = "View all",
57
- emptyLabel = "No collections yet.",
58
- limit,
59
- headingLevel = 2,
60
- countNoun,
61
- class: className = "",
62
- } = Astro.props;
63
-
64
- const cards = typeof limit === "number" ? model.collections.slice(0, limit) : model.collections;
65
- const headerActions = Astro.slots.has("header-actions");
66
- ---
67
-
68
- <section id={id} class={`py-12 sm:py-16 ${className}`}>
69
- <Container>
70
- <div class="mb-6 flex flex-wrap items-end justify-between gap-3">
71
- <SectionHeader eyebrow={eyebrow} title={title} description={description} level={headingLevel} />
72
- {(viewAllHref || headerActions) && (
73
- <div class="flex shrink-0 items-center gap-2 pb-2">
74
- {viewAllHref && (
75
- <a
76
- href={viewAllHref}
77
- class="text-2xs font-medium text-ink-500 transition-colors hover:text-ink-900 sm:inline dark:text-ink-400 dark:hover:text-ink-100"
78
- >
79
- {viewAllLabel} →
80
- </a>
81
- )}
82
- <slot name="header-actions" />
83
- </div>
84
- )}
85
- </div>
86
-
87
- {cards.length === 0 ? (
88
- <EmptyState
89
- title={emptyLabel}
90
- description="Collections are defined in data/collections/*.yml — add a file there and it appears here on the next build."
91
- />
92
- ) : (
93
- <CardGrid>
94
- {cards.map((c) => (
95
- <li class="m-0 p-0">
96
- <CollectionCard collection={c} countNoun={countNoun} />
97
- </li>
98
- ))}
99
- </CardGrid>
100
- )}
101
- </Container>
102
- </section>