@chassis-ui/docs 0.6.1 → 0.6.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @chassis-ui/docs
2
2
 
3
+ ## 0.6.3
4
+
5
+ ### Patch Changes
6
+
7
+ - The home page of a site under a path gets the home form of the title, `title · subtitle`. The layouts took the page at `/` as the home page, so a site with `baseURL` `https://chassis-ui.com/css/` never got it: its home page is at `/css/`. The home page is now the page at the path of `baseURL`. The title of such a home page changes with this release, and the `title` prop of its layout is used for the social tags only. Keep `title · subtitle` within 70 characters where the site validates its HTML.
8
+ - The color mode script is inline in the head and runs before the first paint. It was a module, which runs after the document is parsed, so a page in dark mode showed light first.
9
+ - Five fixes of the search dialog. The page scrolls again after a search result on the current page is opened: the dialog closed without removing the scroll lock of the body. The input has the focus when the dialog is opened with the button, so typing works at once. The keyboard shortcuts open the dialog as the button does: the page behind it no longer scrolls, and a click outside closes it. A click or Enter on a loading placeholder no longer closes the dialog and saves an empty visit. Escape closes the dialog in Safari when the focus is in the empty input.
10
+
11
+ ## 0.6.2
12
+
13
+ ### Patch Changes
14
+
15
+ - Add the `staticPath` key of `config.yml`: the URL path that the pages load the static files from, `/static` by default. A site that is served under a path of another host, such as `chassis-ui.com/css`, sets it to `/css/static` and rewrites that path to `/static`, so that its files no longer depend on the `Referer` header to be found. The layouts, the `Icon` shortcode and the scripts of the package use it, and `getStaticPath()` of `@chassis-ui/docs/site` returns a URL under it for the site's own files. See [A site under a prefix](https://github.com/chassis-ui/website/tree/main/packages/docs#a-site-under-a-prefix) in the README.
16
+
3
17
  ## 0.6.1
4
18
 
5
19
  ### Patch Changes
package/README.md CHANGED
@@ -106,23 +106,24 @@ repo: 'https://github.com/chassis-ui/css'
106
106
  currentVersion: '0.5.2'
