@meistrari/tela-build 1.41.0 → 1.42.1

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.
@@ -0,0 +1,13 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ class?: HTMLAttributes['class']
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <div :class="cn('flex items-center gap-1', props.class)">
11
+ <slot />
12
+ </div>
13
+ </template>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue'
3
+
4
+ const props = defineProps<{
5
+ class?: HTMLAttributes['class']
6
+ }>()
7
+ </script>
8
+
9
+ <template>
10
+ <div
11
+ aria-hidden="true"
12
+ :class="cn('h-0.5 w-2 rounded-full bg-border-strong', props.class)"
13
+ />
14
+ </template>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ import type { SlotProps } from 'vue-input-otp'
3
+ import { useVueOTPContext } from 'vue-input-otp'
4
+ import type { HTMLAttributes } from 'vue'
5
+
6
+ const props = defineProps<SlotProps & {
7
+ index: number
8
+ invalid?: boolean
9
+ class?: HTMLAttributes['class']
10
+ }>()
11
+
12
+ const otpContext = useVueOTPContext()
13
+
14
+ const isMultiSelect = computed(() => {
15
+ const activeSlots = otpContext?.value.slots.filter(s => s.isActive) ?? []
16
+
17
+ return activeSlots.length > 1
18
+ })
19
+
20
+ const indicatorLayoutId = computed(() =>
21
+ isMultiSelect.value ? `indicator-${props.index}` : 'indicator',
22
+ )
23
+ </script>
24
+
25
+ <template>
26
+ <MotionConfig reduced-motion="user">
27
+ <div
28
+ :data-active="props.isActive || undefined"
29
+ :aria-invalid="props.invalid || undefined"
30
+ :class="
31
+ cn(
32
+ 'group relative flex w-40px h-40px items-center justify-center',
33
+ 'rounded-10px border-0.5px border bg',
34
+ 'body-16-semibold text-primary',
35
+ '[box-shadow:0_1px_4px_0_rgba(103,127,148,0.03)]',
36
+ 'aria-invalid:!border-red-500 aria-invalid:!text-red-600',
37
+ 'aria-invalid:data-[active]:!ring-red-100',
38
+ props.class,
39
+ )
40
+ "
41
+ >
42
+ <AnimatePresence mode="wait">
43
+ <div
44
+ v-if="props.char"
45
+ :key="props.char"
46
+ class="relative flex size-[inherit] items-center justify-center overflow-hidden"
47
+ >
48
+ <Motion
49
+ :initial="{ opacity: 0.2, y: 20 }"
50
+ :animate="{ opacity: 1, y: 0 }"
51
+ :exit="{ opacity: 0, y: 0 }"
52
+ :transition="{ duration: 0.09, ease: 'easeOut' }"
53
+ >
54
+ {{ props.char }}
55
+ </Motion>
56
+ </div>
57
+ </AnimatePresence>
58
+
59
+ <template v-if="!props.char && props.placeholderChar !== null">
60
+ <span class="text-text-subtle">{{ props.placeholderChar }}</span>
61
+ </template>
62
+
63
+ <div
64
+ v-if="props.hasFakeCaret"
65
+ class="pointer-events-none absolute inset-0 flex items-center justify-center"
66
+ >
67
+ <div class="otp-caret h-4.5 w-px bg-neutral-600" />
68
+ </div>
69
+
70
+ <AnimatePresence mode="wait">
71
+ <Motion
72
+ v-if="props.isActive"
73
+ :key="`${props.isActive}-${isMultiSelect}`"
74
+ :layout-id="indicatorLayoutId"
75
+ :transition="{ duration: 0.12, ease: 'easeInOut' }"
76
+ class="absolute inset-0 z-10 rounded-[inherit] ring-2 ring-border-strong"
77
+ :class="props.invalid && '!ring-red-500'"
78
+ />
79
+ </AnimatePresence>
80
+ </div>
81
+ </MotionConfig>
82
+ </template>
83
+
84
+ <style scoped>
85
+ @keyframes otp-caret-blink {
86
+ 0%, 70%, 100% { opacity: 1; }
87
+ 20%, 50% { opacity: 0; }
88
+ }
89
+
90
+ @media (prefers-reduced-motion: no-preference) {
91
+ .otp-caret {
92
+ animation: otp-caret-blink 1.2s ease-out infinite;
93
+ }
94
+ }
95
+ </style>
@@ -0,0 +1,116 @@
1
+ import { Meta, Canvas, ArgTypes } from '@storybook/blocks';
2
+ import * as InputOtpStories from './input-otp.stories.ts';
3
+
4
+ <Meta of={InputOtpStories} />
5
+
6
+ # TelaInputOtp
7
+
8
+ One-time passcode input. Renders a row of single-character slots that share one hidden input, so paste, arrow-key navigation, backspace, and mobile OTP autofill work transparently. Built on [`vue-input-otp`](https://github.com/wobsoriano/vue-input-otp). Compose with `TelaInputOtpGroup`, `TelaInputOtpSlot`, and `TelaInputOtpSeparator`.
9
+
10
+ ## Rules
11
+
12
+ - The default slot of `TelaInputOtp` exposes `{ slots }` — iterate it with `v-for` and bind each entry to a `TelaInputOtpSlot` via `v-bind="slot"`. Also pass `:index` (the loop index) so the active-ring animation can track movement between slots.
13
+ - `:maxlength` must match the number of rendered slots.
14
+ - For numeric codes keep the default `inputmode="numeric"`. The hidden input also sets `autocomplete="one-time-code"`, which lets iOS and Android suggest codes from SMS.
15
+ - Inside a `TelaModal`, wrap the OTP in a `w-full` container — the slots themselves keep their fixed width.
16
+
17
+ ## Examples
18
+
19
+ ### Default
20
+
21
+ <Canvas of={InputOtpStories.Default} />
22
+
23
+ ### With Separator
24
+
25
+ <Canvas of={InputOtpStories.WithSeparator} />
26
+
27
+ ### Disabled
28
+
29
+ <Canvas of={InputOtpStories.Disabled} />
30
+
31
+ ### Invalid
32
+
33
+ <Canvas of={InputOtpStories.Invalid} />
34
+
35
+ ## Basic Usage
36
+
37
+ ```vue
38
+ <script setup lang="ts">
39
+ const code = ref('')
40
+
41
+ function onComplete(value: string) {
42
+ submit(value)
43
+ }
44
+ </script>
45
+
46
+ <template>
47
+ <TelaInputOtp
48
+ v-model="code"
49
+ v-slot="{ slots }"
50
+ :maxlength="6"
51
+ inputmode="numeric"
52
+ @complete="onComplete"
53
+ >
54
+ <TelaInputOtpGroup>
55
+ <TelaInputOtpSlot
56
+ v-for="(slot, idx) in slots.slice(0, 3)"
57
+ :key="idx"
58
+ v-bind="slot"
59
+ :index="idx"
60
+ />
61
+ </TelaInputOtpGroup>
62
+ <TelaInputOtpSeparator />
63
+ <TelaInputOtpGroup>
64
+ <TelaInputOtpSlot
65
+ v-for="(slot, idx) in slots.slice(3)"
66
+ :key="idx + 3"
67
+ v-bind="slot"
68
+ :index="idx + 3"
69
+ />
70
+ </TelaInputOtpGroup>
71
+ </TelaInputOtp>
72
+ </template>
73
+ ```
74
+
75
+ ## Props
76
+
77
+ ### TelaInputOtp
78
+
79
+ <ArgTypes />
80
+
81
+ ```typescript
82
+ type InputOtpProps = {
83
+ modelValue?: string
84
+ defaultValue?: string
85
+ maxlength: number
86
+ inputmode?: 'numeric' | 'text'
87
+ textAlign?: 'left' | 'center' | 'right'
88
+ pushPasswordManagerStrategy?: 'increase-width' | 'none'
89
+ pasteTransformer?: (pasted: string | undefined) => string
90
+ autofocus?: boolean
91
+ disabled?: boolean
92
+ containerClass?: string
93
+ }
94
+ ```
95
+
96
+ ### TelaInputOtpSlot
97
+
98
+ Receives the `SlotProps` exposed by the default slot. The optional `invalid` prop turns the box red.
99
+
100
+ ```typescript
101
+ type InputOtpSlotProps = {
102
+ index: number
103
+ isActive: boolean
104
+ char: string | null
105
+ placeholderChar: string | null
106
+ hasFakeCaret: boolean
107
+ invalid?: boolean
108
+ }
109
+ ```
110
+
111
+ ## Accessibility
112
+
113
+ - A single hidden input captures keystrokes, so password managers, autofill, and screen readers work as if there were just one field.
114
+ - `autocomplete="one-time-code"` is set automatically, so iOS and Android suggest codes received via SMS.
115
+ - Arrow keys, Home, End, Backspace, and Delete are all handled by the underlying input.
116
+ - Paste distributes characters across remaining slots and advances the caret.
@@ -0,0 +1,138 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+
3
+ import InputOtp from './input-otp.vue'
4
+ import InputOtpGroup from './input-otp-group.vue'
5
+ import InputOtpSeparator from './input-otp-separator.vue'
6
+ import InputOtpSlot from './input-otp-slot.vue'
7
+
8
+ const meta: Meta<typeof InputOtp> = {
9
+ title: 'Core/InputOtp',
10
+ component: InputOtp,
11
+ parameters: {
12
+ layout: 'centered',
13
+ docs: {
14
+ description: {
15
+ component: 'One-time passcode input. Renders a row of single-character slots that share one hidden input, so paste, arrow-key navigation, backspace, and mobile OTP autofill work transparently. Built on `vue-input-otp`. Compose with TelaInputOtpGroup, TelaInputOtpSlot, and TelaInputOtpSeparator.',
16
+ },
17
+ },
18
+ },
19
+ argTypes: {
20
+ modelValue: {
21
+ control: 'text',
22
+ description: 'The current OTP value as a string (v-model).',
23
+ },
24
+ maxlength: {
25
+ control: 'number',
26
+ description: 'Number of slots in the input.',
27
+ },
28
+ inputmode: {
29
+ control: 'select',
30
+ options: ['numeric', 'text'],
31
+ description: 'Virtual keyboard appearance on mobile.',
32
+ },
33
+ textAlign: {
34
+ control: 'select',
35
+ options: ['left', 'center', 'right'],
36
+ description: 'Caret behavior when the input is partially filled.',
37
+ },
38
+ autofocus: {
39
+ control: 'boolean',
40
+ description: 'Automatically focuses the hidden input on mount.',
41
+ },
42
+ disabled: {
43
+ control: 'boolean',
44
+ description: 'Disables every slot and the hidden input.',
45
+ },
46
+ },
47
+ }
48
+
49
+ export default meta
50
+
51
+ type Story = StoryObj<typeof meta>
52
+
53
+ export const Default: Story = {
54
+ args: {
55
+ maxlength: 6,
56
+ inputmode: 'numeric',
57
+ },
58
+ render: args => ({
59
+ components: { InputOtp, InputOtpGroup, InputOtpSlot },
60
+ setup() {
61
+ return { args }
62
+ },
63
+ template: `
64
+ <InputOtp v-bind="args" v-slot="{ slots }">
65
+ <InputOtpGroup>
66
+ <InputOtpSlot v-for="(slot, idx) in slots" :key="idx" v-bind="slot" :index="idx" />
67
+ </InputOtpGroup>
68
+ </InputOtp>
69
+ `,
70
+ }),
71
+ }
72
+
73
+ export const WithSeparator: Story = {
74
+ args: {
75
+ maxlength: 6,
76
+ inputmode: 'numeric',
77
+ },
78
+ render: args => ({
79
+ components: { InputOtp, InputOtpGroup, InputOtpSlot, InputOtpSeparator },
80
+ setup() {
81
+ return { args }
82
+ },
83
+ template: `
84
+ <InputOtp v-bind="args" v-slot="{ slots }">
85
+ <InputOtpGroup>
86
+ <InputOtpSlot v-for="(slot, idx) in slots.slice(0, 3)" :key="idx" v-bind="slot" :index="idx" />
87
+ </InputOtpGroup>
88
+ <InputOtpSeparator />
89
+ <InputOtpGroup>
90
+ <InputOtpSlot v-for="(slot, idx) in slots.slice(3)" :key="idx + 3" v-bind="slot" :index="idx + 3" />
91
+ </InputOtpGroup>
92
+ </InputOtp>
93
+ `,
94
+ }),
95
+ }
96
+
97
+ export const Disabled: Story = {
98
+ args: {
99
+ maxlength: 4,
100
+ inputmode: 'numeric',
101
+ disabled: true,
102
+ modelValue: '1234',
103
+ },
104
+ render: args => ({
105
+ components: { InputOtp, InputOtpGroup, InputOtpSlot },
106
+ setup() {
107
+ return { args }
108
+ },
109
+ template: `
110
+ <InputOtp v-bind="args" v-slot="{ slots }">
111
+ <InputOtpGroup>
112
+ <InputOtpSlot v-for="(slot, idx) in slots" :key="idx" v-bind="slot" :index="idx" />
113
+ </InputOtpGroup>
114
+ </InputOtp>
115
+ `,
116
+ }),
117
+ }
118
+
119
+ export const Invalid: Story = {
120
+ args: {
121
+ maxlength: 4,
122
+ inputmode: 'numeric',
123
+ modelValue: '1234',
124
+ },
125
+ render: args => ({
126
+ components: { InputOtp, InputOtpGroup, InputOtpSlot },
127
+ setup() {
128
+ return { args }
129
+ },
130
+ template: `
131
+ <InputOtp v-bind="args" v-slot="{ slots }">
132
+ <InputOtpGroup>
133
+ <InputOtpSlot v-for="(slot, idx) in slots" :key="idx" v-bind="slot" :index="idx" invalid />
134
+ </InputOtpGroup>
135
+ </InputOtp>
136
+ `,
137
+ }),
138
+ }
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ import type { OTPInputEmits, OTPInputProps, SlotProps } from 'vue-input-otp'
3
+ import type { HTMLAttributes } from 'vue'
4
+ import { OTPInput } from 'vue-input-otp'
5
+
6
+ const props = defineProps<OTPInputProps & {
7
+ modelValue?: string
8
+ class?: HTMLAttributes['class']
9
+ }>()
10
+
11
+ const emit = defineEmits<OTPInputEmits & {
12
+ (event: 'update:modelValue', value: string | undefined): void
13
+ }>()
14
+
15
+ defineSlots<{
16
+ default: (props: { slots: SlotProps[], isFocused: boolean, isHovering: boolean }) => unknown
17
+ }>()
18
+
19
+ const containerClass = computed(() =>
20
+ cn('flex items-center gap-2 has-[:disabled]:opacity-50', props.containerClass, props.class),
21
+ )
22
+ </script>
23
+
24
+ <template>
25
+ <OTPInput
26
+ :model-value="modelValue"
27
+ :maxlength="maxlength"
28
+ :text-align="textAlign"
29
+ :inputmode="inputmode"
30
+ :push-password-manager-strategy="pushPasswordManagerStrategy"
31
+ :no-script-css-fallback="noScriptCssFallback"
32
+ :default-value="defaultValue"
33
+ :paste-transformer="pasteTransformer"
34
+ :autofocus="autofocus"
35
+ :disabled="disabled"
36
+ :name="name"
37
+ :container-class="containerClass"
38
+ @update:model-value="value => emit('update:modelValue', value)"
39
+ @complete="value => emit('complete', value)"
40
+ @change="event => emit('change', event)"
41
+ @input="value => emit('input', value)"
42
+ @focus="event => emit('focus', event)"
43
+ @blur="event => emit('blur', event)"
44
+ @paste="event => emit('paste', event)"
45
+ >
46
+ <template #default="slotProps">
47
+ <slot v-bind="slotProps" />
48
+ </template>
49
+ </OTPInput>
50
+ </template>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.41.0",
3
+ "version": "1.42.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",
@@ -53,6 +53,7 @@
53
53
  "number-flow": "0.4.1",
54
54
  "nuxt": "3.17.7",
55
55
  "pathe": "1.1.2",
56
+ "pdfjs-dist": "4.10.38",
56
57
  "radix-vue": "1.9.17",
57
58
  "reka-ui": "2.3.0",
58
59
  "resize-observer-polyfill": "1.5.1",
@@ -62,10 +63,10 @@
62
63
  "ts-morph": "22.0.0",
63
64
  "typescript": "5.8.2",
64
65
  "unocss": "66.5.12",
65
- "pdfjs-dist": "4.10.38",
66
66
  "vue": "3.5.13",
67
67
  "vue-component-meta": "3.0.8",
68
68
  "vue-docgen-api": "4.78.0",
69
+ "vue-input-otp": "0.3.2",
69
70
  "vue-router": "4.5.0"
70
71
  },
71
72
  "devDependencies": {