trigo / trigo-web /app /src /components /InlineNicknameEditor.vue
k-l-lambda's picture
Update trigo-web with VS People multiplayer mode
15f353f
<template>
<div class="inline-nickname-editor" :class="{ editable, editing, [playerColor || 'neutral']: true }">
<!-- Display mode -->
<div
v-if="!editing"
class="nickname-display"
:title="editable ? 'Click to edit nickname' : ''"
@click="startEdit"
>
<span class="stone-icon" :class="playerColor"></span>
<span class="nickname-text">{{ displayNickname }}</span>
<span v-if="editable" class="edit-icon">✏️</span>
</div>
<!-- Edit mode -->
<div v-else class="nickname-edit">
<span class="stone-icon" :class="playerColor"></span>
<input
ref="inputRef"
v-model="editValue"
type="text"
class="nickname-input"
:class="{ error: hasError }"
:maxlength="20"
:title="inputTooltip"
@keydown.enter="confirmEdit"
@keydown.esc="cancelEdit"
@blur="confirmEdit"
/>
<span v-if="hasError" class="error-icon" :title="errorMessage">⚠️</span>
</div>
<!-- Character count and help text when editing -->
<div v-if="editing" class="edit-info">
<span class="help-text">Enter to save • Esc to cancel</span>
<span class="char-count">{{ editValue.length }}/20</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from "vue";
const props = defineProps<{
nickname: string;
editable: boolean;
playerColor: "black" | "white" | null;
}>();
const emit = defineEmits<{
(e: "update", nickname: string): void;
}>();
const editing = ref(false);
const editValue = ref(props.nickname);
const errorMessage = ref("");
const inputRef = ref<HTMLInputElement | null>(null);
const displayNickname = computed(() => props.nickname || "Guest");
const hasError = computed(() => errorMessage.value !== "");
const inputTooltip = computed(() =>
"3-20 characters, letters/numbers/spaces only"
);
function validateNickname(nickname: string): { valid: boolean; error: string } {
const trimmed = nickname.trim();
if (trimmed.length < 3) return { valid: false, error: "Must be 3+ chars" };
if (trimmed.length > 20) return { valid: false, error: "Max 20 chars" };
if (!/^[a-zA-Z0-9 ]+$/.test(trimmed)) return { valid: false, error: "Letters, numbers, spaces only" };
if (trimmed !== nickname) return { valid: false, error: "No leading/trailing spaces" };
return { valid: true, error: "" };
}
function startEdit() {
if (!props.editable) return;
editing.value = true;
editValue.value = props.nickname;
errorMessage.value = "";
nextTick(() => {
inputRef.value?.focus();
inputRef.value?.select();
});
}
function confirmEdit() {
if (!editing.value) return;
const trimmed = editValue.value.trim();
if (trimmed === props.nickname) {
cancelEdit();
return;
}
const validation = validateNickname(trimmed);
if (!validation.valid) {
errorMessage.value = validation.error;
return;
}
editing.value = false;
errorMessage.value = "";
emit("update", trimmed);
}
function cancelEdit() {
editing.value = false;
editValue.value = props.nickname;
errorMessage.value = "";
}
watch(
() => props.nickname,
(newNickname) => {
if (!editing.value) {
editValue.value = newNickname;
}
}
);
</script>
<style lang="scss" scoped>
.inline-nickname-editor {
display: flex;
flex-direction: column;
gap: 0.25rem;
&.editable .nickname-display {
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba(255, 255, 255, 0.05);
}
}
.nickname-display,
.nickname-edit {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-radius: 6px;
background-color: rgba(0, 0, 0, 0.2);
}
.stone-icon {
width: 20px;
height: 20px;
border-radius: 50%;
flex-shrink: 0;
&.black {
background-color: #2c2c2c;
border: 2px solid #fff;
}
&.white {
background-color: #f0f0f0;
border: 2px solid #000;
}
}
.nickname-text {
font-weight: 600;
color: #e0e0e0;
}
.edit-icon {
opacity: 0;
transition: opacity 0.2s ease;
font-size: 0.8rem;
}
&.editable:hover .edit-icon {
opacity: 0.6;
}
.nickname-input {
flex: 1;
background-color: rgba(255, 255, 255, 0.1);
border: 2px solid #60a5fa;
border-radius: 4px;
padding: 0.25rem 0.5rem;
color: #e0e0e0;
font-weight: 600;
font-size: 0.95rem;
outline: none;
&.error {
border-color: #ef4444;
}
&:focus {
background-color: rgba(255, 255, 255, 0.15);
}
}
.error-icon {
font-size: 1rem;
cursor: help;
}
.edit-info {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0.5rem;
font-size: 0.75rem;
}
.help-text {
color: #60a5fa;
font-style: italic;
}
.char-count {
color: #9ca3af;
}
}
</style>