diff --git a/next.config.js b/next.config.js index e9c365f66d8d..df1a04ac8f95 100644 --- a/next.config.js +++ b/next.config.js @@ -15,6 +15,7 @@ const withTM = require('next-transpile-modules')([ '@gravity-ui/charts', '@gravity-ui/yagr', '@gravity-ui/markdown-editor', + '@gravity-ui/timeline', ]); const {i18n} = require('./next-i18next.config'); diff --git a/package-lock.json b/package-lock.json index e210f5a46bb9..9681e4aa4a8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@gravity-ui/markdown-editor": "^15.1.0", "@gravity-ui/navigation": "^3.11.0", "@gravity-ui/page-constructor": "^6.0.0-beta.6", + "@gravity-ui/timeline": "^1.25.1", "@gravity-ui/uikit": "^7.28.0", "@gravity-ui/uikit-themer": "^1.4.1", "@mdx-js/mdx": "^2.3.0", @@ -3596,6 +3597,33 @@ "stylelint": "^14.0.0" } }, + "node_modules/@gravity-ui/timeline": { + "version": "1.25.1", + "resolved": "https://registry.npmjs.org/@gravity-ui/timeline/-/timeline-1.25.1.tgz", + "integrity": "sha512-gOAjVdg6mn5Et6Cd6x/UOX5DP1aIIjyaX9o7Axcq8jaiZn/j4CU++kgVJaiU3CNcOBzgdnFQcy/nJp9a2iCKgQ==", + "dependencies": { + "dayjs": "^1.11.13", + "rbush": "^4.0.1" + } + }, + "node_modules/@gravity-ui/timeline/node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==" + }, + "node_modules/@gravity-ui/timeline/node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==" + }, + "node_modules/@gravity-ui/timeline/node_modules/rbush": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz", + "integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==", + "dependencies": { + "quickselect": "^3.0.0" + } + }, "node_modules/@gravity-ui/tsconfig": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@gravity-ui/tsconfig/-/tsconfig-1.0.0.tgz", diff --git a/package.json b/package.json index 5a683d17a83e..a458620c4e25 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@gravity-ui/markdown-editor": "^15.1.0", "@gravity-ui/navigation": "^3.11.0", "@gravity-ui/page-constructor": "^6.0.0-beta.6", + "@gravity-ui/timeline": "^1.25.1", "@gravity-ui/uikit": "^7.28.0", "@gravity-ui/uikit-themer": "^1.4.1", "@mdx-js/mdx": "^2.3.0", diff --git a/public/locales/de/timeline.json b/public/locales/de/timeline.json new file mode 100644 index 000000000000..df0117dc8fb6 --- /dev/null +++ b/public/locales/de/timeline.json @@ -0,0 +1,5 @@ +{ + "goToLibrary": "Zur Bibliothek wechseln", + "title": "Spielwiese" +} + diff --git a/public/locales/en/timeline.json b/public/locales/en/timeline.json new file mode 100644 index 000000000000..de6e4d44ea77 --- /dev/null +++ b/public/locales/en/timeline.json @@ -0,0 +1,5 @@ +{ + "goToLibrary": "Go to library", + "title": "Playground" +} + diff --git a/public/locales/es/timeline.json b/public/locales/es/timeline.json new file mode 100644 index 000000000000..3d2ec0ef2d90 --- /dev/null +++ b/public/locales/es/timeline.json @@ -0,0 +1,5 @@ +{ + "goToLibrary": "Ir a la biblioteca", + "title": "Playground" +} + diff --git a/public/locales/fr/timeline.json b/public/locales/fr/timeline.json new file mode 100644 index 000000000000..809ae4cac112 --- /dev/null +++ b/public/locales/fr/timeline.json @@ -0,0 +1,5 @@ +{ + "goToLibrary": "Aller à la bibliothèque", + "title": "Terrain de jeu" +} + diff --git a/public/locales/ko/timeline.json b/public/locales/ko/timeline.json new file mode 100644 index 000000000000..f65be70254ad --- /dev/null +++ b/public/locales/ko/timeline.json @@ -0,0 +1,5 @@ +{ + "goToLibrary": "라이브러리로 이동", + "title": "플레이그라운드" +} + diff --git a/public/locales/ru/timeline.json b/public/locales/ru/timeline.json new file mode 100644 index 000000000000..d50eb145d51a --- /dev/null +++ b/public/locales/ru/timeline.json @@ -0,0 +1,5 @@ +{ + "goToLibrary": "К библиотеке", + "title": "Редактор" +} + diff --git a/public/locales/zh/timeline.json b/public/locales/zh/timeline.json new file mode 100644 index 000000000000..9fd3cf38c308 --- /dev/null +++ b/public/locales/zh/timeline.json @@ -0,0 +1,5 @@ +{ + "goToLibrary": "前往库", + "title": "Playground" +} + diff --git a/src/components/GraphPlayground/GraphPlayground.scss b/src/components/GraphPlayground/GraphPlayground.scss index 6c8df134f6fa..d1a096c6a1bc 100644 --- a/src/components/GraphPlayground/GraphPlayground.scss +++ b/src/components/GraphPlayground/GraphPlayground.scss @@ -1,83 +1,10 @@ -@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; -@use '~@gravity-ui/uikit/styles/mixins' as ukitMixins; @use '../../variables.scss'; $block: '.#{variables.$ns}graph'; #{$block} { - min-height: 100%; - height: 100%; - margin-block-start: calc(var(--g-spacing-base) * 8); - - &__container { - min-height: 100%; - display: flex; - flex-direction: column; - } - - &__heading { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: pcVariables.$indentXS; - - @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { - margin-bottom: pcVariables.$indentXXXS; - } - } - - &__title { - font-size: 48px; - line-height: 56px; - font-weight: 600; - color: #fff; - margin: 0; - - @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { - font-size: 32px; - line-height: 48px; - margin-bottom: pcVariables.$indentXXXS; - } - } - - &__content { - width: 100%; - min-height: 300px; - } - - &__playground { - padding: 24px 32px; - background: rgba(37, 27, 37, 1); - border-radius: 24px; - flex: 1; - flex-direction: column; - min-height: 80vh; - max-height: 80vh; - gap: 24px; - position: relative; - } - &__graph-viewer { flex: 1; min-height: 100%; } - - &__json-switcher { - position: absolute; - top: 24px; - right: 32px; - z-index: 2; - } - - &__editor-wrap { - display: flex; - flex-direction: column; - gap: 24px; - } - - &__editor { - flex: 1; - border-radius: 24px; - border: 1px solid rgba(255, 255, 255, 0.2); - } } diff --git a/src/components/GraphPlayground/GraphPlayground.tsx b/src/components/GraphPlayground/GraphPlayground.tsx index 8495ee9c0a47..eeb8055430f0 100644 --- a/src/components/GraphPlayground/GraphPlayground.tsx +++ b/src/components/GraphPlayground/GraphPlayground.tsx @@ -1,38 +1,22 @@ -import {Col, Grid, Row} from '@gravity-ui/page-constructor'; -import {Button} from '@gravity-ui/uikit'; import {useTranslation} from 'next-i18next'; import {memo} from 'react'; import {block} from '../../utils'; +import {PlaygroundWrap} from '../PlaygroundWrap'; import './GraphPlayground.scss'; -import {GraphPlayground} from './Playground/GraphPlayground'; +import {GraphPlayground as GraphPlaygroundInner} from './Playground/GraphPlayground'; const b = block('graph'); -export const GraphPlayround = memo(() => { +export const GraphPlayground = memo(() => { const {t} = useTranslation('graph'); return ( - - - -

