feat: release version 1.0.4
This commit is contained in:
1 parent
bd9d5a6ba6
commit
f910db8901
11 files changed
+506
-105
No files matched your search
+249
-73
@@ -1,43 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import cardSummary from '$lib/data/cards-summary.json';
|
||||
import type { CardListPreferences, UserData } from '$lib/models/user-data';
|
||||
import { skillAtLevel, sortCardsByActive, type CardSummary, type CardType, type HighestStat, type SkillLevel } from '$lib/models/card-list';
|
||||
import type { CardListPreferences, CardListSortKey, CardSkillLevel, SortDirection, UserData } from '$lib/models/user-data';
|
||||
import { loadBrowserData, saveBrowserData } from '$lib/storage/browser-storage';
|
||||
|
||||
type CardType = '큐트' | '퓨어' | '해피';
|
||||
type HighestStat = '퍼포먼스' | '테크닉' | '센스';
|
||||
type SkillConditionStatus = '일반' | '조건부';
|
||||
type ActiveProbability = '높음' | '중간';
|
||||
|
||||
type SkillLevel = {
|
||||
level: number;
|
||||
description: string;
|
||||
coolTimeSeconds: number | null;
|
||||
probabilityPercent: number | null;
|
||||
durationSeconds: number | null;
|
||||
effect?: string;
|
||||
condition?: string;
|
||||
};
|
||||
|
||||
type CardDetails = {
|
||||
title: string;
|
||||
passive: SkillLevel[];
|
||||
active: SkillLevel[];
|
||||
special: SkillLevel[];
|
||||
costume: { name: string; description: string } | null;
|
||||
};
|
||||
|
||||
type CardSummary = {
|
||||
id: string;
|
||||
rarity: number;
|
||||
type: CardType;
|
||||
name: string;
|
||||
versionName: string;
|
||||
generation: string;
|
||||
highestStat: HighestStat;
|
||||
details: CardDetails;
|
||||
};
|
||||
|
||||
const allCards = cardSummary.cards as CardSummary[];
|
||||
const cards = allCards.filter((card) => card.rarity === 5);
|
||||
const typeOptions: CardType[] = ['큐트', '퓨어', '해피'];
|
||||
@@ -59,8 +29,10 @@
|
||||
let selectedActiveProbabilities = [...activeProbabilityOptions];
|
||||
let selectedCardVariants = [...cardVariantOptions];
|
||||
let selectedGenerations = [...generationOptions];
|
||||
let sortKey: CardListSortKey | null = null;
|
||||
let sortDirection: SortDirection = 'asc';
|
||||
let expandedCardId: string | null = null;
|
||||
let selectedSkillLevel = 1;
|
||||
let selectedSkillLevel: CardSkillLevel = 1;
|
||||
let collection: UserData['collection'] = {};
|
||||
let memberBoards: UserData['memberBoards'] = {};
|
||||
let boardCards: UserData['boardCards'] = {};
|
||||
@@ -95,14 +67,19 @@
|
||||
} catch {}
|
||||
});
|
||||
|
||||
$: filteredCards = cards.filter((card) =>
|
||||
selectedTypes.includes(card.type)
|
||||
&& selectedStats.includes(card.highestStat)
|
||||
&& selectedActiveConditions.includes(skillConditionStatus(card.details.active))
|
||||
&& selectedSpecialConditions.includes(skillConditionStatus(card.details.special))
|
||||
&& selectedActiveProbabilities.includes(activeProbabilityStatus(card.details.active))
|
||||
&& selectedCardVariants.includes(cardVariantStatus(card))
|
||||
&& generationMemberships(card.generation).some((generation) => selectedGenerations.includes(generation))
|
||||
$: filteredCards = sortCardsByActive(
|
||||
cards.filter((card) =>
|
||||
selectedTypes.includes(card.type)
|
||||
&& selectedStats.includes(card.highestStat)
|
||||
&& matchesSkillCondition(selectedActiveConditions, card.details.active ?? [])
|
||||
&& selectedSpecialConditions.includes(skillConditionStatus(card.details.special))
|
||||
&& matchesActiveProbability(card.details.active ?? [])
|
||||
&& selectedCardVariants.includes(cardVariantStatus(card))
|
||||
&& generationMemberships(card.generation).some((generation) => selectedGenerations.includes(generation))
|
||||
),
|
||||
sortKey,
|
||||
sortDirection,
|
||||
selectedSkillLevel
|
||||
);
|
||||
|
||||
$: allSelected = selectedTypes.length === typeOptions.length
|
||||
@@ -113,6 +90,14 @@
|
||||
&& selectedCardVariants.length === cardVariantOptions.length
|
||||
&& selectedGenerations.length === generationOptions.length;
|
||||
|
||||
$: allDeselected = selectedTypes.length === 0
|
||||
&& selectedStats.length === 0
|
||||
&& selectedActiveConditions.length === 0
|
||||
&& selectedSpecialConditions.length === 0
|
||||
&& selectedActiveProbabilities.length === 0
|
||||
&& selectedCardVariants.length === 0
|
||||
&& selectedGenerations.length === 0;
|
||||
|
||||
$: if (expandedCardId && !filteredCards.some((card) => card.id === expandedCardId)) {
|
||||
expandedCardId = null;
|
||||
}
|
||||
@@ -127,6 +112,16 @@
|
||||
selectedGenerations = [...generationOptions];
|
||||
}
|
||||
|
||||
function selectNone() {
|
||||
selectedTypes = [];
|
||||
selectedStats = [];
|
||||
selectedActiveConditions = [];
|
||||
selectedSpecialConditions = [];
|
||||
selectedActiveProbabilities = [];
|
||||
selectedCardVariants = [];
|
||||
selectedGenerations = [];
|
||||
}
|
||||
|
||||
function defaultPreferences(): CardListPreferences {
|
||||
return {
|
||||
types: [...typeOptions],
|
||||
@@ -135,7 +130,10 @@
|
||||
specialConditions: [...skillConditionOptions],
|
||||
activeProbabilities: [...activeProbabilityOptions],
|
||||
cardVariants: [...cardVariantOptions],
|
||||
generations: [...generationOptions]
|
||||
generations: [...generationOptions],
|
||||
sortKey: null,
|
||||
sortDirection: 'asc',
|
||||
skillLevel: 1
|
||||
};
|
||||
}
|
||||
|
||||
@@ -147,7 +145,10 @@
|
||||
specialConditions: [...selectedSpecialConditions],
|
||||
activeProbabilities: [...selectedActiveProbabilities],
|
||||
cardVariants: [...selectedCardVariants],
|
||||
generations: [...selectedGenerations]
|
||||
generations: [...selectedGenerations],
|
||||
sortKey,
|
||||
sortDirection,
|
||||
skillLevel: selectedSkillLevel
|
||||
};
|
||||
}
|
||||
|
||||
@@ -160,6 +161,9 @@
|
||||
selectedCardVariants = data.preferences.cardVariants.filter((value) => cardVariantOptions.includes(value));
|
||||
selectedGenerations = [...new Set(data.preferences.generations.flatMap(generationMemberships))]
|
||||
.filter((value) => generationOptions.includes(value));
|
||||
sortKey = data.preferences.sortKey;
|
||||
sortDirection = data.preferences.sortDirection;
|
||||
selectedSkillLevel = data.preferences.skillLevel;
|
||||
collection = data.collection;
|
||||
memberBoards = data.memberBoards;
|
||||
boardCards = data.boardCards;
|
||||
@@ -189,23 +193,55 @@
|
||||
return levels.some((level) => Boolean(level.condition)) ? '조건부' : '일반';
|
||||
}
|
||||
|
||||
function activeProbabilityStatus(levels: SkillLevel[]): ActiveProbability {
|
||||
function matchesSkillCondition(selected: SkillConditionStatus[], levels: SkillLevel[]): boolean {
|
||||
return levels.length === 0
|
||||
? selected.length === skillConditionOptions.length
|
||||
: selected.includes(skillConditionStatus(levels));
|
||||
}
|
||||
|
||||
function activeProbabilityStatus(levels: SkillLevel[]): ActiveProbability | null {
|
||||
if (levels.length === 0) return null;
|
||||
return levels.some((level) => level.probabilityPercent === 55) ? '높음' : '중간';
|
||||
}
|
||||
|
||||
function matchesActiveProbability(levels: SkillLevel[]): boolean {
|
||||
const status = activeProbabilityStatus(levels);
|
||||
return status === null
|
||||
? selectedActiveProbabilities.length === activeProbabilityOptions.length
|
||||
: selectedActiveProbabilities.includes(status);
|
||||
}
|
||||
|
||||
function toggleCard(cardId: string) {
|
||||
expandedCardId = expandedCardId === cardId ? null : cardId;
|
||||
selectedSkillLevel = 1;
|
||||
}
|
||||
|
||||
function skillAtLevel(levels: SkillLevel[], selectedLevel: number) {
|
||||
return levels.find((level) => level.level === selectedLevel) ?? levels.at(-1);
|
||||
function toggleSort(nextSortKey: CardListSortKey) {
|
||||
if (sortKey === nextSortKey) {
|
||||
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||
return;
|
||||
}
|
||||
|
||||
sortKey = nextSortKey;
|
||||
sortDirection = 'asc';
|
||||
}
|
||||
|
||||
function probabilityLabel(probabilityPercent: number | null) {
|
||||
function sortButtonLabel(key: CardListSortKey, label: string) {
|
||||
if (sortKey !== key) return `${label} 오름차순 정렬`;
|
||||
return `${label} ${sortDirection === 'asc' ? '내림차순' : '오름차순'}으로 변경`;
|
||||
}
|
||||
|
||||
function secondsLabel(seconds: number | null | undefined) {
|
||||
return seconds == null ? '—' : `${seconds}초`;
|
||||
}
|
||||
|
||||
function probabilityLabel(probabilityPercent: number | null | undefined) {
|
||||
if (probabilityPercent === 55) return '높음';
|
||||
if (probabilityPercent === 46) return '중간';
|
||||
return probabilityPercent === null ? '—' : `${probabilityPercent}%`;
|
||||
return probabilityPercent == null ? '—' : `${probabilityPercent}%`;
|
||||
}
|
||||
|
||||
function activeEffectLabel(active: SkillLevel | undefined) {
|
||||
return active?.effect || active?.description || '—';
|
||||
}
|
||||
|
||||
function descriptionSegments(description: string, highlightNumbers = false) {
|
||||
@@ -287,7 +323,10 @@
|
||||
<section class="filters" aria-labelledby="filter-heading">
|
||||
<div class="filter-header">
|
||||
<h2 id="filter-heading">출력 조건</h2>
|
||||
<button class="select-all" type="button" onclick={selectAll} disabled={allSelected}>모두 선택</button>
|
||||
<div class="filter-actions">
|
||||
<button class="filter-action" type="button" onclick={selectNone} disabled={allDeselected}>모두 취소</button>
|
||||
<button class="filter-action" type="button" onclick={selectAll} disabled={allSelected}>모두 선택</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-groups">
|
||||
@@ -392,12 +431,30 @@
|
||||
</section>
|
||||
|
||||
<section class="table-panel" aria-label="카드 목록">
|
||||
<div class="list-header" aria-hidden="true">
|
||||
<div class="list-header">
|
||||
<span>타입</span>
|
||||
<span>주스탯</span>
|
||||
<span>기수</span>
|
||||
<span>이름</span>
|
||||
<span></span>
|
||||
<button class="sort-button" type="button" class:active={sortKey === 'coolTime'} aria-label={sortButtonLabel('coolTime', '주기')} onclick={() => toggleSort('coolTime')}>
|
||||
<span>주기</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'coolTime' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
|
||||
</button>
|
||||
<button class="sort-button" type="button" class:active={sortKey === 'probability'} aria-label={sortButtonLabel('probability', '확률')} onclick={() => toggleSort('probability')}>
|
||||
<span>확률</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'probability' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
|
||||
</button>
|
||||
<button class="sort-button" type="button" class:active={sortKey === 'duration'} aria-label={sortButtonLabel('duration', '지속')} onclick={() => toggleSort('duration')}>
|
||||
<span>지속</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'duration' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
|
||||
</button>
|
||||
<button class="sort-button" type="button" class:active={sortKey === 'effect'} aria-label={sortButtonLabel('effect', '효과')} onclick={() => toggleSort('effect')}>
|
||||
<span>효과</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'effect' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
|
||||
</button>
|
||||
<div class="header-level-control">
|
||||
<span>개화</span>
|
||||
<div class="level-toggle header-level-toggle" aria-label="목록에 표시할 스킬 개화 상태">
|
||||
<button type="button" class:active={selectedSkillLevel === 1} aria-pressed={selectedSkillLevel === 1} onclick={() => selectedSkillLevel = 1}>명함</button>
|
||||
<button type="button" class:active={selectedSkillLevel === 2} aria-pressed={selectedSkillLevel === 2} onclick={() => selectedSkillLevel = 2}>5 개화</button>
|
||||
</div>
|
||||
</div>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
@@ -448,13 +505,24 @@
|
||||
{#if card.versionName}<span class="version-badge">{card.versionName}</span>{/if}
|
||||
</span>
|
||||
</span>
|
||||
<span class="summary-cell active-cycle-cell active-summary-cell">
|
||||
<span class="mobile-label">주기</span>
|
||||
<span class="active-value">{secondsLabel(active?.coolTimeSeconds)}</span>
|
||||
</span>
|
||||
<span class="summary-cell active-probability-cell active-summary-cell">
|
||||
<span class="mobile-label">확률</span>
|
||||
<span class="active-value">{probabilityLabel(active?.probabilityPercent)}</span>
|
||||
</span>
|
||||
<span class="summary-cell active-duration-cell active-summary-cell">
|
||||
<span class="mobile-label">지속</span>
|
||||
<span class="active-value">{secondsLabel(active?.durationSeconds)}</span>
|
||||
</span>
|
||||
<span class="summary-cell active-effect-cell active-summary-cell">
|
||||
<span class="mobile-label">효과</span>
|
||||
<span class="active-effect" title={activeEffectLabel(active)}>{activeEffectLabel(active)}</span>
|
||||
</span>
|
||||
<span class="summary-cell level-cell">
|
||||
{#if expanded}
|
||||
<div class="level-toggle inline-level-toggle" aria-label="스킬 개화 상태">
|
||||
<button type="button" class:active={selectedSkillLevel === 1} aria-pressed={selectedSkillLevel === 1} onclick={() => selectedSkillLevel = 1}>명함</button>
|
||||
<button type="button" class:active={selectedSkillLevel === 2} aria-pressed={selectedSkillLevel === 2} onclick={() => selectedSkillLevel = 2}>5 개화</button>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="skill-level-label">{selectedSkillLevel === 1 ? '명함' : '5 개화'}</span>
|
||||
</span>
|
||||
<span class="chevron" aria-hidden="true"></span>
|
||||
</div>
|
||||
@@ -704,7 +772,12 @@
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.select-all {
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-action {
|
||||
padding: 7px 12px;
|
||||
border: 1px solid #dce1ea;
|
||||
border-radius: 9px;
|
||||
@@ -717,13 +790,13 @@
|
||||
transition: border-color 150ms ease, color 150ms ease, background 150ms ease;
|
||||
}
|
||||
|
||||
.select-all:hover:not(:disabled) {
|
||||
.filter-action:hover:not(:disabled) {
|
||||
border-color: #ef8bb5;
|
||||
color: #d83f82;
|
||||
background: #fff8fb;
|
||||
}
|
||||
|
||||
.select-all:disabled {
|
||||
.filter-action:disabled {
|
||||
opacity: 0.42;
|
||||
cursor: default;
|
||||
}
|
||||
@@ -831,7 +904,8 @@
|
||||
}
|
||||
|
||||
.table-panel {
|
||||
overflow: hidden;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border: 1px solid #e3e7ef;
|
||||
border-radius: 22px;
|
||||
background: rgb(255 255 255 / 90%);
|
||||
@@ -842,11 +916,12 @@
|
||||
.list-header,
|
||||
.card-summary {
|
||||
display: grid;
|
||||
grid-template-columns: clamp(72px, 5vw, 88px) clamp(84px, 6vw, 104px) clamp(150px, 11vw, 184px) minmax(280px, 1fr) clamp(116px, 9vw, 144px) 40px;
|
||||
grid-template-columns: 62px 72px 110px minmax(160px, 1fr) 66px 66px 66px minmax(160px, 1.15fr) 190px 32px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
min-width: 1260px;
|
||||
min-height: 54px;
|
||||
padding: 0 24px;
|
||||
border-bottom: 1px solid #e8ebf1;
|
||||
@@ -856,7 +931,44 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.list-header .sort-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 5px;
|
||||
width: fit-content;
|
||||
padding: 6px 7px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, background 150ms ease;
|
||||
}
|
||||
|
||||
.list-header .sort-button:hover,
|
||||
.list-header .sort-button.active {
|
||||
color: #d83f82;
|
||||
background: #fff0f7;
|
||||
}
|
||||
|
||||
.list-header .sort-button:focus-visible {
|
||||
outline: 3px solid rgb(73 134 231 / 25%);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
color: #a4adbb;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.list-header .sort-button.active .sort-indicator {
|
||||
color: currentcolor;
|
||||
}
|
||||
|
||||
.card-entry {
|
||||
min-width: 1260px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
}
|
||||
|
||||
@@ -915,11 +1027,49 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.active-summary-cell {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.active-value,
|
||||
.active-effect {
|
||||
color: #4d586d;
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.active-effect {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.level-cell {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.header-level-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.header-level-toggle {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.skill-level-label {
|
||||
padding: 4px 9px;
|
||||
border-radius: 999px;
|
||||
color: #b43d72;
|
||||
background: #fff0f7;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mobile-label {
|
||||
display: none;
|
||||
}
|
||||
@@ -958,12 +1108,6 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.inline-level-toggle {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.level-toggle button {
|
||||
min-width: 50px;
|
||||
padding: 3px 7px;
|
||||
@@ -1395,13 +1539,13 @@
|
||||
box-shadow: 0 14px 42px rgb(0 0 0 / 24%);
|
||||
}
|
||||
|
||||
.select-all {
|
||||
.filter-action {
|
||||
border-color: #343c4b;
|
||||
color: #b5bdcb;
|
||||
background: #1d2430;
|
||||
}
|
||||
|
||||
.select-all:hover:not(:disabled) {
|
||||
.filter-action:hover:not(:disabled) {
|
||||
border-color: #c85b8b;
|
||||
color: #f08db7;
|
||||
background: #2a1c27;
|
||||
@@ -1442,6 +1586,22 @@
|
||||
background: #131923;
|
||||
}
|
||||
|
||||
.list-header .sort-button:hover,
|
||||
.list-header .sort-button.active {
|
||||
color: #f08db7;
|
||||
background: #2a1b26;
|
||||
}
|
||||
|
||||
.active-value,
|
||||
.active-effect {
|
||||
color: #c2cad8;
|
||||
}
|
||||
|
||||
.skill-level-label {
|
||||
color: #f08db7;
|
||||
background: #2a1b26;
|
||||
}
|
||||
|
||||
.card-entry {
|
||||
border-color: #282f3d;
|
||||
}
|
||||
@@ -1574,7 +1734,7 @@
|
||||
@media (max-width: 1100px) {
|
||||
.list-header,
|
||||
.card-summary {
|
||||
grid-template-columns: 72px 84px 150px minmax(0, 1fr) 116px 36px;
|
||||
grid-template-columns: 62px 72px 110px minmax(160px, 1fr) 66px 66px 66px minmax(160px, 1.15fr) 190px 32px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1625,12 +1785,14 @@
|
||||
}
|
||||
|
||||
.card-entry {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-summary {
|
||||
display: grid;
|
||||
grid-template-columns: 72px 84px minmax(0, 1fr);
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
min-width: 0;
|
||||
gap: 14px 12px;
|
||||
padding: 19px 0;
|
||||
padding-right: 32px;
|
||||
@@ -1661,6 +1823,20 @@
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.active-summary-cell {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.active-effect-cell {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.active-effect {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.level-cell {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
Reference in new issue
Block a user