@zhang_jifan/fanui 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/ACCESSIBILITY.md +61 -0
  2. package/CHANGELOG.md +112 -0
  3. package/COMMERCIAL-LICENSE.md +71 -0
  4. package/DESIGN-SYSTEM.md +294 -0
  5. package/LICENSE +661 -0
  6. package/README.md +348 -0
  7. package/USAGE.md +1926 -0
  8. package/_index.scss +20 -0
  9. package/dist/fanui.cjs +15 -0
  10. package/dist/fanui.css +28209 -0
  11. package/dist/fanui.d.ts +353 -0
  12. package/dist/fanui.esm.js +71 -0
  13. package/dist/fanui.js +4782 -0
  14. package/dist/fanui.min.css +1 -0
  15. package/dist/fanui.mjs +71 -0
  16. package/dist/modules/fanui-advanced.css +830 -0
  17. package/dist/modules/fanui-advanced.min.css +129 -0
  18. package/dist/modules/fanui-alert.css +575 -0
  19. package/dist/modules/fanui-alert.min.css +122 -0
  20. package/dist/modules/fanui-badge.css +426 -0
  21. package/dist/modules/fanui-badge.min.css +77 -0
  22. package/dist/modules/fanui-base.css +1394 -0
  23. package/dist/modules/fanui-base.min.css +236 -0
  24. package/dist/modules/fanui-blog.css +762 -0
  25. package/dist/modules/fanui-blog.min.css +108 -0
  26. package/dist/modules/fanui-button.css +526 -0
  27. package/dist/modules/fanui-button.min.css +56 -0
  28. package/dist/modules/fanui-card.css +291 -0
  29. package/dist/modules/fanui-card.min.css +52 -0
  30. package/dist/modules/fanui-choice.css +539 -0
  31. package/dist/modules/fanui-choice.min.css +83 -0
  32. package/dist/modules/fanui-dashboard.css +1057 -0
  33. package/dist/modules/fanui-dashboard.min.css +160 -0
  34. package/dist/modules/fanui-data.css +687 -0
  35. package/dist/modules/fanui-data.min.css +125 -0
  36. package/dist/modules/fanui-feedback.css +432 -0
  37. package/dist/modules/fanui-feedback.min.css +79 -0
  38. package/dist/modules/fanui-form.css +779 -0
  39. package/dist/modules/fanui-form.min.css +123 -0
  40. package/dist/modules/fanui-glass.css +497 -0
  41. package/dist/modules/fanui-glass.min.css +46 -0
  42. package/dist/modules/fanui-input-plus.css +799 -0
  43. package/dist/modules/fanui-input-plus.min.css +124 -0
  44. package/dist/modules/fanui-interactive.css +960 -0
  45. package/dist/modules/fanui-interactive.min.css +138 -0
  46. package/dist/modules/fanui-layout.css +4399 -0
  47. package/dist/modules/fanui-layout.min.css +1172 -0
  48. package/dist/modules/fanui-marketing.css +1125 -0
  49. package/dist/modules/fanui-marketing.min.css +144 -0
  50. package/dist/modules/fanui-nav.css +458 -0
  51. package/dist/modules/fanui-nav.min.css +76 -0
  52. package/dist/modules/fanui-overlay.css +547 -0
  53. package/dist/modules/fanui-overlay.min.css +91 -0
  54. package/dist/modules/fanui-table.css +290 -0
  55. package/dist/modules/fanui-table.min.css +62 -0
  56. package/dist/modules/fanui-tabs.css +333 -0
  57. package/dist/modules/fanui-tabs.min.css +58 -0
  58. package/dist/modules/fanui-themes.css +2728 -0
  59. package/dist/modules/fanui-themes.min.css +40 -0
  60. package/dist/modules/fanui-tokens.css +587 -0
  61. package/dist/modules/fanui-tokens.min.css +3 -0
  62. package/dist/modules/fanui-utilities.css +6557 -0
  63. package/dist/modules/fanui-utilities.min.css +1762 -0
  64. package/dist/modules/sizes.json +103 -0
  65. package/docs/MIGRATION-v2.md +179 -0
  66. package/examples/01-script-tag.html +149 -0
  67. package/examples/02-react/README.md +39 -0
  68. package/examples/02-react/index.html +12 -0
  69. package/examples/02-react/package.json +20 -0
  70. package/examples/02-react/src/App.jsx +108 -0
  71. package/examples/02-react/src/main.jsx +13 -0
  72. package/examples/02-react/src/useFanUI.js +87 -0
  73. package/examples/02-react/vite.config.js +6 -0
  74. package/examples/03-vue3/README.md +67 -0
  75. package/examples/03-vue3/index.html +12 -0
  76. package/examples/03-vue3/package.json +19 -0
  77. package/examples/03-vue3/src/App.vue +93 -0
  78. package/examples/03-vue3/src/main.js +8 -0
  79. package/examples/03-vue3/src/useFanUI.js +85 -0
  80. package/examples/03-vue3/vite.config.js +6 -0
  81. package/examples/04-vue2-cdn.html +93 -0
  82. package/examples/README.md +34 -0
  83. package/package.json +127 -0
  84. package/src/_functions.scss +319 -0
  85. package/src/_mixins.scss +1202 -0
  86. package/src/_tokens.scss +180 -0
  87. package/src/_variables.scss +491 -0
  88. package/src/base/_animations.scss +210 -0
  89. package/src/base/_reset.scss +359 -0
  90. package/src/base/_rtl.scss +133 -0
  91. package/src/base/_typography.scss +353 -0
  92. package/src/components/_advanced.scss +763 -0
  93. package/src/components/_alert.scss +322 -0
  94. package/src/components/_badge.scss +279 -0
  95. package/src/components/_blog.scss +693 -0
  96. package/src/components/_button.scss +351 -0
  97. package/src/components/_capabilities.scss +463 -0
  98. package/src/components/_card.scss +281 -0
  99. package/src/components/_choice.scss +444 -0
  100. package/src/components/_dashboard.scss +950 -0
  101. package/src/components/_data.scss +618 -0
  102. package/src/components/_feedback.scss +403 -0
  103. package/src/components/_form.scss +408 -0
  104. package/src/components/_glass.scss +374 -0
  105. package/src/components/_input-plus.scss +730 -0
  106. package/src/components/_interactive.scss +885 -0
  107. package/src/components/_marketing.scss +982 -0
  108. package/src/components/_nav.scss +480 -0
  109. package/src/components/_overlay.scss +517 -0
  110. package/src/components/_table.scss +276 -0
  111. package/src/components/_tabs.scss +338 -0
  112. package/src/fanui.d.ts +353 -0
  113. package/src/fanui.js +4782 -0
  114. package/src/fanui.scss +79 -0
  115. package/src/glass/_switches.scss +36 -0
  116. package/src/glass/_tokens.scss +259 -0
  117. package/src/layout/_container.scss +111 -0
  118. package/src/layout/_flex.scss +164 -0
  119. package/src/layout/_grid.scss +180 -0
  120. package/src/themes/_accents.scss +201 -0
  121. package/src/themes/_palettes.scss +180 -0
  122. package/src/utilities/_borders.scss +104 -0
  123. package/src/utilities/_display.scss +145 -0
  124. package/src/utilities/_effects.scss +210 -0
  125. package/src/utilities/_sizing.scss +123 -0
  126. package/src/utilities/_spacing.scss +118 -0
  127. package/src/utilities/_texture.scss +323 -0
