@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
|
@@ -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>
|
|
@@ -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
|
|
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?():
|
|
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-
|
|
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="
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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: '
|
|
80
|
-
md: '
|
|
81
|
-
lg: '
|
|
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
|
+
}
|
package/src/constants/sizes.ts
CHANGED
|
@@ -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
|