unnamed-ui
列表

Component Panel

Composed component panel with tabs and cascading selection

"use client";

import * as React from "react";
import { ComponentPanel } from "@/components/composed/component-panel/component-panel";

export function ComponentPanelDefault() {
  const categories = [
    {
      value: "all",
      label: "全部",
      options: [
        { value: "comp1", label: "组件1", tooltip: "这是组件1的说明" },
        { value: "comp2", label: "组件2", tooltip: "这是组件2的说明" },
        { value: "comp3", label: "组件3", tooltip: "这是组件3的说明" },
        { value: "comp4", label: "组件4", tooltip: "这是组件4的说明" },
        { value: "comp5", label: "组件5", tooltip: "这是组件5的说明" },
        { value: "comp6", label: "组件6", tooltip: "这是组件6的说明" },
        { value: "comp7", label: "组件7", tooltip: "这是组件7的说明" },
        { value: "comp8", label: "组件8", tooltip: "这是组件8的说明" },
      ],
    },
    {
      value: "mcp",
      label: "MCP",
      options: [
        { value: "mcp1", label: "MCP组件1", tooltip: "MCP组件1" },
        { value: "mcp2", label: "MCP组件2", tooltip: "MCP组件2" },
        { value: "mcp3", label: "MCP组件3", tooltip: "MCP组件3" },
        { value: "mcp4", label: "MCP组件4", tooltip: "MCP组件4" },
      ],
    },
    {
      value: "tool",
      label: "工具",
      options: [
        { value: "tool1", label: "工具1", tooltip: "工具1说明" },
        { value: "tool2", label: "工具2", tooltip: "工具2说明" },
        { value: "tool3", label: "工具3", tooltip: "工具3说明" },
        { value: "tool4", label: "工具4", tooltip: "工具4说明" },
        { value: "tool5", label: "工具5", tooltip: "工具5说明" },
      ],
    },
    {
      value: "workflow",
      label: "工作流",
      options: [
        { value: "wf1", label: "工作流1", tooltip: "工作流1" },
        { value: "wf2", label: "工作流2", tooltip: "工作流2" },
        { value: "wf3", label: "工作流3", tooltip: "工作流3" },
        { value: "wf4", label: "工作流4", tooltip: "工作流4" },
        { value: "wf5", label: "工作流5", tooltip: "工作流5" },
        { value: "wf6", label: "工作流6", tooltip: "工作流6" },
      ],
    },
  ];

  return (
    <div className="w-full h-full max-w-4xl">
      <ComponentPanel
        categories={categories}
        defaultValue={["comp1", "comp3", "mcp1", "tool1", "wf1"]}
        defaultActiveTab="all"
        onChange={(values) => {
          console.log("选中的值:", values);
        }}
      />
    </div>
  );
}

Component Panel 是一个级联选择面板组件,通过选项卡切换不同分类,在每个分类下选择选项。支持单选和多选模式,受控和非受控状态,适用于功能开关、工具选择、权限配置等场景。

概述

  • 级联选择:通过选项卡切换分类,在分类下选择具体选项
  • 灵活模式:支持单选和多选两种模式
  • 受控/非受控:同时支持受控和非受控状态管理
  • 标准数据格式:使用业界标准的 value/label 数据结构
  • 类型安全:完整的 TypeScript 类型定义
  • 响应式布局:自适应不同屏幕尺寸,移动端友好

快速开始

import { ComponentPanel } from "@/registry/wuhan/composed/component-panel";

export function Example() {
  const categories = [
    {
      value: "all",
      label: "全部",
      options: [
        { value: "comp1", label: "组件1" },
        { value: "comp2", label: "组件2" },
      ],
    },
    {
      value: "tools",
      label: "工具",
      options: [
        { value: "tool1", label: "工具1" },
        { value: "tool2", label: "工具2" },
      ],
    },
  ];

  return (
    <ComponentPanel
      categories={categories}
      defaultValue={["comp1", "tool1"]}
      onChange={(values) => console.log(values)}
    />
  );
}

特性

  • 标准化数据结构:使用 value/label 格式,与主流组件库保持一致
  • 多种选择模式:支持多选(默认)和单选模式
  • 双向状态控制:选项选择和选项卡切换均支持受控/非受控
  • 禁用状态:支持禁用整个分类或单个选项
  • 图标支持:选项可配置自定义图标
  • 工具提示:支持为选项添加 tooltip 说明
  • 响应式网格:选项列表自适应屏幕宽度

