@grove-dev/astro 0.2.18 → 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.
package/dist/index.d.ts CHANGED
@@ -2,4 +2,5 @@ import type { AstroIntegration } from "astro";
2
2
  export * from "@grove-dev/core";
3
3
  export * from "./lib/index.js";
4
4
  export default function groveAstro(): AstroIntegration;
5
+ export declare const CONSUMER_GLOBAL_CSS_ID = "virtual:grove-consumer-global-css";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAE9C,cAAc,iBAAiB,CAAC;AAMhC,cAAc,gBAAgB,CAAC;AAW/B,MAAM,CAAC,OAAO,UAAU,UAAU,IAAI,gBAAgB,CAuBrD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAE9C,cAAc,iBAAiB,CAAC;AAMhC,cAAc,gBAAgB,CAAC;AAkB/B,MAAM,CAAC,OAAO,UAAU,UAAU,IAAI,gBAAgB,CAuDrD;AAID,eAAO,MAAM,sBAAsB,sCAAiC,CAAC"}
package/dist/index.js CHANGED
@@ -16,7 +16,15 @@
16
16
  * - Re-exports the generic `lib/` helpers (search, lenses, scores,
17
17
  * repo, format, display, taxonomy-counts) under a single import
18
18
  * path so consumers can `import { ... } from "@grove-dev/astro"`.
19
+ *
20
+ * Also auto-loads the consumer's `src/styles/global.css` when it
21
+ * exists. Without this hook the scaffold's global.css (which the
22
+ * CLI generates for theme overrides) sits on disk unused, and any
23
+ * custom theme tokens silently degrade to the package defaults —
24
+ * which is the classic "I changed the brand color in global.css
25
+ * and nothing happened" surprise.
19
26
  */
27
+ import { existsSync } from "node:fs";
20
28
  import { fileURLToPath } from "node:url";
21
29
  import { dirname, resolve } from "node:path";
22
30
  export * from "@grove-dev/core";
@@ -33,16 +41,24 @@ const here = dirname(fileURLToPath(import.meta.url));
33
41
  const srcRoot = resolve(here, "..", "src");
34
42
  const componentsDir = resolve(srcRoot, "components");
35
43
  const layoutsDir = resolve(srcRoot, "layouts");
