@nexxtmove/ui 0.1.37 → 0.1.39
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/index.d.ts +79 -34
- package/dist/index.js +2285 -1753
- package/package.json +2 -1
- package/src/components/AnimatedNumber/AnimatedNumber.vue +4 -48
- package/src/components/CustomerJourneyTile/CustomerJourneyTile.vue +39 -14
- package/src/components/DropdownButton/DropdownButton.vue +32 -20
- package/src/components/Header/Header.vue +3 -1
- package/src/components/InputField/InputField.vue +29 -43
- package/src/components/Table/Table.vue +50 -15
- package/src/components/TimelineEvent/TimelineTypes.ts +2 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexxtmove/ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.39",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"tailwind"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@number-flow/vue": "^0.5.0",
|
|
23
24
|
"date-fns": "^4.1.0",
|
|
24
25
|
"date-fns-tz": "^3.2.0",
|
|
25
26
|
"reka-ui": "^2.9.2"
|
|
@@ -1,65 +1,21 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import
|
|
2
|
+
import NumberFlow from '@number-flow/vue'
|
|
3
3
|
|
|
4
4
|
defineOptions({
|
|
5
5
|
name: 'NexxtAnimatedNumber',
|
|
6
6
|
})
|
|
7
7
|
|
|
8
|
-
type EasingFunction = 'ease-linear' | 'ease-in' | 'ease-out' | 'ease-in-out'
|
|
9
|
-
|
|
10
8
|
const {
|
|
11
9
|
value,
|
|
12
10
|
duration = 500,
|
|
13
|
-
|
|
11
|
+
delay = 0,
|
|
14
12
|
} = defineProps<{
|
|
15
13
|
value: number
|
|
16
14
|
duration?: number
|
|
17
|
-
|
|
15
|
+
delay?: number
|
|
18
16
|
}>()
|
|
19
|
-
|
|
20
|
-
const direction = ref<'up' | 'down'>('up')
|
|
21
|
-
const digits = computed(() => String(value).split(''))
|
|
22
|
-
const transitionDuration = computed(() => `duration-${duration}`)
|
|
23
|
-
|
|
24
|
-
watch(
|
|
25
|
-
() => value,
|
|
26
|
-
(newVal, oldVal) => {
|
|
27
|
-
direction.value = newVal > oldVal ? 'up' : 'down'
|
|
28
|
-
},
|
|
29
|
-
)
|
|
30
17
|
</script>
|
|
31
18
|
|
|
32
19
|
<template>
|
|
33
|
-
<
|
|
34
|
-
tag="div"
|
|
35
|
-
class="relative flex shrink-0 items-center tabular-nums"
|
|
36
|
-
:aria-label="value"
|
|
37
|
-
:enter-active-class="`transition-all ${transitionDuration} ${easing}`"
|
|
38
|
-
:leave-active-class="`absolute transition-all ${transitionDuration} ${easing}`"
|
|
39
|
-
:enter-from-class="
|
|
40
|
-
direction === 'up' ? 'translate-y-full opacity-0' : '-translate-y-full opacity-0'
|
|
41
|
-
"
|
|
42
|
-
:leave-to-class="
|
|
43
|
-
direction === 'up' ? '-translate-y-full opacity-0' : 'translate-y-full opacity-0'
|
|
44
|
-
"
|
|
45
|
-
move-class="transition-all duration-500 ease-in-out"
|
|
46
|
-
>
|
|
47
|
-
<div
|
|
48
|
-
v-for="(digit, i) in digits"
|
|
49
|
-
:key="i"
|
|
50
|
-
class="relative inline-flex h-[1.5em] w-[1ch] items-center justify-center overflow-hidden"
|
|
51
|
-
aria-hidden="true"
|
|
52
|
-
>
|
|
53
|
-
<Transition
|
|
54
|
-
:enter-active-class="`transition-transform ${transitionDuration} ${easing}`"
|
|
55
|
-
:leave-active-class="`absolute inset-0 transition-transform ${transitionDuration} ${easing}`"
|
|
56
|
-
:enter-from-class="direction === 'up' ? 'translate-y-full' : '-translate-y-full'"
|
|
57
|
-
:leave-to-class="direction === 'up' ? '-translate-y-full' : 'translate-y-full'"
|
|
58
|
-
>
|
|
59
|
-
<span :key="digit" class="flex h-full w-full items-center justify-center">
|
|
60
|
-
{{ digit }}
|
|
61
|
-
</span>
|
|
62
|
-
</Transition>
|
|
63
|
-
</div>
|
|
64
|
-
</TransitionGroup>
|
|
20
|
+
<NumberFlow :value="value" :transform-timing="{ duration, delay }" />
|
|
65
21
|
</template>
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import NexxtIcon from '../Icon/Icon.vue'
|
|
3
|
-
import
|
|
3
|
+
import NexxtAnimatedNumber from '../AnimatedNumber/AnimatedNumber.vue'
|
|
4
|
+
import { computed, onMounted, ref } from 'vue'
|
|
5
|
+
import { PHASE_MAP } from '../TimelineEvent/TimelineTypes'
|
|
4
6
|
|
|
5
|
-
type Phase =
|
|
7
|
+
type Phase = (typeof PHASE_MAP)[keyof typeof PHASE_MAP]['label']
|
|
6
8
|
|
|
7
9
|
interface NexxtCustomerJourneyTileProps {
|
|
10
|
+
as?: keyof HTMLElementTagNameMap
|
|
8
11
|
title?: string
|
|
9
12
|
amount?: number
|
|
13
|
+
animated?: boolean
|
|
14
|
+
delay?: number
|
|
10
15
|
percentage?: number
|
|
11
16
|
percentageExplanaition?: string
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
const PHASE_ICON_MAP: Record<Phase, string
|
|
19
|
+
const PHASE_ICON_MAP: Partial<Record<Phase, string>> = {
|
|
15
20
|
Latent: 'eye-closed',
|
|
16
21
|
Oriëntatie: 'building-magnifying-glass',
|
|
17
22
|
'Actief zoeken': 'house-person-return',
|
|
@@ -24,8 +29,21 @@ defineOptions({
|
|
|
24
29
|
name: 'NexxtCustomerJourneyTile',
|
|
25
30
|
})
|
|
26
31
|
|
|
27
|
-
const {
|
|
28
|
-
|
|
32
|
+
const {
|
|
33
|
+
as = 'div',
|
|
34
|
+
title,
|
|
35
|
+
amount,
|
|
36
|
+
animated = false,
|
|
37
|
+
delay,
|
|
38
|
+
percentage,
|
|
39
|
+
percentageExplanaition,
|
|
40
|
+
} = defineProps<NexxtCustomerJourneyTileProps>()
|
|
41
|
+
|
|
42
|
+
const displayAmount = ref(animated ? 0 : (amount ?? 0))
|
|
43
|
+
|
|
44
|
+
onMounted(() => {
|
|
45
|
+
if (animated && amount !== undefined) displayAmount.value = amount
|
|
46
|
+
})
|
|
29
47
|
|
|
30
48
|
const icon = computed(() => {
|
|
31
49
|
if (!title) return null
|
|
@@ -46,10 +64,10 @@ const ariaLabel = computed(() => {
|
|
|
46
64
|
</script>
|
|
47
65
|
|
|
48
66
|
<template>
|
|
49
|
-
<
|
|
50
|
-
|
|
67
|
+
<component
|
|
68
|
+
:is="as"
|
|
51
69
|
:aria-label="ariaLabel"
|
|
52
|
-
class="flex
|
|
70
|
+
class="flex rounded-xl bg-white outline outline-gray-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cornflower-blue-500"
|
|
53
71
|
:class="icon ? 'gap-6 p-1.5' : 'p-4'"
|
|
54
72
|
>
|
|
55
73
|
<div
|
|
@@ -63,19 +81,26 @@ const ariaLabel = computed(() => {
|
|
|
63
81
|
<span
|
|
64
82
|
class="heading-1"
|
|
65
83
|
:class="icon ? 'text-purple-500' : 'text-sapphire-900'"
|
|
66
|
-
v-if="amount"
|
|
67
|
-
>{{ amount }}</span
|
|
84
|
+
v-if="amount !== undefined"
|
|
68
85
|
>
|
|
69
|
-
|
|
86
|
+
<NexxtAnimatedNumber :value="displayAmount" :delay="delay" />
|
|
87
|
+
</span>
|
|
88
|
+
<div v-if="percentage !== undefined" class="flex gap-0.5">
|
|
70
89
|
<span
|
|
71
90
|
class="extra-small-semibold"
|
|
72
|
-
:class="
|
|
91
|
+
:class="
|
|
92
|
+
percentage > 0
|
|
93
|
+
? 'text-green-500'
|
|
94
|
+
: percentage === 0
|
|
95
|
+
? 'text-orange-500'
|
|
96
|
+
: 'text-brick-500'
|
|
97
|
+
"
|
|
73
98
|
><template v-if="percentage > 0">+</template>{{ percentage }}%</span
|
|
74
99
|
>
|
|
75
|
-
<span class="extra-small-normal" v-if="percentageExplanaition">{{
|
|
100
|
+
<span class="ml-0.5 text-left extra-small-normal" v-if="percentageExplanaition">{{
|
|
76
101
|
percentageExplanaition
|
|
77
102
|
}}</span>
|
|
78
103
|
</div>
|
|
79
104
|
</div>
|
|
80
|
-
</
|
|
105
|
+
</component>
|
|
81
106
|
</template>
|
|
@@ -6,14 +6,21 @@ interface NexxtDropdownButton {
|
|
|
6
6
|
label: string
|
|
7
7
|
action: string | (() => void)
|
|
8
8
|
icon?: InstanceType<typeof NexxtIcon>['name']
|
|
9
|
+
selected?: boolean
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
type NexxtDropdownButtonBase = {
|
|
12
13
|
variant?: 'primary' | 'secondary'
|
|
13
14
|
options: NexxtDropdownButton[]
|
|
14
15
|
placement?: 'left' | 'right'
|
|
16
|
+
triggerIcon?: InstanceType<typeof NexxtIcon>['name']
|
|
17
|
+
icon?: InstanceType<typeof NexxtIcon>['name']
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
export type NexxtDropdownButtonProps =
|
|
21
|
+
| (NexxtDropdownButtonBase & { iconOnly?: false; label: string; action: string | (() => void) })
|
|
22
|
+
| (NexxtDropdownButtonBase & { iconOnly: true; label?: string; action?: string | (() => void) })
|
|
23
|
+
|
|
17
24
|
const {
|
|
18
25
|
variant = 'primary',
|
|
19
26
|
options,
|
|
@@ -21,6 +28,8 @@ const {
|
|
|
21
28
|
action,
|
|
22
29
|
icon,
|
|
23
30
|
placement = 'right',
|
|
31
|
+
iconOnly = false,
|
|
32
|
+
triggerIcon = 'chevron-down',
|
|
24
33
|
} = defineProps<NexxtDropdownButtonProps>()
|
|
25
34
|
|
|
26
35
|
const emit = defineEmits<{
|
|
@@ -106,31 +115,33 @@ const transitionClasses = computed(() => {
|
|
|
106
115
|
:class="placement === 'right' ? 'items-end' : 'items-start'"
|
|
107
116
|
>
|
|
108
117
|
<div class="inline-flex items-center overflow-hidden rounded-md" :class="componentClasses">
|
|
118
|
+
<template v-if="!iconOnly">
|
|
119
|
+
<button
|
|
120
|
+
class="flex cursor-pointer items-center gap-2 px-4 py-2 transition-colors duration-200"
|
|
121
|
+
:class="buttonClasses"
|
|
122
|
+
@click="action && handleAction(action)"
|
|
123
|
+
>
|
|
124
|
+
<NexxtIcon v-if="icon" :name="icon" aria-hidden="true" />
|
|
125
|
+
{{ label }}
|
|
126
|
+
</button>
|
|
127
|
+
<span
|
|
128
|
+
class="h-7 w-px rounded-sm"
|
|
129
|
+
:class="variant === 'primary' ? 'bg-white' : 'bg-gray-200'"
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
></span>
|
|
132
|
+
</template>
|
|
109
133
|
<button
|
|
110
|
-
class="
|
|
111
|
-
:class="buttonClasses"
|
|
112
|
-
@click="
|
|
113
|
-
>
|
|
114
|
-
<NexxtIcon v-if="icon" :name="icon" aria-hidden="true" />
|
|
115
|
-
{{ label }}
|
|
116
|
-
</button>
|
|
117
|
-
<span
|
|
118
|
-
class="h-7 w-px rounded-sm"
|
|
119
|
-
:class="variant === 'primary' ? 'bg-white' : 'bg-gray-200'"
|
|
120
|
-
aria-hidden="true"
|
|
121
|
-
></span>
|
|
122
|
-
<button
|
|
123
|
-
class="cursor-pointer px-2 py-2 transition-colors duration-200"
|
|
124
|
-
:class="buttonClasses"
|
|
125
|
-
@click="toggleDropdown"
|
|
134
|
+
class="w-10 shrink-0 cursor-pointer transition-colors duration-200"
|
|
135
|
+
:class="[buttonClasses, iconOnly ? 'p-2' : 'px-2 py-2']"
|
|
136
|
+
@click="iconOnly ? toggleDropdown() : toggleDropdown()"
|
|
126
137
|
aria-haspopup="menu"
|
|
127
138
|
:aria-expanded="isOpen"
|
|
128
139
|
:aria-label="`Open ${label} menu`"
|
|
129
140
|
>
|
|
130
141
|
<NexxtIcon
|
|
131
|
-
name="
|
|
142
|
+
:name="triggerIcon"
|
|
132
143
|
class="transition-transform duration-200"
|
|
133
|
-
:class="{ 'rotate-180': isOpen }"
|
|
144
|
+
:class="{ 'rotate-180': isOpen && triggerIcon === 'chevron-down' }"
|
|
134
145
|
aria-hidden="true"
|
|
135
146
|
/>
|
|
136
147
|
</button>
|
|
@@ -140,7 +151,7 @@ const transitionClasses = computed(() => {
|
|
|
140
151
|
<div
|
|
141
152
|
v-if="isOpen"
|
|
142
153
|
role="menu"
|
|
143
|
-
class="absolute top-full z-10 mt-2.5 min-w-full rounded-md border border-gray-200 bg-white"
|
|
154
|
+
class="absolute top-full z-10 mt-2.5 min-w-full rounded-md border border-gray-200 bg-white whitespace-nowrap"
|
|
144
155
|
:class="placement === 'right' ? 'right-0' : 'left-0'"
|
|
145
156
|
>
|
|
146
157
|
<button
|
|
@@ -148,6 +159,7 @@ const transitionClasses = computed(() => {
|
|
|
148
159
|
:key="option.label"
|
|
149
160
|
role="menuitem"
|
|
150
161
|
class="flex w-full min-w-42 cursor-pointer items-center gap-2 px-4 py-2 text-left text-sm whitespace-nowrap text-gray-700 transition-colors duration-200 hover:bg-gray-100 hover:text-purple-600 focus:bg-gray-100 focus:outline-none"
|
|
162
|
+
:class="option.selected ? 'bg-cornflower-blue-100' : ''"
|
|
151
163
|
@click="handleOptionClick(option.action)"
|
|
152
164
|
>
|
|
153
165
|
<NexxtIcon v-if="option.icon" :name="option.icon" aria-hidden="true" />
|
|
@@ -3,6 +3,7 @@ import Button from '../Button/Button.vue'
|
|
|
3
3
|
|
|
4
4
|
interface NexxtHeaderProps {
|
|
5
5
|
title: string
|
|
6
|
+
headingType?: 'heading-1' | 'heading-2' | 'heading-3' | 'heading-4'
|
|
6
7
|
backButtonText?: string
|
|
7
8
|
backButtonAction?: () => void
|
|
8
9
|
}
|
|
@@ -12,6 +13,7 @@ defineOptions({
|
|
|
12
13
|
})
|
|
13
14
|
|
|
14
15
|
const {
|
|
16
|
+
headingType = 'heading-4',
|
|
15
17
|
backButtonText,
|
|
16
18
|
backButtonAction = () => {
|
|
17
19
|
window.history.back()
|
|
@@ -29,7 +31,7 @@ const {
|
|
|
29
31
|
@click="backButtonAction"
|
|
30
32
|
>{{ backButtonText }}</Button
|
|
31
33
|
>
|
|
32
|
-
<span class="block truncate
|
|
34
|
+
<span class="block truncate text-sapphire-500" :class="headingType">
|
|
33
35
|
{{ title }}
|
|
34
36
|
</span>
|
|
35
37
|
</div>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref, computed } from 'vue'
|
|
3
2
|
import Icon from '../Icon/Icon.vue'
|
|
4
3
|
|
|
5
4
|
const generateId = () => 'input-field-' + Math.random().toString(36).slice(2, 10)
|
|
6
|
-
const inputId =
|
|
5
|
+
const inputId = generateId()
|
|
7
6
|
|
|
8
7
|
defineOptions({
|
|
9
8
|
name: 'NexxtInputField',
|
|
@@ -14,71 +13,58 @@ interface NexxtInputFieldProps {
|
|
|
14
13
|
label?: string
|
|
15
14
|
error?: boolean
|
|
16
15
|
errorMessage?: string
|
|
17
|
-
focused?: boolean
|
|
18
16
|
leftIcon?: InstanceType<typeof Icon>['name']
|
|
19
17
|
rightIcon?: InstanceType<typeof Icon>['name']
|
|
20
18
|
}
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
defineProps<NexxtInputFieldProps>()
|
|
23
21
|
|
|
24
|
-
// ✅ Correct usage
|
|
25
22
|
const modelValue = defineModel<string>()
|
|
26
|
-
|
|
27
|
-
const isFocused = ref(false)
|
|
28
|
-
|
|
29
|
-
const wrapperClasses = computed(() => {
|
|
30
|
-
if (props.error) {
|
|
31
|
-
return 'border-brick-400'
|
|
32
|
-
}
|
|
33
|
-
if (props.focused || isFocused.value) {
|
|
34
|
-
return 'border-cornflower-blue-500'
|
|
35
|
-
}
|
|
36
|
-
return 'border-gray-200'
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
const onFocus = () => {
|
|
40
|
-
isFocused.value = true
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const onBlur = () => {
|
|
44
|
-
isFocused.value = false
|
|
45
|
-
}
|
|
46
23
|
</script>
|
|
47
24
|
|
|
48
25
|
<template>
|
|
49
|
-
<div class="flex
|
|
26
|
+
<div class="inline-flex flex-col gap-1">
|
|
50
27
|
<label
|
|
51
|
-
v-if="
|
|
28
|
+
v-if="label"
|
|
52
29
|
class="text-color-gray-700 justify-start body-normal text-sm"
|
|
53
30
|
:for="inputId"
|
|
54
31
|
>
|
|
55
|
-
{{
|
|
32
|
+
{{ label }}
|
|
56
33
|
</label>
|
|
57
34
|
|
|
58
35
|
<div
|
|
59
|
-
class="flex items-center gap-2 rounded-
|
|
60
|
-
:class="
|
|
36
|
+
class="flex items-center gap-2 rounded-sm px-3 ring transition-colors ease-out focus-within:ring-cornflower-blue-500"
|
|
37
|
+
:class="error ? 'ring-brick-400' : 'ring-gray-200'"
|
|
61
38
|
>
|
|
62
|
-
<span v-if="
|
|
63
|
-
<Icon :name="
|
|
39
|
+
<span v-if="leftIcon" class="flex items-center">
|
|
40
|
+
<Icon :name="leftIcon" />
|
|
64
41
|
</span>
|
|
65
42
|
|
|
66
43
|
<input
|
|
67
44
|
:id="inputId"
|
|
68
45
|
v-model="modelValue"
|
|
69
|
-
:placeholder="
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class="w-full bg-transparent text-sm text-gray-900 placeholder-gray-400 focus:outline-none"
|
|
46
|
+
:placeholder="placeholder"
|
|
47
|
+
v-bind="$attrs"
|
|
48
|
+
class="h-10 min-w-60 bg-transparent text-sm text-gray-900 placeholder-gray-400 focus:outline-none"
|
|
73
49
|
/>
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
50
|
+
<slot name="right">
|
|
51
|
+
<span v-if="rightIcon" class="flex items-center">
|
|
52
|
+
<Icon :name="rightIcon" />
|
|
53
|
+
</span>
|
|
54
|
+
</slot>
|
|
78
55
|
</div>
|
|
79
56
|
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
57
|
+
<Transition
|
|
58
|
+
enter-active-class="transition-all duration-200 ease-out"
|
|
59
|
+
enter-from-class="opacity-0 -translate-y-1"
|
|
60
|
+
enter-to-class="opacity-100 translate-y-0"
|
|
61
|
+
leave-active-class="transition-all duration-150 ease-in"
|
|
62
|
+
leave-from-class="opacity-100 translate-y-0"
|
|
63
|
+
leave-to-class="opacity-0 -translate-y-1"
|
|
64
|
+
>
|
|
65
|
+
<p v-if="error && errorMessage" class="text-sm text-brick-400">
|
|
66
|
+
{{ errorMessage }}
|
|
67
|
+
</p>
|
|
68
|
+
</Transition>
|
|
83
69
|
</div>
|
|
84
70
|
</template>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script lang="ts" setup generic="TKey extends string">
|
|
1
|
+
<script lang="ts" setup generic="TKey extends string, TRow extends Record<TKey, unknown>">
|
|
2
2
|
import { computed, ref } from 'vue'
|
|
3
3
|
import Icon from '../Icon/Icon.vue'
|
|
4
4
|
import Checkbox from '../Checkbox/Checkbox.vue'
|
|
@@ -8,24 +8,35 @@ export interface TableColumn<K extends string = string> {
|
|
|
8
8
|
label: string
|
|
9
9
|
sortable?: boolean
|
|
10
10
|
width?: string
|
|
11
|
+
minCellWidth?: number
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export interface NexxtTableProps<
|
|
14
|
+
export interface NexxtTableProps<
|
|
15
|
+
TKey extends string = string,
|
|
16
|
+
TRow extends Record<TKey, unknown> = Record<TKey, unknown>,
|
|
17
|
+
> {
|
|
14
18
|
columns: TableColumn<TKey>[]
|
|
15
|
-
rows:
|
|
19
|
+
rows: TRow[]
|
|
16
20
|
selectable?: boolean
|
|
21
|
+
loading?: boolean
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
const INTERACTIVE = ['a', 'button', 'input', 'select', 'textarea', 'label']
|
|
20
25
|
|
|
21
26
|
defineOptions({ name: 'NexxtTable' })
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
defineSlots<
|
|
29
|
+
{ [K in TKey as `cell-${K}`]?: (props: { row: TRow; value: TRow[K] }) => unknown } & {
|
|
30
|
+
[key: string]: ((props: Record<string, unknown>) => unknown) | undefined
|
|
31
|
+
}
|
|
32
|
+
>()
|
|
33
|
+
|
|
34
|
+
const props = defineProps<NexxtTableProps<TKey, TRow>>()
|
|
24
35
|
const emit = defineEmits<{
|
|
25
|
-
'row-click': [row:
|
|
36
|
+
'row-click': [row: TRow]
|
|
26
37
|
}>()
|
|
27
38
|
|
|
28
|
-
const selected = defineModel<
|
|
39
|
+
const selected = defineModel<TRow[]>({ default: () => [] })
|
|
29
40
|
const sortKey = defineModel<TKey>('sortKey')
|
|
30
41
|
const sortDirection = defineModel<'asc' | 'desc'>('sortDirection', { default: 'asc' })
|
|
31
42
|
|
|
@@ -38,20 +49,22 @@ const someSelected = computed(
|
|
|
38
49
|
() => selected.value.length > 0 && selected.value.length < props.rows.length,
|
|
39
50
|
)
|
|
40
51
|
|
|
41
|
-
const isRowSelected = (row:
|
|
52
|
+
const isRowSelected = (row: TRow) => selected.value.includes(row)
|
|
42
53
|
|
|
43
54
|
const toggleAll = () => {
|
|
44
55
|
selected.value = allSelected.value ? [] : [...props.rows]
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
const toggleRow = (row:
|
|
58
|
+
const toggleRow = (row: TRow) => {
|
|
48
59
|
selected.value = isRowSelected(row)
|
|
49
60
|
? selected.value.filter((r) => r !== row)
|
|
50
61
|
: [...selected.value, row]
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
const columnTemplate = computed(() =>
|
|
54
|
-
props.columns
|
|
65
|
+
props.columns
|
|
66
|
+
.map((col) => `minmax(${col.minCellWidth ?? 150}px, ${col.width ?? '1fr'})`)
|
|
67
|
+
.join(' '),
|
|
55
68
|
)
|
|
56
69
|
|
|
57
70
|
const gridTemplate = computed(() =>
|
|
@@ -84,6 +97,9 @@ const getRowRoundedClass = (rowIndex: number) => {
|
|
|
84
97
|
return ''
|
|
85
98
|
}
|
|
86
99
|
|
|
100
|
+
const rowValue = (row: TRow, key: TKey): TRow[TKey] => row[key]
|
|
101
|
+
const cellSlotName = (key: TKey): string => `cell-${key}`
|
|
102
|
+
|
|
87
103
|
const handleRowKeydown = (event: KeyboardEvent, rowIndex: number) => {
|
|
88
104
|
if (event.key === 'ArrowDown') {
|
|
89
105
|
event.preventDefault()
|
|
@@ -97,12 +113,12 @@ const handleRowKeydown = (event: KeyboardEvent, rowIndex: number) => {
|
|
|
97
113
|
|
|
98
114
|
<template>
|
|
99
115
|
<div class="flex flex-col gap-3">
|
|
100
|
-
<div class="overflow-auto">
|
|
116
|
+
<div class="overflow-x-auto">
|
|
101
117
|
<div
|
|
102
118
|
role="grid"
|
|
103
119
|
:aria-colcount="columns.length + (selectable ? 1 : 0)"
|
|
104
120
|
:aria-rowcount="rows.length + 1"
|
|
105
|
-
class="
|
|
121
|
+
class="min-w-full"
|
|
106
122
|
>
|
|
107
123
|
<div class="sticky top-0 z-10 mb-0.5">
|
|
108
124
|
<div v-if="$slots.toolbar">
|
|
@@ -151,7 +167,9 @@ const handleRowKeydown = (event: KeyboardEvent, rowIndex: number) => {
|
|
|
151
167
|
@keydown.enter.prevent="handleSort(column)"
|
|
152
168
|
@keydown.space.prevent="handleSort(column)"
|
|
153
169
|
>
|
|
154
|
-
|
|
170
|
+
<span :class="sortKey === column.key ? 'text-cornflower-blue-600' : ''">{{
|
|
171
|
+
column.label
|
|
172
|
+
}}</span>
|
|
155
173
|
<Icon
|
|
156
174
|
v-if="column.sortable"
|
|
157
175
|
:name="
|
|
@@ -168,7 +186,7 @@ const handleRowKeydown = (event: KeyboardEvent, rowIndex: number) => {
|
|
|
168
186
|
</div>
|
|
169
187
|
</div>
|
|
170
188
|
|
|
171
|
-
<div role="rowgroup">
|
|
189
|
+
<div role="rowgroup" class="relative">
|
|
172
190
|
<div
|
|
173
191
|
v-for="(row, rowIndex) in rows"
|
|
174
192
|
:key="rowIndex"
|
|
@@ -210,8 +228,8 @@ const handleRowKeydown = (event: KeyboardEvent, rowIndex: number) => {
|
|
|
210
228
|
role="gridcell"
|
|
211
229
|
:aria-colindex="colIndex + (selectable ? 2 : 1)"
|
|
212
230
|
>
|
|
213
|
-
<slot :name="
|
|
214
|
-
{{ row
|
|
231
|
+
<slot :name="cellSlotName(column.key)" :row="row" :value="rowValue(row, column.key)">
|
|
232
|
+
{{ rowValue(row, column.key) }}
|
|
215
233
|
</slot>
|
|
216
234
|
</div>
|
|
217
235
|
</div>
|
|
@@ -219,6 +237,23 @@ const handleRowKeydown = (event: KeyboardEvent, rowIndex: number) => {
|
|
|
219
237
|
<div v-if="!rows.length" class="px-4 py-8 text-center text-sm text-slate-400" role="row">
|
|
220
238
|
<slot name="empty">Geen data beschikbaar</slot>
|
|
221
239
|
</div>
|
|
240
|
+
|
|
241
|
+
<Transition
|
|
242
|
+
enter-active-class="transition-opacity duration-200"
|
|
243
|
+
leave-active-class="transition-opacity duration-200"
|
|
244
|
+
enter-from-class="opacity-0"
|
|
245
|
+
leave-to-class="opacity-0"
|
|
246
|
+
>
|
|
247
|
+
<div
|
|
248
|
+
v-if="loading"
|
|
249
|
+
data-testid="loading-overlay"
|
|
250
|
+
class="absolute inset-0 flex items-center justify-center rounded-md bg-white/40 backdrop-blur-sm"
|
|
251
|
+
>
|
|
252
|
+
<slot name="loading"
|
|
253
|
+
><Icon name="spinner-third" class="fa-spin text-3xl text-purple-500"
|
|
254
|
+
/></slot>
|
|
255
|
+
</div>
|
|
256
|
+
</Transition>
|
|
222
257
|
</div>
|
|
223
258
|
</div>
|
|
224
259
|
</div>
|
|
@@ -186,7 +186,7 @@ export const PHASE_MAP: Record<number, PhaseConfig> = {
|
|
|
186
186
|
textColor: 'text-gray-700',
|
|
187
187
|
},
|
|
188
188
|
1: {
|
|
189
|
-
label: '
|
|
189
|
+
label: 'Latent',
|
|
190
190
|
color: 'border-amber-500',
|
|
191
191
|
bgColor: 'bg-amber-500',
|
|
192
192
|
fromColor: 'from-amber-500',
|
|
@@ -202,7 +202,7 @@ export const PHASE_MAP: Record<number, PhaseConfig> = {
|
|
|
202
202
|
textColor: 'text-white',
|
|
203
203
|
},
|
|
204
204
|
3: {
|
|
205
|
-
label: '
|
|
205
|
+
label: 'Actief zoeken',
|
|
206
206
|
color: 'border-yellow-500',
|
|
207
207
|
bgColor: 'bg-yellow-500',
|
|
208
208
|
fromColor: 'from-yellow-500',
|