安装

pnpm dlx shadcn@latest add http://localhost:3000/r/wuhan/component-panel.json

代码演示

基本

基础多选模式,使用非受控状态。

"use client";

import * as React from "react";
import { ComponentPanel } from "@/components/composed/component-panel/component-panel";

export function ComponentPanelDefault() {
  const categories = [
    {
      value: "all",
      label: "全部",
      options: [
        { value: "comp1", label: "组件1", tooltip: "这是组件1的说明" },
        { value: "comp2", label: "组件2", tooltip: "这是组件2的说明" },
        { value: "comp3", label: "组件3", tooltip: "这是组件3的说明" },
        { value: "comp4", label: "组件4", tooltip: "这是组件4的说明" },
        { value: "comp5", label: "组件5", tooltip: "这是组件5的说明" },
        { value: "comp6", label: "组件6", tooltip: "这是组件6的说明" },
        { value: "comp7", label: "组件7", tooltip: "这是组件7的说明" },
        { value: "comp8", label: "组件8", tooltip: "这是组件8的说明" },
      ],
    },
    {
      value: "mcp",
      label: "MCP",
      options: [
        { value: "mcp1", label: "MCP组件1", tooltip: "MCP组件1" },
        { value: "mcp2", label: "MCP组件2", tooltip: "MCP组件2" },
        { value: "mcp3", label: "MCP组件3", tooltip: "MCP组件3" },
        { value: "mcp4", label: "MCP组件4", tooltip: "MCP组件4" },
      ],
    },
    {
      value: "tool",
      label: "工具",
      options: [
        { value: "tool1", label: "工具1", tooltip: "工具1说明" },
        { value: "tool2", label: "工具2", tooltip: "工具2说明" },
        { value: "tool3", label: "工具3", tooltip: "工具3说明" },
        { value: "tool4", label: "工具4", tooltip: "工具4说明" },
        { value: "tool5", label: "工具5", tooltip: "工具5说明" },
      ],
    },
    {
      value: "workflow",
      label: "工作流",
      options: [
        { value: "wf1", label: "工作流1", tooltip: "工作流1" },
        { value: "wf2", label: "工作流2", tooltip: "工作流2" },
        { value: "wf3", label: "工作流3", tooltip: "工作流3" },
        { value: "wf4", label: "工作流4", tooltip: "工作流4" },
        { value: "wf5", label: "工作流5", tooltip: "工作流5" },
        { value: "wf6", label: "工作流6", tooltip: "工作流6" },
      ],
    },
  ];

  return (
    <div className="w-full h-full max-w-4xl">
      <ComponentPanel
        categories={categories}
        defaultValue={["comp1", "comp3", "mcp1", "tool1", "wf1"]}
        defaultActiveTab="all"
        onChange={(values) => {
          console.log("选中的值:", values);
        }}
      />
    </div>
  );
}

受控模式

完全受控模式,外部管理选中状态。

已选择 2 项: comp1, comp3
"use client";

import * as React from "react";
import { ComponentPanel } from "@/components/composed/component-panel/component-panel";
import { Button } from "@/components/ui/button";

export function ComponentPanelControlled() {
  const [selectedValues, setSelectedValues] = React.useState<string[]>([
    "comp1",
    "comp3",
  ]);

  const categories = [
    {
      value: "all",
      label: "全部",
      options: [
        { value: "comp1", label: "组件1" },
        { value: "comp2", label: "组件2" },
        { value: "comp3", label: "组件3" },
        { value: "comp4", label: "组件4" },
        { value: "comp5", label: "组件5" },
        { value: "comp6", label: "组件6" },
      ],
    },
    {
      value: "tools",
      label: "工具",
      options: [
        { value: "tool1", label: "工具1" },
        { value: "tool2", label: "工具2" },
        { value: "tool3", label: "工具3" },
      ],
    },
  ];

  return (
    <div className="space-y-4 w-full max-w-4xl">
      <div className="flex gap-2">
        <Button
          size="sm"
          variant="outline"
          onClick={() => setSelectedValues([])}
        >
          清空选择
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() =>
            setSelectedValues([
              "comp1",
              "comp2",
              "comp3",
              "comp4",
              "comp5",
              "comp6",
              "tool1",
              "tool2",
              "tool3",
            ])
          }
        >
          全选
        </Button>
        <Button
          size="sm"
          variant="outline"
          onClick={() => setSelectedValues(["comp1", "tool1"])}
        >
          重置
        </Button>
      </div>

      <div className="text-sm text-muted-foreground">
        已选择 {selectedValues.length} 项: {selectedValues.join(", ")}
      </div>

      <ComponentPanel
        categories={categories}
        value={selectedValues}
        onChange={setSelectedValues}
      />
    </div>
  );
}

