720 lines
26 KiB
Vue
720 lines
26 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
|
|
import { useGameFeedback } from '../composables/useGameFeedback';
|
|
import { useSessionStore } from '../stores/session';
|
|
import { useClockDisplay } from '../composables/useClockDisplay';
|
|
const { formatTime: formatSeoulDateTime } = useClockDisplay();
|
|
import { resolveGeneralIconUrl, useDefaultGeneralIcon } from '../utils/generalIcon';
|
|
import { legacyLuminanceTextColor } from '../utils/legacyNationColor';
|
|
import { trpc } from '../utils/trpc';
|
|
|
|
type JoinConfig = Awaited<ReturnType<typeof trpc.join.getConfig.query>>;
|
|
type Reservation = Awaited<ReturnType<typeof trpc.join.getSelectionPool.mutate>>;
|
|
type Candidate = Reservation['candidates'][number];
|
|
type Nation = JoinConfig['nations'][number];
|
|
type PendingSelectionAction = {
|
|
operation: 'create' | 'reselect';
|
|
uniqueName: string;
|
|
personality?: string;
|
|
iconId?: string;
|
|
clientRequestId: string;
|
|
};
|
|
|
|
const router = useRouter();
|
|
const session = useSessionStore();
|
|
const { error: showErrorToast, showDialog } = useGameFeedback();
|
|
|
|
const config = ref<JoinConfig | null>(null);
|
|
const reservation = ref<Reservation | null>(null);
|
|
const selectedUniqueName = ref<string | null>(null);
|
|
const nations = ref<Nation[]>([]);
|
|
const personality = ref('Random');
|
|
const selectedIconId = ref('');
|
|
const loading = ref(true);
|
|
const submitting = ref(false);
|
|
const error = ref('');
|
|
const now = ref(Date.now());
|
|
let timer: number | null = null;
|
|
const pendingActionStorageKey = 'sammo-select-pool-pending-action';
|
|
|
|
const candidates = computed(() => reservation.value?.candidates ?? []);
|
|
const selectedCandidate = computed(
|
|
() => candidates.value.find((candidate) => candidate.uniqueName === selectedUniqueName.value) ?? null
|
|
);
|
|
const hasGeneral = computed(() => reservation.value?.hasGeneral ?? config.value?.selectionPool.hasGeneral ?? false);
|
|
const allowPersonality = computed(() => config.value?.selectionPool.allowOptions.includes('ego') ?? false);
|
|
const personalities = computed(() => config.value?.personalities ?? []);
|
|
const serverInfo = computed(() => config.value?.serverInfo ?? null);
|
|
const validUntil = computed(() => {
|
|
const value = reservation.value?.validUntil;
|
|
return value ? new Date(value).getTime() : 0;
|
|
});
|
|
const expired = computed(() => validUntil.value > 0 && now.value > validUntil.value);
|
|
const validUntilColor = computed(() => {
|
|
const remaining = validUntil.value - now.value;
|
|
if (remaining <= 0 || remaining > 30_000) {
|
|
return '#fff';
|
|
}
|
|
const channel = Math.max(0, Math.round((255 * remaining) / 30_000));
|
|
return `rgb(255, ${channel}, ${channel})`;
|
|
});
|
|
|
|
const errorText = (value: unknown): string =>
|
|
value instanceof Error ? value.message : typeof value === 'string' ? value : 'unknown_error';
|
|
|
|
const readPendingAction = (): PendingSelectionAction | null => {
|
|
try {
|
|
const raw = window.sessionStorage.getItem(pendingActionStorageKey);
|
|
if (!raw) return null;
|
|
const value = JSON.parse(raw) as Partial<PendingSelectionAction>;
|
|
if (
|
|
(value.operation !== 'create' && value.operation !== 'reselect') ||
|
|
typeof value.uniqueName !== 'string' ||
|
|
typeof value.clientRequestId !== 'string' ||
|
|
(value.iconId !== undefined && typeof value.iconId !== 'string')
|
|
) {
|
|
return null;
|
|
}
|
|
return value as PendingSelectionAction;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const getPendingAction = (
|
|
operation: PendingSelectionAction['operation'],
|
|
uniqueName: string,
|
|
requestedPersonality?: string,
|
|
requestedIconId?: string
|
|
): PendingSelectionAction => {
|
|
const current = readPendingAction();
|
|
if (
|
|
current?.operation === operation &&
|
|
current.uniqueName === uniqueName &&
|
|
current.personality === requestedPersonality &&
|
|
current.iconId === requestedIconId
|
|
) {
|
|
return current;
|
|
}
|
|
const next: PendingSelectionAction = {
|
|
operation,
|
|
uniqueName,
|
|
...(requestedPersonality ? { personality: requestedPersonality } : {}),
|
|
...(requestedIconId ? { iconId: requestedIconId } : {}),
|
|
clientRequestId: crypto.randomUUID(),
|
|
};
|
|
window.sessionStorage.setItem(pendingActionStorageKey, JSON.stringify(next));
|
|
return next;
|
|
};
|
|
|
|
const clearPendingAction = (action: PendingSelectionAction): void => {
|
|
if (readPendingAction()?.clientRequestId === action.clientRequestId) {
|
|
window.sessionStorage.removeItem(pendingActionStorageKey);
|
|
}
|
|
};
|
|
|
|
const isIndeterminateTimeout = (value: unknown): boolean => {
|
|
if (!value || typeof value !== 'object' || !('data' in value)) return false;
|
|
const data = value.data;
|
|
return Boolean(data && typeof data === 'object' && 'code' in data && data.code === 'TIMEOUT');
|
|
};
|
|
|
|
const formatDateTime = (value: string | null | undefined): string => {
|
|
if (!value) return '';
|
|
return formatSeoulDateTime(value);
|
|
};
|
|
|
|
const shuffleNations = (source: Nation[]): Nation[] => {
|
|
const shuffled = [...source];
|
|
for (let index = shuffled.length - 1; index > 0; index -= 1) {
|
|
const random = new Uint32Array(1);
|
|
crypto.getRandomValues(random);
|
|
const swapIndex = random[0]! % (index + 1);
|
|
[shuffled[index], shuffled[swapIndex]] = [shuffled[swapIndex]!, shuffled[index]!];
|
|
}
|
|
return shuffled;
|
|
};
|
|
|
|
const imageUrl = (candidate: Candidate): string => resolveGeneralIconUrl(candidate);
|
|
|
|
const personalityName = (key: string | null): string | null => {
|
|
if (!key) return null;
|
|
return personalities.value.find((entry) => entry.key === key)?.name ?? key;
|
|
};
|
|
const personalityInfo = (key: string | null): string =>
|
|
key ? (personalities.value.find((entry) => entry.key === key)?.info ?? '') : '';
|
|
|
|
const selectCandidate = async (candidate: Candidate): Promise<void> => {
|
|
if (!hasGeneral.value) {
|
|
selectedUniqueName.value = candidate.uniqueName;
|
|
return;
|
|
}
|
|
if (!confirm(`이 장수를 선택할까요? : ${candidate.generalName}`)) {
|
|
return;
|
|
}
|
|
submitting.value = true;
|
|
const pending = getPendingAction('reselect', candidate.uniqueName);
|
|
try {
|
|
await trpc.join.reselectPoolGeneral.mutate({
|
|
uniqueName: candidate.uniqueName,
|
|
clientRequestId: pending.clientRequestId,
|
|
});
|
|
clearPendingAction(pending);
|
|
await showDialog({ kind: 'success', message: '선택한 장수로 변경했습니다.' });
|
|
await session.refreshGeneralStatus();
|
|
await router.push('/');
|
|
} catch (cause) {
|
|
console.error(cause);
|
|
if (!isIndeterminateTimeout(cause)) {
|
|
clearPendingAction(pending);
|
|
}
|
|
showErrorToast(`장수 변경에 실패했습니다: ${errorText(cause)}`);
|
|
await loadPage();
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
};
|
|
|
|
const createGeneral = async (): Promise<void> => {
|
|
const candidate = selectedCandidate.value;
|
|
if (!candidate) {
|
|
showErrorToast('장수를 선택해주세요.');
|
|
return;
|
|
}
|
|
if (!confirm('이 장수로 생성할까요?')) {
|
|
return;
|
|
}
|
|
submitting.value = true;
|
|
const pending = getPendingAction('create', candidate.uniqueName, personality.value, selectedIconId.value);
|
|
try {
|
|
await trpc.join.selectPoolGeneral.mutate({
|
|
uniqueName: candidate.uniqueName,
|
|
personality: personality.value,
|
|
...(selectedIconId.value ? { iconId: selectedIconId.value } : {}),
|
|
clientRequestId: pending.clientRequestId,
|
|
});
|
|
clearPendingAction(pending);
|
|
await showDialog({ kind: 'success', message: '선택한 장수로 생성했습니다.' });
|
|
await session.refreshGeneralStatus();
|
|
await router.push('/');
|
|
} catch (cause) {
|
|
console.error(cause);
|
|
if (!isIndeterminateTimeout(cause)) {
|
|
clearPendingAction(pending);
|
|
}
|
|
showErrorToast(`장수 생성에 실패했습니다: ${errorText(cause)}`);
|
|
await loadPage();
|
|
} finally {
|
|
submitting.value = false;
|
|
}
|
|
};
|
|
|
|
async function loadPage(): Promise<void> {
|
|
loading.value = true;
|
|
error.value = '';
|
|
selectedUniqueName.value = null;
|
|
try {
|
|
const nextConfig = await trpc.join.getConfig.query();
|
|
config.value = nextConfig;
|
|
nations.value = shuffleNations(nextConfig.nations);
|
|
if (!nextConfig.selectionPool.enabled) {
|
|
await router.replace(nextConfig.selectionPool.hasGeneral ? '/' : '/join');
|
|
return;
|
|
}
|
|
reservation.value = await trpc.join.getSelectionPool.mutate();
|
|
now.value = Date.now();
|
|
} catch (cause) {
|
|
console.error(cause);
|
|
error.value = errorText(cause);
|
|
showErrorToast(`장수 선택 정보를 불러오지 못했습니다: ${error.value}`);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
const goBack = (): void => {
|
|
if (window.history.length > 1) {
|
|
router.back();
|
|
return;
|
|
}
|
|
void router.push(hasGeneral.value ? '/' : '/join');
|
|
};
|
|
|
|
onMounted(() => {
|
|
timer = window.setInterval(() => {
|
|
now.value = Date.now();
|
|
}, 1_000);
|
|
void loadPage();
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
if (timer !== null) {
|
|
window.clearInterval(timer);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<main class="select-pool-page legacy-bg0">
|
|
<header class="page-title with-border">
|
|
장 수 선 택<br />
|
|
<button class="legacy-button" type="button" @click="goBack">돌아가기</button>
|
|
</header>
|
|
|
|
<table v-if="serverInfo" class="server-info-table legacy-bg0">
|
|
<tbody>
|
|
<tr>
|
|
<td>
|
|
현재 : {{ serverInfo.currentYear }}年 {{ serverInfo.currentMonth }}月 (<span class="cyan"
|
|
>{{ serverInfo.tickMinutes }}분 턴</span
|
|
>
|
|
서버)<br />
|
|
등록 장수 : 유저 {{ serverInfo.userGeneralCount }} / {{ serverInfo.maxGeneral }} 명 +
|
|
<span class="cyan">NPC {{ serverInfo.npcGeneralCount }} 명</span>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<table class="invitation-table legacy-bg0">
|
|
<thead>
|
|
<tr>
|
|
<td colspan="2" class="legacy-bg1">임관 권유 메시지</td>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="nation in nations"
|
|
:key="nation.id"
|
|
:style="{
|
|
color: legacyLuminanceTextColor(nation.color),
|
|
backgroundColor: nation.color,
|
|
}"
|
|
>
|
|
<td class="invitation-nation">{{ nation.name }}</td>
|
|
<td>
|
|
<div class="invitation-message">{{ nation.scoutMessage ?? '-' }}</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<section class="selection-section">
|
|
<h1 class="section-title legacy-bg1 with-border">장수 선택</h1>
|
|
<div class="selection-body with-border">
|
|
<div v-if="loading">불러오는 중...</div>
|
|
<div v-else-if="error" class="error-text">{{ error }}</div>
|
|
<template v-else-if="reservation">
|
|
<small v-if="!expired">
|
|
(<span :style="{ color: validUntilColor }">{{ formatDateTime(reservation.validUntil) }}</span
|
|
>까지 유효)
|
|
</small>
|
|
<small v-else class="expired-text">- 만료 -</small>
|
|
<br />
|
|
<div class="card-holder">
|
|
<article v-for="candidate in candidates" :key="candidate.uniqueName" class="general-card">
|
|
<h4 class="legacy-bg1 with-border">{{ candidate.generalName }}</h4>
|
|
<h4 class="portrait">
|
|
<img
|
|
:src="imageUrl(candidate)"
|
|
:alt="candidate.generalName"
|
|
width="64"
|
|
height="64"
|
|
@error="useDefaultGeneralIcon"
|
|
/>
|
|
</h4>
|
|
<p>
|
|
{{ candidate.leadership }} / {{ candidate.strength }} / {{ candidate.intel }}<br />
|
|
<span v-if="candidate.ego" class="trait-tooltip" tabindex="0">
|
|
{{ personalityName(candidate.ego) }}
|
|
<span role="tooltip">{{ personalityInfo(candidate.ego) }}</span>
|
|
</span>
|
|
<br v-if="candidate.ego" />
|
|
<span class="trait-tooltip" tabindex="0">
|
|
{{ candidate.specialDomesticName }}
|
|
<span role="tooltip">{{ candidate.specialDomesticInfo }}</span>
|
|
</span>
|
|
/
|
|
<span v-if="candidate.specialWarName" class="trait-tooltip" tabindex="0">
|
|
{{ candidate.specialWarName }}
|
|
<span role="tooltip">{{ candidate.specialWarInfo }}</span>
|
|
</span>
|
|
<span v-else>-</span><br /><br />
|
|
보병: {{ Math.trunc(candidate.dex[0] / 1000) }}K<br />
|
|
궁병: {{ Math.trunc(candidate.dex[1] / 1000) }}K<br />
|
|
기병: {{ Math.trunc(candidate.dex[2] / 1000) }}K<br />
|
|
귀병: {{ Math.trunc(candidate.dex[3] / 1000) }}K<br />
|
|
차병: {{ Math.trunc(candidate.dex[4] / 1000) }}K<br />
|
|
</p>
|
|
<button
|
|
class="select-button with-border"
|
|
type="button"
|
|
:disabled="submitting"
|
|
@click="selectCandidate(candidate)"
|
|
>
|
|
선택하기
|
|
</button>
|
|
</article>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</section>
|
|
|
|
<section v-if="reservation && !hasGeneral" class="create-section">
|
|
<h1 class="section-title legacy-bg1 with-border">장수 생성</h1>
|
|
<div class="create-body with-border">
|
|
<div id="left-pad">
|
|
<article v-if="selectedCandidate" class="general-card selected-card">
|
|
<h4 class="legacy-bg1 with-border">{{ selectedCandidate.generalName }}</h4>
|
|
<h4 class="portrait">
|
|
<img
|
|
:src="imageUrl(selectedCandidate)"
|
|
:alt="selectedCandidate.generalName"
|
|
width="64"
|
|
height="64"
|
|
@error="useDefaultGeneralIcon"
|
|
/>
|
|
</h4>
|
|
<p>
|
|
{{ selectedCandidate.leadership }} / {{ selectedCandidate.strength }} /
|
|
{{ selectedCandidate.intel }}<br />
|
|
<span v-if="selectedCandidate.ego" class="trait-tooltip" tabindex="0">
|
|
{{ personalityName(selectedCandidate.ego) }}
|
|
<span role="tooltip">{{ personalityInfo(selectedCandidate.ego) }}</span>
|
|
</span>
|
|
<br v-if="selectedCandidate.ego" />
|
|
<span class="trait-tooltip" tabindex="0">
|
|
{{ selectedCandidate.specialDomesticName }}
|
|
<span role="tooltip">{{ selectedCandidate.specialDomesticInfo }}</span>
|
|
</span>
|
|
/
|
|
<span v-if="selectedCandidate.specialWarName" class="trait-tooltip" tabindex="0">
|
|
{{ selectedCandidate.specialWarName }}
|
|
<span role="tooltip">{{ selectedCandidate.specialWarInfo }}</span>
|
|
</span>
|
|
<span v-else>-</span><br /><br />
|
|
보병: {{ Math.trunc(selectedCandidate.dex[0] / 1000) }}K<br />
|
|
궁병: {{ Math.trunc(selectedCandidate.dex[1] / 1000) }}K<br />
|
|
기병: {{ Math.trunc(selectedCandidate.dex[2] / 1000) }}K<br />
|
|
귀병: {{ Math.trunc(selectedCandidate.dex[3] / 1000) }}K<br />
|
|
차병: {{ Math.trunc(selectedCandidate.dex[4] / 1000) }}K<br />
|
|
</p>
|
|
<button class="select-button with-border" type="button">선택하기</button>
|
|
</article>
|
|
<template v-else>장수를<br />선택해주세요!</template>
|
|
</div>
|
|
<form class="custom-form" @submit.prevent="createGeneral">
|
|
<table>
|
|
<tbody>
|
|
<tr v-if="allowPersonality">
|
|
<th class="legacy-bg1">성격</th>
|
|
<td>
|
|
<select v-model="personality">
|
|
<option value="Random">????</option>
|
|
<option
|
|
v-for="entry in personalities.filter((item) => item.key !== 'Random')"
|
|
:key="entry.key"
|
|
:value="entry.key"
|
|
>
|
|
{{ entry.name }}
|
|
</option>
|
|
</select>
|
|
<span>
|
|
{{ personalities.find((entry) => entry.key === personality)?.info ?? '' }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="config?.user.icons.length">
|
|
<th class="legacy-bg1">전콘 선택</th>
|
|
<td class="pool-icon-choice">
|
|
<label>
|
|
<input v-model="selectedIconId" type="radio" value="" /> 기본 아이콘
|
|
</label>
|
|
<label v-for="icon in config.user.icons" :key="icon.id">
|
|
<input v-model="selectedIconId" type="radio" :value="icon.id" />
|
|
<img
|
|
:src="resolveGeneralIconUrl(icon)"
|
|
width="64"
|
|
height="64"
|
|
alt="내 전용 아이콘"
|
|
@error="useDefaultGeneralIcon"
|
|
/>
|
|
</label>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td colspan="2" class="join-guidance">
|
|
임의의 도시에서 재야로 시작하며 건국과 임관은 게임 내에서 실행합니다.
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="create-action">
|
|
<button
|
|
id="build-general"
|
|
class="legacy-button"
|
|
type="submit"
|
|
:disabled="submitting"
|
|
>
|
|
장수생성
|
|
</button>
|
|
</td>
|
|
<td>
|
|
<button
|
|
class="legacy-button"
|
|
type="reset"
|
|
:disabled="submitting"
|
|
@click="personality = 'Random'"
|
|
>
|
|
다시입력
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<footer class="page-footer">
|
|
<div class="footer-back with-border">
|
|
<button class="legacy-button" type="button" @click="goBack">돌아가기</button>
|
|
</div>
|
|
<div class="footer-banner with-border">
|
|
<small>
|
|
삼국지 모의전투 HiDCHe / KOEI의 이미지를 사용, 응용하였습니다 / 제작 : HideD /
|
|
<a href="https://sam.hided.net/wiki/hidche/credit" target="_blank" rel="noopener noreferrer"
|
|
>Credit</a
|
|
>
|
|
</small>
|
|
</div>
|
|
</footer>
|
|
</main>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.select-pool-page {
|
|
width: 1000px;
|
|
min-width: 1000px;
|
|
margin: 8px auto 0;
|
|
color: #fff;
|
|
line-height: 1.3;
|
|
text-align: center;
|
|
overflow: visible;
|
|
}
|
|
.with-border {
|
|
border: solid 1px;
|
|
border-top-color: gray;
|
|
border-left-color: gray;
|
|
border-right-color: #000;
|
|
border-bottom-color: #000;
|
|
}
|
|
.page-title {
|
|
padding: 0;
|
|
text-align: left;
|
|
}
|
|
.page-title .legacy-button,
|
|
.footer-back .legacy-button {
|
|
padding: 1px 6px;
|
|
font-weight: 400;
|
|
line-height: 1.3;
|
|
}
|
|
.server-info-table,
|
|
.invitation-table {
|
|
border: 1px solid;
|
|
border-collapse: collapse;
|
|
border-top-color: gray;
|
|
border-left-color: gray;
|
|
border-right-color: #000;
|
|
border-bottom-color: #000;
|
|
border-spacing: 0;
|
|
font-size: 14px;
|
|
text-align: center;
|
|
word-break: break-all;
|
|
}
|
|
.server-info-table {
|
|
width: 100%;
|
|
}
|
|
.server-info-table td,
|
|
.invitation-table td {
|
|
border: 1px solid gray;
|
|
padding: 0;
|
|
}
|
|
.server-info-table td {
|
|
padding: 1px 0;
|
|
}
|
|
.invitation-table {
|
|
margin: 0 auto;
|
|
}
|
|
.invitation-nation {
|
|
width: 130px;
|
|
}
|
|
.invitation-message {
|
|
width: 870px;
|
|
max-width: 870px;
|
|
max-height: 200px;
|
|
overflow: hidden;
|
|
}
|
|
.cyan {
|
|
color: cyan;
|
|
}
|
|
.selection-section,
|
|
.create-section {
|
|
margin: 0;
|
|
}
|
|
.section-title {
|
|
margin: 0;
|
|
padding: 0;
|
|
color: inherit;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
line-height: 18.2px;
|
|
}
|
|
.selection-body {
|
|
padding: 0;
|
|
}
|
|
.card-holder {
|
|
text-align: center;
|
|
white-space: normal;
|
|
}
|
|
.general-card {
|
|
width: 125px;
|
|
display: inline-block;
|
|
box-sizing: content-box;
|
|
border: solid 1px;
|
|
border-top-color: gray;
|
|
border-left-color: gray;
|
|
border-right-color: #000;
|
|
border-bottom-color: #000;
|
|
vertical-align: top;
|
|
}
|
|
.general-card h4,
|
|
.general-card p {
|
|
margin: 0;
|
|
}
|
|
.general-card h4 {
|
|
padding: 0;
|
|
}
|
|
.portrait {
|
|
text-align: center;
|
|
}
|
|
.portrait img {
|
|
display: inline;
|
|
width: 64px;
|
|
height: 64px;
|
|
vertical-align: baseline;
|
|
object-fit: fill;
|
|
}
|
|
.select-button {
|
|
width: 100%;
|
|
height: 19px;
|
|
padding: 0 4px;
|
|
border-radius: 0;
|
|
background: #191919;
|
|
color: #fff;
|
|
line-height: normal;
|
|
}
|
|
.expired-text,
|
|
.error-text {
|
|
color: red;
|
|
}
|
|
.create-section {
|
|
margin-top: 10px;
|
|
}
|
|
.create-body {
|
|
display: flex;
|
|
text-align: left;
|
|
}
|
|
#left-pad {
|
|
flex: 1;
|
|
padding-top: 8px;
|
|
text-align: center;
|
|
}
|
|
.selected-card .select-button {
|
|
display: none;
|
|
}
|
|
.custom-form {
|
|
flex: 4;
|
|
}
|
|
.custom-form table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
.custom-form th,
|
|
.custom-form td {
|
|
padding: 0;
|
|
text-align: left;
|
|
}
|
|
.custom-form th {
|
|
width: 200px;
|
|
text-align: right;
|
|
}
|
|
.custom-form select {
|
|
color: #fff;
|
|
background: #000;
|
|
}
|
|
.custom-form .legacy-button {
|
|
padding: 3px 6px;
|
|
font-weight: 400;
|
|
}
|
|
.join-guidance {
|
|
text-align: center;
|
|
}
|
|
.create-action {
|
|
width: 200px;
|
|
text-align: right;
|
|
}
|
|
.footer-back,
|
|
.footer-banner {
|
|
text-align: left;
|
|
}
|
|
.footer-banner a {
|
|
color: #fff;
|
|
text-decoration: underline;
|
|
}
|
|
button:disabled {
|
|
cursor: default;
|
|
opacity: 0.55;
|
|
}
|
|
.select-button:disabled {
|
|
background: #333;
|
|
}
|
|
.select-button:focus-visible,
|
|
.custom-form select:focus-visible,
|
|
.trait-tooltip:focus-visible {
|
|
outline: auto 1px;
|
|
outline-offset: 0;
|
|
}
|
|
.trait-tooltip {
|
|
position: relative;
|
|
cursor: help;
|
|
}
|
|
.trait-tooltip [role='tooltip'] {
|
|
display: none;
|
|
position: absolute;
|
|
z-index: 20;
|
|
left: 50%;
|
|
bottom: calc(100% + 4px);
|
|
width: 220px;
|
|
padding: 5px 7px;
|
|
transform: translateX(-50%);
|
|
border: 1px solid #888;
|
|
background: #202020;
|
|
color: #fff;
|
|
text-align: left;
|
|
white-space: normal;
|
|
word-break: keep-all;
|
|
}
|
|
.trait-tooltip:hover [role='tooltip'],
|
|
.trait-tooltip:focus [role='tooltip'] {
|
|
display: block;
|
|
}
|
|
@media (max-width: 1000px) {
|
|
.select-pool-page {
|
|
margin-left: 8px;
|
|
margin-right: 0;
|
|
}
|
|
}
|
|
</style>
|