@nexxtmove/ui 1.52.0 → 1.54.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
@@ -80,6 +80,7 @@ var s = {
80
80
  NexxtEditorSettingsBlock: "components/editor/atoms/EditorSettingsBlock/EditorSettingsBlock.vue",
81
81
  NexxtEditorSettingsGroup: "components/editor/atoms/EditorSettingsGroup/EditorSettingsGroup.vue",
82
82
  NexxtEditorAlignItems: "components/editor/molecules/EditorAlignItems/EditorAlignItems.vue",
83
+ NexxtEditorAlignmentGrid: "components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.vue",
83
84
  NexxtEditorAnchor: "components/editor/molecules/EditorAnchor/EditorAnchor.vue",
84
85
  NexxtEditorBackgroundSize: "components/editor/molecules/EditorBackgroundSize/EditorBackgroundSize.vue",
85
86
  NexxtEditorBorder: "components/editor/molecules/EditorBorder/EditorBorder.vue",
package/dist/ui.css CHANGED
@@ -1,2 +1,3 @@
1
- input[data-snap][data-v-5d3904ac]{animation:snap-in-5d3904ac .18s var(--ease-out-strong)}input[data-snap=up][data-v-5d3904ac]{--snap-from:2px}input[data-snap=down][data-v-5d3904ac]{--snap-from:-2px}@keyframes snap-in-5d3904ac{0%{opacity:0;transform:translateY(var(--snap-from))}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){input[data-snap][data-v-5d3904ac]{animation:1ms snap-hold-5d3904ac}@keyframes snap-hold-5d3904ac{0%,to{opacity:1}}}input[data-snap][data-v-15feed8b]{animation:snap-in-15feed8b .18s var(--ease-out-strong)}input[data-snap=up][data-v-15feed8b]{--snap-from:2px}input[data-snap=down][data-v-15feed8b]{--snap-from:-2px}@keyframes snap-in-15feed8b{0%{opacity:0;transform:translateY(var(--snap-from))}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){input[data-snap][data-v-15feed8b]{animation:1ms snap-hold-15feed8b}@keyframes snap-hold-15feed8b{0%,to{opacity:1}}}rect[data-v-0a5d9b21]{animation-duration:4s;animation-timing-function:cubic-bezier(.77,0,.175,1);animation-iteration-count:infinite}rect[data-v-0a5d9b21]:first-child{animation-name:load-1-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(2){animation-name:load-2-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(3){animation-name:load-3-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(4){animation-name:load-4-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(5){animation-name:load-5-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(6){animation-name:load-6-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(7){animation-name:load-7-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(8){animation-name:load-8-0a5d9b21}@media (prefers-reduced-motion:reduce){rect[data-v-0a5d9b21]{animation:none}svg[data-v-0a5d9b21]{animation:2.5s ease-in-out infinite pulse-0a5d9b21}}@keyframes pulse-0a5d9b21{0%,to{opacity:1}50%{opacity:.65}}@keyframes load-1-0a5d9b21{0%,80%{transform:translate(0)}5%,20%,60%,75%{transform:translate(-25px,25px)}30%,50%{transform:translate(-75px,75px)}}@keyframes load-2-0a5d9b21{5%,75%{transform:translate(0%)}10%,20%,60%,70%{transform:translate(50px)}30%,50%{transform:translateY(50px)}}@keyframes load-3-0a5d9b21{10%,70%{transform:translate(0%)}15%,65%{transform:translateY(-50px)}20%,60%{fill:var(--color-sapphire-500);transform:translateY(-50px)}30%,50%{fill:var(--color-purple-500);transform:translate(-50px)}}@keyframes load-4-0a5d9b21{20%,60%{transform:translate(0%)}30%,50%{transform:translate(50px,-50px)}}@keyframes load-5-0a5d9b21{0%,80%{transform:translate(0%)}5%,20%,60%,75%{transform:translateY(50px)}30%,50%{transform:translate(50px)}}@keyframes load-6-0a5d9b21{5%,75%{transform:translate(0%)}10%,20%,60%,70%{transform:translate(-50px)}30%,50%{transform:translateY(-50px)}}@keyframes load-7-0a5d9b21{10%,70%{transform:translate(0%)}15%,20%,60%,65%{transform:translate(50px)}30%,50%{transform:translate(100px,-50px)}}@keyframes load-8-0a5d9b21{15%,65%{transform:translate(0%)}20%,60%{transform:translateY(-50px)}30%,50%{transform:translate(50px,-100px)}}.timer-circle[data-v-7e3c5bda]{stroke-dashoffset:0;animation:linear forwards toast-timer-7e3c5bda}@keyframes toast-timer-7e3c5bda{to{stroke-dashoffset:var(--fa0d20de)}}
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){[data-v-add2fbaf],[data-v-add2fbaf]:before,[data-v-add2fbaf]:after,[data-v-add2fbaf]::backdrop{--tw-font-weight:initial}}}@layer components{[role=img][data-v-add2fbaf]{height:fit-content;font-family:var(--font-sans,"Poppins", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);border-top-left-radius:.333333em;border-bottom-left-radius:.333333em;margin-right:calc(.5lh + .333333em);padding-block:.333333em;padding-inline:.666667em}}@property --tw-font-weight{syntax:"*";inherits:false}input[data-snap][data-v-5d3904ac]{animation:snap-in-5d3904ac .18s var(--ease-out-strong)}input[data-snap=up][data-v-5d3904ac]{--snap-from:2px}input[data-snap=down][data-v-5d3904ac]{--snap-from:-2px}@keyframes snap-in-5d3904ac{0%{opacity:0;transform:translateY(var(--snap-from))}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){input[data-snap][data-v-5d3904ac]{animation:1ms snap-hold-5d3904ac}@keyframes snap-hold-5d3904ac{0%,to{opacity:1}}}input[data-snap][data-v-15feed8b]{animation:snap-in-15feed8b .18s var(--ease-out-strong)}input[data-snap=up][data-v-15feed8b]{--snap-from:2px}input[data-snap=down][data-v-15feed8b]{--snap-from:-2px}@keyframes snap-in-15feed8b{0%{opacity:0;transform:translateY(var(--snap-from))}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){input[data-snap][data-v-15feed8b]{animation:1ms snap-hold-15feed8b}@keyframes snap-hold-15feed8b{0%,to{opacity:1}}}rect[data-v-0a5d9b21]{animation-duration:4s;animation-timing-function:cubic-bezier(.77,0,.175,1);animation-iteration-count:infinite}rect[data-v-0a5d9b21]:first-child{animation-name:load-1-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(2){animation-name:load-2-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(3){animation-name:load-3-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(4){animation-name:load-4-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(5){animation-name:load-5-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(6){animation-name:load-6-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(7){animation-name:load-7-0a5d9b21}rect[data-v-0a5d9b21]:nth-child(8){animation-name:load-8-0a5d9b21}@media (prefers-reduced-motion:reduce){rect[data-v-0a5d9b21]{animation:none}svg[data-v-0a5d9b21]{animation:2.5s ease-in-out infinite pulse-0a5d9b21}}@keyframes pulse-0a5d9b21{0%,to{opacity:1}50%{opacity:.65}}@keyframes load-1-0a5d9b21{0%,80%{transform:translate(0)}5%,20%,60%,75%{transform:translate(-25px,25px)}30%,50%{transform:translate(-75px,75px)}}@keyframes load-2-0a5d9b21{5%,75%{transform:translate(0%)}10%,20%,60%,70%{transform:translate(50px)}30%,50%{transform:translateY(50px)}}@keyframes load-3-0a5d9b21{10%,70%{transform:translate(0%)}15%,65%{transform:translateY(-50px)}20%,60%{fill:var(--color-sapphire-500);transform:translateY(-50px)}30%,50%{fill:var(--color-purple-500);transform:translate(-50px)}}@keyframes load-4-0a5d9b21{20%,60%{transform:translate(0%)}30%,50%{transform:translate(50px,-50px)}}@keyframes load-5-0a5d9b21{0%,80%{transform:translate(0%)}5%,20%,60%,75%{transform:translateY(50px)}30%,50%{transform:translate(50px)}}@keyframes load-6-0a5d9b21{5%,75%{transform:translate(0%)}10%,20%,60%,70%{transform:translate(-50px)}30%,50%{transform:translateY(-50px)}}@keyframes load-7-0a5d9b21{10%,70%{transform:translate(0%)}15%,20%,60%,65%{transform:translate(50px)}30%,50%{transform:translate(100px,-50px)}}@keyframes load-8-0a5d9b21{15%,65%{transform:translate(0%)}20%,60%{transform:translateY(-50px)}30%,50%{transform:translate(50px,-100px)}}.timer-circle[data-v-7e3c5bda]{stroke-dashoffset:0;animation:linear forwards toast-timer-7e3c5bda}@keyframes toast-timer-7e3c5bda{to{stroke-dashoffset:var(--fa0d20de)}}
2
3
  /*$vite$:1*/
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "1.52.0",
4
+ "version": "1.54.0",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -1,14 +1,14 @@
1
- <script lang="ts" setup>
2
- import { computed } from 'vue'
3
-
1
+ <script lang="ts">
2
+ /** An energy-label grade, from `A++++` down to `G`. */
4
3
  export type EnergyLabelLetter =
5
4
  'A++++' | 'A+++' | 'A++' | 'A+' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G'
6
5
 
7
- interface NexxtEnergyLabelProps {
8
- label: EnergyLabelLetter | Lowercase<EnergyLabelLetter>
9
- }
10
-
11
- const COLORS: Record<EnergyLabelLetter, string> = {
6
+ /**
7
+ * The swatch colour per grade, as an uppercase hex string. Every `A` grade
8
+ * shares one green. Key order runs from best to worst, so
9
+ * `Object.keys(ENERGY_LABEL_COLORS)` lists the grades in display order.
10
+ */
11
+ export const ENERGY_LABEL_COLORS: Readonly<Record<EnergyLabelLetter, string>> = Object.freeze({
12
12
  'A++++': '#00953F',
13
13
  'A+++': '#00953F',
14
14
  'A++': '#00953F',
@@ -20,7 +20,22 @@ const COLORS: Record<EnergyLabelLetter, string> = {
20
20
  E: '#F9A93C',
21
21
  F: '#EF7D34',
22
22
  G: '#E63329',
23
+ })
24
+
25
+ /**
26
+ * Size, padding, margin and typography follow from classes on the component:
27
+ * a font-size class scales the whole label, arrow included. The arrow always
28
+ * takes the label's full height. When you raise the vertical padding, raise
29
+ * the right margin by the same amount so the arrow keeps its room.
30
+ */
31
+ export interface NexxtEnergyLabelProps {
32
+ /** The grade to show. Lowercase input is accepted and shown in uppercase. */
33
+ label: EnergyLabelLetter | Lowercase<EnergyLabelLetter>
23
34
  }
35
+ </script>
36
+
37
+ <script lang="ts" setup>
38
+ import { computed } from 'vue'
24
39
 
25
40
  defineOptions({
26
41
  name: 'NexxtEnergyLabel',
@@ -30,16 +45,41 @@ const { label } = defineProps<NexxtEnergyLabelProps>()
30
45
 
31
46
  const normalized = computed(() => label.toUpperCase() as EnergyLabelLetter)
32
47
 
33
- const color = computed(() => COLORS[normalized.value])
48
+ const color = computed(() => ENERGY_LABEL_COLORS[normalized.value])
34
49
  </script>
35
50
 
36
51
  <template>
37
52
  <span
38
53
  role="img"
39
54
  :aria-label="`Energielabel ${normalized}`"
40
- class="relative isolate mr-3 inline-flex h-6 items-center justify-center rounded-l px-2 extra-small-semibold text-white after:absolute after:top-1/2 after:left-full after:size-4.25 after:-translate-1/2 after:rotate-45 after:rounded-tr after:bg-(--energy-label-color) after:content-['']"
55
+ class="relative isolate inline-flex items-center justify-center text-white after:absolute after:top-1/2 after:left-full after:aspect-square after:h-[calc(100%*17/24)] after:-translate-1/2 after:rotate-45 after:rounded-tr-[calc(100%*4/17)] after:bg-(--energy-label-color) after:content-['']"
41
56
  :style="{ backgroundColor: color, '--energy-label-color': color }"
42
57
  >
43
58
  <span aria-hidden="true" class="relative z-10">{{ normalized }}</span>
44
59
  </span>
45
60
  </template>
61
+
62
+ <style scoped>
63
+ @reference '../../../index.css';
64
+
65
+ /*
66
+ * The sizing defaults sit in the components layer, below Tailwind's utilities,
67
+ * so padding, margin, radius and typography classes on the component win
68
+ * without `!`. They are in `em` and `lh`, so a font-size class scales the whole
69
+ * label; at the default 12px/16px they come out at the old h-6, px-2, mr-3 and
70
+ * rounded-l. The margin is half the label's height: the room the arrow takes.
71
+ * h-fit stands in for the old fixed h-6: a stretching flex or grid parent
72
+ * must not pull the label (and its arrow) taller. The root is matched by its
73
+ * role, as a class name trips the Tailwind lint.
74
+ *
75
+ * The arrow is a square of 17/24 of the label's height, turned 45 degrees, so
76
+ * its diagonal just covers the height (no seam) and its tip sits half a height
77
+ * past the edge. 17/24 and the 4/17 corner are the old 17px square with its
78
+ * 4px corner on a 24px label, kept as ratios.
79
+ */
80
+ @layer components {
81
+ [role='img'] {
82
+ @apply mr-[calc(0.5lh+1em/3)] h-fit rounded-l-[calc(1em/3)] px-[calc(2em/3)] py-[calc(1em/3)] extra-small-semibold;
83
+ }
84
+ }
85
+ </style>
@@ -0,0 +1,155 @@
1
+ import { computed, toValue, type ComponentPublicInstance, type MaybeRefOrGetter } from 'vue'
2
+
3
+ /**
4
+ * Keyboard behaviour of a two-dimensional radiogroup (WAI-ARIA APG radio group,
5
+ * in 2D): one tab stop, arrows that move *and* choose, no wrap at the edges.
6
+ *
7
+ * Positions are cell indices in reading order, `row * columns + column`. A
8
+ * radio that spans several cells (a band) is registered on one of its cells;
9
+ * `positions` lists those, so the arrows only move along the radios that
10
+ * exist. Cell rendering stays with the component.
11
+ */
12
+ export interface UseRadioGridOptions {
13
+ /** Number of rows of the grid. */
14
+ rows: MaybeRefOrGetter<number>
15
+ /** Number of columns of the grid. */
16
+ columns: MaybeRefOrGetter<number>
17
+ /**
18
+ * The positions that hold a radio, in any order. Defaults to every cell. An
19
+ * arrow towards a cell that is not in here does not move.
20
+ */
21
+ positions?: MaybeRefOrGetter<readonly number[]>
22
+ /** The chosen position, or `null` while no radio is chosen. */
23
+ selected: MaybeRefOrGetter<number | null>
24
+ /**
25
+ * The position that takes the tab stop while none is chosen. Without one, or
26
+ * when it returns a position that holds no radio, the first position does.
27
+ */
28
+ preferred?: () => number | null
29
+ /**
30
+ * What Home and End jump to: the first and last radio of the current row, or
31
+ * of the whole grid. Defaults to `'grid'`.
32
+ */
33
+ homeEnd?: 'row' | 'grid'
34
+ /** Called with the position an arrow, Home or End lands on. */
35
+ onChoose: (position: number) => void
36
+ /**
37
+ * Called when the focus leaves the grid after one or more keyboard choices:
38
+ * the end of a keyboard run, where a consumer closes its undo step.
39
+ */
40
+ onLeave?: () => void
41
+ }
42
+
43
+ export const useRadioGrid = (options: UseRadioGridOptions) => {
44
+ const { homeEnd = 'grid', onChoose, onLeave } = options
45
+
46
+ const elements = new Map<number, HTMLElement>()
47
+
48
+ /** Whether a keyboard choice happened since the focus last left the grid. */
49
+ let pending = false
50
+
51
+ const valid = computed(() => {
52
+ const all = toValue(options.positions)
53
+ const count = toValue(options.rows) * toValue(options.columns)
54
+ return [...(all ?? Array.from({ length: count }, (_, index) => index))].sort((a, b) => a - b)
55
+ })
56
+
57
+ /** Roving tabindex: the chosen radio, else the preferred one, else the first. */
58
+ const tabStop = computed(() => {
59
+ const selected = toValue(options.selected)
60
+ if (selected !== null && valid.value.includes(selected)) return selected
61
+ const preferred = options.preferred?.() ?? null
62
+ if (preferred !== null && valid.value.includes(preferred)) return preferred
63
+ return valid.value[0] ?? null
64
+ })
65
+
66
+ /**
67
+ * Function ref for the radio on `position`, so the arrows can focus it. On
68
+ * unmount only a detached element is dropped: in a crossfade the leaving
69
+ * layer unmounts after the entering one registered the same position.
70
+ */
71
+ const cellRef = (position: number) => (el: Element | ComponentPublicInstance | null) => {
72
+ if (el instanceof HTMLElement) {
73
+ elements.set(position, el)
74
+ return
75
+ }
76
+ for (const [key, stored] of elements) if (!stored.isConnected) elements.delete(key)
77
+ }
78
+
79
+ const step = (position: number, rowStep: number, columnStep: number) => {
80
+ const columns = toValue(options.columns)
81
+ const row = Math.floor(position / columns) + rowStep
82
+ const column = (position % columns) + columnStep
83
+ if (row < 0 || row >= toValue(options.rows) || column < 0 || column >= columns) return position
84
+ const next = row * columns + column
85
+ return valid.value.includes(next) ? next : position
86
+ }
87
+
88
+ const ends = (position: number): [number, number] => {
89
+ const columns = toValue(options.columns)
90
+ const row = Math.floor(position / columns)
91
+ const pool =
92
+ homeEnd === 'row'
93
+ ? valid.value.filter((candidate) => Math.floor(candidate / columns) === row)
94
+ : valid.value
95
+ return [pool[0] ?? position, pool[pool.length - 1] ?? position]
96
+ }
97
+
98
+ /**
99
+ * An arrow moves one cell and chooses it; at an edge, or towards a cell that
100
+ * holds no radio, it stays. A key with a modifier belongs to the browser or
101
+ * the OS (Alt+Left is back, Cmd+Left is line start), so it is left alone: no
102
+ * choice, no preventDefault.
103
+ */
104
+ const onKeydown = (event: KeyboardEvent, position: number) => {
105
+ if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return
106
+
107
+ const [first, last] = ends(position)
108
+ const moves: Record<string, () => number> = {
109
+ ArrowLeft: () => step(position, 0, -1),
110
+ ArrowRight: () => step(position, 0, 1),
111
+ ArrowUp: () => step(position, -1, 0),
112
+ ArrowDown: () => step(position, 1, 0),
113
+ Home: () => first,
114
+ End: () => last,
115
+ }
116
+ const next = moves[event.key]?.()
117
+
118
+ if (next === undefined) return
119
+
120
+ // Also when the arrow does not move: otherwise it scrolls the panel.
121
+ event.preventDefault()
122
+ if (next === position && next === toValue(options.selected)) return
123
+
124
+ onChoose(next)
125
+ pending = true
126
+ elements.get(next)?.focus()
127
+ }
128
+
129
+ /** Bind on the element that holds every radio. */
130
+ const onFocusout = (event: FocusEvent) => {
131
+ const container = event.currentTarget as HTMLElement | null
132
+ const to = event.relatedTarget as Node | null
133
+ if (to && container?.contains(to)) return
134
+ if (!pending) return
135
+ pending = false
136
+ onLeave?.()
137
+ }
138
+
139
+ /**
140
+ * Ends the keyboard run without signalling it: call it when the consumer
141
+ * committed by other means (a click), so leaving the grid does not commit a
142
+ * second time.
143
+ */
144
+ const reset = () => {
145
+ pending = false
146
+ }
147
+
148
+ /** Puts the focus on the tab stop, e.g. after the radios were re-rendered. */
149
+ const focusTabStop = () => {
150
+ const position = tabStop.value
151
+ if (position !== null) elements.get(position)?.focus()
152
+ }
153
+
154
+ return { tabStop, cellRef, onKeydown, onFocusout, reset, focusTabStop }
155
+ }