@nexxtmove/ui 1.38.0 → 1.40.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 (27) hide show
  1. package/dist/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue.d.ts +18 -3
  2. package/dist/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue.d.ts +15 -3
  3. package/dist/components/editor/molecules/EditorBorder/EditorBorder.vue.d.ts +11 -6
  4. package/dist/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue.d.ts +13 -3
  5. package/dist/components/editor/molecules/EditorGap/EditorGap.vue.d.ts +55 -6
  6. package/dist/components/editor/molecules/EditorHeadingType/EditorHeadingType.vue.d.ts +16 -12
  7. package/dist/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue.d.ts +18 -3
  8. package/dist/components/editor/molecules/EditorPadding/EditorPadding.vue.d.ts +106 -0
  9. package/dist/components/editor/molecules/EditorTextAlign/EditorTextAlign.vue.d.ts +18 -3
  10. package/dist/components/organisms/ComboboxPanel/_ComboboxPanelOptions.vue.d.ts +24 -0
  11. package/dist/components/organisms/ComboboxPanel/combobox-types.d.ts +17 -0
  12. package/dist/index.d.ts +11 -1
  13. package/dist/index.js +5079 -4894
  14. package/package.json +1 -1
  15. package/src/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue +35 -11
  16. package/src/components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue +33 -6
  17. package/src/components/editor/molecules/EditorBorder/EditorBorder.vue +19 -12
  18. package/src/components/editor/molecules/EditorFlexDirection/EditorFlexDirection.vue +40 -5
  19. package/src/components/editor/molecules/EditorGap/EditorGap.vue +30 -15
  20. package/src/components/editor/molecules/EditorHeadingType/EditorHeadingType.vue +19 -15
  21. package/src/components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue +35 -15
  22. package/src/components/editor/molecules/EditorPadding/EditorPadding.vue +61 -19
  23. package/src/components/editor/molecules/EditorTextAlign/EditorTextAlign.vue +35 -7
  24. package/src/components/organisms/ComboboxPanel/ComboboxPanel.vue +58 -46
  25. package/src/components/organisms/ComboboxPanel/_ComboboxPanelOptions.vue +96 -0
  26. package/src/components/organisms/ComboboxPanel/combobox-types.ts +19 -0
  27. package/src/index.ts +14 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "1.38.0",
4
+ "version": "1.40.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -1,9 +1,22 @@
1
1
  <script lang="ts">
2
+ /**
3
+ * The options the editor offers, in the order they are shown. `token` is the
4
+ * class without its fixed `items-` prefix, so the class is `items-` plus the
5
+ * token. The labels are not in here; they arrive through `messages`, keyed by
6
+ * token.
7
+ */
8
+ export const EDITOR_ALIGN_ITEMS_OPTIONS = [
9
+ { token: 'start' },
10
+ { token: 'center' },
11
+ { token: 'end' },
12
+ { token: 'stretch' },
13
+ ] as const
14
+
2
15
  /**
3
16
  * The cross-axis alignment as the editor needs it: the Tailwind class itself,
4
17
  * so the consumer can put the model value straight on the element it styles.
5
18
  */
6
- export type EditorAlignItemsValue = 'items-start' | 'items-center' | 'items-end' | 'items-stretch'
19
+ export type EditorAlignItemsValue = `items-${(typeof EDITOR_ALIGN_ITEMS_OPTIONS)[number]['token']}`
7
20
 
8
21
  /** Overridable copy for the four options. Every key is optional. */
