@keplar-404/react-timeline-editor 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/control_area/index.d.ts +0 -0
- package/dist/components/cursor/cursor.d.ts +20 -0
- package/dist/components/cut-overlay/CutOverlay.d.ts +202 -0
- package/dist/components/edit_area/cross_row_drag.d.ts +50 -0
- package/dist/components/edit_area/drag_lines.d.ts +11 -0
- package/dist/components/edit_area/drag_preview.d.ts +14 -0
- package/dist/components/edit_area/drag_utils.d.ts +39 -0
- package/dist/components/edit_area/edit_action.d.ts +19 -0
- package/dist/components/edit_area/edit_area.d.ts +56 -0
- package/dist/components/edit_area/edit_row.d.ts +27 -0
- package/dist/components/edit_area/hooks/use_drag_line.d.ts +33 -0
- package/dist/components/edit_area/insertion_line.d.ts +12 -0
- package/dist/components/loop-zone/LoopZoneOverlay.d.ts +243 -0
- package/dist/components/row_rnd/hooks/useAutoScroll.d.ts +7 -0
- package/dist/components/row_rnd/interactable.d.ts +14 -0
- package/dist/components/row_rnd/row_rnd.d.ts +3 -0
- package/dist/components/row_rnd/row_rnd_interface.d.ts +47 -0
- package/dist/components/time_area/time_area.d.ts +17 -0
- package/dist/components/timeline.d.ts +3 -0
- package/dist/components/transport/TransportBar.d.ts +132 -0
- package/dist/components/transport/useTimelinePlayer.d.ts +164 -0
- package/dist/index.cjs.js +13 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.es.js +10102 -0
- package/dist/index.umd.js +13 -0
- package/dist/interface/common_prop.d.ts +12 -0
- package/dist/interface/const.d.ts +28 -0
- package/dist/interface/timeline.d.ts +342 -0
- package/dist/react-timeline-editor.css +1 -0
- package/dist/utils/check_props.d.ts +2 -0
- package/dist/utils/deal_class_prefix.d.ts +1 -0
- package/dist/utils/deal_data.d.ts +58 -0
- package/dist/utils/logger.d.ts +132 -0
- package/package.json +70 -0
- package/src/components/control_area/index.tsx +1 -0
- package/src/components/cursor/cursor.css +26 -0
- package/src/components/cursor/cursor.tsx +105 -0
- package/src/components/cut-overlay/CutOverlay.css +68 -0
- package/src/components/cut-overlay/CutOverlay.tsx +491 -0
- package/src/components/edit_area/cross_row_drag.tsx +174 -0
- package/src/components/edit_area/drag_lines.css +13 -0
- package/src/components/edit_area/drag_lines.tsx +31 -0
- package/src/components/edit_area/drag_preview.tsx +50 -0
- package/src/components/edit_area/drag_utils.ts +77 -0
- package/src/components/edit_area/edit_action.css +56 -0
- package/src/components/edit_area/edit_action.tsx +362 -0
- package/src/components/edit_area/edit_area.css +24 -0
- package/src/components/edit_area/edit_area.tsx +606 -0
- package/src/components/edit_area/edit_row.css +78 -0
- package/src/components/edit_area/edit_row.tsx +128 -0
- package/src/components/edit_area/hooks/use_drag_line.ts +93 -0
- package/src/components/edit_area/insertion_line.tsx +39 -0
- package/src/components/loop-zone/LoopZoneOverlay.css +65 -0
- package/src/components/loop-zone/LoopZoneOverlay.tsx +461 -0
- package/src/components/row_rnd/hooks/useAutoScroll.ts +81 -0
- package/src/components/row_rnd/interactable.tsx +55 -0
- package/src/components/row_rnd/row_rnd.tsx +365 -0
- package/src/components/row_rnd/row_rnd_interface.ts +59 -0
- package/src/components/time_area/time_area.css +35 -0
- package/src/components/time_area/time_area.tsx +93 -0
- package/src/components/timeline.css +12 -0
- package/src/components/timeline.tsx +227 -0
- package/src/components/transport/TransportBar.css +171 -0
- package/src/components/transport/TransportBar.tsx +322 -0
- package/src/components/transport/useTimelinePlayer.ts +319 -0
- package/src/index.tsx +17 -0
- package/src/interface/common_prop.ts +13 -0
- package/src/interface/const.ts +32 -0
- package/src/interface/timeline.ts +329 -0
- package/src/utils/check_props.ts +77 -0
- package/src/utils/deal_class_prefix.ts +6 -0
- package/src/utils/deal_data.ts +159 -0
- package/src/utils/logger.ts +239 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { ITimelineEngine, TimelineEngine, TimelineRow } from '@keplar-404/timeline-engine';
|
|
2
|
+
import React, { useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { ScrollSync } from 'react-virtualized';
|
|
4
|
+
import { MIN_SCALE_COUNT, PREFIX, START_CURSOR_TIME } from '../interface/const';
|
|
5
|
+
import { TimelineEditor, TimelineState } from '../interface/timeline';
|
|
6
|
+
import { checkProps } from '../utils/check_props';
|
|
7
|
+
import { getScaleCountByRows, parserPixelToTime, parserTimeToPixel } from '../utils/deal_data';
|
|
8
|
+
import { Cursor } from './cursor/cursor';
|
|
9
|
+
import { EditArea } from './edit_area/edit_area';
|
|
10
|
+
import { TimeArea } from './time_area/time_area';
|
|
11
|
+
import './timeline.css';
|
|
12
|
+
|
|
13
|
+
export const Timeline = React.forwardRef<TimelineState, TimelineEditor>((props, ref) => {
|
|
14
|
+
const checkedProps = checkProps(props);
|
|
15
|
+
const { style } = props;
|
|
16
|
+
let {
|
|
17
|
+
effects,
|
|
18
|
+
editorData: data,
|
|
19
|
+
scrollTop,
|
|
20
|
+
autoScroll,
|
|
21
|
+
hideCursor,
|
|
22
|
+
disableDrag,
|
|
23
|
+
scale,
|
|
24
|
+
scaleWidth,
|
|
25
|
+
startLeft,
|
|
26
|
+
minScaleCount,
|
|
27
|
+
maxScaleCount,
|
|
28
|
+
onChange,
|
|
29
|
+
engine,
|
|
30
|
+
autoReRender = true,
|
|
31
|
+
onScroll: onScrollVertical,
|
|
32
|
+
} = checkedProps;
|
|
33
|
+
|
|
34
|
+
const engineRef = useRef<ITimelineEngine>(engine || new TimelineEngine());
|
|
35
|
+
const domRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const areaRef = useRef<HTMLDivElement>(null);
|
|
37
|
+
|
|
38
|
+
const scrollSync = useRef<ScrollSync>(null);
|
|
39
|
+
|
|
40
|
+
// Editor data
|
|
41
|
+
const [editorData, setEditorData] = useState(data);
|
|
42
|
+
// Scale count
|
|
43
|
+
const [scaleCount, setScaleCount] = useState(MIN_SCALE_COUNT);
|
|
44
|
+
// Cursor time
|
|
45
|
+
const [cursorTime, setCursorTime] = useState(START_CURSOR_TIME);
|
|
46
|
+
// Whether it is currently playing
|
|
47
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
48
|
+
// Current timeline width
|
|
49
|
+
const [width, setWidth] = useState(Number.MAX_SAFE_INTEGER);
|
|
50
|
+
|
|
51
|
+
/** Listen for data changes */
|
|
52
|
+
useLayoutEffect(() => {
|
|
53
|
+
handleSetScaleCount(getScaleCountByRows(data, { scale }));
|
|
54
|
+
setEditorData(data);
|
|
55
|
+
}, [data, minScaleCount, maxScaleCount, scale]);
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
engineRef.current.effects = effects;
|
|
59
|
+
}, [effects]);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
engineRef.current.data = editorData;
|
|
63
|
+
}, [editorData]);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
autoReRender && engineRef.current.reRender();
|
|
67
|
+
}, [editorData]);
|
|
68
|
+
|
|
69
|
+
// deprecated
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
scrollSync.current && scrollSync.current.setState({ scrollTop: scrollTop });
|
|
72
|
+
}, [scrollTop]);
|
|
73
|
+
|
|
74
|
+
/** Dynamically set scale count */
|
|
75
|
+
const handleSetScaleCount = (value: number) => {
|
|
76
|
+
const data = Math.min(maxScaleCount, Math.max(minScaleCount, value));
|
|
77
|
+
setScaleCount(data);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Handle active data changes */
|
|
81
|
+
const handleEditorDataChange = (editorData: TimelineRow[]) => {
|
|
82
|
+
const result = onChange?.(editorData);
|
|
83
|
+
if (result !== false) {
|
|
84
|
+
engineRef.current.data = editorData;
|
|
85
|
+
autoReRender && engineRef.current.reRender();
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** Handle cursor */
|
|
90
|
+
const handleSetCursor = (param: { left?: number; time?: number; updateTime?: boolean }) => {
|
|
91
|
+
let { left, time = 0, updateTime = true } = param;
|
|
92
|
+
if (typeof left === 'undefined' && typeof time === 'undefined') return false;
|
|
93
|
+
|
|
94
|
+
if (typeof time === 'undefined') {
|
|
95
|
+
if (typeof left === 'undefined') left = parserTimeToPixel(time, { startLeft, scale, scaleWidth });
|
|
96
|
+
time = parserPixelToTime(left, { startLeft, scale, scaleWidth });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let result = true;
|
|
100
|
+
if (updateTime) {
|
|
101
|
+
result = engineRef.current.setTime(time);
|
|
102
|
+
autoReRender && engineRef.current.reRender();
|
|
103
|
+
}
|
|
104
|
+
result && setCursorTime(time);
|
|
105
|
+
return result;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Set scrollLeft */
|
|
109
|
+
const handleDeltaScrollLeft = (delta: number) => {
|
|
110
|
+
// When exceeding the maximum distance, prohibit automatic scrolling
|
|
111
|
+
const data = (scrollSync.current?.state?.scrollLeft ?? 0) + delta;
|
|
112
|
+
if (data > scaleCount * (scaleWidth - 1) + startLeft - width) return;
|
|
113
|
+
scrollSync.current && scrollSync.current.setState({ scrollLeft: Math.max(scrollSync.current.state.scrollLeft + delta, 0) });
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Handle engine-related data
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const handleTime = ({ time }: { time: number }) => {
|
|
119
|
+
handleSetCursor({ time, updateTime: false });
|
|
120
|
+
};
|
|
121
|
+
const handlePlay = () => setIsPlaying(true);
|
|
122
|
+
const handlePaused = () => setIsPlaying(false);
|
|
123
|
+
engineRef.current.on('setTimeByTick', handleTime);
|
|
124
|
+
engineRef.current.on('play', handlePlay);
|
|
125
|
+
engineRef.current.on('paused', handlePaused);
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
// ref data
|
|
129
|
+
useImperativeHandle(ref, () => ({
|
|
130
|
+
get target() {
|
|
131
|
+
return domRef.current;
|
|
132
|
+
},
|
|
133
|
+
get listener() {
|
|
134
|
+
return engineRef.current;
|
|
135
|
+
},
|
|
136
|
+
get isPlaying() {
|
|
137
|
+
return engineRef.current.isPlaying;
|
|
138
|
+
},
|
|
139
|
+
get isPaused() {
|
|
140
|
+
return engineRef.current.isPaused;
|
|
141
|
+
},
|
|
142
|
+
setPlayRate: engineRef.current.setPlayRate.bind(engineRef.current),
|
|
143
|
+
getPlayRate: engineRef.current.getPlayRate.bind(engineRef.current),
|
|
144
|
+
setTime: (time: number) => handleSetCursor({ time }),
|
|
145
|
+
getTime: engineRef.current.getTime.bind(engineRef.current),
|
|
146
|
+
reRender: engineRef.current.reRender.bind(engineRef.current),
|
|
147
|
+
play: (param: Parameters<TimelineState['play']>[0]) => engineRef.current.play({ ...param }),
|
|
148
|
+
pause: engineRef.current.pause.bind(engineRef.current),
|
|
149
|
+
setScrollLeft: (val) => {
|
|
150
|
+
scrollSync.current && scrollSync.current.setState({ scrollLeft: Math.max(val, 0) });
|
|
151
|
+
},
|
|
152
|
+
setScrollTop: (val) => {
|
|
153
|
+
scrollSync.current && scrollSync.current.setState({ scrollTop: Math.max(val, 0) });
|
|
154
|
+
},
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
// Listen for width changes in the timeline area
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (areaRef.current) {
|
|
160
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
161
|
+
if (!areaRef.current) return;
|
|
162
|
+
setWidth(areaRef.current.getBoundingClientRect().width);
|
|
163
|
+
});
|
|
164
|
+
resizeObserver.observe(areaRef.current!);
|
|
165
|
+
return () => {
|
|
166
|
+
resizeObserver && resizeObserver.disconnect();
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div ref={domRef} style={style} className={`${PREFIX} ${disableDrag ? PREFIX + '-disable' : ''}`}>
|
|
173
|
+
<ScrollSync ref={scrollSync}>
|
|
174
|
+
{({ scrollLeft, scrollTop, onScroll }) => (
|
|
175
|
+
<>
|
|
176
|
+
<TimeArea
|
|
177
|
+
{...checkedProps}
|
|
178
|
+
timelineWidth={width}
|
|
179
|
+
disableDrag={disableDrag || isPlaying}
|
|
180
|
+
setCursor={handleSetCursor}
|
|
181
|
+
cursorTime={cursorTime}
|
|
182
|
+
editorData={editorData}
|
|
183
|
+
scaleCount={scaleCount}
|
|
184
|
+
setScaleCount={handleSetScaleCount}
|
|
185
|
+
onScroll={onScroll}
|
|
186
|
+
scrollLeft={scrollLeft}
|
|
187
|
+
/>
|
|
188
|
+
<EditArea
|
|
189
|
+
{...checkedProps}
|
|
190
|
+
timelineWidth={width}
|
|
191
|
+
ref={(ref) => { (areaRef.current as any) = ref?.domRef.current; }}
|
|
192
|
+
disableDrag={disableDrag || isPlaying}
|
|
193
|
+
editorData={editorData}
|
|
194
|
+
cursorTime={cursorTime}
|
|
195
|
+
scaleCount={scaleCount}
|
|
196
|
+
setScaleCount={handleSetScaleCount}
|
|
197
|
+
scrollTop={scrollTop}
|
|
198
|
+
scrollLeft={scrollLeft}
|
|
199
|
+
setEditorData={handleEditorDataChange}
|
|
200
|
+
deltaScrollLeft={autoScroll ? handleDeltaScrollLeft : () => {}}
|
|
201
|
+
onScroll={(params) => {
|
|
202
|
+
onScroll(params);
|
|
203
|
+
onScrollVertical && onScrollVertical(params);
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
{!hideCursor && (
|
|
207
|
+
<Cursor
|
|
208
|
+
{...checkedProps}
|
|
209
|
+
timelineWidth={width}
|
|
210
|
+
disableDrag={isPlaying}
|
|
211
|
+
scrollLeft={scrollLeft}
|
|
212
|
+
scaleCount={scaleCount}
|
|
213
|
+
setScaleCount={handleSetScaleCount}
|
|
214
|
+
setCursor={handleSetCursor}
|
|
215
|
+
cursorTime={cursorTime}
|
|
216
|
+
editorData={editorData}
|
|
217
|
+
areaRef={areaRef}
|
|
218
|
+
scrollSync={scrollSync}
|
|
219
|
+
deltaScrollLeft={autoScroll ? handleDeltaScrollLeft : undefined}
|
|
220
|
+
/>
|
|
221
|
+
)}
|
|
222
|
+
</>
|
|
223
|
+
)}
|
|
224
|
+
</ScrollSync>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
.timeline-transport-bar {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: space-between;
|
|
5
|
+
padding: 0 16px;
|
|
6
|
+
height: 48px;
|
|
7
|
+
background: rgba(10, 10, 20, 0.85);
|
|
8
|
+
backdrop-filter: blur(12px);
|
|
9
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
|
10
|
+
gap: 16px;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
|
|
13
|
+
}
|
|
14
|
+
.timeline-transport-controls {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 4px;
|
|
18
|
+
}
|
|
19
|
+
.timeline-transport-btn {
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
width: 30px;
|
|
24
|
+
height: 30px;
|
|
25
|
+
border-radius: 6px;
|
|
26
|
+
border: 1px solid rgba(255, 255, 255, 0.07);
|
|
27
|
+
background: rgba(255, 255, 255, 0.03);
|
|
28
|
+
color: #64748b;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
transition: background 0.12s, color 0.12s, border-color 0.12s;
|
|
31
|
+
}
|
|
32
|
+
.timeline-transport-btn:hover {
|
|
33
|
+
background: rgba(255, 255, 255, 0.08);
|
|
34
|
+
color: #94a3b8;
|
|
35
|
+
border-color: rgba(255, 255, 255, 0.12);
|
|
36
|
+
}
|
|
37
|
+
.timeline-transport-btn--primary {
|
|
38
|
+
color: #c4c9d4;
|
|
39
|
+
}
|
|
40
|
+
.timeline-transport-btn--primary:hover {
|
|
41
|
+
background: rgba(255, 255, 255, 0.1);
|
|
42
|
+
color: #e2e8f0;
|
|
43
|
+
}
|
|
44
|
+
.timeline-transport-btn--stop:hover {
|
|
45
|
+
background: rgba(239, 68, 68, 0.12);
|
|
46
|
+
border-color: rgba(239, 68, 68, 0.3);
|
|
47
|
+
color: #ef4444;
|
|
48
|
+
}
|
|
49
|
+
.timeline-transport-btn--loop {
|
|
50
|
+
font-size: 14px;
|
|
51
|
+
line-height: 1;
|
|
52
|
+
width: auto;
|
|
53
|
+
padding: 0 6px;
|
|
54
|
+
color: #4b5563;
|
|
55
|
+
}
|
|
56
|
+
.timeline-transport-btn--loop:hover {
|
|
57
|
+
background: rgba(16, 185, 129, 0.1);
|
|
58
|
+
border-color: rgba(16, 185, 129, 0.3);
|
|
59
|
+
color: #10b981;
|
|
60
|
+
}
|
|
61
|
+
.timeline-transport-btn--loop-active {
|
|
62
|
+
background: rgba(16, 185, 129, 0.18) !important;
|
|
63
|
+
border-color: rgba(16, 185, 129, 0.5) !important;
|
|
64
|
+
color: #10b981 !important;
|
|
65
|
+
box-shadow: 0 0 10px rgba(16, 185, 129, 0.25) !important;
|
|
66
|
+
}
|
|
67
|
+
.timeline-transport-divider {
|
|
68
|
+
width: 1px;
|
|
69
|
+
height: 20px;
|
|
70
|
+
background: rgba(255, 255, 255, 0.08);
|
|
71
|
+
margin: 0 4px;
|
|
72
|
+
flex-shrink: 0;
|
|
73
|
+
}
|
|
74
|
+
.timeline-transport-loop-inputs {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 4px;
|
|
78
|
+
margin-left: 4px;
|
|
79
|
+
}
|
|
80
|
+
.timeline-transport-loop-input {
|
|
81
|
+
width: 52px;
|
|
82
|
+
background: rgba(0, 0, 0, 0.35);
|
|
83
|
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
|
84
|
+
border-radius: 5px;
|
|
85
|
+
color: #10b981;
|
|
86
|
+
font-size: 12px;
|
|
87
|
+
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
88
|
+
font-weight: 600;
|
|
89
|
+
padding: 2px 6px;
|
|
90
|
+
text-align: center;
|
|
91
|
+
outline: none;
|
|
92
|
+
transition: border-color 0.12s;
|
|
93
|
+
}
|
|
94
|
+
.timeline-transport-loop-input:focus {
|
|
95
|
+
border-color: rgba(16, 185, 129, 0.7);
|
|
96
|
+
box-shadow: 0 0 6px rgba(16, 185, 129, 0.2);
|
|
97
|
+
}
|
|
98
|
+
.timeline-transport-loop-input::-webkit-inner-spin-button,
|
|
99
|
+
.timeline-transport-loop-input::-webkit-outer-spin-button {
|
|
100
|
+
-webkit-appearance: none;
|
|
101
|
+
margin: 0;
|
|
102
|
+
}
|
|
103
|
+
.timeline-transport-loop-arrow {
|
|
104
|
+
font-size: 11px;
|
|
105
|
+
color: #334155;
|
|
106
|
+
}
|
|
107
|
+
.timeline-transport-loop-unit {
|
|
108
|
+
font-size: 10px;
|
|
109
|
+
color: #334155;
|
|
110
|
+
font-family: 'SF Mono', 'Cascadia Code', monospace;
|
|
111
|
+
}
|
|
112
|
+
.timeline-transport-time {
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 1px;
|
|
117
|
+
min-width: 100px;
|
|
118
|
+
}
|
|
119
|
+
.timeline-transport-time-value {
|
|
120
|
+
font-size: 22px;
|
|
121
|
+
font-weight: 700;
|
|
122
|
+
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
|
123
|
+
color: #e2e8f0;
|
|
124
|
+
letter-spacing: 0.05em;
|
|
125
|
+
line-height: 1;
|
|
126
|
+
}
|
|
127
|
+
.timeline-transport-time-label {
|
|
128
|
+
font-size: 9px;
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
letter-spacing: 0.15em;
|
|
131
|
+
color: #475569;
|
|
132
|
+
text-transform: uppercase;
|
|
133
|
+
}
|
|
134
|
+
.timeline-transport-rate {
|
|
135
|
+
display: flex;
|
|
136
|
+
align-items: center;
|
|
137
|
+
gap: 8px;
|
|
138
|
+
}
|
|
139
|
+
.timeline-transport-rate-label {
|
|
140
|
+
font-size: 9px;
|
|
141
|
+
font-weight: 600;
|
|
142
|
+
letter-spacing: 0.15em;
|
|
143
|
+
color: #475569;
|
|
144
|
+
text-transform: uppercase;
|
|
145
|
+
}
|
|
146
|
+
.timeline-transport-rate-buttons {
|
|
147
|
+
display: flex;
|
|
148
|
+
gap: 3px;
|
|
149
|
+
}
|
|
150
|
+
.timeline-transport-rate-btn {
|
|
151
|
+
padding: 3px 8px;
|
|
152
|
+
border-radius: 5px;
|
|
153
|
+
border: 1px solid rgba(255, 255, 255, 0.07);
|
|
154
|
+
background: rgba(255, 255, 255, 0.03);
|
|
155
|
+
color: #475569;
|
|
156
|
+
font-size: 11px;
|
|
157
|
+
font-weight: 600;
|
|
158
|
+
font-family: 'SF Mono', 'Cascadia Code', monospace;
|
|
159
|
+
cursor: pointer;
|
|
160
|
+
transition: all 0.12s;
|
|
161
|
+
}
|
|
162
|
+
.timeline-transport-rate-btn:hover {
|
|
163
|
+
background: rgba(255, 255, 255, 0.07);
|
|
164
|
+
color: #94a3b8;
|
|
165
|
+
}
|
|
166
|
+
.timeline-transport-rate-btn--active {
|
|
167
|
+
background: rgba(59, 130, 246, 0.15);
|
|
168
|
+
border-color: rgba(59, 130, 246, 0.4);
|
|
169
|
+
color: #3b82f6;
|
|
170
|
+
box-shadow: 0 0 6px rgba(59, 130, 246, 0.15);
|
|
171
|
+
}
|