@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,885 @@
1
+ // =============================================================================
2
+ // fanUI · 交互创新组件 (Interactive & Novel)
3
+ // -----------------------------------------------------------------------------
4
+ // 这些组件是本套设计系统的「新颖性」集中体现,全部以渐进增强方式实现:
5
+ // 无 JS 时保持可用(纯 CSS 交互),有 JS 时获得更顺滑的行为。
6
+ //
7
+ // .fanui-command 命令面板(⌘K / Ctrl+K)
8
+ // .fanui-dock 磁吸 Dock(纯 CSS :has() 实现相邻放大)
9
+ // .fanui-carousel 轮播(scroll-snap,支持拖拽 / 自动播放)
10
+ // .fanui-fab 悬浮主按钮 + 放射菜单
11
+ // .fanui-scroll-progress 阅读进度条
12
+ // .fanui-spotlight 光斑跟随卡片(鼠标位置驱动边框光效)
13
+ // .fanui-cursor-glow 全局光标辉光
14
+ // .fanui-studio 主题工作台(12 主题色 / 深浅 / 液态玻璃控制台)
15
+ // .fanui-sheet 移动端底部抽屉
16
+ // .fanui-bottom-nav 移动端底部导航
17
+ // =============================================================================
18
+
19
+ @use "sass:map";
20
+ @use "../functions" as fn;
21
+ @use "../variables" as v;
22
+ @use "../mixins" as mx;
23
+
24
+ // -----------------------------------------------------------------------------
25
+ // 1. 命令面板 Command Palette
26
+ // -----------------------------------------------------------------------------
27
+
28
+ .fanui-command {
29
+ @include mx.fanui-fixed-fill();
30
+ z-index: calc(fn.fanui-var("z-modal") + 10);
31
+ display: flex;
32
+ justify-content: center;
33
+ align-items: flex-start;
34
+ padding: 12vh fn.fanui-var("space-4") fn.fanui-var("space-4");
35
+ opacity: 0;
36
+ visibility: hidden;
37
+ @include mx.fanui-transition((opacity, visibility), v.$fanui-duration-base);
38
+
39
+ &.is-open {
40
+ opacity: 1;
41
+ visibility: visible;
42
+ }
43
+
44
+ &__backdrop {
45
+ @include mx.fanui-absolute-fill();
46
+ background-color: fn.fanui-var("backdrop-bg");
47
+ backdrop-filter: blur(6px);
48
+ -webkit-backdrop-filter: blur(6px);
49
+ }
50
+
51
+ &__dialog {
52
+ position: relative;
53
+ width: min(640px, 100%);
54
+ max-height: 70vh;
55
+ display: flex;
56
+ flex-direction: column;
57
+ overflow: hidden;
58
+ border: 1px solid fn.fanui-var("color-border");
59
+ border-radius: fn.fanui-var("radius-2xl");
60
+ background-color: fn.fanui-var("color-overlay");
61
+ box-shadow: fn.fanui-var("shadow-2xl");
62
+ transform: translateY(-12px) scale(0.98);
63
+ @include mx.fanui-transition((transform, opacity), v.$fanui-duration-slow, v.$fanui-ease-spring);
64
+ }
65
+
66
+ &.is-open &__dialog { transform: translateY(0) scale(1); }
67
+
68
+ // 玻璃版:直接复用液态玻璃令牌,开关联动
69
+ &--glass &__dialog {
70
+ @include mx.fanui-glass-surface();
71
+ // 可读性地板:命令面板是高频「阅读面」,
72
+ // 半透明白玻璃直接叠在品牌色 hero 上会让 subtle 文字跌破 AA(实测 #94a3b8 vs 有效底 ≈ 1.9:1),
73
+ // 这里把有效不透明度抬到 ~0.85,同时保留玻璃质感与折射。
74
+ @include mx.fanui-glass-readable-floor(0.55);
75
+ }
76
+
77
+ // 玻璃浮层内的次级文字整体上调一档:subtle → muted(浮层内不允许低于 AA)
78
+ &--glass &__group-title,
79
+ &--glass &__item-desc,
80
+ &--glass &__item-hint,
81
+ &--glass &__item-icon,
82
+ &--glass &__foot {
83
+ @include mx.fanui-glass-readable-text();
84
+ }
85
+
86
+ &__head {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: fn.fanui-var("space-3");
90
+ padding: fn.fanui-var("space-4");
91
+ border-bottom: 1px solid fn.fanui-var("color-border-subtle");
92
+ }
93
+
94
+ &__icon {
95
+ color: fn.fanui-var("color-text-subtle");
96
+ flex: 0 0 auto;
97
+ }
98
+
99
+ &__input {
100
+ flex: 1 1 auto;
101
+ min-width: 0;
102
+ border: 0;
103
+ background: transparent;
104
+ font: inherit;
105
+ font-size: fn.fanui-var("font-size-md");
106
+ color: fn.fanui-var("color-text");
107
+
108
+ &:focus { outline: none; }
109
+ &::placeholder { color: fn.fanui-var("color-text-subtle"); }
110
+ }
111
+
112
+ &__body {
113
+ flex: 1 1 auto;
114
+ min-height: 120px;
115
+ overflow-y: auto;
116
+ padding: fn.fanui-var("space-2");
117
+ @include mx.fanui-scrollbar(6px);
118
+ }
119
+
120
+ &__group-title {
121
+ padding: fn.fanui-var("space-3") fn.fanui-var("space-3") fn.fanui-var("space-1");
122
+ font-size: fn.fanui-var("font-size-xs");
123
+ font-weight: fn.fanui-var("font-weight-semibold");
124
+ letter-spacing: fn.fanui-var("tracking-wider");
125
+ text-transform: uppercase;
126
+ color: fn.fanui-var("color-text-subtle");
127
+ }
128
+
129
+ &__item {
130
+ display: flex;
131
+ align-items: center;
132
+ gap: fn.fanui-var("space-3");
133
+ padding: 9px fn.fanui-var("space-3");
134
+ border-radius: fn.fanui-var("radius-md");
135
+ color: fn.fanui-var("color-text");
136
+ cursor: pointer;
137
+ @include mx.fanui-transition((background-color, transform), v.$fanui-duration-fast);
138
+
139
+ &[aria-selected="true"],
140
+ &.is-active {
141
+ background-color: fn.fanui-var("primary-soft");
142
+ color: fn.fanui-var("primary-ink");
143
+ }
144
+
145
+ &:active { transform: scale(0.995); }
146
+ &[aria-disabled="true"] { opacity: 0.45; pointer-events: none; }
147
+ }
148
+
149
+ &__item-icon {
150
+ display: grid;
151
+ place-items: center;
152
+ width: 28px;
153
+ height: 28px;
154
+ flex: 0 0 auto;
155
+ border-radius: fn.fanui-var("radius-sm");
156
+ background-color: fn.fanui-var("color-surface-sunken");
157
+ color: fn.fanui-var("color-text-muted");
158
+ font-size: fn.fanui-var("font-size-sm");
159
+ }
160
+
161
+ &__item-body { flex: 1 1 auto; min-width: 0; }
162
+
163
+ &__item-title {
164
+ display: block;
165
+ font-size: fn.fanui-var("font-size-sm");
166
+ @include mx.fanui-truncate(1);
167
+ }
168
+
169
+ &__item-desc {
170
+ display: block;
171
+ font-size: fn.fanui-var("font-size-xs");
172
+ color: fn.fanui-var("color-text-subtle");
173
+ @include mx.fanui-truncate(1);
174
+ }
175
+
176
+ &__item-hint {
177
+ flex: 0 0 auto;
178
+ font-size: fn.fanui-var("font-size-xs");
179
+ color: fn.fanui-var("color-text-subtle");
180
+ }
181
+
182
+ &__empty {
183
+ display: grid;
184
+ place-items: center;
185
+ gap: fn.fanui-var("space-2");
186
+ padding: fn.fanui-var("space-10") fn.fanui-var("space-4");
187
+ text-align: center;
188
+ color: fn.fanui-var("color-text-subtle");
189
+ font-size: fn.fanui-var("font-size-sm");
190
+ }
191
+
192
+ &__foot {
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: space-between;
196
+ gap: fn.fanui-var("space-3");
197
+ padding: fn.fanui-var("space-2") fn.fanui-var("space-4");
198
+ border-top: 1px solid fn.fanui-var("color-border-subtle");
199
+ font-size: fn.fanui-var("font-size-xs");
200
+ color: fn.fanui-var("color-text-subtle");
201
+ }
202
+
203
+ &__keys {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ gap: fn.fanui-var("space-1");
207
+ }
208
+
209
+ // 高亮匹配片段
210
+ &__mark {
211
+ padding: 0 1px;
212
+ border-radius: 3px;
213
+ background-color: fn.fanui-var("warning-soft-2");
214
+ color: fn.fanui-var("warning-ink");
215
+ }
216
+ }
217
+
218
+ // -----------------------------------------------------------------------------
219
+ // 2. 磁吸 Dock(纯 CSS 相邻放大,无需 JS)
220
+ // -----------------------------------------------------------------------------
221
+
222
+ .fanui-dock {
223
+ display: inline-flex;
224
+ align-items: flex-end;
225
+ gap: fn.fanui-var("space-2");
226
+ padding: fn.fanui-var("space-2") fn.fanui-var("space-3");
227
+ border-radius: fn.fanui-var("radius-2xl");
228
+
229
+ &__item {
230
+ --fanui-dock-scale: 1;
231
+
232
+ position: relative;
233
+ display: grid;
234
+ place-items: center;
235
+ width: 46px;
236
+ height: 46px;
237
+ flex: 0 0 auto;
238
+ color: fn.fanui-var("color-text");
239
+ text-decoration: none;
240
+ border-radius: fn.fanui-var("radius-lg");
241
+ transform: scale(var(--fanui-dock-scale)) translateY(calc((var(--fanui-dock-scale) - 1) * -8px));
242
+ transform-origin: bottom center;
243
+ @include mx.fanui-transition((transform, box-shadow), v.$fanui-duration-base, v.$fanui-ease-spring);
244
+
245
+ &:focus-visible { @include mx.fanui-focus-ring(); }
246
+ }
247
+
248
+ // 相邻放大:hover 项 1.42,紧邻 1.24,次邻 1.1
249
+ &__item:hover { --fanui-dock-scale: 1.42; }
250
+
251
+ &__item:hover + &__item,
252
+ &__item:has(+ &__item:hover) { --fanui-dock-scale: 1.24; }
253
+
254
+ &__item:hover + &__item + &__item,
255
+ &__item:has(+ &__item + &__item:hover) { --fanui-dock-scale: 1.1; }
256
+
257
+ // 悬停提示
258
+ &__label {
259
+ position: absolute;
260
+ bottom: calc(100% + 10px);
261
+ left: 50%;
262
+ transform: translateX(-50%) translateY(4px);
263
+ padding: 3px 8px;
264
+ font-size: fn.fanui-var("font-size-xs");
265
+ white-space: nowrap;
266
+ color: fn.fanui-var("color-text-inverse");
267
+ background-color: fn.fanui-var("color-text-strong");
268
+ border-radius: fn.fanui-var("radius-sm");
269
+ opacity: 0;
270
+ pointer-events: none;
271
+ @include mx.fanui-transition((opacity, transform), v.$fanui-duration-fast);
272
+ }
273
+
274
+ &__item:hover &__label {
275
+ opacity: 1;
276
+ transform: translateX(-50%) translateY(0);
277
+ }
278
+
279
+ // 运行中标示点
280
+ &__item.is-active::after {
281
+ content: "";
282
+ position: absolute;
283
+ bottom: -5px;
284
+ width: 4px;
285
+ height: 4px;
286
+ border-radius: fn.fanui-var("radius-full");
287
+ background-color: currentColor;
288
+ }
289
+
290
+ &--sm &__item { width: 38px; height: 38px; }
291
+ &--vertical {
292
+ flex-direction: column;
293
+ align-items: center;
294
+
295
+ .fanui-dock__label {
296
+ bottom: auto;
297
+ left: calc(100% + 10px);
298
+ top: 50%;
299
+ transform: translateY(-50%) translateX(-4px);
300
+ }
301
+
302
+ .fanui-dock__item:hover .fanui-dock__label { transform: translateY(-50%) translateX(0); }
303
+ }
304
+ }
305
+
306
+ // -----------------------------------------------------------------------------
307
+ // 3. 轮播 Carousel(scroll-snap 驱动,天然支持触控滑动)
308
+ // -----------------------------------------------------------------------------
309
+
310
+ .fanui-carousel {
311
+ position: relative;
312
+
313
+ &__viewport {
314
+ display: flex;
315
+ gap: var(--fanui-carousel-gap, #{fn.fanui-var("space-4")});
316
+ overflow-x: auto;
317
+ scroll-snap-type: x mandatory;
318
+ scroll-behavior: smooth;
319
+ scrollbar-width: none;
320
+ @include mx.fanui-no-scrollbar();
321
+
322
+ > * {
323
+ flex: 0 0 var(--fanui-carousel-slide, 100%);
324
+ scroll-snap-align: start;
325
+ }
326
+ }
327
+
328
+ &--peek &__viewport > * { --fanui-carousel-slide: calc(100% - 48px); }
329
+ &--2 &__viewport > * { --fanui-carousel-slide: calc((100% - var(--fanui-carousel-gap, 16px)) / 2); }
330
+ &--3 &__viewport > * { --fanui-carousel-slide: calc((100% - var(--fanui-carousel-gap, 16px) * 2) / 3); }
331
+
332
+ &__nav {
333
+ position: absolute;
334
+ top: 50%;
335
+ z-index: 2;
336
+ display: grid;
337
+ place-items: center;
338
+ width: 38px;
339
+ height: 38px;
340
+ border: 1px solid fn.fanui-var("color-border");
341
+ border-radius: fn.fanui-var("radius-full");
342
+ background-color: fn.fanui-var("color-overlay");
343
+ color: fn.fanui-var("color-text");
344
+ box-shadow: fn.fanui-var("shadow-md");
345
+ cursor: pointer;
346
+ transform: translateY(-50%);
347
+ @include mx.fanui-transition((background-color, transform, opacity), v.$fanui-duration-fast);
348
+
349
+ &:hover { background-color: fn.fanui-var("color-hover"); transform: translateY(-50%) scale(1.06); }
350
+ &:disabled { opacity: 0.35; cursor: not-allowed; }
351
+
352
+ &--prev { inset-inline-start: -8px; }
353
+ &--next { inset-inline-end: -8px; }
354
+ }
355
+
356
+ &__dots {
357
+ display: flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ gap: fn.fanui-var("space-2");
361
+ margin-top: fn.fanui-var("space-4");
362
+ }
363
+
364
+ &__dot {
365
+ width: 8px;
366
+ height: 8px;
367
+ padding: 0;
368
+ border: 0;
369
+ border-radius: fn.fanui-var("radius-full");
370
+ background-color: fn.fanui-var("color-border-strong");
371
+ cursor: pointer;
372
+ @include mx.fanui-transition((width, background-color), v.$fanui-duration-base, v.$fanui-ease-spring);
373
+
374
+ &.is-active {
375
+ width: 22px;
376
+ background-color: fn.fanui-var("primary");
377
+ }
378
+ }
379
+
380
+ // 进度条形式的分页器(比圆点更"新")
381
+ &__progress {
382
+ height: 3px;
383
+ margin-top: fn.fanui-var("space-4");
384
+ border-radius: fn.fanui-var("radius-full");
385
+ background-color: fn.fanui-var("color-border");
386
+ overflow: hidden;
387
+ }
388
+
389
+ &__progress-bar {
390
+ height: 100%;
391
+ border-radius: inherit;
392
+ background-image: linear-gradient(to right, var(--fanui-primary), var(--fanui-primary-vivid));
393
+ transform-origin: left center;
394
+ transform: scaleX(var(--fanui-carousel-progress, 0));
395
+ @include mx.fanui-transition(transform, v.$fanui-duration-base);
396
+ }
397
+ }
398
+
399
+ // -----------------------------------------------------------------------------
400
+ // 4. 悬浮主按钮 FAB(含放射菜单)
401
+ // -----------------------------------------------------------------------------
402
+
403
+ .fanui-fab {
404
+ position: fixed;
405
+ inset-block-end: fn.fanui-var("space-6");
406
+ inset-inline-end: fn.fanui-var("space-6");
407
+ z-index: fn.fanui-var("z-fixed");
408
+ display: flex;
409
+ flex-direction: column-reverse;
410
+ align-items: center;
411
+ gap: fn.fanui-var("space-3");
412
+
413
+ &__trigger {
414
+ display: grid;
415
+ place-items: center;
416
+ width: 54px;
417
+ height: 54px;
418
+ border: 0;
419
+ border-radius: fn.fanui-var("radius-full");
420
+ background-image: var(--fanui-brand-grad);
421
+ color: #fff;
422
+ font-size: 20px;
423
+ cursor: pointer;
424
+ box-shadow: 0 10px 24px -8px fn.fanui-var("primary-glow");
425
+ @include mx.fanui-transition((transform, box-shadow), v.$fanui-duration-base, v.$fanui-ease-spring);
426
+
427
+ &:hover { transform: translateY(-2px) scale(1.05); box-shadow: 0 16px 32px -10px fn.fanui-var("primary-glow"); }
428
+ &:active { transform: scale(0.96); }
429
+
430
+ .is-open & { transform: rotate(45deg); }
431
+ }
432
+
433
+ &__list {
434
+ display: flex;
435
+ flex-direction: column-reverse;
436
+ align-items: center;
437
+ gap: fn.fanui-var("space-3");
438
+ margin: 0;
439
+ padding: 0;
440
+ list-style: none;
441
+ }
442
+
443
+ &__item {
444
+ display: flex;
445
+ align-items: center;
446
+ gap: fn.fanui-var("space-2");
447
+ opacity: 0;
448
+ transform: translateY(10px) scale(0.9);
449
+ pointer-events: none;
450
+ @include mx.fanui-transition((opacity, transform), v.$fanui-duration-base, v.$fanui-ease-spring);
451
+
452
+ .is-open & { opacity: 1; transform: none; pointer-events: auto; }
453
+ }
454
+
455
+ &__action {
456
+ display: grid;
457
+ place-items: center;
458
+ width: 42px;
459
+ height: 42px;
460
+ border: 1px solid fn.fanui-var("color-border");
461
+ border-radius: fn.fanui-var("radius-full");
462
+ background-color: fn.fanui-var("color-overlay");
463
+ color: fn.fanui-var("color-text");
464
+ box-shadow: fn.fanui-var("shadow-md");
465
+ cursor: pointer;
466
+ }
467
+
468
+ &__text {
469
+ padding: 3px 8px;
470
+ font-size: fn.fanui-var("font-size-xs");
471
+ white-space: nowrap;
472
+ color: fn.fanui-var("color-text-inverse");
473
+ background-color: fn.fanui-var("color-text-strong");
474
+ border-radius: fn.fanui-var("radius-sm");
475
+ }
476
+
477
+ // 展开后逐项错峰出现
478
+ @for $i from 1 through 5 {
479
+ &__list .fanui-fab__item:nth-child(#{$i}) {
480
+ transition-delay: #{($i - 1) * 40}ms;
481
+ }
482
+ }
483
+
484
+ &--glass &__trigger {
485
+ @include mx.fanui-glass-surface($shadow: false);
486
+ background-image: var(--fanui-brand-grad);
487
+ }
488
+ }
489
+
490
+ // -----------------------------------------------------------------------------
491
+ // 5. 阅读 / 滚动进度条
492
+ // -----------------------------------------------------------------------------
493
+
494
+ .fanui-scroll-progress {
495
+ position: fixed;
496
+ inset-block-start: 0;
497
+ inset-inline: 0;
498
+ z-index: calc(fn.fanui-var("z-fixed") + 1);
499
+ height: 3px;
500
+ background-color: transparent;
501
+ pointer-events: none;
502
+
503
+ &__bar {
504
+ height: 100%;
505
+ background-image: linear-gradient(to right, var(--fanui-primary), var(--fanui-accent));
506
+ transform-origin: left center;
507
+ transform: scaleX(var(--fanui-scroll-progress, 0));
508
+ @include mx.fanui-transition(transform, v.$fanui-duration-fast, v.$fanui-ease-linear);
509
+ }
510
+
511
+ &--thin { height: 2px; }
512
+ &--thick { height: 4px; }
513
+ &--glow &__bar { box-shadow: 0 0 12px fn.fanui-var("primary-glow"); }
514
+ }
515
+
516
+ // -----------------------------------------------------------------------------
517
+ // 6. 光斑跟随卡片 Spotlight
518
+ // -----------------------------------------------------------------------------
519
+ // 由 JS 写入 --fanui-spot-x / --fanui-spot-y,形成"手电筒照在卡片上"的效果。
520
+ // 纯 CSS 时默认位置也能成立(静态高光)。
521
+
522
+ .fanui-spotlight {
523
+ position: relative;
524
+ isolation: isolate;
525
+ overflow: hidden;
526
+ border: 1px solid fn.fanui-var("color-border");
527
+ border-radius: fn.fanui-var("radius-xl");
528
+ background-color: fn.fanui-var("color-surface");
529
+ @include mx.fanui-transition((border-color, box-shadow, transform), v.$fanui-duration-base);
530
+
531
+ // 光斑层
532
+ &::before {
533
+ content: "";
534
+ position: absolute;
535
+ inset: 0;
536
+ z-index: 0;
537
+ pointer-events: none;
538
+ opacity: 0;
539
+ background: radial-gradient(
540
+ 260px circle at var(--fanui-spot-x, 50%) var(--fanui-spot-y, 0%),
541
+ var(--fanui-primary-soft-2, #{fn.fanui-var("primary-soft")}),
542
+ transparent 68%
543
+ );
544
+ @include mx.fanui-transition(opacity, v.$fanui-duration-base);
545
+ }
546
+
547
+ // 边框光效:用遮罩环让描边局部点亮
548
+ &::after {
549
+ content: "";
550
+ position: absolute;
551
+ inset: 0;
552
+ z-index: 1;
553
+ pointer-events: none;
554
+ border-radius: inherit;
555
+ padding: 1px;
556
+ opacity: 0;
557
+ background: radial-gradient(
558
+ 200px circle at var(--fanui-spot-x, 50%) var(--fanui-spot-y, 0%),
559
+ fn.fanui-var("primary"),
560
+ transparent 70%
561
+ );
562
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
563
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
564
+ -webkit-mask-composite: xor;
565
+ mask-composite: exclude;
566
+ @include mx.fanui-transition(opacity, v.$fanui-duration-base);
567
+ }
568
+
569
+ &:hover::before,
570
+ &:hover::after { opacity: 1; }
571
+
572
+ > * { position: relative; z-index: 2; }
573
+
574
+ &:hover {
575
+ transform: translateY(-2px);
576
+ box-shadow: fn.fanui-var("shadow-lg");
577
+ }
578
+
579
+ &--static {
580
+ &::before,
581
+ &::after { opacity: 0.55; }
582
+ }
583
+ }
584
+
585
+ // -----------------------------------------------------------------------------
586
+ // 7. 全局光标辉光(官网 / 落地页氛围)
587
+ // -----------------------------------------------------------------------------
588
+
589
+ .fanui-cursor-glow {
590
+ position: fixed;
591
+ inset: 0;
592
+ z-index: 0;
593
+ pointer-events: none;
594
+ background: radial-gradient(
595
+ 520px circle at var(--fanui-cursor-x, 50%) var(--fanui-cursor-y, 30%),
596
+ fn.fanui-var("brand-aura-a"),
597
+ transparent 62%
598
+ );
599
+ opacity: 0.9;
600
+
601
+ html[data-fanui-glass="off"] & { opacity: 0.5; }
602
+ }
603
+
604
+ // -----------------------------------------------------------------------------
605
+ // 8. 主题工作台 Theme Studio(设计系统的"驾驶舱")
606
+ // -----------------------------------------------------------------------------
607
+ // 这是一个可以放进任何产品的可复用面板:12 主题色 / 深浅模式 / 液态玻璃开关与强度。
608
+
609
+ .fanui-studio {
610
+ position: fixed;
611
+ inset-block-end: fn.fanui-var("space-5");
612
+ inset-inline-end: fn.fanui-var("space-5");
613
+ z-index: calc(fn.fanui-var("z-fixed") + 20);
614
+ display: flex;
615
+ flex-direction: column;
616
+ align-items: flex-end;
617
+ gap: fn.fanui-var("space-3");
618
+ font-size: fn.fanui-var("font-size-sm");
619
+
620
+ &__trigger {
621
+ display: inline-flex;
622
+ align-items: center;
623
+ gap: fn.fanui-var("space-2");
624
+ height: 44px;
625
+ padding: 0 fn.fanui-var("space-4");
626
+ border: 1px solid fn.fanui-var("color-border");
627
+ border-radius: fn.fanui-var("radius-full");
628
+ background-color: fn.fanui-var("color-overlay");
629
+ color: fn.fanui-var("color-text");
630
+ box-shadow: fn.fanui-var("shadow-lg");
631
+ cursor: pointer;
632
+ @include mx.fanui-glass-surface($shadow: false);
633
+ box-shadow: fn.fanui-var("shadow-lg");
634
+ @include mx.fanui-transition((transform, box-shadow), v.$fanui-duration-fast);
635
+
636
+ &:hover { transform: translateY(-2px); }
637
+
638
+ &-dot {
639
+ width: 14px;
640
+ height: 14px;
641
+ border-radius: fn.fanui-var("radius-full");
642
+ background-image: var(--fanui-brand-grad);
643
+ box-shadow: 0 0 0 3px fn.fanui-var("primary-ring");
644
+ }
645
+ }
646
+
647
+ &__panel {
648
+ width: 300px;
649
+ max-height: min(74vh, 620px);
650
+ overflow-y: auto;
651
+ padding: fn.fanui-var("space-5");
652
+ border-radius: fn.fanui-var("radius-2xl");
653
+ @include mx.fanui-scrollbar(6px);
654
+ @include mx.fanui-liquid-glass();
655
+ --fanui-glass-radius: #{fn.fanui-var("radius-2xl")};
656
+
657
+ opacity: 0;
658
+ visibility: hidden;
659
+ transform: translateY(12px) scale(0.97);
660
+ transform-origin: bottom right;
661
+ @include mx.fanui-transition((opacity, transform, visibility), v.$fanui-duration-slow, v.$fanui-ease-spring);
662
+
663
+ .fanui-studio.is-open & { opacity: 1; visibility: visible; transform: none; }
664
+ }
665
+
666
+ &__title {
667
+ display: flex;
668
+ align-items: center;
669
+ justify-content: space-between;
670
+ margin-bottom: fn.fanui-var("space-4");
671
+ font-size: fn.fanui-var("font-size-md");
672
+ font-weight: fn.fanui-var("font-weight-semibold");
673
+ color: fn.fanui-var("color-text-strong");
674
+ }
675
+
676
+ &__section {
677
+ padding-block: fn.fanui-var("space-3");
678
+ border-top: 1px solid fn.fanui-var("glass-divider");
679
+
680
+ &:first-of-type { border-top: 0; padding-top: 0; }
681
+ }
682
+
683
+ &__label {
684
+ display: flex;
685
+ align-items: center;
686
+ justify-content: space-between;
687
+ margin-bottom: fn.fanui-var("space-2");
688
+ font-size: fn.fanui-var("font-size-xs");
689
+ font-weight: fn.fanui-var("font-weight-semibold");
690
+ letter-spacing: fn.fanui-var("tracking-wide");
691
+ color: fn.fanui-var("color-text-subtle");
692
+ text-transform: uppercase;
693
+ }
694
+
695
+ &__value {
696
+ font-variant-numeric: tabular-nums;
697
+ font-weight: fn.fanui-var("font-weight-normal");
698
+ color: fn.fanui-var("primary-ink");
699
+ text-transform: none;
700
+ letter-spacing: 0;
701
+ }
702
+
703
+ // 12 主题色网格
704
+ &__accents {
705
+ display: grid;
706
+ grid-template-columns: repeat(6, 1fr);
707
+ gap: fn.fanui-var("space-2");
708
+ }
709
+
710
+ &__accent {
711
+ position: relative;
712
+ aspect-ratio: 1;
713
+ padding: 0;
714
+ border: 1px solid fn.fanui-alpha(#020617, 0.14);
715
+ border-radius: fn.fanui-var("radius-md");
716
+ background-image: linear-gradient(135deg, var(--fanui-studio-c1), var(--fanui-studio-c2));
717
+ cursor: pointer;
718
+ @include mx.fanui-transition((transform, box-shadow), v.$fanui-duration-fast);
719
+
720
+ &:hover { transform: translateY(-2px) scale(1.05); }
721
+
722
+ &[aria-pressed="true"] {
723
+ box-shadow: 0 0 0 2px fn.fanui-var("color-overlay"), 0 0 0 4px fn.fanui-var("primary");
724
+ }
725
+
726
+ &[aria-pressed="true"]::after {
727
+ content: "";
728
+ position: absolute;
729
+ inset: 0;
730
+ margin: auto;
731
+ width: 8px;
732
+ height: 5px;
733
+ border-inline-start: 2px solid #fff;
734
+ border-bottom: 2px solid #fff;
735
+ transform: rotate(-45deg) translate(1px, -1px);
736
+ filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.5));
737
+ }
738
+ }
739
+
740
+ &__row {
741
+ display: flex;
742
+ align-items: center;
743
+ gap: fn.fanui-var("space-2");
744
+ }
745
+
746
+ &__hint {
747
+ margin-top: fn.fanui-var("space-2");
748
+ font-size: fn.fanui-var("font-size-xs");
749
+ line-height: fn.fanui-var("line-height-normal");
750
+ color: fn.fanui-var("color-text-subtle");
751
+ }
752
+
753
+ &__kbd {
754
+ display: inline-block;
755
+ padding: 0 5px;
756
+ font-family: fn.fanui-var("font-mono");
757
+ font-size: 11px;
758
+ line-height: 18px;
759
+ color: fn.fanui-var("color-text-muted");
760
+ background-color: fn.fanui-var("glass-divider");
761
+ border: 1px solid fn.fanui-var("color-border");
762
+ border-radius: fn.fanui-var("radius-sm");
763
+ }
764
+ }
765
+
766
+ // -----------------------------------------------------------------------------
767
+ // 9. 移动端:底部抽屉 + 底部导航
768
+ // -----------------------------------------------------------------------------
769
+
770
+ .fanui-sheet {
771
+ @include mx.fanui-fixed-fill();
772
+ z-index: fn.fanui-var("z-modal");
773
+ display: flex;
774
+ align-items: flex-end;
775
+ justify-content: center;
776
+ opacity: 0;
777
+ visibility: hidden;
778
+ @include mx.fanui-transition((opacity, visibility), v.$fanui-duration-base);
779
+
780
+ &.is-open { opacity: 1; visibility: visible; }
781
+
782
+ &__backdrop {
783
+ @include mx.fanui-absolute-fill();
784
+ background-color: fn.fanui-var("backdrop-bg");
785
+ }
786
+
787
+ &__panel {
788
+ position: relative;
789
+ width: 100%;
790
+ max-width: 560px;
791
+ max-height: 88vh;
792
+ display: flex;
793
+ flex-direction: column;
794
+ padding: fn.fanui-var("space-3") fn.fanui-var("space-5") fn.fanui-var("space-6");
795
+ border-radius: fn.fanui-var("radius-2xl") fn.fanui-var("radius-2xl") 0 0;
796
+ @include mx.fanui-liquid-glass();
797
+ --fanui-glass-radius: #{fn.fanui-var("radius-2xl")};
798
+ transform: translateY(100%);
799
+ @include mx.fanui-transition(transform, v.$fanui-duration-slow, v.$fanui-ease-out);
800
+ }
801
+
802
+ &.is-open &__panel { transform: translateY(0); }
803
+
804
+ &__handle {
805
+ width: 40px;
806
+ height: 4px;
807
+ margin: 0 auto fn.fanui-var("space-4");
808
+ border-radius: fn.fanui-var("radius-full");
809
+ background-color: fn.fanui-var("color-border-strong");
810
+ flex: 0 0 auto;
811
+ }
812
+
813
+ &__header {
814
+ display: flex;
815
+ align-items: center;
816
+ justify-content: space-between;
817
+ margin-bottom: fn.fanui-var("space-3");
818
+ }
819
+
820
+ &__body {
821
+ flex: 1 1 auto;
822
+ overflow-y: auto;
823
+ padding-bottom: env(safe-area-inset-bottom, 0);
824
+ }
825
+ }
826
+
827
+ .fanui-bottom-nav {
828
+ position: fixed;
829
+ inset-block-end: 0;
830
+ inset-inline: 0;
831
+ z-index: fn.fanui-var("z-fixed");
832
+ display: flex;
833
+ padding-bottom: env(safe-area-inset-bottom, 0);
834
+
835
+ &__item {
836
+ flex: 1 1 0;
837
+ display: flex;
838
+ flex-direction: column;
839
+ align-items: center;
840
+ gap: 3px;
841
+ padding: fn.fanui-var("space-2") 0;
842
+ border: 0;
843
+ background: transparent;
844
+ font-size: fn.fanui-var("font-size-xs");
845
+ color: fn.fanui-var("color-text-subtle");
846
+ text-decoration: none;
847
+ cursor: pointer;
848
+ @include mx.fanui-transition(color, v.$fanui-duration-fast);
849
+
850
+ &[aria-current="page"],
851
+ &.is-active {
852
+ color: fn.fanui-var("primary");
853
+ }
854
+
855
+ &:hover { color: fn.fanui-var("color-text"); }
856
+ }
857
+
858
+ &__icon {
859
+ display: grid;
860
+ place-items: center;
861
+ width: 24px;
862
+ height: 24px;
863
+ font-size: 18px;
864
+ }
865
+
866
+ &__badge {
867
+ position: absolute;
868
+ transform: translate(12px, -6px);
869
+ }
870
+ }
871
+
872
+ // 安全区适配(应用落地场景)
873
+ @supports (padding: env(safe-area-inset-bottom)) {
874
+ .fanui-fab { inset-block-end: calc(#{fn.fanui-var("space-6")} + env(safe-area-inset-bottom, 0px)); }
875
+ }
876
+
877
+ // -----------------------------------------------------------------------------
878
+ // 10. 动效适配
879
+ // -----------------------------------------------------------------------------
880
+
881
+ @media (prefers-reduced-motion: reduce) {
882
+ .fanui-dock__item { transform: none !important; }
883
+ .fanui-carousel__viewport { scroll-behavior: auto; }
884
+ .fanui-spotlight:hover { transform: none; }
885
+ }