@witchcraft/ui 0.0.1 → 0.1.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 (155) hide show
  1. package/README.md +18 -28
  2. package/dist/module.d.mts +3 -1
  3. package/dist/module.d.ts +3 -1
  4. package/dist/module.json +2 -2
  5. package/dist/module.mjs +20 -11
  6. package/dist/runtime/assets/base.css +1 -1
  7. package/dist/runtime/assets/locales/en.json +2 -2
  8. package/dist/runtime/assets/tailwind.css +1 -1
  9. package/dist/runtime/assets/utils.css +1 -0
  10. package/dist/runtime/build/WitchcraftUiResolver.js +1 -1
  11. package/dist/runtime/components/Icon/Icon.vue +10 -5
  12. package/dist/runtime/components/LibButton/LibButton.vue +41 -46
  13. package/dist/runtime/components/LibCheckbox/LibCheckbox.vue +7 -3
  14. package/dist/runtime/components/LibColorInput/LibColorInput.vue +111 -36
  15. package/dist/runtime/components/LibColorPicker/LibColorPicker.stories.d.ts +2 -0
  16. package/dist/runtime/components/LibColorPicker/LibColorPicker.stories.js +26 -9
  17. package/dist/runtime/components/LibColorPicker/LibColorPicker.vue +242 -131
  18. package/dist/runtime/components/LibColorPicker/utils/safeConvertToHsva.d.ts +2 -0
  19. package/dist/runtime/components/LibColorPicker/utils/safeConvertToHsva.js +18 -0
  20. package/dist/runtime/components/LibColorPicker/utils/safeConvertToRgba.d.ts +2 -0
  21. package/dist/runtime/components/LibColorPicker/utils/safeConvertToRgba.js +17 -0
  22. package/dist/runtime/components/LibColorPicker/utils/toLowPrecisionRgbaString.d.ts +2 -0
  23. package/dist/runtime/components/LibColorPicker/utils/toLowPrecisionRgbaString.js +8 -0
  24. package/dist/runtime/components/LibColorPicker/utils/truncate.d.ts +1 -0
  25. package/dist/runtime/components/LibColorPicker/utils/truncate.js +5 -0
  26. package/dist/runtime/components/LibDarkModeSwitcher/LibDarkModeSwitcher.stories.js +1 -1
  27. package/dist/runtime/components/LibDarkModeSwitcher/LibDarkModeSwitcher.vue +11 -8
  28. package/dist/runtime/components/LibDatePicker/LibDatePicker.vue +4 -17
  29. package/dist/runtime/components/LibDatePicker/LibRangeDatePicker.vue +192 -131
  30. package/dist/runtime/components/LibDatePicker/LibSingleDatePicker.vue +183 -115
  31. package/dist/runtime/components/LibDatePicker/LibTimeZonePicker.vue +3 -3
  32. package/dist/runtime/components/LibDebug/LibDebug.vue +15 -5
  33. package/dist/runtime/components/LibDevOnly/LibDevOnly.vue +1 -3
  34. package/dist/runtime/components/LibFileInput/LibFileInput.vue +54 -28
  35. package/dist/runtime/components/{LibInput/LibInput.stories.d.ts → LibInputDeprecated/LibInputDeprecated.stories.d.ts} +6 -6
  36. package/dist/runtime/components/{LibInput/LibInput.stories.js → LibInputDeprecated/LibInputDeprecated.stories.js} +64 -19
  37. package/{src/runtime/components/LibInput/LibInput.vue → dist/runtime/components/LibInputDeprecated/LibInputDeprecated.vue} +40 -33
  38. package/dist/runtime/components/LibLabel/LibLabel.vue +2 -2
  39. package/dist/runtime/components/LibMultiValues/LibMultiValues.stories.d.ts +1 -1
  40. package/dist/runtime/components/LibMultiValues/LibMultiValues.stories.js +5 -4
  41. package/dist/runtime/components/LibMultiValues/LibMultiValues.vue +11 -12
  42. package/dist/runtime/components/LibNotifications/LibNotification.vue +19 -10
  43. package/dist/runtime/components/LibNotifications/LibNotifications.stories.js +2 -2
  44. package/dist/runtime/components/LibNotifications/LibNotifications.vue +20 -11
  45. package/dist/runtime/components/LibPagination/LibPagination.stories.js +2 -2
  46. package/dist/runtime/components/LibPagination/LibPagination.vue +19 -19
  47. package/dist/runtime/components/LibPalette/LibPalette.vue +3 -3
  48. package/dist/runtime/components/LibPopup/LibPopup.stories.js +2 -2
  49. package/dist/runtime/components/LibPopup/LibPopup.vue +30 -66
  50. package/dist/runtime/components/LibProgressBar/LibProgressBar.vue +3 -1
  51. package/dist/runtime/components/LibRecorder/LibRecorder.vue +2 -2
  52. package/dist/runtime/components/LibRoot/LibRoot.vue +14 -1
  53. package/dist/runtime/components/LibSimpleInput/LibSimpleInput.stories.js +1 -1
  54. package/dist/runtime/components/LibSimpleInput/LibSimpleInput.vue +5 -7
  55. package/dist/runtime/components/LibSuggestions/LibSuggestions.vue +42 -25
  56. package/dist/runtime/components/LibTable/LibTable.vue +8 -8
  57. package/dist/runtime/components/Scrolling.stories.d.ts +6 -0
  58. package/dist/runtime/components/Scrolling.stories.js +44 -0
  59. package/dist/runtime/components/Template/NAME.vue +1 -1
  60. package/dist/runtime/components/TestControls/TestControls.vue +1 -1
  61. package/dist/runtime/components/index.d.ts +12 -11
  62. package/dist/runtime/components/index.js +12 -11
  63. package/dist/runtime/components/shared/props.d.ts +81 -16
  64. package/dist/runtime/components/shared/storyHelpers/playInput.js +5 -5
  65. package/dist/runtime/components/shared/storyHelpers/playSuggestions.js +15 -11
  66. package/dist/runtime/composables/index.d.ts +5 -0
  67. package/dist/runtime/composables/index.js +5 -0
  68. package/dist/runtime/composables/useDivideAttrs.js +1 -0
  69. package/dist/runtime/composables/useDragWithThreshold.d.ts +71 -0
  70. package/dist/runtime/composables/useDragWithThreshold.js +40 -0
  71. package/dist/runtime/composables/usePreHydrationValue.d.ts +12 -0
  72. package/dist/runtime/composables/usePreHydrationValue.js +15 -0
  73. package/dist/runtime/composables/useSetupI18n.d.ts +2 -0
  74. package/dist/runtime/composables/useSetupI18n.js +5 -1
  75. package/dist/runtime/composables/useSuggestions.d.ts +7 -5
  76. package/dist/runtime/composables/useSuggestions.js +94 -57
  77. package/dist/runtime/directives/vResizableCols.js +3 -1
  78. package/dist/runtime/helpers/NotificationHandler.d.ts +5 -0
  79. package/dist/runtime/helpers/index.d.ts +3 -1
  80. package/dist/runtime/helpers/index.js +3 -1
  81. package/dist/runtime/types/index.d.ts +6 -0
  82. package/dist/runtime/utils/notifyIfError.d.ts +14 -0
  83. package/dist/runtime/utils/notifyIfError.js +29 -0
  84. package/package.json +18 -20
  85. package/src/module.ts +31 -12
  86. package/src/runtime/assets/base.css +10 -1
  87. package/src/runtime/assets/locales/en.json +2 -2
  88. package/src/runtime/assets/tailwind.css +1 -1
  89. package/src/runtime/assets/{style.css → utils.css} +86 -4
  90. package/src/runtime/build/WitchcraftUiResolver.ts +1 -1
  91. package/src/runtime/components/Icon/Icon.vue +10 -5
  92. package/src/runtime/components/LibButton/LibButton.vue +41 -46
  93. package/src/runtime/components/LibCheckbox/LibCheckbox.vue +7 -3
  94. package/src/runtime/components/LibColorInput/LibColorInput.vue +111 -36
  95. package/src/runtime/components/LibColorPicker/LibColorPicker.stories.ts +25 -4
  96. package/src/runtime/components/LibColorPicker/LibColorPicker.vue +242 -131
  97. package/src/runtime/components/LibColorPicker/utils/safeConvertToHsva.ts +25 -0
  98. package/src/runtime/components/LibColorPicker/utils/safeConvertToRgba.ts +23 -0
  99. package/src/runtime/components/LibColorPicker/utils/toLowPrecisionRgbaString.ts +13 -0
  100. package/src/runtime/components/LibColorPicker/utils/truncate.ts +6 -0
  101. package/src/runtime/components/LibDarkModeSwitcher/LibDarkModeSwitcher.stories.ts +1 -1
  102. package/src/runtime/components/LibDarkModeSwitcher/LibDarkModeSwitcher.vue +11 -8
  103. package/src/runtime/components/LibDatePicker/LibDatePicker.vue +4 -17
  104. package/src/runtime/components/LibDatePicker/LibRangeDatePicker.vue +192 -131
  105. package/src/runtime/components/LibDatePicker/LibSingleDatePicker.vue +183 -115
  106. package/src/runtime/components/LibDatePicker/LibTimeZonePicker.vue +3 -3
  107. package/src/runtime/components/LibDebug/LibDebug.vue +15 -5
  108. package/src/runtime/components/LibDevOnly/LibDevOnly.vue +1 -3
  109. package/src/runtime/components/LibFileInput/LibFileInput.vue +54 -28
  110. package/src/runtime/components/{LibInput/LibInput.stories.ts → LibInputDeprecated/LibInputDeprecated.stories.ts} +64 -19
  111. package/{dist/runtime/components/LibInput/LibInput.vue → src/runtime/components/LibInputDeprecated/LibInputDeprecated.vue} +40 -33
  112. package/src/runtime/components/LibLabel/LibLabel.vue +2 -2
  113. package/src/runtime/components/LibMultiValues/LibMultiValues.stories.ts +5 -4
  114. package/src/runtime/components/LibMultiValues/LibMultiValues.vue +11 -12
  115. package/src/runtime/components/LibNotifications/LibNotification.vue +19 -10
  116. package/src/runtime/components/LibNotifications/LibNotifications.stories.ts +2 -2
  117. package/src/runtime/components/LibNotifications/LibNotifications.vue +20 -11
  118. package/src/runtime/components/LibPagination/LibPagination.stories.ts +2 -2
  119. package/src/runtime/components/LibPagination/LibPagination.vue +19 -19
  120. package/src/runtime/components/LibPalette/LibPalette.vue +3 -3
  121. package/src/runtime/components/LibPopup/LibPopup.stories.ts +2 -2
  122. package/src/runtime/components/LibPopup/LibPopup.vue +30 -66
  123. package/src/runtime/components/LibProgressBar/LibProgressBar.vue +3 -1
  124. package/src/runtime/components/LibRecorder/LibRecorder.vue +2 -2
  125. package/src/runtime/components/LibRoot/LibRoot.vue +14 -1
  126. package/src/runtime/components/LibSimpleInput/LibSimpleInput.stories.ts +1 -1
  127. package/src/runtime/components/LibSimpleInput/LibSimpleInput.vue +5 -7
  128. package/src/runtime/components/LibSuggestions/LibSuggestions.vue +42 -25
  129. package/src/runtime/components/LibTable/LibTable.vue +8 -8
  130. package/src/runtime/components/Scrolling.stories.ts +58 -0
  131. package/src/runtime/components/Template/NAME.vue +1 -1
  132. package/src/runtime/components/TestControls/TestControls.vue +1 -1
  133. package/src/runtime/components/index.ts +12 -12
  134. package/src/runtime/components/shared/props.ts +82 -19
  135. package/src/runtime/components/shared/storyHelpers/playInput.ts +6 -5
  136. package/src/runtime/components/shared/storyHelpers/playSuggestions.ts +25 -11
  137. package/src/runtime/composables/index.ts +5 -0
  138. package/src/runtime/composables/useDarkMode.ts +2 -2
  139. package/src/runtime/composables/useDivideAttrs.ts +1 -0
  140. package/src/runtime/composables/useDragWithThreshold.ts +108 -0
  141. package/src/runtime/composables/usePreHydrationValue.ts +30 -0
  142. package/src/runtime/composables/useSetupI18n.ts +8 -2
  143. package/src/runtime/composables/useSuggestions.ts +92 -45
  144. package/src/runtime/directives/vResizableCols.ts +3 -1
  145. package/src/runtime/helpers/NotificationHandler.ts +5 -0
  146. package/src/runtime/helpers/index.ts +3 -1
  147. package/src/runtime/types/index.ts +5 -0
  148. package/src/runtime/utils/notifyIfError.ts +45 -0
  149. package/dist/runtime/assets/style.css +0 -1
  150. package/dist/runtime/helpers/addValue.d.ts +0 -1
  151. package/dist/runtime/helpers/addValue.js +0 -8
  152. package/src/runtime/helpers/addValue.ts +0 -10
  153. /package/dist/runtime/components/{reset.stories.d.ts → Reset.stories.d.ts} +0 -0
  154. /package/dist/runtime/components/{reset.stories.js → Reset.stories.js} +0 -0
  155. /package/src/runtime/components/{reset.stories.ts → Reset.stories.ts} +0 -0
