fix(ui): prevent grid blowouts and fix detail view reactivity

Add min-width constraints to grid containers to prevent overflow
Replace .detail-grid with .detail-tab-panel for consistent layouts
Add key attributes to detail views to ensure proper state reset
This commit is contained in:
2026-05-13 11:28:35 +08:00
parent 8628bdf68b
commit 5c72766781
5 changed files with 43 additions and 29 deletions

View File

@@ -1205,7 +1205,9 @@ svg {
.page-stack { .page-stack {
position: relative; position: relative;
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 18px; gap: 18px;
min-width: 0;
} }
.page-header { .page-header {
@@ -1649,9 +1651,15 @@ button:disabled,
} }
.modal-edit-form--tabbed { .modal-edit-form--tabbed {
grid-template-columns: minmax(0, 1fr);
gap: 14px; gap: 14px;
} }
.modal-edit-form--tabbed > .pokemon-edit-panel {
grid-column: 1 / -1;
min-width: 0;
}
.ai-moderation-form { .ai-moderation-form {
max-width: 680px; max-width: 680px;
} }
@@ -2337,7 +2345,12 @@ button:disabled,
.tabs--component { .tabs--component {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr);
grid-column: 1 / -1;
flex: 1 0 100%;
gap: 14px; gap: 14px;
width: 100%;
min-width: 0;
} }
.tab-list { .tab-list {
@@ -4581,21 +4594,18 @@ button:disabled,
height: 15px; height: 15px;
} }
.detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.detail-grid--stack {
grid-template-columns: 1fr;
}
.detail-tabs, .detail-tabs,
.detail-tab-panel { .detail-tab-panel {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 16px; gap: 16px;
min-width: 0; min-width: 0;
width: 100%;
}
.detail-tabs > .detail-tab-panel {
grid-column: 1 / -1;
min-width: 0;
} }
.habitat-detail-stack { .habitat-detail-stack {
@@ -7144,8 +7154,14 @@ button:disabled,
.profile-tab-panel, .profile-tab-panel,
.profile-activity-list { .profile-activity-list {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 16px; gap: 16px;
min-width: 0; min-width: 0;
width: 100%;
}
.profile-public-layout > .profile-tab-panel {
grid-column: 1 / -1;
} }
.profile-secondary-tabs .tab-list { .profile-secondary-tabs .tab-list {
@@ -8110,7 +8126,6 @@ button:disabled,
justify-content: flex-start; justify-content: flex-start;
} }
.detail-grid,
.entity-profile-grid, .entity-profile-grid,
.home-hero, .home-hero,
.pokemon-image-detail, .pokemon-image-detail,
@@ -8796,7 +8811,6 @@ button:disabled,
height: 56px; height: 56px;
} }
.detail-grid,
.pokemon-related-grid, .pokemon-related-grid,
.entity-profile-grid, .entity-profile-grid,
.pokemon-profile-grid, .pokemon-profile-grid,

View File

@@ -230,7 +230,7 @@ watch(initialHabitat, applyInitialHabitat, { immediate: true });
</script> </script>
<template> <template>
<section v-if="!habitat" class="page-stack" aria-busy="true" :aria-label="t('pages.habitats.loadingDetail')"> <section v-if="!habitat" key="habitat-detail-loading" class="page-stack" aria-busy="true" :aria-label="t('pages.habitats.loadingDetail')">
<div class="page-header page-header--skeleton" aria-hidden="true"> <div class="page-header page-header--skeleton" aria-hidden="true">
<div class="page-header__copy"> <div class="page-header__copy">
<Skeleton width="132px" /> <Skeleton width="132px" />
@@ -275,7 +275,7 @@ watch(initialHabitat, applyInitialHabitat, { immediate: true });
</section> </section>
</div> </div>
</section> </section>
<section v-else class="page-stack"> <section v-else :key="`habitat-detail-${habitat.id}`" class="page-stack">
<PageHeader :title="habitat.name" :subtitle="t('pages.habitats.detailSubtitle')"> <PageHeader :title="habitat.name" :subtitle="t('pages.habitats.detailSubtitle')">
<template #kicker>{{ detailKicker }}</template> <template #kicker>{{ detailKicker }}</template>
<template #actions> <template #actions>
@@ -293,7 +293,7 @@ watch(initialHabitat, applyInitialHabitat, { immediate: true });
<div class="detail-tabs"> <div class="detail-tabs">
<Tabs id="habitat-detail-tabs" v-model="detailTab" :tabs="detailTabs" :label="t('common.details')" /> <Tabs id="habitat-detail-tabs" v-model="detailTab" :tabs="detailTabs" :label="t('common.details')" />
<div v-if="detailTab === 'details'" class="detail-grid detail-grid--stack"> <div v-if="detailTab === 'details'" class="detail-tab-panel">
<div class="entity-profile-grid"> <div class="entity-profile-grid">
<section class="detail-section entity-profile-media-section" :aria-label="t('media.image')"> <section class="detail-section entity-profile-media-section" :aria-label="t('media.image')">
<div class="entity-detail-image"> <div class="entity-detail-image">

