diff --git a/apps/client/src/stylesheets/theme-next/forms.css b/apps/client/src/stylesheets/theme-next/forms.css index 2fc8a39dbd..cbe5f2cda0 100644 --- a/apps/client/src/stylesheets/theme-next/forms.css +++ b/apps/client/src/stylesheets/theme-next/forms.css @@ -145,6 +145,10 @@ button.tn-low-profile:hover { font-size: calc(var(--icon-button-size) * var(--icon-button-icon-ratio)); } +:root .icon-action.disabled::before { + opacity: .5; +} + :root .icon-action:not(.global-menu-button):hover, :root .icon-action:not(.global-menu-button).show, :root .tn-tool-button:hover, diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index b12ff472dc..377a287183 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2190,8 +2190,9 @@ "percentage": "%" }, "pagination": { - "page_title": "Page of {{startIndex}} - {{endIndex}}", - "total_notes": "{{count}} notes" + "total_notes": "{{count}} notes", + "prev_page": "Previous page", + "next_page": "Next page" }, "collections": { "rendering_error": "Unable to show content due to an error." diff --git a/apps/client/src/widgets/collections/NoteList.css b/apps/client/src/widgets/collections/NoteList.css index 198bc1bdaf..d312f6a427 100644 --- a/apps/client/src/widgets/collections/NoteList.css +++ b/apps/client/src/widgets/collections/NoteList.css @@ -23,14 +23,3 @@ body.prefers-centered-content .note-list-widget:not(.full-height) { .note-list-widget video { height: 100%; } - -/* #region Pagination */ -.note-list-pager { - font-size: 1rem; - - span.current-page { - text-decoration: underline; - font-weight: bold; - } -} -/* #endregion */ diff --git a/apps/client/src/widgets/collections/Pagination.css b/apps/client/src/widgets/collections/Pagination.css new file mode 100644 index 0000000000..93ee13a76d --- /dev/null +++ b/apps/client/src/widgets/collections/Pagination.css @@ -0,0 +1,88 @@ +:where(.note-list-pager) { + --note-list-pager-page-button-width: 40px; + --note-list-pager-page-button-gap: 3px; + --note-list-pager-ellipsis-width: 20px; + --note-list-pager-justify-content: flex-end; + + --note-list-pager-current-page-button-background-color: var(--button-group-active-button-background); + --note-list-pager-current-page-button-text-color: var(--button-group-active-button-text-color); +} + +.note-list-pager-container { + display: flex; + flex-direction: column; + width: 100%; + container: note-list-pager / inline-size; +} + +.note-list-pager { + display: flex; + align-items: center; + font-size: .8rem; + align-self: var(--note-list-pager-justify-content); + + .note-list-pager-nav-button { + --icon-button-icon-ratio: .75; + } + + .note-list-pager-page-button-container { + display: flex; + align-items: baseline; + justify-content: space-around; + gap: var(--note-list-pager-page-button-gap); + + &.note-list-pager-ellipsis-present { + /* Prevent the prev/next buttons from shifting when ellipses appear or disappear */ + --_gap-max-width: calc((var(--note-list-pager-page-button-count) + 2) * var(--note-list-pager-page-button-gap)); + + min-width: calc(var(--note-list-pager-page-button-count) * var(--note-list-pager-page-button-width) + + (var(--note-list-pager-ellipsis-width) * 2) + + var(--_gap-max-width)); + } + + .note-list-pager-page-button { + min-width: var(--note-list-pager-page-button-width); + padding-inline: 0; + padding-block: 4px; + + &.note-list-pager-page-button-current { + background: var(--note-list-pager-current-page-button-background-color); + color: var(--note-list-pager-current-page-button-text-color); + font-weight: bold; + opacity: unset; + } + } + + .note-list-pager-ellipsis { + display: inline-block; + width: var(--note-list-pager-ellipsis-width); + text-align: center; + opacity: .5; + } + } + + .note-list-pager-narrow-counter { + display: none; + min-width: 60px; + text-align: center; + white-space: nowrap; + } + + .note-list-pager-total-count { + margin-inline-start: 8px; + opacity: .5; + white-space: nowrap; + } + + @container note-list-pager (max-width: 550px) { + .note-list-pager-page-button-container, + .note-list-pager-total-count { + display: none; + } + + .note-list-pager-narrow-counter { + display: block; + } + } +} + diff --git a/apps/client/src/widgets/collections/Pagination.tsx b/apps/client/src/widgets/collections/Pagination.tsx index 6b74964a64..26d22215cb 100644 --- a/apps/client/src/widgets/collections/Pagination.tsx +++ b/apps/client/src/widgets/collections/Pagination.tsx @@ -4,6 +4,10 @@ import FNote from "../../entities/fnote"; import froca from "../../services/froca"; import { useNoteLabelInt } from "../react/hooks"; import { t } from "../../services/i18n"; +import ActionButton from "../react/ActionButton"; +import Button from "../react/Button"; +import "./Pagination.css"; +import clsx from "clsx"; interface PaginationContext { page: number; @@ -17,46 +21,106 @@ interface PaginationContext { export function Pager({ page, pageSize, setPage, pageCount, totalNotes }: Omit) { if (pageCount < 2) return; - let lastPrinted = false; - let children: ComponentChildren[] = []; - for (let i = 1; i <= pageCount; i++) { - if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(page - i) <= 2) { - lastPrinted = true; - - const startIndex = (i - 1) * pageSize + 1; - const endIndex = Math.min(totalNotes, i * pageSize); - - if (i !== page) { - children.push(( - setPage(i)} - > - {i} - - )) - } else { - // Current page - children.push({i}) - } - - children.push(<>{" "} {" "}); - } else if (lastPrinted) { - children.push(<>{"... "} {" "}); - lastPrinted = false; - } - } - return ( -
- {children} +
+
+ setPage(page - 1)} + /> - ({t("pagination.total_notes", { count: totalNotes })}) + +
+ {page} / {pageCount} +
+ + setPage(page + 1)} + /> + +
+ {t("pagination.total_notes", { count: totalNotes })} +
+
) } +interface PageButtonsProps { + page: number; + setPage: Dispatch>; + pageCount: number; +} + +function PageButtons(props: PageButtonsProps) { + const maxButtonCount = 9; + const maxLeftRightSegmentLength = 2; + + // The left-side segment + const leftLength = Math.min(props.pageCount, maxLeftRightSegmentLength); + const leftStart = 1; + + // The middle segment + const middleMaxLength = maxButtonCount - maxLeftRightSegmentLength * 2; + const middleLength = Math.min(props.pageCount - leftLength, middleMaxLength); + let middleStart = props.page - Math.floor(middleLength / 2); + middleStart = Math.max(middleStart, leftLength + 1); + + // The right-side segment + const rightLength = Math.min(props.pageCount - (middleLength + leftLength), maxLeftRightSegmentLength); + const rightStart = props.pageCount - rightLength + 1; + middleStart = Math.min(middleStart, rightStart - middleLength); + + const totalButtonCount = leftLength + middleLength + rightLength; + const hasLeadingEllipsis = (middleStart - leftLength > 1); + const hasTrailingEllipsis = (rightStart - (middleStart + middleLength - 1) > 1); + + return
+ {[ + ...createSegment(leftStart, leftLength, props.page, props.setPage, false), + ...createSegment(middleStart, middleLength, props.page, props.setPage, hasLeadingEllipsis), + ...createSegment(rightStart, rightLength, props.page, props.setPage, hasTrailingEllipsis), + ]} +
; +} + +function createSegment(start: number, length: number, currentPage: number, setPage: Dispatch>, prependEllipsis: boolean): ComponentChildren[] { + const children: ComponentChildren[] = []; + + if (prependEllipsis) { + children.push(...); + } + + for (let i = 0; i < length; i++) { + const pageNum = start + i; + const isCurrent = (pageNum === currentPage); + children.push(( +