diff --git a/frui/src/element/Pagination.tsx b/frui/src/element/Pagination.tsx new file mode 100644 index 0000000..966ea27 --- /dev/null +++ b/frui/src/element/Pagination.tsx @@ -0,0 +1,204 @@ +//types +import type { CSSProperties, ReactNode } from 'react'; + +/** + * Pagination Props + */ +export type PaginationProps = { + total?: number; + start?: number; + range?: number; + radius?: 0 | 1 | 2 | 3 | 4; + next?: boolean; + prev?: boolean; + rewind?: boolean; + forward?: boolean; + link?: boolean; + control?: boolean; + border?: boolean; + background?: boolean; + square?: number; + size?: string; + xs?: boolean; + sm?: boolean; + md?: boolean; + lg?: boolean; + xl?: boolean; + xl2?: boolean; + xl3?: boolean; + xl4?: boolean; + xl5?: boolean; + color?: string; + white?: boolean; + black?: boolean; + info?: boolean; + warning?: boolean; + success?: boolean; + error?: boolean; + muted?: boolean; + primary?: boolean; + secondary?: boolean; + page?: (page: number) => void; + className?: string; + style?: CSSProperties; +}; + +/** + * Pagination Component (Main) + */ +export default function Pagination(props: PaginationProps) { + const { + total = 0, + start = 0, + range = 50, + radius = 2, + next, + prev, + rewind, + forward, + link = false, + control = false, + border = true, + background = true, + square = 0, + size, + xs, + sm, + md, + lg, + xl, + xl2, + xl3, + xl4, + xl5, + color, + white, + black, + info, + warning, + success, + error, + muted, + primary, + secondary, + page: select = () => {}, + className, + style, + } = props; + + const defaults: { + classes: string[]; + styles: Record; + } = { + classes: ['frui-pagination'], + styles: {} + }; + + //size handling + const sizeClass = size || ( + xs ? 'xs' : + sm ? 'sm' : + md ? 'md' : + lg ? 'lg' : + xl ? 'xl' : + xl2 ? '2xl' : + xl3 ? '3xl' : + xl4 ? '4xl' : + xl5 ? '5xl' : 'md' + ); + defaults.classes.push(`frui-text-${sizeClass}`); + + //color handling + const colorClass = color || ( + white ? 'white' : + black ? 'black' : + info ? 'info' : + warning ? 'warning' : + success ? 'success' : + error ? 'error' : + muted ? 'muted' : + primary ? 'primary' : + secondary ? 'secondary' : '' + ); + if (colorClass) defaults.classes.push(`frui-tx-${colorClass}`); + + //additional styling + defaults.classes.push(`radius-${radius}`); + if (square > 0) { + defaults.classes.push('square'); + defaults.styles['--square-size'] = `${square}px`; + } + if (link) defaults.classes.push('link'); + if (control) defaults.classes.push('control'); + if (!border) defaults.classes.push('no-border'); + if (!background) defaults.classes.push('no-background'); + + const map = { + classes: [...defaults.classes, className].filter(Boolean).join(' '), + styles: { ...defaults.styles, ...style } + }; + + const totalPages = Math.ceil(total / range); + const currentPage = Math.ceil((start + 1) / range) || 1; + + const calculatePages = () => { + const pages = Array.from({ length: totalPages }, (_, i) => i + 1).filter( + page => page >= currentPage - radius && page <= currentPage + radius + ); + return pages.length > 1 ? pages : []; + }; + + const pages = calculatePages(); + + const renderButton = (content: ReactNode, pageNum: number | null, isControl = false, disabled = false) => { + const onClick = pageNum !== null && !disabled ? () => select(pageNum) : undefined; + const commonProps = { + className: `frui-pagination-btn ${isControl ? 'control' : ''}`, + onClick, + disabled, + 'aria-label': typeof content === 'string' ? content : undefined, + }; + + return link && !isControl ? ( + + {content} + + ) : ( + + ); + }; + + const renderSpan = (content: ReactNode) => { + const isCustomColor = color && !white && !black && !info && !warning && !success && !error && !muted && !primary && !secondary; + const activeStyle = isCustomColor && currentPage === content ? { + backgroundColor: color, + borderColor: color, + color: '#FFFFFF' + } : {}; + + return ( + + {content} + + ); + }; + + return ( + + ); +}; \ No newline at end of file diff --git a/frui/styles/pagination.css b/frui/styles/pagination.css new file mode 100644 index 0000000..1c48e16 --- /dev/null +++ b/frui/styles/pagination.css @@ -0,0 +1,220 @@ +/* Base styles */ +.frui-pagination { + align-items: center; + display: flex; + gap: 8px; + white-space: nowrap; +} + +.frui-pagination-btn, +.frui-pagination-span { + align-items: center; + background-color: var(--bg, #FFFFFF); + border: 1px solid var(--border, #CCCCCC); + color: var(--fg-default, #222222); + display: inline-flex; + height: 32px; + justify-content: center; + min-width: 32px; + padding: 0 8px; + text-decoration: none; + transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease, transform 0.1s ease; +} + +.frui-pagination.link .frui-pagination-btn:not(.control) { + text-decoration: underline; +} + +.frui-pagination.control .frui-pagination-btn.control { + background-color: var(--muted, #777777); + color: var(--white, #FFFFFF); +} + +.frui-pagination.no-border .frui-pagination-btn, +.frui-pagination.no-border .frui-pagination-span { + border: none; +} + +.frui-pagination.no-background .frui-pagination-btn, +.frui-pagination.no-background .frui-pagination-span { + background-color: transparent; +} + +.frui-pagination-span.active { + background-color: var(--primary, #1474FC); + border-color: var(--primary, #1474FC); + color: var(--white, #FFFFFF); + font-weight: bold; +} + +.frui-pagination.frui-tx-white .frui-pagination-span.active { + background-color: var(--white, #FFFFFF); + border-color: var(--white, #FFFFFF); + color: var(--black, #222222); +} + +.frui-pagination.frui-tx-black .frui-pagination-span.active { + background-color: var(--black, #000000); + border-color: var(--black, #000000); + color: var(--white, #FFFFFF); +} + +.frui-pagination.frui-tx-info .frui-pagination-span.active { + background-color: var(--info, #17A2B8); + border-color: var(--info, #17A2B8); + color: var(--white, #FFFFFF); +} + +.frui-pagination.frui-tx-warning .frui-pagination-span.active { + background-color: var(--warning, #FFC107); + border-color: var(--warning, #FFC107); + color: var(--black, #222222); +} + +.frui-pagination.frui-tx-success .frui-pagination-span.active { + background-color: var(--success, #28A745); + border-color: var(--success, #28A745); + color: var(--white, #FFFFFF); +} + +.frui-pagination.frui-tx-error .frui-pagination-span.active { + background-color: var(--error, #DC3545); + border-color: var(--error, #DC3545); + color: var(--white, #FFFFFF); +} + +.frui-pagination.frui-tx-muted .frui-pagination-span.active { + background-color: var(--muted, #6C757D); + border-color: var(--muted, #6C757D); + color: var(--white, #FFFFFF); +} + +.frui-pagination.frui-tx-primary .frui-pagination-span.active { + background-color: var(--primary, #007BFF); + border-color: var(--primary, #007BFF); + color: var(--white, #FFFFFF); +} + +.frui-pagination.frui-tx-secondary .frui-pagination-span.active { + background-color: var(--secondary, #6C757D); + border-color: var(--secondary, #6C757D); + color: var(--white, #FFFFFF); +} + +.frui-pagination-btn:not(.control):hover { + background-color: var(--bg-hover, #F5F5F5); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + color: var(--fg-primary, #007BFF); + transform: translateY(-1px); +} + +.frui-pagination-btn[disabled] { + cursor: not-allowed; + opacity: 0.5; + transform: none; +} + +.frui-pagination.frui-text-xs .frui-pagination-btn, +.frui-pagination.frui-text-xs .frui-pagination-span { + font-size: 12px; + height: 24px; + min-width: 24px; +} + +.frui-pagination.frui-text-sm .frui-pagination-btn, +.frui-pagination.frui-text-sm .frui-pagination-span { + font-size: 14px; + height: 28px; + min-width: 28px; +} + +.frui-pagination.frui-text-md .frui-pagination-btn, +.frui-pagination.frui-text-md .frui-pagination-span { + font-size: 16px; + height: 32px; + min-width: 32px; +} + +.frui-pagination.frui-text-lg .frui-pagination-btn, +.frui-pagination.frui-text-lg .frui-pagination-span { + font-size: 18px; + height: 36px; + min-width: 36px; +} + +.frui-pagination.frui-text-xl .frui-pagination-btn, +.frui-pagination.frui-text-xl .frui-pagination-span { + font-size: 20px; + height: 40px; + min-width: 40px; +} + +.frui-pagination.frui-text-2xl .frui-pagination-btn, +.frui-pagination.frui-text-2xl .frui-pagination-span { + font-size: 24px; + height: 48px; + min-width: 48px; +} + +.frui-pagination.frui-text-3xl .frui-pagination-btn, +.frui-pagination.frui-text-3xl .frui-pagination-span { + font-size: 30px; + height: 56px; + min-width: 56px; +} + +.frui-pagination.frui-text-4xl .frui-pagination-btn, +.frui-pagination.frui-text-4xl .frui-pagination-span { + font-size: 36px; + height: 64px; + min-width: 64px; +} + +.frui-pagination.frui-text-5xl .frui-pagination-btn, +.frui-pagination.frui-text-5xl .frui-pagination-span { + font-size: 48px; + height: 80px; + min-width: 80px; +} + +.frui-pagination.square .frui-pagination-btn, +.frui-pagination.square .frui-pagination-span { + height: var(--square-size); + min-width: unset; + width: var(--square-size); +} + +.frui-pagination.radius-0 .frui-pagination-btn, +.frui-pagination.radius-0 .frui-pagination-span { + border-radius: 0; +} + +.frui-pagination.radius-1 .frui-pagination-btn, +.frui-pagination.radius-1 .frui-pagination-span { + border-radius: 4px; +} + +.frui-pagination.radius-2 .frui-pagination-btn, +.frui-pagination.radius-2 .frui-pagination-span { + border-radius: 8px; +} + +.frui-pagination.radius-3 .frui-pagination-btn, +.frui-pagination.radius-3 .frui-pagination-span { + border-radius: 12px; +} + +.frui-pagination.radius-4 .frui-pagination-btn, +.frui-pagination.radius-4 .frui-pagination-span { + border-radius: 16px; +} + +.frui-pagination.square .frui-pagination-btn, +.frui-pagination.square .frui-pagination-span { + border-radius: 0; +} + +.custom-pagination .frui-pagination-btn:hover:not(.control) { + background-color: #D0E8D9; + color: #00A352; +} \ No newline at end of file diff --git a/web/pages/component/pagination.tsx b/web/pages/component/pagination.tsx new file mode 100644 index 0000000..3dff949 --- /dev/null +++ b/web/pages/component/pagination.tsx @@ -0,0 +1,467 @@ +//types +import type { Crumb } from 'modules/components/Crumbs'; +import type { PaginationProps } from 'frui/element/Pagination'; + +//hooks +import { useState } from 'react'; +import { useLanguage } from 'r22n'; + +//components +import Link from 'next/link'; +import { Translate } from 'r22n'; +import Pagination from 'frui/element/Pagination'; +import { LayoutPanel } from 'modules/theme'; +import Crumbs from 'modules/components/Crumbs'; +import Props from 'modules/components/Props'; +import Code, { InlineCode as C } from 'modules/components/Code'; + +//constants +const codeBasic = ` +const [currentPage, setCurrentPage] = useState(1); + setCurrentPage(p)} +/> +`.trim(); + +const codeMinimal = ` +const [currentPage, setCurrentPage] = useState(1); + setCurrentPage(p)} +/> +`.trim(); + +const codeControls = ` +const [currentPage, setCurrentPage] = useState(1); + setCurrentPage(p)} +/> +`.trim(); + +const codeDisabled = ` +const [currentPage, setCurrentPage] = useState(1); + setCurrentPage(p)} +/> +`.trim(); + +const codeSquare = ` +const [currentPage, setCurrentPage] = useState(1); + setCurrentPage(p)} +/> +`.trim(); + +const codeColors = ` + + + + + + + + + + +`.trim(); + +const codeSizes = ` + + + + + + + + + + +`.trim(); + +const codeCustomColors = ` +const [currentPage, setCurrentPage] = useState(1); + setCurrentPage(p)} +/> +`.trim(); + +const codeCustom = ` +const [currentPage, setCurrentPage] = useState(1); + setCurrentPage(p)} +/> + +// In your CSS file: +.custom-pagination .frui-pagination-btn:hover:not(.control) { + background-color: #D0E8D9; + color: #00A352; +} +`.trim(); + +//functions +function PaginationExample({ total = 500, range = 100, ...props }: PaginationProps) { + const [currentPage, setCurrentPage] = useState(1); + return ( + setCurrentPage(p)} + {...props} + /> + ); +} + +export default function PaginationPage() { + //hooks + const { _ } = useLanguage(); + + //variables + const crumbs: Crumb[] = [ + { icon: 'icons', label: 'Components', href: '/component' }, + { label: 'Pagination' }, + ]; + + const props = [ + [_('total'), 'number', 'No', 'Total items (default: 0)'], + [_('start'), 'number', 'No', 'Starting index (default: 0)'], + [_('range'), 'number', 'No', 'Items per page (default: 50)'], + [_('radius'), '0 | 1 | 2 | 3 | 4', 'No', 'Pages shown before/after current (default: 2)'], + [_('next'), 'boolean', 'No', 'Show next button (disabled on last page)'], + [_('prev'), 'boolean', 'No', 'Show previous button (disabled on first page)'], + [_('rewind'), 'boolean', 'No', 'Show first page button (disabled on first page)'], + [_('forward'), 'boolean', 'No', 'Show last page button (disabled on last page)'], + [_('link'), 'boolean', 'No', 'Render page numbers as links (default: false)'], + [_('control'), 'boolean', 'No', 'Style control buttons differently (default: false)'], + [_('border'), 'boolean', 'No', 'Show button borders (default: true)'], + [_('background'), 'boolean', 'No', 'Show button backgrounds (default: true)'], + [_('square'), 'number', 'No', 'Set square size in pixels (default: 0)'], + [_('size'), 'string', 'No', 'Custom size class (e.g., "custom-size")'], + [_('xs'), 'boolean', 'No', '12px button size'], + [_('sm'), 'boolean', 'No', '14px button size'], + [_('md'), 'boolean', 'No', '16px button size (default)'], + [_('lg'), 'boolean', 'No', '18px button size'], + [_('xl'), 'boolean', 'No', '20px button size'], + [_('xl2'), 'boolean', 'No', '24px button size'], + [_('xl3'), 'boolean', 'No', '30px button size'], + [_('xl4'), 'boolean', 'No', '36px button size'], + [_('xl5'), 'boolean', 'No', '48px button size'], + [_('color'), 'string', 'No', 'Custom color (e.g., "purple")'], + [_('white'), 'boolean', 'No', 'White active color'], + [_('black'), 'boolean', 'No', 'Black active color'], + [_('info'), 'boolean', 'No', 'Blue active color'], + [_('warning'), 'boolean', 'No', 'Orange active color'], + [_('success'), 'boolean', 'No', 'Green active color'], + [_('error'), 'boolean', 'No', 'Red active color'], + [_('muted'), 'boolean', 'No', 'Gray active color'], + [_('primary'), 'boolean', 'No', 'Primary blue active color'], + [_('secondary'), 'boolean', 'No', 'Secondary gray active color'], + [_('page'), '(page: number) => void', 'No', 'Callback for page selection'], + [_('className'), 'string', 'No', 'Additional CSS classes'], + [_('style'), 'CSSProperties', 'No', 'Inline CSS styles'], + ]; + + //render + return ( + +
+
+ +
+
+ +
+

