devnonla-ui 0.3.0 → 0.4.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.
Files changed (48) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -1
  3. package/src/app/App.tsx +4 -4
  4. package/src/button/Button.tsx +1 -1
  5. package/src/button/ButtonCopy.tsx +11 -21
  6. package/src/calendar/Calendar.tsx +1 -1
  7. package/src/chat/AgentPanel.tsx +3 -3
  8. package/src/chat/index.ts +35 -51
  9. package/src/chat/message-ui/ChatError.tsx +2 -4
  10. package/src/chat/message-ui/ChatInput.tsx +5 -9
  11. package/src/chat/message-ui/ChatThinking.tsx +2 -3
  12. package/src/chat/message-ui/ChatUserMessage.tsx +4 -5
  13. package/src/chat/message-ui/ChatWelcome.tsx +3 -8
  14. package/src/chat/message-ui/MermaidBlock.tsx +7 -47
  15. package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +8 -11
  16. package/src/chat/tool-ui/BackgroundTasksBar.tsx +13 -14
  17. package/src/chat/tool-ui/CallAgentToolUI.tsx +10 -4
  18. package/src/chat/tool-ui/ChatToolCall.tsx +11 -17
  19. package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +2 -2
  20. package/src/chat/tool-ui/ReadSkillToolUI.tsx +3 -3
  21. package/src/chat/tool-ui/RunJsToolUI.tsx +2 -2
  22. package/src/chat/tool-ui/ToolUiTrailing.tsx +3 -7
  23. package/src/chat/tool-ui/WebFetchToolUI.tsx +22 -13
  24. package/src/checkbox/Checkbox.tsx +1 -1
  25. package/src/codeblock/CodeBlock.tsx +17 -19
  26. package/src/colorpicker/ColorPicker.tsx +7 -7
  27. package/src/datepicker/DatePicker.tsx +3 -3
  28. package/src/desktop/DesktopHeader.tsx +15 -20
  29. package/src/desktop/DesktopIcon.tsx +3 -9
  30. package/src/desktop/DesktopWindow.tsx +54 -30
  31. package/src/desktop/MeadowDesktop.tsx +1 -1
  32. package/src/form/Form.tsx +1 -1
  33. package/src/form/FormItem.tsx +2 -2
  34. package/src/form/index.ts +23 -24
  35. package/src/index.ts +167 -199
  36. package/src/input/Input.tsx +14 -12
  37. package/src/input/SearchInput.tsx +5 -2
  38. package/src/lib/sizes.ts +1 -1
  39. package/src/modal/Modal.tsx +3 -0
  40. package/src/pagination/Pagination.tsx +528 -51
  41. package/src/scroll/OverlayScroll.tsx +58 -37
  42. package/src/select/Select.tsx +2 -2
  43. package/src/spin/Spin.tsx +14 -17
  44. package/src/splitter/Splitter.tsx +3 -3
  45. package/src/styles.css +13 -11
  46. package/src/switch/Switch.tsx +1 -1
  47. package/src/table/Table.tsx +43 -18
  48. package/src/timepicker/TimePicker.tsx +2 -2
@@ -1,34 +1,82 @@
1
- import type { ReactNode } from "react";
1
+ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
2
+ import { type ComponentType, type CSSProperties, cloneElement, isValidElement, type KeyboardEvent, type ReactElement, type ReactNode, useEffect, useState } from "react";
2
3
  import { cn } from "../lib/cn";
3
- import { type ControlSize, controlHeightVar, useControlSize } from "../lib/sizes";
4
+ import { type CanonicalSize, type ControlSize, controlFieldFocusBorder, controlFieldSurface, controlFieldTransition, controlHeightVar, controlRadiusVar, getSizeTokens, useControlSize } from "../lib/sizes";
5
+ import { Select, type SelectProps } from "../select/Select";
4
6
 
5
7
  export type PaginationItemType = "page" | "prev" | "next" | "jump-prev" | "jump-next";
