From b72046b6dd3cad36ad3d07ae61a58e2405c963a5 Mon Sep 17 00:00:00 2001 From: wangdicoder Date: Fri, 27 Mar 2026 16:36:39 +1100 Subject: [PATCH] feat(tag): add variant prop with filled, soft, solid, and outlined styles --- .changeset/feat-tag-variant.md | 5 + apps/docs/src/routers.tsx | 6 +- packages/react/src/tag/__tests__/tag.test.tsx | 32 ++++ packages/react/src/tag/demo/Variant.tsx | 65 ++++++++ packages/react/src/tag/index.md | 54 ++++--- packages/react/src/tag/index.zh_CN.md | 54 ++++--- packages/react/src/tag/style/_index.scss | 153 ++++++++++++------ packages/react/src/tag/tag.tsx | 27 +++- packages/react/src/tag/types.ts | 3 + 9 files changed, 298 insertions(+), 101 deletions(-) create mode 100644 .changeset/feat-tag-variant.md create mode 100644 packages/react/src/tag/demo/Variant.tsx diff --git a/.changeset/feat-tag-variant.md b/.changeset/feat-tag-variant.md new file mode 100644 index 00000000..1debc7bc --- /dev/null +++ b/.changeset/feat-tag-variant.md @@ -0,0 +1,5 @@ +--- +"@tiny-design/react": minor +--- + +feat(tag): add `variant` prop with `filled`, `soft`, `solid`, and `outlined` styles diff --git a/apps/docs/src/routers.tsx b/apps/docs/src/routers.tsx index 2cf16675..315d22b7 100755 --- a/apps/docs/src/routers.tsx +++ b/apps/docs/src/routers.tsx @@ -171,8 +171,8 @@ export const getGuideMenu = (s: SiteLocale): RouterItem[] => { { title: s.guideMenu.groups.ai, children: [ - { title: s.guideMenu.mcpServer, route: 'mcp-server', component: pick(guide.mcpServer, isZh), tag: New }, - { title: s.guideMenu.cli, route: 'cli', component: pick(guide.cli, isZh), tag: New }, + { title: s.guideMenu.mcpServer, route: 'mcp-server', component: pick(guide.mcpServer, isZh), tag: New }, + { title: s.guideMenu.cli, route: 'cli', component: pick(guide.cli, isZh), tag: New }, ], }, { @@ -257,7 +257,7 @@ export const getComponentMenu = (s: SiteLocale): RouterItem[] => { { title: 'Timeline', route: 'timeline', component: pick(c.timeline, z) }, { title: 'Tooltip', route: 'tooltip', component: pick(c.tooltip, z) }, { title: 'Tree', route: 'tree', component: pick(c.tree, z) }, - { title: 'Chart', route: 'chart', component: pick(c.chart, z), tag: New }, + { title: 'Chart', route: 'chart', component: pick(c.chart, z), tag: New }, ], }, { diff --git a/packages/react/src/tag/__tests__/tag.test.tsx b/packages/react/src/tag/__tests__/tag.test.tsx index c2574eb5..3be51599 100644 --- a/packages/react/src/tag/__tests__/tag.test.tsx +++ b/packages/react/src/tag/__tests__/tag.test.tsx @@ -37,4 +37,36 @@ describe('', () => { container.firstChild && fireEvent.click(container.firstChild); expect(fn).toHaveBeenCalledTimes(1); }); + + // Variant tests + it('should apply filled variant class by default for preset color', () => { + const { container } = render(Blue); + expect(container.firstChild).toHaveClass('ty-tag_blue'); + expect(container.firstChild).not.toHaveClass('ty-tag_blue-solid'); + expect(container.firstChild).not.toHaveClass('ty-tag_blue-outlined'); + }); + + it('should apply solid variant class', () => { + const { container } = render(Blue); + expect(container.firstChild).toHaveClass('ty-tag_blue-solid'); + expect(container.firstChild).not.toHaveClass('ty-tag_blue'); + }); + + it('should apply soft variant class', () => { + const { container } = render(Blue); + expect(container.firstChild).toHaveClass('ty-tag_blue-soft'); + expect(container.firstChild).not.toHaveClass('ty-tag_blue'); + }); + + it('should apply outlined variant class', () => { + const { container } = render(Blue); + expect(container.firstChild).toHaveClass('ty-tag_blue-outlined'); + expect(container.firstChild).not.toHaveClass('ty-tag_blue'); + }); + + it('should not apply variant class without color', () => { + const { container } = render(Tag); + expect(container.firstChild).toHaveClass('ty-tag'); + expect(container.firstChild).not.toHaveClass('ty-tag_solid'); + }); }); diff --git a/packages/react/src/tag/demo/Variant.tsx b/packages/react/src/tag/demo/Variant.tsx new file mode 100644 index 00000000..da998544 --- /dev/null +++ b/packages/react/src/tag/demo/Variant.tsx @@ -0,0 +1,65 @@ +import { Tag, Typography } from '@tiny-design/react'; + +const colors = ['blue', 'green', 'orange', 'red', 'purple'] as const; +const statusColors = ['success', 'warning', 'info', 'danger'] as const; + +export default function VariantDemo() { + return ( + <> + + Filled (default): + +
+ {colors.map((color) => ( + {color} + ))} +
+ + + Soft: + +
+ {colors.map((color) => ( + {color} + ))} +
+ + + Solid: + +
+ {colors.map((color) => ( + {color} + ))} +
+ + + Outlined: + +
+ {colors.map((color) => ( + {color} + ))} +
+ + + Status (solid): + +
+ {statusColors.map((color) => ( + {color} + ))} +
+ + + Custom color variants: + +
+ filled + soft + solid + outlined +
+ + ); +} diff --git a/packages/react/src/tag/index.md b/packages/react/src/tag/index.md index cd4e60a9..967c0c46 100755 --- a/packages/react/src/tag/index.md +++ b/packages/react/src/tag/index.md @@ -10,6 +10,8 @@ import DynamicDemo from './demo/Dynamic'; import DynamicSource from './demo/Dynamic.tsx?raw'; import StatusDemo from './demo/Status'; import StatusSource from './demo/Status.tsx?raw'; +import VariantDemo from './demo/Variant'; +import VariantSource from './demo/Variant.tsx?raw'; # Tag @@ -59,17 +61,6 @@ Adding or removing a set of tags dynamically. - - - - - -### Colorful Tag - -We preset a series of colorful tag styles for use in different situations. You can also set it to a hex color string for custom color. - - - @@ -88,6 +79,26 @@ By using the `visible` prop, you can control the close state of Tag. + + + + + +### Colorful Tag + +We preset a series of colorful tag styles for use in different situations. You can also set it to a hex color string for custom color. + + + + + + +### Variant + +Tags support four variants: `filled` (default), `soft`, `solid`, and `outlined`. + + + @@ -96,16 +107,17 @@ By using the `visible` prop, you can control the close state of Tag. ### Tag -| Property | Description | Type | Default | -| -------------- | ---------------------------------------------- | ------------------------------ | ------- | -| color | color of the tag (preset or custom hex) | string | - | -| closable | whether the tag can be closed | boolean | false | -| defaultVisible | initial visibility | boolean | true | -| visible | controlled visibility | boolean | - | -| onClose | callback when tag is closed | (e: MouseEvent) => void | - | -| onClick | click callback | (e: MouseEvent) => void | - | -| style | style object of container | CSSProperties | - | -| className | className of container | string | - | +| Property | Description | Type | Default | +| -------------- | ---------------------------------------------- | --------------------------------------- | --------- | +| color | color of the tag (preset or custom hex) | string | - | +| variant | variant style of the tag | `'filled'` \| `'soft'` \| `'solid'` \| `'outlined'` | `'filled'` | +| closable | whether the tag can be closed | boolean | false | +| defaultVisible | initial visibility | boolean | true | +| visible | controlled visibility | boolean | - | +| onClose | callback when tag is closed | (e: MouseEvent) => void | - | +| onClick | click callback | (e: MouseEvent) => void | - | +| style | style object of container | CSSProperties | - | +| className | className of container | string | - | Preset colors: `magenta`, `red`, `volcano`, `orange`, `gold`, `lime`, `green`, `cyan`, `blue`, `geekblue`, `purple`. diff --git a/packages/react/src/tag/index.zh_CN.md b/packages/react/src/tag/index.zh_CN.md index 5b48f4f2..86398793 100644 --- a/packages/react/src/tag/index.zh_CN.md +++ b/packages/react/src/tag/index.zh_CN.md @@ -10,6 +10,8 @@ import DynamicDemo from './demo/Dynamic'; import DynamicSource from './demo/Dynamic.tsx?raw'; import StatusDemo from './demo/Status'; import StatusSource from './demo/Status.tsx?raw'; +import VariantDemo from './demo/Variant'; +import VariantSource from './demo/Variant.tsx?raw'; # Tag @@ -59,17 +61,6 @@ const { CheckableTag } = Tag; - - - - - -### 多彩标签 - -我们提供了一系列预设的彩色标签样式,适用于不同场景。你也可以自定义十六进制颜色值。 - - - @@ -88,6 +79,26 @@ const { CheckableTag } = Tag; + + + + + +### 多彩标签 + +我们提供了一系列预设的彩色标签样式,适用于不同场景。你也可以自定义十六进制颜色值。 + + + + + + +### 变体 + +标签支持四种变体样式:`filled`(默认)、`soft`、`solid` 和 `outlined`。 + + + @@ -96,16 +107,17 @@ const { CheckableTag } = Tag; ### Tag -| 属性 | 说明 | 类型 | 默认值 | -| -------------- | ---------------------------------------------- | ------------------------------ | ------- | -| color | 标签颜色(预设颜色或自定义十六进制值) | string | - | -| closable | 标签是否可关闭 | boolean | false | -| defaultVisible | 初始显示状态 | boolean | true | -| visible | 受控的显示状态 | boolean | - | -| onClose | 关闭标签时的回调 | (e: MouseEvent) => void | - | -| onClick | 点击回调 | (e: MouseEvent) => void | - | -| style | 容器样式对象 | CSSProperties | - | -| className | 容器的 className | string | - | +| 属性 | 说明 | 类型 | 默认值 | +| -------------- | ---------------------------------------------- | ----------------------------------------------------- | ---------- | +| color | 标签颜色(预设颜色或自定义十六进制值) | string | - | +| variant | 标签的变体样式 | `'filled'` \| `'soft'` \| `'solid'` \| `'outlined'` | `'filled'` | +| closable | 标签是否可关闭 | boolean | false | +| defaultVisible | 初始显示状态 | boolean | true | +| visible | 受控的显示状态 | boolean | - | +| onClose | 关闭标签时的回调 | (e: MouseEvent) => void | - | +| onClick | 点击回调 | (e: MouseEvent) => void | - | +| style | 容器样式对象 | CSSProperties | - | +| className | 容器的 className | string | - | 预设颜色:`magenta`、`red`、`volcano`、`orange`、`gold`、`lime`、`green`、`cyan`、`blue`、`geekblue`、`purple`。 diff --git a/packages/react/src/tag/style/_index.scss b/packages/react/src/tag/style/_index.scss index 36b0dd9c..e7e2a373 100755 --- a/packages/react/src/tag/style/_index.scss +++ b/packages/react/src/tag/style/_index.scss @@ -1,5 +1,8 @@ @use '../../style/variables' as *; +$tag-preset-colors: magenta, red, volcano, orange, gold, lime, green, cyan, blue, geekblue, purple; +$tag-status-colors: success, info, warning, danger; + .#{$prefix}-tag { user-select: none; display: none; @@ -37,93 +40,139 @@ } } - &_magenta { - color: var(--ty-tag-magenta-color); - background: var(--ty-tag-magenta-bg); - border-color: var(--ty-tag-magenta-border); + // ============= Preset colors: filled (default) ============= + @each $color in $tag-preset-colors { + &_#{$color} { + color: var(--ty-tag-#{$color}-color); + background: var(--ty-tag-#{$color}-bg); + border-color: var(--ty-tag-#{$color}-border); + } } - &_red { - color: var(--ty-tag-red-color); - background: var(--ty-tag-red-bg); - border-color: var(--ty-tag-red-border); + // ============= Status colors: filled (default) ============= + &_success { + color: var(--ty-color-success); + background: var(--ty-color-success-bg); + border-color: var(--ty-color-success-border); } - &_volcano { - color: var(--ty-tag-volcano-color); - background: var(--ty-tag-volcano-bg); - border-color: var(--ty-tag-volcano-border); + &_info { + color: var(--ty-color-info); + background: var(--ty-color-info-bg); + border-color: var(--ty-color-info-border); } - &_orange { - color: var(--ty-tag-orange-color); - background: var(--ty-tag-orange-bg); - border-color: var(--ty-tag-orange-border); + &_warning { + color: var(--ty-color-warning); + background: var(--ty-color-warning-bg); + border-color: var(--ty-color-warning-border); } - &_gold { - color: var(--ty-tag-gold-color); - background: var(--ty-tag-gold-bg); - border-color: var(--ty-tag-gold-border); + &_danger { + color: var(--ty-color-danger); + background: var(--ty-color-danger-bg); + border-color: var(--ty-color-danger-border); } - &_lime { - color: var(--ty-tag-lime-color); - background: var(--ty-tag-lime-bg); - border-color: var(--ty-tag-lime-border); + // ============= Preset colors: soft ============= + @each $color in $tag-preset-colors { + &_#{$color}-soft { + color: var(--ty-tag-#{$color}-color); + background: var(--ty-tag-#{$color}-bg); + border-color: transparent; + } } - &_green { - color: var(--ty-tag-green-color); - background: var(--ty-tag-green-bg); - border-color: var(--ty-tag-green-border); + // ============= Status colors: soft ============= + &_success-soft { + color: var(--ty-color-success); + background: var(--ty-color-success-bg); + border-color: transparent; } - &_cyan { - color: var(--ty-tag-cyan-color); - background: var(--ty-tag-cyan-bg); - border-color: var(--ty-tag-cyan-border); + &_info-soft { + color: var(--ty-color-info); + background: var(--ty-color-info-bg); + border-color: transparent; } - &_blue { - color: var(--ty-tag-blue-color); - background: var(--ty-tag-blue-bg); - border-color: var(--ty-tag-blue-border); + &_warning-soft { + color: var(--ty-color-warning); + background: var(--ty-color-warning-bg); + border-color: transparent; } - &_geekblue { - color: var(--ty-tag-geekblue-color); - background: var(--ty-tag-geekblue-bg); - border-color: var(--ty-tag-geekblue-border); + &_danger-soft { + color: var(--ty-color-danger); + background: var(--ty-color-danger-bg); + border-color: transparent; } - &_purple { - color: var(--ty-tag-purple-color); - background: var(--ty-tag-purple-bg); - border-color: var(--ty-tag-purple-border); + // ============= Preset colors: solid ============= + @each $color in $tag-preset-colors { + &_#{$color}-solid { + color: #fff; + background: var(--ty-tag-#{$color}-color); + border-color: var(--ty-tag-#{$color}-color); + } } - &_success { + // ============= Status colors: solid ============= + &_success-solid { + color: #fff; + background: var(--ty-color-success); + border-color: var(--ty-color-success); + } + + &_info-solid { + color: #fff; + background: var(--ty-color-info); + border-color: var(--ty-color-info); + } + + &_warning-solid { + color: #fff; + background: var(--ty-color-warning); + border-color: var(--ty-color-warning); + } + + &_danger-solid { + color: #fff; + background: var(--ty-color-danger); + border-color: var(--ty-color-danger); + } + + // ============= Preset colors: outlined ============= + @each $color in $tag-preset-colors { + &_#{$color}-outlined { + color: var(--ty-tag-#{$color}-color); + background: transparent; + border-color: var(--ty-tag-#{$color}-border); + } + } + + // ============= Status colors: outlined ============= + &_success-outlined { color: var(--ty-color-success); - background: var(--ty-color-success-bg); + background: transparent; border-color: var(--ty-color-success-border); } - &_info { + &_info-outlined { color: var(--ty-color-info); - background: var(--ty-color-info-bg); + background: transparent; border-color: var(--ty-color-info-border); } - &_warning { + &_warning-outlined { color: var(--ty-color-warning); - background: var(--ty-color-warning-bg); + background: transparent; border-color: var(--ty-color-warning-border); } - &_danger { + &_danger-outlined { color: var(--ty-color-danger); - background: var(--ty-color-danger-bg); + background: transparent; border-color: var(--ty-color-danger-border); } } diff --git a/packages/react/src/tag/tag.tsx b/packages/react/src/tag/tag.tsx index 48b41b7c..6bbc7d88 100644 --- a/packages/react/src/tag/tag.tsx +++ b/packages/react/src/tag/tag.tsx @@ -11,6 +11,7 @@ const Tag = React.memo(forwardRef((props, ref) => { defaultVisible = true, prefixCls: customisedCls, color, + variant = 'filled', onClose, onClick, className, @@ -23,8 +24,12 @@ const Tag = React.memo(forwardRef((props, ref) => { ); const configContext = useContext(ConfigContext); const prefixCls = getPrefixCls('tag', configContext.prefixCls, customisedCls); + const isPresetColor = color && PresetColors.includes(color); const cls = classNames(prefixCls, className, { - [`${prefixCls}_${color}`]: color && PresetColors.includes(color), + [`${prefixCls}_${color}`]: isPresetColor && variant === 'filled', + [`${prefixCls}_${color}-soft`]: isPresetColor && variant === 'soft', + [`${prefixCls}_${color}-solid`]: isPresetColor && variant === 'solid', + [`${prefixCls}_${color}-outlined`]: isPresetColor && variant === 'outlined', [`${prefixCls}_visible`]: visible, [`${prefixCls}_closeable`]: closable, }); @@ -41,10 +46,24 @@ const Tag = React.memo(forwardRef((props, ref) => { visibleProp === undefined && setVisible(false); }; + const getCustomColorStyle = (): React.CSSProperties => { + if (!color || isPresetColor) return {}; + + switch (variant) { + case 'soft': + return { backgroundColor: color, borderColor: 'transparent', color: '#fff' }; + case 'solid': + return { backgroundColor: color, borderColor: color, color: '#fff' }; + case 'outlined': + return { backgroundColor: 'transparent', borderColor: color, color: color }; + case 'filled': + default: + return { backgroundColor: color, borderColor: color, color: '#fff' }; + } + }; + const tagStyle: React.CSSProperties = { - backgroundColor: color ? (PresetColors.includes(color) ? undefined : color) : undefined, - borderColor: color ? (PresetColors.includes(color) ? undefined : color) : undefined, - color: color ? (PresetColors.includes(color) ? undefined : '#fff') : undefined, + ...getCustomColorStyle(), ...style, }; diff --git a/packages/react/src/tag/types.ts b/packages/react/src/tag/types.ts index 7bd3816f..0d23ab00 100644 --- a/packages/react/src/tag/types.ts +++ b/packages/react/src/tag/types.ts @@ -10,6 +10,8 @@ export interface CheckableTagProps extends BaseProps { export type StatusColor = 'success' | 'warning' | 'info' | 'danger'; +export type TagVariant = 'filled' | 'soft' | 'solid' | 'outlined'; + export const StatusColors: StatusColor[] = ['success', 'info', 'warning', 'danger']; export const PresetColors = [ @@ -29,6 +31,7 @@ export const PresetColors = [ export interface TagProps extends BaseProps, React.PropsWithoutRef { color?: string | StatusColor; + variant?: TagVariant; closable?: boolean; onClose?: React.MouseEventHandler; onClick?: React.MouseEventHandler;