+ {_('Pagination')} +

+ + {`import Pagination from 'frui/Pagination';`} + + +

+ {_('Props')} +

+ + +

+ {_('Basic Usage')} +

+

+ Simple pagination with page numbers only. +

+
+
+ +
+ + {codeBasic} + +
+ +

+ {_('Minimal Navigation')} +

+

+ Pagination with just prev and next buttons. +

+
+
+ +
+ + {codeMinimal} + +
+ +

+ {_('Full Controls')} +

+

+ Pagination with rewind, prev, next, and forward controls. +

+
+
+ +
+ + {codeControls} + +
+ +

+ {_('Disabled Controls')} +

+

+ Pagination with controls disabled on first and last pages. +

+
+
+ +
+ + {codeDisabled} + +
+ +

+ {_('Square Layout')} +

+

+ Pagination with square buttons and no borders. +

+
+
+ +
+ + {codeSquare} + +
+ +

+ {_('Colors')} +

+

+ All color variants for active page. +

+
+
+ + + + + + + + + + +
+ + {codeColors} + +
+ +

+ {_('Sizes')} +

+

+ All size variants from xs to xl5, plus custom size. +

+
+
+ + + + + + + + + + +
+ + {codeSizes} + +
+ +

+ {_('Custom Colors and Sizes')} +

+

+ Pagination with custom color and size, plus link and control styling. +

+
+
+ +
+ + {codeCustomColors} + +
+ +

+ {_('Custom Styling')} +

+

+ + You can add your own custom class to the pagination component or use any combination of the following CSS classes: , , to fully customize the appearance. + +

+
+
+ +
+ + {codeCustom} + +
+ +
+ + + {_('Breadcrumb')} + +
+ + {_('Table')} + + +
+
+
+
+
+ ); +} \ No newline at end of file