@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.
Files changed (74) hide show
  1. package/dist/components/inputs/InputCard/InputCard.vue +46 -9
  2. package/dist/components/inputs/InputCryptoAddress/InputCryptoAddress.vue +23 -4
  3. package/dist/components/inputs/InputEmail/InputEmail.vue +14 -7
  4. package/dist/components/inputs/InputNumber/InputNumber.vue +36 -3
  5. package/dist/components/inputs/InputPhone/InputPhone.vue +36 -3
  6. package/dist/components/inputs/InputSecure/InputSecure.vue +27 -3
  7. package/dist/components/pickers/CollectionPicker/CollectionPicker.vue +31 -9
  8. package/dist/components/pickers/CollectionPicker/README.md +50 -0
  9. package/dist/components/pickers/ColorPicker/ColorPicker.vue +27 -4
  10. package/dist/components/pickers/ColorPicker/README.md +50 -0
  11. package/dist/components/pickers/IconPicker/IconPicker.vue +53 -26
  12. package/dist/components/pickers/IconPicker/README.md +43 -0
  13. package/package.json +5 -4
  14. package/src/components/inputs/InputCard/InputCard.vue +46 -9
  15. package/src/components/inputs/InputCryptoAddress/InputCryptoAddress.vue +23 -4
  16. package/src/components/inputs/InputEmail/InputEmail.vue +14 -7
  17. package/src/components/inputs/InputNumber/InputNumber.vue +36 -3
  18. package/src/components/inputs/InputPhone/InputPhone.vue +36 -3
  19. package/src/components/inputs/InputSecure/InputSecure.vue +27 -3
  20. package/src/components/pickers/CollectionPicker/CollectionPicker.vue +31 -9
  21. package/src/components/pickers/CollectionPicker/README.md +50 -0
  22. package/src/components/pickers/ColorPicker/ColorPicker.vue +27 -4
  23. package/src/components/pickers/ColorPicker/README.md +50 -0
  24. package/src/components/pickers/IconPicker/IconPicker.vue +53 -26
  25. package/src/components/pickers/IconPicker/README.md +43 -0
  26. package/src/skills/README.md +108 -0
  27. package/src/skills/umbra.ui-colors-application/SKILL.md +102 -0
  28. package/src/skills/umbra.ui-colors-application/reference.md +178 -0
  29. package/src/skills/umbra.ui-control-button/SKILL.md +34 -0
  30. package/src/skills/umbra.ui-control-checkbox/SKILL.md +34 -0
  31. package/src/skills/umbra.ui-control-dropdown/SKILL.md +34 -0
  32. package/src/skills/umbra.ui-control-icon-button/SKILL.md +33 -0
  33. package/src/skills/umbra.ui-control-inline-dropdown/SKILL.md +32 -0
  34. package/src/skills/umbra.ui-control-radio/SKILL.md +33 -0
  35. package/src/skills/umbra.ui-control-range-slider/SKILL.md +33 -0
  36. package/src/skills/umbra.ui-control-segmented-control/SKILL.md +33 -0
  37. package/src/skills/umbra.ui-control-slider/SKILL.md +33 -0
  38. package/src/skills/umbra.ui-control-stepper/SKILL.md +33 -0
  39. package/src/skills/umbra.ui-control-switch/SKILL.md +33 -0
  40. package/src/skills/umbra.ui-dialog-alert/SKILL.md +34 -0
  41. package/src/skills/umbra.ui-dialog-toast/SKILL.md +35 -0
  42. package/src/skills/umbra.ui-indicator-progress-bar/SKILL.md +33 -0
  43. package/src/skills/umbra.ui-indicator-tooltip/SKILL.md +33 -0
  44. package/src/skills/umbra.ui-input-card/SKILL.md +42 -0
  45. package/src/skills/umbra.ui-input-card/reference.md +36 -0
  46. package/src/skills/umbra.ui-input-crypto-address/SKILL.md +40 -0
  47. package/src/skills/umbra.ui-input-crypto-address/reference.md +40 -0
  48. package/src/skills/umbra.ui-input-email/SKILL.md +45 -0
  49. package/src/skills/umbra.ui-input-number/SKILL.md +39 -0
  50. package/src/skills/umbra.ui-input-otp/SKILL.md +44 -0
  51. package/src/skills/umbra.ui-input-phone/SKILL.md +45 -0
  52. package/src/skills/umbra.ui-input-phone/reference.md +35 -0
  53. package/src/skills/umbra.ui-input-search/SKILL.md +43 -0
  54. package/src/skills/umbra.ui-input-search/reference.md +45 -0
  55. package/src/skills/umbra.ui-input-secure/SKILL.md +43 -0
  56. package/src/skills/umbra.ui-input-secure/reference.md +44 -0
  57. package/src/skills/umbra.ui-input-string-capture/SKILL.md +41 -0
  58. package/src/skills/umbra.ui-input-tags/SKILL.md +46 -0
  59. package/src/skills/umbra.ui-input-tags/reference.md +44 -0
  60. package/src/skills/umbra.ui-input-text/SKILL.md +46 -0
  61. package/src/skills/umbra.ui-menu-action-menu/SKILL.md +34 -0
  62. package/src/skills/umbra.ui-model-popover/SKILL.md +35 -0
  63. package/src/skills/umbra.ui-model-sheet/SKILL.md +35 -0
  64. package/src/skills/umbra.ui-model-sidebar/SKILL.md +35 -0
  65. package/src/skills/umbra.ui-picker-collection/SKILL.md +36 -0
  66. package/src/skills/umbra.ui-picker-collection/reference.md +34 -0
  67. package/src/skills/umbra.ui-picker-color/SKILL.md +36 -0
  68. package/src/skills/umbra.ui-picker-color/reference.md +34 -0
  69. package/src/skills/umbra.ui-picker-date/SKILL.md +36 -0
  70. package/src/skills/umbra.ui-picker-date/reference.md +26 -0
  71. package/src/skills/umbra.ui-picker-file/SKILL.md +42 -0
  72. package/src/skills/umbra.ui-picker-file/reference.md +39 -0
  73. package/src/skills/umbra.ui-picker-icon/SKILL.md +36 -0
  74. 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 (isOutOfRange.value) return "error";
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' && !isOutOfRange"
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="actualState === 'error'"
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 (!isValid.value && internalValue.value.length > 0) {
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="!isValid && internalValue.length > 0"
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="!isValid && internalValue.length > 0"
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 (internalValue.value && !isValid.value) {
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="showRequirementsUI && (isFocused || !isValid) && internalValue"
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="!isValid && isFocused && customValidationMessage"
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 { ref, onMounted, nextTick, onUnmounted } from "vue";
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
- showPopover ? $style.button_selected : $style.button_normal,
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
- <p v-if="selectedItem" class="callout" :class="$style.button_label">
199
- {{ selectedItem.title }}
200
- </p>
201
- <p v-else class="callout" :class="$style.button_label">
202
- {{ buttonLabel }}
203
- </p>
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
- .button:hover {
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 { ref, watch, onBeforeUnmount, nextTick, computed } from "vue";
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
- showPopover ? $style.button_selected : $style.button_normal,
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
- <div :style="dotStyles" :class="$style.dot"></div>
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
- .button:hover {
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 { ref, watch, onBeforeUnmount, nextTick, computed } from "vue";
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="[$style.button, { [$style.active]: showPopover }]"
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
- <component
147
- v-if="selectedIcon && icons[selectedIcon as IconKey]"
148
- :is="icons[selectedIcon as IconKey]"
149
- :size="iconSize"
150
- :color="selectedColor"
151
- />
152
- <component
153
- v-else
154
- :is="icons.folder"
155
- :size="iconSize"
156
- :color="selectedColor"
157
- />
158
- <ChevronDownIcon
159
- :size="selectedSize - 4"
160
- :class="$style.chevron"
161
- :color="selectedColor"
162
- :style="{
163
- transform: `rotate(${showPopover ? 0 : -90}deg)`,
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
- .button:hover,
237
- .button.active {
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
- .button:hover .chevron,
252
- .button.active .chevron {
278
+ .button_normal:hover .chevron,
279
+ .button_selected .chevron {
253
280
  opacity: 1;
254
281
  }
255
282