单选模式

单选模式,每次只能选中一个选项。

当前选择: comp1
"use client";

import * as React from "react";
import { ComponentPanel } from "@/components/composed/component-panel/component-panel";

export function ComponentPanelSingleSelect() {
  const [selected, setSelected] = React.useState<string[]>(["comp1"]);

  const categories = [
    {
      value: "framework",
      label: "框架",
      options: [
        {
          value: "react",
          label: "React",
          tooltip: "用于构建用户界面的 JavaScript 库",
        },
        { value: "vue", label: "Vue", tooltip: "渐进式 JavaScript 框架" },
        { value: "angular", label: "Angular", tooltip: "企业级前端框架" },
        { value: "svelte", label: "Svelte", tooltip: "编译型前端框架" },
      ],
    },
    {
      value: "backend",
      label: "后端",
      options: [
        { value: "node", label: "Node.js", tooltip: "JavaScript 运行时" },
        { value: "python", label: "Python", tooltip: "通用编程语言" },
        { value: "go", label: "Go", tooltip: "高效的并发编程语言" },
        { value: "java", label: "Java", tooltip: "面向对象的编程语言" },
      ],
    },
    {
      value: "database",
      label: "数据库",
      options: [
        {
          value: "postgres",
          label: "PostgreSQL",
          tooltip: "强大的开源关系数据库",
        },
        { value: "mysql", label: "MySQL", tooltip: "流行的关系数据库" },
        { value: "mongodb", label: "MongoDB", tooltip: "文档型 NoSQL 数据库" },
        { value: "redis", label: "Redis", tooltip: "内存数据结构存储" },
      ],
    },
  ];

  return (
    <div className="space-y-4 w-full max-w-4xl">
      <div className="text-sm text-muted-foreground">
        当前选择: {selected[0] || "无"}
      </div>

      <ComponentPanel
        categories={categories}
        value={selected}
        onChange={setSelected}
        multiple={false}
        defaultActiveTab="framework"
      />
    </div>
  );
}

选项图标

为选项添加图标,增强视觉识别。

"use client";

import * as React from "react";
import { ComponentPanel } from "@/components/composed/component-panel/component-panel";
import {
  Package,
  Wrench,
  Workflow,
  Zap,
  Database,
  Code,
  Terminal,
  Box,
} from "lucide-react";

export function ComponentPanelWithIcons() {
  const categories = [
    {
      value: "components",
      label: "组件",
      options: [
        { value: "button", label: "Button", icon: <Box className="size-4" /> },
        {
          value: "input",
          label: "Input",
          icon: <Terminal className="size-4" />,
        },
        { value: "select", label: "Select", icon: <Code className="size-4" /> },
        {
          value: "table",
          label: "Table",
          icon: <Database className="size-4" />,
        },
      ],
    },
    {
      value: "tools",
      label: "工具",
      options: [
        {
          value: "builder",
          label: "Builder",
          icon: <Wrench className="size-4" />,
        },
        {
          value: "packager",
          label: "Packager",
          icon: <Package className="size-4" />,
        },
        {
          value: "optimizer",
          label: "Optimizer",
          icon: <Zap className="size-4" />,
        },
      ],
    },
    {
      value: "workflows",
      label: "工作流",
      options: [
        { value: "ci", label: "CI/CD", icon: <Workflow className="size-4" /> },
        { value: "deploy", label: "Deploy", icon: <Zap className="size-4" /> },
        { value: "test", label: "Test", icon: <Code className="size-4" /> },
      ],
    },
  ];

  return (
    <div className="w-full max-w-4xl">
      <ComponentPanel
        categories={categories}
        defaultValue={["button", "input", "builder"]}
      />
    </div>
  );
}

