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:
2026-05-01 14:31:29 +08:00
parent ca3ca35dfc
commit 9fece8f54f
25 changed files with 361 additions and 80 deletions

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import Modal from '../components/Modal.vue';
@@ -8,6 +9,21 @@ import Skeleton from '../components/Skeleton.vue';
import StatusMessage from '../components/StatusMessage.vue';
import Tabs, { type TabOption } from '../components/Tabs.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 {
api,
@@ -27,6 +43,16 @@ import {
type AdminTab = 'config' | 'languages' | 'checklist' | 'pokemon' | 'items' | 'recipes' | 'habitats';
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 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')">
<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 }}
</button>
</div>
@@ -592,6 +619,7 @@ onMounted(() => {
<div class="detail-section__header">
<h2>{{ t('pages.admin.checklist') }}</h2>
<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') }}
</button>
</div>
@@ -612,8 +640,14 @@ onMounted(() => {
<template #default="{ item }">
<span class="reorderable-row-title">{{ item.title }}</span>
<span class="row-actions">
<button type="button" :disabled="busy" @click="editChecklistItem(item)">{{ t('common.edit') }}</button>
<button type="button" :disabled="busy" @click="removeChecklistItem(item.id)">{{ t('common.delete') }}</button>
<button type="button" :disabled="busy" @click="editChecklistItem(item)">
<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>
</template>
</ReorderableList>
@@ -624,6 +658,7 @@ onMounted(() => {
<div class="detail-section__header">
<h2>{{ t('pages.admin.config') }}</h2>
<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') }}
</button>
</div>
@@ -647,8 +682,14 @@ onMounted(() => {
{{ item.name }}<span v-if="item.hasItemDrop" class="config-flag">{{ t('pages.admin.hasItemDrop') }}</span>
</span>
<span class="row-actions">
<button type="button" :disabled="busy" @click="editConfig(item)">{{ t('common.edit') }}</button>
<button type="button" :disabled="busy" @click="removeConfig(item.id)">{{ t('common.delete') }}</button>
<button type="button" :disabled="busy" @click="editConfig(item)">
<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>
</template>
</ReorderableList>
@@ -659,6 +700,7 @@ onMounted(() => {
<div class="detail-section__header">
<h2>{{ t('pages.admin.languages') }}</h2>
<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') }}
</button>
</div>
@@ -681,8 +723,14 @@ onMounted(() => {
<span v-if="item.isDefault" class="config-flag">{{ t('pages.admin.defaultLanguage') }}</span>
</span>
<span class="row-actions">
<button type="button" @click="editLanguage(item)">{{ t('common.edit') }}</button>
<button type="button" :disabled="item.isDefault" @click="removeLanguage(item.code)">{{ t('common.delete') }}</button>
<button type="button" @click="editLanguage(item)">
<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>
</template>
</ReorderableList>
@@ -707,7 +755,10 @@ onMounted(() => {
<template #default="{ item }">
<RouterLink :to="`/pokemon/${item.id}`">#{{ item.id }} {{ item.name }}</RouterLink>
<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>
</template>
</ReorderableList>
@@ -732,7 +783,10 @@ onMounted(() => {
<template #default="{ item }">
<RouterLink :to="`/items/${item.id}`">{{ item.name }}</RouterLink>
<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>
</template>
</ReorderableList>
@@ -757,7 +811,10 @@ onMounted(() => {
<template #default="{ item }">
<RouterLink :to="`/recipes/${item.id}`">{{ item.name }}</RouterLink>
<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>
</template>
</ReorderableList>
@@ -782,7 +839,10 @@ onMounted(() => {
<template #default="{ item }">
<RouterLink :to="`/habitats/${item.id}`">{{ item.name }}</RouterLink>
<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>
</template>
</ReorderableList>
@@ -804,9 +864,13 @@ onMounted(() => {
<template #footer>
<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') }}
</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>
</Modal>
@@ -826,9 +890,13 @@ onMounted(() => {
<template #footer>
<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') }}
</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>
</Modal>
@@ -853,9 +921,13 @@ onMounted(() => {
<template #footer>
<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') }}
</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>
</Modal>
</section>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
@@ -7,6 +8,7 @@ import EditHistoryPanel from '../components/EditHistoryPanel.vue';
import EntityChips from '../components/EntityChips.vue';
import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import { iconBack, iconEdit } from '../icons';
import { api, type HabitatDetail } from '../services/api';
import HabitatEdit from './HabitatEdit.vue';
@@ -174,8 +176,14 @@ watch(
<PageHeader :title="habitat.name" :subtitle="t('pages.habitats.detailSubtitle')">
<template #kicker>Habitat Detail</template>
<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--blue ui-button--small" to="/habitats">{{ t('common.backToList') }}</RouterLink>
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/habitats/${habitat.id}/edit`">
<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>
</PageHeader>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
@@ -8,6 +9,7 @@ import StatusMessage from '../components/StatusMessage.vue';
import SwitchGroup from '../components/SwitchGroup.vue';
import TagsSelect from '../components/TagsSelect.vue';
import TranslationFields from '../components/TranslationFields.vue';
import { iconAdd, iconCancel, iconDelete, iconPokemon, iconSave } from '../icons';
import {
api,
type ConfigType,
@@ -252,9 +254,15 @@ onMounted(() => {
:search-placeholder="t('pages.pokemon.searchItems')"
/>
<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>
<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 class="field">
@@ -281,6 +289,7 @@ onMounted(() => {
</div>
<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') }}
</button>
</div>
@@ -298,7 +307,10 @@ onMounted(() => {
/>
</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>
</form>
@@ -310,8 +322,14 @@ onMounted(() => {
</section>
<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="button" class="plain-button" :disabled="busy" @click="closeEditor">{{ t('common.cancel') }}</button>
<button type="submit" form="habitat-edit-form" class="link-button" :disabled="busy">
<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>
</Modal>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
@@ -7,6 +8,7 @@ import EntityChips from '../components/EntityChips.vue';
import EntityCard from '../components/EntityCard.vue';
import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import { iconAdd, iconHabitat } from '../icons';
import { api, type Habitat } from '../services/api';
import HabitatEdit from './HabitatEdit.vue';
@@ -28,7 +30,10 @@ onMounted(async () => {
<PageHeader :title="t('pages.habitats.title')" :subtitle="t('pages.habitats.subtitle')">
<template #kicker>Habitats</template>
<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>
</PageHeader>
@@ -48,7 +53,7 @@ onMounted(async () => {
</article>
</div>
<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" />
<EntityChips :items="item.recipe" />
<EntityChips :items="item.pokemon ?? []" />

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
@@ -7,6 +8,7 @@ import EditHistoryPanel from '../components/EditHistoryPanel.vue';
import EntityChips from '../components/EntityChips.vue';
import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import { iconAdd, iconBack, iconEdit } from '../icons';
import { api, type ItemDetail } from '../services/api';
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">
<template #kicker>Item Detail</template>
<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--blue ui-button--small" to="/items">{{ t('common.backToList') }}</RouterLink>
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/items/${item.id}/edit`">
<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>
</PageHeader>
@@ -141,6 +149,7 @@ watch(
<template v-else>
<p class="meta-line">{{ t('common.none') }}</p>
<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') }}
</RouterLink>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
@@ -7,6 +8,7 @@ import Skeleton from '../components/Skeleton.vue';
import StatusMessage from '../components/StatusMessage.vue';
import TagsSelect from '../components/TagsSelect.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';
const route = useRoute();
@@ -252,8 +254,14 @@ onMounted(() => {
</section>
<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="button" class="plain-button" :disabled="busy" @click="closeEditor">{{ t('common.cancel') }}</button>
<button type="submit" form="item-edit-form" class="link-button" :disabled="busy">
<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>
</Modal>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
@@ -10,6 +11,7 @@ import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import Tabs, { type TabOption } from '../components/Tabs.vue';
import TagsSelect from '../components/TagsSelect.vue';
import { iconAdd, iconItem } from '../icons';
import { api, type Item, type Options } from '../services/api';
import ItemEdit from './ItemEdit.vue';
@@ -59,7 +61,10 @@ watch(itemQuery, loadItems);
<PageHeader :title="t('pages.items.title')" :subtitle="t('pages.items.subtitle')">
<template #kicker>Bag</template>
<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>
</PageHeader>
@@ -132,7 +137,7 @@ watch(itemQuery, loadItems);
:title="item.name"
:subtitle="item.usage ? `${item.category.name} · ${item.usage.name}` : item.category.name"
:to="`/items/${item.id}`"
marker=""
:icon="iconItem"
>
<EditMeta :entity="item" />
<EntityChips :items="item.tags" />

View File

@@ -1,9 +1,11 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import PageHeader from '../components/PageHeader.vue';
import StatusMessage from '../components/StatusMessage.vue';
import { iconLogin } from '../icons';
import { api, setAuthToken } from '../services/api';
const route = useRoute();
@@ -59,6 +61,7 @@ async function submitLogin() {
<StatusMessage v-if="errorMessage" variant="danger">{{ errorMessage }}</StatusMessage>
<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') }}
</button>
</form>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
@@ -8,6 +9,7 @@ import EntityChips from '../components/EntityChips.vue';
import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import Tabs, { type TabOption } from '../components/Tabs.vue';
import { iconBack, iconEdit } from '../icons';
import { api, type PokemonDetail } from '../services/api';
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 })">
<template #kicker>Pokédex Detail</template>
<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--blue ui-button--small" to="/pokemon">{{ t('common.backToList') }}</RouterLink>
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/pokemon/${pokemon.id}/edit`">
<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>
</PageHeader>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
@@ -7,6 +8,7 @@ import Skeleton from '../components/Skeleton.vue';
import StatusMessage from '../components/StatusMessage.vue';
import TagsSelect from '../components/TagsSelect.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';
type SkillItemDropForm = {
@@ -287,8 +289,14 @@ watch(() => pokemonForm.value.skillIds.slice(), syncSkillItemDrops);
</section>
<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="button" class="plain-button" :disabled="busy" @click="closeEditor">{{ t('common.cancel') }}</button>
<button type="submit" form="pokemon-edit-form" class="link-button" :disabled="busy">
<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>
</Modal>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
@@ -9,6 +10,7 @@ import FilterPanel from '../components/FilterPanel.vue';
import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import TagsSelect from '../components/TagsSelect.vue';
import { iconAdd } from '../icons';
import { api, type Options, type Pokemon } from '../services/api';
import PokemonEdit from './PokemonEdit.vue';
@@ -55,7 +57,10 @@ watch(query, loadPokemon);
<PageHeader :title="t('pages.pokemon.title')" :subtitle="t('pages.pokemon.subtitle')">
<template #kicker>Pokédex</template>
<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>
</PageHeader>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
@@ -7,6 +8,7 @@ import EditHistoryPanel from '../components/EditHistoryPanel.vue';
import EntityChips from '../components/EntityChips.vue';
import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import { iconBack, iconEdit } from '../icons';
import { api, type RecipeDetail } from '../services/api';
import RecipeEdit from './RecipeEdit.vue';
@@ -72,8 +74,14 @@ watch(
<PageHeader :title="recipe.name" :subtitle="t('pages.recipes.detailSubtitle')">
<template #kicker>Recipe Detail</template>
<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--blue ui-button--small" to="/recipes">{{ t('common.backToList') }}</RouterLink>
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/recipes/${recipe.id}/edit`">
<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>
</PageHeader>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
@@ -6,6 +7,7 @@ import Modal from '../components/Modal.vue';
import Skeleton from '../components/Skeleton.vue';
import StatusMessage from '../components/StatusMessage.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';
const route = useRoute();
@@ -186,9 +188,15 @@ onMounted(() => {
:search-placeholder="t('pages.pokemon.searchItems')"
/>
<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>
<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>
</form>
@@ -200,8 +208,14 @@ onMounted(() => {
</section>
<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="button" class="plain-button" :disabled="busy" @click="closeEditor">{{ t('common.cancel') }}</button>
<button type="submit" form="recipe-edit-form" class="link-button" :disabled="busy">
<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>
</Modal>
</template>

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
@@ -9,6 +10,7 @@ import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import Tabs, { type TabOption } from '../components/Tabs.vue';
import TagsSelect from '../components/TagsSelect.vue';
import { iconAdd, iconNoRecipe, iconRecipe } from '../icons';
import { api, type Item, type Options } from '../services/api';
import RecipeEdit from './RecipeEdit.vue';
@@ -52,12 +54,12 @@ function createRecipeTarget(item: Item) {
return `/recipes/new?itemId=${item.id}`;
}
function itemMarker(item: Item) {
function itemIcon(item: Item) {
if (item.recipe) {
return '▦';
return iconRecipe;
}
return item.noRecipe ? '×' : '';
return item.noRecipe ? iconNoRecipe : iconAdd;
}
async function loadItems() {
@@ -79,7 +81,10 @@ watch(itemQuery, loadItems);
<PageHeader :title="t('pages.recipes.title')" :subtitle="t('pages.recipes.subtitle')">
<template #kicker>Recipes</template>
<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>
</PageHeader>
@@ -145,10 +150,11 @@ watch(itemQuery, loadItems);
:title="item.name"
:subtitle="itemSubtitle(item)"
:to="recipeTarget(item)"
:marker="itemMarker(item)"
:icon="itemIcon(item)"
>
<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)">
<Icon :icon="iconAdd" class="ui-icon" aria-hidden="true" />
{{ t('pages.items.createRecipe') }}
</RouterLink>
</EntityCard>

View File

@@ -1,8 +1,10 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import PageHeader from '../components/PageHeader.vue';
import StatusMessage from '../components/StatusMessage.vue';
import { iconMail } from '../icons';
import { api } from '../services/api';
const email = ref('');
@@ -67,6 +69,7 @@ async function submitRegister() {
<StatusMessage v-if="errorMessage" variant="danger">{{ errorMessage }}</StatusMessage>
<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') }}
</button>
</form>

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import PageHeader from '../components/PageHeader.vue';
import Skeleton from '../components/Skeleton.vue';
import StatusMessage from '../components/StatusMessage.vue';
import { iconLogin } from '../icons';
import { api } from '../services/api';
const route = useRoute();
@@ -48,7 +50,10 @@ onMounted(async () => {
<StatusMessage v-else-if="message" variant="success">{{ message }}</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>
</section>
</template>