@boba-cli/paginator 0.1.0-alpha.1

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/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @boba-cli/paginator
2
+
3
+ Pagination state management and rendering for Boba terminal UIs. Ported from the Charm `bubbles/paginator` component.
4
+
5
+ <img src="../../examples/paginator-demo.gif" width="950" alt="Paginator component demo" />
6
+
7
+ ## Usage
8
+
9
+ ```ts
10
+ import { PaginatorModel, PaginatorType } from '@boba-cli/paginator'
11
+
12
+ const paginator = PaginatorModel.new({
13
+ type: PaginatorType.Dots,
14
+ perPage: 10,
15
+ })
16
+
17
+ const next = paginator.setTotalPages(items.length)
18
+ const [start, end] = next.getSliceBounds(items.length)
19
+ const visibleItems = items.slice(start, end)
20
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+
3
+ var key = require('@boba-cli/key');
4
+ var tea = require('@boba-cli/tea');
5
+
6
+ // src/model.ts
7
+ var PaginatorType = /* @__PURE__ */ ((PaginatorType2) => {
8
+ PaginatorType2["Arabic"] = "arabic";
9
+ PaginatorType2["Dots"] = "dots";
10
+ return PaginatorType2;
11
+ })(PaginatorType || {});
12
+ var defaultKeyMap = {
13
+ prevPage: key.newBinding({ keys: ["pgup", "left", "h"] }),
14
+ nextPage: key.newBinding({ keys: ["pgdown", "right", "l"] })
15
+ };
16
+
17
+ // src/model.ts
18
+ var PaginatorModel = class _PaginatorModel {
19
+ type;
20
+ page;
21
+ perPage;
22
+ totalPages;
23
+ activeDot;
24
+ inactiveDot;
25
+ arabicFormat;
26
+ keyMap;
27
+ constructor(options = {}) {
28
+ this.type = options.type ?? "arabic" /* Arabic */;
29
+ this.page = Math.max(0, options.page ?? 0);
30
+ this.perPage = Math.max(1, options.perPage ?? 1);
31
+ this.totalPages = Math.max(1, options.totalPages ?? 1);
32
+ this.activeDot = options.activeDot ?? "\u2022";
33
+ this.inactiveDot = options.inactiveDot ?? "\u25CB";
34
+ this.arabicFormat = options.arabicFormat ?? "%d/%d";
35
+ this.keyMap = options.keyMap ?? defaultKeyMap;
36
+ }
37
+ /** Create a new paginator with defaults. */
38
+ static new(options = {}) {
39
+ return new _PaginatorModel(options);
40
+ }
41
+ /** Set total pages based on item count (rounded up). */
42
+ setTotalPages(items) {
43
+ if (items < 1) {
44
+ return this;
45
+ }
46
+ const totalPages = Math.max(1, Math.ceil(items / this.perPage));
47
+ return this.with({ totalPages });
48
+ }
49
+ /** Number of items on the current page for a given total. */
50
+ itemsOnPage(totalItems) {
51
+ if (totalItems < 1) {
52
+ return 0;
53
+ }
54
+ const [start, end] = this.getSliceBounds(totalItems);
55
+ return end - start;
56
+ }
57
+ /** Slice bounds for the current page, clamped to length. */
58
+ getSliceBounds(length) {
59
+ const start = this.page * this.perPage;
60
+ const end = Math.min(start + this.perPage, length);
61
+ return [start, end];
62
+ }
63
+ /** Move to previous page (no-op on first page). */
64
+ prevPage() {
65
+ if (this.page <= 0) {
66
+ return this;
67
+ }
68
+ return this.with({ page: this.page - 1 });
69
+ }
70
+ /** Move to next page (no-op on last page). */
71
+ nextPage() {
72
+ if (this.onLastPage()) {
73
+ return this;
74
+ }
75
+ return this.with({ page: this.page + 1 });
76
+ }
77
+ /** Whether the current page is the first page. */
78
+ onFirstPage() {
79
+ return this.page === 0;
80
+ }
81
+ /** Whether the current page is the last page. */
82
+ onLastPage() {
83
+ return this.page >= this.totalPages - 1;
84
+ }
85
+ /** Handle Tea messages (responds to KeyMsg). */
86
+ update(msg) {
87
+ if (!(msg instanceof tea.KeyMsg)) {
88
+ return [this, null];
89
+ }
90
+ if (key.matches(msg, this.keyMap.nextPage)) {
91
+ return [this.nextPage(), null];
92
+ }
93
+ if (key.matches(msg, this.keyMap.prevPage)) {
94
+ return [this.prevPage(), null];
95
+ }
96
+ return [this, null];
97
+ }
98
+ /** Render pagination. */
99
+ view() {
100
+ switch (this.type) {
101
+ case "dots" /* Dots */:
102
+ return this.dotsView();
103
+ case "arabic" /* Arabic */:
104
+ default:
105
+ return this.arabicView();
106
+ }
107
+ }
108
+ dotsView() {
109
+ let s = "";
110
+ for (let i = 0; i < this.totalPages; i++) {
111
+ s += i === this.page ? this.activeDot : this.inactiveDot;
112
+ }
113
+ return s;
114
+ }
115
+ arabicView() {
116
+ const current = this.page + 1;
117
+ return this.arabicFormat.replace("%d", String(current)).replace("%d", String(this.totalPages));
118
+ }
119
+ with(patch) {
120
+ return new _PaginatorModel({
121
+ type: this.type,
122
+ page: this.page,
123
+ perPage: this.perPage,
124
+ totalPages: this.totalPages,
125
+ activeDot: this.activeDot,
126
+ inactiveDot: this.inactiveDot,
127
+ arabicFormat: this.arabicFormat,
128
+ keyMap: this.keyMap,
129
+ ...patch
130
+ });
131
+ }
132
+ };
133
+
134
+ exports.PaginatorModel = PaginatorModel;
135
+ exports.PaginatorType = PaginatorType;
136
+ exports.defaultKeyMap = defaultKeyMap;
137
+ //# sourceMappingURL=index.cjs.map
138
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/model.ts"],"names":["PaginatorType","newBinding","KeyMsg","matches"],"mappings":"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","file":"index.cjs","sourcesContent":["import { Binding, newBinding } from '@boba-cli/key'\n\n/**\n * Pagination display style.\n * @public\n */\nexport enum PaginatorType {\n Arabic = 'arabic',\n Dots = 'dots',\n}\n\n/**\n * Key bindings for paginator navigation.\n * @public\n */\nexport interface KeyMap {\n prevPage: Binding\n nextPage: Binding\n}\n\n/**\n * Default paginator key bindings.\n * @public\n */\nexport const defaultKeyMap: KeyMap = {\n prevPage: newBinding({ keys: ['pgup', 'left', 'h'] }),\n nextPage: newBinding({ keys: ['pgdown', 'right', 'l'] }),\n}\n","import { matches } from '@boba-cli/key'\nimport { KeyMsg, type Cmd, type Msg } from '@boba-cli/tea'\nimport { defaultKeyMap, PaginatorType, type KeyMap } from './types.js'\n\n/**\n * Options for creating a paginator.\n * @public\n */\nexport interface PaginatorOptions {\n type?: PaginatorType\n page?: number\n perPage?: number\n totalPages?: number\n activeDot?: string\n inactiveDot?: string\n arabicFormat?: string\n keyMap?: KeyMap\n}\n\n/**\n * Pagination state and rendering.\n * @public\n */\nexport class PaginatorModel {\n readonly type: PaginatorType\n readonly page: number\n readonly perPage: number\n readonly totalPages: number\n readonly activeDot: string\n readonly inactiveDot: string\n readonly arabicFormat: string\n readonly keyMap: KeyMap\n\n private constructor(options: PaginatorOptions = {}) {\n this.type = options.type ?? PaginatorType.Arabic\n this.page = Math.max(0, options.page ?? 0)\n this.perPage = Math.max(1, options.perPage ?? 1)\n this.totalPages = Math.max(1, options.totalPages ?? 1)\n this.activeDot = options.activeDot ?? '•'\n this.inactiveDot = options.inactiveDot ?? '○'\n this.arabicFormat = options.arabicFormat ?? '%d/%d'\n this.keyMap = options.keyMap ?? defaultKeyMap\n }\n\n /** Create a new paginator with defaults. */\n static new(options: PaginatorOptions = {}): PaginatorModel {\n return new PaginatorModel(options)\n }\n\n /** Set total pages based on item count (rounded up). */\n setTotalPages(items: number): PaginatorModel {\n if (items < 1) {\n return this\n }\n const totalPages = Math.max(1, Math.ceil(items / this.perPage))\n return this.with({ totalPages })\n }\n\n /** Number of items on the current page for a given total. */\n itemsOnPage(totalItems: number): number {\n if (totalItems < 1) {\n return 0\n }\n const [start, end] = this.getSliceBounds(totalItems)\n return end - start\n }\n\n /** Slice bounds for the current page, clamped to length. */\n getSliceBounds(length: number): [number, number] {\n const start = this.page * this.perPage\n const end = Math.min(start + this.perPage, length)\n return [start, end]\n }\n\n /** Move to previous page (no-op on first page). */\n prevPage(): PaginatorModel {\n if (this.page <= 0) {\n return this\n }\n return this.with({ page: this.page - 1 })\n }\n\n /** Move to next page (no-op on last page). */\n nextPage(): PaginatorModel {\n if (this.onLastPage()) {\n return this\n }\n return this.with({ page: this.page + 1 })\n }\n\n /** Whether the current page is the first page. */\n onFirstPage(): boolean {\n return this.page === 0\n }\n\n /** Whether the current page is the last page. */\n onLastPage(): boolean {\n return this.page >= this.totalPages - 1\n }\n\n /** Handle Tea messages (responds to KeyMsg). */\n update(msg: Msg): [PaginatorModel, Cmd<Msg>] {\n if (!(msg instanceof KeyMsg)) {\n return [this, null]\n }\n\n if (matches(msg, this.keyMap.nextPage)) {\n return [this.nextPage(), null]\n }\n if (matches(msg, this.keyMap.prevPage)) {\n return [this.prevPage(), null]\n }\n\n return [this, null]\n }\n\n /** Render pagination. */\n view(): string {\n switch (this.type) {\n case PaginatorType.Dots:\n return this.dotsView()\n case PaginatorType.Arabic:\n default:\n return this.arabicView()\n }\n }\n\n private dotsView(): string {\n let s = ''\n for (let i = 0; i < this.totalPages; i++) {\n s += i === this.page ? this.activeDot : this.inactiveDot\n }\n return s\n }\n\n private arabicView(): string {\n const current = this.page + 1\n return this.arabicFormat\n .replace('%d', String(current))\n .replace('%d', String(this.totalPages))\n }\n\n private with(patch: Partial<PaginatorOptions>): PaginatorModel {\n return new PaginatorModel({\n type: this.type,\n page: this.page,\n perPage: this.perPage,\n totalPages: this.totalPages,\n activeDot: this.activeDot,\n inactiveDot: this.inactiveDot,\n arabicFormat: this.arabicFormat,\n keyMap: this.keyMap,\n ...patch,\n })\n }\n}\n"]}
@@ -0,0 +1,79 @@
1
+ import { Msg, Cmd } from '@boba-cli/tea';
2
+ import { Binding } from '@boba-cli/key';
3
+
4
+ /**
5
+ * Pagination display style.
6
+ * @public
7
+ */
8
+ declare enum PaginatorType {
9
+ Arabic = "arabic",
10
+ Dots = "dots"
11
+ }
12
+ /**
13
+ * Key bindings for paginator navigation.
14
+ * @public
15
+ */
16
+ interface KeyMap {
17
+ prevPage: Binding;
18
+ nextPage: Binding;
19
+ }
20
+ /**
21
+ * Default paginator key bindings.
22
+ * @public
23
+ */
24
+ declare const defaultKeyMap: KeyMap;
25
+
26
+ /**
27
+ * Options for creating a paginator.
28
+ * @public
29
+ */
30
+ interface PaginatorOptions {
31
+ type?: PaginatorType;
32
+ page?: number;
33
+ perPage?: number;
34
+ totalPages?: number;
35
+ activeDot?: string;
36
+ inactiveDot?: string;
37
+ arabicFormat?: string;
38
+ keyMap?: KeyMap;
39
+ }
40
+ /**
41
+ * Pagination state and rendering.
42
+ * @public
43
+ */
44
+ declare class PaginatorModel {
45
+ readonly type: PaginatorType;
46
+ readonly page: number;
47
+ readonly perPage: number;
48
+ readonly totalPages: number;
49
+ readonly activeDot: string;
50
+ readonly inactiveDot: string;
51
+ readonly arabicFormat: string;
52
+ readonly keyMap: KeyMap;
53
+ private constructor();
54
+ /** Create a new paginator with defaults. */
55
+ static new(options?: PaginatorOptions): PaginatorModel;
56
+ /** Set total pages based on item count (rounded up). */
57
+ setTotalPages(items: number): PaginatorModel;
58
+ /** Number of items on the current page for a given total. */
59
+ itemsOnPage(totalItems: number): number;
60
+ /** Slice bounds for the current page, clamped to length. */
61
+ getSliceBounds(length: number): [number, number];
62
+ /** Move to previous page (no-op on first page). */
63
+ prevPage(): PaginatorModel;
64
+ /** Move to next page (no-op on last page). */
65
+ nextPage(): PaginatorModel;
66
+ /** Whether the current page is the first page. */
67
+ onFirstPage(): boolean;
68
+ /** Whether the current page is the last page. */
69
+ onLastPage(): boolean;
70
+ /** Handle Tea messages (responds to KeyMsg). */
71
+ update(msg: Msg): [PaginatorModel, Cmd<Msg>];
72
+ /** Render pagination. */
73
+ view(): string;
74
+ private dotsView;
75
+ private arabicView;
76
+ private with;
77
+ }
78
+
79
+ export { type KeyMap, PaginatorModel, type PaginatorOptions, PaginatorType, defaultKeyMap };
@@ -0,0 +1,79 @@
1
+ import { Msg, Cmd } from '@boba-cli/tea';
2
+ import { Binding } from '@boba-cli/key';
3
+
4
+ /**
5
+ * Pagination display style.
6
+ * @public
7
+ */
8
+ declare enum PaginatorType {
9
+ Arabic = "arabic",
10
+ Dots = "dots"
11
+ }
12
+ /**
13
+ * Key bindings for paginator navigation.
14
+ * @public
15
+ */
16
+ interface KeyMap {
17
+ prevPage: Binding;
18
+ nextPage: Binding;
19
+ }
20
+ /**
21
+ * Default paginator key bindings.
22
+ * @public
23
+ */
24
+ declare const defaultKeyMap: KeyMap;
25
+
26
+ /**
27
+ * Options for creating a paginator.
28
+ * @public
29
+ */
30
+ interface PaginatorOptions {
31
+ type?: PaginatorType;
32
+ page?: number;
33
+ perPage?: number;
34
+ totalPages?: number;
35
+ activeDot?: string;
36
+ inactiveDot?: string;
37
+ arabicFormat?: string;
38
+ keyMap?: KeyMap;
39
+ }
40
+ /**
41
+ * Pagination state and rendering.
42
+ * @public
43
+ */
44
+ declare class PaginatorModel {
45
+ readonly type: PaginatorType;
46
+ readonly page: number;
47
+ readonly perPage: number;
48
+ readonly totalPages: number;
49
+ readonly activeDot: string;
50
+ readonly inactiveDot: string;
51
+ readonly arabicFormat: string;
52
+ readonly keyMap: KeyMap;
53
+ private constructor();
54
+ /** Create a new paginator with defaults. */
55
+ static new(options?: PaginatorOptions): PaginatorModel;
56
+ /** Set total pages based on item count (rounded up). */
57
+ setTotalPages(items: number): PaginatorModel;
58
+ /** Number of items on the current page for a given total. */
59
+ itemsOnPage(totalItems: number): number;
60
+ /** Slice bounds for the current page, clamped to length. */
61
+ getSliceBounds(length: number): [number, number];
62
+ /** Move to previous page (no-op on first page). */
63
+ prevPage(): PaginatorModel;
64
+ /** Move to next page (no-op on last page). */
65
+ nextPage(): PaginatorModel;
66
+ /** Whether the current page is the first page. */
67
+ onFirstPage(): boolean;
68
+ /** Whether the current page is the last page. */
69
+ onLastPage(): boolean;
70
+ /** Handle Tea messages (responds to KeyMsg). */
71
+ update(msg: Msg): [PaginatorModel, Cmd<Msg>];
72
+ /** Render pagination. */
73
+ view(): string;
74
+ private dotsView;
75
+ private arabicView;
76
+ private with;
77
+ }
78
+
79
+ export { type KeyMap, PaginatorModel, type PaginatorOptions, PaginatorType, defaultKeyMap };
package/dist/index.js ADDED
@@ -0,0 +1,134 @@
1
+ import { newBinding, matches } from '@boba-cli/key';
2
+ import { KeyMsg } from '@boba-cli/tea';
3
+
4
+ // src/model.ts
5
+ var PaginatorType = /* @__PURE__ */ ((PaginatorType2) => {
6
+ PaginatorType2["Arabic"] = "arabic";
7
+ PaginatorType2["Dots"] = "dots";
8
+ return PaginatorType2;
9
+ })(PaginatorType || {});
10
+ var defaultKeyMap = {
11
+ prevPage: newBinding({ keys: ["pgup", "left", "h"] }),
12
+ nextPage: newBinding({ keys: ["pgdown", "right", "l"] })
13
+ };
14
+
15
+ // src/model.ts
16
+ var PaginatorModel = class _PaginatorModel {
17
+ type;
18
+ page;
19
+ perPage;
20
+ totalPages;
21
+ activeDot;
22
+ inactiveDot;
23
+ arabicFormat;
24
+ keyMap;
25
+ constructor(options = {}) {
26
+ this.type = options.type ?? "arabic" /* Arabic */;
27
+ this.page = Math.max(0, options.page ?? 0);
28
+ this.perPage = Math.max(1, options.perPage ?? 1);
29
+ this.totalPages = Math.max(1, options.totalPages ?? 1);
30
+ this.activeDot = options.activeDot ?? "\u2022";
31
+ this.inactiveDot = options.inactiveDot ?? "\u25CB";
32
+ this.arabicFormat = options.arabicFormat ?? "%d/%d";
33
+ this.keyMap = options.keyMap ?? defaultKeyMap;
34
+ }
35
+ /** Create a new paginator with defaults. */
36
+ static new(options = {}) {
37
+ return new _PaginatorModel(options);
38
+ }
39
+ /** Set total pages based on item count (rounded up). */
40
+ setTotalPages(items) {
41
+ if (items < 1) {
42
+ return this;
43
+ }
44
+ const totalPages = Math.max(1, Math.ceil(items / this.perPage));
45
+ return this.with({ totalPages });
46
+ }
47
+ /** Number of items on the current page for a given total. */
48
+ itemsOnPage(totalItems) {
49
+ if (totalItems < 1) {
50
+ return 0;
51
+ }
52
+ const [start, end] = this.getSliceBounds(totalItems);
53
+ return end - start;
54
+ }
55
+ /** Slice bounds for the current page, clamped to length. */
56
+ getSliceBounds(length) {
57
+ const start = this.page * this.perPage;
58
+ const end = Math.min(start + this.perPage, length);
59
+ return [start, end];
60
+ }
61
+ /** Move to previous page (no-op on first page). */
62
+ prevPage() {
63
+ if (this.page <= 0) {
64
+ return this;
65
+ }
66
+ return this.with({ page: this.page - 1 });
67
+ }
68
+ /** Move to next page (no-op on last page). */
69
+ nextPage() {
70
+ if (this.onLastPage()) {
71
+ return this;
72
+ }
73
+ return this.with({ page: this.page + 1 });
74
+ }
75
+ /** Whether the current page is the first page. */
76
+ onFirstPage() {
77
+ return this.page === 0;
78
+ }
79
+ /** Whether the current page is the last page. */
80
+ onLastPage() {
81
+ return this.page >= this.totalPages - 1;
82
+ }
83
+ /** Handle Tea messages (responds to KeyMsg). */
84
+ update(msg) {
85
+ if (!(msg instanceof KeyMsg)) {
86
+ return [this, null];
87
+ }
88
+ if (matches(msg, this.keyMap.nextPage)) {
89
+ return [this.nextPage(), null];
90
+ }
91
+ if (matches(msg, this.keyMap.prevPage)) {
92
+ return [this.prevPage(), null];
93
+ }
94
+ return [this, null];
95
+ }
96
+ /** Render pagination. */
97
+ view() {
98
+ switch (this.type) {
99
+ case "dots" /* Dots */:
100
+ return this.dotsView();
101
+ case "arabic" /* Arabic */:
102
+ default:
103
+ return this.arabicView();
104
+ }
105
+ }
106
+ dotsView() {
107
+ let s = "";
108
+ for (let i = 0; i < this.totalPages; i++) {
109
+ s += i === this.page ? this.activeDot : this.inactiveDot;
110
+ }
111
+ return s;
112
+ }
113
+ arabicView() {
114
+ const current = this.page + 1;
115
+ return this.arabicFormat.replace("%d", String(current)).replace("%d", String(this.totalPages));
116
+ }
117
+ with(patch) {
118
+ return new _PaginatorModel({
119
+ type: this.type,
120
+ page: this.page,
121
+ perPage: this.perPage,
122
+ totalPages: this.totalPages,
123
+ activeDot: this.activeDot,
124
+ inactiveDot: this.inactiveDot,
125
+ arabicFormat: this.arabicFormat,
126
+ keyMap: this.keyMap,
127
+ ...patch
128
+ });
129
+ }
130
+ };
131
+
132
+ export { PaginatorModel, PaginatorType, defaultKeyMap };
133
+ //# sourceMappingURL=index.js.map
134
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/model.ts"],"names":["PaginatorType"],"mappings":"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","file":"index.js","sourcesContent":["import { Binding, newBinding } from '@boba-cli/key'\n\n/**\n * Pagination display style.\n * @public\n */\nexport enum PaginatorType {\n Arabic = 'arabic',\n Dots = 'dots',\n}\n\n/**\n * Key bindings for paginator navigation.\n * @public\n */\nexport interface KeyMap {\n prevPage: Binding\n nextPage: Binding\n}\n\n/**\n * Default paginator key bindings.\n * @public\n */\nexport const defaultKeyMap: KeyMap = {\n prevPage: newBinding({ keys: ['pgup', 'left', 'h'] }),\n nextPage: newBinding({ keys: ['pgdown', 'right', 'l'] }),\n}\n","import { matches } from '@boba-cli/key'\nimport { KeyMsg, type Cmd, type Msg } from '@boba-cli/tea'\nimport { defaultKeyMap, PaginatorType, type KeyMap } from './types.js'\n\n/**\n * Options for creating a paginator.\n * @public\n */\nexport interface PaginatorOptions {\n type?: PaginatorType\n page?: number\n perPage?: number\n totalPages?: number\n activeDot?: string\n inactiveDot?: string\n arabicFormat?: string\n keyMap?: KeyMap\n}\n\n/**\n * Pagination state and rendering.\n * @public\n */\nexport class PaginatorModel {\n readonly type: PaginatorType\n readonly page: number\n readonly perPage: number\n readonly totalPages: number\n readonly activeDot: string\n readonly inactiveDot: string\n readonly arabicFormat: string\n readonly keyMap: KeyMap\n\n private constructor(options: PaginatorOptions = {}) {\n this.type = options.type ?? PaginatorType.Arabic\n this.page = Math.max(0, options.page ?? 0)\n this.perPage = Math.max(1, options.perPage ?? 1)\n this.totalPages = Math.max(1, options.totalPages ?? 1)\n this.activeDot = options.activeDot ?? '•'\n this.inactiveDot = options.inactiveDot ?? '○'\n this.arabicFormat = options.arabicFormat ?? '%d/%d'\n this.keyMap = options.keyMap ?? defaultKeyMap\n }\n\n /** Create a new paginator with defaults. */\n static new(options: PaginatorOptions = {}): PaginatorModel {\n return new PaginatorModel(options)\n }\n\n /** Set total pages based on item count (rounded up). */\n setTotalPages(items: number): PaginatorModel {\n if (items < 1) {\n return this\n }\n const totalPages = Math.max(1, Math.ceil(items / this.perPage))\n return this.with({ totalPages })\n }\n\n /** Number of items on the current page for a given total. */\n itemsOnPage(totalItems: number): number {\n if (totalItems < 1) {\n return 0\n }\n const [start, end] = this.getSliceBounds(totalItems)\n return end - start\n }\n\n /** Slice bounds for the current page, clamped to length. */\n getSliceBounds(length: number): [number, number] {\n const start = this.page * this.perPage\n const end = Math.min(start + this.perPage, length)\n return [start, end]\n }\n\n /** Move to previous page (no-op on first page). */\n prevPage(): PaginatorModel {\n if (this.page <= 0) {\n return this\n }\n return this.with({ page: this.page - 1 })\n }\n\n /** Move to next page (no-op on last page). */\n nextPage(): PaginatorModel {\n if (this.onLastPage()) {\n return this\n }\n return this.with({ page: this.page + 1 })\n }\n\n /** Whether the current page is the first page. */\n onFirstPage(): boolean {\n return this.page === 0\n }\n\n /** Whether the current page is the last page. */\n onLastPage(): boolean {\n return this.page >= this.totalPages - 1\n }\n\n /** Handle Tea messages (responds to KeyMsg). */\n update(msg: Msg): [PaginatorModel, Cmd<Msg>] {\n if (!(msg instanceof KeyMsg)) {\n return [this, null]\n }\n\n if (matches(msg, this.keyMap.nextPage)) {\n return [this.nextPage(), null]\n }\n if (matches(msg, this.keyMap.prevPage)) {\n return [this.prevPage(), null]\n }\n\n return [this, null]\n }\n\n /** Render pagination. */\n view(): string {\n switch (this.type) {\n case PaginatorType.Dots:\n return this.dotsView()\n case PaginatorType.Arabic:\n default:\n return this.arabicView()\n }\n }\n\n private dotsView(): string {\n let s = ''\n for (let i = 0; i < this.totalPages; i++) {\n s += i === this.page ? this.activeDot : this.inactiveDot\n }\n return s\n }\n\n private arabicView(): string {\n const current = this.page + 1\n return this.arabicFormat\n .replace('%d', String(current))\n .replace('%d', String(this.totalPages))\n }\n\n private with(patch: Partial<PaginatorOptions>): PaginatorModel {\n return new PaginatorModel({\n type: this.type,\n page: this.page,\n perPage: this.perPage,\n totalPages: this.totalPages,\n activeDot: this.activeDot,\n inactiveDot: this.inactiveDot,\n arabicFormat: this.arabicFormat,\n keyMap: this.keyMap,\n ...patch,\n })\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@boba-cli/paginator",
3
+ "description": "Pagination state + rendering for Boba terminal UIs",
4
+ "version": "0.1.0-alpha.1",
5
+ "dependencies": {
6
+ "@boba-cli/key": "0.1.0-alpha.1",
7
+ "@boba-cli/tea": "0.1.0-alpha.1"
8
+ },
9
+ "devDependencies": {
10
+ "typescript": "5.8.2",
11
+ "vitest": "^4.0.16"
12
+ },
13
+ "engines": {
14
+ "node": ">=20.0.0"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "import": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "require": {
23
+ "types": "./dist/index.d.cts",
24
+ "default": "./dist/index.cjs"
25
+ }
26
+ },
27
+ "./package.json": "./package.json"
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "main": "./dist/index.cjs",
33
+ "module": "./dist/index.js",
34
+ "type": "module",
35
+ "types": "./dist/index.d.ts",
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "check:api-report": "pnpm run generate:api-report",
39
+ "check:eslint": "pnpm run lint",
40
+ "generate:api-report": "api-extractor run --local",
41
+ "lint": "eslint \"{src,test}/**/*.{ts,tsx}\"",
42
+ "test": "vitest run"
43
+ }
44
+ }