View File

@@ -225,7 +225,7 @@ watch(initialItem, applyInitialItem, { immediate: true });
</script> </script>
<template> <template>
<section v-if="!item" class="page-stack" aria-busy="true" :aria-label="t('pages.items.loadingDetail')"> <section v-if="!item" key="item-detail-loading" class="page-stack" aria-busy="true" :aria-label="t('pages.items.loadingDetail')">
<div class="page-header page-header--skeleton" aria-hidden="true"> <div class="page-header page-header--skeleton" aria-hidden="true">
<div class="page-header__copy"> <div class="page-header__copy">
<Skeleton width="96px" /> <Skeleton width="96px" />
@@ -238,7 +238,7 @@ watch(initialItem, applyInitialItem, { immediate: true });
</div> </div>
</div> </div>
<div class="detail-grid" aria-hidden="true"> <div class="detail-tab-panel" aria-hidden="true">
<section v-for="index in 3" :key="`chips-${index}`" class="detail-section skeleton-detail-section"> <section v-for="index in 3" :key="`chips-${index}`" class="detail-section skeleton-detail-section">
<div class="detail-section__header"> <div class="detail-section__header">
<Skeleton :width="index === 2 ? '68px' : '92px'" height="24px" /> <Skeleton :width="index === 2 ? '68px' : '92px'" height="24px" />
@@ -277,7 +277,7 @@ watch(initialItem, applyInitialItem, { immediate: true });
</section> </section>
</div> </div>
</section> </section>
<section v-else class="page-stack"> <section v-else :key="`item-detail-${item.id}`" class="page-stack">
<PageHeader :title="item.name" :subtitle="itemSubtitle"> <PageHeader :title="item.name" :subtitle="itemSubtitle">
<template #kicker>{{ detailKicker }}</template> <template #kicker>{{ detailKicker }}</template>
<template #actions> <template #actions>
@@ -295,7 +295,7 @@ watch(initialItem, applyInitialItem, { immediate: true });
<div class="detail-tabs"> <div class="detail-tabs">
<Tabs id="item-detail-tabs" v-model="detailTab" :tabs="detailTabs" :label="t('common.details')" /> <Tabs id="item-detail-tabs" v-model="detailTab" :tabs="detailTabs" :label="t('common.details')" />
<div v-if="detailTab === 'details'" class="detail-grid detail-grid--stack"> <div v-if="detailTab === 'details'" class="detail-tab-panel">
<div class="entity-profile-grid"> <div class="entity-profile-grid">
<section class="detail-section entity-profile-media-section" :aria-label="t('media.image')"> <section class="detail-section entity-profile-media-section" :aria-label="t('media.image')">
<div class="entity-detail-image"> <div class="entity-detail-image">
@@ -394,7 +394,7 @@ watch(initialItem, applyInitialItem, { immediate: true });
</div> </div>
</DetailSection> </DetailSection>
<div class="detail-grid"> <div class="detail-tab-panel">
<DetailSection :title="t('pages.items.recipeInfo')"> <DetailSection :title="t('pages.items.recipeInfo')">
<template v-if="item.recipe"> <template v-if="item.recipe">
<RouterLink class="related-entity-link related-entity-link--compact" :to="`/recipes/${item.recipe.id}`"> <RouterLink class="related-entity-link related-entity-link--compact" :to="`/recipes/${item.recipe.id}`">

View File

