@nexxtmove/ui 1.40.0 → 1.41.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/EditorFontSize/EditorFontSize.vue.d.ts +117 -0
- package/dist/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue.d.ts +40 -24
- package/dist/components/editor/molecules/EditorLineHeight/EditorLineHeight.vue.d.ts +52 -39
- package/dist/components/molecules/PickerList/PickerList.vue.d.ts +14 -2
- package/dist/components/organisms/FontPicker/FontPicker.vue.d.ts +125 -0
- package/dist/components/organisms/FontPicker/useFontPreviewLoader.d.ts +20 -0
- package/dist/components/organisms/PickerPanel/PickerPanel.vue.d.ts +11 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +2643 -2106
- package/dist/nuxt.js +2 -0
- package/package.json +1 -1
- package/src/components/atoms/Skeleton/Skeleton.vue +1 -1
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue +6 -1
- package/src/components/editor/molecules/EditorFontSize/EditorFontSize.vue +253 -0
- package/src/components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue +102 -28
- package/src/components/editor/molecules/EditorLineHeight/EditorLineHeight.vue +133 -47
- package/src/components/molecules/PickerList/PickerList.vue +50 -10
- package/src/components/organisms/FontPicker/FontPicker.vue +576 -0
- package/src/components/organisms/FontPicker/useFontPreviewLoader.ts +118 -0
- package/src/components/organisms/PickerPanel/PickerPanel.vue +16 -1
- package/src/components.json +2 -0
- package/src/index.ts +20 -1
|
@@ -1,34 +1,55 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { EDITOR_FONT_SIZE_STEPS } from '../EditorFontSize/EditorFontSize.vue'
|
|
3
|
+
import type { EditorFontSizeValue } from '../EditorFontSize/EditorFontSize.vue'
|
|
4
|
+
|
|
2
5
|
/**
|
|
3
|
-
* The steps the slider can land on, as the class they are stored as and the
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
6
|
+
* The steps the slider can land on, as the class they are stored as and the factor of the font
|
|
7
|
+
* size that class is. Every class is a unitless arbitrary value (`leading-[1.5]`), so the line
|
|
8
|
+
* height scales with the text: at 128px a fixed `leading-9` (36px) would make the lines overlap.
|
|
8
9
|
*
|
|
9
|
-
* The first step is no class at all: the element keeps
|
|
10
|
-
*
|
|
10
|
+
* The first step is no class at all: the element keeps the line height of its `text-*` token,
|
|
11
|
+
* see `EDITOR_TEXT_LINE_HEIGHT_RATIOS`. Its `factor` is `null` because it depends on that token.
|
|
11
12
|
*
|
|
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
|
|
14
|
-
* API stays the same.
|
|
13
|
+
* The consumer stores these classes and lists them in an `@source inline` rule (and the back end
|
|
14
|
+
* allows exactly these), so adding or renaming a token is a breaking change even though the
|
|
15
|
+
* component API stays the same.
|
|
15
16
|
*/
|
|
16
17
|
export const EDITOR_LINE_HEIGHT_STEPS = [
|
|
17
|
-
{ token: null,
|
|
18
|
-
{ token: 'leading-
|
|
19
|
-
{ token: 'leading-
|
|
20
|
-
{ token: 'leading-
|
|
21
|
-
{ token: 'leading-
|
|
22
|
-
{ token: 'leading-
|
|
23
|
-
{ token: 'leading-
|
|
24
|
-
{ token: 'leading-
|
|
25
|
-
{ token: 'leading-
|
|
26
|
-
{ token: 'leading-
|
|
27
|
-
{ token: 'leading-12', px: 48 },
|
|
28
|
-
{ token: 'leading-14', px: 56 },
|
|
29
|
-
{ token: 'leading-16', px: 64 },
|
|
18
|
+
{ token: null, factor: null },
|
|
19
|
+
{ token: 'leading-[1]', factor: 1 },
|
|
20
|
+
{ token: 'leading-[1.1]', factor: 1.1 },
|
|
21
|
+
{ token: 'leading-[1.2]', factor: 1.2 },
|
|
22
|
+
{ token: 'leading-[1.3]', factor: 1.3 },
|
|
23
|
+
{ token: 'leading-[1.4]', factor: 1.4 },
|
|
24
|
+
{ token: 'leading-[1.5]', factor: 1.5 },
|
|
25
|
+
{ token: 'leading-[1.6]', factor: 1.6 },
|
|
26
|
+
{ token: 'leading-[1.75]', factor: 1.75 },
|
|
27
|
+
{ token: 'leading-[2]', factor: 2 },
|
|
30
28
|
] as const
|
|
31
29
|
|
|
30
|
+
/**
|
|
31
|
+
* The line height each `text-*` token carries by itself, as a factor of its size: Tailwind v4's
|
|
32
|
+
* default `--text-*--line-height`, written as the same fractions. The theme of this library sets
|
|
33
|
+
* the sizes but not these, so the defaults are what renders. This is what the first step of
|
|
34
|
+
* `EDITOR_LINE_HEIGHT_STEPS`, no class, looks like; exported so a consumer that needs the
|
|
35
|
+
* default line height in px does not keep a copy of its own.
|
|
36
|
+
*/
|
|
37
|
+
export const EDITOR_TEXT_LINE_HEIGHT_RATIOS: Readonly<Record<EditorFontSizeValue, number>> = {
|
|
38
|
+
'text-xs': 1 / 0.75,
|
|
39
|
+
'text-sm': 1.25 / 0.875,
|
|
40
|
+
'text-base': 1.5,
|
|
41
|
+
'text-lg': 1.75 / 1.125,
|
|
42
|
+
'text-xl': 1.75 / 1.25,
|
|
43
|
+
'text-2xl': 2 / 1.5,
|
|
44
|
+
'text-3xl': 2.25 / 1.875,
|
|
45
|
+
'text-4xl': 2.5 / 2.25,
|
|
46
|
+
'text-5xl': 1,
|
|
47
|
+
'text-6xl': 1,
|
|
48
|
+
'text-7xl': 1,
|
|
49
|
+
'text-8xl': 1,
|
|
50
|
+
'text-9xl': 1,
|
|
51
|
+
}
|
|
52
|
+
|
|
32
53
|
/**
|
|
33
54
|
* The line height as the editor needs it: the Tailwind class itself, so the
|
|
34
55
|
* consumer can put the model value straight on the element it styles. The
|
|
@@ -41,13 +62,20 @@ export interface EditorLineHeightMessages {
|
|
|
41
62
|
/** Unit shown behind the value. Defaults to `'px'`. */
|
|
42
63
|
unit?: string
|
|
43
64
|
/**
|
|
44
|
-
*
|
|
65
|
+
* Name of the first step, no class, shown before its px as `Standaard · 20 px`. Defaults to
|
|
45
66
|
* the Dutch `'Standaard'`.
|
|
46
67
|
*/
|
|
47
68
|
default?: string
|
|
48
69
|
}
|
|
49
70
|
|
|
50
71
|
export interface NexxtEditorLineHeightProps {
|
|
72
|
+
/**
|
|
73
|
+
* The `text-*` token the text renders at on the screen on show (for a heading, the token of
|
|
74
|
+
* its heading type): the base the readout turns the factor into px with. Defaults to
|
|
75
|
+
* `text-sm`, the app's text base. A token that is not in `EDITOR_FONT_SIZE_STEPS` falls back
|
|
76
|
+
* to `text-sm`, and a development build warns once per value.
|
|
77
|
+
*/
|
|
78
|
+
fontSize?: EditorFontSizeValue
|
|
51
79
|
/**
|
|
52
80
|
* Header text of the settings block. Defaults to the Dutch
|
|
53
81
|
* `'Regelafstand'`; the library has no i18n layer, so a consumer in another
|
|
@@ -61,6 +89,14 @@ export interface NexxtEditorLineHeightProps {
|
|
|
61
89
|
messages?: EditorLineHeightMessages
|
|
62
90
|
}
|
|
63
91
|
|
|
92
|
+
const DEFAULT_FONT_SIZE = 'text-sm' satisfies EditorFontSizeValue
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Rounds to whole px, half up. The product is snapped to 6 decimals first, so a float a hair under
|
|
96
|
+
* a half (or over a whole px) rounds as the decimal it stands for.
|
|
97
|
+
*/
|
|
98
|
+
const roundPx = (value: number) => Math.round(Number(value.toFixed(6)))
|
|
99
|
+
|
|
64
100
|
const DEFAULT_MESSAGES = {
|
|
65
101
|
unit: 'px',
|
|
66
102
|
default: 'Standaard',
|
|
@@ -68,13 +104,17 @@ const DEFAULT_MESSAGES = {
|
|
|
68
104
|
</script>
|
|
69
105
|
|
|
70
106
|
<script lang="ts" setup>
|
|
71
|
-
import { computed } from 'vue'
|
|
107
|
+
import { computed, watch } from 'vue'
|
|
72
108
|
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
73
109
|
import NexxtSlider from '../../../atoms/Slider/Slider.vue'
|
|
74
110
|
|
|
75
111
|
defineOptions({ name: 'NexxtEditorLineHeight' })
|
|
76
112
|
|
|
77
|
-
const {
|
|
113
|
+
const {
|
|
114
|
+
fontSize = DEFAULT_FONT_SIZE,
|
|
115
|
+
label = 'Regelafstand',
|
|
116
|
+
messages,
|
|
117
|
+
} = defineProps<NexxtEditorLineHeightProps>()
|
|
78
118
|
|
|
79
119
|
const emit = defineEmits<{
|
|
80
120
|
/**
|
|
@@ -87,6 +127,10 @@ const emit = defineEmits<{
|
|
|
87
127
|
/**
|
|
88
128
|
* The selected line height, as the Tailwind class to apply. `null` -- also
|
|
89
129
|
* what an unset model is -- is the default step: no class at all.
|
|
130
|
+
*
|
|
131
|
+
* A value that is not on the list (an old px class such as `leading-6`, say) is shown on the
|
|
132
|
+
* default step and makes a development build warn once per value; the first change writes a
|
|
133
|
+
* valid class back.
|
|
90
134
|
*/
|
|
91
135
|
const model = defineModel<EditorLineHeightValue | null>({ default: null })
|
|
92
136
|
|
|
@@ -94,20 +138,55 @@ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
|
|
|
94
138
|
|
|
95
139
|
const maxIndex = EDITOR_LINE_HEIGHT_STEPS.length - 1
|
|
96
140
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*/
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
141
|
+
const indexOf = (token: string | null | undefined) =>
|
|
142
|
+
EDITOR_LINE_HEIGHT_STEPS.findIndex((step) => step.token === token)
|
|
143
|
+
|
|
144
|
+
const fontSizeStep = (token: string) => EDITOR_FONT_SIZE_STEPS.find((step) => step.token === token)
|
|
145
|
+
|
|
146
|
+
/** The size the readout is based on; an unknown `fontSize` falls back to `text-sm`. */
|
|
147
|
+
const baseSize = computed(() => fontSizeStep(fontSize) ?? fontSizeStep(DEFAULT_FONT_SIZE)!)
|
|
148
|
+
|
|
149
|
+
/** Position of the current value on the step list; off the list is the default step. */
|
|
150
|
+
const stepIndex = computed(() => Math.max(0, indexOf(model.value)))
|
|
151
|
+
|
|
152
|
+
// The same guard as NexxtEditorFontSize: `process.env.NODE_ENV` stays in the library build, so
|
|
153
|
+
// the consumer's bundler decides. Once per value per instance.
|
|
154
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
155
|
+
const warned = new Set<string>()
|
|
156
|
+
const warnOnce = (key: string, message: string) => {
|
|
157
|
+
if (warned.has(key)) return
|
|
158
|
+
warned.add(key)
|
|
159
|
+
console.warn(`[NexxtEditorLineHeight] ${message}`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
watch(
|
|
163
|
+
() => fontSize,
|
|
164
|
+
(token) => {
|
|
165
|
+
if (!fontSizeStep(token)) {
|
|
166
|
+
warnOnce(
|
|
167
|
+
`fontSize:${token}`,
|
|
168
|
+
`fontSize "${token}" is not in EDITOR_FONT_SIZE_STEPS; falling back to ${DEFAULT_FONT_SIZE}.`,
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
{ immediate: true },
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
watch(
|
|
176
|
+
model,
|
|
177
|
+
(value) => {
|
|
178
|
+
if (value !== null && indexOf(value) === -1) {
|
|
179
|
+
warnOnce(
|
|
180
|
+
`model:${value}`,
|
|
181
|
+
`"${value}" is not in EDITOR_LINE_HEIGHT_STEPS; showing the default step until a step is picked.`,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{ immediate: true },
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The slider moves over the index, not over the factor, so its model is
|
|
111
190
|
// translated both ways here.
|
|
112
191
|
const sliderValue = computed({
|
|
113
192
|
get: () => [stepIndex.value],
|
|
@@ -116,10 +195,16 @@ const sliderValue = computed({
|
|
|
116
195
|
},
|
|
117
196
|
})
|
|
118
197
|
|
|
198
|
+
/**
|
|
199
|
+
* The line height in whole px at the current size. The default step shows the token's own line
|
|
200
|
+
* height, named: `Standaard · 20 px`.
|
|
201
|
+
*/
|
|
119
202
|
const displayValue = computed(() => {
|
|
120
|
-
const {
|
|
203
|
+
const { factor } = EDITOR_LINE_HEIGHT_STEPS[stepIndex.value]
|
|
204
|
+
const { token, px: size } = baseSize.value
|
|
205
|
+
const px = `${roundPx((factor ?? EDITOR_TEXT_LINE_HEIGHT_RATIOS[token]) * size)} ${copy.value.unit}`
|
|
121
206
|
|
|
122
|
-
return
|
|
207
|
+
return factor === null ? `${copy.value.default} · ${px}` : px
|
|
123
208
|
})
|
|
124
209
|
</script>
|
|
125
210
|
|
|
@@ -146,13 +231,14 @@ const displayValue = computed(() => {
|
|
|
146
231
|
/>
|
|
147
232
|
|
|
148
233
|
<!--
|
|
149
|
-
`min-w-
|
|
150
|
-
the
|
|
151
|
-
gray-700, not the gray-500 Slider uses
|
|
152
|
-
gray-500 on white at 1.84:1, well under the 4.5:1 it needs
|
|
234
|
+
`min-w-36` (144px) fits the widest readout, `Standaard · 128 px` (127px in Chromium),
|
|
235
|
+
so the slider keeps its size moving between steps; the ReadoutWidth story measures it.
|
|
236
|
+
`tabular-nums` stops the digits from jittering. gray-700, not the gray-500 Slider uses
|
|
237
|
+
for its own readout: axe puts gray-500 on white at 1.84:1, well under the 4.5:1 it needs
|
|
238
|
+
at this size.
|
|
153
239
|
-->
|
|
154
240
|
<span
|
|
155
|
-
class="min-w-
|
|
241
|
+
class="min-w-36 shrink-0 text-right small-normal whitespace-nowrap text-gray-700 tabular-nums"
|
|
156
242
|
>
|
|
157
243
|
{{ displayValue }}
|
|
158
244
|
</span>
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
nextTick,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
ref,
|
|
7
|
+
useId,
|
|
8
|
+
useSlots,
|
|
9
|
+
useTemplateRef,
|
|
10
|
+
watch,
|
|
11
|
+
} from 'vue'
|
|
3
12
|
import { usePreferredReducedMotion } from '@vueuse/core'
|
|
4
13
|
import NexxtButton from '../../atoms/Button/Button.vue'
|
|
5
14
|
import NexxtIcon, { type NexxtIconName } from '../../atoms/Icon/Icon.vue'
|
|
@@ -11,8 +20,8 @@ export interface NexxtPickerItemData {
|
|
|
11
20
|
id: string
|
|
12
21
|
/** Visible label and accessible name of the option; searched and matched by type-ahead. */
|
|
13
22
|
label: string
|
|
14
|
-
/** Icon in front of the label, also passed to the `preview` slot. */
|
|
15
|
-
icon
|
|
23
|
+
/** Icon in front of the label, also passed to the `preview` slot. Without it the row has no icon. */
|
|
24
|
+
icon?: NexxtIconName
|
|
16
25
|
/** Id of a group in `groups`. Missing, or not in `groups`: ungrouped, rendered at the top. */
|
|
17
26
|
group?: string
|
|
18
27
|
/** Extra search terms (synonyms, old names). Matched, never shown. */
|
|
@@ -57,6 +66,12 @@ interface NexxtPickerListProps {
|
|
|
57
66
|
* `(n) => \`${n} resultaten\``. Without it the region stays empty.
|
|
58
67
|
*/
|
|
59
68
|
resultsAnnouncement?: (count: number) => string
|
|
69
|
+
/**
|
|
70
|
+
* More items are on their way. Sets `aria-busy` on the listbox and holds back the empty state,
|
|
71
|
+
* so a search that finds nothing yet does not claim there is nothing. Pair it with placeholder
|
|
72
|
+
* rows in the `append` slot.
|
|
73
|
+
*/
|
|
74
|
+
busy?: boolean
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
interface Section {
|
|
@@ -74,6 +89,7 @@ const props = withDefaults(defineProps<NexxtPickerListProps>(), {
|
|
|
74
89
|
groups: () => [],
|
|
75
90
|
search: '',
|
|
76
91
|
collapsible: true,
|
|
92
|
+
busy: false,
|
|
77
93
|
})
|
|
78
94
|
|
|
79
95
|
/**
|
|
@@ -101,8 +117,15 @@ defineSlots<{
|
|
|
101
117
|
item?(props: { item: NexxtPickerItemData; highlighted: boolean }): unknown
|
|
102
118
|
/** Replaces the content of the empty state. */
|
|
103
119
|
empty?(props: { search: string; clear: () => void }): unknown
|
|
120
|
+
/**
|
|
121
|
+
* Inside the listbox, after the last group: placeholder rows while `busy`, say. Not an option and
|
|
122
|
+
* never reached by the keyboard, so mark it `aria-hidden`.
|
|
123
|
+
*/
|
|
124
|
+
append?(): unknown
|
|
104
125
|
}>()
|
|
105
126
|
|
|
127
|
+
const slots = useSlots()
|
|
128
|
+
|
|
106
129
|
const PAGE_SIZE = 8
|
|
107
130
|
const TYPEAHEAD_RESET_MS = 800
|
|
108
131
|
const ANNOUNCE_DELAY_MS = 500
|
|
@@ -168,7 +191,7 @@ const visibleItems = computed(() => [
|
|
|
168
191
|
|
|
169
192
|
/** Anything rendered inside the listbox, closed group headings included. */
|
|
170
193
|
const hasContent = computed(() => ungrouped.value.length > 0 || sections.value.length > 0)
|
|
171
|
-
const showEmpty = computed(() => searching.value && visibleItems.value.length === 0)
|
|
194
|
+
const showEmpty = computed(() => searching.value && visibleItems.value.length === 0 && !props.busy)
|
|
172
195
|
|
|
173
196
|
// ─── Highlight ────────────────────────────────────────────────────────────────
|
|
174
197
|
|
|
@@ -509,17 +532,31 @@ const clear = () => emit('clear')
|
|
|
509
532
|
const announcement = ref('')
|
|
510
533
|
let announceTimer: ReturnType<typeof setTimeout> | undefined
|
|
511
534
|
|
|
512
|
-
|
|
535
|
+
/** A search change that has not been announced yet. */
|
|
536
|
+
let announcePending = false
|
|
537
|
+
|
|
538
|
+
// Only after the search changed, debounced, so intermediate states while typing stay silent. While
|
|
539
|
+
// `busy` the count is not final, so the announcement waits and follows once `busy` ends; a list
|
|
540
|
+
// that is never busy announces exactly as before.
|
|
541
|
+
const scheduleAnnouncement = () => {
|
|
542
|
+
clearTimeout(announceTimer)
|
|
543
|
+
if (!announcePending || props.busy) return
|
|
544
|
+
announceTimer = setTimeout(() => {
|
|
545
|
+
announcePending = false
|
|
546
|
+
announcement.value = props.resultsAnnouncement?.(visibleItems.value.length) ?? ''
|
|
547
|
+
}, ANNOUNCE_DELAY_MS)
|
|
548
|
+
}
|
|
549
|
+
|
|
513
550
|
watch(
|
|
514
551
|
() => props.search,
|
|
515
552
|
() => {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
announcement.value = props.resultsAnnouncement?.(visibleItems.value.length) ?? ''
|
|
519
|
-
}, ANNOUNCE_DELAY_MS)
|
|
553
|
+
announcePending = true
|
|
554
|
+
scheduleAnnouncement()
|
|
520
555
|
},
|
|
521
556
|
)
|
|
522
557
|
|
|
558
|
+
watch(() => props.busy, scheduleAnnouncement)
|
|
559
|
+
|
|
523
560
|
onBeforeUnmount(() => {
|
|
524
561
|
clearTimeout(announceTimer)
|
|
525
562
|
clearTimeout(typeaheadTimer)
|
|
@@ -548,7 +585,7 @@ const EMPTY_CLASSES = {
|
|
|
548
585
|
|
|
549
586
|
const listboxClass = computed(() => [
|
|
550
587
|
LISTBOX_CLASSES.base,
|
|
551
|
-
hasContent.value && LISTBOX_CLASSES.filled,
|
|
588
|
+
(hasContent.value || !!slots.append) && LISTBOX_CLASSES.filled,
|
|
552
589
|
])
|
|
553
590
|
|
|
554
591
|
defineExpose({
|
|
@@ -575,6 +612,7 @@ defineExpose({
|
|
|
575
612
|
:tabindex="hasContent ? 0 : -1"
|
|
576
613
|
:aria-label="ariaLabel"
|
|
577
614
|
:aria-activedescendant="activeDescendantId"
|
|
615
|
+
:aria-busy="busy ? 'true' : undefined"
|
|
578
616
|
:class="listboxClass"
|
|
579
617
|
@keydown="onKeydown"
|
|
580
618
|
@focus="scrollToItem(effectiveId)"
|
|
@@ -616,6 +654,8 @@ defineExpose({
|
|
|
616
654
|
</template>
|
|
617
655
|
</NexxtPickerItem>
|
|
618
656
|
</NexxtPickerGroup>
|
|
657
|
+
|
|
658
|
+
<slot name="append" />
|
|
619
659
|
</div>
|
|
620
660
|
|
|
621
661
|
<Transition
|