@iqual/nuxt-icms 1.3.0 → 1.4.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.
Files changed (122) hide show
  1. package/assets/symbols/chevron-right.svg +1 -1
  2. package/components/Blokkli/HeroImage/index.vue +1 -1
  3. package/components/Blokkli/Teasers/icon.svg +3 -3
  4. package/components/DesignSystem/Atoms/Button.vue +86 -36
  5. package/components/DesignSystem/Atoms/Caption.vue +10 -10
  6. package/components/DesignSystem/Atoms/Container.vue +10 -6
  7. package/components/DesignSystem/Atoms/GridRow.vue +4 -3
  8. package/components/DesignSystem/Atoms/LinkListItem.vue +31 -8
  9. package/components/DesignSystem/Molecules/ButtonGroup.vue +18 -3
  10. package/components/DesignSystem/Molecules/Card.vue +86 -31
  11. package/components/DesignSystem/Molecules/IconText.vue +37 -16
  12. package/components/DesignSystem/Molecules/MediaLightbox.vue +213 -58
  13. package/components/DesignSystem/Molecules/MediaSlider.vue +88 -25
  14. package/components/DesignSystem/Molecules/Partnership.vue +40 -9
  15. package/components/DesignSystem/Molecules/Table.vue +90 -25
  16. package/components/DesignSystem/Molecules/TitleComposition.vue +15 -12
  17. package/components/DesignSystem/Organisms/ContentBlock.vue +61 -15
  18. package/components/DesignSystem/Organisms/FacetedListing.vue +170 -0
  19. package/components/DesignSystem/Organisms/FlipTeaser.vue +119 -47
  20. package/components/DesignSystem/Organisms/HeaderHero.vue +167 -0
  21. package/components/DesignSystem/Organisms/HeaderTextImage.vue +71 -0
  22. package/components/DesignSystem/Organisms/LogoElement.vue +33 -10
  23. package/components/DesignSystem/Organisms/Map.vue +11 -6
  24. package/components/DesignSystem/Organisms/SearchResults.vue +41 -0
  25. package/components/DesignSystem/Organisms/TeamElement.vue +84 -20
  26. package/components/DesignSystem/Organisms/TeaserCard.vue +87 -34
  27. package/components/DesignSystem/Organisms/Testimonial.vue +82 -12
  28. package/components/DesignSystem/Organisms/TimelineElement.vue +42 -12
  29. package/components/DesignSystem/Organisms/TwoColumnTextElement.vue +44 -9
  30. package/components/DesignSystem/Pages/Search.vue +39 -0
  31. package/components/Drupal/Breadcrumb/index.vue +16 -5
  32. package/components/Drupal/Messages/index.vue +16 -5
  33. package/components/Drupal/Webform/fragment.graphql +8 -5
  34. package/components/Drupal/Webform/index.vue +20 -9
  35. package/components/Drupal/Webform/query.graphql +18 -16
  36. package/components/Form/CheckboxInput.vue +6 -6
  37. package/components/Form/CheckboxInputs.vue +5 -5
  38. package/components/Form/FileUploadInput.vue +6 -6
  39. package/components/Form/RadioButtonsInput.vue +5 -5
  40. package/components/Form/SelectInput.vue +7 -7
  41. package/components/Form/SubmitButton.vue +1 -1
  42. package/components/Form/TextAreaInput.vue +2 -2
  43. package/components/Form/TextEmailInput.vue +2 -2
  44. package/components/Form/TextInput.vue +2 -2
  45. package/components/Form/TextPhoneInput.vue +2 -2
  46. package/components/Form/composables/useFormElement.ts +5 -5
  47. package/components/Form/utils/types.ts +8 -7
  48. package/components/Media/Icon/index.vue +10 -6
  49. package/components/Media/Image/index.vue +26 -20
  50. package/components/Media/RemoteVideo/index.vue +34 -21
  51. package/components/Media/Video/index.vue +6 -5
  52. package/components/Node/Icms/Page/Teaser/index.vue +6 -4
  53. package/components/Node/Icms/Page/index.vue +2 -2
  54. package/components/Paragraph/Button/Element/index.vue +9 -2
  55. package/components/Paragraph/CallToAction/index.vue +43 -10
  56. package/components/Paragraph/Card/Element/index.vue +2 -1
  57. package/components/Paragraph/Card/Layout/index.vue +29 -10
  58. package/components/Paragraph/Contact/index.vue +41 -12
  59. package/components/Paragraph/Downloads/index.vue +82 -33
  60. package/components/Paragraph/FAQ/Element/index.vue +41 -12
  61. package/components/Paragraph/FAQ/Layout/index.vue +47 -13
  62. package/components/Paragraph/FlipTeasers/index.vue +26 -6
  63. package/components/Paragraph/Gallery/index.vue +42 -10
  64. package/components/Paragraph/HeroImage/index.vue +62 -16
  65. package/components/Paragraph/IconText/Element/index.vue +7 -1
  66. package/components/Paragraph/IconText/Layout/index.vue +39 -12
  67. package/components/Paragraph/Introduction/index.vue +50 -15
  68. package/components/Paragraph/Logo/Element/fragment.graphql +1 -0
  69. package/components/Paragraph/Logo/Element/index.vue +7 -2
  70. package/components/Paragraph/Media/index.vue +32 -31
  71. package/components/Paragraph/Partnerships/Element/fragment.graphql +1 -0
  72. package/components/Paragraph/Partnerships/Element/index.vue +7 -0
  73. package/components/Paragraph/Partnerships/Layout/index.vue +100 -27
  74. package/components/Paragraph/SingleTestimonial/index.vue +58 -15
  75. package/components/Paragraph/Table/index.vue +32 -9
  76. package/components/Paragraph/Tabs/Element/index.vue +44 -10
  77. package/components/Paragraph/Tabs/Layout/index.vue +123 -37
  78. package/components/Paragraph/Team/Element/index.vue +18 -1
  79. package/components/Paragraph/Team/Layout/index.vue +30 -7
  80. package/components/Paragraph/Teasers/index.vue +33 -7
  81. package/components/Paragraph/Testimonial/Element/index.vue +30 -5
  82. package/components/Paragraph/Testimonial/Layout/index.vue +78 -24
  83. package/components/Paragraph/Text/index.vue +32 -10
  84. package/components/Paragraph/TextMedia/index.vue +44 -13
  85. package/components/Paragraph/Timeline/Element/index.vue +23 -7
  86. package/components/Paragraph/Timeline/Layout/index.vue +232 -67
  87. package/components/Paragraph/TwoColumnText/Element/index.vue +20 -3
  88. package/components/Paragraph/TwoColumnText/Layout/index.vue +8 -1
  89. package/components/Paragraph/iFrame/index.vue +31 -11
  90. package/components/Search/Facets/FacetDropdownUI.vue +351 -77
  91. package/components/Search/Facets/FacetedSearchUI.vue +257 -101
  92. package/components/Search/Facets/fragment.graphql +5 -1
  93. package/components/Search/Input/index.vue +125 -0
  94. package/components/Section/Footer/index.vue +170 -30
  95. package/components/Section/Header/Buttons.vue +34 -0
  96. package/components/Section/Header/DesktopMainMenu.vue +160 -0
  97. package/components/Section/Header/DesktopMainMenuDropdown.vue +170 -0
  98. package/components/Section/Header/LanguageDropdown.vue +95 -64
  99. package/components/Section/Header/LanguageLinks.vue +39 -0
  100. package/components/Section/Header/Logo.vue +23 -0
  101. package/components/Section/Header/MainButtons.vue +31 -0
  102. package/components/Section/Header/MenuIcon.vue +60 -49
  103. package/components/Section/Header/MetaButtons.vue +31 -0
  104. package/components/Section/Header/MetaMenu.vue +55 -53
  105. package/components/Section/Header/MobileMainMenu.vue +23 -0
  106. package/components/Section/Header/MobileMainMenuItem.vue +191 -0
  107. package/components/Section/Header/index.vue +182 -186
  108. package/composables/useMenu.ts +2 -2
  109. package/composables/useMobileMenuOverlayBus.ts +44 -0
  110. package/composables/useNavigationOffset.ts +2 -2
  111. package/composables/useTheme.ts +107 -0
  112. package/formkit.theme.ts +541 -586
  113. package/layouts/default.vue +7 -35
  114. package/layouts/error.vue +7 -32
  115. package/nuxt.config.ts +13 -0
  116. package/package.json +1 -1
  117. package/plugins/initData.ts +8 -5
  118. package/queries/query.initData.graphql +4 -2
  119. package/tailwind.config.ts +11 -11
  120. package/types/global.ts +1 -0
  121. package/components/Section/Header/GlobalMenu.vue +0 -335
  122. package/components/Section/Header/LanguageSwitcher.vue +0 -37
