@chassis-ui/docs 0.5.1 → 0.6.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@chassis-ui/docs",
3
3
  "description": "Shared Astro components and layouts for Chassis documentation sites.",
4
- "version": "0.5.1",
4
+ "version": "0.6.0",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -25,18 +25,35 @@
25
25
  "files": [
26
26
  "src",
27
27
  "index.ts",
28
+ "virtual.d.ts",
28
29
  "README.md",
29
- "LICENSE"
30
+ "UPGRADING.md",
31
+ "LICENSE",
32
+ "CHANGELOG.md"
30
33
  ],
31
34
  "main": "index.ts",
32
35
  "exports": {
33
36
  ".": "./index.ts",
34
- "./components/*": "./src/components/*",
37
+ "./integration": "./src/integration.ts",
38
+ "./schema": "./src/libs/schema.ts",
39
+ "./site": "./src/site.ts",
40
+ "./layouts/BaseLayout.astro": "./src/layouts/BaseLayout.astro",
41
+ "./layouts/DocsLayout.astro": "./src/layouts/DocsLayout.astro",
42
+ "./layouts/RedirectLayout.astro": "./src/layouts/RedirectLayout.astro",
43
+ "./layouts/SingleLayout.astro": "./src/layouts/SingleLayout.astro",
44
+ "./components/DocsSidebar.astro": "./src/components/DocsSidebar.astro",
45
+ "./components/FeatureCard.astro": "./src/components/FeatureCard.astro",
46
+ "./components/NavLink.astro": "./src/components/NavLink.astro",
47
+ "./components/ResponsiveImage.astro": "./src/components/ResponsiveImage.astro",
48
+ "./components/TableOfContents.astro": "./src/components/TableOfContents.astro",
49
+ "./components/ThemeToggler.astro": "./src/components/ThemeToggler.astro",
35
50
  "./shortcodes/*": "./src/components/shortcodes/*",
36
- "./layouts/*": "./src/layouts/*",
37
- "./libs/*": "./src/libs/*",
38
51
  "./js/*": "./src/js/*",
39
- "./scss/*": "./src/scss/*"
52
+ "./scss/main": "./src/scss/main.scss",
53
+ "./scss/main.scss": "./src/scss/main.scss",
54
+ "./scss/vars": "./src/scss/_vars.scss",
55
+ "./scss/_vars.scss": "./src/scss/_vars.scss",
56
+ "./package.json": "./package.json"
40
57
  },
41
58
  "publishConfig": {
42
59
  "access": "public"
@@ -59,18 +76,24 @@
59
76
  "shiki": "^4.3.1"
60
77
  },
61
78
  "dependencies": {
79
+ "@types/hast": "^3.0.5",
80
+ "@types/mdast": "^4.0.4",
81
+ "clipboard": "^2.0.11",
62
82
  "github-slugger": "^2.0.0",
63
83
  "image-size": "^2.0.4",
84
+ "js-yaml": "^5.4.2",
64
85
  "mdast-util-from-markdown": "^2.0.3",
86
+ "mdast-util-mdx-jsx": "^3.2.0",
65
87
  "mdast-util-to-string": "^4.0.0",
66
88
  "medium-zoom": "^1.1.0",
67
89
  "remark": "^15.0.1",
68
90
  "remark-html": "^16.0.1",
91
+ "unified": "^11.0.5",
69
92
  "unist-util-visit": "^5.1.0"
70
93
  },
71
94
  "devDependencies": {
72
- "@types/hast": "^3.0.5",
73
95
  "astro": "^7.3.5",
74
- "unified": "^11.0.5"
96
+ "remark-mdx": "^3.1.1",
97
+ "vfile": "^6.0.3"
75
98
  }
76
99
  }
@@ -5,13 +5,13 @@
5
5
  * Navigation sidebar for documentation pages with hierarchical menu structure.
6
6
  */
7
7
 
8
- import { getData, type SidebarItem, type SidebarSubItem } from '@libs/data'
9
- import { getConfig } from '@libs/config'
10
- import { docsPages } from '@libs/content'
8
+ import type { SidebarItem, SidebarSubItem } from '../libs/schema'
11
9
  import { getSlug } from '../libs/utils'
10
+ import { getDocsPages, getDocsPath, getSidebar } from '../site'
12
11
  import Icon from './shortcodes/Icon.astro'
13
12
 