9
22
  export interface EditorAlignItemsMessages {
@@ -64,16 +77,27 @@ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
64
77
  // the spacing icons depict gaps between separate objects, which is the
65
78
  // justify-between metaphor, not this one. The copy travels as `ariaLabel`, so
66
79
  // the buttons still have a name for assistive technology.
67
- const options = computed<NexxtToggleGroupOption[]>(() => [
68
- { value: 'items-start', icon: 'objects-align-top', ariaLabel: copy.value.start },
69
- {
70
- value: 'items-center',
71
- icon: 'objects-align-center-vertical',
72
- ariaLabel: copy.value.center,
73
- },
74
- { value: 'items-end', icon: 'objects-align-bottom', ariaLabel: copy.value.end },
75
- { value: 'items-stretch', icon: 'arrows-up-down', ariaLabel: copy.value.stretch },
76
- ])
80
+ /**
81
+ * The icon of each option. Keyed on the token, so a token added to the list
82
+ * without an icon is a type error.
83
+ */
84
+ const ICONS: Record<
85
+ (typeof EDITOR_ALIGN_ITEMS_OPTIONS)[number]['token'],
86
+ NonNullable<NexxtToggleGroupOption['icon']>
87
+ > = {
88
+ start: 'objects-align-top',
89
+ center: 'objects-align-center-vertical',
90
+ end: 'objects-align-bottom',
91
+ stretch: 'arrows-up-down',
92
+ }
93
+
94
+ const options = computed<NexxtToggleGroupOption[]>(() =>
95
+ EDITOR_ALIGN_ITEMS_OPTIONS.map(({ token }) => ({
96
+ value: `items-${token}`,
97
+ icon: ICONS[token],
98
+ ariaLabel: copy.value[token],
99
+ })),
100
+ )
77
101
 
78
102
  // ToggleGroup types its model as the open `string` of its options, so writing
79
103
  // straight back into the narrower model is a type error. The setter is the one
@@ -1,9 +1,21 @@
1
1
  <script lang="ts">
2
+ /**
3
+ * The options the editor offers, in the order they are shown. `token` is the
4
+ * class without its fixed `bg-` prefix, so the class is `bg-` plus the token.
5
+ * The labels are not in here; they arrive through `messages`, keyed by token.
6
+ */
7
+ export const EDITOR_BACKGROUND_SIZE_OPTIONS = [
8
+ { token: 'cover' },
9
+ { token: 'contain' },
10
+ { token: 'auto' },
11
+ ] as const
12
+
2
13
  /**
3
14
  * The background size as the editor needs it: the Tailwind class itself, so the
4
15
  * consumer can put the model value straight on the element it styles.
5
16
  */
6
- export type EditorBackgroundSizeValue = 'bg-cover' | 'bg-contain' | 'bg-auto'
17
+ export type EditorBackgroundSizeValue =
18
+ `bg-${(typeof EDITOR_BACKGROUND_SIZE_OPTIONS)[number]['token']}`
7
19
 
8
20
  /** Overridable copy for the three options. Every key is optional. */
9
21
  export interface EditorBackgroundSizeMessages {
@@ -58,11 +70,26 @@ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
58
70
  // options do not fit the width the editor sidebar gives this block. The copy
59
71
  // travels as `ariaLabel`, so the buttons still have a name for assistive
60
72
  // technology.
61
- const options = computed<NexxtToggleGroupOption[]>(() => [
62
- { value: 'bg-cover', icon: 'arrows-maximize', ariaLabel: copy.value.cover },
63
- { value: 'bg-contain', icon: 'arrows-minimize', ariaLabel: copy.value.contain },
64
- { value: 'bg-auto', icon: 'image', ariaLabel: copy.value.auto },
65
- ])
73
+ /**
74
+ * The icon of each option. Keyed on the token, so a token added to the list
75
+ * without an icon is a type error.
76
+ */
77
+ const ICONS: Record<
78
+ (typeof EDITOR_BACKGROUND_SIZE_OPTIONS)[number]['token'],
79
+ NonNullable<NexxtToggleGroupOption['icon']>
80
+ > = {
81
+ cover: 'arrows-maximize',
82
+ contain: 'arrows-minimize',
83
+ auto: 'image',
84
+ }
85
+
86
+ const options = computed<NexxtToggleGroupOption[]>(() =>
87
+ EDITOR_BACKGROUND_SIZE_OPTIONS.map(({ token }) => ({
88
+ value: `bg-${token}`,
89
+ icon: ICONS[token],
90
+ ariaLabel: copy.value[token],
91
+ })),
92
+ )
66
93
 
67
94
  // ToggleGroup types its model as the open `string` of its options, so writing
68
95
  // straight back into the narrower model is a type error. The setter is the one
@@ -5,16 +5,21 @@
5
5
  * rather than theme tokens -- the theme defines no border-width scale, so
6
6
  * stepping over arbitrary integers would produce classes with no CSS behind
7
7
  * them.
8
+ *
9
+ * `token` is the full width class, not a suffix: the 1px step is the bare
10
+ * `border`, which has none. The model value pairs it with an optional colour
11
+ * class; a value without a width class on the list puts the slider on the first
12
+ * step.
8
13
  */
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
+ export const EDITOR_BORDER_STEPS = [
15
+ { token: 'border', px: 1 },
16
+ { token: 'border-2', px: 2 },
17
+ { token: 'border-4', px: 4 },
18
+ { token: 'border-8', px: 8 },
14
19
  ] as const
15
20
 
16
21
  /** The width part of the model value, as the Tailwind class itself. */
17
- export type EditorBorderWidth = (typeof BORDER_STEPS)[number]['class']
22
+ export type EditorBorderWidth = (typeof EDITOR_BORDER_STEPS)[number]['token']
18
23
 
19
24
  /** Overridable copy of the readout and the picker. Every key is optional. */
20
25
  export interface EditorBorderMessages {
@@ -94,7 +99,7 @@ const enabled = defineModel<boolean>('enabled', { default: true })
94
99
 
95
100
  const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
96
101
 
97
- const maxIndex = BORDER_STEPS.length - 1
102
+ const maxIndex = EDITOR_BORDER_STEPS.length - 1
98
103
 
99
104
  /** The model read as a token list; whitespace of any shape separates them. */
100
105
  const tokens = computed(() => model.value.split(/\s+/).filter(Boolean))
@@ -107,11 +112,13 @@ const tokens = computed(() => model.value.split(/\s+/).filter(Boolean))
107
112
  * first change writes a valid class back.
108
113
  */
109
114
  const stepIndex = computed(() => {
110
- const widthToken = tokens.value.find((token) => BORDER_STEPS.some((step) => step.class === token))
115
+ const widthToken = tokens.value.find((token) =>
116
+ EDITOR_BORDER_STEPS.some((step) => step.token === token),
117
+ )
111
118
 
112
119
  return Math.max(
113
120
  0,
114
- BORDER_STEPS.findIndex((step) => step.class === widthToken),
121
+ EDITOR_BORDER_STEPS.findIndex((step) => step.token === widthToken),
115
122
  )
116
123
  })
117
124
 
@@ -143,7 +150,7 @@ const write = (width: EditorBorderWidth, color: string) => {
143
150
  const sliderValue = computed({
144
151
  get: () => [stepIndex.value],
145
152
  set: ([index]) => {
146
- write(BORDER_STEPS[index].class, hex.value)
153
+ write(EDITOR_BORDER_STEPS[index].token, hex.value)
147
154
  },
148
155
  })
149
156
 
@@ -153,11 +160,11 @@ const picked = computed({
153
160
  // would still wipe a perfectly good colour, so it is dropped rather than
154
161
  // written through. The picker keeps rendering the model's colour.
155
162
  set: (value: string) => {
156
- if (isHex(value)) write(BORDER_STEPS[stepIndex.value].class, value)
163
+ if (isHex(value)) write(EDITOR_BORDER_STEPS[stepIndex.value].token, value)
157
164
  },
158
165
  })
159
166
 
160
- const displayValue = computed(() => `${BORDER_STEPS[stepIndex.value].px} ${copy.value.unit}`)
167
+ const displayValue = computed(() => `${EDITOR_BORDER_STEPS[stepIndex.value].px} ${copy.value.unit}`)
161
168
  </script>
162
169
 
163
170
  <template>
@@ -1,9 +1,17 @@
1
1
  <script lang="ts">
2
+ /**
3
+ * The options the editor offers, in the order they are shown. `token` is the
4
+ * class without its fixed `flex-` prefix, so the class is `flex-` plus the
5
+ * token. The labels are not in here; they arrive through `messages`.
6
+ */
7
+ export const EDITOR_FLEX_DIRECTION_OPTIONS = [{ token: 'row' }, { token: 'col' }] as const
8
+
2
9
  /**
3
10
  * The flex direction as the editor needs it: the Tailwind class itself, so the
4
11
  * consumer can put the model value straight on the element it styles.
5
12
  */
6
- export type EditorFlexDirectionValue = 'flex-row' | 'flex-col'
13
+ export type EditorFlexDirectionValue =
14
+ `flex-${(typeof EDITOR_FLEX_DIRECTION_OPTIONS)[number]['token']}`
7
15
 
8
16
  /** Overridable copy for the two options. Every key is optional. */
9
17
  export interface EditorFlexDirectionMessages {
@@ -49,10 +57,37 @@ const model = defineModel<EditorFlexDirectionValue>({ required: true })
49
57
 
50
58
  const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
51
59
 
52
- const options = computed<NexxtToggleGroupOption[]>(() => [
53
- { value: 'flex-row', label: copy.value.horizontal, icon: 'arrow-right' },
54
- { value: 'flex-col', label: copy.value.vertical, icon: 'arrow-down' },
55
- ])
60
+ /**
61
+ * The icon of each option. Keyed on the token, so a token added to the list
62
+ * without an icon is a type error.
63
+ */
64
+ const ICONS: Record<
65
+ (typeof EDITOR_FLEX_DIRECTION_OPTIONS)[number]['token'],
66
+ NonNullable<NexxtToggleGroupOption['icon']>
67
+ > = {
68
+ row: 'arrow-right',
69
+ col: 'arrow-down',
70
+ }
71
+
72
+ /**
73
+ * The messages key of each option: the copy names the direction, not the
74
+ * class.
75
+ */
76
+ const MESSAGE_KEYS: Record<
77
+ (typeof EDITOR_FLEX_DIRECTION_OPTIONS)[number]['token'],
78
+ keyof EditorFlexDirectionMessages
79
+ > = {
80
+ row: 'horizontal',
81
+ col: 'vertical',
82
+ }
83
+
84
+ const options = computed<NexxtToggleGroupOption[]>(() =>
85
+ EDITOR_FLEX_DIRECTION_OPTIONS.map(({ token }) => ({
86
+ value: `flex-${token}`,
87
+ icon: ICONS[token],
88
+ label: copy.value[MESSAGE_KEYS[token]],
89
+ })),
90
+ )
56
91
 
57
92
  // ToggleGroup types its model as the open `string` of its options, so writing
58
93
  // straight back into the narrower model is a type error. The setter is the one
@@ -1,17 +1,37 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * The steps the slider can land on, as the numeric part of the Tailwind class.
4
- * 13 and 15 are missing on purpose: the theme defines `--spacing-0` through
5
- * `--spacing-12` and then only 14 and 16, so stepping over the px value instead
6
- * of over this list would offer gaps that have no token behind them.
3
+ * The steps the slider can land on, with the px they render (the spacing scale
4
+ * is built on 0.25rem, so a step is 4 px). 13 and 15 are missing on purpose:
5
+ * the theme defines `--spacing-0` through `--spacing-12` and then only 14 and
6
+ * 16, so stepping over the px value instead of over this list would offer gaps
7
+ * that have no token behind them.
8
+ *
9
+ * `token` is the numeric part of the class; the class is `gap-` plus the token.
10
+ * A stored value that is not on the list puts the slider on the first step.
7
11
  */
8
- const GAP_STEPS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16] as const
12
+ export const EDITOR_GAP_STEPS = [
13
+ { token: 0, px: 0 },
14
+ { token: 1, px: 4 },
15
+ { token: 2, px: 8 },
16
+ { token: 3, px: 12 },
17
+ { token: 4, px: 16 },
18
+ { token: 5, px: 20 },
19
+ { token: 6, px: 24 },
20
+ { token: 7, px: 28 },
21
+ { token: 8, px: 32 },
22
+ { token: 9, px: 36 },
23
+ { token: 10, px: 40 },
24
+ { token: 11, px: 44 },
25
+ { token: 12, px: 48 },
26
+ { token: 14, px: 56 },
27
+ { token: 16, px: 64 },
28
+ ] as const
9
29
 
10
30
  /**
11
31
  * The gap as the editor needs it: the Tailwind class itself, so the consumer
12
32
  * can put the model value straight on the element it styles.
13
33
  */
14
- export type EditorGapValue = `gap-${(typeof GAP_STEPS)[number]}`
34
+ export type EditorGapValue = `gap-${(typeof EDITOR_GAP_STEPS)[number]['token']}`
15
35
 
16
36
  /** Overridable copy of the value readout. Every key is optional. */
17
37
  export interface EditorGapMessages {
@@ -36,9 +56,6 @@ export interface NexxtEditorGapProps {
36
56
  const DEFAULT_MESSAGES = {
37
57
  unit: 'px',
38
58
  } satisfies Required<EditorGapMessages>
39
-
40
- /** The spacing scale is built on 0.25rem, so `gap-4` is 4 × 4 = 16 px. */
41
- const PX_PER_STEP = 4
42
59
  </script>
43
60
 
44
61
  <script lang="ts" setup>
@@ -55,7 +72,7 @@ const model = defineModel<EditorGapValue>({ required: true })
55
72
 
56
73
  const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
57
74
 
58
- const maxIndex = GAP_STEPS.length - 1
75
+ const maxIndex = EDITOR_GAP_STEPS.length - 1
59
76
 
60
77
  /**
61
78
  * Position of the current value on the step list. A value that is not on the
@@ -66,7 +83,7 @@ const maxIndex = GAP_STEPS.length - 1
66
83
  const stepIndex = computed(() =>
67
84
  Math.max(
68
85
  0,
69
- GAP_STEPS.findIndex((step) => `gap-${step}` === model.value),
86
+ EDITOR_GAP_STEPS.findIndex(({ token }) => `gap-${token}` === model.value),
70
87
  ),
71
88
  )
72
89
 
@@ -75,13 +92,11 @@ const stepIndex = computed(() =>
75
92
  const sliderValue = computed({
76
93
  get: () => [stepIndex.value],
77
94
  set: ([index]) => {
78
- model.value = `gap-${GAP_STEPS[index]}`
95
+ model.value = `gap-${EDITOR_GAP_STEPS[index].token}`
79
96
  },
80
97
  })
81
98
 
82
- const displayValue = computed(
83
- () => `${GAP_STEPS[stepIndex.value] * PX_PER_STEP} ${copy.value.unit}`,
84
- )
99
+ const displayValue = computed(() => `${EDITOR_GAP_STEPS[stepIndex.value].px} ${copy.value.unit}`)
85
100
  </script>
86
101
 
87
102
  <template>
@@ -1,18 +1,22 @@
1
1
  <script lang="ts">
2
2
  /**
3
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.
4
+ * is what the option shows -- its label and its `h1`..`h6` icon -- and the
5
+ * token is what the model carries. The sizes are Tailwind's default scale on
6
+ * purpose, not the theme's `heading-*` utilities: this value is data the editor
7
+ * stores and puts back on an element, so it has to be a class every consumer
8
+ * resolves.
9
+ *
10
+ * `token` is the full font-size class, not a suffix: it is stored as is. The
11
+ * labels are not in here; they arrive through `messages`, keyed by level.
8
12
  */
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' },
13
+ export const EDITOR_HEADING_TYPE_OPTIONS = [
14
+ { token: 'text-4xl', level: 1 },
15
+ { token: 'text-3xl', level: 2 },
16
+ { token: 'text-2xl', level: 3 },
17
+ { token: 'text-xl', level: 4 },
18
+ { token: 'text-lg', level: 5 },
19
+ { token: 'text-base', level: 6 },
16
20
  ] as const
17
21
 
18
22
  /**
@@ -20,7 +24,7 @@ const HEADING_LEVELS = [
20
24
  * the consumer can put the model value straight on the element it styles.
21
25
  * Weight is deliberately not part of it -- that becomes a setting of its own.
22
26
  */
23
- export type EditorHeadingTypeValue = (typeof HEADING_LEVELS)[number]['size']
27
+ export type EditorHeadingTypeValue = (typeof EDITOR_HEADING_TYPE_OPTIONS)[number]['token']
24
28
 
25
29
  /** Overridable copy for the six options. Every key is optional. */
26
30
  export interface EditorHeadingTypeMessages {
@@ -82,13 +86,13 @@ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
82
86
  // The level doubles as the messages key and the Font Awesome icon name --
83
87
  // `h1` through `h6` all exist in the kit.
84
88
  const options = computed<SelectOption[]>(() =>
85
- HEADING_LEVELS.map(({ level, size }) => {
89
+ EDITOR_HEADING_TYPE_OPTIONS.map(({ token, level }) => {
86
90
  const key = `h${level}` as const
87
- return { value: size, label: copy.value[key], icon: key }
91
+ return { value: token, label: copy.value[key], icon: key }
88
92
  }),
89
93
  )
90
94
 
91
- const values = HEADING_LEVELS.map(({ size }) => size)
95
+ const values = EDITOR_HEADING_TYPE_OPTIONS.map(({ token }) => token)
92
96
 
93
97
  // Select types its model as the open `string | number | null` of its options,
94
98
  // so writing straight back into the narrower model is a type error. The setter
@@ -1,10 +1,23 @@
1
1
  <script lang="ts">
2
+ /**
3
+ * The options the editor offers, in the order they are shown. `token` is the
4
+ * class without its fixed `justify-` prefix, so the class is `justify-` plus
5
+ * the token. The labels are not in here; they arrive through `messages`, keyed
6
+ * by token.
7
+ */
8
+ export const EDITOR_JUSTIFY_CONTENT_OPTIONS = [
9
+ { token: 'start' },
10
+ { token: 'center' },
11
+ { token: 'end' },
12
+ { token: 'between' },
13
+ ] as const
14
+
2
15
  /**
3
16
  * The justification as the editor needs it: the Tailwind class itself, so the
4
17
  * consumer can put the model value straight on the element it styles.
5
18
  */
6
19
  export type EditorJustifyContentValue =
7
- 'justify-start' | 'justify-center' | 'justify-end' | 'justify-between'
20
+ `justify-${(typeof EDITOR_JUSTIFY_CONTENT_OPTIONS)[number]['token']}`
8
21
 
9
22
  /** Overridable copy for the four options. Every key is optional. */
10
23
  export interface EditorJustifyContentMessages {
@@ -61,20 +74,27 @@ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
61
74
  // side at the width the editor sidebar gives this block, and the icons are
62
75
  // conventional enough to stand on their own. The copy travels as `ariaLabel`,
63
76
  // so the buttons still have a name for assistive technology.
64
- const options = computed<NexxtToggleGroupOption[]>(() => [
65
- { value: 'justify-start', icon: 'objects-align-left', ariaLabel: copy.value.start },
66
- {
67
- value: 'justify-center',
68
- icon: 'objects-align-center-horizontal',
69
- ariaLabel: copy.value.center,
70
- },
71
- { value: 'justify-end', icon: 'objects-align-right', ariaLabel: copy.value.end },
72
- {
73
- value: 'justify-between',
74
- icon: 'distribute-spacing-horizontal',
75
- ariaLabel: copy.value.between,
76
- },
77
- ])
77
+ /**
78
+ * The icon of each option. Keyed on the token, so a token added to the list
79
+ * without an icon is a type error.
80
+ */
81
+ const ICONS: Record<
82
+ (typeof EDITOR_JUSTIFY_CONTENT_OPTIONS)[number]['token'],
83
+ NonNullable<NexxtToggleGroupOption['icon']>
84
+ > = {
85
+ start: 'objects-align-left',
86
+ center: 'objects-align-center-horizontal',
87
+ end: 'objects-align-right',
88
+ between: 'distribute-spacing-horizontal',
89
+ }
90
+
91
+ const options = computed<NexxtToggleGroupOption[]>(() =>
92
+ EDITOR_JUSTIFY_CONTENT_OPTIONS.map(({ token }) => ({
93
+ value: `justify-${token}`,
94
+ icon: ICONS[token],
95
+ ariaLabel: copy.value[token],
96
+ })),
97
+ )
78
98
 
79
99
  // ToggleGroup types its model as the open `string` of its options, so writing
80
100
  // straight back into the narrower model is a type error. The setter is the one
@@ -2,22 +2,56 @@
2
2
  import type { PaddingIconSide } from './_PaddingIcon.vue'
3
3
 
4
4
  /**
5
- * The steps a field can land on, as the numeric part of the Tailwind class:
6
- * every integer `--spacing-*` token the theme defines. The list thins out as it
7
- * climbs -- 13 and 15 are absent, and past 16 it moves in fours and then
8
- * eights -- so snapping over this list rather than over the px value keeps the
9
- * field off classes that have no token behind them.
5
+ * The steps a field can land on: every integer `--spacing-*` token the theme
6
+ * defines, with the px it renders (the spacing scale is built on 0.25rem, so a
7
+ * step is 4 px). The list thins out as it climbs -- 13 and 15 are absent, and
8
+ * past 16 it moves in fours and then eights -- so snapping over this list
9
+ * rather than over the px value keeps the field off classes that have no token
10
+ * behind them.
11
+ *
12
+ * `token` is the numeric part of the class, without a prefix: one padding value
13
+ * is written with several of them (`p-`, `px-`, `py-`, `pt-`, `pr-`, `pb-`,
14
+ * `pl-`), so the full class depends on which sides it covers. A value between
15
+ * two steps snaps to the nearest one, the lower on a tie.
10
16
  */
11
- const PADDING_STEPS = [
12
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64,
13
- 72, 80, 96,
17
+ export const EDITOR_PADDING_STEPS = [
18
+ { token: 0, px: 0 },
19
+ { token: 1, px: 4 },
20
+ { token: 2, px: 8 },
21
+ { token: 3, px: 12 },
22
+ { token: 4, px: 16 },
23
+ { token: 5, px: 20 },
24
+ { token: 6, px: 24 },
25
+ { token: 7, px: 28 },
26
+ { token: 8, px: 32 },
27
+ { token: 9, px: 36 },
28
+ { token: 10, px: 40 },
29
+ { token: 11, px: 44 },
30
+ { token: 12, px: 48 },
31
+ { token: 14, px: 56 },
32
+ { token: 16, px: 64 },
33
+ { token: 20, px: 80 },
34
+ { token: 24, px: 96 },
35
+ { token: 28, px: 112 },
36
+ { token: 32, px: 128 },
37
+ { token: 36, px: 144 },
38
+ { token: 40, px: 160 },
39
+ { token: 44, px: 176 },
40
+ { token: 48, px: 192 },
41
+ { token: 52, px: 208 },
42
+ { token: 56, px: 224 },
43
+ { token: 60, px: 240 },
44
+ { token: 64, px: 256 },
45
+ { token: 72, px: 288 },
46
+ { token: 80, px: 320 },
47
+ { token: 96, px: 384 },
14
48
  ] as const
15
49
 
16
- /** The spacing scale is built on 0.25rem, so `p-4` is 4 × 4 = 16 px. */
17
- const PX_PER_STEP = 4
50
+ /** A padding step as the numeric part of the Tailwind class, as in `p-4`. */
51
+ export type EditorPaddingStep = (typeof EDITOR_PADDING_STEPS)[number]['token']
18
52
 
19
53
  /** Highest value a field takes, in px: the top step is `p-96`, which is 384 px. */
20
- const MAX_PX = PADDING_STEPS[PADDING_STEPS.length - 1] * PX_PER_STEP
54
+ const MAX_PX = EDITOR_PADDING_STEPS[EDITOR_PADDING_STEPS.length - 1].px
21
55
 
22
56
  const ALL_SIDES: PaddingIconSide[] = ['top', 'right', 'bottom', 'left']
23
57
 
@@ -103,7 +137,7 @@ const parse = (value: string): PaddingSides => {
103
137
  if (!match) continue
104
138
 
105
139
  const step = Number(match[2])
106
- if (!PADDING_STEPS.includes(step as (typeof PADDING_STEPS)[number])) continue
140
+ if (!EDITOR_PADDING_STEPS.some(({ token }) => token === step)) continue
107
141
 
108
142
  for (const side of PREFIX_SIDES[match[1]]) sides[side] = step
109
143
  }
@@ -126,10 +160,18 @@ const serialize = ({ top, right, bottom, left }: PaddingSides): string => {
126
160
  const snap = (px: number): number => {
127
161
  const clamped = Math.min(Math.max(px, 0), MAX_PX)
128
162
 
129
- return PADDING_STEPS.reduce((best, step) =>
130
- Math.abs(step * PX_PER_STEP - clamped) < Math.abs(best * PX_PER_STEP - clamped) ? step : best,
131
- )
163
+ return EDITOR_PADDING_STEPS.reduce((best, step) =>
164
+ Math.abs(step.px - clamped) < Math.abs(best.px - clamped) ? step : best,
165
+ ).token
132
166
  }
167
+
168
+ /**
169
+ * The px a step renders. Every side only ever holds a token from the list --
170
+ * `parse` skips the rest and every write goes through `snap` or the list -- so
171
+ * the lookup always hits; the 0 is there for the type, not for a real case.
172
+ */
173
+ const pxOf = (step: number): number =>
174
+ EDITOR_PADDING_STEPS.find(({ token }) => token === step)?.px ?? 0
133
175
  </script>
134
176
 
135
177
  <script lang="ts" setup>
@@ -215,7 +257,7 @@ const secondaryInert = computed(() => (expanded.value ? undefined : true))
215
257
  // The fields speak px, not steps: `p-6` reads as 24. A 0 is shown as an empty
216
258
  // field so the placeholder carries it, and an empty field commits back as a 0.
217
259
  const display = (side: PaddingIconSide) =>
218
- sides.value[side] === 0 ? '' : String(sides.value[side] * PX_PER_STEP)
260
+ sides.value[side] === 0 ? '' : String(pxOf(sides.value[side]))
219
261
 
220
262
  const apply = (next: PaddingSides) => {
221
263
  // A change that changes nothing -- committing 30 where the value is already
@@ -239,11 +281,11 @@ const onCommit = (field: PaddingField, px: number) => write(field, snap(px))
239
281
 
240
282
  /** One step along the list, so the arrow keys never stop on a missing token. */
241
283
  const onStep = (field: PaddingField, direction: 1 | -1) => {
242
- const index = PADDING_STEPS.indexOf(sides.value[field.source] as (typeof PADDING_STEPS)[number])
284
+ const index = EDITOR_PADDING_STEPS.findIndex(({ token }) => token === sides.value[field.source])
243
285
  // No wrapping: the ends of the list are the ends of the range.
244
- const next = Math.min(Math.max(index + direction, 0), PADDING_STEPS.length - 1)
286
+ const next = Math.min(Math.max(index + direction, 0), EDITOR_PADDING_STEPS.length - 1)
245
287
 
246
- write(field, PADDING_STEPS[next])
288
+ write(field, EDITOR_PADDING_STEPS[next].token)
247
289
  }
248
290
 
249
291
  /**
@@ -1,9 +1,22 @@
1
1
  <script lang="ts">
2
+ /**
3
+ * The options the editor offers, in the order they are shown. `token` is the
4
+ * class without its fixed `text-` prefix, so the class is `text-` plus the
5
+ * token. The labels are not in here; they arrive through `messages`, keyed by
6
+ * token.
7
+ */
8
+ export const EDITOR_TEXT_ALIGN_OPTIONS = [
9
+ { token: 'left' },
10
+ { token: 'center' },
11
+ { token: 'right' },
12
+ { token: 'justify' },
13
+ ] as const
14
+
2
15
  /**
3
16
  * The text alignment as the editor needs it: the Tailwind class itself, so the
4
17
  * consumer can put the model value straight on the element it styles.
5
18
  */
6
- export type EditorTextAlignValue = 'text-left' | 'text-center' | 'text-right' | 'text-justify'
19
+ export type EditorTextAlignValue = `text-${(typeof EDITOR_TEXT_ALIGN_OPTIONS)[number]['token']}`
7
20
 
8
21
  /** Overridable copy for the four options. Every key is optional. */
9
22
  export interface EditorTextAlignMessages {
@@ -60,12 +73,27 @@ const copy = computed(() => ({ ...DEFAULT_MESSAGES, ...messages }))
60
73
  // side at the width the editor sidebar gives this block, and the icons are
61
74
  // conventional enough to stand on their own. The copy travels as `ariaLabel`,
62
75
  // so the buttons still have a name for assistive technology.
63
- const options = computed<NexxtToggleGroupOption[]>(() => [
64
- { value: 'text-left', icon: 'align-left', ariaLabel: copy.value.left },
65
- { value: 'text-center', icon: 'align-center', ariaLabel: copy.value.center },
66
- { value: 'text-right', icon: 'align-right', ariaLabel: copy.value.right },
67
- { value: 'text-justify', icon: 'align-justify', ariaLabel: copy.value.justify },
68
- ])
76
+ /**
77
+ * The icon of each option. Keyed on the token, so a token added to the list
78
+ * without an icon is a type error.
79
+ */
80
+ const ICONS: Record<
81
+ (typeof EDITOR_TEXT_ALIGN_OPTIONS)[number]['token'],
82
+ NonNullable<NexxtToggleGroupOption['icon']>
83
+ > = {
84
+ left: 'align-left',
85
+ center: 'align-center',
86
+ right: 'align-right',
87
+ justify: 'align-justify',
88
+ }
89
+
90
+ const options = computed<NexxtToggleGroupOption[]>(() =>
91
+ EDITOR_TEXT_ALIGN_OPTIONS.map(({ token }) => ({
92
+ value: `text-${token}`,
93
+ icon: ICONS[token],
94
+ ariaLabel: copy.value[token],
95
+ })),
96
+ )
69
97
 
70
98
  // ToggleGroup types its model as the open `string` of its options, so writing
71
99
  // straight back into the narrower model is a type error. The setter is the one