@nexxtmove/ui 2.2.0 → 2.4.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 (36) hide show
  1. package/dist/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue.d.ts +2 -2
  2. package/dist/components/editor/molecules/EditorBlur/EditorBlur.vue.d.ts +2 -2
  3. package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +44 -20
  4. package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +66 -0
  5. package/dist/components/editor/molecules/EditorColor/colorClass.d.ts +46 -0
  6. package/dist/components/editor/molecules/EditorColor/gradientClass.d.ts +80 -0
  7. package/dist/components/editor/molecules/EditorGap/EditorGap.vue.d.ts +2 -2
  8. package/dist/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue.d.ts +2 -2
  9. package/dist/components/editor/molecules/EditorShadow/EditorShadow.vue.d.ts +2 -2
  10. package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +58 -2
  11. package/dist/components/organisms/ColorPicker/_GradientField.vue.d.ts +23 -0
  12. package/dist/components/organisms/ColorPicker/_GradientPreview.vue.d.ts +11 -0
  13. package/dist/components/organisms/ColorPicker/_GradientStopRow.vue.d.ts +21 -0
  14. package/dist/components/organisms/ColorPicker/_GradientStops.vue.d.ts +19 -0
  15. package/dist/components/organisms/ColorPicker/gradient.d.ts +189 -0
  16. package/dist/components/organisms/ColorPicker/gradientGeometry.d.ts +101 -0
  17. package/dist/components/organisms/ColorPicker/useGradientEditor.d.ts +89 -0
  18. package/dist/components/organisms/VideoControls/VideoControls.vue.d.ts +8 -1
  19. package/dist/index.d.ts +4 -1
  20. package/dist/index.js +3400 -2083
  21. package/package.json +1 -1
  22. package/src/components/editor/molecules/EditorColor/EditorColor.vue +83 -171
  23. package/src/components/editor/molecules/EditorColor/_ColorField.vue +445 -0
  24. package/src/components/editor/molecules/EditorColor/colorClass.ts +61 -0
  25. package/src/components/editor/molecules/EditorColor/gradientClass.ts +225 -0
  26. package/src/components/molecules/ToggleGroup/ToggleGroup.vue +6 -2
  27. package/src/components/organisms/ColorPicker/ColorPicker.vue +673 -271
  28. package/src/components/organisms/ColorPicker/_GradientField.vue +93 -0
  29. package/src/components/organisms/ColorPicker/_GradientPreview.vue +401 -0
  30. package/src/components/organisms/ColorPicker/_GradientStopRow.vue +181 -0
  31. package/src/components/organisms/ColorPicker/_GradientStops.vue +218 -0
  32. package/src/components/organisms/ColorPicker/gradient.ts +303 -0
  33. package/src/components/organisms/ColorPicker/gradientGeometry.ts +216 -0
  34. package/src/components/organisms/ColorPicker/useGradientEditor.ts +358 -0
  35. package/src/components/organisms/VideoControls/VideoControls.vue +15 -4
  36. package/src/index.ts +25 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "2.2.0",
4
+ "version": "2.4.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -1,4 +1,24 @@
1
1
  <script lang="ts">
2
+ import type { EditorColorRole, EditorColorThemeRole } from './colorClass'
3
+ import type { EditorColorGradientValue } from './gradientClass'
4
+ import type {
5
+ ColorPickerFillMode,
6
+ ColorPickerGradientMessages,
7
+ } from '../../../organisms/ColorPicker/ColorPicker.vue'
8
+
9
+ export type { EditorColorRole, EditorColorThemeRole } from './colorClass'
10
+ export type {
11
+ EditorColorGradientValue,
12
+ EditorColorLinearGradientValue,
13
+ EditorColorRadialGradientValue,
14
+ } from './gradientClass'
15
+
16
+ /** A fill the editor can offer: `'solid'` or `'gradient'`; see NexxtColorPicker. */
17
+ export type EditorColorFillMode = ColorPickerFillMode
18
+
19
+ /** Copy of the gradient mode; see NexxtColorPicker. */
20
+ export type EditorColorGradientMessages = ColorPickerGradientMessages
21
+
2
22
  /**
3
23
  * The CSS property the chosen colour is written to. It is the prefix of the
4
24
  * Tailwind class the model carries, so it decides whether the colour becomes a
@@ -7,14 +27,6 @@
7
27
  */
8
28
  export type EditorColorProperty = 'bg' | 'text'
9
29
 
10
- const ROLES = ['main', 'accent', 'background', 'heading', 'text'] as const
11
-
12
- /**
13
- * A role of the customer's theme. A colour bound to a role follows the theme,
14
- * where a literal colour stays what it was picked as.
15
- */
16
- export type EditorColorRole = (typeof ROLES)[number]
17
-
18
30
  /** A literal colour: a Tailwind arbitrary-value class such as `'bg-[#1777EA]'`. */
