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,100 @@
|
|
|
1
|
+
import { forwardRef } from 'react'
|
|
2
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
'inline-flex items-center justify-center gap-1.5 font-medium transition-colors duration-200 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
primary: 'bg-primary text-text-on-primary hover:bg-primary-hover',
|
|
12
|
+
secondary: 'bg-bg-highlight text-text-on-primary hover:bg-bg-highlight-hover',
|
|
13
|
+
tertiary: 'bg-fill-secondary text-text hover:bg-fill',
|
|
14
|
+
ghost: 'bg-transparent text-text-secondary hover:bg-fill-secondary hover:text-text',
|
|
15
|
+
text: 'bg-transparent text-text-secondary hover:text-text',
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
sm: 'h-7 px-2 text-xs',
|
|
19
|
+
md: 'h-9 px-3 text-sm',
|
|
20
|
+
lg: 'h-11 px-4 text-base',
|
|
21
|
+
},
|
|
22
|
+
rounded: {
|
|
23
|
+
square: 'rounded',
|
|
24
|
+
pill: 'rounded-full',
|
|
25
|
+
},
|
|
26
|
+
textOnly: {
|
|
27
|
+
true: 'h-auto px-0 py-0.5',
|
|
28
|
+
false: '',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
compoundVariants: [
|
|
32
|
+
{ textOnly: true, size: 'sm', className: 'text-xs' },
|
|
33
|
+
{ textOnly: true, size: 'md', className: 'text-sm' },
|
|
34
|
+
{ textOnly: true, size: 'lg', className: 'text-base' },
|
|
35
|
+
],
|
|
36
|
+
defaultVariants: {
|
|
37
|
+
variant: 'ghost',
|
|
38
|
+
size: 'md',
|
|
39
|
+
rounded: 'square',
|
|
40
|
+
textOnly: false,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
export interface ButtonProps
|
|
46
|
+
extends ButtonHTMLAttributes<HTMLButtonElement>,
|
|
47
|
+
VariantProps<typeof buttonVariants> {
|
|
48
|
+
children: ReactNode
|
|
49
|
+
textButton?: boolean
|
|
50
|
+
prefixIcon?: ReactNode
|
|
51
|
+
suffixIcon?: ReactNode
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
55
|
+
(
|
|
56
|
+
{
|
|
57
|
+
variant = 'ghost',
|
|
58
|
+
children,
|
|
59
|
+
size = 'md',
|
|
60
|
+
textButton = false,
|
|
61
|
+
prefixIcon,
|
|
62
|
+
suffixIcon,
|
|
63
|
+
rounded = 'square',
|
|
64
|
+
disabled = false,
|
|
65
|
+
className,
|
|
66
|
+
...props
|
|
67
|
+
},
|
|
68
|
+
ref
|
|
69
|
+
) => {
|
|
70
|
+
return (
|
|
71
|
+
<button
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={buttonVariants({
|
|
74
|
+
variant: textButton ? 'text' : variant,
|
|
75
|
+
size,
|
|
76
|
+
rounded,
|
|
77
|
+
textOnly: textButton,
|
|
78
|
+
className,
|
|
79
|
+
})}
|
|
80
|
+
disabled={disabled}
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
{prefixIcon && (
|
|
84
|
+
<span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
|
|
85
|
+
{prefixIcon}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
{children}
|
|
89
|
+
{suffixIcon && (
|
|
90
|
+
<span className="inline-flex shrink-0 items-center justify-center [&>*]:block [&>*]:leading-none">
|
|
91
|
+
{suffixIcon}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
</button>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
Button.displayName = 'Button'
|
|
99
|
+
|
|
100
|
+
export { Button, buttonVariants }
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as AccordionPrimitive from '@radix-ui/react-accordion'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { ArrowDownSLine } from './icons-inline'
|
|
5
|
+
|
|
6
|
+
const Collapse = React.forwardRef<
|
|
7
|
+
React.ElementRef<typeof AccordionPrimitive.Root>,
|
|
8
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>
|
|
9
|
+
>(({ className, ...props }, ref) => (
|
|
10
|
+
<AccordionPrimitive.Root
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn('w-full font-sans', className)}
|
|
13
|
+
data-slot="collapse"
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
))
|
|
17
|
+
Collapse.displayName = 'Collapse'
|
|
18
|
+
|
|
19
|
+
const CollapseItem = React.forwardRef<
|
|
20
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
21
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<AccordionPrimitive.Item
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn(
|
|
26
|
+
'border-b border-border-tertiary last:border-b-0 data-[orientation=horizontal]:border-b-0 data-[orientation=horizontal]:border-r data-[orientation=horizontal]:border-border-tertiary data-[orientation=horizontal]:last:border-r-0',
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
data-slot="collapse-item"
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
))
|
|
33
|
+
CollapseItem.displayName = 'CollapseItem'
|
|
34
|
+
|
|
35
|
+
export interface CollapseTriggerProps
|
|
36
|
+
extends React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> {
|
|
37
|
+
chevronIcon?: React.ReactNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CollapseTrigger = React.forwardRef<
|
|
41
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
42
|
+
CollapseTriggerProps
|
|
43
|
+
>(({ className, children, chevronIcon, ...rest }, ref) => (
|
|
44
|
+
<AccordionPrimitive.Header className="flex font-sans">
|
|
45
|
+
<AccordionPrimitive.Trigger
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
'group flex flex-1 items-center justify-between gap-2 py-3 text-left text-sm font-medium text-text font-sans transition-colors outline-none',
|
|
49
|
+
'hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-primary-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base',
|
|
50
|
+
'disabled:pointer-events-none disabled:opacity-50',
|
|
51
|
+
'data-[orientation=horizontal]:flex-col data-[orientation=horizontal]:justify-between data-[orientation=horizontal]:py-4 data-[orientation=horizontal]:text-left',
|
|
52
|
+
className
|
|
53
|
+
)}
|
|
54
|
+
data-slot="collapse-trigger"
|
|
55
|
+
{...rest}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
<span
|
|
59
|
+
className="shrink-0 inline-flex transition-transform duration-200 group-data-[state=open]:rotate-180"
|
|
60
|
+
aria-hidden
|
|
61
|
+
>
|
|
62
|
+
{chevronIcon ?? <ArrowDownSLine className="size-4 text-text-secondary" />}
|
|
63
|
+
</span>
|
|
64
|
+
</AccordionPrimitive.Trigger>
|
|
65
|
+
</AccordionPrimitive.Header>
|
|
66
|
+
))
|
|
67
|
+
CollapseTrigger.displayName = 'CollapseTrigger'
|
|
68
|
+
|
|
69
|
+
const CollapseContent = React.forwardRef<
|
|
70
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
71
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
72
|
+
>(({ className, children, ...props }, ref) => (
|
|
73
|
+
<AccordionPrimitive.Content
|
|
74
|
+
ref={ref}
|
|
75
|
+
className={cn(
|
|
76
|
+
'overflow-hidden text-sm text-text-secondary font-sans data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',
|
|
77
|
+
'data-[orientation=horizontal]:data-[state=closed]:animate-accordion-left data-[orientation=horizontal]:data-[state=open]:animate-accordion-right',
|
|
78
|
+
className
|
|
79
|
+
)}
|
|
80
|
+
data-slot="collapse-content"
|
|
81
|
+
{...props}
|
|
82
|
+
>
|
|
83
|
+
<div className="pb-3 pt-0 data-[orientation=horizontal]:pb-0 data-[orientation=horizontal]:pl-0 data-[orientation=horizontal]:pr-3">
|
|
84
|
+
{children}
|
|
85
|
+
</div>
|
|
86
|
+
</AccordionPrimitive.Content>
|
|
87
|
+
))
|
|
88
|
+
CollapseContent.displayName = 'CollapseContent'
|
|
89
|
+
|
|
90
|
+
export type CollapseProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Root>
|
|
91
|
+
export type CollapseItemProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
92
|
+
export type CollapseContentProps = React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
93
|
+
|
|
94
|
+
export { Collapse, CollapseItem, CollapseTrigger, CollapseContent }
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useState, useRef, useLayoutEffect, type ReactNode } from 'react'
|
|
2
|
+
import { motion, AnimatePresence } from 'framer-motion'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { ArrowDownSLine } from './icons-inline'
|
|
5
|
+
|
|
6
|
+
export interface CollapsibleCardProps {
|
|
7
|
+
headerIcon?: ReactNode
|
|
8
|
+
headerTitle: ReactNode
|
|
9
|
+
headerRight?: ReactNode
|
|
10
|
+
children?: ReactNode
|
|
11
|
+
defaultExpanded?: boolean
|
|
12
|
+
/** CSS padding 如 '6px 8px',或 Tailwind 类 */
|
|
13
|
+
contentPadding?: string
|
|
14
|
+
onToggle?: (expanded: boolean) => void
|
|
15
|
+
collapsible?: boolean
|
|
16
|
+
footer?: ReactNode
|
|
17
|
+
contentMaxHeight?: number
|
|
18
|
+
/** 是否启用内容区底部展开/收起栏:内容高度超过 maxHeight 时显示,点击切换全部展开或收起 */
|
|
19
|
+
showExpandAllBar?: boolean
|
|
20
|
+
/** showExpandAllBar 为 true 时,未展开全部时的内容区最大高度(px) */
|
|
21
|
+
maxHeight?: number
|
|
22
|
+
titleShimmer?: boolean
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MAX_HEIGHT = 124
|
|
27
|
+
|
|
28
|
+
export function CollapsibleCard({
|
|
29
|
+
headerIcon,
|
|
30
|
+
headerTitle,
|
|
31
|
+
headerRight,
|
|
32
|
+
children,
|
|
33
|
+
defaultExpanded = true,
|
|
34
|
+
contentPadding = 'var(--spacing-1\.5) var(--spacing-2)',
|
|
35
|
+
onToggle,
|
|
36
|
+
collapsible = true,
|
|
37
|
+
footer,
|
|
38
|
+
contentMaxHeight,
|
|
39
|
+
showExpandAllBar = false,
|
|
40
|
+
maxHeight = DEFAULT_MAX_HEIGHT,
|
|
41
|
+
titleShimmer = false,
|
|
42
|
+
className,
|
|
43
|
+
}: CollapsibleCardProps) {
|
|
44
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
|
|
45
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
46
|
+
const [expandAll, setExpandAll] = useState(false)
|
|
47
|
+
const [shouldShowBar, setShouldShowBar] = useState(false)
|
|
48
|
+
const [contentHeight, setContentHeight] = useState(0)
|
|
49
|
+
const [isMeasuring, setIsMeasuring] = useState(showExpandAllBar && defaultExpanded)
|
|
50
|
+
const [isAnimating, setIsAnimating] = useState(false)
|
|
51
|
+
const measureRef = useRef<HTMLDivElement>(null)
|
|
52
|
+
|
|
53
|
+
useLayoutEffect(() => {
|
|
54
|
+
if (!showExpandAllBar || !isExpanded) {
|
|
55
|
+
setShouldShowBar(false)
|
|
56
|
+
setContentHeight(0)
|
|
57
|
+
setIsMeasuring(false)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
setIsMeasuring(true)
|
|
61
|
+
const checkHeight = () => {
|
|
62
|
+
const el = measureRef.current
|
|
63
|
+
if (!el) return
|
|
64
|
+
const actualHeight = el.scrollHeight
|
|
65
|
+
setContentHeight(actualHeight)
|
|
66
|
+
setShouldShowBar(actualHeight > maxHeight)
|
|
67
|
+
setIsMeasuring(false)
|
|
68
|
+
}
|
|
69
|
+
const raf = requestAnimationFrame(checkHeight)
|
|
70
|
+
let resizeObserver: ResizeObserver | null = null
|
|
71
|
+
if (measureRef.current) {
|
|
72
|
+
resizeObserver = new ResizeObserver(checkHeight)
|
|
73
|
+
resizeObserver.observe(measureRef.current)
|
|
74
|
+
}
|
|
75
|
+
return () => {
|
|
76
|
+
cancelAnimationFrame(raf)
|
|
77
|
+
resizeObserver?.disconnect()
|
|
78
|
+
}
|
|
79
|
+
}, [showExpandAllBar, isExpanded, children, maxHeight])
|
|
80
|
+
|
|
81
|
+
const handleToggle = () => {
|
|
82
|
+
if (!collapsible) return
|
|
83
|
+
const next = !isExpanded
|
|
84
|
+
setIsAnimating(true)
|
|
85
|
+
if (next && showExpandAllBar) setIsMeasuring(true)
|
|
86
|
+
setIsExpanded(next)
|
|
87
|
+
onToggle?.(next)
|
|
88
|
+
setTimeout(() => setIsAnimating(false), 300)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const showArrow = collapsible && (isHovered || headerIcon == null)
|
|
92
|
+
const headerIconNode = showArrow
|
|
93
|
+
? <ArrowDownSLine className={cn('w-[var(--font-size-sm)] h-[var(--font-size-sm)] shrink-0 transition-transform duration-200 text-text-secondary', isExpanded ? 'rotate-0' : '-rotate-90')} />
|
|
94
|
+
: headerIcon
|
|
95
|
+
|
|
96
|
+
const useExpandAllLogic = showExpandAllBar && isExpanded && !contentMaxHeight
|
|
97
|
+
const contentClamped = useExpandAllLogic && (isMeasuring || (shouldShowBar && !expandAll) || isAnimating || (!shouldShowBar && contentHeight === 0))
|
|
98
|
+
const contentMaxHeightStyle =
|
|
99
|
+
contentMaxHeight != null
|
|
100
|
+
? `${contentMaxHeight}px`
|
|
101
|
+
: contentClamped
|
|
102
|
+
? `${maxHeight}px`
|
|
103
|
+
: useExpandAllLogic && shouldShowBar && expandAll && contentHeight > 0
|
|
104
|
+
? `${Math.max(contentHeight + 50, 2000)}px`
|
|
105
|
+
: undefined
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
className={cn(
|
|
110
|
+
'rounded-md overflow-hidden border border-border-tertiary bg-fill-tertiary relative',
|
|
111
|
+
className
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<div
|
|
115
|
+
className={cn(
|
|
116
|
+
'flex flex-row items-center gap-2 px-2 py-1.5 select-none',
|
|
117
|
+
isExpanded && 'border-b border-border-tertiary'
|
|
118
|
+
)}
|
|
119
|
+
onMouseEnter={() => collapsible && setIsHovered(true)}
|
|
120
|
+
onMouseLeave={() => collapsible && setIsHovered(false)}
|
|
121
|
+
>
|
|
122
|
+
<div
|
|
123
|
+
className={cn('flex flex-row items-center gap-2 flex-1 min-w-0', collapsible && 'cursor-pointer')}
|
|
124
|
+
onClick={handleToggle}
|
|
125
|
+
role={collapsible ? 'button' : undefined}
|
|
126
|
+
aria-expanded={collapsible ? isExpanded : undefined}
|
|
127
|
+
>
|
|
128
|
+
<div className="w-[var(--font-size-sm)] h-[var(--font-size-sm)] flex items-center justify-center shrink-0 text-text-secondary [&>svg]:w-[var(--font-size-sm)] [&>svg]:h-[var(--font-size-sm)]">
|
|
129
|
+
{headerIconNode}
|
|
130
|
+
</div>
|
|
131
|
+
<div className="min-w-0 flex-1 flex items-center text-xs leading-xs text-text-secondary">
|
|
132
|
+
{titleShimmer ? (
|
|
133
|
+
<span className="animate-pulse">{headerTitle}</span>
|
|
134
|
+
) : (
|
|
135
|
+
headerTitle
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
{headerRight != null && (
|
|
140
|
+
<div
|
|
141
|
+
className="flex items-center gap-1.5 shrink-0 relative z-10"
|
|
142
|
+
onClick={(e) => e.stopPropagation()}
|
|
143
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
144
|
+
>
|
|
145
|
+
{headerRight}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<AnimatePresence initial={false}>
|
|
151
|
+
{isExpanded && (
|
|
152
|
+
<motion.div
|
|
153
|
+
initial={{ height: 0, opacity: 0 }}
|
|
154
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
155
|
+
exit={{ height: 0, opacity: 0 }}
|
|
156
|
+
transition={{ duration: 0.2, ease: [0.2, 0.8, 0.2, 1] }}
|
|
157
|
+
className={cn('overflow-hidden', isAnimating && 'select-none')}
|
|
158
|
+
>
|
|
159
|
+
{/* 隐藏测量用,用于获取内容实际高度 */}
|
|
160
|
+
{useExpandAllLogic && (
|
|
161
|
+
<div
|
|
162
|
+
ref={measureRef}
|
|
163
|
+
className="absolute invisible h-auto w-full pointer-events-none"
|
|
164
|
+
style={{ padding: contentPadding }}
|
|
165
|
+
aria-hidden
|
|
166
|
+
>
|
|
167
|
+
{children}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
<div
|
|
171
|
+
className={cn(
|
|
172
|
+
'relative overflow-x-hidden bg-bg-base scrollbar-auto',
|
|
173
|
+
contentMaxHeight != null
|
|
174
|
+
? 'overflow-y-auto'
|
|
175
|
+
: contentClamped
|
|
176
|
+
? 'overflow-y-hidden'
|
|
177
|
+
: 'overflow-y-visible',
|
|
178
|
+
shouldShowBar && 'transition-[max-height] duration-300 ease-in-out'
|
|
179
|
+
)}
|
|
180
|
+
style={{
|
|
181
|
+
padding: contentPadding,
|
|
182
|
+
maxHeight: contentMaxHeightStyle,
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{children}
|
|
186
|
+
{/* 底部渐变遮罩:内容被截断且未展开全部时显示 */}
|
|
187
|
+
{useExpandAllLogic && shouldShowBar && contentHeight > maxHeight && (
|
|
188
|
+
<div
|
|
189
|
+
className={cn(
|
|
190
|
+
'absolute bottom-0 left-0 right-0 pointer-events-none h-[25px] bg-gradient-to-b from-transparent to-[var(--color-bg-base)] transition-opacity duration-300',
|
|
191
|
+
expandAll ? 'opacity-0' : 'opacity-100'
|
|
192
|
+
)}
|
|
193
|
+
/>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
{/* 内容区底部展开/收起栏:仅当内容高度超过 maxHeight 时显示 */}
|
|
197
|
+
{useExpandAllLogic && shouldShowBar && (
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
className="w-full h-4 flex items-center justify-center bg-bg-base border-0 cursor-pointer text-text-secondary hover:text-text transition-colors"
|
|
201
|
+
onClick={(e) => {
|
|
202
|
+
e.stopPropagation()
|
|
203
|
+
setIsAnimating(true)
|
|
204
|
+
setExpandAll((prev) => !prev)
|
|
205
|
+
setTimeout(() => setIsAnimating(false), 300)
|
|
206
|
+
}}
|
|
207
|
+
aria-label={expandAll ? '收起' : '展开全部'}
|
|
208
|
+
>
|
|
209
|
+
<ArrowDownSLine
|
|
210
|
+
className={cn(
|
|
211
|
+
'w-[var(--font-size-sm)] h-[var(--font-size-sm)] transition-transform duration-200',
|
|
212
|
+
expandAll ? 'rotate-180' : 'rotate-0'
|
|
213
|
+
)}
|
|
214
|
+
/>
|
|
215
|
+
</button>
|
|
216
|
+
)}
|
|
217
|
+
</motion.div>
|
|
218
|
+
)}
|
|
219
|
+
</AnimatePresence>
|
|
220
|
+
|
|
221
|
+
{footer != null && (
|
|
222
|
+
<div className="flex flex-row items-center gap-2 px-2 py-1.5 border-t border-border-tertiary">
|
|
223
|
+
{footer}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
CollapsibleCard.displayName = 'CollapsibleCard'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
const Collapsible = CollapsiblePrimitive.Root
|
|
6
|
+
|
|
7
|
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
|
|
8
|
+
|
|
9
|
+
const CollapsibleContent = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof CollapsiblePrimitive.CollapsibleContent>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.CollapsibleContent>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<CollapsiblePrimitive.CollapsibleContent
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn('overflow-hidden', className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
))
|
|
19
|
+
CollapsibleContent.displayName = CollapsiblePrimitive.CollapsibleContent.displayName
|
|
20
|
+
|
|
21
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent }
|