@@ -1,216 +1,212 @@
1
1
  <template>
2
2
  <header
3
- ref="container"
4
- class="z-30 flex flex-col bg-white"
5
- :class="{
6
- 'max-h-[70px] !overflow-y-hidden 2xl:max-h-screen 2xl:!overflow-y-visible':
7
- !isMenuOpen,
8
- 'max-h-screen overflow-y-auto': isMenuOpen,
9
- 'fixed size-full max-h-dvh': isGlobalSticky,
10
- sticky: !isGlobalSticky,
3
+ :style="{
4
+ '--meta-height': metaHeight + 'px',
11
5
  }"
12
- :style="computedStyle"
6
+ :class="
7
+ themedClass(
8
+ 'headerClass',
9
+ 'sticky top-0 z-[101] 2xl:-top-[var(--meta-height)]',
10
+ )
11
+ "
13
12
  >
14
- <!-- Meta menu full-width container -->
15
- <!-- Hidden on mobile -->
16
- <div class="border-b-1 hidden border-b-gray-100 2xl:block">
17
- <!-- Meta menu container -->
18
- <DesignSystemAtomsGridRow
19
- :class="'w-full max-w-full 2xl:max-w-screen-2xl'"
20
- >
21
- <div class="col-span-12">
13
+ <!-- Desktop Meta Container -->
14
+ <DesignSystemAtomsContainer
15
+ :spacing="{ top: 'none', bottom: 'none' }"
16
+ :class="
17
+ themedClass(
18
+ 'metaContainerClass',
19
+ 'hidden border-b border-gray-100 bg-white 2xl:block',
20
+ )
21
+ "
22
+ >
23
+ <div ref="metaContainer">
24
+ <DesignSystemAtomsGridRow
25
+ :class="
26
+ themedClass(
27
+ 'metaRowClass',
28
+ 'flex items-center justify-end gap-8 py-2',
29
+ )
30
+ "
31
+ >
32
+ <div :class="themedClass('metaMenuContainer')">
33
+ <SectionHeaderMetaMenu />
34
+ </div>
35
+ <div :class="themedClass('metaButtonsContainer')">
36
+ <SectionHeaderButtons :buttons="metaLinks" />
37
+ </div>
22
38
  <div
