@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.
Files changed (60) hide show
  1. package/README.md +2 -0
  2. package/components/YlfAccordion.vue +1 -1
  3. package/components/YlfAvatar.vue +2 -1
  4. package/components/YlfDialog.vue +23 -2
  5. package/components/YlfDropdownMenu.vue +20 -9
  6. package/components/YlfPopover.vue +5 -2
  7. package/components/YlfProgress.vue +29 -5
  8. package/components/YlfRadioGroup.vue +20 -2
  9. package/components/YlfSegmentedControl.vue +1 -1
  10. package/components/YlfSelect.vue +8 -2
  11. package/components/YlfSlider.vue +32 -2
  12. package/components/YlfTabs.vue +1 -1
  13. package/components/YlfTooltip.vue +5 -2
  14. package/components/accordion/demo.vue +1 -1
  15. package/components/accordion/index.md +2 -0
  16. package/components/avatar/index.md +3 -1
  17. package/components/checkbox/demo.vue +10 -9
  18. package/components/checkbox/index.md +2 -0
  19. package/components/dialog/index.md +9 -6
  20. package/components/dropdown-menu/index.md +9 -3
  21. package/components/popover/demo.vue +1 -1
  22. package/components/popover/index.md +8 -5
  23. package/components/progress/index.md +3 -1
  24. package/components/radio/demo.vue +1 -1
  25. package/components/radio/index.md +2 -0
  26. package/components/segmented-control/index.md +1 -1
  27. package/components/select/index.md +6 -0
  28. package/components/slider/demo.vue +2 -2
  29. package/components/slider/index.md +15 -7
  30. package/components/switch/index.md +2 -0
  31. package/components/tabs/index.md +2 -0
  32. package/components/tooltip/index.md +9 -5
  33. package/dist/components/YlfAccordion.vue.d.ts +35 -0
  34. package/dist/components/YlfAiPromptLauncher.vue.d.ts +25 -0
  35. package/dist/components/YlfAvatar.vue.d.ts +14 -0
  36. package/dist/components/YlfBadge.vue.d.ts +29 -0
  37. package/dist/components/YlfBottomMenu.vue.d.ts +19 -0
  38. package/dist/components/YlfBottomMenuItem.vue.d.ts +22 -0
  39. package/dist/components/YlfButton.vue.d.ts +48 -0
  40. package/dist/components/YlfCard.vue.d.ts +28 -0
  41. package/dist/components/YlfCheckbox.vue.d.ts +18 -0
  42. package/dist/components/YlfDialog.vue.d.ts +37 -0
  43. package/dist/components/YlfDropdownMenu.vue.d.ts +34 -0
  44. package/dist/components/YlfLogo.vue.d.ts +13 -0
  45. package/dist/components/YlfPopover.vue.d.ts +31 -0
  46. package/dist/components/YlfProgress.vue.d.ts +17 -0
  47. package/dist/components/YlfRadioGroup.vue.d.ts +24 -0
  48. package/dist/components/YlfSegmentedControl.vue.d.ts +35 -0
  49. package/dist/components/YlfSelect.vue.d.ts +43 -0
  50. package/dist/components/YlfSeparator.vue.d.ts +16 -0
  51. package/dist/components/YlfSlider.vue.d.ts +29 -0
  52. package/dist/components/YlfSwitch.vue.d.ts +24 -0
  53. package/dist/components/YlfTabs.vue.d.ts +32 -0
  54. package/dist/components/YlfTooltip.vue.d.ts +26 -0
  55. package/dist/components/ai-prompt.d.ts +33 -0
  56. package/dist/components/ai-provider-logos.d.ts +3 -0
  57. package/dist/components/bottom-menu/index.d.ts +20 -0
  58. package/dist/components/segmented-control.d.ts +8 -0
  59. package/dist/components/theme.d.ts +4 -0
  60. 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 提示词入口
@@ -9,7 +9,7 @@ interface AccordionItemData {
9
9
  }
10
10
 
11
11
  withDefaults(defineProps<{
12
- items: AccordionItemData[]
12
+ items: readonly AccordionItemData[]
13
13
  type?: 'single' | 'multiple'
14
14
  collapsible?: boolean
15
15
  }>(), {
@@ -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>
@@ -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
- defineProps<{
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
- value?: number
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 percent = computed(() => Math.max(0, Math.min(100, (props.value / props.max) * 100)))
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="value"
25
- :max="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-3, #64748b);
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;
@@ -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
@@ -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 v-for="(_, i) in model" :key="i" class="ylf-slider__thumb" />
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>
@@ -8,7 +8,7 @@ interface TabItem {
8
8
  }
9
9
 
10
10
  withDefaults(defineProps<{
11
- items: TabItem[]
11
+ items: readonly TabItem[]
12
12
  orientation?: 'horizontal' | 'vertical'
13
13
  }>(), {
14
14
  orientation: 'horizontal',
@@ -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: 'reka 管行为与可访问性,我们只管皮肤——省心,且无障碍达标。' },
4
+ { value: 'b', title: '为什么基于 reka-ui?', content: 'Reka UI 提供键盘、焦点和 ARIA 行为,云乐坊提供产品接口与主题;封装后的场景仍需逐项验收。' },
5
5
  { value: 'c', title: '支持暗色吗?', content: '晴空白与深蓝夜空两套中性色,token 驱动,零配置随主题切换。' },
6
6
  ]
7
7
  </script>
@@ -17,3 +17,5 @@ title_zh: 手风琴
17
17
  ## Slots
18
18
 
19
19
  每项内容可用 `value` 同名插槽覆盖 `content`。
20
+
21
+ `items` 支持只读数组。单开模式全部收起时值为 `undefined`,多开模式为字符串数组;调用方应允许收起状态。方向键、Home / End 在标题间移动焦点,跳过禁用项。
@@ -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
- <span class="text-sm">已勾选</span>
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
- <span class="text-sm">未勾选</span>
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
- <span class="text-sm">半选 indeterminate</span>
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
- <span class="text-sm">禁用</span>
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` | `false` |
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` | 菜单项 | `MenuItem[]` | — |
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` 的项不触发选值事件。
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <YlfPopover side="bottom" align="start">
2
+ <YlfPopover side="bottom" align="start" aria-label="云乐坊介绍">
3
3
  <template #trigger>
4
4
  <YlfButton variant="primary">
5
5
  打开浮层
@@ -7,11 +7,14 @@ title_zh: 浮层
7
7
 
8
8
  ## Props
9
9
 
10
- | 属性 | 说明 | 类型 | 默认值 |
11
- | -------------- | -------- | -------------------------------- | -------- |
12
- | `v-model:open` | 显隐状态 | `boolean` | `false` |
13
- | `side` | 方向 | `top \| right \| bottom \| left` | `bottom` |
14
- | `align` | 对齐 | `start \| center \| end` | `center` |
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` | 当前进度 | `number` | `0` |
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>
@@ -21,3 +21,5 @@ title_zh: 单选框
21
21
  | `label` | 文案 | `string` |
22
22
  | `value` | 值 | `string` |
23
23
  | `disabled` | 禁用该项 | `boolean?` |
24
+
25
+ `options` 支持只读数组。通过 `aria-label` 或 `aria-labelledby` 指定整组名称,单项名称来自 `label`。方向键跳过禁用项;`name` / `required` 透传给 Reka UI,可用于原生表单。