This commit is contained in:
+7
-1
@@ -1,7 +1,13 @@
|
|||||||
---
|
---
|
||||||
updatedAt: 2026-10-04T15:00:00.000Z
|
updatedAt: 2026-10-06T02:32:20.545Z
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026년 10월 6일
|
||||||
|
|
||||||
|
### 선택 순서대로 악곡 목록 만들기
|
||||||
|
|
||||||
|
`NEW` 도구에 악곡 목록을 추가했습니다. 악곡을 검색하고 난이도를 골라 선택한 순서대로 목록을 만들 수 있습니다. 같은 악곡·난이도를 여러 번 추가하거나 개별 삭제·목록 초기화를 할 수 있으며, 번호·곡명·난이도·레벨을 목록 순서대로 복사할 수 있습니다. 목록은 브라우저에 자동 저장되어 새로고침하거나 다시 방문해도 선택 순서와 난이도가 유지됩니다.
|
||||||
|
|
||||||
## 2026년 10월 5일
|
## 2026년 10월 5일
|
||||||
|
|
||||||
### KONKON Beats 악곡 추가
|
### KONKON Beats 악곡 추가
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
import HoloWorkPage from '../src/routes/tools/holo-work/+page.svelte';
|
import HoloWorkPage from '../src/routes/tools/holo-work/+page.svelte';
|
||||||
import BoardAwakeningPage from '../src/routes/tools/board-awakening/+page.svelte';
|
import BoardAwakeningPage from '../src/routes/tools/board-awakening/+page.svelte';
|
||||||
import ToolsPage from '../src/routes/tools/+page.svelte';
|
import ToolsPage from '../src/routes/tools/+page.svelte';
|
||||||
|
import SongListPage from '../src/routes/tools/song-list/+page.svelte';
|
||||||
import ComparePage from '../src/routes/compare/+page.svelte';
|
import ComparePage from '../src/routes/compare/+page.svelte';
|
||||||
import EventPage from '../src/routes/event/+page.svelte';
|
import EventPage from '../src/routes/event/+page.svelte';
|
||||||
import DataPage from '../src/routes/data/+page.svelte';
|
import DataPage from '../src/routes/data/+page.svelte';
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
['/tools/ai', AiPage],
|
['/tools/ai', AiPage],
|
||||||
['/compare', ComparePage],
|
['/compare', ComparePage],
|
||||||
['/tools/random', ToolsPage],
|
['/tools/random', ToolsPage],
|
||||||
|
['/tools/song-list', SongListPage],
|
||||||
['/tools/holo-work', HoloWorkPage],
|
['/tools/holo-work', HoloWorkPage],
|
||||||
['/tools/board-awakening', BoardAwakeningPage],
|
['/tools/board-awakening', BoardAwakeningPage],
|
||||||
['/random', ToolsPage],
|
['/random', ToolsPage],
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let active: 'random' | 'holo-work' | 'board-awakening' | 'ai';
|
export let active: 'random' | 'song-list' | 'holo-work' | 'board-awakening' | 'ai';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav aria-label="도구 메뉴">
|
<nav aria-label="도구 메뉴">
|
||||||
<a href="/tools/random" aria-current={active === 'random' ? 'page' : undefined}>악곡 랜덤</a>
|
<a href="/tools/random" aria-current={active === 'random' ? 'page' : undefined}>악곡 랜덤</a>
|
||||||
|
<a href="/tools/song-list" aria-current={active === 'song-list' ? 'page' : undefined}>악곡 목록</a>
|
||||||
<a href="/tools/holo-work" aria-current={active === 'holo-work' ? 'page' : undefined}>홀로워크 관리</a>
|
<a href="/tools/holo-work" aria-current={active === 'holo-work' ? 'page' : undefined}>홀로워크 관리</a>
|
||||||
<a href="/tools/board-awakening" aria-current={active === 'board-awakening' ? 'page' : undefined}>연결노드 개화 추천</a>
|
<a href="/tools/board-awakening" aria-current={active === 'board-awakening' ? 'page' : undefined}>연결노드 개화 추천</a>
|
||||||
<a href="/tools/ai" aria-current={active === 'ai' ? 'page' : undefined}>AI 연결</a>
|
<a href="/tools/ai" aria-current={active === 'ai' ? 'page' : undefined}>AI 연결</a>
|
||||||
@@ -15,4 +16,8 @@
|
|||||||
a:hover { color: #dfe4eb; background: rgb(255 255 255 / 4%); }
|
a:hover { color: #dfe4eb; background: rgb(255 255 255 / 4%); }
|
||||||
a[aria-current='page'] { color: #f3a5c5; background: rgb(239 139 182 / 12%); }
|
a[aria-current='page'] { color: #f3a5c5; background: rgb(239 139 182 / 12%); }
|
||||||
a:focus-visible { outline: 3px solid #ed8bb5; outline-offset: 3px; }
|
a:focus-visible { outline: 3px solid #ed8bb5; outline-offset: 3px; }
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
nav { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
a:last-child { grid-column: 1 / -1; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { isDifficulty } from '../data/difficulty.ts';
|
||||||
|
import type { SongChartSummary } from '../data/song-catalog.ts';
|
||||||
|
|
||||||
|
export const SONG_LIST_STORAGE_KEY = 'holodori.song-list.v1';
|
||||||
|
|
||||||
|
export function serializeSongList(charts: readonly SongChartSummary[]): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
schemaVersion: 1,
|
||||||
|
songs: charts.map(({ songId, difficulty }) => ({ songId, difficulty }))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSavedSongList<T extends SongChartSummary>(text: string, catalog: readonly T[]): { charts: T[]; unavailableCount: number } {
|
||||||
|
const value = JSON.parse(text);
|
||||||
|
if (!value || value.schemaVersion !== 1 || !Array.isArray(value.songs)) {
|
||||||
|
throw new Error('올바른 악곡 목록이 아닙니다.');
|
||||||
|
}
|
||||||
|
const charts: T[] = [];
|
||||||
|
let unavailableCount = 0;
|
||||||
|
for (const song of value.songs) {
|
||||||
|
if (!song || typeof song.songId !== 'string' || !isDifficulty(song.difficulty)) {
|
||||||
|
throw new Error('올바른 악곡·난이도가 아닙니다.');
|
||||||
|
}
|
||||||
|
const chart = catalog.find((chart) => chart.songId === song.songId && chart.difficulty === song.difficulty);
|
||||||
|
if (chart) charts.push(chart);
|
||||||
|
else unavailableCount++;
|
||||||
|
}
|
||||||
|
return { charts, unavailableCount };
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a class="feature-card" href="/tools">
|
<a class="feature-card" href="/tools">
|
||||||
<span class="feature-number">06</span>
|
<span class="feature-number">06</span>
|
||||||
<div><h3>도구</h3><p>악곡과 난이도를 랜덤으로 뽑거나, 홀로워크에 보낸 횟수를 멤버별로 기록하고 확인합니다.</p></div>
|
<div><h3>도구</h3><p>악곡을 랜덤으로 뽑거나 고른 순서대로 목록을 만들고, 홀로워크에 보낸 횟수를 멤버별로 기록합니다.</p></div>
|
||||||
<span class="arrow" aria-hidden="true">→</span>
|
<span class="arrow" aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, tick } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import ToolsNav from '$lib/components/ToolsNav.svelte';
|
||||||
|
import exactChartCatalog from '$lib/data/exact-chart-catalog.json';
|
||||||
|
import { difficulties, difficultyLabel } from '$lib/data/difficulty.ts';
|
||||||
|
import { createInitialReleaseClock, synchronizeReleaseClock } from '$lib/data/release-clock.ts';
|
||||||
|
import { songOptions } from '$lib/data/song-catalog.ts';
|
||||||
|
import { releasedSongCharts } from '$lib/data/song-release.ts';
|
||||||
|
import { formatRandomSongResults } from '$lib/engine/random-song.ts';
|
||||||
|
import { SONG_LIST_STORAGE_KEY, parseSavedSongList, serializeSongList } from '$lib/storage/song-list.ts';
|
||||||
|
|
||||||
|
type Chart = (typeof exactChartCatalog.charts)[number];
|
||||||
|
let releaseClock = createInitialReleaseClock($page.data?.serverNow);
|
||||||
|
let releaseNow = releaseClock.now();
|
||||||
|
let query = '';
|
||||||
|
let difficulty: string = 'expert';
|
||||||
|
let selections: { id: number; chart: Chart }[] = [];
|
||||||
|
let nextId = 0;
|
||||||
|
let announcement = '';
|
||||||
|
let copying = false;
|
||||||
|
let copyStatus = '';
|
||||||
|
let showCopyText = false;
|
||||||
|
let copyTextArea: HTMLTextAreaElement | undefined;
|
||||||
|
let storageNotice = '';
|
||||||
|
|
||||||
|
$: charts = releasedSongCharts(exactChartCatalog.charts, releaseNow);
|
||||||
|
$: difficultyCharts = charts.filter((chart) => chart.difficulty === difficulty);
|
||||||
|
$: options = songOptions(difficultyCharts, difficulty, query);
|
||||||
|
$: resultsText = formatRandomSongResults(selections.map(({ chart }) => chart));
|
||||||
|
|
||||||
|
function resetCopyStatus() {
|
||||||
|
copyStatus = '';
|
||||||
|
showCopyText = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSongs() {
|
||||||
|
try {
|
||||||
|
if (selections.length) {
|
||||||
|
localStorage.setItem(SONG_LIST_STORAGE_KEY, serializeSongList(selections.map(({ chart }) => chart)));
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(SONG_LIST_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
storageNotice = '';
|
||||||
|
} catch {
|
||||||
|
storageNotice = '목록을 브라우저에 저장하지 못했습니다. 페이지를 떠나기 전에 목록을 복사해 주세요.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSong(songId: string) {
|
||||||
|
const chart = difficultyCharts.find((chart) => chart.songId === songId);
|
||||||
|
if (!chart) return;
|
||||||
|
selections = [...selections, { id: nextId++, chart }];
|
||||||
|
saveSongs();
|
||||||
|
resetCopyStatus();
|
||||||
|
announcement = `${selections.length}번째에 ${chart.title}, ${difficultyLabel(chart.difficulty)}를 추가했습니다.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSong(id: number) {
|
||||||
|
const removed = selections.find((entry) => entry.id === id);
|
||||||
|
selections = selections.filter((entry) => entry.id !== id);
|
||||||
|
saveSongs();
|
||||||
|
resetCopyStatus();
|
||||||
|
announcement = `${removed?.chart.title ?? '악곡'}을 삭제했습니다. ${selections.length}개가 남았습니다.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSongs() {
|
||||||
|
selections = [];
|
||||||
|
saveSongs();
|
||||||
|
resetCopyStatus();
|
||||||
|
announcement = '악곡 목록을 초기화했습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copySongs() {
|
||||||
|
if (!selections.length || copying) return;
|
||||||
|
const snapshot = selections;
|
||||||
|
const text = resultsText;
|
||||||
|
copying = true;
|
||||||
|
resetCopyStatus();
|
||||||
|
try {
|
||||||
|
if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable');
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
if (selections === snapshot) copyStatus = `${snapshot.length}개 악곡을 선택 순서대로 복사했습니다.`;
|
||||||
|
} catch {
|
||||||
|
if (selections !== snapshot) return;
|
||||||
|
copyStatus = '자동 복사를 사용할 수 없습니다. 아래 텍스트를 선택해 복사해 주세요.';
|
||||||
|
showCopyText = true;
|
||||||
|
await tick();
|
||||||
|
copyTextArea?.focus();
|
||||||
|
copyTextArea?.select();
|
||||||
|
} finally {
|
||||||
|
copying = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
try {
|
||||||
|
const text = localStorage.getItem(SONG_LIST_STORAGE_KEY);
|
||||||
|
if (text) {
|
||||||
|
const saved = parseSavedSongList(text, exactChartCatalog.charts);
|
||||||
|
selections = saved.charts.map((chart) => ({ id: nextId++, chart }));
|
||||||
|
if (saved.unavailableCount) storageNotice = `현재 악곡 데이터에서 찾을 수 없는 ${saved.unavailableCount}개를 제외하고 목록을 복원했습니다.`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
storageNotice = '저장된 악곡 목록을 불러오지 못했습니다. 브라우저 저장소 설정을 확인해 주세요.';
|
||||||
|
}
|
||||||
|
let mounted = true;
|
||||||
|
void synchronizeReleaseClock().then((clock) => {
|
||||||
|
if (!mounted) return;
|
||||||
|
releaseClock = clock;
|
||||||
|
releaseNow = clock.now();
|
||||||
|
});
|
||||||
|
const timer = window.setInterval(() => releaseNow = releaseClock.now(), 1_000);
|
||||||
|
return () => { mounted = false; window.clearInterval(timer); };
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>악곡 목록 · 도구 · 홀로도리 계산기</title>
|
||||||
|
<meta name="description" content="악곡과 난이도를 고른 순서대로 목록을 만들고 복사하세요." />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
<h1>도구</h1>
|
||||||
|
<p>플레이할 때 유용한 도구를 모았습니다.</p>
|
||||||
|
</header>
|
||||||
|
<ToolsNav active="song-list" />
|
||||||
|
<div class="tool-heading">
|
||||||
|
<h2>악곡 목록</h2>
|
||||||
|
<p>플레이한 악곡을 고르면 선택한 순서대로 목록에 추가됩니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="panel" aria-label="악곡 선택">
|
||||||
|
<div class="filters">
|
||||||
|
<label>악곡 검색
|
||||||
|
<input type="search" bind:value={query} placeholder="곡명 검색" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
<label>난이도
|
||||||
|
<select bind:value={difficulty}>
|
||||||
|
{#each difficulties as item}<option value={item}>{difficultyLabel(item)}</option>{/each}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{#if options.length}
|
||||||
|
<ul class="song-options" aria-label="추가할 악곡">
|
||||||
|
{#each options as song (song.id)}
|
||||||
|
<li>
|
||||||
|
<button class="song-option" type="button" on:click={() => addSong(song.id)} aria-label={`${song.title}, ${difficultyLabel(difficulty)} 목록에 추가`}>
|
||||||
|
<strong class="song-title">{song.title}</strong>
|
||||||
|
<span class="level">{song.difficultyLevel == null ? '레벨 정보 없음' : `Lv. ${song.difficultyLevel}`}</span>
|
||||||
|
<span class="add-label" aria-hidden="true">+ 추가</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{:else}
|
||||||
|
<p class="placeholder" role="status">검색 결과가 없습니다. 검색어나 난이도를 변경해 주세요.</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p class="sr-only" role="status" aria-atomic="true">{announcement}</p>
|
||||||
|
<section class="result" aria-labelledby="results-title">
|
||||||
|
<div class="results-heading">
|
||||||
|
<h2 id="results-title">선택한 악곡 <span>{selections.length}개</span></h2>
|
||||||
|
<div class="result-actions">
|
||||||
|
<button class="secondary-button" type="button" on:click={copySongs} disabled={selections.length === 0 || copying}>{copying ? '복사 중…' : '목록 복사'}</button>
|
||||||
|
<button class="secondary-button" type="button" on:click={clearSongs} disabled={selections.length === 0}>목록 초기화</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if storageNotice}<p class="storage-notice" role="status">{storageNotice}</p>{/if}
|
||||||
|
<p class="copy-status" role="status">{copyStatus}</p>
|
||||||
|
{#if showCopyText}
|
||||||
|
<label class="copy-text">복사용 텍스트
|
||||||
|
<textarea bind:this={copyTextArea} value={resultsText} readonly rows={Math.min(selections.length + 1, 8)} on:focus={(event) => event.currentTarget.select()}></textarea>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
{#if selections.length}
|
||||||
|
<ol class="results-list" aria-label="악곡 선택 순서">
|
||||||
|
{#each selections as entry, index (entry.id)}
|
||||||
|
<li>
|
||||||
|
<span class="selection-number" aria-hidden="true">{index + 1}</span>
|
||||||
|
<strong class="song-title selected-title">{entry.chart.title}</strong>
|
||||||
|
<span class="selected-difficulty">{difficultyLabel(entry.chart.difficulty)}</span>
|
||||||
|
<span class="selected-level">{entry.chart.difficultyLevel == null ? '레벨 정보 없음' : `Lv. ${entry.chart.difficultyLevel}`}</span>
|
||||||
|
<button class="secondary-button remove-button" type="button" on:click={() => removeSong(entry.id)} aria-label={`${index + 1}번째 ${entry.chart.title} 삭제`}>삭제</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
{:else}
|
||||||
|
<p class="placeholder">위에서 악곡을 선택하면 첫 번째 곡부터 차례대로 쌓입니다.</p>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
main { width: min(760px, calc(100% - var(--page-gutter))); margin: 0 auto; padding: 42px 0 80px; }
|
||||||
|
h1 { margin: 0 0 14px; font-size: clamp(30px, 5vw, 42px); letter-spacing: -.04em; }
|
||||||
|
header > p, .tool-heading p { color: #a4adba; line-height: 1.7; }
|
||||||
|
.tool-heading p { margin-bottom: 0; }
|
||||||
|
h2 { margin: 0; font-size: 22px; }
|
||||||
|
h2 span { margin-left: 6px; color: #a4adba; font-size: 14px; font-weight: 500; }
|
||||||
|
.panel, .result { margin-top: 26px; padding: 26px; border: 1px solid #343c49; border-radius: 16px; background: #151a22; }
|
||||||
|
.panel { padding: 20px; }
|
||||||
|
.filters { display: grid; grid-template-columns: minmax(0, 1fr) 140px; gap: 14px; }
|
||||||
|
label { display: grid; gap: 8px; min-width: 0; color: #a4adba; font-size: 14px; }
|
||||||
|
input, select, textarea { width: 100%; min-width: 0; padding: 12px; border: 1px solid #414b5b; border-radius: 9px; background: #101319; color: #e8ebf1; font: inherit; }
|
||||||
|
input, select { height: 44px; }
|
||||||
|
.placeholder { color: #a4adba; line-height: 1.7; }
|
||||||
|
.song-options { max-height: 200px; overflow-y: auto; overscroll-behavior: contain; margin: 12px 0 0; padding: 3px; list-style: none; }
|
||||||
|
.song-options li + li { margin-top: 4px; }
|
||||||
|
button { min-height: 44px; border: 1px solid #343c49; border-radius: 9px; background: #101319; color: #e8ebf1; font: inherit; cursor: pointer; }
|
||||||
|
button:hover:not(:disabled) { border-color: #a76b87; background: #302331; }
|
||||||
|
button:disabled { color: #87909e; cursor: not-allowed; }
|
||||||
|
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible { outline: 3px solid #ed8bb5; outline-offset: 2px; }
|
||||||
|
.song-option { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 10px; width: 100%; min-height: 36px; padding: 6px 10px; border-radius: 7px; text-align: left; }
|
||||||
|
.song-title { min-width: 0; overflow-wrap: anywhere; line-height: 1.6; }
|
||||||
|
.song-option .song-title { font-size: 14px; line-height: 1.35; }
|
||||||
|
.level { color: #a4adba; font-size: 13px; }
|
||||||
|
.add-label { color: #f3b4d0; font-size: 13px; font-weight: 800; white-space: nowrap; }
|
||||||
|
.result { padding: 20px; }
|
||||||
|
.results-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||||
|
.result-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.secondary-button { min-height: 36px; padding: 6px 10px; border-color: #414b5b; background: transparent; color: #bec8d6; font-size: 13px; }
|
||||||
|
.secondary-button:hover:not(:disabled) { border-color: #637083; background: #202733; }
|
||||||
|
.secondary-button:disabled { border-color: #343c49; }
|
||||||
|
.copy-status { margin: 12px 0 0; color: #f3b4d0; font-size: 13px; line-height: 1.6; }
|
||||||
|
.copy-status:empty { margin: 0; }
|
||||||
|
.storage-notice { color: #f3b4d0; font-size: 13px; line-height: 1.7; }
|
||||||
|
.copy-text { margin-top: 12px; }
|
||||||
|
textarea { min-height: 88px; resize: vertical; line-height: 1.7; }
|
||||||
|
.results-list { margin: 12px 0 0; padding: 0; list-style: none; }
|
||||||
|
.results-list li { display: grid; grid-template-columns: 24px minmax(0, 1fr) 52px 46px auto; align-items: center; gap: 10px; padding: 6px 0; border-bottom: 1px solid #2c3441; }
|
||||||
|
.results-list li:last-child { border-bottom: 0; }
|
||||||
|
.selection-number { color: #a4adba; font-size: 13px; font-variant-numeric: tabular-nums; }
|
||||||
|
.selected-title { font-size: 14px; line-height: 1.4; }
|
||||||
|
.selected-difficulty, .selected-level { color: #a4adba; font-size: 12px; }
|
||||||
|
.selected-level { font-variant-numeric: tabular-nums; }
|
||||||
|
.remove-button { min-height: 32px; padding: 4px 8px; font-size: 12px; }
|
||||||
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.panel, .result { padding: 20px; }
|
||||||
|
.result { padding: 16px; }
|
||||||
|
.filters { grid-template-columns: minmax(0, 1fr) 110px; gap: 10px; }
|
||||||
|
.song-option { gap: 8px; padding-inline: 8px; }
|
||||||
|
.song-option .song-title { font-size: 13px; }
|
||||||
|
.level, .add-label { font-size: 11px; }
|
||||||
|
.results-list li { grid-template-columns: 18px minmax(0, 1fr) 42px 36px auto; gap: 6px; }
|
||||||
|
.selected-title { font-size: 13px; }
|
||||||
|
.selected-difficulty, .selected-level { font-size: 11px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+3
-1
@@ -8,10 +8,12 @@ export default defineConfig({
|
|||||||
port: 5173,
|
port: 5173,
|
||||||
// SvelteKit excludes build output itself, but must watch generated route modules.
|
// SvelteKit excludes build output itself, but must watch generated route modules.
|
||||||
// Ignoring all of .svelte-kit leaves cached page imports pointing at old route IDs.
|
// Ignoring all of .svelte-kit leaves cached page imports pointing at old route IDs.
|
||||||
|
// Plugin verification output contains complete project copies, including tsconfigs.
|
||||||
|
// Watching them also triggers unrelated TypeScript cache resets and full reloads.
|
||||||
watch: {
|
watch: {
|
||||||
usePolling: process.env.WATCH_USE_POLLING === 'true',
|
usePolling: process.env.WATCH_USE_POLLING === 'true',
|
||||||
interval: 1000,
|
interval: 1000,
|
||||||
ignored: ['**/output/**', '**/pages-dist/**', '**/offline-dist/**', '**/release/**']
|
ignored: ['**/output/**', '**/pages-dist/**', '**/offline-dist/**', '**/plugin-dist/**', '**/release/**']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user