@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.
Files changed (89) hide show
  1. package/README.md +8 -5
  2. package/dist/UiTimelineItem.vue_vue_type_style_index_0_lang-LpdvGf5X.mjs +6093 -0
  3. package/dist/a11y-BhF_nl7L.mjs +159 -0
  4. package/dist/components.mjs +105 -96
  5. package/dist/index.mjs +68 -64
  6. package/dist/layouts.mjs +16 -16
  7. package/dist/types/index.d.ts +5 -1
  8. package/dist/types/src/components/UiActionBar/UiActionBar.vue.d.ts +3 -1
  9. package/dist/types/src/components/UiActionBar/UiActionBarItem.vue.d.ts +18 -3
  10. package/dist/types/src/components/UiActionBar/types.d.ts +3 -1
  11. package/dist/types/src/components/UiButton/UiButton.vue.d.ts +17 -13
  12. package/dist/types/src/components/UiButtonGroup/UiButtonGroupItem.vue.d.ts +3 -0
  13. package/dist/types/src/components/UiButtonGroup/types.d.ts +1 -1
  14. package/dist/types/src/components/UiDropdown/UiDropdownItem.vue.d.ts +6 -4
  15. package/dist/types/src/components/UiInputEmoji/UiInputEmoji.vue.d.ts +45 -0
  16. package/dist/types/src/components/UiInputEmoji/index.d.ts +1 -0
  17. package/dist/types/src/components/UiInputRadio/UiInputRadio.vue.d.ts +67 -0
  18. package/dist/types/src/components/UiInputRadio/UiInputRadioGroup.vue.d.ts +81 -0
  19. package/dist/types/src/components/UiInputRadio/index.d.ts +3 -0
  20. package/dist/types/src/components/UiInputRadio/types.d.ts +19 -0
  21. package/dist/types/src/components/UiLoader/UiLoader.vue.d.ts +6 -5
  22. package/dist/types/src/components/UiLogo/UiLogo.vue.d.ts +9 -3
  23. package/dist/types/src/components/UiPage/UiPage.vue.d.ts +34 -0
  24. package/dist/types/src/components/UiProgress/UiProgress.vue.d.ts +1 -1
  25. package/dist/types/src/components/UiSwitch/UiSwitch.vue.d.ts +109 -0
  26. package/dist/types/src/components/UiSwitch/index.d.ts +2 -0
  27. package/dist/types/src/components/UiSwitch/types.d.ts +3 -0
  28. package/dist/types/src/components/UiTabs/UiTabs.vue.d.ts +5 -2
  29. package/dist/types/src/composables/useContentMode.d.ts +25 -0
  30. package/dist/types/src/composables/useIconSize.d.ts +11 -0
  31. package/dist/types/src/constants/sizes.d.ts +1 -1
  32. package/dist/ui-vue.css +1 -1
  33. package/dist/{useThemeAndModes-CUK56j0z.mjs → useThemeAndModes-BzYwSOEv.mjs} +1 -1
  34. package/dist/utils.mjs +154 -99
  35. package/index.d.ts +4 -0
  36. package/index.js +8 -0
  37. package/package.json +11 -3
  38. package/src/components/Toast/ToastMessage.vue +6 -6
  39. package/src/components/UiActionBar/UiActionBar.vue +11 -2
  40. package/src/components/UiActionBar/UiActionBarItem.vue +56 -32
  41. package/src/components/UiActionBar/types.ts +3 -1
  42. package/src/components/UiAvatar/UiAvatar.vue +1 -1
  43. package/src/components/UiBanner/UiBanner.vue +7 -7
  44. package/src/components/UiButton/UiButton.vue +9 -9
  45. package/src/components/UiButtonGroup/UiButtonGroup.vue +4 -0
  46. package/src/components/UiButtonGroup/UiButtonGroupItem.vue +14 -0
  47. package/src/components/UiButtonGroup/types.ts +1 -1
  48. package/src/components/UiCarousel/UiCarousel.vue +2 -2
  49. package/src/components/UiColorPicker/UiColorPicker.vue +5 -5
  50. package/src/components/UiDialog/MarketplaceHeaderExample.vue +2 -2
  51. package/src/components/UiDialog/UiDialogClose.vue +1 -1
  52. package/src/components/UiDropdown/UiDropdownContent.vue +2 -2
  53. package/src/components/UiDropdown/UiDropdownItem.vue +8 -1
  54. package/src/components/UiDropdown/UiDropdownTrigger.vue +6 -7
  55. package/src/components/UiFieldWrapper/UiFieldWrapper.vue +1 -1
  56. package/src/components/UiInputCheckbox/UiInputCheckbox.vue +2 -2
  57. package/src/components/UiInputEmoji/UiInputEmoji.vue +173 -0
  58. package/src/components/UiInputEmoji/index.ts +1 -0
  59. package/src/components/UiInputRadio/UiInputRadio.vue +374 -0
  60. package/src/components/UiInputRadio/UiInputRadioGroup.vue +156 -0
  61. package/src/components/UiInputRadio/index.ts +3 -0
  62. package/src/components/UiInputRadio/types.ts +25 -0
  63. package/src/components/UiLoader/UiLoader.vue +8 -10
  64. package/src/components/UiLogo/UiLogo.vue +41 -7
  65. package/src/components/UiMenu/UiMenuContent.vue +1 -1
  66. package/src/components/UiMenu/UiMenuGroup.vue +1 -1
  67. package/src/components/UiMenu/UiMenuItem.vue +1 -1
  68. package/src/components/UiMenu/UiMenuSubContent.vue +1 -1
  69. package/src/components/UiMenu/UiMenuSubTrigger.vue +2 -2
  70. package/src/components/UiPage/UiPage.vue +102 -29
  71. package/src/components/UiPageHeader/UiPageHeader.vue +29 -9
  72. package/src/components/UiRoomButton/UiRoomButton.vue +4 -4
  73. package/src/components/UiShell/UiShell.vue +1 -1
  74. package/src/components/UiSideMenu/UiSideMenu.vue +13 -10
  75. package/src/components/UiSideMenu/UiSideMenuItemContent.vue +2 -2
  76. package/src/components/UiStepper/UiStepperIndicator.vue +8 -11
  77. package/src/components/UiSwitch/UiSwitch.vue +359 -0
  78. package/src/components/UiSwitch/index.ts +2 -0
  79. package/src/components/UiSwitch/types.ts +5 -0
  80. package/src/components/UiTabs/UiTabs.vue +7 -4
  81. package/src/components/UiTextField/UiTextField.vue +6 -11
  82. package/src/components/UiTimeline/README.md +6 -6
  83. package/src/components/UiTimeline/UiTimelineItem.vue +3 -3
  84. package/src/composables/useContentMode.ts +53 -0
  85. package/src/composables/useIconSize.ts +28 -0
  86. package/src/constants/sizes.ts +1 -1
  87. package/src/global.d.ts +8 -0
  88. package/dist/UiTimelineItem.vue_vue_type_style_index_0_lang-BtG5oABt.mjs +0 -5454
  89. package/dist/a11y--y7uaY4j.mjs +0 -217
