diff --git a/.vitepress/config/community.ts b/.vitepress/config/community.ts index 1f530fe3a..bad997cc5 100644 --- a/.vitepress/config/community.ts +++ b/.vitepress/config/community.ts @@ -26,7 +26,7 @@ export function community(words: Words, prefix = '', videos: 'youtube' | 'bilibi text: words.connect, items: [ row('Discord', LINKS.discord, 'icon-[simple-icons--discord]', '#5865F2'), - row('X', LINKS.x, 'icon-[simple-icons--x]', '#000000') + row('X', LINKS.x, 'icon-[simple-icons--x]') ] }, { diff --git a/.vitepress/config/en.ts b/.vitepress/config/en.ts index f92d6a907..575fea8b7 100644 --- a/.vitepress/config/en.ts +++ b/.vitepress/config/en.ts @@ -89,6 +89,7 @@ export const config = defineConfig({ next: 'Next page' }, outline: { + level: [2, 3], label: 'On this page' }, lastUpdated: { diff --git a/.vitepress/config/goravel-code.ts b/.vitepress/config/goravel-code.ts index 051585ec7..094ca1763 100644 --- a/.vitepress/config/goravel-code.ts +++ b/.vitepress/config/goravel-code.ts @@ -1,26 +1,34 @@ import type { ThemeOptions } from 'vitepress' -const INK = '#101820' -const GREY = '#68747d' -const CYAN = '#0077b3' -const RED = '#b02b2b' +interface Palette { + ink: string + grey: string + cyan: string + red: string +} + +// These palettes mirror the light and dark values of the theme tokens in +// .vitepress/theme/goravel.css: ink -> --g-ink, grey -> --g-grey, +// cyan -> --g-cyan-text, red -> --g-red-text. Keep them in step. +const LIGHT: Palette = { ink: '#101820', grey: '#636f78', cyan: '#0074ae', red: '#b02b2b' } +const DARK: Palette = { ink: '#e6edf2', grey: '#8a99a5', cyan: '#5cc4f7', red: '#f59393' } const rule = (scope: string[], foreground: string) => ({ scope, settings: { foreground } }) -export const goravelCode: ThemeOptions = { - name: 'goravel', - type: 'light', +const theme = (type: 'light' | 'dark', { ink, grey, cyan, red }: Palette) => ({ + name: `goravel-${type}`, + type, // transparent, so a callout's lighter ground shows through - colors: { 'editor.background': '#00000000', 'editor.foreground': INK }, + colors: { 'editor.background': '#00000000', 'editor.foreground': ink }, tokenColors: [ - { settings: { foreground: INK } }, - rule(['comment', 'punctuation.definition.comment', 'string.comment'], GREY), - rule(['keyword', 'storage', 'constant.language', 'variable.language'], INK), - rule(['entity.name.function', 'support.function', 'entity.name.command'], CYAN), - rule(['string', 'constant.numeric', 'constant.character'], GREY), - rule(['punctuation', 'keyword.operator', 'meta.brace'], GREY), - rule(['markup.inserted', 'meta.diff.header.to-file'], CYAN), - rule(['markup.deleted', 'meta.diff.header.from-file'], RED), + { settings: { foreground: ink } }, + rule(['comment', 'punctuation.definition.comment', 'string.comment'], grey), + rule(['keyword', 'storage', 'constant.language', 'variable.language'], ink), + rule(['entity.name.function', 'support.function', 'entity.name.command'], cyan), + rule(['string', 'constant.numeric', 'constant.character'], grey), + rule(['punctuation', 'keyword.operator', 'meta.brace'], grey), + rule(['markup.inserted', 'meta.diff.header.to-file'], cyan), + rule(['markup.deleted', 'meta.diff.header.from-file'], red), // narrower scopes that override the grey and cyan rules above rule( [ @@ -28,9 +36,11 @@ export const goravelCode: ThemeOptions = { 'support.type', 'support.class', 'storage.type.numeric.go', 'storage.type.string.go', 'storage.type.boolean.go', 'storage.type.byte.go', 'storage.type.error.go', 'variable', 'meta.definition.variable', 'punctuation.definition.variable.php', - 'string.unquoted.argument.shell', 'constant.other.option' + 'string.unquoted.argument.shell', 'constant.other.option', 'entity.name.tag.yaml' ], - INK + ink ) ] -} +}) + +export const goravelCode: ThemeOptions = { light: theme('light', LIGHT), dark: theme('dark', DARK) } diff --git a/.vitepress/config/shared.ts b/.vitepress/config/shared.ts index bf48bcf00..f23b866f4 100644 --- a/.vitepress/config/shared.ts +++ b/.vitepress/config/shared.ts @@ -40,7 +40,9 @@ function liftFileNames(md: MarkdownRenderer) { const fence = md.renderer.rules.fence! md.renderer.rules.fence = (tokens, idx, options, env, self) => { const token = tokens[idx] - const path = token.content.match(/^(?:\/\/|#|--) ?([\w.@-]+(?:\/[\w.@-]+)*\.\w+)\n/) + // a demo holds markdown, not one code block, so its first line is never a file name + if (/^md\s+demo\b/.test(token.info.trim())) return fence(tokens, idx, options, env, self) + const path = token.content.match(/^(?:\/\/|#|--) ?((?:[\w.@-]+\/)*[\w.@-]*\.\w+)\n/) // line highlights like {2,4} count from the first line, so a block using them keeps it if (!path || /\{[\d,-]+\}/.test(token.info)) return fence(tokens, idx, options, env, self) token.content = token.content.slice(path[0].length) @@ -48,6 +50,19 @@ function liftFileNames(md: MarkdownRenderer) { } } +// a fence opened as ````md demo shows its source and then renders it, so an example is written once; +// the rendered headings are kept out of the outline +function renderDemos(md: MarkdownRenderer) { + const fence = md.renderer.rules.fence! + md.renderer.rules.fence = (tokens, idx, options, env, self) => { + const source = fence(tokens, idx, options, env, self) + if (!/^md\s+demo\b/.test(tokens[idx].info.trim())) return source + // a demo teaches syntax, so a link in it is illustrative; keep it out of the page's dead-link check + // (VitePress's link plugin pushes into env.links) + return source + md.render(tokens[idx].content, { ...env, links: [] }).replace(/]*\bclass=)/g, ' self.renderToken(tokens, idx, options)) @@ -66,9 +81,9 @@ export const shared = defineConfig({ rewrites: { 'en/:rest*': ':rest*' }, + srcExclude: ['**/README.md', '**/AGENTS.md'], - // there is no dark design - appearance: false, + appearance: true, lastUpdated: true, cleanUrls: false, metaChunk: true, @@ -117,6 +132,7 @@ export const shared = defineConfig({ md.use(deflist) markCustomContainerTitles(md) liftFileNames(md) + renderDemos(md) drawCoverageBars(md) }, languages: ['go'] diff --git a/.vitepress/config/zh_CN.ts b/.vitepress/config/zh_CN.ts index 5180fe9a1..07150e006 100644 --- a/.vitepress/config/zh_CN.ts +++ b/.vitepress/config/zh_CN.ts @@ -74,6 +74,7 @@ export const config = defineConfig({ next: "下一页" }, outline: { + level: [2, 3], label: "页面导航" }, lastUpdated: { diff --git a/.vitepress/theme/Layout.vue b/.vitepress/theme/Layout.vue index 5ee90bc50..d4be1a10f 100644 --- a/.vitepress/theme/Layout.vue +++ b/.vitepress/theme/Layout.vue @@ -1,7 +1,6 @@ - - - - diff --git a/.vitepress/theme/components/NavBar.vue b/.vitepress/theme/components/NavBar.vue index 69fee9b93..9e103e406 100644 --- a/.vitepress/theme/components/NavBar.vue +++ b/.vitepress/theme/components/NavBar.vue @@ -15,9 +15,9 @@ import SelectMenu from './SelectMenu.vue' defineProps<{ isScreenOpen: boolean }>() defineEmits<{ (e: 'toggle-screen'): void }>() -const { theme, localeIndex } = useData() +const { theme, localeIndex, isDark } = useData() const { hasSidebar } = useSidebar() -const { languages, currentLang } = useI18n() +const { tr, languages, currentLang } = useI18n() const route = useRoute() const home = computed(() => (localeIndex.value === 'root' ? '/' : `/${localeIndex.value}/`)) @@ -67,6 +67,9 @@ const onFocusOut = useDismiss(navRoot, () => (menuOpen.value = false))
+
@@ -178,7 +181,7 @@ const onFocusOut = useDismiss(navRoot, () => (menuOpen.value = false)) padding: 0 var(--g-shell-inset); border-bottom: 1px solid var(--g-line); background: var(--g-white); - box-shadow: 0 24px 40px -24px rgba(16, 24, 32, 0.12); + box-shadow: 0 24px 40px -24px rgba(var(--g-shadow), 0.12); } .group { @@ -300,6 +303,37 @@ const onFocusOut = useDismiss(navRoot, () => (menuOpen.value = false)) margin-left: 20px; } +.theme { + display: grid; + place-items: center; + width: var(--g-control); + height: var(--g-control); + margin: 0 -8px 0 -6px; + border-radius: 2px; + color: var(--g-grey); +} + +.theme:hover { + color: var(--g-ink); +} + +.theme span { + width: 17px; + height: 17px; +} + +.moon { + display: none; +} + +:global(.dark .bar .theme .sun) { + display: none; +} + +:global(.dark .bar .theme .moon) { + display: block; +} + .social { margin-left: 2px; } diff --git a/.vitepress/theme/components/NotFound.vue b/.vitepress/theme/components/NotFound.vue index 651989c67..9770034d4 100644 --- a/.vitepress/theme/components/NotFound.vue +++ b/.vitepress/theme/components/NotFound.vue @@ -67,15 +67,18 @@ const suggestions = [ height: 46px; margin-top: 28px; padding: 0 22px; + border: 1px solid transparent; border-radius: 2px; - background: var(--g-cyan); + background: var(--g-cyan-text); font-size: 15px; font-weight: 600; - color: var(--g-white); + color: var(--g-on-accent); } .home:hover { - background: var(--g-cyan-text); + border-color: var(--g-cyan-text); + background: var(--g-white); + color: var(--g-cyan-text); } .right { diff --git a/.vitepress/theme/components/SelectMenu.vue b/.vitepress/theme/components/SelectMenu.vue index 0ab6adf14..dd8b2409d 100644 --- a/.vitepress/theme/components/SelectMenu.vue +++ b/.vitepress/theme/components/SelectMenu.vue @@ -68,7 +68,7 @@ const onFocusOut = useDismiss(root, () => (open.value = false)) border: 1px solid var(--g-line); border-radius: 2px; background: var(--g-white); - box-shadow: 0 4px 12px rgba(16, 24, 32, 0.06); + box-shadow: 0 4px 12px rgba(var(--g-shadow), 0.06); } .option { diff --git a/.vitepress/theme/goravel.css b/.vitepress/theme/goravel.css index ddc8db6ea..8a6b82e69 100644 --- a/.vitepress/theme/goravel.css +++ b/.vitepress/theme/goravel.css @@ -1,14 +1,24 @@ :root { --g-white: #ffffff; --g-soft: #f7f9fa; + --g-code: #f7f9fa; --g-hover: #eef2f5; --g-ink: #101820; - --g-grey: #68747d; + --g-grey: #636f78; + --g-grey-strong: #5b6770; --g-line: #e5eaed; + --g-control-line: #e5eaed; --g-cyan: #009fe8; - --g-cyan-text: #0077b3; + --g-cyan-text: #0074ae; --g-cyan-tint: #ebf7fd; --g-construct: rgba(104, 116, 125, 0.4); + --g-on-accent: #ffffff; + --g-link-rule: rgba(0, 116, 174, 0.32); + --g-glass: rgba(255, 255, 255, 0.66); + --g-glass-strong: rgba(255, 255, 255, 0.72); + --g-scrim: rgba(16, 24, 32, 0.32); + --g-shadow: 16, 24, 32; + --g-laravel: #ff2d20; --g-amber: #e08700; --g-amber-text: #8f5a00; @@ -48,6 +58,8 @@ --vp-code-block-color: var(--g-ink); --vp-code-line-highlight-color: var(--g-cyan-tint); + --vp-code-line-warning-color: var(--g-amber-tint); + --vp-code-line-error-color: var(--g-red-tint); --vp-code-line-number-color: var(--g-grey); --vp-code-copy-code-hover-bg: var(--g-white); --vp-code-tab-divider: var(--g-line); @@ -59,13 +71,40 @@ --vp-sidebar-width: 272px; } +.dark { + color-scheme: dark; + + --g-white: #0d141b; + --g-soft: #121b24; + --g-code: #0a0f15; + --g-hover: #19242f; + --g-ink: #e6edf2; + --g-grey: #8a99a5; + --g-grey-strong: #93a2ad; + --g-line: #222f3b; + --g-control-line: #46576a; + --g-cyan-text: #5cc4f7; + --g-cyan-tint: #0f2b3d; + --g-construct: rgba(138, 153, 165, 0.34); + --g-on-accent: #0b1218; + --g-link-rule: rgba(92, 196, 247, 0.4); + --g-glass: rgba(13, 20, 27, 0.5); + --g-glass-strong: rgba(13, 20, 27, 0.6); + --g-scrim: rgba(0, 0, 0, 0.6); + --g-shadow: 0, 0, 0; + + --g-amber-text: #f2b765; + --g-amber-tint: #2a2113; + --g-red-text: #f59393; + --g-red-tint: #2e181a; +} + /* ------------------------------------------------------------------ labels and marks */ .g-label, .vp-doc .custom-block .custom-block-title, .vp-doc th, .vp-doc .footnotes::before, .VPDocAsideOutline .outline-title, -.goravel-methods .outline-title, .VPNavScreenMenuGroupSection .title { font-family: var(--vp-font-family-mono); font-size: 12px; @@ -85,7 +124,6 @@ .g-diamond::before, .vp-doc .custom-block .custom-block-title::before, .VPDocAsideOutline .outline-title::before, -.goravel-methods .outline-title::before, .VPNavScreenMenuGroupSection .title::before, .VPSidebarItem.level-1.is-active > .item::before, .VPDocOutlineItem .outline-link.active::before { @@ -200,7 +238,7 @@ .vp-doc a { font-weight: 400; - text-decoration: underline 1px rgba(0, 119, 179, 0.32); + text-decoration: underline 1px var(--g-link-rule); text-underline-offset: 3px; } @@ -211,7 +249,7 @@ .vp-doc :not(pre) > code { padding: 2px 6px; border-radius: 0; - background: var(--g-soft); + background: var(--g-code); font-size: 14px; color: var(--g-ink); } @@ -246,7 +284,7 @@ /* ------------------------------------------------------------------ code */ /* The ground and rules are painted past the box, so the text keeps the article's edge. */ .vp-doc { - --g-code-ground: var(--g-soft); + --g-code-ground: var(--g-code); --g-code-left: var(--g-left); --g-code-right: var(--g-right); } @@ -521,7 +559,7 @@ } .vp-doc .custom-block { - --g-code-ground: rgba(255, 255, 255, 0.66); + --g-code-ground: var(--g-glass); padding-top: 16px; padding-bottom: 18px; border-radius: 0; @@ -600,17 +638,31 @@ } .vp-doc .custom-block code { - background: rgba(255, 255, 255, 0.72); + background: var(--g-glass-strong); } .vp-doc .custom-block.info code { - background: var(--g-soft); + background: var(--g-code); } .vp-doc .custom-block div[class*='language-'], .vp-doc .custom-block .vp-code-group { margin-top: 14px; margin-bottom: 14px; + border-radius: 0; +} + +.vp-doc .custom-block .vp-code-group .tabs { + border-radius: 0; +} + +.vp-doc .custom-block:has(> div[class*='language-']:last-child, > .vp-code-group:last-child) { + padding-bottom: 0; +} + +.vp-doc .custom-block > div[class*='language-']:last-child, +.vp-doc .custom-block > .vp-code-group:last-child { + margin-bottom: 0; } .vp-doc .custom-block.details { @@ -730,6 +782,7 @@ } .vp-doc th { + overflow-wrap: normal; padding: 0 24px 10px 0; background: transparent; text-align: left; @@ -759,7 +812,8 @@ overflow-wrap: anywhere; } -.vp-doc td:first-child > a { +.vp-doc td:first-child > a, +.vp-doc td:first-child > code { overflow-wrap: normal; } @@ -861,7 +915,7 @@ width: 15px; height: 15px; margin: 7px 0 0; - border: 1.5px solid var(--g-line); + border: 1.5px solid var(--g-control-line); border-radius: 2px; background: var(--g-white) center / 11px no-repeat; } @@ -977,3 +1031,45 @@ :lang(zh) .g-home .g-h2 { line-height: 1.25; } + +/* ------------------------------------------------------------------ keys and badges */ +.vp-doc kbd { + padding: 1px 6px; + border: 1px solid var(--g-control-line); + border-bottom-width: 2px; + border-radius: 2px; + background: var(--g-code); + font-family: var(--vp-font-family-mono); + font-size: 13px; +} + +.vp-doc .VPBadge { + padding: 0 6px; + border: 0; + border-radius: 2px; + font-family: var(--vp-font-family-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.vp-doc .VPBadge.info { + background: var(--g-hover); + color: var(--g-ink); +} + +.vp-doc .VPBadge.tip { + background: var(--g-cyan-tint); + color: var(--g-cyan-text); +} + +.vp-doc .VPBadge.warning { + background: var(--g-amber-tint); + color: var(--g-amber-text); +} + +.vp-doc .VPBadge.danger { + background: var(--g-red-tint); + color: var(--g-red-text); +} diff --git a/.vitepress/theme/home/GoravelMark.vue b/.vitepress/theme/home/GoravelMark.vue index 8b6dddfa7..bd691ff98 100644 --- a/.vitepress/theme/home/GoravelMark.vue +++ b/.vitepress/theme/home/GoravelMark.vue @@ -1,5 +1,6 @@ diff --git a/.vitepress/theme/home/HomeFacades.vue b/.vitepress/theme/home/HomeFacades.vue index 3ff832b13..e35d443aa 100644 --- a/.vitepress/theme/home/HomeFacades.vue +++ b/.vitepress/theme/home/HomeFacades.vue @@ -56,11 +56,12 @@ const names = Object.keys(FACADES).sort((a, b) => a.localeCompare(b, 'en', { sen border-bottom: 1px solid var(--g-line); } -.home-cell::after { +.home-cell::before { content: ''; + display: none; position: absolute; - right: -3px; - bottom: -3px; + left: -3px; + top: -3px; z-index: 1; width: 5px; height: 5px; @@ -83,7 +84,7 @@ const names = Object.keys(FACADES).sort((a, b) => a.localeCompare(b, 'en', { sen letter-spacing: 0.08em; text-transform: uppercase; vertical-align: middle; - color: var(--g-white); + color: var(--g-on-accent); background: var(--g-cyan-text); } @@ -107,7 +108,7 @@ const names = Object.keys(FACADES).sort((a, b) => a.localeCompare(b, 'en', { sen margin-top: auto; padding: 14px 0; border: 1px solid var(--g-line); - background: var(--g-soft); + background: var(--g-code); font-size: 12.5px; line-height: 24px; scrollbar-width: thin; @@ -167,6 +168,19 @@ const names = Object.keys(FACADES).sort((a, b) => a.localeCompare(b, 'en', { sen color: var(--g-ink); } +@media (min-width: 1400px) { + .home-cell:nth-child(3n + 3)::before, + .home-cell:nth-child(3n + 4)::before { + display: block; + } +} + +@media (min-width: 901px) and (max-width: 1399px) { + .home-cell:nth-child(2n + 3)::before { + display: block; + } +} + @media (max-width: 1399px) { .home-cell .home-code { font-size: 13.5px; diff --git a/.vitepress/theme/home/HomeLaravel.vue b/.vitepress/theme/home/HomeLaravel.vue index 8bbb013cc..1c2ae2fe5 100644 --- a/.vitepress/theme/home/HomeLaravel.vue +++ b/.vitepress/theme/home/HomeLaravel.vue @@ -1,6 +1,6 @@