chore(marketplace): add publish-plugin, rewrite init.sh, cleanup obsolete plugins
- Add publish-plugin: marketplace publish + Feishu bot notification - Rewrite init.sh: SSE-first, fixed prod API, remove env selection - Update CLAUDE.md, README.md, claude-config.yaml - Update skills: req-plugin, req-prd-plugin, pull-request-plugin - Delete sync-skills.sh (obsolete) - Delete deprecated plugins: skills-ops/*, skills-projects/*, old skills-dev/req duplicates - Regenerate marketplace.json (27 plugins) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "frontend-design-plugin",
|
||||
"description": "Plugin for frontend-design",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "qiudl"
|
||||
}
|
||||
}
|
||||
@@ -1,695 +0,0 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||
arguments: [component|page|storybook] <description>
|
||||
---
|
||||
|
||||
# Frontend Design 前端设计技能
|
||||
|
||||
创建高质量、有设计感的前端界面和组件,支持 Storybook 组件开发。
|
||||
|
||||
---
|
||||
|
||||
## 命令格式
|
||||
|
||||
| 命令 | 功能 | 示例 |
|
||||
|------|------|------|
|
||||
| `/frontend-design component <描述>` | 创建 React/Vue 组件 | `/frontend-design component 产品卡片` |
|
||||
| `/frontend-design page <描述>` | 创建完整页面 | `/frontend-design page 登录页` |
|
||||
| `/frontend-design storybook <描述>` | 创建带 Storybook 的组件 | `/frontend-design storybook 按钮组件` |
|
||||
|
||||
---
|
||||
|
||||
## 设计原则
|
||||
|
||||
### 1. 设计思维先行
|
||||
|
||||
在编码前,明确以下问题:
|
||||
|
||||
- **目的**:这个界面解决什么问题?谁在使用?
|
||||
- **调性**:选择一个明确的美学方向
|
||||
- **差异化**:什么让这个设计令人难忘?
|
||||
|
||||
### 2. 美学方向选择
|
||||
|
||||
| 风格 | 特点 | 适用场景 |
|
||||
|------|------|----------|
|
||||
| 极简主义 | 大量留白、精炼元素 | 工具类、专业平台 |
|
||||
| 现代商务 | 清晰层次、专业配色 | 企业官网、B2B |
|
||||
| 活力年轻 | 鲜艳色彩、动感动画 | 消费品、社交 |
|
||||
| 奢华精致 | 深色调、金属质感 | 高端品牌、金融 |
|
||||
| 自然有机 | 柔和曲线、自然色系 | 健康、环保 |
|
||||
| 复古怀旧 | 经典字体、做旧质感 | 文化、艺术 |
|
||||
| 未来科技 | 渐变、玻璃拟态 | 科技、创新 |
|
||||
|
||||
### 3. 避免的设计陷阱
|
||||
|
||||
**禁止使用**:
|
||||
- 过度使用的字体:Inter、Roboto、Arial
|
||||
- 陈词滥调的配色:紫色渐变白底
|
||||
- 千篇一律的布局
|
||||
- 缺乏个性的通用组件
|
||||
|
||||
**应该追求**:
|
||||
- 独特的字体组合
|
||||
- 有意图的配色方案
|
||||
- 打破常规的布局
|
||||
- 有记忆点的细节
|
||||
|
||||
---
|
||||
|
||||
## Storybook 组件开发
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── Button/
|
||||
│ │ ├── Button.tsx
|
||||
│ │ ├── Button.stories.tsx
|
||||
│ │ ├── Button.module.css
|
||||
│ │ └── index.ts
|
||||
│ ├── Card/
|
||||
│ │ ├── Card.tsx
|
||||
│ │ ├── Card.stories.tsx
|
||||
│ │ ├── Card.module.css
|
||||
│ │ └── index.ts
|
||||
│ └── index.ts
|
||||
├── styles/
|
||||
│ ├── variables.css
|
||||
│ ├── typography.css
|
||||
│ └── animations.css
|
||||
└── .storybook/
|
||||
├── main.ts
|
||||
└── preview.ts
|
||||
```
|
||||
|
||||
### 组件模板
|
||||
|
||||
#### 1. 组件文件 (Component.tsx)
|
||||
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import styles from './Component.module.css';
|
||||
|
||||
export interface ComponentProps {
|
||||
/** 组件变体 */
|
||||
variant?: 'primary' | 'secondary' | 'outline';
|
||||
/** 尺寸 */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 子元素 */
|
||||
children: React.ReactNode;
|
||||
/** 点击事件 */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Component: React.FC<ComponentProps> = ({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
children,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`${styles.component} ${styles[variant]} ${styles[size]}`}
|
||||
data-disabled={disabled}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
#### 2. Storybook Stories (Component.stories.tsx)
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Component } from './Component';
|
||||
|
||||
const meta: Meta<typeof Component> = {
|
||||
title: 'Components/Component',
|
||||
component: Component,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component: '组件描述文档',
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'outline'],
|
||||
description: '组件变体样式',
|
||||
},
|
||||
size: {
|
||||
control: 'radio',
|
||||
options: ['sm', 'md', 'lg'],
|
||||
description: '组件尺寸',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: '是否禁用',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Component>;
|
||||
|
||||
/** 默认状态 */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: '默认组件',
|
||||
},
|
||||
};
|
||||
|
||||
/** 主要变体 */
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
children: '主要按钮',
|
||||
},
|
||||
};
|
||||
|
||||
/** 次要变体 */
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
variant: 'secondary',
|
||||
children: '次要按钮',
|
||||
},
|
||||
};
|
||||
|
||||
/** 不同尺寸 */
|
||||
export const Sizes: Story = {
|
||||
render: () => (
|
||||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Component size="sm">小号</Component>
|
||||
<Component size="md">中号</Component>
|
||||
<Component size="lg">大号</Component>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
/** 禁用状态 */
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
children: '禁用状态',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. 样式文件 (Component.module.css)
|
||||
|
||||
```css
|
||||
.component {
|
||||
/* 基础样式 */
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius-md);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* 变体 */
|
||||
.primary {
|
||||
background: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.primary:hover {
|
||||
background: var(--color-primary-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px var(--color-primary-shadow);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
background: var(--color-secondary);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.outline {
|
||||
background: transparent;
|
||||
border: 2px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
/* 尺寸 */
|
||||
.sm {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.md {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.lg {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* 状态 */
|
||||
[data-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计系统变量
|
||||
|
||||
### CSS 变量模板
|
||||
|
||||
```css
|
||||
:root {
|
||||
/* 颜色 */
|
||||
--color-primary: #0066ff;
|
||||
--color-primary-dark: #0052cc;
|
||||
--color-primary-light: #4d94ff;
|
||||
--color-primary-shadow: rgba(0, 102, 255, 0.25);
|
||||
|
||||
--color-secondary: #f0f4f8;
|
||||
--color-accent: #ff6b35;
|
||||
|
||||
--color-text: #1a1a2e;
|
||||
--color-text-muted: #64748b;
|
||||
--color-text-inverse: #ffffff;
|
||||
|
||||
--color-background: #ffffff;
|
||||
--color-surface: #f8fafc;
|
||||
--color-border: #e2e8f0;
|
||||
|
||||
--color-success: #10b981;
|
||||
--color-warning: #f59e0b;
|
||||
--color-error: #ef4444;
|
||||
|
||||
/* 字体 */
|
||||
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
--font-display: 'Clash Display', var(--font-sans);
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
|
||||
/* 字号 */
|
||||
--text-xs: 0.75rem;
|
||||
--text-sm: 0.875rem;
|
||||
--text-base: 1rem;
|
||||
--text-lg: 1.125rem;
|
||||
--text-xl: 1.25rem;
|
||||
--text-2xl: 1.5rem;
|
||||
--text-3xl: 2rem;
|
||||
--text-4xl: 2.5rem;
|
||||
|
||||
/* 间距 */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
--space-12: 3rem;
|
||||
--space-16: 4rem;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
/* 阴影 */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* 动画 */
|
||||
--duration-fast: 150ms;
|
||||
--duration-normal: 300ms;
|
||||
--duration-slow: 500ms;
|
||||
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
/* 暗色主题 */
|
||||
[data-theme="dark"] {
|
||||
--color-text: #f1f5f9;
|
||||
--color-text-muted: #94a3b8;
|
||||
--color-background: #0f172a;
|
||||
--color-surface: #1e293b;
|
||||
--color-border: #334155;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常用组件示例
|
||||
|
||||
### 1. 产品卡片 (ProductCard)
|
||||
|
||||
```tsx
|
||||
// ProductCard.tsx
|
||||
import React from 'react';
|
||||
import styles from './ProductCard.module.css';
|
||||
|
||||
export interface ProductCardProps {
|
||||
image: string;
|
||||
title: string;
|
||||
location: string;
|
||||
rating: number;
|
||||
reviewCount: number;
|
||||
price: number;
|
||||
originalPrice?: number;
|
||||
tags?: string[];
|
||||
onAddToCart?: () => void;
|
||||
}
|
||||
|
||||
export const ProductCard: React.FC<ProductCardProps> = ({
|
||||
image,
|
||||
title,
|
||||
location,
|
||||
rating,
|
||||
reviewCount,
|
||||
price,
|
||||
originalPrice,
|
||||
tags = [],
|
||||
onAddToCart,
|
||||
}) => {
|
||||
return (
|
||||
<article className={styles.card}>
|
||||
<div className={styles.imageWrapper}>
|
||||
<img src={image} alt={title} className={styles.image} />
|
||||
{tags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{tags.map((tag) => (
|
||||
<span key={tag} className={styles.tag} data-tag={tag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<h3 className={styles.title}>{title}</h3>
|
||||
<p className={styles.location}>📍 {location}</p>
|
||||
|
||||
<div className={styles.rating}>
|
||||
<span className={styles.stars}>⭐ {rating.toFixed(1)}</span>
|
||||
<span className={styles.reviewCount}>({reviewCount}条评价)</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.priceRow}>
|
||||
<div className={styles.price}>
|
||||
<span className={styles.currency}>¥</span>
|
||||
<span className={styles.amount}>{price}</span>
|
||||
<span className={styles.suffix}>起</span>
|
||||
</div>
|
||||
{originalPrice && (
|
||||
<span className={styles.originalPrice}>¥{originalPrice}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button className={styles.addButton} onClick={onAddToCart}>
|
||||
加入购物车
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ProductCard.stories.tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ProductCard } from './ProductCard';
|
||||
|
||||
const meta: Meta<typeof ProductCard> = {
|
||||
title: 'Components/ProductCard',
|
||||
component: ProductCard,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
backgrounds: {
|
||||
default: 'light',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ProductCard>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
image: 'https://images.unsplash.com/photo-1494947665470-20322015e3a8',
|
||||
title: '袋鼠岛一日游',
|
||||
location: '阿德莱德出发',
|
||||
rating: 4.8,
|
||||
reviewCount: 126,
|
||||
price: 389,
|
||||
tags: ['热卖', '含午餐'],
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDiscount: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
originalPrice: 499,
|
||||
tags: ['特惠', '限时'],
|
||||
},
|
||||
};
|
||||
|
||||
export const Grid: Story = {
|
||||
render: () => (
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, 300px)',
|
||||
gap: '1.5rem'
|
||||
}}>
|
||||
<ProductCard
|
||||
image="https://images.unsplash.com/photo-1494947665470-20322015e3a8"
|
||||
title="袋鼠岛一日游"
|
||||
location="阿德莱德出发"
|
||||
rating={4.8}
|
||||
reviewCount={126}
|
||||
price={389}
|
||||
tags={['热卖']}
|
||||
/>
|
||||
<ProductCard
|
||||
image="https://images.unsplash.com/photo-1506905925346-21bda4d32df4"
|
||||
title="巴罗莎谷酒庄之旅"
|
||||
location="阿德莱德出发"
|
||||
rating={4.9}
|
||||
reviewCount={89}
|
||||
price={299}
|
||||
originalPrice={399}
|
||||
tags={['特惠', '含品酒']}
|
||||
/>
|
||||
<ProductCard
|
||||
image="https://images.unsplash.com/photo-1540202403-b7abd6747a18"
|
||||
title="海豚巡航体验"
|
||||
location="格雷尔海滩"
|
||||
rating={4.7}
|
||||
reviewCount={234}
|
||||
price={159}
|
||||
tags={['亲子']}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 按钮组件 (Button)
|
||||
|
||||
```tsx
|
||||
// Button.tsx
|
||||
import React from 'react';
|
||||
import styles from './Button.module.css';
|
||||
|
||||
export interface ButtonProps {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
fullWidth?: boolean;
|
||||
loading?: boolean;
|
||||
disabled?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const Button: React.FC<ButtonProps> = ({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
fullWidth = false,
|
||||
loading = false,
|
||||
disabled = false,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
children,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
${styles.button}
|
||||
${styles[variant]}
|
||||
${styles[size]}
|
||||
${fullWidth ? styles.fullWidth : ''}
|
||||
`}
|
||||
disabled={disabled || loading}
|
||||
onClick={onClick}
|
||||
>
|
||||
{loading ? (
|
||||
<span className={styles.spinner} />
|
||||
) : (
|
||||
<>
|
||||
{leftIcon && <span className={styles.icon}>{leftIcon}</span>}
|
||||
<span>{children}</span>
|
||||
{rightIcon && <span className={styles.icon}>{rightIcon}</span>}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Storybook 配置
|
||||
|
||||
### .storybook/main.ts
|
||||
|
||||
```ts
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@storybook/addon-a11y',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### .storybook/preview.ts
|
||||
|
||||
```ts
|
||||
import type { Preview } from '@storybook/react';
|
||||
import '../src/styles/variables.css';
|
||||
import '../src/styles/typography.css';
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
backgrounds: {
|
||||
default: 'light',
|
||||
values: [
|
||||
{ name: 'light', value: '#ffffff' },
|
||||
{ name: 'gray', value: '#f8fafc' },
|
||||
{ name: 'dark', value: '#0f172a' },
|
||||
],
|
||||
},
|
||||
},
|
||||
globalTypes: {
|
||||
theme: {
|
||||
description: 'Global theme for components',
|
||||
defaultValue: 'light',
|
||||
toolbar: {
|
||||
title: 'Theme',
|
||||
icon: 'circlehollow',
|
||||
items: ['light', 'dark'],
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速启动命令
|
||||
|
||||
### 创建新组件
|
||||
|
||||
```bash
|
||||
# 创建组件目录
|
||||
mkdir -p src/components/ComponentName
|
||||
|
||||
# 创建文件
|
||||
touch src/components/ComponentName/{ComponentName.tsx,ComponentName.stories.tsx,ComponentName.module.css,index.ts}
|
||||
```
|
||||
|
||||
### 安装 Storybook
|
||||
|
||||
```bash
|
||||
# 初始化 Storybook
|
||||
npx storybook@latest init
|
||||
|
||||
# 安装额外插件
|
||||
npm install -D @storybook/addon-a11y @storybook/addon-interactions
|
||||
|
||||
# 启动 Storybook
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 设计检查清单
|
||||
|
||||
### 组件质量检查
|
||||
|
||||
- [ ] Props 接口定义完整,带 JSDoc 注释
|
||||
- [ ] 支持必要的变体(variant)和尺寸(size)
|
||||
- [ ] 处理禁用和加载状态
|
||||
- [ ] 支持自定义 className
|
||||
- [ ] 键盘可访问性
|
||||
- [ ] 屏幕阅读器友好
|
||||
|
||||
### Storybook 质量检查
|
||||
|
||||
- [ ] 所有变体都有对应 Story
|
||||
- [ ] argTypes 配置完整
|
||||
- [ ] 包含组件文档描述
|
||||
- [ ] 交互状态可测试
|
||||
- [ ] 响应式展示
|
||||
|
||||
### 视觉质量检查
|
||||
|
||||
- [ ] 字体选择有特色
|
||||
- [ ] 配色方案协调
|
||||
- [ ] 动画流畅自然
|
||||
- [ ] 间距一致
|
||||
- [ ] 暗色主题支持
|
||||
Reference in New Issue
Block a user