@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
@@ -0,0 +1,359 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+ import type { ComponentPublicInstance } from 'vue'
4
+ import { Label, SwitchRoot, SwitchThumb, useId } from 'reka-ui'
5
+ import type { SwitchColor, SwitchPosition, SwitchSize } from './types'
6
+ import UiTooltip from '../UiTooltip/UiTooltip.vue'
7
+
8
+ interface UiSwitchProps {
9
+ /**
10
+ * Whether the switch is on
11
+ * @default false
12
+ */
13
+ modelValue?: boolean
14
+ /**
15
+ * The label text
16
+ */
17
+ label?: string
18
+ /**
19
+ * Description text below the label
20
+ */
21
+ description?: string
22
+ /**
23
+ * Info tooltip for the label
24
+ */
25
+ labelInfo?: string
26
+ /**
27
+ * Icon class rendered between the switch and the text (e.g. `i-ph-bell`)
28
+ */
29
+ icon?: string
30
+ /**
31
+ * Whether the switch is disabled
32
+ * @default false
33
+ */
34
+ disabled?: boolean
35
+ /**
36
+ * Whether the switch has an error
37
+ * @default false
38
+ */
39
+ error?: boolean
40
+ /**
41
+ * Color of the on state (error takes precedence)
42
+ * @default 'primary'
43
+ */
44
+ color?: SwitchColor
45
+ /**
46
+ * Size of the switch
47
+ * @default 'sm'
48
+ */
49
+ size?: SwitchSize
50
+ /**
51
+ * Which side of the text the switch sits on
52
+ * @default 'left'
53
+ */
54
+ switchPosition?: SwitchPosition
55
+ /**
56
+ * Whether to show a background behind the switch and its text
57
+ * @default false
58
+ */
59
+ showBg?: boolean
60
+ /**
61
+ * Whether the switch stretches to fill its container, which spreads the
62
+ * text and the switch to opposite edges
63
+ * @default false
64
+ */
65
+ fullWidth?: boolean
66
+ /**
67
+ * Switch name attribute (renders a hidden input for native forms)
68
+ */
69
+ name?: string
70
+ /**
71
+ * Switch id attribute
72
+ */
73
+ id?: string
74
+ }
75
+
76
+ const props = withDefaults(defineProps<UiSwitchProps>(), {
77
+ modelValue: false,
78
+ label: undefined,
79
+ description: undefined,
80
+ labelInfo: undefined,
81
+ icon: undefined,
82
+ disabled: false,
83
+ error: false,
84
+ color: 'primary',
85
+ size: 'sm',
86
+ switchPosition: 'left',
87
+ showBg: false,
88
+ fullWidth: false,
89
+ name: undefined,
90
+ id: undefined,
91
+ })
92
+
93
+ const emit = defineEmits<{
94
+ 'update:modelValue': [value: boolean]
95
+ 'focus': [event: FocusEvent]
96
+ 'blur': [event: FocusEvent]
97
+ }>()
98
+
99
+ const generatedId = useId()
100
+ const inputId = computed(() => props.id ?? generatedId)
101
+
102
+ const isChecked = computed(() => props.modelValue)
103
+
104
+ // Design palette: primary | warning, with error mapping onto the danger tokens
105
+ const palette = computed(() => props.error ? 'danger' : props.color === 'warning' ? 'warning' : 'primary')
106
+
107
+ const descriptionId = computed(() => props.description ? `${inputId.value}-description` : undefined)
108
+
109
+ const trackClasses = computed(() => {
110
+ const baseClasses = [
111
+ 'relative shrink-0 inline-flex items-center rounded-full',
112
+ 'border p-[3px] transition-colors',
113
+ 'focus:outline-hidden',
114
+ 'cursor-pointer disabled:cursor-not-allowed',
115
+ ]
116
+
117
+ // sm: 40x24 track / 16px thumb, xs: 32x20 track / 12px thumb
118
+ baseClasses.push(props.size === 'xs' ? 'h-5 w-8' : 'h-6 w-10')
119
+
120
+ if (isChecked.value) {
121
+ const checkedClasses = {
122
+ primary: 'bg-actions-primary-idle-alpha-strong border-borders-primary-default',
123
+ warning: 'bg-actions-warning-idle-alpha-strong border-actions-warning-idle-alpha-strong',
124
+ danger: 'bg-actions-danger-idle-alpha-strong border-actions-danger-idle-alpha-strong',
125
+ }
126
+ baseClasses.push(checkedClasses[palette.value])
127
+ if (!props.disabled) {
128
+ // On warning/danger the border lightens on hover (primary keeps its border)
129
+ const checkedHoverClasses = {
130
+ primary: '',
131
+ warning: 'group-hover:border-borders-warning-default',
132
+ danger: 'group-hover:border-borders-danger-default',
133
+ }
134
+ baseClasses.push(checkedHoverClasses[palette.value])
135
+ }
136
+ } else {
137
+ const uncheckedBorderClasses = {
138
+ primary: 'border-borders-neutral-strong',
139
+ warning: 'border-borders-warning-strong',
140
+ danger: 'border-borders-danger-strong',
141
+ }
142
+ baseClasses.push('bg-surface-neutral-bg-main', uncheckedBorderClasses[palette.value])
143
+ if (!props.disabled) {
144
+ // Border thickens on hover; padding shrinks in step so the thumb
145
+ // keeps its exact position and travel distance
146
+ baseClasses.push('group-hover:border-2 group-hover:p-0.5')
147
+ }
148
+ }
149
+
150
+ // Focus ring
151
+ if (props.error) {
152
+ baseClasses.push('focus-visible:ring focus-visible:ring-borders-danger-strong')
153
+ } else if (isChecked.value) {
154
+ baseClasses.push(props.color === 'warning'
155
+ ? 'focus-visible:ring focus-visible:ring-borders-warning-strong'
156
+ : 'focus-visible:ring focus-visible:ring-borders-primary-strong')
157
+ } else {
158
+ baseClasses.push('focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-focus-ring')
159
+ }
160
+
161
+ return baseClasses
162
+ })
163
+
164
+ const trackHoverOverlayClasses = computed(() => {
165
+ const overlayClasses = {
166
+ primary: isChecked.value ? 'bg-actions-primary-hover-overlay' : 'bg-actions-neutral-hover-overlay',
167
+ warning: 'bg-actions-warning-hover-overlay',
168
+ danger: 'bg-actions-danger-hover-overlay',
169
+ }
170
+ return overlayClasses[palette.value]
171
+ })
172
+
173
+ const thumbClasses = computed(() => {
174
+ const baseClasses = ['pointer-events-none block rounded-full transition-transform']
175
+
176
+ if (props.size === 'xs') {
177
+ baseClasses.push('size-3 data-[state=checked]:translate-x-3')
178
+ } else {
179
+ baseClasses.push('size-4 data-[state=checked]:translate-x-4')
180
+ }
181
+
182
+ baseClasses.push(isChecked.value
183
+ ? 'bg-actions-neutral-invert-idle'
184
+ : 'bg-actions-neutral-idle-alpha-strong')
185
+
186
+ return baseClasses
187
+ })
188
+
189
+ const innerWrapperClasses = computed(() => {
190
+ const classes = ['group relative flex items-center gap-2 rounded-md']
191
+
192
+ if (props.switchPosition === 'right') {
193
+ classes.push('flex-row-reverse')
194
+ }
195
+
196
+ if (props.disabled) {
197
+ classes.push('opacity-50 cursor-not-allowed')
198
+ } else {
199
+ classes.push('cursor-pointer')
200
+ }
201
+
202
+ if (props.showBg) {
203
+ classes.push('p-2 shadow-[0px_1px_2px_0px_var(--color-shadow-neutral-100)]')
204
+ classes.push(props.error ? 'bg-actions-danger-idle-alpha-light' : 'bg-actions-neutral-idle-alpha-light')
205
+ }
206
+
207
+ return classes
208
+ })
209
+
210
+ const labelClasses = computed(() => ({
211
+ 'text-text-danger-base': props.error,
212
+ 'text-text-neutral-base': !props.error,
213
+ 'cursor-not-allowed': props.disabled,
214
+ 'cursor-pointer': !props.disabled,
215
+ }))
216
+
217
+ // Text hugs the switch when the component sizes to its content, but stays at
218
+ // the leading edge when it stretches, so the row reads label-left/switch-right.
219
+ const textAlignsToSwitch = computed(() => props.switchPosition === 'right' && !props.fullWidth)
220
+
221
+ const descriptionClasses = computed(() => ({
222
+ 'text-right': textAlignsToSwitch.value,
223
+ 'text-text-danger-tertiary': props.error,
224
+ 'text-text-neutral-tertiary': !props.error,
225
+ 'cursor-not-allowed': props.disabled,
226
+ 'cursor-pointer': !props.disabled,
227
+ }))
228
+
229
+ // SwitchRoot is a generic component, so InstanceType<typeof SwitchRoot> can't be used
230
+ const switchRef = ref<ComponentPublicInstance | null>(null)
231
+
232
+ function switchElement (): HTMLElement | undefined {
233
+ return (switchRef.value?.$el as HTMLElement | undefined) ?? undefined
234
+ }
235
+
236
+ // Make the whole wrapper (description, icon, background) toggle the switch.
237
+ // Clicks on the button itself or on the label are skipped: the button handles
238
+ // its own clicks and the label forwards to the button natively via `for`.
239
+ function onWrapperClick (event: MouseEvent) {
240
+ if (props.disabled) {
241
+ return
242
+ }
243
+
244
+ const target = event.target as HTMLElement
245
+ const switchEl = switchElement()
246
+
247
+ if (!switchEl || target === switchEl || switchEl.contains(target)) {
248
+ return
249
+ }
250
+ if (target.closest('label')) {
251
+ return
252
+ }
253
+
254
+ switchEl.click()
255
+ }
256
+
257
+ defineExpose({
258
+ focus: () => switchElement()?.focus(),
259
+ blur: () => switchElement()?.blur(),
260
+ })
261
+ </script>
262
+
263
+ <template>
264
+ <div
265
+ class="ui-switch-wrapper"
266
+ :class="fullWidth ? 'w-full' : 'w-fit'"
267
+ >
268
+ <div
269
+ :class="innerWrapperClasses"
270
+ @click="onWrapperClick"
271
+ >
272
+ <SwitchRoot
273
+ :id="inputId"
274
+ ref="switchRef"
275
+ :model-value="modelValue"
276
+ :disabled="disabled"
277
+ :name="name"
278
+ :class="trackClasses"
279
+ :aria-invalid="error || undefined"
280
+ :aria-describedby="descriptionId"
281
+ @update:model-value="emit('update:modelValue', $event)"
282
+ @focus="emit('focus', $event)"
283
+ @blur="emit('blur', $event)"
284
+ >
285
+ <SwitchThumb :class="thumbClasses" />
286
+
287
+ <div
288
+ v-if="!disabled"
289
+ class="absolute inset-0 rounded-full opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
290
+ :class="trackHoverOverlayClasses"
291
+ />
292
+ </SwitchRoot>
293
+
294
+ <!-- optional leading icon -->
295
+ <slot name="icon">
296
+ <span
297
+ v-if="icon"
298
+ class="size-4 shrink-0"
299
+ :class="[icon, error ? 'text-text-danger-base' : 'text-text-neutral-secondary']"
300
+ />
301
+ </slot>
302
+
303
+ <div
304
+ v-if="label || description || $slots.label"
305
+ class="flex flex-col min-w-0 flex-1"
306
+ :class="{ 'items-end': textAlignsToSwitch }"
307
+ >
308
+ <div
309
+ class="flex items-center gap-2"
310
+ :class="{ 'flex-row-reverse': textAlignsToSwitch }"
311
+ >
312
+ <Label
313
+ :for="inputId"
314
+ class="text-content-text-md-medium"
315
+ :class="labelClasses"
316
+ >
317
+ <slot name="label">
318
+ {{ label }}
319
+ </slot>
320
+ </Label>
321
+
322
+ <!-- info tooltip -->
323
+ <UiTooltip
324
+ v-if="labelInfo"
325
+ :content="labelInfo"
326
+ >
327
+ <!-- stays neutral even in error state, per design -->
328
+ <div
329
+ class="flex items-center justify-center size-4 cursor-help shrink-0 text-text-neutral-tertiary"
330
+ @click.stop
331
+ >
332
+ <span class="i-ph-info-fill size-4" />
333
+ </div>
334
+ </UiTooltip>
335
+ </div>
336
+
337
+ <span
338
+ v-if="description"
339
+ :id="descriptionId"
340
+ class="text-content-text-md-medium"
341
+ :class="descriptionClasses"
342
+ >
343
+ {{ description }}
344
+ </span>
345
+ </div>
346
+
347
+ <!-- Background hover overlay -->
348
+ <div
349
+ v-if="showBg && !disabled"
350
+ class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none rounded-md"
351
+ :class="{
352
+ 'bg-actions-danger-hover-overlay': error,
353
+ 'bg-actions-primary-hover-overlay': isChecked && !error,
354
+ 'bg-actions-neutral-hover-overlay': !isChecked && !error,
355
+ }"
356
+ />
357
+ </div>
358
+ </div>
359
+ </template>
@@ -0,0 +1,2 @@
1
+ export { default as UiSwitch } from './UiSwitch.vue'
2
+ export type { SwitchColor, SwitchPosition, SwitchSize } from './types'
@@ -0,0 +1,5 @@
1
+ export type SwitchSize = 'sm' | 'xs'
2
+
3
+ export type SwitchColor = 'primary' | 'warning'
4
+
5
+ export type SwitchPosition = 'left' | 'right'
@@ -1,6 +1,6 @@
1
1
  <!-- UiTabs.vue - Tabs Component -->
