@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
|
@@ -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 |
|
|
@@ -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
|
|