design-system-next 1.0.21
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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/design-system-next.js +3185 -0
- package/dist/design-system-next.js.gz +0 -0
- package/dist/main.css +1 -0
- package/dist/main.css.gz +0 -0
- package/dist/main.d.ts +9 -0
- package/package.json +85 -0
- package/src/App.vue +179 -0
- package/src/assets/scripts/borderRadius.ts +15 -0
- package/src/assets/scripts/colors.ts +134 -0
- package/src/assets/scripts/maxWidth.ts +11 -0
- package/src/assets/scripts/spacing.ts +23 -0
- package/src/assets/styles/tailwind.css +795 -0
- package/src/components/badge/badge.ts +43 -0
- package/src/components/badge/badge.vue +20 -0
- package/src/components/badge/use-badge.ts +52 -0
- package/src/components/button/button.ts +64 -0
- package/src/components/button/button.vue +25 -0
- package/src/components/button/use-button.ts +166 -0
- package/src/components/lozenge/lozenge.ts +57 -0
- package/src/components/lozenge/lozenge.vue +96 -0
- package/src/components/lozenge/use-lozenge.ts +12 -0
- package/src/components/radio/radio.ts +54 -0
- package/src/components/radio/radio.vue +36 -0
- package/src/components/radio/use-radio.ts +65 -0
- package/src/components/sidenav/sidenav.ts +43 -0
- package/src/components/sidenav/sidenav.vue +235 -0
- package/src/components/sidenav/use-sidenav.ts +31 -0
- package/src/components/switch/switch.ts +35 -0
- package/src/components/switch/switch.vue +106 -0
- package/src/components/switch/use-switch.ts +106 -0
- package/src/main.ts +13 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { computed, ref, ComputedRef } from "vue";
|
|
2
|
+
import { useElementHover } from "@vueuse/core";
|
|
3
|
+
import classNames from "classnames";
|
|
4
|
+
import type { RadioPropTypes } from "./radio";
|
|
5
|
+
|
|
6
|
+
export const useRadioButton = (props: RadioPropTypes) => {
|
|
7
|
+
const radioRef = ref<HTMLInputElement | null>(null);
|
|
8
|
+
const isHovered = useElementHover(radioRef);
|
|
9
|
+
|
|
10
|
+
const radioClasses: ComputedRef<string> = computed(() => {
|
|
11
|
+
const baseClasses = "tw-sr-only tw-peer";
|
|
12
|
+
|
|
13
|
+
if (props.disabled) {
|
|
14
|
+
return classNames(baseClasses, "tw-cursor-default");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return baseClasses;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const indicatorClasses: ComputedRef<string> = computed(() => {
|
|
21
|
+
console.log(props.disabled);
|
|
22
|
+
const baseClasses =
|
|
23
|
+
"tw-inline-block tw-w-4 tw-h-4 tw-rounded-full tw-border-2 tw-border-solid tw-mr-2";
|
|
24
|
+
|
|
25
|
+
if (props.disabled) {
|
|
26
|
+
return classNames(
|
|
27
|
+
baseClasses,
|
|
28
|
+
props.modelValue === props.value
|
|
29
|
+
? "tw-border-color-disabled tw-background-color-disabled tw-shadow-[inset_0px_0px_0px_2.5px_#fff] tw-cursor-default"
|
|
30
|
+
: "tw-border-color-disabled tw-background-color tw-cursor-default"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (props.modelValue === props.value) {
|
|
35
|
+
return classNames(
|
|
36
|
+
baseClasses,
|
|
37
|
+
"tw-border-color-brand-base tw-background-color-brand-base tw-shadow-[inset_0px_0px_0px_2.5px_#fff]"
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isHovered.value) {
|
|
42
|
+
return classNames(
|
|
43
|
+
baseClasses,
|
|
44
|
+
"tw-background-color-base tw-border-2 tw-border-color-supporting tw-shadow-[inset_0px_0px_0px_2.5px_#fff]"
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return classNames(baseClasses, "tw-border-color-supporting tw-shadow-[inset_0px_0px_0px_2.5px_#fff]");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const radioLabelClasses: ComputedRef<string> = computed(() => {
|
|
52
|
+
if (props.disabled) {
|
|
53
|
+
return "tw-text-color-disabled tw-cursor-default";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return "tw-text-color-strong tw-cursor-pointer";
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
radioRef,
|
|
61
|
+
radioClasses,
|
|
62
|
+
indicatorClasses,
|
|
63
|
+
radioLabelClasses,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PropType, ExtractPropTypes } from 'vue';
|
|
2
|
+
|
|
3
|
+
export const sidenavPropTypes = {
|
|
4
|
+
/**
|
|
5
|
+
* @description Sidenav has quick actions
|
|
6
|
+
*/
|
|
7
|
+
hasQuickActions: {
|
|
8
|
+
type: Boolean,
|
|
9
|
+
validator: (value: unknown) => typeof value === 'boolean',
|
|
10
|
+
default: false,
|
|
11
|
+
},
|
|
12
|
+
/**
|
|
13
|
+
* @description Sidenav has search
|
|
14
|
+
*/
|
|
15
|
+
hasSearch: {
|
|
16
|
+
type: Boolean,
|
|
17
|
+
validator: (value: unknown) => typeof value === 'boolean',
|
|
18
|
+
default: false,
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* @description Sidenav active navigation
|
|
22
|
+
*/
|
|
23
|
+
activeNav: {
|
|
24
|
+
type: Object as PropType<{ parentNav: string; menu: string; submenu: string }>,
|
|
25
|
+
validator: (value: unknown) => typeof value === 'object',
|
|
26
|
+
default: () => ({ parentNav: '', menu: '', submenu: '' }),
|
|
27
|
+
},
|
|
28
|
+
/**
|
|
29
|
+
* @description Sidenav navlinks
|
|
30
|
+
*/
|
|
31
|
+
navLinks: {
|
|
32
|
+
type: Array as PropType<Array<{ parentLinks: Array<unknown> }>>,
|
|
33
|
+
validator: (value: unknown) => Array.isArray(value),
|
|
34
|
+
default: () => [],
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const sidenavEmitTypes = {
|
|
39
|
+
'route-push': String,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type SidenavPropTypes = ExtractPropTypes<typeof sidenavPropTypes>;
|
|
43
|
+
export type SidenavEmitTypes = typeof sidenavEmitTypes;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="[
|
|
4
|
+
'tw-hidden-scrolls tw-fixed tw-bottom-0 tw-left-0 tw-top-0',
|
|
5
|
+
'tw-background-color tw-w-auto tw-overflow-y-auto tw-overflow-x-hidden',
|
|
6
|
+
'tw-border-color-weak tw-border-b-0 tw-border-l-0 tw-border-r tw-border-t-0 tw-border-solid',
|
|
7
|
+
'tw-transition tw-duration-150 tw-ease-in-out',
|
|
8
|
+
]"
|
|
9
|
+
>
|
|
10
|
+
<div class="tw-grid tw-h-full tw-grid-rows-[auto_1fr] tw-px-[12px]">
|
|
11
|
+
<div class="tw-hidden-scrolls tw-mt-[16px] tw-overflow-auto">
|
|
12
|
+
<!-- #region - Logo -->
|
|
13
|
+
<div
|
|
14
|
+
:class="[
|
|
15
|
+
{
|
|
16
|
+
'tw tw-grid tw-justify-center': true,
|
|
17
|
+
'[&>img]:tw-mx-auto [&>img]:tw-h-[24px] [&>img]:tw-w-[24px]': true,
|
|
18
|
+
'tw-pb-[16px]': !props.hasQuickActions && !props.hasSearch && !props.navLinks,
|
|
19
|
+
},
|
|
20
|
+
]"
|
|
21
|
+
>
|
|
22
|
+
<slot name="logo-image" />
|
|
23
|
+
</div>
|
|
24
|
+
<!-- #endregion - Logo -->
|
|
25
|
+
|
|
26
|
+
<div class="tw-grid tw-justify-center tw-gap-[8px] tw-pb-[16px] tw-pt-[16px]">
|
|
27
|
+
<!-- #region - Quick Actions -->
|
|
28
|
+
<div
|
|
29
|
+
v-if="props.hasQuickActions"
|
|
30
|
+
:class="[
|
|
31
|
+
'tw-text-color-brand-base tw-mx-auto tw-h-[32px] tw-cursor-pointer tw-text-[28px] tw-transition tw-duration-150 tw-ease-in-out',
|
|
32
|
+
'hover:tw-text-color-success-hover',
|
|
33
|
+
'active:tw-text-color-success-pressed active:tw-scale-90',
|
|
34
|
+
]"
|
|
35
|
+
>
|
|
36
|
+
<IconPlusCircleFill />
|
|
37
|
+
</div>
|
|
38
|
+
<!-- #endregion - Quick Actions -->
|
|
39
|
+
|
|
40
|
+
<!-- #region - Search -->
|
|
41
|
+
<div
|
|
42
|
+
v-if="props.hasSearch"
|
|
43
|
+
:class="[
|
|
44
|
+
'justify-center tw-flex tw-cursor-pointer tw-items-center tw-rounded-[8px] tw-p-[8px] tw-transition tw-duration-150 tw-ease-in-out',
|
|
45
|
+
'hover:tw-background-color-hover',
|
|
46
|
+
'active:tw-background-color-single-active active:tw-scale-90',
|
|
47
|
+
]"
|
|
48
|
+
>
|
|
49
|
+
<IconMagnifyingGlass />
|
|
50
|
+
</div>
|
|
51
|
+
<!-- #endregion - Search -->
|
|
52
|
+
|
|
53
|
+
<!-- #region - Grouped Nav Links -->
|
|
54
|
+
<template v-for="(navLink, navLinkIndex) in props.navLinks" :key="navLinkIndex">
|
|
55
|
+
<template v-for="(parentLink, parentLinkIndex) in navLink.parentLinks" :key="parentLinkIndex">
|
|
56
|
+
<!-- #region - Parent link with menu links -->
|
|
57
|
+
<template v-if="parentLink.menuLinks && parentLink.menuLinks.length > 0">
|
|
58
|
+
<Menu
|
|
59
|
+
aria-id="sidenav-menu-wrapper"
|
|
60
|
+
distance="18"
|
|
61
|
+
placement="right"
|
|
62
|
+
:triggers="['click', 'hover']"
|
|
63
|
+
instant-move
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
:class="[
|
|
67
|
+
{
|
|
68
|
+
'justify-center tw-flex tw-cursor-pointer tw-items-center tw-rounded-[8px] tw-p-[8px] tw-transition tw-duration-150 tw-ease-in-out': true,
|
|
69
|
+
'tw-background-color-single-active tw-border-color-brand-base tw-border-[1.5px] tw-border-solid active:tw-scale-90':
|
|
70
|
+
props.activeNav.parentNav === parentLink.title,
|
|
71
|
+
'hover:tw-background-color-hover': props.activeNav.parentNav != parentLink.title,
|
|
72
|
+
'active:tw-background-color-single-active active:tw-scale-90': true,
|
|
73
|
+
},
|
|
74
|
+
]"
|
|
75
|
+
>
|
|
76
|
+
<component :is="parentLink.icon" v-if="parentLink.icon" class="tw-h-[1.25em] tw-w-[1.25em]" />
|
|
77
|
+
<IconGlobe v-else />
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<template #popper>
|
|
81
|
+
<div
|
|
82
|
+
class="tw-border-color-weak tw-border-x-0 tw-border-b tw-border-t-0 tw-border-solid tw-p-[8px]"
|
|
83
|
+
>
|
|
84
|
+
<h3 class="tw-body-sm-regular-medium tw-m-0">
|
|
85
|
+
{{ parentLink.title }}
|
|
86
|
+
</h3>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<template v-for="(menuLink, menuLinkIndex) in parentLink.menuLinks" :key="menuLinkIndex">
|
|
90
|
+
<!-- #region - Menu link with submenu links -->
|
|
91
|
+
<template v-if="menuLink.submenuLinks && menuLink.submenuLinks.length > 0">
|
|
92
|
+
<Menu
|
|
93
|
+
aria-id="sidenav-submenu-wrapper"
|
|
94
|
+
distance="4"
|
|
95
|
+
placement="right-start"
|
|
96
|
+
:triggers="['click', 'hover']"
|
|
97
|
+
instant-move
|
|
98
|
+
>
|
|
99
|
+
<div
|
|
100
|
+
:class="[
|
|
101
|
+
{
|
|
102
|
+
'tw-body-sm-regular tw-relative tw-m-0 tw-flex tw-cursor-pointer tw-justify-between tw-px-[8px] tw-py-[6px] tw-align-middle tw-duration-150 tw-ease-in-out': true,
|
|
103
|
+
'tw-background-color-single-active': props.activeNav.menu === menuLink.title,
|
|
104
|
+
'hover:tw-background-color-hover': props.activeNav.menu !== menuLink.title,
|
|
105
|
+
'active:tw-background-color-pressed': true,
|
|
106
|
+
},
|
|
107
|
+
]"
|
|
108
|
+
>
|
|
109
|
+
<div
|
|
110
|
+
v-if="props.activeNav.menu === menuLink.title"
|
|
111
|
+
class="tw-background-color-brand-base tw-absolute tw-left-0 tw-top-0 tw-h-full tw-w-[2px]"
|
|
112
|
+
></div>
|
|
113
|
+
<span>{{ menuLink.title }}</span>
|
|
114
|
+
<IconCaretRight
|
|
115
|
+
:class="[
|
|
116
|
+
'tw-h-[16px] tw-w-[16px] tw-transform tw-font-normal tw-transition-transform tw-duration-300',
|
|
117
|
+
props.activeNav.menu === menuLink.title ? '-tw-rotate-90' : 'hover:-tw-rotate-90',
|
|
118
|
+
]"
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<template #popper>
|
|
123
|
+
<template
|
|
124
|
+
v-for="(submenuLink, submenuLinkIndex) in menuLink.submenuLinks"
|
|
125
|
+
:key="submenuLinkIndex"
|
|
126
|
+
>
|
|
127
|
+
<Menu aria-id="sidenav-sub-submenu-wrapper" :triggers="['click', 'hover']" instant-move>
|
|
128
|
+
<div
|
|
129
|
+
:class="[
|
|
130
|
+
{
|
|
131
|
+
'tw-body-sm-regular tw-relative tw-m-0 tw-flex tw-cursor-pointer tw-justify-between tw-px-[8px] tw-py-[6px] tw-align-middle tw-duration-150 tw-ease-in-out': true,
|
|
132
|
+
'tw-background-color-single-active':
|
|
133
|
+
props.activeNav.submenu === submenuLink.title,
|
|
134
|
+
'hover:tw-background-color-hover': props.activeNav.submenu !== submenuLink.title,
|
|
135
|
+
'active:tw-background-color-pressed': true,
|
|
136
|
+
},
|
|
137
|
+
]"
|
|
138
|
+
@click="handleRedirect($event, submenuLink.redirect)"
|
|
139
|
+
>
|
|
140
|
+
<div
|
|
141
|
+
v-show="props.activeNav.submenu === submenuLink.title"
|
|
142
|
+
class="tw-background-color-brand-base tw-absolute tw-left-0 tw-top-0 tw-h-full tw-w-[2px]"
|
|
143
|
+
></div>
|
|
144
|
+
<span>{{ submenuLink.title }}</span>
|
|
145
|
+
</div>
|
|
146
|
+
</Menu>
|
|
147
|
+
</template>
|
|
148
|
+
</template>
|
|
149
|
+
</Menu>
|
|
150
|
+
</template>
|
|
151
|
+
<!-- #endregion - Menu link with submenu links -->
|
|
152
|
+
|
|
153
|
+
<!-- #region - Menu link only -->
|
|
154
|
+
<template v-else>
|
|
155
|
+
<div
|
|
156
|
+
:class="[
|
|
157
|
+
'tw-body-sm-regular tw-m-0 tw-flex tw-cursor-pointer tw-justify-between tw-px-[8px] tw-py-[6px] tw-align-middle tw-duration-300 tw-ease-in-out',
|
|
158
|
+
'hover:tw-background-color-hover',
|
|
159
|
+
'active:tw-background-color-pressed',
|
|
160
|
+
'last:tw-rounded-b-[12px]',
|
|
161
|
+
]"
|
|
162
|
+
@click="handleRedirect($event, menuLink.redirect)"
|
|
163
|
+
>
|
|
164
|
+
<span>{{ menuLink.title }}</span>
|
|
165
|
+
</div>
|
|
166
|
+
</template>
|
|
167
|
+
<!-- #endregion - Menu link only -->
|
|
168
|
+
</template>
|
|
169
|
+
</template>
|
|
170
|
+
</Menu>
|
|
171
|
+
</template>
|
|
172
|
+
<!-- #endregion - Parent link with menu links -->
|
|
173
|
+
|
|
174
|
+
<!-- #region - Parent link only -->
|
|
175
|
+
<template v-else>
|
|
176
|
+
<Tooltip aria-id="default-tooltip" placement="right" distance="18" :triggers="['click']">
|
|
177
|
+
<template #popper>
|
|
178
|
+
<span class="tw-label-xs-medium">{{ parentLink.title }}</span>
|
|
179
|
+
</template>
|
|
180
|
+
<div
|
|
181
|
+
:class="[
|
|
182
|
+
'justify-center tw-flex tw-cursor-pointer tw-items-center tw-rounded-[8px] tw-p-[8px] tw-transition tw-duration-150 tw-ease-in-out',
|
|
183
|
+
'hover:tw-background-color-hover',
|
|
184
|
+
'active:tw-background-color-single-active active:tw-scale-90',
|
|
185
|
+
]"
|
|
186
|
+
@click="handleRedirect($event, parentLink.redirect)"
|
|
187
|
+
>
|
|
188
|
+
<component :is="parentLink.icon" v-if="parentLink.icon" class="tw-h-[1.25em] tw-w-[1.25em]" />
|
|
189
|
+
<IconGlobe v-else />
|
|
190
|
+
</div>
|
|
191
|
+
</Tooltip>
|
|
192
|
+
</template>
|
|
193
|
+
<!-- #endregion - Parent link only -->
|
|
194
|
+
</template>
|
|
195
|
+
<div
|
|
196
|
+
v-if="navLinks.length > 0 && navLinkIndex < navLinks.length - 1"
|
|
197
|
+
class="tw-background-color-hover tw-h-[2px] tw-w-full"
|
|
198
|
+
></div>
|
|
199
|
+
</template>
|
|
200
|
+
<!-- #endregion - Grouped Nav Links -->
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<!-- #region - Avatar -->
|
|
205
|
+
<div
|
|
206
|
+
:class="[
|
|
207
|
+
'tw tw-grid tw-items-end tw-justify-center tw-pb-[16px]',
|
|
208
|
+
'[&>img]:tw-mx-auto [&>img]:tw-h-[24px] [&>img]:tw-w-[24px] [&>img]:tw-rounded-full',
|
|
209
|
+
]"
|
|
210
|
+
>
|
|
211
|
+
<slot name="avatar-image" />
|
|
212
|
+
</div>
|
|
213
|
+
<!-- #endregion - Avatar -->
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</template>
|
|
217
|
+
|
|
218
|
+
<script lang="ts" setup>
|
|
219
|
+
import { sidenavPropTypes, sidenavEmitTypes } from './sidenav';
|
|
220
|
+
import { useSidenav } from './use-sidenav';
|
|
221
|
+
|
|
222
|
+
import { Menu, Tooltip } from 'floating-vue';
|
|
223
|
+
|
|
224
|
+
import IconPlusCircleFill from '~icons/ph/plus-circle-fill';
|
|
225
|
+
import IconMagnifyingGlass from '~icons/ph/magnifying-glass';
|
|
226
|
+
import IconGlobe from '~icons/ph/globe';
|
|
227
|
+
import IconCaretRight from '~icons/ph/caret-right';
|
|
228
|
+
|
|
229
|
+
import 'floating-vue/dist/style.css';
|
|
230
|
+
|
|
231
|
+
const props = defineProps(sidenavPropTypes);
|
|
232
|
+
const emit = defineEmits(sidenavEmitTypes);
|
|
233
|
+
|
|
234
|
+
const { handleRedirect } = useSidenav(props, emit);
|
|
235
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SetupContext } from 'vue';
|
|
2
|
+
|
|
3
|
+
import type { SidenavPropTypes, SidenavEmitTypes } from './sidenav';
|
|
4
|
+
|
|
5
|
+
interface MouseEventWithCtrl extends MouseEvent {
|
|
6
|
+
ctrlKey: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Redirect {
|
|
10
|
+
openInNewTab: boolean;
|
|
11
|
+
isAbsoluteURL: boolean;
|
|
12
|
+
link: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const useSidenav = (props: SidenavPropTypes, emit: SetupContext<SidenavEmitTypes>['emit']) => {
|
|
16
|
+
const handleRedirect = (e: MouseEventWithCtrl, redirect: Redirect) => {
|
|
17
|
+
if (redirect) {
|
|
18
|
+
if (redirect.openInNewTab) {
|
|
19
|
+
window.open(redirect.link, '_blank');
|
|
20
|
+
} else if (redirect.isAbsoluteURL) {
|
|
21
|
+
location.href = redirect.link;
|
|
22
|
+
} else {
|
|
23
|
+
emit('route-push', redirect.link);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
handleRedirect,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PropType, ExtractPropTypes } from 'vue';
|
|
2
|
+
|
|
3
|
+
export const definePropType = <T>(val: unknown): PropType<T> => val as PropType<T>;
|
|
4
|
+
const SWITCH_STATES = ['default', 'hover', 'pressed', 'disabled'] as const;
|
|
5
|
+
|
|
6
|
+
export const switchPropTypes = {
|
|
7
|
+
/**
|
|
8
|
+
* @description Switch UI state when hovered, pressed, disabled
|
|
9
|
+
*/
|
|
10
|
+
state: {
|
|
11
|
+
type: String as PropType<(typeof SWITCH_STATES)[number]>,
|
|
12
|
+
validator: (value: (typeof SWITCH_STATES)[number]) => SWITCH_STATES.includes(value),
|
|
13
|
+
default: 'default',
|
|
14
|
+
},
|
|
15
|
+
/**
|
|
16
|
+
* @description Switch input state when disabled
|
|
17
|
+
*/
|
|
18
|
+
disabled: {
|
|
19
|
+
type: Boolean,
|
|
20
|
+
default: false,
|
|
21
|
+
},
|
|
22
|
+
/**
|
|
23
|
+
* @description Required prop value for v-model
|
|
24
|
+
*/
|
|
25
|
+
modelValue: {
|
|
26
|
+
type: Boolean,
|
|
27
|
+
required: true,
|
|
28
|
+
default: false,
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const switchEmitTypes = ['update:modelValue'];
|
|
33
|
+
|
|
34
|
+
export type SwitchPropTypes = ExtractPropTypes<typeof switchPropTypes>;
|
|
35
|
+
export type SwitchEmitTypes = typeof switchEmitTypes;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-bind="switchProps" :class="['switch', switchTextClass]">
|
|
3
|
+
<label class="switch_text switch_left-text">
|
|
4
|
+
<slot name="leftText">
|
|
5
|
+
<slot></slot>
|
|
6
|
+
</slot>
|
|
7
|
+
</label>
|
|
8
|
+
<div ref="switchWrapperRef" class="switch_wrapper" >
|
|
9
|
+
<input
|
|
10
|
+
ref="switchRef"
|
|
11
|
+
v-model="proxyValue"
|
|
12
|
+
type="checkbox"
|
|
13
|
+
name="checkbox"
|
|
14
|
+
:class="['switch_input', switchInputClass]"
|
|
15
|
+
:disabled="props.disabled"
|
|
16
|
+
>
|
|
17
|
+
<span :class="['switch_mark', switchMarkClass]"></span>
|
|
18
|
+
</div>
|
|
19
|
+
<label class="switch_text switch_right-text">
|
|
20
|
+
<slot name="rightText"></slot>
|
|
21
|
+
</label>
|
|
22
|
+
</div>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script lang="ts" setup>
|
|
26
|
+
import { switchEmitTypes, switchPropTypes } from './switch';
|
|
27
|
+
import { useSwitch } from './use-switch';
|
|
28
|
+
import { useVModel } from '@vueuse/core';
|
|
29
|
+
|
|
30
|
+
const props = defineProps(switchPropTypes);
|
|
31
|
+
const emit = defineEmits(switchEmitTypes);
|
|
32
|
+
|
|
33
|
+
const proxyValue = useVModel(props, "modelValue", emit);
|
|
34
|
+
|
|
35
|
+
const {
|
|
36
|
+
switchWrapperRef,
|
|
37
|
+
switchRef,
|
|
38
|
+
switchProps,
|
|
39
|
+
switchMarkClass,
|
|
40
|
+
switchTextClass,
|
|
41
|
+
switchInputClass,
|
|
42
|
+
} = useSwitch(props);
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<style lang="scss" scoped>
|
|
46
|
+
.switch {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
width: fit-content;
|
|
50
|
+
|
|
51
|
+
&_left-text {
|
|
52
|
+
margin-right: 8px;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
&_right-text {
|
|
56
|
+
margin-left: 8px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
&_wrapper{
|
|
60
|
+
position: relative;
|
|
61
|
+
display: inline;
|
|
62
|
+
height: 24px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
&_input{
|
|
66
|
+
position: absolute;
|
|
67
|
+
top: 0;
|
|
68
|
+
left: 0;
|
|
69
|
+
opacity: 0;
|
|
70
|
+
z-index: 1;
|
|
71
|
+
width: 48px;
|
|
72
|
+
height: 24px;
|
|
73
|
+
margin: 0;
|
|
74
|
+
|
|
75
|
+
&:checked ~ .switch_mark:before{
|
|
76
|
+
left: 28px;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&_mark{
|
|
81
|
+
display: inline-block;
|
|
82
|
+
position: relative;
|
|
83
|
+
box-sizing: border-box;
|
|
84
|
+
border-radius: 40px;
|
|
85
|
+
width: 48px;
|
|
86
|
+
height: 24px;
|
|
87
|
+
padding: 4px;
|
|
88
|
+
|
|
89
|
+
&:before,
|
|
90
|
+
&:after{
|
|
91
|
+
content: "";
|
|
92
|
+
position: absolute;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
&:before{
|
|
96
|
+
@apply tw-bg-white-50;
|
|
97
|
+
|
|
98
|
+
top: 4px;
|
|
99
|
+
left: 4px;
|
|
100
|
+
width: 16px;
|
|
101
|
+
height: 16px;
|
|
102
|
+
border-radius: 50%;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { computed, ref, ComputedRef } from 'vue';
|
|
2
|
+
import { useElementHover, useMousePressed } from '@vueuse/core';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import type { SwitchPropTypes } from './switch';
|
|
5
|
+
|
|
6
|
+
export const useSwitch = (props: SwitchPropTypes) => {
|
|
7
|
+
const switchWrapperRef = ref<HTMLDivElement | null>(null);
|
|
8
|
+
const switchRef = ref<HTMLInputElement | null>(null);
|
|
9
|
+
const isHovered = useElementHover(switchWrapperRef);
|
|
10
|
+
const { pressed } = useMousePressed({ target: switchRef });
|
|
11
|
+
const { disabled, state, modelValue } = props;
|
|
12
|
+
|
|
13
|
+
const switchProps: ComputedRef<Record<string, unknown>> = computed(() => {
|
|
14
|
+
return {
|
|
15
|
+
...(disabled && { ariaDisabled: true }),
|
|
16
|
+
disabled: disabled,
|
|
17
|
+
autofocus: state === 'hover',
|
|
18
|
+
modelValue: modelValue,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// #region - Background CSS Class
|
|
23
|
+
const switchBackgroundCssClass: ComputedRef<string> = computed(() => {
|
|
24
|
+
if (props.disabled) {
|
|
25
|
+
return getDisabledBackground()
|
|
26
|
+
}
|
|
27
|
+
if (pressed.value) {
|
|
28
|
+
return getPressedBackground();
|
|
29
|
+
}
|
|
30
|
+
if (isHovered.value) {
|
|
31
|
+
return getHoveredBackground();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return getDefaultBackground();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function getDefaultBackground(): string {
|
|
38
|
+
return props.modelValue ?
|
|
39
|
+
'tw-background-color-success-base' :
|
|
40
|
+
'tw-switch-background-default';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getHoveredBackground(): string {
|
|
44
|
+
return props.modelValue ?
|
|
45
|
+
'tw-background-color-success-hover' :
|
|
46
|
+
'tw-switch-background-hover';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getPressedBackground(): string {
|
|
50
|
+
return props.modelValue ?
|
|
51
|
+
'tw-background-color-success-pressed' :
|
|
52
|
+
'tw-switch-background-pressed';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getDisabledBackground(): string {
|
|
56
|
+
return classNames(
|
|
57
|
+
{
|
|
58
|
+
'tw-background-color-success-base' : props.modelValue,
|
|
59
|
+
'tw-switch-background-default' : !props.modelValue
|
|
60
|
+
},
|
|
61
|
+
'tw-opacity-60',
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
// #endregion - Background CSS Class
|
|
65
|
+
|
|
66
|
+
const switchTextClass: ComputedRef<string> = computed(() => {
|
|
67
|
+
if (props.disabled) {
|
|
68
|
+
return 'tw-text-color-disabled';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return 'tw-text-color-strong';
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const switchAnimationCssClass: ComputedRef<string> = computed(() => {
|
|
75
|
+
return classNames(
|
|
76
|
+
'tw-transition-colors',
|
|
77
|
+
'before:tw-transition-all',
|
|
78
|
+
'before:tw-duration-150',
|
|
79
|
+
'after:tw-transition-all',
|
|
80
|
+
'after:tw-duration-150',
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const switchMarkClass: ComputedRef<string> = computed(() => {
|
|
85
|
+
return classNames(
|
|
86
|
+
switchBackgroundCssClass.value,
|
|
87
|
+
switchAnimationCssClass.value
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const switchInputClass: ComputedRef<string> = computed(() => {
|
|
92
|
+
return classNames({
|
|
93
|
+
'tw-cursor-not-allowed': props.disabled,
|
|
94
|
+
'tw-cursor-pointer': !props.disabled,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
switchWrapperRef,
|
|
100
|
+
switchRef,
|
|
101
|
+
switchProps,
|
|
102
|
+
switchMarkClass,
|
|
103
|
+
switchTextClass,
|
|
104
|
+
switchInputClass
|
|
105
|
+
};
|
|
106
|
+
};
|
package/src/main.ts
ADDED