@nexxtmove/ui 2.4.0 → 2.6.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/EditorBorder/EditorBorder.vue.d.ts +12 -0
- package/dist/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue.d.ts +16 -0
- package/dist/index.js +1045 -1009
- package/package.json +1 -1
- package/src/components/editor/molecules/EditorBorder/EditorBorder.vue +58 -2
- package/src/components/editor/molecules/EditorColor/_ColorField.vue +7 -29
- package/src/components/editor/molecules/EditorColor/_ThemeRoleSwatches.vue +57 -0
- package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.vue +3 -3
- package/src/components/molecules/Select/Select.vue +10 -4
- package/src/components/organisms/CountrySelect/CountrySelect.vue +9 -4
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import type { EditorColorThemeRole } from '../EditorColor/colorClass'
|
|
3
|
+
|
|
2
4
|
/**
|
|
3
5
|
* The widths the slider can land on: the Tailwind class and the pixels it
|
|
4
6
|
* renders. These are Tailwind's own defaults (`border` is 1px, then 2, 4 and 8)
|
|
@@ -29,6 +31,8 @@ export interface EditorBorderMessages {
|
|
|
29
31
|
placeholder?: string
|
|
30
32
|
/** Accessible name of the width slider. Defaults to `'Dikte'`. */
|
|
31
33
|
width?: string
|
|
34
|
+
/** Label shown above the theme roles. Defaults to `'Huisstijl'`. */
|
|
35
|
+
themeLabel?: string
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export interface NexxtEditorBorderProps {
|
|
@@ -52,12 +56,22 @@ export interface NexxtEditorBorderProps {
|
|
|
52
56
|
* component.
|
|
53
57
|
*/
|
|
54
58
|
messages?: EditorBorderMessages
|
|
59
|
+
/**
|
|
60
|
+
* Theme roles offered as swatches in the colour panel, the same ones
|
|
61
|
+
* NexxtEditorColor takes. A role is a shortcut to its hex, not a binding: a
|
|
62
|
+
* pick writes `border-[#hex]` exactly as a picked colour does, and a swatch
|
|
63
|
+
* shows as selected while the border's hex equals the role's, whatever the
|
|
64
|
+
* case. An entry with an unreadable hex is left out. Defaults to `[]`, which
|
|
65
|
+
* renders no theme section.
|
|
66
|
+
*/
|
|
67
|
+
themeRoles?: EditorColorThemeRole[]
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
const DEFAULT_MESSAGES = {
|
|
58
71
|
unit: 'px',
|
|
59
72
|
placeholder: 'Kies kleur...',
|
|
60
73
|
width: 'Dikte',
|
|
74
|
+
themeLabel: 'Huisstijl',
|
|
61
75
|
} satisfies Required<EditorBorderMessages>
|
|
62
76
|
|
|
63
77
|
/**
|
|
@@ -73,10 +87,17 @@ import { computed } from 'vue'
|
|
|
73
87
|
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
74
88
|
import NexxtSlider from '../../../atoms/Slider/Slider.vue'
|
|
75
89
|
import NexxtColorPicker from '../../../organisms/ColorPicker/ColorPicker.vue'
|
|
90
|
+
import NexxtThemeRoleSwatches from '../EditorColor/_ThemeRoleSwatches.vue'
|
|
91
|
+
import { ROLE_HEX_PATTERN } from '../EditorColor/colorClass'
|
|
76
92
|
|
|
77
93
|
defineOptions({ name: 'NexxtEditorBorder' })
|
|
78
94
|
|
|
79
|
-
const {
|
|
95
|
+
const {
|
|
96
|
+
label = 'Rand',
|
|
97
|
+
switchable = true,
|
|
98
|
+
messages,
|
|
99
|
+
themeRoles = [],
|
|
100
|
+
} = defineProps<NexxtEditorBorderProps>()
|
|
80
101
|
|
|
81
102
|
/**
|
|
82
103
|
* The border as the editor needs it: one class string that carries both halves
|
|
@@ -164,6 +185,32 @@ const picked = computed({
|
|
|
164
185
|
},
|
|
165
186
|
})
|
|
166
187
|
|
|
188
|
+
/** The picker throws on a colour it cannot read, so a role's hex is checked first. */
|
|
189
|
+
const offeredRoles = computed(() => themeRoles.filter((entry) => ROLE_HEX_PATTERN.test(entry.hex)))
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A colour without its alpha pair, as `#rrggbb`. The picker is opened without
|
|
193
|
+
* an alpha channel here, so a role's alpha would be stored but never shown;
|
|
194
|
+
* the role is offered, written and compared by its opaque colour instead.
|
|
195
|
+
*/
|
|
196
|
+
const opaque = (value: string) => value.slice(0, 7).toLowerCase()
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* A role is only a shortcut to a hex here, so its swatch is pressed by colour:
|
|
200
|
+
* two roles that share a hex are pressed together, and a hand-picked colour
|
|
201
|
+
* equal to a role's presses that role too.
|
|
202
|
+
*/
|
|
203
|
+
const isRolePressed = (entry: EditorColorThemeRole) =>
|
|
204
|
+
hex.value !== '' && opaque(hex.value) === opaque(entry.hex)
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Written through the picker's own path, so a role's hex is stored as any
|
|
208
|
+
* picked hex is: six digits, in the case the consumer passed it.
|
|
209
|
+
*/
|
|
210
|
+
const pickRole = (entry: EditorColorThemeRole) => {
|
|
211
|
+
picked.value = entry.hex.slice(0, 7)
|
|
212
|
+
}
|
|
213
|
+
|
|
167
214
|
const displayValue = computed(() => `${EDITOR_BORDER_STEPS[stepIndex.value].px} ${copy.value.unit}`)
|
|
168
215
|
</script>
|
|
169
216
|
|
|
@@ -216,7 +263,16 @@ const displayValue = computed(() => `${EDITOR_BORDER_STEPS[stepIndex.value].px}
|
|
|
216
263
|
:formats="['hex']"
|
|
217
264
|
:show-alpha="false"
|
|
218
265
|
:placeholder="copy.placeholder"
|
|
219
|
-
|
|
266
|
+
>
|
|
267
|
+
<template v-if="offeredRoles.length" #presets>
|
|
268
|
+
<NexxtThemeRoleSwatches
|
|
269
|
+
:roles="offeredRoles"
|
|
270
|
+
:label="copy.themeLabel"
|
|
271
|
+
:is-pressed="isRolePressed"
|
|
272
|
+
@pick="pickRole"
|
|
273
|
+
/>
|
|
274
|
+
</template>
|
|
275
|
+
</NexxtColorPicker>
|
|
220
276
|
</div>
|
|
221
277
|
</NexxtEditorSettingsBlock>
|
|
222
278
|
</template>
|
|
@@ -56,8 +56,8 @@ const OPAQUE_ALPHA_PATTERN = /^(#[0-9A-Fa-f]{6})[fF]{2}$/
|
|
|
56
56
|
|
|
57
57
|
<script lang="ts" setup generic="P extends string">
|
|
58
58
|
import { computed, ref, watch } from 'vue'
|
|
59
|
-
import NexxtTooltip from '../../../atoms/Tooltip/Tooltip.vue'
|
|
60
59
|
import NexxtColorPicker from '../../../organisms/ColorPicker/ColorPicker.vue'
|
|
60
|
+
import NexxtThemeRoleSwatches from './_ThemeRoleSwatches.vue'
|
|
61
61
|
import type { ColorPickerGradient } from '../../../organisms/ColorPicker/ColorPicker.vue'
|
|
62
62
|
import { clampCenter } from '../../../organisms/ColorPicker/gradientGeometry'
|
|
63
63
|
import { COLOR_ROLES, ROLE_HEX_PATTERN, isHex, parseColorClass } from './colorClass'
|
|
@@ -412,34 +412,12 @@ watch(
|
|
|
412
412
|
@close="onClose"
|
|
413
413
|
>
|
|
414
414
|
<template v-if="offeredRoles.length" #presets="{ preset, selectPreset }">
|
|
415
|
-
<
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
-->
|
|
422
|
-
<div class="flex flex-wrap gap-1.5">
|
|
423
|
-
<NexxtTooltip v-for="entry in offeredRoles" :key="entry.role" disable-closing-trigger>
|
|
424
|
-
<button
|
|
425
|
-
type="button"
|
|
426
|
-
:aria-label="entry.label"
|
|
427
|
-
:aria-pressed="isRolePressed(entry.role, preset)"
|
|
428
|
-
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"
|
|
429
|
-
@click="onRoleClick(entry, selectPreset)"
|
|
430
|
-
>
|
|
431
|
-
<span
|
|
432
|
-
aria-hidden="true"
|
|
433
|
-
class="block size-6 rounded-sm shadow-[inset_0_0_0_1px_rgba(0,0,0,0.1)]"
|
|
434
|
-
:style="{ backgroundColor: entry.hex }"
|
|
435
|
-
/>
|
|
436
|
-
</button>
|
|
437
|
-
<template #tooltip>
|
|
438
|
-
<span class="small-normal text-gray-900">{{ entry.label }}</span>
|
|
439
|
-
</template>
|
|
440
|
-
</NexxtTooltip>
|
|
441
|
-
</div>
|
|
442
|
-
</div>
|
|
415
|
+
<NexxtThemeRoleSwatches
|
|
416
|
+
:roles="offeredRoles"
|
|
417
|
+
:label="themeLabel"
|
|
418
|
+
:is-pressed="(entry) => isRolePressed(entry.role, preset)"
|
|
419
|
+
@pick="(entry) => onRoleClick(entry, selectPreset)"
|
|
420
|
+
/>
|
|
443
421
|
</template>
|
|
444
422
|
</NexxtColorPicker>
|
|
445
423
|
</template>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import NexxtTooltip from '../../../atoms/Tooltip/Tooltip.vue'
|
|
3
|
+
import type { EditorColorThemeRole } from './colorClass'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The theme roles as one row of swatches under a section title, for the
|
|
7
|
+
* `presets` slot of NexxtColorPicker. What a pick writes, and when a swatch is
|
|
8
|
+
* pressed, is up to the field that renders it: NexxtEditorColor writes the role
|
|
9
|
+
* class, NexxtEditorBorder the role's hex.
|
|
10
|
+
*/
|
|
11
|
+
defineOptions({ name: 'NexxtThemeRoleSwatches' })
|
|
12
|
+
|
|
13
|
+
defineProps<{
|
|
14
|
+
/** The roles to show, already stripped of entries with an unreadable hex. */
|
|
15
|
+
roles: EditorColorThemeRole[]
|
|
16
|
+
/** Section title above the swatches. */
|
|
17
|
+
label: string
|
|
18
|
+
/** Whether the swatch of a role shows as selected. */
|
|
19
|
+
isPressed: (entry: EditorColorThemeRole) => boolean
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
/** A swatch was clicked, or activated with the keyboard. */
|
|
24
|
+
pick: [entry: EditorColorThemeRole]
|
|
25
|
+
}>()
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div class="flex flex-col gap-1">
|
|
30
|
+
<span class="extra-small-normal text-gray-500">{{ label }}</span>
|
|
31
|
+
<!--
|
|
32
|
+
Same swatches and row as the picker's suggestions. Two roles often
|
|
33
|
+
share a hex (heading and text), so the tooltip is what tells them
|
|
34
|
+
apart; it repeats the accessible name rather than adding copy.
|
|
35
|
+
-->
|
|
36
|
+
<div class="flex flex-wrap gap-1.5">
|
|
37
|
+
<NexxtTooltip v-for="entry in roles" :key="entry.role" disable-closing-trigger>
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
:aria-label="entry.label"
|
|
41
|
+
:aria-pressed="isPressed(entry)"
|
|
42
|
+
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"
|
|
43
|
+
@click="emit('pick', entry)"
|
|
44
|
+
>
|
|
45
|
+
<span
|
|
46
|
+
aria-hidden="true"
|
|
47
|
+
class="block size-6 rounded-sm shadow-[inset_0_0_0_1px_rgba(0,0,0,0.1)]"
|
|
48
|
+
:style="{ backgroundColor: entry.hex }"
|
|
49
|
+
/>
|
|
50
|
+
</button>
|
|
51
|
+
<template #tooltip>
|
|
52
|
+
<span class="small-normal text-gray-900">{{ entry.label }}</span>
|
|
53
|
+
</template>
|
|
54
|
+
</NexxtTooltip>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
@@ -110,9 +110,9 @@ const selected = computed({
|
|
|
110
110
|
<template>
|
|
111
111
|
<NexxtEditorSettingsBlock :label="label">
|
|
112
112
|
<!--
|
|
113
|
-
Select is `inline-flex` and
|
|
114
|
-
`w-full` it would sit at
|
|
115
|
-
class lands on Select's root
|
|
113
|
+
Select is `inline-flex` and 240px wide by default (capped at its
|
|
114
|
+
container), so without `w-full` it would sit at 240px instead of filling
|
|
115
|
+
the block. The class lands on Select's root; the trigger grows with it.
|
|
116
116
|
-->
|
|
117
117
|
<!--
|
|
118
118
|
The block label is a <p>, not a <label>, so the combobox would have no
|
|
@@ -151,7 +151,7 @@ watch(model, () => {
|
|
|
151
151
|
</script>
|
|
152
152
|
|
|
153
153
|
<template>
|
|
154
|
-
<div class="inline-flex flex-col gap-1">
|
|
154
|
+
<div class="inline-flex flex-col gap-1 [:where(&)]:max-w-full">
|
|
155
155
|
<label v-if="label" class="justify-start small-normal text-gray-700" :for="selectId">
|
|
156
156
|
{{ label }}
|
|
157
157
|
<span v-if="required" class="text-brick-500">*</span>
|
|
@@ -165,6 +165,10 @@ watch(model, () => {
|
|
|
165
165
|
@open="activeIndex = selectedIndex >= 0 ? selectedIndex : activeIndex"
|
|
166
166
|
>
|
|
167
167
|
<template #trigger="{ isOpen, toggle }">
|
|
168
|
+
<!-- The 240px default width lives here, as the trigger's flex basis: it
|
|
169
|
+
holds in a shrink-to-fit wrapper, and the trigger shrinks below it
|
|
170
|
+
(min-w-0) or grows past it with any width the root gets. The root's
|
|
171
|
+
max-w-full, at zero specificity, keeps it inside a narrow column. -->
|
|
168
172
|
<div
|
|
169
173
|
:id="selectId"
|
|
170
174
|
role="combobox"
|
|
@@ -174,7 +178,7 @@ watch(model, () => {
|
|
|
174
178
|
:aria-controls="isOpen ? listboxId : undefined"
|
|
175
179
|
:aria-activedescendant="isOpen && activeIndex >= 0 ? optionId(activeIndex) : undefined"
|
|
176
180
|
tabindex="0"
|
|
177
|
-
class="group flex w-
|
|
181
|
+
class="group flex w-60 min-w-0 grow cursor-pointer items-center gap-2 rounded-lg ring transition-colors ease-out focus:ring-cornflower-blue-500 focus:outline-none"
|
|
178
182
|
:class="[
|
|
179
183
|
error ? 'ring-brick-400' : 'ring-gray-200',
|
|
180
184
|
{ 'pl-3': triggerIcon, 'pr-3': true },
|
|
@@ -185,11 +189,13 @@ watch(model, () => {
|
|
|
185
189
|
<span v-if="triggerIcon" class="flex items-center">
|
|
186
190
|
<NexxtIcon :name="triggerIcon" />
|
|
187
191
|
</span>
|
|
192
|
+
<!-- The ellipsis needs a block box: text directly in a flex container is
|
|
193
|
+
an anonymous flex item that `truncate` does not reach. -->
|
|
188
194
|
<span
|
|
189
|
-
class="flex h-10
|
|
195
|
+
class="flex h-10 min-w-0 flex-1 items-center text-sm"
|
|
190
196
|
:class="[selectedLabel ? 'text-gray-900' : 'text-gray-400', { 'pl-3': !triggerIcon }]"
|
|
191
197
|
>
|
|
192
|
-
{{ selectedLabel ?? placeholder }}
|
|
198
|
+
<span class="truncate">{{ selectedLabel ?? placeholder }}</span>
|
|
193
199
|
</span>
|
|
194
200
|
<NexxtIcon
|
|
195
201
|
name="chevron-down"
|
|
@@ -71,7 +71,7 @@ const onOpen = async () => {
|
|
|
71
71
|
</script>
|
|
72
72
|
|
|
73
73
|
<template>
|
|
74
|
-
<div class="inline-flex flex-col gap-1">
|
|
74
|
+
<div class="inline-flex flex-col gap-1 [:where(&)]:max-w-full">
|
|
75
75
|
<label v-if="label" class="justify-start small-normal text-gray-700" :for="selectId">
|
|
76
76
|
{{ label }}
|
|
77
77
|
<span v-if="required" class="text-brick-500">*</span>
|
|
@@ -79,20 +79,24 @@ const onOpen = async () => {
|
|
|
79
79
|
|
|
80
80
|
<NexxtFloatingPanel placement="bottom" match-width @open="onOpen">
|
|
81
81
|
<template #trigger="{ isOpen, toggle }">
|
|
82
|
+
<!-- The 240px default width lives here, as the trigger's flex basis: it
|
|
83
|
+
holds in a shrink-to-fit wrapper, and the trigger shrinks below it
|
|
84
|
+
(min-w-0) or grows past it with any width the root gets. The root's
|
|
85
|
+
max-w-full, at zero specificity, keeps it inside a narrow column. -->
|
|
82
86
|
<div
|
|
83
87
|
:id="selectId"
|
|
84
88
|
role="combobox"
|
|
85
89
|
:aria-expanded="isOpen"
|
|
86
90
|
aria-haspopup="listbox"
|
|
87
91
|
tabindex="0"
|
|
88
|
-
class="group flex w-
|
|
92
|
+
class="group flex w-60 min-w-0 grow cursor-pointer items-center gap-2 rounded-lg pr-3 ring transition-colors ease-out focus:ring-cornflower-blue-500 focus:outline-none"
|
|
89
93
|
:class="error ? 'ring-brick-400' : 'ring-gray-200'"
|
|
90
94
|
@click="toggle"
|
|
91
95
|
@keydown.enter.prevent="toggle"
|
|
92
96
|
@keydown.space.prevent="toggle"
|
|
93
97
|
>
|
|
94
98
|
<span
|
|
95
|
-
class="flex h-10
|
|
99
|
+
class="flex h-10 min-w-0 flex-1 items-center gap-2 pl-3 text-sm"
|
|
96
100
|
:class="selectedCountry ? 'text-gray-900' : 'text-gray-400'"
|
|
97
101
|
>
|
|
98
102
|
<span
|
|
@@ -100,7 +104,8 @@ const onOpen = async () => {
|
|
|
100
104
|
:class="`fi fi-${selectedCountry.code}`"
|
|
101
105
|
class="shrink-0"
|
|
102
106
|
/>
|
|
103
|
-
|
|
107
|
+
<!-- A block box of its own, or `truncate` would not reach the text. -->
|
|
108
|
+
<span class="truncate">{{ selectedCountry?.name ?? placeholder }}</span>
|
|
104
109
|
</span>
|
|
105
110
|
<NexxtIcon
|
|
106
111
|
name="chevron-down"
|