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.
Files changed (118) hide show
  1. package/README.md +188 -4
  2. package/dist/commands/add.js +93 -0
  3. package/dist/commands/diff.js +54 -0
  4. package/dist/commands/init.js +96 -0
  5. package/dist/commands/list.js +25 -0
  6. package/dist/index.js +37 -0
  7. package/dist/utils/config.js +53 -0
  8. package/dist/utils/registry.js +34 -0
  9. package/dist/utils/tokens.js +176 -0
  10. package/dist/utils/transform.js +19 -0
  11. package/package.json +33 -10
  12. package/registry/__tests__/basic/button.test.tsx +333 -0
  13. package/registry/__tests__/chat/markdown.test.tsx +387 -0
  14. package/registry/__tests__/chat/thinking-indicator.test.tsx +244 -0
  15. package/registry/__tests__/chat/tool-invocation-card.test.tsx +346 -0
  16. package/registry/basic/alert-dialog.tsx +180 -0
  17. package/registry/basic/avatar.tsx +120 -0
  18. package/registry/basic/button.tsx +100 -0
  19. package/registry/basic/collapse.tsx +94 -0
  20. package/registry/basic/collapsible-card.tsx +230 -0
  21. package/registry/basic/collapsible.tsx +21 -0
  22. package/registry/basic/dropdown-menu.tsx +254 -0
  23. package/registry/basic/icon-button.tsx +66 -0
  24. package/registry/basic/icons-inline.tsx +206 -0
  25. package/registry/basic/kbd.tsx +50 -0
  26. package/registry/basic/option-list.tsx +125 -0
  27. package/registry/basic/pagination.tsx +132 -0
  28. package/registry/basic/progress.tsx +42 -0
  29. package/registry/basic/radio-group.tsx +69 -0
  30. package/registry/basic/resizable.tsx +67 -0
  31. package/registry/basic/scrollbar.tsx +114 -0
  32. package/registry/basic/select.tsx +177 -0
  33. package/registry/basic/shimmering-text.tsx +115 -0
  34. package/registry/basic/sidebar-menu.tsx +177 -0
  35. package/registry/basic/skeleton.tsx +33 -0
  36. package/registry/basic/slider.tsx +55 -0
  37. package/registry/basic/sonner.tsx +104 -0
  38. package/registry/basic/spinner.tsx +17 -0
  39. package/registry/basic/switch.tsx +49 -0
  40. package/registry/basic/table.tsx +117 -0
  41. package/registry/basic/tabs.tsx +85 -0
  42. package/registry/basic/tag.tsx +161 -0
  43. package/registry/basic/theme-from-document.ts +10 -0
  44. package/registry/basic/toggle.tsx +223 -0
  45. package/registry/basic/tooltip.tsx +80 -0
  46. package/registry/basic/typography.tsx +201 -0
  47. package/registry/chat/ask-user-part.tsx +70 -0
  48. package/registry/chat/browser-action-part.tsx +166 -0
  49. package/registry/chat/chat-input/chat-input-folder-selector.tsx +185 -0
  50. package/registry/chat/chat-input/chat-input-model-switcher.tsx +131 -0
  51. package/registry/chat/chat-input/chat-input-textarea.tsx +67 -0
  52. package/registry/chat/chat-input/compound.tsx +334 -0
  53. package/registry/chat/chat-input/context.tsx +189 -0
  54. package/registry/chat/chat-input/folder-permission-dialog.tsx +61 -0
  55. package/registry/chat/chat-input/index.tsx +123 -0
  56. package/registry/chat/chat-input/types.ts +77 -0
  57. package/registry/chat/chat-input/useAutoResizeTextarea.ts +20 -0
  58. package/registry/chat/code-block-part.tsx +151 -0
  59. package/registry/chat/file-attachment.tsx +44 -0
  60. package/registry/chat/file-card.tsx +68 -0
  61. package/registry/chat/file-review-part.tsx +259 -0
  62. package/registry/chat/folder-button.tsx +169 -0
  63. package/registry/chat/generated-images-grid.tsx +56 -0
  64. package/registry/chat/generation-status-bar.tsx +72 -0
  65. package/registry/chat/hint-banner.tsx +165 -0
  66. package/registry/chat/image-attachment.tsx +166 -0
  67. package/registry/chat/image-generating.tsx +281 -0
  68. package/registry/chat/markdown.tsx +146 -0
  69. package/registry/chat/mermaid-part.tsx +90 -0
  70. package/registry/chat/permission-card.tsx +178 -0
  71. package/registry/chat/plan-part.tsx +168 -0
  72. package/registry/chat/queue-indicator.tsx +234 -0
  73. package/registry/chat/reasoning-step/compound.tsx +336 -0
  74. package/registry/chat/reasoning-step/context.tsx +114 -0
  75. package/registry/chat/reasoning-step/index.tsx +45 -0
  76. package/registry/chat/reasoning-step/types.ts +109 -0
  77. package/registry/chat/related-prompts.tsx +91 -0
  78. package/registry/chat/response/compound.tsx +210 -0
  79. package/registry/chat/response/context.tsx +200 -0
  80. package/registry/chat/response/index.tsx +87 -0
  81. package/registry/chat/response/types.ts +123 -0
  82. package/registry/chat/send-button.tsx +94 -0
  83. package/registry/chat/streaming-markdown-block.tsx +111 -0
  84. package/registry/chat/task-part.tsx +109 -0
  85. package/registry/chat/terminal-code-block-part.tsx +69 -0
  86. package/registry/chat/thinking-indicator.tsx +91 -0
  87. package/registry/chat/tool-invocation-card.tsx +132 -0
  88. package/registry/chat/user-message.tsx +38 -0
  89. package/registry/chat/user-question/UserQuestionCard.tsx +198 -0
  90. package/registry/chat/user-question/UserQuestionFooter.tsx +66 -0
  91. package/registry/chat/user-question/UserQuestionHeader.tsx +64 -0
  92. package/registry/chat/user-question/compound.tsx +324 -0
  93. package/registry/chat/user-question/context.tsx +456 -0
  94. package/registry/chat/user-question/index.tsx +95 -0
  95. package/registry/chat/user-question/types.ts +61 -0
  96. package/registry/chat/user-question/useUserQuestionKeyboard.ts +126 -0
  97. package/registry/chat/user-question/useUserQuestionState.ts +165 -0
  98. package/registry/chat/user-question-answer.tsx +62 -0
  99. package/registry/lib/file-icon-maps.ts +150 -0
  100. package/registry/lib/use-mermaid-render.ts +76 -0
  101. package/registry/lib/utils.ts +6 -0
  102. package/registry/meta.json +1 -0
  103. package/registry/tokens/index.css +31 -0
  104. package/registry/tokens/scale/computed.css +103 -0
  105. package/registry/tokens/scale/config.css +110 -0
  106. package/registry/tokens/scale/index.css +30 -0
  107. package/registry/tokens/scale/presets/compact.css +30 -0
  108. package/registry/tokens/scale/presets/dense.css +64 -0
  109. package/registry/tokens/scale/presets/sharp.css +40 -0
  110. package/registry/tokens/scale/presets/soft.css +16 -0
  111. package/registry/tokens/scale.css +13 -0
  112. package/registry/tokens/scrollbar-utility.css +35 -0
  113. package/registry/tokens/theme.css +633 -0
  114. package/registry/tokens/themes/dark-parchment.css +132 -0
  115. package/registry/tokens/themes/dark-qoder.css +132 -0
  116. package/registry/tokens/themes/light-parchment.css +123 -0
  117. package/registry/tokens/themes/light-qoder.css +131 -0
  118. 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'