@endfield-ui/react 0.2.0 → 1.0.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/NOTICE.md +2 -2
- package/README.md +103 -8
- package/dist/index.d.ts +78 -17
- package/dist/index.js +1184 -264
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/README.md +11 -2
- package/src/components/alert/Alert.tsx +63 -1
- package/src/components/avatar/Avatar.tsx +5 -2
- package/src/components/bracket-title/BracketTitle.tsx +32 -3
- package/src/components/button/Button.tsx +37 -5
- package/src/components/calendar/Calendar.tsx +49 -5
- package/src/components/checkbox/Checkbox.tsx +1 -0
- package/src/components/checkbox/choice-style.ts +13 -5
- package/src/components/chip/capsule-style.ts +18 -3
- package/src/components/combobox/Combobox.tsx +10 -0
- package/src/components/completion-banner/CompletionBanner.tsx +30 -2
- package/src/components/corner-brackets/CornerBrackets.tsx +2 -1
- package/src/components/dash-indicator/DashIndicator.tsx +5 -2
- package/src/components/data-row/DataRow.tsx +2 -1
- package/src/components/date-picker/DateField.tsx +5 -1
- package/src/components/dialog/Dialog.tsx +4 -2
- package/src/components/dropdown-menu/MenuCheck.tsx +4 -2
- package/src/components/dropdown-menu/menu-style.ts +22 -5
- package/src/components/empty-state/EmptyState.tsx +5 -0
- package/src/components/field/Field.tsx +39 -7
- package/src/components/file-upload/FileUpload.tsx +79 -20
- package/src/components/icon-button/IconButton.tsx +18 -5
- package/src/components/image-viewer/ImageViewer.tsx +58 -18
- package/src/components/input/control-box.ts +23 -2
- package/src/components/input/value-chip.ts +3 -2
- package/src/components/item-slot/ItemSlot.tsx +2 -1
- package/src/components/loader/Loader.tsx +12 -41
- package/src/components/nav-menu/NavMenu.tsx +17 -3
- package/src/components/otp-input/OtpInput.tsx +13 -4
- package/src/components/page-header/PageHeader.tsx +39 -3
- package/src/components/pagination/Pagination.tsx +2 -0
- package/src/components/progress/Progress.tsx +10 -3
- package/src/components/progress/ProgressRing.tsx +19 -4
- package/src/components/radio/Radio.tsx +2 -0
- package/src/components/rolling-number/RollingNumber.tsx +125 -0
- package/src/components/rolling-number/index.ts +1 -0
- package/src/components/schedule/Schedule.tsx +5 -3
- package/src/components/segmented-control/SegmentedControl.tsx +25 -4
- package/src/components/skeleton/Skeleton.tsx +4 -1
- package/src/components/slider/Slider.tsx +26 -5
- package/src/components/sparkline/Sparkline.tsx +29 -5
- package/src/components/spinner/Spinner.tsx +6 -2
- package/src/components/stepper/Stepper.tsx +4 -0
- package/src/components/steps/Steps.tsx +30 -9
- package/src/components/switch/Switch.tsx +26 -2
- package/src/components/table/Table.tsx +33 -9
- package/src/components/tabs/Tabs.tsx +55 -9
- package/src/components/tag-input/TagInput.tsx +82 -43
- package/src/components/textarea/Textarea.tsx +2 -1
- package/src/components/timeline/Timeline.tsx +4 -3
- package/src/components/toast/Toast.tsx +18 -5
- package/src/components/toc/Toc.tsx +89 -7
- package/src/components/toolbar/Toolbar.tsx +24 -0
- package/src/components/tooltip/Tooltip.tsx +5 -1
- package/src/components/viewfinder/Viewfinder.tsx +9 -0
- package/src/hooks/README.md +8 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAddedKeys.ts +26 -0
- package/src/hooks/useFlip.ts +211 -0
- package/src/hooks/useIndicator.ts +135 -0
- package/src/hooks/useLingering.ts +122 -0
- package/src/hooks/usePresence.ts +100 -0
- package/src/hooks/useReducedMotion.ts +26 -0
- package/src/hooks/useTheme.ts +44 -4
- package/src/index.ts +1 -0
- package/src/lib/README.md +2 -0
- package/src/lib/cn.ts +3 -0
- package/src/lib/inline-style.ts +12 -0
- package/src/lib/motion.ts +90 -0
- package/src/styles/theme.css +81 -2
- package/src/styles/utilities.css +2 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentProps, ComponentType, ReactNode } from "react";
|
|
2
|
+
import { usePresence } from "../../hooks/usePresence";
|
|
2
3
|
import { Close } from "../../icons/Close";
|
|
3
4
|
import type { IconProps } from "../../icons/createIcon";
|
|
4
5
|
import { StatusDanger } from "../../icons/StatusDanger";
|
|
@@ -7,6 +8,7 @@ import { StatusSuccess } from "../../icons/StatusSuccess";
|
|
|
7
8
|
import { StatusWarning } from "../../icons/StatusWarning";
|
|
8
9
|
import { cn } from "../../lib/cn";
|
|
9
10
|
import { focusRing } from "../../lib/focus-ring";
|
|
11
|
+
import { mergeRefs } from "../../lib/merge-refs";
|
|
10
12
|
|
|
11
13
|
export type AlertTone = "info" | "success" | "warning" | "danger";
|
|
12
14
|
|
|
@@ -17,8 +19,15 @@ export type AlertProps = Omit<ComponentProps<"div">, "title"> & {
|
|
|
17
19
|
title?: ReactNode;
|
|
18
20
|
/** 右侧的一个操作,通常是 `text` 按钮 */
|
|
19
21
|
action?: ReactNode;
|
|
20
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* 传了就出现关闭图标。它只负责通知:显示与否由使用方决定——
|
|
24
|
+
* 要收起的过程,把 `open` 交给提示条;直接不渲染它就是立刻消失
|
|
25
|
+
*/
|
|
21
26
|
onClose?: () => void;
|
|
27
|
+
/** 置为 `false` 时先收起、淡出,再卸载。默认 `true` */
|
|
28
|
+
open?: boolean;
|
|
29
|
+
/** 收完、已经卸载之后触发 */
|
|
30
|
+
onExited?: () => void;
|
|
22
31
|
/** 关闭图标的可访问名称 */
|
|
23
32
|
closeLabel?: string;
|
|
24
33
|
/** 替换默认图标;传 `null` 去掉 */
|
|
@@ -47,18 +56,64 @@ const toneText: Record<AlertTone, string> = {
|
|
|
47
56
|
danger: "text-danger",
|
|
48
57
|
};
|
|
49
58
|
|
|
59
|
+
/*
|
|
60
|
+
* 收起:量一次自己的高度,再把高度、上下的内边距和边线、外边距一起过渡到"它不在了"的样子。
|
|
61
|
+
* 父容器是纵向排的(flex 的列、grid)时,它一走还少一个行间距:下外边距收到负的行间距,
|
|
62
|
+
* 卸载的那一刻后面的内容才不会再跳一下。
|
|
63
|
+
* 不多包一层来做:`className` 是给带边线的这一层的。返回的函数把摆上去的撤掉
|
|
64
|
+
*/
|
|
65
|
+
function collapseOut(node: HTMLElement) {
|
|
66
|
+
const { style } = node;
|
|
67
|
+
const before = style.cssText;
|
|
68
|
+
|
|
69
|
+
let gap = 0;
|
|
70
|
+
if (node.parentElement) {
|
|
71
|
+
const layout = getComputedStyle(node.parentElement);
|
|
72
|
+
const stacked =
|
|
73
|
+
layout.display.includes("grid") ||
|
|
74
|
+
(layout.display.includes("flex") &&
|
|
75
|
+
layout.flexDirection.startsWith("column"));
|
|
76
|
+
if (stacked) gap = Number.parseFloat(layout.rowGap) || 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
style.height = `${node.getBoundingClientRect().height}px`;
|
|
80
|
+
// 先把起点落下去,下面这几样才是"变过去"的
|
|
81
|
+
node.getBoundingClientRect();
|
|
82
|
+
style.height = "0px";
|
|
83
|
+
style.minHeight = "0px";
|
|
84
|
+
style.paddingBlock = "0px";
|
|
85
|
+
style.borderBlockWidth = "0px";
|
|
86
|
+
style.marginTop = "0px";
|
|
87
|
+
style.marginBottom = `${-gap}px`;
|
|
88
|
+
style.opacity = "0";
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
style.cssText = before;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
50
95
|
/** 页面内的一条常驻消息。需要用户处理的重要信息用它,不用会消失的轻提示。 */
|
|
51
96
|
export function Alert({
|
|
52
97
|
tone = "info",
|
|
53
98
|
title,
|
|
54
99
|
action,
|
|
55
100
|
onClose,
|
|
101
|
+
open = true,
|
|
102
|
+
onExited,
|
|
56
103
|
closeLabel = "关闭",
|
|
57
104
|
icon,
|
|
58
105
|
className,
|
|
106
|
+
ref,
|
|
59
107
|
children,
|
|
60
108
|
...props
|
|
61
109
|
}: AlertProps) {
|
|
110
|
+
const {
|
|
111
|
+
ref: rootRef,
|
|
112
|
+
mounted,
|
|
113
|
+
leaving,
|
|
114
|
+
} = usePresence<HTMLDivElement>(open, { onLeave: collapseOut, onExited });
|
|
115
|
+
if (!mounted) return null;
|
|
116
|
+
|
|
62
117
|
const Icon = toneIcon[tone];
|
|
63
118
|
const graphic = icon === undefined ? <Icon size={20} /> : icon;
|
|
64
119
|
|
|
@@ -66,10 +121,17 @@ export function Alert({
|
|
|
66
121
|
<div
|
|
67
122
|
role={tone === "danger" || tone === "warning" ? "alert" : "status"}
|
|
68
123
|
{...props}
|
|
124
|
+
ref={mergeRefs(rootRef, ref)}
|
|
69
125
|
data-tone={tone}
|
|
126
|
+
// 正在收起的提示条点不到、Tab 不进去
|
|
127
|
+
inert={leaving}
|
|
70
128
|
className={cn(
|
|
71
129
|
"flex items-start gap-3 border border-l-4 border-line bg-surface-raised py-3 pr-3 pl-3 text-sm text-ink",
|
|
72
130
|
toneBar[tone],
|
|
131
|
+
leaving && [
|
|
132
|
+
"pointer-events-none overflow-hidden",
|
|
133
|
+
"transition-[height,padding,margin,border-width,opacity] duration-(--duration-fast) ease-standard",
|
|
134
|
+
],
|
|
73
135
|
className,
|
|
74
136
|
)}
|
|
75
137
|
>
|
|
@@ -31,10 +31,13 @@ const sizeClass: Record<AvatarSize, string> = {
|
|
|
31
31
|
|
|
32
32
|
/*
|
|
33
33
|
* 选中环:2px 的间隔 + 3px 的行动色 + 1px 的墨线。
|
|
34
|
-
*
|
|
34
|
+
* 墨线是给亮色页面的——黄色在白底上看不出来。
|
|
35
|
+
*
|
|
36
|
+
* 高对比模式下阴影不画:换成一圈系统"选中"色的边线,画在头像里面
|
|
37
|
+
* (外面那一圈轮廓留给头像切换的焦点环)
|
|
35
38
|
*/
|
|
36
39
|
export const avatarSelectedRing =
|
|
37
|
-
"shadow-[0_0_0_2px_var(--ef-surface),0_0_0_5px_var(--ef-action),0_0_0_6px_var(--ef-ink)]";
|
|
40
|
+
"shadow-[0_0_0_2px_var(--ef-surface),0_0_0_5px_var(--ef-action),0_0_0_6px_var(--ef-ink)] forced-colors:border-[3px] forced-colors:border-[Highlight]";
|
|
38
41
|
|
|
39
42
|
/** 中文名取第一个字;拉丁字母的名字取前两个词的首字母 */
|
|
40
43
|
export function initialsOf(name: string): string {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import type { ComponentProps, ElementType, ReactNode } from "react";
|
|
2
|
+
import { useInView } from "../../hooks/useInView";
|
|
2
3
|
import { cn } from "../../lib/cn";
|
|
4
|
+
import { mergeRefs } from "../../lib/merge-refs";
|
|
3
5
|
|
|
4
6
|
export type BracketTitleProps = Omit<ComponentProps<"h3">, "children"> & {
|
|
5
7
|
/** 标题层级,默认 3(`<h3>`) */
|
|
6
8
|
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
9
|
+
/** 进入视口时播放一次入场:括号先到,名称随后。默认开启 */
|
|
10
|
+
animate?: boolean;
|
|
7
11
|
/** 名称 */
|
|
8
12
|
children: ReactNode;
|
|
9
13
|
};
|
|
@@ -16,23 +20,48 @@ const bracket = "font-medium text-ink-tertiary";
|
|
|
16
20
|
*/
|
|
17
21
|
export function BracketTitle({
|
|
18
22
|
level = 3,
|
|
23
|
+
animate = true,
|
|
19
24
|
className,
|
|
20
25
|
children,
|
|
26
|
+
ref,
|
|
21
27
|
...props
|
|
22
28
|
}: BracketTitleProps) {
|
|
23
29
|
const Heading: ElementType = `h${level}`;
|
|
30
|
+
const [inViewRef, inView] = useInView<HTMLHeadingElement>({
|
|
31
|
+
disabled: !animate,
|
|
32
|
+
});
|
|
33
|
+
// 入场:括号先淡入,名称晚 100ms。只淡入、不位移——它们是排在同一行里的字,行内的字做不了位移
|
|
34
|
+
const pending = animate && !inView;
|
|
35
|
+
const playing = animate && inView;
|
|
36
|
+
const bracketEnter = cn(pending && "opacity-0", playing && "animate-fade-in");
|
|
24
37
|
|
|
25
38
|
return (
|
|
26
39
|
// 括号和名称排在同一行内而不是弹性盒里:名称换行时,右括号跟在最后一个字后面
|
|
27
40
|
<Heading
|
|
28
41
|
{...props}
|
|
42
|
+
ref={mergeRefs(inViewRef, ref)}
|
|
29
43
|
className={cn("text-4xl font-bold wrap-anywhere text-ink", className)}
|
|
30
44
|
>
|
|
31
|
-
<span
|
|
45
|
+
<span
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
className={cn(bracket, "mr-[0.4em]", bracketEnter)}
|
|
48
|
+
>
|
|
32
49
|
[
|
|
33
50
|
</span>
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
<span
|
|
52
|
+
className={
|
|
53
|
+
cn(
|
|
54
|
+
pending && "opacity-0",
|
|
55
|
+
playing && "animate-fade-in [animation-delay:100ms]",
|
|
56
|
+
) || undefined
|
|
57
|
+
}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</span>
|
|
61
|
+
<span
|
|
62
|
+
aria-hidden="true"
|
|
63
|
+
className={cn(bracket, "ml-[0.4em]", bracketEnter)}
|
|
64
|
+
>
|
|
36
65
|
]
|
|
37
66
|
</span>
|
|
38
67
|
</Heading>
|
|
@@ -139,6 +139,17 @@ const enabledClass: Record<ButtonVariant, string> = {
|
|
|
139
139
|
text: "text-ink active:text-ink-secondary",
|
|
140
140
|
};
|
|
141
141
|
|
|
142
|
+
/*
|
|
143
|
+
* 高对比模式(forced-colors)下底色被系统换成页面色,带底色的变体只剩一行字:
|
|
144
|
+
* 补一圈线,竖条、分隔线、加载中的方块这些底色画的记号改用按钮的字色。
|
|
145
|
+
* 禁用的线、字、记号都是灰字色——链接形态禁用时是一个去掉了地址的 <a>,
|
|
146
|
+
* 系统不会替它把字换灰,所以字色也写明
|
|
147
|
+
*/
|
|
148
|
+
const forcedOutline = "forced-colors:border";
|
|
149
|
+
const forcedEnabled = "forced-colors:border-[ButtonText]";
|
|
150
|
+
const forcedDisabled =
|
|
151
|
+
"forced-colors:border-[GrayText] forced-colors:text-[GrayText]";
|
|
152
|
+
|
|
142
153
|
export function Button(props: ButtonProps) {
|
|
143
154
|
const {
|
|
144
155
|
variant = "control",
|
|
@@ -158,6 +169,10 @@ export function Button(props: ButtonProps) {
|
|
|
158
169
|
const isText = variant === "text";
|
|
159
170
|
const marker = markerColor[variant];
|
|
160
171
|
const iconSize = inlineIconSize[size];
|
|
172
|
+
// 记号在高对比模式下的颜色:竖条、分隔线、占着记号位的旋转方块共用
|
|
173
|
+
const forcedMark = disabled
|
|
174
|
+
? "forced-colors:bg-[GrayText]"
|
|
175
|
+
: "forced-colors:bg-[ButtonText]";
|
|
161
176
|
|
|
162
177
|
const classes = cn(
|
|
163
178
|
"group relative inline-flex shrink-0 items-center justify-center font-medium leading-none whitespace-nowrap select-none",
|
|
@@ -173,6 +188,11 @@ export function Button(props: ButtonProps) {
|
|
|
173
188
|
? "cursor-not-allowed text-ink-disabled"
|
|
174
189
|
: "cursor-not-allowed bg-disabled text-on-disabled"
|
|
175
190
|
: enabledClass[variant],
|
|
191
|
+
// text 变体靠下划线成形,不补线;它的下划线跟着字色走,禁用时一起写明
|
|
192
|
+
!isText && forcedOutline,
|
|
193
|
+
disabled ? forcedDisabled : !isText && forcedEnabled,
|
|
194
|
+
// 加载中不是禁用:线和字照旧。链接形态这时候同样去掉了地址,字色写明,和线一个颜色
|
|
195
|
+
loading && !disabled && "forced-colors:text-[ButtonText]",
|
|
176
196
|
loading && "pointer-events-none",
|
|
177
197
|
block && "flex w-full",
|
|
178
198
|
className,
|
|
@@ -187,7 +207,7 @@ export function Button(props: ButtonProps) {
|
|
|
187
207
|
className="inline-flex items-center justify-center"
|
|
188
208
|
style={{ width: iconSize, height: iconSize }}
|
|
189
209
|
>
|
|
190
|
-
<Spinner label={null} className="size-2" />
|
|
210
|
+
<Spinner label={null} className={cn("size-2", forcedMark)} />
|
|
191
211
|
</span>
|
|
192
212
|
) : (
|
|
193
213
|
<ChevronLeft size={iconSize} />
|
|
@@ -199,7 +219,7 @@ export function Button(props: ButtonProps) {
|
|
|
199
219
|
className="inline-flex items-center justify-center"
|
|
200
220
|
style={{ width: iconSize, height: iconSize }}
|
|
201
221
|
>
|
|
202
|
-
<Spinner label={null} className="size-2" />
|
|
222
|
+
<Spinner label={null} className={cn("size-2", forcedMark)} />
|
|
203
223
|
</span>
|
|
204
224
|
) : (
|
|
205
225
|
<ArrowCorner
|
|
@@ -244,7 +264,10 @@ export function Button(props: ButtonProps) {
|
|
|
244
264
|
markerSpinner[size],
|
|
245
265
|
)}
|
|
246
266
|
>
|
|
247
|
-
<Spinner
|
|
267
|
+
<Spinner
|
|
268
|
+
label={null}
|
|
269
|
+
className={cn("block size-full", marker, forcedMark)}
|
|
270
|
+
/>
|
|
248
271
|
</span>
|
|
249
272
|
) : (
|
|
250
273
|
<span
|
|
@@ -253,6 +276,7 @@ export function Button(props: ButtonProps) {
|
|
|
253
276
|
"marker-bar pointer-events-none absolute top-1/2 -translate-y-1/2",
|
|
254
277
|
"transition-[clip-path,translate] duration-(--duration-base) ease-standard",
|
|
255
278
|
markerBox[size],
|
|
279
|
+
forcedMark,
|
|
256
280
|
disabled
|
|
257
281
|
? "bg-on-disabled"
|
|
258
282
|
: [
|
|
@@ -276,7 +300,12 @@ export function Button(props: ButtonProps) {
|
|
|
276
300
|
actionDivider[size],
|
|
277
301
|
disabled
|
|
278
302
|
? "bg-on-disabled/20"
|
|
279
|
-
:
|
|
303
|
+
: [
|
|
304
|
+
"bg-on-action/10 group-hover:bg-on-action/15 group-active:bg-on-action/20",
|
|
305
|
+
// 悬停、按下那两档带着状态,优先级比单写的高:高对比模式下各写一份
|
|
306
|
+
"forced-colors:group-hover:bg-[ButtonText] forced-colors:group-active:bg-[ButtonText]",
|
|
307
|
+
],
|
|
308
|
+
forcedMark,
|
|
280
309
|
)}
|
|
281
310
|
/>
|
|
282
311
|
{loading && (
|
|
@@ -286,7 +315,10 @@ export function Button(props: ButtonProps) {
|
|
|
286
315
|
actionSpinner[size],
|
|
287
316
|
)}
|
|
288
317
|
>
|
|
289
|
-
<Spinner
|
|
318
|
+
<Spinner
|
|
319
|
+
label={null}
|
|
320
|
+
className={cn("block size-full", forcedMark)}
|
|
321
|
+
/>
|
|
290
322
|
</span>
|
|
291
323
|
)}
|
|
292
324
|
</>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useEffect,
|
|
3
3
|
useId,
|
|
4
|
+
useLayoutEffect,
|
|
4
5
|
useMemo,
|
|
5
6
|
useRef,
|
|
6
7
|
useState,
|
|
@@ -202,6 +203,37 @@ export function Calendar({
|
|
|
202
203
|
?.focus();
|
|
203
204
|
}, [focused, month]);
|
|
204
205
|
|
|
206
|
+
// 翻月:新的一个月从翻的那一侧来。行是按日期当 key 的,相邻月份重叠的那几行是同一批元素
|
|
207
|
+
// (焦点靠这个留得住),所以不重挂——在日期那一块上标一下方向,撤了再标就重播
|
|
208
|
+
const turned = useRef({ month, until: 0, length: 0 });
|
|
209
|
+
useLayoutEffect(() => {
|
|
210
|
+
const last = turned.current;
|
|
211
|
+
if (last.month === month) return;
|
|
212
|
+
const forward = month > last.month;
|
|
213
|
+
last.month = month;
|
|
214
|
+
const grid = gridRef.current;
|
|
215
|
+
if (!grid) return;
|
|
216
|
+
|
|
217
|
+
const now = performance.now();
|
|
218
|
+
delete grid.dataset.turn;
|
|
219
|
+
// 上一次的还没播完又翻了(按住 PageDown、连点翻月钮):这一次不播,直接换。
|
|
220
|
+
// 每次都从透明起的话,格子会一直闪。一直连着翻就一直不播
|
|
221
|
+
if (now < last.until) {
|
|
222
|
+
last.until = now + last.length;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
// 先让上一次的撤干净:同一个方向连着翻,标记没变也要重播
|
|
226
|
+
void grid.offsetWidth;
|
|
227
|
+
grid.dataset.turn = forward ? "next" : "previous";
|
|
228
|
+
// 多久算"还没播完":问刚起的这一个动画,不另抄一份时长
|
|
229
|
+
const running =
|
|
230
|
+
typeof grid.getAnimations === "function"
|
|
231
|
+
? grid.getAnimations()[0]
|
|
232
|
+
: undefined;
|
|
233
|
+
last.length = Number(running?.effect?.getComputedTiming().endTime ?? 0);
|
|
234
|
+
last.until = now + last.length;
|
|
235
|
+
}, [month]);
|
|
236
|
+
|
|
205
237
|
const weeks = useMemo(
|
|
206
238
|
() => monthGrid(month, weekStartsOn),
|
|
207
239
|
[month, weekStartsOn],
|
|
@@ -331,6 +363,8 @@ export function Calendar({
|
|
|
331
363
|
</thead>
|
|
332
364
|
<tbody
|
|
333
365
|
ref={gridRef}
|
|
366
|
+
// 翻月时从翻的那一侧来 8px 并淡入;方向标在 data-turn 上(见上面的 turned)
|
|
367
|
+
className="data-turn:animate-shift-in data-[turn=next]:[--shift-x:var(--motion-shift-lg)] data-[turn=previous]:[--shift-x:calc(var(--motion-shift-lg)*-1)]"
|
|
334
368
|
onPointerLeave={() => setHovered(null)}
|
|
335
369
|
onKeyDown={(event) => {
|
|
336
370
|
// 起始日定了之后按 Esc:取消这一次,回到原来的那一段
|
|
@@ -400,10 +434,19 @@ export function Calendar({
|
|
|
400
434
|
"transition-colors duration-(--duration-fast) ease-standard",
|
|
401
435
|
focusRingInset,
|
|
402
436
|
endpoint
|
|
403
|
-
?
|
|
437
|
+
? // 焦点环画在实心块里面:普通的焦点色在亮色下就是这块墨的颜色,
|
|
438
|
+
// 暗色下是黄压在近白上,都看不见——换成反转块上的强调色。
|
|
439
|
+
// 高对比模式下底色被系统换掉:实心块改用系统的"选中"色。
|
|
440
|
+
// 这一格的颜色不让系统再改,不然它会在数字后面垫一块页面色。
|
|
441
|
+
// 画在里面的焦点环换成它上面的字色,再往里收 2px——贴着边的话,
|
|
442
|
+
// 环和页面一个颜色,看上去只是块小了一圈
|
|
443
|
+
"bg-surface-inverse font-bold text-ink-inverse focus-visible:outline-accent-ink-inverse forced-colors:forced-color-adjust-none forced-colors:bg-[Highlight] forced-colors:text-[HighlightText] forced-colors:focus-visible:-outline-offset-4 forced-colors:focus-visible:outline-[HighlightText]"
|
|
404
444
|
: [
|
|
405
|
-
//
|
|
406
|
-
|
|
445
|
+
// 一段的中间是连着的浅带;还没定结束日时是更浅的预览。
|
|
446
|
+
// 高对比模式下浅带没了,换成上下两条系统"选中"色的线——格子之间没有间距,连起来还是一条带子
|
|
447
|
+
between
|
|
448
|
+
? "bg-ink/10 forced-colors:border-y-2 forced-colors:border-[Highlight]"
|
|
449
|
+
: previewing && "bg-ink/5",
|
|
407
450
|
disabled
|
|
408
451
|
? "cursor-not-allowed text-ink-disabled line-through"
|
|
409
452
|
: [
|
|
@@ -416,10 +459,11 @@ export function Calendar({
|
|
|
416
459
|
// 今天:数字下面一条短线,不只是换个字色
|
|
417
460
|
isToday &&
|
|
418
461
|
"after:absolute after:bottom-1.5 after:left-1/2 after:h-0.5 after:w-3 after:-translate-x-1/2 after:content-['']",
|
|
462
|
+
// 高对比模式下这条线也是底色画的:换成所在那块底上的系统字色
|
|
419
463
|
isToday &&
|
|
420
464
|
(endpoint
|
|
421
|
-
? "after:bg-accent-ink-inverse"
|
|
422
|
-
: "after:bg-accent-ink"),
|
|
465
|
+
? "after:bg-accent-ink-inverse forced-colors:after:bg-[HighlightText]"
|
|
466
|
+
: "after:bg-accent-ink forced-colors:after:bg-[CanvasText]"),
|
|
423
467
|
)}
|
|
424
468
|
>
|
|
425
469
|
{dateParts(date).day}
|
|
@@ -69,6 +69,7 @@ export function Checkbox({
|
|
|
69
69
|
// 半选只属于复选框:一组单选都没选时也会命中 :indeterminate,
|
|
70
70
|
// 所以这两个类不能放进与单选共用的 choiceBox
|
|
71
71
|
"indeterminate:border-surface-inverse indeterminate:bg-surface-inverse",
|
|
72
|
+
"forced-colors:indeterminate:border-[Highlight] forced-colors:indeterminate:bg-[Highlight]",
|
|
72
73
|
focusRing,
|
|
73
74
|
)}
|
|
74
75
|
/>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export const choiceLabel =
|
|
7
|
-
"group inline-flex min-h-10 items-start gap-2 py-2 text-base text-ink has-disabled:cursor-not-allowed has-disabled:text-ink-disabled";
|
|
7
|
+
"group inline-flex min-h-10 items-start gap-2 py-2 text-base text-ink has-disabled:cursor-not-allowed has-disabled:text-ink-disabled forced-colors:has-disabled:text-[GrayText]";
|
|
8
8
|
|
|
9
9
|
/** 控件的定位盒:高度等于一行文字,标签换行时控件仍然对齐第一行 */
|
|
10
10
|
export const choiceControl = "flex h-[1lh] shrink-0 items-center";
|
|
@@ -17,14 +17,22 @@ export const choiceControl = "flex h-[1lh] shrink-0 items-center";
|
|
|
17
17
|
* 祖先上有 data-choice="diamond" 时换成菱形(方案 B):方格缩到 14px 并转 45°,
|
|
18
18
|
* 对角线正好还是 20px;未选是空心菱形,已选是实心的,不再画对勾和圆点——
|
|
19
19
|
* 所以禁用的"已选"也要保持实心,否则和"未选"分不出来。
|
|
20
|
+
*
|
|
21
|
+
* 高对比模式下底色都被系统换掉,反转块就没了:已选的改用系统的"选中"色,
|
|
22
|
+
* 记号用它上面的字色;禁用的用灰字色(菱形仍然是实心的)。
|
|
20
23
|
*/
|
|
21
|
-
export const choiceBox =
|
|
22
|
-
"peer size-5 shrink-0 appearance-none border-2 choice-diamond:size-3.5 choice-diamond:rotate-45 choice-diamond:checked:disabled:bg-ink-disabled border-line-strong bg-surface-sunken transition-colors duration-(--duration-fast) ease-standard group-hover:border-ink-secondary checked:border-surface-inverse checked:bg-surface-inverse aria-invalid:border-danger disabled:cursor-not-allowed disabled:border-ink-disabled disabled:bg-surface-sunken"
|
|
24
|
+
export const choiceBox = [
|
|
25
|
+
"peer size-5 shrink-0 appearance-none border-2 choice-diamond:size-3.5 choice-diamond:rotate-45 choice-diamond:checked:disabled:bg-ink-disabled border-line-strong bg-surface-sunken transition-colors duration-(--duration-fast) ease-standard group-hover:border-ink-secondary checked:border-surface-inverse checked:bg-surface-inverse aria-invalid:border-danger disabled:cursor-not-allowed disabled:border-ink-disabled disabled:bg-surface-sunken",
|
|
26
|
+
"forced-colors:checked:border-[Highlight] forced-colors:checked:bg-[Highlight] forced-colors:disabled:border-[GrayText] forced-colors:disabled:bg-[Canvas] forced-colors:choice-diamond:checked:disabled:bg-[GrayText]",
|
|
27
|
+
].join(" ");
|
|
23
28
|
|
|
24
29
|
/** 方格 / 圆的定位盒:菱形比它小一圈,所以居中 */
|
|
25
30
|
export const choiceFrame =
|
|
26
31
|
"relative inline-flex size-5 items-center justify-center";
|
|
27
32
|
|
|
28
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* 压在方格 / 圆正中的记号,默认透明,由 peer 的状态点亮。
|
|
35
|
+
* 淡入淡出,和方格换底色的那一下同时走——底色有过渡而记号硬切,看起来是两件事
|
|
36
|
+
*/
|
|
29
37
|
export const choiceMark =
|
|
30
|
-
"pointer-events-none absolute inset-0 m-auto text-accent-ink-inverse opacity-0 peer-disabled:text-ink-disabled";
|
|
38
|
+
"pointer-events-none absolute inset-0 m-auto text-accent-ink-inverse opacity-0 transition-opacity duration-(--duration-fast) ease-standard peer-disabled:text-ink-disabled forced-colors:text-[HighlightText] forced-colors:peer-disabled:text-[GrayText]";
|
|
@@ -13,10 +13,25 @@ export const capsuleSize: Record<CapsuleSize, string> = {
|
|
|
13
13
|
md: "h-8 px-4 text-sm",
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
/*
|
|
17
|
+
* 高对比模式下底色被系统换掉、透明的边线也显了形,选中的和没选的就成了一个样子:
|
|
18
|
+
* 选中的改用系统的"选中"色(禁用的是灰字色),连边线、焦点环一起写明,
|
|
19
|
+
* 并且不让系统再改这一颗的颜色——不然它会在字后面垫一块页面色,把"选中"色盖掉。
|
|
20
|
+
*/
|
|
21
|
+
const forcedSelected =
|
|
22
|
+
"forced-colors:forced-color-adjust-none forced-colors:focus-visible:outline-[Highlight]";
|
|
23
|
+
|
|
16
24
|
export const capsuleState = {
|
|
17
|
-
selected:
|
|
25
|
+
selected: [
|
|
26
|
+
"border-transparent bg-surface-inverse text-ink-inverse",
|
|
27
|
+
"forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
|
|
28
|
+
forcedSelected,
|
|
29
|
+
].join(" "),
|
|
18
30
|
rest: "border-line-strong text-ink hover:bg-ink/5",
|
|
19
31
|
disabled: "cursor-not-allowed border-line text-ink-disabled",
|
|
20
|
-
selectedDisabled:
|
|
32
|
+
selectedDisabled: [
|
|
21
33
|
"cursor-not-allowed border-transparent bg-disabled text-on-disabled",
|
|
22
|
-
|
|
34
|
+
"forced-colors:border-[GrayText] forced-colors:bg-[GrayText] forced-colors:text-[Canvas]",
|
|
35
|
+
forcedSelected,
|
|
36
|
+
].join(" "),
|
|
37
|
+
};
|
|
@@ -151,6 +151,13 @@ const currentHighlighted: Record<MenuVariant, string> = {
|
|
|
151
151
|
strong: "data-highlighted:bg-action-pressed",
|
|
152
152
|
};
|
|
153
153
|
|
|
154
|
+
/*
|
|
155
|
+
* 高对比模式下底色都被系统换掉,"移到了哪一项"就看不见了:给被移到的那一项画一圈
|
|
156
|
+
* 系统"选中"色的环,和菜单、下拉选择里键盘走到的那一项同一个样子(那边是真的焦点环)
|
|
157
|
+
*/
|
|
158
|
+
const highlightedForced =
|
|
159
|
+
"forced-colors:data-highlighted:outline-2 forced-colors:data-highlighted:-outline-offset-2 forced-colors:data-highlighted:outline-[Highlight]";
|
|
160
|
+
|
|
154
161
|
const iconButton =
|
|
155
162
|
"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
163
|
|
|
@@ -240,6 +247,7 @@ export function Combobox(props: ComboboxProps) {
|
|
|
240
247
|
currentHighlighted[panelVariant],
|
|
241
248
|
]
|
|
242
249
|
: menuItemHover,
|
|
250
|
+
highlightedForced,
|
|
243
251
|
)
|
|
244
252
|
}
|
|
245
253
|
render={
|
|
@@ -306,6 +314,8 @@ export function Combobox(props: ComboboxProps) {
|
|
|
306
314
|
valueChip,
|
|
307
315
|
"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus",
|
|
308
316
|
"data-disabled:text-ink-disabled",
|
|
317
|
+
// 高对比模式:系统不知道这一块禁用了,字和那一圈线都写明灰字色
|
|
318
|
+
"forced-colors:data-disabled:border-[GrayText] forced-colors:data-disabled:text-[GrayText]",
|
|
309
319
|
valueChipSize[size],
|
|
310
320
|
)}
|
|
311
321
|
>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ComponentProps, ElementType, ReactNode } from "react";
|
|
2
|
+
import { useInView } from "../../hooks/useInView";
|
|
2
3
|
import { cn } from "../../lib/cn";
|
|
4
|
+
import { mergeRefs } from "../../lib/merge-refs";
|
|
3
5
|
import { GhostText } from "../ghost-text/GhostText";
|
|
4
6
|
|
|
5
7
|
export type CompletionBannerProps = Omit<ComponentProps<"div">, "title"> & {
|
|
@@ -13,6 +15,8 @@ export type CompletionBannerProps = Omit<ComponentProps<"div">, "title"> & {
|
|
|
13
15
|
action?: ReactNode;
|
|
14
16
|
/** 标题层级,默认 3(`<h3>`) */
|
|
15
17
|
level?: 2 | 3 | 4 | 5 | 6;
|
|
18
|
+
/** 进入视口时播放一次入场:色带从左擦入,随后出字。默认开启 */
|
|
19
|
+
animate?: boolean;
|
|
16
20
|
};
|
|
17
21
|
|
|
18
22
|
/**
|
|
@@ -28,19 +32,28 @@ export function CompletionBanner({
|
|
|
28
32
|
description,
|
|
29
33
|
action,
|
|
30
34
|
level = 3,
|
|
35
|
+
animate = true,
|
|
31
36
|
className,
|
|
32
37
|
children,
|
|
38
|
+
ref,
|
|
33
39
|
...props
|
|
34
40
|
}: CompletionBannerProps) {
|
|
35
41
|
const Heading: ElementType = `h${level}`;
|
|
42
|
+
const [inViewRef, inView] = useInView<HTMLDivElement>({ disabled: !animate });
|
|
43
|
+
// 入场:整块从左擦入(背后的描边词在右下角,自然最后露出来),字和按钮等色带过去了再出
|
|
44
|
+
const pending = animate && !inView;
|
|
45
|
+
const playing = animate && inView;
|
|
36
46
|
|
|
37
47
|
return (
|
|
38
48
|
<div
|
|
39
49
|
role="status"
|
|
40
50
|
data-theme="inverse"
|
|
41
51
|
{...props}
|
|
52
|
+
ref={mergeRefs(inViewRef, ref)}
|
|
42
53
|
className={cn(
|
|
43
54
|
"@container relative isolate overflow-clip bg-surface text-ink",
|
|
55
|
+
pending && "[clip-path:inset(0_100%_0_0)]",
|
|
56
|
+
playing && "animate-wipe-in",
|
|
44
57
|
className,
|
|
45
58
|
)}
|
|
46
59
|
>
|
|
@@ -56,7 +69,13 @@ export function CompletionBanner({
|
|
|
56
69
|
)}
|
|
57
70
|
<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
71
|
{/* 强调色跟着这块底走:亮色页面上是黄字压墨底,暗色页面上是深黄字压近白底 */}
|
|
59
|
-
<div
|
|
72
|
+
<div
|
|
73
|
+
className={cn(
|
|
74
|
+
"min-w-0 border-l-4 border-accent-ink pl-3",
|
|
75
|
+
playing &&
|
|
76
|
+
"animate-shift-in [animation-delay:200ms] [--shift-x:calc(var(--motion-shift-lg)*-1)]",
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
60
79
|
<Heading className="text-lg font-bold wrap-anywhere text-accent-ink @md:text-xl">
|
|
61
80
|
{title}
|
|
62
81
|
</Heading>
|
|
@@ -65,7 +84,16 @@ export function CompletionBanner({
|
|
|
65
84
|
)}
|
|
66
85
|
{children}
|
|
67
86
|
</div>
|
|
68
|
-
{action &&
|
|
87
|
+
{action && (
|
|
88
|
+
<div
|
|
89
|
+
className={cn(
|
|
90
|
+
"shrink-0",
|
|
91
|
+
playing && "animate-fade-in [animation-delay:200ms]",
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{action}
|
|
95
|
+
</div>
|
|
96
|
+
)}
|
|
69
97
|
</div>
|
|
70
98
|
</div>
|
|
71
99
|
);
|
|
@@ -20,6 +20,7 @@ const sizeClass: Record<CornerBracketsSize, string> = {
|
|
|
20
20
|
* 用于矩阵里的单选(物品格、名册卡、图鉴);列表行用左缘色条,页签用底色,都不用它。
|
|
21
21
|
*
|
|
22
22
|
* 括号画在盒子之外 4px:它所在的容器不能贴着盒子裁切。
|
|
23
|
+
* 出现时八段短线从角的顶点伸到满长(200ms),消失是直接消失。
|
|
23
24
|
* 括号只是给眼睛看的,选中态另外要有 `aria-selected` / `aria-pressed`。
|
|
24
25
|
*/
|
|
25
26
|
export function CornerBrackets({
|
|
@@ -34,7 +35,7 @@ export function CornerBrackets({
|
|
|
34
35
|
data-visible={visible ? "" : undefined}
|
|
35
36
|
className={cn(
|
|
36
37
|
"relative",
|
|
37
|
-
visible && "corner-brackets",
|
|
38
|
+
visible && "corner-brackets after:animate-bracket-in",
|
|
38
39
|
sizeClass[size],
|
|
39
40
|
className,
|
|
40
41
|
)}
|
|
@@ -34,8 +34,11 @@ export function DashIndicator({
|
|
|
34
34
|
data-current={position === current ? "" : undefined}
|
|
35
35
|
className={cn(
|
|
36
36
|
"h-0.5 transition-[width,background-color] duration-(--duration-base) ease-standard",
|
|
37
|
-
//
|
|
38
|
-
|
|
37
|
+
// 当前一根除了换色还加长一倍,不只靠颜色。
|
|
38
|
+
// 高对比模式下底色被系统换掉,整排都没了:当前一根用系统的"选中"色,其余用页面的字色
|
|
39
|
+
position === current
|
|
40
|
+
? "w-8 bg-accent-ink forced-colors:bg-[Highlight]"
|
|
41
|
+
: "w-4 bg-line-strong forced-colors:bg-[CanvasText]",
|
|
39
42
|
)}
|
|
40
43
|
/>
|
|
41
44
|
))}
|
|
@@ -167,7 +167,8 @@ const toneClass: Record<DataRowTone, string> = {
|
|
|
167
167
|
};
|
|
168
168
|
|
|
169
169
|
const favoriteDot = "block size-4 rounded-full";
|
|
170
|
-
|
|
170
|
+
/* 高对比模式下底色被系统换掉,实心的圆就没了:改用系统的"选中"色 */
|
|
171
|
+
const favoriteOn = "bg-action forced-colors:bg-[Highlight]";
|
|
171
172
|
/* 未收藏是一个空心的圆环:形状也不同,不只靠颜色 */
|
|
172
173
|
const favoriteOff = "border-2 border-ink-tertiary";
|
|
173
174
|
|
|
@@ -123,7 +123,11 @@ export function DateField({
|
|
|
123
123
|
size={iconSize}
|
|
124
124
|
className={cn(
|
|
125
125
|
"shrink-0",
|
|
126
|
-
|
|
126
|
+
// 高对比模式:写在 <svg> 自己身上的颜色系统不换(它只替图标换继承来的字色),
|
|
127
|
+
// 主题里的灰会原样压在使用者定的页面色上。写明系统色,和按钮里的字一起走
|
|
128
|
+
disabled
|
|
129
|
+
? "text-ink-disabled forced-colors:text-[GrayText]"
|
|
130
|
+
: "text-ink-secondary forced-colors:text-[ButtonText]",
|
|
127
131
|
)}
|
|
128
132
|
/>
|
|
129
133
|
</BasePopover.Trigger>
|
|
@@ -116,8 +116,10 @@ export function Dialog({
|
|
|
116
116
|
className={cn(
|
|
117
117
|
overlaySurface,
|
|
118
118
|
"relative flex max-h-full min-h-0 w-full flex-col",
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// 进场:淡入(200ms),同时从下面升上来 8px(300ms)。退场只淡出——位移只写在起点上。
|
|
120
|
+
// 不放大:这套语言里没有东西是"变大着出来"的
|
|
121
|
+
"transition-[opacity,translate] duration-[var(--duration-fast),var(--duration-base)] ease-[var(--ease-standard),var(--ease-exit)]",
|
|
122
|
+
"data-ending-style:opacity-0 data-starting-style:translate-y-(--motion-shift-lg) data-starting-style:opacity-0",
|
|
121
123
|
sizeClass[size],
|
|
122
124
|
className,
|
|
123
125
|
)}
|