23
- ref="metaRef"
24
- class="flex max-w-full items-center justify-end py-4"
39
+ :class="
40
+ themedClass(
41
+ 'languageDropdownContainer',
42
+ 'flex h-full items-center',
43
+ )
44
+ "
25
45
  >
26
- <!-- Meta menu -->
27
- <SectionHeaderMetaMenu :links="metaLinks" />
28
-
29
- <!-- Desktop Language dropdown -->
30
- <SectionHeaderLanguageDropdown
31
- class="hidden h-full 2xl:block"
32
- @lang-dropdown:change="closeMenu"
33
- />
46
+ <SectionHeaderLanguageDropdown />
34
47
  </div>
48
+ </DesignSystemAtomsGridRow>
49
+ </div>
50
+ </DesignSystemAtomsContainer>
51
+
52
+ <!-- Main Container -->
53
+ <DesignSystemAtomsContainer
54
+ :spacing="{ top: 'none', bottom: 'none' }"
55
+ :class="
56
+ themedClass('mainContainerClass', 'border-b border-gray-100 bg-white')
57
+ "
58
+ >
59
+ <DesignSystemAtomsGridRow
60
+ :class="
61
+ themedClass('mainRowClass', 'flex h-14 items-center gap-8 2xl:h-20')
62
+ "
63
+ >
64
+ <div :class="themedClass('logoContainer', 'shrink-0')">
65
+ <SectionHeaderLogo />
66
+ </div>
67
+ <div
68
+ :class="
69
+ themedClass(
70
+ 'mainMenuContainer',
71
+ 'ml-auto hidden 2xl:flex 2xl:h-full 2xl:items-center',
72
+ )
73
+ "
74
+ >
75
+ <SectionHeaderDesktopMainMenu />
76
+ </div>
77
+ <div :class="themedClass('mainButtonsContainer', 'hidden 2xl:block')">
78
+ <SectionHeaderButtons :buttons="contactButtons" />
79
+ </div>
80
+ <div
81
+ :class="
82
+ themedClass(
83
+ 'mainMenuIconContainer',
84
+ 'ml-auto flex items-center justify-center 2xl:hidden',
85
+ )
86
+ "
87
+ >
88
+ <SectionHeaderMenuIcon :is-in-closable-state="isMobileMenuOpen" />
35
89
  </div>
