@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/components/atoms/EnergyLabel/EnergyLabel.vue.d.ts +16 -2
- package/dist/components/editor/composables/useRadioGrid.d.ts +48 -0
- package/dist/components/editor/molecules/EditorAlignItems/EditorAlignItems.vue.d.ts +2 -2
- package/dist/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.vue.d.ts +101 -0
- package/dist/components/editor/molecules/EditorGap/EditorGap.vue.d.ts +32 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1568 -1196
- package/dist/nuxt.js +1 -0
- package/dist/ui.css +2 -1
- package/package.json +1 -1
- package/src/components/atoms/EnergyLabel/EnergyLabel.vue +50 -10
- package/src/components/editor/composables/useRadioGrid.ts +155 -0
- package/src/components/editor/molecules/EditorAlignmentGrid/EditorAlignmentGrid.vue +515 -0
- package/src/components/editor/molecules/EditorAnchor/EditorAnchor.vue +25 -41
- package/src/components/editor/molecules/EditorAnchor/__snapshots__/EditorAnchor.test.ts.snap +65 -0
- package/src/components/editor/molecules/EditorGap/EditorGap.vue +76 -2
- package/src/components/editor/molecules/EditorGap/__snapshots__/EditorGap.test.ts.snap +103 -0
- package/src/components.json +1 -0
- package/src/index.ts +10 -0
- package/src/nuxt.ts +6 -2
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
|
-
|
|
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,14 +1,14 @@
|
|
|
1
|
-
<script lang="ts"
|
|
2
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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(() =>
|
|
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
|
|
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
|
+
}
|