@nexxtmove/ui 1.44.0 → 1.46.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/atoms/EditorSettingsBlock/EditorSettingsBlock.vue.d.ts +2 -2
- package/dist/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.vue.d.ts +45 -0
- package/dist/components/editor/atoms/EditorSettingsGroup/editorSettingsGroupContext.d.ts +23 -0
- package/dist/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue.d.ts +6 -0
- package/dist/components/editor/molecules/EditorDashPattern/EditorDashPattern.vue.d.ts +61 -0
- package/dist/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue.d.ts +6 -0
- package/dist/components/editor/molecules/EditorLineStyle/EditorLineStyle.vue.d.ts +78 -0
- package/dist/components/editor/molecules/EditorPadding/EditorPadding.vue.d.ts +11 -0
- package/dist/components/editor/molecules/EditorTextAlign/EditorTextAlign.vue.d.ts +8 -0
- package/dist/components/molecules/ToggleGroup/ToggleGroup.vue.d.ts +22 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2207 -1874
- package/dist/nuxt.js +3 -0
- package/package.json +1 -1
- package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue +36 -5
- package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.vue +228 -0
- package/src/components/editor/atoms/EditorSettingsGroup/editorSettingsGroupContext.ts +27 -0
- package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue +16 -2
- package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.vue +139 -0
- package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue +16 -2
- package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.vue +230 -0
- package/src/components/editor/molecules/EditorPadding/EditorPadding.vue +102 -17
- package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.vue +18 -2
- package/src/components/molecules/ToggleGroup/ToggleGroup.vue +30 -8
- package/src/components/molecules/Tree/Tree.vue +30 -10
- package/src/components.json +3 -0
- package/src/index.ts +22 -0
- package/tailwind/nexxtmove.tailwind.custom.css +6 -0
package/dist/nuxt.js
CHANGED
|
@@ -77,10 +77,12 @@ var s = {
|
|
|
77
77
|
NexxtVideoPlayer: "components/organisms/VideoPlayer/VideoPlayer.vue",
|
|
78
78
|
NexxtEditorDimensionField: "components/editor/atoms/EditorDimensionField/EditorDimensionField.vue",
|
|
79
79
|
NexxtEditorSettingsBlock: "components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue",
|
|
80
|
+
NexxtEditorSettingsGroup: "components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.vue",
|
|
80
81
|
NexxtEditorAlignItems: "components/editor/molecules/EditorAlignItems/EditorAlignItems.vue",
|
|
81
82
|
NexxtEditorBackgroundSize: "components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue",
|
|
82
83
|
NexxtEditorBorder: "components/editor/molecules/EditorBorder/EditorBorder.vue",
|
|
83
84
|
NexxtEditorColor: "components/editor/molecules/EditorColor/EditorColor.vue",
|
|
85
|
+
NexxtEditorDashPattern: "components/editor/molecules/EditorDashPattern/EditorDashPattern.vue",
|
|
84
86
|
NexxtEditorFlexDirection: "components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue",
|
|
85
87
|
NexxtEditorFontSize: "components/editor/molecules/EditorFontSize/EditorFontSize.vue",
|
|
86
88
|
NexxtEditorGap: "components/editor/molecules/EditorGap/EditorGap.vue",
|
|
@@ -88,6 +90,7 @@ var s = {
|
|
|
88
90
|
NexxtEditorJustifyContent: "components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue",
|
|
89
91
|
NexxtEditorLetterSpacing: "components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue",
|
|
90
92
|
NexxtEditorLineHeight: "components/editor/molecules/EditorLineHeight/EditorLineHeight.vue",
|
|
93
|
+
NexxtEditorLineStyle: "components/editor/molecules/EditorLineStyle/EditorLineStyle.vue",
|
|
91
94
|
NexxtEditorMaxWidth: "components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue",
|
|
92
95
|
NexxtEditorPadding: "components/editor/molecules/EditorPadding/EditorPadding.vue",
|
|
93
96
|
NexxtEditorRadius: "components/editor/molecules/EditorRadius/EditorRadius.vue",
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, inject } from 'vue'
|
|
3
3
|
import Switch from '../../../atoms/Switch/Switch.vue'
|
|
4
|
+
import { EDITOR_SETTINGS_GROUP_KEY } from '../EditorSettingsGroup/editorSettingsGroupContext'
|
|
4
5
|
|
|
5
6
|
interface NexxtEditorSettingsBlockProps {
|
|
6
7
|
/** Header text of the block. Wraps over multiple lines when it is too long. */
|
|
@@ -14,7 +15,8 @@ interface NexxtEditorSettingsBlockProps {
|
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* One labelled setting in the editor sidebar, optionally collapsible via a
|
|
17
|
-
* switch. The block draws no separator of its own:
|
|
18
|
+
* switch. The block draws no separator of its own: inside a
|
|
19
|
+
* NexxtEditorSettingsGroup the group draws it, and outside one the consumer
|
|
18
20
|
* divides blocks on the wrapper, e.g. `divide-y divide-gray-200`.
|
|
19
21
|
*/
|
|
20
22
|
defineOptions({ name: 'NexxtEditorSettingsBlock' })
|
|
@@ -34,10 +36,37 @@ const open = computed(() => !switchable || model.value)
|
|
|
34
36
|
// `inert="false"`, which a browser still treats as inert. `undefined` is the
|
|
35
37
|
// only value that removes the attribute again.
|
|
36
38
|
const inert = computed(() => (open.value ? undefined : true))
|
|
39
|
+
|
|
40
|
+
/** Set when the block sits in a NexxtEditorSettingsGroup; `null` outside one. */
|
|
41
|
+
const group = inject(EDITOR_SETTINGS_GROUP_KEY, null)
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* In a group all space sits above the block, inside its own box: a collapsed
|
|
45
|
+
* reveal around it then takes that space along, and the group owns the bottom.
|
|
46
|
+
* Outside a group the block keeps the `py-4` other consumers rely on. Every
|
|
47
|
+
* block ends 4px below its control (the content wrapper's `pb-1`), so the
|
|
48
|
+
* group's `pb-5` puts the next line 24px under the last visible control.
|
|
49
|
+
*/
|
|
50
|
+
const rootClass = computed(() => (group ? 'pt-4' : 'py-4'))
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The only setting of a group without a label of its own carries the group's
|
|
54
|
+
* weight, so it reads as heavy as a labelled group next to it.
|
|
55
|
+
*/
|
|
56
|
+
const promoted = computed(() => group?.value.promoted ?? false)
|
|
57
|
+
|
|
58
|
+
const labelTypography = computed(() => (promoted.value ? 'small-semibold' : 'small-normal'))
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A promoted label is a heading, so heading navigation finds it next to the
|
|
62
|
+
* labelled groups. A switchable label stays the switch's <label>: a heading
|
|
63
|
+
* cannot be one.
|
|
64
|
+
*/
|
|
65
|
+
const labelTag = computed(() => (promoted.value && group ? `h${group.value.headingLevel}` : 'p'))
|
|
37
66
|
</script>
|
|
38
67
|
|
|
39
68
|
<template>
|
|
40
|
-
<div class="
|
|
69
|
+
<div :class="rootClass">
|
|
41
70
|
<!--
|
|
42
71
|
Switch.vue sets `inheritAttrs: false` and binds `$attrs` on its sr-only
|
|
43
72
|
input, so a class on <Switch> never reaches its root <label>. The child
|
|
@@ -49,9 +78,11 @@ const inert = computed(() => (open.value ? undefined : true))
|
|
|
49
78
|
-->
|
|
50
79
|
<div class="flex items-center [&>label]:flex [&>label]:w-full [&>label]:gap-3">
|
|
51
80
|
<Switch v-if="switchable" v-model="model" size="sm" color="blue" label-position="before">
|
|
52
|
-
<span class="grow text-left
|
|
81
|
+
<span :class="['grow text-left', labelTypography, 'text-gray-900']">{{ label }}</span>
|
|
53
82
|
</Switch>
|
|
54
|
-
<
|
|
83
|
+
<component :is="labelTag" v-else :class="[labelTypography, 'text-gray-900']">
|
|
84
|
+
{{ label }}
|
|
85
|
+
</component>
|
|
55
86
|
</div>
|
|
56
87
|
|
|
57
88
|
<div
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { EditorSettingsHeadingLevel } from './editorSettingsGroupContext'
|
|
3
|
+
|
|
4
|
+
export interface NexxtEditorSettingsGroupProps {
|
|
5
|
+
/**
|
|
6
|
+
* Heading of the group. Omit it for a section that holds exactly one
|
|
7
|
+
* setting: that setting's own label is then promoted to the group's
|
|
8
|
+
* typography and heading level, so it does not say the same thing twice.
|
|
9
|
+
*/
|
|
10
|
+
label?: string
|
|
11
|
+
/** Heading level of the label. Defaults to 3: the settings panel's own heading is an h2. */
|
|
12
|
+
headingLevel?: EditorSettingsHeadingLevel
|
|
13
|
+
/**
|
|
14
|
+
* Let the user fold the group away with a button in its heading. Only takes
|
|
15
|
+
* effect together with `label`: a group without a heading has nothing to
|
|
16
|
+
* click, so it stays an ordinary, always-open group and warns in
|
|
17
|
+
* development. The open state is `v-model:open`. Defaults to `false`.
|
|
18
|
+
*/
|
|
19
|
+
collapsible?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Per target state, so a reversal mid-way eases from where it is.
|
|
24
|
+
*
|
|
25
|
+
* Opening uses `ease-out-strong`: the content arrives fast and settles. Closing
|
|
26
|
+
* uses `ease-in-out-strong` over the same 200ms. A strong ease-out would take
|
|
27
|
+
* most of a tall group (~320px) away in the first frames and read as a cut;
|
|
28
|
+
* ease-in-out starts gently, so the closing is seen. Height, the root's bottom
|
|
29
|
+
* padding and the chevron share duration and curve in each direction, so they
|
|
30
|
+
* stay in step frame for frame.
|
|
31
|
+
*
|
|
32
|
+
* The fade on closing is 150ms on the softer `ease-in-out`, so the content
|
|
33
|
+
* stays readable while it folds and is gone before the clip cuts it. Under
|
|
34
|
+
* reduced motion height, padding and chevron jump; the fade stays.
|
|
35
|
+
*/
|
|
36
|
+
const MOTION = {
|
|
37
|
+
root: {
|
|
38
|
+
base: 'transition-[padding-bottom] motion-reduce:transition-none',
|
|
39
|
+
open: 'pb-5 duration-200 ease-out-strong',
|
|
40
|
+
closed: 'pb-6 duration-200 ease-in-out-strong',
|
|
41
|
+
},
|
|
42
|
+
rows: {
|
|
43
|
+
base: 'grid transition-[grid-template-rows] motion-reduce:transition-none',
|
|
44
|
+
open: 'grid-rows-[1fr] duration-200 ease-out-strong',
|
|
45
|
+
closed: 'grid-rows-[0fr] duration-200 ease-in-out-strong',
|
|
46
|
+
},
|
|
47
|
+
clip: {
|
|
48
|
+
base: '-mx-1 -mb-1 min-h-0 overflow-hidden transition-opacity',
|
|
49
|
+
open: 'opacity-100 duration-200 ease-out-strong',
|
|
50
|
+
closed: 'opacity-0 duration-150 ease-in-out',
|
|
51
|
+
},
|
|
52
|
+
chevron: {
|
|
53
|
+
base: 'flex size-5 shrink-0 items-center justify-center transition-transform motion-reduce:transition-none',
|
|
54
|
+
open: 'rotate-0 duration-200 ease-out-strong',
|
|
55
|
+
closed: 'rotate-180 duration-200 ease-in-out-strong',
|
|
56
|
+
},
|
|
57
|
+
} as const
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<script lang="ts" setup>
|
|
61
|
+
import { computed, inject, provide, useId, useTemplateRef, watch } from 'vue'
|
|
62
|
+
import NexxtIcon from '../../../atoms/Icon/Icon.vue'
|
|
63
|
+
import { EDITOR_SETTINGS_GROUP_KEY } from './editorSettingsGroupContext'
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A section of an editor settings panel: the settings that are about the same
|
|
67
|
+
* thing. It draws the divider above itself and owns the vertical space, so the
|
|
68
|
+
* settings inside need neither. Optionally collapsible from its heading.
|
|
69
|
+
*/
|
|
70
|
+
defineOptions({ name: 'NexxtEditorSettingsGroup' })
|
|
71
|
+
|
|
72
|
+
const {
|
|
73
|
+
label,
|
|
74
|
+
headingLevel = 3,
|
|
75
|
+
collapsible = false,
|
|
76
|
+
} = defineProps<NexxtEditorSettingsGroupProps>()
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whether a collapsible group is open. Ignored unless the group is both
|
|
80
|
+
* `collapsible` and labelled; the parent owns the state.
|
|
81
|
+
*/
|
|
82
|
+
const open = defineModel<boolean>('open', { default: true })
|
|
83
|
+
|
|
84
|
+
defineSlots<{
|
|
85
|
+
/** The settings: EditorSettingsBlocks, possibly wrapped in a reveal or a mobile marker. */
|
|
86
|
+
default?: () => unknown
|
|
87
|
+
}>()
|
|
88
|
+
|
|
89
|
+
// One heading level per panel is the whole point: a group in a group would be a
|
|
90
|
+
// second level in 312 px of sidebar. It still renders, so a mistake shows up in
|
|
91
|
+
// the console during development rather than as a blank panel. `process.env`
|
|
92
|
+
// rather than `import.meta.env`: the library build bakes the latter in as
|
|
93
|
+
// production, while the consumer's bundler replaces the former for its own mode.
|
|
94
|
+
if (inject(EDITOR_SETTINGS_GROUP_KEY, null) && process.env.NODE_ENV !== 'production') {
|
|
95
|
+
console.warn('NexxtEditorSettingsGroup cannot be nested')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
provide(
|
|
99
|
+
EDITOR_SETTINGS_GROUP_KEY,
|
|
100
|
+
computed(() => ({ grouped: true, promoted: !label, headingLevel })),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if (collapsible && !label && process.env.NODE_ENV !== 'production') {
|
|
104
|
+
console.warn('NexxtEditorSettingsGroup: collapsible needs a label; the group stays open')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const headingId = useId()
|
|
108
|
+
|
|
109
|
+
const heading = computed(() => `h${headingLevel}`)
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Collapsing needs a heading to put the button in. The heading stays a
|
|
113
|
+
* heading, with one button across the whole row: `-my-0.5 py-0.5` makes it
|
|
114
|
+
* 24px tall without changing the 20px layout height and `-mx-1 px-1` gives the
|
|
115
|
+
* focus ring room, so open it is pixel-equal to a group that does not collapse.
|
|
116
|
+
* No `focus:outline-none`: in Tailwind v4 it sets `--tw-outline-style: none`,
|
|
117
|
+
* which `focus-visible:outline-2` then picks up, so the ring would never show.
|
|
118
|
+
* A button shows no outline on a mouse click anyway. The group is named
|
|
119
|
+
* (`group/toggle`) so a `.group` around the panel cannot light the chevron.
|
|
120
|
+
* The heading is `flex` so those negative margins stay on the button instead
|
|
121
|
+
* of collapsing through the heading and shifting it by 2px.
|
|
122
|
+
*
|
|
123
|
+
* The content stays mounted while closed, so the settings keep their local
|
|
124
|
+
* state and `aria-controls` always points at an element. The first block's
|
|
125
|
+
* `pt-4` sits inside the clip box and folds away with it, so the content
|
|
126
|
+
* wrapper is exactly 0px closed. Open, the last block's 4px of focus-ring room
|
|
127
|
+
* plus the root's `pb-5` make 24px under the last control; closed, that room
|
|
128
|
+
* has folded away too, so the root goes to `pb-6` and the title also ends 24px
|
|
129
|
+
* above the next line. The root padding transitions with the rows, so the
|
|
130
|
+
* height moves in one motion instead of jumping 4px. The clip box reaches 4px past each side (`-mx-1` / `px-1`) and 4px
|
|
131
|
+
* below (`-mb-1` / `pb-1`) without taking layout space, so the focus ring of a
|
|
132
|
+
* full-width or last control is not cut off.
|
|
133
|
+
*
|
|
134
|
+
* None of this is written as template comments: a group that does not
|
|
135
|
+
* collapse has to render exactly the markup it did before.
|
|
136
|
+
*/
|
|
137
|
+
const active = computed(() => collapsible && !!label)
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Derived from the heading id rather than a second `useId`, so a group that
|
|
141
|
+
* does not collapse renders and consumes exactly the ids it did before.
|
|
142
|
+
*/
|
|
143
|
+
const contentId = computed(() => `${headingId}-content`)
|
|
144
|
+
|
|
145
|
+
const expanded = computed(() => !active.value || open.value)
|
|
146
|
+
|
|
147
|
+
// `undefined`, never `false`: `:inert="false"` renders `inert="false"`, which a
|
|
148
|
+
// browser still treats as inert. Same as EditorSettingsBlock.
|
|
149
|
+
const inert = computed(() => (expanded.value ? undefined : true))
|
|
150
|
+
|
|
151
|
+
const state = computed(() => (expanded.value ? 'open' : 'closed'))
|
|
152
|
+
|
|
153
|
+
// A group that does not collapse keeps exactly the classes it had before.
|
|
154
|
+
const rootClass = computed(() =>
|
|
155
|
+
active.value
|
|
156
|
+
? ['border-t border-editor-divider pt-6', MOTION.root.base, MOTION.root[state.value]]
|
|
157
|
+
: ['border-t border-editor-divider pb-5', label ? 'pt-6' : 'pt-2'],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
const toggleEl = useTemplateRef<HTMLButtonElement>('toggle')
|
|
161
|
+
const contentEl = useTemplateRef<HTMLElement>('content')
|
|
162
|
+
|
|
163
|
+
// Closed from outside while focus is inside: `inert` would drop it on <body>.
|
|
164
|
+
// Runs before the DOM update, while the content can still hold focus.
|
|
165
|
+
watch(open, (isOpen) => {
|
|
166
|
+
if (isOpen || !active.value) return
|
|
167
|
+
if (contentEl.value?.contains(document.activeElement)) toggleEl.value?.focus()
|
|
168
|
+
})
|
|
169
|
+
</script>
|
|
170
|
+
|
|
171
|
+
<template>
|
|
172
|
+
<div
|
|
173
|
+
:class="rootClass"
|
|
174
|
+
:role="label ? 'group' : undefined"
|
|
175
|
+
:aria-labelledby="label ? headingId : undefined"
|
|
176
|
+
>
|
|
177
|
+
<!--
|
|
178
|
+
The line sits on top, always, the first group included: it separates the
|
|
179
|
+
group from whatever comes before it, so the last group has none below.
|
|
180
|
+
24px between groups: line to heading, and last control to the next line
|
|
181
|
+
(the last block's 4px of focus-ring room plus `pb-5`); 16px inside one.
|
|
182
|
+
Without a label the top is `pt-2`: the one setting inside brings its own
|
|
183
|
+
`pt-4`, so its promoted label also lands 24px under the line.
|
|
184
|
+
-->
|
|
185
|
+
<component
|
|
186
|
+
:is="heading"
|
|
187
|
+
v-if="active"
|
|
188
|
+
:id="headingId"
|
|
189
|
+
class="flex small-semibold text-gray-900"
|
|
190
|
+
>
|
|
191
|
+
<button
|
|
192
|
+
ref="toggle"
|
|
193
|
+
type="button"
|
|
194
|
+
:aria-expanded="open"
|
|
195
|
+
:aria-controls="contentId"
|
|
196
|
+
class="group/toggle -mx-1 -my-0.5 flex w-[calc(100%+0.5rem)] cursor-pointer items-center justify-between gap-2 rounded-md px-1 py-0.5 text-left focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-cornflower-blue-600"
|
|
197
|
+
@click="open = !open"
|
|
198
|
+
>
|
|
199
|
+
{{ label }}
|
|
200
|
+
<span aria-hidden="true" :class="[MOTION.chevron.base, MOTION.chevron[state]]">
|
|
201
|
+
<NexxtIcon
|
|
202
|
+
name="chevron-down"
|
|
203
|
+
class="extra-small-normal text-gray-700 transition-colors duration-150 group-hover/toggle:text-gray-900"
|
|
204
|
+
/>
|
|
205
|
+
</span>
|
|
206
|
+
</button>
|
|
207
|
+
</component>
|
|
208
|
+
<component :is="heading" v-else-if="label" :id="headingId" class="small-semibold text-gray-900">
|
|
209
|
+
{{ label }}
|
|
210
|
+
</component>
|
|
211
|
+
|
|
212
|
+
<div
|
|
213
|
+
v-if="active"
|
|
214
|
+
:id="contentId"
|
|
215
|
+
ref="content"
|
|
216
|
+
data-testid="editor-settings-group-content"
|
|
217
|
+
:inert="inert"
|
|
218
|
+
:class="[MOTION.rows.base, MOTION.rows[state]]"
|
|
219
|
+
>
|
|
220
|
+
<div :class="[MOTION.clip.base, MOTION.clip[state]]">
|
|
221
|
+
<div class="px-1 pb-1">
|
|
222
|
+
<slot />
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
<slot v-else />
|
|
227
|
+
</div>
|
|
228
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ComputedRef, InjectionKey } from 'vue'
|
|
2
|
+
|
|
3
|
+
/** The heading levels a group label can take. The settings panel's own heading is an h2. */
|
|
4
|
+
export type EditorSettingsHeadingLevel = 2 | 3 | 4 | 5 | 6
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a NexxtEditorSettingsGroup tells the settings blocks inside it. Library
|
|
8
|
+
* internal: the group and the block are the only two that read or write it.
|
|
9
|
+
*/
|
|
10
|
+
export interface EditorSettingsGroupContext {
|
|
11
|
+
/** Always `true`: the block is inside a group, so the group owns the line and the bottom space. */
|
|
12
|
+
grouped: true
|
|
13
|
+
/**
|
|
14
|
+
* The group has no label of its own, so the label of the one setting in it
|
|
15
|
+
* is promoted to the group's typography and heading level.
|
|
16
|
+
*/
|
|
17
|
+
promoted: boolean
|
|
18
|
+
/** Heading level of the group label, and of a promoted setting label. */
|
|
19
|
+
headingLevel: EditorSettingsHeadingLevel
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Provided as a computed, so a label or heading level that changes after mount
|
|
24
|
+
* reaches the blocks without them having to remount.
|
|
25
|
+
*/
|
|
26
|
+
export const EDITOR_SETTINGS_GROUP_KEY: InjectionKey<ComputedRef<EditorSettingsGroupContext>> =
|
|
27
|
+
Symbol('NexxtEditorSettingsGroup')
|
|
@@ -44,6 +44,12 @@ export interface NexxtEditorAlignItemsProps {
|
|
|
44
44
|
* Copy is never translated inside the component.
|
|
45
45
|
*/
|
|
46
46
|
messages?: EditorAlignItemsMessages
|
|
47
|
+
/**
|
|
48
|
+
* The options to offer, as a subset of the four. They are always shown in
|
|
49
|
+
* the order of `EDITOR_ALIGN_ITEMS_OPTIONS`, whatever order this list is in;
|
|
50
|
+
* values not on that list are ignored. Defaults to all four.
|
|
51
|
+
*/
|
|
52
|
+
options?: EditorAlignItemsValue[]
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
const DEFAULT_MESSAGES = {
|
|
@@ -62,7 +68,11 @@ import type { NexxtToggleGroupOption } from '../../../molecules/ToggleGroup/Togg
|
|
|
62
68
|
|
|
63
69
|
defineOptions({ name: 'NexxtEditorAlignItems' })
|
|
64
70
|
|
|
65
|
-
const {
|
|
71
|
+
const {
|
|
72
|
+
label = 'Uitlijning items',
|
|
73
|
+
messages,
|
|
74
|
+
options: offered,
|
|
75
|
+
} = defineProps<NexxtEditorAlignItemsProps>()
|
|
66
76
|
|
|
67
77
|
/** The selected alignment, as the Tailwind class to apply. */
|
|
68
78
|
const model = defineModel<EditorAlignItemsValue>({ required: true })
|
|
@@ -91,8 +101,12 @@ const ICONS: Record<
|
|
|
91
101
|
stretch: 'arrows-up-down',
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
// Filtered from the full list rather than mapped from `offered`, so the order is
|
|
105
|
+
// always the canonical one and a duplicate in `offered` cannot double a button.
|
|
94
106
|
const options = computed<NexxtToggleGroupOption[]>(() =>
|
|
95
|
-
EDITOR_ALIGN_ITEMS_OPTIONS.
|
|
107
|
+
EDITOR_ALIGN_ITEMS_OPTIONS.filter(
|
|
108
|
+
({ token }) => !offered || offered.includes(`items-${token}`),
|
|
109
|
+
).map(({ token }) => ({
|
|
96
110
|
value: `items-${token}`,
|
|
97
111
|
icon: ICONS[token],
|
|
98
112
|
ariaLabel: copy.value[token],
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
EditorDimensionUnit,
|
|
4
|
+
EditorDimensionValue,
|
|
5
|
+
} from '../../atoms/EditorDimensionField/EditorDimensionField.vue'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A dash pattern in whole px: the length of one dash and the space between two.
|
|
9
|
+
* Numbers rather than classes, the same as `EditorDimensionValue`: the consumer
|
|
10
|
+
* writes them into CSS variables.
|
|
11
|
+
*/
|
|
12
|
+
export interface EditorDashPatternValue {
|
|
13
|
+
dash: number
|
|
14
|
+
gap: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The range each value is clamped to, in px. Exported so the consumer's class
|
|
19
|
+
* matcher knows the same bounds.
|
|
20
|
+
*/
|
|
21
|
+
export const EDITOR_DASH_PATTERN_LIMITS = {
|
|
22
|
+
dash: { min: 1, max: 64 },
|
|
23
|
+
gap: { min: 1, max: 64 },
|
|
24
|
+
} as const
|
|
25
|
+
|
|
26
|
+
/** Overridable copy of the two fields. Every key is optional. */
|
|
27
|
+
export interface EditorDashPatternMessages {
|
|
28
|
+
/** Caption and accessible name of the dash length field. Defaults to `'Lengte'`. */
|
|
29
|
+
dash?: string
|
|
30
|
+
/** Caption and accessible name of the gap field. Defaults to `'Tussenruimte'`. */
|
|
31
|
+
gap?: string
|
|
32
|
+
/** How the px unit is written in both fields. Defaults to `'px'`. */
|
|
33
|
+
unit?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NexxtEditorDashPatternProps {
|
|
37
|
+
/**
|
|
38
|
+
* Header text of the settings block. Defaults to the Dutch `'Streep'`; the
|
|
39
|
+
* library has no i18n layer, so a consumer in another language passes its
|
|
40
|
+
* own copy in.
|
|
41
|
+
*/
|
|
42
|
+
label?: string
|
|
43
|
+
/**
|
|
44
|
+
* Captions of the fields and the unit. Defaults are Dutch and are merged per
|
|
45
|
+
* key, so overriding one keeps the defaults of the others. Copy is never
|
|
46
|
+
* translated inside the component.
|
|
47
|
+
*/
|
|
48
|
+
messages?: EditorDashPatternMessages
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const DEFAULT_MESSAGES = {
|
|
52
|
+
dash: 'Lengte',
|
|
53
|
+
gap: 'Tussenruimte',
|
|
54
|
+
unit: 'px',
|
|
55
|
+
} satisfies Required<EditorDashPatternMessages>
|
|
56
|
+
|
|
57
|
+
type DashPatternPart = keyof EditorDashPatternValue
|
|
58
|
+
|
|
59
|
+
/** Both values are whole px; there is no other unit to pick. */
|
|
60
|
+
const UNITS: EditorDimensionUnit[] = ['px']
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<script lang="ts" setup>
|
|
64
|
+
import { computed } from 'vue'
|
|
65
|
+
import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
|
|
66
|
+
import NexxtEditorDimensionField from '../../atoms/EditorDimensionField/EditorDimensionField.vue'
|
|
67
|
+
|
|
68
|
+
defineOptions({ name: 'NexxtEditorDashPattern' })
|
|
69
|
+
|
|
70
|
+
const { label = 'Streep', messages } = defineProps<NexxtEditorDashPatternProps>()
|
|
71
|
+
|
|
72
|
+
/** The dash pattern, in whole px. */
|
|
73
|
+
const model = defineModel<EditorDashPatternValue>({ required: true })
|
|
74
|
+
|
|
75
|
+
const emit = defineEmits<{
|
|
76
|
+
/**
|
|
77
|
+
* One edit in either field is finished -- passed on from the field: a typed
|
|
78
|
+
* value confirmed on blur or Enter, or an arrow key let go. The model has
|
|
79
|
+
* already been written by then, so a consumer that folds writes into one
|
|
80
|
+
* undo step closes it here.
|
|
81
|
+
*/
|
|
82
|
+
commit: []
|
|
83
|
+
}>()
|
|
84
|
+
|
|
85
|
+
const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* One field's view of the model. The field is never `optional`, so it never
|
|
89
|
+
* writes `null` -- an emptied field puts the previous value back -- and the
|
|
90
|
+
* guard is only there for the type.
|
|
91
|
+
*/
|
|
92
|
+
const part = (key: DashPatternPart) =>
|
|
93
|
+
computed({
|
|
94
|
+
get: (): EditorDimensionValue | null => ({ value: model.value[key], unit: 'px' }),
|
|
95
|
+
set: (next: EditorDimensionValue | null) => {
|
|
96
|
+
if (next) model.value = { ...model.value, [key]: next.value }
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const dash = part('dash')
|
|
101
|
+
const gap = part('gap')
|
|
102
|
+
|
|
103
|
+
const fieldOf = (key: DashPatternPart, model: typeof dash) => ({
|
|
104
|
+
key,
|
|
105
|
+
model,
|
|
106
|
+
label: copy.value[key],
|
|
107
|
+
min: { px: EDITOR_DASH_PATTERN_LIMITS[key].min },
|
|
108
|
+
max: { px: EDITOR_DASH_PATTERN_LIMITS[key].max },
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const fields = computed(() => [fieldOf('dash', dash), fieldOf('gap', gap)])
|
|
112
|
+
|
|
113
|
+
const fieldMessages = computed(() => ({ px: copy.value.unit }))
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<NexxtEditorSettingsBlock :label="label">
|
|
118
|
+
<div class="grid grid-cols-2 gap-2">
|
|
119
|
+
<!--
|
|
120
|
+
Each field gets a visible caption: the inputs have no label of their
|
|
121
|
+
own, and two px fields side by side cannot be told apart otherwise. The
|
|
122
|
+
caption is repeated as the field's accessible name, with the unit
|
|
123
|
+
appended by the field itself.
|
|
124
|
+
-->
|
|
125
|
+
<div v-for="field in fields" :key="field.key" class="flex min-w-0 flex-col gap-1">
|
|
126
|
+
<span class="small-normal text-gray-700" aria-hidden="true">{{ field.label }}</span>
|
|
127
|
+
<NexxtEditorDimensionField
|
|
128
|
+
v-model="field.model.value"
|
|
129
|
+
:label="field.label"
|
|
130
|
+
:units="UNITS"
|
|
131
|
+
:min="field.min"
|
|
132
|
+
:max="field.max"
|
|
133
|
+
:messages="fieldMessages"
|
|
134
|
+
@commit="emit('commit')"
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</NexxtEditorSettingsBlock>
|
|
139
|
+
</template>
|
|
@@ -45,6 +45,12 @@ export interface NexxtEditorJustifyContentProps {
|
|
|
45
45
|
* Copy is never translated inside the component.
|
|
46
46
|
*/
|
|
47
47
|
messages?: EditorJustifyContentMessages
|
|
48
|
+
/**
|
|
49
|
+
* The options to offer, as a subset of the four. They are always shown in
|
|
50
|
+
* the order of `EDITOR_JUSTIFY_CONTENT_OPTIONS`, whatever order this list is
|
|
51
|
+
* in. Defaults to all four.
|
|
52
|
+
*/
|
|
53
|
+
options?: EditorJustifyContentValue[]
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
const DEFAULT_MESSAGES = {
|
|
@@ -63,7 +69,11 @@ import type { NexxtToggleGroupOption } from '../../../molecules/ToggleGroup/Togg
|
|
|
63
69
|
|
|
64
70
|
defineOptions({ name: 'NexxtEditorJustifyContent' })
|
|
65
71
|
|
|
66
|
-
const {
|
|
72
|
+
const {
|
|
73
|
+
label = 'Uitlijning',
|
|
74
|
+
messages,
|
|
75
|
+
options: offered,
|
|
76
|
+
} = defineProps<NexxtEditorJustifyContentProps>()
|
|
67
77
|
|
|
68
78
|
/** The selected justification, as the Tailwind class to apply. */
|
|
69
79
|
const model = defineModel<EditorJustifyContentValue>({ required: true })
|
|
@@ -88,8 +98,12 @@ const ICONS: Record<
|
|
|
88
98
|
between: 'distribute-spacing-horizontal',
|
|
89
99
|
}
|
|
90
100
|
|
|
101
|
+
// Filtered from the full list rather than mapped from `offered`, so the order is
|
|
102
|
+
// always the canonical one and a duplicate in `offered` cannot double a button.
|
|
91
103
|
const options = computed<NexxtToggleGroupOption[]>(() =>
|
|
92
|
-
EDITOR_JUSTIFY_CONTENT_OPTIONS.
|
|
104
|
+
EDITOR_JUSTIFY_CONTENT_OPTIONS.filter(
|
|
105
|
+
({ token }) => !offered || offered.includes(`justify-${token}`),
|
|
106
|
+
).map(({ token }) => ({
|
|
93
107
|
value: `justify-${token}`,
|
|
94
108
|
icon: ICONS[token],
|
|
95
109
|
ariaLabel: copy.value[token],
|