36
90
  </DesignSystemAtomsGridRow>
37
- </div>
38
-
39
- <!-- Main menu full-width container -->
40
- <div class="border-b-1 h-[70px] shrink-0 border-b-gray-100 2xl:h-[110px]">
41
- <!-- Main menu container -->
42
- <div
43
- class="flex h-full max-w-full items-center justify-between 2xl:justify-start"
91
+ </DesignSystemAtomsContainer>
92
+
93
+ <!-- Mobile Menu Overlay -->
94
+ <Teleport to="body">
95
+ <Transition
96
+ @after-enter="clickAwayArmed = true"
97
+ @after-leave="clickAwayArmed = false"
98
+ :enter-active-class="
99
+ themedClass(
100
+ 'transitionEnterActiveClass',
101
+ 'transition-opacity duration-200 md:motion-safe:transition-transform md:motion-safe:duration-300',
102
+ )
103
+ "
104
+ :enter-from-class="
105
+ themedClass(
106
+ 'transitionEnterFromClass',
107
+ 'opacity-0 md:opacity-100 md:translate-x-full',
108
+ )
109
+ "
110
+ :enter-to-class="
111
+ themedClass(
112
+ 'transitionEnterToClass',
113
+ 'opacity-100 md:opacity-100 md:translate-x-0',
114
+ )
115
+ "
116
+ :leave-active-class="
117
+ themedClass(
118
+ 'transitionLeaveActiveClass',
119
+ 'transition-opacity duration-200 md:motion-safe:transition-transform md:motion-safe:duration-300',
120
+ )
121
+ "
122
+ :leave-from-class="
123
+ themedClass(
124
+ 'transitionLeaveFromClass',
125
+ 'opacity-100 md:opacity-100 md:translate-x-0',
126
+ )
127
+ "
128
+ :leave-to-class="
129
+ themedClass(
130
+ 'transitionLeaveToClass',
131
+ 'opacity-0 md:opacity-100 md:translate-x-full',
132
+ )
133
+ "
44
134
  >
