@tfdesign/b-end 1.0.11 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@
9
9
  *
10
10
  * @component RadioGroup
11
11
  * @prop {'brand'|'black'} [variant='brand'] — 色系:brand 选中绿色、black 选中深灰
12
+ * @prop {'basic'|'card'|'pureCard'} [styleType='basic'] — 样式:基础样式 / 带圆点单选卡片 / 单选卡片
12
13
  * @prop {'vertical'|'horizontal'} [layout='vertical'] — 布局:vertical 纵向 12px / horizontal 横向 16px
13
14
  * @prop {boolean} [disabled=false] — 禁用整组
14
15
  * @prop {string} [value] — 受控选中值
@@ -28,23 +29,40 @@ import {
28
29
  createContext,
29
30
  useContext,
30
31
  useId,
32
+ useRef,
33
+ useLayoutEffect,
34
+ useEffect,
31
35
  useMemo,
32
36
  useState,
33
37
  useCallback,
34
38
  } from 'react';
39
+ import Tooltip from './Tooltip';
35
40
 
36
41
  const RadioContext = createContext(null);
37
42
 
38
43
  /* ── 布局方向 → 纵向组距 12px / 横向组距 16px ── */
39
44
  const LAYOUT_CLASS = {
40
45
  vertical: 'flex flex-col gap-3',
41
- horizontal: 'inline-flex flex-row flex-wrap items-start gap-4',
46
+ horizontal: 'flex w-full flex-row flex-wrap items-start gap-4',
47
+ };
48
+
49
+ const CARD_LAYOUT_CLASS = {
50
+ vertical: 'flex flex-col gap-3',
51
+ horizontal: 'flex w-full flex-row flex-wrap items-start gap-4',
42
52
  };
43
53
 
44
54
  /* ── 基础 → 组容器字体继承 ── */
45
55
  const GROUP_BASE = '[font-family:inherit]';
46
56
 
47
57
  const LABEL_ROW = 'group inline-flex items-start gap-2 select-none rounded-sm';
58
+ const CARD_LABEL_BASE = [
59
+ 'group inline-flex h-[46px] max-w-full select-none items-center justify-start rounded-md',
60
+ 'border border-solid bg-white px-4 text-sm leading-[20px] transition-colors duration-150',
61
+ ].join(' ');
62
+ const CARD_WITH_DOT_SIZE = 'min-w-[128px] gap-2';
63
+ const PURE_CARD_SIZE = 'min-w-[104px]';
64
+ const CARD_TEXT = 'block min-w-0 flex-1 truncate text-left';
65
+ const CARD_TOOLTIP_TRIGGER = 'block min-w-0 flex-1';
48
66
 
49
67
  /* ── 指示器 → 内包裹层,margin-top 2px 与 20px 行高对齐 ── */
50
68
  const INNER_WRAP = 'relative mt-[2px] inline-flex shrink-0';
@@ -66,6 +84,102 @@ const FOCUS_OUTLINE = {
66
84
  black: 'has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-grey-400',
67
85
  };
68
86
 
