@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.
Files changed (74) hide show
  1. package/README.md +16 -16
  2. package/index.ts +6 -1
  3. package/package.json +20 -8
  4. package/src/components/DocsSidebar.astro +63 -23
  5. package/src/components/FeatureCard.astro +3 -3
  6. package/src/components/TableOfContents.astro +8 -7
  7. package/src/components/ThemeToggler.astro +15 -36
  8. package/src/components/shortcodes/Blockquote.astro +1 -1
  9. package/src/components/shortcodes/CSSOnly.astro +6 -2
  10. package/src/components/shortcodes/Callout.astro +6 -2
  11. package/src/components/shortcodes/Code.astro +289 -0
  12. package/src/components/shortcodes/DeprecatedIn.astro +5 -2
  13. package/src/components/shortcodes/Example.astro +132 -0
  14. package/src/components/shortcodes/Icon.astro +57 -0
  15. package/src/components/shortcodes/InFigma.astro +7 -2
  16. package/src/components/shortcodes/JsDocs.astro +64 -0
  17. package/src/components/shortcodes/Placeholder.astro +28 -0
  18. package/src/components/shortcodes/ResizableExample.astro +126 -0
  19. package/src/components/shortcodes/ScssDocs.astro +108 -0
  20. package/src/components/shortcodes/ScssDocsSimple.astro +66 -0
  21. package/src/js/color-modes.js +3 -3
  22. package/src/js/example-mode.js +75 -0
  23. package/src/js/search.js +501 -39
  24. package/src/js/theme-color.js +1 -1
  25. package/src/layouts/DocsLayout.astro +11 -14
  26. package/src/layouts/RedirectLayout.astro +1 -0
  27. package/src/layouts/footer/Scripts.astro +14 -9
  28. package/src/layouts/head/Analytics.astro +12 -6
  29. package/src/layouts/head/Favicons.astro +0 -2
  30. package/src/layouts/head/Head.astro +2 -7
  31. package/src/layouts/head/Stylesheet.astro +18 -11
  32. package/src/layouts/header/Header.astro +2 -5
  33. package/src/layouts/header/Navigation.astro +31 -33
  34. package/src/layouts/header/SearchDialog.astro +74 -0
  35. package/src/layouts/header/SearchTrigger.astro +41 -0
  36. package/src/libs/highlight.ts +91 -0
  37. package/src/libs/icon.ts +75 -0
  38. package/src/libs/image.ts +12 -1
  39. package/src/libs/layout.ts +18 -0
  40. package/src/libs/markdown.ts +128 -0
  41. package/src/libs/placeholder.ts +250 -0
  42. package/src/libs/rehype.ts +0 -17
  43. package/src/libs/shortcodes.ts +129 -0
  44. package/src/libs/site.ts +95 -0
  45. package/src/libs/toc.ts +40 -6
  46. package/src/libs/utils.ts +34 -82
  47. package/src/libs/vite.ts +45 -0
  48. package/src/scss/_anchor.scss +20 -16
  49. package/src/scss/_callouts.scss +29 -28
  50. package/src/scss/_content.scss +72 -103
  51. package/src/scss/_footer.scss +18 -19
  52. package/src/scss/_layout.scss +56 -63
  53. package/src/scss/_navbar.scss +19 -123
  54. package/src/scss/_placeholder-img.scss +11 -7
  55. package/src/scss/_scrolling.scss +14 -12
  56. package/src/scss/_search.scss +418 -0
  57. package/src/scss/_shiki.scss +120 -0
  58. package/src/scss/_sidebar.scss +137 -132
  59. package/src/scss/_skippy.scss +6 -4
  60. package/src/scss/_snippets.scss +181 -0
  61. package/src/scss/_toc.scss +97 -93
  62. package/src/scss/_vars.scss +89 -0
  63. package/src/scss/main.scss +75 -80
  64. package/src/js/icon-loader.js +0 -229
  65. package/src/libs/chassis.ts +0 -38
  66. package/src/scss/_brand.scss +0 -59
  67. package/src/scss/_buttons.scss +0 -33
  68. package/src/scss/_clipboard-js.scss +0 -63
  69. package/src/scss/_code.scss +0 -116
  70. package/src/scss/_colors.scss +0 -140
  71. package/src/scss/_docsearch.scss +0 -212
  72. package/src/scss/_settings.scss +0 -29
  73. package/src/scss/_syntax.scss +0 -160
  74. 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
