diff --git a/demo/SiteHeader.vue b/demo/SiteHeader.vue
index 9c3b62ae4ef..9f2d3ebc433 100644
--- a/demo/SiteHeader.vue
+++ b/demo/SiteHeader.vue
@@ -10,7 +10,7 @@ import {
useDocOptions,
useFlattenedDocOptions,
useLocaleName,
- useThemeName
+ useThemeMode
} from './store'
import { renderMenuLabel } from './store/menu-options'
import { i18n, useIsMobile, useIsTablet } from './utils/composables'
@@ -32,6 +32,8 @@ const locales = {
'zh-CN': {
dark: '深色',
light: '浅色',
+ osTheme: '跟随系统',
+ theme: '主题',
searchPlaceholder: '搜索',
home: '首页',
doc: '文档',
@@ -45,6 +47,8 @@ const locales = {
'en-US': {
dark: 'Dark',
light: 'Light',
+ osTheme: 'System',
+ theme: 'Theme',
searchPlaceholder: 'Search',
home: 'Home',
doc: 'Docs',
@@ -68,17 +72,24 @@ export default defineComponent({
const router = useRouter()
const mobilePopoverRef = ref(null)
- const themeAndLocaleReg = /^(\/[^/]+){2}/
+ const localeReg = /^\/[^/]+/
// i18n
const { t } = i18n(locales)
// theme
- const themeNameRef = useThemeName()
- const themeLabelMapRef = computed(() => ({
- dark: t('light'),
- light: t('dark')
- }))
+ const themePreferenceRef = useThemeMode()
+ const themeOptionsRef = computed(() => [
+ { label: t('light'), value: 'light' },
+ { label: t('dark'), value: 'dark' },
+ { label: t('osTheme'), value: 'auto' }
+ ])
+ const themePreferenceLabelRef = computed(
+ () =>
+ themeOptionsRef.value.find(
+ option => option.value === themePreferenceRef.value
+ )?.label
+ )
// locale
const localeNameRef = useLocaleName()
@@ -89,17 +100,17 @@ export default defineComponent({
{
key: 'home',
label: t('home'),
- path: themeAndLocaleReg.exec(route.path)[0]
+ path: localeReg.exec(route.path)[0]
},
{
key: 'doc',
label: t('doc'),
- path: `${themeAndLocaleReg.exec(route.path)[0]}/docs/introduction`
+ path: `${localeReg.exec(route.path)[0]}/docs/introduction`
},
{
key: 'component',
label: t('component'),
- path: `${themeAndLocaleReg.exec(route.path)[0]}/components/button`
+ path: `${localeReg.exec(route.path)[0]}/components/button`
}
]
})
@@ -120,7 +131,11 @@ export default defineComponent({
return [
{
key: 'theme',
- label: themeLabelMapRef.value[themeNameRef.value]
+ label: `${t('theme')}: ${themePreferenceLabelRef.value}`,
+ children: themeOptionsRef.value.map(option => ({
+ key: `theme:${option.value}`,
+ label: option.label
+ }))
},
{
key: 'locale',
@@ -129,18 +144,18 @@ export default defineComponent({
{
key: 'home',
label: t('home'),
- path: themeAndLocaleReg.exec(route.path)[0]
+ path: localeReg.exec(route.path)[0]
},
{
key: 'doc',
label: t('doc'),
children: docOptionsRef.value,
- path: `${themeAndLocaleReg.exec(route.path)[0]}/docs/introduction`
+ path: `${localeReg.exec(route.path)[0]}/docs/introduction`
},
{
key: 'component',
label: t('component'),
- path: `${themeAndLocaleReg.exec(route.path)[0]}/components/button`,
+ path: `${localeReg.exec(route.path)[0]}/components/button`,
children: componentOptionsRef.value
},
{
@@ -155,8 +170,8 @@ export default defineComponent({
return findMenuValue(mobileMenuOptionsRef.value, route.path)
})
function handleUpdateMobileMenu(value, { path }) {
- if (value === 'theme') {
- handleThemeUpdate()
+ if (value.startsWith('theme:')) {
+ themePreferenceRef.value = value.slice('theme:'.length)
}
else if (value === 'locale') {
if (localeNameRef.value === 'zh-CN') {
@@ -175,15 +190,6 @@ export default defineComponent({
mobilePopoverRef.value.setShow(false)
}
- function handleThemeUpdate() {
- if (themeNameRef.value === 'dark') {
- themeNameRef.value = 'light'
- }
- else {
- themeNameRef.value = 'dark'
- }
- }
-
const localeLabelMap = {
'zh-CN': 'English',
'en-US': '中文'
@@ -272,11 +278,11 @@ export default defineComponent({
const isMobileRef = useIsMobile()
const isTabletRef = useIsTablet()
function handleLogoClick() {
- if (/^(\/[^/]+){2}$/.test(route.path)) {
+ if (/^\/[^/]+$/.test(route.path)) {
message.info(t('alreadyHome'))
return
}
- router.push(/^(\/[^/]+){2}/.exec(route.path)[0])
+ router.push(localeReg.exec(route.path)[0])
}
// responsive menu
@@ -302,9 +308,9 @@ export default defineComponent({
isTablet: isTabletRef,
repoUrl,
// theme
- theme: themeNameRef,
- handleThemeUpdate,
- themeLabelMap: themeLabelMapRef,
+ themePreference: themePreferenceRef,
+ themePreferenceLabel: themePreferenceLabelRef,
+ themeOptions: themeOptionsRef,
// displayMode
displayMode: displayModeRef,
displayModeLabelMap,
@@ -418,14 +424,16 @@ export default defineComponent({
>
{{ localeLabelMap[locale] }}
-
- {{ themeLabelMap[theme] }}
-
+
+ {{ themePreferenceLabel }}
+
+
{
- const langAndThemeReg = /\/(zh-CN|en-US)\/(light|dark|os-theme)/g
- // only theme & lang change do not restore the scroll status
- if (
- value.replace(langAndThemeReg, '')
- !== oldValue.replace(langAndThemeReg, '')
- ) {
+ const langReg = /\/(zh-CN|en-US)/g
+ // only lang change do not restore the scroll status
+ if (value.replace(langReg, '') !== oldValue.replace(langReg, '')) {
layoutInstRef.value.scrollTo(0, 0)
}
})
diff --git a/demo/routes/routes.js b/demo/routes/routes.js
index c1984262585..2c36d9a7b1e 100644
--- a/demo/routes/routes.js
+++ b/demo/routes/routes.js
@@ -1031,41 +1031,55 @@ export const zhComponentRoutes = [
export const routes = [
{
name: 'home',
- path: '/:lang/:theme',
+ path: '/:lang(en-US|zh-CN)',
component: () => import('../pages/home/index.vue')
},
{
name: 'enDocs',
- path: '/en-US/:theme/docs',
+ path: '/en-US/docs',
component: () => import('../pages/Layout.vue'),
children: enDocRoutes
},
{
name: 'zhDocs',
- path: '/zh-CN/:theme/docs',
+ path: '/zh-CN/docs',
component: () => import('../pages/Layout.vue'),
children: zhDocRoutes
},
{
name: 'enComponents',
- path: '/en-US/:theme/components',
+ path: '/en-US/components',
component: () => import('../pages/Layout.vue'),
children: enComponentRoutes
},
{
name: 'zhComponents',
- path: '/zh-CN/:theme/components',
+ path: '/zh-CN/components',
component: () => import('../pages/Layout.vue'),
children: zhComponentRoutes
},
+ {
+ // The theme used to be part of the path (`/zh-CN/os-theme/docs/...`).
+ // Old links are everywhere, so drop that segment and keep the rest. The
+ // theme in an old link is ignored: someone else's link shouldn't change
+ // the reader's own preference.
+ name: 'legacy-theme-path',
+ path: '/:lang(en-US|zh-CN)/:theme(light|dark|os-theme)/:rest(.*)*',
+ redirect: to => ({
+ path: [`/${to.params.lang}`, ...[].concat(to.params.rest || [])].join(
+ '/'
+ ),
+ query: to.query,
+ hash: to.hash
+ })
+ },
{
name: 'not-found',
path: '/:pathMatch(.*)*',
redirect: {
name: 'home',
params: {
- lang: navigator.language === 'zh-CN' ? 'zh-CN' : 'en-US',
- theme: 'os-theme'
+ lang: navigator.language === 'zh-CN' ? 'zh-CN' : 'en-US'
}
}
}
diff --git a/demo/store/index.js b/demo/store/index.js
index 3d85e334bb2..e97144e28b5 100644
--- a/demo/store/index.js
+++ b/demo/store/index.js
@@ -4,12 +4,12 @@ import {
dateZhCN,
enUS,
NConfigProvider,
- useOsTheme,
zhCN
} from 'naive-ui'
import { useMemo } from 'vooks'
import { computed, ref } from 'vue'
import { TsConfigProvider } from '../../themes/tusimple/src'
+import { useColorMode } from '../utils/color-mode'
import { i18n, useIsMobile } from '../utils/composables'
import hljs from './hljs'
import {
@@ -24,9 +24,18 @@ let localeNameRef = null
// useMemo
let dateLocaleRef = null
// theme
-const osThemeRef = useOsTheme()
-let themeNameRef = null
-let rawThemeNameRef = null // could be `os-theme`
+// The theme is the reader's preference, not part of the page, so it lives in
+// localStorage rather than the URL
+const { mode: themeModeRef, state: themeStateRef } = useColorMode({
+ storageKey: 'naive-ui-theme'
+})
+// The theme in effect, `light` or `dark`; setting it stores an explicit pick
+const themeNameRef = computed({
+ get: () => themeStateRef.value,
+ set: (theme) => {
+ themeModeRef.value = theme
+ }
+})
export function initRouter(_router, _route) {
route = _route
@@ -42,22 +51,6 @@ export function initRouter(_router, _route) {
dateLocaleRef = useMemo(() => {
return route.path.startsWith('/zh-CN') ? dateZhCN : dateEnUS
})
- rawThemeNameRef = useMemo(() => route.params.theme)
- themeNameRef = useMemo({
- get() {
- switch (route.params.theme) {
- case 'os-theme':
- return osThemeRef.value
- case 'dark':
- return 'dark'
- default:
- return 'light'
- }
- },
- set(theme) {
- router.push(changeThemeInPath(route.fullPath, theme))
- }
- })
}
// display mode
@@ -94,14 +87,12 @@ const configProviderRef = computed(() => {
// options
const docOptionsRef = computed(() =>
createDocumentationMenuOptions({
- theme: rawThemeNameRef.value,
lang: localeNameRef.value,
mode: displayModeRef.value
})
)
const componentOptionsRef = computed(() =>
createComponentMenuOptions({
- theme: rawThemeNameRef.value,
lang: localeNameRef.value,
mode: displayModeRef.value
})
@@ -143,16 +134,9 @@ function changeLangInPath(path, lang) {
return path.replace(langReg, `/${lang}/`)
}
-function changeThemeInPath(path, theme) {
- const themeReg = /(^\/[^/]+\/)([^/]+)/
- return path.replace(themeReg, `$1${theme}`)
-}
-
export function push(partialPath) {
const { fullPath } = route
- router.push(
- fullPath.replace(/(^\/[^/]+\/[^/]+)((\/.*)|$)/, `$1${partialPath}`)
- )
+ router.push(fullPath.replace(/(^\/[^/]+)((\/.*)|$)/, `$1${partialPath}`))
}
export function useDisplayMode() {
@@ -167,6 +151,10 @@ export function useThemeName() {
return themeNameRef
}
+export function useThemeMode() {
+ return themeModeRef
+}
+
export function useDocOptions() {
return docOptionsRef
}
diff --git a/demo/store/menu-options.js b/demo/store/menu-options.js
index 75b706e1276..196c5d986c6 100644
--- a/demo/store/menu-options.js
+++ b/demo/store/menu-options.js
@@ -68,7 +68,7 @@ function appendCounts(item) {
}
}
-function createItems(lang, theme, prefix, items) {
+function createItems(lang, prefix, items) {
const isZh = lang === 'zh-CN'
const langKey = isZh ? 'zh' : 'en'
return items.map((rawItem) => {
@@ -78,19 +78,17 @@ function createItems(lang, theme, prefix, items) {
label: rawItem[langKey] || rawItem.en,
extra: renderItemExtra(rawItem, isZh),
extraString: getItemExtraString(rawItem, isZh),
- path: rawItem.path
- ? `/${lang}/${theme}${prefix}${rawItem.path}`
- : undefined
+ path: rawItem.path ? `/${lang}${prefix}${rawItem.path}` : undefined
}
if (rawItem.children) {
- item.children = createItems(lang, theme, prefix, rawItem.children)
+ item.children = createItems(lang, prefix, rawItem.children)
}
return item
})
}
-export function createDocumentationMenuOptions({ lang, theme }) {
- return createItems(lang, theme, '/docs', [
+export function createDocumentationMenuOptions({ lang }) {
+ return createItems(lang, '/docs', [
{
en: 'Introduction',
zh: '介绍',
@@ -222,8 +220,8 @@ export function createDocumentationMenuOptions({ lang, theme }) {
])
}
-export function createComponentMenuOptions({ lang, theme }) {
- return createItems(lang, theme, '/components', [
+export function createComponentMenuOptions({ lang }) {
+ return createItems(lang, '/components', [
appendCounts({
zh: '通用组件',
en: 'Common Components',
diff --git a/demo/utils/color-mode.js b/demo/utils/color-mode.js
new file mode 100644
index 00000000000..c3b02e1a960
--- /dev/null
+++ b/demo/utils/color-mode.js
@@ -0,0 +1,73 @@
+import { useOsTheme } from 'naive-ui'
+import { computed, nextTick, ref, watchEffect } from 'vue'
+
+const MODES = ['light', 'dark', 'auto']
+
+function readStorage(key) {
+ try {
+ return localStorage.getItem(key)
+ }
+ catch {
+ return null
+ }
+}
+
+function writeStorage(key, value) {
+ try {
+ if (value === null)
+ localStorage.removeItem(key)
+ else localStorage.setItem(key, value)
+ }
+ catch {}
+}
+
+// Turn transitions off while the colors change, so the page switches in one
+// step instead of every component fading on its own
+async function withoutTransitions(change) {
+ const style = document.createElement('style')
+ style.textContent = '*,*::before,*::after{transition:none!important}'
+ document.head.appendChild(style)
+ change()
+ await nextTick()
+ // Force a style recalc before the transitions come back
+ void getComputedStyle(document.body).opacity
+ style.remove()
+}
+
+/**
+ * The reader's color mode, in the manner of VueUse's `useColorMode`.
+ * `mode` is what they picked (`light`, `dark` or `auto`, which follows the
+ * OS); `state` is the theme in effect. Only an explicit pick is stored, it is
+ * kept in step across tabs, and `color-scheme` on follows `state`.
+ * index.html applies the same rule before the first paint.
+ */
+export function useColorMode({ storageKey }) {
+ const read = () => {
+ const value = readStorage(storageKey)
+ return MODES.includes(value) ? value : 'auto'
+ }
+ const storedRef = ref(read())
+ const osThemeRef = useOsTheme()
+
+ window.addEventListener('storage', (e) => {
+ if (e.key === storageKey || e.key === null)
+ withoutTransitions(() => (storedRef.value = read()))
+ })
+
+ const mode = computed({
+ get: () => storedRef.value,
+ set: (value) => {
+ writeStorage(storageKey, value === 'auto' ? null : value)
+ withoutTransitions(() => (storedRef.value = value))
+ }
+ })
+ const state = computed(() =>
+ storedRef.value === 'auto' ? (osThemeRef.value ?? 'light') : storedRef.value
+ )
+
+ watchEffect(() => {
+ document.documentElement.style.colorScheme = state.value
+ })
+
+ return { mode, state }
+}
diff --git a/index.html b/index.html
index 4fbc4546cb7..47c58a21688 100644
--- a/index.html
+++ b/index.html
@@ -21,6 +21,20 @@
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
Naive UI
+
@@ -48,7 +50,7 @@ const srcList = [
size="small"
lazy
:src="src"
- fallback-src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
+ :fallback-src="fallbackSrc"
:render-placeholder="() => null"
:intersection-observer-options="{
root: '#image-scroll-container',
diff --git a/src/avatar/demos/zhCN/lazy.demo.vue b/src/avatar/demos/zhCN/lazy.demo.vue
index 7684f831f71..722c41368ae 100644
--- a/src/avatar/demos/zhCN/lazy.demo.vue
+++ b/src/avatar/demos/zhCN/lazy.demo.vue
@@ -18,6 +18,8 @@ const srcList = [
'https://picsum.photos/id/10/100/100',
'xxx.png'
]
+
+const fallbackSrc = 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg'
@@ -49,7 +51,7 @@ const srcList = [
size="small"
lazy
:src="src"
- fallback-src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
+ :fallback-src="fallbackSrc"
:render-placeholder="() => null"
:intersection-observer-options="{
root: '#image-scroll-container',
diff --git a/src/image/demos/enUS/component-preview-group.demo.vue b/src/image/demos/enUS/component-preview-group.demo.vue
index 581379efc81..2e4fcce8b92 100644
--- a/src/image/demos/enUS/component-preview-group.demo.vue
+++ b/src/image/demos/enUS/component-preview-group.demo.vue
@@ -10,6 +10,20 @@ import { ref } from 'vue'
const currentIndex = ref(0)
+const srcList = [
+ 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
+ 'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg',
+ 'https://picsum.photos/id/1/100/100',
+ 'https://picsum.photos/id/2/100/100',
+ 'https://picsum.photos/id/3/100/100',
+ 'https://picsum.photos/id/4/100/100',
+ 'https://picsum.photos/id/5/100/100',
+ 'https://picsum.photos/id/7/100/100',
+ 'https://picsum.photos/id/8/100/100',
+ 'https://picsum.photos/id/9/100/100',
+ 'https://picsum.photos/id/10/100/100'
+]
+
const showRef = ref(false)
function onClick() {
showRef.value = true
@@ -32,19 +46,7 @@ function handleUpdateCurrent(current: number) {
diff --git a/src/image/demos/zhCN/component-preview-group.demo.vue b/src/image/demos/zhCN/component-preview-group.demo.vue
index 8f1b749f9fb..22f84a8d763 100644
--- a/src/image/demos/zhCN/component-preview-group.demo.vue
+++ b/src/image/demos/zhCN/component-preview-group.demo.vue
@@ -10,6 +10,20 @@ import { ref } from 'vue'
const currentIndex = ref(0)
+const srcList = [
+ 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
+ 'https://gw.alipayobjects.com/zos/antfincdn/aPkFc8Sj7n/method-draw-image.svg',
+ 'https://picsum.photos/id/1/100/100',
+ 'https://picsum.photos/id/2/100/100',
+ 'https://picsum.photos/id/3/100/100',
+ 'https://picsum.photos/id/4/100/100',
+ 'https://picsum.photos/id/5/100/100',
+ 'https://picsum.photos/id/7/100/100',
+ 'https://picsum.photos/id/8/100/100',
+ 'https://picsum.photos/id/9/100/100',
+ 'https://picsum.photos/id/10/100/100'
+]
+
const showRef = ref(false)
function onClick() {
showRef.value = true
@@ -32,19 +46,7 @@ function handleUpdateCurrent(current: number) {
diff --git a/src/menu/demos/enUS/router-link.demo.vue b/src/menu/demos/enUS/router-link.demo.vue
index 181a348ef54..37f80f2fde3 100644
--- a/src/menu/demos/enUS/router-link.demo.vue
+++ b/src/menu/demos/enUS/router-link.demo.vue
@@ -43,7 +43,7 @@ const menuOptions: MenuOption[] = [
RouterLink,
{
to: {
- path: '/zh-CN/os-theme/components/code'
+ path: '/zh-CN/components/code'
}
},
{ default: () => 'Go to work' }
diff --git a/src/menu/demos/zhCN/router-link.demo.vue b/src/menu/demos/zhCN/router-link.demo.vue
index 075d3b1e8fa..fb0081a9b68 100644
--- a/src/menu/demos/zhCN/router-link.demo.vue
+++ b/src/menu/demos/zhCN/router-link.demo.vue
@@ -43,7 +43,7 @@ const menuOptions: MenuOption[] = [
RouterLink,
{
to: {
- path: '/zh-CN/os-theme/components/code'
+ path: '/zh-CN/components/code'
}
},
{ default: () => '上班' }