@nexxtmove/ui 1.45.0 → 1.47.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.
Files changed (32) hide show
  1. package/dist/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue.d.ts +2 -2
  2. package/dist/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.vue.d.ts +45 -0
  3. package/dist/components/editor/atoms/EditorSettingsGroup/editorSettingsGroupContext.d.ts +23 -0
  4. package/dist/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue.d.ts +6 -0
  5. package/dist/components/editor/molecules/EditorDashPattern/EditorDashPattern.vue.d.ts +61 -0
  6. package/dist/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue.d.ts +6 -0
  7. package/dist/components/editor/molecules/EditorLineStyle/EditorLineStyle.vue.d.ts +78 -0
  8. package/dist/components/editor/molecules/EditorPadding/EditorPadding.vue.d.ts +11 -0
  9. package/dist/components/editor/molecules/EditorShadow/EditorShadow.vue.d.ts +72 -0
  10. package/dist/components/editor/molecules/EditorTextAlign/EditorTextAlign.vue.d.ts +8 -0
  11. package/dist/components/molecules/ToggleGroup/ToggleGroup.vue.d.ts +22 -1
  12. package/dist/index.d.ts +12 -0
  13. package/dist/index.js +2436 -1972
  14. package/dist/nuxt.js +4 -0
  15. package/package.json +3 -3
  16. package/src/components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue +36 -5
  17. package/src/components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.vue +228 -0
  18. package/src/components/editor/atoms/EditorSettingsGroup/editorSettingsGroupContext.ts +27 -0
  19. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue +17 -3
  20. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue +1 -1
  21. package/src/components/editor/molecules/EditorDashPattern/EditorDashPattern.vue +139 -0
  22. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue +1 -1
  23. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue +17 -3
  24. package/src/components/editor/molecules/EditorLineStyle/EditorLineStyle.vue +230 -0
  25. package/src/components/editor/molecules/EditorPadding/EditorPadding.vue +102 -17
  26. package/src/components/editor/molecules/EditorShadow/EditorShadow.vue +106 -0
  27. package/src/components/editor/molecules/EditorSize/EditorSize.vue +1 -1
  28. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.vue +19 -3
  29. package/src/components/molecules/ToggleGroup/ToggleGroup.vue +105 -11
  30. package/src/components.json +4 -0
  31. package/src/index.ts +29 -0
  32. package/tailwind/nexxtmove.tailwind.custom.css +9 -4
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { ref, useTemplateRef, watch, onMounted, nextTick } from 'vue'
2
+ import { computed, ref, useId, useTemplateRef, watch, onMounted, nextTick } from 'vue'
3
3
  import { useResizeObserver } from '@vueuse/core'
4
4
  import type { IconName } from '@awesome.me/kit-37b149f3ef/icons'
5
5
  import NexxtIcon from '../../atoms/Icon/Icon.vue'
