feat: release version 1.0.4

This commit is contained in:
2026-08-19 18:56:00 +09:00
parent bd9d5a6ba6
commit f910db8901
11 changed files with 506 additions and 105 deletions
+49 -19
View File
@@ -277,6 +277,27 @@
return Math.max(0, Math.min(900, Number.isFinite(value) ? Math.round(Number(value)) : 0));
}
function sanitizeGenerationBonusInput(input: HTMLInputElement, previousValue: number | null): string {
if (input.value === '') {
input.value = '';
return '';
}
if (!/^\d+$/.test(input.value)) {
input.value = previousValue === null ? '' : String(previousValue);
return input.value;
}
const value = Number(input.value);
if (value > 900) {
input.value = previousValue === null ? '' : String(previousValue);
return input.value;
}
input.value = String(value);
return input.value;
}
function saveState(state: SavedState): void {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
@@ -913,6 +934,7 @@
<div class="song-fields">
<label>악곡<select bind:value={selectedSongId} on:change={() => result = null}>{#each songs as song}<option value={song.id}>{song.title}</option>{/each}</select></label>
<label>난이도<select bind:value={difficulty} on:change={() => result = null}><option value="expert">Expert</option><option value="hard">Hard</option></select></label>
<div class="chart-meta"><span>길이 {selectedChart?.durationSeconds ?? 0}초</span><span>노트 {selectedChart?.totalNotes?.toLocaleString('ko-KR') ?? 0}개</span></div>
</div>
<div class="candidate-fields">
<div class="candidate-awakening">
@@ -923,20 +945,20 @@
<button type="button" disabled={candidateAwakening >= 5} aria-label="자동 후보 개화 높이기" on:click={() => updateCandidateAwakening(candidateAwakening + 1)}>›</button>
</div>
</div>
<label>자동 후보 기수 보너스 덮어쓰기<input class="number-input" type="text" inputmode="numeric" pattern="[0-9]*" value={candidateGenerationBonus ?? ''} placeholder="미사용" on:change={(event) => updateCandidateGenerationBonus(event.currentTarget.value)}></label>
<label>자동 후보 기수 보너스 덮어쓰기 (0~900)<input class="number-input" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="3" value={candidateGenerationBonus ?? ''} placeholder="미사용" on:input={(event) => updateCandidateGenerationBonus(sanitizeGenerationBonusInput(event.currentTarget, candidateGenerationBonus))}></label>
</div>
<p class="candidate-guide">
보유 카드의 개화가 자동 후보 개화보다 높으면 실제 보유 개화를 우선 적용합니다.<br>
기수 보너스는 녹색 노드를 활성화해 얻을 수 있는 보너스를 의미합니다.
</p>
<div class="fixed-fields">
<div class="fixed-column-headings">
<span>고정 멤버</span><span>개화</span><span>빈도 감소 고정</span>
<div><span>파란 노드 전체</span><span class="generation-heading">기수 보너스<br>덮어쓰기</span></div>
<div class="fixed-column-headings" aria-hidden="true">
<span>고정 카드</span><span>개화</span><span>빈도 감소 고정</span>
<div><span>파란 노드 전체</span><span class="generation-heading">기수 보너스<br>덮어쓰기 (0~900)</span></div>
</div>
{#each fixedCardIds as selectedId, slot}
<div class="fixed-field">
<label>{slot + 1}번 고정
<label><span>{slot + 1}번 고정</span>
<select value={selectedId} on:change={(event) => updateFixedCard(slot, event.currentTarget.value)}>
<option value="">고정하지 않음</option>
{#each fiveStarCards as card}<option value={card.id} disabled={selectedInAnotherSlot(card, slot)}>{cardLabel(card)}</option>{/each}
@@ -961,7 +983,7 @@
<input type="checkbox" disabled={!selectedId} checked={fixedAllBlueNodes[slot]} aria-label={`${slot + 1}번 고정 카드 파란 노드 전체 활성화`} on:change={() => toggleFixedAllBlueNodes(slot)}>
<span aria-hidden="true"></span>
</label>
<input class="generation-input" type="text" inputmode="numeric" pattern="[0-9]*" disabled={!selectedId} value={fixedGenerationBonusOverrides[slot] ?? ''} placeholder="미사용" aria-label={`${slot + 1}번 고정 카드 기수 보너스 덮어쓰기`} on:change={(event) => updateFixedGenerationBonus(slot, event.currentTarget.value)}>
<input class="generation-input" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="3" disabled={!selectedId} value={fixedGenerationBonusOverrides[slot] ?? ''} placeholder="미사용" aria-label={`${slot + 1}번 고정 카드 기수 보너스 덮어쓰기 (0~900)`} on:input={(event) => updateFixedGenerationBonus(slot, sanitizeGenerationBonusInput(event.currentTarget, fixedGenerationBonusOverrides[slot] ?? null))}>
</div>
</div>
{/each}
@@ -1056,26 +1078,32 @@
label { display: grid; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
select, .number-input { width: 100%; min-width: 0; height: 42px; padding: 0 12px; border: 1px solid #343c49; border-radius: 9px; color: #e8ebf1; background: #11151b; font: inherit; }
select:focus, .number-input:focus, .generation-input:focus { border-color: #d86f9c; outline: 3px solid rgb(216 111 156 / 14%); }
.song-fields { display: grid; grid-template-columns: minmax(0, 1fr) clamp(130px, 11vw, 180px); gap: 10px; margin-top: 22px; }
.candidate-fields { display: grid; grid-template-columns: clamp(130px, 11vw, 180px) clamp(260px, 24vw, 380px); gap: 10px; margin-top: 14px; padding-top: 14px; border-top: 1px solid #292f39; }
.song-fields { display: grid; grid-template-columns: minmax(0, 2fr) clamp(110px, 11vw, 170px) minmax(220px, .8fr); align-items: end; gap: 12px; margin-top: 22px; }
.chart-meta { display: flex; align-items: center; gap: 8px; height: 42px; }
.chart-meta span { padding: 7px 9px; border-radius: 7px; color: #adb5c2; background: #202631; font-size: 13px; white-space: nowrap; }
.candidate-fields { display: grid; grid-template-columns: clamp(130px, 12vw, 180px) clamp(260px, 24vw, 380px); gap: 9px; margin-top: 18px; }
.candidate-awakening { display: grid; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
.candidate-guide { margin: 7px 0 0; color: #737d8d; font-size: 12px; line-height: 1.5; }
.fixed-fields { display: grid; gap: 9px; margin-top: 16px; }
.fixed-guide { margin: 8px 0 0; color: #737d8d; font-size: 12px; line-height: 1.5; }
.fixed-column-headings { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(120px, 11vw, 170px) clamp(304px, 26vw, 390px) clamp(180px, 16vw, 240px); gap: 9px; margin-bottom: -3px; }
.fixed-column-headings > span { align-self: center; color: #8f98a8; font-size: 12px; font-weight: 800; text-align: center; }
.fixed-column-headings > span:first-child { text-align: left; }
.fixed-column-headings { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(130px, 12vw, 180px) clamp(304px, 27vw, 400px) clamp(180px, 16vw, 240px); gap: 9px; padding: 0 2px; }
.fixed-column-headings > span { align-self: center; padding-left: 2px; color: #8f98a8; font-size: 13px; font-weight: 800; }
.fixed-column-headings > div { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; color: #8f98a8; font-size: 12px; font-weight: 800; text-align: center; }
.generation-heading { line-height: 1.25; white-space: nowrap; }
.fixed-field { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(120px, 11vw, 170px) clamp(304px, 26vw, 390px) clamp(180px, 16vw, 240px); gap: 9px; }
.fixed-awakening, .fixed-reduction { display: grid; align-self: end; align-items: center; height: 42px; }
.awakening-stepper { display: grid; grid-template-columns: 30px minmax(0, 1fr) 30px; align-items: center; height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
.awakening-stepper button { align-self: stretch; padding: 0; border: 0; color: #c1c8d3; background: #202630; font-size: 22px; cursor: pointer; }
.awakening-stepper strong { color: #e8ebf1; font-size: 14px; text-align: center; }
.fixed-field { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(130px, 12vw, 180px) clamp(304px, 27vw, 400px) clamp(180px, 16vw, 240px); gap: 9px; }
.fixed-field > label { grid-template-columns: 74px minmax(0, 1fr); align-items: center; }
.fixed-awakening, .fixed-reduction { min-width: 0; }
.awakening-stepper { display: grid; grid-template-columns: 32px minmax(0, 1fr) 32px; align-items: center; height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
.awakening-stepper button { align-self: stretch; padding: 0; border: 0; color: #c1c8d3; background: #202630; font-size: 24px; line-height: 1; cursor: pointer; }
.awakening-stepper button:hover:not(:disabled) { color: #f5afcc; background: #2d2530; }
.awakening-stepper button:focus-visible { position: relative; z-index: 1; outline: 2px solid #df74a2; outline-offset: -2px; }
.awakening-stepper strong { color: #e8ebf1; font-size: 14px; text-align: center; white-space: nowrap; }
.reduction-options { display: grid; grid-template-columns: 1.35fr repeat(4, 1fr); height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
.reduction-options button { min-width: 0; padding: 0 5px; border: 0; border-left: 1px solid #2d3440; color: #8f98a8; background: transparent; font-size: 12px; font-weight: 750; cursor: pointer; }
.reduction-options button { min-width: 0; padding: 0 6px; border: 0; border-left: 1px solid #2d3440; color: #8f98a8; background: transparent; font-size: 13px; font-weight: 750; cursor: pointer; }
.reduction-options button:first-child { border-left: 0; }
.reduction-options button:hover:not(:disabled) { color: #e9edf3; background: #202630; }
.reduction-options button.active { color: #171019; background: #ed8bb5; }
.reduction-options button:focus-visible { position: relative; z-index: 1; outline: 2px solid #f1a0c2; outline-offset: -2px; }
.fixed-overrides { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; align-self: end; height: 42px; }
.override-checkbox { display: grid; place-items: center; cursor: pointer; }
.override-checkbox input { position: absolute; width: 1px; height: 1px; opacity: 0; }
@@ -1088,13 +1116,14 @@
.generation-input::placeholder { color: #68717f; opacity: 1; }
.generation-input:disabled { opacity: .35; }
.awakening-stepper button:disabled, .reduction-options button:disabled { opacity: .35; cursor: default; }
.leader-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 26px; padding-top: 20px; border-top: 1px solid #303641; }
.leader-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-top: 16px; }
.outfit-effect { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 12px; margin-top: 9px; padding: 11px 13px; border: 1px solid #2d3440; border-radius: 9px; background: #12161c; }
.outfit-effect span { color: #d993b1; font-size: 13px; font-weight: 850; }
.outfit-effect p { margin: 0; color: #b9c1cd; font-size: 14px; line-height: 1.55; }
.hint { margin: 13px 0 0; color: #858f9f; font-size: 13px; line-height: 1.6; }
.error { margin: 14px 0 0; color: #ff9f9f; font-size: 13px; }
.search { width: 100%; height: 46px; margin-top: 18px; border: 0; border-radius: 10px; color: #171019; background: #ed8bb5; font-weight: 900; cursor: pointer; }
.search { width: 100%; height: 46px; margin-top: 20px; border: 0; border-radius: 10px; color: #171019; background: #ed8bb5; font-weight: 900; cursor: pointer; }
.search:hover:not(:disabled) { background: #f2a0c2; }
.search:disabled { opacity: .55; cursor: default; }
.compute-option { display: flex; align-items: center; gap: 9px; width: fit-content; margin-top: 12px; cursor: pointer; }
.compute-option input { position: absolute; width: 1px; height: 1px; opacity: 0; }
@@ -1150,6 +1179,7 @@
main { width: calc(100% - 24px); padding-top: var(--page-content-top); }
.panel { padding: 17px; }
.song-fields { grid-template-columns: 1fr 100px; }
.chart-meta { grid-column: 1 / -1; height: auto; }
.candidate-fields { grid-template-columns: 1fr; }
.fixed-column-headings { display: none; }
.fixed-field { grid-template-columns: minmax(0, 1fr) 102px; }