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,125 @@
1
+ import { forwardRef } from 'react'
2
+ import type { HTMLAttributes } from 'react'
3
+ import { cva } from 'class-variance-authority'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const LIST_BASE = 'flex flex-col gap-1'
7
+
8
+ const itemVariants = cva(
9
+ 'w-full flex flex-col gap-0.5 items-start p-2 text-sm rounded text-left transition-colors outline-none focus-visible:bg-fill-tertiary focus-visible:ring-0',
10
+ {
11
+ variants: {
12
+ selected: { true: '', false: '' },
13
+ disabled: {
14
+ true: 'opacity-50 cursor-not-allowed',
15
+ false: 'cursor-pointer hover:bg-fill-tertiary',
16
+ },
17
+ dimmed: { true: 'opacity-40', false: '' },
18
+ },
19
+ defaultVariants: { selected: false, disabled: false, dimmed: false },
20
+ }
21
+ )
22
+
23
+ const prefixVariants = cva(
24
+ 'flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center text-xs font-mono font-medium transition-colors',
25
+ {
26
+ variants: {
27
+ selected: {
28
+ true: 'bg-primary-active text-text-on-primary',
29
+ false: 'bg-fill-secondary text-text-tertiary',
30
+ },
31
+ },
32
+ defaultVariants: { selected: false },
33
+ }
34
+ )
35
+
36
+ export interface OptionItem {
37
+ id: string
38
+ label: string
39
+ description?: string
40
+ disabled?: boolean
41
+ }
42
+
43
+ export interface OptionListProps extends HTMLAttributes<HTMLDivElement> {
44
+ items: OptionItem[]
45
+ selectedIds?: string[]
46
+ focusedId?: string
47
+ onItemClick?: (item: OptionItem, index: number) => void
48
+ showPrefix?: boolean
49
+ disabled?: boolean
50
+ }
51
+
52
+ export const OptionList = forwardRef<HTMLDivElement, OptionListProps>(
53
+ (
54
+ {
55
+ items,
56
+ selectedIds = [],
57
+ focusedId,
58
+ onItemClick,
59
+ showPrefix = true,
60
+ disabled = false,
61
+ className,
62
+ ...props
63
+ },
64
+ ref
65
+ ) => {
66
+ const getOptionNumber = (index: number) => String(index + 1)
67
+
68
+ return (
69
+ <div
70
+ ref={ref}
71
+ role="listbox"
72
+ aria-multiselectable={selectedIds.length > 1}
73
+ className={cn(LIST_BASE, className)}
74
+ {...props}
75
+ >
76
+ {items.map((item, index) => {
77
+ const isSelected = selectedIds.includes(item.id)
78
+ const isFocused = focusedId === item.id
79
+ const isDisabled = disabled || item.disabled
80
+ const hasSelection = selectedIds.length > 0
81
+ const dimmed = hasSelection && !isSelected && !isDisabled
82
+
83
+ return (
84
+ <button
85
+ key={item.id}
86
+ type="button"
87
+ role="option"
88
+ aria-selected={isSelected}
89
+ aria-disabled={isDisabled}
90
+ onClick={() => {
91
+ if (!isDisabled) onItemClick?.(item, index)
92
+ }}
93
+ disabled={isDisabled}
94
+ className={cn(
95
+ itemVariants({
96
+ selected: isSelected,
97
+ disabled: isDisabled,
98
+ dimmed,
99
+ }),
100
+ // 键盘焦点态:仅底色,无 ring
101
+ isFocused && 'bg-fill-tertiary'
102
+ )}
103
+ >
104
+ <div className="flex items-center gap-3">
105
+ {showPrefix && (
106
+ <div className={prefixVariants({ selected: isSelected })}>
107
+ {getOptionNumber(index)}
108
+ </div>
109
+ )}
110
+ <span className="font-medium text-text">{item.label}</span>
111
+ </div>
112
+ {item.description && (
113
+ <div className="ml-8 text-xs text-text-tertiary">
114
+ {item.description}
115
+ </div>
116
+ )}
117
+ </button>
118
+ )
119
+ })}
120
+ </div>
121
+ )
122
+ }
123
+ )
124
+
125
+ OptionList.displayName = 'OptionList'
@@ -0,0 +1,132 @@
1
+ import * as React from 'react'
2
+ import { cn } from '@/lib/utils'
3
+ import { ArrowLeftLine, ArrowRightLine } from './icons-inline'
4
+
5
+ const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
6
+ <nav
7
+ role="navigation"
8
+ aria-label="pagination"
9
+ className={cn('flex w-full justify-center', className)}
10
+ {...props}
11
+ />
12
+ )
13
+ Pagination.displayName = 'Pagination'
14
+
15
+ const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<'ul'>>(
16
+ ({ className, ...props }, ref) => (
17
+ <ul
18
+ ref={ref}
19
+ className={cn('flex flex-row flex-wrap items-center gap-2', className)}
20
+ {...props}
21
+ />
22
+ )
23
+ )
24
+ PaginationContent.displayName = 'PaginationContent'
25
+
26
+ const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li'>>(
27
+ ({ className, ...props }, ref) => (
28
+ <li ref={ref} className={cn('list-none', className)} {...props} />
29
+ )
30
+ )
31
+ PaginationItem.displayName = 'PaginationItem'
32
+
33
+ export interface PaginationLinkProps extends React.ComponentProps<'a'> {
34
+ isActive?: boolean
35
+ }
36
+
37
+ const PaginationLink = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(
38
+ ({ className, isActive, ...props }, ref) => (
39
+ <a
40
+ ref={ref}
41
+ aria-current={isActive ? 'page' : undefined}
42
+ className={cn(
43
+ 'inline-flex min-w-9 h-9 items-center justify-center rounded-md border text-sm font-medium transition-colors',
44
+ 'border-border-tertiary bg-bg-container text-text',
45
+ 'hover:bg-fill-secondary hover:text-text hover:border-border-tertiary',
46
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base',
47
+ 'disabled:pointer-events-none disabled:opacity-50',
48
+ isActive && 'bg-fill-secondary border-border-tertiary text-text',
49
+ className
50
+ )}
51
+ {...props}
52
+ />
53
+ )
54
+ )
55
+ PaginationLink.displayName = 'PaginationLink'
56
+
57
+ export interface PaginationPreviousProps extends React.ComponentProps<'a'> {
58
+ text?: string
59
+ }
60
+
61
+ const PaginationPrevious = React.forwardRef<HTMLAnchorElement, PaginationPreviousProps>(
62
+ ({ className, text = 'Previous', ...props }, ref) => (
63
+ <a
64
+ ref={ref}
65
+ aria-label="Go to previous page"
66
+ className={cn(
67
+ 'inline-flex h-9 min-w-9 items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium transition-colors',
68
+ 'border-border-tertiary bg-bg-container text-text',
69
+ 'hover:bg-fill-secondary hover:text-text hover:border-border-tertiary',
70
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base',
71
+ 'disabled:pointer-events-none disabled:opacity-50',
72
+ className
73
+ )}
74
+ {...props}
75
+ >
76
+ <ArrowLeftLine className="h-4 w-4 shrink-0" />
77
+ <span className="hidden sm:inline">{text}</span>
78
+ </a>
79
+ )
80
+ )
81
+ PaginationPrevious.displayName = 'PaginationPrevious'
82
+
83
+ export interface PaginationNextProps extends React.ComponentProps<'a'> {
84
+ text?: string
85
+ }
86
+
87
+ const PaginationNext = React.forwardRef<HTMLAnchorElement, PaginationNextProps>(
88
+ ({ className, text = 'Next', ...props }, ref) => (
89
+ <a
90
+ ref={ref}
91
+ aria-label="Go to next page"
92
+ className={cn(
93
+ 'inline-flex h-9 min-w-9 items-center justify-center gap-2 rounded-md border px-3 text-sm font-medium transition-colors',
94
+ 'border-border-tertiary bg-bg-container text-text',
95
+ 'hover:bg-fill-secondary hover:text-text hover:border-border-tertiary',
96
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base',
97
+ 'disabled:pointer-events-none disabled:opacity-50',
98
+ className
99
+ )}
100
+ {...props}
101
+ >
102
+ <span className="hidden sm:inline">{text}</span>
103
+ <ArrowRightLine className="h-4 w-4 shrink-0" />
104
+ </a>
105
+ )
106
+ )
107
+ PaginationNext.displayName = 'PaginationNext'
108
+
109
+ const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<'span'>) => (
110
+ <span
111
+ role="presentation"
112
+ aria-hidden
113
+ className={cn(
114
+ 'flex h-9 w-9 shrink-0 items-center justify-center text-text-secondary',
115
+ className
116
+ )}
117
+ {...props}
118
+ >
119
+ &#8230;
120
+ </span>
121
+ )
122
+ PaginationEllipsis.displayName = 'PaginationEllipsis'
123
+
124
+ export {
125
+ Pagination,
126
+ PaginationContent,
127
+ PaginationItem,
128
+ PaginationLink,
129
+ PaginationNext,
130
+ PaginationPrevious,
131
+ PaginationEllipsis,
132
+ }
@@ -0,0 +1,42 @@
1
+ import * as React from 'react'
2
+ import * as ProgressPrimitive from '@radix-ui/react-progress'
3
+ import { cn } from '@/lib/utils'
4
+
5
+ export interface ProgressProps
6
+ extends Omit<
7
+ React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>,
8
+ 'value'
9
+ > {
10
+ value?: number | null
11
+ max?: number
12
+ className?: string
13
+ }
14
+
15
+ const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
16
+ ({ className, value, max = 100, ...rootProps }, ref) => {
17
+ const percent = value != null ? Math.min(max, Math.max(0, value)) : undefined
18
+ return (
19
+ <ProgressPrimitive.Root
20
+ ref={ref as React.RefObject<HTMLDivElement>}
21
+ value={percent ?? null}
22
+ max={max}
23
+ className={cn(
24
+ 'relative h-1 w-full overflow-hidden rounded-full bg-fill-secondary',
25
+ className
26
+ )}
27
+ style={{ height: 4 }}
28
+ {...rootProps}
29
+ >
30
+ <ProgressPrimitive.Indicator
31
+ className="h-full rounded-full bg-success transition-[width] duration-200 ease-out"
32
+ style={{
33
+ width: percent != null ? `${(percent / max) * 100}%` : '0%',
34
+ }}
35
+ />
36
+ </ProgressPrimitive.Root>
37
+ )
38
+ }
39
+ )
40
+ Progress.displayName = 'Progress'
41
+
42
+ export { Progress }
@@ -0,0 +1,69 @@
1
+ import * as React from 'react'
2
+ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ const rootVariants = cva('grid gap-2', {
7
+ variants: {
8
+ orientation: {
9
+ vertical: 'grid-flow-row',
10
+ horizontal: 'grid-flow-col auto-cols-max',
11
+ },
12
+ },
13
+ defaultVariants: { orientation: 'vertical' },
14
+ })
15
+
16
+ const itemVariants = cva(
17
+ 'shrink-0 rounded-full border border-border-tertiary outline-offset-2 cursor-pointer transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary-border disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-text data-[state=checked]:bg-text data-[state=checked]:text-bg-base',
18
+ {
19
+ variants: {
20
+ size: { sm: 'size-3.5', md: 'size-4', lg: 'size-5' },
21
+ },
22
+ defaultVariants: { size: 'md' },
23
+ }
24
+ )
25
+
26
+ const indicatorSizeMap: Record<'sm' | 'md' | 'lg', string> = {
27
+ sm: 'size-1.5',
28
+ md: 'size-2',
29
+ lg: 'size-2.5',
30
+ }
31
+
32
+ export interface RadioGroupProps
33
+ extends Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>, 'orientation'>,
34
+ VariantProps<typeof rootVariants> {}
35
+
36
+ export interface RadioGroupItemProps
37
+ extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>,
38
+ VariantProps<typeof itemVariants> {}
39
+
40
+ const RadioGroup = React.forwardRef<
41
+ React.ElementRef<typeof RadioGroupPrimitive.Root>,
42
+ RadioGroupProps
43
+ >(({ className, orientation = 'vertical', ...props }, ref) => (
44
+ <RadioGroupPrimitive.Root
45
+ ref={ref}
46
+ className={cn(rootVariants({ orientation, className }))}
47
+ orientation={orientation ?? 'vertical'}
48
+ {...props}
49
+ />
50
+ ))
51
+ RadioGroup.displayName = 'RadioGroup'
52
+
53
+ const RadioGroupItem = React.forwardRef<
54
+ React.ElementRef<typeof RadioGroupPrimitive.Item>,
55
+ RadioGroupItemProps
56
+ >(({ className, size = 'md', ...props }, ref) => (
57
+ <RadioGroupPrimitive.Item
58
+ ref={ref}
59
+ className={cn(itemVariants({ size, className }))}
60
+ {...props}
61
+ >
62
+ <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
63
+ <span className={cn('rounded-full bg-current', indicatorSizeMap[size ?? 'md'])} />
64
+ </RadioGroupPrimitive.Indicator>
65
+ </RadioGroupPrimitive.Item>
66
+ ))
67
+ RadioGroupItem.displayName = 'RadioGroupItem'
68
+
69
+ export { RadioGroup, RadioGroupItem, rootVariants as radioGroupVariants, itemVariants as radioGroupItemVariants }
@@ -0,0 +1,67 @@
1
+ import * as React from 'react'
2
+ import { Group, Panel, Separator } from 'react-resizable-panels'
3
+ import type { GroupProps, PanelProps, SeparatorProps } from 'react-resizable-panels'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ export interface ResizablePanelGroupProps
7
+ extends Omit<GroupProps, 'orientation'> {
8
+ direction?: 'horizontal' | 'vertical'
9
+ className?: string
10
+ }
11
+
12
+ const ResizablePanelGroup = React.forwardRef<HTMLDivElement, ResizablePanelGroupProps>(
13
+ function ResizablePanelGroup({ direction = 'horizontal', className, ...props }, ref) {
14
+ return (
15
+ <Group
16
+ elementRef={ref}
17
+ orientation={direction}
18
+ className={cn('flex h-full w-full', direction === 'vertical' && 'flex-col', className)}
19
+ {...props}
20
+ />
21
+ )
22
+ }
23
+ )
24
+ ResizablePanelGroup.displayName = 'ResizablePanelGroup'
25
+
26
+ export type ResizablePanelProps = PanelProps
27
+
28
+ const ResizablePanel = React.forwardRef<HTMLDivElement, ResizablePanelProps>(
29
+ function ResizablePanel({ className, ...props }, ref) {
30
+ return <Panel elementRef={ref} className={cn(className)} {...props} />
31
+ }
32
+ )
33
+ ResizablePanel.displayName = 'ResizablePanel'
34
+
35
+ export interface ResizableHandleProps extends SeparatorProps {
36
+ withHandle?: boolean
37
+ className?: string
38
+ }
39
+
40
+ const ResizableHandle = React.forwardRef<HTMLDivElement, ResizableHandleProps>(
41
+ function ResizableHandle({ withHandle = false, className, ...props }, ref) {
42
+ return (
43
+ <Separator
44
+ elementRef={ref}
45
+ className={cn(
46
+ 'relative flex shrink-0 items-center justify-center bg-border-tertiary',
47
+ 'after:absolute after:inset-0 after:transition-colors',
48
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1',
49
+ 'data-[resize-handle-active]:bg-border-secondary',
50
+ 'aria-[orientation=vertical]:h-full aria-[orientation=vertical]:w-px aria-[orientation=vertical]:cursor-col-resize',
51
+ 'aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:cursor-row-resize',
52
+ '[&[aria-orientation=vertical]>div]:h-6 [&[aria-orientation=vertical]>div]:w-1',
53
+ '[&[aria-orientation=horizontal]>div]:h-1 [&[aria-orientation=horizontal]>div]:w-6',
54
+ className
55
+ )}
56
+ {...props}
57
+ >
58
+ {withHandle && (
59
+ <div className="z-10 shrink-0 rounded-lg bg-border-secondary" data-resize-handle />
60
+ )}
61
+ </Separator>
62
+ )
63
+ }
64
+ )
65
+ ResizableHandle.displayName = 'ResizableHandle'
66
+
67
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle }
@@ -0,0 +1,114 @@
1
+ import { forwardRef, useId, useRef, useState, useCallback, useEffect } from 'react'
2
+ import type { ReactNode, CSSProperties } from 'react'
3
+ import { cn } from '@/lib/utils'
4
+
5
+ export type ScrollbarVisibility = 'auto' | 'always' | 'hidden'
6
+
7
+ export interface ScrollbarProps {
8
+ children: ReactNode
9
+ className?: string
10
+ style?: CSSProperties
11
+ maxHeight?: number | string
12
+ /** auto: 默认透明,滚动时与 hover 时可见;always: 始终可见;hidden: 隐藏 */
13
+ scrollbarVisibility?: ScrollbarVisibility
14
+ scrollbarWidth?: number
15
+ }
16
+
17
+ export const Scrollbar = forwardRef<HTMLDivElement, ScrollbarProps>(
18
+ function Scrollbar(
19
+ {
20
+ children,
21
+ className,
22
+ style,
23
+ maxHeight,
24
+ scrollbarVisibility = 'auto',
25
+ scrollbarWidth = 6,
26
+ },
27
+ ref
28
+ ) {
29
+ const scrollId = useId().replace(/:/g, '')
30
+ const scrollClassName = `Scrollbar-${scrollId}`
31
+ const scrollEndTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
32
+ const [isScrolling, setIsScrolling] = useState(false)
33
+
34
+ const handleScroll = useCallback(() => {
35
+ if (scrollbarVisibility !== 'auto') return
36
+ setIsScrolling(true)
37
+ if (scrollEndTimerRef.current) clearTimeout(scrollEndTimerRef.current)
38
+ scrollEndTimerRef.current = setTimeout(() => {
39
+ scrollEndTimerRef.current = null
40
+ setIsScrolling(false)
41
+ }, 800)
42
+ }, [scrollbarVisibility])
43
+
44
+ useEffect(() => {
45
+ return () => {
46
+ if (scrollEndTimerRef.current) clearTimeout(scrollEndTimerRef.current)
47
+ }
48
+ }, [])
49
+
50
+ const getScrollbarStyles = () => {
51
+ if (scrollbarVisibility === 'hidden') {
52
+ return `
53
+ .${scrollClassName}::-webkit-scrollbar { display: none; }
54
+ .${scrollClassName} { scrollbar-width: none; }
55
+ `
56
+ }
57
+ const isAuto = scrollbarVisibility === 'auto'
58
+ const thumbColor =
59
+ scrollbarVisibility === 'always'
60
+ ? 'var(--color-fill-secondary)'
61
+ : 'transparent'
62
+ const thumbHoverColor = 'var(--color-fill-secondary)'
63
+ const autoVisible = isAuto
64
+ ? `
65
+ .${scrollClassName}:hover::-webkit-scrollbar-thumb,
66
+ .${scrollClassName}.Scrollbar-isScrolling::-webkit-scrollbar-thumb {
67
+ background-color: ${thumbHoverColor};
68
+ }
69
+ `
70
+ : `
71
+ .${scrollClassName}:hover::-webkit-scrollbar-thumb {
72
+ background-color: ${thumbHoverColor};
73
+ }
74
+ `
75
+ return `
76
+ .${scrollClassName}::-webkit-scrollbar {
77
+ width: ${scrollbarWidth}px !important;
78
+ height: ${scrollbarWidth}px !important;
79
+ }
80
+ .${scrollClassName}::-webkit-scrollbar-track { background: transparent; }
81
+ .${scrollClassName}::-webkit-scrollbar-thumb {
82
+ background-color: ${thumbColor};
83
+ border-radius: ${Math.max(scrollbarWidth / 2, 2)}px;
84
+ min-height: var(--spacing-8);
85
+ transition: background-color 0.2s ease;
86
+ }
87
+ ${autoVisible}
88
+ .${scrollClassName}::-webkit-scrollbar-corner { background: transparent; }
89
+ `
90
+ }
91
+
92
+ const computedStyle: CSSProperties = { ...style, maxHeight }
93
+
94
+ return (
95
+ <>
96
+ <style>{getScrollbarStyles()}</style>
97
+ <div
98
+ ref={ref}
99
+ className={cn(
100
+ scrollClassName,
101
+ 'overflow-y-auto',
102
+ isScrolling && 'Scrollbar-isScrolling',
103
+ className
104
+ )}
105
+ style={computedStyle}
106
+ onScroll={scrollbarVisibility === 'auto' ? handleScroll : undefined}
107
+ >
108
+ {children}
109
+ </div>
110
+ </>
111
+ )
112
+ }
113
+ )
114
+ Scrollbar.displayName = 'Scrollbar'