@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.
- package/NOTICE.md +2 -2
- package/README.md +103 -8
- package/dist/index.d.ts +78 -17
- package/dist/index.js +1184 -264
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/README.md +11 -2
- package/src/components/alert/Alert.tsx +63 -1
- package/src/components/avatar/Avatar.tsx +5 -2
- package/src/components/bracket-title/BracketTitle.tsx +32 -3
- package/src/components/button/Button.tsx +37 -5
- package/src/components/calendar/Calendar.tsx +49 -5
- package/src/components/checkbox/Checkbox.tsx +1 -0
- package/src/components/checkbox/choice-style.ts +13 -5
- package/src/components/chip/capsule-style.ts +18 -3
- package/src/components/combobox/Combobox.tsx +10 -0
- package/src/components/completion-banner/CompletionBanner.tsx +30 -2
- package/src/components/corner-brackets/CornerBrackets.tsx +2 -1
- package/src/components/dash-indicator/DashIndicator.tsx +5 -2
- package/src/components/data-row/DataRow.tsx +2 -1
- package/src/components/date-picker/DateField.tsx +5 -1
- package/src/components/dialog/Dialog.tsx +4 -2
- package/src/components/dropdown-menu/MenuCheck.tsx +4 -2
- package/src/components/dropdown-menu/menu-style.ts +22 -5
- package/src/components/empty-state/EmptyState.tsx +5 -0
- package/src/components/field/Field.tsx +39 -7
- package/src/components/file-upload/FileUpload.tsx +79 -20
- package/src/components/icon-button/IconButton.tsx +18 -5
- package/src/components/image-viewer/ImageViewer.tsx +58 -18
- package/src/components/input/control-box.ts +23 -2
- package/src/components/input/value-chip.ts +3 -2
- package/src/components/item-slot/ItemSlot.tsx +2 -1
- package/src/components/loader/Loader.tsx +12 -41
- package/src/components/nav-menu/NavMenu.tsx +17 -3
- package/src/components/otp-input/OtpInput.tsx +13 -4
- package/src/components/page-header/PageHeader.tsx +39 -3
- package/src/components/pagination/Pagination.tsx +2 -0
- package/src/components/progress/Progress.tsx +10 -3
- package/src/components/progress/ProgressRing.tsx +19 -4
- package/src/components/radio/Radio.tsx +2 -0
- package/src/components/rolling-number/RollingNumber.tsx +125 -0
- package/src/components/rolling-number/index.ts +1 -0
- package/src/components/schedule/Schedule.tsx +5 -3
- package/src/components/segmented-control/SegmentedControl.tsx +25 -4
- package/src/components/skeleton/Skeleton.tsx +4 -1
- package/src/components/slider/Slider.tsx +26 -5
- package/src/components/sparkline/Sparkline.tsx +29 -5
- package/src/components/spinner/Spinner.tsx +6 -2
- package/src/components/stepper/Stepper.tsx +4 -0
- package/src/components/steps/Steps.tsx +30 -9
- package/src/components/switch/Switch.tsx +26 -2
- package/src/components/table/Table.tsx +33 -9
- package/src/components/tabs/Tabs.tsx +55 -9
- package/src/components/tag-input/TagInput.tsx +82 -43
- package/src/components/textarea/Textarea.tsx +2 -1
- package/src/components/timeline/Timeline.tsx +4 -3
- package/src/components/toast/Toast.tsx +18 -5
- package/src/components/toc/Toc.tsx +89 -7
- package/src/components/toolbar/Toolbar.tsx +24 -0
- package/src/components/tooltip/Tooltip.tsx +5 -1
- package/src/components/viewfinder/Viewfinder.tsx +9 -0
- package/src/hooks/README.md +8 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useAddedKeys.ts +26 -0
- package/src/hooks/useFlip.ts +211 -0
- package/src/hooks/useIndicator.ts +135 -0
- package/src/hooks/useLingering.ts +122 -0
- package/src/hooks/usePresence.ts +100 -0
- package/src/hooks/useReducedMotion.ts +26 -0
- package/src/hooks/useTheme.ts +44 -4
- package/src/index.ts +1 -0
- package/src/lib/README.md +2 -0
- package/src/lib/cn.ts +3 -0
- package/src/lib/inline-style.ts +12 -0
- package/src/lib/motion.ts +90 -0
- package/src/styles/theme.css +81 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
1528
|
-
|
|
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:
|
|
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:
|
|
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
|
-
/**
|
|
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: [
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
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
|
-
/**
|
|
1886
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-[
|
|
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 :
|
|
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 &&
|
|
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
|
-
|
|
3658
|
-
ref: listRef,
|
|
4093
|
+
lingering.entries.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
4094
|
+
ref: mergeRefs(listRef, lingering.ref),
|
|
3659
4095
|
"aria-label": labels.list,
|
|
3660
|
-
|
|
3661
|
-
|
|
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(
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
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
|
-
|
|
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(
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
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,
|
|
4530
|
-
const rootRef =
|
|
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
|
-
}, [
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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: "
|
|
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:
|
|
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
|
|
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 ?
|
|
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" ? "
|
|
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-
|
|
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, "[
|
|
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
|
-
}),
|
|
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
|
-
|
|
7368
|
-
|
|
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" &&
|
|
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
|
-
/**
|
|
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
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
9553
|
+
const next = {
|
|
8653
9554
|
theme,
|
|
8654
9555
|
resolvedTheme: resolve(theme)
|
|
8655
9556
|
};
|
|
8656
|
-
|
|
8657
|
-
|
|
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 };
|