@leavepulse/ui 0.53.1 → 0.54.0
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/chunks/LpOtpInput-PbeNJ5In.js +131 -0
- package/dist/chunks/{LpTabs-UM0FQmPA.js → LpTabs-BMSBB78h.js} +44 -17
- package/dist/components/LpOtpInput.vue.d.ts +17 -0
- package/dist/components/LpOtpInput.vue.js +1 -1
- package/dist/components/LpTabs.vue.d.ts +10 -2
- package/dist/components/LpTabs.vue.js +1 -1
- package/dist/components/otp.d.ts +2 -0
- package/dist/components/otp.js +17 -1
- package/dist/index.js +2 -2
- package/package.json +1 -1
- package/src/components/LpOtpInput.vue +84 -28
- package/src/components/LpTabs.vue +60 -21
- package/src/components/otp.ts +29 -0
- package/dist/chunks/LpOtpInput-CziPF3Oq.js +0 -99
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { defineComponent, computed, ref, watch, openBlock, createBlock, unref, mergeProps, withCtx, createElementBlock, Fragment, renderList, createVNode, normalizeClass, createCommentVNode } from "vue";
|
|
2
|
+
import { PinInputRoot, PinInputInput } from "reka-ui";
|
|
3
|
+
import { useFieldControl } from "../composables/useField.js";
|
|
4
|
+
import { useMergedAttrs } from "../composables/useMergedClass.js";
|
|
5
|
+
import { splitCode, upperCells, joinCells, groupBreaks } from "../components/otp.js";
|
|
6
|
+
const _hoisted_1 = {
|
|
7
|
+
key: 0,
|
|
8
|
+
"aria-hidden": "true",
|
|
9
|
+
class: "h-0.5 w-2 shrink-0 rounded-full bg-muted opacity-60"
|
|
10
|
+
};
|
|
11
|
+
const __default__ = { inheritAttrs: false };
|
|
12
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
13
|
+
...__default__,
|
|
14
|
+
__name: "LpOtpInput",
|
|
15
|
+
props: {
|
|
16
|
+
modelValue: {},
|
|
17
|
+
length: { default: 6 },
|
|
18
|
+
disabled: { type: Boolean },
|
|
19
|
+
mask: { type: Boolean },
|
|
20
|
+
alphanumeric: { type: Boolean, default: false },
|
|
21
|
+
uppercase: { type: Boolean, default: void 0 },
|
|
22
|
+
group: {},
|
|
23
|
+
invalid: { type: Boolean },
|
|
24
|
+
size: { default: "md" }
|
|
25
|
+
},
|
|
26
|
+
emits: ["update:modelValue", "complete"],
|
|
27
|
+
setup(__props, { emit: __emit }) {
|
|
28
|
+
const props = __props;
|
|
29
|
+
const emit = __emit;
|
|
30
|
+
const upper = computed(() => props.alphanumeric && (props.uppercase ?? true));
|
|
31
|
+
const local = ref(splitCode(props.modelValue, props.length));
|
|
32
|
+
let emitted = props.modelValue ?? "";
|
|
33
|
+
watch(
|
|
34
|
+
() => props.modelValue ?? "",
|
|
35
|
+
(value) => {
|
|
36
|
+
if (value === emitted) return;
|
|
37
|
+
emitted = value;
|
|
38
|
+
local.value = splitCode(value, props.length);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
watch(
|
|
42
|
+
() => props.length,
|
|
43
|
+
(length) => {
|
|
44
|
+
local.value = local.value.slice(0, length);
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
const cells = computed({
|
|
48
|
+
get: () => local.value,
|
|
49
|
+
set: (arr) => {
|
|
50
|
+
local.value = upper.value ? upperCells(arr) : arr;
|
|
51
|
+
emitted = joinCells(local.value);
|
|
52
|
+
emit("update:modelValue", emitted);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const field = useFieldControl(() => ({
|
|
56
|
+
wrapped: false,
|
|
57
|
+
invalid: props.invalid,
|
|
58
|
+
disabled: props.disabled
|
|
59
|
+
}));
|
|
60
|
+
function onComplete(arr) {
|
|
61
|
+
emit("complete", joinCells(upper.value ? upperCells(arr) : arr));
|
|
62
|
+
}
|
|
63
|
+
const breaks = computed(() => groupBreaks(props.length, props.group));
|
|
64
|
+
const root = ref(null);
|
|
65
|
+
function onBeforeInput(event, index) {
|
|
66
|
+
if (event.inputType !== "deleteContentBackward") return;
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
const target = event.target;
|
|
69
|
+
const next = [...local.value];
|
|
70
|
+
let clear = index;
|
|
71
|
+
if (!target.value && index > 0) {
|
|
72
|
+
clear = index - 1;
|
|
73
|
+
root.value?.$el.querySelectorAll("input")[clear]?.focus();
|
|
74
|
+
}
|
|
75
|
+
next[clear] = props.alphanumeric ? "" : void 0;
|
|
76
|
+
cells.value = next;
|
|
77
|
+
}
|
|
78
|
+
const cellSize = {
|
|
79
|
+
sm: "max-w-9 text-base",
|
|
80
|
+
md: "max-w-11 text-lg",
|
|
81
|
+
lg: "max-w-13 text-xl"
|
|
82
|
+
};
|
|
83
|
+
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
84
|
+
"flex min-w-0 max-w-full items-center gap-1.5 sm:gap-2"
|
|
85
|
+
);
|
|
86
|
+
return (_ctx, _cache) => {
|
|
87
|
+
return openBlock(), createBlock(unref(PinInputRoot), mergeProps({
|
|
88
|
+
ref_key: "root",
|
|
89
|
+
ref: root,
|
|
90
|
+
modelValue: cells.value,
|
|
91
|
+
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => cells.value = $event),
|
|
92
|
+
type: __props.alphanumeric ? "text" : "number",
|
|
93
|
+
mask: __props.mask,
|
|
94
|
+
disabled: unref(field).disabled,
|
|
95
|
+
otp: "",
|
|
96
|
+
class: unref(rootClass)
|
|
97
|
+
}, unref(rest), { onComplete }), {
|
|
98
|
+
default: withCtx(() => [
|
|
99
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.length, (i) => {
|
|
100
|
+
return openBlock(), createElementBlock(Fragment, { key: i }, [
|
|
101
|
+
createVNode(unref(PinInputInput), {
|
|
102
|
+
index: i - 1,
|
|
103
|
+
id: i === 1 ? unref(field).id : void 0,
|
|
104
|
+
"aria-invalid": unref(field).invalid || void 0,
|
|
105
|
+
"aria-describedby": unref(field).describedBy,
|
|
106
|
+
autocapitalize: upper.value ? "characters" : "none",
|
|
107
|
+
autocorrect: "off",
|
|
108
|
+
spellcheck: false,
|
|
109
|
+
class: normalizeClass([
|
|
110
|
+
cellSize[__props.size],
|
|
111
|
+
"aspect-square h-auto min-w-0 flex-1 basis-0 p-0",
|
|
112
|
+
"rounded-control border bg-surface-soft text-center font-semibold text-ink caret-brand",
|
|
113
|
+
"outline-none transition-colors duration-[var(--duration-fast)]",
|
|
114
|
+
"focus:border-brand focus:ring-2 focus:ring-ring",
|
|
115
|
+
"disabled:cursor-not-allowed disabled:opacity-55",
|
|
116
|
+
unref(field).invalid ? "border-danger focus:border-danger focus:ring-danger-soft" : "border-line"
|
|
117
|
+
]),
|
|
118
|
+
onBeforeinput: ($event) => onBeforeInput($event, i - 1)
|
|
119
|
+
}, null, 8, ["index", "id", "aria-invalid", "aria-describedby", "autocapitalize", "class", "onBeforeinput"]),
|
|
120
|
+
breaks.value.has(i - 1) ? (openBlock(), createElementBlock("span", _hoisted_1)) : createCommentVNode("", true)
|
|
121
|
+
], 64);
|
|
122
|
+
}), 128))
|
|
123
|
+
]),
|
|
124
|
+
_: 1
|
|
125
|
+
}, 16, ["modelValue", "type", "mask", "disabled", "class"]);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
export {
|
|
130
|
+
_sfc_main as _
|
|
131
|
+
};
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import { defineComponent, ref, watch, nextTick, useId,
|
|
1
|
+
import { defineComponent, computed, ref, watch, nextTick, useId, openBlock, createBlock, unref, withCtx, createVNode, normalizeClass, createElementBlock, Fragment, renderList, createCommentVNode, createElementVNode, toDisplayString, renderSlot } from "vue";
|
|
2
2
|
import { useReducedMotion, Motion } from "motion-v";
|
|
3
3
|
import { TabsRoot, TabsList, TabsTrigger, TabsContent } from "reka-ui";
|
|
4
4
|
import { usePillTransition } from "../composables/usePillTransition.js";
|
|
5
5
|
import { _ as _sfc_main$1 } from "./LpIcon-DYtaid0K.js";
|
|
6
|
-
const _hoisted_1 = {
|
|
6
|
+
const _hoisted_1 = {
|
|
7
|
+
key: 1,
|
|
8
|
+
class: "relative z-10 inline-flex"
|
|
9
|
+
};
|
|
10
|
+
const _hoisted_2 = {
|
|
11
|
+
key: 0,
|
|
12
|
+
"aria-hidden": "true",
|
|
13
|
+
class: "absolute -right-1 -top-0.5 size-2 rounded-full bg-accent"
|
|
14
|
+
};
|
|
15
|
+
const _hoisted_3 = {
|
|
16
|
+
key: 2,
|
|
17
|
+
"aria-hidden": "true",
|
|
18
|
+
class: "relative z-10 size-1.5 self-start rounded-full bg-accent"
|
|
19
|
+
};
|
|
7
20
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
8
21
|
__name: "LpTabs",
|
|
9
22
|
props: {
|
|
@@ -12,11 +25,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
12
25
|
items: {},
|
|
13
26
|
variant: { default: "contained" },
|
|
14
27
|
accent: { type: Boolean, default: false },
|
|
15
|
-
block: { type: Boolean, default: false }
|
|
28
|
+
block: { type: Boolean, default: false },
|
|
29
|
+
label: {}
|
|
16
30
|
},
|
|
17
31
|
emits: ["update:modelValue"],
|
|
18
32
|
setup(__props, { emit: __emit }) {
|
|
19
33
|
const props = __props;
|
|
34
|
+
const bottom = computed(() => props.variant === "bottom");
|
|
20
35
|
const emit = __emit;
|
|
21
36
|
const current = ref(props.modelValue ?? props.defaultValue ?? props.items[0]?.value);
|
|
22
37
|
watch(
|
|
@@ -50,6 +65,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
50
65
|
() => reduceMotion.value ? { duration: 0 } : { type: "spring", stiffness: 520, damping: 42, mass: 0.9 }
|
|
51
66
|
);
|
|
52
67
|
function pillUnder(value, active) {
|
|
68
|
+
if (bottom.value) return active === value;
|
|
53
69
|
return hovered.value ? hovered.value === value : active === value;
|
|
54
70
|
}
|
|
55
71
|
function releaseFocusAfterPointer(event) {
|
|
@@ -65,11 +81,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
65
81
|
createVNode(unref(TabsList), {
|
|
66
82
|
ref_key: "list",
|
|
67
83
|
ref: list,
|
|
68
|
-
|
|
84
|
+
"aria-label": __props.label,
|
|
85
|
+
class: normalizeClass(bottom.value ? "grid w-full auto-cols-fr grid-flow-col border-t border-line bg-surface pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]" : [
|
|
86
|
+
"max-w-full gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
|
69
87
|
__props.block ? "flex w-full" : "inline-flex",
|
|
70
88
|
__props.variant === "contained" ? "rounded-control border border-line bg-surface-soft p-1" : "",
|
|
71
89
|
__props.variant === "underline" ? "border-b border-line" : ""
|
|
72
|
-
]
|
|
90
|
+
]),
|
|
73
91
|
onPointerleave: _cache[0] || (_cache[0] = ($event) => hovered.value = null)
|
|
74
92
|
}, {
|
|
75
93
|
default: withCtx(() => [
|
|
@@ -77,11 +95,13 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
77
95
|
return openBlock(), createBlock(unref(TabsTrigger), {
|
|
78
96
|
key: item.value,
|
|
79
97
|
value: item.value,
|
|
80
|
-
class: normalizeClass(["relative inline-flex
|
|
81
|
-
|
|
82
|
-
__props.
|
|
98
|
+
class: normalizeClass(["relative inline-flex items-center justify-center outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring", [
|
|
99
|
+
bottom.value ? "min-h-14 min-w-0 flex-col gap-0.5 px-1 py-1.5 text-[11px] font-medium focus-visible:ring-inset" : "shrink-0 gap-1.5 whitespace-nowrap text-sm",
|
|
100
|
+
__props.block && !bottom.value ? "flex-1" : "",
|
|
101
|
+
__props.variant === "underline" ? "rounded-t-md px-3 pt-1.5 pb-2" : "",
|
|
102
|
+
__props.variant === "contained" || __props.variant === "plain" ? "rounded-md px-3 py-1.5" : "",
|
|
83
103
|
__props.accent ? "data-[state=active]:text-brand" : "data-[state=active]:text-ink",
|
|
84
|
-
hovered.value === item.value ? "text-ink" : "text-muted"
|
|
104
|
+
hovered.value === item.value && !bottom.value ? "text-ink" : "text-muted"
|
|
85
105
|
]]),
|
|
86
106
|
onPointerenter: ($event) => hovered.value = item.value,
|
|
87
107
|
onPointerup: releaseFocusAfterPointer
|
|
@@ -94,25 +114,32 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
94
114
|
class: normalizeClass(__props.variant === "underline" ? [
|
|
95
115
|
"absolute inset-x-0 -bottom-px z-0 h-0.5 rounded-full",
|
|
96
116
|
__props.accent ? "bg-brand" : "bg-ink"
|
|
117
|
+
] : bottom.value ? [
|
|
118
|
+
"absolute inset-x-[22%] top-0 z-0 h-0.5 rounded-b-full",
|
|
119
|
+
__props.accent ? "bg-brand" : "bg-ink"
|
|
97
120
|
] : [
|
|
98
121
|
"absolute inset-0 z-0 rounded-md shadow-sm",
|
|
99
122
|
__props.accent ? "border border-brand/35 bg-brand/12" : "border border-line bg-surface-raised"
|
|
100
123
|
])
|
|
101
124
|
}, null, 8, ["transition", "class"])) : createCommentVNode("", true),
|
|
102
|
-
item.icon ? (openBlock(),
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
item.icon ? (openBlock(), createElementBlock("span", _hoisted_1, [
|
|
126
|
+
createVNode(_sfc_main$1, {
|
|
127
|
+
name: item.icon,
|
|
128
|
+
size: bottom.value ? 22 : 14
|
|
129
|
+
}, null, 8, ["name", "size"]),
|
|
130
|
+
item.dot ? (openBlock(), createElementBlock("span", _hoisted_2)) : createCommentVNode("", true)
|
|
131
|
+
])) : createCommentVNode("", true),
|
|
132
|
+
createElementVNode("span", {
|
|
133
|
+
class: normalizeClass(["relative z-10", bottom.value ? "max-w-full truncate" : ""])
|
|
134
|
+
}, toDisplayString(item.label), 3),
|
|
135
|
+
item.dot && !item.icon ? (openBlock(), createElementBlock("span", _hoisted_3)) : createCommentVNode("", true)
|
|
109
136
|
]),
|
|
110
137
|
_: 2
|
|
111
138
|
}, 1032, ["value", "class", "onPointerenter"]);
|
|
112
139
|
}), 128))
|
|
113
140
|
]),
|
|
114
141
|
_: 1
|
|
115
|
-
}, 8, ["class"]),
|
|
142
|
+
}, 8, ["aria-label", "class"]),
|
|
116
143
|
_ctx.$slots.panel ? (openBlock(), createBlock(unref(Motion), {
|
|
117
144
|
key: 0,
|
|
118
145
|
layout: true,
|
|
@@ -10,6 +10,14 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
10
10
|
mask?: boolean;
|
|
11
11
|
/** Allow letters too (backup codes). Default: digits only (TOTP). */
|
|
12
12
|
alphanumeric?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Turn typed letters into capitals, and open a phone's keyboard in capitals.
|
|
15
|
+
* Default: on with `alphanumeric`, where codes are printed in capitals and
|
|
16
|
+
* compared without regard to case; pass `false` for a case-sensitive code.
|
|
17
|
+
*/
|
|
18
|
+
uppercase?: boolean;
|
|
19
|
+
/** Draw a separator after every `group` cells (e.g. 3 for "123-456"). */
|
|
20
|
+
group?: number;
|
|
13
21
|
/** Error state — tints the cells red (e.g. wrong code). */
|
|
14
22
|
invalid?: boolean;
|
|
15
23
|
size?: "sm" | "md" | "lg";
|
|
@@ -26,6 +34,14 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
26
34
|
mask?: boolean;
|
|
27
35
|
/** Allow letters too (backup codes). Default: digits only (TOTP). */
|
|
28
36
|
alphanumeric?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Turn typed letters into capitals, and open a phone's keyboard in capitals.
|
|
39
|
+
* Default: on with `alphanumeric`, where codes are printed in capitals and
|
|
40
|
+
* compared without regard to case; pass `false` for a case-sensitive code.
|
|
41
|
+
*/
|
|
42
|
+
uppercase?: boolean;
|
|
43
|
+
/** Draw a separator after every `group` cells (e.g. 3 for "123-456"). */
|
|
44
|
+
group?: number;
|
|
29
45
|
/** Error state — tints the cells red (e.g. wrong code). */
|
|
30
46
|
invalid?: boolean;
|
|
31
47
|
size?: "sm" | "md" | "lg";
|
|
@@ -35,5 +51,6 @@ declare const __VLS_export: import("vue").DefineComponent<{
|
|
|
35
51
|
}>, {
|
|
36
52
|
size: "sm" | "md" | "lg";
|
|
37
53
|
length: number;
|
|
54
|
+
uppercase: boolean;
|
|
38
55
|
alphanumeric: boolean;
|
|
39
56
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
@@ -3,6 +3,8 @@ export interface TabItem {
|
|
|
3
3
|
label: string;
|
|
4
4
|
/** Optional iconify name (e.g. "lucide:palette"), shown left of the label. */
|
|
5
5
|
icon?: string;
|
|
6
|
+
/** A small dot on the tab: something behind it wants a look (unread, a problem). */
|
|
7
|
+
dot?: boolean;
|
|
6
8
|
}
|
|
7
9
|
type __VLS_Props = {
|
|
8
10
|
modelValue?: string;
|
|
@@ -15,12 +17,18 @@ type __VLS_Props = {
|
|
|
15
17
|
* sliding pill marking the active one (for nav bars over a custom surface).
|
|
16
18
|
* "underline" is a flat row over a hairline baseline, marking the active
|
|
17
19
|
* tab with a sliding bottom bar instead of a pill (page-section nav style).
|
|
20
|
+
* "bottom" is a phone's bottom navigation: full width, equal-share tabs
|
|
21
|
+
* with the icon over the label, a sliding bar along the top edge marking
|
|
22
|
+
* the active one, and padding for the gesture strip and side insets of an
|
|
23
|
+
* edge-to-edge screen. Place it as the last row of the layout.
|
|
18
24
|
*/
|
|
19
|
-
variant?: "contained" | "plain" | "underline";
|
|
25
|
+
variant?: "contained" | "plain" | "underline" | "bottom";
|
|
20
26
|
/** Tint the active pill + label with the brand colour (nav-bar style). */
|
|
21
27
|
accent?: boolean;
|
|
22
28
|
/** Stretch the bar to full width with equal-share triggers. */
|
|
23
29
|
block?: boolean;
|
|
30
|
+
/** Accessible name of the tab list, e.g. "Main" for an app's navigation. */
|
|
31
|
+
label?: string;
|
|
24
32
|
};
|
|
25
33
|
declare var __VLS_51: {
|
|
26
34
|
value: string;
|
|
@@ -36,7 +44,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
|
|
|
36
44
|
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
37
45
|
}>, {
|
|
38
46
|
block: boolean;
|
|
39
|
-
variant: "contained" | "plain" | "underline";
|
|
47
|
+
variant: "contained" | "plain" | "underline" | "bottom";
|
|
40
48
|
accent: boolean;
|
|
41
49
|
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
42
50
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
package/dist/components/otp.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export type OtpCells = (string | number | undefined)[];
|
|
|
3
3
|
export declare function joinCells(cells: OtpCells): string;
|
|
4
4
|
/** Cells for a code given from outside, one character each. */
|
|
5
5
|
export declare function splitCode(code: string | undefined, length: number): string[];
|
|
6
|
+
export declare function upperCells<T extends OtpCells>(cells: T): T;
|
|
7
|
+
export declare function groupBreaks(length: number, group: number | undefined): Set<number>;
|
package/dist/components/otp.js
CHANGED
|
@@ -4,7 +4,23 @@ function joinCells(cells) {
|
|
|
4
4
|
function splitCode(code, length) {
|
|
5
5
|
return (code ?? "").split("").slice(0, length);
|
|
6
6
|
}
|
|
7
|
+
function upperCells(cells) {
|
|
8
|
+
const out = [];
|
|
9
|
+
cells.forEach((c, i) => {
|
|
10
|
+
out[i] = typeof c === "string" ? c.toUpperCase() : c;
|
|
11
|
+
});
|
|
12
|
+
out.length = cells.length;
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
function groupBreaks(length, group) {
|
|
16
|
+
const breaks = /* @__PURE__ */ new Set();
|
|
17
|
+
if (!group || group < 1 || group >= length) return breaks;
|
|
18
|
+
for (let i = group; i < length; i += group) breaks.add(i - 1);
|
|
19
|
+
return breaks;
|
|
20
|
+
}
|
|
7
21
|
export {
|
|
22
|
+
groupBreaks,
|
|
8
23
|
joinCells,
|
|
9
|
-
splitCode
|
|
24
|
+
splitCode,
|
|
25
|
+
upperCells
|
|
10
26
|
};
|
package/dist/index.js
CHANGED
|
@@ -65,7 +65,7 @@ import { _ as _41 } from "./chunks/LpNotificationBell-wZ3meeim.js";
|
|
|
65
65
|
import { _ as _42 } from "./chunks/LpNumberField-B77O-z28.js";
|
|
66
66
|
import { _ as _43 } from "./chunks/LpTimeField-Cqe7fXnh.js";
|
|
67
67
|
import { _ as _44 } from "./chunks/LpNumberFlow-z4WPZcZu.js";
|
|
68
|
-
import { _ as _45 } from "./chunks/LpOtpInput-
|
|
68
|
+
import { _ as _45 } from "./chunks/LpOtpInput-PbeNJ5In.js";
|
|
69
69
|
import { _ as _46 } from "./chunks/LpPagination-BcDlo3r_.js";
|
|
70
70
|
import { _ as _47 } from "./chunks/LpPasswordInput-eKzQhbQP.js";
|
|
71
71
|
import { _ as _48 } from "./chunks/LpPie-B7ti5qCH.js";
|
|
@@ -96,7 +96,7 @@ import { _ as _69 } from "./chunks/LpSwitch-DG2AhXIL.js";
|
|
|
96
96
|
import { default as default12 } from "./components/LpTable.vue.js";
|
|
97
97
|
import { compareValues } from "./components/tableSort.js";
|
|
98
98
|
import { _ as _70 } from "./chunks/LpTableOfContents-BUcAfGPQ.js";
|
|
99
|
-
import { _ as _71 } from "./chunks/LpTabs-
|
|
99
|
+
import { _ as _71 } from "./chunks/LpTabs-BMSBB78h.js";
|
|
100
100
|
import { _ as _72 } from "./chunks/LpTagsInput-D1SuqbhB.js";
|
|
101
101
|
import { addTags, splitTags } from "./components/tags.js";
|
|
102
102
|
import { _ as _73 } from "./chunks/LpTextarea-uuScWo6-.js";
|
package/package.json
CHANGED
|
@@ -14,12 +14,16 @@ export default { inheritAttrs: false }
|
|
|
14
14
|
* The kit-facing model is a single STRING (the joined code), not reka's per-cell
|
|
15
15
|
* array: `v-model` is "123456", and @complete fires that string once every cell
|
|
16
16
|
* is filled. Digits-only by default (TOTP); flip `alphanumeric` for backup codes.
|
|
17
|
+
*
|
|
18
|
+
* The cells share the row rather than keeping a fixed size: each grows to its
|
|
19
|
+
* size's width and no further, and shrinks with the row when the container is
|
|
20
|
+
* narrower, so eight cells fit a phone instead of running off its edge.
|
|
17
21
|
*/
|
|
18
22
|
import { PinInputInput, PinInputRoot } from "reka-ui"
|
|
19
23
|
import { computed, ref, watch } from "vue"
|
|
20
24
|
import { useFieldControl } from "../composables/useField"
|
|
21
25
|
import { useMergedAttrs } from "../composables/useMergedClass"
|
|
22
|
-
import { joinCells, splitCode, type OtpCells } from "./otp"
|
|
26
|
+
import { groupBreaks, joinCells, splitCode, upperCells, type OtpCells } from "./otp"
|
|
23
27
|
|
|
24
28
|
const props = withDefaults(
|
|
25
29
|
defineProps<{
|
|
@@ -32,11 +36,19 @@ const props = withDefaults(
|
|
|
32
36
|
mask?: boolean
|
|
33
37
|
/** Allow letters too (backup codes). Default: digits only (TOTP). */
|
|
34
38
|
alphanumeric?: boolean
|
|
39
|
+
/**
|
|
40
|
+
* Turn typed letters into capitals, and open a phone's keyboard in capitals.
|
|
41
|
+
* Default: on with `alphanumeric`, where codes are printed in capitals and
|
|
42
|
+
* compared without regard to case; pass `false` for a case-sensitive code.
|
|
43
|
+
*/
|
|
44
|
+
uppercase?: boolean
|
|
45
|
+
/** Draw a separator after every `group` cells (e.g. 3 for "123-456"). */
|
|
46
|
+
group?: number
|
|
35
47
|
/** Error state — tints the cells red (e.g. wrong code). */
|
|
36
48
|
invalid?: boolean
|
|
37
49
|
size?: "sm" | "md" | "lg"
|
|
38
50
|
}>(),
|
|
39
|
-
{ length: 6, size: "md", alphanumeric: false },
|
|
51
|
+
{ length: 6, size: "md", alphanumeric: false, uppercase: undefined },
|
|
40
52
|
)
|
|
41
53
|
|
|
42
54
|
const emit = defineEmits<{
|
|
@@ -45,6 +57,8 @@ const emit = defineEmits<{
|
|
|
45
57
|
(e: "complete", value: string): void
|
|
46
58
|
}>()
|
|
47
59
|
|
|
60
|
+
const upper = computed(() => props.alphanumeric && (props.uppercase ?? true))
|
|
61
|
+
|
|
48
62
|
// reka models the value as an array of per-cell strings; we bridge to a single
|
|
49
63
|
// string at the boundary so callers never touch the array shape.
|
|
50
64
|
//
|
|
@@ -52,7 +66,7 @@ const emit = defineEmits<{
|
|
|
52
66
|
// string on every render: clearing a middle cell leaves a gap the string cannot
|
|
53
67
|
// express, and re-splitting it shifted every later digit one cell left. The
|
|
54
68
|
// cells are only rebuilt when the model changes from outside.
|
|
55
|
-
const local = ref<
|
|
69
|
+
const local = ref<OtpCells>(splitCode(props.modelValue, props.length))
|
|
56
70
|
let emitted = props.modelValue ?? ""
|
|
57
71
|
watch(
|
|
58
72
|
() => props.modelValue ?? "",
|
|
@@ -68,11 +82,13 @@ watch(
|
|
|
68
82
|
local.value = local.value.slice(0, length)
|
|
69
83
|
},
|
|
70
84
|
)
|
|
85
|
+
// Typed as reka's text-mode array; numeric mode holds numbers and holes, which
|
|
86
|
+
// the helpers in ./otp are written for.
|
|
71
87
|
const cells = computed<string[]>({
|
|
72
|
-
get: () => local.value,
|
|
88
|
+
get: () => local.value as string[],
|
|
73
89
|
set: (arr) => {
|
|
74
|
-
local.value = arr
|
|
75
|
-
emitted = joinCells(
|
|
90
|
+
local.value = upper.value ? upperCells(arr) : arr
|
|
91
|
+
emitted = joinCells(local.value)
|
|
76
92
|
emit("update:modelValue", emitted)
|
|
77
93
|
},
|
|
78
94
|
})
|
|
@@ -84,23 +100,53 @@ const field = useFieldControl(() => ({
|
|
|
84
100
|
}))
|
|
85
101
|
|
|
86
102
|
function onComplete(arr: OtpCells) {
|
|
87
|
-
emit("complete", joinCells(arr))
|
|
103
|
+
emit("complete", joinCells(upper.value ? upperCells(arr) : arr))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const breaks = computed(() => groupBreaks(props.length, props.group))
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* Backspace, as a phone's keyboard sends it.
|
|
110
|
+
*
|
|
111
|
+
* reka handles Backspace on keydown, but an on-screen keyboard reports most
|
|
112
|
+
* keys as "Unidentified", so its handler never ran there: the browser deleted
|
|
113
|
+
* the character itself, and reka read the now-empty cell as typed input and
|
|
114
|
+
* moved the caret FORWARD — and an empty cell could not be backed out of at
|
|
115
|
+
* all. `beforeinput` names the deletion on every keyboard, so it is done here
|
|
116
|
+
* the way a hardware Backspace does it: clear this cell, or step back into
|
|
117
|
+
* the previous one and clear that. A hardware key never reaches this, since
|
|
118
|
+
* reka's keydown handler cancels it first.
|
|
119
|
+
*/
|
|
120
|
+
const root = ref<{ $el: HTMLElement } | null>(null)
|
|
121
|
+
function onBeforeInput(event: InputEvent, index: number) {
|
|
122
|
+
if (event.inputType !== "deleteContentBackward") return
|
|
123
|
+
event.preventDefault()
|
|
124
|
+
const target = event.target as HTMLInputElement
|
|
125
|
+
const next = [...local.value]
|
|
126
|
+
let clear = index
|
|
127
|
+
if (!target.value && index > 0) {
|
|
128
|
+
clear = index - 1
|
|
129
|
+
root.value?.$el.querySelectorAll("input")[clear]?.focus()
|
|
130
|
+
}
|
|
131
|
+
// Emptied the way reka empties a cell itself: numeric mode leaves a hole.
|
|
132
|
+
next[clear] = props.alphanumeric ? "" : undefined
|
|
133
|
+
cells.value = next as string[]
|
|
88
134
|
}
|
|
89
135
|
|
|
90
136
|
const cellSize = {
|
|
91
|
-
sm: "
|
|
92
|
-
md: "
|
|
93
|
-
lg: "
|
|
137
|
+
sm: "max-w-9 text-base",
|
|
138
|
+
md: "max-w-11 text-lg",
|
|
139
|
+
lg: "max-w-13 text-xl",
|
|
94
140
|
}
|
|
95
141
|
|
|
96
|
-
// The boxes sit on a fixed gap that a narrow form has to tighten.
|
|
97
142
|
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
98
|
-
"flex items-center gap-2",
|
|
143
|
+
"flex min-w-0 max-w-full items-center gap-1.5 sm:gap-2",
|
|
99
144
|
)
|
|
100
145
|
</script>
|
|
101
146
|
|
|
102
147
|
<template>
|
|
103
148
|
<PinInputRoot
|
|
149
|
+
ref="root"
|
|
104
150
|
v-model="cells"
|
|
105
151
|
:type="alphanumeric ? 'text' : 'number'"
|
|
106
152
|
:mask="mask"
|
|
@@ -110,21 +156,31 @@ const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
|
110
156
|
v-bind="rest"
|
|
111
157
|
@complete="onComplete"
|
|
112
158
|
>
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
<template v-for="i in length" :key="i">
|
|
160
|
+
<PinInputInput
|
|
161
|
+
:index="i - 1"
|
|
162
|
+
:id="i === 1 ? field.id : undefined"
|
|
163
|
+
:aria-invalid="field.invalid || undefined"
|
|
164
|
+
:aria-describedby="field.describedBy"
|
|
165
|
+
:autocapitalize="upper ? 'characters' : 'none'"
|
|
166
|
+
autocorrect="off"
|
|
167
|
+
:spellcheck="false"
|
|
168
|
+
:class="[
|
|
169
|
+
cellSize[size],
|
|
170
|
+
'aspect-square h-auto min-w-0 flex-1 basis-0 p-0',
|
|
171
|
+
'rounded-control border bg-surface-soft text-center font-semibold text-ink caret-brand',
|
|
172
|
+
'outline-none transition-colors duration-[var(--duration-fast)]',
|
|
173
|
+
'focus:border-brand focus:ring-2 focus:ring-ring',
|
|
174
|
+
'disabled:cursor-not-allowed disabled:opacity-55',
|
|
175
|
+
field.invalid ? 'border-danger focus:border-danger focus:ring-danger-soft' : 'border-line',
|
|
176
|
+
]"
|
|
177
|
+
@beforeinput="onBeforeInput($event as InputEvent, i - 1)"
|
|
178
|
+
/>
|
|
179
|
+
<span
|
|
180
|
+
v-if="breaks.has(i - 1)"
|
|
181
|
+
aria-hidden="true"
|
|
182
|
+
class="h-0.5 w-2 shrink-0 rounded-full bg-muted opacity-60"
|
|
183
|
+
/>
|
|
184
|
+
</template>
|
|
129
185
|
</PinInputRoot>
|
|
130
186
|
</template>
|
|
@@ -10,6 +10,8 @@ export interface TabItem {
|
|
|
10
10
|
label: string
|
|
11
11
|
/** Optional iconify name (e.g. "lucide:palette"), shown left of the label. */
|
|
12
12
|
icon?: string
|
|
13
|
+
/** A small dot on the tab: something behind it wants a look (unread, a problem). */
|
|
14
|
+
dot?: boolean
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
const props = withDefaults(
|
|
@@ -24,15 +26,23 @@ const props = withDefaults(
|
|
|
24
26
|
* sliding pill marking the active one (for nav bars over a custom surface).
|
|
25
27
|
* "underline" is a flat row over a hairline baseline, marking the active
|
|
26
28
|
* tab with a sliding bottom bar instead of a pill (page-section nav style).
|
|
29
|
+
* "bottom" is a phone's bottom navigation: full width, equal-share tabs
|
|
30
|
+
* with the icon over the label, a sliding bar along the top edge marking
|
|
31
|
+
* the active one, and padding for the gesture strip and side insets of an
|
|
32
|
+
* edge-to-edge screen. Place it as the last row of the layout.
|
|
27
33
|
*/
|
|
28
|
-
variant?: "contained" | "plain" | "underline"
|
|
34
|
+
variant?: "contained" | "plain" | "underline" | "bottom"
|
|
29
35
|
/** Tint the active pill + label with the brand colour (nav-bar style). */
|
|
30
36
|
accent?: boolean
|
|
31
37
|
/** Stretch the bar to full width with equal-share triggers. */
|
|
32
38
|
block?: boolean
|
|
39
|
+
/** Accessible name of the tab list, e.g. "Main" for an app's navigation. */
|
|
40
|
+
label?: string
|
|
33
41
|
}>(),
|
|
34
42
|
{ variant: "contained", accent: false, block: false },
|
|
35
43
|
)
|
|
44
|
+
|
|
45
|
+
const bottom = computed(() => props.variant === "bottom")
|
|
36
46
|
const emit = defineEmits<{ (e: "update:modelValue", value: string): void }>()
|
|
37
47
|
|
|
38
48
|
// Held here rather than left to reka, so the active marker follows the active
|
|
@@ -88,7 +98,11 @@ const panelLayoutTransition = computed(() =>
|
|
|
88
98
|
: { type: "spring" as const, stiffness: 520, damping: 42, mass: 0.9 },
|
|
89
99
|
)
|
|
90
100
|
|
|
101
|
+
// A bottom bar marks only the active tab: on a phone a tap is the hover, and
|
|
102
|
+
// a bar that chased the mouse across a desktop window would point at a tab
|
|
103
|
+
// nobody chose.
|
|
91
104
|
function pillUnder(value: string, active?: string): boolean {
|
|
105
|
+
if (bottom.value) return active === value
|
|
92
106
|
return hovered.value ? hovered.value === value : active === value
|
|
93
107
|
}
|
|
94
108
|
|
|
@@ -119,30 +133,38 @@ function releaseFocusAfterPointer(event: PointerEvent) {
|
|
|
119
133
|
>
|
|
120
134
|
<TabsList
|
|
121
135
|
ref="list"
|
|
122
|
-
|
|
123
|
-
:class="
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
:aria-label="label"
|
|
137
|
+
:class="bottom
|
|
138
|
+
? 'grid w-full auto-cols-fr grid-flow-col border-t border-line bg-surface pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]'
|
|
139
|
+
: [
|
|
140
|
+
'max-w-full gap-1 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden',
|
|
141
|
+
block ? 'flex w-full' : 'inline-flex',
|
|
142
|
+
variant === 'contained' ? 'rounded-control border border-line bg-surface-soft p-1' : '',
|
|
143
|
+
variant === 'underline' ? 'border-b border-line' : '',
|
|
144
|
+
]"
|
|
128
145
|
@pointerleave="hovered = null"
|
|
129
146
|
>
|
|
130
147
|
<TabsTrigger
|
|
131
148
|
v-for="item in items"
|
|
132
149
|
:key="item.value"
|
|
133
150
|
:value="item.value"
|
|
134
|
-
class="relative inline-flex
|
|
151
|
+
class="relative inline-flex items-center justify-center outline-none transition-colors duration-[var(--duration-fast)] focus-visible:ring-2 focus-visible:ring-ring"
|
|
135
152
|
:class="[
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
bottom
|
|
154
|
+
? 'min-h-14 min-w-0 flex-col gap-0.5 px-1 py-1.5 text-[11px] font-medium focus-visible:ring-inset'
|
|
155
|
+
: 'shrink-0 gap-1.5 whitespace-nowrap text-sm',
|
|
156
|
+
block && !bottom ? 'flex-1' : '',
|
|
157
|
+
variant === 'underline' ? 'rounded-t-md px-3 pt-1.5 pb-2' : '',
|
|
158
|
+
variant === 'contained' || variant === 'plain' ? 'rounded-md px-3 py-1.5' : '',
|
|
138
159
|
accent ? 'data-[state=active]:text-brand' : 'data-[state=active]:text-ink',
|
|
139
|
-
hovered === item.value ? 'text-ink' : 'text-muted',
|
|
160
|
+
hovered === item.value && !bottom ? 'text-ink' : 'text-muted',
|
|
140
161
|
]"
|
|
141
162
|
@pointerenter="hovered = item.value"
|
|
142
163
|
@pointerup="releaseFocusAfterPointer"
|
|
143
164
|
>
|
|
144
|
-
<!-- Active marker: a
|
|
145
|
-
|
|
165
|
+
<!-- Active marker: a bar under the tab in "underline", over it in
|
|
166
|
+
"bottom", a filled pill otherwise. All slide between tabs via the
|
|
167
|
+
shared motion layoutId. -->
|
|
146
168
|
<Motion
|
|
147
169
|
v-if="pillUnder(item.value, current)"
|
|
148
170
|
:layout-id="pillId"
|
|
@@ -152,15 +174,32 @@ function releaseFocusAfterPointer(event: PointerEvent) {
|
|
|
152
174
|
'absolute inset-x-0 -bottom-px z-0 h-0.5 rounded-full',
|
|
153
175
|
accent ? 'bg-brand' : 'bg-ink',
|
|
154
176
|
]
|
|
155
|
-
:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
? '
|
|
159
|
-
|
|
160
|
-
|
|
177
|
+
: bottom
|
|
178
|
+
? [
|
|
179
|
+
'absolute inset-x-[22%] top-0 z-0 h-0.5 rounded-b-full',
|
|
180
|
+
accent ? 'bg-brand' : 'bg-ink',
|
|
181
|
+
]
|
|
182
|
+
: [
|
|
183
|
+
'absolute inset-0 z-0 rounded-md shadow-sm',
|
|
184
|
+
accent
|
|
185
|
+
? 'border border-brand/35 bg-brand/12'
|
|
186
|
+
: 'border border-line bg-surface-raised',
|
|
187
|
+
]"
|
|
188
|
+
/>
|
|
189
|
+
<span v-if="item.icon" class="relative z-10 inline-flex">
|
|
190
|
+
<LpIcon :name="item.icon" :size="bottom ? 22 : 14" />
|
|
191
|
+
<span
|
|
192
|
+
v-if="item.dot"
|
|
193
|
+
aria-hidden="true"
|
|
194
|
+
class="absolute -right-1 -top-0.5 size-2 rounded-full bg-accent"
|
|
195
|
+
/>
|
|
196
|
+
</span>
|
|
197
|
+
<span class="relative z-10" :class="bottom ? 'max-w-full truncate' : ''">{{ item.label }}</span>
|
|
198
|
+
<span
|
|
199
|
+
v-if="item.dot && !item.icon"
|
|
200
|
+
aria-hidden="true"
|
|
201
|
+
class="relative z-10 size-1.5 self-start rounded-full bg-accent"
|
|
161
202
|
/>
|
|
162
|
-
<LpIcon v-if="item.icon" :name="item.icon" :size="14" class="relative z-10" />
|
|
163
|
-
<span class="relative z-10">{{ item.label }}</span>
|
|
164
203
|
</TabsTrigger>
|
|
165
204
|
</TabsList>
|
|
166
205
|
|
package/src/components/otp.ts
CHANGED
|
@@ -14,3 +14,32 @@ export function joinCells(cells: OtpCells): string {
|
|
|
14
14
|
export function splitCode(code: string | undefined, length: number): string[] {
|
|
15
15
|
return (code ?? "").split("").slice(0, length)
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* Upper-case every filled cell, keeping the holes where they are.
|
|
20
|
+
*
|
|
21
|
+
* A phone opens its keyboard in lower case while backup and pairing codes are
|
|
22
|
+
* printed in capitals, so a code typed letter by letter came out "ab12cd"
|
|
23
|
+
* against an "AB12CD" on screen: a difference the reader cannot fix by looking,
|
|
24
|
+
* and one a server comparing exactly rejects.
|
|
25
|
+
*/
|
|
26
|
+
export function upperCells<T extends OtpCells>(cells: T): T {
|
|
27
|
+
const out = [] as unknown as T
|
|
28
|
+
cells.forEach((c, i) => {
|
|
29
|
+
out[i] = typeof c === "string" ? c.toUpperCase() : c
|
|
30
|
+
})
|
|
31
|
+
out.length = cells.length
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* The cell indexes a group separator follows: after every `group` cells, never
|
|
37
|
+
* after the last. Eight characters are read and copied as two blocks of four,
|
|
38
|
+
* which is how such codes are printed, far more easily than as one row.
|
|
39
|
+
*/
|
|
40
|
+
export function groupBreaks(length: number, group: number | undefined): Set<number> {
|
|
41
|
+
const breaks = new Set<number>()
|
|
42
|
+
if (!group || group < 1 || group >= length) return breaks
|
|
43
|
+
for (let i = group; i < length; i += group) breaks.add(i - 1)
|
|
44
|
+
return breaks
|
|
45
|
+
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { defineComponent, ref, watch, computed, openBlock, createBlock, unref, mergeProps, withCtx, createElementBlock, Fragment, renderList, normalizeClass } from "vue";
|
|
2
|
-
import { PinInputRoot, PinInputInput } from "reka-ui";
|
|
3
|
-
import { useFieldControl } from "../composables/useField.js";
|
|
4
|
-
import { useMergedAttrs } from "../composables/useMergedClass.js";
|
|
5
|
-
import { splitCode, joinCells } from "../components/otp.js";
|
|
6
|
-
const __default__ = { inheritAttrs: false };
|
|
7
|
-
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
8
|
-
...__default__,
|
|
9
|
-
__name: "LpOtpInput",
|
|
10
|
-
props: {
|
|
11
|
-
modelValue: {},
|
|
12
|
-
length: { default: 6 },
|
|
13
|
-
disabled: { type: Boolean },
|
|
14
|
-
mask: { type: Boolean },
|
|
15
|
-
alphanumeric: { type: Boolean, default: false },
|
|
16
|
-
invalid: { type: Boolean },
|
|
17
|
-
size: { default: "md" }
|
|
18
|
-
},
|
|
19
|
-
emits: ["update:modelValue", "complete"],
|
|
20
|
-
setup(__props, { emit: __emit }) {
|
|
21
|
-
const props = __props;
|
|
22
|
-
const emit = __emit;
|
|
23
|
-
const local = ref(splitCode(props.modelValue, props.length));
|
|
24
|
-
let emitted = props.modelValue ?? "";
|
|
25
|
-
watch(
|
|
26
|
-
() => props.modelValue ?? "",
|
|
27
|
-
(value) => {
|
|
28
|
-
if (value === emitted) return;
|
|
29
|
-
emitted = value;
|
|
30
|
-
local.value = splitCode(value, props.length);
|
|
31
|
-
}
|
|
32
|
-
);
|
|
33
|
-
watch(
|
|
34
|
-
() => props.length,
|
|
35
|
-
(length) => {
|
|
36
|
-
local.value = local.value.slice(0, length);
|
|
37
|
-
}
|
|
38
|
-
);
|
|
39
|
-
const cells = computed({
|
|
40
|
-
get: () => local.value,
|
|
41
|
-
set: (arr) => {
|
|
42
|
-
local.value = arr;
|
|
43
|
-
emitted = joinCells(arr);
|
|
44
|
-
emit("update:modelValue", emitted);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
const field = useFieldControl(() => ({
|
|
48
|
-
wrapped: false,
|
|
49
|
-
invalid: props.invalid,
|
|
50
|
-
disabled: props.disabled
|
|
51
|
-
}));
|
|
52
|
-
function onComplete(arr) {
|
|
53
|
-
emit("complete", joinCells(arr));
|
|
54
|
-
}
|
|
55
|
-
const cellSize = {
|
|
56
|
-
sm: "size-9 text-base",
|
|
57
|
-
md: "size-11 text-lg",
|
|
58
|
-
lg: "size-13 text-xl"
|
|
59
|
-
};
|
|
60
|
-
const { class: rootClass, attrs: rest } = useMergedAttrs(
|
|
61
|
-
"flex items-center gap-2"
|
|
62
|
-
);
|
|
63
|
-
return (_ctx, _cache) => {
|
|
64
|
-
return openBlock(), createBlock(unref(PinInputRoot), mergeProps({
|
|
65
|
-
modelValue: cells.value,
|
|
66
|
-
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => cells.value = $event),
|
|
67
|
-
type: __props.alphanumeric ? "text" : "number",
|
|
68
|
-
mask: __props.mask,
|
|
69
|
-
disabled: unref(field).disabled,
|
|
70
|
-
otp: "",
|
|
71
|
-
class: unref(rootClass)
|
|
72
|
-
}, unref(rest), { onComplete }), {
|
|
73
|
-
default: withCtx(() => [
|
|
74
|
-
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.length, (i) => {
|
|
75
|
-
return openBlock(), createBlock(unref(PinInputInput), {
|
|
76
|
-
key: i,
|
|
77
|
-
index: i - 1,
|
|
78
|
-
id: i === 1 ? unref(field).id : void 0,
|
|
79
|
-
"aria-invalid": unref(field).invalid || void 0,
|
|
80
|
-
"aria-describedby": unref(field).describedBy,
|
|
81
|
-
class: normalizeClass([
|
|
82
|
-
cellSize[__props.size],
|
|
83
|
-
"rounded-control border bg-surface-soft text-center font-semibold text-ink caret-brand",
|
|
84
|
-
"outline-none transition-colors duration-[var(--duration-fast)]",
|
|
85
|
-
"focus:border-brand focus:ring-2 focus:ring-ring",
|
|
86
|
-
"disabled:cursor-not-allowed disabled:opacity-55",
|
|
87
|
-
unref(field).invalid ? "border-danger focus:border-danger focus:ring-danger-soft" : "border-line"
|
|
88
|
-
])
|
|
89
|
-
}, null, 8, ["index", "id", "aria-invalid", "aria-describedby", "class"]);
|
|
90
|
-
}), 128))
|
|
91
|
-
]),
|
|
92
|
-
_: 1
|
|
93
|
-
}, 16, ["modelValue", "type", "mask", "disabled", "class"]);
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
export {
|
|
98
|
-
_sfc_main as _
|
|
99
|
-
};
|