@@ -1,5 +1,6 @@
1
1
  <template>
2
- <div :id="id ?? fallbackId"
2
+ <div
3
+ :id="id ?? fallbackId"
3
4
  :aria-label="t('color-picker.aria')"
4
5
  :class="twMerge(`color-picker
5
6
  [--slider-size:calc(var(--spacing)_*_4)]
@@ -20,11 +21,12 @@
20
21
  `,
21
22
  border && `
22
23
  border rounded-sm border-neutral-600
23
- `
24
+ `,
25
+ ($attrs as any)?.class
24
26
  )"
25
27
  >
26
28
  <div
27
- :class="`picker
29
+ :class="`color-picker--all-picker
28
30
  no-touch-action
29
31
  w-full
30
32
  aspect-square
@@ -33,7 +35,7 @@
33
35
  rounded-sm
34
36
  focus:border-accent-500
35
37
  `"
36
- @pointerdown="slider.pointerdown($event, 'picker')"
38
+ @pointerdown="slider.pointerdown($event, 'all')"
37
39
  @pointerleave="slider.pointerleave($event)"
38
40
  >
39
41
  <canvas
@@ -43,8 +45,10 @@
43
45
  <div
44
46
  aria-live="assertive"
45
47
  :aria-description="ariaDescription"
48
+ :aria-label="`${t('color-picker.aria.saturation')}: ${localColor.percent.s}, ${t('color-picker.aria.value')}: ${localColor.percent.s}`"
46
49
  :class="`