{t('title')}

-
- -
- -
- - - -
+ + + ); }); + +GraphPlayground.displayName = 'GraphPlayground'; diff --git a/src/components/PlaygroundWrap/PlaygroundWrap.scss b/src/components/PlaygroundWrap/PlaygroundWrap.scss new file mode 100644 index 000000000000..5ce928890711 --- /dev/null +++ b/src/components/PlaygroundWrap/PlaygroundWrap.scss @@ -0,0 +1,53 @@ +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '~@gravity-ui/uikit/styles/mixins' as ukitMixins; +@use '../../variables.scss'; + +$block: '.#{variables.$ns}playground-wrap'; + +#{$block} { + height: 100%; + margin-block-start: calc(var(--g-spacing-base) * 8); + + &__container { + height: 100%; + display: flex; + flex-direction: column; + } + + &__heading { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: pcVariables.$indentXS; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { + margin-bottom: pcVariables.$indentXXXS; + } + } + + &__title { + font-size: 48px; + line-height: 56px; + font-weight: 600; + color: #fff; + margin: 0; + + @media (max-width: map-get(pcVariables.$gridBreakpoints, 'md') - 1) { + font-size: 32px; + line-height: 48px; + margin-bottom: pcVariables.$indentXXXS; + } + } + + &__playground { + padding: 24px 32px; + background: rgba(37, 27, 37, 1); + border-radius: 24px; + flex: 1; + flex-direction: column; + min-height: 80vh; + max-height: 80vh; + gap: 24px; + position: relative; + } +} diff --git a/src/components/PlaygroundWrap/PlaygroundWrap.tsx b/src/components/PlaygroundWrap/PlaygroundWrap.tsx new file mode 100644 index 000000000000..9424f074357f --- /dev/null +++ b/src/components/PlaygroundWrap/PlaygroundWrap.tsx @@ -0,0 +1,44 @@ +import {Col, Grid, Row} from '@gravity-ui/page-constructor'; +import {Button} from '@gravity-ui/uikit'; +import React, {PropsWithChildren, memo} from 'react'; + +import {block} from '../../utils'; + +import './PlaygroundWrap.scss'; + +const b = block('playground-wrap'); + +type Props = { + title: string; + libraryId: string; + goToLibraryText?: string; +}; + +export const PlaygroundWrap = memo>( + ({title, libraryId, goToLibraryText, children}) => { + return ( + + + +

