@yunlefun/vue 0.2.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 +180 -0
- package/components/YlfSelect.vue +47 -13
- package/components/YlfSlider.vue +32 -2
- package/components/YlfTabs.vue +1 -1
- package/components/YlfTooltip.vue +8 -3
- 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/demo.vue +21 -0
- package/components/segmented-control/index.md +41 -0
- package/components/segmented-control.ts +8 -0
- package/components/select/index.md +10 -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 +11 -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/dist/index.d.cts +10 -1
- package/dist/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- 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"
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends string = string">
|
|
2
|
+
import type { YlfSegmentedOption } from './segmented-control'
|
|
3
|
+
import { ToggleGroupItem, ToggleGroupRoot } from 'reka-ui'
|
|
4
|
+
import YlfTooltip from './YlfTooltip.vue'
|
|
5
|
+
|
|
6
|
+
const props = withDefaults(defineProps<{
|
|
7
|
+
options: readonly YlfSegmentedOption<T>[]
|
|
8
|
+
/** Accessible name of the group, including for icon-only controls. */
|
|
9
|
+
label: string
|
|
10
|
+
size?: 'sm' | 'md'
|
|
11
|
+
iconOnly?: boolean
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
}>(), {
|
|
14
|
+
size: 'md',
|
|
15
|
+
iconOnly: false,
|
|
16
|
+
disabled: false,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
defineSlots<{
|
|
20
|
+
icon?: (props: { option: YlfSegmentedOption<T>, selected: boolean }) => unknown
|
|
21
|
+
}>()
|
|
22
|
+
const model = defineModel<T>({ required: true })
|
|
23
|
+
function updateValue(value: unknown): void {
|
|
24
|
+
const option = props.options.find(option => option.value === value)
|
|
25
|
+
// A view or filter always has one active choice, even when clicked again.
|
|
26
|
+
if (!props.disabled && option && !option.disabled)
|
|
27
|
+
model.value = option.value
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<ToggleGroupRoot
|
|
33
|
+
class="ylf-segmented"
|
|
34
|
+
:data-size="size"
|
|
35
|
+
:data-icon-only="iconOnly || undefined"
|
|
36
|
+
type="single"
|
|
37
|
+
orientation="horizontal"
|
|
38
|
+
:aria-label="label"
|
|
39
|
+
:disabled="disabled"
|
|
40
|
+
:model-value="model"
|
|
41
|
+
@update:model-value="updateValue"
|
|
42
|
+
>
|
|
43
|
+
<YlfTooltip v-for="option in options" :key="option.value" :content="option.label" :disabled="!iconOnly || disabled || option.disabled">
|
|
44
|
+
<ToggleGroupItem
|
|
45
|
+
class="ylf-segmented__item"
|
|
46
|
+
:value="option.value"
|
|
47
|
+
:disabled="option.disabled"
|
|
48
|
+
:aria-label="option.ariaLabel ?? option.label"
|
|
49
|
+
>
|
|
50
|
+
<span v-if="$slots.icon" class="ylf-segmented__icon" aria-hidden="true">
|
|
51
|
+
<slot name="icon" :option="option" :selected="model === option.value" />
|
|
52
|
+
</span>
|
|
53
|
+
<span v-if="!iconOnly || !$slots.icon" class="ylf-segmented__label">{{ option.label }}</span>
|
|
54
|
+
<span v-if="option.count !== undefined && !iconOnly" class="ylf-segmented__count">{{ option.count }}</span>
|
|
55
|
+
</ToggleGroupItem>
|
|
56
|
+
</YlfTooltip>
|
|
57
|
+
</ToggleGroupRoot>
|
|
58
|
+
</template>
|
|
59
|
+
|
|
60
|
+
<style>
|
|
61
|
+
.ylf-segmented {
|
|
62
|
+
--ylf-segmented-item-size: 36px;
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
flex: none;
|
|
66
|
+
box-sizing: border-box;
|
|
67
|
+
max-width: 100%;
|
|
68
|
+
gap: 2px;
|
|
69
|
+
padding: 3px;
|
|
70
|
+
border: 1px solid var(--ylf-c-border, #e2e8f0);
|
|
71
|
+
border-radius: var(--ylf-radius-sm, 10px);
|
|
72
|
+
background: var(--ylf-c-bg-soft, #f1f5f9);
|
|
73
|
+
font-family: var(--ylf-font-body, inherit);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.ylf-segmented[data-size='sm'] {
|
|
77
|
+
--ylf-segmented-item-size: 26px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ylf-segmented__item {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
justify-content: center;
|
|
84
|
+
flex: 0 0 auto;
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
min-width: var(--ylf-segmented-item-size);
|
|
87
|
+
height: var(--ylf-segmented-item-size);
|
|
88
|
+
gap: 6px;
|
|
89
|
+
padding: 0 10px;
|
|
90
|
+
border: 1px solid transparent;
|
|
91
|
+
border-radius: calc(var(--ylf-radius-sm, 10px) - 4px);
|
|
92
|
+
background: transparent;
|
|
93
|
+
color: var(--ylf-c-text-2, #475569);
|
|
94
|
+
font: inherit;
|
|
95
|
+
font-size: var(--ylf-text-sm, 14px);
|
|
96
|
+
font-weight: 500;
|
|
97
|
+
line-height: 1;
|
|
98
|
+
white-space: nowrap;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
transition:
|
|
101
|
+
background var(--ylf-duration-fast, 160ms),
|
|
102
|
+
color var(--ylf-duration-fast, 160ms),
|
|
103
|
+
box-shadow var(--ylf-duration-fast, 160ms);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.ylf-segmented[data-size='sm'] .ylf-segmented__item {
|
|
107
|
+
padding-inline: 8px;
|
|
108
|
+
font-size: 12px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.ylf-segmented[data-icon-only] .ylf-segmented__item {
|
|
112
|
+
width: calc(var(--ylf-segmented-item-size) + 4px);
|
|
113
|
+
padding: 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.ylf-segmented__item:hover:not([data-disabled], [aria-pressed='true']) {
|
|
117
|
+
color: var(--ylf-c-text, #0f172a);
|
|
118
|
+
background: color-mix(in srgb, var(--ylf-c-surface, #fff) 60%, transparent);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.ylf-segmented__item[aria-pressed='true'] {
|
|
122
|
+
color: var(--ylf-c-brand, #2563eb);
|
|
123
|
+
background: var(--ylf-c-surface, #fff);
|
|
124
|
+
border-color: var(--ylf-c-border, #e2e8f0);
|
|
125
|
+
box-shadow: var(--ylf-shadow-sm);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ylf-segmented__item:focus-visible {
|
|
129
|
+
position: relative;
|
|
130
|
+
z-index: 1;
|
|
131
|
+
outline: 2px solid var(--ylf-c-brand, #2563eb);
|
|
132
|
+
outline-offset: 2px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.ylf-segmented__item[data-disabled] {
|
|
136
|
+
cursor: not-allowed;
|
|
137
|
+
opacity: 0.45;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.ylf-segmented__icon {
|
|
141
|
+
display: inline-flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
flex: none;
|
|
145
|
+
width: 16px;
|
|
146
|
+
height: 16px;
|
|
147
|
+
font-size: 16px;
|
|
148
|
+
line-height: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.ylf-segmented__icon > :where(svg, i, span) {
|
|
152
|
+
width: 100%;
|
|
153
|
+
height: 100%;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.ylf-segmented__count {
|
|
157
|
+
min-width: 16px;
|
|
158
|
+
color: var(--ylf-c-text-2, #475569);
|
|
159
|
+
font-size: 10px;
|
|
160
|
+
font-variant-numeric: tabular-nums;
|
|
161
|
+
text-align: center;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.ylf-segmented__item[aria-pressed='true'] .ylf-segmented__count {
|
|
165
|
+
color: inherit;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@media (pointer: coarse) {
|
|
169
|
+
.ylf-segmented .ylf-segmented__item {
|
|
170
|
+
min-width: 44px;
|
|
171
|
+
min-height: 44px;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@media (prefers-reduced-motion: reduce) {
|
|
176
|
+
.ylf-segmented__item {
|
|
177
|
+
transition: none;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
</style>
|
package/components/YlfSelect.vue
CHANGED
|
@@ -6,12 +6,21 @@ defineOptions({ inheritAttrs: false })
|
|
|
6
6
|
|
|
7
7
|
const props = withDefaults(defineProps<{
|
|
8
8
|
/** color 可使用 CSS 色值或公共 token,例如 var(--ylf-accent-blue) */
|
|
9
|
-
options: { label: string, value: T, disabled?: boolean, color?: string }[]
|
|
9
|
+
options: readonly { label: string, value: T, disabled?: boolean, color?: string }[]
|
|
10
|
+
/** Compact workbench or comfortable form density. */
|
|
11
|
+
size?: 'sm' | 'md'
|
|
10
12
|
placeholder?: string
|
|
11
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
|
|
12
19
|
}>(), {
|
|
20
|
+
size: 'md',
|
|
13
21
|
placeholder: '请选择',
|
|
14
22
|
disabled: false,
|
|
23
|
+
required: false,
|
|
15
24
|
})
|
|
16
25
|
|
|
17
26
|
const model = defineModel<T>()
|
|
@@ -19,20 +28,22 @@ const selectedOption = computed(() => props.options.find(option => option.value
|
|
|
19
28
|
</script>
|
|
20
29
|
|
|
21
30
|
<template>
|
|
22
|
-
<SelectRoot v-model="model" :disabled="disabled">
|
|
23
|
-
<SelectTrigger class="ylf-select__trigger" :aria-label="$attrs.id || $attrs['aria-labelledby'] ? undefined : placeholder" v-bind="$attrs">
|
|
31
|
+
<SelectRoot v-model="model" :disabled="disabled" :name="name" :required="required">
|
|
32
|
+
<SelectTrigger class="ylf-select__trigger" :data-size="size" :aria-label="$attrs.id || $attrs['aria-labelledby'] ? undefined : placeholder" v-bind="$attrs">
|
|
24
33
|
<SelectValue :placeholder="placeholder" class="ylf-select__label">
|
|
25
34
|
<span v-if="selectedOption?.color" class="ylf-select__swatch" :style="{ backgroundColor: selectedOption.color }" aria-hidden="true" />
|
|
26
35
|
<span>{{ selectedOption?.label ?? placeholder }}</span>
|
|
27
36
|
</SelectValue>
|
|
28
37
|
<SelectIcon class="ylf-select__icon">
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
38
|
+
<slot name="icon">
|
|
39
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
40
|
+
<path d="M6 9l6 6 6-6" />
|
|
41
|
+
</svg>
|
|
42
|
+
</slot>
|
|
32
43
|
</SelectIcon>
|
|
33
44
|
</SelectTrigger>
|
|
34
|
-
<SelectPortal>
|
|
35
|
-
<SelectContent class="ylf-select__content" position="popper" :side-offset="6">
|
|
45
|
+
<SelectPortal :to="portalTo">
|
|
46
|
+
<SelectContent class="ylf-select__content" :data-size="size" position="popper" :side-offset="6">
|
|
36
47
|
<SelectViewport class="ylf-select__viewport">
|
|
37
48
|
<SelectItem
|
|
38
49
|
v-for="o in options"
|
|
@@ -46,9 +57,11 @@ const selectedOption = computed(() => props.options.find(option => option.value
|
|
|
46
57
|
<span>{{ o.label }}</span>
|
|
47
58
|
</SelectItemText>
|
|
48
59
|
<SelectItemIndicator class="ylf-select__check">
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
|
|
60
|
+
<slot name="indicator">
|
|
61
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
|
|
62
|
+
<path d="M5 12.5l4.5 4.5L19 7" />
|
|
63
|
+
</svg>
|
|
64
|
+
</slot>
|
|
52
65
|
</SelectItemIndicator>
|
|
53
66
|
</SelectItem>
|
|
54
67
|
</SelectViewport>
|
|
@@ -119,7 +132,7 @@ const selectedOption = computed(() => props.options.find(option => option.value
|
|
|
119
132
|
display: inline-flex;
|
|
120
133
|
color: var(--ylf-c-text-3, #64748b);
|
|
121
134
|
|
|
122
|
-
svg {
|
|
135
|
+
:where(svg, i, span) {
|
|
123
136
|
width: 18px;
|
|
124
137
|
height: 18px;
|
|
125
138
|
}
|
|
@@ -174,12 +187,33 @@ const selectedOption = computed(() => props.options.find(option => option.value
|
|
|
174
187
|
display: inline-flex;
|
|
175
188
|
color: var(--ylf-c-brand, #2563eb);
|
|
176
189
|
|
|
177
|
-
svg {
|
|
190
|
+
:where(svg, i, span) {
|
|
178
191
|
width: 15px;
|
|
179
192
|
height: 15px;
|
|
180
193
|
}
|
|
181
194
|
}
|
|
182
195
|
|
|
196
|
+
.ylf-select__trigger[data-size='sm'] {
|
|
197
|
+
min-width: 0;
|
|
198
|
+
min-height: 34px;
|
|
199
|
+
padding: 5px 10px;
|
|
200
|
+
font-size: 12px;
|
|
201
|
+
border-radius: var(--ylf-radius-sm, 10px);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.ylf-select__content[data-size='sm'] .ylf-select__item {
|
|
205
|
+
min-height: 30px;
|
|
206
|
+
padding: 5px 8px;
|
|
207
|
+
font-size: 12px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
@media (pointer: coarse) {
|
|
211
|
+
.ylf-select__trigger[data-size='sm'],
|
|
212
|
+
.ylf-select__content[data-size='sm'] .ylf-select__item {
|
|
213
|
+
min-height: 44px;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
183
217
|
@keyframes ylf-pop-in {
|
|
184
218
|
from {
|
|
185
219
|
opacity: 0;
|
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>
|