@endfield-ui/react 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/NOTICE.md +27 -0
- package/README.md +818 -2
- package/dist/index.d.ts +3445 -0
- package/dist/index.js +8706 -0
- package/dist/styles.css +2 -0
- package/package.json +83 -3
- package/src/components/README.md +179 -0
- package/src/components/accordion/Accordion.tsx +168 -0
- package/src/components/accordion/index.ts +8 -0
- package/src/components/alert/Alert.tsx +106 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/avatar/Avatar.tsx +92 -0
- package/src/components/avatar/AvatarSwitcher.tsx +261 -0
- package/src/components/avatar/index.ts +8 -0
- package/src/components/back-to-top/BackToTop.tsx +124 -0
- package/src/components/back-to-top/index.ts +5 -0
- package/src/components/badge/Badge.tsx +75 -0
- package/src/components/badge/index.ts +1 -0
- package/src/components/bracket-title/BracketTitle.tsx +40 -0
- package/src/components/bracket-title/index.ts +1 -0
- package/src/components/breadcrumb/Breadcrumb.tsx +161 -0
- package/src/components/breadcrumb/index.ts +6 -0
- package/src/components/button/Button.tsx +357 -0
- package/src/components/button/ButtonGroup.tsx +55 -0
- package/src/components/button/index.ts +13 -0
- package/src/components/calendar/Calendar.tsx +443 -0
- package/src/components/calendar/index.ts +7 -0
- package/src/components/carousel/Carousel.tsx +252 -0
- package/src/components/carousel/index.ts +7 -0
- package/src/components/carousel/snap-track.ts +74 -0
- package/src/components/checkbox/Checkbox.tsx +94 -0
- package/src/components/checkbox/choice-style.ts +30 -0
- package/src/components/checkbox/index.ts +1 -0
- package/src/components/chip/FilterChip.tsx +68 -0
- package/src/components/chip/ResourceChip.tsx +63 -0
- package/src/components/chip/capsule-style.ts +22 -0
- package/src/components/chip/index.ts +6 -0
- package/src/components/combobox/Combobox.tsx +450 -0
- package/src/components/combobox/index.ts +10 -0
- package/src/components/completion-banner/CompletionBanner.tsx +72 -0
- package/src/components/completion-banner/index.ts +4 -0
- package/src/components/context-menu/ContextMenu.tsx +70 -0
- package/src/components/context-menu/index.ts +5 -0
- package/src/components/corner-brackets/CornerBrackets.tsx +43 -0
- package/src/components/corner-brackets/index.ts +5 -0
- package/src/components/countdown/Countdown.tsx +145 -0
- package/src/components/countdown/index.ts +6 -0
- package/src/components/dash-indicator/DashIndicator.tsx +44 -0
- package/src/components/dash-indicator/index.ts +1 -0
- package/src/components/data-row/DataRow.tsx +282 -0
- package/src/components/data-row/index.ts +8 -0
- package/src/components/date-picker/DateField.tsx +159 -0
- package/src/components/date-picker/DatePicker.tsx +182 -0
- package/src/components/date-picker/DateRangePicker.tsx +203 -0
- package/src/components/date-picker/index.ts +7 -0
- package/src/components/dialog/Dialog.tsx +203 -0
- package/src/components/dialog/index.ts +7 -0
- package/src/components/dialog/initial-focus.ts +19 -0
- package/src/components/dialog/overlay-style.ts +28 -0
- package/src/components/drawer/Drawer.tsx +213 -0
- package/src/components/drawer/index.ts +6 -0
- package/src/components/dropdown-menu/DropdownMenu.tsx +427 -0
- package/src/components/dropdown-menu/MenuCheck.tsx +44 -0
- package/src/components/dropdown-menu/index.ts +22 -0
- package/src/components/dropdown-menu/menu-style.ts +47 -0
- package/src/components/empty-state/EmptyState.tsx +79 -0
- package/src/components/empty-state/index.ts +1 -0
- package/src/components/field/Field.tsx +203 -0
- package/src/components/field/index.ts +6 -0
- package/src/components/file-upload/FileItem.tsx +201 -0
- package/src/components/file-upload/FileUpload.tsx +425 -0
- package/src/components/file-upload/index.ts +9 -0
- package/src/components/flyout-bar/FlyoutBar.tsx +158 -0
- package/src/components/flyout-bar/index.ts +7 -0
- package/src/components/ghost-text/GhostText.tsx +47 -0
- package/src/components/ghost-text/index.ts +5 -0
- package/src/components/hatch/Hatch.tsx +35 -0
- package/src/components/hatch/index.ts +1 -0
- package/src/components/hazard-stripe/HazardStripe.tsx +35 -0
- package/src/components/hazard-stripe/index.ts +5 -0
- package/src/components/hover-card/HoverCard.tsx +101 -0
- package/src/components/hover-card/index.ts +6 -0
- package/src/components/icon-button/IconButton.tsx +95 -0
- package/src/components/icon-button/index.ts +6 -0
- package/src/components/image-viewer/ImageViewer.tsx +427 -0
- package/src/components/image-viewer/index.ts +7 -0
- package/src/components/input/Input.tsx +109 -0
- package/src/components/input/control-box.ts +71 -0
- package/src/components/input/index.ts +6 -0
- package/src/components/input/value-chip.ts +34 -0
- package/src/components/item-slot/ItemGrid.tsx +207 -0
- package/src/components/item-slot/ItemSlot.tsx +284 -0
- package/src/components/item-slot/index.ts +7 -0
- package/src/components/kbd/Kbd.tsx +33 -0
- package/src/components/kbd/index.ts +1 -0
- package/src/components/link-element/LinkElement.tsx +25 -0
- package/src/components/list/List.tsx +229 -0
- package/src/components/list/band-style.ts +18 -0
- package/src/components/list/index.ts +8 -0
- package/src/components/loader/Loader.tsx +154 -0
- package/src/components/loader/index.ts +1 -0
- package/src/components/marquee/Marquee.tsx +96 -0
- package/src/components/marquee/index.ts +1 -0
- package/src/components/media-card/MediaCard.tsx +139 -0
- package/src/components/media-card/index.ts +5 -0
- package/src/components/nav-action/NavAction.tsx +142 -0
- package/src/components/nav-action/index.ts +5 -0
- package/src/components/nav-menu/NavMenu.tsx +288 -0
- package/src/components/nav-menu/index.ts +6 -0
- package/src/components/navigator/Navigator.tsx +136 -0
- package/src/components/navigator/index.ts +5 -0
- package/src/components/otp-input/OtpInput.tsx +211 -0
- package/src/components/otp-input/index.ts +7 -0
- package/src/components/page-header/PageHeader.tsx +182 -0
- package/src/components/page-header/index.ts +6 -0
- package/src/components/pagination/Pagination.tsx +181 -0
- package/src/components/pagination/index.ts +5 -0
- package/src/components/panel/Panel.tsx +114 -0
- package/src/components/panel/index.ts +13 -0
- package/src/components/play-button/PlayButton.tsx +92 -0
- package/src/components/play-button/index.ts +7 -0
- package/src/components/popover/Popover.tsx +97 -0
- package/src/components/popover/index.ts +8 -0
- package/src/components/popover/panel-style.ts +15 -0
- package/src/components/progress/Progress.tsx +133 -0
- package/src/components/progress/ProgressRing.tsx +123 -0
- package/src/components/progress/index.ts +2 -0
- package/src/components/radio/Radio.tsx +182 -0
- package/src/components/radio/index.ts +7 -0
- package/src/components/rec-indicator/RecIndicator.tsx +47 -0
- package/src/components/rec-indicator/index.ts +1 -0
- package/src/components/registration-strip/RegistrationStrip.tsx +51 -0
- package/src/components/registration-strip/index.ts +5 -0
- package/src/components/schedule/Schedule.tsx +457 -0
- package/src/components/schedule/index.ts +9 -0
- package/src/components/scroll-area/ScrollArea.tsx +137 -0
- package/src/components/scroll-area/index.ts +5 -0
- package/src/components/scroll-hint/ScrollHint.tsx +78 -0
- package/src/components/scroll-hint/index.ts +5 -0
- package/src/components/section-title/SectionTitle.tsx +202 -0
- package/src/components/section-title/index.ts +5 -0
- package/src/components/segmented-control/SegmentedControl.tsx +220 -0
- package/src/components/segmented-control/index.ts +8 -0
- package/src/components/select/Select.tsx +390 -0
- package/src/components/select/index.ts +15 -0
- package/src/components/side-rail/SideRail.tsx +317 -0
- package/src/components/side-rail/SideRailSub.tsx +298 -0
- package/src/components/side-rail/index.ts +14 -0
- package/src/components/skeleton/Skeleton.tsx +70 -0
- package/src/components/skeleton/index.ts +1 -0
- package/src/components/slider/Slider.tsx +219 -0
- package/src/components/slider/index.ts +8 -0
- package/src/components/sparkline/Sparkline.tsx +122 -0
- package/src/components/sparkline/index.ts +6 -0
- package/src/components/spinner/Spinner.tsx +64 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/stat/Stat.tsx +79 -0
- package/src/components/stat/index.ts +1 -0
- package/src/components/stepper/Stepper.tsx +316 -0
- package/src/components/stepper/index.ts +1 -0
- package/src/components/steps/Steps.tsx +302 -0
- package/src/components/steps/index.ts +10 -0
- package/src/components/switch/Switch.tsx +120 -0
- package/src/components/switch/index.ts +1 -0
- package/src/components/table/Table.tsx +560 -0
- package/src/components/table/index.ts +20 -0
- package/src/components/tabs/Tabs.tsx +313 -0
- package/src/components/tabs/index.ts +12 -0
- package/src/components/tag/Tag.tsx +66 -0
- package/src/components/tag/TagPair.tsx +34 -0
- package/src/components/tag/index.ts +2 -0
- package/src/components/tag-input/TagInput.tsx +502 -0
- package/src/components/tag-input/index.ts +8 -0
- package/src/components/term/Term.tsx +53 -0
- package/src/components/term/index.ts +1 -0
- package/src/components/textarea/Textarea.tsx +99 -0
- package/src/components/textarea/index.ts +1 -0
- package/src/components/texture/Texture.tsx +45 -0
- package/src/components/texture/index.ts +1 -0
- package/src/components/tick-ring/TickRing.tsx +77 -0
- package/src/components/tick-ring/index.ts +1 -0
- package/src/components/timeline/Timeline.tsx +84 -0
- package/src/components/timeline/index.ts +7 -0
- package/src/components/toast/Toast.tsx +257 -0
- package/src/components/toast/index.ts +10 -0
- package/src/components/toc/Toc.tsx +263 -0
- package/src/components/toc/index.ts +7 -0
- package/src/components/toolbar/Toolbar.tsx +389 -0
- package/src/components/toolbar/index.ts +17 -0
- package/src/components/tooltip/Tooltip.tsx +175 -0
- package/src/components/tooltip/index.ts +8 -0
- package/src/components/top-bar/TopBar.tsx +47 -0
- package/src/components/top-bar/index.ts +1 -0
- package/src/components/viewfinder/Viewfinder.tsx +97 -0
- package/src/components/viewfinder/index.ts +6 -0
- package/src/hooks/README.md +14 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useControllableState.ts +30 -0
- package/src/hooks/useInView.ts +44 -0
- package/src/hooks/useOverflowing.ts +28 -0
- package/src/hooks/usePortalScope.ts +68 -0
- package/src/hooks/useScrollPosition.ts +57 -0
- package/src/hooks/useTheme.ts +102 -0
- package/src/icons/ArrowCorner.tsx +7 -0
- package/src/icons/ArrowLeft.tsx +6 -0
- package/src/icons/ArrowRight.tsx +6 -0
- package/src/icons/CalendarIcon.tsx +10 -0
- package/src/icons/Check.tsx +4 -0
- package/src/icons/ChevronDown.tsx +3 -0
- package/src/icons/ChevronLeft.tsx +6 -0
- package/src/icons/ChevronRight.tsx +6 -0
- package/src/icons/Close.tsx +3 -0
- package/src/icons/Crosshair.tsx +7 -0
- package/src/icons/Lock.tsx +6 -0
- package/src/icons/Menu.tsx +4 -0
- package/src/icons/Minus.tsx +4 -0
- package/src/icons/Plus.tsx +3 -0
- package/src/icons/README.md +31 -0
- package/src/icons/StatusDanger.tsx +7 -0
- package/src/icons/StatusInfo.tsx +7 -0
- package/src/icons/StatusSuccess.tsx +10 -0
- package/src/icons/StatusWarning.tsx +7 -0
- package/src/icons/TriangleRight.tsx +7 -0
- package/src/icons/Upload.tsx +7 -0
- package/src/icons/createIcon.tsx +34 -0
- package/src/icons/index.ts +21 -0
- package/src/index.ts +87 -0
- package/src/lib/README.md +17 -0
- package/src/lib/cn.ts +34 -0
- package/src/lib/date.ts +125 -0
- package/src/lib/decor.ts +6 -0
- package/src/lib/defined.ts +9 -0
- package/src/lib/file.ts +61 -0
- package/src/lib/focus-ring.ts +11 -0
- package/src/lib/merge-refs.ts +11 -0
- package/src/styles/README.md +70 -0
- package/src/styles/index.css +25 -0
- package/src/styles/theme.css +568 -0
- package/src/styles/utilities.css +241 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { Combobox as BaseCombobox } from "@base-ui/react/combobox";
|
|
2
|
+
import { useMemo, useRef, type ReactNode, type Ref } from "react";
|
|
3
|
+
import { usePortalScope } from "../../hooks/usePortalScope";
|
|
4
|
+
import { Close } from "../../icons/Close";
|
|
5
|
+
import { StatusDanger } from "../../icons/StatusDanger";
|
|
6
|
+
import { TriangleRight } from "../../icons/TriangleRight";
|
|
7
|
+
import { cn } from "../../lib/cn";
|
|
8
|
+
import { Spinner } from "../spinner/Spinner";
|
|
9
|
+
import { MenuCheck } from "../dropdown-menu/MenuCheck";
|
|
10
|
+
import {
|
|
11
|
+
menuGroupLabel,
|
|
12
|
+
menuItem,
|
|
13
|
+
menuItemCurrent,
|
|
14
|
+
menuItemHover,
|
|
15
|
+
menuPanel,
|
|
16
|
+
type MenuVariant,
|
|
17
|
+
} from "../dropdown-menu/menu-style";
|
|
18
|
+
import { useFieldControl } from "../field/Field";
|
|
19
|
+
import {
|
|
20
|
+
controlBox,
|
|
21
|
+
controlElement,
|
|
22
|
+
controlIconSize,
|
|
23
|
+
type ControlSize,
|
|
24
|
+
type ControlVariant,
|
|
25
|
+
} from "../input/control-box";
|
|
26
|
+
import {
|
|
27
|
+
multiValueBox,
|
|
28
|
+
valueChip,
|
|
29
|
+
valueChipRemove,
|
|
30
|
+
valueChipSize,
|
|
31
|
+
valueInputHeight,
|
|
32
|
+
} from "../input/value-chip";
|
|
33
|
+
|
|
34
|
+
export type ComboboxSize = ControlSize;
|
|
35
|
+
export type ComboboxVariant = ControlVariant;
|
|
36
|
+
|
|
37
|
+
export type ComboboxOption = {
|
|
38
|
+
value: string;
|
|
39
|
+
/** 显示的文字,也是检索的对象,所以必须是字符串 */
|
|
40
|
+
label: string;
|
|
41
|
+
/** 别名:检索时一并匹配,不显示。编号、缩写、旧称 */
|
|
42
|
+
keywords?: readonly string[];
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type ComboboxOptionGroup = {
|
|
47
|
+
/** 分组的小标题 */
|
|
48
|
+
label: string;
|
|
49
|
+
items: readonly ComboboxOption[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type ComboboxCommonProps = {
|
|
53
|
+
/** 选项,或者 `{ label, items }` 的分组。分组和不分组的不要混着传 */
|
|
54
|
+
items: readonly ComboboxOption[] | readonly ComboboxOptionGroup[];
|
|
55
|
+
/** 没选时输入框里的提示 */
|
|
56
|
+
placeholder?: string;
|
|
57
|
+
/** 表单字段名:选中的值会随表单提交 */
|
|
58
|
+
name?: string;
|
|
59
|
+
/**
|
|
60
|
+
* 外框的画法,和输入框相同:
|
|
61
|
+
* - `sunken` 凹陷的底 + 一条底边线,默认;
|
|
62
|
+
* - `outline` 四边描边,放在凹陷底色的区域里时用。
|
|
63
|
+
*/
|
|
64
|
+
variant?: ComboboxVariant;
|
|
65
|
+
/** 32 / 40 / 56px 高。多选时这是最小高度,已选项多了会长高 */
|
|
66
|
+
size?: ComboboxSize;
|
|
67
|
+
/**
|
|
68
|
+
* 面板的画法:
|
|
69
|
+
* - `plain` 跟随主题,默认;
|
|
70
|
+
* - `strong` 固定的深色面板,当前项整行黄底墨字。
|
|
71
|
+
*/
|
|
72
|
+
panelVariant?: MenuVariant;
|
|
73
|
+
/** 一个都没匹配上时面板里的说明 */
|
|
74
|
+
emptyText?: ReactNode;
|
|
75
|
+
/**
|
|
76
|
+
* 正在取选项:面板里出一行"正在查找…",这时不显示 `emptyText`。
|
|
77
|
+
* 已经拿到的选项照常显示
|
|
78
|
+
*/
|
|
79
|
+
loading?: boolean;
|
|
80
|
+
/** `loading` 时那一行的文字 */
|
|
81
|
+
loadingText?: ReactNode;
|
|
82
|
+
/**
|
|
83
|
+
* 关掉之后不在本地筛选:`items` 给什么就显示什么。
|
|
84
|
+
* 选项由服务器按输入的文字返回时用,配合 `onInputValueChange` 和 `loading`
|
|
85
|
+
*/
|
|
86
|
+
filter?: boolean;
|
|
87
|
+
/** 清除钮的可访问名称 */
|
|
88
|
+
clearLabel?: string;
|
|
89
|
+
/** 右侧三角按钮的可访问名称 */
|
|
90
|
+
toggleLabel?: string;
|
|
91
|
+
/** 错误态。放在 `Field` 里时跟随它的 `error` */
|
|
92
|
+
invalid?: boolean;
|
|
93
|
+
disabled?: boolean;
|
|
94
|
+
required?: boolean;
|
|
95
|
+
open?: boolean;
|
|
96
|
+
defaultOpen?: boolean;
|
|
97
|
+
onOpenChange?: (open: boolean) => void;
|
|
98
|
+
/** 输入框里的文字变了:要自己去取选项(远程检索)时用 */
|
|
99
|
+
onInputValueChange?: (text: string) => void;
|
|
100
|
+
id?: string;
|
|
101
|
+
"aria-label"?: string;
|
|
102
|
+
"aria-describedby"?: string;
|
|
103
|
+
/** 给外框 */
|
|
104
|
+
className?: string;
|
|
105
|
+
/** 给里面的 `<input>` */
|
|
106
|
+
ref?: Ref<HTMLInputElement>;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type ComboboxSingleProps = ComboboxCommonProps & {
|
|
110
|
+
multiple?: false;
|
|
111
|
+
value?: string | null;
|
|
112
|
+
defaultValue?: string | null;
|
|
113
|
+
/** 清除之后拿到的是 `null` */
|
|
114
|
+
onValueChange?: (value: string | null) => void;
|
|
115
|
+
removeLabel?: never;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type ComboboxMultipleProps = ComboboxCommonProps & {
|
|
119
|
+
/**
|
|
120
|
+
* 可以选多项:值是数组,已选项在框里排成一个个小块。
|
|
121
|
+
* 没打字直接选时面板不关;打了字搜出来再选,文字清空、面板关上
|
|
122
|
+
*/
|
|
123
|
+
multiple: true;
|
|
124
|
+
value?: string[];
|
|
125
|
+
defaultValue?: string[];
|
|
126
|
+
onValueChange?: (value: string[]) => void;
|
|
127
|
+
/** 小块上删除叉的可访问名称,拿到的是这一项的文字。默认"移除某某" */
|
|
128
|
+
removeLabel?: (label: string) => string;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export type ComboboxProps = ComboboxSingleProps | ComboboxMultipleProps;
|
|
132
|
+
|
|
133
|
+
const isGrouped = (
|
|
134
|
+
items: ComboboxCommonProps["items"],
|
|
135
|
+
): items is readonly ComboboxOptionGroup[] =>
|
|
136
|
+
items.length > 0 && "items" in items[0]!;
|
|
137
|
+
|
|
138
|
+
/** 单行时是固定高度;多选会换行,所以只定最小高度(`multiValueBox`) */
|
|
139
|
+
const boxSize: Record<ComboboxSize, string> = {
|
|
140
|
+
sm: "h-8 text-sm",
|
|
141
|
+
md: "h-10 text-base",
|
|
142
|
+
lg: "h-14 text-lg",
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* 焦点一直在输入框里,"移到了哪一项"只靠底色表示(没有焦点环可画)。
|
|
147
|
+
* 当前项本来就有底色,被移到时再深一档,否则看不出来。
|
|
148
|
+
*/
|
|
149
|
+
const currentHighlighted: Record<MenuVariant, string> = {
|
|
150
|
+
plain: "data-highlighted:bg-line",
|
|
151
|
+
strong: "data-highlighted:bg-action-pressed",
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const iconButton =
|
|
155
|
+
"flex shrink-0 items-center justify-center self-stretch text-ink-secondary hover:text-ink disabled:cursor-not-allowed disabled:text-ink-disabled disabled:hover:text-ink-disabled";
|
|
156
|
+
|
|
157
|
+
const byValue = (a: ComboboxOption | null, b: ComboboxOption | null) =>
|
|
158
|
+
a?.value === b?.value;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 组合框:一个能打字的下拉。选项多到要找的时候用它;只有几项时用 `Select`。
|
|
162
|
+
* 值只能是选项里有的——它不是"带建议的输入框"。
|
|
163
|
+
* 每个组合框都要有标签:放进 `Field`,或者自己传 `aria-label`。
|
|
164
|
+
*/
|
|
165
|
+
export function Combobox(props: ComboboxProps) {
|
|
166
|
+
const {
|
|
167
|
+
items,
|
|
168
|
+
placeholder,
|
|
169
|
+
name,
|
|
170
|
+
variant = "sunken",
|
|
171
|
+
size = "md",
|
|
172
|
+
panelVariant = "plain",
|
|
173
|
+
emptyText = "没有匹配的选项",
|
|
174
|
+
loading = false,
|
|
175
|
+
loadingText = "正在查找…",
|
|
176
|
+
filter: filterLocally = true,
|
|
177
|
+
clearLabel = "清除",
|
|
178
|
+
toggleLabel = "展开选项",
|
|
179
|
+
invalid: invalidProp,
|
|
180
|
+
disabled: disabledProp,
|
|
181
|
+
required: requiredProp,
|
|
182
|
+
open,
|
|
183
|
+
defaultOpen,
|
|
184
|
+
onOpenChange,
|
|
185
|
+
onInputValueChange,
|
|
186
|
+
id: idProp,
|
|
187
|
+
"aria-label": ariaLabel,
|
|
188
|
+
"aria-describedby": describedByProp,
|
|
189
|
+
className,
|
|
190
|
+
ref,
|
|
191
|
+
} = props;
|
|
192
|
+
const { id, disabled, required, invalid, ...field } = useFieldControl({
|
|
193
|
+
id: idProp,
|
|
194
|
+
disabled: disabledProp,
|
|
195
|
+
required: requiredProp,
|
|
196
|
+
invalid: invalidProp,
|
|
197
|
+
"aria-describedby": describedByProp,
|
|
198
|
+
});
|
|
199
|
+
const boxRef = useRef<HTMLDivElement>(null);
|
|
200
|
+
const { anchorRef, portalRef } = usePortalScope();
|
|
201
|
+
const iconSize = controlIconSize[size];
|
|
202
|
+
const multiple = props.multiple === true;
|
|
203
|
+
const grouped = isGrouped(items);
|
|
204
|
+
|
|
205
|
+
const options = useMemo(
|
|
206
|
+
() => (isGrouped(items) ? items.flatMap((group) => group.items) : items),
|
|
207
|
+
[items],
|
|
208
|
+
);
|
|
209
|
+
// 远程检索时 items 每次只是一部分:选中的那一项可能不在这一批里。
|
|
210
|
+
// 把见过的选项都记下来,值才一直找得到它的文字
|
|
211
|
+
const seen = useRef(new Map<string, ComboboxOption>());
|
|
212
|
+
for (const option of options) seen.current.set(option.value, option);
|
|
213
|
+
|
|
214
|
+
// 对外的值是字符串,基元要的是选项本身
|
|
215
|
+
const find = (value: string | null | undefined) =>
|
|
216
|
+
value == null ? null : (seen.current.get(value) ?? null);
|
|
217
|
+
const findAll = (values: readonly string[]) =>
|
|
218
|
+
values.flatMap((value) => seen.current.get(value) ?? []);
|
|
219
|
+
|
|
220
|
+
// 匹配文字和别名;不分大小写、不分全半角
|
|
221
|
+
const { contains } = BaseCombobox.useFilter({ sensitivity: "base" });
|
|
222
|
+
const filter = (option: ComboboxOption, query: string) =>
|
|
223
|
+
contains(option.label, query) ||
|
|
224
|
+
(option.keywords?.some((keyword) => contains(keyword, query)) ?? false);
|
|
225
|
+
|
|
226
|
+
const renderOption = (option: ComboboxOption) => (
|
|
227
|
+
<BaseCombobox.Item
|
|
228
|
+
key={option.value}
|
|
229
|
+
value={option}
|
|
230
|
+
disabled={option.disabled}
|
|
231
|
+
className={(state) =>
|
|
232
|
+
cn(
|
|
233
|
+
menuItem,
|
|
234
|
+
multiple
|
|
235
|
+
? // 多选时会有好几项被选中:用行首的小方格表达,不铺整行的底色
|
|
236
|
+
[menuItemHover, state.selected && "font-medium"]
|
|
237
|
+
: state.selected
|
|
238
|
+
? [
|
|
239
|
+
menuItemCurrent[panelVariant],
|
|
240
|
+
currentHighlighted[panelVariant],
|
|
241
|
+
]
|
|
242
|
+
: menuItemHover,
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
render={
|
|
246
|
+
multiple
|
|
247
|
+
? ({ children, ...itemProps }, state) => (
|
|
248
|
+
<div {...itemProps}>
|
|
249
|
+
<MenuCheck checked={state.selected} disabled={state.disabled} />
|
|
250
|
+
{children}
|
|
251
|
+
</div>
|
|
252
|
+
)
|
|
253
|
+
: undefined
|
|
254
|
+
}
|
|
255
|
+
>
|
|
256
|
+
<span className="min-w-0 flex-1 truncate">{option.label}</span>
|
|
257
|
+
</BaseCombobox.Item>
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
const input = (hasValue: boolean) => (
|
|
261
|
+
<BaseCombobox.Input
|
|
262
|
+
ref={ref}
|
|
263
|
+
id={id}
|
|
264
|
+
// 多选时已经有小块了,就不再显示提示
|
|
265
|
+
placeholder={hasValue ? undefined : placeholder}
|
|
266
|
+
aria-label={ariaLabel}
|
|
267
|
+
aria-invalid={invalid || undefined}
|
|
268
|
+
aria-describedby={field["aria-describedby"]}
|
|
269
|
+
className={cn(
|
|
270
|
+
controlElement,
|
|
271
|
+
multiple
|
|
272
|
+
? ["min-w-12 px-1", valueInputHeight[size]]
|
|
273
|
+
: "h-full pr-2 pl-3",
|
|
274
|
+
)}
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const content = (
|
|
279
|
+
<>
|
|
280
|
+
<div
|
|
281
|
+
ref={(node) => {
|
|
282
|
+
boxRef.current = node;
|
|
283
|
+
anchorRef(node);
|
|
284
|
+
}}
|
|
285
|
+
data-variant={variant}
|
|
286
|
+
data-size={size}
|
|
287
|
+
data-invalid={invalid ? "" : undefined}
|
|
288
|
+
className={cn(
|
|
289
|
+
controlBox({ variant, invalid, disabled, readOnly: false }),
|
|
290
|
+
"items-center",
|
|
291
|
+
multiple ? multiValueBox[size] : boxSize[size],
|
|
292
|
+
className,
|
|
293
|
+
)}
|
|
294
|
+
>
|
|
295
|
+
{props.multiple ? (
|
|
296
|
+
<BaseCombobox.Chips className="flex min-w-0 flex-1 flex-wrap items-center gap-1 py-1 pl-2">
|
|
297
|
+
<BaseCombobox.Value>
|
|
298
|
+
{(selected: ComboboxOption[]) => (
|
|
299
|
+
<>
|
|
300
|
+
{selected.map((option) => (
|
|
301
|
+
<BaseCombobox.Chip
|
|
302
|
+
key={option.value}
|
|
303
|
+
aria-label={option.label}
|
|
304
|
+
className={cn(
|
|
305
|
+
// 直角的小块:它是已经定下来的值,不是会自己变的状态
|
|
306
|
+
valueChip,
|
|
307
|
+
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus",
|
|
308
|
+
"data-disabled:text-ink-disabled",
|
|
309
|
+
valueChipSize[size],
|
|
310
|
+
)}
|
|
311
|
+
>
|
|
312
|
+
<span className="min-w-0 truncate">{option.label}</span>
|
|
313
|
+
<BaseCombobox.ChipRemove
|
|
314
|
+
aria-label={(
|
|
315
|
+
props.removeLabel ?? ((label) => `移除${label}`)
|
|
316
|
+
)(option.label)}
|
|
317
|
+
className={valueChipRemove}
|
|
318
|
+
>
|
|
319
|
+
<Close size={12} />
|
|
320
|
+
</BaseCombobox.ChipRemove>
|
|
321
|
+
</BaseCombobox.Chip>
|
|
322
|
+
))}
|
|
323
|
+
{input(selected.length > 0)}
|
|
324
|
+
</>
|
|
325
|
+
)}
|
|
326
|
+
</BaseCombobox.Value>
|
|
327
|
+
</BaseCombobox.Chips>
|
|
328
|
+
) : (
|
|
329
|
+
input(false)
|
|
330
|
+
)}
|
|
331
|
+
{invalid && (
|
|
332
|
+
<StatusDanger size={iconSize} className="shrink-0 text-danger" />
|
|
333
|
+
)}
|
|
334
|
+
<BaseCombobox.Clear
|
|
335
|
+
aria-label={clearLabel}
|
|
336
|
+
className={cn(iconButton, "w-7")}
|
|
337
|
+
>
|
|
338
|
+
<Close size={iconSize - 2} />
|
|
339
|
+
</BaseCombobox.Clear>
|
|
340
|
+
<BaseCombobox.Trigger
|
|
341
|
+
aria-label={toggleLabel}
|
|
342
|
+
className={cn(iconButton, "w-8 pr-1")}
|
|
343
|
+
>
|
|
344
|
+
<span className="flex rotate-90 transition-transform duration-(--duration-fast) ease-standard in-data-popup-open:-rotate-90">
|
|
345
|
+
<TriangleRight size={iconSize - 4} />
|
|
346
|
+
</span>
|
|
347
|
+
</BaseCombobox.Trigger>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<BaseCombobox.Portal ref={portalRef}>
|
|
351
|
+
<BaseCombobox.Positioner
|
|
352
|
+
anchor={boxRef}
|
|
353
|
+
side="bottom"
|
|
354
|
+
align="start"
|
|
355
|
+
sideOffset={4}
|
|
356
|
+
className="z-(--z-overlay) outline-none"
|
|
357
|
+
>
|
|
358
|
+
<BaseCombobox.Popup
|
|
359
|
+
data-theme={panelVariant === "strong" ? "dark" : undefined}
|
|
360
|
+
data-variant={panelVariant}
|
|
361
|
+
className={cn(
|
|
362
|
+
menuPanel,
|
|
363
|
+
"max-w-(--available-width) min-w-(--anchor-width)",
|
|
364
|
+
)}
|
|
365
|
+
>
|
|
366
|
+
{/*
|
|
367
|
+
* 这两行是播报给读屏的状态区,元素本身要一直在:空着的时候不占地方
|
|
368
|
+
* (内边距只在有内容时才有),而不是 display: none
|
|
369
|
+
*/}
|
|
370
|
+
<BaseCombobox.Status className="flex items-center gap-2 text-sm text-ink-secondary not-empty:px-3 not-empty:py-2">
|
|
371
|
+
{loading && (
|
|
372
|
+
<>
|
|
373
|
+
<Spinner size="sm" label={null} />
|
|
374
|
+
{loadingText}
|
|
375
|
+
</>
|
|
376
|
+
)}
|
|
377
|
+
</BaseCombobox.Status>
|
|
378
|
+
<BaseCombobox.Empty className="text-sm text-ink-secondary not-empty:px-3 not-empty:py-2">
|
|
379
|
+
{loading ? null : emptyText}
|
|
380
|
+
</BaseCombobox.Empty>
|
|
381
|
+
<BaseCombobox.List
|
|
382
|
+
aria-busy={loading || undefined}
|
|
383
|
+
className="max-h-[min(var(--available-height),20rem)] overflow-y-auto overscroll-contain empty:hidden"
|
|
384
|
+
>
|
|
385
|
+
{grouped
|
|
386
|
+
? (group: ComboboxOptionGroup) => (
|
|
387
|
+
<BaseCombobox.Group key={group.label} items={group.items}>
|
|
388
|
+
<BaseCombobox.GroupLabel className={menuGroupLabel}>
|
|
389
|
+
{group.label}
|
|
390
|
+
</BaseCombobox.GroupLabel>
|
|
391
|
+
<BaseCombobox.Collection>
|
|
392
|
+
{renderOption}
|
|
393
|
+
</BaseCombobox.Collection>
|
|
394
|
+
</BaseCombobox.Group>
|
|
395
|
+
)
|
|
396
|
+
: renderOption}
|
|
397
|
+
</BaseCombobox.List>
|
|
398
|
+
</BaseCombobox.Popup>
|
|
399
|
+
</BaseCombobox.Positioner>
|
|
400
|
+
</BaseCombobox.Portal>
|
|
401
|
+
</>
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const common = {
|
|
405
|
+
items,
|
|
406
|
+
filter: filterLocally ? filter : null,
|
|
407
|
+
name,
|
|
408
|
+
disabled,
|
|
409
|
+
required,
|
|
410
|
+
open,
|
|
411
|
+
defaultOpen,
|
|
412
|
+
onOpenChange: onOpenChange && ((next: boolean) => onOpenChange(next)),
|
|
413
|
+
onInputValueChange:
|
|
414
|
+
onInputValueChange && ((text: string) => onInputValueChange(text)),
|
|
415
|
+
isItemEqualToValue: byValue,
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
if (props.multiple) {
|
|
419
|
+
const { value, defaultValue, onValueChange } = props;
|
|
420
|
+
return (
|
|
421
|
+
<BaseCombobox.Root<ComboboxOption, true>
|
|
422
|
+
{...common}
|
|
423
|
+
multiple
|
|
424
|
+
value={value && findAll(value)}
|
|
425
|
+
defaultValue={defaultValue && findAll(defaultValue)}
|
|
426
|
+
onValueChange={
|
|
427
|
+
onValueChange &&
|
|
428
|
+
((next) => onValueChange(next.map((option) => option.value)))
|
|
429
|
+
}
|
|
430
|
+
>
|
|
431
|
+
{content}
|
|
432
|
+
</BaseCombobox.Root>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const { value, defaultValue, onValueChange } = props;
|
|
437
|
+
return (
|
|
438
|
+
<BaseCombobox.Root<ComboboxOption>
|
|
439
|
+
{...common}
|
|
440
|
+
// 受控时没选就是 null;undefined 留给"不受控"
|
|
441
|
+
value={value === undefined ? undefined : find(value)}
|
|
442
|
+
defaultValue={defaultValue === undefined ? undefined : find(defaultValue)}
|
|
443
|
+
onValueChange={
|
|
444
|
+
onValueChange && ((next) => onValueChange(next?.value ?? null))
|
|
445
|
+
}
|
|
446
|
+
>
|
|
447
|
+
{content}
|
|
448
|
+
</BaseCombobox.Root>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ComponentProps, ElementType, ReactNode } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
import { GhostText } from "../ghost-text/GhostText";
|
|
4
|
+
|
|
5
|
+
export type CompletionBannerProps = Omit<ComponentProps<"div">, "title"> & {
|
|
6
|
+
/** 一句话说明完成了什么:"第三阶段已完成" */
|
|
7
|
+
title: ReactNode;
|
|
8
|
+
/** 背后的巨型描边词,默认 `COMPLETED`。纯装饰;传 `null` 去掉 */
|
|
9
|
+
word?: ReactNode;
|
|
10
|
+
/** 标题下的一行说明 */
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
/** 右侧的一个行动:领取、查看下一阶段 */
|
|
13
|
+
action?: ReactNode;
|
|
14
|
+
/** 标题层级,默认 3(`<h3>`) */
|
|
15
|
+
level?: 2 | 3 | 4 | 5 | 6;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 完成横幅:一个阶段全部完成时,头部区域做一次明暗反转。
|
|
20
|
+
* 只在真正的里程碑上用;日常的"保存成功"用提示条就够了。
|
|
21
|
+
*
|
|
22
|
+
* 底是反转块:亮色主题下是深底黄字,暗色主题下是浅底深字。
|
|
23
|
+
* 整块是一个 `data-theme="inverse"` 的局部主题,`action` 里的按钮按这块底色取值。
|
|
24
|
+
*/
|
|
25
|
+
export function CompletionBanner({
|
|
26
|
+
title,
|
|
27
|
+
word = "COMPLETED",
|
|
28
|
+
description,
|
|
29
|
+
action,
|
|
30
|
+
level = 3,
|
|
31
|
+
className,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: CompletionBannerProps) {
|
|
35
|
+
const Heading: ElementType = `h${level}`;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
role="status"
|
|
40
|
+
data-theme="inverse"
|
|
41
|
+
{...props}
|
|
42
|
+
className={cn(
|
|
43
|
+
"@container relative isolate overflow-clip bg-surface text-ink",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
>
|
|
47
|
+
{word !== null && (
|
|
48
|
+
<GhostText
|
|
49
|
+
variant="outline"
|
|
50
|
+
// 描边比默认的重一些,在这块底上才看得出来。
|
|
51
|
+
// 贴右下角、向左淡出,不从标题和说明后面穿过;容器窄到放不下时去掉
|
|
52
|
+
className="absolute -right-2 -bottom-3 -z-10 hidden text-5xl [--ghost-ink:color-mix(in_srgb,var(--ef-ink)_28%,transparent)] [mask-image:linear-gradient(to_left,black_45%,transparent)] @md:block"
|
|
53
|
+
>
|
|
54
|
+
{word}
|
|
55
|
+
</GhostText>
|
|
56
|
+
)}
|
|
57
|
+
<div className="flex flex-wrap items-center justify-between gap-x-6 gap-y-3 px-4 py-4 @md:px-6 @md:py-5">
|
|
58
|
+
{/* 强调色跟着这块底走:亮色页面上是黄字压墨底,暗色页面上是深黄字压近白底 */}
|
|
59
|
+
<div className="min-w-0 border-l-4 border-accent-ink pl-3">
|
|
60
|
+
<Heading className="text-lg font-bold wrap-anywhere text-accent-ink @md:text-xl">
|
|
61
|
+
{title}
|
|
62
|
+
</Heading>
|
|
63
|
+
{description && (
|
|
64
|
+
<p className="mt-1 text-sm text-ink/70">{description}</p>
|
|
65
|
+
)}
|
|
66
|
+
{children}
|
|
67
|
+
</div>
|
|
68
|
+
{action && <div className="shrink-0">{action}</div>}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ContextMenu as BaseContextMenu } from "@base-ui/react/context-menu";
|
|
2
|
+
import type { ReactElement, ReactNode } from "react";
|
|
3
|
+
import { usePortalScope } from "../../hooks/usePortalScope";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import {
|
|
6
|
+
MenuVariantContext,
|
|
7
|
+
menuPanel,
|
|
8
|
+
type MenuVariant,
|
|
9
|
+
} from "../dropdown-menu/menu-style";
|
|
10
|
+
|
|
11
|
+
export type ContextMenuVariant = MenuVariant;
|
|
12
|
+
|
|
13
|
+
export type ContextMenuProps = {
|
|
14
|
+
/**
|
|
15
|
+
* 菜单里的内容:`DropdownMenuItem`、`DropdownMenuCheckboxItem`、
|
|
16
|
+
* `DropdownMenuGroup`、`DropdownMenuSeparator`、`DropdownMenuSub`
|
|
17
|
+
*/
|
|
18
|
+
menu: ReactNode;
|
|
19
|
+
/** 被右键的区域:一个能接收 `ref` 与事件的元素 */
|
|
20
|
+
children: ReactElement<Record<string, unknown>>;
|
|
21
|
+
/**
|
|
22
|
+
* - `plain` 跟随主题的面板,默认;
|
|
23
|
+
* - `strong` 固定的深色面板。
|
|
24
|
+
*/
|
|
25
|
+
variant?: ContextMenuVariant;
|
|
26
|
+
onOpenChange?: (open: boolean) => void;
|
|
27
|
+
/** 不再拦右键:回到浏览器自己的菜单 */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** 给面板 */
|
|
30
|
+
className?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 右键菜单:在一块区域上点右键(或按菜单键、触屏长按)时出现在指针处的下拉菜单。
|
|
35
|
+
* 它只是捷径——里面的操作在页面上必须另有入口。
|
|
36
|
+
*/
|
|
37
|
+
export function ContextMenu({
|
|
38
|
+
menu,
|
|
39
|
+
children,
|
|
40
|
+
variant = "plain",
|
|
41
|
+
onOpenChange,
|
|
42
|
+
disabled,
|
|
43
|
+
className,
|
|
44
|
+
}: ContextMenuProps) {
|
|
45
|
+
const { anchorRef, portalRef } = usePortalScope();
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<BaseContextMenu.Root
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
onOpenChange={onOpenChange && ((next) => onOpenChange(next))}
|
|
51
|
+
>
|
|
52
|
+
<BaseContextMenu.Trigger ref={anchorRef} render={children} />
|
|
53
|
+
<BaseContextMenu.Portal ref={portalRef}>
|
|
54
|
+
<BaseContextMenu.Positioner className="z-(--z-overlay) outline-none">
|
|
55
|
+
<BaseContextMenu.Popup
|
|
56
|
+
data-theme={variant === "strong" ? "dark" : undefined}
|
|
57
|
+
data-variant={variant}
|
|
58
|
+
className={cn(
|
|
59
|
+
menuPanel,
|
|
60
|
+
"max-h-(--available-height) overflow-y-auto overscroll-contain",
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<MenuVariantContext value={variant}>{menu}</MenuVariantContext>
|
|
65
|
+
</BaseContextMenu.Popup>
|
|
66
|
+
</BaseContextMenu.Positioner>
|
|
67
|
+
</BaseContextMenu.Portal>
|
|
68
|
+
</BaseContextMenu.Root>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { cn } from "../../lib/cn";
|
|
3
|
+
|
|
4
|
+
export type CornerBracketsSize = "sm" | "md";
|
|
5
|
+
|
|
6
|
+
export type CornerBracketsProps = ComponentProps<"div"> & {
|
|
7
|
+
/** 要不要画括号,默认画。用作选中态时把选中状态传进来 */
|
|
8
|
+
visible?: boolean;
|
|
9
|
+
/** 臂长 12px 或 16px */
|
|
10
|
+
size?: CornerBracketsSize;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const sizeClass: Record<CornerBracketsSize, string> = {
|
|
14
|
+
sm: "[--bracket-arm:12px]",
|
|
15
|
+
md: "[--bracket-arm:16px]",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 角括号:四角各一个 L 形短线,把里面的东西框出来。
|
|
20
|
+
* 用于矩阵里的单选(物品格、名册卡、图鉴);列表行用左缘色条,页签用底色,都不用它。
|
|
21
|
+
*
|
|
22
|
+
* 括号画在盒子之外 4px:它所在的容器不能贴着盒子裁切。
|
|
23
|
+
* 括号只是给眼睛看的,选中态另外要有 `aria-selected` / `aria-pressed`。
|
|
24
|
+
*/
|
|
25
|
+
export function CornerBrackets({
|
|
26
|
+
visible = true,
|
|
27
|
+
size = "sm",
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: CornerBracketsProps) {
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
{...props}
|
|
34
|
+
data-visible={visible ? "" : undefined}
|
|
35
|
+
className={cn(
|
|
36
|
+
"relative",
|
|
37
|
+
visible && "corner-brackets",
|
|
38
|
+
sizeClass[size],
|
|
39
|
+
className,
|
|
40
|
+
)}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|