refactor(admin): redesign system wording layout with sidebar and tabs
Replace module and surface dropdowns with a sidebar and tabbed interface. Improve navigation and usability in the admin wording section.
This commit is contained in:
@@ -2409,15 +2409,98 @@ button:disabled,
|
|||||||
font-weight: 850;
|
font-weight: 850;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-wording-toolbar {
|
.system-wording-header {
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system-wording-header__locale {
|
||||||
|
width: min(260px, 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-sidebar {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-card);
|
||||||
|
background: var(--surface-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-sidebar__title {
|
||||||
|
padding: 2px 4px 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-sidebar__button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 9px 10px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-control);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-weight: 850;
|
||||||
|
text-align: left;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-sidebar__button:hover {
|
||||||
|
border-color: rgba(42, 117, 187, 0.24);
|
||||||
|
background: rgba(255, 203, 5, 0.2);
|
||||||
|
color: var(--pokemon-blue-deep);
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-sidebar__button.active {
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
background: var(--pokemon-blue);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 2px 0 var(--line-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-sidebar__button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-content {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-controls .tabs--component {
|
||||||
|
flex: 1 1 320px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.system-wording-toolbar__check {
|
.system-wording-toolbar__check {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.system-wording-list li {
|
.system-wording-list li {
|
||||||
@@ -3795,10 +3878,22 @@ button:disabled,
|
|||||||
.pokemon-profile-grid,
|
.pokemon-profile-grid,
|
||||||
.pokemon-profile-row,
|
.pokemon-profile-row,
|
||||||
.pokemon-related-grid,
|
.pokemon-related-grid,
|
||||||
|
.system-wording-layout,
|
||||||
.admin-layout {
|
.admin-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system-wording-sidebar {
|
||||||
|
display: flex;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-sidebar__button {
|
||||||
|
width: auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.coming-soon-panel {
|
.coming-soon-panel {
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
@@ -3884,6 +3979,32 @@ button:disabled,
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system-wording-header {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-header__locale {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-controls .tabs--component {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-toolbar__check {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-list li {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-wording-list .row-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.life-feed__list {
|
.life-feed__list {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,18 @@ const wordingLocaleOptions = computed(() =>
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
const wordingModules = computed(() => [...new Set(wordingRows.value.map((item) => item.module))].sort((a, b) => a.localeCompare(b)));
|
const wordingModules = computed(() => [...new Set(wordingRows.value.map((item) => item.module))].sort((a, b) => a.localeCompare(b)));
|
||||||
|
const wordingSurfaceTabs = computed<TabOption[]>(() => [
|
||||||
|
{ value: '', label: t('pages.admin.allSurfaces') },
|
||||||
|
{ value: 'frontend', label: t('pages.admin.surfaceFrontend') },
|
||||||
|
{ value: 'backend', label: t('pages.admin.surfaceBackend') },
|
||||||
|
{ value: 'email', label: t('pages.admin.surfaceEmail') }
|
||||||
|
]);
|
||||||
|
const activeWordingSurfaceTab = computed({
|
||||||
|
get: () => wordingSurface.value,
|
||||||
|
set: (value: string) => {
|
||||||
|
wordingSurface.value = value === 'frontend' || value === 'backend' || value === 'email' ? value : '';
|
||||||
|
}
|
||||||
|
});
|
||||||
const filteredWordingRows = computed(() =>
|
const filteredWordingRows = computed(() =>
|
||||||
wordingRows.value.filter((item) => {
|
wordingRows.value.filter((item) => {
|
||||||
if (wordingModule.value && item.module !== wordingModule.value) return false;
|
if (wordingModule.value && item.module !== wordingModule.value) return false;
|
||||||
@@ -230,6 +242,10 @@ function resetWordingForm() {
|
|||||||
wordingForm.value = { key: '', locale: wordingLocale.value || defaultLocale, value: '', defaultValue: '', placeholders: [] };
|
wordingForm.value = { key: '', locale: wordingLocale.value || defaultLocale, value: '', defaultValue: '', placeholders: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectWordingModule(module: string) {
|
||||||
|
wordingModule.value = module;
|
||||||
|
}
|
||||||
|
|
||||||
function openNewConfig() {
|
function openNewConfig() {
|
||||||
resetConfigForm();
|
resetConfigForm();
|
||||||
configModalOpen.value = true;
|
configModalOpen.value = true;
|
||||||
@@ -817,11 +833,9 @@ onMounted(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-else-if="canEdit && activeTab === 'wordings'" class="detail-section">
|
<section v-else-if="canEdit && activeTab === 'wordings'" class="detail-section">
|
||||||
<div class="detail-section__header">
|
<div class="detail-section__header system-wording-header">
|
||||||
<h2>{{ t('pages.admin.wordings') }}</h2>
|
<h2>{{ t('pages.admin.wordings') }}</h2>
|
||||||
</div>
|
<div class="field system-wording-header__locale">
|
||||||
<div class="toolbar system-wording-toolbar">
|
|
||||||
<div class="field">
|
|
||||||
<label for="wording-locale">{{ t('pages.admin.wordingLocale') }}</label>
|
<label for="wording-locale">{{ t('pages.admin.wordingLocale') }}</label>
|
||||||
<select id="wording-locale" v-model="wordingLocale" :disabled="busy" @change="reloadWordings">
|
<select id="wording-locale" v-model="wordingLocale" :disabled="busy" @change="reloadWordings">
|
||||||
<option v-for="language in wordingLocaleOptions" :key="language.code" :value="language.code">
|
<option v-for="language in wordingLocaleOptions" :key="language.code" :value="language.code">
|
||||||
@@ -829,49 +843,66 @@ onMounted(() => {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
</div>
|
||||||
<label for="wording-module">{{ t('pages.admin.wordingModule') }}</label>
|
<div class="system-wording-layout">
|
||||||
<select id="wording-module" v-model="wordingModule" :disabled="busy">
|
<nav class="system-wording-sidebar" :aria-label="t('pages.admin.wordingModule')">
|
||||||
<option value="">{{ t('pages.admin.allModules') }}</option>
|
<span class="system-wording-sidebar__title">{{ t('pages.admin.wordingModule') }}</span>
|
||||||
<option v-for="module in wordingModules" :key="module" :value="module">{{ module }}</option>
|
<button
|
||||||
</select>
|
type="button"
|
||||||
</div>
|
class="system-wording-sidebar__button"
|
||||||
<div class="field">
|
:class="{ active: !wordingModule }"
|
||||||
<label for="wording-surface">{{ t('pages.admin.wordingSurface') }}</label>
|
:aria-current="!wordingModule ? 'true' : undefined"
|
||||||
<select id="wording-surface" v-model="wordingSurface" :disabled="busy">
|
:disabled="busy"
|
||||||
<option value="">{{ t('pages.admin.allSurfaces') }}</option>
|
@click="selectWordingModule('')"
|
||||||
<option value="frontend">{{ t('pages.admin.surfaceFrontend') }}</option>
|
>
|
||||||
<option value="backend">{{ t('pages.admin.surfaceBackend') }}</option>
|
{{ t('pages.admin.allModules') }}
|
||||||
<option value="email">{{ t('pages.admin.surfaceEmail') }}</option>
|
</button>
|
||||||
</select>
|
<button
|
||||||
</div>
|
v-for="module in wordingModules"
|
||||||
<div class="check-row system-wording-toolbar__check">
|
:key="module"
|
||||||
<label>
|
type="button"
|
||||||
<input v-model="wordingMissingOnly" type="checkbox" :disabled="busy" />
|
class="system-wording-sidebar__button"
|
||||||
{{ t('pages.admin.wordingMissingOnly') }}
|
:class="{ active: wordingModule === module }"
|
||||||
</label>
|
:aria-current="wordingModule === module ? 'true' : undefined"
|
||||||
|
:disabled="busy"
|
||||||
|
@click="selectWordingModule(module)"
|
||||||
|
>
|
||||||
|
{{ module }}
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="system-wording-content">
|
||||||
|
<div class="system-wording-controls">
|
||||||
|
<Tabs id="admin-wording-surface" v-model="activeWordingSurfaceTab" :tabs="wordingSurfaceTabs" :label="t('pages.admin.wordingSurface')" />
|
||||||
|
<div class="check-row system-wording-toolbar__check">
|
||||||
|
<label>
|
||||||
|
<input v-model="wordingMissingOnly" type="checkbox" :disabled="busy" />
|
||||||
|
{{ t('pages.admin.wordingMissingOnly') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul v-if="filteredWordingRows.length" class="row-list system-wording-list">
|
||||||
|
<li v-for="item in filteredWordingRows" :key="item.key">
|
||||||
|
<span class="system-wording-row">
|
||||||
|
<strong>{{ item.key }}</strong>
|
||||||
|
<span class="system-wording-row__meta">
|
||||||
|
<span class="config-flag">{{ item.module }}</span>
|
||||||
|
<span class="config-flag">{{ t(`pages.admin.surface${item.surface.charAt(0).toUpperCase()}${item.surface.slice(1)}`) }}</span>
|
||||||
|
<span v-if="item.missing" class="config-flag">{{ t('pages.admin.missingTranslation') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="system-wording-row__value">{{ item.value || item.defaultValue }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="row-actions">
|
||||||
|
<button type="button" :disabled="busy" @click="editWording(item)">
|
||||||
|
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
|
||||||
|
{{ t('common.edit') }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p v-else class="meta-line">{{ t('common.noRecords') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ul v-if="filteredWordingRows.length" class="row-list system-wording-list">
|
|
||||||
<li v-for="item in filteredWordingRows" :key="item.key">
|
|
||||||
<span class="system-wording-row">
|
|
||||||
<strong>{{ item.key }}</strong>
|
|
||||||
<span class="system-wording-row__meta">
|
|
||||||
<span class="config-flag">{{ item.module }}</span>
|
|
||||||
<span class="config-flag">{{ t(`pages.admin.surface${item.surface.charAt(0).toUpperCase()}${item.surface.slice(1)}`) }}</span>
|
|
||||||
<span v-if="item.missing" class="config-flag">{{ t('pages.admin.missingTranslation') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="system-wording-row__value">{{ item.value || item.defaultValue }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="row-actions">
|
|
||||||
<button type="button" :disabled="busy" @click="editWording(item)">
|
|
||||||
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
|
|
||||||
{{ t('common.edit') }}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p v-else class="meta-line">{{ t('common.noRecords') }}</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-else-if="canEdit && activeTab === 'pokemon'" class="detail-section">
|
<section v-else-if="canEdit && activeTab === 'pokemon'" class="detail-section">
|
||||||
|
|||||||
Reference in New Issue
Block a user