2
2
  <script setup lang="ts">
3
- import { computed, ref, watch, nextTick, onMounted, onBeforeUnmount, type Slot, type ComponentPublicInstance } from 'vue'
3
+ import { computed, ref, watch, nextTick, onMounted, onBeforeUnmount, type ComponentPublicInstance } from 'vue'
4
4
  import { useResizeObserver, useDebounceFn } from '@vueuse/core'
5
5
 
6
6
  export interface Tab {
@@ -12,6 +12,8 @@ export interface Tab {
12
12
  icon?: string
13
13
  /** Whether the tab is disabled */
14
14
  disabled?: boolean
15
+ /** Optional data-testid rendered on the tab button */
16
+ testId?: string
15
17
  }
16
18
 
17
19
  export interface UiTabsProps {
@@ -32,7 +34,7 @@ const emit = defineEmits<{
32
34
  }>()
33
35
 
34
36
  const slots = defineSlots<{
35
- badge?(): Slot
37
+ badge?(props: { tab: Tab }): unknown
36
38
  }>()
37
39
 
38
40
  const hasBadgeSlot = computed(() => !!slots.badge)
@@ -40,7 +42,7 @@ const hasBadgeSlot = computed(() => !!slots.badge)
40
42
  const sizeClasses = computed(() => {
41
43
  return {
42
44
  container: 'gap-4',
43
- tab: props.size === 'sm' ? 'pb-4' : 'pb-6',
45
+ tab: props.size === 'sm' ? 'pb-4' : 'pb-5',
44
46
  text: props.size === 'sm' ? 'text-action-button-sm' : 'text-action-button-md',
45
47
  }
46
48
  })
@@ -173,6 +175,7 @@ onBeforeUnmount(() => {
173
175
  role="tab"
174
176
  :aria-selected="isActive(tab.value)"
175
177
  :aria-disabled="tab.disabled"
178
+ :data-testid="tab.testId"
176
179
  :tabindex="isActive(tab.value) ? 0 : -1"
177
180
  class="relative outline-none flex items-center gap-2 cursor-pointer font-semibold transition-colors duration-150"
178
181
  :class="[
@@ -191,7 +194,7 @@ onBeforeUnmount(() => {
191
194
  <span
192
195
  v-if="tab.icon"
193
196
  :class="tab.icon"
194
- class="w-4 h-4"
197
+ class="size-5"
195
198
  />
196
199
  <span>{{ tab.label }}</span>
197
200
  <div
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, nextTick, ref, useSlots } from 'vue'
3
3
  import UiFieldWrapper from '../UiFieldWrapper/UiFieldWrapper.vue'
4
+ import { useIconSize } from '../../composables/useIconSize'
4
5
  import type { TextFieldSize } from './types'
5
6
 
6
7
  interface UiTextFieldProps {
@@ -50,6 +51,8 @@ const slots = useSlots()
50
51
  const textFieldRef = ref<HTMLInputElement | null>(null)
51
52
  const isFocused = ref(false)
52
53
 
54
+ const iconSizeClass = useIconSize(() => props.size)
55
+
53
56
  const hasLeftIcon = computed(() => !!slots.icon)
54
57
  const hasRightAction = computed(() => !!slots['right-action'])
55
58
  const showClearButton = computed(() => props.clearable && !!props.modelValue && !props.disabled && !props.loading)
@@ -147,11 +150,7 @@ defineExpose({
147
150
  <div
148
151
  v-if="hasLeftIcon"
149
152
  class="ui-text-field-icon text-text-neutral-base"
150
- :class="{
151
- 'size-3': size === 'sm',
152
- 'size-4': size === 'md' || size === 'lg',
153
- 'size-5': size === 'xl',
154
- }"
153
+ :class="iconSizeClass"
155
154
  >
156
155
  <slot name="icon" />
157
156
  </div>
@@ -195,14 +194,10 @@ defineExpose({
195
194
  v-else-if="showClearButton"
196
195
  type="button"
197
196
  class="ui-text-field-clear text-text-neutral-base hover:text-text-neutral-secondary"
198
- :class="{
199
- 'size-3': size === 'sm',
200
- 'size-4': size === 'md' || size === 'lg',
201
- 'size-5': size === 'xl',
202
- }"
197
+ :class="iconSizeClass"
203
198
  @click="handleClear"
204
199
  >
205
- <div class="i-ls-x size-full" />
200
+ <div class="i-ph-x size-full" />
206
201
  </button>
207
202
  </div>
208
203
 
@@ -45,7 +45,7 @@ A flexible timeline component for displaying chronological events with various v
45
45
  | `variant` | `'default' \| 'primary' \| 'success' \| 'warning' \| 'danger'` | `'default'` | Visual variant of the timeline item |
46
46
  | `showConnector` | `boolean` | `true` | Whether to show the connecting line |
47
47
  | `indicatorSize` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the indicator dot |
48
- | `indicatorIcon` | `string` | `undefined` | Custom icon class for the indicator (e.g., `'i-ls-check-circle'`) |
48
+ | `indicatorIcon` | `string` | `undefined` | Custom icon class for the indicator (e.g., `'i-ph-check-circle'`) |
49
49
 
50
50
  ## Examples
51
51
 
@@ -72,13 +72,13 @@ A flexible timeline component for displaying chronological events with various v
72
72
 
73
73
  ```vue
74
74
  <UiTimeline>
75
- <UiTimelineItem variant="primary" indicator-icon="i-ls-user" indicator-size="md">
75
+ <UiTimelineItem variant="primary" indicator-icon="i-ph-user" indicator-size="md">
76
76
  <div>User registered</div>
77
77
  </UiTimelineItem>
78
- <UiTimelineItem variant="success" indicator-icon="i-ls-check-circle" indicator-size="md">
78
+ <UiTimelineItem variant="success" indicator-icon="i-ph-check-circle" indicator-size="md">
79
79
  <div>Email verified</div>
80
80
  </UiTimelineItem>
81
- <UiTimelineItem variant="success" indicator-icon="i-ls-play-fill" indicator-size="md">
81
+ <UiTimelineItem variant="success" indicator-icon="i-ph-play-fill" indicator-size="md">
82
82
  <div>Setup complete</div>
83
83
  </UiTimelineItem>
84
84
  </UiTimeline>
@@ -88,7 +88,7 @@ A flexible timeline component for displaying chronological events with various v
88
88
 
89
89
  ```vue
90
90
  <UiTimeline>
91
- <UiTimelineItem variant="primary" indicator-icon="i-ls-user" indicator-size="md">
91
+ <UiTimelineItem variant="primary" indicator-icon="i-ph-user" indicator-size="md">
92
92
  <div>
93
93
  <div class="flex items-center gap-2 mb-2">
94
94
  <h4 class="text-content-text-md-bold font-semibold">John Doe joined</h4>
@@ -134,7 +134,7 @@ A flexible timeline component for displaying chronological events with various v
134
134
 
135
135
  ```vue
136
136
  <UiTimeline>
137
- <UiTimelineItem variant="success" indicator-icon="i-ls-check-circle" indicator-size="md">
137
+ <UiTimelineItem variant="success" indicator-icon="i-ph-check-circle" indicator-size="md">
138
138
  <div>
139
139
  <div class="flex items-center justify-between mb-2">
140
140
  <h4 class="font-semibold">Payment Successful</h4>
@@ -76,9 +76,9 @@ const connectorClasses = computed(() => {
76
76
 
77
77
  const iconSize = computed(() => {
78
78
  const sizes = {
79
- sm: 'w-1 h-1',
80
- md: 'w-2 h-2',
81
- lg: 'w-3 h-3',
79
+ sm: 'size-2',
80
+ md: 'size-3',
81
+ lg: 'size-4',
82
82
  }
83
83
  return sizes[props.indicatorSize]
84
84
  })
@@ -0,0 +1,53 @@
1
+ import { computed, onMounted, ref, type Ref } from 'vue'
2
+ import useThemeAndModes from './useThemeAndModes'
3
+
4
+ /**
5
+ * Registers an element as a page "content" surface so the stored content mode
6
+ * (driven by the ThemePicker) applies its `data-colors-semantic` — the same
7
+ * behaviour `UiPage` gives its page content, without pulling in the full page
8
+ * layout (and its `<main>`).
9
+ *
10
+ * The content historically forces light mode. That pin is only lifted when the
11
+ * internal theming tooling is enabled, so the stored content mode can apply;
12
+ * everyone else stays light regardless of what localStorage may hold.
13
+ *
14
+ * The flag is read in onMounted, not at setup: setup also runs during SSR (no
15
+ * localStorage), and server + client-hydration markup must be identical — the
16
+ * pin lifts right after mount for the internal tooling instead.
17
+ *
18
+ * @param element Template ref of the content element to watch.
19
+ * @param selector Unique key for the watch registry — one per logical surface
20
+ * (e.g. `[data-name="ui-page-content"]`).
21
+ * @returns `contentMode`, to bind to the element's `data-colors-semantic`, and
22
+ * `isInternalThemingEnabled` for any mode-dependent styling.
23
+ */
24
+ export function useContentMode (
25
+ element: Ref<HTMLElement | undefined>,
26
+ selector: string,
27
+ ) {
28
+ const { addElementToWatch, currentContentMode } = useThemeAndModes()
29
+ const isInternalThemingEnabled = ref(false)
30
+
31
+ // Always resolve to a concrete mode, never undefined. useThemeAndModes also
32
+ // sets this attribute imperatively; if the binding resolved to undefined,
33
+ // Vue would strip the attribute on the re-render that follows the onMounted
34
+ // flag flip, and the content would silently inherit the *shell* mode from
35
+ // <body> — reading as "content mode resets to dark on every navigation".
36
+ const contentMode = computed(() => (
37
+ isInternalThemingEnabled.value ? currentContentMode.value : 'light'
38
+ ))
39
+
40
+ onMounted(() => {
41
+ isInternalThemingEnabled.value = localStorage.getItem('ls-internal-theming') === 'true'
42
+
43
+ if (!element.value) { return }
44
+
45
+ addElementToWatch('content', {
46
+ selector,
47
+ element: element.value,
48
+ ...(isInternalThemingEnabled.value ? {} : { modeOverride: 'light' }),
49
+ })
50
+ })
51
+
52
+ return { contentMode, isInternalThemingEnabled }
53
+ }
@@ -0,0 +1,28 @@
1
+ import { computed, type MaybeRefOrGetter, toValue } from 'vue'
2
+
3
+ // Single source of truth for the icon diameter a control renders at, keyed by
4
+ // the control's size name.
5
+ //
6
+ // Every control that puts an icon inside itself consumes this — buttons, button
7
+ // groups, action bar items, dropdown triggers, text fields, stepper indicators,
8
+ // loaders — so a design change to the ladder is one edit here instead of one
9
+ // per component. Components covering only part of the ladder (an action bar
10
+ // stops at md, a text field starts at sm) index the rungs they expose.
11
+ //
12
+ // NOTE: every class must be a complete literal string — Tailwind's static
13
+ // extractor can't see interpolated class names (`size-${n}`).
14
+
15
+ export const ICON_SIZES = {
16
+ xs: 'size-3',
17
+ sm: 'size-4',
18
+ md: 'size-5',
19
+ lg: 'size-6',
20
+ xl: 'size-7',
21
+ } as const
22
+
23
+ export type IconSizeName = keyof typeof ICON_SIZES
24
+
25
+ /** Reactive icon size class for a control's `size`. */
26
+ export function useIconSize(size: MaybeRefOrGetter<IconSizeName>) {
27
+ return computed(() => ICON_SIZES[toValue(size)])
28
+ }
@@ -1,2 +1,2 @@
1
- export const SIZES = ['sm', 'md', 'lg'] as const
1
+ export const SIZES = ['xs', 'sm', 'md', 'lg'] as const
2
2
  export type Size = typeof SIZES[number]
package/src/global.d.ts CHANGED
@@ -30,6 +30,9 @@ import type UiFieldWrapper from './components/UiFieldWrapper/UiFieldWrapper.vue'
30
30
  import type UiFormField from './components/UiForm/UiFormField.vue'
31
31
  import type UiHorizontalSeparator from './components/UiHorizontalSeparator/UiHorizontalSeparator.vue'
32
32
  import type UiInputCheckbox from './components/UiInputCheckbox/UiInputCheckbox.vue'
33
+ import type UiInputEmoji from './components/UiInputEmoji/UiInputEmoji.vue'
34
+ import type UiInputRadio from './components/UiInputRadio/UiInputRadio.vue'
35
+ import type UiInputRadioGroup from './components/UiInputRadio/UiInputRadioGroup.vue'
33
36
  import type UiInputSelect from './components/UiInputSelect/UiInputSelect.vue'
34
37
  import type UiInputTextarea from './components/UiInputTextarea/UiInputTextarea.vue'
35
38
  import type UiLabel from './components/UiLabel/UiLabel.vue'
@@ -67,6 +70,7 @@ import type UiStepperIndicator from './components/UiStepper/UiStepperIndicator.v
67
70
  import type UiStepperItem from './components/UiStepper/UiStepperItem.vue'
68
71
  import type UiStepperSeparator from './components/UiStepper/UiStepperSeparator.vue'
69
72
  import type UiStepperTitle from './components/UiStepper/UiStepperTitle.vue'
73
+ import type UiSwitch from './components/UiSwitch/UiSwitch.vue'
70
74
  import type UiTable from './components/UiTable/UiTable.vue'
71
75
  import type UiTableBody from './components/UiTable/UiTableBody.vue'
72
76
  import type UiTableCell from './components/UiTable/UiTableCell.vue'
@@ -123,6 +127,9 @@ declare module 'vue' {
123
127
  UiFormField: typeof UiFormField
124
128
  UiHorizontalSeparator: typeof UiHorizontalSeparator
125
129
  UiInputCheckbox: typeof UiInputCheckbox
130
+ UiInputEmoji: typeof UiInputEmoji
131
+ UiInputRadio: typeof UiInputRadio
132
+ UiInputRadioGroup: typeof UiInputRadioGroup
126
133
  UiInputSelect: typeof UiInputSelect
127
134
  UiInputTextarea: typeof UiInputTextarea
128
135
  UiLabel: typeof UiLabel
@@ -160,6 +167,7 @@ declare module 'vue' {
160
167
  UiStepperItem: typeof UiStepperItem
161
168
  UiStepperSeparator: typeof UiStepperSeparator
162
169
  UiStepperTitle: typeof UiStepperTitle
170
+ UiSwitch: typeof UiSwitch
163
171
  UiTable: typeof UiTable
164
172
  UiTableBody: typeof UiTableBody
165
173
  UiTableCell: typeof UiTableCell