45
- <DesignSystemAtomsGridRow
46
- :class="'w-full max-w-full 2xl:max-w-screen-2xl'"
135
+ <DesignSystemAtomsContainer
136
+ v-show="isMobileMenuOpen"
137
+ :spacing="{ top: 'none', bottom: 'none' }"
138
+ v-click-away="clickAwayArmed && close"
139
+ :class="
140
+ themedClass(
141
+ 'mobileMenuContainerClass',
142
+ 'fixed inset-0 z-[102] flex h-dvh items-start justify-center overflow-y-auto bg-white md:left-auto md:w-[420px]',
143
+ )
144
+ "
47
145
  >
48
- <div class="col-span-12 flex flex-nowrap justify-between">
49
- <!-- Logo -->
50
- <NuxtLink :to="`/${currentLanguage}`" :title="companyName" prefetch>
51
- <Media
52
- v-if="companyLogo"
53
- :media="companyLogo"
54
- mediaClass="h-10 2xl:h-16"
55
- />
56
- </NuxtLink>
57
-
58
- <!-- Desktop Main navigation menu -->
59
- <!-- Hidden on mobile -->
60
- <SectionHeaderGlobalMenu
61
- class="ml-auto hidden 2xl:block"
62
- @menu:link-click="closeMenu"
63
- />
64
-
65
- <!-- Desktop Contact button -->
66
- <!-- Hidden on mobile -->
146
+ <DesignSystemAtomsGridRow
147
+ :class="
148
+ themedClass(
149
+ 'mobileMenuRowClass',
150
+ 'flex w-full flex-col gap-10 pb-20',
151
+ )
152
+ "
153
+ >
67
154
  <div
68
- v-if="contactButtons?.length"
69
- class="ml-8 hidden items-center justify-center gap-8 2xl:flex"
155
+ :class="
156
+ themedClass(
157
+ 'mobileMenuHeaderContainer',
158
+ 'flex h-14 items-center',
159
+ )
160
+ "
70
161
  >
71
- <DesignSystemAtomsButton
72
- v-for="(contactButton, key) in contactButtons"
73
- :key="key"
74
- :label="contactButton?.label"
75
- :subline="contactButton?.subline"
76
- :title="contactButton?.title"
77
- :link="contactButton?.link?.uri?.path"
78
- :params="contactButton?.params"
79
- :targetBlank="contactButton?.targetBlank"
80
- :style="contactButton?.buttonStyle"
162
+ <SectionHeaderLogo
163
+ :class="themedClass('mobileMenuLogoClass', 'md:hidden')"
81
164
  />
165
+ <SectionHeaderMenuIcon :is-in-closable-state="isMobileMenuOpen" />
82
166
  </div>
83
-
84
- <!-- Hamburger menu -->
85
- <!-- Hidden on desktop -->
86
- <button
87
- class="flex items-center gap-3 leading-none 2xl:hidden"
88
- aria-label="Hamburger Menu"
89
- @click="toggleNav"
90
- >
91
- <span
92
- class="h-auto leading-none opacity-100"
93
- :class="{
94
- 'duration-250 opacity-0 transition-all ease-in-out':
95
- isMenuOpen,
96
- }"
97
- />
98
- <SectionHeaderMenuIcon :is-in-closable-state="isMenuOpen" />
99
- </button>
100
- </div>
101
- </DesignSystemAtomsGridRow>
102
- </div>
103
- </div>
104
-
105
- <!-- Mobile Main/Meta menu container -->
106
- <!-- Hidden on desktop -->
107
- <!-- Initially hidden by overflow when menu closed -->
108
- <div class="container w-full max-w-full 2xl:hidden">
109
- <!-- Mobile Main navigation menu -->
110
- <SectionHeaderGlobalMenu @menu:link-click="closeMenu" />
111
-
112
- <!-- Mobile Language dropdown -->
113
- <SectionHeaderLanguageSwitcher
114
- @lang-dropdown:change="closeMenu"
115
- class="mb-6"
116
- />
117
-
118
- <!-- Mobile Meta menu -->
119
- <SectionHeaderMetaMenu
120
- :links="metaLinks"
121
- @lang-dropdown:change="closeMenu"
122
- />
123
-
124
- <!-- Mobile Contact button -->
125
- <div v-if="contactButtons?.length" class="flex gap-8 2xl:ml-8">
126
- <DesignSystemAtomsButton
127
- v-for="(contactButton, key) in contactButtons"
128
- :key="key"
129
- :label="contactButton?.label"
130
- :subline="contactButton?.subline"
131
- :title="contactButton?.title"
132
- :link="contactButton?.link?.uri?.path"
133
- :params="contactButton?.params"
134
- :targetBlank="contactButton?.targetBlank"
135
- :style="contactButton?.buttonStyle"
136
- />
137
- </div>
138
- </div>
167
+ <SectionHeaderMobileMainMenu />
168
+ <SectionHeaderButtons :buttons="contactButtons" />
169
+ <SectionHeaderMetaMenu />
170
+ <SectionHeaderButtons :buttons="metaLinks" />
171
+ <SectionHeaderLanguageLinks />
172
+ </DesignSystemAtomsGridRow>
173
+ </DesignSystemAtomsContainer>
174
+ </Transition>
175
+ </Teleport>
139
176
  </header>
