@grove-dev/astro 0.2.16 → 0.2.19

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 (38) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +44 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/display.js +4 -4
  6. package/dist/lib/display.js.map +1 -1
  7. package/dist/lib/repo.d.ts.map +1 -1
  8. package/dist/lib/repo.js +3 -4
  9. package/dist/lib/repo.js.map +1 -1
  10. package/dist/lib/search.d.ts +2 -2
  11. package/dist/lib/search.d.ts.map +1 -1
  12. package/dist/lib/search.js +17 -6
  13. package/dist/lib/search.js.map +1 -1
  14. package/dist/theme.test.d.ts +2 -0
  15. package/dist/theme.test.d.ts.map +1 -0
  16. package/dist/theme.test.js +41 -0
  17. package/dist/theme.test.js.map +1 -0
  18. package/package.json +1 -1
  19. package/src/components/FilterGroupMenu.astro +3 -1
  20. package/src/components/Hero.astro +3 -3
  21. package/src/components/Icon.astro +4 -4
  22. package/src/components/IndexRow.astro +15 -2
  23. package/src/index.ts +52 -1
  24. package/src/layouts/BaseLayout.astro +12 -11
  25. package/src/layouts/Seo.astro +12 -4
  26. package/src/lib/display.ts +4 -4
  27. package/src/lib/repo.ts +3 -4
  28. package/src/lib/search.ts +18 -8
  29. package/src/styles.css +184 -69
  30. package/src/theme.test.ts +52 -0
  31. package/templates/default/package.json +2 -1
  32. package/templates/default/public/llms-full.txt +1 -1
  33. package/templates/default/public/sitemap.xml +10 -10
  34. package/templates/default/src/data/records.ts +115 -77
  35. package/templates/default/src/pages/[slug]/[recordSlug].astro +29 -33
  36. package/templates/default/src/pages/[slug]/index.astro +20 -296
  37. package/templates/default/src/styles/global.css +14 -39
  38. package/templates/default/tailwind.config.mjs +0 -130
@@ -85,6 +85,14 @@ const organizationLd = {
85
85
  logo: new URL("/og-image.svg", siteUrl).toString(),
86
86
  ...(siteConfig.repoUrl ? { sameAs: [siteConfig.repoUrl] } : {}),
87
87
  };
