@endfield-ui/react 0.3.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 +69 -15
- package/dist/index.js +1102 -237
- 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 +9 -4
- 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/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/dropdown-menu/MenuCheck.tsx +4 -2
- package/src/components/dropdown-menu/menu-style.ts +13 -3
- package/src/components/empty-state/EmptyState.tsx +5 -0
- package/src/components/field/Field.tsx +39 -7
- package/src/components/file-upload/FileUpload.tsx +78 -19
- package/src/components/icon-button/IconButton.tsx +18 -5
- package/src/components/input/control-box.ts +23 -2
- package/src/components/input/value-chip.ts +3 -2
- package/src/components/loader/Loader.tsx +12 -41
- package/src/components/nav-menu/NavMenu.tsx +3 -2
- package/src/components/otp-input/OtpInput.tsx +8 -2
- 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 +21 -4
- 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 +18 -6
- 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/toc/Toc.tsx +89 -7
- package/src/components/toolbar/Toolbar.tsx +24 -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 -1
- package/src/lib/cn.ts +1 -0
- package/src/lib/inline-style.ts +12 -0
- package/src/lib/motion.ts +75 -0
- package/src/styles/theme.css +38 -2
package/src/components/README.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
| `chip/` | `FilterChip`、`ResourceChip` | [数据展示](../../../../docs/design/components/data-display.md) |
|
|
11
11
|
| `countdown/` | `Countdown` | [数据展示](../../../../docs/design/components/data-display.md) |
|
|
12
12
|
| `stat/` | `Stat` | [数据展示](../../../../docs/design/components/data-display.md) |
|
|
13
|
+
| `rolling-number/` | `RollingNumber`(从 0 滚到终值的数字,放进 `Stat` 的 `value`) | [数据展示](../../../../docs/design/components/data-display.md#滚动数字) |
|
|
13
14
|
| `sparkline/` | `Sparkline` | [数据展示](../../../../docs/design/components/data-display.md) |
|
|
14
15
|
| `data-row/` | `DataRowList`、`DataRow` | [数据展示](../../../../docs/design/components/data-display.md) |
|
|
15
16
|
| `avatar/` | `Avatar`、`AvatarSwitcher`、`AvatarSwitcherItem`(头像切换:一列头像里选一个) | [数据展示](../../../../docs/design/components/data-display.md)、[导航](../../../../docs/design/components/navigation.md) |
|
|
@@ -122,6 +123,14 @@ components/
|
|
|
122
123
|
- 随主题翻转的颜色一律用语义令牌(`surface`、`ink`、`action`…)。只有"有意不随主题变"的填充才直接用 `neutral-*`,并在旁边写一句注释说明原因。
|
|
123
124
|
- 压在反转块(`surface-inverse`)上的强调记号用 `accent-ink-inverse`,压在页面表面上的用 `accent-ink`,都不直接用 `action`:它在浅色底上看不见。
|
|
124
125
|
- 悬停底用 `hover:bg-ink/5`,不用 `surface-sunken`——后者在暗色主题下是纯黑。
|
|
126
|
+
- **只靠底色或阴影说"选中 / 当前"的地方,要另写一份高对比模式(`forced-colors:`)下的画法。** 系统会把作者写的底色都换成页面色、阴影不画:填充反转的块改成 `forced-colors:bg-[Highlight]` 加 `forced-colors:text-[HighlightText]`,底色画的记号改成 `forced-colors:bg-[Highlight]`。系统色关键字只写在 `forced-colors:` 变体里,是"只用令牌"的例外。字压在 `Highlight` 上的那个元素还要加 `forced-colors:forced-color-adjust-none`(不然浏览器会在字后面垫一块页面色),并把它的边线、焦点环一起写成系统色。对应关系和理由见 [色彩](../../../../docs/design/foundations/color.md#高对比模式)。
|
|
127
|
+
- **全靠底色成形的部件,高对比模式下同样要另写**——不只是"选中"。整个是底色画的(滑块、进度条、加载指示、骨架)会直接不见;靠底色围出来的一块(框里的值块、步进器的格子、排期的条目)只剩里面的字。轨道和填上的那一段写成"空心对实心"(轨道 `forced-colors:outline forced-colors:-outline-offset-1`,填充 `forced-colors:bg-[Highlight]`),不靠两种系统色分;围出来的一块补一圈线(`forced-colors:border`)。控件自己的部件用 `ButtonText`,页面上的用 `CanvasText`。还有几件容易漏的:**禁用的字色**写在 `<label>`、`<span>`、`<output>` 上时系统会当普通字换掉,要写明 `GrayText`——禁用的 `<input>` 也要在它自己身上写(`forced-colors:disabled:text-[GrayText]`,[`controlElement`](input/control-box.ts) 里有),浏览器只认写了 `type` 的;**SVG 里写的颜色**(`fill-*`、`stroke-*`)系统不换,要自己写成系统色,直接写在图标 `<svg>` 上的 `text-*` 也一样(图标跟着走的只是继承来的字色);**占位用的透明边线**(只读的输入框)会被系统显形,写成 `forced-colors:border-[Canvas]`;**说着事的线别用 [`decor`](../lib/decor.ts)**,它在高对比模式下整个去掉(排期里"今天"的竖线)。
|
|
128
|
+
- **按钮这一类在高对比模式下的写法**(按钮、图标按钮、工具栏、分页条是照这几条改的):
|
|
129
|
+
- 外形是一圈线,不做成实心块——实心留给"选中"。线只在 `forced-colors:` 下补,平时的尺寸不动:自己要聚焦的元素用 `forced-colors:border`(`outline` 让焦点环占着),不聚焦的容器(带子、条)用 `forced-colors:outline forced-colors:-outline-offset-1` 画在内边距里。用了 `border` 的,伪元素往外补的点击区要跟着多补 1px(它是从线的里沿起算的)。
|
|
130
|
+
- 平时就是透明、靠外面一层围着的(工具栏里的钮),补外面那一层,不逐个补。
|
|
131
|
+
- **带状态的类盖得住单写的 `forced-colors:`**:`hover:`、`group-hover:`、`data-disabled:` 都多一层选择器,优先级更高或一样高。颜色会随状态换的地方,高对比模式下那几档各写一份(`forced-colors:group-hover:bg-[ButtonText]`、`forced-colors:data-disabled:text-[GrayText]`);能在组件里按属性分支的(`disabled ? … : …`),就只放其中一份,不靠优先级。
|
|
132
|
+
- **不是真的 `disabled` 的禁用**(去掉了地址的链接、工具栏里还走得到的钮)系统不会替它把字换灰,`GrayText` 要自己写。
|
|
133
|
+
- 放在控件里的 [`Spinner`](spinner/Spinner.tsx) 自己写的是页面的字色:要它和控件上的字一个颜色,用 `className` 换成 `forced-colors:bg-[ButtonText]`。
|
|
125
134
|
- 组件 API 用语义命名(`variant="action"`、`tone="danger"`),不把颜色名写进属性(不要 `yellow`)。
|
|
126
135
|
- **能当链接用的控件,`href` 之外同时收 `render`**(路由库的链接组件)。链接形态里的那个 `<a>` 换成 [`LinkElement`](link-element/LinkElement.tsx),判断写成 `href !== undefined || render !== undefined`;转给 Base UI 部件的(菜单项)用 [`defined()`](../lib/defined.ts) 去掉没有值的属性,免得把那个元素自己的地址盖掉。
|
|
127
136
|
- 变体写成 `Record<Variant, string>`,用 [`cn()`](../lib/cn.ts) 合并,使用方传入的 `className` 排在最后、可以覆盖内置类。
|
|
@@ -135,7 +144,7 @@ components/
|
|
|
135
144
|
- 用 `group` / `peer` 时给 `group` 起名字(`group/card` + `group-hover/card:`):不起名的 `group-hover:` 会被外层任何一个 `group` 的悬停带着走,控件一旦被放进别的可悬停容器就会出错。
|
|
136
145
|
- `z-index` 不在组件里写数值,用 [theme.css](../styles/theme.css) 里的层叠变量(`z-(--z-nav)`、`z-(--z-overlay)`、`z-(--z-toast)`、`z-(--z-loader)`)。只在自己内部排先后的(表格冻结的那一列)先用 `isolate` 围起来,再写小的数值。
|
|
137
146
|
- **会粘住不动的格子必须是不透明的**(表格冻结的首列):滚过去的内容会从半透明的底下面透出来。它的悬停底不能用 `bg-ink/5`,用 `color-mix()` 把同样的比例混进它所在的底色里,得到一个看上去一样的实色。
|
|
138
|
-
- **压在反转块上的焦点环用 `accent-ink-inverse
|
|
147
|
+
- **压在反转块上的焦点环用 `accent-ink-inverse`**(表格的标题带、月历的选中格);压在行动色上的用 `on-action`(全屏菜单的当前项、楔形页签的选中项、深色面板里的当前项)。普通的焦点色在亮色下是墨色、暗色下是黄色,正好和这两种底撞色。只有环画在块**里面**时才撞:画在外面的(分段选择、工具栏按下的开关钮)隔着 2px 压在页面上,不用换。换了颜色还要看它贴着的那条边外面是什么:`on-action` 是墨色,贴边的话在暗色页面、深色面板上和周围分不开,所以压在行动色上的环都再往里收 2px(`focus-visible:-outline-offset-4`),让环的两边都是黄。
|
|
139
148
|
|
|
140
149
|
### 母题与装饰
|
|
141
150
|
|
|
@@ -155,7 +164,7 @@ components/
|
|
|
155
164
|
- **一块固定深色的区域里还要放别的控件时,把它做成局部主题**(`data-theme="dark"`),里面照常用语义令牌;不要逐个写 `neutral-*`。弹窗的标题带、行带都是这样。
|
|
156
165
|
- **一块跟着主题翻、但总和页面相反的区域(反转块)里要放别的控件时,用 `data-theme="inverse"`**,里面照常用 `bg-surface text-ink`——取到的就是原来的 `surface-inverse` / `ink-inverse`,颜色不变。面板的标题带、表格的表头带、完成横幅都是这样。反转块里不要再嵌反转块。
|
|
157
166
|
- 进出场用基元给的 `data-starting-style` / `data-ending-style` 写 CSS 过渡(`data-starting-style:opacity-0`),不写关键帧。基元等过渡结束才卸载浮层。
|
|
158
|
-
-
|
|
167
|
+
- 没有边线、全靠底色和页面分开的浮层(文字提示)加一圈透明的边线:高对比模式下底色被系统覆盖,边线会自己显形。反过来,只为占位留着的透明边线不想让它显形,就写成 `Canvas`(见上面"全靠底色成形的部件"那一条)。
|
|
159
168
|
- **要画焦点环的元素不要写 `outline-none`。** Tailwind v4 里它把轮廓的线型定成 `none`,后面的 `focus-visible:outline-2` 也跟着画不出来(菜单的选项踩过)。`outline-none` 只留给"焦点环画在别处"的元素:输入框里的 `<input>`、下拉的触发按钮(环在外框上)、弹窗的面。
|
|
160
169
|
- 局部主题里的焦点环如果会画到局部主题之外(外扩 2px),颜色就压在了页面的底色上。轻提示是深色的一块,环是黄的,画到亮色页面上看不见——所以它用画在里面的 `focusRingInset`。
|
|
161
170
|
- 测试里查浮层用角色(`findByRole("dialog")`)。文字提示的内容同时存在于隐藏的描述里,按文字查时要排除 `[hidden]`。
|
|
@@ -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,9 +17,14 @@ 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 =
|
|
@@ -30,4 +35,4 @@ export const choiceFrame =
|
|
|
30
35
|
* 淡入淡出,和方格换底色的那一下同时走——底色有过渡而记号硬切,看起来是两件事
|
|
31
36
|
*/
|
|
32
37
|
export const choiceMark =
|
|
33
|
-
"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";
|
|
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
|
);
|
|
@@ -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
|
|