@iqual/nuxt-icms 1.3.1 → 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.
- package/assets/symbols/chevron-right.svg +1 -1
- package/components/Blokkli/HeroImage/index.vue +1 -1
- package/components/Blokkli/Teasers/icon.svg +3 -3
- package/components/DesignSystem/Atoms/Button.vue +86 -36
- package/components/DesignSystem/Atoms/Caption.vue +10 -10
- package/components/DesignSystem/Atoms/Container.vue +10 -6
- package/components/DesignSystem/Atoms/GridRow.vue +4 -3
- package/components/DesignSystem/Atoms/LinkListItem.vue +31 -8
- package/components/DesignSystem/Molecules/ButtonGroup.vue +18 -3
- package/components/DesignSystem/Molecules/Card.vue +86 -31
- package/components/DesignSystem/Molecules/IconText.vue +37 -16
- package/components/DesignSystem/Molecules/MediaLightbox.vue +213 -58
- package/components/DesignSystem/Molecules/MediaSlider.vue +88 -25
- package/components/DesignSystem/Molecules/Partnership.vue +40 -9
- package/components/DesignSystem/Molecules/Table.vue +90 -25
- package/components/DesignSystem/Molecules/TitleComposition.vue +15 -12
- package/components/DesignSystem/Organisms/ContentBlock.vue +61 -15
- package/components/DesignSystem/Organisms/FacetedListing.vue +170 -0
- package/components/DesignSystem/Organisms/FlipTeaser.vue +119 -47
- package/components/DesignSystem/Organisms/HeaderHero.vue +167 -0
- package/components/DesignSystem/Organisms/HeaderTextImage.vue +71 -0
- package/components/DesignSystem/Organisms/LogoElement.vue +33 -10
- package/components/DesignSystem/Organisms/Map.vue +11 -6
- package/components/DesignSystem/Organisms/SearchResults.vue +41 -0
- package/components/DesignSystem/Organisms/TeamElement.vue +84 -20
- package/components/DesignSystem/Organisms/TeaserCard.vue +87 -34
- package/components/DesignSystem/Organisms/Testimonial.vue +82 -12
- package/components/DesignSystem/Organisms/TimelineElement.vue +42 -12
- package/components/DesignSystem/Organisms/TwoColumnTextElement.vue +44 -9
- package/components/DesignSystem/Pages/Search.vue +39 -0
- package/components/Drupal/Breadcrumb/index.vue +16 -5
- package/components/Drupal/Messages/index.vue +16 -5
- package/components/Drupal/Webform/fragment.graphql +8 -5
- package/components/Drupal/Webform/index.vue +20 -9
- package/components/Drupal/Webform/query.graphql +18 -16
- package/components/Form/CheckboxInput.vue +6 -6
- package/components/Form/CheckboxInputs.vue +5 -5
- package/components/Form/FileUploadInput.vue +6 -6
- package/components/Form/RadioButtonsInput.vue +5 -5
- package/components/Form/SelectInput.vue +7 -7
- package/components/Form/SubmitButton.vue +1 -1
- package/components/Form/TextAreaInput.vue +2 -2
- package/components/Form/TextEmailInput.vue +2 -2
- package/components/Form/TextInput.vue +2 -2
- package/components/Form/TextPhoneInput.vue +2 -2
- package/components/Form/composables/useFormElement.ts +5 -5
- package/components/Form/utils/types.ts +8 -7
- package/components/Media/Icon/index.vue +10 -6
- package/components/Media/Image/index.vue +26 -20
- package/components/Media/RemoteVideo/index.vue +34 -21
- package/components/Media/Video/index.vue +6 -5
- package/components/Node/Icms/Page/Teaser/index.vue +6 -4
- package/components/Node/Icms/Page/index.vue +2 -2
- package/components/Paragraph/Button/Element/index.vue +9 -2
- package/components/Paragraph/CallToAction/index.vue +43 -10
- package/components/Paragraph/Card/Element/index.vue +2 -1
- package/components/Paragraph/Card/Layout/index.vue +29 -10
- package/components/Paragraph/Contact/index.vue +41 -12
- package/components/Paragraph/Downloads/index.vue +82 -33
- package/components/Paragraph/FAQ/Element/index.vue +41 -12
- package/components/Paragraph/FAQ/Layout/index.vue +47 -13
- package/components/Paragraph/FlipTeasers/index.vue +26 -6
- package/components/Paragraph/Gallery/index.vue +42 -10
- package/components/Paragraph/HeroImage/index.vue +62 -16
- package/components/Paragraph/IconText/Element/index.vue +7 -1
- package/components/Paragraph/IconText/Layout/index.vue +39 -12
- package/components/Paragraph/Introduction/index.vue +50 -15
- package/components/Paragraph/Logo/Element/fragment.graphql +1 -0
- package/components/Paragraph/Logo/Element/index.vue +7 -2
- package/components/Paragraph/Media/index.vue +32 -31
- package/components/Paragraph/Partnerships/Element/fragment.graphql +1 -0
- package/components/Paragraph/Partnerships/Element/index.vue +7 -0
- package/components/Paragraph/Partnerships/Layout/index.vue +100 -27
- package/components/Paragraph/SingleTestimonial/index.vue +58 -15
- package/components/Paragraph/Table/index.vue +32 -9
- package/components/Paragraph/Tabs/Element/index.vue +44 -10
- package/components/Paragraph/Tabs/Layout/index.vue +123 -37
- package/components/Paragraph/Team/Element/index.vue +18 -1
- package/components/Paragraph/Team/Layout/index.vue +30 -7
- package/components/Paragraph/Teasers/index.vue +33 -7
- package/components/Paragraph/Testimonial/Element/index.vue +30 -5
- package/components/Paragraph/Testimonial/Layout/index.vue +78 -24
- package/components/Paragraph/Text/index.vue +32 -10
- package/components/Paragraph/TextMedia/index.vue +44 -13
- package/components/Paragraph/Timeline/Element/index.vue +23 -7
- package/components/Paragraph/Timeline/Layout/index.vue +232 -67
- package/components/Paragraph/TwoColumnText/Element/index.vue +20 -3
- package/components/Paragraph/TwoColumnText/Layout/index.vue +8 -1
- package/components/Paragraph/iFrame/index.vue +31 -11
- package/components/Search/Facets/FacetDropdownUI.vue +351 -77
- package/components/Search/Facets/FacetedSearchUI.vue +257 -101
- package/components/Search/Facets/fragment.graphql +5 -1
- package/components/Search/Input/index.vue +78 -20
- package/components/Section/Footer/index.vue +170 -30
- package/components/Section/Header/Buttons.vue +34 -0
- package/components/Section/Header/DesktopMainMenu.vue +160 -0
- package/components/Section/Header/DesktopMainMenuDropdown.vue +170 -0
- package/components/Section/Header/LanguageDropdown.vue +95 -64
- package/components/Section/Header/LanguageLinks.vue +39 -0
- package/components/Section/Header/Logo.vue +23 -0
- package/components/Section/Header/MainButtons.vue +31 -0
- package/components/Section/Header/MenuIcon.vue +60 -49
- package/components/Section/Header/MetaButtons.vue +31 -0
- package/components/Section/Header/MetaMenu.vue +55 -53
- package/components/Section/Header/MobileMainMenu.vue +23 -0
- package/components/Section/Header/MobileMainMenuItem.vue +191 -0
- package/components/Section/Header/index.vue +182 -186
- package/composables/useMenu.ts +2 -2
- package/composables/useMobileMenuOverlayBus.ts +44 -0
- package/composables/useNavigationOffset.ts +2 -2
- package/composables/useTheme.ts +107 -0
- package/formkit.theme.ts +541 -586
- package/layouts/default.vue +7 -35
- package/layouts/error.vue +7 -32
- package/nuxt.config.ts +13 -0
- package/package.json +1 -1
- package/plugins/initData.ts +8 -5
- package/queries/query.initData.graphql +4 -2
- package/tailwind.config.ts +11 -11
- package/types/global.ts +1 -0
- package/components/Section/Header/GlobalMenu.vue +0 -335
- package/components/Section/Header/LanguageSwitcher.vue +0 -37
|
@@ -1,216 +1,212 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<header
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
:
|
|
6
|
+
:class="
|
|
7
|
+
themedClass(
|
|
8
|
+
'headerClass',
|
|
9
|
+
'sticky top-0 z-[101] 2xl:-top-[var(--meta-height)]',
|
|
10
|
+
)
|
|
11
|
+
"
|
|
13
12
|
>
|
|
14
|
-
<!-- Meta
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
39
|
+
:class="
|
|
40
|
+
themedClass(
|
|
41
|
+
'languageDropdownContainer',
|
|
42
|
+
'flex h-full items-center',
|
|
43
|
+
)
|
|
44
|
+
"
|
|
25
45
|
>
|
|
26
|
-
|
|
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
|
-
</
|
|
38
|
-
|
|
39
|
-
<!--
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
<
|
|
46
|
-
|
|
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
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
69
|
-
|
|
155
|
+
:class="
|
|
156
|
+
themedClass(
|
|
157
|
+
'mobileMenuHeaderContainer',
|
|
158
|
+
'flex h-14 items-center',
|
|
159
|
+
)
|
|
160
|
+
"
|
|
70
161
|
>
|
|
71
|
-
<
|
|
72
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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 {
|
|
144
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
metaHeight.value = metaRef.value.getBoundingClientRect().height
|
|
161
|
-
}
|
|
162
|
-
})
|
|
185
|
+
const isMobileMenuOpen = ref(false)
|
|
186
|
+
const clickAwayArmed = ref(false)
|
|
163
187
|
|
|
164
|
-
const
|
|
165
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
191
|
+
onOpen(() => {
|
|
192
|
+
isMobileMenuOpen.value = true
|
|
193
|
+
document.body.classList.toggle('overflow-y-hidden', isMobileMenuOpen.value)
|
|
194
|
+
})
|
|
206
195
|
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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>
|
package/composables/useMenu.ts
CHANGED
|
@@ -48,7 +48,7 @@ export async function useMenu(menuName: MenuName) {
|
|
|
48
48
|
return null
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
const
|
|
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
|
-
|
|
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 =
|
|
4
|
-
const desktopNavHeight: number =
|
|
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
|
+
|