@nexxtmove/ui 1.35.0 → 1.37.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/atoms/Icon/customIcons.d.ts +1 -1
- package/dist/components/editor/molecules/EditorColor/EditorColor.vue.d.ts +53 -16
- package/dist/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue.d.ts +90 -0
- package/dist/components/editor/molecules/EditorLineHeight/EditorLineHeight.vue.d.ts +104 -0
- package/dist/components/organisms/ColorPicker/ColorPicker.vue.d.ts +16 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +957 -732
- package/dist/nuxt.js +2 -0
- package/package.json +1 -1
- package/src/components/editor/molecules/EditorColor/EditorColor.vue +131 -14
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue +163 -0
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.vue +161 -0
- package/src/components/organisms/ColorPicker/ColorPicker.vue +14 -1
- package/src/components.json +2 -0
- package/src/env.d.ts +6 -1
- package/src/index.ts +16 -0
package/dist/nuxt.js
CHANGED
|
@@ -83,6 +83,8 @@ var s = {
|
|
|
83
83
|
NexxtEditorGap: "components/editor/molecules/EditorGap/EditorGap.vue",
|
|
84
84
|
NexxtEditorHeadingType: "components/editor/molecules/EditorHeadingType/EditorHeadingType.vue",
|
|
85
85
|
NexxtEditorJustifyContent: "components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue",
|
|
86
|
+
NexxtEditorLetterSpacing: "components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue",
|
|
87
|
+
NexxtEditorLineHeight: "components/editor/molecules/EditorLineHeight/EditorLineHeight.vue",
|
|
86
88
|
NexxtEditorMaxWidth: "components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue",
|
|
87
89
|
NexxtEditorPadding: "components/editor/molecules/EditorPadding/EditorPadding.vue",
|
|
88
90
|
NexxtEditorRadius: "components/editor/molecules/EditorRadius/EditorRadius.vue",
|
package/package.json
CHANGED
|
@@ -7,18 +7,47 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export type EditorColorProperty = 'bg' | 'text'
|
|
9
9
|
|
|
10
|
+
const ROLES = ['main', 'accent', 'background', 'heading', 'text'] as const
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* element it styles.
|
|
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.
|
|
14
15
|
*/
|
|
15
|
-
export type
|
|
16
|
+
export type EditorColorRole = (typeof ROLES)[number]
|
|
17
|
+
|
|
18
|
+
/** A literal colour: a Tailwind arbitrary-value class such as `'bg-[#1777EA]'`. */
|
|
19
|
+
export type EditorColorLiteralValue = `${EditorColorProperty}-[#${string}]`
|
|
20
|
+
|
|
21
|
+
/** A colour bound to a theme role: a token class such as `'bg-theme-main'`. */
|
|
22
|
+
export type EditorColorRoleValue = `${EditorColorProperty}-theme-${EditorColorRole}`
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The colour as the editor needs it: the class to apply, so the consumer can
|
|
26
|
+
* put the model value straight on the element it styles. Either a literal
|
|
27
|
+
* colour or a theme role.
|
|
28
|
+
*/
|
|
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
|
+
}
|
|
16
43
|
|
|
17
44
|
export interface EditorColorMessages {
|
|
18
45
|
/** Text shown while no colour is set. Defaults to `'Kies kleur...'`. */
|
|
19
46
|
placeholder?: string
|
|
20
47
|
/** Label shown above reusable colours. */
|
|
21
48
|
suggestionsLabel?: string
|
|
49
|
+
/** Label shown above the theme roles. Defaults to `'Huisstijl'`. */
|
|
50
|
+
themeLabel?: string
|
|
22
51
|
}
|
|
23
52
|
|
|
24
53
|
export interface NexxtEditorColorProps {
|
|
@@ -47,26 +76,45 @@ export interface NexxtEditorColorProps {
|
|
|
47
76
|
messages?: EditorColorMessages
|
|
48
77
|
/** Hex colours available inside the picker panel as one-click suggestions. */
|
|
49
78
|
suggestions?: string[]
|
|
79
|
+
/**
|
|
80
|
+
* Theme roles offered above the suggestions. Picking one writes the role
|
|
81
|
+
* class for the current `property` instead of a literal colour. Which roles
|
|
82
|
+
* are offered is up to the consumer; an entry whose `hex` is not a 6- or
|
|
83
|
+
* 8-digit hex is left out. Without roles the section is not rendered.
|
|
84
|
+
*/
|
|
85
|
+
themeRoles?: EditorColorThemeRole[]
|
|
50
86
|
}
|
|
51
87
|
|
|
52
88
|
const DEFAULT_MESSAGES = {
|
|
53
89
|
placeholder: 'Kies kleur...',
|
|
54
90
|
suggestionsLabel: 'Gebruikte kleuren',
|
|
91
|
+
themeLabel: 'Huisstijl',
|
|
55
92
|
} satisfies Required<EditorColorMessages>
|
|
56
93
|
|
|
57
94
|
/**
|
|
58
|
-
* A
|
|
95
|
+
* A literal model value: one of the two prefixes, then a 6- or 8-digit hex between
|
|
59
96
|
* brackets. A `border-[…]` class is therefore read as unparseable, the same as
|
|
60
97
|
* any other class this component does not own. The
|
|
61
98
|
* alpha pair is accepted on read so a stored `#1777EA80` survives a round trip,
|
|
62
99
|
* even though the picker itself is opened without an alpha channel here.
|
|
63
100
|
*/
|
|
64
101
|
const VALUE_PATTERN = /^(bg|text)-\[(#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?)\]$/
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A role class: one of the two prefixes, then `theme-` and one of the five
|
|
105
|
+
* roles. Either prefix takes every role. A tint such as `bg-theme-main/10` is
|
|
106
|
+
* not a value this component writes, so it is read as unparseable.
|
|
107
|
+
*/
|
|
108
|
+
const ROLE_PATTERN = new RegExp(`^(?:bg|text)-theme-(${ROLES.join('|')})$`)
|
|
109
|
+
|
|
110
|
+
/** The picker throws on a colour it cannot read, so a role's hex is checked first. */
|
|
111
|
+
const ROLE_HEX_PATTERN = /^#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?$/
|
|
65
112
|
</script>
|
|
66
113
|
|
|
67
114
|
<script lang="ts" setup>
|
|
68
115
|
import { computed, watch } from 'vue'
|
|
69
116
|
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
117
|
+
import NexxtTooltip from '../../../atoms/Tooltip/Tooltip.vue'
|
|
70
118
|
import NexxtColorPicker from '../../../organisms/ColorPicker/ColorPicker.vue'
|
|
71
119
|
|
|
72
120
|
defineOptions({ name: 'NexxtEditorColor' })
|
|
@@ -77,17 +125,21 @@ const {
|
|
|
77
125
|
property = 'bg',
|
|
78
126
|
messages,
|
|
79
127
|
suggestions = [],
|
|
128
|
+
themeRoles = [],
|
|
80
129
|
} = defineProps<NexxtEditorColorProps>()
|
|
81
130
|
|
|
82
131
|
/**
|
|
83
132
|
* The chosen colour, as the Tailwind class to apply.
|
|
84
133
|
*
|
|
85
134
|
* Beware: Tailwind only generates an arbitrary class for text it finds in the
|
|
86
|
-
* source at build time. A value that arrives at runtime -- from a
|
|
87
|
-
* say -- therefore gets no CSS of its own; the consumer either
|
|
88
|
-
* into an inline style or safelists the classes it can produce.
|
|
89
|
-
* always parseable out of
|
|
90
|
-
* possible.
|
|
135
|
+
* source at build time. A literal value that arrives at runtime -- from a
|
|
136
|
+
* database, say -- therefore gets no CSS of its own; the consumer either
|
|
137
|
+
* translates it into an inline style or safelists the classes it can produce.
|
|
138
|
+
* The hex is always parseable out of a literal value, which is what makes that
|
|
139
|
+
* translation possible.
|
|
140
|
+
*
|
|
141
|
+
* A role class is a finite list, so it is static CSS -- but the consumer's:
|
|
142
|
+
* this library defines no `theme-*` colours.
|
|
91
143
|
*/
|
|
92
144
|
const model = defineModel<EditorColorValue>({ required: true })
|
|
93
145
|
|
|
@@ -109,12 +161,36 @@ const hex = computed(() => {
|
|
|
109
161
|
return captured && isHex(captured) ? captured : ''
|
|
110
162
|
})
|
|
111
163
|
|
|
164
|
+
/** The role out of the model, or `undefined` when it is not a role class. */
|
|
165
|
+
const storedRole = computed(() => {
|
|
166
|
+
const captured = ROLE_PATTERN.exec(model.value)?.[1]
|
|
167
|
+
return ROLES.find((role) => role === captured)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const offeredRoles = computed(() => themeRoles.filter((entry) => ROLE_HEX_PATTERN.test(entry.hex)))
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The stored role as the consumer offers it. Only a role class selects a role:
|
|
174
|
+
* a literal colour that happens to equal a role's hex stays a literal.
|
|
175
|
+
*/
|
|
176
|
+
const activeRole = computed(() => offeredRoles.value.find(({ role }) => role === storedRole.value))
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Picking the role's own colour -- from the suggestions, or typed -- is how a
|
|
180
|
+
* colour is detached from the theme without changing how it looks. The picker
|
|
181
|
+
* reports nothing when it is told to pick the string it already has, so a role's
|
|
182
|
+
* hex is handed over in its 8-digit form: with `show-alpha` off the picker never
|
|
183
|
+
* reports an alpha pair, which makes every pick a change.
|
|
184
|
+
*/
|
|
185
|
+
const pickerHex = (roleHex: string) => (roleHex.length === 7 ? `${roleHex}ff` : roleHex)
|
|
186
|
+
|
|
112
187
|
// An unparseable value -- empty, a token class such as `bg-red-500`, or copy
|
|
113
188
|
// from before this component -- leaves the picker empty and its placeholder
|
|
114
189
|
// visible, rather than being reported as a problem: the first pick writes a
|
|
115
|
-
// valid class back.
|
|
190
|
+
// valid class back. A role the consumer does not offer is shown the same way,
|
|
191
|
+
// because there is no label or hex to show it with; it is not written back.
|
|
116
192
|
const picked = computed({
|
|
117
|
-
get: () => hex.value,
|
|
193
|
+
get: () => (activeRole.value ? pickerHex(activeRole.value.hex) : hex.value),
|
|
118
194
|
// The picker clears its own value in no flow it offers, but an empty string
|
|
119
195
|
// would still wipe a perfectly good colour, so it is dropped rather than
|
|
120
196
|
// written through. The picker keeps rendering the model's colour.
|
|
@@ -123,13 +199,18 @@ const picked = computed({
|
|
|
123
199
|
},
|
|
124
200
|
})
|
|
125
201
|
|
|
202
|
+
const pickRole = (role: EditorColorRole) => {
|
|
203
|
+
model.value = `${property}-theme-${role}`
|
|
204
|
+
}
|
|
205
|
+
|
|
126
206
|
// `property` is a prop, so the parent can flip a background setting into a
|
|
127
207
|
// text-colour one while a value is already set. Rewriting the prefix here is
|
|
128
208
|
// what keeps the stored class and the prop from drifting apart.
|
|
129
209
|
watch(
|
|
130
210
|
() => property,
|
|
131
211
|
(next) => {
|
|
132
|
-
if (
|
|
212
|
+
if (storedRole.value) model.value = `${next}-theme-${storedRole.value}`
|
|
213
|
+
else if (hex.value) model.value = `${next}-[${hex.value}]`
|
|
133
214
|
},
|
|
134
215
|
)
|
|
135
216
|
</script>
|
|
@@ -157,6 +238,42 @@ watch(
|
|
|
157
238
|
:placeholder="copy.placeholder"
|
|
158
239
|
:suggestions="{ hex: suggestions }"
|
|
159
240
|
:suggestions-label="copy.suggestionsLabel"
|
|
160
|
-
|
|
241
|
+
:value-label="activeRole?.label"
|
|
242
|
+
>
|
|
243
|
+
<template v-if="offeredRoles.length" #presets>
|
|
244
|
+
<div class="flex flex-col gap-1">
|
|
245
|
+
<span class="extra-small-normal text-gray-500">{{ copy.themeLabel }}</span>
|
|
246
|
+
<!--
|
|
247
|
+
Same swatches and row as the picker's suggestions. Two roles often
|
|
248
|
+
share a hex (heading and text), so the tooltip is what tells them
|
|
249
|
+
apart; it repeats the accessible name rather than adding copy.
|
|
250
|
+
-->
|
|
251
|
+
<div class="flex flex-wrap gap-1.5">
|
|
252
|
+
<NexxtTooltip
|
|
253
|
+
v-for="{ role, label: roleLabel, hex: roleHex } in offeredRoles"
|
|
254
|
+
:key="role"
|
|
255
|
+
disable-closing-trigger
|
|
256
|
+
>
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
:aria-label="roleLabel"
|
|
260
|
+
:aria-pressed="role === activeRole?.role"
|
|
261
|
+
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"
|
|
262
|
+
@click="pickRole(role)"
|
|
263
|
+
>
|
|
264
|
+
<span
|
|
265
|
+
aria-hidden="true"
|
|
266
|
+
class="block size-6 rounded-sm shadow-[inset_0_0_0_1px_rgba(0,0,0,0.1)]"
|
|
267
|
+
:style="{ backgroundColor: roleHex }"
|
|
268
|
+
/>
|
|
269
|
+
</button>
|
|
270
|
+
<template #tooltip>
|
|
271
|
+
<span class="small-normal text-gray-900">{{ roleLabel }}</span>
|
|
272
|
+
</template>
|
|
273
|
+
</NexxtTooltip>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</template>
|
|
277
|
+
</NexxtColorPicker>
|
|
161
278
|
</NexxtEditorSettingsBlock>
|
|
162
279
|
</template>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The steps the slider can land on, as the class they are stored as and the
|
|
4
|
+
* pixels that class is. Not a scale Tailwind has names for -- `tracking-wide`
|
|
5
|
+
* is an em and moves with the font size, and the editor shows every size in
|
|
6
|
+
* px -- so every step is an arbitrary-value class.
|
|
7
|
+
*
|
|
8
|
+
* The middle step is no class at all: zero is what the text already does, and
|
|
9
|
+
* storing nothing for it keeps the "no class means default" every other
|
|
10
|
+
* setting has. The negative side is there on purpose: tightening a large
|
|
11
|
+
* heading is a real need.
|
|
12
|
+
*
|
|
13
|
+
* The consumer stores these classes and lists them in an `@source inline` rule,
|
|
14
|
+
* so adding or renaming a token is a breaking change even though the component
|
|
15
|
+
* API stays the same.
|
|
16
|
+
*/
|
|
17
|
+
export const EDITOR_LETTER_SPACING_STEPS = [
|
|
18
|
+
{ token: 'tracking-[-1px]', px: -1 },
|
|
19
|
+
{ token: 'tracking-[-0.5px]', px: -0.5 },
|
|
20
|
+
{ token: 'tracking-[-0.25px]', px: -0.25 },
|
|
21
|
+
{ token: null, px: 0 },
|
|
22
|
+
{ token: 'tracking-[0.25px]', px: 0.25 },
|
|
23
|
+
{ token: 'tracking-[0.5px]', px: 0.5 },
|
|
24
|
+
{ token: 'tracking-[1px]', px: 1 },
|
|
25
|
+
{ token: 'tracking-[1.5px]', px: 1.5 },
|
|
26
|
+
{ token: 'tracking-[2px]', px: 2 },
|
|
27
|
+
] as const
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The letter spacing as the editor needs it: the Tailwind class itself, so the
|
|
31
|
+
* consumer can put the model value straight on the element it styles. The
|
|
32
|
+
* default step is `null` on the model, not a member of this type.
|
|
33
|
+
*/
|
|
34
|
+
export type EditorLetterSpacingValue = NonNullable<
|
|
35
|
+
(typeof EDITOR_LETTER_SPACING_STEPS)[number]['token']
|
|
36
|
+
>
|
|
37
|
+
|
|
38
|
+
/** Overridable copy of the value readout. Every key is optional. */
|
|
39
|
+
export interface EditorLetterSpacingMessages {
|
|
40
|
+
/** Unit shown behind the value. Defaults to `'px'`. */
|
|
41
|
+
unit?: string
|
|
42
|
+
/** Decimal separator of the readout. Defaults to the Dutch `','`. */
|
|
43
|
+
decimalSeparator?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface NexxtEditorLetterSpacingProps {
|
|
47
|
+
/**
|
|
48
|
+
* Header text of the settings block. Defaults to the Dutch
|
|
49
|
+
* `'Letterafstand'`; the library has no i18n layer, so a consumer in another
|
|
50
|
+
* language passes its own copy in.
|
|
51
|
+
*/
|
|
52
|
+
label?: string
|
|
53
|
+
/**
|
|
54
|
+
* Copy of the value readout. Merged per key, so overriding one key keeps the
|
|
55
|
+
* defaults of the others. Copy is never translated inside the component.
|
|
56
|
+
*/
|
|
57
|
+
messages?: EditorLetterSpacingMessages
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEFAULT_MESSAGES = {
|
|
61
|
+
unit: 'px',
|
|
62
|
+
decimalSeparator: ',',
|
|
63
|
+
} satisfies Required<EditorLetterSpacingMessages>
|
|
64
|
+
|
|
65
|
+
/** Index of the no-class step, the middle of the list. */
|
|
66
|
+
const DEFAULT_INDEX = EDITOR_LETTER_SPACING_STEPS.findIndex(({ token }) => token === null)
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<script lang="ts" setup>
|
|
70
|
+
import { computed } from 'vue'
|
|
71
|
+
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
72
|
+
import NexxtSlider from '../../../atoms/Slider/Slider.vue'
|
|
73
|
+
|
|
74
|
+
defineOptions({ name: 'NexxtEditorLetterSpacing' })
|
|
75
|
+
|
|
76
|
+
const { label = 'Letterafstand', messages } = defineProps<NexxtEditorLetterSpacingProps>()
|
|
77
|
+
|
|
78
|
+
const emit = defineEmits<{
|
|
79
|
+
/**
|
|
80
|
+
* The slider was let go: the end of one drag, which a consumer can use to
|
|
81
|
+
* close an undo step. Arrow keys emit no pointerup.
|
|
82
|
+
*/
|
|
83
|
+
pointerup: [event: PointerEvent]
|
|
84
|
+
}>()
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The selected letter spacing, as the Tailwind class to apply. `null` -- also
|
|
88
|
+
* what an unset model is -- is the default step: no class at all, zero.
|
|
89
|
+
*/
|
|
90
|
+
const model = defineModel<EditorLetterSpacingValue | null>({ default: null })
|
|
91
|
+
|
|
92
|
+
const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
|
|
93
|
+
|
|
94
|
+
const maxIndex = EDITOR_LETTER_SPACING_STEPS.length - 1
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Position of the current value on the step list. A value that is not on the
|
|
98
|
+
* list -- a stored `tracking-wide`, say -- falls back to the default step
|
|
99
|
+
* rather than the first one: the first step here is the tightest spacing, and
|
|
100
|
+
* an unknown value is no reason to show text as squeezed. The first change
|
|
101
|
+
* writes a valid class back.
|
|
102
|
+
*/
|
|
103
|
+
const stepIndex = computed(() => {
|
|
104
|
+
const index = EDITOR_LETTER_SPACING_STEPS.findIndex(({ token }) => token === model.value)
|
|
105
|
+
|
|
106
|
+
return index === -1 ? DEFAULT_INDEX : index
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// The slider moves over the index, not over the px value, so its model is
|
|
110
|
+
// translated both ways here.
|
|
111
|
+
const sliderValue = computed({
|
|
112
|
+
get: () => [stepIndex.value],
|
|
113
|
+
set: ([index]) => {
|
|
114
|
+
model.value = EDITOR_LETTER_SPACING_STEPS[index].token
|
|
115
|
+
},
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
/** `-0,5 px`: a plain hyphen-minus for the sign, the decimal separator from the copy. */
|
|
119
|
+
const displayValue = computed(() => {
|
|
120
|
+
const { px } = EDITOR_LETTER_SPACING_STEPS[stepIndex.value]
|
|
121
|
+
|
|
122
|
+
return `${String(px).replace('.', copy.value.decimalSeparator)} ${copy.value.unit}`
|
|
123
|
+
})
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<template>
|
|
127
|
+
<NexxtEditorSettingsBlock :label="label">
|
|
128
|
+
<div class="flex items-center gap-4">
|
|
129
|
+
<!--
|
|
130
|
+
Slider's own `showValue` prints its model, which here is the step index
|
|
131
|
+
rather than the spacing, so the readout is rendered next to it instead.
|
|
132
|
+
The thumb has the same problem in `aria-valuenow`, which
|
|
133
|
+
`aria-valuetext` overrides with the string the readout shows. The block
|
|
134
|
+
label is a <p>, not a <label>, so the name goes on the thumb explicitly.
|
|
135
|
+
Slider binds `$attrs` on its root, so the pointerup listener lands there
|
|
136
|
+
and catches the release of the captured pointer.
|
|
137
|
+
-->
|
|
138
|
+
<NexxtSlider
|
|
139
|
+
v-model="sliderValue"
|
|
140
|
+
:min="0"
|
|
141
|
+
:max="maxIndex"
|
|
142
|
+
:step="1"
|
|
143
|
+
:aria-label="label"
|
|
144
|
+
:aria-valuetext="displayValue"
|
|
145
|
+
@pointerup="emit('pointerup', $event)"
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
<!--
|
|
149
|
+
`min-w-16` rather than the `min-w-12` of the other editor sliders:
|
|
150
|
+
`-0,25 px` does not fit in 12. That plus `shrink-0` fixes the width of
|
|
151
|
+
the box, so the slider keeps its size while dragging; `tabular-nums`
|
|
152
|
+
only stops the digits from jittering inside it.
|
|
153
|
+
gray-700, not the gray-500 Slider uses for its own readout: axe puts
|
|
154
|
+
gray-500 on white at 1.84:1, well under the 4.5:1 it needs at this size.
|
|
155
|
+
-->
|
|
156
|
+
<span
|
|
157
|
+
class="min-w-16 shrink-0 text-right small-normal whitespace-nowrap text-gray-700 tabular-nums"
|
|
158
|
+
>
|
|
159
|
+
{{ displayValue }}
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
</NexxtEditorSettingsBlock>
|
|
163
|
+
</template>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The steps the slider can land on, as the class they are stored as and the
|
|
4
|
+
* pixels that class is. The `leading-*` classes are Tailwind's spacing scale
|
|
5
|
+
* (calc of `--spacing`, 4px a step), so `leading-6` really is 24px -- and a
|
|
6
|
+
* line height set here does not scale with the font size; that is the deal the
|
|
7
|
+
* fixed steps make, and nothing here tries to compensate for it.
|
|
8
|
+
*
|
|
9
|
+
* The first step is no class at all: the element keeps its own line height.
|
|
10
|
+
* It has no px of its own, so the readout names it instead of measuring it.
|
|
11
|
+
*
|
|
12
|
+
* The consumer stores these classes and lists them in an `@source inline` rule,
|
|
13
|
+
* so adding or renaming a token is a breaking change even though the component
|
|
14
|
+
* API stays the same.
|
|
15
|
+
*/
|
|
16
|
+
export const EDITOR_LINE_HEIGHT_STEPS = [
|
|
17
|
+
{ token: null, px: null },
|
|
18
|
+
{ token: 'leading-3', px: 12 },
|
|
19
|
+
{ token: 'leading-4', px: 16 },
|
|
20
|
+
{ token: 'leading-5', px: 20 },
|
|
21
|
+
{ token: 'leading-6', px: 24 },
|
|
22
|
+
{ token: 'leading-7', px: 28 },
|
|
23
|
+
{ token: 'leading-8', px: 32 },
|
|
24
|
+
{ token: 'leading-9', px: 36 },
|
|
25
|
+
{ token: 'leading-10', px: 40 },
|
|
26
|
+
{ token: 'leading-11', px: 44 },
|
|
27
|
+
{ token: 'leading-12', px: 48 },
|
|
28
|
+
{ token: 'leading-14', px: 56 },
|
|
29
|
+
{ token: 'leading-16', px: 64 },
|
|
30
|
+
] as const
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The line height as the editor needs it: the Tailwind class itself, so the
|
|
34
|
+
* consumer can put the model value straight on the element it styles. The
|
|
35
|
+
* default step is `null` on the model, not a member of this type.
|
|
36
|
+
*/
|
|
37
|
+
export type EditorLineHeightValue = NonNullable<(typeof EDITOR_LINE_HEIGHT_STEPS)[number]['token']>
|
|
38
|
+
|
|
39
|
+
/** Overridable copy of the value readout. Every key is optional. */
|
|
40
|
+
export interface EditorLineHeightMessages {
|
|
41
|
+
/** Unit shown behind the value. Defaults to `'px'`. */
|
|
42
|
+
unit?: string
|
|
43
|
+
/**
|
|
44
|
+
* Readout of the first step, no class, which has no px size. Defaults to
|
|
45
|
+
* the Dutch `'Standaard'`.
|
|
46
|
+
*/
|
|
47
|
+
default?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface NexxtEditorLineHeightProps {
|
|
51
|
+
/**
|
|
52
|
+
* Header text of the settings block. Defaults to the Dutch
|
|
53
|
+
* `'Regelafstand'`; the library has no i18n layer, so a consumer in another
|
|
54
|
+
* language passes its own copy in.
|
|
55
|
+
*/
|
|
56
|
+
label?: string
|
|
57
|
+
/**
|
|
58
|
+
* Copy of the value readout. Merged per key, so overriding one key keeps the
|
|
59
|
+
* defaults of the others. Copy is never translated inside the component.
|
|
60
|
+
*/
|
|
61
|
+
messages?: EditorLineHeightMessages
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const DEFAULT_MESSAGES = {
|
|
65
|
+
unit: 'px',
|
|
66
|
+
default: 'Standaard',
|
|
67
|
+
} satisfies Required<EditorLineHeightMessages>
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<script lang="ts" setup>
|
|
71
|
+
import { computed } from 'vue'
|
|
72
|
+
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
73
|
+
import NexxtSlider from '../../../atoms/Slider/Slider.vue'
|
|
74
|
+
|
|
75
|
+
defineOptions({ name: 'NexxtEditorLineHeight' })
|
|
76
|
+
|
|
77
|
+
const { label = 'Regelafstand', messages } = defineProps<NexxtEditorLineHeightProps>()
|
|
78
|
+
|
|
79
|
+
const emit = defineEmits<{
|
|
80
|
+
/**
|
|
81
|
+
* The slider was let go: the end of one drag, which a consumer can use to
|
|
82
|
+
* close an undo step. Arrow keys emit no pointerup.
|
|
83
|
+
*/
|
|
84
|
+
pointerup: [event: PointerEvent]
|
|
85
|
+
}>()
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The selected line height, as the Tailwind class to apply. `null` -- also
|
|
89
|
+
* what an unset model is -- is the default step: no class at all.
|
|
90
|
+
*/
|
|
91
|
+
const model = defineModel<EditorLineHeightValue | null>({ default: null })
|
|
92
|
+
|
|
93
|
+
const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
|
|
94
|
+
|
|
95
|
+
const maxIndex = EDITOR_LINE_HEIGHT_STEPS.length - 1
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Position of the current value on the step list. A value that is not on the
|
|
99
|
+
* list -- a stored `leading-13`, say -- falls back to the first step, the
|
|
100
|
+
* default, instead of leaving the thumb off the track; the first change writes
|
|
101
|
+
* a valid class back.
|
|
102
|
+
*/
|
|
103
|
+
const stepIndex = computed(() =>
|
|
104
|
+
Math.max(
|
|
105
|
+
0,
|
|
106
|
+
EDITOR_LINE_HEIGHT_STEPS.findIndex(({ token }) => token === model.value),
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// The slider moves over the index, not over the px value, so its model is
|
|
111
|
+
// translated both ways here.
|
|
112
|
+
const sliderValue = computed({
|
|
113
|
+
get: () => [stepIndex.value],
|
|
114
|
+
set: ([index]) => {
|
|
115
|
+
model.value = EDITOR_LINE_HEIGHT_STEPS[index].token
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const displayValue = computed(() => {
|
|
120
|
+
const { px } = EDITOR_LINE_HEIGHT_STEPS[stepIndex.value]
|
|
121
|
+
|
|
122
|
+
return px === null ? copy.value.default : `${px} ${copy.value.unit}`
|
|
123
|
+
})
|
|
124
|
+
</script>
|
|
125
|
+
|
|
126
|
+
<template>
|
|
127
|
+
<NexxtEditorSettingsBlock :label="label">
|
|
128
|
+
<div class="flex items-center gap-4">
|
|
129
|
+
<!--
|
|
130
|
+
Slider's own `showValue` prints its model, which here is the step index
|
|
131
|
+
rather than the line height, so the readout is rendered next to it
|
|
132
|
+
instead. The thumb has the same problem in `aria-valuenow`, which
|
|
133
|
+
`aria-valuetext` overrides with the string the readout shows. The block
|
|
134
|
+
label is a <p>, not a <label>, so the name goes on the thumb explicitly.
|
|
135
|
+
Slider binds `$attrs` on its root, so the pointerup listener lands there
|
|
136
|
+
and catches the release of the captured pointer.
|
|
137
|
+
-->
|
|
138
|
+
<NexxtSlider
|
|
139
|
+
v-model="sliderValue"
|
|
140
|
+
:min="0"
|
|
141
|
+
:max="maxIndex"
|
|
142
|
+
:step="1"
|
|
143
|
+
:aria-label="label"
|
|
144
|
+
:aria-valuetext="displayValue"
|
|
145
|
+
@pointerup="emit('pointerup', $event)"
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
<!--
|
|
149
|
+
`min-w-16` so `Standaard` fits and the slider keeps its size moving off
|
|
150
|
+
the default step; `tabular-nums` stops the digits from jittering.
|
|
151
|
+
gray-700, not the gray-500 Slider uses for its own readout: axe puts
|
|
152
|
+
gray-500 on white at 1.84:1, well under the 4.5:1 it needs at this size.
|
|
153
|
+
-->
|
|
154
|
+
<span
|
|
155
|
+
class="min-w-16 shrink-0 text-right small-normal whitespace-nowrap text-gray-700 tabular-nums"
|
|
156
|
+
>
|
|
157
|
+
{{ displayValue }}
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
</NexxtEditorSettingsBlock>
|
|
161
|
+
</template>
|
|
@@ -35,10 +35,20 @@ export interface NexxtColorPickerProps {
|
|
|
35
35
|
suggestions?: ColorSuggestions
|
|
36
36
|
/** Accessible and visible label for the suggestions. */
|
|
37
37
|
suggestionsLabel?: string
|
|
38
|
+
/**
|
|
39
|
+
* Text shown in the trigger in place of the formatted colour, for a value that has a name of
|
|
40
|
+
* its own, such as a theme role. Only shown while a colour is set.
|
|
41
|
+
*/
|
|
42
|
+
valueLabel?: string
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
defineOptions({ name: 'NexxtColorPicker' })
|
|
41
46
|
|
|
47
|
+
defineSlots<{
|
|
48
|
+
/** Named colours a wrapper offers next to the free picker; rendered above the suggestions. */
|
|
49
|
+
presets?(): unknown
|
|
50
|
+
}>()
|
|
51
|
+
|
|
42
52
|
const PICKER_ID = 'color-picker-' + Math.random().toString(36).slice(2, 10)
|
|
43
53
|
|
|
44
54
|
const props = withDefaults(defineProps<NexxtColorPickerProps>(), {
|
|
@@ -84,7 +94,9 @@ const hexInputError = ref('')
|
|
|
84
94
|
const channelInputErrors = ref<Partial<Record<ColorChannel, string>>>({})
|
|
85
95
|
|
|
86
96
|
const hexColor = computed(() => colorToString(colorObj.value, 'hex'))
|
|
87
|
-
const displayValue = computed(() =>
|
|
97
|
+
const displayValue = computed(() =>
|
|
98
|
+
model.value ? props.valueLabel || fmt(colorObj.value, activeFormat.value) : '',
|
|
99
|
+
)
|
|
88
100
|
const colorSuggestions = computed(() => {
|
|
89
101
|
const values = new Set<string>()
|
|
90
102
|
|
|
@@ -443,6 +455,7 @@ watch(
|
|
|
443
455
|
</ColorFieldRoot>
|
|
444
456
|
</div>
|
|
445
457
|
</div>
|
|
458
|
+
<slot name="presets" />
|
|
446
459
|
<div v-if="colorSuggestions.length" class="flex flex-col gap-1">
|
|
447
460
|
<span class="extra-small-normal text-gray-500">{{ suggestionsLabel }}</span>
|
|
448
461
|
<div class="flex flex-wrap gap-1.5">
|
package/src/components.json
CHANGED
|
@@ -80,6 +80,8 @@
|
|
|
80
80
|
"NexxtEditorGap": "components/editor/molecules/EditorGap/EditorGap.vue",
|
|
81
81
|
"NexxtEditorHeadingType": "components/editor/molecules/EditorHeadingType/EditorHeadingType.vue",
|
|
82
82
|
"NexxtEditorJustifyContent": "components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue",
|
|
83
|
+
"NexxtEditorLetterSpacing": "components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue",
|
|
84
|
+
"NexxtEditorLineHeight": "components/editor/molecules/EditorLineHeight/EditorLineHeight.vue",
|
|
83
85
|
"NexxtEditorMaxWidth": "components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue",
|
|
84
86
|
"NexxtEditorPadding": "components/editor/molecules/EditorPadding/EditorPadding.vue",
|
|
85
87
|
"NexxtEditorRadius": "components/editor/molecules/EditorRadius/EditorRadius.vue",
|
package/src/env.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// vite-svg-loader compiles a plain SVG import to a Vue component, but
|
|
4
|
+
// vite/client above declares `*.svg` as a URL string, and of two identical
|
|
5
|
+
// wildcard patterns the one loaded first wins. TypeScript picks the pattern
|
|
6
|
+
// with the longest prefix, so the library's own `@ui-assets` SVGs get their
|
|
7
|
+
// component type here regardless of load order.
|
|
8
|
+
declare module '@ui-assets/*.svg' {
|
|
4
9
|
import type { DefineComponent } from 'vue'
|
|
5
10
|
const component: DefineComponent<object, object, unknown>
|
|
6
11
|
export default component
|
package/src/index.ts
CHANGED
|
@@ -49,8 +49,12 @@ export type {
|
|
|
49
49
|
EditorGapValue,
|
|
50
50
|
} from './components/editor/molecules/EditorGap/EditorGap.vue'
|
|
51
51
|
export type {
|
|
52
|
+
EditorColorLiteralValue,
|
|
52
53
|
EditorColorMessages,
|
|
53
54
|
EditorColorProperty,
|
|
55
|
+
EditorColorRole,
|
|
56
|
+
EditorColorRoleValue,
|
|
57
|
+
EditorColorThemeRole,
|
|
54
58
|
EditorColorValue,
|
|
55
59
|
} from './components/editor/molecules/EditorColor/EditorColor.vue'
|
|
56
60
|
export type {
|
|
@@ -79,6 +83,16 @@ export type {
|
|
|
79
83
|
EditorMaxWidthMessages,
|
|
80
84
|
EditorMaxWidthValue,
|
|
81
85
|
} from './components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue'
|
|
86
|
+
export { EDITOR_LINE_HEIGHT_STEPS } from './components/editor/molecules/EditorLineHeight/EditorLineHeight.vue'
|
|
87
|
+
export type {
|
|
88
|
+
EditorLineHeightMessages,
|
|
89
|
+
EditorLineHeightValue,
|
|
90
|
+
} from './components/editor/molecules/EditorLineHeight/EditorLineHeight.vue'
|
|
91
|
+
export { EDITOR_LETTER_SPACING_STEPS } from './components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue'
|
|
92
|
+
export type {
|
|
93
|
+
EditorLetterSpacingMessages,
|
|
94
|
+
EditorLetterSpacingValue,
|
|
95
|
+
} from './components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue'
|
|
82
96
|
export type { EditorPaddingMessages } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
|
|
83
97
|
export type {
|
|
84
98
|
NexxtTemplateCardContentFeature,
|
|
@@ -111,6 +125,8 @@ export { default as NexxtEditorFlexDirection } from './components/editor/molecul
|
|
|
111
125
|
export { default as NexxtEditorGap } from './components/editor/molecules/EditorGap/EditorGap.vue'
|
|
112
126
|
export { default as NexxtEditorHeadingType } from './components/editor/molecules/EditorHeadingType/EditorHeadingType.vue'
|
|
113
127
|
export { default as NexxtEditorJustifyContent } from './components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue'
|
|
128
|
+
export { default as NexxtEditorLetterSpacing } from './components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue'
|
|
129
|
+
export { default as NexxtEditorLineHeight } from './components/editor/molecules/EditorLineHeight/EditorLineHeight.vue'
|
|
114
130
|
export { default as NexxtEditorMaxWidth } from './components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue'
|
|
115
131
|
export { default as NexxtEditorPadding } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
|
|
116
132
|
export { default as NexxtEditorRadius } from './components/editor/molecules/EditorRadius/EditorRadius.vue'
|