feat: streamline search results and open captured analyses
Publish GitHub Pages output / publish (push) Successful in 54s
Publish GitHub Pages output / publish (push) Successful in 54s
This commit is contained in:
@@ -11,6 +11,8 @@
|
||||
import { createInitialReleaseClock, synchronizeReleaseClock } from '$lib/data/release-clock.ts';
|
||||
import { releasedSongCharts } from '$lib/data/song-release.ts';
|
||||
import AnalysisResultView from '$lib/components/AnalysisResultView.svelte';
|
||||
import { ANALYSIS_TRANSFER_KEY, combinationAnalysisSettings, type AnalysisTransfer } from '$lib/models/analysis-transfer.ts';
|
||||
import type { GrowthSnapshot } from '$lib/models/analysis-history.ts';
|
||||
import PresetManager from '$lib/components/PresetManager.svelte';
|
||||
import SongPicker from '$lib/components/SongPicker.svelte';
|
||||
import {
|
||||
@@ -82,6 +84,7 @@
|
||||
ownedCardsOnly?: boolean;
|
||||
maxMediumProbabilityCards?: number | null;
|
||||
maximizeCompute?: boolean;
|
||||
resultLimit?: number;
|
||||
leaderCharacterId: string;
|
||||
leaderOutfitCardId: string;
|
||||
};
|
||||
@@ -141,6 +144,8 @@
|
||||
let songScoreBonusOverride: number | null = 10;
|
||||
let ownedCardsOnly = false;
|
||||
let maxMediumProbabilityCards: number | null = null;
|
||||
let resultLimit = 10;
|
||||
let activeResultLimit = 10;
|
||||
let maximizeCompute = false;
|
||||
let leaderCharacterId = leaderMembers[0]?.characterId ?? '';
|
||||
let leaderOutfitCardId = leaderMembers[0]?.id ?? '';
|
||||
@@ -170,6 +175,43 @@
|
||||
let ready = false;
|
||||
let loadedUserData: string | null | undefined;
|
||||
let userDataChanged = false;
|
||||
let searchSnapshot: { conditions: SavedState; growth: GrowthSnapshot; title: string; durationSeconds: number; analyzedAt: string } | null = null;
|
||||
let baselineTransfer: Omit<AnalysisTransfer, 'result' | 'achievementProbabilities'> | null = null;
|
||||
let transferError = '';
|
||||
|
||||
function openInAnalysis(event: MouseEvent, ranking: DisplayRankedResult): void {
|
||||
event.preventDefault();
|
||||
transferError = '';
|
||||
try {
|
||||
let transfer: AnalysisTransfer;
|
||||
if (ranking.isBaseline) {
|
||||
if (!baselineTransfer) throw new Error('기준 조합 분석이 완료된 뒤 다시 열어 주세요.');
|
||||
transfer = { ...baselineTransfer, result: ranking.result, achievementProbabilities: ranking.achievementProbabilities };
|
||||
} else {
|
||||
if (!searchSnapshot || !activeSearchChart || !ranking.cards) throw new Error('탐색이 완료된 뒤 다시 열어 주세요.');
|
||||
transfer = {
|
||||
schemaVersion: 1,
|
||||
capture: {
|
||||
settings: combinationAnalysisSettings(searchSnapshot.conditions, ranking.result.formationOrder, searchSnapshot.growth),
|
||||
growth: searchSnapshot.growth, title: searchSnapshot.title, durationSeconds: searchSnapshot.durationSeconds,
|
||||
analyzedAt: searchSnapshot.analyzedAt, overallPower: ranking.overallPower
|
||||
},
|
||||
result: ranking.result, cards: ranking.cards, chart: activeSearchChart,
|
||||
directSongScoreBonusPercent: ranking.directSongScoreBonusPercent ?? 0,
|
||||
achievementProbabilities: ranking.achievementProbabilities
|
||||
};
|
||||
}
|
||||
// A same-origin new tab copies sessionStorage at creation; the search tab stays intact.
|
||||
sessionStorage.setItem(ANALYSIS_TRANSFER_KEY, JSON.stringify(transfer));
|
||||
const opened = window.open((event.currentTarget as HTMLAnchorElement).href, '_blank');
|
||||
if (!opened) throw new Error('새 탭이 차단되었습니다. 팝업을 허용한 뒤 다시 열어 주세요.');
|
||||
opened.opener = null;
|
||||
} catch (error) {
|
||||
transferError = error instanceof Error ? error.message : '배치 분석을 열지 못했습니다.';
|
||||
} finally {
|
||||
try { sessionStorage.removeItem(ANALYSIS_TRANSFER_KEY); } catch { /* Storage may be blocked. */ }
|
||||
}
|
||||
}
|
||||
|
||||
function loadUserData(): void {
|
||||
const raw = localStorage.getItem(USER_DATA_STORAGE_KEY);
|
||||
@@ -184,6 +226,7 @@
|
||||
}
|
||||
|
||||
function resetBaseline(): void {
|
||||
baselineTransfer = null;
|
||||
baselineWorker?.terminate();
|
||||
const nextWorker = createScoreOptimizerWorker();
|
||||
baselineWorker = nextWorker;
|
||||
@@ -226,7 +269,8 @@
|
||||
overallPower: baselineOverallPower,
|
||||
achievementProbabilities: baselineAchievementProbabilities,
|
||||
isBaseline: true
|
||||
} : null
|
||||
} : null,
|
||||
activeResultLimit
|
||||
);
|
||||
$: if (ready && connectionCards && baselineState && baselineMatchesConditions && !baselineResult && !baselineRunning && !baselineError) void analyzeBaseline();
|
||||
$: if (!result && rankedResults.length > 0) rankedResults = [];
|
||||
@@ -262,6 +306,7 @@
|
||||
songScoreBonusOverride,
|
||||
ownedCardsOnly,
|
||||
maxMediumProbabilityCards,
|
||||
resultLimit,
|
||||
maximizeCompute,
|
||||
leaderCharacterId,
|
||||
leaderOutfitCardId
|
||||
@@ -280,6 +325,7 @@
|
||||
songScoreBonusOverride,
|
||||
ownedCardsOnly,
|
||||
maxMediumProbabilityCards,
|
||||
resultLimit,
|
||||
maximizeCompute,
|
||||
leaderCharacterId,
|
||||
leaderOutfitCardId
|
||||
@@ -416,6 +462,7 @@
|
||||
? null
|
||||
: normalizeSongScoreBonusOverride(state.songScoreBonusOverride) ?? 10;
|
||||
ownedCardsOnly = state.ownedCardsOnly === true;
|
||||
resultLimit = [10, 20, 30, 50].includes(state.resultLimit ?? 10) ? state.resultLimit ?? 10 : 10;
|
||||
maxMediumProbabilityCards = typeof state.maxMediumProbabilityCards === 'number' &&
|
||||
Number.isInteger(state.maxMediumProbabilityCards) && state.maxMediumProbabilityCards >= 0 && state.maxMediumProbabilityCards < 5
|
||||
? state.maxMediumProbabilityCards : null;
|
||||
@@ -902,6 +949,13 @@
|
||||
});
|
||||
const chart = await loadExactSongChart(chartMetadata.path);
|
||||
if (baselineWorker !== targetWorker) return;
|
||||
baselineTransfer = structuredClone({
|
||||
schemaVersion: 1,
|
||||
capture: { settings: state, growth: { collection, memberBoards, boardCards, memoryCount, memberPowerUpPercent },
|
||||
title: chartMetadata.title, durationSeconds: chartMetadata.durationSeconds, overallPower: power.overallPower,
|
||||
analyzedAt: new Date().toISOString() },
|
||||
cards: optimizerCards, chart, directSongScoreBonusPercent: boardEffects.directSongScoreBonusPercent
|
||||
});
|
||||
baselineOverallPower = power.overallPower;
|
||||
baselineProgress = { evaluatedCandidates: 0, totalCandidates: countOptimizationCandidates(optimizerCards) };
|
||||
baselineWorker.postMessage({
|
||||
@@ -951,7 +1005,7 @@
|
||||
bestByCombination.set(key, ranking);
|
||||
}
|
||||
}
|
||||
rankedResults = Array.from(bestByCombination.values()).sort(compareRankedResults).slice(0, 10);
|
||||
rankedResults = Array.from(bestByCombination.values()).sort(compareRankedResults).slice(0, activeResultLimit);
|
||||
result = rankedResults[0]?.result ?? null;
|
||||
if (rankedResults.length > 0 && activeSearchChart && worker && rankedResults.every((ranking) => ranking.cards)) {
|
||||
searchPhase = 'probability';
|
||||
@@ -976,7 +1030,7 @@
|
||||
exactPass = 'refinement';
|
||||
exactCombinationCount += refinements.length;
|
||||
if (maximizeCompute) runParallelExact(refinements, activeSearchChart);
|
||||
else worker?.postMessage({ type: 'optimize-combinations', candidates: refinements, chart: activeSearchChart });
|
||||
else worker?.postMessage({ type: 'optimize-combinations', candidates: refinements, chart: activeSearchChart, resultLimit: activeResultLimit });
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1028,12 +1082,12 @@
|
||||
if (completedWorkers !== workerCount) return;
|
||||
completedRankings.sort(compareRankedResults);
|
||||
parallelWorkers = [];
|
||||
completeExactPass(completedRankings.slice(0, 10));
|
||||
completeExactPass(completedRankings.slice(0, activeResultLimit));
|
||||
}
|
||||
if (event.data.type === 'error') fail(event.data.message);
|
||||
};
|
||||
parallelWorker.onerror = () => fail('병렬 분석 Worker에서 오류가 발생했습니다.');
|
||||
parallelWorker.postMessage({ type: 'optimize-combinations', candidates, chart });
|
||||
parallelWorker.postMessage({ type: 'optimize-combinations', candidates, chart, resultLimit: activeResultLimit });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1046,6 +1100,7 @@
|
||||
if (previousUserData !== loadedUserData) resetBaseline();
|
||||
// Rebuild reactive connection cards and candidate ownership before searching.
|
||||
await tick();
|
||||
if (running) return;
|
||||
} catch {
|
||||
errorMessage = '사용자 데이터를 읽지 못했습니다. 사용자 데이터 페이지에서 저장 상태를 확인해 주세요.';
|
||||
return;
|
||||
@@ -1065,6 +1120,10 @@
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
activeResultLimit = resultLimit;
|
||||
searchSnapshot = structuredClone({ conditions: presetSnapshot,
|
||||
growth: { collection, memberBoards, boardCards, memoryCount, memberPowerUpPercent },
|
||||
title: selectedChart.title, durationSeconds: selectedChart.durationSeconds, analyzedAt: new Date().toISOString() });
|
||||
searchPhase = 'screening';
|
||||
exactPass = 'primary';
|
||||
activeSearchChart = null;
|
||||
@@ -1090,7 +1149,7 @@
|
||||
activeSearchChart = chart;
|
||||
searchPhase = 'exact';
|
||||
if (maximizeCompute) runParallelExact(finalists, chart);
|
||||
else worker.postMessage({ type: 'optimize-combinations', candidates: finalists, chart });
|
||||
else worker.postMessage({ type: 'optimize-combinations', candidates: finalists, chart, resultLimit: activeResultLimit });
|
||||
} catch (error) {
|
||||
errorMessage = error instanceof Error ? error.message : '조합 탐색을 준비하지 못했습니다.';
|
||||
running = false;
|
||||
@@ -1141,7 +1200,8 @@
|
||||
|
||||
function buildDisplayedRankings(
|
||||
rankings: readonly RankedResult[],
|
||||
baseline: DisplayRankedResult | null
|
||||
baseline: DisplayRankedResult | null,
|
||||
limit = 10
|
||||
): DisplayRankedResult[] {
|
||||
if (rankings.length === 0) return [];
|
||||
const unique = new Map<string, DisplayRankedResult>();
|
||||
@@ -1155,7 +1215,7 @@
|
||||
}
|
||||
return Array.from(unique.values())
|
||||
.sort((left, right) => compareRankedResults(left, right) || Number(right.isBaseline) - Number(left.isBaseline))
|
||||
.slice(0, 10);
|
||||
.slice(0, limit);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1203,6 +1263,7 @@
|
||||
</section>
|
||||
{/if}
|
||||
<section class="panel" aria-labelledby="conditions-title">
|
||||
<fieldset class="search-inputs" disabled={running}>
|
||||
<div class="section-title"><div><span>01</span><h2 id="conditions-title">탐색 조건</h2></div><strong>{ownedCardsOnly ? (eventMode ? '보유 이벤트 후보 대상' : '보유 5성 대상') : (eventMode ? '5성 + 대상 멤버 전 레어리티' : '5성 전체 대상')}</strong></div>
|
||||
<div class="song-fields">
|
||||
<div class="song-picker-field"><SongPicker {charts} {difficulty} bind:value={selectedSongId} on:change={() => result = null} /></div>
|
||||
@@ -1288,6 +1349,12 @@
|
||||
</div>
|
||||
<p class="hint">기수 보너스와 가창 보너스 덮어쓰기는 빈칸이면 저장된 실제 보드를 사용하고, 숫자를 입력하면 해당 값으로 대체합니다. 자동 후보는 파란 노드 전체 활성화·기수 보너스 {candidateGenerationBonus === null ? '저장값' : candidateGenerationBonus}·가창 보너스 {songScoreBonusOverride === null ? '실제 보드값' : `${songScoreBonusOverride}%`} 조건으로 비교합니다.</p>
|
||||
<PresetManager storageKey={PRESET_STORAGE_KEY} snapshot={presetSnapshot} disabled={running} on:apply={applyPreset} />
|
||||
<label class="result-limit">결과 출력 수
|
||||
<select bind:value={resultLimit} disabled={running} aria-describedby="result-limit-guide">
|
||||
{#each [10, 20, 30, 50] as count}<option value={count}>상위 {count}개</option>{/each}
|
||||
</select>
|
||||
</label>
|
||||
<p id="result-limit-guide" class="candidate-guide">다음 탐색부터 적용합니다. 결과 수를 늘리면 달성 확률 계산 시간이 늘어납니다. 분석한 조합이 적으면 선택한 수보다 적게 표시됩니다.</p>
|
||||
{#if errorMessage}<p class="error" role="alert">{errorMessage}</p>{/if}
|
||||
<button class="search" type="button" disabled={running} on:click={search}>{running ? searchPhase === 'screening' ? `후보 선별 중 ${Math.round(screeningProgress * 100)}%` : searchPhase === 'probability' ? `달성 확률 계산 중 ${Math.round(progress.evaluatedCandidates / Math.max(1, progress.totalCandidates) * 100)}%` : `정밀 분석 중 ${Math.round(progress.evaluatedCandidates / Math.max(1, progress.totalCandidates) * 100)}%` : '최적 조합 탐색'}</button>
|
||||
<label class="compute-option">
|
||||
@@ -1297,17 +1364,21 @@
|
||||
</label>
|
||||
<p class="compute-warning">탐색 속도는 빨라지지만, 분석 중 브라우저 및 기기 사용이 불편할 수 있습니다.</p>
|
||||
{#if running}<progress value={searchPhase === 'screening' ? screeningProgress : progress.evaluatedCandidates} max={searchPhase === 'screening' ? 1 : progress.totalCandidates}></progress>{/if}
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<section class="panel" aria-labelledby="result-title">
|
||||
<div class="section-title"><div><span>02</span><h2 id="result-title">탐색 결과</h2></div>{#if result}<strong>{screenedCombinationCount.toLocaleString('ko-KR')}개 선별 · {exactCombinationCount}개 정밀 분석 · {comparableToBaseline ? '기준 조합 포함 ' : ''}상위 {displayedRankings.length}개</strong>{/if}</div>
|
||||
{#if userDataChanged}<p class="baseline-guide" role="status">사용자 데이터가 변경되었습니다. 진행 중인 탐색과 아래 결과는 이전 데이터 기준이며, 다시 탐색하면 최신 데이터를 적용합니다.</p>{/if}
|
||||
{#if !eventMode}<p class="baseline-guide">상세 점수·그래프·액티브 미발동 비교는 각 결과의 ‘배치 분석에서 보기’를 눌러 새 탭에서 확인할 수 있습니다.</p>{/if}
|
||||
{#if transferError}<p class="error" role="alert">{transferError}</p>{/if}
|
||||
{#if displayedRankings.length > 0}
|
||||
<div class="ranking-list">
|
||||
{#each displayedRankings as ranking, rank}
|
||||
<details class:baseline-ranking={ranking.isBaseline} class="ranking-card" open={rank === 0}>
|
||||
<summary>
|
||||
<svelte:element this={eventMode ? 'details' : 'article'} class:baseline-ranking={ranking.isBaseline} class="ranking-card" open={eventMode && rank === 0}>
|
||||
<svelte:element this={eventMode ? 'summary' : 'div'} class="ranking-summary">
|
||||
<span class="rank-number">{rank + 1}위</span>
|
||||
<div class="rank-team">
|
||||
<span class="rank-members">
|
||||
{#if ranking.isBaseline}<b class="baseline-result-label">기준 조합</b>{/if}
|
||||
{#each ranking.result.formationOrder as cardId, memberIndex}
|
||||
@@ -1318,17 +1389,25 @@
|
||||
· +{displayedCardAdditionalAwakenings(cardId, ranking.isBaseline)}회
|
||||
{/if}
|
||||
</small>
|
||||
<span class="member-name">{resultCardName(cardId)}</span>
|
||||
<span class="member-name">{cardById.get(cardId)?.name ?? cardId}</span>
|
||||
{#if cardById.get(cardId)?.versionName}<small class="member-tag">{cardById.get(cardId)?.versionName}</small>{/if}
|
||||
</span>
|
||||
{#if memberIndex < ranking.result.formationOrder.length - 1}<i aria-hidden="true">·</i>{/if}
|
||||
{/each}
|
||||
</span>
|
||||
</div>
|
||||
{#if !eventMode}
|
||||
<div class="team-meta">
|
||||
<b class:complete={displayedAdditionalAwakenings(ranking) === 0}>추가 개화 {displayedAdditionalAwakenings(ranking)}회</b>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="rank-metric">
|
||||
<small>{eventMode ? '평균 이벤트 Pt 지수' : '최고 점수'}</small>
|
||||
<strong>{eventMode ? eventPointIndex(ranking.result.expectedScore, ranking.eventBonusPercent ?? 0).toLocaleString('ko-KR') : ranking.result.theoreticalMaxScore.toLocaleString('ko-KR')}</strong>
|
||||
{#if eventMode}<i>획득량 +{ranking.eventBonusPercent ?? 0}% · 평균 점수 {ranking.result.expectedScore.toLocaleString('ko-KR')}</i>{/if}
|
||||
{#if comparableToBaseline && !ranking.isBaseline}<i>{scoreDelta(ranking.result.theoreticalMaxScore, baselineResult!.theoreticalMaxScore)}</i>{/if}
|
||||
<b class:complete={displayedAdditionalAwakenings(ranking) === 0}>추가 개화 {displayedAdditionalAwakenings(ranking)}회</b>
|
||||
{#if eventMode}<b class:complete={displayedAdditionalAwakenings(ranking) === 0}>추가 개화 {displayedAdditionalAwakenings(ranking)}회</b>
|
||||
{:else}<small class="expected-score">예상 {ranking.result.expectedScore.toLocaleString('ko-KR')}</small>{/if}
|
||||
</span>
|
||||
<span class="rank-probabilities">
|
||||
<small>최고점 대비 달성 확률</small>
|
||||
@@ -1336,7 +1415,9 @@
|
||||
<span><b>99% 이상</b><strong>{probabilityLabel(ranking.achievementProbabilities?.atLeast99Percent)}</strong><i>{scoreThreshold(ranking.result.theoreticalMaxScore, 0.99)}점</i></span>
|
||||
<span><b>95% 이상</b><strong>{probabilityLabel(ranking.achievementProbabilities?.atLeast95Percent)}</strong><i>{scoreThreshold(ranking.result.theoreticalMaxScore, 0.95)}점</i></span>
|
||||
</span>
|
||||
</summary>
|
||||
{#if !eventMode && !running && (!ranking.isBaseline || !baselineRunning)}<a class="analysis-link" href="/analysis" on:click={(event) => openInAnalysis(event, ranking)}>배치 분석에서 보기 ↗</a>{/if}
|
||||
</svelte:element>
|
||||
{#if eventMode}
|
||||
<div class="ranking-detail">
|
||||
<AnalysisResultView
|
||||
result={ranking.result}
|
||||
@@ -1347,7 +1428,8 @@
|
||||
showScoreSummary={false}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
{/if}
|
||||
</svelte:element>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="awakening-guide">추가 개화는 현재 카드 데이터와 조합의 목표 개화를 비교한 값입니다. 미보유 카드를 처음 얻는 과정도 1회로 계산합니다.</p>
|
||||
@@ -1360,6 +1442,8 @@
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.result-limit { max-width: 220px; margin-top: 16px; }
|
||||
.search-inputs { min-width: 0; margin: 0; padding: 0; border: 0; }
|
||||
main { width: min(var(--page-max-width), calc(100% - var(--page-gutter))); margin: 0 auto; padding: var(--page-content-top) 0 80px; }
|
||||
header { margin-bottom: var(--page-header-bottom); }
|
||||
h1 { margin: 0; color: #f3f5f8; font-size: var(--page-title-size); line-height: 1.15; letter-spacing: -.045em; }
|
||||
@@ -1450,17 +1534,23 @@
|
||||
progress { display: block; width: 100%; height: 4px; margin-top: 8px; accent-color: #ed8bb5; }
|
||||
.ranking-list { display: grid; gap: 10px; margin-top: 18px; }
|
||||
.ranking-card { overflow: hidden; border: 1px solid #303744; border-radius: 12px; background: #12161c; }
|
||||
.ranking-card[open] { border-color: #55404f; background: #15151b; }
|
||||
.ranking-card.baseline-ranking { border-color: #665061; background: #1b171d; }
|
||||
.ranking-card summary { position: relative; display: grid; grid-template-columns: 60px minmax(0, 1fr) clamp(150px, 12vw, 200px) clamp(280px, 23vw, 360px); align-items: center; gap: 18px; min-height: 80px; padding: 12px 46px 12px 18px; cursor: pointer; list-style: none; }
|
||||
.ranking-card summary::-webkit-details-marker { display: none; }
|
||||
.ranking-card summary::after { content: '⌄'; position: absolute; top: 50%; right: 15px; color: #858f9f; font-size: 16px; transform: translateY(-50%); transition: transform .15s; }
|
||||
.ranking-card[open] summary::after { transform: translateY(-50%) rotate(180deg); }
|
||||
.ranking-summary { display: grid; grid-template-columns: 60px minmax(0, 1fr) clamp(150px, 12vw, 200px) clamp(280px, 23vw, 360px); align-items: center; gap: 18px; min-height: 80px; padding: 12px 18px; }
|
||||
details.ranking-card > .ranking-summary { cursor: pointer; }
|
||||
article.ranking-card > .ranking-summary { grid-template-columns: 48px minmax(0, 1.5fr) max-content 130px minmax(260px, 1fr) max-content; gap: 18px; padding: 14px 18px; }
|
||||
article.ranking-card .rank-members { align-items: flex-start; }
|
||||
.rank-team { min-width: 0; }
|
||||
.team-meta { display: grid; justify-items: start; gap: 6px; color: #9ca5b3; font-size: 12px; }
|
||||
.team-meta > b { white-space: nowrap; }
|
||||
.team-meta b { padding: 2px 6px; border-radius: 5px; color: #f0b0c9; background: #30212a; font-size: 10px; }
|
||||
.team-meta b.complete { color: #9edbb7; background: #1e3028; }
|
||||
.rank-metric .expected-score { font-size: 12px; font-weight: 500; }
|
||||
.rank-number { display: grid; place-items: center; width: 48px; height: 34px; border-radius: 8px; color: #171019; background: #ed8bb5; font-size: 14px; font-weight: 900; }
|
||||
.rank-members { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-width: 0; color: #dce1e8; font-size: 13px; font-weight: 800; line-height: 1.55; }
|
||||
.rank-members > span { display: inline-grid; justify-items: center; gap: 1px; white-space: nowrap; }
|
||||
.member-awakening { color: #8d96a5; font-size: 10px; font-weight: 750; line-height: 1.2; }
|
||||
.member-name { line-height: 1.35; }
|
||||
.member-tag { margin-top: 3px; padding: 1px 6px; border: 1px solid #394150; border-radius: 5px; color: #aeb8c8; background: #202630; font-size: 10px; font-weight: 600; line-height: 1.4; }
|
||||
.rank-members .added-member .member-awakening { color: #c987a4; }
|
||||
.rank-members > i { flex: 0 0 auto; color: #596270; font-style: normal; }
|
||||
.rank-members .added-member { color: #f3a5c6; }
|
||||
@@ -1482,6 +1572,9 @@
|
||||
.awakening-guide { color: #aa8797; }
|
||||
.awakening-guide + .probability-guide { margin-top: 5px; }
|
||||
.ranking-detail { padding: 0 16px 18px; border-top: 1px solid #2b313c; }
|
||||
.analysis-link { display: inline-block; margin-left: auto; padding: 5px 9px; border: 1px solid #694256; border-radius: 6px; background: #32202b; color: #f2a3c4; font-size: 12px; font-weight: 750; text-decoration: none; white-space: nowrap; }
|
||||
.analysis-link:hover { background: #46303b; }
|
||||
.analysis-link:focus-visible { outline: 2px solid #f2a3c4; outline-offset: 3px; }
|
||||
.scores { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 14px; }
|
||||
.scores div { display: grid; gap: 7px; padding: 15px; border-radius: 10px; background: #22202a; }
|
||||
.scores span { color: #a9b1be; font-size: 14px; }
|
||||
@@ -1489,6 +1582,12 @@
|
||||
.baseline-scores { margin-top: 10px; }
|
||||
.empty { display: grid; place-items: center; min-height: 150px; margin-top: 18px; padding: 28px; border: 1px dashed #303744; border-radius: 12px; color: #858f9f; font-size: 14px; text-align: center; }
|
||||
@media (max-width: 1100px) {
|
||||
article.ranking-card > .ranking-summary { grid-template-columns: 48px minmax(0, 1fr) 280px; }
|
||||
article.ranking-card .rank-team { grid-column: 2 / -1; }
|
||||
article.ranking-card .team-meta { grid-column: 2; grid-row: 2; }
|
||||
article.ranking-card .rank-metric { grid-column: 3; grid-row: 2; }
|
||||
article.ranking-card .rank-probabilities { grid-column: 2 / -1; grid-row: 3; }
|
||||
.analysis-link { grid-column: 2 / -1; grid-row: 4; }
|
||||
.fixed-column-headings,
|
||||
.fixed-field { grid-template-columns: minmax(0, 1fr) 120px 304px 180px; }
|
||||
}
|
||||
@@ -1505,11 +1604,15 @@
|
||||
.leader-fields, .scores { grid-template-columns: 1fr; }
|
||||
.outfit-effect { grid-template-columns: 1fr; gap: 5px; }
|
||||
.baseline-members { grid-template-columns: 1fr; }
|
||||
.ranking-card summary { grid-template-columns: 48px minmax(0, 1fr); gap: 12px 10px; padding: 13px 34px 13px 12px; }
|
||||
.ranking-summary, article.ranking-card > .ranking-summary { grid-template-columns: 48px minmax(0, 1fr); gap: 10px; padding: 12px; }
|
||||
.rank-number { grid-column: 1; grid-row: 1; }
|
||||
.rank-members { grid-column: 2; grid-row: 1; }
|
||||
.rank-metric:nth-of-type(3) { grid-column: 2; grid-row: 2; }
|
||||
.rank-probabilities { grid-column: 1 / -1; grid-row: 3; padding: 10px 0 0; border-top: 1px solid #303744; border-left: 0; }
|
||||
.rank-team, article.ranking-card .rank-team { grid-column: 2; grid-row: 1; }
|
||||
.rank-metric { grid-column: 2; grid-row: 2; }
|
||||
article.ranking-card .rank-metric { grid-column: 2; grid-row: 2; }
|
||||
.rank-probabilities, article.ranking-card .rank-probabilities { grid-column: 1 / -1; grid-row: 3; padding: 10px 0 0; border-top: 1px solid #303744; border-left: 0; }
|
||||
article.ranking-card .team-meta { grid-column: 1 / -1; grid-row: 3; display: flex; flex-wrap: wrap; gap: 8px; font-size: 11px; }
|
||||
article.ranking-card .rank-probabilities { grid-row: 4; }
|
||||
.analysis-link { grid-column: 1 / -1; grid-row: 5; }
|
||||
.ranking-detail { padding: 0 10px 14px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user