@nexxtmove/ui 2.0.0 → 2.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "2.0.0",
4
+ "version": "2.1.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -83,6 +83,16 @@ export interface NexxtEditorColorProps {
83
83
  * 8-digit hex is left out. Without roles the section is not rendered.
84
84
  */
85
85
  themeRoles?: EditorColorThemeRole[]
86
+ /**
87
+ * Offer an opacity slider in the picker. A colour picked below full opacity is
88
+ * written as an 8-digit hex (`bg-[#1777ea80]`); a fully opaque one keeps the
89
+ * 6-digit form. The slider is hidden while a theme role is selected: a role
90
+ * class carries no opacity. Off by default, in which case a stored 8-digit hex
91
+ * is shown opaque and only replaced once a new colour is picked. Turning it off
92
+ * while a translucent colour is set does write at once: the picker drops the
93
+ * alpha and the model gets the opaque 6-digit hex.
94
+ */
95
+ showAlpha?: boolean
86
96
  }
87
97
 
88
98
  const DEFAULT_MESSAGES = {
@@ -94,9 +104,9 @@ const DEFAULT_MESSAGES = {
94
104
  /**
95
105
  * A literal model value: one of the two prefixes, then a 6- or 8-digit hex between
96
106
  * brackets. A `border-[…]` class is therefore read as unparseable, the same as
97
- * any other class this component does not own. The
98
- * alpha pair is accepted on read so a stored `#1777EA80` survives a round trip,
99
- * even though the picker itself is opened without an alpha channel here.
107
+ * any other class this component does not own. The alpha pair is accepted on
108
+ * read whether or not `showAlpha` is set, so a stored `#1777EA80` survives a
109
+ * round trip either way.
100
110
  */
101
111
  const VALUE_PATTERN = /^(bg|text)-\[(#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?)\]$/
102
112
 
@@ -109,6 +119,12 @@ const ROLE_PATTERN = new RegExp(`^(?:bg|text)-theme-(${ROLES.join('|')})$`)
109
119
 
110
120
  /** The picker throws on a colour it cannot read, so a role's hex is checked first. */
111
121
  const ROLE_HEX_PATTERN = /^#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?$/
122
+
123
+ /**
124
+ * The picker writes `#rrggbbaa` only below full opacity, but an alpha a hair
125
+ * under 1 still rounds to `ff`. That pair says nothing, so it is dropped.
126
+ */
127
+ const OPAQUE_ALPHA_PATTERN = /^(#[0-9A-Fa-f]{6})[fF]{2}$/
112
128
  </script>
113
129
 
114
130
  <script lang="ts" setup>
@@ -126,6 +142,7 @@ const {
126
142
  messages,
127
143
  suggestions = [],
128
144
  themeRoles = [],
145
+ showAlpha = false,
129
146
  } = defineProps<NexxtEditorColorProps>()
130
147
 
131
148
  /**
@@ -181,8 +198,19 @@ const activeRole = computed(() => offeredRoles.value.find(({ role }) => role ===
181
198
  * reports nothing when it is told to pick the string it already has, so a role's
182
199
  * hex is handed over in its 8-digit form: with `show-alpha` off the picker never
183
200
  * reports an alpha pair, which makes every pick a change.
201
+ *
202
+ * A role has no opacity here, so an alpha pair in the role's own hex is replaced
203
+ * by `ff`. Handed over translucent, the picker would drop that alpha the moment
204
+ * the role turns `show-alpha` off, and write the opaque hex over the role.
205
+ */
206
+ const pickerHex = (roleHex: string) => `${roleHex.slice(0, 7)}ff`
207
+
208
+ /**
209
+ * A role class has no opacity, so the slider is only offered for a literal.
210
+ * Hiding it is also what keeps the trick in `pickerHex` working: without the
211
+ * alpha channel the picker reads the role's hex as opaque.
184
212
  */
185
- const pickerHex = (roleHex: string) => (roleHex.length === 7 ? `${roleHex}ff` : roleHex)
213
+ const alphaEnabled = computed(() => showAlpha && !activeRole.value)
186
214
 
187
215
  // An unparseable value -- empty, a token class such as `bg-red-500`, or copy
188
216
  // from before this component -- leaves the picker empty and its placeholder
@@ -195,7 +223,8 @@ const picked = computed({
195
223
  // would still wipe a perfectly good colour, so it is dropped rather than
196
224
  // written through. The picker keeps rendering the model's colour.
197
225
  set: (value: string) => {
198
- if (isHex(value)) model.value = `${property}-[${value}]`
226
+ const color = value.replace(OPAQUE_ALPHA_PATTERN, '$1')
227
+ if (isHex(color)) model.value = `${property}-[${color}]`
199
228
  },
200
229
  })
201
230
 
@@ -234,7 +263,7 @@ watch(
234
263
  full-width
235
264
  match-width
236
265
  :formats="['hex']"
237
- :show-alpha="false"
266
+ :show-alpha="alphaEnabled"
238
267
  :placeholder="copy.placeholder"
239
268
  :suggestions="{ hex: suggestions }"
240
269
  :suggestions-label="copy.suggestionsLabel"
@@ -27,6 +27,11 @@ export interface NexxtColorPickerProps {
27
27
  error?: boolean
28
28
  errorMessage?: string
29
29
  required?: boolean
30
+ /**
31
+ * Offer an opacity slider and accept colours below full opacity. Turning it
32
+ * off while the colour is translucent writes the opaque colour to the model
33
+ * at once.
34
+ */
30
35
  showAlpha?: boolean
31
36
  formats?: ColorFormat[]
32
37
  fullWidth?: boolean
@@ -168,11 +173,24 @@ watch(
168
173
  },
169
174
  )
170
175
 
176
+ // A parent can swap the value in the same update that turns alpha off, and this
177
+ // watcher may run before the one above. So the colour is read from the model,
178
+ // which already holds the new value, not from `colorObj`, which may not.
179
+ const currentColor = () => {
180
+ if (!model.value) return colorObj.value
181
+ try {
182
+ return normalizeColor(model.value)
183
+ } catch {
184
+ return colorObj.value
185
+ }
186
+ }
187
+
171
188
  watch(
172
189
  () => props.showAlpha,
173
190
  (showAlpha) => {
174
- if (!showAlpha && colorObj.value.alpha < 1) {
175
- const updated = withoutAlpha(colorObj.value)
191
+ const current = currentColor()
192
+ if (!showAlpha && current.alpha < 1) {
193
+ const updated = withoutAlpha(current)
176
194
  colorObj.value = updated
177
195
  model.value = fmt(updated, activeFormat.value)
178
196
  }