dsh-plugin-image-workbench 1.1.5 → 1.1.7
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/LICENSE +21 -21
- package/README.en.md +5 -3
- package/README.md +7 -3
- package/client.js +241 -241
- package/cordis.patch.yml +14 -14
- package/host.js +51 -0
- package/install.mjs +333 -387
- package/package.json +3 -2
- package/scripts/fetch-style-images.mjs +104 -0
- package/scripts/vendor-snapshot.py +154 -154
- package/server/access.py +26 -26
- package/server/app.py +1371 -1329
- package/server/catalog.py +147 -147
- package/server/composer.py +391 -391
- package/server/drafts.py +158 -158
- package/server/history.py +575 -575
- package/server/imaging.py +172 -172
- package/server/pool.py +1335 -1335
- package/server/pool_admin.py +921 -921
- package/server/pool_service.py +1073 -1073
- package/server/requirements.txt +6 -6
- package/server/smoke_check.py +402 -402
- package/server/static/access.html +202 -202
- package/server/static/index.html +15 -0
- package/vendor/chatgpt2api/source/.env.example +82 -82
- package/vendor/chatgpt2api/source/.python-version +1 -1
- package/vendor/chatgpt2api/source/LICENSE +21 -21
- package/vendor/chatgpt2api/source/README.md +391 -391
- package/vendor/chatgpt2api/source/VERSION +1 -1
- package/vendor/chatgpt2api/source/api/__init__.py +2 -2
- package/vendor/chatgpt2api/source/api/accounts.py +537 -537
- package/vendor/chatgpt2api/source/api/ai.py +217 -217
- package/vendor/chatgpt2api/source/api/app.py +62 -62
- package/vendor/chatgpt2api/source/api/errors.py +46 -46
- package/vendor/chatgpt2api/source/api/image_inputs.py +309 -309
- package/vendor/chatgpt2api/source/api/image_tasks.py +120 -120
- package/vendor/chatgpt2api/source/api/support.py +134 -134
- package/vendor/chatgpt2api/source/api/system.py +365 -365
- package/vendor/chatgpt2api/source/assets/kunrouai.svg +50 -50
- package/vendor/chatgpt2api/source/config.default.json +108 -108
- package/vendor/chatgpt2api/source/main.py +9 -9
- package/vendor/chatgpt2api/source/pyproject.toml +27 -27
- package/vendor/chatgpt2api/source/scripts/init_proxy_config.py +186 -186
- package/vendor/chatgpt2api/source/scripts/migrate_storage.py +163 -163
- package/vendor/chatgpt2api/source/scripts/privoxy-warp.conf +13 -13
- package/vendor/chatgpt2api/source/scripts/test_outlook_token_mailbox.py +385 -385
- package/vendor/chatgpt2api/source/scripts/test_storage.py +129 -129
- package/vendor/chatgpt2api/source/scripts/verify_oauth_refresh.py +92 -92
- package/vendor/chatgpt2api/source/services/account_service.py +1757 -1757
- package/vendor/chatgpt2api/source/services/auth_service.py +253 -253
- package/vendor/chatgpt2api/source/services/backup_service.py +671 -671
- package/vendor/chatgpt2api/source/services/config.py +667 -667
- package/vendor/chatgpt2api/source/services/content_filter.py +242 -242
- package/vendor/chatgpt2api/source/services/cpa_service.py +315 -315
- package/vendor/chatgpt2api/source/services/editable_file_task_service.py +260 -260
- package/vendor/chatgpt2api/source/services/image_service.py +376 -376
- package/vendor/chatgpt2api/source/services/image_storage_service.py +405 -405
- package/vendor/chatgpt2api/source/services/image_tags_service.py +76 -76
- package/vendor/chatgpt2api/source/services/image_task_service.py +550 -550
- package/vendor/chatgpt2api/source/services/log_service.py +337 -337
- package/vendor/chatgpt2api/source/services/model_service.py +182 -182
- package/vendor/chatgpt2api/source/services/oauth_login_service.py +267 -267
- package/vendor/chatgpt2api/source/services/openai_backend_api.py +2763 -2763
- package/vendor/chatgpt2api/source/services/openai_oauth.py +32 -32
- package/vendor/chatgpt2api/source/services/protocol/__init__.py +2 -2
- package/vendor/chatgpt2api/source/services/protocol/anthropic_v1_messages.py +307 -307
- package/vendor/chatgpt2api/source/services/protocol/chat_completion_cache.py +238 -238
- package/vendor/chatgpt2api/source/services/protocol/conversation.py +1644 -1644
- package/vendor/chatgpt2api/source/services/protocol/error_response.py +124 -124
- package/vendor/chatgpt2api/source/services/protocol/openai_search.py +19 -19
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_chat_complete.py +316 -316
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_edit.py +86 -86
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_image_generations.py +42 -42
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_models.py +53 -53
- package/vendor/chatgpt2api/source/services/protocol/openai_v1_response.py +453 -453
- package/vendor/chatgpt2api/source/services/protocol/web_search_tool.py +160 -160
- package/vendor/chatgpt2api/source/services/proxy_service.py +648 -648
- package/vendor/chatgpt2api/source/services/storage/__init__.py +5 -5
- package/vendor/chatgpt2api/source/services/storage/base.py +38 -38
- package/vendor/chatgpt2api/source/services/storage/database_storage.py +194 -194
- package/vendor/chatgpt2api/source/services/storage/factory.py +104 -104
- package/vendor/chatgpt2api/source/services/storage/git_storage.py +183 -183
- package/vendor/chatgpt2api/source/services/storage/json_storage.py +95 -95
- package/vendor/chatgpt2api/source/services/sub2api_service.py +531 -531
- package/vendor/chatgpt2api/source/utils/helper.py +469 -469
- package/vendor/chatgpt2api/source/utils/image_tokens.py +346 -346
- package/vendor/chatgpt2api/source/utils/log.py +110 -110
- package/vendor/chatgpt2api/source/utils/pkce.py +19 -19
- package/vendor/chatgpt2api/source/utils/pow.py +207 -207
- package/vendor/chatgpt2api/source/utils/sentinel.py +159 -159
- package/vendor/chatgpt2api/source/utils/turnstile.py +184 -184
- package/vendor/chatgpt2api/source/uv.lock +904 -904
- package/vendor/chatgpt2api/source/web/bun.lock +1501 -1501
- package/vendor/chatgpt2api/source/web/components.json +24 -24
- package/vendor/chatgpt2api/source/web/eslint.config.mjs +17 -17
- package/vendor/chatgpt2api/source/web/next.config.ts +40 -40
- package/vendor/chatgpt2api/source/web/package.json +58 -58
- package/vendor/chatgpt2api/source/web/postcss.config.mjs +5 -5
- package/vendor/chatgpt2api/source/web/src/app/accounts/components/account-import-dialog.tsx +925 -925
- package/vendor/chatgpt2api/source/web/src/app/accounts/page.tsx +1307 -1307
- package/vendor/chatgpt2api/source/web/src/app/debug/components/chat-panel.tsx +224 -224
- package/vendor/chatgpt2api/source/web/src/app/debug/components/editable-file-panel.tsx +444 -444
- package/vendor/chatgpt2api/source/web/src/app/debug/components/ppt-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/psd-panel.tsx +9 -9
- package/vendor/chatgpt2api/source/web/src/app/debug/components/search-panel.tsx +172 -172
- package/vendor/chatgpt2api/source/web/src/app/debug/components/skill-panel.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/debug/components/types.ts +39 -39
- package/vendor/chatgpt2api/source/web/src/app/debug/page.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/app/globals.css +230 -230
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-composer.tsx +542 -542
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-results.tsx +554 -554
- package/vendor/chatgpt2api/source/web/src/app/image/components/image-sidebar.tsx +176 -176
- package/vendor/chatgpt2api/source/web/src/app/image/page.tsx +1799 -1799
- package/vendor/chatgpt2api/source/web/src/app/image-manager/page.tsx +732 -732
- package/vendor/chatgpt2api/source/web/src/app/layout.tsx +50 -50
- package/vendor/chatgpt2api/source/web/src/app/login/page.tsx +101 -101
- package/vendor/chatgpt2api/source/web/src/app/logs/page.tsx +356 -356
- package/vendor/chatgpt2api/source/web/src/app/page.tsx +30 -30
- package/vendor/chatgpt2api/source/web/src/app/settings/components/api-docs-card.tsx +341 -341
- package/vendor/chatgpt2api/source/web/src/app/settings/components/backup-settings-card.tsx +447 -447
- package/vendor/chatgpt2api/source/web/src/app/settings/components/base-url-card.tsx +71 -71
- package/vendor/chatgpt2api/source/web/src/app/settings/components/config-card.tsx +491 -491
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pool-dialog.tsx +108 -108
- package/vendor/chatgpt2api/source/web/src/app/settings/components/cpa-pools-card.tsx +169 -169
- package/vendor/chatgpt2api/source/web/src/app/settings/components/import-browser-dialog.tsx +185 -185
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-runtime-card.tsx +379 -379
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings-card.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/app/settings/components/proxy-settings.tsx +200 -200
- package/vendor/chatgpt2api/source/web/src/app/settings/components/settings-header.tsx +12 -12
- package/vendor/chatgpt2api/source/web/src/app/settings/components/sub2api-connections.tsx +886 -886
- package/vendor/chatgpt2api/source/web/src/app/settings/components/third-party-apps-card.tsx +81 -81
- package/vendor/chatgpt2api/source/web/src/app/settings/components/user-keys-card.tsx +422 -422
- package/vendor/chatgpt2api/source/web/src/app/settings/page.tsx +135 -135
- package/vendor/chatgpt2api/source/web/src/app/settings/store.ts +1069 -1069
- package/vendor/chatgpt2api/source/web/src/components/date-range-filter.tsx +49 -49
- package/vendor/chatgpt2api/source/web/src/components/header-actions.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/image-lightbox.tsx +434 -434
- package/vendor/chatgpt2api/source/web/src/components/image-thumbnail.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/theme-script.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/theme-toggle.tsx +14 -14
- package/vendor/chatgpt2api/source/web/src/components/top-nav.tsx +263 -263
- package/vendor/chatgpt2api/source/web/src/components/ui/animated-theme-toggler.tsx +256 -256
- package/vendor/chatgpt2api/source/web/src/components/ui/badge.tsx +41 -41
- package/vendor/chatgpt2api/source/web/src/components/ui/button.tsx +59 -59
- package/vendor/chatgpt2api/source/web/src/components/ui/calendar.tsx +46 -46
- package/vendor/chatgpt2api/source/web/src/components/ui/card.tsx +61 -61
- package/vendor/chatgpt2api/source/web/src/components/ui/checkbox.tsx +27 -27
- package/vendor/chatgpt2api/source/web/src/components/ui/dia-text-reveal.tsx +267 -267
- package/vendor/chatgpt2api/source/web/src/components/ui/dialog.tsx +132 -132
- package/vendor/chatgpt2api/source/web/src/components/ui/field.tsx +13 -13
- package/vendor/chatgpt2api/source/web/src/components/ui/input.tsx +19 -19
- package/vendor/chatgpt2api/source/web/src/components/ui/label.tsx +24 -24
- package/vendor/chatgpt2api/source/web/src/components/ui/morphing-text.tsx +149 -149
- package/vendor/chatgpt2api/source/web/src/components/ui/popover.tsx +38 -38
- package/vendor/chatgpt2api/source/web/src/components/ui/select.tsx +175 -175
- package/vendor/chatgpt2api/source/web/src/components/ui/sheet.tsx +143 -143
- package/vendor/chatgpt2api/source/web/src/components/ui/table.tsx +29 -29
- package/vendor/chatgpt2api/source/web/src/components/ui/tabs.tsx +91 -91
- package/vendor/chatgpt2api/source/web/src/components/ui/text-3d-flip.tsx +269 -269
- package/vendor/chatgpt2api/source/web/src/components/ui/textarea.tsx +18 -18
- package/vendor/chatgpt2api/source/web/src/components/version-release-dialog.tsx +127 -127
- package/vendor/chatgpt2api/source/web/src/constants/common-env.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/hooks/use-version-check.ts +85 -85
- package/vendor/chatgpt2api/source/web/src/lib/api.ts +926 -926
- package/vendor/chatgpt2api/source/web/src/lib/auth-session.ts +26 -26
- package/vendor/chatgpt2api/source/web/src/lib/release.ts +25 -25
- package/vendor/chatgpt2api/source/web/src/lib/request.ts +93 -93
- package/vendor/chatgpt2api/source/web/src/lib/use-auth-guard.ts +89 -89
- package/vendor/chatgpt2api/source/web/src/lib/utils.ts +6 -6
- package/vendor/chatgpt2api/source/web/src/store/auth.ts +112 -112
- package/vendor/chatgpt2api/source/web/src/store/editable-file-history.ts +33 -33
- package/vendor/chatgpt2api/source/web/src/store/image-conversations.ts +318 -318
- package/vendor/chatgpt2api/source/web/tsconfig.json +42 -42
|
@@ -1,267 +1,267 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from "react"
|
|
4
|
-
import {
|
|
5
|
-
animate,
|
|
6
|
-
motion,
|
|
7
|
-
useInView,
|
|
8
|
-
useMotionValue,
|
|
9
|
-
useReducedMotion,
|
|
10
|
-
useTransform,
|
|
11
|
-
type HTMLMotionProps,
|
|
12
|
-
} from "motion/react"
|
|
13
|
-
|
|
14
|
-
import { cn } from "@/lib/utils"
|
|
15
|
-
|
|
16
|
-
const DEFAULT_COLORS = ["#c679c4", "#fa3d1d", "#ffb005", "#e1e1fe", "#0358f7"]
|
|
17
|
-
const BAND_HALF = 17
|
|
18
|
-
const SWEEP_START = -BAND_HALF
|
|
19
|
-
const SWEEP_END = 100 + BAND_HALF
|
|
20
|
-
|
|
21
|
-
const sweepEase = (t: number) =>
|
|
22
|
-
t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2
|
|
23
|
-
|
|
24
|
-
function buildGradient(pos: number, colors: string[], textColor: string) {
|
|
25
|
-
const bandStart = pos - BAND_HALF
|
|
26
|
-
const bandEnd = pos + BAND_HALF
|
|
27
|
-
|
|
28
|
-
if (bandStart >= 100) {
|
|
29
|
-
return `linear-gradient(90deg, ${textColor}, ${textColor})`
|
|
30
|
-
}
|
|
31
|
-
const n = colors.length
|
|
32
|
-
const parts: string[] = []
|
|
33
|
-
|
|
34
|
-
if (bandStart > 0)
|
|
35
|
-
parts.push(`${textColor} 0%`, `${textColor} ${bandStart.toFixed(2)}%`)
|
|
36
|
-
|
|
37
|
-
colors.forEach((c, i) => {
|
|
38
|
-
const pct = n === 1 ? pos : bandStart + (i / (n - 1)) * BAND_HALF * 2
|
|
39
|
-
parts.push(`${c} ${pct.toFixed(2)}%`)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
if (bandEnd < 100)
|
|
43
|
-
parts.push(`transparent ${bandEnd.toFixed(2)}%`, `transparent 100%`)
|
|
44
|
-
|
|
45
|
-
return `linear-gradient(90deg, ${parts.join(", ")})`
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function measureWidths(el: HTMLElement, texts: string[]) {
|
|
49
|
-
const ghost = el.cloneNode() as HTMLElement
|
|
50
|
-
Object.assign(ghost.style, {
|
|
51
|
-
position: "absolute",
|
|
52
|
-
visibility: "hidden",
|
|
53
|
-
pointerEvents: "none",
|
|
54
|
-
width: "auto",
|
|
55
|
-
whiteSpace: "nowrap",
|
|
56
|
-
})
|
|
57
|
-
el.parentElement!.appendChild(ghost)
|
|
58
|
-
const widths = texts.map((t) => {
|
|
59
|
-
ghost.textContent = t
|
|
60
|
-
return ghost.getBoundingClientRect().width
|
|
61
|
-
})
|
|
62
|
-
ghost.remove()
|
|
63
|
-
return widths
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Props for {@link DiaTextReveal}.
|
|
68
|
-
*/
|
|
69
|
-
export interface DiaTextRevealProps extends Omit<
|
|
70
|
-
HTMLMotionProps<"span">,
|
|
71
|
-
"ref" | "children" | "style" | "animate" | "transition" | "color"
|
|
72
|
-
> {
|
|
73
|
-
/**
|
|
74
|
-
* Text to reveal. Pass multiple strings to rotate when {@link DiaTextRevealProps.repeat} is `true`.
|
|
75
|
-
*/
|
|
76
|
-
text: string | string[]
|
|
77
|
-
/**
|
|
78
|
-
* Colors sampled across the moving gradient band. Defaults to a built-in palette.
|
|
79
|
-
*/
|
|
80
|
-
colors?: string[]
|
|
81
|
-
/**
|
|
82
|
-
* CSS color for revealed text after the sweep and for leading/trailing regions during the animation.
|
|
83
|
-
* @defaultValue `"var(--foreground)"`
|
|
84
|
-
*/
|
|
85
|
-
textColor?: string
|
|
86
|
-
/**
|
|
87
|
-
* Duration of one sweep pass, in seconds.
|
|
88
|
-
* @defaultValue `1.5`
|
|
89
|
-
*/
|
|
90
|
-
duration?: number
|
|
91
|
-
/**
|
|
92
|
-
* Delay before the sweep starts, in seconds.
|
|
93
|
-
* @defaultValue `0`
|
|
94
|
-
*/
|
|
95
|
-
delay?: number
|
|
96
|
-
/**
|
|
97
|
-
* When `text` is an array, replay the sweep and advance to the next string after each completion.
|
|
98
|
-
* @defaultValue `false`
|
|
99
|
-
*/
|
|
100
|
-
repeat?: boolean
|
|
101
|
-
/**
|
|
102
|
-
* Pause between cycles when {@link DiaTextRevealProps.repeat} is `true`, in seconds.
|
|
103
|
-
* @defaultValue `0.5`
|
|
104
|
-
*/
|
|
105
|
-
repeatDelay?: number
|
|
106
|
-
/**
|
|
107
|
-
* If `true`, the animation starts only after the element enters the viewport.
|
|
108
|
-
* @defaultValue `true`
|
|
109
|
-
*/
|
|
110
|
-
startOnView?: boolean
|
|
111
|
-
/**
|
|
112
|
-
* Passed to `useInView`: if `true`, in-view detection fires at most once (no replay on scroll-back).
|
|
113
|
-
* @defaultValue `true`
|
|
114
|
-
*/
|
|
115
|
-
once?: boolean
|
|
116
|
-
/**
|
|
117
|
-
* Additional class names for the animated `span` (e.g. typography utilities).
|
|
118
|
-
*/
|
|
119
|
-
className?: string
|
|
120
|
-
/**
|
|
121
|
-
* When `text` has multiple entries, use the widest string’s width for layout instead of animating width per line.
|
|
122
|
-
* @defaultValue `false`
|
|
123
|
-
*/
|
|
124
|
-
fixedWidth?: boolean
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function DiaTextReveal({
|
|
128
|
-
text,
|
|
129
|
-
colors = DEFAULT_COLORS,
|
|
130
|
-
textColor = "var(--foreground)",
|
|
131
|
-
duration = 1.5,
|
|
132
|
-
delay = 0,
|
|
133
|
-
repeat = false,
|
|
134
|
-
repeatDelay = 0.5,
|
|
135
|
-
startOnView = true,
|
|
136
|
-
once = true,
|
|
137
|
-
className,
|
|
138
|
-
fixedWidth = false,
|
|
139
|
-
...props
|
|
140
|
-
}: DiaTextRevealProps) {
|
|
141
|
-
const texts = useMemo(() => (Array.isArray(text) ? text : [text]), [text])
|
|
142
|
-
const textKey = texts.join("\0")
|
|
143
|
-
const isMulti = texts.length > 1
|
|
144
|
-
const prefersReducedMotion = useReducedMotion()
|
|
145
|
-
|
|
146
|
-
const spanRef = useRef<HTMLSpanElement>(null)
|
|
147
|
-
const optsRef = useRef({
|
|
148
|
-
colors,
|
|
149
|
-
textColor,
|
|
150
|
-
duration,
|
|
151
|
-
delay,
|
|
152
|
-
repeat,
|
|
153
|
-
repeatDelay,
|
|
154
|
-
texts,
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
const indexRef = useRef(0)
|
|
158
|
-
const hasPlayedRef = useRef(false)
|
|
159
|
-
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
160
|
-
const playRef = useRef<() => void>(null!)
|
|
161
|
-
const stopRef = useRef<(() => void) | null>(null)
|
|
162
|
-
|
|
163
|
-
const [activeIndex, setActiveIndex] = useState(0)
|
|
164
|
-
const [measuredWidths, setMeasuredWidths] = useState<number[]>([])
|
|
165
|
-
|
|
166
|
-
const sweepPos = useMotionValue(SWEEP_START)
|
|
167
|
-
|
|
168
|
-
const backgroundImage = useTransform(sweepPos, (pos) =>
|
|
169
|
-
buildGradient(pos, optsRef.current.colors, optsRef.current.textColor)
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
const isInView = useInView(spanRef, { once, amount: 0.1 })
|
|
173
|
-
|
|
174
|
-
optsRef.current = {
|
|
175
|
-
colors,
|
|
176
|
-
textColor,
|
|
177
|
-
duration,
|
|
178
|
-
delay,
|
|
179
|
-
repeat,
|
|
180
|
-
repeatDelay,
|
|
181
|
-
texts,
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
useEffect(() => {
|
|
185
|
-
const el = spanRef.current
|
|
186
|
-
if (!el || !isMulti) return
|
|
187
|
-
setMeasuredWidths(measureWidths(el, texts))
|
|
188
|
-
}, [isMulti, textKey, texts])
|
|
189
|
-
|
|
190
|
-
useEffect(() => {
|
|
191
|
-
playRef.current = () => {
|
|
192
|
-
const { duration, delay, repeat, repeatDelay, texts } = optsRef.current
|
|
193
|
-
|
|
194
|
-
sweepPos.set(SWEEP_START)
|
|
195
|
-
|
|
196
|
-
const controls = animate(sweepPos, SWEEP_END, {
|
|
197
|
-
duration,
|
|
198
|
-
delay,
|
|
199
|
-
ease: sweepEase,
|
|
200
|
-
onComplete() {
|
|
201
|
-
if (!repeat) return
|
|
202
|
-
timerRef.current = setTimeout(() => {
|
|
203
|
-
const next = (indexRef.current + 1) % texts.length
|
|
204
|
-
indexRef.current = next
|
|
205
|
-
setActiveIndex(next)
|
|
206
|
-
playRef.current()
|
|
207
|
-
}, repeatDelay * 1000)
|
|
208
|
-
},
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
stopRef.current = () => controls.stop()
|
|
212
|
-
}
|
|
213
|
-
}, [sweepPos])
|
|
214
|
-
|
|
215
|
-
useEffect(() => {
|
|
216
|
-
if (prefersReducedMotion) {
|
|
217
|
-
sweepPos.set(SWEEP_END)
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
if (startOnView && !isInView) return
|
|
221
|
-
if (once && hasPlayedRef.current) return
|
|
222
|
-
hasPlayedRef.current = true
|
|
223
|
-
playRef.current()
|
|
224
|
-
|
|
225
|
-
return () => {
|
|
226
|
-
stopRef.current?.()
|
|
227
|
-
clearTimeout(timerRef.current)
|
|
228
|
-
}
|
|
229
|
-
}, [isInView, startOnView, once, prefersReducedMotion, sweepPos])
|
|
230
|
-
|
|
231
|
-
const fixedW =
|
|
232
|
-
isMulti && fixedWidth && measuredWidths.length > 0
|
|
233
|
-
? Math.max(...measuredWidths)
|
|
234
|
-
: undefined
|
|
235
|
-
|
|
236
|
-
const animatedW =
|
|
237
|
-
isMulti && !fixedWidth && measuredWidths[activeIndex] != null
|
|
238
|
-
? measuredWidths[activeIndex]
|
|
239
|
-
: undefined
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
<motion.span
|
|
243
|
-
ref={spanRef}
|
|
244
|
-
className={cn("align-bottom leading-[100%] text-inherit", className)}
|
|
245
|
-
style={{
|
|
246
|
-
transform: "translateY(-2px)",
|
|
247
|
-
color: "transparent",
|
|
248
|
-
backgroundClip: "text",
|
|
249
|
-
WebkitBackgroundClip: "text",
|
|
250
|
-
backgroundSize: "100% 100%",
|
|
251
|
-
backgroundImage,
|
|
252
|
-
...(isMulti && {
|
|
253
|
-
display: "inline-block",
|
|
254
|
-
overflow: "hidden",
|
|
255
|
-
whiteSpace: "nowrap",
|
|
256
|
-
verticalAlign: "text-center",
|
|
257
|
-
...(fixedW != null && { width: fixedW }),
|
|
258
|
-
}),
|
|
259
|
-
}}
|
|
260
|
-
animate={animatedW != null ? { width: animatedW } : undefined}
|
|
261
|
-
transition={{ duration: 0.4, ease: [0.4, 0, 0.2, 1] }}
|
|
262
|
-
{...props}
|
|
263
|
-
>
|
|
264
|
-
{texts[activeIndex]}
|
|
265
|
-
</motion.span>
|
|
266
|
-
)
|
|
267
|
-
}
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from "react"
|
|
4
|
+
import {
|
|
5
|
+
animate,
|
|
6
|
+
motion,
|
|
7
|
+
useInView,
|
|
8
|
+
useMotionValue,
|
|
9
|
+
useReducedMotion,
|
|
10
|
+
useTransform,
|
|
11
|
+
type HTMLMotionProps,
|
|
12
|
+
} from "motion/react"
|
|
13
|
+
|
|
14
|
+
import { cn } from "@/lib/utils"
|
|
15
|
+
|
|
16
|
+
const DEFAULT_COLORS = ["#c679c4", "#fa3d1d", "#ffb005", "#e1e1fe", "#0358f7"]
|
|
17
|
+
const BAND_HALF = 17
|
|
18
|
+
const SWEEP_START = -BAND_HALF
|
|
19
|
+
const SWEEP_END = 100 + BAND_HALF
|
|
20
|
+
|
|
21
|
+
const sweepEase = (t: number) =>
|
|
22
|
+
t < 0.5 ? 4 * t ** 3 : 1 - (-2 * t + 2) ** 3 / 2
|
|
23
|
+
|
|
24
|
+
function buildGradient(pos: number, colors: string[], textColor: string) {
|
|
25
|
+
const bandStart = pos - BAND_HALF
|
|
26
|
+
const bandEnd = pos + BAND_HALF
|
|
27
|
+
|
|
28
|
+
if (bandStart >= 100) {
|
|
29
|
+
return `linear-gradient(90deg, ${textColor}, ${textColor})`
|
|
30
|
+
}
|
|
31
|
+
const n = colors.length
|
|
32
|
+
const parts: string[] = []
|
|
33
|
+
|
|
34
|
+
if (bandStart > 0)
|
|
35
|
+
parts.push(`${textColor} 0%`, `${textColor} ${bandStart.toFixed(2)}%`)
|
|
36
|
+
|
|
37
|
+
colors.forEach((c, i) => {
|
|
38
|
+
const pct = n === 1 ? pos : bandStart + (i / (n - 1)) * BAND_HALF * 2
|
|
39
|
+
parts.push(`${c} ${pct.toFixed(2)}%`)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
if (bandEnd < 100)
|
|
43
|
+
parts.push(`transparent ${bandEnd.toFixed(2)}%`, `transparent 100%`)
|
|
44
|
+
|
|
45
|
+
return `linear-gradient(90deg, ${parts.join(", ")})`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function measureWidths(el: HTMLElement, texts: string[]) {
|
|
49
|
+
const ghost = el.cloneNode() as HTMLElement
|
|
50
|
+
Object.assign(ghost.style, {
|
|
51
|
+
position: "absolute",
|
|
52
|
+
visibility: "hidden",
|
|
53
|
+
pointerEvents: "none",
|
|
54
|
+
width: "auto",
|
|
55
|
+
whiteSpace: "nowrap",
|
|
56
|
+
})
|
|
57
|
+
el.parentElement!.appendChild(ghost)
|
|
58
|
+
const widths = texts.map((t) => {
|
|
59
|
+
ghost.textContent = t
|
|
60
|
+
return ghost.getBoundingClientRect().width
|
|
61
|
+
})
|
|
62
|
+
ghost.remove()
|
|
63
|
+
return widths
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Props for {@link DiaTextReveal}.
|
|
68
|
+
*/
|
|
69
|
+
export interface DiaTextRevealProps extends Omit<
|
|
70
|
+
HTMLMotionProps<"span">,
|
|
71
|
+
"ref" | "children" | "style" | "animate" | "transition" | "color"
|
|
72
|
+
> {
|
|
73
|
+
/**
|
|
74
|
+
* Text to reveal. Pass multiple strings to rotate when {@link DiaTextRevealProps.repeat} is `true`.
|
|
75
|
+
*/
|
|
76
|
+
text: string | string[]
|
|
77
|
+
/**
|
|
78
|
+
* Colors sampled across the moving gradient band. Defaults to a built-in palette.
|
|
79
|
+
*/
|
|
80
|
+
colors?: string[]
|
|
81
|
+
/**
|
|
82
|
+
* CSS color for revealed text after the sweep and for leading/trailing regions during the animation.
|
|
83
|
+
* @defaultValue `"var(--foreground)"`
|
|
84
|
+
*/
|
|
85
|
+
textColor?: string
|
|
86
|
+
/**
|
|
87
|
+
* Duration of one sweep pass, in seconds.
|
|
88
|
+
* @defaultValue `1.5`
|
|
89
|
+
*/
|
|
90
|
+
duration?: number
|
|
91
|
+
/**
|
|
92
|
+
* Delay before the sweep starts, in seconds.
|
|
93
|
+
* @defaultValue `0`
|
|
94
|
+
*/
|
|
95
|
+
delay?: number
|
|
96
|
+
/**
|
|
97
|
+
* When `text` is an array, replay the sweep and advance to the next string after each completion.
|
|
98
|
+
* @defaultValue `false`
|
|
99
|
+
*/
|
|
100
|
+
repeat?: boolean
|
|
101
|
+
/**
|
|
102
|
+
* Pause between cycles when {@link DiaTextRevealProps.repeat} is `true`, in seconds.
|
|
103
|
+
* @defaultValue `0.5`
|
|
104
|
+
*/
|
|
105
|
+
repeatDelay?: number
|
|
106
|
+
/**
|
|
107
|
+
* If `true`, the animation starts only after the element enters the viewport.
|
|
108
|
+
* @defaultValue `true`
|
|
109
|
+
*/
|
|
110
|
+
startOnView?: boolean
|
|
111
|
+
/**
|
|
112
|
+
* Passed to `useInView`: if `true`, in-view detection fires at most once (no replay on scroll-back).
|
|
113
|
+
* @defaultValue `true`
|
|
114
|
+
*/
|
|
115
|
+
once?: boolean
|
|
116
|
+
/**
|
|
117
|
+
* Additional class names for the animated `span` (e.g. typography utilities).
|
|
118
|
+
*/
|
|
119
|
+
className?: string
|
|
120
|
+
/**
|
|
121
|
+
* When `text` has multiple entries, use the widest string’s width for layout instead of animating width per line.
|
|
122
|
+
* @defaultValue `false`
|
|
123
|
+
*/
|
|
124
|
+
fixedWidth?: boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function DiaTextReveal({
|
|
128
|
+
text,
|
|
129
|
+
colors = DEFAULT_COLORS,
|
|
130
|
+
textColor = "var(--foreground)",
|
|
131
|
+
duration = 1.5,
|
|
132
|
+
delay = 0,
|
|
133
|
+
repeat = false,
|
|
134
|
+
repeatDelay = 0.5,
|
|
135
|
+
startOnView = true,
|
|
136
|
+
once = true,
|
|
137
|
+
className,
|
|
138
|
+
fixedWidth = false,
|
|
139
|
+
...props
|
|
140
|
+
}: DiaTextRevealProps) {
|
|
141
|
+
const texts = useMemo(() => (Array.isArray(text) ? text : [text]), [text])
|
|
142
|
+
const textKey = texts.join("\0")
|
|
143
|
+
const isMulti = texts.length > 1
|
|
144
|
+
const prefersReducedMotion = useReducedMotion()
|
|
145
|
+
|
|
146
|
+
const spanRef = useRef<HTMLSpanElement>(null)
|
|
147
|
+
const optsRef = useRef({
|
|
148
|
+
colors,
|
|
149
|
+
textColor,
|
|
150
|
+
duration,
|
|
151
|
+
delay,
|
|
152
|
+
repeat,
|
|
153
|
+
repeatDelay,
|
|
154
|
+
texts,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const indexRef = useRef(0)
|
|
158
|
+
const hasPlayedRef = useRef(false)
|
|
159
|
+
const timerRef = useRef<ReturnType<typeof setTimeout>>(undefined)
|
|
160
|
+
const playRef = useRef<() => void>(null!)
|
|
161
|
+
const stopRef = useRef<(() => void) | null>(null)
|
|
162
|
+
|
|
163
|
+
const [activeIndex, setActiveIndex] = useState(0)
|
|
164
|
+
const [measuredWidths, setMeasuredWidths] = useState<number[]>([])
|
|
165
|
+
|
|
166
|
+
const sweepPos = useMotionValue(SWEEP_START)
|
|
167
|
+
|
|
168
|
+
const backgroundImage = useTransform(sweepPos, (pos) =>
|
|
169
|
+
buildGradient(pos, optsRef.current.colors, optsRef.current.textColor)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const isInView = useInView(spanRef, { once, amount: 0.1 })
|
|
173
|
+
|
|
174
|
+
optsRef.current = {
|
|
175
|
+
colors,
|
|
176
|
+
textColor,
|
|
177
|
+
duration,
|
|
178
|
+
delay,
|
|
179
|
+
repeat,
|
|
180
|
+
repeatDelay,
|
|
181
|
+
texts,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const el = spanRef.current
|
|
186
|
+
if (!el || !isMulti) return
|
|
187
|
+
setMeasuredWidths(measureWidths(el, texts))
|
|
188
|
+
}, [isMulti, textKey, texts])
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
playRef.current = () => {
|
|
192
|
+
const { duration, delay, repeat, repeatDelay, texts } = optsRef.current
|
|
193
|
+
|
|
194
|
+
sweepPos.set(SWEEP_START)
|
|
195
|
+
|
|
196
|
+
const controls = animate(sweepPos, SWEEP_END, {
|
|
197
|
+
duration,
|
|
198
|
+
delay,
|
|
199
|
+
ease: sweepEase,
|
|
200
|
+
onComplete() {
|
|
201
|
+
if (!repeat) return
|
|
202
|
+
timerRef.current = setTimeout(() => {
|
|
203
|
+
const next = (indexRef.current + 1) % texts.length
|
|
204
|
+
indexRef.current = next
|
|
205
|
+
setActiveIndex(next)
|
|
206
|
+
playRef.current()
|
|
207
|
+
}, repeatDelay * 1000)
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
stopRef.current = () => controls.stop()
|
|
212
|
+
}
|
|
213
|
+
}, [sweepPos])
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (prefersReducedMotion) {
|
|
217
|
+
sweepPos.set(SWEEP_END)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if (startOnView && !isInView) return
|
|
221
|
+
if (once && hasPlayedRef.current) return
|
|
222
|
+
hasPlayedRef.current = true
|
|
223
|
+
playRef.current()
|
|
224
|
+
|
|
225
|
+
return () => {
|
|
226
|
+
stopRef.current?.()
|
|
227
|
+
clearTimeout(timerRef.current)
|
|
228
|
+
}
|
|
229
|
+
}, [isInView, startOnView, once, prefersReducedMotion, sweepPos])
|
|
230
|
+
|
|
231
|
+
const fixedW =
|
|
232
|
+
isMulti && fixedWidth && measuredWidths.length > 0
|
|
233
|
+
? Math.max(...measuredWidths)
|
|
234
|
+
: undefined
|
|
235
|
+
|
|
236
|
+
const animatedW =
|
|
237
|
+
isMulti && !fixedWidth && measuredWidths[activeIndex] != null
|
|
238
|
+
? measuredWidths[activeIndex]
|
|
239
|
+
: undefined
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<motion.span
|
|
243
|
+
ref={spanRef}
|
|
244
|
+
className={cn("align-bottom leading-[100%] text-inherit", className)}
|
|
245
|
+
style={{
|
|
246
|
+
transform: "translateY(-2px)",
|
|
247
|
+
color: "transparent",
|
|
248
|
+
backgroundClip: "text",
|
|
249
|
+
WebkitBackgroundClip: "text",
|
|
250
|
+
backgroundSize: "100% 100%",
|
|
251
|
+
backgroundImage,
|
|
252
|
+
...(isMulti && {
|
|
253
|
+
display: "inline-block",
|
|
254
|
+
overflow: "hidden",
|
|
255
|
+
whiteSpace: "nowrap",
|
|
256
|
+
verticalAlign: "text-center",
|
|
257
|
+
...(fixedW != null && { width: fixedW }),
|
|
258
|
+
}),
|
|
259
|
+
}}
|
|
260
|
+
animate={animatedW != null ? { width: animatedW } : undefined}
|
|
261
|
+
transition={{ duration: 0.4, ease: [0.4, 0, 0.2, 1] }}
|
|
262
|
+
{...props}
|
|
263
|
+
>
|
|
264
|
+
{texts[activeIndex]}
|
|
265
|
+
</motion.span>
|
|
266
|
+
)
|
|
267
|
+
}
|