feat: add score timeline to batch analysis
This commit is contained in:
@@ -0,0 +1,210 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { NoteScorePoint } from '$lib/engine/index.ts';
|
||||||
|
|
||||||
|
export let notes: readonly NoteScorePoint[];
|
||||||
|
export let durationSeconds: number;
|
||||||
|
export let minimumWidth = 720;
|
||||||
|
export let timeTicks: readonly number[] = [];
|
||||||
|
|
||||||
|
type ScorePoint = {
|
||||||
|
timeSeconds: number;
|
||||||
|
averageScore: number;
|
||||||
|
maximumScore: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HEIGHT = 220;
|
||||||
|
const LEFT = 0;
|
||||||
|
const RIGHT = 0;
|
||||||
|
const TOP = 8;
|
||||||
|
const BOTTOM = 8;
|
||||||
|
const PLOT_HEIGHT = HEIGHT - TOP - BOTTOM;
|
||||||
|
|
||||||
|
let chartWidth = 1000;
|
||||||
|
let hoverTime: number | null = null;
|
||||||
|
let tooltipPercent = 0;
|
||||||
|
|
||||||
|
$: viewWidth = Math.max(320, chartWidth);
|
||||||
|
$: plotWidth = viewWidth - LEFT - RIGHT;
|
||||||
|
$: points = buildPoints(notes);
|
||||||
|
$: finalMaximumScore = Math.max(1, points.at(-1)?.maximumScore ?? 0);
|
||||||
|
$: chartMaximumScore = Math.max(500_000, Math.ceil(finalMaximumScore / 500_000) * 500_000);
|
||||||
|
$: averagePath = linePath(points, 'averageScore', durationSeconds, chartMaximumScore, viewWidth);
|
||||||
|
$: maximumPath = linePath(points, 'maximumScore', durationSeconds, chartMaximumScore, viewWidth);
|
||||||
|
$: yTicks = Array.from({ length: chartMaximumScore / 500_000 + 1 }, (_, index) => index * 500_000);
|
||||||
|
$: hoveredPoint = hoverTime === null ? null : pointAtTime(points, hoverTime);
|
||||||
|
|
||||||
|
function buildPoints(source: readonly NoteScorePoint[]): ScorePoint[] {
|
||||||
|
let averageScore = 0;
|
||||||
|
let maximumScore = 0;
|
||||||
|
const result: ScorePoint[] = [{ timeSeconds: 0, averageScore: 0, maximumScore: 0 }];
|
||||||
|
for (const note of source) {
|
||||||
|
averageScore += note.expectedScore;
|
||||||
|
maximumScore += note.theoreticalMaxScore;
|
||||||
|
result.push({ timeSeconds: note.timeSeconds, averageScore, maximumScore });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function x(timeSeconds: number, width = viewWidth): number {
|
||||||
|
return LEFT + Math.max(0, Math.min(1, timeSeconds / Math.max(1, durationSeconds))) * (width - LEFT - RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
function y(score: number, scaleMaximum = chartMaximumScore): number {
|
||||||
|
return TOP + (1 - score / scaleMaximum) * PLOT_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linePath(source: readonly ScorePoint[], field: 'averageScore' | 'maximumScore', duration: number, scaleMaximum: number, width: number): string {
|
||||||
|
if (source.length === 0) return '';
|
||||||
|
const commands = [`M ${x(source[0].timeSeconds, width).toFixed(2)} ${y(source[0][field], scaleMaximum).toFixed(2)}`];
|
||||||
|
for (let index = 1; index < source.length; index += 1) {
|
||||||
|
const previous = source[index - 1];
|
||||||
|
const point = source[index];
|
||||||
|
commands.push(
|
||||||
|
`L ${x(point.timeSeconds, width).toFixed(2)} ${y(previous[field], scaleMaximum).toFixed(2)}`,
|
||||||
|
`L ${x(point.timeSeconds, width).toFixed(2)} ${y(point[field], scaleMaximum).toFixed(2)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const last = source.at(-1)!;
|
||||||
|
commands.push(`L ${x(duration, width).toFixed(2)} ${y(last[field], scaleMaximum).toFixed(2)}`);
|
||||||
|
return commands.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointAtTime(source: readonly ScorePoint[], timeSeconds: number): ScorePoint {
|
||||||
|
let low = 0;
|
||||||
|
let high = source.length - 1;
|
||||||
|
while (low < high) {
|
||||||
|
const middle = Math.ceil((low + high) / 2);
|
||||||
|
if (source[middle].timeSeconds <= timeSeconds) low = middle;
|
||||||
|
else high = middle - 1;
|
||||||
|
}
|
||||||
|
return source[low] ?? { timeSeconds: 0, averageScore: 0, maximumScore: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHover(clientX: number, target: EventTarget | null): void {
|
||||||
|
if (!(target instanceof HTMLElement)) return;
|
||||||
|
const bounds = target.getBoundingClientRect();
|
||||||
|
const plotLeft = bounds.left + bounds.width * LEFT / viewWidth;
|
||||||
|
const renderedPlotWidth = bounds.width * plotWidth / viewWidth;
|
||||||
|
const ratio = Math.max(0, Math.min(1, (clientX - plotLeft) / Math.max(1, renderedPlotWidth)));
|
||||||
|
hoverTime = ratio * durationSeconds;
|
||||||
|
tooltipPercent = (LEFT + ratio * plotWidth) / viewWidth * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerMove(event: PointerEvent): void {
|
||||||
|
updateHover(event.clientX, event.currentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(event: KeyboardEvent): void {
|
||||||
|
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const step = event.key === 'Home' ? -durationSeconds : event.key === 'End' ? durationSeconds : event.key === 'ArrowLeft' ? -1 : 1;
|
||||||
|
hoverTime = Math.max(0, Math.min(durationSeconds, (hoverTime ?? 0) + step));
|
||||||
|
tooltipPercent = (LEFT + hoverTime / Math.max(1, durationSeconds) * plotWidth) / viewWidth * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatScore(score: number): string {
|
||||||
|
return Math.round(score).toLocaleString('ko-KR');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAxisScore(score: number): string {
|
||||||
|
return score === 0 ? '0' : `${Math.round(score / 10_000).toLocaleString('ko-KR')}만`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(timeSeconds: number): string {
|
||||||
|
const minutes = Math.floor(timeSeconds / 60);
|
||||||
|
const seconds = Math.floor(timeSeconds % 60);
|
||||||
|
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="score-row">
|
||||||
|
<strong class="lane-label">
|
||||||
|
<span class="label-heading">
|
||||||
|
<span>시간에 따른 점수</span>
|
||||||
|
<small class="legend" aria-label="점수 그래프 범례">
|
||||||
|
<span><i class="average"></i>평균</span>
|
||||||
|
<span><i class="maximum"></i>최고</span>
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
{#each yTicks as tick}
|
||||||
|
<span class="axis-value" style={`top: ${y(tick)}px;`}>{formatAxisScore(tick)}</span>
|
||||||
|
{/each}
|
||||||
|
</strong>
|
||||||
|
<div
|
||||||
|
class="chart-wrap score-track"
|
||||||
|
bind:clientWidth={chartWidth}
|
||||||
|
style={`min-width: ${minimumWidth}px;`}
|
||||||
|
role="slider"
|
||||||
|
aria-label="점수를 확인할 시각"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax={Math.ceil(durationSeconds)}
|
||||||
|
aria-valuenow={Math.round(hoverTime ?? 0)}
|
||||||
|
aria-valuetext={hoveredPoint && hoverTime !== null ? `${formatTime(hoverTime)}, 평균 점수 ${formatScore(hoveredPoint.averageScore)}, 최고 점수 ${formatScore(hoveredPoint.maximumScore)}` : '방향키로 시각을 선택하세요'}
|
||||||
|
tabindex="0"
|
||||||
|
on:pointermove={onPointerMove}
|
||||||
|
on:pointerleave={() => hoverTime = null}
|
||||||
|
on:focus={() => { if (hoverTime === null) { hoverTime = 0; tooltipPercent = LEFT / viewWidth * 100; } }}
|
||||||
|
on:blur={() => hoverTime = null}
|
||||||
|
on:keydown={onKeydown}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
viewBox={`0 0 ${viewWidth} ${HEIGHT}`}
|
||||||
|
role="img"
|
||||||
|
aria-label="시간에 따른 누적 평균 점수와 최고 점수 그래프"
|
||||||
|
>
|
||||||
|
{#each timeTicks as tick}
|
||||||
|
<line class="time-grid-line" x1={x(tick, viewWidth)} x2={x(tick, viewWidth)} y1={TOP} y2={HEIGHT - BOTTOM} />
|
||||||
|
{/each}
|
||||||
|
{#each yTicks as tick}
|
||||||
|
<line class="grid-line" x1={LEFT} x2={viewWidth - RIGHT} y1={y(tick)} y2={y(tick)} />
|
||||||
|
{/each}
|
||||||
|
<path class="score-line maximum-line" d={maximumPath} />
|
||||||
|
<path class="score-line average-line" d={averagePath} />
|
||||||
|
{#if hoveredPoint && hoverTime !== null}
|
||||||
|
<line class="cursor-line" x1={x(hoverTime, viewWidth)} x2={x(hoverTime, viewWidth)} y1={TOP} y2={HEIGHT - BOTTOM} />
|
||||||
|
<circle class="point maximum-point" cx={x(hoverTime, viewWidth)} cy={y(hoveredPoint.maximumScore)} r="5" />
|
||||||
|
<circle class="point average-point" cx={x(hoverTime, viewWidth)} cy={y(hoveredPoint.averageScore)} r="5" />
|
||||||
|
{/if}
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{#if hoveredPoint && hoverTime !== null}
|
||||||
|
<div class:align-right={tooltipPercent > 68} class="tooltip" style={`left: ${tooltipPercent}%;`} role="status">
|
||||||
|
<strong>{formatTime(hoverTime)}</strong>
|
||||||
|
<span><i class="average"></i>평균 점수 <b>{formatScore(hoveredPoint.averageScore)}</b></span>
|
||||||
|
<span><i class="maximum"></i>최고 점수 <b>{formatScore(hoveredPoint.maximumScore)}</b></span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.score-row { display: flex; min-height: 220px; margin-bottom: 8px; }
|
||||||
|
.lane-label { position: sticky; left: 0; z-index: 4; display: block; flex: 0 0 144px; min-width: 144px; padding: 0; color: #e1e5ec; background: #141920; font-size: 12px; line-height: 1.4; }
|
||||||
|
.label-heading { position: absolute; top: 50%; left: 12px; display: grid; gap: 7px; width: 88px; transform: translateY(-50%); }
|
||||||
|
.legend { display: flex; gap: 9px; color: #aab2bf; font-size: 10px; font-weight: 700; }
|
||||||
|
.legend span, .tooltip span { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||||
|
.axis-value { position: absolute; right: 7px; color: #747f90; font-size: 10px; font-weight: 650; transform: translateY(-50%); white-space: nowrap; }
|
||||||
|
.score-track { position: relative; flex: 1 0 auto; height: 220px; overflow: hidden; border-bottom: 1px solid #465062; background: #10141a; }
|
||||||
|
i { display: inline-block; flex: none; width: 8px; height: 8px; border-radius: 50%; }
|
||||||
|
i.average { background: #f08db8; }
|
||||||
|
i.maximum { background: #64cbe4; }
|
||||||
|
svg { display: block; width: 100%; height: 220px; pointer-events: none; }
|
||||||
|
.chart-wrap { outline: none; touch-action: pan-y; }
|
||||||
|
.chart-wrap:focus-visible { z-index: 3; outline: 2px solid #ed8bb5; outline-offset: -2px; }
|
||||||
|
.grid-line { stroke: rgb(255 255 255 / 9%); stroke-width: 1; }
|
||||||
|
.time-grid-line { stroke: rgb(255 255 255 / 7%); stroke-width: 1; }
|
||||||
|
.score-line { fill: none; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2.5; }
|
||||||
|
.maximum-line { stroke: #64cbe4; }
|
||||||
|
.average-line { stroke: #f08db8; }
|
||||||
|
.cursor-line { stroke: rgb(255 255 255 / 58%); stroke-dasharray: 5 4; stroke-width: 1.5; }
|
||||||
|
.point { stroke: #10141a; stroke-width: 3; }
|
||||||
|
.maximum-point { fill: #64cbe4; }
|
||||||
|
.average-point { fill: #f08db8; }
|
||||||
|
.tooltip { position: absolute; z-index: 5; top: 8px; display: grid; gap: 6px; min-width: 176px; padding: 9px 10px; border: 1px solid #465160; border-radius: 8px; color: #c5ccd6; background: rgb(18 23 30 / 96%); box-shadow: 0 8px 24px rgb(0 0 0 / 35%); font-size: 11px; pointer-events: none; transform: translateX(8px); }
|
||||||
|
.tooltip.align-right { transform: translateX(calc(-100% - 10px)); }
|
||||||
|
.tooltip strong { color: #fff; font-size: 12px; }
|
||||||
|
.tooltip b { margin-left: auto; color: #edf0f4; font-size: 12px; }
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.lane-label { flex-basis: 118px; min-width: 118px; padding: 0 8px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
import { createInitialReleaseClock, synchronizeReleaseClock } from '$lib/data/release-clock.ts';
|
import { createInitialReleaseClock, synchronizeReleaseClock } from '$lib/data/release-clock.ts';
|
||||||
import { releasedSongCharts } from '$lib/data/song-release.ts';
|
import { releasedSongCharts } from '$lib/data/song-release.ts';
|
||||||
import PresetManager from '$lib/components/PresetManager.svelte';
|
import PresetManager from '$lib/components/PresetManager.svelte';
|
||||||
|
import ScoreTimelineChart from '$lib/components/ScoreTimelineChart.svelte';
|
||||||
import SongPicker from '$lib/components/SongPicker.svelte';
|
import SongPicker from '$lib/components/SongPicker.svelte';
|
||||||
import { calculateBoardEffects, calculateExactScore, calculateUnitPower, countOptimizationCandidates, memoryParameterPermil, optimizeBoardConnectionCards, optimizeExactOrderAndCooldowns, perfectNoteScoreFromPower, toOptimizerCard } from '$lib/engine/index.ts';
|
import { calculateBoardEffects, calculateExactScore, calculateUnitPower, countOptimizationCandidates, memoryParameterPermil, optimizeBoardConnectionCards, optimizeExactOrderAndCooldowns, perfectNoteScoreFromPower, toOptimizerCard } from '$lib/engine/index.ts';
|
||||||
import type { CooldownReduction, EffectiveSkillWindow, ExactOptimizationResult, ExactSongChart, OptimizableConnectionCard, OptimizationProgress, ScoreAchievementProbabilities, SkillWindow } from '$lib/engine/index.ts';
|
import type { CooldownReduction, EffectiveSkillWindow, ExactOptimizationResult, ExactSongChart, OptimizableConnectionCard, OptimizationProgress, ScoreAchievementProbabilities, SkillWindow } from '$lib/engine/index.ts';
|
||||||
@@ -156,9 +157,7 @@
|
|||||||
$: perfectNoteScore = perfectNoteScoreFromPower(overallPower, selectedChart?.scoreRatio ?? null);
|
$: perfectNoteScore = perfectNoteScoreFromPower(overallPower, selectedChart?.scoreRatio ?? null);
|
||||||
$: scoreCalibrationLabel = selectedChart?.scoreCalibrationSource === 'measured' ? '실측값' : '추정값';
|
$: scoreCalibrationLabel = selectedChart?.scoreCalibrationSource === 'measured' ? '실측값' : '추정값';
|
||||||
$: timelineDuration = selectedChart?.durationSeconds ?? 0;
|
$: timelineDuration = selectedChart?.durationSeconds ?? 0;
|
||||||
$: noteCountsBySecond = result ? countNotesBySecond(result, timelineDuration) : [];
|
|
||||||
$: displayedEffectiveActiveWindows = mergeEffectiveActiveWindows(result?.effectiveActiveWindows ?? []);
|
$: displayedEffectiveActiveWindows = mergeEffectiveActiveWindows(result?.effectiveActiveWindows ?? []);
|
||||||
$: maxNotesPerSecond = Math.max(1, ...noteCountsBySecond);
|
|
||||||
$: timelineTicks = buildTimelineTicks(timelineDuration);
|
$: timelineTicks = buildTimelineTicks(timelineDuration);
|
||||||
$: timelineWidth = Math.max(720, timelineDuration * 6);
|
$: timelineWidth = Math.max(720, timelineDuration * 6);
|
||||||
$: candidateCount = (() => {
|
$: candidateCount = (() => {
|
||||||
@@ -691,21 +690,10 @@
|
|||||||
return Math.ceil(maximumScore * ratio).toLocaleString('ko-KR');
|
return Math.ceil(maximumScore * ratio).toLocaleString('ko-KR');
|
||||||
}
|
}
|
||||||
|
|
||||||
function countNotesBySecond(scoreResult: ExactOptimizationResult, durationSeconds: number): number[] {
|
|
||||||
const counts = Array.from({ length: Math.ceil(durationSeconds) }, () => 0);
|
|
||||||
for (const note of scoreResult.notes) {
|
|
||||||
const second = Math.floor(note.timeSeconds);
|
|
||||||
if (second >= 0 && second < counts.length) counts[second] += 1;
|
|
||||||
}
|
|
||||||
return counts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTimelineTicks(durationSeconds: number): number[] {
|
function buildTimelineTicks(durationSeconds: number): number[] {
|
||||||
if (durationSeconds <= 0) return [];
|
if (durationSeconds <= 0) return [];
|
||||||
const interval = 10;
|
const interval = 10;
|
||||||
const ticks = Array.from({ length: Math.floor(durationSeconds / interval) + 1 }, (_, index) => index * interval);
|
return Array.from({ length: Math.floor(durationSeconds / interval) + 1 }, (_, index) => index * interval);
|
||||||
if (ticks.at(-1) !== durationSeconds) ticks.push(durationSeconds);
|
|
||||||
return ticks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function timelinePosition(seconds: number): number {
|
function timelinePosition(seconds: number): number {
|
||||||
@@ -717,7 +705,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function timelineTrackStyle(): string {
|
function timelineTrackStyle(): string {
|
||||||
return `width: ${timelineWidth}px; --time-grid-step: ${timelinePosition(10)}%;`;
|
return `min-width: ${timelineWidth}px; --time-grid-step: ${timelinePosition(10)}%;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function memberColor(cardId: string): string {
|
function memberColor(cardId: string): string {
|
||||||
@@ -948,14 +936,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="timeline-scroll" role="region" aria-label="스킬 발동 시간대 그래프">
|
<div class="timeline-scroll" role="region" aria-label="스킬 발동 시간대 그래프">
|
||||||
<div class="timeline-canvas" style={`width: ${timelineWidth + 156}px;`}>
|
<div class="timeline-canvas" style={`width: ${timelineWidth + 156}px;`}>
|
||||||
<div class="timeline-row note-row">
|
<ScoreTimelineChart notes={result.notes} durationSeconds={timelineDuration} minimumWidth={timelineWidth} timeTicks={timelineTicks} />
|
||||||
<strong class="lane-label">초당 노트</strong>
|
|
||||||
<div class="timeline-track note-track" style={timelineTrackStyle()} aria-label={`최대 초당 ${maxNotesPerSecond}개`}>
|
|
||||||
{#each noteCountsBySecond as count, second}
|
|
||||||
<span class="note-bar" style={`left: ${timelinePosition(second)}%; width: ${100 / Math.max(1, noteCountsBySecond.length)}%; height: ${count / maxNotesPerSecond * 100}%; opacity: ${count === 0 ? 0 : 0.18 + count / maxNotesPerSecond * 0.82};`} title={`${second}~${second + 1}초 · ${count}개`}></span>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#each result.order as cardId, slot}
|
{#each result.order as cardId, slot}
|
||||||
<div class="timeline-row member-row">
|
<div class="timeline-row member-row">
|
||||||
<strong class="lane-label"><small style={`--member-color: ${memberColor(cardId)};`}>{slot + 1}</small>{resultCardName(cardId)}</strong>
|
<strong class="lane-label"><small style={`--member-color: ${memberColor(cardId)};`}>{slot + 1}</small>{resultCardName(cardId)}</strong>
|
||||||
@@ -1133,16 +1114,13 @@
|
|||||||
.timeline-legend span { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
.timeline-legend span { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
||||||
.timeline-legend i { display: inline-block; width: 9px; height: 9px; border-radius: 50%; }
|
.timeline-legend i { display: inline-block; width: 9px; height: 9px; border-radius: 50%; }
|
||||||
.timeline-scroll { overflow-x: auto; border: 1px solid #2c3440; border-radius: 11px; background: #10141a; scrollbar-width: thin; }
|
.timeline-scroll { overflow-x: auto; border: 1px solid #2c3440; border-radius: 11px; background: #10141a; scrollbar-width: thin; }
|
||||||
.timeline-canvas { min-width: 100%; padding: 12px 12px 8px 0; }
|
.timeline-canvas { min-width: 100%; padding: 12px 0 8px; }
|
||||||
.timeline-row { display: flex; min-height: 36px; }
|
.timeline-row { display: flex; min-height: 36px; }
|
||||||
.timeline-row + .timeline-row { border-top: 1px solid rgb(255 255 255 / 5%); }
|
.timeline-row + .timeline-row { border-top: 1px solid rgb(255 255 255 / 5%); }
|
||||||
.lane-label { position: sticky; left: 0; z-index: 4; display: flex; align-items: center; gap: 7px; flex: 0 0 144px; min-width: 144px; padding: 0 10px 0 12px; overflow: hidden; color: #bac2ce; background: #141920; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
.lane-label { position: sticky; left: 0; z-index: 4; display: flex; align-items: center; gap: 7px; flex: 0 0 144px; min-width: 144px; padding: 0 10px 0 12px; overflow: hidden; color: #bac2ce; background: #141920; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.lane-label small { display: grid; place-items: center; flex: none; width: 19px; height: 19px; border: 1px solid color-mix(in srgb, var(--member-color) 65%, transparent); border-radius: 5px; color: var(--member-color); background: color-mix(in srgb, var(--member-color) 12%, transparent); font-size: 11px; }
|
.lane-label small { display: grid; place-items: center; flex: none; width: 19px; height: 19px; border: 1px solid color-mix(in srgb, var(--member-color) 65%, transparent); border-radius: 5px; color: var(--member-color); background: color-mix(in srgb, var(--member-color) 12%, transparent); font-size: 11px; }
|
||||||
.timeline-track { position: relative; flex: none; }
|
.timeline-track { position: relative; flex: 1 0 auto; }
|
||||||
.timeline-track::before { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, rgb(255 255 255 / 9%) 1px, transparent 1px); background-size: var(--time-grid-step, 10%) 100%; pointer-events: none; }
|
.timeline-track::before { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, rgb(255 255 255 / 9%) 1px, transparent 1px); background-size: var(--time-grid-step, 10%) 100%; pointer-events: none; }
|
||||||
.note-row { min-height: 68px; }
|
|
||||||
.note-track { height: 52px; align-self: center; border-bottom: 1px solid #465062; }
|
|
||||||
.note-bar { position: absolute; bottom: 0; min-height: 1px; border-right: 1px solid #10141a; background: #8ca9d7; }
|
|
||||||
.skill-track { height: 36px; }
|
.skill-track { height: 36px; }
|
||||||
.skill-window { position: absolute; min-width: 2px; border-radius: 3px; background: var(--member-color); }
|
.skill-window { position: absolute; min-width: 2px; border-radius: 3px; background: var(--member-color); }
|
||||||
.active-window { top: 12px; height: 12px; opacity: .72; }
|
.active-window { top: 12px; height: 12px; opacity: .72; }
|
||||||
@@ -1189,6 +1167,6 @@
|
|||||||
.timeline-heading { align-items: flex-start; flex-direction: column; }
|
.timeline-heading { align-items: flex-start; flex-direction: column; }
|
||||||
.timeline-legend { justify-content: flex-start; }
|
.timeline-legend { justify-content: flex-start; }
|
||||||
.lane-label { flex-basis: 118px; min-width: 118px; }
|
.lane-label { flex-basis: 118px; min-width: 118px; }
|
||||||
.timeline-canvas { padding-right: 8px; }
|
.timeline-canvas { padding-right: 0; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user