@meddleware/vitepress-theme 0.0.0-stage → 0.0.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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +14 -0
- package/README.md +55 -2
- package/SECURITY.md +31 -0
- package/dist/config.d.ts +28 -0
- package/dist/landmarks.d.ts +7 -0
- package/package.json +93 -4
- package/src/MeddlewareLayout.vue +22 -0
- package/src/config.js +105 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +25 -0
- package/src/landmarks.js +57 -0
- package/src/theme.css +166 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.0.2] - 2026-11-01
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- The fixture's accessibility gate is the shared sweep (`defineA11ySuite`): besides axe at AA and AAA in every colour mode × season it now
|
|
13
|
+
checks reflow at 320 px, 200% text, forced colours, reduced motion and the keyboard (focus visible and not hidden behind the fixed navigation bar).
|
|
14
|
+
- `SECURITY.md` is part of the published files.
|
|
15
|
+
- **The footer no longer lies under the sidebar.** VitePress's own footer hides on a page with a sidebar; ours stays (it holds the legal links), and the fixed full-height sidebar covered its left edge from 60rem, so the © link could not be seen when focused (WCAG 2.4.11). On such a page the footer now starts where the content does, with VitePress's own offsets. Three width tests in every browser hold it.
|
|
16
|
+
- VitePress 2.0.0-alpha.21 (peer and development pin; the landmark edits and the fixture sweep pass unchanged).
|
|
17
|
+
|
|
18
|
+
## [0.0.1] - 2026-10-31
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- First release. The VitePress default theme mapped onto `@meddleware/design-tokens` (colour mode, seasons, WCAG AAA contrast), a layout
|
|
23
|
+
with the Meddleware footer, the `baseConfig` site config, and landmark fixes for the default theme, extracted from the byte-identical
|
|
24
|
+
copies in docs-site and dev-site.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
BSD Zero Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MeddleWare
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
9
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
10
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
11
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
12
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
13
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
14
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @meddleware/vitepress-theme
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Meddleware [VitePress](https://vitepress.dev) theme and site config. It is the default theme with every colour, surface and font
|
|
4
|
+
taken from [`@meddleware/design-tokens`](https://github.com/meddleware-org/design-tokens), so a site follows the colour mode and the
|
|
5
|
+
seasons and keeps the tokens' WCAG AAA contrast (7:1 for text and labels), plus a footer and the config the sites share.
|
|
6
|
+
|
|
7
|
+
## Use
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// docs/.vitepress/theme/index.ts
|
|
11
|
+
export { default } from '@meddleware/vitepress-theme'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// docs/.vitepress/config.ts
|
|
16
|
+
import { baseConfig } from '@meddleware/vitepress-theme/config'
|
|
17
|
+
|
|
18
|
+
export default baseConfig({
|
|
19
|
+
configUrl: import.meta.url, // docs/ is the source directory, ../dist the output
|
|
20
|
+
title: 'Meddleware Docs',
|
|
21
|
+
description: '…',
|
|
22
|
+
nav: [{ text: 'Guide', link: '/guide' }],
|
|
23
|
+
sidebar: { '/': [{ text: 'Guide', items: [{ text: 'Overview', link: '/' }] }] },
|
|
24
|
+
// appearance: 'dark' (default) | true | false, head: [...], extra: { ...anything else VitePress takes }
|
|
25
|
+
})
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
A site that needs more extends the theme: `{ extends: MeddlewareTheme, enhanceApp() {…} }`.
|
|
29
|
+
|
|
30
|
+
`baseConfig` sets the language (`en-GB`), clean URLs, the favicon, the browser-chrome colour (from the tokens), local search, the GitHub
|
|
31
|
+
link, the high-contrast syntax themes (with comments as readable as the rest) and `ssr.noExternal` for `@meddleware/*`, which ship Vue
|
|
32
|
+
components that import their own CSS. `themeConfig`, `vite` and `markdown` in `extra` are merged one level down.
|
|
33
|
+
|
|
34
|
+
## What the theme does
|
|
35
|
+
|
|
36
|
+
| | |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `theme.css` | maps every `--vp-*` colour onto a token role; tone blocks and badges keep body-colour text with the tone as the border; nothing is purple |
|
|
39
|
+
| `MeddlewareLayout.vue` | the default layout plus `AppFooter` and `CopyrightLine` (a `contentinfo` landmark) |
|
|
40
|
+
| `landmarks.js` | a Vite plugin that gives home, `page` and 404 pages a `<main>`, and turns the mid-page doc footer into a `div` (it was a second `contentinfo`); exact edits that fail the build if VitePress changes |
|
|
41
|
+
| `index.ts` | imports the tokens, the seasons and `@meddleware/ui/reset.css`, and sets the season from the calendar |
|
|
42
|
+
|
|
43
|
+
The reset is declared in a cascade layer, and VitePress's own CSS is unlayered, so it wins wherever it styles an element.
|
|
44
|
+
|
|
45
|
+
## Peers
|
|
46
|
+
|
|
47
|
+
`vitepress` (exact: 2.x alpha), `vue`, `@meddleware/ui`, `@meddleware/design-tokens`.
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
`npm run build:site` builds the fixture site in `docs/`; `npm run test:e2e` runs axe (WCAG 2.2 AA and AAA) over it in every colour mode ×
|
|
52
|
+
season (`E2E_BROWSERS=all` for five browsers); `npm run shots` captures the screenshot matrix.
|
|
53
|
+
|
|
54
|
+
## Licence
|
|
55
|
+
|
|
56
|
+
0BSD.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Scope
|
|
4
|
+
|
|
5
|
+
This policy covers `@meddleware/vitepress-theme`: the VitePress theme and site config for MeddleWare documentation sites. It runs at
|
|
6
|
+
build time (the config, and a Vite plugin that edits two components of VitePress's default theme as they compile) and in the browser
|
|
7
|
+
(the theme's components and CSS). It makes no network requests of its own and handles no user data.
|
|
8
|
+
|
|
9
|
+
It does not cover a consuming site, VitePress, or the content a site publishes.
|
|
10
|
+
|
|
11
|
+
## What it guarantees
|
|
12
|
+
|
|
13
|
+
The theme maps VitePress's colours onto the design tokens and fixes two landmark problems in the default theme; its accessibility
|
|
14
|
+
checks are automated (axe, WCAG 2.2 AA and AAA) over a fixture site and are not a substitute for testing a real site with assistive
|
|
15
|
+
technology. The landmark edits are exact string replacements that fail the build, naming the file, if VitePress changes the text they
|
|
16
|
+
rely on.
|
|
17
|
+
|
|
18
|
+
## Supply chain
|
|
19
|
+
|
|
20
|
+
Releases are published from this repository's `npm-publish.yml` with npm trusted publishing (OIDC and provenance); tokens are not
|
|
21
|
+
used. The Vite plugin runs in a consumer's build, so a compromised release could run code there: report anything that suggests
|
|
22
|
+
tampering.
|
|
23
|
+
|
|
24
|
+
## Supported versions
|
|
25
|
+
|
|
26
|
+
Only the latest published npm version receives fixes.
|
|
27
|
+
|
|
28
|
+
## Reporting a vulnerability
|
|
29
|
+
|
|
30
|
+
Please **do not** open a public GitHub issue for security vulnerabilities. Use GitHub's private vulnerability reporting for this
|
|
31
|
+
repository, or email dev@meddleware.co.uk.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('vitepress').UserConfig<import('vitepress').DefaultTheme.Config>} SiteConfig
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @param {object} options
|
|
6
|
+
* @param {string} options.configUrl `import.meta.url` of the site's `docs/.vitepress/config.ts`: the source directory is `docs/` and the
|
|
7
|
+
* output the repository's `dist/` (a Dockerfile copies `dist/` into the static server).
|
|
8
|
+
* @param {string} options.title
|
|
9
|
+
* @param {string} options.description
|
|
10
|
+
* @param {import('vitepress').DefaultTheme.NavItem[]} [options.nav]
|
|
11
|
+
* @param {import('vitepress').DefaultTheme.Sidebar} [options.sidebar]
|
|
12
|
+
* @param {'dark' | true | false | 'force-dark' | 'force-auto'} [options.appearance] The colour mode a first visit gets (the toggle always
|
|
13
|
+
* works). Default `dark`, the estate's default.
|
|
14
|
+
* @param {SiteConfig['head']} [options.head] Extra `<head>` entries (the favicon and the browser-chrome colour are always there).
|
|
15
|
+
* @param {SiteConfig} [options.extra] Anything else, merged over the defaults (`themeConfig` and `vite` are merged one level down).
|
|
16
|
+
* @returns {SiteConfig}
|
|
17
|
+
*/
|
|
18
|
+
export function baseConfig({ configUrl, title, description, nav, sidebar, appearance, head, extra }: {
|
|
19
|
+
configUrl: string;
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
nav?: import("vitepress").DefaultTheme.NavItem[] | undefined;
|
|
23
|
+
sidebar?: import("vitepress").DefaultTheme.Sidebar | undefined;
|
|
24
|
+
appearance?: boolean | "dark" | "force-dark" | "force-auto" | undefined;
|
|
25
|
+
head?: SiteConfig["head"];
|
|
26
|
+
extra?: import("vitepress").UserConfig<import("vitepress").DefaultTheme.Config> | undefined;
|
|
27
|
+
}): SiteConfig;
|
|
28
|
+
export type SiteConfig = import("vitepress").UserConfig<import("vitepress").DefaultTheme.Config>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Vite plugin that applies {@link EDITS} to the default theme's components.
|
|
3
|
+
* @returns {import('vite').Plugin}
|
|
4
|
+
*/
|
|
5
|
+
export function landmarks(): import("vite").Plugin;
|
|
6
|
+
/** @type {Record<string, (code: string) => string>} */
|
|
7
|
+
export const EDITS: Record<string, (code: string) => string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,95 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meddleware/vitepress-theme",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The Meddleware VitePress theme and config: the default theme mapped onto the design tokens (colour mode, seasons, WCAG AAA contrast), a footer, and a shared site config.",
|
|
6
|
+
"license": "0BSD",
|
|
7
|
+
"author": "MeddleWare <dev@meddleware.co.uk>",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/meddleware-org/vitepress-theme.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"vitepress",
|
|
14
|
+
"theme",
|
|
15
|
+
"design-tokens",
|
|
16
|
+
"accessibility"
|
|
17
|
+
],
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"dist",
|
|
21
|
+
"CHANGELOG.md",
|
|
22
|
+
"SECURITY.md"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./src/index.ts",
|
|
27
|
+
"default": "./src/index.ts"
|
|
28
|
+
},
|
|
29
|
+
"./config": {
|
|
30
|
+
"types": "./dist/config.d.ts",
|
|
31
|
+
"default": "./src/config.js"
|
|
32
|
+
},
|
|
33
|
+
"./theme.css": "./src/theme.css"
|
|
34
|
+
},
|
|
35
|
+
"sideEffects": [
|
|
36
|
+
"*.css",
|
|
37
|
+
"*.vue"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=24"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
44
|
+
"prepublishOnly": "npm run build",
|
|
45
|
+
"type-check": "vue-tsc --noEmit",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"build:site": "vitepress build docs",
|
|
48
|
+
"lint:css": "stylelint \"src/**/*.{css,vue}\"",
|
|
49
|
+
"lint:js": "eslint .",
|
|
50
|
+
"lint:html": "html-validate \"src/**/*.vue\"",
|
|
51
|
+
"lint": "npm run lint:css && npm run lint:js && npm run lint:html",
|
|
52
|
+
"test:e2e": "npm run build:site && playwright test",
|
|
53
|
+
"shots": "SHOTS=1 playwright test"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@meddleware/design-tokens": "^0.1.13",
|
|
57
|
+
"@meddleware/ui": "^0.1.38",
|
|
58
|
+
"vitepress": "2.0.0-alpha.21",
|
|
59
|
+
"vue": "^3.5.0"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@meddleware/design-tokens": "^0.1.13",
|
|
63
|
+
"@meddleware/frontend-config": "^0.0.2",
|
|
64
|
+
"@meddleware/ui": "^0.1.38",
|
|
65
|
+
"@playwright/test": "~1.64.1",
|
|
66
|
+
"@types/node": "~26.6.5",
|
|
67
|
+
"@vue/eslint-config-typescript": "^14.9.0",
|
|
68
|
+
"autoprefixer": "^10.6.2",
|
|
69
|
+
"eslint": "^10.13.0",
|
|
70
|
+
"eslint-plugin-vue": "^10.12.0",
|
|
71
|
+
"eslint-plugin-vuejs-accessibility": "^2.6.3",
|
|
72
|
+
"html-validate": "^11.17.0",
|
|
73
|
+
"html-validate-vue": "^9.3.1",
|
|
74
|
+
"jiti": "^2.7.0",
|
|
75
|
+
"postcss": "^8.5.32",
|
|
76
|
+
"postcss-html": "^2.0.0",
|
|
77
|
+
"stylelint": "^17.16.0",
|
|
78
|
+
"stylelint-config-standard": "^40.0.0",
|
|
79
|
+
"typescript": "~6.0.0",
|
|
80
|
+
"vitepress": "2.0.0-alpha.21",
|
|
81
|
+
"vitest": "~5.0.5",
|
|
82
|
+
"vue": "^3.5.44",
|
|
83
|
+
"vue-eslint-parser": "^10.4.1",
|
|
84
|
+
"vue-tsc": "~3.3.13"
|
|
85
|
+
},
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public"
|
|
88
|
+
},
|
|
89
|
+
"browserslist": [
|
|
90
|
+
"extends @meddleware/frontend-config/browserslist-config"
|
|
91
|
+
],
|
|
92
|
+
"dependencies": {
|
|
93
|
+
"shiki": "^4.4.3"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The default layout with the Meddleware footer: the copyright line and the contact of the organisation, in a `contentinfo` landmark.
|
|
3
|
+
import DefaultTheme, { useLayout } from 'vitepress/theme'
|
|
4
|
+
import { AppFooter, CopyrightLine } from '@meddleware/ui'
|
|
5
|
+
|
|
6
|
+
const { Layout } = DefaultTheme
|
|
7
|
+
// VitePress's own footer hides itself on a page with a sidebar. This one stays (it carries the legal links), so on such a page it starts
|
|
8
|
+
// where the content does, as the content does (theme.css), instead of lying under the fixed sidebar (WCAG 2.4.11).
|
|
9
|
+
const { hasSidebar } = useLayout()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<Layout>
|
|
14
|
+
<template #layout-bottom>
|
|
15
|
+
<div class="mw-layout-footer" :class="{ 'has-sidebar': hasSidebar }">
|
|
16
|
+
<AppFooter>
|
|
17
|
+
<CopyrightLine symbol-variant="kopimi" organisation-name="Meddleware" rights-statement="jam" />
|
|
18
|
+
</AppFooter>
|
|
19
|
+
</div>
|
|
20
|
+
</template>
|
|
21
|
+
</Layout>
|
|
22
|
+
</template>
|
package/src/config.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// The site config every Meddleware VitePress site shares: its language, URLs, appearance, colours in the browser chrome, local search,
|
|
3
|
+
// social link, the high-contrast syntax themes, and the build settings that let VitePress render the design-system components.
|
|
4
|
+
//
|
|
5
|
+
// // docs/.vitepress/config.ts
|
|
6
|
+
// import { baseConfig } from '@meddleware/vitepress-theme/config'
|
|
7
|
+
// export default baseConfig({ configUrl: import.meta.url, title: 'Meddleware Docs', description: '…', nav, sidebar })
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { defineConfig } from 'vitepress'
|
|
10
|
+
import tokens from '@meddleware/design-tokens/tokens.json' with { type: 'json' }
|
|
11
|
+
import githubLight from 'shiki/themes/github-light-high-contrast.mjs'
|
|
12
|
+
import githubDark from 'shiki/themes/github-dark-high-contrast.mjs'
|
|
13
|
+
import { landmarks } from './landmarks.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A syntax theme with its comments in `colour`. The high-contrast GitHub themes keep every token at 7:1 except comments (5:1, which
|
|
17
|
+
* the theme treats as exempt); here a comment is text like any other.
|
|
18
|
+
* @param {import('shiki').ThemeRegistration} theme
|
|
19
|
+
* @param {string} colour
|
|
20
|
+
* @returns {import('shiki').ThemeRegistration}
|
|
21
|
+
*/
|
|
22
|
+
function readableComments(theme, colour) {
|
|
23
|
+
return {
|
|
24
|
+
...theme,
|
|
25
|
+
name: `${theme.name}-readable-comments`,
|
|
26
|
+
tokenColors: (theme.tokenColors ?? []).map((t) =>
|
|
27
|
+
[t.scope ?? []].flat().includes('comment') ? { ...t, settings: { ...t.settings, foreground: colour } } : t,
|
|
28
|
+
),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @typedef {import('vitepress').UserConfig<import('vitepress').DefaultTheme.Config>} SiteConfig
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {object} options
|
|
38
|
+
* @param {string} options.configUrl `import.meta.url` of the site's `docs/.vitepress/config.ts`: the source directory is `docs/` and the
|
|
39
|
+
* output the repository's `dist/` (a Dockerfile copies `dist/` into the static server).
|
|
40
|
+
* @param {string} options.title
|
|
41
|
+
* @param {string} options.description
|
|
42
|
+
* @param {import('vitepress').DefaultTheme.NavItem[]} [options.nav]
|
|
43
|
+
* @param {import('vitepress').DefaultTheme.Sidebar} [options.sidebar]
|
|
44
|
+
* @param {'dark' | true | false | 'force-dark' | 'force-auto'} [options.appearance] The colour mode a first visit gets (the toggle always
|
|
45
|
+
* works). Default `dark`, the estate's default.
|
|
46
|
+
* @param {SiteConfig['head']} [options.head] Extra `<head>` entries (the favicon and the browser-chrome colour are always there).
|
|
47
|
+
* @param {SiteConfig} [options.extra] Anything else, merged over the defaults (`themeConfig` and `vite` are merged one level down).
|
|
48
|
+
* @returns {SiteConfig}
|
|
49
|
+
*/
|
|
50
|
+
export function baseConfig({ configUrl, title, description, nav = [], sidebar = {}, appearance = 'dark', head = [], extra = {} }) {
|
|
51
|
+
const { themeConfig: extraTheme, vite: extraVite, markdown: extraMarkdown, ...rest } = extra
|
|
52
|
+
const { light, dark } = tokens.semantic
|
|
53
|
+
// The browser chrome follows the page: one colour when the site starts dark, a light and a dark one otherwise. Both are the page
|
|
54
|
+
// background of the tokens, so they cannot drift from the theme.
|
|
55
|
+
/** @type {import('vitepress').HeadConfig[]} */
|
|
56
|
+
const chrome =
|
|
57
|
+
appearance === 'dark' || appearance === 'force-dark'
|
|
58
|
+
? [['meta', { name: 'theme-color', content: dark.bg }]]
|
|
59
|
+
: [
|
|
60
|
+
['meta', { name: 'theme-color', content: light.bg, media: '(prefers-color-scheme: light)' }],
|
|
61
|
+
['meta', { name: 'theme-color', content: dark.bg, media: '(prefers-color-scheme: dark)' }],
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
return defineConfig({
|
|
65
|
+
title,
|
|
66
|
+
description,
|
|
67
|
+
lang: 'en-GB',
|
|
68
|
+
srcDir: fileURLToPath(new URL('..', configUrl)),
|
|
69
|
+
outDir: fileURLToPath(new URL('../../dist', configUrl)),
|
|
70
|
+
// The project's security audit lives in docs/audit (the audit corpus convention); it is not site content.
|
|
71
|
+
srcExclude: ['audit/**'],
|
|
72
|
+
cleanUrls: true,
|
|
73
|
+
lastUpdated: false,
|
|
74
|
+
appearance,
|
|
75
|
+
head: [
|
|
76
|
+
['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
|
|
77
|
+
...chrome,
|
|
78
|
+
...head,
|
|
79
|
+
],
|
|
80
|
+
markdown: {
|
|
81
|
+
// The high-contrast syntax themes with readable comments, so code keeps AAA contrast on the token surfaces in both colour modes.
|
|
82
|
+
theme: { light: readableComments(githubLight, light.muted), dark: readableComments(githubDark, dark.muted) },
|
|
83
|
+
...extraMarkdown,
|
|
84
|
+
},
|
|
85
|
+
vite: {
|
|
86
|
+
...extraVite,
|
|
87
|
+
// Landmark fixes for the default theme (see landmarks.js).
|
|
88
|
+
plugins: [landmarks(), ...(extraVite?.plugins ?? [])],
|
|
89
|
+
ssr: {
|
|
90
|
+
...extraVite?.ssr,
|
|
91
|
+
// The design-system packages ship Vue components and import their own CSS, which Node cannot load: Vite must process them
|
|
92
|
+
// while rendering the static pages.
|
|
93
|
+
noExternal: [/^@meddleware\//, ...(Array.isArray(extraVite?.ssr?.noExternal) ? extraVite.ssr.noExternal : [])],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
themeConfig: {
|
|
97
|
+
search: { provider: 'local' },
|
|
98
|
+
socialLinks: [{ icon: 'github', link: 'https://github.com/meddleware-org' }],
|
|
99
|
+
nav,
|
|
100
|
+
sidebar,
|
|
101
|
+
...extraTheme,
|
|
102
|
+
},
|
|
103
|
+
...rest,
|
|
104
|
+
})
|
|
105
|
+
}
|
package/src/env.d.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The Meddleware VitePress theme: the default theme + the design tokens (colour mode, seasons, AAA contrast) + a footer.
|
|
2
|
+
//
|
|
3
|
+
// // docs/.vitepress/theme/index.ts
|
|
4
|
+
// export { default } from '@meddleware/vitepress-theme'
|
|
5
|
+
//
|
|
6
|
+
// A site that needs more extends it: `{ extends: MeddlewareTheme, enhanceApp() {…} }`.
|
|
7
|
+
import DefaultTheme from 'vitepress/theme'
|
|
8
|
+
import type { Theme } from 'vitepress'
|
|
9
|
+
import { useSeason } from '@meddleware/ui'
|
|
10
|
+
import MeddlewareLayout from './MeddlewareLayout.vue'
|
|
11
|
+
import '@meddleware/design-tokens/tokens.css'
|
|
12
|
+
import '@meddleware/design-tokens/seasons.css'
|
|
13
|
+
import '@meddleware/ui/reset.css'
|
|
14
|
+
import './theme.css'
|
|
15
|
+
|
|
16
|
+
const theme: Theme = {
|
|
17
|
+
extends: DefaultTheme,
|
|
18
|
+
Layout: MeddlewareLayout,
|
|
19
|
+
enhanceApp() {
|
|
20
|
+
// Browser only (a no-op while rendering the static pages): the season follows the calendar.
|
|
21
|
+
useSeason()
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default theme
|
package/src/landmarks.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
// Landmark fixes for the VitePress default theme, applied to its source while Vite compiles it.
|
|
3
|
+
//
|
|
4
|
+
// Two of the default theme's pages fail WCAG's landmark rules, and the theme gives no slot to fix them from outside:
|
|
5
|
+
// - The home, `page` layout and 404 pages have no `<main>`: their content sits in a `div#VPContent`. The doc layout has its own
|
|
6
|
+
// `<main>`, so only the other layouts get one (two nested mains would be invalid).
|
|
7
|
+
// - The doc footer (edit link, last updated, previous/next) is a `<footer>` in the middle of the page, so it is a second `contentinfo`
|
|
8
|
+
// landmark next to the site footer. It is a plain `div`; the previous/next links are already a named `nav`.
|
|
9
|
+
//
|
|
10
|
+
// Each edit is an exact string replacement that throws when the text is not found: a VitePress upgrade that changes these files
|
|
11
|
+
// fails the build, with the name of the file, instead of silently losing the fix. `tests/landmarks.test.ts` runs the edits against the
|
|
12
|
+
// installed VitePress.
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} code
|
|
16
|
+
* @param {string} from
|
|
17
|
+
* @param {string} to
|
|
18
|
+
* @param {string} file For the error message.
|
|
19
|
+
*/
|
|
20
|
+
function replaceOnce(code, from, to, file) {
|
|
21
|
+
const first = code.indexOf(from)
|
|
22
|
+
if (first === -1 || code.indexOf(from, first + 1) !== -1) {
|
|
23
|
+
throw new Error(`@meddleware/vitepress-theme: cannot patch ${file}: expected exactly one occurrence of ${JSON.stringify(from)} (VitePress changed? update src/landmarks.js).`)
|
|
24
|
+
}
|
|
25
|
+
return code.replace(from, to)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @type {Record<string, (code: string) => string>} */
|
|
29
|
+
export const EDITS = {
|
|
30
|
+
'VPContent.vue': (code) => {
|
|
31
|
+
let out = replaceOnce(code, 'const { isHome, hasSidebar } = useLayout();', "const { isHome, hasSidebar } = useLayout();\nconst landmark = page.value.isNotFound || ['page', 'home'].includes(frontmatter.value.layout) ? 'main' : 'div';", 'VPContent.vue')
|
|
32
|
+
out = replaceOnce(out, '<div\n class="VPContent"', '<component\n :is="landmark"\n class="VPContent"', 'VPContent.vue')
|
|
33
|
+
return replaceOnce(out, '<component v-else :is="frontmatter.layout || \'doc\'" />\n </div>\n</template>', '<component v-else :is="frontmatter.layout || \'doc\'" />\n </component>\n</template>', 'VPContent.vue')
|
|
34
|
+
},
|
|
35
|
+
'VPDocFooter.vue': (code) => {
|
|
36
|
+
const out = replaceOnce(code, '<footer v-if="showFooter" class="VPDocFooter">', '<div v-if="showFooter" class="VPDocFooter">', 'VPDocFooter.vue')
|
|
37
|
+
return replaceOnce(out, '</footer>\n</template>', '</div>\n</template>', 'VPDocFooter.vue')
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A Vite plugin that applies {@link EDITS} to the default theme's components.
|
|
43
|
+
* @returns {import('vite').Plugin}
|
|
44
|
+
*/
|
|
45
|
+
export function landmarks() {
|
|
46
|
+
return {
|
|
47
|
+
name: 'meddleware:vitepress-landmarks',
|
|
48
|
+
enforce: 'pre',
|
|
49
|
+
transform(code, id) {
|
|
50
|
+
if (id.includes('?')) return null
|
|
51
|
+
const normalised = id.replaceAll('\\', '/')
|
|
52
|
+
if (!normalised.includes('/theme-default/components/')) return null
|
|
53
|
+
const edit = EDITS[normalised.slice(normalised.lastIndexOf('/') + 1)]
|
|
54
|
+
return edit ? { code: edit(code), map: null } : null
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/theme.css
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The VitePress default theme, mapped onto the Meddleware design tokens.
|
|
3
|
+
*
|
|
4
|
+
* Every colour, surface and font of the default theme is a role from @meddleware/design-tokens, so the site follows the colour mode
|
|
5
|
+
* (VitePress's `.dark` class on <html>, which the tokens honour) and the seasons, and keeps the tokens' WCAG AAA contrast (7:1 for
|
|
6
|
+
* text and labels) by construction: nothing here is a colour of its own, only a role or a mix of roles.
|
|
7
|
+
*
|
|
8
|
+
* Declared on `:root, .dark` and loaded after VitePress's own variables, so it wins over them at equal specificity.
|
|
9
|
+
*
|
|
10
|
+
* Tone rules, as in @meddleware/ui: text in a tone colour over a tint of that tone loses contrast, so a tone block or badge keeps
|
|
11
|
+
* body-colour text and shows the tone as its border. Tinted backgrounds ("-soft") are the --lift surface, on which every text role
|
|
12
|
+
* is verified at 7:1 by the token checks; only diff and warning lines in code blocks are tinted (they hold syntax-theme colours).
|
|
13
|
+
* Nothing in the palette is purple (VitePress's "important" tone is the accent here).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
:root,
|
|
17
|
+
.dark {
|
|
18
|
+
--vp-font-family-base: var(--mw-font-sans);
|
|
19
|
+
--vp-font-family-mono: var(--mw-font-mono);
|
|
20
|
+
|
|
21
|
+
/* Surfaces and text. */
|
|
22
|
+
--vp-c-bg: var(--bg);
|
|
23
|
+
--vp-c-bg-alt: var(--lift);
|
|
24
|
+
--vp-c-bg-elv: var(--surface);
|
|
25
|
+
--vp-c-bg-soft: var(--surface);
|
|
26
|
+
--vp-c-border: var(--border);
|
|
27
|
+
--vp-c-divider: var(--border);
|
|
28
|
+
--vp-c-gutter: var(--lift);
|
|
29
|
+
--vp-c-text-1: var(--text);
|
|
30
|
+
--vp-c-text-2: var(--muted);
|
|
31
|
+
--vp-c-text-3: var(--muted);
|
|
32
|
+
|
|
33
|
+
/* Neutral ("default") family: buttons, code backgrounds. */
|
|
34
|
+
--vp-c-gray-1: var(--border);
|
|
35
|
+
--vp-c-gray-2: var(--lift);
|
|
36
|
+
--vp-c-gray-3: var(--surface);
|
|
37
|
+
--vp-c-gray-soft: var(--lift);
|
|
38
|
+
|
|
39
|
+
/* Brand: the accent. -1 is text on the page, -3 a fill with --accent-contrast text, -2 the hover state (toward the text colour, so
|
|
40
|
+
contrast only grows), -soft a tint. */
|
|
41
|
+
--vp-c-brand-1: var(--accent);
|
|
42
|
+
--vp-c-brand-2: color-mix(in srgb, var(--accent) 82%, var(--text));
|
|
43
|
+
--vp-c-brand-3: var(--accent);
|
|
44
|
+
--vp-c-brand-soft: var(--lift);
|
|
45
|
+
|
|
46
|
+
/* Tones. */
|
|
47
|
+
--vp-c-tip-1: var(--secondary);
|
|
48
|
+
--vp-c-tip-2: color-mix(in srgb, var(--secondary) 82%, var(--text));
|
|
49
|
+
--vp-c-tip-3: var(--secondary);
|
|
50
|
+
--vp-c-tip-soft: var(--lift);
|
|
51
|
+
--vp-c-note-1: var(--secondary);
|
|
52
|
+
--vp-c-note-2: color-mix(in srgb, var(--secondary) 82%, var(--text));
|
|
53
|
+
--vp-c-note-3: var(--secondary);
|
|
54
|
+
--vp-c-note-soft: var(--lift);
|
|
55
|
+
--vp-c-success-1: var(--ok);
|
|
56
|
+
--vp-c-success-2: color-mix(in srgb, var(--ok) 82%, var(--text));
|
|
57
|
+
--vp-c-success-3: var(--ok);
|
|
58
|
+
--vp-c-success-soft: var(--lift);
|
|
59
|
+
--vp-c-important-1: var(--accent);
|
|
60
|
+
--vp-c-important-2: color-mix(in srgb, var(--accent) 82%, var(--text));
|
|
61
|
+
--vp-c-important-3: var(--accent);
|
|
62
|
+
--vp-c-important-soft: var(--lift);
|
|
63
|
+
--vp-c-warning-1: var(--warning-text);
|
|
64
|
+
--vp-c-warning-2: color-mix(in srgb, var(--warning-text) 82%, var(--text));
|
|
65
|
+
--vp-c-warning-3: var(--warning-text);
|
|
66
|
+
--vp-c-warning-soft: var(--lift);
|
|
67
|
+
--vp-c-danger-1: var(--danger);
|
|
68
|
+
--vp-c-danger-2: color-mix(in srgb, var(--danger) 82%, var(--text));
|
|
69
|
+
--vp-c-danger-3: var(--danger);
|
|
70
|
+
--vp-c-danger-soft: var(--lift);
|
|
71
|
+
--vp-c-caution-1: var(--danger);
|
|
72
|
+
--vp-c-caution-2: color-mix(in srgb, var(--danger) 82%, var(--text));
|
|
73
|
+
--vp-c-caution-3: var(--danger);
|
|
74
|
+
--vp-c-caution-soft: var(--lift);
|
|
75
|
+
|
|
76
|
+
/* Buttons: a brand fill carries the label colour made for it, not white. */
|
|
77
|
+
--vp-button-brand-text: var(--accent-contrast);
|
|
78
|
+
--vp-button-brand-hover-text: var(--accent-contrast);
|
|
79
|
+
--vp-button-brand-active-text: var(--accent-contrast);
|
|
80
|
+
--vp-button-brand-bg: var(--accent);
|
|
81
|
+
--vp-button-brand-hover-bg: color-mix(in srgb, var(--accent) 82%, var(--text));
|
|
82
|
+
--vp-button-brand-active-bg: color-mix(in srgb, var(--accent) 66%, var(--text));
|
|
83
|
+
--vp-button-alt-text: var(--text);
|
|
84
|
+
--vp-button-alt-hover-text: var(--text);
|
|
85
|
+
--vp-button-alt-active-text: var(--text);
|
|
86
|
+
|
|
87
|
+
/* Custom blocks and badges: body-colour text; the tone is the border and the tint. */
|
|
88
|
+
--vp-custom-block-info-border: var(--border);
|
|
89
|
+
--vp-custom-block-tip-border: var(--secondary);
|
|
90
|
+
--vp-custom-block-tip-text: var(--text);
|
|
91
|
+
--vp-custom-block-note-border: var(--secondary);
|
|
92
|
+
--vp-custom-block-note-text: var(--text);
|
|
93
|
+
--vp-custom-block-important-border: var(--accent);
|
|
94
|
+
--vp-custom-block-important-text: var(--text);
|
|
95
|
+
--vp-custom-block-warning-border: var(--warning-text);
|
|
96
|
+
--vp-custom-block-warning-text: var(--text);
|
|
97
|
+
--vp-custom-block-danger-border: var(--danger);
|
|
98
|
+
--vp-custom-block-danger-text: var(--text);
|
|
99
|
+
--vp-custom-block-caution-border: var(--danger);
|
|
100
|
+
--vp-custom-block-caution-text: var(--text);
|
|
101
|
+
--vp-badge-info-border: var(--border);
|
|
102
|
+
--vp-badge-tip-border: var(--secondary);
|
|
103
|
+
--vp-badge-tip-text: var(--text);
|
|
104
|
+
--vp-badge-note-border: var(--secondary);
|
|
105
|
+
--vp-badge-note-text: var(--text);
|
|
106
|
+
--vp-badge-important-border: var(--accent);
|
|
107
|
+
--vp-badge-important-text: var(--text);
|
|
108
|
+
--vp-badge-warning-border: var(--warning-text);
|
|
109
|
+
--vp-badge-warning-text: var(--text);
|
|
110
|
+
--vp-badge-danger-border: var(--danger);
|
|
111
|
+
--vp-badge-danger-text: var(--text);
|
|
112
|
+
|
|
113
|
+
/* Code: inline code in body colour (links in code keep the accent); blocks on the surface, with the high-contrast syntax themes. */
|
|
114
|
+
--vp-code-color: var(--text);
|
|
115
|
+
--vp-code-bg: var(--lift);
|
|
116
|
+
--vp-code-block-bg: var(--surface);
|
|
117
|
+
--vp-code-block-color: var(--text);
|
|
118
|
+
--vp-code-line-highlight-color: var(--lift);
|
|
119
|
+
--vp-code-line-diff-add-color: color-mix(in srgb, var(--ok) 12%, var(--surface));
|
|
120
|
+
--vp-code-line-diff-remove-color: color-mix(in srgb, var(--danger) 12%, var(--surface));
|
|
121
|
+
--vp-code-line-warning-color: color-mix(in srgb, var(--warning-text) 12%, var(--surface));
|
|
122
|
+
--vp-code-line-error-color: color-mix(in srgb, var(--danger) 12%, var(--surface));
|
|
123
|
+
|
|
124
|
+
/* Home hero: the accent into the primary, both of which pass contrast on the page in every look. */
|
|
125
|
+
--vp-home-hero-name-color: transparent;
|
|
126
|
+
--vp-home-hero-name-background: linear-gradient(135deg, var(--accent), var(--primary));
|
|
127
|
+
--vp-home-hero-image-background-image: linear-gradient(135deg, var(--accent), var(--primary));
|
|
128
|
+
--vp-home-hero-image-filter: blur(var(--space-lg));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Text enlarged to 200% on a wide screen: the doc columns are laid out in pixels while their content grows, so the main column kept its
|
|
132
|
+
desktop width and the page scrolled sideways. Letting the flex children of the doc container shrink (and long lines wrap or scroll inside
|
|
133
|
+
their own blocks) keeps the page at the viewport's width. */
|
|
134
|
+
.VPDoc .container,
|
|
135
|
+
.VPDoc .container > *,
|
|
136
|
+
.VPDoc .content-container,
|
|
137
|
+
.VPDoc .content {
|
|
138
|
+
min-width: 0;
|
|
139
|
+
}
|
|
140
|
+
.VPDoc .container {
|
|
141
|
+
max-width: 100%;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* A feature card on the home page is one link around a title, a description and an action. Links are underlined (a colour is never the only
|
|
145
|
+
cue), but underlining the whole paragraph makes the card unreadable: the title and the action carry the underline. */
|
|
146
|
+
a.VPFeature {
|
|
147
|
+
text-decoration: none;
|
|
148
|
+
}
|
|
149
|
+
a.VPFeature .title,
|
|
150
|
+
a.VPFeature .link-text {
|
|
151
|
+
text-decoration: underline;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* The footer is below the page content, and the sidebar is fixed and runs the full height of the viewport (from 60rem): on a page that has
|
|
155
|
+
* one, the footer starts where the content does (the same offsets as VitePress's VPContent), or its links are covered. */
|
|
156
|
+
@media (width >= 60rem) {
|
|
157
|
+
.mw-layout-footer.has-sidebar {
|
|
158
|
+
padding-inline-start: var(--vp-sidebar-width);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
@media (width >= 90rem) {
|
|
163
|
+
.mw-layout-footer.has-sidebar {
|
|
164
|
+
padding-inline-start: calc((100% - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
|
|
165
|
+
}
|
|
166
|
+
}
|