@chassis-ui/docs 0.1.10 → 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 (79) 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 +8 -8
  6. package/src/components/NavLink.astro +1 -1
  7. package/src/components/TableOfContents.astro +8 -7
  8. package/src/components/ThemeToggler.astro +16 -37
  9. package/src/components/shortcodes/Blockquote.astro +31 -0
  10. package/src/components/shortcodes/CSSOnly.astro +22 -0
  11. package/src/components/shortcodes/Callout.astro +6 -2
  12. package/src/components/shortcodes/Code.astro +289 -0
  13. package/src/components/shortcodes/DeprecatedIn.astro +18 -0
  14. package/src/components/shortcodes/Example.astro +132 -0
  15. package/src/components/shortcodes/Icon.astro +57 -0
  16. package/src/components/shortcodes/InFigma.astro +26 -0
  17. package/src/components/shortcodes/JsDocs.astro +64 -0
  18. package/src/components/shortcodes/Placeholder.astro +28 -0
  19. package/src/components/shortcodes/ResizableExample.astro +126 -0
  20. package/src/components/shortcodes/ScssDocs.astro +108 -0
  21. package/src/components/shortcodes/ScssDocsSimple.astro +66 -0
  22. package/src/js/color-modes.js +3 -3
  23. package/src/js/example-mode.js +75 -0
  24. package/src/js/search.js +501 -39
  25. package/src/js/theme-color.js +1 -1
  26. package/src/layouts/DocsLayout.astro +18 -21
  27. package/src/layouts/RedirectLayout.astro +1 -0
  28. package/src/layouts/SingleLayout.astro +3 -3
  29. package/src/layouts/footer/Footer.astro +5 -5
  30. package/src/layouts/footer/Scripts.astro +14 -7
  31. package/src/layouts/head/Analytics.astro +12 -6
  32. package/src/layouts/head/Favicons.astro +0 -2
  33. package/src/layouts/head/Head.astro +2 -7
  34. package/src/layouts/head/Stylesheet.astro +18 -11
  35. package/src/layouts/header/Header.astro +2 -5
  36. package/src/layouts/header/Navigation.astro +40 -42
  37. package/src/layouts/header/SearchDialog.astro +74 -0
  38. package/src/layouts/header/SearchTrigger.astro +41 -0
  39. package/src/layouts/header/Skippy.astro +1 -1
  40. package/src/libs/highlight.ts +91 -0
  41. package/src/libs/icon.ts +75 -0
  42. package/src/libs/image.ts +12 -1
  43. package/src/libs/layout.ts +18 -0
  44. package/src/libs/markdown.ts +128 -0
  45. package/src/libs/placeholder.ts +250 -0
  46. package/src/libs/rehype.ts +0 -17
  47. package/src/libs/shortcodes.ts +129 -0
  48. package/src/libs/site.ts +95 -0
  49. package/src/libs/toc.ts +40 -6
  50. package/src/libs/utils.ts +34 -82
  51. package/src/libs/vite.ts +45 -0
  52. package/src/scss/_anchor.scss +20 -16
  53. package/src/scss/_callouts.scss +29 -28
  54. package/src/scss/_content.scss +72 -103
  55. package/src/scss/_footer.scss +18 -19
  56. package/src/scss/_layout.scss +56 -61
  57. package/src/scss/_navbar.scss +19 -126
  58. package/src/scss/_placeholder-img.scss +11 -7
  59. package/src/scss/_scrolling.scss +14 -12
  60. package/src/scss/_search.scss +418 -0
  61. package/src/scss/_shiki.scss +120 -0
  62. package/src/scss/_sidebar.scss +137 -132
  63. package/src/scss/_skippy.scss +6 -4
  64. package/src/scss/_snippets.scss +181 -0
  65. package/src/scss/_toc.scss +97 -93
  66. package/src/scss/_vars.scss +89 -0
  67. package/src/scss/main.scss +75 -81
  68. package/src/js/icon-loader.js +0 -229
  69. package/src/libs/chassis.ts +0 -38
  70. package/src/scss/_brand.scss +0 -59
  71. package/src/scss/_buttons.scss +0 -33
  72. package/src/scss/_clipboard-js.scss +0 -63
  73. package/src/scss/_code.scss +0 -116
  74. package/src/scss/_colors.scss +0 -140
  75. package/src/scss/_docsearch.scss +0 -212
  76. package/src/scss/_masthead.scss +0 -124
  77. package/src/scss/_settings.scss +0 -29
  78. package/src/scss/_syntax.scss +0 -160
  79. package/src/scss/_variables.scss +0 -78
