@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bagelink/vue",
3
3
  "type": "module",
4
- "version": "1.15.239",
4
+ "version": "1.15.243",
5
5
  "description": "Bagel core sdk packages",
6
6
  "author": {
7
7
  "name": "Bagel Studio",
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- defineOptions({ name: 'BglImage', inheritAttrs: true })
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, reactive, ref } from 'vue'
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
- function handleKeyDown(event: KeyboardEvent, index: number) {
57
- const noPreventKeys = ['Tab', 'ArrowRight', 'ArrowLeft', 'Control', 'Shift', 'Alt', 'Meta']
58
- if (noPreventKeys.includes(event.key)) {
59
- return
60
- }
61
-
62
- const preventKeys = ['ArrowUp', 'ArrowDown']
63
- if (preventKeys.includes(event.key)) {
64
- event.preventDefault()
65
- return
66
- }
67
-
68
- if (event.key === 'Backspace') {
69
- digits[index] = undefined
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
- if (index !== 0) {
72
- const prevDigit = otpCont.value?.children[index - 1] as any
73
- prevDigit.focus()
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
- const numberRegex = /^\d$/
80
- if (numberRegex.test(event.key)) {
81
- digits[index] = event.key
82
-
83
- if (index !== props.digitCount - 1) {
84
- setTimeout(() => {
85
- const nextDigit = otpCont.value?.children[index + 1] as any
86
- nextDigit.focus()
87
- }, 10)
88
- }
89
- emitUpdate()
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
- if (props.default && props.default.length === props.digitCount) {
94
- for (let i = 0; i < props.digitCount; i++) {
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 isDigitsFull() {
104
- for (const elem of digits) {
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 ref="otpCont" class="otp_wrap ltr" :class="{ 'has-error': !!error }">
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
- v-for="(_el, ind) in digits" :key="ind" :value="digits[ind] || ''" type="number" inputmode="numeric"
121
- class="digit-box" :autofocus="ind === 0" maxlength="1" pattern="[0-9]*"
122
- oninput="this.value = this.value.slice(0, 1)" @keydown="handleKeyDown($event, ind)"
123
- @paste="handlePaste($event, ind)" @keyup.prevent
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 input,
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-block;
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
- -moz-appearance: textfield;
161
- /* Firefox */
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:focus {
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:focus {
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 grid gap-05">
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="{ ...props, ...attrs }"
69
+ v-bind="{ ...navProps, ...attrs }"
62
70
  v-model="slctTab"
63
71
  :group="group"
64
72
  />