fix(skill): use SearchInput with debounce and align card to Figma

Replace custom search input with SearchInput component (built-in clear
button) and add 300ms debounce. Fix template card: use Tailwind token
for icon background, fix Badge to use children with badge-s class and
uppercase, match empty-tag fallback height to badge size.
This commit is contained in:
yyh
2026-01-30 15:30:28 +08:00
parent c33d27938d
commit abe2b37e3a
3 changed files with 24 additions and 25 deletions

View File

@ -17,7 +17,7 @@ import { buildUploadDataFromTemplate } from './templates/template-to-upload'
const SkillTemplatesSection = () => { const SkillTemplatesSection = () => {
const { t } = useTranslation('workflow') const { t } = useTranslation('workflow')
const [activeCategory, setActiveCategory] = useState('all') const [activeCategory, setActiveCategory] = useState('all')
const [searchValue, setSearchValue] = useState('') const [searchQuery, setSearchQuery] = useState('')
const [loadingId, setLoadingId] = useState<string | null>(null) const [loadingId, setLoadingId] = useState<string | null>(null)
const appDetail = useAppStore(s => s.appDetail) const appDetail = useAppStore(s => s.appDetail)
@ -65,14 +65,14 @@ const SkillTemplatesSection = () => {
}, [appId, storeApi]) }, [appId, storeApi])
const filtered = useMemo(() => SKILL_TEMPLATES.filter((entry) => { const filtered = useMemo(() => SKILL_TEMPLATES.filter((entry) => {
if (searchValue) { if (searchQuery) {
const q = searchValue.toLowerCase() const q = searchQuery.toLowerCase()
return entry.name.toLowerCase().includes(q) || entry.description.toLowerCase().includes(q) return entry.name.toLowerCase().includes(q) || entry.description.toLowerCase().includes(q)
} }
if (activeCategory !== 'all') if (activeCategory !== 'all')
return entry.tags?.some(tag => tag.toLowerCase() === activeCategory.toLowerCase()) return entry.tags?.some(tag => tag.toLowerCase() === activeCategory.toLowerCase())
return true return true
}), [searchValue, activeCategory]) }), [searchQuery, activeCategory])
return ( return (
<section className="flex flex-col gap-3 px-6 py-2"> <section className="flex flex-col gap-3 px-6 py-2">
@ -86,8 +86,7 @@ const SkillTemplatesSection = () => {
onCategoryChange={setActiveCategory} onCategoryChange={setActiveCategory}
/> />
<TemplateSearch <TemplateSearch
value={searchValue} onChange={setSearchQuery}
onChange={setSearchValue}
/> />
</div> </div>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">

View File

@ -22,7 +22,7 @@ const TemplateCard = ({ template, onUse }: TemplateCardProps) => {
<AppIcon <AppIcon
size="large" size="large"
icon={template.icon || '📁'} icon={template.icon || '📁'}
background="#f5f3ff" className="!bg-components-icon-bg-violet-soft"
/> />
<div className="flex min-w-0 flex-1 flex-col gap-0.5 py-px"> <div className="flex min-w-0 flex-1 flex-col gap-0.5 py-px">
<span className="system-md-semibold truncate text-text-secondary"> <span className="system-md-semibold truncate text-text-secondary">
@ -43,11 +43,11 @@ const TemplateCard = ({ template, onUse }: TemplateCardProps) => {
? ( ? (
<div className="flex flex-wrap gap-1 transition-opacity group-hover:opacity-0"> <div className="flex flex-wrap gap-1 transition-opacity group-hover:opacity-0">
{template.tags.map(tag => ( {template.tags.map(tag => (
<Badge key={tag} text={tag} /> <Badge key={tag} className="badge-s" uppercase>{tag}</Badge>
))} ))}
</div> </div>
) )
: <div className="h-5" />} : <div className="h-[18px]" />}
<div className="pointer-events-none absolute inset-0 flex items-end px-4 pb-4 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100"> <div className="pointer-events-none absolute inset-0 flex items-end px-4 pb-4 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100">
<Button <Button
variant="primary" variant="primary"

View File

@ -1,33 +1,33 @@
'use client' 'use client'
import { RiSearchLine } from '@remixicon/react' import { useDebounceFn } from 'ahooks'
import { memo } from 'react' import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import SearchInput from '@/app/components/base/search-input'
type TemplateSearchProps = { type TemplateSearchProps = {
value: string
onChange: (value: string) => void onChange: (value: string) => void
} }
const TemplateSearch = ({ const TemplateSearch = ({
value,
onChange, onChange,
}: TemplateSearchProps) => { }: TemplateSearchProps) => {
const { t } = useTranslation('workflow') const { t } = useTranslation('workflow')
const [localValue, setLocalValue] = useState('')
const { run: debouncedOnChange } = useDebounceFn(onChange, { wait: 300 })
const handleChange = useCallback((v: string) => {
setLocalValue(v)
debouncedOnChange(v)
}, [debouncedOnChange])
return ( return (
<div className="flex shrink-0 items-center gap-0.5 rounded-md bg-components-input-bg-normal p-2"> <SearchInput
<RiSearchLine className="size-4 shrink-0 text-text-placeholder" aria-hidden="true" /> className="!h-7"
<input placeholder={t('skill.startTab.searchPlaceholder')}
type="text" value={localValue}
name="template-search" onChange={handleChange}
aria-label={t('skill.startTab.searchPlaceholder')} />
className="system-sm-regular min-w-0 flex-1 bg-transparent px-1 text-text-secondary placeholder:text-components-input-text-placeholder focus:outline-none"
placeholder={t('skill.startTab.searchPlaceholder')}
value={value}
onChange={e => onChange(e.target.value)}
/>
</div>
) )
} }