47
- handle ${handleClasses}
50
+ color-picker--all-handle
51
+ ${handleClasses}
48
52
  border-[var(--fg)]
49
53
  hover:shadow-black
50
54
  active:shadow-black
@@ -53,15 +57,13 @@
53
57
  :style="`
54
58
  left: calc(${localColor.percent.s}% - var(--slider-size)/2);
55
59
  top: calc(${localColor.percent.v}% - var(--slider-size)/2);
56
- background: ${localColorStringOpaque};
60
+ background: ${asRgbaString};
57
61
  `"
58
- @keydown="slider.keydown($event, 'picker')"
59
- >
60
- <aria :value="`${t('color-picker.aria.saturation')}: ${localColor.percent.s}, ${t('color-picker.aria.value')}: ${localColor.percent.s}`"/>
61
- </div>
62
+ @keydown="slider.keydown($event, 'all')"
63
+ />
62
64
  </div>
63
65
  <div
64
- :class="`hue-slider ${sliderClasses}`"
66
+ :class="`color-picker--hue-slider ${sliderClasses}`"
65
67
  @pointerdown="slider.pointerdown($event, 'hue')"
66
68
  >
67
69
  <canvas
@@ -76,7 +78,10 @@
76
78
  :aria-label="t('color-picker.aria.hue')"
77
79
  :aria-description="ariaDescription"
78
80
  tabindex="0"
79
- :class="`handle ${handleClasses} bg-neutral-50`"
81
+ :class="`
82
+ color-picker--hue-handle
83
+ ${handleClasses}
84
+ `"
80
85
  :style="`left: calc(${localColor.percent.h}% - var(--slider-size)/2)`"
81
86
  @keydown="slider.keydown($event, 'hue')"
82
87
  />
@@ -84,8 +89,10 @@
84
89
  <div
85
90
  v-if="allowAlpha"
86
91
 
87
- :class="`alpha-slider ${sliderClasses}`"
88
- @keydown="slider.keydown($event, 'alpha')"
92
+ :class="`
93
+ color-picker--alpha-slider
94
+ ${sliderClasses}
95
+ `"
89
96
  @pointerdown="slider.pointerdown($event, 'alpha')"
90
97
  >
91
98
  <canvas
@@ -100,12 +107,13 @@
100
107
  :aria-valuemax="100"
101
108
  :aria-description="ariaDescription"
102
109
  tabindex="0"
103
- :class="`handle ${handleClasses} bg-neutral-50`"
110
+ :class="`color-picker--alpha-handle ${handleClasses}`"
104
111
  :style="`left: calc(${localColor.percent.a}% - var(--slider-size)/2)`"
112
+ @keydown="slider.keydown($event, 'alpha')"
105
113
  />
106
114
  </div>
107
- <div class="color-group flex w-full flex-1 gap-2">
108
- <div class=" color-wrapper
115
+ <div class="color-picker--footer flex w-full flex-1 gap-2">
116
+ <div class=" color-picker--preview-wrapper
109
117
  bg-transparency-squares
110
118
  relative
111
119
  aspect-square
@@ -114,36 +122,47 @@
114
122
  shadow-xs
115
123
  "
116
124
  >
117
- <!-- <input class="color-input" :value="localColorString" @input="parseInput"> -->
118
- <div class="color
125
+ <div class="
126
+ color-picker--footer--preview
119
127
  size-full
120
128
  rounded-full
121
129
  border-2
122
130
  border-neutral-600
123
131
  dark:border-neutral-300
124
132
  "
125
- :style="`background:${localColorString}`"
133
+ :style="`background: ${asRgbaString}`"
126
134
  />
127
135
  </div>
128
- <div class="color-controls flex flex-1 items-center gap-2">
136
+ <div class="color-picker--input-group flex flex-1 items-center gap-2">
129
137
  <slot name="input">
130
138
  <lib-simple-input
131
- class="w-full"
139
+ :valid="valid"
140
+ class="color-picker--input w-full"
132
141
  :aria-label="label"
133
- :model-value="localColorString"
142
+ v-model="localInputString"
134
143
  @input="parseInput"
144
+ @blur="onBlurFixInvalidValue"
135
145
  />
136
- <lib-button :aria-label="t('copy')" @click="copy()">
146
+ <lib-button class="color-picker--copy-button" :aria-label="t('copy')" @click="copy(copyTransform?.(localColor.val, localColorString) ?? localColorString)">
137
147
  <icon><i-fa6-regular-copy/></icon>
138
148
  </lib-button>
139
149
  </slot>
140
150
  </div>
141
- <!-- <lib-button @click="emits('update:modelValue', localColor.val)">Save</lib-button> -->
142
151
  </div>
143
152
  <slot name="buttons">
144
- <div class="save-cancel-group flex w-full items-center justify-center gap-2">
145
- <lib-button @click="save()">{{ t("save") }}</lib-button>
146
- <lib-button @click="emits('cancel')">{{ t("cancel") }}</lib-button>
153
+ <div class="color-picker--save-cancel-group flex w-full items-center justify-center gap-2">
154
+ <lib-button
155
+ class="color-picker--save-button"
156
+ @click="save()"
157
+ >
158
+ {{ t("save") }}
159
+ </lib-button>
160
+ <lib-button
161
+ class="color-picker--cancel-button"
162
+ @click="emits('cancel')"
163
+ >
164
+ {{ t("cancel") }}
165
+ </lib-button>
147
166
  </div>
148
167
  </slot>
149
168
  </div>
@@ -152,11 +171,18 @@
152
171
  <script setup lang="ts">
153
172
  /* todo change to colorjsio for less dependencies */
154
173
  import { castType } from "@alanscodelog/utils/castType.js"
174
+ import { clampNumber } from "@alanscodelog/utils/clampNumber.js"
155
175
  import { isArray } from "@alanscodelog/utils/isArray.js"
156
- import { colord } from "colord"
157
- import { computed, onMounted, type PropType, reactive, type Ref, ref, type UnwrapRef,watch } from "vue"
176
+ import { unreachable } from "@alanscodelog/utils/unreachable.js"
177
+ import Color from "colorjs.io"
178
+ import { computed, onMounted, reactive, type Ref, ref, type UnwrapRef,useAttrs, watch } from "vue"
179
+
180
+ import { safeConvertToHsva } from "./utils/safeConvertToHsva.js"
181
+ import { safeConvertToRgba } from "./utils/safeConvertToRgba.js"
182
+ import { toLowPrecisionRgbaString } from "./utils/toLowPrecisionRgbaString.js"
158
183
 
159
184
  import { useInjectedI18n } from "../../composables/useInjectedI18n.js"
185
+ import { copy } from "../../helpers/copy.js"
160
186
  import type { HsvaColor, RgbaColor } from "../../types/index.js"
161
187
  import { twMerge } from "../../utils/twMerge.js"
162
188
  import aria from "../Aria/Aria.vue"
@@ -169,6 +195,8 @@ defineOptions({
169
195
  name: "lib-color-picker",
170
196
  })
171
197
 
198
+ const $attrs = useAttrs()
199
+
172
200
  const t = useInjectedI18n()
173
201
 
174
202
  const sliderClasses = `
