@bagelink/vue 1.15.239 → 1.15.243
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/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/OTP.vue.d.ts +1 -1
- package/dist/components/form/inputs/OTP.vue.d.ts.map +1 -1
- package/dist/components/layout/Tabs.vue.d.ts +14 -1
- package/dist/components/layout/Tabs.vue.d.ts.map +1 -1
- package/dist/index.cjs +26 -26
- package/dist/index.mjs +1783 -1802
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Image.vue +6 -2
- package/src/components/form/inputs/OTP.vue +98 -108
- package/src/components/layout/Tabs.vue +11 -3
package/package.json
CHANGED
package/src/components/Image.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
defineOptions({ name: 'BglImage', inheritAttrs:
|
|
2
|
+
defineOptions({ name: 'BglImage', inheritAttrs: false })
|
|
3
3
|
import { Skeleton, normalizeDimension, Icon, useImageSrc } from '@bagelink/vue'
|
|
4
4
|
import { Comment, computed, useSlots, type StyleValue, type VNode } from 'vue'
|
|
5
5
|
|
|
@@ -68,14 +68,18 @@ const imgFit = computed(() => props.fit ?? 'cover')
|
|
|
68
68
|
</script>
|
|
69
69
|
|
|
70
70
|
<template>
|
|
71
|
-
<!-- Framed mode: ratio box with gradient base, blended photo, and overlay slot.
|
|
71
|
+
<!-- Framed mode: ratio box with gradient base, blended photo, and overlay slot.
|
|
72
|
+
Multi-root component → bind $attrs explicitly so class/style (e.g. flex-shrink-0) land on the frame. -->
|
|
72
73
|
<div
|
|
73
74
|
v-if="framed"
|
|
75
|
+
v-bind="$attrs"
|
|
74
76
|
class="bgl-image-frame relative overflow-hidden"
|
|
75
77
|
:class="ratioClass"
|
|
76
78
|
:style="{
|
|
77
79
|
width: normalizeDimension(width),
|
|
80
|
+
minWidth: normalizeDimension(width),
|
|
78
81
|
height: ratio ? undefined : normalizeDimension(height),
|
|
82
|
+
minHeight: ratio ? undefined : normalizeDimension(height),
|
|
79
83
|
background: gradientCss,
|
|
80
84
|
borderRadius: radiusStyle,
|
|
81
85
|
}"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, ref, watch } from 'vue'
|
|
3
3
|
import type { BagelInputShellProps } from './bagelInputShell'
|
|
4
4
|
import { useBagelInputShell } from './bagelInputShell'
|
|
5
5
|
|
|
@@ -12,6 +12,8 @@ const props = defineProps<{
|
|
|
12
12
|
color?: string
|
|
13
13
|
} & BagelInputShellProps>()
|
|
14
14
|
|
|
15
|
+
const emit = defineEmits(['update:modelValue', 'complete'])
|
|
16
|
+
|
|
15
17
|
const { shellClass, shellStyle: baseShellStyle } = useBagelInputShell(props)
|
|
16
18
|
|
|
17
19
|
const digitColor = computed(() => {
|
|
@@ -25,88 +27,49 @@ const shellStyle = computed(() => {
|
|
|
25
27
|
if (!color) { return baseShellStyle.value }
|
|
26
28
|
return { ...baseShellStyle.value, '--bgl-otp-digit-color': color }
|
|
27
29
|
})
|
|
28
|
-
const hasValue = computed(() => digits.some(d => d !== undefined && d !== ''))
|
|
29
|
-
|
|
30
|
-
const emit = defineEmits(['update:modelValue', 'complete'])
|
|
31
|
-
const digits = reactive<(number | string | undefined)[]>([])
|
|
32
|
-
const otpCont = ref<HTMLElement>()
|
|
33
|
-
|
|
34
|
-
function handlePaste(event: ClipboardEvent, index: number) {
|
|
35
|
-
event.preventDefault()
|
|
36
|
-
const { clipboardData } = event
|
|
37
|
-
const pastedText = clipboardData?.getData('text').replaceAll(/\D/g, '') // Ensure only numeric digits are pasted
|
|
38
|
-
const pastedDigits = [...(pastedText ?? '')]
|
|
39
|
-
if (!pastedDigits) { return }
|
|
40
|
-
for (let i = 0; i < pastedDigits.length; i++) {
|
|
41
|
-
const digit = pastedDigits[i]
|
|
42
|
-
if (index + i < digits.length) {
|
|
43
|
-
digits[index + i] = digit
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
emitUpdate()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function emitUpdate() {
|
|
50
|
-
emit('update:modelValue', digits.join(''))
|
|
51
|
-
if (isDigitsFull()) {
|
|
52
|
-
emit('complete', digits.join(''))
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
30
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
31
|
+
// ── Single-input model ──────────────────────────────────────────────────────
|
|
32
|
+
// One real <input> holds the whole code; the N boxes below are display-only
|
|
33
|
+
// mirrors. A single field is what makes OS SMS OTP autofill work — iOS/web need
|
|
34
|
+
// `autocomplete="one-time-code"` on ONE input (a split field never triggers it),
|
|
35
|
+
// and Android's SMS Retriever pastes the whole code at once.
|
|
36
|
+
const inputEl = ref<HTMLInputElement>()
|
|
37
|
+
|
|
38
|
+
const value = ref<string>(
|
|
39
|
+
(props.default ?? props.modelValue ?? '').replace(/\D/g, '').slice(0, props.digitCount),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// Reflect external modelValue changes (e.g. a parent "reset" back to '').
|
|
43
|
+
watch(() => props.modelValue, (next) => {
|
|
44
|
+
const clean = (next ?? '').replace(/\D/g, '').slice(0, props.digitCount)
|
|
45
|
+
if (clean !== value.value) { value.value = clean }
|
|
46
|
+
})
|
|
70
47
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
emitUpdate()
|
|
76
|
-
return
|
|
77
|
-
}
|
|
48
|
+
const digits = computed(() => {
|
|
49
|
+
const chars = value.value.split('')
|
|
50
|
+
return Array.from({ length: props.digitCount }, (_, i) => chars[i] ?? '')
|
|
51
|
+
})
|
|
78
52
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
53
|
+
const hasValue = computed(() => value.value.length > 0)
|
|
54
|
+
// The "active" box is the next empty slot (or the last one when full).
|
|
55
|
+
const activeIndex = computed(() => Math.min(value.value.length, props.digitCount - 1))
|
|
56
|
+
const focused = ref(false)
|
|
57
|
+
|
|
58
|
+
function setValue(next: string) {
|
|
59
|
+
const clean = next.replace(/\D/g, '').slice(0, props.digitCount)
|
|
60
|
+
value.value = clean
|
|
61
|
+
emit('update:modelValue', clean)
|
|
62
|
+
if (clean.length === props.digitCount) {
|
|
63
|
+
emit('complete', clean)
|
|
90
64
|
}
|
|
91
65
|
}
|
|
92
66
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
digits[i] = props.default.charAt(i)
|
|
96
|
-
}
|
|
97
|
-
} else {
|
|
98
|
-
for (let i = 0; i < props.digitCount; i++) {
|
|
99
|
-
digits[i] = undefined
|
|
100
|
-
}
|
|
67
|
+
function onInput(e: Event) {
|
|
68
|
+
setValue((e.target as HTMLInputElement).value)
|
|
101
69
|
}
|
|
102
70
|
|
|
103
|
-
function
|
|
104
|
-
|
|
105
|
-
if (elem === undefined) {
|
|
106
|
-
return false
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return true
|
|
71
|
+
function focusInput() {
|
|
72
|
+
inputEl.value?.focus()
|
|
110
73
|
}
|
|
111
74
|
</script>
|
|
112
75
|
|
|
@@ -114,14 +77,37 @@ function isDigitsFull() {
|
|
|
114
77
|
<div
|
|
115
78
|
class="otp-input" :class="[shellClass, { 'has-value': hasValue }]" :style="shellStyle"
|
|
116
79
|
tabindex="-1"
|
|
80
|
+
@click="focusInput"
|
|
117
81
|
>
|
|
118
|
-
<div
|
|
82
|
+
<div class="otp_wrap ltr" :class="{ 'has-error': !!error }">
|
|
83
|
+
<!-- The single, real input. Transparent + overlaid on the boxes so OS
|
|
84
|
+
autofill, paste, and typing all target it. -->
|
|
119
85
|
<input
|
|
120
|
-
|
|
121
|
-
class="
|
|
122
|
-
|
|
123
|
-
|
|
86
|
+
ref="inputEl"
|
|
87
|
+
class="otp-hidden-input"
|
|
88
|
+
:value="value"
|
|
89
|
+
type="text"
|
|
90
|
+
inputmode="numeric"
|
|
91
|
+
autocomplete="one-time-code"
|
|
92
|
+
:maxlength="digitCount"
|
|
93
|
+
:autofocus="true"
|
|
94
|
+
name="one-time-code"
|
|
95
|
+
pattern="[0-9]*"
|
|
96
|
+
aria-label="verification code"
|
|
97
|
+
@input="onInput"
|
|
98
|
+
@focus="focused = true"
|
|
99
|
+
@blur="focused = false"
|
|
100
|
+
>
|
|
101
|
+
|
|
102
|
+
<!-- Display-only mirror boxes. -->
|
|
103
|
+
<div
|
|
104
|
+
v-for="(d, ind) in digits" :key="ind"
|
|
105
|
+
class="digit-box"
|
|
106
|
+
:class="{ 'is-active': focused && ind === activeIndex, 'is-filled': d !== '' }"
|
|
107
|
+
aria-hidden="true"
|
|
124
108
|
>
|
|
109
|
+
{{ d }}
|
|
110
|
+
</div>
|
|
125
111
|
</div>
|
|
126
112
|
<div v-if="error" class="otp-error-message txt-center txt10 pt-05">
|
|
127
113
|
{{ error }}
|
|
@@ -134,46 +120,59 @@ function isDigitsFull() {
|
|
|
134
120
|
.otp-error-message {
|
|
135
121
|
color: var(--bgl-red);
|
|
136
122
|
}
|
|
137
|
-
.otp_wrap.has-error
|
|
138
|
-
.otp_wrap.has-error textarea {
|
|
123
|
+
.otp_wrap.has-error .digit-box {
|
|
139
124
|
outline: 1px solid var(--bgl-red) !important;
|
|
140
125
|
}
|
|
141
126
|
|
|
142
127
|
.otp_wrap {
|
|
128
|
+
position: relative;
|
|
143
129
|
display: flex;
|
|
144
130
|
flex-direction: row;
|
|
145
131
|
justify-content: center;
|
|
146
132
|
align-items: center;
|
|
133
|
+
cursor: text;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Real input sits on top of the boxes, fully transparent, so taps/focus/autofill
|
|
137
|
+
land on it. The digits render in the boxes below. */
|
|
138
|
+
.otp-hidden-input {
|
|
139
|
+
position: absolute;
|
|
140
|
+
inset: 0;
|
|
141
|
+
width: 100%;
|
|
142
|
+
height: 100%;
|
|
143
|
+
margin: 0;
|
|
144
|
+
padding: 0;
|
|
145
|
+
border: 0;
|
|
146
|
+
background: transparent;
|
|
147
|
+
color: transparent;
|
|
148
|
+
caret-color: transparent;
|
|
149
|
+
text-align: center;
|
|
150
|
+
font-size: 1px;
|
|
151
|
+
letter-spacing: 0;
|
|
152
|
+
z-index: 2;
|
|
153
|
+
outline: none;
|
|
154
|
+
-webkit-tap-highlight-color: transparent;
|
|
147
155
|
}
|
|
156
|
+
.otp-hidden-input::selection { background: transparent; }
|
|
148
157
|
|
|
149
158
|
.digit-box {
|
|
150
159
|
height: 3rem;
|
|
151
160
|
flex-grow: 1;
|
|
152
161
|
color: var(--bgl-otp-digit-color, inherit);
|
|
153
162
|
border: 1px solid var(--bgl-input-label-active-color, var(--bgl-primary-tint));
|
|
154
|
-
display: inline-
|
|
163
|
+
display: inline-flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
justify-content: center;
|
|
155
166
|
background: var(--bgl-gray-light);
|
|
156
167
|
border-radius: 5px;
|
|
157
168
|
margin: 5px;
|
|
158
169
|
text-align: center;
|
|
159
170
|
font-size: 2.4rem;
|
|
160
|
-
-
|
|
161
|
-
|
|
162
|
-
caret-color: transparent;
|
|
163
|
-
/* Remove spinner arrows */
|
|
164
|
-
appearance: textfield;
|
|
165
|
-
/* Standardized property */
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/* Remove spinner arrows for Webkit browsers */
|
|
169
|
-
.digit-box::-webkit-outer-spin-button,
|
|
170
|
-
.digit-box::-webkit-inner-spin-button {
|
|
171
|
-
-webkit-appearance: none;
|
|
172
|
-
appearance: none;
|
|
173
|
-
margin: 0;
|
|
171
|
+
user-select: none;
|
|
172
|
+
transition: outline-color 0.2s ease, box-shadow 0.2s ease;
|
|
174
173
|
}
|
|
175
174
|
|
|
176
|
-
.digit-box
|
|
175
|
+
.digit-box.is-active {
|
|
177
176
|
outline: 1px solid var(--bgl-input-label-active-color, var(--bgl-primary));
|
|
178
177
|
filter: drop-shadow(0 0 0.25rem var(--bgl-input-label-active-color, var(--bgl-primary)));
|
|
179
178
|
outline-offset: 1px;
|
|
@@ -185,10 +184,9 @@ outline-offset: 1px;
|
|
|
185
184
|
background: transparent;
|
|
186
185
|
outline: 1.5px solid var(--bgl-border-color);
|
|
187
186
|
outline-offset: -1px;
|
|
188
|
-
transition: outline-color 0.2s ease, box-shadow 0.2s ease;
|
|
189
187
|
}
|
|
190
188
|
|
|
191
|
-
.otp-input.frame .digit-box
|
|
189
|
+
.otp-input.frame .digit-box.is-active {
|
|
192
190
|
outline-color: var(--bgl-input-label-active-color, var(--bgl-primary));
|
|
193
191
|
outline-width: 2px;
|
|
194
192
|
box-shadow:
|
|
@@ -204,14 +202,6 @@ outline: 1.5px solid var(--bgl-border-color);
|
|
|
204
202
|
outline-offset: -1px;
|
|
205
203
|
}
|
|
206
204
|
|
|
207
|
-
/* Redundant, already covered above */
|
|
208
|
-
/* .digit-box[type="number"] {
|
|
209
|
-
-moz-appearance: textfield;
|
|
210
|
-
} */
|
|
211
|
-
.relative {
|
|
212
|
-
position: relative !important;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
205
|
@media screen and (max-width: 910px) {
|
|
216
206
|
.digit-box {
|
|
217
207
|
padding: 0.25rem;
|
|
@@ -20,6 +20,8 @@ const props = defineProps<{
|
|
|
20
20
|
fullWidthMobile?: boolean
|
|
21
21
|
alignTxt?: 'center' | 'start' | 'end'
|
|
22
22
|
alignTxtMobile?: 'center' | 'start' | 'end'
|
|
23
|
+
/** Horizontally center the tab bar (content panels stay full-width). */
|
|
24
|
+
center?: boolean
|
|
23
25
|
icon?: IconType
|
|
24
26
|
iconEnd?: IconType
|
|
25
27
|
iconSize?: number | string
|
|
@@ -41,6 +43,12 @@ const slctTab = computed({
|
|
|
41
43
|
},
|
|
42
44
|
})
|
|
43
45
|
|
|
46
|
+
/** Props forwarded to TabsNav — omit `center` (layout-only here). */
|
|
47
|
+
const navProps = computed(() => {
|
|
48
|
+
const { center: _center, modelValue: _mv, ...rest } = props
|
|
49
|
+
return rest
|
|
50
|
+
})
|
|
51
|
+
|
|
44
52
|
const tabComponent = defineComponent({
|
|
45
53
|
render() {
|
|
46
54
|
const currentTabIndex = props.tabs.findIndex(
|
|
@@ -55,10 +63,10 @@ const tabComponent = defineComponent({
|
|
|
55
63
|
</script>
|
|
56
64
|
|
|
57
65
|
<template>
|
|
58
|
-
<div class="bgl_tabs
|
|
59
|
-
<div class="flex justify-content-center">
|
|
66
|
+
<div class="bgl_tabs">
|
|
67
|
+
<div :class="center ? 'flex justify-content-center' : undefined">
|
|
60
68
|
<TabsNav
|
|
61
|
-
v-bind="{ ...
|
|
69
|
+
v-bind="{ ...navProps, ...attrs }"
|
|
62
70
|
v-model="slctTab"
|
|
63
71
|
:group="group"
|
|
64
72
|
/>
|