@nexxtmove/ui 1.2.7 → 1.2.8
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/Icon/Icon.vue.d.ts +3 -1
- package/dist/components/Icon/customIcons.d.ts +4 -0
- package/dist/components/Tabs/Tabs.vue.d.ts +6 -1
- package/dist/index.js +509 -473
- package/package.json +1 -1
- package/src/assets/svg/funda.svg +4 -0
- package/src/components/Icon/Icon.vue +17 -3
- package/src/components/Icon/customIcons.ts +8 -0
- package/src/components/Tabs/Tabs.vue +49 -25
package/package.json
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M20.8008 14.123C19.6828 18.3033 15.8964 21.2118 11.5684 21.2119H0.0214844L3.24121 9.16992C4.35937 4.98987 8.14668 2.0822 12.4736 2.08203H24.0215L20.8008 14.123ZM12.4756 3.97168C9.00358 3.97188 5.96564 6.30521 5.06836 9.65918L2.4834 19.3232H11.5664C15.0385 19.3232 18.0763 16.9897 18.9736 13.6357L21.5586 3.97168H12.4756Z" fill="currentColor"/>
|
|
3
|
+
<path d="M14.7658 6.12038C14.7705 6.10214 14.7571 6.08487 14.7389 6.08487H13.8109C13.1909 6.08487 12.6688 6.13669 12.2456 6.24034C11.8319 6.34399 11.4989 6.52249 11.2446 6.77585C10.9999 7.02057 10.8214 7.34878 10.7091 7.76241C10.6055 8.17602 10.5536 8.69334 10.5536 9.31326V9.5532C10.5536 9.56856 10.5412 9.58104 10.5258 9.58104H9.32719C9.31183 9.58104 9.29935 9.59352 9.29935 9.60888V11.3008C9.29935 11.3162 9.31183 11.3287 9.32719 11.3287H10.5258C10.5412 11.3287 10.5536 11.3411 10.5536 11.3564V17.1655C10.5536 17.1808 10.5661 17.1933 10.5815 17.1933H12.4567C12.4721 17.1933 12.4846 17.1808 12.4846 17.1655V11.3564C12.4846 11.3411 12.497 11.3287 12.5124 11.3287H14.2888C14.3041 11.3287 14.3166 11.3162 14.3166 11.3008V9.60888C14.3166 9.59352 14.3041 9.58104 14.2888 9.58104H12.5124C12.497 9.58104 12.4846 9.56856 12.4846 9.5532V9.21444C12.4846 8.92266 12.499 8.69238 12.5267 8.52342C12.5546 8.34492 12.6208 8.20866 12.7244 8.11464C12.8377 8.01097 12.9922 7.94572 13.1899 7.91692C13.3876 7.87949 13.6553 7.8603 13.9932 7.8603H14.2955C14.308 7.8603 14.3195 7.85167 14.3224 7.83919L14.7648 6.11942L14.7658 6.12038Z" fill="currentColor"/>
|
|
4
|
+
</svg>
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
import { computed } from 'vue'
|
|
3
3
|
import type { IconName } from '@awesome.me/kit-37b149f3ef/icons'
|
|
4
4
|
import { brandIconNames } from './brandIconNames'
|
|
5
|
+
import { customIcons, type CustomIconName } from './customIcons'
|
|
5
6
|
|
|
6
7
|
export type IconType = 'regular' | 'solid' | 'light' | 'duotone'
|
|
8
|
+
// every name Icon accepts: the Font Awesome kit plus the local SVGs
|
|
9
|
+
export type NexxtIconName = IconName | CustomIconName
|
|
7
10
|
|
|
8
11
|
interface NexxtIconProps {
|
|
9
|
-
name:
|
|
12
|
+
name: NexxtIconName
|
|
10
13
|
type?: IconType
|
|
11
14
|
}
|
|
12
15
|
|
|
@@ -16,9 +19,20 @@ defineOptions({
|
|
|
16
19
|
|
|
17
20
|
const props = defineProps<NexxtIconProps>()
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
// brand icons without a Font Awesome equivalent, sized and coloured like the font ones
|
|
23
|
+
const custom = computed(() => customIcons[props.name as CustomIconName])
|
|
24
|
+
|
|
25
|
+
const type = computed(
|
|
26
|
+
() => props.type ?? (brandIconNames.has(props.name as IconName) ? 'brands' : 'light'),
|
|
27
|
+
)
|
|
20
28
|
</script>
|
|
21
29
|
|
|
22
30
|
<template>
|
|
23
|
-
<
|
|
31
|
+
<component
|
|
32
|
+
:is="custom"
|
|
33
|
+
v-if="custom"
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
class="inline-block size-[1em] fill-current align-[-0.125em]"
|
|
36
|
+
/>
|
|
37
|
+
<i v-else :class="[`fa-${type}`, `fa-${name}`, 'items-center', 'justify-center']" />
|
|
24
38
|
</template>
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { ref, useTemplateRef, watch, onMounted, nextTick } from 'vue'
|
|
2
|
+
import { computed, ref, useTemplateRef, watch, onMounted, nextTick } from 'vue'
|
|
3
|
+
import Icon, { type NexxtIconName } from '../Icon/Icon.vue'
|
|
4
|
+
|
|
5
|
+
export interface NexxtTabItem {
|
|
6
|
+
label: string
|
|
7
|
+
icon?: NexxtIconName
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
interface NexxtTabs {
|
|
5
|
-
tabs: string[]
|
|
11
|
+
tabs: (string | NexxtTabItem)[]
|
|
6
12
|
tablistLabel?: string
|
|
7
13
|
}
|
|
8
14
|
|
|
@@ -20,9 +26,14 @@ const sliderStyle = ref({ width: '0px', transform: 'translateX(0px)' })
|
|
|
20
26
|
const clipPath = ref('inset(0 100% 0 0 round 9999px)')
|
|
21
27
|
const ready = ref(false)
|
|
22
28
|
|
|
29
|
+
// plain strings and { label, icon } objects are both accepted; work with the object form
|
|
30
|
+
const items = computed<NexxtTabItem[]>(() =>
|
|
31
|
+
props.tabs.map((tab) => (typeof tab === 'string' ? { label: tab } : tab)),
|
|
32
|
+
)
|
|
33
|
+
|
|
23
34
|
const updateSlider = async () => {
|
|
24
35
|
await nextTick()
|
|
25
|
-
const index =
|
|
36
|
+
const index = items.value.findIndex((item) => item.label === selected.value)
|
|
26
37
|
const button = buttonRefs.value[index]
|
|
27
38
|
if (!button || !containerRef.value) return
|
|
28
39
|
|
|
@@ -40,23 +51,28 @@ const updateSlider = async () => {
|
|
|
40
51
|
|
|
41
52
|
const onKeydown = (event: KeyboardEvent, index: number) => {
|
|
42
53
|
let next: number
|
|
43
|
-
if (event.key === 'ArrowRight') next = (index + 1) %
|
|
44
|
-
else if (event.key === 'ArrowLeft') next = (index - 1 +
|
|
54
|
+
if (event.key === 'ArrowRight') next = (index + 1) % items.value.length
|
|
55
|
+
else if (event.key === 'ArrowLeft') next = (index - 1 + items.value.length) % items.value.length
|
|
45
56
|
else if (event.key === 'Home') next = 0
|
|
46
|
-
else if (event.key === 'End') next =
|
|
57
|
+
else if (event.key === 'End') next = items.value.length - 1
|
|
47
58
|
else return
|
|
48
59
|
|
|
49
60
|
event.preventDefault()
|
|
50
|
-
selected.value =
|
|
61
|
+
selected.value = items.value[next].label
|
|
51
62
|
buttonRefs.value[next]?.focus()
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
watch(selected, async () => await updateSlider())
|
|
55
66
|
|
|
67
|
+
// the overlay copy must stay the same width as the button, so re-measure when the tabs change
|
|
68
|
+
watch(items, async () => await updateSlider())
|
|
69
|
+
|
|
56
70
|
onMounted(async () => {
|
|
57
71
|
await document.fonts.ready
|
|
58
72
|
await updateSlider()
|
|
59
73
|
await nextTick()
|
|
74
|
+
|
|
75
|
+
void containerRef.value?.offsetWidth
|
|
60
76
|
ready.value = true
|
|
61
77
|
})
|
|
62
78
|
</script>
|
|
@@ -66,47 +82,55 @@ onMounted(async () => {
|
|
|
66
82
|
ref="containerRef"
|
|
67
83
|
role="tablist"
|
|
68
84
|
:aria-label="tablistLabel"
|
|
69
|
-
class="relative isolate flex rounded-full
|
|
85
|
+
class="relative isolate flex rounded-full bg-white p-1"
|
|
70
86
|
>
|
|
71
|
-
<!-- slider pill -->
|
|
87
|
+
<!-- slider pill; anchored to the padding box, the origin button.offsetLeft is measured from -->
|
|
72
88
|
<div
|
|
73
89
|
aria-hidden="true"
|
|
74
|
-
class="absolute
|
|
90
|
+
class="absolute inset-y-1 left-0 rounded-full bg-cornflower-blue-500"
|
|
75
91
|
:class="ready ? 'transition-all duration-500 ease-[cubic-bezier(0.34,1.2,0.64,1)]' : ''"
|
|
76
92
|
:style="sliderStyle"
|
|
77
93
|
/>
|
|
78
94
|
|
|
79
95
|
<!-- dark text buttons -->
|
|
80
96
|
<button
|
|
81
|
-
v-for="(
|
|
82
|
-
:key="
|
|
97
|
+
v-for="(item, index) in items"
|
|
98
|
+
:key="item.label"
|
|
83
99
|
:ref="(el) => (buttonRefs[index] = el as HTMLButtonElement)"
|
|
84
100
|
role="tab"
|
|
85
|
-
:aria-selected="selected ===
|
|
86
|
-
:tabindex="selected ===
|
|
87
|
-
class="relative h-9 cursor-pointer rounded-full px-4
|
|
88
|
-
:class="
|
|
89
|
-
|
|
101
|
+
:aria-selected="selected === item.label"
|
|
102
|
+
:tabindex="selected === item.label ? 0 : -1"
|
|
103
|
+
class="relative z-10 inline-flex h-9 cursor-pointer items-center gap-2 rounded-full px-4 small-normal"
|
|
104
|
+
:class="[
|
|
105
|
+
// the dark label is what the pill wipe reveals, so it only drops out once the pill
|
|
106
|
+
// has arrived — otherwise its anti-aliased edges halo around the white copy
|
|
107
|
+
selected === item.label ? 'text-transparent' : 'text-gray-950',
|
|
108
|
+
ready ? 'transition-colors duration-0' : '',
|
|
109
|
+
selected === item.label ? 'delay-500' : 'delay-0',
|
|
110
|
+
]"
|
|
111
|
+
@click="selected = item.label"
|
|
90
112
|
@keydown="onKeydown($event, index)"
|
|
91
113
|
>
|
|
92
|
-
|
|
114
|
+
<Icon v-if="item.icon" :name="item.icon" aria-hidden="true" class="body-normal" />
|
|
115
|
+
{{ item.label }}
|
|
93
116
|
</button>
|
|
94
117
|
|
|
95
|
-
<!-- white text overlay, clipped to slider bounds -->
|
|
118
|
+
<!-- white text overlay, clipped to slider bounds; must mirror the buttons exactly -->
|
|
96
119
|
<div
|
|
97
120
|
aria-hidden="true"
|
|
98
|
-
class="pointer-events-none absolute inset-0 z-20 flex"
|
|
121
|
+
class="pointer-events-none absolute inset-0 z-20 flex p-1"
|
|
99
122
|
:class="
|
|
100
|
-
ready ? 'transition-[clip-path] duration-
|
|
123
|
+
ready ? 'transition-[clip-path] duration-300 ease-[cubic-bezier(0.34,1.2,0.64,1)]' : ''
|
|
101
124
|
"
|
|
102
125
|
:style="{ clipPath }"
|
|
103
126
|
>
|
|
104
127
|
<span
|
|
105
|
-
v-for="
|
|
106
|
-
:key="
|
|
107
|
-
class="flex h-9 items-center rounded-full px-4
|
|
128
|
+
v-for="item in items"
|
|
129
|
+
:key="item.label"
|
|
130
|
+
class="flex h-9 items-center gap-2 rounded-full px-4 small-normal text-white"
|
|
108
131
|
>
|
|
109
|
-
|
|
132
|
+
<Icon v-if="item.icon" :name="item.icon" class="body-normal" />
|
|
133
|
+
{{ item.label }}
|
|
110
134
|
</span>
|
|
111
135
|
</div>
|
|
112
136
|
</div>
|