@@ -181,6 +209,7 @@ const sliderClasses = `
181
209
  `
182
210
 
183
211
  const handleClasses = `
212
+ handle
184
213
  h-[var(--slider-size)]
185
214
  w-[var(--slider-size)]
186
215
  shadow-xs
@@ -194,37 +223,61 @@ const handleClasses = `
194
223
  active:border-accent-500
195
224
  hover:border-accent-500
196
225
  `
226
+ const emits = defineEmits<{
227
+ (e: "save", val: RgbaColor): void
228
+ (e: "cancel"): void
229
+ }>()
197
230
 
198
-
199
- const ariaDescription = t("color-picker.aria.description")
200
-
201
- const $value = defineModel<RgbaColor>({ required: false, default: () => ({ r: 0, g: 0, b: 0 }) })
202
-
203
- const fallbackId = getFallbackId()
204
231
  const props = withDefaults(defineProps<
205
- & LabelProps
232
+ LabelProps
206
233
  & LinkableByIdProps
207
234
  & {
208
235
  allowAlpha?: boolean
236
+ /**
237
+ * The precision of the rgba string representation of the color. Defaults to 3. Extra trailing zeros are removed for a prettier number.
238
+ *
239
+ * Does not affect the number saved unless the user manually edits the color.
240
+ *
241
+ * Ignored if `customRepresentation` is set.
242
+ */
243
+ stringPrecision?: number
244
+ /** Allows overriding the string representation of the color. Useful for using a different representation than rgba (e.g. hex). The fromStringToHsva part is rarely needed as the colorjs.io library can normally parse the color. Returning undefined signals an error. */
245
+ customRepresentation?: {
246
+ fromHsvaToString: (hsva: HsvaColor, includeAlpha: boolean) => string
247
+ fromStringToHsva?: (string: string) => HsvaColor | undefined
248
+ }
209
249
  border?: boolean
250
+ /** Modify what the user copies to the clipboard. */
210
251
  copyTransform?: (val: HsvaColor, stringVal: string) => any
252
+ valid?: boolean
211
253
  }>(), {
212
254
  allowAlpha: true,
213
255
  border: true,
256
+ stringPrecision: 3,
214
257
  copyTransform: (_val: HsvaColor, stringVal: string) => stringVal,
258
+ customRepresentation: undefined,
259
+ valid: true,
215
260
  })
216
261
 
217
262
 
218
- const emits = defineEmits<{
219
- (e: "save", val: RgbaColor): void
220
- (e: "cancel"): void
221
- }>()
263
+ const ariaDescription = t("color-picker.aria.description")
264
+ const fallbackId = getFallbackId()
265
+
266
+ const $value = defineModel<RgbaColor>({ required: false, default: () => ({ r: 0, g: 0, b: 0 }) })
267
+ const $tempValue = defineModel<RgbaColor | undefined>("tempValue", { required: false, default: () => (undefined) })
222
268
 
223
269
  const pickerEl = ref<HTMLCanvasElement | null>(null)
224
270
  const hueSliderEl = ref<HTMLCanvasElement | null>(null)
225
271
  const alphaSliderEl = ref<HTMLCanvasElement | null>(null)
226
272
 
227
- type Config = Record<string, { el: Ref<HTMLCanvasElement>, xKey?: keyof HsvaColor, yKey?: keyof HsvaColor, xSteps?: number, ySteps?: number }>
273
+ type Config = Record<string, {
274
+ el: Ref<HTMLCanvasElement>
275
+ xKey?: keyof HsvaColor
276
+ yKey?: keyof HsvaColor
277
+ xSteps?: number
278
+ ySteps?: number
279
+ }>
280
+
228
281
  const config: Config = {
229
282
  hue: {
230
283
  el: hueSliderEl as Ref<HTMLCanvasElement>,
@@ -236,7 +289,7 @@ const config: Config = {
236
289
  xSteps: 1,
237
290
  xKey: "a",
238
291
  },
239
- picker: {
292
+ all: {
240
293
  el: pickerEl as Ref<HTMLCanvasElement>,
241
294
  xSteps: 100,
242
295
  ySteps: 100,
@@ -244,6 +297,7 @@ const config: Config = {
244
297
  yKey: "v",
245
298
  },
246
299
  }
300
+
247
301
  const localColor = reactive<Record<"percent" | "val", HsvaColor>>({
248
302
  percent: {
249
303
  h: 0, s: 0, v: 0, a: 0,
@@ -253,23 +307,33 @@ const localColor = reactive<Record<"percent" | "val", HsvaColor>>({
253
307
  },
254
308
  })
255
309
 
256
- const localColorString = computed(() =>
257
- colord(localColor.val).toRgbString(),
258
- )
259
- const localColorStringOpaque = computed(() =>
260
- colord({ ...localColor.val, a: 1 }).toRgbString(),
261
- )
262
310
 
311
+ const asRgba = computed(() => {
312
+ const rgba = safeConvertToRgba(localColor.val, props.allowAlpha)
313
+ if (!rgba) unreachable()
314
+ return rgba
315
+ })
316
+ const asRgbaString = computed(() => {
317
+ const rgba = asRgba.value
318
+ if (!rgba) unreachable()
319
+ return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`
320
+ })
321
+ const localColorString = computed(() => {
322
+ if (props.customRepresentation) {
323
+ return props.customRepresentation.fromHsvaToString({ ...localColor.val }, props.allowAlpha)
324
+ }
325
+ return toLowPrecisionRgbaString(asRgba.value, props.allowAlpha, props.stringPrecision)
326
+ })
263
327
 
