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