88
+
89
+ // Pre-stringify the JSON-LD payloads in the frontmatter so the
90
+ // template body just interpolates the resulting strings. For SSR
91
+ // this saves the JSON.stringify call on every render; for static
92
+ // builds the cost is unchanged but the template reads cleaner.
93
+ const websiteLdJson = JSON.stringify(websiteLd);
94
+ const organizationLdJson = JSON.stringify(organizationLd);
95
+ const perPageLdJson = jsonLd ? JSON.stringify(Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : null;
88
96
  ---
89
97
 
90
98
  <!-- Primary meta -->
@@ -113,14 +121,14 @@ const organizationLd = {
113
121
  <meta name="twitter:image:alt" content={`${siteConfig.name} social preview`} />
114
122
 
115
123
  <!-- JSON-LD: site-wide identity -->
116
- <script is:inline type="application/ld+json" set:html={JSON.stringify(websiteLd)} />
117
- <script is:inline type="application/ld+json" set:html={JSON.stringify(organizationLd)} />
124
+ <script is:inline type="application/ld+json" set:html={websiteLdJson} />
125
+ <script is:inline type="application/ld+json" set:html={organizationLdJson} />
118
126
 
119
127
  <!-- JSON-LD: per-page (SoftwareSourceCode for /items/[slug], etc.) -->
120
- {jsonLd && (
128
+ {perPageLdJson && (
121
129
  <script
122
130
  is:inline
123
131
  type="application/ld+json"
124
- set:html={JSON.stringify(Array.isArray(jsonLd) ? jsonLd : [jsonLd])}
132
+ set:html={perPageLdJson}
125
133
  />
126
134
  )}
@@ -45,8 +45,8 @@ export const STATUS_DISPLAY: Record<string, string> = {
45
45
  };
46
46
 
47
47
  export function statusDisplay(s: string | null | undefined): string {
48
- if (!s) return STATUS_DISPLAY.unknown;
49
- return STATUS_DISPLAY[s] ?? STATUS_DISPLAY.unknown;
48
+ if (!s) return STATUS_DISPLAY.unknown ?? "";
49
+ return STATUS_DISPLAY[s] ?? STATUS_DISPLAY.unknown ?? "";
50
50
  }
51
51
 
52
52
  // ── Lens ids (the curated tabs on /items) ────────────────────────────
@@ -65,7 +65,7 @@ export const LENS_DISPLAY: Record<string, string> = {
65
65
  };
66
66
 
67
67
  export function lensDisplay(id: string | null | undefined): string {
68
- if (!id) return LENS_DISPLAY.all;
68
+ if (!id) return LENS_DISPLAY.all ?? "All";
69
69
  return LENS_DISPLAY[id] ?? id;
70
70
  }
71
71
 
@@ -79,7 +79,7 @@ export const SORT_DISPLAY: Record<string, string> = {
79
79
  };
80
80
 
81
81
  export function sortDisplay(id: string | null | undefined): string {
82
- if (!id) return SORT_DISPLAY["recently-updated"];
82
+ if (!id) return SORT_DISPLAY["recently-updated"] ?? "";
83
83
  return SORT_DISPLAY[id] ?? id;
84
84
  }
85
85
 
package/src/lib/repo.ts CHANGED
@@ -17,10 +17,9 @@ export function getOwnerAndRepoFromRepoUrl(repoUrl: string): {
17
17
  }
18
18
  const m = repoUrl.match(/github\.com\/([^/]+)\/([^/?#]+)/);
19
19
  if (!m) return { owner: null, repo: null };
20
- return {
21
- owner: m[1],
22
- repo: m[2].replace(/\.git$/, ""),
23
- };
20
+ const owner = m[1] ?? null;
21
+ const repo = m[2]?.replace(/\.git$/, "") ?? null;
22
+ return { owner, repo };
24
23
  }
25
24
 
26
25
  /**
package/src/lib/search.ts CHANGED
@@ -18,9 +18,9 @@ export type IndexFilters = {
18
18
  /** A curated lens (e.g. "good-to-learn", "production-like"). Single value. */
19
19
  lens?: string;
20
20
  /** Sort order. Single value, defaults to "recently-updated". */
21
- sort?: IndexSort;
21
+ sort?: IndexSort | undefined;
22
22
  /** 1-based page number. */
23
- page?: number;
23
+ page?: number | undefined;
24
24
  };
25
25
 
26
26
  /** Available sort orders, in UI order. */
@@ -65,17 +65,19 @@ export function filtersFromSearchParams(sp: URLSearchParams): IndexFilters {
65
65
  : undefined;
66
66
  const rawPage = Number(sp.get(KEYS.page));
67
67
  const page = Number.isFinite(rawPage) && rawPage >= 1 ? Math.floor(rawPage) : undefined;
68
+ const q = sp.get(KEYS.q);
69
+ const lens = sp.get(KEYS.lens);
68
70
  return {
69
- q: sp.get(KEYS.q) ?? undefined,
71
+ ...(q !== null ? { q } : {}),
70
72
  stacks: sp.getAll(KEYS.stacks).filter(Boolean),
71
73
  platforms: sp.getAll(KEYS.platforms).filter(Boolean),
72
74
  categories: sp.getAll(KEYS.categories).filter(Boolean),
73
75
  labels: sp.getAll(KEYS.labels).filter(Boolean),
74
76
  licenses: sp.getAll(KEYS.licenses).filter(Boolean),
75
77
  statuses: sp.getAll(KEYS.statuses).flatMap((s) => s.split(",")).filter(Boolean),
76
- lens: sp.get(KEYS.lens) ?? undefined,
77
- sort,
78
- page,
78
+ ...(lens !== null ? { lens } : {}),
79
+ ...(sort !== undefined ? { sort } : {}),
80
+ ...(page !== undefined ? { page } : {}),
79
81
  };
80
82
  }
81
83
 
@@ -352,12 +354,20 @@ export function removeFilter(
352
354
  ): IndexFilters {
353
355
  let next: IndexFilters;
354
356
  if (key === "q") {
355
- next = { ...f, q: undefined };
357
+ // `q` is `string | undefined` under exactOptionalPropertyTypes; the
358
+ // cleanest way to drop it is to omit the key entirely (delete-style).
359
+ const { q: _drop, ...rest } = f;
360
+ next = rest;
356
361
  } else {
357
362
  const current = f[key] as string[] | undefined;
358
363
  if (!current) return f;
359
364
  const filtered = current.filter((v) => v !== value);
360
- next = { ...f, [key]: filtered.length ? filtered : undefined };
365
+ if (filtered.length === 0) {
366
+ const { [key]: _drop, ...rest } = f;
367
+ next = rest;
368
+ } else {
369
+ next = { ...f, [key]: filtered };
370
+ }
361
371
  }
362
372
  next.page = 1;
363
373
  return next;
package/src/styles.css CHANGED
@@ -17,61 +17,106 @@
17
17
  */
18
18
 
19
19
  @import "tailwindcss";
20
+ /*
21
+ * Custom `dark` variant — `dark:` utilities must key off the `.dark`
22
+ * class on <html> (set by the THEME_INIT script in BaseLayout.astro),
23
+ * NOT off `prefers-color-scheme`. Tailwind v4.3.0 splits the selector
24
+ * argument on commas — including commas inside `:where(...)` — so
25
+ * `(&:where(.dark, .dark *))` is silently parsed as two separate
26
+ * (and broken) selectors and the variant never registers. Use two
27
+ * selectors joined by a space instead.
28
+ */
29
+ @custom-variant dark (&:where(.dark), &:where(.dark *));
20
30
  @source "./**/*.{astro,html,js,jsx,ts,tsx}";
21
31
 
22
32
  @theme {
23
- /* ── Neutral ink scale (V1 default) ───────────────────────────── */
24
- --color-ink-50: #fafafa;
25
- --color-ink-100: #f4f4f5;
26
- --color-ink-200: #e4e4e7;
27
- --color-ink-300: #d4d4d8;
28
- --color-ink-400: #a1a1aa;
29
- --color-ink-500: #71717a;
30
- --color-ink-600: #52525b;
31
- --color-ink-700: #3f3f46;
32
- --color-ink-800: #27272a;
33
- --color-ink-900: #18181b;
34
- --color-ink-950: #09090b;
33
+ /* ── Starlight-aligned neutral scale ──────────────────────────── */
34
+ --color-ink-50: oklch(98.5% 0 0);
35
+ --color-ink-100: oklch(97% 0 0);
36
+ --color-ink-200: oklch(92.2% 0 0);
37
+ --color-ink-300: oklch(87% 0 0);
38
+ --color-ink-400: oklch(70.8% 0 0);
39
+ --color-ink-500: oklch(55.6% 0 0);
40
+ --color-ink-600: oklch(43.9% 0 0);
41
+ --color-ink-700: oklch(37.1% 0 0);
42
+ --color-ink-800: oklch(26.9% 0 0);
43
+ --color-ink-900: oklch(20.5% 0 0);
44
+ --color-ink-950: oklch(14.5% 0 0);
45
+
46
+ /* ── Semantic colors ────────────────────────────────────────────
47
+ * These point at runtime variables instead of fixed palette steps so
48
+ * Tailwind utilities such as `bg-background` and `text-foreground`
49
+ * follow the resolved theme without requiring a `dark:` twin. The
50
+ * values below mirror packages/starlight/styles/{theme,base}.css.
51
+ */
52
+ --color-background: var(--grove-background);
53
+ --color-foreground: var(--grove-foreground);
54
+ --color-primary: var(--grove-primary);
55
+ --color-primary-foreground: var(--grove-primary-foreground);
56
+ --color-secondary: var(--grove-secondary);
57
+ --color-secondary-foreground: var(--grove-secondary-foreground);
58
+ --color-muted: var(--grove-muted);
59
+ --color-muted-foreground: var(--grove-muted-foreground);
60
+ --color-accent: var(--grove-accent);
61
+ --color-accent-foreground: var(--grove-accent-foreground);
62
+ --color-border: var(--grove-border);
63
+ --color-input: var(--grove-input);
64
+ --color-ring: var(--grove-ring);
65
+ --color-card: var(--grove-card);
66
+ --color-card-foreground: var(--grove-card-foreground);
67
+ --color-popover: var(--grove-popover);
68
+ --color-popover-foreground: var(--grove-popover-foreground);
35
69
 
36
70
  /* ── Accent (interactive states) ─────────────────────────────── */
37
- --color-accent: #1f6feb;
38
- --color-accent-muted: #388bfd1a;
71
+ --color-accent-muted: color-mix(in oklab, var(--grove-foreground) 8%, transparent);
39
72
 
40
73
  /* ── Status palettes (badges only) ───────────────────────────── */
41
- --color-emerald-50: #ecfdf5;
42
- --color-emerald-300: #6ee7b7;
43
- --color-emerald-400: #34d399;
44
- --color-emerald-700: #047857;
45
- --color-emerald-800: #065f46;
46
- --color-emerald-950: #022c22;
47
-
48
- --color-orange-50: #fff7ed;
49
- --color-orange-300: #fdba74;
50
- --color-orange-400: #fb923c;
51
- --color-orange-700: #c2410c;
52
- --color-orange-800: #9a3412;
53
- --color-orange-950: #431407;
54
-
55
- --color-blue-50: #eff6ff;
56
- --color-blue-300: #93c5fd;
57
- --color-blue-400: #60a5fa;
58
- --color-blue-700: #1d4ed8;
59
- --color-blue-800: #1e40af;
60
- --color-blue-950: #172554;
61
-
62
- --color-amber-50: #fffbeb;
63
- --color-amber-300: #fcd34d;
64
- --color-amber-400: #fbbf24;
65
- --color-amber-700: #b45309;
66
- --color-amber-800: #92400e;
67
- --color-amber-950: #451a03;
74
+ --color-emerald-50: oklch(98.2% 0.018 155.826);
75
+ --color-emerald-300: oklch(87.1% 0.15 154.449);
76
+ --color-emerald-400: oklch(79.2% 0.209 151.711);
77
+ --color-emerald-500: oklch(72.3% 0.219 149.579);
78
+ --color-emerald-600: oklch(52.7% 0.154 150.069);
79
+ --color-emerald-700: oklch(52.7% 0.154 150.069);
80
+ --color-emerald-800: oklch(26.6% 0.065 152.934);
81
+ --color-emerald-950: oklch(26.6% 0.065 152.934);
82
+
83
+ --color-orange-50: oklch(98% 0.016 73.684);
84
+ --color-orange-300: oklch(90.1% 0.076 70.697);
85
+ --color-orange-400: oklch(83.7% 0.128 66.29);
86
+ --color-orange-500: oklch(70.5% 0.213 47.604);
87
+ --color-orange-700: oklch(70.5% 0.213 47.604);
88
+ --color-orange-800: oklch(55.3% 0.195 38.402);
89
+ --color-orange-950: oklch(40.8% 0.123 38.172);
90
+
91
+ --color-blue-50: oklch(97% 0.014 254.604);
92
+ --color-blue-300: oklch(82.8% 0.111 230.318);
93
+ --color-blue-400: oklch(70.7% 0.165 254.624);
94
+ --color-blue-700: oklch(48.8% 0.243 264.376);
95
+ --color-blue-800: oklch(29.3% 0.066 243.157);
96
+ --color-blue-950: oklch(29.3% 0.066 243.157);
97
+
98
+ --color-amber-50: oklch(98% 0.016 73.684);
99
+ --color-amber-300: oklch(90.1% 0.076 70.697);
100
+ --color-amber-400: oklch(83.7% 0.128 66.29);
101
+ --color-amber-500: oklch(83.7% 0.128 66.29);
102
+ --color-amber-600: oklch(70.5% 0.213 47.604);
103
+ --color-amber-700: oklch(70.5% 0.213 47.604);
104
+ --color-amber-800: oklch(55.3% 0.195 38.402);
105
+ --color-amber-900: oklch(40.8% 0.123 38.172);
106
+ --color-amber-950: oklch(40.8% 0.123 38.172);
68
107
 
69
108
  /* ── Font stacks ─────────────────────────────────────────────── */
70
- --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
71
- "Helvetica Neue", Arial, sans-serif;
109
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
110
+ "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji",
111
+ "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
72
112
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
73
113
  "Liberation Mono", "Courier New", monospace;
74
114
 
115
+ /* ── Starlight typography weight/rhythm ──────────────────────── */
116
+ --font-weight-semibold: 500;
117
+ --font-weight-bold: 600;
118
+ --leading-relaxed: 1.75;
119
+
75
120
  /* ── Font size extensions (2xs) ──────────────────────────────── */
76
121
  --text-2xs: 0.6875rem;
77
122
  --text-2xs--line-height: 1rem;
@@ -139,7 +184,61 @@
139
184
 
140
185
  @layer base {
141
186
  :root {
142
- color-scheme: light dark;
187
+ color-scheme: light;
188
+
189
+ /* Starlight light theme, expressed as Grove's public semantic API. */
190
+ --grove-background: oklch(100% 0 0);
191
+ --grove-foreground: var(--color-ink-950);
192
+ --grove-gray-1: var(--color-ink-900);
193
+ --grove-gray-2: var(--color-ink-800);
194
+ --grove-gray-3: var(--color-ink-700);
195
+ --grove-gray-4: var(--color-ink-500);
196
+ --grove-gray-5: var(--color-ink-400);
197
+ --grove-gray-6: var(--color-ink-300);
198
+ --grove-gray-7: var(--color-ink-100);
199
+ --grove-primary: var(--grove-foreground);
200
+ --grove-primary-foreground: var(--grove-gray-7);
201
+ --grove-secondary: var(--grove-gray-7);
202
+ --grove-secondary-foreground: var(--grove-foreground);
203
+ --grove-muted: var(--grove-gray-7);
204
+ --grove-muted-foreground: var(--grove-gray-4);
205
+ --grove-accent: var(--grove-gray-7);
206
+ --grove-accent-foreground: var(--grove-foreground);
207
+ --grove-border: var(--grove-gray-6);
208
+ --grove-input: var(--grove-gray-6);
209
+ --grove-ring: var(--grove-gray-4);
210
+ --grove-card: var(--grove-background);
211
+ --grove-card-foreground: var(--grove-foreground);
212
+ --grove-popover: var(--grove-background);
213
+ --grove-popover-foreground: var(--grove-foreground);
214
+ }
215
+
216
+ :root.dark {
217
+ color-scheme: dark;
218
+ --grove-background: var(--color-ink-950);
219
+ --grove-foreground: var(--color-ink-50);
220
+ --grove-gray-1: var(--color-ink-50);
221
+ --grove-gray-2: var(--color-ink-300);
222
+ --grove-gray-3: var(--color-ink-400);
223
+ --grove-gray-4: var(--color-ink-500);
224
+ --grove-gray-5: var(--color-ink-700);
225
+ --grove-gray-6: var(--color-ink-800);
226
+ --grove-gray-7: var(--color-ink-900);
227
+ --grove-primary: var(--grove-foreground);
228
+ --grove-primary-foreground: var(--grove-gray-7);
229
+ --grove-secondary: var(--grove-gray-6);
230
+ --grove-secondary-foreground: var(--grove-foreground);
231
+ --grove-muted: var(--grove-gray-6);
232
+ --grove-muted-foreground: var(--grove-gray-3);
233
+ --grove-accent: var(--grove-gray-5);
234
+ --grove-accent-foreground: var(--grove-foreground);
235
+ --grove-border: var(--grove-gray-6);
236
+ --grove-input: var(--grove-gray-5);
237
+ --grove-ring: var(--grove-gray-4);
238
+ --grove-card: var(--grove-background);
239
+ --grove-card-foreground: var(--grove-foreground);
240
+ --grove-popover: var(--grove-gray-7);
241
+ --grove-popover-foreground: var(--grove-foreground);
143
242
  }
144
243
 
145
244
  html {
@@ -150,16 +249,11 @@
150
249
 
151
250
  body {
152
251
  margin: 0;
153
- background: #ffffff;
154
- color: var(--color-ink-900);
252
+ background: var(--grove-background);
253
+ color: var(--grove-foreground);
155
254
  font-family: var(--font-sans);
156
255
  font-size: 15px;
157
- line-height: 1.55;
158
- }
159
-
160
- .dark body {
161
- background: var(--color-ink-950);
162
- color: var(--color-ink-100);
256
+ line-height: 1.75;
163
257
  }
164
258
 
165
259
  a {
@@ -169,30 +263,23 @@
169
263
 
170
264
  /* Subtle selection */
171
265
  ::selection {
172
- background: var(--color-ink-200);
173
- color: var(--color-ink-900);
174
- }
175
- .dark ::selection {
176
- background: var(--color-ink-800);
177
- color: var(--color-ink-100);
266
+ background: var(--grove-muted);
267
+ color: var(--grove-foreground);
178
268
  }
179
269
 
180
270
  /* Slim, neutral scrollbar */
181
271
  * {
182
272
  scrollbar-width: thin;
183
- scrollbar-color: var(--color-ink-300) transparent;
273
+ scrollbar-color: var(--grove-border) transparent;
184
274
  }
185
275
  *::-webkit-scrollbar {
186
276
  width: 8px;
187
277
  height: 8px;
188
278
  }
189
279
  *::-webkit-scrollbar-thumb {
190
- background: var(--color-ink-200);
280
+ background: var(--grove-border);
191
281
  border-radius: 4px;
192
282
  }
193
- .dark *::-webkit-scrollbar-thumb {
194
- background: var(--color-ink-800);
195
- }
196
283
 
197
284
  input,
198
285
  select,
@@ -207,26 +294,47 @@
207
294
  * ────────────────────────────────────────────────────────────────── */
208
295
  .grove-prose {
209
296
  max-width: 72ch;
210
- color: var(--color-ink-700);
211
- font-size: 0.9375rem;
297
+ color: var(--color-ink-800);
298
+ font-size: 15px;
212
299
  line-height: 1.75;
213
300
  }
214
301
 
215
302
  .dark .grove-prose {
216
- color: var(--color-ink-200);
303
+ color: var(--color-ink-300);
217
304
  }
218
305
 
219
306
  .grove-prose :where(h1, h2, h3, h4) {
220
307
  margin: 1.75em 0 0.65em;
221
308
  color: var(--color-ink-900);
222
- font-weight: 650;
223
- line-height: 1.25;
309
+ font-weight: 500;
310
+ letter-spacing: -0.025em;
224
311
  }
225
312
 
226
313
  .dark .grove-prose :where(h1, h2, h3, h4) {
227
314
  color: var(--color-ink-100);
228
315
  }
229
316
 
317
+ .grove-prose h1 {
318
+ font-size: 1.875rem;
319
+ font-weight: 600;
320
+ line-height: 2.25rem;
321
+ }
322
+
323
+ .grove-prose h2 {
324
+ font-size: 1.25rem;
325
+ line-height: 1.75rem;
326
+ }
327
+
328
+ .grove-prose h3 {
329
+ font-size: 1.125rem;
330
+ line-height: 1.75rem;
331
+ }
332
+
333
+ .grove-prose h4 {
334
+ font-size: 1rem;
335
+ line-height: 1.5rem;
336
+ }
337
+
230
338
  .grove-prose :where(p, ul, ol, pre, blockquote) {
231
339
  margin: 1em 0;
232
340
  }
@@ -236,9 +344,16 @@
236
344
  }
237
345
 
238
346
  .grove-prose a {
239
- color: var(--color-accent);
347
+ color: var(--color-ink-900);
348
+ font-weight: 500;
240
349
  text-decoration: underline;
241
350
  text-underline-offset: 0.18em;
351
+ text-decoration-color: var(--color-ink-500);
352
+ }
353
+
354
+ .dark .grove-prose a {
355
+ color: var(--color-ink-50);
356
+ text-decoration-color: var(--color-ink-400);
242
357
  }
243
358
 
244
359
  .grove-prose :where(code, pre) {
@@ -0,0 +1,52 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+
5
+ const astroTheme = readFileSync(resolve(import.meta.dirname, "styles.css"), "utf8");
6
+ const scaffoldTheme = readFileSync(
7
+ resolve(import.meta.dirname, "../templates/default/src/styles/global.css"),
8
+ "utf8",
9
+ );
10
+
11
+ describe("Astro theme contract", () => {
12
+ it("keeps Starlight's light and dark foundation values", () => {
13
+ expect(astroTheme).toContain("--grove-background: oklch(100% 0 0)");
14
+ expect(astroTheme).toContain("--grove-foreground: var(--color-ink-950)");
15
+ expect(astroTheme).toContain("--grove-background: var(--color-ink-950)");
16
+ expect(astroTheme).toContain("--grove-foreground: var(--color-ink-50)");
17
+ expect(astroTheme).toContain("--color-ink-50: oklch(98.5% 0 0)");
18
+ expect(astroTheme).toContain("--color-ink-950: oklch(14.5% 0 0)");
19
+ });
20
+
21
+ it("exposes runtime semantic colors to Tailwind", () => {
22
+ for (const token of [
23
+ "background",
24
+ "foreground",
25
+ "primary",
26
+ "secondary",
27
+ "muted",
28
+ "accent",
29
+ "border",
30
+ "card",
31
+ "popover",
32
+ ]) {
33
+ expect(astroTheme).toContain(`--color-${token}: var(--grove-${token})`);
34
+ }
35
+ });
36
+
37
+ it("uses the same system and mono font stacks as the docs", () => {
38
+ expect(astroTheme).toContain(
39
+ '--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,',
40
+ );
41
+ expect(astroTheme).toContain(
42
+ "--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,",
43
+ );
44
+ expect(astroTheme).toContain("font-family: var(--font-sans)");
45
+ });
46
+
47
+ it("does not duplicate package-owned foundation tokens in the scaffold", () => {
48
+ expect(scaffoldTheme).not.toContain("--color-ink-");
49
+ expect(scaffoldTheme).not.toContain("font-family:");
50
+ expect(scaffoldTheme).not.toContain("color-scheme:");
51
+ });
52
+ });
@@ -25,12 +25,13 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@astrojs/check": "^0.9.9",
28
- "@grove-dev/astro": "0.2.16",
28
+ "@grove-dev/astro": "0.2.18",
29
29
  "@grove-dev/cli": "0.2.16",
30
30
  "@grove-dev/core": "0.2.16",
31
31
  "@tailwindcss/vite": "^4.3.0",
32
32
  "astro": "^6.4.4",
33
33
  "marked": "^18.0.0",
34
+ "sanitize-html": "^2.17.5",
34
35
  "tailwindcss": "^4.3.0",
35
36
  "yaml": "^2.9.0"
36
37
  }
@@ -1,6 +1,6 @@
1
1
  # example.com — full directory
2
2
 
3
- Generated: 2026-06-15T13:49:39.319Z
3
+ Generated: 2026-06-22T14:50:54.564Z
4
4
  Records: 8
5
5
 
6
6
  ---
@@ -2,61 +2,61 @@
2
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
3
  <url>
4
4
  <loc>https://example.com/</loc>
5
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
5
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
6
6
  <changefreq>daily</changefreq>
7
7
  <priority>1.0</priority>
8
8
  </url>
9
9
  <url>
10
10
  <loc>https://example.com/projects</loc>
11
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
11
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
12
12
  <changefreq>daily</changefreq>
13
13
  <priority>0.9</priority>
14
14
  </url>
15
15
  <url>
16
16
  <loc>https://example.com/projects/coolify</loc>
17
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
17
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
18
18
  <changefreq>weekly</changefreq>
19
19
  <priority>0.7</priority>
20
20
  </url>
21
21
  <url>
22
22
  <loc>https://example.com/projects/gitea</loc>
23
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
23
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
24
24
  <changefreq>weekly</changefreq>
25
25
  <priority>0.7</priority>
26
26
  </url>
27
27
  <url>
28
28
  <loc>https://example.com/projects/hoppscotch</loc>
29
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
29
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
30
30
  <changefreq>weekly</changefreq>
31
31
  <priority>0.7</priority>
32
32
  </url>
33
33
  <url>
34
34
  <loc>https://example.com/projects/inventree</loc>
35
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
35
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
36
36
  <changefreq>weekly</changefreq>
37
37
  <priority>0.7</priority>
38
38
  </url>
39
39
  <url>
40
40
  <loc>https://example.com/projects/label-studio</loc>
41
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
41
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
42
42
  <changefreq>weekly</changefreq>
43
43
  <priority>0.7</priority>
44
44
  </url>
45
45
  <url>
46
46
  <loc>https://example.com/projects/logseq</loc>
47
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
47
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
48
48
  <changefreq>weekly</changefreq>
49
49
  <priority>0.7</priority>
50
50
  </url>
51
51
  <url>
52
52
  <loc>https://example.com/projects/private-gpt</loc>
53
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
53
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
54
54
  <changefreq>weekly</changefreq>
55
55
  <priority>0.7</priority>
56
56
  </url>
57
57
  <url>
58
58
  <loc>https://example.com/projects/wakapi</loc>
59
- <lastmod>2026-06-15T13:49:38.754Z</lastmod>
59
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
60
60
  <changefreq>weekly</changefreq>
61
61
  <priority>0.7</priority>
62
62
  </url>