feat(ui): add icons to navigation and UI components
Integrate @iconify/vue for consistent iconography across the app Enhance buttons, entity cards, and status messages with visual indicators
This commit is contained in:
@@ -3,6 +3,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import AppShell from './components/AppShell.vue';
|
import AppShell from './components/AppShell.vue';
|
||||||
|
import { iconAdmin, iconChecklist, iconHabitat, iconItem, iconPokemon, iconRecipe } from './icons';
|
||||||
import { getCurrentLocale, onLocaleChange, setCurrentLocale } from './i18n';
|
import { getCurrentLocale, onLocaleChange, setCurrentLocale } from './i18n';
|
||||||
import { api, getAuthToken, onAuthTokenChange, setAuthToken, type AuthUser, type Language } from './services/api';
|
import { api, getAuthToken, onAuthTokenChange, setAuthToken, type AuthUser, type Language } from './services/api';
|
||||||
|
|
||||||
@@ -18,12 +19,12 @@ let removeAuthListener: (() => void) | null = null;
|
|||||||
let removeLocaleListener: (() => void) | null = null;
|
let removeLocaleListener: (() => void) | null = null;
|
||||||
|
|
||||||
const navItems = computed(() => [
|
const navItems = computed(() => [
|
||||||
{ label: t('nav.pokemon'), to: '/pokemon' },
|
{ label: t('nav.pokemon'), to: '/pokemon', icon: iconPokemon },
|
||||||
{ label: t('nav.habitats'), to: '/habitats' },
|
{ label: t('nav.habitats'), to: '/habitats', icon: iconHabitat },
|
||||||
{ label: t('nav.items'), to: '/items' },
|
{ label: t('nav.items'), to: '/items', icon: iconItem },
|
||||||
{ label: t('nav.recipes'), to: '/recipes' },
|
{ label: t('nav.recipes'), to: '/recipes', icon: iconRecipe },
|
||||||
{ label: t('nav.checklist'), to: '/checklist' },
|
{ label: t('nav.checklist'), to: '/checklist', icon: iconChecklist },
|
||||||
{ label: t('nav.admin'), to: '/admin' }
|
{ label: t('nav.admin'), to: '/admin', icon: iconAdmin }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
async function loadCurrentUser() {
|
async function loadCurrentUser() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Icon } from '@iconify/vue';
|
import { Icon } from '@iconify/vue';
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { iconLogin, iconLogout, iconRegister, iconTranslate, type AppIcon } from '../icons';
|
||||||
import type { AuthUser, Language } from '../services/api';
|
import type { AuthUser, Language } from '../services/api';
|
||||||
import PokeBallMark from './PokeBallMark.vue';
|
import PokeBallMark from './PokeBallMark.vue';
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ defineProps<{
|
|||||||
currentUser: AuthUser | null;
|
currentUser: AuthUser | null;
|
||||||
languages: Language[];
|
languages: Language[];
|
||||||
locale: string;
|
locale: string;
|
||||||
navItems: Array<{ label: string; to: string }>;
|
navItems: Array<{ label: string; to: string; icon?: AppIcon }>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -18,11 +19,6 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const translateIcon = {
|
|
||||||
width: 24,
|
|
||||||
height: 24,
|
|
||||||
body: '<path fill="currentColor" d="m12.65 15.65l-2.85-2.8q.7-.8 1.225-1.75t.825-2.1H15V7h-5V5H8v2H3v2h6.95q-.275.8-.687 1.5T8.3 11.8q-.5-.55-.875-1.125T6.8 9h-2q.35.95.913 1.763T7 13.2l-5.65 5.55L2.75 20l5.55-5.55l3.45 3.4zm5.1 4.35L17 18h-3.5l-.75 2h-2l3.5-9h2l3.5 9zm-2.15-4h2.8L17 12.25z"/>'
|
|
||||||
};
|
|
||||||
const languageMenu = ref<HTMLElement | null>(null);
|
const languageMenu = ref<HTMLElement | null>(null);
|
||||||
const languageMenuButton = ref<HTMLButtonElement | null>(null);
|
const languageMenuButton = ref<HTMLButtonElement | null>(null);
|
||||||
const languageMenuOpen = ref(false);
|
const languageMenuOpen = ref(false);
|
||||||
@@ -78,6 +74,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<nav class="nav-links" :aria-label="t('nav.main')">
|
<nav class="nav-links" :aria-label="t('nav.main')">
|
||||||
<RouterLink v-for="item in navItems" :key="item.to" :to="item.to">
|
<RouterLink v-for="item in navItems" :key="item.to" :to="item.to">
|
||||||
|
<Icon v-if="item.icon" :icon="item.icon" class="ui-icon nav-links__icon" aria-hidden="true" />
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -93,7 +90,7 @@ onBeforeUnmount(() => {
|
|||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
@click="toggleLanguageMenu"
|
@click="toggleLanguageMenu"
|
||||||
>
|
>
|
||||||
<Icon :icon="translateIcon" class="language-menu__icon" aria-hidden="true" />
|
<Icon :icon="iconTranslate" class="language-menu__icon" aria-hidden="true" />
|
||||||
<span class="language-menu__glyph" aria-hidden="true">文/A</span>
|
<span class="language-menu__glyph" aria-hidden="true">文/A</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -116,12 +113,19 @@ onBeforeUnmount(() => {
|
|||||||
<template v-if="currentUser">
|
<template v-if="currentUser">
|
||||||
<span class="auth-user">{{ currentUser.displayName || currentUser.email }}</span>
|
<span class="auth-user">{{ currentUser.displayName || currentUser.email }}</span>
|
||||||
<button class="ui-button ui-button--ghost ui-button--small" type="button" @click="$emit('logout')">
|
<button class="ui-button ui-button--ghost ui-button--small" type="button" @click="$emit('logout')">
|
||||||
|
<Icon :icon="iconLogout" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('nav.logout') }}
|
{{ t('nav.logout') }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<RouterLink class="ui-button ui-button--ghost ui-button--small" to="/login">{{ t('nav.login') }}</RouterLink>
|
<RouterLink class="ui-button ui-button--ghost ui-button--small" to="/login">
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/register">{{ t('nav.register') }}</RouterLink>
|
<Icon :icon="iconLogin" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('nav.login') }}
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/register">
|
||||||
|
<Icon :icon="iconRegister" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('nav.register') }}
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
|
import type { AppIcon } from '../icons';
|
||||||
import PokeBallMark from './PokeBallMark.vue';
|
import PokeBallMark from './PokeBallMark.vue';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
|
icon?: AppIcon;
|
||||||
marker?: string;
|
marker?: string;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
@@ -12,7 +15,8 @@ defineProps<{
|
|||||||
<template>
|
<template>
|
||||||
<RouterLink v-if="to" class="entity-card entity-card--link" :to="to">
|
<RouterLink v-if="to" class="entity-card entity-card--link" :to="to">
|
||||||
<span class="entity-card__mark">
|
<span class="entity-card__mark">
|
||||||
<PokeBallMark v-if="!marker" size="30px" />
|
<Icon v-if="icon" :icon="icon" class="entity-card__icon" aria-hidden="true" />
|
||||||
|
<PokeBallMark v-else-if="!marker" size="30px" />
|
||||||
<span v-else>{{ marker }}</span>
|
<span v-else>{{ marker }}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="entity-card__content">
|
<div class="entity-card__content">
|
||||||
@@ -24,7 +28,8 @@ defineProps<{
|
|||||||
|
|
||||||
<article v-else class="entity-card">
|
<article v-else class="entity-card">
|
||||||
<span class="entity-card__mark">
|
<span class="entity-card__mark">
|
||||||
<PokeBallMark v-if="!marker" size="30px" />
|
<Icon v-if="icon" :icon="icon" class="entity-card__icon" aria-hidden="true" />
|
||||||
|
<PokeBallMark v-else-if="!marker" size="30px" />
|
||||||
<span v-else>{{ marker }}</span>
|
<span v-else>{{ marker }}</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="entity-card__content">
|
<div class="entity-card__content">
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { nextTick, onBeforeUnmount, onMounted, onUpdated, ref, watch } from 'vue';
|
import { nextTick, onBeforeUnmount, onMounted, onUpdated, ref, watch } from 'vue';
|
||||||
|
import { iconClose } from '../icons';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -235,7 +237,7 @@ watch(
|
|||||||
<p v-if="subtitle">{{ subtitle }}</p>
|
<p v-if="subtitle">{{ subtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
<button ref="closeButton" class="modal-close-button" type="button" :aria-label="closeLabel" @click="requestClose">
|
<button ref="closeButton" class="modal-close-button" type="button" :aria-label="closeLabel" @click="requestClose">
|
||||||
×
|
<Icon :icon="iconClose" class="ui-icon" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts" generic="T">
|
<script setup lang="ts" generic="T">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { ref, shallowRef } from 'vue';
|
import { ref, shallowRef } from 'vue';
|
||||||
|
import { iconDragHandle } from '../icons';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
items: T[];
|
items: T[];
|
||||||
@@ -209,7 +211,7 @@ function handleKeydown(item: T, event: KeyboardEvent) {
|
|||||||
@dragend="endDrag"
|
@dragend="endDrag"
|
||||||
@keydown="handleKeydown(item, $event)"
|
@keydown="handleKeydown(item, $event)"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">⋮⋮</span>
|
<Icon :icon="iconDragHandle" class="ui-icon" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<slot :item="item" />
|
<slot :item="item" />
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { Icon } from '@iconify/vue';
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
|
import { iconError, iconInfo, iconSuccess, iconWarning } from '../icons';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -14,6 +16,12 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const visible = ref(true);
|
const visible = ref(true);
|
||||||
let timer: number | null = null;
|
let timer: number | null = null;
|
||||||
|
const statusIcon = computed(() => {
|
||||||
|
if (props.variant === 'success') return iconSuccess;
|
||||||
|
if (props.variant === 'warning') return iconWarning;
|
||||||
|
if (props.variant === 'danger') return iconError;
|
||||||
|
return iconInfo;
|
||||||
|
});
|
||||||
|
|
||||||
function clearTimer() {
|
function clearTimer() {
|
||||||
if (!timer) return;
|
if (!timer) return;
|
||||||
@@ -38,6 +46,7 @@ watch(() => props.duration, scheduleDismiss);
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p class="status-message" :class="[`status-message--${variant}`, { 'status-message--hidden': !visible }]">
|
<p class="status-message" :class="[`status-message--${variant}`, { 'status-message--hidden': !visible }]">
|
||||||
|
<Icon :icon="statusIcon" class="status-message__icon" aria-hidden="true" />
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { iconCheck, iconChevronDown, iconClose } from '../icons';
|
||||||
|
|
||||||
export type TagsSelectOption = {
|
export type TagsSelectOption = {
|
||||||
id: number | string;
|
id: number | string;
|
||||||
@@ -259,14 +261,14 @@ watch(candidateRows, clampActiveIndex);
|
|||||||
@keydown.enter.stop.prevent="remove(option.value)"
|
@keydown.enter.stop.prevent="remove(option.value)"
|
||||||
@keydown.space.stop.prevent="remove(option.value)"
|
@keydown.space.stop.prevent="remove(option.value)"
|
||||||
>
|
>
|
||||||
×
|
<Icon :icon="iconClose" class="ui-icon" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="tags-select__single-value">{{ selectedLabel }}</span>
|
<span v-else class="tags-select__single-value">{{ selectedLabel }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="tags-select__placeholder">{{ placeholderText }}</span>
|
<span v-else class="tags-select__placeholder">{{ placeholderText }}</span>
|
||||||
<span class="tags-select__arrow" aria-hidden="true">⌄</span>
|
<Icon :icon="iconChevronDown" class="tags-select__arrow" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="isOpen" class="tags-select__dropdown">
|
<div v-if="isOpen" class="tags-select__dropdown">
|
||||||
@@ -299,7 +301,10 @@ watch(candidateRows, clampActiveIndex);
|
|||||||
@click="selectOption(option.value)"
|
@click="selectOption(option.value)"
|
||||||
>
|
>
|
||||||
<span>{{ option.label }}</span>
|
<span>{{ option.label }}</span>
|
||||||
<span v-if="selectedValues.has(option.value)" class="tags-select__state">{{ t('common.selected') }}</span>
|
<span v-if="selectedValues.has(option.value)" class="tags-select__state">
|
||||||
|
<Icon :icon="iconCheck" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.selected') }}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canCreate"
|
v-if="canCreate"
|
||||||
|
|||||||
28
frontend/src/icons.ts
Normal file
28
frontend/src/icons.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export type AppIcon = string;
|
||||||
|
|
||||||
|
export const iconAdd: AppIcon = 'mdi:plus';
|
||||||
|
export const iconAdmin: AppIcon = 'mdi:tune-variant';
|
||||||
|
export const iconBack: AppIcon = 'mdi:arrow-left';
|
||||||
|
export const iconCancel: AppIcon = 'mdi:close';
|
||||||
|
export const iconCheck: AppIcon = 'mdi:check';
|
||||||
|
export const iconChecklist: AppIcon = 'mdi:checkbox-marked-outline';
|
||||||
|
export const iconChevronDown: AppIcon = 'mdi:chevron-down';
|
||||||
|
export const iconClose: AppIcon = 'mdi:close';
|
||||||
|
export const iconDelete: AppIcon = 'mdi:trash-can-outline';
|
||||||
|
export const iconDragHandle: AppIcon = 'mdi:drag';
|
||||||
|
export const iconEdit: AppIcon = 'mdi:pencil-outline';
|
||||||
|
export const iconError: AppIcon = 'mdi:close-circle-outline';
|
||||||
|
export const iconHabitat: AppIcon = 'mdi:pine-tree';
|
||||||
|
export const iconInfo: AppIcon = 'mdi:information-outline';
|
||||||
|
export const iconItem: AppIcon = 'mdi:bag-personal-outline';
|
||||||
|
export const iconLogin: AppIcon = 'mdi:login';
|
||||||
|
export const iconLogout: AppIcon = 'mdi:logout';
|
||||||
|
export const iconMail: AppIcon = 'mdi:email-fast-outline';
|
||||||
|
export const iconNoRecipe: AppIcon = 'mdi:file-document-remove-outline';
|
||||||
|
export const iconPokemon: AppIcon = 'mdi:pokeball';
|
||||||
|
export const iconRecipe: AppIcon = 'mdi:book-open-page-variant-outline';
|
||||||
|
export const iconRegister: AppIcon = 'mdi:account-plus-outline';
|
||||||
|
export const iconSave: AppIcon = 'mdi:content-save-outline';
|
||||||
|
export const iconSuccess: AppIcon = 'mdi:check-circle-outline';
|
||||||
|
export const iconTranslate: AppIcon = 'mdi:translate';
|
||||||
|
export const iconWarning: AppIcon = 'mdi:alert-outline';
|
||||||
@@ -92,6 +92,12 @@ svg {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-icon {
|
||||||
|
width: 1.1em;
|
||||||
|
height: 1.1em;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 3px solid var(--focus);
|
outline: 3px solid var(--focus);
|
||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
@@ -163,6 +169,7 @@ svg {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border-radius: var(--radius-control);
|
border-radius: var(--radius-control);
|
||||||
color: var(--ink-soft);
|
color: var(--ink-soft);
|
||||||
@@ -181,6 +188,11 @@ svg {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-links__icon {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
.auth-actions {
|
.auth-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -625,12 +637,14 @@ button:disabled,
|
|||||||
border-radius: var(--radius-control);
|
border-radius: var(--radius-control);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 900;
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-close-button .ui-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-body {
|
.modal-body {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@@ -727,6 +741,11 @@ button:disabled,
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-select__remove .ui-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.tags-select__remove:hover {
|
.tags-select__remove:hover {
|
||||||
background: rgba(42, 117, 187, 0.14);
|
background: rgba(42, 117, 187, 0.14);
|
||||||
}
|
}
|
||||||
@@ -738,8 +757,13 @@ button:disabled,
|
|||||||
.tags-select__arrow {
|
.tags-select__arrow {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 18px;
|
width: 18px;
|
||||||
line-height: 1;
|
height: 18px;
|
||||||
|
transition: transform 0.14s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-select__trigger.open .tags-select__arrow {
|
||||||
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-select__dropdown {
|
.tags-select__dropdown {
|
||||||
@@ -808,12 +832,20 @@ button:disabled,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tags-select__state {
|
.tags-select__state {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
color: var(--pokemon-blue);
|
color: var(--pokemon-blue);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-select__state .ui-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.tags-select__empty {
|
.tags-select__empty {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
@@ -1052,6 +1084,11 @@ button:disabled,
|
|||||||
font-weight: 950;
|
font-weight: 950;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entity-card__icon {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
.entity-card__content {
|
.entity-card__content {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
@@ -1206,6 +1243,11 @@ button:disabled,
|
|||||||
opacity: 0.54;
|
opacity: 0.54;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drag-handle .ui-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
.reorderable-row-title {
|
.reorderable-row-title {
|
||||||
flex: 1 1 180px;
|
flex: 1 1 180px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1685,14 +1727,12 @@ button:disabled,
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-message::before {
|
.status-message__icon {
|
||||||
content: "";
|
width: 20px;
|
||||||
width: 12px;
|
height: 20px;
|
||||||
height: 12px;
|
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
margin-top: 6px;
|
margin-top: 2px;
|
||||||
border-radius: 50%;
|
color: var(--status-accent, var(--pokemon-blue));
|
||||||
background: var(--status-accent, var(--pokemon-blue));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-message--success {
|
.status-message--success {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import Modal from '../components/Modal.vue';
|
import Modal from '../components/Modal.vue';
|
||||||
@@ -8,6 +9,21 @@ import Skeleton from '../components/Skeleton.vue';
|
|||||||
import StatusMessage from '../components/StatusMessage.vue';
|
import StatusMessage from '../components/StatusMessage.vue';
|
||||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||||
import TranslationFields from '../components/TranslationFields.vue';
|
import TranslationFields from '../components/TranslationFields.vue';
|
||||||
|
import {
|
||||||
|
iconAdd,
|
||||||
|
iconAdmin,
|
||||||
|
iconCancel,
|
||||||
|
iconChecklist,
|
||||||
|
iconDelete,
|
||||||
|
iconEdit,
|
||||||
|
iconHabitat,
|
||||||
|
iconItem,
|
||||||
|
iconPokemon,
|
||||||
|
iconRecipe,
|
||||||
|
iconSave,
|
||||||
|
iconTranslate,
|
||||||
|
type AppIcon
|
||||||
|
} from '../icons';
|
||||||
import { defaultLocale, getCurrentLocale, setCurrentLocale } from '../i18n';
|
import { defaultLocale, getCurrentLocale, setCurrentLocale } from '../i18n';
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
@@ -27,6 +43,16 @@ import {
|
|||||||
type AdminTab = 'config' | 'languages' | 'checklist' | 'pokemon' | 'items' | 'recipes' | 'habitats';
|
type AdminTab = 'config' | 'languages' | 'checklist' | 'pokemon' | 'items' | 'recipes' | 'habitats';
|
||||||
type EditableConfig = (NamedEntity | Skill) & { hasItemDrop?: boolean };
|
type EditableConfig = (NamedEntity | Skill) & { hasItemDrop?: boolean };
|
||||||
|
|
||||||
|
const adminTabIcons: Record<AdminTab, AppIcon> = {
|
||||||
|
config: iconAdmin,
|
||||||
|
languages: iconTranslate,
|
||||||
|
checklist: iconChecklist,
|
||||||
|
pokemon: iconPokemon,
|
||||||
|
items: iconItem,
|
||||||
|
recipes: iconRecipe,
|
||||||
|
habitats: iconHabitat
|
||||||
|
};
|
||||||
|
|
||||||
const { locale, t } = useI18n();
|
const { locale, t } = useI18n();
|
||||||
|
|
||||||
const tabs = computed<Array<{ key: AdminTab; label: string }>>(() => [
|
const tabs = computed<Array<{ key: AdminTab; label: string }>>(() => [
|
||||||
@@ -570,6 +596,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<div v-if="canEdit" class="tabs" role="tablist" :aria-label="t('pages.admin.modules')">
|
<div v-if="canEdit" class="tabs" role="tablist" :aria-label="t('pages.admin.modules')">
|
||||||
<button v-for="tab in tabs" :key="tab.key" :class="{ active: activeTab === tab.key }" type="button" @click="setTab(tab.key)">
|
<button v-for="tab in tabs" :key="tab.key" :class="{ active: activeTab === tab.key }" type="button" @click="setTab(tab.key)">
|
||||||
|
<Icon :icon="adminTabIcons[tab.key]" class="ui-icon" aria-hidden="true" />
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -592,6 +619,7 @@ onMounted(() => {
|
|||||||
<div class="detail-section__header">
|
<div class="detail-section__header">
|
||||||
<h2>{{ t('pages.admin.checklist') }}</h2>
|
<h2>{{ t('pages.admin.checklist') }}</h2>
|
||||||
<button type="button" class="ui-button ui-button--primary ui-button--small" :disabled="busy" @click="openNewChecklistItem">
|
<button type="button" class="ui-button ui-button--primary ui-button--small" :disabled="busy" @click="openNewChecklistItem">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('common.new') }}
|
{{ t('common.new') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -612,8 +640,14 @@ onMounted(() => {
|
|||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<span class="reorderable-row-title">{{ item.title }}</span>
|
<span class="reorderable-row-title">{{ item.title }}</span>
|
||||||
<span class="row-actions">
|
<span class="row-actions">
|
||||||
<button type="button" :disabled="busy" @click="editChecklistItem(item)">{{ t('common.edit') }}</button>
|
<button type="button" :disabled="busy" @click="editChecklistItem(item)">
|
||||||
<button type="button" :disabled="busy" @click="removeChecklistItem(item.id)">{{ t('common.delete') }}</button>
|
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" :disabled="busy" @click="removeChecklistItem(item.id)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</ReorderableList>
|
</ReorderableList>
|
||||||
@@ -624,6 +658,7 @@ onMounted(() => {
|
|||||||
<div class="detail-section__header">
|
<div class="detail-section__header">
|
||||||
<h2>{{ t('pages.admin.config') }}</h2>
|
<h2>{{ t('pages.admin.config') }}</h2>
|
||||||
<button type="button" class="ui-button ui-button--primary ui-button--small" :disabled="busy" @click="openNewConfig">
|
<button type="button" class="ui-button ui-button--primary ui-button--small" :disabled="busy" @click="openNewConfig">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('common.new') }}
|
{{ t('common.new') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -647,8 +682,14 @@ onMounted(() => {
|
|||||||
{{ item.name }}<span v-if="item.hasItemDrop" class="config-flag">{{ t('pages.admin.hasItemDrop') }}</span>
|
{{ item.name }}<span v-if="item.hasItemDrop" class="config-flag">{{ t('pages.admin.hasItemDrop') }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="row-actions">
|
<span class="row-actions">
|
||||||
<button type="button" :disabled="busy" @click="editConfig(item)">{{ t('common.edit') }}</button>
|
<button type="button" :disabled="busy" @click="editConfig(item)">
|
||||||
<button type="button" :disabled="busy" @click="removeConfig(item.id)">{{ t('common.delete') }}</button>
|
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" :disabled="busy" @click="removeConfig(item.id)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</ReorderableList>
|
</ReorderableList>
|
||||||
@@ -659,6 +700,7 @@ onMounted(() => {
|
|||||||
<div class="detail-section__header">
|
<div class="detail-section__header">
|
||||||
<h2>{{ t('pages.admin.languages') }}</h2>
|
<h2>{{ t('pages.admin.languages') }}</h2>
|
||||||
<button type="button" class="ui-button ui-button--primary ui-button--small" :disabled="busy" @click="openNewLanguage">
|
<button type="button" class="ui-button ui-button--primary ui-button--small" :disabled="busy" @click="openNewLanguage">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('common.new') }}
|
{{ t('common.new') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -681,8 +723,14 @@ onMounted(() => {
|
|||||||
<span v-if="item.isDefault" class="config-flag">{{ t('pages.admin.defaultLanguage') }}</span>
|
<span v-if="item.isDefault" class="config-flag">{{ t('pages.admin.defaultLanguage') }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="row-actions">
|
<span class="row-actions">
|
||||||
<button type="button" @click="editLanguage(item)">{{ t('common.edit') }}</button>
|
<button type="button" @click="editLanguage(item)">
|
||||||
<button type="button" :disabled="item.isDefault" @click="removeLanguage(item.code)">{{ t('common.delete') }}</button>
|
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" :disabled="item.isDefault" @click="removeLanguage(item.code)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</ReorderableList>
|
</ReorderableList>
|
||||||
@@ -707,7 +755,10 @@ onMounted(() => {
|
|||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<RouterLink :to="`/pokemon/${item.id}`">#{{ item.id }} {{ item.name }}</RouterLink>
|
<RouterLink :to="`/pokemon/${item.id}`">#{{ item.id }} {{ item.name }}</RouterLink>
|
||||||
<span class="row-actions">
|
<span class="row-actions">
|
||||||
<button type="button" :disabled="busy" @click="removePokemon(item.id)">{{ t('common.delete') }}</button>
|
<button type="button" :disabled="busy" @click="removePokemon(item.id)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</ReorderableList>
|
</ReorderableList>
|
||||||
@@ -732,7 +783,10 @@ onMounted(() => {
|
|||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<RouterLink :to="`/items/${item.id}`">{{ item.name }}</RouterLink>
|
<RouterLink :to="`/items/${item.id}`">{{ item.name }}</RouterLink>
|
||||||
<span class="row-actions">
|
<span class="row-actions">
|
||||||
<button type="button" :disabled="busy" @click="removeItem(item.id)">{{ t('common.delete') }}</button>
|
<button type="button" :disabled="busy" @click="removeItem(item.id)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</ReorderableList>
|
</ReorderableList>
|
||||||
@@ -757,7 +811,10 @@ onMounted(() => {
|
|||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<RouterLink :to="`/recipes/${item.id}`">{{ item.name }}</RouterLink>
|
<RouterLink :to="`/recipes/${item.id}`">{{ item.name }}</RouterLink>
|
||||||
<span class="row-actions">
|
<span class="row-actions">
|
||||||
<button type="button" :disabled="busy" @click="removeRecipe(item.id)">{{ t('common.delete') }}</button>
|
<button type="button" :disabled="busy" @click="removeRecipe(item.id)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</ReorderableList>
|
</ReorderableList>
|
||||||
@@ -782,7 +839,10 @@ onMounted(() => {
|
|||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<RouterLink :to="`/habitats/${item.id}`">{{ item.name }}</RouterLink>
|
<RouterLink :to="`/habitats/${item.id}`">{{ item.name }}</RouterLink>
|
||||||
<span class="row-actions">
|
<span class="row-actions">
|
||||||
<button type="button" :disabled="busy" @click="removeHabitat(item.id)">{{ t('common.delete') }}</button>
|
<button type="button" :disabled="busy" @click="removeHabitat(item.id)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</ReorderableList>
|
</ReorderableList>
|
||||||
@@ -804,9 +864,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<button type="submit" form="admin-checklist-form" class="link-button" :disabled="busy">
|
<button type="submit" form="admin-checklist-form" class="link-button" :disabled="busy">
|
||||||
|
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||||
{{ busy ? t('common.saving') : t('common.save') }}
|
{{ busy ? t('common.saving') : t('common.save') }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="plain-button" :disabled="busy" @click="closeChecklistModal">{{ t('common.cancel') }}</button>
|
<button type="button" class="plain-button" :disabled="busy" @click="closeChecklistModal">
|
||||||
|
<Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -826,9 +890,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<button type="submit" form="admin-config-form" class="link-button" :disabled="busy">
|
<button type="submit" form="admin-config-form" class="link-button" :disabled="busy">
|
||||||
|
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||||
{{ busy ? t('common.saving') : t('common.save') }}
|
{{ busy ? t('common.saving') : t('common.save') }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="plain-button" :disabled="busy" @click="closeConfigModal">{{ t('common.cancel') }}</button>
|
<button type="button" class="plain-button" :disabled="busy" @click="closeConfigModal">
|
||||||
|
<Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
@@ -853,9 +921,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<button type="submit" form="admin-language-form" class="link-button" :disabled="busy">
|
<button type="submit" form="admin-language-form" class="link-button" :disabled="busy">
|
||||||
|
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||||
{{ busy ? t('common.saving') : t('common.save') }}
|
{{ busy ? t('common.saving') : t('common.save') }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="plain-button" :disabled="busy" @click="closeLanguageModal">{{ t('common.cancel') }}</button>
|
<button type="button" class="plain-button" :disabled="busy" @click="closeLanguageModal">
|
||||||
|
<Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@@ -7,6 +8,7 @@ import EditHistoryPanel from '../components/EditHistoryPanel.vue';
|
|||||||
import EntityChips from '../components/EntityChips.vue';
|
import EntityChips from '../components/EntityChips.vue';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
|
import { iconBack, iconEdit } from '../icons';
|
||||||
import { api, type HabitatDetail } from '../services/api';
|
import { api, type HabitatDetail } from '../services/api';
|
||||||
import HabitatEdit from './HabitatEdit.vue';
|
import HabitatEdit from './HabitatEdit.vue';
|
||||||
|
|
||||||
@@ -174,8 +176,14 @@ watch(
|
|||||||
<PageHeader :title="habitat.name" :subtitle="t('pages.habitats.detailSubtitle')">
|
<PageHeader :title="habitat.name" :subtitle="t('pages.habitats.detailSubtitle')">
|
||||||
<template #kicker>Habitat Detail</template>
|
<template #kicker>Habitat Detail</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/habitats/${habitat.id}/edit`">{{ t('common.edit') }}</RouterLink>
|
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/habitats/${habitat.id}/edit`">
|
||||||
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/habitats">{{ t('common.backToList') }}</RouterLink>
|
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/habitats">
|
||||||
|
<Icon :icon="iconBack" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.backToList') }}
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
@@ -8,6 +9,7 @@ import StatusMessage from '../components/StatusMessage.vue';
|
|||||||
import SwitchGroup from '../components/SwitchGroup.vue';
|
import SwitchGroup from '../components/SwitchGroup.vue';
|
||||||
import TagsSelect from '../components/TagsSelect.vue';
|
import TagsSelect from '../components/TagsSelect.vue';
|
||||||
import TranslationFields from '../components/TranslationFields.vue';
|
import TranslationFields from '../components/TranslationFields.vue';
|
||||||
|
import { iconAdd, iconCancel, iconDelete, iconPokemon, iconSave } from '../icons';
|
||||||
import {
|
import {
|
||||||
api,
|
api,
|
||||||
type ConfigType,
|
type ConfigType,
|
||||||
@@ -252,9 +254,15 @@ onMounted(() => {
|
|||||||
:search-placeholder="t('pages.pokemon.searchItems')"
|
:search-placeholder="t('pages.pokemon.searchItems')"
|
||||||
/>
|
/>
|
||||||
<input v-model.number="row.quantity" :aria-label="t('common.quantity')" type="number" min="1" />
|
<input v-model.number="row.quantity" :aria-label="t('common.quantity')" type="number" min="1" />
|
||||||
<button type="button" @click="habitatForm.recipeItems.splice(index, 1)">{{ t('common.delete') }}</button>
|
<button type="button" @click="habitatForm.recipeItems.splice(index, 1)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="plain-button" @click="addHabitatRecipeItem">{{ t('pages.habitats.addItem') }}</button>
|
<button type="button" class="plain-button" @click="addHabitatRecipeItem">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('pages.habitats.addItem') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
@@ -281,6 +289,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="button" class="appearance-row__delete" @click="habitatForm.pokemonAppearances.splice(index, 1)">
|
<button type="button" class="appearance-row__delete" @click="habitatForm.pokemonAppearances.splice(index, 1)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('common.delete') }}
|
{{ t('common.delete') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -298,7 +307,10 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="plain-button" @click="addPokemonAppearance">{{ t('pages.habitats.addPokemon') }}</button>
|
<button type="button" class="plain-button" @click="addPokemonAppearance">
|
||||||
|
<Icon :icon="iconPokemon" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('pages.habitats.addPokemon') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -310,8 +322,14 @@ onMounted(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<template v-if="!loading && options" #footer>
|
<template v-if="!loading && options" #footer>
|
||||||
<button type="submit" form="habitat-edit-form" class="link-button" :disabled="busy">{{ busy ? t('common.saving') : t('common.save') }}</button>
|
<button type="submit" form="habitat-edit-form" class="link-button" :disabled="busy">
|
||||||
<button type="button" class="plain-button" :disabled="busy" @click="closeEditor">{{ t('common.cancel') }}</button>
|
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ busy ? t('common.saving') : t('common.save') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="plain-button" :disabled="busy" @click="closeEditor">
|
||||||
|
<Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@@ -7,6 +8,7 @@ import EntityChips from '../components/EntityChips.vue';
|
|||||||
import EntityCard from '../components/EntityCard.vue';
|
import EntityCard from '../components/EntityCard.vue';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
|
import { iconAdd, iconHabitat } from '../icons';
|
||||||
import { api, type Habitat } from '../services/api';
|
import { api, type Habitat } from '../services/api';
|
||||||
import HabitatEdit from './HabitatEdit.vue';
|
import HabitatEdit from './HabitatEdit.vue';
|
||||||
|
|
||||||
@@ -28,7 +30,10 @@ onMounted(async () => {
|
|||||||
<PageHeader :title="t('pages.habitats.title')" :subtitle="t('pages.habitats.subtitle')">
|
<PageHeader :title="t('pages.habitats.title')" :subtitle="t('pages.habitats.subtitle')">
|
||||||
<template #kicker>Habitats</template>
|
<template #kicker>Habitats</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/habitats/new">{{ t('common.add') }}</RouterLink>
|
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/habitats/new">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.add') }}
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -48,7 +53,7 @@ onMounted(async () => {
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="entity-grid">
|
<div v-else class="entity-grid">
|
||||||
<EntityCard v-for="item in habitats" :key="item.id" :title="item.name" :to="`/habitats/${item.id}`" marker="◎">
|
<EntityCard v-for="item in habitats" :key="item.id" :title="item.name" :to="`/habitats/${item.id}`" :icon="iconHabitat">
|
||||||
<EditMeta :entity="item" />
|
<EditMeta :entity="item" />
|
||||||
<EntityChips :items="item.recipe" />
|
<EntityChips :items="item.recipe" />
|
||||||
<EntityChips :items="item.pokemon ?? []" />
|
<EntityChips :items="item.pokemon ?? []" />
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@@ -7,6 +8,7 @@ import EditHistoryPanel from '../components/EditHistoryPanel.vue';
|
|||||||
import EntityChips from '../components/EntityChips.vue';
|
import EntityChips from '../components/EntityChips.vue';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
|
import { iconAdd, iconBack, iconEdit } from '../icons';
|
||||||
import { api, type ItemDetail } from '../services/api';
|
import { api, type ItemDetail } from '../services/api';
|
||||||
import ItemEdit from './ItemEdit.vue';
|
import ItemEdit from './ItemEdit.vue';
|
||||||
|
|
||||||
@@ -110,8 +112,14 @@ watch(
|
|||||||
<PageHeader :title="item.name" :subtitle="item.usage ? `${item.category.name} · ${item.usage.name}` : item.category.name">
|
<PageHeader :title="item.name" :subtitle="item.usage ? `${item.category.name} · ${item.usage.name}` : item.category.name">
|
||||||
<template #kicker>Item Detail</template>
|
<template #kicker>Item Detail</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/items/${item.id}/edit`">{{ t('common.edit') }}</RouterLink>
|
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/items/${item.id}/edit`">
|
||||||
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/items">{{ t('common.backToList') }}</RouterLink>
|
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/items">
|
||||||
|
<Icon :icon="iconBack" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.backToList') }}
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -141,6 +149,7 @@ watch(
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<p class="meta-line">{{ t('common.none') }}</p>
|
<p class="meta-line">{{ t('common.none') }}</p>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/recipes/new?itemId=${item.id}`">
|
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/recipes/new?itemId=${item.id}`">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('pages.items.createRecipe') }}
|
{{ t('pages.items.createRecipe') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
@@ -7,6 +8,7 @@ import Skeleton from '../components/Skeleton.vue';
|
|||||||
import StatusMessage from '../components/StatusMessage.vue';
|
import StatusMessage from '../components/StatusMessage.vue';
|
||||||
import TagsSelect from '../components/TagsSelect.vue';
|
import TagsSelect from '../components/TagsSelect.vue';
|
||||||
import TranslationFields from '../components/TranslationFields.vue';
|
import TranslationFields from '../components/TranslationFields.vue';
|
||||||
|
import { iconCancel, iconSave } from '../icons';
|
||||||
import { api, type ConfigType, type ItemPayload, type Language, type Options, type TranslationMap } from '../services/api';
|
import { api, type ConfigType, type ItemPayload, type Language, type Options, type TranslationMap } from '../services/api';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -252,8 +254,14 @@ onMounted(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<template v-if="!loading && options" #footer>
|
<template v-if="!loading && options" #footer>
|
||||||
<button type="submit" form="item-edit-form" class="link-button" :disabled="busy">{{ busy ? t('common.saving') : t('common.save') }}</button>
|
<button type="submit" form="item-edit-form" class="link-button" :disabled="busy">
|
||||||
<button type="button" class="plain-button" :disabled="busy" @click="closeEditor">{{ t('common.cancel') }}</button>
|
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ busy ? t('common.saving') : t('common.save') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="plain-button" :disabled="busy" @click="closeEditor">
|
||||||
|
<Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@@ -10,6 +11,7 @@ import PageHeader from '../components/PageHeader.vue';
|
|||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||||
import TagsSelect from '../components/TagsSelect.vue';
|
import TagsSelect from '../components/TagsSelect.vue';
|
||||||
|
import { iconAdd, iconItem } from '../icons';
|
||||||
import { api, type Item, type Options } from '../services/api';
|
import { api, type Item, type Options } from '../services/api';
|
||||||
import ItemEdit from './ItemEdit.vue';
|
import ItemEdit from './ItemEdit.vue';
|
||||||
|
|
||||||
@@ -59,7 +61,10 @@ watch(itemQuery, loadItems);
|
|||||||
<PageHeader :title="t('pages.items.title')" :subtitle="t('pages.items.subtitle')">
|
<PageHeader :title="t('pages.items.title')" :subtitle="t('pages.items.subtitle')">
|
||||||
<template #kicker>Bag</template>
|
<template #kicker>Bag</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/items/new">{{ t('common.add') }}</RouterLink>
|
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/items/new">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.add') }}
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -132,7 +137,7 @@ watch(itemQuery, loadItems);
|
|||||||
:title="item.name"
|
:title="item.name"
|
||||||
:subtitle="item.usage ? `${item.category.name} · ${item.usage.name}` : item.category.name"
|
:subtitle="item.usage ? `${item.category.name} · ${item.usage.name}` : item.category.name"
|
||||||
:to="`/items/${item.id}`"
|
:to="`/items/${item.id}`"
|
||||||
marker="+"
|
:icon="iconItem"
|
||||||
>
|
>
|
||||||
<EditMeta :entity="item" />
|
<EditMeta :entity="item" />
|
||||||
<EntityChips :items="item.tags" />
|
<EntityChips :items="item.tags" />
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import StatusMessage from '../components/StatusMessage.vue';
|
import StatusMessage from '../components/StatusMessage.vue';
|
||||||
|
import { iconLogin } from '../icons';
|
||||||
import { api, setAuthToken } from '../services/api';
|
import { api, setAuthToken } from '../services/api';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -59,6 +61,7 @@ async function submitLogin() {
|
|||||||
<StatusMessage v-if="errorMessage" variant="danger">{{ errorMessage }}</StatusMessage>
|
<StatusMessage v-if="errorMessage" variant="danger">{{ errorMessage }}</StatusMessage>
|
||||||
|
|
||||||
<button class="ui-button ui-button--primary" :disabled="busy" type="submit">
|
<button class="ui-button ui-button--primary" :disabled="busy" type="submit">
|
||||||
|
<Icon :icon="iconLogin" class="ui-icon" aria-hidden="true" />
|
||||||
{{ busy ? t('auth.loggingIn') : t('nav.login') }}
|
{{ busy ? t('auth.loggingIn') : t('nav.login') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@@ -8,6 +9,7 @@ import EntityChips from '../components/EntityChips.vue';
|
|||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||||
|
import { iconBack, iconEdit } from '../icons';
|
||||||
import { api, type PokemonDetail } from '../services/api';
|
import { api, type PokemonDetail } from '../services/api';
|
||||||
import PokemonEdit from './PokemonEdit.vue';
|
import PokemonEdit from './PokemonEdit.vue';
|
||||||
|
|
||||||
@@ -208,8 +210,14 @@ watch(
|
|||||||
<PageHeader :title="`#${pokemon.id} ${pokemon.name}`" :subtitle="t('pages.pokemon.environmentPrefix', { name: pokemon.environment.name })">
|
<PageHeader :title="`#${pokemon.id} ${pokemon.name}`" :subtitle="t('pages.pokemon.environmentPrefix', { name: pokemon.environment.name })">
|
||||||
<template #kicker>Pokédex Detail</template>
|
<template #kicker>Pokédex Detail</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/pokemon/${pokemon.id}/edit`">{{ t('common.edit') }}</RouterLink>
|
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/pokemon/${pokemon.id}/edit`">
|
||||||
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/pokemon">{{ t('common.backToList') }}</RouterLink>
|
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/pokemon">
|
||||||
|
<Icon :icon="iconBack" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.backToList') }}
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
@@ -7,6 +8,7 @@ import Skeleton from '../components/Skeleton.vue';
|
|||||||
import StatusMessage from '../components/StatusMessage.vue';
|
import StatusMessage from '../components/StatusMessage.vue';
|
||||||
import TagsSelect from '../components/TagsSelect.vue';
|
import TagsSelect from '../components/TagsSelect.vue';
|
||||||
import TranslationFields from '../components/TranslationFields.vue';
|
import TranslationFields from '../components/TranslationFields.vue';
|
||||||
|
import { iconCancel, iconSave } from '../icons';
|
||||||
import { api, type ConfigType, type Language, type NamedEntity, type Options, type PokemonPayload, type TranslationMap } from '../services/api';
|
import { api, type ConfigType, type Language, type NamedEntity, type Options, type PokemonPayload, type TranslationMap } from '../services/api';
|
||||||
|
|
||||||
type SkillItemDropForm = {
|
type SkillItemDropForm = {
|
||||||
@@ -287,8 +289,14 @@ watch(() => pokemonForm.value.skillIds.slice(), syncSkillItemDrops);
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<template v-if="!loading && options" #footer>
|
<template v-if="!loading && options" #footer>
|
||||||
<button type="submit" form="pokemon-edit-form" class="link-button" :disabled="busy">{{ busy ? t('common.saving') : t('common.save') }}</button>
|
<button type="submit" form="pokemon-edit-form" class="link-button" :disabled="busy">
|
||||||
<button type="button" class="plain-button" :disabled="busy" @click="closeEditor">{{ t('common.cancel') }}</button>
|
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ busy ? t('common.saving') : t('common.save') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="plain-button" :disabled="busy" @click="closeEditor">
|
||||||
|
<Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@@ -9,6 +10,7 @@ import FilterPanel from '../components/FilterPanel.vue';
|
|||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
import TagsSelect from '../components/TagsSelect.vue';
|
import TagsSelect from '../components/TagsSelect.vue';
|
||||||
|
import { iconAdd } from '../icons';
|
||||||
import { api, type Options, type Pokemon } from '../services/api';
|
import { api, type Options, type Pokemon } from '../services/api';
|
||||||
import PokemonEdit from './PokemonEdit.vue';
|
import PokemonEdit from './PokemonEdit.vue';
|
||||||
|
|
||||||
@@ -55,7 +57,10 @@ watch(query, loadPokemon);
|
|||||||
<PageHeader :title="t('pages.pokemon.title')" :subtitle="t('pages.pokemon.subtitle')">
|
<PageHeader :title="t('pages.pokemon.title')" :subtitle="t('pages.pokemon.subtitle')">
|
||||||
<template #kicker>Pokédex</template>
|
<template #kicker>Pokédex</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/pokemon/new">{{ t('common.add') }}</RouterLink>
|
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/pokemon/new">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.add') }}
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@@ -7,6 +8,7 @@ import EditHistoryPanel from '../components/EditHistoryPanel.vue';
|
|||||||
import EntityChips from '../components/EntityChips.vue';
|
import EntityChips from '../components/EntityChips.vue';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
|
import { iconBack, iconEdit } from '../icons';
|
||||||
import { api, type RecipeDetail } from '../services/api';
|
import { api, type RecipeDetail } from '../services/api';
|
||||||
import RecipeEdit from './RecipeEdit.vue';
|
import RecipeEdit from './RecipeEdit.vue';
|
||||||
|
|
||||||
@@ -72,8 +74,14 @@ watch(
|
|||||||
<PageHeader :title="recipe.name" :subtitle="t('pages.recipes.detailSubtitle')">
|
<PageHeader :title="recipe.name" :subtitle="t('pages.recipes.detailSubtitle')">
|
||||||
<template #kicker>Recipe Detail</template>
|
<template #kicker>Recipe Detail</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/recipes/${recipe.id}/edit`">{{ t('common.edit') }}</RouterLink>
|
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/recipes/${recipe.id}/edit`">
|
||||||
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/recipes">{{ t('common.backToList') }}</RouterLink>
|
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</RouterLink>
|
||||||
|
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/recipes">
|
||||||
|
<Icon :icon="iconBack" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.backToList') }}
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
@@ -6,6 +7,7 @@ import Modal from '../components/Modal.vue';
|
|||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
import StatusMessage from '../components/StatusMessage.vue';
|
import StatusMessage from '../components/StatusMessage.vue';
|
||||||
import TagsSelect from '../components/TagsSelect.vue';
|
import TagsSelect from '../components/TagsSelect.vue';
|
||||||
|
import { iconAdd, iconCancel, iconDelete, iconSave } from '../icons';
|
||||||
import { api, type ConfigType, type Item, type Options, type RecipePayload } from '../services/api';
|
import { api, type ConfigType, type Item, type Options, type RecipePayload } from '../services/api';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -186,9 +188,15 @@ onMounted(() => {
|
|||||||
:search-placeholder="t('pages.pokemon.searchItems')"
|
:search-placeholder="t('pages.pokemon.searchItems')"
|
||||||
/>
|
/>
|
||||||
<input v-model.number="row.quantity" :aria-label="t('common.quantity')" type="number" min="1" />
|
<input v-model.number="row.quantity" :aria-label="t('common.quantity')" type="number" min="1" />
|
||||||
<button type="button" @click="recipeForm.materials.splice(index, 1)">{{ t('common.delete') }}</button>
|
<button type="button" @click="recipeForm.materials.splice(index, 1)">
|
||||||
|
<Icon :icon="iconDelete" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.delete') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="plain-button" @click="addRecipeMaterial">{{ t('pages.recipes.addMaterial') }}</button>
|
<button type="button" class="plain-button" @click="addRecipeMaterial">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('pages.recipes.addMaterial') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -200,8 +208,14 @@ onMounted(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<template v-if="!loading && options" #footer>
|
<template v-if="!loading && options" #footer>
|
||||||
<button type="submit" form="recipe-edit-form" class="link-button" :disabled="busy">{{ busy ? t('common.saving') : t('common.save') }}</button>
|
<button type="submit" form="recipe-edit-form" class="link-button" :disabled="busy">
|
||||||
<button type="button" class="plain-button" :disabled="busy" @click="closeEditor">{{ t('common.cancel') }}</button>
|
<Icon :icon="iconSave" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ busy ? t('common.saving') : t('common.save') }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="plain-button" :disabled="busy" @click="closeEditor">
|
||||||
|
<Icon :icon="iconCancel" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
@@ -9,6 +10,7 @@ import PageHeader from '../components/PageHeader.vue';
|
|||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
import Tabs, { type TabOption } from '../components/Tabs.vue';
|
||||||
import TagsSelect from '../components/TagsSelect.vue';
|
import TagsSelect from '../components/TagsSelect.vue';
|
||||||
|
import { iconAdd, iconNoRecipe, iconRecipe } from '../icons';
|
||||||
import { api, type Item, type Options } from '../services/api';
|
import { api, type Item, type Options } from '../services/api';
|
||||||
import RecipeEdit from './RecipeEdit.vue';
|
import RecipeEdit from './RecipeEdit.vue';
|
||||||
|
|
||||||
@@ -52,12 +54,12 @@ function createRecipeTarget(item: Item) {
|
|||||||
return `/recipes/new?itemId=${item.id}`;
|
return `/recipes/new?itemId=${item.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemMarker(item: Item) {
|
function itemIcon(item: Item) {
|
||||||
if (item.recipe) {
|
if (item.recipe) {
|
||||||
return '▦';
|
return iconRecipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
return item.noRecipe ? '×' : '+';
|
return item.noRecipe ? iconNoRecipe : iconAdd;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadItems() {
|
async function loadItems() {
|
||||||
@@ -79,7 +81,10 @@ watch(itemQuery, loadItems);
|
|||||||
<PageHeader :title="t('pages.recipes.title')" :subtitle="t('pages.recipes.subtitle')">
|
<PageHeader :title="t('pages.recipes.title')" :subtitle="t('pages.recipes.subtitle')">
|
||||||
<template #kicker>Recipes</template>
|
<template #kicker>Recipes</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/recipes/new">{{ t('common.add') }}</RouterLink>
|
<RouterLink class="ui-button ui-button--primary ui-button--small" to="/recipes/new">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.add') }}
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
@@ -145,10 +150,11 @@ watch(itemQuery, loadItems);
|
|||||||
:title="item.name"
|
:title="item.name"
|
||||||
:subtitle="itemSubtitle(item)"
|
:subtitle="itemSubtitle(item)"
|
||||||
:to="recipeTarget(item)"
|
:to="recipeTarget(item)"
|
||||||
:marker="itemMarker(item)"
|
:icon="itemIcon(item)"
|
||||||
>
|
>
|
||||||
<EditMeta v-if="item.recipe" :entity="item.recipe" />
|
<EditMeta v-if="item.recipe" :entity="item.recipe" />
|
||||||
<RouterLink v-else-if="!item.noRecipe" class="ui-button ui-button--primary ui-button--small" :to="createRecipeTarget(item)">
|
<RouterLink v-else-if="!item.noRecipe" class="ui-button ui-button--primary ui-button--small" :to="createRecipeTarget(item)">
|
||||||
|
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
|
||||||
{{ t('pages.items.createRecipe') }}
|
{{ t('pages.items.createRecipe') }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</EntityCard>
|
</EntityCard>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import StatusMessage from '../components/StatusMessage.vue';
|
import StatusMessage from '../components/StatusMessage.vue';
|
||||||
|
import { iconMail } from '../icons';
|
||||||
import { api } from '../services/api';
|
import { api } from '../services/api';
|
||||||
|
|
||||||
const email = ref('');
|
const email = ref('');
|
||||||
@@ -67,6 +69,7 @@ async function submitRegister() {
|
|||||||
<StatusMessage v-if="errorMessage" variant="danger">{{ errorMessage }}</StatusMessage>
|
<StatusMessage v-if="errorMessage" variant="danger">{{ errorMessage }}</StatusMessage>
|
||||||
|
|
||||||
<button class="ui-button ui-button--primary" :disabled="busy" type="submit">
|
<button class="ui-button ui-button--primary" :disabled="busy" type="submit">
|
||||||
|
<Icon :icon="iconMail" class="ui-icon" aria-hidden="true" />
|
||||||
{{ busy ? t('auth.sending') : t('auth.sendVerification') }}
|
{{ busy ? t('auth.sending') : t('auth.sendVerification') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import PageHeader from '../components/PageHeader.vue';
|
import PageHeader from '../components/PageHeader.vue';
|
||||||
import Skeleton from '../components/Skeleton.vue';
|
import Skeleton from '../components/Skeleton.vue';
|
||||||
import StatusMessage from '../components/StatusMessage.vue';
|
import StatusMessage from '../components/StatusMessage.vue';
|
||||||
|
import { iconLogin } from '../icons';
|
||||||
import { api } from '../services/api';
|
import { api } from '../services/api';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -48,7 +50,10 @@ onMounted(async () => {
|
|||||||
<StatusMessage v-else-if="message" variant="success">{{ message }}</StatusMessage>
|
<StatusMessage v-else-if="message" variant="success">{{ message }}</StatusMessage>
|
||||||
<StatusMessage v-else variant="danger">{{ errorMessage }}</StatusMessage>
|
<StatusMessage v-else variant="danger">{{ errorMessage }}</StatusMessage>
|
||||||
|
|
||||||
<RouterLink v-if="!busy" class="ui-button ui-button--primary" to="/login">{{ t('auth.goLogin') }}</RouterLink>
|
<RouterLink v-if="!busy" class="ui-button ui-button--primary" to="/login">
|
||||||
|
<Icon :icon="iconLogin" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('auth.goLogin') }}
|
||||||
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user