feat: Revamp notification and audio preferences (#9312)

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
Muhsin Keloth
2024-04-30 16:50:00 +05:30
committed by GitHub
parent 4fd8c7a61b
commit c92ea11eee
13 changed files with 794 additions and 7 deletions

View File

@@ -57,13 +57,19 @@
},
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"NOTE": "This token can be used if you are building an API based integration"
"NOTE": "This token can be used if you are building an API based integration",
"COPY": "Copy"
},
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "Audio Notifications",
"NOTE": "Enable audio notifications in dashboard for new messages and conversations.",
"ALERT_TYPES": {
"NONE": "None",
"MINE": "Assigned",
"ALL": "All"
},
"ALERT_TYPE": {
"TITLE": "Alert events:",
"TITLE": "Alert events for conversations:",
"NONE": "None",
"ASSIGNED": "Assigned Conversations",
"ALL_CONVERSATIONS": "All Conversations"
@@ -89,6 +95,22 @@
"SLA_MISSED_NEXT_RESPONSE": "Send email notifications when a conversation misses next response SLA",
"SLA_MISSED_RESOLUTION": "Send email notifications when a conversation misses resolution SLA"
},
"NOTIFICATIONS": {
"TITLE": "Notification preferences",
"TYPE_TITLE": "Notification type",
"EMAIL": "Email",
"PUSH": "Push notification",
"TYPES": {
"CONVERSATION_CREATED": "A new conversation is created",
"CONVERSATION_ASSIGNED": "A conversation is assigned to you",
"CONVERSATION_MENTION": "You are mentioned in a conversation",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "A new message is created in an assigned conversation",
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "A new message is created in a participating conversation",
"SLA_MISSED_FIRST_RESPONSE": "A conversation misses first response SLA",
"SLA_MISSED_NEXT_RESPONSE": "A conversation misses next response SLA",
"SLA_MISSED_RESOLUTION": "A conversation misses resolution SLA"
}
},
"API": {
"UPDATE_SUCCESS": "Your notification preferences are updated successfully",
"UPDATE_ERROR": "There is an error while updating the preferences, please try again"

View File

@@ -0,0 +1,42 @@
<template>
<div>
<label
class="flex justify-between pb-1 text-sm font-medium leading-6 text-ash-900"
>
{{ label }}
</label>
<div class="flex flex-col gap-4">
<div
v-for="item in items"
:key="item.id"
class="flex flex-row items-start gap-2"
>
<CheckBox
:is-checked="item.model"
:value="item.value"
@update="onChange"
/>
<label class="text-sm font-normal text-ash-900">
{{ item.label }}
</label>
</div>
</div>
</div>
</template>
<script setup>
import CheckBox from 'v3/components/Form/CheckBox.vue';
defineProps({
label: {
type: String,
required: true,
},
items: {
type: Array,
required: true,
},
});
const emit = defineEmits(['change']);
const onChange = (id, value) => {
emit('change', id, value);
};
</script>

View File

@@ -0,0 +1,66 @@
<template>
<div>
<label
class="flex justify-between pb-1 text-sm font-medium leading-6 text-ash-900"
>
{{ label }}
</label>
<div
class="flex flex-row justify-between h-10 max-w-xl p-2 border border-solid rounded-xl border-ash-200"
>
<div
v-for="option in alertEvents"
:key="option.value"
class="flex flex-row items-center justify-center gap-2 px-4 border-r border-ash-200 grow last:border-r-0"
>
<input
:id="`radio-${option.value}`"
v-model="selectedValue"
class="shadow cursor-pointer grid place-items-center border-2 border-ash-200 appearance-none rounded-full w-4 h-4 checked:bg-primary-600 before:content-[''] before:bg-primary-600 before:border-4 before:rounded-full before:border-ash-25 checked:before:w-[14px] checked:before:h-[14px] checked:border checked:border-primary-600"
type="radio"
:value="option.value"
/>
<label
:for="`radio-${option.value}`"
class="text-sm font-medium"
:class="
selectedValue === option.value ? 'text-ash-900' : 'text-ash-800'
"
>
{{
$t(
`PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPES.${option.label.toUpperCase()}`
)
}}
</label>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { ALERT_EVENTS } from './constants';
const props = defineProps({
label: {
type: String,
default: '',
},
value: {
type: String,
default: 'all',
},
});
const alertEvents = ALERT_EVENTS;
const emit = defineEmits(['update']);
const selectedValue = computed({
get: () => props.value,
set: value => {
emit('update', value);
},
});
</script>

View File

@@ -0,0 +1,56 @@
<template>
<form-select
v-model="selectedValue"
name="alertTone"
spacing="compact"
:value="selectedValue"
:options="alertTones"
:label="label"
class="max-w-xl"
>
<option
v-for="tone in alertTones"
:key="tone.label"
:value="tone.value"
:selected="tone.value === selectedValue"
>
{{ tone.label }}
</option>
</form-select>
</template>
<script setup>
import { computed } from 'vue';
import FormSelect from 'v3/components/Form/Select.vue';
const props = defineProps({
value: {
type: String,
required: true,
validator: value => ['ding', 'bell'].includes(value),
},
label: {
type: String,
default: '',
},
});
const alertTones = computed(() => [
{
value: 'ding',
label: 'Ding',
},
{
value: 'bell',
label: 'Bell',
},
]);
const emit = defineEmits(['change']);
const selectedValue = computed({
get: () => props.value,
set: value => {
emit('change', value);
},
});
</script>

View File

@@ -0,0 +1,128 @@
<template>
<div id="profile-settings-notifications" class="flex flex-col gap-6">
<audio-alert-tone
:value="alertTone"
:label="
$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.DEFAULT_TONE.TITLE'
)
"
@change="handleAudioToneChange"
/>
<audio-alert-event
:label="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.ALERT_TYPE.TITLE')
"
:value="audioAlert"
@update="handAudioAlertChange"
/>
<audio-alert-condition
:items="audioAlertConditions"
:label="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.TITLE')
"
@change="handleAudioAlertConditions"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import configMixin from 'shared/mixins/configMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import AudioAlertTone from './AudioAlertTone.vue';
import AudioAlertEvent from './AudioAlertEvent.vue';
import AudioAlertCondition from './AudioAlertCondition.vue';
export default {
components: {
AudioAlertEvent,
AudioAlertTone,
AudioAlertCondition,
},
mixins: [alertMixin, configMixin, uiSettingsMixin],
data() {
return {
audioAlert: '',
playAudioWhenTabIsInactive: false,
alertIfUnreadConversationExist: false,
alertTone: 'ding',
audioAlertConditions: [],
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
uiSettings: 'getUISettings',
}),
},
watch: {
uiSettings(value) {
this.notificationUISettings(value);
},
},
mounted() {
this.notificationUISettings(this.uiSettings);
this.$store.dispatch('userNotificationSettings/get');
},
methods: {
notificationUISettings(uiSettings) {
const {
enable_audio_alerts: audioAlert = '',
always_play_audio_alert: alwaysPlayAudioAlert,
alert_if_unread_assigned_conversation_exist:
alertIfUnreadConversationExist,
notification_tone: alertTone,
} = uiSettings;
this.audioAlert = audioAlert;
this.playAudioWhenTabIsInactive = !alwaysPlayAudioAlert;
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
this.audioAlertConditions = [
{
id: 'audio1',
label: this.$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_ONE'
),
model: this.playAudioWhenTabIsInactive,
value: 'tab_is_inactive',
},
{
id: 'audio2',
label: this.$t(
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.CONDITIONS.CONDITION_TWO'
),
model: this.alertIfUnreadConversationExist,
value: 'conversations_are_read',
},
];
this.alertTone = alertTone || 'ding';
},
handAudioAlertChange(value) {
this.audioAlert = value;
this.updateUISettings({
enable_audio_alerts: this.audioAlert,
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioAlertConditions(id, value) {
if (id === 'tab_is_inactive') {
this.updateUISettings({
always_play_audio_alert: !value,
});
} else if (id === 'conversations_are_read') {
this.updateUISettings({
alert_if_unread_assigned_conversation_exist: value,
});
}
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioToneChange(value) {
this.updateUISettings({ notification_tone: value });
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
},
};
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex items-center w-full overflow-y-auto">
<div class="flex flex-col h-full p-5 pt-16 mx-auto my-0 font-inter">
<div class="flex flex-col gap-16 sm:max-w-[720px]">
<div class="flex flex-col gap-16 pb-8 sm:max-w-[720px]">
<div class="flex flex-col gap-6">
<h2 class="mt-4 text-2xl font-medium text-ash-900">
{{ $t('PROFILE_SETTINGS.TITLE') }}
@@ -62,6 +62,19 @@
>
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
</form-section>
<form-section
:header="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.TITLE')
"
:description="
$t('PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.NOTE')
"
>
<audio-notifications />
</form-section>
<form-section :header="$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TITLE')">
<notification-preferences />
</form-section>
</div>
</div>
</div>
@@ -80,6 +93,8 @@ import UserBasicDetails from './UserBasicDetails.vue';
import MessageSignature from './MessageSignature.vue';
import HotKeyCard from './HotKeyCard.vue';
import ChangePassword from './ChangePassword.vue';
import NotificationPreferences from './NotificationPreferences.vue';
import AudioNotifications from './AudioNotifications.vue';
import FormSection from 'dashboard/components/FormSection.vue';
export default {
@@ -90,6 +105,8 @@ export default {
UserBasicDetails,
HotKeyCard,
ChangePassword,
NotificationPreferences,
AudioNotifications,
},
mixins: [alertMixin, globalConfigMixin, uiSettingsMixin],
data() {

View File

@@ -0,0 +1,53 @@
<template>
<div
class="flex items-start gap-2 px-0 text-sm tracking-[0.5] text-left rtl:text-right"
:class="`col-span-${span}`"
>
<input
v-model="localFlags"
class="mt-1 flex-shrink-0 border-ash-200 border checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
type="checkbox"
:value="localValue"
@input="handleInput"
/>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps({
value: {
type: String,
required: true,
},
span: {
type: Number,
default: 2,
},
selectedFlags: {
type: Array,
required: true,
},
type: {
type: String,
required: true,
},
});
const emit = defineEmits(['input']);
const localValue = ref(props.value);
const localFlags = ref(props.selectedFlags);
watch(
() => props.selectedFlags,
newValue => {
localFlags.value = newValue;
}
);
const handleInput = e => {
emit('input', props.type, e);
};
</script>

View File

@@ -0,0 +1,284 @@
<template>
<div id="profile-settings-notifications" class="flex flex-col gap-6">
<!-- Layout for desktop devices -->
<div class="hidden sm:block">
<div
class="grid content-center h-12 grid-cols-12 gap-4 py-0 rounded-t-xl"
>
<table-header-cell
:span="7"
label="`${$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPE_TITLE')}`"
>
<span class="text-sm font-normal normal-case text-ash-800">
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPE_TITLE') }}
</span>
</table-header-cell>
<table-header-cell
:span="2"
label="`${$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.EMAIL')}`"
>
<span class="text-sm font-medium normal-case text-ash-900">
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.EMAIL') }}
</span>
</table-header-cell>
<table-header-cell
:span="3"
label="`${$t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.PUSH')}`"
>
<div class="flex items-center justify-between gap-1">
<span
class="text-sm font-medium normal-case text-ash-900 whitespace-nowrap"
>
{{ $t('PROFILE_SETTINGS.FORM.NOTIFICATIONS.PUSH') }}
</span>
<form-switch
:value="hasEnabledPushPermissions"
@input="onRequestPermissions"
/>
</div>
</table-header-cell>
</div>
<div
v-for="(notification, index) in filteredNotificationTypes"
:key="index"
>
<div
class="grid items-center content-center h-12 grid-cols-12 gap-4 py-0 rounded-t-xl"
>
<div
class="flex flex-row items-start gap-2 col-span-7 px-0 py-2 text-sm tracking-[0.5] rtl:text-right"
>
<span class="text-sm text-ash-900">
{{ $t(notification.label) }}
</span>
</div>
<div
v-for="(type, typeIndex) in ['email', 'push']"
:key="typeIndex"
class="flex items-start gap-2 px-0 text-sm tracking-[0.5] text-left rtl:text-right"
:class="`col-span-${type === 'push' ? 3 : 2}`"
>
<CheckBox
:value="`${type}_${notification.value}`"
:is-checked="
checkFlagStatus(type, notification.value, selectedPushFlags)
"
@update="id => handleInput(type, id)"
/>
</div>
</div>
</div>
</div>
<!-- Layout for mobile devices -->
<div class="flex flex-col gap-6 sm:hidden">
<span class="text-sm font-medium normal-case text-ash-900">
{{ $t('PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.TITLE') }}
</span>
<div class="flex flex-col gap-4">
<div
v-for="(notification, index) in filteredNotificationTypes"
:key="index"
class="flex flex-row items-start gap-2"
>
<CheckBox
:id="`email_${notification.value}`"
:value="`email_${notification.value}`"
:is-checked="checkFlagStatus('email', notification.value)"
@update="handleEmailInput"
/>
<span class="text-sm text-ash-900">{{ $t(notification.label) }}</span>
</div>
</div>
<div class="flex items-center justify-start gap-2">
<span class="text-sm font-medium normal-case text-ash-900">
{{ $t('PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.TITLE') }}
</span>
<form-switch
:value="hasEnabledPushPermissions"
@input="onRequestPermissions"
/>
</div>
<div class="flex flex-col gap-4">
<div
v-for="(notification, index) in filteredNotificationTypes"
:key="index"
class="flex flex-row items-start gap-2"
>
<CheckBox
:id="`push_${notification.value}`"
:value="`push_${notification.value}`"
:is-checked="checkFlagStatus('push', notification.value)"
@update="handlePushInput"
/>
<span class="text-sm text-ash-900">{{ $t(notification.label) }}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import configMixin from 'shared/mixins/configMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import TableHeaderCell from 'dashboard/components/widgets/TableHeaderCell.vue';
import CheckBox from 'v3/components/Form/CheckBox.vue';
import {
hasPushPermissions,
requestPushPermissions,
verifyServiceWorkerExistence,
} from 'dashboard/helper/pushHelper.js';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import FormSwitch from 'v3/components/Form/Switch.vue';
import { NOTIFICATION_TYPES } from './constants';
export default {
components: {
TableHeaderCell,
FormSwitch,
CheckBox,
},
mixins: [alertMixin, configMixin, uiSettingsMixin],
data() {
return {
selectedEmailFlags: [],
selectedPushFlags: [],
enableAudioAlerts: false,
hasEnabledPushPermissions: false,
notificationTypes: NOTIFICATION_TYPES,
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
emailFlags: 'userNotificationSettings/getSelectedEmailFlags',
pushFlags: 'userNotificationSettings/getSelectedPushFlags',
uiSettings: 'getUISettings',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
hasPushAPISupport() {
return !!('Notification' in window);
},
isSLAEnabled() {
return this.isFeatureEnabledonAccount(this.accountId, FEATURE_FLAGS.SLA);
},
filteredNotificationTypes() {
return this.notificationTypes.filter(notification =>
this.isSLAEnabled
? true
: ![
'sla_missed_first_response',
'sla_missed_next_response',
'sla_missed_resolution',
].includes(notification.value)
);
},
},
watch: {
emailFlags(value) {
this.selectedEmailFlags = value;
},
pushFlags(value) {
this.selectedPushFlags = value;
},
},
mounted() {
if (hasPushPermissions()) {
this.getPushSubscription();
}
this.$store.dispatch('userNotificationSettings/get');
},
methods: {
checkFlagStatus(type, flagType) {
const selectedFlags =
type === 'email' ? this.selectedEmailFlags : this.selectedPushFlags;
return selectedFlags.includes(`${type}_${flagType}`);
},
onRegistrationSuccess() {
this.hasEnabledPushPermissions = true;
},
onRequestPermissions() {
requestPushPermissions({
onSuccess: this.onRegistrationSuccess,
});
},
getPushSubscription() {
verifyServiceWorkerExistence(registration =>
registration.pushManager
.getSubscription()
.then(subscription => {
if (!subscription) {
this.hasEnabledPushPermissions = false;
} else {
this.hasEnabledPushPermissions = true;
}
})
// eslint-disable-next-line no-console
.catch(error => console.log(error))
);
},
async updateNotificationSettings() {
try {
this.$store.dispatch('userNotificationSettings/update', {
selectedEmailFlags: this.selectedEmailFlags,
selectedPushFlags: this.selectedPushFlags,
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
} catch (error) {
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_ERROR'));
}
},
handleAudioInput(e) {
this.enableAudioAlerts = e.target.value;
this.updateUISettings({
enable_audio_alerts: this.enableAudioAlerts,
});
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioAlertConditions(e) {
let condition = e.target.value;
if (condition === 'tab_is_inactive') {
this.updateUISettings({
always_play_audio_alert: !e.target.checked,
});
} else if (condition === 'conversations_are_read') {
this.updateUISettings({
alert_if_unread_assigned_conversation_exist: e.target.checked,
});
}
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleAudioToneChange(value) {
this.updateUISettings({ notification_tone: value });
this.showAlert(this.$t('PROFILE_SETTINGS.FORM.API.UPDATE_SUCCESS'));
},
handleInput(type, id) {
if (type === 'email') {
this.handleEmailInput(id);
} else {
this.handlePushInput(id);
}
},
handleEmailInput(id) {
this.selectedEmailFlags = this.toggleInput(this.selectedEmailFlags, id);
this.updateNotificationSettings();
},
handlePushInput(id) {
this.selectedPushFlags = this.toggleInput(this.selectedPushFlags, id);
this.updateNotificationSettings();
},
toggleInput(selected, current) {
if (selected.includes(current)) {
const newSelectedFlags = selected.filter(flag => flag !== current);
return newSelectedFlags;
}
return [...selected, current];
},
},
};
</script>

View File

@@ -0,0 +1,52 @@
export const NOTIFICATION_TYPES = [
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_CREATED',
value: 'conversation_creation',
},
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_ASSIGNED',
value: 'conversation_assignment',
},
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.CONVERSATION_MENTION',
value: 'conversation_mention',
},
{
label:
'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.ASSIGNED_CONVERSATION_NEW_MESSAGE',
value: 'assigned_conversation_new_message',
},
{
label:
'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.PARTICIPATING_CONVERSATION_NEW_MESSAGE',
value: 'participating_conversation_new_message',
},
{
label:
'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_FIRST_RESPONSE',
value: 'sla_missed_first_response',
},
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_NEXT_RESPONSE',
value: 'sla_missed_next_response',
},
{
label: 'PROFILE_SETTINGS.FORM.NOTIFICATIONS.TYPES.SLA_MISSED_RESOLUTION',
value: 'sla_missed_resolution',
},
];
export const ALERT_EVENTS = [
{
value: 'none',
label: 'none',
},
{
value: 'mine',
label: 'mine',
},
{
value: 'all',
label: 'all',
},
];

View File

@@ -58,7 +58,8 @@ const attrs = useAttrs();
const baseClasses = {
outline: 'outline outline-1 -outline-offset-1 focus:ring focus:ring-offset-1',
ghost: 'hover:text-600 active:text-600 focus:ring focus:ring-offset-1',
solid: 'hover:bg-700 active:bg-700 focus:ring focus:ring-offset-1',
solid:
'hover:bg-700 active:bg-700 focus:ring focus:ring-offset-1 focus:ring-2',
};
const colorClass = computed(() => {

View File

@@ -0,0 +1,31 @@
<template>
<input
:id="value"
v-model="checked"
type="checkbox"
:value="value"
class="flex-shrink-0 mt-0.5 border-ash-200 border checked:border-none checked:bg-primary-600 dark:checked:bg-primary-600 shadow appearance-none rounded-[4px] w-4 h-4 focus:ring-1 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center after:text-center after:text-xs after:font-bold after:relative"
/>
</template>
<script setup>
import { defineProps, defineEmits, computed } from 'vue';
const props = defineProps({
isChecked: {
type: Boolean,
default: false,
},
value: {
type: String,
default: null,
},
});
const emit = defineEmits(['input']);
const checked = computed({
get: () => props.isChecked,
set: value => emit('update', props.value, value),
});
</script>

View File

@@ -11,11 +11,11 @@
:selected="value"
:name="name"
:class="{
'text-slate-400': !value,
'text-slate-900 dark:text-slate-100': value,
'text-ash-400': !value,
'text-ash-900': value,
'pl-9': icon,
}"
class="block w-full px-3 py-2 pr-6 mb-0 border-0 rounded-md shadow-sm outline-none appearance-none select-caret ring-1 ring-inset placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-700 dark:ring-slate-600 dark:focus:ring-woot-500 ring-slate-200"
class="block w-full px-3 py-2 pr-6 mb-0 border-0 shadow-sm outline-none appearance-none rounded-xl select-caret ring-ash-200 ring-1 ring-inset placeholder:text-ash-900 focus:ring-2 focus:ring-inset focus:ring-primary-500 sm:text-sm sm:leading-6"
@input="onInput"
>
<option value="" disabled selected class="hidden">

View File

@@ -0,0 +1,35 @@
<template>
<button
type="button"
class="relative flex-shrink-0 h-4 p-0 border-none shadow-inner w-7 rounded-3xl"
:class="
value ? 'bg-primary-600 shadow-primary-800' : 'shadow-ash-400 bg-ash-200'
"
role="switch"
:aria-checked="value.toString()"
@click="onClick"
>
<span
aria-hidden="true"
class="rounded-full bg-white top-0.5 absolute dark:bg-white w-3 h-3 translate-y-0 duration-200 transition-transform ease-in-out"
:class="
value
? 'ltr:translate-x-0 rtl:translate-x-[12px]'
: 'ltr:-translate-x-[12px] rtl:translate-x-0'
"
/>
</button>
</template>
<script>
export default {
props: {
value: { type: Boolean, default: false },
},
methods: {
onClick() {
this.$emit('input', !this.value);
},
},
};
</script>