@tfdesign/b-end 1.0.4
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.
- package/AI_READ_FIRST.md +131 -0
- package/LICENSE +21 -0
- package/README.md +353 -0
- package/package.json +67 -0
- package/scripts/check-tfds-contract.mjs +334 -0
- package/scripts/check-tfds-integration.mjs +263 -0
- package/scripts/postinstall-cursor-skill.mjs +382 -0
- package/scripts/setup.mjs +520 -0
- package/skills/tfds/CHECKLIST.md +205 -0
- package/skills/tfds/COMMON_FAILURES.md +238 -0
- package/skills/tfds/DESIGN_PRINCIPLES.md +477 -0
- package/skills/tfds/GLOBAL_DESIGN_RULES.md +636 -0
- package/skills/tfds/LAYOUT_RECIPES.md +140 -0
- package/skills/tfds/LAYOUT_RULES.md +1355 -0
- package/skills/tfds/PAGE_ARCHETYPES.md +201 -0
- package/skills/tfds/SKILL.md +188 -0
- package/skills/tfds/components.index.json +7305 -0
- package/skills/tfds/components.summary.json +1809 -0
- package/src/_b_end_runtime/components/AiSuggestionShared.jsx +166 -0
- package/src/_b_end_runtime/components/Avatar.jsx +325 -0
- package/src/_b_end_runtime/components/Avatar.tokens.js +76 -0
- package/src/_b_end_runtime/components/AvatarGridPreview.jsx +56 -0
- package/src/_b_end_runtime/components/AvatarGroup.jsx +80 -0
- package/src/_b_end_runtime/components/AvatarGroup.tokens.js +28 -0
- package/src/_b_end_runtime/components/Button.jsx +144 -0
- package/src/_b_end_runtime/components/Button.tokens.js +90 -0
- package/src/_b_end_runtime/components/Card.jsx +460 -0
- package/src/_b_end_runtime/components/Card.tokens.js +124 -0
- package/src/_b_end_runtime/components/CardPreview.jsx +51 -0
- package/src/_b_end_runtime/components/ChatBubble.jsx +384 -0
- package/src/_b_end_runtime/components/ChatBubble.tokens.js +60 -0
- package/src/_b_end_runtime/components/ChatBubblePreview.jsx +129 -0
- package/src/_b_end_runtime/components/ChatInput.jsx +1399 -0
- package/src/_b_end_runtime/components/ChatInput.tokens.js +75 -0
- package/src/_b_end_runtime/components/ChatMessage.jsx +2215 -0
- package/src/_b_end_runtime/components/ChatMessage.tokens.js +257 -0
- package/src/_b_end_runtime/components/ChatMessagePreview.jsx +388 -0
- package/src/_b_end_runtime/components/Checkbox.jsx +317 -0
- package/src/_b_end_runtime/components/Checkbox.tokens.js +59 -0
- package/src/_b_end_runtime/components/ConversationList.jsx +1264 -0
- package/src/_b_end_runtime/components/ConversationList.tokens.js +135 -0
- package/src/_b_end_runtime/components/ConversationListPreview.jsx +108 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.jsx +324 -0
- package/src/_b_end_runtime/components/CustomerServiceWorkspaceFrame.tokens.js +69 -0
- package/src/_b_end_runtime/components/DatePicker.jsx +739 -0
- package/src/_b_end_runtime/components/DatePicker.tokens.js +99 -0
- package/src/_b_end_runtime/components/Empty.jsx +141 -0
- package/src/_b_end_runtime/components/Empty.tokens.js +40 -0
- package/src/_b_end_runtime/components/Form.jsx +609 -0
- package/src/_b_end_runtime/components/Form.tokens.js +77 -0
- package/src/_b_end_runtime/components/FormFieldStack.jsx +123 -0
- package/src/_b_end_runtime/components/FormFieldStack.tokens.js +12 -0
- package/src/_b_end_runtime/components/FormTitle.jsx +119 -0
- package/src/_b_end_runtime/components/FormTitle.tokens.js +87 -0
- package/src/_b_end_runtime/components/FullScreenPage.jsx +97 -0
- package/src/_b_end_runtime/components/FullScreenPage.tokens.js +19 -0
- package/src/_b_end_runtime/components/Icon.jsx +172 -0
- package/src/_b_end_runtime/components/Icon.tokens.js +26 -0
- package/src/_b_end_runtime/components/IconGridPreview.jsx +277 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.jsx +620 -0
- package/src/_b_end_runtime/components/InfoDisplayPanel.tokens.js +71 -0
- package/src/_b_end_runtime/components/InfoDisplayPanelPreview.jsx +133 -0
- package/src/_b_end_runtime/components/Input.jsx +258 -0
- package/src/_b_end_runtime/components/Input.tokens.js +68 -0
- package/src/_b_end_runtime/components/InputNumber.jsx +242 -0
- package/src/_b_end_runtime/components/InputNumber.tokens.js +55 -0
- package/src/_b_end_runtime/components/Modal.jsx +155 -0
- package/src/_b_end_runtime/components/Modal.tokens.js +73 -0
- package/src/_b_end_runtime/components/NavBar.jsx +842 -0
- package/src/_b_end_runtime/components/NavBar.tokens.js +97 -0
- package/src/_b_end_runtime/components/NavBarPreview.jsx +11 -0
- package/src/_b_end_runtime/components/Radio.jsx +227 -0
- package/src/_b_end_runtime/components/Radio.tokens.js +59 -0
- package/src/_b_end_runtime/components/Select.jsx +766 -0
- package/src/_b_end_runtime/components/Select.tokens.js +99 -0
- package/src/_b_end_runtime/components/Sheet.jsx +132 -0
- package/src/_b_end_runtime/components/Sheet.tokens.js +61 -0
- package/src/_b_end_runtime/components/Slider.jsx +346 -0
- package/src/_b_end_runtime/components/Slider.tokens.js +47 -0
- package/src/_b_end_runtime/components/Switch.jsx +124 -0
- package/src/_b_end_runtime/components/Switch.tokens.js +38 -0
- package/src/_b_end_runtime/components/Table.jsx +1338 -0
- package/src/_b_end_runtime/components/Table.tokens.js +147 -0
- package/src/_b_end_runtime/components/TablePreview.jsx +599 -0
- package/src/_b_end_runtime/components/Tabs.jsx +149 -0
- package/src/_b_end_runtime/components/Tabs.tokens.js +102 -0
- package/src/_b_end_runtime/components/Tag.jsx +199 -0
- package/src/_b_end_runtime/components/Tag.tokens.js +171 -0
- package/src/_b_end_runtime/components/TagBar.jsx +1134 -0
- package/src/_b_end_runtime/components/TagBar.tokens.js +75 -0
- package/src/_b_end_runtime/components/TagGridPreview.jsx +23 -0
- package/src/_b_end_runtime/components/TagInput.jsx +382 -0
- package/src/_b_end_runtime/components/TagInput.tokens.js +52 -0
- package/src/_b_end_runtime/components/TextArea.jsx +363 -0
- package/src/_b_end_runtime/components/TextArea.tokens.js +65 -0
- package/src/_b_end_runtime/components/TimePicker.jsx +444 -0
- package/src/_b_end_runtime/components/TimePicker.tokens.js +77 -0
- package/src/_b_end_runtime/components/Toast.jsx +120 -0
- package/src/_b_end_runtime/components/Toast.tokens.js +146 -0
- package/src/_b_end_runtime/components/Tooltip.jsx +282 -0
- package/src/_b_end_runtime/components/Tooltip.tokens.js +48 -0
- package/src/_b_end_runtime/components/TooltipPreview.jsx +50 -0
- package/src/_b_end_runtime/components/Upload.jsx +455 -0
- package/src/_b_end_runtime/components/Upload.tokens.js +47 -0
- package/src/_b_end_runtime/components/avatar-assets/avatar-default.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-1.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-2.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-3.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-4.png +0 -0
- package/src/_b_end_runtime/components/avatar-group-assets/avatar-group-5.png +0 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-1.svg +40 -0
- package/src/_b_end_runtime/components/empty-assets/administrator-2.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/construction.svg +33 -0
- package/src/_b_end_runtime/components/empty-assets/failure.svg +49 -0
- package/src/_b_end_runtime/components/empty-assets/idle.svg +34 -0
- package/src/_b_end_runtime/components/empty-assets/no-access.svg +36 -0
- package/src/_b_end_runtime/components/empty-assets/no-content.svg +77 -0
- package/src/_b_end_runtime/components/empty-assets/no-result.svg +61 -0
- package/src/_b_end_runtime/components/empty-assets/not-found.svg +46 -0
- package/src/_b_end_runtime/components/empty-assets/success.svg +38 -0
- package/src/_b_end_runtime/components/file-type-assets/batch-report.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/catcat.svg +21 -0
- package/src/_b_end_runtime/components/file-type-assets/code.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/conversation.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/document.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-card.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu-sheet.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/feishu.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/image.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/index.js +105 -0
- package/src/_b_end_runtime/components/file-type-assets/knowledge.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pdf.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/pe.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/strategy.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/table.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/webpage.png +0 -0
- package/src/_b_end_runtime/components/file-type-assets/xmind.png +0 -0
- package/src/_b_end_runtime/components/icons/icon-data.js +12496 -0
- package/src/_b_end_runtime/components/nav-bar-assets/bytehi-logo-mark.svg +21 -0
- package/src/_b_end_runtime/components/table-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-assets/icon-chevron-down.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/avatar.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/button.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/checkbox.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon-chevron-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/icon.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-handle.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/semi-icons-tree-triangle-right.png +0 -0
- package/src/_b_end_runtime/components/table-cell-assets/switch.png +0 -0
- package/src/_b_end_runtime/components/tagShared.js +3 -0
- package/src/_b_end_runtime/components/team-avatar-assets/chengcheng-murphy.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/duan-ran.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/guo-zhezhi.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/li-siru.png +0 -0
- package/src/_b_end_runtime/components/team-avatar-assets/liu-delin.png +0 -0
- package/src/_b_end_runtime/components.js +3499 -0
- package/src/_b_end_runtime/index.js +9 -0
- package/src/_b_end_runtime/page-patterns/BasePageFramePattern.jsx +395 -0
- package/src/_b_end_runtime/page-patterns/ChatConversationPattern.jsx +989 -0
- package/src/_b_end_runtime/page-patterns/ChatHomePagePattern.jsx +281 -0
- package/src/_b_end_runtime/page-patterns/CopilotPagePattern.jsx +380 -0
- package/src/_b_end_runtime/page-patterns/CustomerServiceWorkspaceFramePattern.jsx +392 -0
- package/src/_b_end_runtime/page-patterns/IMConversationPattern.jsx +590 -0
- package/src/_b_end_runtime/page-patterns/McpManagementPage.jsx +237 -0
- package/src/_b_end_runtime/page-patterns/StrategyListPage.jsx +189 -0
- package/src/_b_end_runtime/page-patterns/TabTopBarListPage.jsx +594 -0
- package/src/_b_end_runtime/page-patterns/VariableManagementPage.jsx +87 -0
- package/src/_b_end_runtime/page-patterns/pageListShared.jsx +177 -0
- package/src/_b_end_runtime/patterns.js +428 -0
- package/src/_b_end_runtime/preview-registry.jsx +4719 -0
- package/src/_b_end_runtime/teamMembers.js +56 -0
- package/src/_b_end_runtime/tokens.js +500 -0
- package/src/index.d.ts +1073 -0
- package/src/index.js +52 -0
- package/theme.css +350 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import Form, { FORM_SAMPLE_ITEMS } from './Form';
|
|
2
|
+
|
|
3
|
+
const FORM_COUNT = 5;
|
|
4
|
+
const FORM_SELECT_SAMPLE = FORM_SAMPLE_ITEMS.find((item) => item.type === 'select');
|
|
5
|
+
|
|
6
|
+
function cloneValue(value) {
|
|
7
|
+
if (Array.isArray(value)) return value.map(cloneValue);
|
|
8
|
+
if (value && typeof value === 'object') return { ...value };
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildFormItems(prefix) {
|
|
13
|
+
const item = FORM_SELECT_SAMPLE || {
|
|
14
|
+
id: 'region',
|
|
15
|
+
label: '字段标题',
|
|
16
|
+
type: 'select',
|
|
17
|
+
placeholder: '请选择',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return [{
|
|
21
|
+
...item,
|
|
22
|
+
id: `${prefix}-${item.id}`,
|
|
23
|
+
fullWidth: true,
|
|
24
|
+
value: cloneValue(item.value),
|
|
25
|
+
defaultValue: cloneValue(item.defaultValue),
|
|
26
|
+
options: cloneValue(item.options),
|
|
27
|
+
selectOptions: cloneValue(item.selectOptions),
|
|
28
|
+
}];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 自上而下:2×input、2×select、2×radio、2×checkbox、3×textarea(单 Form 内纵向) */
|
|
32
|
+
function buildMixedFormItems() {
|
|
33
|
+
const inputTpl = FORM_SAMPLE_ITEMS.find((i) => i.type === 'input');
|
|
34
|
+
const selectTpl = FORM_SAMPLE_ITEMS.find((i) => i.type === 'select');
|
|
35
|
+
const radioTpl = FORM_SAMPLE_ITEMS.find((i) => i.type === 'radio');
|
|
36
|
+
const checkboxTpl = FORM_SAMPLE_ITEMS.find((i) => i.type === 'checkbox');
|
|
37
|
+
const textareaTpl = FORM_SAMPLE_ITEMS.find((i) => i.type === 'textarea');
|
|
38
|
+
|
|
39
|
+
const pick = (tpl, overrides = {}) => {
|
|
40
|
+
if (!tpl) return null;
|
|
41
|
+
const { defaultValue: overrideDefault, id, ...rest } = overrides;
|
|
42
|
+
const baseDefault = tpl.defaultValue;
|
|
43
|
+
const resolvedDefault = overrideDefault !== undefined ? overrideDefault : baseDefault;
|
|
44
|
+
return {
|
|
45
|
+
...tpl,
|
|
46
|
+
...rest,
|
|
47
|
+
id,
|
|
48
|
+
fullWidth: true,
|
|
49
|
+
value: cloneValue(tpl.value),
|
|
50
|
+
defaultValue: cloneValue(resolvedDefault),
|
|
51
|
+
options: cloneValue(tpl.options),
|
|
52
|
+
selectOptions: cloneValue(tpl.selectOptions),
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
pick(inputTpl, { id: 'mixed-input-1' }),
|
|
58
|
+
pick(inputTpl, { id: 'mixed-input-2' }),
|
|
59
|
+
pick(selectTpl, { id: 'mixed-select-1' }),
|
|
60
|
+
pick(selectTpl, { id: 'mixed-select-2' }),
|
|
61
|
+
pick(radioTpl, { id: 'mixed-radio-1', defaultValue: 'a' }),
|
|
62
|
+
pick(radioTpl, { id: 'mixed-radio-2', defaultValue: 'b' }),
|
|
63
|
+
pick(checkboxTpl, { id: 'mixed-checkbox-1', defaultValue: ['a'] }),
|
|
64
|
+
pick(checkboxTpl, { id: 'mixed-checkbox-2', defaultValue: ['b'] }),
|
|
65
|
+
pick(textareaTpl, { id: 'mixed-textarea-1' }),
|
|
66
|
+
pick(textareaTpl, { id: 'mixed-textarea-2' }),
|
|
67
|
+
pick(textareaTpl, { id: 'mixed-textarea-3' }),
|
|
68
|
+
].filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const MIXED_FORM_ITEMS = buildMixedFormItems();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* FormFieldStack — 纵向多段 Form 组合(无页级标题、无底栏、无独立卡片壳)
|
|
75
|
+
*
|
|
76
|
+
* - stackLayout=`select-stack`(默认):5 个 Form,每段 1 条 select
|
|
77
|
+
* - stackLayout=`mixed-fields`:1 个 Form,内含 2×input / 2×select / 2×radio / 2×checkbox / 3×textarea
|
|
78
|
+
*
|
|
79
|
+
* 根节点全宽,四周 p-6(24px)内边距,段与段之间 gap-6;背景与描边由宿主决定。
|
|
80
|
+
*/
|
|
81
|
+
export default function FormFieldStack({ className = '', stackLayout = 'select-stack' }) {
|
|
82
|
+
if (stackLayout === 'mixed-fields') {
|
|
83
|
+
return (
|
|
84
|
+
<div
|
|
85
|
+
className={[
|
|
86
|
+
'tfds-form-field-stack box-border flex w-full min-w-0 flex-col gap-6 self-stretch p-6',
|
|
87
|
+
className,
|
|
88
|
+
].filter(Boolean).join(' ')}
|
|
89
|
+
data-tfds-component="FormFieldStack"
|
|
90
|
+
>
|
|
91
|
+
<Form
|
|
92
|
+
key="mixed-form"
|
|
93
|
+
className="w-full min-w-0 !items-stretch"
|
|
94
|
+
items={MIXED_FORM_ITEMS}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const forms = Array.from({ length: FORM_COUNT }, (_, index) => {
|
|
101
|
+
const formKey = `form-section-${index + 1}`;
|
|
102
|
+
const items = buildFormItems(formKey);
|
|
103
|
+
return { formKey, items };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
className={[
|
|
109
|
+
'tfds-form-field-stack box-border flex w-full min-w-0 flex-col gap-6 self-stretch p-6',
|
|
110
|
+
className,
|
|
111
|
+
].filter(Boolean).join(' ')}
|
|
112
|
+
data-tfds-component="FormFieldStack"
|
|
113
|
+
>
|
|
114
|
+
{forms.map(({ formKey, items }) => (
|
|
115
|
+
<Form
|
|
116
|
+
key={formKey}
|
|
117
|
+
className="w-full min-w-0 !items-stretch"
|
|
118
|
+
items={items}
|
|
119
|
+
/>
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** FormFieldStack — Token 说明(透明布局壳;字段视觉走 Form.tokens) */
|
|
2
|
+
export const FORM_FIELD_STACK_TOKEN_MAP = {
|
|
3
|
+
留白与节奏: [
|
|
4
|
+
{ label: '壳层', cssProp: 'background, border', value: '不设背景与描边,随宿主容器' },
|
|
5
|
+
{ label: '四周内边距', cssProp: 'padding', token: '--spacing-6', value: '24px' },
|
|
6
|
+
{ label: 'Form 区块间距', cssProp: 'gap', token: '--spacing-6', value: '24px' },
|
|
7
|
+
{ label: 'Form 宽度', cssProp: 'width', value: '默认撑满宿主白色容器(w-full / self-stretch)' },
|
|
8
|
+
],
|
|
9
|
+
引用组件: [
|
|
10
|
+
{ label: '表单', cssProp: 'component', value: 'Form / vertical;业务按场景自由编排字段类型与数量;内置示意含 select-stack 与 mixed-fields(含 3 段 textarea)' },
|
|
11
|
+
],
|
|
12
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormTitle — B端表单区块标题组件 (Tailwind 内联)
|
|
3
|
+
*
|
|
4
|
+
* 用于页面级总标题、表单区块和白卡内容区标题的统一组件,内置 5 种设计稿变体:
|
|
5
|
+
* form / level-1 / level-2 / level-3 / card。
|
|
6
|
+
* 描述文案默认隐藏;仅在有板块介绍、场景说明或明确要求时通过 showDescription 显示。
|
|
7
|
+
*
|
|
8
|
+
* **AI / 页面生成:层级选型(必须按场景选 variant,禁止手写 h2/h3)**
|
|
9
|
+
* | 场景 | variant | 说明 |
|
|
10
|
+
* |------|---------|------|
|
|
11
|
+
* | 整体页面左上角总标题、工作台名、Playground 页面主标题(如 `Model Playground`) | `form` | 全页最高层级,20px 大字号,左侧**没有**绿色竖条;需要说明时描述在标题下方 |
|
|
12
|
+
* | 页面内一级板块 / 列表管理页单张主白卡顶栏主标题 | `level-1` | 左侧品牌色竖条 + 16px 标题;用于页面总标题之下的大卡片/一级内容块,不用于整页左上角总标题 |
|
|
13
|
+
* | `level-1` 之下的第一级分区 / 白卡内大模块标题 | `level-2` | 14px 竖条;有板块说明诉求时描述显示在标题**同行右侧** |
|
|
14
|
+
* | `level-2` 再下的子分组、折叠面板头、表格上方小区域标题 | `level-3` | 与 level-2 同阶字级,用于再嵌套一层时的视觉降级(仍带竖条) |
|
|
15
|
+
* | `Card` 组件标题区、统计/摘要条、表格卡片外框标题(弱化、**无竖条**、标题与描述同排) | `card` | 不要用于整页总标题或大卡片主标题 |
|
|
16
|
+
*
|
|
17
|
+
* 同一页面必须拉开标题层级:页面总标题用 `form`,卡片/板块标题再按 `level-1/2/3` 递进;
|
|
18
|
+
* ⛔ 禁止把整页所有左上角标题都统一写成同一个 variant。
|
|
19
|
+
*
|
|
20
|
+
* @prop {'form'|'level-1'|'level-2'|'level-3'|'card'} [variant='form'] — 标题形态
|
|
21
|
+
* @prop {string} [title] — 主标题;不传则用当前变体示例文案
|
|
22
|
+
* @prop {string} [description] — 描述;不传则用当前变体示例文案
|
|
23
|
+
* @prop {React.ReactNode} [titleSuffix] — 主标题右侧补充内容,如状态 Tag;紧跟 title 渲染,不进入 header 右侧操作区
|
|
24
|
+
* @prop {boolean} [showDescription=false] — 是否展示描述;默认隐藏
|
|
25
|
+
* @prop {string} [className=''] — 额外类名
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/* ── 变体 → 布局与排版(颜色走语义 Token 短类) ── */
|
|
29
|
+
// 字重使用显式 CSS 变量,避免 Tailwind v4 将 token 解析为 font-family token。
|
|
30
|
+
const VARIANT_CONFIG = {
|
|
31
|
+
form: {
|
|
32
|
+
root: 'inline-flex max-w-full min-w-0 flex-col items-start gap-1',
|
|
33
|
+
titleWrap: 'flex shrink-0 items-center gap-[6px]',
|
|
34
|
+
title: 'shrink-0 whitespace-nowrap text-xl [font-weight:var(--font-semibold)] leading-7 text-foreground',
|
|
35
|
+
description: 'whitespace-pre-wrap text-sm font-normal leading-5 text-foreground-muted',
|
|
36
|
+
indicator: null,
|
|
37
|
+
descriptionPlacement: 'bottom',
|
|
38
|
+
},
|
|
39
|
+
'level-1': {
|
|
40
|
+
root: 'inline-flex max-w-full min-w-0 flex-col items-start gap-1',
|
|
41
|
+
titleWrap: 'flex shrink-0 items-center gap-[6px]',
|
|
42
|
+
title: 'shrink-0 whitespace-nowrap text-base [font-weight:var(--font-semibold)] leading-[22px] text-foreground',
|
|
43
|
+
description: 'whitespace-pre-wrap text-sm font-normal leading-5 text-foreground-muted',
|
|
44
|
+
indicator: 'h-4 w-[4px] shrink-0 rounded-full bg-brand-500',
|
|
45
|
+
descriptionPlacement: 'bottom',
|
|
46
|
+
},
|
|
47
|
+
'level-2': {
|
|
48
|
+
root: 'flex max-w-full min-w-0 items-start gap-1',
|
|
49
|
+
titleWrap: 'flex min-w-0 flex-wrap items-center gap-x-[6px] gap-y-1',
|
|
50
|
+
title: 'shrink-0 whitespace-nowrap text-sm [font-weight:var(--font-semibold)] leading-5 text-foreground',
|
|
51
|
+
description: 'min-w-0 flex-1 whitespace-normal break-words text-sm font-normal leading-5 text-foreground-muted',
|
|
52
|
+
indicator: 'h-[14px] w-[4px] shrink-0 rounded-full bg-brand-500',
|
|
53
|
+
descriptionPlacement: 'right',
|
|
54
|
+
},
|
|
55
|
+
'level-3': {
|
|
56
|
+
root: 'flex max-w-full min-w-0 items-start gap-1',
|
|
57
|
+
titleWrap: 'flex min-w-0 flex-wrap items-center gap-x-[6px] gap-y-1',
|
|
58
|
+
title: 'shrink-0 whitespace-nowrap text-sm [font-weight:var(--font-semibold)] leading-5 text-foreground',
|
|
59
|
+
description: 'min-w-0 flex-1 whitespace-normal break-words text-sm font-normal leading-5 text-foreground-muted',
|
|
60
|
+
indicator: 'h-[14px] w-[4px] shrink-0 rounded-full bg-brand-500',
|
|
61
|
+
descriptionPlacement: 'right',
|
|
62
|
+
},
|
|
63
|
+
card: {
|
|
64
|
+
root: 'flex max-w-full min-w-0 gap-1',
|
|
65
|
+
titleWrap: 'flex min-w-0 flex-wrap items-center gap-x-[6px] gap-y-1',
|
|
66
|
+
title: 'shrink-0 whitespace-nowrap text-sm [font-weight:var(--font-semibold)] leading-5 text-foreground-secondary',
|
|
67
|
+
description: 'min-w-0 flex-1 whitespace-normal break-words text-sm font-normal leading-5 text-foreground-muted',
|
|
68
|
+
indicator: null,
|
|
69
|
+
descriptionPlacement: 'right',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const SAMPLE_COPY = {
|
|
74
|
+
form: { title: '表单标题', description: '一级标题描述' },
|
|
75
|
+
'level-1': { title: '一级标题', description: '一级标题描述' },
|
|
76
|
+
'level-2': { title: '二级标题', description: '二级标题描述' },
|
|
77
|
+
'level-3': { title: '三级标题', description: '二级标题描述' },
|
|
78
|
+
card: { title: '卡片标题', description: '二级标题描述' },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default function FormTitle({
|
|
82
|
+
variant = 'form',
|
|
83
|
+
title,
|
|
84
|
+
description,
|
|
85
|
+
titleSuffix = null,
|
|
86
|
+
showDescription = false,
|
|
87
|
+
className = '',
|
|
88
|
+
}) {
|
|
89
|
+
const resolvedVariant = VARIANT_CONFIG[variant] ? variant : 'form';
|
|
90
|
+
const config = VARIANT_CONFIG[resolvedVariant];
|
|
91
|
+
const sample = SAMPLE_COPY[resolvedVariant];
|
|
92
|
+
const resolvedTitle = title ?? sample.title;
|
|
93
|
+
const resolvedDescription = description ?? sample.description;
|
|
94
|
+
const shouldShowDescription = (
|
|
95
|
+
showDescription
|
|
96
|
+
&& typeof resolvedDescription === 'string'
|
|
97
|
+
&& resolvedDescription.trim().length > 0
|
|
98
|
+
);
|
|
99
|
+
const descriptionNode = shouldShowDescription ? (
|
|
100
|
+
<span className={config.description}>{resolvedDescription}</span>
|
|
101
|
+
) : null;
|
|
102
|
+
const suffixNode = titleSuffix ? (
|
|
103
|
+
<span className="inline-flex shrink-0 items-center">{titleSuffix}</span>
|
|
104
|
+
) : null;
|
|
105
|
+
|
|
106
|
+
const descOnRight = config.descriptionPlacement === 'right';
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className={[`tfds-form-title`, [config.root, className].filter(Boolean).join(' ')].filter(Boolean).join(' ')} data-tfds-component="FormTitle">
|
|
110
|
+
<div className={config.titleWrap}>
|
|
111
|
+
{config.indicator ? <span aria-hidden="true" className={config.indicator} /> : null}
|
|
112
|
+
<span className={config.title}>{resolvedTitle}</span>
|
|
113
|
+
{suffixNode}
|
|
114
|
+
{descOnRight ? descriptionNode : null}
|
|
115
|
+
</div>
|
|
116
|
+
{!descOnRight ? descriptionNode : null}
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormTitle — TOKEN_MAP(供平台属性面板展示)
|
|
3
|
+
* 组件实现代码见 FormTitle.jsx
|
|
4
|
+
*
|
|
5
|
+
* 说明:`variants` 键名与 `ComponentDetailPage` 约定一致,用于按当前 variant 切换展示;
|
|
6
|
+
* 其余分组为中文 UI 元素分组(platform-rules T1)。
|
|
7
|
+
*/
|
|
8
|
+
export const FORM_TITLE_TOKEN_MAP = {
|
|
9
|
+
variants: {
|
|
10
|
+
form: [
|
|
11
|
+
{ label: '标题色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
|
|
12
|
+
{ label: '标题号', cssProp: 'font-size', token: '--text-xl', value: '20px' },
|
|
13
|
+
{ label: '标题重', cssProp: 'font-weight', token: '--font-semibold', value: '600' },
|
|
14
|
+
{ label: '标题高', cssProp: 'line-height', value: '28px' },
|
|
15
|
+
{ label: '描述色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
16
|
+
{ label: '描述号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
17
|
+
{ label: '描述重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
|
|
18
|
+
{ label: '描述高', cssProp: 'line-height', value: '20px' },
|
|
19
|
+
{ label: '排列', cssProp: 'display', value: 'inline-flex' },
|
|
20
|
+
{ label: '方向', cssProp: 'flex-direction', value: 'column' },
|
|
21
|
+
{ label: '间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
|
|
22
|
+
],
|
|
23
|
+
'level-1': [
|
|
24
|
+
{ label: '标题色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
|
|
25
|
+
{ label: '标题号', cssProp: 'font-size', token: '--text-base', value: '16px' },
|
|
26
|
+
{ label: '标题重', cssProp: 'font-weight', token: '--font-semibold', value: '600' },
|
|
27
|
+
{ label: '标题高', cssProp: 'line-height', value: '22px' },
|
|
28
|
+
{ label: '描述色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
29
|
+
{ label: '描述号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
30
|
+
{ label: '描述重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
|
|
31
|
+
{ label: '描述高', cssProp: 'line-height', value: '20px' },
|
|
32
|
+
{ label: '标识色', cssProp: 'background', token: '--color-brand-500', value: '#56D3BC', semanticRef: 'status-primary' },
|
|
33
|
+
{ label: '标识宽', cssProp: 'width', value: '3px' },
|
|
34
|
+
{ label: '标识高', cssProp: 'height', value: '16px' },
|
|
35
|
+
{ label: '方向', cssProp: 'flex-direction', value: 'column' },
|
|
36
|
+
{ label: '间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
|
|
37
|
+
],
|
|
38
|
+
'level-2': [
|
|
39
|
+
{ label: '标题色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
|
|
40
|
+
{ label: '标题号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
41
|
+
{ label: '标题重', cssProp: 'font-weight', token: '--font-semibold', value: '600' },
|
|
42
|
+
{ label: '标题高', cssProp: 'line-height', value: '20px' },
|
|
43
|
+
{ label: '描述色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
44
|
+
{ label: '描述号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
45
|
+
{ label: '描述重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
|
|
46
|
+
{ label: '描述高', cssProp: 'line-height', value: '20px' },
|
|
47
|
+
{ label: '标识色', cssProp: 'background', token: '--color-brand-500', value: '#56D3BC', semanticRef: 'status-primary' },
|
|
48
|
+
{ label: '标识宽', cssProp: 'width', value: '3px' },
|
|
49
|
+
{ label: '标识高', cssProp: 'height', value: '14px' },
|
|
50
|
+
{ label: '方向', cssProp: 'flex-direction', value: 'row' },
|
|
51
|
+
{ label: '间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
|
|
52
|
+
],
|
|
53
|
+
'level-3': [
|
|
54
|
+
{ label: '标题色', cssProp: 'color', token: '--color-foreground', value: '#182230', semanticRef: 'text-primary' },
|
|
55
|
+
{ label: '标题号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
56
|
+
{ label: '标题重', cssProp: 'font-weight', token: '--font-semibold', value: '600' },
|
|
57
|
+
{ label: '标题高', cssProp: 'line-height', value: '20px' },
|
|
58
|
+
{ label: '描述色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
59
|
+
{ label: '描述号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
60
|
+
{ label: '描述重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
|
|
61
|
+
{ label: '描述高', cssProp: 'line-height', value: '20px' },
|
|
62
|
+
{ label: '标识色', cssProp: 'background', token: '--color-brand-500', value: '#56D3BC', semanticRef: 'status-primary' },
|
|
63
|
+
{ label: '标识宽', cssProp: 'width', value: '3px' },
|
|
64
|
+
{ label: '标识高', cssProp: 'height', value: '14px' },
|
|
65
|
+
{ label: '方向', cssProp: 'flex-direction', value: 'row' },
|
|
66
|
+
{ label: '间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
|
|
67
|
+
],
|
|
68
|
+
card: [
|
|
69
|
+
{ label: '标题色', cssProp: 'color', token: '--color-foreground-secondary', value: '#475467', semanticRef: 'text-secondary' },
|
|
70
|
+
{ label: '标题号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
71
|
+
{ label: '标题重', cssProp: 'font-weight', token: '--font-semibold', value: '600' },
|
|
72
|
+
{ label: '标题高', cssProp: 'line-height', value: '20px' },
|
|
73
|
+
{ label: '描述色', cssProp: 'color', token: '--color-foreground-muted', value: '#667085', semanticRef: 'text-tertiary' },
|
|
74
|
+
{ label: '描述号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
75
|
+
{ label: '描述重', cssProp: 'font-weight', token: '--font-normal', value: '400' },
|
|
76
|
+
{ label: '描述高', cssProp: 'line-height', value: '20px' },
|
|
77
|
+
{ label: '方向', cssProp: 'flex-direction', value: 'row' },
|
|
78
|
+
{ label: '间距', cssProp: 'gap', token: '--spacing-1', value: '4px' },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
布局: [
|
|
82
|
+
{ label: '标题组距', cssProp: 'gap (title)', value: '6px' },
|
|
83
|
+
{ label: '圆角', cssProp: 'border-radius (indicator)', value: '9999px' },
|
|
84
|
+
{ label: '标题后缀', cssProp: 'slot', value: 'titleSuffix 紧跟主标题,用于状态 Tag / 业务胶囊' },
|
|
85
|
+
{ label: '描述默认', cssProp: 'visibility', value: '默认隐藏;showDescription=true 时显示' },
|
|
86
|
+
],
|
|
87
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ArrowLeft } from 'lucide-react';
|
|
2
|
+
import Button from './Button';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* FullScreenPage — 全屏操作页容器(B端设计系统,Tailwind 内联)
|
|
6
|
+
*
|
|
7
|
+
* 覆盖整个定位父级(含侧边导航)的全屏操作页,用于内容量大、需长时间沉浸编辑的场景。
|
|
8
|
+
* 定位原理:absolute inset-0,须挂在 relative 定位父级内(如 BasePageFramePattern 根节点)。
|
|
9
|
+
*
|
|
10
|
+
* 两种背景色模式,对应不同的内容承载规范:
|
|
11
|
+
* white — 白底,内容直接铺在页面上(如轻量创建表单)
|
|
12
|
+
* grey — 浅灰底,内容须在 children 内自行包白色卡片容器承载(如复杂配置编辑页)
|
|
13
|
+
*
|
|
14
|
+
* 与 Modal / Sheet 的场景区别:
|
|
15
|
+
* Modal — 内容量少,快速确认,居中浮层
|
|
16
|
+
* Sheet — 内容中等,侧边补充信息
|
|
17
|
+
* Modal fullscreen — 内容较多,结构化表单,覆盖右侧内容区
|
|
18
|
+
* FullScreenPage — 内容非常多,沉浸长时间编辑,覆盖整个框架含侧边导航
|
|
19
|
+
*
|
|
20
|
+
* @prop {'white'|'grey'} [bg='white'] — 背景色模式
|
|
21
|
+
* @prop {string} [title='页面标题'] — 顶栏标题
|
|
22
|
+
* @prop {function} [onBack=null] — 返回/关闭回调,传入才显示返回按钮
|
|
23
|
+
* @prop {ReactNode} [headerActions=null] — 顶栏右侧操作区插槽(保存/发布等按钮)
|
|
24
|
+
* @prop {ReactNode} [children] — 页面主体内容
|
|
25
|
+
* @prop {boolean} [contained=false] — 预览/嵌入模式:true 时改用相对定位填满父容器,而非 absolute 覆盖
|
|
26
|
+
* @prop {string} [className=''] — 附加类名
|
|
27
|
+
* @prop {object} [style] — 内联样式
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/* ── 背景色模式 → Tailwind 类名 ── */
|
|
31
|
+
const BG_CLASS = {
|
|
32
|
+
/* 白底:内容直接铺在页面上 */
|
|
33
|
+
white: 'bg-surface',
|
|
34
|
+
/* 灰底:内容须在 children 内自行包白色卡片容器承载 */
|
|
35
|
+
grey: 'bg-blueGrey-200',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/* ── 顶栏:透明背景,无分割线,左右 24px / 上下 16px ── */
|
|
39
|
+
const HEADER = [
|
|
40
|
+
'flex shrink-0 items-center justify-between gap-3',
|
|
41
|
+
'px-6 py-4',
|
|
42
|
+
].join(' ');
|
|
43
|
+
|
|
44
|
+
/* ── 顶栏标题 ── */
|
|
45
|
+
const TITLE = 'text-base [font-weight:var(--font-semibold)] text-foreground truncate min-w-0';
|
|
46
|
+
|
|
47
|
+
export default function FullScreenPage({
|
|
48
|
+
bg = 'white',
|
|
49
|
+
title = '页面标题',
|
|
50
|
+
onBack,
|
|
51
|
+
headerActions,
|
|
52
|
+
children,
|
|
53
|
+
contained = false,
|
|
54
|
+
className = '',
|
|
55
|
+
style,
|
|
56
|
+
}) {
|
|
57
|
+
const positionClass = contained
|
|
58
|
+
? 'relative flex h-full min-h-0 min-w-0 w-full flex-col overflow-hidden'
|
|
59
|
+
: 'absolute inset-0 z-50 flex h-full min-h-0 min-w-0 w-full flex-col overflow-hidden';
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div
|
|
63
|
+
className={[`tfds-full-screen-page`, positionClass, BG_CLASS[bg], className].filter(Boolean).join(' ')}
|
|
64
|
+
style={style}
|
|
65
|
+
data-tfds-component="FullScreenPage"
|
|
66
|
+
>
|
|
67
|
+
{/* 顶栏:返回键 + 标题 + 右侧操作区 */}
|
|
68
|
+
<header className={HEADER} data-tfds-component="FullScreenPage.Header">
|
|
69
|
+
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
70
|
+
{onBack ? (
|
|
71
|
+
<Button
|
|
72
|
+
type="button"
|
|
73
|
+
variant="ghost-black"
|
|
74
|
+
size="sm"
|
|
75
|
+
icon={<ArrowLeft size={16} strokeWidth={2} />}
|
|
76
|
+
iconOnly
|
|
77
|
+
onClick={onBack}
|
|
78
|
+
aria-label="返回"
|
|
79
|
+
className="shrink-0"
|
|
80
|
+
data-tfds-component="FullScreenPage.Back"
|
|
81
|
+
/>
|
|
82
|
+
) : null}
|
|
83
|
+
<h1 className={TITLE}>{title}</h1>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{headerActions ? (
|
|
87
|
+
<div className="flex items-center gap-2 shrink-0">{headerActions}</div>
|
|
88
|
+
) : null}
|
|
89
|
+
</header>
|
|
90
|
+
|
|
91
|
+
{/* 内容区:flex-1 独立滚动,具体布局由 children 自定义 */}
|
|
92
|
+
<div className="flex flex-1 min-h-0 min-w-0 overflow-hidden" data-tfds-component="FullScreenPage.Content">
|
|
93
|
+
{children}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const FULL_SCREEN_PAGE_TOKEN_MAP = {
|
|
2
|
+
顶栏: [
|
|
3
|
+
{ label: '背景色', cssProp: 'background', value: 'transparent' },
|
|
4
|
+
{ label: '高度', cssProp: 'height', value: '56px' },
|
|
5
|
+
{ label: '横向内距', cssProp: 'padding-inline', value: '16px' },
|
|
6
|
+
],
|
|
7
|
+
标题: [
|
|
8
|
+
{ label: '颜色', cssProp: 'color', token: '--color-foreground', value: '#182230' },
|
|
9
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-base', value: '16px' },
|
|
10
|
+
{ label: '字重', cssProp: 'font-weight', value: '600' },
|
|
11
|
+
],
|
|
12
|
+
内容区: [
|
|
13
|
+
{ label: '白底背景', cssProp: 'background', token: '--color-surface', value: '#FFFFFF' },
|
|
14
|
+
{ label: '灰底背景', cssProp: 'background', token: '--color-blueGrey-200', value: '#F2F4F7', state: 'bg=grey' },
|
|
15
|
+
],
|
|
16
|
+
引用组件: [
|
|
17
|
+
{ label: '返回按钮', cssProp: '—', value: 'Button ghost-black sm iconOnly' },
|
|
18
|
+
],
|
|
19
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Icon — 矢量图标(非人像)
|
|
3
|
+
*
|
|
4
|
+
* **何时用**:按钮/输入前后缀、导航项、状态提示、空状态插画中的符号;默认继承父级文字色(`currentColor`)。
|
|
5
|
+
* **不用**:账号/用户/机器人「脸」→ `Avatar`;需要整段可点击文案 → `Button`;切换同页多块主内容 → `Tabs`。
|
|
6
|
+
*
|
|
7
|
+
* @prop {string} [name='check-stroked'] — 图标名称(kebab-case,见图标库)
|
|
8
|
+
* @prop {'xs'|'sm'|'md'|'lg'|'xl'|number} [size='sm'] — 尺寸档位:与 **Input/Select md(36px)** 搭配的前缀、行内符号默认 **sm(16px)**;独立展示用 md/lg/xl;也可传数字像素(少用)
|
|
9
|
+
* @prop {string} [color='currentColor'] — 仅在需脱离父级文字色时覆盖
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useId } from 'react';
|
|
13
|
+
import { ICON_MAP } from './icons/icon-data';
|
|
14
|
+
|
|
15
|
+
/* ── 尺寸映射 ── */
|
|
16
|
+
const SIZE_MAP = {
|
|
17
|
+
xs: 12,
|
|
18
|
+
sm: 16,
|
|
19
|
+
md: 20,
|
|
20
|
+
lg: 24,
|
|
21
|
+
xl: 32,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function normalizeIconEntry(entry) {
|
|
25
|
+
if (Array.isArray(entry)) {
|
|
26
|
+
return {
|
|
27
|
+
viewBox: '0 0 24 24',
|
|
28
|
+
defs: [],
|
|
29
|
+
shapes: entry,
|
|
30
|
+
contentTransform: '',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
viewBox: entry?.viewBox || '0 0 24 24',
|
|
36
|
+
defs: entry?.defs || [],
|
|
37
|
+
shapes: entry?.shapes || [],
|
|
38
|
+
contentTransform: entry?.contentTransform || '',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function remapPaintRef(value, idMap) {
|
|
43
|
+
if (!value || !idMap) return value;
|
|
44
|
+
return value.replace(/url\(#([^)]+)\)/g, (_, id) => `url(#${idMap[id] || id})`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function Icon({
|
|
48
|
+
name,
|
|
49
|
+
size = 'sm',
|
|
50
|
+
color = 'currentColor',
|
|
51
|
+
className,
|
|
52
|
+
style,
|
|
53
|
+
...rest
|
|
54
|
+
}) {
|
|
55
|
+
const entry = ICON_MAP[name];
|
|
56
|
+
if (!entry) return null;
|
|
57
|
+
|
|
58
|
+
const px = SIZE_MAP[size] ?? (typeof size === 'number' ? size : 24);
|
|
59
|
+
const iconId = useId().replace(/:/g, '');
|
|
60
|
+
const { viewBox, defs, shapes, contentTransform } = normalizeIconEntry(entry);
|
|
61
|
+
const defIdMap = Object.fromEntries(defs.map((def) => [def.id, `${iconId}-${def.id}`]));
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<svg
|
|
65
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
66
|
+
width={px}
|
|
67
|
+
height={px}
|
|
68
|
+
viewBox={viewBox}
|
|
69
|
+
preserveAspectRatio="xMidYMid meet"
|
|
70
|
+
fill="none"
|
|
71
|
+
className={[`tfds-icon`, className].filter(Boolean).join(' ')}
|
|
72
|
+
style={{ flexShrink: 0, overflow: 'visible', ...style }}
|
|
73
|
+
{...rest}
|
|
74
|
+
data-tfds-component="Icon">
|
|
75
|
+
{defs.length ? (
|
|
76
|
+
<defs>
|
|
77
|
+
{defs.map((def) => {
|
|
78
|
+
if (def.type === 'linearGradient') {
|
|
79
|
+
return (
|
|
80
|
+
<linearGradient
|
|
81
|
+
key={def.id}
|
|
82
|
+
id={defIdMap[def.id]}
|
|
83
|
+
x1={def.x1}
|
|
84
|
+
y1={def.y1}
|
|
85
|
+
x2={def.x2}
|
|
86
|
+
y2={def.y2}
|
|
87
|
+
gradientUnits={def.gradientUnits}
|
|
88
|
+
>
|
|
89
|
+
{def.stops.map((stop, index) => (
|
|
90
|
+
<stop
|
|
91
|
+
key={index}
|
|
92
|
+
offset={stop.offset}
|
|
93
|
+
stopColor={stop.stopColor}
|
|
94
|
+
stopOpacity={stop.stopOpacity}
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
</linearGradient>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
})}
|
|
103
|
+
</defs>
|
|
104
|
+
) : null}
|
|
105
|
+
|
|
106
|
+
<g transform={contentTransform || undefined}>
|
|
107
|
+
{shapes.map((shape, index) => {
|
|
108
|
+
const fill = remapPaintRef(shape.fill, defIdMap) || color;
|
|
109
|
+
const transform = shape.transform || undefined;
|
|
110
|
+
|
|
111
|
+
if (shape.type === 'ellipse') {
|
|
112
|
+
return (
|
|
113
|
+
<ellipse
|
|
114
|
+
key={index}
|
|
115
|
+
cx={shape.cx}
|
|
116
|
+
cy={shape.cy}
|
|
117
|
+
rx={shape.rx}
|
|
118
|
+
ry={shape.ry}
|
|
119
|
+
fill={fill}
|
|
120
|
+
transform={transform}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (shape.type === 'rect') {
|
|
126
|
+
return (
|
|
127
|
+
<rect
|
|
128
|
+
key={index}
|
|
129
|
+
x={shape.x}
|
|
130
|
+
y={shape.y}
|
|
131
|
+
width={shape.width}
|
|
132
|
+
height={shape.height}
|
|
133
|
+
rx={shape.rx}
|
|
134
|
+
ry={shape.ry}
|
|
135
|
+
fill={shape.fill === 'none' ? 'none' : fill}
|
|
136
|
+
stroke={shape.stroke ? (shape.strokeColor || color) : undefined}
|
|
137
|
+
strokeWidth={shape.stroke ? (shape.strokeWidth || '2') : undefined}
|
|
138
|
+
transform={transform}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (shape.stroke) {
|
|
144
|
+
return (
|
|
145
|
+
<path
|
|
146
|
+
key={index}
|
|
147
|
+
d={shape.d}
|
|
148
|
+
fill="none"
|
|
149
|
+
stroke={shape.strokeColor || color}
|
|
150
|
+
strokeWidth={shape.strokeWidth || '2'}
|
|
151
|
+
strokeLinecap={shape.strokeLinecap || 'round'}
|
|
152
|
+
strokeLinejoin={shape.strokeLinejoin || 'round'}
|
|
153
|
+
transform={transform}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<path
|
|
160
|
+
key={index}
|
|
161
|
+
d={shape.d}
|
|
162
|
+
fill={fill}
|
|
163
|
+
fillRule={shape.fillRule || undefined}
|
|
164
|
+
clipRule={shape.clipRule || undefined}
|
|
165
|
+
transform={transform}
|
|
166
|
+
/>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
169
|
+
</g>
|
|
170
|
+
</svg>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Icon — Token 映射表
|
|
2
|
+
// 注:Icon 组件通过 props 直接控制尺寸和颜色(非 CSS 变量),
|
|
3
|
+
// 此 TOKEN_MAP 仅供平台属性面板展示参考值。
|
|
4
|
+
|
|
5
|
+
export const ICON_TOKEN_MAP = {
|
|
6
|
+
base: [
|
|
7
|
+
{ token: '--icon-color', label: '图标颜色', type: 'color', default: 'currentColor' },
|
|
8
|
+
],
|
|
9
|
+
sizes: {
|
|
10
|
+
xs: [
|
|
11
|
+
{ token: '--icon-size-xs', label: '尺寸', type: 'size', default: '12px' },
|
|
12
|
+
],
|
|
13
|
+
sm: [
|
|
14
|
+
{ token: '--icon-size-sm', label: '尺寸', type: 'size', default: '16px' },
|
|
15
|
+
],
|
|
16
|
+
md: [
|
|
17
|
+
{ token: '--icon-size-md', label: '尺寸', type: 'size', default: '20px' },
|
|
18
|
+
],
|
|
19
|
+
lg: [
|
|
20
|
+
{ token: '--icon-size-lg', label: '尺寸', type: 'size', default: '24px' },
|
|
21
|
+
],
|
|
22
|
+
xl: [
|
|
23
|
+
{ token: '--icon-size-xl', label: '尺寸', type: 'size', default: '32px' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|