19
31
  export type EditorColorLiteralValue = `${EditorColorProperty}-[#${string}]`
20
32
 
@@ -24,22 +36,11 @@ export type EditorColorRoleValue = `${EditorColorProperty}-theme-${EditorColorRo
24
36
  /**
25
37
  * The colour as the editor needs it: the class to apply, so the consumer can
26
38
  * put the model value straight on the element it styles. Either a literal
27
- * colour or a theme role.
39
+ * colour or a theme role, or with `modes` including `'gradient'` a gradient: a string of classes
40
+ * such as `'bg-linear-135 from-[#7f56d9] to-[#432e73]'`.
28
41
  */
29
- export type EditorColorValue = EditorColorLiteralValue | EditorColorRoleValue
30
-
31
- export interface EditorColorThemeRole {
32
- /** The role the class is written for: `'main'` becomes `bg-theme-main`. */
33
- role: EditorColorRole
34
- /** Name of the role as the user reads it, in the swatch's tooltip and in the trigger. */
35
- label: string
36
- /**
37
- * The role's current colour as a 6- or 8-digit hex. Only used to paint the
38
- * swatch: the library ships no CSS for the role classes, and the hex never
39
- * reaches the model.
40
- */
41
- hex: string
42
- }
42
+ export type EditorColorValue =
43
+ EditorColorLiteralValue | EditorColorRoleValue | EditorColorGradientValue
43
44
 
44
45
  export interface EditorColorMessages {
45
46
  /** Text shown while no colour is set. Defaults to `'Kies kleur...'`. */
@@ -98,6 +99,38 @@ export interface NexxtEditorColorProps {
98
99
  * alpha and the model gets the opaque 6-digit hex.
99
100
  */
100
101
  showAlpha?: boolean
102
+ /**
103
+ * The fills the picker offers. `['solid']` (the default) is the plain colour
104
+ * picker, unchanged. `['solid', 'gradient']`, in any order, adds an
105
+ * "Effen | Verloop" switch; `['gradient']` offers only the gradient, without a
106
+ * switch -- an empty value or a solid colour then opens on the gradient a
107
+ * switch would give, written only on the first change. A gradient only exists
108
+ * for `property` `'bg'`: with `'text'` this falls back to `['solid']`, also
109
+ * for `['gradient']`. A gradient is written as
110
+ * one string of classes in a fixed order -- type, from, from-position, via,
111
+ * via-position, to, to-position -- such as
112
+ * `'bg-linear-135 from-[#7f56d9] via-theme-main via-40% to-[#432e73cc]'` or
113
+ * `'bg-radial-[at_30%_70%] from-theme-accent to-[#ffffff00]'`. Hexes are in
114
+ * lower case without an opaque `ff`, and a position is only written when it
115
+ * differs from 0 %, 50 % or 100 %. In the gradient mode the opacity slider is
116
+ * always offered, whatever `showAlpha` says. To draw a stored gradient as an
117
+ * inline style, parse it with `parseGradientClass` and turn it into CSS with
118
+ * `gradientClassToCss`, which always interpolates in oklab, as the preview does.
119
+ * A radial gradient off centre renders in sRGB as a Tailwind class; draw it
120
+ * with `gradientClassToCss` for the oklab result the picker shows.
121
+ */
122
+ modes?: EditorColorFillMode[]
123
+ /**
124
+ * Copy of the gradient mode. Merged per key over the Dutch defaults, so
125
+ * overriding one key keeps the others.
126
+ */
127
+ gradientMessages?: EditorColorGradientMessages
128
+ /**
129
+ * Id(s) of an element that describes the setting, such as a hint the
130
+ * consumer renders itself. Set as `aria-describedby` on the picker's trigger,
131
+ * and on the switch when `switchable`. Without it no attribute is set.
132
+ */
133
+ describedby?: string
101
134
  }
102
135
 
103
136
  const DEFAULT_MESSAGES = {
@@ -108,36 +141,17 @@ const DEFAULT_MESSAGES = {
108
141
  } satisfies Required<EditorColorMessages>
109
142
 
110
143
  /**
111
- * A literal model value: one of the two prefixes, then a 6- or 8-digit hex between
112
- * brackets. A `border-[…]` class is therefore read as unparseable, the same as
113
- * any other class this component does not own. The alpha pair is accepted on
114
- * read whether or not `showAlpha` is set, so a stored `#1777EA80` survives a
115
- * round trip either way.
116
- */
117
- const VALUE_PATTERN = /^(bg|text)-\[(#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?)\]$/
118
-
119
- /**
120
- * A role class: one of the two prefixes, then `theme-` and one of the five
121
- * roles. Either prefix takes every role. A tint such as `bg-theme-main/10` is
122
- * not a value this component writes, so it is read as unparseable.
123
- */
124
- const ROLE_PATTERN = new RegExp(`^(?:bg|text)-theme-(${ROLES.join('|')})$`)
125
-
126
- /** The picker throws on a colour it cannot read, so a role's hex is checked first. */
127
- const ROLE_HEX_PATTERN = /^#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?$/
128
-
129
- /**
130
- * The picker writes `#rrggbbaa` only below full opacity, but an alpha a hair
131
- * under 1 still rounds to `ff`. That pair says nothing, so it is dropped.
144
+ * The prefixes read as a colour. A value with the other prefix is shown and
145
+ * rewritten to `property`; a `border-[…]` class, like any other class this
146
+ * component does not own, is read as unparseable.
132
147
  */
133
- const OPAQUE_ALPHA_PATTERN = /^(#[0-9A-Fa-f]{6})[fF]{2}$/
148
+ const PROPERTIES = ['bg', 'text'] as const satisfies readonly EditorColorProperty[]
134
149
  </script>
135
150
 
136
151
  <script lang="ts" setup>
137
- import { computed, watch } from 'vue'
152
+ import { computed } from 'vue'
138
153
  import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
139
- import NexxtTooltip from '../../../atoms/Tooltip/Tooltip.vue'
140
- import NexxtColorPicker from '../../../organisms/ColorPicker/ColorPicker.vue'
154
+ import NexxtColorField from './_ColorField.vue'
141
155
 
142
156
  defineOptions({ name: 'NexxtEditorColor' })
143
157
 
@@ -149,6 +163,9 @@ const {
149
163
  suggestions = [],
150
164
  themeRoles = [],
151
165
  showAlpha = false,
166
+ modes = ['solid'],
167
+ gradientMessages,
168
+ describedby,
152
169
  } = defineProps<NexxtEditorColorProps>()
153
170
 
154
171
  /**
@@ -170,88 +187,15 @@ const model = defineModel<EditorColorValue>({ required: true })
170
187
  const enabled = defineModel<boolean>('enabled', { default: true })
171
188
 
172
189
  const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
173
-
174
- /**
175
- * The `#` is part of the model's type, so a plain `string` cannot be written
176
- * into it. This narrows the picker's output and the regex capture to the shape
177
- * the model promises, without a cast.
178
- */
179
- const isHex = (value: string): value is `#${string}` => value.startsWith('#')
180
-
181
- /** The hex out of the model, or an empty string when it is not a colour class. */
182
- const hex = computed(() => {
183
- const captured = VALUE_PATTERN.exec(model.value)?.[2]
184
- return captured && isHex(captured) ? captured : ''
185
- })
186
-
187
- /** The role out of the model, or `undefined` when it is not a role class. */
188
- const storedRole = computed(() => {
189
- const captured = ROLE_PATTERN.exec(model.value)?.[1]
190
- return ROLES.find((role) => role === captured)
191
- })
192
-
193
- const offeredRoles = computed(() => themeRoles.filter((entry) => ROLE_HEX_PATTERN.test(entry.hex)))
194
-
195
- /**
196
- * The stored role as the consumer offers it. Only a role class selects a role:
197
- * a literal colour that happens to equal a role's hex stays a literal.
198
- */
199
- const activeRole = computed(() => offeredRoles.value.find(({ role }) => role === storedRole.value))
200
-
201
- /**
202
- * Picking the role's own colour -- from the suggestions, or typed -- is how a
203
- * colour is detached from the theme without changing how it looks. The picker
204
- * reports nothing when it is told to pick the string it already has, so a role's
205
- * hex is handed over in its 8-digit form: with `show-alpha` off the picker never
206
- * reports an alpha pair, which makes every pick a change.
207
- *
208
- * A role has no opacity here, so an alpha pair in the role's own hex is replaced
209
- * by `ff`. Handed over translucent, the picker would drop that alpha the moment
210
- * the role turns `show-alpha` off, and write the opaque hex over the role.
211
- */
212
- const pickerHex = (roleHex: string) => `${roleHex.slice(0, 7)}ff`
213
-
214
- /**
215
- * A role class has no opacity, so the slider is only offered for a literal.
216
- * Hiding it is also what keeps the trick in `pickerHex` working: without the
217
- * alpha channel the picker reads the role's hex as opaque.
218
- */
219
- const alphaEnabled = computed(() => showAlpha && !activeRole.value)
220
-
221
- // An unparseable value -- empty, a token class such as `bg-red-500`, or copy
222
- // from before this component -- leaves the picker empty and its placeholder
223
- // visible, rather than being reported as a problem: the first pick writes a
224
- // valid class back. A role the consumer does not offer is shown the same way,
225
- // because there is no label or hex to show it with; it is not written back.
226
- const picked = computed({
227
- get: () => (activeRole.value ? pickerHex(activeRole.value.hex) : hex.value),
228
- // The picker clears its own value in no flow it offers, but an empty string
229
- // would still wipe a perfectly good colour, so it is dropped rather than
230
- // written through. The picker keeps rendering the model's colour.
231
- set: (value: string) => {
232
- const color = value.replace(OPAQUE_ALPHA_PATTERN, '$1')
233
- if (isHex(color)) model.value = `${property}-[${color}]`
234
- },
235
- })
236
-
237
- const pickRole = (role: EditorColorRole) => {
238
- model.value = `${property}-theme-${role}`
239
- }
240
-
241
- // `property` is a prop, so the parent can flip a background setting into a
242
- // text-colour one while a value is already set. Rewriting the prefix here is
243
- // what keeps the stored class and the prop from drifting apart.
244
- watch(
245
- () => property,
246
- (next) => {
247
- if (storedRole.value) model.value = `${next}-theme-${storedRole.value}`
248
- else if (hex.value) model.value = `${next}-[${hex.value}]`
249
- },
250
- )
251
190
  </script>
252
191
 
253
192
  <template>
254
- <NexxtEditorSettingsBlock v-model="enabled" :label="label" :switchable="switchable">
193
+ <NexxtEditorSettingsBlock
194
+ v-model="enabled"
195
+ :label="label"
196
+ :switchable="switchable"
197
+ :describedby="describedby"
198
+ >
255
199
  <!--
256
200
  The block label is a <p> (or the switch's own label), so the picker gets
257
201
  no `label` of its own -- that would print the heading twice. Its trigger
@@ -264,52 +208,20 @@ watch(
264
208
  floating element, so a panel in a container narrower than its content
265
209
  would still be wider than the trigger; in the settings column it is not.
266
210
  -->
267
- <NexxtColorPicker
268
- v-model="picked"
269
- full-width
270
- match-width
271
- :formats="['hex']"
272
- :show-alpha="alphaEnabled"
211
+ <NexxtColorField
212
+ v-model="model"
213
+ :property="property"
214
+ :accepts="PROPERTIES"
215
+ :show-alpha="showAlpha"
273
216
  :placeholder="copy.placeholder"
274
- :suggestions="{ hex: suggestions }"
217
+ :suggestions="suggestions"
275
218
  :suggestions-label="copy.suggestionsLabel"
219
+ :theme-label="copy.themeLabel"
276
220
  :alpha-label="copy.alphaLabel"
277
- :value-label="activeRole?.label"
278
- >
279
- <template v-if="offeredRoles.length" #presets>
280
- <div class="flex flex-col gap-1">
281
- <span class="extra-small-normal text-gray-500">{{ copy.themeLabel }}</span>
282
- <!--
283
- Same swatches and row as the picker's suggestions. Two roles often
284
- share a hex (heading and text), so the tooltip is what tells them
285
- apart; it repeats the accessible name rather than adding copy.
286
- -->
287
- <div class="flex flex-wrap gap-1.5">
288
- <NexxtTooltip
289
- v-for="{ role, label: roleLabel, hex: roleHex } in offeredRoles"
290
- :key="role"
291
- disable-closing-trigger
292
- >
293
- <button
294
- type="button"
295
- :aria-label="roleLabel"
296
- :aria-pressed="role === activeRole?.role"
297
- class="cursor-pointer rounded-md p-0.5 ring-gray-200 transition-shadow hover:ring-2 hover:ring-cornflower-blue-400 focus-visible:ring-2 focus-visible:ring-cornflower-blue-500 focus-visible:outline-none aria-pressed:ring-2 aria-pressed:ring-cornflower-blue-500"
298
- @click="pickRole(role)"
299
- >
300
- <span
301
- aria-hidden="true"
302
- class="block size-6 rounded-sm shadow-[inset_0_0_0_1px_rgba(0,0,0,0.1)]"
303
- :style="{ backgroundColor: roleHex }"
304
- />
305
- </button>
306
- <template #tooltip>
307
- <span class="small-normal text-gray-900">{{ roleLabel }}</span>
308
- </template>
309
- </NexxtTooltip>
310
- </div>
311
- </div>
312
- </template>
313
- </NexxtColorPicker>
221
+ :theme-roles="themeRoles"
222
+ :modes="modes"
223
+ :gradient-messages="gradientMessages"
224
+ :describedby="describedby"
225
+ />
314
226
  </NexxtEditorSettingsBlock>
315
227
  </template>