fix: improve mobile navigation and calculator usability
Publish GitHub Pages output / publish (push) Successful in 32s

This commit is contained in:
2026-09-21 11:30:22 +09:00
parent 3b1c1aeb6f
commit 0bdade44e3
11 changed files with 205 additions and 22 deletions
+5 -1
View File
@@ -1,9 +1,13 @@
---
updatedAt: 2026-09-21T01:10:33Z
updatedAt: 2026-09-21T02:28:48Z
---
## 2026년 9월 21일
### 모바일 화면 사용성 개선
`IMPROVED` 전체 메뉴 버튼으로 모든 메뉴에 접근할 수 있습니다. 배치 분석·조합 탐색의 입력 항목 설명과 선택한 곡명·카드명을 좁은 화면에서도 확인할 수 있으며, 개화 버튼과 입력 영역을 넓혔습니다. 보드에 확대·축소·화면 맞춤 버튼을 추가하고, 연결 카드 선택 창은 화면 높이에 맞춰 목록 끝까지 스크롤할 수 있도록 개선했습니다. 분석 그래프의 작은 구간 대신 누를 수 있는 액티브 발동 목록도 제공합니다. 분석 설정 버튼은 본문을 가리지 않도록 페이지 아래로 옮겼습니다.
### 초록 보드 간편 활성화 확장
`IMPROVED` 모든 멤버의 초록 보드를 3칸·5칸·7칸 중 선택해 활성화할 수 있습니다. 5칸은 기존 세로 3칸의 가운데 양옆을, 7칸은 그 아래 2칸을 추가합니다. 이미 활성화한 다른 노드는 유지됩니다. 모든 멤버 간편 설정의 3칸·5칸·7칸 버튼에서 적용 범위를 작은 보드 그림으로 확인할 수 있습니다.
+18 -2
View File
@@ -43,6 +43,8 @@
let currentPath = pathFromHash();
let CurrentPage = routes.get(currentPath) ?? HomePage;
let eventNow = Date.now();
let menuOpen = false;
$: if (currentPath) menuOpen = false;
$: showEventTab = Boolean(activeEventAt(events, eventNow));
setOfflinePage(currentPath);
@@ -86,7 +88,8 @@
<header class="site-nav">
<div class="nav-inner">
<a class="brand" href="#/" aria-label="홀로도리 계산기 홈">홀로도리 계산기</a>
<nav aria-label="주요 메뉴">
<button class="menu-toggle" type="button" aria-controls="primary-navigation" aria-expanded={menuOpen} on:click={() => menuOpen = !menuOpen}>{menuOpen ? '메뉴 닫기' : '전체 메뉴'}</button>
<nav id="primary-navigation" class:open={menuOpen} aria-label="주요 메뉴">
<a href="#/cards" aria-current={currentPath.startsWith('/cards') ? 'page' : undefined}>카드 목록</a>
<a href="#/analysis" aria-current={currentPath.startsWith('/analysis') ? 'page' : undefined}>배치 분석</a>
<a href="#/rating" aria-current={currentPath.startsWith('/rating') ? 'page' : undefined}>리더 레이팅</a>
@@ -155,8 +158,11 @@
text-decoration: none;
}
nav { display: flex; min-width: 0; flex: 1; align-items: center; justify-content: flex-end; gap: 4px; overflow-x: auto; scrollbar-width: none; }
.brand { flex-shrink: 0; }
.menu-toggle { display: none; }
nav { display: flex; min-width: 0; flex: 1; align-items: center; justify-content: safe flex-end; gap: 4px; overflow-x: auto; scrollbar-width: thin; }
nav::-webkit-scrollbar { display: none; }
nav a {
flex: 0 0 auto;
padding: 8px 12px;
@@ -167,6 +173,7 @@
text-decoration: none;
white-space: nowrap;
}
nav a:hover { color: #e2e6ed; background: rgb(255 255 255 / 5%); }
nav a[aria-current='page'] { color: #f1a1c3; background: rgb(239 139 182 / 12%); }
.feedback-link { border: 1px solid #9f5877; color: #edb8cf; background: linear-gradient(135deg, #482c39, #38232e); box-shadow: 0 0 12px rgb(175 91 128 / 10%); }
@@ -176,6 +183,15 @@
.report-link span { margin-left: 2px; font-size: 11px; }
a:focus-visible { outline: 3px solid rgb(239 139 182 / 28%); outline-offset: 2px; }
@media (max-width: 1100px) {
.nav-inner { flex-wrap: wrap; padding-block: 5px; }
.menu-toggle { display: block; min-height: 44px; padding: 8px 14px; border: 1px solid #694256; border-radius: 9px; color: #f1a1c3; background: #29202a; font: inherit; font-size: 14px; cursor: pointer; }
.menu-toggle:focus-visible { outline: 3px solid #ef8bb6; outline-offset: 2px; }
nav { display: none; flex: 0 0 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); max-height: 65dvh; overflow-y: auto; padding: 4px 2px 10px; }
nav.open { display: grid; }
nav a { display: flex; align-items: center; min-height: 44px; white-space: normal; }
}
@media (max-width: 560px) {
:global(:root) { --page-gutter: 24px; }
.nav-inner { width: calc(100% - 24px); min-height: 54px; }
+4 -1
View File
@@ -123,7 +123,9 @@
{#each result.order as cardId}<span><i style={`background: ${memberColor(cardId)};`}></i>{cardName(cardId)}</span>{/each}
</div>
</div>
<div class="timeline-scroll" role="region" aria-label="스킬 발동 시간대 그래프">
<p class="scroll-guide">그래프를 좌우로 밀면 전체 시간대를 확인할 수 있습니다.</p>
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Keyboard users must be able to scroll the timeline.) -->
<div class="timeline-scroll" role="region" aria-label="스킬 발동 시간대 그래프" tabindex="0">
<div class="timeline-canvas" style={`width: ${timelineWidth + 156}px;`}>
<div class="timeline-row note-row">
<strong class="lane-label">초당 노트</strong>
@@ -172,6 +174,7 @@
<SpecialNoteSummary {result} {cardName} />
<style>
.scroll-guide { color: #adb5c2; font-size: 13px; line-height: 1.5; }
.order-sections { display: grid; grid-template-columns: 1fr; gap: 14px; margin-top: 18px; }
.order-section { min-width: 0; }
.order-section h3 { margin: 0 0 9px; color: #aeb7c5; font-size: 14px; }
+8 -6
View File
@@ -71,6 +71,8 @@
background: rgb(24 28 36 / 97%);
box-shadow: 0 18px 50px rgb(0 0 0 / 42%);
color: #e8ebf1;
max-height: calc(100dvh - 36px);
overflow-y: auto;
}
.consent-copy { min-width: 0; flex: 1; }
@@ -94,11 +96,10 @@
.primary { color: #21121a; background: #ef8bb6; }
.secondary { border-color: #596273; color: #d4dae4; background: #303641; }
.analytics-settings {
position: fixed;
z-index: 90;
right: 12px;
bottom: 12px;
min-height: 30px;
display: block;
margin: 16px 12px max(12px, env(safe-area-inset-bottom));
margin-left: auto;
min-height: 44px;
padding: 5px 9px;
border-color: rgb(255 255 255 / 12%);
color: #8e98a8;
@@ -107,7 +108,8 @@
}
@media (max-width: 560px) {
.analytics-consent { right: 12px; bottom: 12px; width: calc(100% - 24px); align-items: stretch; flex-direction: column; gap: 14px; }
.analytics-consent { right: 12px; bottom: max(12px, env(safe-area-inset-bottom)); width: calc(100% - 24px); align-items: stretch; flex-direction: column; gap: 14px; }
.consent-actions { justify-content: flex-end; }
.consent-actions button { min-height: 44px; flex: 1; }
}
</style>
+3
View File
@@ -11,6 +11,7 @@
let sort: SongSort = 'title';
$: options = songOptions(charts, difficulty, query, sort);
$: selectedTitle = charts.find((chart) => chart.songId === value)?.title;
function selectSong(event: Event) {
const nextValue = (event.currentTarget as HTMLSelectElement).value;
@@ -48,6 +49,7 @@
{/if}
</select>
</label>
{#if selectedTitle}<p class="selected-title">선택한 악곡: {selectedTitle}</p>{/if}
{#if query.trim()}
<p class:no-results={options.length === 0} class="search-status" aria-live="polite">
{options.length > 0
@@ -59,6 +61,7 @@
<style>
.song-picker { display: grid; grid-template-columns: minmax(180px, .9fr) minmax(140px, .55fr) minmax(260px, 1.4fr); gap: 9px; }
.selected-title { grid-column: 1 / -1; margin: 0; color: #b9c1cd; font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
label { display: grid; gap: 7px; min-width: 0; color: #8f98a8; font-size: 14px; font-weight: 750; }
label > span { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
small { color: #737d8d; font-size: 11px; }
+9
View File
@@ -6,3 +6,12 @@
--page-header-bottom: 28px;
--page-description-size: 16px;
}
@media (max-width: 700px) {
:root { --page-title-size: 30px; --page-description-size: 15px; }
/* Keep editable text readable without triggering focus zoom on mobile Safari. */
body input:not([type='checkbox']):not([type='radio']):not([type='range']):not([type='file']),
body select, body textarea { font-size: 16px; }
}
html { scroll-padding-top: 80px; }
+16 -2
View File
@@ -10,6 +10,8 @@
let eventClock = createInitialReleaseClock($page.data?.serverNow);
let eventNow = eventClock.now();
let menuOpen = false;
$: if ($page.url.pathname) menuOpen = false;
$: showEventTab = Boolean(activeEventAt(events, eventNow));
onMount(() => {
@@ -32,7 +34,8 @@
<header class="site-nav">
<div class="nav-inner">
<a class="brand" href="/" aria-label="홀로도리 계산기 홈">홀로도리 계산기</a>
<nav aria-label="주요 메뉴">
<button class="menu-toggle" type="button" aria-controls="primary-navigation" aria-expanded={menuOpen} on:click={() => menuOpen = !menuOpen}>{menuOpen ? '메뉴 닫기' : '전체 메뉴'}</button>
<nav id="primary-navigation" class:open={menuOpen} aria-label="주요 메뉴">
<a href="/cards" aria-current={$page.url.pathname.startsWith('/cards') ? 'page' : undefined}>카드 목록</a>
<a href="/analysis" aria-current={$page.url.pathname.startsWith('/analysis') ? 'page' : undefined}>배치 분석</a>
<a href="/rating" aria-current={$page.url.pathname.startsWith('/rating') ? 'page' : undefined}>리더 레이팅</a>
@@ -101,7 +104,9 @@
text-decoration: none;
}
nav { display: flex; min-width: 0; flex: 1; align-items: center; justify-content: flex-end; gap: 4px; overflow-x: auto; scrollbar-width: none; }
.brand { flex-shrink: 0; }
.menu-toggle { display: none; }
nav { display: flex; min-width: 0; flex: 1; align-items: center; justify-content: safe flex-end; gap: 4px; overflow-x: auto; scrollbar-width: thin; }
nav::-webkit-scrollbar { display: none; }
nav a {
@@ -124,6 +129,15 @@
.report-link span { margin-left: 2px; font-size: 11px; }
a:focus-visible { outline: 3px solid rgb(239 139 182 / 28%); outline-offset: 2px; }
@media (max-width: 1100px) {
.nav-inner { flex-wrap: wrap; padding-block: 5px; }
.menu-toggle { display: block; min-height: 44px; padding: 8px 14px; border: 1px solid #694256; border-radius: 9px; color: #f1a1c3; background: #29202a; font: inherit; font-size: 14px; cursor: pointer; }
.menu-toggle:focus-visible { outline: 3px solid #ef8bb6; outline-offset: 2px; }
nav { display: none; flex: 0 0 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); max-height: 65dvh; overflow-y: auto; padding: 4px 2px 10px; }
nav.open { display: grid; }
nav a { display: flex; align-items: center; min-height: 44px; white-space: normal; }
}
@media (max-width: 560px) {
:global(:root) { --page-gutter: 24px; }
.nav-inner { width: calc(100% - 24px); min-height: 54px; }
+49 -2
View File
@@ -1053,8 +1053,10 @@
<option value={card.id} disabled={selectedCardIds.some((id, index) => index !== slot && cardById.get(id)?.characterId === card.characterId)}>{collection[card.id]?.owned ? '● ' : ''}{cardLabel(card)}</option>
{/each}
</select>
{#if selectedId && cardById.get(selectedId)}<small class="selected-card-name">{cardLabel(cardById.get(selectedId)!)}</small>{/if}
</label>
<div class="awakening">
<span class="mobile-field-label">개화 (0~5)</span>
<div class="awakening-stepper">
<button
type="button"
@@ -1072,6 +1074,7 @@
</div>
</div>
<div class="cooldown-lock">
<span class="mobile-field-label">빈도 감소 고정</span>
<div class="reduction-options">
{#each REDUCTION_LOCK_OPTIONS as reduction}
<button
@@ -1084,6 +1087,8 @@
</div>
</div>
<div class="board-overrides">
<span class="mobile-field-label">파란 노드 전체</span>
<span class="mobile-field-label">기수 보너스 덮어쓰기 (0~900)</span>
<label class="override-checkbox">
<input type="checkbox" checked={allBlueNodes[slot]} aria-label={`${slot + 1}번 카드 파란 노드 전체 활성화`} on:change={() => toggleAllBlueNodes(slot)}>
<span aria-hidden="true"></span>
@@ -1237,7 +1242,26 @@
{/if}
</p>
{/if}
<div class="timeline-scroll" role="region" aria-label="스킬 발동 시간대 그래프">
{#if mode === 'analysis'}
<details class="active-window-list">
<summary>액티브 발동 목록 · 눌러서 미발동/복원</summary>
<p>작은 그래프 대신 아래 버튼으로도 조작할 수 있습니다. 미발동으로 설정한 구간은 다시 누르면 복원됩니다.</p>
<div>
{#each result.activeWindows as window}
{@const excluded = excludedActiveWindows.some((item) => item.cardId === window.cardId && item.startSeconds === window.startSeconds)}
{@const impact = activeImpacts.get(activeWindowKey(window))}
<button type="button" disabled={!simulationContext || running} aria-pressed={excluded} on:click={() => toggleActiveWindow(window)}>
<strong>{resultCardName(window.cardId)}</strong>
<span>{formatSeconds(window.startSeconds)}~{formatSeconds(window.endSeconds)} · 효과 {window.scoreUpPercent}% · {excluded ? '미발동 (눌러서 복원)' : '발동 (눌러서 제외)'}</span>
{#if impact}<span>변경 시 최고점 {signedScoreChange(impact.scoreChange)}</span>{/if}
</button>
{/each}
</div>
</details>
{/if}
<p class="board-guide">그래프를 좌우로 밀면 전체 시간대를 확인할 수 있습니다.</p>
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Keyboard users must be able to scroll the timeline.) -->
<div class="timeline-scroll" role="region" aria-label="스킬 발동 시간대 그래프" tabindex="0">
<div class="timeline-canvas" style={`width: ${timelineWidth + 156}px;`}>
<ScoreTimelineChart notes={displayedResult!.notes} {maximumScoreProbabilities} durationSeconds={timelineDuration} minimumWidth={timelineWidth} timeTicks={timelineTicks} />
{#each result.order as cardId, slot}
@@ -1308,6 +1332,14 @@
</main>
<style>
.mobile-field-label, .selected-card-name { display: none; }
.active-window-list { margin: 12px 0; padding: 12px; border: 1px solid #343c49; border-radius: 10px; }
.active-window-list summary { min-height: 44px; align-content: center; cursor: pointer; font-size: 14px; }
.active-window-list p { color: #adb5c2; font-size: 13px; line-height: 1.6; }
.active-window-list > div { display: grid; gap: 8px; max-height: 360px; overflow-y: auto; }
.active-window-list button { display: grid; gap: 6px; min-height: 44px; padding: 12px; border: 1px solid #414b5b; border-radius: 8px; background: #202630; color: #e8ebf1; text-align: left; cursor: pointer; overflow-wrap: anywhere; }
.active-window-list button[aria-pressed='true'] { border-color: #ef8bb6; background: #37232e; }
.active-window-list button span { font-size: 12px; line-height: 1.5; }
#result-title { scroll-margin-top: 90px; }
.stale-warning { margin: 14px 0; padding: 11px 14px; border: 1px solid #665333; border-radius: 9px; color: #edcf92; background: #292319; font-size: 13px; line-height: 1.6; }
main { width: min(var(--page-max-width), calc(100% - var(--page-gutter))); margin: 0 auto; padding: var(--page-content-top) 0 80px; }
@@ -1475,7 +1507,7 @@
.card-field-header, .card-field { grid-template-columns: minmax(0, 1fr) 120px 280px 180px; }
}
@media (max-width: 700px) {
@media (max-width: 900px) {
.order-sections { grid-template-columns: 1fr; }
main { width: calc(100% - 24px); padding-top: var(--page-content-top); }
.page-header { align-items: flex-start; }
@@ -1504,4 +1536,19 @@
.lane-label { flex-basis: 118px; min-width: 118px; }
.timeline-canvas { padding-right: 0; }
}
@media (max-width: 900px) {
.song-fields { grid-template-columns: minmax(0, 1fr); }
.section-title { flex-wrap: wrap; }
.card-field { grid-template-columns: minmax(0, 1fr); padding: 14px 0; border-top: 1px solid #343c49; gap: 12px; }
.card-field > label { grid-template-columns: minmax(0, 1fr); gap: 7px; }
.mobile-field-label { display: block; color: #aab4c3; font-size: 13px; line-height: 1.5; margin-bottom: 6px; }
.selected-card-name { display: block; color: #c1c8d3; font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; }
.board-overrides { height: auto; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: end; }
.board-overrides .mobile-field-label { margin-bottom: 0; }
.override-checkbox, .generation-input { min-height: 44px; }
.awakening-stepper { height: 46px; grid-template-columns: 44px minmax(0, 1fr) 44px; }
.reduction-options { height: 46px; }
.chart-meta span { white-space: normal; }
}
</style>
+1 -1
View File
@@ -1753,7 +1753,7 @@
}
.description {
display: none;
display: block;
}
.total {
+22 -1
View File
@@ -1320,8 +1320,10 @@
<option value="">고정하지 않음</option>
{#each candidateCards as card}<option value={card.id} disabled={fixedCardIds.some((id, index) => index !== slot && cardById.get(id)?.characterId === card.characterId)}>{cardLabel(card)}</option>{/each}
</select>
{#if selectedId && cardById.get(selectedId)}<small class="selected-card-name">{cardLabel(cardById.get(selectedId)!)}</small>{/if}
</label>
<div class="fixed-awakening">
<span class="mobile-field-label">개화 (0~5)</span>
<div class="awakening-stepper">
<button type="button" disabled={!selectedId || fixedAwakenings[slot] <= 0} aria-label={`${slot + 1}번 고정 카드 개화 낮추기`} on:click={() => updateFixedAwakening(slot, fixedAwakenings[slot] - 1)}>‹</button>
<strong>{fixedAwakenings[slot]}</strong>
@@ -1329,6 +1331,7 @@
</div>
</div>
<div class="fixed-reduction">
<span class="mobile-field-label">빈도 감소 고정</span>
<div class="reduction-options">
{#each REDUCTION_LOCK_OPTIONS as reduction}
<button type="button" disabled={!selectedId} class:active={fixedCooldownReductions[slot] === reduction} aria-pressed={fixedCooldownReductions[slot] === reduction} on:click={() => updateFixedReduction(slot, reduction)}>{reduction === null ? '미사용' : `${reduction}%`}</button>
@@ -1336,6 +1339,8 @@
</div>
</div>
<div class="fixed-overrides">
<span class="mobile-field-label">파란 노드 전체</span>
<span class="mobile-field-label">기수 보너스 덮어쓰기 (0~900)</span>
<label class="override-checkbox">
<input type="checkbox" disabled={!selectedId} checked={fixedAllBlueNodes[slot]} aria-label={`${slot + 1}번 고정 카드 파란 노드 전체 활성화`} on:change={() => toggleFixedAllBlueNodes(slot)}>
<span aria-hidden="true"></span>
@@ -1451,6 +1456,7 @@
</main>
<style>
.mobile-field-label, .selected-card-name { display: none; }
.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; }
@@ -1601,7 +1607,7 @@
.fixed-field { grid-template-columns: minmax(0, 1fr) 120px 304px 180px; }
}
@media (max-width: 700px) {
@media (max-width: 900px) {
main { width: calc(100% - 24px); padding-top: var(--page-content-top); }
.panel { padding: 17px; }
.song-fields { grid-template-columns: 1fr 100px; }
@@ -1624,4 +1630,19 @@
.analysis-link { grid-column: 1 / -1; grid-row: 5; }
.ranking-detail { padding: 0 10px 14px; }
}
@media (max-width: 900px) {
.song-fields { grid-template-columns: minmax(0, 1fr); }
.section-title { flex-wrap: wrap; }
.fixed-field { grid-template-columns: minmax(0, 1fr); padding: 14px 0; border-top: 1px solid #343c49; gap: 12px; }
.fixed-field > label { grid-template-columns: minmax(0, 1fr); gap: 7px; }
.mobile-field-label { display: block; color: #aab4c3; font-size: 13px; line-height: 1.5; margin-bottom: 6px; }
.selected-card-name { display: block; color: #c1c8d3; font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; }
.fixed-overrides { height: auto; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: end; }
.fixed-overrides .mobile-field-label { margin-bottom: 0; }
.override-checkbox, .generation-input { min-height: 44px; }
.awakening-stepper { height: 46px; grid-template-columns: 44px minmax(0, 1fr) 44px; }
.reduction-options { height: 46px; }
.chart-meta span { white-space: normal; }
}
</style>
+70 -6
View File
@@ -317,6 +317,43 @@
boardViewport.scrollTop = contentY * boardZoom - pointerY;
}
async function changeBoardZoom(factor: number) {
if (!boardViewport) return;
const centerX = boardViewport.clientWidth / 2;
const centerY = boardViewport.clientHeight / 2;
const contentX = (boardViewport.scrollLeft + centerX) / boardZoom;
const contentY = (boardViewport.scrollTop + centerY) / boardZoom;
boardZoom = Math.max(0.4, Math.min(1.8, boardZoom * factor));
await tick();
boardViewport.scrollLeft = contentX * boardZoom - centerX;
boardViewport.scrollTop = contentY * boardZoom - centerY;
}
function manageCardDialog(node: HTMLElement) {
const previousFocus = document.activeElement;
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
node.focus();
function keydown(event: KeyboardEvent) {
if (event.key === 'Escape') { event.preventDefault(); selectedSocketGroupId = ''; }
if (event.key !== 'Tab') return;
const controls = Array.from(node.querySelectorAll<HTMLElement>('button:not(:disabled), input:not(:disabled), [tabindex="0"]'));
const first = controls[0];
const last = controls.at(-1);
if (event.shiftKey && (document.activeElement === first || document.activeElement === node)) {
event.preventDefault(); last?.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault(); first?.focus();
}
}
node.addEventListener('keydown', keydown);
return { destroy() {
node.removeEventListener('keydown', keydown);
document.body.style.overflow = previousOverflow;
if (previousFocus instanceof HTMLElement) previousFocus.focus({ preventScroll: true });
} };
}
function startBoardDrag(event: PointerEvent) {
if (!boardViewport || event.button !== 0 || (event.target as HTMLElement).closest('.board-node')) return;
boardDragging = true;
@@ -1378,6 +1415,13 @@
<p><strong>{selectedActiveBoardNodeCount}</strong> / {selectedBoardNodeCount}개 활성화</p>
<div><button type="button" on:click={activateAllBlueNodes}>모든 파란 노드 활성화</button><button type="button" on:click={() => setAllBoardNodes(true)}>모두 활성화</button><button type="button" on:click={() => setAllBoardNodes(false)}>모두 해제</button></div>
</div>
<div class="board-zoom" aria-label="보드 확대 및 축소">
<button type="button" aria-label="보드 축소" disabled={boardZoom <= .4} on:click={() => changeBoardZoom(1 / 1.25)}>−</button>
<span>{Math.round(boardZoom * 100)}%</span>
<button type="button" aria-label="보드 확대" disabled={boardZoom >= 1.8} on:click={() => changeBoardZoom(1.25)}>+</button>
<button type="button" on:click={fitBoardViewport}>화면 맞춤</button>
</div>
<p class="board-note">보드의 빈 곳을 드래그하면 이동합니다. +·− 버튼으로 확대·축소하고, 보드 바깥을 쓸어 페이지를 이동하세요.</p>
<div
class="board-scroll"
class:dragging={boardDragging}
@@ -1451,7 +1495,7 @@
{#if selectedSocketGroupId}
<div class="card-picker-backdrop" role="presentation" on:click={(event) => { if (event.target === event.currentTarget) selectedSocketGroupId = ''; }}>
<div class="card-picker" role="dialog" aria-modal="true" aria-labelledby="card-picker-title" tabindex="-1">
<div class="card-picker" use:manageCardDialog role="dialog" aria-modal="true" aria-labelledby="card-picker-title" tabindex="-1">
<div class="card-picker-heading">
<div><p class="section-label">연결 노드</p><h3 id="card-picker-title">배치할 카드 선택</h3></div>
<button type="button" aria-label="닫기" on:click={() => selectedSocketGroupId = ''}>×</button>
@@ -1753,7 +1797,7 @@
.board-legend i { width: 9px; height: 9px; border-radius: 50%; }
.board-note { margin: 10px 0 0; color: #7f8999; font-size: 13px; line-height: 1.5; }
.card-picker-backdrop { position: fixed; inset: 0; z-index: 30; display: grid; place-items: center; padding: 20px; background: rgb(5 8 12 / 72%); }
.card-picker { width: min(620px, 100%); max-height: min(760px, calc(100vh - 40px)); overflow: hidden; border: 1px solid #3b4453; border-radius: 16px; background: #171c24; box-shadow: 0 20px 70px rgb(0 0 0 / 45%); }
.card-picker { display: flex; flex-direction: column; width: min(620px, 100%); max-height: min(760px, calc(100dvh - 40px)); overflow: hidden; border: 1px solid #3b4453; border-radius: 16px; background: #171c24; box-shadow: 0 20px 70px rgb(0 0 0 / 45%); }
.card-picker-heading { display: flex; align-items: center; justify-content: space-between; padding: 20px 20px 10px; }
.card-picker-heading h3 { margin: 2px 0 0; color: #edf0f5; font-size: 20px; }
.card-picker-heading button { width: 34px; height: 34px; border: 1px solid #3b4453; border-radius: 9px; color: #c9d0da; background: #242b36; font-size: 22px; cursor: pointer; }
@@ -1763,7 +1807,7 @@
.current-placement span { color: #d990af; font-size: 12px; font-weight: 850; }
.current-placement strong { overflow: hidden; color: #dce1e9; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
.current-placement button { padding: 5px 8px; border: 1px solid #65404f; border-radius: 6px; color: #f0a8c5; background: transparent; font-size: 12px; cursor: pointer; }
.connect-card-list { display: grid; gap: 6px; max-height: 520px; overflow-y: auto; padding: 0 20px 20px; scrollbar-width: thin; }
.connect-card-list { min-height: 0; display: grid; gap: 6px; max-height: 520px; overflow-y: auto; padding: 0 20px 20px; scrollbar-width: thin; }
.connect-card-list > button { display: grid; grid-template-columns: 38px minmax(0, 1fr) 54px auto; align-items: center; gap: 11px; min-height: 66px; padding: 8px 10px; border: 1px solid #303845; border-radius: 10px; color: inherit; background: #12171e; text-align: left; cursor: pointer; }
.connect-card-list > button:hover, .connect-card-list > button.selected { border-color: #a95e7f; background: #211a23; }
.connect-card-rarity { color: #f1a5c5; font-size: 13px; font-weight: 900; }
@@ -1791,7 +1835,7 @@
aside { margin-top: 16px; padding: 18px 20px; border: 1px solid #292f3b; border-radius: 14px; color: #929cad; background: #151920; font-size: 13px; line-height: 1.6; }
aside strong { color: #cbd1db; }
aside p { margin: 5px 0 0; }
@media (max-width: 700px) {
@media (max-width: 900px) {
main { width: min(100% - 24px, 900px); padding-top: var(--page-content-top); }
.summary { grid-template-columns: repeat(2, 1fr); }
.settings-grid { grid-template-columns: 1fr; }
@@ -1822,11 +1866,31 @@
.efficiency-table-head, .efficiency-list > button { grid-template-columns: 28px 210px 100px 90px 110px 115px; min-width: 710px; }
.board-scroll { height: min(58vh, 560px); }
.card-picker-backdrop { align-items: end; padding: 0; }
.card-picker { max-height: 88vh; border-radius: 16px 16px 0 0; }
.connect-card-list { max-height: 56vh; }
.card-picker { max-height: 88dvh; padding-bottom: env(safe-area-inset-bottom); border-radius: 16px 16px 0 0; }
.connect-card-list { max-height: none; overflow-y: auto; }
.connect-card-list > button { grid-template-columns: 28px minmax(0, 1fr) 48px auto; gap: 7px; padding-inline: 8px; }
.connect-range-preview { width: 44px; height: 44px; }
.actions { display: grid; }
.actions .danger { margin: 10px 0 0; }
}
.card-picker-heading, .card-picker-note, .card-picker-search, .current-placement { flex-shrink: 0; }
.connect-card-list { grid-auto-rows: max-content; align-content: start; }
.board-zoom { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.board-zoom button { min-width: 44px; min-height: 44px; padding: 8px 12px; border: 1px solid #465064; border-radius: 8px; color: #d5dbe4; background: #202733; cursor: pointer; }
.board-zoom button:disabled { opacity: .45; cursor: default; }
.board-zoom span { min-width: 42px; text-align: center; }
.board-zoom + .board-note { margin-bottom: 10px; }
@media (max-width: 900px) {
.board-toolbar, .board-toolbar div { flex-wrap: wrap; }
.board-toolbar button, .card-picker-heading button { min-height: 44px; }
.card-picker-heading button { min-width: 44px; }
.card-table .table-row { grid-template-columns: 34px minmax(0, 1fr); }
.awakening-field { grid-column: 2; }
.awakening-stepper { height: 46px; grid-template-columns: 44px minmax(0, 1fr) 44px; }
.identity { flex-wrap: wrap; }
.identity strong, .max-awakening-check > strong, .connect-card-name strong, .current-placement strong { white-space: normal; overflow-wrap: anywhere; }
.connect-card-list > button { grid-template-columns: 28px minmax(0, 1fr) 48px; }
.connect-card-effect { grid-column: 2 / -1; justify-items: start; }
}
</style>