- <!-- Google tag (gtag.js) -->
13
- <script is:inline async src={`https://www.googletagmanager.com/gtag/js?id=${googleId}`}></script>
14
- <script
15
- is:inline
16
- set:html={`
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
+ }
@@ -4,8 +4,6 @@
4
4
  *
5
5
  * Favicon and app icon link tags for various platforms.
6
6
  */
7
-
8
- // <meta name="theme-color" content="#00A4CC" />
9
7
  ---
10
8
 
11
9
  <link rel="apple-touch-icon" href={'/static/images/apple-touch-icon.png'} sizes="180x180" />
@@ -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, direction, layout, robots, thumbnail, title } = Astro.props
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 direction={direction} layout={layout} />
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 { getChassisCssProps } from '../../libs/chassis'
9
- import type { Layout } from '../../libs/layout'
8
+ import type { HTMLAttributes } from 'astro/types'
10
9
 
11
- interface Props {
12
- /** Text direction (for RTL languages) */
13
- direction?: 'rtl'
14
- /** Current layout context */
15
- layout: Layout
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 { direction } = Astro.props
19
+ const cxCssLinkProps: HTMLAttributes<'link'> = {
20
+ href: cxCssLinkHref,
21
+ rel: 'stylesheet'
22
+ }
23
+
24
+ return cxCssLinkProps
25
+ }
19
26
  ---
20
27
 
21
- <link {...getChassisCssProps(direction)} />
22
- <link rel="stylesheet" href={`/static/icons/chassis-icons.min.css`} />
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, title } = Astro.props
19
+ const { layout } = Astro.props
23
20
  ---
24
21
 
25
22
  <Skippy layout={layout} />
26
23
 
27
- <Navigation layout={layout} title={title} />
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="Main navigation">
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 p-xsmall2"
41
+ class="button icon-only navbar-toggler"
41
42
  type="button"
42
- data-cx-toggle="offcanvas"
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
- <svg class="icon icon-large" aria-hidden="true">
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" id="docsearch"></div>
61
+ <div class="cxd-search"><SearchTrigger placeholder="Search docs…" /></div>
63
62
 
64
63
  <button
65
- class="navbar-toggler d-flex large:d-none order-3 p-xsmall"
64
+ class="button icon-only navbar-toggler"
66
65
  type="button"
67
- data-cx-toggle="offcanvas"
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
- <svg class="icon" aria-hidden="true"><use xlink:href="#ellipsis-h-solid"></use></svg>
71
+ <Icon name="ellipsis-h-solid" />
73
72
  </button>
74
73
  </div>
75
74
 
76
- <div
77
- class="offcanvas large:offcanvas offcanvas-end flex-grow-1"
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="offcanvas-header px-large pb-0">
84
- <h5 class="offcanvas-title text-white" id="cxdNavbarOffcanvasLabel">Chassis</h5>
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="offcanvas"
85
+ data-cx-dismiss="drawer"
89
86
  aria-label="Close"
90
87
  data-cx-target="#cxdNavbar"></button>
91
88
  </div>
92
89
 
93
- <div class="offcanvas-body p-large pt-0 large:p-0">
94
- <hr class="large:d-none" />
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-2"></div>
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 medium:ms-auto align-items-center">
107
+ <ul class="navbar-nav flex-row flex-wrap large:ms-auto align-items-center">
111
108
  <NavLink
112
- class="nav-link px-0 large:px-2"
109
+ class="px-0 large:px-small"
113
110
  href={getConfig().github_org}
114
111
  target="_blank"
115
112
  rel="noopener"
116
113
  >
117
- <svg class="icon"><title>GitHub</title><use href="#github-brand"></use></svg>
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="nav-link px-0 large:px-2"
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
- <svg class="icon"><title>X</title><use href="#x-twitter-brand"></use></svg>
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-2"></div>
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 dropdown">
131
+ <li class="nav-item">
135
132
  <ThemeToggler layout={layout} />
136
133
  </li>
137
134
  </ul>
138
135
  </div>
139
- </div>
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
+ }
@@ -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, '&amp;')
72
+ .replace(/</g, '&lt;')
73
+ .replace(/>/g, '&gt;')
74
+ .replace(/"/g, '&quot;')
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 = await sizeOf(new Uint8Array(buffer))
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}'.`)
@@ -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
  }