= 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' && }
);
};