@nexxtmove/ui 1.2.6 → 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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nexxtmove/ui",
3
3
  "type": "module",
4
- "version": "1.2.6",
4
+ "version": "1.2.8",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -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: IconName
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
- const type = computed(() => props.type ?? (brandIconNames.has(props.name) ? 'brands' : 'light'))
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
- <i :class="[`fa-${type}`, `fa-${name}`, 'items-center', 'justify-center']" />
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>
@@ -0,0 +1,8 @@
1
+ import FundaSvg from '@ui-assets/svg/funda.svg'
2
+
3
+ // Brand icons that Font Awesome doesn't ship, rendered from local SVGs instead.
4
+ export const customIcons = {
5
+ funda: FundaSvg,
6
+ }
7
+
8
+ export type CustomIconName = keyof typeof customIcons
@@ -10,6 +10,7 @@ export interface ProposalReference {
10
10
  postalCode: string
11
11
  streetName: string
12
12
  houseNumber: number
13
+ houseNumberExt?: string | number | null
13
14
  houseType: string
14
15
  city: string
15
16
  mainImageUrl: string | null
@@ -65,7 +66,16 @@ const selected = defineModel<boolean>({ default: false })
65
66
 
66
67
  defineEmits<{ details: [reference: ProposalReference] }>()
67
68
 
68
- const address = computed(() => `${reference.streetName} ${reference.houseNumber}`)
69
+ const formattedHouseNumberExtension = (value: string | number | null | undefined) => {
70
+ const extension = String(value ?? '').trim()
71
+
72
+ return extension ? (extension.startsWith('-') ? extension : `-${extension}`) : ''
73
+ }
74
+
75
+ const address = computed(
76
+ () =>
77
+ `${reference.streetName} ${reference.houseNumber}${formattedHouseNumberExtension(reference.houseNumberExt)}`,
78
+ )
69
79
 
70
80
  const location = computed(() => `${reference.postalCode} ${reference.city}`)
71
81
 
@@ -74,7 +84,13 @@ const energyLabel = computed(() => (reference.energyLabel ?? null) as EnergyLabe
74
84
  const salePrice = computed(() => {
75
85
  if (!reference.salePrice) return null
76
86
 
77
- const amount = Number(reference.salePrice)
87
+ const normalized = String(reference.salePrice)
88
+ .replace(/[^\d,-]/g, '')
89
+ .replace(/\./g, '')
90
+ .replace(',', '.')
91
+ if (!normalized) return reference.salePrice
92
+
93
+ const amount = Number(normalized)
78
94
  if (Number.isNaN(amount)) return reference.salePrice
79
95
 
80
96
  return new Intl.NumberFormat(locale, {
@@ -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 = props.tabs.indexOf(selected.value)
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) % props.tabs.length
44
- else if (event.key === 'ArrowLeft') next = (index - 1 + props.tabs.length) % props.tabs.length
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 = props.tabs.length - 1
57
+ else if (event.key === 'End') next = items.value.length - 1
47
58
  else return
48
59
 
49
60
  event.preventDefault()
50
- selected.value = props.tabs[next]
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 border-4 border-white bg-white"
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 h-9 rounded-full bg-cornflower-blue-500"
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="(tab, index) in tabs"
82
- :key="tab"
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 === tab"
86
- :tabindex="selected === tab ? 0 : -1"
87
- class="relative h-9 cursor-pointer rounded-full px-4 text-sm text-gray-950 transition-none"
88
- :class="selected === tab ? 'z-10' : 'z-30'"
89
- @click="selected = tab"
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
- {{ tab }}
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-500 ease-[cubic-bezier(0.34,1.2,0.64,1)]' : ''
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="tab in tabs"
106
- :key="tab"
107
- class="flex h-9 items-center rounded-full px-4 text-sm text-white"
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
- {{ tab }}
132
+ <Icon v-if="item.icon" :name="item.icon" class="body-normal" />
133
+ {{ item.label }}
110
134
  </span>
111
135
  </div>
112
136
  </div>