107
107
  ```
108
108
 
109
- | Key | Required | Meaning |
110
- | --------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------- |
111
- | `title`, `subtitle`, `description`, `authors` | yes | Used in the page title and the meta tags |
112
- | `baseURL` | yes | Canonical URL of the site |
113
- | `docsPath` | yes | URL path of the docs pages. Starts with `/` |
114
- | `repo` | yes | URL of the repository |
115
- | `currentVersion` | yes | Version that the site documents. Links to source files use the tag `v<currentVersion>` |
116
- | `githubOrg` | no | GitHub organisation name, not a URL. The header links to it |
117
- | `figmaHandle` | no | Figma Community handle. The header links to it |
118
- | `xUsername` | no | X handle. Used in the social meta tags |
119
- | `analytics.googleId` | no | Google Analytics ID. In production builds, loaded after the visitor accepts in a consent banner |
120
- | `anchors.min`, `anchors.max` | no | Heading levels that get an anchor link. Default 2 to 5 |
121
- | `toc.min`, `toc.max` | no | Heading levels in the table of contents. Default 2 to 6 |
122
- | `sourceDir` | no | Directory that `file` props are relative to, from the site's root, e.g. `"../css"`. Default `"."` |
123
- | `sourcePath` | no | The same directory from the root of the repository, e.g. `"packages/css"` |
124
- | `sitePath` | no | The site's root from the root of the repository, e.g. `"packages/site"` |
125
- | `siteBranch` | no | The branch that "View on GitHub" links to. Default `main` |
109
+ | Key | Required | Meaning |
110
+ | --------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
111
+ | `title`, `subtitle`, `description`, `authors` | yes | Used in the page title and the meta tags |
112
+ | `baseURL` | yes | Canonical URL of the site. The page at its path is the home page: `/css/` for `https://chassis-ui.com/css/` |
113
+ | `docsPath` | yes | URL path of the docs pages. Starts with `/` |
114
+ | `staticPath` | no | URL path that the pages load the static files from. Default `/static`. See [A site under a prefix](#a-site-under-a-prefix) |
115
+ | `repo` | yes | URL of the repository |
116
+ | `currentVersion` | yes | Version that the site documents. Links to source files use the tag `v<currentVersion>` |
117
+ | `githubOrg` | no | GitHub organisation name, not a URL. The header links to it |
118
+ | `figmaHandle` | no | Figma Community handle. The header links to it |
119
+ | `xUsername` | no | X handle. Used in the social meta tags |
120
+ | `analytics.googleId` | no | Google Analytics ID. In production builds, loaded after the visitor accepts in a consent banner |
121
+ | `anchors.min`, `anchors.max` | no | Heading levels that get an anchor link. Default 2 to 5 |
122
+ | `toc.min`, `toc.max` | no | Heading levels in the table of contents. Default 2 to 6 |
123
+ | `sourceDir` | no | Directory that `file` props are relative to, from the site's root, e.g. `"../css"`. Default `"."` |
124
+ | `sourcePath` | no | The same directory from the root of the repository, e.g. `"packages/css"` |
125
+ | `sitePath` | no | The site's root from the root of the repository, e.g. `"packages/site"` |
126
+ | `siteBranch` | no | The branch that "View on GitHub" links to. Default `main` |
126
127
 
127
128
  `file` props are those of `<ScssDocs>`, `<ScssDocsSimple>`, `<JsDocs>` and `<Code filePath>`.
128
129
 
@@ -181,7 +182,7 @@ export const collections = {
181
182
 
182
183
  ### 4. Provide the static files
183
184
 
184
- The layouts link to files under `/static/`, which the site puts into its `public` directory:
185
+ The layouts link to files under `/static/`, which the site puts into `static/` of its `public` directory:
185
186
 
186
187
  | URL | From |
187
188
  | -------------------------------------------------------------------- | ----------------------------------------- |
@@ -195,6 +196,27 @@ These helpers find the folders, whether `node_modules` is in the site's root or
195
196
 
196
197
  A site that type-checks with `astro check` declares the `@chassis-ui/css` module, which has no types: `declare module '@chassis-ui/css'`.
197
198
 
199
+ #### A site under a prefix
200
+
201
+ A site that is served under a path of another host, such as `chassis-ui.com/css`, cannot use `/static/` there: that path belongs to the host's own site. It loads its static files under a path of its own instead:
202
+
203
+ ```yaml
204
+ # config.yml
205
+ staticPath: '/css/static'
206
+ ```
207
+
208
+ The layouts, the `Icon` shortcode and the scripts of the package then link `/css/static/css/chassis.min.css` and so on. The files do not move: they stay in `static/` of the `public` directory. Two rewrites make the path answer:
209
+
210
+ - on the host, from `/css/static/*` to `/static/*` of the site's deployment.
211
+ - in the site's own `vercel.json`, from `/css/static/(.*)` to `/static/$1`, so that the deployment also works when it is opened directly.
212
+
213
+ Two kinds of URL are the site's own to move:
214
+
215
+ - URLs that the site writes itself, in its pages and components. `getStaticPath('images/hero.png')` of `@chassis-ui/docs/site` returns the URL under `staticPath`.
216
+ - The files that Astro builds, when the site puts them under `static/` with `build.assets`. Astro's `build.assetsPrefix` adds the prefix to them, `'/css'` here.
217
+
218
+ A social image named in the frontmatter as `static/images/…` is read from the `public` directory and linked under `staticPath`.
219
+
198
220
  ### 5. Use the layouts
199
221
 
200
222
  ```astro
@@ -207,6 +229,8 @@ import SingleLayout from '@chassis-ui/docs/layouts/SingleLayout.astro'
207
229
  </SingleLayout>
208
230
  ```
209
231
 
232
+ The `<title>` of a page is its `title` and the `title` of `config.yml`: `About · Chassis UI`. The home page, which is the page at the path of `baseURL`, gets `title` and `subtitle` of `config.yml` instead: `Chassis UI · Design Systems that Scale & Sync`. Its own `title` is used for the social tags only. html-validate reports a `<title>` of more than 70 characters, so keep the two within that.
233
+
210
234
  ## Shortcodes
211
235
 
212
236
  Every component in `@chassis-ui/docs/shortcodes/` is available in MDX files without an import. So is every component in the site's `src/components/shortcodes/`. A component of the site replaces the package's component of the same name.
@@ -233,6 +257,7 @@ chassisDocs({
233
257
  | `getCallout(name)` | An entry of the `callouts` collection |
234
258
  | `getSiteRoot()`, `getSiteFsPath(file)` | Absolute path of the site's root, or of a file in it |
235
259
  | `getPublicFsPath(file)` | Absolute path of a file in the `public` directory |
260
+ | `getStaticPath(file)` | URL path of a static file, under `staticPath` of the config |
236
261
  | `getSourceFsPath(file)` | Absolute path of a file that a `file` prop names |
237
262
  | `getSourceUrl(file)` | URL of a source file on GitHub, at the tag of the current version |
238
263
  | `getSiteFileUrl(filePath)` | URL of a file of the site on GitHub, on the site's branch |
@@ -343,7 +368,7 @@ These import paths are the public API.
343
368
 
344
369
  No other path resolves: `exports` in `package.json` lists these and nothing else. The layouts and components are listed by name, so the folders inside `layouts/` and `components/` are not reachable.
345
370
 
346
- The integration provides four modules: `virtual:chassis-docs/config`, `virtual:chassis-docs/sidebar`, `virtual:chassis-docs/paths` and `virtual:chassis-docs/styles`. Use the functions of `@chassis-ui/docs/site` instead of importing them.
371
+ The integration provides five modules: `virtual:chassis-docs/config`, `virtual:chassis-docs/sidebar`, `virtual:chassis-docs/paths`, `virtual:chassis-docs/static` and `virtual:chassis-docs/styles`. Use the functions of `@chassis-ui/docs/site` instead of importing them.
347
372
 
348
373
  ## Compatibility
349
374
 
@@ -351,6 +376,8 @@ The integration provides four modules: `virtual:chassis-docs/config`, `virtual:c
351
376
 
352
377
  | `@chassis-ui/docs` | Requires Astro | Requires `@chassis-ui/css` | Requires Node | Built with |
353
378
  | ------------------ | -------------------------- | -------------------------- | ------------- | ------------------------------------ |
379
+ | 0.6.3 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.6.0 |
380
+ | 0.6.2 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.6.0 |
354
381
  | 0.6.1 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.6.0 |
355
382
  | 0.6.0 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.5.3 |
356
383
  | 0.5.1 | `^7.0.0` | `^0.5.0-0` | `>=22.12.0` | Astro 7.3.5, css 0.5.2, tokens 0.5.3 |
@@ -378,7 +405,7 @@ A change is breaking when it changes something that this README documents:
378
405
  - the keys of `config.yml`, and the schemas of the sidebar and the content collections
379
406
  - the props and slots of the layouts, components and shortcodes
380
407
  - the `[[config:]]` and `[[docsref:]]` syntax
381
- - the URLs under `/static/` that the layouts link to
408
+ - the URLs under `staticPath`, `/static/` by default, that the layouts link to
382
409
  - the supported versions of Node, Astro and `@chassis-ui/css`
383
410
 
384
411
  Everything else may change in a patch release: the rendered HTML, class names, the content of the styles, and every file that is not reachable through a documented import path.
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.6.1",
4
+ "version": "0.6.3",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -3,6 +3,7 @@ import { replacePlaceholdersInHtml } from '../../libs/placeholder'
3
3
  import { replaceIconsInHtml } from '../../libs/icon'
4
4
  import Code from './Code.astro'
5
5
  import Icon from './Icon.astro'
6
+ import { getStaticPath } from '../../site'
6
7
 
7
8
  interface Props {
8
9
  /**
@@ -69,7 +70,7 @@ const {
69
70
 
70
71
  let markup = Array.isArray(code) ? code.join('\n') : code
71
72
  markup = replacePlaceholdersInHtml(markup)
72
- markup = replaceIconsInHtml(markup)
73
+ markup = replaceIconsInHtml(markup, getStaticPath())
73
74
  markup = markup
74
75
  .split('\n')
75
76
  .map((line) => line.trimEnd())
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { getStaticPath } from '../../site'
3
+
2
4
  /**
3
5
  * Icon
4
6
  *
@@ -29,7 +31,7 @@ const {
29
31
  size = 24,
30
32
  font = false,
31
33
  title,
32
- sprite = '/static/icons/chassis-icons.svg',
34
+ sprite = getStaticPath('icons/chassis-icons.svg'),
33
35
  ...rest
34
36
  } = Astro.props
35
37
  ---
@@ -3,6 +3,7 @@ import { replacePlaceholdersInHtml } from '../../libs/placeholder'
3
3
  import { replaceIconsInHtml } from '../../libs/icon'
4
4
  import Code from './Code.astro'
5
5
  import Icon from './Icon.astro'
6
+ import { getStaticPath } from '../../site'
6
7
 
7
8
  interface Props {
8
9
  /**
@@ -67,7 +68,7 @@ const {
67
68
 
68
69
  let markup = Array.isArray(code) ? code.join('\n') : code
69
70
  markup = replacePlaceholdersInHtml(markup)
70
- markup = replaceIconsInHtml(markup)
71
+ markup = replaceIconsInHtml(markup, getStaticPath())
71
72
 
72
73
  // The resizable container needs `overflow: hidden` for the CSS `resize` handle
73
74
  // to work, which clips popovers like menus/dropdowns when they open. Render them
@@ -90,6 +90,10 @@ declare module 'virtual:chassis-docs/paths' {
90
90
  export const packageRoot: string
91
91
  }
92
92
 
93
+ declare module 'virtual:chassis-docs/static' {
94
+ export const staticPath: string
95
+ }
96
+
93
97
  declare module 'virtual:chassis-docs/styles' {}
94
98
  `
95
99
 
@@ -234,6 +238,8 @@ export function chassisDocs<TSchema extends z.ZodType = typeof configSchema>(
234
238
  paths: Object.entries(paths)
235
239
  .map(([name, value]) => `export const ${name} = ${JSON.stringify(value)}`)
236
240
  .join('\n'),
241
+ // A config that was not loaded with `loadConfig()` may lack the default.
242
+ static: `export const staticPath = ${JSON.stringify(config.staticPath ?? '/static')}`,
237
243
  styles: styles.map((file) => `import ${JSON.stringify(file)}`).join('\n')
238
244
  })
239
245
  ]
@@ -1,4 +1,5 @@
1
1
  import { Tooltip } from '@chassis-ui/css'
2
+ import { staticPath } from 'virtual:chassis-docs/static'
2
3
 
3
4
  // Helper to detect the user's preferred theme
4
5
  function getPreferredTheme() {
@@ -43,7 +44,7 @@ function toggleTheme(button) {
43
44
  const iconUse = button.querySelector('use')
44
45
  if (iconUse) {
45
46
  const iconHref = newTheme === 'dark' ? 'sun-solid' : 'moon-solid'
46
- iconUse.setAttribute('href', `/static/icons/chassis-icons.svg#${iconHref}`)
47
+ iconUse.setAttribute('href', `${staticPath}/icons/chassis-icons.svg#${iconHref}`)
47
48
  }
48
49
  }
49
50
 
@@ -64,7 +65,7 @@ document.addEventListener('DOMContentLoaded', () => {
64
65
  const example = button.closest('.cxd-example-snippet')?.querySelector('.cxd-example')
65
66
  const currentTheme = example?.getAttribute('data-cx-theme') || preferredTheme
66
67
  const iconHref = currentTheme === 'dark' ? 'sun-solid' : 'moon-solid'
67
- iconUse.setAttribute('href', `/static/icons/chassis-icons.svg#${iconHref}`)
68
+ iconUse.setAttribute('href', `${staticPath}/icons/chassis-icons.svg#${iconHref}`)
68
69
  }
69
70
 
70
71
  // Add event listener if not already set through onclick
package/src/js/search.js CHANGED
@@ -13,8 +13,12 @@
13
13
  // is needed here.
14
14
 
15
15
  import { getInstanceManager } from '@pagefind/component-ui'
16
+ import { staticPath } from 'virtual:chassis-docs/static'
16
17
 
17
18
  const DIALOG_SELECTOR = '#cxdSearchDialog'
19
+ // The rows that are links. The loading skeleton reuses the class on a `<div>`,
20
+ // which must not be followed, focused or saved as a visit.
21
+ const ITEM_SELECTOR = 'a.cxd-search-item'
18
22
  const SUB_RESULTS_LIMIT = 3
19
23
  // How long a search may take before we show the loading skeleton. Most queries
20
24
  // resolve faster, so the previous results stay on-screen instead of flashing.
@@ -233,7 +237,7 @@ const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) =>
233
237
  const renderItem = ({ url, titleHtml, excerptHtml = '', icon, sub = false }) => `
234
238
  <a class="cxd-search-item${sub ? ' cxd-search-subitem' : ''}" href="${escapeHtml(url)}">
235
239
  <svg class="icon cxd-search-item-icon" width="16" height="16" aria-hidden="true">
236
- <use href="/static/icons/chassis-icons.svg#${icon}"></use>
240
+ <use href="${staticPath}/icons/chassis-icons.svg#${icon}"></use>
237
241
  </svg>
238
242
  <div class="cxd-search-item-body">
239
243
  <div class="cxd-search-item-title">${titleHtml}</div>
@@ -293,12 +297,21 @@ class CxdSearchInput extends HTMLElement {
293
297
  return
294
298
  }
295
299
 
300
+ // Safari keeps Escape for the search field, even an empty one, and the
301
+ // dialog never gets its cancel event. With text in the field the key clears
302
+ // it, as in every browser; without text it closes the dialog here.
303
+ if (event.key === 'Escape' && !this.input.value) {
304
+ event.preventDefault()
305
+ closeSearchDialog(this.closest('dialog'))
306
+ return
307
+ }
308
+
296
309
  // Enter follows the first result link.
297
310
  // Always preventDefault so the form never implicit-submits.
298
311
  if (event.key === 'Enter') {
299
312
  event.preventDefault()
300
313
  const dialogEl = this.closest('dialog')
301
- dialogEl?.querySelector('.cxd-search-item')?.click()
314
+ dialogEl?.querySelector(ITEM_SELECTOR)?.click()
302
315
  }
303
316
  }
304
317
 
@@ -338,11 +351,11 @@ class CxdSearchResults extends HTMLElement {
338
351
  }
339
352
 
340
353
  _getLinks() {
341
- return [...this.querySelectorAll('.cxd-search-item')]
354
+ return [...this.querySelectorAll(ITEM_SELECTOR)]
342
355
  }
343
356
 
344
357
  _onKeydown(event) {
345
- const link = event.target.closest('.cxd-search-item')
358
+ const link = event.target.closest(ITEM_SELECTOR)
346
359
  if (!link) return
347
360
 
348
361
  const links = this._getLinks()
@@ -616,6 +629,29 @@ const isEditableTarget = (target) => {
616
629
  return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT'
617
630
  }
618
631
 
632
+ const TRIGGER_SELECTOR = `[data-cx-toggle="dialog"][data-cx-target="${DIALOG_SELECTOR}"]`
633
+
634
+ // Puts the caret in the input once the dialog is open. The chassis Dialog
635
+ // focuses the dialog element itself when it opens, so a key typed right after a
636
+ // click on the trigger went nowhere.
637
+ const focusSearchInput = (dialogEl) => {
638
+ requestAnimationFrame(() => {
639
+ if (!dialogEl.open) return
640
+ dialogEl.querySelector('cxd-search-input')?.focus()
641
+ if (!instance.searchTerm) {
642
+ dialogEl.querySelector('cxd-search-results')?._renderEmpty?.()
643
+ }
644
+ })
645
+ }
646
+
647
+ const registerTriggerHandler = () => {
648
+ document.addEventListener('click', (event) => {
649
+ if (!event.target.closest(TRIGGER_SELECTOR)) return
650
+ const dialogEl = document.querySelector(DIALOG_SELECTOR)
651
+ if (dialogEl) focusSearchInput(dialogEl)
652
+ })
653
+ }
654
+
619
655
  const openSearchDialog = () => {
620
656
  const dialogEl = document.querySelector(DIALOG_SELECTOR)
621
657
  if (!dialogEl) return
@@ -625,15 +661,24 @@ const openSearchDialog = () => {
625
661
  return
626
662
  }
627
663
 
628
- // Native showModal() — CSS @starting-style handles the entry animation without
629
- // needing the chassis bundle. Works in both dev (no bundle) and prod.
630
- dialogEl.showModal()
631
- requestAnimationFrame(() => {
632
- dialogEl.querySelector('cxd-search-input')?.focus()
633
- if (!instance.searchTerm) {
634
- dialogEl.querySelector('cxd-search-results')?._renderEmpty?.()
635
- }
636
- })
664
+ // Open through the trigger, so that a shortcut does what the button does: the
665
+ // chassis Dialog locks the scroll of the body, closes on a click on the
666
+ // backdrop and gives the focus back. Without the chassis bundle the click does
667
+ // nothing, and the native showModal() opens the dialog.
668
+ document.querySelector(TRIGGER_SELECTOR)?.click()
669
+ if (!dialogEl.open) {
670
+ dialogEl.showModal()
671
+ focusSearchInput(dialogEl)
672
+ }
673
+ }
674
+
675
+ // Closes through the dismiss button, so that the chassis Dialog animates the
676
+ // dialog out, unlocks the scroll of the body and gives the focus back. Without
677
+ // the chassis bundle the click does nothing, and the native close() closes it.
678
+ const closeSearchDialog = (dialogEl) => {
679
+ if (!dialogEl?.open) return
680
+ dialogEl.querySelector('[data-cx-dismiss="dialog"]')?.click()
681
+ if (dialogEl.open && !dialogEl.classList.contains('hiding')) dialogEl.close()
637
682
  }
638
683
 
639
684
  const registerGlobalShortcuts = () => {
@@ -682,7 +727,7 @@ const setupDialogResetOnClose = () => {
682
727
 
683
728
  const registerResultLinkHandler = () => {
684
729
  document.addEventListener('click', (event) => {
685
- const link = event.target.closest('.cxd-search-item')
730
+ const link = event.target.closest(ITEM_SELECTOR)
686
731
  if (!link) return
687
732
  const dialogEl = link.closest('dialog')
688
733
  if (!dialogEl?.matches(DIALOG_SELECTOR)) return
@@ -696,6 +741,14 @@ const registerResultLinkHandler = () => {
696
741
  })
697
742
 
698
743
  dialogEl.close()
744
+
745
+ // The trigger opens the dialog through the chassis Dialog, which locks the
746
+ // scroll of the body with this class and unlocks it in its own hide(). The
747
+ // native close() above skips that, so the class stays: when the result is on
748
+ // the current page, nothing reloads and the page can no longer scroll.
749
+ if (!document.querySelector('dialog[open]:modal')) {
750
+ document.body.classList.remove('dialog-open')
751
+ }
699
752
  })
700
753
  }
701
754
 
@@ -703,6 +756,7 @@ const registerResultLinkHandler = () => {
703
756
 
704
757
  defineSearchCustomElements()
705
758
  registerGlobalShortcuts()
759
+ registerTriggerHandler()
706
760
  registerResultLinkHandler()
707
761
 
708
762
  if (document.readyState === 'loading') {
@@ -4,6 +4,8 @@
4
4
  *
5
5
  * Site footer with logo, license information, and navigation links.
6
6
  */
7
+
8
+ import { getStaticPath } from '../../site'
7
9
  ---
8
10
 
9
11
  <footer class="cxd-footer py-6xl">
@@ -15,7 +17,7 @@
15
17
  href="/"
16
18
  aria-label="Chassis"
17
19
  >
18
- <img src="/static/images/site-logo.svg" alt="Chassis UI" height="32" />
20
+ <img src={getStaticPath('images/site-logo.svg')} alt="Chassis UI" height="32" />
19
21
  </a>
20
22
  <p class="mb-xs">
21
23
  This project is licensed under the MIT License, which allows for free use, modification,
@@ -4,11 +4,13 @@
4
4
  *
5
5
  * Favicon and app icon link tags for various platforms.
6
6
  */
7
+
8
+ import { getStaticPath } from '../../site'
7
9
  ---
8
10
 
9
- <link rel="apple-touch-icon" href={'/static/images/apple-touch-icon.png'} sizes="180x180" />
10
- <link rel="icon" href={'/static/images/favicon-32x32.png'} sizes="32x32" type="image/png" />
11
- <link rel="icon" href={'/static/images/favicon-16x16.png'} sizes="16x16" type="image/png" />
12
- <link rel="manifest" href={'/static/images/manifest.json'} />
13
- <link rel="icon" href={'/static/images/favicon.png'} />
11
+ <link rel="apple-touch-icon" href={getStaticPath('images/apple-touch-icon.png')} sizes="180x180" />
12
+ <link rel="icon" href={getStaticPath('images/favicon-32x32.png')} sizes="32x32" type="image/png" />
13
+ <link rel="icon" href={getStaticPath('images/favicon-16x16.png')} sizes="16x16" type="image/png" />
14
+ <link rel="manifest" href={getStaticPath('images/manifest.json')} />
15
+ <link rel="icon" href={getStaticPath('images/favicon.png')} />
14
16
  <meta name="theme-color" content="#00A4CC" />
@@ -6,10 +6,12 @@
6
6
  */
7
7
 
8
8
  import { getConfig } from '../../site'
9
+ import { isHomePath } from '../../libs/docs-path'
9
10
  import type { Layout } from '../../libs/layout'
10
11
  import Stylesheet from './Stylesheet.astro'
11
12
  import Favicons from './Favicons.astro'
12
13
  import Social from './Social.astro'
14
+ import colorModes from '../../js/color-modes.js?raw'
13
15
  import 'virtual:chassis-docs/styles'
14
16
 
15
17
  interface Props {
@@ -29,7 +31,8 @@ const { description, layout, robots, thumbnail, title } = Astro.props
29
31
 
30
32
  const canonicalUrl = new URL(Astro.url.pathname, Astro.site)
31
33
 
32
- const isHome = Astro.url.pathname === '/'
34
+ // The home page of a site under a path, such as `/css/`, is not at `/`
35
+ const isHome = isHomePath(getConfig().baseURL, Astro.url.pathname)
33
36
  const pageTitle = isHome
34
37
  ? `${getConfig().title} · ${getConfig().subtitle}`
35
38
  : `${title} · ${getConfig().title}`
@@ -60,7 +63,11 @@ const pageTitle = isHome
60
63
 
61
64
  {robots && <meta name="robots" content={robots} />}
62
65
 
63
- <script src="../../js/color-modes.js"></script>
66
+ <!--
67
+ Inline and not a module, so that it sets the theme before the first paint. A module runs
68
+ after the document is parsed, and a page in dark mode would show light first
69
+ -->
70
+ <script is:inline set:html={colorModes} />
64
71
 
65
72
  <Stylesheet />
66
73
  <slot name="styles" />
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { getRemoteImageSize, getStaticImageSize } from '../../libs/image'
9
9
  import type { Layout } from '../../libs/layout'
10
- import { getConfig, getPublicFsPath } from '../../site'
10
+ import { getConfig, getPublicFsPath, getStaticPath } from '../../site'
11
11
 
12
12
  interface Props {
13
13
  /** Page description */
@@ -22,7 +22,10 @@ interface Props {
22
22
 
23
23
  const { description, layout, thumbnail, title } = Astro.props
24
24
 
25
- const socialImageUrl = new URL(thumbnail, Astro.site)
25
+ // A file in `static/` of the `public` directory is read from there, and linked under
26
+ // `staticPath` of the config.
27
+ const staticFile = thumbnail.match(/^\/?static\/(.+)$/)?.[1]
28
+ const socialImageUrl = new URL(staticFile ? getStaticPath(staticFile) : thumbnail, Astro.site)
26
29
  const isRemoteThumbnail = /^https?:\/\//i.test(thumbnail)
27
30
  const socialImageSize = isRemoteThumbnail
28
31
  ? await getRemoteImageSize(thumbnail)
@@ -6,9 +6,10 @@
6
6
  */
7
7
 
8
8
  import type { HTMLAttributes } from 'astro/types'
9
+ import { getStaticPath } from '../../site'
9
10
 
10
11
  function getChassisCssProps() {
11
- let cxCssLinkHref = '/static/css/chassis'
12
+ let cxCssLinkHref = getStaticPath('css/chassis')
12
13
 
13
14
  if (import.meta.env.PROD) {
14
15
  cxCssLinkHref = `${cxCssLinkHref}.min`
@@ -26,4 +27,4 @@ function getChassisCssProps() {
26
27
  ---
27
28
 
28
29
  <link {...getChassisCssProps()} />
29
- <link rel="stylesheet" href={`/static/icons/chassis-icons.css`} />
30
+ <link rel="stylesheet" href={getStaticPath('icons/chassis-icons.css')} />
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Layout } from '../../libs/layout'
9
- import { getConfig } from '../../site'
9
+ import { getConfig, getStaticPath } from '../../site'
10
10
  import NavLink from '../../components/NavLink.astro'
11
11
  import ThemeToggler from '../../components/ThemeToggler.astro'
12
12
  import SearchTrigger from './SearchTrigger.astro'
@@ -55,7 +55,7 @@ const isActive = (path: string) => {
55
55
  {layout !== 'docs' && <div class="lg:d-none" style="width: 4.25rem;" />}
56
56
 
57
57
  <a class="navbar-brand m-0 py-sm lg:me-md" href="/" aria-label="Chassis">
58
- <img src="/static/images/site-logo.svg" alt="Chassis UI" width="123" height="32" />
58
+ <img src={getStaticPath('images/site-logo.svg')} alt="Chassis UI" width="123" height="32" />
59
59
  </a>
60
60
 
61
61
  <div class="d-flex">
@@ -10,3 +10,23 @@ export function joinDocsPath(docsPath: string, inputPath: string): string {
10
10
 
11
11
  return page ? `${base}/${page}` : base
12
12
  }
13
+
14
+ /** Joins the static path of the config and a file: `('/css/static', 'css/chassis.css')`. */
15
+ export function joinStaticPath(staticPath: string, file = ''): string {
16
+ const name = file.replace(/^\/+/, '')
17
+
18
+ return name ? `${staticPath}/${name}` : staticPath
19
+ }
20
+
21
+ /**
22
+ * Whether a page is the home page of the site: the page at the path of `baseURL`. That is
23
+ * `/` for `https://chassis-ui.com` and `/css/` for `https://chassis-ui.com/css/`.
24
+ *
25
+ * @param baseURL The `baseURL` of the site's `config.yml`.
26
+ * @param pathname URL path of the page, with or without a trailing slash.
27
+ */
28
+ export function isHomePath(baseURL: string, pathname: string): boolean {
29
+ const trim = (value: string) => value.replace(/\/+$/, '')
30
+
31
+ return trim(pathname) === trim(new URL(baseURL).pathname)
32
+ }
package/src/libs/icon.ts CHANGED
@@ -20,7 +20,7 @@ interface IconOptions {
20
20
  * actually invoked — they need to be expanded to their rendered markup ahead of time.
21
21
  * @see src/components/shortcodes/Icon.astro
22
22
  */
23
- export function replaceIconsInHtml(html: string) {
23
+ export function replaceIconsInHtml(html: string, staticPath = '/static') {
24
24
  return html.replace(iconRegex, (match) => {
25
25
  const document = htmlparser2.parseDocument(match, { xmlMode: true })
26
26
  const iconElement = document.firstChild
@@ -34,11 +34,11 @@ export function replaceIconsInHtml(html: string) {
34
34
  throw new Error('Invalid icon element.')
35
35
  }
36
36
 
37
- return renderIconToString(getOptionsWithDefaults(iconElement.attribs))
37
+ return renderIconToString(getOptionsWithDefaults(iconElement.attribs, staticPath))
38
38
  })
39
39
  }
40
40
 
41
- function getOptionsWithDefaults(attribs: Record<string, string>): IconOptions {
41
+ function getOptionsWithDefaults(attribs: Record<string, string>, staticPath: string): IconOptions {
42
42
  if (!attribs.name) {
43
43
  throw new Error('Icon element is missing the required "name" attribute.')
44
44
  }
@@ -48,7 +48,7 @@ function getOptionsWithDefaults(attribs: Record<string, string>): IconOptions {
48
48
  class: attribs.class,
49
49
  size: attribs.size ? Number(attribs.size.replace(/[{}]/g, '')) : 24,
50
50
  font: attribs.font !== undefined && attribs.font !== '{false}',
51
- sprite: attribs.sprite || '/static/icons/chassis-icons.svg',
51
+ sprite: attribs.sprite || `${staticPath}/icons/chassis-icons.svg`,
52
52
  title: attribs.title
53
53
  }
54
54
  }
@@ -70,6 +70,16 @@ export const configSchema = z.strictObject({
70
70
  sourceDir: z.string().default('.'),
71
71
  /** The same directory from the root of the repository, e.g. `"packages/css"`. */
72
72
  sourcePath: z.string().optional(),
73
+ /**
74
+ * URL path that the pages load the static files from, e.g. `"/css/static"`. The files stay
75
+ * in `static/` of the `public` directory: a site under a prefix of another host sets this
76
+ * and rewrites the path to `/static`.
77
+ */
78
+ staticPath: z
79
+ .string()
80
+ .regex(/^\/(.*[^/])?$/, 'Expected a URL path that starts with "/" and has no trailing slash')
81
+ .refine((value) => value !== '/', 'Expected a path below the root, such as "/static"')
82
+ .default('/static'),
73
83
  subtitle: z.string(),
74
84
  title: z.string(),
75
85
  /** Heading levels that the table of contents lists. */
package/src/site.ts CHANGED
@@ -9,7 +9,7 @@ import sidebar from 'virtual:chassis-docs/sidebar'
9
9
  import * as paths from 'virtual:chassis-docs/paths'
10
10
  import { getCollection, getEntry, type render } from 'astro:content'
11
11
  import { loadData } from './libs/config'
12
- import { joinDocsPath } from './libs/docs-path'
12
+ import { joinDocsPath, joinStaticPath } from './libs/docs-path'
13
13
  import { getNodeModulesFsPaths, resolvePackageFilePath } from './libs/paths'
14
14
  import { replaceConfigInText, replaceDocsrefInText } from './libs/remark'
15
15
  import type { ChassisConfig, DocsPage, Sidebar } from './libs/schema'
@@ -35,6 +35,14 @@ export function getDocsPath(inputPath: string): string {
35
35
  return joinDocsPath(config.docsPath, inputPath)
36
36
  }
37
37
 
38
+ /**
39
+ * URL path of a static file, under `staticPath` of the config:
40
+ * `getStaticPath('images/site-logo.svg')` is `/static/images/site-logo.svg` by default.
41
+ */
42
+ export function getStaticPath(file = ''): string {
43
+ return joinStaticPath(config.staticPath ?? '/static', file)
44
+ }
45
+
38
46
  /** Absolute path of the site's root: the directory that holds `astro.config.ts`. */
39
47
  export function getSiteRoot(): string {
40
48
  return paths.root
package/virtual.d.ts CHANGED
@@ -22,4 +22,9 @@ declare module 'virtual:chassis-docs/paths' {
22
22
  export const packageRoot: string
23
23
  }
24
24
 
25
+ declare module 'virtual:chassis-docs/static' {
26
+ /** `staticPath` of the config, for scripts that run in the browser. */
27
+ export const staticPath: string
28
+ }
29
+
25
30
  declare module 'virtual:chassis-docs/styles' {}