refactor(frontend): extract PaginationItem to own pagination-link styling

BasePagination was reaching across slot boundaries with :deep() to style
.pagination-previous / -next / -link — markup it doesn't actually render.
Move that markup and the related scoped rules into a new PaginationItem
component that polymorphically renders RouterLink (when `to` is given)
or BaseButton (emit-based). BasePagination keeps only the scaffold it
actually owns: .pagination, .pagination-list, .pagination-ellipsis.

Pagination.vue and PaginationEmit.vue become thin wrappers around
BasePagination + PaginationItem; no more raw pagination-* class usage or
BaseButton imports in the emit wrapper.

The .app-container.has-background / .link-share-container.has-background
theme override moves with the .pagination-link rules into PaginationItem
as its own unscoped <style> block.

Result: 0 remaining :deep(.pagination-*) selectors (was 14).
This commit is contained in:
kolaente 2026-04-17 16:40:04 +02:00 committed by kolaente
parent 5ea7853dd6
commit 8f64836999
4 changed files with 176 additions and 110 deletions

View File

@ -69,7 +69,7 @@ function createPagination(totalPages: number, currentPage: number): PaginationPa
}
continue
}
pages.push({
number: i + 1,
isEllipsis: false,
@ -82,10 +82,10 @@ const pages = computed(() => createPagination(props.totalPages, props.currentPag
</script>
<style lang="scss" scoped>
// Rules ported from bulma-css-variables/sass/components/pagination.sass.
// Slot content (.pagination-previous, .pagination-next, .pagination-link) is
// rendered by Pagination.vue / PaginationEmit.vue, so scoped attributes don't
// reach them we use :deep() to style across the slot boundary.
// Layout/scaffold rules ported from bulma-css-variables/sass/components/pagination.sass.
// BasePagination only owns .pagination / .pagination-list / .pagination-ellipsis
// the actual pagination items (.pagination-previous / -next / -link) and their
// styles live in PaginationItem.vue.
.pagination {
align-items: center;
@ -113,9 +113,6 @@ const pages = computed(() => createPagination(props.totalPages, props.currentPag
}
}
:deep(.pagination-previous),
:deep(.pagination-next),
:deep(.pagination-link),
.pagination-ellipsis {
appearance: none;
align-items: center;
@ -136,64 +133,6 @@ const pages = computed(() => createPagination(props.totalPages, props.currentPag
-webkit-touch-callout: none;
user-select: none;
&:focus,
&:active {
outline: none;
}
&[disabled],
fieldset[disabled] & {
cursor: not-allowed;
}
}
:deep(.pagination-previous),
:deep(.pagination-next),
:deep(.pagination-link) {
border-color: var(--border);
color: var(--text-strong);
min-inline-size: 2.5em;
&:hover {
border-color: var(--link-hover-border);
color: var(--link-hover);
}
&:focus {
border-color: var(--link-focus-border);
}
&:active {
box-shadow: inset 0 1px 2px rgba($scheme-invert, 0.2);
}
&[disabled] {
background-color: var(--border);
border-color: var(--border);
box-shadow: none;
color: var(--text-light);
opacity: 0.5;
}
}
:deep(.pagination-previous),
:deep(.pagination-next) {
padding-inline: 0.75em;
white-space: nowrap;
&:not(:disabled):hover {
background: $scheme-main;
cursor: pointer;
}
}
:deep(.pagination-link.is-current) {
background-color: var(--link);
border-color: var(--link);
color: var(--link-invert);
}
.pagination-ellipsis {
color: var(--grey-light);
pointer-events: none;
}
@ -203,12 +142,6 @@ const pages = computed(() => createPagination(props.totalPages, props.currentPag
flex-wrap: wrap;
}
:deep(.pagination-previous),
:deep(.pagination-next) {
flex-grow: 1;
flex-shrink: 1;
}
.pagination-list li {
flex-grow: 1;
flex-shrink: 1;
@ -221,9 +154,6 @@ const pages = computed(() => createPagination(props.totalPages, props.currentPag
flex-shrink: 1;
}
:deep(.pagination-previous),
:deep(.pagination-next),
:deep(.pagination-link),
.pagination-ellipsis {
margin-block: 0;
}
@ -239,14 +169,3 @@ const pages = computed(() => createPagination(props.totalPages, props.currentPag
}
}
</style>
<style lang="scss">
// Unscoped: this rule relies on ancestors (.app-container.has-background /
// .link-share-container.has-background) that live outside BasePagination.
// Previously lived in styles/theme/background.scss.
.app-container.has-background .pagination-link:not(.is-current),
.link-share-container.has-background .pagination-link:not(.is-current) {
background: var(--grey-100);
}
</style>

View File

@ -4,38 +4,39 @@
:current-page="currentPage"
>
<template #previous="{ disabled }">
<RouterLink
:disabled="disabled || undefined"
<PaginationItem
variant="previous"
:to="getRouteForPagination(currentPage - 1)"
class="pagination-previous"
:disabled="disabled"
>
{{ $t('misc.previous') }}
</RouterLink>
</PaginationItem>
</template>
<template #next="{ disabled }">
<RouterLink
:disabled="disabled || undefined"
<PaginationItem
variant="next"
:to="getRouteForPagination(currentPage + 1)"
class="pagination-next"
:disabled="disabled"
>
{{ $t('misc.next') }}
</RouterLink>
</PaginationItem>
</template>
<template #page-link="{ page, isCurrent }">
<RouterLink
class="pagination-link"
:aria-label="'Goto page ' + page.number"
:class="{ 'is-current': isCurrent }"
<PaginationItem
variant="link"
:to="getRouteForPagination(page.number)"
:is-current="isCurrent"
:aria-label="'Goto page ' + page.number"
>
{{ page.number }}
</RouterLink>
</PaginationItem>
</template>
</BasePagination>
</template>
<script lang="ts" setup>
import BasePagination from '@/components/base/BasePagination.vue'
import PaginationItem from '@/components/misc/PaginationItem.vue'
import { useRoute } from 'vue-router'
withDefaults(defineProps<{

View File

@ -4,39 +4,39 @@
:current-page="currentPage"
>
<template #previous="{ disabled }">
<BaseButton
<PaginationItem
variant="previous"
:disabled="disabled"
class="pagination-previous"
@click="changePage(currentPage - 1)"
>
{{ $t('misc.previous') }}
</BaseButton>
</PaginationItem>
</template>
<template #next="{ disabled }">
<BaseButton
<PaginationItem
variant="next"
:disabled="disabled"
class="pagination-next"
@click="changePage(currentPage + 1)"
>
{{ $t('misc.next') }}
</BaseButton>
</PaginationItem>
</template>
<template #page-link="{ page, isCurrent }">
<BaseButton
class="pagination-link"
<PaginationItem
variant="link"
:is-current="isCurrent"
:aria-label="'Goto page ' + page.number"
:class="{ 'is-current': isCurrent }"
@click="changePage(page.number)"
>
{{ page.number }}
</BaseButton>
</PaginationItem>
</template>
</BasePagination>
</template>
<script lang="ts" setup>
import BasePagination from '@/components/base/BasePagination.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import PaginationItem from '@/components/misc/PaginationItem.vue'
const props = withDefaults(defineProps<{
totalPages: number,

View File

@ -0,0 +1,146 @@
<template>
<RouterLink
v-if="to !== undefined"
:to="to"
:disabled="disabled || undefined"
:class="[`pagination-${variant}`, {'is-current': isCurrent}]"
>
<slot />
</RouterLink>
<BaseButton
v-else
:disabled="disabled"
:class="[`pagination-${variant}`, {'is-current': isCurrent}]"
@click="emit('click')"
>
<slot />
</BaseButton>
</template>
<script lang="ts" setup>
import type {RouteLocationRaw} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue'
withDefaults(defineProps<{
variant: 'previous' | 'next' | 'link',
isCurrent?: boolean,
disabled?: boolean,
to?: RouteLocationRaw,
}>(), {
isCurrent: false,
disabled: false,
to: undefined,
})
const emit = defineEmits<{
(e: 'click'): void,
}>()
</script>
<style lang="scss" scoped>
// Rules ported from bulma-css-variables/sass/components/pagination.sass.
// PaginationItem owns the .pagination-previous / .pagination-next /
// .pagination-link markup, so scoped attributes attach directly to these
// classes no :deep() necessary.
.pagination-previous,
.pagination-next,
.pagination-link {
appearance: none;
align-items: center;
border: 1px solid transparent;
border-radius: $radius;
box-shadow: none;
display: inline-flex;
font-size: 1em;
block-size: 2.5em;
justify-content: center;
line-height: 1.5;
margin: 0.25rem;
padding: calc(0.5em - 1px) 0.5em;
position: relative;
text-align: center;
vertical-align: top;
-webkit-touch-callout: none;
user-select: none;
&:focus,
&:active {
outline: none;
}
&[disabled],
fieldset[disabled] & {
cursor: not-allowed;
}
border-color: var(--border);
color: var(--text-strong);
min-inline-size: 2.5em;
&:hover {
border-color: var(--link-hover-border);
color: var(--link-hover);
}
&:focus {
border-color: var(--link-focus-border);
}
&:active {
box-shadow: inset 0 1px 2px rgba($scheme-invert, 0.2);
}
&[disabled] {
background-color: var(--border);
border-color: var(--border);
box-shadow: none;
color: var(--text-light);
opacity: 0.5;
}
}
.pagination-previous,
.pagination-next {
padding-inline: 0.75em;
white-space: nowrap;
&:not(:disabled):hover {
background: $scheme-main;
cursor: pointer;
}
}
.pagination-link.is-current {
background-color: var(--link);
border-color: var(--link);
color: var(--link-invert);
}
@media screen and (max-width: $tablet - 1px) {
.pagination-previous,
.pagination-next {
flex-grow: 1;
flex-shrink: 1;
}
}
@media screen and (min-width: $tablet), print {
.pagination-previous,
.pagination-next,
.pagination-link {
margin-block: 0;
}
}
</style>
<style lang="scss">
// Unscoped: this rule relies on ancestors (.app-container.has-background /
// .link-share-container.has-background) that live outside PaginationItem.
// Previously lived in styles/theme/background.scss, then BasePagination.vue.
.app-container.has-background .pagination-link:not(.is-current),
.link-share-container.has-background .pagination-link:not(.is-current) {
background: var(--grey-100);
}
</style>