@endfield-ui/react 1.0.1 → 1.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/NOTICE.md +33 -2
- package/README.md +60 -3
- package/dist/game/LICENSE.lucide +43 -0
- package/dist/game/assets/alert_banner_bg_left.png +0 -0
- package/dist/game/assets/alert_banner_bg_right.png +0 -0
- package/dist/game/assets/alert_bg.png +0 -0
- package/dist/game/assets/deco_common_logo_bg.png +0 -0
- package/dist/game/assets/detail.png +0 -0
- package/dist/game/assets/enemies_background.webp +0 -0
- package/dist/game/assets/enemies_background_dark.webp +0 -0
- package/dist/game/assets/header.webp +0 -0
- package/dist/game/assets/icon_card_rarity_1_bottom_edge.webp +0 -0
- package/dist/game/assets/icon_card_rarity_2_bottom_edge.webp +0 -0
- package/dist/game/assets/icon_card_rarity_3_bottom_edge.webp +0 -0
- package/dist/game/assets/icon_card_rarity_4_bottom_edge.webp +0 -0
- package/dist/game/assets/icon_card_rarity_5_bottom_edge.webp +0 -0
- package/dist/game/assets/icon_card_rarity_6_bottom_edge.webp +0 -0
- package/dist/game/assets/items_background.webp +0 -0
- package/dist/game/assets/items_background_dark.webp +0 -0
- package/dist/game/assets/link.webp +0 -0
- package/dist/game/assets/pag-button-texture.png +0 -0
- package/dist/game/assets/splash_bg.jpg +0 -0
- package/dist/game/assets/splash_bg_portrait.jpg +0 -0
- package/dist/game/assets/splash_deco.svg +4 -0
- package/dist/game/assets/splash_logo.svg +1 -0
- package/dist/game/assets/splash_triangles.svg +4 -0
- package/dist/game/assets/weapons_background.webp +0 -0
- package/dist/game/assets/weapons_background_dark.webp +0 -0
- package/dist/game/resource-provenance.json +453 -0
- package/dist/game/styles.css +2 -0
- package/dist/game.d.ts +384 -0
- package/dist/game.js +2069 -0
- package/dist/index.d.ts +42 -1
- package/dist/index.js +253 -30
- package/dist/styles.css +1 -1
- package/package.json +9 -2
- package/src/components/README.md +3 -0
- package/src/components/alert/Alert.tsx +1 -1
- package/src/components/date-picker/DateField.tsx +0 -2
- package/src/components/dropdown-menu/menu-style.ts +6 -1
- package/src/components/file-upload/FileUpload.tsx +5 -2
- package/src/components/input/control-box.ts +30 -10
- package/src/components/otp-input/OtpInput.tsx +35 -43
- package/src/components/segmented-control/SegmentedControl.tsx +2 -2
- package/src/components/select/Select.tsx +0 -2
- package/src/components/splash-screen/SplashScreen.tsx +340 -0
- package/src/components/splash-screen/index.ts +1 -0
- package/src/components/tag-input/TagInput.tsx +5 -2
- package/src/components/toast/Toast.tsx +4 -3
- package/src/index.ts +1 -0
- package/src/styles/README.md +9 -2
- package/src/styles/index.css +5 -0
- package/src/styles/theme.css +33 -0
- package/src/styles/utilities.css +8 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type ComponentProps,
|
|
8
|
+
type CSSProperties,
|
|
9
|
+
type KeyboardEvent,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { usePresence } from "../../hooks/usePresence";
|
|
13
|
+
import { cn } from "../../lib/cn";
|
|
14
|
+
import { decor as decorLayer } from "../../lib/decor";
|
|
15
|
+
import { mergeRefs } from "../../lib/merge-refs";
|
|
16
|
+
|
|
17
|
+
export type SplashScreenProps = Omit<ComponentProps<"div">, "children"> & {
|
|
18
|
+
/** 真实进度,0 – 100。不知道就不传:显示不确定进度,不显示数字 */
|
|
19
|
+
value?: number;
|
|
20
|
+
/** 标志:自己的图或 SVG。放在画面偏右的一个盒子里,竖排时居中 */
|
|
21
|
+
logo?: ReactNode;
|
|
22
|
+
/** 标志下面的一行标语 */
|
|
23
|
+
tagline?: ReactNode;
|
|
24
|
+
/** 标语上方那组微标里的微文字。纯氛围,对读屏隐藏 */
|
|
25
|
+
caption?: ReactNode;
|
|
26
|
+
/**
|
|
27
|
+
* 换掉标语上方的整组微标。不传是代码画的一组(小方框、`caption`、方点阵,旁边一枚三角标),
|
|
28
|
+
* 传 `null` 是不要
|
|
29
|
+
*/
|
|
30
|
+
decor?: ReactNode;
|
|
31
|
+
/** 读数下面的状态字。默认 `LOADING` */
|
|
32
|
+
status?: ReactNode;
|
|
33
|
+
/** 底图:铺满整块,随进度从模糊变清晰 */
|
|
34
|
+
backdrop?: ReactNode;
|
|
35
|
+
/** 置为 `false` 开始退场:走满、铺满、淡出,结束后卸载。默认 `true` */
|
|
36
|
+
open?: boolean;
|
|
37
|
+
/** 铺满视口并锁住页面(默认);关掉则铺满所在的定位容器 */
|
|
38
|
+
fullscreen?: boolean;
|
|
39
|
+
/** 进度条的可访问名称 */
|
|
40
|
+
label?: string;
|
|
41
|
+
/** 开始淡出、页面露出来的那一刻触发:页面自己的入场从这里起 */
|
|
42
|
+
onReveal?: () => void;
|
|
43
|
+
/** 淡出结束、已经卸载之后触发 */
|
|
44
|
+
onExited?: () => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* 进度的补间 0.5s(官网实测)。两个变量一起走:位置用小数的那个,读数用整数的那个。
|
|
49
|
+
* 退场时再加上整层的淡出:等 1.4s(走满 0.5s、铺满 0.6s、停一下)之后淡 1s。
|
|
50
|
+
*/
|
|
51
|
+
const progressTransition =
|
|
52
|
+
"[transition:--ef-splash-progress_500ms_var(--ease-exit),--ef-splash-percent_500ms_var(--ease-exit)]";
|
|
53
|
+
const leavingTransition =
|
|
54
|
+
"[transition:--ef-splash-progress_500ms_var(--ease-exit),--ef-splash-percent_500ms_var(--ease-exit),opacity_1s_var(--ease-standard)_1.4s]";
|
|
55
|
+
|
|
56
|
+
/** 三角标:三个朝下的小三角拼成一个倒三角 */
|
|
57
|
+
function Mark() {
|
|
58
|
+
return (
|
|
59
|
+
<svg
|
|
60
|
+
viewBox="0 0 24 21"
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
className={cn(
|
|
63
|
+
decorLayer,
|
|
64
|
+
"absolute -top-[calc(var(--ef-splash-unit)*2.5)] -left-[calc(var(--ef-splash-unit)*3.375)] h-[calc(var(--ef-splash-unit)*1.3125)] w-[calc(var(--ef-splash-unit)*1.4375)] fill-action cq-portrait:hidden",
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<path d="M0 0h11.6L5.8 10zM12.4 0H24l-5.8 10zM6.2 11h11.6L12 21z" />
|
|
68
|
+
</svg>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 代码画的那组微标:带框的加号、微文字、两排方点。至少占官网那张图的高度(4 个单位);
|
|
74
|
+
* 微文字有字号的下限,多出来的往上长。竖排时上面紧挨着进度条的读数,放不下微文字
|
|
75
|
+
*/
|
|
76
|
+
function DefaultDecor({ caption }: { caption: ReactNode }) {
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex min-h-[calc(var(--ef-splash-unit)*4)] flex-col items-start justify-between gap-[calc(var(--ef-splash-unit)*0.5)] text-neutral-600">
|
|
79
|
+
<svg
|
|
80
|
+
viewBox="0 0 12 12"
|
|
81
|
+
fill="none"
|
|
82
|
+
stroke="currentColor"
|
|
83
|
+
className="size-[calc(var(--ef-splash-unit)*1.125)]"
|
|
84
|
+
>
|
|
85
|
+
<path d="M.5.5h11v11H.5zM6 3v6M3 6h6" />
|
|
86
|
+
</svg>
|
|
87
|
+
{caption !== undefined && (
|
|
88
|
+
<p className="font-latin text-[length:max(calc(var(--ef-splash-unit)*0.75),var(--text-micro))] leading-[1.2] font-medium tracking-label whitespace-pre-line text-ink-tertiary uppercase cq-portrait:hidden">
|
|
89
|
+
{caption}
|
|
90
|
+
</p>
|
|
91
|
+
)}
|
|
92
|
+
{/* 画成一张图:方点只有半个单位大,一个个排成元素的话,小尺寸下间隔会忽宽忽窄 */}
|
|
93
|
+
<svg
|
|
94
|
+
viewBox="0 0 62 18"
|
|
95
|
+
fill="currentColor"
|
|
96
|
+
className="h-[calc(var(--ef-splash-unit)*1.125)] w-[calc(var(--ef-splash-unit)*3.875)]"
|
|
97
|
+
>
|
|
98
|
+
{[0, 11].flatMap((y) =>
|
|
99
|
+
[0, 11, 22, 33, 44, 55].map((x) => (
|
|
100
|
+
<rect key={`${x}-${y}`} x={x} y={y} width="7" height="7" />
|
|
101
|
+
)),
|
|
102
|
+
)}
|
|
103
|
+
</svg>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 开屏页:照官网首次载入的那一页排的。近黑底,一根贴边的黄色进度条,读数跟着条的末端走;
|
|
110
|
+
* 画面偏右是标志,下面一组微标、一条分隔线和一行标语。就绪后黄色铺满整屏,再整层淡出。
|
|
111
|
+
*
|
|
112
|
+
* 标志和底图是插槽,库里不带任何图。横竖看它所在的那一块自己的宽高比,不看视口;
|
|
113
|
+
* 尺寸跟着那一块的宽度等比缩放,插槽里的东西可以用 `--ef-splash-unit` 跟着缩。
|
|
114
|
+
*
|
|
115
|
+
* 只要一块盖住整页的等待、不要这些排场,用 `Loader`。
|
|
116
|
+
*/
|
|
117
|
+
export function SplashScreen({
|
|
118
|
+
value,
|
|
119
|
+
logo,
|
|
120
|
+
tagline,
|
|
121
|
+
caption,
|
|
122
|
+
decor,
|
|
123
|
+
status = "LOADING",
|
|
124
|
+
backdrop,
|
|
125
|
+
open = true,
|
|
126
|
+
fullscreen = true,
|
|
127
|
+
label = "加载中",
|
|
128
|
+
onReveal,
|
|
129
|
+
onExited,
|
|
130
|
+
onKeyDown,
|
|
131
|
+
className,
|
|
132
|
+
style,
|
|
133
|
+
ref,
|
|
134
|
+
...props
|
|
135
|
+
}: SplashScreenProps) {
|
|
136
|
+
// 页面露出来了没有:开始淡出的那一刻。在那之前这一层一直盖着、锁着
|
|
137
|
+
const [revealed, setRevealed] = useState(false);
|
|
138
|
+
if (open && revealed) setRevealed(false);
|
|
139
|
+
|
|
140
|
+
const handlers = useRef({ onReveal });
|
|
141
|
+
const fired = useRef(false);
|
|
142
|
+
useLayoutEffect(() => {
|
|
143
|
+
handlers.current = { onReveal };
|
|
144
|
+
if (open) fired.current = false;
|
|
145
|
+
});
|
|
146
|
+
const reveal = useCallback(() => {
|
|
147
|
+
if (fired.current) return;
|
|
148
|
+
fired.current = true;
|
|
149
|
+
setRevealed(true);
|
|
150
|
+
handlers.current.onReveal?.();
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
// 退出:淡出的过渡走完后卸载;重新打开立刻挂回来
|
|
154
|
+
const { ref: rootRef, mounted } = usePresence<HTMLDivElement>(open, {
|
|
155
|
+
onExited: () => {
|
|
156
|
+
// 没有过渡可等的环境里等不到"开始淡出":卸载之前补上
|
|
157
|
+
reveal();
|
|
158
|
+
onExited?.();
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const leaving = mounted && !open;
|
|
162
|
+
|
|
163
|
+
// 淡出排在铺满之后,等的是它真的开始的那一刻(延迟走完),不是退场的开头
|
|
164
|
+
useLayoutEffect(() => {
|
|
165
|
+
const node = rootRef.current;
|
|
166
|
+
if (!leaving || !node) return;
|
|
167
|
+
const onStart = (event: TransitionEvent) => {
|
|
168
|
+
if (event.target === node && event.propertyName === "opacity") reveal();
|
|
169
|
+
};
|
|
170
|
+
node.addEventListener("transitionstart", onStart);
|
|
171
|
+
return () => node.removeEventListener("transitionstart", onStart);
|
|
172
|
+
}, [leaving, rootRef, reveal]);
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
* 整页模式:锁住页面滚动,把焦点收进来。和 Loader 不一样,开始退场时不放开——
|
|
176
|
+
* 黄色铺满的那一段页面还完全被盖着,放开了只会点到看不见的东西。到开始淡出才放
|
|
177
|
+
*/
|
|
178
|
+
const blocking = mounted && (open || !revealed);
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!fullscreen || !blocking) return;
|
|
181
|
+
const page = document.documentElement;
|
|
182
|
+
const previousOverflow = page.style.overflow;
|
|
183
|
+
const previousFocus = document.activeElement;
|
|
184
|
+
page.style.overflow = "hidden";
|
|
185
|
+
rootRef.current?.focus({ preventScroll: true });
|
|
186
|
+
return () => {
|
|
187
|
+
page.style.overflow = previousOverflow;
|
|
188
|
+
if (previousFocus instanceof HTMLElement && previousFocus.isConnected) {
|
|
189
|
+
previousFocus.focus({ preventScroll: true });
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}, [fullscreen, blocking, rootRef]);
|
|
193
|
+
|
|
194
|
+
if (!mounted) return null;
|
|
195
|
+
|
|
196
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
197
|
+
onKeyDown?.(event);
|
|
198
|
+
// 焦点留在开屏页上,不让 Tab 走到被盖住的内容里
|
|
199
|
+
if (fullscreen && blocking && event.key === "Tab") event.preventDefault();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const indeterminate = value === undefined;
|
|
203
|
+
const current = indeterminate ? 0 : Math.min(Math.max(value, 0), 100);
|
|
204
|
+
// 退场先把条走满:就绪就是百分之百,铺满前的那半秒留给它
|
|
205
|
+
const shown = open ? current : 100;
|
|
206
|
+
const travelling = indeterminate && open;
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div
|
|
210
|
+
tabIndex={-1}
|
|
211
|
+
{...props}
|
|
212
|
+
ref={mergeRefs(rootRef, ref)}
|
|
213
|
+
// 固定的近黑底,不随页面的主题变:做成局部的暗色主题,插槽里照常用语义令牌
|
|
214
|
+
data-theme="dark"
|
|
215
|
+
data-state={open ? "open" : "closing"}
|
|
216
|
+
onKeyDown={handleKeyDown}
|
|
217
|
+
style={
|
|
218
|
+
{
|
|
219
|
+
...style,
|
|
220
|
+
"--ef-splash-progress": shown,
|
|
221
|
+
"--ef-splash-percent": Math.round(shown),
|
|
222
|
+
} as CSSProperties
|
|
223
|
+
}
|
|
224
|
+
className={cn(
|
|
225
|
+
"overflow-hidden bg-surface text-ink outline-none [container-type:size]",
|
|
226
|
+
fullscreen ? "fixed inset-0 z-(--z-loader)" : "absolute inset-0",
|
|
227
|
+
leaving ? leavingTransition : progressTransition,
|
|
228
|
+
leaving && "opacity-0",
|
|
229
|
+
!blocking && "pointer-events-none",
|
|
230
|
+
className,
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
{/*
|
|
234
|
+
* 一个单位是官网的 1rem:横排是宽度的 1/160,竖排是 1/67.5。
|
|
235
|
+
* 状态字和标语有字号的下限(官网在小屏上会缩到看不清),收成两个变量:
|
|
236
|
+
* 竖排时下面那一摞是从底边往上排的,条要按它们多占的高度往上让
|
|
237
|
+
*/}
|
|
238
|
+
<div className="absolute inset-0 [--_status:max(var(--ef-splash-unit),var(--text-xs))] [--_tagline:max(calc(var(--ef-splash-unit)*1.5),var(--text-xs))] [--ef-splash-unit:0.625cqw] cq-portrait:[--ef-splash-unit:1.4815cqw]">
|
|
239
|
+
{backdrop !== undefined && (
|
|
240
|
+
<div
|
|
241
|
+
aria-hidden="true"
|
|
242
|
+
className={cn(
|
|
243
|
+
decorLayer,
|
|
244
|
+
"absolute inset-0 blur-[calc((100_-_var(--ef-splash-progress))*0.08px)] *:size-full [&>img]:object-cover",
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
{backdrop}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{/*
|
|
252
|
+
* 纵向位置和官网一样按宽度定(标志在上 27.625 个单位,微标组的分隔线在 63.375),
|
|
253
|
+
* 所以 16:9 以外的比例下这一列不在正中。官网在超宽的屏上会掉出底边:这里放不下时
|
|
254
|
+
* 往上收,标语的底边至少离底 4 个单位。竖排的标志同理,不低于高度的 41.667%
|
|
255
|
+
*/}
|
|
256
|
+
{logo !== undefined && (
|
|
257
|
+
<div className="absolute top-[min(calc(var(--ef-splash-unit)*27.625),calc(100%_-_var(--ef-splash-unit)*44.25))] left-[64.4531%] flex h-[calc(var(--ef-splash-unit)*16.75)] w-[calc(var(--ef-splash-unit)*16.5)] items-center justify-center cq-portrait:top-[min(calc(var(--ef-splash-unit)*50),41.667%)] cq-portrait:left-1/2 cq-portrait:h-[calc(var(--ef-splash-unit)*20.9375)] cq-portrait:w-[calc(var(--ef-splash-unit)*19.3125)] cq-portrait:-translate-1/2 [&>:is(img,svg)]:size-full [&>img]:object-contain">
|
|
258
|
+
{logo}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
|
|
262
|
+
<div className="absolute top-[min(calc(var(--ef-splash-unit)*63.375),calc(100%_-_var(--ef-splash-unit)*8.5))] right-[calc(var(--ef-splash-unit)*2)] left-[64.4531%] cq-portrait:top-auto cq-portrait:right-[calc(var(--ef-splash-unit)*4.625)] cq-portrait:bottom-[calc(var(--ef-splash-unit)*5.75)] cq-portrait:left-[calc(var(--ef-splash-unit)*4.625)]">
|
|
263
|
+
{decor === undefined && <Mark />}
|
|
264
|
+
{decor !== null && (
|
|
265
|
+
<div
|
|
266
|
+
aria-hidden="true"
|
|
267
|
+
className={cn(decorLayer, "absolute bottom-full left-0")}
|
|
268
|
+
>
|
|
269
|
+
{decor === undefined ? <DefaultDecor caption={caption} /> : decor}
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
<span
|
|
273
|
+
aria-hidden="true"
|
|
274
|
+
className={cn(
|
|
275
|
+
decorLayer,
|
|
276
|
+
"mt-[calc(var(--ef-splash-unit)*1.25)] mb-[calc(var(--ef-splash-unit))] -ml-[calc(var(--ef-splash-unit)*21.625)] block h-0.5 w-[calc(var(--ef-splash-unit)*70)] bg-linear-to-r from-transparent via-neutral-0/10 to-transparent",
|
|
277
|
+
)}
|
|
278
|
+
/>
|
|
279
|
+
{tagline !== undefined && (
|
|
280
|
+
<p className="text-[length:var(--_tagline)] leading-[1.35] wrap-anywhere">
|
|
281
|
+
{tagline}
|
|
282
|
+
</p>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div
|
|
287
|
+
role="progressbar"
|
|
288
|
+
aria-label={label}
|
|
289
|
+
aria-valuemin={0}
|
|
290
|
+
aria-valuemax={100}
|
|
291
|
+
aria-valuenow={travelling ? undefined : shown}
|
|
292
|
+
// 高对比模式下底色都被系统换掉:轨道剩一圈线,填充用系统的"选中"色(同 Progress)
|
|
293
|
+
className="absolute top-0 left-0 h-full w-[calc(var(--ef-splash-unit)*1.25)] forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[CanvasText] cq-portrait:top-auto cq-portrait:bottom-[calc(var(--ef-splash-unit)*17.75_+_(var(--_status)_-_var(--ef-splash-unit))*1.25_+_(var(--_tagline)_-_var(--ef-splash-unit)*1.5)*1.35)] cq-portrait:h-[calc(var(--ef-splash-unit)*2.5)] cq-portrait:w-[82.4074%]"
|
|
294
|
+
>
|
|
295
|
+
{travelling ? (
|
|
296
|
+
<span className="absolute inset-0 overflow-hidden">
|
|
297
|
+
<span className="absolute inset-x-0 top-0 h-2/5 animate-indeterminate-y bg-action motion-reduce:translate-y-3/4 forced-colors:bg-[Highlight] cq-portrait:inset-x-auto cq-portrait:inset-y-0 cq-portrait:left-0 cq-portrait:h-auto cq-portrait:w-2/5 cq-portrait:animate-indeterminate cq-portrait:motion-reduce:translate-x-3/4 cq-portrait:motion-reduce:translate-y-0" />
|
|
298
|
+
</span>
|
|
299
|
+
) : (
|
|
300
|
+
<span className="absolute top-0 left-0 h-[calc(var(--ef-splash-progress)*1%)] w-full bg-action forced-colors:bg-[Highlight] cq-portrait:h-full cq-portrait:w-[calc(var(--ef-splash-progress)*1%)]" />
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{/* 读数骑在条的末端上:一条没有高度的基线,数字在线上面,状态字在线下面 */}
|
|
304
|
+
<div
|
|
305
|
+
aria-hidden="true"
|
|
306
|
+
className={cn(
|
|
307
|
+
"absolute top-[calc(var(--ef-splash-progress)*1%)] left-[calc(var(--ef-splash-unit)*3.125)] h-0 whitespace-nowrap cq-portrait:top-auto cq-portrait:-bottom-[calc(var(--ef-splash-unit)*1.75)] cq-portrait:left-[calc(var(--ef-splash-progress)*1%)] cq-portrait:ml-[calc(var(--ef-splash-unit)*0.75)]",
|
|
308
|
+
// 没有数字的时候条的末端不在哪:状态字往下让一点,别贴着上沿
|
|
309
|
+
travelling &&
|
|
310
|
+
"translate-y-[calc(var(--ef-splash-unit)*3)] cq-portrait:translate-y-0",
|
|
311
|
+
)}
|
|
312
|
+
>
|
|
313
|
+
{!indeterminate && (
|
|
314
|
+
<p className="absolute bottom-[calc(var(--ef-splash-unit)*1.625)] left-0 leading-none text-action">
|
|
315
|
+
<span className="mb-[calc(var(--ef-splash-unit)*0.25)] block h-[calc(var(--ef-splash-unit)*1.875)] w-[calc(var(--ef-splash-unit)*0.5)] rounded-[calc(var(--ef-splash-unit)*0.25)] bg-action" />
|
|
316
|
+
{/* 数字是计数器画的:它跟着那个整数变量补间,脚本不用一帧一帧去写 */}
|
|
317
|
+
<span className="text-[length:calc(var(--ef-splash-unit)*4.375)] font-medium tabular-nums [counter-reset:ef-splash_var(--ef-splash-percent)] before:content-[counter(ef-splash)]" />
|
|
318
|
+
<span className="text-[length:calc(var(--ef-splash-unit)*3.25)]">
|
|
319
|
+
%
|
|
320
|
+
</span>
|
|
321
|
+
</p>
|
|
322
|
+
)}
|
|
323
|
+
<p className="absolute -top-[calc(var(--ef-splash-unit)*0.5)] left-0 text-[length:var(--_status)] leading-[1.25] text-ink-tertiary">
|
|
324
|
+
<span className="block h-[calc(var(--ef-splash-unit)*0.5)] w-[calc(var(--ef-splash-unit)*1.25)] border-x-[length:calc(var(--ef-splash-unit)*0.5)] border-current" />
|
|
325
|
+
{status}
|
|
326
|
+
</p>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
{/* 铺满:等条走满(0.5s)再从左往右盖过去,0.6s,先慢后快(官网实测) */}
|
|
332
|
+
{leaving && (
|
|
333
|
+
<span
|
|
334
|
+
aria-hidden="true"
|
|
335
|
+
className="absolute inset-0 origin-left animate-fill bg-action [animation-delay:500ms] [animation-timing-function:cubic-bezier(1,0,0.7,1)] motion-reduce:hidden forced-colors:bg-[Highlight]"
|
|
336
|
+
/>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SplashScreen, type SplashScreenProps } from "./SplashScreen";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useEffect,
|
|
3
3
|
useId,
|
|
4
|
+
useLayoutEffect,
|
|
4
5
|
useMemo,
|
|
5
6
|
useRef,
|
|
6
7
|
useState,
|
|
@@ -203,9 +204,11 @@ export function TagInput({
|
|
|
203
204
|
return () => clearTimeout(timer);
|
|
204
205
|
}, [flash]);
|
|
205
206
|
|
|
206
|
-
//
|
|
207
|
+
// 删掉一个之后焦点落到哪:等这一次渲染落到页面上,旁边那个小块才是"旁边那个",当场交出去。
|
|
208
|
+
// 不用 useEffect:它晚一拍,上一次渲染留下的那一份可能在这次删除之后才补跑——
|
|
209
|
+
// 看到的还是删之前的那一排,焦点留在马上要走的那一块上,随后掉到 body
|
|
207
210
|
const pendingFocus = useRef<number | "input" | null>(null);
|
|
208
|
-
|
|
211
|
+
useLayoutEffect(() => {
|
|
209
212
|
const target = pendingFocus.current;
|
|
210
213
|
if (target === null) return;
|
|
211
214
|
pendingFocus.current = null;
|
|
@@ -178,9 +178,10 @@ function ToastHost({ placement, label, closeLabel, children }: ToastHostProps) {
|
|
|
178
178
|
data-theme="dark"
|
|
179
179
|
className={cn(
|
|
180
180
|
"pointer-events-auto absolute left-1/2 w-max max-w-full",
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
// 暗色页面上黑底和页面只差一档,靠环境阴影分开
|
|
182
|
+
"bg-black/80 shadow-sm text-base leading-[1.4] text-white",
|
|
183
|
+
// 高对比模式下系统把阴影关掉、底色换成页面色,提示就没有边界了:补一圈线
|
|
184
|
+
"forced-colors:border forced-colors:border-[CanvasText]",
|
|
184
185
|
// 三样各走各的:淡入淡出 200ms;进场的位移 300ms;没划够就松手,滑回原位 200ms。
|
|
185
186
|
// 拖动中基元把过渡整个关掉,位置贴着手指
|
|
186
187
|
"transition-[opacity,translate,transform] duration-[var(--duration-fast),var(--duration-base),var(--duration-fast)] ease-[var(--ease-standard),var(--ease-exit),var(--ease-standard)]",
|
package/src/index.ts
CHANGED
|
@@ -63,6 +63,7 @@ export * from "./components/skeleton";
|
|
|
63
63
|
export * from "./components/slider";
|
|
64
64
|
export * from "./components/sparkline";
|
|
65
65
|
export * from "./components/spinner";
|
|
66
|
+
export * from "./components/splash-screen";
|
|
66
67
|
export * from "./components/stat";
|
|
67
68
|
export * from "./components/stepper";
|
|
68
69
|
export * from "./components/steps";
|
package/src/styles/README.md
CHANGED
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
| [utilities.css](utilities.css) | `@utility`:母题工具类(斜纹、切角、斜楔、角括号、镂空字、警示条纹、竖条变箭头),以及菱形表单符号的开关 |
|
|
7
7
|
| [index.css](index.css) | 入口:`tailwindcss` → `theme.css` → `utilities.css` → 基础样式 → `@source` |
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
对外的入口(见包的 `exports`):
|
|
10
10
|
|
|
11
11
|
| 引入路径 | 对应 | 给谁用 |
|
|
12
12
|
| --- | --- | --- |
|
|
13
13
|
| `@endfield-ui/react/tailwind.css` | `index.css` | 用 Tailwind v4 的项目,代替 `@import "tailwindcss"` |
|
|
14
14
|
| `@endfield-ui/react/styles.css` | `dist/styles.css` | 不用 Tailwind 的项目,预编译产物 |
|
|
15
15
|
| `@endfield-ui/react/theme.css` | `theme.css` | 只要令牌、不要组件 |
|
|
16
|
+
| `@endfield-ui/react/utilities.css` | `utilities.css` | 只要母题工具类 |
|
|
17
|
+
| `@endfield-ui/react/game.css` | `dist/game/styles.css` | 用了 [Game 实机风格](../game/README.md)那一类控件的项目;纯 CSS,和上面的主样式一起引 |
|
|
18
|
+
|
|
19
|
+
Game 实机风格的样式不在这个目录里,也不在主样式里:源码是 [`../game/styles.css`](../game/styles.css) 汇总的每个控件一份纯 CSS,`index.css` 用 `@source not` 把那个目录排除在扫描之外。
|
|
16
20
|
|
|
17
21
|
## theme.css 的结构
|
|
18
22
|
|
|
@@ -63,7 +67,10 @@
|
|
|
63
67
|
|
|
64
68
|
这些类只画形状。什么东西可以切角、哪里能放斜纹,见 [docs/design/elements](../../../../docs/design/elements/corner-and-wedge.md) 各篇的"什么时候用"。预编译的 `styles.css` 里只有组件自己用到的那几个;不用 Tailwind 的项目用对应的装饰组件。
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
文件末尾还有两个自定义变体:
|
|
71
|
+
|
|
72
|
+
- `choice-diamond:`,在祖先带 `data-choice="diamond"` 时生效,复选与单选靠它整体换成菱形符号。
|
|
73
|
+
- `cq-portrait:`,在最近的尺寸容器(`container-type: size`)高大于宽时生效。开屏页靠它换成竖排——看的是它所在的那一块,不是视口。
|
|
67
74
|
|
|
68
75
|
## 注意
|
|
69
76
|
|
package/src/styles/index.css
CHANGED
package/src/styles/theme.css
CHANGED
|
@@ -374,6 +374,7 @@
|
|
|
374
374
|
--animate-turn-in: ef-turn-in 300ms ease-out both;
|
|
375
375
|
--animate-fade-in: ef-fade-in 300ms ease-out both;
|
|
376
376
|
--animate-indeterminate: ef-indeterminate 1.2s linear infinite alternate;
|
|
377
|
+
--animate-indeterminate-y: ef-indeterminate-y 1.2s linear infinite alternate;
|
|
377
378
|
--animate-pulse: ef-pulse 2s ease-in-out infinite;
|
|
378
379
|
--animate-bracket-in: ef-bracket-in 200ms ease-out both;
|
|
379
380
|
--animate-shift-in: ef-shift-in 300ms ease-out both;
|
|
@@ -507,6 +508,16 @@
|
|
|
507
508
|
}
|
|
508
509
|
}
|
|
509
510
|
|
|
511
|
+
/* 同上,竖着的轨道:开屏页贴左缘的那根条 */
|
|
512
|
+
@keyframes ef-indeterminate-y {
|
|
513
|
+
from {
|
|
514
|
+
transform: translateY(0);
|
|
515
|
+
}
|
|
516
|
+
to {
|
|
517
|
+
transform: translateY(150%);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
510
521
|
/* 骨架:很轻的明度往返,不是扫光 */
|
|
511
522
|
@keyframes ef-pulse {
|
|
512
523
|
0%,
|
|
@@ -610,6 +621,28 @@
|
|
|
610
621
|
--color-info: var(--ef-info);
|
|
611
622
|
}
|
|
612
623
|
|
|
624
|
+
/* ------------------------------------------------------------------ */
|
|
625
|
+
/* 开屏页的进度 */
|
|
626
|
+
/* ------------------------------------------------------------------ */
|
|
627
|
+
|
|
628
|
+
/*
|
|
629
|
+
* SplashScreen 把进度写成根上的这两个变量,注册成数值之后才能过渡:条长、读数的位置、
|
|
630
|
+
* 底图的模糊都由第一个算,读数的数字是第二个(整数,经计数器显示)。两个一起过渡,
|
|
631
|
+
* 所以这四样天然同拍,半路改值也是从当前的位置折过去。
|
|
632
|
+
* 不认 @property 的浏览器里它们只是普通变量:值照样对,只是不补间、直接到位。
|
|
633
|
+
*/
|
|
634
|
+
@property --ef-splash-progress {
|
|
635
|
+
syntax: "<number>";
|
|
636
|
+
inherits: true;
|
|
637
|
+
initial-value: 0;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
@property --ef-splash-percent {
|
|
641
|
+
syntax: "<integer>";
|
|
642
|
+
inherits: true;
|
|
643
|
+
initial-value: 0;
|
|
644
|
+
}
|
|
645
|
+
|
|
613
646
|
/* ------------------------------------------------------------------ */
|
|
614
647
|
/* 主题切换 */
|
|
615
648
|
/* ------------------------------------------------------------------ */
|
package/src/styles/utilities.css
CHANGED
|
@@ -240,3 +240,11 @@
|
|
|
240
240
|
/* ------------------------------------------------------------------ */
|
|
241
241
|
|
|
242
242
|
@custom-variant choice-diamond (&:where([data-choice="diamond"], [data-choice="diamond"] *));
|
|
243
|
+
|
|
244
|
+
/* ------------------------------------------------------------------ */
|
|
245
|
+
/* 所在的容器是竖着的(高大于宽):开屏页据此换成竖排。 */
|
|
246
|
+
/* 看的是最近的尺寸容器(container-type: size),不是视口—— */
|
|
247
|
+
/* 放进一个框里的开屏页,横竖由那个框说了算 */
|
|
248
|
+
/* ------------------------------------------------------------------ */
|
|
249
|
+
|
|
250
|
+
@custom-variant cq-portrait (@container (orientation: portrait));
|