禁用

禁用分类或选项,用于权限控制。

"use client";

import * as React from "react";
import { ComponentPanel } from "@/components/composed/component-panel/component-panel";

export function ComponentPanelDisabled() {
  const categories = [
    {
      value: "available",
      label: "可用功能",
      options: [
        { value: "feature1", label: "功能 1" },
        { value: "feature2", label: "功能 2" },
        { value: "feature3", label: "功能 3" },
        {
          value: "feature4",
          label: "功能 4",
          disabled: true,
          tooltip: "此功能暂不可用",
        },
      ],
    },
    {
      value: "premium",
      label: "高级功能",
      disabled: true,
      options: [
        { value: "premium1", label: "高级功能 1" },
        { value: "premium2", label: "高级功能 2" },
        { value: "premium3", label: "高级功能 3" },
      ],
    },
    {
      value: "experimental",
      label: "实验性功能",
      options: [
        { value: "exp1", label: "实验功能 1" },
        {
          value: "exp2",
          label: "实验功能 2",
          disabled: true,
          tooltip: "开发中",
        },
        { value: "exp3", label: "实验功能 3" },
      ],
    },
  ];

  return (
    <div className="w-full max-w-4xl">
      <ComponentPanel
        categories={categories}
        defaultValue={["feature1", "feature2"]}
      />
    </div>
  );
}

选项卡受控

受控选项卡切换,外部控制当前分类。

当前选项卡: dev
"use client";

import * as React from "react";
import { ComponentPanel } from "@/components/composed/component-panel/component-panel";

export function ComponentPanelTabControlled() {
  const [activeTab, setActiveTab] = React.useState("dev");
  const [selected, setSelected] = React.useState<string[]>(["vscode"]);

  const categories = [
    {
      value: "dev",
      label: "开发工具",
      options: [
        { value: "vscode", label: "VS Code" },
        { value: "webstorm", label: "WebStorm" },
        { value: "sublime", label: "Sublime Text" },
      ],
    },
    {
      value: "design",
      label: "设计工具",
      options: [
        { value: "figma", label: "Figma" },
        { value: "sketch", label: "Sketch" },
        { value: "adobe", label: "Adobe XD" },
      ],
    },
    {
      value: "productivity",
      label: "生产力",
      options: [
        { value: "notion", label: "Notion" },
        { value: "slack", label: "Slack" },
        { value: "trello", label: "Trello" },
      ],
    },
  ];

  return (
    <div className="space-y-4 w-full max-w-4xl">
      <div className="flex gap-2">
        <button
          className="px-3 py-1.5 text-sm rounded border"
          onClick={() => setActiveTab("dev")}
          disabled={activeTab === "dev"}
        >
          切换到开发工具
        </button>
        <button
          className="px-3 py-1.5 text-sm rounded border"
          onClick={() => setActiveTab("design")}
          disabled={activeTab === "design"}
        >
          切换到设计工具
        </button>
        <button
          className="px-3 py-1.5 text-sm rounded border"
          onClick={() => setActiveTab("productivity")}
          disabled={activeTab === "productivity"}
        >
          切换到生产力
        </button>
      </div>

      <div className="text-sm text-muted-foreground">
        当前选项卡: {activeTab}
      </div>

      <ComponentPanel
        categories={categories}
        value={selected}
        onChange={setSelected}
        activeTab={activeTab}
        onTabChange={setActiveTab}
      />
    </div>
  );
}

API

ComponentPanel

主组件,级联选择面板。

Props

PropTypeDefaultDescription
categoriesComponentPanelCategory[]-分类列表(必填)
valuestring[]-选中的选项值数组(受控)
defaultValuestring[][]默认选中的选项值数组(非受控)
onChange(value: string[]) => void-选中值变化回调
activeTabstring-当前激活的选项卡(受控)
defaultActiveTabstring-默认激活的选项卡(非受控)
onTabChange(tab: string) => void-选项卡切换回调
multiplebooleantrue是否支持多选
classNamestring-自定义样式类名

Example

import { ComponentPanel } from "@/registry/wuhan/composed/component-panel";
import { useState } from "react";

