@chassis-ui/docs 0.6.0 → 0.6.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 +17 -0
- package/README.md +149 -20
- package/bin/chassis-docs.js +6 -0
- package/package.json +16 -2
- package/src/cli/assets.js +111 -0
- package/src/cli/files.js +97 -0
- package/src/cli/html-validate.js +111 -0
- package/src/cli/main.js +131 -0
- package/src/cli/shared.js +69 -0
- package/src/cli/vnu.js +94 -0
- package/src/components/shortcodes/Code.astro +2 -2
- package/src/components/shortcodes/Example.astro +2 -1
- package/src/components/shortcodes/Icon.astro +3 -1
- package/src/components/shortcodes/ResizableExample.astro +2 -1
- package/src/integration.ts +6 -0
- package/src/js/example-mode.js +3 -2
- package/src/js/search.js +2 -1
- package/src/layouts/BaseLayout.astro +5 -2
- package/src/layouts/footer/Consent.astro +135 -0
- package/src/layouts/footer/Footer.astro +14 -4
- package/src/layouts/head/Favicons.astro +7 -6
- package/src/layouts/head/Head.astro +8 -2
- package/src/layouts/head/Social.astro +5 -2
- package/src/layouts/head/Stylesheet.astro +3 -2
- package/src/layouts/header/Navigation.astro +3 -2
- package/src/libs/docs-path.ts +7 -0
- package/src/libs/highlight.ts +2 -2
- package/src/libs/icon.ts +4 -4
- package/src/libs/markdown.ts +13 -1
- package/src/libs/schema.ts +11 -1
- package/src/scss/_consent.scss +14 -0
- package/src/scss/_footer.scss +11 -1
- package/src/scss/main.scss +1 -1
- package/src/site.ts +9 -1
- package/virtual.d.ts +5 -0
- package/src/layouts/head/Analytics.astro +0 -31
- package/src/scss/fonts.scss +0 -1
package/src/cli/main.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util'
|
|
2
|
+
import { DEFAULT_BRANCH, buildAssets, syncAssets } from './assets.js'
|
|
3
|
+
import { DEFAULT_PATHS, defaultIgnores } from './files.js'
|
|
4
|
+
import { htmlValidate } from './html-validate.js'
|
|
5
|
+
import { CommandError, log } from './shared.js'
|
|
6
|
+
import { vnu } from './vnu.js'
|
|
7
|
+
|
|
8
|
+
export const usage = `Usage: chassis-docs <command> [options]
|
|
9
|
+
|
|
10
|
+
Paths are relative to the working directory. vendor and sync-submodules work from any
|
|
11
|
+
folder of the repository.
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
vendor Build vendor/assets at the commit that the repository pins
|
|
15
|
+
sync-submodules Move vendor/assets to the latest commit of a branch, and build it
|
|
16
|
+
--branch <name> The branch of chassis-assets (default: $SUBMODULE_BRANCH or ${DEFAULT_BRANCH})
|
|
17
|
+
html-validate [paths] Validate the HTML files in paths (default: _site) with html-validate
|
|
18
|
+
--config <file> A JSON config of html-validate, added to the defaults
|
|
19
|
+
vnu [paths] Validate paths (default: _site) with the Nu Html Checker. Needs Java
|
|
20
|
+
--filter <regex> Leave out the messages that match. Repeatable
|
|
21
|
+
--filter-file <f> Read filters from a file, one per line
|
|
22
|
+
|
|
23
|
+
Both validators:
|
|
24
|
+
--ignore <path> Skip a file or folder as well. Repeatable
|
|
25
|
+
--no-default-ignore Check static/icons of each path, which is skipped by default
|
|
26
|
+
help Show this message
|
|
27
|
+
|
|
28
|
+
html-validate and vnu need the html-validate and vnu-jar packages in the site's devDependencies.`
|
|
29
|
+
|
|
30
|
+
const ignoreOptions = {
|
|
31
|
+
ignore: { type: 'string', multiple: true },
|
|
32
|
+
'no-default-ignore': { type: 'boolean' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The static/icons folder of each path, unless left out, and the paths of --ignore.
|
|
36
|
+
function ignoresOf(values, paths) {
|
|
37
|
+
return [...(values['no-default-ignore'] ? [] : defaultIgnores(paths)), ...(values.ignore ?? [])]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const commands = {
|
|
41
|
+
vendor: {
|
|
42
|
+
options: {},
|
|
43
|
+
run: () => {
|
|
44
|
+
buildAssets()
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
'sync-submodules': {
|
|
49
|
+
options: { branch: { type: 'string' } },
|
|
50
|
+
run: ({ values }) => {
|
|
51
|
+
syncAssets({ branch: values.branch ?? process.env.SUBMODULE_BRANCH ?? DEFAULT_BRANCH })
|
|
52
|
+
return 0
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
'html-validate': {
|
|
56
|
+
options: { config: { type: 'string' }, ...ignoreOptions },
|
|
57
|
+
positionals: true,
|
|
58
|
+
run: ({ values, paths }) =>
|
|
59
|
+
htmlValidate({
|
|
60
|
+
paths,
|
|
61
|
+
ignores: ignoresOf(values, paths),
|
|
62
|
+
config: values.config
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
vnu: {
|
|
66
|
+
options: {
|
|
67
|
+
filter: { type: 'string', multiple: true },
|
|
68
|
+
'filter-file': { type: 'string' },
|
|
69
|
+
...ignoreOptions
|
|
70
|
+
},
|
|
71
|
+
positionals: true,
|
|
72
|
+
run: ({ values, paths }) =>
|
|
73
|
+
vnu({
|
|
74
|
+
paths,
|
|
75
|
+
ignores: ignoresOf(values, paths),
|
|
76
|
+
filters: values.filter,
|
|
77
|
+
filterFile: values['filter-file']
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Runs a command and returns its exit code.
|
|
84
|
+
* @param {string[]} argv The arguments after `chassis-docs`
|
|
85
|
+
* @returns {Promise<number>}
|
|
86
|
+
*/
|
|
87
|
+
export async function main(argv) {
|
|
88
|
+
const [name, ...args] = argv
|
|
89
|
+
|
|
90
|
+
if (!name || ['help', '--help', '-h'].includes(name)) {
|
|
91
|
+
console.log(usage)
|
|
92
|
+
return 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const command = commands[name]
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
if (!Object.hasOwn(commands, name)) {
|
|
99
|
+
throw new CommandError(`Unknown command '${name}'. Run chassis-docs help for the list.`)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let parsed
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
parsed = parseArgs({
|
|
106
|
+
args,
|
|
107
|
+
options: { ...command.options, help: { type: 'boolean', short: 'h' } },
|
|
108
|
+
allowPositionals: command.positionals ?? false,
|
|
109
|
+
strict: true
|
|
110
|
+
})
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new CommandError(`${error.message}. Run chassis-docs help for the options.`, {
|
|
113
|
+
cause: error
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (parsed.values.help) {
|
|
118
|
+
console.log(usage)
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const paths = parsed.positionals.length > 0 ? parsed.positionals : DEFAULT_PATHS
|
|
123
|
+
|
|
124
|
+
return await command.run({ values: parsed.values, paths })
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (!(error instanceof CommandError)) throw error
|
|
127
|
+
|
|
128
|
+
log(error.message, 'error')
|
|
129
|
+
return 1
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { styleText } from 'node:util'
|
|
3
|
+
|
|
4
|
+
/** An error that the command reports in one line, without a stack trace. */
|
|
5
|
+
export class CommandError extends Error {}
|
|
6
|
+
|
|
7
|
+
const colors = { info: 'cyan', success: 'green', warning: 'yellow', error: 'red' }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Prints a message in the colour of its type. Errors and warnings go to stderr.
|
|
11
|
+
* @param {string} message
|
|
12
|
+
* @param {'info' | 'success' | 'warning' | 'error'} [type]
|
|
13
|
+
*/
|
|
14
|
+
export function log(message, type = 'info') {
|
|
15
|
+
const stream = type === 'error' || type === 'warning' ? process.stderr : process.stdout
|
|
16
|
+
|
|
17
|
+
stream.write(`${styleText(colors[type], message)}\n`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runs a program without a shell, except on Windows, where `pnpm` is a `.cmd` file.
|
|
22
|
+
* Returns what it printed when `quiet` is set, and prints it otherwise.
|
|
23
|
+
* @param {string} command
|
|
24
|
+
* @param {string[]} args
|
|
25
|
+
* @param {{ cwd?: string, quiet?: boolean }} [options]
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
export function run(command, args, { cwd = process.cwd(), quiet = false } = {}) {
|
|
29
|
+
if (!quiet) {
|
|
30
|
+
log(`$ ${[command, ...args].join(' ')}`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return (
|
|
35
|
+
execFileSync(command, args, {
|
|
36
|
+
cwd,
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
stdio: quiet ? 'pipe' : 'inherit',
|
|
39
|
+
shell: process.platform === 'win32'
|
|
40
|
+
}) ?? ''
|
|
41
|
+
)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const stderr = typeof error.stderr === 'string' ? error.stderr.trim() : ''
|
|
44
|
+
|
|
45
|
+
throw new CommandError(
|
|
46
|
+
`${[command, ...args].join(' ')} failed${stderr ? `:\n${stderr}` : ''}`,
|
|
47
|
+
{ cause: error }
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Imports an optional peer dependency, or says how to install it.
|
|
54
|
+
* @param {string} name
|
|
55
|
+
*/
|
|
56
|
+
export async function importPeer(name) {
|
|
57
|
+
try {
|
|
58
|
+
return await import(name)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND' && error.message.includes(`'${name}'`)) {
|
|
61
|
+
throw new CommandError(
|
|
62
|
+
`${name} is not installed. Add it to the devDependencies of the site: pnpm add -D ${name}`,
|
|
63
|
+
{ cause: error }
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/cli/vnu.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execFile, spawn } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import { CommandError, importPeer, log } from './shared.js'
|
|
4
|
+
import { expandPaths } from './files.js'
|
|
5
|
+
|
|
6
|
+
/** Messages that every Chassis site produces. Astro writes void elements with a slash. */
|
|
7
|
+
export const DEFAULT_FILTERS = ['Trailing slash on void elements.*']
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Reads a filter file: one regular expression per line. Blank lines and lines that start
|
|
11
|
+
* with `#` are skipped.
|
|
12
|
+
* @param {string} file
|
|
13
|
+
*/
|
|
14
|
+
export function readFilterFile(file) {
|
|
15
|
+
let text
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
text = fs.readFileSync(file, 'utf8')
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new CommandError(`Could not read ${file}: ${error.message}`, { cause: error })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return text
|
|
24
|
+
.split(/\r?\n/)
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The arguments for `java`.
|
|
31
|
+
* @param {{ jar: string, paths: string[], filters: string[], is32bit?: boolean }} options
|
|
32
|
+
*/
|
|
33
|
+
export function vnuArgs({ jar, paths, filters, is32bit = false }) {
|
|
34
|
+
return [
|
|
35
|
+
// 32-bit Java needs a smaller thread stack
|
|
36
|
+
...(is32bit ? ['-Xss512k'] : []),
|
|
37
|
+
'-jar',
|
|
38
|
+
jar,
|
|
39
|
+
'--skip-non-html',
|
|
40
|
+
'--Werror',
|
|
41
|
+
'--filterpattern',
|
|
42
|
+
filters.join('|'),
|
|
43
|
+
...paths
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function javaVersion() {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
// `java -version` prints to stderr
|
|
50
|
+
execFile('java', ['-version'], (error, _stdout, stderr) => resolve(error ? null : stderr))
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validates `paths` with the Nu Html Checker. Skips, and succeeds, when Java is missing.
|
|
56
|
+
* Returns the exit code.
|
|
57
|
+
* @param {{ paths: string[], ignores: string[], filters?: string[], filterFile?: string }} options
|
|
58
|
+
*/
|
|
59
|
+
export async function vnu({ paths, ignores, filters = [], filterFile }) {
|
|
60
|
+
const { default: jar } = await importPeer('vnu-jar')
|
|
61
|
+
const allFilters = [
|
|
62
|
+
...DEFAULT_FILTERS,
|
|
63
|
+
...filters,
|
|
64
|
+
...(filterFile ? readFilterFile(filterFile) : [])
|
|
65
|
+
]
|
|
66
|
+
const targets = expandPaths(paths, ignores)
|
|
67
|
+
const version = await javaVersion()
|
|
68
|
+
|
|
69
|
+
if (version === null) {
|
|
70
|
+
log('Java is not installed. Skipping the Nu Html Checker.', 'warning')
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
log('Validating HTML with the Nu Html Checker')
|
|
75
|
+
|
|
76
|
+
const args = vnuArgs({
|
|
77
|
+
jar,
|
|
78
|
+
paths: targets,
|
|
79
|
+
filters: allFilters,
|
|
80
|
+
is32bit: !/64-Bit/.test(version)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const code = await new Promise((resolve) => {
|
|
84
|
+
spawn('java', args, { stdio: 'inherit' }).on('exit', (exitCode) => resolve(exitCode ?? 1))
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (code === 0) {
|
|
88
|
+
log('Every HTML file is valid.', 'success')
|
|
89
|
+
} else {
|
|
90
|
+
log('The Nu Html Checker found errors.', 'error')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return code
|
|
94
|
+
}
|
|
@@ -226,7 +226,7 @@ if (codeToDisplay && detectedLang) {
|
|
|
226
226
|
) : (
|
|
227
227
|
<div class:list={['cxd-code', { 'line-numbers': lineNumbers }]}>
|
|
228
228
|
{/* prettier-ignore */}
|
|
229
|
-
<pre class={className}><slot /></pre>
|
|
229
|
+
<pre class={className} tabindex="0"><slot /></pre>
|
|
230
230
|
</div>
|
|
231
231
|
)}
|
|
232
232
|
</>
|
|
@@ -279,7 +279,7 @@ if (codeToDisplay && detectedLang) {
|
|
|
279
279
|
) : (
|
|
280
280
|
<div class:list={['cxd-code', { 'line-numbers': lineNumbers }]}>
|
|
281
281
|
{/* prettier-ignore */}
|
|
282
|
-
<pre class={className}><slot /></pre>
|
|
282
|
+
<pre class={className} tabindex="0"><slot /></pre>
|
|
283
283
|
</div>
|
|
284
284
|
)}
|
|
285
285
|
</div>
|
|
@@ -3,6 +3,7 @@ import { replacePlaceholdersInHtml } from '../../libs/placeholder'
|
|
|
3
3
|
import { replaceIconsInHtml } from '../../libs/icon'
|
|
4
4
|
import Code from './Code.astro'
|
|
5
5
|
import Icon from './Icon.astro'
|
|
6
|
+
import { getStaticPath } from '../../site'
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
/**
|
|
@@ -69,7 +70,7 @@ const {
|
|
|
69
70
|
|
|
70
71
|
let markup = Array.isArray(code) ? code.join('\n') : code
|
|
71
72
|
markup = replacePlaceholdersInHtml(markup)
|
|
72
|
-
markup = replaceIconsInHtml(markup)
|
|
73
|
+
markup = replaceIconsInHtml(markup, getStaticPath())
|
|
73
74
|
markup = markup
|
|
74
75
|
.split('\n')
|
|
75
76
|
.map((line) => line.trimEnd())
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { getStaticPath } from '../../site'
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* Icon
|
|
4
6
|
*
|
|
@@ -29,7 +31,7 @@ const {
|
|
|
29
31
|
size = 24,
|
|
30
32
|
font = false,
|
|
31
33
|
title,
|
|
32
|
-
sprite = '
|
|
34
|
+
sprite = getStaticPath('icons/chassis-icons.svg'),
|
|
33
35
|
...rest
|
|
34
36
|
} = Astro.props
|
|
35
37
|
---
|
|
@@ -3,6 +3,7 @@ import { replacePlaceholdersInHtml } from '../../libs/placeholder'
|
|
|
3
3
|
import { replaceIconsInHtml } from '../../libs/icon'
|
|
4
4
|
import Code from './Code.astro'
|
|
5
5
|
import Icon from './Icon.astro'
|
|
6
|
+
import { getStaticPath } from '../../site'
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
/**
|
|
@@ -67,7 +68,7 @@ const {
|
|
|
67
68
|
|
|
68
69
|
let markup = Array.isArray(code) ? code.join('\n') : code
|
|
69
70
|
markup = replacePlaceholdersInHtml(markup)
|
|
70
|
-
markup = replaceIconsInHtml(markup)
|
|
71
|
+
markup = replaceIconsInHtml(markup, getStaticPath())
|
|
71
72
|
|
|
72
73
|
// The resizable container needs `overflow: hidden` for the CSS `resize` handle
|
|
73
74
|
// to work, which clips popovers like menus/dropdowns when they open. Render them
|
package/src/integration.ts
CHANGED
|
@@ -90,6 +90,10 @@ declare module 'virtual:chassis-docs/paths' {
|
|
|
90
90
|
export const packageRoot: string
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
declare module 'virtual:chassis-docs/static' {
|
|
94
|
+
export const staticPath: string
|
|
95
|
+
}
|
|
96
|
+
|
|
93
97
|
declare module 'virtual:chassis-docs/styles' {}
|
|
94
98
|
`
|
|
95
99
|
|
|
@@ -234,6 +238,8 @@ export function chassisDocs<TSchema extends z.ZodType = typeof configSchema>(
|
|
|
234
238
|
paths: Object.entries(paths)
|
|
235
239
|
.map(([name, value]) => `export const ${name} = ${JSON.stringify(value)}`)
|
|
236
240
|
.join('\n'),
|
|
241
|
+
// A config that was not loaded with `loadConfig()` may lack the default.
|
|
242
|
+
static: `export const staticPath = ${JSON.stringify(config.staticPath ?? '/static')}`,
|
|
237
243
|
styles: styles.map((file) => `import ${JSON.stringify(file)}`).join('\n')
|
|
238
244
|
})
|
|
239
245
|
]
|
package/src/js/example-mode.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tooltip } from '@chassis-ui/css'
|
|
2
|
+
import { staticPath } from 'virtual:chassis-docs/static'
|
|
2
3
|
|
|
3
4
|
// Helper to detect the user's preferred theme
|
|
4
5
|
function getPreferredTheme() {
|
|
@@ -43,7 +44,7 @@ function toggleTheme(button) {
|
|
|
43
44
|
const iconUse = button.querySelector('use')
|
|
44
45
|
if (iconUse) {
|
|
45
46
|
const iconHref = newTheme === 'dark' ? 'sun-solid' : 'moon-solid'
|
|
46
|
-
iconUse.setAttribute('href',
|
|
47
|
+
iconUse.setAttribute('href', `${staticPath}/icons/chassis-icons.svg#${iconHref}`)
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -64,7 +65,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
64
65
|
const example = button.closest('.cxd-example-snippet')?.querySelector('.cxd-example')
|
|
65
66
|
const currentTheme = example?.getAttribute('data-cx-theme') || preferredTheme
|
|
66
67
|
const iconHref = currentTheme === 'dark' ? 'sun-solid' : 'moon-solid'
|
|
67
|
-
iconUse.setAttribute('href',
|
|
68
|
+
iconUse.setAttribute('href', `${staticPath}/icons/chassis-icons.svg#${iconHref}`)
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
// Add event listener if not already set through onclick
|
package/src/js/search.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// is needed here.
|
|
14
14
|
|
|
15
15
|
import { getInstanceManager } from '@pagefind/component-ui'
|
|
16
|
+
import { staticPath } from 'virtual:chassis-docs/static'
|
|
16
17
|
|
|
17
18
|
const DIALOG_SELECTOR = '#cxdSearchDialog'
|
|
18
19
|
const SUB_RESULTS_LIMIT = 3
|
|
@@ -233,7 +234,7 @@ const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) =>
|
|
|
233
234
|
const renderItem = ({ url, titleHtml, excerptHtml = '', icon, sub = false }) => `
|
|
234
235
|
<a class="cxd-search-item${sub ? ' cxd-search-subitem' : ''}" href="${escapeHtml(url)}">
|
|
235
236
|
<svg class="icon cxd-search-item-icon" width="16" height="16" aria-hidden="true">
|
|
236
|
-
<use href="/
|
|
237
|
+
<use href="${staticPath}/icons/chassis-icons.svg#${icon}"></use>
|
|
237
238
|
</svg>
|
|
238
239
|
<div class="cxd-search-item-body">
|
|
239
240
|
<div class="cxd-search-item-title">${titleHtml}</div>
|
|
@@ -12,6 +12,7 @@ import Head from './head/Head.astro'
|
|
|
12
12
|
import Header from './header/Header.astro'
|
|
13
13
|
import Scripts from './footer/Scripts.astro'
|
|
14
14
|
import Footer from './footer/Footer.astro'
|
|
15
|
+
import Consent from './footer/Consent.astro'
|
|
15
16
|
import { stripMarkdown } from '../libs/utils'
|
|
16
17
|
|
|
17
18
|
// The following props can be directly passed to the base layout component from any page or layout extending it,
|
|
@@ -25,7 +26,8 @@ type Props = {
|
|
|
25
26
|
overrides?: {
|
|
26
27
|
body?: LayoutOverridesHTMLAttributes<'body'>
|
|
27
28
|
// Note that main can also be overridden by the Astro slot named "main" and that the slot will take precedence over
|
|
28
|
-
// any override.
|
|
29
|
+
// any override. `<main>` has `id="content"`, the target of the skip link in the header. A page that fills the "main"
|
|
30
|
+
// slot gives that id to an element of its own.
|
|
29
31
|
main?: LayoutOverridesHTMLAttributes<'main'>
|
|
30
32
|
}
|
|
31
33
|
// An override for the page title. If not defined, the title will either be the content of the `title` frontmatter
|
|
@@ -61,7 +63,7 @@ const thumbnail = frontmatter?.thumbnail
|
|
|
61
63
|
: 'static/images/social-image.png'
|
|
62
64
|
|
|
63
65
|
const bodyProps = overrides?.body ?? {}
|
|
64
|
-
const mainProps = overrides?.main
|
|
66
|
+
const mainProps = { id: 'content', ...overrides?.main }
|
|
65
67
|
---
|
|
66
68
|
|
|
67
69
|
<!doctype html>
|
|
@@ -90,6 +92,7 @@ const mainProps = overrides?.main ?? {}
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
<Footer />
|
|
95
|
+
<Consent />
|
|
93
96
|
|
|
94
97
|
<Scripts layout={layout}>
|
|
95
98
|
<slot name="scripts" slot="scripts" />
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Consent
|
|
4
|
+
*
|
|
5
|
+
* Asks before Google Analytics loads. Rendered in production builds, when
|
|
6
|
+
* `analytics.googleId` is set in `config.yml`. Nothing is loaded and no cookie is set until
|
|
7
|
+
* the visitor accepts. The choice is kept in `localStorage`, so every Chassis site served
|
|
8
|
+
* from the same host shares it. The "Cookie settings" button of the footer opens the
|
|
9
|
+
* banner again.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getConfig } from '../../site'
|
|
13
|
+
|
|
14
|
+
const isProd = import.meta.env.PROD
|
|
15
|
+
const { googleId } = getConfig().analytics
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
{
|
|
19
|
+
isProd && googleId && (
|
|
20
|
+
<section class="cxd-consent" aria-label="Cookie consent" data-google-id={googleId} hidden>
|
|
21
|
+
<div class="container 2xl">
|
|
22
|
+
<div class="notification default shadow-lg d-flex flex-wrap align-items-center gap-md p-md">
|
|
23
|
+
<p class="mb-0 flex-grow-1">
|
|
24
|
+
We would like to count visits with Google Analytics, which sets cookies. Nothing is sent
|
|
25
|
+
to Google unless you accept. <a href="/privacy/">Privacy notice</a>
|
|
26
|
+
</p>
|
|
27
|
+
<div class="d-flex gap-sm">
|
|
28
|
+
<button type="button" class="button primary" data-consent="granted">
|
|
29
|
+
Accept
|
|
30
|
+
</button>
|
|
31
|
+
<button type="button" class="button default" data-consent="denied">
|
|
32
|
+
Decline
|
|
33
|
+
</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</section>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
<script>
|
|
42
|
+
type AnalyticsWindow = Window &
|
|
43
|
+
Record<`ga-disable-${string}`, boolean> & {
|
|
44
|
+
dataLayer?: unknown[]
|
|
45
|
+
gtag?: (...args: unknown[]) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const KEY = 'chassis-analytics-consent'
|
|
49
|
+
const analytics = window as unknown as AnalyticsWindow
|
|
50
|
+
const banner = document.querySelector<HTMLElement>('.cxd-consent')
|
|
51
|
+
const googleId = banner?.dataset.googleId
|
|
52
|
+
|
|
53
|
+
function readChoice(): string | null {
|
|
54
|
+
try {
|
|
55
|
+
return localStorage.getItem(KEY)
|
|
56
|
+
} catch {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saveChoice(choice: string) {
|
|
62
|
+
try {
|
|
63
|
+
localStorage.setItem(KEY, choice)
|
|
64
|
+
} catch {
|
|
65
|
+
// Without storage the banner shows again on the next page
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function loadAnalytics(id: string) {
|
|
70
|
+
analytics[`ga-disable-${id}`] = false
|
|
71
|
+
if (analytics.gtag) return
|
|
72
|
+
|
|
73
|
+
analytics.dataLayer ??= []
|
|
74
|
+
analytics.gtag = function () {
|
|
75
|
+
// gtag.js reads the `arguments` object, not an array
|
|
76
|
+
// eslint-disable-next-line prefer-rest-params
|
|
77
|
+
analytics.dataLayer!.push(arguments)
|
|
78
|
+
}
|
|
79
|
+
analytics.gtag('js', new Date())
|
|
80
|
+
analytics.gtag('config', id)
|
|
81
|
+
|
|
82
|
+
const script = document.createElement('script')
|
|
83
|
+
script.async = true
|
|
84
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(id)}`
|
|
85
|
+
document.head.append(script)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Stops Google Analytics on this page and deletes its cookies, which it sets on the
|
|
89
|
+
// registrable domain, such as `.chassis-ui.com`
|
|
90
|
+
function stopAnalytics(id: string) {
|
|
91
|
+
analytics[`ga-disable-${id}`] = true
|
|
92
|
+
|
|
93
|
+
const labels = location.hostname.split('.')
|
|
94
|
+
const domains = labels.slice(0, -1).map((_, index) => labels.slice(index).join('.'))
|
|
95
|
+
|
|
96
|
+
for (const cookie of document.cookie.split(';')) {
|
|
97
|
+
const name = cookie.split('=')[0].trim()
|
|
98
|
+
|
|
99
|
+
if (!name.startsWith('_ga')) continue
|
|
100
|
+
|
|
101
|
+
for (const domain of ['', ...domains.map((d) => `; domain=.${d}`)]) {
|
|
102
|
+
document.cookie = `${name}=; max-age=0; path=/${domain}`
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (banner && googleId) {
|
|
108
|
+
const choice = readChoice()
|
|
109
|
+
|
|
110
|
+
if (choice === 'granted') loadAnalytics(googleId)
|
|
111
|
+
else if (choice !== 'denied') banner.hidden = false
|
|
112
|
+
|
|
113
|
+
banner.addEventListener('click', (event) => {
|
|
114
|
+
const button = (event.target as Element).closest<HTMLElement>('[data-consent]')
|
|
115
|
+
|
|
116
|
+
if (!button) return
|
|
117
|
+
|
|
118
|
+
const decision = button.dataset.consent!
|
|
119
|
+
|
|
120
|
+
saveChoice(decision)
|
|
121
|
+
banner.hidden = true
|
|
122
|
+
|
|
123
|
+
if (decision === 'granted') loadAnalytics(googleId)
|
|
124
|
+
else stopAnalytics(googleId)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
for (const opener of document.querySelectorAll<HTMLElement>('[data-consent-open]')) {
|
|
128
|
+
opener.hidden = false
|
|
129
|
+
opener.addEventListener('click', () => {
|
|
130
|
+
banner.hidden = false
|
|
131
|
+
banner.querySelector<HTMLElement>('[data-consent]')?.focus()
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
</script>
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Site footer with logo, license information, and navigation links.
|
|
6
6
|
*/
|
|
7
|
+
|
|
8
|
+
import { getStaticPath } from '../../site'
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
<footer class="cxd-footer py-6xl">
|
|
@@ -15,20 +17,27 @@
|
|
|
15
17
|
href="/"
|
|
16
18
|
aria-label="Chassis"
|
|
17
19
|
>
|
|
18
|
-
<img src=
|
|
20
|
+
<img src={getStaticPath('images/site-logo.svg')} alt="Chassis UI" height="32" />
|
|
19
21
|
</a>
|
|
20
|
-
<p>
|
|
22
|
+
<p class="mb-xs">
|
|
21
23
|
This project is licensed under the MIT License, which allows for free use, modification,
|
|
22
24
|
and distribution of the software.
|
|
23
25
|
</p>
|
|
26
|
+
<p>
|
|
27
|
+
<a href="/privacy/" class="fg-main">Privacy</a>
|
|
28
|
+
<button hidden data-consent-open type="button" class="cxd-footer-link fg-main ms-md"
|
|
29
|
+
>Cookie settings</button
|
|
30
|
+
>
|
|
31
|
+
</p>
|
|
24
32
|
</div>
|
|
25
33
|
<div class="sm:col-4 lg:col-2 lg:offset-1 mb-md">
|
|
26
34
|
<p class="h6 mb-xs">Docs</p>
|
|
27
35
|
<ul class="nav">
|
|
28
36
|
<li><a href="/tokens">Design Tokens</a></li>
|
|
29
|
-
<li><a href="/assets">Asset Distributor</a></li>
|
|
30
37
|
<li><a href="/figma">Figma Library</a></li>
|
|
31
38
|
<li><a href="/css">CSS Framework</a></li>
|
|
39
|
+
<li><a href="/react">React Components</a></li>
|
|
40
|
+
<li><a href="/assets">Asset Distributor</a></li>
|
|
32
41
|
<li><a href="/icons">Icon Generator</a></li>
|
|
33
42
|
</ul>
|
|
34
43
|
</div>
|
|
@@ -36,9 +45,10 @@
|
|
|
36
45
|
<p class="h6 mb-xs">Github</p>
|
|
37
46
|
<ul class="nav">
|
|
38
47
|
<li><a href="https://github.com/chassis-ui/tokens">Chassis Tokens</a></li>
|
|
39
|
-
<li><a href="https://github.com/chassis-ui/assets">Chassis Assets</a></li>
|
|
40
48
|
<li><a href="https://github.com/chassis-ui/figma">Chassis Figma</a></li>
|
|
41
49
|
<li><a href="https://github.com/chassis-ui/css">Chassis CSS</a></li>
|
|
50
|
+
<li><a href="https://github.com/chassis-ui/react">Chassis React</a></li>
|
|
51
|
+
<li><a href="https://github.com/chassis-ui/assets">Chassis Assets</a></li>
|
|
42
52
|
<li><a href="https://github.com/chassis-ui/icons">Chassis Icons</a></li>
|
|
43
53
|
</ul>
|
|
44
54
|
</div>
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Favicon and app icon link tags for various platforms.
|
|
6
6
|
*/
|
|
7
|
+
|
|
8
|
+
import { getStaticPath } from '../../site'
|
|
7
9
|
---
|
|
8
10
|
|
|
9
|
-
<link rel="apple-touch-icon" href={'
|
|
10
|
-
<link rel="icon" href={'
|
|
11
|
-
<link rel="icon" href={'
|
|
12
|
-
<link rel="manifest" href={'
|
|
13
|
-
<link rel="
|
|
14
|
-
<link rel="icon" href={'/static/images/favicon.png'} />
|
|
11
|
+
<link rel="apple-touch-icon" href={getStaticPath('images/apple-touch-icon.png')} sizes="180x180" />
|
|
12
|
+
<link rel="icon" href={getStaticPath('images/favicon-32x32.png')} sizes="32x32" type="image/png" />
|
|
13
|
+
<link rel="icon" href={getStaticPath('images/favicon-16x16.png')} sizes="16x16" type="image/png" />
|
|
14
|
+
<link rel="manifest" href={getStaticPath('images/manifest.json')} />
|
|
15
|
+
<link rel="icon" href={getStaticPath('images/favicon.png')} />
|
|
15
16
|
<meta name="theme-color" content="#00A4CC" />
|