feat(skill-editor): add CategoryTabs and TemplateSearch to skill templates section

Add filter controls for skill templates:
- CategoryTabs: tab navigation with mock categories (All, Productivity, etc.)
- TemplateSearch: search input with accessibility attributes
- Grid layout fix to prevent tab width changes on font-weight switch

Update SectionHeader to accept className prop for flexible styling.
Add search placeholder i18n translations.
This commit is contained in:
yyh
2026-01-23 14:39:36 +08:00
parent 4d465d6cf9
commit a91d709aa5
7 changed files with 150 additions and 4 deletions

View File

@ -0,0 +1,46 @@
'use client'
import type { FC } from 'react'
import { memo } from 'react'
import TabItem from './tab-item'
export type TemplateCategory = {
id: string
label: string
}
// TODO: use real categories from backend
const MOCK_CATEGORIES: TemplateCategory[] = [
{ id: 'all', label: 'All' },
{ id: 'productivity', label: 'Productivity' },
{ id: 'analysis', label: 'Analysis' },
{ id: 'search', label: 'Search' },
{ id: 'development', label: 'Development' },
{ id: 'security', label: 'Security' },
]
type CategoryTabsProps = {
categories?: TemplateCategory[]
activeCategory: string
onCategoryChange: (categoryId: string) => void
}
const CategoryTabs: FC<CategoryTabsProps> = ({
categories = MOCK_CATEGORIES,
activeCategory,
onCategoryChange,
}) => {
return (
<div className="flex flex-1 items-center gap-1">
{categories.map(category => (
<TabItem
key={category.id}
label={category.label}
isActive={activeCategory === category.id}
onClick={() => onCategoryChange(category.id)}
/>
))}
</div>
)
}
export default memo(CategoryTabs)

View File

@ -0,0 +1,47 @@
'use client'
import type { FC } from 'react'
import { memo } from 'react'
import { cn } from '@/utils/classnames'
type TabItemProps = {
label: string
isActive: boolean
onClick: () => void
}
const TabItem: FC<TabItemProps> = ({
label,
isActive,
onClick,
}) => {
return (
<button
type="button"
className={cn(
'grid shrink-0 rounded-lg px-3 py-2 transition-colors',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-components-input-border-active',
isActive
? 'bg-state-base-active'
: 'hover:bg-state-base-hover',
)}
onClick={onClick}
>
<span className="system-sm-semibold col-start-1 row-start-1 opacity-0" aria-hidden="true">
{label}
</span>
<span
className={cn(
'col-start-1 row-start-1',
isActive
? 'system-sm-semibold text-text-primary'
: 'system-sm-medium text-text-tertiary',
)}
>
{label}
</span>
</button>
)
}
export default memo(TabItem)

View File

@ -6,18 +6,20 @@ import { memo } from 'react'
type SectionHeaderProps = {
title: string
description: string
className?: string
}
const SectionHeader: FC<SectionHeaderProps> = ({
title,
description,
className,
}) => {
return (
<header className="mb-3 flex flex-col gap-0.5">
<header className={className}>
<h2 className="title-xl-semi-bold text-text-primary">
{title}
</h2>
<p className="system-xs-regular text-text-tertiary">
<p className="system-xs-regular mt-0.5 text-text-tertiary">
{description}
</p>
</header>

View File

@ -1,19 +1,33 @@
'use client'
import type { FC } from 'react'
import { memo } from 'react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import CategoryTabs from './category-tabs'
import SectionHeader from './section-header'
import TemplateSearch from './template-search'
const SkillTemplatesSection: FC = () => {
const { t } = useTranslation('workflow')
const [activeCategory, setActiveCategory] = useState('all')
const [searchValue, setSearchValue] = useState('')
return (
<section className="px-6">
<section className="flex flex-col gap-3 px-6 py-2">
<SectionHeader
title={t('skill.startTab.templatesTitle')}
description={t('skill.startTab.templatesDesc')}
/>
<div className="flex w-full items-start gap-1">
<CategoryTabs
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
/>
<TemplateSearch
value={searchValue}
onChange={setSearchValue}
/>
</div>
<div className="flex min-h-[200px] items-center justify-center rounded-xl border border-dashed border-divider-regular bg-background-section-burn">
<span className="system-sm-regular text-text-quaternary">
{t('skill.startTab.templatesComingSoon')}

View File

@ -0,0 +1,35 @@
'use client'
import type { FC } from 'react'
import { RiSearchLine } from '@remixicon/react'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
type TemplateSearchProps = {
value: string
onChange: (value: string) => void
}
const TemplateSearch: FC<TemplateSearchProps> = ({
value,
onChange,
}) => {
const { t } = useTranslation('workflow')
return (
<div className="flex shrink-0 items-center gap-0.5 rounded-md bg-components-input-bg-normal p-2">
<RiSearchLine className="size-4 shrink-0 text-text-placeholder" aria-hidden="true" />
<input
type="text"
name="template-search"
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>
)
}
export default memo(TemplateSearch)

View File

@ -1035,6 +1035,7 @@
"skill.startTab.createBlankSkillDesc": "Start with an empty folder structure",
"skill.startTab.importSkill": "Import Skill",
"skill.startTab.importSkillDesc": "Import skill from skill.zip file",
"skill.startTab.searchPlaceholder": "Search…",
"skill.startTab.templatesComingSoon": "Templates coming soon…",
"skill.startTab.templatesDesc": "Choose a template to bootstrap your agent's capabilities",
"skill.startTab.templatesTitle": "Skill Templates",

View File

@ -1027,6 +1027,7 @@
"skill.startTab.createBlankSkillDesc": "从空文件夹结构开始",
"skill.startTab.importSkill": "导入 Skill",
"skill.startTab.importSkillDesc": "从 skill.zip 文件导入",
"skill.startTab.searchPlaceholder": "搜索…",
"skill.startTab.templatesComingSoon": "模板即将推出…",
"skill.startTab.templatesDesc": "选择模板来快速构建你的 Agent 能力",
"skill.startTab.templatesTitle": "Skill 模板",