devnonla-ui 0.1.0
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/LICENSE +21 -0
- package/README.md +140 -0
- package/package.json +66 -0
- package/src/alert/Alert.tsx +109 -0
- package/src/app/App.tsx +36 -0
- package/src/button/Button.tsx +278 -0
- package/src/button/ButtonCopy.tsx +60 -0
- package/src/calendar/Calendar.tsx +133 -0
- package/src/chat/AgentPanel.tsx +203 -0
- package/src/chat/common/bgTasks.ts +41 -0
- package/src/chat/common/sse.ts +156 -0
- package/src/chat/common/types.ts +43 -0
- package/src/chat/common/useAgentStream.ts +351 -0
- package/src/chat/common/utils.ts +73 -0
- package/src/chat/index.ts +56 -0
- package/src/chat/message-ui/ChatAgentMessage.tsx +29 -0
- package/src/chat/message-ui/ChatError.tsx +24 -0
- package/src/chat/message-ui/ChatInput.tsx +162 -0
- package/src/chat/message-ui/ChatMarkdown.tsx +128 -0
- package/src/chat/message-ui/ChatMarkdownTable.tsx +45 -0
- package/src/chat/message-ui/ChatSpinner.tsx +5 -0
- package/src/chat/message-ui/ChatThinking.tsx +61 -0
- package/src/chat/message-ui/ChatUserMessage.tsx +72 -0
- package/src/chat/message-ui/ChatWelcome.tsx +70 -0
- package/src/chat/message-ui/MermaidBlock.tsx +266 -0
- package/src/chat/message-ui/mermaidFence.ts +55 -0
- package/src/chat/message-ui/sanitizeMermaid.ts +135 -0
- package/src/chat/tool-ui/BackgroundTaskToolUI.tsx +36 -0
- package/src/chat/tool-ui/BackgroundTasksBar.tsx +152 -0
- package/src/chat/tool-ui/CallAgentToolUI.tsx +168 -0
- package/src/chat/tool-ui/ChatToolCall.tsx +77 -0
- package/src/chat/tool-ui/GetCurrentTimeToolUI.tsx +49 -0
- package/src/chat/tool-ui/ReadSkillToolUI.tsx +80 -0
- package/src/chat/tool-ui/RunJsToolUI.tsx +106 -0
- package/src/chat/tool-ui/ToolUiBadge.tsx +10 -0
- package/src/chat/tool-ui/ToolUiTrailing.tsx +24 -0
- package/src/chat/tool-ui/WebFetchToolUI.tsx +53 -0
- package/src/chat/tool-ui/registry.ts +29 -0
- package/src/chat/tool-ui/types.ts +22 -0
- package/src/checkbox/Checkbox.tsx +50 -0
- package/src/codeblock/CodeBlock.tsx +76 -0
- package/src/codeblock/highlight.ts +159 -0
- package/src/colorpicker/ColorPicker.tsx +45 -0
- package/src/datepicker/DatePicker.tsx +481 -0
- package/src/desktop/DesktopHeader.tsx +92 -0
- package/src/desktop/DesktopIcon.tsx +34 -0
- package/src/desktop/DesktopStage.tsx +6 -0
- package/src/desktop/DesktopWindow.tsx +504 -0
- package/src/desktop/MeadowDesktop.tsx +15 -0
- package/src/desktop/MeadowShell.tsx +12 -0
- package/src/desktop/MeadowWallpaper.tsx +5 -0
- package/src/desktop/assets/desktop-meadow.jpg +0 -0
- package/src/desktop/assets.d.ts +9 -0
- package/src/desktop/surface.ts +1 -0
- package/src/drawer/Drawer.tsx +125 -0
- package/src/dropdown/ContextMenu.tsx +97 -0
- package/src/dropdown/Dropdown.tsx +139 -0
- package/src/dropdown/menuClasses.ts +9 -0
- package/src/empty/Empty.tsx +47 -0
- package/src/form/Form.tsx +95 -0
- package/src/form/FormItem.tsx +145 -0
- package/src/form/common/context.ts +13 -0
- package/src/form/common/enum.ts +20 -0
- package/src/form/common/rules.ts +68 -0
- package/src/form/common/types.ts +169 -0
- package/src/form/index.ts +35 -0
- package/src/form/partials/FormItemHelps.tsx +31 -0
- package/src/form/variants/FieldCheckbox.tsx +20 -0
- package/src/form/variants/FieldColor.tsx +63 -0
- package/src/form/variants/FieldDateTime.tsx +25 -0
- package/src/form/variants/FieldHidden.tsx +9 -0
- package/src/form/variants/FieldInput.tsx +24 -0
- package/src/form/variants/FieldNumber.tsx +28 -0
- package/src/form/variants/FieldObject.tsx +29 -0
- package/src/form/variants/FieldRadio.tsx +40 -0
- package/src/form/variants/FieldRepeater.tsx +62 -0
- package/src/form/variants/FieldRepeaterItem.tsx +64 -0
- package/src/form/variants/FieldSelect.tsx +27 -0
- package/src/form/variants/FieldSelectMultiple.tsx +32 -0
- package/src/form/variants/FieldSelectRemote.tsx +66 -0
- package/src/form/variants/FieldSwitch.tsx +17 -0
- package/src/form/variants/FieldTextarea.tsx +25 -0
- package/src/form/variants/FieldTime.tsx +25 -0
- package/src/form-layout/FormLayout.tsx +74 -0
- package/src/icon/FluentIcon.tsx +28 -0
- package/src/icon/fluent.ts +75 -0
- package/src/index.ts +214 -0
- package/src/input/Input.tsx +371 -0
- package/src/input/SearchInput.tsx +73 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/placement.ts +44 -0
- package/src/lib/sizes.ts +117 -0
- package/src/lib/surface.ts +8 -0
- package/src/menu/Menu.tsx +98 -0
- package/src/message/message.tsx +214 -0
- package/src/modal/Modal.tsx +277 -0
- package/src/pagination/Pagination.tsx +103 -0
- package/src/popconfirm/Popconfirm.tsx +134 -0
- package/src/popover/Popover.tsx +116 -0
- package/src/scroll/OverlayScroll.tsx +141 -0
- package/src/segmented/Segmented.tsx +67 -0
- package/src/select/Select.tsx +319 -0
- package/src/skeleton/Skeleton.tsx +47 -0
- package/src/spin/Spin.tsx +160 -0
- package/src/styles.css +897 -0
- package/src/switch/Switch.tsx +58 -0
- package/src/table/Table.tsx +387 -0
- package/src/tag/Tag.tsx +109 -0
- package/src/theme.ts +131 -0
- package/src/timepicker/TimePicker.tsx +751 -0
- package/src/tooltip/Tooltip.tsx +59 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode, useState } from "react";
|
|
2
|
+
import { Button } from "../button/Button";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { type PopperPlacement } from "../lib/placement";
|
|
5
|
+
import { Popover } from "../popover/Popover";
|
|
6
|
+
|
|
7
|
+
export type PopconfirmProps = {
|
|
8
|
+
title?: ReactNode;
|
|
9
|
+
description?: ReactNode;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
onConfirm?: () => void | Promise<void>;
|
|
12
|
+
onCancel?: () => void;
|
|
13
|
+
okText?: ReactNode;
|
|
14
|
+
cancelText?: ReactNode;
|
|
15
|
+
okType?: "primary" | "default" | "dashed" | "link" | "text" | "danger";
|
|
16
|
+
okButtonProps?: { danger?: boolean; type?: string; color?: string; variant?: string };
|
|
17
|
+
cancelButtonProps?: Record<string, unknown>;
|
|
18
|
+
/** Customize / hide the leading icon. Pass `null` to hide. */
|
|
19
|
+
icon?: ReactNode | null;
|
|
20
|
+
showCancel?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
open?: boolean;
|
|
23
|
+
onOpenChange?: (open: boolean) => void;
|
|
24
|
+
placement?: PopperPlacement;
|
|
25
|
+
className?: string;
|
|
26
|
+
overlayClassName?: string;
|
|
27
|
+
getPopupContainer?: () => HTMLElement;
|
|
28
|
+
styles?: { root?: CSSProperties };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function WarningIcon() {
|
|
32
|
+
return (
|
|
33
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden className="mt-0.5 shrink-0">
|
|
34
|
+
<circle cx="8" cy="8" r="8" fill="var(--warn)" />
|
|
35
|
+
<path d="M8 4.4V8.6M8 10.7V11.4" stroke="var(--nonla-bg)" strokeWidth="1.7" strokeLinecap="round" />
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function Popconfirm({
|
|
41
|
+
title,
|
|
42
|
+
description,
|
|
43
|
+
children,
|
|
44
|
+
onConfirm,
|
|
45
|
+
onCancel,
|
|
46
|
+
okText = "OK",
|
|
47
|
+
cancelText = "Cancel",
|
|
48
|
+
okType = "primary",
|
|
49
|
+
okButtonProps,
|
|
50
|
+
cancelButtonProps,
|
|
51
|
+
icon,
|
|
52
|
+
showCancel = true,
|
|
53
|
+
disabled,
|
|
54
|
+
open: openProp,
|
|
55
|
+
onOpenChange,
|
|
56
|
+
placement = "top",
|
|
57
|
+
className,
|
|
58
|
+
overlayClassName,
|
|
59
|
+
getPopupContainer,
|
|
60
|
+
styles,
|
|
61
|
+
}: PopconfirmProps) {
|
|
62
|
+
const [innerOpen, setInnerOpen] = useState(false);
|
|
63
|
+
const [loading, setLoading] = useState(false);
|
|
64
|
+
const open = openProp ?? innerOpen;
|
|
65
|
+
const setOpen = (v: boolean) => {
|
|
66
|
+
setInnerOpen(v);
|
|
67
|
+
onOpenChange?.(v);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (disabled) return <>{children}</>;
|
|
71
|
+
|
|
72
|
+
const showIcon = icon !== null;
|
|
73
|
+
const resolvedIcon = icon === undefined ? <WarningIcon /> : icon;
|
|
74
|
+
const okDanger = okType === "danger" || okButtonProps?.danger;
|
|
75
|
+
const okBtnType = okButtonProps?.type ?? (okType === "danger" ? "primary" : okType);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Popover
|
|
79
|
+
open={open}
|
|
80
|
+
onOpenChange={setOpen}
|
|
81
|
+
placement={placement}
|
|
82
|
+
arrow
|
|
83
|
+
overlayClassName={overlayClassName}
|
|
84
|
+
contentClassName={cn("p-3!", className)}
|
|
85
|
+
getPopupContainer={getPopupContainer}
|
|
86
|
+
style={styles?.root}
|
|
87
|
+
content={
|
|
88
|
+
<div className="min-w-48 max-w-72">
|
|
89
|
+
<div className="flex gap-2">
|
|
90
|
+
{showIcon ? <span className="nonla-popconfirm-icon">{resolvedIcon}</span> : null}
|
|
91
|
+
<div className="min-w-0 flex-1">
|
|
92
|
+
{title != null && title !== false ? <div className="text-sm font-medium leading-snug text-foreground">{title}</div> : null}
|
|
93
|
+
{description != null && description !== false ? <div className={cn("text-[13px] leading-snug text-muted-foreground", title != null && title !== false && "mt-1")}>{description}</div> : null}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="mt-3 flex justify-end gap-2">
|
|
97
|
+
{showCancel ? (
|
|
98
|
+
<Button
|
|
99
|
+
size="small"
|
|
100
|
+
type="default"
|
|
101
|
+
{...(cancelButtonProps as object)}
|
|
102
|
+
onClick={() => {
|
|
103
|
+
setOpen(false);
|
|
104
|
+
onCancel?.();
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{cancelText}
|
|
108
|
+
</Button>
|
|
109
|
+
) : null}
|
|
110
|
+
<Button
|
|
111
|
+
size="small"
|
|
112
|
+
type={okBtnType as "primary" | "default" | "dashed" | "link" | "text"}
|
|
113
|
+
danger={okDanger}
|
|
114
|
+
loading={loading}
|
|
115
|
+
onClick={async () => {
|
|
116
|
+
try {
|
|
117
|
+
setLoading(true);
|
|
118
|
+
await onConfirm?.();
|
|
119
|
+
setOpen(false);
|
|
120
|
+
} finally {
|
|
121
|
+
setLoading(false);
|
|
122
|
+
}
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{okText}
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
}
|
|
130
|
+
>
|
|
131
|
+
{children}
|
|
132
|
+
</Popover>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
2
|
+
import { type CSSProperties, type ReactNode, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useAppConfig } from "../app/App";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
import { type PopperPlacement, placementToRadix } from "../lib/placement";
|
|
6
|
+
import { glassOverlayClass } from "../lib/surface";
|
|
7
|
+
|
|
8
|
+
export type PopoverProps = {
|
|
9
|
+
content?: ReactNode;
|
|
10
|
+
title?: ReactNode;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
open?: boolean;
|
|
13
|
+
defaultOpen?: boolean;
|
|
14
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
|
+
trigger?: "click" | "hover";
|
|
16
|
+
placement?: PopperPlacement;
|
|
17
|
+
mouseEnterDelay?: number;
|
|
18
|
+
mouseLeaveDelay?: number;
|
|
19
|
+
getPopupContainer?: () => HTMLElement;
|
|
20
|
+
/** Show arrow pointing at the trigger (antd default for Popconfirm). */
|
|
21
|
+
arrow?: boolean | { pointAtCenter?: boolean };
|
|
22
|
+
className?: string;
|
|
23
|
+
overlayClassName?: string;
|
|
24
|
+
/** Extra class on the content panel (padding / width). */
|
|
25
|
+
contentClassName?: string;
|
|
26
|
+
style?: CSSProperties;
|
|
27
|
+
/** antd `styles` — `root`/`body` map to content panel; `container` accepted as alias. */
|
|
28
|
+
styles?: {
|
|
29
|
+
root?: CSSProperties;
|
|
30
|
+
body?: CSSProperties;
|
|
31
|
+
content?: CSSProperties;
|
|
32
|
+
container?: CSSProperties;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function Popover({
|
|
37
|
+
content,
|
|
38
|
+
title,
|
|
39
|
+
children,
|
|
40
|
+
open: openProp,
|
|
41
|
+
defaultOpen,
|
|
42
|
+
onOpenChange,
|
|
43
|
+
trigger = "click",
|
|
44
|
+
placement = "bottom",
|
|
45
|
+
mouseEnterDelay = 0.1,
|
|
46
|
+
mouseLeaveDelay = 0.1,
|
|
47
|
+
getPopupContainer,
|
|
48
|
+
arrow = false,
|
|
49
|
+
className,
|
|
50
|
+
overlayClassName,
|
|
51
|
+
contentClassName,
|
|
52
|
+
style,
|
|
53
|
+
styles,
|
|
54
|
+
}: PopoverProps) {
|
|
55
|
+
const showArrow = Boolean(arrow);
|
|
56
|
+
const panelStyle = { ...style, ...styles?.root, ...styles?.content, ...styles?.container, ...styles?.body };
|
|
57
|
+
const app = useAppConfig();
|
|
58
|
+
const { side, align } = placementToRadix(placement);
|
|
59
|
+
const hover = trigger === "hover";
|
|
60
|
+
const [innerOpen, setInnerOpen] = useState(defaultOpen ?? false);
|
|
61
|
+
const open = openProp ?? innerOpen;
|
|
62
|
+
const enterTimer = useRef<number>(0);
|
|
63
|
+
const leaveTimer = useRef<number>(0);
|
|
64
|
+
|
|
65
|
+
const setOpen = (v: boolean) => {
|
|
66
|
+
if (openProp === undefined) setInnerOpen(v);
|
|
67
|
+
onOpenChange?.(v);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
useEffect(() => () => {
|
|
71
|
+
window.clearTimeout(enterTimer.current);
|
|
72
|
+
window.clearTimeout(leaveTimer.current);
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const onEnter = () => {
|
|
76
|
+
if (!hover) return;
|
|
77
|
+
window.clearTimeout(leaveTimer.current);
|
|
78
|
+
enterTimer.current = window.setTimeout(() => setOpen(true), mouseEnterDelay * 1000);
|
|
79
|
+
};
|
|
80
|
+
const onLeave = () => {
|
|
81
|
+
if (!hover) return;
|
|
82
|
+
window.clearTimeout(enterTimer.current);
|
|
83
|
+
leaveTimer.current = window.setTimeout(() => setOpen(false), mouseLeaveDelay * 1000);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const container = getPopupContainer ?? app.getPopupContainer;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<PopoverPrimitive.Root
|
|
90
|
+
open={open}
|
|
91
|
+
onOpenChange={(v) => {
|
|
92
|
+
if (hover && v) return;
|
|
93
|
+
setOpen(v);
|
|
94
|
+
}}
|
|
95
|
+
>
|
|
96
|
+
<PopoverPrimitive.Trigger asChild onMouseEnter={onEnter} onMouseLeave={onLeave}>
|
|
97
|
+
{children}
|
|
98
|
+
</PopoverPrimitive.Trigger>
|
|
99
|
+
<PopoverPrimitive.Portal container={container?.()}>
|
|
100
|
+
<PopoverPrimitive.Content
|
|
101
|
+
side={side}
|
|
102
|
+
align={align}
|
|
103
|
+
sideOffset={showArrow ? 8 : 6}
|
|
104
|
+
onMouseEnter={onEnter}
|
|
105
|
+
onMouseLeave={onLeave}
|
|
106
|
+
className={cn("w-max max-w-sm p-3", glassOverlayClass, contentClassName, className, overlayClassName)}
|
|
107
|
+
style={panelStyle}
|
|
108
|
+
>
|
|
109
|
+
{title ? <div className="mb-2 text-sm font-medium">{title}</div> : null}
|
|
110
|
+
{content}
|
|
111
|
+
{showArrow ? <PopoverPrimitive.Arrow width={12} height={6} className="fill-glass drop-shadow-[0_1px_0_var(--glass-border)]" /> : null}
|
|
112
|
+
</PopoverPrimitive.Content>
|
|
113
|
+
</PopoverPrimitive.Portal>
|
|
114
|
+
</PopoverPrimitive.Root>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { type PointerEvent as ReactPointerEvent, type ReactNode, type Ref, type UIEventHandler, useCallback, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type OverlayScrollVisibility = "hover" | "always";
|
|
5
|
+
|
|
6
|
+
export type OverlayScrollProps = {
|
|
7
|
+
/** `hover` shows the thumb on hover / while scrolling. `always` keeps it visible. */
|
|
8
|
+
visibility?: OverlayScrollVisibility;
|
|
9
|
+
className?: string;
|
|
10
|
+
innerClassName?: string;
|
|
11
|
+
children?: ReactNode;
|
|
12
|
+
onScroll?: UIEventHandler<HTMLDivElement>;
|
|
13
|
+
scrollRef?: Ref<HTMLDivElement>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Thumb = { top: number; height: number; shown: boolean };
|
|
17
|
+
|
|
18
|
+
function assignRef(ref: Ref<HTMLDivElement> | undefined, node: HTMLDivElement | null) {
|
|
19
|
+
if (!ref) return;
|
|
20
|
+
if (typeof ref === "function") ref(node);
|
|
21
|
+
else ref.current = node;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function OverlayScroll({ visibility = "hover", className, innerClassName, children, onScroll, scrollRef }: OverlayScrollProps) {
|
|
25
|
+
const elRef = useRef<HTMLDivElement | null>(null);
|
|
26
|
+
const hideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
27
|
+
const thumbRef = useRef<Thumb>({ top: 0, height: 0, shown: false });
|
|
28
|
+
const [scrolling, setScrolling] = useState(false);
|
|
29
|
+
const [dragging, setDragging] = useState(false);
|
|
30
|
+
const [thumb, setThumb] = useState<Thumb>({ top: 0, height: 0, shown: false });
|
|
31
|
+
thumbRef.current = thumb;
|
|
32
|
+
|
|
33
|
+
const updateThumb = useCallback(() => {
|
|
34
|
+
const el = elRef.current;
|
|
35
|
+
if (!el) return;
|
|
36
|
+
const { scrollTop, scrollHeight, clientHeight } = el;
|
|
37
|
+
const overflow = scrollHeight - clientHeight;
|
|
38
|
+
if (overflow <= 1) {
|
|
39
|
+
setThumb((prev) => (prev.shown ? { top: 0, height: 0, shown: false } : prev));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const height = Math.max(32, (clientHeight / scrollHeight) * clientHeight);
|
|
43
|
+
const top = (scrollTop / overflow) * (clientHeight - height);
|
|
44
|
+
setThumb({ top, height, shown: true });
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const setNode = useCallback(
|
|
48
|
+
(node: HTMLDivElement | null) => {
|
|
49
|
+
elRef.current = node;
|
|
50
|
+
assignRef(scrollRef, node);
|
|
51
|
+
if (node) requestAnimationFrame(updateThumb);
|
|
52
|
+
},
|
|
53
|
+
[scrollRef, updateThumb],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
const el = elRef.current;
|
|
58
|
+
if (!el) return;
|
|
59
|
+
updateThumb();
|
|
60
|
+
const ro = new ResizeObserver(updateThumb);
|
|
61
|
+
ro.observe(el);
|
|
62
|
+
const child = el.firstElementChild;
|
|
63
|
+
if (child) ro.observe(child);
|
|
64
|
+
return () => ro.disconnect();
|
|
65
|
+
}, [updateThumb]);
|
|
66
|
+
|
|
67
|
+
useLayoutEffect(() => {
|
|
68
|
+
return () => {
|
|
69
|
+
if (hideTimer.current) clearTimeout(hideTimer.current);
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const handleScroll: UIEventHandler<HTMLDivElement> = (e) => {
|
|
74
|
+
updateThumb();
|
|
75
|
+
if (visibility === "hover" && !dragging) {
|
|
76
|
+
setScrolling(true);
|
|
77
|
+
if (hideTimer.current) clearTimeout(hideTimer.current);
|
|
78
|
+
hideTimer.current = setTimeout(() => setScrolling(false), 700);
|
|
79
|
+
}
|
|
80
|
+
onScroll?.(e);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const onThumbPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
|
|
84
|
+
const el = elRef.current;
|
|
85
|
+
const current = thumbRef.current;
|
|
86
|
+
if (!el || !current.shown || e.button !== 0) return;
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
e.stopPropagation();
|
|
89
|
+
|
|
90
|
+
const overflow = el.scrollHeight - el.clientHeight;
|
|
91
|
+
const track = el.clientHeight - current.height;
|
|
92
|
+
if (overflow <= 0 || track <= 0) return;
|
|
93
|
+
|
|
94
|
+
const startY = e.clientY;
|
|
95
|
+
const startTop = current.top;
|
|
96
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
97
|
+
setDragging(true);
|
|
98
|
+
setScrolling(true);
|
|
99
|
+
|
|
100
|
+
const onMove = (ev: PointerEvent) => {
|
|
101
|
+
if (ev.pointerId !== e.pointerId) return;
|
|
102
|
+
const nextTop = Math.min(track, Math.max(0, startTop + (ev.clientY - startY)));
|
|
103
|
+
el.scrollTop = (nextTop / track) * overflow;
|
|
104
|
+
};
|
|
105
|
+
const onUp = (ev: PointerEvent) => {
|
|
106
|
+
if (ev.pointerId !== e.pointerId) return;
|
|
107
|
+
window.removeEventListener("pointermove", onMove, true);
|
|
108
|
+
window.removeEventListener("pointerup", onUp, true);
|
|
109
|
+
window.removeEventListener("pointercancel", onUp, true);
|
|
110
|
+
setDragging(false);
|
|
111
|
+
if (hideTimer.current) clearTimeout(hideTimer.current);
|
|
112
|
+
hideTimer.current = setTimeout(() => setScrolling(false), 700);
|
|
113
|
+
};
|
|
114
|
+
window.addEventListener("pointermove", onMove, true);
|
|
115
|
+
window.addEventListener("pointerup", onUp, true);
|
|
116
|
+
window.addEventListener("pointercancel", onUp, true);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const thumbVisible = visibility === "always" || dragging || scrolling;
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<div className={cn("group/scroll relative min-h-0 min-w-0 w-full overflow-hidden", className)} data-scrolling={thumbVisible ? "true" : undefined}>
|
|
123
|
+
<div ref={setNode} onScroll={handleScroll} className={cn("nonla-scroll-hidden absolute inset-0 overflow-y-auto overflow-x-hidden [overflow-anchor:none]", innerClassName)}>
|
|
124
|
+
{children}
|
|
125
|
+
</div>
|
|
126
|
+
{thumb.shown ? (
|
|
127
|
+
<div
|
|
128
|
+
aria-hidden
|
|
129
|
+
onPointerDown={onThumbPointerDown}
|
|
130
|
+
className={cn(
|
|
131
|
+
"nonla-overlay-thumb absolute z-20 rounded-full transition-opacity duration-150",
|
|
132
|
+
visibility === "always" || dragging
|
|
133
|
+
? "pointer-events-auto opacity-100"
|
|
134
|
+
: "pointer-events-none opacity-0 group-hover/scroll:pointer-events-auto group-hover/scroll:opacity-100 group-data-[scrolling]/scroll:pointer-events-auto group-data-[scrolling]/scroll:opacity-100",
|
|
135
|
+
)}
|
|
136
|
+
style={{ right: 2, width: "var(--nonla-scrollbar-size)", height: thumb.height, transform: `translateY(${thumb.top}px)` }}
|
|
137
|
+
/>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
import { type ControlSize, controlHeightVar, controlRadiusVar, getSizeTokens, normalizeSize } from "../lib/sizes";
|
|
4
|
+
|
|
5
|
+
export type SegmentedOption<V extends string | number = string | number> = V | { label: ReactNode; value: V; disabled?: boolean; icon?: ReactNode };
|
|
6
|
+
|
|
7
|
+
export type SegmentedProps<V extends string | number = string | number> = {
|
|
8
|
+
options: SegmentedOption<V>[];
|
|
9
|
+
value?: V;
|
|
10
|
+
defaultValue?: V;
|
|
11
|
+
onChange?: (value: V) => void;
|
|
12
|
+
size?: ControlSize;
|
|
13
|
+
block?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function norm<V extends string | number>(opt: SegmentedOption<V>): { label: ReactNode; value: V; disabled?: boolean; icon?: ReactNode } {
|
|
19
|
+
if (typeof opt === "string" || typeof opt === "number") return { label: String(opt), value: opt };
|
|
20
|
+
return opt;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function Segmented<V extends string | number = string | number>({ options, value, defaultValue, onChange, size, block, disabled, className }: SegmentedProps<V>) {
|
|
24
|
+
const items = options.map((o) => norm(o));
|
|
25
|
+
const current = value ?? defaultValue ?? items[0]?.value;
|
|
26
|
+
const tok = getSizeTokens(size);
|
|
27
|
+
const itemPadX = Math.max(tok.paddingInline - 4, 6);
|
|
28
|
+
const radius = controlRadiusVar(size);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className={cn("inline-flex w-fit max-w-full shrink-0 items-center gap-0.5 rounded-lg bg-brand-50 p-0.5", block && "flex w-full", className)}
|
|
33
|
+
role="tablist"
|
|
34
|
+
style={{ borderRadius: radius }}
|
|
35
|
+
data-size={normalizeSize(size)}
|
|
36
|
+
>
|
|
37
|
+
{items.map((item) => {
|
|
38
|
+
const active = item.value === current;
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
key={String(item.value)}
|
|
42
|
+
type="button"
|
|
43
|
+
role="tab"
|
|
44
|
+
aria-selected={active}
|
|
45
|
+
disabled={disabled || item.disabled}
|
|
46
|
+
className={cn(
|
|
47
|
+
"inline-flex cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap border-0 transition-colors focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-45",
|
|
48
|
+
block ? "min-w-0 flex-1" : "shrink-0",
|
|
49
|
+
active ? "bg-brand-200 text-foreground" : "bg-transparent text-foreground hover:bg-brand-100",
|
|
50
|
+
)}
|
|
51
|
+
style={{
|
|
52
|
+
height: `max(22px, calc(${controlHeightVar(size)} - 4px))`,
|
|
53
|
+
paddingLeft: itemPadX,
|
|
54
|
+
paddingRight: itemPadX,
|
|
55
|
+
fontSize: tok.fontSize,
|
|
56
|
+
borderRadius: `max(4px, calc(${radius} - 2px))`,
|
|
57
|
+
}}
|
|
58
|
+
onClick={() => onChange?.(item.value)}
|
|
59
|
+
>
|
|
60
|
+
{item.icon}
|
|
61
|
+
{item.label}
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
})}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|