@fragments-sdk/ui 0.8.7 → 0.8.8
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/fragments.json +1 -1
- package/package.json +2 -2
- package/src/assets/fragments-logo.tsx +9 -8
- package/src/blocks/CommandPalette.block.ts +34 -0
- package/src/blocks/PaginatedTable.block.ts +36 -0
- package/src/blocks/SettingsDrawer.block.ts +47 -0
- package/src/components/Command/Command.fragment.tsx +237 -0
- package/src/components/Command/Command.module.scss +153 -0
- package/src/components/Command/Command.test.tsx +363 -0
- package/src/components/Command/index.tsx +502 -0
- package/src/components/Drawer/Drawer.fragment.tsx +206 -0
- package/src/components/Drawer/Drawer.module.scss +215 -0
- package/src/components/Drawer/Drawer.test.tsx +227 -0
- package/src/components/Drawer/index.tsx +239 -0
- package/src/components/Pagination/Pagination.fragment.tsx +152 -0
- package/src/components/Pagination/Pagination.module.scss +109 -0
- package/src/components/Pagination/Pagination.test.tsx +171 -0
- package/src/components/Pagination/index.tsx +360 -0
- package/src/components/Tooltip/index.tsx +25 -1
- package/src/index.ts +34 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import styles from './Pagination.module.scss';
|
|
3
|
+
import '../../styles/globals.scss';
|
|
4
|
+
|
|
5
|
+
// ============================================
|
|
6
|
+
// Types
|
|
7
|
+
// ============================================
|
|
8
|
+
|
|
9
|
+
export interface PaginationProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
/** Total number of pages. Clamped to Math.max(0, totalPages). Renders nothing when 0. */
|
|
12
|
+
totalPages: number;
|
|
13
|
+
/** Controlled current page (1-indexed). Clamped to [1, totalPages]. */
|
|
14
|
+
page?: number;
|
|
15
|
+
/** Default page (uncontrolled). Clamped to [1, totalPages]. Default: 1 */
|
|
16
|
+
defaultPage?: number;
|
|
17
|
+
/** Called when page changes */
|
|
18
|
+
onPageChange?: (page: number) => void;
|
|
19
|
+
/** Number of pages shown at edges: default 1 */
|
|
20
|
+
edgeCount?: number;
|
|
21
|
+
/** Number of pages shown around current: default 1 */
|
|
22
|
+
siblingCount?: number;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PaginationItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
27
|
+
children?: React.ReactNode;
|
|
28
|
+
/** Override page number (auto-assigned by context if omitted) */
|
|
29
|
+
page?: number;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================
|
|
34
|
+
// Context
|
|
35
|
+
// ============================================
|
|
36
|
+
|
|
37
|
+
interface PaginationContextValue {
|
|
38
|
+
currentPage: number;
|
|
39
|
+
totalPages: number;
|
|
40
|
+
edgeCount: number;
|
|
41
|
+
siblingCount: number;
|
|
42
|
+
setPage: (page: number) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const PaginationContext = React.createContext<PaginationContextValue | null>(null);
|
|
46
|
+
|
|
47
|
+
function usePaginationContext() {
|
|
48
|
+
const ctx = React.useContext(PaginationContext);
|
|
49
|
+
if (!ctx) throw new Error('Pagination sub-components must be used within <Pagination>');
|
|
50
|
+
return ctx;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================
|
|
54
|
+
// Page range algorithm
|
|
55
|
+
// ============================================
|
|
56
|
+
|
|
57
|
+
type RangeItem = number | 'ellipsis';
|
|
58
|
+
|
|
59
|
+
function usePaginationRange(
|
|
60
|
+
totalPages: number,
|
|
61
|
+
currentPage: number,
|
|
62
|
+
siblingCount: number,
|
|
63
|
+
edgeCount: number,
|
|
64
|
+
): RangeItem[] {
|
|
65
|
+
return React.useMemo(() => {
|
|
66
|
+
if (totalPages <= 0) return [];
|
|
67
|
+
|
|
68
|
+
// If total pages is small enough, show all pages
|
|
69
|
+
const totalSlots = edgeCount * 2 + siblingCount * 2 + 1 + 2; // edges + siblings + current + 2 ellipses
|
|
70
|
+
if (totalPages <= totalSlots) {
|
|
71
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const leftEdge = Array.from({ length: edgeCount }, (_, i) => i + 1);
|
|
75
|
+
const rightEdge = Array.from({ length: edgeCount }, (_, i) => totalPages - edgeCount + 1 + i);
|
|
76
|
+
|
|
77
|
+
const siblingStart = Math.max(edgeCount + 1, currentPage - siblingCount);
|
|
78
|
+
const siblingEnd = Math.min(totalPages - edgeCount, currentPage + siblingCount);
|
|
79
|
+
|
|
80
|
+
const result: RangeItem[] = [];
|
|
81
|
+
|
|
82
|
+
// Add left edge
|
|
83
|
+
for (const p of leftEdge) {
|
|
84
|
+
result.push(p);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Left ellipsis
|
|
88
|
+
if (siblingStart > edgeCount + 1) {
|
|
89
|
+
// If there's only one gap, show the number instead of ellipsis
|
|
90
|
+
if (siblingStart === edgeCount + 2) {
|
|
91
|
+
result.push(edgeCount + 1);
|
|
92
|
+
} else {
|
|
93
|
+
result.push('ellipsis');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Siblings + current
|
|
98
|
+
for (let i = siblingStart; i <= siblingEnd; i++) {
|
|
99
|
+
if (!result.includes(i)) {
|
|
100
|
+
result.push(i);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Right ellipsis
|
|
105
|
+
if (siblingEnd < totalPages - edgeCount) {
|
|
106
|
+
if (siblingEnd === totalPages - edgeCount - 1) {
|
|
107
|
+
result.push(totalPages - edgeCount);
|
|
108
|
+
} else {
|
|
109
|
+
result.push('ellipsis');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Add right edge
|
|
114
|
+
for (const p of rightEdge) {
|
|
115
|
+
if (!result.includes(p)) {
|
|
116
|
+
result.push(p);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}, [totalPages, currentPage, siblingCount, edgeCount]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================
|
|
125
|
+
// Chevron Icons
|
|
126
|
+
// ============================================
|
|
127
|
+
|
|
128
|
+
function ChevronLeftIcon() {
|
|
129
|
+
return (
|
|
130
|
+
<svg
|
|
131
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
132
|
+
width="16"
|
|
133
|
+
height="16"
|
|
134
|
+
viewBox="0 0 24 24"
|
|
135
|
+
fill="none"
|
|
136
|
+
stroke="currentColor"
|
|
137
|
+
strokeWidth="2"
|
|
138
|
+
strokeLinecap="round"
|
|
139
|
+
strokeLinejoin="round"
|
|
140
|
+
aria-hidden="true"
|
|
141
|
+
>
|
|
142
|
+
<polyline points="15 18 9 12 15 6" />
|
|
143
|
+
</svg>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ChevronRightIcon() {
|
|
148
|
+
return (
|
|
149
|
+
<svg
|
|
150
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
151
|
+
width="16"
|
|
152
|
+
height="16"
|
|
153
|
+
viewBox="0 0 24 24"
|
|
154
|
+
fill="none"
|
|
155
|
+
stroke="currentColor"
|
|
156
|
+
strokeWidth="2"
|
|
157
|
+
strokeLinecap="round"
|
|
158
|
+
strokeLinejoin="round"
|
|
159
|
+
aria-hidden="true"
|
|
160
|
+
>
|
|
161
|
+
<polyline points="9 18 15 12 9 6" />
|
|
162
|
+
</svg>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ============================================
|
|
167
|
+
// Components
|
|
168
|
+
// ============================================
|
|
169
|
+
|
|
170
|
+
function clamp(value: number, min: number, max: number) {
|
|
171
|
+
return Math.max(min, Math.min(max, value));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function PaginationRoot({
|
|
175
|
+
children,
|
|
176
|
+
totalPages: rawTotalPages,
|
|
177
|
+
page: controlledPage,
|
|
178
|
+
defaultPage = 1,
|
|
179
|
+
onPageChange,
|
|
180
|
+
edgeCount = 1,
|
|
181
|
+
siblingCount = 1,
|
|
182
|
+
className,
|
|
183
|
+
}: PaginationProps) {
|
|
184
|
+
const totalPages = Math.max(0, Math.floor(rawTotalPages));
|
|
185
|
+
const [uncontrolledPage, setUncontrolledPage] = React.useState(() =>
|
|
186
|
+
totalPages > 0 ? clamp(defaultPage, 1, totalPages) : 1
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const isControlled = controlledPage !== undefined;
|
|
190
|
+
const currentPage = isControlled
|
|
191
|
+
? (totalPages > 0 ? clamp(controlledPage, 1, totalPages) : 1)
|
|
192
|
+
: (totalPages > 0 ? clamp(uncontrolledPage, 1, totalPages) : 1);
|
|
193
|
+
|
|
194
|
+
const setPage = React.useCallback(
|
|
195
|
+
(newPage: number) => {
|
|
196
|
+
if (totalPages <= 0) return;
|
|
197
|
+
const clamped = clamp(newPage, 1, totalPages);
|
|
198
|
+
if (clamped === currentPage) return;
|
|
199
|
+
if (!isControlled) {
|
|
200
|
+
setUncontrolledPage(clamped);
|
|
201
|
+
}
|
|
202
|
+
onPageChange?.(clamped);
|
|
203
|
+
},
|
|
204
|
+
[totalPages, currentPage, isControlled, onPageChange]
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const contextValue = React.useMemo<PaginationContextValue>(
|
|
208
|
+
() => ({ currentPage, totalPages, edgeCount, siblingCount, setPage }),
|
|
209
|
+
[currentPage, totalPages, edgeCount, siblingCount, setPage]
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (totalPages <= 0) {
|
|
213
|
+
return <nav aria-label="Pagination" className={className} />;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<PaginationContext.Provider value={contextValue}>
|
|
218
|
+
<nav aria-label="Pagination" className={[styles.pagination, className].filter(Boolean).join(' ')}>
|
|
219
|
+
<ul className={styles.list}>
|
|
220
|
+
{children}
|
|
221
|
+
</ul>
|
|
222
|
+
</nav>
|
|
223
|
+
</PaginationContext.Provider>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function PaginationPrevious({ className, ...htmlProps }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
228
|
+
const { currentPage, setPage } = usePaginationContext();
|
|
229
|
+
const disabled = currentPage <= 1;
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<li>
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
aria-label="Go to previous page"
|
|
236
|
+
disabled={disabled}
|
|
237
|
+
onClick={() => setPage(currentPage - 1)}
|
|
238
|
+
className={[styles.item, styles.navButton, disabled && styles.itemDisabled, className].filter(Boolean).join(' ')}
|
|
239
|
+
{...htmlProps}
|
|
240
|
+
>
|
|
241
|
+
<ChevronLeftIcon />
|
|
242
|
+
</button>
|
|
243
|
+
</li>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function PaginationNext({ className, ...htmlProps }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
248
|
+
const { currentPage, totalPages, setPage } = usePaginationContext();
|
|
249
|
+
const disabled = currentPage >= totalPages;
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<li>
|
|
253
|
+
<button
|
|
254
|
+
type="button"
|
|
255
|
+
aria-label="Go to next page"
|
|
256
|
+
disabled={disabled}
|
|
257
|
+
onClick={() => setPage(currentPage + 1)}
|
|
258
|
+
className={[styles.item, styles.navButton, disabled && styles.itemDisabled, className].filter(Boolean).join(' ')}
|
|
259
|
+
{...htmlProps}
|
|
260
|
+
>
|
|
261
|
+
<ChevronRightIcon />
|
|
262
|
+
</button>
|
|
263
|
+
</li>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function PaginationItems() {
|
|
268
|
+
const { currentPage, totalPages, siblingCount, edgeCount, setPage } = usePaginationContext();
|
|
269
|
+
const range = usePaginationRange(totalPages, currentPage, siblingCount, edgeCount);
|
|
270
|
+
let ellipsisCount = 0;
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<>
|
|
274
|
+
{range.map((item) => {
|
|
275
|
+
if (item === 'ellipsis') {
|
|
276
|
+
ellipsisCount++;
|
|
277
|
+
return (
|
|
278
|
+
<li key={`ellipsis-${ellipsisCount}`}>
|
|
279
|
+
<span className={styles.ellipsis} aria-hidden="true">
|
|
280
|
+
…
|
|
281
|
+
</span>
|
|
282
|
+
</li>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const isActive = item === currentPage;
|
|
287
|
+
return (
|
|
288
|
+
<li key={item}>
|
|
289
|
+
<button
|
|
290
|
+
type="button"
|
|
291
|
+
aria-label={`Go to page ${item}`}
|
|
292
|
+
aria-current={isActive ? 'page' : undefined}
|
|
293
|
+
onClick={() => setPage(item)}
|
|
294
|
+
className={[styles.item, isActive && styles.itemActive].filter(Boolean).join(' ')}
|
|
295
|
+
>
|
|
296
|
+
{item}
|
|
297
|
+
</button>
|
|
298
|
+
</li>
|
|
299
|
+
);
|
|
300
|
+
})}
|
|
301
|
+
</>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function PaginationItem({
|
|
306
|
+
children,
|
|
307
|
+
page: pageProp,
|
|
308
|
+
className,
|
|
309
|
+
...htmlProps
|
|
310
|
+
}: PaginationItemProps) {
|
|
311
|
+
const { currentPage, setPage } = usePaginationContext();
|
|
312
|
+
const page = pageProp ?? 1;
|
|
313
|
+
const isActive = page === currentPage;
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<li>
|
|
317
|
+
<button
|
|
318
|
+
type="button"
|
|
319
|
+
aria-label={`Go to page ${page}`}
|
|
320
|
+
aria-current={isActive ? 'page' : undefined}
|
|
321
|
+
onClick={() => setPage(page)}
|
|
322
|
+
className={[styles.item, isActive && styles.itemActive, className].filter(Boolean).join(' ')}
|
|
323
|
+
{...htmlProps}
|
|
324
|
+
>
|
|
325
|
+
{children ?? page}
|
|
326
|
+
</button>
|
|
327
|
+
</li>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function PaginationEllipsis({ className, ...htmlProps }: React.HTMLAttributes<HTMLSpanElement>) {
|
|
332
|
+
return (
|
|
333
|
+
<li>
|
|
334
|
+
<span className={[styles.ellipsis, className].filter(Boolean).join(' ')} aria-hidden="true" {...htmlProps}>
|
|
335
|
+
…
|
|
336
|
+
</span>
|
|
337
|
+
</li>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ============================================
|
|
342
|
+
// Export compound component
|
|
343
|
+
// ============================================
|
|
344
|
+
|
|
345
|
+
export const Pagination = Object.assign(PaginationRoot, {
|
|
346
|
+
Previous: PaginationPrevious,
|
|
347
|
+
Next: PaginationNext,
|
|
348
|
+
Items: PaginationItems,
|
|
349
|
+
Item: PaginationItem,
|
|
350
|
+
Ellipsis: PaginationEllipsis,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
export {
|
|
354
|
+
PaginationRoot,
|
|
355
|
+
PaginationPrevious,
|
|
356
|
+
PaginationNext,
|
|
357
|
+
PaginationItems,
|
|
358
|
+
PaginationItem,
|
|
359
|
+
PaginationEllipsis,
|
|
360
|
+
};
|
|
@@ -78,6 +78,30 @@ function TooltipRoot({
|
|
|
78
78
|
className,
|
|
79
79
|
...htmlProps
|
|
80
80
|
}: TooltipProps) {
|
|
81
|
+
const renderTrigger = React.useCallback(
|
|
82
|
+
(triggerProps: React.HTMLAttributes<HTMLElement> & { ref?: React.Ref<HTMLElement> }) => {
|
|
83
|
+
const childProps = children.props as Record<string, unknown>;
|
|
84
|
+
const mergedProps: Record<string, unknown> = { ...childProps, ...triggerProps };
|
|
85
|
+
|
|
86
|
+
for (const [key, triggerHandler] of Object.entries(triggerProps)) {
|
|
87
|
+
const childHandler = childProps[key];
|
|
88
|
+
if (
|
|
89
|
+
key.startsWith('on') &&
|
|
90
|
+
typeof triggerHandler === 'function' &&
|
|
91
|
+
typeof childHandler === 'function'
|
|
92
|
+
) {
|
|
93
|
+
mergedProps[key] = (...args: unknown[]) => {
|
|
94
|
+
(childHandler as (...event: unknown[]) => void)(...args);
|
|
95
|
+
(triggerHandler as (...event: unknown[]) => void)(...args);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return React.cloneElement(children, mergedProps);
|
|
101
|
+
},
|
|
102
|
+
[children],
|
|
103
|
+
);
|
|
104
|
+
|
|
81
105
|
if (disabled) {
|
|
82
106
|
return children;
|
|
83
107
|
}
|
|
@@ -89,7 +113,7 @@ function TooltipRoot({
|
|
|
89
113
|
defaultOpen={defaultOpen}
|
|
90
114
|
onOpenChange={onOpenChange}
|
|
91
115
|
>
|
|
92
|
-
<BaseTooltip.Trigger render={
|
|
116
|
+
<BaseTooltip.Trigger render={renderTrigger} />
|
|
93
117
|
<BaseTooltip.Portal>
|
|
94
118
|
<BaseTooltip.Positioner
|
|
95
119
|
side={side}
|
package/src/index.ts
CHANGED
|
@@ -527,7 +527,7 @@ export {
|
|
|
527
527
|
} from './components/Chart';
|
|
528
528
|
|
|
529
529
|
// Assets
|
|
530
|
-
export { FragmentsLogo, type FragmentsLogoProps } from './assets/fragments-logo';
|
|
530
|
+
export { FragmentsLogo, fragmentsLogoSvg, type FragmentsLogoProps } from './assets/fragments-logo';
|
|
531
531
|
|
|
532
532
|
// NavigationMenu
|
|
533
533
|
export {
|
|
@@ -554,6 +554,39 @@ export {
|
|
|
554
554
|
type NavigationMenuMobileSectionProps,
|
|
555
555
|
} from './components/NavigationMenu';
|
|
556
556
|
|
|
557
|
+
// Drawer
|
|
558
|
+
export {
|
|
559
|
+
Drawer,
|
|
560
|
+
type DrawerProps,
|
|
561
|
+
type DrawerContentProps,
|
|
562
|
+
type DrawerTriggerProps,
|
|
563
|
+
type DrawerHeaderProps,
|
|
564
|
+
type DrawerTitleProps,
|
|
565
|
+
type DrawerDescriptionProps,
|
|
566
|
+
type DrawerBodyProps,
|
|
567
|
+
type DrawerFooterProps,
|
|
568
|
+
type DrawerCloseProps,
|
|
569
|
+
} from './components/Drawer';
|
|
570
|
+
|
|
571
|
+
// Pagination
|
|
572
|
+
export {
|
|
573
|
+
Pagination,
|
|
574
|
+
type PaginationProps,
|
|
575
|
+
type PaginationItemProps,
|
|
576
|
+
} from './components/Pagination';
|
|
577
|
+
|
|
578
|
+
// Command
|
|
579
|
+
export {
|
|
580
|
+
Command,
|
|
581
|
+
type CommandProps,
|
|
582
|
+
type CommandInputProps,
|
|
583
|
+
type CommandListProps,
|
|
584
|
+
type CommandItemProps,
|
|
585
|
+
type CommandGroupProps,
|
|
586
|
+
type CommandEmptyProps,
|
|
587
|
+
type CommandSeparatorProps,
|
|
588
|
+
} from './components/Command';
|
|
589
|
+
|
|
557
590
|
// Accessibility Utilities
|
|
558
591
|
export {
|
|
559
592
|
useId,
|