44
+ // Virtual module id used to inject the consumer's `src/styles/global.css`.
45
+ // Resolved by the inline Vite plugin below to the absolute file path
46
+ // when the file exists, or to an empty module when it doesn't.
47
+ const CONSUMER_GLOBAL_CSS_VIRTUAL_ID = "virtual:grove-consumer-global-css";
48
+ const CONSUMER_GLOBAL_CSS_EMPTY_ID = "\0virtual:grove-consumer-global-css:empty";
36
49
  export default function groveAstro() {
37
50
  return {
38
51
  name: "@grove-dev/astro",
39
52
  hooks: {
40
- "astro:config:setup": ({ updateConfig }) => {
53
+ "astro:config:setup": ({ config, updateConfig, injectScript }) => {
41
54
  // Alias the components/layouts source directories so
42
55
  // consumer builds can import from
43
56
  // @grove-dev/astro/components/ItemCard.astro
44
57
  // without us shipping a glob-shaped `exports` map that
45
58
  // Vite/Rollup does not expand reliably.
59
+ const consumerRoot = fileURLToPath(config.root);
60
+ const globalCssPath = resolve(consumerRoot, "src/styles/global.css");
61
+ const globalCssExists = existsSync(globalCssPath);
46
62
  updateConfig({
47
63
  vite: {
48
64
  resolve: {
@@ -51,10 +67,37 @@ export default function groveAstro() {
51
67
  "@grove-dev/astro/layouts": layoutsDir,
52
68
  },
53
69
  },
70
+ plugins: [
71
+ {
72
+ name: "grove:consumer-global-css-resolver",
73
+ resolveId(id) {
74
+ if (id === CONSUMER_GLOBAL_CSS_VIRTUAL_ID) {
75
+ return globalCssExists
76
+ ? globalCssPath
77
+ : CONSUMER_GLOBAL_CSS_EMPTY_ID;
78
+ }
79
+ return null;
80
+ },
81
+ load(id) {
82
+ if (id === CONSUMER_GLOBAL_CSS_EMPTY_ID) {
83
+ return "";
84
+ }
85
+ return null;
86
+ },
87
+ },
88
+ ],
54
89
  },
55
90
  });
91
+ // Always inject the virtual module so the build is symmetric
92
+ // regardless of whether the consumer has a global.css yet.
93
+ // Vite's CSS pipeline (with @tailwindcss/vite) handles the
94
+ // resulting import as a stylesheet — no JS bundle weight.
95
+ injectScript("page-ssr", `import "${CONSUMER_GLOBAL_CSS_VIRTUAL_ID}";`);
56
96
  },
57
97
  },
58
98
  };
59
99
  }
100
+ // Re-export the virtual module id so consumers (and tests) can
101
+ // reason about it without copy-pasting the literal string.
102
+ export const CONSUMER_GLOBAL_CSS_ID = CONSUMER_GLOBAL_CSS_VIRTUAL_ID;
60
103
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG7C,cAAc,iBAAiB,CAAC;AAEhC,+DAA+D;AAC/D,mEAAmE;AACnE,iEAAiE;AACjE,mCAAmC;AACnC,cAAc,gBAAgB,CAAC;AAE/B,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACrD,+DAA+D;AAC/D,0DAA0D;AAC1D,8DAA8D;AAC9D,sCAAsC;AACtC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AAC3C,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AACrD,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AAE/C,MAAM,CAAC,OAAO,UAAU,UAAU;IAChC,OAAO;QACL,IAAI,EAAE,kBAAkB;QACxB,KAAK,EAAE;YACL,oBAAoB,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE;gBACzC,qDAAqD;gBACrD,kCAAkC;gBAClC,+CAA+C;gBAC/C,uDAAuD;gBACvD,wCAAwC;gBACxC,YAAY,CAAC;oBACX,IAAI,EAAE;wBACJ,OAAO,EAAE;4BACP,KAAK,EAAE;gCACL,6BAA6B,EAAE,aAAa;gCAC5C,0BAA0B,EAAE,UAAU;6BACvC;yBACF;qBACF;iBACF,CAAC,CAAC;YACL,CAAC;SACF;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAG7C,cAAc,iBAAiB,CAAC;AAEhC,+DAA+D;AAC/D,mEAAmE;AACnE,iEAAiE;AACjE,mCAAmC;AACnC,cAAc,gBAAgB,CAAC;AAE/B,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACrD,+DAA+D;AAC/D,0DAA0D;AAC1D,8DAA8D;AAC9D,sCAAsC;AACtC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AAC3C,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AACrD,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AAE/C,2EAA2E;AAC3E,qEAAqE;AACrE,+DAA+D;AAC/D,MAAM,8BAA8B,GAAG,mCAAmC,CAAC;AAC3E,MAAM,4BAA4B,GAChC,2CAA2C,CAAC;AAE9C,MAAM,CAAC,OAAO,UAAU,UAAU;IAChC,OAAO;QACL,IAAI,EAAE,kBAAkB;QACxB,KAAK,EAAE;YACL,oBAAoB,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,EAAE;gBAC/D,qDAAqD;gBACrD,kCAAkC;gBAClC,+CAA+C;gBAC/C,uDAAuD;gBACvD,wCAAwC;gBACxC,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBAChD,MAAM,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,uBAAuB,CAAC,CAAC;gBACrE,MAAM,eAAe,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;gBAElD,YAAY,CAAC;oBACX,IAAI,EAAE;wBACJ,OAAO,EAAE;4BACP,KAAK,EAAE;gCACL,6BAA6B,EAAE,aAAa;gCAC5C,0BAA0B,EAAE,UAAU;6BACvC;yBACF;wBACD,OAAO,EAAE;4BACP;gCACE,IAAI,EAAE,oCAAoC;gCAC1C,SAAS,CAAC,EAAE;oCACV,IAAI,EAAE,KAAK,8BAA8B,EAAE,CAAC;wCAC1C,OAAO,eAAe;4CACpB,CAAC,CAAC,aAAa;4CACf,CAAC,CAAC,4BAA4B,CAAC;oCACnC,CAAC;oCACD,OAAO,IAAI,CAAC;gCACd,CAAC;gCACD,IAAI,CAAC,EAAE;oCACL,IAAI,EAAE,KAAK,4BAA4B,EAAE,CAAC;wCACxC,OAAO,EAAE,CAAC;oCACZ,CAAC;oCACD,OAAO,IAAI,CAAC;gCACd,CAAC;6BACF;yBACF;qBACF;iBACF,CAAC,CAAC;gBAEH,6DAA6D;gBAC7D,2DAA2D;gBAC3D,2DAA2D;gBAC3D,0DAA0D;gBAC1D,YAAY,CACV,UAAU,EACV,WAAW,8BAA8B,IAAI,CAC9C,CAAC;YACJ,CAAC;SACF;KACF,CAAC;AACJ,CAAC;AAED,+DAA+D;AAC/D,2DAA2D;AAC3D,MAAM,CAAC,MAAM,sBAAsB,GAAG,8BAA8B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=theme.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.test.d.ts","sourceRoot":"","sources":["../src/theme.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ const astroTheme = readFileSync(resolve(import.meta.dirname, "styles.css"), "utf8");
5
+ const scaffoldTheme = readFileSync(resolve(import.meta.dirname, "../templates/default/src/styles/global.css"), "utf8");
6
+ describe("Astro theme contract", () => {
7
+ it("keeps Starlight's light and dark foundation values", () => {
8
+ expect(astroTheme).toContain("--grove-background: oklch(100% 0 0)");
9
+ expect(astroTheme).toContain("--grove-foreground: var(--color-ink-950)");
10
+ expect(astroTheme).toContain("--grove-background: var(--color-ink-950)");
11
+ expect(astroTheme).toContain("--grove-foreground: var(--color-ink-50)");
12
+ expect(astroTheme).toContain("--color-ink-50: oklch(98.5% 0 0)");
13
+ expect(astroTheme).toContain("--color-ink-950: oklch(14.5% 0 0)");
14
+ });
15
+ it("exposes runtime semantic colors to Tailwind", () => {
16
+ for (const token of [
17
+ "background",
18
+ "foreground",
19
+ "primary",
20
+ "secondary",
21
+ "muted",
22
+ "accent",
23
+ "border",
24
+ "card",
25
+ "popover",
26
+ ]) {
27
+ expect(astroTheme).toContain(`--color-${token}: var(--grove-${token})`);
28
+ }
29
+ });
30
+ it("uses the same system and mono font stacks as the docs", () => {
31
+ expect(astroTheme).toContain('--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,');
32
+ expect(astroTheme).toContain("--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,");
33
+ expect(astroTheme).toContain("font-family: var(--font-sans)");
34
+ });
35
+ it("does not duplicate package-owned foundation tokens in the scaffold", () => {
36
+ expect(scaffoldTheme).not.toContain("--color-ink-");
37
+ expect(scaffoldTheme).not.toContain("font-family:");
38
+ expect(scaffoldTheme).not.toContain("color-scheme:");
39
+ });
40
+ });
41
+ //# sourceMappingURL=theme.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.test.js","sourceRoot":"","sources":["../src/theme.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,MAAM,UAAU,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAC;AACpF,MAAM,aAAa,GAAG,YAAY,CAChC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,4CAA4C,CAAC,EAC1E,MAAM,CACP,CAAC;AAEF,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,qCAAqC,CAAC,CAAC;QACpE,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,0CAA0C,CAAC,CAAC;QACzE,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,0CAA0C,CAAC,CAAC;QACzE,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,yCAAyC,CAAC,CAAC;QACxE,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,kCAAkC,CAAC,CAAC;QACjE,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,mCAAmC,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,KAAK,MAAM,KAAK,IAAI;YAClB,YAAY;YACZ,YAAY;YACZ,SAAS;YACT,WAAW;YACX,OAAO;YACP,QAAQ;YACR,QAAQ;YACR,MAAM;YACN,SAAS;SACV,EAAE,CAAC;YACF,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,WAAW,KAAK,iBAAiB,KAAK,GAAG,CAAC,CAAC;QAC1E,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAC1B,qEAAqE,CACtE,CAAC;QACF,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAC1B,qEAAqE,CACtE,CAAC;QACF,MAAM,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACpD,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;QACpD,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grove-dev/astro",
3
- "version": "0.2.18",
3
+ "version": "0.2.19",
4
4
  "type": "module",
5
5
  "description": "Astro framework adapter for Grove. Components, layouts, design tokens, and a default template.",
6
6
  "license": "MIT",
@@ -114,9 +114,9 @@ const trustStats: { value: string; label: string }[] = [
114
114
  class="pointer-events-none absolute inset-0 opacity-60 dark:opacity-40"
115
115
  style="
116
116
  background-image:
117
- radial-gradient(60% 50% at 15% 20%, rgba(31, 111, 235, 0.12), transparent 60%),
118
- radial-gradient(50% 40% at 85% 10%, rgba(110, 231, 183, 0.10), transparent 60%),
119
- radial-gradient(40% 40% at 50% 90%, rgba(251, 146, 60, 0.08), transparent 60%);
117
+ radial-gradient(60% 50% at 15% 20%, color-mix(in oklab, var(--color-ink-300) 34%, transparent), transparent 60%),
118
+ radial-gradient(50% 40% at 85% 10%, color-mix(in oklab, var(--color-ink-200) 28%, transparent), transparent 60%),
119
+ radial-gradient(40% 40% at 50% 90%, color-mix(in oklab, var(--color-ink-400) 18%, transparent), transparent 60%);
120
120
  "
121
121
  ></div>
122
122
 
@@ -185,16 +185,16 @@ const a11y: Record<string, string> = title
185
185
  align-items: center;
186
186
  justify-content: center;
187
187
  border-radius: 0.25rem;
188
- background: var(--color-ink-100, #f4f4f5);
189
- color: var(--color-ink-700, #3f3f46);
188
+ background: var(--color-ink-100, oklch(97% 0 0));
189
+ color: var(--color-ink-700, oklch(37.1% 0 0));
190
190
  font-weight: 600;
191
191
  line-height: 1;
192
192
  text-transform: uppercase;
193
193
  user-select: none;
194
194
  }
195
195
  :global(.dark) .grove-icon-fallback {
196
- background: var(--color-ink-800, #27272a);
197
- color: var(--color-ink-200, #e4e4e7);
196
+ background: var(--color-ink-800, oklch(26.9% 0 0));
197
+ color: var(--color-ink-200, oklch(92.2% 0 0));
198
198
  }
199
199
  </style>
200
200
 
package/src/index.ts CHANGED
@@ -16,7 +16,15 @@
16
16
  * - Re-exports the generic `lib/` helpers (search, lenses, scores,
17
17
  * repo, format, display, taxonomy-counts) under a single import
18
18
  * path so consumers can `import { ... } from "@grove-dev/astro"`.
19
+ *
20
+ * Also auto-loads the consumer's `src/styles/global.css` when it
21
+ * exists. Without this hook the scaffold's global.css (which the
22
+ * CLI generates for theme overrides) sits on disk unused, and any
23
+ * custom theme tokens silently degrade to the package defaults —
24
+ * which is the classic "I changed the brand color in global.css
25
+ * and nothing happened" surprise.
19
26
  */
27
+ import { existsSync } from "node:fs";
20
28
  import { fileURLToPath } from "node:url";
21
29
  import { dirname, resolve } from "node:path";
22
30
  import type { AstroIntegration } from "astro";
@@ -38,16 +46,27 @@ const srcRoot = resolve(here, "..", "src");
38
46
  const componentsDir = resolve(srcRoot, "components");
39
47
  const layoutsDir = resolve(srcRoot, "layouts");
40
48
 
49
+ // Virtual module id used to inject the consumer's `src/styles/global.css`.
50
+ // Resolved by the inline Vite plugin below to the absolute file path
51
+ // when the file exists, or to an empty module when it doesn't.
52
+ const CONSUMER_GLOBAL_CSS_VIRTUAL_ID = "virtual:grove-consumer-global-css";
53
+ const CONSUMER_GLOBAL_CSS_EMPTY_ID =
54
+ "\0virtual:grove-consumer-global-css:empty";
55
+
41
56
  export default function groveAstro(): AstroIntegration {
42
57
  return {
43
58
  name: "@grove-dev/astro",
44
59
  hooks: {
45
- "astro:config:setup": ({ updateConfig }) => {
60
+ "astro:config:setup": ({ config, updateConfig, injectScript }) => {
46
61
  // Alias the components/layouts source directories so
47
62
  // consumer builds can import from
48
63
  // @grove-dev/astro/components/ItemCard.astro
49
64
  // without us shipping a glob-shaped `exports` map that
50
65
  // Vite/Rollup does not expand reliably.
66
+ const consumerRoot = fileURLToPath(config.root);
67
+ const globalCssPath = resolve(consumerRoot, "src/styles/global.css");
68
+ const globalCssExists = existsSync(globalCssPath);
69
+
51
70
  updateConfig({
52
71
  vite: {
53
72
  resolve: {
@@ -56,9 +75,41 @@ export default function groveAstro(): AstroIntegration {
56
75
  "@grove-dev/astro/layouts": layoutsDir,
57
76
  },
58
77
  },
78
+ plugins: [
79
+ {
80
+ name: "grove:consumer-global-css-resolver",
81
+ resolveId(id) {
82
+ if (id === CONSUMER_GLOBAL_CSS_VIRTUAL_ID) {
83
+ return globalCssExists
84
+ ? globalCssPath
85
+ : CONSUMER_GLOBAL_CSS_EMPTY_ID;
86
+ }
87
+ return null;
88
+ },
89
+ load(id) {
90
+ if (id === CONSUMER_GLOBAL_CSS_EMPTY_ID) {
91
+ return "";
92
+ }
93
+ return null;
94
+ },
95
+ },
96
+ ],
59
97
  },
60
98
  });
99
+
100
+ // Always inject the virtual module so the build is symmetric
101
+ // regardless of whether the consumer has a global.css yet.
102
+ // Vite's CSS pipeline (with @tailwindcss/vite) handles the
103
+ // resulting import as a stylesheet — no JS bundle weight.
104
+ injectScript(
105
+ "page-ssr",
106
+ `import "${CONSUMER_GLOBAL_CSS_VIRTUAL_ID}";`,
107
+ );
61
108
  },
62
109
  },
63
110
  };
64
111
  }
112
+
113
+ // Re-export the virtual module id so consumers (and tests) can
114
+ // reason about it without copy-pasting the literal string.
115
+ export const CONSUMER_GLOBAL_CSS_ID = CONSUMER_GLOBAL_CSS_VIRTUAL_ID;
@@ -186,7 +186,7 @@ const favicon =
186
186
  })();
187
187
  </script>
188
188
  </head>
189
- <body class="min-h-screen bg-white text-ink-900 dark:bg-ink-950 dark:text-ink-100">
189
+ <body class="min-h-screen bg-background text-foreground">
190
190
  <slot name="header">
191
191
  <Header
192
192
  site={site}
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,7 +25,7 @@
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",
@@ -1,6 +1,6 @@
1
1
  # example.com — full directory
2
2
 
3
- Generated: 2026-06-17T13:40:59.793Z
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-17T13:40:59.064Z</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-17T13:40:59.064Z</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-17T13:40:59.064Z</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-17T13:40:59.064Z</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-17T13:40:59.064Z</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-17T13:40:59.064Z</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-17T13:40:59.064Z</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-17T13:40:59.064Z</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-17T13:40:59.064Z</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-17T13:40:59.064Z</lastmod>
59
+ <lastmod>2026-06-22T14:50:53.941Z</lastmod>
60
60
  <changefreq>weekly</changefreq>
61
61
  <priority>0.7</priority>
62
62
  </url>
@@ -7,7 +7,6 @@
7
7
  * - RefinePanel (Stack / Platform / Category / License / Sort)
8
8
  * - RecordSection rows (full record cards; the `ItemSection` V0
9
9
  * name was renamed to `RecordSection` in V1)
10
- * - ExploreByCategory / ExploreByStack (compact pill lists)
11
10
  *
12
11
  * Generic: the URL slug (e.g. `/projects/`, `/resources/`,
13
12
  * `/entities/`) is derived from the blueprint via `getStaticPaths`,
@@ -28,14 +27,10 @@ import SmartLensTabs from "@grove-dev/astro/components/SmartLensTabs.astro";
28
27
  import RefinePanel from "@grove-dev/astro/components/RefinePanel.astro";
29
28
  import IndexRow from "@grove-dev/astro/components/IndexRow.astro";
30
29
  import Pagination from "@grove-dev/astro/components/Pagination.astro";
31
- import ExploreByCategory from "@grove-dev/astro/components/ExploreByCategory.astro";
32
- import ExploreByStack from "@grove-dev/astro/components/ExploreByStack.astro";
33
30
  import {
34
31
  activeFilterChips,
35
32
  applySort,
36
33
  buildFacets,
37
- countByCategory,
38
- countByStack,
39
34
  effectivePage,
40
35
  effectiveSort,
41
36
  filterRecords,
@@ -70,16 +65,6 @@ const chips = activeFilterChips(filters);
70
65
  const title = `Browse ${slug} — ${siteConfig.name}`;
71
66
  const description = `A searchable directory of ${itemLabelPlural()} for ${siteConfig.name}.`;
72
67
 
73
- // Compute the top categories and stacks for the explore pills
74
- const categoryEntries = [...countByCategory(items as Parameters<typeof countByCategory>[0]).entries()]
75
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
76
- .slice(0, 12)
77
- .map(([name, count]) => ({ name, slug: name, count }));
78
- const stackEntries = [...countByStack(items as Parameters<typeof countByStack>[0]).entries()]
79
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
80
- .slice(0, 18)
81
- .map(([name, count]) => ({ name, slug: name, count }));
82
-
83
68
  function hrefForPage(target: number): string {
84
69
  const next: IndexFilters = { ...filters, page: target };
85
70
  const query = searchParamsFromFilters(next).toString();
@@ -207,20 +192,6 @@ function hrefForRemove(key: keyof IndexFilters, value: string): string {
207
192
  </div>
208
193
  </section>
209
194
 
210
- {categoryEntries.length > 0 && (
211
- <ExploreByCategory
212
- categories={categoryEntries}
213
- pathPrefix={`/${slug}`}
214
- />
215
- )}
216
-
217
- {stackEntries.length > 0 && (
218
- <ExploreByStack
219
- stacks={stackEntries}
220
- pathPrefix={`/${slug}`}
221
- />
222
- )}
223
-
224
195
  <section class="grove-section-tight">
225
196
  <div class="grove-container-wide mx-auto px-5 sm:px-7">
226
197
  <header class="mb-6">
@@ -5,26 +5,22 @@
5
5
  * at scaffold time; we expose the same names as CSS variables so
6
6
  * consumers can layer their own brand colors in without forking.
7
7
  *
8
- * The `ink-*` color scale is preserved from the V0 template because
9
- * the page-level classes (`text-ink-900`, `bg-ink-50`, ...) still
10
- * reference it. New pages should prefer the default Tailwind
11
- * `gray-*` / `slate-*` palettes; the ink scale exists for
12
- * back-compat with the V1 minimal template.
8
+ * The package owns the Starlight-aligned palette, semantic theme tokens,
9
+ * font stacks, and light/dark base styles. This consumer sheet contains
10
+ * only scaffold-level overrides so those foundations cannot drift when
11
+ * the package theme changes.
12
+ *
13
+ * NOTE: do NOT add `@custom-variant dark` here. `@grove-dev/astro`'s
14
+ * `styles.css` already declares the `.dark`-class variant, and
15
+ * Tailwind v4 only honors one definition per build — adding a
16
+ * second one in this consumer file has been observed to confuse
17
+ * the variant resolution and produce utilities that key off
18
+ * `prefers-color-scheme` instead of the `.dark` class on <html>.
19
+ * The net effect: `<html class="dark">` is set but the page stays
20
+ * in light mode because the `dark:` utilities never match.
13
21
  */
14
22
  @theme {
15
- --color-ink-50: #fafafa;
16
- --color-ink-100: #f4f4f5;
17
- --color-ink-200: #e4e4e7;
18
- --color-ink-300: #d4d4d8;
19
- --color-ink-400: #a1a1aa;
20
- --color-ink-500: #71717a;
21
- --color-ink-600: #52525b;
22
- --color-ink-700: #3f3f46;
23
- --color-ink-800: #27272a;
24
- --color-ink-900: #18181b;
25
- --color-ink-950: #09090b;
26
-
27
- --color-grove-primary: #16a34a;
23
+ --color-grove-primary: var(--color-foreground);
28
24
  }
29
25
 
30
26
  @layer base {
@@ -33,26 +29,5 @@
33
29
  --grove-radius: 0.5rem;
34
30
  --grove-container-width: 72rem;
35
31
 
36
- color-scheme: light dark;
37
- }
38
-
39
- html {
40
- -webkit-text-size-adjust: 100%;
41
- -webkit-font-smoothing: antialiased;
42
- -moz-osx-font-smoothing: grayscale;
43
- }
44
-
45
- body {
46
- background: #ffffff;
47
- color: var(--color-ink-900);
48
- font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
49
- "Helvetica Neue", Arial, sans-serif;
50
- font-size: 15px;
51
- line-height: 1.55;
52
- }
53
-
54
- body.dark {
55
- background: var(--color-ink-950);
56
- color: var(--color-ink-100);
57
32
  }
58
33
  }
@@ -1,130 +0,0 @@
1
- /** @type {import('tailwindcss').Config} */
2
- export default {
3
- content: ['./src/**/*.{astro,html,js,jsx,ts,tsx,md,mdx}'],
4
- // Use a `.dark` class on <html> (toggled by the theme script in
5
- // the document head) rather than the OS media query, so a manual
6
- // theme switcher can override the user's system preference.
7
- darkMode: 'class',
8
- theme: {
9
- // Disable default palette so we only ship our own neutral / accent
10
- // tokens. Keeps the bundle small and forces a unified look.
11
- colors: {
12
- transparent: 'transparent',
13
- current: 'currentColor',
14
- white: '#ffffff',
15
- black: '#000000',
16
- ink: {
17
- 50: '#fafafa',
18
- 100: '#f4f4f5',
19
- 200: '#e4e4e7',
20
- 300: '#d4d4d8',
21
- 400: '#a1a1aa',
22
- 500: '#71717a',
23
- 600: '#52525b',
24
- 700: '#3f3f46',
25
- 800: '#27272a',
26
- 900: '#18181b',
27
- 950: '#09090b',
28
- },
29
- accent: {
30
- DEFAULT: '#1f6feb',
31
- muted: '#388bfd1a',
32
- },
33
- emerald: {
34
- 50: '#ecfdf5',
35
- 300: '#6ee7b7',
36
- 400: '#34d399',
37
- 700: '#047857',
38
- 800: '#065f46',
39
- 950: '#022c22',
40
- },
41
- orange: {
42
- 50: '#fff7ed',
43
- 300: '#fdba74',
44
- 400: '#fb923c',
45
- 700: '#c2410c',
46
- 800: '#9a3412',
47
- 950: '#431407',
48
- },
49
- blue: {
50
- 50: '#eff6ff',
51
- 300: '#93c5fd',
52
- 400: '#60a5fa',
53
- 700: '#1d4ed8',
54
- 800: '#1e40af',
55
- 950: '#172554',
56
- },
57
- amber: {
58
- 50: '#fffbeb',
59
- 300: '#fcd34d',
60
- 400: '#fbbf24',
61
- 700: '#b45309',
62
- 800: '#92400e',
63
- 950: '#451a03',
64
- },
65
- },
66
- extend: {
67
- fontFamily: {
68
- sans: [
69
- 'ui-sans-serif',
70
- 'system-ui',
71
- '-apple-system',
72
- 'BlinkMacSystemFont',
73
- '"Segoe UI"',
74
- 'Roboto',
75
- '"Helvetica Neue"',
76
- 'Arial',
77
- 'sans-serif',
78
- ],
79
- mono: [
80
- 'ui-monospace',
81
- 'SFMono-Regular',
82
- 'Menlo',
83
- 'Monaco',
84
- 'Consolas',
85
- '"Liberation Mono"',
86
- '"Courier New"',
87
- 'monospace',
88
- ],
89
- },
90
- fontSize: {
91
- '2xs': ['0.6875rem', { lineHeight: '1rem' }],
92
- },
93
- maxWidth: {
94
- container: '1140px',
95
- wide: '1400px',
96
- narrow: '720px',
97
- },
98
- borderRadius: {
99
- DEFAULT: '0.375rem',
100
- lg: '0.5rem',
101
- xl: '0.75rem',
102
- },
103
- keyframes: {
104
- 'fade-in-up': {
105
- '0%': { opacity: '0', transform: 'translateY(8px)' },
106
- '100%': { opacity: '1', transform: 'translateY(0)' },
107
- },
108
- 'fade-in': {
109
- '0%': { opacity: '0' },
110
- '100%': { opacity: '1' },
111
- },
112
- 'gradient-shift': {
113
- '0%, 100%': { 'background-position': '0% 50%' },
114
- '50%': { 'background-position': '100% 50%' },
115
- },
116
- 'subtle-bob': {
117
- '0%, 100%': { transform: 'translateY(0)' },
118
- '50%': { transform: 'translateY(-2px)' },
119
- },
120
- },
121
- animation: {
122
- 'fade-in-up': 'fade-in-up 0.6s ease-out both',
123
- 'fade-in': 'fade-in 0.8s ease-out both',
124
- 'gradient-shift': 'gradient-shift 8s ease-in-out infinite',
125
- 'subtle-bob': 'subtle-bob 3s ease-in-out infinite',
126
- },
127
- },
128
- },
129
- plugins: [],
130
- };