140
177
  </template>
141
178
 
142
179
  <script lang="ts" setup>
143
- const { companyLogo, companyName, contactButtons, metaLinks } =
144
- await useGlobalConfig()
145
-
146
- const container = ref<HTMLElement | null>(null)
147
- const metaRef = useTemplateRef('metaRef')
148
- const metaHeight = ref<number | null>()
149
- const isGlobalSticky = ref(false)
150
- const isMenuOpen = ref(false)
151
- const isMetaHidden = ref(false)
180
+ const { metaLinks, contactButtons } = await useGlobalConfig()
181
+ const themedClass = useTheme()
182
+ const { close, onOpen, onClose } = useMobileMenuOverlayBus()
152
183
  const viewport = useViewport()
153
- const { isMobile } = useIsMobile()
154
- const currentLanguage = useCurrentLanguage()
155
- const drupalUser = useDrupalUser()
156
- const isUserLoggedIn = drupalUser.value.accessToolbar
157
184
 
158
- onMounted(() => {
159
- if (metaRef.value) {
160
- metaHeight.value = metaRef.value.getBoundingClientRect().height
161
- }
162
- })
185
+ const isMobileMenuOpen = ref(false)
186
+ const clickAwayArmed = ref(false)
163
187
 
164
- const computedStyle = computed(() => {
165
- let style = null
166
- if (!isMobile.value && isUserLoggedIn) {
167
- style = { top: `-${metaHeight.value}px` }
168
- } else {
169
- style = { top: 0 }
170
- }
171
- return style
172
- })
173
-
174
- const emit = defineEmits([
175
- 'menu:open',
176
- 'menu:close:start',
177
- 'menu:close:finished',
178
- ])
179
-
180
- function toggleNav() {
181
- if (!isMenuOpen.value) {
182
- openMenu()
183
- } else {
184
- closeMenu()
185
- }
186
- }
187
-
188
- function openMenu() {
189
- emit('menu:open')
190
- isMenuOpen.value = true
191
- isGlobalSticky.value = true
192
- }
193
-
194
- function closeMenu() {
195
- emit('menu:close:start')
196
- isMenuOpen.value = false
197
- if (container.value) {
198
- container.value.scrollTop = 0
199
- }
188
+ const metaContainer = ref<HTMLElement | null>(null)
189
+ const metaHeight = ref(0)
200
190
 
201
- setTimeout(() => {
202
- emit('menu:close:finished')
203
- isGlobalSticky.value = false
204
- }, 250)
205
- }
191
+ onOpen(() => {
192
+ isMobileMenuOpen.value = true
193
+ document.body.classList.toggle('overflow-y-hidden', isMobileMenuOpen.value)
194
+ })
206
195
 
