@timmo001/docs-kit 0.2.0 → 0.3.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/README.md +29 -4
- package/package.json +4 -1
- package/src/blume/config.ts +18 -5
- package/src/blume/header-search.astro +9 -1
- package/src/blume/home-banner.astro +1 -1
- package/src/blume/theme-picker.astro +226 -0
- package/src/blume/theme.css +1 -1
- package/src/blume/themes/all.css +475 -0
- package/src/blume/themes/base.css +47 -22
- package/src/blume/themes/bridge.css +47 -22
- package/src/blume/themes/glazing.css +47 -22
- package/src/blume/themes/slate.css +48 -23
- package/src/blume/themes/snag.css +47 -22
- package/src/blume/themes/survey.css +47 -22
- package/src/brand-images.ts +2 -2
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ The `blume` export needs the `blume` version in `peerDependencies`.
|
|
|
21
21
|
|
|
22
22
|
`docsConfig` returns a full Blume config from the site details. Pass anything
|
|
23
23
|
else Blume accepts alongside them; it overrides the default it replaces, and
|
|
24
|
-
`navigation` and `seo` merge one level deep.
|
|
24
|
+
`markdown`, `navigation` and `seo` merge one level deep.
|
|
25
25
|
|
|
26
26
|
```ts
|
|
27
27
|
import { docsConfig } from "@timmo001/docs-kit/blume";
|
|
@@ -46,6 +46,8 @@ The defaults:
|
|
|
46
46
|
with the title as the logo text.
|
|
47
47
|
- Content in `src/content/docs`, last-modified dates from git and external links
|
|
48
48
|
opening in a new tab.
|
|
49
|
+
- GitHub's high-contrast code themes, since the default ones miss WCAG AA on
|
|
50
|
+
comments and keywords. Set `markdown.code.theme` to change them.
|
|
49
51
|
- Cloudflare deployment to `https://<site>`.
|
|
50
52
|
- `llms.txt`, the MCP endpoint at `/mcp`, WebMCP and agent readability on. Ask
|
|
51
53
|
AI and page feedback off.
|
|
@@ -98,12 +100,35 @@ Glazing, Survey, Slate, Bridge or Snag:
|
|
|
98
100
|
@import "@timmo001/docs-kit/blume/themes/bridge.css";
|
|
99
101
|
```
|
|
100
102
|
|
|
101
|
-
It sets Blume's
|
|
102
|
-
leave `theme.accent` out of `blume.config.ts`. The fonts are stacks,
|
|
103
|
-
the theme's font files in `theme.css` too, for example with
|
|
103
|
+
It sets Blume's colors, radius and fonts from the theme in light, dark and
|
|
104
|
+
Darker, so leave `theme.accent` out of `blume.config.ts`. The fonts are stacks,
|
|
105
|
+
so load the theme's font files in `theme.css` too, for example with
|
|
104
106
|
`@import "@fontsource-variable/inter";`. Use Base until the app has a theme of
|
|
105
107
|
its own.
|
|
106
108
|
|
|
109
|
+
`HeaderSearch` adds a mode picker next to Blume's light and dark toggle, with
|
|
110
|
+
Light, Dark and Darker. To let readers pick the theme as well, import every
|
|
111
|
+
theme after the site's default and pass the themes to `HeaderSearch` from a
|
|
112
|
+
wrapper component:
|
|
113
|
+
|
|
114
|
+
```css
|
|
115
|
+
@import "@timmo001/docs-kit/blume/themes/base.css";
|
|
116
|
+
@import "@timmo001/docs-kit/blume/themes/all.css";
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```astro
|
|
120
|
+
---
|
|
121
|
+
import HeaderSearch from "@timmo001/docs-kit/blume/header-search.astro";
|
|
122
|
+
|
|
123
|
+
const themes = [
|
|
124
|
+
{ id: "base", name: "Base" },
|
|
125
|
+
{ id: "bridge", name: "Bridge" },
|
|
126
|
+
];
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
<HeaderSearch {...Astro.props} themes={themes} defaultTheme="base" />
|
|
130
|
+
```
|
|
131
|
+
|
|
107
132
|
Set these in `theme.css` to change the banner:
|
|
108
133
|
|
|
109
134
|
```css
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmo001/docs-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared branding, layout and config for timmo001 documentation sites",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"./blume": "./src/blume/index.ts",
|
|
10
10
|
"./blume/header-search.astro": "./src/blume/header-search.astro",
|
|
11
11
|
"./blume/home-banner.astro": "./src/blume/home-banner.astro",
|
|
12
|
+
"./blume/theme-picker.astro": "./src/blume/theme-picker.astro",
|
|
12
13
|
"./blume/theme.css": "./src/blume/theme.css",
|
|
13
14
|
"./blume/themes/*.css": "./src/blume/themes/*.css",
|
|
14
15
|
"./package.json": "./package.json"
|
|
@@ -38,6 +39,8 @@
|
|
|
38
39
|
"format:check": "prettier --check ."
|
|
39
40
|
},
|
|
40
41
|
"dependencies": {
|
|
42
|
+
"@timmo001/groundwork-tokens": "0.3.0",
|
|
43
|
+
"@timmo001/groundwork-ui": "0.3.0",
|
|
41
44
|
"sharp": "0.35.5"
|
|
42
45
|
},
|
|
43
46
|
"peerDependencies": {
|
package/src/blume/config.ts
CHANGED
|
@@ -17,12 +17,14 @@ export interface DocsConfigOptions extends Omit<
|
|
|
17
17
|
/**
|
|
18
18
|
* Build a Blume config with the shared defaults: light and dark logos from
|
|
19
19
|
* `public/`, Cloudflare deployment, agent surfaces on, Ask AI and feedback off,
|
|
20
|
-
* and generated share cards. Any option given here
|
|
21
|
-
* replaces; `navigation` and `seo` merge
|
|
20
|
+
* high-contrast code themes and generated share cards. Any option given here
|
|
21
|
+
* wins over the default it replaces; `markdown`, `navigation` and `seo` merge
|
|
22
|
+
* one level deep.
|
|
22
23
|
*/
|
|
23
24
|
export const docsConfig = ({
|
|
24
25
|
site,
|
|
25
26
|
github,
|
|
27
|
+
markdown,
|
|
26
28
|
navigation,
|
|
27
29
|
seo,
|
|
28
30
|
...config
|
|
@@ -38,9 +40,6 @@ export const docsConfig = ({
|
|
|
38
40
|
content: {
|
|
39
41
|
root: "src/content/docs",
|
|
40
42
|
},
|
|
41
|
-
markdown: {
|
|
42
|
-
externalLinks: true,
|
|
43
|
-
},
|
|
44
43
|
ai: {
|
|
45
44
|
assistant: {
|
|
46
45
|
enabled: false,
|
|
@@ -71,6 +70,20 @@ export const docsConfig = ({
|
|
|
71
70
|
dir: "docs",
|
|
72
71
|
...github,
|
|
73
72
|
},
|
|
73
|
+
// The high-contrast GitHub themes, since the default ones miss WCAG AA on
|
|
74
|
+
// comments and keywords
|
|
75
|
+
markdown: {
|
|
76
|
+
externalLinks: true,
|
|
77
|
+
...markdown,
|
|
78
|
+
code: {
|
|
79
|
+
...markdown?.code,
|
|
80
|
+
theme: {
|
|
81
|
+
dark: "github-dark-high-contrast",
|
|
82
|
+
light: "github-light-high-contrast",
|
|
83
|
+
...markdown?.code?.theme,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
74
87
|
navigation: {
|
|
75
88
|
repo: true,
|
|
76
89
|
...navigation,
|
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
import Search from "blume/components/layout/Search.astro";
|
|
3
3
|
import { SOCIAL_ICONS } from "blume/components/layout/footer-socials.ts";
|
|
4
4
|
import data from "blume:data";
|
|
5
|
+
import ThemePicker from "./theme-picker.astro";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `themes` and `defaultTheme` go to the theme picker; everything else Blume
|
|
9
|
+
* passes goes to its search.
|
|
10
|
+
*/
|
|
11
|
+
const { themes, defaultTheme, ...searchProps } = Astro.props;
|
|
5
12
|
|
|
6
13
|
const repoUrl = data.config.repoUrl;
|
|
7
14
|
|
|
8
15
|
const github = SOCIAL_ICONS.github;
|
|
9
16
|
---
|
|
10
17
|
|
|
11
|
-
<Search {...
|
|
18
|
+
<Search {...searchProps} />
|
|
19
|
+
<ThemePicker themes={themes} defaultTheme={defaultTheme} />
|
|
12
20
|
{
|
|
13
21
|
repoUrl && (
|
|
14
22
|
<a
|
|
@@ -32,7 +32,7 @@ const description = page.description ?? data.config.description;
|
|
|
32
32
|
<style>
|
|
33
33
|
/* Set --home-banner-logo-size, --home-banner-height,
|
|
34
34
|
--home-banner-background or --home-banner-background-dark in the site's
|
|
35
|
-
theme.css to resize or
|
|
35
|
+
theme.css to resize or recolor the banner. */
|
|
36
36
|
.home-banner {
|
|
37
37
|
position: relative;
|
|
38
38
|
overflow: hidden;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Picks the color mode, Darker included, and optionally the Groundwork
|
|
4
|
+
* theme. Blume's own toggle still switches light and dark; this keeps in step
|
|
5
|
+
* with it. Theme choices need `@timmo001/docs-kit/blume/themes/all.css`.
|
|
6
|
+
* The controls are Groundwork's `gw-select` and `gw-segmented`, styled by the
|
|
7
|
+
* Groundwork theme tokens imported here.
|
|
8
|
+
*/
|
|
9
|
+
import "@timmo001/groundwork-tokens/themes.css";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Themes to offer, in order. Leave out to offer the mode only. */
|
|
13
|
+
themes?: readonly { id: string; name: string }[];
|
|
14
|
+
/** The theme the site's own `theme.css` imports, shown when none is picked. */
|
|
15
|
+
defaultTheme?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { themes = [], defaultTheme = "base" } = Astro.props;
|
|
19
|
+
|
|
20
|
+
// MDI's sunny, waning crescent and night icons
|
|
21
|
+
const modes = [
|
|
22
|
+
{
|
|
23
|
+
value: "light",
|
|
24
|
+
label: "Light",
|
|
25
|
+
icon: "M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M3.36,17L5.12,13.23C5.26,14 5.53,14.78 5.95,15.5C6.37,16.24 6.91,16.86 7.5,17.37L3.36,17M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M20.64,17L16.5,17.36C17.09,16.85 17.62,16.22 18.04,15.5C18.46,14.77 18.73,14 18.87,13.21L20.64,17M12,22L9.59,18.56C10.33,18.83 11.14,19 12,19C12.82,19 13.63,18.83 14.37,18.56L12,22Z",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
value: "dark",
|
|
29
|
+
label: "Dark",
|
|
30
|
+
icon: "M2 12A10 10 0 0 0 15 21.54A10 10 0 0 1 15 2.46A10 10 0 0 0 2 12Z",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
value: "darker",
|
|
34
|
+
label: "Darker",
|
|
35
|
+
icon: "M17.75,4.09L15.22,6.03L16.13,9.09L13.5,7.28L10.87,9.09L11.78,6.03L9.25,4.09L12.44,4L13.5,1L14.56,4L17.75,4.09M21.25,11L19.61,12.25L20.2,14.23L18.5,13.06L16.8,14.23L17.39,12.25L15.75,11L17.81,10.95L18.5,9L19.19,10.95L21.25,11M18.97,15.95C19.8,15.87 20.69,17.05 20.16,17.8C19.84,18.25 19.5,18.67 19.08,19.07C15.17,23 8.84,23 4.94,19.07C1.03,15.17 1.03,8.83 4.94,4.93C5.34,4.53 5.76,4.17 6.21,3.85C6.96,3.32 8.14,4.21 8.06,5.04C7.79,7.9 8.75,10.87 10.95,13.06C13.14,15.26 16.1,16.22 18.97,15.95M17.33,17.97C14.5,17.81 11.7,16.64 9.53,14.5C7.36,12.31 6.2,9.5 6.04,6.68C3.23,9.82 3.34,14.64 6.35,17.66C9.37,20.67 14.19,20.78 17.33,17.97Z",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<script is:inline>
|
|
41
|
+
(() => {
|
|
42
|
+
const root = document.documentElement;
|
|
43
|
+
|
|
44
|
+
const read = (key) => {
|
|
45
|
+
try {
|
|
46
|
+
return localStorage.getItem(key);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const apply = () => {
|
|
53
|
+
const theme = read("gw-docs-theme");
|
|
54
|
+
|
|
55
|
+
if (theme) root.dataset.gwDocsTheme = theme;
|
|
56
|
+
else delete root.dataset.gwDocsTheme;
|
|
57
|
+
|
|
58
|
+
if (read("gw-darker") === "1" && root.dataset.theme === "dark") {
|
|
59
|
+
root.setAttribute("data-gw-darker", "");
|
|
60
|
+
} else {
|
|
61
|
+
root.removeAttribute("data-gw-darker");
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
apply();
|
|
66
|
+
document.addEventListener("astro:after-swap", apply);
|
|
67
|
+
})();
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
class="gw-theme-picker"
|
|
72
|
+
data-gw-theme-picker
|
|
73
|
+
data-default-theme={defaultTheme}
|
|
74
|
+
data-gw-theme={defaultTheme}
|
|
75
|
+
>
|
|
76
|
+
{
|
|
77
|
+
themes.length > 0 && (
|
|
78
|
+
<gw-select
|
|
79
|
+
data-gw-theme-select
|
|
80
|
+
label="Theme"
|
|
81
|
+
inline-label
|
|
82
|
+
search="never"
|
|
83
|
+
options={JSON.stringify(themes.map((theme) => ({ value: theme.id, label: theme.name })))}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
<gw-segmented
|
|
88
|
+
data-gw-mode-select
|
|
89
|
+
label="Color mode"
|
|
90
|
+
labels="hide"
|
|
91
|
+
options={JSON.stringify(modes)}></gw-segmented>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<script>
|
|
95
|
+
import { GwSegmented } from "@timmo001/groundwork-ui/gw-segmented.ts";
|
|
96
|
+
import { GwSelect } from "@timmo001/groundwork-ui/gw-select.ts";
|
|
97
|
+
|
|
98
|
+
const root = document.documentElement;
|
|
99
|
+
|
|
100
|
+
const save = (key: string, value: string | null) => {
|
|
101
|
+
try {
|
|
102
|
+
if (value === null) localStorage.removeItem(key);
|
|
103
|
+
else localStorage.setItem(key, value);
|
|
104
|
+
} catch {
|
|
105
|
+
// Storage can be blocked; the choice then lasts for the page.
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const currentMode = () =>
|
|
110
|
+
root.dataset.theme !== "dark"
|
|
111
|
+
? "light"
|
|
112
|
+
: root.hasAttribute("data-gw-darker")
|
|
113
|
+
? "darker"
|
|
114
|
+
: "dark";
|
|
115
|
+
|
|
116
|
+
const sync = () => {
|
|
117
|
+
for (const picker of document.querySelectorAll("div[data-gw-theme-picker]")) {
|
|
118
|
+
if (!(picker instanceof HTMLElement)) continue;
|
|
119
|
+
|
|
120
|
+
const theme = root.dataset.gwDocsTheme ?? picker.dataset.defaultTheme ?? "";
|
|
121
|
+
const mode = currentMode();
|
|
122
|
+
|
|
123
|
+
// The controls are Groundwork components, so the picker carries the
|
|
124
|
+
// page's theme and mode for them to follow
|
|
125
|
+
picker.dataset.gwTheme = theme;
|
|
126
|
+
picker.dataset.gwMode = mode;
|
|
127
|
+
|
|
128
|
+
for (const control of picker.querySelectorAll("gw-select, gw-segmented")) {
|
|
129
|
+
if (control instanceof GwSegmented) control.value = mode;
|
|
130
|
+
else if (control instanceof GwSelect) control.value = theme;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
document.addEventListener("change", (event) => {
|
|
136
|
+
const target = event.target;
|
|
137
|
+
|
|
138
|
+
if (!(target instanceof GwSelect || target instanceof GwSegmented)) return;
|
|
139
|
+
|
|
140
|
+
// Theme and mode change the whole page, so they crossfade where the
|
|
141
|
+
// browser has View Transitions and motion isn't reduced.
|
|
142
|
+
const transition = (change: () => void) => {
|
|
143
|
+
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
144
|
+
|
|
145
|
+
if (document.startViewTransition === undefined || reduced) change();
|
|
146
|
+
else document.startViewTransition(change).ready.catch(() => undefined);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (target.matches("[data-gw-mode-select]")) {
|
|
150
|
+
const dark = target.value !== "light";
|
|
151
|
+
const value = target.value;
|
|
152
|
+
|
|
153
|
+
transition(() => {
|
|
154
|
+
root.dataset.theme = dark ? "dark" : "light";
|
|
155
|
+
root.toggleAttribute("data-gw-darker", value === "darker");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
save("blume-theme", dark ? "dark" : "light");
|
|
159
|
+
save("gw-darker", value === "darker" ? "1" : null);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (target.matches("[data-gw-theme-select]")) {
|
|
163
|
+
const value = target.value;
|
|
164
|
+
|
|
165
|
+
transition(() => {
|
|
166
|
+
root.dataset.gwDocsTheme = value;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
save("gw-docs-theme", value);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Blume's toggle flips data-theme; going light ends Darker.
|
|
174
|
+
new MutationObserver(() => {
|
|
175
|
+
if (root.dataset.theme !== "dark" && root.hasAttribute("data-gw-darker")) {
|
|
176
|
+
root.removeAttribute("data-gw-darker");
|
|
177
|
+
save("gw-darker", null);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
sync();
|
|
181
|
+
}).observe(root, {
|
|
182
|
+
attributes: true,
|
|
183
|
+
attributeFilter: ["data-theme", "data-gw-darker", "data-gw-docs-theme"],
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
sync();
|
|
187
|
+
|
|
188
|
+
document.addEventListener("astro:after-swap", sync);
|
|
189
|
+
</script>
|
|
190
|
+
|
|
191
|
+
<style>
|
|
192
|
+
/* Gaps leave room for the focus ring, which sits outside each control */
|
|
193
|
+
.gw-theme-picker {
|
|
194
|
+
display: inline-flex;
|
|
195
|
+
flex-shrink: 0;
|
|
196
|
+
align-items: center;
|
|
197
|
+
gap: 0.625rem;
|
|
198
|
+
margin-inline: 0.25rem;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* Keep the room the controls take once they upgrade, so the header
|
|
202
|
+
doesn't move */
|
|
203
|
+
.gw-theme-picker:has(:not(:defined)) {
|
|
204
|
+
min-inline-size: 16.75rem;
|
|
205
|
+
min-block-size: 2.5rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* The picker's mode toggle replaces Blume's own light and dark button */
|
|
209
|
+
:global([data-blume-header]:has(.gw-theme-picker) [data-blume-theme-toggle]) {
|
|
210
|
+
display: none;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* The components draw on Groundwork's surface, so the header's own
|
|
214
|
+
background shows through instead */
|
|
215
|
+
.gw-theme-picker {
|
|
216
|
+
background: transparent;
|
|
217
|
+
color: inherit;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Phones keep the header to one row, so the select shows only its value */
|
|
221
|
+
@media (max-width: 40rem) {
|
|
222
|
+
.gw-theme-picker :global(.gw-select-inline-label) {
|
|
223
|
+
display: none;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
</style>
|
package/src/blume/theme.css
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/* Keep the "On this page" rail beside the content instead of at the far right
|
|
15
|
-
edge:
|
|
15
|
+
edge: center the content and rail, with equal space either side. The content
|
|
16
16
|
column is the content width plus main's 2.5rem padding either side.
|
|
17
17
|
home-banner.astro works out its full-width bleed from these columns. */
|
|
18
18
|
@media (min-width: 80rem) {
|