@nexxtmove/ui 1.12.0 → 1.14.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/molecules/TemplateCard/TemplateCard.vue.d.ts +39 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +267 -197
- package/dist/ui.css +1 -1
- package/package.json +2 -1
- package/src/components/atoms/Avatar/Avatar.vue +3 -3
- package/src/components/atoms/Button/Button.vue +11 -1
- package/src/components/atoms/Card/Card.vue +1 -1
- package/src/components/atoms/Checkbox/Checkbox.vue +1 -1
- package/src/components/atoms/EnergyLabel/EnergyLabel.vue +1 -1
- package/src/components/atoms/PresenceIndicator/PresenceIndicator.vue +3 -3
- package/src/components/atoms/PropertyListing/PropertyListing.vue +1 -1
- package/src/components/atoms/SelectableOption/SelectableOption.vue +1 -1
- package/src/components/atoms/Skeleton/Skeleton.vue +1 -1
- package/src/components/atoms/Squircle/Squircle.vue +1 -1
- package/src/components/molecules/CustomerJourneyTile/CustomerJourneyTile.vue +1 -1
- package/src/components/molecules/DoughnutChart/DoughnutChart.vue +1 -4
- package/src/components/molecules/InputField/InputField.vue +1 -1
- package/src/components/molecules/InputSelect/InputSelect.vue +1 -1
- package/src/components/molecules/Pagination/Pagination.vue +5 -5
- package/src/components/molecules/PropertyCard/PropertyCard.vue +4 -4
- package/src/components/molecules/SkeletonPropertyCard/SkeletonPropertyCard.vue +2 -2
- package/src/components/molecules/SocialIcons/SocialIcons.vue +1 -1
- package/src/components/molecules/SocialMediaCustomTemplate/SocialMediaCustomTemplate.vue +1 -1
- package/src/components/molecules/TemplateCard/TemplateCard.vue +156 -30
- package/src/components/molecules/TimelineEvent/TimelineEvent.vue +3 -3
- package/src/components/molecules/TimelineLanes/TimelineLanes.vue +2 -2
- package/src/components/molecules/Toast/Toast.vue +11 -3
- package/src/components/organisms/Calendar/_CalendarDayView.vue +2 -2
- package/src/components/organisms/Calendar/_CalendarHeader.vue +7 -7
- package/src/components/organisms/Calendar/_CalendarTimePicker.vue +4 -4
- package/src/components/organisms/ColorPicker/ColorPicker.vue +12 -12
- package/src/components/organisms/DropdownButton/DropdownButton.vue +1 -1
- package/src/components/organisms/Modal/Modal.vue +2 -2
- package/src/components/organisms/SocialMediaTemplate/SocialMediaTemplate.vue +4 -4
- package/src/components/organisms/Table/Table.vue +1 -1
- package/src/components/organisms/TimelinePhaseblock/TimelinePhaseblock.vue +3 -6
- package/src/components/organisms/VideoPlayer/VideoPlayer.vue +5 -5
- package/src/index.ts +5 -0
package/dist/ui.css
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
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-
|
|
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
2
|
/*$vite$:1*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexxtmove/ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.14.0",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"consola": "^3.4.2",
|
|
58
58
|
"eslint": "^10.8.1",
|
|
59
59
|
"eslint-config-prettier": "^10.1.8",
|
|
60
|
+
"eslint-plugin-better-tailwindcss": "4.7.0",
|
|
60
61
|
"eslint-plugin-prettier": "^5.5.6",
|
|
61
62
|
"eslint-plugin-storybook": "10.5.10",
|
|
62
63
|
"eslint-plugin-vue": "^10.10.0",
|
|
@@ -63,14 +63,14 @@ const fontSizeClasses = {
|
|
|
63
63
|
<template>
|
|
64
64
|
<NexxtSquircle v-if="squircle" :size="size">
|
|
65
65
|
<div
|
|
66
|
-
class="flex
|
|
66
|
+
class="flex size-full items-center justify-center bg-cornflower-blue-50 font-semibold text-cornflower-blue-500"
|
|
67
67
|
:class="[fontSizeClasses[size]]"
|
|
68
68
|
>
|
|
69
69
|
<img
|
|
70
70
|
v-if="showImage"
|
|
71
71
|
:src="src"
|
|
72
72
|
:alt="name"
|
|
73
|
-
class="
|
|
73
|
+
class="size-full object-cover"
|
|
74
74
|
@error="imageFailed = true"
|
|
75
75
|
/>
|
|
76
76
|
<template v-else>{{ computedInitials }}</template>
|
|
@@ -88,7 +88,7 @@ const fontSizeClasses = {
|
|
|
88
88
|
v-if="showImage"
|
|
89
89
|
:src="src"
|
|
90
90
|
:alt="name"
|
|
91
|
-
class="
|
|
91
|
+
class="size-full object-cover"
|
|
92
92
|
@error="imageFailed = true"
|
|
93
93
|
/>
|
|
94
94
|
<template v-else>{{ computedInitials }}</template>
|
|
@@ -23,6 +23,16 @@ const {
|
|
|
23
23
|
rounded = false,
|
|
24
24
|
fullWidth = false,
|
|
25
25
|
} = defineProps<NexxtButtonProps>()
|
|
26
|
+
|
|
27
|
+
// Full class names so Tailwind and the no-unknown-classes lint rule see them.
|
|
28
|
+
const VARIANT_CLASSES = {
|
|
29
|
+
primary: 'button-primary',
|
|
30
|
+
secondary: 'button-secondary',
|
|
31
|
+
success: 'button-success',
|
|
32
|
+
danger: 'button-danger',
|
|
33
|
+
warning: 'button-warning',
|
|
34
|
+
link: 'button-link',
|
|
35
|
+
} as const
|
|
26
36
|
</script>
|
|
27
37
|
|
|
28
38
|
<template>
|
|
@@ -33,7 +43,7 @@ const {
|
|
|
33
43
|
'button relative flex h-10 cursor-pointer items-center overflow-hidden transition-colors duration-200 focus-visible:outline-3 disabled:cursor-not-allowed',
|
|
34
44
|
fullWidth ? 'w-full justify-start' : 'justify-center',
|
|
35
45
|
rounded ? 'rounded-full' : 'rounded-lg',
|
|
36
|
-
|
|
46
|
+
VARIANT_CLASSES[variant],
|
|
37
47
|
$slots.default ?? 'w-10',
|
|
38
48
|
!$slots.default && icon ? 'aspect-square px-0' : '',
|
|
39
49
|
loading ? 'pointer-events-none' : '',
|
|
@@ -14,7 +14,7 @@ defineProps<Props>()
|
|
|
14
14
|
v-if="image"
|
|
15
15
|
:src="image"
|
|
16
16
|
:alt="imageAlt ?? 'Card afbeelding'"
|
|
17
|
-
class="block h-40 max-h-40 min-h-40 w-full rounded-
|
|
17
|
+
class="block h-40 max-h-40 min-h-40 w-full rounded-t-lg object-cover"
|
|
18
18
|
/>
|
|
19
19
|
</slot>
|
|
20
20
|
|
|
@@ -33,7 +33,7 @@ watchEffect(() => {
|
|
|
33
33
|
<span
|
|
34
34
|
aria-hidden="true"
|
|
35
35
|
:class="[
|
|
36
|
-
'flex
|
|
36
|
+
'flex size-5 shrink-0 items-center justify-center rounded-sm border transition-colors duration-200',
|
|
37
37
|
'peer-focus-visible:ring-2 peer-focus-visible:ring-cornflower-blue-400 peer-focus-visible:ring-offset-2',
|
|
38
38
|
model || indeterminate
|
|
39
39
|
? 'border-cornflower-blue-600 text-cornflower-blue-600'
|
|
@@ -37,7 +37,7 @@ const color = computed(() => COLORS[normalized.value])
|
|
|
37
37
|
<span
|
|
38
38
|
role="img"
|
|
39
39
|
: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-
|
|
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-['']"
|
|
41
41
|
:style="{ backgroundColor: color, '--energy-label-color': color }"
|
|
42
42
|
>
|
|
43
43
|
<span aria-hidden="true" class="relative z-10">{{ normalized }}</span>
|
|
@@ -17,13 +17,13 @@ const { online = false } = defineProps<NexxtPresenceIndicatorProps>()
|
|
|
17
17
|
:aria-label="online ? 'Online' : 'Offline'"
|
|
18
18
|
role="status"
|
|
19
19
|
>
|
|
20
|
-
<span class="relative flex
|
|
20
|
+
<span class="relative flex size-1.5">
|
|
21
21
|
<span
|
|
22
22
|
v-if="online"
|
|
23
|
-
class="absolute inline-flex
|
|
23
|
+
class="absolute inline-flex size-full animate-ping rounded-full bg-green-400 opacity-75"
|
|
24
24
|
/>
|
|
25
25
|
<i
|
|
26
|
-
class="relative block
|
|
26
|
+
class="relative block size-1.5 rounded-full"
|
|
27
27
|
:class="online ? 'bg-green-500' : 'bg-gray-700'"
|
|
28
28
|
/>
|
|
29
29
|
</span>
|
|
@@ -56,7 +56,7 @@ const RADIUS = {
|
|
|
56
56
|
<slot :selected="selected" />
|
|
57
57
|
<span
|
|
58
58
|
v-if="checkmark && selected"
|
|
59
|
-
class="absolute -top-2 -right-2 flex
|
|
59
|
+
class="absolute -top-2 -right-2 flex size-6 items-center justify-center rounded-full bg-cornflower-blue-500 text-white shadow-sm"
|
|
60
60
|
>
|
|
61
61
|
<NexxtIcon name="check" type="solid" class="text-xs" />
|
|
62
62
|
</span>
|
|
@@ -43,7 +43,7 @@ const style = computed(() => ({
|
|
|
43
43
|
<template>
|
|
44
44
|
<div class="relative block shrink-0 overflow-hidden" :style="style">
|
|
45
45
|
<svg
|
|
46
|
-
class="absolute
|
|
46
|
+
class="absolute size-0 overflow-hidden"
|
|
47
47
|
aria-hidden="true"
|
|
48
48
|
focusable="false"
|
|
49
49
|
width="0"
|
|
@@ -102,7 +102,7 @@ const iconColors = computed(() => {
|
|
|
102
102
|
>
|
|
103
103
|
<div
|
|
104
104
|
v-if="icon"
|
|
105
|
-
class="flex min-h-10 w-14 items-center justify-center overflow-hidden rounded-
|
|
105
|
+
class="flex min-h-10 w-14 items-center justify-center overflow-hidden rounded-l-md"
|
|
106
106
|
:class="iconColors"
|
|
107
107
|
>
|
|
108
108
|
<NexxtIcon :name="icon" class="text-2xl" aria-hidden="true" />
|
|
@@ -107,10 +107,7 @@ const resolvedBackgroundColor = computed(() =>
|
|
|
107
107
|
:width="size - thickness * 2"
|
|
108
108
|
:height="size - thickness * 2"
|
|
109
109
|
>
|
|
110
|
-
<div
|
|
111
|
-
xmlns="http://www.w3.org/1999/xhtml"
|
|
112
|
-
class="flex h-full w-full items-center justify-center"
|
|
113
|
-
>
|
|
110
|
+
<div xmlns="http://www.w3.org/1999/xhtml" class="flex size-full items-center justify-center">
|
|
114
111
|
<slot>
|
|
115
112
|
<span class="heading-1">{{ value }}</span>
|
|
116
113
|
</slot>
|
|
@@ -97,7 +97,7 @@ const decrement = () => {
|
|
|
97
97
|
v-model="modelValue"
|
|
98
98
|
:placeholder="placeholder"
|
|
99
99
|
v-bind="inputAttrs"
|
|
100
|
-
class="
|
|
100
|
+
class="h-10 w-full min-w-60 bg-transparent text-sm text-gray-900 placeholder-gray-400 focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
101
101
|
:class="{ 'pl-3': !leftIcon, 'pr-3': !rightIcon }"
|
|
102
102
|
/>
|
|
103
103
|
<slot name="right">
|
|
@@ -141,7 +141,7 @@ const select = (opt: SelectOption, close: () => void) => {
|
|
|
141
141
|
v-model="inputModel"
|
|
142
142
|
:placeholder="placeholder"
|
|
143
143
|
v-bind="inputAttrs"
|
|
144
|
-
class="
|
|
144
|
+
class="h-10 w-full min-w-40 bg-transparent px-3 text-sm text-gray-900 placeholder-gray-400 focus:outline-none"
|
|
145
145
|
/>
|
|
146
146
|
</div>
|
|
147
147
|
|
|
@@ -47,13 +47,13 @@ const {
|
|
|
47
47
|
>
|
|
48
48
|
<PaginationFirst
|
|
49
49
|
v-if="items.some((item) => item.type === 'ellipsis')"
|
|
50
|
-
class="flex
|
|
50
|
+
class="flex size-8 cursor-pointer items-center justify-center rounded-lg bg-transparent transition hover:bg-cornflower-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
51
51
|
:disabled="disabled"
|
|
52
52
|
>
|
|
53
53
|
<NexxtIcon name="angles-left" />
|
|
54
54
|
</PaginationFirst>
|
|
55
55
|
<PaginationPrev
|
|
56
|
-
class="mr-4 flex
|
|
56
|
+
class="mr-4 flex size-8 cursor-pointer items-center justify-center rounded-lg bg-transparent transition hover:bg-cornflower-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
57
57
|
:disabled="disabled"
|
|
58
58
|
>
|
|
59
59
|
<NexxtIcon name="chevron-left" />
|
|
@@ -72,20 +72,20 @@ const {
|
|
|
72
72
|
v-else
|
|
73
73
|
:key="page.type"
|
|
74
74
|
:index="index"
|
|
75
|
-
class="flex
|
|
75
|
+
class="flex size-8 items-center justify-center"
|
|
76
76
|
>
|
|
77
77
|
…
|
|
78
78
|
</PaginationEllipsis>
|
|
79
79
|
</template>
|
|
80
80
|
<PaginationNext
|
|
81
|
-
class="ml-4 flex
|
|
81
|
+
class="ml-4 flex size-8 cursor-pointer items-center justify-center rounded-lg bg-transparent transition hover:bg-cornflower-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
82
82
|
:disabled="disabled"
|
|
83
83
|
>
|
|
84
84
|
<NexxtIcon name="chevron-right" />
|
|
85
85
|
</PaginationNext>
|
|
86
86
|
<PaginationLast
|
|
87
87
|
v-if="items.some((item) => item.type === 'ellipsis')"
|
|
88
|
-
class="flex
|
|
88
|
+
class="flex size-8 cursor-pointer items-center justify-center rounded-lg bg-transparent transition hover:bg-cornflower-blue-50 disabled:cursor-not-allowed disabled:opacity-50"
|
|
89
89
|
:disabled="disabled"
|
|
90
90
|
>
|
|
91
91
|
<NexxtIcon name="angles-right" />
|
|
@@ -20,17 +20,17 @@ defineProps<NexxtPropertyCardProps>()
|
|
|
20
20
|
<template>
|
|
21
21
|
<NexxtCard :image="image" :image-alt="imageAlt">
|
|
22
22
|
<template v-if="!image" #image>
|
|
23
|
-
<div class="relative isolate h-40 w-full overflow-hidden rounded-
|
|
23
|
+
<div class="relative isolate h-40 w-full overflow-hidden rounded-t-lg">
|
|
24
24
|
<img
|
|
25
25
|
:src="PropertyPlaceholder"
|
|
26
26
|
alt="Woning placeholder"
|
|
27
27
|
loading="lazy"
|
|
28
|
-
class="
|
|
28
|
+
class="size-full object-cover"
|
|
29
29
|
/>
|
|
30
30
|
<div
|
|
31
|
-
class="absolute inset-0 flex items-center justify-center rounded-
|
|
31
|
+
class="absolute inset-0 flex items-center justify-center rounded-t-lg bg-white/20 backdrop-blur-sm"
|
|
32
32
|
>
|
|
33
|
-
<NexxtIcon name="house" class="text-6xl text-white drop-shadow" />
|
|
33
|
+
<NexxtIcon name="house" class="text-6xl text-white drop-shadow-sm" />
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
|
36
36
|
</template>
|
|
@@ -18,14 +18,14 @@ withDefaults(defineProps<NexxtSkeletonPropertyCardProps>(), {
|
|
|
18
18
|
role="status"
|
|
19
19
|
:aria-label="ariaLabel"
|
|
20
20
|
>
|
|
21
|
-
<NexxtSkeleton class="h-40 w-full rounded-
|
|
21
|
+
<NexxtSkeleton class="h-40 w-full rounded-t-lg rounded-b-none" />
|
|
22
22
|
|
|
23
23
|
<div
|
|
24
24
|
class="box-border flex w-full flex-col gap-2 rounded-b-lg border border-gray-200 px-4 py-2"
|
|
25
25
|
>
|
|
26
26
|
<div class="flex items-center gap-2">
|
|
27
27
|
<NexxtSkeleton class="h-4 w-24" />
|
|
28
|
-
<NexxtSkeleton class="
|
|
28
|
+
<NexxtSkeleton class="size-4" />
|
|
29
29
|
<NexxtSkeleton class="h-4 w-28" />
|
|
30
30
|
</div>
|
|
31
31
|
|
|
@@ -48,7 +48,7 @@ const activeIcons = computed(() =>
|
|
|
48
48
|
<li
|
|
49
49
|
v-for="platform in activeIcons"
|
|
50
50
|
:key="platform.name"
|
|
51
|
-
class="inline-flex
|
|
51
|
+
class="inline-flex size-5 items-center justify-center rounded-full outline-1 outline-white"
|
|
52
52
|
:class="brandColors[platform.name]"
|
|
53
53
|
:aria-label="platform.label"
|
|
54
54
|
>
|
|
@@ -7,11 +7,31 @@ import NexxtTooltip from '../../atoms/Tooltip/Tooltip.vue'
|
|
|
7
7
|
export type NexxtTemplateCardContentFeature = 'references' | 'valuation' | 'reviews' | 'packages'
|
|
8
8
|
|
|
9
9
|
export interface NexxtTemplateCardProps {
|
|
10
|
+
/** Preview image of the template. Falls back to a placeholder of the same height when omitted. */
|
|
10
11
|
image?: string
|
|
12
|
+
/** Template name, shown as the card heading. Truncated on one line. */
|
|
11
13
|
title: string
|
|
14
|
+
/** Secondary line under the title, e.g. when the template was last changed. Truncated on one line. */
|
|
12
15
|
subtitle?: string
|
|
16
|
+
/** Content blocks the template contains; each renders an icon with a tooltip. */
|
|
13
17
|
contentFeatures?: NexxtTemplateCardContentFeature[]
|
|
18
|
+
/** Overrides the default Dutch tooltip text per content feature. */
|
|
14
19
|
messages?: Partial<Record<NexxtTemplateCardContentFeature, string>>
|
|
20
|
+
/** Lays a full-card button over the content that emits `select`. Off keeps the card inert, so a read-only grid adds no empty tabstop per card. */
|
|
21
|
+
selectable?: boolean
|
|
22
|
+
/** Whether this card is currently chosen: draws the blue border and, with `checkmark`, the badge. Only does something with `selectable`. A prop plus the `select` emit rather than a `defineModel`, because the card owns no state: the parent grid decides which single card may be chosen. */
|
|
23
|
+
selected?: boolean
|
|
24
|
+
/** Shows a check badge in the top-right corner while selected. Only does something with `selectable`. */
|
|
25
|
+
checkmark?: boolean
|
|
26
|
+
/** Disables the select button, dims the card content and drops the overlay. Only does something with `selectable`. */
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type NexxtTemplateCardSlots = {
|
|
31
|
+
/** Covers the whole card and fades in on hover or keyboard focus; the content underneath dims. Not rendered while the card is selectable and disabled. On a coarse pointer it shrinks to the image band, without veil or blur, so the title stays readable and the feature icons stay tappable. The layer is click-through, but `*:pointer-events-auto` only reaches its direct children: an own `absolute inset-0` wrapper inside the slot swallows the select click. */
|
|
32
|
+
overlay?(props: { selected: boolean }): unknown
|
|
33
|
+
/** Top-right corner of the card, for a preview or menu button. Stays interactive while `disabled`, which makes it the place for e.g. a preview button: the overlay is not rendered then. */
|
|
34
|
+
actions?(props: { selected: boolean }): unknown
|
|
15
35
|
}
|
|
16
36
|
|
|
17
37
|
type TemplateCardIcon = {
|
|
@@ -45,46 +65,152 @@ const props = withDefaults(defineProps<NexxtTemplateCardProps>(), {
|
|
|
45
65
|
subtitle: undefined,
|
|
46
66
|
contentFeatures: () => [],
|
|
47
67
|
messages: () => ({}),
|
|
68
|
+
selectable: false,
|
|
69
|
+
selected: false,
|
|
70
|
+
checkmark: false,
|
|
71
|
+
disabled: false,
|
|
48
72
|
})
|
|
49
73
|
|
|
74
|
+
const emit = defineEmits<{
|
|
75
|
+
/** The card was chosen. No payload: the consumer knows which template it rendered. */
|
|
76
|
+
select: []
|
|
77
|
+
}>()
|
|
78
|
+
|
|
79
|
+
// The root of the template stays a plain `<div>` and must stay single-root: the heading, the
|
|
80
|
+
// paragraph and the tooltip triggers are flow content, which is invalid inside a `<button>`, and
|
|
81
|
+
// wrapping them also dropped the heading role and turned the accessible name of the button into
|
|
82
|
+
// the whole card as one long string. A leading comment in the template would make it a fragment,
|
|
83
|
+
// after which class and attributes no longer reach the card, so the notes live here.
|
|
84
|
+
// Selection, hover and the dim are driven from the props and from the root's own `:hover`, not
|
|
85
|
+
// from the button. The slot checks are read inline rather than through a computed -- slot presence
|
|
86
|
+
// is not reactive, so a consumer toggling `<template v-if>` on a slot would never re-evaluate a
|
|
87
|
+
// cached computed.
|
|
88
|
+
//
|
|
89
|
+
// The overlay and the actions are siblings of the select button, never children: a button inside a
|
|
90
|
+
// button is invalid markup and swallows the inner click, and both layers stack above it so their
|
|
91
|
+
// own buttons get the click. They are handed the selection because their content often styles
|
|
92
|
+
// itself by it.
|
|
93
|
+
defineSlots<NexxtTemplateCardSlots>()
|
|
94
|
+
|
|
50
95
|
const featureMessages = computed<Record<NexxtTemplateCardContentFeature, string>>(() => ({
|
|
51
96
|
...DEFAULT_MESSAGES,
|
|
52
97
|
...props.messages,
|
|
53
98
|
}))
|
|
99
|
+
|
|
100
|
+
const onSelect = () => {
|
|
101
|
+
if (props.selectable && !props.disabled) emit('select')
|
|
102
|
+
}
|
|
54
103
|
</script>
|
|
55
104
|
|
|
56
105
|
<template>
|
|
57
|
-
<div
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
106
|
+
<div
|
|
107
|
+
class="group relative size-full max-w-105 rounded-xl border border-gray-200 transition-colors duration-200 ease-out"
|
|
108
|
+
:class="[
|
|
109
|
+
selectable && selected && 'border-cornflower-blue-500',
|
|
110
|
+
selectable && !selected && !disabled && 'hover:border-gray-300',
|
|
111
|
+
]"
|
|
112
|
+
>
|
|
113
|
+
<div
|
|
114
|
+
class="relative z-10 transition-opacity duration-200 ease-out"
|
|
115
|
+
:class="[
|
|
116
|
+
// Lets a click anywhere on the card reach the stretched button underneath; only the
|
|
117
|
+
// tooltip triggers claim their own pointer events back.
|
|
118
|
+
selectable && 'pointer-events-none',
|
|
119
|
+
selectable && disabled && 'opacity-50',
|
|
120
|
+
$slots.overlay &&
|
|
121
|
+
!(selectable && disabled) &&
|
|
122
|
+
'group-focus-within:opacity-40 group-hover:opacity-40 pointer-coarse:opacity-100!',
|
|
123
|
+
]"
|
|
124
|
+
>
|
|
125
|
+
<picture class="block h-38 w-full">
|
|
126
|
+
<!-- Decorative: the template name is right below it as text. -->
|
|
127
|
+
<img
|
|
128
|
+
v-if="image"
|
|
129
|
+
:src="image"
|
|
130
|
+
alt=""
|
|
131
|
+
loading="lazy"
|
|
132
|
+
class="h-38 w-full rounded-t-xl object-cover"
|
|
133
|
+
/>
|
|
134
|
+
<div v-else class="flex h-38 w-full items-center justify-center rounded-t-xl bg-gray-100">
|
|
135
|
+
<NexxtIcon name="image" class="text-4xl text-gray-400" />
|
|
136
|
+
</div>
|
|
137
|
+
</picture>
|
|
138
|
+
<div class="rounded-b-xl px-8 pt-3.5 pb-4">
|
|
139
|
+
<h3 class="truncate heading-4 text-gray-900">{{ title }}</h3>
|
|
140
|
+
<p v-if="subtitle" class="truncate body-normal text-gray-700">{{ subtitle }}</p>
|
|
141
|
+
<ul v-if="contentFeatures.length" class="mt-3 flex flex-wrap gap-2">
|
|
142
|
+
<li v-for="feature in contentFeatures" :key="feature">
|
|
143
|
+
<NexxtTooltip>
|
|
144
|
+
<!--
|
|
145
|
+
Claims its pointer events back from the content layer so the tooltip still opens on
|
|
146
|
+
hover, and forwards the click to the card so the icon is no dead zone on a
|
|
147
|
+
selectable card. `onSelect` guards on selectable and disabled itself.
|
|
148
|
+
NexxtIcon is aria-hidden and the trigger is not focusable, so the label is only
|
|
149
|
+
there for screen readers.
|
|
150
|
+
-->
|
|
151
|
+
<div
|
|
152
|
+
:class="[ICON_MAP[feature].backgroundColor, ICON_MAP[feature].textColor]"
|
|
153
|
+
class="pointer-events-auto flex size-8 items-center justify-center rounded-full"
|
|
154
|
+
@click="onSelect"
|
|
155
|
+
>
|
|
156
|
+
<NexxtIcon :name="ICON_MAP[feature].icon" />
|
|
157
|
+
<span class="sr-only">{{ featureMessages[feature] }}</span>
|
|
158
|
+
</div>
|
|
159
|
+
<template #tooltip>
|
|
160
|
+
<span class="small-normal text-gray-700">{{ featureMessages[feature] }}</span>
|
|
161
|
+
</template>
|
|
162
|
+
</NexxtTooltip>
|
|
163
|
+
</li>
|
|
164
|
+
</ul>
|
|
68
165
|
</div>
|
|
69
|
-
</picture>
|
|
70
|
-
<div class="rounded-b-xl px-8 pt-3.5 pb-4">
|
|
71
|
-
<h3 class="truncate heading-4 text-gray-900">{{ title }}</h3>
|
|
72
|
-
<p v-if="subtitle" class="truncate body-normal text-gray-700">{{ subtitle }}</p>
|
|
73
|
-
<ul v-if="contentFeatures.length" class="mt-3 flex flex-wrap gap-2">
|
|
74
|
-
<li v-for="feature in contentFeatures" :key="feature">
|
|
75
|
-
<NexxtTooltip>
|
|
76
|
-
<div
|
|
77
|
-
:class="[ICON_MAP[feature].backgroundColor, ICON_MAP[feature].textColor]"
|
|
78
|
-
class="flex size-8 items-center justify-center rounded-full"
|
|
79
|
-
>
|
|
80
|
-
<NexxtIcon :name="ICON_MAP[feature].icon" />
|
|
81
|
-
</div>
|
|
82
|
-
<template #tooltip>
|
|
83
|
-
<span class="body-small text-gray-700">{{ featureMessages[feature] }}</span>
|
|
84
|
-
</template>
|
|
85
|
-
</NexxtTooltip>
|
|
86
|
-
</li>
|
|
87
|
-
</ul>
|
|
88
166
|
</div>
|
|
167
|
+
|
|
168
|
+
<!--
|
|
169
|
+
A stretched button instead of a wrapper: it carries the click, the pressed state and the
|
|
170
|
+
focus ring, while the card content above it stays real document structure. It holds the
|
|
171
|
+
lowest stacking level of the layers, so a click on an actions or overlay button lands there
|
|
172
|
+
and not here. The focus ring follows Button.vue (`outline-3` plus offset), so a selected card
|
|
173
|
+
with keyboard focus shows two distinguishable indicators: blue border and ring.
|
|
174
|
+
-->
|
|
175
|
+
<button
|
|
176
|
+
v-if="selectable"
|
|
177
|
+
type="button"
|
|
178
|
+
:aria-label="title"
|
|
179
|
+
:aria-pressed="selected"
|
|
180
|
+
:disabled="disabled || undefined"
|
|
181
|
+
class="absolute inset-0 cursor-pointer rounded-xl focus-visible:outline-3 focus-visible:outline-offset-2 focus-visible:outline-cornflower-blue-500 disabled:cursor-not-allowed"
|
|
182
|
+
@click="onSelect"
|
|
183
|
+
/>
|
|
184
|
+
|
|
185
|
+
<div v-if="$slots.actions" class="absolute top-4 right-4 z-20">
|
|
186
|
+
<slot name="actions" :selected="selected" />
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!--
|
|
190
|
+
Dropped while selectable and disabled: an invisible layer would still hold tabbable buttons,
|
|
191
|
+
and a card that cannot be chosen reveals nothing. A read-only card (no `selectable`) keeps
|
|
192
|
+
its overlay, disabled or not.
|
|
193
|
+
`pointer-events-none` on the layer with `*:pointer-events-auto` on its children lets a click
|
|
194
|
+
on the veil fall through to the select button while the overlay's own buttons keep theirs.
|
|
195
|
+
On a coarse pointer there is no hover to reveal it, so it is always there -- but then without
|
|
196
|
+
veil or blur and shrunk to the image band at the top, because a permanent veil over the title
|
|
197
|
+
and the subtitle drops their contrast below 4.5:1 and a full-height layer would swallow the
|
|
198
|
+
taps on the feature icons.
|
|
199
|
+
jsdom resolves no Tailwind and the storybook project cannot hit-test, so the layering itself
|
|
200
|
+
is asserted by neither suite; only the DOM order is.
|
|
201
|
+
-->
|
|
202
|
+
<div
|
|
203
|
+
v-if="$slots.overlay && !(selectable && disabled)"
|
|
204
|
+
class="pointer-events-none absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 rounded-xl bg-white/60 opacity-0 backdrop-blur-xs transition-opacity duration-200 ease-out *:pointer-events-auto group-focus-within:opacity-100 group-hover:opacity-100 pointer-coarse:inset-x-0 pointer-coarse:top-0 pointer-coarse:bottom-auto pointer-coarse:h-38 pointer-coarse:justify-end pointer-coarse:bg-transparent pointer-coarse:p-4 pointer-coarse:opacity-100 pointer-coarse:backdrop-blur-none"
|
|
205
|
+
>
|
|
206
|
+
<slot name="overlay" :selected="selected" />
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<span
|
|
210
|
+
v-if="selectable && selected && checkmark"
|
|
211
|
+
class="absolute -top-2 -right-2 z-30 flex size-6 items-center justify-center rounded-full bg-cornflower-blue-500 text-white shadow-sm"
|
|
212
|
+
>
|
|
213
|
+
<NexxtIcon name="check" type="solid" class="text-xs" />
|
|
214
|
+
</span>
|
|
89
215
|
</div>
|
|
90
216
|
</template>
|
|
@@ -87,10 +87,10 @@ const isContactEvent = computed(() => {
|
|
|
87
87
|
}"
|
|
88
88
|
>
|
|
89
89
|
<div
|
|
90
|
-
class="flex
|
|
90
|
+
class="flex size-10 shrink-0 items-center justify-center rounded-full"
|
|
91
91
|
:class="[eventConfig.color]"
|
|
92
92
|
>
|
|
93
|
-
<NexxtIcon :name="eventConfig.icon as any" class="flex
|
|
93
|
+
<NexxtIcon :name="eventConfig.icon as any" class="flex size-6 text-xl text-white" />
|
|
94
94
|
</div>
|
|
95
95
|
<div
|
|
96
96
|
class="flex w-full flex-col gap-2 rounded-xl border border-gray-200 p-8"
|
|
@@ -108,7 +108,7 @@ const isContactEvent = computed(() => {
|
|
|
108
108
|
<span class="truncate group-hover:underline">
|
|
109
109
|
{{ event.description || event.url }}
|
|
110
110
|
</span>
|
|
111
|
-
<NexxtIcon name="external-link" class="ml-1
|
|
111
|
+
<NexxtIcon name="external-link" class="ml-1 size-4 text-gray-950" />
|
|
112
112
|
</a>
|
|
113
113
|
<span v-else-if="event.description" class="mt-1 text-sm text-gray-500">
|
|
114
114
|
{{ event.description }}
|
|
@@ -30,7 +30,7 @@ withDefaults(
|
|
|
30
30
|
<span class="flex gap-1">
|
|
31
31
|
{{ leftLabel }}
|
|
32
32
|
<NexxtTooltip v-if="leftTooltip">
|
|
33
|
-
<NexxtIcon name="circle-info" class="
|
|
33
|
+
<NexxtIcon name="circle-info" class="size-4 text-xs text-gray-600" />
|
|
34
34
|
<template #tooltip
|
|
35
35
|
><span class="text-sm">{{ leftTooltip }}</span></template
|
|
36
36
|
>
|
|
@@ -39,7 +39,7 @@ withDefaults(
|
|
|
39
39
|
<span class="flex gap-1">
|
|
40
40
|
{{ rightLabel }}
|
|
41
41
|
<NexxtTooltip v-if="rightTooltip">
|
|
42
|
-
<NexxtIcon name="circle-info" class="
|
|
42
|
+
<NexxtIcon name="circle-info" class="size-4 text-xs text-gray-600" />
|
|
43
43
|
<template #tooltip
|
|
44
44
|
><span class="text-sm">{{ rightTooltip }}</span></template
|
|
45
45
|
>
|
|
@@ -48,6 +48,14 @@ const VARIANT_CONFIG: Record<
|
|
|
48
48
|
},
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Full class names so Tailwind and the no-unknown-classes lint rule see them.
|
|
52
|
+
const VARIANT_CLASSES: Record<ToastVariant, string> = {
|
|
53
|
+
info: 'toast-info',
|
|
54
|
+
success: 'toast-success',
|
|
55
|
+
warning: 'toast-warning',
|
|
56
|
+
error: 'toast-error',
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
const { variant = 'info', dismissible = true, autoClose = 0 } = defineProps<NexxtToastProps>()
|
|
52
60
|
const emit = defineEmits<{ dismiss: [] }>()
|
|
53
61
|
|
|
@@ -94,13 +102,13 @@ onUnmounted(() => {
|
|
|
94
102
|
:aria-live="variant === 'error' ? 'assertive' : 'polite'"
|
|
95
103
|
aria-atomic="true"
|
|
96
104
|
class="toast flex w-100 gap-4 rounded-xl bg-white shadow-md"
|
|
97
|
-
:class="
|
|
105
|
+
:class="VARIANT_CLASSES[variant]"
|
|
98
106
|
@mouseenter="autoClose ? pause() : undefined"
|
|
99
107
|
@mouseleave="autoClose ? resume() : undefined"
|
|
100
108
|
>
|
|
101
109
|
<div
|
|
102
110
|
aria-hidden="true"
|
|
103
|
-
class="flex
|
|
111
|
+
class="flex size-8 shrink-0 items-center justify-center rounded-full text-white"
|
|
104
112
|
:class="selectedVariant.iconBg"
|
|
105
113
|
>
|
|
106
114
|
<NexxtIcon :name="selectedVariant.icon" class="text-xl" />
|
|
@@ -115,7 +123,7 @@ onUnmounted(() => {
|
|
|
115
123
|
<button
|
|
116
124
|
v-if="dismissible"
|
|
117
125
|
type="button"
|
|
118
|
-
class="relative flex
|
|
126
|
+
class="relative flex size-8 shrink-0 cursor-pointer items-center justify-center focus:outline-none"
|
|
119
127
|
:aria-label="
|
|
120
128
|
autoClose ? `Sluiten (verdwijnt automatisch na ${autoClose / 1000} seconden)` : 'Sluiten'
|
|
121
129
|
"
|
|
@@ -137,7 +137,7 @@ defineExpose({
|
|
|
137
137
|
:aria-label="format(day, 'd MMMM yyyy', { locale })"
|
|
138
138
|
:tabindex="isSameMonth(day, viewDate) ? 0 : -1"
|
|
139
139
|
:class="[
|
|
140
|
-
'relative flex
|
|
140
|
+
'relative flex size-8 items-center justify-center rounded-xl text-xs transition-colors duration-150 outline-none focus:ring-2 focus:ring-cornflower-blue-500 focus:ring-offset-1',
|
|
141
141
|
!isSameMonth(day, viewDate) ? 'text-gray-300' : 'text-gray-900',
|
|
142
142
|
isSelected(day) ? 'bg-cornflower-blue-500 font-medium text-white!' : '',
|
|
143
143
|
isTodayByTimezone(day) && !isSelected(day)
|
|
@@ -158,7 +158,7 @@ defineExpose({
|
|
|
158
158
|
<span
|
|
159
159
|
v-if="isMarked(day)"
|
|
160
160
|
:class="[
|
|
161
|
-
'absolute bottom-1 left-1/2
|
|
161
|
+
'absolute bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full',
|
|
162
162
|
isSelected(day) ? 'bg-white' : 'bg-cornflower-blue-500',
|
|
163
163
|
]"
|
|
164
164
|
/>
|