@umbra.ui/core 0.4.6 → 0.5.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/components/inputs/InputCard/InputCard.vue +46 -9
- package/dist/components/inputs/InputCryptoAddress/InputCryptoAddress.vue +23 -4
- package/dist/components/inputs/InputEmail/InputEmail.vue +14 -7
- package/dist/components/inputs/InputNumber/InputNumber.vue +36 -3
- package/dist/components/inputs/InputPhone/InputPhone.vue +36 -3
- package/dist/components/inputs/InputSecure/InputSecure.vue +27 -3
- package/dist/components/pickers/CollectionPicker/CollectionPicker.vue +31 -9
- package/dist/components/pickers/CollectionPicker/README.md +50 -0
- package/dist/components/pickers/ColorPicker/ColorPicker.vue +27 -4
- package/dist/components/pickers/ColorPicker/README.md +50 -0
- package/dist/components/pickers/IconPicker/IconPicker.vue +53 -26
- package/dist/components/pickers/IconPicker/README.md +43 -0
- package/package.json +5 -4
- package/src/components/inputs/InputCard/InputCard.vue +46 -9
- package/src/components/inputs/InputCryptoAddress/InputCryptoAddress.vue +23 -4
- package/src/components/inputs/InputEmail/InputEmail.vue +14 -7
- package/src/components/inputs/InputNumber/InputNumber.vue +36 -3
- package/src/components/inputs/InputPhone/InputPhone.vue +36 -3
- package/src/components/inputs/InputSecure/InputSecure.vue +27 -3
- package/src/components/pickers/CollectionPicker/CollectionPicker.vue +31 -9
- package/src/components/pickers/CollectionPicker/README.md +50 -0
- package/src/components/pickers/ColorPicker/ColorPicker.vue +27 -4
- package/src/components/pickers/ColorPicker/README.md +50 -0
- package/src/components/pickers/IconPicker/IconPicker.vue +53 -26
- package/src/components/pickers/IconPicker/README.md +43 -0
- package/src/skills/README.md +108 -0
- package/src/skills/umbra.ui-colors-application/SKILL.md +102 -0
- package/src/skills/umbra.ui-colors-application/reference.md +178 -0
- package/src/skills/umbra.ui-control-button/SKILL.md +34 -0
- package/src/skills/umbra.ui-control-checkbox/SKILL.md +34 -0
- package/src/skills/umbra.ui-control-dropdown/SKILL.md +34 -0
- package/src/skills/umbra.ui-control-icon-button/SKILL.md +33 -0
- package/src/skills/umbra.ui-control-inline-dropdown/SKILL.md +32 -0
- package/src/skills/umbra.ui-control-radio/SKILL.md +33 -0
- package/src/skills/umbra.ui-control-range-slider/SKILL.md +33 -0
- package/src/skills/umbra.ui-control-segmented-control/SKILL.md +33 -0
- package/src/skills/umbra.ui-control-slider/SKILL.md +33 -0
- package/src/skills/umbra.ui-control-stepper/SKILL.md +33 -0
- package/src/skills/umbra.ui-control-switch/SKILL.md +33 -0
- package/src/skills/umbra.ui-dialog-alert/SKILL.md +34 -0
- package/src/skills/umbra.ui-dialog-toast/SKILL.md +35 -0
- package/src/skills/umbra.ui-indicator-progress-bar/SKILL.md +33 -0
- package/src/skills/umbra.ui-indicator-tooltip/SKILL.md +33 -0
- package/src/skills/umbra.ui-input-card/SKILL.md +42 -0
- package/src/skills/umbra.ui-input-card/reference.md +36 -0
- package/src/skills/umbra.ui-input-crypto-address/SKILL.md +40 -0
- package/src/skills/umbra.ui-input-crypto-address/reference.md +40 -0
- package/src/skills/umbra.ui-input-email/SKILL.md +45 -0
- package/src/skills/umbra.ui-input-number/SKILL.md +39 -0
- package/src/skills/umbra.ui-input-otp/SKILL.md +44 -0
- package/src/skills/umbra.ui-input-phone/SKILL.md +45 -0
- package/src/skills/umbra.ui-input-phone/reference.md +35 -0
- package/src/skills/umbra.ui-input-search/SKILL.md +43 -0
- package/src/skills/umbra.ui-input-search/reference.md +45 -0
- package/src/skills/umbra.ui-input-secure/SKILL.md +43 -0
- package/src/skills/umbra.ui-input-secure/reference.md +44 -0
- package/src/skills/umbra.ui-input-string-capture/SKILL.md +41 -0
- package/src/skills/umbra.ui-input-tags/SKILL.md +46 -0
- package/src/skills/umbra.ui-input-tags/reference.md +44 -0
- package/src/skills/umbra.ui-input-text/SKILL.md +46 -0
- package/src/skills/umbra.ui-menu-action-menu/SKILL.md +34 -0
- package/src/skills/umbra.ui-model-popover/SKILL.md +35 -0
- package/src/skills/umbra.ui-model-sheet/SKILL.md +35 -0
- package/src/skills/umbra.ui-model-sidebar/SKILL.md +35 -0
- package/src/skills/umbra.ui-picker-collection/SKILL.md +36 -0
- package/src/skills/umbra.ui-picker-collection/reference.md +34 -0
- package/src/skills/umbra.ui-picker-color/SKILL.md +36 -0
- package/src/skills/umbra.ui-picker-color/reference.md +34 -0
- package/src/skills/umbra.ui-picker-date/SKILL.md +36 -0
- package/src/skills/umbra.ui-picker-date/reference.md +26 -0
- package/src/skills/umbra.ui-picker-file/SKILL.md +42 -0
- package/src/skills/umbra.ui-picker-file/reference.md +39 -0
- package/src/skills/umbra.ui-picker-icon/SKILL.md +36 -0
- package/src/skills/umbra.ui-picker-icon/reference.md +28 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ref,
|
|
4
|
+
watch,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
nextTick,
|
|
7
|
+
computed,
|
|
8
|
+
useSlots,
|
|
9
|
+
} from "vue";
|
|
3
10
|
import { colors, colorPickerColors } from "./colors";
|
|
4
11
|
import type { Color } from "./colors";
|
|
5
12
|
import {
|
|
@@ -17,6 +24,7 @@ export interface Props {
|
|
|
17
24
|
color: Color;
|
|
18
25
|
pickerOffsetX: number;
|
|
19
26
|
preventPopup: boolean;
|
|
27
|
+
label?: boolean;
|
|
20
28
|
// Dot styling props
|
|
21
29
|
dotSize?: number | string;
|
|
22
30
|
dotRadius?: number | string;
|
|
@@ -31,6 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
31
39
|
color: () => colorPickerColors.gray700,
|
|
32
40
|
pickerOffsetX: 0,
|
|
33
41
|
preventPopup: false,
|
|
42
|
+
label: true,
|
|
34
43
|
dotSize: "1.25rem",
|
|
35
44
|
dotRadius: "999px",
|
|
36
45
|
dotBorderWidth: 1,
|
|
@@ -38,6 +47,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
38
47
|
dotBorderColorActive: "var(--colorpicker-dot-border-color-active)",
|
|
39
48
|
showColorInfo: true,
|
|
40
49
|
});
|
|
50
|
+
const slots = useSlots();
|
|
51
|
+
const hasCustomLabelSlot = computed(() => Boolean(slots.label));
|
|
41
52
|
|
|
42
53
|
const emits = defineEmits(["update:color"]);
|
|
43
54
|
|
|
@@ -173,12 +184,16 @@ onBeforeUnmount(() => {
|
|
|
173
184
|
<div
|
|
174
185
|
:class="[
|
|
175
186
|
$style.button,
|
|
176
|
-
|
|
187
|
+
hasCustomLabelSlot ? $style.button_unstyled : null,
|
|
188
|
+
!hasCustomLabelSlot &&
|
|
189
|
+
(showPopover ? $style.button_selected : $style.button_normal),
|
|
177
190
|
]"
|
|
178
191
|
ref="button"
|
|
179
192
|
@click="togglePopover"
|
|
180
193
|
>
|
|
181
|
-
<
|
|
194
|
+
<slot v-if="label" name="label">
|
|
195
|
+
<div :style="dotStyles" :class="$style.dot"></div>
|
|
196
|
+
</slot>
|
|
182
197
|
</div>
|
|
183
198
|
|
|
184
199
|
<!-- Teleport the overlay and picker to body -->
|
|
@@ -253,11 +268,19 @@ onBeforeUnmount(() => {
|
|
|
253
268
|
transition: padding-left 0.3s, padding-right 0.3s, background-color 0.3s,
|
|
254
269
|
box-shadow 0.3s;
|
|
255
270
|
cursor: pointer;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.button_unstyled {
|
|
274
|
+
padding: 0;
|
|
275
|
+
transition: none;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.button_normal {
|
|
256
279
|
background-color: var(--picker-button-bg);
|
|
257
280
|
border: var(--picker-button-border);
|
|
258
281
|
}
|
|
259
282
|
|
|
260
|
-
.
|
|
283
|
+
.button_normal:hover {
|
|
261
284
|
padding-left: 0.588rem;
|
|
262
285
|
padding-right: 0.588rem;
|
|
263
286
|
box-shadow: 0px 1px 0px 0px var(--picker-button-hover-shadow),
|
|
@@ -92,6 +92,55 @@ const handleColorChange = (color: Color) => {
|
|
|
92
92
|
</style>
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
## Custom Label Slot
|
|
96
|
+
|
|
97
|
+
When you provide a custom `label` slot, the default dot/button visuals are removed
|
|
98
|
+
so you can fully control the trigger presentation and animation.
|
|
99
|
+
|
|
100
|
+
```vue
|
|
101
|
+
<script setup lang="ts">
|
|
102
|
+
import { ref } from "vue";
|
|
103
|
+
import { ColorPicker, colorPickerColors } from "@umbra-ui/core";
|
|
104
|
+
import type { Color } from "@umbra-ui/core";
|
|
105
|
+
|
|
106
|
+
const selectedColor = ref<Color>(colorPickerColors.blue500);
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<template>
|
|
110
|
+
<ColorPicker v-model:color="selectedColor">
|
|
111
|
+
<template #label>
|
|
112
|
+
<span class="color-chip">
|
|
113
|
+
<span class="swatch" :style="{ backgroundColor: selectedColor.hex }" />
|
|
114
|
+
{{ selectedColor.hex }}
|
|
115
|
+
</span>
|
|
116
|
+
</template>
|
|
117
|
+
</ColorPicker>
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<style module>
|
|
121
|
+
.color-chip {
|
|
122
|
+
display: inline-flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: 0.353rem;
|
|
125
|
+
padding: 0.294rem 0.471rem;
|
|
126
|
+
border-radius: 0.471rem;
|
|
127
|
+
border: 1px solid var(--border-soft, #d1d5db);
|
|
128
|
+
}
|
|
129
|
+
.swatch {
|
|
130
|
+
width: 0.706rem;
|
|
131
|
+
height: 0.706rem;
|
|
132
|
+
border-radius: 999px;
|
|
133
|
+
border: 1px solid #ffffff;
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Hiding the Label
|
|
139
|
+
|
|
140
|
+
```vue
|
|
141
|
+
<ColorPicker v-model:color="selectedColor" :label="false" />
|
|
142
|
+
```
|
|
143
|
+
|
|
95
144
|
## Props
|
|
96
145
|
|
|
97
146
|
| Prop Name | Type | Required | Default | Description |
|
|
@@ -99,6 +148,7 @@ const handleColorChange = (color: Color) => {
|
|
|
99
148
|
| `color` | `Color` | Yes | `colorPickerColors.gray700` | Currently selected color |
|
|
100
149
|
| `pickerOffsetX` | `number` | No | `0` | Horizontal offset for picker positioning |
|
|
101
150
|
| `preventPopup` | `boolean` | No | `false` | Whether to prevent the popup from opening |
|
|
151
|
+
| `label` | `boolean` | No | `true` | Show or hide the trigger label/slot |
|
|
102
152
|
| `dotSize` | `number \| string` | No | `"1.25rem"` | Size of the color dot |
|
|
103
153
|
| `dotRadius` | `number \| string` | No | `"999px"` | Border radius of the color dot |
|
|
104
154
|
| `dotBorderWidth` | `number` | No | `1` | Border width of the color dot |
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ref,
|
|
4
|
+
watch,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
nextTick,
|
|
7
|
+
computed,
|
|
8
|
+
useSlots,
|
|
9
|
+
} from "vue";
|
|
3
10
|
import {
|
|
4
11
|
offset,
|
|
5
12
|
flip,
|
|
@@ -16,6 +23,7 @@ export interface Props {
|
|
|
16
23
|
icon: string;
|
|
17
24
|
pickerOffsetX: number;
|
|
18
25
|
preventPopup: boolean;
|
|
26
|
+
label?: boolean;
|
|
19
27
|
iconList?: IconKey[];
|
|
20
28
|
iconSize?: number;
|
|
21
29
|
}
|
|
@@ -23,9 +31,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
23
31
|
icon: "",
|
|
24
32
|
pickerOffsetX: 0,
|
|
25
33
|
preventPopup: false,
|
|
34
|
+
label: true,
|
|
26
35
|
iconList: undefined,
|
|
27
36
|
iconSize: 18,
|
|
28
37
|
});
|
|
38
|
+
const slots = useSlots();
|
|
39
|
+
const hasCustomLabelSlot = computed(() => Boolean(slots.label));
|
|
29
40
|
|
|
30
41
|
const emits = defineEmits(["update:icon"]);
|
|
31
42
|
|
|
@@ -139,30 +150,37 @@ onBeforeUnmount(() => {
|
|
|
139
150
|
<template>
|
|
140
151
|
<div :class="$style.container" ref="container">
|
|
141
152
|
<div
|
|
142
|
-
:class="[
|
|
153
|
+
:class="[
|
|
154
|
+
$style.button,
|
|
155
|
+
hasCustomLabelSlot ? $style.button_unstyled : null,
|
|
156
|
+
!hasCustomLabelSlot &&
|
|
157
|
+
(showPopover ? $style.button_selected : $style.button_normal),
|
|
158
|
+
]"
|
|
143
159
|
@click="togglePopover"
|
|
144
160
|
ref="button"
|
|
145
161
|
>
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
<slot v-if="label" name="label">
|
|
163
|
+
<component
|
|
164
|
+
v-if="selectedIcon && icons[selectedIcon as IconKey]"
|
|
165
|
+
:is="icons[selectedIcon as IconKey]"
|
|
166
|
+
:size="iconSize"
|
|
167
|
+
:color="selectedColor"
|
|
168
|
+
/>
|
|
169
|
+
<component
|
|
170
|
+
v-else
|
|
171
|
+
:is="icons.folder"
|
|
172
|
+
:size="iconSize"
|
|
173
|
+
:color="selectedColor"
|
|
174
|
+
/>
|
|
175
|
+
<ChevronDownIcon
|
|
176
|
+
:size="selectedSize - 4"
|
|
177
|
+
:class="$style.chevron"
|
|
178
|
+
:color="selectedColor"
|
|
179
|
+
:style="{
|
|
180
|
+
transform: `rotate(${showPopover ? 0 : -90}deg)`,
|
|
181
|
+
}"
|
|
182
|
+
/>
|
|
183
|
+
</slot>
|
|
166
184
|
</div>
|
|
167
185
|
|
|
168
186
|
<!-- Teleport the overlay and picker to body -->
|
|
@@ -229,12 +247,21 @@ onBeforeUnmount(() => {
|
|
|
229
247
|
cursor: pointer;
|
|
230
248
|
transition: background-color 0.3s ease, box-shadow 0.3s ease,
|
|
231
249
|
padding 0.3s ease, gap 0.3s ease;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.button_unstyled {
|
|
253
|
+
padding: 0;
|
|
254
|
+
gap: 0;
|
|
255
|
+
transition: none;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.button_normal {
|
|
232
259
|
background-color: var(--picker-button-bg);
|
|
233
260
|
border: var(--picker-button-border);
|
|
234
261
|
}
|
|
235
262
|
|
|
236
|
-
.
|
|
237
|
-
.
|
|
263
|
+
.button_normal:hover,
|
|
264
|
+
.button_selected {
|
|
238
265
|
background-color: var(--picker-button-hover-bg);
|
|
239
266
|
box-shadow: 0px 1px 0px 0px var(--picker-button-hover-shadow),
|
|
240
267
|
inset 0px 1px 0px 0px var(--picker-button-hover-inset-shadow);
|
|
@@ -248,8 +275,8 @@ onBeforeUnmount(() => {
|
|
|
248
275
|
opacity: 0;
|
|
249
276
|
}
|
|
250
277
|
|
|
251
|
-
.
|
|
252
|
-
.
|
|
278
|
+
.button_normal:hover .chevron,
|
|
279
|
+
.button_selected .chevron {
|
|
253
280
|
opacity: 1;
|
|
254
281
|
}
|
|
255
282
|
|
|
@@ -64,6 +64,48 @@ const handleIconChange = (icon: IconKey) => {
|
|
|
64
64
|
</style>
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
## Custom Label Slot
|
|
68
|
+
|
|
69
|
+
When you provide a custom `label` slot, the default trigger icon/chevron visuals
|
|
70
|
+
are removed so you can fully control the trigger UI and animation.
|
|
71
|
+
|
|
72
|
+
```vue
|
|
73
|
+
<script setup lang="ts">
|
|
74
|
+
import { computed, ref } from "vue";
|
|
75
|
+
import { IconPicker } from "@umbra-ui/core";
|
|
76
|
+
import type { IconKey } from "@umbra-ui/icons";
|
|
77
|
+
|
|
78
|
+
const selectedIcon = ref<IconKey>("home");
|
|
79
|
+
const triggerText = computed(() => `Icon: ${selectedIcon.value}`);
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<template>
|
|
83
|
+
<IconPicker v-model:icon="selectedIcon">
|
|
84
|
+
<template #label>
|
|
85
|
+
<span class="icon-chip">{{ triggerText }}</span>
|
|
86
|
+
</template>
|
|
87
|
+
</IconPicker>
|
|
88
|
+
</template>
|
|
89
|
+
|
|
90
|
+
<style module>
|
|
91
|
+
.icon-chip {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
padding: 0.353rem 0.529rem;
|
|
95
|
+
border-radius: 0.471rem;
|
|
96
|
+
background: var(--background-subtle, #f3f4f6);
|
|
97
|
+
color: var(--text-primary, #111827);
|
|
98
|
+
border: 1px solid var(--border-soft, #d1d5db);
|
|
99
|
+
}
|
|
100
|
+
</style>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Hiding the Label
|
|
104
|
+
|
|
105
|
+
```vue
|
|
106
|
+
<IconPicker v-model:icon="selectedIcon" :label="false" />
|
|
107
|
+
```
|
|
108
|
+
|
|
67
109
|
## Props
|
|
68
110
|
|
|
69
111
|
| Prop Name | Type | Required | Default | Description |
|
|
@@ -71,6 +113,7 @@ const handleIconChange = (icon: IconKey) => {
|
|
|
71
113
|
| `icon` | `string` | Yes | `""` | Currently selected icon key |
|
|
72
114
|
| `pickerOffsetX` | `number` | No | `0` | Horizontal offset for picker positioning |
|
|
73
115
|
| `preventPopup` | `boolean` | No | `false` | Whether to prevent the popup from opening |
|
|
116
|
+
| `label` | `boolean` | No | `true` | Show or hide the trigger label/slot |
|
|
74
117
|
| `iconList` | `IconKey[]` | No | `undefined` | Custom list of available icons |
|
|
75
118
|
| `iconSize` | `number` | No | `18` | Size of the displayed icon |
|
|
76
119
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umbra.ui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Core components for Umbra UI",
|
|
5
|
+
"homepage": "https://www.umbraui.com/",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "dist/index.js",
|
|
7
8
|
"types": "dist/index.d.ts",
|
|
@@ -35,9 +36,9 @@
|
|
|
35
36
|
"@tiptap/markdown": "^3.19.0",
|
|
36
37
|
"@tiptap/starter-kit": "^3.19.0",
|
|
37
38
|
"@tiptap/vue-3": "^3.19.0",
|
|
38
|
-
"@umbra.ui/colors": "^0.
|
|
39
|
-
"@umbra.ui/icons": "^0.
|
|
40
|
-
"@umbra.ui/typography": "^0.
|
|
39
|
+
"@umbra.ui/colors": "^0.5.0",
|
|
40
|
+
"@umbra.ui/icons": "^0.5.0",
|
|
41
|
+
"@umbra.ui/typography": "^0.5.0",
|
|
41
42
|
"autosize": "^6.0.1",
|
|
42
43
|
"@types/autosize": "^4.0.3",
|
|
43
44
|
"gsap": "^3.13.0",
|
|
@@ -39,7 +39,7 @@ const currentDigitCount = computed(() => {
|
|
|
39
39
|
|
|
40
40
|
const maxAllowedDigits = computed(() => {
|
|
41
41
|
if (cardInfo.value) {
|
|
42
|
-
return
|
|
42
|
+
return cardInfo.value.format.reduce((sum, group) => sum + group, 0);
|
|
43
43
|
}
|
|
44
44
|
// Default max for unknown cards
|
|
45
45
|
return 16;
|
|
@@ -48,12 +48,14 @@ const maxAllowedDigits = computed(() => {
|
|
|
48
48
|
const internalValue = ref(props.value);
|
|
49
49
|
const cursorPosition = ref<number | null>(null);
|
|
50
50
|
const inputRef = ref<HTMLInputElement | null>(null);
|
|
51
|
+
const isFocused = ref(false);
|
|
52
|
+
const hasBlurred = ref(false);
|
|
51
53
|
|
|
52
54
|
// Card type patterns
|
|
53
55
|
const cardPatterns = {
|
|
54
56
|
visa: {
|
|
55
57
|
pattern: /^4/,
|
|
56
|
-
lengths: [16
|
|
58
|
+
lengths: [16],
|
|
57
59
|
format: [4, 4, 4, 4],
|
|
58
60
|
cvvLength: 3,
|
|
59
61
|
name: "Visa",
|
|
@@ -187,15 +189,38 @@ const isComplete = computed(() => {
|
|
|
187
189
|
if (!cardInfo.value) return false;
|
|
188
190
|
return cardInfo.value.lengths.includes(cleaned.length);
|
|
189
191
|
});
|
|
192
|
+
const hasCardInput = computed(() => {
|
|
193
|
+
return internalValue.value.replace(/\D/g, "").length > 0;
|
|
194
|
+
});
|
|
190
195
|
|
|
191
|
-
// Override state to show error when card is
|
|
196
|
+
// Override state to show error on blur when card input is invalid
|
|
192
197
|
const effectiveState = computed(() => {
|
|
193
|
-
if (
|
|
198
|
+
if (props.state === "error") {
|
|
199
|
+
return "error";
|
|
200
|
+
}
|
|
201
|
+
if (props.state === "disabled" || props.state === "readonly") {
|
|
202
|
+
return props.state;
|
|
203
|
+
}
|
|
204
|
+
if (
|
|
205
|
+
!isFocused.value &&
|
|
206
|
+
hasBlurred.value &&
|
|
207
|
+
hasCardInput.value &&
|
|
208
|
+
!isValid.value
|
|
209
|
+
) {
|
|
194
210
|
return "error";
|
|
195
211
|
}
|
|
196
212
|
return props.state;
|
|
197
213
|
});
|
|
198
214
|
|
|
215
|
+
const showInternalError = computed(() => {
|
|
216
|
+
return (
|
|
217
|
+
!isFocused.value &&
|
|
218
|
+
hasBlurred.value &&
|
|
219
|
+
hasCardInput.value &&
|
|
220
|
+
!isValid.value
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
199
224
|
// Watch for changes to value prop
|
|
200
225
|
watch(
|
|
201
226
|
() => props.value,
|
|
@@ -244,12 +269,13 @@ const handleInput = (event: Event) => {
|
|
|
244
269
|
// Format the number
|
|
245
270
|
const { formatted, cursorPos } = formatCardNumber(digitsOnly);
|
|
246
271
|
internalValue.value = formatted;
|
|
272
|
+
const normalizedDigits = formatted.replace(/\D/g, "");
|
|
247
273
|
|
|
248
274
|
// Update cursor position
|
|
249
275
|
cursorPosition.value = cursorPos;
|
|
250
276
|
|
|
251
277
|
// Emit events
|
|
252
|
-
emit("update:value",
|
|
278
|
+
emit("update:value", normalizedDigits); // Raw digits
|
|
253
279
|
emit("update:formatted", formatted); // Formatted display
|
|
254
280
|
emit("update:cardType", cardInfo.value?.name || "");
|
|
255
281
|
emit("update:valid", isValid.value && isComplete.value);
|
|
@@ -275,7 +301,7 @@ const handlePaste = (event: ClipboardEvent) => {
|
|
|
275
301
|
// Detect card type first to know the limit
|
|
276
302
|
const cardType = detectCardType(cleanedNumber);
|
|
277
303
|
const maxLength = cardType
|
|
278
|
-
?
|
|
304
|
+
? cardPatterns[cardType].format.reduce((sum, group) => sum + group, 0)
|
|
279
305
|
: 16;
|
|
280
306
|
|
|
281
307
|
// Enforce the limit
|
|
@@ -283,7 +309,7 @@ const handlePaste = (event: ClipboardEvent) => {
|
|
|
283
309
|
|
|
284
310
|
const { formatted } = formatCardNumber(cleanedNumber);
|
|
285
311
|
internalValue.value = formatted;
|
|
286
|
-
emit("update:value",
|
|
312
|
+
emit("update:value", formatted.replace(/\D/g, ""));
|
|
287
313
|
emit("update:formatted", formatted);
|
|
288
314
|
emit("update:cardType", cardInfo.value?.name || "");
|
|
289
315
|
emit("update:valid", isValid.value && isComplete.value);
|
|
@@ -323,6 +349,15 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
323
349
|
}
|
|
324
350
|
};
|
|
325
351
|
|
|
352
|
+
const handleFocus = () => {
|
|
353
|
+
isFocused.value = true;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const handleBlur = () => {
|
|
357
|
+
isFocused.value = false;
|
|
358
|
+
hasBlurred.value = true;
|
|
359
|
+
};
|
|
360
|
+
|
|
326
361
|
// SVG icons for card types
|
|
327
362
|
const cardIcons = {
|
|
328
363
|
visa: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><g class="nc-icon-wrapper"><rect x="2" y="7" width="28" height="18" rx="3" ry="3" fill="#1434cb" stroke-width="0"></rect><path d="m27,7H5c-1.657,0-3,1.343-3,3v12c0,1.657,1.343,3,3,3h22c1.657,0,3-1.343,3-3v-12c0-1.657-1.343-3-3-3Zm2,15c0,1.103-.897,2-2,2H5c-1.103,0-2-.897-2-2v-12c0-1.103.897-2,2-2h22c1.103,0,2,.897,2,2v12Z" stroke-width="0" opacity=".15"></path><path d="m27,8H5c-1.105,0-2,.895-2,2v1c0-1.105.895-2,2-2h22c1.105,0,2,.895,2,2v-1c0-1.105-.895-2-2-2Z" fill="#fff" opacity=".2" stroke-width="0"></path><path d="m13.392,12.624l-2.838,6.77h-1.851l-1.397-5.403c-.085-.332-.158-.454-.416-.595-.421-.229-1.117-.443-1.728-.576l.041-.196h2.98c.38,0,.721.253.808.69l.738,3.918,1.822-4.608h1.84Z" fill="#fff" stroke-width="0"></path><path d="m20.646,17.183c.008-1.787-2.47-1.886-2.453-2.684.005-.243.237-.501.743-.567.251-.032.943-.058,1.727.303l.307-1.436c-.421-.152-.964-.299-1.638-.299-1.732,0-2.95.92-2.959,2.238-.011.975.87,1.518,1.533,1.843.683.332.912.545.909.841-.005.454-.545.655-1.047.663-.881.014-1.392-.238-1.799-.428l-.318,1.484c.41.188,1.165.351,1.947.359,1.841,0,3.044-.909,3.05-2.317" fill="#fff" stroke-width="0"></path><path d="m25.423,12.624h-1.494c-.337,0-.62.195-.746.496l-2.628,6.274h1.839l.365-1.011h2.247l.212,1.011h1.62l-1.415-6.77Zm-2.16,4.372l.922-2.542.53,2.542h-1.452Z" fill="#fff" stroke-width="0"></path><path fill="#fff" stroke-width="0" d="M15.894 12.624L14.446 19.394 12.695 19.394 14.143 12.624 15.894 12.624z"></path></g></svg>`,
|
|
@@ -365,6 +400,8 @@ const getPlaceholder = computed(() => {
|
|
|
365
400
|
@input="handleInput"
|
|
366
401
|
@paste="handlePaste"
|
|
367
402
|
@keydown="handleKeyDown"
|
|
403
|
+
@focus="handleFocus"
|
|
404
|
+
@blur="handleBlur"
|
|
368
405
|
:maxlength="24"
|
|
369
406
|
:disabled="state === 'disabled'"
|
|
370
407
|
:readonly="state === 'readonly'"
|
|
@@ -377,7 +414,7 @@ const getPlaceholder = computed(() => {
|
|
|
377
414
|
></div>
|
|
378
415
|
</transition>
|
|
379
416
|
<transition name="fade">
|
|
380
|
-
<div v-if="
|
|
417
|
+
<div v-if="showInternalError" :class="$style.error_icon">
|
|
381
418
|
<TriangleWarningIcon size="16" />
|
|
382
419
|
</div>
|
|
383
420
|
</transition>
|
|
@@ -385,7 +422,7 @@ const getPlaceholder = computed(() => {
|
|
|
385
422
|
</div>
|
|
386
423
|
<transition name="slide-fade">
|
|
387
424
|
<p
|
|
388
|
-
v-if="
|
|
425
|
+
v-if="showInternalError"
|
|
389
426
|
:class="[$style.error_message, 'footnote']"
|
|
390
427
|
>
|
|
391
428
|
Please enter a valid card number
|
|
@@ -51,6 +51,7 @@ const emit = defineEmits<{
|
|
|
51
51
|
const internalValue = ref(props.value);
|
|
52
52
|
const inputRef = ref<HTMLInputElement | null>(null);
|
|
53
53
|
const isFocused = ref(false);
|
|
54
|
+
const hasBlurred = ref(false);
|
|
54
55
|
|
|
55
56
|
watch(
|
|
56
57
|
() => props.value,
|
|
@@ -95,11 +96,22 @@ const validationState = computed(() => {
|
|
|
95
96
|
return validateValue(value) ? "valid" : "invalid";
|
|
96
97
|
});
|
|
97
98
|
|
|
99
|
+
const showInternalInvalid = computed(() => {
|
|
100
|
+
return (
|
|
101
|
+
!isFocused.value &&
|
|
102
|
+
hasBlurred.value &&
|
|
103
|
+
validationState.value === "invalid"
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
98
107
|
const computedState = computed(() => {
|
|
108
|
+
if (props.state === "error") {
|
|
109
|
+
return "error";
|
|
110
|
+
}
|
|
99
111
|
if (props.state === "disabled" || props.state === "readonly") {
|
|
100
112
|
return props.state;
|
|
101
113
|
}
|
|
102
|
-
if (
|
|
114
|
+
if (showInternalInvalid.value) return "error";
|
|
103
115
|
return props.state;
|
|
104
116
|
});
|
|
105
117
|
|
|
@@ -113,7 +125,7 @@ const iconColor = computed(() => {
|
|
|
113
125
|
if (validationState.value === "valid") {
|
|
114
126
|
return "black";
|
|
115
127
|
}
|
|
116
|
-
if (
|
|
128
|
+
if (computedState.value === "error") {
|
|
117
129
|
return "var(--input-error-text)";
|
|
118
130
|
}
|
|
119
131
|
switch (computedState.value) {
|
|
@@ -140,9 +152,16 @@ const handleFocus = () => {
|
|
|
140
152
|
|
|
141
153
|
const handleBlur = () => {
|
|
142
154
|
isFocused.value = false;
|
|
155
|
+
hasBlurred.value = true;
|
|
143
156
|
closeList();
|
|
144
157
|
};
|
|
145
158
|
|
|
159
|
+
const showStatusMessage = computed(() => {
|
|
160
|
+
if (validationState.value === "idle") return false;
|
|
161
|
+
if (validationState.value === "valid") return true;
|
|
162
|
+
return showInternalInvalid.value;
|
|
163
|
+
});
|
|
164
|
+
|
|
146
165
|
const focusInput = () => {
|
|
147
166
|
if (
|
|
148
167
|
computedState.value === "disabled" ||
|
|
@@ -265,7 +284,7 @@ onMounted(() => {
|
|
|
265
284
|
$style.button,
|
|
266
285
|
!itemsInDrawer ? $style.button_drawer_open : '',
|
|
267
286
|
validationState === 'valid' ? $style.button_valid : '',
|
|
268
|
-
|
|
287
|
+
computedState === 'error' ? $style.button_error : '',
|
|
269
288
|
isFocused && !hasKnown ? $style.button_focus : '',
|
|
270
289
|
]"
|
|
271
290
|
@click="focusInput"
|
|
@@ -311,7 +330,7 @@ onMounted(() => {
|
|
|
311
330
|
]"
|
|
312
331
|
></div>
|
|
313
332
|
<p
|
|
314
|
-
v-if="
|
|
333
|
+
v-if="showStatusMessage"
|
|
315
334
|
:class="[
|
|
316
335
|
$style.status_message,
|
|
317
336
|
validationState === 'invalid'
|
|
@@ -35,7 +35,8 @@ const internalValue = ref(props.value);
|
|
|
35
35
|
const showSuggestionsList = ref(false);
|
|
36
36
|
const selectedSuggestionIndex = ref(-1);
|
|
37
37
|
const inputRef = ref<HTMLInputElement | null>(null);
|
|
38
|
-
const
|
|
38
|
+
const hasBlurred = ref(false);
|
|
39
|
+
const isFocused = ref(false);
|
|
39
40
|
|
|
40
41
|
// Common email domains for suggestions
|
|
41
42
|
const commonDomains = [
|
|
@@ -193,15 +194,21 @@ const hasTypo = computed(() => {
|
|
|
193
194
|
|
|
194
195
|
const showError = computed(() => {
|
|
195
196
|
return (
|
|
196
|
-
|
|
197
|
+
!isFocused.value &&
|
|
198
|
+
hasBlurred.value &&
|
|
197
199
|
!isValid.value &&
|
|
198
|
-
internalValue.value.length > 0
|
|
199
|
-
(!props.validateOnType || internalValue.value.includes("@"))
|
|
200
|
+
internalValue.value.length > 0
|
|
200
201
|
);
|
|
201
202
|
});
|
|
202
203
|
|
|
203
204
|
// Override state to show error when email is invalid
|
|
204
205
|
const effectiveState = computed(() => {
|
|
206
|
+
if (props.state === "error") {
|
|
207
|
+
return "error";
|
|
208
|
+
}
|
|
209
|
+
if (props.state === "disabled" || props.state === "readonly") {
|
|
210
|
+
return props.state;
|
|
211
|
+
}
|
|
205
212
|
if (showError.value) {
|
|
206
213
|
return "error";
|
|
207
214
|
}
|
|
@@ -245,7 +252,6 @@ const handleInput = (event: Event) => {
|
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
internalValue.value = value;
|
|
248
|
-
hasInteracted.value = true;
|
|
249
255
|
|
|
250
256
|
// Show suggestions when @ is typed
|
|
251
257
|
if (value.includes("@") && props.showSuggestions) {
|
|
@@ -264,7 +270,8 @@ const handleInput = (event: Event) => {
|
|
|
264
270
|
|
|
265
271
|
// Handle blur
|
|
266
272
|
const handleBlur = () => {
|
|
267
|
-
|
|
273
|
+
isFocused.value = false;
|
|
274
|
+
hasBlurred.value = true;
|
|
268
275
|
// Delay hiding suggestions to allow click
|
|
269
276
|
setTimeout(() => {
|
|
270
277
|
showSuggestionsList.value = false;
|
|
@@ -273,6 +280,7 @@ const handleBlur = () => {
|
|
|
273
280
|
|
|
274
281
|
// Handle focus
|
|
275
282
|
const handleFocus = () => {
|
|
283
|
+
isFocused.value = true;
|
|
276
284
|
if (
|
|
277
285
|
internalValue.value.includes("@") &&
|
|
278
286
|
generateSuggestions.value.length > 0
|
|
@@ -350,7 +358,6 @@ const handlePaste = (event: ClipboardEvent) => {
|
|
|
350
358
|
}
|
|
351
359
|
|
|
352
360
|
internalValue.value = cleaned;
|
|
353
|
-
hasInteracted.value = true;
|
|
354
361
|
|
|
355
362
|
emit("update:value", cleaned);
|
|
356
363
|
emit("update:valid", isValidEmail(cleaned));
|