@nexxtmove/ui 1.21.0 → 1.22.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/nuxt.js CHANGED
@@ -68,9 +68,10 @@ var s = {
68
68
  NexxtEditorAlignItems: "components/editor/molecules/EditorAlignItems/EditorAlignItems.vue",
69
69
  NexxtEditorFlexDirection: "components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue",
70
70
  NexxtEditorGap: "components/editor/molecules/EditorGap/EditorGap.vue",
71
+ NexxtEditorHeadingType: "components/editor/molecules/EditorHeadingType/EditorHeadingType.vue",
71
72
  NexxtEditorJustifyContent: "components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue",
72
- NexxtEditorRadius: "components/editor/molecules/EditorRadius/EditorRadius.vue",
73
- NexxtEditorPadding: "components/editor/molecules/EditorPadding/EditorPadding.vue"
73
+ NexxtEditorPadding: "components/editor/molecules/EditorPadding/EditorPadding.vue",
74
+ NexxtEditorRadius: "components/editor/molecules/EditorRadius/EditorRadius.vue"
74
75
  }, c = ["@nexxtmove/ui/ui.css", "@nexxtmove/ui/dist/ui.css"], l = (e, t) => e.some((e) => e === t || c.includes(e)), u = i({
75
76
  meta: {
76
77
  name: "@nexxtmove/ui",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "1.21.0",
4
+ "version": "1.22.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The heading levels the editor offers, in the order they are listed. The level
4
+ * is what the option shows -- its label and its `h1`..`h6` icon -- and the size
5
+ * is what the model carries. The sizes are Tailwind's default scale on purpose,
6
+ * not the theme's `heading-*` utilities: this value is data the editor stores
7
+ * and puts back on an element, so it has to be a class every consumer resolves.
8
+ */
9
+ const HEADING_LEVELS = [
10
+ { level: 1, size: 'text-4xl' },
11
+ { level: 2, size: 'text-3xl' },
12
+ { level: 3, size: 'text-2xl' },
13
+ { level: 4, size: 'text-xl' },
14
+ { level: 5, size: 'text-lg' },
15
+ { level: 6, size: 'text-base' },
16
+ ] as const
17
+
18
+ /**
19
+ * The heading as the editor needs it: the Tailwind font-size class itself, so
20
+ * the consumer can put the model value straight on the element it styles.
21
+ * Weight is deliberately not part of it -- that becomes a setting of its own.
22
+ */
23
+ export type EditorHeadingTypeValue = (typeof HEADING_LEVELS)[number]['size']
24
+
25
+ /** Overridable copy for the six options. Every key is optional. */
26
+ export interface EditorHeadingTypeMessages {
27
+ /** Label of the `h1` option. Defaults to `'Heading 1'`. */
28
+ h1?: string
29
+ /** Label of the `h2` option. Defaults to `'Heading 2'`. */
30
+ h2?: string
31
+ /** Label of the `h3` option. Defaults to `'Heading 3'`. */
32
+ h3?: string
33
+ /** Label of the `h4` option. Defaults to `'Heading 4'`. */
34
+ h4?: string
35
+ /** Label of the `h5` option. Defaults to `'Heading 5'`. */
36
+ h5?: string
37
+ /** Label of the `h6` option. Defaults to `'Heading 6'`. */
38
+ h6?: string
39
+ }
40
+
41
+ export interface NexxtEditorHeadingTypeProps {
42
+ /**
43
+ * Header text of the settings block. Defaults to the Dutch `'Type'`; the
44
+ * library has no i18n layer, so a consumer in another language passes its
45
+ * own copy in.
46
+ */
47
+ label?: string
48
+ /**
49
+ * Labels of the six options. The defaults use the English term "Heading",
50
+ * as the Dutch design in Figma does, and are merged per key, so overriding
51
+ * one label keeps the defaults of the others. Copy is never translated
52
+ * inside the component.
53
+ */
54
+ messages?: EditorHeadingTypeMessages
55
+ }
56
+
57
+ const DEFAULT_MESSAGES = {
58
+ h1: 'Heading 1',
59
+ h2: 'Heading 2',
60
+ h3: 'Heading 3',
61
+ h4: 'Heading 4',
62
+ h5: 'Heading 5',
63
+ h6: 'Heading 6',
64
+ } satisfies Required<EditorHeadingTypeMessages>
65
+ </script>
66
+
67
+ <script lang="ts" setup>
68
+ import { computed } from 'vue'
69
+ import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
70
+ import NexxtSelect from '../../../molecules/Select/Select.vue'
71
+ import type { SelectOption } from '../../../molecules/Select/Select.vue'
72
+
73
+ defineOptions({ name: 'NexxtEditorHeadingType' })
74
+
75
+ const { label = 'Type', messages } = defineProps<NexxtEditorHeadingTypeProps>()
76
+
77
+ /** The selected heading, as the Tailwind font-size class to apply. */
78
+ const model = defineModel<EditorHeadingTypeValue>({ required: true })
79
+
80
+ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
81
+
82
+ // The level doubles as the messages key and the Font Awesome icon name --
83
+ // `h1` through `h6` all exist in the kit.
84
+ const options = computed<SelectOption[]>(() =>
85
+ HEADING_LEVELS.map(({ level, size }) => {
86
+ const key = `h${level}` as const
87
+ return { value: size, label: copy.value[key], icon: key }
88
+ }),
89
+ )
90
+
91
+ const values = HEADING_LEVELS.map(({ size }) => size)
92
+
93
+ // Select types its model as the open `string | number | null` of its options,
94
+ // so writing straight back into the narrower model is a type error. The setter
95
+ // is the one place that narrowing happens, and it is guarded: a value that is
96
+ // not one of the six options is dropped rather than written through.
97
+ const selected = computed({
98
+ get: () => model.value,
99
+ set: (value: string | number | null) => {
100
+ const found = values.find((option) => option === value)
101
+ if (found) model.value = found
102
+ },
103
+ })
104
+ </script>
105
+
106
+ <template>
107
+ <NexxtEditorSettingsBlock :label="label">
108
+ <!--
109
+ Select is `inline-flex` and its field carries a `min-w-60`, so without
110
+ `w-full` it would sit at that minimum instead of filling the block. The
111
+ class lands on Select's root, whose trigger is already `w-full`.
112
+ -->
113
+ <!--
114
+ The block label is a <p>, not a <label>, so the combobox would have no
115
+ accessible name of its own; `aria-label` gives it the same copy without
116
+ repeating it on screen.
117
+ -->
118
+ <NexxtSelect v-model="selected" :options="options" :aria-label="label" class="w-full" />
119
+ </NexxtEditorSettingsBlock>
120
+ </template>
@@ -3,11 +3,25 @@ import { computed, ref, useTemplateRef, watch } from 'vue'
3
3
  import NexxtIcon from '../../atoms/Icon/Icon.vue'
4
4
  import NexxtFloatingPanel from '../../atoms/FloatingPanel/FloatingPanel.vue'
5
5
  import type Icon from '../../atoms/Icon/Icon.vue'
6
-
7
- export type SelectOption = { label: string; value: string | number } | string
6
+ import type { NexxtIconName } from '../../atoms/Icon/Icon.vue'
7
+
8
+ export type SelectOption =
9
+ | {
10
+ label: string
11
+ value: string | number
12
+ /**
13
+ * Icon rendered before the option's label in the list, and in the field
14
+ * while the option is selected. Optional; an explicit `leftIcon` on the
15
+ * field wins over it.
16
+ */
17
+ icon?: NexxtIconName
18
+ }
19
+ | string
8
20
 
9
21
  const getLabel = (opt: SelectOption) => (typeof opt === 'string' ? opt : opt.label)
10
22
  const getValue = (opt: SelectOption): string | number => (typeof opt === 'string' ? opt : opt.value)
23
+ const getIcon = (opt: SelectOption): NexxtIconName | undefined =>
24
+ typeof opt === 'string' ? undefined : opt.icon
11
25
 
12
26
  const generateId = () => 'select-' + Math.random().toString(36).slice(2, 10)
13
27
  const selectId = generateId()
@@ -31,6 +45,13 @@ interface NexxtSelectProps {
31
45
  leftIcon?: InstanceType<typeof Icon>['name']
32
46
  /** Mark the field as required (renders an asterisk next to the label). */
33
47
  required?: boolean
48
+ /**
49
+ * Accessible name of the field, for when it is labelled by something outside
50
+ * the component and `label` would duplicate that copy on screen. Lands on the
51
+ * combobox itself, which a fallthrough attribute cannot reach: the component
52
+ * root is the wrapper around it.
53
+ */
54
+ ariaLabel?: string
34
55
  }
35
56
 
36
57
  const props = withDefaults(defineProps<NexxtSelectProps>(), {
@@ -55,6 +76,14 @@ const selectedIndex = computed(() =>
55
76
  props.options.findIndex((opt) => getValue(opt) === model.value),
56
77
  )
57
78
 
79
+ // The field shows one icon at most. An explicit `leftIcon` is the field's own
80
+ // decoration and outranks the selected option's icon, which is only a fallback.
81
+ const triggerIcon = computed<NexxtIconName | undefined>(() => {
82
+ if (props.leftIcon) return props.leftIcon
83
+ const found = props.options.find((opt) => getValue(opt) === model.value)
84
+ return found ? getIcon(found) : undefined
85
+ })
86
+
58
87
  const select = (opt: SelectOption, close: () => void) => {
59
88
  model.value = getValue(opt)
60
89
  close()
@@ -139,22 +168,26 @@ watch(model, () => {
139
168
  <div
140
169
  :id="selectId"
141
170
  role="combobox"
171
+ :aria-label="ariaLabel"
142
172
  :aria-expanded="isOpen"
143
173
  aria-haspopup="listbox"
144
174
  :aria-controls="isOpen ? listboxId : undefined"
145
175
  :aria-activedescendant="isOpen && activeIndex >= 0 ? optionId(activeIndex) : undefined"
146
176
  tabindex="0"
147
177
  class="group flex w-full cursor-pointer items-center gap-2 rounded-lg ring transition-colors ease-out focus:ring-cornflower-blue-500 focus:outline-none"
148
- :class="[error ? 'ring-brick-400' : 'ring-gray-200', { 'pl-3': leftIcon, 'pr-3': true }]"
178
+ :class="[
179
+ error ? 'ring-brick-400' : 'ring-gray-200',
180
+ { 'pl-3': triggerIcon, 'pr-3': true },
181
+ ]"
149
182
  @click="toggle"
150
183
  @keydown="onKeydown"
151
184
  >
152
- <span v-if="leftIcon" class="flex items-center">
153
- <NexxtIcon :name="leftIcon" />
185
+ <span v-if="triggerIcon" class="flex items-center">
186
+ <NexxtIcon :name="triggerIcon" />
154
187
  </span>
155
188
  <span
156
189
  class="flex h-10 w-full min-w-60 items-center text-sm"
157
- :class="[selectedLabel ? 'text-gray-900' : 'text-gray-400', { 'pl-3': !leftIcon }]"
190
+ :class="[selectedLabel ? 'text-gray-900' : 'text-gray-400', { 'pl-3': !triggerIcon }]"
158
191
  >
159
192
  {{ selectedLabel ?? placeholder }}
160
193
  </span>
@@ -186,7 +219,16 @@ watch(model, () => {
186
219
  @click="select(opt, close)"
187
220
  @mousemove="activeIndex = index"
188
221
  >
189
- {{ getLabel(opt) }}
222
+ <span class="flex items-center gap-2">
223
+ <!-- The cast is the v-if guard restated: `name` is required, and
224
+ template narrowing does not carry across a function call. -->
225
+ <NexxtIcon
226
+ v-if="getIcon(opt)"
227
+ :name="getIcon(opt) as NexxtIconName"
228
+ class="shrink-0"
229
+ />
230
+ {{ getLabel(opt) }}
231
+ </span>
190
232
  <NexxtIcon v-if="model === getValue(opt)" name="check" class="text-xs" />
191
233
  </li>
192
234
  </ul>
@@ -65,7 +65,8 @@
65
65
  "NexxtEditorAlignItems": "components/editor/molecules/EditorAlignItems/EditorAlignItems.vue",
66
66
  "NexxtEditorFlexDirection": "components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue",
67
67
  "NexxtEditorGap": "components/editor/molecules/EditorGap/EditorGap.vue",
68
+ "NexxtEditorHeadingType": "components/editor/molecules/EditorHeadingType/EditorHeadingType.vue",
68
69
  "NexxtEditorJustifyContent": "components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue",
69
- "NexxtEditorRadius": "components/editor/molecules/EditorRadius/EditorRadius.vue",
70
- "NexxtEditorPadding": "components/editor/molecules/EditorPadding/EditorPadding.vue"
70
+ "NexxtEditorPadding": "components/editor/molecules/EditorPadding/EditorPadding.vue",
71
+ "NexxtEditorRadius": "components/editor/molecules/EditorRadius/EditorRadius.vue"
71
72
  }
package/src/index.ts CHANGED
@@ -60,9 +60,10 @@ export { default as NexxtDropdownButton } from './components/organisms/DropdownB
60
60
  export { default as NexxtEditorAlignItems } from './components/editor/molecules/EditorAlignItems/EditorAlignItems.vue'
61
61
  export { default as NexxtEditorFlexDirection } from './components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue'
62
62
  export { default as NexxtEditorGap } from './components/editor/molecules/EditorGap/EditorGap.vue'
63
+ export { default as NexxtEditorHeadingType } from './components/editor/molecules/EditorHeadingType/EditorHeadingType.vue'
63
64
  export { default as NexxtEditorJustifyContent } from './components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue'
64
- export { default as NexxtEditorRadius } from './components/editor/molecules/EditorRadius/EditorRadius.vue'
65
65
  export { default as NexxtEditorPadding } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
66
+ export { default as NexxtEditorRadius } from './components/editor/molecules/EditorRadius/EditorRadius.vue'
66
67
  export { default as NexxtEditorSettingsBlock } from './components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
67
68
  export { default as NexxtEnergyLabel } from './components/atoms/EnergyLabel/EnergyLabel.vue'
68
69
  export { default as NexxtFloatingPanel } from './components/atoms/FloatingPanel/FloatingPanel.vue'