function App() {
  const [selected, setSelected] = useState(["comp1"]);
  const [activeTab, setActiveTab] = useState("all");

  return (
    <ComponentPanel
      categories={categories}
      // 受控选择
      value={selected}
      onChange={setSelected}
      // 受控选项卡
      activeTab={activeTab}
      onTabChange={setActiveTab}
      // 单选模式
      multiple={false}
    />
  );
}

ComponentPanelCategory

分类配置接口。

interface ComponentPanelCategory {
  /** 分类的唯一值 */
  value: string;
  /** 分类的显示文本 */
  label: React.ReactNode;
  /** 该分类下的选项列表 */
  options: ComponentPanelOption[];
  /** 是否禁用该分类 */
  disabled?: boolean;
}

示例

const category: ComponentPanelCategory = {
  value: "tools",
  label: "工具集",
  disabled: false,
  options: [
    { value: "tool1", label: "工具1" },
    { value: "tool2", label: "工具2" },
  ],
};

ComponentPanelOption

选项配置接口。

interface ComponentPanelOption {
  /** 选项的唯一值 */
  value: string;
  /** 选项的显示文本 */
  label: React.ReactNode;
  /** 选项图标 */
  icon?: React.ReactNode;
  /** 选项提示信息 */
  tooltip?: React.ReactNode;
  /** 是否禁用该选项 */
  disabled?: boolean;
}

示例

import { Wrench } from "lucide-react";

const option: ComponentPanelOption = {
  value: "builder",
  label: "构建工具",
  icon: <Wrench className="size-4" />,
  tooltip: "用于构建和打包项目",
  disabled: false,
};

使用场景

  • 功能开关面板:管理应用中各模块功能的启用/禁用状态
  • 工具选择器:在开发工具、设计工具等分类中选择常用工具
  • 权限配置:按模块配置用户或角色的权限
  • 插件管理:管理各类插件的启用状态
  • 组件库选择:在组件库文档中选择要查看的组件
  • 筛选面板:多维度筛选数据,如电商网站的商品筛选

最佳实践

  1. 合理分类:将选项按逻辑归类到不同 tab,每个 tab 的选项数量适中(建议 3-12 个)
  2. 使用 tooltip:为复杂或缩写的选项添加 tooltip 说明,提升用户体验
  3. 添加图标:为选项添加图标可以增强视觉识别,特别适用于工具类选择
  4. 状态持久化:考虑将选中状态保存到 localStorage 或后端,避免刷新丢失
  5. 受控使用:涉及复杂业务逻辑时建议使用受控模式,便于状态管理和同步
  6. 禁用反馈:禁用的选项应该提供 tooltip 说明原因,如"需要高级会员"

注意事项

  • 组件基于 Radix UI Tabs 构建,需要安装 @radix-ui/react-tabs 依赖
  • categories 数组不应为空,至少包含一个分类
  • 选项的 value 在所有分类中应该是唯一的
  • 单选模式下 value 数组只会包含一个元素
  • 禁用的分类无法切换,禁用的选项无法选中
  • 受控模式下,valueonChange 必须配合使用

原语组件

Component Panel 基于以下原语组件构建:

主要原语

  • ComponentPanelContainerPrimitive - 主容器(基于 Radix Tabs.Root)
  • ComponentPanelTabsListPrimitive - 选项卡列表
  • ComponentPanelTabsTriggerPrimitive - 选项卡触发器
  • ComponentPanelTabsContentPrimitive - 选项卡内容

列表原语

  • ComponentPanelListPrimitive - 选项列表容器
  • ComponentPanelListItemPrimitive - 选项列表项
  • ComponentPanelListItemIconPrimitive - 选项默认图标

原语组件提供了更底层的控制,可以在 component-panel-01.tsx 中找到。

样式定制

组件使用 Tailwind CSS 构建,可以通过 className 自定义样式:

<ComponentPanel
  className="custom-panel"
  categories={categories}
/>

响应式布局

选项列表默认使用响应式网格布局:

  • 移动端:每行 2 个选项
  • 小屏幕:每行 3 个选项
  • 中等及以上屏幕:每行 4 个选项

可以通过 CSS 覆盖原语组件的网格样式自定义布局。

扩展示例

动态分类

根据权限动态显示分类。

import { ComponentPanel } from "@/registry/wuhan/composed/component-panel";

