@yunlefun/vue 0.2.0 → 0.3.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.
@@ -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-3, #64748b);
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>
@@ -6,10 +6,13 @@ 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
12
14
  }>(), {
15
+ size: 'md',
13
16
  placeholder: '请选择',
14
17
  disabled: false,
15
18
  })
@@ -20,19 +23,21 @@ const selectedOption = computed(() => props.options.find(option => option.value
20
23
 
21
24
  <template>
22
25
  <SelectRoot v-model="model" :disabled="disabled">
23
- <SelectTrigger class="ylf-select__trigger" :aria-label="$attrs.id || $attrs['aria-labelledby'] ? undefined : placeholder" v-bind="$attrs">
26
+ <SelectTrigger class="ylf-select__trigger" :data-size="size" :aria-label="$attrs.id || $attrs['aria-labelledby'] ? undefined : placeholder" v-bind="$attrs">
24
27
  <SelectValue :placeholder="placeholder" class="ylf-select__label">
25
28
  <span v-if="selectedOption?.color" class="ylf-select__swatch" :style="{ backgroundColor: selectedOption.color }" aria-hidden="true" />
26
29
  <span>{{ selectedOption?.label ?? placeholder }}</span>
27
30
  </SelectValue>
28
31
  <SelectIcon class="ylf-select__icon">
29
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
30
- <path d="M6 9l6 6 6-6" />
31
- </svg>
32
+ <slot name="icon">
33
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
34
+ <path d="M6 9l6 6 6-6" />
35
+ </svg>
36
+ </slot>
32
37
  </SelectIcon>
33
38
  </SelectTrigger>
34
39
  <SelectPortal>
35
- <SelectContent class="ylf-select__content" position="popper" :side-offset="6">
40
+ <SelectContent class="ylf-select__content" :data-size="size" position="popper" :side-offset="6">
36
41
  <SelectViewport class="ylf-select__viewport">
37
42
  <SelectItem
38
43
  v-for="o in options"
@@ -46,9 +51,11 @@ const selectedOption = computed(() => props.options.find(option => option.value
46
51
  <span>{{ o.label }}</span>
47
52
  </SelectItemText>
48
53
  <SelectItemIndicator class="ylf-select__check">
49
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
50
- <path d="M5 12.5l4.5 4.5L19 7" />
51
- </svg>
54
+ <slot name="indicator">
55
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
56
+ <path d="M5 12.5l4.5 4.5L19 7" />
57
+ </svg>
58
+ </slot>
52
59
  </SelectItemIndicator>
53
60
  </SelectItem>
54
61
  </SelectViewport>
@@ -119,7 +126,7 @@ const selectedOption = computed(() => props.options.find(option => option.value
119
126
  display: inline-flex;
120
127
  color: var(--ylf-c-text-3, #64748b);
121
128
 
122
- svg {
129
+ :where(svg, i, span) {
123
130
  width: 18px;
124
131
  height: 18px;
125
132
  }
@@ -174,12 +181,33 @@ const selectedOption = computed(() => props.options.find(option => option.value
174
181
  display: inline-flex;
175
182
  color: var(--ylf-c-brand, #2563eb);
176
183
 
177
- svg {
184
+ :where(svg, i, span) {
178
185
  width: 15px;
179
186
  height: 15px;
180
187
  }
181
188
  }
182
189
 
190
+ .ylf-select__trigger[data-size='sm'] {
191
+ min-width: 0;
192
+ min-height: 34px;
193
+ padding: 5px 10px;
194
+ font-size: 12px;
195
+ border-radius: var(--ylf-radius-sm, 10px);
196
+ }
197
+
198
+ .ylf-select__content[data-size='sm'] .ylf-select__item {
199
+ min-height: 30px;
200
+ padding: 5px 8px;
201
+ font-size: 12px;
202
+ }
203
+
204
+ @media (pointer: coarse) {
205
+ .ylf-select__trigger[data-size='sm'],
206
+ .ylf-select__content[data-size='sm'] .ylf-select__item {
207
+ min-height: 44px;
208
+ }
209
+ }
210
+
183
211
  @keyframes ylf-pop-in {
184
212
  from {
185
213
  opacity: 0;
@@ -6,15 +6,17 @@ withDefaults(defineProps<{
6
6
  content?: string
7
7
  side?: 'top' | 'right' | 'bottom' | 'left'
8
8
  delay?: number
9
+ disabled?: boolean
9
10
  }>(), {
10
11
  side: 'top',
11
12
  delay: 300,
13
+ disabled: false,
12
14
  })
13
15
  </script>
14
16
 
15
17
  <template>
16
18
  <TooltipProvider :delay-duration="delay">
17
- <TooltipRoot>
19
+ <TooltipRoot :disabled="disabled">
18
20
  <TooltipTrigger as-child>
19
21
  <slot />
20
22
  </TooltipTrigger>
@@ -0,0 +1,21 @@
1
+ <script setup lang="ts">
2
+ import { shallowRef } from 'vue'
3
+ import YlfSegmentedControl from '../YlfSegmentedControl.vue'
4
+
5
+ const view = shallowRef<'grid' | 'list'>('grid')
6
+ const kind = shallowRef('all')
7
+ const options = [{ value: 'grid', label: '网格视图' }, { value: 'list', label: '列表视图' }] as const
8
+ const icons = { grid: 'i-ri-grid-line', list: 'i-ri-list-check' }
9
+ const kinds = [{ value: 'all', label: '全部', count: 24 }, { value: 'image', label: '图片', count: 18 }, { value: 'video', label: '视频', count: 6 }]
10
+ </script>
11
+
12
+ <template>
13
+ <div class="flex flex-wrap items-center gap-4">
14
+ <YlfSegmentedControl v-model="kind" :options="kinds" label="素材类型" size="sm" />
15
+ <YlfSegmentedControl v-model="view" :options="options" label="资源视图" size="sm" icon-only>
16
+ <template #icon="{ option }">
17
+ <i :class="icons[option.value]" />
18
+ </template>
19
+ </YlfSegmentedControl>
20
+ </div>
21
+ </template>
@@ -0,0 +1,41 @@
1
+ ---
2
+ title: Segmented Control
3
+ title_zh: 分段选择器
4
+ ---
5
+
6
+ 单选视图与筛选控件,基于 [Reka UI ToggleGroup](https://reka-ui.com/docs/components/toggle-group)。保留方向键、Home / End 和禁用项处理,再次点击当前项不会清空选择。切换独立内容面板请使用 Tabs。
7
+
8
+ | 属性 | 说明 | 默认值 |
9
+ | ----------- | ----------------------------------------------------------------- | ------- |
10
+ | `v-model` | 当前选项,支持字符串联合类型 | 必填 |
11
+ | `options` | `{ value, label, disabled?, count?, ariaLabel? }[]`,支持只读数组 | 必填 |
12
+ | `label` | 组合控件的无障碍名称 | 必填 |
13
+ | `size` | `sm`:34px 工作台;`md`:44px 常规界面 | `md` |
14
+ | `icon-only` | 仅显示图标,并提供文字 Tooltip | `false` |
15
+ | `disabled` | 禁用全部选项 | `false` |
16
+
17
+ `#icon="{ option, selected }"` 插槽接收当前项及选中状态。应用通过完整的 UnoCSS / Iconify 类名映射渲染图标,公共组件不依赖应用的图标构建方式。没有图标插槽时仍显示标签。
18
+
19
+ ```vue
20
+ <script setup lang="ts">
21
+ import YlfSegmentedControl from '@yunlefun/vue/components/YlfSegmentedControl.vue'
22
+ import { shallowRef } from 'vue'
23
+
24
+ const view = shallowRef<'grid' | 'list'>('grid')
25
+ const options = [
26
+ { value: 'grid', label: '网格视图' },
27
+ { value: 'list', label: '列表视图' },
28
+ ] as const
29
+ const icons = { grid: 'i-lucide-grid-2x2', list: 'i-lucide-list' }
30
+ </script>
31
+
32
+ <template>
33
+ <YlfSegmentedControl v-model="view" :options="options" label="资源视图" size="sm" icon-only>
34
+ <template #icon="{ option }">
35
+ <i :class="icons[option.value]" />
36
+ </template>
37
+ </YlfSegmentedControl>
38
+ </template>
39
+ ```
40
+
41
+ 样式统一来自 `--ylf-*`:轻灰底座、表面色选中项、品牌色文字、细边框与焦点环。`sm` 与 `YlfSelect size="sm"` 对齐。触摸设备中选项至少 44px;减少动态效果时禁用过渡。不需要在业务页面覆盖选中态或圆角。
@@ -0,0 +1,8 @@
1
+ /** One choice in a single-selection segmented control. */
2
+ export interface YlfSegmentedOption<T extends string = string> {
3
+ value: T
4
+ label: string
5
+ disabled?: boolean
6
+ count?: number
7
+ ariaLabel?: string
8
+ }
@@ -39,3 +39,7 @@ const options = [
39
39
  ```
40
40
 
41
41
  使用 token 时,色点会随所在主题更新;页面和浮层需处于同一主题范围。
42
+
43
+ ## 工作台密度与图标
44
+
45
+ `size="sm"` 使用 34px 高度、紧凑文字与 10px 圆角,适合排序、项目范围等工具栏控件;默认 `md` 保持 44px 表单尺寸。触摸设备中紧凑选择器与选项仍至少 44px。`#icon` 和 `#indicator` 可传入应用已有的 UnoCSS / Iconify 图标;未提供时保留内置 SVG。
@@ -19,3 +19,5 @@ title_zh: 文字提示
19
19
  | --------- | ---------------------- |
20
20
  | `default` | 触发元素(`as-child`) |
21
21
  | `content` | 自定义提示内容 |
22
+
23
+ `disabled` 可关闭 Tooltip;例如分段选择器显示文字时不重复提示,仅图标模式下再启用。
package/dist/index.d.cts CHANGED
@@ -19,6 +19,15 @@ interface BottomMenuItem {
19
19
  onClick?: (...args: any[]) => void;
20
20
  }
21
21
 
22
+ /** One choice in a single-selection segmented control. */
23
+ interface YlfSegmentedOption<T extends string = string> {
24
+ value: T;
25
+ label: string;
26
+ disabled?: boolean;
27
+ count?: number;
28
+ ariaLabel?: string;
29
+ }
30
+
22
31
  /** Shared accents, defaulting to sky blue. Status colors use their own semantic variants. */
23
32
  type YlfAccentTone = 'blue' | 'sun' | 'cyan' | 'coral' | 'pink' | 'green';
24
33
  /** Visual emphasis for accent and status buttons/badges. */
@@ -28,4 +37,4 @@ declare const _dirname: string;
28
37
  declare const componentsDir: string;
29
38
 
30
39
  export { _dirname, componentsDir };
31
- export type { BottomMenuItem, YlfAccentTone, YlfColorAppearance };
40
+ export type { BottomMenuItem, YlfAccentTone, YlfColorAppearance, YlfSegmentedOption };
package/dist/index.d.mts CHANGED
@@ -19,6 +19,15 @@ interface BottomMenuItem {
19
19
  onClick?: (...args: any[]) => void;
20
20
  }
21
21
 
22
+ /** One choice in a single-selection segmented control. */
23
+ interface YlfSegmentedOption<T extends string = string> {
24
+ value: T;
25
+ label: string;
26
+ disabled?: boolean;
27
+ count?: number;
28
+ ariaLabel?: string;
29
+ }
30
+
22
31
  /** Shared accents, defaulting to sky blue. Status colors use their own semantic variants. */
23
32
  type YlfAccentTone = 'blue' | 'sun' | 'cyan' | 'coral' | 'pink' | 'green';
24
33
  /** Visual emphasis for accent and status buttons/badges. */
@@ -28,4 +37,4 @@ declare const _dirname: string;
28
37
  declare const componentsDir: string;
29
38
 
30
39
  export { _dirname, componentsDir };
31
- export type { BottomMenuItem, YlfAccentTone, YlfColorAppearance };
40
+ export type { BottomMenuItem, YlfAccentTone, YlfColorAppearance, YlfSegmentedOption };
package/dist/index.d.ts CHANGED
@@ -19,6 +19,15 @@ interface BottomMenuItem {
19
19
  onClick?: (...args: any[]) => void;
20
20
  }
21
21
 
22
+ /** One choice in a single-selection segmented control. */
23
+ interface YlfSegmentedOption<T extends string = string> {
24
+ value: T;
25
+ label: string;
26
+ disabled?: boolean;
27
+ count?: number;
28
+ ariaLabel?: string;
29
+ }
30
+
22
31
  /** Shared accents, defaulting to sky blue. Status colors use their own semantic variants. */
23
32
  type YlfAccentTone = 'blue' | 'sun' | 'cyan' | 'coral' | 'pink' | 'green';
24
33
  /** Visual emphasis for accent and status buttons/badges. */
@@ -28,4 +37,4 @@ declare const _dirname: string;
28
37
  declare const componentsDir: string;
29
38
 
30
39
  export { _dirname, componentsDir };
31
- export type { BottomMenuItem, YlfAccentTone, YlfColorAppearance };
40
+ export type { BottomMenuItem, YlfAccentTone, YlfColorAppearance, YlfSegmentedOption };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yunlefun/vue",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Vue components and Nuxt integration for YunLeFun Design",
6
6
  "author": "YunLeFun",
7
7
  "license": "MIT",