@uxfront/layer-docs 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -213
- package/app/app.config.ts +8 -92
- package/app/components/app/AppHeaderLeft.vue +84 -0
- 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 +43 -38
- package/nuxt.config.ts +16 -170
- package/package.json +11 -77
- package/CHANGELOG.md +0 -192
- 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,81 @@
|
|
|
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. It can also set the header's site name the way the UXFront homepages do, signed "by UXFront".
|
|
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.
|
|
63
|
-
|
|
64
|
-
## Styling
|
|
65
|
-
|
|
66
|
-
**The consumer owns the single Tailwind entry.** The layer ships a palette-free
|
|
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.
|
|
72
|
-
|
|
73
|
-
Register one CSS entry:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
// nuxt.config.ts
|
|
77
|
-
export default defineNuxtConfig({
|
|
78
|
-
extends: ["@uxfront/layer-docs"],
|
|
79
|
-
css: ["./app/assets/css/main.css"],
|
|
80
|
-
});
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
whose first line imports the layer base, followed by your palette:
|
|
84
|
-
|
|
85
|
-
```css
|
|
86
|
-
/* app/assets/css/main.css */
|
|
87
|
-
@import "@uxfront/layer-docs/app/assets/css/main.css";
|
|
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.
|
|
88
34
|
|
|
89
|
-
|
|
90
|
-
node_modules), so re-scan your content and config for utility classes. */
|
|
91
|
-
@source "../../../content/**/*";
|
|
92
|
-
@source "../../app.config.ts";
|
|
35
|
+
## The header wordmark
|
|
93
36
|
|
|
94
|
-
|
|
95
|
-
--color-teal: hsl(189, 53%, 41%);
|
|
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
|
-
});
|
|
120
|
-
```
|
|
37
|
+
Docus prints the site name in the header as plain text. Set `docsTheme.wordmark` to write it the way the UXFront homepages do, one part bold, and `docsTheme.byline` to sign it "by UXFront", linked to uxfront.com:
|
|
121
38
|
|
|
122
39
|
```ts
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
40
|
+
// app/app.config.ts
|
|
41
|
+
export default defineAppConfig({
|
|
42
|
+
docsTheme: {
|
|
43
|
+
// **Open**Components
|
|
44
|
+
wordmark: { bold: "Open", regular: "Components" },
|
|
45
|
+
byline: true,
|
|
46
|
+
},
|
|
129
47
|
});
|
|
130
48
|
```
|
|
131
49
|
|
|
132
|
-
The
|
|
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
|
-
```
|
|
50
|
+
The wordmark links home, and its accessible name is Docus's `header.title`, or the site name. The byline sits beside the link, not inside it, and phones leave it out so the header's buttons keep their room. With neither set, the header shows Docus's own title or logo.
|
|
193
51
|
|
|
194
|
-
|
|
195
|
-
does not follow the page's colour mode.
|
|
52
|
+
## Writing examples
|
|
196
53
|
|
|
197
|
-
|
|
54
|
+
Put one slot per framework in a `::framework-switcher`:
|
|
198
55
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
56
|
+
````md
|
|
57
|
+
::framework-switcher
|
|
58
|
+
#react
|
|
202
59
|
|
|
203
|
-
```
|
|
204
|
-
|
|
60
|
+
```tsx [Button.tsx]
|
|
61
|
+
export const Button = () => <button>Save</button>;
|
|
205
62
|
```
|
|
206
63
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
## OG images
|
|
64
|
+
#vue
|
|
210
65
|
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
});
|
|
66
|
+
```vue [Button.vue]
|
|
67
|
+
<template><button>Save</button></template>
|
|
228
68
|
```
|
|
229
69
|
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
70
|
+
::
|
|
71
|
+
````
|
|
245
72
|
|
|
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.
|
|
73
|
+
## What it adds
|
|
249
74
|
|
|
250
|
-
|
|
75
|
+
- **`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.
|
|
76
|
+
- **A Framework select above the sidebar.** It replaces Docus's `DocsAsideLeftTop` and renders Docus's below it.
|
|
77
|
+
- **A header wordmark and byline.** It replaces Docus's `AppHeaderLeft`, and renders Docus's unless the app sets a wordmark.
|
|
78
|
+
- **`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.
|
|
79
|
+
- **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
80
|
|
|
252
|
-
[
|
|
81
|
+
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
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Replaces Docus's, which prints the site name as plain text, to set the app's
|
|
3
|
+
// wordmark (`docsTheme.wordmark`) and sign it "by UXFront" (`docsTheme.byline`),
|
|
4
|
+
// the way the UXFront homepage headers do. With neither, it renders Docus's own.
|
|
5
|
+
import UxFrontMark from "@uxfront/ui/components/icons/UxFrontMark.vue";
|
|
6
|
+
import DocusAppHeaderLeft from "docus/app/components/app/AppHeaderLeft.vue";
|
|
7
|
+
|
|
8
|
+
const appConfig = useAppConfig();
|
|
9
|
+
const site = useSiteConfig();
|
|
10
|
+
const { localePath } = useDocusI18n();
|
|
11
|
+
|
|
12
|
+
const wordmark = computed(() => appConfig.docsTheme.wordmark);
|
|
13
|
+
const ariaLabel = computed(() => appConfig.header?.title || site.name);
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div class="docs-header-left">
|
|
18
|
+
<NuxtLink v-if="wordmark" :to="localePath('/')" :aria-label="ariaLabel" class="docs-wordmark">
|
|
19
|
+
<strong>{{ wordmark.bold }}</strong
|
|
20
|
+
>{{ wordmark.regular }}
|
|
21
|
+
</NuxtLink>
|
|
22
|
+
<DocusAppHeaderLeft v-else />
|
|
23
|
+
<!-- Beside the home link, not inside it, since it holds a link of its own. -->
|
|
24
|
+
<span v-if="appConfig.docsTheme.byline" class="docs-byline">
|
|
25
|
+
by
|
|
26
|
+
<a href="https://uxfront.com">
|
|
27
|
+
<UxFrontMark />
|
|
28
|
+
<span><strong>UX</strong>Front</span>
|
|
29
|
+
</a>
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
34
|
+
<style scoped>
|
|
35
|
+
/* Styled here rather than with Tailwind utilities: Docus's stylesheet doesn't
|
|
36
|
+
list this layer among Tailwind's sources, so only utilities Docus happens to
|
|
37
|
+
use itself would be generated. */
|
|
38
|
+
.docs-header-left {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: 0.75rem;
|
|
42
|
+
min-width: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.docs-wordmark {
|
|
46
|
+
flex-shrink: 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Phones leave the byline out: beside the header's buttons, it pushes the menu
|
|
50
|
+
toggle off-screen. */
|
|
51
|
+
.docs-byline {
|
|
52
|
+
display: none;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: 0.375rem;
|
|
55
|
+
font-size: 0.75rem;
|
|
56
|
+
line-height: 1rem;
|
|
57
|
+
white-space: nowrap;
|
|
58
|
+
color: var(--ui-text-muted);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.docs-byline a {
|
|
62
|
+
display: inline-flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 0.25rem;
|
|
65
|
+
font-weight: 500;
|
|
66
|
+
color: var(--ui-text-toned);
|
|
67
|
+
transition: color 0.15s;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.docs-byline a:hover {
|
|
71
|
+
color: var(--ui-text-highlighted);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.docs-byline svg {
|
|
75
|
+
width: 0.875rem;
|
|
76
|
+
height: 0.875rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@media (min-width: 40rem) {
|
|
80
|
+
.docs-byline {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
</style>
|
|
@@ -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>
|