@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,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimePicker — 时间选择器
|
|
3
|
+
* @prop {'time'|'timerange'} [type='time'] — 选择类型
|
|
4
|
+
* @prop {string|Array<string>} value — 当前值(受控)
|
|
5
|
+
* @prop {string|Array<string>} [defaultValue=undefined] — 非受控初始值
|
|
6
|
+
* @prop {string} [placeholder=''] — 占位文案
|
|
7
|
+
* @prop {boolean} [disabled=false] — 是否禁用
|
|
8
|
+
* @prop {boolean} [defaultOpen=false] — 初始是否展开
|
|
9
|
+
* @prop {Function} [onChange=null] — 变更回调
|
|
10
|
+
* @prop {string} [className=''] — 额外类名
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
import { createPortal } from 'react-dom';
|
|
15
|
+
|
|
16
|
+
const PANEL_GAP = 4;
|
|
17
|
+
const PANEL_Z = 10000;
|
|
18
|
+
const PANEL_MIN_WIDTH = 150;
|
|
19
|
+
|
|
20
|
+
const TYPE_CONFIG = {
|
|
21
|
+
time: {
|
|
22
|
+
widthClass: 'w-full min-w-0 max-w-full',
|
|
23
|
+
placeholder: '请选择时间',
|
|
24
|
+
isRange: false,
|
|
25
|
+
panelHeight: 252,
|
|
26
|
+
},
|
|
27
|
+
timerange: {
|
|
28
|
+
widthClass: 'w-full min-w-0 max-w-full',
|
|
29
|
+
placeholderStart: '开始时间',
|
|
30
|
+
placeholderEnd: '结束时间',
|
|
31
|
+
isRange: true,
|
|
32
|
+
panelHeight: 304,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const FRAME_BASE = [
|
|
37
|
+
'relative inline-flex h-[36px] items-center',
|
|
38
|
+
'border border-solid border-border rounded-md',
|
|
39
|
+
'bg-surface pl-3 pr-2 gap-2',
|
|
40
|
+
'[font-family:inherit] outline-none cursor-pointer',
|
|
41
|
+
'transition-colors duration-150',
|
|
42
|
+
'hover:border-blueGrey-400',
|
|
43
|
+
'focus-within:border-primary focus-within:hover:border-primary',
|
|
44
|
+
].join(' ');
|
|
45
|
+
|
|
46
|
+
const DISABLED_CLASS = 'bg-disabled border-border cursor-not-allowed opacity-60 pointer-events-none';
|
|
47
|
+
const TEXT_BASE = 'text-sm leading-[20px]';
|
|
48
|
+
const VALUE_TEXT = [TEXT_BASE, 'text-foreground truncate'].join(' ');
|
|
49
|
+
const PLACEHOLDER_TEXT = [TEXT_BASE, 'text-foreground-muted truncate'].join(' ');
|
|
50
|
+
const RANGE_SEP = [TEXT_BASE, 'text-foreground-muted shrink-0'].join(' ');
|
|
51
|
+
const CONTENT_SLOT = 'min-w-0 flex-1 text-left';
|
|
52
|
+
const ICON_SLOT = 'inline-flex size-[16px] shrink-0 items-center justify-center text-foreground-disabled [&>svg]:size-[16px]';
|
|
53
|
+
|
|
54
|
+
const PANEL_BASE = [
|
|
55
|
+
'overflow-hidden rounded-md border border-solid border-border',
|
|
56
|
+
'bg-surface shadow-lg',
|
|
57
|
+
].join(' ');
|
|
58
|
+
const PANEL_HEADER = 'flex h-[52px] items-center justify-center border-b border-solid border-b-border px-3 text-sm [font-weight:var(--font-semibold)] text-foreground';
|
|
59
|
+
const PANEL_BODY = 'relative h-[252px] overflow-hidden';
|
|
60
|
+
const WHEEL = 'relative flex h-full w-full overflow-hidden';
|
|
61
|
+
const SELECTOR_AREA = 'pointer-events-none absolute left-0 right-0 top-1/2 z-10 h-[36px] -translate-y-1/2 bg-brand-50';
|
|
62
|
+
const COLUMN = 'relative z-20 h-full min-w-0 flex-1 overflow-y-auto py-[108px] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden';
|
|
63
|
+
const COLUMN_DIVIDER = 'border-r border-solid border-r-border';
|
|
64
|
+
const OPTION = [
|
|
65
|
+
'flex h-[36px] w-full items-center justify-center',
|
|
66
|
+
'border-none bg-transparent px-0',
|
|
67
|
+
'text-sm leading-[36px] tracking-normal text-foreground-secondary',
|
|
68
|
+
'hover:bg-transparent hover:text-foreground',
|
|
69
|
+
'transition-colors duration-100',
|
|
70
|
+
].join(' ');
|
|
71
|
+
const OPTION_ACTIVE = 'text-foreground hover:bg-transparent hover:text-foreground';
|
|
72
|
+
const OPTION_UNIT = 'text-sm leading-[36px] font-normal text-foreground';
|
|
73
|
+
const ITEM_HEIGHT = 36;
|
|
74
|
+
|
|
75
|
+
function pad2(n) {
|
|
76
|
+
return String(n).padStart(2, '0');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const HOURS = Array.from({ length: 24 }, (_, i) => pad2(i));
|
|
80
|
+
const MINUTES = Array.from({ length: 60 }, (_, i) => pad2(i));
|
|
81
|
+
|
|
82
|
+
function ClockIcon() {
|
|
83
|
+
return (
|
|
84
|
+
<svg viewBox="0 0 16 16" fill="none" className="size-[16px]" aria-hidden="true">
|
|
85
|
+
<circle cx="8" cy="8" r="5.5" stroke="currentColor" strokeWidth="1.2" />
|
|
86
|
+
<path d="M8 5.4V8L9.8 9" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
87
|
+
</svg>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseTime(raw, fallback = '') {
|
|
92
|
+
if (typeof raw !== 'string') return fallback;
|
|
93
|
+
const match = raw.trim().match(/^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/);
|
|
94
|
+
if (!match) return fallback;
|
|
95
|
+
const h = Math.max(0, Math.min(23, Number(match[1])));
|
|
96
|
+
const m = Math.max(0, Math.min(59, Number(match[2])));
|
|
97
|
+
return `${pad2(h)}:${pad2(m)}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function splitTime(raw, fallback = '09:00') {
|
|
101
|
+
const [hour, minute] = parseTime(raw, fallback).split(':');
|
|
102
|
+
return [hour, minute];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeValue(raw, config) {
|
|
106
|
+
if (!config.isRange) return parseTime(raw, '');
|
|
107
|
+
if (!Array.isArray(raw)) return ['', ''];
|
|
108
|
+
return [parseTime(raw[0], ''), parseTime(raw[1], '')];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function measurePanel(triggerEl, minPanelWidth, panelHeight) {
|
|
112
|
+
const rect = triggerEl.getBoundingClientRect();
|
|
113
|
+
const panelWidth = Math.max(minPanelWidth, rect.width);
|
|
114
|
+
const spaceBelow = window.innerHeight - rect.bottom - PANEL_GAP;
|
|
115
|
+
const spaceAbove = rect.top - PANEL_GAP;
|
|
116
|
+
const placeBelow = spaceBelow >= panelHeight || spaceBelow >= spaceAbove;
|
|
117
|
+
|
|
118
|
+
const top = placeBelow
|
|
119
|
+
? rect.bottom + PANEL_GAP
|
|
120
|
+
: Math.max(8, rect.top - panelHeight - PANEL_GAP);
|
|
121
|
+
|
|
122
|
+
let left = rect.left;
|
|
123
|
+
if (left + panelWidth > window.innerWidth - 8) {
|
|
124
|
+
left = Math.max(8, window.innerWidth - panelWidth - 8);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { top, left, width: panelWidth };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function TimeColumn({ part, values, active, unit, showDivider, onPick }) {
|
|
131
|
+
const columnRef = useRef(null);
|
|
132
|
+
const scrollTimerRef = useRef(null);
|
|
133
|
+
const isScrollingRef = useRef(false);
|
|
134
|
+
const lastPickedRef = useRef(active);
|
|
135
|
+
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
lastPickedRef.current = active;
|
|
138
|
+
const node = columnRef.current;
|
|
139
|
+
const activeIndex = values.indexOf(active);
|
|
140
|
+
if (!node || activeIndex < 0) return;
|
|
141
|
+
if (isScrollingRef.current) return;
|
|
142
|
+
node.scrollTop = activeIndex * ITEM_HEIGHT;
|
|
143
|
+
}, [active, values]);
|
|
144
|
+
|
|
145
|
+
useEffect(() => () => {
|
|
146
|
+
if (scrollTimerRef.current) window.clearTimeout(scrollTimerRef.current);
|
|
147
|
+
}, []);
|
|
148
|
+
|
|
149
|
+
const handleScroll = () => {
|
|
150
|
+
isScrollingRef.current = true;
|
|
151
|
+
const node = columnRef.current;
|
|
152
|
+
if (node) {
|
|
153
|
+
const nextIndex = Math.max(0, Math.min(values.length - 1, Math.round(node.scrollTop / ITEM_HEIGHT)));
|
|
154
|
+
const nextValue = values[nextIndex];
|
|
155
|
+
if (nextValue && nextValue !== lastPickedRef.current) {
|
|
156
|
+
lastPickedRef.current = nextValue;
|
|
157
|
+
onPick(part, nextValue);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (scrollTimerRef.current) window.clearTimeout(scrollTimerRef.current);
|
|
162
|
+
scrollTimerRef.current = window.setTimeout(() => {
|
|
163
|
+
const currentNode = columnRef.current;
|
|
164
|
+
if (!currentNode) return;
|
|
165
|
+
const nextIndex = Math.max(0, Math.min(values.length - 1, Math.round(currentNode.scrollTop / ITEM_HEIGHT)));
|
|
166
|
+
currentNode.scrollTo({ top: nextIndex * ITEM_HEIGHT, behavior: 'smooth' });
|
|
167
|
+
window.setTimeout(() => {
|
|
168
|
+
isScrollingRef.current = false;
|
|
169
|
+
}, 120);
|
|
170
|
+
}, 120);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const pickItem = (item) => {
|
|
174
|
+
const node = columnRef.current;
|
|
175
|
+
const nextIndex = values.indexOf(item);
|
|
176
|
+
isScrollingRef.current = false;
|
|
177
|
+
lastPickedRef.current = item;
|
|
178
|
+
if (node && nextIndex >= 0) node.scrollTo({ top: nextIndex * ITEM_HEIGHT, behavior: 'smooth' });
|
|
179
|
+
onPick(part, item);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div ref={columnRef} className={[COLUMN, showDivider ? COLUMN_DIVIDER : ''].filter(Boolean).join(' ')} onScroll={handleScroll}>
|
|
184
|
+
{values.map((item) => {
|
|
185
|
+
const selected = item === active;
|
|
186
|
+
return (
|
|
187
|
+
<button
|
|
188
|
+
key={item}
|
|
189
|
+
type="button"
|
|
190
|
+
className={[OPTION, selected ? OPTION_ACTIVE : ''].filter(Boolean).join(' ')}
|
|
191
|
+
onClick={() => pickItem(item)}
|
|
192
|
+
>
|
|
193
|
+
{item}{selected ? <span className={OPTION_UNIT}>{unit}</span> : null}
|
|
194
|
+
</button>
|
|
195
|
+
);
|
|
196
|
+
})}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function TimeColumns({ value, fallback, onPick }) {
|
|
202
|
+
const [hour, minute] = splitTime(value, fallback);
|
|
203
|
+
const columns = [
|
|
204
|
+
{ key: 'hour', values: HOURS, active: hour, unit: '时' },
|
|
205
|
+
{ key: 'minute', values: MINUTES, active: minute, unit: '分' },
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
const pick = (part, next) => {
|
|
209
|
+
const current = { hour, minute };
|
|
210
|
+
current[part] = next;
|
|
211
|
+
onPick(`${current.hour}:${current.minute}`);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
return columns.map((column, index) => (
|
|
215
|
+
<TimeColumn
|
|
216
|
+
key={column.key}
|
|
217
|
+
part={column.key}
|
|
218
|
+
values={column.values}
|
|
219
|
+
active={column.active}
|
|
220
|
+
unit={column.unit}
|
|
221
|
+
showDivider={index < columns.length - 1}
|
|
222
|
+
onPick={pick}
|
|
223
|
+
/>
|
|
224
|
+
));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function TimeWheel({ value, fallback, onPick }) {
|
|
228
|
+
return (
|
|
229
|
+
<div className={WHEEL}>
|
|
230
|
+
<div className={SELECTOR_AREA} />
|
|
231
|
+
<TimeColumns value={value} fallback={fallback} onPick={onPick} />
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function TimeRangeWheel({ startValue, endValue, fallbackStart, fallbackEnd, onPick }) {
|
|
237
|
+
const [startHour, startMinute] = splitTime(startValue, fallbackStart);
|
|
238
|
+
const [endHour, endMinute] = splitTime(endValue, fallbackEnd);
|
|
239
|
+
const columns = [
|
|
240
|
+
{ key: 'startHour', values: HOURS, active: startHour, unit: '时' },
|
|
241
|
+
{ key: 'startMinute', values: MINUTES, active: startMinute, unit: '分' },
|
|
242
|
+
{ key: 'endHour', values: HOURS, active: endHour, unit: '时' },
|
|
243
|
+
{ key: 'endMinute', values: MINUTES, active: endMinute, unit: '分' },
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
const pick = (part, next) => {
|
|
247
|
+
const current = { startHour, startMinute, endHour, endMinute };
|
|
248
|
+
current[part] = next;
|
|
249
|
+
onPick([
|
|
250
|
+
`${current.startHour}:${current.startMinute}`,
|
|
251
|
+
`${current.endHour}:${current.endMinute}`,
|
|
252
|
+
]);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<div className={WHEEL}>
|
|
257
|
+
<div className={SELECTOR_AREA} />
|
|
258
|
+
{columns.map((column, index) => (
|
|
259
|
+
<TimeColumn
|
|
260
|
+
key={column.key}
|
|
261
|
+
part={column.key}
|
|
262
|
+
values={column.values}
|
|
263
|
+
active={column.active}
|
|
264
|
+
unit={column.unit}
|
|
265
|
+
showDivider={index < columns.length - 1}
|
|
266
|
+
onPick={pick}
|
|
267
|
+
/>
|
|
268
|
+
))}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export default function TimePicker({
|
|
274
|
+
type = 'time',
|
|
275
|
+
value,
|
|
276
|
+
defaultValue,
|
|
277
|
+
placeholder,
|
|
278
|
+
disabled = false,
|
|
279
|
+
defaultOpen = false,
|
|
280
|
+
onChange,
|
|
281
|
+
className = '',
|
|
282
|
+
...rest
|
|
283
|
+
}) {
|
|
284
|
+
const config = TYPE_CONFIG[type] || TYPE_CONFIG.time;
|
|
285
|
+
const isControlled = value !== undefined;
|
|
286
|
+
const [innerValue, setInnerValue] = useState(defaultValue);
|
|
287
|
+
const currentRawValue = isControlled ? value : innerValue;
|
|
288
|
+
const normalizedValue = useMemo(() => normalizeValue(currentRawValue, config), [currentRawValue, config]);
|
|
289
|
+
const [open, setOpen] = useState(Boolean(defaultOpen) && !disabled);
|
|
290
|
+
const [panelPos, setPanelPos] = useState({ top: 0, left: 0, width: PANEL_MIN_WIDTH });
|
|
291
|
+
const triggerRef = useRef(null);
|
|
292
|
+
const panelRef = useRef(null);
|
|
293
|
+
|
|
294
|
+
useEffect(() => {
|
|
295
|
+
setOpen(Boolean(defaultOpen) && !disabled);
|
|
296
|
+
}, [defaultOpen, disabled, type]);
|
|
297
|
+
|
|
298
|
+
const updatePanelPos = useCallback(() => {
|
|
299
|
+
if (!triggerRef.current) return;
|
|
300
|
+
setPanelPos(measurePanel(triggerRef.current, PANEL_MIN_WIDTH, config.panelHeight));
|
|
301
|
+
}, [config.panelHeight]);
|
|
302
|
+
|
|
303
|
+
useLayoutEffect(() => {
|
|
304
|
+
if (!open) return;
|
|
305
|
+
updatePanelPos();
|
|
306
|
+
}, [open, updatePanelPos]);
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (!open) return;
|
|
310
|
+
let frameId = 0;
|
|
311
|
+
const startedAt = performance.now();
|
|
312
|
+
const syncDuringPreviewMotion = () => {
|
|
313
|
+
updatePanelPos();
|
|
314
|
+
if (performance.now() - startedAt < 260) {
|
|
315
|
+
frameId = window.requestAnimationFrame(syncDuringPreviewMotion);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
frameId = window.requestAnimationFrame(syncDuringPreviewMotion);
|
|
319
|
+
return () => window.cancelAnimationFrame(frameId);
|
|
320
|
+
}, [open, updatePanelPos]);
|
|
321
|
+
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!open) return;
|
|
324
|
+
const onMove = () => updatePanelPos();
|
|
325
|
+
window.addEventListener('resize', onMove);
|
|
326
|
+
window.addEventListener('scroll', onMove, true);
|
|
327
|
+
return () => {
|
|
328
|
+
window.removeEventListener('resize', onMove);
|
|
329
|
+
window.removeEventListener('scroll', onMove, true);
|
|
330
|
+
};
|
|
331
|
+
}, [open, updatePanelPos]);
|
|
332
|
+
|
|
333
|
+
useEffect(() => {
|
|
334
|
+
if (!open) return;
|
|
335
|
+
const onDown = (e) => {
|
|
336
|
+
const t = e.target;
|
|
337
|
+
if (triggerRef.current?.contains(t) || panelRef.current?.contains(t)) return;
|
|
338
|
+
setOpen(false);
|
|
339
|
+
};
|
|
340
|
+
const onKey = (e) => {
|
|
341
|
+
if (e.key === 'Escape') setOpen(false);
|
|
342
|
+
};
|
|
343
|
+
document.addEventListener('mousedown', onDown);
|
|
344
|
+
document.addEventListener('keydown', onKey);
|
|
345
|
+
return () => {
|
|
346
|
+
document.removeEventListener('mousedown', onDown);
|
|
347
|
+
document.removeEventListener('keydown', onKey);
|
|
348
|
+
};
|
|
349
|
+
}, [open]);
|
|
350
|
+
|
|
351
|
+
const commitValue = useCallback((nextVal) => {
|
|
352
|
+
if (!isControlled) setInnerValue(nextVal);
|
|
353
|
+
onChange?.(nextVal);
|
|
354
|
+
}, [isControlled, onChange]);
|
|
355
|
+
|
|
356
|
+
const triggerClass = [
|
|
357
|
+
FRAME_BASE,
|
|
358
|
+
config.widthClass,
|
|
359
|
+
disabled ? DISABLED_CLASS : '',
|
|
360
|
+
className,
|
|
361
|
+
].filter(Boolean).join(' ');
|
|
362
|
+
|
|
363
|
+
const displaySingle = !config.isRange ? normalizedValue : '';
|
|
364
|
+
const displayStart = config.isRange ? normalizedValue[0] : '';
|
|
365
|
+
const displayEnd = config.isRange ? normalizedValue[1] : '';
|
|
366
|
+
|
|
367
|
+
const panel = open && typeof document !== 'undefined' ? createPortal(
|
|
368
|
+
<div
|
|
369
|
+
ref={panelRef}
|
|
370
|
+
className={PANEL_BASE}
|
|
371
|
+
style={{
|
|
372
|
+
position: 'fixed',
|
|
373
|
+
top: panelPos.top,
|
|
374
|
+
left: panelPos.left,
|
|
375
|
+
width: `${panelPos.width}px`,
|
|
376
|
+
height: `${config.panelHeight}px`,
|
|
377
|
+
zIndex: PANEL_Z,
|
|
378
|
+
}}
|
|
379
|
+
>
|
|
380
|
+
{!config.isRange ? (
|
|
381
|
+
<div className={PANEL_BODY}>
|
|
382
|
+
<TimeWheel value={displaySingle} fallback="12:15" onPick={commitValue} />
|
|
383
|
+
</div>
|
|
384
|
+
) : (
|
|
385
|
+
<>
|
|
386
|
+
<div className="grid grid-cols-2">
|
|
387
|
+
<div className={PANEL_HEADER}>开始时间</div>
|
|
388
|
+
<div className={PANEL_HEADER}>结束时间</div>
|
|
389
|
+
</div>
|
|
390
|
+
<div className={PANEL_BODY}>
|
|
391
|
+
<TimeRangeWheel
|
|
392
|
+
startValue={displayStart}
|
|
393
|
+
endValue={displayEnd}
|
|
394
|
+
fallbackStart="09:30"
|
|
395
|
+
fallbackEnd="18:00"
|
|
396
|
+
onPick={commitValue}
|
|
397
|
+
/>
|
|
398
|
+
</div>
|
|
399
|
+
</>
|
|
400
|
+
)}
|
|
401
|
+
</div>,
|
|
402
|
+
document.body,
|
|
403
|
+
) : null;
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<>
|
|
407
|
+
<button
|
|
408
|
+
ref={triggerRef}
|
|
409
|
+
type="button"
|
|
410
|
+
className={[`tfds-time-picker`, triggerClass].filter(Boolean).join(' ')}
|
|
411
|
+
disabled={disabled}
|
|
412
|
+
onClick={() => {
|
|
413
|
+
if (!disabled) setOpen((v) => !v);
|
|
414
|
+
}}
|
|
415
|
+
aria-haspopup="dialog"
|
|
416
|
+
aria-expanded={open}
|
|
417
|
+
{...rest}
|
|
418
|
+
data-tfds-component="TimePicker"
|
|
419
|
+
>
|
|
420
|
+
{!config.isRange ? (
|
|
421
|
+
<span className={CONTENT_SLOT}>
|
|
422
|
+
{displaySingle ? (
|
|
423
|
+
<span className={VALUE_TEXT}>{displaySingle}</span>
|
|
424
|
+
) : (
|
|
425
|
+
<span className={PLACEHOLDER_TEXT}>{placeholder || config.placeholder}</span>
|
|
426
|
+
)}
|
|
427
|
+
</span>
|
|
428
|
+
) : (
|
|
429
|
+
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
430
|
+
<span className={[CONTENT_SLOT, displayStart ? VALUE_TEXT : PLACEHOLDER_TEXT].join(' ')}>
|
|
431
|
+
{displayStart || config.placeholderStart}
|
|
432
|
+
</span>
|
|
433
|
+
<span className={RANGE_SEP}>~</span>
|
|
434
|
+
<span className={[CONTENT_SLOT, displayEnd ? VALUE_TEXT : PLACEHOLDER_TEXT].join(' ')}>
|
|
435
|
+
{displayEnd || config.placeholderEnd}
|
|
436
|
+
</span>
|
|
437
|
+
</span>
|
|
438
|
+
)}
|
|
439
|
+
<span className={ICON_SLOT}><ClockIcon /></span>
|
|
440
|
+
</button>
|
|
441
|
+
{panel}
|
|
442
|
+
</>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimePicker — TOKEN_MAP(供平台属性面板展示)
|
|
3
|
+
*
|
|
4
|
+
* 对齐 Figma TimePicker trigger 与 panel,支持单时间与时间范围。
|
|
5
|
+
*/
|
|
6
|
+
export const TIMEPICKER_TOKEN_MAP = {
|
|
7
|
+
base: [
|
|
8
|
+
{ label: '默认高度', cssProp: 'height', value: '36px' },
|
|
9
|
+
],
|
|
10
|
+
值文本: [
|
|
11
|
+
{ label: '颜色', cssProp: 'color', token: '--text-primary', value: '#182230', semanticRef: 'text-primary' },
|
|
12
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
13
|
+
{ label: '行高', cssProp: 'line-height', value: '20px' },
|
|
14
|
+
],
|
|
15
|
+
占位符: [
|
|
16
|
+
{ label: '颜色', cssProp: 'color', token: '--text-tertiary', value: '#667085', semanticRef: 'text-tertiary' },
|
|
17
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
18
|
+
{ label: '行高', cssProp: 'line-height', value: '20px' },
|
|
19
|
+
],
|
|
20
|
+
范围分隔: [
|
|
21
|
+
{ label: '颜色', cssProp: 'color', token: '--text-tertiary', value: '#667085', semanticRef: 'text-tertiary' },
|
|
22
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
23
|
+
],
|
|
24
|
+
图标: [
|
|
25
|
+
{ label: '颜色', cssProp: 'color', token: '--text-disabled', value: '#98A2B3', semanticRef: 'text-disabled' },
|
|
26
|
+
{ label: '尺寸', cssProp: 'width / height', value: '16px' },
|
|
27
|
+
],
|
|
28
|
+
触发器: [
|
|
29
|
+
{ label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
|
|
30
|
+
{ label: '边框色', cssProp: 'border-color', token: '--color-border-default', value: '#E4E7EC', semanticRef: 'border-default' },
|
|
31
|
+
{ label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
|
|
32
|
+
{ label: '左内边距', cssProp: 'padding-left', token: '--spacing-3', value: '12px' },
|
|
33
|
+
{ label: '右内边距', cssProp: 'padding-right', token: '--spacing-2', value: '8px' },
|
|
34
|
+
{ label: '元素间距', cssProp: 'gap', token: '--spacing-2', value: '8px' },
|
|
35
|
+
{ label: '悬浮边框', cssProp: 'border-color', token: '--color-blueGrey-400', value: '#D0D5DD', state: 'hover' },
|
|
36
|
+
{ label: '聚焦边框', cssProp: 'border-color', token: '--status-primary', value: '#56D3BC', semanticRef: 'status-primary', state: 'focus' },
|
|
37
|
+
{ label: '禁用背景', cssProp: 'background', token: '--color-disabled', value: '#F9FAFB', semanticRef: 'bg-disabled', state: 'disabled' },
|
|
38
|
+
{ label: '透明度', cssProp: 'opacity', value: '0.6', state: 'disabled' },
|
|
39
|
+
],
|
|
40
|
+
面板容器: [
|
|
41
|
+
{ label: '背景色', cssProp: 'background', token: '--color-surface', value: '#FFFFFF', semanticRef: 'bg-surface' },
|
|
42
|
+
{ label: '圆角', cssProp: 'border-radius', token: '--radius-md', value: '8px' },
|
|
43
|
+
{ label: '描边', cssProp: 'border', token: '--color-border-default', value: '1px #E4E7EC', semanticRef: 'border-default' },
|
|
44
|
+
{ label: '阴影', cssProp: 'box-shadow', value: 'shadow-lg(与 Select / Modal 浮层一致)' },
|
|
45
|
+
],
|
|
46
|
+
范围标题: [
|
|
47
|
+
{ label: '高度', cssProp: 'height', value: '52px' },
|
|
48
|
+
{ label: '文字色', cssProp: 'color', token: '--text-primary', value: '#182230', semanticRef: 'text-primary' },
|
|
49
|
+
{ label: '字号', cssProp: 'font-size', token: '--text-sm', value: '14px' },
|
|
50
|
+
{ label: '字重', cssProp: 'font-weight', value: '600' },
|
|
51
|
+
{ label: '下边框', cssProp: 'border-bottom-color', token: '--border-default', value: 'rgba(45,66,107,0.12)', semanticRef: 'border-default' },
|
|
52
|
+
],
|
|
53
|
+
时间列: [
|
|
54
|
+
{ label: '列宽', cssProp: 'width', value: 'flex: 1, equal columns' },
|
|
55
|
+
{ label: '列高', cssProp: 'height', value: '252px' },
|
|
56
|
+
{ label: '上下滚动留白', cssProp: 'padding-top / padding-bottom', value: '108px' },
|
|
57
|
+
{ label: '选项尺寸', cssProp: 'width / height', value: '100% / 36px' },
|
|
58
|
+
{ label: '普通文字', cssProp: 'color', token: '--text-secondary', value: '#475467', semanticRef: 'text-secondary' },
|
|
59
|
+
{ label: '选中文字', cssProp: 'color', token: '--text-primary', value: '#182230', semanticRef: 'text-primary', state: 'selected' },
|
|
60
|
+
{ label: '列分割线', cssProp: 'border-right-color', token: '--border-default', value: 'rgba(45,66,107,0.12)', semanticRef: 'border-default' },
|
|
61
|
+
{ label: '滚动吸附', cssProp: 'scroll behavior', value: 'scrolling item in center selector commits value' },
|
|
62
|
+
],
|
|
63
|
+
固定选择区: [
|
|
64
|
+
{ label: '位置', cssProp: 'top', value: '50%' },
|
|
65
|
+
{ label: '高度', cssProp: 'height', value: '36px' },
|
|
66
|
+
{ label: '背景色', cssProp: 'background', token: '--color-brand-50', value: '#EAFAF6', semanticRef: 'brand-50' },
|
|
67
|
+
{ label: '覆盖范围', cssProp: 'left / right', value: '0 / 0, spans all columns' },
|
|
68
|
+
],
|
|
69
|
+
面板尺寸: [
|
|
70
|
+
{ label: '面板宽度', cssProp: 'width', value: 'same as trigger, min 150px' },
|
|
71
|
+
{ label: '单时间高度', cssProp: 'height', value: '252px' },
|
|
72
|
+
{ label: '时间范围高度', cssProp: 'height', value: '304px' },
|
|
73
|
+
],
|
|
74
|
+
类型宽度: [
|
|
75
|
+
{ label: '触发器宽度', cssProp: 'width', token: '--size-input-width', value: '300px' },
|
|
76
|
+
],
|
|
77
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { X } from 'lucide-react';
|
|
2
|
+
import Button from './Button';
|
|
3
|
+
import Icon from './Icon';
|
|
4
|
+
|
|
5
|
+
const SHADOW =
|
|
6
|
+
'shadow-[0px_0px_1px_rgba(0,0,0,0.3),0px_4px_14px_rgba(0,0,0,0.1)]';
|
|
7
|
+
|
|
8
|
+
/* ── 语义类型 → 背景 + 阴影 ── */
|
|
9
|
+
const TYPE_BG = {
|
|
10
|
+
info: ['bg-brand-50', SHADOW].join(' '),
|
|
11
|
+
success: ['bg-green-50', SHADOW].join(' '),
|
|
12
|
+
warning: ['bg-orange-50', SHADOW].join(' '),
|
|
13
|
+
error: ['bg-red-50', SHADOW].join(' '),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/* ── 语义类型 → 描边颜色(bordered=true 时使用) ── */
|
|
17
|
+
const TYPE_BORDER = {
|
|
18
|
+
info: 'border-teal-600',
|
|
19
|
+
success: 'border-green-500',
|
|
20
|
+
warning: 'border-orange-500',
|
|
21
|
+
error: 'border-red-500',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/* ── 语义类型 → 状态图标(Icon name + 父级着色) ── */
|
|
25
|
+
const TYPE_ICON = {
|
|
26
|
+
info: { name: 'info-circle-stroked', tint: 'text-teal-600' },
|
|
27
|
+
success: { name: 'check-circle-stroked', tint: 'text-green-500' },
|
|
28
|
+
warning: { name: 'alert-triangle-stroked', tint: 'text-orange-500' },
|
|
29
|
+
error: { name: 'alert-circle-stroked', tint: 'text-red-500' },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/* ── 无描边时根容器边框类(占位透明,避免布局跳动) ── */
|
|
33
|
+
const BORDER_OFF = 'border-transparent';
|
|
34
|
+
|
|
35
|
+
const ROOT = [
|
|
36
|
+
'inline-flex w-fit max-w-[min(100%,560px)] min-h-[44px] min-w-0 flex-wrap items-center',
|
|
37
|
+
'gap-x-2 gap-y-1',
|
|
38
|
+
'rounded-lg border border-solid p-3',
|
|
39
|
+
'font-[inherit]',
|
|
40
|
+
].join(' ');
|
|
41
|
+
|
|
42
|
+
const ICON_WRAP = 'ml-1 inline-flex shrink-0 items-center';
|
|
43
|
+
const MESSAGE = 'min-w-0 shrink text-sm [font-weight:var(--font-semibold)] leading-5 text-foreground whitespace-pre-line break-words';
|
|
44
|
+
const ACTIONS_ROW = 'flex shrink-0 flex-wrap items-center gap-x-2';
|
|
45
|
+
const ACTION_BTN_CLASS =
|
|
46
|
+
'!h-auto min-h-0 py-0.5 px-0 -my-0.5 !text-sm [font-weight:var(--font-semibold)] leading-5';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Toast — 轻量反馈条(Tailwind 内联)
|
|
50
|
+
*
|
|
51
|
+
* HiUI 风格四态反馈条:图标 + 主文案 + 可选文字操作 + 关闭;仅单条视觉,不含全局队列与 Portal。
|
|
52
|
+
* 根为 `w-fit max-w-[min(100%,560px)]`,图标与文案及操作与关闭之间水平间距 8px(`gap-x-2`)。
|
|
53
|
+
*
|
|
54
|
+
* @prop {'info'|'success'|'warning'|'error'} [type='info'] — 语义类型
|
|
55
|
+
* @prop {string} [message] — 主文案(与 children 二选一,未传 children 时使用)
|
|
56
|
+
* @prop {import('react').ReactNode} [children] — 覆盖 message 的展示内容
|
|
57
|
+
* @prop {{ label: string, onClick?: () => void }[] | null} [actions=null] — 文字操作(建议 1~2 项,渲染为 Button text-brand sm)
|
|
58
|
+
* @prop {boolean} [bordered=true] — 是否显示语义色描边(false 时为 border-transparent)
|
|
59
|
+
* @prop {boolean} [showClose=true] — 是否允许展示关闭位(仍需传入 onClose 才渲染关闭按钮)
|
|
60
|
+
* @prop {() => void} [onClose] — 关闭回调
|
|
61
|
+
* @prop {string} [className=''] — 附加到根容器类名
|
|
62
|
+
*/
|
|
63
|
+
export default function Toast({
|
|
64
|
+
type = 'info',
|
|
65
|
+
message,
|
|
66
|
+
children,
|
|
67
|
+
actions = null,
|
|
68
|
+
bordered = true,
|
|
69
|
+
showClose = true,
|
|
70
|
+
onClose,
|
|
71
|
+
className = '',
|
|
72
|
+
}) {
|
|
73
|
+
const bg = TYPE_BG[type] || TYPE_BG.info;
|
|
74
|
+
const stroke = bordered ? (TYPE_BORDER[type] || TYPE_BORDER.info) : BORDER_OFF;
|
|
75
|
+
const iconCfg = TYPE_ICON[type] || TYPE_ICON.info;
|
|
76
|
+
const role = type === 'error' ? 'alert' : 'status';
|
|
77
|
+
const live = type === 'error' ? 'assertive' : 'polite';
|
|
78
|
+
const text = children ?? message;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
className={[`tfds-toast`, [ROOT, bg, stroke, className].filter(Boolean).join(' ')].filter(Boolean).join(' ')}
|
|
83
|
+
role={role}
|
|
84
|
+
aria-live={live}
|
|
85
|
+
data-tfds-component="Toast">
|
|
86
|
+
<span className={`${ICON_WRAP} ${iconCfg.tint}`} aria-hidden>
|
|
87
|
+
<Icon name={iconCfg.name} size="md" />
|
|
88
|
+
</span>
|
|
89
|
+
<div className={MESSAGE}>{text}</div>
|
|
90
|
+
{Array.isArray(actions) && actions.length > 0 ? (
|
|
91
|
+
<div className={ACTIONS_ROW}>
|
|
92
|
+
{actions.map((a, i) => (
|
|
93
|
+
<Button
|
|
94
|
+
key={`${a.label}-${i}`}
|
|
95
|
+
type="button"
|
|
96
|
+
variant="text-brand"
|
|
97
|
+
size="sm"
|
|
98
|
+
className={ACTION_BTN_CLASS}
|
|
99
|
+
onClick={a.onClick}
|
|
100
|
+
>
|
|
101
|
+
{a.label}
|
|
102
|
+
</Button>
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
) : null}
|
|
106
|
+
{showClose && onClose ? (
|
|
107
|
+
<Button
|
|
108
|
+
type="button"
|
|
109
|
+
variant="ghost-black"
|
|
110
|
+
size="sm"
|
|
111
|
+
icon={<X size={16} strokeWidth={2} />}
|
|
112
|
+
iconOnly
|
|
113
|
+
onClick={onClose}
|
|
114
|
+
aria-label="关闭"
|
|
115
|
+
className="shrink-0 border-transparent bg-transparent shadow-none"
|
|
116
|
+
/>
|
|
117
|
+
) : null}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|