@chassis-ui/docs 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 +16 -16
- package/index.ts +6 -1
- package/package.json +20 -8
- package/src/components/DocsSidebar.astro +63 -23
- package/src/components/FeatureCard.astro +3 -3
- package/src/components/TableOfContents.astro +8 -7
- package/src/components/ThemeToggler.astro +15 -36
- package/src/components/shortcodes/Blockquote.astro +1 -1
- package/src/components/shortcodes/CSSOnly.astro +6 -2
- package/src/components/shortcodes/Callout.astro +6 -2
- package/src/components/shortcodes/Code.astro +289 -0
- package/src/components/shortcodes/DeprecatedIn.astro +5 -2
- package/src/components/shortcodes/Example.astro +132 -0
- package/src/components/shortcodes/Icon.astro +57 -0
- package/src/components/shortcodes/InFigma.astro +7 -2
- package/src/components/shortcodes/JsDocs.astro +64 -0
- package/src/components/shortcodes/Placeholder.astro +28 -0
- package/src/components/shortcodes/ResizableExample.astro +126 -0
- package/src/components/shortcodes/ScssDocs.astro +108 -0
- package/src/components/shortcodes/ScssDocsSimple.astro +66 -0
- package/src/js/color-modes.js +3 -3
- package/src/js/example-mode.js +75 -0
- package/src/js/search.js +501 -39
- package/src/js/theme-color.js +1 -1
- package/src/layouts/DocsLayout.astro +11 -14
- package/src/layouts/RedirectLayout.astro +1 -0
- package/src/layouts/footer/Scripts.astro +14 -9
- package/src/layouts/head/Analytics.astro +12 -6
- package/src/layouts/head/Favicons.astro +0 -2
- package/src/layouts/head/Head.astro +2 -7
- package/src/layouts/head/Stylesheet.astro +18 -11
- package/src/layouts/header/Header.astro +2 -5
- package/src/layouts/header/Navigation.astro +31 -33
- package/src/layouts/header/SearchDialog.astro +74 -0
- package/src/layouts/header/SearchTrigger.astro +41 -0
- package/src/libs/highlight.ts +91 -0
- package/src/libs/icon.ts +75 -0
- package/src/libs/image.ts +12 -1
- package/src/libs/layout.ts +18 -0
- package/src/libs/markdown.ts +128 -0
- package/src/libs/placeholder.ts +250 -0
- package/src/libs/rehype.ts +0 -17
- package/src/libs/shortcodes.ts +129 -0
- package/src/libs/site.ts +95 -0
- package/src/libs/toc.ts +40 -6
- package/src/libs/utils.ts +34 -82
- package/src/libs/vite.ts +45 -0
- package/src/scss/_anchor.scss +20 -16
- package/src/scss/_callouts.scss +29 -28
- package/src/scss/_content.scss +72 -103
- package/src/scss/_footer.scss +18 -19
- package/src/scss/_layout.scss +56 -63
- package/src/scss/_navbar.scss +19 -123
- package/src/scss/_placeholder-img.scss +11 -7
- package/src/scss/_scrolling.scss +14 -12
- package/src/scss/_search.scss +418 -0
- package/src/scss/_shiki.scss +120 -0
- package/src/scss/_sidebar.scss +137 -132
- package/src/scss/_skippy.scss +6 -4
- package/src/scss/_snippets.scss +181 -0
- package/src/scss/_toc.scss +97 -93
- package/src/scss/_vars.scss +89 -0
- package/src/scss/main.scss +75 -80
- package/src/js/icon-loader.js +0 -229
- package/src/libs/chassis.ts +0 -38
- package/src/scss/_brand.scss +0 -59
- package/src/scss/_buttons.scss +0 -33
- package/src/scss/_clipboard-js.scss +0 -63
- package/src/scss/_code.scss +0 -116
- package/src/scss/_colors.scss +0 -140
- package/src/scss/_docsearch.scss +0 -212
- package/src/scss/_settings.scss +0 -29
- package/src/scss/_syntax.scss +0 -160
- package/src/scss/_variables.scss +0 -73
|
@@ -6,18 +6,24 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getConfig } from '@libs/config'
|
|
9
|
+
const isProd = import.meta.env.PROD
|
|
9
10
|
const googleId = getConfig().analytics.google_id
|
|
10
11
|
---
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
{
|
|
14
|
+
isProd && (
|
|
15
|
+
<>
|
|
16
|
+
<script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${googleId}`} />
|
|
17
|
+
<script
|
|
18
|
+
is:inline
|
|
19
|
+
set:html={`
|
|
17
20
|
window.dataLayer = window.dataLayer || [];
|
|
18
21
|
function gtag(){dataLayer.push(arguments);}
|
|
19
22
|
gtag('js', new Date());
|
|
20
23
|
|
|
21
24
|
gtag('config', '${googleId}');
|
|
22
25
|
`}
|
|
23
|
-
/>
|
|
26
|
+
/>
|
|
27
|
+
</>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -16,8 +16,6 @@ import '@scss/docs.scss'
|
|
|
16
16
|
interface Props {
|
|
17
17
|
/** Page description for meta tags */
|
|
18
18
|
description: string
|
|
19
|
-
/** Text direction (for RTL languages) */
|
|
20
|
-
direction?: 'rtl'
|
|
21
19
|
/** Current layout context */
|
|
22
20
|
layout: Layout
|
|
23
21
|
/** Robots meta tag value */
|
|
@@ -28,7 +26,7 @@ interface Props {
|
|
|
28
26
|
title: string
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
const { description,
|
|
29
|
+
const { description, layout, robots, thumbnail, title } = Astro.props
|
|
32
30
|
|
|
33
31
|
const canonicalUrl = new URL(Astro.url.pathname, Astro.site)
|
|
34
32
|
|
|
@@ -45,19 +43,16 @@ const pageTitle = isHome
|
|
|
45
43
|
|
|
46
44
|
<meta name="author" content={getConfig().authors} />
|
|
47
45
|
<meta name="generator" content={Astro.generator} />
|
|
48
|
-
<meta name="docsearch:language" content="en" />
|
|
49
46
|
|
|
50
47
|
<link rel="canonical" href={canonicalUrl} />
|
|
51
48
|
|
|
52
|
-
<link rel="preconnect" href={`https://${getConfig().algolia.app_id}-dsn.algolia.net`} crossorigin />
|
|
53
|
-
|
|
54
49
|
<title>{pageTitle}</title>
|
|
55
50
|
|
|
56
51
|
{robots && <meta name="robots" content={robots} />}
|
|
57
52
|
|
|
58
53
|
<script src="../../js/color-modes.js"></script>
|
|
59
54
|
|
|
60
|
-
<Stylesheet
|
|
55
|
+
<Stylesheet />
|
|
61
56
|
<slot name="styles" />
|
|
62
57
|
<Favicons />
|
|
63
58
|
<Social description={description} layout={layout} thumbnail={thumbnail} title={title} />
|
|
@@ -5,18 +5,25 @@
|
|
|
5
5
|
* Stylesheet link tags for external CSS resources.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import type { Layout } from '../../libs/layout'
|
|
8
|
+
import type { HTMLAttributes } from 'astro/types'
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
10
|
+
function getChassisCssProps() {
|
|
11
|
+
let cxCssLinkHref = '/static/css/chassis'
|
|
12
|
+
|
|
13
|
+
if (import.meta.env.PROD) {
|
|
14
|
+
cxCssLinkHref = `${cxCssLinkHref}.min`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
cxCssLinkHref = `${cxCssLinkHref}.css`
|
|
17
18
|
|
|
18
|
-
const
|
|
19
|
+
const cxCssLinkProps: HTMLAttributes<'link'> = {
|
|
20
|
+
href: cxCssLinkHref,
|
|
21
|
+
rel: 'stylesheet'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return cxCssLinkProps
|
|
25
|
+
}
|
|
19
26
|
---
|
|
20
27
|
|
|
21
|
-
<link {...getChassisCssProps(
|
|
22
|
-
<link rel="stylesheet" href={`/static/icons/chassis-icons.
|
|
28
|
+
<link {...getChassisCssProps()} />
|
|
29
|
+
<link rel="stylesheet" href={`/static/icons/chassis-icons.css`} />
|
|
@@ -5,23 +5,20 @@
|
|
|
5
5
|
* Site header with navigation and page title.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { CollectionEntry } from 'astro:content'
|
|
9
8
|
import type { Layout } from '../../libs/layout'
|
|
10
9
|
import Skippy from './Skippy.astro'
|
|
11
10
|
import Navigation from './Navigation.astro'
|
|
12
11
|
|
|
13
12
|
interface Props {
|
|
14
|
-
/** Version when the feature was added */
|
|
15
|
-
addedIn?: CollectionEntry<'docs'>['data']['added']
|
|
16
13
|
/** Current layout context */
|
|
17
14
|
layout: Layout
|
|
18
15
|
/** Page title */
|
|
19
16
|
title: string
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
const { layout
|
|
19
|
+
const { layout } = Astro.props
|
|
23
20
|
---
|
|
24
21
|
|
|
25
22
|
<Skippy layout={layout} />
|
|
26
23
|
|
|
27
|
-
<Navigation layout={layout}
|
|
24
|
+
<Navigation layout={layout} />
|
|
@@ -9,12 +9,13 @@ import { getConfig } from '@libs/config'
|
|
|
9
9
|
import type { Layout } from '../../libs/layout'
|
|
10
10
|
import NavLink from '../../components/NavLink.astro'
|
|
11
11
|
import ThemeToggler from '../../components/ThemeToggler.astro'
|
|
12
|
+
import SearchTrigger from './SearchTrigger.astro'
|
|
13
|
+
import SearchDialog from './SearchDialog.astro'
|
|
14
|
+
import Icon from '../../components/shortcodes/Icon.astro'
|
|
12
15
|
|
|
13
16
|
interface Props {
|
|
14
17
|
/** Current layout context */
|
|
15
18
|
layout: Layout
|
|
16
|
-
/** Page title for the navbar */
|
|
17
|
-
title: string
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const { layout } = Astro.props
|
|
@@ -31,22 +32,20 @@ const isActive = (path: string) => {
|
|
|
31
32
|
}
|
|
32
33
|
---
|
|
33
34
|
|
|
34
|
-
<header class="navbar large:navbar-expand cxd-navbar sticky-top">
|
|
35
|
-
<nav class="container 2xlarge" aria-label="
|
|
35
|
+
<header class="navbar translucent large:navbar-expand cxd-navbar sticky-top">
|
|
36
|
+
<nav class="container 2xlarge" aria-label="Site navigation">
|
|
36
37
|
{
|
|
37
38
|
layout === 'docs' && (
|
|
38
39
|
<div class="cxd-navbar-toggle">
|
|
39
40
|
<button
|
|
40
|
-
class="navbar-toggler
|
|
41
|
+
class="button icon-only navbar-toggler"
|
|
41
42
|
type="button"
|
|
42
|
-
data-cx-toggle="
|
|
43
|
+
data-cx-toggle="drawer"
|
|
43
44
|
data-cx-target="#cxdSidebar"
|
|
44
45
|
aria-controls="cxdSidebar"
|
|
45
46
|
aria-label="Toggle docs navigation"
|
|
46
47
|
>
|
|
47
|
-
<
|
|
48
|
-
<use href="#bars-solid" />
|
|
49
|
-
</svg>
|
|
48
|
+
<Icon name="bars-solid" class="icon-large" />
|
|
50
49
|
<span class="d-none large:d-inline fs-6 pe-2xsmall">Browse</span>
|
|
51
50
|
</button>
|
|
52
51
|
</div>
|
|
@@ -54,44 +53,42 @@ const isActive = (path: string) => {
|
|
|
54
53
|
}
|
|
55
54
|
{layout !== 'docs' && <div class="large:d-none" style="width: 4.25rem;" />}
|
|
56
55
|
|
|
57
|
-
<a class="navbar-brand large:me-medium" href="/" aria-label="Chassis">
|
|
56
|
+
<a class="navbar-brand m-0 py-small large:me-medium" href="/" aria-label="Chassis">
|
|
58
57
|
<img src="/static/images/site-logo.svg" alt="Chassis UI" width="123" height="32" />
|
|
59
58
|
</a>
|
|
60
59
|
|
|
61
60
|
<div class="d-flex">
|
|
62
|
-
<div class="cxd-search"
|
|
61
|
+
<div class="cxd-search"><SearchTrigger placeholder="Search docs…" /></div>
|
|
63
62
|
|
|
64
63
|
<button
|
|
65
|
-
class="
|
|
64
|
+
class="button icon-only navbar-toggler"
|
|
66
65
|
type="button"
|
|
67
|
-
data-cx-toggle="
|
|
66
|
+
data-cx-toggle="drawer"
|
|
68
67
|
data-cx-target="#cxdNavbar"
|
|
69
68
|
aria-controls="cxdNavbar"
|
|
70
69
|
aria-label="Toggle navigation"
|
|
71
70
|
>
|
|
72
|
-
<
|
|
71
|
+
<Icon name="ellipsis-h-solid" />
|
|
73
72
|
</button>
|
|
74
73
|
</div>
|
|
75
74
|
|
|
76
|
-
<
|
|
77
|
-
class="
|
|
78
|
-
role="dialog"
|
|
79
|
-
tabindex="-1"
|
|
75
|
+
<dialog
|
|
76
|
+
class="max-large:drawer drawer-end flex-grow-1"
|
|
80
77
|
id="cxdNavbar"
|
|
81
78
|
aria-labelledby="cxdNavbarOffcanvasLabel"
|
|
82
79
|
>
|
|
83
|
-
<div class="
|
|
84
|
-
<
|
|
80
|
+
<div class="drawer-header">
|
|
81
|
+
<h2 class="drawer-title" id="cxdNavbarOffcanvasLabel">Chassis</h2>
|
|
85
82
|
<button
|
|
86
83
|
type="button"
|
|
87
84
|
class="close-button close-button-white"
|
|
88
|
-
data-cx-dismiss="
|
|
85
|
+
data-cx-dismiss="drawer"
|
|
89
86
|
aria-label="Close"
|
|
90
87
|
data-cx-target="#cxdNavbar"></button>
|
|
91
88
|
</div>
|
|
92
89
|
|
|
93
|
-
<
|
|
94
|
-
|
|
90
|
+
<hr class="large:d-none m-0" />
|
|
91
|
+
<div class="drawer-body large:p-0">
|
|
95
92
|
<ul class="navbar-nav flex-row flex-wrap cxd-navbar-nav mx-0 large:mx-medium">
|
|
96
93
|
<NavLink active={isActive('/tokens')} href="/tokens">Tokens</NavLink>
|
|
97
94
|
<NavLink active={isActive('/figma')} href="/figma">Figma</NavLink>
|
|
@@ -99,43 +96,44 @@ const isActive = (path: string) => {
|
|
|
99
96
|
<NavLink active={isActive('/assets')} href="/assets">Assets</NavLink>
|
|
100
97
|
<NavLink active={isActive('/icons')} href="/icons">Icons</NavLink>
|
|
101
98
|
<li class="nav-item col-12 large:col-auto large:d-flex">
|
|
102
|
-
<div class="vr d-none large:d-flex my-auto large:mx-
|
|
99
|
+
<div class="vr d-none large:d-flex my-auto large:mx-small"></div>
|
|
103
100
|
<hr class="large:d-none" />
|
|
104
101
|
</li>
|
|
105
102
|
<NavLink active={isActive('/examples')} href="/examples">Examples</NavLink>
|
|
106
103
|
</ul>
|
|
107
104
|
|
|
108
|
-
<hr class="large:d-none" />
|
|
105
|
+
<hr class="large:d-none m-0" />
|
|
109
106
|
|
|
110
|
-
<ul class="navbar-nav flex-row flex-wrap
|
|
107
|
+
<ul class="navbar-nav flex-row flex-wrap large:ms-auto align-items-center">
|
|
111
108
|
<NavLink
|
|
112
|
-
class="
|
|
109
|
+
class="px-0 large:px-small"
|
|
113
110
|
href={getConfig().github_org}
|
|
114
111
|
target="_blank"
|
|
115
112
|
rel="noopener"
|
|
116
113
|
>
|
|
117
|
-
<
|
|
114
|
+
<Icon name="github-brand" title="GitHub" />
|
|
118
115
|
<small class="large:d-none ms-xsmall">GitHub</small>
|
|
119
116
|
</NavLink>
|
|
120
117
|
<NavLink
|
|
121
|
-
class="
|
|
118
|
+
class="px-0 large:px-small"
|
|
122
119
|
href={`https://x.com/${getConfig().x}`}
|
|
123
120
|
target="_blank"
|
|
124
121
|
rel="noopener"
|
|
125
122
|
>
|
|
126
|
-
<
|
|
123
|
+
<Icon name="x-twitter-brand" title="X" />
|
|
127
124
|
<small class="large:d-none ms-xsmall">X</small>
|
|
128
125
|
</NavLink>
|
|
129
126
|
<li class="nav-item col-12 large:col-auto">
|
|
130
|
-
<div class="vr d-none large:d-flex h-100 large:mx-
|
|
127
|
+
<div class="vr d-none large:d-flex h-100 large:mx-2xsmall"></div>
|
|
131
128
|
<hr class="large:d-none" />
|
|
132
129
|
</li>
|
|
133
130
|
|
|
134
|
-
<li class="nav-item
|
|
131
|
+
<li class="nav-item">
|
|
135
132
|
<ThemeToggler layout={layout} />
|
|
136
133
|
</li>
|
|
137
134
|
</ul>
|
|
138
135
|
</div>
|
|
139
|
-
</
|
|
136
|
+
</dialog>
|
|
140
137
|
</nav>
|
|
141
138
|
</header>
|
|
139
|
+
<SearchDialog placeholder="Search docs…" />
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Search Dialog
|
|
4
|
+
*
|
|
5
|
+
* Modal dialog containing the Pagefind search input and results. Opened by
|
|
6
|
+
* `SearchTrigger` or the ⌘K / Ctrl+K / "/" keyboard shortcuts defined in
|
|
7
|
+
* `src/js/search.js`. Uses native `<dialog>` with `showModal()`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import Icon from '../../components/shortcodes/Icon.astro'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
/** HTML `id` of the dialog element. Must match the `target` in `SearchTrigger`. */
|
|
14
|
+
id?: string
|
|
15
|
+
/** Placeholder text for the search input and the visually-hidden label. */
|
|
16
|
+
placeholder?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { id = 'cxdSearchDialog', placeholder = 'Search docs…' } = Astro.props
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<dialog
|
|
23
|
+
class="modal dialog large cxd-search-dialog scrollable"
|
|
24
|
+
id={id}
|
|
25
|
+
aria-labelledby={`${id}-label`}
|
|
26
|
+
>
|
|
27
|
+
<search class="modal-header cxd-search-form">
|
|
28
|
+
<label class="visually-hidden" id={`${id}-label`} for={`${id}-input`}>
|
|
29
|
+
{placeholder}
|
|
30
|
+
</label>
|
|
31
|
+
<cxd-search-input>
|
|
32
|
+
<div class="form-input">
|
|
33
|
+
<div class="input-help">
|
|
34
|
+
<Icon name="search-solid" />
|
|
35
|
+
</div>
|
|
36
|
+
<input
|
|
37
|
+
id={`${id}-input`}
|
|
38
|
+
type="search"
|
|
39
|
+
class="ghost-input cxd-search-input"
|
|
40
|
+
placeholder={placeholder}
|
|
41
|
+
autocomplete="off"
|
|
42
|
+
spellcheck="false"
|
|
43
|
+
enterkeyhint="search"
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
</cxd-search-input>
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
class="close-button"
|
|
50
|
+
aria-label="Close"
|
|
51
|
+
data-cx-dismiss="dialog"
|
|
52
|
+
data-cx-target={`#${id}`}>×</button
|
|
53
|
+
>
|
|
54
|
+
</search>
|
|
55
|
+
|
|
56
|
+
<div class="modal-body cxd-search-body">
|
|
57
|
+
<cxd-search-results></cxd-search-results>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="modal-footer cxd-search-footer">
|
|
60
|
+
<ul class="list horizontal cxd-search-shortcuts-list">
|
|
61
|
+
<li class="list-item">
|
|
62
|
+
<kbd class="cxd-search-shortcut-key font-2xsmall">⮐</kbd>
|
|
63
|
+
<span class="cxd-search-shortcut-label">to select</span>
|
|
64
|
+
</li><li class="list-item">
|
|
65
|
+
<kbd class="cxd-search-shortcut-key font-small">↑</kbd>
|
|
66
|
+
<kbd class="cxd-search-shortcut-key font-small">↓</kbd>
|
|
67
|
+
<span class="cxd-search-shortcut-label">to navigate</span>
|
|
68
|
+
</li><li class="list-item">
|
|
69
|
+
<kbd class="cxd-search-shortcut-key font-xsmall">ESC</kbd>
|
|
70
|
+
<span class="cxd-search-shortcut-label">to close</span>
|
|
71
|
+
</li>
|
|
72
|
+
</ul>
|
|
73
|
+
</div>
|
|
74
|
+
</dialog>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Search Trigger
|
|
4
|
+
*
|
|
5
|
+
* Button that opens the `SearchDialog`. Displays a platform-aware keyboard
|
|
6
|
+
* shortcut hint (⌘K on Mac, Ctrl K elsewhere) that `search.js` fills in after
|
|
7
|
+
* detecting the platform — rendered hidden until then so non-JS visitors never
|
|
8
|
+
* see a misleading hint.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Icon from '../../components/shortcodes/Icon.astro'
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Additional CSS classes to apply to the trigger button. */
|
|
15
|
+
class?: string
|
|
16
|
+
/** Accessible label and visible placeholder text inside the trigger. */
|
|
17
|
+
placeholder?: string
|
|
18
|
+
/** CSS selector for the dialog to open. Must match `SearchDialog`'s `id`. */
|
|
19
|
+
target?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const { class: className, placeholder = 'Search docs…', target = '#cxdSearchDialog' } = Astro.props
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
<button
|
|
26
|
+
type="button"
|
|
27
|
+
class:list={['cxd-search-trigger', className]}
|
|
28
|
+
data-cx-toggle="dialog"
|
|
29
|
+
data-cx-target={target}
|
|
30
|
+
aria-haspopup="dialog"
|
|
31
|
+
aria-label={placeholder}
|
|
32
|
+
>
|
|
33
|
+
<Icon name="search-solid" size={16} />
|
|
34
|
+
<span class="cxd-search-trigger-label">{placeholder}</span>
|
|
35
|
+
{
|
|
36
|
+
/* Shortcut hint is empty + hidden until search.js detects the platform and
|
|
37
|
+
fills in either ⌘K (Mac) or Ctrl K (other). Without JS the shortcut
|
|
38
|
+
itself doesn't work, so showing nothing is more honest than guessing. */
|
|
39
|
+
}
|
|
40
|
+
<span class="cxd-search-trigger-shortcut" hidden></span>
|
|
41
|
+
</button>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { codeToHtml, type ShikiTransformer } from 'shiki'
|
|
2
|
+
import { DOCS_SHIKI_THEMES } from './markdown'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shiki transformer that renames the `shiki` CSS class on `<pre>` elements to
|
|
6
|
+
* `astro-code` so that Astro's built-in syntax highlighting styles apply.
|
|
7
|
+
*/
|
|
8
|
+
const classTransformer: ShikiTransformer = {
|
|
9
|
+
name: 'class-name-transformer',
|
|
10
|
+
pre(node) {
|
|
11
|
+
if (typeof node.properties.class === 'string') {
|
|
12
|
+
node.properties.class = node.properties.class.replace(/shiki/g, 'astro-code')
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shiki transformer that wraps non-comment lines in a `<span class="line">`
|
|
19
|
+
* element. Applied only to shell languages (bash, sh, powershell) where line
|
|
20
|
+
* wrapping is needed for the copy-button to correctly extract individual commands.
|
|
21
|
+
*/
|
|
22
|
+
const lineWrapperTransformer: ShikiTransformer = {
|
|
23
|
+
name: 'line-wrapper',
|
|
24
|
+
line(node) {
|
|
25
|
+
const hasOnlyComments = node.children.every(
|
|
26
|
+
(child) =>
|
|
27
|
+
child.type === 'element' &&
|
|
28
|
+
child.properties?.class &&
|
|
29
|
+
Array.isArray(child.properties.class) &&
|
|
30
|
+
child.properties.class.some((cls) => typeof cls === 'string' && cls.includes('comment'))
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if (!hasOnlyComments) {
|
|
34
|
+
node.properties = node.properties || {}
|
|
35
|
+
const existing = node.properties.class ?? ''
|
|
36
|
+
const classes = typeof existing === 'string' ? existing.split(' ') : []
|
|
37
|
+
if (!classes.includes('line')) classes.push('line')
|
|
38
|
+
node.properties.class = classes.join(' ').trim()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Replaces any remaining `shiki` / `shiki-themes` class strings in the
|
|
45
|
+
* serialized HTML that the transformer may have missed (e.g. in inline styles
|
|
46
|
+
* converted by `transformerStyleToClass`).
|
|
47
|
+
*/
|
|
48
|
+
function replaceShikiClasses(html: string): string {
|
|
49
|
+
return html
|
|
50
|
+
.replace(/class=(["'])shiki(\s+)/g, 'class=$1astro-code$2')
|
|
51
|
+
.replace(/class=(["'])shiki(["'])/g, 'class=$1astro-code$2')
|
|
52
|
+
.replace(/shiki-themes/g, 'astro-code-themes')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Highlights a code string using Shiki with the GitHub light/dark dual theme.
|
|
57
|
+
*
|
|
58
|
+
* Inline styles are converted to CSS classes via `transformerStyleToClass` so
|
|
59
|
+
* that a single `<style>` block can be emitted per page rather than repeating
|
|
60
|
+
* styles on every token. The returned `css` string must be injected into the
|
|
61
|
+
* page (e.g. inside a `<style>` tag) for the highlighted output to render correctly.
|
|
62
|
+
*
|
|
63
|
+
* For shell languages (`bash`, `sh`, `powershell`) each non-comment line is
|
|
64
|
+
* additionally wrapped in `<span class="line">` to support per-line copy.
|
|
65
|
+
*
|
|
66
|
+
* @param code - Source code to highlight.
|
|
67
|
+
* @param lang - Shiki language identifier (e.g. `"html"`, `"ts"`, `"bash"`).
|
|
68
|
+
* @param extraTransformers - Additional Shiki transformers to apply before the built-ins.
|
|
69
|
+
* @returns An object with `html` (the highlighted markup) and `css` (the extracted styles).
|
|
70
|
+
*/
|
|
71
|
+
export async function highlightCode(
|
|
72
|
+
code: string,
|
|
73
|
+
lang: string,
|
|
74
|
+
extraTransformers: ShikiTransformer[] = []
|
|
75
|
+
): Promise<{ html: string }> {
|
|
76
|
+
const shouldWrapLines = ['bash', 'sh', 'powershell'].includes(lang)
|
|
77
|
+
|
|
78
|
+
const transformers: ShikiTransformer[] = [...extraTransformers, classTransformer]
|
|
79
|
+
if (shouldWrapLines) {
|
|
80
|
+
transformers.push(lineWrapperTransformer)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const highlighted = await codeToHtml(code, {
|
|
84
|
+
lang,
|
|
85
|
+
themes: DOCS_SHIKI_THEMES,
|
|
86
|
+
defaultColor: false,
|
|
87
|
+
transformers
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return { html: replaceShikiClasses(highlighted) }
|
|
91
|
+
}
|
package/src/libs/icon.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as htmlparser2 from 'htmlparser2'
|
|
2
|
+
|
|
3
|
+
const iconRegex = /<Icon\s+([^>]+)\/>/g
|
|
4
|
+
|
|
5
|
+
interface IconOptions {
|
|
6
|
+
name: string
|
|
7
|
+
class?: string
|
|
8
|
+
size: number
|
|
9
|
+
font: boolean
|
|
10
|
+
sprite: string
|
|
11
|
+
title?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Replaces `<Icon />` elements described using the `<Icon />` Astro component in HTML markup with the
|
|
16
|
+
* expected HTML content.
|
|
17
|
+
*
|
|
18
|
+
* This is needed because the `<Example />` shortcode renders its `code` prop as raw HTML via `set:html`
|
|
19
|
+
* rather than as MDX, so nested Astro components (like `<Icon />`, similar to `<Placeholder />`) are never
|
|
20
|
+
* actually invoked — they need to be expanded to their rendered markup ahead of time.
|
|
21
|
+
* @see src/components/shortcodes/Icon.astro
|
|
22
|
+
*/
|
|
23
|
+
export function replaceIconsInHtml(html: string) {
|
|
24
|
+
return html.replace(iconRegex, (match) => {
|
|
25
|
+
const document = htmlparser2.parseDocument(match, { xmlMode: true })
|
|
26
|
+
const iconElement = document.firstChild
|
|
27
|
+
|
|
28
|
+
if (
|
|
29
|
+
document.children.length > 1 ||
|
|
30
|
+
!iconElement ||
|
|
31
|
+
iconElement.type !== htmlparser2.ElementType.Tag ||
|
|
32
|
+
iconElement.name !== 'Icon'
|
|
33
|
+
) {
|
|
34
|
+
throw new Error('Invalid icon element.')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return renderIconToString(getOptionsWithDefaults(iconElement.attribs))
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getOptionsWithDefaults(attribs: Record<string, string>): IconOptions {
|
|
42
|
+
if (!attribs.name) {
|
|
43
|
+
throw new Error('Icon element is missing the required "name" attribute.')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
name: attribs.name,
|
|
48
|
+
class: attribs.class,
|
|
49
|
+
size: attribs.size ? Number(attribs.size.replace(/[{}]/g, '')) : 24,
|
|
50
|
+
font: attribs.font !== undefined && attribs.font !== '{false}',
|
|
51
|
+
sprite: attribs.sprite || '/static/icons/chassis-icons.svg',
|
|
52
|
+
title: attribs.title
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderIconToString({ name, class: className, size, font, sprite, title }: IconOptions) {
|
|
57
|
+
const classes = ['icon', font ? `cx-${name}` : undefined, className].filter(Boolean).join(' ')
|
|
58
|
+
const ariaHidden = title ? '' : ' aria-hidden="true"'
|
|
59
|
+
|
|
60
|
+
if (font) {
|
|
61
|
+
const ariaLabel = title ? ` aria-label="${escapeHtml(title)}"` : ''
|
|
62
|
+
return `<span class="${classes}"${ariaHidden}${ariaLabel}></span>`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const titleTag = title ? `<title>${escapeHtml(title)}</title>` : ''
|
|
66
|
+
return `<svg class="${classes}" width="${size}" height="${size}"${ariaHidden}>${titleTag}<use href="${sprite}#${name}"></use></svg>`
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function escapeHtml(value: string) {
|
|
70
|
+
return value
|
|
71
|
+
.replace(/&/g, '&')
|
|
72
|
+
.replace(/</g, '<')
|
|
73
|
+
.replace(/>/g, '>')
|
|
74
|
+
.replace(/"/g, '"')
|
|
75
|
+
}
|
package/src/libs/image.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs'
|
|
2
2
|
import sizeOf from 'image-size'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Reads an image file from disk and returns its pixel dimensions.
|
|
6
|
+
*
|
|
7
|
+
* Intended for build-time use only (e.g. generating `<meta og:image:width/height>`
|
|
8
|
+
* tags). Do not call at runtime in a server or edge context where filesystem
|
|
9
|
+
* access is unavailable.
|
|
10
|
+
*
|
|
11
|
+
* @param imagePath - Absolute path to the image file.
|
|
12
|
+
* @returns An object with `width` and `height` in pixels.
|
|
13
|
+
* @throws If the file cannot be read or its dimensions cannot be determined.
|
|
14
|
+
*/
|
|
4
15
|
export async function getStaticImageSize(imagePath: string) {
|
|
5
16
|
const buffer = await fs.readFile(imagePath)
|
|
6
|
-
const size =
|
|
17
|
+
const size = sizeOf(new Uint8Array(buffer))
|
|
7
18
|
|
|
8
19
|
if (!size?.height || !size?.width) {
|
|
9
20
|
throw new Error(`Failed to get size of static image at '${imagePath}'.`)
|
package/src/libs/layout.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import type { HTMLAttributes, HTMLTag } from 'astro/types'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Identifies which top-level layout a page is using. Passed down through
|
|
5
|
+
* layouts and components to conditionally render layout-specific UI (e.g.
|
|
6
|
+
* sidebar toggle, TOC, search).
|
|
7
|
+
*
|
|
8
|
+
* `undefined` means a plain page with no special layout context.
|
|
9
|
+
*/
|
|
3
10
|
export type Layout = 'docs' | 'icons' | 'examples' | 'single' | 'blog' | undefined
|
|
4
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Extends Astro's standard HTML element attributes with an open-ended set of
|
|
14
|
+
* `data-*` attributes so that arbitrary data attributes can be passed as
|
|
15
|
+
* override props without TypeScript errors.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const bodyProps: LayoutOverridesHTMLAttributes<'body'> = {
|
|
19
|
+
* class: 'docs-body',
|
|
20
|
+
* 'data-cx-spy': 'scroll',
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
5
23
|
export type LayoutOverridesHTMLAttributes<TTag extends HTMLTag> = HTMLAttributes<TTag> & {
|
|
6
24
|
[key in `data-${string}`]: string //eslint-disable-line no-unused-vars
|
|
7
25
|
}
|