orio-ui 1.28.1 → 1.29.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/README.md +4 -4
- package/dist/agents/ROUTING.md +1 -0
- package/dist/agents/component-finder.md +1 -0
- package/dist/agents/component-worker.md +1 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/Input.USAGE.md +4 -0
- package/dist/runtime/components/Input.d.vue.ts +4 -1
- package/dist/runtime/components/Input.vue +6 -0
- package/dist/runtime/components/Input.vue.d.ts +4 -1
- package/dist/runtime/components/NumberInput/USAGE.md +4 -0
- package/dist/runtime/components/NumberInput/index.d.vue.ts +7 -4
- package/dist/runtime/components/NumberInput/index.vue +12 -7
- package/dist/runtime/components/NumberInput/index.vue.d.ts +7 -4
- package/dist/runtime/components/SegmentedInput.USAGE.md +67 -0
- package/dist/runtime/components/SegmentedInput.d.vue.ts +22 -0
- package/dist/runtime/components/SegmentedInput.vue +114 -0
- package/dist/runtime/components/SegmentedInput.vue.d.ts +22 -0
- package/dist/runtime/composables/useUrlSync.d.ts +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,12 +18,12 @@ orio-ui's shipped, version-pinned agent docs. Details in
|
|
|
18
18
|
|
|
19
19
|
## Features
|
|
20
20
|
|
|
21
|
-
✨ **
|
|
21
|
+
✨ **59 Components** - Beautiful, accessible components ready to use
|
|
22
22
|
🎨 **Themeable** - 5 built-in accent themes with light/dark mode support
|
|
23
23
|
🚀 **Auto-imported** - Works seamlessly with Nuxt's auto-import system
|
|
24
24
|
📦 **Tree-shakeable** - Only bundle what you use
|
|
25
25
|
🎯 **TypeScript** - Fully typed for great developer experience
|
|
26
|
-
🧪 **Tested** -
|
|
26
|
+
🧪 **Tested** - 38 test suites for reliability
|
|
27
27
|
📱 **Responsive** - Mobile-first design approach
|
|
28
28
|
♿ **Accessible** - ARIA-compliant components
|
|
29
29
|
🌐 **i18n** - Built-in vue-i18n support with English defaults
|
|
@@ -77,7 +77,7 @@ function handleClick() {
|
|
|
77
77
|
|
|
78
78
|
## What's Included
|
|
79
79
|
|
|
80
|
-
### Components (
|
|
80
|
+
### Components (59)
|
|
81
81
|
|
|
82
82
|
#### Form Controls
|
|
83
83
|
|
|
@@ -269,7 +269,7 @@ npm run docs:dev
|
|
|
269
269
|
orio-ui/
|
|
270
270
|
├── src/
|
|
271
271
|
│ ├── runtime/
|
|
272
|
-
│ │ ├── components/ #
|
|
272
|
+
│ │ ├── components/ # 59 Vue components
|
|
273
273
|
│ │ ├── composables/ # 15 composables
|
|
274
274
|
│ │ ├── assets/css/ # Theme CSS files
|
|
275
275
|
│ │ └── utils/ # Icon registry
|
package/dist/agents/ROUTING.md
CHANGED
|
@@ -82,6 +82,7 @@ installed version.
|
|
|
82
82
|
- `NumberInput/Horizontal.vue` — number input variant with minus/plus buttons flanking the field and press-and-hold repeat. **Read USAGE.md first.**
|
|
83
83
|
- `NumberInput/Vertical.vue` — number input variant with chevron up/down stacked on the right and press-and-hold repeat. **Read USAGE.md first.**
|
|
84
84
|
- `RadioButton.vue` — single radio option wrapping ControlElement; group by sharing the same v-model. **Read USAGE.md first.**
|
|
85
|
+
- `SegmentedInput.vue` — fixed-length code input split into auto-advancing per-character segments for OTP / PIN entry. **Read USAGE.md first.**
|
|
85
86
|
- `Selector.vue` — button-triggered listbox in a popover; supports single and multi-select with string or object options. **Read USAGE.md first.**
|
|
86
87
|
- `SwitchButton.vue` — boolean on/off pill button (not a sliding switch) wrapping ControlElement; click/Enter/Space toggle. **Read USAGE.md first.**
|
|
87
88
|
- `TaggableSelector.vue` — multi-select Selector that renders chosen options as tag chips in the trigger. **Read USAGE.md first.**
|
|
@@ -55,6 +55,7 @@ not edit by hand.
|
|
|
55
55
|
- **number input horizontal, minus-plus stepper, quantity stepper** → `NumberInput/Horizontal.vue` (read USAGE.md first)
|
|
56
56
|
- **number input vertical, chevron stepper, stacked-arrow numeric input** → `NumberInput/Vertical.vue` (read USAGE.md first)
|
|
57
57
|
- **radio, radio button, single-choice from group** → `RadioButton.vue` (read USAGE.md first)
|
|
58
|
+
- **OTP input, one-time password, verification code, PIN code, segmented code boxes** → `SegmentedInput.vue` (read USAGE.md first)
|
|
58
59
|
- **select, dropdown, combobox, listbox picker, single or multi-select** → `Selector.vue` (read USAGE.md first)
|
|
59
60
|
- **toggle, on/off switch, pill toggle, boolean button** → `SwitchButton.vue` (read USAGE.md first)
|
|
60
61
|
- **multi-select with tag chips, taggable selector, chip picker** → `TaggableSelector.vue` (read USAGE.md first)
|
|
@@ -50,6 +50,7 @@ not edit by hand.
|
|
|
50
50
|
- **number input horizontal, minus-plus stepper, quantity stepper** → `NumberInput/Horizontal.vue` (read USAGE.md first)
|
|
51
51
|
- **number input vertical, chevron stepper, stacked-arrow numeric input** → `NumberInput/Vertical.vue` (read USAGE.md first)
|
|
52
52
|
- **radio, radio button, single-choice from group** → `RadioButton.vue` (read USAGE.md first)
|
|
53
|
+
- **OTP input, one-time password, verification code, PIN code, segmented code boxes** → `SegmentedInput.vue` (read USAGE.md first)
|
|
53
54
|
- **select, dropdown, combobox, listbox picker, single or multi-select** → `Selector.vue` (read USAGE.md first)
|
|
54
55
|
- **toggle, on/off switch, pill toggle, boolean button** → `SwitchButton.vue` (read USAGE.md first)
|
|
55
56
|
- **multi-select with tag chips, taggable selector, chip picker** → `TaggableSelector.vue` (read USAGE.md first)
|
package/dist/module.json
CHANGED
|
@@ -26,6 +26,10 @@ invariants: true
|
|
|
26
26
|
on the underlying `<input>`.
|
|
27
27
|
- **v-model is `string`** (default `""`). For numeric input use
|
|
28
28
|
`<orio-number-input>` instead.
|
|
29
|
+
- **`defineExpose({ input, focused })`** — `input` is the template ref to the
|
|
30
|
+
native `<input>`; `focused` is the **writable** `Ref<boolean>` from
|
|
31
|
+
`useFocus`. Reading reflects focus state; assigning `true`/`false` focuses or
|
|
32
|
+
blurs the element. Drive programmatic focus via `focused`, not `input.focus()`.
|
|
29
33
|
|
|
30
34
|
## Gotchas
|
|
31
35
|
|
|
@@ -14,7 +14,10 @@ type __VLS_Slots = {} & {
|
|
|
14
14
|
} & {
|
|
15
15
|
after?: (props: typeof __VLS_10) => any;
|
|
16
16
|
};
|
|
17
|
-
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
17
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
18
|
+
input: Readonly<import("vue").ShallowRef<HTMLInputElement | null, HTMLInputElement | null>>;
|
|
19
|
+
focused: import("vue").Ref<boolean, boolean>;
|
|
20
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
21
|
"update:modelValue": (value: string) => any;
|
|
19
22
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
20
23
|
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import { useTemplateRef } from "vue";
|
|
3
|
+
import { useFocus } from "@vueuse/core";
|
|
2
4
|
const props = defineProps({
|
|
3
5
|
layout: { type: String, required: false, default: "vertical" },
|
|
4
6
|
appearance: { type: String, required: false },
|
|
@@ -15,7 +17,10 @@ const props = defineProps({
|
|
|
15
17
|
name: { type: String, required: false },
|
|
16
18
|
ariaLabel: { type: String, required: false }
|
|
17
19
|
});
|
|
20
|
+
const input = useTemplateRef("input");
|
|
21
|
+
const { focused } = useFocus(input);
|
|
18
22
|
const modelValue = defineModel({ type: String, ...{ default: "" } });
|
|
23
|
+
defineExpose({ input, focused });
|
|
19
24
|
</script>
|
|
20
25
|
|
|
21
26
|
<template>
|
|
@@ -27,6 +32,7 @@ const modelValue = defineModel({ type: String, ...{ default: "" } });
|
|
|
27
32
|
>
|
|
28
33
|
<slot name="before" />
|
|
29
34
|
<input
|
|
35
|
+
ref="input"
|
|
30
36
|
v-model="modelValue"
|
|
31
37
|
type="text"
|
|
32
38
|
v-bind="{ ...$attrs, ...control }"
|
|
@@ -14,7 +14,10 @@ type __VLS_Slots = {} & {
|
|
|
14
14
|
} & {
|
|
15
15
|
after?: (props: typeof __VLS_10) => any;
|
|
16
16
|
};
|
|
17
|
-
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
17
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
18
|
+
input: Readonly<import("vue").ShallowRef<HTMLInputElement | null, HTMLInputElement | null>>;
|
|
19
|
+
focused: import("vue").Ref<boolean, boolean>;
|
|
20
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
18
21
|
"update:modelValue": (value: string) => any;
|
|
19
22
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
20
23
|
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
@@ -40,6 +40,10 @@ invariants: true
|
|
|
40
40
|
prop bag.
|
|
41
41
|
- **`$attrs` is spread before `control`** on the inner `<input>`, same as
|
|
42
42
|
Input.
|
|
43
|
+
- **`defineExpose({ input, focused })`** — `input` is the template ref to the
|
|
44
|
+
native `<input>`; `focused` is the **writable** `Ref<boolean>` from
|
|
45
|
+
`useFocus`. Reading reflects focus state; assigning `true`/`false` focuses or
|
|
46
|
+
blurs the element. Drive programmatic focus via `focused`, not `input.focus()`.
|
|
43
47
|
|
|
44
48
|
## Gotchas
|
|
45
49
|
|
|
@@ -11,7 +11,7 @@ type __VLS_Props = NumberInputProps;
|
|
|
11
11
|
declare function increase(): void;
|
|
12
12
|
declare function decrease(): void;
|
|
13
13
|
type __VLS_ModelProps = {
|
|
14
|
-
modelValue?: number;
|
|
14
|
+
modelValue?: number | null;
|
|
15
15
|
};
|
|
16
16
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
17
17
|
declare var __VLS_8: {
|
|
@@ -23,10 +23,13 @@ declare var __VLS_8: {
|
|
|
23
23
|
type __VLS_Slots = {} & {
|
|
24
24
|
controls?: (props: typeof __VLS_8) => any;
|
|
25
25
|
};
|
|
26
|
-
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
27
|
-
|
|
26
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
27
|
+
input: Readonly<import("vue").ShallowRef<HTMLInputElement | null, HTMLInputElement | null>>;
|
|
28
|
+
focused: import("vue").Ref<boolean, boolean>;
|
|
29
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
30
|
+
"update:modelValue": (value: number | null) => any;
|
|
28
31
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
29
|
-
"onUpdate:modelValue"?: ((value: number) => any) | undefined;
|
|
32
|
+
"onUpdate:modelValue"?: ((value: number | null) => any) | undefined;
|
|
30
33
|
}>, {
|
|
31
34
|
layout: InputLayout;
|
|
32
35
|
step: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
-
import { computed, toRefs } from "vue";
|
|
2
|
+
import { computed, toRefs, useTemplateRef } from "vue";
|
|
3
|
+
import { useFocus } from "@vueuse/core";
|
|
3
4
|
const props = defineProps({
|
|
4
5
|
layout: { type: String, required: false, default: "vertical" },
|
|
5
6
|
min: { type: Number, required: false, default: void 0 },
|
|
@@ -21,9 +22,11 @@ const props = defineProps({
|
|
|
21
22
|
ariaLabel: { type: String, required: false }
|
|
22
23
|
});
|
|
23
24
|
const { min, max, step, decimalPlaces } = toRefs(props);
|
|
24
|
-
const modelValue = defineModel({ type: Number, ...{ default: 0 } });
|
|
25
|
+
const modelValue = defineModel({ type: [Number, null], ...{ default: 0 } });
|
|
26
|
+
const input = useTemplateRef("input");
|
|
27
|
+
const { focused } = useFocus(input);
|
|
25
28
|
function setValidatedValue(value) {
|
|
26
|
-
let finalValue = value;
|
|
29
|
+
let finalValue = value ?? 0;
|
|
27
30
|
if (Number.isFinite(max.value) && finalValue > max.value) {
|
|
28
31
|
finalValue = max.value;
|
|
29
32
|
}
|
|
@@ -37,16 +40,16 @@ function onBlur() {
|
|
|
37
40
|
setValidatedValue(modelValue.value);
|
|
38
41
|
}
|
|
39
42
|
function increase() {
|
|
40
|
-
setValidatedValue(modelValue.value + step.value);
|
|
43
|
+
setValidatedValue((modelValue.value ?? 0) + step.value);
|
|
41
44
|
}
|
|
42
45
|
function decrease() {
|
|
43
|
-
setValidatedValue(modelValue.value - step.value);
|
|
46
|
+
setValidatedValue((modelValue.value ?? 0) - step.value);
|
|
44
47
|
}
|
|
45
48
|
const isAtMax = computed(
|
|
46
|
-
() => Number.isFinite(max.value) && modelValue.value >= max.value
|
|
49
|
+
() => Number.isFinite(max.value) && (modelValue.value ?? 0) >= max.value
|
|
47
50
|
);
|
|
48
51
|
const isAtMin = computed(
|
|
49
|
-
() => Number.isFinite(min.value) && modelValue.value <= min.value
|
|
52
|
+
() => Number.isFinite(min.value) && (modelValue.value ?? 0) <= min.value
|
|
50
53
|
);
|
|
51
54
|
const controlProps = computed(() => {
|
|
52
55
|
const {
|
|
@@ -64,6 +67,7 @@ const slotExpose = computed(() => ({
|
|
|
64
67
|
isAtMax: isAtMax.value,
|
|
65
68
|
isAtMin: isAtMin.value
|
|
66
69
|
}));
|
|
70
|
+
defineExpose({ input, focused });
|
|
67
71
|
</script>
|
|
68
72
|
|
|
69
73
|
<template>
|
|
@@ -76,6 +80,7 @@ const slotExpose = computed(() => ({
|
|
|
76
80
|
<div class="wrapper">
|
|
77
81
|
<input
|
|
78
82
|
v-bind="{ ...$attrs, ...control }"
|
|
83
|
+
ref="input"
|
|
79
84
|
v-model="modelValue"
|
|
80
85
|
type="number"
|
|
81
86
|
class="number-input"
|
|
@@ -11,7 +11,7 @@ type __VLS_Props = NumberInputProps;
|
|
|
11
11
|
declare function increase(): void;
|
|
12
12
|
declare function decrease(): void;
|
|
13
13
|
type __VLS_ModelProps = {
|
|
14
|
-
modelValue?: number;
|
|
14
|
+
modelValue?: number | null;
|
|
15
15
|
};
|
|
16
16
|
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
17
17
|
declare var __VLS_8: {
|
|
@@ -23,10 +23,13 @@ declare var __VLS_8: {
|
|
|
23
23
|
type __VLS_Slots = {} & {
|
|
24
24
|
controls?: (props: typeof __VLS_8) => any;
|
|
25
25
|
};
|
|
26
|
-
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
27
|
-
|
|
26
|
+
declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
27
|
+
input: Readonly<import("vue").ShallowRef<HTMLInputElement | null, HTMLInputElement | null>>;
|
|
28
|
+
focused: import("vue").Ref<boolean, boolean>;
|
|
29
|
+
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
30
|
+
"update:modelValue": (value: number | null) => any;
|
|
28
31
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
29
|
-
"onUpdate:modelValue"?: ((value: number) => any) | undefined;
|
|
32
|
+
"onUpdate:modelValue"?: ((value: number | null) => any) | undefined;
|
|
30
33
|
}>, {
|
|
31
34
|
layout: InputLayout;
|
|
32
35
|
step: number;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: component
|
|
3
|
+
category: Form inputs
|
|
4
|
+
purpose: OTP input, one-time password, verification code, PIN code, segmented code boxes
|
|
5
|
+
short: fixed-length code input split into auto-advancing per-character segments for OTP / PIN entry
|
|
6
|
+
invariants: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# SegmentedInput — agent-only invariants
|
|
10
|
+
|
|
11
|
+
`<orio-segmented-input>` renders `segments` × `<orio-input>` boxes and joins
|
|
12
|
+
them into a single v-model string. Built for OTP / verification-code entry.
|
|
13
|
+
|
|
14
|
+
## Invariants
|
|
15
|
+
|
|
16
|
+
- **Does NOT extend `ControlProps`** — unlike other form inputs there is no
|
|
17
|
+
`label`, `layout`, `disabled`, or ControlElement wrapper. Add your own
|
|
18
|
+
label / a11y wrapper in the consumer.
|
|
19
|
+
- **v-model is the plain concatenation** of all segment values
|
|
20
|
+
(`string | number`, default `""`). External model writes re-split across
|
|
21
|
+
segments (`segmentSize` characters each).
|
|
22
|
+
- **Sparse fills collapse.** Segments `["1", "", "3"]` join to `"13"`, which
|
|
23
|
+
immediately re-splits as `["1", "3", ""]` — characters shift left past
|
|
24
|
+
empty segments. The model can never hold positional gaps.
|
|
25
|
+
- **Keyboard handling is a window-level `onKeyDown`** that bails unless one
|
|
26
|
+
of its own segments is focused. Auto-advance happens on keydown when the
|
|
27
|
+
focused segment is already full (the key lands in the next segment);
|
|
28
|
+
Backspace on an empty segment moves focus back.
|
|
29
|
+
- **`type="number"`** (default) `preventDefault`s non-digit printable keys
|
|
30
|
+
and sets `inputmode="numeric"` on every segment. Modifier shortcuts
|
|
31
|
+
(Cmd/Ctrl/Alt) and navigation keys pass through. `type="string"` accepts
|
|
32
|
+
anything.
|
|
33
|
+
- **`start` / `end` events** fire when focus would step before the first
|
|
34
|
+
segment (Backspace) or past the last one (typed into the final full
|
|
35
|
+
segment). Use `end` for auto-submit.
|
|
36
|
+
- The first segment has `autocomplete="one-time-code"`; the rest are
|
|
37
|
+
`autocomplete="off"`.
|
|
38
|
+
- **Paste is intercepted** (`@paste.prevent` on the wrapper): the pasted
|
|
39
|
+
text replaces `modelValue` wholesale — digits-only filtered for
|
|
40
|
+
`type="number"`, trimmed for `type="string"`, truncated to
|
|
41
|
+
`segments × segmentSize`. A paste that fills every segment emits `end`.
|
|
42
|
+
|
|
43
|
+
## Gotchas
|
|
44
|
+
|
|
45
|
+
- **Paste replaces the whole value**, regardless of which segment is
|
|
46
|
+
focused — it does not insert at the focused position.
|
|
47
|
+
- **SMS autofill** (`one-time-code`) inserts via the input value setter, not
|
|
48
|
+
a paste event — it still lands in segment 0 and is truncated by
|
|
49
|
+
`maxlength`.
|
|
50
|
+
- `segments` / `segmentSize` accept `number | string` (loose HTML-attr
|
|
51
|
+
usage) and are `Number()`-coerced internally.
|
|
52
|
+
- Programmatic focus of a segment goes through the exposed `focused` ref of
|
|
53
|
+
`Input.vue` (`inputComponent.focused = true`), not `element.focus()`.
|
|
54
|
+
- Multiple instances on one page are safe — each window listener no-ops
|
|
55
|
+
unless one of its own segments is focused.
|
|
56
|
+
|
|
57
|
+
## Quick reference
|
|
58
|
+
|
|
59
|
+
```vue
|
|
60
|
+
<orio-segmented-input
|
|
61
|
+
v-model="code"
|
|
62
|
+
:segments="6"
|
|
63
|
+
:segment-size="1"
|
|
64
|
+
type="number"
|
|
65
|
+
@end="submitCode()"
|
|
66
|
+
/>
|
|
67
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
segments?: number | string;
|
|
3
|
+
segmentSize?: number | string;
|
|
4
|
+
type?: "number" | "string";
|
|
5
|
+
}
|
|
6
|
+
type __VLS_Props = Props;
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
modelValue?: string | number;
|
|
9
|
+
};
|
|
10
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:modelValue": (value: string | number) => any;
|
|
13
|
+
} & {
|
|
14
|
+
end: () => any;
|
|
15
|
+
start: () => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
17
|
+
onEnd?: (() => any) | undefined;
|
|
18
|
+
onStart?: (() => any) | undefined;
|
|
19
|
+
"onUpdate:modelValue"?: ((value: string | number) => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const _default: typeof __VLS_export;
|
|
22
|
+
export default _default;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref, computed, watch, watchEffect } from "vue";
|
|
3
|
+
import Input from "./Input.vue";
|
|
4
|
+
import { useTemplateRef } from "vue";
|
|
5
|
+
import { onKeyDown } from "@vueuse/core";
|
|
6
|
+
const { segments = 6, segmentSize = 1, type = "number" } = defineProps({
|
|
7
|
+
segments: { type: [Number, String], required: false },
|
|
8
|
+
segmentSize: { type: [Number, String], required: false },
|
|
9
|
+
type: { type: String, required: false }
|
|
10
|
+
});
|
|
11
|
+
const emit = defineEmits(["start", "end"]);
|
|
12
|
+
const modelValue = defineModel({ type: [String, Number], ...{ default: "" } });
|
|
13
|
+
function applyModel(index) {
|
|
14
|
+
return String(modelValue.value).slice(
|
|
15
|
+
index * Number(segmentSize),
|
|
16
|
+
index * Number(segmentSize) + Number(segmentSize)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function setupValue() {
|
|
20
|
+
return Array.from(
|
|
21
|
+
{ length: Number(segments) },
|
|
22
|
+
(_, index) => applyModel(index)
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
const inputComponents = useTemplateRef("inputComponents");
|
|
26
|
+
const inputComponentsModel = ref(setupValue());
|
|
27
|
+
function focusSegment(index) {
|
|
28
|
+
const target = inputComponents.value?.[index];
|
|
29
|
+
if (target) target.focused = true;
|
|
30
|
+
}
|
|
31
|
+
const focusState = computed(() => {
|
|
32
|
+
const currentIndex = inputComponents.value?.findIndex(
|
|
33
|
+
(inputComponent) => inputComponent?.focused
|
|
34
|
+
);
|
|
35
|
+
if (currentIndex === void 0 || currentIndex === -1)
|
|
36
|
+
return { currentIndex: void 0, focusedElement: null };
|
|
37
|
+
const focusedElement = inputComponents.value?.[currentIndex];
|
|
38
|
+
return { currentIndex, focusedElement };
|
|
39
|
+
});
|
|
40
|
+
function focusMove(delta) {
|
|
41
|
+
if (focusState.value.currentIndex === void 0) return;
|
|
42
|
+
let newIndex = focusState.value.currentIndex + delta;
|
|
43
|
+
const maxIndex = inputComponents.value?.length ?? 0;
|
|
44
|
+
if (newIndex >= maxIndex - 1) {
|
|
45
|
+
emit("end");
|
|
46
|
+
newIndex = maxIndex - 1;
|
|
47
|
+
}
|
|
48
|
+
if (newIndex <= 0) {
|
|
49
|
+
emit("start");
|
|
50
|
+
newIndex = 0;
|
|
51
|
+
}
|
|
52
|
+
focusSegment(newIndex);
|
|
53
|
+
}
|
|
54
|
+
onKeyDown(true, (event) => {
|
|
55
|
+
const { currentIndex } = focusState.value;
|
|
56
|
+
if (currentIndex === void 0) return;
|
|
57
|
+
if (event.key === "Backspace") {
|
|
58
|
+
if (inputComponentsModel.value[currentIndex]?.length === 0) focusMove(-1);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
62
|
+
if (event.key.length > 1) return;
|
|
63
|
+
if (type === "number" && !/^\d$/.test(event.key)) {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const currentValue = inputComponentsModel.value[currentIndex] ?? "";
|
|
68
|
+
if (currentValue.length >= Number(segmentSize)) focusMove(1);
|
|
69
|
+
});
|
|
70
|
+
function handlePaste(event) {
|
|
71
|
+
const pastedText = event.clipboardData?.getData("text") ?? "";
|
|
72
|
+
const sanitizedText = type === "number" ? pastedText.replace(/\D/g, "") : pastedText.trim();
|
|
73
|
+
if (!sanitizedText) return;
|
|
74
|
+
const capacity = Number(segments) * Number(segmentSize);
|
|
75
|
+
modelValue.value = sanitizedText.slice(0, capacity);
|
|
76
|
+
if (sanitizedText.length >= capacity) emit("end");
|
|
77
|
+
}
|
|
78
|
+
watch(
|
|
79
|
+
() => inputComponentsModel.value.join(""),
|
|
80
|
+
(joinedValue) => {
|
|
81
|
+
modelValue.value = joinedValue;
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
watchEffect(() => {
|
|
85
|
+
inputComponentsModel.value = setupValue();
|
|
86
|
+
});
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<div class="segmented-input" @paste.prevent="handlePaste">
|
|
91
|
+
<Input
|
|
92
|
+
v-for="(segment, index) in inputComponentsModel"
|
|
93
|
+
:key="index"
|
|
94
|
+
ref="inputComponents"
|
|
95
|
+
v-model="inputComponentsModel[index]"
|
|
96
|
+
:maxlength="segmentSize"
|
|
97
|
+
:inputmode="type === 'number' ? 'numeric' : void 0"
|
|
98
|
+
:autocomplete="index === 0 ? 'one-time-code' : 'off'"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
|
|
103
|
+
<style scoped>
|
|
104
|
+
.segmented-input {
|
|
105
|
+
display: flex;
|
|
106
|
+
}
|
|
107
|
+
.segmented-input > div {
|
|
108
|
+
flex: 1 1 0;
|
|
109
|
+
min-width: 0;
|
|
110
|
+
}
|
|
111
|
+
.segmented-input :deep(input) {
|
|
112
|
+
text-align: center;
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
segments?: number | string;
|
|
3
|
+
segmentSize?: number | string;
|
|
4
|
+
type?: "number" | "string";
|
|
5
|
+
}
|
|
6
|
+
type __VLS_Props = Props;
|
|
7
|
+
type __VLS_ModelProps = {
|
|
8
|
+
modelValue?: string | number;
|
|
9
|
+
};
|
|
10
|
+
type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
|
|
11
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
12
|
+
"update:modelValue": (value: string | number) => any;
|
|
13
|
+
} & {
|
|
14
|
+
end: () => any;
|
|
15
|
+
start: () => any;
|
|
16
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
17
|
+
onEnd?: (() => any) | undefined;
|
|
18
|
+
onStart?: (() => any) | undefined;
|
|
19
|
+
"onUpdate:modelValue"?: ((value: string | number) => any) | undefined;
|
|
20
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
21
|
+
declare const _default: typeof __VLS_export;
|
|
22
|
+
export default _default;
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Button } from "./components/Button.vue.js";
|
|
|
3
3
|
export { default as Form, type FormProps } from "./components/Form.vue.js";
|
|
4
4
|
export { default as NavButton } from "./components/NavButton.vue.js";
|
|
5
5
|
export { default as Input } from "./components/Input.vue.js";
|
|
6
|
+
export { default as SegmentedInput } from "./components/SegmentedInput.vue.js";
|
|
6
7
|
export { default as NumberInput } from "./components/NumberInput/index.vue.js";
|
|
7
8
|
export { default as NumberInputVertical } from "./components/NumberInput/Vertical.vue.js";
|
|
8
9
|
export { default as NumberInputHorizontal } from "./components/NumberInput/Horizontal.vue.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export { default as Button } from "./components/Button.vue";
|
|
|
3
3
|
export { default as Form } from "./components/Form.vue";
|
|
4
4
|
export { default as NavButton } from "./components/NavButton.vue";
|
|
5
5
|
export { default as Input } from "./components/Input.vue";
|
|
6
|
+
export { default as SegmentedInput } from "./components/SegmentedInput.vue";
|
|
6
7
|
export { default as NumberInput } from "./components/NumberInput/index.vue";
|
|
7
8
|
export { default as NumberInputVertical } from "./components/NumberInput/Vertical.vue";
|
|
8
9
|
export { default as NumberInputHorizontal } from "./components/NumberInput/Horizontal.vue";
|