@nexxtmove/ui 1.42.0 → 1.44.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.
@@ -0,0 +1,257 @@
1
+ <script lang="ts">
2
+ import type {
3
+ EditorDimensionUnit,
4
+ EditorDimensionValue,
5
+ } from '../../atoms/EditorDimensionField/EditorDimensionField.vue'
6
+
7
+ /**
8
+ * The options the editor offers, in the order they are shown. The labels are
9
+ * not in here; they arrive through `messages`, keyed by token.
10
+ */
11
+ export const EDITOR_SIZE_OPTIONS = [
12
+ { token: 'auto' },
13
+ { token: 'fill' },
14
+ { token: 'fixed' },
15
+ ] as const
16
+
17
+ /** How a dimension is sized: by its content, filling its parent, or fixed. */
18
+ export type EditorSizeMode = (typeof EDITOR_SIZE_OPTIONS)[number]['token']
19
+
20
+ export interface EditorSizeValue {
21
+ mode: EditorSizeMode
22
+ /**
23
+ * The fixed dimension. Kept when another mode is picked, so switching back
24
+ * to `'fixed'` restores it; it only applies while `mode` is `'fixed'`. The
25
+ * app always fills it with the measured size of the element, so picking
26
+ * `'fixed'` starts from that size.
27
+ */
28
+ fixed?: EditorDimensionValue
29
+ }
30
+
31
+ /** Overridable copy of the options and the field. Every key is optional. */
32
+ export interface EditorSizeMessages {
33
+ /** Label of the `auto` option. Defaults to `'Automatisch'`. */
34
+ auto?: string
35
+ /** Label of the `fill` option. Defaults to `'Vullen'`. */
36
+ fill?: string
37
+ /** Label of the `fixed` option. Defaults to `'Vast'`. */
38
+ fixed?: string
39
+ /**
40
+ * Accessible name of the `auto` option. Defaults to the label followed by
41
+ * the option, lowercased: `'Breedte automatisch'`. Pass it for a language
42
+ * where that concatenation does not read.
43
+ */
44
+ autoAriaLabel?: string
45
+ /** Accessible name of the `fill` option. Defaults to `'Breedte vullen'`, built the same way. */
46
+ fillAriaLabel?: string
47
+ /** Accessible name of the `fixed` option. Defaults to `'Breedte vast'`, built the same way. */
48
+ fixedAriaLabel?: string
49
+ /** Accessible name of the unit selector, when more than one unit is offered. Defaults to `'Eenheid'`. */
50
+ unit?: string
51
+ /** How the px unit is written. Defaults to `'px'`. */
52
+ px?: string
53
+ /** How the percentage unit is written. Defaults to `'%'`. */
54
+ percent?: string
55
+ }
56
+
57
+ export interface NexxtEditorSizeProps {
58
+ /**
59
+ * Header text of the settings block, and the accessible name of the fixed
60
+ * field. Defaults to the Dutch `'Formaat'`; a consumer showing a width and a
61
+ * height passes `'Breedte'` and `'Hoogte'`.
62
+ */
63
+ label?: string
64
+ /**
65
+ * The modes offered, as a subset of `'auto'`, `'fill'` and `'fixed'`; they
66
+ * are always shown in that order. An image, which has nothing to fill,
67
+ * passes `['auto', 'fixed']`. Defaults to all three. A model whose mode is
68
+ * not offered shows no option selected.
69
+ */
70
+ modes?: EditorSizeMode[]
71
+ /** The units the fixed field offers. Defaults to `['px']`. */
72
+ units?: EditorDimensionUnit[]
73
+ /** Lowest fixed value per unit, merged over `EDITOR_DIMENSION_LIMITS`. */
74
+ min?: Partial<Record<EditorDimensionUnit, number>>
75
+ /** Highest fixed value per unit, merged over `EDITOR_DIMENSION_LIMITS`. */
76
+ max?: Partial<Record<EditorDimensionUnit, number>>
77
+ /**
78
+ * Copy of the options and the field. Defaults are Dutch and are merged per
79
+ * key, so overriding one keeps the defaults of the others. Copy is never
80
+ * translated inside the component.
81
+ */
82
+ messages?: EditorSizeMessages
83
+ }
84
+
85
+ const DEFAULT_MESSAGES = {
86
+ auto: 'Automatisch',
87
+ fill: 'Vullen',
88
+ fixed: 'Vast',
89
+ unit: 'Eenheid',
90
+ px: 'px',
91
+ percent: '%',
92
+ // The accessible names default to a string built from the label, not to copy of their own.
93
+ } satisfies Required<Omit<EditorSizeMessages, 'autoAriaLabel' | 'fillAriaLabel' | 'fixedAriaLabel'>>
94
+ </script>
95
+
96
+ <script lang="ts" setup>
97
+ import { computed, onBeforeUnmount, useTemplateRef, watch } from 'vue'
98
+ import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
99
+ import NexxtEditorDimensionField from '../../atoms/EditorDimensionField/EditorDimensionField.vue'
100
+ import NexxtToggleGroup from '../../../molecules/ToggleGroup/ToggleGroup.vue'
101
+ import type { NexxtToggleGroupOption } from '../../../molecules/ToggleGroup/ToggleGroup.vue'
102
+
103
+ defineOptions({ name: 'NexxtEditorSize' })
104
+
105
+ const {
106
+ label = 'Formaat',
107
+ modes,
108
+ units = ['px'],
109
+ min,
110
+ max,
111
+ messages,
112
+ } = defineProps<NexxtEditorSizeProps>()
113
+
114
+ const emit = defineEmits<{
115
+ /**
116
+ * One edit is finished: a mode was picked, or the fixed field committed (see
117
+ * `NexxtEditorDimensionField`). A consumer that folds model writes into one
118
+ * undo step closes it here.
119
+ */
120
+ commit: []
121
+ }>()
122
+
123
+ /**
124
+ * The size. Fill `fixed` with the measured size of the element, as the app
125
+ * always does. Without it, picking `'fixed'` writes `{ mode: 'fixed' }` and
126
+ * commits straight away, leaving a fixed mode without a size until a value is
127
+ * typed; with the measured size, that state does not come up.
128
+ */
129
+ const model = defineModel<EditorSizeValue>({ required: true })
130
+
131
+ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
132
+
133
+ const fieldEl = useTemplateRef<InstanceType<typeof NexxtEditorDimensionField>>('field')
134
+
135
+ // Two of these blocks usually sit together (width and height), and without
136
+ // the label in the name both would read out as the same three buttons.
137
+ const ariaLabelOf = (token: EditorSizeMode) =>
138
+ messages?.[`${token}AriaLabel`] ?? `${label} ${copy.value[token].toLocaleLowerCase()}`
139
+
140
+ const options = computed<NexxtToggleGroupOption[]>(() =>
141
+ EDITOR_SIZE_OPTIONS.filter(({ token }) => !modes || modes.includes(token)).map(({ token }) => ({
142
+ value: token,
143
+ label: copy.value[token],
144
+ ariaLabel: ariaLabelOf(token),
145
+ })),
146
+ )
147
+
148
+ // ToggleGroup types its model as the open `string` of its options; the only
149
+ // values it can emit are the tokens built above.
150
+ //
151
+ /**
152
+ * Set when the user picks `'fixed'`: the field takes focus once the model
153
+ * actually turns fixed. That may be later than the pick -- a consumer can
154
+ * debounce the model or route it through a store -- and until then the field
155
+ * is `inert` and cannot take focus.
156
+ */
157
+ let focusOnFixed = false
158
+
159
+ // The pick expires on the user's next press anywhere, so a pick the consumer
160
+ // never applied does not move focus on a later model change from outside.
161
+ const EXPIRE_ON = ['pointerdown', 'keydown'] as const
162
+
163
+ const expireFocus = () => {
164
+ focusOnFixed = false
165
+ EXPIRE_ON.forEach((type) => document.removeEventListener(type, expireFocus, true))
166
+ }
167
+
168
+ onBeforeUnmount(expireFocus)
169
+
170
+ // Only a pick in the group passes this setter -- a click, or Enter or Space on
171
+ // an option; the group has no roving focus, so arrow keys pick nothing. A mount
172
+ // in `'fixed'` or a model change from outside does not come through here.
173
+ const mode = computed({
174
+ get: () => model.value.mode,
175
+ set: (value: string) => {
176
+ if (value === model.value.mode) return
177
+
178
+ expireFocus()
179
+ if (value === 'fixed') {
180
+ focusOnFixed = true
181
+ // Added while the activating event is already past the document, so the
182
+ // press that picked `'fixed'` does not expire it.
183
+ EXPIRE_ON.forEach((type) => document.addEventListener(type, expireFocus, true))
184
+ }
185
+
186
+ model.value = { ...model.value, mode: value as EditorSizeMode }
187
+ emit('commit')
188
+ },
189
+ })
190
+
191
+ // The field never writes `null` -- it is not optional -- so there is always a
192
+ // dimension to store.
193
+ const fixed = computed({
194
+ get: () => model.value.fixed ?? null,
195
+ set: (value: EditorDimensionValue | null) => {
196
+ if (value) model.value = { ...model.value, fixed: value }
197
+ },
198
+ })
199
+
200
+ const isFixed = computed(() => model.value.mode === 'fixed')
201
+
202
+ // `:inert="false"` still renders the attribute, so `undefined` removes it.
203
+ const fieldInert = computed(() => (isFixed.value ? undefined : true))
204
+
205
+ // After the render that lifts `inert`, so the field can take focus. The first
206
+ // mode change after a pick settles it either way. No scrolling, or the browser
207
+ // would scroll the clipping box of the row that is still sliding open.
208
+ watch(
209
+ () => model.value.mode,
210
+ (next) => {
211
+ const focus = focusOnFixed && next === 'fixed'
212
+ expireFocus()
213
+ if (focus) fieldEl.value?.focus({ preventScroll: true })
214
+ },
215
+ { flush: 'post' },
216
+ )
217
+ </script>
218
+
219
+ <template>
220
+ <NexxtEditorSettingsBlock :label="label">
221
+ <NexxtToggleGroup v-model="mode" :options="options" />
222
+
223
+ <!--
224
+ The field slides open under the choice it belongs to, the same 0fr/1fr
225
+ grid as the second padding row, and stays mounted so it can animate.
226
+ `-mx-1` / `px-1` leave room for the focus ring inside the clipping box.
227
+ -->
228
+ <div
229
+ data-testid="editor-size-fixed-row"
230
+ :inert="fieldInert"
231
+ :class="[
232
+ 'grid transition-[grid-template-rows] duration-200 ease-out-strong motion-reduce:transition-none',
233
+ isFixed ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]',
234
+ ]"
235
+ >
236
+ <div
237
+ :class="[
238
+ '-mx-1 min-h-0 overflow-hidden transition-opacity duration-200 ease-out-strong motion-reduce:transition-none',
239
+ isFixed ? 'opacity-100' : 'opacity-0',
240
+ ]"
241
+ >
242
+ <div class="px-1 pt-2 pb-1">
243
+ <NexxtEditorDimensionField
244
+ ref="field"
245
+ v-model="fixed"
246
+ :label="label"
247
+ :units="units"
248
+ :min="min"
249
+ :max="max"
250
+ :messages="{ unit: copy.unit, px: copy.px, percent: copy.percent }"
251
+ @commit="emit('commit')"
252
+ />
253
+ </div>
254
+ </div>
255
+ </div>
256
+ </NexxtEditorSettingsBlock>
257
+ </template>
@@ -0,0 +1,69 @@
1
+ <script lang="ts">
2
+ import type {
3
+ EditorDimensionFieldMessages,
4
+ EditorDimensionUnit,
5
+ } from '../../atoms/EditorDimensionField/EditorDimensionField.vue'
6
+
7
+ /** Overridable copy of the field. Every key is optional. */
8
+ export type EditorSizeLimitMessages = EditorDimensionFieldMessages
9
+
10
+ /**
11
+ * A free minimum or maximum of an element's size, in px or %, that can be
12
+ * none. Replaces the deprecated `NexxtEditorMaxWidth`, which offers only
13
+ * Tailwind's `max-w-*` steps.
14
+ */
15
+ export interface NexxtEditorSizeLimitProps {
16
+ /** Visible label above the field, and its accessible name, as in `'Maximale breedte'`. */
17
+ label: string
18
+ /** The units the field offers. Defaults to `['px']`. */
19
+ units?: EditorDimensionUnit[]
20
+ /** Lowest value per unit, merged over `EDITOR_DIMENSION_LIMITS`. */
21
+ min?: Partial<Record<EditorDimensionUnit, number>>
22
+ /** Highest value per unit, merged over `EDITOR_DIMENSION_LIMITS`. */
23
+ max?: Partial<Record<EditorDimensionUnit, number>>
24
+ /**
25
+ * Copy of the placeholder and the units. Defaults are Dutch and are merged
26
+ * per key. Copy is never translated inside the component.
27
+ */
28
+ messages?: EditorSizeLimitMessages
29
+ }
30
+ </script>
31
+
32
+ <script lang="ts" setup>
33
+ import NexxtEditorDimensionField from '../../atoms/EditorDimensionField/EditorDimensionField.vue'
34
+ import type { EditorDimensionValue } from '../../atoms/EditorDimensionField/EditorDimensionField.vue'
35
+
36
+ defineOptions({ name: 'NexxtEditorSizeLimit' })
37
+
38
+ const { label, units = ['px'], min, max, messages } = defineProps<NexxtEditorSizeLimitProps>()
39
+
40
+ const emit = defineEmits<{
41
+ /** One edit is finished; see `NexxtEditorDimensionField`. */
42
+ commit: []
43
+ }>()
44
+
45
+ /** The limit, or `null` for none: emptying the field removes it. */
46
+ const model = defineModel<EditorDimensionValue | null>({ default: null })
47
+ </script>
48
+
49
+ <template>
50
+ <!--
51
+ No settings block of its own: a limit sits inside a block that holds the
52
+ sizes it limits, typically two side by side (maximum width, minimum height).
53
+ The label is a real one above the field, like a toggle group's label.
54
+ -->
55
+ <label class="flex min-w-0 flex-col gap-1">
56
+ <span class="small-normal text-gray-700">{{ label }}</span>
57
+
58
+ <NexxtEditorDimensionField
59
+ v-model="model"
60
+ :label="label"
61
+ :units="units"
62
+ :min="min"
63
+ :max="max"
64
+ :messages="messages"
65
+ optional
66
+ @commit="emit('commit')"
67
+ />
68
+ </label>
69
+ </template>
@@ -72,6 +72,7 @@
72
72
  "NexxtTimelineTrack": "components/organisms/TimelineTrack/TimelineTrack.vue",
