@nexxtmove/ui 2.2.0 → 2.3.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/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue.d.ts +2 -2
- package/dist/components/editor/molecules/EditorBlur/EditorBlur.vue.d.ts +2 -2
- package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +44 -20
- package/dist/components/editor/molecules/EditorColor/_ColorField.vue.d.ts +66 -0
- package/dist/components/editor/molecules/EditorColor/colorClass.d.ts +46 -0
- package/dist/components/editor/molecules/EditorColor/gradientClass.d.ts +80 -0
- package/dist/components/editor/molecules/EditorGap/EditorGap.vue.d.ts +2 -2
- package/dist/components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue.d.ts +2 -2
- package/dist/components/editor/molecules/EditorShadow/EditorShadow.vue.d.ts +2 -2
- package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +58 -2
- package/dist/components/organisms/ColorPicker/_GradientField.vue.d.ts +23 -0
- package/dist/components/organisms/ColorPicker/_GradientPreview.vue.d.ts +11 -0
- package/dist/components/organisms/ColorPicker/_GradientStopRow.vue.d.ts +21 -0
- package/dist/components/organisms/ColorPicker/_GradientStops.vue.d.ts +19 -0
- package/dist/components/organisms/ColorPicker/gradient.d.ts +189 -0
- package/dist/components/organisms/ColorPicker/gradientGeometry.d.ts +101 -0
- package/dist/components/organisms/ColorPicker/useGradientEditor.d.ts +89 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3392 -2082
- package/package.json +1 -1
- package/src/components/editor/molecules/EditorColor/EditorColor.vue +83 -171
- package/src/components/editor/molecules/EditorColor/_ColorField.vue +445 -0
- package/src/components/editor/molecules/EditorColor/colorClass.ts +61 -0
- package/src/components/editor/molecules/EditorColor/gradientClass.ts +225 -0
- package/src/components/molecules/ToggleGroup/ToggleGroup.vue +6 -2
- package/src/components/organisms/ColorPicker/ColorPicker.vue +673 -271
- package/src/components/organisms/ColorPicker/_GradientField.vue +93 -0
- package/src/components/organisms/ColorPicker/_GradientPreview.vue +401 -0
- package/src/components/organisms/ColorPicker/_GradientStopRow.vue +181 -0
- package/src/components/organisms/ColorPicker/_GradientStops.vue +218 -0
- package/src/components/organisms/ColorPicker/gradient.ts +303 -0
- package/src/components/organisms/ColorPicker/gradientGeometry.ts +216 -0
- package/src/components/organisms/ColorPicker/useGradientEditor.ts +358 -0
- package/src/index.ts +25 -0
package/package.json
CHANGED
|
@@ -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 =
|
|
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
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
|
148
|
+
const PROPERTIES = ['bg', 'text'] as const satisfies readonly EditorColorProperty[]
|
|
134
149
|
</script>
|
|
135
150
|
|
|
136
151
|
<script lang="ts" setup>
|
|
137
|
-
import { computed
|
|
152
|
+
import { computed } from 'vue'
|
|
138
153
|
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
139
|
-
import
|
|
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
|
|
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
|
-
<
|
|
268
|
-
v-model="
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
:
|
|
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="
|
|
217
|
+
:suggestions="suggestions"
|
|
275
218
|
:suggestions-label="copy.suggestionsLabel"
|
|
219
|
+
:theme-label="copy.themeLabel"
|
|
276
220
|
:alpha-label="copy.alphaLabel"
|
|
277
|
-
:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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>
|