sparkdesign 0.0.1 → 0.1.10
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 +188 -4
- package/dist/commands/add.js +93 -0
- package/dist/commands/diff.js +54 -0
- package/dist/commands/init.js +96 -0
- package/dist/commands/list.js +25 -0
- package/dist/index.js +37 -0
- package/dist/utils/config.js +53 -0
- package/dist/utils/registry.js +34 -0
- package/dist/utils/tokens.js +176 -0
- package/dist/utils/transform.js +19 -0
- package/package.json +33 -10
- package/registry/__tests__/basic/button.test.tsx +333 -0
- package/registry/__tests__/chat/markdown.test.tsx +387 -0
- package/registry/__tests__/chat/thinking-indicator.test.tsx +244 -0
- package/registry/__tests__/chat/tool-invocation-card.test.tsx +346 -0
- package/registry/basic/alert-dialog.tsx +180 -0
- package/registry/basic/avatar.tsx +120 -0
- package/registry/basic/button.tsx +100 -0
- package/registry/basic/collapse.tsx +94 -0
- package/registry/basic/collapsible-card.tsx +230 -0
- package/registry/basic/collapsible.tsx +21 -0
- package/registry/basic/dropdown-menu.tsx +254 -0
- package/registry/basic/icon-button.tsx +66 -0
- package/registry/basic/icons-inline.tsx +206 -0
- package/registry/basic/kbd.tsx +50 -0
- package/registry/basic/option-list.tsx +125 -0
- package/registry/basic/pagination.tsx +132 -0
- package/registry/basic/progress.tsx +42 -0
- package/registry/basic/radio-group.tsx +69 -0
- package/registry/basic/resizable.tsx +67 -0
- package/registry/basic/scrollbar.tsx +114 -0
- package/registry/basic/select.tsx +177 -0
- package/registry/basic/shimmering-text.tsx +115 -0
- package/registry/basic/sidebar-menu.tsx +177 -0
- package/registry/basic/skeleton.tsx +33 -0
- package/registry/basic/slider.tsx +55 -0
- package/registry/basic/sonner.tsx +104 -0
- package/registry/basic/spinner.tsx +17 -0
- package/registry/basic/switch.tsx +49 -0
- package/registry/basic/table.tsx +117 -0
- package/registry/basic/tabs.tsx +85 -0
- package/registry/basic/tag.tsx +161 -0
- package/registry/basic/theme-from-document.ts +10 -0
- package/registry/basic/toggle.tsx +223 -0
- package/registry/basic/tooltip.tsx +80 -0
- package/registry/basic/typography.tsx +201 -0
- package/registry/chat/ask-user-part.tsx +70 -0
- package/registry/chat/browser-action-part.tsx +166 -0
- package/registry/chat/chat-input/chat-input-folder-selector.tsx +185 -0
- package/registry/chat/chat-input/chat-input-model-switcher.tsx +131 -0
- package/registry/chat/chat-input/chat-input-textarea.tsx +67 -0
- package/registry/chat/chat-input/compound.tsx +334 -0
- package/registry/chat/chat-input/context.tsx +189 -0
- package/registry/chat/chat-input/folder-permission-dialog.tsx +61 -0
- package/registry/chat/chat-input/index.tsx +123 -0
- package/registry/chat/chat-input/types.ts +77 -0
- package/registry/chat/chat-input/useAutoResizeTextarea.ts +20 -0
- package/registry/chat/code-block-part.tsx +151 -0
- package/registry/chat/file-attachment.tsx +44 -0
- package/registry/chat/file-card.tsx +68 -0
- package/registry/chat/file-review-part.tsx +259 -0
- package/registry/chat/folder-button.tsx +169 -0
- package/registry/chat/generated-images-grid.tsx +56 -0
- package/registry/chat/generation-status-bar.tsx +72 -0
- package/registry/chat/hint-banner.tsx +165 -0
- package/registry/chat/image-attachment.tsx +166 -0
- package/registry/chat/image-generating.tsx +281 -0
- package/registry/chat/markdown.tsx +146 -0
- package/registry/chat/mermaid-part.tsx +90 -0
- package/registry/chat/permission-card.tsx +178 -0
- package/registry/chat/plan-part.tsx +168 -0
- package/registry/chat/queue-indicator.tsx +234 -0
- package/registry/chat/reasoning-step/compound.tsx +336 -0
- package/registry/chat/reasoning-step/context.tsx +114 -0
- package/registry/chat/reasoning-step/index.tsx +45 -0
- package/registry/chat/reasoning-step/types.ts +109 -0
- package/registry/chat/related-prompts.tsx +91 -0
- package/registry/chat/response/compound.tsx +210 -0
- package/registry/chat/response/context.tsx +200 -0
- package/registry/chat/response/index.tsx +87 -0
- package/registry/chat/response/types.ts +123 -0
- package/registry/chat/send-button.tsx +94 -0
- package/registry/chat/streaming-markdown-block.tsx +111 -0
- package/registry/chat/task-part.tsx +109 -0
- package/registry/chat/terminal-code-block-part.tsx +69 -0
- package/registry/chat/thinking-indicator.tsx +91 -0
- package/registry/chat/tool-invocation-card.tsx +132 -0
- package/registry/chat/user-message.tsx +38 -0
- package/registry/chat/user-question/UserQuestionCard.tsx +198 -0
- package/registry/chat/user-question/UserQuestionFooter.tsx +66 -0
- package/registry/chat/user-question/UserQuestionHeader.tsx +64 -0
- package/registry/chat/user-question/compound.tsx +324 -0
- package/registry/chat/user-question/context.tsx +456 -0
- package/registry/chat/user-question/index.tsx +95 -0
- package/registry/chat/user-question/types.ts +61 -0
- package/registry/chat/user-question/useUserQuestionKeyboard.ts +126 -0
- package/registry/chat/user-question/useUserQuestionState.ts +165 -0
- package/registry/chat/user-question-answer.tsx +62 -0
- package/registry/lib/file-icon-maps.ts +150 -0
- package/registry/lib/use-mermaid-render.ts +76 -0
- package/registry/lib/utils.ts +6 -0
- package/registry/meta.json +1 -0
- package/registry/tokens/index.css +31 -0
- package/registry/tokens/scale/computed.css +103 -0
- package/registry/tokens/scale/config.css +110 -0
- package/registry/tokens/scale/index.css +30 -0
- package/registry/tokens/scale/presets/compact.css +30 -0
- package/registry/tokens/scale/presets/dense.css +64 -0
- package/registry/tokens/scale/presets/sharp.css +40 -0
- package/registry/tokens/scale/presets/soft.css +16 -0
- package/registry/tokens/scale.css +13 -0
- package/registry/tokens/scrollbar-utility.css +35 -0
- package/registry/tokens/theme.css +633 -0
- package/registry/tokens/themes/dark-parchment.css +132 -0
- package/registry/tokens/themes/dark-qoder.css +132 -0
- package/registry/tokens/themes/light-parchment.css +123 -0
- package/registry/tokens/themes/light-qoder.css +131 -0
- package/index.js +0 -5
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as SelectPrimitive from '@radix-ui/react-select'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { ArrowDownSLine, ArrowUpLine, CheckLine } from './icons-inline'
|
|
5
|
+
import { getThemeFromDocument } from './theme-from-document'
|
|
6
|
+
|
|
7
|
+
/** 取消 outline/ring,避免与全局 *:focus-visible 冲突产生偶现 focus 环;用 !important 压过全局 */
|
|
8
|
+
const FOCUS_RESET =
|
|
9
|
+
'outline-none focus:outline-none focus:ring-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:!outline-none focus-visible:!ring-0 focus-visible:outline-offset-0 focus-visible:!outline-offset-0 focus-visible:ring-offset-0 focus-visible:!ring-offset-0'
|
|
10
|
+
|
|
11
|
+
const Select = SelectPrimitive.Root
|
|
12
|
+
const SelectGroup = SelectPrimitive.Group
|
|
13
|
+
const SelectValue = SelectPrimitive.Value
|
|
14
|
+
|
|
15
|
+
export interface SelectTriggerProps
|
|
16
|
+
extends React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {
|
|
17
|
+
triggerIcon?: React.ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SelectTrigger = React.forwardRef<
|
|
21
|
+
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
22
|
+
SelectTriggerProps
|
|
23
|
+
>(({ className, children, disabled, triggerIcon, ...props }, ref) => (
|
|
24
|
+
<SelectPrimitive.Trigger
|
|
25
|
+
ref={ref}
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
className={cn(
|
|
28
|
+
'flex h-9 w-full items-center justify-between gap-2 whitespace-nowrap rounded-lg border border-border-tertiary bg-bg-container px-3 py-2 text-sm text-text placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
|
|
29
|
+
FOCUS_RESET,
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
<SelectPrimitive.Icon asChild>
|
|
36
|
+
<span className="pointer-events-none flex shrink-0 text-current [&>svg]:text-current">
|
|
37
|
+
{triggerIcon ?? <ArrowDownSLine className="h-[1em] w-[1em] shrink-0" />}
|
|
38
|
+
</span>
|
|
39
|
+
</SelectPrimitive.Icon>
|
|
40
|
+
</SelectPrimitive.Trigger>
|
|
41
|
+
))
|
|
42
|
+
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
43
|
+
|
|
44
|
+
const SelectScrollUpButton = React.forwardRef<
|
|
45
|
+
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
46
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> & { scrollUpIcon?: React.ReactNode }
|
|
47
|
+
>(({ className, scrollUpIcon, ...rest }, ref) => (
|
|
48
|
+
<SelectPrimitive.ScrollUpButton
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn('flex cursor-default items-center justify-center py-1', FOCUS_RESET, className)}
|
|
51
|
+
{...rest}
|
|
52
|
+
>
|
|
53
|
+
{scrollUpIcon ?? <ArrowUpLine className="h-[1em] w-[1em]" />}
|
|
54
|
+
</SelectPrimitive.ScrollUpButton>
|
|
55
|
+
))
|
|
56
|
+
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
57
|
+
|
|
58
|
+
const SelectScrollDownButton = React.forwardRef<
|
|
59
|
+
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
60
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> & { scrollDownIcon?: React.ReactNode }
|
|
61
|
+
>(({ className, scrollDownIcon, ...rest }, ref) => (
|
|
62
|
+
<SelectPrimitive.ScrollDownButton
|
|
63
|
+
ref={ref}
|
|
64
|
+
className={cn('flex cursor-default items-center justify-center py-1', FOCUS_RESET, className)}
|
|
65
|
+
{...rest}
|
|
66
|
+
>
|
|
67
|
+
{scrollDownIcon ?? <ArrowDownSLine className="h-[1em] w-[1em]" />}
|
|
68
|
+
</SelectPrimitive.ScrollDownButton>
|
|
69
|
+
))
|
|
70
|
+
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
|
71
|
+
|
|
72
|
+
const SelectContent = React.forwardRef<
|
|
73
|
+
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
74
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> & {
|
|
75
|
+
/** When provided (e.g. from ThemeStyleProvider), used for portal wrapper */
|
|
76
|
+
dataStyle?: string
|
|
77
|
+
dataTheme?: string
|
|
78
|
+
}
|
|
79
|
+
>(({ className, children, position = 'popper', sideOffset = 4, dataStyle, dataTheme, ...props }, ref) => {
|
|
80
|
+
const fromDoc = getThemeFromDocument()
|
|
81
|
+
const dataProps =
|
|
82
|
+
dataStyle !== undefined
|
|
83
|
+
? { 'data-style': dataStyle, 'data-theme': dataTheme ?? fromDoc['data-theme'] ?? '' }
|
|
84
|
+
: fromDoc
|
|
85
|
+
return (
|
|
86
|
+
<SelectPrimitive.Portal>
|
|
87
|
+
<SelectPrimitive.Content
|
|
88
|
+
ref={ref}
|
|
89
|
+
className={cn(
|
|
90
|
+
'relative z-[60] max-h-96 min-w-32 overflow-hidden rounded-lg border border-border-tertiary bg-bg-container p-1 text-text shadow-lg',
|
|
91
|
+
FOCUS_RESET,
|
|
92
|
+
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
93
|
+
className
|
|
94
|
+
)}
|
|
95
|
+
position={position}
|
|
96
|
+
sideOffset={sideOffset}
|
|
97
|
+
{...dataProps}
|
|
98
|
+
{...props}
|
|
99
|
+
>
|
|
100
|
+
<SelectScrollUpButton />
|
|
101
|
+
<SelectPrimitive.Viewport
|
|
102
|
+
className={cn(
|
|
103
|
+
FOCUS_RESET,
|
|
104
|
+
position === 'popper' &&
|
|
105
|
+
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
{children}
|
|
109
|
+
</SelectPrimitive.Viewport>
|
|
110
|
+
<SelectScrollDownButton />
|
|
111
|
+
</SelectPrimitive.Content>
|
|
112
|
+
</SelectPrimitive.Portal>
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
116
|
+
|
|
117
|
+
const SelectLabel = React.forwardRef<
|
|
118
|
+
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
119
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
120
|
+
>(({ className, ...props }, ref) => (
|
|
121
|
+
<SelectPrimitive.Label
|
|
122
|
+
ref={ref}
|
|
123
|
+
className={cn('flex h-9 items-center px-3 text-sm font-semibold text-text-secondary', className)}
|
|
124
|
+
{...props}
|
|
125
|
+
/>
|
|
126
|
+
))
|
|
127
|
+
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
128
|
+
|
|
129
|
+
const SelectItem = React.forwardRef<
|
|
130
|
+
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
131
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> & { itemIndicatorIcon?: React.ReactNode }
|
|
132
|
+
>(({ className, children, itemIndicatorIcon, ...rest }, ref) => (
|
|
133
|
+
<SelectPrimitive.Item
|
|
134
|
+
ref={ref}
|
|
135
|
+
className={cn(
|
|
136
|
+
'relative flex h-9 w-full min-w-0 cursor-pointer select-none items-center gap-2 rounded-sm pl-3 pr-8 text-sm transition-colors',
|
|
137
|
+
FOCUS_RESET,
|
|
138
|
+
'hover:bg-fill-secondary data-[highlighted]:bg-fill-secondary data-[highlighted]:text-text data-[state=checked]:bg-fill-secondary data-[state=checked]:text-text',
|
|
139
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
140
|
+
className
|
|
141
|
+
)}
|
|
142
|
+
{...rest}
|
|
143
|
+
>
|
|
144
|
+
<span className="absolute right-3 flex h-[1em] w-[1em] items-center justify-center">
|
|
145
|
+
<SelectPrimitive.ItemIndicator>
|
|
146
|
+
{itemIndicatorIcon ?? <CheckLine className="h-[1em] w-[1em]" />}
|
|
147
|
+
</SelectPrimitive.ItemIndicator>
|
|
148
|
+
</span>
|
|
149
|
+
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
150
|
+
</SelectPrimitive.Item>
|
|
151
|
+
))
|
|
152
|
+
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
153
|
+
|
|
154
|
+
const SelectSeparator = React.forwardRef<
|
|
155
|
+
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
156
|
+
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
157
|
+
>(({ className, ...props }, ref) => (
|
|
158
|
+
<SelectPrimitive.Separator
|
|
159
|
+
ref={ref}
|
|
160
|
+
className={cn('my-1 mx-1 h-px bg-border-tertiary', className)}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
))
|
|
164
|
+
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|
165
|
+
|
|
166
|
+
export {
|
|
167
|
+
Select,
|
|
168
|
+
SelectGroup,
|
|
169
|
+
SelectValue,
|
|
170
|
+
SelectTrigger,
|
|
171
|
+
SelectContent,
|
|
172
|
+
SelectLabel,
|
|
173
|
+
SelectItem,
|
|
174
|
+
SelectSeparator,
|
|
175
|
+
SelectScrollUpButton,
|
|
176
|
+
SelectScrollDownButton,
|
|
177
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useMemo, forwardRef, useRef } from 'react'
|
|
2
|
+
import { motion, useInView, type HTMLMotionProps } from 'framer-motion'
|
|
3
|
+
import { cn } from '../lib/utils'
|
|
4
|
+
|
|
5
|
+
export interface ShimmeringTextProps extends Omit<HTMLMotionProps<'span'>, 'children'> {
|
|
6
|
+
/** Required. Text to display with shimmer effect */
|
|
7
|
+
text: string
|
|
8
|
+
/** Animation duration in seconds. Default: 2 */
|
|
9
|
+
duration?: number
|
|
10
|
+
/** Delay before starting animation in seconds. Default: 0 */
|
|
11
|
+
delay?: number
|
|
12
|
+
/** Whether to repeat the animation. Default: true */
|
|
13
|
+
repeat?: boolean
|
|
14
|
+
/** Pause duration between repeats in seconds. Default: 0.5 */
|
|
15
|
+
repeatDelay?: number
|
|
16
|
+
/** Whether to start animation when entering viewport. Default: true */
|
|
17
|
+
startOnView?: boolean
|
|
18
|
+
/** Whether to animate only once. Default: false */
|
|
19
|
+
once?: boolean
|
|
20
|
+
/** Margin for viewport detection (e.g., "0px 0px -10%"). Default: undefined */
|
|
21
|
+
inViewMargin?: string
|
|
22
|
+
/** Shimmer spread multiplier. Default: 2 */
|
|
23
|
+
spread?: number
|
|
24
|
+
/** Base text color (CSS color value) */
|
|
25
|
+
color?: string
|
|
26
|
+
/** Shimmer gradient color (CSS color value) */
|
|
27
|
+
shimmerColor?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ShimmeringText = forwardRef<HTMLSpanElement, ShimmeringTextProps>(
|
|
31
|
+
(
|
|
32
|
+
{
|
|
33
|
+
text,
|
|
34
|
+
duration = 2,
|
|
35
|
+
delay = 0,
|
|
36
|
+
repeat = true,
|
|
37
|
+
repeatDelay = 0.5,
|
|
38
|
+
startOnView = true,
|
|
39
|
+
once = false,
|
|
40
|
+
inViewMargin,
|
|
41
|
+
spread = 2,
|
|
42
|
+
color,
|
|
43
|
+
shimmerColor,
|
|
44
|
+
className,
|
|
45
|
+
style,
|
|
46
|
+
...props
|
|
47
|
+
},
|
|
48
|
+
ref
|
|
49
|
+
) => {
|
|
50
|
+
const containerRef = useRef<HTMLSpanElement>(null)
|
|
51
|
+
const isInView = useInView(containerRef, {
|
|
52
|
+
once: once,
|
|
53
|
+
margin: inViewMargin as `${number}px ${number}px ${number}px ${number}px` | undefined,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const shouldAnimate = startOnView ? isInView : true
|
|
57
|
+
|
|
58
|
+
const dynamicSpread = useMemo(() => {
|
|
59
|
+
return text.length * spread
|
|
60
|
+
}, [text, spread])
|
|
61
|
+
|
|
62
|
+
const baseColor = color || 'var(--color-text-secondary)'
|
|
63
|
+
const highlightColor = shimmerColor || 'var(--color-text)'
|
|
64
|
+
|
|
65
|
+
const backgroundStyle = useMemo(() => {
|
|
66
|
+
return {
|
|
67
|
+
backgroundImage: `linear-gradient(90deg, transparent calc(50% - ${dynamicSpread}px), ${highlightColor}, transparent calc(50% + ${dynamicSpread}px)), linear-gradient(${baseColor}, ${baseColor})`,
|
|
68
|
+
}
|
|
69
|
+
}, [dynamicSpread, highlightColor, baseColor])
|
|
70
|
+
|
|
71
|
+
const combinedRef = (node: HTMLSpanElement | null) => {
|
|
72
|
+
;(containerRef as React.MutableRefObject<HTMLSpanElement | null>).current = node
|
|
73
|
+
if (typeof ref === 'function') {
|
|
74
|
+
ref(node)
|
|
75
|
+
} else if (ref) {
|
|
76
|
+
ref.current = node
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<motion.span
|
|
82
|
+
ref={combinedRef}
|
|
83
|
+
className={cn(
|
|
84
|
+
'relative inline-block bg-clip-text text-transparent',
|
|
85
|
+
'bg-[length:250%_100%,auto] bg-no-repeat',
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
style={{
|
|
89
|
+
...backgroundStyle,
|
|
90
|
+
...style,
|
|
91
|
+
}}
|
|
92
|
+
initial={{ backgroundPosition: '100% center' }}
|
|
93
|
+
animate={
|
|
94
|
+
shouldAnimate
|
|
95
|
+
? { backgroundPosition: '0% center' }
|
|
96
|
+
: { backgroundPosition: '100% center' }
|
|
97
|
+
}
|
|
98
|
+
transition={{
|
|
99
|
+
repeat: repeat && !once ? Infinity : 0,
|
|
100
|
+
repeatDelay: repeatDelay,
|
|
101
|
+
duration: duration,
|
|
102
|
+
delay: delay,
|
|
103
|
+
ease: 'linear',
|
|
104
|
+
}}
|
|
105
|
+
{...props}
|
|
106
|
+
>
|
|
107
|
+
{text}
|
|
108
|
+
</motion.span>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
ShimmeringText.displayName = 'ShimmeringText'
|
|
114
|
+
|
|
115
|
+
export { ShimmeringText }
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { forwardRef, useState } from 'react'
|
|
2
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from './dropdown-menu'
|
|
10
|
+
import { MoreLine } from './icons-inline'
|
|
11
|
+
|
|
12
|
+
export interface SidebarMenuItemAction {
|
|
13
|
+
id: string
|
|
14
|
+
label: string
|
|
15
|
+
icon?: ReactNode
|
|
16
|
+
onClick?: () => void
|
|
17
|
+
destructive?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SidebarMenuItem {
|
|
21
|
+
id: string
|
|
22
|
+
label: string
|
|
23
|
+
icon?: ReactNode
|
|
24
|
+
disabled?: boolean
|
|
25
|
+
actions?: SidebarMenuItemAction[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SidebarMenuProps extends HTMLAttributes<HTMLDivElement> {
|
|
29
|
+
items: SidebarMenuItem[]
|
|
30
|
+
selectedId?: string
|
|
31
|
+
onItemClick?: (item: SidebarMenuItem) => void
|
|
32
|
+
width?: string
|
|
33
|
+
moreIcon?: ReactNode
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const moreClass = 'w-4 h-4'
|
|
37
|
+
|
|
38
|
+
export const SidebarMenu = forwardRef<HTMLDivElement, SidebarMenuProps>(
|
|
39
|
+
(
|
|
40
|
+
{
|
|
41
|
+
items,
|
|
42
|
+
selectedId,
|
|
43
|
+
onItemClick,
|
|
44
|
+
width = 'w-52',
|
|
45
|
+
className,
|
|
46
|
+
moreIcon,
|
|
47
|
+
...props
|
|
48
|
+
},
|
|
49
|
+
ref
|
|
50
|
+
) => {
|
|
51
|
+
const [openDropdownId, setOpenDropdownId] = useState<string | null>(null)
|
|
52
|
+
const baseStyles = 'flex flex-col gap-1.5 rounded-md bg-transparent'
|
|
53
|
+
const defaultMore = moreIcon ?? <MoreLine className={moreClass} />
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div ref={ref} className={cn(baseStyles, width, className)} {...props}>
|
|
57
|
+
{items.map((item) => {
|
|
58
|
+
const isSelected = selectedId === item.id
|
|
59
|
+
const isDisabled = item.disabled
|
|
60
|
+
const isOpen = openDropdownId === item.id
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div
|
|
64
|
+
key={item.id}
|
|
65
|
+
className={cn(
|
|
66
|
+
'group flex items-center gap-2 py-1 px-2 rounded-md text-sm leading-sm transition-colors',
|
|
67
|
+
'focus-within:outline-none min-w-0',
|
|
68
|
+
isDisabled && 'opacity-50 cursor-not-allowed',
|
|
69
|
+
!isDisabled && 'cursor-pointer',
|
|
70
|
+
isSelected
|
|
71
|
+
? 'bg-fill-secondary text-text'
|
|
72
|
+
: 'bg-transparent text-text-secondary hover:bg-fill-secondary hover:text-text'
|
|
73
|
+
)}
|
|
74
|
+
>
|
|
75
|
+
<button
|
|
76
|
+
onClick={() => {
|
|
77
|
+
if (!isDisabled) onItemClick?.(item)
|
|
78
|
+
setOpenDropdownId(null)
|
|
79
|
+
}}
|
|
80
|
+
disabled={isDisabled}
|
|
81
|
+
className="flex-1 text-left truncate min-w-0 outline-none bg-transparent border-none p-0 cursor-pointer"
|
|
82
|
+
>
|
|
83
|
+
{item.label}
|
|
84
|
+
</button>
|
|
85
|
+
{item.actions && item.actions.length > 0 ? (
|
|
86
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
87
|
+
<DropdownMenu
|
|
88
|
+
open={isOpen}
|
|
89
|
+
onOpenChange={(open) => {
|
|
90
|
+
setOpenDropdownId(open ? item.id : null)
|
|
91
|
+
}}
|
|
92
|
+
>
|
|
93
|
+
<DropdownMenuTrigger asChild>
|
|
94
|
+
<button
|
|
95
|
+
className={cn(
|
|
96
|
+
'flex items-center justify-center p-1 rounded outline-none transition-all duration-200 relative z-10 cursor-pointer hover:bg-fill-primary'
|
|
97
|
+
)}
|
|
98
|
+
style={{ pointerEvents: 'auto' }}
|
|
99
|
+
onClick={(e) => e.stopPropagation()}
|
|
100
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
101
|
+
>
|
|
102
|
+
<span
|
|
103
|
+
className={cn(
|
|
104
|
+
'transition-opacity pointer-events-none flex items-center justify-center',
|
|
105
|
+
isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
{item.icon ?? (
|
|
109
|
+
<span
|
|
110
|
+
className={cn(
|
|
111
|
+
'w-4 h-4 inline-flex items-center justify-center',
|
|
112
|
+
isSelected ? 'text-text' : 'text-text-secondary'
|
|
113
|
+
)}
|
|
114
|
+
>
|
|
115
|
+
{defaultMore}
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
</span>
|
|
119
|
+
</button>
|
|
120
|
+
</DropdownMenuTrigger>
|
|
121
|
+
<DropdownMenuContent align="end">
|
|
122
|
+
{item.actions.map((action) => (
|
|
123
|
+
<DropdownMenuItem
|
|
124
|
+
key={action.id}
|
|
125
|
+
variant={action.destructive ? 'destructive' : 'default'}
|
|
126
|
+
onClick={(e) => {
|
|
127
|
+
e.stopPropagation()
|
|
128
|
+
setOpenDropdownId(null)
|
|
129
|
+
action.onClick?.()
|
|
130
|
+
}}
|
|
131
|
+
>
|
|
132
|
+
{action.icon && (
|
|
133
|
+
<span className="mr-2 w-4 h-4 flex items-center justify-center">
|
|
134
|
+
{action.icon}
|
|
135
|
+
</span>
|
|
136
|
+
)}
|
|
137
|
+
{action.label}
|
|
138
|
+
</DropdownMenuItem>
|
|
139
|
+
))}
|
|
140
|
+
</DropdownMenuContent>
|
|
141
|
+
</DropdownMenu>
|
|
142
|
+
</div>
|
|
143
|
+
) : (
|
|
144
|
+
<span
|
|
145
|
+
className="flex-shrink-0 w-4 h-4 flex items-center justify-center"
|
|
146
|
+
onClick={(e) => e.stopPropagation()}
|
|
147
|
+
>
|
|
148
|
+
{item.icon ? (
|
|
149
|
+
<span
|
|
150
|
+
className={cn(
|
|
151
|
+
'transition-opacity pointer-events-none',
|
|
152
|
+
isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
{item.icon}
|
|
156
|
+
</span>
|
|
157
|
+
) : (
|
|
158
|
+
<span
|
|
159
|
+
className={cn(
|
|
160
|
+
'w-4 h-4 inline-flex items-center justify-center transition-opacity pointer-events-none',
|
|
161
|
+
isSelected ? 'opacity-100 text-text' : 'opacity-0 group-hover:opacity-100 text-text-secondary'
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
{defaultMore}
|
|
165
|
+
</span>
|
|
166
|
+
)}
|
|
167
|
+
</span>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
})}
|
|
172
|
+
</div>
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
SidebarMenu.displayName = 'SidebarMenu'
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
import type { HTMLAttributes } from 'react'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
const skeletonVariants = cva('animate-pulse bg-fill-tertiary', {
|
|
7
|
+
variants: {
|
|
8
|
+
rounded: {
|
|
9
|
+
default: 'rounded-md',
|
|
10
|
+
circle: 'rounded-full',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
defaultVariants: { rounded: 'default' },
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
export interface SkeletonProps
|
|
17
|
+
extends HTMLAttributes<HTMLDivElement>,
|
|
18
|
+
VariantProps<typeof skeletonVariants> {
|
|
19
|
+
rounded?: 'default' | 'circle'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
|
|
23
|
+
({ className, rounded = 'default', ...props }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(skeletonVariants({ rounded }), className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
Skeleton.displayName = 'Skeleton'
|
|
32
|
+
|
|
33
|
+
export { skeletonVariants }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as SliderPrimitive from '@radix-ui/react-slider'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
export interface SliderProps
|
|
6
|
+
extends React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> {
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Slider = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof SliderPrimitive.Root>,
|
|
12
|
+
SliderProps
|
|
13
|
+
>(({ className, ...props }, ref) => {
|
|
14
|
+
const value = props.value ?? props.defaultValue ?? [50]
|
|
15
|
+
return (
|
|
16
|
+
<SliderPrimitive.Root
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
'relative flex w-full touch-none select-none cursor-pointer items-center',
|
|
20
|
+
'data-[orientation=vertical]:flex-col data-[orientation=vertical]:w-auto data-[orientation=vertical]:h-full',
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
<SliderPrimitive.Track
|
|
26
|
+
className={cn(
|
|
27
|
+
'relative grow overflow-hidden rounded-full bg-fill-secondary',
|
|
28
|
+
'data-[orientation=horizontal]:h-2 data-[orientation=horizontal]:w-full',
|
|
29
|
+
'data-[orientation=vertical]:w-2 data-[orientation=vertical]:h-full'
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
<SliderPrimitive.Range
|
|
33
|
+
className={cn(
|
|
34
|
+
'absolute rounded-full bg-primary',
|
|
35
|
+
'data-[orientation=horizontal]:h-full',
|
|
36
|
+
'data-[orientation=vertical]:w-full'
|
|
37
|
+
)}
|
|
38
|
+
/>
|
|
39
|
+
</SliderPrimitive.Track>
|
|
40
|
+
{value.map((_, i) => (
|
|
41
|
+
<SliderPrimitive.Thumb
|
|
42
|
+
key={i}
|
|
43
|
+
className={cn(
|
|
44
|
+
'block size-5 rounded-full border-2 border-primary bg-bg-base shadow-sm',
|
|
45
|
+
'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base',
|
|
46
|
+
'disabled:pointer-events-none disabled:opacity-50'
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</SliderPrimitive.Root>
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
Slider.displayName = 'Slider'
|
|
54
|
+
|
|
55
|
+
export { Slider }
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
3
|
+
import { Toaster as SonnerToaster } from 'sonner'
|
|
4
|
+
import type { ToasterProps as SonnerToasterProps } from 'sonner'
|
|
5
|
+
import { getThemeFromDocument } from './theme-from-document'
|
|
6
|
+
|
|
7
|
+
export { toast } from 'sonner'
|
|
8
|
+
|
|
9
|
+
export type ToasterProps = Omit<SonnerToasterProps, 'theme'> & {
|
|
10
|
+
theme?: 'light' | 'dark'
|
|
11
|
+
/** 可选:Portal 根节点 data-style,主库可传入 useThemeStyle 的值 */
|
|
12
|
+
dataStyle?: string
|
|
13
|
+
/** 可选:Portal 根节点 data-theme,主库可传入 useThemeStyle 的值 */
|
|
14
|
+
dataTheme?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 图标/文字用 2px;按钮与容器边缘、按钮之间用固定 8px */
|
|
18
|
+
const TOAST_INNER_GAP_PX = '2px'
|
|
19
|
+
const TOAST_EDGE_PADDING_PX = '8px'
|
|
20
|
+
const TOAST_BUTTON_GAP_PX = '8px'
|
|
21
|
+
|
|
22
|
+
/** 强制 toast 四边内边距 + 根节点 text-sm 变量 + 非首个按钮左间距;选择器带 data-styled 以覆盖 sonner */
|
|
23
|
+
const TOAST_PADDING_TOP_BOTTOM_PX = '12px'
|
|
24
|
+
/** 根节点用设计系统 text-sm 变量;[data-content] 撑满中间;非首个按钮左间距 */
|
|
25
|
+
const TOAST_LAYOUT_FIX_CSS = `[data-sonner-toaster] [data-sonner-toast][data-styled="true"] { padding-left: ${TOAST_EDGE_PADDING_PX} !important; padding-right: ${TOAST_EDGE_PADDING_PX} !important; padding-top: ${TOAST_PADDING_TOP_BOTTOM_PX} !important; padding-bottom: ${TOAST_PADDING_TOP_BOTTOM_PX} !important; font-size: var(--font-size-sm) !important; line-height: var(--font-size-sm--line-height) !important; }
|
|
26
|
+
[data-sonner-toaster] [data-sonner-toast] [data-content] { flex: 1 !important; min-width: 0 !important; }
|
|
27
|
+
[data-sonner-toaster] [data-sonner-toast] [data-button]:not(:first-of-type) { margin-left: ${TOAST_BUTTON_GAP_PX} !important; }`
|
|
28
|
+
|
|
29
|
+
const defaultToastOptions: NonNullable<SonnerToasterProps['toastOptions']> = {
|
|
30
|
+
className: 'text-sm',
|
|
31
|
+
style: {
|
|
32
|
+
padding: `var(--spacing-3) ${TOAST_EDGE_PADDING_PX}`,
|
|
33
|
+
gap: TOAST_INNER_GAP_PX,
|
|
34
|
+
background: 'var(--color-bg-container)',
|
|
35
|
+
borderColor: 'var(--color-border-tertiary)',
|
|
36
|
+
color: 'var(--color-text)',
|
|
37
|
+
},
|
|
38
|
+
descriptionClassName: 'text-xs text-text-secondary',
|
|
39
|
+
actionButtonStyle: {
|
|
40
|
+
borderRadius: 'var(--radius)',
|
|
41
|
+
paddingLeft: 'var(--spacing-2)',
|
|
42
|
+
paddingRight: 'var(--spacing-2)',
|
|
43
|
+
height: 'var(--spacing-7)',
|
|
44
|
+
fontSize: 'var(--font-size-xs)',
|
|
45
|
+
},
|
|
46
|
+
cancelButtonStyle: {
|
|
47
|
+
borderRadius: 'var(--radius)',
|
|
48
|
+
paddingLeft: 'var(--spacing-2)',
|
|
49
|
+
paddingRight: 'var(--spacing-2)',
|
|
50
|
+
height: 'var(--spacing-7)',
|
|
51
|
+
fontSize: 'var(--font-size-xs)',
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const SONNER_GAP = 14
|
|
56
|
+
const SONNER_VISIBLE_TOASTS = 3
|
|
57
|
+
/** toaster 距视口右侧默认 24px,改为 8px 使按钮到屏幕边缘不会过大 */
|
|
58
|
+
const DEFAULT_OFFSET = { right: 8, left: 8, top: 24, bottom: 24 }
|
|
59
|
+
|
|
60
|
+
export function Toaster({ theme, toastOptions, dataStyle, dataTheme, icons, gap = SONNER_GAP, visibleToasts = SONNER_VISIBLE_TOASTS, offset = DEFAULT_OFFSET, ...props }: ToasterProps) {
|
|
61
|
+
const fromDocument = getThemeFromDocument()
|
|
62
|
+
const dataProps =
|
|
63
|
+
dataStyle !== undefined || dataTheme !== undefined
|
|
64
|
+
? { 'data-style': dataStyle, 'data-theme': dataTheme }
|
|
65
|
+
: fromDocument
|
|
66
|
+
const resolvedTheme = theme ?? (typeof document !== 'undefined' && document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light')
|
|
67
|
+
|
|
68
|
+
const content = (
|
|
69
|
+
<>
|
|
70
|
+
<style dangerouslySetInnerHTML={{ __html: TOAST_LAYOUT_FIX_CSS }} />
|
|
71
|
+
<div style={{ display: 'contents' }} {...dataProps}>
|
|
72
|
+
<SonnerToaster
|
|
73
|
+
theme={resolvedTheme}
|
|
74
|
+
closeButton
|
|
75
|
+
icons={icons}
|
|
76
|
+
gap={gap}
|
|
77
|
+
visibleToasts={visibleToasts}
|
|
78
|
+
offset={offset}
|
|
79
|
+
toastOptions={{
|
|
80
|
+
...defaultToastOptions,
|
|
81
|
+
...toastOptions,
|
|
82
|
+
style: { ...defaultToastOptions.style, ...toastOptions?.style },
|
|
83
|
+
}}
|
|
84
|
+
style={
|
|
85
|
+
{
|
|
86
|
+
'--border-radius': 'var(--radius-lg)',
|
|
87
|
+
fontFamily: 'inherit',
|
|
88
|
+
'--gap': `${gap}px`,
|
|
89
|
+
'--toast-icon-margin-start': '0',
|
|
90
|
+
'--toast-icon-margin-end': TOAST_INNER_GAP_PX,
|
|
91
|
+
/* 控制 action/cancel 按钮间距:按钮间仅靠 toast 根 gap,不再额外 margin */
|
|
92
|
+
'--toast-button-margin-start': 'auto',
|
|
93
|
+
'--toast-button-margin-end': '0',
|
|
94
|
+
} as React.CSSProperties
|
|
95
|
+
}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
</>
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return typeof document !== 'undefined' ? createPortal(content, document.body) : null
|
|
103
|
+
}
|
|
104
|
+
Toaster.displayName = 'Toaster'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
import type { HTMLAttributes } from 'react'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { LoaderLine } from './icons-inline'
|
|
5
|
+
|
|
6
|
+
export interface SpinnerProps extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Spinner = forwardRef<HTMLSpanElement, SpinnerProps>(
|
|
11
|
+
({ className, ...props }, ref) => (
|
|
12
|
+
<span ref={ref} role="status" aria-label="Loading" {...props}>
|
|
13
|
+
<LoaderLine className={cn('size-4 animate-spin', className)} />
|
|
14
|
+
</span>
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
Spinner.displayName = 'Spinner'
|