@endfield-ui/react 0.2.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.
Files changed (77) hide show
  1. package/NOTICE.md +2 -2
  2. package/README.md +103 -8
  3. package/dist/index.d.ts +78 -17
  4. package/dist/index.js +1184 -264
  5. package/dist/styles.css +1 -1
  6. package/package.json +1 -1
  7. package/src/components/README.md +11 -2
  8. package/src/components/alert/Alert.tsx +63 -1
  9. package/src/components/avatar/Avatar.tsx +5 -2
  10. package/src/components/bracket-title/BracketTitle.tsx +32 -3
  11. package/src/components/button/Button.tsx +37 -5
  12. package/src/components/calendar/Calendar.tsx +49 -5
  13. package/src/components/checkbox/Checkbox.tsx +1 -0
  14. package/src/components/checkbox/choice-style.ts +13 -5
  15. package/src/components/chip/capsule-style.ts +18 -3
  16. package/src/components/combobox/Combobox.tsx +10 -0
  17. package/src/components/completion-banner/CompletionBanner.tsx +30 -2
  18. package/src/components/corner-brackets/CornerBrackets.tsx +2 -1
  19. package/src/components/dash-indicator/DashIndicator.tsx +5 -2
  20. package/src/components/data-row/DataRow.tsx +2 -1
  21. package/src/components/date-picker/DateField.tsx +5 -1
  22. package/src/components/dialog/Dialog.tsx +4 -2
  23. package/src/components/dropdown-menu/MenuCheck.tsx +4 -2
  24. package/src/components/dropdown-menu/menu-style.ts +22 -5
  25. package/src/components/empty-state/EmptyState.tsx +5 -0
  26. package/src/components/field/Field.tsx +39 -7
  27. package/src/components/file-upload/FileUpload.tsx +79 -20
  28. package/src/components/icon-button/IconButton.tsx +18 -5
  29. package/src/components/image-viewer/ImageViewer.tsx +58 -18
  30. package/src/components/input/control-box.ts +23 -2
  31. package/src/components/input/value-chip.ts +3 -2
  32. package/src/components/item-slot/ItemSlot.tsx +2 -1
  33. package/src/components/loader/Loader.tsx +12 -41
  34. package/src/components/nav-menu/NavMenu.tsx +17 -3
  35. package/src/components/otp-input/OtpInput.tsx +13 -4
  36. package/src/components/page-header/PageHeader.tsx +39 -3
  37. package/src/components/pagination/Pagination.tsx +2 -0
  38. package/src/components/progress/Progress.tsx +10 -3
  39. package/src/components/progress/ProgressRing.tsx +19 -4
  40. package/src/components/radio/Radio.tsx +2 -0
  41. package/src/components/rolling-number/RollingNumber.tsx +125 -0
  42. package/src/components/rolling-number/index.ts +1 -0
  43. package/src/components/schedule/Schedule.tsx +5 -3
  44. package/src/components/segmented-control/SegmentedControl.tsx +25 -4
  45. package/src/components/skeleton/Skeleton.tsx +4 -1
  46. package/src/components/slider/Slider.tsx +26 -5
  47. package/src/components/sparkline/Sparkline.tsx +29 -5
  48. package/src/components/spinner/Spinner.tsx +6 -2
  49. package/src/components/stepper/Stepper.tsx +4 -0
  50. package/src/components/steps/Steps.tsx +30 -9
  51. package/src/components/switch/Switch.tsx +26 -2
  52. package/src/components/table/Table.tsx +33 -9
  53. package/src/components/tabs/Tabs.tsx +55 -9
  54. package/src/components/tag-input/TagInput.tsx +82 -43
  55. package/src/components/textarea/Textarea.tsx +2 -1
  56. package/src/components/timeline/Timeline.tsx +4 -3
  57. package/src/components/toast/Toast.tsx +18 -5
  58. package/src/components/toc/Toc.tsx +89 -7
  59. package/src/components/toolbar/Toolbar.tsx +24 -0
  60. package/src/components/tooltip/Tooltip.tsx +5 -1
  61. package/src/components/viewfinder/Viewfinder.tsx +9 -0
  62. package/src/hooks/README.md +8 -2
  63. package/src/hooks/index.ts +1 -0
  64. package/src/hooks/useAddedKeys.ts +26 -0
  65. package/src/hooks/useFlip.ts +211 -0
  66. package/src/hooks/useIndicator.ts +135 -0
  67. package/src/hooks/useLingering.ts +122 -0
  68. package/src/hooks/usePresence.ts +100 -0
  69. package/src/hooks/useReducedMotion.ts +26 -0
  70. package/src/hooks/useTheme.ts +44 -4
  71. package/src/index.ts +1 -0
  72. package/src/lib/README.md +2 -0
  73. package/src/lib/cn.ts +3 -0
  74. package/src/lib/inline-style.ts +12 -0
  75. package/src/lib/motion.ts +90 -0
  76. package/src/styles/theme.css +81 -2
  77. package/src/styles/utilities.css +2 -1
package/dist/index.js CHANGED
@@ -76,7 +76,10 @@ const twMerge = extendTailwindMerge({ extend: { theme: {
76
76
  "slide-in",
77
77
  "turn-in",
78
78
  "fade-in",
79
- "indeterminate"
79
+ "indeterminate",
80
+ "bracket-in",
81
+ "shift-in",
82
+ "wipe-in"
80
83
  ]
81
84
  } } });
82
85
  function cn(...inputs) {
@@ -168,6 +171,74 @@ function AccordionItem({ value, title, extra, disabled, className, children, ...
168
171
  });
169
172
  }
170
173
  //#endregion
174
+ //#region src/hooks/usePresence.ts
175
+ /** 循环的动画(加载指示那一类)等不到头,不等 */
176
+ function ends(animation) {
177
+ return animation.effect?.getComputedTiming().iterations !== Infinity;
178
+ }
179
+ /**
180
+ * 等这个元素上**正在跑的**、有尽头的过渡和动画走完,再调 `done`;一个都没有
181
+ * (没有写退场的样式、测试环境里没有动效)就当场调。返回的函数取消这一次等待。
182
+ */
183
+ function afterMotion(node, done) {
184
+ let live = true;
185
+ const finish = () => {
186
+ if (live) done();
187
+ };
188
+ const running = typeof node?.getAnimations === "function" ? node.getAnimations().filter(ends) : [];
189
+ if (running.length === 0) finish();
190
+ else Promise.allSettled(running.map((animation) => animation.finished)).then(finish);
191
+ return () => {
192
+ live = false;
193
+ };
194
+ }
195
+ /**
196
+ * 退场后再卸载:`open` 变成假之后元素先留着,等它身上的动效走完才卸载。
197
+ *
198
+ * 等的是这个元素上**正在跑的**过渡和动画,不另抄一份时长;一个都没有
199
+ * (没有写退场的样式、测试环境里没有动效)就当场卸载。退场途中又打开:不卸载,
200
+ * 过渡自己折回去。
201
+ */
202
+ function usePresence(open, { onLeave, onExited } = {}) {
203
+ const ref = useRef(null);
204
+ const [mounted, setMounted] = useState(open);
205
+ const [entered, setEntered] = useState(false);
206
+ if (open && !mounted) {
207
+ setMounted(true);
208
+ setEntered(true);
209
+ }
210
+ const leaving = !open && mounted;
211
+ const handlers = useRef({
212
+ onLeave,
213
+ onExited
214
+ });
215
+ useLayoutEffect(() => {
216
+ handlers.current = {
217
+ onLeave,
218
+ onExited
219
+ };
220
+ });
221
+ useLayoutEffect(() => {
222
+ if (!leaving) return;
223
+ const node = ref.current;
224
+ const undo = node ? handlers.current.onLeave?.(node) : void 0;
225
+ const cancel = afterMotion(node, () => {
226
+ setMounted(false);
227
+ handlers.current.onExited?.();
228
+ });
229
+ return () => {
230
+ cancel();
231
+ undo?.();
232
+ };
233
+ }, [leaving]);
234
+ return {
235
+ ref,
236
+ mounted,
237
+ leaving,
238
+ entered
239
+ };
240
+ }
241
+ //#endregion
171
242
  //#region src/icons/Close.tsx
172
243
  const Close = createIcon("Close", /* @__PURE__ */ jsx("path", { d: "M5 5l14 14M19 5L5 19" }));
173
244
  //#endregion
