+}
diff --git a/themes/theme-next/src/client/styles/base.css b/themes/theme-next/src/client/styles/base.css
new file mode 100644
index 0000000000..f0e232aaec
--- /dev/null
+++ b/themes/theme-next/src/client/styles/base.css
@@ -0,0 +1,273 @@
+@media (prefers-reduced-motion: reduce) {
+ *,
+ ::before,
+ ::after {
+ background-attachment: initial !important;
+
+ scroll-behavior: auto !important;
+
+ transition-delay: 0s !important;
+ transition-duration: 0s !important;
+
+ animation-duration: 1ms !important;
+ animation-delay: -1ms !important;
+ animation-iteration-count: 1 !important;
+ }
+}
+
+*,
+::before,
+::after {
+ box-sizing: border-box;
+}
+
+html {
+ font-size: 16px;
+ line-height: 1.4;
+ scroll-padding-top: 130px;
+ text-size-adjust: 100%;
+}
+
+@media (min-width: 960px) {
+ html {
+ scroll-padding-top: 150px;
+ }
+}
+
+@media (min-width: 1280px) {
+ html {
+ scroll-padding-top: 104px;
+ }
+}
+
+html[data-theme='dark'] {
+ color-scheme: dark;
+}
+
+body {
+ width: 100%;
+ min-width: 320px;
+ min-height: 100vh;
+ margin: 0;
+
+ background-color: var(--vp-c-bg);
+ color: var(--vp-c-text);
+
+ font-weight: 400;
+ font-size: 16px;
+ font-family: var(--vp-font-family-base);
+ font-synthesis: style;
+ line-height: 24px;
+
+ text-rendering: optimizelegibility;
+
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+main {
+ display: block;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ margin: 0;
+ font-weight: 400;
+ font-size: 16px;
+ line-height: 24px;
+}
+
+p {
+ margin: 0;
+}
+
+strong,
+b {
+ font-weight: 600;
+}
+
+/**
+ * Avoid 300ms click delay on touch devices that support the `touch-action`
+ * CSS property.
+ *
+ * In particular, unlike most other browsers, IE11+Edge on Windows 10 on
+ * touch devices and IE Mobile 10-11 DON'T remove the click delay when
+ * `` is present.
+ * However, they DO support removing the click delay via
+ * `touch-action: manipulation`.
+ *
+ * See:
+ * - http://v4-alpha.getbootstrap.com/content/reboot/#click-delay-optimization-for-touch
+ * - http://caniuse.com/#feat=css-touch-action
+ * - http://patrickhlauke.github.io/touch/tests/results/#suppressing-300ms-delay
+ */
+a,
+area,
+button,
+[role='button'],
+input,
+label,
+select,
+summary,
+textarea {
+ touch-action: manipulation;
+}
+
+a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+ol,
+ul {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+blockquote {
+ margin: 0;
+}
+
+pre,
+code,
+kbd,
+samp {
+ font-family: var(--vp-font-family-mono);
+}
+
+img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+ display: block;
+}
+
+figure {
+ margin: 0;
+}
+
+img,
+video {
+ max-width: 100%;
+ height: auto;
+}
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ padding: 0;
+ border: 0;
+ color: inherit;
+ line-height: inherit;
+}
+
+button {
+ padding: 0;
+ background-color: transparent;
+ background-image: none;
+ font-family: inherit;
+}
+
+button:enabled,
+[role='button']:enabled {
+ cursor: pointer;
+}
+
+button:focus,
+button:focus-visible {
+ outline: 1px dotted;
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+button:focus:not(:focus-visible) {
+ outline: none !important;
+}
+
+input:focus,
+textarea:focus,
+select:focus {
+ outline: none;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+input {
+ background-color: transparent;
+}
+
+input:input-placeholder,
+textarea:input-placeholder {
+ color: var(--vp-c-text-subtle);
+}
+
+input::input-placeholder,
+textarea::input-placeholder {
+ color: var(--vp-c-text-subtle);
+}
+
+input::placeholder,
+textarea::placeholder {
+ color: var(--vp-c-text-subtle);
+}
+
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+ margin: 0;
+ appearance: none;
+}
+
+input[type='number'] {
+ appearance: textfield;
+}
+
+textarea {
+ resize: vertical;
+}
+
+select {
+ appearance: none;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+li,
+p {
+ overflow-wrap: break-word;
+}
+
+vite-error-overlay {
+ z-index: 9999;
+}
+
+mjx-container {
+ display: inline-block;
+ vertical-align: middle;
+ margin: auto 2px;
+}
+
+mjx-container > svg {
+ display: inline-block;
+ margin: auto;
+}
diff --git a/themes/theme-next/src/client/styles/compat.css b/themes/theme-next/src/client/styles/compat.css
new file mode 100644
index 0000000000..444ca84690
--- /dev/null
+++ b/themes/theme-next/src/client/styles/compat.css
@@ -0,0 +1,281 @@
+/*
+ ** The style here is designed to accommodate the differences in plugins between new and old themes.
+ */
+
+/**
+ * plugin-docsearch
+ * ----------------------------------------------------------------- */
+.DocSearch {
+ --docsearch-primary-color: var(--vp-c-accent);
+ --docsearch-highlight-color: var(--docsearch-primary-color);
+ --docsearch-text-color: var(--vp-c-text);
+ --docsearch-muted-color: var(--vp-c-text-mute);
+ --docsearch-searchbox-shadow: none;
+ --docsearch-searchbox-background: var(--vp-c-default-soft);
+ --docsearch-searchbox-focus-background: var(--vp-c-default-3);
+ --docsearch-key-gradient: transparent;
+ --docsearch-key-shadow: none;
+ --docsearch-modal-background: var(--vp-c-bg-soft);
+ --docsearch-footer-background: var(--vp-c-bg);
+}
+
+[data-theme='dark'] .DocSearch {
+ --docsearch-modal-shadow: none;
+ --docsearch-footer-shadow: none;
+ --docsearch-logo-color: var(--vp-c-text-mute);
+ --docsearch-hit-background: var(--vp-c-default-soft);
+ --docsearch-hit-color: var(--vp-c-text-mute);
+ --docsearch-hit-shadow: none;
+}
+
+.vp-navbar-search .DocSearch-Button {
+ display: flex;
+ align-items: center;
+ justify-content: center !important;
+
+ width: 32px;
+ height: 32px;
+ margin: 0;
+ padding: 0;
+
+ background: var(--docsearch-searchbox-background);
+
+ transition:
+ border-color 0.5s ease,
+ background 0.5s ease;
+}
+
+.vp-navbar-search .DocSearch-Button:hover {
+ background: var(--docsearch-searchbox-focus-background);
+}
+
+.vp-navbar-search .DocSearch-Button:focus {
+ outline: 1px dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+}
+
+.vp-navbar-search .DocSearch-Button:focus:not(:focus-visible) {
+ outline: none !important;
+}
+
+.vp-navbar-search #docsearch-container {
+ min-width: 32px;
+}
+
+.DocSearch-Button .DocSearch-Button-Container {
+ display: flex;
+ align-items: center;
+}
+
+.DocSearch-Button .DocSearch-Search-Icon {
+ position: relative;
+
+ width: 16px;
+ height: 16px;
+
+ color: var(--vp-c-text);
+
+ transition: color 0.5s ease;
+
+ fill: currentcolor;
+}
+
+.DocSearch-Button:hover .DocSearch-Search-Icon {
+ color: var(--vp-c-text);
+}
+
+.DocSearch-Button .DocSearch-Button-Placeholder {
+ display: none;
+
+ margin-top: 2px;
+ padding: 0 16px 0 0;
+
+ color: var(--vp-c-text-mute);
+
+ font-weight: 500;
+ font-size: 13px;
+
+ transition: color 0.5s ease;
+}
+
+.DocSearch-Button:hover .DocSearch-Button-Placeholder {
+ color: var(--vp-c-text);
+}
+
+.DocSearch-Button .DocSearch-Button-Keys {
+ display: none;
+ min-width: auto;
+
+ /* rtl:ignore */
+ direction: ltr;
+}
+
+.DocSearch-Button .DocSearch-Button-Key {
+ display: block;
+
+ width: auto;
+
+ /* rtl:end:ignore */
+ min-width: 0;
+ height: 22px;
+ margin: 2px 0 0;
+ padding-left: 6px;
+ border: 1px solid var(--vp-c-divider);
+
+ /* rtl:begin:ignore */
+ border-right: none;
+ border-radius: 4px 0 0 4px;
+
+ font-weight: 500;
+ font-size: 12px;
+ font-family: var(--vp-font-family-base);
+ line-height: 22px;
+
+ transition:
+ color 0.5s ease,
+ border-color 0.5s ease;
+}
+
+.DocSearch-Button .DocSearch-Button-Key + .DocSearch-Button-Key {
+ padding-right: 6px;
+ padding-left: 2px;
+
+ /* rtl:begin:ignore */
+ border-right: 1px solid var(--vp-c-divider);
+ border-left: none;
+ border-radius: 0 4px 4px 0;
+
+ /* rtl:end:ignore */
+}
+
+.DocSearch-Button .DocSearch-Button-Key:first-child {
+ color: transparent;
+ font-size: 1px;
+ letter-spacing: -12px;
+}
+
+.DocSearch-Button .DocSearch-Button-Key:first-child::after {
+ content: 'Ctrl';
+ color: var(--docsearch-muted-color);
+ font-size: 12px;
+ letter-spacing: normal;
+}
+
+.mac .DocSearch-Button .DocSearch-Button-Key:first-child::after {
+ content: '\2318';
+}
+
+.DocSearch-Button .DocSearch-Button-Key:first-child > * {
+ display: none;
+}
+
+[data-theme='dark'] .DocSearch-Footer {
+ border-top: 1px solid var(--vp-c-divider);
+}
+
+.DocSearch-Form {
+ border: 1px solid var(--vp-c-accent);
+ background-color: var(--vp-c-white);
+}
+
+[data-theme='dark'] .DocSearch-Form {
+ background-color: var(--vp-c-bg-soft);
+}
+
+@media (min-width: 768px) {
+ .vp-navbar-search .DocSearch-Button {
+ justify-content: flex-start;
+
+ width: 100%;
+ height: 40px;
+ padding: 0 10px 0 12px;
+ border: 1px solid transparent;
+ border-radius: 8px;
+
+ background-color: var(--vp-c-bg-alt);
+ }
+
+ .vp-navbar-search .DocSearch-Button:hover {
+ border-color: var(--vp-c-accent);
+ background: var(--docsearch-searchbox-focus-background);
+ }
+}
+
+@media (min-width: 768px) {
+ .DocSearch-Button .DocSearch-Search-Icon {
+ top: 1px;
+
+ width: 14px;
+ height: 14px;
+ margin-right: 8px;
+
+ color: var(--vp-c-text-mute);
+ }
+}
+
+@media (min-width: 768px) {
+ .DocSearch-Button .DocSearch-Button-Placeholder {
+ display: inline-block;
+ }
+}
+
+@media (min-width: 768px) {
+ .DocSearch-Button .DocSearch-Button-Keys {
+ display: flex;
+ align-items: center;
+ }
+}
+
+/**
+ * plugin-search
+ * ----------------------------------------------------------------- */
+
+.vp-navbar-search {
+ --search-bg-color: var(--vp-c-bg);
+ --search-accent-color: var(--vp-c-accent);
+ --search-text-color: var(--vp-c-text);
+ --search-border-color: var(--vp-c-divider);
+ --search-item-text-color: var(--vp-c-text-mute);
+ --search-item-focus-bg-color: var(--vp-c-bg-soft);
+}
+
+.vp-navbar-search .search-box input {
+ padding: 0 0.3rem 0 1.655rem;
+ background-position: 0.5rem 0.4rem;
+}
+
+.vp-navbar-search .search-box .suggestions {
+ top: 40px;
+ right: -16px;
+ z-index: 10;
+
+ padding: 16px 12px;
+ border-radius: 12px;
+
+ background-color: var(--vp-c-bg);
+ box-shadow: var(--vp-shadow-3);
+}
+
+@media (min-width: 768px) {
+ .vp-navbar-search .search-box .suggestions {
+ right: unset;
+ left: -16px;
+ }
+}
+
+/**
+ * plugin-markdown-tab
+ * ----------------------------------------------------------------- */
+.vp-tabs .vp-code-tabs .vp-code-tabs-nav,
+.vp-tabs div[class*='language-'] {
+ margin-right: 0;
+ margin-left: 0;
+}
+
+.vp-tabs .vp-tab > *:nth-child(2) {
+ margin-top: 0;
+}
+
+.vp-tabs .vp-tab > *:last-child {
+ margin-bottom: 0;
+}
diff --git a/themes/theme-next/src/client/styles/components/custom-block.css b/themes/theme-next/src/client/styles/components/custom-block.css
new file mode 100644
index 0000000000..3c97445019
--- /dev/null
+++ b/themes/theme-next/src/client/styles/components/custom-block.css
@@ -0,0 +1,240 @@
+.hint-container {
+ padding: 16px 16px 8px;
+ border: 1px solid transparent;
+ border-radius: 8px;
+
+ color: var(--vp-c-text-mute);
+
+ font-size: var(--vp-custom-block-font-size);
+ line-height: 24px;
+}
+
+.hint-container.info {
+ border-color: var(--vp-custom-block-info-border);
+ background-color: var(--vp-custom-block-info-bg);
+ color: var(--vp-custom-block-info-text);
+}
+
+.hint-container.info .hint-container-title::before {
+ color: var(--vp-custom-block-info-icon-color);
+}
+
+.hint-container.info a,
+.hint-container.info code {
+ color: var(--vp-c-accent);
+}
+
+.hint-container.info a:hover,
+.hint-container.info a:hover > code {
+ color: var(--vp-c-accent-hover);
+}
+
+.hint-container.info code {
+ background-color: var(--vp-custom-block-info-code-bg);
+}
+
+.hint-container.note {
+ border-color: var(--vp-custom-block-note-border);
+ background-color: var(--vp-custom-block-note-bg);
+ color: var(--vp-custom-block-note-text);
+}
+
+.hint-container.note .hint-container-title::before {
+ color: var(--vp-custom-block-note-icon-color);
+}
+
+.hint-container.note a,
+.hint-container.note code {
+ color: var(--vp-c-accent);
+}
+
+.hint-container.note a:hover,
+.hint-container.note a:hover > code {
+ color: var(--vp-c-accent-hover);
+}
+
+.hint-container.note code {
+ background-color: var(--vp-custom-block-note-code-bg);
+}
+
+.hint-container.tip {
+ border-color: var(--vp-custom-block-tip-border);
+ background-color: var(--vp-custom-block-tip-bg);
+ color: var(--vp-custom-block-tip-text);
+}
+
+.hint-container.tip .hint-container-title::before {
+ color: var(--vp-custom-block-tip-icon-color);
+}
+
+.hint-container.tip a,
+.hint-container.tip code {
+ color: var(--vp-c-tip-1);
+}
+
+.hint-container.tip a:hover,
+.hint-container.tip a:hover > code {
+ color: var(--vp-c-tip-2);
+}
+
+.hint-container.tip code {
+ background-color: var(--vp-custom-block-tip-code-bg);
+}
+
+.hint-container.important {
+ border-color: var(--vp-custom-block-important-border);
+ background-color: var(--vp-custom-block-important-bg);
+ color: var(--vp-custom-block-important-text);
+}
+
+.hint-container.important .hint-container-title::before {
+ color: var(--vp-custom-block-important-icon-color);
+}
+
+.hint-container.important a,
+.hint-container.important code {
+ color: var(--vp-c-important-1);
+}
+
+.hint-container.important a:hover,
+.hint-container.important a:hover > code {
+ color: var(--vp-c-important-2);
+}
+
+.hint-container.important code {
+ background-color: var(--vp-custom-block-important-code-bg);
+}
+
+.hint-container.warning {
+ border-color: var(--vp-custom-block-warning-border);
+ background-color: var(--vp-custom-block-warning-bg);
+ color: var(--vp-custom-block-warning-text);
+}
+
+.hint-container.warning .hint-container-title::before {
+ color: var(--vp-custom-block-warning-icon-color);
+}
+
+.hint-container.warning a,
+.hint-container.warning code {
+ color: var(--vp-c-warning-1);
+}
+
+.hint-container.warning a:hover,
+.hint-container.warning a:hover > code {
+ color: var(--vp-c-warning-2);
+}
+
+.hint-container.warning code {
+ background-color: var(--vp-custom-block-warning-code-bg);
+}
+
+.hint-container.danger {
+ border-color: var(--vp-custom-block-danger-border);
+ background-color: var(--vp-custom-block-danger-bg);
+ color: var(--vp-custom-block-danger-text);
+}
+
+.hint-container.danger .hint-container-title::before {
+ color: var(--vp-custom-block-danger-icon-color);
+}
+
+.hint-container.danger a,
+.hint-container.danger code {
+ color: var(--vp-c-danger-1);
+}
+
+.hint-container.danger a:hover,
+.hint-container.danger a:hover > code {
+ color: var(--vp-c-danger-2);
+}
+
+.hint-container.danger code {
+ background-color: var(--vp-custom-block-danger-code-bg);
+}
+
+.hint-container.caution {
+ border-color: var(--vp-custom-block-caution-border);
+ background-color: var(--vp-custom-block-caution-bg);
+ color: var(--vp-custom-block-caution-text);
+}
+
+.hint-container.caution .hint-container-title::before {
+ color: var(--vp-custom-block-caution-icon-color);
+}
+
+.hint-container.caution a,
+.hint-container.caution code {
+ color: var(--vp-c-caution-1);
+}
+
+.hint-container.caution a:hover,
+.hint-container.caution a:hover > code {
+ color: var(--vp-c-caution-2);
+}
+
+.hint-container.caution code {
+ background-color: var(--vp-custom-block-caution-code-bg);
+}
+
+.hint-container.details {
+ border-color: var(--vp-custom-block-details-border);
+ background-color: var(--vp-custom-block-details-bg);
+ color: var(--vp-custom-block-details-text);
+}
+
+.hint-container.details a {
+ color: var(--vp-c-accent);
+}
+
+.hint-container.details a:hover,
+.hint-container.details a:hover > code {
+ color: var(--vp-c-accent-hover);
+}
+
+.hint-container.details code {
+ background-color: var(--vp-custom-block-details-code-bg);
+}
+
+.hint-container p + p {
+ margin: 8px 0;
+}
+
+.hint-container.details summary {
+ font-weight: 700;
+ cursor: pointer;
+ user-select: none;
+}
+
+.hint-container.details summary + p {
+ margin: 8px 0;
+}
+
+.hint-container a {
+ color: inherit;
+
+ font-weight: 600;
+ text-decoration: underline;
+
+ transition: opacity 0.25s;
+
+ text-underline-offset: 2px;
+}
+
+.hint-container a:hover {
+ opacity: 0.75;
+}
+
+.hint-container code {
+ font-size: var(--vp-custom-block-code-font-size);
+}
+
+.hint-container th,
+.hint-container blockquote > p {
+ color: inherit;
+ font-size: var(--vp-custom-block-font-size);
+}
+
+.hint-container > .hint-container-title {
+ font-weight: 600;
+}
diff --git a/themes/theme-next/src/client/styles/components/vp-code-tabs.css b/themes/theme-next/src/client/styles/components/vp-code-tabs.css
new file mode 100644
index 0000000000..0eb0e50dac
--- /dev/null
+++ b/themes/theme-next/src/client/styles/components/vp-code-tabs.css
@@ -0,0 +1,92 @@
+.vp-code-tabs {
+ margin-top: 16px;
+}
+
+.vp-code-tabs .vp-code-tabs-nav {
+ position: relative;
+
+ display: flex;
+
+ overflow: auto hidden;
+
+ margin-right: -24px;
+ margin-bottom: -16px;
+ margin-left: -24px;
+ padding: 0 12px;
+
+ background-color: var(--vp-code-tab-bg);
+ box-shadow: inset 0 -1px var(--vp-code-tab-divider);
+}
+
+@media (min-width: 640px) {
+ .vp-code-tabs .vp-code-tabs-nav {
+ margin-right: 0;
+ margin-left: 0;
+ border-radius: 8px 8px 0 0;
+ }
+}
+
+.vp-code-tabs .vp-code-tabs-nav .vp-code-tab-nav {
+ position: relative;
+
+ display: inline-block;
+
+ padding: 0 12px;
+ border-bottom: 1px solid transparent;
+
+ background: none;
+ color: var(--vp-code-tab-text-color);
+
+ font-weight: 500;
+ font-size: 14px;
+ line-height: 48px;
+ white-space: nowrap;
+
+ cursor: pointer;
+
+ transition: color 0.25s;
+}
+
+.vp-code-tabs .vp-code-tabs-nav .vp-code-tab-nav::after {
+ content: '';
+
+ position: absolute;
+ right: 8px;
+ bottom: -1px;
+ left: 8px;
+ z-index: 1;
+
+ width: unset;
+ height: 2px;
+ border-radius: 2px;
+
+ background: none;
+ background-color: transparent;
+
+ transition: background-color 0.25s;
+}
+
+.vp-code-tabs .vp-code-tab-nav:hover {
+ color: var(--vp-code-tab-hover-text-color);
+}
+
+.vp-code-tabs .vp-code-tab-nav.active {
+ color: var(--vp-code-tab-active-text-color);
+}
+
+.vp-code-tabs .vp-code-tab-nav.active::after {
+ background-color: var(--vp-code-tab-active-bar-color);
+}
+
+.vp-code-tabs .vp-code-tab-nav::before {
+ display: none;
+}
+
+.vp-block {
+ padding: 20px 24px;
+}
+
+.vp-doc .vp-code-tabs [class*='language-'] {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
diff --git a/themes/theme-next/src/client/styles/components/vp-code.css b/themes/theme-next/src/client/styles/components/vp-code.css
new file mode 100644
index 0000000000..86c77a7981
--- /dev/null
+++ b/themes/theme-next/src/client/styles/components/vp-code.css
@@ -0,0 +1,355 @@
+/* stylelint-disable selector-max-compound-selectors */
+/* stylelint-disable rule-empty-line-before */
+
+[data-theme='dark'] .vp-code span {
+ color: var(--shiki-dark, inherit);
+}
+
+html:not([data-theme='dark']) .vp-code span {
+ color: var(--shiki-light, inherit);
+}
+
+/**
+ * Code
+ * -------------------------------------------------------------------------- */
+
+/* inline code */
+.vp-doc :not(pre, h1, h2, h3, h4, h5, h6) > code {
+ color: var(--vp-code-color);
+ font-size: var(--vp-code-font-size);
+}
+
+.vp-doc :not(pre) > code {
+ padding: 3px 6px;
+ border-radius: 4px;
+ background-color: var(--vp-code-bg);
+ transition:
+ color 0.25s,
+ background-color 0.5s;
+}
+
+.vp-doc a > code {
+ color: var(--vp-code-link-color);
+}
+
+.vp-doc a:hover > code {
+ color: var(--vp-code-link-hover-color);
+}
+
+.vp-doc h1 > code,
+.vp-doc h2 > code,
+.vp-doc h3 > code {
+ font-size: 0.9em;
+}
+
+.vp-doc div[class*='language-'],
+.vp-block {
+ position: relative;
+
+ overflow-x: auto;
+
+ margin: 16px -24px;
+ border-radius: 0;
+
+ background-color: var(--vp-code-block-bg);
+
+ transition: background-color 0.5s;
+}
+
+@media (min-width: 640px) {
+ .vp-doc div[class*='language-'],
+ .vp-block {
+ margin: 16px 0;
+ border-radius: 8px;
+ }
+}
+
+@media (max-width: 639px) {
+ .vp-doc li div[class*='language-'] {
+ border-radius: 8px 0 0 8px;
+ }
+}
+
+.vp-doc div[class*='language-'] + div[class*='language-'],
+.vp-doc div[class$='-api'] + div[class*='language-'],
+.vp-doc div[class*='language-'] + div[class$='-api'] > div[class*='language-'] {
+ margin-top: -8px;
+}
+
+.vp-doc [class*='language-'] pre,
+.vp-doc [class*='language-'] code {
+ background-color: transparent !important;
+
+ /* rtl:ignore */
+ text-align: left;
+
+ /* rtl:ignore */
+ direction: ltr;
+ white-space: pre;
+ word-spacing: normal;
+ word-wrap: normal;
+ word-break: normal;
+ tab-size: 4;
+ hyphens: none;
+}
+
+.vp-doc [class*='language-'] pre {
+ position: relative;
+ z-index: 1;
+
+ overflow-x: auto;
+
+ margin: 0;
+ padding: 20px 0;
+ border-radius: 0;
+
+ background: transparent;
+
+ font-size: inherit;
+ font-family: inherit;
+ line-height: inherit;
+}
+
+.vp-doc div[class*='language-'].line-numbers-mode pre {
+ margin-left: 0;
+}
+
+.vp-doc [class*='language-'] code {
+ display: block;
+
+ width: fit-content;
+ min-width: 100%;
+ padding: 0 24px !important;
+
+ color: var(--vp-code-block-color);
+
+ font-size: var(--vp-code-font-size);
+ line-height: var(--vp-code-line-height);
+
+ transition: color 0.5s;
+}
+
+.vp-doc [class*='language-'] code .line.highlighted {
+ display: inline-block;
+
+ width: calc(100% + 2 * 24px);
+ margin: 0 -24px;
+ padding: 0 24px;
+
+ background-color: var(--vp-code-line-highlight-color);
+
+ transition: background-color 0.5s;
+}
+
+.vp-doc [class*='language-'] code .line.highlighted.error {
+ background-color: var(--vp-code-line-error-color);
+}
+
+.vp-doc [class*='language-'] code .line.highlighted.warning {
+ background-color: var(--vp-code-line-warning-color);
+}
+
+.vp-doc [class*='language-'] code .line.diff {
+ display: inline-block;
+
+ width: calc(100% + 2 * 24px);
+ margin: 0 -24px;
+ padding: 0 24px;
+
+ transition: background-color 0.5s;
+}
+
+.vp-doc [class*='language-'] code .line.diff::before {
+ position: absolute;
+ left: 10px;
+}
+
+.vp-doc [class*='language-'] .has-focused-lines .line:not(.has-focus) {
+ opacity: 0.7;
+ filter: blur(0.095rem);
+ transition:
+ filter 0.35s,
+ opacity 0.35s;
+}
+
+.vp-doc [class*='language-']:hover .has-focused-lines .line:not(.has-focus) {
+ opacity: 1;
+ filter: blur(0);
+}
+
+.vp-doc [class*='language-'] code .line.diff.remove {
+ background-color: var(--vp-code-line-diff-remove-color);
+ opacity: 0.7;
+}
+
+.vp-doc [class*='language-'] code .line.diff.remove::before {
+ content: '-';
+ color: var(--vp-code-line-diff-remove-symbol-color);
+}
+
+.vp-doc [class*='language-'] code .line.diff.add {
+ background-color: var(--vp-code-line-diff-add-color);
+}
+
+.vp-doc [class*='language-'] code .line.diff.add::before {
+ content: '+';
+ color: var(--vp-code-line-diff-add-symbol-color);
+}
+
+.vp-doc div[class*='language-'].line-numbers-mode {
+ /* rtl:ignore */
+ padding-left: 32px;
+}
+
+.vp-doc div[class*='language-'].line-numbers-mode .line-numbers {
+ counter-reset: line-number;
+
+ position: absolute;
+ top: 0;
+ bottom: 0;
+
+ /* rtl:ignore */
+ left: 0;
+ z-index: 3;
+
+ width: 32px;
+ padding-top: 20px;
+
+ /* rtl:ignore */
+ border-right: 1px solid var(--vp-code-block-divider-color);
+
+ color: var(--vp-code-line-number-color);
+
+ font-size: var(--vp-code-font-size);
+ font-family: var(--vp-font-family-mono);
+ line-height: var(--vp-code-line-height);
+ text-align: center;
+
+ transition:
+ border-color 0.5s,
+ color 0.5s;
+}
+
+.vp-doc div[class*='language-'].line-numbers-mode .line-numbers .line-number {
+ position: relative;
+ z-index: 3;
+ font-family: var(--vp-font-family-mono);
+ user-select: none;
+}
+
+.vp-doc
+ div[class*='language-'].line-numbers-mode
+ .line-numbers
+ .line-number::before {
+ content: counter(line-number);
+ counter-increment: line-number;
+}
+
+.vp-doc div[class*='language-'].line-numbers-mode::after {
+ content: none;
+}
+
+.vp-doc div[class*='language-']:not(.line-numbers-mode) .line-numbers {
+ display: none;
+}
+
+.vp-doc div[class*='language'] > .vp-copy-code-button {
+ top: 12px;
+
+ /* rtl:ignore */
+ right: 12px;
+
+ border: 1px solid var(--vp-code-copy-code-border-color);
+ border-radius: 4px;
+
+ background-color: var(--vp-code-block-bg);
+
+ font-size: 20px;
+
+ transition:
+ background-color 0.25s,
+ border-color 0.25s,
+ opacity 0.25s;
+}
+
+.vp-doc div[class*='language'] > .vp-copy-code-button::before {
+ width: 100%;
+ height: 100%;
+ color: var(--vp-code-block-color);
+ mask-image: var(--vp-icon-copy);
+}
+
+.vp-doc div[class*='language'] > .vp-copy-code-button:hover,
+.vp-doc div[class*='language'] > .vp-copy-code-button.copied {
+ border-color: var(--vp-code-copy-code-hover-border-color);
+ background-color: var(--vp-code-copy-code-hover-bg);
+}
+
+.vp-doc [class*='language-'] > button.vp-copy-code-button.copied,
+.vp-doc [class*='language-'] > button.vp-copy-code-button:hover.copied {
+ /* rtl:ignore */
+ border-radius: 0 4px 4px 0;
+}
+
+.vp-doc [class*='language-'] > button.vp-copy-code-button.copied::before,
+.vp-doc [class*='language-'] > button.vp-copy-code-button:hover.copied::before {
+ mask-image: var(--vp-icon-copied);
+}
+
+.vp-doc [class*='language-'] > button.vp-copy-code-button.copied::after,
+.vp-doc [class*='language-'] > button.vp-copy-code-button:hover.copied::after {
+ top: -1px;
+ right: unset;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: fit-content;
+ height: 40px;
+ padding: 0 10px;
+ border: 1px solid var(--vp-code-copy-code-hover-border-color);
+
+ /* rtl:ignore */
+ border-right: 0;
+ border-radius: 4px 0 0 4px;
+
+ background-color: var(--vp-code-copy-code-hover-bg);
+ color: var(--vp-code-copy-code-active-text);
+
+ font-weight: 500;
+ font-size: 12px;
+ text-align: center;
+ white-space: nowrap;
+
+ /* rtl:ignore */
+ transform: translateX(calc(-100% - 1px));
+}
+
+.vp-doc [class*='language-']::before {
+ content: attr(data-title);
+
+ position: absolute;
+ top: 5px;
+ right: 1em;
+ z-index: 3;
+
+ color: var(--vp-code-line-number-color);
+
+ font-size: 0.75rem;
+
+ transition: color var(--t-color);
+}
+
+.vp-doc
+ [class*='language-']:hover
+ > button.vp-copy-code-button
+ + .vp-doc
+ [class*='language-']::before,
+.vp-doc
+ [class*='language-']
+ > button.vp-copy-code-button:focus
+ + .vp-doc
+ [class*='language-']::before {
+ opacity: 0;
+}
diff --git a/themes/theme-next/src/client/styles/components/vp-doc.css b/themes/theme-next/src/client/styles/components/vp-doc.css
new file mode 100644
index 0000000000..9c8a4744b3
--- /dev/null
+++ b/themes/theme-next/src/client/styles/components/vp-doc.css
@@ -0,0 +1,307 @@
+/* stylelint-disable rule-empty-line-before */
+/* stylelint-disable selector-max-compound-selectors */
+
+/**
+ * Headings
+ * -------------------------------------------------------------------------- */
+
+.vp-doc h1,
+.vp-doc h2,
+.vp-doc h3,
+.vp-doc h4,
+.vp-doc h5,
+.vp-doc h6 {
+ position: relative;
+ outline: none;
+ font-weight: 600;
+ overflow-wrap: break-word;
+}
+
+.vp-doc h1 {
+ font-size: 28px;
+ line-height: 40px;
+ letter-spacing: -0.02em;
+}
+
+.vp-doc h2 {
+ margin: 48px 0 16px;
+ padding-top: 24px;
+ border-top: 1px solid var(--vp-c-divider);
+
+ font-size: 24px;
+ line-height: 32px;
+ letter-spacing: -0.02em;
+}
+
+.vp-doc h3 {
+ margin: 32px 0 0;
+ font-size: 20px;
+ line-height: 28px;
+ letter-spacing: -0.01em;
+}
+
+.vp-doc .header-anchor {
+ position: relative;
+ color: inherit;
+ text-decoration: none;
+}
+
+.vp-doc .header-anchor::before {
+ content: var(--vp-header-anchor-symbol);
+
+ position: absolute;
+ left: -0.75em;
+
+ color: var(--vp-c-accent);
+
+ font-size: 0.8em;
+
+ opacity: 0;
+
+ transition: color 0.25s ease;
+}
+
+.vp-doc .header-anchor:hover {
+ color: inherit;
+}
+
+.vp-doc .header-anchor:hover::before {
+ opacity: 1;
+}
+
+.vp-doc .header-anchor:focus-visible {
+ outline: none;
+}
+
+.vp-doc .header-anchor:focus-visible::before {
+ content: var(--vp-header-anchor-symbol);
+
+ position: absolute;
+ left: -0.75em;
+
+ color: var(--vp-c-accent);
+ outline: auto;
+}
+
+@media (min-width: 768px) {
+ .vp-doc h1 {
+ font-size: 32px;
+ line-height: 40px;
+ letter-spacing: -0.02em;
+ }
+}
+
+/**
+ * Paragraph and inline elements
+ * -------------------------------------------------------------------------- */
+
+.vp-doc p,
+.vp-doc summary {
+ margin: 16px 0;
+}
+
+.vp-doc p {
+ line-height: 28px;
+}
+
+.vp-doc blockquote {
+ margin: 16px 0;
+ padding-left: 16px;
+ border-left: 2px solid var(--vp-c-divider);
+ transition: border-color 0.5s;
+}
+
+.vp-doc blockquote > p {
+ margin: 0;
+ color: var(--vp-c-text-mute);
+ font-size: 16px;
+ transition: color 0.5s;
+}
+
+.vp-doc a {
+ color: var(--vp-c-accent);
+
+ font-weight: 500;
+ text-decoration: underline;
+
+ transition:
+ color 0.25s,
+ opacity 0.25s;
+
+ text-underline-offset: 2px;
+}
+
+.vp-doc a:hover {
+ color: var(--vp-c-accent-hover);
+}
+
+.vp-doc strong {
+ font-weight: 600;
+}
+
+.vp-doc a > img {
+ display: inline-block;
+}
+
+/**
+ * Lists
+ * -------------------------------------------------------------------------- */
+
+.vp-doc ul,
+.vp-doc ol {
+ margin: 16px 0;
+ padding-left: 1.25rem;
+}
+
+.vp-doc ul {
+ list-style: disc;
+}
+
+.vp-doc ol {
+ list-style: decimal;
+}
+
+.vp-doc li + li {
+ margin-top: 8px;
+}
+
+.vp-doc li > ol,
+.vp-doc li > ul {
+ margin: 8px 0 0;
+}
+
+/**
+ * Table
+ * -------------------------------------------------------------------------- */
+
+.vp-doc table {
+ display: block;
+ overflow-x: auto;
+ margin: 20px 0;
+ border-collapse: collapse;
+}
+
+.vp-doc tr {
+ border-top: 1px solid var(--vp-c-divider);
+ background-color: var(--vp-c-bg);
+ transition: background-color 0.5s;
+}
+
+.vp-doc tr:nth-child(2n) {
+ background-color: var(--vp-c-bg-soft);
+}
+
+.vp-doc th,
+.vp-doc td {
+ padding: 8px 16px;
+ border: 1px solid var(--vp-c-divider);
+}
+
+.vp-doc th {
+ background-color: var(--vp-c-bg-soft);
+ color: var(--vp-c-text-mute);
+
+ font-weight: 600;
+ font-size: 14px;
+ text-align: left;
+}
+
+.vp-doc td {
+ font-size: 14px;
+}
+
+/**
+ * Decorational elements
+ * -------------------------------------------------------------------------- */
+
+.vp-doc hr {
+ margin: 16px 0;
+ border: none;
+ border-top: 1px solid var(--vp-c-divider);
+}
+
+/**
+ * Custom Block
+ * -------------------------------------------------------------------------- */
+
+.vp-doc .custom-block {
+ margin: 16px 0;
+}
+
+.vp-doc .custom-block p {
+ margin: 8px 0;
+ line-height: 24px;
+}
+
+.vp-doc .custom-block p:first-child {
+ margin: 0;
+}
+
+.vp-doc .custom-block div[class*='language-'] {
+ margin: 8px 0;
+ border-radius: 8px;
+}
+
+.vp-doc .custom-block div[class*='language-'] code {
+ background-color: transparent;
+ font-weight: 400;
+}
+
+.vp-doc .custom-block .vp-code-group .tabs {
+ margin: 0;
+ border-radius: 8px 8px 0 0;
+}
+
+/**
+ * Component: Team
+ * -------------------------------------------------------------------------- */
+
+.vp-doc .vp-team-members {
+ margin-top: 24px;
+}
+
+.vp-doc .vp-team-members.small.count-1 .container {
+ max-width: calc((100% - 24px) / 2) !important;
+ margin: 0 !important;
+}
+
+.vp-doc .vp-team-members.small.count-2 .container,
+.vp-doc .vp-team-members.small.count-3 .container {
+ max-width: 100% !important;
+}
+
+.vp-doc .vp-team-members.medium.count-1 .container {
+ max-width: calc((100% - 24px) / 2) !important;
+ margin: 0 !important;
+}
+
+/**
+ * External links
+ * -------------------------------------------------------------------------- */
+
+/* prettier-ignore */
+:is(.vp-external-link-icon, .vp-doc a[href*='://'], .vp-doc a[target='_blank']):not(.no-icon)::after {
+ --icon: url("data:image/svg+xml, %3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='M0 0h24v24H0V0z' fill='none' /%3E%3Cpath d='M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5H9z' /%3E%3C/svg%3E");
+ display: inline-block;
+ flex-shrink: 0;
+
+ width: 11px;
+ height: 11px;
+ margin-top: -1px;
+ margin-left: 4px;
+
+ background: currentcolor;
+ color: var(--vp-c-text-subtle);
+
+ mask-image: var(--icon);
+}
+
+.vp-external-link-icon::after {
+ content: '';
+}
+
+/* prettier-ignore */
+.external-link-icon-enabled :is(.vp-doc a[href*='://'], .vp-doc a[target='_blank'])::after {
+ content: '';
+ color: currentcolor;
+}
diff --git a/themes/theme-next/src/client/styles/components/vp-sponsor.css b/themes/theme-next/src/client/styles/components/vp-sponsor.css
new file mode 100644
index 0000000000..a92e39eaa5
--- /dev/null
+++ b/themes/theme-next/src/client/styles/components/vp-sponsor.css
@@ -0,0 +1,162 @@
+/**
+ * VPSponsors styles are defined as global because a new class gets
+ * allied in onMounted` hook and we can't use scoped style.
+ */
+.vp-sponsor {
+ overflow: hidden;
+ border-radius: 16px;
+}
+
+.vp-sponsor.aside {
+ border-radius: 12px;
+}
+
+.vp-sponsor-section + .vp-sponsor-section {
+ margin-top: 4px;
+}
+
+.vp-sponsor-tier {
+ width: 100%;
+ margin: 0 0 4px !important;
+
+ background-color: var(--vp-c-bg-soft);
+ color: var(--vp-c-text-mute);
+
+ font-weight: 600;
+ line-height: 24px;
+ letter-spacing: 1px !important;
+ text-align: center;
+}
+
+.vp-sponsor.normal .vp-sponsor-tier {
+ padding: 13px 0 11px;
+ font-size: 14px;
+}
+
+.vp-sponsor.aside .vp-sponsor-tier {
+ padding: 9px 0 7px;
+ font-size: 12px;
+}
+
+.vp-sponsor-grid + .vp-sponsor-tier {
+ margin-top: 4px;
+}
+
+.vp-sponsor-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.vp-sponsor-grid.xmini .vp-sponsor-grid-link {
+ height: 64px;
+}
+
+.vp-sponsor-grid.xmini .vp-sponsor-grid-image {
+ max-width: 64px;
+ max-height: 22px;
+}
+
+.vp-sponsor-grid.mini .vp-sponsor-grid-link {
+ height: 72px;
+}
+
+.vp-sponsor-grid.mini .vp-sponsor-grid-image {
+ max-width: 96px;
+ max-height: 24px;
+}
+
+.vp-sponsor-grid.small .vp-sponsor-grid-link {
+ height: 96px;
+}
+
+.vp-sponsor-grid.small .vp-sponsor-grid-image {
+ max-width: 96px;
+ max-height: 24px;
+}
+
+.vp-sponsor-grid.medium .vp-sponsor-grid-link {
+ height: 112px;
+}
+
+.vp-sponsor-grid.medium .vp-sponsor-grid-image {
+ max-width: 120px;
+ max-height: 36px;
+}
+
+.vp-sponsor-grid.big .vp-sponsor-grid-link {
+ height: 184px;
+}
+
+.vp-sponsor-grid.big .vp-sponsor-grid-image {
+ max-width: 192px;
+ max-height: 56px;
+}
+
+.vp-sponsor-grid[data-vp-grid='2'] .vp-sponsor-grid-item {
+ width: calc((100% - 4px) / 2);
+}
+
+.vp-sponsor-grid[data-vp-grid='3'] .vp-sponsor-grid-item {
+ width: calc((100% - 4px * 2) / 3);
+}
+
+.vp-sponsor-grid[data-vp-grid='4'] .vp-sponsor-grid-item {
+ width: calc((100% - 4px * 3) / 4);
+}
+
+.vp-sponsor-grid[data-vp-grid='5'] .vp-sponsor-grid-item {
+ width: calc((100% - 4px * 4) / 5);
+}
+
+.vp-sponsor-grid[data-vp-grid='6'] .vp-sponsor-grid-item {
+ width: calc((100% - 4px * 5) / 6);
+}
+
+.vp-sponsor-grid-item {
+ flex-shrink: 0;
+ width: 100%;
+ background-color: var(--vp-c-bg-soft);
+ transition: background-color 0.25s;
+}
+
+.vp-sponsor-grid-item:hover {
+ background-color: var(--vp-c-default-soft);
+}
+
+.vp-sponsor-grid-item:hover .vp-sponsor-grid-image {
+ filter: grayscale(0) invert(0);
+}
+
+.vp-sponsor-grid-item.empty:hover {
+ background-color: var(--vp-c-bg-soft);
+}
+
+[data-theme='dark'] .vp-sponsor-grid-item:hover {
+ background-color: var(--vp-c-white);
+}
+
+[data-theme='dark'] .vp-sponsor-grid-item.empty:hover {
+ background-color: var(--vp-c-bg-soft);
+}
+
+.vp-sponsor-grid-link {
+ display: flex;
+}
+
+.vp-sponsor-grid-box {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+}
+
+.vp-sponsor-grid-image {
+ max-width: 100%;
+ filter: grayscale(1);
+ transition: filter 0.25s;
+}
+
+[data-theme='dark'] .vp-sponsor-grid-image {
+ filter: grayscale(1) invert(1);
+}
diff --git a/themes/theme-next/src/client/styles/fonts.css b/themes/theme-next/src/client/styles/fonts.css
new file mode 100644
index 0000000000..262fb19cef
--- /dev/null
+++ b/themes/theme-next/src/client/styles/fonts.css
@@ -0,0 +1,189 @@
+@font-face {
+ font-weight: 100 900;
+ font-style: normal;
+ font-family: Inter;
+ src: url('../fonts/inter-roman-cyrillic-ext.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
+ U+FE2E-FE2F;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: normal;
+ font-family: Inter;
+ src: url('../fonts/inter-roman-cyrillic.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: normal;
+ font-family: Inter;
+ src: url('../fonts/inter-roman-greek-ext.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+1F00-1FFF;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: normal;
+ font-family: Inter;
+ src: url('../fonts/inter-roman-greek.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1,
+ U+03A3-03FF;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: normal;
+ font-family: Inter;
+ src: url('../fonts/inter-roman-vietnamese.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
+ U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
+ U+1EA0-1EF9, U+20AB;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: normal;
+ font-family: Inter;
+ src: url('../fonts/inter-roman-latin-ext.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: normal;
+ font-family: Inter;
+ src: url('../fonts/inter-roman-latin.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
+ U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: italic;
+ font-family: Inter;
+ src: url('../fonts/inter-italic-cyrillic-ext.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
+ U+FE2E-FE2F;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: italic;
+ font-family: Inter;
+ src: url('../fonts/inter-italic-cyrillic.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: italic;
+ font-family: Inter;
+ src: url('../fonts/inter-italic-greek-ext.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+1F00-1FFF;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: italic;
+ font-family: Inter;
+ src: url('../fonts/inter-italic-greek.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1,
+ U+03A3-03FF;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: italic;
+ font-family: Inter;
+ src: url('../fonts/inter-italic-vietnamese.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
+ U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
+ U+1EA0-1EF9, U+20AB;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: italic;
+ font-family: Inter;
+ src: url('../fonts/inter-italic-latin-ext.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
+ U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-weight: 100 900;
+ font-style: italic;
+ font-family: Inter;
+ src: url('../fonts/inter-italic-latin.woff2') format('woff2');
+
+ font-display: swap;
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
+ U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
+ U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-weight: 400;
+ font-family: 'Punctuation SC';
+ src: local('PingFang SC Regular'), local('Noto Sans CJK SC'),
+ local('Microsoft YaHei');
+ unicode-range: U+201C, U+201D, U+2018, U+2019, U+2E3A, U+2014, U+2013, U+2026,
+ U+00B7, U+007E, U+002F;
+}
+
+@font-face {
+ font-weight: 500;
+ font-family: 'Punctuation SC';
+ src: local('PingFang SC Medium'), local('Noto Sans CJK SC'),
+ local('Microsoft YaHei');
+ unicode-range: U+201C, U+201D, U+2018, U+2019, U+2E3A, U+2014, U+2013, U+2026,
+ U+00B7, U+007E, U+002F;
+}
+
+@font-face {
+ font-weight: 600;
+ font-family: 'Punctuation SC';
+ src: local('PingFang SC Semibold'), local('Noto Sans CJK SC Bold'),
+ local('Microsoft YaHei Bold');
+ unicode-range: U+201C, U+201D, U+2018, U+2019, U+2E3A, U+2014, U+2013, U+2026,
+ U+00B7, U+007E, U+002F;
+}
+
+@font-face {
+ font-weight: 700;
+ font-family: 'Punctuation SC';
+ src: local('PingFang SC Semibold'), local('Noto Sans CJK SC Bold'),
+ local('Microsoft YaHei Bold');
+ unicode-range: U+201C, U+201D, U+2018, U+2019, U+2E3A, U+2014, U+2013, U+2026,
+ U+00B7, U+007E, U+002F;
+}
diff --git a/themes/theme-next/src/client/styles/icons.css b/themes/theme-next/src/client/styles/icons.css
new file mode 100644
index 0000000000..8614c13408
--- /dev/null
+++ b/themes/theme-next/src/client/styles/icons.css
@@ -0,0 +1,154 @@
+[class^='vpi-'],
+[class*=' vpi-'],
+.vp-icon {
+ width: 1em;
+ height: 1em;
+}
+
+[class^='vpi-'].bg,
+[class*=' vpi-'].bg,
+.vp-icon.bg {
+ background-color: transparent;
+ background-size: 100% 100%;
+}
+
+[class^='vpi-']:not(.bg),
+[class*=' vpi-']:not(.bg),
+.vp-icon:not(.bg) {
+ background-color: currentcolor;
+ color: inherit;
+ mask: var(--icon) no-repeat;
+ mask-size: 100% 100%;
+}
+
+/* internal icons - used under ISC from https://lucide.dev/ */
+.vpi-align-left {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M21 6H3M15 12H3M17 18H3'/%3E%3C/svg%3E");
+}
+
+.vpi-arrow-right,
+.vpi-arrow-down,
+.vpi-arrow-left,
+.vpi-arrow-up {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5l7 7-7 7'/%3E%3C/svg%3E");
+}
+
+.vpi-chevron-right,
+.vpi-chevron-down,
+.vpi-chevron-left,
+.vpi-chevron-up {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 18 6-6-6-6'/%3E%3C/svg%3E");
+}
+
+.vpi-chevron-down,
+.vpi-arrow-down {
+ transform: rotate(90deg);
+}
+
+.vpi-chevron-left,
+.vpi-arrow-left {
+ transform: rotate(180deg);
+}
+
+.vpi-chevron-up,
+.vpi-arrow-up {
+ transform: rotate(-90deg);
+}
+
+.vpi-square-pen {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7'/%3E%3Cpath d='M18.375 2.625a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4Z'/%3E%3C/svg%3E");
+}
+
+.vpi-plus {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M5 12h14M12 5v14'/%3E%3C/svg%3E");
+}
+
+.vpi-sun {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='4'/%3E%3Cpath d='M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41'/%3E%3C/svg%3E");
+}
+
+.vpi-moon {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z'/%3E%3C/svg%3E");
+}
+
+.vpi-more-horizontal {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='1'/%3E%3Ccircle cx='19' cy='12' r='1'/%3E%3Ccircle cx='5' cy='12' r='1'/%3E%3C/svg%3E");
+}
+
+.vpi-languages {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m5 8 6 6M4 14l6-6 2-3M2 5h12M7 2h1M22 22l-5-10-5 10M14 18h6'/%3E%3C/svg%3E");
+}
+
+.vpi-heart {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z'/%3E%3C/svg%3E");
+}
+
+.vpi-search {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E");
+}
+
+.vpi-layout-list {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='7' height='7' x='3' y='3' rx='1'/%3E%3Crect width='7' height='7' x='3' y='14' rx='1'/%3E%3Cpath d='M14 4h7M14 9h7M14 15h7M14 20h7'/%3E%3C/svg%3E");
+}
+
+.vpi-delete {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='M20 5H9l-7 7 7 7h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2ZM18 9l-6 6M12 9l6 6'/%3E%3C/svg%3E");
+}
+
+.vpi-corner-down-left {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Cpath d='m9 10-5 5 5 5'/%3E%3Cpath d='M20 4v7a4 4 0 0 1-4 4H4'/%3E%3C/svg%3E");
+}
+
+.vpi-back-to-top {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='4' d='M24.008 14.1V42M12 26l12-12l12 12M12 6h24' /%3E%3C/svg%3E");
+}
+
+:root {
+ /* clipboard */
+ --vp-icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");
+
+ /* clipboard-copy */
+ --vp-icon-copied: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' stroke='rgba(128,128,128,1)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 24 24'%3E%3Crect width='8' height='4' x='8' y='2' rx='1' ry='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3Cpath d='m9 14 2 2 4-4'/%3E%3C/svg%3E");
+}
+
+/* social icons - used under CC0 1.0 from https://simpleicons.org/ */
+.vpi-social-discord {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418Z'/%3E%3C/svg%3E");
+}
+
+.vpi-social-facebook {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z'/%3E%3C/svg%3E");
+}
+
+.vpi-social-github {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E");
+}
+
+.vpi-social-instagram {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7.03.084c-1.277.06-2.149.264-2.91.563a5.874 5.874 0 0 0-2.124 1.388 5.878 5.878 0 0 0-1.38 2.127C.321 4.926.12 5.8.064 7.076.008 8.354-.005 8.764.001 12.023c.007 3.259.021 3.667.083 4.947.061 1.277.264 2.149.563 2.911.308.789.72 1.457 1.388 2.123a5.872 5.872 0 0 0 2.129 1.38c.763.295 1.636.496 2.913.552 1.278.056 1.689.069 4.947.063 3.257-.007 3.668-.021 4.947-.082 1.28-.06 2.147-.265 2.91-.563a5.881 5.881 0 0 0 2.123-1.388 5.881 5.881 0 0 0 1.38-2.129c.295-.763.496-1.636.551-2.912.056-1.28.07-1.69.063-4.948-.006-3.258-.02-3.667-.081-4.947-.06-1.28-.264-2.148-.564-2.911a5.892 5.892 0 0 0-1.387-2.123 5.857 5.857 0 0 0-2.128-1.38C19.074.322 18.202.12 16.924.066 15.647.009 15.236-.006 11.977 0 8.718.008 8.31.021 7.03.084m.14 21.693c-1.17-.05-1.805-.245-2.228-.408a3.736 3.736 0 0 1-1.382-.895 3.695 3.695 0 0 1-.9-1.378c-.165-.423-.363-1.058-.417-2.228-.06-1.264-.072-1.644-.08-4.848-.006-3.204.006-3.583.061-4.848.05-1.169.246-1.805.408-2.228.216-.561.477-.96.895-1.382a3.705 3.705 0 0 1 1.379-.9c.423-.165 1.057-.361 2.227-.417 1.265-.06 1.644-.072 4.848-.08 3.203-.006 3.583.006 4.85.062 1.168.05 1.804.244 2.227.408.56.216.96.475 1.382.895.421.42.681.817.9 1.378.165.422.362 1.056.417 2.227.06 1.265.074 1.645.08 4.848.005 3.203-.006 3.583-.061 4.848-.051 1.17-.245 1.805-.408 2.23-.216.56-.477.96-.896 1.38a3.705 3.705 0 0 1-1.378.9c-.422.165-1.058.362-2.226.418-1.266.06-1.645.072-4.85.079-3.204.007-3.582-.006-4.848-.06m9.783-16.192a1.44 1.44 0 1 0 1.437-1.442 1.44 1.44 0 0 0-1.437 1.442M5.839 12.012a6.161 6.161 0 1 0 12.323-.024 6.162 6.162 0 0 0-12.323.024M8 12.008A4 4 0 1 1 12.008 16 4 4 0 0 1 8 12.008'/%3E%3C/svg%3E");
+}
+
+.vpi-social-linkedin {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z'/%3E%3C/svg%3E");
+}
+
+.vpi-social-mastodon {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z'/%3E%3C/svg%3E");
+}
+
+.vpi-social-npm {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z'/%3E%3C/svg%3E");
+}
+
+.vpi-social-slack {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.268 0a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z'/%3E%3C/svg%3E");
+}
+
+.vpi-social-twitter,
+.vpi-social-x {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z'/%3E%3C/svg%3E");
+}
+
+.vpi-social-youtube {
+ --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z'/%3E%3C/svg%3E");
+}
diff --git a/themes/theme-next/src/client/styles/index.css b/themes/theme-next/src/client/styles/index.css
new file mode 100644
index 0000000000..a26f9e9eb5
--- /dev/null
+++ b/themes/theme-next/src/client/styles/index.css
@@ -0,0 +1,11 @@
+@import url('./vars.css');
+@import url('./fonts.css');
+@import url('./base.css');
+@import url('./icons.css');
+@import url('./utils.css');
+@import url('./components/custom-block.css');
+@import url('./components/vp-code.css');
+@import url('./components/vp-code-tabs.css');
+@import url('./components/vp-doc.css');
+@import url('./components/vp-sponsor.css');
+@import url('./compat.css');
diff --git a/themes/theme-next/src/client/styles/utils.css b/themes/theme-next/src/client/styles/utils.css
new file mode 100644
index 0000000000..8b2e80cd7a
--- /dev/null
+++ b/themes/theme-next/src/client/styles/utils.css
@@ -0,0 +1,12 @@
+.visually-hidden {
+ position: absolute;
+
+ overflow: hidden;
+ clip-path: inset(50%);
+ clip: rect(0 0 0 0);
+
+ width: 1px;
+ height: 1px;
+
+ white-space: nowrap;
+}
diff --git a/themes/theme-next/src/client/styles/vars.css b/themes/theme-next/src/client/styles/vars.css
new file mode 100644
index 0000000000..a1ea41970e
--- /dev/null
+++ b/themes/theme-next/src/client/styles/vars.css
@@ -0,0 +1,603 @@
+/**
+ * Colors: Solid
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-c-white: #fff;
+ --vp-c-black: #000;
+
+ --vp-c-neutral: var(--vp-c-black);
+ --vp-c-neutral-inverse: var(--vp-c-white);
+}
+
+[data-theme='dark'] {
+ --vp-c-neutral: var(--vp-c-white);
+ --vp-c-neutral-inverse: var(--vp-c-black);
+}
+
+/**
+ * Colors: Palette
+ *
+ * The primitive colors used for accent colors. These colors are referenced
+ * by functional colors such as "Text", "Background", or "Brand".
+ *
+ * Each colors have exact same color scale system with 3 levels of solid
+ * colors with different brightness, and 1 soft color.
+ *
+ * - `XXX-1`: The most solid color used mainly for colored text. It must
+ * satisfy the contrast ratio against when used on top of `XXX-soft`.
+ *
+ * - `XXX-2`: The color used mainly for hover state of the button.
+ *
+ * - `XXX-3`: The color for solid background, such as bg color of the button.
+ * It must satisfy the contrast ratio with pure white (#ffffff) text on
+ * top of it.
+ *
+ * - `XXX-soft`: The color used for subtle background such as custom container
+ * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
+ * on top of it.
+ *
+ * The soft color must be semi transparent alpha channel. This is crucial
+ * because it allows adding multiple "soft" colors on top of each other
+ * to create a accent, such as when having inline code block inside
+ * custom containers.
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-c-gray-1: #dddde3;
+ --vp-c-gray-2: #e4e4e9;
+ --vp-c-gray-3: #ebebef;
+ --vp-c-gray-soft: rgb(142 150 170 / 14%);
+
+ --vp-c-indigo-1: #3451b2;
+ --vp-c-indigo-2: #3a5ccc;
+ --vp-c-indigo-3: #5672cd;
+ --vp-c-indigo-soft: rgb(100 108 255 / 14%);
+
+ --vp-c-purple-1: #6f42c1;
+ --vp-c-purple-2: #7e4cc9;
+ --vp-c-purple-3: #8e5cd9;
+ --vp-c-purple-soft: rgb(159 122 234 / 14%);
+
+ --vp-c-green-1: #18794e;
+ --vp-c-green-2: #299764;
+ --vp-c-green-3: #30a46c;
+ --vp-c-green-soft: rgb(16 185 129 / 14%);
+
+ --vp-c-yellow-1: #915930;
+ --vp-c-yellow-2: #946300;
+ --vp-c-yellow-3: #9f6a00;
+ --vp-c-yellow-soft: rgb(234 179 8 / 14%);
+
+ --vp-c-red-1: #b8272c;
+ --vp-c-red-2: #d5393e;
+ --vp-c-red-3: #e0575b;
+ --vp-c-red-soft: rgb(244 63 94 / 14%);
+
+ --vp-c-sponsor: #db2777;
+}
+
+[data-theme='dark'] {
+ --vp-c-gray-1: #515c67;
+ --vp-c-gray-2: #414853;
+ --vp-c-gray-3: #32363f;
+ --vp-c-gray-soft: rgb(101 117 133 / 16%);
+
+ --vp-c-indigo-1: #a8b1ff;
+ --vp-c-indigo-2: #5c73e7;
+ --vp-c-indigo-3: #3e63dd;
+ --vp-c-indigo-soft: rgb(100 108 255 / 16%);
+
+ --vp-c-purple-1: #c8abfa;
+ --vp-c-purple-2: #a879e6;
+ --vp-c-purple-3: #8e5cd9;
+ --vp-c-purple-soft: rgb(159 122 234 / 16%);
+
+ --vp-c-green-1: #3dd68c;
+ --vp-c-green-2: #30a46c;
+ --vp-c-green-3: #298459;
+ --vp-c-green-soft: rgb(16 185 129 / 16%);
+
+ --vp-c-yellow-1: #f9b44e;
+ --vp-c-yellow-2: #da8b17;
+ --vp-c-yellow-3: #a46a0a;
+ --vp-c-yellow-soft: rgb(234 179 8 / 16%);
+
+ --vp-c-red-1: #f66f81;
+ --vp-c-red-2: #f14158;
+ --vp-c-red-3: #b62a3c;
+ --vp-c-red-soft: rgb(244 63 94 / 16%);
+}
+
+/**
+ * Colors: Background
+ *
+ * - `bg`: The bg color used for main screen.
+ *
+ * - `bg-alt`: The alternative bg color used in places such as "sidebar",
+ * or "code block".
+ *
+ * - `bg-elv`: The elevated bg color. This is used at parts where it "floats",
+ * such as "dialog".
+ *
+ * - `bg-soft`: The bg color to slightly distinguish some components from
+ * the page. Used for things like "carbon ads" or "table".
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-c-bg: #fff;
+ --vp-c-bg-alt: #f6f6f7;
+ --vp-c-bg-elv: #fff;
+ --vp-c-bg-soft: #f6f6f7;
+}
+
+[data-theme='dark'] {
+ --vp-c-bg: #1b1b1f;
+ --vp-c-bg-alt: #161618;
+ --vp-c-bg-elv: #202127;
+ --vp-c-bg-soft: #202127;
+}
+
+/**
+ * Colors: Borders
+ *
+ * - `divider`: This is used for separators. This is used to divide sections
+ * within the same components, such as having separator on "h2" heading.
+ *
+ * - `border`: This is designed for borders on interactive components.
+ * For example this should be used for a button outline.
+ *
+ * - `gutter`: This is used to divide components in the page. For example
+ * the header and the lest of the page.
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-c-border: #c2c2c4;
+ --vp-c-border-hard: #b2b2b4;
+ --vp-c-divider: #e2e2e3;
+ --vp-c-gutter: #e2e2e3;
+}
+
+[data-theme='dark'] {
+ --vp-c-border: #3c3f44;
+ --vp-c-divider: #2e2e32;
+ --vp-c-gutter: #000;
+}
+
+/**
+ * Colors: Text
+ *
+ * - `text`: Used for primary text.
+ *
+ * - `text-mute`: Used for muted texts, such as "inactive menu" or "info texts".
+ *
+ * - `text-subtle`: Used for subtle texts, such as "placeholders" or "caret icon".
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-c-text: rgb(60 60 67);
+ --vp-c-text-mute: rgb(60 60 67 / 78%);
+ --vp-c-text-subtle: rgb(60 60 67 / 56%);
+}
+
+[data-theme='dark'] {
+ --vp-c-text: rgb(255 255 245 / 86%);
+ --vp-c-text-mute: rgb(235 235 245 / 60%);
+ --vp-c-text-subtle: rgb(235 235 245 / 38%);
+}
+
+/**
+ * Colors: Function
+ *
+ * - `default`: The color used purely for subtle indication without any
+ * special meanings attached to it such as bg color for menu hover state.
+ *
+ * - `brand`: Used for primary brand colors, such as link text, button with
+ * brand theme, etc.
+ *
+ * - `tip`: Used to indicate useful information. The default theme uses the
+ * brand color for this by default.
+ *
+ * - `warning`: Used to indicate warning to the users. Used in custom
+ * container, badges, etc.
+ *
+ * - `danger`: Used to show error, or dangerous message to the users. Used
+ * in custom container, badges, etc.
+ *
+ * To understand the scaling system, refer to "Colors: Palette" section.
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-c-default-1: var(--vp-c-gray-1);
+ --vp-c-default-2: var(--vp-c-gray-2);
+ --vp-c-default-3: var(--vp-c-gray-3);
+ --vp-c-default-soft: var(--vp-c-gray-soft);
+
+ --vp-c-accent: var(--vp-c-indigo-1);
+ --vp-c-accent-hover: var(--vp-c-indigo-2);
+ --vp-c-accent-bg: var(--vp-c-indigo-3);
+ --vp-c-accent-text: var(--vp-c-indigo-1);
+ --vp-c-accent-soft: var(--vp-c-indigo-soft);
+
+ --vp-c-tip-1: var(--vp-c-accent);
+ --vp-c-tip-2: var(--vp-c-accent-hover);
+ --vp-c-tip-3: var(--vp-c-accent-bg);
+ --vp-c-tip-soft: var(--vp-c-accent-soft);
+
+ --vp-c-note-1: var(--vp-c-accent);
+ --vp-c-note-2: var(--vp-c-accent-hover);
+ --vp-c-note-3: var(--vp-c-accent-bg);
+ --vp-c-note-soft: var(--vp-c-accent-soft);
+
+ --vp-c-success-1: var(--vp-c-green-1);
+ --vp-c-success-2: var(--vp-c-green-2);
+ --vp-c-success-3: var(--vp-c-green-3);
+ --vp-c-success-soft: var(--vp-c-green-soft);
+
+ --vp-c-important-1: var(--vp-c-purple-1);
+ --vp-c-important-2: var(--vp-c-purple-2);
+ --vp-c-important-3: var(--vp-c-purple-3);
+ --vp-c-important-soft: var(--vp-c-purple-soft);
+
+ --vp-c-warning-1: var(--vp-c-yellow-1);
+ --vp-c-warning-2: var(--vp-c-yellow-2);
+ --vp-c-warning-3: var(--vp-c-yellow-3);
+ --vp-c-warning-soft: var(--vp-c-yellow-soft);
+
+ --vp-c-danger-1: var(--vp-c-red-1);
+ --vp-c-danger-2: var(--vp-c-red-2);
+ --vp-c-danger-3: var(--vp-c-red-3);
+ --vp-c-danger-soft: var(--vp-c-red-soft);
+
+ --vp-c-caution-1: var(--vp-c-red-1);
+ --vp-c-caution-2: var(--vp-c-red-2);
+ --vp-c-caution-3: var(--vp-c-red-3);
+ --vp-c-caution-soft: var(--vp-c-red-soft);
+}
+
+/**
+ * Typography
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-font-family-base: 'Inter', ui-sans-serif, system-ui, sans-serif,
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
+ --vp-font-family-mono: ui-monospace, 'Menlo', 'Monaco', 'Consolas',
+ 'Liberation Mono', 'Courier New', monospace;
+ font-optical-sizing: auto;
+}
+
+:root:where(:lang(zh)) {
+ --vp-font-family-base: 'Punctuation SC', 'Inter', ui-sans-serif, system-ui,
+ sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+}
+
+/**
+ * Shadows
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-shadow-1: 0 1px 2px rgb(0 0 0 / 4%), 0 1px 2px rgb(0 0 0 / 6%);
+ --vp-shadow-2: 0 3px 12px rgb(0 0 0 / 7%), 0 1px 4px rgb(0 0 0 / 7%);
+ --vp-shadow-3: 0 12px 32px rgb(0 0 0 / 10%), 0 2px 6px rgb(0 0 0 / 8%);
+ --vp-shadow-4: 0 14px 44px rgb(0 0 0 / 12%), 0 3px 9px rgb(0 0 0 / 12%);
+ --vp-shadow-5: 0 18px 56px rgb(0 0 0 / 16%), 0 4px 12px rgb(0 0 0 / 16%);
+
+ --vp-shadow: var(--vp-shadow-1);
+ --vp-shadow-hard: var(--vp-shadow-3);
+}
+
+/**
+ * Z-indexes
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-z-index-footer: 10;
+ --vp-z-index-local-nav: 20;
+ --vp-z-index-nav: 30;
+ --vp-z-index-layout-top: 40;
+ --vp-z-index-backdrop: 50;
+ --vp-z-index-sidebar: 60;
+}
+
+@media (min-width: 960px) {
+ :root {
+ --vp-z-index-sidebar: 25;
+ }
+}
+
+/**
+ * Transitions
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-t-color: ease 0.3s;
+ --vp-t-transform: ease 0.3s;
+}
+
+/**
+ * Layouts
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-layout-max-width: 1440px;
+}
+
+/**
+ * Controls
+ * -------------------------------------------------------------------------- */
+:root {
+ --vp-c-control: var(--vp-c-default-3);
+ --vp-c-control-hover: var(--vp-c-default-2);
+ --vp-c-control-active: var(--vp-c-default-1);
+ --vp-c-control-disabled: var(--vp-c-default-soft);
+}
+
+/**
+ * Component: Header Anchor
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-header-anchor-symbol: '#';
+}
+
+/**
+ * Component: Tabs
+ * -------------------------------------------------------------------------- */
+:root {
+ --tab-c-bg-nav: var(--vp-c-gray-soft);
+}
+
+/**
+ * Component: Code
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-code-line-height: 1.7;
+ --vp-code-font-size: 0.875em;
+ --vp-code-color: var(--vp-c-accent);
+ --vp-code-link-color: var(--vp-c-accent);
+ --vp-code-link-hover-color: var(--vp-c-accent-hover);
+ --vp-code-bg: var(--vp-c-default-soft);
+
+ --vp-code-block-color: var(--vp-c-text-mute);
+ --vp-code-block-bg: var(--vp-c-bg-alt);
+ --vp-code-block-divider-color: var(--vp-c-gutter);
+
+ --vp-code-lang-color: var(--vp-c-text-subtle);
+
+ --vp-code-line-highlight-color: var(--vp-c-default-soft);
+ --vp-code-line-number-color: var(--vp-c-text-subtle);
+
+ --vp-code-line-diff-add-color: var(--vp-c-success-soft);
+ --vp-code-line-diff-add-symbol-color: var(--vp-c-success-1);
+
+ --vp-code-line-diff-remove-color: var(--vp-c-danger-soft);
+ --vp-code-line-diff-remove-symbol-color: var(--vp-c-danger-1);
+
+ --vp-code-line-warning-color: var(--vp-c-warning-soft);
+ --vp-code-line-error-color: var(--vp-c-danger-soft);
+
+ --vp-code-copy-code-border-color: var(--vp-c-divider);
+ --vp-code-copy-code-bg: var(--vp-c-bg-soft);
+ --vp-code-copy-code-hover-border-color: var(--vp-c-divider);
+ --vp-code-copy-code-hover-bg: var(--vp-c-bg);
+ --vp-code-copy-code-active-text: var(--vp-c-text-mute);
+ --vp-code-copy-copied-text-content: 'Copied';
+
+ --vp-code-tab-divider: var(--vp-code-block-divider-color);
+ --vp-code-tab-text-color: var(--vp-c-text-mute);
+ --vp-code-tab-bg: var(--vp-code-block-bg);
+ --vp-code-tab-hover-text-color: var(--vp-c-text);
+ --vp-code-tab-active-text-color: var(--vp-c-text);
+ --vp-code-tab-active-bar-color: var(--vp-c-accent);
+}
+
+/**
+ * Component: Button
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-button-brand-border: transparent;
+ --vp-button-brand-text: var(--vp-c-white);
+ --vp-button-brand-bg: var(--vp-c-accent-bg);
+ --vp-button-brand-hover-border: transparent;
+ --vp-button-brand-hover-text: var(--vp-c-white);
+ --vp-button-brand-hover-bg: var(--vp-c-accent-hover);
+ --vp-button-brand-active-border: transparent;
+ --vp-button-brand-active-text: var(--vp-c-white);
+ --vp-button-brand-active-bg: var(--vp-c-accent);
+
+ --vp-button-alt-border: transparent;
+ --vp-button-alt-text: var(--vp-c-text);
+ --vp-button-alt-bg: var(--vp-c-default-3);
+ --vp-button-alt-hover-border: transparent;
+ --vp-button-alt-hover-text: var(--vp-c-text);
+ --vp-button-alt-hover-bg: var(--vp-c-default-2);
+ --vp-button-alt-active-border: transparent;
+ --vp-button-alt-active-text: var(--vp-c-text);
+ --vp-button-alt-active-bg: var(--vp-c-default-1);
+
+ --vp-button-sponsor-border: var(--vp-c-text-mute);
+ --vp-button-sponsor-text: var(--vp-c-text-mute);
+ --vp-button-sponsor-bg: transparent;
+ --vp-button-sponsor-hover-border: var(--vp-c-sponsor);
+ --vp-button-sponsor-hover-text: var(--vp-c-sponsor);
+ --vp-button-sponsor-hover-bg: transparent;
+ --vp-button-sponsor-active-border: var(--vp-c-sponsor);
+ --vp-button-sponsor-active-text: var(--vp-c-sponsor);
+ --vp-button-sponsor-active-bg: transparent;
+}
+
+/**
+ * Component: Custom Block
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-custom-block-font-size: 14px;
+ --vp-custom-block-code-font-size: 13px;
+
+ --vp-custom-block-info-border: transparent;
+ --vp-custom-block-info-text: var(--vp-c-text);
+ --vp-custom-block-info-bg: var(--vp-c-default-soft);
+ --vp-custom-block-info-code-bg: var(--vp-c-default-soft);
+ --vp-custom-block-info-icon-color: var(--vp-c-text-subtle);
+
+ --vp-custom-block-note-border: transparent;
+ --vp-custom-block-note-text: var(--vp-c-text);
+ --vp-custom-block-note-bg: var(--vp-c-default-soft);
+ --vp-custom-block-note-code-bg: var(--vp-c-default-soft);
+ --vp-custom-block-note-icon-color: var(--vp-c-text-subtle);
+
+ --vp-custom-block-tip-border: transparent;
+ --vp-custom-block-tip-text: var(--vp-c-text);
+ --vp-custom-block-tip-bg: var(--vp-c-tip-soft);
+ --vp-custom-block-tip-code-bg: var(--vp-c-tip-soft);
+ --vp-custom-block-tip-icon-color: var(--vp-c-tip-3);
+
+ --vp-custom-block-important-border: transparent;
+ --vp-custom-block-important-text: var(--vp-c-text);
+ --vp-custom-block-important-bg: var(--vp-c-important-soft);
+ --vp-custom-block-important-code-bg: var(--vp-c-important-soft);
+ --vp-custom-block-important-icon-color: var(--vp-c-important-3);
+
+ --vp-custom-block-warning-border: transparent;
+ --vp-custom-block-warning-text: var(--vp-c-text);
+ --vp-custom-block-warning-bg: var(--vp-c-warning-soft);
+ --vp-custom-block-warning-code-bg: var(--vp-c-warning-soft);
+ --vp-custom-block-warning-icon-color: var(--vp-c-warning-3);
+
+ --vp-custom-block-danger-border: transparent;
+ --vp-custom-block-danger-text: var(--vp-c-text);
+ --vp-custom-block-danger-bg: var(--vp-c-danger-soft);
+ --vp-custom-block-danger-code-bg: var(--vp-c-danger-soft);
+ --vp-custom-block-danger-icon-color: var(--vp-c-danger-3);
+
+ --vp-custom-block-caution-border: transparent;
+ --vp-custom-block-caution-text: var(--vp-c-text);
+ --vp-custom-block-caution-bg: var(--vp-c-caution-soft);
+ --vp-custom-block-caution-code-bg: var(--vp-c-caution-soft);
+ --vp-custom-block-caution-icon-color: var(--vp-c-caution-3);
+
+ --vp-custom-block-details-border: var(--vp-custom-block-info-border);
+ --vp-custom-block-details-text: var(--vp-custom-block-info-text);
+ --vp-custom-block-details-bg: var(--vp-custom-block-info-bg);
+ --vp-custom-block-details-code-bg: var(--vp-custom-block-info-code-bg);
+}
+
+/**
+ * Component: Input
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-input-border-color: var(--vp-c-border);
+ --vp-input-bg-color: var(--vp-c-bg-alt);
+
+ --vp-input-switch-bg-color: var(--vp-c-default-soft);
+}
+
+/**
+ * Component: Nav
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-nav-height: 64px;
+ --vp-nav-bg-color: var(--vp-c-bg);
+ --vp-nav-screen-bg-color: var(--vp-c-bg);
+ --vp-nav-logo-height: 24px;
+}
+
+.hide-nav {
+ --vp-nav-height: 0;
+}
+
+.hide-nav .vp-sidebar {
+ --vp-nav-height: 22px;
+}
+
+/**
+ * Component: Local Nav
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-local-nav-bg-color: var(--vp-c-bg);
+}
+
+/**
+ * Component: Sidebar
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-sidebar-width: 272px;
+ --vp-sidebar-bg-color: var(--vp-c-bg-alt);
+}
+
+/**
+ * Colors Backdrop
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-backdrop-bg-color: rgb(0 0 0 / 60%);
+}
+
+/**
+ * Component: Home
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-home-hero-name-color: var(--vp-c-accent);
+ --vp-home-hero-name-background: transparent;
+
+ --vp-home-hero-image-background-image: none;
+ --vp-home-hero-image-filter: none;
+}
+
+/**
+ * Component: Badge
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-badge-info-border: transparent;
+ --vp-badge-info-text: var(--vp-c-text-mute);
+ --vp-badge-info-bg: var(--vp-c-default-soft);
+
+ --vp-badge-tip-border: transparent;
+ --vp-badge-tip-text: var(--vp-c-tip-1);
+ --vp-badge-tip-bg: var(--vp-c-tip-soft);
+
+ --vp-badge-warning-border: transparent;
+ --vp-badge-warning-text: var(--vp-c-warning-1);
+ --vp-badge-warning-bg: var(--vp-c-warning-soft);
+
+ --vp-badge-danger-border: transparent;
+ --vp-badge-danger-text: var(--vp-c-danger-1);
+ --vp-badge-danger-bg: var(--vp-c-danger-soft);
+}
+
+/**
+ * Component: Carbon Ads
+ * -------------------------------------------------------------------------- */
+
+:root {
+ --vp-carbon-ads-text-color: var(--vp-c-text);
+ --vp-carbon-ads-poweredby-color: var(--vp-c-text-mute);
+ --vp-carbon-ads-bg-color: var(--vp-c-bg-soft);
+ --vp-carbon-ads-hover-text-color: var(--vp-c-accent);
+ --vp-carbon-ads-hover-poweredby-color: var(--vp-c-text);
+}
+
+/**
+ * plugin-nprogress
+ * -------------------------------------------------------------------------- */
+#nprogress {
+ --nprogress-color: var(--vp-c-accent);
+}
+
+/**
+ * plugin-photo-swipe
+ * -------------------------------------------------------------------------- */
+:root {
+ --photo-swipe-bullet: var(--vp-c-bg);
+ --photo-swipe-bullet-active: var(--vp-c-accent);
+}
diff --git a/themes/theme-next/src/client/types.ts b/themes/theme-next/src/client/types.ts
new file mode 100644
index 0000000000..80c6dc9151
--- /dev/null
+++ b/themes/theme-next/src/client/types.ts
@@ -0,0 +1,5 @@
+import type { VNode } from 'vue'
+
+export type Slot = never extends P
+ ? () => VNode | VNode[] | string | null
+ : (props: P) => VNode | VNode[] | string | null
diff --git a/themes/theme-next/src/client/utils/constants.ts b/themes/theme-next/src/client/utils/constants.ts
new file mode 100644
index 0000000000..7a1d300ff3
--- /dev/null
+++ b/themes/theme-next/src/client/utils/constants.ts
@@ -0,0 +1,4 @@
+export const HASH_RE = /#.*$/
+export const EXT_RE = /(index|README)?\.(md|html)$/
+
+export const inBrowser = typeof document !== 'undefined'
diff --git a/themes/theme-next/src/client/utils/getNavLink.ts b/themes/theme-next/src/client/utils/getNavLink.ts
new file mode 100644
index 0000000000..27e0371181
--- /dev/null
+++ b/themes/theme-next/src/client/utils/getNavLink.ts
@@ -0,0 +1,24 @@
+import {
+ ensureEndingSlash,
+ ensureLeadingSlash,
+ isLinkAbsolute,
+ isLinkWithProtocol,
+} from '@vuepress/helper/client'
+import { resolveRoute } from 'vuepress/client'
+import type { ResolvedNavItemWithLink } from '../../shared/resolved/navbar.js'
+
+export const getNavLink = (filepath: string): ResolvedNavItemWithLink => {
+ const { notFound, path, meta } = resolveRoute<{ title?: string }>(filepath)
+ if (notFound) {
+ return { text: path, link: path }
+ }
+ return { text: meta.title || path, link: path }
+}
+
+export const normalizeLink = (base = '', link = ''): string =>
+ isLinkAbsolute(link) || isLinkWithProtocol(link)
+ ? link
+ : ensureLeadingSlash(`${base}/${link}`.replace(/\/+/g, '/'))
+
+export const normalizePrefix = (base: string, link = ''): string =>
+ ensureEndingSlash(normalizeLink(base, link))
diff --git a/themes/theme-next/src/client/utils/getScrollOffset.ts b/themes/theme-next/src/client/utils/getScrollOffset.ts
new file mode 100644
index 0000000000..72139dfea5
--- /dev/null
+++ b/themes/theme-next/src/client/utils/getScrollOffset.ts
@@ -0,0 +1,36 @@
+import type { DefaultThemeLocaleData } from '../../shared'
+
+const tryOffsetSelector = (selector: string, padding: number): number => {
+ const el = document.querySelector(selector)
+ if (!el) return 0
+ const bot = el.getBoundingClientRect().bottom
+ if (bot < 0) return 0
+ return bot + padding
+}
+
+export const getScrollOffset = (
+ scrollOffset?: DefaultThemeLocaleData['scrollOffset'],
+): number => {
+ let offset = 0
+ let padding = 24
+ if (typeof scrollOffset === 'object' && 'padding' in scrollOffset) {
+ padding = scrollOffset.padding
+ // eslint-disable-next-line no-param-reassign
+ scrollOffset = scrollOffset.selector
+ }
+ if (typeof scrollOffset === 'number') {
+ offset = scrollOffset
+ } else if (typeof scrollOffset === 'string') {
+ offset = tryOffsetSelector(scrollOffset, padding)
+ } else if (Array.isArray(scrollOffset)) {
+ for (const selector of scrollOffset) {
+ const res = tryOffsetSelector(selector, padding)
+ if (res) {
+ offset = res
+ break
+ }
+ }
+ }
+
+ return offset
+}
diff --git a/themes/theme-next/src/client/utils/index.ts b/themes/theme-next/src/client/utils/index.ts
new file mode 100644
index 0000000000..7eded5a7f1
--- /dev/null
+++ b/themes/theme-next/src/client/utils/index.ts
@@ -0,0 +1,5 @@
+export * from './constants.js'
+export * from './isActive.js'
+export * from './throttleAndDebounce.js'
+export * from './getScrollOffset.js'
+export * from './getNavLink.js'
diff --git a/themes/theme-next/src/client/utils/isActive.ts b/themes/theme-next/src/client/utils/isActive.ts
new file mode 100644
index 0000000000..f68949eebc
--- /dev/null
+++ b/themes/theme-next/src/client/utils/isActive.ts
@@ -0,0 +1,25 @@
+import { EXT_RE, HASH_RE, inBrowser } from './constants.js'
+
+export const normalize = (path: string): string =>
+ decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '')
+
+export const isActive = (
+ currentPath: string,
+ matchPath?: string,
+ asRegex = false,
+): boolean => {
+ if (matchPath === undefined) return false
+
+ // eslint-disable-next-line no-param-reassign
+ currentPath = normalize(`/${currentPath.replace(/^\//, '')}`)
+
+ if (asRegex) return new RegExp(matchPath).test(currentPath)
+
+ if (normalize(matchPath) !== currentPath) return false
+
+ const hashMatch = matchPath.match(HASH_RE)
+
+ if (hashMatch) return (inBrowser ? window.location.hash : '') === hashMatch[0]
+
+ return true
+}
diff --git a/themes/theme-next/src/client/utils/throttleAndDebounce.ts b/themes/theme-next/src/client/utils/throttleAndDebounce.ts
new file mode 100644
index 0000000000..99dd5ec941
--- /dev/null
+++ b/themes/theme-next/src/client/utils/throttleAndDebounce.ts
@@ -0,0 +1,19 @@
+export const throttleAndDebounce = (
+ fn: () => void,
+ delay: number,
+): (() => void) => {
+ let timeoutId: NodeJS.Timeout | null = null
+ let called = false
+
+ return () => {
+ if (timeoutId) clearTimeout(timeoutId)
+
+ if (!called) {
+ fn()
+ called = true
+ setTimeout(() => {
+ called = false
+ }, delay)
+ } else timeoutId = setTimeout(fn, delay)
+ }
+}
diff --git a/themes/theme-next/src/node/config/extendsBundlerOptions.ts b/themes/theme-next/src/node/config/extendsBundlerOptions.ts
new file mode 100644
index 0000000000..0b1cf3e0f5
--- /dev/null
+++ b/themes/theme-next/src/node/config/extendsBundlerOptions.ts
@@ -0,0 +1,10 @@
+import { addViteOptimizeDepsExclude } from '@vuepress/helper'
+import type { App, BundlerOptions } from 'vuepress'
+
+export const extendsBundlerOptions = (
+ bundlerOptions: BundlerOptions,
+ app: App,
+): void => {
+ // ensure theme alias is not optimized by Vite
+ addViteOptimizeDepsExclude(bundlerOptions, app, '@theme')
+}
diff --git a/themes/theme-next/src/node/config/index.ts b/themes/theme-next/src/node/config/index.ts
new file mode 100644
index 0000000000..c5f88a467d
--- /dev/null
+++ b/themes/theme-next/src/node/config/index.ts
@@ -0,0 +1,3 @@
+export * from './resolveThemeData.js'
+export * from './extendsBundlerOptions.js'
+export * from './templateBuildRenderer.js'
diff --git a/themes/theme-next/src/node/config/resolveThemeData.ts b/themes/theme-next/src/node/config/resolveThemeData.ts
new file mode 100644
index 0000000000..c7678ec624
--- /dev/null
+++ b/themes/theme-next/src/node/config/resolveThemeData.ts
@@ -0,0 +1,55 @@
+import { entries, fromEntries, getLocaleConfig } from '@vuepress/helper'
+import type { App } from 'vuepress'
+import type {
+ DefaultThemeLocaleData,
+ DefaultThemeLocaleOptions,
+} from '../../shared/index.js'
+import { LOCALES_OPTIONS } from '../locales/index.js'
+import { THEME_NAME } from '../utils/index.js'
+
+const EXCLUDE_LIST = ['locales', 'container']
+
+const FALLBACK_OPTIONS: DefaultThemeLocaleData = {
+ appearance: true,
+ // outline
+ outline: [2, 3],
+ aside: true,
+ scrollOffset: 134,
+ editLink: true,
+ lastUpdated: true,
+ contributors: true,
+ externalLinkIcon: true,
+}
+
+const resolveOptions = (
+ options: DefaultThemeLocaleData,
+): DefaultThemeLocaleData =>
+ fromEntries(entries(options).filter(([key]) => !EXCLUDE_LIST.includes(key)))
+
+export const resolveThemeData = (
+ app: App,
+ options: DefaultThemeLocaleOptions,
+): DefaultThemeLocaleOptions => {
+ const resolved = resolveOptions(options)
+
+ const themeData: DefaultThemeLocaleOptions = {
+ ...FALLBACK_OPTIONS,
+ ...resolved,
+ locales: getLocaleConfig({
+ app,
+ name: THEME_NAME,
+ default: LOCALES_OPTIONS,
+ config: fromEntries(
+ entries({
+ '/': {},
+ ...options.locales,
+ }).map(([locale, opt]) => [
+ locale,
+ { ...resolved, ...resolveOptions(opt) },
+ ]),
+ ),
+ }),
+ }
+
+ return themeData
+}
diff --git a/themes/theme-next/src/node/config/templateBuildRenderer.ts b/themes/theme-next/src/node/config/templateBuildRenderer.ts
new file mode 100644
index 0000000000..9cea199203
--- /dev/null
+++ b/themes/theme-next/src/node/config/templateBuildRenderer.ts
@@ -0,0 +1,37 @@
+import type { TemplateRendererContext } from 'vuepress/utils'
+import { templateRenderer } from 'vuepress/utils'
+import type { DefaultThemeData } from '../../shared/index.js'
+
+export const templateBuildRenderer = (
+ template: string,
+ context: TemplateRendererContext,
+ options: DefaultThemeData,
+): Promise | string => {
+ // eslint-disable-next-line no-useless-assignment
+ let temp = template.replace(
+ '',
+ ``,
+ )
+
+ if (options.appearance ?? true) {
+ const appearance =
+ typeof options.appearance === 'string' ? options.appearance : 'auto'
+ const script =
+ appearance === 'force-dark'
+ ? `document.documentElement.dataset.theme = 'dark'`
+ : `;(() => {
+ const preference = localStorage.getItem('vuepress-color-scheme') || '${appearance}'
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
+ const isDark = !preference || preference === 'auto' ? prefersDark : preference === 'dark'
+ document.documentElement.dataset.theme = isDark ? 'dark' : 'light'
+ })();`
+ temp = template.replace(
+ '',
+ ``,
+ )
+ } else {
+ temp = template.replace('', '')
+ }
+
+ return templateRenderer(temp, context)
+}
diff --git a/themes/theme-next/src/node/defaultTheme.ts b/themes/theme-next/src/node/defaultTheme.ts
new file mode 100644
index 0000000000..1e66770e71
--- /dev/null
+++ b/themes/theme-next/src/node/defaultTheme.ts
@@ -0,0 +1,94 @@
+import { extendsEditLinkPage } from '@vuepress/theme-helper'
+import { watch } from 'chokidar'
+import type { Page, Theme } from 'vuepress/core'
+import { fs, getDirname, path } from 'vuepress/utils'
+import type { DefaultThemePageData } from '../shared/index.js'
+import { extendsBundlerOptions, templateBuildRenderer } from './config/index.js'
+import type { DefaultThemeOptions } from './options.js'
+import { getPlugins } from './plugins/index.js'
+import { prepareSidebarData } from './prepare/index.js'
+import { THEME_NAME, logger } from './utils/index.js'
+
+const __dirname = getDirname(import.meta.url)
+
+export const defaultTheme =
+ ({
+ hostname,
+ themePlugins = {},
+ sidebarSorter,
+ ...localeOptions
+ }: DefaultThemeOptions): Theme =>
+ (app) => {
+ if (app.env.isDebug) {
+ logger.info('Plugin Options:', themePlugins)
+ }
+
+ return {
+ name: THEME_NAME,
+
+ templateBuild: path.resolve(__dirname, '../../templates/build.html'),
+ templateBuildRenderer: (bundleOptions, _app) =>
+ templateBuildRenderer(bundleOptions, _app, localeOptions),
+
+ alias: {
+ // use alias to make all components replaceable
+ ...Object.fromEntries(
+ fs
+ .readdirSync(path.resolve(__dirname, '../client/components'))
+ .filter((file) => file.endsWith('.vue'))
+ .map((file) => [
+ `@theme/${file}`,
+ path.resolve(__dirname, '../client/components', file),
+ ]),
+ ),
+ // use alias to make all composables replaceable
+ ...Object.fromEntries(
+ fs
+ .readdirSync(path.resolve(__dirname, '../client/composables'))
+ .filter((file) => file.endsWith('.js'))
+ .map((file) => [
+ `@theme/${file.substring(0, file.length - 3)}`,
+ path.resolve(__dirname, '../client/composables', file),
+ ]),
+ ),
+ },
+
+ clientConfigFile: path.resolve(__dirname, '../client/config.js'),
+
+ plugins: getPlugins(app, { hostname, themePlugins, localeOptions }),
+
+ onPrepared: (_app) =>
+ prepareSidebarData(_app, localeOptions, sidebarSorter),
+
+ onWatched: (_app, watchers) => {
+ const watcher = watch(
+ // This ensures the page is generated or updated
+ 'pages/**/*.vue',
+ {
+ cwd: _app.dir.temp(),
+ ignoreInitial: true,
+ },
+ )
+
+ watcher.on('add', () => {
+ void prepareSidebarData(app, localeOptions, sidebarSorter)
+ })
+ watcher.on('change', () => {
+ void prepareSidebarData(app, localeOptions, sidebarSorter)
+ })
+ watcher.on('unlink', () => {
+ void prepareSidebarData(app, localeOptions, sidebarSorter)
+ })
+
+ watchers.push(watcher)
+ },
+
+ extendsPage: (page: Page>) => {
+ extendsEditLinkPage(page)
+ // save title into route meta to generate navbar and sidebar
+ page.routeMeta.title = page.title
+ },
+
+ extendsBundlerOptions,
+ }
+ }
diff --git a/themes/theme-next/src/node/index.ts b/themes/theme-next/src/node/index.ts
new file mode 100644
index 0000000000..99de00840a
--- /dev/null
+++ b/themes/theme-next/src/node/index.ts
@@ -0,0 +1,4 @@
+export * from './defaultTheme.js'
+export * from './utils/index.js'
+export type * from './options.js'
+export type * from '../shared/index.js'
diff --git a/themes/theme-next/src/node/locales/de.ts b/themes/theme-next/src/node/locales/de.ts
new file mode 100644
index 0000000000..b1f1b1ca69
--- /dev/null
+++ b/themes/theme-next/src/node/locales/de.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const de: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Aussehen',
+ lightModeSwitchTitle: 'Zum hellen Thema wechseln',
+ darkModeSwitchTitle: 'Zum dunklen Thema wechseln',
+
+ selectLanguageText: 'Sprachen',
+ selectLanguageName: 'Deutsch',
+
+ // nav
+ returnToTopLabel: 'Zurück nach oben',
+ sidebarMenuLabel: 'Menü',
+ outlineTitle: 'Auf dieser Seite',
+
+ // page meta
+ editLinkText: 'Diese Seite bearbeiten',
+ lastUpdatedText: 'Zuletzt aktualisiert',
+ contributorsText: 'Mitwirkende',
+ docFooter: {
+ prev: 'Vorherige Seite',
+ next: 'Nächste Seite',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'SEITE NICHT GEFUNDEN',
+ quote:
+ 'Aber wenn du deine Richtung nicht änderst und weiter suchst, könntest du schließlich dort landen, wohin du unterwegs bist.',
+ linkLabel: 'zur Startseite',
+ linkText: 'Bring mich nach Hause',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/deAT.ts b/themes/theme-next/src/node/locales/deAT.ts
new file mode 100644
index 0000000000..e7866c0dad
--- /dev/null
+++ b/themes/theme-next/src/node/locales/deAT.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const deAT: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Aussehen',
+ lightModeSwitchTitle: 'Zum hellen Thema wechseln',
+ darkModeSwitchTitle: 'Zum dunklen Thema wechseln',
+
+ selectLanguageText: 'Sprachen',
+ selectLanguageName: 'Deutsch (Österreich)',
+
+ // nav
+ returnToTopLabel: 'Zurück nach oben',
+ sidebarMenuLabel: 'Menü',
+ outlineTitle: 'Auf dieser Seite',
+
+ // page meta
+ editLinkText: 'Diese Seite bearbeiten',
+ lastUpdatedText: 'Zuletzt aktualisiert',
+ contributorsText: 'Mitwirkende',
+ docFooter: {
+ prev: 'Vorherige Seite',
+ next: 'Nächste Seite',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'SEITE NICHT GEFUNDEN',
+ quote:
+ 'Aber wenn du deine Richtung nicht änderst und weiter suchst, könntest du schließlich dort landen, wohin du unterwegs bist.',
+ linkLabel: 'zur Startseite',
+ linkText: 'Bring mich nach Hause',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/en.ts b/themes/theme-next/src/node/locales/en.ts
new file mode 100644
index 0000000000..30d8d3ed5b
--- /dev/null
+++ b/themes/theme-next/src/node/locales/en.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const en: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Appearance',
+ lightModeSwitchTitle: 'Switch to light theme',
+ darkModeSwitchTitle: 'Switch to dark theme',
+
+ selectLanguageText: 'Languages',
+ selectLanguageName: 'English',
+
+ // nav
+ returnToTopLabel: 'Return to top',
+ sidebarMenuLabel: 'Menu',
+ outlineTitle: 'On This Page',
+
+ // page meta
+ editLinkText: 'Edit this page',
+ lastUpdatedText: 'Last Updated',
+ contributorsText: 'Contributors',
+ docFooter: {
+ prev: 'Previous Page',
+ next: 'Next Page',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'PAGE NOT FOUND',
+ quote:
+ "But if you don't change your direction, and if you keep looking, you may end up where you are heading.",
+ linkLabel: 'go to home',
+ linkText: 'Take me home',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/es.ts b/themes/theme-next/src/node/locales/es.ts
new file mode 100644
index 0000000000..da6476cbd5
--- /dev/null
+++ b/themes/theme-next/src/node/locales/es.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const es: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Apariencia',
+ lightModeSwitchTitle: 'Cambiar a tema claro',
+ darkModeSwitchTitle: 'Cambiar a tema oscuro',
+
+ selectLanguageText: 'Idiomas',
+ selectLanguageName: 'Español',
+
+ // nav
+ returnToTopLabel: 'Volver arriba',
+ sidebarMenuLabel: 'Menú',
+ outlineTitle: 'En esta página',
+
+ // page meta
+ editLinkText: 'Editar esta página',
+ lastUpdatedText: 'Última actualización',
+ contributorsText: 'Colaboradores',
+ docFooter: {
+ prev: 'Página anterior',
+ next: 'Página siguiente',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'PÁGINA NO ENCONTRADA',
+ quote:
+ 'Pero si no cambias de dirección y sigues buscando, podrías terminar donde te diriges.',
+ linkLabel: 'ir a la página de inicio',
+ linkText: 'Llévame a casa',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/fi.ts b/themes/theme-next/src/node/locales/fi.ts
new file mode 100644
index 0000000000..cc7cb3f7f0
--- /dev/null
+++ b/themes/theme-next/src/node/locales/fi.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const fi: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Ulkoasu',
+ lightModeSwitchTitle: 'Vaihda vaaleaan teemaan',
+ darkModeSwitchTitle: 'Vaihda tummaan teemaan',
+
+ selectLanguageText: 'Kielet',
+ selectLanguageName: 'Suomi',
+
+ // nav
+ returnToTopLabel: 'Palaa ylös',
+ sidebarMenuLabel: 'Valikko',
+ outlineTitle: 'Tällä sivulla',
+
+ // page meta
+ editLinkText: 'Muokkaa tätä sivua',
+ lastUpdatedText: 'Viimeksi päivitetty',
+ contributorsText: 'Osallistujat',
+ docFooter: {
+ prev: 'Edellinen sivu',
+ next: 'Seuraava sivu',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'SIVUA EI LÖYDY',
+ quote:
+ 'Mutta jos et vaihda suuntaasi ja jatkat etsimistä, saatat päätyä sinne, minne olet menossa.',
+ linkLabel: 'mene kotiin',
+ linkText: 'Vie minut kotiin',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/fr.ts b/themes/theme-next/src/node/locales/fr.ts
new file mode 100644
index 0000000000..c3c839263a
--- /dev/null
+++ b/themes/theme-next/src/node/locales/fr.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const fr: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Apparence',
+ lightModeSwitchTitle: 'Passer au thème clair',
+ darkModeSwitchTitle: 'Passer au thème sombre',
+
+ selectLanguageText: 'Langues',
+ selectLanguageName: 'Français',
+
+ // nav
+ returnToTopLabel: 'Retour en haut',
+ sidebarMenuLabel: 'Menu',
+ outlineTitle: 'Sur cette page',
+
+ // page meta
+ editLinkText: 'Modifier cette page',
+ lastUpdatedText: 'Dernière mise à jour',
+ contributorsText: 'Contributeurs',
+ docFooter: {
+ prev: 'Page précédente',
+ next: 'Page suivante',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'PAGE NON TROUVÉE',
+ quote:
+ 'Mais si vous ne changez pas de direction et continuez à chercher, vous pourriez finir là où vous vous dirigez.',
+ linkLabel: "aller à la page d'accueil",
+ linkText: 'Ramène-moi à la maison',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/id.ts b/themes/theme-next/src/node/locales/id.ts
new file mode 100644
index 0000000000..f4ac3b4950
--- /dev/null
+++ b/themes/theme-next/src/node/locales/id.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const id: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Tampilan',
+ lightModeSwitchTitle: 'Beralih ke tema terang',
+ darkModeSwitchTitle: 'Beralih ke tema gelap',
+
+ selectLanguageText: 'Bahasa',
+ selectLanguageName: 'Bahasa Indonesia',
+
+ // nav
+ returnToTopLabel: 'Kembali ke atas',
+ sidebarMenuLabel: 'Menu',
+ outlineTitle: 'Pada Halaman Ini',
+
+ // page meta
+ editLinkText: 'Edit halaman ini',
+ lastUpdatedText: 'Terakhir Diperbarui',
+ contributorsText: 'Kontributor',
+ docFooter: {
+ prev: 'Halaman Sebelumnya',
+ next: 'Halaman Berikutnya',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'HALAMAN TIDAK DITEMUKAN',
+ quote:
+ 'Tetapi jika Anda tidak mengubah arah Anda, dan jika Anda terus mencari, Anda mungkin akan berakhir di tempat yang Anda tuju.',
+ linkLabel: 'ke beranda',
+ linkText: 'Bawa saya ke beranda',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/index.ts b/themes/theme-next/src/node/locales/index.ts
new file mode 100644
index 0000000000..47a3fc0c98
--- /dev/null
+++ b/themes/theme-next/src/node/locales/index.ts
@@ -0,0 +1,42 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+import { de } from './de.js'
+import { deAT } from './deAT.js'
+import { en } from './en.js'
+import { es } from './es.js'
+import { fi } from './fi.js'
+import { fr } from './fr.js'
+import { id } from './id.js'
+import { ja } from './ja.js'
+import { ko } from './ko.js'
+import { nl } from './nl.js'
+import { pl } from './pl.js'
+import { pt } from './pt.js'
+import { ru } from './ru.js'
+import { sk } from './sk.js'
+import { tr } from './tr.js'
+import { uk } from './uk.js'
+import { vi } from './vi.js'
+import { zh } from './zh.js'
+import { zhTW } from './zhTW.js'
+
+export const LOCALES_OPTIONS: Record = {
+ '/en/': en, // en-US English
+ '/zh/': zh, // zh-CN 简体中文
+ '/zh-tw/': zhTW, // zh-TW 繁體中文
+ '/de/': de, // de-DE Deutsch
+ '/de-at/': deAT, // de-AT Deutsch (Austria)
+ '/ru/': ru, // ru-RU Русский
+ '/uk/': uk, // uk-UA Українська
+ '/vi/': vi, // vi-VN Tiếng Việt
+ '/pt/': pt, // pt-BR Portugês
+ '/pl/': pl, // pl-PL Polski
+ '/fr/': fr, // fr-FR Français
+ '/es/': es, // es-ES Español
+ '/sk/': sk, // sk-SK Slovensky
+ '/ja/': ja, // ja-JP 日本語
+ '/tr/': tr, // tr-TR Türkçe
+ '/ko/': ko, // ko-KR 한국어
+ '/fi/': fi, // fi-FI Suomi
+ '/id/': id, // id-ID Bahasa
+ '/nl/': nl, // nl-BE Nederlands
+}
diff --git a/themes/theme-next/src/node/locales/ja.ts b/themes/theme-next/src/node/locales/ja.ts
new file mode 100644
index 0000000000..effc6b09b8
--- /dev/null
+++ b/themes/theme-next/src/node/locales/ja.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const ja: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: '外観',
+ lightModeSwitchTitle: 'ライトテーマに切り替え',
+ darkModeSwitchTitle: 'ダークテーマに切り替え',
+
+ selectLanguageText: '言語',
+ selectLanguageName: '日本語',
+
+ // nav
+ returnToTopLabel: 'トップに戻る',
+ sidebarMenuLabel: 'メニュー',
+ outlineTitle: 'このページの内容',
+
+ // page meta
+ editLinkText: 'このページを編集',
+ lastUpdatedText: '最終更新',
+ contributorsText: '貢献者',
+ docFooter: {
+ prev: '前のページ',
+ next: '次のページ',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'ページが見つかりません',
+ quote:
+ 'しかし、あなたが方向を変えず、探し続けるなら、あなたが向かっている場所にたどり着くかもしれません。',
+ linkLabel: 'ホームに戻る',
+ linkText: 'ホームに戻る',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/ko.ts b/themes/theme-next/src/node/locales/ko.ts
new file mode 100644
index 0000000000..a43cfd31a0
--- /dev/null
+++ b/themes/theme-next/src/node/locales/ko.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const ko: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: '외관',
+ lightModeSwitchTitle: '밝은 테마로 전환',
+ darkModeSwitchTitle: '어두운 테마로 전환',
+
+ selectLanguageText: '언어',
+ selectLanguageName: '한국어',
+
+ // nav
+ returnToTopLabel: '맨 위로 돌아가기',
+ sidebarMenuLabel: '메뉴',
+ outlineTitle: '이 페이지에서',
+
+ // page meta
+ editLinkText: '이 페이지 편집',
+ lastUpdatedText: '마지막 업데이트',
+ contributorsText: '기여자',
+ docFooter: {
+ prev: '이전 페이지',
+ next: '다음 페이지',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: '페이지를 찾을 수 없음',
+ quote:
+ '하지만 방향을 바꾸지 않고 계속 찾는다면, 당신이 향하는 곳에 도달할 수 있을 것입니다.',
+ linkLabel: '홈으로 가기',
+ linkText: '홈으로 데려다 주세요',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/nl.ts b/themes/theme-next/src/node/locales/nl.ts
new file mode 100644
index 0000000000..f499c8b600
--- /dev/null
+++ b/themes/theme-next/src/node/locales/nl.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const nl: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Weergave',
+ lightModeSwitchTitle: 'Schakel naar licht thema',
+ darkModeSwitchTitle: 'Schakel naar donker thema',
+
+ selectLanguageText: 'Talen',
+ selectLanguageName: 'Nederlands',
+
+ // nav
+ returnToTopLabel: 'Terug naar boven',
+ sidebarMenuLabel: 'Menu',
+ outlineTitle: 'Op deze pagina',
+
+ // page meta
+ editLinkText: 'Bewerk deze pagina',
+ lastUpdatedText: 'Laatst bijgewerkt',
+ contributorsText: 'Bijdragers',
+ docFooter: {
+ prev: 'Vorige pagina',
+ next: 'Volgende pagina',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'PAGINA NIET GEVONDEN',
+ quote:
+ 'Maar als je je richting niet verandert en blijft zoeken, kan het zijn dat je uitkomt waar je naartoe gaat.',
+ linkLabel: 'ga naar home',
+ linkText: 'Breng me naar huis',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/pl.ts b/themes/theme-next/src/node/locales/pl.ts
new file mode 100644
index 0000000000..813cf9b791
--- /dev/null
+++ b/themes/theme-next/src/node/locales/pl.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const pl: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Wygląd',
+ lightModeSwitchTitle: 'Przełącz na jasny motyw',
+ darkModeSwitchTitle: 'Przełącz na ciemny motyw',
+
+ selectLanguageText: 'Języki',
+ selectLanguageName: 'Polski',
+
+ // nav
+ returnToTopLabel: 'Wróć na górę',
+ sidebarMenuLabel: 'Menu',
+ outlineTitle: 'Na tej stronie',
+
+ // page meta
+ editLinkText: 'Edytuj tę stronę',
+ lastUpdatedText: 'Ostatnia aktualizacja',
+ contributorsText: 'Współtwórcy',
+ docFooter: {
+ prev: 'Poprzednia strona',
+ next: 'Następna strona',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'STRONA NIE ZOSTAŁA ZNALEZIONA',
+ quote:
+ 'Ale jeśli nie zmienisz kierunku i będziesz dalej szukać, możesz skończyć tam, gdzie zmierzasz.',
+ linkLabel: 'przejdź do strony głównej',
+ linkText: 'Zabierz mnie do domu',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/pt.ts b/themes/theme-next/src/node/locales/pt.ts
new file mode 100644
index 0000000000..7659a1cf95
--- /dev/null
+++ b/themes/theme-next/src/node/locales/pt.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const pt: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Aparência',
+ lightModeSwitchTitle: 'Mudar para o tema claro',
+ darkModeSwitchTitle: 'Mudar para o tema escuro',
+
+ selectLanguageText: 'Idiomas',
+ selectLanguageName: 'Português (Brasil)',
+
+ // nav
+ returnToTopLabel: 'Voltar ao topo',
+ sidebarMenuLabel: 'Menu',
+ outlineTitle: 'Nesta Página',
+
+ // page meta
+ editLinkText: 'Editar esta página',
+ lastUpdatedText: 'Última atualização',
+ contributorsText: 'Colaboradores',
+ docFooter: {
+ prev: 'Página anterior',
+ next: 'Próxima página',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'PÁGINA NÃO ENCONTRADA',
+ quote:
+ 'Mas se você não mudar sua direção e continuar procurando, você pode acabar onde está indo.',
+ linkLabel: 'ir para a página inicial',
+ linkText: 'Leve-me para casa',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/ru.ts b/themes/theme-next/src/node/locales/ru.ts
new file mode 100644
index 0000000000..720af800f4
--- /dev/null
+++ b/themes/theme-next/src/node/locales/ru.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const ru: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Внешний вид',
+ lightModeSwitchTitle: 'Переключить на светлую тему',
+ darkModeSwitchTitle: 'Переключить на темную тему',
+
+ selectLanguageText: 'Языки',
+ selectLanguageName: 'Русский',
+
+ // nav
+ returnToTopLabel: 'Вернуться наверх',
+ sidebarMenuLabel: 'Меню',
+ outlineTitle: 'На этой странице',
+
+ // page meta
+ editLinkText: 'Редактировать эту страницу',
+ lastUpdatedText: 'Последнее обновление',
+ contributorsText: 'Участники',
+ docFooter: {
+ prev: 'Предыдущая страница',
+ next: 'Следующая страница',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'СТРАНИЦА НЕ НАЙДЕНА',
+ quote:
+ 'Но если вы не измените свое направление и продолжите искать, вы можете оказаться там, куда направляетесь.',
+ linkLabel: 'на главную',
+ linkText: 'Вернуться на главную',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/sk.ts b/themes/theme-next/src/node/locales/sk.ts
new file mode 100644
index 0000000000..0a09971991
--- /dev/null
+++ b/themes/theme-next/src/node/locales/sk.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const sk: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Vzhľad',
+ lightModeSwitchTitle: 'Prepnúť na svetlú tému',
+ darkModeSwitchTitle: 'Prepnúť na tmavú tému',
+
+ selectLanguageText: 'Jazyky',
+ selectLanguageName: 'Slovenčina',
+
+ // nav
+ returnToTopLabel: 'Návrat hore',
+ sidebarMenuLabel: 'Menu',
+ outlineTitle: 'Na tejto stránke',
+
+ // page meta
+ editLinkText: 'Upraviť túto stránku',
+ lastUpdatedText: 'Naposledy aktualizované',
+ contributorsText: 'Prispievatelia',
+ docFooter: {
+ prev: 'Predchádzajúca stránka',
+ next: 'Ďalšia stránka',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'STRÁNKA NENÁJDENÁ',
+ quote:
+ 'Ale ak nezmeníte svoj smer a budete pokračovať v hľadaní, môžete skončiť tam, kam smerujete.',
+ linkLabel: 'prejsť na domovskú stránku',
+ linkText: 'Vezmi ma domov',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/tr.ts b/themes/theme-next/src/node/locales/tr.ts
new file mode 100644
index 0000000000..4d072fae8b
--- /dev/null
+++ b/themes/theme-next/src/node/locales/tr.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const tr: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Görünüm',
+ lightModeSwitchTitle: 'Açık tema olarak değiştir',
+ darkModeSwitchTitle: 'Koyu tema olarak değiştir',
+
+ selectLanguageText: 'Diller',
+ selectLanguageName: 'Türkçe',
+
+ // nav
+ returnToTopLabel: 'Başa dön',
+ sidebarMenuLabel: 'Menü',
+ outlineTitle: 'Bu Sayfada',
+
+ // page meta
+ editLinkText: 'Bu sayfayı düzenle',
+ lastUpdatedText: 'Son Güncelleme',
+ contributorsText: 'Katkıda Bulunanlar',
+ docFooter: {
+ prev: 'Önceki Sayfa',
+ next: 'Sonraki Sayfa',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'SAYFA BULUNAMADI',
+ quote:
+ 'Ancak yönünü değiştirmezsen ve aramaya devam edersen, gideceğin yere ulaşabilirsin.',
+ linkLabel: 'ana sayfaya git',
+ linkText: 'Beni eve götür',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/uk.ts b/themes/theme-next/src/node/locales/uk.ts
new file mode 100644
index 0000000000..6da5b0ece2
--- /dev/null
+++ b/themes/theme-next/src/node/locales/uk.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const uk: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Вигляд',
+ lightModeSwitchTitle: 'Перемкнути на світлу тему',
+ darkModeSwitchTitle: 'Перемкнути на темну тему',
+
+ selectLanguageText: 'Мови',
+ selectLanguageName: 'Українська',
+
+ // nav
+ returnToTopLabel: 'Повернутися нагору',
+ sidebarMenuLabel: 'Меню',
+ outlineTitle: 'На цій сторінці',
+
+ // page meta
+ editLinkText: 'Редагувати цю сторінку',
+ lastUpdatedText: 'Останнє оновлення',
+ contributorsText: 'Автори',
+ docFooter: {
+ prev: 'Попередня сторінка',
+ next: 'Наступна сторінка',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'СТОРІНКА НЕ ЗНАЙДЕНА',
+ quote:
+ 'Але якщо ви не зміните свого напрямку і продовжите шукати, ви можете опинитися там, куди прямуєте.',
+ linkLabel: 'на головну',
+ linkText: 'Відвезти додому',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/vi.ts b/themes/theme-next/src/node/locales/vi.ts
new file mode 100644
index 0000000000..14a669e41a
--- /dev/null
+++ b/themes/theme-next/src/node/locales/vi.ts
@@ -0,0 +1,35 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const vi: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: 'Giao diện',
+ lightModeSwitchTitle: 'Chuyển sang chủ đề sáng',
+ darkModeSwitchTitle: 'Chuyển sang chủ đề tối',
+
+ selectLanguageText: 'Ngôn ngữ',
+ selectLanguageName: 'Tiếng Việt',
+
+ // nav
+ returnToTopLabel: 'Trở về đầu trang',
+ sidebarMenuLabel: 'Menu',
+ outlineTitle: 'Trên trang này',
+
+ // page meta
+ editLinkText: 'Chỉnh sửa trang này',
+ lastUpdatedText: 'Cập nhật lần cuối',
+ contributorsText: 'Người đóng góp',
+ docFooter: {
+ prev: 'Trang trước',
+ next: 'Trang tiếp theo',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: 'KHÔNG TÌM THẤY TRANG',
+ quote:
+ 'Nhưng nếu bạn không thay đổi hướng đi của mình và tiếp tục tìm kiếm, bạn có thể kết thúc ở nơi bạn đang hướng tới.',
+ linkLabel: 'về trang chủ',
+ linkText: 'Đưa tôi về nhà',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/zh.ts b/themes/theme-next/src/node/locales/zh.ts
new file mode 100644
index 0000000000..ee0bc47d88
--- /dev/null
+++ b/themes/theme-next/src/node/locales/zh.ts
@@ -0,0 +1,34 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const zh: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: '外观',
+ lightModeSwitchTitle: '切换到浅色主题',
+ darkModeSwitchTitle: '切换到深色主题',
+
+ selectLanguageText: '选择语言',
+ selectLanguageName: '简体中文',
+
+ // nav
+ returnToTopLabel: '返回顶部',
+ sidebarMenuLabel: '目录',
+ outlineTitle: '此页内容',
+
+ // page meta
+ editLinkText: '编辑此页',
+ lastUpdatedText: '最后更新于',
+ contributorsText: '贡献者',
+ docFooter: {
+ prev: '上一页',
+ next: '下一页',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: '页面未找到',
+ quote: '如果你不改变方向,继续寻找,最终可能会到达你正在前往的地方。',
+ linkLabel: '回到首页',
+ linkText: '回到首页',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/locales/zhTW.ts b/themes/theme-next/src/node/locales/zhTW.ts
new file mode 100644
index 0000000000..791608a266
--- /dev/null
+++ b/themes/theme-next/src/node/locales/zhTW.ts
@@ -0,0 +1,34 @@
+import type { DefaultThemeLocaleData } from '../../shared/index.js'
+
+export const zhTW: DefaultThemeLocaleData = {
+ // appearance
+ darkModeSwitchLabel: '外觀',
+ lightModeSwitchTitle: '切換到淺色主題',
+ darkModeSwitchTitle: '切換到深色主題',
+
+ selectLanguageText: '選擇語言',
+ selectLanguageName: '簡體中文',
+
+ // nav
+ returnToTopLabel: '返回頂部',
+ sidebarMenuLabel: '目錄',
+ outlineTitle: '此頁內容',
+
+ // page meta
+ editLinkText: '編輯此頁',
+ lastUpdatedText: '最後更新於',
+ contributorsText: '貢獻者',
+ docFooter: {
+ prev: '上一頁',
+ next: '下一頁',
+ },
+
+ // 404 page messages
+ notFound: {
+ title: '頁面未找到',
+ quote: '如果你不改變方向,繼續尋找,最終可能會到達你正在前往的地方。',
+ linkLabel: '回到首頁',
+ linkText: '回到首頁',
+ code: '404',
+ },
+}
diff --git a/themes/theme-next/src/node/options.ts b/themes/theme-next/src/node/options.ts
new file mode 100644
index 0000000000..ab5397a9e2
--- /dev/null
+++ b/themes/theme-next/src/node/options.ts
@@ -0,0 +1,91 @@
+import type { CopyCodePluginOptions } from '@vuepress/plugin-copy-code'
+import type { LinksCheckPluginOptions } from '@vuepress/plugin-links-check'
+import type { MarkdownHintPluginOptions } from '@vuepress/plugin-markdown-hint'
+import type { MarkdownTabPluginOptions } from '@vuepress/plugin-markdown-tab'
+import type { SeoPluginOptions } from '@vuepress/plugin-seo'
+import type { ShikiPluginOptions } from '@vuepress/plugin-shiki'
+import type { SitemapPluginOptions } from '@vuepress/plugin-sitemap'
+import type {
+ DefaultThemeLocaleOptions,
+ SidebarSorter,
+} from '../shared/index.js'
+
+export interface DefaultThemePluginsOptions {
+ /**
+ * Enable @vuepress/plugin-active-header-links or not
+ */
+ activeHeaderLinks?: boolean
+
+ /**
+ * Enable @vuepress/plugin-copy-code or not
+ */
+ copyCode?: CopyCodePluginOptions | boolean
+
+ /**
+ * Enable @vuepress/plugin-git or not
+ */
+ git?: boolean
+
+ /**
+ * Enable @vuepress/plugin-markdown-hint or not
+ */
+ hint?: MarkdownHintPluginOptions | boolean
+
+ /**
+ * Enable @vuepress/plugin-markdown-tab or not
+ */
+ tab?: MarkdownTabPluginOptions | boolean
+
+ /**
+ * Enable @vuepress/plugin-links-check or not
+ */
+ linksCheck?: LinksCheckPluginOptions | boolean
+
+ /**
+ * Enable @vuepress/plugin-photo-swipe or not
+ */
+ photoSwipe?: boolean
+
+ /**
+ * Enable @vuepress/plugin-nprogress or not
+ */
+ nprogress?: boolean
+
+ /**
+ * Enable @vuepress/plugin-shiki or not
+ */
+ shiki?: ShikiPluginOptions | boolean
+
+ /**
+ * Enable @vuepress/plugin-seo or not
+ */
+ seo?: Partial | boolean
+
+ /**
+ * Enable @vuepress/plugin-sitemap or not
+ */
+ sitemap?: Partial | boolean
+}
+
+export interface DefaultThemeOptions extends DefaultThemeLocaleOptions {
+ /**
+ * deployed hostname
+ */
+ hostname?: string
+
+ /**
+ * To avoid confusion with the root `plugins` option,
+ * we use `themePlugins`
+ *
+ * 为避免与根`plugins`选项混淆,我们使用`themePlugins`。
+ */
+ themePlugins?: DefaultThemePluginsOptions
+
+ /**
+ * The sidebar sorters. only `'structure'` optional
+ *
+ * 侧边栏排序。仅支持 `'structure'`
+ *
+ */
+ sidebarSorter?: SidebarSorter
+}
diff --git a/themes/theme-next/src/node/plugins/getPlugins.ts b/themes/theme-next/src/node/plugins/getPlugins.ts
new file mode 100644
index 0000000000..8947d26890
--- /dev/null
+++ b/themes/theme-next/src/node/plugins/getPlugins.ts
@@ -0,0 +1,147 @@
+import { activeHeaderLinksPlugin } from '@vuepress/plugin-active-header-links'
+import { copyCodePlugin } from '@vuepress/plugin-copy-code'
+import { gitPlugin } from '@vuepress/plugin-git'
+import { linksCheckPlugin } from '@vuepress/plugin-links-check'
+import { markdownHintPlugin } from '@vuepress/plugin-markdown-hint'
+import { markdownTabPlugin } from '@vuepress/plugin-markdown-tab'
+import { nprogressPlugin } from '@vuepress/plugin-nprogress'
+import { photoSwipePlugin } from '@vuepress/plugin-photo-swipe'
+import type { SeoPluginOptions } from '@vuepress/plugin-seo'
+import { seoPlugin } from '@vuepress/plugin-seo'
+import type { ShikiPluginOptions } from '@vuepress/plugin-shiki'
+import { shikiPlugin } from '@vuepress/plugin-shiki'
+import type { SitemapPluginOptions } from '@vuepress/plugin-sitemap'
+import { sitemapPlugin } from '@vuepress/plugin-sitemap'
+import { themeDataPlugin } from '@vuepress/plugin-theme-data'
+import type { App, PluginConfig } from 'vuepress/core'
+import { isPlainObject } from 'vuepress/shared'
+import type { DefaultThemeLocaleOptions } from '../../shared/index.js'
+import { resolveThemeData } from '../config/index.js'
+import type { DefaultThemePluginsOptions } from '../options.js'
+
+interface PluginsOptions {
+ hostname?: string
+ themePlugins: DefaultThemePluginsOptions
+ localeOptions: DefaultThemeLocaleOptions
+}
+
+export const getPlugins = (
+ app: App,
+ { hostname, themePlugins, localeOptions }: PluginsOptions,
+): PluginConfig => {
+ const plugins: PluginConfig = []
+ const isProd = app.env.isBuild
+
+ if (themePlugins.activeHeaderLinks !== false) {
+ plugins.push(
+ activeHeaderLinksPlugin({
+ headerLinkSelector: 'a.outline-link',
+ headerAnchorSelector: '.header-anchor',
+ // should greater than page transition duration
+ delay: 300,
+ }),
+ )
+ }
+
+ if (themePlugins.tab !== false) {
+ plugins.push(
+ markdownTabPlugin({
+ ...(isPlainObject(themePlugins.tab)
+ ? themePlugins.tab
+ : { tabs: true, codeTabs: true }),
+ }),
+ )
+ }
+
+ if (themePlugins.copyCode !== false) {
+ plugins.push(
+ copyCodePlugin({
+ selector: '.vp-content div[class*="language-"] pre',
+ ...(isPlainObject(themePlugins.copyCode) ? themePlugins.copyCode : {}),
+ }),
+ )
+ }
+
+ if (themePlugins.hint !== false) {
+ plugins.push(
+ markdownHintPlugin({
+ hint: true,
+ alert: true,
+ ...(isPlainObject(themePlugins.hint) ? themePlugins.hint : {}),
+ }),
+ )
+ }
+
+ if (themePlugins.git !== false) {
+ plugins.push(
+ gitPlugin({
+ createdTime: false,
+ updatedTime: localeOptions.lastUpdated !== false,
+ contributors: localeOptions.contributors !== false,
+ }),
+ )
+ }
+
+ if (themePlugins.linksCheck !== false) {
+ plugins.push(
+ linksCheckPlugin(
+ isPlainObject(themePlugins.linksCheck) ? themePlugins.linksCheck : {},
+ ),
+ )
+ }
+
+ if (themePlugins.photoSwipe !== false) {
+ plugins.push(
+ photoSwipePlugin({
+ selector: '.vp-content > img, .vp-content :not(a) > img',
+ // should greater than page transition duration
+ delay: 300,
+ }),
+ )
+ }
+
+ if (themePlugins.nprogress !== false) {
+ plugins.push(nprogressPlugin())
+ }
+
+ if (themePlugins.shiki !== false) {
+ const shikiOptions = isPlainObject(themePlugins.shiki)
+ ? themePlugins.shiki
+ : {}
+ const defaultOptions: ShikiPluginOptions = {
+ notationDiff: true,
+ notationErrorLevel: true,
+ notationFocus: true,
+ notationHighlight: true,
+ themes: { light: 'github-light', dark: 'github-dark' },
+ }
+ if ('theme' in shikiOptions) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
+ delete (defaultOptions as any).themes
+ }
+ plugins.push(shikiPlugin({ ...defaultOptions, ...shikiOptions }))
+ }
+
+ if ((themePlugins.seo ?? isProd) !== false) {
+ const seoOptions = isPlainObject(themePlugins.seo) ? themePlugins.seo : {}
+ seoOptions.hostname ||= hostname
+ if (seoOptions.hostname)
+ plugins.push(seoPlugin(seoOptions as SeoPluginOptions))
+ }
+
+ if ((themePlugins.sitemap ?? isProd) !== false) {
+ const sitemapOptions = isPlainObject(themePlugins.sitemap)
+ ? themePlugins.sitemap
+ : {}
+ sitemapOptions.hostname ||= hostname
+ if (sitemapOptions.hostname)
+ plugins.push(sitemapPlugin(sitemapOptions as SitemapPluginOptions))
+ }
+
+ // @vuepress/plugin-theme-data
+ plugins.push(
+ themeDataPlugin({ themeData: resolveThemeData(app, localeOptions) }),
+ )
+
+ return plugins
+}
diff --git a/themes/theme-next/src/node/plugins/index.ts b/themes/theme-next/src/node/plugins/index.ts
new file mode 100644
index 0000000000..62d0b452eb
--- /dev/null
+++ b/themes/theme-next/src/node/plugins/index.ts
@@ -0,0 +1 @@
+export * from './getPlugins.js'
diff --git a/themes/theme-next/src/node/prepare/index.ts b/themes/theme-next/src/node/prepare/index.ts
new file mode 100644
index 0000000000..493d6738ad
--- /dev/null
+++ b/themes/theme-next/src/node/prepare/index.ts
@@ -0,0 +1 @@
+export * from './sidebar/index.js'
diff --git a/themes/theme-next/src/node/prepare/sidebar/getSidebarInfo.ts b/themes/theme-next/src/node/prepare/sidebar/getSidebarInfo.ts
new file mode 100644
index 0000000000..264fc0820a
--- /dev/null
+++ b/themes/theme-next/src/node/prepare/sidebar/getSidebarInfo.ts
@@ -0,0 +1,194 @@
+import { startsWith } from '@vuepress/helper'
+import type { Page } from 'vuepress/core'
+import { sanitizeFileName } from 'vuepress/utils'
+import type {
+ DefaultThemeNormalPageFrontmatter,
+ DefaultThemePageData,
+ SidebarDirInfo,
+ SidebarFileInfo,
+ SidebarInfo,
+ SidebarSorterFunction,
+} from '../../../shared/index.js'
+import { getTitleFromFilename } from '../../utils/index.js'
+import type { StructureInfo } from './getStructureInfo.js'
+import { getStructureInfo } from './getStructureInfo.js'
+
+export interface FileInfo {
+ type: 'file'
+ filename: string
+ path: string
+}
+
+export interface DirInfo {
+ type: 'dir'
+ dirname: string
+ path: string
+ items: (DirInfo | FileInfo)[]
+}
+
+export interface ThemeSidebarInfoOptions {
+ pages: Page[]
+ sorters: SidebarSorterFunction[]
+ scope: string
+}
+
+/**
+ * @private
+ */
+const getSidebarChildrenInfo = (
+ { scope, pages, sorters }: ThemeSidebarInfoOptions,
+ children: StructureInfo[],
+): SidebarInfo[] =>
+ children
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ .map((item) => getSidebarInfoFromStructure({ pages, scope, sorters }, item))
+ .filter((item): item is SidebarInfo => item !== null)
+ // sort items
+ .sort((infoA, infoB) => {
+ for (const sorter of sorters) {
+ const result = sorter(infoA, infoB)
+
+ if (result !== 0) return result
+ }
+
+ return 0
+ })
+
+/**
+ * @private
+ */
+const getSidebarInfoFromStructure = (
+ { scope, pages, sorters }: ThemeSidebarInfoOptions,
+ info: StructureInfo,
+): SidebarInfo | null => {
+ // handle file
+ if (info.type === 'file') {
+ const page = pages.find(
+ ({ filePathRelative }) => filePathRelative === `${scope}${info.path}`,
+ )! as Page
+
+ if (page.frontmatter.index === false) return null
+
+ const fileInfo: SidebarFileInfo = {
+ type: 'file',
+ filename: info.filename,
+
+ title: page.frontmatter.title ?? page.title,
+ order: page.frontmatter.order ?? null,
+ path: decodeURI(page.path) === page.pathInferred ? null : page.path,
+
+ frontmatter: page.frontmatter,
+ pageData: page.data,
+ }
+
+ return fileInfo
+ }
+
+ // handle dir
+
+ // performance improvements
+ const relatedPages = pages.filter(({ filePathRelative }) =>
+ startsWith(filePathRelative, `${scope}${info.path}/`),
+ )
+ const READMEFile = info.children.find(
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ (info) =>
+ info.type === 'file' && info.filename.toLowerCase() === 'readme.md',
+ )
+
+ if (READMEFile) {
+ const readmePage = relatedPages.find(
+ ({ filePathRelative }) =>
+ filePathRelative === `${scope}${READMEFile.path}`,
+ )! as Page
+
+ // get dir information
+ const dirOptions = readmePage.frontmatter.dir
+
+ const title = dirOptions?.text ?? readmePage.title
+ const collapsible = dirOptions?.collapsible ?? true
+
+ if (dirOptions?.index === false) return null
+
+ const dirInfo: SidebarDirInfo = {
+ type: 'dir',
+ dirname: info.dirname,
+ children: getSidebarChildrenInfo(
+ { pages: relatedPages, scope, sorters },
+ dirOptions?.link
+ ? // filter README.md
+ info.children.filter(
+ (item) =>
+ item.type !== 'file' ||
+ item.filename.toLowerCase() !== 'readme.md',
+ )
+ : info.children,
+ ),
+
+ title,
+ order: dirOptions?.order ?? null,
+ // group information
+ groupInfo: {
+ ...(collapsible ? { collapsible } : {}),
+ ...(dirOptions?.link
+ ? {
+ link:
+ readmePage.pathInferred === decodeURI(readmePage.path)
+ ? `${sanitizeFileName(info.dirname)}/`
+ : readmePage.path,
+ }
+ : {}),
+ },
+
+ frontmatter: readmePage.frontmatter,
+ pageData: readmePage.data,
+ }
+
+ return dirInfo
+ }
+
+ const dirInfo: SidebarDirInfo = {
+ type: 'dir',
+ dirname: info.dirname,
+ children: getSidebarChildrenInfo(
+ { pages: relatedPages, scope, sorters },
+ info.children,
+ ),
+
+ title: getTitleFromFilename(info.dirname),
+ order: null,
+
+ // group information
+ groupInfo: {
+ collapsible: true,
+ },
+
+ frontmatter: null,
+ pageData: null,
+ }
+
+ return dirInfo
+}
+
+/**
+ * @private
+ */
+export const getSidebarInfo = ({
+ pages,
+ sorters,
+ scope,
+}: ThemeSidebarInfoOptions): // base = ""
+SidebarInfo[] =>
+ getStructureInfo(pages, scope)
+ .map((info) => getSidebarInfoFromStructure({ scope, pages, sorters }, info))
+ .filter((item): item is SidebarInfo => item !== null)
+ // sort items
+ .sort((infoA, infoB) => {
+ for (const sorter of sorters) {
+ const result = sorter(infoA, infoB)
+
+ if (result !== 0) return result
+ }
+
+ return 0
+ })
diff --git a/themes/theme-next/src/node/prepare/sidebar/getSidebarSorter.ts b/themes/theme-next/src/node/prepare/sidebar/getSidebarSorter.ts
new file mode 100644
index 0000000000..8b4b5e20b5
--- /dev/null
+++ b/themes/theme-next/src/node/prepare/sidebar/getSidebarSorter.ts
@@ -0,0 +1,153 @@
+import { isArray, isFunction, isString, keys } from '@vuepress/helper'
+import type {
+ SidebarInfo,
+ SidebarSorter,
+ SidebarSorterFunction,
+} from '../../../shared/index.js'
+
+export const sidebarReadmeSorter = (
+ infoA: SidebarInfo,
+ infoB: SidebarInfo,
+): number => {
+ if (infoA.type === 'file' && infoA.filename.toLowerCase() === 'readme.md')
+ return -1
+
+ if (infoB.type === 'file' && infoB.filename.toLowerCase() === 'readme.md')
+ return 1
+
+ return 0
+}
+
+export const sidebarOrderSorter = (
+ infoA: SidebarInfo,
+ infoB: SidebarInfo,
+): number => {
+ // itemA order is absent
+ if (infoA.order === null) {
+ // both item do not have orders
+ if (infoB.order === null) return 0
+
+ // itemA order is absent while itemB order is present
+ return infoB.order
+ }
+
+ // itemA order is present while itemB order is absent
+ if (infoB.order === null) return -infoA.order
+
+ // now we are sure both order exist
+
+ // itemA order is positive
+ if (infoA.order > 0) {
+ // both order are negative
+ if (infoB.order > 0) return infoA.order - infoB.order
+
+ // infoA.order is positive while infoB.order is negative
+ return -1
+ }
+
+ // both order are negative
+ if (infoB.order < 0) return infoA.order - infoB.order
+
+ // infoA.order is negative while infoB.order is positive
+ return 1
+}
+
+export const sidebarDateSorter = (
+ infoA: SidebarInfo,
+ infoB: SidebarInfo,
+): number => {
+ if (infoA.frontmatter?.date instanceof Date) {
+ if (infoB.frontmatter?.date instanceof Date)
+ return infoA.frontmatter.date.getTime() - infoB.frontmatter.date.getTime()
+
+ return -1
+ }
+
+ if (infoB.frontmatter?.date instanceof Date) return 1
+
+ return 0
+}
+
+export const sidebarDateDescSorter = (
+ infoA: SidebarInfo,
+ infoB: SidebarInfo,
+): number => {
+ if (infoA.frontmatter?.date instanceof Date) {
+ if (infoB.frontmatter?.date instanceof Date)
+ return infoB.frontmatter.date.getTime() - infoA.frontmatter.date.getTime()
+
+ return -1
+ }
+
+ if (infoB.frontmatter?.date instanceof Date) return 1
+
+ return 0
+}
+
+const getFilename = (info: SidebarInfo): string =>
+ info.type === 'file' ? info.filename.replace(/\.md$/u, '') : info.dirname
+
+export const sidebarFilenameSorter = (
+ infoA: SidebarInfo,
+ infoB: SidebarInfo,
+): number => {
+ const result = getFilename(infoA).localeCompare(
+ getFilename(infoB),
+ undefined,
+ {
+ numeric: true,
+ sensitivity: 'accent',
+ },
+ )
+
+ if (result !== 0) return result
+
+ if (infoA.type === 'file' && infoB.type === 'dir') return -1
+ if (infoA.type === 'dir' && infoB.type === 'file') return 1
+
+ return 0
+}
+
+export const sidebarTitleSorter = (
+ infoA: SidebarInfo,
+ infoB: SidebarInfo,
+): number =>
+ infoA.title.localeCompare(infoB.title, undefined, {
+ numeric: true,
+ })
+
+const sortKeyMap: Record = {
+ 'readme': sidebarReadmeSorter,
+ 'order': sidebarOrderSorter,
+ 'date': sidebarDateSorter,
+ 'date-desc': sidebarDateDescSorter,
+ 'filename': sidebarFilenameSorter,
+ 'title': sidebarTitleSorter,
+}
+
+const availableKeywords = keys(sortKeyMap)
+
+/** @private */
+export const getSidebarSorter = (
+ sorter?: SidebarSorter,
+): SidebarSorterFunction[] => {
+ if (isString(sorter) && availableKeywords.includes(sorter))
+ return [sortKeyMap[sorter]]
+
+ if (isFunction(sorter)) return [sorter]
+
+ if (isArray(sorter)) {
+ const result = sorter
+ .map((item: unknown) => (isString(item) ? sortKeyMap[item] : item))
+ .filter((item) => isFunction(item))
+
+ if (result.length) return result as SidebarSorterFunction[]
+ }
+
+ return [
+ sidebarReadmeSorter,
+ sidebarOrderSorter,
+ sidebarTitleSorter,
+ sidebarFilenameSorter,
+ ]
+}
diff --git a/themes/theme-next/src/node/prepare/sidebar/getStructureInfo.ts b/themes/theme-next/src/node/prepare/sidebar/getStructureInfo.ts
new file mode 100644
index 0000000000..8aa752ce8f
--- /dev/null
+++ b/themes/theme-next/src/node/prepare/sidebar/getStructureInfo.ts
@@ -0,0 +1,90 @@
+import { startsWith } from '@vuepress/helper'
+import type { Page } from 'vuepress/core'
+import { path } from 'vuepress/utils'
+
+export interface FileInfo {
+ type: 'file'
+ filename: string
+ path: string
+}
+
+export interface DirInfo {
+ type: 'dir'
+ dirname: string
+ path: string
+ children: StructureInfo[]
+}
+
+export type StructureInfo = DirInfo | FileInfo
+
+/**
+ * @private
+ */
+export const getStructureInfo = (
+ pages: Page[],
+ scope: string,
+): StructureInfo[] => {
+ const relatedPages = pages.filter(
+ ({ filePathRelative, pathLocale }) =>
+ // Generated from file and inside current scope
+ startsWith(filePathRelative, scope) &&
+ // Filter other locales in root dir
+ (scope !== '' || pathLocale === '/'),
+ )
+
+ const sortedPages = relatedPages
+ // Sort pages
+ .sort(
+ (
+ { filePathRelative: filePathRelative1 },
+ { filePathRelative: filePathRelative2 },
+ ) =>
+ filePathRelative1!.localeCompare(filePathRelative2!, undefined, {
+ numeric: true,
+ sensitivity: 'accent',
+ }),
+ )
+
+ const structure: StructureInfo[] = []
+
+ sortedPages.forEach((page) => {
+ const relativePath = path.relative(scope, page.filePathRelative!)
+ const filename = path.basename(relativePath)
+
+ let currentDir = structure
+ const levels = relativePath.split('/')
+
+ levels.forEach((level, index) => {
+ // Already gets filename
+ if (index === levels.length - 1) {
+ currentDir.push({ type: 'file', filename, path: relativePath })
+ }
+ // Still generating dir
+ else {
+ const result = currentDir.find(
+ (item): item is DirInfo =>
+ item.type === 'dir' && item.dirname === level,
+ )
+
+ if (result) {
+ currentDir = result.children
+ }
+ // We shall create this dir
+ else {
+ const dirInfo: DirInfo = {
+ type: 'dir',
+ dirname: level,
+ path: levels.slice(0, index + 1).join('/'),
+ children: [],
+ }
+
+ currentDir.push(dirInfo)
+
+ currentDir = dirInfo.children
+ }
+ }
+ })
+ })
+
+ return structure
+}
diff --git a/themes/theme-next/src/node/prepare/sidebar/index.ts b/themes/theme-next/src/node/prepare/sidebar/index.ts
new file mode 100644
index 0000000000..89d944731b
--- /dev/null
+++ b/themes/theme-next/src/node/prepare/sidebar/index.ts
@@ -0,0 +1,3 @@
+export * from './prepareSidebarData.js'
+export * from './getSidebarInfo.js'
+export * from './getSidebarSorter.js'
diff --git a/themes/theme-next/src/node/prepare/sidebar/prepareSidebarData.ts b/themes/theme-next/src/node/prepare/sidebar/prepareSidebarData.ts
new file mode 100644
index 0000000000..00dd82a695
--- /dev/null
+++ b/themes/theme-next/src/node/prepare/sidebar/prepareSidebarData.ts
@@ -0,0 +1,170 @@
+import {
+ ensureEndingSlash,
+ ensureLeadingSlash,
+ entries,
+ isArray,
+ isPlainObject,
+ removeLeadingSlash,
+} from '@vuepress/helper'
+import type { App } from 'vuepress/core'
+import type {
+ DefaultThemeLocaleOptions,
+ Sidebar,
+ SidebarInfo,
+ SidebarItem,
+ SidebarSorter,
+ SidebarSorterFunction,
+} from '../../../shared/index.js'
+import type { ResolvedSidebarItem } from '../../../shared/resolved/sidebar.js'
+import { normalizeLink } from '../../utils/index.js'
+import { getSidebarInfo } from './getSidebarInfo.js'
+import { getSidebarSorter } from './getSidebarSorter.js'
+
+const HMR_CODE = `
+if (import.meta.webpackHot) {
+ import.meta.webpackHot.accept()
+ if (__VUE_HMR_RUNTIME__.updateSidebarData) {
+ __VUE_HMR_RUNTIME__.updateSidebarData(sidebarData)
+ }
+}
+
+if (import.meta.hot) {
+ import.meta.hot.accept(({ sidebarData }) => {
+ __VUE_HMR_RUNTIME__.updateSidebarData(sidebarData)
+ })
+}
+`
+
+export const prepareSidebarData = async (
+ app: App,
+ localesOptions: DefaultThemeLocaleOptions,
+ sorters?: SidebarSorter,
+): Promise => {
+ const locales: Record = {}
+
+ entries(localesOptions.locales ?? {}).forEach(([localePath, locale]) => {
+ locales[localePath] = locale.sidebar
+ })
+
+ if (!locales['/']) {
+ locales['/'] = localesOptions.sidebar
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ const sidebarData = getSidebarData(app, locales, sorters)
+
+ let content = `\
+export const sidebarData = ${JSON.stringify(sidebarData)}
+`
+
+ if (app.env.isDev) {
+ content += HMR_CODE
+ }
+
+ await app.writeTemp('internal/sidebar.js', content)
+}
+
+export const getSidebarData = (
+ app: App,
+ locales: Record,
+ sorter?: SidebarSorter,
+): Sidebar => {
+ const structureDir: string[] = []
+
+ const resolved: Sidebar = {}
+ const sorters = getSidebarSorter(sorter)
+
+ entries(locales).forEach(([localePath, sidebar]) => {
+ if (!sidebar) return
+
+ if (isArray(sidebar)) {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ structureDir.push(...findStructureList(sidebar, localePath))
+ } else if (isPlainObject(sidebar)) {
+ entries(sidebar).forEach(([dirname, config]) => {
+ const prefix = normalizeLink(localePath, dirname)
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+ config === 'structure'
+ ? structureDir.push(prefix)
+ : isArray(config)
+ ? // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ structureDir.push(...findStructureList(config, prefix))
+ : config.items === 'structure'
+ ? structureDir.push(normalizeLink(prefix, config.prefix))
+ : structureDir.push(
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ ...findStructureList(
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ config.items ?? [],
+ normalizeLink(prefix, config.prefix),
+ ),
+ )
+ })
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ } else if (sidebar === 'structure') {
+ structureDir.push(localePath)
+ }
+ })
+
+ structureDir.forEach((dirname) => {
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ resolved[dirname] = getSidebarItemsFromStructure(app, sorters, dirname)
+ })
+
+ return resolved
+}
+
+const findStructureList = (
+ sidebar: (SidebarItem | string)[],
+ prefix = '',
+): string[] => {
+ const list: string[] = []
+ if (!sidebar.length) return list
+
+ sidebar.forEach((item) => {
+ if (isPlainObject(item)) {
+ const nextPrefix = normalizeLink(prefix, item.prefix)
+ if (item.items === 'structure') {
+ list.push(nextPrefix)
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+ item.items?.length &&
+ list.push(...findStructureList(item.items, nextPrefix))
+ }
+ }
+ })
+
+ return list
+}
+
+const getSidebarItemsFromStructure = (
+ app: App,
+ sorters: SidebarSorterFunction[],
+ dirname: string,
+): ResolvedSidebarItem[] => {
+ const infos = getSidebarInfo({
+ pages: app.pages,
+ sorters,
+ scope: removeLeadingSlash(ensureEndingSlash(dirname)),
+ })
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ return getSidebarItemsFromInfos(infos)
+}
+
+const getSidebarItemsFromInfos = (
+ infos: SidebarInfo[],
+): ResolvedSidebarItem[] =>
+ infos.map((info) => {
+ if (info.type === 'file') {
+ return {
+ link:
+ info.path || ensureLeadingSlash(info.pageData.filePathRelative || ''),
+ text: info.title,
+ }
+ }
+ return {
+ text: info.title,
+ ...info.groupInfo,
+ items: getSidebarItemsFromInfos(info.children),
+ }
+ })
diff --git a/themes/theme-next/src/node/utils/constants.ts b/themes/theme-next/src/node/utils/constants.ts
new file mode 100644
index 0000000000..48d921c812
--- /dev/null
+++ b/themes/theme-next/src/node/utils/constants.ts
@@ -0,0 +1 @@
+export const THEME_NAME = '@vuepress/theme-default'
diff --git a/themes/theme-next/src/node/utils/getTitleFromFilename.ts b/themes/theme-next/src/node/utils/getTitleFromFilename.ts
new file mode 100644
index 0000000000..f8b5892a42
--- /dev/null
+++ b/themes/theme-next/src/node/utils/getTitleFromFilename.ts
@@ -0,0 +1,37 @@
+const EN_PREPOSITION = [
+ 'and',
+ 'or',
+ 'in',
+ 'on',
+ 'with',
+ 'by',
+ 'for',
+ 'at',
+ 'about',
+ 'under',
+ 'of',
+ 'to',
+ 'the',
+ 'into',
+]
+
+export const getTitleFromFilename = (filename: string): string => {
+ const words = filename
+ .replace(/[-_]/gu, ' ')
+ .replace(
+ /(^|[^A-Z])([A-Z])/gu,
+ (_all, match1: string, match2: string) =>
+ `${match1} ${match2.toLowerCase()}`,
+ )
+ .replace(/ +/gu, ' ')
+ .trim()
+ .split(' ')
+
+ return words
+ .map((word, index) =>
+ EN_PREPOSITION.includes(word) && index !== 0
+ ? word
+ : word.charAt(0).toUpperCase() + word.slice(1),
+ )
+ .join(' ')
+}
diff --git a/themes/theme-next/src/node/utils/index.ts b/themes/theme-next/src/node/utils/index.ts
new file mode 100644
index 0000000000..ea8033504e
--- /dev/null
+++ b/themes/theme-next/src/node/utils/index.ts
@@ -0,0 +1,4 @@
+export * from './constants.js'
+export * from './getTitleFromFilename.js'
+export * from './logger.js'
+export * from './normalizeLink.js'
diff --git a/themes/theme-next/src/node/utils/logger.ts b/themes/theme-next/src/node/utils/logger.ts
new file mode 100644
index 0000000000..f2d5b5ce57
--- /dev/null
+++ b/themes/theme-next/src/node/utils/logger.ts
@@ -0,0 +1,4 @@
+import { Logger } from '@vuepress/helper'
+import { THEME_NAME } from './constants.js'
+
+export const logger = new Logger(THEME_NAME)
diff --git a/themes/theme-next/src/node/utils/normalizeLink.ts b/themes/theme-next/src/node/utils/normalizeLink.ts
new file mode 100644
index 0000000000..13ca28f2ba
--- /dev/null
+++ b/themes/theme-next/src/node/utils/normalizeLink.ts
@@ -0,0 +1,10 @@
+import {
+ ensureLeadingSlash,
+ isLinkAbsolute,
+ isLinkWithProtocol,
+} from '@vuepress/helper'
+
+export const normalizeLink = (base: string, link = ''): string =>
+ isLinkAbsolute(link) || isLinkWithProtocol(link)
+ ? link
+ : ensureLeadingSlash(`${base}/${link}/`.replace(/\/+/g, '/'))
diff --git a/themes/theme-next/src/shared/index.ts b/themes/theme-next/src/shared/index.ts
new file mode 100644
index 0000000000..b11cb09f8d
--- /dev/null
+++ b/themes/theme-next/src/shared/index.ts
@@ -0,0 +1,5 @@
+export type * from './shared.js'
+export type * from './navbar.js'
+export type * from './sidebar.js'
+export type * from './page.js'
+export type * from './locales.js'
diff --git a/themes/theme-next/src/shared/locales.ts b/themes/theme-next/src/shared/locales.ts
new file mode 100644
index 0000000000..1478f28c93
--- /dev/null
+++ b/themes/theme-next/src/shared/locales.ts
@@ -0,0 +1,361 @@
+import type { ThemeData } from '@vuepress/plugin-theme-data'
+import type {
+ ContributorThemeData,
+ EditLinkThemeData,
+} from '@vuepress/theme-helper/shared'
+import type { UseDarkOptions } from '@vueuse/core'
+import type { LocaleData } from 'vuepress/shared'
+import type { NavItem } from './navbar.js'
+import type { DefaultThemeImage, SocialLink } from './shared.js'
+import type { Sidebar } from './sidebar.js'
+
+export type DefaultThemeLocaleOptions = DefaultThemeData
+
+export type DefaultThemeData = ThemeData
+
+export interface DefaultThemeLocaleData
+ extends LocaleData,
+ ContributorThemeData,
+ EditLinkThemeData {
+ /**
+ * Custom site title in navbar. If the value is undefined,
+ * `userConfig.title` will be used.
+ *
+ * 自定义导航栏中的站点标题。如果值为 `undefined`,将使用 `userConfig.title`
+ */
+ siteTitle?: string | false
+
+ /**
+ * The logo file of the site.
+ *
+ * 站点 Logo
+ *
+ * @example '/logo.svg'
+ */
+ logo?: DefaultThemeImage
+
+ /**
+ * Overrides the link of the site logo.
+ *
+ * 覆盖站点 Logo 的链接
+ */
+ logoLink?: string | { link?: string; rel?: string; target?: string }
+
+ /**
+ * Appearance color mode
+ *
+ * 是否开启 浅色/深色模式
+ *
+ * @default true
+ */
+ appearance?:
+ | boolean
+ | 'dark'
+ | 'force-dark'
+ | (Omit<
+ UseDarkOptions,
+ 'initialValue' | 'onChanged' | 'storage' | 'storageKey' | 'storageRef'
+ > & { initialValue?: 'dark' })
+
+ /**
+ * The navbar items.
+ *
+ * 导航栏
+ */
+ navbar?: NavItem[]
+
+ /**
+ * The sidebar items.
+ *
+ * 侧边栏
+ */
+ sidebar?: Sidebar
+
+ /**
+ * Whether to enable page internal aside in `doc` layout.
+ * - Set to `false` to prevent rendering of aside container.
+ * - Set to `true` to render the aside to the right.
+ * - Set to `left` to render the aside to the left.
+ *
+ * 是否在 `doc` 布局中启用页内侧边栏
+ * - 将此值设置为 `false` 可禁用 aside 容器。
+ * - 将此值设置为 `true` 将在页面右侧渲染。
+ * - 将此值设置为 `left` 将在页面左侧渲染。
+ *
+ * @default true
+ */
+ aside?: boolean | 'left'
+
+ /**
+ * Custom header levels of outline in the aside component.
+ *
+ * 自定义侧边栏中的标题层级
+ *
+ * @default [2,3]
+ */
+ outline?: Outline | false
+
+ /**
+ * The title of the outline
+ *
+ * 侧边栏 标题
+ *
+ * @default 'On this page'
+ */
+ outlineTitle?: string
+
+ /**
+ * The social links to be displayed at the end of the nav bar. Perfect for
+ * placing links to social services such as GitHub, Twitter, Facebook, etc.
+ *
+ * 在导航栏中显示的社交链接, 适合放置与 GitHub, Twitter, Facebook 等社交服务相关的链接
+ */
+ socialLinks?: SocialLink[]
+
+ /**
+ * The footer configuration.
+ *
+ * 页脚
+ */
+ footer?: Footer
+
+ /**
+ * Customize text of 404 page.
+ *
+ * 定制404页面的文本。
+ */
+ notFound?: NotFoundOptions
+
+ /**
+ * Page meta - last updated config
+ *
+ * Whether to show "Last Updated" or not
+ *
+ * 页面元数据 - 最后更新时间
+ *
+ * 是否显示 "最后更新时间"
+ */
+ lastUpdated?: boolean
+
+ /**
+ * Page meta - last updated config
+ *
+ * The text to replace the default "Last Updated"
+ *
+ * 页面元数据 - 最后更新时间
+ *
+ * 最后更新时间的文本
+ */
+ lastUpdatedText?: string
+
+ /**
+ * Set options for last updated time formatting.
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options
+ *
+ * @default
+ * { dateStyle: 'short', timeStyle: 'short' }
+ */
+ lastUpdatedFormatOptions?: Intl.DateTimeFormatOptions & {
+ forceLocale?: boolean
+ }
+
+ /**
+ * Page meta - contributors config
+ *
+ * The text to replace the default "Contributors"
+ *
+ * 页面元数据 - 贡献者
+ *
+ * 贡献者的文本
+ */
+ contributorsText?: string
+
+ /**
+ * Set custom prev/next labels.
+ *
+ * 自定义 prev/next
+ */
+ docFooter?: DocFooter
+
+ /**
+ * The sidebar menu label
+ *
+ * 侧边栏 菜单标签
+ *
+ * @default 'Menu'
+ */
+ sidebarMenuLabel?: string
+
+ /**
+ * Can be used to customize the dark mode switch label.
+ * This label is only displayed in the mobile view.
+ *
+ * 用于自定义深色模式开关标签,该标签仅在移动端视图中显示。
+ *
+ * @default 'Appearance'
+ */
+ darkModeSwitchLabel?: string
+
+ /**
+ * @default 'Switch to light theme'
+ */
+ lightModeSwitchTitle?: string
+
+ /**
+ * @default 'Switch to dark theme'
+ */
+ darkModeSwitchTitle?: string
+
+ /**
+ * Local Nav config
+ *
+ * Local Nav 配置
+ *
+ * @default 'Return to top'
+ */
+ returnToTopLabel?: string
+
+ /**
+ * Navbar language selection config
+ *
+ * Text of the language selection dropdown
+ *
+ * 导航栏语言选择配置
+ *
+ * 语言选择下拉菜单的文本
+ *
+ * @default 'Languages'
+ */
+ selectLanguageText?: string
+
+ /**
+ * Navbar language selection config
+ *
+ * Language name of current locale
+ *
+ * Displayed inside the language selection dropdown
+ *
+ * 导航栏语言选择配置
+ *
+ * 当前区域的语言名称
+ *
+ * 显示在语言选择下拉菜单中
+ */
+ selectLanguageName?: string
+
+ /**
+ * The carbon ads options. Leave it undefined to disable the ads feature.
+ *
+ * Carbon广告选项。将其保留为未定义以禁用广告功能。
+ */
+ carbonAds?: CarbonAdsOptions
+
+ /**
+ * Show external link icon in Markdown links.
+ *
+ * 在Markdown链接中显示外部链接图标。
+ *
+ * @default true
+ */
+ externalLinkIcon?: boolean
+
+ /**
+ * Configure the scroll offset when the theme has a sticky header.
+ * Can be a number or a selector element to get the offset from.
+ * Can also be an array of selectors in case some elements will be
+ * invisible due to responsive layout. VuePress will fallback to the next
+ * selector if a selector fails to match, or the matched element is not
+ * currently visible in viewport.
+ *
+ * 配置主题具有粘性标题时的滚动偏移量。
+ * 可以是一个数字或选择器元素以获取偏移量。
+ * 也可以是选择器数组,以防某些元素由于响应式布局而不可见。
+ * VuePress 将在选择器无法匹配或匹配的元素当前不可见于视口时,回退到下一个选择器。
+ */
+ scrollOffset?:
+ | string[]
+ | number
+ | string
+ | { selector: string[] | string; padding: number }
+}
+
+// prev-next -----------------------------------------------------------------
+
+export interface DocFooter {
+ /**
+ * Custom label for previous page button. Can be set to `false` to disable.
+ *
+ * 设置上一页按钮的自定义标签。可以设置为 `false` 来禁用。
+ *
+ * @default 'Previous page'
+ */
+ prev?: boolean | string
+
+ /**
+ * Custom label for next page button. Can be set to `false` to disable.
+ *
+ * 设置下一页按钮的自定义标签。可以设置为 `false` 来禁用。
+ *
+ * @default 'Next page'
+ */
+ next?: boolean | string
+}
+
+// footer --------------------------------------------------------------------
+
+export interface Footer {
+ message?: string
+ copyright?: string
+}
+
+// outline -------------------------------------------------------------------
+
+export type Outline = number | 'deep' | [number, number]
+
+// carbon ads ----------------------------------------------------------------
+
+export interface CarbonAdsOptions {
+ code: string
+ placement: string
+}
+
+// not found -----------------------------------------------------------------
+
+export interface NotFoundOptions {
+ /**
+ * Set custom not found message.
+ *
+ * 自定义 页面未找到 消息。
+ *
+ * @default 'PAGE NOT FOUND'
+ */
+ title?: string
+
+ /**
+ * Set custom not found description.
+ *
+ * 自定义 页面的未找到 描述。
+ *
+ * @default "But if you don't change your direction, and if you keep looking, you may end up where you are heading."
+ */
+ quote?: string
+
+ /**
+ * Set aria label for home link.
+ *
+ * @default 'go to home'
+ */
+ linkLabel?: string
+
+ /**
+ * Set custom home link text.
+ *
+ * @default 'Take me home'
+ */
+ linkText?: string
+
+ /**
+ * @default '404'
+ */
+ code?: string
+}
diff --git a/themes/theme-next/src/shared/navbar.ts b/themes/theme-next/src/shared/navbar.ts
new file mode 100644
index 0000000000..c88eac0ec5
--- /dev/null
+++ b/themes/theme-next/src/shared/navbar.ts
@@ -0,0 +1,100 @@
+export type NavItem = NavItemWithChildren | NavItemWithLink | string
+
+export interface NavItemWithLink {
+ text: string
+ link: string
+
+ /**
+ * A Regexp string, matching path will be active
+ *
+ * It's expected to be a regex string as RegExp object isn't serializable
+ *
+ * 正则表达式字符串,匹配的路径将被激活
+ *
+ * 由于 RegExp 对象不可序列化,因此我们需要将其定义为字符串。
+ */
+ activeMatch?: string
+
+ rel?: string
+ target?: string
+
+ /**
+ * Whether to display external link icon.
+ * If the navigation link is an external link, the external link icon will be displayed by default. Setting it to `true` will disable the external link icon.
+ *
+ * 是否显示外部链接图标,如果导航链接是外部链接,默认将显示外部链接图标,设置为 `true` 可以关闭外部链接图标
+ */
+ noIcon?: boolean
+
+ prefix?: never
+ items?: never
+ /**
+ * @deprecated Use `items` instead
+ *
+ * @deprecated 使用 `items` 替换
+ * */
+ children?: never
+}
+
+export interface NavItemChildren {
+ /**
+ * The text of the dropdown menu
+ */
+ text?: string
+
+ /**
+ * Link prefix of current group
+ *
+ * 当前分组的页面前缀
+ */
+ prefix?: string
+
+ /**
+ * The items in the dropdown menu
+ *
+ * 导航栏下拉菜单
+ */
+ items: (NavItemWithLink | string)[]
+
+ /**
+ * @deprecated Use `items` instead
+ *
+ * @deprecated 使用 `items` 替换
+ * */
+ children?: (NavItemWithLink | string)[]
+}
+
+export interface NavItemWithChildren {
+ text?: string
+ /**
+ * Link prefix of current group
+ *
+ * 当前分组的页面前缀
+ */
+ prefix?: string
+
+ /**
+ * A Regexp string, matching path will be active
+ *
+ * It's expected to be a regex string as RegExp object isn't serializable
+ *
+ * 正则表达式字符串,匹配的路径将被激活
+ *
+ * 由于 RegExp 对象不可序列化,因此我们需要将其定义为字符串。
+ */
+ activeMatch?: string
+
+ /**
+ * The items in the dropdown menu
+ *
+ * 导航栏下拉菜单
+ */
+ items: (NavItemChildren | NavItemWithLink | string)[]
+
+ /**
+ * @deprecated Use `items` instead
+ *
+ * @deprecated 使用 `items` 替换
+ * */
+ children?: (NavItemChildren | NavItemWithLink | string)[]
+}
diff --git a/themes/theme-next/src/shared/page.ts b/themes/theme-next/src/shared/page.ts
new file mode 100644
index 0000000000..e31eb4798f
--- /dev/null
+++ b/themes/theme-next/src/shared/page.ts
@@ -0,0 +1,244 @@
+import type { GitData } from '@vuepress/plugin-git'
+import type {
+ ContributorFrontmatter,
+ EditLinkFrontmatter,
+} from '@vuepress/theme-helper/shared'
+import type { Outline } from './locales.js'
+import type { NavItemWithLink } from './navbar.js'
+import type { DefaultThemeImage, Feature, HeroAction } from './shared.js'
+
+export interface DefaultThemePageData extends Record {
+ filePathRelative: string | null
+ git?: GitData
+}
+
+export interface DefaultThemePageFrontmatter extends Record {
+ /**
+ * Whether is homepage
+ *
+ * 是否是主页
+ */
+ home?: boolean
+ /**
+ * Extra class name for page
+ *
+ * 页面额外类名
+ */
+ pageClass?: string
+ /**
+ * Page layout
+ *
+ * 页面布局
+ *
+ * @type { 'doc' | 'page' | 'home' }
+ * @default 'doc'
+ */
+ pageLayout?: string | false
+ /**
+ * Whether show navbar
+ *
+ * 是否显示导航
+ *
+ * @default true
+ */
+ navbar?: boolean
+ /**
+ * Whether show footer
+ *
+ * 是否显示 页脚
+ *
+ * @default true
+ */
+ footer?: boolean
+ /**
+ * Whether show external link icon
+ *
+ * 是否显示外链图标
+ *
+ * @default true
+ */
+ externalLinkIcon?: boolean
+}
+
+export interface DefaultThemeHomePageFrontmatter
+ extends DefaultThemePageFrontmatter {
+ /**
+ * Whether use markdown styles
+ *
+ * 是否使用 markdown 样式
+ *
+ * @default true
+ */
+ markdownStyles?: false
+
+ hero?: {
+ /**
+ * The string shown top of `text`. Comes with brand color
+ * and expected to be short, such as product name.
+ *
+ * `text` 上方的字符,带有品牌颜色, 尽量简短,例如产品名称
+ */
+ name?: string
+ /**
+ * The main text for the hero section. This will be defined as `h1` tag.
+ *
+ * hero 部分的主要文字,被定义为 `h1` 标签
+ */
+ text?: string
+ /**
+ * Tagline displayed below `text`.
+ *
+ * `text` 下方的标语
+ */
+ tagline?: string
+ /**
+ * The image is displayed next to the text and tagline area.
+ *
+ * text 和 tagline 区域旁的图片
+ */
+ image?: DefaultThemeImage
+ }
+ /**
+ * Action buttons to display in home hero section.
+ *
+ * 主页 hero 部分的操作按钮
+ */
+ actions?: HeroAction[]
+
+ /**
+ * you can list any number of features you would like to show right after the Hero section
+ *
+ * 在 Hero 部分之后列出任意数量的 Feature
+ */
+ features?: Feature[]
+}
+
+export interface DefaultThemeNormalPageFrontmatter
+ extends DefaultThemePageFrontmatter,
+ ContributorFrontmatter,
+ EditLinkFrontmatter {
+ /**
+ * Whether show sidebar
+ *
+ * 是否显示侧边栏
+ *
+ * @default true
+ */
+ sidebar?: boolean
+ /**
+ * 定义侧边栏组件在 `doc` 布局中的位置
+ *
+ * @default true
+ */
+ aside?: boolean | 'left'
+ /**
+ * Whether show last updated time
+ *
+ * 是否显示最后更新时间
+ *
+ * @default true
+ */
+ lastUpdated?: boolean
+ /**
+ * 大纲中显示的标题级别。
+ *
+ * @default [2,3]
+ */
+ outline?: Outline
+ /**
+ * Whether show Prev link, or define the Prev link
+ *
+ * 是否显示 Prev 链接,或定义 Prev 链接
+ *
+ * @default true
+ */
+ prev: NavItemWithLink | boolean | string
+ /**
+ * Whether show Next link, or define the Next link
+ *
+ * 是否显示 Next 链接,或定义 Next 链接
+ *
+ * @default true
+ */
+ next: NavItemWithLink | boolean | string
+ /**
+ * Whether index current page
+ *
+ * 是否索引此页面
+ *
+ * @default true
+ */
+ index?: boolean
+
+ /**
+ * Page order in sidebar
+ *
+ * 页面在侧边栏的顺序
+ *
+ * @default 0
+ */
+ order?: number
+ /**
+ * Dir config
+ *
+ * @description Only available at README.md
+ *
+ * 目录配置
+ *
+ * @description 仅在 README.md 中可用
+ */
+ dir?: {
+ /**
+ * Dir title
+ *
+ * @default title of README.md
+ *
+ * 目录标题
+ *
+ * @default README.md 标题
+ */
+ text?: string
+
+ /**
+ * Whether Dir is collapsible
+ *
+ * 目录是否可折叠
+ *
+ * @default true
+ */
+
+ collapsible?: boolean
+
+ /**
+ * Whether Dir is clickable
+ *
+ * @description Will set group link to link of README.md
+ *
+ * 目录是否可点击
+ *
+ * @description 将会将目录分组的链接设置为 README.md 对应的链接
+ *
+ * @default false
+ */
+
+ link?: boolean
+
+ /**
+ * Whether index current dir
+ *
+ * 是否索引此目录
+ *
+ * @default true
+ */
+ index?: boolean
+
+ /**
+ * Dir order in sidebar
+ *
+ * 目录在侧边栏中的顺序
+ *
+ * @default 0
+ */
+ order?: number
+ }
+}
diff --git a/themes/theme-next/src/shared/resolved/navbar.ts b/themes/theme-next/src/shared/resolved/navbar.ts
new file mode 100644
index 0000000000..7eae2f35f0
--- /dev/null
+++ b/themes/theme-next/src/shared/resolved/navbar.ts
@@ -0,0 +1,44 @@
+export type ResolvedNavItem =
+ | ResolvedNavItemWithChildren
+ | ResolvedNavItemWithLink
+
+export interface ResolvedNavItemWithLink {
+ text: string
+ link: string
+ items?: never
+
+ /**
+ * A Regexp string, matching path will be active
+ *
+ * It's expected to be a regex string as RegExp object isn't serializable
+ *
+ * 正则表达式字符串,匹配的路径将被激活
+ *
+ * 由于 RegExp 对象不可序列化,因此我们需要将其定义为字符串。
+ */
+ activeMatch?: string
+ rel?: string
+ target?: string
+ noIcon?: boolean
+}
+
+export interface ResolvedNavItemChildren {
+ text?: string
+ items: ResolvedNavItemWithLink[]
+}
+
+export interface ResolvedNavItemWithChildren {
+ text?: string
+ items: (ResolvedNavItemChildren | ResolvedNavItemWithLink)[]
+
+ /**
+ * A Regexp string, matching path will be active
+ *
+ * It's expected to be a regex string as RegExp object isn't serializable
+ *
+ * 正则表达式字符串,匹配的路径将被激活
+ *
+ * 由于 RegExp 对象不可序列化,因此我们需要将其定义为字符串。
+ */
+ activeMatch?: string
+}
diff --git a/themes/theme-next/src/shared/resolved/sidebar.ts b/themes/theme-next/src/shared/resolved/sidebar.ts
new file mode 100644
index 0000000000..c7a2777faa
--- /dev/null
+++ b/themes/theme-next/src/shared/resolved/sidebar.ts
@@ -0,0 +1,50 @@
+// resolved sidebar -------------------------------------------------------------------
+
+export type ResolvedSidebar = ResolvedSidebarItem[] | ResolvedSidebarMulti
+
+export type ResolvedSidebarMulti = Record<
+ string,
+ ResolvedSidebarItem[] | { items: ResolvedSidebarItem[] }
+>
+
+export interface ResolvedSidebarItem {
+ /**
+ * The text label of the item.
+ */
+ text?: string
+
+ /**
+ * The link of the item.
+ */
+ link?: string
+
+ /**
+ * The children of the item.
+ */
+ items?: ResolvedSidebarItem[]
+
+ /**
+ * If not specified, group is not collapsible.
+ *
+ * If `true`, group is collapsible and collapsed by default
+ *
+ * If `false`, group is collapsible but expanded by default
+ *
+ * 若未指定,分组不可折叠。
+ *
+ * 若为 `true`,分组可折叠且默认折叠
+ *
+ * 若为 `false`,分组可折叠但默认展开
+ */
+ collapsed?: boolean
+
+ /**
+ * Customize text that appears on the footer of previous/next page.
+ *
+ * 自定义上一页/下一页页脚显示的文本。
+ */
+ docFooterText?: string
+
+ rel?: string
+ target?: string
+}
diff --git a/themes/theme-next/src/shared/shared.ts b/themes/theme-next/src/shared/shared.ts
new file mode 100644
index 0000000000..485cbadf4b
--- /dev/null
+++ b/themes/theme-next/src/shared/shared.ts
@@ -0,0 +1,160 @@
+// image ---------------------------------------------------------------------
+
+export type DefaultThemeImage =
+ | string
+ | { light: string; dark: string; alt?: string; [prop: string]: unknown }
+ | { src: string; alt?: string; [prop: string]: unknown }
+
+export type FeatureIcon =
+ | string
+ | {
+ light: string
+ dark: string
+ alt?: string
+ width?: string
+ height?: string
+ wrap?: boolean
+ }
+ | {
+ src: string
+ alt?: string
+ width?: string
+ height?: string
+ wrap?: boolean
+ }
+
+// home ---------------------------------------------------------------------
+
+export interface HeroAction {
+ /**
+ * Color theme of the button. Defaults to `brand`.
+ *
+ * 按钮的颜色主题,默认为 `brand`
+ *
+ * @default 'brand'
+ */
+ theme?: 'alt' | 'brand'
+ /**
+ * Label of the button.
+ *
+ * 按钮的标签
+ */
+ text: string
+ /**
+ * Destination link of the button.
+ *
+ * 按钮的目标链接
+ */
+ link: string
+ /**
+ * Link target attribute.
+ *
+ * 链接的 target 属性
+ */
+ target?: string
+ /**
+ * Link rel attribute.
+ *
+ * 链接的 rel 属性
+ */
+ rel?: string
+}
+
+export interface Feature {
+ /**
+ * Show icon on each feature box.
+ *
+ * 在每个 feature 框中显示图标
+ */
+ icon?: FeatureIcon
+ /**
+ * Title of the feature.
+ *
+ * feature 标题
+ */
+ title: string
+ /**
+ * Details of the feature.
+ *
+ * feature 的详情
+ */
+ details: string
+ /**
+ * Link when clicked on feature component. The link can be both internal or external.
+ *
+ * 点击 feature 组件时的链接,可以是内部链接,也可以是外部链接。
+ */
+ link?: string
+ /**
+ * Link text to be shown inside feature component. Best used with `link` option.
+ *
+ * e.g. `Learn more`, `Visit page`, etc.
+ *
+ * feature 组件内显示的链接文本,最好与 `link` 选项一起使用。
+ *
+ * 例如 `Learn more`, `Visit page` 等
+ */
+ linkText?: string
+ /**
+ * Link rel attribute for the `link` option.
+ *
+ * `link` 选项的链接 rel 属性
+ */
+ rel?: string
+ /**
+ * Link target attribute for the `link` option.
+ *
+ * `link` 选项的链接 target 属性
+ */
+ target?: string
+}
+
+// social link ---------------------------------------------------------------
+
+export interface SocialLink {
+ icon: SocialLinkIcon
+ link: string
+ ariaLabel?: string
+}
+
+export type SocialLinkIcon =
+ | 'discord'
+ | 'facebook'
+ | 'github'
+ | 'instagram'
+ | 'linkedin'
+ | 'mastodon'
+ | 'npm'
+ | 'slack'
+ | 'twitter'
+ | 'x'
+ | 'youtube'
+ | { svg: string }
+
+// sponsor -------------------------------------------------------------------
+
+export interface Sponsor {
+ name: string
+ img: string
+ url: string
+}
+
+export interface Sponsors {
+ tier?: string
+ size?: 'big' | 'medium' | 'mini' | 'small' | 'xmini'
+ items: Sponsor[]
+}
+
+// team ----------------------------------------------------------------------
+
+export interface TeamMember {
+ avatar: string
+ name: string
+ title?: string
+ org?: string
+ orgLink?: string
+ desc?: string
+ links?: SocialLink[]
+ sponsor?: string
+ actionText?: string
+}
diff --git a/themes/theme-next/src/shared/sidebar.ts b/themes/theme-next/src/shared/sidebar.ts
new file mode 100644
index 0000000000..083ef75ada
--- /dev/null
+++ b/themes/theme-next/src/shared/sidebar.ts
@@ -0,0 +1,128 @@
+import type { PageFrontmatter } from 'vuepress/core'
+import type {
+ DefaultThemeNormalPageFrontmatter,
+ DefaultThemePageData,
+} from './page.js'
+
+export type Sidebar = (SidebarItem | string)[] | SidebarMulti | 'structure'
+
+export type SidebarMulti = Record<
+ string,
+ | (SidebarItem | string)[]
+ | 'structure'
+ | { items: (SidebarItem | string)[] | 'structure'; prefix?: string }
+>
+
+export interface SidebarItem {
+ /**
+ * The text label of the item.
+ *
+ * 项目文本
+ */
+ text?: string
+
+ /**
+ * The link of the item.
+ *
+ * 项目链接
+ */
+ link?: string
+
+ /**
+ * The children of the item.
+ *
+ * 子项目列表
+ *
+ */
+ items?: (SidebarItem | string)[] | 'structure'
+
+ /**
+ * The children of the item.
+ * @deprecated Use `items` instead
+ *
+ * 子项目列表
+ * @deprecated 使用 `items` 代替
+ */
+ children?: (SidebarItem | string)[]
+
+ /**
+ * Whether the current group is collapsible
+ * - If not specified, group is not collapsible.
+ * - If `true`, group is collapsible and collapsed by default
+ * - If `false`, group is collapsible but expanded by default
+ *
+ * 当前子项目列表是否可折叠
+ * - 如果未指定,分组不可折叠
+ * - 如果为 `true`,分组可折叠且默认折叠
+ * - 如果为 `false`,分组可折叠且默认展开
+ */
+ collapsed?: boolean
+
+ /**
+ * prefix path for the children items.
+ *
+ * 子项目的路径前缀
+ */
+ prefix?: string
+
+ /**
+ * Customize text that appears on the footer of previous/next page.
+ *
+ * 自定义上一页/下一页页脚显示的文本。
+ */
+ docFooterText?: string
+
+ rel?: string
+ target?: string
+}
+
+export interface SidebarFileInfo {
+ type: 'file'
+ filename: string
+
+ title: string
+ order: number | null
+ path?: string | null
+
+ frontmatter: PageFrontmatter
+ pageData: DefaultThemePageData
+}
+
+export interface SidebarDirInfo {
+ type: 'dir'
+ dirname: string
+ children: SidebarInfo[]
+
+ title: string
+ order: number | null
+
+ groupInfo: {
+ icon?: string
+ collapsible?: boolean
+ link?: string
+ }
+
+ frontmatter: PageFrontmatter | null
+ pageData: DefaultThemePageData | null
+}
+
+export type SidebarInfo = SidebarDirInfo | SidebarFileInfo
+
+export type SidebarSorterKeyword =
+ | 'date-desc'
+ | 'date'
+ | 'filename'
+ | 'order'
+ | 'readme'
+ | 'title'
+
+export type SidebarSorterFunction = (
+ infoA: SidebarInfo,
+ infoB: SidebarInfo,
+) => number
+
+export type SidebarSorter =
+ | SidebarSorterFunction
+ | SidebarSorterFunction[]
+ | SidebarSorterKeyword
+ | SidebarSorterKeyword[]
diff --git a/themes/theme-next/templates/build.html b/themes/theme-next/templates/build.html
new file mode 100644
index 0000000000..7c3f6bcc9a
--- /dev/null
+++ b/themes/theme-next/templates/build.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/themes/theme-next/tsconfig.build.json b/themes/theme-next/tsconfig.build.json
new file mode 100644
index 0000000000..10f32ebeb2
--- /dev/null
+++ b/themes/theme-next/tsconfig.build.json
@@ -0,0 +1,43 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./lib",
+ "baseUrl": ".",
+ "paths": {
+ "@theme/*": ["./src/client/components/*"]
+ },
+ "types": ["vuepress/client-types", "vite/client", "webpack-env"]
+ },
+ "include": ["./src"],
+ "references": [
+ {
+ "path": "../../plugins/development/plugin-active-header-links/tsconfig.build.json"
+ },
+ { "path": "../../plugins/development/plugin-git/tsconfig.build.json" },
+ {
+ "path": "../../plugins/development/plugin-theme-data/tsconfig.build.json"
+ },
+
+ {
+ "path": "../../plugins/features/plugin-copy-code/tsconfig.build.json"
+ },
+ {
+ "path": "../../plugins/features/plugin-medium-zoom/tsconfig.build.json"
+ },
+ {
+ "path": "../../plugins/features/plugin-nprogress/tsconfig.build.json"
+ },
+
+ {
+ "path": "../../plugins/markdown/plugin-links-check/tsconfig.build.json"
+ },
+ {
+ "path": "../../plugins/markdown/plugin-shiki/tsconfig.build.json"
+ },
+
+ { "path": "../../plugins/seo/plugin-seo/tsconfig.build.json" },
+ { "path": "../../plugins/seo/plugin-sitemap/tsconfig.build.json" },
+ { "path": "../../tools/theme-helper/tsconfig.build.json" }
+ ]
+}
diff --git a/tools/helper/package.json b/tools/helper/package.json
index ff15317a32..411f3b7a84 100644
--- a/tools/helper/package.json
+++ b/tools/helper/package.json
@@ -31,7 +31,6 @@
"./noopComponent": "./lib/client/noopComponent.js",
"./noopModule": "./lib/client/noopModule.js",
"./colors.css": "./lib/client/styles/colors.css",
- "./normalize.css": "./lib/client/styles/normalize.css",
"./sr-only.css": "./lib/client/styles/sr-only.css",
"./package.json": "./package.json"
},
diff --git a/tools/helper/tests/__fixtures__/package-manager/config/pnpm/package.json b/tools/helper/tests/__fixtures__/package-manager/config/pnpm/package.json
index bb96fa3ed7..8adc04822e 100644
--- a/tools/helper/tests/__fixtures__/package-manager/config/pnpm/package.json
+++ b/tools/helper/tests/__fixtures__/package-manager/config/pnpm/package.json
@@ -1,4 +1,4 @@
{
"name": "test",
- "packageManager": "pnpm@9.14.4"
+ "packageManager": "pnpm@9.15.0"
}
diff --git a/tools/theme-helper/package.json b/tools/theme-helper/package.json
new file mode 100644
index 0000000000..55d2647380
--- /dev/null
+++ b/tools/theme-helper/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "@vuepress/theme-helper",
+ "version": "2.0.0-rc.61",
+ "description": "VuePress Theme Helper",
+ "keywords": [
+ "vuepress",
+ "helper",
+ "bundler-helper",
+ "excerpt"
+ ],
+ "homepage": "https://ecosystem.vuejs.press/tools/theme-helper/",
+ "bugs": {
+ "url": "https://github.com/vuepress/ecosystem/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vuepress/ecosystem.git",
+ "directory": "tools/theme-helper"
+ },
+ "license": "MIT",
+ "author": {
+ "name": "Mr.Hope",
+ "email": "mister-hope@outlook.com",
+ "url": "https://mister-hope.com"
+ },
+ "type": "module",
+ "exports": {
+ ".": "./lib/node/index.js",
+ "./client": "./lib/client/index.js",
+ "./shared": "./lib/shared/index.js",
+ "./normalize.css": "./lib/client/styles/normalize.css",
+ "./package.json": "./package.json"
+ },
+ "main": "./lib/node/index.js",
+ "types": "./lib/node/index.d.ts",
+ "files": [
+ "lib"
+ ],
+ "scripts": {
+ "build": "tsc -b tsconfig.build.json",
+ "bundle": "rollup -c rollup.config.ts --configPlugin esbuild",
+ "clean": "rimraf --glob ./lib ./*.tsbuildinfo",
+ "style": "sass src:lib --embed-sources --style=compressed"
+ },
+ "dependencies": {
+ "@vuepress/plugin-git": "workspace:*",
+ "@vuepress/plugin-theme-data": "workspace:*",
+ "@vueuse/core": "^12.0.0",
+ "vue": "^3.5.13"
+ },
+ "peerDependencies": {
+ "vuepress": "2.0.0-rc.18"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/tools/theme-helper/rollup.config.ts b/tools/theme-helper/rollup.config.ts
new file mode 100644
index 0000000000..2ef3ff904c
--- /dev/null
+++ b/tools/theme-helper/rollup.config.ts
@@ -0,0 +1,17 @@
+import { rollupBundle } from '../../scripts/rollup.js'
+
+export default [
+ ...rollupBundle('node/index', {
+ external: [],
+ }),
+ ...rollupBundle(
+ {
+ base: 'client',
+ files: ['index'],
+ },
+ {
+ external: ['@vuepress/plugin-theme-data/client'],
+ },
+ ),
+ ...rollupBundle('shared/index'),
+]
diff --git a/tools/theme-helper/src/client/composables/index.ts b/tools/theme-helper/src/client/composables/index.ts
new file mode 100644
index 0000000000..3073d0ea08
--- /dev/null
+++ b/tools/theme-helper/src/client/composables/index.ts
@@ -0,0 +1 @@
+export * from './useContributors.js'
diff --git a/tools/theme-helper/src/client/composables/useContributors.ts b/tools/theme-helper/src/client/composables/useContributors.ts
new file mode 100644
index 0000000000..88b0adc754
--- /dev/null
+++ b/tools/theme-helper/src/client/composables/useContributors.ts
@@ -0,0 +1,25 @@
+import type { GitContributor, GitPluginPageData } from '@vuepress/plugin-git'
+import type { ComputedRef } from 'vue'
+import { computed } from 'vue'
+import type {
+ ContributorFrontmatter,
+ ContributorThemeData,
+} from '../../shared/index.js'
+import { useData } from './useData.js'
+
+export const useContributors = (): ComputedRef => {
+ const { page, frontmatter, theme } = useData<
+ ContributorThemeData,
+ ContributorFrontmatter,
+ GitPluginPageData
+ >()
+
+ return computed(() => {
+ const showContributors =
+ frontmatter.value.contributors ?? theme.value.contributors ?? true
+
+ if (!showContributors) return null
+
+ return page.value.git.contributors ?? null
+ })
+}
diff --git a/tools/theme-helper/src/client/composables/useData.ts b/tools/theme-helper/src/client/composables/useData.ts
new file mode 100644
index 0000000000..4decd9bee7
--- /dev/null
+++ b/tools/theme-helper/src/client/composables/useData.ts
@@ -0,0 +1,18 @@
+import type { ThemeLocaleDataRef } from '@vuepress/plugin-theme-data/client'
+import { useThemeLocaleData } from '@vuepress/plugin-theme-data/client'
+import type { PageDataRef, PageFrontmatterRef } from 'vuepress/client'
+import { usePageData, usePageFrontmatter } from 'vuepress/client'
+
+export const useData = (): {
+ theme: ThemeLocaleDataRef & ThemeLocaleData>
+ frontmatter: PageFrontmatterRef>
+ page: PageDataRef>
+} => {
+ const theme = useThemeLocaleData & ThemeLocaleData>()
+ const frontmatter = usePageFrontmatter<
+ Frontmatter & Record
+ >()
+ const page = usePageData>()
+
+ return { theme, frontmatter, page }
+}
diff --git a/themes/theme-default/src/client/composables/useEditLink.ts b/tools/theme-helper/src/client/composables/useEditLink.ts
similarity index 55%
rename from themes/theme-default/src/client/composables/useEditLink.ts
rename to tools/theme-helper/src/client/composables/useEditLink.ts
index cbca17a0b5..56c89677a6 100644
--- a/themes/theme-default/src/client/composables/useEditLink.ts
+++ b/tools/theme-helper/src/client/composables/useEditLink.ts
@@ -1,22 +1,25 @@
-import { useThemeLocaleData } from '@theme/useThemeData'
+import { resolveEditLink } from '@vuepress/theme-helper/client'
import type { ComputedRef } from 'vue'
import { computed } from 'vue'
import type { AutoLinkConfig } from 'vuepress/client'
-import { usePageData, usePageFrontmatter } from 'vuepress/client'
import type {
- DefaultThemeNormalPageFrontmatter,
- DefaultThemePageData,
+ EditLinkFrontmatter,
+ EditLinkPageData,
+ EditLinkThemeData,
} from '../../shared/index.js'
-import { resolveEditLink } from '../utils/index.js'
+import { useData } from './useData.js'
export const useEditLink = (): ComputedRef => {
- const themeLocale = useThemeLocaleData()
- const page = usePageData()
- const frontmatter = usePageFrontmatter()
+ const { page, frontmatter, theme } = useData<
+ EditLinkThemeData,
+ EditLinkFrontmatter,
+ EditLinkPageData
+ >()
return computed(() => {
const showEditLink =
- frontmatter.value.editLink ?? themeLocale.value.editLink ?? true
+ frontmatter.value.editLink ?? theme.value.editLink ?? true
+
if (!showEditLink) {
return null
}
@@ -27,9 +30,11 @@ export const useEditLink = (): ComputedRef => {
docsBranch = 'main',
docsDir = '',
editLinkText,
- } = themeLocale.value
+ } = theme.value
- if (!docsRepo) return null
+ if (!docsRepo) {
+ return null
+ }
const editLink = resolveEditLink({
docsRepo,
@@ -37,7 +42,7 @@ export const useEditLink = (): ComputedRef => {
docsDir,
filePathRelative: page.value.filePathRelative,
editLinkPattern:
- frontmatter.value.editLinkPattern ?? themeLocale.value.editLinkPattern,
+ frontmatter.value.editLinkPattern ?? theme.value.editLinkPattern,
})
if (!editLink) return null
diff --git a/tools/theme-helper/src/client/index.ts b/tools/theme-helper/src/client/index.ts
new file mode 100644
index 0000000000..3b732676ac
--- /dev/null
+++ b/tools/theme-helper/src/client/index.ts
@@ -0,0 +1,3 @@
+export * from './composables/index.js'
+export * from './utils/index.js'
+export type * from '../shared/index.js'
diff --git a/tools/helper/src/client/styles/normalize.scss b/tools/theme-helper/src/client/styles/normalize.scss
similarity index 100%
rename from tools/helper/src/client/styles/normalize.scss
rename to tools/theme-helper/src/client/styles/normalize.scss
diff --git a/tools/theme-helper/src/client/utils/index.ts b/tools/theme-helper/src/client/utils/index.ts
new file mode 100644
index 0000000000..0d40221937
--- /dev/null
+++ b/tools/theme-helper/src/client/utils/index.ts
@@ -0,0 +1,2 @@
+export * from './resolveEditLink.js'
+export * from './resolveRepoType.js'
diff --git a/themes/theme-default/src/client/utils/resolveEditLink.ts b/tools/theme-helper/src/client/utils/resolveEditLink.ts
similarity index 89%
rename from themes/theme-default/src/client/utils/resolveEditLink.ts
rename to tools/theme-helper/src/client/utils/resolveEditLink.ts
index e6e43f218c..e2b3fbc766 100644
--- a/themes/theme-default/src/client/utils/resolveEditLink.ts
+++ b/tools/theme-helper/src/client/utils/resolveEditLink.ts
@@ -6,7 +6,7 @@ import {
import type { RepoType } from './resolveRepoType.js'
import { resolveRepoType } from './resolveRepoType.js'
-export const editLinkPatterns: Record, string> = {
+export const EDIT_LINK_PATTENS: Record, string> = {
GitHub: ':repo/edit/:branch/:path',
GitLab: ':repo/-/edit/:branch/:path',
Gitee: ':repo/edit/:branch/:path',
@@ -26,11 +26,8 @@ const resolveEditLinkPatterns = ({
}
const repoType = resolveRepoType(docsRepo)
- if (repoType !== null) {
- return editLinkPatterns[repoType]
- }
- return null
+ return repoType ? EDIT_LINK_PATTENS[repoType] : null
}
export const resolveEditLink = ({
diff --git a/themes/theme-default/src/client/utils/resolveRepoType.ts b/tools/theme-helper/src/client/utils/resolveRepoType.ts
similarity index 100%
rename from themes/theme-default/src/client/utils/resolveRepoType.ts
rename to tools/theme-helper/src/client/utils/resolveRepoType.ts
diff --git a/tools/theme-helper/src/node/editLink.ts b/tools/theme-helper/src/node/editLink.ts
new file mode 100644
index 0000000000..69a3c8005a
--- /dev/null
+++ b/tools/theme-helper/src/node/editLink.ts
@@ -0,0 +1,9 @@
+import type { Page } from 'vuepress'
+import type { EditLinkPageData } from '../shared/index.js'
+
+export const extendsEditLinkPage = (
+ page: Page>,
+): void => {
+ // save relative file path into page data to generate edit link
+ page.data.filePathRelative = page.filePathRelative
+}
diff --git a/tools/theme-helper/src/node/index.ts b/tools/theme-helper/src/node/index.ts
new file mode 100644
index 0000000000..f6463d57f5
--- /dev/null
+++ b/tools/theme-helper/src/node/index.ts
@@ -0,0 +1,2 @@
+export * from './editLink.js'
+export type * from '../shared/index.js'
diff --git a/tools/theme-helper/src/shared/contributor.ts b/tools/theme-helper/src/shared/contributor.ts
new file mode 100644
index 0000000000..7d96dd6507
--- /dev/null
+++ b/tools/theme-helper/src/shared/contributor.ts
@@ -0,0 +1,16 @@
+export interface ContributorOptions {
+ /**
+ * @kind Page meta / 页面元数据
+ *
+ * Whether to show contributors
+ *
+ * 是否显示贡献者
+ *
+ * @default true
+ */
+ contributors?: boolean
+}
+
+export type ContributorFrontmatter = ContributorOptions
+
+export type ContributorThemeData = ContributorOptions
diff --git a/tools/theme-helper/src/shared/editLink.ts b/tools/theme-helper/src/shared/editLink.ts
new file mode 100644
index 0000000000..15446ed0a3
--- /dev/null
+++ b/tools/theme-helper/src/shared/editLink.ts
@@ -0,0 +1,91 @@
+export interface EditLinkOptions {
+ /**
+ * Source repo link
+ *
+ * 源代码仓库链接
+ */
+ repo?: string | null
+
+ /**
+ * @kind Page meta
+ *
+ * Whether to show "Edit this page" or not
+ *
+ * 是否显示“编辑此页面”
+ *
+ * @default true
+ */
+ editLink?: boolean
+
+ /**
+ * @kind Page meta
+ *
+ * The text for edit link
+ *
+ * 编辑链接的文本
+ *
+ * @default 'Edit this page'
+ */
+ editLinkText?: string
+
+ /**
+ * @kind Page meta
+ *
+ * Pattern of edit link
+ *
+ * 编辑链接的模式
+ *
+ * @description `:repo` {@link docsRepo} | `:branch` {@link docsBranch} | `:path` {@link docsDir}
+ * @example ':repo/edit/:branch/:path'
+ */
+ editLinkPattern?: string
+
+ /**
+ * @kind Page meta
+ *
+ * Docs repo link
+ *
+ * 文档仓库链接
+ *
+ * @default repo
+ */
+ docsRepo?: string
+
+ /**
+ * @kind Page meta
+ *
+ * Docs branch
+ *
+ * 文档分支
+ *
+ * @default 'main'
+ */
+ docsBranch?: string
+
+ /**
+ * @kind Page meta
+ *
+ * Docs dir
+ *
+ * 文档目录
+ *
+ * @default ''
+ */
+ docsDir?: string
+}
+
+export type EditLinkFrontmatter = Pick<
+ EditLinkOptions,
+ 'editLink' | 'editLinkPattern'
+>
+
+export type EditLinkThemeData = EditLinkOptions
+
+export interface EditLinkPageData {
+ /**
+ * Relative path of current page
+ *
+ * 当前页面的相对路径
+ */
+ filePathRelative: string | null
+}
diff --git a/tools/theme-helper/src/shared/index.ts b/tools/theme-helper/src/shared/index.ts
new file mode 100644
index 0000000000..7c0592a1d3
--- /dev/null
+++ b/tools/theme-helper/src/shared/index.ts
@@ -0,0 +1,2 @@
+export type * from './contributor.js'
+export type * from './editLink.js'
diff --git a/tools/theme-helper/tsconfig.build.json b/tools/theme-helper/tsconfig.build.json
new file mode 100644
index 0000000000..4f60f73883
--- /dev/null
+++ b/tools/theme-helper/tsconfig.build.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./lib",
+ "baseUrl": "."
+ },
+ "include": ["./src"]
+}
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 4315e4adbc..8c013a75d7 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -103,11 +103,13 @@
// themes
{ "path": "./themes/theme-default/tsconfig.build.json" },
+ { "path": "./themes/theme-next/tsconfig.build.json" },
// tools
{ "path": "./tools/create-vuepress/tsconfig.build.json" },
{ "path": "./tools/helper/tsconfig.build.json" },
{ "path": "./tools/highlighter-helper/tsconfig.build.json" },
+ { "path": "./tools/theme-helper/tsconfig.build.json" },
{ "path": "./tools/vp-update/tsconfig.build.json" }
],
"files": []
diff --git a/tsconfig.json b/tsconfig.json
index 1694ff63ad..7a7ccbb738 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,8 +11,8 @@
"./plugins/plugin-theme-data/src/client/themeData.d.ts"
],
"@theme/*": [
- "./themes/theme-default/src/client/components/*",
- "./themes/theme-default/src/client/composables/*.js"
+ "./themes/theme-next/src/client/components/*",
+ "./themes/theme-next/src/client/composables/*"
]
},
"types": ["vuepress/client-types", "webpack-env", "vite/client"]