@veltra/mobile 1.0.0 → 1.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 (245) hide show
  1. package/package.json +32 -6
  2. package/src/components/_internal/bottom-sheet/bottom-sheet.vue +81 -0
  3. package/src/components/_internal/bottom-sheet/index.ts +1 -0
  4. package/src/components/_internal/bottom-sheet/style.scss +116 -0
  5. package/src/components/_internal/bottom-sheet/style.ts +1 -0
  6. package/src/components/_internal/overlay-container.ts +34 -0
  7. package/src/components/avatar/avatar.vue +44 -0
  8. package/src/components/avatar/index.ts +1 -0
  9. package/src/components/avatar/style.scss +53 -0
  10. package/src/components/avatar/style.ts +1 -0
  11. package/src/components/badge/badge.vue +59 -0
  12. package/src/components/badge/index.ts +1 -0
  13. package/src/components/badge/style.scss +62 -0
  14. package/src/components/badge/style.ts +1 -0
  15. package/src/components/button/button.vue +76 -0
  16. package/src/components/button/index.ts +1 -0
  17. package/src/components/button/style.scss +207 -0
  18. package/src/components/button/style.ts +1 -0
  19. package/src/components/card/card.vue +24 -0
  20. package/src/components/card/index.ts +1 -0
  21. package/src/components/card/style.scss +35 -0
  22. package/src/components/card/style.ts +1 -0
  23. package/src/components/cascade/cascade.vue +595 -0
  24. package/src/components/cascade/index.ts +1 -0
  25. package/src/components/cascade/node.ts +29 -0
  26. package/src/components/cascade/style.scss +424 -0
  27. package/src/components/cascade/style.ts +2 -0
  28. package/src/components/check-tag/check-tag.vue +35 -0
  29. package/src/components/check-tag/index.ts +1 -0
  30. package/src/components/check-tag/style.scss +46 -0
  31. package/src/components/check-tag/style.ts +1 -0
  32. package/src/components/checkbox/checkbox.vue +67 -0
  33. package/src/components/checkbox/index.ts +1 -0
  34. package/src/components/checkbox/style.scss +132 -0
  35. package/src/components/checkbox/style.ts +2 -0
  36. package/src/components/checkbox-group/checkbox-group.vue +94 -0
  37. package/src/components/checkbox-group/index.ts +1 -0
  38. package/src/components/checkbox-group/style.scss +57 -0
  39. package/src/components/checkbox-group/style.ts +2 -0
  40. package/src/components/collapse/collapse-item.vue +134 -0
  41. package/src/components/collapse/collapse.vue +117 -0
  42. package/src/components/collapse/di.ts +18 -0
  43. package/src/components/collapse/index.ts +3 -0
  44. package/src/components/collapse/style.scss +157 -0
  45. package/src/components/collapse/style.ts +1 -0
  46. package/src/components/date-picker/date-picker.vue +326 -0
  47. package/src/components/date-picker/index.ts +1 -0
  48. package/src/components/date-picker/style.scss +236 -0
  49. package/src/components/date-picker/style.ts +2 -0
  50. package/src/components/descriptions/descriptions-item.vue +12 -0
  51. package/src/components/descriptions/descriptions.vue +50 -0
  52. package/src/components/descriptions/index.ts +2 -0
  53. package/src/components/descriptions/style.scss +120 -0
  54. package/src/components/descriptions/style.ts +1 -0
  55. package/src/components/dialog/dialog.vue +143 -0
  56. package/src/components/dialog/index.ts +1 -0
  57. package/src/components/dialog/style.scss +163 -0
  58. package/src/components/dialog/style.ts +4 -0
  59. package/src/components/divider/divider.vue +29 -0
  60. package/src/components/divider/index.ts +1 -0
  61. package/src/components/divider/style.scss +91 -0
  62. package/src/components/divider/style.ts +1 -0
  63. package/src/components/drawer/drawer.vue +168 -0
  64. package/src/components/drawer/index.ts +1 -0
  65. package/src/components/drawer/style.scss +240 -0
  66. package/src/components/drawer/style.ts +2 -0
  67. package/src/components/empty/empty.vue +22 -0
  68. package/src/components/empty/index.ts +1 -0
  69. package/src/components/empty/style.scss +36 -0
  70. package/src/components/empty/style.ts +1 -0
  71. package/src/components/form/form.vue +119 -0
  72. package/src/components/form/index.ts +1 -0
  73. package/src/components/form/is-field-modified.ts +16 -0
  74. package/src/components/form/style.scss +58 -0
  75. package/src/components/form/style.ts +2 -0
  76. package/src/components/form/use-form-fields.ts +115 -0
  77. package/src/components/form/use-node-interceptor.ts +101 -0
  78. package/src/components/form-item/form-item.vue +171 -0
  79. package/src/components/form-item/helper.ts +9 -0
  80. package/src/components/form-item/index.ts +1 -0
  81. package/src/components/form-item/style.scss +121 -0
  82. package/src/components/form-item/style.ts +1 -0
  83. package/src/components/form-item/validate.ts +131 -0
  84. package/src/components/icon/icon.vue +23 -0
  85. package/src/components/icon/index.ts +1 -0
  86. package/src/components/icon/style.scss +37 -0
  87. package/src/components/icon/style.ts +1 -0
  88. package/src/components/input/index.ts +1 -0
  89. package/src/components/input/input.vue +187 -0
  90. package/src/components/input/style.scss +121 -0
  91. package/src/components/input/style.ts +1 -0
  92. package/src/components/list/index.ts +3 -0
  93. package/src/components/list/list-item.vue +20 -0
  94. package/src/components/list/list.vue +20 -0
  95. package/src/components/list/style.scss +50 -0
  96. package/src/components/list/style.ts +1 -0
  97. package/src/components/loading/directive.ts +34 -0
  98. package/src/components/loading/index.ts +3 -0
  99. package/src/components/loading/loading.vue +25 -0
  100. package/src/components/loading/style.scss +179 -0
  101. package/src/components/loading/style.ts +2 -0
  102. package/src/components/message/helper.ts +28 -0
  103. package/src/components/message/index.ts +3 -0
  104. package/src/components/message/message-box.vue +31 -0
  105. package/src/components/message/message.ts +122 -0
  106. package/src/components/message/message.vue +54 -0
  107. package/src/components/message/style.scss +130 -0
  108. package/src/components/message/style.ts +1 -0
  109. package/src/components/message-confirm/index.ts +3 -0
  110. package/src/components/message-confirm/message-confirm-box.vue +36 -0
  111. package/src/components/message-confirm/message-confirm.ts +139 -0
  112. package/src/components/message-confirm/message-confirm.vue +53 -0
  113. package/src/components/message-confirm/style.scss +138 -0
  114. package/src/components/message-confirm/style.ts +1 -0
  115. package/src/components/multi-select/index.ts +1 -0
  116. package/src/components/multi-select/multi-select.vue +410 -0
  117. package/src/components/multi-select/style.scss +376 -0
  118. package/src/components/multi-select/style.ts +2 -0
  119. package/src/components/number-input/index.ts +1 -0
  120. package/src/components/number-input/number-input.vue +407 -0
  121. package/src/components/number-input/style.scss +118 -0
  122. package/src/components/number-input/style.ts +2 -0
  123. package/src/components/password-input/index.ts +1 -0
  124. package/src/components/password-input/password-input.vue +134 -0
  125. package/src/components/password-input/style.scss +40 -0
  126. package/src/components/password-input/style.ts +2 -0
  127. package/src/components/progress/index.ts +1 -0
  128. package/src/components/progress/progress.vue +90 -0
  129. package/src/components/progress/style.scss +73 -0
  130. package/src/components/progress/style.ts +1 -0
  131. package/src/components/radio/index.ts +1 -0
  132. package/src/components/radio/radio.vue +57 -0
  133. package/src/components/radio/style.scss +113 -0
  134. package/src/components/radio/style.ts +1 -0
  135. package/src/components/radio-group/index.ts +1 -0
  136. package/src/components/radio-group/radio-group.vue +60 -0
  137. package/src/components/radio-group/style.scss +29 -0
  138. package/src/components/radio-group/style.ts +2 -0
  139. package/src/components/segment/index.ts +1 -0
  140. package/src/components/segment/segment.vue +121 -0
  141. package/src/components/segment/style.scss +118 -0
  142. package/src/components/segment/style.ts +1 -0
  143. package/src/components/select/index.ts +1 -0
  144. package/src/components/select/select.vue +278 -0
  145. package/src/components/select/style.scss +237 -0
  146. package/src/components/select/style.ts +2 -0
  147. package/src/components/select/use-options.ts +195 -0
  148. package/src/components/skeleton/index.ts +1 -0
  149. package/src/components/skeleton/skeleton.vue +37 -0
  150. package/src/components/skeleton/style.scss +95 -0
  151. package/src/components/skeleton/style.ts +1 -0
  152. package/src/components/space/index.ts +1 -0
  153. package/src/components/space/space.vue +45 -0
  154. package/src/components/space/style.scss +41 -0
  155. package/src/components/space/style.ts +1 -0
  156. package/src/components/steps/index.ts +1 -0
  157. package/src/components/steps/steps.vue +114 -0
  158. package/src/components/steps/style.scss +231 -0
  159. package/src/components/steps/style.ts +1 -0
  160. package/src/components/switch/index.ts +1 -0
  161. package/src/components/switch/style.scss +137 -0
  162. package/src/components/switch/style.ts +1 -0
  163. package/src/components/switch/switch.vue +63 -0
  164. package/src/components/tabs/index.ts +1 -0
  165. package/src/components/tabs/style.scss +285 -0
  166. package/src/components/tabs/style.ts +1 -0
  167. package/src/components/tabs/tabs.vue +127 -0
  168. package/src/components/tag/index.ts +1 -0
  169. package/src/components/tag/style.scss +114 -0
  170. package/src/components/tag/style.ts +1 -0
  171. package/src/components/tag/tag.vue +49 -0
  172. package/src/components/text/index.ts +1 -0
  173. package/src/components/text/style.scss +63 -0
  174. package/src/components/text/style.ts +1 -0
  175. package/src/components/text/text.vue +63 -0
  176. package/src/components/textarea/index.ts +1 -0
  177. package/src/components/textarea/style.scss +124 -0
  178. package/src/components/textarea/style.ts +1 -0
  179. package/src/components/textarea/textarea.vue +121 -0
  180. package/src/components/textarea/utils.ts +11 -0
  181. package/src/components/time-picker/index.ts +1 -0
  182. package/src/components/time-picker/style.scss +216 -0
  183. package/src/components/time-picker/style.ts +2 -0
  184. package/src/components/time-picker/time-picker.vue +263 -0
  185. package/src/components/timeline/index.ts +2 -0
  186. package/src/components/timeline/style.scss +107 -0
  187. package/src/components/timeline/style.ts +1 -0
  188. package/src/components/timeline/timeline-item.vue +43 -0
  189. package/src/components/timeline/timeline.vue +16 -0
  190. package/src/components/uploader/index.ts +1 -0
  191. package/src/components/uploader/style.scss +220 -0
  192. package/src/components/uploader/style.ts +1 -0
  193. package/src/components/uploader/uploader.vue +306 -0
  194. package/src/env.d.ts +7 -0
  195. package/src/index.ts +99 -1
  196. package/src/shared/bem.ts +12 -0
  197. package/src/shared/use-scroll-lock.ts +111 -0
  198. package/src/shared/use-touch-gesture.ts +150 -0
  199. package/src/styles/_tokens.scss +129 -0
  200. package/src/types/avatar.ts +31 -0
  201. package/src/types/badge.ts +28 -0
  202. package/src/types/button.ts +48 -0
  203. package/src/types/card.ts +16 -0
  204. package/src/types/cascade.ts +87 -0
  205. package/src/types/check-tag.ts +24 -0
  206. package/src/types/checkbox-group.ts +20 -0
  207. package/src/types/checkbox.ts +15 -0
  208. package/src/types/collapse.ts +79 -0
  209. package/src/types/date-picker.ts +39 -0
  210. package/src/types/descriptions.ts +22 -0
  211. package/src/types/dialog.ts +54 -0
  212. package/src/types/divider.ts +14 -0
  213. package/src/types/drawer.ts +38 -0
  214. package/src/types/empty.ts +19 -0
  215. package/src/types/form-item.ts +22 -0
  216. package/src/types/form.ts +54 -0
  217. package/src/types/icon.ts +10 -0
  218. package/src/types/input.ts +51 -0
  219. package/src/types/list.ts +13 -0
  220. package/src/types/loading.ts +20 -0
  221. package/src/types/message-confirm.ts +72 -0
  222. package/src/types/message.ts +71 -0
  223. package/src/types/multi-select.ts +38 -0
  224. package/src/types/number-input.ts +32 -0
  225. package/src/types/password-input.ts +19 -0
  226. package/src/types/progress.ts +22 -0
  227. package/src/types/radio-group.ts +33 -0
  228. package/src/types/radio.ts +18 -0
  229. package/src/types/segment.ts +42 -0
  230. package/src/types/select.ts +63 -0
  231. package/src/types/skeleton.ts +29 -0
  232. package/src/types/space.ts +22 -0
  233. package/src/types/steps.ts +67 -0
  234. package/src/types/switch.ts +17 -0
  235. package/src/types/tabs.ts +63 -0
  236. package/src/types/tag.ts +19 -0
  237. package/src/types/text.ts +28 -0
  238. package/src/types/textarea.ts +76 -0
  239. package/src/types/time-picker.ts +40 -0
  240. package/src/types/timeline.ts +15 -0
  241. package/src/types/uploader.ts +50 -0
  242. package/.turbo/turbo-build.log +0 -2
  243. package/.turbo/turbo-check-types.log +0 -1
  244. package/tsconfig.json +0 -12
  245. package/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,137 @@
