@uxfront/layer-docs 0.4.0 → 0.5.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/README.md +31 -220
- package/app/app.config.ts +8 -92
- package/app/components/content/FrameworkSwitcher.vue +66 -40
- package/app/components/docs/DocsAsideLeftTop.vue +9 -15
- package/app/components/docs/DocsFrameworkSelect.vue +4 -7
- package/app/composables/useFramework.ts +30 -38
- package/nuxt.config.ts +14 -170
- package/package.json +10 -77
- package/CHANGELOG.md +0 -180
- package/LICENSE +0 -21
- package/app/app.vue +0 -138
- package/app/assets/css/main.css +0 -15
- package/app/components/IconMenuToggle.vue +0 -92
- package/app/components/LanguageSelect.vue +0 -73
- package/app/components/MorphingGradientBackground.vue +0 -261
- package/app/components/OgImage/OgImageDocs.satori.vue +0 -40
- package/app/components/OgImage/OgImageLanding.satori.vue +0 -41
- package/app/components/app/AppFooter.vue +0 -13
- package/app/components/app/AppFooterCenter.vue +0 -17
- package/app/components/app/AppFooterLeft.vue +0 -21
- package/app/components/app/AppFooterRight.vue +0 -33
- package/app/components/app/AppHeader.vue +0 -123
- package/app/components/app/AppHeaderAttribution.vue +0 -45
- package/app/components/app/AppHeaderBody.vue +0 -14
- package/app/components/app/AppHeaderCTA.vue +0 -31
- package/app/components/app/AppHeaderCenter.vue +0 -10
- package/app/components/app/AppHeaderLogo.vue +0 -16
- package/app/components/app/AppOgDecoration.vue +0 -27
- package/app/components/app/AppOgLogo.vue +0 -19
- package/app/components/app/AppSearch.vue +0 -59
- package/app/components/app/AppSubHeader.vue +0 -21
- package/app/components/content/BrowserFrame.vue +0 -28
- package/app/components/content/GradientPageHero.vue +0 -35
- package/app/components/content/StorybookEmbed.vue +0 -160
- package/app/components/content/Video.vue +0 -103
- package/app/components/docs/DocsAsideLeftBody.vue +0 -20
- package/app/components/docs/DocsAsideRightBottom.vue +0 -15
- package/app/components/docs/DocsPageHeaderLinks.vue +0 -75
- package/app/composables/useDocsSections.ts +0 -57
- package/app/composables/useDocusI18n.ts +0 -49
- package/app/constants/sections.ts +0 -25
- package/app/error.vue +0 -140
- package/app/layouts/default.vue +0 -24
- package/app/pages/[[lang]]/[...slug].vue +0 -58
- package/app/pages/[[lang]]/docs/[section]/[...slug].vue +0 -180
- package/app/plugins/i18n.ts +0 -21
- package/app/plugins/posthog.client.ts +0 -56
- package/app/types/non-route-categories.ts +0 -12
- package/app/utils/flattenNavigation.ts +0 -22
- package/app/utils/foldNonRouteCategories.ts +0 -47
- package/app/utils/prerender.ts +0 -9
- package/app/utils/storybookEmbed.test.ts +0 -98
- package/app/utils/storybookEmbed.ts +0 -93
- package/i18n/locales/ar.json +0 -24
- package/i18n/locales/be.json +0 -24
- package/i18n/locales/bn.json +0 -24
- package/i18n/locales/ca.json +0 -24
- package/i18n/locales/ckb.json +0 -24
- package/i18n/locales/cs.json +0 -24
- package/i18n/locales/da.json +0 -24
- package/i18n/locales/de.json +0 -24
- package/i18n/locales/el.json +0 -24
- package/i18n/locales/en.json +0 -24
- package/i18n/locales/et.json +0 -24
- package/i18n/locales/fr.json +0 -24
- package/i18n/locales/he.json +0 -24
- package/i18n/locales/hi.json +0 -24
- package/i18n/locales/hy.json +0 -24
- package/i18n/locales/it.json +0 -24
- package/i18n/locales/ja.json +0 -24
- package/i18n/locales/kk.json +0 -24
- package/i18n/locales/km.json +0 -24
- package/i18n/locales/ko.json +0 -24
- package/i18n/locales/ky.json +0 -24
- package/i18n/locales/lb.json +0 -24
- package/i18n/locales/ms.json +0 -24
- package/i18n/locales/nb.json +0 -24
- package/i18n/locales/pl.json +0 -24
- package/i18n/locales/ru.json +0 -24
- package/i18n/locales/sl.json +0 -24
- package/i18n/locales/sv.json +0 -24
- package/i18n/locales/uk.json +0 -24
- package/i18n/locales/ur.json +0 -24
- package/i18n/locales/vi.json +0 -24
- package/modules/config.ts +0 -144
- package/modules/optimizeDeps.ts +0 -45
- package/modules/routing.ts +0 -20
- package/nuxt.schema.ts +0 -374
- package/server/plugins/llms-redirect.ts +0 -60
- package/server/routes/raw/[...slug].md.get.ts +0 -74
- package/storybook/index.test.ts +0 -110
- package/storybook/index.ts +0 -362
- package/test/brand-palette.ts +0 -235
- package/test/no-brand-leakage.test.ts +0 -124
- package/tsconfig.json +0 -17
- package/utils/accent.ts +0 -80
- package/utils/content.ts +0 -193
- package/utils/git.ts +0 -114
- package/utils/meta.ts +0 -28
package/README.md
CHANGED
|
@@ -1,252 +1,63 @@
|
|
|
1
1
|
# @uxfront/layer-docs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
own branding and content, and get a Docus-shaped docs site — header, footer,
|
|
5
|
-
sidebar, SEO, i18n routing, OG images and analytics — without copying boilerplate
|
|
6
|
-
between repos.
|
|
7
|
-
|
|
8
|
-
The layer ships **no product branding**. Consuming apps provide title, logos,
|
|
9
|
-
socials, GitHub, footer, table-of-contents links and colour palette via their own
|
|
10
|
-
`app.config.ts`, merged over the layer's neutral defaults by Nuxt's `defu` layer
|
|
11
|
-
merge. Site metadata, SEO and git info are auto-inferred from the consumer's
|
|
12
|
-
`package.json` + git when not supplied.
|
|
3
|
+
The Nuxt layer for UXFront's documentation sites. It extends [Docus](https://docus.dev), which renders the markdown in `content/docs/` with a header, sidebar, search and table of contents, and adds a framework switcher: every reader sees the examples for their framework, on every page.
|
|
13
4
|
|
|
14
5
|
## Install
|
|
15
6
|
|
|
16
|
-
```
|
|
17
|
-
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @uxfront/layer-docs
|
|
18
9
|
```
|
|
19
10
|
|
|
20
|
-
Then install the peer dependencies the layer expects (the Nuxt documentation
|
|
21
|
-
stack — see [`package.json`](./package.json) `peerDependencies` for pinned
|
|
22
|
-
ranges): `nuxt`, `vue`, `@nuxt/ui`, `@nuxt/image`, `@nuxt/scripts`,
|
|
23
|
-
`@nuxtjs/robots`, `nuxt-og-image`, `satori`, `@resvg/resvg-js`, `nuxt-llms`,
|
|
24
|
-
`tailwindcss`, and — for content and translation — `@nuxt/content`,
|
|
25
|
-
`@nuxtjs/i18n`, `@nuxtjs/mdc`.
|
|
26
|
-
|
|
27
|
-
## Usage
|
|
28
|
-
|
|
29
|
-
Extend the layer from your app's `nuxt.config.ts`:
|
|
30
|
-
|
|
31
11
|
```ts
|
|
12
|
+
// nuxt.config.ts
|
|
32
13
|
export default defineNuxtConfig({
|
|
33
14
|
extends: ["@uxfront/layer-docs"],
|
|
34
15
|
});
|
|
35
16
|
```
|
|
36
17
|
|
|
37
|
-
|
|
18
|
+
Then list the frameworks the docs' examples come in, in display order. The first is the default:
|
|
38
19
|
|
|
39
20
|
```ts
|
|
21
|
+
// app/app.config.ts
|
|
40
22
|
export default defineAppConfig({
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`header.attribution` renders an optional credit beside the wordmark, where only
|
|
49
|
-
the `label` is a link:
|
|
50
|
-
|
|
51
|
-
```ts
|
|
52
|
-
export default defineAppConfig({
|
|
53
|
-
header: {
|
|
54
|
-
title: "uxd",
|
|
55
|
-
attribution: { prefix: "by", label: "UXFront", to: "https://uxfront.com" },
|
|
23
|
+
docsTheme: {
|
|
24
|
+
frameworks: [
|
|
25
|
+
{ value: "react", label: "React", icon: "i-simple-icons-react" },
|
|
26
|
+
{ value: "vue", label: "Vue", icon: "i-simple-icons-vuedotjs" },
|
|
27
|
+
{ value: "svelte", label: "Svelte", icon: "i-simple-icons-svelte" },
|
|
28
|
+
],
|
|
56
29
|
},
|
|
57
30
|
});
|
|
58
31
|
```
|
|
59
32
|
|
|
60
|
-
|
|
61
|
-
text, and `UXFront` links out. Omit `attribution` (or leave `label` empty) to
|
|
62
|
-
render the wordmark alone.
|
|
33
|
+
`value` is the slot name pages write each framework's examples in, and `icon` any [Iconify](https://icones.js.org) icon. The layer ships no default list, so with none, the switcher and the select render nothing.
|
|
63
34
|
|
|
64
|
-
##
|
|
35
|
+
## Writing examples
|
|
65
36
|
|
|
66
|
-
|
|
67
|
-
Tailwind base at `@uxfront/layer-docs/app/assets/css/main.css` and deliberately
|
|
68
|
-
does not register it itself. A brand `@theme` only compiles into real `:root`
|
|
69
|
-
custom properties when it lives inside a Tailwind pass, so your CSS file has to
|
|
70
|
-
_import_ the layer base rather than sit beside it as a second entry — two
|
|
71
|
-
entries each re-emit every base utility into the shipped stylesheet.
|
|
37
|
+
Put one slot per framework in a `::framework-switcher`:
|
|
72
38
|
|
|
73
|
-
|
|
39
|
+
````md
|
|
40
|
+
::framework-switcher
|
|
41
|
+
#react
|
|
74
42
|
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
export default defineNuxtConfig({
|
|
78
|
-
extends: ["@uxfront/layer-docs"],
|
|
79
|
-
css: ["./app/assets/css/main.css"],
|
|
80
|
-
});
|
|
43
|
+
```tsx [Button.tsx]
|
|
44
|
+
export const Button = () => <button>Save</button>;
|
|
81
45
|
```
|
|
82
46
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```css
|
|
86
|
-
/* app/assets/css/main.css */
|
|
87
|
-
@import "@uxfront/layer-docs/app/assets/css/main.css";
|
|
88
|
-
|
|
89
|
-
/* The layer's own `@source` paths are package-relative (they resolve inside
|
|
90
|
-
node_modules), so re-scan your content and config for utility classes. */
|
|
91
|
-
@source "../../../content/**/*";
|
|
92
|
-
@source "../../app.config.ts";
|
|
47
|
+
#vue
|
|
93
48
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
/* … the rest of the scale … */
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
:root {
|
|
100
|
-
--ui-primary: var(--color-teal);
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
The scale name must match `ui.colors.primary` in your `app.config.ts` for Nuxt
|
|
105
|
-
UI to resolve component variants onto it.
|
|
106
|
-
|
|
107
|
-
### Guardrail tests
|
|
108
|
-
|
|
109
|
-
Both halves of that contract fail silently, so the layer ships the guards.
|
|
110
|
-
`vitest` is an optional peer dependency, needed only for these.
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
// test/brand-palette-css.test.ts — source-level, no build required
|
|
114
|
-
import { describeBrandPaletteCss } from "@uxfront/layer-docs/test";
|
|
115
|
-
|
|
116
|
-
describeBrandPaletteCss({
|
|
117
|
-
entry: new URL("../app/assets/css/main.css", import.meta.url),
|
|
118
|
-
scale: "teal",
|
|
119
|
-
});
|
|
49
|
+
```vue [Button.vue]
|
|
50
|
+
<template><button>Save</button></template>
|
|
120
51
|
```
|
|
121
52
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
import { describeSingleTailwindPass } from "@uxfront/layer-docs/test";
|
|
125
|
-
|
|
126
|
-
describeSingleTailwindPass({
|
|
127
|
-
output: new URL("../.output/public/_nuxt", import.meta.url),
|
|
128
|
-
scale: "teal",
|
|
129
|
-
});
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
The first asserts your CSS entry is a Tailwind entry and defines the palette.
|
|
133
|
-
The second reads the built stylesheet and asserts every base utility is emitted
|
|
134
|
-
**exactly once** — catching both a duplicate Tailwind pass (double payload) and
|
|
135
|
-
a CSS entry that never reached the bundle (no base utilities at all). Run it in
|
|
136
|
-
whichever CI job already builds the app; it needs `.output/` on disk.
|
|
137
|
-
|
|
138
|
-
## Storybook embeds
|
|
139
|
-
|
|
140
|
-
`StorybookEmbed` renders a deployed Storybook story inside a docs page. Point it
|
|
141
|
-
at your Storybook once, from env — the host is a deployment fact, not a code
|
|
142
|
-
fact, so it lives in runtime config rather than `app.config`:
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
# a single Storybook
|
|
146
|
-
NUXT_PUBLIC_STORYBOOK_BASE_URL="https://storybook.example.com"
|
|
147
|
-
|
|
148
|
-
# or one per framework — `{framework}` is substituted per embed
|
|
149
|
-
NUXT_PUBLIC_STORYBOOK_BASE_URL="https://{framework}.storybook.example.com"
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Then use it from markdown:
|
|
153
|
-
|
|
154
|
-
```mdc
|
|
155
|
-
:storybook-embed{story="components-actions-button--default"}
|
|
156
|
-
|
|
157
|
-
:storybook-embed{story="components-actions-button--default" mode="panel"}
|
|
158
|
-
|
|
159
|
-
:storybook-embed{story="components-actions-button--default" title="Button"}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
| Prop | Default | Notes |
|
|
163
|
-
| ----------- | ---------------- | --------------------------------------------------------------------------------------------------- |
|
|
164
|
-
| `story` | — | Storybook story id, e.g. `components-actions-button--default`. |
|
|
165
|
-
| `framework` | `useFramework()` | Fills `{framework}` in the base URL. Follows the tab by default. |
|
|
166
|
-
| `mode` | `"preview"` | `preview` (canvas only) · `full` (manager, no sidebar) · `panel` (manager with the controls panel). |
|
|
167
|
-
| `height` | mode default | Number (px) or any CSS length. Set it and auto-height is off. |
|
|
168
|
-
| `title` | — | Wraps the embed in `BrowserFrame` with this title. |
|
|
169
|
-
|
|
170
|
-
The embed mounts its iframe only once it is 200px from the viewport, keeps the
|
|
171
|
-
Storybook's colour mode in sync with the page's, and grows to fit its story.
|
|
172
|
-
Height is always reserved, so none of that shifts layout.
|
|
173
|
-
|
|
174
|
-
### The Storybook side
|
|
175
|
-
|
|
176
|
-
Auto-height and theme sync are a two-way `postMessage` contract, so the
|
|
177
|
-
Storybook has to answer. Install the bridge — it is framework-free and imports
|
|
178
|
-
nothing from Nuxt:
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
// .storybook/preview.ts
|
|
182
|
-
import { installDocsEmbedPreviewBridge } from "@uxfront/layer-docs/storybook";
|
|
183
|
-
|
|
184
|
-
installDocsEmbedPreviewBridge({ onTheme: (theme) => applyTheme(theme) });
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
// .storybook/manager.ts — only needed for `full` / `panel`
|
|
189
|
-
import { installDocsEmbedManagerBridge } from "@uxfront/layer-docs/storybook";
|
|
190
|
-
|
|
191
|
-
installDocsEmbedManagerBridge({ onTheme: (theme) => applyTheme(theme) });
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Without the bridge the embed still renders; it just holds its default height and
|
|
195
|
-
does not follow the page's colour mode.
|
|
196
|
-
|
|
197
|
-
### Migrating an existing Storybook
|
|
198
|
-
|
|
199
|
-
If your Storybook already speaks a branded namespace (`<brand>:theme`,
|
|
200
|
-
`<brand>:height`), name it and both sides accept **and** emit both spellings, so
|
|
201
|
-
the docs site and the Storybook can deploy in either order:
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
NUXT_PUBLIC_STORYBOOK_LEGACY_MESSAGE_NAMESPACE="acme"
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
Unset it once both sides are on `@uxfront/layer-docs/storybook`.
|
|
208
|
-
|
|
209
|
-
## OG images
|
|
210
|
-
|
|
211
|
-
Social share cards are on by default — the layer registers `nuxt-og-image` and
|
|
212
|
-
calls `defineOgImage` from the docs and landing pages, so a consumer that sets up
|
|
213
|
-
the CSS entry above gets branded 1200×630 cards without writing anything. Opt out
|
|
214
|
-
with the module's own switch: `ogImage: { enabled: false }`.
|
|
215
|
-
|
|
216
|
-
The accent is resolved to a **literal colour at build time** by following
|
|
217
|
-
`--ui-primary` to the `--color-<scale>` value it aliases in your CSS entry.
|
|
218
|
-
Satori — the renderer behind `.satori.vue` templates — has no CSS cascade and no
|
|
219
|
-
custom properties, so `var(--ui-primary)` would render as nothing, and a utility
|
|
220
|
-
class like `text-teal-500` would render Tailwind's teal rather than yours (your
|
|
221
|
-
`@theme` shadows the stock scale name). Override it with a literal if you need
|
|
222
|
-
something other than the brand colour:
|
|
223
|
-
|
|
224
|
-
```ts
|
|
225
|
-
export default defineAppConfig({
|
|
226
|
-
ogImage: { accent: "#a78bfa" },
|
|
227
|
-
});
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
Two pieces of the card are components rather than props, because `defineOgImage`
|
|
231
|
-
serialises its arguments and a Vue slot cannot cross that boundary. Shadow either
|
|
232
|
-
by creating a file at the same path in your own `app/components/`:
|
|
233
|
-
|
|
234
|
-
| Component | Default |
|
|
235
|
-
| ------------------------- | ----------------------------------- |
|
|
236
|
-
| `app/AppOgDecoration.vue` | A neutral radial-gradient flourish |
|
|
237
|
-
| `app/AppOgLogo.vue` | A text wordmark from `header.title` |
|
|
238
|
-
|
|
239
|
-
The wordmark is text, not `AppHeaderLogo`: that component renders
|
|
240
|
-
`UColorModeImage`, and satori has neither a colour mode nor a browser to resolve
|
|
241
|
-
relative image paths against. A brand that wants its mark on the card overrides
|
|
242
|
-
`AppOgLogo.vue` with an `<img>` pointing at an **absolute** URL.
|
|
243
|
-
|
|
244
|
-
## Compatibility
|
|
53
|
+
::
|
|
54
|
+
````
|
|
245
55
|
|
|
246
|
-
|
|
247
|
-
i18n 10.4, og-image 6, llms 0.2). TypeScript is pinned to the range the Nuxt
|
|
248
|
-
stack supports (`^6.0.3`). See `peerDependencies` for the authoritative ranges.
|
|
56
|
+
## What it adds
|
|
249
57
|
|
|
250
|
-
|
|
58
|
+
- **`FrameworkSwitcher`.** One tab per framework, on Nuxt UI's tabs, so it follows the WAI-ARIA tabs pattern (arrow keys between tabs, panels labelled by their tab). A page doesn't have to cover every framework: a missing one shows the first one the page has, with a note saying so. When the tabs outgrow the width, the list scrolls.
|
|
59
|
+
- **A Framework select above the sidebar.** It replaces Docus's `DocsAsideLeftTop` and renders Docus's below it.
|
|
60
|
+
- **`useFramework()`.** The reader's pick, shared by every switcher and the select, and kept in `localStorage` across visits. It's read once the page is mounted, so the prerendered HTML shows the default framework and hydrates cleanly.
|
|
61
|
+
- **Bundled icons.** Nuxt Icon bundles the icons named in app config too, so the select's icons don't come from the Iconify API.
|
|
251
62
|
|
|
252
|
-
[
|
|
63
|
+
Everything else is Docus's: configure it as its [docs](https://docus.dev) describe.
|
package/app/app.config.ts
CHANGED
|
@@ -1,96 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* Neutral shell defaults. This layer ships NO product branding — consuming
|
|
4
|
-
* apps supply title, logos, socials, GitHub, footer, TOC links and palette
|
|
5
|
-
* via their own `app.config.ts` (merged over these defaults by Nuxt's `defu`
|
|
6
|
-
* layer merge). `modules/config.ts` also fills `seo`/`header`/`github` from
|
|
7
|
-
* the consumer's `package.json` + git as fallbacks.
|
|
8
|
-
*
|
|
9
|
-
* @docs https://www.docus.dev/concepts/configuration#global-configuration
|
|
10
|
-
*/
|
|
11
|
-
toc: {
|
|
12
|
-
// Title of the main table of contents
|
|
13
|
-
title: "On this page",
|
|
14
|
-
},
|
|
1
|
+
import type { FrameworkOption } from "./composables/useFramework";
|
|
15
2
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* renders "uxd by UXFront" with only "UXFront" linked. An empty `label`
|
|
21
|
-
* renders nothing, which is the neutral default.
|
|
22
|
-
*/
|
|
23
|
-
attribution: {
|
|
24
|
-
prefix: "",
|
|
25
|
-
label: "",
|
|
26
|
-
to: "",
|
|
27
|
-
},
|
|
28
|
-
},
|
|
3
|
+
// Empty, so an app that lists none gets no framework switcher or select. Nuxt
|
|
4
|
+
// concatenates app config arrays across layers, so a list here would be added
|
|
5
|
+
// to the app's instead of replaced by it.
|
|
6
|
+
const frameworks: FrameworkOption[] = [];
|
|
29
7
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*/
|
|
34
|
-
analytics: {
|
|
35
|
-
// PostHog analytics plugin (production only, requires a runtime key).
|
|
36
|
-
enabled: true,
|
|
37
|
-
},
|
|
38
|
-
i18nRedirect: {
|
|
39
|
-
// Redirect `/` to `/{locale}` (only fires when i18n is configured).
|
|
40
|
-
enabled: true,
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
ui: {
|
|
44
|
-
// Palette-free type discriminant — NOT branding. Nuxt UI's wide
|
|
45
|
-
// `AppConfigUI` type (which permits the component slot overrides below)
|
|
46
|
-
// only applies to `ui` when a `colors` key is present; without it the
|
|
47
|
-
// narrow `nuxt.schema.ts` Studio type wins and the slots fail
|
|
48
|
-
// excess-property checks. The empty object bakes in no palette; each
|
|
49
|
-
// consumer supplies the real `colors` (merged over this by `defu`).
|
|
50
|
-
colors: {},
|
|
51
|
-
// Neutral Nuxt UI Pro component polish — reusable shell defaults every
|
|
52
|
-
// consumer inherits, not product branding.
|
|
53
|
-
commandPalette: {
|
|
54
|
-
slots: {
|
|
55
|
-
input: "[&_.iconify]:size-4 [&_.iconify]:mx-0.5",
|
|
56
|
-
itemLeadingIcon: "size-4 mx-0.5",
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
contentNavigation: {
|
|
60
|
-
slots: {
|
|
61
|
-
trigger: "font-normal text-muted data-[state=open]:text-muted cursor-pointer",
|
|
62
|
-
linkLeadingIcon: "size-4 mr-1",
|
|
63
|
-
linkTrailing: "hidden",
|
|
64
|
-
},
|
|
65
|
-
compoundVariants: [
|
|
66
|
-
{
|
|
67
|
-
variant: "link",
|
|
68
|
-
active: false,
|
|
69
|
-
disabled: false,
|
|
70
|
-
class: {
|
|
71
|
-
linkLeadingIcon: "group-data-[state=open]:text-dimmed",
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
defaultVariants: {
|
|
76
|
-
variant: "link",
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
pageLinks: {
|
|
80
|
-
slots: {
|
|
81
|
-
linkLeadingIcon: "size-4",
|
|
82
|
-
linkLabelExternalIcon: "size-2.5",
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
pageCard: {
|
|
86
|
-
slots: {
|
|
87
|
-
root: "rounded-xl",
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
pricingTable: {
|
|
91
|
-
slots: {
|
|
92
|
-
tierTitle: "text-highlighted text-2xl sm:text-3xl text-pretty font-semibold",
|
|
93
|
-
},
|
|
94
|
-
},
|
|
8
|
+
export default defineAppConfig({
|
|
9
|
+
docsTheme: {
|
|
10
|
+
frameworks,
|
|
95
11
|
},
|
|
96
12
|
});
|
|
@@ -1,48 +1,74 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
const slots = useSlots();
|
|
4
|
-
|
|
5
|
-
const current = computed(
|
|
6
|
-
() => frameworks.value.find((f) => f.value === framework.value) ?? frameworks.value[0],
|
|
7
|
-
);
|
|
2
|
+
import type { FrameworkOption } from "../../composables/useFramework";
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Shows the reader's framework out of a page's per-framework examples:
|
|
6
|
+
*
|
|
7
|
+
* ```md
|
|
8
|
+
* ::framework-switcher
|
|
9
|
+
* #react
|
|
10
|
+
* …
|
|
11
|
+
* #vue
|
|
12
|
+
* …
|
|
13
|
+
* ::
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* One tab per framework in `docsTheme.frameworks`. Picking one here picks it
|
|
17
|
+
* everywhere, the sidebar's select included. Nuxt UI's tabs bring the WAI-ARIA
|
|
18
|
+
* tabs pattern: arrow keys move between tabs, and each panel is labelled by its tab.
|
|
19
|
+
*/
|
|
20
|
+
const { framework, frameworks, current } = useFramework();
|
|
21
|
+
const slots = useSlots();
|
|
22
|
+
const tabs = useTemplateRef("tabs");
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
// A page can cover only some frameworks. The others show the first one it covers.
|
|
25
|
+
function shown(option: FrameworkOption) {
|
|
26
|
+
return slots[option.value] ? option : frameworks.value.find((f) => slots[f.value]);
|
|
19
27
|
}
|
|
28
|
+
|
|
29
|
+
// When the tabs outgrow the width, the list scrolls. A pick made elsewhere (in
|
|
30
|
+
// another switcher, in the sidebar, or restored once mounted) scrolls the list
|
|
31
|
+
// to its tab. scrollIntoView() would scroll the page to the switcher too.
|
|
32
|
+
watch(
|
|
33
|
+
current,
|
|
34
|
+
(option) => {
|
|
35
|
+
const index = option ? frameworks.value.indexOf(option) : -1;
|
|
36
|
+
const tab: HTMLElement | undefined = tabs.value?.triggersRef[index]?.$el;
|
|
37
|
+
const list = tab?.parentElement;
|
|
38
|
+
if (!tab || !list) return;
|
|
39
|
+
const end = tab.offsetLeft + tab.offsetWidth - list.clientWidth;
|
|
40
|
+
list.scrollLeft = Math.min(tab.offsetLeft, Math.max(list.scrollLeft, end));
|
|
41
|
+
},
|
|
42
|
+
{ flush: "post" },
|
|
43
|
+
);
|
|
20
44
|
</script>
|
|
21
45
|
|
|
22
46
|
<template>
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
<UTabs
|
|
48
|
+
v-if="current"
|
|
49
|
+
ref="tabs"
|
|
50
|
+
:model-value="current.value"
|
|
51
|
+
:items="frameworks"
|
|
52
|
+
color="primary"
|
|
53
|
+
variant="link"
|
|
54
|
+
size="sm"
|
|
55
|
+
class="framework-switcher"
|
|
56
|
+
:ui="{
|
|
57
|
+
root: 'my-5 gap-4',
|
|
58
|
+
// Scrolls when the tabs outgrow the width. The baseline and the selected
|
|
59
|
+
// tab's underline are drawn inside the list, where scrolling doesn't clip them.
|
|
60
|
+
list: 'overflow-x-auto border-b-0 mb-0 shadow-[inset_0_-1px_0_var(--ui-border)]',
|
|
61
|
+
indicator: 'bottom-0',
|
|
62
|
+
trigger: 'shrink-0',
|
|
63
|
+
content: '*:first:mt-0 *:last:mb-0',
|
|
64
|
+
}"
|
|
65
|
+
@update:model-value="framework = String($event)"
|
|
66
|
+
>
|
|
67
|
+
<template #content="{ item }">
|
|
68
|
+
<p v-if="!slots[item.value] && shown(item)" class="mb-2 text-sm text-muted">
|
|
69
|
+
Not available for {{ item.label }}, showing {{ shown(item)!.label }}.
|
|
70
|
+
</p>
|
|
71
|
+
<slot v-if="shown(item)" :name="shown(item)!.value" />
|
|
72
|
+
</template>
|
|
73
|
+
</UTabs>
|
|
48
74
|
</template>
|
|
@@ -1,24 +1,18 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* `app.config.ts`. The in-content `FrameworkSwitcher` shares the same state,
|
|
7
|
-
* but per-page tabs alone scale poorly past a handful of frameworks — this is
|
|
8
|
-
* the always-visible control for the same preference.
|
|
9
|
-
*/
|
|
2
|
+
// Replaces Docus's own to put the framework select above the sidebar navigation,
|
|
3
|
+
// and renders Docus's below it, so its section links still show when enabled.
|
|
4
|
+
import DocusAsideLeftTop from "docus/app/components/docs/DocsAsideLeftTop.vue";
|
|
5
|
+
|
|
10
6
|
const { frameworks } = useFramework();
|
|
11
|
-
const
|
|
7
|
+
const labelId = useId();
|
|
12
8
|
</script>
|
|
13
9
|
|
|
14
10
|
<template>
|
|
15
11
|
<div v-if="frameworks.length">
|
|
16
|
-
<span
|
|
17
|
-
|
|
18
|
-
>
|
|
19
|
-
<span class="truncate">{{ t("docs.framework") }}</span>
|
|
12
|
+
<span :id="labelId" class="w-full pr-2.5 py-1.5 flex items-center text-sm text-muted">
|
|
13
|
+
Framework
|
|
20
14
|
</span>
|
|
21
|
-
|
|
22
|
-
<DocsFrameworkSelect class="mb-2" />
|
|
15
|
+
<DocsFrameworkSelect :aria-labelledby="labelId" class="mb-2" />
|
|
23
16
|
</div>
|
|
17
|
+
<DocusAsideLeftTop />
|
|
24
18
|
</template>
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const { framework, frameworks } = useFramework();
|
|
3
|
-
|
|
4
|
-
const current = computed(
|
|
5
|
-
() => frameworks.value.find((option) => option.value === framework.value) ?? frameworks.value[0],
|
|
6
|
-
);
|
|
2
|
+
const { framework, frameworks, current } = useFramework();
|
|
7
3
|
</script>
|
|
8
4
|
|
|
9
5
|
<template>
|
|
10
6
|
<USelect
|
|
11
|
-
|
|
12
|
-
:items="
|
|
7
|
+
:model-value="current?.value"
|
|
8
|
+
:items="frameworks"
|
|
13
9
|
variant="ghost"
|
|
14
10
|
color="neutral"
|
|
15
11
|
:icon="current?.icon"
|
|
16
12
|
class="w-full"
|
|
13
|
+
@update:model-value="framework = $event"
|
|
17
14
|
/>
|
|
18
15
|
</template>
|
|
@@ -1,61 +1,53 @@
|
|
|
1
1
|
import { useLocalStorage } from "@vueuse/core";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* A
|
|
5
|
-
* `DocsFrameworkSelect`.
|
|
6
|
-
* storage token; `label` is the visible text; `icon` is an Iconify name.
|
|
7
|
-
* `value` is an open string so a consumer can declare any framework set
|
|
8
|
-
* (e.g. `svelte`, `solid`, `angular`).
|
|
4
|
+
* A framework offered by the `FrameworkSwitcher` content component and the
|
|
5
|
+
* sidebar's `DocsFrameworkSelect`.
|
|
9
6
|
*/
|
|
10
7
|
export interface FrameworkOption {
|
|
8
|
+
/** The slot name pages write this framework's examples in (`#react`), and the stored pick. */
|
|
11
9
|
value: string;
|
|
12
10
|
label: string;
|
|
11
|
+
/** An Iconify icon, as `i-simple-icons-react` or `simple-icons:react`. */
|
|
13
12
|
icon: string;
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
declare module "nuxt/schema" {
|
|
16
|
+
interface AppConfigInput {
|
|
17
|
+
docsTheme?: {
|
|
18
|
+
/** The frameworks the docs' examples come in, in display order. The first is the default. */
|
|
19
|
+
frameworks?: FrameworkOption[];
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
|
-
*
|
|
21
|
-
* `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* export default defineAppConfig({
|
|
25
|
-
* docsTheme: {
|
|
26
|
-
* frameworks: [
|
|
27
|
-
* { value: "react", label: "React", icon: "i-mdi-react" },
|
|
28
|
-
* { value: "vue", label: "Vue", icon: "i-mdi-vuejs" },
|
|
29
|
-
* { value: "svelte", label: "Svelte", icon: "i-mdi-svelte" },
|
|
30
|
-
* ],
|
|
31
|
-
* },
|
|
32
|
-
* });
|
|
33
|
-
* ```
|
|
25
|
+
* The reader's framework, shared by every `FrameworkSwitcher` on the page and
|
|
26
|
+
* the sidebar's `DocsFrameworkSelect`, and kept across visits. The options are
|
|
27
|
+
* the app's `docsTheme.frameworks`; with none, the switcher and the select
|
|
28
|
+
* render nothing.
|
|
34
29
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* default list; a consumer that declares none gets an empty list and the
|
|
38
|
-
* framework UI renders nothing.
|
|
39
|
-
*
|
|
40
|
-
* The list is read from app config rather than merged as a layer default on
|
|
41
|
-
* purpose: Nuxt's `defu` layer merge *concatenates* arrays, so a shipped default
|
|
42
|
-
* would append to (not replace) the consumer's list.
|
|
30
|
+
* The stored pick is only read once mounted, so the prerendered HTML and the
|
|
31
|
+
* first client render both show the default, and hydrate cleanly.
|
|
43
32
|
*/
|
|
44
|
-
export
|
|
45
|
-
const appConfig = useAppConfig()
|
|
46
|
-
docsTheme?: { frameworks?: FrameworkOption[] };
|
|
47
|
-
};
|
|
33
|
+
export function useFramework() {
|
|
34
|
+
const appConfig = useAppConfig();
|
|
48
35
|
|
|
49
|
-
const frameworks = computed<
|
|
50
|
-
() => appConfig.docsTheme?.frameworks ?? [],
|
|
51
|
-
);
|
|
36
|
+
const frameworks = computed<FrameworkOption[]>(() => appConfig.docsTheme.frameworks);
|
|
52
37
|
|
|
53
|
-
const framework = useLocalStorage
|
|
38
|
+
const framework = useLocalStorage("docs-theme:framework", frameworks.value[0]?.value ?? "", {
|
|
54
39
|
initOnMounted: true,
|
|
55
40
|
});
|
|
56
41
|
|
|
42
|
+
// A stored pick that's no longer on the list falls back to the default.
|
|
43
|
+
const current = computed(
|
|
44
|
+
() =>
|
|
45
|
+
frameworks.value.find((option) => option.value === framework.value) ?? frameworks.value[0],
|
|
46
|
+
);
|
|
47
|
+
|
|
57
48
|
return {
|
|
58
49
|
framework,
|
|
59
50
|
frameworks,
|
|
51
|
+
current,
|
|
60
52
|
};
|
|
61
|
-
}
|
|
53
|
+
}
|