@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,1202 @@
1
+ // =============================================================================
2
+ // fanUI · 混合宏与工具函数层 (Mixins)
3
+ // -----------------------------------------------------------------------------
4
+ // 所有 mixin 统一以 `fanui-` 为前缀,用法:@include fanui-flex(...)
5
+ // =============================================================================
6
+
7
+ @use "sass:math";
8
+ @use "sass:map";
9
+ @use "sass:list";
10
+ @use "sass:meta";
11
+ @use "sass:string";
12
+ @use "functions" as fn;
13
+ @use "variables" as v;
14
+
15
+ // -----------------------------------------------------------------------------
16
+ // 1. 布局 · Flexbox
17
+ // -----------------------------------------------------------------------------
18
+
19
+ /// Flex 容器快捷方式。
20
+ /// @example @include fanui-flex(row, space-between, center, nowrap, 12px);
21
+ @mixin fanui-flex(
22
+ $direction: row,
23
+ $justify: flex-start,
24
+ $align: stretch,
25
+ $wrap: nowrap,
26
+ $gap: null,
27
+ $inline: false
28
+ ) {
29
+ @if $inline {
30
+ display: inline-flex;
31
+ } @else {
32
+ display: flex;
33
+ }
34
+ flex-direction: $direction;
35
+ justify-content: $justify;
36
+ align-items: $align;
37
+ flex-wrap: $wrap;
38
+ @if $gap != null {
39
+ gap: $gap;
40
+ }
41
+ }
42
+
43
+ /// 行内 Flex 容器。
44
+ @mixin fanui-inline-flex(
45
+ $direction: row,
46
+ $justify: flex-start,
47
+ $align: stretch,
48
+ $wrap: nowrap,
49
+ $gap: null
50
+ ) {
51
+ @include fanui-flex($direction, $justify, $align, $wrap, $gap, $inline: true);
52
+ }
53
+
54
+ /// 主轴/交叉轴居中(最常用组合)。
55
+ @mixin fanui-center-flex($direction: row, $gap: null) {
56
+ @include fanui-flex($direction, center, center, nowrap, $gap);
57
+ }
58
+
59
+ /// Flex 子项简写。
60
+ @mixin fanui-flex-item($grow: 0, $shrink: 1, $basis: auto) {
61
+ flex: $grow $shrink $basis;
62
+ }
63
+
64
+ /// 占满剩余空间(等价 flex: 1 1 0%)。
65
+ @mixin fanui-flex-fill($min-width: 0) {
66
+ flex: 1 1 0%;
67
+ min-width: $min-width;
68
+ }
69
+
70
+ // -----------------------------------------------------------------------------
71
+ // 2. 布局 · Grid
72
+ // -----------------------------------------------------------------------------
73
+
74
+ /// CSS Grid 容器。
75
+ /// @example @include fanui-grid(repeat(3, 1fr), 16px, 16px);
76
+ @mixin fanui-grid($columns: null, $column-gap: null, $row-gap: null, $auto-flow: null) {
77
+ display: grid;
78
+ @if $columns != null { grid-template-columns: $columns; }
79
+ @if $column-gap != null { column-gap: $column-gap; }
80
+ @if $row-gap != null { row-gap: $row-gap; }
81
+ @if $auto-flow != null { grid-auto-flow: $auto-flow; }
82
+ }
83
+
84
+ /// 自适应卡片网格(auto-fit + minmax),无需媒体查询。
85
+ /// @example @include fanui-grid-auto(280px, 16px);
86
+ @mixin fanui-grid-auto($min-width: 280px, $gap: 16px, $fill: auto-fit) {
87
+ display: grid;
88
+ grid-template-columns: repeat(#{$fill}, minmax(#{$min-width}, 1fr));
89
+ gap: $gap;
90
+ }
91
+
92
+ /// 12 栅格体系下的列宽计算。
93
+ @mixin fanui-make-col($size, $columns: v.$fanui-grid-columns) {
94
+ @if $size == 0 {
95
+ display: none;
96
+ } @else if $size == "auto" {
97
+ flex: 0 0 auto;
98
+ width: auto;
99
+ max-width: 100%;
100
+ } @else {
101
+ flex: 0 0 auto;
102
+ width: fn.fanui-percentage($size, $columns);
103
+ }
104
+ }
105
+
106
+ /// 栅格行容器。
107
+ @mixin fanui-make-row($gutter-x: v.$fanui-grid-gutter-x, $gutter-y: v.$fanui-grid-gutter-y) {
108
+ --fanui-gutter-x: #{$gutter-x};
109
+ --fanui-gutter-y: #{$gutter-y};
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ margin-top: fn.fanui-negate($gutter-y);
113
+ margin-inline-end: calc(fn.fanui-negate($gutter-x) * 0.5);
114
+ margin-inline-start: calc(fn.fanui-negate($gutter-x) * 0.5);
115
+
116
+ > * {
117
+ box-sizing: border-box;
118
+ flex-shrink: 0;
119
+ width: 100%;
120
+ max-width: 100%;
121
+ padding-inline-end: calc(var(--fanui-gutter-x, #{$gutter-x}) * 0.5);
122
+ padding-inline-start: calc(var(--fanui-gutter-x, #{$gutter-x}) * 0.5);
123
+ margin-top: var(--fanui-gutter-y, #{$gutter-y});
124
+ }
125
+ }
126
+
127
+ // -----------------------------------------------------------------------------
128
+ // 3. 响应式
129
+ // -----------------------------------------------------------------------------
130
+
131
+ /// min-width 媒体查询(移动优先)。
132
+ /// @example @include fanui-media("md") { ... }
133
+ @mixin fanui-media($breakpoint, $type: min) {
134
+ $width: fn.fanui-breakpoint(v.$fanui-breakpoints, $breakpoint);
135
+ @if $type == min {
136
+ @if $width == 0 {
137
+ @content;
138
+ } @else {
139
+ @media (min-width: $width) { @content; }
140
+ }
141
+ } @else if $type == max {
142
+ @if $width != 0 {
143
+ @media (max-width: fn.fanui-negate(math.div(1px, 20)) + $width) { @content; }
144
+ }
145
+ }
146
+ }
147
+
148
+ /// max-width 媒体查询(向下覆盖)。
149
+ @mixin fanui-media-down($breakpoint) {
150
+ @include fanui-media($breakpoint, max) { @content; }
151
+ }
152
+
153
+ /// 区间媒体查询。
154
+ @mixin fanui-media-between($from, $to) {
155
+ $min: fn.fanui-breakpoint(v.$fanui-breakpoints, $from);
156
+ $max: fn.fanui-breakpoint(v.$fanui-breakpoints, $to);
157
+ @if $min != 0 {
158
+ @media (min-width: $min) and (max-width: fn.fanui-negate(math.div(1px, 20)) + $max) {
159
+ @content;
160
+ }
161
+ } @else {
162
+ @media (max-width: fn.fanui-negate(math.div(1px, 20)) + $max) { @content; }
163
+ }
164
+ }
165
+
166
+ /// 仅当前断点区间。
167
+ @mixin fanui-media-only($breakpoint) {
168
+ $keys: map.keys(v.$fanui-breakpoints);
169
+ $index: list.index($keys, $breakpoint);
170
+ @if $index == null {
171
+ @error "fanUI: 未知断点 `#{$breakpoint}`,可用值:#{$keys}。";
172
+ }
173
+ @if $index == list.length($keys) {
174
+ @include fanui-media($breakpoint, min) { @content; }
175
+ } @else {
176
+ @include fanui-media-between($breakpoint, list.nth($keys, $index + 1)) { @content; }
177
+ }
178
+ }
179
+
180
+ /// 遍历所有(或指定)断点,生成响应式工具类。
181
+ /// @example @include fanui-each-breakpoint("md" "lg") { ... }
182
+ @mixin fanui-each-breakpoint($breakpoints: map.keys(v.$fanui-breakpoints)) {
183
+ @each $name in $breakpoints {
184
+ @include fanui-media($name, min) {
185
+ $fanui-infix: fn.fanui-bp-infix($name);
186
+ @content;
187
+ }
188
+ }
189
+ }
190
+
191
+ /// 生成带断点中缀的类名后缀。配合 fanui-each-breakpoint 使用。
192
+ @mixin fanui-breakpoint-infix($name) {
193
+ @if $name != "xs" {
194
+ -#{$name} {
195
+ @content;
196
+ }
197
+ } @else {
198
+ @content;
199
+ }
200
+ }
201
+
202
+ /// 深浅模式(媒体查询自动跟随系统)。
203
+ @mixin fanui-prefers-dark {
204
+ @media (prefers-color-scheme: dark) { @content; }
205
+ }
206
+
207
+ // -----------------------------------------------------------------------------
208
+ // 4. 容器
209
+ // -----------------------------------------------------------------------------
210
+
211
+ /// 固定宽度容器(各断点最大宽度)。
212
+ @mixin fanui-container($gutter: v.$fanui-container-padding-x) {
213
+ width: 100%;
214
+ padding-inline-end: $gutter;
215
+ padding-inline-start: $gutter;
216
+ margin-inline-end: auto;
217
+ margin-inline-start: auto;
218
+ box-sizing: border-box;
219
+
220
+ @each $name, $max in v.$fanui-container-max-widths {
221
+ @include fanui-media($name, min) {
222
+ max-width: $max;
223
+ }
224
+ }
225
+ }
226
+
227
+ /// 全宽容器。
228
+ @mixin fanui-container-fluid($gutter: v.$fanui-container-padding-x) {
229
+ width: 100%;
230
+ padding-inline-end: $gutter;
231
+ padding-inline-start: $gutter;
232
+ margin-inline-end: auto;
233
+ margin-inline-start: auto;
234
+ box-sizing: border-box;
235
+ }
236
+
237
+ // -----------------------------------------------------------------------------
238
+ // 5. 尺寸与定位
239
+ // -----------------------------------------------------------------------------
240
+
241
+ /// 同时设置宽高。
242
+ @mixin fanui-size($width, $height: $width) {
243
+ width: $width;
244
+ height: $height;
245
+ }
246
+
247
+ /// 宽高相等。
248
+ @mixin fanui-square($size) {
249
+ @include fanui-size($size, $size);
250
+ }
251
+
252
+ /// 绝对定位四向铺满。
253
+ @mixin fanui-absolute-fill($offset: 0) {
254
+ position: absolute;
255
+ top: $offset;
256
+ right: $offset;
257
+ bottom: $offset;
258
+ left: $offset;
259
+ }
260
+
261
+ /// 绝对居中(支持横向/纵向)。
262
+ /// @example @include fanui-abs-center(both);
263
+ @mixin fanui-abs-center($axis: both, $translate: -50%) {
264
+ position: absolute;
265
+ @if $axis == both or $axis == x {
266
+ left: 50%;
267
+ }
268
+ @if $axis == both or $axis == y {
269
+ top: 50%;
270
+ }
271
+ @if $axis == both {
272
+ transform: translate($translate, $translate);
273
+ } @else if $axis == x {
274
+ transform: translateX($translate);
275
+ } @else if $axis == y {
276
+ transform: translateY($translate);
277
+ }
278
+ }
279
+
280
+ /// 固定定位铺满(常用于遮罩层)。
281
+ @mixin fanui-fixed-fill() {
282
+ position: fixed;
283
+ top: 0;
284
+ right: 0;
285
+ bottom: 0;
286
+ left: 0;
287
+ }
288
+
289
+ /// 维持宽高比。
290
+ @mixin fanui-aspect-ratio($width: 16, $height: 9) {
291
+ aspect-ratio: math.div($width, $height);
292
+ }
293
+
294
+ // -----------------------------------------------------------------------------
295
+ // 6. 文本处理
296
+ // -----------------------------------------------------------------------------
297
+
298
+ /// 单行 / 多行省略。
299
+ /// @example @include fanui-truncate(2);
300
+ @mixin fanui-truncate($lines: 1) {
301
+ @if $lines == 1 {
302
+ overflow: hidden;
303
+ text-overflow: ellipsis;
304
+ white-space: nowrap;
305
+ } @else {
306
+ display: -webkit-box;
307
+ -webkit-line-clamp: $lines;
308
+ -webkit-box-orient: vertical;
309
+ overflow: hidden;
310
+ text-overflow: ellipsis;
311
+ }
312
+ }
313
+
314
+ /// 无衬线/等宽字体栈。
315
+ @mixin fanui-font($family: base, $size: null, $weight: null, $line-height: null) {
316
+ @if $family == base { font-family: v.$fanui-font-family-base; }
317
+ @else if $family == sans { font-family: v.$fanui-font-family-sans; }
318
+ @else if $family == serif { font-family: v.$fanui-font-family-serif; }
319
+ @else if $family == mono { font-family: v.$fanui-font-family-mono; }
320
+ @if $size != null { font-size: $size; }
321
+ @if $weight != null { font-weight: $weight; }
322
+ @if $line-height != null { line-height: $line-height; }
323
+ }
324
+
325
+ /// 文本渐变填充。
326
+ @mixin fanui-text-gradient($from, $to, $direction: to right) {
327
+ background-image: linear-gradient($direction, $from, $to);
328
+ -webkit-background-clip: text;
329
+ background-clip: text;
330
+ color: transparent;
331
+ -webkit-text-fill-color: transparent;
332
+ }
333
+
334
+ /// 屏幕阅读器可见、视觉隐藏(可聚焦时显示)。
335
+ @mixin fanui-visually-hidden($focusable: false) {
336
+ position: absolute !important;
337
+ width: 1px !important;
338
+ height: 1px !important;
339
+ padding: 0 !important;
340
+ margin: -1px !important;
341
+ overflow: hidden !important;
342
+ clip: rect(0, 0, 0, 0) !important;
343
+ white-space: nowrap !important;
344
+ border: 0 !important;
345
+
346
+ @if $focusable {
347
+ &:active,
348
+ &:focus {
349
+ position: static !important;
350
+ width: auto !important;
351
+ height: auto !important;
352
+ margin: 0 !important;
353
+ overflow: visible !important;
354
+ clip: auto !important;
355
+ white-space: normal !important;
356
+ }
357
+ }
358
+ }
359
+
360
+ /// 输入框 placeholder 样式(跨浏览器)。
361
+ @mixin fanui-placeholder($color: null, $opacity: 1) {
362
+ @if $color != null {
363
+ &::placeholder { color: $color; opacity: $opacity; }
364
+ } @else {
365
+ &::placeholder { opacity: $opacity; }
366
+ }
367
+ &::-webkit-input-placeholder { @if $color != null { color: $color; } opacity: $opacity; }
368
+ &::-moz-placeholder { @if $color != null { color: $color; } opacity: $opacity; }
369
+ &:-ms-input-placeholder { @if $color != null { color: $color; } opacity: $opacity; }
370
+ }
371
+
372
+ /// 清除浮动。
373
+ @mixin fanui-clearfix() {
374
+ &::after {
375
+ display: block;
376
+ clear: both;
377
+ content: "";
378
+ }
379
+ }
380
+
381
+ // -----------------------------------------------------------------------------
382
+ // 7. 外观
383
+ // -----------------------------------------------------------------------------
384
+
385
+ /// 去掉原生外观。
386
+ @mixin fanui-appearance-none() {
387
+ appearance: none;
388
+ -webkit-appearance: none;
389
+ -moz-appearance: none;
390
+ background-clip: padding-box;
391
+ }
392
+
393
+ /// 焦点环(无障碍)。
394
+ /// @example @include fanui-focus-ring(); // 使用主题色(运行时可变)
395
+ /// @example @include fanui-focus-ring(v.$fanui-danger); // 指定固定颜色
396
+ @mixin fanui-focus-ring($color: null, $width: null, $offset: null) {
397
+ // C3:焦点环三参数全部走运行时令牌(--fanui-focus-ring-*),
398
+ // 业务方可整体覆盖焦点环颜色 / 宽度 / 偏移,无需改 SCSS。
399
+ // 注:这里用 @if/@else 而不是 if() 函数——后者在新版 Sass 中已弃用
400
+ // (DEPRECATION WARNING [if-function]),@if 控制指令无弃用风险且语义等价。
401
+ $ring: null;
402
+ @if $color != null {
403
+ $ring: fn.fanui-alpha($color, v.$fanui-focus-ring-alpha);
404
+ } @else {
405
+ $ring: fn.fanui-var("focus-ring-color", fn.fanui-alpha(v.$fanui-primary, v.$fanui-focus-ring-alpha));
406
+ }
407
+ $w: null;
408
+ @if $width != null {
409
+ $w: $width;
410
+ } @else {
411
+ $w: fn.fanui-var("focus-ring-width", v.$fanui-focus-ring-width);
412
+ }
413
+ $o: null;
414
+ @if $offset != null {
415
+ $o: $offset;
416
+ } @else {
417
+ $o: fn.fanui-var("focus-ring-offset", v.$fanui-focus-ring-offset);
418
+ }
419
+
420
+ outline: 0;
421
+ box-shadow: 0 0 0 $o fn.fanui-var("color-surface", #fff),
422
+ 0 0 0 calc(#{$o} + #{$w}) #{$ring};
423
+
424
+ // 高对比度模式下回退为系统 outline
425
+ @media (forced-colors: active) {
426
+ outline: 2px solid transparent;
427
+ outline-offset: 2px;
428
+ }
429
+ }
430
+
431
+ /// 仅在键盘聚焦时显示焦点环(默认主题色,运行时可变)。
432
+ @mixin fanui-focus-ring-visible($color: null) {
433
+ &:focus-visible {
434
+ @include fanui-focus-ring($color);
435
+ }
436
+ }
437
+
438
+ /// 阴影层级。
439
+ @mixin fanui-elevation($level: "base") {
440
+ @if $level == 0 or $level == "none" or $level == null {
441
+ box-shadow: none;
442
+ } @else {
443
+ box-shadow: map.get(v.$fanui-shadows, $level);
444
+ }
445
+ }
446
+
447
+ /// 圆角(可单独指定四角)。
448
+ @mixin fanui-border-radius($radius: v.$fanui-radius-base, $corners: all) {
449
+ @if $corners == all {
450
+ border-radius: $radius;
451
+ } @else if $corners == top {
452
+ border-start-start-radius: $radius;
453
+ border-start-end-radius: $radius;
454
+ } @else if $corners == bottom {
455
+ border-end-start-radius: $radius;
456
+ border-end-end-radius: $radius;
457
+ } @else if $corners == start {
458
+ border-start-start-radius: $radius;
459
+ border-end-start-radius: $radius;
460
+ } @else if $corners == end {
461
+ border-start-end-radius: $radius;
462
+ border-end-end-radius: $radius;
463
+ }
464
+ }
465
+
466
+ /// 边框。
467
+ @mixin fanui-border($color: fn.fanui-var("color-border"), $width: v.$fanui-border-width, $style: solid, $sides: all) {
468
+ @if $sides == all {
469
+ border: $width $style $color;
470
+ } @else {
471
+ @each $side in $sides {
472
+ border-#{$side}: $width $style $color;
473
+ }
474
+ }
475
+ }
476
+
477
+ /// 过渡(统一动效语言)。
478
+ @mixin fanui-transition($props: all, $duration: v.$fanui-duration-base, $timing: v.$fanui-ease-base, $delay: 0s) {
479
+ @if v.$fanui-enable-transitions {
480
+ $list: ();
481
+ @each $p in $props {
482
+ $list: list.append($list, #{$p} #{$duration} #{$timing} #{$delay}, comma);
483
+ }
484
+ transition: $list;
485
+ }
486
+ }
487
+
488
+ /// 禁用状态统一样式。
489
+ @mixin fanui-disabled($opacity: 0.55, $cursor: not-allowed) {
490
+ &:disabled,
491
+ &.is-disabled,
492
+ &[aria-disabled="true"] {
493
+ opacity: $opacity;
494
+ cursor: $cursor;
495
+ pointer-events: none;
496
+ box-shadow: none;
497
+ }
498
+ }
499
+
500
+ /// 自定义滚动条。
501
+ @mixin fanui-scrollbar($size: 8px, $thumb: fn.fanui-alpha(#64748b, 0.45), $track: transparent, $radius: v.$fanui-radius-full) {
502
+ scrollbar-width: thin;
503
+ scrollbar-color: #{$thumb} #{$track};
504
+
505
+ &::-webkit-scrollbar {
506
+ width: $size;
507
+ height: $size;
508
+ }
509
+ &::-webkit-scrollbar-track {
510
+ background: $track;
511
+ }
512
+ &::-webkit-scrollbar-thumb {
513
+ background: $thumb;
514
+ border-radius: $radius;
515
+
516
+ &:hover { background: fn.fanui-alpha(#64748b, 0.7); }
517
+ }
518
+ }
519
+
520
+ /// 隐藏滚动条但完全保留滚动能力(跨浏览器)。
521
+ @mixin fanui-no-scrollbar() {
522
+ scrollbar-width: none;
523
+ -ms-overflow-style: none;
524
+
525
+ &::-webkit-scrollbar {
526
+ width: 0;
527
+ height: 0;
528
+ display: none;
529
+ }
530
+ }
531
+
532
+ /// 渐变背景。
533
+ @mixin fanui-gradient($from, $to, $direction: to bottom) {
534
+ background-image: linear-gradient($direction, $from, $to);
535
+ }
536
+
537
+ /// 点击反馈(水波纹替代方案:轻微缩放)。
538
+ @mixin fanui-pressable($scale: 0.97) {
539
+ @include fanui-transition((transform, box-shadow), v.$fanui-duration-fast);
540
+ &:active:not(:disabled):not(.is-disabled) {
541
+ transform: scale($scale);
542
+ }
543
+ }
544
+
545
+ // -----------------------------------------------------------------------------
546
+ // 8. 组件级生成器
547
+ // -----------------------------------------------------------------------------
548
+
549
+ /// 按钮变体生成器:一次性产出 背景/边框/前景/hover/active/focus/outline。
550
+ /// @example @include fanui-button-variant(v.$fanui-primary);
551
+ @mixin fanui-button-variant($bg, $fg: null, $border: null, $hover-bg: null, $active-bg: null) {
552
+ @if $fg == null { $fg: fn.fanui-contrast($bg); }
553
+ @if $border == null { $border: $bg; }
554
+ @if $hover-bg == null { $hover-bg: fn.fanui-shade($bg, 10%); }
555
+ @if $active-bg == null { $active-bg: fn.fanui-shade($bg, 18%); }
556
+
557
+ --fanui-btn-bg: #{$bg};
558
+ --fanui-btn-fg: #{$fg};
559
+ --fanui-btn-border: #{$border};
560
+ --fanui-btn-hover-bg: #{$hover-bg};
561
+ --fanui-btn-hover-fg: #{$fg};
562
+ --fanui-btn-hover-border: #{$hover-bg};
563
+ --fanui-btn-active-bg: #{$active-bg};
564
+ --fanui-btn-active-fg: #{$fg};
565
+ --fanui-btn-active-border: #{$active-bg};
566
+ --fanui-btn-ring: #{fn.fanui-alpha($bg, v.$fanui-focus-ring-alpha)};
567
+ }
568
+
569
+ /// 描边(outline)按钮变体。
570
+ @mixin fanui-button-outline-variant($color, $hover-fg: null) {
571
+ @if $hover-fg == null { $hover-fg: fn.fanui-contrast($color); }
572
+ --fanui-btn-bg: transparent;
573
+ --fanui-btn-fg: #{$color};
574
+ --fanui-btn-border: #{$color};
575
+ --fanui-btn-hover-bg: #{$color};
576
+ --fanui-btn-hover-fg: #{$hover-fg};
577
+ --fanui-btn-hover-border: #{$color};
578
+ --fanui-btn-active-bg: #{fn.fanui-shade($color, 18%)};
579
+ --fanui-btn-active-fg: #{$hover-fg};
580
+ --fanui-btn-active-border: #{fn.fanui-shade($color, 18%)};
581
+ --fanui-btn-ring: #{fn.fanui-alpha($color, v.$fanui-focus-ring-alpha)};
582
+ }
583
+
584
+ /// 文字(text/link)按钮变体。
585
+ @mixin fanui-button-text-variant($color) {
586
+ --fanui-btn-bg: transparent;
587
+ --fanui-btn-fg: #{$color};
588
+ --fanui-btn-border: transparent;
589
+ --fanui-btn-hover-bg: #{fn.fanui-alpha($color, 0.10)};
590
+ --fanui-btn-hover-fg: #{fn.fanui-shade($color, 8%)};
591
+ --fanui-btn-hover-border: transparent;
592
+ --fanui-btn-active-bg: #{fn.fanui-alpha($color, 0.18)};
593
+ --fanui-btn-active-fg: #{fn.fanui-shade($color, 16%)};
594
+ --fanui-btn-active-border: transparent;
595
+ --fanui-btn-ring: #{fn.fanui-alpha($color, v.$fanui-focus-ring-alpha)};
596
+ }
597
+
598
+ /// 按钮尺寸。
599
+ @mixin fanui-button-size($height, $padding-x, $font-size, $radius: null) {
600
+ --fanui-btn-height: #{$height};
601
+ --fanui-btn-padding-x: #{$padding-x};
602
+ --fanui-btn-font-size: #{$font-size};
603
+ @if $radius != null {
604
+ --fanui-btn-radius: #{$radius};
605
+ }
606
+ }
607
+
608
+ /// 徽标变体。
609
+ @mixin fanui-badge-variant($bg, $fg: null) {
610
+ @if $fg == null { $fg: fn.fanui-contrast($bg); }
611
+ background-color: $bg;
612
+ color: $fg;
613
+ border-color: transparent;
614
+ }
615
+
616
+ /// 柔和(soft)徽标变体。
617
+ @mixin fanui-badge-soft-variant($color) {
618
+ background-color: fn.fanui-alpha($color, 0.12);
619
+ color: fn.fanui-shade($color, 12%);
620
+ border-color: fn.fanui-alpha($color, 0.22);
621
+ }
622
+
623
+ /// 警告框(alert)变体。
624
+ @mixin fanui-alert-variant($color) {
625
+ $bg: fn.fanui-alpha($color, 0.10);
626
+ $border: fn.fanui-alpha($color, 0.28);
627
+ background-color: $bg;
628
+ border-color: $border;
629
+ color: fn.fanui-shade($color, 22%);
630
+
631
+ .fanui-alert__title { color: fn.fanui-shade($color, 30%); }
632
+
633
+ [data-fanui-theme="dark"] & {
634
+ background-color: fn.fanui-alpha($color, 0.16);
635
+ border-color: fn.fanui-alpha($color, 0.34);
636
+ color: fn.fanui-tint($color, 42%);
637
+ }
638
+ }
639
+
640
+ /// 表单控件样式(输入框/选择器/文本域共用)。
641
+ @mixin fanui-form-control($height: v.$fanui-control-height-base, $padding-x: v.$fanui-control-padding-x, $font-size: v.$fanui-control-font-size, $radius: v.$fanui-control-radius) {
642
+ display: block;
643
+ width: 100%;
644
+ min-height: $height;
645
+ padding: math.div($height - $font-size * v.$fanui-control-line-height - v.$fanui-control-border-width * 2, 2) $padding-x;
646
+ font-family: inherit;
647
+ font-size: $font-size;
648
+ line-height: v.$fanui-control-line-height;
649
+ color: fn.fanui-var("color-text");
650
+ background-color: fn.fanui-var("color-surface");
651
+ border: v.$fanui-control-border-width solid fn.fanui-var("color-border");
652
+ border-radius: $radius;
653
+ box-sizing: border-box;
654
+ @include fanui-appearance-none();
655
+ @include fanui-transition((border-color, box-shadow, background-color), v.$fanui-duration-fast);
656
+
657
+ @include fanui-placeholder(fn.fanui-var("color-text-subtle"));
658
+
659
+ &:hover:not(:disabled):not(.is-disabled) {
660
+ border-color: fn.fanui-var("color-border-strong");
661
+ }
662
+
663
+ &:focus,
664
+ &:focus-visible {
665
+ border-color: fn.fanui-var("primary");
666
+ @include fanui-focus-ring();
667
+ }
668
+
669
+ &:disabled,
670
+ &.is-disabled {
671
+ background-color: fn.fanui-var("color-disabled-bg");
672
+ color: fn.fanui-var("color-disabled-text");
673
+ cursor: not-allowed;
674
+ resize: none;
675
+ }
676
+
677
+ &[readonly] {
678
+ background-color: fn.fanui-var("color-surface-sunken");
679
+ }
680
+ }
681
+
682
+ /// 校验状态(成功/警告/错误)。
683
+ @mixin fanui-form-validation($color) {
684
+ $ring: fn.fanui-alpha($color, v.$fanui-focus-ring-alpha);
685
+
686
+ .fanui-form-control,
687
+ .fanui-select,
688
+ .fanui-textarea {
689
+ border-color: $color;
690
+
691
+ &:focus,
692
+ &:focus-visible {
693
+ border-color: $color;
694
+ box-shadow: 0 0 0 v.$fanui-focus-ring-offset fn.fanui-var("color-surface", #fff),
695
+ 0 0 0 (v.$fanui-focus-ring-offset + v.$fanui-focus-ring-width) $ring;
696
+ }
697
+ }
698
+
699
+ .fanui-form-feedback { color: $color; }
700
+ }
701
+
702
+ /// 卡片变体。
703
+ @mixin fanui-card-variant($bg, $border: null, $fg: null, $shadow: null) {
704
+ background-color: $bg;
705
+ @if $border != null { border-color: $border; }
706
+ @if $fg != null { color: $fg; }
707
+ @if $shadow != null { @include fanui-elevation($shadow); }
708
+ }
709
+
710
+ /// 交互式容器(卡片/列表项)的 hover / active / focus 统一行为。
711
+ @mixin fanui-interactive-surface($radius: v.$fanui-radius-base) {
712
+ cursor: pointer;
713
+ @include fanui-transition((background-color, border-color, box-shadow, transform), v.$fanui-duration-fast);
714
+
715
+ &:hover {
716
+ background-color: fn.fanui-var("color-hover");
717
+ }
718
+ &:active {
719
+ background-color: fn.fanui-var("color-active");
720
+ }
721
+ &:focus-visible {
722
+ @include fanui-focus-ring();
723
+ }
724
+ &.is-selected,
725
+ &[aria-selected="true"] {
726
+ background-color: fn.fanui-var("primary-soft");
727
+ border-color: fn.fanui-var("primary-border");
728
+ color: fn.fanui-var("primary-ink");
729
+ }
730
+ }
731
+
732
+ /// 遮罩层。
733
+ @mixin fanui-backdrop($bg: fn.fanui-var("backdrop-bg", v.$fanui-backdrop-bg), $blur: fn.fanui-var("backdrop-blur", v.$fanui-backdrop-blur)) {
734
+ @include fanui-fixed-fill();
735
+ background-color: $bg;
736
+ @if $blur != null and $blur != 0 {
737
+ backdrop-filter: blur($blur);
738
+ -webkit-backdrop-filter: blur($blur);
739
+ }
740
+ z-index: fn.fanui-var("z-backdrop", v.$fanui-zindex-backdrop);
741
+ }
742
+
743
+ /// 表格变体。
744
+ @mixin fanui-table-variant($striped-bg: fn.fanui-var("color-surface-sunken"), $hover-bg: fn.fanui-var("color-hover"), $border-color: fn.fanui-var("color-border")) {
745
+ --fanui-table-striped-bg: #{$striped-bg};
746
+ --fanui-table-hover-bg: #{$hover-bg};
747
+ --fanui-table-border-color: #{$border-color};
748
+ }
749
+
750
+ /// 进度条变体。
751
+ @mixin fanui-progress-variant($bg, $fg: null, $stripes: true) {
752
+ @if $fg == null { $fg: fn.fanui-contrast($bg); }
753
+ --fanui-progress-bg: #{$bg};
754
+ --fanui-progress-fg: #{$fg};
755
+
756
+ @if $stripes {
757
+ background-image: linear-gradient(
758
+ 45deg,
759
+ rgba(255, 255, 255, 0.18) 25%, transparent 25%,
760
+ transparent 50%, rgba(255, 255, 255, 0.18) 50%,
761
+ rgba(255, 255, 255, 0.18) 75%, transparent 75%, transparent
762
+ );
763
+ background-size: 1rem 1rem;
764
+ }
765
+ }
766
+
767
+ /// 开关(switch)变体。
768
+ @mixin fanui-switch-variant($checked-bg: v.$fanui-primary, $thumb: #ffffff) {
769
+ &:checked + .fanui-switch__track,
770
+ &.is-checked .fanui-switch__track {
771
+ background-color: $checked-bg;
772
+ border-color: $checked-bg;
773
+
774
+ .fanui-switch__thumb {
775
+ background-color: $thumb;
776
+ transform: translateX(var(--fanui-switch-travel, 18px));
777
+ }
778
+ }
779
+ }
780
+
781
+ // -----------------------------------------------------------------------------
782
+ // 9. 主题输出
783
+ // -----------------------------------------------------------------------------
784
+
785
+ /// 输出一组主题令牌为 CSS 自定义属性。
786
+ @mixin fanui-emit-theme($tokens, $prefix: "fanui") {
787
+ @each $name, $value in $tokens {
788
+ --#{$prefix}-#{$name}: #{$value};
789
+ }
790
+ }
791
+
792
+ /// 输出调色板为 CSS 自定义属性(如 --fanui-primary-500)。
793
+ @mixin fanui-emit-palette($palette, $prefix: "fanui") {
794
+ @each $name, $tones in $palette {
795
+ @each $tone, $value in $tones {
796
+ --#{$prefix}-#{$name}-#{$tone}: #{$value};
797
+ }
798
+ }
799
+ }
800
+
801
+ /// 输出语义色快捷变量(--fanui-primary / --fanui-primary-fg / --fanui-primary-soft ...)。
802
+ @mixin fanui-emit-semantic-colors($colors, $prefix: "fanui") {
803
+ @each $name, $value in $colors {
804
+ --#{$prefix}-#{$name}: #{$value};
805
+ --#{$prefix}-#{$name}-hover: #{fn.fanui-shade($value, 10%)};
806
+ --#{$prefix}-#{$name}-active: #{fn.fanui-shade($value, 18%)};
807
+ --#{$prefix}-#{$name}-soft: #{fn.fanui-alpha($value, 0.12)};
808
+ --#{$prefix}-#{$name}-border: #{fn.fanui-alpha($value, 0.28)};
809
+ --#{$prefix}-#{$name}-fg: #{fn.fanui-contrast($value)};
810
+ --#{$prefix}-#{$name}-ring: #{fn.fanui-alpha($value, v.$fanui-focus-ring-alpha)};
811
+ }
812
+ }
813
+
814
+ // -----------------------------------------------------------------------------
815
+ // 9.1 色彩角色令牌生成器(12 主题色 × 全套派生令牌的统一出口)
816
+ // -----------------------------------------------------------------------------
817
+
818
+ /// 一次性输出某个颜色名称的「全套角色令牌」。
819
+ ///
820
+ /// 这是设计系统的核心出口:无论语义色(success/danger…)还是 12 套主题色,
821
+ /// 都通过同一 mixin 产出完全一致的令牌面,保证全组件引用口径统一。
822
+ ///
823
+ /// 产出令牌:
824
+ /// --fanui-<name> 基准色(实心按钮 / 链接 / 描边)
825
+ /// --fanui-<name>-fg 基准色上的前景色(自动取对比度更高者)
826
+ /// --fanui-<name>-hover 悬停态(浅色压暗 / 深色提亮)
827
+ /// --fanui-<name>-active 激活态
828
+ /// --fanui-<name>-vivid 鲜亮色(渐变 / 光晕 / 图表 / 装饰,不保证白字对比度)
829
+ /// --fanui-<name>-vivid-2 次级鲜亮色(渐变止点)
830
+ /// --fanui-<name>-soft 柔和背景(浅色=50 阶,深色=低透明度)
831
+ /// --fanui-<name>-soft-2 次级柔和背景
832
+ /// --fanui-<name>-border 柔和描边
833
+ /// --fanui-<name>-ink 柔和背景上的文字色
834
+ /// --fanui-<name>-ring 焦点环
835
+ /// --fanui-<name>-glow 外发光阴影
836
+ /// --fanui-<name>-grad-a/b 品牌渐变端点
837
+ ///
838
+ /// @param $name 角色名(primary / accent / success / …)
839
+ /// @param $base 基准色(必须是对比度合规的深阶或浅阶)
840
+ /// @param $vivid 鲜亮色,默认同 $base
841
+ /// @param $vivid-2 次鲜亮色,默认同 $vivid
842
+ /// @param $soft / $soft-2 / $border / $ink 可显式指定,未指定则由 $vivid 派生
843
+ /// @param $mode light | dark —— 决定 hover/active 的方向与默认派生策略
844
+ @mixin fanui-emit-color-roles(
845
+ $name,
846
+ $base,
847
+ $vivid: null,
848
+ $vivid-2: null,
849
+ $soft: null,
850
+ $soft-2: null,
851
+ $border: null,
852
+ $ink: null,
853
+ $mode: light,
854
+ $prefix: "fanui"
855
+ ) {
856
+ @if $vivid == null { $vivid: $base; }
857
+ @if $vivid-2 == null { $vivid-2: $vivid; }
858
+
859
+ @if $mode == dark {
860
+ @if $soft == null { $soft: fn.fanui-alpha($vivid, 0.16); }
861
+ @if $soft-2 == null { $soft-2: fn.fanui-alpha($vivid, 0.26); }
862
+ @if $border == null { $border: fn.fanui-alpha($vivid, 0.38); }
863
+ @if $ink == null { $ink: fn.fanui-tint($vivid, 46%); }
864
+ } @else {
865
+ @if $soft == null { $soft: fn.fanui-alpha($vivid, 0.10); }
866
+ @if $soft-2 == null { $soft-2: fn.fanui-alpha($vivid, 0.18); }
867
+ @if $border == null { $border: fn.fanui-alpha($vivid, 0.30); }
868
+ @if $ink == null { $ink: $base; }
869
+ }
870
+
871
+ --#{$prefix}-#{$name}: #{$base};
872
+ --#{$prefix}-#{$name}-fg: #{fn.fanui-best-fg($base)};
873
+ --#{$prefix}-#{$name}-hover: #{fn.fanui-state-color($base, $mode, 9%)};
874
+ --#{$prefix}-#{$name}-active: #{fn.fanui-state-color($base, $mode, 17%)};
875
+ --#{$prefix}-#{$name}-vivid: #{$vivid};
876
+ --#{$prefix}-#{$name}-vivid-2: #{$vivid-2};
877
+ --#{$prefix}-#{$name}-soft: #{$soft};
878
+ --#{$prefix}-#{$name}-soft-2: #{$soft-2};
879
+ --#{$prefix}-#{$name}-border: #{$border};
880
+ --#{$prefix}-#{$name}-ink: #{$ink};
881
+ --#{$prefix}-#{$name}-ring: #{fn.fanui-alpha($vivid, v.$fanui-focus-ring-alpha)};
882
+ --#{$prefix}-#{$name}-glow: #{fn.fanui-alpha($vivid, 0.42)};
883
+ --#{$prefix}-#{$name}-grad-a: #{$vivid};
884
+ --#{$prefix}-#{$name}-grad-b: #{$vivid-2};
885
+ }
886
+
887
+ /// 从调色板自动推导出「合规基准色阶」并输出全套角色令牌。
888
+ ///
889
+ /// 自动择优规则(这是「可获得性由构造保证」的关键):
890
+ /// 浅色模式 → 在 (600,700,800,900,500) 中选第一个「白字对比度 ≥ 4.5」的色阶
891
+ /// (既保证实心按钮白字合规,也保证该色作为正文文字压在白底上合规)
892
+ /// 深色模式 → 在 (500,400,600,300) 中选第一个「与深色底对比度 ≥ 4.5」的色阶
893
+ /// (保证品牌色作为文字/图标出现在深色底上依然合规)
894
+ /// 因此 12 套主题色无需人工逐个调校,任何新增调色板都能自动得到合规主题。
895
+ ///
896
+ /// @param $name 角色名
897
+ /// @param $palette 11 阶调色板(50…950)
898
+ /// @param $mode light | dark
899
+ @mixin fanui-emit-accent-roles($name, $palette, $mode: light, $prefix: "fanui") {
900
+ $candidates: (600, 700, 800, 900, 500);
901
+ $against: #ffffff;
902
+ $vivid-tone: 500;
903
+ $vivid-tone-2: 600;
904
+
905
+ @if $mode == dark {
906
+ $candidates: (500, 400, 600, 300);
907
+ $against: #0f172a;
908
+ $vivid-tone: 400;
909
+ $vivid-tone-2: 300;
910
+ }
911
+
912
+ $tone: fn.fanui-pick-tone($palette, $candidates, 4.5, $against, list.nth($candidates, 1));
913
+
914
+ $base: map.get($palette, $tone);
915
+ $vivid: map.get($palette, $vivid-tone);
916
+ $vivid-2: map.get($palette, $vivid-tone-2);
917
+
918
+ // 柔和面 / 描边 / 墨色:浅色模式取真实浅阶,深色模式取半透明(叠加玻璃与纹理更自然)
919
+ $soft: null;
920
+ $soft-2: null;
921
+ $border: null;
922
+ $ink: null;
923
+
924
+ @if $mode == dark {
925
+ $ink: map.get($palette, 200);
926
+ } @else {
927
+ $soft: map.get($palette, 50);
928
+ $soft-2: map.get($palette, 100);
929
+ $border: map.get($palette, 200);
930
+ $ink: map.get($palette, fn.fanui-pick-tone($palette, (800, 700, 900, 600), 4.5, #ffffff, 800));
931
+ }
932
+
933
+ @include fanui-emit-color-roles($name, $base, $vivid, $vivid-2, $soft, $soft-2, $border, $ink, $mode, $prefix);
934
+
935
+ // 原始色阶重映射,使调色板工具类(.fanui-bg-primary-100…)随主题色联动
936
+ @each $t, $value in $palette {
937
+ --#{$prefix}-#{$name}-#{$t}: #{$value};
938
+ }
939
+ }
940
+
941
+ // -----------------------------------------------------------------------------
942
+ // 9.2 液态玻璃 (Liquid Glass)
943
+ // -----------------------------------------------------------------------------
944
+
945
+ /// 液态玻璃基面。
946
+ ///
947
+ /// 所有参数均通过 CSS 自定义属性驱动,因此可以:
948
+ /// · 全局调强度:--fanui-glass-intensity
949
+ /// · 全局关效果:<html data-fanui-glass="off">
950
+ /// · 单点覆盖:任意元素上写 --fanui-glass-blur-base / --fanui-glass-tint-base …
951
+ ///
952
+ /// 分层策略(关键设计决策):
953
+ /// 内容层 —— 永远在最上,不受影响
954
+ /// 高光层 —— 镜面光斑 + 水感渐变,作为 background-image 叠加,
955
+ /// 因此天然位于内容之下、玻璃底色之上,不会糊掉文字
956
+ /// 底色层 —— 半透明色调(受强度与明暗模式共同调制)
957
+ /// 折射层 —— backdrop-filter 一次性模糊/饱和/提亮背后的画面
958
+ /// 边缘层 —— ::after 渐变描边(mask-composite 挖空中心),模拟玻璃厚度切面
959
+ ///
960
+ /// 注意:刻意不使用 z-index 负值伪元素承载高光 —— 那会被自身背景遮住,
961
+ /// 是这类效果常见的失效点。
962
+ // -----------------------------------------------------------------------------
963
+ // 液态玻璃材质
964
+ // -----------------------------------------------------------------------------
965
+ // 全部公式在此内联展开,而不是引用 :root 上的合成令牌。
966
+ // 原因:CSS 自定义属性在被继承时已在声明元素上完成 var() 替换,
967
+ // 若写成 `--fanui-glass-bg: rgba(var(--tint-rgb), calc(base + k * var(--intensity)))`,
968
+ // 元素上再覆盖 --fanui-glass-intensity 不会重算 → 预设/局部调参失效。
969
+ // -----------------------------------------------------------------------------
970
+
971
+ /// ① 玻璃底色:alpha = tint-base + tint-k × intensity
972
+ @mixin fanui-glass-tint() {
973
+ background-color: rgba(
974
+ var(--fanui-glass-tint-rgb),
975
+ calc(var(--fanui-glass-tint-base) + var(--fanui-glass-tint-k) * var(--fanui-glass-intensity))
976
+ );
977
+ }
978
+
979
+ /// ② 水感高光渐变(仅 sheen,用于只需一层亮面的组件)
980
+ @mixin fanui-glass-sheen() {
981
+ background-image: linear-gradient(
982
+ 180deg,
983
+ rgba(255, 255, 255, calc(var(--fanui-glass-sheen-a) * var(--fanui-glass-intensity))) 0%,
984
+ rgba(255, 255, 255, 0) 46%
985
+ );
986
+ }
987
+
988
+ /// ③ 完整高光层:指针镜面光斑 + 水感渐变;$flow 追加品牌色流体层
989
+ /// @param $flow 是否叠加品牌色流体(配合 .fanui-glass--flow 的位移动画)
990
+ @mixin fanui-glass-layer($flow: false) {
991
+ @if $flow {
992
+ background-image:
993
+ radial-gradient(38% 44% at 24% 30%, var(--fanui-brand-aura-a), transparent 62%),
994
+ radial-gradient(32% 40% at 76% 70%, var(--fanui-brand-aura-b), transparent 60%),
995
+ radial-gradient(
996
+ calc(var(--fanui-glass-specular-size-base) + var(--fanui-glass-specular-size-k) * var(--fanui-glass-intensity))
997
+ circle at var(--fanui-glass-mx, 30%) var(--fanui-glass-my, 0%),
998
+ rgba(
999
+ var(--fanui-glass-specular-rgb),
1000
+ calc(var(--fanui-glass-specular-a) * var(--fanui-glass-intensity) * var(--fanui-glass-specular-alpha))
1001
+ ),
1002
+ transparent 70%
1003
+ ),
1004
+ linear-gradient(
1005
+ 180deg,
1006
+ rgba(255, 255, 255, calc(var(--fanui-glass-sheen-a) * var(--fanui-glass-intensity))) 0%,
1007
+ rgba(255, 255, 255, 0) 46%
1008
+ );
1009
+ } @else {
1010
+ background-image:
1011
+ radial-gradient(
1012
+ calc(var(--fanui-glass-specular-size-base) + var(--fanui-glass-specular-size-k) * var(--fanui-glass-intensity))
1013
+ circle at var(--fanui-glass-mx, 30%) var(--fanui-glass-my, 0%),
1014
+ rgba(
1015
+ var(--fanui-glass-specular-rgb),
1016
+ calc(var(--fanui-glass-specular-a) * var(--fanui-glass-intensity) * var(--fanui-glass-specular-alpha))
1017
+ ),
1018
+ transparent 70%
1019
+ ),
1020
+ linear-gradient(
1021
+ 180deg,
1022
+ rgba(255, 255, 255, calc(var(--fanui-glass-sheen-a) * var(--fanui-glass-intensity))) 0%,
1023
+ rgba(255, 255, 255, 0) 46%
1024
+ );
1025
+ }
1026
+ }
1027
+
1028
+ /// ④ 折射:blur / saturate / brightness 三者同受 intensity 驱动
1029
+ @mixin fanui-glass-refraction() {
1030
+ backdrop-filter: blur(calc(var(--fanui-glass-blur-base) * var(--fanui-glass-intensity) * var(--fanui-glass-blur-mode-k, 1)))
1031
+ saturate(calc(100% + var(--fanui-glass-sat-k) * var(--fanui-glass-intensity)))
1032
+ brightness(calc(100% + var(--fanui-glass-bri-k) * var(--fanui-glass-intensity)));
1033
+ -webkit-backdrop-filter: blur(calc(var(--fanui-glass-blur-base) * var(--fanui-glass-intensity) * var(--fanui-glass-blur-mode-k, 1)))
1034
+ saturate(calc(100% + var(--fanui-glass-sat-k) * var(--fanui-glass-intensity)))
1035
+ brightness(calc(100% + var(--fanui-glass-bri-k) * var(--fanui-glass-intensity)));
1036
+ }
1037
+
1038
+ /// ⑤ 描边色:alpha = border-a + border-k × intensity
1039
+ @mixin fanui-glass-border-color() {
1040
+ border-color: rgba(
1041
+ var(--fanui-glass-border-rgb),
1042
+ calc(var(--fanui-glass-border-a) + var(--fanui-glass-border-k) * var(--fanui-glass-intensity))
1043
+ );
1044
+ }
1045
+
1046
+ /// ⑥ 厚度阴影:投影随强度变长变深 + 顶部内高光
1047
+ @mixin fanui-glass-depth() {
1048
+ box-shadow:
1049
+ 0 calc(var(--fanui-glass-shadow-y-k) * var(--fanui-glass-intensity))
1050
+ calc(var(--fanui-glass-shadow-blur-k) * var(--fanui-glass-intensity))
1051
+ calc(var(--fanui-glass-shadow-spread-k) * var(--fanui-glass-intensity))
1052
+ rgba(var(--fanui-glass-shadow-rgb), calc(var(--fanui-glass-shadow-a) + var(--fanui-glass-shadow-k) * var(--fanui-glass-intensity))),
1053
+ inset 0 1px 0 rgba(255, 255, 255, calc(var(--fanui-glass-inner-hi) * min(1, var(--fanui-glass-intensity))));
1054
+ }
1055
+
1056
+ /// ⑦ 材质叠加:只换「材质层」,不动布局与圆角(供既有组件复用同一套玻璃令牌)
1057
+ @mixin fanui-glass-surface($sheen: true, $shadow: true, $border: true, $refraction: true) {
1058
+ @include fanui-glass-tint();
1059
+
1060
+ @if $sheen {
1061
+ @include fanui-glass-sheen();
1062
+ }
1063
+
1064
+ @if $refraction {
1065
+ @include fanui-glass-refraction();
1066
+ }
1067
+
1068
+ @if $border {
1069
+ @include fanui-glass-border-color();
1070
+ }
1071
+
1072
+ @if $shadow {
1073
+ @include fanui-glass-depth();
1074
+ }
1075
+
1076
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
1077
+ background-color: var(--fanui-glass-fallback-bg);
1078
+ background-image: none;
1079
+ }
1080
+ }
1081
+
1082
+ /// ⑧ 液态玻璃主体材质(.fanui-glass / @include fanui-liquid-glass())
1083
+ ///
1084
+ /// @param $radius 圆角(null 时取 --fanui-glass-radius,再退回 $fanui-radius-lg)
1085
+ /// @param $highlight 是否启用指针镜面高光
1086
+ /// @param $edge 是否绘制边缘流光(::after 遮罩描边)
1087
+ /// @param $flow 是否叠加品牌色流体层
1088
+ @mixin fanui-glass-material($radius: null, $highlight: true, $edge: true, $flow: false) {
1089
+ position: relative;
1090
+ isolation: isolate;
1091
+
1092
+ @if $radius != null { --fanui-glass-radius: #{$radius}; }
1093
+
1094
+ @include fanui-glass-tint();
1095
+ @include fanui-glass-layer($flow);
1096
+ background-repeat: no-repeat;
1097
+ background-size: 100% 100%;
1098
+ border: 1px solid rgba(
1099
+ var(--fanui-glass-border-rgb),
1100
+ calc(var(--fanui-glass-border-a) + var(--fanui-glass-border-k) * var(--fanui-glass-intensity))
1101
+ );
1102
+ border-radius: var(--fanui-glass-radius, #{v.$fanui-radius-lg});
1103
+ color: var(--fanui-color-text);
1104
+ @include fanui-glass-refraction();
1105
+ @include fanui-glass-depth();
1106
+ @include fanui-transition((background-color, box-shadow, border-color, transform), v.$fanui-duration-slow);
1107
+
1108
+ @if not $highlight {
1109
+ --fanui-glass-specular-alpha: 0;
1110
+ }
1111
+
1112
+ @if $edge {
1113
+ &::after {
1114
+ content: "";
1115
+ position: absolute;
1116
+ inset: 0;
1117
+ z-index: 1;
1118
+ border-radius: inherit;
1119
+ pointer-events: none;
1120
+ padding: var(--fanui-glass-edge-width, calc(1px + 0.5px * var(--fanui-glass-intensity)));
1121
+ background: linear-gradient(
1122
+ var(--fanui-glass-edge-angle, 150deg),
1123
+ rgba(255, 255, 255, calc(var(--fanui-glass-edge-hi-a) * min(1, var(--fanui-glass-intensity)))) 0%,
1124
+ rgba(255, 255, 255, calc(var(--fanui-glass-edge-lo-a) * var(--fanui-glass-intensity))) 38%,
1125
+ rgba(255, 255, 255, calc(var(--fanui-glass-edge-lo-a) * var(--fanui-glass-intensity))) 62%,
1126
+ rgba(255, 255, 255, calc(var(--fanui-glass-edge-hi-a) * min(1, var(--fanui-glass-intensity)))) 100%
1127
+ );
1128
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
1129
+ mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
1130
+ -webkit-mask-composite: xor;
1131
+ mask-composite: exclude;
1132
+ opacity: var(--fanui-glass-edge-alpha, 1);
1133
+ }
1134
+ }
1135
+
1136
+ // 无 backdrop-filter 支持时降级为实体表面,保证内容始终可读
1137
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
1138
+ background-color: var(--fanui-glass-fallback-bg);
1139
+ background-image: none;
1140
+ }
1141
+ }
1142
+
1143
+ /// 兼容别名(v1 API):@include fanui-liquid-glass(...)
1144
+ @mixin fanui-liquid-glass($radius: null, $highlight: true, $edge: true, $flow: false) {
1145
+ @include fanui-glass-material($radius, $highlight, $edge, $flow);
1146
+ }
1147
+
1148
+ /// 关闭液态玻璃:退回实体表面(用于强制单点关闭;全局开关走令牌覆盖)。
1149
+ @mixin fanui-glass-off() {
1150
+ background-color: var(--fanui-glass-fallback-bg);
1151
+ background-image: none;
1152
+ backdrop-filter: none;
1153
+ -webkit-backdrop-filter: none;
1154
+ border-color: var(--fanui-color-border);
1155
+
1156
+ &::after {
1157
+ display: none;
1158
+ }
1159
+ }
1160
+
1161
+ /// 液态玻璃上的内容可读性保护:为文字添加极轻的投影,避免透过玻璃后对比度下降。
1162
+ @mixin fanui-glass-legibility() {
1163
+ text-shadow: var(--fanui-glass-text-shadow, 0 1px 1px rgba(0, 0, 0, 0.04));
1164
+ }
1165
+
1166
+ /// 玻璃浮层的「可读性地板」:在玻璃底色之上再叠一层近实色的 overlay 渐变,
1167
+ /// 把浮层有效不透明度抬到 ~0.85+,保证内部次级文字(desc / hint / subtle 令牌)
1168
+ /// 在任何页面背景(亮色 hero、品牌色渐变、图片)上都满足 WCAG AA。
1169
+ /// 仅用于承载正文的玻璃浮层:modal / command / dropdown / popover。
1170
+ /// 不支持 color-mix 的浏览器会整条丢弃该声明,安全回退为普通玻璃。
1171
+ @mixin fanui-glass-readable-floor($alpha: 0.55) {
1172
+ $a1: $alpha;
1173
+ $a2: $alpha + 0.15;
1174
+
1175
+ background-image: linear-gradient(
1176
+ color-mix(in srgb, var(--fanui-color-overlay) #{math.percentage($a1)}, transparent),
1177
+ color-mix(in srgb, var(--fanui-color-overlay) #{math.percentage($a2)}, transparent)
1178
+ );
1179
+ }
1180
+
1181
+ /// 玻璃浮层内的次级文字整体上调一档(subtle → muted):
1182
+ /// 浮层是「阅读面」,次级信息也不允许低于 AA。
1183
+ @mixin fanui-glass-readable-text() {
1184
+ color: fn.fanui-var("color-text-muted");
1185
+ }
1186
+
1187
+ /// 兼容旧 API:早期版本的简单毛玻璃。
1188
+ @mixin fanui-glass($bg: fn.fanui-alpha(#ffffff, 0.72), $blur: 12px, $saturate: 180%) {
1189
+ background-color: $bg;
1190
+ backdrop-filter: blur($blur) saturate($saturate);
1191
+ -webkit-backdrop-filter: blur($blur) saturate($saturate);
1192
+ }
1193
+
1194
+ /// 关键帧定义包装(便于统一命名)。
1195
+ @mixin fanui-keyframes($name) {
1196
+ @keyframes #{$name} { @content; }
1197
+ }
1198
+
1199
+ /// 减少动效偏好。
1200
+ @mixin fanui-motion-safe() {
1201
+ @media (prefers-reduced-motion: reduce) { @content; }
1202
+ }