@yunlefun/vue 0.3.0 → 0.4.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.
- package/README.md +2 -0
- package/components/YlfAccordion.vue +1 -1
- package/components/YlfAvatar.vue +2 -1
- package/components/YlfDialog.vue +23 -2
- package/components/YlfDropdownMenu.vue +20 -9
- package/components/YlfPopover.vue +5 -2
- package/components/YlfProgress.vue +29 -5
- package/components/YlfRadioGroup.vue +20 -2
- package/components/YlfSegmentedControl.vue +1 -1
- package/components/YlfSelect.vue +8 -2
- package/components/YlfSlider.vue +32 -2
- package/components/YlfTabs.vue +1 -1
- package/components/YlfTooltip.vue +5 -2
- package/components/accordion/demo.vue +1 -1
- package/components/accordion/index.md +2 -0
- package/components/avatar/index.md +3 -1
- package/components/checkbox/demo.vue +10 -9
- package/components/checkbox/index.md +2 -0
- package/components/dialog/index.md +9 -6
- package/components/dropdown-menu/index.md +9 -3
- package/components/popover/demo.vue +1 -1
- package/components/popover/index.md +8 -5
- package/components/progress/index.md +3 -1
- package/components/radio/demo.vue +1 -1
- package/components/radio/index.md +2 -0
- package/components/segmented-control/index.md +1 -1
- package/components/select/index.md +6 -0
- package/components/slider/demo.vue +2 -2
- package/components/slider/index.md +15 -7
- package/components/switch/index.md +2 -0
- package/components/tabs/index.md +2 -0
- package/components/tooltip/index.md +9 -5
- package/dist/components/YlfAccordion.vue.d.ts +35 -0
- package/dist/components/YlfAiPromptLauncher.vue.d.ts +25 -0
- package/dist/components/YlfAvatar.vue.d.ts +14 -0
- package/dist/components/YlfBadge.vue.d.ts +29 -0
- package/dist/components/YlfBottomMenu.vue.d.ts +19 -0
- package/dist/components/YlfBottomMenuItem.vue.d.ts +22 -0
- package/dist/components/YlfButton.vue.d.ts +48 -0
- package/dist/components/YlfCard.vue.d.ts +28 -0
- package/dist/components/YlfCheckbox.vue.d.ts +18 -0
- package/dist/components/YlfDialog.vue.d.ts +37 -0
- package/dist/components/YlfDropdownMenu.vue.d.ts +34 -0
- package/dist/components/YlfLogo.vue.d.ts +13 -0
- package/dist/components/YlfPopover.vue.d.ts +31 -0
- package/dist/components/YlfProgress.vue.d.ts +17 -0
- package/dist/components/YlfRadioGroup.vue.d.ts +24 -0
- package/dist/components/YlfSegmentedControl.vue.d.ts +35 -0
- package/dist/components/YlfSelect.vue.d.ts +43 -0
- package/dist/components/YlfSeparator.vue.d.ts +16 -0
- package/dist/components/YlfSlider.vue.d.ts +29 -0
- package/dist/components/YlfSwitch.vue.d.ts +24 -0
- package/dist/components/YlfTabs.vue.d.ts +32 -0
- package/dist/components/YlfTooltip.vue.d.ts +26 -0
- package/dist/components/ai-prompt.d.ts +33 -0
- package/dist/components/ai-provider-logos.d.ts +3 -0
- package/dist/components/bottom-menu/index.d.ts +20 -0
- package/dist/components/segmented-control.d.ts +8 -0
- package/dist/components/theme.d.ts +4 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ export default defineNuxtConfig({
|
|
|
38
38
|
|
|
39
39
|
根入口目前提供类型和组件目录信息,组件通过 `components/*.vue` 子路径或 Nuxt 模块使用;没有公开的 `YlfResolver`。
|
|
40
40
|
|
|
41
|
+
组件子路径提供预生成类型声明,运行时继续使用原始 SFC。现有 15 个 Reka UI 封装的验收范围见[组件验收](../guide/component-acceptance.md)。
|
|
42
|
+
|
|
41
43
|
更多内容见 [Vue 接入](./index.md)、[子包职责](../guide/packages.md)与[设计体系](../guide/design-system.md)。
|
|
42
44
|
|
|
43
45
|
## AI 提示词入口
|
package/components/YlfAvatar.vue
CHANGED
|
@@ -10,13 +10,14 @@ withDefaults(defineProps<{
|
|
|
10
10
|
}>(), {
|
|
11
11
|
size: 'md',
|
|
12
12
|
fallback: '',
|
|
13
|
+
alt: '',
|
|
13
14
|
})
|
|
14
15
|
</script>
|
|
15
16
|
|
|
16
17
|
<template>
|
|
17
18
|
<AvatarRoot class="ylf-avatar" :class="`ylf-avatar--${size}`">
|
|
18
19
|
<AvatarImage v-if="src" :src="src" :alt="alt" class="ylf-avatar__img" />
|
|
19
|
-
<AvatarFallback class="ylf-avatar__fallback" :delay-ms="200">
|
|
20
|
+
<AvatarFallback class="ylf-avatar__fallback" :delay-ms="200" :role="alt || fallback ? 'img' : undefined" :aria-label="alt || fallback || undefined">
|
|
20
21
|
{{ fallback }}
|
|
21
22
|
</AvatarFallback>
|
|
22
23
|
</AvatarRoot>
|
package/components/YlfDialog.vue
CHANGED
|
@@ -13,11 +13,15 @@ import {
|
|
|
13
13
|
} from 'reka-ui'
|
|
14
14
|
import { computed, useSlots } from 'vue'
|
|
15
15
|
|
|
16
|
+
defineOptions({ inheritAttrs: false })
|
|
17
|
+
|
|
16
18
|
const props = withDefaults(defineProps<{
|
|
17
19
|
title?: string
|
|
18
20
|
description?: string
|
|
19
21
|
/** 没有可见标题时供屏幕阅读器使用 */
|
|
20
22
|
accessibleTitle?: string
|
|
23
|
+
/** Existing container in the same theme boundary. */
|
|
24
|
+
portalTo?: string | HTMLElement
|
|
21
25
|
}>(), {
|
|
22
26
|
accessibleTitle: '对话框',
|
|
23
27
|
})
|
|
@@ -37,9 +41,9 @@ const descriptionProps = computed(() => hasDescription.value
|
|
|
37
41
|
<slot name="trigger" />
|
|
38
42
|
</DialogTrigger>
|
|
39
43
|
|
|
40
|
-
<DialogPortal>
|
|
44
|
+
<DialogPortal :to="portalTo">
|
|
41
45
|
<DialogOverlay class="ylf-dialog__overlay" />
|
|
42
|
-
<DialogContent class="ylf-dialog__content" v-bind="descriptionProps">
|
|
46
|
+
<DialogContent class="ylf-dialog__content" v-bind="{ ...descriptionProps, ...$attrs }">
|
|
43
47
|
<DialogTitle v-if="hasVisibleTitle" class="ylf-dialog__title">
|
|
44
48
|
<slot name="title">
|
|
45
49
|
{{ title }}
|
|
@@ -110,6 +114,8 @@ const descriptionProps = computed(() => hasDescription.value
|
|
|
110
114
|
|
|
111
115
|
.ylf-dialog__title {
|
|
112
116
|
margin: 0 0 6px;
|
|
117
|
+
padding-right: 32px;
|
|
118
|
+
overflow-wrap: anywhere;
|
|
113
119
|
font-family: var(--ylf-font-heading, inherit);
|
|
114
120
|
font-size: 19px;
|
|
115
121
|
font-weight: 700;
|
|
@@ -118,6 +124,7 @@ const descriptionProps = computed(() => hasDescription.value
|
|
|
118
124
|
|
|
119
125
|
.ylf-dialog__desc {
|
|
120
126
|
margin: 0 0 16px;
|
|
127
|
+
overflow-wrap: anywhere;
|
|
121
128
|
font-size: 14px;
|
|
122
129
|
line-height: 1.6;
|
|
123
130
|
color: var(--ylf-c-text-2, #475569);
|
|
@@ -146,12 +153,26 @@ const descriptionProps = computed(() => hasDescription.value
|
|
|
146
153
|
color: var(--ylf-c-text, #0f172a);
|
|
147
154
|
}
|
|
148
155
|
|
|
156
|
+
&:focus-visible {
|
|
157
|
+
outline: 2px solid var(--ylf-c-brand, #2563eb);
|
|
158
|
+
outline-offset: 2px;
|
|
159
|
+
}
|
|
160
|
+
|
|
149
161
|
svg {
|
|
150
162
|
width: 18px;
|
|
151
163
|
height: 18px;
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
166
|
|
|
167
|
+
@media (pointer: coarse) {
|
|
168
|
+
.ylf-dialog__close {
|
|
169
|
+
top: 10px;
|
|
170
|
+
right: 10px;
|
|
171
|
+
width: 44px;
|
|
172
|
+
height: 44px;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
@keyframes ylf-dialog-overlay-in {
|
|
156
177
|
from {
|
|
157
178
|
opacity: 0;
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuRoot, DropdownMenuSeparator, DropdownMenuTrigger } from 'reka-ui'
|
|
3
3
|
|
|
4
|
+
defineOptions({ inheritAttrs: false })
|
|
5
|
+
|
|
6
|
+
defineProps<{
|
|
7
|
+
items: readonly MenuItem[]
|
|
8
|
+
portalTo?: string | HTMLElement
|
|
9
|
+
}>()
|
|
10
|
+
|
|
11
|
+
const emit = defineEmits<{ select: [value: string] }>()
|
|
12
|
+
|
|
4
13
|
interface MenuItem {
|
|
5
14
|
label?: string
|
|
6
15
|
value?: string
|
|
@@ -9,27 +18,23 @@ interface MenuItem {
|
|
|
9
18
|
separator?: boolean
|
|
10
19
|
}
|
|
11
20
|
|
|
12
|
-
|
|
13
|
-
items: MenuItem[]
|
|
14
|
-
}>()
|
|
15
|
-
|
|
16
|
-
const emit = defineEmits<{ select: [value: string] }>()
|
|
21
|
+
const open = defineModel<boolean>('open', { default: false })
|
|
17
22
|
</script>
|
|
18
23
|
|
|
19
24
|
<template>
|
|
20
|
-
<DropdownMenuRoot>
|
|
25
|
+
<DropdownMenuRoot v-model:open="open">
|
|
21
26
|
<DropdownMenuTrigger as-child>
|
|
22
27
|
<slot name="trigger" />
|
|
23
28
|
</DropdownMenuTrigger>
|
|
24
|
-
<DropdownMenuPortal>
|
|
25
|
-
<DropdownMenuContent class="ylf-menu" :side-offset="6" align="start">
|
|
29
|
+
<DropdownMenuPortal :to="portalTo">
|
|
30
|
+
<DropdownMenuContent v-bind="$attrs" class="ylf-menu" :side-offset="6" align="start">
|
|
26
31
|
<template v-for="(it, i) in items" :key="i">
|
|
27
32
|
<DropdownMenuSeparator v-if="it.separator" class="ylf-menu__sep" />
|
|
28
33
|
<DropdownMenuItem
|
|
29
34
|
v-else
|
|
30
35
|
class="ylf-menu__item"
|
|
31
36
|
:disabled="it.disabled"
|
|
32
|
-
@select="it.value && emit('select', it.value)"
|
|
37
|
+
@select="it.value !== undefined && emit('select', it.value)"
|
|
33
38
|
>
|
|
34
39
|
{{ it.label }}
|
|
35
40
|
</DropdownMenuItem>
|
|
@@ -83,6 +88,12 @@ const emit = defineEmits<{ select: [value: string] }>()
|
|
|
83
88
|
}
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
@media (pointer: coarse) {
|
|
92
|
+
.ylf-menu__item {
|
|
93
|
+
min-height: 44px;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
@keyframes ylf-pop-in {
|
|
87
98
|
from {
|
|
88
99
|
opacity: 0;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { PopoverArrow, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
|
|
3
3
|
|
|
4
|
+
defineOptions({ inheritAttrs: false })
|
|
5
|
+
|
|
4
6
|
withDefaults(defineProps<{
|
|
5
7
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
6
8
|
align?: 'start' | 'center' | 'end'
|
|
9
|
+
portalTo?: string | HTMLElement
|
|
7
10
|
}>(), {
|
|
8
11
|
side: 'bottom',
|
|
9
12
|
align: 'center',
|
|
@@ -17,8 +20,8 @@ const open = defineModel<boolean>('open', { default: false })
|
|
|
17
20
|
<PopoverTrigger as-child>
|
|
18
21
|
<slot name="trigger" />
|
|
19
22
|
</PopoverTrigger>
|
|
20
|
-
<PopoverPortal>
|
|
21
|
-
<PopoverContent :side="side" :align="align" :side-offset="8" class="ylf-popover">
|
|
23
|
+
<PopoverPortal :to="portalTo">
|
|
24
|
+
<PopoverContent v-bind="$attrs" :side="side" :align="align" :side-offset="8" class="ylf-popover">
|
|
22
25
|
<slot />
|
|
23
26
|
<PopoverArrow class="ylf-popover__arrow" :width="12" :height="6" />
|
|
24
27
|
</PopoverContent>
|
|
@@ -4,7 +4,8 @@ import { ProgressIndicator, ProgressRoot } from 'reka-ui'
|
|
|
4
4
|
import { computed } from 'vue'
|
|
5
5
|
|
|
6
6
|
const props = withDefaults(defineProps<{
|
|
7
|
-
|
|
7
|
+
/** null means that progress is unknown. */
|
|
8
|
+
value?: number | null
|
|
8
9
|
max?: number
|
|
9
10
|
/** 进度条填充:brand 主色或 accent 纯色;aurora 是 accent 的旧别名 */
|
|
10
11
|
variant?: 'brand' | 'accent' | 'aurora'
|
|
@@ -16,20 +17,24 @@ const props = withDefaults(defineProps<{
|
|
|
16
17
|
tone: 'blue',
|
|
17
18
|
})
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
+
const safeMax = computed(() => Number.isFinite(props.max) && props.max > 0 ? props.max : 100)
|
|
21
|
+
const safeValue = computed(() => props.value === null || !Number.isFinite(props.value)
|
|
22
|
+
? null
|
|
23
|
+
: Math.max(0, Math.min(safeMax.value, props.value)))
|
|
24
|
+
const percent = computed(() => safeValue.value === null ? null : safeValue.value / safeMax.value * 100)
|
|
20
25
|
</script>
|
|
21
26
|
|
|
22
27
|
<template>
|
|
23
28
|
<ProgressRoot
|
|
24
|
-
:model-value="
|
|
25
|
-
:max="
|
|
29
|
+
:model-value="safeValue"
|
|
30
|
+
:max="safeMax"
|
|
26
31
|
class="ylf-progress"
|
|
27
32
|
:data-ylf-tone="tone"
|
|
28
33
|
:class="`ylf-progress--${variant}`"
|
|
29
34
|
>
|
|
30
35
|
<ProgressIndicator
|
|
31
36
|
class="ylf-progress__indicator"
|
|
32
|
-
:style="{ transform: `translateX(-${100 - percent}%)` }"
|
|
37
|
+
:style="{ transform: percent === null ? undefined : `translateX(-${100 - percent}%)` }"
|
|
33
38
|
/>
|
|
34
39
|
</ProgressRoot>
|
|
35
40
|
</template>
|
|
@@ -52,6 +57,20 @@ const percent = computed(() => Math.max(0, Math.min(100, (props.value / props.ma
|
|
|
52
57
|
transition: transform 0.4s var(--ylf-ease-bounce, cubic-bezier(0.34, 1.56, 0.64, 1));
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
.ylf-progress[data-state='indeterminate'] .ylf-progress__indicator {
|
|
61
|
+
width: 40%;
|
|
62
|
+
animation: ylf-progress-indeterminate 1.4s ease-in-out infinite;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@keyframes ylf-progress-indeterminate {
|
|
66
|
+
from {
|
|
67
|
+
transform: translateX(-100%);
|
|
68
|
+
}
|
|
69
|
+
to {
|
|
70
|
+
transform: translateX(250%);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
.ylf-progress--accent .ylf-progress__indicator,
|
|
56
75
|
.ylf-progress--aurora .ylf-progress__indicator {
|
|
57
76
|
background: var(--ylf-accent, var(--ylf-c-brand, #2563eb));
|
|
@@ -61,5 +80,10 @@ const percent = computed(() => Math.max(0, Math.min(100, (props.value / props.ma
|
|
|
61
80
|
.ylf-progress__indicator {
|
|
62
81
|
transition: none;
|
|
63
82
|
}
|
|
83
|
+
|
|
84
|
+
.ylf-progress[data-state='indeterminate'] .ylf-progress__indicator {
|
|
85
|
+
animation: none;
|
|
86
|
+
transform: translateX(75%);
|
|
87
|
+
}
|
|
64
88
|
}
|
|
65
89
|
</style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
// 行为 / 可访问性由 reka-ui 提供(role=radiogroup、方向键 roving focus);皮肤走 token。
|
|
3
3
|
import { RadioGroupIndicator, RadioGroupItem, RadioGroupRoot } from 'reka-ui'
|
|
4
|
+
import { nextTick } from 'vue'
|
|
4
5
|
|
|
5
6
|
interface RadioOption {
|
|
6
7
|
label: string
|
|
@@ -8,8 +9,8 @@ interface RadioOption {
|
|
|
8
9
|
disabled?: boolean
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
withDefaults(defineProps<{
|
|
12
|
-
options: RadioOption[]
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
|
+
options: readonly RadioOption[]
|
|
13
14
|
disabled?: boolean
|
|
14
15
|
orientation?: 'vertical' | 'horizontal'
|
|
15
16
|
}>(), {
|
|
@@ -18,6 +19,22 @@ withDefaults(defineProps<{
|
|
|
18
19
|
})
|
|
19
20
|
|
|
20
21
|
const model = defineModel<string>()
|
|
22
|
+
|
|
23
|
+
function selectFocusedRadio(event: KeyboardEvent) {
|
|
24
|
+
if (props.disabled || !event.defaultPrevented || !['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key))
|
|
25
|
+
return
|
|
26
|
+
const group = event.currentTarget as HTMLElement
|
|
27
|
+
// Reka 2.10.x defers selection until after focus; a fast keyup can clear its flag.
|
|
28
|
+
// Let Reka move focus and skip disabled items, then synchronize that choice.
|
|
29
|
+
nextTick(() => {
|
|
30
|
+
const focused = document.activeElement
|
|
31
|
+
if (!(focused instanceof HTMLElement) || !group.contains(focused) || focused.getAttribute('role') !== 'radio')
|
|
32
|
+
return
|
|
33
|
+
const option = props.options.find(option => option.value === focused.getAttribute('value'))
|
|
34
|
+
if (option && !option.disabled && model.value !== option.value)
|
|
35
|
+
model.value = option.value
|
|
36
|
+
})
|
|
37
|
+
}
|
|
21
38
|
</script>
|
|
22
39
|
|
|
23
40
|
<template>
|
|
@@ -27,6 +44,7 @@ const model = defineModel<string>()
|
|
|
27
44
|
:orientation="orientation"
|
|
28
45
|
class="ylf-radio-group"
|
|
29
46
|
:class="`ylf-radio-group--${orientation}`"
|
|
47
|
+
@keydown="selectFocusedRadio"
|
|
30
48
|
>
|
|
31
49
|
<RadioGroupItem
|
|
32
50
|
v-for="opt in options"
|
|
@@ -155,7 +155,7 @@ function updateValue(value: unknown): void {
|
|
|
155
155
|
|
|
156
156
|
.ylf-segmented__count {
|
|
157
157
|
min-width: 16px;
|
|
158
|
-
color: var(--ylf-c-text-
|
|
158
|
+
color: var(--ylf-c-text-2, #475569);
|
|
159
159
|
font-size: 10px;
|
|
160
160
|
font-variant-numeric: tabular-nums;
|
|
161
161
|
text-align: center;
|
package/components/YlfSelect.vue
CHANGED
|
@@ -11,10 +11,16 @@ const props = withDefaults(defineProps<{
|
|
|
11
11
|
size?: 'sm' | 'md'
|
|
12
12
|
placeholder?: string
|
|
13
13
|
disabled?: boolean
|
|
14
|
+
/** Native form field name and validation. */
|
|
15
|
+
name?: string
|
|
16
|
+
required?: boolean
|
|
17
|
+
/** Existing container in the same theme boundary. */
|
|
18
|
+
portalTo?: string | HTMLElement
|
|
14
19
|
}>(), {
|
|
15
20
|
size: 'md',
|
|
16
21
|
placeholder: '请选择',
|
|
17
22
|
disabled: false,
|
|
23
|
+
required: false,
|
|
18
24
|
})
|
|
19
25
|
|
|
20
26
|
const model = defineModel<T>()
|
|
@@ -22,7 +28,7 @@ const selectedOption = computed(() => props.options.find(option => option.value
|
|
|
22
28
|
</script>
|
|
23
29
|
|
|
24
30
|
<template>
|
|
25
|
-
<SelectRoot v-model="model" :disabled="disabled">
|
|
31
|
+
<SelectRoot v-model="model" :disabled="disabled" :name="name" :required="required">
|
|
26
32
|
<SelectTrigger class="ylf-select__trigger" :data-size="size" :aria-label="$attrs.id || $attrs['aria-labelledby'] ? undefined : placeholder" v-bind="$attrs">
|
|
27
33
|
<SelectValue :placeholder="placeholder" class="ylf-select__label">
|
|
28
34
|
<span v-if="selectedOption?.color" class="ylf-select__swatch" :style="{ backgroundColor: selectedOption.color }" aria-hidden="true" />
|
|
@@ -36,7 +42,7 @@ const selectedOption = computed(() => props.options.find(option => option.value
|
|
|
36
42
|
</slot>
|
|
37
43
|
</SelectIcon>
|
|
38
44
|
</SelectTrigger>
|
|
39
|
-
<SelectPortal>
|
|
45
|
+
<SelectPortal :to="portalTo">
|
|
40
46
|
<SelectContent class="ylf-select__content" :data-size="size" position="popper" :side-offset="6">
|
|
41
47
|
<SelectViewport class="ylf-select__viewport">
|
|
42
48
|
<SelectItem
|
package/components/YlfSlider.vue
CHANGED
|
@@ -6,13 +6,19 @@ withDefaults(defineProps<{
|
|
|
6
6
|
max?: number
|
|
7
7
|
step?: number
|
|
8
8
|
disabled?: boolean
|
|
9
|
+
/** Accessible name for a single value or the range as a whole. */
|
|
10
|
+
label?: string
|
|
11
|
+
/** Per-thumb accessible names, such as minimum and maximum price. */
|
|
12
|
+
thumbLabels?: readonly string[]
|
|
9
13
|
}>(), {
|
|
10
14
|
min: 0,
|
|
11
15
|
max: 100,
|
|
12
16
|
step: 1,
|
|
13
17
|
disabled: false,
|
|
18
|
+
label: '数值',
|
|
14
19
|
})
|
|
15
20
|
|
|
21
|
+
const emit = defineEmits<{ valueCommit: [value: number[]] }>()
|
|
16
22
|
const model = defineModel<number[]>({ default: () => [50] })
|
|
17
23
|
</script>
|
|
18
24
|
|
|
@@ -24,11 +30,17 @@ const model = defineModel<number[]>({ default: () => [50] })
|
|
|
24
30
|
:step="step"
|
|
25
31
|
:disabled="disabled"
|
|
26
32
|
class="ylf-slider"
|
|
33
|
+
@value-commit="emit('valueCommit', $event)"
|
|
27
34
|
>
|
|
28
35
|
<SliderTrack class="ylf-slider__track">
|
|
29
36
|
<SliderRange class="ylf-slider__range" />
|
|
30
37
|
</SliderTrack>
|
|
31
|
-
<SliderThumb
|
|
38
|
+
<SliderThumb
|
|
39
|
+
v-for="(_, i) in model"
|
|
40
|
+
:key="i"
|
|
41
|
+
class="ylf-slider__thumb"
|
|
42
|
+
:aria-label="thumbLabels?.[i] || (model.length === 1 ? label : `${label} ${i + 1}`)"
|
|
43
|
+
/>
|
|
32
44
|
</SliderRoot>
|
|
33
45
|
</template>
|
|
34
46
|
|
|
@@ -66,7 +78,7 @@ const model = defineModel<number[]>({ default: () => [50] })
|
|
|
66
78
|
width: 18px;
|
|
67
79
|
height: 18px;
|
|
68
80
|
border-radius: 50%;
|
|
69
|
-
background: #fff;
|
|
81
|
+
background: var(--ylf-c-surface, #fff);
|
|
70
82
|
border: 2px solid var(--ylf-c-brand, #2563eb);
|
|
71
83
|
box-shadow: var(--ylf-shadow-sm, 0 2px 8px -2px rgba(15, 23, 42, 0.12));
|
|
72
84
|
cursor: grab;
|
|
@@ -86,4 +98,22 @@ const model = defineModel<number[]>({ default: () => [50] })
|
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
100
|
}
|
|
101
|
+
|
|
102
|
+
@media (pointer: coarse) {
|
|
103
|
+
.ylf-slider {
|
|
104
|
+
height: 44px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.ylf-slider__thumb::before {
|
|
108
|
+
position: absolute;
|
|
109
|
+
inset: -13px;
|
|
110
|
+
content: '';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@media (prefers-reduced-motion: reduce) {
|
|
115
|
+
.ylf-slider__thumb {
|
|
116
|
+
transition: none;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
89
119
|
</style>
|
package/components/YlfTabs.vue
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
// 行为 / 定位 / a11y 由 reka-ui 提供;皮肤走 token。
|
|
3
3
|
import { TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger } from 'reka-ui'
|
|
4
4
|
|
|
5
|
+
defineOptions({ inheritAttrs: false })
|
|
6
|
+
|
|
5
7
|
withDefaults(defineProps<{
|
|
6
8
|
content?: string
|
|
7
9
|
side?: 'top' | 'right' | 'bottom' | 'left'
|
|
8
10
|
delay?: number
|
|
9
11
|
disabled?: boolean
|
|
12
|
+
portalTo?: string | HTMLElement
|
|
10
13
|
}>(), {
|
|
11
14
|
side: 'top',
|
|
12
15
|
delay: 300,
|
|
@@ -17,10 +20,10 @@ withDefaults(defineProps<{
|
|
|
17
20
|
<template>
|
|
18
21
|
<TooltipProvider :delay-duration="delay">
|
|
19
22
|
<TooltipRoot :disabled="disabled">
|
|
20
|
-
<TooltipTrigger as-child>
|
|
23
|
+
<TooltipTrigger as-child v-bind="$attrs">
|
|
21
24
|
<slot />
|
|
22
25
|
</TooltipTrigger>
|
|
23
|
-
<TooltipPortal>
|
|
26
|
+
<TooltipPortal :to="portalTo">
|
|
24
27
|
<TooltipContent :side="side" :side-offset="6" class="ylf-tooltip">
|
|
25
28
|
<slot name="content">
|
|
26
29
|
{{ content }}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
const items = [
|
|
3
3
|
{ value: 'a', title: '什么是 YunLeFun Design?', content: '云乐坊设计系统,统一设计规范、品牌视觉与 UI 实现;默认晴空蓝为主,高饱和纯色点缀。' },
|
|
4
|
-
{ value: 'b', title: '为什么基于 reka-ui?', content: '
|
|
4
|
+
{ value: 'b', title: '为什么基于 reka-ui?', content: 'Reka UI 提供键盘、焦点和 ARIA 行为,云乐坊提供产品接口与主题;封装后的场景仍需逐项验收。' },
|
|
5
5
|
{ value: 'c', title: '支持暗色吗?', content: '晴空白与深蓝夜空两套中性色,token 驱动,零配置随主题切换。' },
|
|
6
6
|
]
|
|
7
7
|
</script>
|
|
@@ -10,6 +10,8 @@ title_zh: 头像
|
|
|
10
10
|
| 属性 | 说明 | 类型 | 默认值 |
|
|
11
11
|
| ---------- | ---------------------- | ---------------- | ------ |
|
|
12
12
|
| `src` | 图片地址 | `string` | — |
|
|
13
|
-
| `alt` | 替代文字 | `string` |
|
|
13
|
+
| `alt` | 替代文字 | `string` | `''` |
|
|
14
14
|
| `fallback` | 回退文字(如姓名首字) | `string` | — |
|
|
15
15
|
| `size` | 尺寸 | `sm \| md \| lg` | `md` |
|
|
16
|
+
|
|
17
|
+
有意义的头像使用完整 `alt`(例如用户姓名);图片缺失时,回退图像也使用该名称。没有 `alt` 时回退文字作为名称;仅装饰的图片可保留空 `alt`。`fallback` 决定显示的文字,不会从图片地址自动推断姓名。
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { ref, useId } from 'vue'
|
|
3
3
|
|
|
4
4
|
const a = ref(true)
|
|
5
5
|
const b = ref(false)
|
|
6
6
|
const c = ref<boolean | 'indeterminate'>('indeterminate')
|
|
7
|
+
const id = useId()
|
|
7
8
|
</script>
|
|
8
9
|
|
|
9
10
|
<template>
|
|
10
11
|
<div class="flex flex-col gap-4">
|
|
11
12
|
<div class="flex items-center gap-2">
|
|
12
|
-
<YlfCheckbox v-model="a" />
|
|
13
|
-
<
|
|
13
|
+
<YlfCheckbox :id="`${id}-checked`" v-model="a" />
|
|
14
|
+
<label :for="`${id}-checked`" class="text-sm">已勾选</label>
|
|
14
15
|
</div>
|
|
15
16
|
<div class="flex items-center gap-2">
|
|
16
|
-
<YlfCheckbox v-model="b" />
|
|
17
|
-
<
|
|
17
|
+
<YlfCheckbox :id="`${id}-unchecked`" v-model="b" />
|
|
18
|
+
<label :for="`${id}-unchecked`" class="text-sm">未勾选</label>
|
|
18
19
|
</div>
|
|
19
20
|
<div class="flex items-center gap-2">
|
|
20
|
-
<YlfCheckbox v-model="c" />
|
|
21
|
-
<
|
|
21
|
+
<YlfCheckbox :id="`${id}-mixed`" v-model="c" />
|
|
22
|
+
<label :for="`${id}-mixed`" class="text-sm">半选 indeterminate</label>
|
|
22
23
|
</div>
|
|
23
24
|
<div class="flex items-center gap-2 op-60">
|
|
24
|
-
<YlfCheckbox :model-value="true" disabled />
|
|
25
|
-
<
|
|
25
|
+
<YlfCheckbox :id="`${id}-disabled`" :model-value="true" disabled />
|
|
26
|
+
<label :for="`${id}-disabled`" class="text-sm">禁用</label>
|
|
26
27
|
</div>
|
|
27
28
|
</div>
|
|
28
29
|
</template>
|
|
@@ -12,3 +12,5 @@ title_zh: 复选框
|
|
|
12
12
|
| `v-model` | 勾选状态 | `boolean \| 'indeterminate'` | `false` |
|
|
13
13
|
| `value` | 表单提交值 | `string` | — |
|
|
14
14
|
| `disabled` | 禁用 | `boolean` | `false` |
|
|
15
|
+
|
|
16
|
+
每个复选框必须有可访问名称:传入 `aria-label="同意服务条款"`,或通过 `id` 与 `<label for>` 关联。旁边的普通 `<span>` 不会自动成为标签。`name` 和 `required` 透传给 Reka UI;置于原生表单时,勾选值进入 `FormData`,禁用项不提交。
|
|
@@ -7,15 +7,18 @@ title_zh: 对话框
|
|
|
7
7
|
|
|
8
8
|
## Props
|
|
9
9
|
|
|
10
|
-
| 属性 | 说明 | 类型
|
|
11
|
-
| ----------------- | -------------------------------- |
|
|
12
|
-
| `v-model:open` | 显隐状态 | `boolean`
|
|
13
|
-
| `title` | 可见标题 | `string`
|
|
14
|
-
| `description` | 描述 | `string`
|
|
15
|
-
| `accessibleTitle` | 无可见标题时供辅助技术读取的标题 | `string`
|
|
10
|
+
| 属性 | 说明 | 类型 | 默认值 |
|
|
11
|
+
| ----------------- | -------------------------------- | ----------------------- | ----------------------- |
|
|
12
|
+
| `v-model:open` | 显隐状态 | `boolean` | `false` |
|
|
13
|
+
| `title` | 可见标题 | `string` | — |
|
|
14
|
+
| `description` | 描述 | `string` | — |
|
|
15
|
+
| `accessibleTitle` | 无可见标题时供辅助技术读取的标题 | `string` | `'对话框'` |
|
|
16
|
+
| `portalTo` | 已存在的主题内浮层容器 | `string \| HTMLElement` | ConfigProvider / `body` |
|
|
16
17
|
|
|
17
18
|
建议始终提供 `title`。只有视觉设计明确隐藏标题时,才改用描述场景的 `accessible-title`。
|
|
18
19
|
|
|
20
|
+
`class`、`id`、`data-*` 和事件传给对话框内容;显式的 `aria-describedby` 可关联调用方的说明。焦点陷阱、Escape 关闭和关闭后回到触发器仍由 Reka UI 管理。局部主题中把 `portal-to` 指向同一主题边界内的容器。长标题会换行并为关闭按钮保留空间。
|
|
21
|
+
|
|
19
22
|
## Registry
|
|
20
23
|
|
|
21
24
|
Dialog 可通过 [Registry 分发](/guide/registry)复制源码;安装时会自动带入 `reka-ui`、Sass 与共享 token。
|
|
@@ -7,9 +7,13 @@ title_zh: 下拉菜单
|
|
|
7
7
|
|
|
8
8
|
## Props
|
|
9
9
|
|
|
10
|
-
| 属性
|
|
11
|
-
|
|
|
12
|
-
| `items`
|
|
10
|
+
| 属性 | 说明 | 类型 | 默认值 |
|
|
11
|
+
| -------------- | ---------------------- | ----------------------- | ----------------------- |
|
|
12
|
+
| `items` | 菜单项 | `readonly MenuItem[]` | — |
|
|
13
|
+
| `v-model:open` | 显隐状态 | `boolean` | `false` |
|
|
14
|
+
| `portalTo` | 已存在的主题内浮层容器 | `string \| HTMLElement` | ConfigProvider / `body` |
|
|
15
|
+
|
|
16
|
+
`class`、`data-*`、ARIA 属性和事件传给菜单内容。`portal-to` 可把菜单放入已有的局部主题容器;未传时继续使用 ConfigProvider 或 `body`。支持方向键、Home / End、Enter 选择和 Escape 关闭;禁用项会被跳过。
|
|
13
17
|
|
|
14
18
|
### MenuItem
|
|
15
19
|
|
|
@@ -25,3 +29,5 @@ title_zh: 下拉菜单
|
|
|
25
29
|
| 名称 | 说明 |
|
|
26
30
|
| -------- | ---------------------------- |
|
|
27
31
|
| `select` | 选中某项时触发,回传 `value` |
|
|
32
|
+
|
|
33
|
+
`value=""` 可表示“全部”或“清除筛选”,仍会触发 `select`。没有 `value` 的项不触发选值事件。
|
|
@@ -7,11 +7,14 @@ title_zh: 浮层
|
|
|
7
7
|
|
|
8
8
|
## Props
|
|
9
9
|
|
|
10
|
-
| 属性 | 说明
|
|
11
|
-
| -------------- |
|
|
12
|
-
| `v-model:open` | 显隐状态
|
|
13
|
-
| `side` | 方向
|
|
14
|
-
| `align` | 对齐
|
|
10
|
+
| 属性 | 说明 | 类型 | 默认值 |
|
|
11
|
+
| -------------- | ---------------------- | -------------------------------- | ----------------------- |
|
|
12
|
+
| `v-model:open` | 显隐状态 | `boolean` | `false` |
|
|
13
|
+
| `side` | 方向 | `top \| right \| bottom \| left` | `bottom` |
|
|
14
|
+
| `align` | 对齐 | `start \| center \| end` | `center` |
|
|
15
|
+
| `portalTo` | 已存在的主题内浮层容器 | `string \| HTMLElement` | ConfigProvider / `body` |
|
|
16
|
+
|
|
17
|
+
`class`、`data-*`、ARIA 属性和事件传给浮层内容,内部关联 ID 由 Reka UI 管理。默认可访问名称与触发器关联;触发器应使用说明用途的文字。局部主题的 `portal-to` 容器应处于相同主题边界。
|
|
15
18
|
|
|
16
19
|
## Slots
|
|
17
20
|
|
|
@@ -9,9 +9,11 @@ title_zh: 进度条
|
|
|
9
9
|
|
|
10
10
|
| 属性 | 说明 | 类型 | 默认值 |
|
|
11
11
|
| --------- | ------------------------- | ----------------------------------------------- | ------- |
|
|
12
|
-
| `value` |
|
|
12
|
+
| `value` | 当前进度;`null` 表示未知 | `number \| null` | `0` |
|
|
13
13
|
| `max` | 最大值 | `number` | `100` |
|
|
14
14
|
| `variant` | 填充 | `brand \| accent` | `brand` |
|
|
15
15
|
| `tone` | 强调色,仅作用于 `accent` | `blue \| sun \| cyan \| coral \| pink \| green` | `blue` |
|
|
16
16
|
|
|
17
17
|
`tone` 接受 `blue | sun | cyan | coral | pink | green`,默认 `blue`(晴空蓝),只在 `accent` 下生效。旧 `aurora` 值映射到 `accent`。同一流程中的同类状态使用同一种颜色。
|
|
18
|
+
|
|
19
|
+
提供用途名称,例如 `aria-label="文件上传进度"`。有限数值会限制在 `0…max`,无效的 `max` 回退到 `100`,确保显示与 ARIA 值一致。`null`、NaN 或无限值按未知进度显示,不输出 `aria-valuenow`;减少动态效果时显示静态进度段。
|
|
@@ -12,7 +12,7 @@ const options = [
|
|
|
12
12
|
|
|
13
13
|
<template>
|
|
14
14
|
<div class="flex flex-col gap-4">
|
|
15
|
-
<YlfRadioGroup v-model="picked" :options="options" orientation="horizontal" />
|
|
15
|
+
<YlfRadioGroup v-model="picked" :options="options" orientation="horizontal" aria-label="主题颜色" />
|
|
16
16
|
<p class="text-sm op-70">
|
|
17
17
|
当前:{{ picked }}
|
|
18
18
|
</p>
|