@qingwu-ui/scroll-fab 0.9.0-beta.1 → 0.9.0-beta.2

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/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @qingwu-ui/scroll-fab
2
+
3
+ [青梧UI](https://github.com/apricotdream/qingwu-ui) 的 **悬浮滚动栏** —— 框架无关,纯 DOM + CSS,零强依赖。
4
+
5
+ - **绕圈翻转**:桌面悬停 / 触屏按住推进描边进度,绕满一圈在「滚动到底部 / 返回顶部」间纯切换(不附带滚动),未绕满移开/松手则描边衰减倒转
6
+ - **点击与描边正交**:轻点恒直接执行当前模式动作;触屏绕满后的释放被消费,避免同一手势既翻转又滚动
7
+ - **Lenis 可插拔**:内置 rAF 缓动,可选接入 [Lenis](https://github.com/darkroomengineering/lenis)(未安装静默回退)
8
+ - **异形形状**:银杏叶等自定义外形,进度描边沿外扩轮廓完全包裹
9
+ - 程序滚动支持 wheel / touch / keydown 打断、懒加载变长追击
10
+ - 移动端一等支持:Pointer Events 单路径、ghost click 抑制、safe-area 定位、44px 命中下限
11
+ - 尊重 `prefers-reduced-motion`;window 与自定义容器双支持
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ npm install @qingwu-ui/scroll-fab
17
+ # 可选:启用 Lenis 平滑滚动
18
+ npm install lenis
19
+ ```
20
+
21
+ ## 使用
22
+
23
+ ```ts
24
+ import { ScrollFab } from "@qingwu-ui/scroll-fab";
25
+ import "@qingwu-ui/scroll-fab/style.css";
26
+
27
+ // 零配置:右下角圆形悬浮栏
28
+ const fab = new ScrollFab();
29
+
30
+ // 销毁
31
+ fab.destroy();
32
+ ```
33
+
34
+ ### 接入 Lenis
35
+
36
+ ```ts
37
+ // ① 自建实例(仅驱动按钮程序滚动,不劫持页面滚轮)
38
+ new ScrollFab({
39
+ lenis: true,
40
+ onLenisReady: (lenis) => console.log("Lenis 就绪", lenis),
41
+ });
42
+
43
+ // ② 透传 Lenis 配置(target 容器自动映射为 wrapper)
44
+ new ScrollFab({ lenis: { duration: 1.1 } });
45
+
46
+ // ③ 复用宿主已有的 Lenis 实例(组件永不销毁它)
47
+ const lenis = new Lenis();
48
+ new ScrollFab({ lenis });
49
+ ```
50
+
51
+ 未安装 `lenis` 时自动回退内置 rAF,无任何报错。
52
+
53
+ ### 异形形状(银杏叶等)
54
+
55
+ 进度要「完全包裹」形状必须用双 path:`fill` 是叶片实体,`outline` 是外扩一圈的闭合轮廓(SVG 描边居中于路径,单 path 会有一半笔画压进形状内部)。
56
+
57
+ ```ts
58
+ const LEAF = "M24 46 L23.2 34 C16 33 9 28 7 20 Z /* …完整 path d… */";
59
+
60
+ new ScrollFab({
61
+ shape: {
62
+ fill: LEAF,
63
+ outline: LEAF, // 与 fill 同形
64
+ outlineTransform: "translate(24 24) scale(1.13) translate(-24 -24)",
65
+ },
66
+ });
67
+ ```
68
+
69
+ 异形模式下圆形外壳自动退场(背景/边框/圆角),阴影改用 `drop-shadow` 跟随叶片真实形状;命中区仍为矩形以保证 44px 触控目标。
70
+
71
+ ## API
72
+
73
+ ```ts
74
+ new ScrollFab(options?: ScrollFabOptions): ScrollFab
75
+
76
+ interface ScrollFabOptions {
77
+ target?: HTMLElement | null; // 滚动容器,默认 window
78
+ size?: number; // 直径 px,默认 48(最小 44)
79
+ ringDuration?: number; // 绕圈时长 ms,默认 800
80
+ decayDuration?: number; // 衰减倒转 ms,默认 300
81
+ cancelThreshold?: number; // 触屏位移放弃阈值 px,默认 10
82
+ content?: string | HTMLElement; // 中心内容,默认方向箭头
83
+ position?: ScrollFabPosition; // 悬浮位置,默认右下 24/24
84
+ zIndex?: number; // 默认 9999
85
+ className?: string;
86
+ animate?: boolean; // 默认 true
87
+ lenis?: boolean | ScrollFabLenisConfig | LenisLike;
88
+ shape?: ScrollFabShape;
89
+ onLenisReady?: (lenis: LenisLike) => void;
90
+ ariaLabelToBottom?: string;
91
+ ariaLabelToTop?: string;
92
+ onModeChange?: (mode: "to-bottom" | "to-top") => void;
93
+ }
94
+ ```
95
+
96
+ 实例方法:`scrollToBottom()` / `scrollToTop()` / `refresh()` / `destroy()`;只读属性 `currentMode`、`el`。
package/dist/index.cjs CHANGED
@@ -74,19 +74,26 @@ S16("<path d=\"m9 18 6-6-6-6\"/>");
74
74
  //#endregion
75
75
  //#region src/scroll-fab.ts
76
76
  /** 青梧UI ScrollFab 悬浮滚动栏:默认“滚动到底部”,桌面悬停/触屏按住绕满描边一圈翻转为“返回顶部”,点击(轻点)直接执行当前模式动作 */
77
+ const SVG_NS = "http://www.w3.org/2000/svg";
77
78
  const RADIUS = 21;
79
+ const FALLBACK_PATH_LEN = 100;
78
80
  const DEFAULT_LABEL_BOTTOM = "滚动到底部(悬停或按住绕圈可切换为返回顶部)";
79
81
  const DEFAULT_LABEL_TOP = "返回顶部(悬停或按住绕圈可切换为滚动到底部)";
80
82
  function easeInOutCubic(p) {
81
83
  return p < .5 ? 4 * p * p * p : 1 - (-2 * p + 2) ** 3 / 2;
82
84
  }
85
+ function isLenisInstance(v) {
86
+ return typeof v === "object" && v !== null && typeof v.scrollTo === "function";
87
+ }
83
88
  var ScrollFab = class {
84
89
  el;
85
90
  ring;
86
91
  track;
92
+ fillPath = null;
87
93
  icon;
88
94
  len;
89
95
  targetEl;
96
+ shape;
90
97
  ringMs;
91
98
  decayMs;
92
99
  cancelPx;
@@ -94,6 +101,13 @@ var ScrollFab = class {
94
101
  labelBottom;
95
102
  labelTop;
96
103
  onModeChange;
104
+ onLenisReady;
105
+ /** 外部注入:宿主实例,组件永不销毁 */
106
+ externalLenis = null;
107
+ /** 自建配置:null = 不走 lenis;true 形态也归一成对象 */
108
+ ownLenisConfig = null;
109
+ ownLenis = null;
110
+ ownLenisPromise = null;
97
111
  mode = "to-bottom";
98
112
  progress = 0;
99
113
  hovering = false;
@@ -105,12 +119,18 @@ var ScrollFab = class {
105
119
  ringRaf = null;
106
120
  lastTs = null;
107
121
  scrollRaf = null;
122
+ lenisLoopRaf = null;
123
+ lenisScrolling = false;
124
+ lenisDest = null;
108
125
  flashTimer = null;
109
126
  tapResetTimer = null;
110
127
  touchTap = false;
111
128
  ro = null;
129
+ actionSeq = 0;
130
+ destroyed = false;
112
131
  constructor(options = {}) {
113
132
  this.targetEl = options.target ?? null;
133
+ this.shape = options.shape ?? null;
114
134
  this.ringMs = Math.max(100, Math.min(5e3, options.ringDuration ?? 800));
115
135
  this.decayMs = Math.max(100, options.decayDuration ?? 300);
116
136
  this.cancelPx = Math.max(4, options.cancelThreshold ?? 10);
@@ -118,10 +138,20 @@ var ScrollFab = class {
118
138
  this.labelBottom = options.ariaLabelToBottom ?? DEFAULT_LABEL_BOTTOM;
119
139
  this.labelTop = options.ariaLabelToTop ?? DEFAULT_LABEL_TOP;
120
140
  this.onModeChange = options.onModeChange;
141
+ this.onLenisReady = options.onLenisReady;
142
+ if (isLenisInstance(options.lenis)) this.externalLenis = options.lenis;
143
+ else if (options.lenis != null && options.lenis !== false) {
144
+ const cfg = options.lenis === true ? {} : options.lenis;
145
+ this.ownLenisConfig = {
146
+ smoothWheel: false,
147
+ ...cfg
148
+ };
149
+ }
121
150
  this.el = document.createElement("button");
122
151
  this.el.type = "button";
123
152
  this.el.className = `qsf-root${options.className ? ` ${options.className}` : ""}`;
124
153
  this.el.dataset.mode = this.mode;
154
+ if (this.shape) this.el.dataset.shape = "on";
125
155
  this.el.setAttribute("aria-label", this.labelBottom);
126
156
  this.el.style.setProperty("--qsf-size", `${Math.max(44, options.size ?? 48)}px`);
127
157
  if (options.zIndex != null) this.el.style.setProperty("--qsf-z", String(options.zIndex));
@@ -129,17 +159,32 @@ var ScrollFab = class {
129
159
  if (options.position?.bottom != null) this.el.style.setProperty("--qsf-bottom", `${options.position.bottom}px`);
130
160
  if (options.position?.left != null) this.el.style.setProperty("--qsf-left", `${options.position.left}px`);
131
161
  if (options.position?.top != null) this.el.style.setProperty("--qsf-top", `${options.position.top}px`);
132
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
162
+ const svg = document.createElementNS(SVG_NS, "svg");
133
163
  svg.setAttribute("class", "qsf-svg");
134
- svg.setAttribute("viewBox", "0 0 48 48");
164
+ svg.setAttribute("viewBox", this.shape?.viewBox ?? "0 0 48 48");
135
165
  svg.setAttribute("aria-hidden", "true");
136
- this.track = this.makeCircle("qsf-track");
137
- this.ring = this.makeCircle("qsf-bar");
138
- svg.append(this.track, this.ring);
139
- let len = 2 * Math.PI * RADIUS;
166
+ if (this.shape) {
167
+ this.fillPath = document.createElementNS(SVG_NS, "path");
168
+ this.fillPath.setAttribute("class", "qsf-shape-fill");
169
+ this.fillPath.setAttribute("d", this.shape.fill);
170
+ this.track = this.makePath("qsf-track", this.shape.outline);
171
+ this.ring = this.makePath("qsf-bar", this.shape.outline);
172
+ if (this.shape.outlineTransform) {
173
+ this.track.setAttribute("transform", this.shape.outlineTransform);
174
+ this.ring.setAttribute("transform", this.shape.outlineTransform);
175
+ }
176
+ svg.append(this.fillPath, this.track, this.ring);
177
+ } else {
178
+ this.track = this.makeCircle("qsf-track");
179
+ this.ring = this.makeCircle("qsf-bar");
180
+ svg.append(this.track, this.ring);
181
+ }
182
+ let len;
183
+ if (this.shape) len = FALLBACK_PATH_LEN;
184
+ else len = 2 * Math.PI * RADIUS;
140
185
  try {
141
186
  const measured = this.ring.getTotalLength();
142
- if (Number.isFinite(measured)) len = measured;
187
+ if (Number.isFinite(measured) && measured > 0) len = measured;
143
188
  } catch {}
144
189
  this.len = len;
145
190
  this.ring.style.strokeDasharray = String(this.len);
@@ -180,9 +225,10 @@ var ScrollFab = class {
180
225
  this.animateScroll("bottom");
181
226
  }
182
227
  scrollToTop() {
183
- this.scrollTo(0);
228
+ this.animateScroll("top");
184
229
  }
185
230
  destroy() {
231
+ this.destroyed = true;
186
232
  this.el.removeEventListener("pointerenter", this.onEnter);
187
233
  this.el.removeEventListener("pointerleave", this.onLeave);
188
234
  this.el.removeEventListener("pointerdown", this.onDown);
@@ -195,6 +241,9 @@ var ScrollFab = class {
195
241
  (this.targetEl ?? window).removeEventListener("scroll", this.onRecheck);
196
242
  this.ro?.disconnect();
197
243
  this.cancelScroll();
244
+ this.stopLenisLoop();
245
+ this.ownLenis?.destroy?.();
246
+ this.ownLenis = null;
198
247
  if (this.ringRaf != null) cancelAnimationFrame(this.ringRaf);
199
248
  this.ringRaf = null;
200
249
  if (this.flashTimer != null) clearTimeout(this.flashTimer);
@@ -202,13 +251,19 @@ var ScrollFab = class {
202
251
  this.el.remove();
203
252
  }
204
253
  makeCircle(cls) {
205
- const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
254
+ const c = document.createElementNS(SVG_NS, "circle");
206
255
  c.setAttribute("class", cls);
207
256
  c.setAttribute("cx", "24");
208
257
  c.setAttribute("cy", "24");
209
258
  c.setAttribute("r", String(RADIUS));
210
259
  return c;
211
260
  }
261
+ makePath(cls, d) {
262
+ const p = document.createElementNS(SVG_NS, "path");
263
+ p.setAttribute("class", cls);
264
+ p.setAttribute("d", d);
265
+ return p;
266
+ }
212
267
  onEnter = (e) => {
213
268
  if (e.pointerType !== "mouse") return;
214
269
  this.hovering = true;
@@ -322,12 +377,110 @@ var ScrollFab = class {
322
377
  this.animateScroll(this.mode === "to-bottom" ? "bottom" : "top");
323
378
  }
324
379
  animateScroll(dest) {
380
+ ++this.actionSeq;
325
381
  this.cancelScroll();
326
382
  if (!this.animate || this.reducedMotion()) {
327
383
  this.setScrollPos(this.targetPos(dest));
328
384
  return;
329
385
  }
330
- this.attachInterrupt();
386
+ if (this.externalLenis) {
387
+ this.scrollWithLenis(this.externalLenis, dest, false);
388
+ return;
389
+ }
390
+ if (this.ownLenisConfig) {
391
+ const seq = ++this.actionSeq;
392
+ this.resolveOwnLenis().then((lenis) => {
393
+ if (this.destroyed || seq !== this.actionSeq) return;
394
+ if (lenis) this.scrollWithLenis(lenis, dest, true);
395
+ else this.animateRaf(dest);
396
+ });
397
+ return;
398
+ }
399
+ this.animateRaf(dest);
400
+ }
401
+ /** 首次点击时动态 import lenis 并自建实例;未安装(或加载失败)→ null 永久回退 rAF */
402
+ resolveOwnLenis() {
403
+ if (this.ownLenis) return Promise.resolve(this.ownLenis);
404
+ if (this.ownLenisPromise) return this.ownLenisPromise;
405
+ this.ownLenisPromise = import("lenis").then((mod) => {
406
+ const LenisCtor = mod.default ?? mod;
407
+ const cfg = { ...this.ownLenisConfig };
408
+ if (this.targetEl) cfg.wrapper = this.targetEl;
409
+ this.ownLenis = new LenisCtor(cfg);
410
+ this.onLenisReady?.(this.ownLenis);
411
+ return this.ownLenis;
412
+ }).catch(() => {
413
+ this.ownLenisConfig = null;
414
+ this.ownLenisPromise = null;
415
+ return null;
416
+ });
417
+ return this.ownLenisPromise;
418
+ }
419
+ /**
420
+ * 用 Lenis 执行程序滚动。
421
+ * 自建实例(own):组件自跑 raf loop(Lenis 需外部驱动 raf),保留 wheel/touch/keydown 打断与到底追击;
422
+ * 外部实例:宿主自带 raf loop 与手势处理,组件只发 scrollTo,不追击不打断。
423
+ */
424
+ scrollWithLenis(lenis, dest, own) {
425
+ const immediate = !this.animate || this.reducedMotion();
426
+ const target = this.targetPos(dest);
427
+ this.lenisDest = dest;
428
+ if (own) {
429
+ lenis.start?.();
430
+ lenis.scrollTo(target, {
431
+ immediate,
432
+ onComplete: () => this.endLenisScroll()
433
+ });
434
+ if (!immediate) {
435
+ this.lenisScrolling = true;
436
+ this.attachInterrupt();
437
+ this.startLenisLoop(lenis, dest);
438
+ } else this.endLenisScroll();
439
+ } else {
440
+ lenis.scrollTo(target, { immediate });
441
+ this.lenisDest = null;
442
+ }
443
+ }
444
+ /** 自建实例的 raf 驱动 + 懒加载到底追击(对齐内置 rAF 语义) */
445
+ startLenisLoop(lenis, dest) {
446
+ let lastEnd = this.targetPos(dest);
447
+ const loop = (time) => {
448
+ if (!this.lenisScrolling) return;
449
+ if (dest === "bottom") {
450
+ const live = this.targetPos("bottom");
451
+ if (Math.abs(live - lastEnd) > 2) {
452
+ lastEnd = live;
453
+ lenis.scrollTo(live, {
454
+ force: true,
455
+ onComplete: () => this.endLenisScroll()
456
+ });
457
+ }
458
+ }
459
+ lenis.raf?.(time);
460
+ this.lenisLoopRaf = requestAnimationFrame(loop);
461
+ };
462
+ this.lenisLoopRaf = requestAnimationFrame(loop);
463
+ }
464
+ stopLenisLoop() {
465
+ if (this.lenisLoopRaf != null) {
466
+ cancelAnimationFrame(this.lenisLoopRaf);
467
+ this.lenisLoopRaf = null;
468
+ }
469
+ }
470
+ /** 打断自建 Lenis 程序滚动:stop 冻结动画(下次发起前 start 复位) */
471
+ onLenisInterrupt = () => {
472
+ if (!this.lenisScrolling) return;
473
+ this.ownLenis?.stop?.();
474
+ this.endLenisScroll();
475
+ };
476
+ endLenisScroll() {
477
+ this.lenisScrolling = false;
478
+ this.lenisDest = null;
479
+ this.stopLenisLoop();
480
+ this.detachInterrupt();
481
+ }
482
+ animateRaf(dest) {
483
+ this.attachRafInterrupt();
331
484
  let init = false;
332
485
  let start = 0;
333
486
  let end = 0;
@@ -354,7 +507,7 @@ var ScrollFab = class {
354
507
  if (p < 1) this.scrollRaf = requestAnimationFrame(step);
355
508
  else {
356
509
  this.scrollRaf = null;
357
- this.detachInterrupt();
510
+ this.detachRafInterrupt();
358
511
  }
359
512
  };
360
513
  this.scrollRaf = requestAnimationFrame(step);
@@ -368,15 +521,33 @@ var ScrollFab = class {
368
521
  interruptSource() {
369
522
  return this.targetEl ?? window;
370
523
  }
524
+ /** 自建 Lenis 路线的打断监听 */
371
525
  attachInterrupt() {
372
526
  const src = this.interruptSource();
373
527
  for (const ev of [
374
528
  "wheel",
375
529
  "touchstart",
376
530
  "keydown"
377
- ]) src.addEventListener(ev, this.onInterrupt, { passive: true });
531
+ ]) src.addEventListener(ev, this.onLenisInterrupt, { passive: true });
378
532
  }
379
533
  detachInterrupt() {
534
+ const src = this.interruptSource();
535
+ for (const ev of [
536
+ "wheel",
537
+ "touchstart",
538
+ "keydown"
539
+ ]) src.removeEventListener(ev, this.onLenisInterrupt);
540
+ }
541
+ /** 内置 rAF 路线的打断监听 */
542
+ attachRafInterrupt() {
543
+ const src = this.interruptSource();
544
+ for (const ev of [
545
+ "wheel",
546
+ "touchstart",
547
+ "keydown"
548
+ ]) src.addEventListener(ev, this.onInterrupt, { passive: true });
549
+ }
550
+ detachRafInterrupt() {
380
551
  const src = this.interruptSource();
381
552
  for (const ev of [
382
553
  "wheel",
@@ -389,7 +560,16 @@ var ScrollFab = class {
389
560
  cancelAnimationFrame(this.scrollRaf);
390
561
  this.scrollRaf = null;
391
562
  }
392
- this.detachInterrupt();
563
+ if (this.lenisScrolling) this.endLenisScroll();
564
+ const src = this.interruptSource();
565
+ for (const ev of [
566
+ "wheel",
567
+ "touchstart",
568
+ "keydown"
569
+ ]) {
570
+ src.removeEventListener(ev, this.onInterrupt);
571
+ src.removeEventListener(ev, this.onLenisInterrupt);
572
+ }
393
573
  }
394
574
  maxScroll() {
395
575
  if (this.targetEl) return Math.max(0, this.targetEl.scrollHeight - this.targetEl.clientHeight);
@@ -406,9 +586,6 @@ var ScrollFab = class {
406
586
  if (this.targetEl) this.targetEl.scrollTop = v;
407
587
  else window.scrollTo(0, v);
408
588
  }
409
- scrollTo(v) {
410
- this.animateScroll(v === 0 ? "top" : "bottom");
411
- }
412
589
  onRecheck = () => {
413
590
  this.syncVisible();
414
591
  };
package/dist/index.d.cts CHANGED
@@ -7,6 +7,46 @@ interface ScrollFabPosition {
7
7
  bottom?: number;
8
8
  left?: number;
9
9
  }
10
+ /** Lenis 实例的最小结构性类型(组件不硬依赖 lenis 包,按结构识别) */
11
+ interface LenisLike {
12
+ scrollTo: (target: number | string | HTMLElement, options?: Record<string, unknown>) => void;
13
+ raf?: (time: number) => void;
14
+ destroy?: () => void;
15
+ start?: () => void;
16
+ stop?: () => void;
17
+ on?: (event: string, callback: (...args: unknown[]) => void) => void;
18
+ off?: (event: string, callback: (...args: unknown[]) => void) => void;
19
+ isScrolling?: boolean;
20
+ }
21
+ /** 内部自建 Lenis 时的构造配置(lenis 官方选项子集,允许透传其他字段) */
22
+ interface ScrollFabLenisConfig {
23
+ wrapper?: HTMLElement | Window;
24
+ content?: HTMLElement;
25
+ duration?: number;
26
+ easing?: (t: number) => number;
27
+ /** 默认强制 false:自建实例不劫持用户滚轮,只驱动按钮触发的程序滚动;显式置 true 则接管全局 */
28
+ smoothWheel?: boolean;
29
+ smoothTouch?: boolean;
30
+ wheelMultiplier?: number;
31
+ touchMultiplier?: number;
32
+ [key: string]: unknown;
33
+ }
34
+ /**
35
+ * 异形按钮形状(如银杏叶)。
36
+ * 双 path 才能保证进度描边「完全包裹」:fill 是叶片实体,outline 是外扩一圈的闭合轮廓,
37
+ * SVG 描边居中于路径,单条 path 必有一半笔画压进形状内部。
38
+ * outline 的起点约定在 12 点钟方向(描边顺时针推进)。
39
+ */
40
+ interface ScrollFabShape {
41
+ /** SVG viewBox,默认 "0 0 48 48";自定义 path 建议按 48×48 绘制 */
42
+ viewBox?: string;
43
+ /** 形状实体 path d(被填充) */
44
+ fill: string;
45
+ /** 外扩进度轮廓 path d(track + 进度描边沿它走);可与 fill 同形再配 outlineTransform 外扩 */
46
+ outline: string;
47
+ /** 作用于 outline path 的 SVG transform,如 "translate(24 24) scale(1.1) translate(-24 -24)" */
48
+ outlineTransform?: string;
49
+ }
10
50
  interface ScrollFabOptions {
11
51
  /** 滚动容器,null(默认)为 window */
12
52
  target?: HTMLElement | null;
@@ -28,6 +68,18 @@ interface ScrollFabOptions {
28
68
  className?: string;
29
69
  /** 动画开关,关闭或 prefers-reduced-motion 时程序滚动瞬时到位,默认 true */
30
70
  animate?: boolean;
71
+ /**
72
+ * Lenis 平滑滚动(lenis 为 optional peerDependency,未安装静默回退内置 rAF):
73
+ * - 传 Lenis 实例:复用宿主实例,组件不负责销毁;
74
+ * - true:点击时动态 import lenis 并自建实例(仅驱动按钮程序滚动,不劫持滚轮);
75
+ * - 传配置对象:自建实例并透传配置(target 容器自动映射为 wrapper);
76
+ * - false / 缺省:内置 rAF 缓动。
77
+ */
78
+ lenis?: boolean | ScrollFabLenisConfig | LenisLike;
79
+ /** 异形形状(银杏叶等);传入后圆形外壳消失,进度沿外扩轮廓包裹 */
80
+ shape?: ScrollFabShape;
81
+ /** 自建 Lenis 实例就绪回调(外部实例注入时不触发),与 onEditorReady 模式对齐 */
82
+ onLenisReady?: (lenis: LenisLike) => void;
31
83
  /** 模式①(滚动到底部)无障碍标签 */
32
84
  ariaLabelToBottom?: string;
33
85
  /** 模式②(返回顶部)无障碍标签 */
@@ -39,11 +91,13 @@ interface ScrollFabOptions {
39
91
  //#region src/scroll-fab.d.ts
40
92
  declare class ScrollFab {
41
93
  readonly el: HTMLButtonElement;
42
- private readonly ring;
43
- private readonly track;
94
+ private ring;
95
+ private track;
96
+ private fillPath;
44
97
  private readonly icon;
45
- private readonly len;
98
+ private len;
46
99
  private readonly targetEl;
100
+ private readonly shape;
47
101
  private readonly ringMs;
48
102
  private readonly decayMs;
49
103
  private readonly cancelPx;
@@ -51,6 +105,13 @@ declare class ScrollFab {
51
105
  private readonly labelBottom;
52
106
  private readonly labelTop;
53
107
  private readonly onModeChange?;
108
+ private readonly onLenisReady?;
109
+ /** 外部注入:宿主实例,组件永不销毁 */
110
+ private externalLenis;
111
+ /** 自建配置:null = 不走 lenis;true 形态也归一成对象 */
112
+ private ownLenisConfig;
113
+ private ownLenis;
114
+ private ownLenisPromise;
54
115
  private mode;
55
116
  private progress;
56
117
  private hovering;
@@ -62,10 +123,15 @@ declare class ScrollFab {
62
123
  private ringRaf;
63
124
  private lastTs;
64
125
  private scrollRaf;
126
+ private lenisLoopRaf;
127
+ private lenisScrolling;
128
+ private lenisDest;
65
129
  private flashTimer;
66
130
  private tapResetTimer;
67
131
  private touchTap;
68
132
  private ro;
133
+ private actionSeq;
134
+ private destroyed;
69
135
  constructor(options?: ScrollFabOptions);
70
136
  /** 当前模式 */
71
137
  get currentMode(): ScrollFabMode;
@@ -75,6 +141,7 @@ declare class ScrollFab {
75
141
  scrollToTop(): void;
76
142
  destroy(): void;
77
143
  private makeCircle;
144
+ private makePath;
78
145
  private onEnter;
79
146
  private onLeave;
80
147
  private onDown;
@@ -92,20 +159,38 @@ declare class ScrollFab {
92
159
  private setMode;
93
160
  private doAction;
94
161
  private animateScroll;
162
+ /** 首次点击时动态 import lenis 并自建实例;未安装(或加载失败)→ null 永久回退 rAF */
163
+ private resolveOwnLenis;
164
+ /**
165
+ * 用 Lenis 执行程序滚动。
166
+ * 自建实例(own):组件自跑 raf loop(Lenis 需外部驱动 raf),保留 wheel/touch/keydown 打断与到底追击;
167
+ * 外部实例:宿主自带 raf loop 与手势处理,组件只发 scrollTo,不追击不打断。
168
+ */
169
+ private scrollWithLenis;
170
+ /** 自建实例的 raf 驱动 + 懒加载到底追击(对齐内置 rAF 语义) */
171
+ private startLenisLoop;
172
+ private stopLenisLoop;
173
+ /** 打断自建 Lenis 程序滚动:stop 冻结动画(下次发起前 start 复位) */
174
+ private onLenisInterrupt;
175
+ private endLenisScroll;
176
+ private animateRaf;
95
177
  private reducedMotion;
96
178
  private onInterrupt;
97
179
  private interruptSource;
180
+ /** 自建 Lenis 路线的打断监听 */
98
181
  private attachInterrupt;
99
182
  private detachInterrupt;
183
+ /** 内置 rAF 路线的打断监听 */
184
+ private attachRafInterrupt;
185
+ private detachRafInterrupt;
100
186
  private cancelScroll;
101
187
  private maxScroll;
102
188
  private targetPos;
103
189
  private scrollPos;
104
190
  private setScrollPos;
105
- private scrollTo;
106
191
  private onRecheck;
107
192
  private syncVisible;
108
193
  }
109
194
  //#endregion
110
- export { ScrollFab, type ScrollFabMode, type ScrollFabOptions, type ScrollFabPosition };
195
+ export { type LenisLike, ScrollFab, type ScrollFabLenisConfig, type ScrollFabMode, type ScrollFabOptions, type ScrollFabPosition, type ScrollFabShape };
111
196
  //# sourceMappingURL=index.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/scroll-fab.ts"],"mappings":";KAAY;;UAGK;EACf;EACA;EACA;EACA;;UAGe;;EAEf,SAAS;;EAET;;EAEA;;EAEA;;EAEA;;EAEA,mBAAmB;;EAEnB,WAAW;;EAEX;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,gBAAgB,MAAM;;;;cCvBX;WACF,IAAI;mBAEI;mBACA;mBACA;mBACA;mBAEA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;UAET;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;EAER,YAAY,UAAS;;MA4EjB,eAAe;;EAKnB;EAIA;EAIA;EAIA;UAoBQ;UAeA;UAMA;UAMA;UAeA;UAUA;UAeA;UAOA;UAUA;UAQA;UAIA;UAMA;;UAoBA;UAYA;UAOA;UAUA;UAIA;UAwCA;UAMA;UAIA;UAIA;UAMA;UAMA;UAUA;UAKA;UAIA;UAKA;UAKA;UAIA;UAIA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/scroll-fab.ts"],"mappings":";KAAY;;UAGK;EACf;EACA;EACA;EACA;;;UAIe;EACf,WACE,0BAA0B,aAC1B,UAAU;EAEZ,OAAO;EACP;EACA;EACA;EACA,MAAM,eAAe,cAAc;EACnC,OAAO,eAAe,cAAc;EACpC;;;UAIe;EACf,UAAU,cAAc;EACxB,UAAU;EACV;EACA,UAAU;;EAEV;EACA;EACA;EACA;GACC;;;;;;;;UASc;;EAEf;;EAEA;;EAEA;;EAEA;;UAGe;;EAEf,SAAS;;EAET;;EAEA;;EAEA;;EAEA;;EAEA,mBAAmB;;EAEnB,WAAW;;EAEX;;EAEA;;EAEA;;;;;;;;EAQA,kBAAkB,uBAAuB;;EAEzC,QAAQ;;EAER,gBAAgB,OAAO;;EAEvB;;EAEA;;EAEA,gBAAgB,MAAM;;;;cCjEX;WACF,IAAI;UAEL;UACA;UACA;mBACS;UACT;mBAES;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;mBACA;;UAGT;;UAEA;UACA;UACA;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;EAER,YAAY,UAAS;;MA2GjB,eAAe;;EAKnB;EAIA;EAIA;EAIA;UAyBQ;UASA;UAaA;UAMA;UAMA;UAeA;UAUA;UAeA;UAOA;UAUA;UAQA;UAIA;UAMA;;UAoBA;UAYA;UAOA;UAUA;UAIA;;UAyBA;;;;;;UA6BA;;UA6BA;UAqBA;;UAQA;UAMA;UASA;UAmCA;UAMA;UAIA;;UAKA;UAMA;;UAOA;UAMA;UAMA;UAeA;UAKA;UAIA;UAKA;UAKA;UAIA"}