diff --git a/package.json b/package.json index 74a39b403573..067481320c7c 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ ] }, "lint-staged": { - "*.{scss}": [ + "*.scss": [ "stylelint --fix --quiet" ], "*.{js,jsx,ts,tsx}": [ diff --git a/src/api/.mocks/en/pages/graph.json b/src/api/.mocks/en/pages/graph.json new file mode 100644 index 000000000000..3b29fcce63e9 --- /dev/null +++ b/src/api/.mocks/en/pages/graph.json @@ -0,0 +1,74 @@ +{ + "id": 71, + "name": "blog/graph", + "createdAt": "2026-01-15T13:00:46.903Z", + "updatedAt": "2026-01-15T13:00:46.903Z", + "type": "default", + "isDeleted": false, + "versionOnTranslationId": null, + "searchCategorySlug": "blog", + "regions": [], + "pageId": 71, + "regionCode": "en", + "publishedVersionId": 216, + "lastVersionId": 216, + "content": "blocks:\n - type: blog-header-block\n resetPaddings: true\n paddingBottom: l\n width: m\n verticalOffset: m\n theme: dark\n background:\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png\n disableCompress: true\n color: '#CCDAFF'\n fullWidth: false\n - type: blog-layout-block\n resetPaddings: true\n mobileOrder: reverse\n children:\n - type: blog-yfm-block\n resetPaddings: true\n column: right\n text: >\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/speaker.jpg\n =80x)\n\n\n **Andrey Shchetinin**\n\n Senior Frontend Developer\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: |\n\n In this article:\n\n - [Where the task came from](#task)\n - [How we arrived at the solution](#solution)\n - [Customization](#customization)\n - [Our graph library: what the benefits are and how to use it](#library)\n - [Are there any alternatives?](#analogs)\n - [Plans for the future](#future)\n - [Try it and join in](#try)\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Hi! My name is Andrey, I’m a frontend developer on the User Experience\n team for Yandex infrastructure services. We develop Gravity UI—an\n open-source design system and React component library used by dozens\n of products inside the company and beyond. Today I’ll расскажу how we\n ran into the task of visualizing complex graphs, why existing\n solutions didn’t work for us, and how @gravity‑ui/graph ended up being\n created—a library we decided to open to the community.\n\n\n This story started with a practical problem: we needed to render\n graphs with 10,000+ elements and interactive components. At Yandex\n there are many projects where users build complex data processing\n pipelines—from simple ETL processes to machine learning. When such\n pipelines are created programmatically, the number of blocks can reach\n tens of thousands.\n\n\n Existing solutions didn’t work for us:\n\n * **HTML/SVG libraries** look great and are convenient to develop with, but they start lagging already at hundreds of elements.\n * **Canvas solutions** handle performance, but require a huge amount of code to build complex UI elements.\n\n Drawing a button with rounded corners and a gradient in Canvas isn’t\n hard. However, problems appear when you need to create complex custom\n controls or layout—you’ll have to write dozens of lines of low-level\n drawing commands. Each UI element has to be programmed from\n scratch—from click handling to animations. And we needed full-fledged\n UI components: buttons, selects, input fields, drag-and-drop.\n\n\n We decided not to choose between Canvas and HTML, but to use the best\n of both technologies. The idea was simple: automatically switch\n between modes depending on how close the user is looking at the graph.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic1.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n #### Try it yourself\n\n * [GitHub repository](https://github.com/gravity-ui/graph){target=\"_blank\"}\n * [Storybook with examples](https://preview.gravity-ui.com/graph/){target=\"_blank\"}\n * [Playground](https://gravity-ui.com/ru/libraries/graph/playground){target=\"_blank\"}\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Where the task came from{#task}\n\n\n ### Nirvana and its graphs\n\n\n At Yandex we have a service called Nirvana for creating and running\n data-processing graphs (we wrote about it\n [here](https://habr.com/ru/companies/yandex/articles/351016/){target=\"_blank\"}\n back in 2018). It’s a large, popular service that has been around for\n a long time.\n\n\n Some users build graphs by hand—dragging with the mouse, adding\n blocks, connecting them. With those graphs there’s no problem: there\n aren’t many blocks, and everything works great. But there are projects\n that create graphs programmatically. And that’s where the difficulties\n start: they can put up to 10,000 operations into a single graph. And\n you end up with this:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n {% cut \"And this:\" %}\n\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-1.png\n =830x)\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-2.png\n =830x)\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-3.png\n =830x)\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-4.png\n =830x)\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-5.png\n =830x)\n\n\n {% endcut %}\n\n\n A typical HTML + SVG combo simply can’t handle such graphs. The\n browser starts lagging, memory leaks, the user suffers. We tried to\n solve the problem head-on: optimize HTML rendering, but sooner or\n later we hit physical limits—DOM is simply not designed for thousands\n of simultaneously visible floating interactive elements.\n\n\n We needed a different solution, and in the browser we only had Canvas\n left. Only it can provide the required performance.\n\n\n The first thought was to find a ready-made solution. It was 2017–2018,\n and we went through popular libraries for Canvas or graph rendering,\n but all solutions ran into the same problem: either use Canvas with\n primitive elements, or use HTML/SVG and sacrifice performance.\n\n\n What if we don’t choose?\n\n\n ### Level of Details: inspiration from GameDev\n\n\n In GameDev and cartography there’s a great concept—Level of Details\n (LOD). This technique was born out of necessity—how do you show a huge\n world without killing performance?\n\n\n The idea is simple: a single object can have several levels of detail\n depending on how closely it’s viewed. In games it’s especially\n noticeable:\n\n * Far away you see mountains—simple polygons with a basic texture.\n * As you get closer—details appear: grass, rocks, shadows.\n * Even closer—you can see individual leaves on trees.\n\n Nobody renders millions of grass polygons when the player is standing\n on a mountain peak looking into the distance.\n\n\n In maps, the principle is the same—each zoom level has its own dataset\n and its own detail level:\n\n * Continent scale—only countries are visible.\n * Zooming into a city—streets and districts appear.\n * Even closer—house numbers, cafés, bus stops.\n\n We realized: the user doesn’t need interactive buttons at a\n large-scale view of a graph with 10,000 blocks—they won’t see them\n anyway and won’t be able to work with them.\n\n\n Moreover, attempting to render 10,000 HTML elements at once will\n freeze the browser. But when the user zooms into a specific area, the\n number of visible blocks drops sharply—from 10,000 to, say, 50. That’s\n when resources are freed up for HTML components with rich\n interactivity.\n\n\n ### Three levels in our Level of Details scheme\n\n\n #### Minimalistic (scale 0.1–0.3) — Canvas with simple primitives\n\n\n In this mode the user sees the overall architecture of the system:\n where the main groups of blocks are located and how they are\n connected. Each block is a simple rectangle with basic color coding.\n No text, buttons, or detailed icons. But you can comfortably render\n thousands of elements. At this level the user selects an area for\n detailed exploration.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic3.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n #### Schematic (scale 0.3–0.7) — Canvas with details\n\n\n Block names, status icons, and connection anchors appear. Text is\n rendered via the Canvas API—this is fast, but styling options are\n limited. Connections between blocks become more informative: you can\n show the direction of the data flow, the connection status. This is a\n transitional mode where Canvas performance is combined with basic\n informativeness.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic4.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n #### Detailed (scale 0.7+) — HTML with full interactivity\n\n\n Here blocks turn into full-fledged UI components: with control\n buttons, parameter fields, progress bars, selects. You can use any\n HTML/CSS capabilities and plug in UI libraries. In this mode the\n viewport typically contains no more than 20–50 blocks, which is\n comfortable for detailed work.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic5.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n **What if we use FPS to choose the level of detail?**\n\n\n We had approaches to selecting the level of detail based on FPS. But\n it turned out that this approach creates instability—when performance\n increases, the system switches to a more detailed mode, which lowers\n FPS and can cause switching back—and so on in a loop.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## How we arrived at the solution{#solution}\n\n\n Okay, LOD is cool. But implementation requires Canvas for performance,\n and that’s a new headache. Drawing on Canvas isn’t very hard—problems\n appear when you need interactivity.\n\n\n ### Problem: how do we understand where the user clicked?\n\n\n In HTML it’s simple: click a button—you immediately get an event on\n that element. With Canvas it’s harder: you click the canvas—and then\n what? You have to figure out yourself which element the user clicked.\n\n\n Basically there are three approaches:\n\n * Pixel Testing (color picking),\n * Geometric approach (simple iteration over all elements),\n * Spatial Indexing (spatial index).\n\n #### Pixel Testing (color picking)\n\n\n The idea is simple: create a second invisible canvas, copy the scene\n there, but fill each element with a unique color that will be treated\n as the object ID. On click, read the pixel color under the mouse\n pointer via getImageData and thus get the element ID.\n\n\n #|\n\n ||**Pros**|**Cons**||\n\n ||* Implemented in a couple dozen lines\n\n\n * Doesn’t require additional data structures|* Canvas anti-aliasing\n blends colors—clicking on a shape boundary can produce an “invalid” ID\n\n\n * Disabling anti-aliasing in 2D Canvas is not available\n\n\n * A second canvas duplicates memory and doubles the render pass||\n\n |#\n\n\n For small scenes the method is fine, but with 10,000+ elements the\n error rate becomes unacceptable—so we set Pixel Testing aside.\n\n\n #### Geometric approach (simple iteration over all elements)\n\n\n The idea is simple: iterate over all elements and check whether the\n click point lies inside the element.\n\n\n #|\n\n ||**Pros**|**Cons**||\n\n ||* Implemented in a couple dozen lines\n\n\n * Doesn’t require additional data structures|* Very slow with a large\n number of elements\n\n\n * Not suitable for large scenes||\n\n |#\n\n\n #### Spatial Indexing\n\n\n An evolution of the geometric approach. In the geometric approach we\n hit the number of elements. Spatial indexing algorithms try to group\n nearby elements in some way, mostly using trees, which makes it\n possible to reduce complexity to log n.\n\n\n There are quite a lot of spatial indexing algorithms; we chose the\n R-Tree data structure via the\n [rbush](https://github.com/mourner/rbush){target=\"_blank\"} library.\n\n\n R-Tree is, as the name suggests, a tree where each object is placed\n into a minimum bounding rectangle (MBR), and then those rectangles are\n grouped into larger rectangles. This produces a tree where each\n rectangle contains other rectangles.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: >-\n Image from Wikipedia\n [R‑tree](https://en.wikipedia.org/wiki/R-tree){target=\"_blank\"}\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic6.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n To search in an RTree, we need to descend the tree (into the\n rectangles) until we reach a specific element. The path is chosen by\n checking intersection of the search rectangle with MBRs. All branches\n whose bounding boxes don’t even touch the search rectangle are\n discarded immediately—that’s why traversal depth is usually limited to\n 3–5 levels, and the search itself takes microseconds even with tens of\n thousands of elements.\n\n\n This option works, although slower (O(log n) in the best case and O(n)\n in the worst) than pixel testing, but it is more accurate and less\n demanding on memory.\n\n\n #### Event model\n\n\n Based on the RTree, we can now build our event model. When the user\n clicks, a hit-test procedure runs: we form a 1×1 pixel rectangle at\n the cursor coordinates and search for its intersections in the R-Tree.\n Having obtained the element that this rectangle hits, we delegate the\n event to that element. If the element did not stop the event, then it\n is passed to its parent, and so on up to the root. The behavior of\n this model resembles the familiar event model in the browser. Events\n can be intercepted, prevented, or propagation can be stopped.\n\n\n As I mentioned, during hit-testing we form a 1×1 pixel rectangle,\n which means we can form a rectangle of any size. And this will help us\n implement another very important optimization—Spatial Culling.\n\n\n ### Spatial Culling\n\n\n Spatial Culling is a rendering optimization technique aimed at not\n drawing what is not visible. For example, not drawing objects that are\n outside the camera space or that are occluded by other scene elements.\n Since our graph is drawn in 2D space, it is sufficient for us to not\n draw only those objects that are outside the camera’s visible area\n (viewport).\n\n\n How it works:\n\n * on each camera pan or zoom we form a rectangle equal to the current viewport;\n * search for its intersections in the R-Tree;\n * the result is a list of elements that are actually visible;\n * we render only them; everything else is skipped.\n\n This technique makes performance almost independent of the total\n number of elements: if 40 blocks fit in the frame, the library will\n draw exactly 40, not tens of thousands hidden beyond the screen. At\n far zoom levels, many elements fall into the viewport, so we draw\n lightweight Canvas primitives; as the camera zooms in, the number of\n elements decreases and freed resources allow switching to HTML mode\n with full detail.\n\n\n Putting it all together, we get a simple scheme:\n\n * Canvas is responsible for speed,\n * HTML—for interactivity,\n * R-Tree and Spatial Culling seamlessly combine them into a single system, allowing us to quickly understand which elements can be drawn on the HTML layer.\n\n While the camera moves, the small viewport asks the R-Tree only for\n those objects that are actually in the frame. This approach allows us\n to draw truly large graphs, or at least have a performance reserve\n until the user narrows the viewport.\n\n\n So at its core the library contains:\n\n * a Canvas mode with simple primitives;\n * an HTML mode with full detail;\n * R-Tree and Spatial Culling for performance optimization;\n * a familiar event model.\n\n But that’s not enough for production: we need the ability to extend\n the library and customize it to our needs.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Customization{#customization}\n\n\n The library offers two complementary ways to extend and change\n behavior:\n\n * Overriding base components. Change the logic of standard Block, Anchor, Connection.\n * Extending via layers (Layers). Add fundamentally new functionality above/below the existing scene.\n\n ### Overriding components\n\n\n When you need to modify the appearance or behavior of existing\n elements, inherit from the base class and override key methods. Then\n register the component under your own name.\n\n\n #### Block customization\n\n\n For example, if you need to create a graph with progress bars on\n blocks—say, to display task execution status in a pipeline—you can\n easily customize the standard blocks:\n\n\n ```json\n\n import { CanvasBlock } from \"@gravity‑ui/graph\";\n\n\n class ProgressBlock extends CanvasBlock {\n // Base block shape with rounded corners\n public override renderBody(ctx: CanvasRenderingContext2D): void {\n ctx.fillStyle = \"#ddd\";\n ctx.beginPath();\n ctx.roundRect(this.state.x, this.state.y, this.state.width, this.state.height, 12);\n ctx.fill();\n ctx.closePath();\n }\n\n public renderSchematicView(ctx: CanvasRenderingContext2D): void {\n const progress = this.state.meta?.progress || 0;\n\n // Draw the block base\n this.renderBody(ctx);\n\n // Progress bar with color indication\n const progressWidth = (this.state.width - 20) * (progress / 100);\n ctx.fillStyle = progress < 50 ? \"#ff6b6b\" : progress < 80 ? \"#feca57\" : \"#48cae4\";\n ctx.fillRect(this.state.x + 10, this.state.y + this.state.height - 15, progressWidth, 8);\n\n // Progress bar border\n ctx.strokeStyle = \"#ddd\";\n ctx.lineWidth = 1;\n ctx.strokeRect(this.state.x + 10, this.state.y + this.state.height - 15, this.state.width - 20, 8);\n\n // Text with percentages and name\n ctx.fillStyle = \"#2d3436\";\n ctx.font = \"12px Arial\";\n ctx.textAlign = \"center\";\n ctx.fillText(`${Math.round(progress)}%`, this.state.x + this.state.width / 2, this.state.y + 20);\n ctx.fillText(this.state.name, this.state.x + this.state.width / 2, this.state.y + 40);\n }\n }\n\n ```\n\n\n #### Connection customization\n\n\n Similarly, if you need to change the behavior and appearance of\n connections—for example, to show data flow intensity between\n blocks—you can create a custom connection:\n\n\n ```json\n\n import { BlockConnection } from \"@gravity-ui/graph\";\n\n\n class DataFlowConnection extends BlockConnection {\n public override style(ctx: CanvasRenderingContext2D) {\n // Get flow data from the connected blocks\n const sourceBlock = this.sourceBlock;\n const targetBlock = this.targetBlock;\n\n const sourceProgress = sourceBlock?.state.meta?.progress || 0;\n const targetProgress = targetBlock?.state.meta?.progress || 0;\n\n // Compute flow intensity based on block progress\n const flowRate = Math.min(sourceProgress, targetProgress);\n const isActive = flowRate > 10; // Flow is active when progress > 10%\n\n if (isActive) {\n // Active flow -- thick green line\n ctx.strokeStyle = \"#00b894\";\n ctx.lineWidth = Math.max(2, Math.min(6, flowRate / 20));\n } else {\n // Inactive flow -- dashed gray line\n ctx.strokeStyle = \"#ddd\";\n ctx.lineWidth = this.context.camera.getCameraScale();\n ctx.setLineDash([5, 5]);\n }\n\n return { type: \"stroke\" };\n }\n }\n\n ```\n\n\n #### Using custom components\n\n\n Register the created components in the graph settings:\n\n\n ```json\n\n const customGraph = new Graph({\n blocks: [\n {\n id: \"task1\",\n is: \"progress\",\n x: 100,\n y: 100,\n width: 200,\n height: 80,\n name: \"Data Processing\",\n meta: { progress: 75 },\n },\n {\n id: \"task2\",\n is: \"progress\",\n x: 400,\n y: 100,\n width: 200,\n height: 80,\n name: \"Analysis\",\n meta: { progress: 30 },\n },\n {\n id: \"task3\",\n is: \"progress\",\n x: 700,\n y: 100,\n width: 200,\n height: 80,\n name: \"Output\",\n meta: { progress: 5 },\n },\n ],\n connections: [\n { sourceBlockId: \"task1\", targetBlockId: \"task2\" },\n { sourceBlockId: \"task2\", targetBlockId: \"task3\" },\n ],\n settings: {\n // Register custom blocks\n blockComponents: {\n 'progress': ProgressBlock,\n },\n // Register a custom connection for all links\n connection: DataFlowConnection,\n useBezierConnections: true,\n },\n });\n\n\n customGraph.setEntities({\n blocks: [\n {\n is: 'progress',\n id: '1',\n name: \"progress block',\n x: 10, \n y: 10, \n width: 10, \n height: 10,\n anchors: [],\n selected: false,\n }\n ]\n })\n\n\n customGraph.start();\n\n ```\n\n\n #### Result\n\n\n The result is a graph where:\n\n * blocks show current progress with color indication;\n * connections visualize data flow: active flows are green and thick, inactive ones are gray and dashed;\n * when zooming, blocks automatically switch to HTML mode with full interactivity.\n\n ### Extending with layers\n\n\n Layers are additional Canvas or HTML elements that are inserted into\n the graph “space”. Essentially, each layer is a separate rendering\n channel that can contain its own canvas for fast graphics or an HTML\n container for complex interactive elements.\n\n\n By the way, this is exactly how React integration works in our\n library: React components are rendered into the HTML layer via a React\n Portal.\n\n\n #### Layer architecture\n\n\n Layers are another key solution to the Canvas vs HTML dilemma. Layers\n synchronize the positions of Canvas and HTML elements, ensuring they\n overlay correctly. This makes it possible to seamlessly switch between\n Canvas and HTML while staying in a single coordinate space. The graph\n consists of independent layers stacked on top of each other:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic7.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Layers can work in two coordinate systems:\n\n * Attached to the graph (`transformByCameraPosition: true`):\n\n * elements move together with the camera,\n * blocks, connections, graph elements.\n\n * Fixed on the screen (`transformByCameraPosition: false`):\n\n * stay in place when panning,\n * toolbars, legends, UI controls.\n\n #### How React integration works\n\n\n A layer with React integration is quite illustrative for demonstrating\n what layers are. First, let’s look at a component that highlights the\n list of blocks that are in the camera’s visible area. To do this, we\n need to subscribe to camera changes and after each change check the\n intersection of the camera viewport with the elements’ hitboxes.\n\n\n ```json\n\n import { Graph } from \"@gravity-ui/graph\";\n\n\n const BlocksList = ({ graph, renderBlock }: { graph: Graph,\n renderBlock: (graph: Graph, block: TBlock) => React.JSX.Element }) =>\n {\n const [blocks, setBlocks] = useState([]);\n\n const updateVisibleList = useCallback(() => {\n const cameraState = graph.cameraService.getCameraState();\n const CAMERA_VIEWPORT_TRESHOLD = 0.5;\n const x = -cameraState.relativeX - cameraState.relativeWidth * CAMERA_VIEWPORT_TRESHOLD;\n const y = -cameraState.relativeY - cameraState.relativeHeight * CAMERA_VIEWPORT_TRESHOLD;\n const width = -cameraState.relativeX + cameraState.relativeWidth * (1 + CAMERA_VIEWPORT_TRESHOLD) - x;\n const height = -cameraState.relativeY + cameraState.relativeHeight * (1 + CAMERA_VIEWPORT_TRESHOLD) - y;\n \n const blocks = graph\n .getElementsOverRect(\n {\n x,\n y,\n width,\n height,\n }, // defines the area in which the list of blocks will be searched\n [CanvasBlock] // defines the element types that will be searched in the camera viewport\n ).map((component) => component.connectedState); // Get the list of block models\n\n setBlocks(blocks);\n });\n\n useGraphEvent(graph, \"camera-change\", ({ scale }) => {\n if (scale >= 0.7) {\n // If the scale is greater than 0.7, then update the list of blocks\n updateVisibleList()\n return;\n }\n setBlocks([]);\n });\n\n return blocks.map(block => {renderBlock(graphObject, block)})\n }\n\n ```\n\n\n Now let’s look at the description of the layer itself that will use\n this component.\n\n\n ```json\n\n import { Layer } from '@gravity-ui/graph';\n\n\n class ReactLayer extends Layer {\n constructor(props: TReactLayerProps) {\n super({\n html: {\n zIndex: 3, // bring the layer above the other layers\n classNames: [\"no-user-select\"], // add a class to disable text selection\n transformByCameraPosition: true, // layer is attached to the camera - now the layer will move together with the camera\n },\n ...props,\n });\n }\n\n public renderPortal(renderBlock: (block: T) => React.JSX.Element) {\n if (!this.getHTML()) {\n return null;\n }\n\n const htmlLayer = this.getHTML() as HTMLDivElement;\n\n return createPortal(\n React.createElement(BlocksList, {\n graph: this.context.graph,\n renderBlock: renderBlock,\n }),\n htmlLayer,\n );\n }\n }\n\n ```\n\n\n Now we can use this layer in our application.\n\n\n ```json\n\n import { Flex } from \"@gravity-ui/uikit\";\n\n\n const graph = useMemo(() => new Graph());\n\n const containerRef = useRef();\n\n\n useEffect(() => {\n if (containerRef.current) {\n graph.attach(containerRef.current);\n }\n\n return () => {\n graph.detach();\n };\n }, [graph, containerRef]);\n\n\n const reactLayer = useLayer(graph, ReactLayer, {});\n\n\n const renderBlock = useCallback((graph, block) => {block.name})\n\n return (\n
\n
\n {graph && reactLayer && reactLayer.renderPortal(renderBlock)}\n
\n
\n );\n ```\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Overall, everything is quite simple. Nothing described above needs to\n be written by yourself—everything is already implemented and ready to\n use.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Our graph library: what the benefits are and how to use\n it{#library}\n\n\n When we started working on the library, the main question was: how do\n we make it so that a developer doesn’t have to choose between\n performance and development convenience? The answer turned out to be\n automating that choice.\n\n\n ### Benefits\n\n\n #### Performance + convenience\n\n\n [@gravity‑ui/graph](https://github.com/gravity-ui/graph){target=\"_blank\"}\n automatically switches between Canvas and HTML depending on the scale.\n This means you get:\n\n * Stable 60 FPS on graphs with thousands of elements.\n * The ability to use full-fledged HTML components with rich interactivity when viewing in detail.\n * A single event model regardless of rendering method—click, mouseenter work the same on Canvas and in HTML.\n\n #### Compatibility with UI libraries\n\n\n One of the main advantages is compatibility with any UI libraries. If\n your team uses:\n\n * Gravity UI,\n * Material‑UI,\n * Ant Design,\n * custom components.\n\n …then you don’t need to give them up! When you zoom in, the graph\n automatically switches to HTML mode, where familiar `Button`,\n `Select`, `DatePicker` in the color theme you need work exactly the\n same as in a regular React app.\n\n\n #### Framework agnostic\n\n\n Although we implemented the basic HTML renderer using React, we tried\n to develop the library so that it remains framework-agnostic. This\n means that if necessary, you can fairly easily implement a layer\n integrating your favorite framework.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Are there any alternatives?{#analogs}\\n\\nThere are quite a lot of solutions on the market for graph rendering, from paid solutions like [yFiles](https://yfiles.dev/){target=\\\"_blank\\\"}, [JointJS](https://github.com/clientIO/joint){target=\\\"_blank\\\"}, to open-source solutions [Foblex Flow](https://github.com/Foblex/f-flow){target=\\\"_blank\\\"}, [baklavajs](https://github.com/newcat/baklavajs){target=\\\"_blank\\\"}, [jsPlumb](https://github.com/jsplumb/community-edition){target=\\\"_blank\\\"}. But for comparison we consider [@antv/g6](https://github.com/antvis/G6){target=\\\"_blank\\\"} and [React Flow](https://github.com/xyflow/xyflow){target=\\\"_blank\\\"} as the most popular tools. Each of them has its own features.\\n\\nReact Flow is a good library tailored for building node-based interfaces. It has very extensive capabilities, but due to using svg and html its performance is rather modest. The library is good when you’re confident graphs won’t exceed 100–200 blocks.\\n\\nIn turn, @antv/g6 has a ton of features; it supports Canvas and in particular WebGL. Comparing @antv/g6 and @gravity‑ui/graph directly is probably not quite correct: their team is more focused on building graphs and charts—but node-based UI is also supported. So antv/g6 is suitable if you care not only about a node-based interface but also about drawing charts.\\n\\nAlthough @antv/g6 can do both canvas/webgl and html/svg, you’ll have to manage the switching rules manually, and you need to do it correctly. Performance-wise it is much faster than React Flow, but there are still questions about the library. While WebGL support is claimed, if you look at their [stress test](https://g6.antv.antgroup.com/en/examples/performance/massive-data#60000){target=\\\"_blank\\\"}, it’s noticeable that on 60k nodes the library can’t deliver dynamics—on a MacBook M3 rendering a single frame took 4 seconds. For comparison, our [stress test](https://preview.gravity-ui.com/graph/?path=/story/stories-main-grapheditor--graph-stress-test){target=\\\"_blank\\\"} on 111k nodes and 109k connections on the same Macbook M3: rendering the entire graph scene takes ~60ms, which yields ~15–20 FPS. That’s not very much, but with Spatial Culling there is an option to limit the viewport and thus improve responsiveness. Although the maintainers [stated](https://github.com/antvis/G6/issues/1597){target=\\\"_blank\\\"} they want to achieve rendering 100k nodes at 30 FPS, apparently they have not managed to do so yet.\\n\\nAnother point where @gravity‑ui/graph wins is bundle size.\\n\\n#|\\n|||Bundle size Minified|Bundle size Minified + Gzipped||\\n||@antv/g6 [bundlephobia](https://bundlephobia.com/package/@antv/g6@5.0.49){target=\\\"_blank\\\"}|1.1 MB|324.5\\_kB||\\n||react flow [bundlephobia](https://bundlephobia.com/package/@xyflow/react@12.8.1){target=\\\"_blank\\\"}|181.2\\_kB|56.4\\_kB||\\n||@gravity-ui/graph [bundlephobia](https://bundlephobia.com/package/@gravity-ui/graph){target=\\\"_blank\\\"}|2.2\\_kB|672\\_B||\\n|#\\n\\nAlthough both libraries are quite strong in terms of performance or integration convenience, @gravity‑ui/graph has a number of advantages—it can provide performance on truly large graphs while preserving UI/UX for the user and simplifying development.\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Plans for the future{#future}\n\n\n The library already has sufficient performance headroom for most\n tasks, so in the near future we will focus more on developing the\n ecosystem around the library—building layers (plugins), integrations\n for other libraries and frameworks (Angular/Vue/Svelte, …etc), adding\n support for touch devices, adaptation for mobile browsers, and\n generally improving UX/DX.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Try it and join in{#try}\n\n\n In the\n [repository](https://github.com/gravity-ui/graph){target=\"_blank\"}\n you’ll find a fully working library:\n\n * Canvas + R-Tree core (≈ 30K lines of code),\n * React integration,\n * Storybook with examples.\n\n You can install the library in one line:\n\n\n `npm install @gravity-ui/graph`\n\n\n --------------\n\n\n For quite a long time, the library that is now called\n @gravity‑ui/graph was an internal tool inside Nirvana, and the chosen\n approach has proven itself well. Now we want to share our work and\n help developers outside draw their graphs more easily, faster, and\n more efficiently.\n\n\n We want to standardize approaches to displaying complex graphs in the\n open-source community—too many teams reinvent the wheel or struggle\n with unsuitable tools.\n\n\n That’s why it’s very important for us to collect your\n feedback—different projects bring different edge cases that help\n evolve the library. This will help us refine the library and grow the\n Gravity UI ecosystem faster.\n - type: blog-layout-block\n resetPaddings: true\n fullWidth: false\n children:\n - type: blog-meta-block\n column: left\n resetPaddings: true\n - type: blog-suggest-block\n resetPaddings: true\n", + "title": "", + "noIndex": false, + "shareTitle": null, + "shareDescription": null, + "shareImage": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "pageLocaleId": null, + "author": "timofeyevvv", + "metaDescription": null, + "keywords": [], + "shareGenTitle": null, + "canonicalLink": null, + "sharingType": "semi-full", + "sharingTheme": "dark", + "comment": "sharing pic", + "shareImageUrl": "https://storage.cloud-preprod.yandex.net/ui-api-ru-preprod-stable-share-generator-screenshots/cache/b155df2ab692d6e154ff809a7d91b9ad4789de53.png", + "pageRegionId": 76, + "summary": null, + "versionId": 216, + "service": null, + "solution": null, + "locales": [ + { + "id": 75, + "pageId": 71, + "locale": "ru", + "createdAt": "2026-01-15T11:26:48.440Z", + "updatedAt": "2026-01-15T11:26:48.519Z", + "publishedVersionId": null, + "lastVersionId": 195 + }, + { + "id": 76, + "pageId": 71, + "locale": "en", + "createdAt": "2026-01-15T11:26:48.532Z", + "updatedAt": "2026-01-15T11:26:48.609Z", + "publishedVersionId": null, + "lastVersionId": 196 + } + ], + "pageRegions": [ + { + "regionCode": "ru-ru", + "publishedVersionId": 199 + }, + { + "regionCode": "en", + "publishedVersionId": 216 + } + ], + "searchCategory": { + "id": 7, + "slug": "blog", + "title": "Blog", + "url": "/blog" + }, + "voiceovers": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/en/pages/gravity-ui-in-opensource.json b/src/api/.mocks/en/pages/gravity-ui-in-opensource.json index 80e028a39a9f..26307ed008cc 100644 --- a/src/api/.mocks/en/pages/gravity-ui-in-opensource.json +++ b/src/api/.mocks/en/pages/gravity-ui-in-opensource.json @@ -1,8 +1,8 @@ { "id": 70, "name": "blog/gravity-ui-in-opensource", - "createdAt": "2025-12-25T10:46:31.596Z", - "updatedAt": "2025-12-25T10:46:31.596Z", + "createdAt": "2026-01-15T11:59:09.523Z", + "updatedAt": "2026-01-15T11:59:09.523Z", "type": "default", "isDeleted": false, "versionOnTranslationId": null, @@ -10,27 +10,27 @@ "regions": [], "pageId": 70, "regionCode": "en", - "publishedVersionId": 194, - "lastVersionId": 194, - "content": "blocks:\n - type: blog-header-block\n resetPaddings: true\n paddingBottom: l\n width: m\n verticalOffset: m\n theme: dark\n background:\n image:\n src: https://storage.yandexcloud.net/yandex-opensource/blog-cover-bg.png\n disableCompress: true\n color: ''\n fullWidth: false\n - type: blog-layout-block\n resetPaddings: true\n mobileOrder: reverse\n children:\n - type: blog-author-block\n column: right\n resetPaddings: true\n authorId: 1069\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: \"\\nВ этой статье:\\n\\n - [В чём особенности Gravity\\_UI](#peculiarities)\\n - [Как работать с Gravity\\_UI](#work)\\n - [Как воспользоваться Gravity\\_UI](#use)\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nВсем привет, я Алексей Сизиков, руководитель отдела User Experience в Yandex Cloud. В этой статье я хочу поделиться новостью: мы выпустили нашу дизайн‑систему и библиотеку компонентов [Gravity\\_UI](https://github.com/gravity-ui){target=\\\"_blank\\\"} в опенсорс. \\n\\nС помощью библиотеки компонентов Gravity\\_UI можно строить современные интерфейсы. В неё входит:\\n\\n * набор базовых React‑компонентов;\\n * библиотека‑конструктор для лендингов;\\n * [подробные гайды](https://gravity-ui.com/design){target=\\\"_blank\\\"} по использованию компонентов;\\n * библиотека в [Figma](https://www.figma.com/community/file/1271150067798118027/Gravity-UI-Design-System-(Beta)){target=\\\"_blank\\\"}; \\n * набор готовых иконок, в составе которого почти 600 вариантов;\\n * ChartKit — пакет для визуализации данных;\\n * Yagr — высокопроизводительный рендеринг графиков, основанный на uPlot;\\n * I18n — пакет для локализации интерфейса\\n * и ещё более [25 полезных библиотек](https://gravity-ui.com/libraries){target=\\\"_blank\\\"}.\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic0.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nПод катом — рассказ, зачем мы сделали Gravity\\_UI, как его используем, в чём особенности и преимущества нашего подхода и как мы планируем развивать его дальше. А ещё — как настроить разные цветовые схемы в своих проектах и почему у нас четыре темы вместо двух стандартных.\\n\\n{% cut \\\"Зачем мы сделали Gravity\\_UI\\\" %}\\n\\nИзначально библиотека UIKit была внутренним продуктом для ускорения работы команды. По мере роста числа новых сервисов мы поставили цель: построить единый UX в наших продуктах. Нам было важно использовать одинаковые паттерны поведения пользователей в сервисах, чтобы пользователь ощущал всю платформу как единое целое.\\n\\nДополнительной целью дизайн‑команды было создать такие инструменты, чтобы разработчики смогли без привлечения дизайнеров сделать новый сервис.\\n\\nОтносительно недавно платформы и сервисы, использующие нашу дизайн‑систему, начали выходить в опенсорс: [YTsaurus](https://ytsaurus.tech/){target=\\\"_blank\\\"}, [YDB](https://ydb.tech/){target=\\\"_blank\\\"}, [DataLens](https://datalens.tech/){target=\\\"_blank\\\"}, [Diplodoc](https://diplodoc.com/){target=\\\"_blank\\\"}. Многие пользователи позитивно отзывались о них и помогали улучшать код вместе с разработчиками. Вдохновившись их примером, мы пришли к идее выпустить в открытый доступ и Gravity\\_UI, потому что видели, насколько библиотека может быть полезна многим сервисам не только внутри Яндекса.\\n\\n{% endcut %}\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## В чём особенности Gravity\\_UI{#peculiarities}\\n\\n### Основано на реальном опыте\\n\\nОдна из отличительных черт нашей дизайн‑системы — она развивается на основе потребностей наших пользователей, большинство из которых являются разработчиками. Более того, наши дизайнеры неразрывно связаны с продуктами, в которых они работают. У каждого дизайнера есть несколько сервисов с собственными сценариями использования. После того как решение протестировано в их сервисах, продуктовый дизайнер передаёт его в другие сервисы, чтобы убедиться, что оно работает и помогает разработчикам. \\n\\nНапример, компонент боковой навигации изначально развивался только с логотипом и пунктами меню. Позже для удобства мы добавили пункт со всеми сервисами и поиском. А когда у нас появился сервис, где была необходимость создавать новую сущность сразу в боковом меню, у нас появилась отдельная кнопка с плюсиком. Дальше появились разделы меню с разделителями для сложных сервисов с большим количеством пунктов меню, а также кнопка «Остальные пункты».\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: >-\n Компонент Навигации — можно настроить как простой вариант, так и\n максимально нагруженный\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic1.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n### Гибкая вариативность\\n\\nМногие компоненты Gravity\\_UI можно настроить, учитывая разные сценарии. Например, у компонента [pagination](https://gravity-ui.com/components/uikit/pagination){target=\\\"_blank\\\"} есть несколько вариантов отображения: с общим числом страниц, полными подписями кнопок, возможностью перейти к конкретной странице и изменить число ответов на странице — этот вариант рассчитан на большое число страниц. А если у вас кейс, где результатов ответов немного, можно сделать компактный режим или даже скрыть цифры и показывать только стрелочки. \\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: Пример отображения разных вариантов пагинации\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic2.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n### Широкая область применения\\n\\nПри помощи Gravity\\_UI можно сделать как простой интерфейс для администрирования, так и сложный дашборд с графиками. Вот несколько примеров того, что можно собрать с помощью нашей дизайн‑системы:\\n\\n * лендинги,\\n * админки,\\n * дашборды,\\n * графики,\\n * СRM,\\n * аналитический сервис.\\n\\nНапример, наши коллеги сделали такой дашборд:\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: \"Пример дашборда, который можно сделать с помощью Gravity\\_UI\"\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic3.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nТакже можно сделать простой лендинг. Например, [сайт](https://opensource.yandex/){target=\\\"_blank\\\"} с нашими проектами в опенсорс сделан на компонентах Gravity\\_UI. \\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: >-\n А это пример сайта, который тоже можно сделать с помощью нашей\n дизайн‑системы\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic4.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ### Более 150 контрибьюторов\n\n\n Наша экосистема постоянно развивается и улучшается. Кроме обратной\n связи от нашего сообщества разработчиков, к нам поступают предложения\n более чем от 100 сервисов, которые уже используют нашу\n дизайн‑систему. \n\n\n Приведу пример. Изначально у нас было две темы — тёмная и светлая. Но\n мы стали получать фидбэк от команд, которые транслируют интерфейс на\n телевизоры для просмотра графиков или во время стендапа. Дело в том,\n что на телевизоре плохо видно интерфейс. Похожая проблема встречается\n также у пользователей со старыми или низкокачественными мониторами.\n\n\n В итоге разработали повышенную контрастность для каждой из тем. Она\n увеличивает яркость тёмного и светлого, а также семантических цветов.\n Это настраивается CSS‑стилями, а управляется в настройках\n пользователя.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: \"Пример интерфейса Yandex Tracker, сделанный также на компонентах Gravity\\_UI, с возможностью включения повышенной контрастности\"\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic5.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Кстати, теперь вы тоже можете сделать свой вклад. Для этого присылайте\n PR в GitHub или оставляйте комментарии в Figma.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Как работать с Gravity\\_UI{#work}\\n\\n### Настройка интерфейса под свой бренд\\n\\nДалее я расскажу о том, что ещё можно сделать с помощью Gravity\\_UI. Начну с возможности настроить дизайн‑систему под свой бренд. Например, когда YDB выходил в опенсорс, перед нами встала задача сохранить единую дизайн‑систему, но при этом сделать её особенной для разных брендов. Для этого мы создали отдельную группу CSS‑переменных. В неё входят цвета бренда, шрифты и радиусы скруглений. В коде это выглядит как небольшой блок:\\n\\n```json \\n.g-root {\\n --g-font-family-sans: 'Inter', sans-serif;\\n\\n --g-text-header-font-weight: 600;\\n --g-text-subheader-font-weight: 600;\\n --g-text-display-font-weight: 600;\\n --g-text-accent-font-weight: 600;\\n\\n --g-color-base-brand: rgb(117, 155, 255);\\n --g-color-base-brand-hover: rgb(99, 143, 255);\\n --g-color-base-selection: rgba(82, 130, 255, 0.05);\\n --g-color-base-selection-hover: rgba(82, 130, 255, 0.1);\\n --g-color-line-brand: rgb(117, 155, 255);\\n --g-color-text-brand: rgb(117, 155, 255);\\n --g-color-text-brand-contrast: rgb(255, 255 ,255);\\n --g-color-text-link: rgb(117, 155, 255);\\n --g-color-text-link-hover: rgb(82, 130, 255);\\n\\n --g-border-radius-xs: 3px;\\n --g-border-radius-s: 5px;\\n --g-border-radius-m: 6px;\\n --g-border-radius-l: 8px;\\n --g-border-radius-xl: 10px;\\n --g-border-radius-2xl: 16px;\\n}\\n```\\n\\nВ этой группе можно заменить шрифт, цвет акцентных кнопок, скругления. И таким образом можно использовать одну дизайн‑систему для разных брендов, сохраняя при этом фирменный стиль. Если же этот вариант вам не подходит, можно создать собственную цветовую схему. Подробная инструкция есть в [документации](https://preview.gravity-ui.com/uikit/?path=/docs/branding-overview--docs#additional-customization){target=\\\"_blank\\\"}.\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: Пример интерфейсов с разными переменными бренда\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic6.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Стоит отметить, что изменение цветов бренда не повлияет на основной\n интерфейс. Это особенно важно для тех элементов, где используются\n семантические цвета: например, красный по‑прежнему будет означать\n ошибку, а зелёный — успех.\n\n\n ### Компоненты в Figma\n\n\n Для удобства работы наша команда дизайнеров подготовила и загрузила\n все состояния компонентов в\n [Figma](https://www.figma.com/community/file/1271150067798118027){target=\"_blank\"}.\n При желании можно сделать копию библиотеки и попробовать собрать\n интерфейс сразу в сервисе.\n\n\n С последним обновлением в библиотеке не дублируются все четыре темы.\n По умолчанию вы работаете в светлой теме, но в разделе Layer вы можете\n переключить любой элемент или всю страницу на другую тему. \n - type: blog-media-block\n column: left\n resetPaddings: true\n text: Как просмотреть элемент в разных темах\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic7.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Все элементы библиотеки представлены на странице Overview. Тут вы\n можете найти нужный компонент и перейти на страницу со всеми\n состояниями элемента. Кроме того, у каждого компонента есть пример\n использования. \n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: \"Страница со всеми элементами Gravity\\_UI\"\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic8.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ### Почти 600 иконок\n\n Иконки — один из обязательных элементов дизайна и юзабилити. Они\n помогают организовать и структурировать контент, расставить акценты и\n улучшить восприятие информации. На первый взгляд, сделать пак иконок\n не так уж и сложно. Но мы столкнулись с вызовом при создании иконок\n для сложных метафор, таких как виртуальные машины, базы данных и\n различные типы графиков.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: Пока у нас 594 иконки, но скоро их станет больше\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic9.png\n fullscreen: true\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: >-\n Особой задачей стала и разработка иконок для графического редактора\n контента (WYSIWYG), который внедряется в различные места для\n форматирования текста \n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic10.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Чтобы сделать поиск иконок более удобным, мы использовали специальную\n систему нейминга. Теперь одну и ту же иконку можно найти, вводя разные\n названия. Например, чтобы найти такую иконку со стрелочкой, можно\n ввести любое слово: arrow, enter, move, login.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: >-\n Все иконки доступны на [отдельной\n странице](https://gravity-ui.com/icons). Вы можете скопировать SVG или\n скачать любую иконку\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic11.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ### Гайды\n\n\n Как я уже писал выше, наша цель заключалась в том, чтобы разработчики\n могли легко создавать типовые интерфейсы, используя описания\n компонентов в нашем гайде. Мы стремились сделать этот процесс простым,\n без необходимости обращаться к дизайнеру за каждым элементом. На\n данный момент у нас уже есть пять примеров внутренних сервисов,\n которые были полностью созданы разработчиками с использованием только\n гайдов.\n\n\n Конечно, одними руками разработчика эту задачу решить не получится:\n так или иначе есть сложные сценарии, которые требуют внимания\n UX/UI‑специалиста. Тем не менее мы смогли снять значительную часть\n нагрузки с наших дизайнеров.\n\n\n В гайдах команда дизайнеров описала компоненты и дала рекомендации по\n их использованию, демонстрируя примеры правильного и неправильного\n подхода. Вы можете ознакомиться с ними на [этой\n странице](https://gravity-ui.com/components/uikit/alert){target=\"_blank\"}.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic12.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Как воспользоваться Gravity\\_UI{#use}\\n\\nВсё, что нужно сделать, — вбить команду в консоль:\\n\\n```\\ngit clone git@github.com:gravity-ui/uikit-example-cra.git my-project && cd my-project\\nnpm i \\nnpm run start\\n```\\n\\nБолее подробная инструкция есть на главной странице репозитория в [GitHub](https://github.com/gravity-ui/uikit){target=\\\"_blank\\\"}.\\n\\nВсе компоненты Gravity\\_UI можно посмотреть в [UIkit](https://preview.gravity-ui.com/uikit/){target=\\\"_blank\\\"}, а подключить нужную библиотеку для ваших потребностей — в [Libraries](https://gravity-ui.com/libraries){target=\\\"_blank\\\"}. \\n\\n--------\\n\\nПодводя итоги, хочется ещё раз отметить: Gravity\\_UI — это дизайн‑система, которая выросла из реального опыта наших пользователей, потребности разработчиков и экспертизы дизайнеров. Это позволяет ей быть практичной и эффективной. И теперь ею может бесплатно воспользоваться любой желающий.\\n\\nМы стремимся сделать проект ещё лучше, учитывая потребности и отзывы наших пользователей. Заходите к нам в GitHub, оставляйте свои PR, пишите комментарии в Figma и делитесь примерами использования.\\n\"\n - type: blog-layout-block\n resetPaddings: true\n fullWidth: false\n children:\n - type: blog-meta-block\n column: left\n resetPaddings: true\n - type: blog-suggest-block\n resetPaddings: true\n", + "publishedVersionId": 201, + "lastVersionId": 201, + "content": "blocks:\n - type: blog-header-block\n resetPaddings: true\n paddingBottom: l\n width: m\n verticalOffset: m\n theme: dark\n background:\n image:\n src: https://storage.yandexcloud.net/yandex-opensource/blog-cover-bg.png\n disableCompress: true\n color: '#CCDAFF'\n fullWidth: false\n - type: blog-layout-block\n resetPaddings: true\n mobileOrder: reverse\n children:\n - type: blog-author-block\n column: right\n resetPaddings: true\n authorId: 1069\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: \"\\nIn this article:\\n\\n - [What are the features of Gravity\\_UI](#peculiarities)\\n - [How to work with Gravity\\_UI](#work)\\n - [How to use Gravity\\_UI](#use)\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nHello everyone, I’m Alexey Sizikov, Head of User Experience at Yandex Cloud. In this article, I’d like to share some news: we’ve open-sourced our design system and component library [Gravity\\_UI](https://github.com/gravity-ui){target=\\\"_blank\\\"}. \\n\\nWith the Gravity\\_UI component library, you can build modern interfaces. It includes:\\n\\n * a set of basic React components;\\n * a landing page builder library;\\n * [detailed guides](https://gravity-ui.com/design){target=\\\"_blank\\\"} on using the components;\\n * a library in [Figma](https://www.figma.com/community/file/1271150067798118027/Gravity-UI-Design-System-(Beta)){target=\\\"_blank\\\"}; \\n * a set of ready-made icons with nearly 600 options;\\n * ChartKit — a data visualization package;\\n * Yagr — high-performance chart rendering based on uPlot;\\n * I18n — a UI localization package\\n * and more than [25 useful libraries](https://gravity-ui.com/libraries){target=\\\"_blank\\\"}.\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic0.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nBelow the cut is a story about why we created Gravity\\_UI, how we use it, what the features and advantages of our approach are, and how we plan to evolve it further. Also: how to configure different color schemes in your projects and why we have four themes instead of the usual two.\\n\\n{% cut \\\"Why we created Gravity\\_UI\\\" %}\\n\\nInitially, the UIKit library was an internal product to speed up the team’s work. As the number of new services grew, we set a goal: to build a unified UX across our products. It was important to use the same user behavior patterns across services so that users would perceive the entire platform as a single whole.\\n\\nAn additional goal for the design team was to create tools so that developers could build a new service without involving designers.\\n\\nRelatively recently, platforms and services using our design system started going open source: [YTsaurus](https://ytsaurus.tech/){target=\\\"_blank\\\"}, [YDB](https://ydb.tech/){target=\\\"_blank\\\"}, [DataLens](https://datalens.tech/){target=\\\"_blank\\\"}, [Diplodoc](https://diplodoc.com/){target=\\\"_blank\\\"}. Many users spoke positively about them and helped improve the code together with the developers. Inspired by their example, we came up with the idea to release Gravity\\_UI to the public as well, because we saw how useful the library could be for many services — not only inside Yandex.\\n\\n{% endcut %}\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## What are the features of Gravity\\_UI{#peculiarities}\\n\\n### Based on real-world experience\\n\\nOne of the distinguishing features of our design system is that it evolves based on the needs of our users — most of whom are developers. Moreover, our designers are closely tied to the products they work on. Each designer has several services with their own usage scenarios. After a solution has been tested in their services, the product designer rolls it out to other services to make sure it works and helps developers. \\n\\nFor example, the side navigation component initially evolved with only a logo and menu items. Later, for convenience, we added an item with all services and a search feature. And when we got a service where there was a need to create a new entity right from the side menu, we added a separate plus button. Next came menu sections with dividers for complex services with lots of menu items, as well as an “Other items” button.\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: >-\n The Navigation component — you can configure it as a simple version or\n as a fully loaded one\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic1.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n### Flexible variability\\n\\nMany Gravity\\_UI components can be configured for different scenarios. For example, the [pagination](https://gravity-ui.com/components/uikit/pagination){target=\\\"_blank\\\"} component has several display options: with a total page count, full button labels, the ability to jump to a specific page, and changing the number of results per page — this option is designed for a large number of pages. But if your case has only a few results, you can use a compact mode or even hide the numbers and show only arrows. \\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: Example of displaying different pagination variants\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic2.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n### Broad range of applications\\n\\nWith Gravity\\_UI, you can build both a simple admin interface and a complex dashboard with charts. Here are a few examples of what you can put together with our design system:\\n\\n * landing pages,\\n * admin panels,\\n * dashboards,\\n * charts,\\n * CRM,\\n * an analytics service.\\n\\nFor example, our colleagues built this dashboard:\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: \"An example dashboard you can build with Gravity\\_UI\"\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic3.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nYou can also build a simple landing page. For example, the [website](https://opensource.yandex/){target=\\\"_blank\\\"} with our open-source projects is built using Gravity\\_UI components. \\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: >-\n And this is an example of a website you can also build with our design\n system\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic4.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ### More than 150 contributors\n\n\n Our ecosystem is constantly evolving and improving. In addition to\n feedback from our developer community, we receive suggestions from\n more than 100 services that already use our design system. \n\n\n Here’s an example. Initially, we had two themes — dark and light. But\n we started receiving feedback from teams that broadcast the interface\n to TVs for viewing charts or during stand-ups. The issue is that the\n interface is hard to see on a TV. A similar problem also occurs for\n users with old or low-quality monitors.\n\n\n As a result, we developed increased contrast for each theme. It\n increases the brightness of dark and light colors as well as semantic\n colors. This is configured via CSS styles and controlled in user\n settings.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: \"Example of the Yandex Tracker interface, also built with Gravity\\_UI components, with the option to enable increased contrast\"\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic5.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n By the way, now you can contribute too. Send PRs on GitHub or leave\n comments in Figma.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## How to work with Gravity\\_UI{#work}\\n\\n### Customizing the interface for your brand\\n\\nNext, I’ll talk about what else you can do with Gravity\\_UI. I’ll start with the ability to customize the design system to match your brand. For example, when YDB went open source, we faced the task of keeping a unified design system while also making it distinctive for different brands. To do this, we created a separate group of CSS variables. It includes brand colors, fonts, and border radii. In code, it looks like a small block:\\n\\n```json \\n.g-root {\\n --g-font-family-sans: 'Inter', sans-serif;\\n\\n --g-text-header-font-weight: 600;\\n --g-text-subheader-font-weight: 600;\\n --g-text-display-font-weight: 600;\\n --g-text-accent-font-weight: 600;\\n\\n --g-color-base-brand: rgb(117, 155, 255);\\n --g-color-base-brand-hover: rgb(99, 143, 255);\\n --g-color-base-selection: rgba(82, 130, 255, 0.05);\\n --g-color-base-selection-hover: rgba(82, 130, 255, 0.1);\\n --g-color-line-brand: rgb(117, 155, 255);\\n --g-color-text-brand: rgb(117, 155, 255);\\n --g-color-text-brand-contrast: rgb(255, 255 ,255);\\n --g-color-text-link: rgb(117, 155, 255);\\n --g-color-text-link-hover: rgb(82, 130, 255);\\n\\n --g-border-radius-xs: 3px;\\n --g-border-radius-s: 5px;\\n --g-border-radius-m: 6px;\\n --g-border-radius-l: 8px;\\n --g-border-radius-xl: 10px;\\n --g-border-radius-2xl: 16px;\\n}\\n```\\n\\nIn this group, you can replace the font, the color of accent buttons, and the rounding radii. This way, you can use a single design system for different brands while preserving each brand’s identity. If this option doesn’t work for you, you can create your own color scheme. There’s a detailed guide in the [documentation](https://preview.gravity-ui.com/uikit/?path=/docs/branding-overview--docs#additional-customization){target=\\\"_blank\\\"}.\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: Example interfaces with different brand variables\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic6.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n It’s worth noting that changing brand colors won’t affect the core\n interface. This is especially important for elements that use semantic\n colors: for example, red will still mean an error, and green will\n still mean success.\n\n\n ### Components in Figma\n\n\n For convenience, our design team prepared and uploaded all component\n states to\n [Figma](https://www.figma.com/community/file/1271150067798118027){target=\"_blank\"}.\n If you want, you can make a copy of the library and try building an\n interface directly in the tool.\n\n\n With the latest update, the library no longer duplicates all four\n themes. By default, you work in the light theme, but in the Layer\n section you can switch any element — or the whole page — to another\n theme. \n - type: blog-media-block\n column: left\n resetPaddings: true\n text: How to preview an element in different themes\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic7.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n All library elements are presented on the Overview page. Here you can\n find the component you need and go to the page with all its states. In\n addition, each component has a usage example. \n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: \"A page with all Gravity\\_UI elements\"\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic8.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ### Nearly 600 icons\n\n Icons are an essential part of design and usability. They help\n organize and structure content, set accents, and improve how\n information is perceived. At first glance, creating an icon pack\n doesn’t seem too hard. But we faced a challenge when creating icons\n for complex metaphors such as virtual machines, databases, and various\n chart types.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: We currently have 594 icons, but there will be more soon\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic9.png\n fullscreen: true\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: >-\n Developing icons for a WYSIWYG content editor was also a special\n challenge — it’s being integrated in various places for text\n formatting\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic10.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n To make icon search more convenient, we used a special naming system.\n Now you can find the same icon by typing different names. For example,\n to find an arrow-like icon, you can type any of these words: arrow,\n enter, move, login.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: >-\n All icons are available on a [separate\n page](https://gravity-ui.com/icons). You can copy the SVG or download\n any icon\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic11.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ### Guides\n\n\n As I wrote above, our goal was to make it easy for developers to build\n standard interfaces using component descriptions in our guide. We\n aimed to make this process simple, without the need to contact a\n designer for every single element. At the moment, we already have five\n examples of internal services that were fully built by developers\n using only the guides.\n\n\n Of course, you can’t solve this task with developers’ hands alone: in\n any case, there are complex scenarios that require the attention of a\n UX/UI specialist. Nevertheless, we managed to remove a significant\n portion of the load from our designers.\n\n\n In the guides, the design team described the components and provided\n recommendations on how to use them, showing examples of the right and\n wrong approach. You can view them on [this\n page](https://gravity-ui.com/components/uikit/alert){target=\"_blank\"}.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/pic12.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## How to use Gravity\\_UI{#use}\\n\\nAll you need to do is run the command in the terminal:\\n\\n```\\ngit clone git@github.com:gravity-ui/uikit-example-cra.git my-project && cd my-project\\nnpm i \\nnpm run start\\n```\\n\\nMore detailed instructions are available on the repository’s main page on [GitHub](https://github.com/gravity-ui/uikit){target=\\\"_blank\\\"}.\\n\\nYou can browse all Gravity\\_UI components in [UIkit](https://preview.gravity-ui.com/uikit/){target=\\\"_blank\\\"}, and connect the library you need for your use case in [Libraries](https://gravity-ui.com/libraries){target=\\\"_blank\\\"}. \\n\\n--------\\n\\nTo sum it up, I’d like to emphasize once again: Gravity\\_UI is a design system that grew out of the real experience of our users, developers’ needs, and designers’ expertise. This makes it practical and effective. And now anyone can use it for free.\\n\\nWe strive to make the project even better by taking into account our users’ needs and feedback. Visit us on GitHub, submit PRs, leave comments in Figma, and share examples of how you use it.\\n\"\n - type: blog-layout-block\n resetPaddings: true\n fullWidth: false\n children:\n - type: blog-meta-block\n column: left\n resetPaddings: true\n - type: blog-suggest-block\n resetPaddings: true\n", "title": "", "noIndex": false, "shareTitle": null, "shareDescription": null, - "shareImage": "https://storage.yandexcloud.net/yandex-opensource/blog-cover-bg.png", + "shareImage": null, "pageLocaleId": null, - "author": "ngorin", + "author": "timofeyevvv", "metaDescription": null, "keywords": [], "shareGenTitle": null, "canonicalLink": null, - "sharingType": "semi-full", - "sharingTheme": "dark", - "comment": "gravity", - "shareImageUrl": "https://storage.cloud-preprod.yandex.net/ui-api-ru-preprod-stable-share-generator-screenshots/cache/8c45f347a6e585754d1d6e4949c9c5a20a2d570e.png", - "pageRegionId": 73, + "sharingType": "auto", + "sharingTheme": "light", + "comment": "translation", + "shareImageUrl": "https://storage.cloud-preprod.yandex.net/ui-api-ru-preprod-stable-share-generator-screenshots/cache/6a79bcd295f4b97553c519c462935cc394f84cb8.png", + "pageRegionId": 74, "summary": null, - "versionId": 194, + "versionId": 201, "service": null, "solution": null, "locales": [ @@ -54,20 +54,20 @@ } ], "pageRegions": [ - { - "regionCode": "en", - "publishedVersionId": null - }, { "regionCode": "ru-ru", "publishedVersionId": 194 + }, + { + "regionCode": "en", + "publishedVersionId": 201 } ], "searchCategory": { - "id": 7, - "slug": "blog", - "title": "Блог", - "url": "/blog" + "id": 7, + "slug": "blog", + "title": "Blog", + "url": "/blog" }, "voiceovers": [] - } \ No newline at end of file +} diff --git a/src/api/.mocks/en/pages/md-editor-in-gravity-ui.json b/src/api/.mocks/en/pages/md-editor-in-gravity-ui.json new file mode 100644 index 000000000000..5dfe4c249b2b --- /dev/null +++ b/src/api/.mocks/en/pages/md-editor-in-gravity-ui.json @@ -0,0 +1,74 @@ +{ + "id": 73, + "name": "blog/md-editor-in-gravity-ui", + "createdAt": "2026-01-15T12:37:10.557Z", + "updatedAt": "2026-01-15T12:37:10.557Z", + "type": "default", + "isDeleted": false, + "versionOnTranslationId": null, + "searchCategorySlug": "blog", + "regions": [], + "pageId": 73, + "regionCode": "en", + "publishedVersionId": 212, + "lastVersionId": 212, + "content": "blocks:\n - type: blog-header-block\n resetPaddings: true\n paddingBottom: l\n width: m\n verticalOffset: m\n theme: dark\n background:\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png\n disableCompress: true\n color: '#2A1A2A'\n fullWidth: false\n - type: blog-layout-block\n resetPaddings: true\n mobileOrder: reverse\n children:\n - type: blog-yfm-block\n resetPaddings: true\n column: right\n text: >\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/speaker.jpg\n =80x)\n\n\n **Sergey Makhnatkin**\n\n Frontend Developer\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: \"\\nIn this article:\\n\\n - [Why we need our own Markdown Editor](#why)\\n - [Markdown Editor capabilities in Gravity\\_UI](#capabilities)\\n - [Architecture](#architecture)\\n - [Integration](#integration)\\n - [Integrating custom extensions](#extensions)\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic1.png\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n{% cut \\\"TL;DR\\\" %}\\n\\n* Lets you work simultaneously in [WYSIWYG](https://ru.wikipedia.org/wiki/WYSIWYG){target=\\\"_blank\\\"} and [markdown](https://en.wikipedia.org/wiki/Markdown){target=\\\"_blank\\\"} markup modes (with preview and split view).\\n* Supports a large number of blocks out of the box.\\n* Lets you extend functionality — there is an extension system in WYSIWYG mode.\\n* Built for use in React applications.\\n* Uses theming and components from [Gravity\\_UI](https://gravity-ui.com/){target=\\\"_blank\\\"}.\\n* Fully built on open-source technologies ([ProseMirror](https://prosemirror.net/){target=\\\"_blank\\\"}, [CodeMirror](https://codemirror.net/){target=\\\"_blank\\\"}, [markdown-it](https://github.com/markdown-it/markdown-it){target=\\\"_blank\\\"}, [Diplodoc](https://diplodoc.com/){target=\\\"_blank\\\"}, [Gravity\\_UI](https://gravity-ui.com/){target=\\\"_blank\\\"}).\\n* Complies with the [CommonMark](https://spec.commonmark.org/){target=\\\"_blank\\\"} standard, supports standard markdown and [Yandex Flavored Markdown (YFM)](https://diplodoc.com/docs/ru/index-yfm){target=\\\"_blank\\\"}.\\n\\n{% endcut %}\\n\\nHi! My name is Sergey Makhnatkin, and I work as a developer in the User Experience team at Yandex\\_Cloud. Last year, we wrote about our [Gravity\\_UI design system and component library](https://habr.com/ru/companies/yandex/articles/773870/){target=\\\"_blank\\\"}. Since then, the system has been updated multiple times and expanded with new features, and today I want to talk about a new tool — Markdown Editor — which significantly simplifies working with documentation.\\n\\nWe’ll talk about the history of building the user interface, architectural features, and technical details of integration and developing custom extensions — and, of course, why all of this is available as open source.\\n\\nBy the way, you can try the tool here:\\n\\n * [Demo](https://gravity-ui.com/libraries/markdown-editor/playground){target=\\\"_blank\\\"} \\n * [GitHub](https://github.com/gravity-ui/markdown-editor/){target=\\\"_blank\\\"} \\n * [Storybook](https://preview.gravity-ui.com/md-editor/){target=\\\"_blank\\\"}\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Why we need our own Markdown Editor{#why}\n\n\n To make it convenient to store and structure corporate information, we\n built the [Wiki](https://wiki.yandex.ru/){target=\"_blank\"} platform,\n which allows creating knowledge bases. Alongside the knowledge base,\n we developed documentation approaches such as Docs as Code, where\n documentation and code live side by side in a file repository (.md\n files). This is how the\n [Diplodoc](https://diplodoc.com/){target=\"_blank\"} platform emerged.\n\n\n What Wiki and Diplodoc have in common is that both platforms work with\n a markdown dialect — Yandex Flavored Markdown (YFM) — which is used at\n Nebius, Bitrix, DoubleCloud, Mappable, and Meteum.\n\n\n Over time, we noticed there are two groups of users who think about\n creating and editing text differently. Some prefer to see the final\n result right away, working with text like in MS Word, Confluence, or\n Notion. Others trust only markup and prefer to format pages using\n markdown. We couldn’t find any well-known libraries that work in both\n WYSIWYG and markdown modes at the same time. For example, Notion is\n WYSIWYG only, while code editors usually provide only markdown and\n preview mode.\n\n\n We developed a markdown editor that can work in two modes\n simultaneously: a visual mode (WYSIWYG) and a markup mode (markdown).\n In the first mode, toolbar icons help format the text; in the second,\n users can manually edit the markdown source. In addition, our solution\n stores the document as an .md file regardless of which mode was used\n to create it.\n\n\n This is what the visual editor looks like, where you can format text\n using buttons:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic2.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n And this is the markup mode, where formatting elements are indicated\n using special characters:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic3.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Markdown Editor capabilities in Gravity\\_UI{#capabilities}\\n\\nThe editor complies with the CommonMark standard and supports both standard markdown and YFM. We also added the ability to extend the syntax with other markdown dialects, such as GitHub Flavored Markdown. At the same time, the editor lets you switch from markup mode to WYSIWYG mode, while the document itself is stored as md markup or extended md (for example, in the case of YFM).\\n\\n### Extensions\\n\\nThe editor comes with many built-in extensions and settings. For example, Mermaid diagrams and HTML blocks:\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic4.png\n fullscreen: true\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic5.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n We tried to make the editor core easy to extend. Developers can create\n their own extension or add extra functionality to:\n\n * add new entities — blocks or text modifiers;\n * further configure the markdown parser;\n * add actions that allow controlling the editor from the outside;\n * enrich UI functionality — for example, show a list of available commands when typing a slash;\n * modify current behavior — for example, insert images and files and upload them to storage.\n\n Here are some examples of such extensions we built for our Wiki:\n\n * collaborative editing mode;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic6.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * a [draw.io](http://draw.io/){target=\"_blank\"} diagrams block;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic7.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * a [YandexGPT](https://ya.ru/ai/gpt-3){target=\"_blank\"} plugin;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic8.png\n fullscreen: true\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic9.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * includes;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic10.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * section structure;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic11.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * sections for creating a convenient grid;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic12.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * markdown mode with preview;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic13.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * and many more.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic14.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Markup can be transformed automatically. If you prefer working without\n a mouse, the visual editor mode supports special characters that let\n you apply markup directly in the text. For example, `**` turns text\n bold in WYSIWYG mode. With these characters, you can format text and\n create inline and block code.\n\n\n You can also open the extensions menu by typing `/`.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic15.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ### Presets\n\n\n The editor lets you configure the toolbar for each project\n individually, but it ships with a set of ready-made configurations —\n presets.\n\n\n The editor without presets:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic16.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n The [CommonMark\n preset](https://preview.gravity-ui.com/md-editor/?path=/story/markdown-editor-presets--common-mark){target=\"_blank\"}\n provides support for standard markdown elements: bold, italic,\n headings, lists, links, quotes, and code blocks.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic17.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n In the [default\n preset](https://preview.gravity-ui.com/md-editor/?path=/story/markdown-editor-presets--default){target=\"_blank\"},\n you also get strikethrough text, plus a table where only text can be\n placed in cells. This preset corresponds to standard markdown-it.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic18.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n As mentioned above, the editor also supports YFM, so it integrates\n great with Diplodoc. In the [YFM\n preset](https://preview.gravity-ui.com/md-editor/?path=/story/markdown-editor-presets--yfm){target=\"_blank\"},\n additional elements appear: advanced tables, file and image insertion,\n checkboxes, cut, tabs, and monospace font.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic19.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n The [full\n preset](https://preview.gravity-ui.com/md-editor/?path=/story/markdown-editor-presets--full){target=\"_blank\"}\n includes even more elements.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic20.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Architecture{#architecture}\n\n\n The editor’s WYSIWYG mode is built on the well-known\n [ProseMirror](http://prosemirror.net/){target=\"_blank\"} library, while\n markup mode uses\n [CodeMirror](https://codemirror.net/){target=\"_blank\"}. ProseMirror\n supports editing with formatting, whereas CodeMirror is better suited\n for situations where you need to work with raw text.\n\n\n We chose these libraries because they were created by the same author,\n are consistent in architecture and implementation approaches, are\n backed by a large community, are used in many editors, and are well\n optimized for working with text. For example, the transaction system\n for applying changes to the document, view decorations, DOM\n virtualization, and support for the syntax of many programming\n languages.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Integration{#integration}\n\n\n Our editor is easy to connect as a [React\n hook](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-add-editor-with-create-react-app.md){target=\"_blank\"}:\n\n\n ```javascript\n\n\n import React from 'react';\n\n import {useMarkdownEditor, MarkdownEditorView} from\n '@gravity-ui/markdown-editor';\n\n import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18';\n\n\n function Editor({onSubmit}) {\n const editor = useMarkdownEditor({allowHTML: false});\n\n React.useEffect(() => {\n function submitHandler() {\n // Serialize current content to markdown markup\n const value = editor.getValue();\n onSubmit(value);\n }\n\n editor.on('submit', submitHandler);\n return () => {\n editor.off('submit', submitHandler);\n };\n }, [onSubmit]);\n\n return ;\n }\n\n ```\n\n We use components from the GravityUI uikit library. This ensures that\n the entire interface is consistent and follows unified style\n guidelines. Using these components also provides a high degree of\n consistency and recognizability for users, making the editor even more\n convenient to work with.\n\n\n We have detailed\n [instructions](https://github.com/gravity-ui/markdown-editor/blob/main/README.md#getting-started){target=\"_blank\"}\n on how to connect the editor to a React app, as well as how to connect\n various extensions, such as\n [YandexGPT](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-gpt-extensions.md){target=\"_blank\"},\n [Mermaid](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-mermaid-extension.md){target=\"_blank\"}\n or\n [LaTeX](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-latex-extension.md){target=\"_blank\"}.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Integrating custom extensions{#extensions}\\n\\nA number of additional extensions are already integrated into the editor. But if that’s not enough, developers can add their own extensions to the editor’s WYSIWYG mode. We discussed what extensions can enable above.\\n\\nIf you want to add a new block or text modifier, you first need to configure the internal markdown-it instance using the configureMd method. Next, you should add knowledge about the new entity using the addNode or addMark methods, passing the entity name and a callback function that returns an object with three required fields:\\n\\n```javascript\\nimport insPlugin from 'markdown-it-ins';\\nexport const underlineMarkName = 'ins';\\n\\nexport const UnderlineSpecs: ExtensionAuto = (builder) => {\\n builder\\n .configureMd((md) => md.use(insPlugin))\\n .addMark(underlineMarkName, () => ({\\n spec: {\\n parseDOM: [{tag: 'ins'}, {tag: 'u'}],\\n toDOM() {\\n return ['ins'];\\n },\\n },\\n toMd: {open: '++', close: '++', mixable: true, expelEnclosingWhitespace: true},\\n fromMd: {tokenSpec: {name: underlineMarkName, type: 'mark'}},\\n }));\\n};\\n```\\n\\n * `spec` — the ProseMirror specification;\\n * `fromMd` — configuration for parsing markdown markup into ProseMirror’s internal representation;\\n * `toMd` — configuration for serializing the entity into markdown markup.\\n\\nFor example, below is the configuration of an extension for underlined text. It can be extended by adding an action using the `addAction` method:\\n\\n```javascript\\nimport {toggleMark} from 'prosemirror-commands';\\n\\nconst undAction = 'underline';\\n\\nbuilder\\n .addAction(undAction, ({schema}) => ({\\n isActive: (state) => Boolean(isMarkActive(state, markType)),\\n isEnable: toggleMark(underlineType(schema)),\\n run: toggleMark(underlineType(schema)),\\n })\\n )\\n```\\n\\nSuch an action can be called in code as follows:\\n\\n```javascript\\n// editor – an editor instance obtained as a result of calling useMarkdownEditor\\neditor.actions.underline.run(),\\n```\\n\\nIn the [documentation](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-create-extension.md){target=\\\"_blank\\\"}, you can find the full guide on creating a new extension.\\n\\n---------------------\\n\\nWe’re constantly expanding the horizons of how our editor can be used: right now we’re working on a plugin for [VS Code](https://code.visualstudio.com/){target=\\\"_blank\\\"}, which will allow working with .md files in a convenient WYSIWYG mode прямо from within the editor. We also plan to add a fully featured mobile mode. This will enable every user to work in our editor with only a mobile phone at hand.\\n\\nOur editor didn’t appear overnight: it’s the result of accumulated experience and knowledge. We’re proud that it is fully based on open-source products, including the reliable and proven tools ProseMirror, CodeMirror, and markdown-it, as well as our own developments — Diplodoc and Gravity\\_UI.\\n\\nYou can always contribute to the editor’s development: [create a pull request](https://github.com/gravity-ui/markdown-editor/pulls){target=\\\"_blank\\\"} or help resolve the current issues listed in the [Issues](https://github.com/gravity-ui/markdown-editor/issues){target=\\\"_blank\\\"} section. Your support and fresh perspective will help us make the editor better. And if you find our project useful, please star our [GitHub repository](https://github.com/gravity-ui/markdown-editor){target=\\\"_blank\\\"} — it really matters :)\\n\"\n - type: blog-layout-block\n resetPaddings: true\n fullWidth: false\n children:\n - type: blog-meta-block\n column: left\n resetPaddings: true\n - type: blog-suggest-block\n resetPaddings: true\n", + "title": "", + "noIndex": false, + "shareTitle": null, + "shareDescription": null, + "shareImage": null, + "pageLocaleId": null, + "author": "timofeyevvv", + "metaDescription": null, + "keywords": [], + "shareGenTitle": null, + "canonicalLink": null, + "sharingType": "auto", + "sharingTheme": "light", + "comment": "initial", + "shareImageUrl": "https://storage.cloud-preprod.yandex.net/ui-api-ru-preprod-stable-share-generator-screenshots/cache/6a79bcd295f4b97553c519c462935cc394f84cb8.png", + "pageRegionId": 80, + "summary": null, + "versionId": 212, + "service": null, + "solution": null, + "locales": [ + { + "id": 79, + "pageId": 73, + "locale": "ru", + "createdAt": "2026-01-15T12:31:20.911Z", + "updatedAt": "2026-01-15T12:31:20.973Z", + "publishedVersionId": null, + "lastVersionId": 208 + }, + { + "id": 80, + "pageId": 73, + "locale": "en", + "createdAt": "2026-01-15T12:31:20.982Z", + "updatedAt": "2026-01-15T12:31:21.040Z", + "publishedVersionId": null, + "lastVersionId": 209 + } + ], + "pageRegions": [ + { + "regionCode": "ru-ru", + "publishedVersionId": 210 + }, + { + "regionCode": "en", + "publishedVersionId": 212 + } + ], + "searchCategory": { + "id": 7, + "slug": "blog", + "title": "Blog", + "url": "/blog" + }, + "voiceovers": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/en/pages/yc-site-in-gravity-ui.json b/src/api/.mocks/en/pages/yc-site-in-gravity-ui.json new file mode 100644 index 000000000000..79e8453b747e --- /dev/null +++ b/src/api/.mocks/en/pages/yc-site-in-gravity-ui.json @@ -0,0 +1,74 @@ +{ + "id": 72, + "name": "blog/yc-site-in-gravity-ui", + "createdAt": "2026-01-15T12:28:18.553Z", + "updatedAt": "2026-01-15T12:28:18.553Z", + "type": "default", + "isDeleted": false, + "versionOnTranslationId": null, + "searchCategorySlug": "blog", + "regions": [], + "pageId": 72, + "regionCode": "en", + "publishedVersionId": 207, + "lastVersionId": 207, + "content": "blocks:\n - type: blog-header-block\n resetPaddings: true\n paddingBottom: l\n width: m\n verticalOffset: m\n theme: dark\n background:\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png\n disableCompress: true\n color: '#2A1A2A'\n fullWidth: false\n - type: blog-layout-block\n resetPaddings: true\n mobileOrder: reverse\n children:\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: \"\\n![image](https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/speaker.jpg =80x)\\n\\n**Vladimir Timofeev**\\nTechnical Project Manager, Yandex\\_Cloud\\n\"\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: \"\\nIn this article:\\n\\n - [It all started with Gravity\\_UI](#start)\\n - [Yandex\\_Cloud accessibility](#availability)\\n - [Diplodoc](#diplodoc)\\n - [Results and plans](#results)\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nHi, my name is Vova Timofeev, and I’m a Technical Project Manager at Yandex\\_Cloud. In this article, I’ll share how we made the Yandex\\_Cloud platform website more accessible, how many iterations we went through, and what role Gravity\\_UI played in it.\\n\\nAt the core of accessibility for any service is how well it supports screen readers. Through these programs, users with disabilities perceive the interface and interact with it.\\n\\nWebsites are no exception. And we had to find out how accessible Yandex\\_Cloud is for all users.\\n\\nAt Yandex, by accessibility we mean that our services should be comfortable for everyone to use, regardless of temporary or permanent physical limitations. For example, 16 Yandex services are currently adapted for blind users: Lavka, Go, Search, Browser, Mail, and others. A non-visual testing team helps with accessibility work on each service — and in the case I’m describing in this article, we couldn’t have done it without their help either.\\n\\n**Spoiler**: the testing revealed several controversial points in how screen readers work with the site, and those turned into actionable tasks.\\n\\nBut let’s start from the beginning.\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## It all started with Gravity\\_UI{#start}\\n\\nGravity\\_UI is a design system and component library that powers the Yandex\\_Cloud website and dozens of other cloud products. It is [open-sourced](http://gravity-ui.com/){target=\\\"_blank\\\"} and available to everyone (we’re happy to see that activity in the community chat has noticeably increased over the last six months).\\n\\nWhat we have:\\n\\n * a set of basic React components;\\n * a landing page builder library;\\n * [detailed guides](https://gravity-ui.com/design){target=\\\"_blank\\\"} from designers on how to use the components;\\n * a library in [Figma](https://www.figma.com/community/file/1271150067798118027/Gravity-UI-Design-System-(Beta)){target=\\\"_blank\\\"};\\n * a set of nearly 600 ready-made icons;\\n * ChartKit — a data visualization package;\\n * Yagr — high-performance chart rendering based on uPlot;\\n * i18n — a UI localization package;\\n * other [useful libraries](https://gravity-ui.com/libraries){target=\\\"_blank\\\"}.\\n\\nIn March 2024, the key library was updated — UIKit v6. It introduced an updated List component, RTL support across all components, and a set of a11y improvements that enhance accessibility.\\n\\n{% cut \\\"What’s new in UIKit v6\\\" %}\\n\\n 1. **List 2.0 component**. UIKit originally had a List component, but there were things we wanted to improve. After gathering requests, we compiled the following list:\\n\\n - support for different sizes and widths;\\n - an icon on list items, with different counts and positions of icons;\\n - support for states;\\n - different content in list items (single-line, multi-line, or a list of users);\\n - support for different types of dividers and groupings.\\n\\n These are significant changes, so we created List 2.0. For now it’s released in a prestable version, but we recommend that users migrate to it and share feedback.\\n\\n 2. **RTL**. If your applications or websites need to be displayed in Hebrew, Arabic, or other right-to-left languages, you need RTL support. At the same time, in RTL:\\n\\n - an inserted Latin word is written left to right;\\n - numbers are written left to right;\\n - punctuation marks in Arabic are also written left to right, etc.\\n\\n We added RTL support across all components. To have a complete example at hand, we made an [Arabic promo page](https://gravity-ui.com/rtl){target=\\\"_blank\\\"}. You can see how it’s implemented in the source code of [landing](https://github.com/gravity-ui/landing){target=\\\"_blank\\\"}. There are also examples in [storybook](https://preview.gravity-ui.com/uikit/?path=/story/components-inputs-button--default&globals=direction:rtl){target=\\\"_blank\\\"}.\\n\\n 3. **Accessibility (a11y)**:\\n\\n - added the [eslint](https://www.npmjs.com/package/eslint-plugin-jsx-a11y){target=\\\"_blank\\\"} plugin to the project;\\n - added keyboard support for the clickable and closable states of the Persona component;\\n - removed onClick from 15 non-interactive components;\\n - added keyboard support to the SelectionTable component.\\n\\n{% endcut %}\\n\\n### How we arrived at the a11y improvements\\n\\nThis was helped by the non-visual testing team and its lead, Anatoly Popko. During a meeting, Anatoly tested the sandbox and the Gravity\\_UI [website](https://gravity-ui.com/){target=\\\"_blank\\\"} step by step to understand what accessibility issues existed at the time.\\n\\nWe tested component accessibility by navigating the site using the keyboard and special screen reader commands.\\n\\nVisually, it looked like this:\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic1.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n After the meeting, the team got actionable tasks, and GitHub received\n three new issues for the base components.\n\n\n Here’s more about them.\n\n * In the dropdown list, the second level of items [doesn’t open](https://github.com/gravity-ui/uikit/issues/1564){target=\"_blank\"} using the keyboard — only via a mouse click.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic2.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n * It’s\n [unclear](https://github.com/gravity-ui/uikit/issues/1563){target=\"_blank\"}\n which item is selected in a markdown bulleted list inside the Select\n component.\n\n\n *\n [Buttons](https://github.com/gravity-ui/uikit/issues/1562){target=\"_blank\"}\n without text labels, indicated only visually, are read out simply as\n “button” or “radio button”. This bug occurs only on the landing page;\n the component itself supports aria-label, but we didn’t use it.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic3.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n As a result, we realized that since we didn’t find dozens of issues,\n the library’s accessibility was already at a fairly good level.\n Testing components outside a real context is very difficult, so we\n decided to start checking the accessibility of a finished product.\n This helped us find additional a11y improvements.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Yandex\\_Cloud accessibility{#availability}\\n\\nInspired by the Gravity\\_UI update, we decided to run accessibility testing for our services: starting with the Yandex\\_Cloud website, and then extending this practice to other interfaces.\\n\\nThere are a number of standards that can help achieve accessibility. But to test Yandex\\_Cloud interfaces reliably and better understand how convenient our site is for everyone, we conducted an audit.\\n\\n### Accessibility audit\\n\\nWe again reached out to colleagues from the non-visual testing team and to Anatoly to test the site together, document issues, and take them into work. In total, there were two iterations nearly a month apart — the initial test and a retest.\\n\\nDuring testing, we documented a whole set of fixes that we needed to implement.\\n\\n#### On the home page\\n\\n * The search control needed a redesign. In our interface, the search component is implemented as a search area with a magnifying glass icon. Clicking either of them opens an input field for the query. In the previous implementation, for screen readers these were independent elements, which confused blind users.\\n\\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic4.png)\\n\\n * In the language selector, we used a “collapsed” state attribute even though it was essentially a selection button, not a dropdown list. There was no “language” label, and a space was missing in the “language — region” transition.\\n * In the account menu, focus wasn’t trapped: when activated, the user could move outside the menu items.\\n * The main tag on the home page was duplicated, so we had to remove one.\\n * The examples section: we needed to use a tab instead of a button.\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic5.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n paddingBottom: xs\n text: >\n\n * In the example card, the text was placed into aria-describedby, so\n the screen reader read it only once, which is inconvenient when\n reviewing important information. While investigating the bug, we\n realized we should do a comprehensive refactor of the Card component,\n so we created an issue where you can learn the details of the changes\n and take part in the discussion.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n#### Navigation\\n\\n * Focus issue: when expanding the top-level menu, focus should be moved into the submenu.\\n * We needed to remove TabIndex from the top-level menu. The current behavior caused all menu items to be announced twice.\\n * We needed to trap keyboard focus in navigation when it is expanded. Otherwise, you can “fall out” of the menu onto the page itself and then be unable to get back.\\n\\n#### Footer\\n\\n * Yandex\\_Cloud was wrapped in a list. The headings of link lists in the footer were wrapped in a list item, so NVDA treated them as part of the list and read the same list to the user twice.\\n * Labels were missing for the App Store and Google Play links. At the time of testing, fragments of the URL were read out, which gave the user no meaningful information.\\n\\n#### “Blog” section\\n\\n * The “All topics” and “All services” buttons were not associated with the button. The select buttons did not announce their content.\\n * Accessibility support was needed in the lists: when opening a dropdown, the screen reader focus should switch to the items of that list. Additionally, it should support simplified navigation between them using the regular arrow keys — without any key combinations.\\n\\n#### Blog articles\\n\\n * Breadcrumbs were missing a “You are here” element.\\n * Focus needed to be trapped in the dialog.\\n * The favorites counter had no name. The counter was a button with an icon and a number. Screen readers read the number — without the icon it wasn’t clear what the button does.\\n\\nWe collected all tasks into an epic and started working on them. For some of the tasks, GitHub issues were created.\\n\\nI’ll describe the most interesting cases in more detail.\\n\\n### Select component\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic6.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n During testing, we found that when navigating with the keyboard, the\n list item names are not announced by the screen reader. We partially\n fixed the issue using the aria-activedescendant attribute, but not\n completely.\n\n\n #### Here are the issues that remain\n\n * In Safari, this approach doesn’t work, and we don’t yet understand how to solve it.\n * Search filtering is not always supported (in fact, it’s never supported — it just hasn’t been implemented). Typically, aria-activedescendant is attached to the main dropdown element and points to the currently selected list item. Right now, it’s attached to the button that opens the dropdown. When pressing the up or down arrows, we change the button’s aria-activedescendant value to the previous or next list item. That way, the screen reader can read from the focused button which item is currently selected.\n \n The issue with search filtering is that the input field doesn’t have aria-activedescendant. If a person focuses the filter input and wants to type something, the screen reader can’t determine which list item is active, and arrow-key navigation in the list doesn’t work.\n\n * Selected options aren’t marked; we need to add an attribute to them, for example aria-selected.\n\n You can see the issue with the current problems\n [here](https://github.com/gravity-ui/uikit/issues/1760){target=\"_blank\"}.\n\n\n ### Breadcrumbs\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic7.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Breadcrumbs are a navigation element that shows the user’s path\n through the site. In our case, the screen reader read out the entire\n path, and it was impossible to understand which part of the site the\n person was on. Also, the entire component was presented simply as a\n set of links.\n\n\n We decided to step away from the standard and solve this in a simple\n and optimal way — by adding a “You are here” label for screen readers.\n In the end, we combined the standard approach with the label.\n\n\n During the work, we found that for this label to be read out, it must\n be attached to an element that screen readers do not ignore. It was\n easier to attach the label to a conventional structure than to come up\n with a way to avoid it. Still, the label is useful: it helps the user\n understand faster that what they are hearing is breadcrumbs.\n\n\n ### Images without captions\n\n Some images on the site were placed without captions, and the screen\n reader read them as “Image”. This provides no useful information to\n the user and doesn’t convey anything about the interface, so we\n decided to hide images without captions from the screen reader.\n\n\n ### Text order in blocks\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic8.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n In several places on our site, information is presented as a single\n block — for example, an event card or a blog article card.\n\n\n We noticed that screen readers read the information in an order that\n sighted users don’t follow. For an event card, the screen reader read\n the data as: registration status, time, location, and only then the\n title and event description.\n\n\n Typically, users don’t follow a linear path: they look at the title\n first, then the subtitle, and the image. The problem is that screen\n readers read elements in the order they appear in the page’s DOM tree.\n To fix the order, we had to rebuild it.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic9.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n We were surprised, but in some “operating system — browser”\n combinations this didn’t work. For example, in Mozilla Firefox on\n macOS the problem persisted despite changing the order in the page\n tree. Hopefully, Firefox developers will fix this behavior in future\n browser versions.\n\n\n ### Modal windows\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic10.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n When a sighted user opens a pop-up in the interface, their attention\n shifts to what’s inside the window. However, the content of the entire\n site is still available, and they can return their attention to it\n when needed.\n\n\n When interacting with a site via a screen reader, the situation\n changes. If the pop-up isn’t made modal, it has no boundaries. As a\n result, navigating the site becomes harder: a person can accidentally\n move out of the pop-up using element navigation.\n\n\n There is a standard for working with modal windows, and if you follow\n it, it’s not necessary to make all pop-ups in the interface modal.\n\n\n But during testing, we concluded that making pop-ups modal is a good\n practice that simplifies working with the site when using a screen\n reader.\n\n\n We decided to make all pop-ups modal. At the same time, we kept the\n option for the user to configure an alternative interaction scenario\n with such windows.\n\n\n You can set up such a scenario by assigning `role=\"dialog\",\n aria-modal=\"true\"`. In the VoiceOver + Firefox combination, this\n solution is not supported: details are in a private\n [issue](https://github.com/gravity-ui/uikit/issues/1746){target=\"_blank\"}.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Diplodoc{#diplodoc}\\n\\nInspired by our activity, the developers of the technical documentation platform [Diplodoc](https://diplodoc.com/){target=\\\"_blank\\\"} implemented a number of a11y improvements in their product:\\n\\n * They reviewed all additional YFM (Yandex Flavored Markdown) syntax elements and rewrote them so they are visible to screen readers.\\n * They improved the keyboard workflow for working with documentation: fixed the order of selecting elements and added the ability to select all interactive elements.\\n * They added correct labels and indications for UI elements, which will significantly simplify using the service for screen reader users.\\n\\nSince we use Diplodoc to render our documentation, these improvements also increased the accessibility of documentation on the Yandex\\_Cloud website.\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Results and plans{#results}\\n\\nWe’ve taken the first steps toward improving the accessibility of Yandex\\_Cloud interfaces for all users. We still need to transfer this experience to other service interfaces and fix the issues we already know about.\\n\\nGravity\\_UI made our accessibility work easier, and we continue to address open issues labeled a11y. At the time of writing, we had 14 open and 24 closed issues — you can see them [here](https://github.com/gravity-ui/uikit/issues?q=is%3Aissue+is%3Aopen+label%3Aa11y){target=\\\"_blank\\\"}.\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic11.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nWe’re looking forward to your PRs and comments on accessibility and how the services work, as well as examples of using Gravity\\_UI on your websites.\\n\"\n - type: blog-layout-block\n resetPaddings: true\n fullWidth: false\n children:\n - type: blog-meta-block\n column: left\n resetPaddings: true\n - type: blog-suggest-block\n resetPaddings: true\n", + "title": "", + "noIndex": false, + "shareTitle": null, + "shareDescription": null, + "shareImage": null, + "pageLocaleId": null, + "author": "timofeyevvv", + "metaDescription": null, + "keywords": [], + "shareGenTitle": null, + "canonicalLink": null, + "sharingType": "auto", + "sharingTheme": "light", + "comment": "initial", + "shareImageUrl": "https://storage.cloud-preprod.yandex.net/ui-api-ru-preprod-stable-share-generator-screenshots/cache/6a79bcd295f4b97553c519c462935cc394f84cb8.png", + "pageRegionId": 78, + "summary": null, + "versionId": 207, + "service": null, + "solution": null, + "locales": [ + { + "id": 77, + "pageId": 72, + "locale": "ru", + "createdAt": "2026-01-15T12:21:17.154Z", + "updatedAt": "2026-01-15T12:21:17.224Z", + "publishedVersionId": null, + "lastVersionId": 202 + }, + { + "id": 78, + "pageId": 72, + "locale": "en", + "createdAt": "2026-01-15T12:21:17.234Z", + "updatedAt": "2026-01-15T12:21:17.297Z", + "publishedVersionId": null, + "lastVersionId": 203 + } + ], + "pageRegions": [ + { + "regionCode": "ru-ru", + "publishedVersionId": null + }, + { + "regionCode": "en", + "publishedVersionId": 207 + } + ], + "searchCategory": { + "id": 7, + "slug": "blog", + "title": "Blog", + "url": "/blog" + }, + "voiceovers": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/en/posts/graph.json b/src/api/.mocks/en/posts/graph.json new file mode 100644 index 000000000000..db680b132b16 --- /dev/null +++ b/src/api/.mocks/en/posts/graph.json @@ -0,0 +1,24 @@ +{ + "id": 36, + "name": "graph", + "isPinned": false, + "postId": 36, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "readingTime": 17, + "title": "Graph Visualization Library: How We Solved the Canvas vs. HTML Dilemma", + "date": "2025-08-07T00:00:00Z", + "likes": 0, + "hasUserLike": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": null, + "slug": "articles", + "name": "Articles" + } + ], + "authors": [], + "services": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/en/posts/index.json b/src/api/.mocks/en/posts/index.json index d29ff18dd054..23167f6d9257 100644 --- a/src/api/.mocks/en/posts/index.json +++ b/src/api/.mocks/en/posts/index.json @@ -1,13 +1,85 @@ { "posts": [ + { + "id": 36, + "name": "graph", + "date": "2025-08-07T00:00:00Z", + "title": "Graph Visualization Library: How We Solved the Canvas vs. HTML Dilemma", + "postId": 36, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/graph-cover-bg.png", + "readingTime": 17, + "likes": 0, + "hasUserLike": false, + "hasPublishedVersionInRegion": true, + "hasPublishedVersionInLocale": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": null, + "slug": "articles", + "name": "Articles" + } + ], + "services": [], + "url": "/blog/graph" + }, + { + "id": 37, + "name": "yc-site-in-gravity-ui", + "date": "2024-10-28T00:00:00Z", + "title": "How We Made Yandex Cloud More Accessible with the Gravity UI Design System", + "postId": 37, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "readingTime": 20, + "likes": 0, + "hasUserLike": false, + "hasPublishedVersionInRegion": true, + "hasPublishedVersionInLocale": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": null, + "slug": "articles", + "name": "Articles" + } + ], + "services": [], + "url": "/blog/yc-site-in-gravity-ui" + }, + { + "id": 38, + "name": "md-editor-in-gravity-ui", + "date": "2024-10-01T00:00:00Z", + "title": "Markdown Editor: A WYSIWYG and Markup Editor Built on Gravity UI", + "postId": 38, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/mdeditor-cover-bg.png", + "readingTime": 7, + "likes": 0, + "hasUserLike": false, + "hasPublishedVersionInRegion": true, + "hasPublishedVersionInLocale": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": null, + "slug": "articles", + "name": "Articles" + } + ], + "services": [], + "url": "/blog/md-editor-in-gravity-ui" + }, { "id": 35, "name": "gravity-ui-in-opensource", - "date": "2025-12-25T00:00:00Z", - "title": "Gravity UI Design System: How to Easily Build Your Interface", + "date": "2023-12-12T00:00:00Z", + "title": "Gravity UI design system: how to build your own interface easily", "postId": 35, "description": "", - "image": "https://storage.yandexcloud.net/yandex-opensource/blog-cover-bg.png", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/gravity-ui-cover.png", "readingTime": 10, "likes": 0, "hasUserLike": false, @@ -16,8 +88,8 @@ "addLegacyPrefix": false, "tags": [ { + "icon": null, "slug": "articles", - "icon": "", "name": "Articles" } ], @@ -25,8 +97,6 @@ "url": "/blog/gravity-ui-in-opensource" } ], - "totalCount": 1, - "count": 1, - "pinnedPost": null + "totalCount": 4, + "count": 4 } - diff --git a/src/api/.mocks/en/posts/md-editor-in-gravity-ui.json b/src/api/.mocks/en/posts/md-editor-in-gravity-ui.json new file mode 100644 index 000000000000..5f79fa8c36c6 --- /dev/null +++ b/src/api/.mocks/en/posts/md-editor-in-gravity-ui.json @@ -0,0 +1,24 @@ +{ + "id": 38, + "name": "md-editor-in-gravity-ui", + "isPinned": false, + "postId": 38, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "readingTime": 7, + "title": "Markdown Editor: A WYSIWYG and Markup Editor Built on Gravity UI", + "date": "2024-10-01T00:00:00Z", + "likes": 0, + "hasUserLike": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": null, + "slug": "articles", + "name": "Articles" + } + ], + "authors": [], + "services": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/en/posts/yc-site-in-gravity-ui.json b/src/api/.mocks/en/posts/yc-site-in-gravity-ui.json new file mode 100644 index 000000000000..aea5563f3f90 --- /dev/null +++ b/src/api/.mocks/en/posts/yc-site-in-gravity-ui.json @@ -0,0 +1,24 @@ +{ + "id": 37, + "name": "yc-site-in-gravity-ui", + "isPinned": false, + "postId": 37, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "readingTime": 20, + "title": "How We Made Yandex Cloud More Accessible with the Gravity UI Design System", + "date": "2024-10-28T00:00:00Z", + "likes": 0, + "hasUserLike": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": null, + "slug": "articles", + "name": "Articles" + } + ], + "authors": [], + "services": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/en/tags.json b/src/api/.mocks/en/tags.json index 455fe3df1efb..3d16e46319cc 100644 --- a/src/api/.mocks/en/tags.json +++ b/src/api/.mocks/en/tags.json @@ -4,7 +4,7 @@ "slug": "articles", "createdAt": "2020-03-13T11:00:57.360Z", "updatedAt": "2022-07-22T08:50:25.432Z", - "icon": "", + "icon": "", "isDeleted": false, "name": "Articles", "locale": "en", diff --git a/src/api/.mocks/ru/pages/graph.json b/src/api/.mocks/ru/pages/graph.json new file mode 100644 index 000000000000..f8d7f77ae4a1 --- /dev/null +++ b/src/api/.mocks/ru/pages/graph.json @@ -0,0 +1,74 @@ +{ + "id": 71, + "name": "blog/graph", + "createdAt": "2026-01-15T11:28:28.437Z", + "updatedAt": "2026-01-15T11:28:28.437Z", + "type": "default", + "isDeleted": false, + "versionOnTranslationId": null, + "searchCategorySlug": "blog", + "regions": [], + "pageId": 71, + "regionCode": "ru-ru", + "publishedVersionId": 199, + "lastVersionId": 199, + "content": "blocks:\n - type: blog-header-block\n resetPaddings: true\n paddingBottom: l\n width: m\n verticalOffset: m\n theme: dark\n background:\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png\n disableCompress: true\n color: '#CCDAFF'\n fullWidth: false\n - type: blog-layout-block\n resetPaddings: true\n mobileOrder: reverse\n children:\n - type: blog-yfm-block\n resetPaddings: true\n column: right\n text: >\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/speaker.jpg\n =80x)\n\n\n **Андрей Щетинин**\n\n Старший разработчик интерфейсов\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: |\n\n В этой статье:\n\n - [Откуда взялась задача](#task)\n - [Как мы пришли к решению](#solution)\n - [Кастомизация](#customization)\n - [Наша библиотека графов: в чем плюсы и как пользоваться](#library)\n - [А есть ли аналоги?](#analogs)\n - [Планы на будущее](#future)\n - [Попробуйте и присоединяйтесь](#try)\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Привет! Меня зовут Андрей, я разработчик интерфейсов в команде User\n Experience инфраструктурных сервисов Яндекса. Мы развиваем Gravity UI\n — опенсорсную дизайн‑систему и библиотеку React‑компонентов, которую\n используют десятки продуктов внутри компании и за её пределами.\n Сегодня расскажу, как мы столкнулись с задачей визуализации сложных\n графов, почему существующие решения нас не устроили, и как в итоге\n появилась @gravity‑ui/graph — библиотека, которую мы решили сделать\n открытой для сообщества.\n\n\n Эта история началась с практической проблемы: нам нужно было рендерить\n графы на 10 000+ элементов с интерактивными компонентами. В Яндексе\n много проектов, где пользователи создают сложные пайплайны обработки\n данных — от простых ETL‑процессов до машинного обучения. Когда такие\n пайплайны создаются программно, количество блоков может достигать\n десятков тысяч.\n\n\n Существующие решения нас не устраивали:\n\n * **HTML/SVG‑библиотеки** красиво выглядят и удобны в разработке, но начинают тормозить уже на сотнях элементов.\n * **Canvas‑решения** справляются с производительностью, но требуют огромного количества кода для создания сложных UI‑элементов.\n\n Нарисовать кнопку с закруглёнными углами и градиентом в Canvas\n несложно. Однако проблемы появляются, когда нужно создать свои сложные\n контролы или разметку — потребуется писать десятки строк\n низкоуровневых команд рисования. Каждый элемент интерфейса приходится\n программировать с нуля — от обработки кликов до анимаций. А нам нужны\n были полноценные UI‑компоненты: кнопки, селекты, поля ввода,\n drag‑and‑drop.\n\n\n Мы решили не выбирать между Canvas и HTML, а использовать всё лучшее\n из обеих технологий. Идея была проста: автоматически переключаться\n между режимами в зависимости от того, насколько близко пользователь\n смотрит на граф.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic1.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n #### Попробуйте сами\n\n * [GitHub репозиторий](https://github.com/gravity-ui/graph){target=\"_blank\"}\n * [Storybook с примерами](https://preview.gravity-ui.com/graph/){target=\"_blank\"}\n * [Playground](https://gravity-ui.com/ru/libraries/graph/playground){target=\"_blank\"}\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Откуда взялась задача{#task}\n\n\n ### Нирвана и её графы\n\n\n У нас в Яндексе есть сервис Нирвана для создания и выполнения графов\n обработки данных (мы про неё\n [писали](https://habr.com/ru/companies/yandex/articles/351016/){target=\"_blank\"}\n аж в 2018 году). Сервис большой, популярный, существует уже давно.\n\n\n Часть пользователей создаёт графы руками — водят мышкой, добавляют\n блоки, соединяют их. С такими графами проблем нет: блоков не много, и\n всё работает отлично. Но есть проекты, которые создают графы\n программно. И здесь начинаются сложности: они могут положить в один\n граф до 10 000 операций. И получается такое:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n {% cut \"И такое:\" %}\n\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-1.png\n =830x)\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-2.png\n =830x)\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-3.png\n =830x)\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-4.png\n =830x)\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic2-5.png\n =830x)\n\n\n {% endcut %}\n\n\n Такие графы обычная связка HTML + SVG просто не тянет. Браузер\n начинает тормозить, память утекает, пользователь страдает. Мы пытались\n решить проблему в лоб: оптимизировать рендеринг HTML, но рано или\n поздно упирались в физические ограничения — DOM просто не рассчитан на\n тысячи одновременно видимых плавающих интерактивных элементов.\n\n\n Нужно другое решение, и в браузере у нас остался только Canvas. Только\n он сможет обеспечить необходимую производительность.\n\n\n Первая мысль — найти готовое решение. На дворе был 2017–2018 год, и мы\n перелопатили популярные библиотеки для Canvas или рендеринга графов,\n но все решения упирались в одну и ту же проблему: либо используй\n Canvas и примитивные элементы, либо используй HTML/SVG и жертвуй\n производительностью.\n\n\n А что если не выбирать?\n\n\n ### Level of Details: вдохновение из GameDev\n\n\n В GameDev и картографии есть классная концепция — Level of Details\n (LOD). Эта техника родилась из необходимости — как показать огромный\n мир, не убив производительность?\n\n\n Суть простая: у одного объекта может быть несколько уровней\n детализации в зависимости от того, как близко на него смотрят. В играх\n это особенно заметно:\n\n * Вдалеке видны горы — это простые полигоны с базовой текстурой.\n * Подходите ближе — появляютcя детали: трава, камни, тени.\n * Ещё ближе — видны отдельные листья на деревьях.\n\n Никто не рендерит миллионы полигонов травы, когда игрок стоит на\n вершине горы и смотрит вдаль.\n\n\n В картах принцип тот же — у каждого уровня масштаба свой набор данных\n и своя детализация:\n\n * Масштаб континента — видны только страны.\n * Приближаетесь к городу — появляются улицы и районы.\n * Ещё ближе — номера домов, кафе, автобусные остановки.\n\n Мы поняли: пользователю не нужны интерактивные кнопки на большом\n масштабе графа из 10 000 блоков — он их всё равно не увидит и не\n сможет с ними работать.\n\n\n Более того, попытка отрендерить 10 000 HTML‑элементов одновременно\n приведёт к фризу браузера. Но когда он зумится на конкретную область,\n количество видимых блоков резко падает — с 10 000 до, скажем, 50. Вот\n тут‑то и освобождаются ресурсы для HTML‑компонентов с богатой\n интерактивностью.\n\n\n ### Три уровня нашей схемы Level of Details\n\n\n #### Minimalistic (масштаб 0,1–0,3) — Canvas с простыми примитивами\n\n\n В этом режиме пользователь видит общую архитектуру системы: где\n расположены основные группы блоков, как они соединены между собой.\n Каждый блок — это простой прямоугольник с базовой цветовой кодировкой.\n Никаких текстов, кнопок, детализированных иконок. Зато можно комфортно\n рендерить тысячи элементов. На этом уровне пользователь выбирает\n область для детального изучения.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic3.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n #### Schematic (масштаб 0,3–0,7) — Canvas с деталями\n\n\n Появляются названия блоков, иконки состояний, якоря для соединений.\n Текст рендерится средствами Canvas API — это быстро, но возможности\n стилизации ограничены. Связи между блоками становятся более\n информативными: можно показать направление потока данных, статус\n соединения. Это переходный режим, где производительность Canvas\n сочетается с базовой информативностью.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic4.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n #### Detailed (масштаб 0,7+) — HTML с полной интерактивностью\n\n\n Здесь блоки превращаются в полноценные компоненты интерфейса: с\n кнопками управления, полями для параметров, прогресс‑барами,\n селектами. Можно использовать любые возможности HTML/CSS, подключать\n UI‑библиотеки. В этом режиме в viewport обычно помещается не больше\n 20–50 блоков, что комфортно для детальной работы.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic5.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n **А что если считать FPS для выбора уровня детализации?**\n\n\n У нас были подходы к выборе детализации основе FPS. Но оказалось что\n такой подход создаёт нестабильность — при росте производительности\n система переключается на более детальный режим, что снижает FPS и\n может вызывать переключение обратно — и так по кругу.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Как мы пришли к решению{#solution}\n\n\n Хорошо, LOD — это круто. Но реализация потребует Canvas для\n производительности, а это новая головная боль. Рисовать на Canvas это\n не очень сложно — проблемы появляются когда нужно сделать\n интерактивность.\n\n\n ### Проблема: как понять, куда кликнул пользователь?\n\n\n В HTML всё просто: кликнул на кнопку — получил событие сразу на\n элементе. В Canvas сложнее: кликнул на холст — и что дальше? Нужно\n самим выяснить, на какой элемент кликнул пользователь.\n\n\n Базово существуют три подхода:\n\n * Pixel Testing (color picking),\n * Geometric approach (простой перебор всех элементов),\n * Spatial Indexing (пространственный индекс).\n\n #### Pixel Testing (color picking)\n\n\n Идея проста: создаём второй невидимый canvas, копируем туда сцену, но\n каждый элемент заливаем уникальным цветом, который будет считаться ID\n объекта. При клике считываем цвет пикселя под указателем мыши через\n getImageData и так получаем ID элемента.\n\n\n #|\n\n ||**Плюсы**|**Минусы**||\n\n ||* Реализуется за пару десятков строк\n\n\n * Не требует дополнительных структур данных|* Сглаживание Canvas\n смешивает цвета — клик на границе фигуры может дать «невалидный» ID\n\n\n * Выключение anti‑aliasing в 2D‑Canvas недоступно\n\n\n * Второй холст дублирует память и удваивает рендер‑проход||\n\n |#\n\n\n Для небольших сцен метод годится, но при 10 000+ элементах процент\n ошибок становится неприемлемым — откладываем Pixel Testing.\n\n\n #### Geometric approach (простой перебор всех элементов)\n\n\n Идея проста: перебираем все элементы и проверяем, находится ли точка\n клика внутри элемента.\n\n\n #|\n\n ||**Плюсы**|**Минусы**||\n\n ||* Реализуется за пару десятков строк\n\n\n * Не требует дополнительных структур данных|* Очень медленно работает\n при большом количестве элементов\n\n\n * Не подходит для больших сцен||\n\n |#\n\n\n #### Spatial Indexing\n\n\n Развитие геометрического подхода. В геометрическом подходе мы\n упирались в количество элементов. Алгоритмы пространственного индекса\n стараются как‑то сгруппировать рядом стоящие элементы, используя в\n основном деревья, что позволяет снизить сложность до log n.\n\n\n Алгоритмов пространственного индекса довольно много, мы выбрали\n структуру данных R‑Tree в виде библиотеки\n [rbush](https://github.com/mourner/rbush){target=\"_blank\"}.\n\n\n R‑Tree — это, как понятно из названия, дерево, где каждый объект\n помещается в прямоугольник минимального размера (MBR), а затем эти\n прямоугольники группируются в более крупные прямоугольники. И так\n получается дерево, где каждый прямоугольник содержит в себе другие\n прямоугольники.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: >-\n Картинка из википедии\n [R‑tree](https://en.wikipedia.org/wiki/R-tree){target=\"_blank\"}\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic6.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Для поиска в RTree нам нужно спускаться по дереву (вглубь\n прямоугольника), пока мы не попадём в конкретный элемент. Путь\n выбирается проверкой пересечения поискового прямоугольника с MBR. Все\n ветви, чьи bounding‑box даже не задевают поисковый прямоугольник,\n отбрасываются сразу — именно поэтому глубина обхода обычно\n ограничивается 3–5 уровнями, а сам поиск занимает микросекунды даже на\n десятках тысяч элементов.\n\n\n Этот вариант работает хоть и медленнее (O(log n) в лучшем случае и\n O(n) в худшем), чем pixel testing, но он точнее и менее требователен к\n памяти.\n\n\n #### Событийная модель\n\n\n На основе RTree мы теперь можем построить нашу событийную модель.\n Когда пользователь кликает, запускается процедура хит‑тест: формируем\n прямоугольник размером 1×1 пиксель в координатах курсора и ищем его\n пересечение в R‑Tree. Получив элемент, в который попадает этот\n прямоугольник, мы делегируем событие этому элементу. Если элемент не\n остановил событие, то это событие передаётся его родителю и так до\n рута. Поведение этой модели похоже на поведение привычной нам\n событийной модели в браузере. События можно перехватить, запревентить\n или остановить всплытие.\n\n\n Как я уже упомянул, при хит‑тесте мы формируем прямоугольник размером\n 1×1 пиксель, а это значит, что мы можем сформировать прямоугольник\n любого размера. И это нам поможет сделать еще одну очень важную\n оптимизацию — Spatial Culling.\n\n\n ### Spatial Culling\n\n\n Spatial Culling — это техника оптимизации рендеринга, которая нацелена\n на то, чтобы не рисовать то, что не видно. Например, чтобы не рисовать\n объекты, которые находятся вне пространства камеры или которые\n загорожены другими элементами сцены. Так как наш граф рисуется в\n 2D‑пространстве, то нам достаточно не рисовать лишь те объекты,\n которые находятся вне области видимости камеры (viewport).\n\n\n Как это работает:\n\n * при каждом перемещении или зуме камеры мы формируем прямоугольник, равный текущему viewport;\n * ищем его пересечение в R‑Tree;\n * результатом становится список элементов, которые действительно видны;\n * мы рендерим только их, всё остальное пропускается.\n\n Такой приём делает производительность почти независимой от общего\n количества элементов: если в кадре помещается 40 блоков — библиотека\n нарисует ровно 40, а не десятки тысяч, скрытых за пределами экрана. На\n дальних масштабах в viewport попадает большое количество элементов,\n поэтому мы рисуем лёгкие Canvas‑примитивы, а при приближении камеры\n количество элементов снижается и высвобождённые ресурсы позволяют\n переключиться на HTML‑режим с полной детализацией.\n\n\n Сводя всё воедино, получается простая схема:\n\n * Canvas отвечает за скорость,\n * HTML — за интерактивность,\n * R‑Tree и Spatial Culling незаметно объединяют их в единую систему, позволяя быстро понимать какие элементы можно нарисовать на html‑слое.\n\n Пока камера двигается, маленький viewport спрашивает у R‑Tree лишь те\n объекты, которые реально находятся в кадре. Такой подход позволяет нам\n рисовать действительно большие графы, или по крайней мере иметь запас\n производительности до тех пор, пока пользователь не ограничит\n viewport.\n\n\n Итого в своём ядре библиотека содержит:\n\n * Canvas‑режим с простыми примитивами;\n * HTML‑режим с полной детализацией;\n * R‑Tree и Spatial Culling для оптимизации производительности;\n * привычную событийную модель.\n\n Но для продакшена этого недостаточно, нужно иметь возможность\n расширять библиотеку и кастомизировать её под свои нужды.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Кастомизация{#customization}\n\n\n Библиотека предлагает два взаимодополняющих способа расширения и\n изменения поведения:\n\n * Переопределение базовых компонентов. Меняем логику стандартных Block, Anchor, Connection.\n * Расширение через слои (Layers). Добавляем принципиально новую функциональность поверх/под существующей сцены.\n\n ### Переопределение компонентов\n\n\n Когда нужно модифицировать внешний вид или поведение уже существующих\n элементов, наследуемся от базового класса и переопределяем ключевые\n методы. Затем регистрируем компонент под собственным именем.\n\n\n #### Кастомизация блоков\n\n\n Например, если вам нужно создать граф с прогресс‑барами на блоках —\n скажем, для отображения статуса выполнения задач в пайплайне — вы\n можете легко кастомизировать стандартные блоки:\n\n\n ```json\n\n import { CanvasBlock } from \"@gravity‑ui/graph\";\n\n\n class ProgressBlock extends CanvasBlock {\n // Базовая форма блока с закругленными углами\n public override renderBody(ctx: CanvasRenderingContext2D): void {\n ctx.fillStyle = \"#ddd\";\n ctx.beginPath();\n ctx.roundRect(this.state.x, this.state.y, this.state.width, this.state.height, 12);\n ctx.fill();\n ctx.closePath();\n }\n\n public renderSchematicView(ctx: CanvasRenderingContext2D): void {\n const progress = this.state.meta?.progress || 0;\n\n // Рисуем основу блока\n this.renderBody(ctx);\n\n // Прогресс‑бар с цветовой индикацией\n const progressWidth = (this.state.width - 20) * (progress / 100);\n ctx.fillStyle = progress < 50 ? \"#ff6b6b\" : progress < 80 ? \"#feca57\" : \"#48cae4\";\n ctx.fillRect(this.state.x + 10, this.state.y + this.state.height - 15, progressWidth, 8);\n\n // Рамка прогресс‑бара\n ctx.strokeStyle = \"#ddd\";\n ctx.lineWidth = 1;\n ctx.strokeRect(this.state.x + 10, this.state.y + this.state.height - 15, this.state.width - 20, 8);\n\n // Текст с процентами и названием\n ctx.fillStyle = \"#2d3436\";\n ctx.font = \"12px Arial\";\n ctx.textAlign = \"center\";\n ctx.fillText(`${Math.round(progress)}%`, this.state.x + this.state.width / 2, this.state.y + 20);\n ctx.fillText(this.state.name, this.state.x + this.state.width / 2, this.state.y + 40);\n }\n }\n\n ```\n\n\n #### Кастомизация соединений\n\n\n Аналогично, если вам нужно изменить поведение и внешний вид связей, —\n например, показать интенсивность потока данных между блоками — вы\n можете создать кастомное соединение:\n\n\n ```json\n\n import { BlockConnection } from \"@gravity-ui/graph\";\n\n\n class DataFlowConnection extends BlockConnection {\n public override style(ctx: CanvasRenderingContext2D) {\n // Получаем данные о потоке из связанных блоков\n const sourceBlock = this.sourceBlock;\n const targetBlock = this.targetBlock;\n\n const sourceProgress = sourceBlock?.state.meta?.progress || 0;\n const targetProgress = targetBlock?.state.meta?.progress || 0;\n\n // Вычисляем интенсивность потока на основе прогресса блоков\n const flowRate = Math.min(sourceProgress, targetProgress);\n const isActive = flowRate > 10; // Поток активен при прогрессе > 10%\n\n if (isActive) {\n // Активный поток -- толстая зеленая линия\n ctx.strokeStyle = \"#00b894\";\n ctx.lineWidth = Math.max(2, Math.min(6, flowRate / 20));\n } else {\n // Неактивный поток -- пунктирная серая линия\n ctx.strokeStyle = \"#ddd\";\n ctx.lineWidth = this.context.camera.getCameraScale();\n ctx.setLineDash([5, 5]);\n }\n\n return { type: \"stroke\" };\n }\n }\n\n ```\n\n\n #### Использование кастомных компонентов\n\n\n Регистрируем созданные компоненты в настройках графа:\n\n\n ```json\n\n const customGraph = new Graph({\n blocks: [\n {\n id: \"task1\",\n is: \"progress\",\n x: 100,\n y: 100,\n width: 200,\n height: 80,\n name: \"Data Processing\",\n meta: { progress: 75 },\n },\n {\n id: \"task2\",\n is: \"progress\",\n x: 400,\n y: 100,\n width: 200,\n height: 80,\n name: \"Analysis\",\n meta: { progress: 30 },\n },\n {\n id: \"task3\",\n is: \"progress\",\n x: 700,\n y: 100,\n width: 200,\n height: 80,\n name: \"Output\",\n meta: { progress: 5 },\n },\n ],\n connections: [\n { sourceBlockId: \"task1\", targetBlockId: \"task2\" },\n { sourceBlockId: \"task2\", targetBlockId: \"task3\" },\n ],\n settings: {\n // Регистрируем кастомные блоки\n blockComponents: {\n 'progress': ProgressBlock,\n },\n // Регистрируем кастомное соединение для всех связей\n connection: DataFlowConnection,\n useBezierConnections: true,\n },\n });\n\n\n customGraph.setEntities({\n blocks: [\n {\n is: 'progress',\n id: '1',\n name: \"progress block',\n x: 10, \n y: 10, \n width: 10, \n height: 10,\n anchors: [],\n selected: false,\n }\n ]\n })\n\n\n customGraph.start();\n\n ```\n\n\n #### Результат\n\n\n В результате получается граф, где:\n\n * блоки показывают текущий прогресс с цветовой индикацией;\n * соединения визуализируют поток данных: активные потоки — зелёные и толстые, неактивные — серые и пунктирные;\n * при зуме блоки автоматически переключаются на HTML‑режим с полной интерактивностью.\n\n ### Расширение слоями\n\n\n Слои — это дополнительные Canvas или HTML‑элементы, которые\n вставляются в «пространство» графа. По сути, каждый слой — это\n отдельный канал рендеринга, который может содержать собственный canvas\n для быстрой графики или HTML‑контейнер для сложных интерактивных\n элементов.\n\n\n Кстати, именно через слои работает React‑интеграция нашей библиотеки:\n React‑компоненты рендерятся в HTML‑слой через React Portal.\n\n\n #### Архитектура слоёв\n\n\n Слои — это еще одно решение ключевое решение дилеммы Canvas vs HTML.\n Слои синхронизируют позиции Canvas и HTML‑элементов, обеспечивая\n правильное наложение их друг на друга. Это позволяет бесшовно\n переключать Canvas и HTML оставаясь в едином пространстве. Граф\n состоит из независимых слоёв, наложенных друг на друга:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/canvas-vs-html/pic7.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Слои могут работать в двух системах координат:\n\n * Привязанные к графу (`transformByCameraPosition: true`):\n\n * элементы движутся вместе с камерой,\n * блоки, соединения, элементы графа.\n\n * Зафиксированные на экране (`transformByCameraPosition: false`):\n\n * остаются на месте при панорамировании,\n * тулбары, легенды, контролы UI.\n\n #### Как устроена React‑интеграция\n\n\n Слой с React‑интеграцией достаточно показателен для демонстрации, что\n такое слои. Для начала давайте посмотрим на компонент, выделяющий\n список блоков, которые находятся в области видимости камеры. Для этого\n нам нужно подписаться на изменения камеры и после каждого изменения\n делать проверку пересечения viewport камеры c hitbox элементов.\n\n\n ```json\n\n import { Graph } from \"@gravity-ui/graph\";\n\n\n const BlocksList = ({ graph, renderBlock }: { graph: Graph,\n renderBlock: (graph: Graph, block: TBlock) => React.JSX.Element }) =>\n {\n const [blocks, setBlocks] = useState([]);\n\n const updateVisibleList = useCallback(() => {\n const cameraState = graph.cameraService.getCameraState();\n const CAMERA_VIEWPORT_TRESHOLD = 0.5;\n const x = -cameraState.relativeX - cameraState.relativeWidth * CAMERA_VIEWPORT_TRESHOLD;\n const y = -cameraState.relativeY - cameraState.relativeHeight * CAMERA_VIEWPORT_TRESHOLD;\n const width = -cameraState.relativeX + cameraState.relativeWidth * (1 + CAMERA_VIEWPORT_TRESHOLD) - x;\n const height = -cameraState.relativeY + cameraState.relativeHeight * (1 + CAMERA_VIEWPORT_TRESHOLD) - y;\n \n const blocks = graph\n .getElementsOverRect(\n {\n x,\n y,\n width,\n height,\n }, // определяет области в которой будет искаться список блоков\n [CanvasBlock] // определяет типы элементов, которые будут искаться в области видимости камеры\n ).map((component) => component.connectedState); // Получаем список моделей блоков\n\n setBlocks(blocks);\n });\n\n useGraphEvent(graph, \"camera-change\", ({ scale }) => {\n if (scale >= 0.7) {\n // Если масштаб больше 0.7, то обновляем список блоков\n updateVisibleList()\n return;\n }\n setBlocks([]);\n });\n\n return blocks.map(block => {renderBlock(graphObject, block)})\n }\n\n ```\n\n\n Теперь давайте посмотрим на описание самого слоя, который будет\n использовать этот компонент.\n\n\n ```json\n\n import { Layer } from '@gravity-ui/graph';\n\n\n class ReactLayer extends Layer {\n constructor(props: TReactLayerProps) {\n super({\n html: {\n zIndex: 3, // поднимаем слой над остальными слоями\n classNames: [\"no-user-select\"], // добавляем класс для отключения выделения текста\n transformByCameraPosition: true, // слой привязан к камере - теперь слой будет двигаться вместе с камерой\n },\n ...props,\n });\n }\n\n public renderPortal(renderBlock: (block: T) => React.JSX.Element) {\n if (!this.getHTML()) {\n return null;\n }\n\n const htmlLayer = this.getHTML() as HTMLDivElement;\n\n return createPortal(\n React.createElement(BlocksList, {\n graph: this.context.graph,\n renderBlock: renderBlock,\n }),\n htmlLayer,\n );\n }\n }\n\n ```\n\n\n Теперь мы можем использовать этот слой в нашем приложении.\n\n\n ```json\n\n import { Flex } from \"@gravity-ui/uikit\";\n\n\n const graph = useMemo(() => new Graph());\n\n const containerRef = useRef();\n\n\n useEffect(() => {\n if (containerRef.current) {\n graph.attach(containerRef.current);\n }\n\n return () => {\n graph.detach();\n };\n }, [graph, containerRef]);\n\n\n const reactLayer = useLayer(graph, ReactLayer, {});\n\n\n const renderBlock = useCallback((graph, block) => {block.name})\n\n return (\n
\n
\n {graph && reactLayer && reactLayer.renderPortal(renderBlock)}\n
\n
\n );\n ```\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n В целом всё довольно просто. Ничего из того, что было описано выше, не\n нужно писать самому — всё уже написано и готово к использованию.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Наша библиотека графов: в чем плюсы и как пользоваться{#library}\n\n\n Когда мы начинали работу над библиотекой, главным вопросом было: как\n сделать так, чтобы разработчику не пришлось выбирать между\n производительностью и удобством разработки? Ответ оказался в\n автоматизации этого выбора.\n\n\n ### Преимущества\n\n\n #### Производительность + удобство\n\n\n [@gravity‑ui/graph](https://github.com/gravity-ui/graph){target=\"_blank\"}\n автоматически переключается между Canvas и HTML в зависимости от\n масштаба. Это означает, что вы получаете:\n\n * Стабильные 60 FPS на графах из тысяч элементов.\n * Возможность использовать полноценные HTML‑компоненты с богатой интерактивностью при детальном просмотре.\n * Единую событийную модель независимо от способа рендеринга — click, mouseenter работают одинаково на Canvas и в HTML.\n\n #### Совместимость с UI‑библиотеками\n\n\n Одно из главных преимуществ — совместимость с любыми UI‑библиотеками.\n Если ваша команда использует:\n\n * Gravity UI,\n * Material‑UI,\n * Ant Design,\n * кастомные компоненты.\n\n …, то вам не нужно от них отказываться! При увеличении масштаба граф\n автоматически переключается в HTML‑режим, где привычные `Button`,\n `Select`, `DatePicker` в нужной вам цветовой теме работают точно так\n же, как в обычном React‑приложении.\n\n\n #### Framework agnostic\n\n\n Хотя мы и реализовали базовый HTML‑renderer используя React, мы\n постарались разрабатывать библиотеку так, чтобы библиотека оставалась\n framework‑agnostic. Это значит при необходимости вы можете довольно\n просто реализовать слой с интеграцией вашего любимого фреймворка.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## А есть ли аналоги?{#analogs}\\n\\nНа рынке сейчас довольно много решений для отрисовки графов, от платных решений вроде [yFiles](https://yfiles.dev/){target=\\\"_blank\\\"}, [JointJS](https://github.com/clientIO/joint){target=\\\"_blank\\\"}, до опенсорс‑решений [Foblex Flow](https://github.com/Foblex/f-flow){target=\\\"_blank\\\"}, [baklavajs](https://github.com/newcat/baklavajs){target=\\\"_blank\\\"}, [jsPlumb](https://github.com/jsplumb/community-edition){target=\\\"_blank\\\"}. Но мы для сравнения рассматриваем [@antv/g6](https://github.com/antvis/G6){target=\\\"_blank\\\"} и [React Flow](https://github.com/xyflow/xyflow){target=\\\"_blank\\\"} как наиболее популярные инструменты. Каждый из них обладает своими особенностями.\\n\\nReact Flow — хорошая библиотека, заточенная на построение node‑based интерфейсов. У неё есть очень большие возможности, но из‑за использования svg и html — довольно скромная производительность. Библиотека хороша, когда есть уверенность, что графы не будут превышать 100–200 блоков.\\n\\nВ свою очередь у @antv/g6 есть куча возможностей, она поддерживает Canvas и в частности WebGL. Напрямую @antv/g6 и @gravity‑ui/graph, наверное, сравнивать нельзя: ребята больше ориентируются на построения графов и диаграмм, — но node‑based UI тоже поддерживается. Так что antv/g6 подойдёт, если вам важно не только node‑based интерфейс, но и нарисовать графики.\\n\\nХотя библиотека @antv/g6 умеет как в canvas/webgl, так и в html/svg, управление правилами переключения придётся делать руками, и нужно это сделать правильно. По производительности она сильно быстрее, чем React Flow, но к библиотеке всё же есть вопросы. Хотя заявлено, что есть поддержка WebGL, если посмотреть на их [стресс‑тест](https://g6.antv.antgroup.com/en/examples/performance/massive-data#60000){target=\\\"_blank\\\"}, то заметно, что на 60к нодах библиотека не способна обеспечить динамики — на MacBook M3 отрисовка одного кадра заняла 4 секунды. Для сравнения наш [стресс‑тест](https://preview.gravity-ui.com/graph/?path=/story/stories-main-grapheditor--graph-stress-test){target=\\\"_blank\\\"} на 111к нод и 109к связей на том же Macbook M3: рендеринг сцены всего графа занимает ~60ms что даёт ~15–20FPS. Это не очень много, но с учетом Spatial Culling есть возможность ограничить viewport и таким образом улучшить отзывчивость. Хотя мейнтейнеры [заявляли](https://github.com/antvis/G6/issues/1597){target=\\\"_blank\\\"}, что хотят добиться рендеринга 100к нод в 30 FPS, судя по всему, добиться этого им пока не удалось.\\n\\nЕщё один пункт, по которому @gravity‑ui/graph выигрывает, — это размер бандла.\\n\\n#|\\n|||Bundle size Minified|Bundle size Minified + Gzipped||\\n||@antv/g6 [bundlephobia](https://bundlephobia.com/package/@antv/g6@5.0.49){target=\\\"_blank\\\"}|1.1 MB|324.5\\_kB||\\n||react flow [bundlephobia](https://bundlephobia.com/package/@xyflow/react@12.8.1){target=\\\"_blank\\\"}|181.2\\_kB|56.4\\_kB||\\n||@gravity-ui/graph [bundlephobia](https://bundlephobia.com/package/@gravity-ui/graph){target=\\\"_blank\\\"}|2.2\\_kB|672\\_B||\\n|#\\n\\nХотя обе библиотеки довольно мощные по производительности или по удобству интеграции, @gravity‑ui/graph обладает рядом преимуществ — библиотека способна обеспечить производительность на действительно больших графах и сохранить UI/UX для пользователя и упростить разработку.\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Планы на будущее{#future}\n\n\n Уже сейчас библиотека имеет достаточный запас производительности для\n большинства задач, поэтому в ближайшее время мы больше внимания будем\n уделять развитию экосистемы вокруг библиотеки — разрабатывать слои\n (плагины), интеграции для других библиотек и фреймворков\n (Angular/Vue/Svelte, …etc), добавим поддержку touch‑девайсов,\n адаптацию для мобильных браузеров и в целом улучшим UX/DX.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Попробуйте и присоединяйтесь{#try}\n\n\n В [репозитории](https://github.com/gravity-ui/graph){target=\"_blank\"}\n вы найдёте полностью рабочую библиотеку:\n\n * Ядро на Canvas + R‑Tree (≈ 30K строк кода),\n * React‑интеграцию,\n * Storybook с примерами.\n\n Установить библиотеку можно в одну строку:\n\n\n `npm install @gravity-ui/graph`\n\n\n --------------\n\n\n Довольно долго библиотека, которая сейчас зовётся @gravity‑ui/graph,\n была внутренним инструментом внутри Нирваны, и выбранный подход хорошо\n себя зарекомендовал. Сейчас нам хочется поделиться нашими разработками\n и помочь разработчикам снаружи рисовать свои графы проще, быстрее и\n производительнее.\n\n\n Мы хотим стандартизировать подходы к отображению сложных графов в\n опенсорс‑сообществе — слишком много команд изобретают велосипед или\n мучаются с неподходящими инструментами.\n\n\n Поэтому нам очень важно собрать ваш фидбек — разные проекты приносят\n разные edge‑случаи, которые позволяют развивать библиотеку. Это\n поможет нам доработать библиотеку и быстрее растить экосистему Gravity\n UI.\n - type: blog-layout-block\n resetPaddings: true\n fullWidth: false\n children:\n - type: blog-meta-block\n column: left\n resetPaddings: true\n - type: blog-suggest-block\n resetPaddings: true\n", + "title": "", + "noIndex": false, + "shareTitle": null, + "shareDescription": null, + "shareImage": null, + "pageLocaleId": null, + "author": "timofeyevvv", + "metaDescription": null, + "keywords": [], + "shareGenTitle": null, + "canonicalLink": null, + "sharingType": "auto", + "sharingTheme": "light", + "comment": "initial", + "shareImageUrl": "https://storage.cloud-preprod.yandex.net/ui-api-ru-preprod-stable-share-generator-screenshots/cache/292d9f3e0a443a096ee408a6f28fc6fec674eb78.png", + "pageRegionId": 75, + "summary": null, + "versionId": 199, + "service": null, + "solution": null, + "locales": [ + { + "id": 75, + "pageId": 71, + "locale": "ru", + "createdAt": "2026-01-15T11:26:48.440Z", + "updatedAt": "2026-01-15T11:26:48.519Z", + "publishedVersionId": null, + "lastVersionId": 195 + }, + { + "id": 76, + "pageId": 71, + "locale": "en", + "createdAt": "2026-01-15T11:26:48.532Z", + "updatedAt": "2026-01-15T11:26:48.609Z", + "publishedVersionId": null, + "lastVersionId": 196 + } + ], + "pageRegions": [ + { + "regionCode": "en", + "publishedVersionId": null + }, + { + "regionCode": "ru-ru", + "publishedVersionId": 199 + } + ], + "searchCategory": { + "id": 7, + "slug": "blog", + "title": "Блог", + "url": "/blog" + }, + "voiceovers": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/ru/pages/md-editor-in-gravity-ui.json b/src/api/.mocks/ru/pages/md-editor-in-gravity-ui.json new file mode 100644 index 000000000000..625f6dbd4528 --- /dev/null +++ b/src/api/.mocks/ru/pages/md-editor-in-gravity-ui.json @@ -0,0 +1,74 @@ +{ + "id": 73, + "name": "blog/md-editor-in-gravity-ui", + "createdAt": "2026-01-15T12:40:07.363Z", + "updatedAt": "2026-01-15T12:40:07.363Z", + "type": "default", + "isDeleted": false, + "versionOnTranslationId": null, + "searchCategorySlug": "blog", + "regions": [], + "pageId": 73, + "regionCode": "ru-ru", + "publishedVersionId": 214, + "lastVersionId": 214, + "content": "blocks:\n - type: blog-header-block\n resetPaddings: true\n paddingBottom: l\n width: m\n verticalOffset: m\n theme: dark\n background:\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png\n disableCompress: true\n color: '#2A1A2A'\n fullWidth: false\n - type: blog-layout-block\n resetPaddings: true\n mobileOrder: reverse\n children:\n - type: blog-yfm-block\n resetPaddings: true\n column: right\n text: >\n\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/speaker.jpg\n =80x)\n\n\n **Сергей Махнаткин**\n\n Разработчик интерфейсов\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: \"\\nВ этой статье:\\n\\n - [Зачем нам свой Markdown Editor](#why)\\n - [Возможности Markdown Editor в Gravity\\_UI](#capabilities)\\n - [Архитектура](#architecture)\\n - [Интеграция](#integration)\\n - [Интеграция самописных расширений](#extensions)\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic1.png\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n{% cut \\\"TL;DR\\\" %}\\n\\n* Позволяет работать одновременно в режиме [WYSIWYG](https://ru.wikipedia.org/wiki/WYSIWYG){target=\\\"_blank\\\"} и [markdown](https://en.wikipedia.org/wiki/Markdown){target=\\\"_blank\\\"} markup (с превью и cплитом).\\n* Поддерживает большое количество блоков из коробки.\\n* Позволяет дополнять функциональность — в режиме WYSIWYG есть extension system.\\n* Создан для работы в React‑приложениях.\\n* Использует темизацию и компоненты из [Gravity\\_UI](https://gravity-ui.com/){target=\\\"_blank\\\"}.\\n* Полностью построен на опенсорс‑технологиях ([ProseMirror](https://prosemirror.net/){target=\\\"_blank\\\"}, [CodeMirror](https://codemirror.net/){target=\\\"_blank\\\"}, [markdown‑it](https://github.com/markdown-it/markdown-it){target=\\\"_blank\\\"}, [Diplodoc](https://diplodoc.com/){target=\\\"_blank\\\"}, [Gravity\\_UI](https://gravity-ui.com/){target=\\\"_blank\\\"}).\\n* Соответствует стандарту [CommonMark](https://spec.commonmark.org/){target=\\\"_blank\\\"}, поддерживает стандартный язык markdown и [Yandex Flavored Markdown (YFM)](https://diplodoc.com/docs/ru/index-yfm){target=\\\"_blank\\\"}.\\n\\n{% endcut %}\\n\\nПривет! Меня зовут Сергей Махнаткин, я работаю разработчиком в отделе User Experience в Yandex\\_Cloud. В прошлом году мы писали о нашей [дизайн‑системе и библиотеке компонентов Gravity\\_UI](https://habr.com/ru/companies/yandex/articles/773870/){target=\\\"_blank\\\"}. С тех пор система не раз обновлялась и обрастала новыми функциями, и сегодня я хочу рассказать о новом инструменте — Markdown Editor, который значительно упрощает процесс работы с документацией.\\n\\nПоговорим об истории создания пользовательского интерфейса, архитектурных особенностях и технических деталях интеграции и разработки собственных расширений, а потом — почему всё это доступно в опенсорсе.\\n\\nКстати, попробовать инструмент можно здесь:\\n\\n * [Демо](https://gravity-ui.com/libraries/markdown-editor/playground){target=\\\"_blank\\\"} \\n * [Гитхаб](https://github.com/gravity-ui/markdown-editor/){target=\\\"_blank\\\"} \\n * [Storybook](https://preview.gravity-ui.com/md-editor/){target=\\\"_blank\\\"}\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Зачем нам свой Markdown Editor{#why}\n\n\n Чтобы было удобно хранить и структурировать корпоративную информацию,\n мы разработали платформу\n [Wiki](https://wiki.yandex.ru/){target=\"_blank\"}, которая позволяет\n создавать базы знаний. Кроме базы знаний, мы развивали подходы к\n документации, такие как Docs as Code, где документация и код живут бок\n о бок в файловом хранилище (.md‑файлы). Так появилась платформа\n [Diplodoс](https://diplodoc.com/){target=\"_blank\"}.\n\n\n Wiki и Diplodoc объединяет то, что обе платформы работают с диалектом\n markdown — Yandex Flavored Markdown (YFM), который используется в\n Nebius, Bitrix, DoubleCloud, Mappable, Meteum.\n\n\n Со временем мы заметили, что есть две группы пользователей, которые\n по‑разному представляют себе процесс создания и редактирования текста.\n Одни предпочитают сразу видеть финальный результат, работая с текстом,\n как в MS Word, Confluence или Notion. Другие доверяют только разметке\n и предпочитают оформлять страницы с помощью markdown. Известных\n библиотек, которые работают одновременно в режиме WYSIWYG/markdown, мы\n не нашли. Например, Notion — это только WYSIWYG, а в редакторах кода\n есть только markdown и режим превью.\n\n\n Мы разработали markdown‑редактор, который может работать одновременно\n в двух режимах: визуальном (WYSIWYG) и режиме разметки (markdown). В\n первом режиме разметить текст помогают значки на панели, а во втором\n пользователи могут вручную редактировать markdown‑код. Кроме того,\n наше решение сохраняет документ как md‑файл, независимо от того, какой\n режим использовался при его создании.\n\n\n Так выглядит визуальный редактор, в котором текст можно форматировать\n с помощью кнопок:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic2.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n А так — режим разметки, в котором элементы форматирования обозначаются\n с помощью специальных символов:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic3.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Возможности Markdown Editor в Gravity\\_UI{#capabilities}\\n\\nРедактор соответствует стандарту CommonMark, поддерживает стандартный язык markdown и язык YFM. Также мы добавили возможность расширять синтаксис и другими диалектами markdown, например Github Flavored Markdown. При этом редактор позволяет переключаться из режима markup в режим WYSIWYG, а сам документ будет храниться как разметка md или расширенный md (например, в случае с YFM).\\n\\n### Расширения\\n\\nВ редактор изначально вшито много расширений и настроек. Например, диаграммы Mermaid и блоки HTML:\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic4.png\n fullscreen: true\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic5.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Мы старались сделать ядро редактора легкорасширяемым. Разработчики\n могут создать собственное расширение или дополнительную\n функциональность, которые помогут:\n\n * добавить новые сущности — блоки или текстовые модификаторы;\n * дополнительно конфигурировать парсер markdown;\n * добавить actions, которые позволяют работать с редактором извне;\n * обогатить функциональность интерфейса, например показывать меню доступных команд при вводе слеша;\n * модифицировать текущее поведение, например вставлять изображения и файлы и загружать их в хранилище.\n\n Вот ряд примеров таких расширений, который мы разработали для нашей\n Wiki:\n\n * коллаборативный режим редактирования;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic6.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * блок диаграмм [draw.io](http://draw.io/){target=\"_blank\"};\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic7.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * плагин [YandexGPT](https://ya.ru/ai/gpt-3){target=\"_blank\"};\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic8.png\n fullscreen: true\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic9.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * инклюды;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic10.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * структура раздела;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic11.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * секции для создания удобной сетки;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic12.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * режим markdown с превью;\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic13.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: |\n\n * и множество других.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic14.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Разметка может преобразовываться автоматически. Если вы предпочитаете\n работать без мыши, в режиме визуального редактора предусмотрены\n специальные символы, позволяющие применять разметку прямо в тексте.\n Например, `**` переводят текст в жирное начертание в режиме WYSIWYG. С\n помощью этих символов можно форматировать текст, создавать инлайновый\n и блочный код.\n\n\n Также можно вызывать меню расширений, введя символ `/`.\n - type: blog-media-block\n column: left\n resetPaddings: true\n paddingBottom: s\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic15.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ### Пресеты\n\n\n Редактор позволяет сконфигурировать панель с инструментами для каждого\n проекта отдельно, но поставляется он с рядом готовых конфигураций —\n пресетов.\n\n\n Редактор без пресетов:\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic16.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n [Пресет\n CommonMark](https://preview.gravity-ui.com/md-editor/?path=/story/markdown-editor-presets--common-mark){target=\"_blank\"}\n обеспечивает поддержку стандартных элементов markdown: жирного шрифта,\n курсива, заголовков, списков, ссылок, цитат, блоков кода.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic17.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n В [пресете по\n умолчанию](https://preview.gravity-ui.com/md-editor/?path=/story/markdown-editor-presets--default){target=\"_blank\"}\n также появляется перечёркнутый текст, а ещё таблица, в ячейках которой\n может быть только текст. Такой пресет соответствует стандартному\n markdown‑it.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic18.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Как было сказано выше, редактор поддерживает и YFM, поэтому отлично\n интегрируется с Diplodoc. В [пресете\n YFM](https://preview.gravity-ui.com/md-editor/?path=/story/markdown-editor-presets--yfm){target=\"_blank\"}\n появляются дополнительные элементы: прокачанные таблицы, вставка\n файлов и изображений, чекбоксы, кат, табы, моноширинный шрифт.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic19.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n [Полный\n пресет](https://preview.gravity-ui.com/md-editor/?path=/story/markdown-editor-presets--full){target=\"_blank\"}\n содержит ещё больше элементов.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/md-editor-in-gravity-ui/pic20.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Архитектура{#architecture}\n\n\n В основе редактора в режиме WYSIWYG лежит известная библиотека\n [ProseMirror](http://prosemirror.net/){target=\"_blank\"}, а для\n разметки используется\n [CodeMirror](https://codemirror.net/){target=\"_blank\"}. ProseMirror\n поддерживает редактирование с форматированием, тогда как CodeMirror\n подходит для ситуаций, где необходимо работать с неразмеченным\n текстом.\n\n\n Мы выбрали именно эти библиотеки, потому что они разработаны одним\n автором, едины в архитектуре и подходах к реализации, поддерживаются\n большим комьюнити, используются во многих редакторах и хорошо\n оптимизированы для работы с текстом. Например, система транзакции для\n внесения изменений в документ, декорации для view, виртуализация DOM\n или поддержка синтаксиса множества языков программирования.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n ## Интеграция{#integration}\n\n\n Наш редактор легко подключается как [hook\n React](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-add-editor-with-create-react-app.md){target=\"_blank\"}:\n\n\n ```javascript\n\n\n import React from 'react';\n\n import {useMarkdownEditor, MarkdownEditorView} from\n '@gravity-ui/markdown-editor';\n\n import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18';\n\n\n function Editor({onSubmit}) {\n const editor = useMarkdownEditor({allowHTML: false});\n\n React.useEffect(() => {\n function submitHandler() {\n // Serialize current content to markdown markup\n const value = editor.getValue();\n onSubmit(value);\n }\n\n editor.on('submit', submitHandler);\n return () => {\n editor.off('submit', submitHandler);\n };\n }, [onSubmit]);\n\n return ;\n }\n\n ```\n\n Мы используем компоненты из библиотеки uikit GravityUI. Это\n гарантирует, что весь интерфейс будет консистентным и соответствовать\n единым стилевым гайдам. Использование этих компонентов также\n обеспечивает высокую степень согласованности и узнаваемости для\n пользователей, что делает работу с редактором ещё удобнее.\n\n\n У нас есть подробные\n [инструкции](https://github.com/gravity-ui/markdown-editor/blob/main/README.md#getting-started){target=\"_blank\"},\n как подключить редактор в приложение React, а также о том, как\n подключить разного рода расширения: например,\n [YandexGPT](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-gpt-extensions.md){target=\"_blank\"},\n [Mermaid](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-mermaid-extension.md){target=\"_blank\"}\n или\n [LaTeX](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-latex-extension.md){target=\"_blank\"}.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Интеграция самописных расширений{#extensions}\\n\\nВ редактор уже интегрирован ряд дополнительных расширений. Но если этого мало, то разработчики могут добавлять свои расширения в WYSIWYG‑режим редактора. О том, что могут дать расширения, мы говорили выше.\\n\\nЕсли вы хотите добавить новый блок или текстовый модификатор, сначала нужно сконфигурировать внутренний экземпляр markdown‑it c помощью метода configureMd. Затем следует добавить знание о новой сущности с помощью методов addNode или addMark, передав имя сущности и колбэк‑функцию, которая возвращает объект с тремя обязательными полями:\\n\\n```javascript\\nimport insPlugin from 'markdown-it-ins';\\nexport const underlineMarkName = 'ins';\\n\\nexport const UnderlineSpecs: ExtensionAuto = (builder) => {\\n builder\\n .configureMd((md) => md.use(insPlugin))\\n .addMark(underlineMarkName, () => ({\\n spec: {\\n parseDOM: [{tag: 'ins'}, {tag: 'u'}],\\n toDOM() {\\n return ['ins'];\\n },\\n },\\n toMd: {open: '++', close: '++', mixable: true, expelEnclosingWhitespace: true},\\n fromMd: {tokenSpec: {name: underlineMarkName, type: 'mark'}},\\n }));\\n};\\n```\\n\\n * `spec` — спецификация для ProseMirror;\\n * `fromMd` — конфигурация парсинга markdown‑разметки в представление внутри ProseMirror;\\n * `toMd` — конфигурация для сериализации сущности в markdown‑разметку.\\n\\nНапример, ниже конфигурация расширения для подчёркнутого текста. Он может быть расширен добавлением action с помощью метода `addAction`:\\n\\n```javascript\\nimport {toggleMark} from 'prosemirror-commands';\\n\\nconst undAction = 'underline';\\n\\nbuilder\\n .addAction(undAction, ({schema}) => ({\\n isActive: (state) => Boolean(isMarkActive(state, markType)),\\n isEnable: toggleMark(underlineType(schema)),\\n run: toggleMark(underlineType(schema)),\\n })\\n )\\n```\\n\\nТакой action может быть вызван в коде следующим образом:\\n\\n```javascript\\n// editor – инстанс редактора, полученный в результате вызова useMarkdownEditor\\neditor.actions.underline.run(),\\n```\\n\\nВ [документации](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-create-extension.md){target=\\\"_blank\\\"} можно посмотреть полную инструкцию по созданию нового расширения.\\n\\n---------------------\\n\\nМы постоянно расширяем горизонты использования нашего редактора: сейчас работаем над плагином для [VS Code](https://code.visualstudio.com/){target=\\\"_blank\\\"}, который позволит работать с md‑файлами в удобном WYSIWYG‑режиме прямо из редактора. Ещё мы планируем добавить полнофункциональный мобильный режим. Это позволит каждому пользователю работать в нашем редакторе, имея под рукой только мобильный телефон.\\n\\nНаш редактор не возник мгновенно: это результат накопления опыта и знаний. Мы гордимся, что редактор полностью базируется на опенсорс‑продуктах, включая надёжные и проверенные инструменты ProseMirror, CodeMirror, markdown‑it, а также наши собственные разработки — Diplodoc и Gravity\\_UI.\\n\\nВы всегда можете внести свой вклад в развитие редактора: [создать пул‑реквест](https://github.com/gravity-ui/markdown-editor/pulls){target=\\\"_blank\\\"} или помочь с решением текущих проблем, перечисленных в разделе [Issues](https://github.com/gravity-ui/markdown-editor/issues){target=\\\"_blank\\\"}. Ваша поддержка и свежий взгляд помогут нам сделать редактор лучше. А если вы считаете наш проект полезным — поставьте звезду на нашем [репозитории в GitHub](https://github.com/gravity-ui/markdown-editor){target=\\\"_blank\\\"}, это ценно :)\\n\"\n - type: blog-layout-block\n resetPaddings: true\n fullWidth: false\n children:\n - type: blog-meta-block\n column: left\n resetPaddings: true\n - type: blog-suggest-block\n resetPaddings: true\n", + "title": "", + "noIndex": false, + "shareTitle": null, + "shareDescription": null, + "shareImage": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "pageLocaleId": null, + "author": "timofeyevvv", + "metaDescription": null, + "keywords": [], + "shareGenTitle": null, + "canonicalLink": null, + "sharingType": "semi-full", + "sharingTheme": "dark", + "comment": "sharing pic", + "shareImageUrl": "https://storage.cloud-preprod.yandex.net/ui-api-ru-preprod-stable-share-generator-screenshots/cache/6a311e551d92070ebb8b4c9b6bf0dc6303aadb34.png", + "pageRegionId": 79, + "summary": null, + "versionId": 214, + "service": null, + "solution": null, + "locales": [ + { + "id": 79, + "pageId": 73, + "locale": "ru", + "createdAt": "2026-01-15T12:31:20.911Z", + "updatedAt": "2026-01-15T12:31:20.973Z", + "publishedVersionId": null, + "lastVersionId": 208 + }, + { + "id": 80, + "pageId": 73, + "locale": "en", + "createdAt": "2026-01-15T12:31:20.982Z", + "updatedAt": "2026-01-15T12:31:21.040Z", + "publishedVersionId": null, + "lastVersionId": 209 + } + ], + "pageRegions": [ + { + "regionCode": "en", + "publishedVersionId": 212 + }, + { + "regionCode": "ru-ru", + "publishedVersionId": 214 + } + ], + "searchCategory": { + "id": 7, + "slug": "blog", + "title": "Блог", + "url": "/blog" + }, + "voiceovers": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/ru/pages/yc-site-in-gravity-ui.json b/src/api/.mocks/ru/pages/yc-site-in-gravity-ui.json new file mode 100644 index 000000000000..6379c4e01b49 --- /dev/null +++ b/src/api/.mocks/ru/pages/yc-site-in-gravity-ui.json @@ -0,0 +1,74 @@ +{ + "id": 72, + "name": "blog/yc-site-in-gravity-ui", + "createdAt": "2026-01-15T12:23:53.501Z", + "updatedAt": "2026-01-15T12:23:53.501Z", + "type": "default", + "isDeleted": false, + "versionOnTranslationId": null, + "searchCategorySlug": "blog", + "regions": [], + "pageId": 72, + "regionCode": "ru-ru", + "publishedVersionId": 206, + "lastVersionId": 206, + "content": "blocks:\n - type: blog-header-block\n resetPaddings: true\n paddingBottom: l\n width: m\n verticalOffset: m\n theme: dark\n background:\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png\n disableCompress: true\n color: '#2A1A2A'\n fullWidth: false\n - type: blog-layout-block\n resetPaddings: true\n mobileOrder: reverse\n children:\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: \"\\n![image](https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/speaker.jpg =80x)\\n\\n**Владимир Тимофеев**\\nМенеджер технических проектов Yandex\\_Cloud\\n\"\n - type: blog-yfm-block\n column: right\n resetPaddings: true\n text: \"\\nВ этой статье:\\n\\n - [Всё началось с Gravity\\_UI](#start)\\n - [Доступность Yandex\\_Cloud](#availability)\\n - [Diplodoc](#diplodoc)\\n - [Результаты и планы](#results)\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nПривет, меня зовут Вова Тимофеев, я менеджер технических проектов Yandex\\_Cloud. В статье поделюсь тем, как мы делали сайт облачной платформы доступнее, сколько итераций прошли и какую роль в этом сыграл Gravity\\_UI.\\n\\nВ основе доступности всех сервисов — то, насколько хорошо они поддерживают работу с программами экранного доступа (Screen reader). Через эти программы пользователи с ограничениями воспринимают интерфейс и взаимодействуют с ним.\\n\\nСайты — не исключение. И нам предстояло выяснить, насколько доступен Yandex\\_Cloud для всех пользователей.\\n\\nВ Яндексе под доступностью мы подразумеваем то, что наши сервисы должны с комфортом использовать все, вне зависимости от временных или постоянных физических ограничений. Например, сейчас для незрячих пользователей адаптировано 16 сервисов Яндекса: Лавка, Go, Поиск, Браузер, Почта и другие. В работе над доступностью каждого сервиса помогает команда невизуального тестирования — и в кейсе, о котором расскажу в этой статье, без их помощи тоже не обошлось.\\n\\n**Спойлер**: в результате тестирования обнаружилось несколько спорных моментов при работе с программами экранного доступа, которые превратились в рабочие задачи.\\n\\nНо обо всём по порядку.\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Всё началось с Gravity\\_UI{#start}\\n\\nGravity\\_UI — дизайн‑система и библиотека компонентов, на которой работает сайт Yandex\\_Cloud и десятки других продуктов облака. Она выложена в [опенсорс](http://gravity-ui.com/){target=\\\"_blank\\\"} и доступна всем (нам приятно, что за последние полгода активность в community‑чате ощутимо выросла).\\n\\nЧто у нас есть:\\n\\n * набор базовых React‑компонентов;\\n * библиотека‑конструктор для лендингов;\\n * [подробные гайды](https://gravity-ui.com/design){target=\\\"_blank\\\"} от дизайнеров по использованию компонентов;\\n * библиотека в [Figma](https://www.figma.com/community/file/1271150067798118027/Gravity-UI-Design-System-(Beta)){target=\\\"_blank\\\"};\\n * набор из почти 600 готовых иконок;\\n * ChartKit — пакет для визуализации данных;\\n * Yagr — высокопроизводительный рендеринг графиков, основанный на uPlot;\\n * i18n — пакет для локализации интерфейса;\\n * другие [полезные библиотеки](https://gravity-ui.com/libraries){target=\\\"_blank\\\"}.\\n\\nВ марте 2024 года вышло обновление ключевой библиотеки — 6‑я версия UIKit. В ней обновился компонент List, появились поддержка параметра RTL во всех компонентах и пакет доработок a11y, улучшающих доступность.\\n\\n{% cut \\\"Что нового в 6‑й версии UIKit\\\" %}\\n\\n 1. **Компонент List\\_2.0**. В UIKit изначально был List, но в нём хотелось кое‑что доработать. При сборе запросов собрали список:\\n\\n - поддержка разных размеров и ширины;\\n - иконка у элемента списка, разное количество и положение иконок;\\n - поддержка стейтов;\\n - разный контент в элементах списка (однострочный, многострочный или список пользователей);\\n - поддержка разных видов разделителей и группировок.\\n\\n Это существенные изменения, поэтому мы создали List\\_2.0. Пока он выходит в версии prestable, но рекомендуем пользователям переходить на него и приносить фидбэк.\\n\\n 2. **RTL**. Если ваши приложения или сайты должны отображаться на иврите, арабском и других языках с направлением письма справа налево, нужна поддержка RTL‑стандартов. При этом в RTL:\\n\\n - вставленное слово на латинице пишется слева направо;\\n - цифры пишутся слева направо;\\n - знаки препинания в арабском также пишутся слева направо и т. д.\\n\\n Во всех компонентах мы поддержали параметр RTL. Чтобы под рукой был полный пример, сделали [промостраницу на арабском](https://gravity-ui.com/rtl){target=\\\"_blank\\\"}. Посмотреть, как это реализовано, можно в исходном коде [landing](https://github.com/gravity-ui/landing){target=\\\"_blank\\\"}. Также примеры есть в [storybook](https://preview.gravity-ui.com/uikit/?path=/story/components-inputs-button--default&globals=direction:rtl){target=\\\"_blank\\\"}.\\n\\n 3. **Доступность (a11y)**:\\n\\n - добавили в проект плагин [eslint](https://www.npmjs.com/package/eslint-plugin-jsx-a11y){target=\\\"_blank\\\"};\\n - поддержали клавиатуру для clickable и closable состояния компонента Persona;\\n - отключили onClick у 15 неинтерактивных компонент;\\n - поддержали клавиатуру в компоненте SelectionTable.\\n\\n{% endcut %}\\n\\n### Как пришли к улучшениям a11y\\n\\nВ этом помогла команда невизуального тестирования и её руководитель Анатолий Попко. На встрече Анатолий пошагово тестировал песочницу и [сайт](https://gravity-ui.com/){target=\\\"_blank\\\"} Gravity\\_UI, чтобы понять, какие проблемы с доступностью сейчас есть.\\n\\nПроверяли доступность компонентов, перемещаясь по сайту с помощью клавиатуры и специальных команд скринридеров.\\n\\nВизуально это выглядело так:\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic1.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n После встречи у команды появились рабочие задачи, а в GitHub — три\n новых issue по базовым компонентам.\n\n\n Подробнее о них.\n\n * В раскрывающемся списке второй уровень элементов [не открывается](https://github.com/gravity-ui/uikit/issues/1564){target=\"_blank\"} с помощью клавиатуры, только через клик мышкой.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic2.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n *\n [Непонятно](https://github.com/gravity-ui/uikit/issues/1563){target=\"_blank\"},\n какой элемент в маркированном списке markdown выбран в компоненте\n Select.\n\n\n *\n [Кнопки](https://github.com/gravity-ui/uikit/issues/1562){target=\"_blank\"}\n без текстовых меток, обозначенные только графически, воспроизводятся\n как просто «кнопка» или «радиокнопка». Этот баг встречается только на\n лендинге, сам компонент поддерживает aria‑label, но мы им не\n воспользовались.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic3.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n В результате мы поняли, что, раз мы не нашли десятки замечаний,\n доступность библиотеки уже на неплохом уровне. Тестировать компоненты\n не в реальном контексте очень тяжело, из‑за этого мы решили начать\n проверку доступности готового продукта. Так нам удалось найти\n дополнительные улучшения a11y.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Доступность Yandex\\_Cloud{#availability}\\n\\nВдохновившись обновлением Gravity\\_UI, мы решили протестировать на доступность наши сервисы: начать с сайта Yandex\\_Cloud, а после распространить этот опыт и на другие интерфейсы.\\n\\nЕсть ряд стандартов, соблюдая которые можно добиться доступности. Но чтобы достоверно протестировать интерфейсы Yandex\\_Cloud и лучше понять, насколько наш сайт удобен для всех, мы провели аудит.\\n\\n### Аудит доступности \\n\\nМы снова обратились к коллегам из команды невизуального тестирования и к Анатолию, чтобы вместе протестировать сайт, зафиксировать проблемы и забрать их на доработку. Всего было две итерации с разницей почти в месяц — тестирование и ретест.\\n\\nВ ходе тестирования зафиксировали целый пул правок, которые предстояло взять в работу.\\n\\n#### На главной\\n\\n * Контрол поиска нуждался в переработке. В нашем интерфейсе компонент поиска реализован в виде зоны поиска с иконкой лупы. По клику на них открывается поле для ввода запроса. В прошлой реализации для скринридера это были независимые элементы, что путало незрячих пользователей.\\n\\n ![image](https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic4.png)\\n\\n * В выборе языка мы использовали атрибут состояния «свёрнуто», хотя по сути это была кнопка выбора, а не выпадающий список. Не было подписи «язык», не хватало пробела в переходе «язык — регион».\\n * В меню аккаунта не запирался фокус: при активации пользователь выходил за пределы пунктов меню.\\n * Тег main на главной дублировался, поэтому предстояло один убрать.\\n * Раздел с примерами: необходимо использовать вкладку вместо кнопки.\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic5.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n paddingBottom: xs\n text: >\n\n * В карточке примера текст был убран в aria‑describedby, из‑за этого\n скринридер зачитывал текст однократно, что неудобно при изучении\n важной информации. Когда мы исследовали баг, мы поняли, что стоит\n сделать комплексный рефакторинг компонента Card и завели issue, в\n котором можно узнать детали изменений и поучаствовать в их обсуждении.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n#### Навигация\\n\\n * Проблема с фокусом: при развороте верхнего уровня меню нужно перемещать фокус в подменю.\\n * Было необходимо убрать TabIndex у верхнего уровня меню. Текущий вид приводил к двукратному перечислению всех пунктов меню.\\n * Нужно было запереть клавиатурный фокус в навигации, если она развёрнута. Иначе можно вылететь из меню на саму страницу, а после не вернуться назад.\\n\\n#### Footer\\n\\n * Yandex\\_Cloud обёрнут списком. Заголовки списков ссылок в футере были завёрнуты в элемент списка, поэтому NVDA определял их как часть и дважды зачитывал один и тот же список пользователю.\\n * Не хватало подписей для ссылок на AppStore, Google Play. В момент теста зачитывались фрагменты URL, из‑за этого пользователю ничего не было понятно.\\n\\n#### Раздел «Блог»\\n\\n * Кнопки «Все темы» и «Все сервисы» не были соотнесены с кнопкой. Кнопки селектов не озвучивали их содержание.\\n * Нужна была поддержка доступности в списках: она заключается в том, чтобы при открытии выпадающего списка фокус программ экранного доступа переключался на элементы этого списка. Дополнительно он должен поддерживать упрощённое перемещение между ними с помощью обычных стрелок — без использования каких‑либо сочетаний клавиш.\\n\\n#### Статьи блога\\n\\n * В хлебных крошках не хватало элемента «Вы здесь».\\n * Нужно было запереть фокус в диалог.\\n * Не было названия у счётчика избранного. Счётчик представлял собой кнопку с иконкой и числом. Программы экранного доступа зачитывали число — без иконки не было понятно, что делает кнопка.\\n\\nВсе задачи мы собрали в эпик и приступили к работе. К части задач были созданы issue на GitHub.\\n\\nО самых интересных кейсах расскажу подробнее.\\n\\n### Компонент Select\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic6.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n При тестировании мы обнаружили, что при клавиатурной навигации\n названия элементов списка не объявляются скринридером. Частично\n проблему удалось пофиксить с помощью атрибута aria‑activedescendant,\n но не всё. \n\n\n #### Вот какие проблемы сохранились\n\n * В Safari этот способ не работает, и пока нет понимания, как это решить.\n * Не всегда поддерживается фильтр поиска (на самом деле всегда, просто его поддержка не реализована). Обычно aria‑activedescendant навешивается на главный элемент выпадающего списка и указывает на выбранный в списке элемент. Сейчас он вешается на кнопку, открывающую выпадающий список. При нажатии стрелок вверх или вниз мы меняем у кнопки значение aria‑activedescendant на предыдущий или следующий элемент списка. Так программа экранного доступа может прочитать с выбранной кнопки, какой элемент сейчас выбран.\n \n Проблема с фильтром поиска заключается в том, что на его поле ввода нет атрибута aria‑activedescendant. Если человек сфокусировался на поле ввода фильтра и хочет туда что‑то ввести, программа экранного доступа не может с него считать, какой элемент списка активный, и навигация по списку стрелками не работает.\n\n * Выбранные опции не отмечены, необходимо добавить на них атрибут, например aria‑selected.\n\n Issue с актуальными проблемами можно посмотреть\n [тут](https://github.com/gravity-ui/uikit/issues/1760){target=\"_blank\"}.\n\n\n ### Хлебные крошки \n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic7.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Хлебные крошки (Breadcrumbs) — это элемент навигации, который\n показывает путь пользователя по сайту. В нашем случае скринридер\n зачитывал весь путь, и было невозможно понять, в какой части сайта\n человек находится. Кроме того, весь компонент представлялся просто как\n набор ссылок.\n\n\n Мы решили отойти от стандартов и решить эту задачу простым и\n оптимальным способом — повесив для скринридера тег «Вы здесь». В\n конечном итоге мы совместили стандартный подход и тег.\n\n\n В процессе работы выяснилось: чтобы эта подпись зачитывалась, нужно\n навесить её на элемент, который программы экранного доступа не\n игнорируют. Проще было навесить подпись на конвенциональную структуру,\n чем придумывать способ её избежать. Тем не менее подпись всё ещё\n полезна: она помогает пользователю быстрее понять, что он сейчас\n слышит именно хлебные крошки.\n\n\n ### Изображения без подписи\n\n Некоторые изображения на сайте были расположены без подписей к ним, и\n программа экранного доступа зачитывала их как «Изображение». Эта\n информация не несёт для пользователя полезной информации и не даёт\n представления об интерфейсе, поэтому мы решили прятать изображения без\n подписей от скринридера.\n\n\n ### Порядок текста в блоках\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic8.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n В нескольких местах на нашем сайте информация представлена в виде\n единого блока, например карточка мероприятия или карточка статьи в\n блоге.\n\n\n Мы заметили, что скринридеры зачитывают информацию в порядке, которому\n не следуют зрячие пользователи. Для карточки мероприятия программа\n экранного доступа зачитывала данные так: статус регистрации, время,\n место и только потом заголовок и описание мероприятия.\n\n\n Обычно пользователь не следует линейному пути: он смотрит на\n заголовок, потом — на подзаголовок и саму картинку. Проблема в том,\n что скринридер читает элементы в том порядке, в котором они\n расположены в DOM‑дереве страницы. Чтобы поправить порядок, нам\n пришлось его пересобрать.\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic9.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Мы были удивлены, но в некоторых связках «операционная система —\n браузер» это не сработало. Например, в Mozilla Firefox на macOS\n проблема сохранилась, несмотря на изменение порядка в дереве страницы.\n Будем надеяться, что разработчики Firefox исправят такое поведение в\n новых версиях браузера.\n\n\n ### Модальные окна \n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic10.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: >\n\n Когда зрячий пользователь открывает всплывающее окно в интерфейсе, его\n взгляд падает на то, что внутри этого окна. Однако ему всё ещё\n доступен контент всего сайта, и при необходимости он может вернуть\n своё внимание к нему.\n\n\n Когда работа с сайтом идёт через скринридер, ситуация для пользователя\n меняется. Если всплывающее окно не сделать модальным, то у него не\n будет границ. Как следствие, навигация по сайту станет сложнее:\n человек может выйти из всплывающего окна по ошибке, используя\n навигацию по элементам.\n\n\n При работе с модальными окнами есть стандарт, и, если следовать ему,\n запирать все всплывающие окна в интерфейсе необязательно.\n\n\n Но в процессе тестирования мы пришли к выводу, что делать всплывающие\n окна модальными — хорошая практика, упрощающая работу с сайтом при\n использовании скринридера.\n\n\n Мы решили сделать все всплывающие окна модальными. При этом мы\n оставили пользователю возможность настроить альтернативный сценарий\n работы с такими окнами.\n\n\n Вы можете настроить такой сценарий, назначив `role=\"dialog\",\n aria-modal=\"true\"`. В связке VoiceOver + Firefox это решение не\n поддерживается: подробности есть в закрытом\n [issue](https://github.com/gravity-ui/uikit/issues/1746){target=\"_blank\"}.\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Diplodoc{#diplodoc}\\n\\nЗаразившись нашей активностью, разработчики платформы для создания технической документации [Diplodoc](https://diplodoc.com/){target=\\\"_blank\\\"} сделали ряд a11y‑доработок в своём продукте:\\n\\n * Проверили все дополнительные элементы синтаксиса YFM (Yandex Flavored Markdown) и переписали так, чтобы они были видны для скринридеров.\\n * Улучшили флоу работы с документацией через клавиатуру: исправили порядок выбора элементов и добавили возможность выбора всем интерактивным элементам.\\n * Добавили корректные подписи и обозначения для элементов интерфейса, что значительно упростит использование сервиса для пользователей со скринридерами.\\n\\nПоскольку мы используем Diplodoc для отображения своей документации, эти доработки также повысили доступность документации на сайте Yandex\\_Cloud.\\n\"\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\n## Результаты и планы{#results}\\n\\nМы сделали первые шаги по улучшению доступности интерфейсов Yandex\\_Cloud для всех пользователей. Нам предстоит перенести опыт на другие интерфейсы сервиса и доработать те проблемы, о которых знаем сейчас.\\n\\nGravity\\_UI облегчил нашу работу над доступностью, и мы продолжаем дорабатывать открытые issue с тегом a11y. На момент написания статьи у нас 14 открытых и 24 закрытых issue, вы можете посмотреть на них [тут](https://github.com/gravity-ui/uikit/issues?q=is%3Aissue+is%3Aopen+label%3Aa11y){target=\\\"_blank\\\"}.\\n\"\n - type: blog-media-block\n column: left\n resetPaddings: true\n text: ''\n image:\n src: >-\n https://storage.yandexcloud.net/gravity-landing-static/blog/yc-site-in-gravity-ui/pic11.png\n fullscreen: true\n - type: blog-yfm-block\n column: left\n resetPaddings: true\n text: \"\\nЖдём ваших PR и комментариев о доступности и работе сервисов, а также примеров использования Gravity\\_UI на ваших сайтах.\\n\"\n - type: blog-layout-block\n resetPaddings: true\n fullWidth: false\n children:\n - type: blog-meta-block\n column: left\n resetPaddings: true\n - type: blog-suggest-block\n resetPaddings: true\n", + "title": "", + "noIndex": false, + "shareTitle": null, + "shareDescription": null, + "shareImage": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "pageLocaleId": null, + "author": "timofeyevvv", + "metaDescription": null, + "keywords": [], + "shareGenTitle": "", + "canonicalLink": null, + "sharingType": "semi-full", + "sharingTheme": "dark", + "comment": "initial", + "shareImageUrl": "https://storage.cloud-preprod.yandex.net/ui-api-ru-preprod-stable-share-generator-screenshots/cache/9e1d978a4a2158346f63727e7f846fb8d47f15cf.png", + "pageRegionId": 77, + "summary": null, + "versionId": 206, + "service": null, + "solution": null, + "locales": [ + { + "id": 77, + "pageId": 72, + "locale": "ru", + "createdAt": "2026-01-15T12:21:17.154Z", + "updatedAt": "2026-01-15T12:21:17.224Z", + "publishedVersionId": null, + "lastVersionId": 202 + }, + { + "id": 78, + "pageId": 72, + "locale": "en", + "createdAt": "2026-01-15T12:21:17.234Z", + "updatedAt": "2026-01-15T12:21:17.297Z", + "publishedVersionId": null, + "lastVersionId": 203 + } + ], + "pageRegions": [ + { + "regionCode": "en", + "publishedVersionId": 207 + }, + { + "regionCode": "ru-ru", + "publishedVersionId": 206 + } + ], + "searchCategory": { + "id": 7, + "slug": "blog", + "title": "Блог", + "url": "/blog" + }, + "voiceovers": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/ru/posts/graph.json b/src/api/.mocks/ru/posts/graph.json new file mode 100644 index 000000000000..6c398c13e6d3 --- /dev/null +++ b/src/api/.mocks/ru/posts/graph.json @@ -0,0 +1,35 @@ +{ + "id": 36, + "name": "graph", + "isPinned": false, + "postId": 36, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "readingTime": 17, + "title": "Библиотека визуализации графов: как мы решили дилемму Canvas vs HTML в Gravity UI", + "date": "2025-08-07T00:00:00Z", + "likes": 0, + "hasUserLike": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": "", + "slug": "articles", + "name": "Статьи" + } + ], + "authors": [ + { + "id": 1069, + "avatar": "https://storage.yandexcloud.net/cloud-www-assets/blog-assets/ru/posts/2025/12/gravity-ui-in-opensource/sizikov.png", + "firstName": "Алексей", + "secondName": "Сизиков", + "description": "Yandex Cloud", + "fullDescription": "Руководитель отдела User Experience, Yandex Cloud", + "shortDescription": "Руководитель отдела User Experience, Yandex Cloud", + "fullName": "Алексей Сизиков" + } + ], + "services": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/ru/posts/index.json b/src/api/.mocks/ru/posts/index.json index c962bbb73b23..7692f3aa34d6 100644 --- a/src/api/.mocks/ru/posts/index.json +++ b/src/api/.mocks/ru/posts/index.json @@ -1,13 +1,85 @@ { "posts": [ + { + "id": 36, + "name": "graph", + "date": "2025-08-07T00:00:00Z", + "title": "Библиотека визуализации графов: как мы решили дилемму Canvas vs HTML в Gravity UI", + "postId": 36, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/graph-cover-bg.png", + "readingTime": 17, + "likes": 0, + "hasUserLike": false, + "hasPublishedVersionInRegion": true, + "hasPublishedVersionInLocale": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": "", + "slug": "articles", + "name": "Статьи" + } + ], + "services": [], + "url": "/blog/graph" + }, + { + "id": 37, + "name": "yc-site-in-gravity-ui", + "date": "2024-10-28T00:00:00Z", + "title": "Как мы делали Yandex Cloud на дизайн‑системе Gravity UI доступнее", + "postId": 37, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "readingTime": 20, + "likes": 0, + "hasUserLike": false, + "hasPublishedVersionInRegion": true, + "hasPublishedVersionInLocale": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": "", + "slug": "articles", + "name": "Статьи" + } + ], + "services": [], + "url": "/blog/yc-site-in-gravity-ui" + }, + { + "id": 38, + "name": "md-editor-in-gravity-ui", + "date": "2024-10-01T00:00:00Z", + "title": "Markdown Editor: WYSIWYG и markup‑редактор на базе Gravity UI", + "postId": 38, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/mdeditor-cover-bg.png", + "readingTime": 7, + "likes": 0, + "hasUserLike": false, + "hasPublishedVersionInRegion": true, + "hasPublishedVersionInLocale": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": "", + "slug": "articles", + "name": "Статьи" + } + ], + "services": [], + "url": "/blog/md-editor-in-gravity-ui" + }, { "id": 35, "name": "gravity-ui-in-opensource", - "date": "2025-12-25T00:00:00Z", + "date": "2023-12-12T00:00:00Z", "title": "Дизайн-система Gravity UI: как легко построить свой интерфейс", "postId": 35, "description": "", - "image": "https://storage.yandexcloud.net/yandex-opensource/blog-cover-bg.png", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/gravity-ui-cover.png", "readingTime": 10, "likes": 0, "hasUserLike": false, @@ -16,8 +88,8 @@ "addLegacyPrefix": false, "tags": [ { - "slug": "articles", "icon": "", + "slug": "articles", "name": "Статьи" } ], @@ -25,7 +97,6 @@ "url": "/blog/gravity-ui-in-opensource" } ], - "totalCount": 1, - "count": 1, - "pinnedPost": null + "totalCount": 4, + "count": 4 } diff --git a/src/api/.mocks/ru/posts/md-editor-in-gravity-ui.json b/src/api/.mocks/ru/posts/md-editor-in-gravity-ui.json new file mode 100644 index 000000000000..15c76d79655b --- /dev/null +++ b/src/api/.mocks/ru/posts/md-editor-in-gravity-ui.json @@ -0,0 +1,24 @@ +{ + "id": 38, + "name": "md-editor-in-gravity-ui", + "isPinned": false, + "postId": 38, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "readingTime": 7, + "title": "Markdown Editor: WYSIWYG и markup‑редактор на базе Gravity UI", + "date": "2024-10-01T00:00:00Z", + "likes": 0, + "hasUserLike": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": "", + "slug": "articles", + "name": "Статьи" + } + ], + "authors": [], + "services": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/ru/posts/yc-site-in-gravity-ui.json b/src/api/.mocks/ru/posts/yc-site-in-gravity-ui.json new file mode 100644 index 000000000000..0fa675909f7d --- /dev/null +++ b/src/api/.mocks/ru/posts/yc-site-in-gravity-ui.json @@ -0,0 +1,24 @@ +{ + "id": 37, + "name": "yc-site-in-gravity-ui", + "isPinned": false, + "postId": 37, + "description": "", + "image": "https://storage.yandexcloud.net/gravity-landing-static/blog/blog-cover-bg.png", + "readingTime": 20, + "title": "Как мы делали Yandex Cloud на дизайн‑системе Gravity UI доступнее", + "date": "2024-10-28T00:00:00Z", + "likes": 0, + "hasUserLike": false, + "addLegacyPrefix": false, + "tags": [ + { + "icon": "", + "slug": "articles", + "name": "Статьи" + } + ], + "authors": [], + "services": [] + } + \ No newline at end of file diff --git a/src/api/.mocks/ru/tags.json b/src/api/.mocks/ru/tags.json index fe3e142affd9..d4ba7a907f02 100644 --- a/src/api/.mocks/ru/tags.json +++ b/src/api/.mocks/ru/tags.json @@ -4,7 +4,7 @@ "slug": "articles", "createdAt": "2020-03-13T11:00:57.360Z", "updatedAt": "2022-07-22T08:50:25.432Z", - "icon": "", + "icon": "", "isDeleted": false, "name": "Статьи", "locale": "ru", diff --git a/src/pages/blog/[...slug].tsx b/src/pages/blog/[...slug].tsx index 80c327067f6b..74d32a57326f 100644 --- a/src/pages/blog/[...slug].tsx +++ b/src/pages/blog/[...slug].tsx @@ -105,11 +105,11 @@ export default function BlogPostPage({ // Share options for the post const shareOptions = [ - ShareOptions.Twitter, - ShareOptions.Facebook, ShareOptions.Telegram, - ShareOptions.VK, + ShareOptions.Facebook, ShareOptions.LinkedIn, + ShareOptions.VK, + ShareOptions.Twitter, ]; // Breadcrumbs for navigation diff --git a/src/styles.scss b/src/styles.scss index 3efa1c2967c8..e898e6c11005 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -131,6 +131,114 @@ body.g-root, .pc-page-constructor { background-color: transparent; } + + // Стили для блоков кода в blog-constructor с цветами из prism-tomorrow + // stylelint-disable declaration-no-important + .yfm_blog { + // Фон блоков кода (аналогично prism-tomorrow) + pre { + background-color: #2d2d2d !important; + } + + pre > code { + background-color: #2d2d2d !important; + color: #ccc !important; // Базовый цвет текста + } + + // Подсветка синтаксиса highlight.js с цветами из prism-tomorrow + .hljs-subst { + color: #ccc !important; // Базовый цвет + } + + .hljs-comment { + color: #999 !important; // Комментарии + } + + .hljs-keyword, + .hljs-attribute, + .hljs-selector-tag, + .hljs-meta-keyword, + .hljs-doctag, + .hljs-name { + color: #cc99cd !important; // Ключевые слова + } + + .hljs-type, + .hljs-string, + .hljs-quote, + .hljs-template-tag { + color: #7ec699 !important; // Строки + } + + .hljs-number { + color: #f08d49 !important; // Числа + } + + .hljs-selector-id, + .hljs-selector-class { + color: #7ec699 !important; // Селекторы + } + + .hljs-deletion { + color: #e2777a !important; // Удалённое + } + + .hljs-title, + .hljs-section { + color: #e2777a !important; // Заголовки + } + + .hljs-regexp, + .hljs-symbol, + .hljs-variable, + .hljs-template-variable, + .hljs-link, + .hljs-selector-attr, + .hljs-selector-pseudo { + color: #67cdcc !important; // Переменные и операторы + } + + .hljs-literal { + color: #f08d49 !important; // Литералы (boolean, number) + } + + .hljs-built_in, + .hljs-bullet, + .hljs-code, + .hljs-addition { + color: #f8c555 !important; // Встроенные функции и константы + } + + .hljs-meta { + color: #cc99cd !important; // Мета-информация + } + + .hljs-meta-string { + color: #7ec699 !important; // Мета-строки + } + + // Дополнительные классы для лучшей совместимости + .hljs-function { + color: #f08d49 !important; // Функции + } + + .hljs-class { + color: #f8c555 !important; // Классы + } + + .hljs-tag { + color: #e2777a !important; // Теги + } + + .hljs-attr { + color: #e2777a !important; // Атрибуты + } + + .hljs-attr-value { + color: #7ec699 !important; // Значения атрибутов + } + } + // stylelint-enable declaration-no-important } .g-fun-gravity {