207
- function hideMeta() {
208
- isMetaHidden.value = true
209
- }
196
+ onClose(() => {
197
+ isMobileMenuOpen.value = false
198
+ document.body.classList.toggle('overflow-y-hidden', isMobileMenuOpen.value)
199
+ })
210
200
 
211
201
  watch(viewport.breakpoint, (newValue: string) => {
212
202
  if (newValue === '2xl') {
213
- isMenuOpen.value = false
203
+ isMobileMenuOpen.value = false
214
204
  }
215
205
  })
206
+
207
+ onMounted(() => {
208
+ requestAnimationFrame(() => {
209
+ metaHeight.value = metaContainer.value?.offsetHeight ?? 0
210
+ })
211
+ })
216
212
  </script>
@@ -48,7 +48,7 @@ export async function useMenu(menuName: MenuName) {
48
48
  return null
49
49
  })
50
50
 
51
- const menuTiemActiveLevel3 = computed(() => {
51
+ const menuItemActiveLevel3 = computed(() => {
52
52
  const activeSubtree = menuItemActiveLevel2.value?.children
53
53
  if (activeSubtree) {
54
54
  return activeSubtree.find((l) => isLinkTreeActive(l, currentRoute.path))
@@ -61,6 +61,6 @@ export async function useMenu(menuName: MenuName) {
61
61
  menuItemActive,
62
62
  menuItemActiveLevel1,
63
63
  menuItemActiveLevel2,
64
- menuTiemActiveLevel3,
64
+ menuItemActiveLevel3,
65
65
  }
66
66
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Mobile Menu Overlay Bus Composable
3
+ *
4
+ * Emits open/close events globally using a reactive event key.
5
+ */
6
+
7
+ import { ref, watch } from 'vue'
8
+
9
+ export enum MobileMenuOverlayEvent {
10
+ Open = 'open',
11
+ Close = 'close'
12
+ }
13
+
14
+ const mobileMenuOverlayBus = ref<MobileMenuOverlayEvent | null>(null)
15
+
16
+ function open() {
17
+ mobileMenuOverlayBus.value = MobileMenuOverlayEvent.Open
18
+ }
19
+
20
+ function close() {
21
+ mobileMenuOverlayBus.value = MobileMenuOverlayEvent.Close
22
+ }
23
+
24
+ function onOpen(callback: () => void) {
25
+ watch(mobileMenuOverlayBus, (val) => {
26
+ if (val === MobileMenuOverlayEvent.Open) callback()
27
+ })
28
+ }
29
+
30
+ function onClose(callback: () => void) {
31
+ watch(mobileMenuOverlayBus, (val) => {
32
+ if (val === MobileMenuOverlayEvent.Close) callback()
33
+ })
34
+ }
35
+
36
+ export function useMobileMenuOverlayBus() {
37
+ return {
38
+ bus: mobileMenuOverlayBus,
39
+ open,
40
+ close,
41
+ onOpen,
42
+ onClose,
43
+ }
44
+ }
@@ -1,7 +1,7 @@
1
1
  import { useIsMobile } from './useIsMobile'
2
2
 
3
- const mobileNavHeight: number = 73
4
- const desktopNavHeight: number = 114
3
+ const mobileNavHeight: number = 60 // actual height + 4
4
+ const desktopNavHeight: number = 85 // actual height + 4
5
5
 
6
6
  export function useNavigationOffset(): { navigationOffset: ComputedRef<number> } {
7
7
  const { isMobile } = useIsMobile()
@@ -0,0 +1,107 @@
1
+ import { getCurrentInstance } from 'vue'
2
+ import cn from '@iqual/nuxt-icms/composables/useCn'
3
+
4
+ /**
5
+ * Helper function that extracts a component name from a file path by converting the path structure to a component identifier.
6
+ *
7
+ * @param filePath - The full file path of the component
8
+ * @returns The component name derived from the path (e.g., "Section/Header/index.vue" becomes "SectionHeader")
9
+ */
10
+ function getComponentName(filePath: string): string {
11
+ if (!filePath.includes('/components/')) return 'Unknown'
12
+
13
+ return filePath.substring(filePath.indexOf('/components/') + 12)
14
+ .replace(/\.(vue|tsx?|jsx?)$/, '')
15
+ .replace(/\/index$/, '')
16
+ .split('/').join('')
17
+ }
18
+
19
+ interface UseThemeOptions {
20
+ componentName?: string
21
+ themeName?: string
22
+ }
23
+
24
+ /**
25
+ * A composable that provides theme-aware class generation for Vue components.
26
+ * Automatically detects the calling component and merges theme classes with component props and default classes.
27
+ *
28
+ * @param options - Configuration options for the theme
29
+ * @param options.componentName - Optional component name to manually override auto-detection
30
+ * @param options.themeName - The name of the theme to use from app config (defaults to 'default')
31
+ * @returns A function that generates themed classes for component elements
32
+ *
33
+ * @example
34
+ * ```vue
35
+ * <template>
36
+ * <button :class="theme('button', 'px-4 py-2')">
37
+ * <span :class="theme('label')">Click me</span>
38
+ * </button>
39
+ * </template>
40
+ *
41
+ * <script setup>
42
+ * // Auto-detects component name and uses default theme
43
+ * const theme = useTheme()
44
+ *
45
+ * // Manually set component name
46
+ * const customTheme = useTheme({ componentName: 'CustomComponent' })
47
+ *
48
+ * // Use a specific theme with auto-detection
49
+ * const darkTheme = useTheme({ themeName: 'dark' })
50
+ *
51
+ * // Manually set component name with specific theme
52
+ * const customDarkTheme = useTheme({
53
+ * componentName: 'CustomComponent',
54
+ * themeName: 'dark'
55
+ * })
56
+ *
57
+ * // The returned function merges:
58
+ * // 1. Default classes passed as second parameter
59
+ * // 2. Theme classes from app.config.ts
60
+ * // 3. Component prop classes (e.g., buttonClass, labelClass props)
61
+ * </script>
62
+ * ```
63
+ *
64
+ * @remarks
65
+ * The composable expects theme configuration in app.config.ts structured as:
66
+ * ```typescript
67
+ * export default defineAppConfig({
68
+ * theme: {
69
+ * default: {
70
+ * ComponentName: {
71
+ * elementName: 'theme-classes-here'
72
+ * }
73
+ * }
74
+ * }
75
+ * })
76
+ * ```
77
+ */
78
+ export function useTheme(options: UseThemeOptions = {}) {
79
+ const { componentName, themeName = 'default' } = options
80
+ const instance = getCurrentInstance()
81
+ const { props } = instance || {}
82
+ const appConfig = useAppConfig()
83
+
84
+ const component = componentName || getComponentName(instance?.type?.__file || '')
85
+ const componentTheme = (appConfig?.theme as any)?.[themeName]?.[component] ?? {}
86
+
87
+ /**
88
+ * Generates themed classes by merging default classes, theme classes, and component prop classes.
89
+ *
90
+ * @param elementName - The name of the element to style (should match theme config keys and props)
91
+ * @param defaultClass - Optional default CSS classes to include
92
+ * @returns Merged CSS class string using the cn() utility
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * // Returns merged classes from theme config, props, and defaults
97
+ * const buttonClasses = theme('button', 'rounded-lg')
98
+ *
99
+ * // Just use theme and prop classes without defaults
100
+ * const labelClasses = theme('label')
101
+ * ```
102
+ */
103
+ return function (elementName: string, defaultClass?: string) {
104
+ return cn(defaultClass, componentTheme[elementName] as string, props?.[elementName] as string)
105
+ }
106
+ }
107
+