@@ -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
  }
@@ -0,0 +1,128 @@
1
+ import type { AstroUserConfig } from 'astro'
2
+ import type { Element, Text } from 'hast'
3
+ import type { ShikiTransformer } from 'shiki'
4
+ import { rehypeHeadingIds, unified } from '@astrojs/markdown-remark'
5
+ import type { UnifiedProcessorOptions } from '@astrojs/markdown-remark'
6
+ import { transformerNotationDiff, transformerNotationHighlight } from '@shikijs/transformers'
7
+ import rehypeAutolinkHeadings from 'rehype-autolink-headings'
8
+ import { rehypeCxTable } from './rehype'
9
+
10
+ type MarkdownConfig = NonNullable<AstroUserConfig['markdown']>
11
+
12
+ /**
13
+ * Shiki theme pair used for both programmatic highlighting (`highlightCode`) and
14
+ * the Astro markdown pipeline (`getDocsMarkdownConfig`). Defined once here to
15
+ * keep both in sync.
16
+ */
17
+ export const DOCS_SHIKI_THEMES = {
18
+ light: 'github-light' as const,
19
+ dark: 'github-dark' as const
20
+ }
21
+
22
+ /**
23
+ * Shiki transformer that exposes the current language as a `data-language`
24
+ * attribute on `<pre>` elements. The `Code` component reads this attribute when
25
+ * rendering inline markdown code blocks so it can display the correct language
26
+ * label without requiring an explicit `lang` prop.
27
+ */
28
+ const addLanguageAttributeTransformer: ShikiTransformer = {
29
+ name: 'add-language-attribute',
30
+ pre(node) {
31
+ const lang = this.options.lang
32
+ if (lang) {
33
+ node.properties['dataLanguage'] = lang
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Options accepted by `getDocsMarkdownConfig`.
40
+ */
41
+ export interface DocsMarkdownConfigOptions {
42
+ /**
43
+ * Heading depth range used to determine which headings receive an autolink
44
+ * anchor. Typically sourced from the project's `config.yml` `anchors` section.
45
+ *
46
+ * @example { min: 2, max: 4 } // anchors h2–h4
47
+ */
48
+ anchors: {
49
+ min: number
50
+ max: number
51
+ }
52
+ /** Additional project-specific rehype plugins, appended after the base set. */
53
+ rehypePlugins?: UnifiedProcessorOptions['rehypePlugins']
54
+ /** Additional project-specific remark plugins, appended after the base set. */
55
+ remarkPlugins?: UnifiedProcessorOptions['remarkPlugins']
56
+ /**
57
+ * Passed directly to `remark-rehype`. Shallow-merged so individual handler
58
+ * overrides don't discard base handlers.
59
+ */
60
+ remarkRehype?: UnifiedProcessorOptions['remarkRehype']
61
+ }
62
+
63
+ /**
64
+ * Returns the shared Astro markdown configuration for Chassis docs sites.
65
+ *
66
+ * Includes the following base rehype plugins (in order):
67
+ * - `rehypeHeadingIds` — adds `id` attributes to all headings.
68
+ * - `rehypeAutolinkHeadings` — appends anchor links to headings in the
69
+ * `anchors.min`–`anchors.max` depth range.
70
+ * - `rehypeCxTable` — applies CSS classes to `<CxTable>`-wrapped tables.
71
+ * - _(any `options.rehypePlugins`)_
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * // astro.config.ts
76
+ * import { getDocsMarkdownConfig } from '@chassis-ui/docs'
77
+ * import { getConfig } from './src/libs/config'
78
+ *
79
+ * export default defineConfig({
80
+ * markdown: getDocsMarkdownConfig({
81
+ * anchors: getConfig().anchors,
82
+ * rehypePlugins: [myExtraPlugin],
83
+ * remarkPlugins: [remarkCxConfig, remarkDefinitionList],
84
+ * remarkRehype: { handlers: defListHastHandlers }
85
+ * })
86
+ * })
87
+ * ```
88
+ */
89
+ export function getDocsMarkdownConfig(options: DocsMarkdownConfigOptions): MarkdownConfig {
90
+ const headingsRangeRegex = new RegExp(`^h[${options.anchors.min}-${options.anchors.max}]$`)
91
+
92
+ return {
93
+ processor: unified({
94
+ smartypants: false,
95
+ rehypePlugins: [
96
+ rehypeHeadingIds,
97
+ [
98
+ rehypeAutolinkHeadings,
99
+ {
100
+ behavior: 'append',
101
+ content: [{ type: 'text', value: ' ' }],
102
+ properties: (element: Element) => ({
103
+ class: 'anchor-link',
104
+ ariaLabel: `Link to this section: ${(element.children[0] as Text).value}`
105
+ }),
106
+ test: (element: Element) => element.tagName.match(headingsRangeRegex)
107
+ }
108
+ ],
109
+ rehypeCxTable,
110
+ ...(options.rehypePlugins ?? [])
111
+ ],
112
+ remarkPlugins: options.remarkPlugins ?? [],
113
+ ...(options.remarkRehype && {
114
+ remarkRehype: options.remarkRehype
115
+ })
116
+ }),
117
+ syntaxHighlight: 'shiki',
118
+ shikiConfig: {
119
+ themes: DOCS_SHIKI_THEMES,
120
+ defaultColor: false,
121
+ transformers: [
122
+ transformerNotationDiff(),
123
+ transformerNotationHighlight(),
124
+ addLanguageAttributeTransformer
125
+ ]
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,250 @@
1
+ import type { HTMLAttributes } from 'astro/types'
2
+ import * as htmlparser2 from 'htmlparser2'
3
+
4
+ const placeholderRegex = /<Placeholder\s+([^>]+)\/>/g
5
+
6
+ /**
7
+ * Generates all the placeholder attributes and options required to render a placeholder.
8
+ * @see src/components/shortcodes/Placeholder.astro
9
+ */
10
+ export function getPlaceholder(userOptions: Partial<PlaceholderOptions>): Placeholder {
11
+ const options = getOptionsWithDefaults(userOptions)
12
+ const { class: className, height, markup, text, title, width } = options
13
+
14
+ const showText = text !== false
15
+ const showTitle = title !== false
16
+
17
+ const placeholderClassList = ['cxd-placeholder-image', className].join(' ')
18
+ const placeholderRole = showTitle || showText ? 'img' : undefined
19
+ const placeholderAriaHidden = !showText && !showTitle ? 'true' : undefined
20
+
21
+ const placeholderLabel =
22
+ showText || showTitle
23
+ ? `${showTitle ? title : ''}${showTitle && showText ? ': ' : ''}${showText ? text : ''}`
24
+ : undefined
25
+
26
+ const optionsWithVisibilities = { ...options, showText, showTitle }
27
+
28
+ if (markup === 'img') {
29
+ return {
30
+ type: 'img',
31
+ options: optionsWithVisibilities,
32
+ props: {
33
+ alt: placeholderLabel,
34
+ class: placeholderClassList,
35
+ height,
36
+ src: getPlaceholderSrc(showTitle, showText, options),
37
+ width
38
+ }
39
+ }
40
+ }
41
+
42
+ return {
43
+ type: 'svg',
44
+ options: optionsWithVisibilities,
45
+ props: {
46
+ 'aria-hidden': placeholderAriaHidden,
47
+ 'aria-label': placeholderLabel,
48
+ class: placeholderClassList,
49
+ height,
50
+ preserveAspectRatio: 'xMidYMid slice',
51
+ role: placeholderRole,
52
+ width,
53
+ xmlns: 'http://www.w3.org/2000/svg'
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Replaces placeholders described using the `<Placeholder />` component in HTML markup with the expected HTML content.
60
+ * This is useful to render examples that have a pretty large set of constraints:
61
+ *
62
+ * - The provided HTML code is not valid MDX (e.g. unclosed void elements like <img>) but can contain the
63
+ * `<Placeholder />` Astro component. This means that we cannot use an Astro slot for example that requires valid
64
+ * MDX.
65
+ * - The provided HTML code cannot be parsed in a forgiving way with XML mode enabled (to not lose the structure due
66
+ * to self-closing MDX or Astro components) and serialized back to a string while closing all known void elements
67
+ * in order to render it as MDX using `@mdx-js/mdx` & `astro/jsx-runtime`. This works perfectly (tested) but the
68
+ * DOM needs to contains the exact same HTML markup (even indentation) provided to the example as it is used on the
69
+ * client to send the example to StackBlitz with the same indentation as the original example.
70
+ *
71
+ * If you are not sure if you need to use this function, you probably don't.
72
+ */
73
+ export function replacePlaceholdersInHtml(html: string) {
74
+ return html.replace(placeholderRegex, (match) => {
75
+ const document = htmlparser2.parseDocument(match, { xmlMode: true })
76
+ const placeholderElement = document.firstChild
77
+
78
+ if (
79
+ document.children.length > 1 ||
80
+ !placeholderElement ||
81
+ placeholderElement.type !== htmlparser2.ElementType.Tag ||
82
+ placeholderElement.name !== 'Placeholder'
83
+ ) {
84
+ throw new Error('Invalid placeholder element.')
85
+ }
86
+
87
+ const placeholder = getPlaceholder(sanitizeHtmlAttributesFromMdx(placeholderElement.attribs))
88
+
89
+ return renderPlaceholderToString(placeholder)
90
+ })
91
+ }
92
+
93
+ function renderPlaceholderToString(placeholder: Placeholder) {
94
+ let placeholderStr = `<${placeholder.type}`
95
+
96
+ for (const [key, value] of Object.entries(placeholder.props)) {
97
+ if (value === undefined) {
98
+ continue
99
+ }
100
+
101
+ placeholderStr = `${placeholderStr} ${key}="${value}"`
102
+ }
103
+
104
+ if (placeholder.type === 'img') {
105
+ return `${placeholderStr} />`
106
+ }
107
+
108
+ placeholderStr = `${placeholderStr}>`
109
+
110
+ if (placeholder.options.showTitle) {
111
+ placeholderStr = `${placeholderStr}<title>${placeholder.options.title}</title>`
112
+ }
113
+
114
+ placeholderStr = `${placeholderStr}<rect width="100%" height="100%" fill="${placeholder.options.background}" />`
115
+
116
+ if (placeholder.options.showText) {
117
+ placeholderStr = `${placeholderStr}<text x="50%" y="50%" fill="${placeholder.options.color}" dy=".3em">${placeholder.options.text}</text>`
118
+ }
119
+
120
+ return `${placeholderStr}</${placeholder.type}>`
121
+ }
122
+
123
+ function getOptionsWithDefaults(options: Partial<PlaceholderOptions>) {
124
+ const optionsWithDefaults = Object.assign(
125
+ {},
126
+ {
127
+ background: 'var(--cx-neutral-bg-evident)',
128
+ color: 'var(--cx-neutral-fg-subtle)',
129
+ // width: '100%',
130
+ // height: '180',
131
+ markup: 'svg',
132
+ title: 'Placeholder'
133
+ },
134
+ options
135
+ )
136
+
137
+ if (optionsWithDefaults.text === undefined) {
138
+ optionsWithDefaults.text = `${optionsWithDefaults.width}x${optionsWithDefaults.height}`
139
+ }
140
+
141
+ return optionsWithDefaults as PlaceholderOptions
142
+ }
143
+
144
+ function getPlaceholderSrc(
145
+ showTitle: boolean,
146
+ showText: boolean,
147
+ { background, color, text, title }: PlaceholderOptions
148
+ ) {
149
+ // Sanitize the background and text colors by removing the leading hash if any.
150
+ const bgColor = background.replace(/^#/, '')
151
+ const textColor = color.replace(/^#/, '')
152
+
153
+ // Build the raw SVG string first
154
+ let svg = `<svg style='font-size: 1.125rem; font-family:system-ui; -webkit-user-select: none; -moz-user-select: none; user-select: none; text-anchor: middle;' width='200' height='200' xmlns='http://www.w3.org/2000/svg'>`
155
+
156
+ if (showTitle) {
157
+ svg += `<title>${title}</title>`
158
+ }
159
+
160
+ svg += `<rect width='100%' height='100%' fill='#${bgColor}'></rect>`
161
+
162
+ if (showText) {
163
+ svg += `<text x='50%' y='50%' fill='#${textColor}' dy='.3em'>${text}</text>`
164
+ }
165
+
166
+ svg += `</svg>`
167
+
168
+ const encodedSvg = encodeURIComponent(svg)
169
+
170
+ return `data:image/svg+xml,${encodedSvg}`
171
+ }
172
+
173
+ function sanitizeHtmlAttributesFromMdx(attributes: Record<string, unknown>) {
174
+ const sanitizedAttributes: typeof attributes = {}
175
+
176
+ for (const [key, value] of Object.entries(attributes)) {
177
+ if (value === undefined) {
178
+ continue
179
+ } else if (value === '{false}') {
180
+ sanitizedAttributes[key] = false
181
+ } else if (value === '{true}') {
182
+ sanitizedAttributes[key] = true
183
+ } else {
184
+ sanitizedAttributes[key] = value
185
+ }
186
+ }
187
+
188
+ return sanitizedAttributes
189
+ }
190
+
191
+ export interface PlaceholderOptions {
192
+ /**
193
+ * The SVG background color.
194
+ * @default "#868f96"
195
+ */
196
+ background: string
197
+ /**
198
+ * CSS classes to append to `cxd-placeholder-img` for the `svg` or `img` elements.
199
+ */
200
+ class?: string
201
+ /**
202
+ * The text color (foreground).
203
+ * @default "#dee2e6"
204
+ */
205
+ color: string
206
+ /**
207
+ * The placeholder height.
208
+ * @default "180"
209
+ */
210
+ height: string
211
+ /**
212
+ * If it should render `svg` or `img` tags.
213
+ * @default "svg"
214
+ */
215
+ markup: 'img' | 'svg'
216
+ /**
217
+ * The text to show in the image. You can explicitely pass the `false` boolean value (and not the string "false") to
218
+ * hide the text.
219
+ * @default "${width}x${height}"
220
+ */
221
+ text: string | false
222
+ /**
223
+ * Used in the SVG `title` tag. You can explicitely pass the `false` boolean value (and not the string "false") to
224
+ * hide the title.
225
+ * @default "Placeholder"
226
+ */
227
+ title: string | false
228
+ /**
229
+ * The placeholder width.
230
+ * @default "100%"
231
+ */
232
+ width: string
233
+ }
234
+
235
+ interface PlaceholderVisibilities {
236
+ showText: boolean
237
+ showTitle: boolean
238
+ }
239
+
240
+ type Placeholder =
241
+ | {
242
+ type: 'img'
243
+ options: PlaceholderOptions & PlaceholderVisibilities
244
+ props: HTMLAttributes<'img'>
245
+ }
246
+ | {
247
+ type: 'svg'
248
+ options: PlaceholderOptions & PlaceholderVisibilities
249
+ props: HTMLAttributes<'svg'>
250
+ }
@@ -41,20 +41,3 @@ export const rehypeCxTable: Plugin<[], Root> = function () {
41
41
  })
42
42
  }
43
43
  }
44
-
45
- /**
46
- * Rehype plugin to strip `is:raw` attributes from code elements.
47
- *
48
- * The `@astrojs/markdown-remark` rehype-prism plugin adds `is:raw` as an Astro
49
- * directive, but it leaks into the final HTML output because rehype processes
50
- * it as a plain attribute rather than an Astro compiler directive.
51
- */
52
- export const rehypeStripIsRaw: Plugin<[], Root> = function () {
53
- return function rehypeStripIsRawPlugin(tree) {
54
- visit(tree, 'element', (node) => {
55
- if (node.properties && 'is:raw' in node.properties) {
56
- delete node.properties['is:raw']
57
- }
58
- })
59
- }
60
- }
@@ -0,0 +1,129 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { AstroIntegration } from 'astro'
4
+
5
+ interface MdxjsEsmNode {
6
+ type: 'mdxjsEsm'
7
+ value: string
8
+ data: { estree: object }
9
+ }
10
+
11
+ function buildImportsNode(imports: string[]): MdxjsEsmNode {
12
+ const body = imports.map((filePath) => {
13
+ const name = path.parse(filePath).name.replaceAll(/[^\w\d]/g, '')
14
+ return {
15
+ type: 'ImportDeclaration',
16
+ specifiers: [{ type: 'ImportDefaultSpecifier', local: { type: 'Identifier', name } }],
17
+ source: { type: 'Literal', value: filePath, raw: JSON.stringify(filePath) }
18
+ }
19
+ })
20
+
21
+ return {
22
+ type: 'mdxjsEsm',
23
+ value: '',
24
+ data: { estree: { type: 'Program', sourceType: 'module', body } }
25
+ }
26
+ }
27
+
28
+ export interface ChassisAutoImportOptions {
29
+ /** Absolute path to the docs site root (e.g. `getDocsFsPath()`). Used for the
30
+ * site-local shortcodes directory and the type definitions output path. */
31
+ docsPath: string
32
+ /** Absolute path to the directory containing `node_modules`. Defaults to
33
+ * `docsPath` when omitted — override when `node_modules` lives at the repo root. */
34
+ modulesPath?: string
35
+ }
36
+
37
+ function scanComponents({ docsPath, modulesPath = docsPath }: ChassisAutoImportOptions) {
38
+ const dirs = [
39
+ path.join(modulesPath, 'node_modules/@chassis-ui/docs/src/components/shortcodes'),
40
+ path.join(docsPath, 'src/components/shortcodes')
41
+ ]
42
+
43
+ const components: string[] = []
44
+ const definitions: string[] = []
45
+
46
+ for (const dir of dirs) {
47
+ if (!fs.existsSync(dir)) continue
48
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
49
+ if (!entry.isFile()) continue
50
+ components.push(`${dir}/${entry.name}`)
51
+ if (entry.name.endsWith('.astro')) {
52
+ const importPath = dir.includes('@chassis-ui/docs')
53
+ ? `@chassis-ui/docs/shortcodes`
54
+ : `@shortcodes`
55
+ definitions.push(
56
+ `export const ${entry.name.replace('.astro', '')}: typeof import('${importPath}/${entry.name}').default`
57
+ )
58
+ }
59
+ }
60
+ }
61
+
62
+ return { components, definitions }
63
+ }
64
+
65
+ /**
66
+ * Astro integration that writes the auto-import type definitions file, and a
67
+ * remark plugin factory for injecting shortcode imports into every MDX file.
68
+ *
69
+ * Replaces `astro-auto-import`, which triggers a deprecation warning in Astro 7.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * // astro.ts
74
+ * const { integration, plugin } = chassisAutoImport({ docsPath: getDocsFsPath(), modulesPath: process.cwd() })
75
+ *
76
+ * // astro.config.ts
77
+ * markdown: getDocsMarkdownConfig({ remarkPlugins: [plugin()] })
78
+ * integrations: [integration()]
79
+ * ```
80
+ */
81
+ export function chassisAutoImport(options: ChassisAutoImportOptions): {
82
+ /** Astro integration that writes `src/types/auto-import.d.ts`. */
83
+ integration: () => AstroIntegration
84
+ /** Factory for the remark plugin that injects imports into every `.mdx` file. */
85
+ plugin: () => ReturnType<typeof createAutoImportPlugin>
86
+ } {
87
+ const { components, definitions } = scanComponents(options)
88
+ const typeDefinitionsPath = path.join(options.docsPath, 'src/types/auto-import.d.ts')
89
+
90
+ return {
91
+ integration: () => ({
92
+ name: 'chassis-auto-import',
93
+ hooks: {
94
+ 'astro:config:setup': () => {
95
+ fs.writeFileSync(
96
+ typeDefinitionsPath,
97
+ `/**
98
+ * DO NOT EDIT THIS FILE MANUALLY.
99
+ *
100
+ * This file is automatically generated by the Chassis Astro Integration.
101
+ * It contains the type definitions for the components that are auto imported in all pages.
102
+ */
103
+ export declare global {
104
+ ${definitions.join('\n ')}
105
+ }
106
+ `
107
+ )
108
+ }
109
+ }
110
+ }),
111
+ plugin: () => createAutoImportPlugin(components)
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Returns a remark plugin that prepends import statements for the given component
117
+ * paths into every `.mdx` file.
118
+ */
119
+ export function createAutoImportPlugin(imports: string[]) {
120
+ const importsNode = buildImportsNode(imports)
121
+
122
+ return function remarkAutoImport() {
123
+ return function (tree: any, vfile: any) {
124
+ if (!vfile.basename?.endsWith('.md')) {
125
+ tree.children.unshift(importsNode)
126
+ }
127
+ }
128
+ }
129
+ }