366 lines
22 KiB
Svelte
366 lines
22 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from 'svelte';
|
||
import ToolsNav from '$lib/components/ToolsNav.svelte';
|
||
import HoloWorkBonuses from '$lib/components/HoloWorkBonuses.svelte';
|
||
import cardSummary from '$lib/data/cards-summary.json';
|
||
import { calculateHoloWorkBoardBonuses, sumHoloWorkBoardBonuses, type HoloWorkBoardBonus } from '$lib/engine/board-effects';
|
||
import HoloWorkProgress from '$lib/components/HoloWorkProgress.svelte';
|
||
import boardSummary from '$lib/data/boards-summary.json';
|
||
import { adjustWorkTeam, isWorkCount, parseWorkTeams, workAchievement, type HoloWorkCounts } from '$lib/storage/holo-work';
|
||
import type { HoloWorkTeam } from '$lib/models/user-data';
|
||
import { recommendWorkMembers, sortHoloWorkMembers, type HoloWorkSort } from '$lib/models/holo-work-sort';
|
||
import achievements from '$lib/data/holo-work-achievements.json';
|
||
import { USER_DATA_STORAGE_KEY, loadBrowserData, saveBrowserData } from '$lib/storage/browser-storage';
|
||
import { defaultPreferences } from '$lib/models/default-preferences';
|
||
|
||
const members = [...boardSummary.members].sort((a, b) => a.order - b.order);
|
||
let bonuses: Record<string, HoloWorkBoardBonus> = {};
|
||
let counts: HoloWorkCounts = {};
|
||
let teams: HoloWorkTeam[] = [];
|
||
let teamName = '';
|
||
let selectedMembers = ['', '', '', '', ''];
|
||
const thresholds = achievements.thresholdsByMember;
|
||
let pinned = [false, false, false, false, false];
|
||
let recommendationCounts: HoloWorkCounts | null = null;
|
||
$: recommendationStale = recommendationCounts !== null && members.some(({ id }) => (counts[id] ?? 0) !== (recommendationCounts?.[id] ?? 0));
|
||
let editingId = '';
|
||
let deletingId = '';
|
||
$: validTeam = Boolean(teamName.trim()) && selectedMembers.every((id) => members.some((member) => member.id === id)) && new Set(selectedMembers).size === 5;
|
||
let ready = false;
|
||
let search = '';
|
||
let sort: HoloWorkSort = 'member';
|
||
let sortedMembers = [...members];
|
||
let status = '';
|
||
let error = '';
|
||
$: visibleMembers = sortedMembers.filter((member) => member.name.replace(/\s/g, '').toLocaleLowerCase().includes(search.replace(/\s/g, '').toLocaleLowerCase()));
|
||
$: total = members.reduce((sum, member) => sum + BigInt(counts[member.id] ?? 0), 0n);
|
||
$: recorded = members.filter((member) => (counts[member.id] ?? 0) > 0).length;
|
||
|
||
function applySort(nextSort: HoloWorkSort = sort) {
|
||
sort = nextSort;
|
||
sortedMembers = sortHoloWorkMembers(members, counts, sort, thresholds);
|
||
status = '현재 횟수로 정렬했습니다.';
|
||
}
|
||
|
||
function loadLatestData() {
|
||
const data = loadBrowserData(defaultPreferences());
|
||
bonuses = calculateHoloWorkBoardBonuses(boardSummary, data.memberBoards, {
|
||
placements: data.boardCards,
|
||
cards: cardSummary.cards.filter((card) => card.details.connect.length > 0).map((card) => ({
|
||
id: card.id, awakening: data.collection[card.id]?.awakening ?? 0, connect: card.details.connect
|
||
}))
|
||
});
|
||
return data;
|
||
}
|
||
|
||
function read() {
|
||
try {
|
||
const data = loadLatestData();
|
||
counts = data.holoWorkCounts;
|
||
teams = data.holoWorkTeams;
|
||
ready = true;
|
||
error = '';
|
||
} catch {
|
||
ready = false;
|
||
error = '저장된 기록을 읽을 수 없습니다. 브라우저 저장소 설정이나 기록을 확인한 뒤 다시 불러와 주세요.';
|
||
}
|
||
}
|
||
|
||
function update(id: string, value: number) {
|
||
if (!ready) return;
|
||
if (!isWorkCount(value)) {
|
||
error = '횟수는 0 이상의 정수로 입력해 주세요.';
|
||
return;
|
||
}
|
||
try {
|
||
// Read the latest document before changing one member to preserve other tabs' edits.
|
||
const data = loadLatestData();
|
||
const next = { ...data.holoWorkCounts, [id]: value };
|
||
saveBrowserData({ ...data, holoWorkCounts: next });
|
||
counts = next;
|
||
error = '';
|
||
status = `${members.find((member) => member.id === id)?.name}: ${value.toLocaleString('ko-KR')}회 저장했습니다.`;
|
||
} catch {
|
||
error = '기록을 저장하지 못했습니다. 브라우저 저장소 설정을 확인한 뒤 다시 시도해 주세요.';
|
||
}
|
||
}
|
||
|
||
function changeInput(id: string, input: HTMLInputElement) {
|
||
update(id, input.value === '' ? NaN : input.valueAsNumber);
|
||
input.value = String(counts[id] ?? 0);
|
||
}
|
||
|
||
function adjust(id: string, amount: number) {
|
||
try {
|
||
const latest = loadLatestData().holoWorkCounts;
|
||
update(id, (latest[id] ?? 0) + amount);
|
||
} catch { read(); }
|
||
}
|
||
|
||
function clearEditor() {
|
||
editingId = '';
|
||
teamName = '';
|
||
selectedMembers = ['', '', '', '', ''];
|
||
pinned = [false, false, false, false, false];
|
||
recommendationCounts = null;
|
||
}
|
||
|
||
function editTeam(team: HoloWorkTeam) {
|
||
editingId = team.id;
|
||
teamName = team.name;
|
||
selectedMembers = [...team.memberIds];
|
||
pinned = [false, false, false, false, false];
|
||
recommendationCounts = null;
|
||
document.getElementById('team-name')?.focus();
|
||
}
|
||
|
||
function chooseMember(index: number, id: string) {
|
||
selectedMembers[index] = id;
|
||
pinned[index] = Boolean(id);
|
||
}
|
||
|
||
function refreshRecommendation() {
|
||
if (!ready) return;
|
||
try {
|
||
counts = loadLatestData().holoWorkCounts;
|
||
selectedMembers = recommendWorkMembers(members, counts, thresholds, selectedMembers, pinned);
|
||
recommendationCounts = { ...counts };
|
||
error = '';
|
||
status = selectedMembers.every(Boolean)
|
||
? '고정 멤버를 유지하고 업적에 가까운 멤버를 추천했습니다. 조합 저장을 눌러 확정하세요.'
|
||
: '추천할 멤버가 부족합니다. 남은 자리는 직접 선택해 주세요.';
|
||
} catch { error = '추천을 불러오지 못했습니다. 다시 시도해 주세요.'; }
|
||
}
|
||
|
||
function saveTeam() {
|
||
if (!ready || !validTeam) return;
|
||
try {
|
||
const data = loadLatestData();
|
||
if (editingId && !data.holoWorkTeams.some((team) => team.id === editingId)) throw new Error('다른 탭에서 삭제된 조합입니다. 새 조합으로 저장해 주세요.');
|
||
const team = { id: editingId || crypto.randomUUID(), name: teamName.trim(), memberIds: [...selectedMembers] };
|
||
const next = parseWorkTeams(editingId ? data.holoWorkTeams.map((item) => item.id === editingId ? team : item) : [...data.holoWorkTeams, team]);
|
||
saveBrowserData({ ...data, holoWorkTeams: next });
|
||
counts = data.holoWorkCounts;
|
||
teams = next;
|
||
clearEditor();
|
||
error = '';
|
||
status = `${team.name} 조합을 저장했습니다.`;
|
||
} catch (cause) { error = cause instanceof Error ? cause.message : '조합을 저장하지 못했습니다.'; }
|
||
}
|
||
|
||
function deleteTeam(id: string) {
|
||
try {
|
||
const data = loadLatestData();
|
||
const next = data.holoWorkTeams.filter((team) => team.id !== id);
|
||
saveBrowserData({ ...data, holoWorkTeams: next });
|
||
counts = data.holoWorkCounts;
|
||
teams = next;
|
||
deletingId = '';
|
||
if (editingId === id) clearEditor();
|
||
error = '';
|
||
status = '조합을 삭제했습니다. 멤버별 횟수는 유지됩니다.';
|
||
} catch { error = '조합을 삭제하지 못했습니다. 다시 시도해 주세요.'; }
|
||
}
|
||
|
||
function adjustTeam(id: string, amount: 1 | -1) {
|
||
try {
|
||
const data = loadLatestData();
|
||
const team = data.holoWorkTeams.find((item) => item.id === id);
|
||
if (!team || team.memberIds.some((id) => !members.some((member) => member.id === id))) throw new Error('조합의 멤버를 확인해 주세요.');
|
||
const next = adjustWorkTeam(data.holoWorkCounts, team.memberIds, amount);
|
||
saveBrowserData({ ...data, holoWorkCounts: next });
|
||
counts = next;
|
||
teams = data.holoWorkTeams;
|
||
error = '';
|
||
status = `${team.name}: 5명 모두 ${amount === 1 ? '1회 추가' : '1회 감소'}했습니다.`;
|
||
} catch (cause) { error = cause instanceof Error ? cause.message : '횟수를 저장하지 못했습니다.'; }
|
||
}
|
||
|
||
onMount(() => {
|
||
read();
|
||
const sync = (event: StorageEvent) => {
|
||
if (event.storageArea === localStorage && (event.key === USER_DATA_STORAGE_KEY || event.key === null)) {
|
||
read();
|
||
status = '다른 탭의 기록을 반영했습니다.';
|
||
}
|
||
};
|
||
window.addEventListener('storage', sync);
|
||
return () => window.removeEventListener('storage', sync);
|
||
});
|
||
</script>
|
||
|
||
<svelte:head>
|
||
<title>홀로워크 관리 · 도구 · 갤산기</title>
|
||
<meta name="description" content="멤버별 Holo워크 파견 횟수를 기록하고 확인하세요." />
|
||
</svelte:head>
|
||
|
||
<main>
|
||
<header><h1>도구</h1><p>플레이할 때 유용한 도구를 모았습니다.</p></header>
|
||
<ToolsNav active="holo-work" />
|
||
<h2>홀로워크 관리</h2>
|
||
<p class="description">각 멤버를 Holo워크에 보낸 횟수를 기록하세요. 기존 횟수를 직접 입력하거나, 보낼 때마다 +1을 누르면 됩니다.</p>
|
||
<p class="hint">횟수 / 다음 업적 요구량으로 표시합니다. 업적은 Holo워크 <strong>완료 횟수</strong> 기준이며, 400회 달성 후에는 최종 목표와 완료 표시를 유지합니다.</p>
|
||
<p class="hint">보드 보너스는 <a href="/data/boards">사용자 데이터의 멤버 보드</a>에 저장된 활성 노드와 연결 카드 증폭을 반영합니다. 조합 합계는 보상 종류별 획득량 증가율의 합입니다.</p>
|
||
<section class="summary" aria-label="파견 기록 요약">
|
||
<div><span>전체 파견 횟수</span><strong>{ready ? total.toLocaleString('ko-KR') : '—'}<small>회</small></strong></div>
|
||
<div><span>파견한 멤버</span><strong>{ready ? recorded : '—'}<small>/ {members.length}명</small></strong></div>
|
||
</section>
|
||
<p role="status" class="status">{status}</p>
|
||
{#if error}<p role="alert" class="error">{error}</p>{/if}
|
||
{#if !ready}<button class="reload" on:click={read}>기록 다시 불러오기</button>{/if}
|
||
<section class="team-panel" aria-labelledby="team-editor-title">
|
||
<h2 id="team-editor-title">{editingId ? '조합 수정' : '5명 조합 저장'}</h2>
|
||
<form on:submit|preventDefault={saveTeam}>
|
||
<label for="team-name">조합 이름</label>
|
||
<input id="team-name" bind:value={teamName} maxlength="80" placeholder="예: 매일 보내는 조합" required disabled={!ready} />
|
||
<p class="hint">직접 선택한 멤버는 고정됩니다. 추천 갱신을 누르면 고정하지 않은 자리를 다음 업적에 가까운 멤버로 채웁니다.</p>
|
||
<div class="team-actions">
|
||
<button type="button" on:click={refreshRecommendation} disabled={!ready || pinned.every(Boolean)}>추천 갱신</button>
|
||
<button type="button" on:click={() => pinned = [false, false, false, false, false]} disabled={!pinned.some(Boolean)}>고정 모두 해제</button>
|
||
{#if recommendationStale}<span class="refresh-hint">횟수가 변경되었습니다. 추천을 갱신해 주세요.</span>{/if}
|
||
</div>
|
||
<div class="member-selects">
|
||
{#each selectedMembers as memberId, index}
|
||
<div class="member-slot" class:pinned={pinned[index]}>
|
||
<label>멤버 {index + 1}
|
||
<select value={memberId} on:change={(event) => chooseMember(index, event.currentTarget.value)} required disabled={!ready}>
|
||
<option value="">멤버 선택</option>
|
||
{#each members as member}
|
||
<option value={member.id} disabled={selectedMembers.includes(member.id) && memberId !== member.id}>{member.name}</option>
|
||
{/each}
|
||
</select>
|
||
</label>
|
||
<button type="button" aria-pressed={pinned[index]} aria-label={`멤버 ${index + 1} 고정`} on:click={() => pinned[index] = !pinned[index]} disabled={!ready || !memberId}>{pinned[index] ? '고정됨' : '고정'}</button>
|
||
{#if memberId}<HoloWorkProgress memberId={memberId} count={counts[memberId] ?? 0} /><HoloWorkBonuses bonus={bonuses[memberId]} />{/if}
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
<div class="team-bonus-total"><h4>선택한 조합 보드 보너스 합계</h4><HoloWorkBonuses bonus={sumHoloWorkBoardBonuses(bonuses, selectedMembers.filter(Boolean))} showEmpty /></div>
|
||
<div class="team-actions">
|
||
<button class="plus" type="submit" disabled={!ready || !validTeam}>{editingId ? '변경 저장' : '조합 저장'}</button>
|
||
{#if editingId}<button type="button" on:click={clearEditor}>수정 취소</button>{/if}
|
||
</div>
|
||
</form>
|
||
</section>
|
||
<section class="saved-teams" aria-labelledby="saved-teams-title">
|
||
<h2 id="saved-teams-title">저장한 조합 <small>{teams.length}개</small></h2>
|
||
<p class="hint">같은 멤버의 횟수는 모든 조합에 함께 반영됩니다. 조합을 삭제해도 횟수는 유지됩니다.</p>
|
||
{#each teams as team (team.id)}
|
||
<article class="team-panel" aria-label={`${team.name} 조합`}>
|
||
<h3>{team.name}</h3>
|
||
<ul class="team-members">
|
||
{#each team.memberIds as id}
|
||
<li><div class="member-info"><span>{members.find((member) => member.id === id)?.name ?? '알 수 없는 멤버'}</span><HoloWorkBonuses bonus={bonuses[id]} /></div><HoloWorkProgress memberId={id} count={counts[id] ?? 0} /></li>
|
||
{/each}
|
||
</ul>
|
||
<div class="team-bonus-total"><h4>조합 보드 보너스 합계</h4><HoloWorkBonuses bonus={sumHoloWorkBoardBonuses(bonuses, team.memberIds)} showEmpty /></div>
|
||
<div class="team-actions">
|
||
<button class="plus" on:click={() => adjustTeam(team.id, 1)} disabled={!ready || team.memberIds.some((id) => (counts[id] ?? 0) >= Number.MAX_SAFE_INTEGER)}>5명 모두 +1</button>
|
||
<button on:click={() => adjustTeam(team.id, -1)} disabled={!ready || team.memberIds.some((id) => !(counts[id] > 0))}>5명 모두 −1</button>
|
||
<button on:click={() => editTeam(team)} disabled={!ready}>수정</button>
|
||
<button on:click={() => deletingId = team.id} disabled={!ready}>삭제</button>
|
||
</div>
|
||
{#if deletingId === team.id}
|
||
<div class="delete-confirm"><p>‘{team.name}’ 조합을 삭제할까요? 횟수는 유지됩니다.</p><button on:click={() => deleteTeam(team.id)}>삭제 확인</button> <button on:click={() => deletingId = ''}>취소</button></div>
|
||
{/if}
|
||
</article>
|
||
{:else}<p class="hint">저장한 조합이 없습니다. 서로 다른 멤버 5명을 선택해 저장하세요.</p>{/each}
|
||
</section>
|
||
<h2 class="all-members-title">멤버별 기록</h2>
|
||
<div class="filters">
|
||
<label>멤버 검색<input type="search" bind:value={search} placeholder="멤버 이름 입력" /></label>
|
||
<label>정렬<select value={sort} on:change={(event) => applySort(event.currentTarget.value as HoloWorkSort)}><option value="member">멤버순</option><option value="asc">횟수 적은 순</option><option value="desc">횟수 많은 순</option><option value="achievement">업적 가까운 순</option></select></label>
|
||
</div>
|
||
<div class="sort-actions"><p class="hint">횟수를 변경해도 위치는 유지됩니다. 정렬 기준을 바꾸거나 다시 정렬하면 최신 횟수를 반영합니다.</p><button type="button" on:click={() => applySort()} disabled={!ready}>다시 정렬</button></div>
|
||
<ul class="member-grid" aria-label="멤버별 Holo워크 횟수">
|
||
{#each visibleMembers as member (member.id)}
|
||
{@const count = counts[member.id] ?? 0}
|
||
{@const progress = workAchievement(count, (thresholds as Record<string, number[]>)[member.id] ?? [])}
|
||
<li>
|
||
<strong class="member-name">{member.name}</strong>
|
||
<div class="counter">
|
||
<button on:click={() => adjust(member.id, -1)} disabled={!ready || !(counts[member.id] > 0)} aria-label={`${member.name} 횟수 1 감소`}>−1</button>
|
||
<input type="number" min="0" max={Number.MAX_SAFE_INTEGER} step="1" value={counts[member.id] ?? 0} disabled={!ready} on:change={(event) => changeInput(member.id, event.currentTarget)} aria-label={`${member.name} 파견 횟수`} />
|
||
<button class="plus" on:click={() => adjust(member.id, 1)} disabled={!ready || counts[member.id] === Number.MAX_SAFE_INTEGER} aria-label={`${member.name} 횟수 1 증가`}>+1</button>
|
||
<span class="achievement-target">/ {progress.target?.toLocaleString('ko-KR') ?? '—'}회</span>
|
||
</div>
|
||
{#if progress.complete}<p class="achievement-note complete">모든 업적 완료</p>{/if}
|
||
<HoloWorkBonuses bonus={bonuses[member.id]} />
|
||
</li>
|
||
{:else}<li class="empty">검색에 맞는 멤버가 없습니다.</li>{/each}
|
||
</ul>
|
||
</main>
|
||
|
||
<style>
|
||
main { width: min(1600px, 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; }
|
||
h2 { margin: 0; font-size: 22px; }
|
||
header p, .description { color: #a4adba; line-height: 1.7; }
|
||
.hint { color: #a4adba; font-size: 13px; line-height: 1.7; }
|
||
.summary { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 24px 0; }
|
||
.summary div { padding: 20px; border: 1px solid #694254; border-radius: 12px; background: rgb(237 139 181 / 6%); }
|
||
.summary span { color: #a4adba; font-size: 13px; }
|
||
.summary strong { display: block; margin-top: 10px; color: #f3b4d0; font-size: 26px; overflow-wrap: anywhere; }
|
||
small { margin-left: 7px; color: #a4adba; font-size: 13px; font-weight: 400; }
|
||
.filters { display: grid; grid-template-columns: 1fr 150px; gap: 12px; }
|
||
.sort-actions { display: flex; align-items: center; gap: 12px; justify-content: space-between; margin-top: 12px; }
|
||
.sort-actions .hint { margin: 0; }
|
||
.sort-actions button { flex-shrink: 0; padding: 8px 12px; font-size: 13px; }
|
||
label { display: grid; gap: 8px; color: #a4adba; font-size: 13px; }
|
||
input, select { box-sizing: border-box; min-width: 0; width: 100%; height: 44px; padding: 0 12px; border: 1px solid #414b5b; border-radius: 9px; background: #101319; color: #e8ebf1; font: inherit; }
|
||
.status { min-height: 20px; color: #f3b4d0; font-size: 13px; }
|
||
.error { color: #f3a1a1; line-height: 1.7; }
|
||
ul { padding: 0; list-style: none; border: 1px solid #343c49; border-radius: 14px; background: #151a22; overflow: hidden; }
|
||
li { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 16px 20px; border-bottom: 1px solid #343c49; }
|
||
li:last-child { border-bottom: 0; }
|
||
.counter { display: flex; align-items: center; gap: 6px; }
|
||
.counter input { width: 76px; height: 36px; padding: 0 4px; text-align: center; font-size: 16px; font-variant-numeric: tabular-nums; appearance: textfield; -moz-appearance: textfield; }
|
||
.counter input::-webkit-inner-spin-button, .counter input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
|
||
.counter button { min-width: 36px; min-height: 36px; padding: 0 4px; border-radius: 7px; font-size: 13px; }
|
||
.achievement-target { margin-left: 4px; color: #a4adba; font-size: 14px; white-space: nowrap; }
|
||
.achievement-note { margin: 0; color: #a4adba; font-size: 12px; line-height: 1.5; }
|
||
.achievement-note.complete { color: #98dbbc; }
|
||
button { min-width: 44px; min-height: 44px; border: 1px solid #414b5b; border-radius: 9px; background: #222936; color: #e8ebf1; font: inherit; font-weight: 700; cursor: pointer; }
|
||
button.plus { background: #ed8bb5; border-color: #ed8bb5; color: #21121a; }
|
||
button:hover:not(:disabled) { filter: brightness(1.15); }
|
||
button:disabled { opacity: .4; cursor: not-allowed; }
|
||
button:focus-visible, input:focus-visible, select:focus-visible { outline: 3px solid #ed8bb5; outline-offset: 3px; }
|
||
.reload { padding: 0 14px; }
|
||
.empty { color: #a4adba; }
|
||
.hint a { color: #f3b4d0; }
|
||
.team-bonus-total { padding: 14px; margin: 14px 0; background: rgb(226 190 95 / 5%); border: 1px solid #65552c; border-radius: 10px; }
|
||
.team-bonus-total h4 { margin: 0 0 10px; color: #e8d69d; font-size: 13px; }
|
||
.member-info { display: grid; gap: 6px; }
|
||
.team-panel { margin: 20px 0; padding: 20px; border: 1px solid #343c49; border-radius: 14px; background: #151a22; }
|
||
form { display: grid; gap: 12px; margin-top: 18px; }
|
||
.member-selects { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
||
.team-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||
.team-actions button, .delete-confirm button { padding: 8px 12px; }
|
||
.saved-teams { margin-top: 30px; }
|
||
h3 { margin: 0; font-size: 18px; overflow-wrap: anywhere; }
|
||
.team-members { border: 0; margin: 12px 0; }
|
||
.team-members li { padding: 10px 0; }
|
||
.delete-confirm { color: #f3b4d0; font-size: 14px; }
|
||
.all-members-title { margin: 32px 0 18px; }
|
||
.member-slot { display: grid; align-content: start; gap: 10px; padding: 12px; border: 1px solid #343c49; border-radius: 10px; min-width: 0; }
|
||
.member-slot.pinned { border-color: #ed8bb5; }
|
||
.member-slot button[aria-pressed="true"] { color: #f3b4d0; }
|
||
.refresh-hint { align-self: center; color: #e8d69d; font-size: 13px; }
|
||
.member-grid, .team-members { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); gap: 12px; border: 0; background: transparent; overflow: visible; }
|
||
.member-grid li, .team-members li { min-width: 0; flex-direction: column; align-items: stretch; padding: 16px; border: 1px solid #343c49; border-radius: 12px; background: #151a22; }
|
||
.member-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 16px; }
|
||
.member-grid li { justify-content: flex-start; padding: 16px; gap: 10px; }
|
||
.member-name { min-width: 0; overflow-wrap: anywhere; font-size: 15px; line-height: 1.5; }
|
||
.member-grid .empty { grid-column: 1 / -1; }
|
||
@media (min-width: 1024px) {
|
||
.team-members { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
|
||
.team-members li { padding: 12px; overflow-wrap: anywhere; }
|
||
}
|
||
@media (pointer: coarse) { .counter button { min-width: 36px; min-height: 40px; } .counter input { height: 40px; font-size: 16px; } }
|
||
@media (max-width: 1439px) { .member-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
||
@media (max-width: 1199px) { .member-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||
@media (max-width: 899px) { .member-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||
@media (max-width: 599px) { .member-grid { grid-template-columns: minmax(0, 1fr); gap: 12px; } }
|
||
@media (max-width: 540px) { .summary { gap: 10px; } .summary div { padding: 16px; } .filters { grid-template-columns: minmax(0, 1fr) 130px; } }
|
||
</style>
|