@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
package/dist/nuxt.js
CHANGED
|
@@ -62,6 +62,7 @@ var s = {
|
|
|
62
62
|
NexxtCountrySelect: "components/organisms/CountrySelect/CountrySelect.vue",
|
|
63
63
|
NexxtDatePicker: "components/organisms/DatePicker/DatePicker.vue",
|
|
64
64
|
NexxtDropdownButton: "components/organisms/DropdownButton/DropdownButton.vue",
|
|
65
|
+
NexxtFontPicker: "components/organisms/FontPicker/FontPicker.vue",
|
|
65
66
|
NexxtHeader: "components/organisms/Header/Header.vue",
|
|
66
67
|
NexxtModal: "components/organisms/Modal/Modal.vue",
|
|
67
68
|
NexxtPickerPanel: "components/organisms/PickerPanel/PickerPanel.vue",
|
|
@@ -80,6 +81,7 @@ var s = {
|
|
|
80
81
|
NexxtEditorBorder: "components/editor/molecules/EditorBorder/EditorBorder.vue",
|
|
81
82
|
NexxtEditorColor: "components/editor/molecules/EditorColor/EditorColor.vue",
|
|
82
83
|
NexxtEditorFlexDirection: "components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue",
|
|
84
|
+
NexxtEditorFontSize: "components/editor/molecules/EditorFontSize/EditorFontSize.vue",
|
|
83
85
|
NexxtEditorGap: "components/editor/molecules/EditorGap/EditorGap.vue",
|
|
84
86
|
NexxtEditorHeadingType: "components/editor/molecules/EditorHeadingType/EditorHeadingType.vue",
|
|
85
87
|
NexxtEditorJustifyContent: "components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue",
|
package/package.json
CHANGED
|
@@ -12,6 +12,11 @@ interface NexxtEditorSettingsBlockProps {
|
|
|
12
12
|
switchable?: boolean
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* One labelled setting in the editor sidebar, optionally collapsible via a
|
|
17
|
+
* switch. The block draws no separator of its own: the consumer groups and
|
|
18
|
+
* divides blocks on the wrapper, e.g. `divide-y divide-gray-200`.
|
|
19
|
+
*/
|
|
15
20
|
defineOptions({ name: 'NexxtEditorSettingsBlock' })
|
|
16
21
|
|
|
17
22
|
const { label, switchable = false } = defineProps<NexxtEditorSettingsBlockProps>()
|
|
@@ -32,7 +37,7 @@ const inert = computed(() => (open.value ? undefined : true))
|
|
|
32
37
|
</script>
|
|
33
38
|
|
|
34
39
|
<template>
|
|
35
|
-
<div class="
|
|
40
|
+
<div class="py-4">
|
|
36
41
|
<!--
|
|
37
42
|
Switch.vue sets `inheritAttrs: false` and binds `$attrs` on its sr-only
|
|
38
43
|
input, so a class on <Switch> never reaches its root <label>. The child
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The steps the slider can land on, as the font-size class they are stored as and the pixels that
|
|
4
|
+
* class is: every `--text-*` size of the theme, `text-xs` to `text-9xl`, with the px taken from
|
|
5
|
+
* `tailwind/nexxtmove.tailwind.theme.css` at 16px a rem. The size is absolute: a step replaces
|
|
6
|
+
* the size the text had.
|
|
7
|
+
*
|
|
8
|
+
* The consumer stores these classes and lists them in an `@source inline` rule, so adding or
|
|
9
|
+
* renaming a token is a breaking change even though the component API stays the same.
|
|
10
|
+
*/
|
|
11
|
+
export const EDITOR_FONT_SIZE_STEPS = [
|
|
12
|
+
{ token: 'text-xs', px: 12 },
|
|
13
|
+
{ token: 'text-sm', px: 14 },
|
|
14
|
+
{ token: 'text-base', px: 16 },
|
|
15
|
+
{ token: 'text-lg', px: 18 },
|
|
16
|
+
{ token: 'text-xl', px: 20 },
|
|
17
|
+
{ token: 'text-2xl', px: 24 },
|
|
18
|
+
{ token: 'text-3xl', px: 30 },
|
|
19
|
+
{ token: 'text-4xl', px: 36 },
|
|
20
|
+
{ token: 'text-5xl', px: 48 },
|
|
21
|
+
{ token: 'text-6xl', px: 60 },
|
|
22
|
+
{ token: 'text-7xl', px: 72 },
|
|
23
|
+
{ token: 'text-8xl', px: 96 },
|
|
24
|
+
{ token: 'text-9xl', px: 128 },
|
|
25
|
+
] as const
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The font size as the editor needs it: the Tailwind class itself, so the consumer can put the
|
|
29
|
+
* model value straight on the element it styles. "No size of its own" is `null` on the model, not
|
|
30
|
+
* a member of this type.
|
|
31
|
+
*/
|
|
32
|
+
export type EditorFontSizeValue = (typeof EDITOR_FONT_SIZE_STEPS)[number]['token']
|
|
33
|
+
|
|
34
|
+
/** Overridable copy of the readout and the reset button. Every key is optional. */
|
|
35
|
+
export interface EditorFontSizeMessages {
|
|
36
|
+
/** Unit behind the pixel size. Defaults to `'px'`. */
|
|
37
|
+
unit?: string
|
|
38
|
+
/** Accessible name and tooltip of the reset button. Defaults to the Dutch `'Terug naar standaard'`. */
|
|
39
|
+
reset?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NexxtEditorFontSizeProps {
|
|
43
|
+
/**
|
|
44
|
+
* The size the text has without a class of its own; the slider sits on its step while the
|
|
45
|
+
* model is `null`. Defaults to `text-sm`, the app's text base. Pass a token from
|
|
46
|
+
* `EDITOR_FONT_SIZE_STEPS`: one that is not on the list falls back to `text-sm`, and a
|
|
47
|
+
* development build warns once per value. The fallback is for existing or faulty data, not a
|
|
48
|
+
* feature to rely on.
|
|
49
|
+
*/
|
|
50
|
+
defaultToken?: EditorFontSizeValue
|
|
51
|
+
/**
|
|
52
|
+
* Header text of the settings block. Defaults to the Dutch `'Grootte'`; the library has no i18n
|
|
53
|
+
* layer, so a consumer in another language passes its own copy in.
|
|
54
|
+
*/
|
|
55
|
+
label?: string
|
|
56
|
+
/**
|
|
57
|
+
* Copy of the readout and the reset button. Merged per key, so overriding one key keeps the
|
|
58
|
+
* defaults of the others. Copy is never translated inside the component.
|
|
59
|
+
*/
|
|
60
|
+
messages?: EditorFontSizeMessages
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const DEFAULT_TOKEN = 'text-sm' satisfies EditorFontSizeValue
|
|
64
|
+
|
|
65
|
+
// A no-break space between number and unit, so a readout never wraps inside a value
|
|
66
|
+
const NBSP = '\u00a0'
|
|
67
|
+
|
|
68
|
+
const DEFAULT_MESSAGES = {
|
|
69
|
+
unit: 'px',
|
|
70
|
+
reset: 'Terug naar standaard',
|
|
71
|
+
} satisfies Required<EditorFontSizeMessages>
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<script lang="ts" setup>
|
|
75
|
+
import { computed, useTemplateRef, watch } from 'vue'
|
|
76
|
+
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
77
|
+
import NexxtIcon from '../../../atoms/Icon/Icon.vue'
|
|
78
|
+
import NexxtSlider from '../../../atoms/Slider/Slider.vue'
|
|
79
|
+
import NexxtTooltip from '../../../atoms/Tooltip/Tooltip.vue'
|
|
80
|
+
|
|
81
|
+
defineOptions({ name: 'NexxtEditorFontSize' })
|
|
82
|
+
|
|
83
|
+
const {
|
|
84
|
+
defaultToken = DEFAULT_TOKEN,
|
|
85
|
+
label = 'Grootte',
|
|
86
|
+
messages,
|
|
87
|
+
} = defineProps<NexxtEditorFontSizeProps>()
|
|
88
|
+
|
|
89
|
+
const emit = defineEmits<{
|
|
90
|
+
/**
|
|
91
|
+
* The slider was let go: the end of one drag, which a consumer can use to close an undo step.
|
|
92
|
+
* Arrow keys emit no pointerup.
|
|
93
|
+
*/
|
|
94
|
+
pointerup: [event: PointerEvent]
|
|
95
|
+
/**
|
|
96
|
+
* The reset button was used, after the model was set to `null`. A consumer closes an undo step
|
|
97
|
+
* here, so a reset never merges into the drag before it.
|
|
98
|
+
*/
|
|
99
|
+
reset: []
|
|
100
|
+
}>()
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The chosen size, as the Tailwind class to apply. `null` -- also what an unset model is -- is no
|
|
104
|
+
* class of its own: the element keeps `defaultToken`. Moving the slider always writes a token,
|
|
105
|
+
* the default step included; only the reset button writes `null`.
|
|
106
|
+
*
|
|
107
|
+
* The app should only write tokens from `EDITOR_FONT_SIZE_STEPS`. A value that is not on the list
|
|
108
|
+
* (existing or faulty data, `text-[15px]` say) is shown on the default step, keeps the reset
|
|
109
|
+
* button visible so the user can get back to a clean `null`, and makes a development build warn
|
|
110
|
+
* once per value. It is never rewritten until the user picks a step.
|
|
111
|
+
*/
|
|
112
|
+
const model = defineModel<EditorFontSizeValue | null>({ default: null })
|
|
113
|
+
|
|
114
|
+
const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
|
|
115
|
+
|
|
116
|
+
const root = useTemplateRef<HTMLElement>('root')
|
|
117
|
+
|
|
118
|
+
const maxIndex = EDITOR_FONT_SIZE_STEPS.length - 1
|
|
119
|
+
|
|
120
|
+
const indexOf = (token: string | null | undefined) =>
|
|
121
|
+
EDITOR_FONT_SIZE_STEPS.findIndex((step) => step.token === token)
|
|
122
|
+
|
|
123
|
+
/** Step of the default size; an unknown `defaultToken` falls back to `text-sm`. */
|
|
124
|
+
const defaultIndex = computed(() => {
|
|
125
|
+
const index = indexOf(defaultToken)
|
|
126
|
+
return index === -1 ? indexOf(DEFAULT_TOKEN) : index
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Position of the current value on the step list. `null`, and a value that is not on the list (a
|
|
131
|
+
* size outside the theme, or one that no longer exists), sit on the default step instead of
|
|
132
|
+
* leaving the thumb off the track; the first change writes a valid class back.
|
|
133
|
+
*/
|
|
134
|
+
const stepIndex = computed(() => {
|
|
135
|
+
const index = indexOf(model.value)
|
|
136
|
+
return index === -1 ? defaultIndex.value : index
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// The same guard as NexxtFontPicker: `process.env.NODE_ENV` stays in the library build, so the
|
|
140
|
+
// consumer's bundler decides; `import.meta.env.DEV` would be fixed when the library is built.
|
|
141
|
+
// Once per value per instance: a re-render does not repeat it, a new instance starts afresh.
|
|
142
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
143
|
+
const warned = new Set<string>()
|
|
144
|
+
const warnOnce = (key: string, message: string) => {
|
|
145
|
+
if (warned.has(key)) return
|
|
146
|
+
warned.add(key)
|
|
147
|
+
console.warn(`[NexxtEditorFontSize] ${message}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
watch(
|
|
151
|
+
() => defaultToken,
|
|
152
|
+
(token) => {
|
|
153
|
+
if (indexOf(token) === -1) {
|
|
154
|
+
warnOnce(
|
|
155
|
+
`default:${token}`,
|
|
156
|
+
`defaultToken "${token}" is not in EDITOR_FONT_SIZE_STEPS; falling back to ${DEFAULT_TOKEN}.`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{ immediate: true },
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
watch(
|
|
164
|
+
model,
|
|
165
|
+
(value) => {
|
|
166
|
+
if (value !== null && indexOf(value) === -1) {
|
|
167
|
+
warnOnce(
|
|
168
|
+
`model:${value}`,
|
|
169
|
+
`"${value}" is not in EDITOR_FONT_SIZE_STEPS; showing the default step until a step is picked.`,
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{ immediate: true },
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The slider moves over the index, not over the px value, so its model is translated both ways.
|
|
178
|
+
// Every step writes its token, the default one too: `null` is the reset button's alone.
|
|
179
|
+
const sliderValue = computed({
|
|
180
|
+
get: () => [stepIndex.value],
|
|
181
|
+
set: ([index]) => {
|
|
182
|
+
model.value = EDITOR_FONT_SIZE_STEPS[index].token
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const displayValue = computed(
|
|
187
|
+
() => `${EDITOR_FONT_SIZE_STEPS[stepIndex.value].px}${NBSP}${copy.value.unit}`,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
// The button turns invisible under the focus it has, which would drop focus on <body>, so it is
|
|
191
|
+
// handed to the thumb: the control the user is most likely to reach for next.
|
|
192
|
+
const onReset = () => {
|
|
193
|
+
model.value = null
|
|
194
|
+
emit('reset')
|
|
195
|
+
root.value?.querySelector<HTMLElement>('[role="slider"]')?.focus()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const RESET_CLASSES = {
|
|
199
|
+
base: 'flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-gray-700 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cornflower-blue-500',
|
|
200
|
+
hidden: 'invisible',
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Only `null` hides it: an explicit token on the default step is still a value of its own
|
|
204
|
+
const resetClass = computed(() => [
|
|
205
|
+
RESET_CLASSES.base,
|
|
206
|
+
model.value === null && RESET_CLASSES.hidden,
|
|
207
|
+
])
|
|
208
|
+
</script>
|
|
209
|
+
|
|
210
|
+
<template>
|
|
211
|
+
<NexxtEditorSettingsBlock :label="label">
|
|
212
|
+
<div ref="root" class="flex items-center gap-4">
|
|
213
|
+
<!--
|
|
214
|
+
Same set-up as NexxtEditorLineHeight: the slider walks the step index, so the readout is
|
|
215
|
+
rendered next to it and the thumb announces the readout through `aria-valuetext` instead
|
|
216
|
+
of the index. The block label is a <p>, so the name goes on the thumb explicitly.
|
|
217
|
+
-->
|
|
218
|
+
<NexxtSlider
|
|
219
|
+
v-model="sliderValue"
|
|
220
|
+
:min="0"
|
|
221
|
+
:max="maxIndex"
|
|
222
|
+
:step="1"
|
|
223
|
+
:aria-label="label"
|
|
224
|
+
:aria-valuetext="displayValue"
|
|
225
|
+
@pointerup="emit('pointerup', $event)"
|
|
226
|
+
/>
|
|
227
|
+
|
|
228
|
+
<!--
|
|
229
|
+
`min-w-14` (56px) fits the widest readout, "128 px" (45px in Chromium with tabular
|
|
230
|
+
digits), with room to spare for wider digits in another font or browser, so the slider
|
|
231
|
+
keeps its width over the whole list; the ReadoutWidth story measures it.
|
|
232
|
+
-->
|
|
233
|
+
<span
|
|
234
|
+
class="min-w-14 shrink-0 text-right small-normal whitespace-nowrap text-gray-700 tabular-nums"
|
|
235
|
+
>
|
|
236
|
+
{{ displayValue }}
|
|
237
|
+
</span>
|
|
238
|
+
|
|
239
|
+
<!--
|
|
240
|
+
Always rendered and only `invisible` without a value of its own: it keeps its 28px, so the
|
|
241
|
+
slider never jumps, and an invisible button is out of the tab order.
|
|
242
|
+
-->
|
|
243
|
+
<NexxtTooltip>
|
|
244
|
+
<button type="button" :aria-label="copy.reset" :class="resetClass" @click="onReset">
|
|
245
|
+
<NexxtIcon name="arrow-rotate-left" aria-hidden="true" />
|
|
246
|
+
</button>
|
|
247
|
+
<template #tooltip>
|
|
248
|
+
<span class="extra-small-normal text-gray-900">{{ copy.reset }}</span>
|
|
249
|
+
</template>
|
|
250
|
+
</NexxtTooltip>
|
|
251
|
+
</div>
|
|
252
|
+
</NexxtEditorSettingsBlock>
|
|
253
|
+
</template>
|
|
@@ -1,29 +1,32 @@
|
|
|
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
|
-
*
|
|
6
|
+
* The steps the slider can land on, as the class they are stored as and the em that class is.
|
|
7
|
+
* In em, so the spacing moves with the font size: a fixed 2px does nothing at 128px. Not a scale
|
|
8
|
+
* Tailwind has names for (`tracking-wide` is 0.025em, but there is no -0.01em), so every step is
|
|
9
|
+
* an arbitrary-value class.
|
|
7
10
|
*
|
|
8
11
|
* The middle step is no class at all: zero is what the text already does, and
|
|
9
12
|
* storing nothing for it keeps the "no class means default" every other
|
|
10
13
|
* setting has. The negative side is there on purpose: tightening a large
|
|
11
14
|
* heading is a real need.
|
|
12
15
|
*
|
|
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
|
|
15
|
-
* API stays the same.
|
|
16
|
+
* The consumer stores these classes and lists them in an `@source inline` rule (and the back end
|
|
17
|
+
* allows exactly these), so adding or renaming a token is a breaking change even though the
|
|
18
|
+
* component API stays the same.
|
|
16
19
|
*/
|
|
17
20
|
export const EDITOR_LETTER_SPACING_STEPS = [
|
|
18
|
-
{ token: 'tracking-[-
|
|
19
|
-
{ token: 'tracking-[-0.
|
|
20
|
-
{ token: 'tracking-[-0.
|
|
21
|
-
{ token: null,
|
|
22
|
-
{ token: 'tracking-[0.
|
|
23
|
-
{ token: 'tracking-[0.
|
|
24
|
-
{ token: 'tracking-[
|
|
25
|
-
{ token: 'tracking-[
|
|
26
|
-
{ token: 'tracking-[
|
|
21
|
+
{ token: 'tracking-[-0.05em]', em: -0.05 },
|
|
22
|
+
{ token: 'tracking-[-0.025em]', em: -0.025 },
|
|
23
|
+
{ token: 'tracking-[-0.01em]', em: -0.01 },
|
|
24
|
+
{ token: null, em: 0 },
|
|
25
|
+
{ token: 'tracking-[0.01em]', em: 0.01 },
|
|
26
|
+
{ token: 'tracking-[0.025em]', em: 0.025 },
|
|
27
|
+
{ token: 'tracking-[0.05em]', em: 0.05 },
|
|
28
|
+
{ token: 'tracking-[0.075em]', em: 0.075 },
|
|
29
|
+
{ token: 'tracking-[0.1em]', em: 0.1 },
|
|
27
30
|
] as const
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -44,6 +47,13 @@ export interface EditorLetterSpacingMessages {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export interface NexxtEditorLetterSpacingProps {
|
|
50
|
+
/**
|
|
51
|
+
* The `text-*` token the text renders at on the screen on show (for a heading, the token of
|
|
52
|
+
* its heading type): the base the readout turns the em into px with. Defaults to `text-sm`,
|
|
53
|
+
* the app's text base. A token that is not in `EDITOR_FONT_SIZE_STEPS` falls back to
|
|
54
|
+
* `text-sm`, and a development build warns once per value.
|
|
55
|
+
*/
|
|
56
|
+
fontSize?: EditorFontSizeValue
|
|
47
57
|
/**
|
|
48
58
|
* Header text of the settings block. Defaults to the Dutch
|
|
49
59
|
* `'Letterafstand'`; the library has no i18n layer, so a consumer in another
|
|
@@ -57,6 +67,15 @@ export interface NexxtEditorLetterSpacingProps {
|
|
|
57
67
|
messages?: EditorLetterSpacingMessages
|
|
58
68
|
}
|
|
59
69
|
|
|
70
|
+
const DEFAULT_FONT_SIZE = 'text-sm' satisfies EditorFontSizeValue
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Rounds to one decimal, half away from zero. The product is snapped to 6 decimals first, so a
|
|
74
|
+
* float such as 0.075 * 18 = 1.3499999… rounds as the 1.35 it is, to 1.4.
|
|
75
|
+
*/
|
|
76
|
+
const roundTenths = (value: number) =>
|
|
77
|
+
(Math.sign(value) * Math.round(Number((Math.abs(value) * 10).toFixed(6)))) / 10
|
|
78
|
+
|
|
60
79
|
const DEFAULT_MESSAGES = {
|
|
61
80
|
unit: 'px',
|
|
62
81
|
decimalSeparator: ',',
|
|
@@ -67,13 +86,17 @@ const DEFAULT_INDEX = EDITOR_LETTER_SPACING_STEPS.findIndex(({ token }) => token
|
|
|
67
86
|
</script>
|
|
68
87
|
|
|
69
88
|
<script lang="ts" setup>
|
|
70
|
-
import { computed } from 'vue'
|
|
89
|
+
import { computed, watch } from 'vue'
|
|
71
90
|
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
72
91
|
import NexxtSlider from '../../../atoms/Slider/Slider.vue'
|
|
73
92
|
|
|
74
93
|
defineOptions({ name: 'NexxtEditorLetterSpacing' })
|
|
75
94
|
|
|
76
|
-
const {
|
|
95
|
+
const {
|
|
96
|
+
fontSize = DEFAULT_FONT_SIZE,
|
|
97
|
+
label = 'Letterafstand',
|
|
98
|
+
messages,
|
|
99
|
+
} = defineProps<NexxtEditorLetterSpacingProps>()
|
|
77
100
|
|
|
78
101
|
const emit = defineEmits<{
|
|
79
102
|
/**
|
|
@@ -86,6 +109,10 @@ const emit = defineEmits<{
|
|
|
86
109
|
/**
|
|
87
110
|
* The selected letter spacing, as the Tailwind class to apply. `null` -- also
|
|
88
111
|
* what an unset model is -- is the default step: no class at all, zero.
|
|
112
|
+
*
|
|
113
|
+
* A value that is not on the list (an old px class such as `tracking-[1px]`, say) is shown on
|
|
114
|
+
* the default step and makes a development build warn once per value; the first change writes a
|
|
115
|
+
* valid class back.
|
|
89
116
|
*/
|
|
90
117
|
const model = defineModel<EditorLetterSpacingValue | null>({ default: null })
|
|
91
118
|
|
|
@@ -93,20 +120,63 @@ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
|
|
|
93
120
|
|
|
94
121
|
const maxIndex = EDITOR_LETTER_SPACING_STEPS.length - 1
|
|
95
122
|
|
|
123
|
+
const indexOf = (token: string | null | undefined) =>
|
|
124
|
+
EDITOR_LETTER_SPACING_STEPS.findIndex((step) => step.token === token)
|
|
125
|
+
|
|
126
|
+
const fontSizeStep = (token: string) => EDITOR_FONT_SIZE_STEPS.find((step) => step.token === token)
|
|
127
|
+
|
|
128
|
+
/** The size in px the readout is based on; an unknown `fontSize` falls back to `text-sm`. */
|
|
129
|
+
const basePx = computed(() => (fontSizeStep(fontSize) ?? fontSizeStep(DEFAULT_FONT_SIZE)!).px)
|
|
130
|
+
|
|
96
131
|
/**
|
|
97
132
|
* Position of the current value on the step list. A value that is not on the
|
|
98
|
-
* list
|
|
99
|
-
*
|
|
100
|
-
* an unknown value is no reason to show text as squeezed. The first change
|
|
101
|
-
* writes a valid class back.
|
|
133
|
+
* list falls back to the default step rather than the first one: the first step here is the
|
|
134
|
+
* tightest spacing, and an unknown value is no reason to show text as squeezed.
|
|
102
135
|
*/
|
|
103
136
|
const stepIndex = computed(() => {
|
|
104
|
-
const index =
|
|
137
|
+
const index = indexOf(model.value)
|
|
105
138
|
|
|
106
139
|
return index === -1 ? DEFAULT_INDEX : index
|
|
107
140
|
})
|
|
108
141
|
|
|
109
|
-
// The
|
|
142
|
+
// The same guard as NexxtEditorFontSize: `process.env.NODE_ENV` stays in the library build, so
|
|
143
|
+
// the consumer's bundler decides. Once per value per instance.
|
|
144
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
145
|
+
const warned = new Set<string>()
|
|
146
|
+
const warnOnce = (key: string, message: string) => {
|
|
147
|
+
if (warned.has(key)) return
|
|
148
|
+
warned.add(key)
|
|
149
|
+
console.warn(`[NexxtEditorLetterSpacing] ${message}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
watch(
|
|
153
|
+
() => fontSize,
|
|
154
|
+
(token) => {
|
|
155
|
+
if (!fontSizeStep(token)) {
|
|
156
|
+
warnOnce(
|
|
157
|
+
`fontSize:${token}`,
|
|
158
|
+
`fontSize "${token}" is not in EDITOR_FONT_SIZE_STEPS; falling back to ${DEFAULT_FONT_SIZE}.`,
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
{ immediate: true },
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
watch(
|
|
166
|
+
model,
|
|
167
|
+
(value) => {
|
|
168
|
+
if (value !== null && indexOf(value) === -1) {
|
|
169
|
+
warnOnce(
|
|
170
|
+
`model:${value}`,
|
|
171
|
+
`"${value}" is not in EDITOR_LETTER_SPACING_STEPS; showing the default step until a step is picked.`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
{ immediate: true },
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The slider moves over the index, not over the em value, so its model is
|
|
110
180
|
// translated both ways here.
|
|
111
181
|
const sliderValue = computed({
|
|
112
182
|
get: () => [stepIndex.value],
|
|
@@ -115,11 +185,15 @@ const sliderValue = computed({
|
|
|
115
185
|
},
|
|
116
186
|
})
|
|
117
187
|
|
|
118
|
-
/**
|
|
188
|
+
/**
|
|
189
|
+
* The spacing in px at the current size, to one decimal: `-0,7 px` for -0.05em at 14px. A plain
|
|
190
|
+
* hyphen-minus for the sign, the decimal separator from the copy; `+ 0` turns a -0 into 0.
|
|
191
|
+
*/
|
|
119
192
|
const displayValue = computed(() => {
|
|
120
|
-
const {
|
|
193
|
+
const { em } = EDITOR_LETTER_SPACING_STEPS[stepIndex.value]
|
|
194
|
+
const px = String(roundTenths(em * basePx.value) + 0)
|
|
121
195
|
|
|
122
|
-
return `${
|
|
196
|
+
return `${px.replace('.', copy.value.decimalSeparator)} ${copy.value.unit}`
|
|
123
197
|
})
|
|
124
198
|
</script>
|
|
125
199
|
|
|
@@ -147,7 +221,7 @@ const displayValue = computed(() => {
|
|
|
147
221
|
|
|
148
222
|
<!--
|
|
149
223
|
`min-w-16` rather than the `min-w-12` of the other editor sliders:
|
|
150
|
-
`-
|
|
224
|
+
`-6,4 px` and `12,8 px`, the ends at text-9xl, do not fit in 12. That plus `shrink-0` fixes the width of
|
|
151
225
|
the box, so the slider keeps its size while dragging; `tabular-nums`
|
|
152
226
|
only stops the digits from jittering inside it.
|
|
153
227
|
gray-700, not the gray-500 Slider uses for its own readout: axe puts
|