@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.
@@ -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 = '/static/icons/chassis-icons.svg',
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
@@ -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
  ]
@@ -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', `/static/icons/chassis-icons.svg#${iconHref}`)
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', `/static/icons/chassis-icons.svg#${iconHref}`)
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="/static/icons/chassis-icons.svg#${icon}"></use>
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="/static/images/site-logo.svg" alt="Chassis UI" height="32" />
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={'/static/images/apple-touch-icon.png'} sizes="180x180" />
10
- <link rel="icon" href={'/static/images/favicon-32x32.png'} sizes="32x32" type="image/png" />
11
- <link rel="icon" href={'/static/images/favicon-16x16.png'} sizes="16x16" type="image/png" />
12
- <link rel="manifest" href={'/static/images/manifest.json'} />
13
- <link rel="mask-icon" href={'/static/images/safari-pinned-tab.svg'} color="#712cf9" />
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" />