@umbra.ui/core 0.4.5 → 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/DatePicker/DatePicker.vue +6 -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/DatePicker/DatePicker.vue +6 -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
|
@@ -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));
|
|
@@ -28,13 +28,35 @@ const emit = defineEmits<{
|
|
|
28
28
|
const internalValue = ref(props.value);
|
|
29
29
|
const isOutOfRange = ref(false);
|
|
30
30
|
const rangeErrorMessage = ref("");
|
|
31
|
+
const isFocused = ref(false);
|
|
32
|
+
const hasBlurred = ref(false);
|
|
31
33
|
|
|
32
34
|
// Computed property to determine the actual state
|
|
33
35
|
const actualState = computed(() => {
|
|
34
|
-
if (
|
|
36
|
+
if (props.state === "error") return "error";
|
|
37
|
+
if (props.state === "disabled" || props.state === "readonly") {
|
|
38
|
+
return props.state;
|
|
39
|
+
}
|
|
40
|
+
if (
|
|
41
|
+
!isFocused.value &&
|
|
42
|
+
hasBlurred.value &&
|
|
43
|
+
isOutOfRange.value &&
|
|
44
|
+
internalValue.value !== undefined
|
|
45
|
+
) {
|
|
46
|
+
return "error";
|
|
47
|
+
}
|
|
35
48
|
return props.state;
|
|
36
49
|
});
|
|
37
50
|
|
|
51
|
+
const showInternalRangeError = computed(() => {
|
|
52
|
+
return (
|
|
53
|
+
!isFocused.value &&
|
|
54
|
+
hasBlurred.value &&
|
|
55
|
+
isOutOfRange.value &&
|
|
56
|
+
internalValue.value !== undefined
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
38
60
|
// Get the appropriate text color for icons based on state
|
|
39
61
|
const getIconColor = () => {
|
|
40
62
|
return "var(--input-text-filled)";
|
|
@@ -76,6 +98,15 @@ const handleInput = (event: Event) => {
|
|
|
76
98
|
emit("update:value", numValue || 0);
|
|
77
99
|
};
|
|
78
100
|
|
|
101
|
+
const handleFocus = () => {
|
|
102
|
+
isFocused.value = true;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleBlur = () => {
|
|
106
|
+
isFocused.value = false;
|
|
107
|
+
hasBlurred.value = true;
|
|
108
|
+
};
|
|
109
|
+
|
|
79
110
|
const getPlaceholder = computed(() => {
|
|
80
111
|
if (props.state === "readonly") {
|
|
81
112
|
return "Field Cannot Be Edited";
|
|
@@ -95,11 +126,13 @@ const getPlaceholder = computed(() => {
|
|
|
95
126
|
:min="min"
|
|
96
127
|
:max="max"
|
|
97
128
|
@input="handleInput"
|
|
129
|
+
@focus="handleFocus"
|
|
130
|
+
@blur="handleBlur"
|
|
98
131
|
:disabled="state === 'disabled'"
|
|
99
132
|
:readonly="state === 'readonly'"
|
|
100
133
|
/>
|
|
101
134
|
<RippleAnimOutlineIcon
|
|
102
|
-
v-if="state === 'active' && !
|
|
135
|
+
v-if="state === 'active' && !showInternalRangeError"
|
|
103
136
|
:class="$style.icon"
|
|
104
137
|
:size="16"
|
|
105
138
|
:color="getIconColor()"
|
|
@@ -115,7 +148,7 @@ const getPlaceholder = computed(() => {
|
|
|
115
148
|
>
|
|
116
149
|
</div>
|
|
117
150
|
<p
|
|
118
|
-
v-if="
|
|
151
|
+
v-if="props.state === 'error' || showInternalRangeError"
|
|
119
152
|
:class="[$style.error_message, 'footnote']"
|
|
120
153
|
>
|
|
121
154
|
{{ rangeErrorMessage || "Information About The Error" }}
|
|
@@ -34,6 +34,8 @@ const emit = defineEmits<{
|
|
|
34
34
|
const internalValue = ref(props.value);
|
|
35
35
|
const cursorPosition = ref<number | null>(null);
|
|
36
36
|
const inputRef = ref<HTMLInputElement | null>(null);
|
|
37
|
+
const isFocused = ref(false);
|
|
38
|
+
const hasBlurred = ref(false);
|
|
37
39
|
|
|
38
40
|
// Add this computed property to track current digit count
|
|
39
41
|
const currentDigitCount = computed(() => {
|
|
@@ -238,12 +240,32 @@ const currentPlaceholder = computed(() => {
|
|
|
238
240
|
|
|
239
241
|
// Override state to show error when phone number is invalid
|
|
240
242
|
const effectiveState = computed(() => {
|
|
241
|
-
if (
|
|
243
|
+
if (props.state === "error") {
|
|
244
|
+
return "error";
|
|
245
|
+
}
|
|
246
|
+
if (props.state === "disabled" || props.state === "readonly") {
|
|
247
|
+
return props.state;
|
|
248
|
+
}
|
|
249
|
+
if (
|
|
250
|
+
!isFocused.value &&
|
|
251
|
+
hasBlurred.value &&
|
|
252
|
+
!isValid.value &&
|
|
253
|
+
internalValue.value.length > 0
|
|
254
|
+
) {
|
|
242
255
|
return "error";
|
|
243
256
|
}
|
|
244
257
|
return props.state;
|
|
245
258
|
});
|
|
246
259
|
|
|
260
|
+
const showInternalError = computed(() => {
|
|
261
|
+
return (
|
|
262
|
+
!isFocused.value &&
|
|
263
|
+
hasBlurred.value &&
|
|
264
|
+
!isValid.value &&
|
|
265
|
+
internalValue.value.length > 0
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
247
269
|
// Get the appropriate text color for icons based on state
|
|
248
270
|
const getIconColor = () => {
|
|
249
271
|
return "var(--input-text-filled)";
|
|
@@ -424,6 +446,15 @@ const getPlaceholder = computed(() => {
|
|
|
424
446
|
}
|
|
425
447
|
return currentPlaceholder.value;
|
|
426
448
|
});
|
|
449
|
+
|
|
450
|
+
const handleFocus = () => {
|
|
451
|
+
isFocused.value = true;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const handleBlur = () => {
|
|
455
|
+
isFocused.value = false;
|
|
456
|
+
hasBlurred.value = true;
|
|
457
|
+
};
|
|
427
458
|
</script>
|
|
428
459
|
|
|
429
460
|
<template>
|
|
@@ -439,6 +470,8 @@ const getPlaceholder = computed(() => {
|
|
|
439
470
|
@input="handleInput"
|
|
440
471
|
@paste="handlePaste"
|
|
441
472
|
@keydown="handleKeyDown"
|
|
473
|
+
@focus="handleFocus"
|
|
474
|
+
@blur="handleBlur"
|
|
442
475
|
:maxlength="20"
|
|
443
476
|
:disabled="state === 'disabled'"
|
|
444
477
|
:readonly="state === 'readonly'"
|
|
@@ -446,7 +479,7 @@ const getPlaceholder = computed(() => {
|
|
|
446
479
|
<PhoneIcon v-if="!internalValue" :size="16" />
|
|
447
480
|
<transition name="fade">
|
|
448
481
|
<div
|
|
449
|
-
v-if="
|
|
482
|
+
v-if="showInternalError"
|
|
450
483
|
:class="$style.error_icon"
|
|
451
484
|
>
|
|
452
485
|
<TriangleWarningIcon size="16" />
|
|
@@ -455,7 +488,7 @@ const getPlaceholder = computed(() => {
|
|
|
455
488
|
</div>
|
|
456
489
|
<transition name="slide-fade">
|
|
457
490
|
<p
|
|
458
|
-
v-if="
|
|
491
|
+
v-if="showInternalError"
|
|
459
492
|
:class="[$style.error_message, 'footnote']"
|
|
460
493
|
>
|
|
461
494
|
Please enter a valid phone number
|
|
@@ -77,6 +77,7 @@ const showRequirementsUI = computed(() => {
|
|
|
77
77
|
const showInput = ref<boolean>(false);
|
|
78
78
|
const internalValue = ref(props.value);
|
|
79
79
|
const isFocused = ref(false);
|
|
80
|
+
const hasBlurred = ref(false);
|
|
80
81
|
const inputRef = ref<HTMLInputElement | null>(null);
|
|
81
82
|
|
|
82
83
|
// Common weak passwords to check against
|
|
@@ -210,12 +211,32 @@ const strengthIndicator = computed(() => {
|
|
|
210
211
|
|
|
211
212
|
// Override state to show error when password is invalid
|
|
212
213
|
const effectiveState = computed(() => {
|
|
213
|
-
if (
|
|
214
|
+
if (props.state === "error") {
|
|
215
|
+
return "error";
|
|
216
|
+
}
|
|
217
|
+
if (props.state === "disabled" || props.state === "readonly") {
|
|
218
|
+
return props.state;
|
|
219
|
+
}
|
|
220
|
+
if (
|
|
221
|
+
!isFocused.value &&
|
|
222
|
+
hasBlurred.value &&
|
|
223
|
+
internalValue.value &&
|
|
224
|
+
!isValid.value
|
|
225
|
+
) {
|
|
214
226
|
return "error";
|
|
215
227
|
}
|
|
216
228
|
return props.state;
|
|
217
229
|
});
|
|
218
230
|
|
|
231
|
+
const showInternalError = computed(() => {
|
|
232
|
+
return (
|
|
233
|
+
!isFocused.value &&
|
|
234
|
+
hasBlurred.value &&
|
|
235
|
+
!!internalValue.value &&
|
|
236
|
+
!isValid.value
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
219
240
|
// Get the appropriate text color for icons based on state
|
|
220
241
|
const getIconColor = () => {
|
|
221
242
|
return "var(--input-text-filled)";
|
|
@@ -299,6 +320,7 @@ const handleFocus = () => {
|
|
|
299
320
|
};
|
|
300
321
|
|
|
301
322
|
const handleBlur = () => {
|
|
323
|
+
hasBlurred.value = true;
|
|
302
324
|
// Delay to allow clicks on requirements
|
|
303
325
|
setTimeout(() => {
|
|
304
326
|
isFocused.value = false;
|
|
@@ -411,7 +433,9 @@ const getPlaceholder = computed(() => {
|
|
|
411
433
|
<!-- Requirements list -->
|
|
412
434
|
<transition name="slide-fade">
|
|
413
435
|
<div
|
|
414
|
-
v-if="
|
|
436
|
+
v-if="
|
|
437
|
+
showRequirementsUI && (isFocused || showInternalError) && internalValue
|
|
438
|
+
"
|
|
415
439
|
:class="$style.requirements"
|
|
416
440
|
>
|
|
417
441
|
<div
|
|
@@ -442,7 +466,7 @@ const getPlaceholder = computed(() => {
|
|
|
442
466
|
|
|
443
467
|
<transition name="slide-fade">
|
|
444
468
|
<p
|
|
445
|
-
v-if="
|
|
469
|
+
v-if="showInternalError && customValidationMessage"
|
|
446
470
|
:class="[$style.error_message, 'footnote']"
|
|
447
471
|
>
|
|
448
472
|
{{ customValidationMessage }}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ref,
|
|
4
|
+
onMounted,
|
|
5
|
+
nextTick,
|
|
6
|
+
onUnmounted,
|
|
7
|
+
useSlots,
|
|
8
|
+
computed,
|
|
9
|
+
} from "vue";
|
|
3
10
|
import { icons } from "@umbra.ui/icons";
|
|
4
11
|
import { IconButton } from "@umbra.ui/core";
|
|
5
12
|
import {
|
|
@@ -24,6 +31,7 @@ export interface Props {
|
|
|
24
31
|
items: CollectionItem[];
|
|
25
32
|
selectedItem: CollectionItem | null;
|
|
26
33
|
loading?: boolean;
|
|
34
|
+
label?: boolean;
|
|
27
35
|
// Customizable labels
|
|
28
36
|
buttonLabel?: string;
|
|
29
37
|
headerLabel?: string;
|
|
@@ -40,6 +48,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
40
48
|
items: () => [],
|
|
41
49
|
selectedItem: null,
|
|
42
50
|
loading: false,
|
|
51
|
+
label: true,
|
|
43
52
|
buttonLabel: "Select Item",
|
|
44
53
|
headerLabel: "Choose an Item",
|
|
45
54
|
newItemLabel: "New Item",
|
|
@@ -48,6 +57,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
48
57
|
allowCreate: true,
|
|
49
58
|
placeholder: "Enter name...",
|
|
50
59
|
});
|
|
60
|
+
const slots = useSlots();
|
|
61
|
+
const hasCustomLabelSlot = computed(() => Boolean(slots.label));
|
|
51
62
|
|
|
52
63
|
// - Emits (Pure Actions)
|
|
53
64
|
const emits = defineEmits<{
|
|
@@ -190,17 +201,21 @@ const getIconComponent = (iconName: string) => {
|
|
|
190
201
|
<div
|
|
191
202
|
:class="[
|
|
192
203
|
$style.button,
|
|
193
|
-
|
|
204
|
+
hasCustomLabelSlot ? $style.button_unstyled : null,
|
|
205
|
+
!hasCustomLabelSlot &&
|
|
206
|
+
(showPopover ? $style.button_selected : $style.button_normal),
|
|
194
207
|
]"
|
|
195
208
|
@click="togglePopover"
|
|
196
209
|
ref="button"
|
|
197
210
|
>
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
211
|
+
<slot v-if="label" name="label">
|
|
212
|
+
<p v-if="selectedItem" class="callout" :class="$style.button_label">
|
|
213
|
+
{{ selectedItem.title }}
|
|
214
|
+
</p>
|
|
215
|
+
<p v-else class="callout" :class="$style.button_label">
|
|
216
|
+
{{ buttonLabel }}
|
|
217
|
+
</p>
|
|
218
|
+
</slot>
|
|
204
219
|
</div>
|
|
205
220
|
|
|
206
221
|
<!-- Teleport the overlay and picker to body -->
|
|
@@ -278,10 +293,17 @@ const getIconComponent = (iconName: string) => {
|
|
|
278
293
|
transition: padding-left 0.3s, padding-right 0.3s, background-color 0.3s,
|
|
279
294
|
box-shadow 0.3s;
|
|
280
295
|
cursor: default;
|
|
296
|
+
}
|
|
297
|
+
.button_unstyled {
|
|
298
|
+
padding: 0;
|
|
299
|
+
gap: 0;
|
|
300
|
+
transition: none;
|
|
301
|
+
}
|
|
302
|
+
.button_normal {
|
|
281
303
|
background-color: var(--picker-button-bg);
|
|
282
304
|
border: var(--picker-button-border);
|
|
283
305
|
}
|
|
284
|
-
.
|
|
306
|
+
.button_normal:hover {
|
|
285
307
|
background-color: var(--picker-button-hover-bg);
|
|
286
308
|
padding-left: 0.588rem;
|
|
287
309
|
padding-right: 0.588rem;
|
|
@@ -82,6 +82,55 @@ const handleItemCreate = (title: string) => {
|
|
|
82
82
|
</style>
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
## Custom Label Slot
|
|
86
|
+
|
|
87
|
+
When you provide a custom `label` slot, the default trigger visuals are removed so
|
|
88
|
+
you can fully control the trigger content, styling, and animation behavior.
|
|
89
|
+
|
|
90
|
+
```vue
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { computed, ref } from "vue";
|
|
93
|
+
import { CollectionPicker } from "@umbra-ui/core";
|
|
94
|
+
import type { CollectionItem } from "@umbra-ui/core";
|
|
95
|
+
|
|
96
|
+
const items = ref<CollectionItem[]>([
|
|
97
|
+
{ id: "1", title: "Roadmap" },
|
|
98
|
+
{ id: "2", title: "Backlog" },
|
|
99
|
+
]);
|
|
100
|
+
const selectedItem = ref<CollectionItem | null>(items.value[0]);
|
|
101
|
+
const triggerLabel = computed(() => selectedItem.value?.title ?? "Select collection");
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<template>
|
|
105
|
+
<CollectionPicker v-model:selected-item="selectedItem" :items="items">
|
|
106
|
+
<template #label>
|
|
107
|
+
<span class="collection-chip">{{ triggerLabel }}</span>
|
|
108
|
+
</template>
|
|
109
|
+
</CollectionPicker>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<style module>
|
|
113
|
+
.collection-chip {
|
|
114
|
+
display: inline-flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
padding: 0.353rem 0.588rem;
|
|
117
|
+
border-radius: 999px;
|
|
118
|
+
background: var(--background-dark, #111827);
|
|
119
|
+
color: var(--text-light, #ffffff);
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Hiding the Label
|
|
125
|
+
|
|
126
|
+
```vue
|
|
127
|
+
<CollectionPicker
|
|
128
|
+
v-model:selected-item="selectedItem"
|
|
129
|
+
:items="items"
|
|
130
|
+
:label="false"
|
|
131
|
+
/>
|
|
132
|
+
```
|
|
133
|
+
|
|
85
134
|
## Props
|
|
86
135
|
|
|
87
136
|
| Prop Name | Type | Required | Default | Description |
|
|
@@ -89,6 +138,7 @@ const handleItemCreate = (title: string) => {
|
|
|
89
138
|
| `items` | `CollectionItem[]` | Yes | `[]` | Array of items to display in the picker |
|
|
90
139
|
| `selectedItem` | `CollectionItem \| null` | No | `null` | Currently selected item |
|
|
91
140
|
| `loading` | `boolean` | No | `false` | Whether the picker is in loading state |
|
|
141
|
+
| `label` | `boolean` | No | `true` | Show or hide the trigger label/slot |
|
|
92
142
|
| `buttonLabel` | `string` | No | `"Select Item"` | Label for the trigger button |
|
|
93
143
|
| `headerLabel` | `string` | No | `"Choose an Item"` | Header text in the picker |
|
|
94
144
|
| `newItemLabel` | `string` | No | `"New Item"` | Label for the create new item option |
|