264
- const copy = (): void => {
265
- if (navigator.clipboard) {
266
- const text = props.copyTransform?.(localColor.val, localColorString.value)
267
-
268
- navigator.clipboard.writeText(text).catch(() => { })
328
+ const localInputString = ref(localColorString.value)
329
+ // fixes the localInputString not updating when the user inputs an invalid value
330
+ function onBlurFixInvalidValue() {
331
+ if (localInputString.value !== localColorString.value) {
332
+ localInputString.value = localColorString.value
269
333
  }
270
334
  }
271
335
 
272
- const updatePicker = (el: HTMLCanvasElement, hue: number): void => {
336
+ function updatePicker(el: HTMLCanvasElement, hue: number): void {
273
337
  const ctx = el.getContext("2d")!
274
338
  const { height, width } = el
275
339
  ctx.clearRect(0, 0, width, height)
@@ -290,9 +354,9 @@ const updatePicker = (el: HTMLCanvasElement, hue: number): void => {
290
354
  ctx.globalCompositeOperation = "source-over"
291
355
  }
292
356
 
293
- const updateSlider = (el: HTMLCanvasElement, stops: ((i: number) => string) | string[], length: number = 360): void => {
357
+ function updateSlider(el: HTMLCanvasElement, stops: ((i: number) => string) | string[], length: number = 360): void {
294
358
  const ctx = el.getContext("2d")!
295
- const { height, width } = el// .getBoundingClientRect()
359
+ const { height, width } = el
296
360
  ctx.clearRect(0, 0, width, height)
297
361
 
298
362
  const end = isArray(stops) ? stops.length - 1 : length
@@ -301,6 +365,7 @@ const updateSlider = (el: HTMLCanvasElement, stops: ((i: number) => string) | st
301
365
 
302
366
  for (let i = 0; i < end + 1; i++) {
303
367
  const stop = stops instanceof Function ? stops(i) : stops[i]
368
+ if (stop === undefined) unreachable()
304
369
  gradient.addColorStop(i / end, stop)
305
370
  }
306
371
 
@@ -309,8 +374,7 @@ const updateSlider = (el: HTMLCanvasElement, stops: ((i: number) => string) | st
309
374
  }
310
375
 
311
376
 
312
- const getVal = (x: number, width: number, steps: number = 100, accuracy: number = 100, reverse = false): { val: number, percent: number } => {
313
- // const stepWidth = width / (steps * accuracy)
377
+ function getVal(x: number, width: number, steps: number = 100, accuracy: number = 100, reverse = false): { val: number, percent: number } {
314
378
  const percent = (x / width)
315
379
  const unrounded = percent * steps
316
380
 
@@ -320,53 +384,70 @@ const getVal = (x: number, width: number, steps: number = 100, accuracy: number
320
384
  if (reverse) res.val = steps - val
321
385
  return res
322
386
  }
387
+
323
388
  type Types = keyof UnwrapRef<Config>
324
389
  const elDragging = ref<Types | "">("")
325
390
  let dragging = false
326
391
 
327
- const moveHandle = (e: { clientX: number, clientY: number }, type: string) => {
328
- const el = config[type].el.value
329
- const { x, y, width, height } = el.getBoundingClientRect()
330
-
331
- const info = config[type]
332
- if (info.xKey !== undefined) {
333
- let newPosX = e.clientX - x
334
- newPosX = newPosX < 0 ? 0 : newPosX > width ? width : newPosX
335
- const newX = getVal(newPosX, width, info.xSteps ?? 100)
336
-
337
- localColor.percent[info.xKey] = newX.percent
338
- localColor.val[info.xKey] = newX.val
339
- }
392
+ function moveHandle(e: { clientX: number, clientY: number }, type: string) {
393
+ requestAnimationFrame(() => {
394
+ if (type === "") return
395
+ const el = config[type]?.el.value
396
+ if (!el || !config[type]) return
397
+ const { x, y, width, height } = el.getBoundingClientRect()
398
+
399
+ const info = config[type]
400
+ if (info.xKey !== undefined) {
401
+ let newPosX = e.clientX - x
402
+ newPosX = newPosX < 0 ? 0 : newPosX > width ? width : newPosX
403
+ const newX = getVal(newPosX, width, info.xSteps ?? 100)
404
+
405
+ localColor.percent[info.xKey] = newX.percent
406
+ localColor.val[info.xKey] = newX.val
407
+ }
340
408
 
341
- if (info.yKey !== undefined) {
342
- let newPosY = e.clientY - y
343
- newPosY = newPosY < 0 ? 0 : newPosY > height ? height : newPosY
344
- const newY = getVal(newPosY, height, info.ySteps ?? 100, 100, true)
409
+ if (info.yKey !== undefined) {
410
+ let newPosY = e.clientY - y
411
+ newPosY = newPosY < 0 ? 0 : newPosY > height ? height : newPosY
412
+ const newY = getVal(newPosY, height, info.ySteps ?? 100, 100, true)
345
413
 
346
- localColor.percent[info.yKey] = newY.percent
347
- localColor.val[info.yKey] = newY.val
348
- }
414
+ localColor.percent[info.yKey] = newY.percent
415
+ localColor.val[info.yKey] = newY.val
416
+ }
417
+ })
349
418
  }
350
419
  const slider = {
351
420
  keydown: (e: KeyboardEvent, type: Types) => {
352
421
  castType<HTMLElement | undefined>(e.target)
353
- if (e.target?.getBoundingClientRect && ["ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown"].includes(e.key)) {
354
- e.preventDefault()
355
- const { x, y, width, height } = e.target.getBoundingClientRect()
356
- let xDiff = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0
357
- let yDiff = e.key === "ArrowUp" ? -1 : e.key === "ArrowDown" ? 1 : 0
358
- if (e.shiftKey) {xDiff *= 10}
359
- if (e.shiftKey) {yDiff *= 10}
360
- moveHandle({ clientX: x + (width / 2) + xDiff, clientY: y + (height / 2) + yDiff }, type)
422
+ if (e.target?.getBoundingClientRect) {
423
+ if (["ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown"].includes(e.key)) {
424
+ e.preventDefault()
425
+ const { x, y, width, height } = e.target.getBoundingClientRect()
426
+ let xDiff = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0
427
+ let yDiff = e.key === "ArrowUp" ? -1 : e.key === "ArrowDown" ? 1 : 0
428
+ if (e.shiftKey) {xDiff *= 10}
429
+ if (e.shiftKey) {yDiff *= 10}
430
+ moveHandle({ clientX: x + (width / 2) + xDiff, clientY: y + (height / 2) + yDiff }, type)
431
+ }
432
+ if (e.key === "Enter") {
433
+ e.preventDefault()
434
+ save()
435
+ }
361
436
  }
362
437
  },
363
438
  pointerdown: (e: PointerEvent, type: Types) => {
439
+ const focusTargetClass = `#${props.id ?? fallbackId} .color-picker--${type}-handle`
440
+ const focusTarget = document.querySelector(focusTargetClass)
441
+ // allows enter to work when the user drags any slider as the even will be captured by the keydown listener
442
+ if (focusTarget instanceof HTMLElement) focusTarget.focus()
443
+
364
444
  if (dragging) return
365
445
  e.preventDefault()
366
446
  elDragging.value = type
367
447
  dragging = true
368
448
  document.addEventListener("pointermove", slider.pointermove)
369
449
  document.addEventListener("pointerup", slider.pointerup)
450
+ moveHandle(e, elDragging.value)
370
451
  },
371
452
  pointerleave: (e: PointerEvent) => {
372
453
  if (dragging) {
@@ -374,75 +455,105 @@ const slider = {
374
455
  }
375
456
  },
376
457
  pointermove: (e: PointerEvent) => {
377
- if (dragging) {
378
- e.preventDefault()
379
- requestAnimationFrame(() => {
380
- const type = elDragging.value
381
- moveHandle(e, type)
382
- })
383
- }
458
+ e.preventDefault()
459
+ moveHandle(e, elDragging.value)
384
460
  },
385
461
  pointerup: (e: PointerEvent) => {
386
462
  e.preventDefault()
387
- elDragging.value = ""
388
463
  dragging = false
464
+ elDragging.value = ""
389
465
  document.removeEventListener("pointermove", slider.pointermove)
390
466
  document.removeEventListener("pointerup", slider.pointerup)
391
467
  },
392
468
  }
393
- const update = (_: HsvaColor, { updatePosition = true, updateValue = true }: { updatePosition?: boolean, updateValue?: boolean } = {}): void => {
469
+ function updateSliders(_: HsvaColor): void {
394
470
  if (alphaSliderEl.value) {
395
- const hsl = colord(_)
396
- const hsl0 = hsl.alpha(0).toHslString()
397
- const hsl1 = hsl.alpha(1).toHslString()
398
- updateSlider(alphaSliderEl.value, [hsl0, hsl1])
471
+ // https://colorjs.io/docs/output#get-a-displayable-css-color-value
472
+ const color = new Color("hsv", [_.h, _.s, _.v], _.a).to("hsl")
473
+ const hsl0 = color.clone()
474
+ hsl0.alpha = 0
475
+ const hsl1 = color.clone()
476
+ hsl1.alpha = 1
477
+ updateSlider(alphaSliderEl.value, [hsl0.toString(), hsl1.toString()])
399
478
  }
400
479
  updateSlider(hueSliderEl.value!, i => `hsl(${i} 100% 50%)`)
401
480
  updatePicker(pickerEl.value!, _.h)
402
- if (updatePosition) {
403
- localColor.percent.h = Math.round((_.h / 360) * 10000) / 100
404
- localColor.percent.s = _.s
405
- localColor.percent.v = 100 - _.v
406
- localColor.percent.a =
407
- props.allowAlpha
408
- ? _.a !== undefined
409
- ? _.a * 100
410
- : 1
481
+ }
482
+
483
+ function updateValueAndPosition(_: HsvaColor): void {
484
+ localColor.percent.h = Math.round((_.h / 360) * 10000) / 100
485
+ localColor.percent.s = _.s
486
+ localColor.percent.v = 100 - _.v
487
+ localColor.percent.a =
488
+ props.allowAlpha
489
+ ? _.a !== undefined
490
+ ? _.a * 100
411
491
  : 1
412
- }
413
- if (updateValue) {
414
- localColor.val = { ..._, a: props.allowAlpha ? _.a : 1 }
415
- }
492
+ : 1
493
+ localColor.val = { ..._, a: props.allowAlpha ? _.a : 1 }
416
494
  }
417
495
 
418
- const parseInput = (e: Event): void => {
496
+ function convertAndUpdateAll(rgba: RgbaColor) {
497
+ const hsva = safeConvertToHsva(rgba, props.allowAlpha)
498
+ if (!hsva) return
499
+ updateSliders(hsva)
500
+ updateValueAndPosition(hsva)
501
+ }
502
+
503
+ function save(): void {
504
+ const rgba = safeConvertToRgba(localColor.val, props.allowAlpha)
505
+ if (!rgba) return
506
+ // update(localColor.val, { updatePosition: false, updateValue: false })
507
+ $value.value = rgba
508
+ $tempValue.value = undefined
509
+ emits("save", rgba)
510
+ }
511
+
512
+
513
+ function parseInput(e: Event): void {
419
514
  const val = (e.target as HTMLInputElement)?.value
420
- if (val) {
421
- const color = colord(val)
422
- if (!color.isValid()) return
423
- update(color.toHsv())
515
+ const converted = props.customRepresentation?.fromStringToHsva
516
+ ? props.customRepresentation.fromStringToHsva(val)
517
+ : safeConvertToHsva(val, props.allowAlpha)
518
+ if (converted) {
519
+ updateSliders(converted)
520
+ updateValueAndPosition(converted)
424
521
  }
425
522
  }
426
523
 
524
+
525
+ let disableUpdateTempValue = false
526
+
427
527
  onMounted(() => {
428
- const color = colord($value.value)
429
- update(color.toHsv())
528
+ convertAndUpdateAll($value.value)
529
+ if ($tempValue.value !== undefined) {
530
+ convertAndUpdateAll($tempValue.value)
531
+ }
532
+
533
+ const handle = document.querySelector(`#${props.id ?? fallbackId} .color-picker--all-handle`)
534
+ if (handle instanceof HTMLElement) handle.focus()
430
535
  })
431
- watch(props, () => {
432
- const color = colord($value.value)
433
- update(color.toHsv())
536
+
537
+ watch($value, () => {
538
+ convertAndUpdateAll($value.value)
434
539
  })
435
540
 
436
- watch(localColor, () => {
437
- update(localColor.val, { updatePosition: false, updateValue: false })
541
+ watch($tempValue, () => {
542
+ if ($tempValue.value !== undefined) {
543
+ disableUpdateTempValue = true
544
+ convertAndUpdateAll($tempValue.value)
545
+ setTimeout(() => { disableUpdateTempValue = false }, 0)
546
+ }
438
547
  })
439
548
 
440
- const save = (): void => {
441
- const color = colord(localColor.val).toRgb()
442
- update(localColor.val, { updatePosition: false, updateValue: false })
443
- $value.value = color
444
- emits("save", color)
445
- }
549
+ watch(localColor, () => {
550
+ updateSliders(localColor.val)
551
+ localInputString.value = localColorString.value
552
+ if (disableUpdateTempValue) return
553
+ const rgba = safeConvertToRgba(localColor.val, props.allowAlpha)
554
+ if (!rgba) return
555
+ $tempValue.value = rgba
556
+ })
446
557
 
447
558
  const invertColors = computed(() => !!(localColor.percent.v < 50 || (localColor.val.a === undefined || localColor.val.a < 0.5)))
448
559
  </script>
@@ -0,0 +1,2 @@
1
+ import type { HsvaColor, RgbaColor } from "../../../types/index.js.js";
2
+ export declare function safeConvertToHsva(val: string | RgbaColor, allowAlpha: boolean): HsvaColor | undefined;
@@ -0,0 +1,18 @@
1
+ import { clampNumber } from "@alanscodelog/utils/clampNumber.js";
2
+ import Color from "colorjs.io";
3
+ export function safeConvertToHsva(val, allowAlpha) {
4
+ try {
5
+ const color = typeof val === "string" ? new Color(val) : new Color("srgb", [val.r / 255, val.g / 255, val.b / 255], allowAlpha ? val.a : 1);
6
+ const hsv = color.hsv;
7
+ if (!hsv || hsv[1] === void 0 || hsv[2] === void 0) return void 0;
8
+ const final = {
9
+ h: clampNumber(hsv[0] ?? 0, 0, Number.MAX_SAFE_INTEGER),
10
+ s: clampNumber(hsv[1], 0, 100),
11
+ v: clampNumber(hsv[2], 0, 100),
12
+ a: clampNumber(allowAlpha ? color.alpha : 1, 0, 1)
13
+ };
14
+ return final;
15
+ } catch {
16
+ return void 0;
17
+ }
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { HsvaColor, RgbaColor } from "../../../types/index.js.js";
2
+ export declare function safeConvertToRgba(val: string | HsvaColor, allowAlpha: boolean): RgbaColor | undefined;
@@ -0,0 +1,17 @@
1
+ import { clampNumber } from "@alanscodelog/utils/clampNumber.js";
2
+ import Color from "colorjs.io";
3
+ export function safeConvertToRgba(val, allowAlpha) {
4
+ try {
5
+ const color = typeof val === "string" ? new Color(val) : new Color("hsv", [val.h, val.s, val.v], allowAlpha ? val.a : 1);
6
+ const rgb = color.srgb;
7
+ if (!rgb || rgb[0] === void 0 || rgb[1] === void 0 || rgb[2] === void 0) return void 0;
8
+ return {
9
+ r: clampNumber(rgb[0] / 1 * 255, 0, 255),
10
+ g: clampNumber(rgb[1] / 1 * 255, 0, 255),
11
+ b: clampNumber(rgb[2] / 1 * 255, 0, 255),
12
+ a: clampNumber(allowAlpha ? color.alpha : 1, 0, 1)
13
+ };
14
+ } catch {
15
+ return void 0;
16
+ }
17
+ }
@@ -0,0 +1,2 @@
1
+ import type { RgbaColor } from "../../../main.lib.js.js";
2
+ export declare function toLowPrecisionRgbaString(rgba: RgbaColor, withAlpha: boolean, precision: number): string;
@@ -0,0 +1,8 @@
1
+ import { truncate } from "./truncate.js";
2
+ export function toLowPrecisionRgbaString(rgba, withAlpha, precision) {
3
+ const r = truncate(rgba.r, precision);
4
+ const g = truncate(rgba.g, precision);
5
+ const b = truncate(rgba.b, precision);
6
+ const a = rgba.a !== void 0 ? truncate(rgba.a, precision) : void 0;
7
+ return withAlpha ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`;
8
+ }
@@ -0,0 +1 @@
1
+ export declare function truncate(val: number, precision: number): string;