style: pretty up; fix extension shadcn
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
"tsx": true,
|
"tsx": true,
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "",
|
"config": "",
|
||||||
"css": "src/index.css",
|
"css": "src/assets/index.css",
|
||||||
"baseColor": "neutral",
|
"baseColor": "neutral",
|
||||||
"cssVariables": true,
|
"cssVariables": true,
|
||||||
"prefix": ""
|
"prefix": ""
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"utils": "@/lib/utils",
|
"utils": "@/lib/utils",
|
||||||
"ui": "@/components/ui",
|
"ui": "@/components/ui",
|
||||||
"lib": "@/lib",
|
"lib": "@/lib",
|
||||||
"hooks": "@/hooks"
|
"hooks": "@/lib/hooks"
|
||||||
},
|
},
|
||||||
"registries": {
|
"registries": {
|
||||||
"@magicui": "https://magicui.design/r/{name}.json"
|
"@magicui": "https://magicui.design/r/{name}.json"
|
||||||
|
|||||||
@@ -111,17 +111,40 @@
|
|||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--animate-blink-cursor: blink-cursor 1.2s step-end infinite;
|
||||||
|
@keyframes blink-cursor {
|
||||||
|
0%,
|
||||||
|
49% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50%,
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glow-breathe {
|
@keyframes glow-a {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.45;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glow-b {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.85);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.35;
|
opacity: 0.45;
|
||||||
transform: scale(1.1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
164
packages/extension/src/components/ui/typing-animation.tsx
Normal file
164
packages/extension/src/components/ui/typing-animation.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { MotionProps, motion, useInView } from 'motion/react'
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface TypingAnimationProps extends MotionProps {
|
||||||
|
children?: string
|
||||||
|
words?: string[]
|
||||||
|
className?: string
|
||||||
|
duration?: number
|
||||||
|
typeSpeed?: number
|
||||||
|
deleteSpeed?: number
|
||||||
|
delay?: number
|
||||||
|
pauseDelay?: number
|
||||||
|
loop?: boolean
|
||||||
|
as?: React.ElementType
|
||||||
|
startOnView?: boolean
|
||||||
|
showCursor?: boolean
|
||||||
|
blinkCursor?: boolean
|
||||||
|
cursorStyle?: 'line' | 'block' | 'underscore'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TypingAnimation({
|
||||||
|
children,
|
||||||
|
words,
|
||||||
|
className,
|
||||||
|
duration = 100,
|
||||||
|
typeSpeed,
|
||||||
|
deleteSpeed,
|
||||||
|
delay = 0,
|
||||||
|
pauseDelay = 1000,
|
||||||
|
loop = false,
|
||||||
|
as: Component = 'span',
|
||||||
|
startOnView = true,
|
||||||
|
showCursor = true,
|
||||||
|
blinkCursor = true,
|
||||||
|
cursorStyle = 'line',
|
||||||
|
...props
|
||||||
|
}: TypingAnimationProps) {
|
||||||
|
const MotionComponent = motion.create(Component, {
|
||||||
|
forwardMotionProps: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [displayedText, setDisplayedText] = useState<string>('')
|
||||||
|
const [currentWordIndex, setCurrentWordIndex] = useState(0)
|
||||||
|
const [currentCharIndex, setCurrentCharIndex] = useState(0)
|
||||||
|
const [phase, setPhase] = useState<'typing' | 'pause' | 'deleting'>('typing')
|
||||||
|
const elementRef = useRef<HTMLElement | null>(null)
|
||||||
|
const isInView = useInView(elementRef as React.RefObject<Element>, {
|
||||||
|
amount: 0.3,
|
||||||
|
once: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const wordsToAnimate = useMemo(() => words || (children ? [children] : []), [words, children])
|
||||||
|
const hasMultipleWords = wordsToAnimate.length > 1
|
||||||
|
|
||||||
|
const typingSpeed = typeSpeed || duration
|
||||||
|
const deletingSpeed = deleteSpeed || typingSpeed / 2
|
||||||
|
|
||||||
|
const shouldStart = startOnView ? isInView : true
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldStart || wordsToAnimate.length === 0) return
|
||||||
|
|
||||||
|
const timeoutDelay =
|
||||||
|
delay > 0 && displayedText === ''
|
||||||
|
? delay
|
||||||
|
: phase === 'typing'
|
||||||
|
? typingSpeed
|
||||||
|
: phase === 'deleting'
|
||||||
|
? deletingSpeed
|
||||||
|
: pauseDelay
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
const currentWord = wordsToAnimate[currentWordIndex] || ''
|
||||||
|
const graphemes = Array.from(currentWord)
|
||||||
|
|
||||||
|
switch (phase) {
|
||||||
|
case 'typing':
|
||||||
|
if (currentCharIndex < graphemes.length) {
|
||||||
|
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(''))
|
||||||
|
setCurrentCharIndex(currentCharIndex + 1)
|
||||||
|
} else {
|
||||||
|
if (hasMultipleWords || loop) {
|
||||||
|
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
|
||||||
|
if (!isLastWord || loop) {
|
||||||
|
setPhase('pause')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'pause':
|
||||||
|
setPhase('deleting')
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'deleting':
|
||||||
|
if (currentCharIndex > 0) {
|
||||||
|
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(''))
|
||||||
|
setCurrentCharIndex(currentCharIndex - 1)
|
||||||
|
} else {
|
||||||
|
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
|
||||||
|
setCurrentWordIndex(nextIndex)
|
||||||
|
setPhase('typing')
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, timeoutDelay)
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [
|
||||||
|
shouldStart,
|
||||||
|
phase,
|
||||||
|
currentCharIndex,
|
||||||
|
currentWordIndex,
|
||||||
|
displayedText,
|
||||||
|
wordsToAnimate,
|
||||||
|
hasMultipleWords,
|
||||||
|
loop,
|
||||||
|
typingSpeed,
|
||||||
|
deletingSpeed,
|
||||||
|
pauseDelay,
|
||||||
|
delay,
|
||||||
|
])
|
||||||
|
|
||||||
|
const currentWordGraphemes = Array.from(wordsToAnimate[currentWordIndex] || '')
|
||||||
|
const isComplete =
|
||||||
|
!loop &&
|
||||||
|
currentWordIndex === wordsToAnimate.length - 1 &&
|
||||||
|
currentCharIndex >= currentWordGraphemes.length &&
|
||||||
|
phase !== 'deleting'
|
||||||
|
|
||||||
|
const shouldShowCursor =
|
||||||
|
showCursor &&
|
||||||
|
!isComplete &&
|
||||||
|
(hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
|
||||||
|
|
||||||
|
const getCursorChar = () => {
|
||||||
|
switch (cursorStyle) {
|
||||||
|
case 'block':
|
||||||
|
return '▌'
|
||||||
|
case 'underscore':
|
||||||
|
return '_'
|
||||||
|
case 'line':
|
||||||
|
default:
|
||||||
|
return '|'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionComponent
|
||||||
|
ref={elementRef}
|
||||||
|
className={cn('leading-[5rem] tracking-[-0.02em]', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{displayedText}
|
||||||
|
{shouldShowCursor && (
|
||||||
|
<span className={cn('inline-block', blinkCursor && 'animate-blink-cursor')}>
|
||||||
|
{getCursorChar()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</MotionComponent>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -125,7 +125,7 @@ export default function App() {
|
|||||||
<Logo className="size-5" />
|
<Logo className="size-5" />
|
||||||
<span className="text-sm font-medium">Page Agent Ext</span>
|
<span className="text-sm font-medium">Page Agent Ext</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-1">
|
||||||
<StatusDot status={status} />
|
<StatusDot status={status} />
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { AgentStatus } from '@page-agent/core'
|
import type { AgentStatus } from '@page-agent/core'
|
||||||
import { Motion } from 'ai-motion'
|
import { Motion } from 'ai-motion'
|
||||||
|
import { BookOpen, Globe } from 'lucide-react'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { siGithub } from 'simple-icons'
|
||||||
|
|
||||||
|
import { TypingAnimation } from '@/components/ui/typing-animation'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
// Status dot indicator
|
// Status dot indicator
|
||||||
@@ -21,7 +24,7 @@ export function StatusDot({ status }: { status: AgentStatus }) {
|
|||||||
}[status]
|
}[status]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5 mr-2">
|
||||||
<span
|
<span
|
||||||
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
|
className={cn('size-2 rounded-full', colorClass, status === 'running' && 'animate-pulse')}
|
||||||
/>
|
/>
|
||||||
@@ -40,11 +43,12 @@ export function MotionOverlay({ active }: { active: boolean }) {
|
|||||||
const motionRef = useRef<Motion | null>(null)
|
const motionRef = useRef<Motion | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const mode = document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
||||||
const motion = new Motion({
|
const motion = new Motion({
|
||||||
mode: 'dark',
|
mode,
|
||||||
borderWidth: 0,
|
borderWidth: 4,
|
||||||
glowWidth: 120,
|
borderRadius: 14,
|
||||||
borderRadius: 0,
|
glowWidth: mode === 'dark' ? 120 : 60,
|
||||||
styles: { position: 'absolute', inset: '0' },
|
styles: { position: 'absolute', inset: '0' },
|
||||||
})
|
})
|
||||||
motionRef.current = motion
|
motionRef.current = motion
|
||||||
@@ -61,11 +65,15 @@ export function MotionOverlay({ active }: { active: boolean }) {
|
|||||||
const motion = motionRef.current
|
const motion = motionRef.current
|
||||||
if (!motion) return
|
if (!motion) return
|
||||||
|
|
||||||
|
let disposed = false
|
||||||
if (active) {
|
if (active) {
|
||||||
motion.start()
|
motion.start()
|
||||||
motion.fadeIn()
|
motion.fadeIn()
|
||||||
} else {
|
} else {
|
||||||
motion.fadeOut().then(() => motion.pause())
|
motion.fadeOut().then(() => !disposed && motion.pause())
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
disposed = true
|
||||||
}
|
}
|
||||||
}, [active])
|
}, [active])
|
||||||
|
|
||||||
@@ -81,14 +89,59 @@ export function MotionOverlay({ active }: { active: boolean }) {
|
|||||||
// Empty state with logo and breathing glow
|
// Empty state with logo and breathing glow
|
||||||
export function EmptyState() {
|
export function EmptyState() {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center h-full gap-3 text-center px-6">
|
<div className="flex flex-col items-center justify-center h-full gap-4 text-center px-6">
|
||||||
<div className="relative">
|
<div className="relative select-none pointer-events-none">
|
||||||
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_180deg,oklch(0.55_0.2_280),oklch(0.55_0.15_220),oklch(0.6_0.18_160),oklch(0.55_0.2_280))] opacity-0 blur-2xl animate-[glow-breathe_4s_ease-in-out_infinite]" />
|
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_180deg,oklch(0.55_0.2_280),oklch(0.5_0.15_230),oklch(0.6_0.18_310),oklch(0.55_0.2_280))] blur-2xl animate-[glow-a_5s_ease-in-out_infinite]" />
|
||||||
|
<div className="absolute inset-0 -m-6 rounded-full bg-[conic-gradient(from_0deg,oklch(0.55_0.18_160),oklch(0.5_0.2_200),oklch(0.6_0.15_120),oklch(0.55_0.18_160))] blur-2xl animate-[glow-b_5s_ease-in-out_infinite]" />
|
||||||
<Logo className="relative size-20 opacity-80" />
|
<Logo className="relative size-20 opacity-80" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-sm font-medium text-foreground">Page Agent Ext</h2>
|
<h2 className="text-base font-medium text-foreground mb-1">Page Agent Ext</h2>
|
||||||
<p className="text-xs text-muted-foreground mt-1">Enter a task to automate this page</p>
|
<TypingAnimation
|
||||||
|
className="text-sm text-muted-foreground"
|
||||||
|
words={[
|
||||||
|
'Enter a task to automate this page',
|
||||||
|
'Execute multi-page tasks',
|
||||||
|
'Call this extension from your web page',
|
||||||
|
'Use this extension in your own agents',
|
||||||
|
]}
|
||||||
|
cursorStyle="underscore"
|
||||||
|
loop
|
||||||
|
typeSpeed={20}
|
||||||
|
deleteSpeed={10}
|
||||||
|
pauseDelay={3000}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3 mt-1 text-muted-foreground">
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:text-foreground transition-colors"
|
||||||
|
title="GitHub"
|
||||||
|
>
|
||||||
|
<svg role="img" viewBox="0 0 24 24" className="size-4 fill-current">
|
||||||
|
<path d={siGithub.path} />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://alibaba.github.io/page-agent/#/docs/features/chrome-extension"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:text-foreground transition-colors"
|
||||||
|
title="Documentation"
|
||||||
|
>
|
||||||
|
<BookOpen className="size-4" />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://alibaba.github.io/page-agent"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="hover:text-foreground transition-colors"
|
||||||
|
title="Website"
|
||||||
|
>
|
||||||
|
<Globe className="size-4" />
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export default function ChromeExtension() {
|
|||||||
href={chromeWebStoreUrl}
|
href={chromeWebStoreUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors"
|
className="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white! font-medium rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<path d={siChromewebstore.path} />
|
<path d={siChromewebstore.path} />
|
||||||
@@ -74,7 +74,7 @@ export default function ChromeExtension() {
|
|||||||
href={githubReleasesUrl}
|
href={githubReleasesUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 text-white font-medium rounded-lg transition-colors"
|
className="inline-flex items-center gap-2 px-6 py-3 bg-gray-900 hover:bg-gray-800 dark:bg-gray-700 dark:hover:bg-gray-600 text-white! font-medium rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<path d={siGithub.path} />
|
<path d={siGithub.path} />
|
||||||
@@ -309,6 +309,7 @@ window.PAGE_AGENT_EXT.stop()`
|
|||||||
? '将 MultiPageAgent 集成你自己的插件'
|
? '将 MultiPageAgent 集成你自己的插件'
|
||||||
: 'Integrate MultiPageAgent into Your Extension'}
|
: 'Integrate MultiPageAgent into Your Extension'}
|
||||||
</h2>
|
</h2>
|
||||||
|
<p>@TODO</p>
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
<p className="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
{isZh
|
{isZh
|
||||||
? '建议先阅读扩展 API 文档,再参考 background entry implementation。'
|
? '建议先阅读扩展 API 文档,再参考 background entry implementation。'
|
||||||
|
|||||||
Reference in New Issue
Block a user