@@ -191,6 +262,15 @@ const StatusSuccess = createIcon("StatusSuccess", /* @__PURE__ */ jsxs(Fragment$
191
262
  /** 三角:警告 */
192
263
  const StatusWarning = createIcon("StatusWarning", /* @__PURE__ */ jsx("path", { d: "M12 4l9 16H3zM12 10v5M12 16.5v2" }));
193
264
  //#endregion
265
+ //#region src/lib/merge-refs.ts
266
+ /** 把几个 ref 合成一个:组件自己要用节点,同时还得把它交给使用方。 */
267
+ function mergeRefs(...refs) {
268
+ return (node) => {
269
+ for (const ref of refs) if (typeof ref === "function") ref(node);
270
+ else if (ref) ref.current = node;
271
+ };
272
+ }
273
+ //#endregion
194
274
  //#region src/components/alert/Alert.tsx
195
275
  const toneIcon$1 = {
196
276
  info: StatusInfo,
@@ -210,15 +290,43 @@ const toneText$1 = {
210
290
  warning: "text-warning",
211
291
  danger: "text-danger"
212
292
  };
293
+ function collapseOut(node) {
294
+ const { style } = node;
295
+ const before = style.cssText;
296
+ let gap = 0;
297
+ if (node.parentElement) {
298
+ const layout = getComputedStyle(node.parentElement);
299
+ if (layout.display.includes("grid") || layout.display.includes("flex") && layout.flexDirection.startsWith("column")) gap = Number.parseFloat(layout.rowGap) || 0;
300
+ }
301
+ style.height = `${node.getBoundingClientRect().height}px`;
302
+ node.getBoundingClientRect();
303
+ style.height = "0px";
304
+ style.minHeight = "0px";
305
+ style.paddingBlock = "0px";
306
+ style.borderBlockWidth = "0px";
307
+ style.marginTop = "0px";
308
+ style.marginBottom = `${-gap}px`;
309
+ style.opacity = "0";
310
+ return () => {
311
+ style.cssText = before;
312
+ };
313
+ }
213
314
  /** 页面内的一条常驻消息。需要用户处理的重要信息用它,不用会消失的轻提示。 */
214
- function Alert({ tone = "info", title, action, onClose, closeLabel = "关闭", icon, className, children, ...props }) {
315
+ function Alert({ tone = "info", title, action, onClose, open = true, onExited, closeLabel = "关闭", icon, className, ref, children, ...props }) {
316
+ const { ref: rootRef, mounted, leaving } = usePresence(open, {
317
+ onLeave: collapseOut,
318
+ onExited
319
+ });
320
+ if (!mounted) return null;
215
321
  const Icon = toneIcon$1[tone];
216
322
  const graphic = icon === void 0 ? /* @__PURE__ */ jsx(Icon, { size: 20 }) : icon;
217
323
  return /* @__PURE__ */ jsxs("div", {
218
324
  role: tone === "danger" || tone === "warning" ? "alert" : "status",
219
325
  ...props,
326
+ ref: mergeRefs(rootRef, ref),
220
327
  "data-tone": tone,
221
- className: cn("flex items-start gap-3 border border-l-4 border-line bg-surface-raised py-3 pr-3 pl-3 text-sm text-ink", toneBar[tone], className),
328
+ inert: leaving,
329
+ className: cn("flex items-start gap-3 border border-l-4 border-line bg-surface-raised py-3 pr-3 pl-3 text-sm text-ink", toneBar[tone], leaving && ["pointer-events-none overflow-hidden", "transition-[height,padding,margin,border-width,opacity] duration-(--duration-fast) ease-standard"], className),
222
330
  children: [
223
331
  graphic !== null && /* @__PURE__ */ jsx("span", {
224
332
  "aria-hidden": "true",
@@ -276,7 +384,7 @@ function Avatar({ name, src, alt = name ?? "", size = "md", selected = false, cl
276
384
  },
277
385
  "data-size": size,
278
386
  "data-selected": selected ? "" : void 0,
279
- className: cn("relative inline-flex shrink-0 items-center justify-center overflow-clip rounded-full bg-surface-muted font-medium text-ink-secondary select-none", sizeClass$13[size], selected && "shadow-[0_0_0_2px_var(--ef-surface),0_0_0_5px_var(--ef-action),0_0_0_6px_var(--ef-ink)]", className),
387
+ className: cn("relative inline-flex shrink-0 items-center justify-center overflow-clip rounded-full bg-surface-muted font-medium text-ink-secondary select-none", sizeClass$13[size], selected && "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]", className),
280
388
  children: [src && /* @__PURE__ */ jsx(Avatar$1.Image, {
281
389
  src,
282
390
  alt: "",
@@ -317,7 +425,7 @@ const ChevronRight = createIcon("ChevronRight", /* @__PURE__ */ jsx("path", { d:
317
425
  //#endregion
318
426
  //#region src/components/icon-button/IconButton.tsx
319
427
  const sizeClass$12 = {
320
- sm: "size-8 after:absolute after:-inset-1 after:content-[''] [&_svg]:size-4",
428
+ sm: "size-8 after:absolute after:-inset-1 after:content-[''] forced-colors:after:-inset-[5px] [&_svg]:size-4",
321
429
  md: "size-10 [&_svg]:size-5",
322
430
  lg: "size-12 [&_svg]:size-6"
323
431
  };
@@ -333,6 +441,10 @@ const pressedClass = {
333
441
  accent: "bg-action text-on-action active:bg-action-pressed",
334
442
  inverse: "bg-surface-inverse text-accent-ink-inverse"
335
443
  };
444
+ const outlineForced = "forced-colors:border";
445
+ const enabledForced = "forced-colors:border-[ButtonText]";
446
+ const pressedForced = "forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]";
447
+ const disabledForced = "forced-colors:border-[GrayText]";
336
448
  const disabledClass = {
337
449
  plain: "cursor-not-allowed bg-surface-sunken text-ink-disabled",
338
450
  floating: "cursor-not-allowed rounded-full bg-neutral-50 text-neutral-400 shadow-sm",
@@ -347,7 +459,7 @@ function IconButton({ variant = "plain", size = "md", pressed, disabled = false,
347
459
  "aria-pressed": pressed,
348
460
  "data-variant": variant,
349
461
  "data-size": size,
350
- className: cn("relative inline-flex shrink-0 items-center justify-center", "transition-colors duration-(--duration-fast) ease-standard", focusRing, sizeClass$12[size], disabled ? disabledClass[variant] : pressed ? pressedClass[variant] : enabledClass$1[variant], className),
462
+ className: cn("relative inline-flex shrink-0 items-center justify-center", "transition-colors duration-(--duration-fast) ease-standard", focusRing, sizeClass$12[size], outlineForced, disabled ? [disabledClass[variant], disabledForced] : pressed ? [pressedClass[variant], pressedForced] : [enabledClass$1[variant], enabledForced], className),
351
463
  children
352
464
  });
353
465
  }
@@ -606,27 +718,69 @@ function Badge({ count, max = 99, showZero = false, dot = false, label, classNam
606
718
  });
607
719
  }
608
720
  //#endregion
721
+ //#region src/hooks/useInView.ts
722
+ /**
723
+ * 元素第一次进入视口后返回 true,之后不再变回 false——入场动画只播一次。
724
+ */
725
+ function useInView({ disabled = false, rootMargin = "0px", threshold = 0 } = {}) {
726
+ const ref = useRef(null);
727
+ const [inView, setInView] = useState(false);
728
+ useEffect(() => {
729
+ const node = ref.current;
730
+ if (disabled || inView || !node) return;
731
+ if (typeof IntersectionObserver === "undefined") {
732
+ setInView(true);
733
+ return;
734
+ }
735
+ const observer = new IntersectionObserver((entries) => {
736
+ if (entries.some((entry) => entry.isIntersecting)) {
737
+ setInView(true);
738
+ observer.disconnect();
739
+ }
740
+ }, {
741
+ rootMargin,
742
+ threshold
743
+ });
744
+ observer.observe(node);
745
+ return () => observer.disconnect();
746
+ }, [
747
+ disabled,
748
+ inView,
749
+ rootMargin,
750
+ threshold
751
+ ]);
752
+ return [ref, inView];
753
+ }
754
+ //#endregion
609
755
  //#region src/components/bracket-title/BracketTitle.tsx
610
756
  const bracket = "font-medium text-ink-tertiary";
611
757
  /**
612
758
  * 方括号标题 `[ 名称 ]`:人物、地点、物品的名称——"一个被归档的条目"。
613
759
  * 普通的分节标题用 `SectionTitle`。字号用 `className` 改,默认是名称用的 `text-4xl`。
614
760
  */
615
- function BracketTitle({ level = 3, className, children, ...props }) {
761
+ function BracketTitle({ level = 3, animate = true, className, children, ref, ...props }) {
616
762
  const Heading = `h${level}`;
763
+ const [inViewRef, inView] = useInView({ disabled: !animate });
764
+ const pending = animate && !inView;
765
+ const playing = animate && inView;
766
+ const bracketEnter = cn(pending && "opacity-0", playing && "animate-fade-in");
617
767
  return /* @__PURE__ */ jsxs(Heading, {
618
768
  ...props,
769
+ ref: mergeRefs(inViewRef, ref),
619
770
  className: cn("text-4xl font-bold wrap-anywhere text-ink", className),
620
771
  children: [
621
772
  /* @__PURE__ */ jsx("span", {
622
773
  "aria-hidden": "true",
623
- className: cn(bracket, "mr-[0.4em]"),
774
+ className: cn(bracket, "mr-[0.4em]", bracketEnter),
624
775
  children: "["
625
776
  }),
626
- children,
777
+ /* @__PURE__ */ jsx("span", {
778
+ className: cn(pending && "opacity-0", playing && "animate-fade-in [animation-delay:100ms]") || void 0,
779
+ children
780
+ }),
627
781
  /* @__PURE__ */ jsx("span", {
628
782
  "aria-hidden": "true",
629
- className: cn(bracket, "ml-[0.4em]"),
783
+ className: cn(bracket, "ml-[0.4em]", bracketEnter),
630
784
  children: "]"
631
785
  })
632
786
  ]
@@ -745,7 +899,7 @@ const sizeClass$10 = {
745
899
  sm: "size-3",
746
900
  md: "size-4"
747
901
  };
748
- const square = "animate-spin bg-current motion-reduce:rotate-45 motion-reduce:animate-none";
902
+ const square = ["animate-spin bg-current motion-reduce:rotate-45 motion-reduce:animate-none", "forced-colors:bg-[CanvasText]"].join(" ");
749
903
  /**
750
904
  * 行内加载指示:一个匀速旋转的小方块,颜色继承文字。
751
905
  * 圆环形的用 `<ProgressRing size={16} />`;整块内容加载用 `Skeleton`。
@@ -835,13 +989,17 @@ const enabledClass = {
835
989
  back: "hatch hatch-mid bg-control text-white shadow-xs hover:bg-neutral-600 active:bg-control-pressed",
836
990
  text: "text-ink active:text-ink-secondary"
837
991
  };
992
+ const forcedOutline = "forced-colors:border";
993
+ const forcedEnabled = "forced-colors:border-[ButtonText]";
994
+ const forcedDisabled = "forced-colors:border-[GrayText] forced-colors:text-[GrayText]";
838
995
  function Button(props) {
839
996
  const { variant = "control", size = "md", iconStart, iconEnd, loading = false, loadingText, block = false, disabled = false, className, children, ...rest } = props;
840
997
  const inert = disabled || loading;
841
998
  const isText = variant === "text";
842
999
  const marker = markerColor[variant];
843
1000
  const iconSize = inlineIconSize[size];
844
- const classes = cn("group relative inline-flex shrink-0 items-center justify-center font-medium leading-none whitespace-nowrap select-none", "transition-colors duration-(--duration-fast) ease-standard", focusRing, sizeClass$9[size], marker && markerPadding[size], variant === "action" && actionPadding[size], variant === "back" && backPadding[size], isText && "h-auto border-b border-current py-1", disabled ? isText ? "cursor-not-allowed text-ink-disabled" : "cursor-not-allowed bg-disabled text-on-disabled" : enabledClass[variant], loading && "pointer-events-none", block && "flex w-full", className);
1001
+ const forcedMark = disabled ? "forced-colors:bg-[GrayText]" : "forced-colors:bg-[ButtonText]";
1002
+ const classes = cn("group relative inline-flex shrink-0 items-center justify-center font-medium leading-none whitespace-nowrap select-none", "transition-colors duration-(--duration-fast) ease-standard", focusRing, sizeClass$9[size], marker && markerPadding[size], variant === "action" && actionPadding[size], variant === "back" && backPadding[size], isText && "h-auto border-b border-current py-1", disabled ? isText ? "cursor-not-allowed text-ink-disabled" : "cursor-not-allowed bg-disabled text-on-disabled" : enabledClass[variant], !isText && forcedOutline, disabled ? forcedDisabled : !isText && forcedEnabled, loading && !disabled && "forced-colors:text-[ButtonText]", loading && "pointer-events-none", block && "flex w-full", className);
845
1003
  let start = iconStart;
846
1004
  let end = iconEnd;
847
1005
  if (variant === "back" && start === void 0) start = loading ? /* @__PURE__ */ jsx("span", {
@@ -852,7 +1010,7 @@ function Button(props) {
852
1010
  },
853
1011
  children: /* @__PURE__ */ jsx(Spinner, {
854
1012
  label: null,
855
- className: "size-2"
1013
+ className: cn("size-2", forcedMark)
856
1014
  })
857
1015
  }) : /* @__PURE__ */ jsx(ChevronLeft, { size: iconSize });
858
1016
  if (isText && end === void 0) end = loading ? /* @__PURE__ */ jsx("span", {
@@ -863,7 +1021,7 @@ function Button(props) {
863
1021
  },
864
1022
  children: /* @__PURE__ */ jsx(Spinner, {
865
1023
  label: null,
866
- className: "size-2"
1024
+ className: cn("size-2", forcedMark)
867
1025
  })
868
1026
  }) : /* @__PURE__ */ jsx(ArrowCorner, {
869
1027
  size: iconSize,
@@ -886,11 +1044,11 @@ function Button(props) {
886
1044
  className: cn("pointer-events-none absolute top-1/2 -translate-y-1/2", markerSpinner[size]),
887
1045
  children: /* @__PURE__ */ jsx(Spinner, {
888
1046
  label: null,
889
- className: cn("block size-full", marker)
1047
+ className: cn("block size-full", marker, forcedMark)
890
1048
  })
891
1049
  }) : /* @__PURE__ */ jsx("span", {
892
1050
  "aria-hidden": "true",
893
- className: cn("marker-bar pointer-events-none absolute top-1/2 -translate-y-1/2", "transition-[clip-path,translate] duration-(--duration-base) ease-standard", markerBox[size], disabled ? "bg-on-disabled" : [
1051
+ className: cn("marker-bar pointer-events-none absolute top-1/2 -translate-y-1/2", "transition-[clip-path,translate] duration-(--duration-base) ease-standard", markerBox[size], forcedMark, disabled ? "bg-on-disabled" : [
894
1052
  marker,
895
1053
  "group-hover:marker-arrow group-active:marker-arrow",
896
1054
  markerShift[size]
@@ -901,12 +1059,12 @@ function Button(props) {
901
1059
  end,
902
1060
  variant === "action" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
903
1061
  "aria-hidden": "true",
904
- className: cn("pointer-events-none absolute top-[17.5%] h-[65%]", "transition-colors duration-(--duration-fast) ease-standard", actionDivider[size], disabled ? "bg-on-disabled/20" : "bg-on-action/10 group-hover:bg-on-action/15 group-active:bg-on-action/20")
1062
+ className: cn("pointer-events-none absolute top-[17.5%] h-[65%]", "transition-colors duration-(--duration-fast) ease-standard", actionDivider[size], disabled ? "bg-on-disabled/20" : ["bg-on-action/10 group-hover:bg-on-action/15 group-active:bg-on-action/20", "forced-colors:group-hover:bg-[ButtonText] forced-colors:group-active:bg-[ButtonText]"], forcedMark)
905
1063
  }), loading && /* @__PURE__ */ jsx("span", {
906
1064
  className: cn("pointer-events-none absolute top-1/2 -translate-y-1/2", actionSpinner[size]),
907
1065
  children: /* @__PURE__ */ jsx(Spinner, {
908
1066
  label: null,
909
- className: "block size-full"
1067
+ className: cn("block size-full", forcedMark)
910
1068
  })
911
1069
  })] })
912
1070
  ] });
@@ -1133,6 +1291,30 @@ function Calendar({ range = false, value: valueProp, defaultValue = null, onValu
1133
1291
  pendingFocus.current = false;
1134
1292
  gridRef.current?.querySelector(`[data-date="${focused}"]`)?.focus();
1135
1293
  }, [focused, month]);
1294
+ const turned = useRef({
1295
+ month,
1296
+ until: 0,
1297
+ length: 0
1298
+ });
1299
+ useLayoutEffect(() => {
1300
+ const last = turned.current;
1301
+ if (last.month === month) return;
1302
+ const forward = month > last.month;
1303
+ last.month = month;
1304
+ const grid = gridRef.current;
1305
+ if (!grid) return;
1306
+ const now = performance.now();
1307
+ delete grid.dataset.turn;
1308
+ if (now < last.until) {
1309
+ last.until = now + last.length;
1310
+ return;
1311
+ }
1312
+ grid.offsetWidth;
1313
+ grid.dataset.turn = forward ? "next" : "previous";
1314
+ const running = typeof grid.getAnimations === "function" ? grid.getAnimations()[0] : void 0;
1315
+ last.length = Number(running?.effect?.getComputedTiming().endTime ?? 0);
1316
+ last.until = now + last.length;
1317
+ }, [month]);
1136
1318
  const weeks = useMemo(() => monthGrid(month, weekStartsOn), [month, weekStartsOn]);
1137
1319
  const formats = useMemo(() => ({
1138
1320
  title: new Intl.DateTimeFormat(locale, {
@@ -1230,6 +1412,7 @@ function Calendar({ range = false, value: valueProp, defaultValue = null, onValu
1230
1412
  children: formats.weekdayShort.format(toDateObject(date))
1231
1413
  }, date)) }) }), /* @__PURE__ */ jsx("tbody", {
1232
1414
  ref: gridRef,
1415
+ 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)]",
1233
1416
  onPointerLeave: () => setHovered(null),
1234
1417
  onKeyDown: (event) => {
1235
1418
  if (event.key === "Escape" && anchor !== null) {
@@ -1276,7 +1459,7 @@ function Calendar({ range = false, value: valueProp, defaultValue = null, onValu
1276
1459
  onClick: () => select(date),
1277
1460
  onFocus: () => setFocused(date),
1278
1461
  onPointerEnter: range ? () => setHovered(date) : void 0,
1279
- className: cn("relative flex size-10 items-center justify-center font-tech text-sm tabular-nums", "transition-colors duration-(--duration-fast) ease-standard", focusRingInset, endpoint ? "bg-surface-inverse font-bold text-ink-inverse" : [between ? "bg-ink/10" : previewing && "bg-ink/5", disabled ? "cursor-not-allowed text-ink-disabled line-through" : [outside ? "text-ink-tertiary" : "text-ink", between ? "hover:bg-ink/15" : "hover:bg-ink/5"]], isToday && "after:absolute after:bottom-1.5 after:left-1/2 after:h-0.5 after:w-3 after:-translate-x-1/2 after:content-['']", isToday && (endpoint ? "after:bg-accent-ink-inverse" : "after:bg-accent-ink")),
1462
+ className: cn("relative flex size-10 items-center justify-center font-tech text-sm tabular-nums", "transition-colors duration-(--duration-fast) ease-standard", focusRingInset, endpoint ? "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]" : [between ? "bg-ink/10 forced-colors:border-y-2 forced-colors:border-[Highlight]" : previewing && "bg-ink/5", disabled ? "cursor-not-allowed text-ink-disabled line-through" : [outside ? "text-ink-tertiary" : "text-ink", between ? "hover:bg-ink/15" : "hover:bg-ink/5"]], isToday && "after:absolute after:bottom-1.5 after:left-1/2 after:h-0.5 after:w-3 after:-translate-x-1/2 after:content-['']", isToday && (endpoint ? "after:bg-accent-ink-inverse forced-colors:after:bg-[HighlightText]" : "after:bg-accent-ink forced-colors:after:bg-[CanvasText]")),
1280
1463
  children: dateParts(date).day
1281
1464
  })
1282
1465
  }, date);
@@ -1307,7 +1490,7 @@ function DashIndicator({ count, index, className, ...props }) {
1307
1490
  className: cn("inline-flex items-center gap-1", className),
1308
1491
  children: Array.from({ length: total }, (_, position) => /* @__PURE__ */ jsx("span", {
1309
1492
  "data-current": position === current ? "" : void 0,
1310
- className: cn("h-0.5 transition-[width,background-color] duration-(--duration-base) ease-standard", position === current ? "w-8 bg-accent-ink" : "w-4 bg-line-strong")
1493
+ className: cn("h-0.5 transition-[width,background-color] duration-(--duration-base) ease-standard", position === current ? "w-8 bg-accent-ink forced-colors:bg-[Highlight]" : "w-4 bg-line-strong forced-colors:bg-[CanvasText]")
1311
1494
  }, position))
1312
1495
  });
1313
1496
  }
@@ -1508,24 +1691,18 @@ function CarouselSlide({ title: _title, description: _description, className, ch
1508
1691
  /** 对勾:已选、已完成 */
1509
1692
  const Check = createIcon("Check", /* @__PURE__ */ jsx("path", { d: "M4 12l6 6L20 6" }));
1510
1693
  //#endregion
1511
- //#region src/lib/merge-refs.ts
1512
- /** 把几个 ref 合成一个:组件自己要用节点,同时还得把它交给使用方。 */
1513
- function mergeRefs(...refs) {
1514
- return (node) => {
1515
- for (const ref of refs) if (typeof ref === "function") ref(node);
1516
- else if (ref) ref.current = node;
1517
- };
1518
- }
1519
- //#endregion
1520
1694
  //#region src/components/checkbox/choice-style.ts
1521
- const choiceLabel = "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";
1695
+ const choiceLabel = "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]";
1522
1696
  /** 控件的定位盒:高度等于一行文字,标签换行时控件仍然对齐第一行 */
1523
1697
  const choiceControl = "flex h-[1lh] shrink-0 items-center";
1524
- const choiceBox = "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";
1698
+ const choiceBox = ["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", "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]"].join(" ");
1525
1699
  /** 方格 / 圆的定位盒:菱形比它小一圈,所以居中 */
1526
1700
  const choiceFrame = "relative inline-flex size-5 items-center justify-center";
1527
- /** 压在方格 / 圆正中的记号,默认透明,由 peer 的状态点亮 */
1528
- const choiceMark = "pointer-events-none absolute inset-0 m-auto text-accent-ink-inverse opacity-0 peer-disabled:text-ink-disabled";
1701
+ /**
1702
+ * 压在方格 / 圆正中的记号,默认透明,由 peer 的状态点亮。
1703
+ * 淡入淡出,和方格换底色的那一下同时走——底色有过渡而记号硬切,看起来是两件事
1704
+ */
1705
+ const choiceMark = "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]";
1529
1706
  //#endregion
1530
1707
  //#region src/components/checkbox/Checkbox.tsx
1531
1708
  /**
@@ -1554,7 +1731,7 @@ function Checkbox({ indeterminate = false, invalid = false, onCheckedChange, onC
1554
1731
  type: "checkbox",
1555
1732
  "aria-invalid": invalid || void 0,
1556
1733
  onChange: handleChange,
1557
- className: cn(choiceBox, "indeterminate:border-surface-inverse indeterminate:bg-surface-inverse", focusRing)
1734
+ className: cn(choiceBox, "indeterminate:border-surface-inverse indeterminate:bg-surface-inverse", "forced-colors:indeterminate:border-[Highlight] forced-colors:indeterminate:bg-[Highlight]", focusRing)
1558
1735
  }),
1559
1736
  /* @__PURE__ */ jsx(Check, {
1560
1737
  size: 14,
@@ -1581,11 +1758,20 @@ const capsuleSize = {
1581
1758
  sm: "h-7 px-3 text-xs",
1582
1759
  md: "h-8 px-4 text-sm"
1583
1760
  };
1761
+ const forcedSelected = "forced-colors:forced-color-adjust-none forced-colors:focus-visible:outline-[Highlight]";
1584
1762
  const capsuleState = {
1585
- selected: "border-transparent bg-surface-inverse text-ink-inverse",
1763
+ selected: [
1764
+ "border-transparent bg-surface-inverse text-ink-inverse",
1765
+ "forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
1766
+ forcedSelected
1767
+ ].join(" "),
1586
1768
  rest: "border-line-strong text-ink hover:bg-ink/5",
1587
1769
  disabled: "cursor-not-allowed border-line text-ink-disabled",
1588
- selectedDisabled: "cursor-not-allowed border-transparent bg-disabled text-on-disabled"
1770
+ selectedDisabled: [
1771
+ "cursor-not-allowed border-transparent bg-disabled text-on-disabled",
1772
+ "forced-colors:border-[GrayText] forced-colors:bg-[GrayText] forced-colors:text-[Canvas]",
1773
+ forcedSelected
1774
+ ].join(" ")
1589
1775
  };
1590
1776
  //#endregion
1591
1777
  //#region src/components/chip/FilterChip.tsx
@@ -1707,7 +1893,7 @@ function MenuCheck({ checked, disabled = false }) {
1707
1893
  "data-checked": checked ? "" : void 0,
1708
1894
  className: "flex size-4 shrink-0 items-center justify-center",
1709
1895
  children: /* @__PURE__ */ jsx("span", {
1710
- className: cn("flex size-4 items-center justify-center border-2 transition-colors duration-(--duration-fast) ease-standard choice-diamond:size-3 choice-diamond:rotate-45", disabled ? ["border-ink-disabled bg-surface-sunken text-ink-disabled", checked && "choice-diamond:bg-ink-disabled"] : checked ? "border-surface-inverse bg-surface-inverse text-accent-ink-inverse" : "border-line-strong bg-surface-sunken"),
1896
+ className: cn("flex size-4 items-center justify-center border-2 transition-colors duration-(--duration-fast) ease-standard choice-diamond:size-3 choice-diamond:rotate-45", disabled ? ["border-ink-disabled bg-surface-sunken text-ink-disabled", checked && "choice-diamond:bg-ink-disabled forced-colors:choice-diamond:bg-[GrayText]"] : checked ? "border-surface-inverse bg-surface-inverse text-accent-ink-inverse forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]" : "border-line-strong bg-surface-sunken"),
1711
1897
  children: checked && /* @__PURE__ */ jsx(Check, {
1712
1898
  size: 10,
1713
1899
  strokeWidth: 4,
@@ -1717,14 +1903,85 @@ function MenuCheck({ checked, disabled = false }) {
1717
1903
  });
1718
1904
  }
1719
1905
  //#endregion
1906
+ //#region src/lib/motion.ts
1907
+ /**
1908
+ * 浮层从触发它的那一侧来:进场的起点往触发处偏 `--motion-shift`,走完归位。
1909
+ * 用在基元的浮层面板上——它们带着 `data-side`(面板在触发元素的哪一侧)。
1910
+ *
1911
+ * 位移只写在 `data-starting-style` 上:退场不走位移,只淡出。
1912
+ * 用的地方自己给 `translate` 加过渡。`inline-start` / `inline-end` 按从左往右的排版算。
1913
+ */
1914
+ const enterFromSide = [
1915
+ "data-starting-style:data-[side=bottom]:-translate-y-(--motion-shift)",
1916
+ "data-starting-style:data-[side=top]:translate-y-(--motion-shift)",
1917
+ "data-starting-style:data-[side=right]:-translate-x-(--motion-shift)",
1918
+ "data-starting-style:data-[side=left]:translate-x-(--motion-shift)",
1919
+ "data-starting-style:data-[side=inline-end]:-translate-x-(--motion-shift)",
1920
+ "data-starting-style:data-[side=inline-start]:translate-x-(--motion-shift)"
1921
+ ].join(" ");
1922
+ /**
1923
+ * 按高度收放,不量高度:外面这一层是只有一行的网格,行高在 `0fr` 和 `1fr` 之间过渡;
1924
+ * 里面再套一层 `collapseInner`,裁掉还放不下的那一截。表格的明细行、字段下面的错误文字
1925
+ * 都这么收放——内容后来变高了也跟着走。
1926
+ *
1927
+ * 展开 300ms、收起 200ms。收起由 `data-leaving` 触发,配 `usePresence`:收完再卸载。
1928
+ */
1929
+ const collapse = [
1930
+ "grid grid-rows-[1fr]",
1931
+ "transition-[grid-template-rows] duration-(--duration-base) ease-standard",
1932
+ "data-leaving:grid-rows-[0fr] data-leaving:duration-(--duration-fast)"
1933
+ ].join(" ");
1934
+ /** 只裁纵向:横向照旧,里面的东西该多宽还是多宽 */
1935
+ const collapseInner = "min-h-0 min-w-0 overflow-y-clip";
1936
+ /**
1937
+ * 选中指示:画在容器的 `::after` 上的一块东西,压在容器的底色之上、各项之下。
1938
+ * 位置和尺寸来自 `useIndicator` 写在容器上的变量;换了一项时滑过去(200ms,
1939
+ * 和各项换字色同一拍),没有当前项时原地淡出。
1940
+ *
1941
+ * 容器上没有 `data-indicator`(量不到)时不画——各项照旧自己画选中态,
1942
+ * 所以给各项的选中底加一个 `in-data-indicator:` 的变体把它让出来。
1943
+ * 这里只管定位和过渡:颜色、圆角由用的地方给,几何用 `indicatorBox` 或者自己写。
1944
+ */
1945
+ const indicator = [
1946
+ "relative isolate",
1947
+ "after:pointer-events-none after:absolute after:top-0 after:left-0 after:-z-1",
1948
+ "after:hidden data-indicator:after:block",
1949
+ "after:opacity-0 data-[indicator=on]:after:opacity-100",
1950
+ "after:transition-[translate,width,height,opacity] after:duration-(--duration-fast) after:ease-standard",
1951
+ "data-[indicator=off]:after:transition-[opacity]",
1952
+ "data-indicator-instant:after:transition-none"
1953
+ ].join(" ");
1954
+ /** 指示盖住当前项的整个盒子:分段选择的墨块、胶囊页签的底 */
1955
+ const indicatorBox = ["after:h-(--indicator-h) after:w-(--indicator-w)", "after:translate-x-(--indicator-x) after:translate-y-(--indicator-y)"].join(" ");
1956
+ /**
1957
+ * 补位(`useFlip`):一排里有一项走了,其余的从旧位置滑到新位置。
1958
+ *
1959
+ * `flipBox` 给那一排——走的那一项以它为参照钉在原处,并且压在别的项下面。
1960
+ * 少了一行它就矮了:高度由钩子量着收(200ms),这期间还在滑的项不露到它外面去;
1961
+ * 各行靠上排,撑着的那一段空在底下。
1962
+ * `flipLeaving` 给会走的每一项:带上 `data-leaving` 之后脱离排版(位置由钩子给)、
1963
+ * 原地淡出、点不到。
1964
+ *
1965
+ * 过渡由用的地方自己写:各项的 `translate`(滑回原位)、走的那一项的 `opacity`,
1966
+ * 都是 200ms——这几项多半本来就带着换色的过渡,得写进同一份清单里。
1967
+ */
1968
+ const flipBox = ["relative isolate content-start overflow-y-clip", "transition-[min-height] duration-(--duration-fast) ease-standard"].join(" ");
1969
+ const flipLeaving = ["data-leaving:pointer-events-none data-leaving:absolute data-leaving:-z-1", "data-leaving:opacity-0"].join(" ");
1970
+ //#endregion
1720
1971
  //#region src/components/dropdown-menu/menu-style.ts
1721
1972
  /** 选项要知道自己在哪种面板里,才知道"当前项"怎么画 */
1722
1973
  const MenuVariantContext = createContext("plain");
1723
- /** 面板:直角、1px 线、环境阴影;顶部一条半宽的行动色细条,展开时从 0 伸到一半宽 */
1974
+ /**
1975
+ * 面板:直角、1px 线、环境阴影;顶部一条半宽的行动色细条,展开时从 0 伸到一半宽。
1976
+ *
1977
+ * 进场:淡入(200ms),同时从触发处那一侧挪过来 4px(300ms,和细条同一拍)。
1978
+ * 退场只淡出——位移只写在起点上。两个时长、两条缓动按属性一一对应
1979
+ */
1724
1980
  const menuPanel = [
1725
1981
  "relative min-w-40 border border-line bg-surface-raised py-1 text-ink shadow-sm outline-none",
1726
- "origin-(--transform-origin) transition-opacity duration-(--duration-fast) ease-standard",
1982
+ "origin-(--transform-origin) transition-[opacity,translate] duration-[var(--duration-fast),var(--duration-base)] ease-[var(--ease-standard),var(--ease-exit)]",
1727
1983
  "data-ending-style:opacity-0 data-starting-style:opacity-0",
1984
+ enterFromSide,
1728
1985
  "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-['']",
1729
1986
  "before:transition-transform before:duration-(--duration-base) before:ease-exit data-starting-style:before:scale-x-0",
1730
1987
  "forced-colors:before:hidden"
@@ -1740,10 +1997,19 @@ const menuItem = [
1740
1997
  ].join(" ");
1741
1998
  /** 不是当前项的选项才有悬停底:当前项的底不能被悬停盖掉 */
1742
1999
  const menuItemHover = "data-highlighted:bg-ink/5";
1743
- /** 当前项(下拉选择的已选项、单选菜单项的选中项) */
2000
+ /**
2001
+ * 当前项(下拉选择的已选项、单选菜单项的选中项)。
2002
+ *
2003
+ * `strong` 的当前项是一整行黄底,而面板是深色的局部主题,焦点色也是黄的:
2004
+ * 画在里面的焦点环换成压得住黄底的颜色,并且再往里收 2px——那个颜色是墨色,
2005
+ * 贴着边的话和深色的面板分不开,看上去只是黄底小了一圈。
2006
+ *
2007
+ * 高对比模式下底色都被系统换掉:两种面板的当前项都是左缘一条系统"选中"色的竖条
2008
+ * (`strong` 平时没有这一条,只在这时候画)
2009
+ */
1744
2010
  const menuItemCurrent = {
1745
- plain: "bg-surface-muted font-medium before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-ink before:content-['']",
1746
- strong: "bg-action font-medium text-on-action"
2011
+ plain: "bg-surface-muted font-medium before:absolute before:inset-y-0 before:left-0 before:w-[3px] before:bg-ink before:content-[''] forced-colors:before:bg-[Highlight]",
2012
+ strong: "bg-action font-medium text-on-action focus-visible:-outline-offset-4 focus-visible:outline-on-action forced-colors:before:absolute forced-colors:before:inset-y-0 forced-colors:before:left-0 forced-colors:before:w-[3px] forced-colors:before:bg-[Highlight] forced-colors:before:content-['']"
1747
2013
  };
1748
2014
  const menuGroupLabel = "px-3 pt-2 pb-1 text-xs text-ink-tertiary";
1749
2015
  const menuSeparator = "my-1 h-px bg-line";
@@ -1779,6 +2045,9 @@ function Field({ label, help, error, required = false, disabled = false, group =
1779
2045
  const hasError = error !== void 0 && error !== null && error !== false;
1780
2046
  const hasHelp = help !== void 0 && help !== null && help !== false;
1781
2047
  const describedBy = [hasError && errorId, hasHelp && helpId].filter(Boolean).join(" ") || void 0;
2048
+ const { ref: errorRef, mounted: errorMounted, leaving: errorLeaving, entered: errorEntered } = usePresence(hasError);
2049
+ const [lastError, setLastError] = useState(error);
2050
+ if (hasError && !Object.is(lastError, error)) setLastError(error);
1782
2051
  const context = useMemo(() => ({
1783
2052
  controlId: group ? void 0 : id,
1784
2053
  labelId,
@@ -1802,16 +2071,25 @@ function Field({ label, help, error, required = false, disabled = false, group =
1802
2071
  "data-required": "",
1803
2072
  className: cn("ml-1.5 inline-block size-1.5 rotate-45 align-middle", disabled ? "bg-ink-disabled" : "bg-accent-ink")
1804
2073
  })] });
1805
- const messages = /* @__PURE__ */ jsxs(Fragment$1, { children: [hasError && /* @__PURE__ */ jsxs("p", {
1806
- id: errorId,
1807
- className: "mt-1.5 flex items-start gap-1.5 text-sm text-danger",
1808
- children: [/* @__PURE__ */ jsx(StatusDanger, {
1809
- size: 16,
1810
- className: "mt-0.5 shrink-0"
1811
- }), /* @__PURE__ */ jsx("span", {
1812
- className: "min-w-0 wrap-anywhere",
1813
- children: error
1814
- })]
2074
+ const messages = /* @__PURE__ */ jsxs(Fragment$1, { children: [errorMounted && /* @__PURE__ */ jsx("div", {
2075
+ ref: errorRef,
2076
+ "data-leaving": errorLeaving ? "" : void 0,
2077
+ "aria-hidden": errorLeaving || void 0,
2078
+ className: cn(collapse, "transition-[grid-template-rows,opacity] duration-(--duration-fast) data-leaving:opacity-0", errorEntered && ["starting:grid-rows-[0fr]", "starting:opacity-0"]),
2079
+ children: /* @__PURE__ */ jsx("div", {
2080
+ className: "min-h-0 min-w-0 overflow-y-clip",
2081
+ children: /* @__PURE__ */ jsxs("p", {
2082
+ id: errorId,
2083
+ className: "flex items-start gap-1.5 pt-1.5 text-sm text-danger",
2084
+ children: [/* @__PURE__ */ jsx(StatusDanger, {
2085
+ size: 16,
2086
+ className: "mt-0.5 shrink-0"
2087
+ }), /* @__PURE__ */ jsx("span", {
2088
+ className: "min-w-0 wrap-anywhere",
2089
+ children: hasError ? error : lastError
2090
+ })]
2091
+ })
2092
+ })
1815
2093
  }), hasHelp && /* @__PURE__ */ jsx("p", {
1816
2094
  id: helpId,
1817
2095
  className: cn("mt-1.5 text-sm", disabled ? "text-ink-disabled" : "text-ink-secondary"),
@@ -1880,10 +2158,16 @@ const fill$1 = {
1880
2158
  outline: "bg-surface"
1881
2159
  };
1882
2160
  function controlBox({ variant = "sunken", invalid, disabled, readOnly }) {
1883
- return cn("flex w-full min-w-0 text-ink", frame[variant], "transition-colors duration-(--duration-fast) ease-standard", focusRingWithin, readOnly ? "border-transparent bg-transparent" : fill$1[variant], disabled ? "cursor-not-allowed border-line text-ink-disabled" : invalid ? "border-danger" : !readOnly && "border-line-strong hover:not-focus-within:border-ink-secondary focus-within:border-ink");
2161
+ return cn("flex w-full min-w-0 text-ink", frame[variant], "transition-colors duration-(--duration-fast) ease-standard", focusRingWithin, readOnly ? "border-transparent bg-transparent" : fill$1[variant], readOnly && !disabled && !invalid && "forced-colors:border-[Canvas]", disabled ? "cursor-not-allowed border-line text-ink-disabled" : invalid ? "border-danger" : !readOnly && "border-line-strong hover:not-focus-within:border-ink-secondary focus-within:border-ink");
1884
2162
  }
1885
- /** 框里真正的 `<input>` / `<textarea>`:没有自己的底和边,焦点环画在外框上 */
1886
- const controlElement = "min-w-0 flex-1 bg-transparent outline-none placeholder:text-ink-tertiary disabled:cursor-not-allowed disabled:placeholder:text-ink-disabled";
2163
+ /**
2164
+ * 框里真正的 `<input>` / `<textarea>`:没有自己的底和边,焦点环画在外框上。
2165
+ *
2166
+ * 高对比模式下禁用的字色要自己写明:平时的 `ink-disabled` 写在外框上,系统换掉之后
2167
+ * 只有它自己认得的禁用控件才是灰字色——`<textarea>`、写了 `type` 的 `<input>` 认得,
2168
+ * 没写 `type` 的 `<input>`(输入框、组合框都是)不认,字和没禁用的一个颜色
2169
+ */
2170
+ const controlElement = "min-w-0 flex-1 bg-transparent outline-none placeholder:text-ink-tertiary disabled:cursor-not-allowed disabled:placeholder:text-ink-disabled forced-colors:disabled:text-[GrayText]";
1887
2171
  //#endregion
1888
2172
  //#region src/components/input/value-chip.ts
1889
2173
  /** 小块高 20 / 24 / 32px,跟着外框的三档 */
@@ -1892,9 +2176,9 @@ const valueChipSize = {
1892
2176
  md: "h-6 text-sm",
1893
2177
  lg: "h-8 text-base"
1894
2178
  };
1895
- const valueChip = "flex max-w-full min-w-0 cursor-default items-center bg-surface-muted pl-2 text-ink";
2179
+ const valueChip = "flex max-w-full min-w-0 cursor-default items-center bg-surface-muted pl-2 text-ink forced-colors:border forced-colors:border-[ButtonText]";
1896
2180
  /** 小块右端的删除叉:一个和小块等高的方钮 */
1897
- const valueChipRemove = "flex aspect-square h-full shrink-0 items-center justify-center text-ink-secondary hover:bg-line hover:text-ink";
2181
+ const valueChipRemove = "flex aspect-square h-full shrink-0 items-center justify-center text-ink-secondary hover:bg-line hover:text-ink forced-colors:in-data-disabled:text-[GrayText]";
1898
2182
  /** 和小块排在一起的那个输入框:和小块一样高,这一行才不会比小块高出一截 */
1899
2183
  const valueInputHeight = {
1900
2184
  sm: "h-5",
@@ -1920,6 +2204,7 @@ const currentHighlighted = {
1920
2204
  plain: "data-highlighted:bg-line",
1921
2205
  strong: "data-highlighted:bg-action-pressed"
1922
2206
  };
2207
+ const highlightedForced = "forced-colors:data-highlighted:outline-2 forced-colors:data-highlighted:-outline-offset-2 forced-colors:data-highlighted:outline-[Highlight]";
1923
2208
  const iconButton = "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";
1924
2209
  const byValue = (a, b) => a?.value === b?.value;
1925
2210
  /**
@@ -1951,7 +2236,7 @@ function Combobox(props) {
1951
2236
  const renderOption = (option) => /* @__PURE__ */ jsx(Combobox$1.Item, {
1952
2237
  value: option,
1953
2238
  disabled: option.disabled,
1954
- className: (state) => cn(menuItem, multiple ? [menuItemHover, state.selected && "font-medium"] : state.selected ? [menuItemCurrent[panelVariant], currentHighlighted[panelVariant]] : menuItemHover),
2239
+ className: (state) => cn(menuItem, multiple ? [menuItemHover, state.selected && "font-medium"] : state.selected ? [menuItemCurrent[panelVariant], currentHighlighted[panelVariant]] : menuItemHover, highlightedForced),
1955
2240
  render: multiple ? ({ children, ...itemProps }, state) => /* @__PURE__ */ jsxs("div", {
1956
2241
  ...itemProps,
1957
2242
  children: [/* @__PURE__ */ jsx(MenuCheck, {
@@ -1992,7 +2277,7 @@ function Combobox(props) {
1992
2277
  className: "flex min-w-0 flex-1 flex-wrap items-center gap-1 py-1 pl-2",
1993
2278
  children: /* @__PURE__ */ jsx(Combobox$1.Value, { children: (selected) => /* @__PURE__ */ jsxs(Fragment$1, { children: [selected.map((option) => /* @__PURE__ */ jsxs(Combobox$1.Chip, {
1994
2279
  "aria-label": option.label,
1995
- className: cn(valueChip, "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus", "data-disabled:text-ink-disabled", valueChipSize[size]),
2280
+ className: cn(valueChip, "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus", "data-disabled:text-ink-disabled", "forced-colors:data-disabled:border-[GrayText] forced-colors:data-disabled:text-[GrayText]", valueChipSize[size]),
1996
2281
  children: [/* @__PURE__ */ jsx("span", {
1997
2282
  className: "min-w-0 truncate",
1998
2283
  children: option.label
@@ -2130,13 +2415,17 @@ function GhostText({ variant = "hatch", className, ...props }) {
2130
2415
  * 底是反转块:亮色主题下是深底黄字,暗色主题下是浅底深字。
2131
2416
  * 整块是一个 `data-theme="inverse"` 的局部主题,`action` 里的按钮按这块底色取值。
2132
2417
  */
2133
- function CompletionBanner({ title, word = "COMPLETED", description, action, level = 3, className, children, ...props }) {
2418
+ function CompletionBanner({ title, word = "COMPLETED", description, action, level = 3, animate = true, className, children, ref, ...props }) {
2134
2419
  const Heading = `h${level}`;
2420
+ const [inViewRef, inView] = useInView({ disabled: !animate });
2421
+ const pending = animate && !inView;
2422
+ const playing = animate && inView;
2135
2423
  return /* @__PURE__ */ jsxs("div", {
2136
2424
  role: "status",
2137
2425
  "data-theme": "inverse",
2138
2426
  ...props,
2139
- className: cn("@container relative isolate overflow-clip bg-surface text-ink", className),
2427
+ ref: mergeRefs(inViewRef, ref),
2428
+ className: cn("@container relative isolate overflow-clip bg-surface text-ink", pending && "[clip-path:inset(0_100%_0_0)]", playing && "animate-wipe-in", className),
2140
2429
  children: [word !== null && /* @__PURE__ */ jsx(GhostText, {
2141
2430
  variant: "outline",
2142
2431
  className: "absolute -right-2 -bottom-3 -z-10 hidden text-5xl [--ghost-ink:color-mix(in_srgb,var(--ef-ink)_28%,transparent)] [mask-image:linear-gradient(to_left,black_45%,transparent)] @md:block",
@@ -2144,7 +2433,7 @@ function CompletionBanner({ title, word = "COMPLETED", description, action, leve
2144
2433
  }), /* @__PURE__ */ jsxs("div", {
2145
2434
  className: "flex flex-wrap items-center justify-between gap-x-6 gap-y-3 px-4 py-4 @md:px-6 @md:py-5",
2146
2435
  children: [/* @__PURE__ */ jsxs("div", {
2147
- className: "min-w-0 border-l-4 border-accent-ink pl-3",
2436
+ className: cn("min-w-0 border-l-4 border-accent-ink pl-3", playing && "animate-shift-in [animation-delay:200ms] [--shift-x:calc(var(--motion-shift-lg)*-1)]"),
2148
2437
  children: [
2149
2438
  /* @__PURE__ */ jsx(Heading, {
2150
2439
  className: "text-lg font-bold wrap-anywhere text-accent-ink @md:text-xl",
@@ -2157,7 +2446,7 @@ function CompletionBanner({ title, word = "COMPLETED", description, action, leve
2157
2446
  children
2158
2447
  ]
2159
2448
  }), action && /* @__PURE__ */ jsx("div", {
2160
- className: "shrink-0",
2449
+ className: cn("shrink-0", playing && "animate-fade-in [animation-delay:200ms]"),
2161
2450
  children: action
2162
2451
  })]
2163
2452
  })]
@@ -2205,13 +2494,14 @@ const sizeClass$7 = {
2205
2494
  * 用于矩阵里的单选(物品格、名册卡、图鉴);列表行用左缘色条,页签用底色,都不用它。
2206
2495
  *
2207
2496
  * 括号画在盒子之外 4px:它所在的容器不能贴着盒子裁切。
2497
+ * 出现时八段短线从角的顶点伸到满长(200ms),消失是直接消失。
2208
2498
  * 括号只是给眼睛看的,选中态另外要有 `aria-selected` / `aria-pressed`。
2209
2499
  */
2210
2500
  function CornerBrackets({ visible = true, size = "sm", className, ...props }) {
2211
2501
  return /* @__PURE__ */ jsx("div", {
2212
2502
  ...props,
2213
2503
  "data-visible": visible ? "" : void 0,
2214
- className: cn("relative", visible && "corner-brackets", sizeClass$7[size], className)
2504
+ className: cn("relative", visible && "corner-brackets after:animate-bracket-in", sizeClass$7[size], className)
2215
2505
  });
2216
2506
  }
2217
2507
  //#endregion
@@ -2336,6 +2626,8 @@ function sparklinePath(data, range = {}) {
2336
2626
  area: `${line}L${SIZE} ${SIZE}L0 ${SIZE}Z`
2337
2627
  };
2338
2628
  }
2629
+ const areaForced = "forced-colors:fill-[CanvasText]";
2630
+ const lineForced = "forced-colors:stroke-[CanvasText]";
2339
2631
  const areaClass = {
2340
2632
  info: "fill-data/75",
2341
2633
  danger: "fill-alert/75",
@@ -2352,11 +2644,14 @@ const lineClass = {
2352
2644
  * 行内的小型图表:没有坐标轴、网格和图例,只有一块面积或一条折线。
2353
2645
  * 撑满给它的盒子(默认高 32px、通宽)。它旁边应该有数字——图只是帮着看趋势。
2354
2646
  */
2355
- function Sparkline({ data, variant = "area", tone = "info", min, max, label, className, ...props }) {
2647
+ function Sparkline({ data, variant = "area", tone = "info", min, max, label, animate = true, className, ref, ...props }) {
2356
2648
  const path = sparklinePath(data, {
2357
2649
  min,
2358
2650
  max
2359
2651
  });
2652
+ const [inViewRef, inView] = useInView({ disabled: !animate });
2653
+ const pending = animate && !inView;
2654
+ const playing = animate && inView;
2360
2655
  return /* @__PURE__ */ jsx("svg", {
2361
2656
  viewBox: `0 0 ${SIZE} ${SIZE}`,
2362
2657
  preserveAspectRatio: "none",
@@ -2367,17 +2662,18 @@ function Sparkline({ data, variant = "area", tone = "info", min, max, label, cla
2367
2662
  "data-variant": variant,
2368
2663
  "data-tone": tone,
2369
2664
  ...props,
2370
- className: cn("block h-8 w-full overflow-visible", className),
2665
+ ref: mergeRefs(inViewRef, ref),
2666
+ className: cn("block h-8 w-full overflow-visible", pending && "[clip-path:inset(0_100%_0_0)]", playing && "animate-wipe-in [animation-duration:var(--duration-slower)] [--wipe-bleed:4px]", className),
2371
2667
  children: path && (variant === "area" ? /* @__PURE__ */ jsx("path", {
2372
2668
  d: path.area,
2373
- className: areaClass[tone]
2669
+ className: cn(areaClass[tone], areaForced)
2374
2670
  }) : /* @__PURE__ */ jsx("path", {
2375
2671
  d: path.line,
2376
2672
  fill: "none",
2377
2673
  strokeWidth: 1.5,
2378
2674
  strokeLinejoin: "miter",
2379
2675
  vectorEffect: "non-scaling-stroke",
2380
- className: lineClass[tone]
2676
+ className: cn(lineClass[tone], lineForced)
2381
2677
  }))
2382
2678
  });
2383
2679
  }
@@ -2451,7 +2747,7 @@ const toneClass$1 = {
2451
2747
  danger: "text-danger"
2452
2748
  };
2453
2749
  const favoriteDot = "block size-4 rounded-full";
2454
- const favoriteOn = "bg-action";
2750
+ const favoriteOn = "bg-action forced-colors:bg-[Highlight]";
2455
2751
  const favoriteOff = "border-2 border-ink-tertiary";
2456
2752
  /** 数据行带里的一行。放在 `DataRowList` 里。 */
2457
2753
  function DataRow({ name, categoryColor, favorite, onFavoriteChange, favoriteLabel = "收藏", series, value, tone = "neutral", reference, className, style, ...props }) {
@@ -2576,7 +2872,7 @@ function DateField({ display, placeholder, fields, variant, size, invalid, disab
2576
2872
  }),
2577
2873
  /* @__PURE__ */ jsx(CalendarIcon, {
2578
2874
  size: iconSize,
2579
- className: cn("shrink-0", disabled ? "text-ink-disabled" : "text-ink-secondary")
2875
+ className: cn("shrink-0", disabled ? "text-ink-disabled forced-colors:text-[GrayText]" : "text-ink-secondary forced-colors:text-[ButtonText]")
2580
2876
  })
2581
2877
  ]
2582
2878
  }), fields]
@@ -2868,7 +3164,7 @@ function Dialog({ trigger, open, defaultOpen, onOpenChange, title, description,
2868
3164
  initialFocus: initialFocus ?? firstFocusableIn(contentRef),
2869
3165
  finalFocus,
2870
3166
  "data-size": size,
2871
- className: cn(overlaySurface, "relative flex max-h-full min-h-0 w-full flex-col", "transition-[scale,opacity] duration-(--duration-base) ease-exit", "data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0", sizeClass$5[size], className),
3167
+ className: cn(overlaySurface, "relative flex max-h-full min-h-0 w-full flex-col", "transition-[opacity,translate] duration-[var(--duration-fast),var(--duration-base)] ease-[var(--ease-standard),var(--ease-exit)]", "data-ending-style:opacity-0 data-starting-style:translate-y-(--motion-shift-lg) data-starting-style:opacity-0", sizeClass$5[size], className),
2872
3168
  children: [/* @__PURE__ */ jsxs("div", {
2873
3169
  "data-theme": "dark",
2874
3170
  className: cn(overlayHeader, "min-h-16 justify-center px-12"),
@@ -3200,12 +3496,12 @@ function Viewfinder$1() {
3200
3496
  });
3201
3497
  }
3202
3498
  /** 没有内容时的占位。说清楚为什么为空、接下来能做什么;不要用大幅插画填满。 */
3203
- function EmptyState({ title, description, action, icon, bordered = true, level = 3, className, children, ...props }) {
3499
+ function EmptyState({ title, description, action, icon, bordered = true, level = 3, animate = true, className, children, ...props }) {
3204
3500
  const Heading = `h${level}`;
3205
3501
  const graphic = icon === void 0 ? /* @__PURE__ */ jsx(Viewfinder$1, {}) : icon;
3206
3502
  return /* @__PURE__ */ jsxs("div", {
3207
3503
  ...props,
3208
- className: cn("flex flex-col items-center px-6 py-10 text-center text-ink", bordered && "border border-dashed border-line-strong", className),
3504
+ className: cn("flex flex-col items-center px-6 py-10 text-center text-ink", bordered && "border border-dashed border-line-strong", animate && "animate-fade-in", className),
3209
3505
  children: [
3210
3506
  graphic !== null && /* @__PURE__ */ jsx("div", {
3211
3507
  "aria-hidden": "true",
@@ -3285,7 +3581,8 @@ const trackSize = {
3285
3581
  sm: "h-1",
3286
3582
  md: "h-2"
3287
3583
  };
3288
- const fillClass = "bg-action";
3584
+ const fillClass = "bg-action forced-colors:bg-[Highlight]";
3585
+ const trackClass$1 = "bg-line forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[CanvasText]";
3289
3586
  /**
3290
3587
  * 进度:灰轨 + 黄填充。表示"完成了多少";表示"数量有多少"用图表,那是蓝色的事。
3291
3588
  *
@@ -3304,7 +3601,7 @@ function Progress({ value, max, segments, size = "md", showValue = false, format
3304
3601
  className: "flex h-full gap-1",
3305
3602
  children: Array.from({ length: segments }, (_, index) => /* @__PURE__ */ jsx("span", {
3306
3603
  "data-done": index < done ? "" : void 0,
3307
- className: cn("flex-1", index < done ? fillClass : "bg-line")
3604
+ className: cn("flex-1", index < done ? fillClass : trackClass$1)
3308
3605
  }, index))
3309
3606
  });
3310
3607
  } else if (indeterminate) bar = /* @__PURE__ */ jsx("span", { className: cn("absolute inset-y-0 left-0 w-2/5 animate-indeterminate", "motion-reduce:translate-x-3/4", fillClass) });
@@ -3329,7 +3626,7 @@ function Progress({ value, max, segments, size = "md", showValue = false, format
3329
3626
  "aria-valuenow": indeterminate ? void 0 : current,
3330
3627
  "aria-valuetext": segmented && !indeterminate ? `${current} / ${total}` : void 0,
3331
3628
  ...props,
3332
- className: cn("relative min-w-0 flex-1", trackSize[size], !segmented && "bg-line"),
3629
+ className: cn("relative min-w-0 flex-1", trackSize[size], !segmented && trackClass$1),
3333
3630
  children: bar
3334
3631
  }), text !== null && /* @__PURE__ */ jsx("span", {
3335
3632
  className: "min-w-[4ch] shrink-0 text-right font-tech text-sm leading-none font-bold tabular-nums",
@@ -3448,6 +3745,121 @@ function FileItem({ name, size, status, progress, error, onRemove, onRetry, remo
3448
3745
  });
3449
3746
  }
3450
3747
  //#endregion
3748
+ //#region src/hooks/useAddedKeys.ts
3749
+ const NONE = /* @__PURE__ */ new Set();
3750
+ const same$1 = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
3751
+ /**
3752
+ * 一串 key 里哪几个是后来加进来的。一开始就在的不算;删掉之后又加回来的算。
3753
+ *
3754
+ * 给"新项淡入"用:入场的类只加在这几个上。一开始就有的不该在载入时播一遍,
3755
+ * 已经在的也不会因为别的项变了而重播——它一直带着那个类,动画只在挂上时播一次。
3756
+ */
3757
+ function useAddedKeys(keys) {
3758
+ const [state, setState] = useState({
3759
+ keys,
3760
+ added: NONE
3761
+ });
3762
+ if (!same$1(state.keys, keys)) {
3763
+ const before = new Set(state.keys);
3764
+ setState({
3765
+ keys,
3766
+ added: new Set(keys.filter((key) => !before.has(key) || state.added.has(key)))
3767
+ });
3768
+ }
3769
+ return state.added;
3770
+ }
3771
+ //#endregion
3772
+ //#region src/hooks/useLingering.ts
3773
+ const same = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
3774
+ /**
3775
+ * 新的一份:现在的那几项按现在的顺序,走着的插回它原来挨着的那一项后面。
3776
+ * `keep` 是这一次新走的要不要留。
3777
+ */
3778
+ function merge(before, keys, keep) {
3779
+ const now = new Set(keys);
3780
+ const next = keys.map((key) => ({
3781
+ key,
3782
+ leaving: false
3783
+ }));
3784
+ let anchor = null;
3785
+ for (const entry of before) {
3786
+ if (now.has(entry.key)) {
3787
+ anchor = entry.key;
3788
+ continue;
3789
+ }
3790
+ if (!entry.leaving && !keep) continue;
3791
+ const at = anchor === null ? -1 : next.findIndex((item) => item.key === anchor);
3792
+ next.splice(at + 1, 0, {
3793
+ key: entry.key,
3794
+ leaving: true
3795
+ });
3796
+ anchor = entry.key;
3797
+ }
3798
+ return next;
3799
+ }
3800
+ /**
3801
+ * 列表里被拿掉的项先留着:`keys` 里少了一项,它不当场消失,而是带着 `leaving`
3802
+ * 留在原来的位置上,等它身上的退场走完才真的拿掉(等法和 `usePresence` 是同一套:
3803
+ * 看元素上正在跑的动效,一个都没有就当场拿掉——测试环境里东西删了就是删了)。
3804
+ *
3805
+ * 用法:`entries` 按顺序渲染成 `ref` 那个容器的直接子元素,走着的那几项带上
3806
+ * `data-leaving`(退场的样式挂在它上面),并且对键盘和读屏隐藏。钩子按这个标记找元素。
3807
+ *
3808
+ * - 这一次变化里同时有加进来的(整批换掉、"只收一个"换了一个):新走的不留,照旧直接换。
3809
+ * - 走到一半又加回来:它不再是走着的,留在页面上的还是同一个元素。
3810
+ * - `enabled` 为假时不留新走的(项太多的时候)。
3811
+ */
3812
+ function useLingering(keys, enabled = true) {
3813
+ const ref = useRef(null);
3814
+ const [state, setState] = useState(() => ({
3815
+ keys,
3816
+ entries: merge([], keys, false)
3817
+ }));
3818
+ let { entries } = state;
3819
+ if (!same(state.keys, keys)) {
3820
+ const known = new Set(state.keys);
3821
+ const grew = keys.some((key) => !known.has(key));
3822
+ entries = merge(state.entries, keys, enabled && !grew);
3823
+ setState({
3824
+ keys,
3825
+ entries
3826
+ });
3827
+ }
3828
+ const drop = useCallback((key) => {
3829
+ setState((current) => ({
3830
+ keys: current.keys,
3831
+ entries: current.entries.filter((entry) => !(entry.leaving && entry.key === key))
3832
+ }));
3833
+ }, []);
3834
+ const waits = useRef(/* @__PURE__ */ new Map());
3835
+ useLayoutEffect(() => {
3836
+ const pending = waits.current;
3837
+ const leaving = entries.filter((entry) => entry.leaving).map((entry) => entry.key);
3838
+ for (const [key, cancel] of pending) {
3839
+ if (leaving.includes(key)) continue;
3840
+ cancel();
3841
+ pending.delete(key);
3842
+ }
3843
+ if (leaving.length === 0) return;
3844
+ const nodes = [...ref.current?.children ?? []].filter((node) => node.hasAttribute("data-leaving"));
3845
+ leaving.forEach((key, index) => {
3846
+ if (pending.has(key)) return;
3847
+ pending.set(key, afterMotion(nodes[index] ?? null, () => drop(key)));
3848
+ });
3849
+ }, [entries, drop]);
3850
+ useEffect(() => {
3851
+ const pending = waits.current;
3852
+ return () => {
3853
+ for (const cancel of pending.values()) cancel();
3854
+ pending.clear();
3855
+ };
3856
+ }, []);
3857
+ return {
3858
+ ref,
3859
+ entries
3860
+ };
3861
+ }
3862
+ //#endregion
3451
3863
  //#region src/icons/Upload.tsx
3452
3864
  /** 上传:一个托盘,上面一支朝上的箭头 */
3453
3865
  const Upload = createIcon("Upload", /* @__PURE__ */ jsx("path", { d: "M12 16V4M7 9l5-5 5 5M4 15v5h16v-5" }));
@@ -3465,6 +3877,8 @@ const zoneSize = {
3465
3877
  sm: "min-h-10 flex-wrap gap-x-3 px-3 py-2 text-sm",
3466
3878
  md: "gap-3 px-4 py-4"
3467
3879
  };
3880
+ /** 列表里认一个文件靠的那串字 */
3881
+ const fileKey = (file) => `${file.name}-${file.size}-${file.lastModified}`;
3468
3882
  /** 拖进来的东西里的文件;文件夹不收 */
3469
3883
  function droppedFiles(transfer) {
3470
3884
  const items = [...transfer.items ?? []];
@@ -3476,6 +3890,25 @@ function droppedFiles(transfer) {
3476
3890
  });
3477
3891
  }
3478
3892
  /**
3893
+ * 列表里的一行。淡入、收起都写在这一层:里面那一行可能是使用方自己画的(renderFile)。
3894
+ * 移除的那一行先留着——高度收到 0 并淡出,连它占的 4px 行距一起收,收完才卸载;
3895
+ * 下面的行和控件后面的内容是被带着上来的。
3896
+ */
3897
+ function FileRow({ leaving, entering, children }) {
3898
+ const [last, setLast] = useState(children);
3899
+ if (!leaving && last !== children) setLast(children);
3900
+ return /* @__PURE__ */ jsx("li", {
3901
+ "data-leaving": leaving ? "" : void 0,
3902
+ inert: leaving,
3903
+ "aria-hidden": leaving || void 0,
3904
+ className: cn(collapse, "transition-[grid-template-rows,margin,opacity] duration-(--duration-fast)", "data-leaving:-mb-1 data-leaving:opacity-0", entering && "animate-fade-in [animation-duration:var(--duration-fast)]"),
3905
+ children: /* @__PURE__ */ jsx("div", {
3906
+ className: "min-h-0 min-w-0 in-data-leaving:overflow-y-clip",
3907
+ children: leaving ? last : children
3908
+ })
3909
+ });
3910
+ }
3911
+ /**
3479
3912
  * 文件上传:选文件,或者把文件拖进来。它只管"选"和"列"——发请求、算进度是使用方的事。
3480
3913
  *
3481
3914
  * 里面是一个真的 `<input type="file">`,铺满整个投放区:点哪儿都是打开系统的文件框。
@@ -3500,6 +3933,9 @@ function FileUpload({ value, defaultValue, onValueChange, multiple = false, acce
3500
3933
  defaultValue: defaultValue ?? [],
3501
3934
  onChange: onValueChange
3502
3935
  });
3936
+ const keys = files.map(fileKey);
3937
+ const added = useAddedKeys(keys);
3938
+ const lingering = useLingering(keys);
3503
3939
  const [rejections, setRejections] = useState([]);
3504
3940
  const [dragging, setDragging] = useState(false);
3505
3941
  const depth = useRef(0);
@@ -3518,7 +3954,7 @@ function FileUpload({ value, defaultValue, onValueChange, multiple = false, acce
3518
3954
  const index = pendingFocus.current;
3519
3955
  if (index === null) return;
3520
3956
  pendingFocus.current = null;
3521
- const buttons = listRef.current?.querySelectorAll("[data-file-remove]") ?? [];
3957
+ const buttons = listRef.current?.querySelectorAll("li:not([data-leaving]) [data-file-remove]") ?? [];
3522
3958
  const target = buttons[Math.min(index, buttons.length - 1)];
3523
3959
  if (target) target.focus();
3524
3960
  else inputRef.current?.focus();
@@ -3620,7 +4056,7 @@ function FileUpload({ value, defaultValue, onValueChange, multiple = false, acce
3620
4056
  dragging && /* @__PURE__ */ jsx("span", {
3621
4057
  "aria-hidden": "true",
3622
4058
  "data-brackets": "",
3623
- className: cn("pointer-events-none select-none forced-colors:hidden print:hidden", "corner-brackets absolute inset-1.5 [--bracket-offset:0px]")
4059
+ className: cn("pointer-events-none select-none forced-colors:hidden print:hidden", "corner-brackets absolute inset-1.5 [--bracket-offset:0px] after:animate-bracket-in")
3624
4060
  }),
3625
4061
  /* @__PURE__ */ jsx("input", {
3626
4062
  ref: mergeRefs(inputRef, ref),
@@ -3654,22 +4090,29 @@ function FileUpload({ value, defaultValue, onValueChange, multiple = false, acce
3654
4090
  })]
3655
4091
  })
3656
4092
  }),
3657
- files.length > 0 && /* @__PURE__ */ jsx("ul", {
3658
- ref: listRef,
4093
+ lingering.entries.length > 0 && /* @__PURE__ */ jsx("ul", {
4094
+ ref: mergeRefs(listRef, lingering.ref),
3659
4095
  "aria-label": labels.list,
3660
- className: "flex flex-col gap-1",
3661
- children: files.map((file, index) => {
4096
+ "data-emptying": files.length === 0 ? "" : void 0,
4097
+ className: "flex flex-col gap-1 transition-[margin] duration-(--duration-fast) ease-standard data-emptying:-mt-2",
4098
+ children: lingering.entries.map(({ key, leaving }) => {
4099
+ const index = keys.indexOf(key);
4100
+ const file = files[index];
3662
4101
  const actions = {
3663
4102
  remove: () => remove(index),
3664
4103
  index
3665
4104
  };
3666
- return /* @__PURE__ */ jsx("li", { children: renderFile ? renderFile(file, actions) : /* @__PURE__ */ jsx(FileItem, {
3667
- name: file.name,
3668
- size: file.size,
3669
- disabled,
3670
- removeLabel: removeLabel(file.name),
3671
- onRemove: actions.remove
3672
- }) }, `${file.name}-${file.size}-${file.lastModified}`);
4105
+ return /* @__PURE__ */ jsx(FileRow, {
4106
+ leaving,
4107
+ entering: added.has(key),
4108
+ children: file !== void 0 && (renderFile ? renderFile(file, actions) : /* @__PURE__ */ jsx(FileItem, {
4109
+ name: file.name,
4110
+ size: file.size,
4111
+ disabled,
4112
+ removeLabel: removeLabel(file.name),
4113
+ onRemove: actions.remove
4114
+ }))
4115
+ }, key);
3673
4116
  })
3674
4117
  }),
3675
4118
  /* @__PURE__ */ jsx("span", {
@@ -3849,7 +4292,7 @@ const readoutPosition = {
3849
4292
  *
3850
4293
  * 只用在展示性的画面上,一屏一处。取景角与准星是装饰,读数是真实信息、读屏读得到。
3851
4294
  */
3852
- function Viewfinder({ size = "sm", crosshair = false, readouts, className, children, ...props }) {
4295
+ function Viewfinder({ size = "sm", crosshair = false, animate = false, readouts, className, children, ...props }) {
3853
4296
  return /* @__PURE__ */ jsxs("div", {
3854
4297
  ...props,
3855
4298
  className: cn("relative text-ink", className),
@@ -3857,7 +4300,7 @@ function Viewfinder({ size = "sm", crosshair = false, readouts, className, child
3857
4300
  children,
3858
4301
  /* @__PURE__ */ jsx("span", {
3859
4302
  "aria-hidden": "true",
3860
- className: cn(decor, "corner-brackets absolute inset-3 [--bracket-color:currentColor] [--bracket-offset:0px] [--bracket-width:1.5px]", armClass[size])
4303
+ className: cn(decor, "corner-brackets absolute inset-3 [--bracket-color:currentColor] [--bracket-offset:0px] [--bracket-width:1.5px]", armClass[size], animate && "after:animate-bracket-in after:[animation-delay:var(--bracket-delay,0s)]")
3861
4304
  }),
3862
4305
  crosshair && /* @__PURE__ */ jsx("svg", {
3863
4306
  "aria-hidden": "true",
@@ -4016,22 +4459,17 @@ function Stage({ label, items, index, setIndex, loop, previousLabel, nextLabel,
4016
4459
  }),
4017
4460
  /* @__PURE__ */ jsx(Viewfinder, {
4018
4461
  size: "md",
4019
- className: "min-h-0 flex-1",
4462
+ animate: true,
4463
+ className: "min-h-0 flex-1 [--bracket-delay:var(--duration-fast)]",
4020
4464
  children: /* @__PURE__ */ jsx("div", {
4021
4465
  ref: trackRef,
4022
4466
  "data-image-viewer-track": "",
4023
4467
  onScroll,
4024
4468
  className: "absolute inset-0 flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain [scrollbar-width:none]",
4025
- children: items.map((item, position) => /* @__PURE__ */ jsx("div", {
4026
- role: "group",
4027
- "aria-roledescription": "幻灯片",
4028
- "aria-label": `第 ${position + 1} 张,共 ${count} 张`,
4029
- inert: position !== index,
4030
- "data-current": position === index ? "" : void 0,
4031
- onClick: (event) => {
4032
- if (event.target === event.currentTarget) onClose();
4033
- },
4034
- className: cn("flex size-full shrink-0 snap-start items-center justify-center p-6 sm:p-10", "*:max-h-full *:max-w-full [&>img]:object-contain [&>video]:object-contain"),
4469
+ children: items.map((item, position) => /* @__PURE__ */ jsx(Slide, {
4470
+ label: `第 ${position + 1} 张,共 ${count} 张`,
4471
+ current: position === index,
4472
+ onClose,
4035
4473
  children: Math.abs(position - index) <= 1 ? item.props.children : null
4036
4474
  }, position))
4037
4475
  })
@@ -4077,6 +4515,30 @@ function Stage({ label, items, index, setIndex, loop, previousLabel, nextLabel,
4077
4515
  ]
4078
4516
  });
4079
4517
  }
4518
+ /**
4519
+ * 轨道上的一张。图是使用方给的元素,这里不碰它,只在外面听它的 `load`:
4520
+ * 还没取到的图是透明的,取到了淡入——不是"啪"地一下出现。
4521
+ * 只管直接放进来的 `<img>`;视频、别的内容不等,和原来一样直接在
4522
+ */
4523
+ function Slide({ label, current, onClose, children }) {
4524
+ const [loaded, setLoaded] = useState(false);
4525
+ const settle = () => setLoaded(true);
4526
+ return /* @__PURE__ */ jsx("div", {
4527
+ role: "group",
4528
+ "aria-roledescription": "幻灯片",
4529
+ "aria-label": label,
4530
+ inert: !current,
4531
+ "data-current": current ? "" : void 0,
4532
+ "data-loaded": loaded ? "" : void 0,
4533
+ onLoadCapture: settle,
4534
+ onErrorCapture: settle,
4535
+ onClick: (event) => {
4536
+ if (event.target === event.currentTarget) onClose();
4537
+ },
4538
+ className: cn("flex size-full shrink-0 snap-start items-center justify-center p-6 sm:p-10", "*:max-h-full *:max-w-full [&>img]:object-contain [&>video]:object-contain", "[&>img]:transition-opacity [&>img]:duration-(--duration-fast) [&>img]:ease-standard not-data-loaded:[&>img]:opacity-0"),
4539
+ children
4540
+ });
4541
+ }
4080
4542
  /** 图片查看里的一张。直接放在 `ImageViewer` 里。 */
4081
4543
  function ImageViewerItem({ title, description: _description, thumbnail, "aria-label": ariaLabel, className, children, ...props }) {
4082
4544
  const item = useContext(ItemContext);
@@ -4397,7 +4859,7 @@ function ItemSlot({ name, label, count, rarity, ratio = "1/1", selected = false,
4397
4859
  ...props,
4398
4860
  "data-selected": selected ? "" : void 0,
4399
4861
  "data-rarity": rarity,
4400
- className: cn("relative", ratioClass$1[ratio], selected && "corner-brackets", className),
4862
+ className: cn("relative", ratioClass$1[ratio], selected && "corner-brackets after:animate-bracket-in", className),
4401
4863
  children: node
4402
4864
  });
4403
4865
  }
@@ -4526,23 +4988,8 @@ function ListRow({ start, end, description, selected, completed = false, complet
4526
4988
  * 加载页:近黑底、大号等宽百分比、一行标语、黄色进度条。只用于整页的首次加载。
4527
4989
  * 不伪造进度;内容就绪后立刻把 `open` 置为 `false`,不要为了播完动画而等待。
4528
4990
  */
4529
- function Loader({ value, tagline, open = true, fullscreen = true, label = "加载中", onExited, onKeyDown, onTransitionEnd, className, ...props }) {
4530
- const rootRef = useRef(null);
4531
- const [mounted, setMounted] = useState(open);
4532
- if (open && !mounted) setMounted(true);
4533
- const onExitedRef = useRef(onExited);
4534
- useEffect(() => {
4535
- onExitedRef.current = onExited;
4536
- });
4537
- const leaving = !open && mounted;
4538
- useEffect(() => {
4539
- if (!leaving) return;
4540
- const timer = window.setTimeout(() => {
4541
- setMounted(false);
4542
- onExitedRef.current?.();
4543
- }, 700);
4544
- return () => window.clearTimeout(timer);
4545
- }, [leaving]);
4991
+ function Loader({ value, tagline, open = true, fullscreen = true, label = "加载中", onExited, onKeyDown, className, ref, ...props }) {
4992
+ const { ref: rootRef, mounted } = usePresence(open, { onExited });
4546
4993
  useEffect(() => {
4547
4994
  if (!fullscreen || !open) return;
4548
4995
  const page = document.documentElement;
@@ -4554,15 +5001,12 @@ function Loader({ value, tagline, open = true, fullscreen = true, label = "加
4554
5001
  page.style.overflow = previousOverflow;
4555
5002
  if (previousFocus instanceof HTMLElement && previousFocus.isConnected) previousFocus.focus({ preventScroll: true });
4556
5003
  };
4557
- }, [fullscreen, open]);
5004
+ }, [
5005
+ fullscreen,
5006
+ open,
5007
+ rootRef
5008
+ ]);
4558
5009
  if (!mounted) return null;
4559
- const handleTransitionEnd = (event) => {
4560
- onTransitionEnd?.(event);
4561
- if (leaving && event.target === event.currentTarget) {
4562
- setMounted(false);
4563
- onExitedRef.current?.();
4564
- }
4565
- };
4566
5010
  const handleKeyDown = (event) => {
4567
5011
  onKeyDown?.(event);
4568
5012
  if (fullscreen && open && event.key === "Tab") event.preventDefault();
@@ -4570,12 +5014,11 @@ function Loader({ value, tagline, open = true, fullscreen = true, label = "加
4570
5014
  const indeterminate = value === void 0;
4571
5015
  const current = indeterminate ? 0 : Math.min(Math.max(value, 0), 100);
4572
5016
  return /* @__PURE__ */ jsx("div", {
4573
- ref: rootRef,
4574
5017
  tabIndex: -1,
4575
5018
  ...props,
5019
+ ref: mergeRefs(rootRef, ref),
4576
5020
  "data-state": open ? "open" : "closing",
4577
5021
  onKeyDown: handleKeyDown,
4578
- onTransitionEnd: handleTransitionEnd,
4579
5022
  className: cn("@container flex flex-col justify-end bg-neutral-950 text-neutral-0 outline-none", fullscreen ? "fixed inset-0 z-(--z-loader)" : "absolute inset-0", "transition-[translate] duration-(--duration-slower) ease-emphasis", open ? "translate-y-0" : "pointer-events-none -translate-y-full", className),
4580
5023
  children: /* @__PURE__ */ jsxs("div", {
4581
5024
  className: "flex flex-col gap-4 p-6 @md:p-10",
@@ -4601,9 +5044,9 @@ function Loader({ value, tagline, open = true, fullscreen = true, label = "加
4601
5044
  "aria-valuemin": 0,
4602
5045
  "aria-valuemax": 100,
4603
5046
  "aria-valuenow": indeterminate ? void 0 : current,
4604
- className: "relative h-1.5 overflow-hidden rounded-sm bg-neutral-800",
4605
- children: indeterminate ? /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 left-0 w-2/5 animate-indeterminate bg-action motion-reduce:translate-x-3/4" }) : /* @__PURE__ */ jsx("span", {
4606
- className: "absolute inset-0 origin-left bg-action transition-transform duration-(--duration-base) ease-standard",
5047
+ className: "relative h-1.5 overflow-hidden rounded-sm bg-neutral-800 forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[CanvasText]",
5048
+ children: indeterminate ? /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 left-0 w-2/5 animate-indeterminate bg-action motion-reduce:translate-x-3/4 forced-colors:bg-[Highlight]" }) : /* @__PURE__ */ jsx("span", {
5049
+ className: "absolute inset-0 origin-left bg-action transition-transform duration-(--duration-base) ease-standard forced-colors:bg-[Highlight]",
4607
5050
  style: { transform: `scaleX(${current / 100})` }
4608
5051
  })
4609
5052
  })
@@ -4895,8 +5338,13 @@ function NavMenu({ trigger, open, defaultOpen = false, onOpenChange, title, bran
4895
5338
  })]
4896
5339
  });
4897
5340
  }
5341
+ const itemEnter = [
5342
+ "animate-shift-in [--shift-x:calc(var(--motion-shift-lg)*-1)]",
5343
+ "[animation-delay:100ms] nth-2:[animation-delay:150ms] nth-3:[animation-delay:200ms]",
5344
+ "nth-4:[animation-delay:250ms] nth-5:[animation-delay:300ms] nth-[n+6]:[animation-delay:350ms]"
5345
+ ].join(" ");
4898
5346
  /** 全屏菜单里的一个栏目。 */
4899
- function NavMenuItem({ icon, current = false, disabled = false, closeOnClick = true, href, target, rel, render, onClick, children, ...props }) {
5347
+ function NavMenuItem({ icon, current = false, disabled = false, closeOnClick = true, href, target, rel, render, onClick, className, children, ...props }) {
4900
5348
  const close = useContext(NavMenuContext);
4901
5349
  const isLink = href !== void 0 || render !== void 0;
4902
5350
  const handleClick = (event) => {
@@ -4910,7 +5358,7 @@ function NavMenuItem({ icon, current = false, disabled = false, closeOnClick = t
4910
5358
  const classes = cn("relative flex h-14 w-full items-center gap-3 px-4 text-left text-base font-medium", "transition-colors duration-(--duration-fast) ease-standard", disabled ? "cursor-not-allowed bg-surface-sunken text-ink-disabled" : current ? [
4911
5359
  "bg-action font-bold text-on-action",
4912
5360
  "before:absolute before:inset-y-0 before:left-0 before:w-1 before:bg-on-action before:content-['']",
4913
- "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-on-action"
5361
+ "focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-on-action"
4914
5362
  ] : ["bg-surface-sunken text-ink hover:bg-surface-muted active:bg-line", focusRingInset]);
4915
5363
  const mark = disabled ? "text-ink-disabled" : current ? "text-on-action" : "text-ink-secondary";
4916
5364
  const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
@@ -4933,6 +5381,7 @@ function NavMenuItem({ icon, current = false, disabled = false, closeOnClick = t
4933
5381
  ] });
4934
5382
  return /* @__PURE__ */ jsx("li", {
4935
5383
  ...props,
5384
+ className: cn(itemEnter, className),
4936
5385
  children: isLink ? /* @__PURE__ */ jsx(LinkElement, {
4937
5386
  render,
4938
5387
  href: disabled ? void 0 : href,
@@ -5071,7 +5520,7 @@ function OtpInput({ length = 6, value, defaultValue, onValueChange, onComplete,
5071
5520
  const labelId = useId();
5072
5521
  const ownLabel = ariaLabelledBy ?? (ariaLabel ? labelId : void 0);
5073
5522
  const groupLabel = ownLabel ?? fieldContext?.labelId;
5074
- const slotClass = cn("min-w-0 shrink text-center font-tech font-medium tabular-nums text-ink caret-ink", "transition-colors duration-(--duration-fast) ease-standard", focusRing, slotSize[size], slotFrame[variant], readOnly ? "border-transparent bg-transparent" : slotFill[variant], disabled ? "cursor-not-allowed border-line text-ink-disabled" : invalid ? "border-danger" : !readOnly && "border-line-strong hover:not-focus:border-ink-secondary focus:border-ink");
5523
+ const slotClass = cn("min-w-0 shrink text-center font-tech font-medium tabular-nums text-transparent caret-ink", "transition-colors duration-(--duration-fast) ease-standard", disabled ? "data-filled:text-ink-disabled" : "data-filled:text-ink", focusRing, slotSize[size], slotFrame[variant], readOnly ? "border-transparent bg-transparent" : slotFill[variant], readOnly && !disabled && !invalid && "forced-colors:border-[Canvas]", disabled ? "cursor-not-allowed border-line" : invalid ? "border-danger" : !readOnly && "border-line-strong hover:not-focus:border-ink-secondary focus:border-ink");
5075
5524
  const group = groupSize !== void 0 && groupSize > 0 ? groupSize : 0;
5076
5525
  return /* @__PURE__ */ jsxs(OTPField.Root, {
5077
5526
  ...props,
@@ -5103,7 +5552,7 @@ function OtpInput({ length = 6, value, defaultValue, onValueChange, onComplete,
5103
5552
  }), Array.from({ length }, (_, index) => /* @__PURE__ */ jsxs(Fragment, { children: [group > 0 && index > 0 && index % group === 0 && /* @__PURE__ */ jsx("span", {
5104
5553
  "aria-hidden": "true",
5105
5554
  "data-separator": "",
5106
- className: "h-0.5 w-2 shrink-0 bg-line-strong"
5555
+ className: "h-0.5 w-2 shrink-0 bg-line-strong forced-colors:bg-[CanvasText]"
5107
5556
  }), /* @__PURE__ */ jsx(OTPField.Input, {
5108
5557
  ...index === 0 ? {
5109
5558
  "aria-labelledby": ownLabel,
@@ -5121,12 +5570,20 @@ function OtpInput({ length = 6, value, defaultValue, onValueChange, onComplete,
5121
5570
  const ArrowLeft = createIcon("ArrowLeft", /* @__PURE__ */ jsx("path", { d: "M20 12H5M11 5l-7 7 7 7" }));
5122
5571
  //#endregion
5123
5572
  //#region src/components/page-header/PageHeader.tsx
5573
+ const enter = "animate-shift-in [--shift-x:calc(var(--motion-shift-lg)*-1)]";
5574
+ const enterStep = [
5575
+ enter,
5576
+ `${enter} [animation-delay:100ms]`,
5577
+ `${enter} [animation-delay:200ms]`
5578
+ ];
5124
5579
  /**
5125
5580
  * 页头:这是哪一页、怎么回去、在这一页能做什么。一页一个。
5126
5581
  * 按自身的宽度响应:行动区放不下时折到标题下面。
5127
5582
  */
5128
- function PageHeader({ title, level = 1, meta, description, breadcrumb, back, actions, divider = false, sticky = false, className, children, ...props }) {
5583
+ function PageHeader({ title, level = 1, meta, description, breadcrumb, back, actions, divider = false, sticky = false, animate = true, className, children, ...props }) {
5129
5584
  const Heading = `h${level}`;
5585
+ const reveal = animate && !sticky;
5586
+ const first = meta ? 1 : 0;
5130
5587
  return /* @__PURE__ */ jsxs("header", {
5131
5588
  ...props,
5132
5589
  "data-sticky": sticky ? "" : void 0,
@@ -5136,7 +5593,7 @@ function PageHeader({ title, level = 1, meta, description, breadcrumb, back, act
5136
5593
  /* @__PURE__ */ jsxs("div", {
5137
5594
  className: "flex flex-col gap-1",
5138
5595
  children: [meta && /* @__PURE__ */ jsx("p", {
5139
- className: "font-tech text-xs text-ink-secondary",
5596
+ className: cn("font-tech text-xs text-ink-secondary", reveal && enterStep[0]),
5140
5597
  children: meta
5141
5598
  }), /* @__PURE__ */ jsxs("div", {
5142
5599
  className: "flex flex-wrap items-start justify-between gap-x-6 gap-y-4",
@@ -5148,10 +5605,10 @@ function PageHeader({ title, level = 1, meta, description, breadcrumb, back, act
5148
5605
  }), /* @__PURE__ */ jsxs("div", {
5149
5606
  className: "flex min-w-0 flex-col gap-1",
5150
5607
  children: [/* @__PURE__ */ jsx(Heading, {
5151
- className: "min-w-0 text-2xl leading-10 font-bold wrap-anywhere @md:text-3xl @md:leading-12",
5608
+ className: cn("min-w-0 text-2xl leading-10 font-bold wrap-anywhere @md:text-3xl @md:leading-12", reveal && enterStep[first]),
5152
5609
  children: title
5153
5610
  }), description && /* @__PURE__ */ jsx("p", {
5154
- className: "max-w-prose text-sm text-ink-secondary",
5611
+ className: cn("max-w-prose text-sm text-ink-secondary", reveal && enterStep[first + 1]),
5155
5612
  children: description
5156
5613
  })]
5157
5614
  })]
@@ -5245,7 +5702,7 @@ function Pagination({ page, defaultPage = 1, onPageChange, pageCount, jump = fal
5245
5702
  "aria-label": "分页",
5246
5703
  ...props,
5247
5704
  "data-size": size,
5248
- className: cn("@container hatch flex items-center justify-between gap-2 bg-surface-muted px-1 text-ink", barSize[size], className),
5705
+ className: cn("@container hatch flex items-center justify-between gap-2 bg-surface-muted px-1 text-ink", "forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[ButtonText]", barSize[size], className),
5249
5706
  children: [
5250
5707
  /* @__PURE__ */ jsx(IconButton, {
5251
5708
  variant: "accent",
@@ -5404,6 +5861,8 @@ function PopoverClose({ children }) {
5404
5861
  }
5405
5862
  //#endregion
5406
5863
  //#region src/components/progress/ProgressRing.tsx
5864
+ const trackClass = "stroke-line forced-colors:stroke-[CanvasText] forced-colors:stroke-1";
5865
+ const arcClass = "stroke-action forced-colors:stroke-[Highlight]";
5407
5866
  /**
5408
5867
  * 进度环:细圆环轨道 + 从 12 点方向顺时针的黄弧。用在紧凑的位置,或者围住一个头像、图标。
5409
5868
  * 取色与 `Progress` 相同;需要名称时传 `aria-label`。
@@ -5425,16 +5884,16 @@ function ProgressRing({ value, max = 100, size = 48, thickness, showValue = fals
5425
5884
  let arc = null;
5426
5885
  if (indeterminate) arc = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("circle", {
5427
5886
  ...circle,
5428
- className: "origin-center animate-spin stroke-action motion-reduce:hidden",
5887
+ className: cn("origin-center animate-spin motion-reduce:hidden", arcClass),
5429
5888
  strokeDasharray: `${circumference / 4} ${circumference}`
5430
5889
  }), /* @__PURE__ */ jsx("circle", {
5431
5890
  ...circle,
5432
- className: "hidden stroke-action motion-reduce:block",
5891
+ className: cn("hidden motion-reduce:block", arcClass),
5433
5892
  strokeDasharray: `${stroke} ${stroke * 2}`
5434
5893
  })] });
5435
5894
  else if (ratio > 0) arc = /* @__PURE__ */ jsx("circle", {
5436
5895
  ...circle,
5437
- className: "stroke-action transition-[stroke-dashoffset] duration-(--duration-base) ease-standard",
5896
+ className: cn("transition-[stroke-dashoffset] duration-(--duration-base) ease-standard", arcClass),
5438
5897
  strokeDasharray: circumference,
5439
5898
  strokeDashoffset: circumference * (1 - ratio)
5440
5899
  });
@@ -5468,7 +5927,7 @@ function ProgressRing({ value, max = 100, size = 48, thickness, showValue = fals
5468
5927
  className: "absolute inset-0 -rotate-90",
5469
5928
  children: [/* @__PURE__ */ jsx("circle", {
5470
5929
  ...circle,
5471
- className: "stroke-line"
5930
+ className: trackClass
5472
5931
  }), arc]
5473
5932
  }), center]
5474
5933
  });
@@ -5556,7 +6015,7 @@ function Radio({ value, disabled, onChange, className, children, ...props }) {
5556
6015
  className: cn(choiceBox, "rounded-full choice-diamond:rounded-none", focusRing)
5557
6016
  }), /* @__PURE__ */ jsx("span", {
5558
6017
  "aria-hidden": "true",
5559
- className: cn(choiceMark, "size-2 rounded-full bg-accent-ink-inverse peer-checked:opacity-100 peer-disabled:bg-ink-disabled choice-diamond:hidden")
6018
+ className: cn(choiceMark, "size-2 rounded-full bg-accent-ink-inverse peer-checked:opacity-100 peer-disabled:bg-ink-disabled choice-diamond:hidden", "forced-colors:bg-[HighlightText] forced-colors:peer-disabled:bg-[GrayText]")
5560
6019
  })]
5561
6020
  })
5562
6021
  }), children !== void 0 && /* @__PURE__ */ jsx("span", {
@@ -5617,6 +6076,105 @@ function RegistrationStrip({ orientation = "horizontal", rule = false, className
5617
6076
  });
5618
6077
  }
5619
6078
  //#endregion
6079
+ //#region src/hooks/useReducedMotion.ts
6080
+ const QUERY = "(prefers-reduced-motion: reduce)";
6081
+ function subscribe$1(notify) {
6082
+ if (typeof window.matchMedia !== "function") return () => {};
6083
+ const media = window.matchMedia(QUERY);
6084
+ media.addEventListener("change", notify);
6085
+ return () => media.removeEventListener("change", notify);
6086
+ }
6087
+ function read() {
6088
+ return typeof window.matchMedia === "function" && window.matchMedia(QUERY).matches;
6089
+ }
6090
+ /**
6091
+ * 系统开了"减少动态效果"时返回 true,设置变了会跟着变。
6092
+ *
6093
+ * CSS 的动画和过渡不用它:`theme.css` 末尾的全局规则已经统一降级了。
6094
+ * 只有脚本驱动的动效(一帧一帧自己算的)才要看它——那条规则管不到脚本。
6095
+ */
6096
+ function useReducedMotion() {
6097
+ return useSyncExternalStore(subscribe$1, read, () => false);
6098
+ }
6099
+ //#endregion
6100
+ //#region src/components/rolling-number/RollingNumber.tsx
6101
+ const FALLBACK_DURATION = 600;
6102
+ function readDuration(node) {
6103
+ if (!node) return FALLBACK_DURATION;
6104
+ const raw = getComputedStyle(node).getPropertyValue("--duration-slower");
6105
+ const amount = Number.parseFloat(raw);
6106
+ if (!Number.isFinite(amount) || amount <= 0) return FALLBACK_DURATION;
6107
+ return raw.trim().endsWith("ms") ? amount : amount * 1e3;
6108
+ }
6109
+ /** 减速:先快后慢,停得住。和 `ease-exit` 是一类,不带过冲 */
6110
+ const decelerate = (progress) => 1 - (1 - progress) ** 3;
6111
+ function fractionDigits(value) {
6112
+ const fraction = new Intl.NumberFormat("en-US", {
6113
+ maximumFractionDigits: 20,
6114
+ useGrouping: false
6115
+ }).format(value).split(".")[1];
6116
+ return fraction ? fraction.length : 0;
6117
+ }
6118
+ const never = () => () => {};
6119
+ /**
6120
+ * 滚动数字:进入视口时从 0 滚到终值,只滚一次。放进 `Stat` 的 `value` 里,
6121
+ * 或者任何"一个需要被一眼看到的数字"的地方。等宽数字由所在处给(`tabular-nums`)。
6122
+ *
6123
+ * - 读屏只读终值:终值一直在,滚的时候只是看不见;滚动的那一份对读屏隐藏。
6124
+ * - 宽度由终值占着,滚的时候旁边的东西不动。
6125
+ * - 服务端渲染出来的、打印出来的、"减少动态效果"下的,都直接是终值。
6126
+ * - 值后来变了直接换,不再滚。
6127
+ */
6128
+ function RollingNumber({ value, format, animate = true, className, ref, ...props }) {
6129
+ const digits = fractionDigits(value);
6130
+ const write = format ?? ((amount) => amount.toLocaleString("en-US", {
6131
+ minimumFractionDigits: digits,
6132
+ maximumFractionDigits: digits
6133
+ }));
6134
+ const mounted = useSyncExternalStore(never, () => true, () => false);
6135
+ const reduced = useReducedMotion();
6136
+ const [done, setDone] = useState(false);
6137
+ const [shown, setShown] = useState(0);
6138
+ const rolling = mounted && animate && !reduced && !done;
6139
+ const [inViewRef, inView] = useInView({ disabled: !rolling });
6140
+ useEffect(() => {
6141
+ if (!rolling || !inView) return;
6142
+ const duration = readDuration(inViewRef.current);
6143
+ let started;
6144
+ let frame = requestAnimationFrame(function tick(now) {
6145
+ started ??= now;
6146
+ const progress = Math.min((now - started) / duration, 1);
6147
+ if (progress >= 1) {
6148
+ setDone(true);
6149
+ return;
6150
+ }
6151
+ setShown(Number((value * decelerate(progress)).toFixed(digits)));
6152
+ frame = requestAnimationFrame(tick);
6153
+ });
6154
+ return () => cancelAnimationFrame(frame);
6155
+ }, [
6156
+ rolling,
6157
+ inView,
6158
+ inViewRef,
6159
+ value,
6160
+ digits
6161
+ ]);
6162
+ return /* @__PURE__ */ jsxs("span", {
6163
+ ...props,
6164
+ ref: mergeRefs(inViewRef, ref),
6165
+ "data-rolling": rolling ? "" : void 0,
6166
+ className: cn(rolling && "relative inline-block", className),
6167
+ children: [/* @__PURE__ */ jsx("span", {
6168
+ className: rolling ? "opacity-0 print:opacity-100" : void 0,
6169
+ children: write(value)
6170
+ }), rolling && /* @__PURE__ */ jsx("span", {
6171
+ "aria-hidden": "true",
6172
+ className: "absolute inset-0 print:hidden",
6173
+ children: write(shown)
6174
+ })]
6175
+ });
6176
+ }
6177
+ //#endregion
5620
6178
  //#region src/hooks/useOverflowing.ts
5621
6179
  /**
5622
6180
  * 容器里的内容是不是比容器宽、比容器高。
@@ -5752,11 +6310,11 @@ function Schedule({ label, start, end, anchors: anchorsProp, today, dayWidth = 3
5752
6310
  }),
5753
6311
  todayColumn !== null && /* @__PURE__ */ jsx("div", {
5754
6312
  "aria-hidden": "true",
5755
- className: cn("pointer-events-none select-none forced-colors:hidden print:hidden", "absolute inset-y-0 right-0 z-2", AFTER_RAIL, dayColumns),
6313
+ className: cn("pointer-events-none absolute inset-y-0 right-0 z-2 select-none print:hidden", AFTER_RAIL, dayColumns),
5756
6314
  children: /* @__PURE__ */ jsx("span", {
5757
6315
  "data-today": "",
5758
6316
  style: { gridColumn: todayColumn },
5759
- className: "row-start-1 border-l-2 border-accent-ink"
6317
+ className: "row-start-1 border-l-2 border-accent-ink forced-colors:border-[Highlight]"
5760
6318
  })
5761
6319
  })
5762
6320
  ]
@@ -5818,7 +6376,7 @@ function ScheduleItem({ start, end, title, type, variant = "banner", media, href
5818
6376
  gridColumn: `${from} / ${to}`,
5819
6377
  ...style
5820
6378
  },
5821
- className: cn("@container relative isolate flex min-w-0 overflow-clip text-sm", "has-focus-visible:outline-2 has-focus-visible:-outline-offset-2 has-focus-visible:outline-focus", banner ? "items-start justify-between gap-2 bg-surface-muted p-2" : "items-center gap-2 bg-control pr-2 text-on-control", className),
6379
+ className: cn("@container relative isolate flex min-w-0 overflow-clip text-sm", "has-focus-visible:outline-2 has-focus-visible:-outline-offset-2 has-focus-visible:outline-focus", "forced-colors:border forced-colors:border-[CanvasText]", banner ? "items-start justify-between gap-2 bg-surface-muted p-2" : "items-center gap-2 bg-control pr-2 text-on-control", className),
5822
6380
  children: [banner ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
5823
6381
  media && /* @__PURE__ */ jsx("div", {
5824
6382
  "aria-hidden": "true",
@@ -5941,40 +6499,6 @@ function ScrollHint({ label = "SCROLL", variant = "line", target, threshold = 8,
5941
6499
  });
5942
6500
  }
5943
6501
  //#endregion
5944
- //#region src/hooks/useInView.ts
5945
- /**
5946
- * 元素第一次进入视口后返回 true,之后不再变回 false——入场动画只播一次。
5947
- */
5948
- function useInView({ disabled = false, rootMargin = "0px", threshold = 0 } = {}) {
5949
- const ref = useRef(null);
5950
- const [inView, setInView] = useState(false);
5951
- useEffect(() => {
5952
- const node = ref.current;
5953
- if (disabled || inView || !node) return;
5954
- if (typeof IntersectionObserver === "undefined") {
5955
- setInView(true);
5956
- return;
5957
- }
5958
- const observer = new IntersectionObserver((entries) => {
5959
- if (entries.some((entry) => entry.isIntersecting)) {
5960
- setInView(true);
5961
- observer.disconnect();
5962
- }
5963
- }, {
5964
- rootMargin,
5965
- threshold
5966
- });
5967
- observer.observe(node);
5968
- return () => observer.disconnect();
5969
- }, [
5970
- disabled,
5971
- inView,
5972
- rootMargin,
5973
- threshold
5974
- ]);
5975
- return [ref, inView];
5976
- }
5977
- //#endregion
5978
6502
  //#region src/components/section-title/SectionTitle.tsx
5979
6503
  function SectionTitle({ variant = "standard", children, latin, subtitle, meta, level = 2, animate = true, latinHidden = true, tone = "accent", illustration, className, ref: forwardedRef, ...props }) {
5980
6504
  const Heading = `h${level}`;
@@ -6077,6 +6601,105 @@ function SectionTitle({ variant = "standard", children, latin, subtitle, meta, l
6077
6601
  });
6078
6602
  }
6079
6603
  //#endregion
6604
+ //#region src/hooks/useIndicator.ts
6605
+ const VARS = [
6606
+ "--indicator-x",
6607
+ "--indicator-y",
6608
+ "--indicator-w",
6609
+ "--indicator-h"
6610
+ ];
6611
+ function clear(node) {
6612
+ if (node.dataset.indicator === void 0) return;
6613
+ delete node.dataset.indicator;
6614
+ for (const name of VARS) node.style.removeProperty(name);
6615
+ }
6616
+ /** 量一次、写一次。`last` 记着上一次指的是哪一项:换了才滑 */
6617
+ function place(node, selector, last) {
6618
+ const box = node.getBoundingClientRect();
6619
+ if (box.width === 0 || box.height === 0) {
6620
+ clear(node);
6621
+ last.current = null;
6622
+ return;
6623
+ }
6624
+ const item = node.querySelector(selector);
6625
+ const rect = item?.getBoundingClientRect();
6626
+ if (!item || !rect || rect.width === 0 || rect.height === 0) {
6627
+ node.dataset.indicator = "off";
6628
+ last.current = null;
6629
+ return;
6630
+ }
6631
+ const laid = Number.parseFloat(getComputedStyle(node).width);
6632
+ const scale = laid > 0 ? box.width / laid : 1;
6633
+ const values = [
6634
+ (rect.left - box.left) / scale - node.clientLeft + node.scrollLeft,
6635
+ (rect.top - box.top) / scale - node.clientTop + node.scrollTop,
6636
+ rect.width / scale,
6637
+ rect.height / scale
6638
+ ].map((value) => `${Math.round(value * 100) / 100}px`);
6639
+ const slide = node.dataset.indicator === "on" && last.current !== null && last.current !== item;
6640
+ last.current = item;
6641
+ if (VARS.some((name, at) => node.style.getPropertyValue(name) !== values[at])) {
6642
+ if (!slide) node.dataset.indicatorInstant = "";
6643
+ VARS.forEach((name, at) => node.style.setProperty(name, values[at]));
6644
+ if (!slide) {
6645
+ node.getBoundingClientRect();
6646
+ delete node.dataset.indicatorInstant;
6647
+ }
6648
+ }
6649
+ node.dataset.indicator = "on";
6650
+ }
6651
+ /**
6652
+ * 选中指示的位置:量出容器里"当前项"相对容器的位置和尺寸,写成容器上的 CSS 变量
6653
+ * (`--indicator-x` / `-y` / `-w` / `-h`),并在容器上标 `data-indicator`——`on` 是有当前项,
6654
+ * `off` 是没有。指示本身由用的地方画在容器的 `::after` 上(`lib/motion.ts` 的 `indicator`),
6655
+ * 换了一项时由 CSS 的过渡滑过去。
6656
+ *
6657
+ * 只有"换了一项"才滑;第一次到位、同一项挪了地方或变了大小(容器宽了、字体到了、
6658
+ * 前面多了一项)都直接到位。量不到(服务端、还没排版、藏着)时什么都不标,
6659
+ * 各项照旧自己画选中态——不会有哪一帧没有选中态。
6660
+ *
6661
+ * `current` 是当前项在容器里的选择器;传 `null` 是不用。
6662
+ */
6663
+ function useIndicator(current) {
6664
+ const ref = useRef(null);
6665
+ const last = useRef(null);
6666
+ const observer = useRef(null);
6667
+ const watched = useRef([]);
6668
+ useLayoutEffect(() => {
6669
+ const node = ref.current;
6670
+ if (!node) return;
6671
+ if (current === null) {
6672
+ clear(node);
6673
+ last.current = null;
6674
+ return;
6675
+ }
6676
+ place(node, current, last);
6677
+ const watching = observer.current;
6678
+ if (!watching) return;
6679
+ const next = [node, ...node.children];
6680
+ const before = watched.current;
6681
+ if (next.length === before.length && next.every((element, at) => element === before[at])) return;
6682
+ watching.disconnect();
6683
+ for (const element of next) watching.observe(element);
6684
+ watched.current = next;
6685
+ });
6686
+ useLayoutEffect(() => {
6687
+ const node = ref.current;
6688
+ if (!node || current === null || typeof ResizeObserver === "undefined") return;
6689
+ const watching = new ResizeObserver(() => place(node, current, last));
6690
+ const targets = [node, ...node.children];
6691
+ for (const element of targets) watching.observe(element);
6692
+ observer.current = watching;
6693
+ watched.current = targets;
6694
+ return () => {
6695
+ watching.disconnect();
6696
+ observer.current = null;
6697
+ watched.current = [];
6698
+ };
6699
+ }, [current]);
6700
+ return ref;
6701
+ }
6702
+ //#endregion
6080
6703
  //#region src/components/segmented-control/SegmentedControl.tsx
6081
6704
  const SegmentedContext = createContext(null);
6082
6705
  const track = {
@@ -6090,8 +6713,9 @@ const track = {
6090
6713
  *
6091
6714
  * 切换的是下面显示哪一块内容时用 `Tabs`,不是它。
6092
6715
  */
6093
- function SegmentedControl({ value, defaultValue, onValueChange, name, size = "md", variant = "sunken", disabled: disabledProp, required: requiredProp, invalid: invalidProp, className, ...props }) {
6716
+ function SegmentedControl({ value, defaultValue, onValueChange, name, size = "md", variant = "sunken", disabled: disabledProp, required: requiredProp, invalid: invalidProp, className, ref, ...props }) {
6094
6717
  const field = useFieldContext();
6718
+ const trackRef = useIndicator("[data-selected]");
6095
6719
  const autoName = useId();
6096
6720
  const [current, setCurrent] = useControllableState({
6097
6721
  value,
@@ -6131,9 +6755,10 @@ function SegmentedControl({ value, defaultValue, onValueChange, name, size = "md
6131
6755
  "aria-required": required || void 0,
6132
6756
  "aria-disabled": disabled || void 0,
6133
6757
  ...props,
6758
+ ref: mergeRefs(trackRef, ref),
6134
6759
  "data-variant": variant,
6135
6760
  "data-size": size,
6136
- className: cn("inline-grid max-w-full auto-cols-fr grid-flow-col p-0.5 text-ink", "transition-colors duration-(--duration-fast) ease-standard", track[variant], controlSize$1[size], disabled ? "border-line" : invalid ? "border-danger" : "border-line-strong focus-within:border-ink", className)
6761
+ className: cn("inline-grid max-w-full auto-cols-fr grid-flow-col p-0.5 text-ink", "transition-colors duration-(--duration-fast) ease-standard", indicator, indicatorBox, "after:bg-surface-inverse has-[[data-selected][data-disabled]]:after:bg-disabled", "forced-colors:after:bg-[Highlight] forced-colors:has-[[data-selected][data-disabled]]:after:bg-[GrayText]", track[variant], controlSize$1[size], disabled ? "border-line" : invalid ? "border-danger" : "border-line-strong focus-within:border-ink", className)
6137
6762
  })
6138
6763
  });
6139
6764
  }
@@ -6152,7 +6777,11 @@ function Segment({ value, icon, disabled: disabledProp, "aria-label": ariaLabel,
6152
6777
  ...props,
6153
6778
  "data-selected": checked ? "" : void 0,
6154
6779
  "data-disabled": disabled ? "" : void 0,
6155
- className: cn("relative flex min-w-0 items-center justify-center font-medium whitespace-nowrap select-none", "transition-colors duration-(--duration-fast) ease-standard", "has-focus-visible:z-1 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-focus", segmentPadding[context.size], disabled ? "cursor-not-allowed" : "cursor-pointer", checked ? disabled ? "bg-disabled text-on-disabled" : "bg-surface-inverse text-ink-inverse" : disabled ? "text-ink-disabled" : "text-ink-secondary hover:bg-ink/5 hover:text-ink", className),
6780
+ className: cn("relative flex min-w-0 items-center justify-center font-medium whitespace-nowrap select-none", "transition-colors duration-(--duration-fast) ease-standard", "has-focus-visible:z-1 has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-focus", segmentPadding[context.size], disabled ? "cursor-not-allowed" : "cursor-pointer", checked ? [
6781
+ disabled ? "bg-disabled text-on-disabled forced-colors:bg-[GrayText] forced-colors:text-[Canvas]" : "bg-surface-inverse text-ink-inverse forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
6782
+ "in-data-indicator:bg-transparent forced-colors:in-data-indicator:bg-transparent",
6783
+ "forced-colors:forced-color-adjust-none forced-colors:has-focus-visible:outline-[Highlight]"
6784
+ ] : disabled ? "text-ink-disabled forced-colors:text-[GrayText]" : "text-ink-secondary hover:bg-ink/5 hover:text-ink", className),
6156
6785
  children: [
6157
6786
  /* @__PURE__ */ jsx("input", {
6158
6787
  type: "radio",
@@ -6651,7 +7280,7 @@ function SideRailSubItem({ current = false, end, disabled = false, href, target,
6651
7280
  }
6652
7281
  //#endregion
6653
7282
  //#region src/components/skeleton/Skeleton.tsx
6654
- const fill = "block bg-surface-muted";
7283
+ const fill = "block bg-surface-muted forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[GrayText]";
6655
7284
  /**
6656
7285
  * 骨架:只画大块,不画细节。
6657
7286
  * 自身对辅助技术隐藏;"正在加载"由外层区域的 `aria-busy="true"` 表达。
@@ -6685,12 +7314,16 @@ const THUMB_HALF = 6;
6685
7314
  const markLeft = (ratio) => `calc(${THUMB_HALF}px + (100% - 12px) * ${ratio})`;
6686
7315
  const thumb = [
6687
7316
  "relative h-5 w-3 bg-surface-inverse shadow-[0_0_0_2px_var(--ef-surface)]",
7317
+ "transition-[inset-inline-start] duration-(--duration-fast) ease-standard data-dragging:transition-none",
6688
7318
  "before:absolute before:-inset-x-3.5 before:-inset-y-2.5 before:content-['']",
6689
- "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",
7319
+ "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",
6690
7320
  "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",
6691
7321
  "data-disabled:bg-ink-disabled",
7322
+ "forced-colors:bg-[ButtonText] forced-colors:data-disabled:bg-[GrayText]",
6692
7323
  focusRingWithin
6693
7324
  ].join(" ");
7325
+ const trackForced = "forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[ButtonText] forced-colors:data-disabled:outline-[GrayText]";
7326
+ const indicatorForced = "forced-colors:bg-[Highlight] forced-colors:data-disabled:bg-[GrayText]";
6694
7327
  /**
6695
7328
  * 滑块:在一个范围里拖着选一个数。要的是确切的数时用 `Stepper`。
6696
7329
  * 每个滑块都要有名称:放进 `Field`,或者自己传 `aria-label`。
@@ -6740,8 +7373,8 @@ function Slider(props) {
6740
7373
  children: [/* @__PURE__ */ jsx(Slider$1.Control, {
6741
7374
  className: cn("flex touch-none items-center px-1.5 select-none data-disabled:cursor-not-allowed", controlSize[size]),
6742
7375
  children: /* @__PURE__ */ jsxs(Slider$1.Track, {
6743
- className: "h-1 w-full bg-line",
6744
- children: [/* @__PURE__ */ jsx(Slider$1.Indicator, { className: "bg-ink data-disabled:bg-ink-disabled" }), (range ? [0, 1] : [0]).map((index) => /* @__PURE__ */ jsx(Slider$1.Thumb, {
7376
+ className: cn("h-1 w-full bg-line", trackForced),
7377
+ children: [/* @__PURE__ */ jsx(Slider$1.Indicator, { className: cn("bg-ink transition-[inset-inline-start,width] duration-(--duration-fast) ease-standard data-disabled:bg-ink-disabled data-dragging:transition-none", indicatorForced) }), (range ? [0, 1] : [0]).map((index) => /* @__PURE__ */ jsx(Slider$1.Thumb, {
6745
7378
  index,
6746
7379
  ...range ? { "aria-label": thumbLabels[index] } : {
6747
7380
  "aria-label": ariaLabel,
@@ -6756,13 +7389,13 @@ function Slider(props) {
6756
7389
  className: "relative h-6 text-xs",
6757
7390
  children: marks.map((mark) => /* @__PURE__ */ jsx("span", {
6758
7391
  style: { left: markLeft((mark.value - min) / (max - min)) },
6759
- className: cn("absolute top-0 flex flex-col gap-0.5 whitespace-nowrap text-ink-secondary before:h-1.5 before:w-px before:bg-line-strong before:content-['']", mark.value <= min ? "items-start" : mark.value >= max ? "-translate-x-full items-end" : "-translate-x-1/2 items-center"),
7392
+ className: cn("absolute top-0 flex flex-col gap-0.5 whitespace-nowrap text-ink-secondary before:h-1.5 before:w-px before:bg-line-strong before:content-[''] forced-colors:before:bg-[ButtonText]", mark.value <= min ? "items-start" : mark.value >= max ? "-translate-x-full items-end" : "-translate-x-1/2 items-center"),
6760
7393
  children: mark.label
6761
7394
  }, mark.value))
6762
7395
  })]
6763
7396
  }), showValue && /* @__PURE__ */ jsx(Slider$1.Value, {
6764
7397
  style: { minWidth: valueWidth },
6765
- className: cn("flex shrink-0 items-center justify-end font-tech text-sm font-bold tabular-nums data-disabled:text-ink-disabled", controlSize[size]),
7398
+ className: cn("flex shrink-0 items-center justify-end font-tech text-sm font-bold tabular-nums data-disabled:text-ink-disabled forced-colors:data-disabled:text-[GrayText]", controlSize[size]),
6766
7399
  children: (formatted) => formatted.join(" – ")
6767
7400
  })]
6768
7401
  });
@@ -6952,7 +7585,7 @@ function Stepper({ value: valueProp, defaultValue, onValueChange, min, max, step
6952
7585
  onBlur?.(event);
6953
7586
  commitDraft();
6954
7587
  };
6955
- const buttonClass = (inactive) => cn("flex shrink-0 items-center justify-center select-none", "transition-colors duration-(--duration-fast) ease-standard", buttonSize[size], inactive ? "cursor-not-allowed bg-disabled text-on-disabled" : "bg-control text-on-control hover:bg-control-hover active:bg-control-pressed");
7588
+ const buttonClass = (inactive) => cn("flex shrink-0 items-center justify-center select-none", "transition-colors duration-(--duration-fast) ease-standard", "forced-colors:border forced-colors:border-[ButtonText] forced-colors:disabled:border-[GrayText]", buttonSize[size], inactive ? "cursor-not-allowed bg-disabled text-on-disabled" : "bg-control text-on-control hover:bg-control-hover active:bg-control-pressed");
6956
7589
  return /* @__PURE__ */ jsxs("div", {
6957
7590
  "data-size": size,
6958
7591
  "data-invalid": invalid ? "" : void 0,
@@ -6990,7 +7623,7 @@ function Stepper({ value: valueProp, defaultValue, onValueChange, min, max, step
6990
7623
  onChange: handleChange,
6991
7624
  onKeyDown: handleKeyDown,
6992
7625
  onBlur: handleBlur,
6993
- className: cn("min-w-0 flex-1 border-y-2 border-transparent bg-surface-sunken text-center font-tech font-bold tabular-nums outline-none", "disabled:cursor-not-allowed", fieldSize[size], disabled ? "text-ink-disabled" : "text-ink", invalid && !disabled && "border-b-danger")
7626
+ className: cn("min-w-0 flex-1 border-y-2 border-transparent bg-surface-sunken text-center font-tech font-bold tabular-nums outline-none", "disabled:cursor-not-allowed", "forced-colors:border-y forced-colors:border-[ButtonText] forced-colors:disabled:border-[GrayText]", fieldSize[size], disabled ? "text-ink-disabled" : "text-ink", invalid && !disabled && "border-b-danger")
6994
7627
  }),
6995
7628
  /* @__PURE__ */ jsx("button", {
6996
7629
  type: "button",
@@ -7029,8 +7662,8 @@ const railLayout = {
7029
7662
  horizontal: "flex-col @md:flex-row"
7030
7663
  };
7031
7664
  const lineLayout = {
7032
- vertical: "my-2 min-h-4 w-0.5",
7033
- horizontal: "my-2 min-h-4 w-0.5 @md:mx-2 @md:my-0 @md:h-0.5 @md:min-h-0 @md:w-auto"
7665
+ vertical: "my-2 min-h-4 w-0.5 forced-colors:border-r",
7666
+ horizontal: "my-2 min-h-4 w-0.5 @md:mx-2 @md:my-0 @md:h-0.5 @md:min-h-0 @md:w-auto forced-colors:@max-md:border-r forced-colors:@md:border-b"
7034
7667
  };
7035
7668
  const bodyLayout = {
7036
7669
  vertical: "pb-4 group-last/step:pb-0",
@@ -7073,8 +7706,8 @@ const nodeSize = {
7073
7706
  md: "size-6 text-xs [&_svg]:size-3.5"
7074
7707
  };
7075
7708
  const nodeClass$1 = {
7076
- done: "bg-surface-inverse text-accent-ink-inverse",
7077
- current: "border border-ink bg-action font-bold text-on-action",
7709
+ done: "bg-surface-inverse text-accent-ink-inverse forced-colors:bg-[CanvasText] forced-colors:text-[Canvas]",
7710
+ current: "border border-ink bg-action font-bold text-on-action forced-colors:forced-color-adjust-none forced-colors:border-[Highlight] forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]",
7078
7711
  upcoming: "border border-line-strong text-ink-secondary"
7079
7712
  };
7080
7713
  const titleSize = {
@@ -7108,9 +7741,9 @@ function Step({ title, description, invalid = false, href, target, rel, render,
7108
7741
  "aria-hidden": "true",
7109
7742
  className: cn("flex shrink-0 items-center", railLayout[orientation]),
7110
7743
  children: [/* @__PURE__ */ jsx("span", {
7111
- className: cn("flex shrink-0 items-center justify-center font-tech leading-none tabular-nums", nodeSize[size], invalid ? "border border-danger text-danger" : nodeClass$1[status]),
7744
+ className: cn("flex shrink-0 items-center justify-center font-tech leading-none tabular-nums", "transition-colors duration-(--duration-fast) ease-standard", nodeSize[size], invalid ? "border border-danger text-danger" : nodeClass$1[status]),
7112
7745
  children: invalid ? /* @__PURE__ */ jsx(StatusDanger, {}) : status === "done" ? /* @__PURE__ */ jsx(Check, {}) : String(index + 1).padStart(2, "0")
7113
- }), /* @__PURE__ */ jsx("span", { className: cn("flex-1 group-last/step:hidden", lineLayout[orientation], status === "done" ? "bg-ink" : "bg-line-strong") })]
7746
+ }), /* @__PURE__ */ jsx("span", { className: cn("relative flex-1 overflow-clip bg-line-strong group-last/step:hidden", "forced-colors:border-[CanvasText] forced-colors:before:bg-[CanvasText]", "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-['']", lineLayout[orientation], status === "done" ? "before:scale-100" : "before:scale-0") })]
7114
7747
  }), /* @__PURE__ */ jsxs("div", {
7115
7748
  className: cn("min-w-0 flex-1", bodyLayout[orientation]),
7116
7749
  children: [/* @__PURE__ */ jsx("div", {
@@ -7140,6 +7773,8 @@ function Step({ title, description, invalid = false, href, target, rel, render,
7140
7773
  //#region src/components/switch/Switch.tsx
7141
7774
  const trackTransition = "transition-colors duration-(--duration-fast) ease-standard";
7142
7775
  const dualLabel = "pointer-events-none relative min-w-9 px-3 text-center transition-colors duration-(--duration-fast) ease-standard peer-disabled:text-ink-disabled";
7776
+ const dualOffForced = "forced-colors:forced-color-adjust-none forced-colors:text-[HighlightText] forced-colors:peer-checked:text-[CanvasText] forced-colors:peer-disabled:text-[Canvas] forced-colors:peer-checked:peer-disabled:text-[GrayText]";
7777
+ const dualOnForced = "forced-colors:forced-color-adjust-none forced-colors:text-[CanvasText] forced-colors:peer-checked:text-[HighlightText] forced-colors:peer-disabled:text-[GrayText] forced-colors:peer-checked:peer-disabled:text-[Canvas]";
7143
7778
  /**
7144
7779
  * 开关:胶囊轨道 + 圆形旋钮,立即生效的设置用它(需要"保存"才生效的用复选框)。
7145
7780
  * 原生 `<input type="checkbox" role="switch">`,受控用 `checked`、非受控用 `defaultChecked`。
@@ -7161,22 +7796,22 @@ function Switch({ onCheckedChange, onChange, offLabel, onLabel, className, child
7161
7796
  type: "checkbox",
7162
7797
  role: "switch",
7163
7798
  onChange: handleChange,
7164
- className: cn("peer absolute inset-0 size-full appearance-none rounded-full bg-surface-inverse/80 group-hover:bg-surface-inverse disabled:cursor-not-allowed disabled:bg-surface-muted", trackTransition, focusRing)
7799
+ className: cn("peer absolute inset-0 size-full appearance-none rounded-full bg-surface-inverse/80 group-hover:bg-surface-inverse disabled:cursor-not-allowed disabled:bg-surface-muted", "forced-colors:border forced-colors:border-[ButtonText] forced-colors:disabled:border-[GrayText]", trackTransition, focusRing)
7165
7800
  }),
7166
7801
  /* @__PURE__ */ jsx("span", {
7167
7802
  "aria-hidden": "true",
7168
- className: cn("pointer-events-none absolute inset-y-1 left-1 w-[calc(50%-0.25rem)] rounded-full bg-surface-muted", "transition-[translate] duration-(--duration-fast) ease-standard", "peer-checked:translate-x-full peer-disabled:bg-surface-raised")
7803
+ className: cn("pointer-events-none absolute inset-y-1 left-1 w-[calc(50%-0.25rem)] rounded-full bg-surface-muted", "transition-[translate] duration-(--duration-fast) ease-standard", "peer-checked:translate-x-full peer-disabled:bg-surface-raised", "forced-colors:bg-[Highlight] forced-colors:peer-disabled:bg-[GrayText]")
7169
7804
  }),
7170
7805
  /* @__PURE__ */ jsx("span", {
7171
7806
  "aria-hidden": "true",
7172
7807
  "data-value": "off",
7173
- className: cn(dualLabel, "text-ink peer-checked:text-ink-inverse/70"),
7808
+ className: cn(dualLabel, "text-ink peer-checked:text-ink-inverse/70", dualOffForced),
7174
7809
  children: offLabel
7175
7810
  }),
7176
7811
  /* @__PURE__ */ jsx("span", {
7177
7812
  "aria-hidden": "true",
7178
7813
  "data-value": "on",
7179
- className: cn(dualLabel, "text-ink-inverse/70 peer-checked:text-ink"),
7814
+ className: cn(dualLabel, "text-ink-inverse/70 peer-checked:text-ink", dualOnForced),
7180
7815
  children: onLabel
7181
7816
  })
7182
7817
  ]
@@ -7187,10 +7822,10 @@ function Switch({ onCheckedChange, onChange, offLabel, onLabel, className, child
7187
7822
  type: "checkbox",
7188
7823
  role: "switch",
7189
7824
  onChange: handleChange,
7190
- className: cn("peer h-7 w-13 appearance-none rounded-full bg-line-strong group-hover:bg-ink-tertiary checked:bg-surface-inverse disabled:cursor-not-allowed disabled:bg-surface-muted", trackTransition, focusRing)
7825
+ className: cn("peer h-7 w-13 appearance-none rounded-full bg-line-strong group-hover:bg-ink-tertiary checked:bg-surface-inverse disabled:cursor-not-allowed disabled:bg-surface-muted", "forced-colors:border-2 forced-colors:border-[ButtonText] forced-colors:checked:border-[Highlight] forced-colors:checked:bg-[Highlight]", "forced-colors:disabled:border-[GrayText] forced-colors:checked:disabled:bg-[GrayText]", trackTransition, focusRing)
7191
7826
  }), /* @__PURE__ */ jsx("span", {
7192
7827
  "aria-hidden": "true",
7193
- className: cn("pointer-events-none absolute top-1 left-1 size-5 rounded-full bg-surface-raised", "transition-[translate,background-color] duration-(--duration-fast) ease-standard", "peer-checked:translate-x-6 peer-checked:bg-accent-ink-inverse peer-disabled:bg-ink-disabled")
7828
+ className: cn("pointer-events-none absolute top-1 left-1 size-5 rounded-full bg-surface-raised", "transition-[translate,background-color] duration-(--duration-fast) ease-standard", "peer-checked:translate-x-6 peer-checked:bg-accent-ink-inverse peer-disabled:bg-ink-disabled", "forced-colors:bg-[ButtonText] forced-colors:peer-checked:bg-[HighlightText] forced-colors:peer-disabled:bg-[GrayText] forced-colors:peer-checked:peer-disabled:bg-[Canvas]")
7194
7829
  })]
7195
7830
  });
7196
7831
  return /* @__PURE__ */ jsxs("label", {
@@ -7281,7 +7916,7 @@ function TableHead(props) {
7281
7916
  })
7282
7917
  });
7283
7918
  }
7284
- const ruledLines = ["[&>tr:nth-child(5n_of_:not([data-detail])):not([data-expanded])>*]:border-line-strong", "[&>tr:nth-child(5n_of_:not([data-detail]))+tr[data-detail]>*]:border-line-strong"].join(" ");
7919
+ const ruledLines = ["[&>tr:nth-child(5n_of_:not([data-detail])):not(:has(+tr[data-detail]))>*]:border-line-strong", "[&>tr:nth-child(5n_of_:not([data-detail]))+tr[data-detail]>*]:border-line-strong"].join(" ");
7285
7920
  function TableBody({ className, ...props }) {
7286
7921
  const { ruled } = useContext(TableContext);
7287
7922
  return /* @__PURE__ */ jsx(SectionContext, {
@@ -7305,6 +7940,7 @@ function TableRow({ selected, detail, expanded: expandedProp, defaultExpanded =
7305
7940
  });
7306
7941
  const expandable = detail !== void 0 && detail !== null && detail !== false;
7307
7942
  const open = expandable && expanded;
7943
+ const detailPresence = usePresence(open);
7308
7944
  const context = useMemo(() => expandable ? {
7309
7945
  expanded: open,
7310
7946
  toggle: () => setExpanded(!open),
@@ -7327,15 +7963,16 @@ function TableRow({ selected, detail, expanded: expandedProp, defaultExpanded =
7327
7963
  "aria-selected": selected,
7328
7964
  "data-selected": selected ? "" : void 0,
7329
7965
  "data-expanded": open ? "" : void 0,
7330
- className: cn("group/row transition-colors duration-(--duration-fast) ease-standard", selected ? rowSelected : rowIdle, "[&[data-expanded]>*]:border-b-transparent", className),
7966
+ className: cn("group/row transition-colors duration-(--duration-fast) ease-standard", selected ? rowSelected : rowIdle, "[&:has(+tr[data-detail])>*]:border-b-transparent", className),
7331
7967
  children: /* @__PURE__ */ jsx(RowContext, {
7332
7968
  value: context,
7333
7969
  children
7334
7970
  })
7335
- }), open && /* @__PURE__ */ jsx(DetailRow, {
7971
+ }), detailPresence.mounted && /* @__PURE__ */ jsx(DetailRow, {
7336
7972
  id: detailId,
7337
7973
  rowRef,
7338
7974
  estimate: countCells(children),
7975
+ presence: detailPresence,
7339
7976
  children: detail
7340
7977
  })] });
7341
7978
  }
@@ -7348,7 +7985,7 @@ function countCells(children) {
7348
7985
  }
7349
7986
  return Math.max(total, 1);
7350
7987
  }
7351
- function DetailRow({ id, rowRef, estimate, children }) {
7988
+ function DetailRow({ id, rowRef, estimate, presence, children }) {
7352
7989
  const [measured, setMeasured] = useState(null);
7353
7990
  useLayoutEffect(() => {
7354
7991
  const row = rowRef.current;
@@ -7360,12 +7997,21 @@ function DetailRow({ id, rowRef, estimate, children }) {
7360
7997
  return /* @__PURE__ */ jsx("tr", {
7361
7998
  id,
7362
7999
  "data-detail": "",
8000
+ inert: presence.leaving,
7363
8001
  children: /* @__PURE__ */ jsx("td", {
7364
8002
  colSpan: measured ?? estimate,
7365
8003
  className: "border-b border-line bg-surface-sunken p-0 [tr:last-child>&]:border-b-0",
7366
8004
  children: /* @__PURE__ */ jsx("div", {
7367
- className: "sticky left-0 w-[var(--table-viewport,100%)] px-4 py-3 text-sm whitespace-normal",
7368
- children
8005
+ ref: presence.ref,
8006
+ "data-leaving": presence.leaving ? "" : void 0,
8007
+ className: cn("sticky left-0 w-[var(--table-viewport,100%)]", collapse, presence.entered && "starting:grid-rows-[0fr]"),
8008
+ children: /* @__PURE__ */ jsx("div", {
8009
+ className: collapseInner,
8010
+ children: /* @__PURE__ */ jsx("div", {
8011
+ className: "px-4 py-3 text-sm whitespace-normal",
8012
+ children
8013
+ })
8014
+ })
7369
8015
  })
7370
8016
  })
7371
8017
  });
@@ -7466,18 +8112,23 @@ function Tabs({ value, defaultValue, onValueChange, variant = "block", size = "m
7466
8112
  defaultValue: defaultValue ?? "",
7467
8113
  onChange: onValueChange
7468
8114
  });
8115
+ const [initial] = useState(current);
8116
+ const [switched, setSwitched] = useState(false);
8117
+ if (!switched && current !== initial) setSwitched(true);
7469
8118
  const context = useMemo(() => ({
7470
8119
  value: current,
7471
8120
  select,
7472
8121
  baseId,
7473
8122
  variant,
7474
- size
8123
+ size,
8124
+ switched
7475
8125
  }), [
7476
8126
  current,
7477
8127
  select,
7478
8128
  baseId,
7479
8129
  variant,
7480
- size
8130
+ size,
8131
+ switched
7481
8132
  ]);
7482
8133
  return /* @__PURE__ */ jsx(TabsContext, {
7483
8134
  value: context,
@@ -7487,8 +8138,9 @@ function Tabs({ value, defaultValue, onValueChange, variant = "block", size = "m
7487
8138
  })
7488
8139
  });
7489
8140
  }
7490
- function TabList({ className, onKeyDown, ...props }) {
8141
+ function TabList({ className, onKeyDown, ref, ...props }) {
7491
8142
  const { variant } = useTabsContext("TabList");
8143
+ const listRef = useIndicator(variant === "capsule" ? "[role=\"tab\"][aria-selected=\"true\"]" : null);
7492
8144
  const handleKeyDown = (event) => {
7493
8145
  onKeyDown?.(event);
7494
8146
  if (event.defaultPrevented) return;
@@ -7518,10 +8170,17 @@ function TabList({ className, onKeyDown, ...props }) {
7518
8170
  };
7519
8171
  return /* @__PURE__ */ jsx("div", {
7520
8172
  ...props,
8173
+ ref: mergeRefs(listRef, ref),
7521
8174
  role: "tablist",
7522
8175
  "aria-orientation": "horizontal",
7523
8176
  onKeyDown: handleKeyDown,
7524
- className: cn("flex overflow-x-auto [scrollbar-width:thin]", variant === "capsule" && "gap-2 p-1", variant === "wedge" && "border-b-2 border-ink", className)
8177
+ className: cn("flex overflow-x-auto [scrollbar-width:thin]", variant === "capsule" && [
8178
+ "gap-2 p-1",
8179
+ indicator,
8180
+ indicatorBox,
8181
+ "after:z-0 after:rounded-full after:bg-surface-inverse",
8182
+ "forced-colors:after:bg-[Highlight]"
8183
+ ], variant === "wedge" && "border-b-2 border-ink", className)
7525
8184
  });
7526
8185
  }
7527
8186
  const blockSize = {
@@ -7561,12 +8220,15 @@ function Tab({ value, disabled = false, className, children, onClick, ...props }
7561
8220
  };
7562
8221
  if (variant === "capsule") return /* @__PURE__ */ jsx("button", {
7563
8222
  ...shared,
7564
- className: cn(capsuleBase, focusRing, capsuleSize[size], selected ? capsuleState.selected : disabled ? capsuleState.disabled : capsuleState.rest, className),
7565
- children
8223
+ className: cn(capsuleBase, focusRing, capsuleSize[size], selected ? [capsuleState.selected, "in-data-indicator:bg-transparent forced-colors:in-data-indicator:bg-transparent"] : disabled ? capsuleState.disabled : capsuleState.rest, className),
8224
+ children: /* @__PURE__ */ jsx("span", {
8225
+ className: "relative z-1 inline-flex items-center gap-[inherit]",
8226
+ children
8227
+ })
7566
8228
  });
7567
8229
  if (variant === "wedge") return /* @__PURE__ */ jsx("button", {
7568
8230
  ...shared,
7569
- className: cn("relative isolate inline-flex shrink-0 items-center justify-center leading-none font-medium whitespace-nowrap", "transition-colors duration-(--duration-fast) ease-standard", "before:absolute before:inset-0 before:-z-10 before:wedge-r before:transition-colors before:duration-(--duration-fast) before:ease-standard before:content-['']", focusRingInset, wedgeSize[size], selected ? "text-on-action before:bg-action" : disabled ? "cursor-not-allowed text-ink-disabled" : "text-ink hover:before:bg-ink/5", className),
8231
+ className: cn("relative isolate inline-flex shrink-0 items-center justify-center leading-none font-medium whitespace-nowrap", "transition-colors duration-(--duration-fast) ease-standard", "before:absolute before:inset-0 before:-z-10 before:wedge-r before:transition-colors before:duration-(--duration-fast) before:ease-standard before:content-['']", focusRingInset, wedgeSize[size], selected ? "text-on-action before:bg-action focus-visible:-outline-offset-4 focus-visible:outline-on-action forced-colors:forced-color-adjust-none forced-colors:text-[HighlightText] forced-colors:before:bg-[Highlight] forced-colors:focus-visible:-outline-offset-4 forced-colors:focus-visible:outline-[HighlightText]" : disabled ? "cursor-not-allowed text-ink-disabled" : "text-ink hover:before:bg-ink/5", className),
7570
8232
  children
7571
8233
  });
7572
8234
  return /* @__PURE__ */ jsxs("button", {
@@ -7582,7 +8244,10 @@ function Tab({ value, disabled = false, className, children, onClick, ...props }
7582
8244
  })]
7583
8245
  });
7584
8246
  }
7585
- /** 未选中的面板保留一个隐藏的空容器,让页签的 `aria-controls` 始终有所指。 */
8247
+ /**
8248
+ * 未选中的面板保留一个隐藏的空容器,让页签的 `aria-controls` 始终有所指。
8249
+ * 切过来的面板淡入;一开始就选中的那一块不动。
8250
+ */
7586
8251
  function TabPanel({ value, className, children, ...props }) {
7587
8252
  const context = useTabsContext("TabPanel");
7588
8253
  const selected = context.value === value;
@@ -7593,7 +8258,7 @@ function TabPanel({ value, className, children, ...props }) {
7593
8258
  "aria-labelledby": toId(context.baseId, "tab", value),
7594
8259
  hidden: !selected,
7595
8260
  tabIndex: 0,
7596
- className: cn(focusRing, className),
8261
+ className: cn(focusRing, selected && context.switched && "animate-fade-in [animation-duration:var(--duration-fast)]", className),
7597
8262
  children: selected ? children : null
7598
8263
  });
7599
8264
  }
@@ -7616,6 +8281,170 @@ function TagPair({ name, value, emphasis = false, size = "md", className, ...pro
7616
8281
  });
7617
8282
  }
7618
8283
  //#endregion
8284
+ //#region src/lib/inline-style.ts
8285
+ /**
8286
+ * 往元素上临时写过内联样式、又都撤掉之后调:原来没有 `style` 属性的,
8287
+ * 不留一个空的在上面。
8288
+ *
8289
+ * 先问一句"有没有这个属性"不是多余的:浏览器是懒着把内联样式同步成属性的,
8290
+ * 不问就去删,删的那一下落空,回头它又冒出来一个空的 `style=""`。
8291
+ */
8292
+ function dropEmptyStyle(node) {
8293
+ if (node.style.length === 0 && node.hasAttribute("style")) node.removeAttribute("style");
8294
+ }
8295
+ const px = (value) => `${Math.round(value * 100) / 100}px`;
8296
+ /** 这个元素眼下挂着的位移:补位滑到一半时不是 0 */
8297
+ function flight(node) {
8298
+ const value = getComputedStyle(node).translate;
8299
+ if (!value || value === "none") return [0, 0];
8300
+ const [x = 0, y = 0] = value.split(" ").map((part) => Number.parseFloat(part) || 0);
8301
+ return [x, y];
8302
+ }
8303
+ /** 把写上去的内联样式撤干净:原来没有 style 属性的,不留一个空的在上面 */
8304
+ function unset(node, names) {
8305
+ for (const name of names) node.style.removeProperty(name);
8306
+ dropEmptyStyle(node);
8307
+ }
8308
+ const PIN = [
8309
+ "left",
8310
+ "top",
8311
+ "width",
8312
+ "height"
8313
+ ];
8314
+ /** 往走的那一项上写过的全部:钉它的那四样,钉不住时的藏 */
8315
+ const HELD = [...PIN, "display"];
8316
+ /**
8317
+ * 量一遍、记下来。这一次有一项刚走,就补位:走的钉在原处,
8318
+ * 其余挪了地方的先摆回旧位置(不过渡),再撤掉——撤的那一下滑回原位;
8319
+ * 容器因此变矮了的话,高度也从原来的收到新的。
8320
+ */
8321
+ function arrange(node, memory) {
8322
+ const { seen, pinned } = memory;
8323
+ const items = [...node.children];
8324
+ const leaving = (item) => item.hasAttribute("data-leaving");
8325
+ for (const item of items) {
8326
+ if (leaving(item) || !pinned.has(item)) continue;
8327
+ pinned.delete(item);
8328
+ unset(item, HELD);
8329
+ }
8330
+ const box = node.getBoundingClientRect();
8331
+ if (box.width === 0 || box.height === 0 || items.filter((item) => !leaving(item)).length > 50) {
8332
+ seen.clear();
8333
+ memory.height = 0;
8334
+ for (const item of items) {
8335
+ if (!leaving(item) || pinned.has(item)) continue;
8336
+ pinned.add(item);
8337
+ item.style.setProperty("display", "none");
8338
+ }
8339
+ return;
8340
+ }
8341
+ const laid = Number.parseFloat(getComputedStyle(node).width);
8342
+ const scale = laid > 0 ? box.width / laid : 1;
8343
+ const gone = [];
8344
+ const moved = [];
8345
+ const next = [];
8346
+ for (const item of items) {
8347
+ const before = seen.get(item);
8348
+ if (leaving(item)) {
8349
+ if (!pinned.has(item)) gone.push([item, before]);
8350
+ continue;
8351
+ }
8352
+ const rect = item.getBoundingClientRect();
8353
+ const [x, y] = flight(item);
8354
+ const place = {
8355
+ left: (rect.left - box.left) / scale - node.clientLeft + node.scrollLeft - x,
8356
+ top: (rect.top - box.top) / scale - node.clientTop + node.scrollTop - y,
8357
+ width: rect.width / scale,
8358
+ height: rect.height / scale
8359
+ };
8360
+ next.push([item, place]);
8361
+ if (!before) continue;
8362
+ const dx = before.left + x - place.left;
8363
+ const dy = before.top + y - place.top;
8364
+ if (Math.abs(dx) >= .5 || Math.abs(dy) >= .5) moved.push([
8365
+ item,
8366
+ dx,
8367
+ dy
8368
+ ]);
8369
+ }
8370
+ seen.clear();
8371
+ for (const [item, place] of next) seen.set(item, place);
8372
+ const tall = box.height / scale;
8373
+ const shrinking = memory.settle !== null;
8374
+ const from = shrinking ? tall : memory.height;
8375
+ if (!shrinking) memory.height = tall;
8376
+ if (gone.length === 0) return;
8377
+ for (const [item, place] of gone) {
8378
+ pinned.add(item);
8379
+ if (place) PIN.forEach((name) => item.style.setProperty(name, px(place[name])));
8380
+ else item.style.setProperty("display", "none");
8381
+ }
8382
+ memory.settle?.();
8383
+ memory.settle = null;
8384
+ node.style.setProperty("transition", "none");
8385
+ node.style.removeProperty("min-height");
8386
+ const to = node.getBoundingClientRect().height / scale;
8387
+ memory.height = to;
8388
+ const shrink = from - to >= .5;
8389
+ if (shrink) node.style.setProperty("min-height", px(from));
8390
+ for (const [item, dx, dy] of moved) {
8391
+ item.style.setProperty("transition", "none");
8392
+ item.style.setProperty("translate", `${px(dx)} ${px(dy)}`);
8393
+ }
8394
+ node.getBoundingClientRect();
8395
+ for (const [item] of moved) unset(item, ["transition", "translate"]);
8396
+ node.style.removeProperty("transition");
8397
+ if (shrink) {
8398
+ node.style.setProperty("min-height", px(to));
8399
+ memory.settle = afterMotion(node, () => {
8400
+ memory.settle = null;
8401
+ unset(node, ["min-height"]);
8402
+ });
8403
+ } else dropEmptyStyle(node);
8404
+ }
8405
+ /**
8406
+ * 补位:容器里有一项走了,其余的直接子元素从旧位置滑到新位置,不是瞬移过去。
8407
+ * 返回的 `ref` 给容器。
8408
+ *
8409
+ * - 走的那一项要带 `data-leaving`、并且脱离排版(`lib/motion.ts` 的 `flipLeaving`):
8410
+ * 钩子把它钉在原来的位置上,它在那里把退场走完;容器要是定位的参照(`flipBox`)。
8411
+ * - 其余各项要给 `translate` 加上过渡:钩子只负责把它们摆回旧位置再放手,
8412
+ * 滑回来靠的是 CSS——所以系统开了"减少动态效果"时是直接到位的。
8413
+ * - 容器因此变矮了(少了一行),它的高度也从原来的收到新的(量着收,过渡同样在
8414
+ * `flipBox` 里);收完撤掉,不留在容器上。
8415
+ * - 各项只动 `translate`。正滑到一半又少一项,从眼前的位置接着走。
8416
+ * - 只有"少了一项"才补位;加一项、容器尺寸变了,各项直接到位。
8417
+ * - 超过 `FLIP_LIMIT` 项、量不到位置时不做:走的那一项也不留,照旧直接消失。
8418
+ */
8419
+ function useFlip() {
8420
+ const ref = useRef(null);
8421
+ const memory = useRef({
8422
+ seen: /* @__PURE__ */ new Map(),
8423
+ height: 0,
8424
+ pinned: /* @__PURE__ */ new WeakSet(),
8425
+ settle: null
8426
+ });
8427
+ useLayoutEffect(() => {
8428
+ const node = ref.current;
8429
+ if (node) arrange(node, memory.current);
8430
+ });
8431
+ useLayoutEffect(() => {
8432
+ const node = ref.current;
8433
+ if (!node || typeof ResizeObserver === "undefined") return;
8434
+ const observer = new ResizeObserver(() => arrange(node, memory.current));
8435
+ observer.observe(node);
8436
+ return () => observer.disconnect();
8437
+ }, []);
8438
+ useEffect(() => {
8439
+ const held = memory.current;
8440
+ return () => {
8441
+ held.settle?.();
8442
+ held.settle = null;
8443
+ };
8444
+ }, []);
8445
+ return ref;
8446
+ }
8447
+ //#endregion
7619
8448
  //#region src/components/tag-input/TagInput.tsx
7620
8449
  const defaultLabels = {
7621
8450
  summary: (tags) => tags.length === 0 ? "还没有标签" : `已有 ${tags.length} 个标签:${tags.join("、")}`,
@@ -7663,6 +8492,9 @@ function TagInput({ value, defaultValue, onValueChange, name, max, validate, sep
7663
8492
  const [flash, setFlash] = useState(null);
7664
8493
  const [message, setMessage] = useState("");
7665
8494
  const full = max !== void 0 && tags.length >= max;
8495
+ const added = useAddedKeys(tags);
8496
+ const rowRef = useFlip();
8497
+ const lingering = useLingering(tags, tags.length < 50);
7666
8498
  useEffect(() => {
7667
8499
  if (flash === null) return;
7668
8500
  const timer = setTimeout(() => setFlash(null), FLASH_MS);
@@ -7820,24 +8652,31 @@ function TagInput({ value, defaultValue, onValueChange, name, max, validate, sep
7820
8652
  }), "items-center", multiValueBox[size], className),
7821
8653
  children: [
7822
8654
  /* @__PURE__ */ jsxs("div", {
7823
- className: "flex min-w-0 flex-1 flex-wrap items-center gap-1 py-1 pl-2",
7824
- children: [tags.map((tag, index) => /* @__PURE__ */ jsxs("span", {
7825
- "data-tag": "",
7826
- "data-flash": flash === tag ? "" : void 0,
7827
- className: cn(valueChip, valueChipSize[size], "transition-colors duration-(--duration-fast) ease-standard", "data-flash:bg-surface-inverse data-flash:text-ink-inverse", disabled && "text-ink-disabled", !editable && "pr-2"),
7828
- children: [/* @__PURE__ */ jsx("span", {
7829
- className: "min-w-0 truncate",
7830
- children: tag
7831
- }), editable && /* @__PURE__ */ jsx("button", {
7832
- type: "button",
7833
- tabIndex: -1,
7834
- "aria-label": removeLabel(tag),
7835
- onClick: (event) => remove(index, event.detail === 0 && tags.length > 1 ? index : "input"),
7836
- onKeyDown: (event) => handleChipKeyDown(event, index),
7837
- className: cn("flex aspect-square h-full shrink-0 items-center justify-center text-ink-secondary hover:bg-line hover:text-ink", "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus", flash === tag && "text-ink-inverse"),
7838
- children: /* @__PURE__ */ jsx(Close, { size: 12 })
7839
- })]
7840
- }, tag)), /* @__PURE__ */ jsx("input", {
8655
+ ref: mergeRefs(rowRef, lingering.ref),
8656
+ className: cn("flex min-w-0 flex-1 flex-wrap items-center gap-1 py-1 pl-2", flipBox),
8657
+ children: [lingering.entries.map(({ key: tag, leaving }) => {
8658
+ const index = tags.indexOf(tag);
8659
+ return /* @__PURE__ */ jsxs("span", {
8660
+ "data-tag": leaving ? void 0 : "",
8661
+ "data-leaving": leaving ? "" : void 0,
8662
+ "data-flash": flash === tag && !leaving ? "" : void 0,
8663
+ inert: leaving,
8664
+ "aria-hidden": leaving || void 0,
8665
+ className: cn(valueChip, valueChipSize[size], "transition-[color,background-color,translate,opacity] duration-(--duration-fast) ease-standard", "data-flash:bg-surface-inverse data-flash:text-ink-inverse", "forced-colors:data-flash:outline-2 forced-colors:data-flash:outline-[Highlight]", flipLeaving, added.has(tag) && "animate-fade-in [animation-duration:var(--duration-fast)]", disabled && "text-ink-disabled forced-colors:border-[GrayText] forced-colors:text-[GrayText]", !editable && "pr-2"),
8666
+ children: [/* @__PURE__ */ jsx("span", {
8667
+ className: "min-w-0 truncate",
8668
+ children: tag
8669
+ }), editable && /* @__PURE__ */ jsx("button", {
8670
+ type: "button",
8671
+ tabIndex: -1,
8672
+ "aria-label": removeLabel(tag),
8673
+ onClick: (event) => remove(index, event.detail === 0 && tags.length > 1 ? index : "input"),
8674
+ onKeyDown: (event) => handleChipKeyDown(event, index),
8675
+ className: cn("flex aspect-square h-full shrink-0 items-center justify-center text-ink-secondary hover:bg-line hover:text-ink forced-colors:in-data-disabled:text-[GrayText]", "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-focus", flash === tag && "text-ink-inverse"),
8676
+ children: /* @__PURE__ */ jsx(Close, { size: 12 })
8677
+ })]
8678
+ }, tag);
8679
+ }), /* @__PURE__ */ jsx("input", {
7841
8680
  ...props,
7842
8681
  ref: mergeRefs(inputRef, ref),
7843
8682
  id,
@@ -7862,7 +8701,7 @@ function TagInput({ value, defaultValue, onValueChange, name, max, validate, sep
7862
8701
  onBlur?.(event);
7863
8702
  if (commitOnBlur && editable && text.trim() !== "") setText(commit([text]));
7864
8703
  },
7865
- className: cn(controlElement, "min-w-12 px-1", valueInputHeight[size])
8704
+ className: cn(controlElement, "min-w-12 px-1", valueInputHeight[size], "transition-[translate] duration-(--duration-fast) ease-standard")
7866
8705
  })]
7867
8706
  }),
7868
8707
  (invalid || max !== void 0) && /* @__PURE__ */ jsxs("span", {
@@ -7873,7 +8712,7 @@ function TagInput({ value, defaultValue, onValueChange, name, max, validate, sep
7873
8712
  }), max !== void 0 && /* @__PURE__ */ jsx("span", {
7874
8713
  "aria-hidden": "true",
7875
8714
  "data-count": "",
7876
- className: cn("font-tech text-xs tabular-nums", disabled ? "text-ink-disabled" : full ? "text-ink" : "text-ink-secondary"),
8715
+ className: cn("font-tech text-xs tabular-nums", disabled ? "text-ink-disabled forced-colors:text-[GrayText]" : full ? "text-ink" : "text-ink-secondary"),
7877
8716
  children: `${tags.length} / ${max}`
7878
8717
  })]
7879
8718
  }),
@@ -7965,7 +8804,7 @@ function Textarea({ variant = "sunken", invalid: invalidProp, showCount = false,
7965
8804
  className: cn(controlElement, "block min-h-[calc(3lh+1rem)] w-full resize-y px-3 py-2")
7966
8805
  }), showCount && /* @__PURE__ */ jsx("span", {
7967
8806
  "data-count": "",
7968
- className: cn("pointer-events-none self-end px-3 pb-1.5 font-tech text-xs tabular-nums", disabled ? "text-ink-disabled" : "text-ink-secondary"),
8807
+ className: cn("pointer-events-none self-end px-3 pb-1.5 font-tech text-xs tabular-nums", disabled ? "text-ink-disabled forced-colors:text-[GrayText]" : "text-ink-secondary"),
7969
8808
  children: maxLength !== void 0 ? `${length} / ${maxLength}` : length
7970
8809
  })]
7971
8810
  });
@@ -8034,8 +8873,8 @@ function Timeline({ className, ...props }) {
8034
8873
  });
8035
8874
  }
8036
8875
  const nodeClass = {
8037
- past: "bg-ink",
8038
- current: "border border-ink bg-action",
8876
+ past: "bg-ink forced-colors:bg-[CanvasText]",
8877
+ current: "border border-ink bg-action forced-colors:bg-[Highlight]",
8039
8878
  upcoming: "border-2 border-ink-tertiary"
8040
8879
  };
8041
8880
  const titleClass = {
@@ -8056,7 +8895,7 @@ function TimelineItem({ status = "past", date, title, className, children, ...pr
8056
8895
  children: [/* @__PURE__ */ jsx("span", {
8057
8896
  className: "flex h-[1lh] min-h-4 w-4 items-center justify-center",
8058
8897
  children: /* @__PURE__ */ jsx("span", { className: cn("size-[11px] rotate-45", nodeClass[status]) })
8059
- }), /* @__PURE__ */ jsx("span", { className: "w-0.5 flex-1 bg-line-strong group-last/item:hidden" })]
8898
+ }), /* @__PURE__ */ jsx("span", { className: "w-0.5 flex-1 bg-line-strong group-last/item:hidden forced-colors:bg-[CanvasText]" })]
8060
8899
  }), /* @__PURE__ */ jsxs("div", {
8061
8900
  className: "min-w-0 flex-1 pb-6 group-last/item:pb-0",
8062
8901
  children: [
@@ -8101,11 +8940,11 @@ const toneText = {
8101
8940
  const placementClass = {
8102
8941
  bottom: {
8103
8942
  viewport: "bottom-6",
8104
- root: "bottom-0 translate-y-[var(--toast-swipe-movement-y,0px)]"
8943
+ 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)"
8105
8944
  },
8106
8945
  center: {
8107
8946
  viewport: "top-1/2",
8108
- root: "top-0 translate-y-[calc(-50%+var(--toast-swipe-movement-y,0px))]"
8947
+ root: "top-0 transform-[translate(calc(-50%+var(--toast-swipe-movement-x,0px)),calc(-50%+var(--toast-swipe-movement-y,0px)))]"
8109
8948
  }
8110
8949
  };
8111
8950
  function isOptions(input) {
@@ -8152,7 +8991,7 @@ function ToastHost({ placement, label, closeLabel, children }) {
8152
8991
  return /* @__PURE__ */ jsx(Toast.Root, {
8153
8992
  toast,
8154
8993
  "data-theme": "dark",
8155
- className: cn("pointer-events-auto absolute left-1/2 w-max max-w-full translate-x-[calc(-50%+var(--toast-swipe-movement-x,0px))]", "border border-line bg-black/80 text-base leading-[1.4] text-white", "transition-opacity duration-(--duration-fast) ease-standard data-ending-style:opacity-0 data-starting-style:opacity-0", focusRingInset, placementClass[placement].root),
8994
+ className: cn("pointer-events-auto absolute left-1/2 w-max max-w-full", "border border-line bg-black/80 text-base leading-[1.4] text-white", "transition-[opacity,translate,transform] duration-[var(--duration-fast),var(--duration-base),var(--duration-fast)] ease-[var(--ease-standard),var(--ease-exit),var(--ease-standard)]", "data-ending-style:opacity-0 data-starting-style:opacity-0", "data-swiping:select-none", focusRingInset, placementClass[placement].root),
8156
8995
  children: /* @__PURE__ */ jsxs(Toast.Content, {
8157
8996
  className: cn("flex items-center gap-3 py-2.5 pl-5", toast.data?.dismissible ? "pr-2" : "pr-5"),
8158
8997
  children: [
@@ -8202,13 +9041,42 @@ const TocContext = createContext({
8202
9041
  /** 点了一项之后,多久没有滚动事件算这一次滚动停下了 */
8203
9042
  const SETTLE_MS = 150;
8204
9043
  /**
9044
+ * 让接下来的这一次滚动是平滑的:给这几个滚动容器写上内联的 `scroll-behavior`,
9045
+ * 滚动停下(`SETTLE_MS` 里没有再滚)就还原成原来的内联值。返回的函数提前还原。
9046
+ *
9047
+ * 滚动本身不归它管——跳过去的仍然是浏览器(锚点跳转)。
9048
+ */
9049
+ function smoothOnce(scrollers) {
9050
+ const before = scrollers.map((scroller) => scroller.style.getPropertyValue("scroll-behavior"));
9051
+ for (const scroller of scrollers) scroller.style.setProperty("scroll-behavior", "smooth");
9052
+ const sources = scrollers.map((scroller) => scroller === document.documentElement ? window : scroller);
9053
+ let timer;
9054
+ const restore = () => {
9055
+ clearTimeout(timer);
9056
+ for (const source of sources) source.removeEventListener("scroll", wait);
9057
+ scrollers.forEach((scroller, index) => {
9058
+ scroller.style.setProperty("scroll-behavior", before[index]);
9059
+ dropEmptyStyle(scroller);
9060
+ });
9061
+ };
9062
+ function wait() {
9063
+ clearTimeout(timer);
9064
+ timer = setTimeout(restore, SETTLE_MS);
9065
+ }
9066
+ for (const source of sources) source.addEventListener("scroll", wait, { passive: true });
9067
+ wait();
9068
+ return restore;
9069
+ }
9070
+ /**
8205
9071
  * 页内目录:这一页有哪几节、现在看到了哪一节。里面放 `TocItem`。
8206
9072
  *
8207
- * 位置由使用方给,通常是 `className="sticky top-6"`。点一项就是点一个锚点链接:
8208
- * 滚不滚、平不平滑是浏览器和页面 CSS 的事,它不接管滚动。
9073
+ * 位置由使用方给,通常是 `className="sticky top-6"`。点一项仍然是点一个锚点链接——
9074
+ * 地址、后退、`scroll-mt` 都是浏览器原生的那一套;它只让这一次跳转滚得平滑
9075
+ * (系统开了"减少动态效果"时照旧直接跳)。
8209
9076
  */
8210
9077
  function Toc({ title, offset = 0, target, onActiveChange, "aria-label": ariaLabel, className, ref, children, ...props }) {
8211
9078
  const navRef = useRef(null);
9079
+ const listRef = useIndicator("[aria-current]");
8212
9080
  const titleId = useId();
8213
9081
  const count = Children.count(children);
8214
9082
  const spied = useScrollPosition(target, useCallback(({ scrolled, room }) => {
@@ -8257,6 +9125,17 @@ function Toc({ title, offset = 0, target, onActiveChange, "aria-label": ariaLabe
8257
9125
  };
8258
9126
  }, [pinned, target]);
8259
9127
  const active = pinned ?? spied;
9128
+ const restoreScroll = useRef(null);
9129
+ useEffect(() => () => restoreScroll.current?.(), []);
9130
+ const pick = useCallback((section) => {
9131
+ setPinned(section);
9132
+ restoreScroll.current?.();
9133
+ restoreScroll.current = null;
9134
+ if (!document.getElementById(section)) return;
9135
+ if (typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
9136
+ const container = target?.current;
9137
+ restoreScroll.current = smoothOnce(container ? [container, document.documentElement] : [document.documentElement]);
9138
+ }, [target]);
8260
9139
  const report = useRef(onActiveChange);
8261
9140
  report.current = onActiveChange;
8262
9141
  const reported = useRef(null);
@@ -8278,8 +9157,8 @@ function Toc({ title, offset = 0, target, onActiveChange, "aria-label": ariaLabe
8278
9157
  }, [active]);
8279
9158
  const context = useMemo(() => ({
8280
9159
  active,
8281
- pick: setPinned
8282
- }), [active]);
9160
+ pick
9161
+ }), [active, pick]);
8283
9162
  const hasTitle = title !== void 0 && title !== null && title !== false;
8284
9163
  return /* @__PURE__ */ jsxs("nav", {
8285
9164
  ...props,
@@ -8292,7 +9171,8 @@ function Toc({ title, offset = 0, target, onActiveChange, "aria-label": ariaLabe
8292
9171
  className: "mb-2 pl-4 font-tech text-xs text-ink-secondary",
8293
9172
  children: title
8294
9173
  }), /* @__PURE__ */ jsx("ul", {
8295
- className: "relative before:absolute before:inset-y-0 before:left-0 before:w-px before:bg-line before:content-['']",
9174
+ ref: listRef,
9175
+ className: cn("before:absolute before:inset-y-0 before:left-0 before:-z-1 before:w-px before:bg-line before:content-['']", indicator, "after:top-1 after:h-[calc(var(--indicator-h)-0.5rem)] after:w-[3px] after:translate-y-(--indicator-y) after:bg-ink", "forced-colors:after:bg-[Highlight]"),
8296
9176
  children: /* @__PURE__ */ jsx(TocContext, {
8297
9177
  value: context,
8298
9178
  children
@@ -8330,7 +9210,7 @@ function TocItem({ href, section = sectionOf(href), level = 1, render, onClick,
8330
9210
  onClick?.(event);
8331
9211
  if (!event.defaultPrevented && section !== void 0) pick(section);
8332
9212
  },
8333
- className: cn("relative block py-2 pr-2 leading-snug wrap-anywhere", "transition-colors duration-(--duration-fast) ease-standard", focusRingInset, levelPadding[level], current ? "text-ink before:absolute before:inset-y-1 before:left-0 before:w-[3px] before:bg-ink before:content-['']" : "text-ink-secondary hover:text-ink"),
9213
+ className: cn("relative block py-2 pr-2 leading-snug wrap-anywhere", "transition-colors duration-(--duration-fast) ease-standard", focusRingInset, levelPadding[level], current ? "text-ink before:absolute before:inset-y-1 before:left-0 before:w-[3px] before:bg-ink before:content-[''] in-data-indicator:before:hidden forced-colors:before:bg-[Highlight]" : "text-ink-secondary hover:text-ink"),
8334
9214
  children
8335
9215
  })
8336
9216
  });
@@ -8345,6 +9225,10 @@ const barVariant = {
8345
9225
  sunken: "bg-surface-sunken p-0.5",
8346
9226
  outline: "border border-line-strong bg-surface p-px"
8347
9227
  };
9228
+ const barForced = {
9229
+ sunken: "forced-colors:outline forced-colors:-outline-offset-1 forced-colors:outline-[ButtonText] forced-colors:data-disabled:outline-[GrayText]",
9230
+ outline: "forced-colors:border-[ButtonText] forced-colors:data-disabled:border-[GrayText]"
9231
+ };
8348
9232
  /**
8349
9233
  * 工具栏:一排作用于同一个对象的小工具,收在一条带子里。整条只占一个 Tab 停靠点,
8350
9234
  * 方向键在各项之间走。里面放 `ToolbarButton`、`ToolbarToggle`、`ToolbarToggleGroup`、
@@ -8377,7 +9261,7 @@ function Toolbar({ size = "md", variant = "sunken", orientation = "horizontal",
8377
9261
  onKeyDown: handleKeyDown,
8378
9262
  "data-size": size,
8379
9263
  "data-variant": variant,
8380
- className: cn("inline-flex max-w-full gap-0.5 text-ink", orientation === "vertical" ? "flex-col" : "flex-wrap items-center", barVariant[variant], className)
9264
+ className: cn("inline-flex max-w-full gap-0.5 text-ink", orientation === "vertical" ? "flex-col" : "flex-wrap items-center", barVariant[variant], barForced[variant], className)
8381
9265
  })
8382
9266
  });
8383
9267
  }
@@ -8402,6 +9286,10 @@ const itemBase = [
8402
9286
  "data-popup-open:bg-surface-inverse data-popup-open:text-ink-inverse",
8403
9287
  "data-disabled:cursor-not-allowed data-disabled:bg-transparent data-disabled:text-ink-disabled",
8404
9288
  "data-disabled:data-pressed:bg-disabled data-disabled:data-pressed:text-on-disabled",
9289
+ "forced-colors:data-pressed:forced-color-adjust-none forced-colors:data-pressed:outline-[Highlight]",
9290
+ "forced-colors:data-pressed:bg-[Highlight] forced-colors:data-pressed:text-[HighlightText]",
9291
+ "forced-colors:data-disabled:data-pressed:bg-[GrayText] forced-colors:data-disabled:data-pressed:text-[Canvas]",
9292
+ "forced-colors:data-disabled:text-[GrayText]",
8405
9293
  "focus-visible:z-1",
8406
9294
  focusRing
8407
9295
  ].join(" ");
@@ -8409,7 +9297,9 @@ const itemIcon = [
8409
9297
  "flex shrink-0",
8410
9298
  "group-data-pressed/item:text-accent-ink-inverse",
8411
9299
  "group-data-popup-open/item:text-accent-ink-inverse",
8412
- "group-data-disabled/item:group-data-pressed/item:text-on-disabled"
9300
+ "group-data-disabled/item:group-data-pressed/item:text-on-disabled",
9301
+ "forced-colors:group-data-pressed/item:text-[HighlightText]",
9302
+ "forced-colors:group-data-disabled/item:group-data-pressed/item:text-[Canvas]"
8413
9303
  ].join(" ");
8414
9304
  function useItemClass(iconOnly, className) {
8415
9305
  const { size } = useContext(ToolbarContext);
@@ -8500,7 +9390,7 @@ function ToolbarGroup({ className, ...props }) {
8500
9390
  function ToolbarSeparator({ className, ...props }) {
8501
9391
  return /* @__PURE__ */ jsx(Toolbar$1.Separator, {
8502
9392
  ...props,
8503
- className: cn("shrink-0 self-center bg-line-strong", "data-[orientation=vertical]:mx-1 data-[orientation=vertical]:h-4 data-[orientation=vertical]:w-px", "data-[orientation=horizontal]:my-1 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-4", className)
9393
+ className: cn("shrink-0 self-center bg-line-strong", "forced-colors:bg-[ButtonText] forced-colors:in-data-disabled:bg-[GrayText]", "data-[orientation=vertical]:mx-1 data-[orientation=vertical]:h-4 data-[orientation=vertical]:w-px", "data-[orientation=horizontal]:my-1 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-4", className)
8504
9394
  });
8505
9395
  }
8506
9396
  //#endregion
@@ -8568,7 +9458,7 @@ function Tooltip({ content, children, side = "top", align = "center", delay, arr
8568
9458
  sideOffset: arrow ? 8 : 2,
8569
9459
  className: "z-(--z-overlay)",
8570
9460
  children: /* @__PURE__ */ jsxs(Tooltip$1.Popup, {
8571
- className: cn("relative max-w-64 border border-transparent bg-surface-raised px-2.5 py-1.5 text-sm wrap-anywhere text-ink", "origin-(--transform-origin) transition-opacity duration-(--duration-fast) ease-standard", "data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0", className),
9461
+ className: cn("relative max-w-64 border border-transparent bg-surface-raised px-2.5 py-1.5 text-sm wrap-anywhere text-ink", "origin-(--transform-origin) transition-[opacity,translate] duration-(--duration-fast) ease-standard", "data-ending-style:opacity-0 data-instant:transition-none data-starting-style:opacity-0", enterFromSide, className),
8572
9462
  children: [arrow && /* @__PURE__ */ jsx(Tooltip$1.Arrow, {
8573
9463
  "aria-hidden": "true",
8574
9464
  className: cn("size-2 rotate-45 bg-surface-raised forced-colors:hidden", "data-[side=bottom]:-top-1 data-[side=left]:-right-1 data-[side=right]:-left-1 data-[side=top]:-bottom-1")
@@ -8616,6 +9506,7 @@ function TopBar({ brand, tools, action, menu, className, children, ...props }) {
8616
9506
  //#region src/hooks/useTheme.ts
8617
9507
  const STORAGE_KEY = "ef-theme";
8618
9508
  const DARK_QUERY = "(prefers-color-scheme: dark)";
9509
+ const REDUCE_QUERY = "(prefers-reduced-motion: reduce)";
8619
9510
  const serverSnapshot = {
8620
9511
  theme: "light",
8621
9512
  resolvedTheme: "light"
@@ -8623,6 +9514,8 @@ const serverSnapshot = {
8623
9514
  const listeners = /* @__PURE__ */ new Set();
8624
9515
  let snapshot = null;
8625
9516
  let stopWatchingSystem = null;
9517
+ /** 正在走的那一次切换(视图过渡):它走完才撤掉 <html> 上的标记 */
9518
+ let switching = null;
8626
9519
  function isPreference(value) {
8627
9520
  return value === "light" || value === "dark" || value === "system";
8628
9521
  }
@@ -8648,13 +9541,39 @@ function getSnapshot() {
8648
9541
  }
8649
9542
  return snapshot;
8650
9543
  }
9544
+ /**
9545
+ * 换主题。亮暗真的变了、浏览器有视图过渡、系统没开"减少动态效果"时,放在视图过渡里换:
9546
+ * 整页交叉淡化一次,而不是带换色过渡的慢慢过去、别的硬切,参差不齐。否则当场换。
9547
+ *
9548
+ * 走视图过渡的时候是晚一帧才换的——浏览器要先把旧的样子拍下来。
9549
+ * 期间 <html> 上标着 data-theme-switching:淡化的时长挂在它下面(theme.css),
9550
+ * 使用方自己的视图过渡不受影响。局部主题那几块新旧一个样,看不出变化。
9551
+ */
8651
9552
  function commit(theme) {
8652
- snapshot = {
9553
+ const next = {
8653
9554
  theme,
8654
9555
  resolvedTheme: resolve(theme)
8655
9556
  };
8656
- document.documentElement.dataset.theme = snapshot.resolvedTheme;
8657
- for (const listener of listeners) listener();
9557
+ const element = document.documentElement;
9558
+ const apply = () => {
9559
+ snapshot = next;
9560
+ element.dataset.theme = next.resolvedTheme;
9561
+ for (const listener of listeners) listener();
9562
+ };
9563
+ if (element.dataset.theme === next.resolvedTheme || typeof document.startViewTransition !== "function" || window.matchMedia?.(REDUCE_QUERY).matches) {
9564
+ apply();
9565
+ return;
9566
+ }
9567
+ element.dataset.themeSwitching = "";
9568
+ const transition = document.startViewTransition(apply);
9569
+ switching = transition;
9570
+ const done = () => {
9571
+ if (switching !== transition) return;
9572
+ switching = null;
9573
+ delete element.dataset.themeSwitching;
9574
+ };
9575
+ transition.finished.then(done, done);
9576
+ transition.ready.catch(() => {});
8658
9577
  }
8659
9578
  function subscribe(listener) {
8660
9579
  listeners.add(listener);
@@ -8685,7 +9604,8 @@ function setTheme(theme) {
8685
9604
  *
8686
9605
  * - `theme`:用户的选择,可以是 `"system"`;
8687
9606
  * - `resolvedTheme`:实际生效的 `"light"` 或 `"dark"`;
8688
- * - `setTheme`:切换并记到 `localStorage`。
9607
+ * - `setTheme`:切换并记到 `localStorage`。亮暗变了的时候整页交叉淡化一次
9608
+ * (浏览器支持视图过渡、系统没开"减少动态效果"时;否则直接切)。
8689
9609
  */
8690
9610
  function useTheme() {
8691
9611
  const { theme, resolvedTheme } = useSyncExternalStore(subscribe, getSnapshot, () => serverSnapshot);
@@ -8703,4 +9623,4 @@ function useTheme() {
8703
9623
  /** 三条横线:打开菜单 */
8704
9624
  const Menu = createIcon("Menu", /* @__PURE__ */ jsx("path", { d: "M3 6h18M3 12h18M3 18h18" }));
8705
9625
  //#endregion
8706
- export { Accordion, AccordionItem, Alert, ArrowCorner, ArrowLeft, ArrowRight, Avatar, AvatarSwitcher, AvatarSwitcherItem, BackToTop, Badge, BracketTitle, Breadcrumb, BreadcrumbItem, Button, ButtonGroup, Calendar, CalendarIcon, Carousel, CarouselSlide, Check, Checkbox, ChevronDown, ChevronLeft, ChevronRight, Close, Combobox, CompletionBanner, ContextMenu, CornerBrackets, Countdown, Crosshair, DashIndicator, DataRow, DataRowList, DatePicker, DateRangePicker, Dialog, DialogClose, Drawer, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuGroup, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, EmptyState, Field, FileItem, FileUpload, FilterChip, FlyoutBar, FlyoutBarItem, GhostText, Hatch, HazardStripe, HoverCard, IconButton, ImageViewer, ImageViewerItem, Input, ItemGrid, ItemSlot, Kbd, List, ListRow, Loader, Lock, Marquee, MediaCard, Menu, Minus, NavAction, NavMenu, NavMenuItem, Navigator, OtpInput, PageHeader, PageHeaderBack, Pagination, Panel, PanelBody, PanelHeader, PanelRow, PanelRows, PlayButton, PlayMark, Plus, Popover, PopoverClose, Progress, ProgressRing, Radio, RadioGroup, RecIndicator, RegistrationStrip, ResourceChip, Schedule, ScheduleItem, ScheduleTrack, ScrollArea, ScrollHint, SectionTitle, Segment, SegmentedControl, Select, SelectGroup, SelectItem, SelectSeparator, SideRail, SideRailGroup, SideRailItem, SideRailSub, SideRailSubItem, Skeleton, Slider, Sparkline, Spinner, Stat, StatusDanger, StatusInfo, StatusSuccess, StatusWarning, Step, Stepper, Steps, Switch, Tab, TabList, TabPanel, Table, TableBody, TableCell, TableExpander, TableHead, TableHeaderCell, TableRow, Tabs, Tag, TagInput, TagPair, Term, Textarea, Texture, TickRing, Timeline, TimelineItem, ToastProvider, Toc, TocItem, Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator, ToolbarToggle, ToolbarToggleGroup, Tooltip, TooltipProvider, TopBar, TriangleRight, Upload, Viewfinder, cn, createIcon, useControllableState, useFieldContext, useFieldControl, useInView, usePortalScope, useTheme, useToast };
9626
+ export { Accordion, AccordionItem, Alert, ArrowCorner, ArrowLeft, ArrowRight, Avatar, AvatarSwitcher, AvatarSwitcherItem, BackToTop, Badge, BracketTitle, Breadcrumb, BreadcrumbItem, Button, ButtonGroup, Calendar, CalendarIcon, Carousel, CarouselSlide, Check, Checkbox, ChevronDown, ChevronLeft, ChevronRight, Close, Combobox, CompletionBanner, ContextMenu, CornerBrackets, Countdown, Crosshair, DashIndicator, DataRow, DataRowList, DatePicker, DateRangePicker, Dialog, DialogClose, Drawer, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuGroup, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, EmptyState, Field, FileItem, FileUpload, FilterChip, FlyoutBar, FlyoutBarItem, GhostText, Hatch, HazardStripe, HoverCard, IconButton, ImageViewer, ImageViewerItem, Input, ItemGrid, ItemSlot, Kbd, List, ListRow, Loader, Lock, Marquee, MediaCard, Menu, Minus, NavAction, NavMenu, NavMenuItem, Navigator, OtpInput, PageHeader, PageHeaderBack, Pagination, Panel, PanelBody, PanelHeader, PanelRow, PanelRows, PlayButton, PlayMark, Plus, Popover, PopoverClose, Progress, ProgressRing, Radio, RadioGroup, RecIndicator, RegistrationStrip, ResourceChip, RollingNumber, Schedule, ScheduleItem, ScheduleTrack, ScrollArea, ScrollHint, SectionTitle, Segment, SegmentedControl, Select, SelectGroup, SelectItem, SelectSeparator, SideRail, SideRailGroup, SideRailItem, SideRailSub, SideRailSubItem, Skeleton, Slider, Sparkline, Spinner, Stat, StatusDanger, StatusInfo, StatusSuccess, StatusWarning, Step, Stepper, Steps, Switch, Tab, TabList, TabPanel, Table, TableBody, TableCell, TableExpander, TableHead, TableHeaderCell, TableRow, Tabs, Tag, TagInput, TagPair, Term, Textarea, Texture, TickRing, Timeline, TimelineItem, ToastProvider, Toc, TocItem, Toolbar, ToolbarButton, ToolbarGroup, ToolbarSeparator, ToolbarToggle, ToolbarToggleGroup, Tooltip, TooltipProvider, TopBar, TriangleRight, Upload, Viewfinder, cn, createIcon, useControllableState, useFieldContext, useFieldControl, useInView, usePortalScope, useReducedMotion, useTheme, useToast };