function DynamicCategories() {
  const userRole = "admin"; // 从上下文获取

  const allCategories = [
    {
      value: "basic",
      label: "基础功能",
      options: [
        { value: "view", label: "查看" },
        { value: "edit", label: "编辑" },
      ],
    },
    {
      value: "advanced",
      label: "高级功能",
      options: [
        { value: "delete", label: "删除" },
        { value: "export", label: "导出" },
      ],
    },
  ];

  // 根据角色过滤分类
  const categories =
    userRole === "admin"
      ? allCategories
      : allCategories.filter((c) => c.value === "basic");

  return <ComponentPanel categories={categories} />;
}

全选/反选

提供全选和反选功能。

import { ComponentPanel } from "@/registry/wuhan/composed/component-panel/component-panel";
import { getAllOptions } from "@/registry/wuhan/composed/component-panel/component-panel-utils";
import { Button } from "@/components/ui/button";
import { useState } from "react";

function SelectAll() {
  const [selected, setSelected] = useState<string[]>([]);

  const categories = [
    /* ... */
  ];

  const allOptions = getAllOptions(categories);
  const allValues = allOptions.map((opt) => opt.value);

  return (
    <div className="space-y-4">
      <div className="flex gap-2">
        <Button onClick={() => setSelected(allValues)}>全选</Button>
        <Button onClick={() => setSelected([])}>清空</Button>
        <Button
          onClick={() =>
            setSelected(
              allValues.filter((v) => !selected.includes(v))
            )
          }
        >
          反选
        </Button>
      </div>

      <ComponentPanel
        categories={categories}
        value={selected}
        onChange={setSelected}
      />
    </div>
  );
}

搜索过滤

为选项添加搜索功能。

import { ComponentPanel } from "@/registry/wuhan/composed/component-panel";
import { Input } from "@/components/ui/input";
import { useState, useMemo } from "react";

function SearchablePanel() {
  const [search, setSearch] = useState("");
  const [selected, setSelected] = useState<string[]>([]);

  const allCategories = [
    /* ... */
  ];

  // 根据搜索词过滤选项
  const filteredCategories = useMemo(() => {
    if (!search) return allCategories;

    return allCategories.map((category) => ({
      ...category,
      options: category.options.filter((opt) =>
        String(opt.label).toLowerCase().includes(search.toLowerCase())
      ),
    })).filter((category) => category.options.length > 0);
  }, [search, allCategories]);

  return (
    <div className="space-y-4">
      <Input
        placeholder="搜索选项..."
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />
      <ComponentPanel
        categories={filteredCategories}
        value={selected}
        onChange={setSelected}
      />
    </div>
  );
}

分组统计

显示每个分类的选中统计。

import { ComponentPanel } from "@/registry/wuhan/composed/component-panel";
import { useState } from "react";

function GroupStats() {
  const [selected, setSelected] = useState<string[]>([]);

  const categories = [
    {
      value: "cat1",
      label: "分类1",
      options: [
        { value: "c1-1", label: "选项1" },
        { value: "c1-2", label: "选项2" },
        { value: "c1-3", label: "选项3" },
      ],
    },
    {
      value: "cat2",
      label: "分类2",
      options: [
        { value: "c2-1", label: "选项1" },
        { value: "c2-2", label: "选项2" },
      ],
    },
  ];

  // 统计每个分类的选中数
  const stats = categories.map((cat) => ({
    ...cat,
    selectedCount: cat.options.filter((opt) =>
      selected.includes(opt.value)
    ).length,
    totalCount: cat.options.length,
  }));

  return (
    <div className="space-y-4">
      <div className="grid grid-cols-2 gap-4">
        {stats.map((stat) => (
          <div key={stat.value} className="p-3 border rounded">
            <div className="font-medium">{stat.label}</div>
            <div className="text-sm text-muted-foreground">
              已选 {stat.selectedCount} / {stat.totalCount}
            </div>
          </div>
        ))}
      </div>

      <ComponentPanel
        categories={categories}
        value={selected}
        onChange={setSelected}
      />
    </div>
  );
}

自定义渲染

完全自定义选项的渲染方式。

import { ComponentPanelContainerPrimitive, /* ... */ } from "@/registry/wuhan/blocks/component-panel/component-panel-01";
import { Badge } from "@/components/ui/badge";
import { useState } from "react";

