@grove-dev/starlight 0.2.20 → 0.3.2

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.
@@ -0,0 +1,111 @@
1
+ ---
2
+ /**
3
+ * Starlight `<Card>` override that adds an optional `href` prop.
4
+ *
5
+ * The upstream `Card.astro` in `@astrojs/starlight@0.40` only accepts
6
+ * `icon` and `title`, so a `<Card title="..." href="...">` call silently
7
+ * dropped the link and rendered a non-clickable `<article>`. We wrap the
8
+ * inner content in an `<a>` when `href` is present so the entire card
9
+ * surface is interactive (the same affordance the `LinkCard` component
10
+ * uses, but with our visual `Card` styling).
11
+ *
12
+ * Visual styles are identical to upstream — only the structure gains a
13
+ * single `<a>` wrapper when `href` is set.
14
+ */
15
+ import { Icon } from '@astrojs/starlight/components';
16
+ import type { StarlightIcon } from '@astrojs/starlight/components-internals/Icons';
17
+
18
+ interface Props {
19
+ icon?: StarlightIcon;
20
+ title: string;
21
+ href?: string;
22
+ }
23
+
24
+ const { icon, title, href } = Astro.props;
25
+ ---
26
+
27
+ <article class="card sl-flex">
28
+ <p class="title sl-flex">
29
+ {icon && <Icon name={icon} class="icon" size="1.333em" />}
30
+ <span set:html={title} />
31
+ </p>
32
+ <div class="body">
33
+ {href ? <a {href} class="sl-card-link"><slot /></a> : <slot />}
34
+ </div>
35
+ </article>
36
+
37
+ <style>
38
+ @layer starlight.components {
39
+ .card {
40
+ --sl-card-border: var(--sl-color-purple);
41
+ --sl-card-bg: var(--sl-color-purple-low);
42
+ border: 1px solid var(--sl-color-gray-5);
43
+ background-color: var(--sl-color-black);
44
+ padding: clamp(1rem, calc(0.125rem + 3vw), 2.5rem);
45
+ flex-direction: column;
46
+ gap: clamp(0.5rem, calc(0.125rem + 1vw), 1rem);
47
+ transition: border-color 0.15s ease, transform 0.15s ease;
48
+ }
49
+ .card:nth-child(4n + 1) {
50
+ --sl-card-border: var(--sl-color-orange);
51
+ --sl-card-bg: var(--sl-color-orange-low);
52
+ }
53
+ .card:nth-child(4n + 3) {
54
+ --sl-card-border: var(--sl-color-green);
55
+ --sl-card-bg: var(--sl-color-green-low);
56
+ }
57
+ .card:nth-child(4n + 4) {
58
+ --sl-card-border: var(--sl-color-red);
59
+ --sl-card-bg: var(--sl-color-red-low);
60
+ }
61
+ .card:nth-child(4n + 5) {
62
+ --sl-card-border: var(--sl-color-blue);
63
+ --sl-card-bg: var(--sl-color-blue-low);
64
+ }
65
+ .title {
66
+ font-weight: 600;
67
+ font-size: var(--sl-text-h4);
68
+ color: var(--sl-color-white);
69
+ line-height: var(--sl-line-height-headings);
70
+ gap: 1rem;
71
+ align-items: center;
72
+ }
73
+ .card .icon {
74
+ border: 1px solid var(--sl-card-border);
75
+ background-color: var(--sl-card-bg);
76
+ padding: 0.2em;
77
+ border-radius: 0.25rem;
78
+ flex-shrink: 0;
79
+ }
80
+ .card .body {
81
+ margin: 0;
82
+ font-size: clamp(var(--sl-text-sm), calc(0.5rem + 1vw), var(--sl-text-body));
83
+ }
84
+ .card .body .sl-card-link {
85
+ /* The link is invisible text-decoration-wise; the card itself
86
+ * is the click target thanks to the ::before pseudo-element
87
+ * below. Display:contents lets the link wrap the body text
88
+ * without breaking the flex/grid layout. */
89
+ display: contents;
90
+ color: inherit;
91
+ }
92
+ .card:has(.sl-card-link) {
93
+ position: relative;
94
+ cursor: pointer;
95
+ }
96
+ .card:has(.sl-card-link):hover {
97
+ border-color: var(--sl-color-gray-2);
98
+ }
99
+ .card:has(.sl-card-link):hover .title {
100
+ color: var(--sl-color-accent-high);
101
+ }
102
+ /* a11y: a transparent overlay makes the whole card clickable
103
+ * without changing layout. Same trick Starlight's LinkCard uses. */
104
+ .card:has(.sl-card-link) .sl-card-link::before {
105
+ content: '';
106
+ position: absolute;
107
+ inset: 0;
108
+ border-radius: inherit;
109
+ }
110
+ }
111
+ </style>
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@grove-dev/starlight",
3
- "version": "0.2.20",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "description": "Grove's theme for Starlight (the Astro native documentation site generator)",
6
6
  "author": "grove-dev",
