@nexxtmove/ui 1.24.0 → 1.25.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
@@ -66,6 +66,7 @@ var s = {
66
66
  NexxtVideoPlayer: "components/organisms/VideoPlayer/VideoPlayer.vue",
67
67
  NexxtEditorSettingsBlock: "components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue",
68
68
  NexxtEditorAlignItems: "components/editor/molecules/EditorAlignItems/EditorAlignItems.vue",
69
+ NexxtEditorBorder: "components/editor/molecules/EditorBorder/EditorBorder.vue",
69
70
  NexxtEditorColor: "components/editor/molecules/EditorColor/EditorColor.vue",
70
71
  NexxtEditorFlexDirection: "components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue",
71
72
  NexxtEditorGap: "components/editor/molecules/EditorGap/EditorGap.vue",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "1.24.0",
4
+ "version": "1.25.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -0,0 +1,215 @@
1
+ <script lang="ts">
2
+ /**
3
+ * The widths the slider can land on: the Tailwind class and the pixels it
4
+ * renders. These are Tailwind's own defaults (`border` is 1px, then 2, 4 and 8)
5
+ * rather than theme tokens -- the theme defines no border-width scale, so
6
+ * stepping over arbitrary integers would produce classes with no CSS behind
7
+ * them.
8
+ */
9
+ const BORDER_STEPS = [
10
+ { class: 'border', px: 1 },
11
+ { class: 'border-2', px: 2 },
12
+ { class: 'border-4', px: 4 },
13
+ { class: 'border-8', px: 8 },
14
+ ] as const
15
+
16
+ /** The width part of the model value, as the Tailwind class itself. */
17
+ export type EditorBorderWidth = (typeof BORDER_STEPS)[number]['class']
18
+
19
+ /** Overridable copy of the readout and the picker. Every key is optional. */
20
+ export interface EditorBorderMessages {
21
+ /** Unit shown behind the width. Defaults to `'px'`. */
22
+ unit?: string
23
+ /** Text shown while no colour is set. Defaults to `'Kies kleur...'`. */
24
+ placeholder?: string
25
+ /** Accessible name of the width slider. Defaults to `'Dikte'`. */
26
+ width?: string
27
+ }
28
+
29
+ export interface NexxtEditorBorderProps {
30
+ /**
31
+ * Header text of the settings block. Defaults to the Dutch `'Rand'`; the
32
+ * library has no i18n layer, so a consumer in another language passes its own
33
+ * copy in.
34
+ */
35
+ label?: string
36
+ /**
37
+ * Render a switch next to the label that collapses and expands the controls.
38
+ * Defaults to `true`: a border is the kind of setting that is off as often as
39
+ * it is on, so turning it off is part of the control rather than an extra.
40
+ * The switch state is the `enabled` model; the width and the colour are kept
41
+ * while the block is collapsed.
42
+ */
43
+ switchable?: boolean
44
+ /**
45
+ * Copy of the readout and the picker. Merged per key, so overriding one key
46
+ * keeps the defaults of the others. Copy is never translated inside the
47
+ * component.
48
+ */
49
+ messages?: EditorBorderMessages
50
+ }
51
+
52
+ const DEFAULT_MESSAGES = {
53
+ unit: 'px',
54
+ placeholder: 'Kies kleur...',
55
+ width: 'Dikte',
56
+ } satisfies Required<EditorBorderMessages>
57
+
58
+ /**
59
+ * The colour token: `border-[` then a 6- or 8-digit hex. The alpha pair is
60
+ * accepted on read so a stored `#1777EA80` survives a round trip, even though
61
+ * the picker itself is opened without an alpha channel here.
62
+ */
63
+ const COLOR_PATTERN = /^border-\[(#[0-9A-Fa-f]{6}(?:[0-9A-Fa-f]{2})?)\]$/
64
+ </script>
65
+
66
+ <script lang="ts" setup>
67
+ import { computed } from 'vue'
68
+ import NexxtEditorSettingsBlock from '../../atoms/EditorSettingsBlock/EditorSettingsBlock.vue'
69
+ import NexxtSlider from '../../../atoms/Slider/Slider.vue'
70
+ import NexxtColorPicker from '../../../organisms/ColorPicker/ColorPicker.vue'
71
+
72
+ defineOptions({ name: 'NexxtEditorBorder' })
73
+
74
+ const { label = 'Rand', switchable = true, messages } = defineProps<NexxtEditorBorderProps>()
75
+
76
+ /**
77
+ * The border as the editor needs it: one class string that carries both halves
78
+ * of the setting -- a width class, a space, and a `border-[#hex]` colour class,
79
+ * as in `'border-2 border-[#1777EA]'`. Either half may be missing on read and
80
+ * the two may arrive in any order; a change always writes the width first.
81
+ * Tokens this component does not own are dropped by that rewrite.
82
+ *
83
+ * Beware: Tailwind only generates an arbitrary class for text it finds in the
84
+ * source at build time. A value that arrives at runtime -- from a database,
85
+ * say -- therefore gets no CSS of its own; the consumer either translates it
86
+ * into an inline style or safelists the classes it can produce. The hex is
87
+ * always parseable out of the string, which is what makes that translation
88
+ * possible.
89
+ */
90
+ const model = defineModel<string>({ required: true })
91
+
92
+ /** Open state of the block. Only has an effect when `switchable` is `true`. */
93
+ const enabled = defineModel<boolean>('enabled', { default: true })
94
+
95
+ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
96
+
97
+ const maxIndex = BORDER_STEPS.length - 1
98
+
99
+ /** The model read as a token list; whitespace of any shape separates them. */
100
+ const tokens = computed(() => model.value.split(/\s+/).filter(Boolean))
101
+
102
+ /**
103
+ * Position of the current width on the step list, taken from the first token
104
+ * that is a width class so that the token order in the string decides. A string
105
+ * without one -- a colour-only value, or copy from before this component --
106
+ * falls back to the first step instead of leaving the thumb off the track; the
107
+ * first change writes a valid class back.
108
+ */
109
+ const stepIndex = computed(() => {
110
+ const widthToken = tokens.value.find((token) => BORDER_STEPS.some((step) => step.class === token))
111
+
112
+ return Math.max(
113
+ 0,
114
+ BORDER_STEPS.findIndex((step) => step.class === widthToken),
115
+ )
116
+ })
117
+
118
+ /**
119
+ * The hex is part of the class the model writes, so a bare `string` cannot be
120
+ * handed to the writer. This narrows the picker's output and the regex capture
121
+ * to the shape the value promises, without a cast.
122
+ */
123
+ const isHex = (value: string): value is `#${string}` => value.startsWith('#')
124
+
125
+ /** The hex out of the model, or an empty string when it carries no colour. */
126
+ const hex = computed(() => {
127
+ const captured = tokens.value.map((token) => COLOR_PATTERN.exec(token)?.[1]).find(Boolean)
128
+
129
+ return captured && isHex(captured) ? captured : ''
130
+ })
131
+
132
+ /**
133
+ * Both halves are written as one string, always width first, so the stored
134
+ * value has a single shape no matter what arrived. Without a colour the width
135
+ * stands alone rather than gaining an empty `border-[]`.
136
+ */
137
+ const write = (width: EditorBorderWidth, color: string) => {
138
+ model.value = color ? `${width} border-[${color}]` : width
139
+ }
140
+
141
+ // The slider moves over the index, not over the px value, so its model is
142
+ // translated both ways here.
143
+ const sliderValue = computed({
144
+ get: () => [stepIndex.value],
145
+ set: ([index]) => {
146
+ write(BORDER_STEPS[index].class, hex.value)
147
+ },
148
+ })
149
+
150
+ const picked = computed({
151
+ get: () => hex.value,
152
+ // The picker clears its own value in no flow it offers, but an empty string
153
+ // would still wipe a perfectly good colour, so it is dropped rather than
154
+ // written through. The picker keeps rendering the model's colour.
155
+ set: (value: string) => {
156
+ if (isHex(value)) write(BORDER_STEPS[stepIndex.value].class, value)
157
+ },
158
+ })
159
+
160
+ const displayValue = computed(() => `${BORDER_STEPS[stepIndex.value].px} ${copy.value.unit}`)
161
+ </script>
162
+
163
+ <template>
164
+ <NexxtEditorSettingsBlock v-model="enabled" :label="label" :switchable="switchable">
165
+ <div class="flex flex-col gap-3">
166
+ <div class="flex items-center gap-4">
167
+ <!--
168
+ Slider's own `showValue` prints its model, which here is the step index
169
+ rather than the width, so the readout is rendered next to it instead.
170
+ The thumb has the same problem in `aria-valuenow`, which
171
+ `aria-valuetext` overrides with the string the readout shows. The block
172
+ header names the whole setting ('Rand'), so the thumb is named after
173
+ the half it controls instead.
174
+ -->
175
+ <NexxtSlider
176
+ v-model="sliderValue"
177
+ :min="0"
178
+ :max="maxIndex"
179
+ :step="1"
180
+ :aria-label="copy.width"
181
+ :aria-valuetext="displayValue"
182
+ />
183
+
184
+ <!--
185
+ gray-700, not the gray-500 Slider uses for its own readout: axe puts
186
+ gray-500 on white at 1.84:1, well under the 4.5:1 it needs at this size.
187
+ -->
188
+ <span class="min-w-12 shrink-0 text-right small-normal whitespace-nowrap text-gray-700">
189
+ {{ displayValue }}
190
+ </span>
191
+ </div>
192
+
193
+ <!--
194
+ The block label is the switch's own label, so the picker gets no `label`
195
+ of its own -- that would print the heading twice. Its trigger falls back
196
+ to the placeholder for its accessible name, which is why the placeholder
197
+ is the one piece of copy this component passes down.
198
+ -->
199
+ <!--
200
+ `matchWidth` makes the panel as wide as the full-width trigger instead of
201
+ the fixed `w-70` it defaults to. It works by setting a `min-width` on the
202
+ floating element, so a panel in a container narrower than its content
203
+ would still be wider than the trigger; in the settings column it is not.
204
+ -->
205
+ <NexxtColorPicker
206
+ v-model="picked"
207
+ full-width
208
+ match-width
209
+ :formats="['hex']"
210
+ :show-alpha="false"
211
+ :placeholder="copy.placeholder"
212
+ />
213
+ </div>
214
+ </NexxtEditorSettingsBlock>
215
+ </template>
@@ -63,6 +63,7 @@
63
63
  "NexxtVideoPlayer": "components/organisms/VideoPlayer/VideoPlayer.vue",
64
64
  "NexxtEditorSettingsBlock": "components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue",
65
65
  "NexxtEditorAlignItems": "components/editor/molecules/EditorAlignItems/EditorAlignItems.vue",
66
+ "NexxtEditorBorder": "components/editor/molecules/EditorBorder/EditorBorder.vue",
66
67
  "NexxtEditorColor": "components/editor/molecules/EditorColor/EditorColor.vue",
67
68
  "NexxtEditorFlexDirection": "components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue",
68
69
  "NexxtEditorGap": "components/editor/molecules/EditorGap/EditorGap.vue",
package/src/index.ts CHANGED
@@ -30,6 +30,10 @@ export type {
30
30
  EditorColorProperty,
31
31
  EditorColorValue,
32
32
  } from './components/editor/molecules/EditorColor/EditorColor.vue'
33
+ export type {
34
+ EditorBorderMessages,
35
+ EditorBorderWidth,
36
+ } from './components/editor/molecules/EditorBorder/EditorBorder.vue'
33
37
  export type {
34
38
  EditorHeadingTypeMessages,
35
39
  EditorHeadingTypeValue,
@@ -71,6 +75,7 @@ export { default as NexxtDatePicker } from './components/organisms/DatePicker/Da
71
75
  export { default as NexxtDoughnutChart } from './components/molecules/DoughnutChart/DoughnutChart.vue'
72
76
  export { default as NexxtDropdownButton } from './components/organisms/DropdownButton/DropdownButton.vue'
73
77
  export { default as NexxtEditorAlignItems } from './components/editor/molecules/EditorAlignItems/EditorAlignItems.vue'
78
+ export { default as NexxtEditorBorder } from './components/editor/molecules/EditorBorder/EditorBorder.vue'
74
79
  export { default as NexxtEditorColor } from './components/editor/molecules/EditorColor/EditorColor.vue'
75
80
  export { default as NexxtEditorFlexDirection } from './components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue'
76
81
  export { default as NexxtEditorGap } from './components/editor/molecules/EditorGap/EditorGap.vue'