1
+ @use 'pkg:@veltra/styles/mixins' as m with ($namespace: um);
2
+ @use 'pkg:@veltra/styles/functions' as fn;
3
+ @use '../../styles/tokens' as t;
4
+
5
+ $root-name: switch;
6
+
7
+ @include m.b($root-name) {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ user-select: none;
11
+ vertical-align: middle;
12
+ -webkit-tap-highlight-color: transparent;
13
+ gap: t.spacing(default);
14
+
15
+ @include m.size using ($size) {
16
+ // 拨动热区:整个 label 可点,不小于触控下限;轨道尺寸走 --um-* 移动档
17
+ $touch-size: max(t.control-height($size), t.use-var(touch-target));
18
+ $track: t.switch-track($size);
19
+
20
+ min-height: $touch-size;
21
+ min-width: t.use-var(touch-target);
22
+
23
+ @include m.bem($root-name, button) {
24
+ height: $track;
25
+ width: calc($track * 2);
26
+ }
27
+
28
+ @include m.bem($root-name, thumb) {
29
+ width: calc($track - 4px);
30
+ height: calc($track - 4px);
31
+ }
32
+ }
33
+
34
+ // 状态样式
35
+ @include m.is(disabled) {
36
+ cursor: not-allowed;
37
+
38
+ @include m.bem($root-name, button) {
39
+ background-color: fn.use-var(bg-color, bottom);
40
+ }
41
+
42
+ @include m.bem($root-name, thumb) {
43
+ background-color: fn.use-var(bg-color, middle);
44
+ }
45
+
46
+ @include m.is(checked) {
47
+ @include m.bem($root-name, button) {
48
+ background-color: fn.use-var(color, primary, light-5);
49
+ }
50
+ }
51
+ }
52
+
53
+ @include m.is(readonly) {
54
+ cursor: default;
55
+ }
56
+
57
+ @include m.is-not(disabled, readonly) {
58
+ // label 不可聚焦,ring 由视觉隐藏的原生 input 驱动;移动端无 hover,按压反馈走 :active
59
+ &:active {
60
+ @include m.bem($root-name, button) {
61
+ background-color: fn.use-var(color, primary, light-7);
62
+ border-color: fn.use-var(color, primary, light-5);
63
+ }
64
+
65
+ @include m.is(checked) {
66
+ @include m.bem($root-name, button) {
67
+ background-color: fn.use-var(color, primary, dark-1);
68
+ }
69
+ }
70
+ }
71
+
72
+ .um-#{$root-name}__native:focus-visible ~ .um-#{$root-name}__button {
73
+ box-shadow: fn.use-var(focus-ring);
74
+ }
75
+ }
76
+
77
+ @include m.e(native) {
78
+ position: absolute;
79
+ width: 0;
80
+ height: 0;
81
+ opacity: 0;
82
+ margin: 0;
83
+ pointer-events: none;
84
+ }
85
+
86
+ @include m.e(button) {
87
+ display: flex;
88
+ align-items: center;
89
+ // 胶囊圆角(远大于轨道高度,保证两端全圆)
90
+ border-radius: 999px;
91
+ background-color: fn.use-var(bg-color, bottom);
92
+ border: 1px solid fn.use-var(border, muted-color);
93
+ box-sizing: border-box;
94
+ transition:
95
+ background-color fn.use-var(transition, normal) fn.use-var(transition, ease),
96
+ border-color fn.use-var(transition, normal) fn.use-var(transition, ease),
97
+ box-shadow fn.use-var(transition, normal) fn.use-var(transition, ease);
98
+ padding: 2px;
99
+ flex-shrink: 0;
100
+ }
101
+
102
+ @include m.e(thumb) {
103
+ display: block;
104
+ background-color: fn.use-var(bg-color, top);
105
+ border-radius: 50%;
106
+ box-shadow: fn.use-var(shadow, sm);
107
+ transition: transform fn.use-var(transition, normal) fn.use-var(transition, ease);
108
+ pointer-events: none;
109
+ }
110
+
111
+ @include m.e(text) {
112
+ font-size: t.use-var(font-size-main);
113
+ font-weight: 500;
114
+
115
+ @include m.is(selected) {
116
+ color: fn.use-var(color-primary);
117
+ font-weight: 800;
118
+ }
119
+ }
120
+
121
+ @include m.is(checked) {
122
+ @include m.bem($root-name, button) {
123
+ background-color: fn.use-var(color-primary);
124
+ border-color: fn.use-var(color-primary);
125
+ }
126
+
127
+ // 位移行程 = 内容区剩余宽度(轨道 - 手柄 - 两侧 border/padding 留白),手柄贴右缘
128
+ @include m.bem($root-name, thumb) {
129
+ transform: translateX(calc(100% + 2px));
130
+ }
131
+ }
132
+ }
133
+
134
+ // 嵌入表单行时热区拉满行高,与列表行式布局(--um-form-row-height)对齐
135
+ .um-form-item__content .um-#{$root-name} {
136
+ min-height: t.use-var(form-row-height);
137
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,63 @@
1
+ <template>
2
+ <label :class="switchClass" @click.stop>
3
+ <input
4
+ type="checkbox"
5
+ :class="cls.e('native')"
6
+ :checked="model"
7
+ :disabled="disabled"
8
+ @input="handleInput"
9
+ />
10
+ <span :class="[cls.e('text'), bem.is('selected', !model)]" v-if="inactiveText">
11
+ {{ inactiveText }}
12
+ </span>
13
+
14
+ <div :class="cls.e('button')">
15
+ <span :class="cls.e('thumb')"></span>
16
+ </div>
17
+
18
+ <span v-if="activeText" :class="[cls.e('text'), bem.is('selected', model)]">
19
+ {{ activeText }}
20
+ </span>
21
+ </label>
22
+ </template>
23
+
24
+ <script lang="ts" setup>
25
+ import { useFormFallbackProps } from '@veltra/compositions'
26
+ import { injectFormContext } from '@veltra/utils'
27
+ import { computed } from 'vue'
28
+
29
+ import { bem } from '../../shared/bem'
30
+ import type { SwitchEmits, SwitchProps } from '../../types/switch'
31
+
32
+ defineOptions({ name: 'USwitch' })
33
+
34
+ const props = withDefaults(defineProps<SwitchProps>(), { disabled: undefined, readonly: undefined })
35
+
36
+ const emit = defineEmits<SwitchEmits>()
37
+
38
+ const model = defineModel<boolean>()
39
+
40
+ const cls = bem('switch')
41
+
42
+ const { formProps } = injectFormContext()
43
+
44
+ const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])
45
+
46
+ const switchClass = computed(() => {
47
+ return [
48
+ cls.b,
49
+ cls.m(size.value),
50
+ bem.is('disabled', disabled.value),
51
+ bem.is('readonly', readonly.value),
52
+ bem.is('checked', model.value)
53
+ ]
54
+ })
55
+
56
+ const handleInput = (e: Event) => {
57
+ if (disabled.value || readonly.value) return
58
+
59
+ const newValue = (e.target as HTMLInputElement).checked
60
+ model.value = newValue
61
+ emit('change', newValue)
62
+ }
63
+ </script>
@@ -0,0 +1 @@
1
+ export { default as UTabs } from './tabs.vue'
@@ -0,0 +1,285 @@
1
+ @use 'pkg:@veltra/styles/mixins' as m with ($namespace: um);
2
+ @use 'pkg:@veltra/styles/functions' as fn;
3
+ @use '../../styles/tokens' as t;
4
+
5
+ $ease: cubic-bezier(0.4, 0, 0.2, 1);
6
+
7
+ /* ===== 复合标签页:bar + content 布局容器 ===== */
8
+ @include m.b(tabs) {
9
+ display: flex;
10
+ gap: fn.use-var(gap, small);
11
+ max-width: 100%;
12
+
13
+ @include m.e(content) {
14
+ flex: 1 1 auto;
15
+ width: 100%;
16
+ min-width: 0;
17
+ min-height: 0;
18
+ border-radius: t.control-radius(default);
19
+ }
20
+
21
+ @include m.m(top) {
22
+ flex-direction: column;
23
+ }
24
+
25
+ @include m.m(bottom) {
26
+ flex-direction: column-reverse;
27
+ }
28
+
29
+ @include m.m(left) {
30
+ flex-direction: row;
31
+
32
+ @include m.bem(tabs, content) {
33
+ height: 100%;
34
+ }
35
+ }
36
+
37
+ @include m.m(right) {
38
+ flex-direction: row-reverse;
39
+
40
+ @include m.bem(tabs, content) {
41
+ height: 100%;
42
+ }
43
+ }
44
+ }
45
+
46
+ /* ===== 标签栏:水平 / 垂直,溢出经触屏滑动滚动 ===== */
47
+ $list-bg: fn.component-var(tabs, bar-bg, fn.use-var(bg-color, hover));
48
+ $bar-radius: t.control-radius(default);
49
+ $item-radius: calc(#{t.control-radius(default)} - 2px);
50
+ $bar-padding: 4px;
51
+ $active-bg: fn.component-var(tabs, active-bg, fn.use-var(bg-color, top));
52
+ $active-shadow: fn.use-var(shadow, sm);
53
+ // 默认横向形态的激活下划线高度
54
+ $indicator-height: 3px;
55
+
56
+ @include m.b(tabs-bar) {
57
+ display: inline-flex;
58
+ align-self: flex-start;
59
+ max-width: 100%;
60
+ min-width: 0;
61
+ // 仅水平方向内边距;垂直空间迁移到 __viewport,避免 __viewport 的 overflow 裁切激活态 box-shadow
62
+ padding: 0 $bar-padding;
63
+ // 默认横向形态为下划线风格(透明底);胶囊(rounded)与垂直形态保留卡片底色
64
+ background-color: transparent;
65
+ gap: 2px;
66
+
67
+ @include m.m(horizontal) {
68
+ align-items: center;
69
+ position: relative;
70
+ flex-shrink: 0;
71
+ }
72
+
73
+ @include m.m(vertical) {
74
+ flex-direction: column;
75
+ background-color: $list-bg;
76
+ border-radius: $bar-radius;
77
+ // 垂直方向没有 __viewport 承载左右 padding,这里直接为容器补上,
78
+ // 与水平 __viewport 的 padding: 0 $bar-padding 对称
79
+ padding: $bar-padding 0;
80
+ }
81
+
82
+ @include m.is(rounded) {
83
+ background-color: $list-bg;
84
+ border-radius: $bar-radius;
85
+ }
86
+
87
+ @include m.e(viewport) {
88
+ flex: 1 1 auto;
89
+ min-width: 0;
90
+ // 上下 padding 与 overflow-y: hidden 配合:裁切发生在 padding-box 外沿,
91
+ // 激活 __item 的 box-shadow 得以在 padding 区域内完整渲染
92
+ padding: $bar-padding 0;
93
+ overflow-x: auto;
94
+ overflow-y: hidden;
95
+ scrollbar-width: none;
96
+ -webkit-overflow-scrolling: touch;
97
+ scroll-snap-type: x proximity;
98
+
99
+ &::-webkit-scrollbar {
100
+ display: none;
101
+ }
102
+ }
103
+
104
+ @include m.e(list) {
105
+ display: inline-flex;
106
+ flex-wrap: nowrap;
107
+ flex-shrink: 0;
108
+ width: max-content;
109
+ margin: 0;
110
+ padding: 0;
111
+ gap: 2px;
112
+ }
113
+
114
+ @include m.e(item) {
115
+ cursor: pointer;
116
+ font-weight: 500;
117
+ letter-spacing: 0.25px;
118
+ color: fn.use-var(text-color, main);
119
+ user-select: none;
120
+ position: relative;
121
+ border-radius: $item-radius;
122
+ background-color: transparent;
123
+ box-shadow: none;
124
+ border: none;
125
+ outline: none;
126
+ white-space: nowrap;
127
+ display: inline-flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ gap: 0;
131
+ -webkit-tap-highlight-color: transparent;
132
+ scroll-snap-align: center;
133
+ transition:
134
+ color 0.22s $ease,
135
+ background-color 0.22s $ease,
136
+ box-shadow 0.22s $ease,
137
+ opacity 0.22s $ease;
138
+
139
+ // 按压反馈(移动端无 hover);激活与禁用项无按压态
140
+ &:active:not(.is-active):not(.is-disabled) {
141
+ color: fn.use-var(text-color, title);
142
+ background-color: fn.color-a(color, 5, primary);
143
+ }
144
+
145
+ // 禁用态:压暗 + 禁用配色,视觉上与可点项明确区分
146
+ @include m.is(disabled) {
147
+ color: fn.use-var(text-color, disabled);
148
+ cursor: not-allowed;
149
+ opacity: 0.5;
150
+ }
151
+
152
+ // 默认横向形态的激活指示:主题色下划线(垂直 / 胶囊形态改为卡片高亮)
153
+ @include m.is(active) {
154
+ color: fn.use-var(color, primary);
155
+ font-weight: 600;
156
+
157
+ &::after {
158
+ content: '';
159
+ position: absolute;
160
+ left: t.use-var('spacing-md');
161
+ right: t.use-var('spacing-md');
162
+ bottom: 0;
163
+ height: $indicator-height;
164
+ border-radius: $indicator-height;
165
+ background-color: fn.use-var(color, primary);
166
+ }
167
+ }
168
+
169
+ &:focus-visible {
170
+ box-shadow: fn.use-var(focus-ring);
171
+ }
172
+ }
173
+
174
+ @include m.e(item-label) {
175
+ display: inline-flex;
176
+ align-items: center;
177
+ min-width: 0;
178
+ }
179
+
180
+ // 关闭按钮:移动端无 hover,可关闭时常显;44px 宽 + 拉伸高度保证触控热区
181
+ @include m.e(close) {
182
+ flex: 0 0 auto;
183
+ display: inline-flex;
184
+ align-items: center;
185
+ justify-content: center;
186
+ align-self: stretch;
187
+ width: t.use-var('touch-target');
188
+ margin-left: fn.use-var(gap, small);
189
+ overflow: hidden;
190
+ padding: 0;
191
+ border: none;
192
+ background: transparent;
193
+ color: fn.use-var(text-color, second);
194
+ cursor: pointer;
195
+ border-radius: $item-radius;
196
+ font-size: t.use-var('font-size-auxiliary');
197
+ line-height: 1;
198
+ transition:
199
+ color 0.15s ease,
200
+ background-color 0.15s ease;
201
+
202
+ svg {
203
+ width: 1em;
204
+ height: 1em;
205
+ fill: currentColor;
206
+ }
207
+
208
+ &:active {
209
+ color: fn.use-var(text-color, title);
210
+ background-color: fn.color-a(text-color, 11, title);
211
+ }
212
+ }
213
+
214
+ // 垂直:滚动轴翻转为纵向,item 两端对齐(label 左、close 右)
215
+ @include m.m(vertical) {
216
+ @include m.bem(tabs-bar, viewport) {
217
+ padding: 0 $bar-padding;
218
+ overflow-x: hidden;
219
+ overflow-y: auto;
220
+ scroll-snap-type: y proximity;
221
+ }
222
+
223
+ @include m.bem(tabs-bar, list) {
224
+ flex-direction: column;
225
+ width: auto;
226
+ }
227
+
228
+ @include m.bem(tabs-bar, item) {
229
+ justify-content: space-between;
230
+ scroll-snap-align: center;
231
+
232
+ // 下划线是横向习语,垂直形态激活项沿用卡片高亮
233
+ @include m.is(active) {
234
+ background-color: $active-bg;
235
+ box-shadow: $active-shadow;
236
+
237
+ &::after {
238
+ content: none;
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ // 填充宽度(仅水平布局生效):容器铺满父级,items 自身宽度保持不变
245
+ @include m.is(block) {
246
+ display: flex;
247
+ width: 100%;
248
+ max-width: 100%;
249
+ align-self: stretch;
250
+ }
251
+
252
+ // 触控热区:任何尺寸的标签可点击区域不小于 44×44,字号 / 高度走移动端密度 token
253
+ @include m.size using ($size) {
254
+ @include m.bem(tabs-bar, item) {
255
+ min-height: max(t.control-height($size), t.use-var('touch-target'));
256
+ min-width: t.use-var('touch-target');
257
+ padding: 0 t.spacing($size);
258
+ font-size: t.use-var('font-size-main');
259
+ }
260
+ }
261
+
262
+ // 胶囊风格:item 始终 pill;容器形状按方向分治——
263
+ // 水平容器也变为胶囊;垂直容器保持常规圆角(避免高度较大时胶囊值的形变)。
264
+ @include m.is(rounded) {
265
+ @include m.bem(tabs-bar, item) {
266
+ border-radius: 999px;
267
+
268
+ // 胶囊形态激活项沿用卡片高亮,不用下划线
269
+ @include m.is(active) {
270
+ background-color: $active-bg;
271
+ box-shadow: $active-shadow;
272
+
273
+ &::after {
274
+ content: none;
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ @include m.m(horizontal) {
281
+ @include m.is(rounded) {
282
+ border-radius: 999px;
283
+ }
284
+ }
285
+ }
@@ -0,0 +1 @@
1
+ import './style.scss'
@@ -0,0 +1,127 @@
1
+ <template>
2
+ <div :class="[cls.b, cls.m(position)]">
3
+ <div :class="barClassList">
4
+ <div ref="viewportRef" :class="barCls.e('viewport')">
5
+ <div ref="listRef" :class="barCls.e('list')">
6
+ <button
7
+ v-for="(item, index) in items"
8
+ :key="item.key"
9
+ type="button"
10
+ role="tab"
11
+ :class="[
12
+ barCls.e('item'),
13
+ bem.is('active', model === item.key),
14
+ bem.is('disabled', item.disabled === true)
15
+ ]"
16
+ @click.stop="handleClick(item, index)"
17
+ >
18
+ <slot v-bind="{ item, index }">
19
+ <span :class="barCls.e('item-label')">{{ item.name ?? item.key }}</span>
20
+ </slot>
21
+
22
+ <span
23
+ v-if="isItemClosable(item)"
24
+ role="button"
25
+ tabindex="0"
26
+ :class="barCls.e('close')"
27
+ :aria-label="`close ${item.name ?? item.key}`"
28
+ @click.stop="handleClose(item, index)"
29
+ >
30
+ <Close />
31
+ </span>
32
+ </button>
33
+ </div>
34
+ </div>
35
+ </div>
36
+
37
+ <KeepAlive v-if="keepAlive">
38
+ <component :key="model" :is="renderContent()" />
39
+ </KeepAlive>
40
+ <component v-else :key="model" :is="renderContent()" />
41
+ </div>
42
+ </template>
43
+
44
+ <script lang="ts" setup>
45
+ import { Close } from '@veltra/icons/normal'
46
+ import { computed, createVNode, nextTick, onMounted, shallowRef, watch } from 'vue'
47
+
48
+ import { bem } from '../../shared/bem'
49
+ import type { TabItem, TabsEmits, TabsProps } from '../../types/tabs'
50
+
51
+ defineOptions({ name: 'UTabs' })
52
+
53
+ const props = withDefaults(defineProps<TabsProps>(), {
54
+ position: 'top',
55
+ closable: false,
56
+ block: false,
57
+ rounded: false,
58
+ keepAlive: false
59
+ })
60
+
61
+ const emit = defineEmits<TabsEmits>()
62
+
63
+ const slots = defineSlots<Record<string, (props: any) => any>>()
64
+
65
+ const cls = bem('tabs')
66
+ const barCls = bem('tabs-bar')
67
+
68
+ const model = defineModel<string>()
69
+
70
+ const isHorizontal = computed(() => props.position === 'top' || props.position === 'bottom')
71
+
72
+ const barClassList = computed(() => [
73
+ barCls.b,
74
+ barCls.m(isHorizontal.value ? 'horizontal' : 'vertical'),
75
+ barCls.m(props.position),
76
+ barCls.m(props.size ?? 'default'),
77
+ bem.is('block', props.block),
78
+ bem.is('rounded', props.rounded)
79
+ ])
80
+
81
+ const isItemClosable = (item: TabItem) => {
82
+ if (item.disabled) return false
83
+ return item.closable ?? props.closable
84
+ }
85
+
86
+ const handleClick = (item: TabItem, index: number) => {
87
+ if (item.disabled) return
88
+ model.value = item.key
89
+ emit('click', item, index)
90
+ }
91
+
92
+ const handleClose = (item: TabItem, index: number) => {
93
+ if (item.disabled) return
94
+ emit('close', item, index)
95
+ }
96
+
97
+ const renderContent = () => {
98
+ const key = model.value
99
+ if (!key) return null
100
+ const nodes = slots[key]?.({ key })
101
+ if (!nodes) return null
102
+ return createVNode('div', { class: cls.e('content') }, nodes)
103
+ }
104
+
105
+ const viewportRef = shallowRef<HTMLElement>()
106
+ const listRef = shallowRef<HTMLElement>()
107
+
108
+ /** 活动标签自动滚入视野:触屏滑动替代 desktop 的溢出导航按钮 */
109
+ const ensureActiveVisible = async () => {
110
+ await nextTick()
111
+ const list = listRef.value
112
+ if (!list) return
113
+ const index = props.items.findIndex((i) => i.key === model.value)
114
+ const el = list.children[index] as HTMLElement | undefined
115
+ if (!el) return
116
+ el.scrollIntoView({
117
+ behavior: 'smooth',
118
+ // 滚动轴居中展示活动标签,非滚动轴 nearest 避免带动页面滚动
119
+ inline: isHorizontal.value ? 'center' : 'nearest',
120
+ block: isHorizontal.value ? 'nearest' : 'center'
121
+ })
122
+ }
123
+
124
+ watch([model, () => props.items], ensureActiveVisible)
125
+
126
+ onMounted(ensureActiveVisible)
127
+ </script>
@@ -0,0 +1 @@
1
+ export { default as UTag } from './tag.vue'