@endfield-ui/react 0.2.0 → 0.3.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.
@@ -5,6 +5,7 @@ import {
5
5
  isValidElement,
6
6
  useContext,
7
7
  useRef,
8
+ useState,
8
9
  type ComponentProps,
9
10
  type KeyboardEvent,
10
11
  type ReactElement,
@@ -279,7 +280,12 @@ function Stage({
279
280
  </BaseDialog.Close>
280
281
  </div>
281
282
 
282
- <Viewfinder size="md" className="min-h-0 flex-1">
283
+ {/* 取景角等整层淡入完了再落位:一起动的话被淡入盖住,看不见 */}
284
+ <Viewfinder
285
+ size="md"
286
+ animate
287
+ className="min-h-0 flex-1 [--bracket-delay:var(--duration-fast)]"
288
+ >
283
289
  <div
284
290
  ref={trackRef}
285
291
  data-image-viewer-track=""
@@ -287,27 +293,15 @@ function Stage({
287
293
  className="absolute inset-0 flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain [scrollbar-width:none]"
288
294
  >
289
295
  {items.map((item, position) => (
290
- <div
296
+ <Slide
291
297
  key={position}
292
- role="group"
293
- aria-roledescription="幻灯片"
294
- aria-label={`第 ${position + 1} 张,共 ${count} 张`}
295
- // 不在眼前的:Tab 走不进去,读屏也读不到
296
- inert={position !== index}
297
- data-current={position === index ? "" : undefined}
298
- // 点图以外的空处就是关
299
- onClick={(event) => {
300
- if (event.target === event.currentTarget) onClose();
301
- }}
302
- className={cn(
303
- "flex size-full shrink-0 snap-start items-center justify-center p-6 sm:p-10",
304
- // 图按原比例整张放进去:不裁切,也不放大过它自己的尺寸
305
- "*:max-h-full *:max-w-full [&>img]:object-contain [&>video]:object-contain",
306
- )}
298
+ label={`第 ${position + 1} 张,共 ${count} 张`}
299
+ current={position === index}
300
+ onClose={onClose}
307
301
  >
308
302
  {/* 只渲染当前这张和左右各一张:二十张图不会一打开就全去取 */}
309
303
  {Math.abs(position - index) <= 1 ? item.props.children : null}
310
- </div>
304
+ </Slide>
311
305
  ))}
312
306
  </div>
313
307
  </Viewfinder>
@@ -360,6 +354,52 @@ function Stage({
360
354
  );
361
355
  }
362
356
 
357
+ type SlideProps = {
358
+ label: string;
359
+ current: boolean;
360
+ onClose: () => void;
361
+ children: ReactNode;
362
+ };
363
+
364
+ /**
365
+ * 轨道上的一张。图是使用方给的元素,这里不碰它,只在外面听它的 `load`:
366
+ * 还没取到的图是透明的,取到了淡入——不是"啪"地一下出现。
367
+ * 只管直接放进来的 `<img>`;视频、别的内容不等,和原来一样直接在
368
+ */
369
+ function Slide({ label, current, onClose, children }: SlideProps) {
370
+ // 取到过一次就算数:翻远了再翻回来,图是现成的,不用再藏一次
371
+ const [loaded, setLoaded] = useState(false);
372
+ const settle = () => setLoaded(true);
373
+
374
+ return (
375
+ <div
376
+ role="group"
377
+ aria-roledescription="幻灯片"
378
+ aria-label={label}
379
+ // 不在眼前的:Tab 走不进去,读屏也读不到
380
+ inert={!current}
381
+ data-current={current ? "" : undefined}
382
+ data-loaded={loaded ? "" : undefined}
383
+ // load 和 error 不冒泡,但捕获阶段从外往里走,在这一层听得到。
384
+ // 取不到的图也要露出来:使用方写的替代文字在那里
385
+ onLoadCapture={settle}
386
+ onErrorCapture={settle}
387
+ // 点图以外的空处就是关
388
+ onClick={(event) => {
389
+ if (event.target === event.currentTarget) onClose();
390
+ }}
391
+ className={cn(
392
+ "flex size-full shrink-0 snap-start items-center justify-center p-6 sm:p-10",
393
+ // 图按原比例整张放进去:不裁切,也不放大过它自己的尺寸
394
+ "*:max-h-full *:max-w-full [&>img]:object-contain [&>video]:object-contain",
395
+ "[&>img]:transition-opacity [&>img]:duration-(--duration-fast) [&>img]:ease-standard not-data-loaded:[&>img]:opacity-0",
396
+ )}
397
+ >
398
+ {children}
399
+ </div>
400
+ );
401
+ }
402
+
363
403
  export type ImageViewerItemProps = Omit<
364
404
  ComponentProps<"li">,
365
405
  "title" | "aria-label"
@@ -274,7 +274,8 @@ export function ItemSlot({
274
274
  className={cn(
275
275
  "relative",
276
276
  ratioClass[ratio],
277
- selected && "corner-brackets",
277
+ // 选中的那一刻括号从角上伸出来;取消时直接消失
278
+ selected && "corner-brackets after:animate-bracket-in",
278
279
  className,
279
280
  )}
280
281
  >
@@ -85,6 +85,7 @@ export function NavMenu({
85
85
  initialFocus={firstFocusableIn(bodyRef)}
86
86
  className={cn(
87
87
  "fixed inset-0 z-(--z-overlay) flex flex-col overflow-hidden bg-surface text-ink outline-none",
88
+ // 整层淡入;里面的栏目另有自己的入场(见 itemEnter)
88
89
  "transition-opacity duration-(--duration-fast) ease-standard",
89
90
  "data-ending-style:opacity-0 data-starting-style:opacity-0",
90
91
  className,
@@ -155,6 +156,17 @@ export function NavMenu({
155
156
  );
156
157
  }
157
158
 
159
+ /*
160
+ * 栏目入场:逐条从左滑入 8px。第一项等层淡入过半(100ms),之后每项晚 50ms,
161
+ * 第六项起一起到——从第一项动到最后一项停下 550ms。
162
+ * 只动 <li> 的位置和透明度:焦点一打开就在第一项上,键盘不用等它
163
+ */
164
+ const itemEnter = [
165
+ "animate-shift-in [--shift-x:calc(var(--motion-shift-lg)*-1)]",
166
+ "[animation-delay:100ms] nth-2:[animation-delay:150ms] nth-3:[animation-delay:200ms]",
167
+ "nth-4:[animation-delay:250ms] nth-5:[animation-delay:300ms] nth-[n+6]:[animation-delay:350ms]",
168
+ ].join(" ");
169
+
158
170
  type ItemOwnProps = {
159
171
  /** 图标,后面跟一条竖向的分隔线 */
160
172
  icon?: ReactNode;
@@ -188,6 +200,7 @@ export function NavMenuItem({
188
200
  rel,
189
201
  render,
190
202
  onClick,
203
+ className,
191
204
  children,
192
205
  ...props
193
206
  }: NavMenuItemProps) {
@@ -256,7 +269,7 @@ export function NavMenuItem({
256
269
  );
257
270
 
258
271
  return (
259
- <li {...props}>
272
+ <li {...props} className={cn(itemEnter, className)}>
260
273
  {isLink ? (
261
274
  <LinkElement
262
275
  render={render}
@@ -129,8 +129,11 @@ export function OtpInput({
129
129
  const groupLabel = ownLabel ?? fieldContext?.labelId;
130
130
 
131
131
  const slotClass = cn(
132
- "min-w-0 shrink text-center font-tech font-medium tabular-nums text-ink caret-ink",
132
+ // 字色:空格是透明的,填上(基元给的 data-filled)才有颜色。下面那条颜色的过渡
133
+ // 本来是给边线的,字色跟着它走——所以填进去的那一位是淡入的。光标另有自己的颜色
134
+ "min-w-0 shrink text-center font-tech font-medium tabular-nums text-transparent caret-ink",
133
135
  "transition-colors duration-(--duration-fast) ease-standard",
136
+ disabled ? "data-filled:text-ink-disabled" : "data-filled:text-ink",
134
137
  focusRing,
135
138
  slotSize[size],
136
139
  slotFrame[variant],
@@ -139,7 +142,7 @@ export function OtpInput({
139
142
  "border-transparent bg-transparent"
140
143
  : slotFill[variant],
141
144
  disabled
142
- ? "cursor-not-allowed border-line text-ink-disabled"
145
+ ? "cursor-not-allowed border-line"
143
146
  : invalid
144
147
  ? "border-danger"
145
148
  : !readOnly &&
@@ -79,10 +79,13 @@ const markLeft = (ratio: number) =>
79
79
 
80
80
  const thumb = [
81
81
  "relative h-5 w-3 bg-surface-inverse shadow-[0_0_0_2px_var(--ef-surface)]",
82
+ // 按键、外部改值时滑过去。拖动中位置必须贴着指针,所以那时候关掉——
83
+ // 点轨道也算:基元在按下的那一刻就开始拖了
84
+ "transition-[inset-inline-start] duration-(--duration-fast) ease-standard data-dragging:transition-none",
82
85
  // 触屏的点击区补到 40 × 40px
83
86
  "before:absolute before:-inset-x-3.5 before:-inset-y-2.5 before:content-['']",
84
87
  // 按住时正中亮一条强调色细线
85
- "after:absolute after:inset-y-1 after:left-1/2 after:w-0.5 after:-translate-x-1/2 after:bg-accent-ink-inverse after:opacity-0 after:content-[''] data-dragging:after:opacity-100",
88
+ "after:absolute after:inset-y-1 after:left-1/2 after:w-0.5 after:-translate-x-1/2 after:bg-accent-ink-inverse after:opacity-0 after:transition-opacity after:duration-(--duration-fast) after:ease-standard after:content-[''] data-dragging:after:opacity-100",
86
89
  // 菱形方案:滑块是一个 14px 的菱形,按住时中间亮一个小方块
87
90
  "choice-diamond:size-3.5 choice-diamond:rotate-45 choice-diamond:after:inset-1 choice-diamond:after:w-auto choice-diamond:after:translate-x-0",
88
91
  "data-disabled:bg-ink-disabled",
@@ -165,7 +168,8 @@ export function Slider(props: SliderProps) {
165
168
  )}
166
169
  >
167
170
  <BaseSlider.Track className="h-1 w-full bg-line">
168
- <BaseSlider.Indicator className="bg-ink data-disabled:bg-ink-disabled" />
171
+ {/* 走过的一段跟着滑块头走:范围滑块两头都会动 */}
172
+ <BaseSlider.Indicator className="bg-ink transition-[inset-inline-start,width] duration-(--duration-fast) ease-standard data-disabled:bg-ink-disabled data-dragging:transition-none" />
169
173
  {(range ? [0, 1] : [0]).map((index) => (
170
174
  <BaseSlider.Thumb
171
175
  key={index}
@@ -243,6 +243,8 @@ export function Step({
243
243
  <span
244
244
  className={cn(
245
245
  "flex shrink-0 items-center justify-center font-tech leading-none tabular-nums",
246
+ // 未到 → 当前 → 已完成,换色不硬切
247
+ "transition-colors duration-(--duration-fast) ease-standard",
246
248
  nodeSize[size],
247
249
  invalid ? "border border-danger text-danger" : nodeClass[status],
248
250
  )}
@@ -255,12 +257,19 @@ export function Step({
255
257
  String(index + 1).padStart(2, "0")
256
258
  )}
257
259
  </span>
258
- {/* 这一步走完了,通向下一步的线才是实的 */}
260
+ {/*
261
+ 这一步走完了,通向下一步的线才是实的。线自己一直是"没到"的颜色,
262
+ 墨色画在 ::before 上,走完时从起点一端充到头,往回走时收回去。
263
+ 从左上角两个方向一起放大,所以横排、竖排、容器变窄自动改竖排用的是同一组类。
264
+ 墨色那一层比线多出去 16px、由线自己裁掉:这样粗的那个方向一开始就是满的
265
+ (不多出去的话,线是一边变长一边从 0 变粗的,半路上看着比别的线细)
266
+ */}
259
267
  <span
260
268
  className={cn(
261
- "flex-1 group-last/step:hidden",
269
+ "relative flex-1 overflow-clip bg-line-strong group-last/step:hidden",
270
+ "before:absolute before:top-0 before:-right-4 before:-bottom-4 before:left-0 before:origin-top-left before:bg-ink before:transition-[scale] before:duration-(--duration-base) before:ease-exit before:content-['']",
262
271
  lineLayout[orientation],
263
- status === "done" ? "bg-ink" : "bg-line-strong",
272
+ status === "done" ? "before:scale-100" : "before:scale-0",
264
273
  )}
265
274
  />
266
275
  </span>
@@ -82,7 +82,15 @@ const toneText: Record<Exclude<ToastTone, "neutral">, string> = {
82
82
 
83
83
  /*
84
84
  * 水平居中 + 跟着手指走的位移(可以往下或往右划走)。
85
- * 新旧两条在同一个位置交替淡入淡出,所以用绝对定位叠在一起
85
+ *
86
+ * 写在 transform 上,不能写在 translate 上:基元在拖动的时候自己往元素上写内联的
87
+ * transform(从元素现在的 transform 出发,加上指针走过的距离),并把过渡关掉;松手
88
+ * 之后交还给这里的变量。位置写在别的属性上的话两处会叠加——提示走的是手指的两倍。
89
+ *
90
+ * 进场的位移另写在 translate 上(只在起点有):两个属性各管各的,"滑入"的过渡碰不到
91
+ * "跟手"的那一份。底部的从下面来;正中的是官网的做法,只有透明度。
92
+ *
93
+ * 新旧两条在同一个位置交替,所以用绝对定位叠在一起:旧的原地淡出,新的升上来
86
94
  */
87
95
  const placementClass: Record<
88
96
  ToastPlacement,
@@ -90,11 +98,11 @@ const placementClass: Record<
90
98
  > = {
91
99
  bottom: {
92
100
  viewport: "bottom-6",
93
- root: "bottom-0 translate-y-[var(--toast-swipe-movement-y,0px)]",
101
+ root: "bottom-0 transform-[translate(calc(-50%+var(--toast-swipe-movement-x,0px)),var(--toast-swipe-movement-y,0px))] data-starting-style:translate-y-(--motion-shift-lg)",
94
102
  },
95
103
  center: {
96
104
  viewport: "top-1/2",
97
- root: "top-0 translate-y-[calc(-50%+var(--toast-swipe-movement-y,0px))]",
105
+ root: "top-0 transform-[translate(calc(-50%+var(--toast-swipe-movement-x,0px)),calc(-50%+var(--toast-swipe-movement-y,0px)))]",
98
106
  },
99
107
  };
100
108
 
@@ -169,11 +177,16 @@ function ToastHost({ placement, label, closeLabel, children }: ToastHostProps) {
169
177
  // 黑底白字不随主题变:整条做成暗色的局部主题,图形和焦点环按深色底取值
170
178
  data-theme="dark"
171
179
  className={cn(
172
- "pointer-events-auto absolute left-1/2 w-max max-w-full translate-x-[calc(-50%+var(--toast-swipe-movement-x,0px))]",
180
+ "pointer-events-auto absolute left-1/2 w-max max-w-full",
173
181
  // 边线是给暗色页面的:那里黑底和页面几乎一样深,要靠它把提示勾出来。
174
182
  // 亮色页面上它和提示的底几乎同色,看不出来
175
183
  "border border-line bg-black/80 text-base leading-[1.4] text-white",
176
- "transition-opacity duration-(--duration-fast) ease-standard data-ending-style:opacity-0 data-starting-style:opacity-0",
184
+ // 三样各走各的:淡入淡出 200ms;进场的位移 300ms;没划够就松手,滑回原位 200ms。
185
+ // 拖动中基元把过渡整个关掉,位置贴着手指
186
+ "transition-[opacity,translate,transform] duration-[var(--duration-fast),var(--duration-base),var(--duration-fast)] ease-[var(--ease-standard),var(--ease-exit),var(--ease-standard)]",
187
+ "data-ending-style:opacity-0 data-starting-style:opacity-0",
188
+ // 拖的时候不选中文字:用鼠标拖会把字选上,下一次再拖,浏览器当成"拖这段字",手势就被取消了
189
+ "data-swiping:select-none",
177
190
  // 焦点环画在里面:它是按深色底取的颜色,画到外面会压在亮色的页面上
178
191
  focusRingInset,
179
192
  placementClass[placement].root,
@@ -11,6 +11,7 @@ import {
11
11
  import { createPortal } from "react-dom";
12
12
  import { usePortalScope } from "../../hooks/usePortalScope";
13
13
  import { cn } from "../../lib/cn";
14
+ import { enterFromSide } from "../../lib/motion";
14
15
 
15
16
  export type TooltipSide = "top" | "bottom" | "left" | "right";
16
17
  export type TooltipAlign = "start" | "center" | "end";
@@ -152,8 +153,11 @@ export function Tooltip({
152
153
  className={cn(
153
154
  // 透明的边线:高对比模式下背景色被系统覆盖,边线会自己显形
154
155
  "relative max-w-64 border border-transparent bg-surface-raised px-2.5 py-1.5 text-sm wrap-anywhere text-ink",
155
- "origin-(--transform-origin) transition-opacity duration-(--duration-fast) ease-standard",
156
+ // 进场从触发元素那一侧挪过来 4px,退场只淡出。小件:进出都是 200ms。
157
+ // data-instant(键盘聚焦、移到相邻的提示)时基元要它立刻出现,整个不过渡
158
+ "origin-(--transform-origin) transition-[opacity,translate] duration-(--duration-fast) ease-standard",
156
159
  "data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0",
160
+ enterFromSide,
157
161
  className,
158
162
  )}
159
163
  >
@@ -16,6 +16,12 @@ export type ViewfinderProps = ComponentProps<"div"> & {
16
16
  size?: ViewfinderSize;
17
17
  /** 正中放一个小准星,标出焦点 */
18
18
  crosshair?: boolean;
19
+ /**
20
+ * 挂上的时候取景角落位:八段短线各自从角上伸出来,播一次。
21
+ * 默认不动——取景角平时是静态的装饰。要它晚一点再动(等外面那一层先淡入完),
22
+ * 在外层设 `--bracket-delay`
23
+ */
24
+ animate?: boolean;
19
25
  /**
20
26
  * 四个角上的读数:坐标、比例、版本、时间码。
21
27
  * 必须是真实的数据;没有就不放。
@@ -44,6 +50,7 @@ const readoutPosition: Record<keyof ViewfinderReadouts, string> = {
44
50
  export function Viewfinder({
45
51
  size = "sm",
46
52
  crosshair = false,
53
+ animate = false,
47
54
  readouts,
48
55
  className,
49
56
  children,
@@ -58,6 +65,8 @@ export function Viewfinder({
58
65
  decor,
59
66
  "corner-brackets absolute inset-3 [--bracket-color:currentColor] [--bracket-offset:0px] [--bracket-width:1.5px]",
60
67
  armClass[size],
68
+ animate &&
69
+ "after:animate-bracket-in after:[animation-delay:var(--bracket-delay,0s)]",
61
70
  )}
62
71
  />
63
72
  {crosshair && (
package/src/lib/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  | `merge-refs.ts` | `mergeRefs()`:组件自己要用节点、同时还得把它交给使用方时,把几个 ref 合成一个 |
10
10
  | `defined.ts` | `defined()`:去掉值是 `undefined` 的属性。把控件的属性合并到使用方给的元素上(`render`)之前用,免得把那个元素自己写的盖掉 |
11
11
  | `decor.ts` | `decor`:装饰层共用的类 |
12
+ | `motion.ts` | `enterFromSide`:浮层面板进场的方向位移——按基元给的 `data-side`,起点往触发元素那一侧偏 `--motion-shift`。见 [动效](../../../../docs/design/foundations/motion.md#方向位移) |
12
13
  | `date.ts` | 日期的纯函数:加减日和月、比较、把两天排成先后、夹到范围内、月历的六行七列。日期一律是 `YYYY-MM-DD` 的字符串,全部按 UTC 算,不碰本地时区。`Calendar`、`DatePicker`、`Schedule` 共用,不对外导出 |
13
14
  | `file.ts` | 文件的纯函数:按 `accept` 认类型(扩展名、MIME、`image/*` 通配)、把字节数写成 `1.2 MB`、取扩展名、判断是不是同一个文件。`FileUpload` 用,不对外导出 |
14
15
 
package/src/lib/cn.ts CHANGED
@@ -24,6 +24,8 @@ const twMerge = extendTailwindMerge({
24
24
  "turn-in",
25
25
  "fade-in",
26
26
  "indeterminate",
27
+ "bracket-in",
28
+ "shift-in",
27
29
  ],
28
30
  },
29
31
  },
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 浮层从触发它的那一侧来:进场的起点往触发处偏 `--motion-shift`,走完归位。
3
+ * 用在基元的浮层面板上——它们带着 `data-side`(面板在触发元素的哪一侧)。
4
+ *
5
+ * 位移只写在 `data-starting-style` 上:退场不走位移,只淡出。
6
+ * 用的地方自己给 `translate` 加过渡。`inline-start` / `inline-end` 按从左往右的排版算。
7
+ */
8
+ export const enterFromSide = [
9
+ "data-starting-style:data-[side=bottom]:-translate-y-(--motion-shift)",
10
+ "data-starting-style:data-[side=top]:translate-y-(--motion-shift)",
11
+ "data-starting-style:data-[side=right]:-translate-x-(--motion-shift)",
12
+ "data-starting-style:data-[side=left]:translate-x-(--motion-shift)",
13
+ "data-starting-style:data-[side=inline-end]:-translate-x-(--motion-shift)",
14
+ "data-starting-style:data-[side=inline-start]:translate-x-(--motion-shift)",
15
+ ].join(" ");
@@ -336,6 +336,13 @@
336
336
  --duration-slow: 400ms;
337
337
  --duration-slower: 600ms;
338
338
 
339
+ /*
340
+ * 进场的方向位移:浮层从触发它的那一侧来。只是提示方向,不是滑行。
341
+ * 大的那一档给大面积的和贴着视口边缘出来的(全屏菜单的栏目、轻提示)
342
+ */
343
+ --motion-shift: 4px;
344
+ --motion-shift-lg: 8px;
345
+
339
346
  /* —— 母题尺寸 —— */
340
347
  --cut-sm: 6px;
341
348
  --cut-md: 10px;
@@ -368,6 +375,8 @@
368
375
  --animate-fade-in: ef-fade-in 300ms ease-out both;
369
376
  --animate-indeterminate: ef-indeterminate 1.2s linear infinite alternate;
370
377
  --animate-pulse: ef-pulse 2s ease-in-out infinite;
378
+ --animate-bracket-in: ef-bracket-in 200ms ease-out both;
379
+ --animate-shift-in: ef-shift-in 300ms ease-out both;
371
380
 
372
381
  /* 三次短促闪烁后点亮(官网实测的关键帧节奏) */
373
382
  @keyframes ef-flicker {
@@ -507,6 +516,37 @@
507
516
  opacity: 0.6;
508
517
  }
509
518
  }
519
+
520
+ /*
521
+ * 角括号落位:八段短线各自从角的顶点伸到满长。写成 after:animate-bracket-in,
522
+ * 和 corner-brackets 一起用——八层的顺序(横、竖交替)跟着 utilities.css 里那八层背景走。
523
+ * 只写起点:终点就是括号自己的尺寸。只动背景的尺寸,括号不会比平时多占一点地方
524
+ */
525
+ @keyframes ef-bracket-in {
526
+ from {
527
+ background-size:
528
+ 0 var(--_w),
529
+ var(--_w) 0,
530
+ 0 var(--_w),
531
+ var(--_w) 0,
532
+ 0 var(--_w),
533
+ var(--_w) 0,
534
+ 0 var(--_w),
535
+ var(--_w) 0;
536
+ }
537
+ }
538
+
539
+ /*
540
+ * 位移入场:从偏着的地方淡入归位。往哪边偏、偏多少由用的地方给
541
+ * (--shift-x / --shift-y,量用 --motion-shift 那两档),不给就只是淡入。
542
+ * 只写起点:终点就是元素自己的样子
543
+ */
544
+ @keyframes ef-shift-in {
545
+ from {
546
+ opacity: 0;
547
+ translate: var(--shift-x, 0) var(--shift-y, 0);
548
+ }
549
+ }
510
550
  }
511
551
 
512
552
  /* ------------------------------------------------------------------ */
@@ -563,6 +603,9 @@
563
603
  animation-duration: 0.01ms !important;
564
604
  animation-iteration-count: 1 !important;
565
605
  transition-duration: 0.01ms !important;
606
+ /* 延迟也归零:不然错开的入场会变成一项一项蹦出来 */
607
+ animation-delay: 0s !important;
608
+ transition-delay: 0s !important;
566
609
  scroll-behavior: auto !important;
567
610
  }
568
611
  }
@@ -121,7 +121,8 @@
121
121
  /* ------------------------------------------------------------------ */
122
122
  /* 角括号:四角各一个 L 形短线,用 ::after 的背景画,不占用 outline。 */
123
123
  /* 宿主要自己定位(relative 或 absolute);括号画在宿主之外, */
124
- /* 所以宿主和它的裁切祖先之间要留出 --bracket-offset 的空隙 */
124
+ /* 所以宿主和它的裁切祖先之间要留出 --bracket-offset 的空隙。 */
125
+ /* 要它出现时"落位",再加 after:animate-bracket-in(关键帧在 theme.css) */
125
126
  /* ------------------------------------------------------------------ */
126
127
 
127
128
  @utility corner-brackets {