@startupjs-ui/pagination 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
7
+
8
+ **Note:** Version bump only for package @startupjs-ui/pagination
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
15
+
16
+
17
+ ### Features
18
+
19
+ * add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
20
+ * **pagination:** refactor Pagination component ([469d5f3](https://github.com/startupjs/startupjs-ui/commit/469d5f35b5d32122622a627d68832ec9a7b34729))
package/README.mdx ADDED
@@ -0,0 +1,165 @@
1
+ import { useState } from 'react'
2
+ import { $ } from 'startupjs'
3
+ import { Sandbox } from '@startupjs-ui/docs'
4
+ import Div from '@startupjs-ui/div'
5
+ import Pagination, { _PropsJsonSchema as PaginationPropsJsonSchema } from './index'
6
+
7
+ # Pagination
8
+
9
+ The Pagination component enables the user to select a specific page from a range of pages.
10
+
11
+ ```jsx
12
+ import { Pagination } from 'startupjs-ui'
13
+ ```
14
+
15
+ ## Simple example
16
+
17
+ - `pages` specifies the total number of pages
18
+ - `page` specifies the current page, starting from `0` to `pages - 1`
19
+
20
+ ```jsx example
21
+ const [page, setPage] = useState(0)
22
+ return (
23
+ <Div row align='center'>
24
+ <Pagination
25
+ page={page}
26
+ pages={10}
27
+ onChangePage={setPage}
28
+ />
29
+ </Div>
30
+ )
31
+ ```
32
+
33
+ ## Alternatives
34
+
35
+ - `count` - total count of items
36
+ - `limit` - count of items to display per page
37
+ - `skip` - count of items to skip
38
+
39
+ You can use them instead of `pages` or `page` properties:
40
+
41
+ - `count` and `limit` instead of `pages`
42
+ - `limit` and `skip` instead of `page`
43
+
44
+ ```jsx example
45
+ const COUNT = 100
46
+ const LIMIT = 10
47
+ const [skip, setSkip] = useState(0)
48
+ return (
49
+ <Div row align='center'>
50
+ <Pagination
51
+ count={COUNT}
52
+ limit={LIMIT}
53
+ skip={skip}
54
+ onChangePage={(val) => setSkip(val * LIMIT)}
55
+ />
56
+ </Div>
57
+ )
58
+ ```
59
+
60
+ ## Two-way data bindings
61
+
62
+ You can use two-way data bindings `$page`, `$limit`, `$skip` instead of `page`, `limit`, `skip` respectively.
63
+
64
+ ```jsx example
65
+ const $page = $(0)
66
+ return (
67
+ <Div row align='center'>
68
+ <Pagination
69
+ $page={$page}
70
+ pages={10}
71
+ />
72
+ </Div>
73
+ )
74
+ ```
75
+
76
+ ```jsx example
77
+ const LIMIT = 10
78
+ const $skip = $(0)
79
+ return (
80
+ <Div row align='center'>
81
+ <Pagination
82
+ pages={10}
83
+ limit={LIMIT}
84
+ $skip={$skip}
85
+ onChangePage={(val) => $skip.set(val * LIMIT)}
86
+ />
87
+ </Div>
88
+ )
89
+ ```
90
+
91
+ ## Buttons
92
+
93
+ - `showPrevButton` displays the previous-page button, (default `true`)
94
+ - `showNextButton` displays the next-page button (default `true`)
95
+ - `showFirstButton` displays the first-page button (default `false`)
96
+ - `showLastButton` displays the last-page button (default `false`)
97
+
98
+ ```jsx example
99
+ const [page, setPage] = useState(0)
100
+ return (
101
+ <Div row align='center'>
102
+ <Pagination
103
+ page={page}
104
+ pages={10}
105
+ showPrevButton={false}
106
+ showNextButton={false}
107
+ showFirstButton
108
+ showLastButton
109
+ onChangePage={setPage}
110
+ />
111
+ </Div>
112
+ )
113
+ ```
114
+
115
+ ## Compact view
116
+
117
+ To display the component in a compact view pass `variant='compact'`.
118
+
119
+ ```jsx example
120
+ const [page, setPage] = useState(0)
121
+ return (
122
+ <Div row align='center'>
123
+ <Pagination
124
+ variant='compact'
125
+ page={page}
126
+ pages={10}
127
+ onChangePage={setPage}
128
+ />
129
+ </Div>
130
+ )
131
+ ```
132
+
133
+ ## Visible pages
134
+
135
+ You can control the visibility of pages when component has default view (`variant='full'`).
136
+
137
+ - `boundaryCount` controls the number of visible pages at the beginning and end
138
+ - `siblingCount` controls the number of visible pages before and after the current page
139
+
140
+ ```jsx example
141
+ const [page, setPage] = useState(5)
142
+ return (
143
+ <Div row align='center'>
144
+ <Pagination
145
+ page={page}
146
+ boundaryCount={2}
147
+ siblingCount={0}
148
+ pages={10}
149
+ onChangePage={setPage}
150
+ />
151
+ </Div>
152
+ )
153
+ ```
154
+
155
+ ## Sandbox
156
+
157
+ <Sandbox
158
+ Component={Pagination}
159
+ propsJsonSchema={PaginationPropsJsonSchema}
160
+ props={{
161
+ page: $.session.Props.Pagination.page.get(),
162
+ onChangePage: page => $.session.Props.Pagination.page.set(page)
163
+ }}
164
+ $props={$.session.Props.Pagination}
165
+ />
@@ -0,0 +1,21 @@
1
+ .item
2
+ min-width 4u
3
+ height 4u
4
+ align-items center
5
+ justify-content center
6
+ padding-left 1u
7
+ padding-right @padding-left
8
+
9
+ .page
10
+ &.selected
11
+ color var(--color-text-primary)
12
+
13
+ .status
14
+ margin-left 1u
15
+ margin-right @margin-left
16
+
17
+ .icon
18
+ color var(--color-text-secondary)
19
+
20
+ &.disabled
21
+ color var(--color-text-placeholder)
package/index.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ /* eslint-disable */
2
+ // DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
3
+
4
+ import React from 'react';
5
+ import { type StyleProp, type ViewStyle } from 'react-native';
6
+ import './index.cssx.styl';
7
+ declare const _default: React.ComponentType<PaginationProps>;
8
+ export default _default;
9
+ export declare const _PropsJsonSchema: {};
10
+ export interface PaginationProps {
11
+ /** Custom styles applied to the root container */
12
+ style?: StyleProp<ViewStyle>;
13
+ /** Display variant controlling layout @default 'full' */
14
+ variant?: 'full' | 'compact';
15
+ /** Zero-based page index */
16
+ page?: number;
17
+ /** Scoped model for page index */
18
+ $page?: any;
19
+ /** Total number of pages */
20
+ pages?: number;
21
+ /** Number of items to skip before current page @default 0 */
22
+ skip?: number;
23
+ /** Scoped model for skip value */
24
+ $skip?: any;
25
+ /** Number of items per page @default 1 */
26
+ limit?: number;
27
+ /** Scoped model for limit value */
28
+ $limit?: any;
29
+ /** Total number of items @default 0 */
30
+ count?: number;
31
+ /** Visible pages at the start and end @default 1 */
32
+ boundaryCount?: number;
33
+ /** Visible sibling pages around the current page @default 1 */
34
+ siblingCount?: number;
35
+ /** Show button for the first page @default false */
36
+ showFirstButton?: boolean;
37
+ /** Show button for the last page @default false */
38
+ showLastButton?: boolean;
39
+ /** Show previous page button @default true */
40
+ showPrevButton?: boolean;
41
+ /** Show next page button @default true */
42
+ showNextButton?: boolean;
43
+ /** Disable all navigation @default false */
44
+ disabled?: boolean;
45
+ /** Called when the page changes */
46
+ onChangePage?: (page: number) => void;
47
+ /** Called when the page size changes */
48
+ onChangeLimit?: (limit: number) => void;
49
+ }
package/index.tsx ADDED
@@ -0,0 +1,140 @@
1
+ import React, { type ReactNode } from 'react'
2
+ import { type StyleProp, type ViewStyle } from 'react-native'
3
+ import { pug, observer } from 'startupjs'
4
+ import { themed } from '@startupjs-ui/core'
5
+ import Div from '@startupjs-ui/div'
6
+ import Icon from '@startupjs-ui/icon'
7
+ import Span from '@startupjs-ui/span'
8
+ import { faAngleLeft } from '@fortawesome/free-solid-svg-icons/faAngleLeft'
9
+ import { faAngleDoubleLeft } from '@fortawesome/free-solid-svg-icons/faAngleDoubleLeft'
10
+ import { faAngleRight } from '@fortawesome/free-solid-svg-icons/faAngleRight'
11
+ import { faAngleDoubleRight } from '@fortawesome/free-solid-svg-icons/faAngleDoubleRight'
12
+ import usePagination from './usePagination'
13
+ import './index.cssx.styl'
14
+
15
+ const ICONS = {
16
+ first: faAngleDoubleLeft,
17
+ last: faAngleDoubleRight,
18
+ previous: faAngleLeft,
19
+ next: faAngleRight
20
+ }
21
+
22
+ export default observer(themed('Pagination', Pagination))
23
+
24
+ export const _PropsJsonSchema = {/* PaginationProps */} // used in docs generation
25
+
26
+ export interface PaginationProps {
27
+ /** Custom styles applied to the root container */
28
+ style?: StyleProp<ViewStyle>
29
+ /** Display variant controlling layout @default 'full' */
30
+ variant?: 'full' | 'compact'
31
+ /** Zero-based page index */
32
+ page?: number
33
+ /** Scoped model for page index */
34
+ $page?: any
35
+ /** Total number of pages */
36
+ pages?: number
37
+ /** Number of items to skip before current page @default 0 */
38
+ skip?: number
39
+ /** Scoped model for skip value */
40
+ $skip?: any
41
+ /** Number of items per page @default 1 */
42
+ limit?: number
43
+ /** Scoped model for limit value */
44
+ $limit?: any
45
+ /** Total number of items @default 0 */
46
+ count?: number
47
+ /** Visible pages at the start and end @default 1 */
48
+ boundaryCount?: number
49
+ /** Visible sibling pages around the current page @default 1 */
50
+ siblingCount?: number
51
+ /** Show button for the first page @default false */
52
+ showFirstButton?: boolean
53
+ /** Show button for the last page @default false */
54
+ showLastButton?: boolean
55
+ /** Show previous page button @default true */
56
+ showPrevButton?: boolean
57
+ /** Show next page button @default true */
58
+ showNextButton?: boolean
59
+ /** Disable all navigation @default false */
60
+ disabled?: boolean
61
+ /** Called when the page changes */
62
+ onChangePage?: (page: number) => void
63
+ /** Called when the page size changes */
64
+ onChangeLimit?: (limit: number) => void
65
+ }
66
+
67
+ function Pagination ({
68
+ style,
69
+ variant = 'full',
70
+ page,
71
+ $page,
72
+ pages,
73
+ skip = 0,
74
+ $skip,
75
+ limit = 1,
76
+ $limit,
77
+ count = 0,
78
+ boundaryCount = 1, // min 1
79
+ siblingCount = 1, // min 0
80
+ showFirstButton = false,
81
+ showLastButton = false,
82
+ showPrevButton = true,
83
+ showNextButton = true,
84
+ disabled = false,
85
+ onChangePage,
86
+ onChangeLimit
87
+ }: PaginationProps): ReactNode {
88
+ const items = usePagination({
89
+ variant,
90
+ page,
91
+ $page,
92
+ pages,
93
+ skip,
94
+ $skip,
95
+ limit,
96
+ $limit,
97
+ count,
98
+ boundaryCount,
99
+ siblingCount,
100
+ showFirstButton,
101
+ showLastButton,
102
+ showPrevButton,
103
+ showNextButton,
104
+ disabled,
105
+ onChangePage,
106
+ onChangeLimit
107
+ })
108
+
109
+ return pug`
110
+ Div(row style=style)
111
+ each item, index in items
112
+ React.Fragment(key=index)
113
+ - const { type, value, selected, disabled, ...itemProps } = item
114
+ if type === 'page'
115
+ Div.item(
116
+ variant='highlight'
117
+ shape='circle'
118
+ disabled=disabled
119
+ ...itemProps
120
+ )
121
+ Span.page(styleName={ selected })= value + 1
122
+ else if ['first', 'last', 'previous', 'next'].includes(type)
123
+ Div.item(
124
+ variant='highlight'
125
+ shape='circle'
126
+ disabled=disabled
127
+ ...itemProps
128
+ )
129
+ Icon.icon(
130
+ styleName={disabled}
131
+ icon=ICONS[type]
132
+ )
133
+ else if ~type.indexOf('ellipsis')
134
+ Div.item
135
+ Span ...
136
+ else if type === 'status'
137
+ Div.status(vAlign='center' row)
138
+ Span= value
139
+ `
140
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@startupjs-ui/pagination",
3
+ "version": "0.1.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "main": "index.tsx",
8
+ "types": "index.d.ts",
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@startupjs-ui/core": "^0.1.3",
12
+ "@startupjs-ui/div": "^0.1.3",
13
+ "@startupjs-ui/icon": "^0.1.3",
14
+ "@startupjs-ui/span": "^0.1.3"
15
+ },
16
+ "peerDependencies": {
17
+ "react": "*",
18
+ "react-native": "*",
19
+ "startupjs": "*"
20
+ },
21
+ "gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
22
+ }
@@ -0,0 +1,212 @@
1
+ import { useBind } from 'startupjs'
2
+
3
+ export interface UsePaginationProps {
4
+ variant?: 'full' | 'compact'
5
+ page?: number
6
+ $page?: any
7
+ pages?: number
8
+ skip?: number
9
+ $skip?: any
10
+ limit?: number
11
+ $limit?: any
12
+ count?: number
13
+ boundaryCount?: number
14
+ siblingCount?: number
15
+ showFirstButton?: boolean
16
+ showLastButton?: boolean
17
+ showPrevButton?: boolean
18
+ showNextButton?: boolean
19
+ disabled?: boolean
20
+ onChangePage?: (page: number) => void
21
+ onChangeLimit?: (limit: number) => void
22
+ }
23
+
24
+ type PaginationItemType =
25
+ | 'page'
26
+ | 'first'
27
+ | 'last'
28
+ | 'previous'
29
+ | 'next'
30
+ | 'status'
31
+ | 'start-ellipsis'
32
+ | 'end-ellipsis'
33
+
34
+ interface PaginationItem {
35
+ onPress?: () => void
36
+ type: PaginationItemType
37
+ value: number | string | null
38
+ selected: boolean
39
+ disabled: boolean
40
+ }
41
+
42
+ export default function usePagination ({
43
+ variant,
44
+ page,
45
+ $page,
46
+ pages,
47
+ skip,
48
+ $skip,
49
+ limit,
50
+ $limit,
51
+ count,
52
+ boundaryCount = 1, // min 1
53
+ siblingCount = 1, // min 0
54
+ showFirstButton = false,
55
+ showLastButton = false,
56
+ showPrevButton = true,
57
+ showNextButton = true,
58
+ disabled,
59
+ onChangePage,
60
+ onChangeLimit
61
+ }: UsePaginationProps): PaginationItem[] {
62
+ ({ page, onChangePage } = useBind({ $page, page, onChangePage }) as any)
63
+ ;({ skip } = useBind({ $skip, skip }) as any)
64
+ // TODO: Add selectbox to component to change limit
65
+ ;({ limit, onChangeLimit } = useBind({ $limit, limit, onChangeLimit }) as any)
66
+
67
+ if (page == null) {
68
+ if (skip != null && limit != null) {
69
+ page = Math.ceil(skip / limit)
70
+ } else {
71
+ throw new Error(
72
+ '[@startupjs/ui] usePagination: page cannot be calculated'
73
+ )
74
+ }
75
+ }
76
+
77
+ if (pages == null) {
78
+ if (count != null && limit != null) {
79
+ pages = Math.ceil(count / limit)
80
+ } else {
81
+ throw new Error(
82
+ '[@startupjs/ui] usePagination: pages cannot be calculated'
83
+ )
84
+ }
85
+ }
86
+
87
+ // min 1
88
+ const pagesCount = pages != null && pages > 0 ? pages : 1
89
+ const currentPage = page
90
+
91
+ // Basic list of items to render
92
+ const itemList: Array<PaginationItemType | number> = []
93
+
94
+ if (showFirstButton) itemList.push('first')
95
+ if (showPrevButton) itemList.push('previous')
96
+
97
+ if (variant === 'compact') {
98
+ itemList.push('status')
99
+ } else {
100
+ // this logic was taken from here
101
+ // https://github.com/mui-org/material-ui/blob/master/packages/material-ui-lab/src/Pagination/usePagination.js
102
+ const range = (start: number, end: number) => {
103
+ const length = end - start + 1
104
+ return Array.from({ length }, (_, i) => start + i)
105
+ }
106
+
107
+ const startPages = range(0, Math.min(boundaryCount, pagesCount) - 1)
108
+ const endPages = range(Math.max(pagesCount - boundaryCount, boundaryCount), pagesCount - 1)
109
+
110
+ const siblingsStart = Math.max(
111
+ Math.min(
112
+ // Natural start
113
+ currentPage - siblingCount,
114
+ // Lower boundary when page is high
115
+ pagesCount - boundaryCount - siblingCount * 2 - 2
116
+ ),
117
+ // Greater than startPages
118
+ boundaryCount + 1
119
+ )
120
+
121
+ const firstEndPage = endPages[0] ?? Number.NaN
122
+
123
+ const siblingsEnd = Math.min(
124
+ Math.max(
125
+ // Natural end
126
+ currentPage + siblingCount,
127
+ // Upper boundary when page is low
128
+ boundaryCount + siblingCount * 2 + 1
129
+ ),
130
+ // Less than endPages
131
+ firstEndPage - 2
132
+ )
133
+
134
+ itemList.push(...startPages)
135
+
136
+ // Start ellipsis
137
+ if (siblingsStart > boundaryCount + 1) {
138
+ itemList.push('start-ellipsis')
139
+ } else if (boundaryCount + 1 < pagesCount - boundaryCount) {
140
+ itemList.push(boundaryCount)
141
+ }
142
+
143
+ // Sibling pages
144
+ itemList.push(...range(siblingsStart, siblingsEnd))
145
+
146
+ // End ellipsis
147
+ if (siblingsEnd < pagesCount - boundaryCount - 2) {
148
+ itemList.push('end-ellipsis')
149
+ } else if (pagesCount - boundaryCount > boundaryCount) {
150
+ itemList.push(pagesCount - boundaryCount - 1)
151
+ }
152
+
153
+ itemList.push(...endPages)
154
+ }
155
+
156
+ if (showNextButton) itemList.push('next')
157
+ if (showLastButton) itemList.push('last')
158
+
159
+ // Map the button type to its page number
160
+ const buttonPage = (type: PaginationItemType): number | null => {
161
+ switch (type) {
162
+ case 'first':
163
+ return 0
164
+ case 'previous':
165
+ return currentPage - 1
166
+ case 'next':
167
+ return currentPage + 1
168
+ case 'last':
169
+ return pagesCount - 1
170
+ default:
171
+ return null
172
+ }
173
+ }
174
+
175
+ // Convert the basic item list to pagination item props objects
176
+ const items = itemList.map<PaginationItem>((item) => {
177
+ if (typeof item === 'number') {
178
+ return {
179
+ onPress: () => { onChangePage?.(item) },
180
+ type: 'page',
181
+ value: item,
182
+ selected: item === currentPage,
183
+ disabled: Boolean(disabled)
184
+ }
185
+ }
186
+
187
+ if (item === 'status') {
188
+ return {
189
+ type: item,
190
+ value: `${currentPage + 1} of ${pagesCount}`,
191
+ selected: false,
192
+ disabled: Boolean(disabled)
193
+ }
194
+ }
195
+
196
+ const value = buttonPage(item)
197
+ const isDisabled: boolean = disabled
198
+ ? true
199
+ : (!item.includes('ellipsis') &&
200
+ (item === 'next' || item === 'last' ? currentPage >= pagesCount - 1 : currentPage <= 0))
201
+
202
+ return {
203
+ onPress: () => { value !== null && onChangePage?.(value) },
204
+ type: item,
205
+ value,
206
+ selected: false,
207
+ disabled: isDisabled
208
+ }
209
+ })
210
+
211
+ return items
212
+ }