@anglefeint/astro-theme 0.2.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anglefeint/astro-theme",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "Anglefeint core theme package for Astro",
6
6
  "keywords": [
@@ -44,6 +44,9 @@
44
44
  "./styles/*": "./src/styles/*",
45
45
  "./assets/*": "./src/assets/*",
46
46
  "./utils/merge": "./src/utils/merge.ts",
47
+ "./utils/number": "./src/utils/number.ts",
48
+ "./utils/pagination": "./src/utils/pagination.ts",
49
+ "./utils/pagination-style": "./src/utils/pagination-style.ts",
47
50
  "./utils/*": "./src/utils/*",
48
51
  "./content-schema": "./src/content-schema.ts"
49
52
  },
@@ -0,0 +1,235 @@
1
+ ---
2
+ import type { PaginationModel } from '../../utils/pagination';
3
+ import {
4
+ resolvePaginationItemVariant,
5
+ resolvePaginationVariant,
6
+ } from '../../utils/pagination-style';
7
+
8
+ interface Props {
9
+ model: PaginationModel;
10
+ locale: string;
11
+ blogRoot: string;
12
+ pathname: string;
13
+ labels: {
14
+ previous: string;
15
+ next: string;
16
+ paginationAria: string;
17
+ jumpTo: string;
18
+ jumpInputLabel: string;
19
+ jumpGo: string;
20
+ };
21
+ config: {
22
+ jump: {
23
+ enabled: boolean;
24
+ enterToGo: boolean;
25
+ };
26
+ style: {
27
+ enabled: boolean;
28
+ mode: 'random' | 'sequential' | 'fixed';
29
+ variants: number;
30
+ fixedVariant: number;
31
+ };
32
+ };
33
+ }
34
+
35
+ const { model, locale, blogRoot, pathname, labels, config } = Astro.props as Props;
36
+
37
+ const pageHref = (pageNum: number) => (pageNum === 1 ? blogRoot : `${blogRoot}${pageNum}/`);
38
+ const styleEnabled = config.style.enabled;
39
+ const styleMode = config.style.mode;
40
+ const styleVariants = Math.max(1, Math.min(12, Math.floor(config.style.variants)));
41
+ const styleFixedVariant = Math.max(
42
+ 1,
43
+ Math.min(styleVariants, Math.floor(config.style.fixedVariant))
44
+ );
45
+ const showJump = config.jump.enabled && model.showJump;
46
+
47
+ const wrapVariant = resolvePaginationVariant({
48
+ currentPage: model.currentPage,
49
+ totalPages: model.totalPages,
50
+ locale,
51
+ pathname,
52
+ config: {
53
+ ENABLED: styleEnabled,
54
+ MODE: styleMode,
55
+ VARIANTS: styleVariants,
56
+ FIXED_VARIANT: styleFixedVariant,
57
+ },
58
+ });
59
+
60
+ const itemVariantClass = (seed: string, index: number) => {
61
+ if (!styleEnabled) return 'pg-var-1';
62
+ if (styleMode === 'random') return 'pg-var-random';
63
+ return resolvePaginationItemVariant({
64
+ seed,
65
+ index,
66
+ config: {
67
+ ENABLED: styleEnabled,
68
+ MODE: styleMode,
69
+ VARIANTS: styleVariants,
70
+ FIXED_VARIANT: styleFixedVariant,
71
+ },
72
+ }).className;
73
+ };
74
+ ---
75
+
76
+ {
77
+ model.totalPages > 1 && (
78
+ <div
79
+ class={`pagination-wrap ${wrapVariant.className}`}
80
+ data-pagination-component="cyber"
81
+ data-style-enabled={styleEnabled ? 'true' : 'false'}
82
+ data-style-mode={styleMode}
83
+ data-style-variants={String(styleVariants)}
84
+ data-style-fixed-variant={String(styleFixedVariant)}
85
+ >
86
+ <nav class="pagination" aria-label={labels.paginationAria}>
87
+ {model.currentPage > 1 && (
88
+ <a
89
+ class={`pg-item pg-nav ${itemVariantClass(`${locale}:prev:${model.currentPage}`, 0)}`}
90
+ href={pageHref(model.currentPage - 1)}
91
+ aria-label={labels.previous}
92
+ >
93
+ {labels.previous}
94
+ </a>
95
+ )}
96
+ {model.items.map((item, index) =>
97
+ item.kind === 'ellipsis' ? (
98
+ <span
99
+ class={`pg-item pg-ellipsis ${itemVariantClass(`${locale}:ellipsis:${item.id}`, index)}`}
100
+ aria-hidden="true"
101
+ >
102
+ ...
103
+ </span>
104
+ ) : (
105
+ <a
106
+ class={`pg-item ${itemVariantClass(`${locale}:page:${item.page}`, index)} ${item.page === model.currentPage ? 'current' : ''}`}
107
+ href={pageHref(item.page)}
108
+ aria-current={item.page === model.currentPage ? 'page' : undefined}
109
+ >
110
+ {item.page}
111
+ </a>
112
+ )
113
+ )}
114
+ {model.currentPage < model.totalPages && (
115
+ <a
116
+ class={`pg-item pg-nav ${itemVariantClass(`${locale}:next:${model.currentPage}`, model.items.length + 2)}`}
117
+ href={pageHref(model.currentPage + 1)}
118
+ aria-label={labels.next}
119
+ >
120
+ {labels.next}
121
+ </a>
122
+ )}
123
+ </nav>
124
+ {showJump && (
125
+ <form
126
+ class="pagination-jump"
127
+ data-blog-root={blogRoot}
128
+ data-total-pages={String(model.totalPages)}
129
+ data-enter-to-go={config.jump.enterToGo ? 'true' : 'false'}
130
+ aria-label={labels.jumpTo}
131
+ >
132
+ <label class="sr-only" for="pagination-jump-input">
133
+ {labels.jumpInputLabel}
134
+ </label>
135
+ <span class="pagination-jump-label">{labels.jumpTo}</span>
136
+ <input
137
+ id="pagination-jump-input"
138
+ class="pagination-jump-input"
139
+ type="text"
140
+ inputmode="numeric"
141
+ pattern="[0-9]*"
142
+ value={model.currentPage}
143
+ />
144
+ <button type="submit" class="pagination-jump-btn">
145
+ {labels.jumpGo}
146
+ </button>
147
+ </form>
148
+ )}
149
+ </div>
150
+ )
151
+ }
152
+
153
+ <script is:inline>
154
+ document.querySelectorAll('[data-pagination-component="cyber"]').forEach((wrap) => {
155
+ if (!(wrap instanceof HTMLElement)) return;
156
+ const toInt = (value, fallback) => {
157
+ const parsed = Number.parseInt(value ?? '', 10);
158
+ return Number.isFinite(parsed) ? parsed : fallback;
159
+ };
160
+ const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
161
+
162
+ const styleEnabled = wrap.dataset.styleEnabled === 'true';
163
+ const styleMode = wrap.dataset.styleMode || 'random';
164
+ const variants = clamp(toInt(wrap.dataset.styleVariants, 9), 1, 12);
165
+ const fixedVariant = clamp(toInt(wrap.dataset.styleFixedVariant, 1), 1, variants);
166
+
167
+ const items = wrap.querySelectorAll('.pagination .pg-item');
168
+
169
+ if (!styleEnabled) {
170
+ items.forEach((item) => {
171
+ item.className = item.className
172
+ .replace(/\bpg-var-\d+\b/g, '')
173
+ .replace(/\bpg-var-random\b/g, '')
174
+ .trim();
175
+ item.classList.add('pg-var-1');
176
+ });
177
+ } else if (styleMode === 'random') {
178
+ items.forEach((item) => {
179
+ item.className = item.className
180
+ .replace(/\bpg-var-\d+\b/g, '')
181
+ .replace(/\bpg-var-random\b/g, '')
182
+ .trim();
183
+ const variant = Math.floor(Math.random() * variants) + 1;
184
+ item.classList.add(`pg-var-${variant}`);
185
+ });
186
+ } else if (styleMode === 'fixed') {
187
+ items.forEach((item) => {
188
+ item.className = item.className
189
+ .replace(/\bpg-var-\d+\b/g, '')
190
+ .replace(/\bpg-var-random\b/g, '')
191
+ .trim();
192
+ item.classList.add(`pg-var-${fixedVariant}`);
193
+ });
194
+ }
195
+
196
+ const form = wrap.querySelector('.pagination-jump');
197
+ if (!(form instanceof HTMLFormElement)) return;
198
+
199
+ const blogRoot = form.getAttribute('data-blog-root') || '/blog/';
200
+ const totalPages = Math.max(1, parseInt(form.getAttribute('data-total-pages') || '1', 10));
201
+ const enterToGo = form.getAttribute('data-enter-to-go') !== 'false';
202
+ const input = form.querySelector('.pagination-jump-input');
203
+ if (!(input instanceof HTMLInputElement)) return;
204
+
205
+ const normalizeRoot = (root) => {
206
+ if (!root) return '/blog/';
207
+ return root.endsWith('/') ? root : `${root}/`;
208
+ };
209
+
210
+ const toHref = (pageNum) => {
211
+ const root = normalizeRoot(blogRoot);
212
+ return pageNum === 1 ? root : `${root}${pageNum}/`;
213
+ };
214
+
215
+ const runJump = () => {
216
+ const normalized = (input.value || '').replace(/[^\d]/g, '');
217
+ const value = Number.parseInt(normalized || '1', 10);
218
+ if (!Number.isFinite(value)) return;
219
+ const target = Math.max(1, Math.min(totalPages, Math.round(value)));
220
+ window.location.assign(toHref(target));
221
+ };
222
+
223
+ form.addEventListener('submit', (event) => {
224
+ event.preventDefault();
225
+ runJump();
226
+ });
227
+
228
+ if (!enterToGo) {
229
+ input.addEventListener('keydown', (event) => {
230
+ if (event.key !== 'Enter') return;
231
+ event.preventDefault();
232
+ });
233
+ }
234
+ });
235
+ </script>
@@ -0,0 +1,8 @@
1
+ export function clamp(value: number, min: number, max: number): number {
2
+ return Math.max(min, Math.min(max, value));
3
+ }
4
+
5
+ export function int(value: number): number {
6
+ if (!Number.isFinite(value)) return 0;
7
+ return Math.floor(value);
8
+ }
@@ -0,0 +1,81 @@
1
+ import { clamp, int } from './number';
2
+
3
+ type PaginationStyleMode = 'random' | 'sequential' | 'fixed';
4
+
5
+ interface PaginationStyleConfig {
6
+ ENABLED: boolean;
7
+ MODE: PaginationStyleMode;
8
+ VARIANTS: number;
9
+ FIXED_VARIANT: number;
10
+ }
11
+
12
+ interface ResolvePaginationVariantOptions {
13
+ currentPage: number;
14
+ totalPages: number;
15
+ locale: string;
16
+ pathname: string;
17
+ config: PaginationStyleConfig;
18
+ }
19
+
20
+ function hashString(value: string): number {
21
+ let hash = 5381;
22
+ for (let i = 0; i < value.length; i += 1) {
23
+ hash = (hash * 33) ^ value.charCodeAt(i);
24
+ }
25
+ return Math.abs(hash) >>> 0;
26
+ }
27
+
28
+ export function resolvePaginationVariant(options: ResolvePaginationVariantOptions): {
29
+ className: string;
30
+ variant: number;
31
+ } {
32
+ const variants = clamp(int(options.config.VARIANTS), 1, 12);
33
+ if (!options.config.ENABLED) {
34
+ return { className: 'cyber-pg-v1', variant: 1 };
35
+ }
36
+
37
+ const mode = options.config.MODE;
38
+ let variant = 1;
39
+
40
+ if (mode === 'fixed') {
41
+ variant = clamp(int(options.config.FIXED_VARIANT), 1, variants);
42
+ } else if (mode === 'sequential') {
43
+ variant = ((Math.max(1, int(options.currentPage)) - 1) % variants) + 1;
44
+ } else {
45
+ const seedKey = `${options.locale}:${options.pathname}:${options.currentPage}:${options.totalPages}`;
46
+ variant = (hashString(seedKey) % variants) + 1;
47
+ }
48
+
49
+ return {
50
+ className: `cyber-pg-v${variant}`,
51
+ variant,
52
+ };
53
+ }
54
+
55
+ interface ResolvePaginationItemVariantOptions {
56
+ seed: string;
57
+ index: number;
58
+ config: PaginationStyleConfig;
59
+ }
60
+
61
+ export function resolvePaginationItemVariant(options: ResolvePaginationItemVariantOptions): {
62
+ className: string;
63
+ variant: number;
64
+ } {
65
+ const variants = clamp(int(options.config.VARIANTS), 1, 12);
66
+ if (!options.config.ENABLED) {
67
+ return { className: 'pg-var-1', variant: 1 };
68
+ }
69
+
70
+ const mode = options.config.MODE;
71
+ let variant = 1;
72
+ if (mode === 'fixed') {
73
+ variant = clamp(int(options.config.FIXED_VARIANT), 1, variants);
74
+ } else if (mode === 'sequential') {
75
+ variant = (int(options.index) % variants) + 1;
76
+ } else {
77
+ variant = (hashString(options.seed) % variants) + 1;
78
+ }
79
+
80
+ return { className: `pg-var-${variant}`, variant };
81
+ }
@@ -0,0 +1,67 @@
1
+ import { clamp, int } from './number';
2
+
3
+ export type PaginationItem = { kind: 'page'; page: number } | { kind: 'ellipsis'; id: string };
4
+
5
+ export interface PaginationModel {
6
+ currentPage: number;
7
+ totalPages: number;
8
+ showJump: boolean;
9
+ items: PaginationItem[];
10
+ }
11
+
12
+ interface BuildPaginationModelOptions {
13
+ currentPage: number;
14
+ totalPages: number;
15
+ windowSize?: number;
16
+ showJumpThreshold?: number;
17
+ }
18
+
19
+ function addRange(set: Set<number>, start: number, end: number, totalPages: number) {
20
+ const s = clamp(int(start), 1, totalPages);
21
+ const e = clamp(int(end), 1, totalPages);
22
+ for (let page = s; page <= e; page += 1) set.add(page);
23
+ }
24
+
25
+ export function buildPaginationModel(options: BuildPaginationModelOptions): PaginationModel {
26
+ const totalPages = Math.max(1, int(options.totalPages));
27
+ const currentPage = clamp(int(options.currentPage), 1, totalPages);
28
+ const windowSize = clamp(int(options.windowSize ?? 7), 5, 21);
29
+ const showJumpThreshold = Math.max(1, int(options.showJumpThreshold ?? 12));
30
+ const showJump = totalPages > showJumpThreshold;
31
+
32
+ if (totalPages <= windowSize) {
33
+ return {
34
+ currentPage,
35
+ totalPages,
36
+ showJump,
37
+ items: Array.from({ length: totalPages }, (_, i) => ({ kind: 'page', page: i + 1 })),
38
+ };
39
+ }
40
+
41
+ const boundaryCount = 1;
42
+ const siblingCount = Math.max(1, Math.floor((windowSize - (boundaryCount * 2 + 3)) / 2));
43
+ const nearEdgeSlots = boundaryCount + siblingCount * 2 + 2;
44
+ const picked = new Set<number>();
45
+
46
+ addRange(picked, 1, boundaryCount, totalPages);
47
+ addRange(picked, totalPages - boundaryCount + 1, totalPages, totalPages);
48
+ addRange(picked, currentPage - siblingCount, currentPage + siblingCount, totalPages);
49
+
50
+ if (currentPage <= nearEdgeSlots) addRange(picked, 1, nearEdgeSlots + 1, totalPages);
51
+ if (currentPage >= totalPages - nearEdgeSlots + 1) {
52
+ addRange(picked, totalPages - nearEdgeSlots, totalPages, totalPages);
53
+ }
54
+
55
+ const pages = [...picked].sort((a, b) => a - b);
56
+ const items: PaginationItem[] = [];
57
+ for (let i = 0; i < pages.length; i += 1) {
58
+ const page = pages[i];
59
+ const prev = pages[i - 1];
60
+ if (typeof prev === 'number' && page - prev > 1) {
61
+ items.push({ kind: 'ellipsis', id: `ellipsis-${prev}-${page}` });
62
+ }
63
+ items.push({ kind: 'page', page });
64
+ }
65
+
66
+ return { currentPage, totalPages, showJump, items };
67
+ }