@dolanske/vui 0.1.4 → 0.1.5
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 +6 -3
- package/dist/components/OTP/OTP.vue.d.ts +43 -0
- package/dist/components/OTP/OTPItem.vue.d.ts +5 -0
- package/dist/index.d.ts +3 -1
- package/dist/shared/helpers.d.ts +2 -1
- package/dist/style.css +1 -1
- package/dist/vui.js +3539 -3409
- package/package.json +1 -1
- package/src/App.vue +16 -22
- package/src/components/Accordion/Accordion.vue +12 -2
- package/src/components/OTP/OTP.vue +133 -0
- package/src/components/OTP/OTPItem.vue +37 -0
- package/src/components/OTP/otp.scss +84 -0
- package/src/index.ts +4 -0
- package/src/shared/helpers.ts +7 -1
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import Accordion from './components/Accordion/Accordion.vue'
|
|
4
4
|
import Button from './components/Button/Button.vue'
|
|
5
|
-
|
|
6
|
-
import Input from './components/Input/Input.vue'
|
|
5
|
+
|
|
7
6
|
import Tab from './components/Tabs/Tab.vue'
|
|
7
|
+
|
|
8
8
|
import Tabs from './components/Tabs/Tabs.vue'
|
|
9
9
|
|
|
10
10
|
const tab = ref('components')
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
const count = ref(10)
|
|
12
13
|
</script>
|
|
13
14
|
|
|
14
15
|
<template>
|
|
@@ -23,27 +24,20 @@ const value = ref('')
|
|
|
23
24
|
home
|
|
24
25
|
</div>
|
|
25
26
|
<div v-if="tab === 'components'">
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
</Button>
|
|
30
|
-
<Button variant="success" plain>
|
|
31
|
-
Hello
|
|
32
|
-
</Button>
|
|
33
|
-
<Button variant="success" active>
|
|
34
|
-
Hello
|
|
35
|
-
</Button>
|
|
27
|
+
<Button @click="count += 10">
|
|
28
|
+
Increase!!!
|
|
29
|
+
</Button>
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<Accordion label="Hi">
|
|
45
|
-
<Input v-model="value" />
|
|
31
|
+
<Accordion>
|
|
32
|
+
<template #header>
|
|
33
|
+
Hiiii
|
|
34
|
+
</template>
|
|
35
|
+
<p v-for="item in count" :key="item">
|
|
36
|
+
I am a bro
|
|
37
|
+
</p>
|
|
46
38
|
</Accordion>
|
|
39
|
+
|
|
40
|
+
<br>
|
|
47
41
|
</div>
|
|
48
42
|
<div v-else-if="tab === 'typography'" class="typeset" :style="{ maxWidth: '688px', margin: 'auto' }">
|
|
49
43
|
<h1>The Joke Tax Chronicles</h1>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { Icon } from '@iconify/vue'
|
|
3
|
+
import { useResizeObserver } from '@vueuse/core'
|
|
3
4
|
import { ref, useTemplateRef, watch, watchEffect } from 'vue'
|
|
4
5
|
import './accordion.scss'
|
|
5
6
|
|
|
@@ -16,6 +17,7 @@ const emits = defineEmits<{
|
|
|
16
17
|
|
|
17
18
|
const isOpen = ref(false)
|
|
18
19
|
const contentRef = useTemplateRef('content')
|
|
20
|
+
// const contentChild = useTemplateRef('content-child')
|
|
19
21
|
const contentMaxHeight = ref(0)
|
|
20
22
|
|
|
21
23
|
watchEffect(() => {
|
|
@@ -55,6 +57,12 @@ defineExpose({
|
|
|
55
57
|
toggle,
|
|
56
58
|
isOpen,
|
|
57
59
|
})
|
|
60
|
+
|
|
61
|
+
useResizeObserver(contentRef, ([entry]) => {
|
|
62
|
+
if (isOpen.value && contentMaxHeight.value !== entry.contentRect.height) {
|
|
63
|
+
contentMaxHeight.value = entry.contentRect.height || 0
|
|
64
|
+
}
|
|
65
|
+
})
|
|
58
66
|
</script>
|
|
59
67
|
|
|
60
68
|
<template>
|
|
@@ -68,8 +76,10 @@ defineExpose({
|
|
|
68
76
|
<Icon icon="ph:caret-down" />
|
|
69
77
|
</button>
|
|
70
78
|
|
|
71
|
-
<div
|
|
72
|
-
<
|
|
79
|
+
<div class="vui-accordion-content" :style="{ 'max-height': isOpen ? `${contentMaxHeight}px` : '0px' }">
|
|
80
|
+
<div ref="content">
|
|
81
|
+
<slot />
|
|
82
|
+
</div>
|
|
73
83
|
</div>
|
|
74
84
|
</div>
|
|
75
85
|
</template>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import type { ModelRef, Ref } from 'vue'
|
|
3
|
+
import { computed, provide, ref, toRef, useTemplateRef, watch } from 'vue'
|
|
4
|
+
import { setCharAt } from '../../shared/helpers'
|
|
5
|
+
import './otp.scss'
|
|
6
|
+
|
|
7
|
+
export interface OtpContext {
|
|
8
|
+
otpValue: ModelRef<string>
|
|
9
|
+
cursorIndex: Ref<number>
|
|
10
|
+
redacted: Ref<boolean>
|
|
11
|
+
register: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
mode?: 'num' | 'char' | 'both'
|
|
16
|
+
redacted?: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
mode = 'both',
|
|
21
|
+
redacted = false,
|
|
22
|
+
} = defineProps<Props>()
|
|
23
|
+
|
|
24
|
+
const emits = defineEmits<{
|
|
25
|
+
change: [value?: string]
|
|
26
|
+
complete: [value: string]
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const otpValue = defineModel<string>({
|
|
30
|
+
default: '',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const cursorIndex = ref<number>(-1)
|
|
34
|
+
const regexNumbers = '^\\d+$'
|
|
35
|
+
const regexChars = '^[a-z]+$'
|
|
36
|
+
const regexBoth = '^[a-z0-9]+$'
|
|
37
|
+
|
|
38
|
+
const pattern = computed(() => {
|
|
39
|
+
if (mode === 'num')
|
|
40
|
+
return new RegExp(regexNumbers)
|
|
41
|
+
else if (mode === 'char')
|
|
42
|
+
return new RegExp(regexChars, 'i')
|
|
43
|
+
else return new RegExp(regexBoth, 'i')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const maxLen = ref(0)
|
|
47
|
+
|
|
48
|
+
const input = useTemplateRef('inputRef')
|
|
49
|
+
|
|
50
|
+
provide('otp-context', {
|
|
51
|
+
otpValue,
|
|
52
|
+
cursorIndex,
|
|
53
|
+
redacted: toRef(() => redacted),
|
|
54
|
+
// Called by all OTPItem child components to properly set max length of the input.
|
|
55
|
+
register: () => maxLen.value++,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
watch(otpValue, value => emits('change', value))
|
|
59
|
+
|
|
60
|
+
function setOtpValue(value: string) {
|
|
61
|
+
otpValue.value = value
|
|
62
|
+
if (input.value) {
|
|
63
|
+
input.value.value = value
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function updateValue(e: KeyboardEvent) {
|
|
68
|
+
const key = e.key
|
|
69
|
+
|
|
70
|
+
// Capping at length 0 prevents all non-character keyboard inputs
|
|
71
|
+
if (pattern.value.test(key) && key.length === 1) {
|
|
72
|
+
const newValue = setCharAt(otpValue.value, key, cursorIndex.value)
|
|
73
|
+
|
|
74
|
+
if (newValue.length <= maxLen.value) {
|
|
75
|
+
setOtpValue(newValue)
|
|
76
|
+
|
|
77
|
+
if (cursorIndex.value < maxLen.value - 1)
|
|
78
|
+
cursorIndex.value++
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (key === 'ArrowLeft' && cursorIndex.value > 0) {
|
|
82
|
+
cursorIndex.value--
|
|
83
|
+
}
|
|
84
|
+
else if (key === 'ArrowRight' && cursorIndex.value < otpValue.value.length) {
|
|
85
|
+
cursorIndex.value++
|
|
86
|
+
}
|
|
87
|
+
else if (key === 'Backspace') {
|
|
88
|
+
// If we press backspace multiple times make sure to traverse back by 1
|
|
89
|
+
if (otpValue.value.charAt(cursorIndex.value) === '' && cursorIndex.value > 0) {
|
|
90
|
+
cursorIndex.value--
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const newValue = setCharAt(otpValue.value, '', cursorIndex.value)
|
|
94
|
+
setOtpValue(newValue)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handlePaste(e: any) {
|
|
99
|
+
const clipboard = e.clipboardData?.getData('text/plain')
|
|
100
|
+
if (clipboard) {
|
|
101
|
+
const clipboardTrim = clipboard.trim().slice(0, maxLen.value - cursorIndex.value)
|
|
102
|
+
|
|
103
|
+
if (!pattern.value.test(clipboardTrim)) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const currentTrimStart = otpValue.value.slice(0, cursorIndex.value)
|
|
108
|
+
const currentTrimEnd = otpValue.value.slice(cursorIndex.value + clipboardTrim.length)
|
|
109
|
+
const newValue = (currentTrimStart + clipboardTrim + currentTrimEnd).trim()
|
|
110
|
+
setOtpValue(newValue)
|
|
111
|
+
cursorIndex.value = Math.min(newValue.length, maxLen.value - 1)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="vui-otp">
|
|
118
|
+
<input
|
|
119
|
+
ref="inputRef"
|
|
120
|
+
type="text"
|
|
121
|
+
class="vui-otp-input"
|
|
122
|
+
contenteditable="true"
|
|
123
|
+
@keydown="updateValue"
|
|
124
|
+
@blur="cursorIndex = -1"
|
|
125
|
+
@focus="cursorIndex = Math.min(otpValue.length, maxLen - 1)"
|
|
126
|
+
@paste="handlePaste"
|
|
127
|
+
>
|
|
128
|
+
|
|
129
|
+
<div class="vui-otp-items">
|
|
130
|
+
<slot />
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<script setup lang='ts'>
|
|
2
|
+
import type { OtpContext } from './OTP.vue'
|
|
3
|
+
import { Icon } from '@iconify/vue'
|
|
4
|
+
import { inject } from 'vue'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
i: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const props = defineProps<Props>()
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
otpValue,
|
|
14
|
+
cursorIndex,
|
|
15
|
+
redacted,
|
|
16
|
+
register,
|
|
17
|
+
} = inject('otp-context') as OtpContext
|
|
18
|
+
|
|
19
|
+
register()
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<div
|
|
24
|
+
class="vui-otp-item" :class="{
|
|
25
|
+
'active': props.i === cursorIndex,
|
|
26
|
+
'has-value': otpValue.trim().at(props.i),
|
|
27
|
+
}"
|
|
28
|
+
>
|
|
29
|
+
<div class="blinker" />
|
|
30
|
+
<template v-if="otpValue.trim().at(props.i)">
|
|
31
|
+
<Icon v-if="redacted" icon="ph:asterisk" />
|
|
32
|
+
<template v-else>
|
|
33
|
+
{{ otpValue.at(props.i) }}
|
|
34
|
+
</template>
|
|
35
|
+
</template>
|
|
36
|
+
</div>
|
|
37
|
+
</template>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
.vui-otp {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
position: relative;
|
|
4
|
+
|
|
5
|
+
.vui-otp-items {
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
gap: 0;
|
|
8
|
+
|
|
9
|
+
.vui-otp-item {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
width: 42px;
|
|
14
|
+
height: 42px;
|
|
15
|
+
border: 1px solid var(--color-border-strong);
|
|
16
|
+
color: var(--color-text);
|
|
17
|
+
z-index: 1;
|
|
18
|
+
font-size: var(--font-size-l);
|
|
19
|
+
outline: 0 solid var(--color-text-light);
|
|
20
|
+
transition: var(--transition);
|
|
21
|
+
|
|
22
|
+
.blinker {
|
|
23
|
+
display: none;
|
|
24
|
+
height: 16px;
|
|
25
|
+
width: 1px;
|
|
26
|
+
background-color: var(--color-text);
|
|
27
|
+
animation: blink 1.25s ease-out infinite;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@keyframes blink {
|
|
31
|
+
0%,
|
|
32
|
+
70%,
|
|
33
|
+
100% {
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
20%,
|
|
37
|
+
50% {
|
|
38
|
+
opacity: 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
&.has-value {
|
|
43
|
+
background-color: var(--color-bg-raised);
|
|
44
|
+
|
|
45
|
+
.blinker {
|
|
46
|
+
display: none !important;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// TODO: animate blinking cursor
|
|
51
|
+
&.active {
|
|
52
|
+
z-index: 2;
|
|
53
|
+
outline-width: 2px;
|
|
54
|
+
|
|
55
|
+
.blinker {
|
|
56
|
+
display: block;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
&:not(:first-child) {
|
|
61
|
+
margin-left: -1px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&:first-child {
|
|
65
|
+
border-top-left-radius: var(--border-radius-m);
|
|
66
|
+
border-bottom-left-radius: var(--border-radius-m);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
&:last-child {
|
|
70
|
+
border-top-right-radius: var(--border-radius-m);
|
|
71
|
+
border-bottom-right-radius: var(--border-radius-m);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.vui-otp-input {
|
|
77
|
+
position: absolute;
|
|
78
|
+
inset: 0;
|
|
79
|
+
outline-width: 0px;
|
|
80
|
+
opacity: 0;
|
|
81
|
+
background: transparent;
|
|
82
|
+
z-index: 5;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,8 @@ import Kbd from './components/Kbd/Kbd.vue'
|
|
|
28
28
|
import KbdGroup from './components/Kbd/KbdGroup.vue'
|
|
29
29
|
import Confirm from './components/Modal/Confirm.vue'
|
|
30
30
|
import Modal from './components/Modal/Modal.vue'
|
|
31
|
+
import OTP from './components/OTP/OTP.vue'
|
|
32
|
+
import OTPItem from './components/OTP/OTPItem.vue'
|
|
31
33
|
import Pagination from './components/Pagination/Pagination.vue'
|
|
32
34
|
import Popout from './components/Popout/Popout.vue'
|
|
33
35
|
import Progress from './components/Progress/Progress.vue'
|
|
@@ -85,6 +87,8 @@ export {
|
|
|
85
87
|
Kbd,
|
|
86
88
|
KbdGroup,
|
|
87
89
|
Modal,
|
|
90
|
+
OTP,
|
|
91
|
+
OTPItem,
|
|
88
92
|
Pagination,
|
|
89
93
|
Password,
|
|
90
94
|
Popout,
|
package/src/shared/helpers.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function getMaybeRefLength(value: string | number): number {
|
|
|
21
21
|
return typeof value === 'number' ? value : value.length
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function isNil(value: any): value is undefined {
|
|
24
|
+
export function isNil(value: any): value is undefined | null {
|
|
25
25
|
return value === undefined || value === null
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -51,3 +51,9 @@ export function randomMinMax(min: number, max: number): number {
|
|
|
51
51
|
export function delay(amount: number): Promise<any> {
|
|
52
52
|
return new Promise(r => setTimeout(r, amount))
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
export function setCharAt(str: string, char: string | number, index: number): string {
|
|
56
|
+
if (str.length === 0)
|
|
57
|
+
return char.toString()
|
|
58
|
+
return str.substring(0, index) + char + str.substring(index + 1)
|
|
59
|
+
}
|