@gsa-tts/graymatter-ui 0.0.2 → 0.1.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/index.ts +2 -0
- package/package.json +8 -5
- package/src/components/DesktopHeader.svelte +89 -0
- package/src/components/DesktopSideNav.svelte +375 -0
- package/src/components/GoogleTagManager.astro +16 -0
- package/src/components/Logo.svelte +8 -7
- package/src/components/MobileBottomNav.svelte +99 -0
- package/src/components/MobileSideMenu.svelte +170 -0
- package/src/components/MobileTopNav.svelte +101 -0
- package/src/components/NavigationInitializer.svelte +19 -0
- package/src/components/icons/ApiIcon.svelte +49 -0
- package/src/components/icons/ChatIcon.svelte +27 -0
- package/src/components/icons/CloseIcon.svelte +18 -0
- package/src/components/icons/ConsoleIcon.svelte +25 -0
- package/src/components/icons/DiscoverIcon.svelte +30 -0
- package/src/components/icons/MenuIcon.svelte +38 -0
- package/src/components/icons/index.ts +6 -0
- package/src/components/index.ts +9 -0
- package/src/layouts/AppLayout.astro +1 -1
- package/src/layouts/BaseLayout.astro +4 -0
- package/src/layouts/GlobalAppLayout.astro +585 -0
- package/src/stores/navigationStore.ts +153 -0
- package/src/stores/subNavReadyStore.ts +3 -0
- package/src/styles/base/global.css +8 -1
- package/src/styles/base/typography.css +15 -14
- package/src/styles/components/globalAppLayout.css +76 -0
- package/src/styles/components/globalAppNav.css +548 -0
- package/assets/images/gsai-logo-black.svg +0 -18
- package/assets/images/gsai-logo-white.svg +0 -18
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsa-tts/graymatter-ui",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"exports": {
|
|
9
9
|
".": "./index.ts",
|
|
10
|
+
"./stores/navigationStore": "./src/stores/navigationStore.ts",
|
|
11
|
+
"./stores/subNavReadyStore": "./src/stores/subNavReadyStore.ts",
|
|
10
12
|
"./assets/*": "./assets/*",
|
|
11
13
|
"./astro-config": "./src/astro-config/index.ts",
|
|
12
14
|
"./component-options": "./src/component-options/index.ts",
|
|
@@ -45,17 +47,18 @@
|
|
|
45
47
|
"astro-purgecss": "^5.2.2",
|
|
46
48
|
"eslint": "^9.21.0",
|
|
47
49
|
"gulp": "^5.0.0",
|
|
48
|
-
"svelte": "^5.
|
|
50
|
+
"svelte": "^5.35.4",
|
|
49
51
|
"typescript": "5.7.3",
|
|
50
52
|
"vitest": "^3.0.7",
|
|
51
|
-
"
|
|
53
|
+
"vite": "^6.3.5",
|
|
52
54
|
"@gsa-tts/graymatter-typescript-config": "0.0.1",
|
|
53
|
-
"@gsa-tts/graymatter-vitest-config": "0.0.1"
|
|
55
|
+
"@gsa-tts/graymatter-vitest-config": "0.0.1",
|
|
56
|
+
"@gsa-tts/graymatter-eslint": "0.0.1"
|
|
54
57
|
},
|
|
55
58
|
"dependencies": {
|
|
56
59
|
"@fontsource-variable/archivo": "^5.2.6",
|
|
57
60
|
"@uswds/uswds": "3.12.0",
|
|
58
|
-
"astro": "^5.
|
|
61
|
+
"astro": "^5.11.0",
|
|
59
62
|
"slugify": "^1.6.6",
|
|
60
63
|
"@gsa-tts/graymatter-style-tokens": "0.0.2"
|
|
61
64
|
},
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { selectedItem, isExpanded, selectedSubNavItemTitle } from '../stores/navigationStore.js';
|
|
3
|
+
import Logo from './Logo.svelte';
|
|
4
|
+
import { onMount } from 'svelte';
|
|
5
|
+
import { subNavReady } from '../stores/subNavReadyStore';
|
|
6
|
+
|
|
7
|
+
const { customHeaderText = '' } = $props();
|
|
8
|
+
|
|
9
|
+
let isLayoutTransitioning = false;
|
|
10
|
+
|
|
11
|
+
const showHeader = $derived(() =>
|
|
12
|
+
$selectedItem && $selectedItem !== 'discover' && !$isExpanded && !isLayoutTransitioning && $subNavReady
|
|
13
|
+
);
|
|
14
|
+
const headerText = $derived(() =>
|
|
15
|
+
customHeaderText || $selectedSubNavItemTitle
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
onMount(() => {
|
|
19
|
+
const handleSectionVisible = (event: Event) => {
|
|
20
|
+
if (typeof window !== 'undefined' && window.innerWidth <= 640) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let topLevelTitle = null;
|
|
24
|
+
if (typeof window !== 'undefined') {
|
|
25
|
+
topLevelTitle = sessionStorage.getItem('subNav-topLevelTitle');
|
|
26
|
+
if (topLevelTitle) {
|
|
27
|
+
try {
|
|
28
|
+
topLevelTitle = JSON.parse(topLevelTitle);
|
|
29
|
+
} catch {/* ignore parse error */}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (typeof topLevelTitle === 'string' && topLevelTitle.trim() !== '') {
|
|
33
|
+
selectedSubNavItemTitle.set(topLevelTitle);
|
|
34
|
+
sessionStorage.setItem('subNav-selectedTitle', JSON.stringify(topLevelTitle));
|
|
35
|
+
} else {
|
|
36
|
+
const customEvent = event as CustomEvent;
|
|
37
|
+
if (customEvent.detail && customEvent.detail.title) {
|
|
38
|
+
selectedSubNavItemTitle.set(customEvent.detail.title);
|
|
39
|
+
sessionStorage.setItem('subNav-selectedTitle', JSON.stringify(customEvent.detail.title));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleLayoutTransition = (event: Event) => {
|
|
45
|
+
const customEvent = event as CustomEvent;
|
|
46
|
+
const { isMobile } = customEvent.detail;
|
|
47
|
+
if (!isMobile) {
|
|
48
|
+
isLayoutTransitioning = true;
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
isLayoutTransitioning = false;
|
|
51
|
+
}, 300);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
window.addEventListener('sectionVisible', handleSectionVisible);
|
|
56
|
+
window.addEventListener('layoutTransition', handleLayoutTransition);
|
|
57
|
+
|
|
58
|
+
return () => {
|
|
59
|
+
window.removeEventListener('sectionVisible', handleSectionVisible);
|
|
60
|
+
window.removeEventListener('layoutTransition', handleLayoutTransition);
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
{#if showHeader()}
|
|
66
|
+
<div class="global-header">
|
|
67
|
+
<div class="header-content">
|
|
68
|
+
<Logo width={75} height={22} />
|
|
69
|
+
<span class="header-text">{headerText()}</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
{/if}
|
|
73
|
+
|
|
74
|
+
<style>
|
|
75
|
+
.global-header {
|
|
76
|
+
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Optional: Add a subtle slide effect */
|
|
80
|
+
.global-header {
|
|
81
|
+
transform: translateY(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* When hiding, we can add a subtle slide up effect */
|
|
85
|
+
.global-header:not(:defined) {
|
|
86
|
+
opacity: 0;
|
|
87
|
+
transform: translateY(-10px);
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Controls which nav item is selected on initial server-side render (SSR) or first paint.
|
|
4
|
+
* - For the Discover page, set ssrSelectedItem="discover".
|
|
5
|
+
* - For other pages, set ssrSelectedItem to the corresponding nav key (e.g., "api", "chat", "console").
|
|
6
|
+
* - If not provided, the navigation will default to expanded/open on SSR.
|
|
7
|
+
*/
|
|
8
|
+
import { onMount } from 'svelte';
|
|
9
|
+
import Logo from './Logo.svelte';
|
|
10
|
+
import {
|
|
11
|
+
isExpanded,
|
|
12
|
+
selectedItem,
|
|
13
|
+
isMenuButtonDisabled,
|
|
14
|
+
navigationStore,
|
|
15
|
+
type NavigationItem,
|
|
16
|
+
} from '../stores/navigationStore.js';
|
|
17
|
+
import DiscoverIcon from './icons/DiscoverIcon.svelte';
|
|
18
|
+
import ChatIcon from './icons/ChatIcon.svelte';
|
|
19
|
+
import ConsoleIcon from './icons/ConsoleIcon.svelte';
|
|
20
|
+
import ApiIcon from './icons/ApiIcon.svelte';
|
|
21
|
+
import MenuIcon from './icons/MenuIcon.svelte';
|
|
22
|
+
|
|
23
|
+
const { ssrSelectedItem = undefined, onNavToggle = undefined, onNavItemClick = undefined, onProfileClick = undefined, children } = $props();
|
|
24
|
+
|
|
25
|
+
let hydrated = false;
|
|
26
|
+
let navContainer: HTMLDivElement;
|
|
27
|
+
let hoveredItem = $state<string | null>(null);
|
|
28
|
+
let menuButtonHovered = $state(false);
|
|
29
|
+
let canHover = $state(false);
|
|
30
|
+
|
|
31
|
+
function ssrOrHydrated<T>(hydratedValue: T, ssrValue: T): T {
|
|
32
|
+
return hydrated ? hydratedValue : ssrValue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNavSelected(key: NavigationItem) {
|
|
36
|
+
return ssrOrHydrated($selectedItem === key, ssrSelectedItem === key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isNavHovered(key: string) {
|
|
40
|
+
return hoveredItem === key;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const sideNavExpanded = $derived(() =>
|
|
44
|
+
ssrOrHydrated($isExpanded, ssrSelectedItem ? ssrSelectedItem !== 'discover' : true)
|
|
45
|
+
);
|
|
46
|
+
const menuBtnDisabled = $derived(() =>
|
|
47
|
+
ssrOrHydrated($isMenuButtonDisabled, ssrSelectedItem ? ssrSelectedItem === 'discover' : false)
|
|
48
|
+
);
|
|
49
|
+
const menuBtnOpen = $derived(() =>
|
|
50
|
+
ssrOrHydrated($isExpanded, ssrSelectedItem ? ssrSelectedItem !== 'discover' : true)
|
|
51
|
+
);
|
|
52
|
+
const menuBtnHovered = $derived(() =>
|
|
53
|
+
canHover && menuButtonHovered && !menuBtnDisabled()
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
onMount(() => {
|
|
57
|
+
hydrated = true;
|
|
58
|
+
// Remove the CSS class since reactive state takes over
|
|
59
|
+
document.documentElement.classList.remove('nav-initially-expanded');
|
|
60
|
+
// Remove the custom property since reactive CSS takes over
|
|
61
|
+
document.documentElement.style.removeProperty('--initial-main-top-padding');
|
|
62
|
+
// Add hydrated class to re-enable transitions
|
|
63
|
+
if (navContainer) {
|
|
64
|
+
navContainer.classList.add('hydrated');
|
|
65
|
+
}
|
|
66
|
+
// Detect if device supports hover (desktop only)
|
|
67
|
+
if (typeof window !== 'undefined') {
|
|
68
|
+
const mq = window.matchMedia('(hover: hover)');
|
|
69
|
+
canHover = mq.matches;
|
|
70
|
+
mq.addEventListener('change', e => {
|
|
71
|
+
canHover = e.matches;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Listen for layout transitions
|
|
75
|
+
const handleLayoutTransition = (event: Event) => {
|
|
76
|
+
const customEvent = event as CustomEvent;
|
|
77
|
+
const { isMobile } = customEvent.detail;
|
|
78
|
+
console.log('[DesktopSideNav] Layout transition detected:', { isMobile });
|
|
79
|
+
|
|
80
|
+
if (isMobile) {
|
|
81
|
+
// Switching to mobile: close navigation
|
|
82
|
+
console.log('[DesktopSideNav] Closing navigation for mobile');
|
|
83
|
+
navigationStore.setExpanded(false);
|
|
84
|
+
} else {
|
|
85
|
+
// Switching to desktop: open navigation if on non-discover page
|
|
86
|
+
const currentItem = $selectedItem;
|
|
87
|
+
console.log('[DesktopSideNav] Desktop transition - current item:', currentItem);
|
|
88
|
+
|
|
89
|
+
if (currentItem && currentItem !== 'discover') {
|
|
90
|
+
console.log('[DesktopSideNav] Opening navigation for desktop (non-discover page)');
|
|
91
|
+
navigationStore.setExpanded(true);
|
|
92
|
+
} else {
|
|
93
|
+
console.log('[DesktopSideNav] Keeping navigation closed for desktop (discover page or no selection)');
|
|
94
|
+
navigationStore.setExpanded(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
window.addEventListener('layoutTransition', handleLayoutTransition);
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
window.removeEventListener('layoutTransition', handleLayoutTransition);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
function toggleNav() {
|
|
108
|
+
if ($isMenuButtonDisabled) return;
|
|
109
|
+
navigationStore.toggleExpanded();
|
|
110
|
+
onNavToggle?.({ expanded: $isExpanded });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function handleNavItemClick(item: NavigationItem) {
|
|
114
|
+
console.log('[DesktopSideNav] handleNavItemClick called for:', item, 'at:', Date.now());
|
|
115
|
+
|
|
116
|
+
// Set navigation flag immediately to prevent header restoration
|
|
117
|
+
if (item !== 'discover' && typeof window !== 'undefined') {
|
|
118
|
+
sessionStorage.setItem('navigatingToMainNav', 'true');
|
|
119
|
+
console.log('[DesktopSideNav] Set navigation flag for:', item, 'at:', Date.now());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
navigationStore.setSelectedItem(item);
|
|
123
|
+
|
|
124
|
+
// Clear header state for main navigation items (including discover)
|
|
125
|
+
if (item !== 'discover') {
|
|
126
|
+
try {
|
|
127
|
+
import('@gsa-tts/graymatter-ui/stores/navigationStore').then(({ selectedSubNavItemTitle }) => {
|
|
128
|
+
// Clear the store value immediately
|
|
129
|
+
selectedSubNavItemTitle.set('');
|
|
130
|
+
if (typeof window !== 'undefined') {
|
|
131
|
+
sessionStorage.removeItem('subNav-selectedTitle');
|
|
132
|
+
sessionStorage.removeItem('subNav-selectedId');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Set default header from first submenu item after a short delay
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
const mainContent = document.getElementById('main-content');
|
|
138
|
+
if (mainContent) {
|
|
139
|
+
const firstSection = mainContent.querySelector('h2[id], h3[id], h4[id], h5[id], h6[id]');
|
|
140
|
+
if (firstSection && firstSection.textContent) {
|
|
141
|
+
const firstSectionTitle = firstSection.textContent.trim();
|
|
142
|
+
console.log('[DesktopSideNav] Setting default header for', item, ':', firstSectionTitle);
|
|
143
|
+
selectedSubNavItemTitle.set(firstSectionTitle);
|
|
144
|
+
if (typeof window !== 'undefined') {
|
|
145
|
+
sessionStorage.setItem('subNav-selectedTitle', JSON.stringify(firstSectionTitle));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}, 200); // Delay to ensure page content is loaded
|
|
150
|
+
}).catch((err) => {
|
|
151
|
+
console.log('[DesktopSideNav] Error clearing header state:', err);
|
|
152
|
+
});
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.log('[DesktopSideNav] Import failed for clearing state:', err);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Clear header state for Discover page
|
|
158
|
+
try {
|
|
159
|
+
import('@gsa-tts/graymatter-ui/stores/navigationStore').then(({ selectedSubNavItemTitle }) => {
|
|
160
|
+
selectedSubNavItemTitle.set('');
|
|
161
|
+
if (typeof window !== 'undefined') {
|
|
162
|
+
sessionStorage.removeItem('subNav-selectedTitle');
|
|
163
|
+
sessionStorage.removeItem('subNav-selectedId');
|
|
164
|
+
}
|
|
165
|
+
}).catch((err) => {
|
|
166
|
+
console.log('[DesktopSideNav] Error clearing header state for Discover:', err);
|
|
167
|
+
});
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.log('[DesktopSideNav] Import failed for clearing Discover state:', err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
onNavItemClick?.({ item, expanded: $isExpanded });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function handleKeyDown(event: KeyboardEvent, item: NavigationItem) {
|
|
177
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
handleNavItemClick(item);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
</script>
|
|
183
|
+
|
|
184
|
+
<div
|
|
185
|
+
class="side-nav-container"
|
|
186
|
+
class:expanded={sideNavExpanded()}
|
|
187
|
+
bind:this={navContainer}
|
|
188
|
+
>
|
|
189
|
+
<div class="nav-content">
|
|
190
|
+
<div class="nav-main">
|
|
191
|
+
<div class="menu-section">
|
|
192
|
+
<button
|
|
193
|
+
class="menu-button"
|
|
194
|
+
class:disabled={$isMenuButtonDisabled}
|
|
195
|
+
disabled={$isMenuButtonDisabled}
|
|
196
|
+
onclick={toggleNav}
|
|
197
|
+
onmouseenter={() => (menuButtonHovered = true)}
|
|
198
|
+
onmouseleave={() => (menuButtonHovered = false)}
|
|
199
|
+
aria-label="Open navigation menu"
|
|
200
|
+
>
|
|
201
|
+
<span class="sr-only">Open navigation menu</span>
|
|
202
|
+
<div class="icon-container">
|
|
203
|
+
<div class="icon-wrapper">
|
|
204
|
+
<MenuIcon
|
|
205
|
+
isHovered={menuBtnHovered()}
|
|
206
|
+
isDisabled={menuBtnDisabled()}
|
|
207
|
+
isOpen={menuBtnOpen()}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
<div class="nav-items">
|
|
215
|
+
<a
|
|
216
|
+
href="/discover"
|
|
217
|
+
class="nav-item discover-item"
|
|
218
|
+
class:selected={isNavSelected('discover')}
|
|
219
|
+
class:hovered={isNavHovered('discover')}
|
|
220
|
+
role="button"
|
|
221
|
+
tabindex="0"
|
|
222
|
+
onclick={() => handleNavItemClick('discover')}
|
|
223
|
+
onkeydown={e => handleKeyDown(e, 'discover')}
|
|
224
|
+
onmouseenter={() => (hoveredItem = 'discover')}
|
|
225
|
+
onmouseleave={() => (hoveredItem = null)}
|
|
226
|
+
>
|
|
227
|
+
<div class="icon-container">
|
|
228
|
+
<div class="icon-wrapper">
|
|
229
|
+
<DiscoverIcon isSelected={isNavSelected('discover')} />
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<span class="nav-text">Discover</span>
|
|
233
|
+
</a>
|
|
234
|
+
<a
|
|
235
|
+
href="/#chat"
|
|
236
|
+
class="nav-item chat-item"
|
|
237
|
+
class:selected={isNavSelected('chat')}
|
|
238
|
+
class:hovered={isNavHovered('chat')}
|
|
239
|
+
role="button"
|
|
240
|
+
tabindex="0"
|
|
241
|
+
onclick={() => handleNavItemClick('chat')}
|
|
242
|
+
onkeydown={e => handleKeyDown(e, 'chat')}
|
|
243
|
+
onmouseenter={() => (hoveredItem = 'chat')}
|
|
244
|
+
onmouseleave={() => (hoveredItem = null)}
|
|
245
|
+
>
|
|
246
|
+
<div class="icon-container">
|
|
247
|
+
<div class="icon-wrapper">
|
|
248
|
+
<ChatIcon isSelected={isNavSelected('chat') as boolean} />
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
<span class="nav-text">Chat</span>
|
|
252
|
+
</a>
|
|
253
|
+
|
|
254
|
+
<a
|
|
255
|
+
href="/"
|
|
256
|
+
class="nav-item console-item"
|
|
257
|
+
class:selected={isNavSelected('console')}
|
|
258
|
+
class:hovered={isNavHovered('console')}
|
|
259
|
+
role="button"
|
|
260
|
+
tabindex="0"
|
|
261
|
+
onclick={() => handleNavItemClick('console')}
|
|
262
|
+
onkeydown={e => handleKeyDown(e, 'console')}
|
|
263
|
+
onmouseenter={() => (hoveredItem = 'console')}
|
|
264
|
+
onmouseleave={() => (hoveredItem = null)}
|
|
265
|
+
>
|
|
266
|
+
<div class="icon-container">
|
|
267
|
+
<div class="icon-wrapper">
|
|
268
|
+
<ConsoleIcon isSelected={isNavSelected('console') as boolean} />
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
<span class="nav-text">Console</span>
|
|
272
|
+
</a>
|
|
273
|
+
|
|
274
|
+
<a
|
|
275
|
+
href="/api"
|
|
276
|
+
class="nav-item api-item"
|
|
277
|
+
class:selected={isNavSelected('api')}
|
|
278
|
+
class:hovered={isNavHovered('api')}
|
|
279
|
+
role="button"
|
|
280
|
+
tabindex="0"
|
|
281
|
+
onclick={() => handleNavItemClick('api')}
|
|
282
|
+
onkeydown={e => handleKeyDown(e, 'api')}
|
|
283
|
+
onmouseenter={() => (hoveredItem = 'api')}
|
|
284
|
+
onmouseleave={() => (hoveredItem = null)}
|
|
285
|
+
>
|
|
286
|
+
<div class="icon-container">
|
|
287
|
+
<div class="icon-wrapper">
|
|
288
|
+
<ApiIcon isSelected={isNavSelected('api')} />
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
<span class="nav-text">API</span>
|
|
292
|
+
</a>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div class="nav-footer">
|
|
297
|
+
<button
|
|
298
|
+
class="user-profile-button"
|
|
299
|
+
tabindex="0"
|
|
300
|
+
onclick={() => onProfileClick?.()}
|
|
301
|
+
onkeydown={e => {
|
|
302
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
303
|
+
e.preventDefault();
|
|
304
|
+
onProfileClick?.();
|
|
305
|
+
}
|
|
306
|
+
}}
|
|
307
|
+
>
|
|
308
|
+
<div class="user-profile-container">
|
|
309
|
+
<div class="user-profile-icon">
|
|
310
|
+
<span class="user-initials">ZW</span>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div class="expanded-content drawer-anim" class:open={sideNavExpanded()}>
|
|
318
|
+
<div class="drawer-content" class:visible={sideNavExpanded()}>
|
|
319
|
+
<div class="logo-container">
|
|
320
|
+
<div class="logo-wrapper">
|
|
321
|
+
<div class="logo-inner">
|
|
322
|
+
<div class="logo-component padding-left-2">
|
|
323
|
+
<Logo width={75} height={22} />
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
<nav aria-label="Sub navigation">
|
|
329
|
+
{@render children?.()}
|
|
330
|
+
</nav>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
<style>
|
|
336
|
+
.expanded-content.drawer-anim {
|
|
337
|
+
width: 0;
|
|
338
|
+
min-width: 0;
|
|
339
|
+
max-width: 0;
|
|
340
|
+
overflow: hidden;
|
|
341
|
+
opacity: 0.7;
|
|
342
|
+
pointer-events: none;
|
|
343
|
+
transition:
|
|
344
|
+
width 0.4s cubic-bezier(.4,0,.2,1),
|
|
345
|
+
min-width 0.4s cubic-bezier(.4,0,.2,1),
|
|
346
|
+
max-width 0.4s cubic-bezier(.4,0,.2,1),
|
|
347
|
+
opacity 0.2s;
|
|
348
|
+
}
|
|
349
|
+
.expanded-content.drawer-anim.open {
|
|
350
|
+
width: 12.5rem;
|
|
351
|
+
min-width: 12.5rem;
|
|
352
|
+
max-width: 12.5rem;
|
|
353
|
+
opacity: 1;
|
|
354
|
+
pointer-events: auto;
|
|
355
|
+
}
|
|
356
|
+
.drawer-content {
|
|
357
|
+
opacity: 0;
|
|
358
|
+
transform: translateX(-10px);
|
|
359
|
+
transition:
|
|
360
|
+
opacity 0.4s 0.20s cubic-bezier(.4,0,.2,1),
|
|
361
|
+
transform 0.4s 0.20s cubic-bezier(.4,0,.2,1);
|
|
362
|
+
}
|
|
363
|
+
.drawer-content.visible {
|
|
364
|
+
opacity: 1;
|
|
365
|
+
transform: translateX(0);
|
|
366
|
+
transition:
|
|
367
|
+
opacity 0.4s 0.20s cubic-bezier(.4,0,.2,1),
|
|
368
|
+
transform 0.4s 0.20s cubic-bezier(.4,0,.2,1);
|
|
369
|
+
}
|
|
370
|
+
.expanded-content.drawer-anim:not(.open) .drawer-content {
|
|
371
|
+
transition:
|
|
372
|
+
opacity 0.20s cubic-bezier(.4,0,.2,1),
|
|
373
|
+
transform 0.20s cubic-bezier(.4,0,.2,1);
|
|
374
|
+
}
|
|
375
|
+
</style>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
let gtmCode = '';
|
|
3
|
+
if (Astro.props.gtmID) {
|
|
4
|
+
gtmCode = `<!-- Google tag (gtag.js) -->
|
|
5
|
+
<script async src="https://www.googletagmanager.com/gtag/js?id=${Astro.props.gtmID}"></script>
|
|
6
|
+
<script>
|
|
7
|
+
window.dataLayer = window.dataLayer || [];
|
|
8
|
+
function gtag(){dataLayer.push(arguments);}
|
|
9
|
+
gtag('js', new Date());
|
|
10
|
+
|
|
11
|
+
gtag('config', '${Astro.props.gtmID}');
|
|
12
|
+
</script>`;
|
|
13
|
+
}
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<Fragment set:html={gtmCode} />
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
export let isInverted: boolean = false;
|
|
3
3
|
export let width: number | undefined;
|
|
4
4
|
export let height: number | undefined;
|
|
5
|
-
const defaultWidth =
|
|
6
|
-
const defaultHeight =
|
|
5
|
+
const defaultWidth = 102;
|
|
6
|
+
const defaultHeight = 30;
|
|
7
7
|
width = width || defaultWidth;
|
|
8
8
|
height = height || defaultHeight;
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<svg
|
|
14
14
|
{width}
|
|
15
15
|
{height}
|
|
16
|
-
viewBox="0 0
|
|
16
|
+
viewBox="0 0 102 30"
|
|
17
17
|
fill="none"
|
|
18
18
|
xmlns="http://www.w3.org/2000/svg"
|
|
19
19
|
style="fill: {logoColor};"
|
|
@@ -21,14 +21,15 @@
|
|
|
21
21
|
>
|
|
22
22
|
<g>
|
|
23
23
|
<path
|
|
24
|
-
d="
|
|
24
|
+
d="M57.5791 20.4177C57.5791 15.5645 53.7309 11.6187 48.9988 11.6187H40.188C38.6688 11.6187 37.4304 10.3567 37.4304 8.80197C37.4304 7.24729 38.6658 5.98522 40.188 5.98522H56.2373V0H38.8757C34.1466 0 30.2983 3.94877 30.2983 8.80197C30.2983 13.6552 34.1466 17.6039 38.8757 17.6039H47.6865C49.2057 17.6039 50.4442 18.866 50.4442 20.4207C50.4442 21.9754 49.2087 23.2374 47.6865 23.2374H31.0284V29.2197H48.9988C53.7309 29.2197 57.5791 25.2709 57.5791 20.4207V20.4177Z"
|
|
25
25
|
/>
|
|
26
26
|
<path
|
|
27
|
-
d="
|
|
27
|
+
d="M82.1466 24.7153L82.188 24.7685L83.6747 29.2197H90.6914L80.917 0H68.3525L58.5781 29.2167H65.5949L67.0993 24.7153H82.1466ZM69.186 18.4818L73.3653 5.98522H75.9013L75.919 6.03547L80.1161 18.5793H69.1535L69.186 18.4818Z"
|
|
28
28
|
/>
|
|
29
|
-
<path d="M104.809 0.478271H99.8516V32.3468H104.809V0.478271Z" />
|
|
30
29
|
<path
|
|
31
|
-
d="
|
|
30
|
+
d="M27.198 0H20.5626V15.8158C20.5626 21.4345 16.9626 23.4355 13.5961 23.4355C9.03842 23.4355 6.62956 20.799 6.62956 15.8158V0H0V17.0069C0 20.6985 1.46305 24.0532 4.11724 26.4532C6.64729 28.7409 10.0138 30 13.599 30C20.3586 30 27.198 25.5369 27.198 17.0039V0Z"
|
|
32
31
|
/>
|
|
32
|
+
<path d="M101.708 9.78915H93.8662V29.2197H101.708V9.78915Z" />
|
|
33
|
+
<path d="M101.823 0H93.8662V5.98227H101.823V0Z" />
|
|
33
34
|
</g>
|
|
34
35
|
</svg>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { selectedItem, navigationStore } from '../stores/navigationStore.js';
|
|
3
|
+
import type { NavigationItem } from '../stores/navigationStore.js';
|
|
4
|
+
import DiscoverIcon from './icons/DiscoverIcon.svelte';
|
|
5
|
+
import ChatIcon from './icons/ChatIcon.svelte';
|
|
6
|
+
import ConsoleIcon from './icons/ConsoleIcon.svelte';
|
|
7
|
+
import ApiIcon from './icons/ApiIcon.svelte';
|
|
8
|
+
|
|
9
|
+
export let onNavItemClick: ((data: { item: NavigationItem; expanded: boolean }) => void) | undefined = undefined;
|
|
10
|
+
|
|
11
|
+
const navItems = [
|
|
12
|
+
{ id: 'discover', label: 'Discover', icon: DiscoverIcon, href: '/discover' },
|
|
13
|
+
{ id: 'chat', label: 'Chat', icon: ChatIcon, href: '/' },
|
|
14
|
+
{ id: 'console', label: 'Console', icon: ConsoleIcon, href: '/' },
|
|
15
|
+
{ id: 'api', label: 'API', icon: ApiIcon, href: '/api' }
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
function handleNavItemClick(item: NavigationItem) {
|
|
19
|
+
console.log('[MobileBottomNav] handleNavItemClick called for:', item, 'at:', Date.now());
|
|
20
|
+
|
|
21
|
+
// Set navigation flag immediately to prevent header restoration
|
|
22
|
+
if (item !== 'discover' && typeof window !== 'undefined') {
|
|
23
|
+
sessionStorage.setItem('navigatingToMainNav', 'true');
|
|
24
|
+
console.log('[MobileBottomNav] Set navigation flag for:', item, 'at:', Date.now());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
navigationStore.setSelectedItem(item);
|
|
28
|
+
|
|
29
|
+
// Clear header state for main navigation items (including discover)
|
|
30
|
+
if (item !== 'discover') {
|
|
31
|
+
try {
|
|
32
|
+
import('@gsa-tts/graymatter-ui/stores/navigationStore').then(({ selectedSubNavItemTitle }) => {
|
|
33
|
+
// Clear the store value immediately
|
|
34
|
+
selectedSubNavItemTitle.set('');
|
|
35
|
+
if (typeof window !== 'undefined') {
|
|
36
|
+
sessionStorage.removeItem('subNav-selectedTitle');
|
|
37
|
+
sessionStorage.removeItem('subNav-selectedId');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Set default header from first submenu item after a short delay
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
const mainContent = document.getElementById('main-content');
|
|
43
|
+
if (mainContent) {
|
|
44
|
+
const firstSection = mainContent.querySelector('h2[id], h3[id], h4[id], h5[id], h6[id]');
|
|
45
|
+
if (firstSection && firstSection.textContent) {
|
|
46
|
+
const firstSectionTitle = firstSection.textContent.trim();
|
|
47
|
+
console.log('[MobileBottomNav] Setting default header for', item, ':', firstSectionTitle);
|
|
48
|
+
selectedSubNavItemTitle.set(firstSectionTitle);
|
|
49
|
+
if (typeof window !== 'undefined') {
|
|
50
|
+
sessionStorage.setItem('subNav-selectedTitle', JSON.stringify(firstSectionTitle));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}, 200); // Delay to ensure page content is loaded
|
|
55
|
+
}).catch((err) => {
|
|
56
|
+
console.log('[MobileBottomNav] Error clearing header state:', err);
|
|
57
|
+
});
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.log('[MobileBottomNav] Import failed for clearing state:', err);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// Clear header state for Discover page
|
|
63
|
+
try {
|
|
64
|
+
import('@gsa-tts/graymatter-ui/stores/navigationStore').then(({ selectedSubNavItemTitle }) => {
|
|
65
|
+
selectedSubNavItemTitle.set('');
|
|
66
|
+
if (typeof window !== 'undefined') {
|
|
67
|
+
sessionStorage.removeItem('subNav-selectedTitle');
|
|
68
|
+
sessionStorage.removeItem('subNav-selectedId');
|
|
69
|
+
}
|
|
70
|
+
}).catch((err) => {
|
|
71
|
+
console.log('[MobileBottomNav] Error clearing header state for Discover:', err);
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.log('[MobileBottomNav] Import failed for clearing Discover state:', err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onNavItemClick?.({ item, expanded: false });
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<nav class="mobile-bottom-nav">
|
|
83
|
+
{#each navItems as navItem}
|
|
84
|
+
<a
|
|
85
|
+
href={navItem.href}
|
|
86
|
+
class="nav-item"
|
|
87
|
+
class:active={$selectedItem === navItem.id}
|
|
88
|
+
on:click={() => handleNavItemClick(navItem.id)}
|
|
89
|
+
>
|
|
90
|
+
<div class="icon">
|
|
91
|
+
<svelte:component
|
|
92
|
+
this={navItem.icon}
|
|
93
|
+
isActive={$selectedItem === navItem.id}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
<span class="label">{navItem.label}</span>
|
|
97
|
+
</a>
|
|
98
|
+
{/each}
|
|
99
|
+
</nav>
|