@chassis-ui/docs 0.9.0 → 0.11.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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @chassis-ui/docs
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **Breaking.** The head links the web app manifest of the docs build of `chassis-assets`, `other/manifest.json` under the static path, in place of `images/manifest.json`, which each site provided itself. chassis-assets added it in `v0.5.0` of `app/docs` on 2026-10-23, with a maskable icon. A site moves its `vendor/assets` submodule to `v0.5.0` or later with `pnpm sync-submodules`, and deletes its own `images/manifest.json`; with an earlier commit the page links a manifest that is not there. The starter keeps a manifest with its own name at `static/other/manifest.json`. See [UPGRADING.md](https://github.com/chassis-ui/website/blob/main/packages/docs/UPGRADING.md#from-010-to-011).
8
+
9
+ ### Patch Changes
10
+
11
+ - The head links the Chassis CSS, `icons/chassis-icons.css` and `fonts/fonts.css` with `?v=` and the first 8 characters of the MD5 hash of the file, and the `<Icon>` shortcode, the icons of `<Example>` and `<ResizableExample>`, the search results and the color mode button of the examples load the sprite the same way: `/static/css/chassis.min.css?v=8e2106f6`. A new file is a new URL, so a host can let the browser keep these files for good, and a new CSS reaches a visitor with the pages that need it. The hash is read from the file of the site's `public` directory at the path of the URL; without that file the URL has no version, as before. `getVersionedStaticPath(file)` of `@chassis-ui/docs/site` returns such a URL, `replaceIconsInHtml()` takes the sprite as a third argument, and `virtual:chassis-docs/static` exports `spritePath`.
12
+
13
+ ## 0.10.0
14
+
15
+ ### Minor Changes
16
+
17
+ - **Breaking.** The head links one font stylesheet, `fonts/fonts.css` under the static path, in place of `fonts/text.css`, `fonts/display.css` and `fonts/code.css`, and the navigation bar and the footer inline the logo, `images/site-logo.svg`, in place of loading it with `<img>`. chassis-assets made both changes on `app/docs` on 2026-10-19: a brand has one `fonts.css`, and the logo of the `chassis` brand has no `xmlns` and is colored by the classes `icon-default` and `icon-primary` of the page, so it shows only inline. A site moves its `vendor/assets` submodule to `db6d64b` of `app/docs` or later with `pnpm sync-submodules`; with an earlier commit the pages show the fallback fonts. The build reads `static/images/site-logo.svg` of the site's `public` directory and fails without it. The logo is `2rem` high, and the footer gives its link the margin `mb-md` in place of `mb-xs`. The search of the header is `5rem` wide from the `lg` breakpoint, the icon and the shortcut, and `12.5rem` from `xl`, where it was 200px, so that no link of the header runs under it. See [UPGRADING.md](https://github.com/chassis-ui/website/blob/main/packages/docs/UPGRADING.md#from-09-to-010).
18
+
3
19
  ## 0.9.0
4
20
 
5
21
  ### Minor Changes
package/README.md CHANGED
@@ -184,15 +184,15 @@ export const collections = {
184
184
 
185
185
  The layouts link to files under `/static/`, which the site puts into `static/` of its `public` directory:
186
186
 
187
- | URL | From |
188
- | ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- |
189
- | `/static/css/chassis.css`, `/static/css/chassis.min.css` | the `dist` folder of `@chassis-ui/css` |
190
- | `/static/icons/chassis-icons.svg`, `/static/icons/chassis-icons.css` | the `icons` folder of `@chassis-ui/icons` |
191
- | `/static/images/site-logo.svg`, the logo of the navigation bar and the footer | the docs build of `chassis-assets` |
192
- | `/static/images/favicon.png`, `favicon-16x16.png`, `favicon-32x32.png` and `apple-touch-icon.png` | the docs build of `chassis-assets` |
193
- | `/static/images/social-image.png`, the image of a page without a `thumbnail`. The build reads its size, and fails without the file | the docs build of `chassis-assets` |
194
- | `/static/fonts/text.css`, `/static/fonts/display.css`, `/static/fonts/code.css`, and the font files they load | the docs build of `chassis-assets` |
195
- | `/static/images/manifest.json`, the web app manifest | the site itself. The starter has one in `static/images/` |
187
+ | URL | From |
188
+ | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
189
+ | `/static/css/chassis.css`, `/static/css/chassis.min.css` | the `dist` folder of `@chassis-ui/css` |
190
+ | `/static/icons/chassis-icons.svg`, `/static/icons/chassis-icons.css` | the `icons` folder of `@chassis-ui/icons` |
191
+ | `/static/images/site-logo.svg`, the logo of the navigation bar and the footer. The build inlines it, and fails without the file | the docs build of `chassis-assets` |
192
+ | `/static/images/favicon.png`, `favicon-16x16.png`, `favicon-32x32.png` and `apple-touch-icon.png` | the docs build of `chassis-assets` |
193
+ | `/static/images/social-image.png`, the image of a page without a `thumbnail`. The build reads its size, and fails without the file | the docs build of `chassis-assets` |
194
+ | `/static/fonts/fonts.css`, and the font files it loads | the docs build of `chassis-assets` |
195
+ | `/static/other/manifest.json`, the web app manifest | the docs build of `chassis-assets`, `v0.5.0` or later. A site may put its own in its `static/other/`, as the starter does |
196
196
 
197
197
  `chassis-docs vendor` builds the docs build of `chassis-assets` in the `vendor/assets` submodule. See [Commands](#commands).
198
198
 
@@ -210,9 +210,9 @@ The sites of the Chassis projects read these files besides, by name, from the do
210
210
  | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
211
211
  | `icons/cx-sprite.svg` | The home page of each of the seven sites, which imports it. The build fails without it |
212
212
  | `images/apple-touch-icon.png`, `images/favicon.png` | Each of the seven sites, which copies them to `/apple-touch-icon.png` and `/favicon.ico` |
213
- | `images/android-chrome-192x192.png`, `images/android-chrome-512x512.png` | The `manifest.json` of each of the seven sites |
213
+ | `images/android-chrome-192x192.png`, `images/android-chrome-512x512.png` | `other/manifest.json` of the docs build, which the head links |
214
214
  | `images/home/` | The home page of the website. The home page of chassis-tokens reads `comp-gallery-*` of it |
215
- | `images/logo/chassis-{logo,icon}-{brand,white}-banner.svg` | The about page of the website |
215
+ | `images/logo/logo-{light,dark}.svg` | The about page of the website |
216
216
  | `images/social-image.png` | The blog of the website, for a post without an image |
217
217
  | `images/site-logo.svg` | The examples of the navbar page of chassis-css. The README of `@chassis-ui/react`, by its URL on chassis-ui.com |
218
218
  | `images/figma/components/` | The component pages of chassis-figma |
@@ -242,6 +242,8 @@ Two kinds of URL are the site's own to move:
242
242
 
243
243
  A social image named in the frontmatter as `static/images/…` is read from the `public` directory and linked under `staticPath`.
244
244
 
245
+ The layouts link the Chassis CSS, the icon stylesheet, `fonts/fonts.css` and the icon sprite with a version: `/static/css/chassis.min.css?v=8e2106f6`, the first 8 characters of the MD5 hash of the file. A new file is a new URL, so a host can let the browser keep these files for good. The version is read from the file of `public` at the path of the URL, so a site under a prefix whose `public` holds the files in `static/` links them without one.
246
+
245
247
  ### 5. Use the layouts
246
248
 
247
249
  ```astro
@@ -283,6 +285,7 @@ chassisDocs({
283
285
  | `getSiteRoot()`, `getSiteFsPath(file)` | Absolute path of the site's root, or of a file in it |
284
286
  | `getPublicFsPath(file)` | Absolute path of a file in the `public` directory |
285
287
  | `getStaticPath(file)` | URL path of a static file, under `staticPath` of the config |
288
+ | `getVersionedStaticPath(file)` | The same, with `?v=` and a hash of the file in `public` |
286
289
  | `getSourceFsPath(file)` | Absolute path of a file that a `file` prop names |
287
290
  | `getSourceUrl(file)` | URL of a source file on GitHub, at the tag of the current version |
288
291
  | `getSiteFileUrl(filePath)` | URL of a file of the site on GitHub, on the site's branch |
@@ -417,6 +420,8 @@ The integration provides five modules: `virtual:chassis-docs/config`, `virtual:c
417
420
 
418
421
  | `@chassis-ui/docs` | Requires Astro | Requires `@chassis-ui/css` | Requires Node | Built with |
419
422
  | ------------------ | -------------------------- | -------------------------- | ------------- | -------------------------------------- |
423
+ | 0.11.0 | `^7.0.0` | `>=0.10.0` | `>=22.12.0` | Astro 7.3.8, css 0.10.0, tokens 0.10.0 |
424
+ | 0.10.0 | `^7.0.0` | `>=0.10.0` | `>=22.12.0` | Astro 7.3.8, css 0.10.0, tokens 0.10.0 |
420
425
  | 0.9.0 | `^7.0.0` | `>=0.10.0` | `>=22.12.0` | Astro 7.3.8, css 0.10.0, tokens 0.10.0 |
421
426
  | 0.8.0 | `^7.0.0` | `>=0.6.0` | `>=22.12.0` | Astro 7.3.5, css 0.7.1, tokens 0.7.0 |
422
427
  | 0.7.0 | `^7.0.0` | `>=0.6.0` | `>=22.12.0` | Astro 7.3.5, css 0.6.0, tokens 0.6.0 |
package/UPGRADING.md CHANGED
@@ -1,5 +1,183 @@
1
1
  # Upgrading @chassis-ui/docs
2
2
 
3
+ ## From 0.10 to 0.11
4
+
5
+ 0.11.0 links the web app manifest of the docs build of `chassis-assets`,
6
+ `other/manifest.json` under the static path, in place of `images/manifest.json`, which each
7
+ site provided itself. chassis-assets added the manifest, with a maskable icon, in `v0.5.0`
8
+ of `app/docs` on 2026-10-23.
9
+
10
+ ### 1. Update the dependency and the assets
11
+
12
+ In the package of the site, then from the root of the repository:
13
+
14
+ ```sh
15
+ pnpm add -D @chassis-ui/docs@^0.11.0
16
+ pnpm sync-submodules
17
+ git add vendor/assets
18
+ ```
19
+
20
+ The docs build has `other/manifest.json` from `v0.5.0` on. With an earlier commit the page
21
+ links a manifest that is not there. Stage or commit the pin before you run `pnpm vendor` or a
22
+ build, which check the submodule out at the commit that the repository records.
23
+
24
+ ### 2. Remove the site's own manifest
25
+
26
+ Delete `images/manifest.json` from the files that the site copies into `public/static/`,
27
+ `static/static/images/` in most Chassis repositories. A site that keeps a manifest with a name
28
+ of its own puts it at `static/other/manifest.json` of what it copies after the docs build, as
29
+ the starter does, and names the icons `../images/…`.
30
+
31
+ ### What changes in the package
32
+
33
+ - **The head links `other/manifest.json`** under the static path, and no longer
34
+ `images/manifest.json`.
35
+ - The pages link the Chassis CSS, `icons/chassis-icons.css`, `fonts/fonts.css` and the icon
36
+ sprite with `?v=` and a hash of the file, so a host can cache them for good.
37
+
38
+ ### Check the result
39
+
40
+ Open the page in a browser and check that the manifest loads: in Chrome, Application >
41
+ Manifest names the site and shows its icons.
42
+
43
+ ## From 0.8 to 0.10
44
+
45
+ 0.9 and 0.10 are both breaking minors, and a site on 0.8 takes them in one upgrade: the
46
+ steps of [From 0.8 to 0.9](#from-08-to-09) and [From 0.9 to 0.10](#from-09-to-010) together,
47
+ in this order. 0.9 reads the typography names of `@chassis-ui/css` 0.10. 0.10 links one font
48
+ stylesheet, `fonts/fonts.css`, and inlines the logo, after chassis-assets changed both on
49
+ `app/docs` on 2026-10-19.
50
+
51
+ ### 1. Update the dependencies
52
+
53
+ In the package of the site, `packages/site` in most Chassis repositories:
54
+
55
+ ```sh
56
+ pnpm add -D @chassis-ui/docs@^0.10.0 @chassis-ui/css@^0.10.0 @chassis-ui/tokens@^0.10.0
57
+ ```
58
+
59
+ The default token source of css, `scss/vendor/_chassis-tokens.scss`, forwards the
60
+ `@chassis-ui/tokens` that the site installs, and css 0.10 reads tokens that only tokens 0.10
61
+ has, so the three move together. A site that takes css or tokens from its own workspace keeps
62
+ that dependency and moves the workspace to 0.10. css 0.8 to 0.10 have changes of their own.
63
+ Read the [changelog of css](https://github.com/chassis-ui/css/blob/main/packages/css/CHANGELOG.md)
64
+ first.
65
+
66
+ ### 2. Move the `vendor/assets` submodule
67
+
68
+ From the root of the repository:
69
+
70
+ ```sh
71
+ pnpm sync-submodules
72
+ git add vendor/assets
73
+ ```
74
+
75
+ `sync-submodules` moves the submodule to the latest commit of `app/docs` and builds it. The
76
+ docs build has `fonts/fonts.css` and the `ui-*` font files from `db6d64b` on, and the logo
77
+ for inline use from `4382de9` on. With a commit before `db6d64b` the pages show the fallback
78
+ fonts.
79
+
80
+ Stage the pin, or commit it, before you run `pnpm vendor` or a build: `vendor` checks the
81
+ submodule out at the commit that the repository records, and a build from the old pin links a
82
+ stylesheet that is not there.
83
+
84
+ chassis-assets has no submodule: its site is built from its own `source/`, which has
85
+ `fonts.css` from `db6d64b` on. A site that does not copy the docs build provides
86
+ `fonts/fonts.css` and the font files it names itself.
87
+
88
+ ### 3. Rename the typography names in your own pages and styles
89
+
90
+ The table under [From 0.8 to 0.9](#2-rename-the-typography-names-in-your-own-pages-and-styles)
91
+ lists them: `.h1` to `.h6` are `.font-h1` to `.font-h6`, `.font-text` is `.font-ui`, the
92
+ `--cx-*-text-*` properties are `--cx-*-ui-*`, and the `$font-context-*` maps are `$font-*`.
93
+ css 0.10 keeps `.h1` to `.h6`, `.font-text` and `--cx-font-family-text` as deprecated names.
94
+ The other old names are gone: a Sass name that is gone fails the build, and a class or a
95
+ custom property that is gone does nothing, so look for those in the pages.
96
+
97
+ ### 4. Look for the old files of the docs build in your own files
98
+
99
+ The package links the new files, but a page or a stylesheet of your own may name the old
100
+ ones:
101
+
102
+ ```sh
103
+ git grep -nE "fonts/(text|display|code)\.css|text-[a-z0-9-]*\.woff2|site-logo\.svg" -- ':!vendor'
104
+ ```
105
+
106
+ - **`fonts/text.css`, `fonts/display.css`, `fonts/code.css`:** link `fonts/fonts.css`.
107
+ - **`text-*.woff2`:** the files of the `text` role are `ui-*.woff2` now, such as
108
+ `ui-latin.woff2` and `ui-normal.woff2`.
109
+ - **`images/site-logo.svg` in an `<img>`:** the logo from `4382de9` on has no `xmlns`, so a
110
+ browser does not show it as an image, and its paths take their colors from the classes
111
+ `icon-default` and `icon-primary` of the page. Inline it, as the navigation bar does, or use a
112
+ file of `images/logo/`, which an `<img>` shows, such as `images/logo/chassis-logo-brand.svg`.
113
+
114
+ ### What changes on the pages
115
+
116
+ - **The head links `fonts/fonts.css`** under the static path, in place of the three
117
+ stylesheets of the roles.
118
+ - **The navigation bar and the footer inline the logo,** `2rem` high, in the colors of the
119
+ theme. The build reads `static/images/site-logo.svg` of the site's `public` directory and
120
+ fails without it. The link of the logo in the footer has the margin `mb-md`, where it was
121
+ `mb-xs`.
122
+ - **The search of the header is `5rem` wide** from the `lg` breakpoint, the icon and the
123
+ shortcut, and `12.5rem` from `xl`, where it was 200px.
124
+ - **The footer and the docs sidebar use `.font-h6` and `.font-h5`,** and `.cxd-subtitle`
125
+ reads `$font-lead`.
126
+ - **From css 0.10 and tokens 0.10:** the headings and the titles are in the display family,
127
+ Montserrat, the default tokens have new base colors, and buttons, badges and the other
128
+ components that had one lose their background gradient.
129
+
130
+ ### Check the result
131
+
132
+ Build the site before and after the upgrade and compare the pages in a browser, in both color
133
+ modes and between 1024 and 1280 pixels, where the search of the header changes. In the
134
+ network panel, no request under `/static/fonts/` may fail.
135
+
136
+ ## From 0.9 to 0.10
137
+
138
+ 0.10.0 links one font stylesheet, `fonts/fonts.css`, in place of `fonts/text.css`,
139
+ `fonts/display.css` and `fonts/code.css`, and inlines the logo of the navigation bar and the
140
+ footer, `images/site-logo.svg`, in place of loading it with `<img>`. chassis-assets made both
141
+ changes on `app/docs` on 2026-10-19: a brand has one `fonts.css`, the files of the `text`
142
+ role are `ui` files, and the logo of the `chassis` brand is for inline use. It has no `xmlns`,
143
+ and the classes `icon-default` and `icon-primary` of the page color its paths.
144
+
145
+ ### 1. Update the dependency and the assets
146
+
147
+ In the package of the site, then from the root of the repository:
148
+
149
+ ```sh
150
+ pnpm add -D @chassis-ui/docs@^0.10.0
151
+ pnpm sync-submodules
152
+ git add vendor/assets
153
+ ```
154
+
155
+ The docs build of `chassis-assets` has `fonts/fonts.css` from `db6d64b` of `app/docs` on. With
156
+ an earlier commit the pages show the fallback fonts. Stage or commit the pin before you run
157
+ `pnpm vendor` or a build, which check the submodule out at the commit that the repository
158
+ records. A site that does not copy the docs build provides `fonts/fonts.css` itself. Step 2
159
+ and step 4 of [From 0.8 to 0.10](#from-08-to-010) say more, and how to find the old files in
160
+ your own pages.
161
+
162
+ ### What changes in the package
163
+
164
+ - **The head links `fonts/fonts.css`** under the static path, and no longer the three
165
+ stylesheets of the roles.
166
+ - **The navigation bar and the footer inline the logo.** The build reads
167
+ `static/images/site-logo.svg` of the site's `public` directory and fails without it. The
168
+ logo is `2rem` high, its width follows its `viewBox`, and the link around it names it. A
169
+ logo of an earlier docs build, with its own colors, a width and a height, shows at the same
170
+ height.
171
+ - **The link of the logo in the footer has the bottom margin `mb-md`,** where it was `mb-xs`.
172
+ - **The search of the header is `5rem` wide** from the `lg` breakpoint, the icon and the
173
+ shortcut, and `12.5rem` from `xl`, where it was 200px, so that no link of the header runs
174
+ under it between 1024 and 1280 pixels.
175
+
176
+ ### Check the result
177
+
178
+ Build the site before and after the upgrade and compare the pages in a browser, in both color
179
+ modes: the logo takes the colors of the theme.
180
+
3
181
  ## From 0.8 to 0.9
4
182
 
5
183
  0.9.0 needs `@chassis-ui/css` 0.10.0 or later. css 0.10 names the font families by their
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.9.0",
4
+ "version": "0.11.0",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -3,7 +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
+ import { getStaticPath, getVersionedStaticPath } from '../../site'
7
7
 
8
8
  interface Props {
9
9
  /**
@@ -70,7 +70,11 @@ const {
70
70
 
71
71
  let markup = Array.isArray(code) ? code.join('\n') : code
72
72
  markup = replacePlaceholdersInHtml(markup)
73
- markup = replaceIconsInHtml(markup, getStaticPath())
73
+ markup = replaceIconsInHtml(
74
+ markup,
75
+ getStaticPath(),
76
+ getVersionedStaticPath('icons/chassis-icons.svg')
77
+ )
74
78
  markup = markup
75
79
  .split('\n')
76
80
  .map((line) => line.trimEnd())
@@ -93,7 +97,7 @@ const simplifiedMarkup = sourceMarkup
93
97
  (_match, classes) => `<img src="..."${classes ? ` class="${classes}"` : ''} alt="...">`
94
98
  )
95
99
  .replace(/(<svg class="icon\b[^"]*")\s+width="\d+"\s+height="\d+"/g, '$1')
96
- .replace(/href="\/static\/icons\/chassis-icons\.svg#/g, 'href="#')
100
+ .replace(/href="\/static\/icons\/chassis-icons\.svg(?:\?v=[0-9a-f]+)?#/g, 'href="#')
97
101
  ---
98
102
 
99
103
  <div class="cxd-example-snippet cxd-code-snippet not-prose" data-pagefind-ignore>
@@ -1,5 +1,5 @@
1
1
  ---
2
- import { getStaticPath } from '../../site'
2
+ import { getVersionedStaticPath } from '../../site'
3
3
 
4
4
  /**
5
5
  * Icon
@@ -31,7 +31,7 @@ const {
31
31
  size = 24,
32
32
  font = false,
33
33
  title,
34
- sprite = getStaticPath('icons/chassis-icons.svg'),
34
+ sprite = getVersionedStaticPath('icons/chassis-icons.svg'),
35
35
  ...rest
36
36
  } = Astro.props
37
37
  ---
@@ -3,7 +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
+ import { getStaticPath, getVersionedStaticPath } from '../../site'
7
7
 
8
8
  interface Props {
9
9
  /**
@@ -68,7 +68,11 @@ const {
68
68
 
69
69
  let markup = Array.isArray(code) ? code.join('\n') : code
70
70
  markup = replacePlaceholdersInHtml(markup)
71
- markup = replaceIconsInHtml(markup, getStaticPath())
71
+ markup = replaceIconsInHtml(
72
+ markup,
73
+ getStaticPath(),
74
+ getVersionedStaticPath('icons/chassis-icons.svg')
75
+ )
72
76
 
73
77
  // The resizable container needs `overflow: hidden` for the CSS `resize` handle
74
78
  // to work, which clips popovers like menus/dropdowns when they open. Render them
@@ -95,7 +99,7 @@ const simplifiedMarkup = sourceMarkup
95
99
  (_match, classes) => `<img src="..."${classes ? ` class="${classes}"` : ''} alt="...">`
96
100
  )
97
101
  .replace(/(<svg class="icon\b[^"]*")\s+width="\d+"\s+height="\d+"/g, '$1')
98
- .replace(/href="\/static\/icons\/chassis-icons\.svg#/g, 'href="#')
102
+ .replace(/href="\/static\/icons\/chassis-icons\.svg(?:\?v=[0-9a-f]+)?#/g, 'href="#')
99
103
  ---
100
104
 
101
105
  <div class="cxd-example-snippet cxd-code-snippet" data-pagefind-ignore>
@@ -4,13 +4,14 @@ import { fileURLToPath } from 'node:url'
4
4
  import type { AstroIntegration, AstroUserConfig, ViteUserConfig } from 'astro'
5
5
  import type { z } from 'astro/zod'
6
6
  import { loadConfig, loadSidebar } from './libs/config'
7
- import { joinDocsPath } from './libs/docs-path'
7
+ import { joinDocsPath, joinStaticPath } from './libs/docs-path'
8
8
  import { getDocsMarkdownConfig, type DocsMarkdownConfigOptions } from './libs/markdown'
9
9
  import { getInstalledPackageFsPath, getPackageRoot } from './libs/paths'
10
10
  import { remarkCxConfig, remarkCxDocsref } from './libs/remark'
11
11
  import { configSchema, type ChassisConfig, type Sidebar } from './libs/schema'
12
12
  import { chassisAutoImport, type ChassisAutoImportOptions } from './libs/shortcodes'
13
13
  import { getSiteUrl } from './libs/site'
14
+ import { versionStaticPath } from './libs/static-version'
14
15
 
15
16
  export interface ChassisDocsOptions<TSchema extends z.ZodType = typeof configSchema> {
16
17
  /**
@@ -56,7 +57,8 @@ type VitePlugin = Extract<NonNullable<ViteUserConfig['plugins']>[number], { name
56
57
  const virtualPrefix = 'virtual:chassis-docs/'
57
58
  const resolvedPrefix = '\0' + virtualPrefix
58
59
 
59
- function virtualModules(modules: Record<string, string>): VitePlugin {
60
+ // The code of each module, or a function that returns it when the module is loaded.
61
+ function virtualModules(modules: Record<string, string | (() => string)>): VitePlugin {
60
62
  return {
61
63
  name: 'chassis-docs:virtual',
62
64
  resolveId(id) {
@@ -66,7 +68,9 @@ function virtualModules(modules: Record<string, string>): VitePlugin {
66
68
  },
67
69
  load(id) {
68
70
  if (id.startsWith(resolvedPrefix)) {
69
- return modules[id.slice(resolvedPrefix.length)]
71
+ const code = modules[id.slice(resolvedPrefix.length)]
72
+
73
+ return typeof code === 'function' ? code() : code
70
74
  }
71
75
  }
72
76
  }
@@ -92,6 +96,7 @@ declare module 'virtual:chassis-docs/paths' {
92
96
 
93
97
  declare module 'virtual:chassis-docs/static' {
94
98
  export const staticPath: string
99
+ export const spritePath: string
95
100
  }
96
101
 
97
102
  declare module 'virtual:chassis-docs/styles' {}
@@ -238,8 +243,21 @@ export function chassisDocs<TSchema extends z.ZodType = typeof configSchema>(
238
243
  paths: Object.entries(paths)
239
244
  .map(([name, value]) => `export const ${name} = ${JSON.stringify(value)}`)
240
245
  .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')}`,
246
+ // A config that was not loaded with `loadConfig()` may lack the default. The
247
+ // version of the sprite is read when a script loads the module: a site's own
248
+ // integration may copy the file into `public` after this hook.
249
+ static: () => {
250
+ const staticPath = config.staticPath ?? '/static'
251
+ const spritePath = versionStaticPath(
252
+ joinStaticPath(staticPath, 'icons/chassis-icons.svg'),
253
+ paths.publicDir
254
+ )
255
+
256
+ return [
257
+ `export const staticPath = ${JSON.stringify(staticPath)}`,
258
+ `export const spritePath = ${JSON.stringify(spritePath)}`
259
+ ].join('\n')
260
+ },
243
261
  styles: styles.map((file) => `import ${JSON.stringify(file)}`).join('\n')
244
262
  })
245
263
  ]
@@ -1,5 +1,5 @@
1
1
  import { Tooltip } from '@chassis-ui/css'
2
- import { staticPath } from 'virtual:chassis-docs/static'
2
+ import { spritePath } from 'virtual:chassis-docs/static'
3
3
 
4
4
  // Helper to detect the user's preferred theme
5
5
  function getPreferredTheme() {
@@ -44,7 +44,7 @@ function toggleTheme(button) {
44
44
  const iconUse = button.querySelector('use')
45
45
  if (iconUse) {
46
46
  const iconHref = newTheme === 'dark' ? 'sun-solid' : 'moon-solid'
47
- iconUse.setAttribute('href', `${staticPath}/icons/chassis-icons.svg#${iconHref}`)
47
+ iconUse.setAttribute('href', `${spritePath}#${iconHref}`)
48
48
  }
49
49
  }
50
50
 
@@ -65,7 +65,7 @@ document.addEventListener('DOMContentLoaded', () => {
65
65
  const example = button.closest('.cxd-example-snippet')?.querySelector('.cxd-example')
66
66
  const currentTheme = example?.getAttribute('data-cx-theme') || preferredTheme
67
67
  const iconHref = currentTheme === 'dark' ? 'sun-solid' : 'moon-solid'
68
- iconUse.setAttribute('href', `${staticPath}/icons/chassis-icons.svg#${iconHref}`)
68
+ iconUse.setAttribute('href', `${spritePath}#${iconHref}`)
69
69
  }
70
70
 
71
71
  // Add event listener if not already set through onclick
package/src/js/search.js CHANGED
@@ -13,7 +13,7 @@
13
13
  // is needed here.
14
14
 
15
15
  import { getInstanceManager } from '@pagefind/component-ui'
16
- import { staticPath } from 'virtual:chassis-docs/static'
16
+ import { spritePath } from 'virtual:chassis-docs/static'
17
17
 
18
18
  const DIALOG_SELECTOR = '#cxdSearchDialog'
19
19
  // The rows that are links. The loading skeleton reuses the class on a `<div>`,
@@ -237,7 +237,7 @@ const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) =>
237
237
  const renderItem = ({ url, titleHtml, excerptHtml = '', icon, sub = false }) => `
238
238
  <a class="cxd-search-item${sub ? ' cxd-search-subitem' : ''}" href="${escapeHtml(url)}">
239
239
  <svg class="icon cxd-search-item-icon" width="16" height="16" aria-hidden="true">
240
- <use href="${staticPath}/icons/chassis-icons.svg#${icon}"></use>
240
+ <use href="${spritePath}#${icon}"></use>
241
241
  </svg>
242
242
  <div class="cxd-search-item-body">
243
243
  <div class="cxd-search-item-title">${titleHtml}</div>
@@ -0,0 +1,22 @@
1
+ ---
2
+ /**
3
+ * SiteLogo
4
+ *
5
+ * The logo of the site, `images/site-logo.svg` of the static files, inlined: the logo of the
6
+ * docs build of chassis-assets colors its paths with the classes `icon-default` and
7
+ * `icon-primary` of `@chassis-ui/css`, which reach a file that is in the page only. The link
8
+ * around it names it, so the drawing is hidden from assistive technology. The build fails
9
+ * without the file.
10
+ */
11
+
12
+ import fs from 'node:fs'
13
+ import { getPublicFsPath } from '../site'
14
+
15
+ const svg = fs
16
+ .readFileSync(getPublicFsPath('static/images/site-logo.svg'), 'utf8')
17
+ .replace(/^\s*<\?xml[^>]*\?>\s*/, '')
18
+ .replace(/^<svg\b/, '<svg aria-hidden="true" focusable="false"')
19
+ .trim()
20
+ ---
21
+
22
+ <Fragment set:html={svg} />
@@ -5,7 +5,7 @@
5
5
  * Site footer with logo, license information, and navigation links.
6
6
  */
7
7
 
8
- import { getStaticPath } from '../../site'
8
+ import SiteLogo from '../SiteLogo.astro'
9
9
  ---
10
10
 
11
11
  <footer class="cxd-footer py-6xl">
@@ -13,11 +13,11 @@ import { getStaticPath } from '../../site'
13
13
  <div class="grid row-gap-0">
14
14
  <div class="col-span-full md:col-span-10 lg:col-span-5 mb-md">
15
15
  <a
16
- class="d-inline-flex align-items-center mb-xs text-body-emphasis text-decoration-none"
16
+ class="cxd-site-logo d-inline-flex align-items-center mb-md text-body-emphasis text-decoration-none"
17
17
  href="/"
18
18
  aria-label="Chassis"
19
19
  >
20
- <img src={getStaticPath('images/site-logo.svg')} alt="Chassis UI" height="32" />
20
+ <SiteLogo />
21
21
  </a>
22
22
  <p class="mb-xs">
23
23
  This project is licensed under the MIT License, which allows for free use, modification,
@@ -11,6 +11,6 @@ import { getStaticPath } from '../../site'
11
11
  <link rel="apple-touch-icon" href={getStaticPath('images/apple-touch-icon.png')} sizes="180x180" />
12
12
  <link rel="icon" href={getStaticPath('images/favicon-32x32.png')} sizes="32x32" type="image/png" />
13
13
  <link rel="icon" href={getStaticPath('images/favicon-16x16.png')} sizes="16x16" type="image/png" />
14
- <link rel="manifest" href={getStaticPath('images/manifest.json')} />
14
+ <link rel="manifest" href={getStaticPath('other/manifest.json')} />
15
15
  <link rel="icon" href={getStaticPath('images/favicon.png')} />
16
16
  <meta name="theme-color" content="#00A4CC" />
@@ -5,7 +5,7 @@
5
5
  * HTML head section with meta tags, stylesheets, and SEO configuration.
6
6
  */
7
7
 
8
- import { getConfig, getStaticPath } from '../../site'
8
+ import { getConfig, getVersionedStaticPath } from '../../site'
9
9
  import { isHomePath } from '../../libs/docs-path'
10
10
  import type { Layout } from '../../libs/layout'
11
11
  import Stylesheet from './Stylesheet.astro'
@@ -42,13 +42,11 @@ const pageTitle = isHome
42
42
  <meta name="viewport" content="width=device-width, initial-scale=1" />
43
43
 
44
44
  <!--
45
- The fonts of the text, display and code roles, from the docs build of chassis-assets.
46
- Links, not `@import`s in the styles, so that the browser finds them with the page and
47
- not after the stylesheet that imports them
45
+ The fonts of the brand, from the docs build of chassis-assets: one stylesheet declares the
46
+ family of every role. A link, not an `@import` in the styles, so that the browser finds it
47
+ with the page and not after the stylesheet that imports it
48
48
  -->
49
- <link rel="stylesheet" href={getStaticPath('fonts/text.css')} />
50
- <link rel="stylesheet" href={getStaticPath('fonts/display.css')} />
51
- <link rel="stylesheet" href={getStaticPath('fonts/code.css')} />
49
+ <link rel="stylesheet" href={getVersionedStaticPath('fonts/fonts.css')} />
52
50
 
53
51
  <meta name="description" content={description} />
54
52
 
@@ -6,19 +6,11 @@
6
6
  */
7
7
 
8
8
  import type { HTMLAttributes } from 'astro/types'
9
- import { getStaticPath } from '../../site'
9
+ import { getVersionedStaticPath } from '../../site'
10
10
 
11
11
  function getChassisCssProps() {
12
- let cxCssLinkHref = getStaticPath('css/chassis')
13
-
14
- if (import.meta.env.PROD) {
15
- cxCssLinkHref = `${cxCssLinkHref}.min`
16
- }
17
-
18
- cxCssLinkHref = `${cxCssLinkHref}.css`
19
-
20
12
  const cxCssLinkProps: HTMLAttributes<'link'> = {
21
- href: cxCssLinkHref,
13
+ href: getVersionedStaticPath(import.meta.env.PROD ? 'css/chassis.min.css' : 'css/chassis.css'),
22
14
  rel: 'stylesheet'
23
15
  }
24
16
 
@@ -27,4 +19,4 @@ function getChassisCssProps() {
27
19
  ---
28
20
 
29
21
  <link {...getChassisCssProps()} />
30
- <link rel="stylesheet" href={getStaticPath('icons/chassis-icons.css')} />
22
+ <link rel="stylesheet" href={getVersionedStaticPath('icons/chassis-icons.css')} />
@@ -6,7 +6,8 @@
6
6
  */
7
7
 
8
8
  import type { Layout } from '../../libs/layout'
9
- import { getConfig, getStaticPath } from '../../site'
9
+ import { getConfig } from '../../site'
10
+ import SiteLogo from '../SiteLogo.astro'
10
11
  import NavLink from '../../components/NavLink.astro'
11
12
  import ThemeToggler from '../../components/ThemeToggler.astro'
12
13
  import SearchTrigger from './SearchTrigger.astro'
@@ -52,8 +53,8 @@ const isActive = (path: string) => {
52
53
  )}
53
54
  {layout !== 'docs' && <div class="lg:d-none" style="width: 4.25rem;" />}
54
55
 
55
- <a class="navbar-brand m-0 py-sm lg:me-md" href="/" aria-label="Chassis">
56
- <img src={getStaticPath('images/site-logo.svg')} alt="Chassis UI" width="123" height="32" />
56
+ <a class="navbar-brand cxd-site-logo m-0 py-sm lg:me-md" href="/" aria-label="Chassis">
57
+ <SiteLogo />
57
58
  </a>
58
59
 
59
60
  <div class="d-flex">
package/src/libs/icon.ts CHANGED
@@ -18,9 +18,16 @@ interface IconOptions {
18
18
  * This is needed because the `<Example />` shortcode renders its `code` prop as raw HTML via `set:html`
19
19
  * rather than as MDX, so nested Astro components (like `<Icon />`, similar to `<Placeholder />`) are never
20
20
  * actually invoked — they need to be expanded to their rendered markup ahead of time.
21
+ *
22
+ * @param staticPath The `staticPath` of the site.
23
+ * @param sprite URL of the sprite that an icon without a `sprite` attribute uses.
21
24
  * @see src/components/shortcodes/Icon.astro
22
25
  */
23
- export function replaceIconsInHtml(html: string, staticPath = '/static') {
26
+ export function replaceIconsInHtml(
27
+ html: string,
28
+ staticPath = '/static',
29
+ sprite = `${staticPath}/icons/chassis-icons.svg`
30
+ ) {
24
31
  return html.replace(iconRegex, (match) => {
25
32
  const document = htmlparser2.parseDocument(match, { xmlMode: true })
26
33
  const iconElement = document.firstChild
@@ -34,11 +41,11 @@ export function replaceIconsInHtml(html: string, staticPath = '/static') {
34
41
  throw new Error('Invalid icon element.')
35
42
  }
36
43
 
37
- return renderIconToString(getOptionsWithDefaults(iconElement.attribs, staticPath))
44
+ return renderIconToString(getOptionsWithDefaults(iconElement.attribs, sprite))
38
45
  })
39
46
  }
40
47
 
41
- function getOptionsWithDefaults(attribs: Record<string, string>, staticPath: string): IconOptions {
48
+ function getOptionsWithDefaults(attribs: Record<string, string>, sprite: string): IconOptions {
42
49
  if (!attribs.name) {
43
50
  throw new Error('Icon element is missing the required "name" attribute.')
44
51
  }
@@ -48,7 +55,7 @@ function getOptionsWithDefaults(attribs: Record<string, string>, staticPath: str
48
55
  class: attribs.class,
49
56
  size: attribs.size ? Number(attribs.size.replace(/[{}]/g, '')) : 24,
50
57
  font: attribs.font !== undefined && attribs.font !== '{false}',
51
- sprite: attribs.sprite || `${staticPath}/icons/chassis-icons.svg`,
58
+ sprite: attribs.sprite || sprite,
52
59
  title: attribs.title
53
60
  }
54
61
  }
@@ -0,0 +1,53 @@
1
+ import { createHash } from 'node:crypto'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ // The hash of each file, kept until its size or time of change differs: a page of the build
6
+ // reads the same stylesheet as every other page, and the dev server sees a file copied anew.
7
+ const versions = new Map<string, { mtimeMs: number; size: number; version: string }>()
8
+
9
+ /**
10
+ * The first 8 characters of the MD5 hash of a file's content, or `undefined` when there is
11
+ * no file at `fsPath`.
12
+ */
13
+ export function getFileVersion(fsPath: string): string | undefined {
14
+ let stats: fs.Stats
15
+
16
+ try {
17
+ stats = fs.statSync(fsPath)
18
+ } catch {
19
+ return undefined
20
+ }
21
+
22
+ if (!stats.isFile()) return undefined
23
+
24
+ const cached = versions.get(fsPath)
25
+
26
+ if (cached && cached.mtimeMs === stats.mtimeMs && cached.size === stats.size) {
27
+ return cached.version
28
+ }
29
+
30
+ const version = createHash('md5').update(fs.readFileSync(fsPath)).digest('hex').slice(0, 8)
31
+ versions.set(fsPath, { mtimeMs: stats.mtimeMs, size: stats.size, version })
32
+
33
+ return version
34
+ }
35
+
36
+ /**
37
+ * Adds `?v=` and the version of the file to the URL path of a file of the public directory:
38
+ * `/static/css/chassis.min.css?v=8e2106f6`. A new content is a new URL, so the file can be
39
+ * kept by the browser until the page names another. A path that the public directory has no
40
+ * file at is returned as it is.
41
+ *
42
+ * @param urlPath URL path of the file, e.g. `"/static/css/chassis.min.css"`.
43
+ * @param publicDir Absolute path of the site's `public` directory.
44
+ */
45
+ export function versionStaticPath(urlPath: string, publicDir: string): string {
46
+ const fsPath = path.join(publicDir, urlPath)
47
+
48
+ if (path.relative(publicDir, fsPath).startsWith('..')) return urlPath
49
+
50
+ const version = getFileVersion(fsPath)
51
+
52
+ return version ? `${urlPath}?v=${version}` : urlPath
53
+ }
@@ -2,6 +2,14 @@
2
2
  @use "@chassis-ui/css/scss/mixins" as *;
3
3
 
4
4
  @layer custom {
5
+ // The navbar and the footer inline the logo. The height sets its size, also for a file that
6
+ // has a width and a height of its own.
7
+ .cxd-site-logo > svg {
8
+ display: block;
9
+ width: auto;
10
+ height: 2rem;
11
+ }
12
+
5
13
  .cxd-gutter {
6
14
  --cx-container-padding: #{$cxd-gutter-x * .5};
7
15
  }
@@ -12,15 +12,17 @@
12
12
  @include media-breakpoint-up(lg) {
13
13
  position: absolute;
14
14
  top: 20%;
15
- right: 11rem;
16
- width: 200px;
15
+ right: 11.5rem;
16
+ width: 5rem;
17
17
 
18
18
  [dir="rtl"] & {
19
19
  right: auto;
20
- left: 11rem;
20
+ left: 11.5rem;
21
21
  }
22
22
  }
23
-
23
+ @include media-breakpoint-up(xl) {
24
+ width: 12.5rem;
25
+ }
24
26
  }
25
27
 
26
28
 
package/src/site.ts CHANGED
@@ -14,6 +14,7 @@ import { getNodeModulesFsPaths, resolvePackageFilePath } from './libs/paths'
14
14
  import { replaceConfigInText, replaceDocsrefInText } from './libs/remark'
15
15
  import type { ChassisConfig, DocsPage, Sidebar } from './libs/schema'
16
16
  import { resolveSiteFileUrl, resolveSourceFsPath, resolveSourceUrl } from './libs/source'
17
+ import { versionStaticPath } from './libs/static-version'
17
18
 
18
19
  /**
19
20
  * The parsed `config.yml`.
@@ -43,6 +44,18 @@ export function getStaticPath(file = ''): string {
43
44
  return joinStaticPath(config.staticPath ?? '/static', file)
44
45
  }
45
46
 
47
+ /**
48
+ * URL path of a static file with the version of its content:
49
+ * `getVersionedStaticPath('css/chassis.min.css')` is
50
+ * `/static/css/chassis.min.css?v=8e2106f6`. The version is read from the file of the site's
51
+ * `public` directory, so a page links the file of its own build, and a browser that keeps
52
+ * the file fetches it again when it changes. Without that file, the path is the one of
53
+ * `getStaticPath()`.
54
+ */
55
+ export function getVersionedStaticPath(file: string): string {
56
+ return versionStaticPath(getStaticPath(file), paths.publicDir)
57
+ }
58
+
46
59
  /** Absolute path of the site's root: the directory that holds `astro.config.ts`. */
47
60
  export function getSiteRoot(): string {
48
61
  return paths.root
package/virtual.d.ts CHANGED
@@ -25,6 +25,8 @@ declare module 'virtual:chassis-docs/paths' {
25
25
  declare module 'virtual:chassis-docs/static' {
26
26
  /** `staticPath` of the config, for scripts that run in the browser. */
27
27
  export const staticPath: string
28
+ /** URL of the icon sprite, with the version of its content. */
29
+ export const spritePath: string
28
30
  }
29
31
 
30
32
  declare module 'virtual:chassis-docs/styles' {}