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,165 @@
1
+ import type { ReactNode } from 'react'
2
+ import { cn } from '@/lib/utils'
3
+ import { Button } from '../basic/button'
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuTrigger,
9
+ } from '../basic/dropdown-menu'
10
+ import { CloseLine, ArrowDownSLine, WarningLine, SparklingLine } from '../basic/icons-inline'
11
+ import { Scrollbar } from '../basic/scrollbar'
12
+
13
+ export interface HintBannerProps {
14
+ type?: 'wiki' | 'credits'
15
+ title?: string
16
+ description?: string
17
+ highlightText?: string
18
+ language?: string
19
+ rechargeText?: string
20
+ onClose?: () => void
21
+ onGenerate?: () => void
22
+ onRecharge?: () => void
23
+ onLanguageChange?: (language: string) => void
24
+ closeIcon?: ReactNode
25
+ arrowDownSIcon?: ReactNode
26
+ suggestionIcon?: ReactNode
27
+ /** 供 Portal 浮层继承(与 ChatInput FolderButton 一致),不传则由 dropdown-menu 使用 getThemeFromDocument() 兜底 */
28
+ dataStyle?: string
29
+ dataTheme?: string
30
+ }
31
+
32
+ const DEFAULT_HIGHLIGHT = 'Repo Wiki'
33
+ const LANGUAGES = ['English', '中文', '日本語', '한국어']
34
+
35
+ const closeClass = 'h-3.5 w-3.5'
36
+ const arrowClass = 'h-3 w-3'
37
+ const suggestionClass = 'h-4 w-4'
38
+
39
+ export function HintBanner({
40
+ type = 'wiki',
41
+ title,
42
+ description,
43
+ highlightText = DEFAULT_HIGHLIGHT,
44
+ language = 'English',
45
+ rechargeText,
46
+ onClose,
47
+ onGenerate,
48
+ onRecharge,
49
+ onLanguageChange,
50
+ closeIcon,
51
+ arrowDownSIcon,
52
+ suggestionIcon,
53
+ dataStyle,
54
+ dataTheme,
55
+ }: HintBannerProps) {
56
+ const isCreditsType = type === 'credits'
57
+ const defaultClose = closeIcon ?? <CloseLine className={closeClass} />
58
+ const defaultArrow = arrowDownSIcon ?? <ArrowDownSLine className={arrowClass} />
59
+ const defaultSuggestion =
60
+ suggestionIcon ?? (isCreditsType ? <WarningLine className={suggestionClass} /> : <SparklingLine className={suggestionClass} />)
61
+
62
+ const displayTitle =
63
+ title ?? (isCreditsType ? 'Credits Exhausted' : 'Free Wiki Generation Available')
64
+
65
+ const descriptionTemplate = isCreditsType
66
+ ? 'Your credits have been exhausted. Please recharge to continue using.'
67
+ : `Generate ${highlightText} for this repository to boost AI agent with structured context.`
68
+ const displayDescription = description ?? descriptionTemplate
69
+
70
+ const renderDescription = (): ReactNode => {
71
+ if (!displayDescription.includes(highlightText)) {
72
+ return <span>{displayDescription}</span>
73
+ }
74
+ const parts = displayDescription.split(highlightText)
75
+ const result: ReactNode[] = []
76
+ for (let i = 0; i < parts.length; i++) {
77
+ if (parts[i]) {
78
+ result.push(<span key={`text-${i}`}>{parts[i]}</span>)
79
+ }
80
+ if (i < parts.length - 1) {
81
+ result.push(
82
+ <span key={`highlight-${i}`} className="text-[var(--color-link)]">
83
+ {highlightText}
84
+ </span>
85
+ )
86
+ }
87
+ }
88
+ return <>{result}</>
89
+ }
90
+
91
+ return (
92
+ <div className="relative rounded-lg border border-border-tertiary bg-bg-base p-3">
93
+ {onClose && (
94
+ <button
95
+ type="button"
96
+ onClick={onClose}
97
+ className="absolute top-3 right-3 flex h-4 w-4 cursor-pointer items-center justify-center text-text-secondary transition-colors duration-200 hover:text-text"
98
+ aria-label="Close"
99
+ >
100
+ {defaultClose}
101
+ </button>
102
+ )}
103
+
104
+ <div className="mb-1 flex items-center gap-2">
105
+ <div
106
+ className={cn(
107
+ 'flex h-4 w-4 shrink-0 items-center justify-center',
108
+ isCreditsType ? 'text-warning' : 'text-primary'
109
+ )}
110
+ >
111
+ {defaultSuggestion}
112
+ </div>
113
+ <h3 className="min-w-0 flex-1 text-base leading-base font-semibold text-text">
114
+ {displayTitle}
115
+ </h3>
116
+ </div>
117
+
118
+ <>
119
+ <Scrollbar
120
+ scrollbarVisibility="auto"
121
+ maxHeight={96}
122
+ className="mb-2 ml-6 text-xs leading-xs text-text-tertiary"
123
+ >
124
+ {renderDescription()}
125
+ </Scrollbar>
126
+ </>
127
+
128
+ <div className="flex items-center justify-end gap-2">
129
+ {isCreditsType ? (
130
+ rechargeText && (
131
+ <Button variant="secondary" onClick={onRecharge}>
132
+ {rechargeText ?? '前往充值'}
133
+ </Button>
134
+ )
135
+ ) : (
136
+ <>
137
+ <DropdownMenu>
138
+ <DropdownMenuTrigger asChild>
139
+ <Button variant="ghost" className="flex items-center gap-1">
140
+ <span>{language}</span>
141
+ {defaultArrow}
142
+ </Button>
143
+ </DropdownMenuTrigger>
144
+ <DropdownMenuContent dataStyle={dataStyle} dataTheme={dataTheme}>
145
+ {LANGUAGES.map((lang) => (
146
+ <DropdownMenuItem
147
+ key={lang}
148
+ onSelect={() => onLanguageChange?.(lang)}
149
+ >
150
+ {lang}
151
+ </DropdownMenuItem>
152
+ ))}
153
+ </DropdownMenuContent>
154
+ </DropdownMenu>
155
+ <Button variant="secondary" onClick={onGenerate}>
156
+ Generate
157
+ </Button>
158
+ </>
159
+ )}
160
+ </div>
161
+ </div>
162
+ )
163
+ }
164
+
165
+ HintBanner.displayName = 'HintBanner'
@@ -0,0 +1,166 @@
1
+ import { forwardRef, useState, useCallback, useEffect } from 'react'
2
+ import type { HTMLAttributes } from 'react'
3
+ import { createPortal } from 'react-dom'
4
+ import { cn } from '@/lib/utils'
5
+ import type { ReactNode } from 'react'
6
+ import { CloseLine, CloseLargeLine } from '../basic/icons-inline'
7
+ import { IconButton } from '../basic/icon-button'
8
+
9
+ const SIZE_CLASS = {
10
+ sm: 'w-12 h-12',
11
+ md: 'w-16 h-16',
12
+ lg: 'w-20 h-20',
13
+ } as const
14
+
15
+ const PREVIEW_MAX = 'max-w-[90vw] max-h-[90vh] object-contain'
16
+ const closeClass = 'h-2.5 w-2.5 text-text-secondary'
17
+ const closeLargeClass = 'h-4 w-4 text-text-secondary'
18
+
19
+ export interface ImageAttachmentProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
20
+ src: string
21
+ alt?: string
22
+ onRemove?: (e: React.MouseEvent) => void
23
+ previewable?: boolean
24
+ onClick?: (e: React.MouseEvent) => void
25
+ size?: keyof typeof SIZE_CLASS
26
+ closeIcon?: ReactNode
27
+ closeLargeIcon?: ReactNode
28
+ }
29
+
30
+ export const ImageAttachment = forwardRef<HTMLDivElement, ImageAttachmentProps>(
31
+ (
32
+ {
33
+ src,
34
+ alt = '',
35
+ onRemove,
36
+ previewable = true,
37
+ onClick,
38
+ size = 'sm',
39
+ className,
40
+ closeIcon,
41
+ closeLargeIcon,
42
+ ...props
43
+ },
44
+ ref
45
+ ) => {
46
+ const [previewOpen, setPreviewOpen] = useState(false)
47
+ const defaultClose = closeIcon ?? <CloseLine className={closeClass} />
48
+ const defaultCloseLarge = closeLargeIcon ?? <CloseLargeLine className={closeLargeClass} />
49
+ const showClose = typeof onRemove === 'function'
50
+
51
+ const openPreview = useCallback(() => {
52
+ if (previewable) setPreviewOpen(true)
53
+ }, [previewable])
54
+
55
+ const closePreview = useCallback(() => setPreviewOpen(false), [])
56
+
57
+ const handleThumbClick = useCallback(
58
+ (e: React.MouseEvent) => {
59
+ onClick?.(e)
60
+ if (!e.defaultPrevented && previewable) openPreview()
61
+ },
62
+ [onClick, previewable, openPreview]
63
+ )
64
+
65
+ const handleRemove = useCallback(
66
+ (e: React.MouseEvent) => {
67
+ e.stopPropagation()
68
+ onRemove?.(e)
69
+ },
70
+ [onRemove]
71
+ )
72
+
73
+ useEffect(() => {
74
+ if (!previewOpen) return
75
+ const onKeyDown = (e: KeyboardEvent) => {
76
+ if (e.key === 'Escape') closePreview()
77
+ }
78
+ window.addEventListener('keydown', onKeyDown)
79
+ return () => window.removeEventListener('keydown', onKeyDown)
80
+ }, [previewOpen, closePreview])
81
+
82
+ const previewPortal =
83
+ previewOpen && typeof document !== 'undefined'
84
+ ? createPortal(
85
+ <div
86
+ className="fixed inset-0 z-[9999] flex items-center justify-center bg-bg-mask"
87
+ role="dialog"
88
+ aria-modal="true"
89
+ aria-label="图片预览"
90
+ onClick={(e) => {
91
+ if (e.target === e.currentTarget) closePreview()
92
+ }}
93
+ onKeyDown={(e) => e.key === 'Escape' && closePreview()}
94
+ >
95
+ <img
96
+ src={src}
97
+ alt={alt}
98
+ className={cn('rounded-lg shadow-xl', PREVIEW_MAX)}
99
+ onClick={(e) => e.stopPropagation()}
100
+ draggable={false}
101
+ />
102
+ <IconButton
103
+ variant="tertiary"
104
+ size="md"
105
+ rounded="pill"
106
+ icon={defaultCloseLarge}
107
+ onClick={closePreview}
108
+ aria-label="关闭预览"
109
+ className="absolute top-4 right-4 bg-bg-base border border-border-tertiary shadow-sm"
110
+ />
111
+ </div>,
112
+ document.body
113
+ )
114
+ : null
115
+
116
+ return (
117
+ <>
118
+ <div
119
+ ref={ref}
120
+ role={previewable ? 'button' : undefined}
121
+ tabIndex={previewable ? 0 : undefined}
122
+ className={cn(
123
+ 'relative group shrink-0',
124
+ previewable && 'cursor-pointer',
125
+ className
126
+ )}
127
+ onClick={handleThumbClick}
128
+ onKeyDown={
129
+ previewable
130
+ ? (e) => {
131
+ if (e.key === 'Enter' || e.key === ' ') {
132
+ e.preventDefault()
133
+ openPreview()
134
+ }
135
+ }
136
+ : undefined
137
+ }
138
+ {...props}
139
+ >
140
+ <img
141
+ src={src}
142
+ alt={alt}
143
+ className={cn(
144
+ 'rounded-lg object-cover border border-border-tertiary',
145
+ SIZE_CLASS[size]
146
+ )}
147
+ draggable={false}
148
+ />
149
+ {showClose && (
150
+ <button
151
+ type="button"
152
+ onClick={handleRemove}
153
+ aria-label="移除"
154
+ className="absolute -top-0.5 -right-0.5 flex h-4 w-4 cursor-pointer items-center justify-center rounded-full bg-bg-base border border-border-tertiary shadow-sm opacity-0 group-hover:opacity-100 transition-opacity hover:bg-fill-secondary"
155
+ >
156
+ {defaultClose}
157
+ </button>
158
+ )}
159
+ </div>
160
+ {previewPortal}
161
+ </>
162
+ )
163
+ }
164
+ )
165
+
166
+ ImageAttachment.displayName = 'ImageAttachment'
@@ -0,0 +1,281 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import type { HTMLAttributes, ReactNode } from 'react'
3
+ import { motion } from 'framer-motion'
4
+ import { SparklingLine } from '../basic/icons-inline'
5
+
6
+ export interface ImageGeneratingProps extends HTMLAttributes<HTMLDivElement> {
7
+ width?: string
8
+ height?: string
9
+ speed?: number
10
+ caption?: string
11
+ captionClassName?: string
12
+ showOverlay?: boolean
13
+ overlayText?: string
14
+ sparklingIcon?: ReactNode
15
+ }
16
+
17
+ function cn(...inputs: (string | undefined | false)[]): string {
18
+ return inputs.filter(Boolean).join(' ')
19
+ }
20
+
21
+ const defaultSparkling = <SparklingLine className="h-5 w-5 text-text-on-primary" />
22
+
23
+ export function ImageGenerating({
24
+ width = 'var(--spacing-80)',
25
+ height = 'var(--spacing-60)',
26
+ speed = 1.0,
27
+ caption = 'Qoder is drawing...',
28
+ captionClassName,
29
+ className,
30
+ style: styleProp,
31
+ showOverlay = false,
32
+ overlayText,
33
+ sparklingIcon = defaultSparkling,
34
+ ...props
35
+ }: ImageGeneratingProps) {
36
+ const containerRef = useRef<HTMLDivElement>(null)
37
+
38
+ useEffect(() => {
39
+ if (typeof CSS !== 'undefined' && CSS.registerProperty) {
40
+ try {
41
+ CSS.registerProperty({ name: '--a', syntax: '<angle>', inherits: true, initialValue: '0deg' })
42
+ CSS.registerProperty({ name: '--l', syntax: '<number>', inherits: true, initialValue: '0' })
43
+ CSS.registerProperty({ name: '--x', syntax: '<length>', inherits: false, initialValue: '0' })
44
+ CSS.registerProperty({ name: '--y', syntax: '<length>', inherits: false, initialValue: '0' })
45
+ CSS.registerProperty({ name: '--o', syntax: '<number>', inherits: false, initialValue: '0' })
46
+ CSS.registerProperty({ name: '--value', syntax: '<angle>', inherits: true, initialValue: '0deg' })
47
+ CSS.registerProperty({ name: '--width-ratio', syntax: '<number>', inherits: true, initialValue: '0' })
48
+ CSS.registerProperty({ name: '--scale', syntax: '<number>', inherits: true, initialValue: '0' })
49
+ } catch {
50
+ // ignore
51
+ }
52
+ }
53
+ }, [])
54
+
55
+ useEffect(() => {
56
+ const container = containerRef.current
57
+ if (!container) return
58
+ const updateSize = () => {
59
+ const rect = container.getBoundingClientRect()
60
+ if (rect.width > 0 && rect.height > 0) {
61
+ const size = Math.max(rect.width, rect.height)
62
+ container.style.setProperty('--actual-size', `${size}px`)
63
+ }
64
+ }
65
+ const isPercent = (typeof width === 'string' && width.includes('%')) || (typeof height === 'string' && height.includes('%'))
66
+ let resizeObserver: ResizeObserver | null = null
67
+ const setup = () => {
68
+ updateSize()
69
+ resizeObserver = new ResizeObserver(updateSize)
70
+ resizeObserver.observe(container)
71
+ }
72
+ if (isPercent) {
73
+ const id = requestAnimationFrame(() => setup())
74
+ return () => {
75
+ cancelAnimationFrame(id)
76
+ resizeObserver?.disconnect()
77
+ }
78
+ }
79
+ setup()
80
+ return () => resizeObserver?.disconnect()
81
+ }, [width, height])
82
+
83
+ return (
84
+ <>
85
+ <style>{`
86
+ @keyframes ai {
87
+ from { --a: 360deg; --l: 0.35; --o: 1; }
88
+ 30% { --l: 1.5; }
89
+ 70% { --o: 0.4; --l: 0.05; }
90
+ 98% { --o: 0.7; }
91
+ to { --a: 0deg; --l: 0.35; --o: 1; }
92
+ }
93
+ @keyframes ring {
94
+ from { --value: var(--start); --scale: 1; }
95
+ 50% { --scale: 1.2; --width-ratio: 1.5; }
96
+ 70% { --scale: 1; --value: calc(var(--start) + 180deg); --width-ratio: 1; }
97
+ 80% { --scale: 1.2; --width-ratio: 1.5; }
98
+ to { --value: calc(var(--start) + 360deg); --scale: 1; --width-ratio: 1; }
99
+ }
100
+ .image-generating-container {
101
+ --s: var(--actual-size, max(var(--container-width), var(--container-height)));
102
+ --p: calc(var(--s) / 4);
103
+ --radius: calc(var(--s) * 0.25);
104
+ --count: 4;
105
+ --width: calc(var(--s) * 0.025);
106
+ --duration: calc(8s / var(--image-generating-speed, 1));
107
+ --ai-duration: calc(5.5s / var(--image-generating-speed, 1));
108
+ width: var(--container-width);
109
+ height: var(--container-height);
110
+ --bg-color: color-mix(in srgb, var(--color-primary), transparent 92%);
111
+ background: radial-gradient(60% 75% at center, var(--bg-color) 50%, transparent 50%),
112
+ radial-gradient(75% 60% at center, var(--bg-color) 50%, transparent 50%);
113
+ overflow: hidden;
114
+ position: relative;
115
+ border-radius: var(--radius-xl);
116
+ }
117
+ .image-generating-container .fluid-inner {
118
+ overflow: hidden;
119
+ background: var(--color-bg-base);
120
+ width: 100%;
121
+ height: 100%;
122
+ position: relative;
123
+ display: grid;
124
+ place-items: center;
125
+ border-radius: var(--radius-xl);
126
+ }
127
+ .image-generating-container .c {
128
+ opacity: 0.9;
129
+ position: absolute;
130
+ width: calc(var(--s) * 0.4);
131
+ aspect-ratio: 1;
132
+ border-radius: 50%;
133
+ --offset-per-item: calc(360deg / var(--count));
134
+ --current-angle-offset: calc(var(--offset-per-item) * var(--i) + var(--a));
135
+ translate: calc(cos(var(--current-angle-offset)) * var(--radius) + var(--x, 0))
136
+ calc(sin(var(--current-angle-offset)) * var(--radius) * -1);
137
+ scale: calc(0.6 + var(--l));
138
+ animation: ai var(--ai-duration) cubic-bezier(0.45, -0.35, 0.16, 1.5) infinite;
139
+ transition: opacity 0.3s linear;
140
+ opacity: var(--o, 1);
141
+ }
142
+ .image-generating-container .c1 {
143
+ background: radial-gradient(50% 50% at center, var(--color-primary), color-mix(in srgb, var(--color-primary), transparent 30%));
144
+ --x: calc(var(--s) * 0.04);
145
+ width: calc(var(--s) * 0.6);
146
+ animation-timing-function: cubic-bezier(0.12, 0.32, 0.68, 0.24);
147
+ }
148
+ .image-generating-container .c2 {
149
+ background: radial-gradient(50% 50% at center, var(--color-primary-active), color-mix(in srgb, var(--color-primary-active), var(--color-bg-base) 45%));
150
+ width: calc(var(--s) * 0.55);
151
+ }
152
+ .image-generating-container .c3 {
153
+ background: radial-gradient(50% 50% at center, var(--color-primary-hover), transparent);
154
+ width: calc(var(--s) * 0.2);
155
+ opacity: 0.6;
156
+ --x: calc(var(--s) * -0.04);
157
+ }
158
+ .image-generating-container .c4 {
159
+ background: var(--color-primary-border-hover);
160
+ animation-timing-function: cubic-bezier(0.39, -0.03, 0.75, 0.47);
161
+ }
162
+ .image-generating-container .glass {
163
+ overflow: hidden;
164
+ position: absolute;
165
+ inset: 0;
166
+ backdrop-filter: blur(calc(var(--s) * 0.12));
167
+ box-shadow: 0 0 calc(var(--s) * 0.2) color-mix(in srgb, var(--color-text-base), transparent 70%);
168
+ border-radius: var(--radius-xl);
169
+ pointer-events: none;
170
+ }
171
+ .image-generating-container .glass::after {
172
+ content: "";
173
+ position: absolute;
174
+ inset: 0;
175
+ --c: color-mix(in srgb, var(--color-text-base), transparent 97%);
176
+ --w: 0.0625rem;
177
+ --g: 0.1875rem;
178
+ background: repeating-linear-gradient(
179
+ var(--c), var(--c), var(--w), transparent var(--w), transparent calc(var(--w) + var(--g))
180
+ );
181
+ border-radius: var(--radius-xl);
182
+ }
183
+ .image-generating-container .rings {
184
+ aspect-ratio: 1;
185
+ border-radius: 50%;
186
+ position: absolute;
187
+ inset: 0;
188
+ perspective: calc(var(--s) * 2.75);
189
+ opacity: 0.9;
190
+ }
191
+ .image-generating-container .rings::before,
192
+ .image-generating-container .rings::after {
193
+ content: "";
194
+ position: absolute;
195
+ inset: 0;
196
+ border-radius: 50%;
197
+ --width-ratio: 1;
198
+ border: calc(var(--width) * var(--width-ratio)) solid transparent;
199
+ mask: linear-gradient(var(--color-image-generating-mask, var(--color-bg-base)) 0 0) padding-box, linear-gradient(var(--color-image-generating-mask, var(--color-bg-base)) 0 0);
200
+ background: linear-gradient(
201
+ var(--color-bg-base),
202
+ var(--color-primary),
203
+ var(--color-primary-active),
204
+ var(--color-primary-hover)
205
+ ) border-box;
206
+ mask-composite: exclude;
207
+ animation: ring var(--duration) ease-in-out infinite;
208
+ --start: 180deg;
209
+ --value: var(--start);
210
+ --scale: 1;
211
+ transform: rotateY(var(--value)) rotateX(var(--value)) rotateZ(var(--value)) scale(var(--scale));
212
+ }
213
+ .image-generating-container .rings::before { --start: 180deg; }
214
+ .image-generating-container .rings::after { --start: 90deg; }
215
+ .image-generating-container .content-overlay {
216
+ position: absolute;
217
+ inset: 0;
218
+ display: flex;
219
+ flex-direction: column;
220
+ align-items: center;
221
+ justify-content: center;
222
+ gap: var(--spacing-3);
223
+ z-index: 2;
224
+ pointer-events: none;
225
+ }
226
+ .image-generating-container .content-overlay .caption {
227
+ font-size: var(--font-size-xs);
228
+ font-weight: 500;
229
+ color: var(--color-text-on-primary);
230
+ }
231
+ .image-generating-container .remaining-overlay {
232
+ background: var(--color-bg-mask);
233
+ border-radius: var(--radius-xl);
234
+ }
235
+ .image-generating-container .remaining-text {
236
+ font-size: var(--font-size-3xl);
237
+ font-weight: 600;
238
+ color: var(--color-text-on-primary);
239
+ }
240
+ `}</style>
241
+ <div
242
+ ref={containerRef}
243
+ className={cn('image-generating-container', className)}
244
+ style={{
245
+ ['--container-width' as string]: width,
246
+ ['--container-height' as string]: height,
247
+ ['--image-generating-speed' as string]: String(speed),
248
+ ...(styleProp && typeof styleProp === 'object' ? styleProp : {}),
249
+ }}
250
+ {...props}
251
+ >
252
+ <div className="fluid-inner">
253
+ <div className="c c4" style={{ ['--i' as string]: 0 }} />
254
+ <div className="c c1" style={{ ['--i' as string]: 1 }} />
255
+ <div className="c c2" style={{ ['--i' as string]: 2 }} />
256
+ <div className="c c3" style={{ ['--i' as string]: 3 }} />
257
+ <div className="rings" />
258
+ </div>
259
+ <div className="glass" />
260
+ {showOverlay ? (
261
+ <div className="content-overlay remaining-overlay">
262
+ <span className="remaining-text">{overlayText}</span>
263
+ </div>
264
+ ) : (
265
+ <div className="content-overlay">
266
+ <motion.span
267
+ className="inline-flex items-center justify-center"
268
+ animate={{ scale: [1, 1.08, 1], opacity: [0.9, 1, 0.9] }}
269
+ transition={{ duration: 1.2, repeat: Infinity, ease: [0.16, 1, 0.3, 1] as const }}
270
+ >
271
+ {sparklingIcon}
272
+ </motion.span>
273
+ <span className={cn('caption', captionClassName)}>{caption}</span>
274
+ </div>
275
+ )}
276
+ </div>
277
+ </>
278
+ )
279
+ }
280
+
281
+ ImageGenerating.displayName = 'ImageGenerating'