73
73
  "NexxtToaster": "components/organisms/Toaster/Toaster.vue",
74
74
  "NexxtVideoPlayer": "components/organisms/VideoPlayer/VideoPlayer.vue",
75
+ "NexxtEditorDimensionField": "components/editor/atoms/EditorDimensionField/EditorDimensionField.vue",
75
76
  "NexxtEditorSettingsBlock": "components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue",
76
77
  "NexxtEditorAlignItems": "components/editor/molecules/EditorAlignItems/EditorAlignItems.vue",
77
78
  "NexxtEditorBackgroundSize": "components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue",
@@ -87,5 +88,7 @@
87
88
  "NexxtEditorMaxWidth": "components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue",
88
89
  "NexxtEditorPadding": "components/editor/molecules/EditorPadding/EditorPadding.vue",
89
90
  "NexxtEditorRadius": "components/editor/molecules/EditorRadius/EditorRadius.vue",
91
+ "NexxtEditorSize": "components/editor/molecules/EditorSize/EditorSize.vue",
92
+ "NexxtEditorSizeLimit": "components/editor/molecules/EditorSizeLimit/EditorSizeLimit.vue",
90
93
  "NexxtEditorTextAlign": "components/editor/molecules/EditorTextAlign/EditorTextAlign.vue"
