@dolanske/vui 0.1.3 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dolanske/vui",
3
3
  "type": "module",
4
- "version": "0.1.3",
4
+ "version": "0.1.5",
5
5
  "private": false,
6
6
  "description": "Brother in Christ there's a new UI library ",
7
7
  "author": "dolanske",
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
- import Flex from './components/Flex/Flex.vue'
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
- const value = ref('')
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
- <Flex>
27
- <Button variant="success">
28
- Hello
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
- {{ value }}
38
-
39
- <Button variant="success" disabled>
40
- Hello
41
- </Button>
42
- </Flex>
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 ref="content" class="vui-accordion-content" :style="{ 'max-height': isOpen ? `${contentMaxHeight}px` : '0px' }">
72
- <slot />
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>
@@ -1,5 +1,5 @@
1
1
  .vui-button {
2
- --button-height: 32px;
2
+ --button-height: 34px;
3
3
  --button-padding: 8px;
4
4
 
5
5
  &.disabled {
@@ -16,7 +16,7 @@
16
16
  width: fit-content;
17
17
  gap: var(--space-xs);
18
18
  width: 100%;
19
- height: 32px;
19
+ height: 34px;
20
20
  border-radius: var(--border-radius-m);
21
21
  font-size: var(--font-size-s);
22
22
  cursor: default;
@@ -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,
@@ -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
+ }