14
- const sidebar = getData('sidebar')
13
+ const sidebar = getSidebar()
14
+ const docsPages = await getDocsPages()
15
15
  ---
16
16
 
17
17
  <nav class="cxd-links w-100" id="cxd-docs-nav" aria-label="Docs navigation">
@@ -32,7 +32,7 @@ const sidebar = getData('sidebar')
32
32
  {group.icon && (
33
33
  <Icon
34
34
  name={group.icon}
35
- style={group.icon_color && `color: var(--cx-${group.icon_color});`}
35
+ style={group.iconColor && `color: var(--cx-${group.iconColor});`}
36
36
  />
37
37
  )}
38
38
  {group.title}
@@ -48,7 +48,7 @@ const sidebar = getData('sidebar')
48
48
  const docSlug = getSlug(page.title)
49
49
  const unversionedPageSlug = `${groupSlug}/${docSlug}`
50
50
 
51
- const url = `${getConfig().docsPath}/${unversionedPageSlug}`
51
+ const url = getDocsPath(unversionedPageSlug)
52
52
  const active = Astro.params.slug === unversionedPageSlug
53
53
 
54
54
  const generatedPage = docsPages.find(
@@ -58,7 +58,7 @@ const sidebar = getData('sidebar')
58
58
  // This test should not be necessary, see comments for `getSlug()` in `src/libs/utils.ts`.
59
59
  if (!generatedPage) {
60
60
  throw new Error(
61
- `The page '${page.title}' referenced in 'site/data/sidebar.yml' does not exist at '${url}'.`
61
+ `The page '${page.title}' referenced in 'data/sidebar.yml' does not exist at '${url}'.`
62
62
  )
63
63
  }
64
64
 
@@ -86,7 +86,7 @@ const sidebar = getData('sidebar')
86
86
  const docSlug = getSlug(item.title)
87
87
  const unversionedPageSlug = `${groupSlug}/${docSlug}`
88
88
 
89
- const url = `${getConfig().docsPath}/${unversionedPageSlug}`
89
+ const url = getDocsPath(unversionedPageSlug)
90
90
  const active = Astro.params.slug === unversionedPageSlug
91
91
 
92
92
  const generatedPage = docsPages.find((page) => page.id === unversionedPageSlug)
@@ -120,7 +120,7 @@ const sidebar = getData('sidebar')
120
120
  <>
121
121
  <li class="cxd-links-span-all mt-2xs mb-md mx-lg border-top" />
122
122
  <a
123
- href={`${getConfig().docsPath}/${groupSlug}/`}
123
+ href={getDocsPath(`${groupSlug}/`)}
124
124
  class:list={['cxd-links-link cxd-links-span-all', { active }]}
125
125
  aria-current={active ? 'page' : undefined}
126
126
  >
@@ -5,16 +5,16 @@
5
5
  * Display informational callout boxes with different types (info, danger, warning).
6
6
  */
7
7
 
8
- import { getCalloutByName, type CalloutName } from '@libs/content'
9
8
  import { render } from 'astro:content'
9
+ import { getCallout } from '../../site'
10
10
  import type { MarkdownInstance } from 'astro'
11
11
 
12
12
  interface Props {
13
13
  /**
14
- * The name of an existing callout to display located in `src/content/callouts`.
14
+ * The name of an entry of the site's `callouts` collection to display.
15
15
  * This will override any content passed in via the default slot.
16
16
  */
17
- name?: CalloutName
17
+ name?: string
18
18
  /**
19
19
  * The type of callout to display. One of `info`, `danger`, or `warning`.
20
20
  * @default 'info'
@@ -31,7 +31,7 @@ const { name, type = 'info', class: className } = Astro.props
31
31
  let Content: MarkdownInstance<{}>['Content'] | undefined
32
32
 
33
33
  if (name) {
34
- const callout = await getCalloutByName(name)
34
+ const callout = await getCallout(name)
35
35
 
36
36
  if (!callout) {
37
37
  throw new Error(`Could not find callout with name '${name}'.`)
@@ -2,7 +2,7 @@
2
2
  import fs from 'node:fs'
3
3
  import { transformerNotationDiff, transformerNotationHighlight } from '@shikijs/transformers'
4
4
  import { highlightCode } from '../../libs/highlight'
5
- import { getSourceFsPath, getSourceUrl } from '../../libs/source'
5
+ import { getSourceFsPath, getSourceUrl } from '../../site'
6
6
  import Icon from './Icon.astro'
7
7
 
8
8
  interface Props {
@@ -41,7 +41,7 @@ interface Props {
41
41
  fileMatch?: string
42
42
  /**
43
43
  * A path to the file containing the code to highlight, relative to the `sourceDir` of the
44
- * site's `config.yml` (the working directory of the build when it is not set).
44
+ * site's `config.yml` (the site's root when it is not set).
45
45
  * This takes precedence over the `code` prop.
46
46
  */
47
47
  filePath?: string
@@ -147,7 +147,7 @@ if (codeToDisplay && detectedLang) {
147
147
 
148
148
  <script>
149
149
  import { Tooltip } from '@chassis-ui/css'
150
- import { initCopyButtons } from '@libs/clipboard'
150
+ import { initCopyButtons } from '../../js/clipboard'
151
151
 
152
152
  document.querySelectorAll('.button-edit').forEach((button) => {
153
153
  Tooltip.getOrCreateInstance(button, { title: 'Edit on StackBlitz' })
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import fs from 'node:fs'
3
- import { getSourceFsPath } from '../../libs/source'
3
+ import { getSourceFsPath } from '../../site'
4
+ import { extractDocsSnippet } from '../../libs/snippet'
4
5
  import Code from './Code.astro'
5
6
 
6
7
  // Prints everything between `// js-docs-start "name"` and `// js-docs-end "name"`
@@ -13,7 +14,7 @@ interface Props {
13
14
  name: string
14
15
  /**
15
16
  * File path that contains the content to display, relative to the `sourceDir` of the site's
16
- * `config.yml` (the working directory of the build when it is not set).
17
+ * `config.yml` (the site's root when it is not set).
17
18
  */
18
19
  file: string
19
20
  }
@@ -31,17 +32,15 @@ let content: string
31
32
  try {
32
33
  const fileContent = fs.readFileSync(getSourceFsPath(file), 'utf8')
33
34
 
34
- const matches = fileContent.match(
35
- new RegExp(`// js-docs-start ${name}\n((?:.|\n)*)// js-docs-end ${name}`, 'm')
36
- )
35
+ const snippet = extractDocsSnippet(fileContent, 'js', name)
37
36
 
38
- if (!matches || !matches[1]) {
37
+ if (!snippet) {
39
38
  throw new Error(
40
39
  `Failed to find the content named '${name}', make sure that '// js-docs-start ${name}' and '// js-docs-end ${name}' are defined.`
41
40
  )
42
41
  }
43
42
 
44
- content = matches[1]
43
+ content = snippet
45
44
 
46
45
  // Fix the identation by removing extra spaces at the beginning of each line
47
46
  const lines = content.split('\n')
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  import fs from 'node:fs'
3
- import path from 'node:path'
4
3
  import * as sass from 'sass'
5
- import { getSourceFsPath } from '../../libs/source'
4
+ import { getNodeModulesPaths, getSourceFsPath } from '../../site'
5
+ import { extractDocsSnippet } from '../../libs/snippet'
6
6
  import Code from './Code.astro'
7
7
 
8
8
  // Prints everything between `// scss-docs-start "name"` and `// scss-docs-end "name"`
@@ -16,7 +16,7 @@ interface Props {
16
16
  name: string
17
17
  /**
18
18
  * File path that contains the content to display, relative to the `sourceDir` of the site's
19
- * `config.yml` (the working directory of the build when it is not set).
19
+ * `config.yml` (the site's root when it is not set).
20
20
  */
21
21
  file: string
22
22
  /**
@@ -78,17 +78,15 @@ let lang: string = 'scss'
78
78
  try {
79
79
  const fileContent = fs.readFileSync(getSourceFsPath(file), 'utf8')
80
80
 
81
- const matches = fileContent.match(
82
- new RegExp(`// scss-docs-start ${name}\n((?:.|\n)*)// scss-docs-end ${name}`, 'm')
83
- )
81
+ const snippet = extractDocsSnippet(fileContent, 'scss', name)
84
82
 
85
- if (!matches || !matches[1]) {
83
+ if (!snippet) {
86
84
  throw new Error(
87
85
  `Failed to find the content named '${name}', make sure that '// scss-docs-start ${name}' and '// scss-docs-end ${name}' are defined.`
88
86
  )
89
87
  }
90
88
 
91
- const rawContent = matches[1]
89
+ const rawContent = snippet
92
90
 
93
91
  if (compile) {
94
92
  // Derive the SCSS load path from the file argument (e.g. "scss/_foo.scss" → "scss/")
@@ -113,7 +111,7 @@ try {
113
111
  // Vite's own Sass importer resolves bare npm specifiers (e.g. `@chassis-ui/tokens/...`)
114
112
  // through its module resolver, independent of `loadPaths`. This raw Sass JS API call
115
113
  // bypasses Vite, so `node_modules` must be added here explicitly.
116
- path.resolve(process.cwd(), 'node_modules')
114
+ ...getNodeModulesPaths()
117
115
  ],
118
116
  // Match the same deprecation silencing used in astro.config.ts / vite scss options
119
117
  silenceDeprecations: ['import', 'global-builtin', 'color-functions', 'if-function'] as any
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import fs from 'node:fs'
3
- import { getSourceFsPath } from '../../libs/source'
3
+ import { getSourceFsPath } from '../../site'
4
+ import { extractDocsSnippet } from '../../libs/snippet'
4
5
  import Code from './Code.astro'
5
6
 
6
7
  // Prints everything between `// scss-docs-start "name"` and `// scss-docs-end "name"`
@@ -13,7 +14,7 @@ interface Props {
13
14
  name: string
14
15
  /**
15
16
  * File path that contains the content to display, relative to the `sourceDir` of the site's
16
- * `config.yml` (the working directory of the build when it is not set).
17
+ * `config.yml` (the site's root when it is not set).
17
18
  */
18
19
  file: string
19
20
  }
@@ -33,17 +34,15 @@ let content: string
33
34
  try {
34
35
  const fileContent = fs.readFileSync(getSourceFsPath(file), 'utf8')
35
36
 
36
- const matches = fileContent.match(
37
- new RegExp(`// scss-docs-start ${name}\n((?:.|\n)*)// scss-docs-end ${name}`, 'm')
38
- )
37
+ const snippet = extractDocsSnippet(fileContent, 'scss', name)
39
38
 
40
- if (!matches || !matches[1]) {
39
+ if (!snippet) {
41
40
  throw new Error(
42
41
  `Failed to find the content named '${name}', make sure that '// scss-docs-start ${name}' and '// scss-docs-end ${name}' are defined.`
43
42
  )
44
43
  }
45
44
 
46
- content = matches[1].replaceAll(' !default', '')
45
+ content = snippet.replaceAll(' !default', '')
47
46
 
48
47
  // Fix the identation by removing extra spaces at the beginning of each line
49
48
  const lines = content.split('\n')
@@ -0,0 +1,271 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import type { AstroIntegration, AstroUserConfig, ViteUserConfig } from 'astro'
5
+ import type { z } from 'astro/zod'
6
+ import { loadConfig, loadSidebar } from './libs/config'
7
+ import { joinDocsPath } from './libs/docs-path'
8
+ import { getDocsMarkdownConfig, type DocsMarkdownConfigOptions } from './libs/markdown'
9
+ import { getInstalledPackageFsPath, getPackageRoot } from './libs/paths'
10
+ import { remarkCxConfig, remarkCxDocsref } from './libs/remark'
11
+ import { configSchema, type ChassisConfig, type Sidebar } from './libs/schema'
12
+ import { chassisAutoImport, type ChassisAutoImportOptions } from './libs/shortcodes'
13
+ import { getSiteUrl } from './libs/site'
14
+
15
+ export interface ChassisDocsOptions<TSchema extends z.ZodType = typeof configSchema> {
16
+ /**
17
+ * The config, for a site that has loaded it with `loadConfig()` to use it in
18
+ * `astro.config.ts`. `configFile` is then only watched, and `configSchema` is not used.
19
+ */
20
+ config?: ChassisConfig
21
+ /**
22
+ * Path of the config file, from the site's root.
23
+ * @default 'config.yml'
24
+ */
25
+ configFile?: string
26
+ /**
27
+ * Schema of the config file: `configSchema` from `@chassis-ui/docs/schema`, or an
28
+ * extension of it when the site has keys of its own.
29
+ */
30
+ configSchema?: TSchema
31
+ /**
32
+ * Path of the sidebar data file, from the site's root. `false` for a site that does not
33
+ * render `<DocsSidebar>`.
34
+ * @default 'data/sidebar.yml' when the file exists
35
+ */
36
+ sidebarFile?: string | false
37
+ /**
38
+ * Stylesheets that every page loads, from the site's root. A site's stylesheet is expected
39
+ * to `@use "@chassis-ui/docs/scss/main"`.
40
+ * @default ['src/scss/docs.scss'] when the file exists, the package's styles otherwise
41
+ */
42
+ styles?: string[]
43
+ /** Which shortcodes are imported into every MDX file. */
44
+ shortcodes?: Pick<ChassisAutoImportOptions, 'dir' | 'include' | 'exclude'>
45
+ /** Plugins of the site. They run after the plugins of the package. */
46
+ markdown?: Pick<DocsMarkdownConfigOptions, 'rehypePlugins' | 'remarkPlugins' | 'remarkRehype'>
47
+ /**
48
+ * What happens when a `[[docsref:]]` link points to a page that was not built.
49
+ * @default 'error'
50
+ */
51
+ brokenDocsrefs?: 'error' | 'warn' | 'ignore'
52
+ }
53
+
54
+ type VitePlugin = Extract<NonNullable<ViteUserConfig['plugins']>[number], { name: string }>
55
+
56
+ const virtualPrefix = 'virtual:chassis-docs/'
57
+ const resolvedPrefix = '\0' + virtualPrefix
58
+
59
+ function virtualModules(modules: Record<string, string>): VitePlugin {
60
+ return {
61
+ name: 'chassis-docs:virtual',
62
+ resolveId(id) {
63
+ if (id.startsWith(virtualPrefix) && id.slice(virtualPrefix.length) in modules) {
64
+ return '\0' + id
65
+ }
66
+ },
67
+ load(id) {
68
+ if (id.startsWith(resolvedPrefix)) {
69
+ return modules[id.slice(resolvedPrefix.length)]
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ // The declarations of `virtual.d.ts`, for a site.
76
+ const virtualTypes = `declare module 'virtual:chassis-docs/config' {
77
+ const config: import('@chassis-ui/docs/schema').ChassisConfig
78
+ export default config
79
+ }
80
+
81
+ declare module 'virtual:chassis-docs/sidebar' {
82
+ const sidebar: import('@chassis-ui/docs/schema').Sidebar
83
+ export default sidebar
84
+ }
85
+
86
+ declare module 'virtual:chassis-docs/paths' {
87
+ export const root: string
88
+ export const publicDir: string
89
+ export const sourceDir: string
90
+ export const packageRoot: string
91
+ }
92
+
93
+ declare module 'virtual:chassis-docs/styles' {}
94
+ `
95
+
96
+ // The folder of `@chassis-ui/css` with its default `_chassis-tokens.scss`, when the site has
97
+ // the package installed.
98
+ function getDefaultTokensLoadPath(root: string): string | undefined {
99
+ try {
100
+ return path.join(getInstalledPackageFsPath('@chassis-ui/css', root), 'scss/vendor')
101
+ } catch {
102
+ return undefined
103
+ }
104
+ }
105
+
106
+ // Append `index.html` when the path names a page, and drop the hash.
107
+ function toBuiltFilePath(docsPath: string): string {
108
+ const withoutHash = docsPath.split('#')[0]
109
+
110
+ return withoutHash.includes('.') ? withoutHash : path.join(withoutHash, 'index.html')
111
+ }
112
+
113
+ /**
114
+ * The Astro integration of `@chassis-ui/docs`. A site adds it in `astro.config.ts`.
115
+ *
116
+ * It reads `config.yml` and `data/sidebar.yml`, gives them to the layouts and components of
117
+ * the package, sets `site` and `markdown` of the Astro config, and imports the shortcodes
118
+ * into every MDX file.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * import { defineConfig } from 'astro/config'
123
+ * import { chassisDocs } from '@chassis-ui/docs/integration'
124
+ *
125
+ * export default defineConfig({
126
+ * integrations: [chassisDocs()]
127
+ * })
128
+ * ```
129
+ */
130
+ export function chassisDocs<TSchema extends z.ZodType = typeof configSchema>(
131
+ options: ChassisDocsOptions<TSchema> = {}
132
+ ): AstroIntegration {
133
+ const { brokenDocsrefs = 'error' } = options
134
+
135
+ // Docs paths that `[[docsref:]]` links name, collected while the pages are rendered.
136
+ const linkedDocsPaths = new Set<string>()
137
+ let config: ChassisConfig
138
+ let typeDefinitions = ''
139
+
140
+ return {
141
+ name: '@chassis-ui/docs',
142
+ hooks: {
143
+ 'astro:config:setup': ({ config: astroConfig, command, updateConfig, addWatchFile }) => {
144
+ const root = path.resolve(fileURLToPath(astroConfig.root))
145
+ const configFile = path.resolve(root, options.configFile ?? 'config.yml')
146
+
147
+ config =
148
+ options.config ??
149
+ loadConfig({
150
+ file: configFile,
151
+ schema: (options.configSchema ?? configSchema) as z.ZodType<ChassisConfig>
152
+ })
153
+ addWatchFile(configFile)
154
+
155
+ let sidebar: Sidebar = []
156
+
157
+ if (options.sidebarFile !== false) {
158
+ const sidebarFile = path.resolve(root, options.sidebarFile ?? 'data/sidebar.yml')
159
+
160
+ // The default file is optional. A file that the site names must exist.
161
+ if (options.sidebarFile !== undefined || fs.existsSync(sidebarFile)) {
162
+ sidebar = loadSidebar({ file: sidebarFile })
163
+ addWatchFile(sidebarFile)
164
+ }
165
+ }
166
+
167
+ const defaultStyles = path.join(root, 'src/scss/docs.scss')
168
+ const styles = options.styles
169
+ ? options.styles.map((file) => path.resolve(root, file))
170
+ : fs.existsSync(defaultStyles)
171
+ ? [defaultStyles]
172
+ : ['@chassis-ui/docs/scss/main.scss']
173
+
174
+ const autoImport = chassisAutoImport({ root, ...options.shortcodes })
175
+ typeDefinitions = autoImport.typeDefinitions
176
+
177
+ const paths = {
178
+ root,
179
+ publicDir: path.resolve(fileURLToPath(astroConfig.publicDir)),
180
+ sourceDir: path.resolve(root, config.sourceDir),
181
+ packageRoot: getPackageRoot()
182
+ }
183
+
184
+ const tokensLoadPath = getDefaultTokensLoadPath(root)
185
+
186
+ const update: AstroUserConfig = {
187
+ // A `site` that the Astro config sets itself is kept.
188
+ ...(astroConfig.site ? {} : { site: getSiteUrl(config) }),
189
+ markdown: getDocsMarkdownConfig({
190
+ anchors: config.anchors,
191
+ remarkPlugins: [
192
+ autoImport.plugin(),
193
+ [remarkCxConfig, { config }],
194
+ [
195
+ remarkCxDocsref,
196
+ {
197
+ docsPath: config.docsPath,
198
+ onDocsPath:
199
+ command === 'build'
200
+ ? (docsPath: string) => linkedDocsPaths.add(docsPath)
201
+ : undefined
202
+ }
203
+ ],
204
+ ...(options.markdown?.remarkPlugins ?? [])
205
+ ],
206
+ rehypePlugins: options.markdown?.rehypePlugins,
207
+ remarkRehype: options.markdown?.remarkRehype
208
+ }),
209
+ vite: {
210
+ // Scripts of the package and of the site both import `@chassis-ui/css`, and two
211
+ // copies of it register every listener twice. In the dev server, a script of the
212
+ // package would be pre-bundled with a copy of its own, and a script of the site
213
+ // would get a pre-bundled copy while the package gets the source. Both packages
214
+ // are served from source, and every import resolves to the site's copy.
215
+ optimizeDeps: {
216
+ exclude: ['@chassis-ui/docs', '@chassis-ui/css'],
217
+ // The one CommonJS dependency of the package's scripts. It has no default export
218
+ // unless it is pre-bundled.
219
+ include: ['@chassis-ui/docs > clipboard']
220
+ },
221
+ resolve: {
222
+ dedupe: ['@chassis-ui/css']
223
+ },
224
+ // The styles of `@chassis-ui/css` load the design tokens as `chassis-tokens`. This
225
+ // adds its default as the last load path, after the site's own load paths, so a
226
+ // site with a `_chassis-tokens.scss` of its own puts that folder in its config.
227
+ ...(tokensLoadPath
228
+ ? { css: { preprocessorOptions: { scss: { loadPaths: [tokensLoadPath] } } } }
229
+ : {}),
230
+ plugins: [
231
+ virtualModules({
232
+ config: `export default ${JSON.stringify(config)}`,
233
+ sidebar: `export default ${JSON.stringify(sidebar)}`,
234
+ paths: Object.entries(paths)
235
+ .map(([name, value]) => `export const ${name} = ${JSON.stringify(value)}`)
236
+ .join('\n'),
237
+ styles: styles.map((file) => `import ${JSON.stringify(file)}`).join('\n')
238
+ })
239
+ ]
240
+ }
241
+ }
242
+
243
+ // The user config is wider than what `updateConfig()` declares for `markdown`.
244
+ updateConfig(update as Parameters<typeof updateConfig>[0])
245
+ },
246
+ 'astro:config:done': ({ injectTypes }) => {
247
+ injectTypes({ filename: 'virtual.d.ts', content: virtualTypes })
248
+ injectTypes({ filename: 'auto-import.d.ts', content: typeDefinitions })
249
+ },
250
+ 'astro:build:done': ({ dir, logger }) => {
251
+ if (brokenDocsrefs === 'ignore') return
252
+
253
+ const broken = [...linkedDocsPaths].filter((docsPath) => {
254
+ const builtPath = toBuiltFilePath(joinDocsPath(config.docsPath, docsPath))
255
+
256
+ return !fs.existsSync(path.join(fileURLToPath(dir), builtPath))
257
+ })
258
+
259
+ if (broken.length === 0) return
260
+
261
+ const message = `These [[docsref:]] links point to a page or file that was not built:\n${broken.map((docsPath) => ` - ${docsPath}`).join('\n')}`
262
+
263
+ if (brokenDocsrefs === 'error') {
264
+ throw new Error(message)
265
+ }
266
+
267
+ logger.warn(message)
268
+ }
269
+ }
270
+ }
271
+ }
@@ -0,0 +1,65 @@
1
+ import ClipboardJS from 'clipboard'
2
+ import { Tooltip } from '@chassis-ui/css'
3
+
4
+ const btnTitle = 'Copy'
5
+
6
+ export function initCopyButtons(
7
+ selector: string,
8
+ textFn: (_element: Element) => string
9
+ ): ClipboardJS {
10
+ document.querySelectorAll(selector).forEach((btn) => {
11
+ Tooltip.getOrCreateInstance(btn, { title: btnTitle })
12
+ })
13
+
14
+ const clipboard = new ClipboardJS(selector, { text: textFn })
15
+
16
+ clipboard.on('success', (event) => {
17
+ const useEl = event.trigger.querySelector('.icon use')
18
+ const tooltipBtn = Tooltip.getInstance(event.trigger)
19
+ const originalHref = useEl?.getAttribute('href')
20
+
21
+ if (originalHref === '#check-solid') {
22
+ return
23
+ }
24
+
25
+ tooltipBtn?.setContent({ '.tooltip-inner': 'Copied!' })
26
+
27
+ event.trigger.addEventListener(
28
+ 'hidden.cx.tooltip',
29
+ () => {
30
+ tooltipBtn?.setContent({ '.tooltip-inner': btnTitle })
31
+ },
32
+ { once: true }
33
+ )
34
+
35
+ event.clearSelection()
36
+
37
+ if (useEl) {
38
+ useEl.setAttribute('href', '#check-solid')
39
+ }
40
+
41
+ setTimeout(() => {
42
+ if (useEl && originalHref) {
43
+ useEl.setAttribute('href', originalHref)
44
+ }
45
+ }, 2000)
46
+ })
47
+
48
+ clipboard.on('error', (event) => {
49
+ const modifierKey = /mac/i.test(navigator.userAgent) ? '\u2318' : 'Ctrl-'
50
+ const fallbackMsg = `Press ${modifierKey}C to copy`
51
+ const tooltipBtn = Tooltip.getInstance(event.trigger)
52
+
53
+ tooltipBtn?.setContent({ '.tooltip-inner': fallbackMsg })
54
+
55
+ event.trigger.addEventListener(
56
+ 'hidden.cx.tooltip',
57
+ () => {
58
+ tooltipBtn?.setContent({ '.tooltip-inner': btnTitle })
59
+ },
60
+ { once: true }
61
+ )
62
+ })
63
+
64
+ return clipboard
65
+ }