7
7
  "license": "MIT",
8
+ "engines": {
9
+ "node": ">=22.12.0"
10
+ },
8
11
  "readme": "README.md",
9
12
  "homepage": "https://grove.dev.mn/",
10
13
  "repository": {
@@ -18,6 +21,18 @@
18
21
  "publishConfig": {
19
22
  "access": "public"
20
23
  },
24
+ "files": [
25
+ "components",
26
+ "core",
27
+ "styles",
28
+ "global.d.ts",
29
+ "index.ts",
30
+ "schema.ts",
31
+ "user-components.ts",
32
+ "virtual.d.ts",
33
+ "README.md",
34
+ "THIRD_PARTY_LICENSES.md"
35
+ ],
21
36
  "exports": {
22
37
  ".": "./index.ts",
23
38
  "./schema": "./schema.ts",
package/styles/base.css CHANGED
@@ -330,6 +330,11 @@
330
330
  /* built-in Starlight Cards and LinkCards */
331
331
  .sl-markdown-content .card-grid {
332
332
  gap: 1rem;
333
+ grid-auto-rows: 1fr;
334
+ }
335
+
336
+ .sl-markdown-content .card-grid > * {
337
+ height: 100%;
333
338
  }
334
339
 
335
340
  .sl-markdown-content :is(.card, .sl-link-card) {
@@ -339,6 +344,7 @@
339
344
  background-color: var(--code-background);
340
345
  box-shadow: none;
341
346
  transition: background-color 0.15s;
347
+ height: 100%;
342
348
  }
343
349
 
344
350
  .sl-markdown-content :is(.card, .sl-link-card):hover {
@@ -1,3 +1,4 @@
1
+ export { default as Card } from './components/custom/Card.astro';
1
2
  export { default as ContainerSection } from './components/custom/ContainerSection.astro';
2
3
  export { default as LinkButton } from './components/custom/LinkButton.astro';
3
4
  export { default as Dropdown } from './components/custom/dropdown';
@@ -1,191 +0,0 @@
1
- /**
2
- * @grove-dev/starlight — config / override / virtual module tests.
3
- *
4
- * Coverage (per the brief):
5
- * - plugin.ts:parseConfig: accepts empty config (all defaults),
6
- * honours user-supplied fields, throws on invalid input
7
- * - virtual module resolution: the vite plugin's `load` and
8
- * `resolveId` hooks return the canonical module id when
9
- * called with the expected arguments, and pass through for
10
- * unrelated ids
11
- * - override composition: the override() function fills in
12
- * every component override slot, warns the user on a clash,
13
- * and preserves a pre-set user override (does NOT clobber it
14
- * with the default)
15
- */
16
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
17
- import { plugin } from "../core/plugin.js";
18
- import { override, COMPONENT_OVERRIDES } from "../core/config/override.js";
19
- import { vitePlugin } from "../core/config/vite.js";
20
- import { LucodeStarlightConfigSchema } from "../core/config/schemas.js";
21
-
22
- describe("parseConfig (exercised through plugin())", () => {
23
- it("accepts an empty config and fills in all defaults", () => {
24
- // We can't import parseConfig directly (it's local to
25
- // plugin.ts), so we exercise it through `plugin(undefined)`,
26
- // which goes through the same Zod parse path. The hook
27
- // callback is called with the parsed config; we read it
28
- // back via the test.
29
- let captured: unknown;
30
- const p = plugin();
31
- // The plugin's `config:setup` hook is the only place
32
- // parseConfig's output is consumed. Invoke it with a
33
- // minimal stub of the hook parameters and capture the
34
- // addIntegration's vite config.
35
- const addIntegration = vi.fn();
36
- const updateConfig = vi.fn();
37
- const calls: Array<{ vite?: { plugins?: Array<{ load?: unknown; resolveId?: unknown }> } }> = [];
38
- addIntegration.mockImplementation((integration: { hooks: { "astro:config:setup": (args: { updateConfig: (cfg: { vite?: { plugins?: Array<{ load?: unknown; resolveId?: unknown }> } }) => void }) => void } }) => {
39
- integration.hooks["astro:config:setup"]({
40
- updateConfig: (cfg) => calls.push(cfg),
41
- });
42
- });
43
- p.hooks["config:setup"]({
44
- config: { customCss: [] },
45
- logger: { warn: () => {}, info: () => {}, error: () => {}, debug: () => {} } as never,
46
- updateConfig: updateConfig as never,
47
- addIntegration: addIntegration as never,
48
- command: "build" as never,
49
- isRestart: false,
50
- });
51
- expect(calls).toHaveLength(1);
52
- const call = calls[0];
53
- const vite = call?.vite;
54
- expect(vite?.plugins).toBeDefined();
55
- captured = vite;
56
- // Sanity: parseConfig didn't throw on the empty input.
57
- expect(captured).toBeDefined();
58
- });
59
-
60
- it("honours user-supplied docs.includeAiUtilities", () => {
61
- // Capture the vite plugin's resolved config to assert
62
- // that the user's boolean is preserved.
63
- let capturedPlugin: ReturnType<typeof vitePlugin> | undefined;
64
- const addIntegration = vi.fn((integration: { hooks: { "astro:config:setup": (args: { updateConfig: (cfg: { vite: { plugins: Array<ReturnType<typeof vitePlugin>> } }) => void }) => void } }) => {
65
- integration.hooks["astro:config:setup"]({
66
- updateConfig: (cfg) => {
67
- capturedPlugin = cfg.vite.plugins[0];
68
- },
69
- });
70
- });
71
- const p = plugin({ docs: { includeAiUtilities: true } });
72
- p.hooks["config:setup"]({
73
- config: { customCss: [] },
74
- logger: { warn: () => {}, info: () => {}, error: () => {}, debug: () => {} } as never,
75
- updateConfig: vi.fn() as never,
76
- addIntegration: addIntegration as never,
77
- command: "build" as never,
78
- isRestart: false,
79
- });
80
- expect(capturedPlugin).toBeDefined();
81
- // The vite plugin's moduleContent embeds the JSON-serialized
82
- // config. Load it to confirm `includeAiUtilities: true`
83
- // survived the parse.
84
- const loaded = capturedPlugin!.load("\0virtual:lucode-starlight-config") as string;
85
- expect(loaded).toContain('"includeAiUtilities":true');
86
- });
87
-
88
- it("falls back to the schema default for an empty docs object", () => {
89
- // The schema has `.default({ includeAiUtilities: false })`
90
- // for the docs sub-object. Pin it.
91
- const parsed = LucodeStarlightConfigSchema.parse({});
92
- expect(parsed.docs?.includeAiUtilities).toBe(false);
93
- });
94
-
95
- it("throws on invalid input (zod validation failure)", () => {
96
- // A non-string label fails the linkSchema validation.
97
- expect(() => LucodeStarlightConfigSchema.parse({ navLinks: "not-an-array" })).toThrow();
98
- });
99
- });
100
-
101
- describe("vitePlugin — virtual module resolution", () => {
102
- it("resolveId returns the resolved id for the canonical module id", () => {
103
- const p = vitePlugin(LucodeStarlightConfigSchema.parse({}));
104
- // The virtual module prefix is 'virtual:'. The plugin
105
- // matches on the un-prefixed name and returns the
106
- // '\0'-prefixed resolved id (Vite convention for virtual
107
- // modules — the null byte prevents the resolved id from
108
- // being treated as a real file path).
109
- const result = p.resolveId?.("virtual:lucode-starlight-config");
110
- expect(result).toBe("\0virtual:lucode-starlight-config");
111
- });
112
-
113
- it("resolveId returns undefined for unrelated ids (passthrough)", () => {
114
- const p = vitePlugin(LucodeStarlightConfigSchema.parse({}));
115
- expect(p.resolveId?.("virtual:some-other-module")).toBeUndefined();
116
- expect(p.resolveId?.("./relative-import")).toBeUndefined();
117
- });
118
-
119
- it("load returns the JSON-serialized config when given the resolved id", () => {
120
- const config = LucodeStarlightConfigSchema.parse({ docs: { includeAiUtilities: true } });
121
- const p = vitePlugin(config);
122
- const module = p.load?.("\0virtual:lucode-starlight-config") as string;
123
- expect(module).toContain("export default");
124
- expect(module).toContain('"includeAiUtilities":true');
125
- });
126
-
127
- it("load returns undefined for unrelated ids", () => {
128
- const p = vitePlugin(LucodeStarlightConfigSchema.parse({}));
129
- expect(p.load?.("not-the-virtual-id")).toBeUndefined();
130
- });
131
- });
132
-
133
- describe("override — component override composition", () => {
134
- let warnSpy: ReturnType<typeof vi.spyOn>;
135
-
136
- beforeEach(() => {
137
- warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
138
- });
139
-
140
- afterEach(() => {
141
- warnSpy.mockRestore();
142
- });
143
-
144
- it("fills every component override slot when the user has none", () => {
145
- // Starlight starts with no components overridden (empty
146
- // config). The override() function should set every key in
147
- // COMPONENT_OVERRIDES to point at the @grove-dev/starlight
148
- // path.
149
- const components = override(
150
- { components: {} } as never,
151
- COMPONENT_OVERRIDES,
152
- { warn: warnSpy, info: () => {}, error: () => {}, debug: () => {} } as never,
153
- );
154
- for (const key of COMPONENT_OVERRIDES) {
155
- expect(components?.[key]).toBe(`@grove-dev/starlight/components/overrides/${key}.astro`);
156
- }
157
- });
158
-
159
- it("preserves a user-supplied component override (does not clobber)", () => {
160
- // If the user already set components.Footer, we should
161
- // NOT overwrite it with the default — we warn and skip.
162
- // Pin: a previous "always overwrite" implementation would
163
- // silently clobber the user's custom footer.
164
- const userFooter = "./my-custom/Footer.astro";
165
- const components = override(
166
- { components: { Footer: userFooter } } as never,
167
- COMPONENT_OVERRIDES,
168
- { warn: warnSpy, info: () => {}, error: () => {}, debug: () => {} } as never,
169
- );
170
- expect(components?.Footer).toBe(userFooter);
171
- // The warn call names the slot and points at the override path.
172
- const warnings = warnSpy.mock.calls.map((c: unknown[]) => String(c[0]));
173
- expect(warnings.some((w: string) => w.includes("Footer"))).toBe(true);
174
- // All OTHER slots are still filled in.
175
- expect(components?.Header).toBe(`@grove-dev/starlight/components/overrides/Header.astro`);
176
- });
177
-
178
- it("handles a config with no `components` field at all (undefined)", () => {
179
- // Defensive: Starlight's config can omit `components`
180
- // entirely. override() should still produce a full set
181
- // without throwing.
182
- const components = override(
183
- {} as never,
184
- COMPONENT_OVERRIDES,
185
- { warn: warnSpy, info: () => {}, error: () => {}, debug: () => {} } as never,
186
- );
187
- for (const key of COMPONENT_OVERRIDES) {
188
- expect(components?.[key]).toBeDefined();
189
- }
190
- });
191
- });
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "rootDir": ".",
5
- "noEmit": true,
6
- "types": ["node", "vitest/globals"]
7
- },
8
- "include": ["core/**/*.ts", "tests/**/*.ts", "global.d.ts", "virtual.d.ts", "index.ts", "schema.ts", "user-components.ts"]
9
- }