@livestorm/ui-vue 2.0.4 → 3.0.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 +8 -5
- package/dist/UiTimelineItem.vue_vue_type_style_index_0_lang-LpdvGf5X.mjs +6093 -0
- package/dist/a11y-BhF_nl7L.mjs +159 -0
- package/dist/components.mjs +105 -96
- package/dist/index.mjs +68 -64
- package/dist/layouts.mjs +16 -16
- package/dist/types/index.d.ts +5 -1
- package/dist/types/src/components/UiActionBar/UiActionBar.vue.d.ts +3 -1
- package/dist/types/src/components/UiActionBar/UiActionBarItem.vue.d.ts +18 -3
- package/dist/types/src/components/UiActionBar/types.d.ts +3 -1
- package/dist/types/src/components/UiButton/UiButton.vue.d.ts +17 -13
- package/dist/types/src/components/UiButtonGroup/UiButtonGroupItem.vue.d.ts +3 -0
- package/dist/types/src/components/UiButtonGroup/types.d.ts +1 -1
- package/dist/types/src/components/UiDropdown/UiDropdownItem.vue.d.ts +6 -4
- package/dist/types/src/components/UiInputEmoji/UiInputEmoji.vue.d.ts +45 -0
- package/dist/types/src/components/UiInputEmoji/index.d.ts +1 -0
- package/dist/types/src/components/UiInputRadio/UiInputRadio.vue.d.ts +67 -0
- package/dist/types/src/components/UiInputRadio/UiInputRadioGroup.vue.d.ts +81 -0
- package/dist/types/src/components/UiInputRadio/index.d.ts +3 -0
- package/dist/types/src/components/UiInputRadio/types.d.ts +19 -0
- package/dist/types/src/components/UiLoader/UiLoader.vue.d.ts +6 -5
- package/dist/types/src/components/UiLogo/UiLogo.vue.d.ts +9 -3
- package/dist/types/src/components/UiPage/UiPage.vue.d.ts +34 -0
- package/dist/types/src/components/UiProgress/UiProgress.vue.d.ts +1 -1
- package/dist/types/src/components/UiSwitch/UiSwitch.vue.d.ts +109 -0
- package/dist/types/src/components/UiSwitch/index.d.ts +2 -0
- package/dist/types/src/components/UiSwitch/types.d.ts +3 -0
- package/dist/types/src/components/UiTabs/UiTabs.vue.d.ts +5 -2
- package/dist/types/src/composables/useContentMode.d.ts +25 -0
- package/dist/types/src/composables/useIconSize.d.ts +11 -0
- package/dist/types/src/constants/sizes.d.ts +1 -1
- package/dist/ui-vue.css +1 -1
- package/dist/{useThemeAndModes-CUK56j0z.mjs → useThemeAndModes-BzYwSOEv.mjs} +1 -1
- package/dist/utils.mjs +154 -99
- package/index.d.ts +4 -0
- package/index.js +8 -0
- package/package.json +11 -3
- package/src/components/Toast/ToastMessage.vue +6 -6
- package/src/components/UiActionBar/UiActionBar.vue +11 -2
- package/src/components/UiActionBar/UiActionBarItem.vue +56 -32
- package/src/components/UiActionBar/types.ts +3 -1
- package/src/components/UiAvatar/UiAvatar.vue +1 -1
- package/src/components/UiBanner/UiBanner.vue +7 -7
- package/src/components/UiButton/UiButton.vue +9 -9
- package/src/components/UiButtonGroup/UiButtonGroup.vue +4 -0
- package/src/components/UiButtonGroup/UiButtonGroupItem.vue +14 -0
- package/src/components/UiButtonGroup/types.ts +1 -1
- package/src/components/UiCarousel/UiCarousel.vue +2 -2
- package/src/components/UiColorPicker/UiColorPicker.vue +5 -5
- package/src/components/UiDialog/MarketplaceHeaderExample.vue +2 -2
- package/src/components/UiDialog/UiDialogClose.vue +1 -1
- package/src/components/UiDropdown/UiDropdownContent.vue +2 -2
- package/src/components/UiDropdown/UiDropdownItem.vue +8 -1
- package/src/components/UiDropdown/UiDropdownTrigger.vue +6 -7
- package/src/components/UiFieldWrapper/UiFieldWrapper.vue +1 -1
- package/src/components/UiInputCheckbox/UiInputCheckbox.vue +2 -2
- package/src/components/UiInputEmoji/UiInputEmoji.vue +173 -0
- package/src/components/UiInputEmoji/index.ts +1 -0
- package/src/components/UiInputRadio/UiInputRadio.vue +374 -0
- package/src/components/UiInputRadio/UiInputRadioGroup.vue +156 -0
- package/src/components/UiInputRadio/index.ts +3 -0
- package/src/components/UiInputRadio/types.ts +25 -0
- package/src/components/UiLoader/UiLoader.vue +8 -10
- package/src/components/UiLogo/UiLogo.vue +41 -7
- package/src/components/UiMenu/UiMenuContent.vue +1 -1
- package/src/components/UiMenu/UiMenuGroup.vue +1 -1
- package/src/components/UiMenu/UiMenuItem.vue +1 -1
- package/src/components/UiMenu/UiMenuSubContent.vue +1 -1
- package/src/components/UiMenu/UiMenuSubTrigger.vue +2 -2
- package/src/components/UiPage/UiPage.vue +102 -29
- package/src/components/UiPageHeader/UiPageHeader.vue +29 -9
- package/src/components/UiRoomButton/UiRoomButton.vue +4 -4
- package/src/components/UiShell/UiShell.vue +1 -1
- package/src/components/UiSideMenu/UiSideMenu.vue +13 -10
- package/src/components/UiSideMenu/UiSideMenuItemContent.vue +2 -2
- package/src/components/UiStepper/UiStepperIndicator.vue +8 -11
- package/src/components/UiSwitch/UiSwitch.vue +359 -0
- package/src/components/UiSwitch/index.ts +2 -0
- package/src/components/UiSwitch/types.ts +5 -0
- package/src/components/UiTabs/UiTabs.vue +7 -4
- package/src/components/UiTextField/UiTextField.vue +6 -11
- package/src/components/UiTimeline/README.md +6 -6
- package/src/components/UiTimeline/UiTimelineItem.vue +3 -3
- package/src/composables/useContentMode.ts +53 -0
- package/src/composables/useIconSize.ts +28 -0
- package/src/constants/sizes.ts +1 -1
- package/src/global.d.ts +8 -0
- package/dist/UiTimelineItem.vue_vue_type_style_index_0_lang-BtG5oABt.mjs +0 -5454
- package/dist/a11y--y7uaY4j.mjs +0 -217
|
@@ -115,11 +115,11 @@ onMounted(() => {
|
|
|
115
115
|
// Icon mapping based on color
|
|
116
116
|
const iconClass = computed(() => {
|
|
117
117
|
const colorMap = {
|
|
118
|
-
neutral: 'i-
|
|
119
|
-
primary: 'i-
|
|
120
|
-
success: 'i-
|
|
121
|
-
warning: 'i-
|
|
122
|
-
danger: 'i-
|
|
118
|
+
neutral: 'i-ph-info',
|
|
119
|
+
primary: 'i-ph-info',
|
|
120
|
+
success: 'i-ph-check-circle',
|
|
121
|
+
warning: 'i-ph-warning',
|
|
122
|
+
danger: 'i-ph-warning-circle',
|
|
123
123
|
}
|
|
124
124
|
return colorMap[props.color]
|
|
125
125
|
})
|
|
@@ -295,7 +295,7 @@ function handleAction () {
|
|
|
295
295
|
<!-- Icon -->
|
|
296
296
|
<div
|
|
297
297
|
v-if="showIcon"
|
|
298
|
-
class="shrink-0
|
|
298
|
+
class="shrink-0 size-5"
|
|
299
299
|
:class="[iconClass, titleColorClass]"
|
|
300
300
|
/>
|
|
301
301
|
|
|
@@ -355,7 +355,7 @@ function handleAction () {
|
|
|
355
355
|
:label="translate('general_close')"
|
|
356
356
|
variant="tertiary"
|
|
357
357
|
color="neutral"
|
|
358
|
-
icon="i-
|
|
358
|
+
icon="i-ph-x"
|
|
359
359
|
class="shrink-0"
|
|
360
360
|
data-testid="ui-banner-close"
|
|
361
361
|
@click="handleClose"
|
|
@@ -86,6 +86,7 @@ import { computed, getCurrentInstance, watch } from 'vue'
|
|
|
86
86
|
import { useSlotsAsComponents, RenderSlot } from '../../utils/slot'
|
|
87
87
|
import { SIZES, type Size } from '../../constants'
|
|
88
88
|
import { useButtonStyles, BUTTON_COLORS, type ButtonVariant, type ButtonColor } from '../../composables/useButtonStyles'
|
|
89
|
+
import { useIconSize } from '../../composables/useIconSize'
|
|
89
90
|
import UiLoader from '../UiLoader/UiLoader.vue'
|
|
90
91
|
import UiTooltip from '../UiTooltip/UiTooltip.vue'
|
|
91
92
|
import type { RouteLocationRaw } from 'vue-router'
|
|
@@ -107,7 +108,7 @@ export default {
|
|
|
107
108
|
default: 'cta',
|
|
108
109
|
},
|
|
109
110
|
size: {
|
|
110
|
-
type: String,
|
|
111
|
+
type: String as () => Size,
|
|
111
112
|
default: 'md',
|
|
112
113
|
validator: (value: Size) => SIZES.includes(value),
|
|
113
114
|
},
|
|
@@ -223,11 +224,7 @@ export default {
|
|
|
223
224
|
const isLink = computed(() => !finalDisabled.value && (!!props.href || !!props.to))
|
|
224
225
|
|
|
225
226
|
// Icon size classes
|
|
226
|
-
const iconSizeClasses =
|
|
227
|
-
'size-3': props.size === 'sm',
|
|
228
|
-
'size-4': props.size === 'md',
|
|
229
|
-
'size-5': props.size === 'lg',
|
|
230
|
-
}))
|
|
227
|
+
const iconSizeClasses = useIconSize(() => props.size)
|
|
231
228
|
|
|
232
229
|
// Label value
|
|
233
230
|
const labelFinal = computed(() => {
|
|
@@ -237,8 +234,8 @@ export default {
|
|
|
237
234
|
|
|
238
235
|
// Fallback: generate label from icon for icon-only buttons
|
|
239
236
|
if (isSquare.value && props.icon && typeof props.icon === 'string') {
|
|
240
|
-
// Extract readable name from icon class (e.g. "i-
|
|
241
|
-
const iconName = props.icon.replace(/^i-
|
|
237
|
+
// Extract readable name from icon class (e.g. "i-ph-gear" -> "gear")
|
|
238
|
+
const iconName = props.icon.replace(/^i-[a-z0-9]+-/, '').replace(/-/g, ' ')
|
|
242
239
|
return iconName ? `${iconName} icon` : 'icon'
|
|
243
240
|
}
|
|
244
241
|
|
|
@@ -258,17 +255,20 @@ export default {
|
|
|
258
255
|
// Style: Dynamic button classes
|
|
259
256
|
const buttonClasses = computed(() => [
|
|
260
257
|
{
|
|
258
|
+
'size-6': isSquare.value && props.size === 'xs',
|
|
261
259
|
'size-8': isSquare.value && props.size === 'sm',
|
|
262
260
|
'size-10': isSquare.value && props.size === 'md',
|
|
263
261
|
'size-12': isSquare.value && props.size === 'lg',
|
|
264
262
|
},
|
|
265
263
|
{ 'grid place-items-center': isSquare.value },
|
|
266
264
|
{
|
|
265
|
+
// xs reuses the sm text style — the design system defines no xs one
|
|
266
|
+
'h-6 text-action-button-sm': props.size === 'xs',
|
|
267
267
|
'h-8 text-action-button-sm': props.size === 'sm',
|
|
268
268
|
'h-10 text-action-button-md': props.size === 'md',
|
|
269
269
|
'h-12 text-action-button-lg': props.size === 'lg',
|
|
270
270
|
},
|
|
271
|
-
props.size === 'sm' ? 'px-2.5' : props.size === 'lg' ? 'px-4' : 'px-3',
|
|
271
|
+
props.size === 'xs' ? 'px-2' : props.size === 'sm' ? 'px-2.5' : props.size === 'lg' ? 'px-4' : 'px-3',
|
|
272
272
|
// Links should be inline-flex to prevent full width while maintaining flex centering
|
|
273
273
|
{ 'inline-flex': isLink.value },
|
|
274
274
|
])
|
|
@@ -69,6 +69,8 @@ const indicatorStyle = ref<Record<string, string>>({
|
|
|
69
69
|
const indicatorVisible = ref(false)
|
|
70
70
|
|
|
71
71
|
function getRootEl (): HTMLElement | null {
|
|
72
|
+
// HTMLElement does not exist during SSR — this can run server-side.
|
|
73
|
+
if (typeof HTMLElement === 'undefined') return null
|
|
72
74
|
const el = (rootRef.value as unknown as { $el?: unknown })?.$el
|
|
73
75
|
return el instanceof HTMLElement ? el : null
|
|
74
76
|
}
|
|
@@ -122,6 +124,7 @@ useResizeObserver(resizeTarget, debouncedUpdate)
|
|
|
122
124
|
|
|
123
125
|
const sizeClasses = computed(() => {
|
|
124
126
|
switch (props.size) {
|
|
127
|
+
case 'xs': return 'h-6 p-0.5 gap-0.5'
|
|
125
128
|
case 'sm': return 'h-8 p-0.5 gap-0.5'
|
|
126
129
|
case 'lg': return 'h-12 p-1 gap-1'
|
|
127
130
|
case 'md':
|
|
@@ -148,6 +151,7 @@ const INDICATOR_BG: Record<ButtonGroupColor, string> = {
|
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
const INDICATOR_RADIUS: Record<ButtonGroupSize, string> = {
|
|
154
|
+
xs: 'rounded-md',
|
|
151
155
|
sm: 'rounded-md',
|
|
152
156
|
md: 'rounded-lg',
|
|
153
157
|
lg: 'rounded-lg',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ToggleGroupItem } from 'reka-ui'
|
|
3
3
|
import { computed, inject, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
4
|
+
import { useIconSize } from '../../composables/useIconSize'
|
|
4
5
|
import type { ButtonGroupColor } from './types'
|
|
5
6
|
import {
|
|
6
7
|
BUTTON_GROUP_COLOR_KEY,
|
|
@@ -12,11 +13,14 @@ interface UiButtonGroupItemProps {
|
|
|
12
13
|
value: string
|
|
13
14
|
disabled?: boolean
|
|
14
15
|
ariaLabel?: string
|
|
16
|
+
/** Icon class (e.g. "i-ph-text-align-left") rendered before the slot, sized per group size. */
|
|
17
|
+
icon?: string
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
const props = withDefaults(defineProps<UiButtonGroupItemProps>(), {
|
|
18
21
|
disabled: false,
|
|
19
22
|
ariaLabel: undefined,
|
|
23
|
+
icon: undefined,
|
|
20
24
|
})
|
|
21
25
|
|
|
22
26
|
const size = inject(BUTTON_GROUP_SIZE_KEY, computed(() => 'md' as const))
|
|
@@ -26,6 +30,8 @@ const registry = inject(BUTTON_GROUP_REGISTER_KEY, null)
|
|
|
26
30
|
const itemRef = ref<InstanceType<typeof ToggleGroupItem> | null>(null)
|
|
27
31
|
|
|
28
32
|
function resolveEl (): HTMLElement | null {
|
|
33
|
+
// HTMLElement does not exist during SSR — this can run server-side.
|
|
34
|
+
if (typeof HTMLElement === 'undefined') return null
|
|
29
35
|
const el = (itemRef.value as unknown as { $el?: unknown })?.$el
|
|
30
36
|
return el instanceof HTMLElement ? el : null
|
|
31
37
|
}
|
|
@@ -53,6 +59,7 @@ onBeforeUnmount(() => {
|
|
|
53
59
|
// (rail height − rail padding) in horizontal mode, and drives height in vertical mode.
|
|
54
60
|
const sizeClasses = computed(() => {
|
|
55
61
|
switch (size.value) {
|
|
62
|
+
case 'xs': return 'px-2 py-0.5 gap-1 rounded-md text-action-button-sm'
|
|
56
63
|
case 'sm': return 'px-2.5 py-1.5 gap-1 rounded-md text-action-button-sm'
|
|
57
64
|
case 'lg': return 'px-6 py-2.5 gap-2 rounded-lg text-action-button-lg'
|
|
58
65
|
case 'md':
|
|
@@ -60,6 +67,8 @@ const sizeClasses = computed(() => {
|
|
|
60
67
|
}
|
|
61
68
|
})
|
|
62
69
|
|
|
70
|
+
const iconSizeClass = useIconSize(size)
|
|
71
|
+
|
|
63
72
|
// Per-color state classes. The selected fill is drawn by the parent's sliding
|
|
64
73
|
// indicator <div>, so items only set text color per state. Hover bg/text apply
|
|
65
74
|
// only to unselected items (data-[state=off]) so the hover tint never stacks
|
|
@@ -118,6 +127,11 @@ const colorClasses = computed(() => COLOR_CLASSES[color.value])
|
|
|
118
127
|
data-[state=off]:data-[disabled]:hover:bg-transparent"
|
|
119
128
|
:class="[sizeClasses, colorClasses]"
|
|
120
129
|
>
|
|
130
|
+
<span
|
|
131
|
+
v-if="icon"
|
|
132
|
+
:class="[icon, iconSizeClass]"
|
|
133
|
+
class="shrink-0"
|
|
134
|
+
/>
|
|
121
135
|
<slot />
|
|
122
136
|
</ToggleGroupItem>
|
|
123
137
|
</template>
|
|
@@ -176,7 +176,7 @@ defineExpose({
|
|
|
176
176
|
variant="tertiary"
|
|
177
177
|
color="primary"
|
|
178
178
|
size="md"
|
|
179
|
-
icon="i-
|
|
179
|
+
icon="i-ph-caret-left"
|
|
180
180
|
label="Previous slide"
|
|
181
181
|
:disabled="!canScrollPrev"
|
|
182
182
|
@click="scrollPrev"
|
|
@@ -205,7 +205,7 @@ defineExpose({
|
|
|
205
205
|
variant="tertiary"
|
|
206
206
|
color="primary"
|
|
207
207
|
size="md"
|
|
208
|
-
icon="i-
|
|
208
|
+
icon="i-ph-caret-right"
|
|
209
209
|
label="Next slide"
|
|
210
210
|
:disabled="!canScrollNext"
|
|
211
211
|
@click="scrollNext"
|
|
@@ -54,8 +54,8 @@ const presetsLabel = computed<string | null>(() => {
|
|
|
54
54
|
// Preset selector swatch sizes; the selection check icon scales to half the swatch.
|
|
55
55
|
// maxPerRow is how many swatches (at that size + gap) fit the panel's content width.
|
|
56
56
|
const SWATCH_SIZE_CLASSES: Record<UiColorPickerSwatchSize, { swatch: string; swatchPickerList: string; indicatorWrapper: string; indicator: string; maxPerRow: number }> = {
|
|
57
|
-
sm: { swatch: 'size-6', swatchPickerList: 'gap-2', indicatorWrapper: 'ring-2', indicator: '
|
|
58
|
-
lg: { swatch: 'size-12', swatchPickerList: 'gap-4', indicatorWrapper: 'ring-4', indicator: '
|
|
57
|
+
sm: { swatch: 'size-6', swatchPickerList: 'gap-2', indicatorWrapper: 'ring-2', indicator: 'size-3', maxPerRow: 8 },
|
|
58
|
+
lg: { swatch: 'size-12', swatchPickerList: 'gap-4', indicatorWrapper: 'ring-4', indicator: 'size-5', maxPerRow: 4 },
|
|
59
59
|
}
|
|
60
60
|
const swatchSizeClasses = computed(() => SWATCH_SIZE_CLASSES[props.presetConfig?.swatchSize ?? 'lg'])
|
|
61
61
|
|
|
@@ -328,7 +328,7 @@ watch(
|
|
|
328
328
|
<!-- Mode switcher — its own bordered box; hidden when only one mode is available -->
|
|
329
329
|
<div
|
|
330
330
|
v-if="showSwitcher"
|
|
331
|
-
class="h-full w-
|
|
331
|
+
class="h-full w-21 shrink-0"
|
|
332
332
|
>
|
|
333
333
|
<UiDropdown
|
|
334
334
|
v-model="colorMode"
|
|
@@ -456,7 +456,7 @@ watch(
|
|
|
456
456
|
class="absolute inset-0 flex items-center justify-center rounded-full bg-actions-neutral-hover-overlay opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
|
|
457
457
|
>
|
|
458
458
|
<div
|
|
459
|
-
class="i-
|
|
459
|
+
class="i-ph-check-circle-fill text-text-neutral-invert-tertiary"
|
|
460
460
|
:class="swatchSizeClasses.indicator"
|
|
461
461
|
/>
|
|
462
462
|
</span>
|
|
@@ -472,7 +472,7 @@ watch(
|
|
|
472
472
|
:class="swatchSizeClasses.indicatorWrapper"
|
|
473
473
|
>
|
|
474
474
|
<div
|
|
475
|
-
class="i-
|
|
475
|
+
class="i-ph-check-circle-fill text-text-neutral-invert-base"
|
|
476
476
|
:class="swatchSizeClasses.indicator"
|
|
477
477
|
/>
|
|
478
478
|
</span>
|
|
@@ -150,14 +150,14 @@ const goToImage = (index: number) => {
|
|
|
150
150
|
class="absolute left-4 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-surface-neutral-bg-main/80 backdrop-blur-xs flex items-center justify-center hover:bg-surface-neutral-bg-main transition-colors"
|
|
151
151
|
@click="prevImage"
|
|
152
152
|
>
|
|
153
|
-
<span class="i-
|
|
153
|
+
<span class="i-ph-caret-left w-4 h-4 text-text-neutral-base" />
|
|
154
154
|
</button>
|
|
155
155
|
<button
|
|
156
156
|
v-if="images.length > 1"
|
|
157
157
|
class="absolute right-4 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-surface-neutral-bg-main/80 backdrop-blur-xs flex items-center justify-center hover:bg-surface-neutral-bg-main transition-colors"
|
|
158
158
|
@click="nextImage"
|
|
159
159
|
>
|
|
160
|
-
<span class="i-
|
|
160
|
+
<span class="i-ph-caret-right w-4 h-4 text-text-neutral-base" />
|
|
161
161
|
</button>
|
|
162
162
|
|
|
163
163
|
<!-- Dots indicator -->
|
|
@@ -46,7 +46,7 @@ const slots = useSlots()
|
|
|
46
46
|
v-if="showScrollButtons"
|
|
47
47
|
class="flex items-center justify-center h-6 cursor-default text-text-neutral-base"
|
|
48
48
|
>
|
|
49
|
-
<div class="i-
|
|
49
|
+
<div class="i-ph-caret-up size-4" />
|
|
50
50
|
</SelectScrollUpButton>
|
|
51
51
|
|
|
52
52
|
<SelectViewport class="flex flex-col gap-1">
|
|
@@ -57,7 +57,7 @@ const slots = useSlots()
|
|
|
57
57
|
v-if="showScrollButtons"
|
|
58
58
|
class="flex items-center justify-center h-6 cursor-default text-text-neutral-base"
|
|
59
59
|
>
|
|
60
|
-
<div class="i-
|
|
60
|
+
<div class="i-ph-caret-down size-4" />
|
|
61
61
|
</SelectScrollDownButton>
|
|
62
62
|
</div>
|
|
63
63
|
|
|
@@ -48,8 +48,15 @@ const isMultiple = inject(UI_DROPDOWN_MULTIPLE_KEY, undefined)
|
|
|
48
48
|
class="shrink-0 group-data-highlighted:text-actions-neutral-idle group-data-[state=checked]:text-actions-primary-idle"
|
|
49
49
|
/>
|
|
50
50
|
|
|
51
|
+
<div
|
|
52
|
+
v-if="$slots.leading"
|
|
53
|
+
class="shrink-0"
|
|
54
|
+
>
|
|
55
|
+
<slot name="leading" />
|
|
56
|
+
</div>
|
|
57
|
+
|
|
51
58
|
<div class="flex flex-1 flex-col items-start justify-center min-w-0 py-1.5">
|
|
52
|
-
<SelectItemText class="whitespace-nowrap text-content-text-md-
|
|
59
|
+
<SelectItemText class="whitespace-nowrap text-content-text-md-medium text-actions-neutral-idle group-data-highlighted:text-actions-neutral-idle group-data-[state=checked]:text-actions-primary-idle">
|
|
53
60
|
<slot name="title">
|
|
54
61
|
{{ title }}
|
|
55
62
|
</slot>
|
|
@@ -3,6 +3,7 @@ import type { SelectTriggerProps } from 'reka-ui'
|
|
|
3
3
|
import { SelectIcon, SelectTrigger, SelectValue } from 'reka-ui'
|
|
4
4
|
import { computed, useSlots } from 'vue'
|
|
5
5
|
import { useButtonStyles } from '../../composables/useButtonStyles'
|
|
6
|
+
import { useIconSize } from '../../composables/useIconSize'
|
|
6
7
|
|
|
7
8
|
type TSize = 'sm' | 'md' | 'lg' | 'xl'
|
|
8
9
|
type TColor = 'neutral' | 'primary'
|
|
@@ -59,25 +60,21 @@ const sizeClasses = computed(() => {
|
|
|
59
60
|
sm: {
|
|
60
61
|
field: 'h-8 px-2 gap-1',
|
|
61
62
|
text: 'text-action-button-sm tracking-[0.05px]',
|
|
62
|
-
icon: 'size-3',
|
|
63
63
|
padding: 'px-2',
|
|
64
64
|
},
|
|
65
65
|
md: {
|
|
66
66
|
field: 'h-10 px-3 gap-1.5',
|
|
67
67
|
text: 'text-action-button-md leading-[20px] tracking-[-0.04px]',
|
|
68
|
-
icon: 'size-4',
|
|
69
68
|
padding: 'px-3',
|
|
70
69
|
},
|
|
71
70
|
lg: {
|
|
72
71
|
field: 'h-12 px-4 gap-2',
|
|
73
72
|
text: 'text-action-button-lg leading-[20px] tracking-[-0.04px]',
|
|
74
|
-
icon: 'size-4',
|
|
75
73
|
padding: 'px-4',
|
|
76
74
|
},
|
|
77
75
|
xl: {
|
|
78
76
|
field: 'h-16 px-4 gap-2',
|
|
79
77
|
text: 'text-action-button-lg leading-[24px] tracking-[-0.04px]',
|
|
80
|
-
icon: 'size-6',
|
|
81
78
|
padding: 'px-4',
|
|
82
79
|
},
|
|
83
80
|
}
|
|
@@ -85,6 +82,8 @@ const sizeClasses = computed(() => {
|
|
|
85
82
|
return sizes[props.size]
|
|
86
83
|
})
|
|
87
84
|
|
|
85
|
+
const iconSizeClass = useIconSize(() => props.size)
|
|
86
|
+
|
|
88
87
|
const fieldClasses = computed(() => {
|
|
89
88
|
const base = [
|
|
90
89
|
'focus-indicator-none',
|
|
@@ -204,7 +203,7 @@ const iconContainerClasses = computed(() => {
|
|
|
204
203
|
isTertiary.value
|
|
205
204
|
? textClass.value
|
|
206
205
|
: isPrimary.value ? 'text-actions-primary-idle' : 'text-text-neutral-tertiary',
|
|
207
|
-
|
|
206
|
+
iconSizeClass.value,
|
|
208
207
|
].join(' ')
|
|
209
208
|
})
|
|
210
209
|
</script>
|
|
@@ -231,8 +230,8 @@ const iconContainerClasses = computed(() => {
|
|
|
231
230
|
:class="iconContainerClasses"
|
|
232
231
|
>
|
|
233
232
|
<div
|
|
234
|
-
class="i-
|
|
235
|
-
:class="
|
|
233
|
+
class="i-ph-caret-down transition-transform duration-200 ease-in-out group-data-[state=open]:rotate-180"
|
|
234
|
+
:class="iconSizeClass"
|
|
236
235
|
/>
|
|
237
236
|
</SelectIcon>
|
|
238
237
|
</SelectTrigger>
|
|
@@ -54,7 +54,7 @@ defineExpose({ fieldId })
|
|
|
54
54
|
:content="labelInfo"
|
|
55
55
|
>
|
|
56
56
|
<div class="flex items-center justify-center size-4 text-text-neutral-secondary cursor-help">
|
|
57
|
-
<span class="i-
|
|
57
|
+
<span class="i-ph-info size-4" />
|
|
58
58
|
</div>
|
|
59
59
|
</UiTooltip>
|
|
60
60
|
</div>
|
|
@@ -191,7 +191,7 @@ const iconClasses = computed(() => {
|
|
|
191
191
|
|
|
192
192
|
// Icon type and color based on state
|
|
193
193
|
if (props.indeterminate) {
|
|
194
|
-
baseClasses.push('i-
|
|
194
|
+
baseClasses.push('i-ph-minus text-actions-neutral-invert-idle opacity-100')
|
|
195
195
|
} else if (isChecked.value) {
|
|
196
196
|
// Checked: always visible with light color on primary/error background
|
|
197
197
|
baseClasses.push('i-ls-checkbox-check text-actions-neutral-invert-idle opacity-100')
|
|
@@ -403,7 +403,7 @@ defineExpose({
|
|
|
403
403
|
'text-text-primary-base': isChecked && !error,
|
|
404
404
|
}"
|
|
405
405
|
>
|
|
406
|
-
<span class="i-
|
|
406
|
+
<span class="i-ph-info-fill size-4" />
|
|
407
407
|
</div>
|
|
408
408
|
</UiTooltip>
|
|
409
409
|
</div>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import { useStorage } from '@vueuse/core'
|
|
4
|
+
import EmojiPicker from 'vue-frimousse'
|
|
5
|
+
import UiPopover from '../UiPopover/UiPopover.vue'
|
|
6
|
+
import UiPopoverContent from '../UiPopover/UiPopoverContent.vue'
|
|
7
|
+
import UiPopoverTrigger from '../UiPopover/UiPopoverTrigger.vue'
|
|
8
|
+
|
|
9
|
+
interface UiInputEmojiProps {
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
label?: string
|
|
12
|
+
locale?: string
|
|
13
|
+
searchPlaceholder?: string
|
|
14
|
+
loadingLabel?: string
|
|
15
|
+
emptyLabel?: string
|
|
16
|
+
previewPlaceholder?: string
|
|
17
|
+
skinToneLabel?: string
|
|
18
|
+
sideOffset?: number
|
|
19
|
+
open?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<UiInputEmojiProps>(), {
|
|
23
|
+
disabled: false,
|
|
24
|
+
label: 'Emoji',
|
|
25
|
+
locale: 'en',
|
|
26
|
+
searchPlaceholder: 'Search emoji...',
|
|
27
|
+
loadingLabel: 'Loading...',
|
|
28
|
+
emptyLabel: 'No emoji found',
|
|
29
|
+
previewPlaceholder: 'Select an emoji',
|
|
30
|
+
skinToneLabel: 'Change skin tone',
|
|
31
|
+
sideOffset: 8,
|
|
32
|
+
open: undefined,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const emit = defineEmits<{
|
|
36
|
+
select: [emoji: string]
|
|
37
|
+
'update:open': [value: boolean]
|
|
38
|
+
}>()
|
|
39
|
+
|
|
40
|
+
const uncontrolledOpen = ref(false)
|
|
41
|
+
type SkinTone = 'none' | 'light' | 'medium-light' | 'medium' | 'medium-dark' | 'dark'
|
|
42
|
+
const skinTones: SkinTone[] = ['none', 'light', 'medium-light', 'medium', 'medium-dark', 'dark']
|
|
43
|
+
const preferredSkinTone = useStorage<SkinTone>('livestorm:emoji-skin-tone', 'none')
|
|
44
|
+
|
|
45
|
+
const open = computed({
|
|
46
|
+
get: () => props.open ?? uncontrolledOpen.value,
|
|
47
|
+
set: (value: boolean) => {
|
|
48
|
+
uncontrolledOpen.value = value
|
|
49
|
+
emit('update:open', value)
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const locale = computed(() => props.locale.toLowerCase().replace('_', '-'))
|
|
54
|
+
|
|
55
|
+
function selectEmoji (emoji: { emoji: string }): void {
|
|
56
|
+
emit('select', emoji.emoji)
|
|
57
|
+
open.value = false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function cycleSkinTone (): void {
|
|
61
|
+
const currentIndex = skinTones.indexOf(preferredSkinTone.value)
|
|
62
|
+
preferredSkinTone.value = skinTones[(currentIndex + 1) % skinTones.length] ?? 'none'
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
65
|
+
|
|
66
|
+
<template>
|
|
67
|
+
<UiPopover v-model:open="open">
|
|
68
|
+
<UiPopoverTrigger
|
|
69
|
+
as-child
|
|
70
|
+
:disabled="disabled"
|
|
71
|
+
>
|
|
72
|
+
<slot
|
|
73
|
+
:open="open"
|
|
74
|
+
:disabled="disabled"
|
|
75
|
+
>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
:aria-label="label"
|
|
79
|
+
:disabled="disabled"
|
|
80
|
+
class="inline-flex size-8 items-center justify-center rounded-lg text-actions-neutral-idle hover:bg-actions-neutral-hover-overlay disabled:pointer-events-none disabled:opacity-50"
|
|
81
|
+
>
|
|
82
|
+
<span
|
|
83
|
+
class="i-ph-smiley size-4"
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
/>
|
|
86
|
+
</button>
|
|
87
|
+
</slot>
|
|
88
|
+
</UiPopoverTrigger>
|
|
89
|
+
|
|
90
|
+
<UiPopoverContent
|
|
91
|
+
side="top"
|
|
92
|
+
:side-offset="sideOffset"
|
|
93
|
+
class="w-[min(320px,calc(100vw-16px))] overflow-hidden border-borders-neutral-default bg-surface-neutral-bg-main p-0"
|
|
94
|
+
:aria-label="label"
|
|
95
|
+
>
|
|
96
|
+
<EmojiPicker.Root
|
|
97
|
+
:locale="locale"
|
|
98
|
+
:columns="9"
|
|
99
|
+
:skin-tone="preferredSkinTone"
|
|
100
|
+
:on-emoji-select="selectEmoji"
|
|
101
|
+
class="flex flex-col gap-2 p-3"
|
|
102
|
+
>
|
|
103
|
+
<EmojiPicker.Search
|
|
104
|
+
class="h-8 w-full rounded-md border border-borders-neutral-default bg-surface-neutral-bg-main px-2 text-content-legends-medium-md text-text-neutral-base outline-none placeholder:text-text-neutral-tertiary focus:border-actions-primary-idle-alpha-strong focus:ring-1 focus:ring-focus-ring"
|
|
105
|
+
:placeholder="searchPlaceholder"
|
|
106
|
+
/>
|
|
107
|
+
|
|
108
|
+
<EmojiPicker.Viewport class="box-border h-64 pr-2">
|
|
109
|
+
<EmojiPicker.Loading class="flex h-64 items-center justify-center text-content-legends-medium-md text-text-neutral-tertiary">
|
|
110
|
+
{{ loadingLabel }}
|
|
111
|
+
</EmojiPicker.Loading>
|
|
112
|
+
<EmojiPicker.Empty class="flex h-64 items-center justify-center text-content-legends-medium-md text-text-neutral-tertiary">
|
|
113
|
+
{{ emptyLabel }}
|
|
114
|
+
</EmojiPicker.Empty>
|
|
115
|
+
<EmojiPicker.List class="flex w-full flex-col gap-2">
|
|
116
|
+
<template #category-header="{ category }">
|
|
117
|
+
<div class="bg-surface-neutral-bg-main py-1 text-content-legends-bold-md text-text-neutral-secondary">
|
|
118
|
+
{{ category.label }}
|
|
119
|
+
</div>
|
|
120
|
+
</template>
|
|
121
|
+
|
|
122
|
+
<template #emoji="{ emoji }">
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
:aria-label="emoji.label"
|
|
126
|
+
class="flex size-8 w-full! items-center justify-center rounded-md text-xl leading-none hover:bg-actions-neutral-hover-overlay data-[active]:bg-actions-neutral-hover-overlay"
|
|
127
|
+
>
|
|
128
|
+
{{ emoji.emoji }}
|
|
129
|
+
</button>
|
|
130
|
+
</template>
|
|
131
|
+
</EmojiPicker.List>
|
|
132
|
+
</EmojiPicker.Viewport>
|
|
133
|
+
|
|
134
|
+
<div class="flex h-10 items-center justify-between gap-2 border-t border-borders-neutral-default pt-2">
|
|
135
|
+
<EmojiPicker.ActiveEmoji>
|
|
136
|
+
<template #default="{ emoji }">
|
|
137
|
+
<span
|
|
138
|
+
v-if="emoji"
|
|
139
|
+
class="flex h-6 min-w-0 flex-1 items-center gap-2 text-content-legends-medium-md text-text-neutral-secondary"
|
|
140
|
+
>
|
|
141
|
+
<span class="flex size-6 shrink-0 items-center justify-center text-xl leading-none">{{ emoji.emoji }}</span>
|
|
142
|
+
<span class="min-w-0 truncate">{{ emoji.label }}</span>
|
|
143
|
+
</span>
|
|
144
|
+
<span
|
|
145
|
+
v-else
|
|
146
|
+
class="flex h-6 min-w-0 flex-1 items-center text-content-legends-medium-md text-text-neutral-secondary"
|
|
147
|
+
>{{ previewPlaceholder }}</span>
|
|
148
|
+
</template>
|
|
149
|
+
</EmojiPicker.ActiveEmoji>
|
|
150
|
+
|
|
151
|
+
<EmojiPicker.SkinToneSelector
|
|
152
|
+
class="flex size-8 shrink-0 items-center justify-center rounded-md text-xl leading-none hover:bg-actions-neutral-hover-overlay"
|
|
153
|
+
:aria-label="skinToneLabel"
|
|
154
|
+
@click="cycleSkinTone"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
</EmojiPicker.Root>
|
|
158
|
+
</UiPopoverContent>
|
|
159
|
+
</UiPopover>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<style scoped>
|
|
163
|
+
:deep([frimousse-row]) {
|
|
164
|
+
display: grid !important;
|
|
165
|
+
width: 100%;
|
|
166
|
+
grid-template-columns: repeat(9, minmax(0, 1fr));
|
|
167
|
+
gap: 0.125rem;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
:deep([frimousse-row] > [frimousse-emoji]) {
|
|
171
|
+
min-width: 0;
|
|
172
|
+
}
|
|
173
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as UiInputEmoji } from './UiInputEmoji.vue'
|