diff --git a/packages/vkui-docs-theme/index.ts b/packages/vkui-docs-theme/index.ts index 933afae866..f491c34243 100644 --- a/packages/vkui-docs-theme/index.ts +++ b/packages/vkui-docs-theme/index.ts @@ -5,3 +5,5 @@ export { useFetch } from './src/hooks/useFetch'; export { StorybookIcon, GithubIcon, FigmaIcon } from './src/icons'; export { type DocsThemeConfig }; export default Layout; + +export { TagTitle, TagName, getStaticPathsTags } from './src/blog/tags'; diff --git a/packages/vkui-docs-theme/src/blog/PostsLayout.tsx b/packages/vkui-docs-theme/src/blog/PostsLayout.tsx new file mode 100644 index 0000000000..f1815e665a --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/PostsLayout.tsx @@ -0,0 +1,45 @@ +import { SimpleGrid } from '@vkontakte/vkui'; +import { useRouter } from 'next/router'; +import { useConfig } from '../contexts'; +import { Post } from './components/Post'; +import { findPosts } from './helpers'; +import { getTags } from './tags'; + +export function PostsLayout() { + const config = useConfig(); + const { pageMap, frontMatter } = config; + const posts = findPosts(pageMap); + const router = useRouter(); + const { type } = frontMatter; + const tagName = type === 'tag' ? router.query.tag : null; + + return ( + + {posts.map((post) => { + const tags = getTags(post.frontMatter); + if (tagName) { + if (!Array.isArray(tagName) && !tags.includes(tagName)) { + return null; + } + } else if (type === 'tag') { + return null; + } + + const postTitle = post.frontMatter?.title || post.name; + const date: Date | null = post.frontMatter?.date ? new Date(post.frontMatter.date) : null; + + return ( + + ); + })} + + ); +} diff --git a/packages/vkui-docs-theme/src/blog/components/Heading.tsx b/packages/vkui-docs-theme/src/blog/components/Heading.tsx new file mode 100644 index 0000000000..8cfbf38d33 --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/components/Heading.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; +import { Title } from '@vkontakte/vkui'; + +export interface HeadingProps extends React.ComponentProps<'h2'> { + Tag?: `h${1 | 2 | 3}`; +} + +export function Heading({ Tag = 'h2', children, ...props }: HeadingProps) { + return ( + + {children} + + ); +} diff --git a/packages/vkui-docs-theme/src/blog/components/Post/Post.module.css b/packages/vkui-docs-theme/src/blog/components/Post/Post.module.css new file mode 100644 index 0000000000..a20268105d --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/components/Post/Post.module.css @@ -0,0 +1,40 @@ +.root { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.heading { + --vkui--color_text_link: var(--vkui--color_text_primary); + + color: var(--vkui--color_text_primary); +} + +.headingPrimary { + font-size: 40px; + line-height: 46px; +} + +.date:not(:only-child)::after { + content: '•'; + padding-inline: var(--vkui--spacing_size_s); +} + +.content { + padding-block: var(--vkui--size_base_padding_vertical--regular); + padding-inline: var(--vkui--size_base_padding_horizontal--regular); + display: flex; + flex-direction: column; + flex-grow: 1; + + --vkui--color_text_link: var(--vkui--color_text_primary); +} + +.description { + margin-block-start: var(--vkui--spacing_size_s); +} + +.meta { + margin-block-start: auto; + padding-block-start: var(--vkui--spacing_size_s); +} diff --git a/packages/vkui-docs-theme/src/blog/components/Post/Post.tsx b/packages/vkui-docs-theme/src/blog/components/Post/Post.tsx new file mode 100644 index 0000000000..1c354f0723 --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/components/Post/Post.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { Card, Link, Text } from '@vkontakte/vkui'; +import NextLink from 'next/link'; +import { useRouter } from 'next/router'; +import { PostHeading } from './PostHeading'; +import { PostMeta, type PostMetaProps } from './PostMeta'; +import styles from './Post.module.css'; + +interface PostProps extends PostMetaProps { + title: React.ReactNode; + description?: React.ReactNode; + route?: string; + image?: string; +} + +export function Post({ title, description, tags, publishDate, route, image }: PostProps) { + const router = useRouter(); + + return ( + + {`Лого +
+ + + {title} + + + {description && ( + + {description}{' '} + + Читать далее... + + + )} + +
+
+ ); +} diff --git a/packages/vkui-docs-theme/src/blog/components/Post/PostHeading.tsx b/packages/vkui-docs-theme/src/blog/components/Post/PostHeading.tsx new file mode 100644 index 0000000000..fcc48ea79f --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/components/Post/PostHeading.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { classNames } from '@vkontakte/vkjs'; +import { Heading, type HeadingProps } from '../Heading'; +import styles from './Post.module.css'; + +export interface PostHeadingProps extends HeadingProps { + children?: React.ReactNode; +} + +export function PostHeading({ children, Tag }: PostHeadingProps) { + if (Tag === 'h1') { + return

{children}

; + } + + return ( + + {children} + + ); +} diff --git a/packages/vkui-docs-theme/src/blog/components/Post/PostMeta.tsx b/packages/vkui-docs-theme/src/blog/components/Post/PostMeta.tsx new file mode 100644 index 0000000000..56962e7745 --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/components/Post/PostMeta.tsx @@ -0,0 +1,27 @@ +import { ContentBadge, Footnote } from '@vkontakte/vkui'; +import NextLink from 'next/link'; +import { DateFormatter } from '../../../helpers/date'; +import styles from './Post.module.css'; + +export interface PostMetaProps { + publishDate?: Date | null; + tags?: string[]; + className?: string; +} + +export function PostMeta({ publishDate, tags = [], className }: PostMetaProps) { + return ( +
+ {publishDate && ( + + + + )} + {tags.map((tag) => ( + + # {tag} + + ))} +
+ ); +} diff --git a/packages/vkui-docs-theme/src/blog/components/Post/index.ts b/packages/vkui-docs-theme/src/blog/components/Post/index.ts new file mode 100644 index 0000000000..45508430e1 --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/components/Post/index.ts @@ -0,0 +1,3 @@ +export { Post } from './Post'; +export { PostMeta } from './PostMeta'; +export { PostHeading } from './PostHeading'; diff --git a/packages/vkui-docs-theme/src/blog/components/PostHeader.tsx b/packages/vkui-docs-theme/src/blog/components/PostHeader.tsx new file mode 100644 index 0000000000..698dfb1909 --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/components/PostHeader.tsx @@ -0,0 +1,29 @@ +import { Icon12ChevronLeft } from '@vkontakte/icons'; +import { Button } from '@vkontakte/vkui'; +import { useRouter } from 'next/router'; +import { useConfig } from '../../contexts'; +import { getTags } from '../tags'; +import { PostHeading, PostMeta } from './Post'; + +export function PostHeader() { + const config = useConfig(); + const { frontMatter } = config; + const router = useRouter(); + const tags = getTags(frontMatter); + + const back = () => { + void router.push('/blog'); + }; + + const date: Date | null = frontMatter.date ? new Date(frontMatter.date) : null; + + return ( +
+ + + {frontMatter.title} +
+ ); +} diff --git a/packages/vkui-docs-theme/src/blog/helpers.ts b/packages/vkui-docs-theme/src/blog/helpers.ts new file mode 100644 index 0000000000..82dd7911f1 --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/helpers.ts @@ -0,0 +1,33 @@ +import type { MdxFile, PageMapItem, PageOpts } from 'nextra'; + +const sortPosts = (a: MdxFile, b: MdxFile): number => { + if (!a.frontMatter?.date || !b.frontMatter?.date) { + return -1; + } + + return new Date(b.frontMatter.date).getTime() - new Date(a.frontMatter.date).getTime(); +}; + +export const isPost = (page: PageMapItem): page is MdxFile => { + if ('frontMatter' in page) { + const { draft, type } = page.frontMatter || {}; + return !draft && type === 'post'; + } + return false; +}; + +export function findPosts(pageMap: PageOpts['pageMap']) { + const posts: MdxFile[] = []; + + for (const item of pageMap) { + if ('children' in item && item.name === 'blog') { + for (const pageMapItem of item.children) { + if (isPost(pageMapItem)) { + posts.push(pageMapItem); + } + } + } + } + posts.sort(sortPosts); + return posts; +} diff --git a/packages/vkui-docs-theme/src/blog/index.ts b/packages/vkui-docs-theme/src/blog/index.ts new file mode 100644 index 0000000000..73163f276f --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/index.ts @@ -0,0 +1,2 @@ +export { PostsLayout } from './PostsLayout'; +export { PostHeader } from './components/PostHeader'; diff --git a/packages/vkui-docs-theme/src/blog/tags.tsx b/packages/vkui-docs-theme/src/blog/tags.tsx new file mode 100644 index 0000000000..18b82623d1 --- /dev/null +++ b/packages/vkui-docs-theme/src/blog/tags.tsx @@ -0,0 +1,54 @@ +import type { GetStaticPaths } from 'next'; +import Head from 'next/head'; +import type { FrontMatter, PageMapItem } from 'nextra'; +import { useData } from 'nextra/hooks'; +import { isPost } from './helpers'; + +const NEXTRA_INTERNAL = Symbol.for('__nextra_internal__'); + +export const TagTitle = () => { + const { tag } = useData(); + const title = `Посты по теме ${tag}`; + return ( + + {title} + + ); +}; + +export const TagName = () => { + const { tag } = useData(); + return tag || null; +}; + +export function getTags(frontMatter: FrontMatter | undefined) { + if (!frontMatter) { + return []; + } + const tags: string | string[] = frontMatter.tag || []; + return (Array.isArray(tags) ? tags : tags.split(',')).map((s) => s.trim()); +} + +const getStaticTags = (pageMap: PageMapItem[]) => { + const tags = []; + + for (const item of pageMap) { + if ('children' in item && item.name === 'blog') { + for (const pageMapItem of item.children) { + if (isPost(pageMapItem)) { + tags.push(...getTags(pageMapItem.frontMatter)); + } + } + } + } + + return [...new Set(tags)]; +}; + +export const getStaticPathsTags: GetStaticPaths = () => { + const tags = getStaticTags((globalThis as any)[NEXTRA_INTERNAL].pageMap); + return { + paths: tags.map((v: any) => ({ params: { tag: v } })), + fallback: false, + }; +}; diff --git a/packages/vkui-docs-theme/src/components/Navbar/Navbar.module.css b/packages/vkui-docs-theme/src/components/Navbar/Navbar.module.css index fecdac703c..89bc74654e 100644 --- a/packages/vkui-docs-theme/src/components/Navbar/Navbar.module.css +++ b/packages/vkui-docs-theme/src/components/Navbar/Navbar.module.css @@ -3,23 +3,22 @@ inset-block-start: 0; z-index: 20; inline-size: 100%; - background-color: transparent; color: var(--vkui--color_text_primary); border-block-end: var(--vkui--size_border--regular) solid var(--vkui_docs--color_stroke_separator_secondary); box-shadow: 0 8px 30px 0 rgba(0, 0, 0, 0.04); + block-size: var(--vkui_docs--navbar-height); + background-color: var(--vkui--color_background_content); } .navbar { display: flex; margin-inline: auto; align-items: center; - block-size: var(--vkui_docs--navbar-height); max-inline-size: var(--vkui_docs--max-width); justify-content: space-between; padding-block: var(--vkui--spacing_size_xl); padding-inline: var(--vkui--spacing_size_2xl); - background-color: var(--vkui--color_background_content); } @media (--viewWidth-desktopPlus) { @@ -47,8 +46,8 @@ .navbarLink { color: var(--vkui--color_text_primary); - padding-block: 8px; - padding-inline: 16px; + padding-block: var(--vkui--spacing_size_m); + padding-inline: var(--vkui--spacing_size_l); text-decoration: none; } diff --git a/packages/vkui-docs-theme/src/contexts/config.tsx b/packages/vkui-docs-theme/src/contexts/config.tsx index 1bdb716cf8..a11687a0d2 100644 --- a/packages/vkui-docs-theme/src/contexts/config.tsx +++ b/packages/vkui-docs-theme/src/contexts/config.tsx @@ -3,10 +3,11 @@ import type { PageOpts } from 'nextra'; import { useFSRoute } from 'nextra/hooks'; import { normalizePages } from 'nextra/normalize-pages'; -type Config = Pick & { +type Config = Pick & { hideSidebar: boolean; normalizePagesResult: ReturnType; metaData?: Record; + isBlog?: boolean; }; const ConfigContext = React.createContext({ @@ -16,6 +17,7 @@ const ConfigContext = React.createContext({ hideSidebar: false, // @ts-expect-error: TS2740 No default value normalizePagesResult: {}, + isBlog: false, }); export function useConfig() { @@ -30,6 +32,7 @@ export const ConfigProvider = ({ value: PageOpts; }): React.ReactElement => { const fsPath = useFSRoute(); + const isBlog = fsPath.includes('/blog'); const normalizePagesResult = React.useMemo( () => normalizePages({ list: pageOpts.pageMap, route: fsPath }), @@ -40,22 +43,26 @@ export const ConfigProvider = ({ // There are no more additional fields on item, so we extract meta by this mess let metaData: Record = {}; - pageOpts.pageMap.forEach((page) => { - if ('name' in page && page.name === activePath[0].name && 'children' in page) { - const data = page.children.find((value) => 'data' in value)?.data || {}; - const metaKeys = Object.keys(data); - for (let key of metaKeys) { - // @ts-expect-error: TS2339 No icon type on item - metaData[key] = data[key].icon; + if (pageOpts.frontMatter.type !== 'tag' || !themeContext.sidebar) { + pageOpts.pageMap.forEach((page) => { + if ('name' in page && page.name === activePath[0].name && 'children' in page) { + const data = page.children.find((value) => 'data' in value)?.data || {}; + const metaKeys = Object.keys(data); + for (let key of metaKeys) { + // @ts-expect-error: TS2339 No icon type on item + metaData[key] = data[key].icon; + } } - } - }); + }); + } const extendedConfig: Config = { title: pageOpts.title, + pageMap: pageOpts.pageMap, frontMatter: pageOpts.frontMatter, filePath: pageOpts.filePath, hideSidebar: !themeContext.sidebar || themeContext.layout === 'raw' || activeType === 'page', + isBlog, normalizePagesResult, metaData, }; diff --git a/packages/vkui-docs-theme/src/helpers/date.ts b/packages/vkui-docs-theme/src/helpers/date.ts new file mode 100644 index 0000000000..3031a629c5 --- /dev/null +++ b/packages/vkui-docs-theme/src/helpers/date.ts @@ -0,0 +1,6 @@ +export const DateFormatter = new Intl.DateTimeFormat('ru', { + weekday: 'short', + day: '2-digit', + month: 'long', + year: 'numeric', +}); diff --git a/packages/vkui-docs-theme/src/index.tsx b/packages/vkui-docs-theme/src/index.tsx index a0ee926eb2..2be5d231a6 100644 --- a/packages/vkui-docs-theme/src/index.tsx +++ b/packages/vkui-docs-theme/src/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import type { NextraThemeLayoutProps } from 'nextra'; import { MDXProvider } from 'nextra/mdx'; +import { PostHeader, PostsLayout } from './blog'; import { Head } from './components'; import { ColorSchemeProvider, @@ -12,7 +13,7 @@ import { } from './contexts'; import { renderComponent } from './helpers/render'; import { getMdxComponents } from './mdx'; -import { VKUIWrapper } from './wrappers'; +import { ContentWrapper, VKUIWrapper } from './wrappers'; export default function Layout({ children, themeConfig, pageOpts }: NextraThemeLayoutProps) { return ( @@ -41,9 +42,19 @@ function InnerLayout({ children }: { children: React.ReactNode }) { renderComponent(themeConfig.navbar.component, { items: topLevelNavbarItems, })} - - {children} - + + {config.frontMatter.type === 'post' && } + + {children} + + {config.frontMatter.type === 'posts' && } + {config.frontMatter.type === 'tag' && } + ); diff --git a/packages/vkui-docs-theme/src/mdx/Callout/Callout.module.css b/packages/vkui-docs-theme/src/mdx/Callout/Callout.module.css index a9d8df6b1c..544092c03f 100644 --- a/packages/vkui-docs-theme/src/mdx/Callout/Callout.module.css +++ b/packages/vkui-docs-theme/src/mdx/Callout/Callout.module.css @@ -13,6 +13,11 @@ .content { margin-inline-start: var(--vkui--spacing_size_xl); + display: flex; + align-items: flex-start; + flex-direction: column; + justify-content: center; + min-inline-size: 0; } .root:not(:last-child) { diff --git a/packages/vkui-docs-theme/src/mdx/Code/Code.module.css b/packages/vkui-docs-theme/src/mdx/Code/Code.module.css index c4270fc968..5c7d6a37b9 100644 --- a/packages/vkui-docs-theme/src/mdx/Code/Code.module.css +++ b/packages/vkui-docs-theme/src/mdx/Code/Code.module.css @@ -9,6 +9,12 @@ color: var(--vkui--color_text_primary); } +/* stylelint-disable-next-line selector-max-type */ +pre .root { + display: flex; + flex-direction: column; +} + /* stylelint-disable-next-line selector-max-type */ .root span { /* stylelint-disable-next-line csstools/value-no-unknown-custom-properties */ diff --git a/packages/vkui-docs-theme/src/mdx/HeadingLink/HeadingLink.tsx b/packages/vkui-docs-theme/src/mdx/HeadingLink/HeadingLink.tsx index 34eb4c6ad4..3e5904eded 100644 --- a/packages/vkui-docs-theme/src/mdx/HeadingLink/HeadingLink.tsx +++ b/packages/vkui-docs-theme/src/mdx/HeadingLink/HeadingLink.tsx @@ -3,13 +3,13 @@ import { classNames } from '@vkontakte/vkjs'; import styles from './HeadingLink.module.css'; export function HeadingLink({ - tag: Tag, + Tag, children, id, className, ...props }: React.ComponentProps<'h2'> & { - tag: `h${2 | 3 | 4 | 5 | 6}`; + Tag: `h${2 | 3 | 4 | 5 | 6}`; }): React.ReactElement { return ( - - {/* TODO [docs] (@BlackySoul): добавить компонент */} - {/* TODO [docs] (@BlackySoul): */} -
{children}
- - ); -} - -function Main({ children }: { children: React.ReactNode }) { +export function Main({ children }: { children: React.ReactNode }) { const config = useConfig(); const themeConfig = useThemeConfig(); const { diff --git a/packages/vkui-docs-theme/src/mdx/Pre/CopyToClipboard/CopyToClipboard.tsx b/packages/vkui-docs-theme/src/mdx/Pre/CopyToClipboard/CopyToClipboard.tsx index 81b59333cf..3572279e9d 100644 --- a/packages/vkui-docs-theme/src/mdx/Pre/CopyToClipboard/CopyToClipboard.tsx +++ b/packages/vkui-docs-theme/src/mdx/Pre/CopyToClipboard/CopyToClipboard.tsx @@ -4,7 +4,7 @@ import { classNames, copyTextToClipboard } from '@vkontakte/vkjs'; import { IconButton } from '@vkontakte/vkui'; import styles from './CopyToClipboard.module.css'; -interface CopyToClipboardProps extends React.ComponentProps<'button'> { +export interface CopyToClipboardProps extends React.ComponentProps<'button'> { getValue: () => string; } diff --git a/packages/vkui-docs-theme/src/mdx/Pre/Pre.module.css b/packages/vkui-docs-theme/src/mdx/Pre/Pre.module.css index c52fab958b..0a06bbd2b5 100644 --- a/packages/vkui-docs-theme/src/mdx/Pre/Pre.module.css +++ b/packages/vkui-docs-theme/src/mdx/Pre/Pre.module.css @@ -4,6 +4,7 @@ overflow: auto; border: 1px solid var(--vkui_docs--color_stroke_separator_secondary); border-radius: 8px; + inline-size: 100%; --vkui_docs--code-background: transparent; --vkui_docs--code-inline-padding: 0px; @@ -21,9 +22,6 @@ } .buttons { - position: absolute; - inset-inline-end: var(--vkui--spacing_size_m); - inset-block-start: var(--vkui--spacing_size_m); display: flex; color: var(--vkui--color_icon_tertiary); } @@ -32,3 +30,15 @@ .buttons > *:not(:last-child) { margin-inline-end: var(--vkui--spacing_size_xs); } + +.buttonsAbsolute { + position: absolute; + inset-inline-end: var(--vkui--spacing_size_m); + inset-block-start: var(--vkui--spacing_size_m); +} + +.header { + background-color: var(--vkui--color_background_modal); + padding-inline: var(--vkui--spacing_size_4xl) var(--vkui--spacing_size_m); + border-block-end: 1px solid var(--vkui_docs--color_stroke_separator_secondary); +} diff --git a/packages/vkui-docs-theme/src/mdx/Pre/Pre.tsx b/packages/vkui-docs-theme/src/mdx/Pre/Pre.tsx index c9e576aa44..4a6d4cbd9c 100644 --- a/packages/vkui-docs-theme/src/mdx/Pre/Pre.tsx +++ b/packages/vkui-docs-theme/src/mdx/Pre/Pre.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { classNames } from '@vkontakte/vkjs'; -import { CopyToClipboard } from './CopyToClipboard/CopyToClipboard'; +import { Flex } from '@vkontakte/vkui'; +import { CopyToClipboard, type CopyToClipboardProps } from './CopyToClipboard/CopyToClipboard'; import styles from './Pre.module.css'; interface PreProps extends React.ComponentProps<'pre'> { @@ -19,19 +20,30 @@ export function Pre({ }: PreProps) { const preRef = React.useRef(null); + const getValue = () => preRef.current?.querySelector('code')?.textContent || ''; + return (
- {filename &&
{filename}
} + {filename && ( + + {filename} + {copy === '' && } + + )}
         {children}
       
- {copy === '' && ( -
- preRef.current?.querySelector('code')?.textContent || ''} - /> -
+ {copy === '' && !filename && ( + )}
); } + +function CopyButton({ getValue, className }: Pick) { + return ( +
+ +
+ ); +} diff --git a/packages/vkui-docs-theme/src/mdx/index.module.css b/packages/vkui-docs-theme/src/mdx/index.module.css index cfb3e5d084..ef5784a0db 100644 --- a/packages/vkui-docs-theme/src/mdx/index.module.css +++ b/packages/vkui-docs-theme/src/mdx/index.module.css @@ -24,6 +24,7 @@ line-height: 46px; font-weight: var(--vkui--font_weight_accent1); font-family: var(--vkui--font_family_accent); + color: var(--vkui--color_text_primary); } .strong { diff --git a/packages/vkui-docs-theme/src/mdx/index.tsx b/packages/vkui-docs-theme/src/mdx/index.tsx index e0eed77e89..91181d99e9 100644 --- a/packages/vkui-docs-theme/src/mdx/index.tsx +++ b/packages/vkui-docs-theme/src/mdx/index.tsx @@ -4,18 +4,18 @@ import { type DocsThemeConfig } from '../types'; import { Callout } from './Callout/Callout'; import { Code } from './Code/Code'; import { HeadingLink } from './HeadingLink/HeadingLink'; -import { Layout } from './Layout/Layout'; +import { Main } from './Main/Main'; import { Overview } from './Overview/Overview'; import { Pre } from './Pre/Pre'; import styles from './index.module.css'; const DEFAULT_COMPONENTS: MDXComponents = { h1: (props) =>

, - h2: (props) => , - h3: (props) => , - h4: (props) => , - h5: (props) => , - h6: (props) => , + h2: (props) => , + h3: (props) => , + h4: (props) => , + h5: (props) => , + h6: (props) => , ul: (props) =>
    , ol: (props) =>
      , li: (props) =>
    1. , @@ -26,7 +26,7 @@ const DEFAULT_COMPONENTS: MDXComponents = { pre: Pre, code: Code, strong: (props) => , - wrapper: Layout, + wrapper: Main, Overview, Callout, }; diff --git a/packages/vkui-docs-theme/src/mdx/Layout/Layout.module.css b/packages/vkui-docs-theme/src/wrappers/content/index.module.css similarity index 56% rename from packages/vkui-docs-theme/src/mdx/Layout/Layout.module.css rename to packages/vkui-docs-theme/src/wrappers/content/index.module.css index fc2d614f5a..9dcea099f4 100644 --- a/packages/vkui-docs-theme/src/mdx/Layout/Layout.module.css +++ b/packages/vkui-docs-theme/src/wrappers/content/index.module.css @@ -9,17 +9,16 @@ max-inline-size: var(--vkui_docs--max-width); } -.content { - padding-block: 36px 60px; +.blog { + max-inline-size: var(--vkui_docs_blog--max-width); + padding-block-start: var(--vkui--spacing_size_4xl); + padding-block-end: 60px; padding-inline: 40px; - inline-size: 100%; - min-inline-size: 0; - min-block-size: calc(100vh - var(--vkui_docs--navbar-height)); + flex-direction: column; } -@media (--viewWidth-tablet), (--viewWidth-tabletMinus) { - .content { +@media (--viewWidth-smallTabletMinus) { + .blog { padding-inline: var(--vkui--size_base_padding_horizontal--regular); - padding-block-start: 32px; } } diff --git a/packages/vkui-docs-theme/src/wrappers/content/index.tsx b/packages/vkui-docs-theme/src/wrappers/content/index.tsx new file mode 100644 index 0000000000..577b3138aa --- /dev/null +++ b/packages/vkui-docs-theme/src/wrappers/content/index.tsx @@ -0,0 +1,36 @@ +import type * as React from 'react'; +import { classNames } from '@vkontakte/vkui'; +import { Sidebar } from '../../components'; +import { useConfig } from '../../contexts'; +import styles from './index.module.css'; + +export function ContentWrapper({ children }: { children: React.ReactNode }) { + const config = useConfig(); + const { + activeThemeContext: themeContext, + docsDirectories, + flatDirectories, + directories, + } = config.normalizePagesResult; + + const isFullLayout = themeContext.layout === 'full'; + const isBlog = config.isBlog; + const Component = isBlog ? 'main' : 'div'; + + return ( + + + {/* TODO [docs] (@BlackySoul): добавить компонент */} + {/* TODO [docs] (@BlackySoul): */} + {children} + + ); +} diff --git a/packages/vkui-docs-theme/src/wrappers/index.ts b/packages/vkui-docs-theme/src/wrappers/index.ts index f95471eb62..24b59f460d 100644 --- a/packages/vkui-docs-theme/src/wrappers/index.ts +++ b/packages/vkui-docs-theme/src/wrappers/index.ts @@ -1 +1,2 @@ export { VKUIWrapper } from './vkui'; +export { ContentWrapper } from './content'; diff --git a/packages/vkui-docs-theme/styles/constants.css b/packages/vkui-docs-theme/styles/constants.css index d072ad6949..493dd41df8 100644 --- a/packages/vkui-docs-theme/styles/constants.css +++ b/packages/vkui-docs-theme/styles/constants.css @@ -2,6 +2,7 @@ :root { --vkui_docs--navbar-height: 60px; --vkui_docs--max-width: 1440px; + --vkui_docs_blog--max-width: 1024px; } @media (--viewWidth-desktopPlus) { diff --git a/website/pages/_meta.tsx b/website/pages/_meta.tsx index 96a7b91a4f..087ed9bf24 100644 --- a/website/pages/_meta.tsx +++ b/website/pages/_meta.tsx @@ -1,10 +1,7 @@ export default { - index: { + overview: { type: 'page', - title: 'Главная', - theme: { - layout: 'raw', - }, + title: 'О системе', }, components: { type: 'page', @@ -22,8 +19,12 @@ export default { href: 'https://vkcom.github.io/vkui-tokens/', newWindow: true, }, - overview: { + blog: { type: 'page', - title: 'О системе', + title: 'Блог', + theme: { + breadcrumb: false, + layout: 'full', + }, }, }; diff --git a/website/pages/blog.mdx b/website/pages/blog.mdx new file mode 100644 index 0000000000..27dba91e8c --- /dev/null +++ b/website/pages/blog.mdx @@ -0,0 +1,13 @@ +--- +type: posts +title: Blog +--- + + +# Блог + +Здесь мы будем делиться с вами наиболее важными новостями, планами и релизами нашей библиотеки, +а также постараемся собирать лучшие советы и практики в цикл статей, +которые позволят ускорить процесс разработки ваших приложений. + + diff --git a/website/pages/blog/_meta.tsx b/website/pages/blog/_meta.tsx new file mode 100644 index 0000000000..725ff3f30a --- /dev/null +++ b/website/pages/blog/_meta.tsx @@ -0,0 +1,5 @@ +export default { + '*': { + display: 'hidden', + }, +}; diff --git a/website/pages/blog/tags/[tag].mdx b/website/pages/blog/tags/[tag].mdx new file mode 100644 index 0000000000..b9cad002d6 --- /dev/null +++ b/website/pages/blog/tags/[tag].mdx @@ -0,0 +1,23 @@ +--- +type: tag +--- + +import { TagName, TagTitle } from '@vkontakte/vkui-docs-theme'; + + + +# Посты по теме “” + + + +export { getStaticPathsTags as getStaticPaths } from '@vkontakte/vkui-docs-theme'; + +export function getStaticProps({ params }) { + return { + props: { + ssg: { + tag: params?.tag, + }, + }, + }; +} diff --git a/website/pages/blog/testing-vkui-apps.mdx b/website/pages/blog/testing-vkui-apps.mdx new file mode 100644 index 0000000000..0b322803f5 --- /dev/null +++ b/website/pages/blog/testing-vkui-apps.mdx @@ -0,0 +1,181 @@ +--- +type: post +title: Тестирование VKUI-приложений +date: 2024/12/24 +description: Разберём основные принципы тестирования приложений на базе VKUI. +tag: testing +image: /blog/vkui-testing.png +--- + +## Тестирование с помощью Jest и React Testing Library + +Несмотря на то, что существует несколько различных инструментов по тестированию React-приложений, +мы рекомендуем использовать `Jest` и `React Testing Library` для функционального или unit-тестирования VKUI-приложений. +В данном руководстве мы рассмотрим основные подходы к тестированию, используя данный стек технологий. +Его же мы применяем сами для тестирования компонентов VKUI. + + + Предполагается, что вы уже обладаете базовыми знаниями работы с выбранными инструментами, если это + не так, то обратитесь к документации [Jest](https://jestjs.io/) и [React Testing + Library](https://testing-library.com/docs/react-testing-library/intro). В данном руководстве мы + сосредоточимся именно на специфичных настройках под VKUI. + + +### Jest + +Специфичной настройки `Jest` для тестирования VKUI-компонентов не требуется, но могут возникнуть трудности +с рядом компонентов, которые опираются на браузерное API. Подробнее об этом написано в [секции ниже](#special-components). + +Для корректной работы VKUI-компонентов необходимо использовать обязательную обёртку, об этом упоминается +в [Быстром старте](/overview/quick-start#step4). + +### React Testing Library + +Для того, чтобы тесты было проще поддерживать, рекомендуется избегать завязки на детали имплементации компонентов. +Не полагаясь на внутреннее устройство, вы делаете свои тесты более устойчивыми к изменениям структуры в VKUI, +которые могут произойти даже в рамках минорных изменений. +В VKUI мы стараемся снабжать все компоненты функциональными ролями и атрибутами или обеспечивать их поддержку, +поэтому в связке с `React Testing Library`, которая позволяет работает с настоящим DOM-представлением, вы можете +взаимодействовать с компонентами максимально близко к пользовательскому опыту. + +Например, компонент `Checkbox` по умолчанию имеет `role="checkbox"`, поэтому вы можете найти этот компонент, +используя специальное API, предоставляемое `React Testing Library`: + +```jsx +import { render, screen } from '@testing-library/react'; + +// ✅ - используем функциональную роль +render(Text); +const checkbox = screen.getByRole('checkbox'); + +// ❌ - завязываемся на класс, который может измениться +render(Text); +const checkbox = document.querySelector('.CheckboxInput__input'); +``` + +Обратите внимание, что в случае, если компонент представляет собой композицию более мелких компонентов, +не всегда возможно опираться на функциональную роль или атрибут, поэтому в компонент есть возможность прокинуть +`data-testid` для нужной части. Например: + +```jsx +import { render, screen } from '@testing-library/react'; + +render(); + +const prevButton = screen.getByTestId('prevButton'); +expect(prevButton).toBeDisabled(); +``` + +Если вы все-таки не можете протестировать нужную часть без необходимости завязываться на, например, +внутренний класс, создайте нам [feature request](https://github.com/VKCOM/VKUI/issues/new/choose) на Github. + +## Особые компоненты [#special-components] + +### Snackbar, Spinner, SplitCol + +Некоторые компоненты зависят от браузерного API, которое не реализовано в окружении `Jest`, поэтому для +тестирования подобных компонентов вам необходимо предоставить `mock`-реализацию самостоятельно. +Подробнее читайте в [этом руководстве](https://jestjs.io/docs/manual-mocks). + +### Select + +Так как VKUI стремится к мимикрии под различные платформы, некоторые компоненты могут существенно отличаться +в структуре. Например, это справедливо для компонента `Select`, который на мобильных платформах (`android/iOS`) +представляет собой нативный элемент и на десктопе его кастомную реализацию. + +Из-за поддержки SSR мы не всегда можем заранее знать, какое представление необходимо отрисовать. +Если попытаться отрендерить `Select` без указания платформы, то в DOM-представлении у нас окажется две реализации. +Чтобы избежать ошибок, проще всего обернуть `Select` в `PlatformProvider` с указанием `vkcom` платформы для +`web`-реализации или `android` для мобильной платформы. + +```jsx +import { render } from '@testing-library/react'; +import { PlatformProvider, Select } from '@vkontakte/vkui'; + +// тестируем web-представление +render( + + + , +); +``` + +Для тестирования мобильного представления, вы можете воспользоваться стандартным API от `React Testing Library` и `Testing Library`: + +```jsx +import { render, screen } from '@testing-library/react'; +import { userEvent } from './testing/helpers'; +import { PlatformProvider, Select } from '@vkontakte/vkui'; + +render( + + + , +); + +fireEvent.click(screen.getByTestId('labelTextTestId')); +const unselectedOption = screen.getByRole('option', { selected: false, name: 'Josh' }); +fireEvent.mouseEnter(unselectedOption); +fireEvent.click(unselectedOption); + +expect(screen.getByTestId('labelTextTestId').textContent).toEqual('Josh'); +``` + + + Обратите внимание, выше мы используем `labelTextTestId` вместо просто `data-testid`, это позволяет + эмулировать работу мыши или тач-устройства и быстрее получить доступ к выбранной опции. + + diff --git a/website/pages/blog/vkui-v7-release.mdx b/website/pages/blog/vkui-v7-release.mdx new file mode 100644 index 0000000000..a03f48b669 --- /dev/null +++ b/website/pages/blog/vkui-v7-release.mdx @@ -0,0 +1,79 @@ +--- +type: post +title: Релиз VKUI v7 +date: 2024/12/24 +description: Версия VKUI v7 вышла из беты и теперь доступна для широкого использования. +tag: release +image: /blog/v7-release.png +--- + +Спешим сообщить, что мы завершили бета-тестирование VKUI v7 и теперь эта версия полностью стабильна. +Это означает, что мы прекращаем поддержку версии v6 и рекомендуем всем пользователям библиотеки обновиться, +чтобы иметь возможность использовать новые возможности и вовремя получать исправления. + +## ⚙️ Сборка + +- библиотека теперь поставляется только в `ESM`-формате, `CommonJS` сборка была удалена. `ESM` обрёл + достаточно широкую поддержку, поэтому мы решили отказаться от устаревшего формата. + +- поднята целевая версия `ECMAScript` для компиляции до `es2017`, а также подняты минимальные версии поддерживаемых браузеров: + + ``` + ChromeAndroid >= 63 + iOS >= 12 + Chrome >= 63 + Firefox >= 55 + Edge >= 79 + Opera >= 50 + Safari >= 12 + Samsung >= 8.2 + ``` + + Это позволит нам использовать новое `API` без необходимости подключать полифиллы. + +- изменились названия `CSS`-классов, теперь они формируются на основе `CSS Modules`. В будущем мы бы хотели + отказаться от статичных классов, поэтому исключите их использование в своем коде. + +## 🧩 Компоненты + +У многих компонентов изменилось публичное `API`, это связано с тем, что мы стремимся улучшить +разработческий опыт при общении с дизайном, и в этом релизе разом сократили расхождения по названиям параметров `React` с `Figma`. +Так же мы избавились от `deprecated`-свойств и параметров и унифицировали `API` разных компонентов. + +### Модальные окна + +В новой версии мы существенно изменили подход к определению модальных окон и исправили бо́льшее количество багов. + +- `ModalPage`/`ModalCard` можно определять в любом месте приложения, без необходимости оборачивать в `ModalRoot` +- `ModalRoot` теперь нужен только для последовательно открывающихся модальных окон, есть возможность создавать динамические модальные окна + +### SplitLayout + +Теперь этот компонент не является обязательным для использования при наличии в приложении всплывающих окон. +Свойства `popout` и `modal` отмечены как `@deprecated` и будут удалены в следующем релизе. + +### DatePicker + +Данный компонент был удален в пользу `Input`, `Select` и `DateInput`. Для выбора подходящей замены мы +рекомендуем руководствоваться следующим [обсуждением](https://github.com/VKCOM/VKUI/discussions/7070). + +### Типографика + +Мы изменили поведение по-умолчанию для всех типографических элементов - отключили использование акцентных начертаний, +если вам необходимо вернуть прежнее поведение, то используйте `useAccentWeight={true}`. + +## 🌗 Светлая/темная тема + +Ранее для для указания светлой или тёмной темы мы использовали название `Appearance`, +что совпадало с названием параметров некоторых компонентов (например, в компоненте `Button`), что могло путать, +поэтому мы пришли к названию `ColorScheme` (так это свойство называется в CSS). + +В связи с этим изменились названия типов, хуков и провайдеров, которые отвечали за взаимодействие со светлой и темной темами. + +## 🚀 Хочу обновиться + +Если вы готовы обновиться, то рекомендуем начать с [инструкции по миграции](https://vkcom.github.io/VKUI/#/Migrations). +Большинство изменений не придется делать вручную - у нас подготовлены автоматизации, +о применении которых вы сможете узнать из инструкции выше. +Если у вас возникнут какие-то сложности, вы всегда можете задать вопрос в [Дискуссиях](https://github.com/VKCOM/VKUI/discussions) на Github +или в [публичном чате](https://vk.cc/c1CHgf) в VK Messenger. diff --git a/website/pages/index.mdx b/website/pages/index.mdx deleted file mode 100644 index cbfee35337..0000000000 --- a/website/pages/index.mdx +++ /dev/null @@ -1 +0,0 @@ -# UNDER CONSTRUCTION diff --git a/website/public/blog/v7-release.png b/website/public/blog/v7-release.png new file mode 100644 index 0000000000..5b507b4813 Binary files /dev/null and b/website/public/blog/v7-release.png differ diff --git a/website/public/blog/vkui-testing.png b/website/public/blog/vkui-testing.png new file mode 100644 index 0000000000..55df1f1b59 Binary files /dev/null and b/website/public/blog/vkui-testing.png differ