@@ -0,0 +1,463 @@
1
+ // =============================================================================
2
+ // fanUI · 能力补齐组件(Capabilities)
3
+ // -----------------------------------------------------------------------------
4
+ // 补齐主流 UI 库普遍具备、此前缺失的 7 项能力:
5
+ // .fanui-virtual-list 虚拟滚动列表(JS: FanUI.virtualList)
6
+ // .fanui-lightbox 图片预览 / 灯箱(JS: FanUI.lightbox)
7
+ // .fanui-watermark 水印(JS: FanUI.watermark)
8
+ // .fanui-tour 引导 Tour(JS: FanUI.tour)
9
+ // .fanui-splitter 可拖拽分割面板(JS: FanUI.splitter)
10
+ // .fanui-affix 吸顶容器(JS: FanUI.affix)
11
+ // .fanui-backtop 回到顶部(JS: FanUI.backTop)
12
+ //
13
+ // 约定:
14
+ // · 全部行内轴属性使用**逻辑属性**(inline-start/end),[dir="rtl"] 自动镜像;
15
+ // · 所有动效在 prefers-reduced-motion: reduce 下降级为「无过渡」;
16
+ // · 交互组件保留原生键盘可达性(role / tabindex / aria-*)。
17
+ // =============================================================================
18
+
19
+ @use "../functions" as fn;
20
+ @use "../variables" as v;
21
+ @use "../mixins" as mx;
22
+
23
+ // -----------------------------------------------------------------------------
24
+ // 1. 虚拟滚动列表 virtual-list
25
+ // 结构:容器(滚动) > __inner(撑满总高) > __item(绝对定位 + translateY)
26
+ // -----------------------------------------------------------------------------
27
+
28
+ .fanui-virtual-list {
29
+ position: relative;
30
+ overflow: auto;
31
+ overscroll-behavior: contain;
32
+ height: fn.fanui-var("vl-height", 320px);
33
+ max-height: 100%;
34
+ border: 1px solid fn.fanui-var("color-border");
35
+ border-radius: fn.fanui-var("radius-md");
36
+ background-color: fn.fanui-var("color-surface");
37
+ contain: strict;
38
+
39
+ &__inner { position: relative; width: 100%; }
40
+
41
+ &__item {
42
+ position: absolute;
43
+ inset-inline: 0;
44
+ top: 0;
45
+ display: flex;
46
+ align-items: center;
47
+ gap: fn.fanui-var("space-3");
48
+ padding: fn.fanui-var("space-2") fn.fanui-var("space-4");
49
+ box-sizing: border-box;
50
+ border-bottom: 1px solid fn.fanui-var("color-border-subtle");
51
+ color: fn.fanui-var("color-text");
52
+ will-change: transform;
53
+
54
+ &:hover { background-color: fn.fanui-var("color-surface-raised"); }
55
+ &.is-selected {
56
+ background-color: fn.fanui-var("primary-soft");
57
+ color: fn.fanui-var("primary-ink");
58
+ font-weight: 600;
59
+ }
60
+ }
61
+
62
+ // 原始数据源:**默认可见**,作为「SSR 直出 / 未启用 JS」时的可读降级;
63
+ // JS 接管(容器被加上 .is-virtualized)后才隐藏,改由 __inner 承担渲染。
64
+ // ⚠️ 不要写成无条件的 display:none —— 那会让服务端直出的行全部不可见,
65
+ // 与本组件「渐进增强」的设计意图相反。
66
+ &__source { display: block; }
67
+ &.is-virtualized &__source { display: none; }
68
+
69
+ &__empty {
70
+ display: grid;
71
+ place-items: center;
72
+ height: 100%;
73
+ color: fn.fanui-var("color-text-muted");
74
+ }
75
+
76
+ // 首屏未启用 JS 的降级:正常流式展示(SSR 直出即可读,不再锁死 320px 高 + 滚动容器)
77
+ &:not(.is-virtualized) {
78
+ height: auto;
79
+ max-height: none;
80
+ contain: none;
81
+ }
82
+ &:not(.is-virtualized) &__inner { height: auto; }
83
+ &:not(.is-virtualized) &__item { position: static; transform: none; }
84
+ }
85
+
86
+ // -----------------------------------------------------------------------------
87
+ // 2. 图片预览 / 灯箱 lightbox
88
+ // -----------------------------------------------------------------------------
89
+
90
+ .fanui-lightbox {
91
+ position: fixed;
92
+ inset: 0;
93
+ z-index: fn.fanui-var("z-modal", v.$fanui-zindex-modal);
94
+ display: flex;
95
+ flex-direction: column;
96
+ align-items: center;
97
+ justify-content: center;
98
+
99
+ &__backdrop {
100
+ position: absolute;
101
+ inset: 0;
102
+ background-color: fn.fanui-var("lb-backdrop", rgba(2, 6, 23, 0.88));
103
+ backdrop-filter: blur(6px);
104
+ animation: fanui-fade-in fn.fanui-var("duration-base") fn.fanui-var("ease-out") both;
105
+ }
106
+
107
+ &__stage {
108
+ position: relative;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ width: 100%;
113
+ height: 100%;
114
+ overflow: hidden;
115
+ touch-action: none;
116
+ }
117
+
118
+ &__img {
119
+ max-width: 92vw;
120
+ max-height: 84vh;
121
+ border-radius: fn.fanui-var("radius-lg");
122
+ box-shadow: fn.fanui-var("shadow-2xl");
123
+ cursor: grab;
124
+ user-select: none;
125
+ transition: transform fn.fanui-var("duration-base") fn.fanui-var("ease-out");
126
+ animation: fanui-lightbox-in fn.fanui-var("duration-slow") fn.fanui-var("ease-spring") both;
127
+
128
+ &.is-dragging { cursor: grabbing; transition: none; }
129
+ }
130
+
131
+ &__caption {
132
+ position: absolute;
133
+ inset-inline: 0;
134
+ bottom: fn.fanui-var("space-5");
135
+ text-align: center;
136
+ font-size: fn.fanui-var("font-size-sm");
137
+ color: rgba(255, 255, 255, 0.82);
138
+ pointer-events: none;
139
+ }
140
+
141
+ &__counter {
142
+ position: absolute;
143
+ top: fn.fanui-var("space-5");
144
+ inset-inline-start: fn.fanui-var("space-5");
145
+ padding: 2px fn.fanui-var("space-3");
146
+ border-radius: fn.fanui-var("radius-full");
147
+ background-color: rgba(255, 255, 255, 0.14);
148
+ color: #ffffff;
149
+ font-size: fn.fanui-var("font-size-xs");
150
+ font-variant-numeric: tabular-nums;
151
+ }
152
+
153
+ &__toolbar {
154
+ position: absolute;
155
+ top: fn.fanui-var("space-4");
156
+ inset-inline-end: fn.fanui-var("space-4");
157
+ display: flex;
158
+ gap: fn.fanui-var("space-2");
159
+ z-index: 2;
160
+ }
161
+
162
+ &__btn {
163
+ display: grid;
164
+ place-items: center;
165
+ width: 36px;
166
+ height: 36px;
167
+ padding: 0;
168
+ border: 1px solid rgba(255, 255, 255, 0.18);
169
+ border-radius: fn.fanui-var("radius-md");
170
+ background-color: rgba(255, 255, 255, 0.1);
171
+ color: #ffffff;
172
+ font-size: 15px;
173
+ line-height: 1;
174
+ cursor: pointer;
175
+ @include mx.fanui-transition((background-color, border-color, transform), v.$fanui-duration-fast, v.$fanui-ease-out);
176
+
177
+ &:hover { background-color: rgba(255, 255, 255, 0.2); }
178
+ &:active { transform: scale(0.94); }
179
+ &:focus-visible { outline: 2px solid #ffffff; outline-offset: 2px; }
180
+ }
181
+
182
+ &__nav {
183
+ position: absolute;
184
+ top: 50%;
185
+ transform: translateY(-50%);
186
+ z-index: 2;
187
+
188
+ &--prev { inset-inline-start: fn.fanui-var("space-4"); }
189
+ &--next { inset-inline-end: fn.fanui-var("space-4"); }
190
+ }
191
+
192
+ &__thumbstrip {
193
+ position: absolute;
194
+ bottom: fn.fanui-var("space-4");
195
+ display: flex;
196
+ gap: fn.fanui-var("space-2");
197
+ max-width: 90vw;
198
+ padding: fn.fanui-var("space-2");
199
+ overflow-x: auto;
200
+ border-radius: fn.fanui-var("radius-md");
201
+ background-color: rgba(255, 255, 255, 0.08);
202
+
203
+ img {
204
+ width: 48px;
205
+ height: 48px;
206
+ object-fit: cover;
207
+ border-radius: fn.fanui-var("radius-base");
208
+ opacity: 0.55;
209
+ cursor: pointer;
210
+ &.is-active { opacity: 1; outline: 2px solid #ffffff; }
211
+ }
212
+ }
213
+
214
+ // 触发器:任意带 data-fanui-lightbox 的元素
215
+ &-trigger { cursor: zoom-in; }
216
+ }
217
+
218
+ @keyframes fanui-lightbox-in {
219
+ from { opacity: 0; transform: scale(0.94); }
220
+ to { opacity: 1; transform: scale(1); }
221
+ }
222
+
223
+ // -----------------------------------------------------------------------------
224
+ // 3. 水印 watermark
225
+ // -----------------------------------------------------------------------------
226
+
227
+ .fanui-watermark {
228
+ position: absolute;
229
+ inset: 0;
230
+ z-index: fn.fanui-var("wm-z", 1);
231
+ pointer-events: none;
232
+ background-repeat: repeat;
233
+ background-position: 0 0;
234
+
235
+ &__host { position: relative; }
236
+ }
237
+
238
+ // -----------------------------------------------------------------------------
239
+ // 4. 引导 Tour
240
+ // -----------------------------------------------------------------------------
241
+
242
+ .fanui-tour {
243
+ position: fixed;
244
+ inset: 0;
245
+ z-index: fn.fanui-var("z-modal", v.$fanui-zindex-modal);
246
+
247
+ &__mask {
248
+ position: absolute;
249
+ inset: 0;
250
+ background-color: fn.fanui-var("tour-mask", rgba(2, 6, 23, 0.62));
251
+ animation: fanui-fade-in fn.fanui-var("duration-base") fn.fanui-var("ease-out") both;
252
+ }
253
+
254
+ // 聚光灯:靠 4 个方向遮罩或 box-shadow 挖孔,这里用 box-shadow 挖孔(性能更好)
255
+ &__spotlight {
256
+ position: absolute;
257
+ border-radius: fn.fanui-var("radius-md");
258
+ box-shadow: 0 0 0 9999px fn.fanui-var("tour-mask", rgba(2, 6, 23, 0.62));
259
+ transition: all fn.fanui-var("duration-base") fn.fanui-var("ease-out");
260
+ pointer-events: none;
261
+ }
262
+
263
+ &__popover {
264
+ position: absolute;
265
+ width: min(92vw, 340px);
266
+ padding: fn.fanui-var("space-5");
267
+ border: 1px solid fn.fanui-var("color-border");
268
+ border-radius: fn.fanui-var("radius-lg");
269
+ background-color: fn.fanui-var("color-surface");
270
+ box-shadow: fn.fanui-var("shadow-xl");
271
+ color: fn.fanui-var("color-text");
272
+ animation: fanui-tour-in fn.fanui-var("duration-base") fn.fanui-var("ease-spring") both;
273
+ z-index: 1;
274
+ }
275
+
276
+ &__arrow {
277
+ position: absolute;
278
+ width: 10px;
279
+ height: 10px;
280
+ background-color: fn.fanui-var("color-surface");
281
+ border: 1px solid fn.fanui-var("color-border");
282
+ transform: rotate(45deg);
283
+ }
284
+
285
+ &__head { display: flex; align-items: flex-start; gap: fn.fanui-var("space-3"); }
286
+ &__title { flex: 1 1 auto; margin: 0; font-size: fn.fanui-var("font-size-lg"); font-weight: 700; color: fn.fanui-var("color-text-strong"); }
287
+ &__desc { margin: fn.fanui-var("space-2") 0 0; font-size: fn.fanui-var("font-size-sm"); color: fn.fanui-var("color-text-muted"); line-height: 1.7; }
288
+
289
+ &__close {
290
+ flex: 0 0 auto;
291
+ width: 26px;
292
+ height: 26px;
293
+ padding: 0;
294
+ border: 0;
295
+ border-radius: fn.fanui-var("radius-base");
296
+ background: transparent;
297
+ color: fn.fanui-var("color-text-muted");
298
+ cursor: pointer;
299
+ &:hover { background-color: fn.fanui-var("color-surface-raised"); color: fn.fanui-var("color-text"); }
300
+ }
301
+
302
+ &__footer {
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: space-between;
306
+ gap: fn.fanui-var("space-3");
307
+ margin-top: fn.fanui-var("space-4");
308
+ }
309
+
310
+ &__dots { display: flex; gap: 6px; align-items: center; }
311
+
312
+ &__dot {
313
+ width: 7px;
314
+ height: 7px;
315
+ padding: 0;
316
+ border: 0;
317
+ border-radius: 50%;
318
+ background-color: fn.fanui-var("color-border-strong");
319
+ cursor: pointer;
320
+ &.is-active { width: 18px; border-radius: fn.fanui-var("radius-full"); background-color: fn.fanui-var("primary"); }
321
+ }
322
+
323
+ &__actions { display: flex; gap: fn.fanui-var("space-2"); margin-inline-start: auto; }
324
+ }
325
+
326
+ @keyframes fanui-tour-in {
327
+ from { opacity: 0; transform: translateY(6px) scale(0.97); }
328
+ to { opacity: 1; transform: translateY(0) scale(1); }
329
+ }
330
+
331
+ // -----------------------------------------------------------------------------
332
+ // 5. 分割面板 splitter
333
+ // -----------------------------------------------------------------------------
334
+
335
+ .fanui-splitter {
336
+ display: flex;
337
+ width: 100%;
338
+ min-height: 120px;
339
+ overflow: hidden;
340
+ border: 1px solid fn.fanui-var("color-border");
341
+ border-radius: fn.fanui-var("radius-md");
342
+ background-color: fn.fanui-var("color-surface");
343
+
344
+ &--vertical { flex-direction: column; }
345
+
346
+ &__pane {
347
+ min-width: 0;
348
+ min-height: 0;
349
+ overflow: auto;
350
+ padding: fn.fanui-var("space-4");
351
+ box-sizing: border-box;
352
+ flex: 0 0 var(--fanui-splitter-size, 50%);
353
+ }
354
+
355
+ &__bar {
356
+ position: relative;
357
+ flex: 0 0 auto;
358
+ display: grid;
359
+ place-items: center;
360
+ background-color: fn.fanui-var("color-border");
361
+ cursor: col-resize;
362
+ @include mx.fanui-transition((background-color), v.$fanui-duration-fast, v.$fanui-ease-out);
363
+
364
+ // 纵向分割时改为行方向拖动
365
+ .fanui-splitter--vertical & { cursor: row-resize; }
366
+
367
+ &:hover,
368
+ &.is-active { background-color: fn.fanui-var("primary"); }
369
+
370
+ &:focus-visible { outline: 2px solid fn.fanui-var("primary"); outline-offset: 2px; }
371
+ }
372
+
373
+ &__bar-handle {
374
+ width: 3px;
375
+ height: 28px;
376
+ border-radius: fn.fanui-var("radius-full");
377
+ background-color: fn.fanui-var("color-text-subtle");
378
+ opacity: 0.55;
379
+
380
+ .fanui-splitter--vertical & { width: 28px; height: 3px; }
381
+ }
382
+
383
+ // 拖动中的全局保护:避免选中文本与 pointer 丢失
384
+ &.is-resizing { user-select: none; cursor: col-resize; }
385
+ &.is-resizing.fanui-splitter--vertical { cursor: row-resize; }
386
+ }
387
+
388
+ // -----------------------------------------------------------------------------
389
+ // 6. 吸顶 affix
390
+ // -----------------------------------------------------------------------------
391
+
392
+ .fanui-affix {
393
+ // 占位块:吸顶期间撑住原位置,避免页面跳动
394
+ &__placeholder { display: block; }
395
+
396
+ // 被吸顶的目标元素(JS 会加上 .fanui-affix__inner 与 .is-affixed)
397
+ &__inner.is-affixed {
398
+ position: fixed;
399
+ z-index: fn.fanui-var("z-sticky", v.$fanui-zindex-sticky);
400
+ inset-inline: 0;
401
+ background-color: fn.fanui-var("color-surface");
402
+ box-shadow: fn.fanui-var("shadow-sm");
403
+
404
+ &.is-affixed-bottom { position: absolute; top: auto; }
405
+ }
406
+ }
407
+
408
+ // -----------------------------------------------------------------------------
409
+ // 7. 回到顶部 backtop
410
+ // -----------------------------------------------------------------------------
411
+
412
+ .fanui-backtop {
413
+ position: fixed;
414
+ inset-inline-end: fn.fanui-var("space-5");
415
+ bottom: fn.fanui-var("space-5");
416
+ z-index: fn.fanui-var("z-fixed", v.$fanui-zindex-fixed);
417
+ display: grid;
418
+ place-items: center;
419
+ width: 44px;
420
+ height: 44px;
421
+ padding: 0;
422
+ border: 1px solid fn.fanui-var("color-border");
423
+ border-radius: 50%;
424
+ background-color: fn.fanui-var("color-surface");
425
+ color: fn.fanui-var("color-text");
426
+ box-shadow: fn.fanui-var("shadow-lg");
427
+ cursor: pointer;
428
+ opacity: 0;
429
+ visibility: hidden;
430
+ transform: translateY(8px);
431
+ @include mx.fanui-transition((opacity, visibility, transform, background-color), v.$fanui-duration-base, v.$fanui-ease-out);
432
+
433
+ &.is-visible { opacity: 1; visibility: visible; transform: translateY(0); }
434
+ &:hover { background-color: fn.fanui-var("primary"); color: fn.fanui-var("primary-fg"); border-color: transparent; }
435
+ &:focus-visible { outline: 2px solid fn.fanui-var("primary"); outline-offset: 2px; }
436
+
437
+ &__ring {
438
+ position: absolute;
439
+ inset: -3px;
440
+ border-radius: 50%;
441
+ background: conic-gradient(fn.fanui-var("primary") var(--fanui-backtop-progress, 0%), transparent 0);
442
+ mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 0);
443
+ pointer-events: none;
444
+ }
445
+
446
+ &__icon { position: relative; font-size: 16px; line-height: 1; }
447
+ }
448
+
449
+ // -----------------------------------------------------------------------------
450
+ // 8. 降级:减少动效
451
+ // -----------------------------------------------------------------------------
452
+
453
+ @media (prefers-reduced-motion: reduce) {
454
+ .fanui-lightbox__img,
455
+ .fanui-lightbox__backdrop,
456
+ .fanui-tour__mask,
457
+ .fanui-tour__popover,
458
+ .fanui-tour__spotlight,
459
+ .fanui-backtop {
460
+ animation: none !important;
461
+ transition: none !important;
462
+ }
463
+ }