8
+ export type PaginationAlign = "start" | "center" | "end";
9
+ export type PaginationSemanticSlot = "root" | "item";
10
+
11
+ export type PaginationSizeChangerProps = {
12
+ disabled?: boolean;
13
+ size?: ControlSize;
14
+ value: number;
15
+ onChange: (pageSize: number) => void;
16
+ className?: string;
17
+ options: number[];
18
+ };
19
+
20
+ type SemanticMap<T> = Partial<Record<PaginationSemanticSlot, T>>;
21
+ type SemanticProp<T> = SemanticMap<T> | ((info: { props: PaginationProps }) => SemanticMap<T>);
22
+
23
+ const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
24
+ const TOTAL_BOUNDARY_SHOW_SIZE_CHANGER = 50;
6
25
 
7
26
  export type PaginationProps = {
27
+ align?: PaginationAlign;
28
+ className?: string;
29
+ style?: CSSProperties;
30
+ classNames?: SemanticProp<string>;
31
+ styles?: SemanticProp<CSSProperties>;
32
+ components?: { sizeChanger?: ComponentType<PaginationSizeChangerProps> };
8
33
  current?: number;
34
+ defaultCurrent?: number;
9
35
  pageSize?: number;
36
+ defaultPageSize?: number;
10
37
  total?: number;
11
- onChange?: (page: number, pageSize: number) => void;
12
- showSizeChanger?: boolean;
13
- className?: string;
14
- simple?: boolean;
15
38
  disabled?: boolean;
16
- size?: ControlSize;
39
+ hideOnSinglePage?: boolean;
17
40
  itemRender?: (page: number, type: PaginationItemType, originalElement: ReactNode) => ReactNode;
41
+ pageSizeOptions?: Array<number | string>;
42
+ responsive?: boolean;
43
+ showLessItems?: boolean;
44
+ showQuickJumper?: boolean | { goButton?: ReactNode };
45
+ showSizeChanger?: boolean | SelectProps;
46
+ showTitle?: boolean;
47
+ showTotal?: (total: number, range: [number, number]) => ReactNode;
48
+ simple?: boolean | { readOnly?: boolean };
49
+ size?: ControlSize;
50
+ totalBoundaryShowSizeChanger?: number;
51
+ onChange?: (page: number, pageSize: number) => void;
52
+ onShowSizeChange?: (current: number, size: number) => void;
18
53
  };
19
54
 
