@nexxtmove/ui 1.24.0 → 1.26.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
@@ -28,7 +28,10 @@ var s = {
28
28
  NexxtInputField: "components/molecules/InputField/InputField.vue",
29
29
  NexxtInputSelect: "components/molecules/InputSelect/InputSelect.vue",
30
30
  NexxtModalFooter: "components/molecules/ModalFooter/ModalFooter.vue",
31
+ NexxtPageCard: "components/molecules/PageCard/PageCard.vue",
31
32
  NexxtPageContainer: "components/molecules/PageContainer/PageContainer.vue",
33
+ NexxtPageMockup: "components/molecules/PageMockup/PageMockup.vue",
34
+ NexxtPageTypeTile: "components/molecules/PageTypeTile/PageTypeTile.vue",
32
35
  NexxtPagination: "components/molecules/Pagination/Pagination.vue",
33
36
  NexxtPropertyCard: "components/molecules/PropertyCard/PropertyCard.vue",
34
37
  NexxtSelect: "components/molecules/Select/Select.vue",
@@ -66,6 +69,7 @@ var s = {
66
69
  NexxtVideoPlayer: "components/organisms/VideoPlayer/VideoPlayer.vue",
67
70
  NexxtEditorSettingsBlock: "components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue",
68
71
  NexxtEditorAlignItems: "components/editor/molecules/EditorAlignItems/EditorAlignItems.vue",
72
+ NexxtEditorBorder: "components/editor/molecules/EditorBorder/EditorBorder.vue",
69
73
  NexxtEditorColor: "components/editor/molecules/EditorColor/EditorColor.vue",
70
74
  NexxtEditorFlexDirection: "components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue",
71
75
  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.26.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -0,0 +1,27 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 150">
2
+ <rect x="0" y="0" width="20" height="20" rx="5" fill="#E2E8F0"></rect>
3
+ <rect x="26" y="7" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
4
+ <rect x="240" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
5
+ <rect x="268" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
6
+ <rect x="296" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
7
+ <rect x="0" y="32" width="150" height="8" rx="4" fill="#E2E8F0"></rect>
8
+ <rect x="0" y="46" width="100" height="6" rx="3" fill="#CBD5E1"></rect>
9
+ <g>
10
+ <rect x="0" y="62" width="100" height="44" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
11
+ <rect x="6" y="68" width="88" height="22" rx="4" fill="#FBE4E4"></rect>
12
+ <rect x="6" y="96" width="20" height="4" rx="2" fill="#E2E8F0"></rect>
13
+ </g>
14
+ <g>
15
+ <rect x="110" y="62" width="100" height="44" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
16
+ <rect x="116" y="68" width="88" height="22" rx="4" fill="#FDEBDD"></rect>
17
+ <rect x="116" y="96" width="20" height="4" rx="2" fill="#E2E8F0"></rect>
18
+ </g>
19
+ <g>
20
+ <rect x="220" y="62" width="100" height="44" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
21
+ <rect x="226" y="68" width="88" height="22" rx="4" fill="#E3E8FD"></rect>
22
+ <rect x="226" y="96" width="20" height="4" rx="2" fill="#E2E8F0"></rect>
23
+ </g>
24
+ <rect x="0" y="116" width="320" height="14" rx="4" fill="#7C7CF0"></rect>
25
+ <rect x="8" y="121" width="60" height="4" rx="2" fill="#FFFFFF"></rect>
26
+ <rect x="296" y="121" width="16" height="4" rx="2" fill="#FFFFFF"></rect>
27
+ </svg>
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 150">
2
+ <rect x="0" y="0" width="20" height="20" rx="5" fill="#E2E8F0"></rect>
3
+ <rect x="26" y="7" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
4
+ <rect x="240" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
5
+ <rect x="268" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
6
+ <rect x="296" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
7
+ <rect x="0" y="118" width="150" height="8" rx="4" fill="#E2E8F0"></rect>
8
+ <rect x="0" y="132" width="100" height="6" rx="3" fill="#CBD5E1"></rect>
9
+ </svg>
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 150">
2
+ <rect x="0" y="0" width="20" height="20" rx="5" fill="#E2E8F0"></rect>
3
+ <rect x="26" y="7" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
4
+ <rect x="240" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
5
+ <rect x="268" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
6
+ <rect x="296" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
7
+ <rect x="0" y="34" width="180" height="10" rx="4" fill="#E2E8F0"></rect>
8
+ <rect x="0" y="58" width="320" height="6" rx="3" fill="#CBD5E1"></rect>
9
+ <rect x="0" y="72" width="320" height="6" rx="3" fill="#CBD5E1"></rect>
10
+ <rect x="0" y="86" width="320" height="6" rx="3" fill="#CBD5E1"></rect>
11
+ <rect x="0" y="100" width="320" height="6" rx="3" fill="#CBD5E1"></rect>
12
+ <rect x="0" y="114" width="160" height="6" rx="3" fill="#CBD5E1"></rect>
13
+ </svg>
@@ -0,0 +1,34 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 150">
2
+ <rect x="0" y="0" width="20" height="20" rx="5" fill="#E2E8F0"></rect>
3
+ <rect x="26" y="7" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
4
+ <rect x="240" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
5
+ <rect x="268" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
6
+ <rect x="296" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
7
+ <g>
8
+ <rect x="0" y="34" width="100" height="100" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
9
+ <rect x="10" y="44" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
10
+ <rect x="10" y="58" width="60" height="10" rx="3" fill="#CBD5E1"></rect>
11
+ <rect x="10" y="88" width="70" height="4" rx="2" fill="#E2E8F0"></rect>
12
+ <rect x="10" y="100" width="70" height="4" rx="2" fill="#E2E8F0"></rect>
13
+ <rect x="10" y="112" width="70" height="4" rx="2" fill="#E2E8F0"></rect>
14
+ <rect x="10" y="120" width="80" height="8" rx="4" fill="#E2E8F0"></rect>
15
+ </g>
16
+ <g>
17
+ <rect x="110" y="34" width="100" height="100" rx="6" fill="#E3E3FB" stroke="#7C7CF0"></rect>
18
+ <rect x="120" y="44" width="40" height="6" rx="3" fill="#7C7CF0"></rect>
19
+ <rect x="120" y="58" width="60" height="10" rx="3" fill="#CBD5E1"></rect>
20
+ <rect x="120" y="88" width="70" height="4" rx="2" fill="#E2E8F0"></rect>
21
+ <rect x="120" y="100" width="70" height="4" rx="2" fill="#E2E8F0"></rect>
22
+ <rect x="120" y="112" width="70" height="4" rx="2" fill="#E2E8F0"></rect>
23
+ <rect x="120" y="120" width="80" height="8" rx="4" fill="#7C7CF0"></rect>
24
+ </g>
25
+ <g>
26
+ <rect x="220" y="34" width="100" height="100" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
27
+ <rect x="230" y="44" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
28
+ <rect x="230" y="58" width="60" height="10" rx="3" fill="#CBD5E1"></rect>
29
+ <rect x="230" y="88" width="70" height="4" rx="2" fill="#E2E8F0"></rect>
30
+ <rect x="230" y="100" width="70" height="4" rx="2" fill="#E2E8F0"></rect>
31
+ <rect x="230" y="112" width="70" height="4" rx="2" fill="#E2E8F0"></rect>
32
+ <rect x="230" y="120" width="80" height="8" rx="4" fill="#E2E8F0"></rect>
33
+ </g>
34
+ </svg>
@@ -0,0 +1,19 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 150">
2
+ <rect x="0" y="0" width="20" height="20" rx="5" fill="#E2E8F0"></rect>
3
+ <rect x="26" y="7" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
4
+ <rect x="240" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
5
+ <rect x="268" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
6
+ <rect x="296" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
7
+ <g>
8
+ <rect x="0" y="34" width="155" height="64" rx="6" fill="#E3E8FD"></rect>
9
+ <rect x="0" y="106" width="90" height="7" rx="3" fill="#E2E8F0"></rect>
10
+ <rect x="0" y="118" width="60" height="5" rx="2" fill="#CBD5E1"></rect>
11
+ <rect x="115" y="106" width="40" height="12" rx="6" fill="#E3E3FB"></rect>
12
+ </g>
13
+ <g>
14
+ <rect x="165" y="34" width="155" height="64" rx="6" fill="#E3E8FD"></rect>
15
+ <rect x="165" y="106" width="90" height="7" rx="3" fill="#E2E8F0"></rect>
16
+ <rect x="165" y="118" width="60" height="5" rx="2" fill="#CBD5E1"></rect>
17
+ <rect x="280" y="106" width="40" height="12" rx="6" fill="#E3E3FB"></rect>
18
+ </g>
19
+ </svg>
@@ -0,0 +1,46 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 150">
2
+ <rect x="0" y="0" width="20" height="20" rx="5" fill="#E2E8F0"></rect>
3
+ <rect x="26" y="7" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
4
+ <rect x="240" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
5
+ <rect x="268" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
6
+ <rect x="296" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
7
+ <g>
8
+ <rect x="0" y="34" width="100" height="96" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
9
+ <rect x="16" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
10
+ <rect x="28" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
11
+ <rect x="40" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
12
+ <rect x="52" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
13
+ <rect x="64" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
14
+ <rect x="10" y="64" width="80" height="5" rx="2" fill="#E2E8F0"></rect>
15
+ <rect x="10" y="76" width="80" height="5" rx="2" fill="#E2E8F0"></rect>
16
+ <rect x="10" y="88" width="50" height="5" rx="2" fill="#E2E8F0"></rect>
17
+ <circle cx="20" cy="114" r="8" fill="#FBE4E4"></circle>
18
+ <rect x="34" y="111" width="40" height="6" rx="3" fill="#CBD5E1"></rect>
19
+ </g>
20
+ <g>
21
+ <rect x="110" y="34" width="100" height="96" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
22
+ <rect x="126" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
23
+ <rect x="138" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
24
+ <rect x="150" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
25
+ <rect x="162" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
26
+ <rect x="174" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
27
+ <rect x="120" y="64" width="80" height="5" rx="2" fill="#E2E8F0"></rect>
28
+ <rect x="120" y="76" width="80" height="5" rx="2" fill="#E2E8F0"></rect>
29
+ <rect x="120" y="88" width="50" height="5" rx="2" fill="#E2E8F0"></rect>
30
+ <circle cx="130" cy="114" r="8" fill="#FDEBDD"></circle>
31
+ <rect x="144" y="111" width="40" height="6" rx="3" fill="#CBD5E1"></rect>
32
+ </g>
33
+ <g>
34
+ <rect x="220" y="34" width="100" height="96" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
35
+ <rect x="236" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
36
+ <rect x="248" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
37
+ <rect x="260" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
38
+ <rect x="272" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
39
+ <rect x="284" y="44" width="8" height="8" rx="2" fill="#F6D97A"></rect>
40
+ <rect x="230" y="64" width="80" height="5" rx="2" fill="#E2E8F0"></rect>
41
+ <rect x="230" y="76" width="80" height="5" rx="2" fill="#E2E8F0"></rect>
42
+ <rect x="230" y="88" width="50" height="5" rx="2" fill="#E2E8F0"></rect>
43
+ <circle cx="240" cy="114" r="8" fill="#E3E8FD"></circle>
44
+ <rect x="254" y="111" width="40" height="6" rx="3" fill="#CBD5E1"></rect>
45
+ </g>
46
+ </svg>
@@ -0,0 +1,22 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 150">
2
+ <rect x="0" y="0" width="20" height="20" rx="5" fill="#E2E8F0"></rect>
3
+ <rect x="26" y="7" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
4
+ <rect x="240" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
5
+ <rect x="268" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
6
+ <rect x="296" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
7
+ <g>
8
+ <rect x="0" y="34" width="24" height="24" rx="6" fill="#FBE4E4"></rect>
9
+ <rect x="34" y="38" width="140" height="7" rx="3" fill="#E2E8F0"></rect>
10
+ <rect x="34" y="50" width="220" height="5" rx="2" fill="#CBD5E1"></rect>
11
+ </g>
12
+ <g>
13
+ <rect x="0" y="70" width="24" height="24" rx="6" fill="#FDEBDD"></rect>
14
+ <rect x="34" y="74" width="140" height="7" rx="3" fill="#E2E8F0"></rect>
15
+ <rect x="34" y="86" width="220" height="5" rx="2" fill="#CBD5E1"></rect>
16
+ </g>
17
+ <g>
18
+ <rect x="0" y="106" width="24" height="24" rx="6" fill="#E3E8FD"></rect>
19
+ <rect x="34" y="110" width="140" height="7" rx="3" fill="#E2E8F0"></rect>
20
+ <rect x="34" y="122" width="220" height="5" rx="2" fill="#CBD5E1"></rect>
21
+ </g>
22
+ </svg>
@@ -0,0 +1,24 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 150">
2
+ <rect x="0" y="0" width="20" height="20" rx="5" fill="#E2E8F0"></rect>
3
+ <rect x="26" y="7" width="40" height="6" rx="3" fill="#E2E8F0"></rect>
4
+ <rect x="240" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
5
+ <rect x="268" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
6
+ <rect x="296" y="7" width="22" height="6" rx="3" fill="#E2E8F0"></rect>
7
+ <rect x="0" y="32" width="150" height="8" rx="4" fill="#E2E8F0"></rect>
8
+ <rect x="0" y="46" width="100" height="6" rx="3" fill="#CBD5E1"></rect>
9
+ <g>
10
+ <rect x="0" y="62" width="100" height="44" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
11
+ <rect x="6" y="68" width="88" height="22" rx="4" fill="#FBE4E4"></rect>
12
+ <rect x="6" y="96" width="20" height="4" rx="2" fill="#E2E8F0"></rect>
13
+ </g>
14
+ <g>
15
+ <rect x="110" y="62" width="100" height="44" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
16
+ <rect x="116" y="68" width="88" height="22" rx="4" fill="#FDEBDD"></rect>
17
+ <rect x="116" y="96" width="20" height="4" rx="2" fill="#E2E8F0"></rect>
18
+ </g>
19
+ <g>
20
+ <rect x="220" y="62" width="100" height="44" rx="6" fill="#FFFFFF" stroke="#E2E8F0"></rect>
21
+ <rect x="226" y="68" width="88" height="22" rx="4" fill="#E3E8FD"></rect>
22
+ <rect x="226" y="96" width="20" height="4" rx="2" fill="#E2E8F0"></rect>
23
+ </g>
24
+ </svg>
@@ -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>
@@ -0,0 +1,92 @@
1
+ <script lang="ts" setup>
2
+ import NexxtIcon, { type NexxtIconName } from '../../atoms/Icon/Icon.vue'
3
+ import NexxtPageMockup, { type NexxtPageMockupType } from '../PageMockup/PageMockup.vue'
4
+
5
+ export interface NexxtPageCardProps {
6
+ /** Page name, shown in the footer. Truncated on one line. */
7
+ title: string
8
+ /** Icon for the page type, shown before the title. */
9
+ icon?: NexxtIconName
10
+ /** Draws the sketch for this page type in the preview area. The `preview` slot wins when both are given. */
11
+ mockup?: NexxtPageMockupType
12
+ /** Lays a full-card button over the content that emits `select`. Off keeps the card inert. */
13
+ selectable?: boolean
14
+ /** Whether this card is the current one: draws the blue border. Only does something with `selectable`. */
15
+ selected?: boolean
16
+ /** The card is being dragged: dims it and draws the blue border, so the list shows where it came from. */
17
+ dragging?: boolean
18
+ }
19
+
20
+ export type NexxtPageCardSlots = {
21
+ /** The preview area above the footer, for a mockup or thumbnail of the page. Empty shows a plain surface. */
22
+ preview?(): unknown
23
+ /** Right side of the footer, for a menu. Kept above the select button so a press there never selects. */
24
+ actions?(): unknown
25
+ }
26
+
27
+ defineOptions({ name: 'NexxtPageCard' })
28
+
29
+ const props = withDefaults(defineProps<NexxtPageCardProps>(), {
30
+ icon: undefined,
31
+ mockup: undefined,
32
+ selectable: false,
33
+ selected: false,
34
+ dragging: false,
35
+ })
36
+
37
+ const emit = defineEmits<{
38
+ /** The card was chosen. No payload: the consumer knows which page it rendered. */
39
+ select: []
40
+ }>()
41
+
42
+ // The heading may not sit inside a button, so like TemplateCard the select button is a stretched
43
+ // sibling under the content, with the actions layered above it
44
+ defineSlots<NexxtPageCardSlots>()
45
+
46
+ const onSelect = () => {
47
+ if (props.selectable) emit('select')
48
+ }
49
+ </script>
50
+
51
+ <template>
52
+ <div
53
+ class="relative flex w-full flex-col overflow-hidden rounded-xl border bg-white transition-[border-color,opacity] duration-200 ease-out"
54
+ :class="[
55
+ (selectable && selected) || dragging ? 'border-cornflower-blue-500' : 'border-gray-200',
56
+ selectable && !selected && !dragging && 'hover:border-gray-300',
57
+ dragging && 'opacity-60',
58
+ ]"
59
+ >
60
+ <div
61
+ class="relative z-10 h-52 overflow-hidden border-b border-gray-200 bg-white p-3"
62
+ :class="selectable && 'pointer-events-none'"
63
+ >
64
+ <slot name="preview">
65
+ <NexxtPageMockup v-if="mockup" :type="mockup" />
66
+ </slot>
67
+ </div>
68
+
69
+ <div class="relative flex items-center gap-2.5 px-4 py-3">
70
+ <div
71
+ class="relative z-10 flex min-w-0 flex-1 items-center gap-2.5"
72
+ :class="selectable && 'pointer-events-none'"
73
+ >
74
+ <NexxtIcon v-if="icon" :name="icon" class="shrink-0 text-lg text-gray-500" />
75
+ <h3 class="truncate body-semibold text-gray-900">{{ title }}</h3>
76
+ </div>
77
+
78
+ <div v-if="$slots.actions" class="relative z-20 flex shrink-0 items-center">
79
+ <slot name="actions" />
80
+ </div>
81
+ </div>
82
+
83
+ <button
84
+ v-if="selectable"
85
+ type="button"
86
+ :aria-label="title"
87
+ :aria-current="selected ? 'true' : undefined"
88
+ class="absolute inset-0 cursor-pointer rounded-xl focus-visible:outline-3 focus-visible:-outline-offset-3 focus-visible:outline-cornflower-blue-500"
89
+ @click="onSelect"
90
+ />
91
+ </div>
92
+ </template>
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ export const PAGE_MOCKUP_TYPES = [
3
+ 'introduction',
4
+ 'about',
5
+ 'valuation',
6
+ 'services',
7
+ 'packages',
8
+ 'references',
9
+ 'reviews',
10
+ 'other',
11
+ ] as const
12
+
13
+ export type NexxtPageMockupType = (typeof PAGE_MOCKUP_TYPES)[number]
14
+ </script>
15
+
16
+ <script lang="ts" setup>
17
+ import { computed, type Component } from 'vue'
18
+ import IntroductionSvg from '@ui-assets/svg/page_mockup_introduction.svg'
19
+ import AboutSvg from '@ui-assets/svg/page_mockup_about.svg'
20
+ import ValuationSvg from '@ui-assets/svg/page_mockup_valuation.svg'
21
+ import ServicesSvg from '@ui-assets/svg/page_mockup_services.svg'
22
+ import PackagesSvg from '@ui-assets/svg/page_mockup_packages.svg'
23
+ import ReferencesSvg from '@ui-assets/svg/page_mockup_references.svg'
24
+ import ReviewsSvg from '@ui-assets/svg/page_mockup_reviews.svg'
25
+ import OtherSvg from '@ui-assets/svg/page_mockup_other.svg'
26
+
27
+ export interface NexxtPageMockupProps {
28
+ /** Which kind of proposal page to sketch. */
29
+ type: NexxtPageMockupType
30
+ }
31
+
32
+ defineOptions({ name: 'NexxtPageMockup' })
33
+
34
+ const props = defineProps<NexxtPageMockupProps>()
35
+
36
+ const SKETCHES: Record<NexxtPageMockupType, Component> = {
37
+ introduction: IntroductionSvg,
38
+ about: AboutSvg,
39
+ valuation: ValuationSvg,
40
+ services: ServicesSvg,
41
+ packages: PackagesSvg,
42
+ references: ReferencesSvg,
43
+ reviews: ReviewsSvg,
44
+ other: OtherSvg,
45
+ }
46
+
47
+ const sketch = computed(() => SKETCHES[props.type])
48
+ </script>
49
+
50
+ <template>
51
+ <component :is="sketch" class="size-full" aria-hidden="true" />
52
+ </template>
@@ -0,0 +1,34 @@
1
+ <script lang="ts" setup>
2
+ import NexxtIcon, { type NexxtIconName } from '../../atoms/Icon/Icon.vue'
3
+
4
+ export interface NexxtPageTypeTileProps {
5
+ /** What the tile stands for, e.g. a page type. Shown under the icon. */
6
+ label: string
7
+ icon: NexxtIconName
8
+ /** The tile is being dragged: draws the blue border. The consumer makes it draggable. */
9
+ dragging?: boolean
10
+ }
11
+
12
+ defineOptions({ name: 'NexxtPageTypeTile' })
13
+
14
+ withDefaults(defineProps<NexxtPageTypeTileProps>(), {
15
+ dragging: false,
16
+ })
17
+
18
+ const emit = defineEmits<{
19
+ /** The tile was clicked, as opposed to dragged. */
20
+ select: []
21
+ }>()
22
+ </script>
23
+
24
+ <template>
25
+ <button
26
+ type="button"
27
+ class="flex w-full cursor-grab flex-col items-center gap-2 rounded-xl border bg-white px-3 py-5 text-center transition-colors duration-200 ease-out hover:bg-gray-50 focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-cornflower-blue-500 active:cursor-grabbing"
28
+ :class="dragging ? 'border-cornflower-blue-500' : 'border-gray-200 hover:border-gray-300'"
29
+ @click="emit('select')"
30
+ >
31
+ <NexxtIcon :name="icon" class="text-xl text-gray-700" />
32
+ <span class="extra-small-semibold text-gray-900">{{ label }}</span>
33
+ </button>
34
+ </template>