mirror of
https://github.com/langgenius/dify.git
synced 2026-03-17 12:57:51 +08:00
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:
@ -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)
|
||||
@ -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)
|
||||
@ -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>
|
||||
|
||||
@ -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')}
|
||||
|
||||
@ -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)
|
||||
@ -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",
|
||||
|
||||
@ -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 模板",
|
||||
|
||||
Reference in New Issue
Block a user