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.
- package/README.md +3 -3
- package/package.json +2 -1
- package/src/app/App.tsx +4 -4
- package/src/button/Button.tsx +1 -1
- package/src/button/ButtonCopy.tsx +11 -21
- package/src/calendar/Calendar.tsx +1 -1
- package/src/chat/AgentPanel.tsx +3 -3
- package/src/chat/index.ts +35 -51
- package/src/chat/message-ui/ChatError.tsx +2 -4
- package/src/chat/message-ui/ChatInput.tsx +5 -9
- package/src/chat/message-ui/ChatThinking.tsx +2 -3
- package/src/chat/message-ui/ChatUserMessage.tsx +4 -5
- package/src/chat/message-ui/ChatWelcome.tsx +3 -8
- package/src/chat/message-ui/MermaidBlock.tsx +7 -47
- package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +8 -11
- package/src/chat/tool-ui/BackgroundTasksBar.tsx +13 -14
- package/src/chat/tool-ui/CallAgentToolUI.tsx +10 -4
- package/src/chat/tool-ui/ChatToolCall.tsx +11 -17
- package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +2 -2
- package/src/chat/tool-ui/ReadSkillToolUI.tsx +3 -3
- package/src/chat/tool-ui/RunJsToolUI.tsx +2 -2
- package/src/chat/tool-ui/ToolUiTrailing.tsx +3 -7
- package/src/chat/tool-ui/WebFetchToolUI.tsx +22 -13
- package/src/checkbox/Checkbox.tsx +1 -1
- package/src/codeblock/CodeBlock.tsx +17 -19
- package/src/colorpicker/ColorPicker.tsx +7 -7
- package/src/datepicker/DatePicker.tsx +3 -3
- package/src/desktop/DesktopHeader.tsx +15 -20
- package/src/desktop/DesktopIcon.tsx +3 -9
- package/src/desktop/DesktopWindow.tsx +54 -30
- package/src/desktop/MeadowDesktop.tsx +1 -1
- package/src/form/Form.tsx +1 -1
- package/src/form/FormItem.tsx +2 -2
- package/src/form/index.ts +23 -24
- package/src/index.ts +167 -199
- package/src/input/Input.tsx +14 -12
- package/src/input/SearchInput.tsx +5 -2
- package/src/lib/sizes.ts +1 -1
- package/src/modal/Modal.tsx +3 -0
- package/src/pagination/Pagination.tsx +528 -51
- package/src/scroll/OverlayScroll.tsx +58 -37
- package/src/select/Select.tsx +2 -2
- package/src/spin/Spin.tsx +14 -17
- package/src/splitter/Splitter.tsx +3 -3
- package/src/styles.css +13 -11
- package/src/switch/Switch.tsx +1 -1
- package/src/table/Table.tsx +43 -18
- package/src/timepicker/TimePicker.tsx +2 -2
|
@@ -1,34 +1,82 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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(
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
167
|
+
const commit = () => {
|
|
168
|
+
onSubmit(text);
|
|
169
|
+
if (!Number.isFinite(Number(text))) setText(value);
|
|
170
|
+
};
|
|
67
171
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
<
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
|
|
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
|
-
<
|
|
90
|
-
|
|
91
|
-
</
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
{
|
|
102
|
-
|
|
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
|
}
|