@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@endfield-ui/react",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "终末地风格的 React 基础控件库(非官方)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"src",
|
|
33
33
|
"!src/**/*.test.*",
|
|
34
34
|
"!src/test",
|
|
35
|
+
"!src/game",
|
|
35
36
|
"NOTICE.md"
|
|
36
37
|
],
|
|
37
38
|
"exports": {
|
|
@@ -43,6 +44,11 @@
|
|
|
43
44
|
"./styles.css": "./dist/styles.css",
|
|
44
45
|
"./theme.css": "./src/styles/theme.css",
|
|
45
46
|
"./utilities.css": "./src/styles/utilities.css",
|
|
47
|
+
"./game": {
|
|
48
|
+
"types": "./dist/game.d.ts",
|
|
49
|
+
"default": "./dist/game.js"
|
|
50
|
+
},
|
|
51
|
+
"./game.css": "./dist/game/styles.css",
|
|
46
52
|
"./package.json": "./package.json"
|
|
47
53
|
},
|
|
48
54
|
"peerDependencies": {
|
|
@@ -77,8 +83,9 @@
|
|
|
77
83
|
"vitest": "^5.0.3"
|
|
78
84
|
},
|
|
79
85
|
"scripts": {
|
|
80
|
-
"build": "tsdown && pnpm build:css",
|
|
86
|
+
"build": "tsdown && pnpm build:css && pnpm build:game",
|
|
81
87
|
"build:css": "tailwindcss -i src/styles/index.css -o dist/styles.css --minify",
|
|
88
|
+
"build:game": "tailwindcss -i src/game/styles.css -o dist/game/styles.css --minify && node scripts/copy-game-assets.mjs",
|
|
82
89
|
"test": "vitest run",
|
|
83
90
|
"test:watch": "vitest",
|
|
84
91
|
"typecheck": "tsc --noEmit"
|
package/src/components/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# components
|
|
2
2
|
|
|
3
|
+
主入口(`@endfield-ui/react`)的控件。照游戏内界面复刻的那一组在 [`../game`](../game/README.md),独立入口,约定不同;这个目录里的东西不许引它。
|
|
4
|
+
|
|
3
5
|
| 目录 | 导出 | 规范 |
|
|
4
6
|
| --- | --- | --- |
|
|
5
7
|
| `button/` | `Button`、`ButtonGroup` | [按钮](../../../../docs/design/components/button.md) |
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
| `skeleton/` | `Skeleton` | [反馈](../../../../docs/design/components/feedback.md) |
|
|
61
63
|
| `empty-state/` | `EmptyState` | [反馈](../../../../docs/design/components/feedback.md) |
|
|
62
64
|
| `loader/` | `Loader` | [反馈](../../../../docs/design/components/feedback.md) |
|
|
65
|
+
| `splash-screen/` | `SplashScreen`(开屏页:照官网首次载入的那一页排的,标志和底图是插槽) | [反馈](../../../../docs/design/components/feedback.md#开屏页) |
|
|
63
66
|
| `spinner/` | `Spinner` | [反馈](../../../../docs/design/components/feedback.md) |
|
|
64
67
|
| `completion-banner/` | `CompletionBanner` | [反馈](../../../../docs/design/components/feedback.md) |
|
|
65
68
|
| `tooltip/` | `Tooltip`、`TooltipProvider` | [浮层](../../../../docs/design/components/overlay.md) |
|
|
@@ -126,7 +126,7 @@ export function Alert({
|
|
|
126
126
|
// 正在收起的提示条点不到、Tab 不进去
|
|
127
127
|
inert={leaving}
|
|
128
128
|
className={cn(
|
|
129
|
-
"flex items-start gap-3 border
|
|
129
|
+
"flex items-start gap-3 border-l-4 bg-surface-raised py-3 pr-3 pl-3 text-sm text-ink",
|
|
130
130
|
toneBar[tone],
|
|
131
131
|
leaving && [
|
|
132
132
|
"pointer-events-none overflow-hidden",
|
|
@@ -91,8 +91,6 @@ export function DateField({
|
|
|
91
91
|
data-invalid={invalid ? "" : undefined}
|
|
92
92
|
className={cn(
|
|
93
93
|
controlBox({ variant, invalid, disabled, readOnly: false }),
|
|
94
|
-
// 面板开着的时候边线保持聚焦的墨色
|
|
95
|
-
!disabled && !invalid && "has-data-popup-open:border-ink",
|
|
96
94
|
controlSize[size],
|
|
97
95
|
className,
|
|
98
96
|
)}
|
|
@@ -16,6 +16,10 @@ export const MenuVariantContext = createContext<MenuVariant>("plain");
|
|
|
16
16
|
/**
|
|
17
17
|
* 面板:直角、1px 线、环境阴影;顶部一条半宽的行动色细条,展开时从 0 伸到一半宽。
|
|
18
18
|
*
|
|
19
|
+
* 细条上挂一个朝下的小三角,和它是同一个伪元素裁出来的:条高 3px,三角露出 8×4px,
|
|
20
|
+
* 中心距条的左端 32px(实机风格弹窗分隔条的一半大)。它是条上的记号,不指向触发处——
|
|
21
|
+
* 面板翻到上面、在侧边时照旧在顶上、朝下。三角探进第一行 2px,所以压在各行的底色之上
|
|
22
|
+
*
|
|
19
23
|
* 进场:淡入(200ms),同时从触发处那一侧挪过来 4px(300ms,和细条同一拍)。
|
|
20
24
|
* 退场只淡出——位移只写在起点上。两个时长、两条缓动按属性一一对应
|
|
21
25
|
*/
|
|
@@ -24,7 +28,8 @@ export const menuPanel = [
|
|
|
24
28
|
"origin-(--transform-origin) transition-[opacity,translate] duration-[var(--duration-fast),var(--duration-base)] ease-[var(--ease-standard),var(--ease-exit)]",
|
|
25
29
|
"data-ending-style:opacity-0 data-starting-style:opacity-0",
|
|
26
30
|
enterFromSide,
|
|
27
|
-
"before:pointer-events-none before:absolute before:-top-px before:-left-px before:h-[
|
|
31
|
+
"before:pointer-events-none before:absolute before:-top-px before:-left-px before:z-1 before:h-[7px] before:w-1/2 before:origin-left before:bg-action before:content-['']",
|
|
32
|
+
"before:[clip-path:polygon(0_0,100%_0,100%_3px,36px_3px,32px_100%,28px_3px,0_3px)]",
|
|
28
33
|
"before:transition-transform before:duration-(--duration-base) before:ease-exit data-starting-style:before:scale-x-0",
|
|
29
34
|
"forced-colors:before:hidden",
|
|
30
35
|
].join(" ");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useEffect,
|
|
3
|
+
useLayoutEffect,
|
|
3
4
|
useRef,
|
|
4
5
|
useState,
|
|
5
6
|
type ChangeEvent,
|
|
@@ -235,9 +236,11 @@ export function FileUpload({
|
|
|
235
236
|
}
|
|
236
237
|
}, [files, revision]);
|
|
237
238
|
|
|
238
|
-
//
|
|
239
|
+
// 移除之后焦点落到哪:等这一次渲染落到页面上,当场交出去。
|
|
240
|
+
// 不用 useEffect:它晚一拍,上一次渲染留下的那一份可能在这次移除之后才补跑——
|
|
241
|
+
// 看到的还是移除之前的页面,焦点留在马上要收起的那一行上,随后掉到 body
|
|
239
242
|
const pendingFocus = useRef<number | null>(null);
|
|
240
|
-
|
|
243
|
+
useLayoutEffect(() => {
|
|
241
244
|
const index = pendingFocus.current;
|
|
242
245
|
if (index === null) return;
|
|
243
246
|
pendingFocus.current = null;
|
|
@@ -5,7 +5,7 @@ import { focusRingWithin } from "../../lib/focus-ring";
|
|
|
5
5
|
* 输入框与多行文本共用的外框,直角,两种画法:
|
|
6
6
|
* - `sunken` 凹陷的底 + 一条 2px 的底边线,默认;
|
|
7
7
|
* - `outline` 四边 1px 的描边 + 页面底色,放在凹陷底色的区域里时用。
|
|
8
|
-
*
|
|
8
|
+
* 状态靠边线说:默认灰 → 悬停深一档 → 错误红;聚焦另画一条墨线。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export type ControlVariant = "sunken" | "outline";
|
|
@@ -41,11 +41,29 @@ const fill: Record<ControlVariant, string> = {
|
|
|
41
41
|
outline: "bg-surface",
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
/* 画在 `::before` 上:分段选择的 `::after` 是选中的那块底 */
|
|
45
|
+
const lineFrame: Record<ControlVariant, string> = {
|
|
46
|
+
sunken:
|
|
47
|
+
"before:pointer-events-none before:absolute before:inset-x-0 before:-bottom-[2px] before:h-[2px] before:origin-left before:scale-x-0 before:bg-ink before:content-[''] forced-colors:before:bg-[Highlight]",
|
|
48
|
+
outline:
|
|
49
|
+
"before:pointer-events-none before:absolute before:-inset-px before:border before:border-ink before:[clip-path:inset(0_100%_0_0)] before:content-['']",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const lineOpen: Record<ControlVariant, string> = {
|
|
53
|
+
sunken:
|
|
54
|
+
"focus-within:before:scale-x-100 has-data-popup-open:before:scale-x-100",
|
|
55
|
+
outline:
|
|
56
|
+
"focus-within:before:[clip-path:inset(0)] has-data-popup-open:before:[clip-path:inset(0)]",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/* 擦出 300ms、收回 200ms */
|
|
60
|
+
const lineMotion =
|
|
61
|
+
"before:transition-[scale,clip-path] before:duration-(--duration-fast) before:ease-standard focus-within:before:duration-(--duration-base) focus-within:before:ease-exit has-data-popup-open:before:duration-(--duration-base) has-data-popup-open:before:ease-exit";
|
|
62
|
+
|
|
63
|
+
/** 聚焦的墨线,接在带 `relative` 的外框上用 */
|
|
64
|
+
export function controlLine(variant: ControlVariant = "sunken") {
|
|
65
|
+
return cn(lineFrame[variant], lineOpen[variant], lineMotion);
|
|
66
|
+
}
|
|
49
67
|
|
|
50
68
|
export function controlBox({
|
|
51
69
|
variant = "sunken",
|
|
@@ -54,7 +72,7 @@ export function controlBox({
|
|
|
54
72
|
readOnly,
|
|
55
73
|
}: BoxState) {
|
|
56
74
|
return cn(
|
|
57
|
-
"flex w-full min-w-0 text-ink",
|
|
75
|
+
"relative flex w-full min-w-0 text-ink",
|
|
58
76
|
frame[variant],
|
|
59
77
|
"transition-colors duration-(--duration-fast) ease-standard",
|
|
60
78
|
focusRingWithin,
|
|
@@ -62,15 +80,17 @@ export function controlBox({
|
|
|
62
80
|
? // 只读:没有边线、底变透明。边线留透明的,尺寸不跳
|
|
63
81
|
"border-transparent bg-transparent"
|
|
64
82
|
: fill[variant],
|
|
65
|
-
//
|
|
66
|
-
|
|
83
|
+
// 高对比模式下系统会把透明的边线也换成字色显出来,而底色本来就都没了:
|
|
84
|
+
// 只读的写成页面色,不然和能填的一个样
|
|
85
|
+
readOnly && !disabled && !invalid && "forced-colors:border-[Canvas]",
|
|
67
86
|
disabled
|
|
68
87
|
? "cursor-not-allowed border-line text-ink-disabled"
|
|
69
88
|
: invalid
|
|
70
89
|
? "border-danger"
|
|
71
90
|
: !readOnly &&
|
|
72
91
|
// 悬停只在没聚焦时生效:生成的 CSS 里 hover 排在 focus-within 之后,会盖掉聚焦的墨色
|
|
73
|
-
"border-line-strong hover:not-focus-within:border-ink-secondary
|
|
92
|
+
"border-line-strong hover:not-focus-within:border-ink-secondary",
|
|
93
|
+
!disabled && !invalid && !readOnly && controlLine(variant),
|
|
74
94
|
);
|
|
75
95
|
}
|
|
76
96
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { OTPField as BaseOtpField } from "@base-ui/react/otp-field";
|
|
2
2
|
import { Fragment, useId, type ComponentProps } from "react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
|
-
import { focusRing } from "../../lib/focus-ring";
|
|
5
4
|
import { useFieldContext, useFieldControl } from "../field/Field";
|
|
6
5
|
import {
|
|
7
|
-
|
|
6
|
+
controlBox,
|
|
7
|
+
controlElement,
|
|
8
8
|
type ControlSize,
|
|
9
9
|
type ControlVariant,
|
|
10
10
|
} from "../input/control-box";
|
|
@@ -53,21 +53,17 @@ export type OtpInputProps = Omit<
|
|
|
53
53
|
slotLabel?: (position: number, length: number) => string;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
/*
|
|
56
|
+
/* 格子见方;容器窄了宽度可以让,高度不变。字号在里面那个 <input> 上 */
|
|
57
57
|
const slotSize: Record<ControlSize, string> = {
|
|
58
|
-
sm: "h-8 w-8
|
|
59
|
-
md: "h-10 w-10
|
|
60
|
-
lg: "h-14 w-14
|
|
58
|
+
sm: "h-8 w-8",
|
|
59
|
+
md: "h-10 w-10",
|
|
60
|
+
lg: "h-14 w-14",
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const slotFill: Record<ControlVariant, string> = {
|
|
69
|
-
sunken: "bg-surface-sunken",
|
|
70
|
-
outline: "bg-surface",
|
|
63
|
+
const slotText: Record<ControlSize, string> = {
|
|
64
|
+
sm: "text-base",
|
|
65
|
+
md: "text-lg",
|
|
66
|
+
lg: "text-2xl",
|
|
71
67
|
};
|
|
72
68
|
|
|
73
69
|
const gapClass: Record<ControlSize, string> = {
|
|
@@ -132,27 +128,12 @@ export function OtpInput({
|
|
|
132
128
|
const ownLabel = ariaLabelledBy ?? (ariaLabel ? labelId : undefined);
|
|
133
129
|
const groupLabel = ownLabel ?? fieldContext?.labelId;
|
|
134
130
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
"min-w-0 shrink text-center font-tech font-medium tabular-nums text-transparent caret-ink",
|
|
131
|
+
const inputClass = cn(
|
|
132
|
+
controlElement,
|
|
133
|
+
"h-full w-full text-center font-tech font-medium tabular-nums caret-ink text-transparent",
|
|
139
134
|
"transition-colors duration-(--duration-fast) ease-standard",
|
|
140
135
|
disabled ? "data-filled:text-ink-disabled" : "data-filled:text-ink",
|
|
141
|
-
|
|
142
|
-
slotSize[size],
|
|
143
|
-
slotFrame[variant],
|
|
144
|
-
readOnly
|
|
145
|
-
? // 只读:没有边线、底变透明。边线留透明的,尺寸不跳
|
|
146
|
-
"border-transparent bg-transparent"
|
|
147
|
-
: slotFill[variant],
|
|
148
|
-
readOnly && !disabled && !invalid && readOnlyFrame,
|
|
149
|
-
disabled
|
|
150
|
-
? "cursor-not-allowed border-line"
|
|
151
|
-
: invalid
|
|
152
|
-
? "border-danger"
|
|
153
|
-
: !readOnly &&
|
|
154
|
-
// 悬停只在没聚焦时生效:聚焦中的格子不会因为鼠标经过而变浅
|
|
155
|
-
"border-line-strong hover:not-focus:border-ink-secondary focus:border-ink",
|
|
136
|
+
slotText[size],
|
|
156
137
|
);
|
|
157
138
|
|
|
158
139
|
const group = groupSize !== undefined && groupSize > 0 ? groupSize : 0;
|
|
@@ -203,16 +184,27 @@ export function OtpInput({
|
|
|
203
184
|
className="h-0.5 w-2 shrink-0 bg-line-strong forced-colors:bg-[CanvasText]"
|
|
204
185
|
/>
|
|
205
186
|
)}
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
187
|
+
<span
|
|
188
|
+
className={cn(
|
|
189
|
+
controlBox({ variant, invalid, disabled, readOnly }),
|
|
190
|
+
"shrink items-center justify-center",
|
|
191
|
+
slotSize[size],
|
|
192
|
+
)}
|
|
193
|
+
>
|
|
194
|
+
<BaseOtpField.Input
|
|
195
|
+
// 第一格的名称是字段的标签(或者上面那段隐藏的文字),说明文字也挂在它上面
|
|
196
|
+
{...(index === 0
|
|
197
|
+
? {
|
|
198
|
+
"aria-labelledby": ownLabel,
|
|
199
|
+
"aria-describedby": describedBy,
|
|
200
|
+
}
|
|
201
|
+
: { "aria-label": slotLabel(index + 1, length) })}
|
|
202
|
+
aria-invalid={invalid || undefined}
|
|
203
|
+
autoFocus={autoFocus && index === 0}
|
|
204
|
+
autoCapitalize={uppercase ? "characters" : "none"}
|
|
205
|
+
className={inputClass}
|
|
206
|
+
/>
|
|
207
|
+
</span>
|
|
216
208
|
</Fragment>
|
|
217
209
|
))}
|
|
218
210
|
</BaseOtpField.Root>
|
|
@@ -13,6 +13,7 @@ import { mergeRefs } from "../../lib/merge-refs";
|
|
|
13
13
|
import { indicator, indicatorBox } from "../../lib/motion";
|
|
14
14
|
import { useFieldContext } from "../field/Field";
|
|
15
15
|
import {
|
|
16
|
+
controlLine,
|
|
16
17
|
controlSize,
|
|
17
18
|
type ControlSize,
|
|
18
19
|
type ControlVariant,
|
|
@@ -140,12 +141,11 @@ export function SegmentedControl({
|
|
|
140
141
|
"forced-colors:after:bg-[Highlight] forced-colors:has-[[data-selected][data-disabled]]:after:bg-[GrayText]",
|
|
141
142
|
track[variant],
|
|
142
143
|
controlSize[size],
|
|
143
|
-
// 和输入框一样靠边线说状态
|
|
144
144
|
disabled
|
|
145
145
|
? "border-line"
|
|
146
146
|
: invalid
|
|
147
147
|
? "border-danger"
|
|
148
|
-
: "border-line-strong
|
|
148
|
+
: ["border-line-strong", controlLine(variant)],
|
|
149
149
|
className,
|
|
150
150
|
)}
|
|
151
151
|
/>
|
|
@@ -191,8 +191,6 @@ export function Select(props: SelectProps) {
|
|
|
191
191
|
data-invalid={invalid ? "" : undefined}
|
|
192
192
|
className={cn(
|
|
193
193
|
controlBox({ variant, invalid, disabled, readOnly: false }),
|
|
194
|
-
// 面板开着的时候边线保持聚焦的墨色
|
|
195
|
-
!disabled && !invalid && "has-data-popup-open:border-ink",
|
|
196
194
|
controlSize[size],
|
|
197
195
|
className,
|
|
198
196
|
)}
|