87
+ function OverflowTooltipText({
88
+ children,
89
+ triggerClassName,
90
+ textClassName,
91
+ textStyle,
92
+ }) {
93
+ const textRef = useRef(null);
94
+ const [showTooltip, setShowTooltip] = useState(false);
95
+
96
+ const measureOverflow = useCallback(() => {
97
+ const el = textRef.current;
98
+ if (!el) return;
99
+ setShowTooltip(el.scrollWidth - el.clientWidth > 1);
100
+ }, []);
101
+
102
+ useLayoutEffect(() => {
103
+ measureOverflow();
104
+ }, [children, measureOverflow]);
105
+
106
+ useEffect(() => {
107
+ const el = textRef.current;
108
+ if (!el) return undefined;
109
+
110
+ measureOverflow();
111
+
112
+ let observer;
113
+ if (typeof ResizeObserver !== 'undefined') {
114
+ observer = new ResizeObserver(() => measureOverflow());
115
+ observer.observe(el);
116
+ }
117
+
118
+ const handleResize = () => measureOverflow();
119
+ window.addEventListener('resize', handleResize);
120
+
121
+ return () => {
122
+ observer?.disconnect();
123
+ window.removeEventListener('resize', handleResize);
124
+ };
125
+ }, [measureOverflow]);
126
+
127
+ const textNode = (
128
+ <span
129
+ ref={textRef}
130
+ className={textClassName}
131
+ style={textStyle}
132
+ >
133
+ {children}
134
+ </span>
135
+ );
136
+
137
+ if (!showTooltip) {
138
+ return (
139
+ <span className={[triggerClassName, textClassName].filter(Boolean).join(' ')} style={textStyle} ref={textRef}>
140
+ {children}
141
+ </span>
142
+ );
143
+ }
144
+
145
+ return (
146
+ <Tooltip content={children} placement="top" triggerClassName={triggerClassName}>
147
+ {textNode}
148
+ </Tooltip>
149
+ );
150
+ }
151
+
152
+ function buildCardLabelClass(checked, disabled, variant, styleType) {
153
+ const widthCls = styleType === 'card' ? CARD_WITH_DOT_SIZE : PURE_CARD_SIZE;
154
+ const selectedCls = variant === 'black'
155
+ ? [
156
+ 'border-grey-950 !bg-[var(--fill-default,var(--color-fill))] text-foreground',
157
+ 'hover:border-grey-900 hover:bg-blueGrey-100',
158
+ 'active:border-grey-800',
159
+ ].join(' ')
160
+ : [
161
+ 'border-border-brand !bg-[var(--color-brand-50)] text-foreground',
162
+ 'hover:border-brand-600 hover:bg-brand-100',
163
+ 'active:border-brand-700',
164
+ ].join(' ');
165
+
166
+ if (disabled) {
167
+ return [
168
+ CARD_LABEL_BASE,
169
+ widthCls,
170
+ 'cursor-not-allowed border-border-default bg-disabled text-foreground-disabled opacity-75',
171
+ ].join(' ');
172
+ }
173
+
174
+ return [
175
+ CARD_LABEL_BASE,
176
+ widthCls,
177
+ checked
178
+ ? selectedCls
179
+ : 'cursor-pointer border-border-default text-foreground hover:border-border-strong hover:bg-fill',
180
+ ].join(' ');
181
+ }
182
+
69
183
  function buildDisplayClass(checked, disabled, variant) {
70
184
  const sz = OUTER_SIZE;
71
185
  if (disabled) {
@@ -107,7 +221,9 @@ function buildDisplayClass(checked, disabled, variant) {
107
221
  return [
108
222
  OUTER_BASE, sz,
109
223
  'border-foreground-muted bg-transparent',
110
- 'group-hover:bg-fill group-hover:border-border-brand',
224
+ variant === 'black'
225
+ ? 'group-hover:bg-fill group-hover:border-grey-950'
226
+ : 'group-hover:bg-fill group-hover:border-border-brand',
111
227
  'group-active:bg-blueGrey-100',
112
228
  ].join(' ');
113
229
  }
@@ -121,6 +237,7 @@ export function Radio({ value, disabled: itemDisabled = false, className = '', c
121
237
  setSelected,
122
238
  groupDisabled,
123
239
  variant,
240
+ styleType,
124
241
  name,
125
242
  } = ctx;
126
243
 
@@ -138,6 +255,56 @@ export function Radio({ value, disabled: itemDisabled = false, className = '', c
138
255
  setSelected(value, e);
139
256
  }, [disabled, setSelected, value]);
140
257
 
258
+ if (styleType === 'card' || styleType === 'pureCard') {
259
+ const cardCls = buildCardLabelClass(checked, disabled, variant, styleType);
260
+ const showIndicator = styleType === 'card';
261
+ const cardStyle = checked && !disabled
262
+ ? {
263
+ backgroundColor: variant === 'black'
264
+ ? 'var(--fill-default, var(--color-fill))'
265
+ : 'var(--color-brand-50)',
266
+ }
267
+ : undefined;
268
+
269
+ return (
270
+ <label
271
+ className={[`tfds-radio`, cardCls, fo, className].filter(Boolean).join(' ')}
272
+ style={cardStyle}
273
+ data-tfds-component="Radio"
274
+ >
275
+ <input
276
+ type="radio"
277
+ name={name}
278
+ value={String(value)}
279
+ checked={checked}
280
+ disabled={disabled}
281
+ onChange={onChange}
282
+ className="sr-only"
283
+ />
284
+ {showIndicator ? (
285
+ <span className={displayCls} aria-hidden>
286
+ {checked ? (
287
+ <span className={`rounded-full bg-white ${dot}`} />
288
+ ) : null}
289
+ </span>
290
+ ) : null}
291
+ {children != null && children !== false ? (
292
+ <OverflowTooltipText
293
+ triggerClassName={CARD_TOOLTIP_TRIGGER}
294
+ textClassName={[
295
+ CARD_TEXT,
296
+ checked ? '![font-weight:var(--font-semibold)]' : '',
297
+ disabled ? 'text-foreground-disabled' : 'text-current',
298
+ ].join(' ')}
299
+ textStyle={checked ? { fontWeight: 'var(--font-semibold)' } : undefined}
300
+ >
301
+ {children}
302
+ </OverflowTooltipText>
303
+ ) : null}
304
+ </label>
305
+ );
306
+ }
307
+
141
308
  return (
142
309
  <label
143
310
  className={[`tfds-radio`, [
@@ -179,6 +346,7 @@ export function Radio({ value, disabled: itemDisabled = false, className = '', c
179
346
 
180
347
  export default function RadioGroup({
181
348
  variant = 'brand',
349
+ styleType = 'basic',
182
350
  size: _size = 'md',
183
351
  layout = 'vertical',
184
352
  disabled = false,
@@ -207,10 +375,12 @@ export default function RadioGroup({
207
375
  setSelected,
208
376
  groupDisabled: disabled,
209
377
  variant,
378
+ styleType,
210
379
  name,
211
- }), [selected, setSelected, disabled, variant, name]);
380
+ }), [selected, setSelected, disabled, variant, styleType, name]);
212
381
 
213
- const layoutCls = LAYOUT_CLASS[layout] || LAYOUT_CLASS.vertical;
382
+ const layoutMap = styleType === 'card' || styleType === 'pureCard' ? CARD_LAYOUT_CLASS : LAYOUT_CLASS;
383
+ const layoutCls = layoutMap[layout] || layoutMap.vertical;
214
384
  const sizeText = 'text-sm';
215
385
 
216
386
  return (
@@ -8,6 +8,11 @@ export const RADIO_TOKEN_MAP = {
8
8
  { label: '纵向间距', cssProp: 'row-gap', token: '--spacing-3', value: '12px' },
9
9
  { label: '横向间距', cssProp: 'gap', token: '--spacing-4', value: '16px' },
10
10
  ],
11
+ 样式分类: [
12
+ { label: '基础样式', cssProp: 'styleType', value: 'basic', state: 'default' },
13
+ { label: '带圆点单选卡片', cssProp: 'styleType', value: 'card', state: 'card' },
14
+ { label: '单选卡片', cssProp: 'styleType', value: 'pureCard', state: 'pureCard' },
15
+ ],
11
16
  组内排版: [
12
17
  { label: '图文间距', cssProp: 'column-gap', token: '--spacing-2', value: '8px' },
13
18
  { label: '顶部偏移', cssProp: 'margin-top', value: '2px' },
@@ -44,6 +49,23 @@ export const RADIO_TOKEN_MAP = {
44
49
  { label: '禁用背景', cssProp: 'background', token: '--color-disabled', value: '#F9FAFB', semanticRef: 'bg-disabled', state: 'disabled' },
45
50
  { label: '禁用边框', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default', state: 'disabled' },
46
51
  ],
52
+ 单选卡片: [
53
+ { label: '带圆点卡片宽度', cssProp: 'min-width', value: '128px', state: 'card' },
54
+ { label: '纯卡片宽度', cssProp: 'min-width', value: '104px', state: 'pureCard' },
55
+ { label: '卡片高度', cssProp: 'height', value: '46px', state: 'card / pureCard' },
56
+ { label: '卡片圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
57
+ { label: '卡片水平内边距', cssProp: 'padding-inline', token: '--spacing-4', value: '16px' },
58
+ { label: '默认背景', cssProp: 'background', token: '--color-white', value: '#FFFFFF' },
59
+ { label: '默认描边', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
60
+ { label: '悬浮描边', cssProp: 'border-color', token: '--color-border-strong', value: '#D0D5DD', semanticRef: 'border-strong', state: 'hover' },
61
+ { label: '文案对齐', cssProp: 'text-align', value: 'left', state: 'card / pureCard' },
62
+ { label: '文案溢出', cssProp: 'overflow', value: 'truncate + Tooltip', state: 'card / pureCard' },
63
+ { label: '选中文字重', cssProp: 'font-weight', value: '600', state: 'card / pureCard + checked' },
64
+ { label: '品牌选中浅底', cssProp: 'background', token: '--color-brand-50', value: '#EAFAF6', state: 'brand+checked' },
65
+ { label: '品牌选中描边', cssProp: 'border-color', token: '--color-border-brand', value: '#56D3BC', semanticRef: 'border-brand', state: 'brand+checked' },
66
+ { label: '主色黑选中浅底', cssProp: 'background', token: '--fill-default', value: 'rgba(83, 96, 143, 0.07)', semanticRef: 'fill-default', state: 'black+checked' },
67
+ { label: '主色黑选中描边', cssProp: 'border-color', token: '--color-grey-950', value: '#111116', state: 'black+checked' },
68
+ ],
47
69
  sizes: {
48
70
  sm: [
49
71
  { label: '外圆边长', cssProp: 'width / height', value: '14px' },
@@ -1232,7 +1232,7 @@ export const COMPONENTS = [
1232
1232
  '【信息密度】标题最多 1 行,主状态标签建议 1 个,辅助标签建议总数不超过 2 个;超过时优先保留最影响当前决策的信息。',
1233
1233
  '【辅助信息】用户名、单号、时间属于辅助识别信息;空间不足时允许截断,但应通过 Tooltip 提供完整信息。',
1234
1234
  '【标签语义】右侧状态标签只承载“当前处理状态”;待干预 / 异常用 red,托管中 / 正常托管用 green,挂起 / 等待用 orange,已处理 / 已完成用 blue;原因、规则、倒计时等信息作为辅助标签。',
1235
- '【收起态】仅当空间受限但仍需保留对象切换能力时收起为纯头像态;收起态只承担切换,不承担复杂状态判断,展开后应恢复用户上次工作宽度。',
1235
+ '【收起态】仅当空间受限但仍需保留对象切换能力时收起为纯头像态;收起态只承担切换,不承担复杂状态判断,展开后应恢复用户上次工作宽度。纯头像模式每个头像按钮行高固定为 68px,选中态与未选中态高度必须一致;选中态不得因只包裹头像内容而变成自适应高度,禁止用 h-auto、min-h 或按 Avatar 内容高度撑开替代固定 68px。',
1236
1236
  '【卡片列表限制】`variant="card"` 时顶部 tabs 是唯一的分类切换入口;卡片列表不显示分组标题,也不支持分组展开/收起。选中“全部”时,卡片必须按状态分类顺序 `待干预 → 托管中 → 其它` 自上而下连续排列;禁止再引入独立 `cards` 数据源或把 card 形态拆成另一套列表。',
1237
1237
  '【卡片列表宽度】`variant="card"` 时不进入纯头像收起态,但保留整体宽度拖拽能力;拖拽变宽后卡片宽度必须按实际可用宽度自适应扩展,拖窄时不得小于 333px。',
1238
1238
  '【卡片列表栅格】当 `variant="card"` 时,列数必须基于组件实际可用宽度动态判断:`<=580px` 为 1 列,`>580px` 为 2 列,`>950px` 为 3 列;嵌入客服工作台等外层可拖拽容器时,也必须读取外层给到的实际宽度,而不是固定按默认 400px 判断。卡片横向与纵向间距都统一为 `16px`。',
@@ -1624,12 +1624,14 @@ export const COMPONENTS = [
1624
1624
  /* —— 3. 执行流 —— */
1625
1625
  '【执行流·结构】title + steps[] 构成完整执行流;步骤左侧 16px 轨道槽位 + 12px 内容间距 + 14px/20px 正文;右侧 chevron 控制收起展开',
1626
1626
  '【执行流·状态】status="completed" 绿色对勾;status="processing" 旋转环形动效 + 最新一条操作卡片浅灰骨架屏扫光',
1627
- '【执行流·结果区】resultText 独立通栏文本;resultArtifacts[] 多张产物卡(网页/代码/表格,统一 32px 玻璃感图标 + 标题 + meta + 更多按钮 + 单行省略号);confirms[] 是结果区的另一种形态——人工确认卡(mode="text-card" 文本+卡片,"card-only" 仅卡片)',
1627
+ '【执行流·结果区】resultText 独立通栏文本;resultArtifacts[] 多张产物卡(网页/代码/表格,统一 32px 玻璃感图标 + 标题 + meta + 更多按钮 + 单行省略号);confirms[] 是结果区的另一种形态——人工确认卡(mode="text-card" 文本+卡片,"card-only" 仅卡片,"option-card" 澄清确认卡片1,"form-card" 澄清确认卡片2)',
1628
1628
  '【执行流·任务进行中徽章】taskBadge 传字符串启用 indigo 徽章 + 星标,位于结果区之后、消息操作之前,常用于"任务进行中"提示',
1629
1629
 
1630
1630
  /* —— 4. 卡片回复 —— */
1631
1631
  '【卡片·任务规划】plan 传对象启用:灰底圆角 12 容器 + 头部图标 + 任务数徽章 + chevron;展开后白底内容卡内紫色编号方块(gradient violet)+ 任务标题 + 灰圆点子项;右下"开始执行任务"黑底主按钮',
1632
1632
  '【卡片·配置表单】配置表单类卡片复用执行流的 confirms(mode="text-card" 或 "card-only"),需要"次操作 + 主操作"按钮组的人工确认场景',
1633
+ '【卡片·澄清确认卡片1】凡是涉及用户来确认/澄清以帮助 AI 确认内容的场景,统一使用 confirms mode="option-card";内部必须复用基础组件 RadioGroup/Radio 的 styleType="pureCard" + variant="brand" 单选卡片,不允许手搓 option div;options 支持 { value, label, disabled },defaultSelectedValue 设置默认选项,onPrimaryAction 接收 { value, option };面板尺寸/底色完全对齐【配置表单】(mode="text-card"/"card-only"),仅内容由文本换成单选组件',
1634
+ '【卡片·澄清确认卡片2】当 AI 需要用户通过结构化字段(多个选项 + 文本输入)来确认/澄清信息时,统一使用 confirms mode="form-card";内部必须复用基础组件 Form + FormField,formItems[] 支持 select/input/input-number/textarea/radio/checkbox/switch/date-picker 等字段类型,每项 { id, label, type, placeholder, defaultValue, options?, fullWidth },不允许手搓 input/select;面板尺寸/底色完全对齐【配置表单】与【澄清确认卡片1】,仅内容由单选组件换成 Form 表单组件',
1633
1635
 
1634
1636
  /* —— 5. 深度思考 —— */
1635
1637
  '【深度思考·进行中】status="thinking" + thinking={ state:"thinking", inProgressLabel:"深度思考中 ..." },仅显示头像 + 思考文案,不渲染其他内容',
@@ -1666,6 +1668,8 @@ export const COMPONENTS = [
1666
1668
  /* 卡片回复 */
1667
1669
  { label: '卡片 · 任务规划', code: '<ChatMessage header title="" steps={null} plan={{ tasks: [{ title: "信息准备", items: ["分析背景获取接入方信息", "明确 Workflow 改动范围"] }, { title: "结构准备", items: ["查询 DSL 语法", "整理可复用组件"] }] }} />' },
1668
1670
  { label: '卡片 · 配置表单(人工确认)', code: `<ChatMessage header title="" steps={null} resultText="已完成售后政策摘要整理,以下审核口径需要人工确认后再继续生成标准答复。" confirms={[{ mode: "text-card", title: "售后政策确认", description: "请确认退货退款、换货与仅退款的适用范围", iconName: "sticker-square-stroked", secondaryActionLabel: "返回修改", primaryActionLabel: "确认继续" }]} />` },
1671
+ { label: '卡片 · 澄清确认卡片1', code: `<ChatMessage header title="" steps={null} confirms={[{ mode: "option-card", title: "澄清确认", iconName: "clipboard-check-stroked", primaryActionLabel: "确认", defaultSelectedValue: "handoff-guide", options: [{ value: "handoff-guide", label: "方案1: 用户请求转人工但系统未触发转接,持续引导自助操作" }, { value: "handoff", label: "方案2: 用户请求转人工" }, { value: "self-service", label: "方案3: 持续引导自助操作" }], onPrimaryAction: ({ value, option }) => {} }]} />` },
1672
+ { label: '卡片 · 澄清确认卡片2', code: `<ChatMessage header title="" steps={null} confirms={[{ mode: "form-card", title: "澄清确认", iconName: "clipboard-check-stroked", primaryActionLabel: "确认", formItems: [{ id: "scene", label: "业务场景", type: "select", placeholder: "请选择业务场景", defaultValue: "after-sales", options: [{ value: "after-sales", label: "售后服务" }, { value: "pre-sales", label: "售前咨询" }, { value: "logistics", label: "物流配送" }], fullWidth: true }, { id: "channel", label: "处理渠道", type: "select", placeholder: "请选择处理渠道", defaultValue: "self-service", options: [{ value: "self-service", label: "自助引导" }, { value: "handoff", label: "转人工" }], fullWidth: true }, { id: "remark", label: "补充说明", type: "input", placeholder: "请输入补充说明(可选)", fullWidth: true }] }]} />` },
1669
1673
 
1670
1674
  /* 深度思考 */
1671
1675
  { label: '深度思考 · 思考中', code: '<ChatMessage header status="thinking" steps={null} thinking={{ state: "thinking", inProgressLabel: "深度思考中 ..." }} />' },
@@ -2001,7 +2005,10 @@ export const COMPONENTS = [
2001
2005
  '【组件定位】Form 是字段组合层,只管理 label、提示、错误反馈、top/left 布局和字段编排,不重新定义已有表单控件样式',
2002
2006
  '【控件复用】已有基础组件必须复用:input 复用 Input,textarea 复用 TextArea(仅传基组件已有 props,默认 minRows=3、resize 纵向增高与 TextArea 一致;System Prompt / JSON / 代码参数字段必须传 variant="code"),input-number 复用 InputNumber,select 复用 Select,tag-input 复用 TagInput,checkbox 复用 CheckboxGroup,radio 复用 RadioGroup,switch 复用 Switch,date-picker 复用 DatePicker,time-picker 复用 TimePicker,slider 复用 Slider,upload 复用 Upload',
2003
2007
  '【布局】Form 在页面主内容区、白色卡片容器与纵向堆叠场景中默认 `w-full min-w-0 self-stretch`;labelPosition="top" 时字段容器与控件区都默认全宽,label 与控件间距 4px;labelPosition="left" 时标签列固定 96px、间距 24px、右侧控件区使用 `flex-1 min-w-0 w-full` 撑满剩余宽度',
2004
- '【表单宽度铁律】页面主表单、白卡表单、抽屉/弹窗正文表单、侧栏详情表单中,Input / Select / TextArea / InputNumber / DatePicker / TimePicker / TagInput 必须默认撑满字段内容列,且同一区域左右边界对齐;只有筛选栏、工具条、紧凑查询区允许固定宽 160/200/240px,禁止把 300px 示意宽或筛选栏固定宽套到业务表单正文',
2008
+ '【正文表单宽度铁律】页面主表单、白卡/卡片内配置表单、澄清确认卡片、抽屉/弹窗正文表单、侧栏详情表单中,Input / Select / TextArea / InputNumber / DatePicker / TimePicker / TagInput / Slider / InputGroup 这类字段型控件必须默认撑满字段内容列,且同一表单区域左右边界对齐、宽度一致。多个字段上下排列在同一卡片容器内时,每个字段都应随卡片内容区动态撑满,不受 240px / 200px / 320px 的筛选搜索宽度限制。',
2009
+ '【筛选工具条宽度例外】只有列表筛选栏、工具条、紧凑查询区、AI 首页模板搜索、Tabs 右侧搜索这类横向辅助查询入口,才允许固定中窄宽:搜索 Input 默认约 240px、最小 200px、最大 320px;Select / DatePicker / TimePicker / TagInput 等筛选控件常用 160-240px(日期时间范围可更宽)。禁止把筛选工具条固定宽套到业务表单正文。',
2010
+ '【筛选栏不是纵向表单】列表页顶部筛选栏、工具条、紧凑查询区不使用 Form 的纵向正文表单排布;搜索 Input、Select/DatePicker/TagInput 等筛选控件、重置/查询 Button 必须放在同一个横向 flex 行流中(推荐 `flex items-center flex-wrap gap-2`),禁止用 `Form layout="vertical"`、`flex-col` 或 `space-y-*` 把筛选条件和按钮上下堆叠。只有真正的配置填写、详情编辑、弹窗正文才使用 Form 的纵向/网格字段布局。',
2011
+ '【紧凑/特殊控件宽度】Switch、普通 CheckboxGroup、普通 RadioGroup 的控件本体按语义保持自然宽度,但字段容器仍占满表单区域并与其它字段左边界对齐;卡片型 Radio/Checkbox 选项可按场景设置 `w-full` 形成等宽选项。Upload 字段区域占满表单区域,内部图片 tile 保持固定尺寸并自动换行,不强行拉伸 tile。',
2005
2012
  '【自定义控件宽度】使用 children 或 item.control 传入自定义字段时,外层必须补 `className="w-full min-w-0 max-w-full"`;否则会绕过 Form 内置的 `!w-full` 控件宽度约束,导致 Input、Select、TextArea 等宽度不一致',
2006
2013
  '【详情页全集预览】组件详情页的“全集”分类按字段类型从上到下单列排列,每个组件间距 40px,超出预览画布时在画布内滚动查看',
2007
2014
  '【字段配置】items 数组每项支持 id、label、type、required、labelPosition、middleHelpText、helpText、error、errorText、disabled、value/defaultValue/onChange 等字段;当字段命中 AI 推荐语义时,可继续透传 aiSuggestion、aiSuggestions、onAdoptSuggestion、onRefreshAiSuggestions 到 Input / Select / TextArea',
@@ -2081,7 +2088,8 @@ export const COMPONENTS = [
2081
2088
  '【AI推荐误判边界】普通 placeholder、helpText、默认值、静态示例文案不等于 AI 推荐能力;只有明确存在“推荐/建议/AI辅助填写”语义时,才默认展示 Input 的 AI 推荐区。',
2082
2089
  '【刷新推荐】当存在 AI 推荐时,Input 默认应支持真实刷新;未接业务服务时,组件内部仍需轮换下一组建议,不允许点了刷新但内容不变。若传入 onRefreshAiSuggestions,应在本地刷新闭环之外继续向外通知;按钮位于清空按钮右侧,tooltip 固定为“刷新推荐”,icon 使用 `ai-fill-3` 渐变 token',
2083
2090
  '【采纳回调】点击任一推荐内容时都会触发 onAdoptSuggestion(suggestion),并默认把推荐值真实写入输入框;受控模式下需配合 value + onChange 完成外部值更新',
2084
- '【宽度】默认随父容器满宽(避免板块内输入框不撑满);需要固定宽(如筛选栏)时,用外层 style.width(240/200/160)或传 `style={{ "--size-input-width": "240px" }}` 控制',
2091
+ '【宽度·正文表单】Input 默认随父容器满宽;在 Form 正文、卡片内纵向表单、配置表单、详情编辑、抽屉/弹窗正文中,Input 必须跟随字段列动态撑满,并与同组 Select / TextArea / InputNumber 等字段宽度一致。不要把搜索框 240px 规则套到正文表单字段。',
2092
+ '【宽度·辅助查询】只有列表筛选栏、工具条、AI 首页模板搜索、Tabs 右侧搜索等横向辅助查询入口,才使用固定中等宽度:默认 240px,最小 200px,最大不超过 320px;可用外层 style.width 或 `style={{ "--size-input-width": "240px" }}` 控制。此类场景禁止写 `flex-1` / `w-full` 让搜索框撑满整行或撑满 Tabs 右侧全部剩余空间。',
2085
2093
  '【尺寸】仅保留 MD 一种尺寸,高度固定 36px;与 Button md 高度一致',
2086
2094
  '【页面默认】页面主表单、筛选栏、白卡内录入区、侧栏与弹窗正文中的单行 Input 默认使用 36px 高度,不再提供 size 配置',
2087
2095
  ],
@@ -2168,7 +2176,7 @@ export const COMPONENTS = [
2168
2176
  '【自动增高】resize="vertical" 时高度随正文 scrollHeight 变化;禁止原生拖拽(resize-none)。resize="none" 时高度锁定在 minRows 对应高度,超出部分框内滚动',
2169
2177
  '【撑满父容器】fillHeight=true 时,TextArea 外层、字段框和正文区都会进入 `flex-1 min-h-0` 撑满模式,并强制正文区内部滚动;父容器若缺 `flex-1 min-h-0 overflow-hidden`,撑满不会生效。',
2170
2178
  '【字数】maxLength 可选,不传则不限制字数、不施加原生 maxLength;showCount=true 且无 maxLength 时仅展示已输入字数;有 maxLength 时展示「当前/上限」,enforceMaxLength=false 可超出仅标红',
2171
- '【宽度】默认随父容器满宽;筛选栏/工具条等并排场景才使用固定宽(240/200/160)。主编辑区配合 fillHeight 可撑满高度,宽度仍默认满宽',
2179
+ '【宽度】默认随父容器满宽;在 Form 正文、卡片内纵向表单、配置表单、详情编辑、抽屉/弹窗正文中,TextArea 必须跟随字段列动态撑满,并与同组 Input / Select 等字段宽度一致。筛选栏/工具条等并排辅助查询场景才允许固定宽(240/200/160)。主编辑区配合 fillHeight 可撑满高度,宽度仍默认满宽',
2172
2180
  '【AI推荐】支持单条 aiSuggestion 和多条 aiSuggestions;推荐区固定展示在 TextArea 下方,并支持真实点击采纳与刷新推荐。未接业务服务时,组件内部也必须能够轮换下一组草稿建议,不能只展示静态文案',
2173
2181
  '【AI推荐采纳】点击推荐草稿后,TextArea 默认应真实写入推荐内容,并触发 onAdoptSuggestion(suggestion);若传入 onRefreshAiSuggestions,应在本地轮换草稿之外继续对外通知',
2174
2182
  '【详情预览·侧栏默认】与 _preview.panelDefaults 一致:内容「空」、超长计数「关」、可用性「默认可用」、默认高度「3 行」、校验态「默认」、高度适配「纵向拉伸」',
@@ -2246,7 +2254,7 @@ export const COMPONENTS = [
2246
2254
  _preview: INPUT_NUMBER_PREVIEW,
2247
2255
  rules: [
2248
2256
  '【⛔ AI 强制使用场景】所有数字类参数字段必须使用 InputNumber,禁止用 `<input type="number">` 或 Input 组件代替。典型场景:Temperature(0~2 精度 0.01)、Top-P(0~1 精度 0.01)、Max Tokens(整数步进)、学习率、权重、阈值等一切数值配置项。原生 input 会产生黑色边框并丢失步进、范围约束、精度控制能力',
2249
- '【组件定位】InputNumber 是数字输入原子组件,输入框视觉必须复用 Input:表单正文默认随字段列满宽,MD 高 36px、圆角 8px、白底灰边框、聚焦 1px 绿色描边;仅筛选栏/工具条/详情预览示意可由父容器限制为 160/200/240/300px',
2257
+ '【组件定位】InputNumber 是数字输入原子组件,输入框视觉必须复用 Input:表单正文、卡片内纵向表单、配置表单、详情编辑和抽屉/弹窗正文默认随字段列满宽,并与同组 Input / Select 等字段宽度一致;MD 高 36px、圆角 8px、白底灰边框、聚焦 1px 绿色描边。仅筛选栏/工具条/详情预览示意可由父容器限制为 160/200/240/300px',
2250
2258
  '【按钮布局】默认 outer 外置步进按钮:位于输入框右侧,宽 16px、高 36px,与输入框间距 4px,上下两个箭头图标均为 8×8px',
2251
2259
  '【内置按钮】innerButtons=true 时步进按钮内嵌在输入框右侧,默认隐藏,仅在容器 hover 或 focus-within 时显示,输入内容需预留右侧空间',
2252
2260
  '【禁用态】disabled 时输入框与步进按钮同步进入浅灰底、禁用文字和 0.6 透明度;按钮不可点击',
@@ -2333,7 +2341,8 @@ export const COMPONENTS = [
2333
2341
  '【关闭】点击触发器、选项或外部 mousedown(触发器与面板均排除)关闭',
2334
2342
  '【键盘】Enter/Space 展开或选中高亮项;↑/↓ 移动高亮;Esc 关闭;Tab 失焦关闭',
2335
2343
  '【无障碍】触发器 role="combobox" aria-expanded;列表 role="listbox" 与 role="option"',
2336
- '【宽度】触发器为 w-full min-w-0 max-w-full,表单正文默认随字段列满宽;筛选栏、工具条或详情预览示意才允许由父容器限制宽度,禁止在业务表单正文把 Select 固定为 300px 导致与 Input / TextArea 不对齐',
2344
+ '【宽度·正文表单】触发器为 w-full min-w-0 max-w-full;在 Form 正文、卡片内纵向表单、配置表单、详情编辑、抽屉/弹窗正文中,Select 必须默认随字段列满宽,并与同组 Input / TextArea / DatePicker 等字段宽度一致。禁止在业务表单正文把 Select 固定为 240px / 300px 导致与其它字段不对齐。',
2345
+ '【宽度·辅助查询】仅筛选栏、工具条、紧凑查询区等横向辅助查询入口允许由父容器限制为中窄宽,常用 160-240px;该限制不作用于正文表单字段。',
2337
2346
  '【右侧图标】下拉箭头、清除按钮与自定义 indicator 均固定为 16×16px',
2338
2347
  '【尺寸】仅保留 MD 一种尺寸,触发器高度固定 36px;与 Input 保持一致',
2339
2348
  ],
@@ -2375,11 +2384,12 @@ export const COMPONENTS = [
2375
2384
  element: 'div',
2376
2385
  category: 'basic',
2377
2386
  description:
2378
- '互斥单选:同一组内只能选一个值。适合选项少、需要并排或纵列展示全部标签的表单场景。多选请用 CheckboxGroup;布尔即时开关请用 Switch;选项很多请用 Select。',
2387
+ '互斥单选:同一组内只能选一个值。支持基础样式、带圆点单选卡片、单选卡片 3 类形态,适合选项少且需要平铺比较的表单场景。多选请用 CheckboxGroup;布尔即时开关请用 Switch;选项很多请用 Select。',
2379
2388
  componentFile: './components/Radio.jsx',
2380
2389
  tokensFile: './components/Radio.tokens.js',
2381
2390
  props: [
2382
2391
  { name: 'variant', type: 'enum', options: ['brand', 'black'], default: 'brand' },
2392
+ { name: 'styleType', type: 'enum', options: ['basic', 'card', 'pureCard'], default: 'basic' },
2383
2393
  { name: 'layout', type: 'enum', options: ['vertical', 'horizontal'], default: 'vertical' },
2384
2394
  { name: 'disabled', type: 'boolean', default: false },
2385
2395
  { name: 'value', type: 'string' },
@@ -2389,6 +2399,7 @@ export const COMPONENTS = [
2389
2399
  ],
2390
2400
  labels: {
2391
2401
  variant: { brand: '品牌绿', black: '主色黑' },
2402
+ styleType: { basic: '基础样式', card: '带圆点单选卡片', pureCard: '单选卡片' },
2392
2403
  layout: { vertical: '纵向', horizontal: '横向' },
2393
2404
  },
2394
2405
  _preview: RADIO_PREVIEW,
@@ -2399,7 +2410,11 @@ export const COMPONENTS = [
2399
2410
  '【受控】RadioGroup 使用 value + onChange(nextValue, event);非受控用 defaultValue',
2400
2411
  '【禁用】RadioGroup disabled 禁用整组;Radio 可单独 disabled',
2401
2412
  '【变体】brand 为选中实心主色 + 白点;black 为深灰主色 + 白点',
2402
- '【布局】vertical 组间距 12px;horizontal 组间距 16px(对齐 Semi radioGroup)',
2413
+ '【样式分类】styleType=basic 为基础样式;styleType=card 为带圆点单选卡片;styleType=pureCard 为单选卡片。3 类都支持 variant=brand/black',
2414
+ '【卡片样式】带圆点单选卡片保留左侧 16px 圆点指示器,适合需要强调“单选控件感”的配置项;单选卡片隐藏圆点,仅用卡片描边/浅底表达选中,适合更轻量的枚举切换',
2415
+ '【卡片文案】card / pureCard 的文案默认左对齐并向右撑满;当组件宽度受限时单行省略,hover 文案区域通过 Tooltip 展示完整内容',
2416
+ '【选中强调】card / pureCard 选中后文案字重自动变粗;未选中保持常规字重。基础样式不跟随这条规则变化',
2417
+ '【布局】vertical 组间距 12px;horizontal 组间距 16px,全部选项优先横向排列,整组容器宽度不够时自动换行到下一行',
2403
2418
  '【样式来源】HIUIpackage/es/components/radio-group 为 Semi 封装,视觉以 umd/view.css 为准',
2404
2419
  '【表单】未传 name 时自动用稳定 id,同一组内各 input 共享 name',
2405
2420
  '【无障碍】使用原生 type="radio" 与 radiogroup 容器;聚焦可见环在指示圈上',
@@ -2407,6 +2422,8 @@ export const COMPONENTS = [
2407
2422
  ],
2408
2423
  examples: [
2409
2424
  { label: '基础', code: '<RadioGroup defaultValue="b" onChange={(v) => {}}><Radio value="a">选项 A</Radio><Radio value="b">选项 B</Radio></RadioGroup>' },
2425
+ { label: '带圆点单选卡片', code: '<RadioGroup styleType="card" layout="horizontal" defaultValue="a"><Radio value="a">单选框标题</Radio><Radio value="b">单选框标题</Radio></RadioGroup>' },
2426
+ { label: '单选卡片', code: '<RadioGroup styleType="pureCard" layout="horizontal" defaultValue="a"><Radio value="a">单选框标题</Radio><Radio value="b">单选框标题</Radio></RadioGroup>' },
2410
2427
  { label: '横向', code: '<RadioGroup layout="horizontal" defaultValue="a"><Radio value="a">月付</Radio><Radio value="b">年付</Radio></RadioGroup>' },
2411
2428
  { label: '主色黑', code: '<RadioGroup variant="black" defaultValue="1"><Radio value="1">是</Radio><Radio value="0">否</Radio></RadioGroup>' },
2412
2429
  { label: '整组禁用', code: '<RadioGroup disabled defaultValue="a"><Radio value="a">A</Radio><Radio value="b">B</Radio></RadioGroup>' },
@@ -2823,7 +2840,8 @@ export const COMPONENTS = [
2823
2840
  '【场景·daterange / datetimerange】表格/列表顶部时间范围筛选、统计报表数据周期选择、合同有效期区间',
2824
2841
  '【禁忌】不要用 type="date" 代替 type="datetime"(会丢失时分信息);不要把两个 DatePicker 手拼成范围选择(直接用 type="daterange");不要在极紧凑行内(宽度 < 200px)使用,面板最小宽度 280px',
2825
2842
  '【类型说明】date / datetime → 单月面板单值选择;daterange / datetimerange → 双月面板范围选择,范围中间区间浅色连续高亮',
2826
- '【宽度】表单正文默认随字段列满宽;筛选栏、工具条或详情预览示意才允许由父容器限制宽度(date / datetime / daterange 常用 300px,datetimerange 常用 400px)。禁止在业务表单正文把 DatePicker 固定为 300px 导致与 Input / Select 不对齐',
2843
+ '【宽度·正文表单】表单正文、卡片内纵向表单、配置表单、详情编辑和抽屉/弹窗正文默认随字段列满宽,并与同组 Input / Select / TimePicker 等字段宽度一致;禁止在业务表单正文把 DatePicker 固定为 300px 导致字段不对齐。',
2844
+ '【宽度·辅助查询】仅筛选栏、工具条或详情预览示意允许由父容器限制宽度(date / datetime / daterange 常用 240-300px,datetimerange 常用 320-400px)。该限制不作用于正文表单字段。',
2827
2845
  '【面板行为】单值点击即选中并关闭;范围需点击两次确定起止后自动关闭',
2828
2846
  '【时间栏】datetime / datetimerange 面板底部固定显示 52px 日期/时间信息栏',
2829
2847
  '【组合·表单】配 Form 使用时包在 Form.Item(type="date-picker")里;时间范围筛选常配合 Table 顶部工具栏 + "查询"Button 使用',
@@ -2892,7 +2910,8 @@ export const COMPONENTS = [
2892
2910
  rules: [
2893
2911
  FONT_WEIGHT_RUNTIME_RULE,
2894
2912
  '【类型】time 为单时间输入框;timerange 为时间范围输入框',
2895
- '【触发器】视觉对齐 Figma / DatePicker:表单正文默认随字段列满宽,详情预览示意可用 300px36px 高、白底、1px 默认描边、8px 圆角,内容默认左对齐,右侧时钟图标固定为 16×16px',
2913
+ '【触发器】视觉对齐 Figma / DatePicker:表单正文、卡片内纵向表单、配置表单、详情编辑和抽屉/弹窗正文默认随字段列满宽,并与同组 Input / Select / DatePicker 等字段宽度一致;详情预览示意可用 300px36px 高、白底、1px 默认描边、8px 圆角,内容默认左对齐,右侧时钟图标固定为 16×16px',
2914
+ '【宽度·辅助查询】仅筛选栏、工具条、紧凑查询区等横向辅助查询入口允许由父容器限制为中窄宽,time 常用 160-200px,timerange 常用 200-240px;该限制不作用于正文表单字段。',
2896
2915
  '【面板】点击 Trigger 展开浮层;面板默认与 Trigger 等宽,最小宽度 150px;单时间面板高 252px,无标题;时间范围面板高 304px,顶部显示开始/结束标题',
2897
2916
  '【固定选择区】面板中间固定 36px 高浅绿选择区,时/分列按面板宽度等分并居中对齐;timerange 使用同一条横跨四列的固定选择区,四列都支持真实滚动选择',
2898
2917
  '【选择】点击任一时间项会滚动到中间选择区;滚动时/分列时实时同步选择区内最近项,停止后自动吸附',
@@ -3078,7 +3097,8 @@ export const COMPONENTS = [
3078
3097
  },
3079
3098
  _preview: TAGINPUT_PREVIEW,
3080
3099
  rules: [
3081
- '【视觉】容器对齐 Figma taginput:表单正文默认随字段列满宽,详情预览示意可用 300px36px 高、白底、1px 默认描边、8px 圆角,空态显示“请输入”',
3100
+ '【视觉】容器对齐 Figma taginput:表单正文、卡片内纵向表单、配置表单、详情编辑和抽屉/弹窗正文默认随字段列满宽,并与同组 Input / Select 等字段宽度一致;详情预览示意可用 300px36px 高、白底、1px 默认描边、8px 圆角,空态显示“请输入”',
3101
+ '【宽度·辅助查询】仅筛选栏、工具条、紧凑查询区等横向辅助查询入口允许由父容器限制为中窄宽,常用 200-240px;该限制不作用于正文表单字段。',
3082
3102
  '【Tag 复用】已选项、+N 折叠项、Tooltip 内省略项、隐藏测量项都必须使用平台现有 Tag 组件;TagInput 不自绘任何标签视觉',
3083
3103
  '【宽度自适应】通过 ResizeObserver 监听容器宽度,按真实 Tag 宽度计算可见数量;宽度变窄自动折叠,变宽自动展开',
3084
3104
  '【溢出折叠】可见区域放不下的标签折叠为 +N,+N 固定在末尾并以 Tag 渲染;悬浮 +N 使用白底 tone="light" Tooltip 展示被省略的所有 Tag,该白底样式仅用于 TagInput 更多标签场景',
@@ -11,7 +11,7 @@ import Icon from '../components/Icon';
11
11
  *
12
12
  * 右侧内容区从上到下:
13
13
  * 1. Hero 区:大标题 + 副标题 + 居中 ChatInput + 快捷建议 chip
14
- * 2. 筛选行(左 Search + 右胶囊 Tab),左右 40px 边距
14
+ * 2. 筛选行(左胶囊 Tab + 右固定宽 Search),左右 40px 边距
15
15
  * 3. 自适应卡片网格(auto-fill minmax(260px,1fr)),左右 40px 边距
16
16
  */
17
17
 
@@ -23,6 +23,13 @@ const FILTER_TABS = [
23
23
  { label: '数据完善', id: '数据完善' },
24
24
  ];
25
25
 
26
+ const FILTER_SEARCH_WRAP_STYLE = {
27
+ flex: '0 0 240px',
28
+ width: '240px',
29
+ minWidth: '240px',
30
+ maxWidth: '240px',
31
+ };
32
+
26
33
  /* ── 示例卡片数据(20 张,category 对应 Tab id)── */
27
34
  const MOCK_CARDS = [
28
35
  {
@@ -197,16 +204,16 @@ export default function ChatHomePagePattern() {
197
204
  />
198
205
  </div>
199
206
 
200
- {/* 右侧内容区 */}
201
- <div className="flex flex-1 min-w-0 flex-col min-h-0 overflow-hidden">
207
+ {/* 右侧内容区:整体滚动,Hero / 筛选 / 卡片一起滑动 */}
208
+ <div className="flex flex-1 min-w-0 flex-col min-h-0 overflow-y-auto overflow-x-hidden">
202
209
 
203
210
  {/* ① Hero 区 */}
204
211
  <div
205
212
  className="flex flex-col items-center shrink-0"
206
- style={{ padding: '120px 40px 80px' }}
213
+ style={{ padding: '84px 40px 80px' }}
207
214
  >
208
215
  <h1
209
- className="m-0 text-center text-3xl [font-weight:var(--font-semibold)] leading-9"
216
+ className="m-0 text-center text-4xl [font-weight:var(--font-semibold)] leading-10"
210
217
  style={{
211
218
  color: 'var(--foreground, #0F1C35)',
212
219
  marginBottom: '6px',
@@ -219,7 +226,7 @@ export default function ChatHomePagePattern() {
219
226
  className="m-0 text-sm text-center"
220
227
  style={{
221
228
  color: 'var(--foreground-muted, rgba(15,28,53,0.45))',
222
- marginBottom: '24px',
229
+ marginBottom: '48px',
223
230
  }}
224
231
  >
225
232
  直接描述你的需求,或从下方模板快速开始
@@ -230,32 +237,34 @@ export default function ChatHomePagePattern() {
230
237
  </div>
231
238
  </div>
232
239
 
233
- {/* ② 筛选行:左侧胶囊 Tab + 右侧搜索,左右 40px */}
240
+ {/* ② 筛选行:左侧胶囊 Tab + 右侧固定宽搜索,左右 40px */}
234
241
  <div
235
- className="flex min-w-0 shrink-0 flex-wrap items-center justify-between gap-4"
242
+ className="flex min-w-0 shrink-0 items-center gap-4"
236
243
  style={{ padding: '16px 40px' }}
237
244
  >
238
- <Tabs
239
- variant="pill"
240
- size="sm"
241
- items={FILTER_TABS}
242
- defaultIndex={0}
243
- onChange={(index) => setActiveTabIndex(index)}
244
- />
245
- <Input
246
- placeholder="搜索标题、描述"
247
- prefix={<Icon name="search-md-stroked" size="sm" />}
248
- allowClear
249
- value={searchValue}
250
- onChange={(e) => setSearchValue(e.target.value)}
251
- className="max-w-full"
252
- style={{ flex: '0 1 240px', width: '240px', minWidth: '200px', '--size-input-width': '100%' }}
253
- />
245
+ <div className="flex shrink-0">
246
+ <Tabs
247
+ variant="pill"
248
+ size="sm"
249
+ items={FILTER_TABS}
250
+ defaultIndex={0}
251
+ onChange={(index) => setActiveTabIndex(index)}
252
+ />
253
+ </div>
254
+ <div className="ml-auto shrink-0" style={FILTER_SEARCH_WRAP_STYLE}>
255
+ <Input
256
+ placeholder="搜索标题、描述"
257
+ prefix={<Icon name="search-md-stroked" size="sm" />}
258
+ allowClear
259
+ value={searchValue}
260
+ onChange={(e) => setSearchValue(e.target.value)}
261
+ />
262
+ </div>
254
263
  </div>
255
264
 
256
- {/* ③ 卡片网格(自适应列,可滚动,左右 40px) */}
265
+ {/* ③ 卡片网格(随页面整体滚动,左右 40px) */}
257
266
  <div
258
- className="flex-1 min-h-0 overflow-y-auto"
267
+ className="shrink-0"
259
268
  style={{ padding: '0 40px 32px' }}
260
269
  >
261
270
  <div