@grove-dev/astro 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -33
- package/dist/index.js.map +1 -1
- package/dist/lib/display.d.ts +1 -1
- package/dist/lib/display.js +1 -1
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/icon-kinds.d.ts +1 -1
- package/dist/lib/icon-kinds.js +43 -43
- package/dist/lib/icon-registry.d.ts +2 -2
- package/dist/lib/icon-registry.js +23 -23
- package/dist/lib/index.d.ts +9 -9
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +9 -9
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/lenses.d.ts +1 -1
- package/dist/lib/lenses.js +1 -1
- package/dist/lib/packaged-icons.d.ts +1 -1
- package/dist/lib/packaged-icons.js +4 -4
- package/dist/lib/repo.d.ts +1 -1
- package/dist/lib/repo.js +1 -1
- package/dist/lib/search.d.ts +1 -1
- package/dist/lib/search.js +1 -1
- package/dist/lib/taxonomy-counts.d.ts +1 -1
- package/dist/lib/taxonomy-counts.js +1 -1
- package/package.json +7 -17
- package/src/index.ts +23 -38
- package/src/lib/display.ts +1 -1
- package/src/lib/format.ts +1 -1
- package/src/lib/icon-kinds.ts +44 -44
- package/src/lib/icon-registry.ts +25 -25
- package/src/lib/index.ts +9 -9
- package/src/lib/lenses.ts +1 -1
- package/src/lib/packaged-icons.ts +8 -8
- package/src/lib/repo.ts +1 -1
- package/src/lib/search.ts +1 -1
- package/src/lib/taxonomy-counts.ts +1 -1
- package/src/server/collections.ts +53 -42
- package/src/server/contrast.ts +14 -9
- package/src/server/directory.ts +22 -17
- package/src/server/index.ts +3 -2
- package/src/server/models.ts +404 -213
- package/src/server/seo.ts +34 -31
- package/src/styles.css +235 -91
- package/dist/layouts/index.d.ts +0 -25
- package/dist/layouts/index.d.ts.map +0 -1
- package/dist/layouts/index.js +0 -25
- package/dist/layouts/index.js.map +0 -1
- package/dist/ui/button.d.ts +0 -53
- package/dist/ui/button.d.ts.map +0 -1
- package/dist/ui/button.js +0 -90
- package/dist/ui/button.js.map +0 -1
- package/src/components/CardGrid.astro +0 -41
- package/src/components/CardIcon.astro +0 -67
- package/src/components/CategoryGrid.astro +0 -114
- package/src/components/CollectionCard.astro +0 -56
- package/src/components/CollectionIndex.astro +0 -102
- package/src/components/CollectionPage.astro +0 -128
- package/src/components/CollectionRow.astro +0 -81
- package/src/components/CollectionTeaser.astro +0 -47
- package/src/components/ContributorsGrid.astro +0 -143
- package/src/components/DirectoryIndexClient.astro +0 -510
- package/src/components/EditorialSummary.astro +0 -58
- package/src/components/FilterGroupMenu.astro +0 -69
- package/src/components/FilterOptions.astro +0 -49
- package/src/components/FinalCta.astro +0 -43
- package/src/components/Hero.astro +0 -243
- package/src/components/Icon.astro +0 -191
- package/src/components/IndexRow.astro +0 -37
- package/src/components/LanguageBreakdown.astro +0 -67
- package/src/components/MarkdownBody.astro +0 -37
- package/src/components/OriginalCollection.astro +0 -117
- package/src/components/Pagination.astro +0 -50
- package/src/components/PoweredBy.astro +0 -60
- package/src/components/ProjectCard.astro +0 -321
- package/src/components/RecordHeader.astro +0 -221
- package/src/components/RecordSection.astro +0 -128
- package/src/components/RecordSidebar.astro +0 -387
- package/src/components/RefinePanel.astro +0 -168
- package/src/components/SmartLensTabs.astro +0 -81
- package/src/components/StackGrid.astro +0 -151
- package/src/components/StackPlatformChips.astro +0 -73
- package/src/components/SubmissionClient.astro +0 -254
- package/src/components/TableOfContents.astro +0 -140
- package/src/components/WhyThisExists.astro +0 -116
- package/src/layouts/BaseLayout.astro +0 -415
- package/src/layouts/Container.astro +0 -18
- package/src/layouts/Footer.astro +0 -231
- package/src/layouts/Header.astro +0 -214
- package/src/layouts/SectionHeader.astro +0 -43
- package/src/layouts/Seo.astro +0 -171
- package/src/layouts/ThemeToggle.astro +0 -118
- package/src/layouts/astro-shim.d.ts +0 -29
- package/src/layouts/index.ts +0 -24
- package/src/server/collections.test.ts +0 -212
- package/src/server/contrast.test.ts +0 -82
- package/src/server/directory.test.ts +0 -177
- package/src/server/github-repo.test.ts +0 -88
- package/src/server/models-home.test.ts +0 -101
- package/src/server/models.test.ts +0 -135
- package/src/server/seo.test.ts +0 -123
- package/src/ui/Badge.astro +0 -38
- package/src/ui/Button.astro +0 -42
- package/src/ui/EmptyState.astro +0 -38
- package/src/ui/FilterDrawer.astro +0 -124
- package/src/ui/PageHeader.astro +0 -39
- package/src/ui/SearchField.astro +0 -106
- package/src/ui/button.test.ts +0 -54
- package/src/ui/button.ts +0 -107
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* WhyThisExists — the "what is this site for?" section.
|
|
4
|
-
*
|
|
5
|
-
* Renders 3 short, opinionated points (icon + title + body). The
|
|
6
|
-
* icon set is small and generic: "item" (window/grid glyph),
|
|
7
|
-
* "code" (angle brackets), "search" (magnifier). Defaults to
|
|
8
|
-
* the canonical "directory, source, search" trio.
|
|
9
|
-
*
|
|
10
|
-
* Generic: every point comes through props. No blueprint-specific
|
|
11
|
-
* copy. Consumers can pass `points` to swap the order or copy.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
interface WhyPoint {
|
|
15
|
-
title: string;
|
|
16
|
-
body: string;
|
|
17
|
-
icon: "item" | "code" | "search";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface Props {
|
|
21
|
-
points?: WhyPoint[];
|
|
22
|
-
eyebrow?: string;
|
|
23
|
-
title?: string;
|
|
24
|
-
description?: string;
|
|
25
|
-
class?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const {
|
|
29
|
-
points,
|
|
30
|
-
eyebrow = "Why this exists",
|
|
31
|
-
title = "Most directory discovery is fragmented.",
|
|
32
|
-
description = "Lists are useful, but they often become long, static pages without enough context to compare what is real, maintained, and worth exploring.",
|
|
33
|
-
class: className = "",
|
|
34
|
-
} = Astro.props;
|
|
35
|
-
|
|
36
|
-
const defaultPoints: WhyPoint[] = [
|
|
37
|
-
{
|
|
38
|
-
icon: "item",
|
|
39
|
-
title: "Real projects",
|
|
40
|
-
body: "Not SDKs, templates, or tiny demos. Focused on complete projects you can actually run.",
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
icon: "code",
|
|
44
|
-
title: "Codebase context",
|
|
45
|
-
body: "Stack, platform, license, activity, maturity, and contribution signals in one place.",
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
icon: "search",
|
|
49
|
-
title: "Searchable discovery",
|
|
50
|
-
body: "Browse by category, stack, platform, stars, recency, and status instead of scanning a flat list.",
|
|
51
|
-
},
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
const items = points && points.length > 0 ? points : defaultPoints;
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
<section
|
|
58
|
-
id="about"
|
|
59
|
-
class:list={["py-12 sm:py-16 scroll-mt-20 border-b border-ink-200 dark:border-ink-800", className]}
|
|
60
|
-
aria-labelledby="grove-why-heading"
|
|
61
|
-
>
|
|
62
|
-
<div class="mx-auto w-full max-w-container px-5 sm:px-7">
|
|
63
|
-
<header class="flex flex-col gap-2">
|
|
64
|
-
<span class="text-2xs font-medium uppercase tracking-wider text-ink-500 dark:text-ink-400">
|
|
65
|
-
{eyebrow}
|
|
66
|
-
</span>
|
|
67
|
-
<h2
|
|
68
|
-
id="grove-why-heading"
|
|
69
|
-
class="m-0 max-w-[40ch] text-[1.5rem] sm:text-[1.75rem] font-semibold tracking-tight text-ink-900 dark:text-ink-100"
|
|
70
|
-
>
|
|
71
|
-
{title}
|
|
72
|
-
</h2>
|
|
73
|
-
{description && (
|
|
74
|
-
<p class="m-0 max-w-[64ch] text-[0.9375rem] leading-relaxed text-ink-500 dark:text-ink-400">
|
|
75
|
-
{description}
|
|
76
|
-
</p>
|
|
77
|
-
)}
|
|
78
|
-
</header>
|
|
79
|
-
|
|
80
|
-
<ul
|
|
81
|
-
class="m-0 mt-7 grid list-none grid-cols-1 gap-3 p-0 sm:grid-cols-3"
|
|
82
|
-
aria-label="Why this directory"
|
|
83
|
-
>
|
|
84
|
-
{items.map((p) => (
|
|
85
|
-
<li class="rounded-[calc(var(--radius)+0.25rem)] border border-border bg-card shadow-none transition-colors p-4">
|
|
86
|
-
<div
|
|
87
|
-
aria-hidden="true"
|
|
88
|
-
class="mb-3 flex h-8 w-8 items-center justify-center rounded-md border border-ink-200 bg-white text-ink-700 dark:border-ink-800 dark:bg-ink-950 dark:text-ink-300"
|
|
89
|
-
>
|
|
90
|
-
{p.icon === "item" && (
|
|
91
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
|
92
|
-
<path d="M1.75 3.5a.25.25 0 0 1 .25-.25h12a.25.25 0 0 1 .25.25v8a.25.25 0 0 1-.25.25h-12a.25.25 0 0 1-.25-.25v-8ZM2 4.75v6h12v-6H2ZM2.5 6.5h4v2.5h-4V6.5Z"></path>
|
|
93
|
-
</svg>
|
|
94
|
-
)}
|
|
95
|
-
{p.icon === "code" && (
|
|
96
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
|
97
|
-
<path d="M4.72 3.22a.75.75 0 0 1 1.06 1.06L2.06 8l3.72 3.72a.75.75 0 1 1-1.06 1.06L.47 8.53a.75.75 0 0 1 0-1.06l4.25-4.25Zm6.56 0a.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.06L13.94 8l-3.72-3.72a.75.75 0 0 1 0-1.06Z"></path>
|
|
98
|
-
</svg>
|
|
99
|
-
)}
|
|
100
|
-
{p.icon === "search" && (
|
|
101
|
-
<svg viewBox="0 0 16 16" width="16" height="16" fill="currentColor">
|
|
102
|
-
<path d="M10.68 11.74a6 6 0 1 1 1.06-1.06l3.04 3.04a.75.75 0 1 1-1.06 1.06l-3.04-3.04ZM11.5 7a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z"></path>
|
|
103
|
-
</svg>
|
|
104
|
-
)}
|
|
105
|
-
</div>
|
|
106
|
-
<h3 class="m-0 text-[0.9375rem] font-semibold text-ink-900 dark:text-ink-100">
|
|
107
|
-
{p.title}
|
|
108
|
-
</h3>
|
|
109
|
-
<p class="m-0 mt-1.5 text-[0.8125rem] leading-relaxed text-ink-500 dark:text-ink-400">
|
|
110
|
-
{p.body}
|
|
111
|
-
</p>
|
|
112
|
-
</li>
|
|
113
|
-
))}
|
|
114
|
-
</ul>
|
|
115
|
-
</div>
|
|
116
|
-
</section>
|
|
@@ -1,415 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
/**
|
|
3
|
-
* BaseLayout — wraps every page. Owns the theme-detection inline
|
|
4
|
-
* script and the global shell (Header, Footer). Per-page SEO
|
|
5
|
-
* (title, description, OG, JSON-LD) is delegated to `<Seo />` so
|
|
6
|
-
* each page can declare what it needs without polluting the layout
|
|
7
|
-
* file.
|
|
8
|
-
*
|
|
9
|
-
* Generic: takes a `site` prop with brand + nav + stats. No
|
|
10
|
-
* `siteConfig` import — consumers compose the props from their
|
|
11
|
-
* own config (typically `data/generated/site-config.json` for the
|
|
12
|
-
* showcase).
|
|
13
|
-
*
|
|
14
|
-
* GA4 can be configured once through `site.analytics.googleAnalyticsId`
|
|
15
|
-
* or overridden per page with `gaId`. Omit both for privacy-first deployments.
|
|
16
|
-
*/
|
|
17
|
-
import '../styles.css';
|
|
18
|
-
import Header from './Header.astro';
|
|
19
|
-
import Footer from './Footer.astro';
|
|
20
|
-
import Seo from './Seo.astro';
|
|
21
|
-
import { derivePrimaryPalette } from '../server/contrast';
|
|
22
|
-
|
|
23
|
-
interface NavItem {
|
|
24
|
-
label: string;
|
|
25
|
-
href: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface SiteStats {
|
|
29
|
-
totalApps?: number;
|
|
30
|
-
totalCategories?: number;
|
|
31
|
-
totalStacks?: number;
|
|
32
|
-
totalOwners?: number;
|
|
33
|
-
totalStars?: number;
|
|
34
|
-
repositoryStars?: number;
|
|
35
|
-
repositoryForks?: number;
|
|
36
|
-
repositoryContributors?: number;
|
|
37
|
-
originalRepo?: string;
|
|
38
|
-
originalStars?: number;
|
|
39
|
-
originalForks?: number;
|
|
40
|
-
originalContributors?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface Site {
|
|
44
|
-
name: string;
|
|
45
|
-
tagline?: string;
|
|
46
|
-
description?: string;
|
|
47
|
-
nav: NavItem[];
|
|
48
|
-
footer?: {
|
|
49
|
-
columns?: Array<{
|
|
50
|
-
heading: string;
|
|
51
|
-
items: Array<{ label: string; href: string; external?: boolean }>;
|
|
52
|
-
}>;
|
|
53
|
-
copyright?: string;
|
|
54
|
-
license?: string;
|
|
55
|
-
};
|
|
56
|
-
submission?: {
|
|
57
|
-
eyebrow?: string;
|
|
58
|
-
title?: string;
|
|
59
|
-
description?: string;
|
|
60
|
-
good?: string[];
|
|
61
|
-
avoid?: string[];
|
|
62
|
-
};
|
|
63
|
-
repoUrl?: string;
|
|
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;
|
|
69
|
-
/** BCP-47 language tag (e.g. "en"). Drives `<html lang>` and og:locale. */
|
|
70
|
-
locale?: string;
|
|
71
|
-
/** Twitter/X handle (e.g. "@grove") — emitted as twitter:site. */
|
|
72
|
-
twitter?: string;
|
|
73
|
-
analytics?: {
|
|
74
|
-
googleAnalyticsId?: string;
|
|
75
|
-
};
|
|
76
|
-
/** Optional directory-level stats — populated from site-config.json
|
|
77
|
-
* at generate time. Components use it to render hero counts,
|
|
78
|
-
* origin card stats, and footer summary lines. */
|
|
79
|
-
stats?: SiteStats;
|
|
80
|
-
/** Theme knobs from grove.config.ts — resolved into CSS custom
|
|
81
|
-
* properties on `<html>` so the package stylesheet picks them up.
|
|
82
|
-
* Values are plain strings because the prop usually arrives from a
|
|
83
|
-
* JSON import; unknown values fall back to the package defaults. */
|
|
84
|
-
theme?: {
|
|
85
|
-
primaryColor?: string;
|
|
86
|
-
/** "none" | "soft" | "round" */
|
|
87
|
-
radius?: string;
|
|
88
|
-
/** "compact" | "comfortable" | "spacious" */
|
|
89
|
-
density?: string;
|
|
90
|
-
containerWidth?: string;
|
|
91
|
-
};
|
|
92
|
-
blueprintConfig?: {
|
|
93
|
-
routeSlug?: string;
|
|
94
|
-
labelSingular?: string;
|
|
95
|
-
labelPlural?: string;
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
interface Props {
|
|
100
|
-
title: string;
|
|
101
|
-
description: string;
|
|
102
|
-
site: Site;
|
|
103
|
-
type?: 'website' | 'article' | 'profile';
|
|
104
|
-
noindex?: boolean;
|
|
105
|
-
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
|
|
106
|
-
/** Open Graph / Twitter image path (e.g. "/og/records/foo.png").
|
|
107
|
-
* Defaults to the site-wide /og-image.svg inside <Seo />. */
|
|
108
|
-
image?: string;
|
|
109
|
-
/** Alt text for the OG/Twitter image. */
|
|
110
|
-
imageAlt?: string;
|
|
111
|
-
/** Optional Google Analytics 4 Measurement ID (e.g. "G-XXXXXX"). */
|
|
112
|
-
gaId?: string;
|
|
113
|
-
/** Path used for JSON-LD `SearchAction` target. Defaults to "/items". */
|
|
114
|
-
searchPath?: string;
|
|
115
|
-
/** Submit button label rendered in the header. Omit to hide. */
|
|
116
|
-
submitLabel?: string;
|
|
117
|
-
/** Submit button href rendered in the header. */
|
|
118
|
-
submitHref?: string;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const {
|
|
122
|
-
title,
|
|
123
|
-
description,
|
|
124
|
-
site,
|
|
125
|
-
type,
|
|
126
|
-
noindex,
|
|
127
|
-
jsonLd,
|
|
128
|
-
image,
|
|
129
|
-
imageAlt,
|
|
130
|
-
gaId,
|
|
131
|
-
searchPath,
|
|
132
|
-
submitLabel,
|
|
133
|
-
submitHref,
|
|
134
|
-
} = Astro.props;
|
|
135
|
-
const directoryPath = searchPath ?? `/${site.blueprintConfig?.routeSlug ?? 'items'}`;
|
|
136
|
-
const effectiveGaId = gaId ?? site.analytics?.googleAnalyticsId;
|
|
137
|
-
|
|
138
|
-
// ── Theme wiring ─────────────────────────────────────────────────
|
|
139
|
-
// `theme.*` from grove.config.ts resolves to CSS custom properties
|
|
140
|
-
// on the <html> style attribute. An attribute (not an inline
|
|
141
|
-
// <style>) so the override wins over the bundled stylesheet
|
|
142
|
-
// regardless of how Vite orders the CSS output.
|
|
143
|
-
const RADIUS_PRESETS: Record<string, [string, string, string]> = {
|
|
144
|
-
none: ["0rem", "0rem", "0rem"],
|
|
145
|
-
soft: ["0.625rem", "0.5rem", "0.75rem"],
|
|
146
|
-
round: ["0.875rem", "0.75rem", "1rem"],
|
|
147
|
-
};
|
|
148
|
-
const DENSITY_SPACING: Record<string, string> = {
|
|
149
|
-
compact: "0.225rem",
|
|
150
|
-
comfortable: "0.25rem",
|
|
151
|
-
spacious: "0.28rem",
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
// Brand fill and text derive from ONE WCAG computation (contrast.ts)
|
|
155
|
-
// so background and foreground can never drift apart across themes.
|
|
156
|
-
|
|
157
|
-
const themeConfig = site.theme;
|
|
158
|
-
const themeStyleParts: string[] = [];
|
|
159
|
-
if (themeConfig) {
|
|
160
|
-
const radius = RADIUS_PRESETS[themeConfig.radius ?? "soft"];
|
|
161
|
-
if (radius && themeConfig.radius && themeConfig.radius !== "soft") {
|
|
162
|
-
themeStyleParts.push(
|
|
163
|
-
`--grove-radius: ${radius[0]}`,
|
|
164
|
-
`--grove-radius-lg: ${radius[1]}`,
|
|
165
|
-
`--grove-radius-xl: ${radius[2]}`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
const spacing = DENSITY_SPACING[themeConfig.density ?? "comfortable"];
|
|
169
|
-
if (spacing && themeConfig.density && themeConfig.density !== "comfortable") {
|
|
170
|
-
themeStyleParts.push(`--spacing: ${spacing}`);
|
|
171
|
-
}
|
|
172
|
-
if (themeConfig.containerWidth) {
|
|
173
|
-
themeStyleParts.push(`--grove-container: ${themeConfig.containerWidth}`);
|
|
174
|
-
}
|
|
175
|
-
const primary = themeConfig.primaryColor;
|
|
176
|
-
if (typeof primary === "string" && /^#[0-9a-fA-F]{3,8}$/.test(primary)) {
|
|
177
|
-
const palette = derivePrimaryPalette(primary);
|
|
178
|
-
if (palette) {
|
|
179
|
-
themeStyleParts.push(
|
|
180
|
-
`--grove-theme-primary: ${palette.solid}`,
|
|
181
|
-
`--grove-theme-primary-foreground: ${palette.solidForeground}`,
|
|
182
|
-
`--grove-theme-primary-dark: ${palette.dark}`,
|
|
183
|
-
`--grove-theme-primary-dark-foreground: ${palette.darkForeground}`,
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const themeStyle = themeStyleParts.length > 0 ? `${themeStyleParts.join("; ")};` : undefined;
|
|
189
|
-
|
|
190
|
-
// `site.favicon` points at a file the consumer owns under `public/`.
|
|
191
|
-
// The fallback is a generated square — tinted with the configured
|
|
192
|
-
// brand color rather than a hardcoded near-black, so an unconfigured
|
|
193
|
-
// site still gets a tab mark that is recognisably its own.
|
|
194
|
-
const faviconFill = (site.theme?.primaryColor ?? "#18181b").trim();
|
|
195
|
-
const favicon =
|
|
196
|
-
site.favicon ??
|
|
197
|
-
`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`;
|
|
198
|
-
---
|
|
199
|
-
|
|
200
|
-
<!doctype html>
|
|
201
|
-
<html lang={site.locale ?? 'en'} style={themeStyle}>
|
|
202
|
-
<head>
|
|
203
|
-
<meta charset="UTF-8" />
|
|
204
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
205
|
-
<meta name="generator" content={Astro.generator} />
|
|
206
|
-
<meta name="theme-color" content="#0a0a0c" media="(prefers-color-scheme: dark)" />
|
|
207
|
-
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme: light)" />
|
|
208
|
-
<link rel="icon" href={favicon} />
|
|
209
|
-
<link rel="sitemap" href="/sitemap.xml" />
|
|
210
|
-
{/* Preconnect to GitHub's avatar CDN — most pages render at least
|
|
211
|
-
one avatar (record list cards, detail page logo) and the
|
|
212
|
-
connection is reusable across cards. Cross-origin only, no
|
|
213
|
-
document fetch, so the cost is one TLS handshake per page. */}
|
|
214
|
-
<link rel="preconnect" href="https://avatars.githubusercontent.com" crossorigin="anonymous" />
|
|
215
|
-
<link
|
|
216
|
-
rel="alternate"
|
|
217
|
-
type="text/plain"
|
|
218
|
-
href="/llms.txt"
|
|
219
|
-
title="LLM usage guidance"
|
|
220
|
-
/>
|
|
221
|
-
|
|
222
|
-
{effectiveGaId && (
|
|
223
|
-
<>
|
|
224
|
-
<script
|
|
225
|
-
is:inline
|
|
226
|
-
async
|
|
227
|
-
src={`https://www.googletagmanager.com/gtag/js?id=${effectiveGaId}`}
|
|
228
|
-
/>
|
|
229
|
-
<script is:inline define:vars={{ gaId: effectiveGaId }}>
|
|
230
|
-
window.dataLayer = window.dataLayer || [];
|
|
231
|
-
function gtag() {
|
|
232
|
-
dataLayer.push(arguments);
|
|
233
|
-
}
|
|
234
|
-
gtag("js", new Date());
|
|
235
|
-
gtag("config", gaId, {
|
|
236
|
-
page_path: window.location.pathname + window.location.search,
|
|
237
|
-
page_title: document.title,
|
|
238
|
-
});
|
|
239
|
-
</script>
|
|
240
|
-
</>
|
|
241
|
-
)}
|
|
242
|
-
|
|
243
|
-
<Seo
|
|
244
|
-
title={title}
|
|
245
|
-
description={description}
|
|
246
|
-
type={type}
|
|
247
|
-
noindex={noindex}
|
|
248
|
-
jsonLd={jsonLd}
|
|
249
|
-
{...(image ? { image } : {})}
|
|
250
|
-
imageAlt={imageAlt}
|
|
251
|
-
site={site}
|
|
252
|
-
searchPath={directoryPath}
|
|
253
|
-
/>
|
|
254
|
-
|
|
255
|
-
<slot name="head" />
|
|
256
|
-
|
|
257
|
-
<script is:inline>
|
|
258
|
-
// THEME_INIT — runs before paint so the user never sees a
|
|
259
|
-
// flash of the wrong theme. State is persisted in localStorage
|
|
260
|
-
// under the key "grove-theme" (matching ThemeToggle.astro).
|
|
261
|
-
(function () {
|
|
262
|
-
var STORAGE_KEY = "grove-theme";
|
|
263
|
-
function getMode() {
|
|
264
|
-
try {
|
|
265
|
-
var stored = localStorage.getItem(STORAGE_KEY);
|
|
266
|
-
if (stored === "light" || stored === "dark" || stored === "system")
|
|
267
|
-
return stored;
|
|
268
|
-
} catch (e) {
|
|
269
|
-
// localStorage may be blocked (private mode, etc.) —
|
|
270
|
-
// fall through to the system default.
|
|
271
|
-
}
|
|
272
|
-
return "system";
|
|
273
|
-
}
|
|
274
|
-
function apply(mode) {
|
|
275
|
-
var resolved =
|
|
276
|
-
mode === "system"
|
|
277
|
-
? (window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
278
|
-
? "dark"
|
|
279
|
-
: "light")
|
|
280
|
-
: mode;
|
|
281
|
-
var root = document.documentElement;
|
|
282
|
-
if (resolved === "dark") root.classList.add("dark");
|
|
283
|
-
else root.classList.remove("dark");
|
|
284
|
-
root.dataset.theme = mode;
|
|
285
|
-
root.dataset.resolvedTheme = resolved;
|
|
286
|
-
}
|
|
287
|
-
apply(getMode());
|
|
288
|
-
window.addEventListener("theme:set", function (e) {
|
|
289
|
-
apply(e.detail);
|
|
290
|
-
});
|
|
291
|
-
var mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
292
|
-
mq.addEventListener("change", function () {
|
|
293
|
-
if (getMode() === "system") apply("system");
|
|
294
|
-
});
|
|
295
|
-
})();
|
|
296
|
-
</script>
|
|
297
|
-
</head>
|
|
298
|
-
<body class="min-h-screen bg-background text-foreground">
|
|
299
|
-
<a
|
|
300
|
-
href="#main"
|
|
301
|
-
class="sr-only z-50 rounded-md bg-background px-3 py-2 text-sm font-medium text-foreground outline-none ring-2 ring-ring focus:not-sr-only focus:fixed focus:left-3 focus:top-3"
|
|
302
|
-
>
|
|
303
|
-
Skip to content
|
|
304
|
-
</a>
|
|
305
|
-
<slot name="header">
|
|
306
|
-
<Header
|
|
307
|
-
site={site}
|
|
308
|
-
submitLabel={submitLabel ?? (site.blueprintConfig ? `Submit ${site.blueprintConfig.labelSingular ?? "item"}` : undefined)}
|
|
309
|
-
submitHref={submitHref ?? (site.blueprintConfig ? "/submit" : undefined)}
|
|
310
|
-
/>
|
|
311
|
-
</slot>
|
|
312
|
-
|
|
313
|
-
<main id="main">
|
|
314
|
-
<slot />
|
|
315
|
-
</main>
|
|
316
|
-
|
|
317
|
-
<slot name="footer">
|
|
318
|
-
<Footer
|
|
319
|
-
site={site}
|
|
320
|
-
stats={{
|
|
321
|
-
totalApps: site.stats?.totalApps,
|
|
322
|
-
totalCategories: site.stats?.totalCategories,
|
|
323
|
-
totalStacks: site.stats?.totalStacks,
|
|
324
|
-
totalOwners: site.stats?.totalOwners,
|
|
325
|
-
}}
|
|
326
|
-
itemsLabel={site.blueprintConfig?.labelPlural ?? "items"}
|
|
327
|
-
discover={{
|
|
328
|
-
heading: "Discover",
|
|
329
|
-
items: [
|
|
330
|
-
...(site.stats?.totalApps
|
|
331
|
-
? [{ label: "Browse all", href: directoryPath }]
|
|
332
|
-
: []),
|
|
333
|
-
{ label: "Community", href: "/contributors" },
|
|
334
|
-
...(site.stats?.originalRepo
|
|
335
|
-
? [{ label: "View on GitHub", href: site.stats.originalRepo, external: true }]
|
|
336
|
-
: []),
|
|
337
|
-
],
|
|
338
|
-
}}
|
|
339
|
-
contribute={{
|
|
340
|
-
heading: "Contribute",
|
|
341
|
-
items: [
|
|
342
|
-
...(site.repoUrl
|
|
343
|
-
? [
|
|
344
|
-
{
|
|
345
|
-
label: "Submit via GitHub",
|
|
346
|
-
href: `${site.repoUrl}/blob/main/CONTRIBUTING.md`,
|
|
347
|
-
external: true,
|
|
348
|
-
},
|
|
349
|
-
]
|
|
350
|
-
: []),
|
|
351
|
-
...(site.repoUrl
|
|
352
|
-
? [
|
|
353
|
-
{
|
|
354
|
-
label: "Report an issue",
|
|
355
|
-
href: `${site.repoUrl}/issues`,
|
|
356
|
-
external: true,
|
|
357
|
-
},
|
|
358
|
-
]
|
|
359
|
-
: []),
|
|
360
|
-
],
|
|
361
|
-
}}
|
|
362
|
-
project={{
|
|
363
|
-
heading: "Project",
|
|
364
|
-
items: [
|
|
365
|
-
...(site.repoUrl
|
|
366
|
-
? [
|
|
367
|
-
{
|
|
368
|
-
label: "Source on GitHub",
|
|
369
|
-
href: site.repoUrl,
|
|
370
|
-
external: true,
|
|
371
|
-
},
|
|
372
|
-
]
|
|
373
|
-
: []),
|
|
374
|
-
...(site.repoUrl
|
|
375
|
-
? [
|
|
376
|
-
{
|
|
377
|
-
label: "Changelog",
|
|
378
|
-
href: `${site.repoUrl}/blob/main/CHANGELOG.md`,
|
|
379
|
-
external: true,
|
|
380
|
-
},
|
|
381
|
-
]
|
|
382
|
-
: []),
|
|
383
|
-
],
|
|
384
|
-
}}
|
|
385
|
-
/>
|
|
386
|
-
</slot>
|
|
387
|
-
|
|
388
|
-
<slot name="body-end" />
|
|
389
|
-
|
|
390
|
-
{effectiveGaId && (
|
|
391
|
-
<script is:inline define:vars={{ gaId: effectiveGaId }}>
|
|
392
|
-
(function () {
|
|
393
|
-
// GA4's `gtag('config', id)` call in the <head> already auto-pings
|
|
394
|
-
// `page_view` on full navigations, so this block only needs to
|
|
395
|
-
// catch history changes (back/forward). No history.pushState /
|
|
396
|
-
// replaceState wrap: the package does not install
|
|
397
|
-
// @astrojs/transitions' ClientRouter, and those wraps
|
|
398
|
-
// double-fired page_view events on every navigation.
|
|
399
|
-
var last = location.pathname + location.search;
|
|
400
|
-
function report() {
|
|
401
|
-
var next = location.pathname + location.search;
|
|
402
|
-
if (next === last) return;
|
|
403
|
-
last = next;
|
|
404
|
-
window.gtag("event", "page_view", {
|
|
405
|
-
page_path: next,
|
|
406
|
-
page_title: document.title,
|
|
407
|
-
page_location: location.href,
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
window.addEventListener("popstate", report);
|
|
411
|
-
})();
|
|
412
|
-
</script>
|
|
413
|
-
)}
|
|
414
|
-
</body>
|
|
415
|
-
</html>
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
interface Props {
|
|
3
|
-
size?: "default" | "wide" | "narrow";
|
|
4
|
-
class?: string;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const { size = "default", class: className = "" } = Astro.props;
|
|
8
|
-
const sizeClass =
|
|
9
|
-
size === "wide"
|
|
10
|
-
? "mx-auto w-full max-w-wide"
|
|
11
|
-
: size === "narrow"
|
|
12
|
-
? "mx-auto w-full max-w-narrow"
|
|
13
|
-
: "mx-auto w-full max-w-container";
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
<div class={`${sizeClass} px-5 sm:px-7 ${className}`}>
|
|
17
|
-
<slot />
|
|
18
|
-
</div>
|