@bndynet/vue-site 1.4.1 → 1.8.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 CHANGED
@@ -38,6 +38,7 @@ You can also create the configuration manually:
38
38
  import { defineConfig } from '@bndynet/vue-site'
39
39
 
40
40
  export default defineConfig({
41
+ name: 'My Project',
41
42
  title: 'My Project',
42
43
  favicon: '/favicon.ico',
43
44
  nav: [
@@ -100,10 +101,11 @@ Add `"dev": "vue-site dev"` (or `vs dev`) in `package.json` scripts if you like.
100
101
 
101
102
  | Property | Description |
102
103
  |----------|-------------|
103
- | `title` | Site title (sidebar + tab). `LocalizedString` |
104
+ | `name` | Brand name shown next to the logo (header + sidebar). `LocalizedString`. Optional — omitted or empty means no brand text is rendered |
105
+ | `title` | Browser tab title. `LocalizedString`. Optional — omitted or empty means the document title is left unchanged |
104
106
  | `nav` | `NavItem[]` |
105
107
  | `navPosition` | Optional navigation placement: `'top'` forces the top/tiered layout, `'sidebar'` forces the full nav tree into the sidebar. Omit to keep automatic behavior based on nav depth |
106
- | `defaultPath` | Path the site opens at; `/` and unknown paths redirect here. Must match a registered route (a `nav` item's resolved path or a `pages` entry's `path`). Defaults to the first top-level `nav` item |
108
+ | `defaultPath` | Path the site opens at; `/` and unknown paths redirect here. Must match a registered route (a `nav` item's resolved path or a `pages` entry's `path`). Defaults to the first registered page in navigation order, then the first visible standalone page |
107
109
  | `favicon` | Browser tab icon URL. Use a public asset path, remote URL, or imported asset URL |
108
110
  | `logo` | Logo URL or imported image |
109
111
  | `theme` | See `ThemeConfig` below; set to `false` to disable theming (hides the switcher, forces a fixed `light` palette, no localStorage persistence) |
@@ -117,7 +119,7 @@ Add `"dev": "vue-site dev"` (or `vs dev`) in `package.json` scripts if you like.
117
119
  | `auth` | Central authorization policy (`AuthConfig`) — see [Per-page authorization](#per-page-authorization-auth) |
118
120
  | `router` | History mode (`RouterConfig`) — `hash` (default) or HTML5 `web`; see [Router history](#router-history-router) |
119
121
  | `packageRepository` | Usually set by CLI from `package.json`; omit when using `createSiteApp` alone |
120
- | `custom` | Public application-specific values, available from `useSiteConfig().config.custom`; see [Custom client configuration](#custom-client-configuration-custom) |
122
+ | `custom` | Public application-specific values, available from `useSiteConfig().config.custom` in components or `getSiteConfig().custom` elsewhere; see [Custom client configuration](#custom-client-configuration-custom) |
121
123
  | `env` | Dev/build options — see below |
122
124
  | `bootstrap` | Optional path from site root (e.g. `./bootstrap.ts`) — module loaded once before the Vue app |
123
125
  | `configureApp` | Optional `(app) => void \| Promise<void>` after router install, before `mount` (see [Local packages in `configureApp`](#local-packages-in-configureapp)) |
@@ -125,7 +127,7 @@ Add `"dev": "vue-site dev"` (or `vs dev`) in `package.json` scripts if you like.
125
127
  ### Custom client configuration (`custom`)
126
128
 
127
129
  Use `custom` for public, application-specific values that the framework should preserve without
128
- interpreting. They are available to every component through `useSiteConfig()`:
130
+ interpreting:
129
131
 
130
132
  ```typescript
131
133
  // site.config.ts
@@ -144,6 +146,21 @@ const { config } = useSiteConfig()
144
146
  const apiBaseUrl = config.custom?.apiBaseUrl
145
147
  ```
146
148
 
149
+ Ordinary modules such as `utils.ts` do not have Vue's injection context. Read the active config
150
+ with `getSiteConfig()` instead:
151
+
152
+ ```typescript
153
+ // utils/api.ts
154
+ import { getSiteConfig } from '@bndynet/vue-site'
155
+
156
+ export function getApiBaseUrl() {
157
+ return getSiteConfig().custom?.apiBaseUrl
158
+ }
159
+ ```
160
+
161
+ Call `getSiteConfig()` at runtime, after `createSiteApp()` has started. Do not evaluate it at the
162
+ top level of a module imported by `site.config.ts`; the active config has not been registered yet.
163
+
147
164
  Use a different config file when the value changes between builds:
148
165
 
149
166
  ```typescript
@@ -216,6 +233,20 @@ export default defineConfig({
216
233
  })
217
234
  ```
218
235
 
236
+ ### Responsive helpers
237
+
238
+ Wide content — such as an Element Plus `el-table` or a multi-column `el-form` — can overflow on
239
+ small screens. Wrap it in a `.site-responsive-scroll` container to give it a horizontal scrollbar
240
+ (with momentum scrolling on touch and a visible scrollbar hint) instead of overflowing the page:
241
+
242
+ ```vue
243
+ <template>
244
+ <div class="site-responsive-scroll">
245
+ <el-table :data="rows">…</el-table>
246
+ </div>
247
+ </template>
248
+ ```
249
+
219
250
  ### Icon registry
220
251
 
221
252
  The CLI scans configured Lucide icon names in `nav`, `links`, `i18n.locales`, and `theme.extraThemes`
@@ -250,13 +281,84 @@ Built-in themes are `light`, `dark`, plus the always-on extras `sepia` and `ocea
250
281
  | `palettes` | — | Partial overrides for built-in light/dark only |
251
282
  | `extraThemes` | — | Extra themes: `id`, `label`, `icon`, optional `basedOn`, `palette`; reuse a built-in id (`sepia`/`ocean`) to override it. Import `builtinThemePalettes` for full defaults |
252
283
 
284
+ ### Semantic color tokens
285
+
286
+ Every theme is driven by semantic `--color-*` CSS variables. Shared Element Plus variables are
287
+ mapped to them in `element-plus-theme.css`, so common controls and surfaces follow the active theme.
288
+ Some components also have component-specific variables; those may need additional mapping when
289
+ adding new Element Plus components. Built-in palettes define seed colors and derive related values
290
+ with `color-mix()` / `var()`, so `builtinThemePalettes` may contain CSS expressions rather than
291
+ literal colors.
292
+
293
+ | Token | Purpose |
294
+ |-------|---------|
295
+ | `--color-primary` | Brand / primary (`--el-color-primary` and its tints) |
296
+ | `--color-primary-contrast` | Text color on filled primary controls; defaults to the theme background |
297
+ | `--color-primary-hover` | Hover color for filled primary controls; derived from primary and text colors |
298
+ | `--color-success` | Success |
299
+ | `--color-warning` | Warning |
300
+ | `--color-danger` | Danger / error (`--el-color-error` reuses it) |
301
+ | `--color-info` | Info |
302
+ | `--color-surface-raised` | Elevated surfaces: popovers, dropdowns, dialogs, masks |
303
+ | `--color-overlay` | Modal / loading overlay |
304
+
305
+ Define just these base colors in a custom theme; the framework derives Element Plus's
306
+ `light-3/5/7/8/9` and `dark-2` shades with `color-mix()`:
307
+
308
+ ```typescript
309
+ theme: {
310
+ extraThemes: [
311
+ {
312
+ id: 'forest',
313
+ label: 'Forest',
314
+ basedOn: 'light',
315
+ palette: {
316
+ '--color-primary': '#2f855a',
317
+ '--color-surface-raised': '#f7faf8',
318
+ },
319
+ },
320
+ ],
321
+ }
322
+ ```
323
+
324
+ If a custom primary/background pair has low contrast, set `--color-primary-contrast` explicitly.
325
+ It is used for text on filled primary buttons, tags, and selected button groups.
326
+
327
+ ### Overriding any token
328
+
329
+ Every token can be overridden, not just the base colors. Overrides are applied in three levels,
330
+ from lowest to highest precedence:
331
+
332
+ 1. `palettes.light` / `palettes.dark`, or an `extraThemes[].palette` — merged onto the built-in
333
+ palette for that theme.
334
+ 2. `colors` — applied globally after the active palette, so it wins across every theme.
335
+ 3. Runtime `document.documentElement.style.setProperty('--color-…', …)`.
336
+
337
+ Overriding a **seed** color (e.g. `--color-border`) also updates the derived tokens that
338
+ reference it (`--color-sidebar-border`, `--color-code-border`, `--color-table-border`,
339
+ `--color-blockquote-border`). Overriding a **derived** token affects only that token. Prefer
340
+ overriding seeds when the value should adapt across themes.
341
+
342
+ ```typescript
343
+ theme: {
344
+ colors: {
345
+ '--color-theme-switch-hover': 'rgba(0, 0, 0, 0.08)',
346
+ },
347
+ palettes: {
348
+ light: {
349
+ '--color-sidebar-bg': '#f0f2f5',
350
+ },
351
+ },
352
+ }
353
+ ```
354
+
253
355
  ## Shell actions (`shell.actions`)
254
356
 
255
357
  Use `shell.actions` to render app-specific Vue components in the active layout's global toolbar.
256
- In top/tiered navigation they appear at the end of the top header; in sidebar-only navigation they
257
- appear in the sidebar footer after the built-in links, locale switcher, and theme switcher. This is
258
- the recommended place for a signed-in user avatar, notification button, help menu, or tenant
259
- switcher.
358
+ On mobile, they appear at the right side of the mobile header. On desktop, they appear at the end
359
+ of the top header in tiered navigation, or in the sidebar footer after the built-in controls in
360
+ sidebar-only navigation. This is the recommended place for a signed-in user avatar, notification
361
+ button, help menu, or tenant switcher.
260
362
 
261
363
  ```typescript
262
364
  export default defineConfig({
@@ -292,8 +394,8 @@ Set `align` to `'left'`, `'center'`, or `'right'` for sidebar-footer placement;
292
394
  ## Internationalization (`i18n`)
293
395
 
294
396
  Set `i18n` to enable multi-language support. The framework adds a locale switcher to the header,
295
- resolves every `LocalizedString` field (`title`, `nav[].label`, `footer`, `links[].title`) against
296
- the active locale, and exposes the current locale via `useLocale()` / `useLocalize()`.
397
+ resolves every `LocalizedString` field (`name`, `title`, `nav[].label`, `footer`, `links[].title`)
398
+ against the active locale, and exposes the current locale via `useLocale()` / `useLocalize()`.
297
399
 
298
400
  ```typescript
299
401
  import { defineConfig } from '@bndynet/vue-site'
@@ -306,7 +408,8 @@ export default defineConfig({
306
408
  ],
307
409
  defaultLocale: 'en',
308
410
  },
309
- title: { en: 'My Site', zh: '我的站点' }, // a LocalizedString
411
+ name: { en: 'My Site', zh: '我的站点' }, // brand text next to the logo
412
+ title: { en: 'My Site', zh: '我的站点' }, // browser-tab title
310
413
  footer: { en: '© 2026', zh: '© 2026 版权所有' },
311
414
  nav: [
312
415
  { label: { en: 'Home', zh: '首页' }, icon: 'home', page: '../README.md' },
@@ -408,8 +511,8 @@ base file → first entry).
408
511
 
409
512
  | Property | Default | Description |
410
513
  |----------|---------|-------------|
411
- | `locales` | discovered `locales/*.json` | `{ code, label, icon? }[]` — supported languages, in display order. Optional: derived from the auto-loaded file names (with built-in labels) when omitted. First entry is the fallback |
412
- | `defaultLocale` | `locales[0].code` | Initial locale when nothing is stored and detection finds no match |
514
+ | `locales` | discovered `locales/*.json` | `{ code, label, icon? }[]` — supported languages, in display order. Optional in CLI mode; without the CLI, codes are derived from `i18n.messages` keys, or from `defaultLocale` / `en` if omitted. First entry is the fallback |
515
+ | `defaultLocale` | first resolved locale, or `en` | Initial locale when nothing is stored and browser detection finds no match |
413
516
  | `detectBrowser` | `true` | Detect the initial locale from `navigator.language(s)` on first visit |
414
517
  | `storageKey` | `vue-site-locale` | localStorage key for the chosen locale |
415
518
  | `messages` | auto-loaded from `locales/<code>.json` | `Record<LocaleCode, Record<string, string>>` — extra/override translations, merged over the auto-loaded files and built-in UI strings |
@@ -424,7 +527,7 @@ no `messages` field) and you don't even have to list the languages.
424
527
  ```jsonc
425
528
  // locales/en.json — nested groups (recommended), flattened to dotted ids
426
529
  {
427
- "site": { "title": "My Site" },
530
+ "site": { "name": "My Site", "title": "My Site" },
428
531
  "nav": { "home": "Home", "guide": "Guide" }
429
532
  }
430
533
  ```
@@ -432,7 +535,7 @@ no `messages` field) and you don't even have to list the languages.
432
535
  ```jsonc
433
536
  // locales/zh.json
434
537
  {
435
- "site": { "title": "我的站点" },
538
+ "site": { "name": "我的站点", "title": "我的站点" },
436
539
  "nav": { "home": "首页", "guide": "指南" }
437
540
  }
438
541
  ```
@@ -454,6 +557,7 @@ export default defineConfig({
454
557
  ],
455
558
  defaultLocale: 'en',
456
559
  },
560
+ name: tk('site.name'),
457
561
  title: tk('site.title'),
458
562
  nav: [
459
563
  { label: tk('nav.home'), icon: 'home', page: '../README.md' },
@@ -476,9 +580,12 @@ mixed freely — use `tk()` for shared/centrally managed text and an inline map
476
580
  relative to the config directory. `code` is the file name (a `LocaleCode` like `en` or `zh-TW`).
477
581
  - An explicit `i18n.messages` is still supported and **overrides** auto-loaded keys (per id); an
478
582
  explicit `i18n.locales` controls the label/icon/order. Both are optional.
479
- - Auto-discovery is a **CLI** feature. If you embed the library yourself (calling `createSiteApp`
480
- without the `vue-site` CLI), pass `i18n.messages` directly — e.g. build it from JSON with explicit
481
- imports (avoid `import.meta.glob` inside `site.config.ts`, which the CLI pre-loads in Node).
583
+ - Auto-discovery of `locales/<code>.json` is a **CLI** feature. If you embed the library yourself
584
+ (calling `createSiteApp` without the `vue-site` CLI), pass `i18n.messages` directly — e.g. build it
585
+ from JSON with explicit imports (avoid `import.meta.glob` inside `site.config.ts`, which the CLI
586
+ pre-loads in Node). When `i18n.locales` is omitted, the library derives locale codes from the
587
+ `messages` keys and uses those codes as display labels. For localized content without a messages
588
+ catalog, provide `locales` explicitly so the locale switcher has the intended options and labels.
482
589
 
483
590
  ### Localizing in your own pages
484
591
 
@@ -592,7 +699,7 @@ Notes:
592
699
  | Property | Description |
593
700
  |----------|-------------|
594
701
  | `port` | Dev server port |
595
- | `outDir` | Build output (relative to site root; default `{folder}-dist`) |
702
+ | `outDir` | Build output (relative to site root; default `dist`) |
596
703
  | `customElements` | Tag prefixes for custom elements (e.g. `['chat-', 'i-']`) |
597
704
  | `watchPackages` | Local packages — see [env.watchPackages](#envwatchpackages) |
598
705
  | `vite` | Vite overrides (not `root`); framework merges aliases, `server.fs.allow`, `build.outDir`, etc. |
@@ -639,11 +746,11 @@ app.mount('#app')
639
746
 
640
747
  Use a top-level `await` in your entry (or an async IIFE): `createSiteApp` is async and **awaits** `configureApp` when it returns a `Promise`. If you set optional `bootstrap` in config, that module loads before the app is created; if you omit `bootstrap`, that step is skipped.
641
748
 
642
- Exports: `createSiteApp`, `defineConfig`, `useTheme`, `useSiteConfig`, `useLocale`, `useLocalize`, `tk`, `resolveLocalized`, `resolveField`, `resolveMessage`, `mergeCatalog`, `flattenMessages`, `isMessageRef`, `localizedPage`, `builtinMessages`, `themeRefKey`, `localeRefKey`. Types: `SiteConfig`, `SiteEnvConfig`, `SiteViteConfig`, `SiteCustomConfig`, `SiteExternalLink`, `ShellConfig`, `ShellAction`, `ShellActionLoader`, `NavItem`, `StandalonePage`, `PageLayout`, `AuthRule`, `AuthContext`, `AuthConfig`, `RouterConfig`, `ThemeConfig`, `ThemeOption`, `ThemePaletteVars`, `ResolvedNavItem`, `I18nConfig`, `LocaleOption`, `LocaleCode`, `LocalizedString`, `IconRegistry`, `MessageRef`, `MessageTree`, `MessageCatalog`, `PageLoader`, `LocalizedPageOptions`.
749
+ Exports: `createSiteApp`, `defineConfig`, `useTheme`, `useMobileViewport`, `useSiteConfig`, `getSiteConfig`, `useLocale`, `useLocalize`, `tk`, `resolveLocalized`, `resolveField`, `resolveMessage`, `mergeCatalog`, `flattenMessages`, `isMessageRef`, `localizedPage`, `builtinMessages`, `themeRefKey`, `localeRefKey`. Types: `SiteConfig`, `SiteEnvConfig`, `SiteViteConfig`, `SiteCustomConfig`, `SiteExternalLink`, `ShellConfig`, `ShellAction`, `ShellActionLoader`, `NavItem`, `StandalonePage`, `PageLayout`, `AuthRule`, `AuthContext`, `AuthConfig`, `RouterConfig`, `ThemeConfig`, `ThemeOption`, `ThemePaletteVars`, `ResolvedNavItem`, `I18nConfig`, `LocaleOption`, `LocaleCode`, `LocalizedString`, `IconRegistry`, `MessageRef`, `MessageTree`, `MessageCatalog`, `PageLoader`, `LocalizedPageOptions`.
643
750
 
644
751
  ### Theme in Vue pages (`useTheme`)
645
752
 
646
- Import `useTheme` from `@bndynet/vue-site`. It returns a reactive `theme` ref (the active theme id, e.g. `light`, `dark`, or an `extraThemes[].id`) plus `setTheme` and `toggleTheme`. The root layout also sets `document.documentElement` attribute `data-theme` and applies CSS variables, so you can style with `var(--color-*)` without JavaScript.
753
+ Import `useTheme` from `@bndynet/vue-site`. It returns a reactive `theme` ref (the active theme id, e.g. `light`, `dark`, or an `extraThemes[].id`) plus `setTheme` and `toggleTheme`. The root layout also sets `document.documentElement` attribute `data-theme`, applies CSS variables, and updates `color-scheme` so native browser controls and scrollbars use the active theme's light or dark scheme.
647
754
 
648
755
  The theme ref is **provided** by `createSiteApp()` (same Vue runtime as your app), so `watch(theme, …)` works reliably even when another tool would otherwise load a second copy of `vue` from nested `node_modules`.
649
756
 
@@ -666,6 +773,28 @@ watch(theme, (next, prev) => {
666
773
 
667
774
  `watch` only runs when the theme **changes** after your component is set up (for example after the user clicks the theme control). It does not run on the initial value unless you pass `{ immediate: true }`.
668
775
 
776
+ ### Responsive viewport state (`useMobileViewport`)
777
+
778
+ Import `useMobileViewport` when component behavior needs to react to the same mobile breakpoint as
779
+ the site layout. It returns a read-only ref that is `true` at viewport widths of 840 CSS pixels or
780
+ narrower. The layout provides one shared ref, so components do not need to create their own media
781
+ query listeners. For visual-only changes, prefer CSS media queries.
782
+
783
+ ```vue
784
+ <script setup lang="ts">
785
+ import { useMobileViewport } from '@bndynet/vue-site'
786
+
787
+ const isMobileViewport = useMobileViewport()
788
+ </script>
789
+
790
+ <template>
791
+ <button v-if="isMobileViewport">Open compact menu</button>
792
+ </template>
793
+ ```
794
+
795
+ Call the composable from a Vue component rendered inside the site layout. It throws a `[vue-site]`
796
+ error when used outside that context.
797
+
669
798
  ## Upgrade
670
799
 
671
800
  ```bash
@@ -678,7 +807,7 @@ npm update @bndynet/vue-site
678
807
  git clone https://github.com/bndynet/vue-site.git && cd vue-site
679
808
  npm install
680
809
  npm run dev # watch-build lib + example site
681
- npm run build # `dist/` + `example/example-dist`
810
+ npm run build # `dist/` + `example/dist`
682
811
  ```
683
812
 
684
813
  ### Using a local build in another project
package/README.zh.md CHANGED
@@ -100,7 +100,7 @@ npx vue-site build --base /app/
100
100
  | `title` | 站点标题(侧边栏 + 标签页)。`LocalizedString` |
101
101
  | `nav` | `NavItem[]` |
102
102
  | `navPosition` | 可选的导航位置:`'top'` 强制使用顶部/分层导航,`'sidebar'` 强制将完整导航树放在侧边栏。不传时保持基于导航深度的自动行为 |
103
- | `defaultPath` | 站点打开时的路径;`/` 和未知路径会重定向到此。必须匹配一个已注册的路由(某个 `nav` 条目解析后的路径,或某个 `pages` 条目的 `path`)。默认为第一个顶级 `nav` 条目 |
103
+ | `defaultPath` | 站点打开时的路径;`/` 和未知路径会重定向到此。必须匹配一个已注册的路由(某个 `nav` 条目解析后的路径,或某个 `pages` 条目的 `path`)。默认选择导航顺序中的第一个已注册页面;如果导航中没有页面,则选择第一个可见的独立页面 |
104
104
  | `favicon` | 浏览器标签页图标 URL。可使用 public 资源路径、远程 URL 或导入后的资源 URL |
105
105
  | `logo` | Logo 的 URL 或导入的图片 |
106
106
  | `theme` | 见下方 `ThemeConfig`;设为 `false` 可禁用主题(隐藏切换器、强制使用固定的 `light` 调色板、不进行 localStorage 持久化) |
@@ -112,15 +112,14 @@ npx vue-site build --base /app/
112
112
  | `auth` | 中心化的授权策略(`AuthConfig`)—— 参见[按页面授权](#按页面授权-auth) |
113
113
  | `router` | 历史模式(`RouterConfig`)—— `hash`(默认)或 HTML5 `web`;参见[路由历史](#路由历史-router) |
114
114
  | `packageRepository` | 通常由 CLI 从 `package.json` 设置;单独使用 `createSiteApp` 时可省略 |
115
- | `custom` | 公开的业务自定义配置,可通过 `useSiteConfig().config.custom` 读取;参见[自定义客户端配置](#自定义客户端配置-custom) |
115
+ | `custom` | 公开的业务自定义配置;组件内通过 `useSiteConfig().config.custom` 读取,其他代码通过 `getSiteConfig().custom` 读取;参见[自定义客户端配置](#自定义客户端配置-custom) |
116
116
  | `env` | 开发/构建选项 —— 见下文 |
117
117
  | `bootstrap` | 可选的站点根目录相对路径(如 `./bootstrap.ts`)—— 在 Vue 应用之前加载一次的模块 |
118
118
  | `configureApp` | 可选的 `(app) => void \| Promise<void>`,在路由安装之后、`mount` 之前执行(参见 [`configureApp` 中的本地包](#在-configureapp-中使用本地包)) |
119
119
 
120
120
  ### 自定义客户端配置(`custom`)
121
121
 
122
- `custom` 用于存放框架无需解释、只需原样保留的公开业务配置。任何组件都可以通过
123
- `useSiteConfig()` 读取:
122
+ `custom` 用于存放框架无需解释、只需原样保留的公开业务配置:
124
123
 
125
124
  ```typescript
126
125
  // site.config.ts
@@ -139,6 +138,20 @@ const { config } = useSiteConfig()
139
138
  const apiBaseUrl = config.custom?.apiBaseUrl
140
139
  ```
141
140
 
141
+ `utils.ts` 等普通模块没有 Vue 注入上下文,应改用 `getSiteConfig()` 读取当前配置:
142
+
143
+ ```typescript
144
+ // utils/api.ts
145
+ import { getSiteConfig } from '@bndynet/vue-site'
146
+
147
+ export function getApiBaseUrl() {
148
+ return getSiteConfig().custom?.apiBaseUrl
149
+ }
150
+ ```
151
+
152
+ 请在 `createSiteApp()` 已开始执行后的运行时代码中调用 `getSiteConfig()`。不要在被
153
+ `site.config.ts` 导入的模块顶层立即求值,因为此时当前配置尚未注册。
154
+
142
155
  如果不同构建环境使用不同的值,可以选择不同的配置文件:
143
156
 
144
157
  ```typescript
@@ -209,6 +222,20 @@ export default defineConfig({
209
222
  })
210
223
  ```
211
224
 
225
+ ### 响应式辅助
226
+
227
+ 宽内容——例如 Element Plus 的 `el-table` 或多列 `el-form`——在小屏幕上可能溢出。把它包进一个
228
+ `.site-responsive-scroll` 容器,即可获得横向滚动条(触屏惯性滚动 + 可见的滚动条提示),而不是
229
+ 溢出页面:
230
+
231
+ ```vue
232
+ <template>
233
+ <div class="site-responsive-scroll">
234
+ <el-table :data="rows">…</el-table>
235
+ </div>
236
+ </template>
237
+ ```
238
+
212
239
  ### `ThemeConfig`
213
240
 
214
241
  内置主题为 `light`、`dark`,外加始终可用的额外主题 `sepia` 和 `ocean`(在每个站点的切换器中都会显示)。在 `SiteConfig` 上设置 `theme: false` 可完全禁用主题。
@@ -220,6 +247,73 @@ export default defineConfig({
220
247
  | `palettes` | — | 仅针对内置 light/dark 的部分覆盖 |
221
248
  | `extraThemes` | — | 额外主题:`id`、`label`、`icon`,可选 `basedOn`、`palette`;复用内置 id(`sepia`/`ocean`)即可覆盖它。导入 `builtinThemePalettes` 获取完整默认值 |
222
249
 
250
+ ### 语义化颜色变量
251
+
252
+ 每个主题都由语义化的 `--color-*` CSS 变量驱动。Element Plus 的通用变量在
253
+ `element-plus-theme.css` 中映射到这些变量,因此常见控件和表面会跟随当前主题。部分组件
254
+ 还有专属变量;新增 Element Plus 组件时,可能需要补充对应映射。内置调色板定义种子色,
255
+ 其余值通过 `color-mix()` / `var()` 派生,因此 `builtinThemePalettes` 中可能包含 CSS 表达式,
256
+ 而不只是字面色值。
257
+
258
+ | 变量 | 用途 |
259
+ |------|------|
260
+ | `--color-primary` | 品牌/主色(映射到 `--el-color-primary` 及其色阶) |
261
+ | `--color-primary-contrast` | 主色填充控件上的文字色,默认取当前主题背景色 |
262
+ | `--color-primary-hover` | 主色填充控件的悬停色,由主色和文字色派生 |
263
+ | `--color-success` | 成功 |
264
+ | `--color-warning` | 警告 |
265
+ | `--color-danger` | 危险/错误(`--el-color-error` 复用该值) |
266
+ | `--color-info` | 信息 |
267
+ | `--color-surface-raised` | 浮层表面:下拉、对话框、遮罩背景 |
268
+ | `--color-overlay` | 弹窗 / 加载遮罩 |
269
+
270
+ 自定义主题只需定义这些基础色,框架会用 `color-mix()` 自动派生 Element Plus 的
271
+ `light-3/5/7/8/9` 与 `dark-2` 色阶:
272
+
273
+ ```typescript
274
+ theme: {
275
+ extraThemes: [
276
+ {
277
+ id: 'forest',
278
+ label: 'Forest',
279
+ basedOn: 'light',
280
+ palette: {
281
+ '--color-primary': '#2f855a',
282
+ '--color-surface-raised': '#f7faf8',
283
+ },
284
+ },
285
+ ],
286
+ }
287
+ ```
288
+
289
+ 如果自定义主色与背景的对比度不足,可以显式设置 `--color-primary-contrast`。它用于主色按钮、
290
+ 深色标签和选中按钮组等填充控件上的文字。
291
+
292
+ ### 覆盖任意 token
293
+
294
+ 不只是基础色,**任何 token 都可以覆盖**。覆盖按三级优先级从低到高应用:
295
+
296
+ 1. `palettes.light` / `palettes.dark`,或 `extraThemes[].palette` —— 合并到该主题的内置调色板。
297
+ 2. `colors` —— 在激活调色板之后全局应用,对所有主题生效。
298
+ 3. 运行时直接调用 `document.documentElement.style.setProperty('--color-…', …)`。
299
+
300
+ 覆盖 **种子色**(如 `--color-border`)会同时更新引用它的派生 token(`--color-sidebar-border`、
301
+ `--color-code-border`、`--color-table-border`、`--color-blockquote-border`);覆盖 **派生 token**
302
+ 则只影响它本身。如果希望颜色能随主题自动适配,优先覆盖种子色。
303
+
304
+ ```typescript
305
+ theme: {
306
+ colors: {
307
+ '--color-theme-switch-hover': 'rgba(0, 0, 0, 0.08)',
308
+ },
309
+ palettes: {
310
+ light: {
311
+ '--color-sidebar-bg': '#f0f2f5',
312
+ },
313
+ },
314
+ }
315
+ ```
316
+
223
317
  ## 国际化(`i18n`)
224
318
 
225
319
  设置 `i18n` 以启用多语言支持。框架会在头部添加语言切换器,
@@ -327,8 +421,8 @@ export default defineConfig({
327
421
 
328
422
  | 属性 | 默认值 | 说明 |
329
423
  |----------|---------|-------------|
330
- | `locales` | 自动发现的 `locales/*.json` | `{ code, label, icon? }[]` —— 支持的语言,按显示顺序排列。可选:省略时从自动加载的文件名派生(使用内置标签)。第一个条目为回退项 |
331
- | `defaultLocale` | `locales[0].code` | 当没有存储值且检测未命中时使用的初始语言 |
424
+ | `locales` | 自动发现的 `locales/*.json` | `{ code, label, icon? }[]` —— 支持的语言,按显示顺序排列。CLI 模式下可省略;不使用 CLI 时,省略后从 `i18n.messages` 的键派生,若也没有则使用 `defaultLocale` 或 `en`。第一个条目为回退项 |
425
+ | `defaultLocale` | 第一个解析出的语言,或 `en` | 当没有存储值且检测未命中时使用的初始语言 |
332
426
  | `detectBrowser` | `true` | 首次访问时从 `navigator.language(s)` 检测初始语言 |
333
427
  | `storageKey` | `vue-site-locale` | 用于存储所选语言的 localStorage 键 |
334
428
  | `messages` | 自动从 `locales/<code>.json` 加载 | `Record<LocaleCode, Record<string, string>>` —— 额外/覆盖的翻译,会合并到自动加载的文件和内置 UI 字符串之上 |
@@ -394,9 +488,11 @@ export default defineConfig({
394
488
  `code` 即文件名(一个 `LocaleCode`,如 `en` 或 `zh-TW`)。
395
489
  - 仍支持显式的 `i18n.messages`,并会**覆盖**自动加载的键(按 id);显式的 `i18n.locales`
396
490
  控制 label/icon/顺序。两者均为可选。
397
- - 自动发现是 **CLI** 特性。如果你自行嵌入该库(不使用 `vue-site` CLI 而直接调用 `createSiteApp`),
398
- 请直接传入 `i18n.messages` —— 例如用显式 import 从 JSON 构建(避免在 `site.config.ts` 中使用
399
- `import.meta.glob`,CLI 会在 Node 中预加载它)。
491
+ - 自动发现 `locales/<code>.json` 是 **CLI** 特性。如果你自行嵌入该库(不使用 `vue-site` CLI 而直接调用
492
+ `createSiteApp`),请直接传入 `i18n.messages` —— 例如用显式 import 从 JSON 构建(避免在
493
+ `site.config.ts` 中使用 `import.meta.glob`,CLI 会在 Node 中预加载它)。省略 `i18n.locales` 时,库会
494
+ 从 `messages` 的键派生语言代码,并直接用代码作为显示名称。若本地化内容没有消息目录,请显式提供
495
+ `locales`,以设置语言切换器的选项和名称。
400
496
 
401
497
  ### 在你自己的页面中本地化
402
498
 
@@ -577,14 +673,15 @@ app.mount('#app')
577
673
  返回 `Promise` 时会 **await** 它。如果你在配置中设置了可选的 `bootstrap`,该模块会在应用创建前加载;
578
674
  如果省略 `bootstrap`,则跳过该步骤。
579
675
 
580
- 导出:`createSiteApp`、`defineConfig`、`useTheme`、`useSiteConfig`、`useLocale`、`useLocalize`、`tk`、`resolveLocalized`、`resolveField`、`resolveMessage`、`mergeCatalog`、`flattenMessages`、`isMessageRef`、`localizedPage`、`builtinMessages`、`themeRefKey`、`localeRefKey`。类型:`SiteConfig`、`SiteEnvConfig`、`SiteViteConfig`、`SiteCustomConfig`、`SiteExternalLink`、`NavItem`、`StandalonePage`、`PageLayout`、`AuthRule`、`AuthContext`、`AuthConfig`、`RouterConfig`、`ThemeConfig`、`ThemeOption`、`ThemePaletteVars`、`ResolvedNavItem`、`I18nConfig`、`LocaleOption`、`LocaleCode`、`LocalizedString`、`MessageRef`、`MessageTree`、`MessageCatalog`、`PageLoader`、`LocalizedPageOptions`。
676
+ 导出:`createSiteApp`、`defineConfig`、`useTheme`、`useMobileViewport`、`useSiteConfig`、`getSiteConfig`、`useLocale`、`useLocalize`、`tk`、`resolveLocalized`、`resolveField`、`resolveMessage`、`mergeCatalog`、`flattenMessages`、`isMessageRef`、`localizedPage`、`builtinMessages`、`themeRefKey`、`localeRefKey`。类型:`SiteConfig`、`SiteEnvConfig`、`SiteViteConfig`、`SiteCustomConfig`、`SiteExternalLink`、`NavItem`、`StandalonePage`、`PageLayout`、`AuthRule`、`AuthContext`、`AuthConfig`、`RouterConfig`、`ThemeConfig`、`ThemeOption`、`ThemePaletteVars`、`ResolvedNavItem`、`I18nConfig`、`LocaleOption`、`LocaleCode`、`LocalizedString`、`MessageRef`、`MessageTree`、`MessageCatalog`、`PageLoader`、`LocalizedPageOptions`。
581
677
 
582
678
  ### 在 Vue 页面中使用主题(`useTheme`)
583
679
 
584
680
  从 `@bndynet/vue-site` 导入 `useTheme`。它返回一个响应式的 `theme` ref(当前激活的主题 id,
585
681
  如 `light`、`dark` 或某个 `extraThemes[].id`),外加 `setTheme` 和 `toggleTheme`。
586
682
  根布局还会在 `document.documentElement` 上设置 `data-theme` 属性并应用 CSS 变量,
587
- 因此你可以用 `var(--color-*)` 来设置样式,无需 JavaScript。
683
+ 因此你可以用 `var(--color-*)` 来设置样式,无需 JavaScript。它还会更新 `color-scheme`,
684
+ 让浏览器原生控件和滚动条跟随当前主题的亮色或暗色模式。
588
685
 
589
686
  theme ref 由 `createSiteApp()` **提供**(与你的应用同一个 Vue 运行时),所以 `watch(theme, …)`
590
687
  能可靠工作,即使其他工具可能会从嵌套的 `node_modules` 加载第二份 `vue`。
@@ -609,6 +706,26 @@ watch(theme, (next, prev) => {
609
706
  `watch` 仅在你的组件初始化之后主题**发生变化**时运行(例如用户点击主题控件之后)。
610
707
  除非你传入 `{ immediate: true }`,否则它不会在初始值上运行。
611
708
 
709
+ ### 响应式视口状态(`useMobileViewport`)
710
+
711
+ 当组件行为需要和站点布局使用相同的移动端断点时,导入 `useMobileViewport`。它返回一个只读 ref;
712
+ 视口宽度不超过 840 CSS 像素时值为 `true`。站点布局提供的是共享 ref,因此组件无需各自创建媒体查询监听器。
713
+ 如果只是调整视觉样式,优先使用 CSS 媒体查询。
714
+
715
+ ```vue
716
+ <script setup lang="ts">
717
+ import { useMobileViewport } from '@bndynet/vue-site'
718
+
719
+ const isMobileViewport = useMobileViewport()
720
+ </script>
721
+
722
+ <template>
723
+ <button v-if="isMobileViewport">打开紧凑菜单</button>
724
+ </template>
725
+ ```
726
+
727
+ 请在站点布局内部渲染的 Vue 组件中调用此 composable。在该上下文之外调用时,会抛出 `[vue-site]` 错误。
728
+
612
729
  ## 升级
613
730
 
614
731
  ```bash
package/bin/vue-site.mjs CHANGED
@@ -129,6 +129,7 @@ const configCandidates = [
129
129
  const initSiteConfig = `import { defineConfig } from '@bndynet/vue-site'
130
130
 
131
131
  export default defineConfig({
132
+ name: 'My Site',
132
133
  title: 'My Site',
133
134
  // Public client configuration. Never place secrets here.
134
135
  custom: {
@@ -227,6 +228,7 @@ function resolveSiteConfig(cliConfig) {
227
228
  'Run `npx vue-site init` to create starter files, or create site.config.ts manually:\n\n' +
228
229
  ' import type { SiteConfig } from \'@bndynet/vue-site\'\n\n' +
229
230
  ' export default {\n' +
231
+ ' name: \'My Site\',\n' +
230
232
  ' title: \'My Site\',\n' +
231
233
  ' nav: [\n' +
232
234
  ' { label: \'Home\', icon: \'home\', page: () => import(\'./README.md?raw\') },\n' +
@@ -443,7 +445,7 @@ function buildHtmlShell(scriptTag, siteConfig) {
443
445
  <html lang="en">
444
446
  <head>
445
447
  <meta charset="UTF-8" />
446
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
448
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
447
449
  ${buildFaviconLink(siteConfig)} <title></title>
448
450
  </head>
449
451
  <body>
@@ -1057,7 +1059,7 @@ async function buildViteConfig(options = {}) {
1057
1059
  },
1058
1060
  },
1059
1061
  build: {
1060
- outDir: resolve(cwd, outDir || `${basename(cwd)}-dist`),
1062
+ outDir: resolve(cwd, outDir || 'dist'),
1061
1063
  emptyOutDir: true,
1062
1064
  },
1063
1065
  ...(cliBase != null && { base: cliBase }),
@@ -1,2 +1,6 @@
1
- declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, HTMLDivElement>;
1
+ declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {
2
+ menuButton: HTMLButtonElement;
3
+ mobileDrawer: HTMLElement;
4
+ drawerCloseButton: HTMLButtonElement;
5
+ }, HTMLDivElement>;
2
6
  export default _default;
@@ -6,5 +6,6 @@ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {
6
6
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
7
7
  detailsRef: HTMLDetailsElement;
8
8
  summaryRef: HTMLElement;
9
+ menuRef: HTMLDivElement;
9
10
  }, HTMLDetailsElement>;
10
11
  export default _default;
@@ -1,6 +1,14 @@
1
1
  import { ResolvedNavItem } from '../types';
2
2
  type __VLS_Props = {
3
3
  item: ResolvedNavItem;
4
+ collapsed?: boolean;
5
+ hoverExpand?: boolean;
4
6
  };
5
- declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLDivElement>;
7
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
8
+ collapsed: boolean;
9
+ hoverExpand: boolean;
10
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
11
+ trigger: HTMLButtonElement;
12
+ popup: HTMLDivElement;
13
+ }, HTMLDivElement>;
6
14
  export default _default;
@@ -2,8 +2,10 @@ import { ResolvedNavItem } from '../types';
2
2
  type __VLS_Props = {
3
3
  item: ResolvedNavItem;
4
4
  indent?: boolean;
5
+ collapsed?: boolean;
5
6
  };
6
7
  declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
7
8
  indent: boolean;
9
+ collapsed: boolean;
8
10
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
9
11
  export default _default;
@@ -1,2 +1,13 @@
1
- declare const _default: import('vue').DefineComponent<{}, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, true, {}, HTMLElement>;
1
+ import { ResolvedNavItem } from '../types';
2
+ type __VLS_Props = {
3
+ /** Optional navigation tree override, used by the mobile drawer. */
4
+ items?: ResolvedNavItem[];
5
+ /** Whether desktop navigation is shown as an icon rail. */
6
+ collapsed?: boolean;
7
+ };
8
+ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
9
+ "toggle-collapse": () => any;
10
+ }, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
11
+ "onToggle-collapse"?: (() => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, HTMLElement>;
2
13
  export default _default;
@@ -6,5 +6,6 @@ declare const _default: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {
6
6
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {
7
7
  detailsRef: HTMLDetailsElement;
8
8
  summaryRef: HTMLElement;
9
+ menuRef: HTMLDivElement;
9
10
  }, HTMLDetailsElement>;
10
11
  export default _default;