@@ -706,7 +706,7 @@ watch(initialPokemon, applyInitialPokemon, { immediate: true });
</script> </script>
<template> <template>
<section v-if="!pokemon" class="page-stack" aria-busy="true" :aria-label="t('pages.pokemon.loadingDetail')"> <section v-if="!pokemon" key="pokemon-detail-loading" class="page-stack" aria-busy="true" :aria-label="t('pages.pokemon.loadingDetail')">
<div class="page-header page-header--skeleton" aria-hidden="true"> <div class="page-header page-header--skeleton" aria-hidden="true">
<div class="page-header__copy"> <div class="page-header__copy">
<Skeleton width="142px" /> <Skeleton width="142px" />
@@ -719,7 +719,7 @@ watch(initialPokemon, applyInitialPokemon, { immediate: true });
</div> </div>
</div> </div>
<div class="detail-grid detail-grid--stack" aria-hidden="true"> <div class="detail-tab-panel" aria-hidden="true">
<section class="detail-section skeleton-detail-section"> <section class="detail-section skeleton-detail-section">
<div class="detail-section__header"> <div class="detail-section__header">
<Skeleton width="56px" height="24px" /> <Skeleton width="56px" height="24px" />
@@ -762,7 +762,7 @@ watch(initialPokemon, applyInitialPokemon, { immediate: true });
</section> </section>
</div> </div>
</section> </section>
<section v-else class="page-stack"> <section v-else :key="`pokemon-detail-${pokemon.id}`" class="page-stack">
<PageHeader :title="`#${pokemon.displayId} ${pokemon.name}`" :subtitle="pokemon.genus || t('pages.pokemon.detailSubtitle')"> <PageHeader :title="`#${pokemon.displayId} ${pokemon.name}`" :subtitle="pokemon.genus || t('pages.pokemon.detailSubtitle')">
<template #kicker>{{ detailKicker }}</template> <template #kicker>{{ detailKicker }}</template>
<template #actions> <template #actions>
@@ -780,7 +780,7 @@ watch(initialPokemon, applyInitialPokemon, { immediate: true });
<div class="detail-tabs"> <div class="detail-tabs">
<Tabs id="pokemon-detail-tabs" v-model="detailTab" :tabs="detailTabs" :label="t('common.details')" /> <Tabs id="pokemon-detail-tabs" v-model="detailTab" :tabs="detailTabs" :label="t('common.details')" />
<div v-if="detailTab === 'details'" class="detail-grid detail-grid--stack"> <div v-if="detailTab === 'details'" class="detail-tab-panel">
<div class="pokemon-description-grid"> <div class="pokemon-description-grid">
<button v-if="pokemon.image" type="button" class="pokemon-description-image" :aria-label="pokemonImageLabel()" @click="openImageModal"> <button v-if="pokemon.image" type="button" class="pokemon-description-image" :aria-label="pokemonImageLabel()" @click="openImageModal">
<img :src="pokemon.image.url" :alt="pokemonImageAlt()" /> <img :src="pokemon.image.url" :alt="pokemonImageAlt()" />
@@ -983,7 +983,7 @@ watch(initialPokemon, applyInitialPokemon, { immediate: true });
</DetailSection> </DetailSection>
</div> </div>
<div v-else-if="detailTab === 'reference'" class="detail-grid detail-grid--stack"> <div v-else-if="detailTab === 'reference'" class="detail-tab-panel">
<DetailSection :title="t('pages.pokemon.referenceData')"> <DetailSection :title="t('pages.pokemon.referenceData')">
<p class="meta-line">{{ t('pages.pokemon.pokedexReferenceNote') }}</p> <p class="meta-line">{{ t('pages.pokemon.pokedexReferenceNote') }}</p>
</DetailSection> </DetailSection>

View File

@@ -128,7 +128,7 @@ watch(initialRecipe, applyInitialRecipe, { immediate: true });
</script> </script>
<template> <template>
<section v-if="!recipe" class="page-stack" aria-busy="true" :aria-label="t('pages.recipes.loadingDetail')"> <section v-if="!recipe" key="recipe-detail-loading" class="page-stack" aria-busy="true" :aria-label="t('pages.recipes.loadingDetail')">
<div class="page-header page-header--skeleton" aria-hidden="true"> <div class="page-header page-header--skeleton" aria-hidden="true">
<div class="page-header__copy"> <div class="page-header__copy">
<Skeleton width="112px" /> <Skeleton width="112px" />
@@ -141,7 +141,7 @@ watch(initialRecipe, applyInitialRecipe, { immediate: true });
</div> </div>
</div> </div>
<div class="detail-grid" aria-hidden="true"> <div class="detail-tab-panel" aria-hidden="true">
<section v-for="index in 2" :key="index" class="detail-section skeleton-detail-section"> <section v-for="index in 2" :key="index" class="detail-section skeleton-detail-section">
<div class="detail-section__header"> <div class="detail-section__header">
<Skeleton :width="index === 1 ? '92px' : '88px'" height="24px" /> <Skeleton :width="index === 1 ? '92px' : '88px'" height="24px" />
@@ -154,7 +154,7 @@ watch(initialRecipe, applyInitialRecipe, { immediate: true });
</section> </section>
</div> </div>
</section> </section>
<section v-else class="page-stack"> <section v-else :key="`recipe-detail-${recipe.id}`" class="page-stack">
<PageHeader :title="recipe.name" :subtitle="recipeSubtitle"> <PageHeader :title="recipe.name" :subtitle="recipeSubtitle">
<template #kicker>{{ t('pages.recipes.detailKicker') }}</template> <template #kicker>{{ t('pages.recipes.detailKicker') }}</template>
<template #actions> <template #actions>
@@ -172,7 +172,7 @@ watch(initialRecipe, applyInitialRecipe, { immediate: true });
<div class="detail-tabs"> <div class="detail-tabs">
<Tabs id="recipe-detail-tabs" v-model="detailTab" :tabs="detailTabs" :label="t('common.details')" /> <Tabs id="recipe-detail-tabs" v-model="detailTab" :tabs="detailTabs" :label="t('common.details')" />
<div v-if="detailTab === 'details'" class="detail-grid detail-grid--stack"> <div v-if="detailTab === 'details'" class="detail-tab-panel">
<div class="entity-profile-grid"> <div class="entity-profile-grid">
<section class="detail-section entity-profile-media-section" :aria-label="t('pages.recipes.item')"> <section class="detail-section entity-profile-media-section" :aria-label="t('pages.recipes.item')">
<div class="entity-detail-image"> <div class="entity-detail-image">