function CustomRender() {
  const [selected, setSelected] = useState<string[]>([]);

  const categories = [
    {
      value: "plugins",
      label: "插件",
      options: [
        { value: "p1", label: "插件1", meta: { downloads: "1.2k", rating: 4.5 } },
        { value: "p2", label: "插件2", meta: { downloads: "890", rating: 4.2 } },
      ],
    },
  ];

  return (
    <ComponentPanelContainerPrimitive defaultValue="plugins">
      <ComponentPanelTabsListPrimitive>
        {categories.map((cat) => (
          <ComponentPanelTabsTriggerPrimitive key={cat.value} value={cat.value}>
            {cat.label}
          </ComponentPanelTabsTriggerPrimitive>
        ))}
      </ComponentPanelTabsListPrimitive>

      {categories.map((cat) => (
        <ComponentPanelTabsContentPrimitive key={cat.value} value={cat.value}>
          <div className="grid grid-cols-1 gap-2">
            {cat.options.map((opt) => (
              <div
                key={opt.value}
                className="p-3 border rounded cursor-pointer hover:bg-muted"
                onClick={() =>
                  setSelected((prev) =>
                    prev.includes(opt.value)
                      ? prev.filter((v) => v !== opt.value)
                      : [...prev, opt.value]
                  )
                }
              >
                <div className="flex items-center justify-between">
                  <span className="font-medium">{opt.label}</span>
                  {selected.includes(opt.value) && (
                    <Badge variant="secondary">已选</Badge>
                  )}
                </div>
                <div className="flex gap-4 text-sm text-muted-foreground mt-1">
                  <span>下载: {opt.meta.downloads}</span>
                  <span>评分: {opt.meta.rating}</span>
                </div>
              </div>
            ))}
          </div>
        </ComponentPanelTabsContentPrimitive>
      ))}
    </ComponentPanelContainerPrimitive>
  );
}

状态持久化

将选择状态保存到 localStorage。

import { ComponentPanel } from "@/registry/wuhan/composed/component-panel";
import { useState, useEffect } from "react";

function PersistentPanel() {
  const [selected, setSelected] = useState<string[]>(() => {
    const saved = localStorage.getItem("panel-selection");
    return saved ? JSON.parse(saved) : [];
  });

  useEffect(() => {
    localStorage.setItem("panel-selection", JSON.stringify(selected));
  }, [selected]);

  const categories = [
    /* ... */
  ];

  return (
    <ComponentPanel
      categories={categories}
      value={selected}
      onChange={setSelected}
    />
  );
}

API

ComponentPanelContainerPrimitive

组件面板容器,使用 Tabs 组件实现标签页功能。

Props:

  • defaultValue?: string - 默认激活的 tab
  • value?: string - 当前激活的 tab(受控模式)
  • onValueChange?: (value: string) => void - tab 变化回调
  • children?: React.ReactNode - 子元素

ComponentPanelTabsListPrimitive

标签页列表容器。

Props:

  • children?: React.ReactNode - 子元素(通常是 ComponentPanelTabsTriggerPrimitive)

ComponentPanelTabsTriggerPrimitive

标签页触发器。

Props:

  • value: string - 标签值
  • children?: React.ReactNode - 标签内容

ComponentPanelTabsContentPrimitive

标签页内容容器。

Props:

  • value: string - 标签值
  • children?: React.ReactNode - 内容

ComponentPanelListPrimitive

列表容器,使用 flex-wrap 实现自适应布局。

Props:

  • children?: React.ReactNode - 子元素(通常是 ComponentPanelListItemPrimitive)

ComponentPanelListItemPrimitive

列表项样式原语,基于 ToggleButtonPrimitive 实现,提供自适应布局样式。 只提供样式,不包含任何业务逻辑和状态管理,用户需要自己处理点击事件和状态。

Props:

继承自 ToggleButtonPrimitive 的所有属性,包括:

  • selected?: boolean - 是否选中(由用户控制)
  • onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void - 点击事件处理(由用户实现)
  • disabled?: boolean - 是否禁用
  • variant?: "default" | "compact" - 按钮变体样式
  • multiple?: boolean - 是否多选模式
  • children?: React.ReactNode - 按钮内容(通常是标签文本)

注意: 组件只提供样式和布局,状态管理和事件处理完全由用户控制。