pcp-tgu-ops-ui: вкладка Редактор — шаг времени маршрута и формат длительности
- RouteTimeStepper и доработки EditorPanels/TguEditorTab по вводу времени; - formatDuration: секунды/минуты для коротких интервалов; - стили tgu-editor.css.
This commit is contained in:
@@ -2,7 +2,8 @@ import { formatDate, formatDateTime, formatDuration, formatTime, kindColor, kind
|
||||
import { statusStyle } from "../tgu-planning/tguStatus";
|
||||
import { formatUtcDateTime, parseUtc } from "../../utils/utcTime";
|
||||
import type { RvaZone } from "../../api/ballisticsApi";
|
||||
import type { SurveyRoute, SurveyRouteAnchor } from "../../api/missionPlanningApi";
|
||||
import type { ChangeEvent } from "react";
|
||||
import type { SurveyRoute } from "../../api/missionPlanningApi";
|
||||
import type { Tgu2DMapSelection } from "../tgu-map-2d/model/mapSelectionTypes";
|
||||
import type { RequestMapItem } from "../../model/requestTypes";
|
||||
import type {
|
||||
@@ -45,11 +46,27 @@ type ShootPanelProps = {
|
||||
selectedPass?: EditorVisibilityWindow;
|
||||
modeId?: string;
|
||||
modes: EditorMode[];
|
||||
routeAnchor: SurveyRouteAnchor;
|
||||
/** Окно строящегося маршрута (UTC, мс). undefined пока не выбран проход. */
|
||||
routeWindow?: { startMs: number; endMs: number };
|
||||
/** Текущий крен (gamma) маршрута, градусы. */
|
||||
routeRoll?: number;
|
||||
/** Окно видимости выбранного прохода — для подписи длительности. */
|
||||
passStartMs?: number;
|
||||
passEndMs?: number;
|
||||
/** Допустимый диапазон времени окна маршрута (±30 мин от окна видимости), мс. */
|
||||
routeMinMs?: number;
|
||||
routeMaxMs?: number;
|
||||
routeLoading?: boolean;
|
||||
routeError?: string;
|
||||
route?: SurveyRoute;
|
||||
onAnchorChange: (anchor: SurveyRouteAnchor) => void;
|
||||
onRouteStartChange: (startMs: number) => void;
|
||||
onRouteEndChange: (endMs: number) => void;
|
||||
/** Сдвиг окна целиком вперёд/назад (мс), длительность сохраняется. */
|
||||
onRouteShift: (deltaMs: number) => void;
|
||||
/** Изменение крена влево/вправо (градусы, дельта от кнопок). */
|
||||
onRouteRollChange: (deltaDeg: number) => void;
|
||||
/** Установка крена напрямую (ручной ввод), градусы. */
|
||||
onRouteRollSet: (deg: number) => void;
|
||||
onSelectPass: (pass?: EditorVisibilityWindow) => void;
|
||||
onModeChange: (modeId: string) => void;
|
||||
onAdd: () => void;
|
||||
@@ -57,6 +74,9 @@ type ShootPanelProps = {
|
||||
onClearTarget: () => void;
|
||||
};
|
||||
|
||||
/** Минимальная длительность маршрута, мс (нижняя граница окна). */
|
||||
const MIN_ROUTE_DURATION_MS = 5_000;
|
||||
|
||||
type DownlinkPanelProps = {
|
||||
stationOptions: DownlinkStationOption[];
|
||||
selected?: DownlinkSelection;
|
||||
@@ -204,11 +224,20 @@ export function ShootPanel({
|
||||
selectedPass,
|
||||
modeId,
|
||||
modes,
|
||||
routeAnchor,
|
||||
routeWindow,
|
||||
routeRoll,
|
||||
passStartMs,
|
||||
passEndMs,
|
||||
routeMinMs,
|
||||
routeMaxMs,
|
||||
routeLoading,
|
||||
routeError,
|
||||
route,
|
||||
onAnchorChange,
|
||||
onRouteStartChange,
|
||||
onRouteEndChange,
|
||||
onRouteShift,
|
||||
onRouteRollChange,
|
||||
onRouteRollSet,
|
||||
onSelectPass,
|
||||
onModeChange,
|
||||
onAdd,
|
||||
@@ -287,25 +316,57 @@ export function ShootPanel({
|
||||
</section>
|
||||
)}
|
||||
|
||||
{target && selectedPass && (
|
||||
{target && selectedPass && routeWindow && (
|
||||
<section className="tgu-editor-panel__section">
|
||||
<div className="tgu-editor-eyebrow">4 · маршрут съёмки</div>
|
||||
<div className="tgu-editor-chip-row">
|
||||
<button
|
||||
className={routeAnchor === "CENTERED" ? "is-selected" : ""}
|
||||
type="button"
|
||||
onClick={() => onAnchorChange("CENTERED")}
|
||||
>
|
||||
вокруг момента
|
||||
</button>
|
||||
<button
|
||||
className={routeAnchor === "FORWARD" ? "is-selected" : ""}
|
||||
type="button"
|
||||
onClick={() => onAnchorChange("FORWARD")}
|
||||
>
|
||||
от момента вперёд
|
||||
</button>
|
||||
<div className="tgu-editor-eyebrow">4 · окно маршрута</div>
|
||||
<RouteTimeStepper
|
||||
label="начало"
|
||||
value={routeWindow.startMs}
|
||||
min={routeMinMs}
|
||||
max={routeWindow.endMs - MIN_ROUTE_DURATION_MS}
|
||||
onChange={onRouteStartChange}
|
||||
/>
|
||||
<RouteTimeStepper
|
||||
label="конец"
|
||||
value={routeWindow.endMs}
|
||||
min={routeWindow.startMs + MIN_ROUTE_DURATION_MS}
|
||||
max={routeMaxMs}
|
||||
onChange={onRouteEndChange}
|
||||
/>
|
||||
<div className="tgu-editor-panel__hint">
|
||||
длительность: {formatDuration(routeWindow.endMs - routeWindow.startMs)}
|
||||
{passStartMs != null && passEndMs != null && ` · окно видимости: ${formatDuration(passEndMs - passStartMs)}`}
|
||||
</div>
|
||||
|
||||
<div className="tgu-editor-time-stepper">
|
||||
<span>сдвиг — ◀ назад · вперёд ▶</span>
|
||||
<div className="tgu-editor-time-stepper__steps">
|
||||
<button type="button" onClick={() => onRouteShift(-30_000)}>−30с</button>
|
||||
<button type="button" onClick={() => onRouteShift(-5_000)}>−5с</button>
|
||||
<button type="button" onClick={() => onRouteShift(5_000)}>+5с</button>
|
||||
<button type="button" onClick={() => onRouteShift(30_000)}>+30с</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tgu-editor-time-stepper">
|
||||
<span>крен — ◀ влево · вправо ▶</span>
|
||||
<div>
|
||||
<button type="button" onClick={() => onRouteRollChange(-5)}>−5°</button>
|
||||
<button type="button" onClick={() => onRouteRollChange(-1)}>−1°</button>
|
||||
<input
|
||||
type="number"
|
||||
step={0.5}
|
||||
value={routeRoll ?? 0}
|
||||
onChange={(event) => {
|
||||
const deg = Number.parseFloat(event.target.value);
|
||||
if (Number.isFinite(deg)) onRouteRollSet(deg);
|
||||
}}
|
||||
/>
|
||||
<button type="button" onClick={() => onRouteRollChange(1)}>+1°</button>
|
||||
<button type="button" onClick={() => onRouteRollChange(5)}>+5°</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{routeLoading ? (
|
||||
<div className="tgu-editor-panel__hint">Построение маршрута…</div>
|
||||
) : routeError ? (
|
||||
@@ -628,3 +689,47 @@ function TimeStepper({
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Степпер времени окна маршрута с секундной точностью: кнопки ±5 c / ±30 c и ручной
|
||||
* ввод времени `чч:мм:сс` (UTC, без даты — дата берётся из текущего значения).
|
||||
* Значение зажимается в [min, max].
|
||||
*/
|
||||
function RouteTimeStepper({
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
onChange
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
onChange: (value: number) => void;
|
||||
}) {
|
||||
const clamp = (next: number) => Math.max(min ?? Number.NEGATIVE_INFINITY, Math.min(max ?? Number.POSITIVE_INFINITY, next));
|
||||
const change = (deltaMs: number) => onChange(clamp(value + deltaMs));
|
||||
const onInput = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const raw = event.target.value;
|
||||
if (!raw) return;
|
||||
// Ввод — только время суток; день оставляем прежним (UTC).
|
||||
const datePart = new Date(value).toISOString().slice(0, 10);
|
||||
const ms = parseUtc(`${datePart}T${raw}`);
|
||||
if (Number.isFinite(ms)) onChange(clamp(ms));
|
||||
};
|
||||
|
||||
return (
|
||||
<label className="tgu-editor-time-stepper tgu-editor-time-stepper--seconds">
|
||||
<span>{label}</span>
|
||||
{/* lang=ru-RU — чтобы поле показывало время в 24-часовом формате, а не AM/PM. */}
|
||||
<input lang="ru-RU" type="time" step={1} value={new Date(value).toISOString().slice(11, 19)} onChange={onInput} />
|
||||
<div className="tgu-editor-time-stepper__steps">
|
||||
<button type="button" onClick={() => change(-30_000)}>−30с</button>
|
||||
<button type="button" onClick={() => change(-5_000)}>−5с</button>
|
||||
<button type="button" onClick={() => change(5_000)}>+5с</button>
|
||||
<button type="button" onClick={() => change(30_000)}>+30с</button>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fetchFlightLine, type FlightLinePoint } from "../../api/tguApi";
|
||||
import type { RequestMapItem } from "../../model/requestTypes";
|
||||
import { stationPasses, type OrbitPassInfo, type PassRange } from "../tgu-map-2d/geometry/passes";
|
||||
import { fetchPointVisibility, fetchSatelliteRva, type PointVisibilityParam, type RvaZone } from "../../api/ballisticsApi";
|
||||
import { buildSurveyRoute, saveDropRoute, saveSurveyRoute, type SurveyRoute, type SurveyRouteAnchor } from "../../api/missionPlanningApi";
|
||||
import { buildSurveyRoute, saveDropRoute, saveSurveyRoute, type SurveyRoute } from "../../api/missionPlanningApi";
|
||||
import { parseUtc } from "../../utils/utcTime";
|
||||
import { Tgu2DMapView } from "../tgu-map-2d/Tgu2DMapView";
|
||||
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
|
||||
@@ -58,6 +58,12 @@ const SHOOT_WINDOW_MS = 6 * 60 * 1000;
|
||||
* это значение будет определять бэкенд по профилю наблюдения КА.
|
||||
*/
|
||||
const DEFAULT_CAPTURE_ANGLE_DEG = 1.5;
|
||||
/** Дефолтная длительность строящегося маршрута съёмки (мс). */
|
||||
const DEFAULT_ROUTE_DURATION_MS = 30_000;
|
||||
/** Предел крена (gamma) маршрута при ручной правке, градусы. */
|
||||
const MAX_ROUTE_ROLL_DEG = 89;
|
||||
/** Допустимый выход окна маршрута за пределы окна видимости, мс (±30 минут). */
|
||||
const ROUTE_TIME_MARGIN_MS = 30 * 60_000;
|
||||
const EDITOR_MAP_LAYERS: Tgu2DMapLayersState = {
|
||||
tracks: true,
|
||||
swath: true,
|
||||
@@ -144,7 +150,10 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
const [shootPasses, setShootPasses] = useState<EditorVisibilityWindow[]>([]);
|
||||
const [shootPassesLoading, setShootPassesLoading] = useState(false);
|
||||
const [shootPassesError, setShootPassesError] = useState<string>();
|
||||
const [routeAnchor, setRouteAnchor] = useState<SurveyRouteAnchor>("CENTERED");
|
||||
// Окно строящегося маршрута съёмки: задаётся вручную в правой панели (начало/конец, UTC).
|
||||
const [routeWindow, setRouteWindow] = useState<{ startMs: number; endMs: number }>();
|
||||
// Крен (gamma) строящегося маршрута, градусы. По умолчанию — из прохода, дальше правится вручную.
|
||||
const [routeRoll, setRouteRoll] = useState<number>();
|
||||
const [surveyRoute, setSurveyRoute] = useState<SurveyRoute>();
|
||||
const [routeLoading, setRouteLoading] = useState(false);
|
||||
const [routeError, setRouteError] = useState<string>();
|
||||
@@ -292,29 +301,44 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
};
|
||||
}, [rightMode, target, noradId, window.fromMs, window.toMs]);
|
||||
|
||||
// При выборе нового прохода задаём дефолтное окно маршрута: 30 c вокруг момента
|
||||
// видимости, обрезанное окном видимости прохода. Дальше пользователь правит вручную.
|
||||
useEffect(() => {
|
||||
if (rightMode !== "shoot" || !selectedShootPass) {
|
||||
setRouteWindow(undefined);
|
||||
setRouteRoll(undefined);
|
||||
return;
|
||||
}
|
||||
const passDurationMs = selectedShootPass.endMs - selectedShootPass.startMs;
|
||||
const half = Math.min(DEFAULT_ROUTE_DURATION_MS, passDurationMs) / 2;
|
||||
const mid = (selectedShootPass.startMs + selectedShootPass.endMs) / 2;
|
||||
setRouteWindow({ startMs: mid - half, endMs: mid + half });
|
||||
setRouteRoll(selectedShootPass.rollDeg ?? 0);
|
||||
}, [rightMode, selectedShootPass]);
|
||||
|
||||
useEffect(() => {
|
||||
const numericNorad = noradId ? Number(noradId) : NaN;
|
||||
if (rightMode !== "shoot" || !target || !selectedShootPass || !Number.isFinite(numericNorad)) {
|
||||
if (rightMode !== "shoot" || !target || !selectedShootPass || !routeWindow || routeRoll == null || !Number.isFinite(numericNorad)) {
|
||||
setSurveyRoute(undefined);
|
||||
setRouteError(undefined);
|
||||
setRouteLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const momentMs = (selectedShootPass.startMs + selectedShootPass.endMs) / 2;
|
||||
const durationSeconds = Math.max(1, (selectedShootPass.endMs - selectedShootPass.startMs) / 1000);
|
||||
const durationSeconds = Math.max(1, (routeWindow.endMs - routeWindow.startMs) / 1000);
|
||||
|
||||
let cancelled = false;
|
||||
setRouteLoading(true);
|
||||
setRouteError(undefined);
|
||||
void buildSurveyRoute({
|
||||
satelliteId: numericNorad,
|
||||
time: new Date(momentMs).toISOString().slice(0, 19),
|
||||
// Окно задано явно (начало/конец) — строим вперёд от начала на выбранную длительность.
|
||||
time: new Date(routeWindow.startMs).toISOString().slice(0, 19),
|
||||
durationSeconds,
|
||||
roll: selectedShootPass.rollDeg ?? 0,
|
||||
roll: routeRoll,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
revolution: selectedShootPass.revolution ?? 0,
|
||||
anchor: routeAnchor
|
||||
anchor: "FORWARD"
|
||||
})
|
||||
.then((route) => {
|
||||
if (!cancelled) setSurveyRoute(route);
|
||||
@@ -330,7 +354,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [rightMode, target, selectedShootPass, noradId, routeAnchor]);
|
||||
}, [rightMode, target, selectedShootPass, noradId, routeWindow, routeRoll]);
|
||||
|
||||
// Зоны радиовидимости (ЗРВ) для ручного сброса — грузим при входе в режим «Сброс».
|
||||
useEffect(() => {
|
||||
@@ -396,21 +420,20 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
// выбранного плана (planId == mission_id). Сохраняем только если план выбран и маршрут
|
||||
// уже построен; иначе (открыт только КА без плана) добавляем включение без сохранения.
|
||||
const numericNorad = noradId ? Number(noradId) : NaN;
|
||||
if (selectedPlan && surveyRoute && Number.isFinite(numericNorad)) {
|
||||
if (selectedPlan && surveyRoute && routeWindow && routeRoll != null && Number.isFinite(numericNorad)) {
|
||||
const planId = selectedPlan.planId;
|
||||
const momentMs = (selectedShootPass.startMs + selectedShootPass.endMs) / 2;
|
||||
const routeDurationSeconds = Math.max(1, (selectedShootPass.endMs - selectedShootPass.startMs) / 1000);
|
||||
const routeDurationSeconds = Math.max(1, (routeWindow.endMs - routeWindow.startMs) / 1000);
|
||||
setRouteLoading(true);
|
||||
setRouteError(undefined);
|
||||
try {
|
||||
const saved = await saveSurveyRoute(planId, {
|
||||
satelliteId: numericNorad,
|
||||
time: new Date(momentMs).toISOString().slice(0, 19),
|
||||
time: new Date(routeWindow.startMs).toISOString().slice(0, 19),
|
||||
durationSeconds: routeDurationSeconds,
|
||||
roll: selectedShootPass.rollDeg ?? 0,
|
||||
roll: routeRoll,
|
||||
captureAngle: DEFAULT_CAPTURE_ANGLE_DEG,
|
||||
revolution: selectedShootPass.revolution ?? 0,
|
||||
anchor: routeAnchor,
|
||||
anchor: "FORWARD",
|
||||
// Окно выбранного плана — чтобы при автосоздании миссии/плана задать корректный интервал.
|
||||
planStart: new Date(selectedPlan.startMs).toISOString().slice(0, 19),
|
||||
planEnd: new Date(selectedPlan.endMs).toISOString().slice(0, 19)
|
||||
@@ -424,8 +447,9 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
}
|
||||
}
|
||||
|
||||
const durationMs = Math.min(selectedShootPass.endMs - selectedShootPass.startMs, 30 * 60 * 1000);
|
||||
const startMs = selectedShootPass.startMs + Math.floor((selectedShootPass.endMs - selectedShootPass.startMs - durationMs) / 2);
|
||||
// Окно включения в черновике — заданное вручную окно маршрута (фолбэк — окно прохода).
|
||||
const startMs = routeWindow?.startMs ?? selectedShootPass.startMs;
|
||||
const endMs = routeWindow?.endMs ?? selectedShootPass.endMs;
|
||||
const mode = modes.find((item) => item.id === modeId);
|
||||
const id = nextWorkId(idCounterRef);
|
||||
|
||||
@@ -433,7 +457,7 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
addShoot(current, {
|
||||
id,
|
||||
startMs,
|
||||
endMs: startMs + durationMs,
|
||||
endMs,
|
||||
label: mode?.label ?? "Съёмка",
|
||||
modeId,
|
||||
target
|
||||
@@ -752,11 +776,39 @@ export function TguEditorTab({ selectedSpacecraftId, selectedPlan, appliedRange,
|
||||
selectedPass={selectedShootPass}
|
||||
modeId={modeId}
|
||||
modes={modes}
|
||||
routeAnchor={routeAnchor}
|
||||
routeWindow={routeWindow}
|
||||
routeRoll={routeRoll}
|
||||
passStartMs={selectedShootPass?.startMs}
|
||||
passEndMs={selectedShootPass?.endMs}
|
||||
routeMinMs={selectedShootPass != null ? selectedShootPass.startMs - ROUTE_TIME_MARGIN_MS : undefined}
|
||||
routeMaxMs={selectedShootPass != null ? selectedShootPass.endMs + ROUTE_TIME_MARGIN_MS : undefined}
|
||||
routeLoading={routeLoading}
|
||||
routeError={routeError}
|
||||
route={surveyRoute}
|
||||
onAnchorChange={setRouteAnchor}
|
||||
onRouteStartChange={(startMs) => setRouteWindow((current) => (current ? { ...current, startMs } : current))}
|
||||
onRouteEndChange={(endMs) => setRouteWindow((current) => (current ? { ...current, endMs } : current))}
|
||||
onRouteShift={(deltaMs) =>
|
||||
setRouteWindow((current) => {
|
||||
if (!current || selectedShootPass == null) return current;
|
||||
// Двигаем окно целиком, сохраняя длительность; не выходим за ±30 мин от окна видимости.
|
||||
const minMs = selectedShootPass.startMs - ROUTE_TIME_MARGIN_MS;
|
||||
const maxMs = selectedShootPass.endMs + ROUTE_TIME_MARGIN_MS;
|
||||
let shift = deltaMs;
|
||||
if (current.endMs + shift > maxMs) shift = maxMs - current.endMs;
|
||||
if (current.startMs + shift < minMs) shift = minMs - current.startMs;
|
||||
return { startMs: current.startMs + shift, endMs: current.endMs + shift };
|
||||
})
|
||||
}
|
||||
onRouteRollChange={(deltaDeg) =>
|
||||
setRouteRoll((current) =>
|
||||
Math.max(-MAX_ROUTE_ROLL_DEG, Math.min(MAX_ROUTE_ROLL_DEG, (current ?? 0) + deltaDeg))
|
||||
)
|
||||
}
|
||||
onRouteRollSet={(deg) =>
|
||||
setRouteRoll((current) =>
|
||||
Number.isFinite(deg) ? Math.max(-MAX_ROUTE_ROLL_DEG, Math.min(MAX_ROUTE_ROLL_DEG, deg)) : current
|
||||
)
|
||||
}
|
||||
onSelectPass={setSelectedShootPass}
|
||||
onModeChange={setModeId}
|
||||
onAdd={addShootWork}
|
||||
|
||||
@@ -37,6 +37,16 @@ export function formatDateTime(ms: number): string {
|
||||
}
|
||||
|
||||
export function formatDuration(ms: number): string {
|
||||
const totalSeconds = Math.max(0, Math.round(ms / 1000));
|
||||
if (totalSeconds < 60) {
|
||||
return `${totalSeconds} с`;
|
||||
}
|
||||
if (totalSeconds < 3600) {
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
return seconds > 0 ? `${minutes} мин ${seconds} с` : `${minutes} мин`;
|
||||
}
|
||||
|
||||
const hours = ms / 3_600_000;
|
||||
if (hours < 24) {
|
||||
return `${Number.isInteger(hours) ? hours : hours.toFixed(1)} ч`;
|
||||
|
||||
@@ -910,6 +910,18 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Узкая правая панель: строка прохода не помещается в один ряд — раскладываем
|
||||
подпись и параметры видимости в столбец, чтобы текст не обрезался. */
|
||||
.tgu-editor-pass-list button {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.tgu-editor-pass-list small {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.tgu-editor-pass-list span,
|
||||
.tgu-editor-station span {
|
||||
color: var(--text);
|
||||
@@ -975,7 +987,8 @@
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
|
||||
.tgu-editor-time-stepper output {
|
||||
.tgu-editor-time-stepper output,
|
||||
.tgu-editor-time-stepper input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--line);
|
||||
@@ -988,6 +1001,31 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tgu-editor-time-stepper input::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
opacity: 0.5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Секундный степпер окна маршрута: поле ввода на своей строке, кнопки ± — ниже,
|
||||
чтобы помещаться в узкую правую панель. */
|
||||
.tgu-editor-time-stepper--seconds {
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.tgu-editor-time-stepper--seconds > input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tgu-editor-time-stepper__steps {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.tgu-editor-time-stepper__steps button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tgu-editor-panel__color {
|
||||
position: absolute;
|
||||
top: 0.2rem;
|
||||
|
||||
Reference in New Issue
Block a user