{title}

+
+ {goToLibraryText && ( + + )} +
+ +
+ {children} +
+ ); + }, +); + +PlaygroundWrap.displayName = 'PlaygroundWrap'; diff --git a/src/components/PlaygroundWrap/index.ts b/src/components/PlaygroundWrap/index.ts new file mode 100644 index 000000000000..f9d96c2d75e1 --- /dev/null +++ b/src/components/PlaygroundWrap/index.ts @@ -0,0 +1 @@ +export {PlaygroundWrap} from './PlaygroundWrap'; diff --git a/src/components/TimelinePlayground/Playground/Editor.scss b/src/components/TimelinePlayground/Playground/Editor.scss new file mode 100644 index 000000000000..b9f73b55dfa2 --- /dev/null +++ b/src/components/TimelinePlayground/Playground/Editor.scss @@ -0,0 +1,31 @@ +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '~@gravity-ui/uikit/styles/mixins' as ukitMixins; +@use '../../../variables.scss'; + +$block: '.#{variables.$ns}editor'; + +#{$block} { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + overflow: hidden; + + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + + &__monaco { + flex: 1; + height: 100%; + min-height: 0; + } + + &__actions { + border-top: 1px solid rgba(255, 255, 255, 0.2); + padding: var(--g-spacing-3); + + .g-button { + --_--border-radius: 8px; + } + } +} diff --git a/src/components/TimelinePlayground/Playground/Editor.tsx b/src/components/TimelinePlayground/Playground/Editor.tsx new file mode 100644 index 000000000000..8c17cc49292f --- /dev/null +++ b/src/components/TimelinePlayground/Playground/Editor.tsx @@ -0,0 +1,148 @@ +import { + Timeline as GravityTimeline, + TimeLineConfig, + TimelineEvent, + TimelineMarker, + TimelineSection, +} from '@gravity-ui/timeline'; +import {Button, Flex, Hotkey, Text} from '@gravity-ui/uikit'; +import {Editor as MonacoEditor, OnMount, OnValidate, useMonaco} from '@monaco-editor/react'; +import cloneDeep from 'lodash/cloneDeep'; +import {KeyCode, KeyMod} from 'monaco-editor'; +import React, {FC, useCallback, useEffect, useRef, useState} from 'react'; + +import {block} from '../../../utils'; + +import './Editor.scss'; +import {initialState} from './constants'; +import {defineTimelineConfigSchema} from './schema'; +import {GravityTheme, defineTheme} from './theme'; + +const b = block('editor'); + +type Props = { + timeline: GravityTimeline; +}; +type ExtractTypeFromArray = T extends Array ? E : never; + +export const Editor: FC = ({timeline}) => { + const monaco = useMonaco(); + const monacoRef = useRef[0]>(); + const valueRef = useRef(cloneDeep(initialState)); + const [errorMarker, setErrorMarker] = + useState[0]>>(); + + useEffect(() => { + if (monaco) { + defineTimelineConfigSchema(monaco); + } + }, [monaco]); + + const handleBeforeMount = (monacoInstance: typeof monaco) => { + if (monacoInstance) { + defineTheme(monacoInstance); + } + }; + + const handleApplyChanges = useCallback(() => { + const model = monacoRef.current?.getModel(); + if (!model) { + return; + } + try { + const { + settings: {start, end, axes, events, markers, sections}, + viewConfiguration, + }: TimeLineConfig = JSON.parse( + model.getValue(), + ); + + const currentConfig = timeline.settings; + + if (currentConfig.start !== start || currentConfig.end !== end) { + timeline.api.setRange(start, end); + } + + timeline.api.setAxes(axes); + timeline.api.setEvents(events); + timeline.api.setMarkers(markers || []); + timeline.api.setSections(sections || []); + timeline.api.setViewConfiguration(viewConfiguration || {}); + } catch (e) { + console.error(e); + } + }, [timeline]); + + const handleReset = useCallback(() => { + monacoRef.current?.setValue(JSON.stringify(initialState, null, 2)); + handleApplyChanges(); + }, [handleApplyChanges]); + + return ( +
+ { + monacoRef.current = editor; + monacoRef.current?.setValue(JSON.stringify(valueRef.current, null, 2)); + // eslint-disable-next-line no-bitwise + editor.addCommand(KeyMod.CtrlCmd | KeyCode.Enter, handleApplyChanges); + handleApplyChanges(); + }} + onValidate={(markers) => { + setErrorMarker(markers.filter((m) => m.severity === 8)[0] || null); + }} + language={'json'} + theme={GravityTheme} + options={{ + contextmenu: false, + lineNumbersMinChars: 4, + glyphMargin: false, + colorDecorators: true, + minimap: {enabled: false}, + smoothScrolling: true, + bracketPairColorization: {enabled: true}, + }} + /> + + + + {errorMarker && ( + + + { + monacoRef.current?.revealLinesInCenter( + errorMarker.startLineNumber, + errorMarker.endLineNumber, + 0, + ); + + monacoRef.current?.setSelection({ + startColumn: errorMarker.startColumn, + startLineNumber: errorMarker.startLineNumber, + endColumn: errorMarker.endColumn, + endLineNumber: errorMarker.endLineNumber, + }); + }} + > + {errorMarker.message} + + + + )} + +
+ ); +}; diff --git a/src/components/TimelinePlayground/Playground/Playground.scss b/src/components/TimelinePlayground/Playground/Playground.scss new file mode 100644 index 000000000000..0d3f2dbaf854 --- /dev/null +++ b/src/components/TimelinePlayground/Playground/Playground.scss @@ -0,0 +1,21 @@ +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '~@gravity-ui/uikit/styles/mixins' as ukitMixins; +@use '../../../variables.scss'; + +$pg: '.#{variables.$ns}timeline-playground'; + +#{$pg} { + height: 100%; + width: 100%; + position: relative; + display: flex; + justify-content: space-between; + gap: 32px; + + &__content { + height: 100%; + width: calc(50% - 32px / 2); + min-height: 0; + overflow: hidden; + } +} diff --git a/src/components/TimelinePlayground/Playground/Playground.tsx b/src/components/TimelinePlayground/Playground/Playground.tsx new file mode 100644 index 000000000000..25ac5a860ffd --- /dev/null +++ b/src/components/TimelinePlayground/Playground/Playground.tsx @@ -0,0 +1,50 @@ +import {ChevronsCollapseHorizontal} from '@gravity-ui/icons'; +import {useTimeline} from '@gravity-ui/timeline/react'; +import {Button, Flex, Icon, Text} from '@gravity-ui/uikit'; +import cloneDeep from 'lodash/cloneDeep'; +import React, {useEffect} from 'react'; + +import {block} from '../../../utils'; + +import {Editor} from './Editor'; +import './Playground.scss'; +import {Timeline} from './Timeline'; +import {initialState} from './constants'; +import {getTimelineItemsRange} from './helpers/getTimelineItemsRange'; + +const b = block('timeline-playground'); + +export const Playground = () => { + const {timeline} = useTimeline(cloneDeep(initialState)); + + useEffect(() => { + return () => { + if (timeline) { + timeline.destroy(); + } + }; + }, [timeline]); + + const handleCameraFocus = () => { + const {min, max} = getTimelineItemsRange(timeline); + timeline.api.setRange(min, max); + }; + + return ( +
+ + + Timeline + + + + + + JSON Editor + + +
+ ); +}; diff --git a/src/components/TimelinePlayground/Playground/Timeline.scss b/src/components/TimelinePlayground/Playground/Timeline.scss new file mode 100644 index 000000000000..0d7d2167d601 --- /dev/null +++ b/src/components/TimelinePlayground/Playground/Timeline.scss @@ -0,0 +1,10 @@ +@use '../../../variables.scss'; + +$block: '.#{variables.$ns}timeline-canvas-wrap'; + +#{$block} { + display: block; + position: relative; + width: 100%; + height: 100%; +} diff --git a/src/components/TimelinePlayground/Playground/Timeline.tsx b/src/components/TimelinePlayground/Playground/Timeline.tsx new file mode 100644 index 000000000000..73ebc05c6129 --- /dev/null +++ b/src/components/TimelinePlayground/Playground/Timeline.tsx @@ -0,0 +1,26 @@ +import { + Timeline as GravityTimeline, + TimelineEvent, + TimelineMarker, + TimelineSection, +} from '@gravity-ui/timeline'; +import {TimelineCanvas} from '@gravity-ui/timeline/react'; +import React, {FC} from 'react'; + +import {block} from '../../../utils'; + +import './Timeline.scss'; + +const b = block('timeline-canvas-wrap'); + +type Props = { + timeline: GravityTimeline; +}; + +export const Timeline: FC = ({timeline}) => { + return ( +
+ +
+ ); +}; diff --git a/src/components/TimelinePlayground/Playground/constants/index.ts b/src/components/TimelinePlayground/Playground/constants/index.ts new file mode 100644 index 000000000000..1a34152ddfa4 --- /dev/null +++ b/src/components/TimelinePlayground/Playground/constants/index.ts @@ -0,0 +1,123 @@ +import {TimeLineConfig, TimelineEvent, TimelineMarker, TimelineSection} from '@gravity-ui/timeline'; + +export const LINE_HEIGHT = 20; + +export const initialState: TimeLineConfig = { + settings: { + start: 1739537126347, + end: 1739537186347, + axes: [ + { + id: 'main', + tracksCount: 6, + top: 0, + height: LINE_HEIGHT, + }, + ], + events: [ + { + id: 'test2', + from: 1739537144007, + to: 1739537166347, + axisId: 'main', + trackIndex: 1, + color: 'rgb(161, 193, 129)', + }, + { + id: 'test3', + from: 1739537126347, + to: 1739537150000, + axisId: 'main', + trackIndex: 2, + color: 'rgb(254, 127, 45)', + }, + { + id: 'test4', + from: 1739537146347, + to: 1739537160000, + axisId: 'main', + trackIndex: 2, + color: 'rgb(45,181,254)', + }, + { + id: 'test5', + from: 1739537150000, + to: 1739537170000, + axisId: 'main', + trackIndex: 3, + color: 'rgb(87, 156, 135)', + }, + { + id: 'test6', + from: 1739537170000, + to: 1739537186347, + axisId: 'main', + trackIndex: 4, + color: 'rgb(11, 180, 193)', + }, + ], + sections: [ + { + id: 'planning-phase', + from: 1739537126347, + to: 1739537145000, + color: 'rgba(63, 81, 181, 0.2)', // Indigo - planning + hoverColor: 'rgba(63, 81, 181, 0.3)', + }, + { + id: 'development-phase', + from: 1739537145000, + to: 1739537172000, + color: 'rgba(33, 150, 243, 0.2)', // Blue - development + hoverColor: 'rgba(33, 150, 243, 0.3)', + }, + { + id: 'testing-phase', + from: 1739537172000, + to: 1739537182000, + color: 'rgba(255, 152, 0, 0.2)', // Orange - testing + hoverColor: 'rgba(255, 152, 0, 0.3)', + }, + { + id: 'deployment-phase', + from: 1739537182000, + // Extends to end + color: 'rgba(76, 175, 80, 0.2)', // Green - deployment + hoverColor: 'rgba(76, 175, 80, 0.3)', + }, + ], + markers: [ + { + time: 1739537145000, + color: 'rgb(63, 81, 181)', + activeColor: 'rgb(92, 107, 192)', + hoverColor: 'rgb(57, 73, 171)', + label: 'Dev Start', + }, + { + time: 1739537172000, + color: 'rgb(255, 152, 0)', + activeColor: 'rgb(255, 193, 7)', + hoverColor: 'rgb(255, 87, 34)', + label: 'Testing', + }, + { + time: 1739537182000, + color: 'rgb(76, 175, 80)', + activeColor: 'rgb(102, 187, 106)', + hoverColor: 'rgb(67, 160, 71)', + label: 'Deploy', + }, + ], + }, + viewConfiguration: { + ruler: { + color: { + primaryLevel: 'white', + secondaryLevel: 'white', + textOutlineColor: 'transparent', + }, + }, + hideRuler: false, + }, +}; diff --git a/src/components/TimelinePlayground/Playground/helpers/getTimelineItemsRange.ts b/src/components/TimelinePlayground/Playground/helpers/getTimelineItemsRange.ts new file mode 100644 index 000000000000..266b0e9c0964 --- /dev/null +++ b/src/components/TimelinePlayground/Playground/helpers/getTimelineItemsRange.ts @@ -0,0 +1,33 @@ +import {Timeline, TimelineEvent, TimelineMarker, TimelineSection} from '@gravity-ui/timeline'; + +export const getTimelineItemsRange = < + Event extends TimelineEvent, + Marker extends TimelineMarker, + Section extends TimelineSection, +>( + timeline: Timeline, +) => { + const {events, markers = [], sections = [], start, end} = timeline.settings; + const items = [...events, ...markers, ...sections]; + + if (!items.length) { + return {min: start, max: end}; + } + + return items.reduce( + (acc, item) => { + if ('from' in item) { + acc.min = Math.min(acc.min, item.from); + acc.max = Math.max(acc.max, item.to || item.from); + } + + if ('time' in item) { + acc.min = Math.min(acc.min, item.time); + acc.max = Math.max(acc.max, item.time); + } + + return acc; + }, + {min: Infinity, max: -Infinity}, + ); +}; diff --git a/src/components/TimelinePlayground/Playground/index.ts b/src/components/TimelinePlayground/Playground/index.ts new file mode 100644 index 000000000000..028480e77fa5 --- /dev/null +++ b/src/components/TimelinePlayground/Playground/index.ts @@ -0,0 +1 @@ +export {Playground} from './Playground'; diff --git a/src/components/TimelinePlayground/Playground/schema.ts b/src/components/TimelinePlayground/Playground/schema.ts new file mode 100644 index 000000000000..a5c719f2344e --- /dev/null +++ b/src/components/TimelinePlayground/Playground/schema.ts @@ -0,0 +1,190 @@ +import {Monaco} from '@monaco-editor/react'; + +export function defineTimelineConfigSchema(monaco: Monaco) { + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + schemaValidation: 'error', + schemas: [ + { + uri: 'http://gravity/timeline-playground/schema.json', + fileMatch: ['*'], + schema: { + type: 'object', + properties: { + settings: { + $ref: '#/definitions/TimelineSettings', + description: 'Timeline settings configuration', + }, + }, + required: ['settings'], + definitions: { + TimelineSettings: { + type: 'object', + properties: { + start: { + type: 'number', + description: + 'Start timestamp of the visible range (Unix timestamp in ms)', + }, + end: { + type: 'number', + description: + 'End timestamp of the visible range (Unix timestamp in ms)', + }, + axes: { + type: 'array', + items: { + $ref: '#/definitions/TimelineAxis', + }, + description: 'List of timeline axes', + }, + events: { + type: 'array', + items: { + $ref: '#/definitions/TimelineEvent', + }, + description: 'List of timeline events', + }, + markers: { + type: 'array', + items: { + $ref: '#/definitions/TimelineMarker', + }, + description: 'List of timeline markers (optional)', + }, + sections: { + type: 'array', + items: { + $ref: '#/definitions/TimelineSection', + }, + description: 'List of timeline sections (optional)', + }, + }, + required: ['start', 'end', 'axes', 'events'], + }, + TimelineAxis: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Unique identifier for the axis', + }, + tracksCount: { + type: 'number', + description: 'Number of tracks in this axis', + }, + top: { + type: 'number', + description: 'Top position offset', + }, + height: { + type: 'number', + description: 'Height of each track in the axis', + }, + }, + required: ['id'], + }, + TimelineEvent: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Unique identifier for the event', + }, + from: { + type: 'number', + description: + 'Start timestamp of the event (Unix timestamp in ms)', + }, + to: { + type: 'number', + description: + 'End timestamp of the event (Unix timestamp in ms)', + }, + axisId: { + type: 'string', + description: 'ID of the axis this event belongs to', + }, + trackIndex: { + type: 'number', + description: 'Index of the track within the axis', + }, + color: { + type: 'string', + description: 'Color of the event (CSS color value)', + }, + name: { + type: 'string', + description: 'Display name of the event (optional)', + }, + selected: { + type: 'boolean', + description: 'Whether the event is selected (optional)', + }, + }, + required: ['id', 'from', 'axisId'], + }, + TimelineMarker: { + type: 'object', + properties: { + time: { + type: 'number', + description: + 'Timestamp position of the marker (Unix timestamp in ms)', + }, + color: { + type: 'string', + description: 'Default color of the marker (CSS color value)', + }, + activeColor: { + type: 'string', + description: + 'Color when the marker is active (CSS color value)', + }, + hoverColor: { + type: 'string', + description: + 'Color when hovering over the marker (CSS color value)', + }, + label: { + type: 'string', + description: 'Label text displayed on the marker', + }, + }, + required: ['time'], + }, + TimelineSection: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Unique identifier for the section', + }, + from: { + type: 'number', + description: + 'Start timestamp of the section (Unix timestamp in ms)', + }, + to: { + type: 'number', + description: + 'End timestamp of the section (Unix timestamp in ms, optional - extends to end if not set)', + }, + color: { + type: 'string', + description: + 'Background color of the section (CSS color value)', + }, + hoverColor: { + type: 'string', + description: 'Background color when hovering (CSS color value)', + }, + }, + required: ['id', 'from'], + }, + }, + }, + }, + ], + }); +} diff --git a/src/components/TimelinePlayground/Playground/theme.ts b/src/components/TimelinePlayground/Playground/theme.ts new file mode 100644 index 000000000000..88af951a49ac --- /dev/null +++ b/src/components/TimelinePlayground/Playground/theme.ts @@ -0,0 +1,24 @@ +import type {Monaco} from '@monaco-editor/react'; + +export const GravityTheme = 'gravity'; + +export function defineTheme(monaco: Monaco) { + monaco.editor.defineTheme(GravityTheme, { + base: 'vs-dark', + inherit: true, + rules: [ + { + token: 'string.key.json', + foreground: '#febe5c', + }, + ], + colors: { + 'editor.foreground': '#ffdb4d4d', + 'editor.background': '#251b25', + 'editor.lineHighlightBackground': '#ffdb4d4d', + 'editorLineNumber.foreground': '#bd5c0a', + 'editor.selectionBackground': '#ffdb4d4d', + 'editor.inactiveSelectionBackground': '#ffdb4d4d', + }, + }); +} diff --git a/src/components/TimelinePlayground/TimelinePlayground.tsx b/src/components/TimelinePlayground/TimelinePlayground.tsx new file mode 100644 index 000000000000..8345beaa615e --- /dev/null +++ b/src/components/TimelinePlayground/TimelinePlayground.tsx @@ -0,0 +1,18 @@ +import {useTranslation} from 'next-i18next'; +import {memo} from 'react'; + +import {PlaygroundWrap} from '../PlaygroundWrap'; + +import {Playground} from './Playground'; + +export const TimelinePlayground = memo(() => { + const {t} = useTranslation('timeline'); + + return ( + + + + ); +}); + +TimelinePlayground.displayName = 'TimelinePlayground'; diff --git a/src/components/TimelinePlayground/index.ts b/src/components/TimelinePlayground/index.ts new file mode 100644 index 000000000000..5bc46e11ebef --- /dev/null +++ b/src/components/TimelinePlayground/index.ts @@ -0,0 +1 @@ +export {TimelinePlayground} from './TimelinePlayground'; diff --git a/src/libs.mjs b/src/libs.mjs index 8ba0c9917ea1..24491e7caf1b 100644 --- a/src/libs.mjs +++ b/src/libs.mjs @@ -444,9 +444,9 @@ export const libs = [ npmId: '@gravity-ui/timeline', title: 'Timeline', primary: false, - landing: false, + landing: true, tags: ['ui'], - storybookUrl: '', + storybookUrl: 'https://preview.gravity-ui.com/timeline/', readmeUrl: getReadmeUrls( 'https://raw.githubusercontent.com/gravity-ui/timeline/main', ), diff --git a/src/pages/libraries/[libId]/playground/index.tsx b/src/pages/libraries/[libId]/playground/index.tsx index 97cca672fe37..d5deb45ed78e 100644 --- a/src/pages/libraries/[libId]/playground/index.tsx +++ b/src/pages/libraries/[libId]/playground/index.tsx @@ -15,17 +15,24 @@ const MarkdownEditor = dynamic( ssr: false, }, ); -const GraphPlayround = dynamic( +const GraphPlayground = dynamic( () => import('../../../../components/GraphPlayground/GraphPlayground').then( - (mod) => mod.GraphPlayround, + (mod) => mod.GraphPlayground, ), { ssr: false, }, ); -export const availablePlaygrounds = ['markdown-editor', 'graph']; +const TimelinePlayground = dynamic( + () => import('../../../../components/TimelinePlayground').then((mod) => mod.TimelinePlayground), + { + ssr: false, + }, +); + +export const availablePlaygrounds = ['markdown-editor', 'graph', 'timeline']; export const getServerSideProps: GetServerSideProps = async (context) => { const libId = Array.isArray(context.params?.libId) @@ -52,7 +59,8 @@ export const PlaygroundPage = ({libId}: {libId: string}) => { return ( {libId === 'markdown-editor' && } - {libId === 'graph' && } + {libId === 'graph' && } + {libId === 'timeline' && } ); };