@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.
Files changed (54) hide show
  1. package/NOTICE.md +33 -2
  2. package/README.md +60 -3
  3. package/dist/game/LICENSE.lucide +43 -0
  4. package/dist/game/assets/alert_banner_bg_left.png +0 -0
  5. package/dist/game/assets/alert_banner_bg_right.png +0 -0
  6. package/dist/game/assets/alert_bg.png +0 -0
  7. package/dist/game/assets/deco_common_logo_bg.png +0 -0
  8. package/dist/game/assets/detail.png +0 -0
  9. package/dist/game/assets/enemies_background.webp +0 -0
  10. package/dist/game/assets/enemies_background_dark.webp +0 -0
  11. package/dist/game/assets/header.webp +0 -0
  12. package/dist/game/assets/icon_card_rarity_1_bottom_edge.webp +0 -0
  13. package/dist/game/assets/icon_card_rarity_2_bottom_edge.webp +0 -0
  14. package/dist/game/assets/icon_card_rarity_3_bottom_edge.webp +0 -0
  15. package/dist/game/assets/icon_card_rarity_4_bottom_edge.webp +0 -0
  16. package/dist/game/assets/icon_card_rarity_5_bottom_edge.webp +0 -0
  17. package/dist/game/assets/icon_card_rarity_6_bottom_edge.webp +0 -0
  18. package/dist/game/assets/items_background.webp +0 -0
  19. package/dist/game/assets/items_background_dark.webp +0 -0
  20. package/dist/game/assets/link.webp +0 -0
  21. package/dist/game/assets/pag-button-texture.png +0 -0
  22. package/dist/game/assets/splash_bg.jpg +0 -0
  23. package/dist/game/assets/splash_bg_portrait.jpg +0 -0
  24. package/dist/game/assets/splash_deco.svg +4 -0
  25. package/dist/game/assets/splash_logo.svg +1 -0
  26. package/dist/game/assets/splash_triangles.svg +4 -0
  27. package/dist/game/assets/weapons_background.webp +0 -0
  28. package/dist/game/assets/weapons_background_dark.webp +0 -0
  29. package/dist/game/resource-provenance.json +453 -0
  30. package/dist/game/styles.css +2 -0
  31. package/dist/game.d.ts +384 -0
  32. package/dist/game.js +2069 -0
  33. package/dist/index.d.ts +42 -1
  34. package/dist/index.js +253 -30
  35. package/dist/styles.css +1 -1
  36. package/package.json +9 -2
  37. package/src/components/README.md +3 -0
  38. package/src/components/alert/Alert.tsx +1 -1
  39. package/src/components/date-picker/DateField.tsx +0 -2
  40. package/src/components/dropdown-menu/menu-style.ts +6 -1
  41. package/src/components/file-upload/FileUpload.tsx +5 -2
  42. package/src/components/input/control-box.ts +30 -10
  43. package/src/components/otp-input/OtpInput.tsx +35 -43
  44. package/src/components/segmented-control/SegmentedControl.tsx +2 -2
  45. package/src/components/select/Select.tsx +0 -2
  46. package/src/components/splash-screen/SplashScreen.tsx +340 -0
  47. package/src/components/splash-screen/index.ts +1 -0
  48. package/src/components/tag-input/TagInput.tsx +5 -2
  49. package/src/components/toast/Toast.tsx +4 -3
  50. package/src/index.ts +1 -0
  51. package/src/styles/README.md +9 -2
  52. package/src/styles/index.css +5 -0
  53. package/src/styles/theme.css +33 -0
  54. 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.1",
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"
@@ -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 border-l-4 border-line bg-surface-raised py-3 pr-3 pl-3 text-sm text-ink",
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-[3px] before:w-1/2 before:origin-left before:bg-action before:content-['']",
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
- useEffect(() => {
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
- export const readOnlyFrame = "forced-colors:border-[Canvas]";
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
- readOnly && !disabled && !invalid && readOnlyFrame,
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 focus-within:border-ink",
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
- readOnlyFrame,
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 text-base",
59
- md: "h-10 w-10 text-lg",
60
- lg: "h-14 w-14 text-2xl",
58
+ sm: "h-8 w-8",
59
+ md: "h-10 w-10",
60
+ lg: "h-14 w-14",
61
61
  };
62
62
 
63
- const slotFrame: Record<ControlVariant, string> = {
64
- sunken: "border-b-2",
65
- outline: "border",
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 slotClass = cn(
136
- // 字色:空格是透明的,填上(基元给的 data-filled)才有颜色。下面那条颜色的过渡
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
- focusRing,
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
- <BaseOtpField.Input
207
- // 第一格的名称是字段的标签(或者上面那段隐藏的文字),说明文字也挂在它上面
208
- {...(index === 0
209
- ? { "aria-labelledby": ownLabel, "aria-describedby": describedBy }
210
- : { "aria-label": slotLabel(index + 1, length) })}
211
- aria-invalid={invalid || undefined}
212
- autoFocus={autoFocus && index === 0}
213
- autoCapitalize={uppercase ? "characters" : "none"}
214
- className={slotClass}
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 focus-within:border-ink",
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
  )}