20
- function pageList(current: number, pages: number): (number | "ellipsis")[] {
21
- if (pages <= 7) return Array.from({ length: pages }, (_, i) => i + 1);
22
- const set = new Set<number>([1, pages, current, current - 1, current + 1]);
23
- if (current <= 3) for (const n of [2, 3, 4]) set.add(n);
24
- if (current >= pages - 2) for (const n of [pages - 1, pages - 2, pages - 3]) set.add(n);
25
- const nums = [...set].filter((n) => n >= 1 && n <= pages).sort((a, b) => a - b);
26
- const out: (number | "ellipsis")[] = [];
27
- for (let i = 0; i < nums.length; i++) {
28
- const n = nums[i]!;
29
- if (i > 0 && n - nums[i - 1]! > 1) out.push("ellipsis");
30
- out.push(n);
31
- }
55
+ function toSizes(opts?: Array<number | string>): number[] {
56
+ const raw = opts?.length ? opts : DEFAULT_PAGE_SIZE_OPTIONS;
57
+ const nums = [...new Set(raw.map(Number).filter((n) => Number.isFinite(n) && n > 0))];
58
+ return nums.length ? nums : DEFAULT_PAGE_SIZE_OPTIONS;
59
+ }
60
+
61
+ function resolveSemantic<T>(value: SemanticProp<T> | undefined, props: PaginationProps): SemanticMap<T> {
62
+ if (!value) return {};
63
+ if (typeof value === "function") return value({ props }) ?? {};
64
+ return value;
65
+ }
66
+
67
+ /** Visible page numbers + jump ellipses. Buffer 2 (1 when `showLessItems`). */
68
+ function pageItems(current: number, pages: number, less: boolean): Array<number | "jump-prev" | "jump-next"> {
69
+ const buffer = less ? 1 : 2;
70
+ if (pages <= buffer * 2 + 5) return Array.from({ length: pages }, (_, i) => i + 1);
71
+ const left = Math.max(2, current - buffer);
72
+ const right = Math.min(pages - 1, current + buffer);
73
+ const out: Array<number | "jump-prev" | "jump-next"> = [1];
74
+ if (left > 2) out.push("jump-prev");
75
+ else for (let n = 2; n < left; n++) out.push(n);
76
+ for (let n = left; n <= right; n++) out.push(n);
77
+ if (right < pages - 1) out.push("jump-next");
78
+ else for (let n = right + 1; n < pages; n++) out.push(n);
79
+ out.push(pages);
32
80
  return out;
33
81
  }
34
82
 
@@ -38,67 +86,496 @@ function ItemBtn({
38
86
  disabled,
39
87
  onClick,
40
88
  className,
89
+ style,
90
+ title,
91
+ size,
92
+ ariaLabel,
41
93
  }: {
42
94
  children: ReactNode;
43
95
  active?: boolean;
44
96
  disabled?: boolean;
45
97
  onClick?: () => void;
46
98
  className?: string;
99
+ style?: CSSProperties;
100
+ title?: string;
101
+ size: CanonicalSize;
102
+ ariaLabel?: string;
47
103
  }) {
104
+ const tok = getSizeTokens(size);
48
105
  return (
49
106
  <button
50
107
  type="button"
108
+ data-slot="pagination-item"
51
109
  disabled={disabled}
110
+ title={title}
111
+ aria-label={ariaLabel}
112
+ aria-current={active ? "page" : undefined}
52
113
  onClick={onClick}
53
- className={cn("inline-flex min-w-7 cursor-pointer items-center justify-center rounded-md px-1.5 text-sm transition-colors", "disabled:cursor-not-allowed disabled:opacity-40", active ? "bg-secondary text-foreground" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground", className)}
114
+ className={cn(
115
+ "group inline-flex min-w-0 cursor-pointer items-center justify-center px-1.5 tabular-nums select-none",
116
+ "transition-colors disabled:cursor-not-allowed disabled:opacity-40",
117
+ active ? "bg-brand font-medium text-(--nonla-solid-fg)" : "text-muted-foreground hover:bg-foreground/5 hover:text-foreground",
118
+ className,
119
+ )}
120
+ style={{
121
+ minWidth: controlHeightVar(size),
122
+ height: controlHeightVar(size),
123
+ fontSize: tok.fontSize,
124
+ lineHeight: `${tok.lineHeight}px`,
125
+ borderRadius: controlRadiusVar(size),
126
+ ...style,
127
+ }}
54
128
  >
55
129
  {children}
56
130
  </button>
57
131
  );
58
132
  }
59
133
 
60
- export function Pagination({ current = 1, pageSize = 10, total = 0, onChange, className, disabled, size, itemRender }: PaginationProps) {
61
- const resolvedSize = useControlSize(size);
62
- const compact = resolvedSize === "small";
63
- const h = compact ? controlHeightVar(resolvedSize) : 28;
64
- const pages = Math.max(1, Math.ceil(total / pageSize));
134
+ function JumpInner({ dir, icon }: { dir: "prev" | "next"; icon: number }) {
135
+ return (
136
+ <span className="relative inline-flex size-full items-center justify-center">
137
+ <span className="leading-none group-hover:opacity-0 group-focus-visible:opacity-0">…</span>
138
+ <span className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100">
139
+ {dir === "prev" ? <ChevronsLeft size={icon} strokeWidth={2} /> : <ChevronsRight size={icon} strokeWidth={2} />}
140
+ </span>
141
+ </span>
142
+ );
143
+ }
144
+
145
+ function MiniField({
146
+ value,
147
+ disabled,
148
+ size,
149
+ width,
150
+ ariaLabel,
151
+ onSubmit,
152
+ }: {
153
+ value: string;
154
+ disabled?: boolean;
155
+ size: CanonicalSize;
156
+ width: number;
157
+ ariaLabel: string;
158
+ onSubmit: (raw: string) => void;
159
+ }) {
160
+ const [text, setText] = useState(value);
161
+ const tok = getSizeTokens(size);
162
+
163
+ useEffect(() => {
164
+ setText(value);
165
+ }, [value]);
65
166
 
66
- const wrap = (page: number, type: PaginationItemType, node: ReactNode) => (itemRender ? itemRender(page, type, node) : node);
167
+ const commit = () => {
168
+ onSubmit(text);
169
+ if (!Number.isFinite(Number(text))) setText(value);
170
+ };
67
171
 
68
- const prev = wrap(
69
- current - 1,
70
- "prev",
71
- <ItemBtn disabled={disabled || current <= 1} onClick={() => onChange?.(current - 1, pageSize)}>
72
-
73
- </ItemBtn>,
172
+ return (
173
+ <input
174
+ aria-label={ariaLabel}
175
+ disabled={disabled}
176
+ inputMode="numeric"
177
+ value={text}
178
+ onChange={(e) => setText(e.target.value)}
179
+ onBlur={commit}
180
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
181
+ if (e.key === "Enter") {
182
+ e.preventDefault();
183
+ commit();
184
+ }
185
+ }}
186
+ className={cn(
187
+ "text-center tabular-nums text-foreground placeholder:text-quaternary-foreground",
188
+ controlFieldSurface,
189
+ controlFieldTransition,
190
+ controlFieldFocusBorder,
191
+ "focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-45",
192
+ )}
193
+ style={{
194
+ width,
195
+ height: controlHeightVar(size),
196
+ fontSize: tok.fontSize,
197
+ lineHeight: `${tok.lineHeight}px`,
198
+ borderRadius: controlRadiusVar(size),
199
+ paddingLeft: 4,
200
+ paddingRight: 4,
201
+ }}
202
+ />
74
203
  );
75
- const next = wrap(
76
- current + 1,
77
- "next",
78
- <ItemBtn disabled={disabled || current >= pages} onClick={() => onChange?.(current + 1, pageSize)}>
79
-
80
- </ItemBtn>,
204
+ }
205
+
206
+ function DefaultSizeChanger({ disabled, size, value, onChange, className, options, selectProps }: PaginationSizeChangerProps & { selectProps?: SelectProps }) {
207
+ const opts = options.includes(value) ? options : [value, ...options];
208
+ return (
209
+ <Select
210
+ {...selectProps}
211
+ disabled={disabled || selectProps?.disabled}
212
+ size={selectProps?.size ?? size}
213
+ value={value}
214
+ className={cn("w-[8.5rem]", className, selectProps?.className)}
215
+ onChange={(v) => {
216
+ const n = Number(v);
217
+ if (Number.isFinite(n)) onChange(n);
218
+ }}
219
+ options={selectProps?.options ?? opts.map((n) => ({ value: n, label: `${n} / page` }))}
220
+ />
81
221
  );
222
+ }
82
223
 
224
+ function GoButton({
225
+ goButton,
226
+ disabled,
227
+ size,
228
+ onGo,
229
+ }: {
230
+ goButton: ReactNode;
231
+ disabled?: boolean;
232
+ size: CanonicalSize;
233
+ onGo: () => void;
234
+ }) {
235
+ if (isValidElement(goButton)) {
236
+ return cloneElement(goButton as ReactElement<{ onClick?: () => void; disabled?: boolean }>, { onClick: onGo, disabled });
237
+ }
238
+ const tok = getSizeTokens(size);
83
239
  return (
84
- <div className={cn("inline-flex items-center gap-0.5", className)} style={{ height: h }}>
85
- {prev}
86
- {pageList(current, pages).map((item, i, list) => {
87
- if (item === "ellipsis") {
240
+ <button
241
+ type="button"
242
+ disabled={disabled}
243
+ onClick={onGo}
244
+ className={cn(
245
+ "inline-flex cursor-pointer items-center justify-center px-2 text-foreground",
246
+ controlFieldSurface,
247
+ controlFieldTransition,
248
+ "hover:bg-foreground/5 disabled:cursor-not-allowed disabled:opacity-45",
249
+ )}
250
+ style={{
251
+ height: controlHeightVar(size),
252
+ fontSize: tok.fontSize,
253
+ borderRadius: controlRadiusVar(size),
254
+ }}
255
+ >
256
+ {goButton === true ? "Go" : goButton}
257
+ </button>
258
+ );
259
+ }
260
+
261
+ export function Pagination(props: PaginationProps) {
262
+ const {
263
+ align,
264
+ className,
265
+ style,
266
+ classNames,
267
+ styles,
268
+ components,
269
+ current,
270
+ defaultCurrent = 1,
271
+ pageSize,
272
+ defaultPageSize = 10,
273
+ total = 0,
274
+ disabled,
275
+ hideOnSinglePage = false,
276
+ itemRender,
277
+ pageSizeOptions,
278
+ responsive,
279
+ showLessItems = false,
280
+ showQuickJumper = false,
281
+ showSizeChanger,
282
+ showTitle = true,
283
+ showTotal,
284
+ simple,
285
+ size,
286
+ totalBoundaryShowSizeChanger = TOTAL_BOUNDARY_SHOW_SIZE_CHANGER,
287
+ onChange,
288
+ onShowSizeChange,
289
+ } = props;
290
+
291
+ const pageControlled = current !== undefined;
292
+ const sizeControlled = pageSize !== undefined;
293
+ const [innerPage, setInnerPage] = useState(defaultCurrent);
294
+ const [innerSize, setInnerSize] = useState(defaultPageSize);
295
+ const [narrow, setNarrow] = useState(false);
296
+ const [jumpRaw, setJumpRaw] = useState("");
297
+
298
+ const appSize = useControlSize(size);
299
+ const resolvedSize: CanonicalSize = size == null && responsive && narrow ? "small" : appSize;
300
+ const tok = getSizeTokens(resolvedSize);
301
+ const icon = tok.icon;
302
+
303
+ const simpleCfg = simple === true ? { readOnly: false } : simple || null;
304
+ const isSimple = Boolean(simpleCfg);
305
+ const simpleReadOnly = Boolean(simpleCfg?.readOnly);
306
+
307
+ const jumperCfg = showQuickJumper === true ? {} : typeof showQuickJumper === "object" && showQuickJumper ? showQuickJumper : null;
308
+ const showJumper = Boolean(jumperCfg) && !isSimple;
309
+ const goButton = jumperCfg?.goButton;
310
+
311
+ const sizeChangerOn =
312
+ showSizeChanger === false
313
+ ? false
314
+ : showSizeChanger === true || (typeof showSizeChanger === "object" && showSizeChanger != null)
315
+ ? true
316
+ : !isSimple && total > totalBoundaryShowSizeChanger;
317
+ const sizeChangerSelectProps = typeof showSizeChanger === "object" && showSizeChanger != null ? showSizeChanger : undefined;
318
+
319
+ const mergedSize = Math.max(1, sizeControlled ? (pageSize ?? defaultPageSize) : innerSize);
320
+ const mergedPage = pageControlled ? (current ?? defaultCurrent) : innerPage;
321
+ const pages = Math.max(1, Math.ceil(total / mergedSize));
322
+ const page = Math.min(Math.max(1, mergedPage), pages);
323
+
324
+ useEffect(() => {
325
+ if (!responsive || size != null) {
326
+ setNarrow(false);
327
+ return;
328
+ }
329
+ const mq = window.matchMedia("(max-width: 575px)");
330
+ const apply = () => setNarrow(mq.matches);
331
+ apply();
332
+ mq.addEventListener("change", apply);
333
+ return () => mq.removeEventListener("change", apply);
334
+ }, [responsive, size]);
335
+
336
+ useEffect(() => {
337
+ if (pageControlled) return;
338
+ if (innerPage > pages) {
339
+ setInnerPage(pages);
340
+ onChange?.(pages, mergedSize);
341
+ }
342
+ }, [pages, innerPage, pageControlled, mergedSize, onChange]);
343
+
344
+ const goTo = (nextPage: number, nextSize: number, sizeChanged = false) => {
345
+ if (disabled) return;
346
+ const nextSz = Math.max(1, nextSize);
347
+ const nextPages = Math.max(1, Math.ceil(total / nextSz));
348
+ const raw = Number(nextPage);
349
+ const next = Math.min(Math.max(1, Number.isFinite(raw) ? Math.floor(raw) : 1), nextPages);
350
+ if (sizeChanged) onShowSizeChange?.(page, nextSz);
351
+ if (next === page && nextSz === mergedSize) return;
352
+ if (!pageControlled) setInnerPage(next);
353
+ if (!sizeControlled) setInnerSize(nextSz);
354
+ onChange?.(next, nextSz);
355
+ };
356
+
357
+ if (hideOnSinglePage && pages <= 1) return null;
358
+
359
+ const wrap = (p: number, type: PaginationItemType, node: ReactNode) => (itemRender ? itemRender(p, type, node) : node);
360
+ const jumpStep = showLessItems ? 3 : 5;
361
+ const semanticClass = resolveSemantic(classNames, props);
362
+ const semanticStyle = resolveSemantic(styles, props);
363
+ const itemClass = semanticClass.item;
364
+ const itemStyle = semanticStyle.item;
365
+
366
+ const start = total === 0 ? 0 : (page - 1) * mergedSize + 1;
367
+ const end = Math.min(page * mergedSize, total);
368
+
369
+ const sizeOptions = toSizes(pageSizeOptions);
370
+
371
+ const prevInner = wrap(page - 1, "prev", <ChevronLeft size={icon} strokeWidth={2} />);
372
+ const nextInner = wrap(page + 1, "next", <ChevronRight size={icon} strokeWidth={2} />);
373
+
374
+ const pager = isSimple ? (
375
+ <>
376
+ <ItemBtn size={resolvedSize} disabled={disabled || page <= 1} title={showTitle ? "Previous Page" : undefined} ariaLabel="Previous Page" className={itemClass} style={itemStyle} onClick={() => goTo(page - 1, mergedSize)}>
377
+ {prevInner}
378
+ </ItemBtn>
379
+ {simpleReadOnly ? (
380
+ <span className="px-1.5 text-sm tabular-nums text-foreground">
381
+ {page}
382
+ <span className="text-muted-foreground">/{pages}</span>
383
+ </span>
384
+ ) : (
385
+ <span className="inline-flex items-center gap-1 text-sm tabular-nums text-muted-foreground">
386
+ <MiniField
387
+ value={String(page)}
388
+ disabled={disabled}
389
+ size={resolvedSize}
390
+ width={Math.max(36, String(pages).length * 10 + 16)}
391
+ ariaLabel="Page"
392
+ onSubmit={(raw) => {
393
+ const n = Number(raw);
394
+ if (Number.isFinite(n)) goTo(n, mergedSize);
395
+ }}
396
+ />
397
+ <span>/{pages}</span>
398
+ </span>
399
+ )}
400
+ <ItemBtn size={resolvedSize} disabled={disabled || page >= pages} title={showTitle ? "Next Page" : undefined} ariaLabel="Next Page" className={itemClass} style={itemStyle} onClick={() => goTo(page + 1, mergedSize)}>
401
+ {nextInner}
402
+ </ItemBtn>
403
+ </>
404
+ ) : (
405
+ <>
406
+ <ItemBtn size={resolvedSize} disabled={disabled || page <= 1} title={showTitle ? "Previous Page" : undefined} ariaLabel="Previous Page" className={itemClass} style={itemStyle} onClick={() => goTo(page - 1, mergedSize)}>
407
+ {prevInner}
408
+ </ItemBtn>
409
+ {pageItems(page, pages, showLessItems).map((item) => {
410
+ if (item === "jump-prev" || item === "jump-next") {
411
+ const dir = item === "jump-prev" ? "prev" : "next";
412
+ const target = item === "jump-prev" ? Math.max(1, page - jumpStep) : Math.min(pages, page + jumpStep);
413
+ const title = item === "jump-prev" ? `Previous ${jumpStep} Pages` : `Next ${jumpStep} Pages`;
414
+ const inner = wrap(target, item, <JumpInner dir={dir} icon={icon} />);
88
415
  return (
89
- <span key={`ellipsis-${list[i - 1]}-${list[i + 1]}`} className="px-1 text-sm text-muted-foreground">
90
-
91
- </span>
416
+ <ItemBtn key={item} size={resolvedSize} disabled={disabled} title={showTitle ? title : undefined} ariaLabel={title} className={itemClass} style={itemStyle} onClick={() => goTo(target, mergedSize)}>
417
+ {inner}
418
+ </ItemBtn>
92
419
  );
93
420
  }
94
- const node = (
95
- <ItemBtn active={item === current} disabled={disabled} onClick={() => onChange?.(item, pageSize)}>
96
- {item}
421
+ const inner = wrap(item, "page", item);
422
+ return (
423
+ <ItemBtn
424
+ key={item}
425
+ size={resolvedSize}
426
+ active={item === page}
427
+ disabled={disabled}
428
+ title={showTitle ? String(item) : undefined}
429
+ ariaLabel={`Page ${item}`}
430
+ className={itemClass}
431
+ style={itemStyle}
432
+ onClick={() => goTo(item, mergedSize)}
433
+ >
434
+ {inner}
97
435
  </ItemBtn>
98
436
  );
99
- return <span key={item}>{wrap(item, "page", node)}</span>;
100
437
  })}
101
- {next}
102
- </div>
438
+ <ItemBtn size={resolvedSize} disabled={disabled || page >= pages} title={showTitle ? "Next Page" : undefined} ariaLabel="Next Page" className={itemClass} style={itemStyle} onClick={() => goTo(page + 1, mergedSize)}>
439
+ {nextInner}
440
+ </ItemBtn>
441
+ </>
442
+ );
443
+
444
+ const jumperLive = showJumper ? (
445
+ <Jumper
446
+ disabled={disabled}
447
+ size={resolvedSize}
448
+ pages={pages}
449
+ goButton={goButton}
450
+ jumpRaw={jumpRaw}
451
+ setJumpRaw={setJumpRaw}
452
+ onGo={(n) => goTo(n, mergedSize)}
453
+ />
454
+ ) : null;
455
+
456
+ return (
457
+ <nav
458
+ data-slot="pagination"
459
+ aria-label="Pagination"
460
+ className={cn(
461
+ "inline-flex flex-wrap items-center gap-3",
462
+ align && "flex w-full",
463
+ align === "start" && "justify-start",
464
+ align === "center" && "justify-center",
465
+ align === "end" && "justify-end",
466
+ semanticClass.root,
467
+ className,
468
+ )}
469
+ style={{ ...semanticStyle.root, ...style }}
470
+ >
471
+ {showTotal ? <span className="text-sm text-muted-foreground">{showTotal(total, [start, end])}</span> : null}
472
+ <div className="inline-flex flex-wrap items-center gap-0.5">{pager}</div>
473
+ {sizeChangerOn ? (
474
+ components?.sizeChanger ? (
475
+ <components.sizeChanger
476
+ disabled={disabled}
477
+ size={resolvedSize}
478
+ value={mergedSize}
479
+ options={sizeOptions}
480
+ className={sizeChangerSelectProps?.className}
481
+ onChange={(next) => goTo(page, next, true)}
482
+ />
483
+ ) : (
484
+ <DefaultSizeChanger
485
+ disabled={disabled}
486
+ size={resolvedSize}
487
+ value={mergedSize}
488
+ options={sizeOptions}
489
+ className={sizeChangerSelectProps?.className}
490
+ selectProps={sizeChangerSelectProps}
491
+ onChange={(next) => goTo(page, next, true)}
492
+ />
493
+ )
494
+ ) : null}
495
+ {jumperLive}
496
+ </nav>
497
+ );
498
+ }
499
+
500
+ function Jumper({
501
+ disabled,
502
+ size,
503
+ pages,
504
+ goButton,
505
+ jumpRaw,
506
+ setJumpRaw,
507
+ onGo,
508
+ }: {
509
+ disabled?: boolean;
510
+ size: CanonicalSize;
511
+ pages: number;
512
+ goButton?: ReactNode;
513
+ jumpRaw: string;
514
+ setJumpRaw: (v: string) => void;
515
+ onGo: (page: number) => void;
516
+ }) {
517
+ const submit = (raw: string) => {
518
+ const n = Number(raw);
519
+ if (!Number.isFinite(n)) return;
520
+ onGo(n);
521
+ setJumpRaw("");
522
+ };
523
+
524
+ return (
525
+ <span className="inline-flex items-center gap-1.5 text-sm text-muted-foreground">
526
+ <span>Go to</span>
527
+ <JumperField disabled={disabled} size={size} width={Math.max(40, String(pages).length * 10 + 18)} value={jumpRaw} onChange={setJumpRaw} onSubmit={submit} />
528
+ <span>Page</span>
529
+ {goButton != null && goButton !== false ? <GoButton goButton={goButton} disabled={disabled} size={size} onGo={() => submit(jumpRaw)} /> : null}
530
+ </span>
531
+ );
532
+ }
533
+
534
+ function JumperField({
535
+ value,
536
+ onChange,
537
+ onSubmit,
538
+ disabled,
539
+ size,
540
+ width,
541
+ }: {
542
+ value: string;
543
+ onChange: (v: string) => void;
544
+ onSubmit: (raw: string) => void;
545
+ disabled?: boolean;
546
+ size: CanonicalSize;
547
+ width: number;
548
+ }) {
549
+ const tok = getSizeTokens(size);
550
+ return (
551
+ <input
552
+ aria-label="Go to page"
553
+ disabled={disabled}
554
+ inputMode="numeric"
555
+ value={value}
556
+ onChange={(e) => onChange(e.target.value)}
557
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
558
+ if (e.key === "Enter") {
559
+ e.preventDefault();
560
+ onSubmit(value);
561
+ }
562
+ }}
563
+ className={cn(
564
+ "text-center tabular-nums text-foreground placeholder:text-quaternary-foreground",
565
+ controlFieldSurface,
566
+ controlFieldTransition,
567
+ controlFieldFocusBorder,
568
+ "focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-45",
569
+ )}
570
+ style={{
571
+ width,
572
+ height: controlHeightVar(size),
573
+ fontSize: tok.fontSize,
574
+ lineHeight: `${tok.lineHeight}px`,
575
+ borderRadius: controlRadiusVar(size),
576
+ paddingLeft: 4,
577
+ paddingRight: 4,
578
+ }}
579
+ />
103
580
  );
104
581
  }