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,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
|
+
…
|
|
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'
|