91
94
  }
package/src/index.ts CHANGED
@@ -125,6 +125,19 @@ export type {
125
125
  EditorPaddingMessages,
126
126
  EditorPaddingStep,
127
127
  } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
128
+ export { EDITOR_DIMENSION_LIMITS } from './components/editor/atoms/EditorDimensionField/EditorDimensionField.vue'
129
+ export type {
130
+ EditorDimensionFieldMessages,
131
+ EditorDimensionUnit,
132
+ EditorDimensionValue,
133
+ } from './components/editor/atoms/EditorDimensionField/EditorDimensionField.vue'
134
+ export { EDITOR_SIZE_OPTIONS } from './components/editor/molecules/EditorSize/EditorSize.vue'
135
+ export type {
136
+ EditorSizeMessages,
137
+ EditorSizeMode,
138
+ EditorSizeValue,
139
+ } from './components/editor/molecules/EditorSize/EditorSize.vue'
140
+ export type { EditorSizeLimitMessages } from './components/editor/molecules/EditorSizeLimit/EditorSizeLimit.vue'
128
141
  export type {
129
142
  NexxtTemplateCardContentFeature,
130
143
  NexxtTemplateCardProps,
@@ -152,6 +165,7 @@ export { default as NexxtEditorAlignItems } from './components/editor/molecules/
152
165
  export { default as NexxtEditorBackgroundSize } from './components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue'
153
166
  export { default as NexxtEditorBorder } from './components/editor/molecules/EditorBorder/EditorBorder.vue'
154
167
  export { default as NexxtEditorColor } from './components/editor/molecules/EditorColor/EditorColor.vue'
168
+ export { default as NexxtEditorDimensionField } from './components/editor/atoms/EditorDimensionField/EditorDimensionField.vue'
155
169
  export { default as NexxtEditorFlexDirection } from './components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue'
156
170
  export { default as NexxtEditorFontSize } from './components/editor/molecules/EditorFontSize/EditorFontSize.vue'
157
171
  export { default as NexxtEditorGap } from './components/editor/molecules/EditorGap/EditorGap.vue'
@@ -163,6 +177,8 @@ export { default as NexxtEditorMaxWidth } from './components/editor/molecules/Ed
163
177
  export { default as NexxtEditorPadding } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
164
178
  export { default as NexxtEditorRadius } from './components/editor/molecules/EditorRadius/EditorRadius.vue'
165
179
  export { default as NexxtEditorSettingsBlock } from './components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
180
+ export { default as NexxtEditorSize } from './components/editor/molecules/EditorSize/EditorSize.vue'
181
+ export { default as NexxtEditorSizeLimit } from './components/editor/molecules/EditorSizeLimit/EditorSizeLimit.vue'
166
182
  export { default as NexxtEditorTextAlign } from './components/editor/molecules/EditorTextAlign/EditorTextAlign.vue'
167
183
  export { default as NexxtEnergyLabel } from './components/atoms/EnergyLabel/EnergyLabel.vue'
168
184
  export { default as NexxtFloatingPanel } from './components/atoms/FloatingPanel/FloatingPanel.vue'