@@ -36,6 +36,22 @@ const props = withDefaults(defineProps<NexxtToggleGroupProps>(), {
36
36
 
37
37
  const model = defineModel<string>({ required: true })
38
38
 
39
+ defineSlots<{
40
+ /**
41
+ * Content of one option, replacing the default icon and label. Rendered
42
+ * twice per option -- once on the button and once in the clipped overlay
43
+ * that shows the active colour -- so it should draw in `currentColor` and
44
+ * carry no state or ids of its own. The button keeps its `ariaLabel` /
45
+ * `tooltip` naming, so content without text still needs `ariaLabel`.
46
+ */
47
+ option?: (props: {
48
+ /** The option being rendered. */
49
+ option: NexxtToggleGroupOption
50
+ /** Whether this option is the selected value. */
51
+ selected: boolean
52
+ }) => unknown
53
+ }>()
54
+
39
55
  const containerRef = useTemplateRef('containerRef')
40
56
  const buttonRefs = ref<HTMLButtonElement[]>([])
41
57
  const sliderStyle = ref({ width: '0px', transform: 'translateX(0px)' })
@@ -65,6 +81,71 @@ const updateSlider = async () => {
65
81
 
66
82
  watch(model, async () => await updateSlider())
67
83
 
84
+ /**
85
+ * Keyboard: the toolbar pattern (WAI-ARIA APG) with a roving tabindex. The
86
+ * group is one tab stop -- the selected option, or the first when none is --
87
+ * and the arrow keys move focus between the options, wrapping at the ends;
88
+ * Home and End jump to the first and last. Moving focus selects nothing:
89
+ * Enter and Space do, through the button's own click. There are no disabled
90
+ * options to skip, since an option cannot be disabled.
91
+ *
92
+ * While focus moves inside the group the tab stop moves with it, so Shift+Tab
93
+ * from a later option leaves the group instead of landing on the selected one.
94
+ * Once focus leaves, the tab stop goes back to the selected option.
95
+ */
96
+ /**
97
+ * The root is a `toolbar` (WAI-ARIA APG): a set of buttons with one tab stop
98
+ * and arrow-key navigation. Not a `radiogroup`, because there the arrows select
99
+ * and here they only move focus. Each option reports whether it is the chosen
100
+ * one with `aria-pressed`, so a screen reader hears "Week, toggle button,
101
+ * pressed". The toolbar is named by the visible `label` when there is one;
102
+ * otherwise a consumer passes `aria-label`, which falls through to the root.
103
+ */
104
+ const labelId = useId()
105
+
106
+ const selectedIndex = computed(() =>
107
+ Math.max(
108
+ 0,
109
+ props.options.findIndex((o) => o.value === model.value),
110
+ ),
111
+ )
112
+
113
+ /** The option that is focused inside the group; `null` while focus is outside. */
114
+ const focusedIndex = ref<number | null>(null)
115
+
116
+ const tabStop = computed(() => focusedIndex.value ?? selectedIndex.value)
117
+
118
+ const NEXT_KEYS: Record<string, (index: number, count: number) => number> = {
119
+ ArrowRight: (index, count) => (index + 1) % count,
120
+ ArrowDown: (index, count) => (index + 1) % count,
121
+ ArrowLeft: (index, count) => (index - 1 + count) % count,
122
+ ArrowUp: (index, count) => (index - 1 + count) % count,
123
+ Home: () => 0,
124
+ End: (_, count) => count - 1,
125
+ }
126
+
127
+ const onKeydown = (event: KeyboardEvent) => {
128
+ const move = NEXT_KEYS[event.key]
129
+ if (!move) return
130
+
131
+ const index = buttonRefs.value.indexOf(event.target as HTMLButtonElement)
132
+ if (index === -1) return
133
+
134
+ // Or the arrows would scroll the panel the group sits in.
135
+ event.preventDefault()
136
+ buttonRefs.value[move(index, props.options.length)]?.focus()
137
+ }
138
+
139
+ const onFocusin = (event: FocusEvent) => {
140
+ const index = buttonRefs.value.indexOf(event.target as HTMLButtonElement)
141
+ if (index !== -1) focusedIndex.value = index
142
+ }
143
+
144
+ const onFocusout = (event: FocusEvent) => {
145
+ const next = event.relatedTarget as Node | null
146
+ if (!next || !containerRef.value?.contains(next)) focusedIndex.value = null
147
+ }
148
+
68
149
  useResizeObserver(containerRef, () => updateSlider())
69
150
 
70
151
  onMounted(async () => {
@@ -76,14 +157,17 @@ onMounted(async () => {
76
157
  </script>
77
158
 
78
159
  <template>
79
- <div class="flex flex-col gap-1">
80
- <label v-if="label" class="justify-start small-normal text-gray-700">
160
+ <div class="flex flex-col gap-1" role="toolbar" :aria-labelledby="label ? labelId : undefined">
161
+ <label v-if="label" :id="labelId" class="justify-start small-normal text-gray-700">
81
162
  {{ label }}
82
163
  </label>
83
164
  <div
84
165
  ref="containerRef"
85
166
  class="relative isolate flex h-10 gap-1 rounded-xl bg-gray-100 p-1"
86
167
  :class="{ 'flex-wrap': !equalWidth }"
168
+ @keydown="onKeydown"
169
+ @focusin="onFocusin"
170
+ @focusout="onFocusout"
87
171
  >
88
172
  <!-- slider pill -->
89
173
  <div
@@ -111,13 +195,17 @@ onMounted(async () => {
111
195
  <button
112
196
  :ref="(el) => (buttonRefs[index] = el as HTMLButtonElement)"
113
197
  type="button"
198
+ :tabindex="index === tabStop ? 0 : -1"
199
+ :aria-pressed="option.value === model"
114
200
  :aria-label="option.ariaLabel ?? (option.label ? undefined : option.tooltip)"
115
- class="relative z-10 flex cursor-pointer items-center justify-center gap-1.5 rounded-lg px-4 py-1.5 whitespace-nowrap text-gray-500 hover:text-gray-700"
201
+ class="relative z-10 flex cursor-pointer items-center justify-center gap-1.5 rounded-lg px-4 py-1.5 whitespace-nowrap text-gray-700 hover:text-gray-900"
116
202
  :class="{ 'flex-1': equalWidth }"
117
203
  @click="model = option.value"
118
204
  >
119
- <NexxtIcon v-if="option.icon" :name="option.icon" />
120
- <span v-if="option.label" class="extra-small-semibold">{{ option.label }}</span>
205
+ <slot name="option" :option="option" :selected="option.value === model">
206
+ <NexxtIcon v-if="option.icon" :name="option.icon" />
207
+ <span v-if="option.label" class="extra-small-semibold">{{ option.label }}</span>
208
+ </slot>
121
209
  </button>
122
210
  <template #tooltip>{{ option.tooltip }}</template>
123
211
  </NexxtTooltip>
@@ -125,13 +213,17 @@ onMounted(async () => {
125
213
  v-else
126
214
  :ref="(el) => (buttonRefs[index] = el as HTMLButtonElement)"
127
215
  type="button"
216
+ :tabindex="index === tabStop ? 0 : -1"
217
+ :aria-pressed="option.value === model"
128
218
  :aria-label="option.ariaLabel ?? (option.label ? undefined : option.tooltip)"
129
- class="relative z-10 flex cursor-pointer items-center justify-center gap-1.5 rounded-lg px-4 py-1.5 whitespace-nowrap text-gray-500 hover:text-gray-700"
219
+ class="relative z-10 flex cursor-pointer items-center justify-center gap-1.5 rounded-lg px-4 py-1.5 whitespace-nowrap text-gray-700 hover:text-gray-900"
130
220
  :class="{ 'flex-1': equalWidth }"
131
221
  @click="model = option.value"
132
222
  >
133
- <NexxtIcon v-if="option.icon" :name="option.icon" />
134
- <span v-if="option.label" class="extra-small-semibold">{{ option.label }}</span>
223
+ <slot name="option" :option="option" :selected="option.value === model">
224
+ <NexxtIcon v-if="option.icon" :name="option.icon" />
225
+ <span v-if="option.label" class="extra-small-semibold">{{ option.label }}</span>
226
+ </slot>
135
227
  </button>
136
228
  </template>
137
229
 
@@ -150,8 +242,10 @@ onMounted(async () => {
150
242
  class="flex items-center justify-center gap-1.5 rounded-lg px-4 py-1.5 whitespace-nowrap"
151
243
  :class="[{ 'flex-1': equalWidth }, option.activeClass ?? 'text-cornflower-blue-600']"
152
244
  >
153
- <NexxtIcon v-if="option.icon" :name="option.icon" />
154
- <span v-if="option.label" class="extra-small-semibold">{{ option.label }}</span>
245
+ <slot name="option" :option="option" :selected="option.value === model">
246
+ <NexxtIcon v-if="option.icon" :name="option.icon" />
247
+ <span v-if="option.label" class="extra-small-semibold">{{ option.label }}</span>
248
+ </slot>
155
249
  </span>
156
250
  </div>
157
251
  </div>
@@ -74,10 +74,12 @@
74
74
  "NexxtVideoPlayer": "components/organisms/VideoPlayer/VideoPlayer.vue",
75
75
  "NexxtEditorDimensionField": "components/editor/atoms/EditorDimensionField/EditorDimensionField.vue",
76
76
  "NexxtEditorSettingsBlock": "components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue",
77
+ "NexxtEditorSettingsGroup": "components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.vue",
77
78
  "NexxtEditorAlignItems": "components/editor/molecules/EditorAlignItems/EditorAlignItems.vue",
78
79
  "NexxtEditorBackgroundSize": "components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue",
79
80
  "NexxtEditorBorder": "components/editor/molecules/EditorBorder/EditorBorder.vue",
80
81
  "NexxtEditorColor": "components/editor/molecules/EditorColor/EditorColor.vue",
82
+ "NexxtEditorDashPattern": "components/editor/molecules/EditorDashPattern/EditorDashPattern.vue",
81
83
  "NexxtEditorFlexDirection": "components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue",
82
84
  "NexxtEditorFontSize": "components/editor/molecules/EditorFontSize/EditorFontSize.vue",
83
85
  "NexxtEditorGap": "components/editor/molecules/EditorGap/EditorGap.vue",
@@ -85,9 +87,11 @@
85
87
  "NexxtEditorJustifyContent": "components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue",
86
88
  "NexxtEditorLetterSpacing": "components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue",
87
89
  "NexxtEditorLineHeight": "components/editor/molecules/EditorLineHeight/EditorLineHeight.vue",
90
+ "NexxtEditorLineStyle": "components/editor/molecules/EditorLineStyle/EditorLineStyle.vue",
88
91
  "NexxtEditorMaxWidth": "components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue",
89
92
  "NexxtEditorPadding": "components/editor/molecules/EditorPadding/EditorPadding.vue",
90
93
  "NexxtEditorRadius": "components/editor/molecules/EditorRadius/EditorRadius.vue",
94
+ "NexxtEditorShadow": "components/editor/molecules/EditorShadow/EditorShadow.vue",
91
95
  "NexxtEditorSize": "components/editor/molecules/EditorSize/EditorSize.vue",
92
96
  "NexxtEditorSizeLimit": "components/editor/molecules/EditorSizeLimit/EditorSizeLimit.vue",
93
97
  "NexxtEditorTextAlign": "components/editor/molecules/EditorTextAlign/EditorTextAlign.vue"
package/src/index.ts CHANGED
@@ -78,6 +78,12 @@ export type {
78
78
  EditorRadiusMessages,
79
79
  EditorRadiusValue,
80
80
  } from './components/editor/molecules/EditorRadius/EditorRadius.vue'
81
+ export { EDITOR_SHADOW_OPTIONS } from './components/editor/molecules/EditorShadow/EditorShadow.vue'
82
+ export type {
83
+ EditorShadowMessages,
84
+ EditorShadowValue,
85
+ NexxtEditorShadowProps,
86
+ } from './components/editor/molecules/EditorShadow/EditorShadow.vue'
81
87
  export { EDITOR_TEXT_ALIGN_OPTIONS } from './components/editor/molecules/EditorTextAlign/EditorTextAlign.vue'
82
88
  export type {
83
89
  EditorTextAlignMessages,
@@ -138,6 +144,25 @@ export type {
138
144
  EditorSizeValue,
139
145
  } from './components/editor/molecules/EditorSize/EditorSize.vue'
140
146
  export type { EditorSizeLimitMessages } from './components/editor/molecules/EditorSizeLimit/EditorSizeLimit.vue'
147
+ export {
148
+ EDITOR_LINE_MIN_THICKNESS,
149
+ EDITOR_LINE_STYLE_OPTIONS,
150
+ EDITOR_LINE_THICKNESS_STEPS,
151
+ } from './components/editor/molecules/EditorLineStyle/EditorLineStyle.vue'
152
+ export type {
153
+ EditorLineStyleMessages,
154
+ EditorLineStyleValue,
155
+ EditorLineThickness,
156
+ NexxtEditorLineStyleProps,
157
+ } from './components/editor/molecules/EditorLineStyle/EditorLineStyle.vue'
158
+ export type { NexxtEditorSettingsGroupProps } from './components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.vue'
159
+ export type { EditorSettingsHeadingLevel } from './components/editor/atoms/EditorSettingsGroup/editorSettingsGroupContext'
160
+ export { EDITOR_DASH_PATTERN_LIMITS } from './components/editor/molecules/EditorDashPattern/EditorDashPattern.vue'
161
+ export type {
162
+ EditorDashPatternMessages,
163
+ EditorDashPatternValue,
164
+ NexxtEditorDashPatternProps,
165
+ } from './components/editor/molecules/EditorDashPattern/EditorDashPattern.vue'
141
166
  export type {
142
167
  NexxtTemplateCardContentFeature,
143
168
  NexxtTemplateCardProps,
@@ -165,6 +190,7 @@ export { default as NexxtEditorAlignItems } from './components/editor/molecules/
165
190
  export { default as NexxtEditorBackgroundSize } from './components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue'
166
191
  export { default as NexxtEditorBorder } from './components/editor/molecules/EditorBorder/EditorBorder.vue'
167
192
  export { default as NexxtEditorColor } from './components/editor/molecules/EditorColor/EditorColor.vue'
193
+ export { default as NexxtEditorDashPattern } from './components/editor/molecules/EditorDashPattern/EditorDashPattern.vue'
168
194
  export { default as NexxtEditorDimensionField } from './components/editor/atoms/EditorDimensionField/EditorDimensionField.vue'
169
195
  export { default as NexxtEditorFlexDirection } from './components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue'
170
196
  export { default as NexxtEditorFontSize } from './components/editor/molecules/EditorFontSize/EditorFontSize.vue'
@@ -173,10 +199,13 @@ export { default as NexxtEditorHeadingType } from './components/editor/molecules
173
199
  export { default as NexxtEditorJustifyContent } from './components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue'
174
200
  export { default as NexxtEditorLetterSpacing } from './components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue'
175
201
  export { default as NexxtEditorLineHeight } from './components/editor/molecules/EditorLineHeight/EditorLineHeight.vue'
202
+ export { default as NexxtEditorLineStyle } from './components/editor/molecules/EditorLineStyle/EditorLineStyle.vue'
176
203
  export { default as NexxtEditorMaxWidth } from './components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue'
177
204
  export { default as NexxtEditorPadding } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
178
205
  export { default as NexxtEditorRadius } from './components/editor/molecules/EditorRadius/EditorRadius.vue'
179
206
  export { default as NexxtEditorSettingsBlock } from './components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
207
+ export { default as NexxtEditorSettingsGroup } from './components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.vue'
208
+ export { default as NexxtEditorShadow } from './components/editor/molecules/EditorShadow/EditorShadow.vue'
180
209
  export { default as NexxtEditorSize } from './components/editor/molecules/EditorSize/EditorSize.vue'
181
210
  export { default as NexxtEditorSizeLimit } from './components/editor/molecules/EditorSizeLimit/EditorSizeLimit.vue'
182
211
  export { default as NexxtEditorTextAlign } from './components/editor/molecules/EditorTextAlign/EditorTextAlign.vue'
@@ -50,9 +50,14 @@
50
50
  --spacing-8xl: 100rem;
51
51
  --spacing-full-width: 116rem; /* 1920px (full hd) minus the sidebar: max app width */
52
52
 
53
- /* Motion easings. `ease-out-strong` decelerates hard: things that arrive or
54
- settle into place. `ease-in-out-strong` for elements that move from one
55
- spot to another. */
56
- --ease-out-strong: cubic-bezier(0.23, 1, 0.32, 1);
53
+ /* Motion easings. `ease-out-strong` (above) decelerates hard: things that
54
+ arrive or settle into place. `ease-in-out-strong` for elements that move
55
+ from one spot to another. */
57
56
  --ease-in-out-strong: cubic-bezier(0.77, 0, 0.175, 1);
57
+
58
+ /* The one colour of every divider in the editor settings panel, as
59
+ `border-editor-divider`. Drawn by NexxtEditorSettingsGroup. Same name as
60
+ the copy in the app theme (app/assets/css/main.css), so the app's own
61
+ dividers keep resolving; the app drops its copy once it adopts this. */
62
+ --color-editor-divider: var(--color-gray-200);
58
63
  }