@nexxtmove/ui 2.0.0 → 2.2.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/atoms/EditorSettingsBlock/EditorSettingsBlock.vue.d.ts +6 -0
- package/dist/components/editor/molecules/EditorBlur/EditorBlur.vue.d.ts +106 -0
- package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +15 -0
- package/dist/components/molecules/ToggleGroup/ToggleGroup.vue.d.ts +7 -0
- package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2021 -1906
- package/dist/nuxt.js +1 -0
- package/package.json +1 -1
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue +15 -2
- package/src/components/editor/molecules/EditorBlur/EditorBlur.vue +160 -0
- package/src/components/editor/molecules/EditorColor/EditorColor.vue +42 -6
- package/src/components/molecules/ToggleGroup/ToggleGroup.vue +12 -1
- package/src/components/organisms/ColorPicker/ColorPicker.vue +70 -25
- package/src/components.json +1 -0
- package/src/index.ts +7 -0
package/dist/nuxt.js
CHANGED
|
@@ -84,6 +84,7 @@ var s = {
|
|
|
84
84
|
NexxtEditorAlignmentGrid: "components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.vue",
|
|
85
85
|
NexxtEditorAnchor: "components/editor/molecules/EditorAnchor/EditorAnchor.vue",
|
|
86
86
|
NexxtEditorBackgroundSize: "components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue",
|
|
87
|
+
NexxtEditorBlur: "components/editor/molecules/EditorBlur/EditorBlur.vue",
|
|
87
88
|
NexxtEditorBorder: "components/editor/molecules/EditorBorder/EditorBorder.vue",
|
|
88
89
|
NexxtEditorColor: "components/editor/molecules/EditorColor/EditorColor.vue",
|
|
89
90
|
NexxtEditorDashPattern: "components/editor/molecules/EditorDashPattern/EditorDashPattern.vue",
|
package/package.json
CHANGED
|
@@ -11,6 +11,12 @@ interface NexxtEditorSettingsBlockProps {
|
|
|
11
11
|
* When `false` the content is always visible and the model is ignored.
|
|
12
12
|
*/
|
|
13
13
|
switchable?: boolean
|
|
14
|
+
/**
|
|
15
|
+
* Id(s) of an element that describes the setting, set as `aria-describedby`
|
|
16
|
+
* on the switch. Only has an effect when `switchable` is `true`; without it
|
|
17
|
+
* no attribute is rendered.
|
|
18
|
+
*/
|
|
19
|
+
describedby?: string
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/**
|
|
@@ -21,7 +27,7 @@ interface NexxtEditorSettingsBlockProps {
|
|
|
21
27
|
*/
|
|
22
28
|
defineOptions({ name: 'NexxtEditorSettingsBlock' })
|
|
23
29
|
|
|
24
|
-
const { label, switchable = false } = defineProps<NexxtEditorSettingsBlockProps>()
|
|
30
|
+
const { label, switchable = false, describedby } = defineProps<NexxtEditorSettingsBlockProps>()
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
33
|
* Open state of the block, driven by the switch. Only has an effect when
|
|
@@ -80,7 +86,14 @@ const labelTag = computed(() => (promoted.value && group ? `h${group.value.headi
|
|
|
80
86
|
back to the `gap-2` that Switch.vue sets for itself.
|
|
81
87
|
-->
|
|
82
88
|
<div class="flex items-center [&>label]:flex [&>label]:w-full [&>label]:gap-3">
|
|
83
|
-
<Switch
|
|
89
|
+
<Switch
|
|
90
|
+
v-if="switchable"
|
|
91
|
+
v-model="model"
|
|
92
|
+
size="sm"
|
|
93
|
+
color="blue"
|
|
94
|
+
label-position="before"
|
|
95
|
+
:aria-describedby="describedby"
|
|
96
|
+
>
|
|
84
97
|
<span :class="['grow text-left', labelTypography, labelColor]">{{ label }}</span>
|
|
85
98
|
</Switch>
|
|
86
99
|
<component :is="labelTag" v-else :class="[labelTypography, labelColor]">
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The blur steps the editor offers, in the order they are shown. `token` is the
|
|
4
|
+
* class without its fixed `backdrop-blur-` prefix, so the class is
|
|
5
|
+
* `backdrop-blur-` plus the token -- the same shape as `EDITOR_SHADOW_OPTIONS`.
|
|
6
|
+
* "No blur" is not a step: it is the switch being off. The labels are not in
|
|
7
|
+
* here; they arrive through `messages`, keyed by token.
|
|
8
|
+
*/
|
|
9
|
+
export const EDITOR_BLUR_OPTIONS = [
|
|
10
|
+
{ token: 'xs' },
|
|
11
|
+
{ token: 'sm' },
|
|
12
|
+
{ token: 'md' },
|
|
13
|
+
{ token: 'lg' },
|
|
14
|
+
{ token: 'xl' },
|
|
15
|
+
] as const
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The blur as the editor needs it: the Tailwind class itself, so the consumer
|
|
19
|
+
* can put the model value straight on the group it styles.
|
|
20
|
+
*/
|
|
21
|
+
export type EditorBlurValue = `backdrop-blur-${(typeof EDITOR_BLUR_OPTIONS)[number]['token']}`
|
|
22
|
+
|
|
23
|
+
/** Overridable copy of the step control. Every key is optional. */
|
|
24
|
+
export interface EditorBlurMessages {
|
|
25
|
+
/**
|
|
26
|
+
* Accessible name of the group of steps. Visually hidden: the block label
|
|
27
|
+
* already says what the steps are. Defaults to the Dutch `'Sterkte'`.
|
|
28
|
+
*/
|
|
29
|
+
strength?: string
|
|
30
|
+
/** Name of the `backdrop-blur-xs` option. Defaults to `'XS'`. */
|
|
31
|
+
xs?: string
|
|
32
|
+
/** Name of the `backdrop-blur-sm` option. Defaults to `'S'`. */
|
|
33
|
+
sm?: string
|
|
34
|
+
/** Name of the `backdrop-blur-md` option. Defaults to `'M'`. */
|
|
35
|
+
md?: string
|
|
36
|
+
/** Name of the `backdrop-blur-lg` option. Defaults to `'L'`. */
|
|
37
|
+
lg?: string
|
|
38
|
+
/** Name of the `backdrop-blur-xl` option. Defaults to `'XL'`. */
|
|
39
|
+
xl?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NexxtEditorBlurProps {
|
|
43
|
+
/**
|
|
44
|
+
* Header text of the settings block. Defaults to the Dutch `'Vervagen'`; the
|
|
45
|
+
* library has no i18n layer, so a consumer in another language passes its own
|
|
46
|
+
* copy in.
|
|
47
|
+
*/
|
|
48
|
+
label?: string
|
|
49
|
+
/**
|
|
50
|
+
* Render a switch next to the label that collapses and expands the steps.
|
|
51
|
+
* Defaults to `true`: there is no "none" step, so the switch is the only way
|
|
52
|
+
* to turn the blur off. The switch state is the `enabled` model; switching
|
|
53
|
+
* off leaves the step alone.
|
|
54
|
+
*/
|
|
55
|
+
switchable?: boolean
|
|
56
|
+
/**
|
|
57
|
+
* Copy of the step control. Merged per key, so overriding one key keeps the
|
|
58
|
+
* defaults of the others. Copy is never translated inside the component.
|
|
59
|
+
*/
|
|
60
|
+
messages?: EditorBlurMessages
|
|
61
|
+
/**
|
|
62
|
+
* Id(s) of an element that describes the setting, such as a hint the
|
|
63
|
+
* consumer renders itself. Set as `aria-describedby` on the switch (when
|
|
64
|
+
* `switchable`) and on the group of steps. Without it no attribute is set.
|
|
65
|
+
*/
|
|
66
|
+
describedby?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DEFAULT_MESSAGES = {
|
|
70
|
+
strength: 'Sterkte',
|
|
71
|
+
xs: 'XS',
|
|
72
|
+
sm: 'S',
|
|
73
|
+
md: 'M',
|
|
74
|
+
lg: 'L',
|
|
75
|
+
xl: 'XL',
|
|
76
|
+
} satisfies Required<EditorBlurMessages>
|
|
77
|
+
|
|
78
|
+
/** The class a control starts on when it is switched on without a valid one. */
|
|
79
|
+
const DEFAULT_VALUE: EditorBlurValue = 'backdrop-blur-md'
|
|
80
|
+
|
|
81
|
+
const toValue = (token: (typeof EDITOR_BLUR_OPTIONS)[number]['token']): EditorBlurValue =>
|
|
82
|
+
`backdrop-blur-${token}`
|
|
83
|
+
|
|
84
|
+
const isValue = (value: unknown): value is EditorBlurValue =>
|
|
85
|
+
EDITOR_BLUR_OPTIONS.some(({ token }) => toValue(token) === value)
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<script lang="ts" setup>
|
|
89
|
+
import { computed, watch } from 'vue'
|
|
90
|
+
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
91
|
+
import NexxtToggleGroup from '../../../molecules/ToggleGroup/ToggleGroup.vue'
|
|
92
|
+
import type { NexxtToggleGroupOption } from '../../../molecules/ToggleGroup/ToggleGroup.vue'
|
|
93
|
+
|
|
94
|
+
defineOptions({ name: 'NexxtEditorBlur' })
|
|
95
|
+
|
|
96
|
+
const {
|
|
97
|
+
label = 'Vervagen',
|
|
98
|
+
switchable = true,
|
|
99
|
+
messages,
|
|
100
|
+
describedby,
|
|
101
|
+
} = defineProps<NexxtEditorBlurProps>()
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The selected blur, as the Tailwind class to apply (`backdrop-blur-md`).
|
|
105
|
+
* Optional: a consumer may have no class stored yet. Without a valid class the
|
|
106
|
+
* control shows `md` but writes nothing, so the consumer has to assume
|
|
107
|
+
* `backdrop-blur-md` itself. Only a switch from off to on (when `switchable`)
|
|
108
|
+
* writes `backdrop-blur-md` back; mounting never emits.
|
|
109
|
+
*/
|
|
110
|
+
const model = defineModel<EditorBlurValue>()
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Open state of the block. Only has an effect when `switchable` is `true`; then
|
|
114
|
+
* off is the way to express "no blur". With `switchable` set to `false` the
|
|
115
|
+
* steps are always active, whatever this model says.
|
|
116
|
+
*/
|
|
117
|
+
const enabled = defineModel<boolean>('enabled', { default: true })
|
|
118
|
+
|
|
119
|
+
const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
|
|
120
|
+
|
|
121
|
+
const options = computed<NexxtToggleGroupOption[]>(() =>
|
|
122
|
+
EDITOR_BLUR_OPTIONS.map(({ token }) => ({ value: toValue(token), label: copy.value[token] })),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// ToggleGroup types its model as an open `string`; the setter narrows it, which
|
|
126
|
+
// is safe because it can only emit the option values built above. A missing or
|
|
127
|
+
// unknown class shows `md` rather than leaving every option unselected.
|
|
128
|
+
const selected = computed({
|
|
129
|
+
get: () => (isValue(model.value) ? model.value : DEFAULT_VALUE),
|
|
130
|
+
set: (value: string) => {
|
|
131
|
+
model.value = value as EditorBlurValue
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Switching on without a valid class writes `backdrop-blur-md` back, so the
|
|
136
|
+
// consumer never ends up with an enabled blur and no class to apply. Without a
|
|
137
|
+
// switch there is no switching on, so `enabled` is ignored there like everywhere
|
|
138
|
+
// else. The current model is read here rather than captured, so a parent that
|
|
139
|
+
// sets both in one tick wins.
|
|
140
|
+
watch(enabled, (on, wasOn) => {
|
|
141
|
+
if (switchable && on && !wasOn && !isValue(model.value)) model.value = DEFAULT_VALUE
|
|
142
|
+
})
|
|
143
|
+
</script>
|
|
144
|
+
|
|
145
|
+
<template>
|
|
146
|
+
<NexxtEditorSettingsBlock
|
|
147
|
+
v-model="enabled"
|
|
148
|
+
:label="label"
|
|
149
|
+
:switchable="switchable"
|
|
150
|
+
:describedby="describedby"
|
|
151
|
+
>
|
|
152
|
+
<NexxtToggleGroup
|
|
153
|
+
v-model="selected"
|
|
154
|
+
:label="copy.strength"
|
|
155
|
+
label-hidden
|
|
156
|
+
:options="options"
|
|
157
|
+
:aria-describedby="describedby"
|
|
158
|
+
/>
|
|
159
|
+
</NexxtEditorSettingsBlock>
|
|
160
|
+
</template>
|
|
@@ -48,6 +48,11 @@ export interface EditorColorMessages {
|
|
|
48
48
|
suggestionsLabel?: string
|
|
49
49
|
/** Label shown above the theme roles. Defaults to `'Huisstijl'`. */
|
|
50
50
|
themeLabel?: string
|
|
51
|
+
/**
|
|
52
|
+
* Label above the opacity slider, and its accessible name. Defaults to
|
|
53
|
+
* `'Dekking'`. Only shown with `showAlpha`.
|
|
54
|
+
*/
|
|
55
|
+
alphaLabel?: string
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
export interface NexxtEditorColorProps {
|
|
@@ -83,20 +88,31 @@ export interface NexxtEditorColorProps {
|
|
|
83
88
|
* 8-digit hex is left out. Without roles the section is not rendered.
|
|
84
89
|
*/
|
|
85
90
|
themeRoles?: EditorColorThemeRole[]
|
|
91
|
+
/**
|
|
92
|
+
* Offer an opacity slider in the picker. A colour picked below full opacity is
|
|
93
|
+
* written as an 8-digit hex (`bg-[#1777ea80]`); a fully opaque one keeps the
|
|
94
|
+
* 6-digit form. The slider is hidden while a theme role is selected: a role
|
|
95
|
+
* class carries no opacity. Off by default, in which case a stored 8-digit hex
|
|
96
|
+
* is shown opaque and only replaced once a new colour is picked. Turning it off
|
|
97
|
+
* while a translucent colour is set does write at once: the picker drops the
|
|
98
|
+
* alpha and the model gets the opaque 6-digit hex.
|
|
99
|
+
*/
|
|
100
|
+
showAlpha?: boolean
|
|
86
101
|
}
|
|
87
102
|
|
|
88
103
|
const DEFAULT_MESSAGES = {
|
|
89
104
|
placeholder: 'Kies kleur...',
|
|
90
105
|
suggestionsLabel: 'Gebruikte kleuren',
|
|
91
106
|
themeLabel: 'Huisstijl',
|
|
107
|
+
alphaLabel: 'Dekking',
|
|
92
108
|
} satisfies Required<EditorColorMessages>
|
|
93
109
|
|
|
94
110
|
/**
|
|
95
111
|
* A literal model value: one of the two prefixes, then a 6- or 8-digit hex between
|
|
96
112
|
* brackets. A `border-[…]` class is therefore read as unparseable, the same as
|
|
97
|
-
* any other class this component does not own. The
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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.
|
|
100
116
|
*/
|
|
101
117
|
const VALUE_PATTERN = /^(bg|text)-\[(#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?)\]$/
|
|
102
118
|
|
|
@@ -109,6 +125,12 @@ const ROLE_PATTERN = new RegExp(`^(?:bg|text)-theme-(${ROLES.join('|')})$`)
|
|
|
109
125
|
|
|
110
126
|
/** The picker throws on a colour it cannot read, so a role's hex is checked first. */
|
|
111
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.
|
|
132
|
+
*/
|
|
133
|
+
const OPAQUE_ALPHA_PATTERN = /^(#[0-9A-Fa-f]{6})[fF]{2}$/
|
|
112
134
|
</script>
|
|
113
135
|
|
|
114
136
|
<script lang="ts" setup>
|
|
@@ -126,6 +148,7 @@ const {
|
|
|
126
148
|
messages,
|
|
127
149
|
suggestions = [],
|
|
128
150
|
themeRoles = [],
|
|
151
|
+
showAlpha = false,
|
|
129
152
|
} = defineProps<NexxtEditorColorProps>()
|
|
130
153
|
|
|
131
154
|
/**
|
|
@@ -181,8 +204,19 @@ const activeRole = computed(() => offeredRoles.value.find(({ role }) => role ===
|
|
|
181
204
|
* reports nothing when it is told to pick the string it already has, so a role's
|
|
182
205
|
* hex is handed over in its 8-digit form: with `show-alpha` off the picker never
|
|
183
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.
|
|
184
218
|
*/
|
|
185
|
-
const
|
|
219
|
+
const alphaEnabled = computed(() => showAlpha && !activeRole.value)
|
|
186
220
|
|
|
187
221
|
// An unparseable value -- empty, a token class such as `bg-red-500`, or copy
|
|
188
222
|
// from before this component -- leaves the picker empty and its placeholder
|
|
@@ -195,7 +229,8 @@ const picked = computed({
|
|
|
195
229
|
// would still wipe a perfectly good colour, so it is dropped rather than
|
|
196
230
|
// written through. The picker keeps rendering the model's colour.
|
|
197
231
|
set: (value: string) => {
|
|
198
|
-
|
|
232
|
+
const color = value.replace(OPAQUE_ALPHA_PATTERN, '$1')
|
|
233
|
+
if (isHex(color)) model.value = `${property}-[${color}]`
|
|
199
234
|
},
|
|
200
235
|
})
|
|
201
236
|
|
|
@@ -234,10 +269,11 @@ watch(
|
|
|
234
269
|
full-width
|
|
235
270
|
match-width
|
|
236
271
|
:formats="['hex']"
|
|
237
|
-
:show-alpha="
|
|
272
|
+
:show-alpha="alphaEnabled"
|
|
238
273
|
:placeholder="copy.placeholder"
|
|
239
274
|
:suggestions="{ hex: suggestions }"
|
|
240
275
|
:suggestions-label="copy.suggestionsLabel"
|
|
276
|
+
:alpha-label="copy.alphaLabel"
|
|
241
277
|
:value-label="activeRole?.label"
|
|
242
278
|
>
|
|
243
279
|
<template v-if="offeredRoles.length" #presets>
|
|
@@ -24,6 +24,12 @@ interface NexxtToggleGroupProps {
|
|
|
24
24
|
options: NexxtToggleGroupOption[]
|
|
25
25
|
label?: string
|
|
26
26
|
equalWidth?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Hide `label` visually while it stays the group's accessible name, for a
|
|
29
|
+
* group whose purpose is already clear from a heading around it. Defaults to
|
|
30
|
+
* `false`. Has no effect without a `label`.
|
|
31
|
+
*/
|
|
32
|
+
labelHidden?: boolean
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
defineOptions({
|
|
@@ -32,6 +38,7 @@ defineOptions({
|
|
|
32
38
|
|
|
33
39
|
const props = withDefaults(defineProps<NexxtToggleGroupProps>(), {
|
|
34
40
|
equalWidth: true,
|
|
41
|
+
labelHidden: false,
|
|
35
42
|
})
|
|
36
43
|
|
|
37
44
|
const model = defineModel<string>({ required: true })
|
|
@@ -158,7 +165,11 @@ onMounted(async () => {
|
|
|
158
165
|
|
|
159
166
|
<template>
|
|
160
167
|
<div class="flex flex-col gap-1" role="toolbar" :aria-labelledby="label ? labelId : undefined">
|
|
161
|
-
<label
|
|
168
|
+
<label
|
|
169
|
+
v-if="label"
|
|
170
|
+
:id="labelId"
|
|
171
|
+
:class="['justify-start small-normal text-gray-700', labelHidden && 'sr-only']"
|
|
172
|
+
>
|
|
162
173
|
{{ label }}
|
|
163
174
|
</label>
|
|
164
175
|
<div
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { ref, watch, computed } from 'vue'
|
|
2
|
+
import { ref, watch, computed, useId } from 'vue'
|
|
3
3
|
import type { Color, ColorChannel } from 'reka-ui'
|
|
4
4
|
import {
|
|
5
5
|
ColorAreaArea,
|
|
@@ -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
|
|
@@ -35,6 +40,8 @@ export interface NexxtColorPickerProps {
|
|
|
35
40
|
suggestions?: ColorSuggestions
|
|
36
41
|
/** Accessible and visible label for the suggestions. */
|
|
37
42
|
suggestionsLabel?: string
|
|
43
|
+
/** Visible label of the opacity slider, and the slider's accessible name. */
|
|
44
|
+
alphaLabel?: string
|
|
38
45
|
/**
|
|
39
46
|
* Text shown in the trigger in place of the formatted colour, for a value that has a name of
|
|
40
47
|
* its own, such as a theme role. Only shown while a colour is set.
|
|
@@ -49,7 +56,16 @@ defineSlots<{
|
|
|
49
56
|
presets?(): unknown
|
|
50
57
|
}>()
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
// `useId` is the same on the server and in the browser; a random id would not hydrate.
|
|
60
|
+
const PICKER_ID = useId()
|
|
61
|
+
const ALPHA_LABEL_ID = useId()
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* reka reports the alpha channel as `alpha * 100`, which is a fraction for most
|
|
65
|
+
* stored colours (`#…80` is 50.196) and float noise for some steps (7.000…01).
|
|
66
|
+
* Only what the slider announces is rounded; the colour itself is untouched.
|
|
67
|
+
*/
|
|
68
|
+
const roundedChannel = (value: number) => Math.round(value)
|
|
53
69
|
|
|
54
70
|
const props = withDefaults(defineProps<NexxtColorPickerProps>(), {
|
|
55
71
|
placeholder: 'Kies kleur...',
|
|
@@ -59,6 +75,7 @@ const props = withDefaults(defineProps<NexxtColorPickerProps>(), {
|
|
|
59
75
|
matchWidth: false,
|
|
60
76
|
suggestions: () => ({}),
|
|
61
77
|
suggestionsLabel: 'Gebruikte kleuren',
|
|
78
|
+
alphaLabel: 'Dekking',
|
|
62
79
|
})
|
|
63
80
|
|
|
64
81
|
const model = defineModel<string>({ default: '' })
|
|
@@ -168,11 +185,24 @@ watch(
|
|
|
168
185
|
},
|
|
169
186
|
)
|
|
170
187
|
|
|
188
|
+
// A parent can swap the value in the same update that turns alpha off, and this
|
|
189
|
+
// watcher may run before the one above. So the colour is read from the model,
|
|
190
|
+
// which already holds the new value, not from `colorObj`, which may not.
|
|
191
|
+
const currentColor = () => {
|
|
192
|
+
if (!model.value) return colorObj.value
|
|
193
|
+
try {
|
|
194
|
+
return normalizeColor(model.value)
|
|
195
|
+
} catch {
|
|
196
|
+
return colorObj.value
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
171
200
|
watch(
|
|
172
201
|
() => props.showAlpha,
|
|
173
202
|
(showAlpha) => {
|
|
174
|
-
|
|
175
|
-
|
|
203
|
+
const current = currentColor()
|
|
204
|
+
if (!showAlpha && current.alpha < 1) {
|
|
205
|
+
const updated = withoutAlpha(current)
|
|
176
206
|
colorObj.value = updated
|
|
177
207
|
model.value = fmt(updated, activeFormat.value)
|
|
178
208
|
}
|
|
@@ -270,28 +300,43 @@ watch(
|
|
|
270
300
|
</ColorSliderRoot>
|
|
271
301
|
|
|
272
302
|
<!-- Alpha slider (opt-in via showAlpha prop) -->
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
color-space="hsl"
|
|
287
|
-
class="relative z-10 flex size-full touch-none items-center select-none"
|
|
288
|
-
@update:color="handleColorUpdate"
|
|
289
|
-
>
|
|
290
|
-
<ColorSliderTrack class="relative h-3 flex-1 overflow-hidden rounded-full" />
|
|
291
|
-
<ColorSliderThumb
|
|
292
|
-
class="block size-5 cursor-grab rounded-full border-2 border-white bg-white shadow-sm focus:ring-2 focus:ring-cornflower-blue-500 focus:outline-none active:cursor-grabbing"
|
|
303
|
+
<div v-if="showAlpha" class="flex flex-col gap-1">
|
|
304
|
+
<span :id="ALPHA_LABEL_ID" class="extra-small-normal text-gray-500">{{
|
|
305
|
+
alphaLabel
|
|
306
|
+
}}</span>
|
|
307
|
+
<!-- The checkerboard wrapper is behind the track; the gradient goes from
|
|
308
|
+
rgba(…,0) → rgba(…,1) so its transparent left end reveals it. -->
|
|
309
|
+
<div class="relative flex h-5 items-center">
|
|
310
|
+
<div
|
|
311
|
+
class="absolute inset-x-0 inset-y-1 rounded-full"
|
|
312
|
+
style="
|
|
313
|
+
background-image: repeating-conic-gradient(#e5e7eb 0% 25%, #ffffff 0% 50%);
|
|
314
|
+
background-size: 10px 10px;
|
|
315
|
+
"
|
|
293
316
|
/>
|
|
294
|
-
|
|
317
|
+
<ColorSliderRoot
|
|
318
|
+
:model-value="colorObj"
|
|
319
|
+
channel="alpha"
|
|
320
|
+
color-space="hsl"
|
|
321
|
+
class="relative z-10 flex size-full touch-none items-center select-none"
|
|
322
|
+
@update:color="handleColorUpdate"
|
|
323
|
+
>
|
|
324
|
+
<ColorSliderTrack class="relative h-3 flex-1 overflow-hidden rounded-full" />
|
|
325
|
+
<!--
|
|
326
|
+
With `as-child` reka merges its own attributes under the child's,
|
|
327
|
+
so the child's win: the name comes from the visible label instead
|
|
328
|
+
of reka's English "Alpha", and the value is announced whole.
|
|
329
|
+
-->
|
|
330
|
+
<ColorSliderThumb v-slot="{ channelValue }" as-child>
|
|
331
|
+
<span
|
|
332
|
+
:aria-label="undefined"
|
|
333
|
+
:aria-labelledby="ALPHA_LABEL_ID"
|
|
334
|
+
:aria-valuenow="roundedChannel(channelValue)"
|
|
335
|
+
class="block size-5 cursor-grab rounded-full border-2 border-white bg-white shadow-sm focus:ring-2 focus:ring-cornflower-blue-500 focus:outline-none active:cursor-grabbing"
|
|
336
|
+
/>
|
|
337
|
+
</ColorSliderThumb>
|
|
338
|
+
</ColorSliderRoot>
|
|
339
|
+
</div>
|
|
295
340
|
</div>
|
|
296
341
|
|
|
297
342
|
<!-- Format input fields — only active format shown, cycle with chevrons -->
|
package/src/components.json
CHANGED
|
@@ -81,6 +81,7 @@
|
|
|
81
81
|
"NexxtEditorAlignmentGrid": "components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.vue",
|
|
82
82
|
"NexxtEditorAnchor": "components/editor/molecules/EditorAnchor/EditorAnchor.vue",
|
|
83
83
|
"NexxtEditorBackgroundSize": "components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue",
|
|
84
|
+
"NexxtEditorBlur": "components/editor/molecules/EditorBlur/EditorBlur.vue",
|
|
84
85
|
"NexxtEditorBorder": "components/editor/molecules/EditorBorder/EditorBorder.vue",
|
|
85
86
|
"NexxtEditorColor": "components/editor/molecules/EditorColor/EditorColor.vue",
|
|
86
87
|
"NexxtEditorDashPattern": "components/editor/molecules/EditorDashPattern/EditorDashPattern.vue",
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,12 @@ export type {
|
|
|
70
70
|
EditorBackgroundSizeMessages,
|
|
71
71
|
EditorBackgroundSizeValue,
|
|
72
72
|
} from './components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue'
|
|
73
|
+
export { EDITOR_BLUR_OPTIONS } from './components/editor/molecules/EditorBlur/EditorBlur.vue'
|
|
74
|
+
export type {
|
|
75
|
+
EditorBlurMessages,
|
|
76
|
+
EditorBlurValue,
|
|
77
|
+
NexxtEditorBlurProps,
|
|
78
|
+
} from './components/editor/molecules/EditorBlur/EditorBlur.vue'
|
|
73
79
|
export { EDITOR_FLEX_DIRECTION_OPTIONS } from './components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue'
|
|
74
80
|
export type {
|
|
75
81
|
EditorFlexDirectionMessages,
|
|
@@ -233,6 +239,7 @@ export { default as NexxtEditorAlignItems } from './components/editor/molecules/
|
|
|
233
239
|
export { default as NexxtEditorAlignmentGrid } from './components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.vue'
|
|
234
240
|
export { default as NexxtEditorAnchor } from './components/editor/molecules/EditorAnchor/EditorAnchor.vue'
|
|
235
241
|
export { default as NexxtEditorBackgroundSize } from './components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue'
|
|
242
|
+
export { default as NexxtEditorBlur } from './components/editor/molecules/EditorBlur/EditorBlur.vue'
|
|
236
243
|
export { default as NexxtEditorBorder } from './components/editor/molecules/EditorBorder/EditorBorder.vue'
|
|
237
244
|
export { default as NexxtEditorColor } from './components/editor/molecules/EditorColor/EditorColor.vue'
|
|
238
245
|
export { default as NexxtEditorDashPattern } from './components/editor/molecules/EditorDashPattern/EditorDashPattern.vue'
|