keystone-design-bootstrap 1.0.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/README.md +179 -0
- package/package.json +59 -0
- package/src/contexts/ThemeContext.tsx +34 -0
- package/src/contexts/index.ts +1 -0
- package/src/design_system/elements/IconComponent.tsx +98 -0
- package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
- package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
- package/src/design_system/elements/avatar/avatar.tsx +131 -0
- package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
- package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
- package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
- package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
- package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
- package/src/design_system/elements/avatar/utils.ts +12 -0
- package/src/design_system/elements/badges/avatar.tsx +132 -0
- package/src/design_system/elements/badges/badge-groups.tsx +176 -0
- package/src/design_system/elements/badges/badge-types.ts +266 -0
- package/src/design_system/elements/badges/badges.tsx +430 -0
- package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
- package/src/design_system/elements/button-group/button-group.tsx +106 -0
- package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
- package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
- package/src/design_system/elements/buttons/button-utility.tsx +116 -0
- package/src/design_system/elements/buttons/button.aman.tsx +174 -0
- package/src/design_system/elements/buttons/button.tsx +271 -0
- package/src/design_system/elements/buttons/close-button.tsx +42 -0
- package/src/design_system/elements/buttons/round-button.tsx +29 -0
- package/src/design_system/elements/buttons/social-button.tsx +148 -0
- package/src/design_system/elements/buttons/social-logos.tsx +115 -0
- package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
- package/src/design_system/elements/carousel/carousel.tsx +308 -0
- package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
- package/src/design_system/elements/date-picker/calendar.tsx +101 -0
- package/src/design_system/elements/date-picker/cell.tsx +106 -0
- package/src/design_system/elements/date-picker/date-input.tsx +32 -0
- package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
- package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
- package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
- package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
- package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
- package/src/design_system/elements/form/form.tsx +10 -0
- package/src/design_system/elements/form/hook-form.tsx +75 -0
- package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
- package/src/design_system/elements/index.tsx +158 -0
- package/src/design_system/elements/input/hint-text.tsx +33 -0
- package/src/design_system/elements/input/input-group.tsx +133 -0
- package/src/design_system/elements/input/input.aman.tsx +172 -0
- package/src/design_system/elements/input/input.tsx +271 -0
- package/src/design_system/elements/input/label.tsx +50 -0
- package/src/design_system/elements/label/label.tsx +50 -0
- package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
- package/src/design_system/elements/map/GoogleMap.tsx +286 -0
- package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
- package/src/design_system/elements/modals/modal.tsx +41 -0
- package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
- package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
- package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
- package/src/design_system/elements/pagination/pagination.tsx +330 -0
- package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
- package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
- package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
- package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
- package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
- package/src/design_system/elements/rating/rating-badge.tsx +144 -0
- package/src/design_system/elements/rating/rating-stars.tsx +77 -0
- package/src/design_system/elements/select/combobox.tsx +152 -0
- package/src/design_system/elements/select/multi-select.tsx +363 -0
- package/src/design_system/elements/select/popover.tsx +34 -0
- package/src/design_system/elements/select/select-item.tsx +97 -0
- package/src/design_system/elements/select/select-native.tsx +69 -0
- package/src/design_system/elements/select/select.aman.tsx +75 -0
- package/src/design_system/elements/select/select.tsx +146 -0
- package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
- package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
- package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
- package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
- package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
- package/src/design_system/elements/tabs/tabs.tsx +225 -0
- package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
- package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
- package/src/design_system/elements/tags/tags.tsx +176 -0
- package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
- package/src/design_system/elements/textarea/textarea.tsx +111 -0
- package/src/design_system/elements/toggle/toggle.tsx +140 -0
- package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
- package/src/design_system/hooks/use-breakpoint.ts +37 -0
- package/src/design_system/hooks/use-resize-observer.ts +68 -0
- package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
- package/src/design_system/logo/keystone-logo.tsx +22 -0
- package/src/design_system/sections/about-home.aman.tsx +85 -0
- package/src/design_system/sections/about-home.tsx +115 -0
- package/src/design_system/sections/blog-cards.tsx +848 -0
- package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
- package/src/design_system/sections/blog-gallery.tsx +204 -0
- package/src/design_system/sections/blog-home.aman.tsx +84 -0
- package/src/design_system/sections/blog-home.tsx +153 -0
- package/src/design_system/sections/blog-post.aman.tsx +74 -0
- package/src/design_system/sections/blog-post.tsx +301 -0
- package/src/design_system/sections/blog-section.aman.tsx +101 -0
- package/src/design_system/sections/blog-section.tsx +179 -0
- package/src/design_system/sections/contact-home.tsx +25 -0
- package/src/design_system/sections/contact-section.aman.tsx +173 -0
- package/src/design_system/sections/contact-section.tsx +143 -0
- package/src/design_system/sections/faq-grid.aman.tsx +79 -0
- package/src/design_system/sections/faq-grid.tsx +102 -0
- package/src/design_system/sections/faq-home.aman.tsx +92 -0
- package/src/design_system/sections/faq-home.tsx +134 -0
- package/src/design_system/sections/feature-tab.tsx +43 -0
- package/src/design_system/sections/feature-text.tsx +284 -0
- package/src/design_system/sections/footer-home.aman.tsx +62 -0
- package/src/design_system/sections/footer-home.tsx +259 -0
- package/src/design_system/sections/generic-header-component.tsx +103 -0
- package/src/design_system/sections/header-navigation.aman.tsx +360 -0
- package/src/design_system/sections/header-navigation.tsx +334 -0
- package/src/design_system/sections/hero-faq.aman.tsx +38 -0
- package/src/design_system/sections/hero-faq.tsx +55 -0
- package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
- package/src/design_system/sections/hero-generic-text.tsx +51 -0
- package/src/design_system/sections/hero-home.aman.tsx +84 -0
- package/src/design_system/sections/hero-home.tsx +246 -0
- package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
- package/src/design_system/sections/hero-location-detail.tsx +72 -0
- package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
- package/src/design_system/sections/hero-service-detail.tsx +51 -0
- package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
- package/src/design_system/sections/hero-social-media.tsx +35 -0
- package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
- package/src/design_system/sections/hero-testimonials.tsx +55 -0
- package/src/design_system/sections/home-hero-component.tsx +228 -0
- package/src/design_system/sections/index.tsx +131 -0
- package/src/design_system/sections/job-gallery.aman.tsx +91 -0
- package/src/design_system/sections/job-gallery.tsx +183 -0
- package/src/design_system/sections/location-details-section.aman.tsx +179 -0
- package/src/design_system/sections/location-details-section.tsx +196 -0
- package/src/design_system/sections/location-grid.aman.tsx +76 -0
- package/src/design_system/sections/location-grid.tsx +123 -0
- package/src/design_system/sections/services-grid.aman.tsx +85 -0
- package/src/design_system/sections/services-grid.tsx +104 -0
- package/src/design_system/sections/services-home.aman.tsx +78 -0
- package/src/design_system/sections/services-home.tsx +131 -0
- package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
- package/src/design_system/sections/social-media-grid.tsx +189 -0
- package/src/design_system/sections/statistics-section.aman.tsx +79 -0
- package/src/design_system/sections/statistics-section.tsx +97 -0
- package/src/design_system/sections/team-grid.aman.tsx +85 -0
- package/src/design_system/sections/team-grid.tsx +88 -0
- package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
- package/src/design_system/sections/testimonials-home.tsx +90 -0
- package/src/design_system/sections/values-section.aman.tsx +73 -0
- package/src/design_system/sections/values-section.tsx +128 -0
- package/src/design_system/utils/icon-mapping.tsx +28 -0
- package/src/index.ts +7 -0
- package/src/lib/component-registry.ts +53 -0
- package/src/lib/hooks/index.ts +8 -0
- package/src/lib/hooks/use-breakpoint.ts +37 -0
- package/src/lib/hooks/use-clipboard.ts +79 -0
- package/src/lib/hooks/use-resize-observer.ts +68 -0
- package/src/lib/server-api.ts +115 -0
- package/src/styles/style-overrides.aman.css +101 -0
- package/src/styles/theme.css +224 -0
- package/src/styles/typography.css +430 -0
- package/src/themes/index.ts +23 -0
- package/src/types/api/blog-post.ts +53 -0
- package/src/types/api/company-information.ts +44 -0
- package/src/types/api/contact.ts +63 -0
- package/src/types/api/faq.ts +37 -0
- package/src/types/api/job-posting.ts +34 -0
- package/src/types/api/location.ts +36 -0
- package/src/types/api/photos.ts +28 -0
- package/src/types/api/service.ts +37 -0
- package/src/types/api/social-post.ts +28 -0
- package/src/types/api/team-member.ts +29 -0
- package/src/types/api/testimonial.ts +29 -0
- package/src/types/api/website-photos.ts +22 -0
- package/src/types/config.ts +21 -0
- package/src/types/index.ts +21 -0
- package/src/utils/countries.tsx +1351 -0
- package/src/utils/cx.ts +25 -0
- package/src/utils/gradient-placeholder.ts +59 -0
- package/src/utils/is-react-component.ts +33 -0
- package/src/utils/markdown-toc.ts +54 -0
- package/src/utils/photo-helpers.ts +94 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, FC, HTMLAttributes, ReactNode } from "react";
|
|
4
|
+
import React, { cloneElement, createContext, isValidElement, useCallback, useContext, useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
type PaginationPage = {
|
|
7
|
+
/** The type of the pagination item. */
|
|
8
|
+
type: "page";
|
|
9
|
+
/** The value of the pagination item. */
|
|
10
|
+
value: number;
|
|
11
|
+
/** Whether the pagination item is the current page. */
|
|
12
|
+
isCurrent: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type PaginationEllipsisType = {
|
|
16
|
+
type: "ellipsis";
|
|
17
|
+
key: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type PaginationItemType = PaginationPage | PaginationEllipsisType;
|
|
21
|
+
|
|
22
|
+
interface PaginationContextType {
|
|
23
|
+
/** The pages of the pagination. */
|
|
24
|
+
pages: PaginationItemType[];
|
|
25
|
+
/** The current page of the pagination. */
|
|
26
|
+
currentPage: number;
|
|
27
|
+
/** The total number of pages. */
|
|
28
|
+
total: number;
|
|
29
|
+
/** The function to call when the page changes. */
|
|
30
|
+
onPageChange: (page: number) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PaginationContext = createContext<PaginationContextType | undefined>(undefined);
|
|
34
|
+
|
|
35
|
+
export interface PaginationRootProps {
|
|
36
|
+
/** Number of sibling pages to show on each side of the current page */
|
|
37
|
+
siblingCount?: number;
|
|
38
|
+
/** Current active page number */
|
|
39
|
+
page: number;
|
|
40
|
+
/** Total number of pages */
|
|
41
|
+
total: number;
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
/** The style of the pagination root. */
|
|
44
|
+
style?: CSSProperties;
|
|
45
|
+
/** The class name of the pagination root. */
|
|
46
|
+
className?: string;
|
|
47
|
+
/** Callback function that's called when the page changes with the new page number. */
|
|
48
|
+
onPageChange?: (page: number) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PaginationRoot = ({ total, siblingCount = 1, page, onPageChange, children, style, className }: PaginationRootProps) => {
|
|
52
|
+
const [pages, setPages] = useState<PaginationItemType[]>([]);
|
|
53
|
+
|
|
54
|
+
const createPaginationItems = useCallback((): PaginationItemType[] => {
|
|
55
|
+
const items: PaginationItemType[] = [];
|
|
56
|
+
// Calculate the maximum number of pagination elements (pages, potential ellipsis, first and last) to show
|
|
57
|
+
const totalPageNumbers = siblingCount * 2 + 5;
|
|
58
|
+
|
|
59
|
+
// If the total number of items to show is greater than or equal to the total pages,
|
|
60
|
+
// we can simply list all pages without needing to collapse with ellipsis
|
|
61
|
+
if (totalPageNumbers >= total) {
|
|
62
|
+
for (let i = 1; i <= total; i++) {
|
|
63
|
+
items.push({
|
|
64
|
+
type: "page",
|
|
65
|
+
value: i,
|
|
66
|
+
isCurrent: i === page,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// Calculate left and right sibling boundaries around the current page
|
|
71
|
+
const leftSiblingIndex = Math.max(page - siblingCount, 1);
|
|
72
|
+
const rightSiblingIndex = Math.min(page + siblingCount, total);
|
|
73
|
+
|
|
74
|
+
// Determine if we need to show ellipsis on either side
|
|
75
|
+
const showLeftEllipsis = leftSiblingIndex > 2;
|
|
76
|
+
const showRightEllipsis = rightSiblingIndex < total - 1;
|
|
77
|
+
|
|
78
|
+
// Case 1: No left ellipsis, but right ellipsis is needed
|
|
79
|
+
if (!showLeftEllipsis && showRightEllipsis) {
|
|
80
|
+
// Calculate how many page numbers to show starting from the beginning
|
|
81
|
+
const leftItemCount = siblingCount * 2 + 3;
|
|
82
|
+
const leftRange = range(1, leftItemCount);
|
|
83
|
+
|
|
84
|
+
leftRange.forEach((pageNum) =>
|
|
85
|
+
items.push({
|
|
86
|
+
type: "page",
|
|
87
|
+
value: pageNum,
|
|
88
|
+
isCurrent: pageNum === page,
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Insert ellipsis after the left range and add the last page
|
|
93
|
+
items.push({ type: "ellipsis", key: leftItemCount + 1 });
|
|
94
|
+
items.push({
|
|
95
|
+
type: "page",
|
|
96
|
+
value: total,
|
|
97
|
+
isCurrent: total === page,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// Case 2: Left ellipsis needed, but right ellipsis is not needed
|
|
101
|
+
else if (showLeftEllipsis && !showRightEllipsis) {
|
|
102
|
+
// Determine how many items from the end should be shown
|
|
103
|
+
const rightItemCount = siblingCount * 2 + 3;
|
|
104
|
+
const rightRange = range(total - rightItemCount + 1, total);
|
|
105
|
+
|
|
106
|
+
// Always show the first page, then add an ellipsis to indicate skipped pages
|
|
107
|
+
items.push({
|
|
108
|
+
type: "page",
|
|
109
|
+
value: 1,
|
|
110
|
+
isCurrent: page === 1,
|
|
111
|
+
});
|
|
112
|
+
items.push({ type: "ellipsis", key: total - rightItemCount });
|
|
113
|
+
rightRange.forEach((pageNum) =>
|
|
114
|
+
items.push({
|
|
115
|
+
type: "page",
|
|
116
|
+
value: pageNum,
|
|
117
|
+
isCurrent: pageNum === page,
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
// Case 3: Both left and right ellipsis are needed
|
|
122
|
+
else if (showLeftEllipsis && showRightEllipsis) {
|
|
123
|
+
// Always show the first page
|
|
124
|
+
items.push({
|
|
125
|
+
type: "page",
|
|
126
|
+
value: 1,
|
|
127
|
+
isCurrent: page === 1,
|
|
128
|
+
});
|
|
129
|
+
// Insert left ellipsis after the first page
|
|
130
|
+
items.push({ type: "ellipsis", key: leftSiblingIndex - 1 });
|
|
131
|
+
|
|
132
|
+
// Show a range of pages around the current page
|
|
133
|
+
const middleRange = range(leftSiblingIndex, rightSiblingIndex);
|
|
134
|
+
middleRange.forEach((pageNum) =>
|
|
135
|
+
items.push({
|
|
136
|
+
type: "page",
|
|
137
|
+
value: pageNum,
|
|
138
|
+
isCurrent: pageNum === page,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Insert right ellipsis and finally the last page
|
|
143
|
+
items.push({ type: "ellipsis", key: rightSiblingIndex + 1 });
|
|
144
|
+
items.push({
|
|
145
|
+
type: "page",
|
|
146
|
+
value: total,
|
|
147
|
+
isCurrent: total === page,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return items;
|
|
153
|
+
}, [total, siblingCount, page]);
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const paginationItems = createPaginationItems();
|
|
157
|
+
setPages(paginationItems);
|
|
158
|
+
}, [createPaginationItems]);
|
|
159
|
+
|
|
160
|
+
const onPageChangeHandler = (newPage: number) => {
|
|
161
|
+
onPageChange?.(newPage);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const paginationContextValue: PaginationContextType = {
|
|
165
|
+
pages,
|
|
166
|
+
currentPage: page,
|
|
167
|
+
total,
|
|
168
|
+
onPageChange: onPageChangeHandler,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<PaginationContext.Provider value={paginationContextValue}>
|
|
173
|
+
<nav aria-label="Pagination Navigation" style={style} className={className}>
|
|
174
|
+
{children}
|
|
175
|
+
</nav>
|
|
176
|
+
</PaginationContext.Provider>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates an array of numbers from start to end.
|
|
182
|
+
* @param start - The start number.
|
|
183
|
+
* @param end - The end number.
|
|
184
|
+
* @returns An array of numbers from start to end.
|
|
185
|
+
*/
|
|
186
|
+
const range = (start: number, end: number): number[] => {
|
|
187
|
+
const length = end - start + 1;
|
|
188
|
+
|
|
189
|
+
return Array.from({ length }, (_, index) => index + start);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
interface TriggerRenderProps {
|
|
193
|
+
isDisabled: boolean;
|
|
194
|
+
onClick: () => void;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
interface TriggerProps {
|
|
198
|
+
/** The children of the trigger. Can be a render prop or a valid element. */
|
|
199
|
+
children: ReactNode | ((props: TriggerRenderProps) => ReactNode);
|
|
200
|
+
/** The style of the trigger. */
|
|
201
|
+
style?: CSSProperties;
|
|
202
|
+
/** The class name of the trigger. */
|
|
203
|
+
className?: string | ((args: { isDisabled: boolean }) => string);
|
|
204
|
+
/** If true, the child element will be cloned and passed down the prop of the trigger. */
|
|
205
|
+
asChild?: boolean;
|
|
206
|
+
/** The direction of the trigger. */
|
|
207
|
+
direction: "prev" | "next";
|
|
208
|
+
/** The aria label of the trigger. */
|
|
209
|
+
ariaLabel?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const Trigger: FC<TriggerProps> = ({ children, style, className, asChild = false, direction, ariaLabel }) => {
|
|
213
|
+
const context = useContext(PaginationContext);
|
|
214
|
+
if (!context) {
|
|
215
|
+
throw new Error("Pagination components must be used within a Pagination.Root");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const { currentPage, total, onPageChange } = context;
|
|
219
|
+
|
|
220
|
+
const isDisabled = direction === "prev" ? currentPage <= 1 : currentPage >= total;
|
|
221
|
+
|
|
222
|
+
const handleClick = () => {
|
|
223
|
+
if (isDisabled) return;
|
|
224
|
+
|
|
225
|
+
const newPage = direction === "prev" ? currentPage - 1 : currentPage + 1;
|
|
226
|
+
onPageChange?.(newPage);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const computedClassName = typeof className === "function" ? className({ isDisabled }) : className;
|
|
230
|
+
|
|
231
|
+
const defaultAriaLabel = direction === "prev" ? "Previous Page" : "Next Page";
|
|
232
|
+
|
|
233
|
+
// If the children is a render prop, we need to pass the isDisabled and onClick to the render prop.
|
|
234
|
+
if (typeof children === "function") {
|
|
235
|
+
return <>{children({ isDisabled, onClick: handleClick })}</>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// If the children is a valid element, we need to clone it and pass the isDisabled and onClick to the cloned element.
|
|
239
|
+
if (asChild && isValidElement(children)) {
|
|
240
|
+
return cloneElement(children, {
|
|
241
|
+
onClick: handleClick,
|
|
242
|
+
disabled: isDisabled,
|
|
243
|
+
isDisabled,
|
|
244
|
+
"aria-label": ariaLabel || defaultAriaLabel,
|
|
245
|
+
style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
|
|
246
|
+
className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
|
|
247
|
+
} as HTMLAttributes<HTMLElement>);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<button aria-label={ariaLabel || defaultAriaLabel} onClick={handleClick} disabled={isDisabled} style={style} className={computedClassName}>
|
|
252
|
+
{children}
|
|
253
|
+
</button>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const PaginationPrevTrigger: FC<Omit<TriggerProps, "direction">> = (props) => <Trigger {...props} direction="prev" />;
|
|
258
|
+
|
|
259
|
+
const PaginationNextTrigger: FC<Omit<TriggerProps, "direction">> = (props) => <Trigger {...props} direction="next" />;
|
|
260
|
+
|
|
261
|
+
interface PaginationItemRenderProps {
|
|
262
|
+
isSelected: boolean;
|
|
263
|
+
onClick: () => void;
|
|
264
|
+
value: number;
|
|
265
|
+
"aria-current"?: "page";
|
|
266
|
+
"aria-label"?: string;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface PaginationItemProps {
|
|
270
|
+
/** The value of the pagination item. */
|
|
271
|
+
value: number;
|
|
272
|
+
/** Whether the pagination item is the current page. */
|
|
273
|
+
isCurrent: boolean;
|
|
274
|
+
/** The children of the pagination item. Can be a render prop or a valid element. */
|
|
275
|
+
children?: ReactNode | ((props: PaginationItemRenderProps) => ReactNode);
|
|
276
|
+
/** The style object of the pagination item. */
|
|
277
|
+
style?: CSSProperties;
|
|
278
|
+
/** The class name of the pagination item. */
|
|
279
|
+
className?: string | ((args: { isSelected: boolean }) => string);
|
|
280
|
+
/** The aria label of the pagination item. */
|
|
281
|
+
ariaLabel?: string;
|
|
282
|
+
/** If true, the child element will be cloned and passed down the prop of the item. */
|
|
283
|
+
asChild?: boolean;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const PaginationItem = ({ value, isCurrent, children, style, className, ariaLabel, asChild = false }: PaginationItemProps) => {
|
|
287
|
+
const context = useContext(PaginationContext);
|
|
288
|
+
if (!context) {
|
|
289
|
+
throw new Error("Pagination components must be used within a <Pagination.Root />");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const { onPageChange } = context;
|
|
293
|
+
|
|
294
|
+
const isSelected = isCurrent;
|
|
295
|
+
|
|
296
|
+
const handleClick = () => {
|
|
297
|
+
onPageChange?.(value);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const computedClassName = typeof className === "function" ? className({ isSelected }) : className;
|
|
301
|
+
|
|
302
|
+
// If the children is a render prop, we need to pass the necessary props to the render prop.
|
|
303
|
+
if (typeof children === "function") {
|
|
304
|
+
return (
|
|
305
|
+
<>
|
|
306
|
+
{children({
|
|
307
|
+
isSelected,
|
|
308
|
+
onClick: handleClick,
|
|
309
|
+
value,
|
|
310
|
+
"aria-current": isCurrent ? "page" : undefined,
|
|
311
|
+
"aria-label": ariaLabel || `Page ${value}`,
|
|
312
|
+
})}
|
|
313
|
+
</>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// If the children is a valid element, we need to clone it and pass the necessary props to the cloned element.
|
|
318
|
+
if (asChild && isValidElement(children)) {
|
|
319
|
+
return cloneElement(children, {
|
|
320
|
+
onClick: handleClick,
|
|
321
|
+
"aria-current": isCurrent ? "page" : undefined,
|
|
322
|
+
"aria-label": ariaLabel || `Page ${value}`,
|
|
323
|
+
style: { ...(children.props as HTMLAttributes<HTMLElement>).style, ...style },
|
|
324
|
+
className: [computedClassName, (children.props as HTMLAttributes<HTMLElement>).className].filter(Boolean).join(" ") || undefined,
|
|
325
|
+
} as HTMLAttributes<HTMLElement>);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<button
|
|
330
|
+
onClick={handleClick}
|
|
331
|
+
style={style}
|
|
332
|
+
className={computedClassName}
|
|
333
|
+
aria-current={isCurrent ? "page" : undefined}
|
|
334
|
+
aria-label={ariaLabel || `Page ${value}`}
|
|
335
|
+
role="listitem"
|
|
336
|
+
>
|
|
337
|
+
{children}
|
|
338
|
+
</button>
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
interface PaginationEllipsisProps {
|
|
342
|
+
key: number;
|
|
343
|
+
children?: ReactNode;
|
|
344
|
+
style?: CSSProperties;
|
|
345
|
+
className?: string | (() => string);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const PaginationEllipsis: FC<PaginationEllipsisProps> = ({ children, style, className }) => {
|
|
349
|
+
const computedClassName = typeof className === "function" ? className() : className;
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
<span style={style} className={computedClassName} aria-hidden="true">
|
|
353
|
+
{children}
|
|
354
|
+
</span>
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
interface PaginationContextComponentProps {
|
|
359
|
+
children: (pagination: PaginationContextType) => ReactNode;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const PaginationContextComponent: FC<PaginationContextComponentProps> = ({ children }) => {
|
|
363
|
+
const context = useContext(PaginationContext);
|
|
364
|
+
if (!context) {
|
|
365
|
+
throw new Error("Pagination components must be used within a Pagination.Root");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return <>{children(context)}</>;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
export const Pagination = {
|
|
372
|
+
Root: PaginationRoot,
|
|
373
|
+
PrevTrigger: PaginationPrevTrigger,
|
|
374
|
+
NextTrigger: PaginationNextTrigger,
|
|
375
|
+
Item: PaginationItem,
|
|
376
|
+
Ellipsis: PaginationEllipsis,
|
|
377
|
+
Context: PaginationContextComponent,
|
|
378
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cx } from '../../../utils/cx';
|
|
4
|
+
import type { PaginationRootProps } from "./pagination-base";
|
|
5
|
+
import { Pagination } from "./pagination-base";
|
|
6
|
+
|
|
7
|
+
interface PaginationDotProps extends Omit<PaginationRootProps, "children"> {
|
|
8
|
+
/** The size of the pagination dot. */
|
|
9
|
+
size?: "md" | "lg";
|
|
10
|
+
/** Whether the pagination uses brand colors. */
|
|
11
|
+
isBrand?: boolean;
|
|
12
|
+
/** Whether the pagination is displayed in a card. */
|
|
13
|
+
framed?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const PaginationDot = ({ framed, className, size = "md", isBrand, ...props }: PaginationDotProps) => {
|
|
17
|
+
const sizes = {
|
|
18
|
+
md: {
|
|
19
|
+
root: cx("gap-3", framed && "p-2"),
|
|
20
|
+
button: "h-2 w-2 after:-inset-x-1.5 after:-inset-y-2",
|
|
21
|
+
},
|
|
22
|
+
lg: {
|
|
23
|
+
root: cx("gap-4", framed && "p-3"),
|
|
24
|
+
button: "h-2.5 w-2.5 after:-inset-x-2 after:-inset-y-3",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Pagination.Root {...props} className={cx("flex h-max w-max", sizes[size].root, framed && "rounded-full bg-alpha-white/90 backdrop-blur", className)}>
|
|
30
|
+
<Pagination.Context>
|
|
31
|
+
{({ pages }) =>
|
|
32
|
+
pages.map((page, index) =>
|
|
33
|
+
page.type === "page" ? (
|
|
34
|
+
<Pagination.Item
|
|
35
|
+
{...page}
|
|
36
|
+
asChild
|
|
37
|
+
key={index}
|
|
38
|
+
className={cx(
|
|
39
|
+
"relative cursor-pointer rounded-full bg-quaternary outline-focus-ring after:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
40
|
+
sizes[size].button,
|
|
41
|
+
page.isCurrent && "bg-fg-brand-primary_alt",
|
|
42
|
+
isBrand && "bg-fg-brand-secondary",
|
|
43
|
+
isBrand && page.isCurrent && "bg-fg-white",
|
|
44
|
+
)}
|
|
45
|
+
></Pagination.Item>
|
|
46
|
+
) : (
|
|
47
|
+
<Pagination.Ellipsis {...page} key={index} />
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
</Pagination.Context>
|
|
52
|
+
</Pagination.Root>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cx } from '../../../utils/cx';
|
|
4
|
+
import type { PaginationRootProps } from "./pagination-base";
|
|
5
|
+
import { Pagination } from "./pagination-base";
|
|
6
|
+
|
|
7
|
+
interface PaginationLineProps extends Omit<PaginationRootProps, "children"> {
|
|
8
|
+
/** The size of the pagination line. */
|
|
9
|
+
size?: "md" | "lg";
|
|
10
|
+
/** Whether the pagination is displayed in a card. */
|
|
11
|
+
framed?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const PaginationLine = ({ framed, className, size = "md", ...props }: PaginationLineProps) => {
|
|
15
|
+
const sizes = {
|
|
16
|
+
md: {
|
|
17
|
+
root: cx("gap-2", framed && "p-2"),
|
|
18
|
+
button: "h-1.5 w-full after:-inset-x-1.5 after:-inset-y-2",
|
|
19
|
+
},
|
|
20
|
+
lg: {
|
|
21
|
+
root: cx("gap-3", framed && "p-3"),
|
|
22
|
+
button: "h-2 w-full after:-inset-x-2 after:-inset-y-3",
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Pagination.Root {...props} className={cx("flex h-max w-max", sizes[size].root, framed && "rounded-full bg-alpha-white/90 backdrop-blur", className)}>
|
|
28
|
+
<Pagination.Context>
|
|
29
|
+
{({ pages }) =>
|
|
30
|
+
pages.map((page, index) =>
|
|
31
|
+
page.type === "page" ? (
|
|
32
|
+
<Pagination.Item
|
|
33
|
+
{...page}
|
|
34
|
+
asChild
|
|
35
|
+
key={index}
|
|
36
|
+
className={cx(
|
|
37
|
+
"relative cursor-pointer rounded-full bg-quaternary outline-focus-ring after:absolute focus-visible:outline-2 focus-visible:outline-offset-2",
|
|
38
|
+
sizes[size].button,
|
|
39
|
+
page.isCurrent && "bg-fg-brand-primary_alt",
|
|
40
|
+
)}
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
<Pagination.Ellipsis {...page} key={index} />
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
</Pagination.Context>
|
|
48
|
+
</Pagination.Root>
|
|
49
|
+
);
|
|
50
|
+
};
|