@@ -80,7 +80,7 @@ const props = withDefaults(defineProps<Props>(), {
80
80
  variant: 'default',
81
81
  alt: undefined,
82
82
  color: null,
83
- icon: 'i-ls-user-fill',
83
+ icon: 'i-ph-user-fill',
84
84
  })
85
85
 
86
86
  const customStyle = computed(() => {
@@ -115,11 +115,11 @@ onMounted(() => {
115
115
  // Icon mapping based on color
116
116
  const iconClass = computed(() => {
117
117
  const colorMap = {
118
- neutral: 'i-ls-info',
119
- primary: 'i-ls-info',
120
- success: 'i-ls-check-circle',
121
- warning: 'i-ls-alert-triangle',
122
- danger: 'i-ls-alert-circle',
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 w-4 h-4"
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-ls-x"
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 = computed(() => ({
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-ls-settings" -> "settings")
241
- const iconName = props.icon.replace(/^i-ls-/, '').replace(/-/g, ' ')
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>
@@ -1,6 +1,6 @@
1
1
  import type { ComputedRef, InjectionKey } from 'vue'
2
2
 
3
- export type ButtonGroupSize = 'sm' | 'md' | 'lg'
3
+ export type ButtonGroupSize = 'xs' | 'sm' | 'md' | 'lg'
4
4
  export type ButtonGroupColor =
5
5
  | 'neutral'
6
6
  | 'neutral-invert'
@@ -176,7 +176,7 @@ defineExpose({
176
176
  variant="tertiary"
177
177
  color="primary"
178
178
  size="md"
179
- icon="i-ls-chevron-left"
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-ls-chevron-right"
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: 'h-3 w-3', maxPerRow: 8 },
58
- lg: { swatch: 'size-12', swatchPickerList: 'gap-4', indicatorWrapper: 'ring-4', indicator: 'h-4 w-4', maxPerRow: 4 },
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-20 shrink-0"
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-ls-check-circle-fill text-text-neutral-invert-tertiary"
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-ls-check-circle-fill text-text-neutral-invert-base"
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-ls-chevron-left w-4 h-4 text-text-neutral-base" />
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-ls-chevron-right w-4 h-4 text-text-neutral-base" />
160
+ <span class="i-ph-caret-right w-4 h-4 text-text-neutral-base" />
161
161
  </button>
162
162
 
163
163
  <!-- Dots indicator -->
@@ -21,7 +21,7 @@ withDefaults(defineProps<UiDialogCloseProps>(), {
21
21
  variant="tertiary"
22
22
  color="neutral"
23
23
  size="md"
24
- icon="i-ls-x-stream"
24
+ icon="i-ph-x"
25
25
  :aria-label="ariaLabel"
26
26
  :data-colors-semantic="mode"
27
27
  />
@@ -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-ls-chevron-up size-4" />
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-ls-chevron-down size-4" />
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-regular text-actions-neutral-idle group-data-highlighted:text-actions-neutral-idle group-data-[state=checked]:text-actions-primary-idle">
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
- sizeClasses.value.icon,
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-ls-chevron-down transition-transform duration-200 ease-in-out group-data-[state=open]:rotate-180"
235
- :class="sizeClasses.icon"
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-ls-info size-4" />
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-ls-minus text-actions-neutral-invert-idle opacity-100')
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-ls-info-fill size-4" />
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'