@gsa-tts/graymatter-ui 0.4.17 → 0.5.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/dist/graymatter-ui.js +4340 -3190
- package/dist/graymatter-ui.umd.cjs +53 -52
- package/package.json +4 -4
- package/src/components/ApiDocSubNavMenu.svelte +28 -7
- package/src/components/Button.svelte +4 -2
- package/src/components/Button.webcomponent.svelte +19 -5
- package/src/components/DesktopHeader.svelte +11 -13
- package/src/components/DesktopSideNav.svelte +145 -108
- package/src/components/DesktopSideNav.webcomponent.svelte +5 -3
- package/src/components/Logo.svelte +5 -5
- package/src/components/MobileBottomNav.svelte +34 -23
- package/src/components/MobileBottomNav.webcomponent.svelte +2 -0
- package/src/components/MobileSideMenu.svelte +5 -5
- package/src/components/MobileSideMenu.webcomponent.svelte +2 -4
- package/src/components/MobileTopNav.svelte +22 -27
- package/src/components/NavigationInitializer.svelte +7 -4
- package/src/components/ProfileMenu.svelte +27 -18
- package/src/components/UsaBanner.svelte +3 -3
- package/src/components/icons/ApiIcon.svelte +1 -1
- package/src/components/icons/ChatIcon.svelte +1 -1
- package/src/components/icons/ConsoleIcon.svelte +1 -1
- package/src/components/icons/DiscoverIcon.svelte +1 -1
- package/src/components/icons/HelpIcon.svelte +1 -1
- package/src/components/icons/MenuIcon.svelte +15 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsa-tts/graymatter-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -53,13 +53,13 @@
|
|
|
53
53
|
"@types/unist": "^3.0.3",
|
|
54
54
|
"@uswds/compile": "1.2.2",
|
|
55
55
|
"@vitest/coverage-istanbul": "^3.2.4",
|
|
56
|
-
"astro": "^5.
|
|
56
|
+
"astro": "^5.15.9",
|
|
57
57
|
"astro-purgecss": "^5.2.2",
|
|
58
58
|
"eslint": "^9.21.0",
|
|
59
59
|
"gulp": "^5.0.0",
|
|
60
|
-
"svelte": "^5.
|
|
60
|
+
"svelte": "^5.53.5",
|
|
61
61
|
"typescript": "5.7.3",
|
|
62
|
-
"vite": "^6.
|
|
62
|
+
"vite": "^6.4.1",
|
|
63
63
|
"vitest": "^3.0.7",
|
|
64
64
|
"@gsa-tts/graymatter-eslint": "0.0.2",
|
|
65
65
|
"@gsa-tts/graymatter-typescript-config": "0.0.2",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { createEventDispatcher
|
|
2
|
+
import { createEventDispatcher } from 'svelte';
|
|
3
3
|
import { subNavReady } from '../stores/subNavReadyStore';
|
|
4
4
|
import type { SubNavData, SubNavItem } from '../types/subnav.ts';
|
|
5
5
|
import ApiDocSubNavList from './ApiDocSubNavList.svelte';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
let { data = null } = $props();
|
|
8
8
|
// TODO: remove legacy /api/documentation path once all consuming apps migrate
|
|
9
9
|
const API_DOC_PATHS = ['/api-documentation', '/api/documentation'];
|
|
10
10
|
|
|
@@ -187,6 +187,18 @@
|
|
|
187
187
|
let currentSelectedId = $state<string | null>(null);
|
|
188
188
|
let hydrated = $state(false);
|
|
189
189
|
let pageLoadComplete = $state(false);
|
|
190
|
+
let hasMounted = $state(false);
|
|
191
|
+
let scrollTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
192
|
+
|
|
193
|
+
function queueScrollToAnchor(hash: string, delay: number) {
|
|
194
|
+
if (scrollTimeoutId) {
|
|
195
|
+
clearTimeout(scrollTimeoutId);
|
|
196
|
+
}
|
|
197
|
+
scrollTimeoutId = setTimeout(() => {
|
|
198
|
+
scrollTimeoutId = null;
|
|
199
|
+
scrollToAnchorWithOffset(hash);
|
|
200
|
+
}, delay);
|
|
201
|
+
}
|
|
190
202
|
|
|
191
203
|
const effectiveSelectedId = $derived(() => {
|
|
192
204
|
if (!hydrated || !data) return null;
|
|
@@ -257,7 +269,10 @@
|
|
|
257
269
|
}
|
|
258
270
|
});
|
|
259
271
|
|
|
260
|
-
|
|
272
|
+
$effect(() => {
|
|
273
|
+
if (hasMounted) return;
|
|
274
|
+
hasMounted = true;
|
|
275
|
+
|
|
261
276
|
hydrated = true;
|
|
262
277
|
|
|
263
278
|
if (!data) return;
|
|
@@ -296,13 +311,20 @@
|
|
|
296
311
|
if (targetItem && targetItem.href && targetItem.href.includes('#')) {
|
|
297
312
|
const hash = targetItem.href.split('#')[1];
|
|
298
313
|
if (hash) {
|
|
299
|
-
|
|
314
|
+
queueScrollToAnchor(hash, 100);
|
|
300
315
|
}
|
|
301
316
|
}
|
|
302
317
|
|
|
303
318
|
// Mark page load as complete
|
|
304
319
|
pageLoadComplete = true;
|
|
305
320
|
subNavReady.set(true);
|
|
321
|
+
|
|
322
|
+
return () => {
|
|
323
|
+
if (scrollTimeoutId) {
|
|
324
|
+
clearTimeout(scrollTimeoutId);
|
|
325
|
+
scrollTimeoutId = null;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
306
328
|
});
|
|
307
329
|
|
|
308
330
|
function handleItemClick(event: Event, item: SubNavItem) {
|
|
@@ -332,9 +354,7 @@
|
|
|
332
354
|
window.location.hash = hash;
|
|
333
355
|
if (window.innerWidth > 640) {
|
|
334
356
|
// Desktop: use delay for consistent timing
|
|
335
|
-
|
|
336
|
-
scrollToAnchorWithOffset(hash);
|
|
337
|
-
}, 350);
|
|
357
|
+
queueScrollToAnchor(hash, 350);
|
|
338
358
|
} else {
|
|
339
359
|
// Mobile: immediate scroll without delay
|
|
340
360
|
window.dispatchEvent(
|
|
@@ -382,6 +402,7 @@
|
|
|
382
402
|
|
|
383
403
|
// Utility: Scroll to anchor with header offset
|
|
384
404
|
function scrollToAnchorWithOffset(anchorId: string) {
|
|
405
|
+
if (typeof document === 'undefined') return;
|
|
385
406
|
const mainContent = document.getElementById('main-content');
|
|
386
407
|
if (mainContent) {
|
|
387
408
|
const anchor = mainContent.querySelector(
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
type?: 'button' | 'submit' | 'reset';
|
|
29
29
|
onclick?: (event: MouseEvent) => void;
|
|
30
30
|
class?: string;
|
|
31
|
+
children?: import('svelte').Snippet;
|
|
31
32
|
[key: `data-${string}`]: string;
|
|
32
33
|
[key: `aria-${string}`]: string;
|
|
33
34
|
}
|
|
@@ -42,6 +43,7 @@
|
|
|
42
43
|
size = 'medium',
|
|
43
44
|
type = 'button',
|
|
44
45
|
class: className = '',
|
|
46
|
+
children,
|
|
45
47
|
// eslint-disable-next-line svelte/valid-compile
|
|
46
48
|
...props
|
|
47
49
|
}: Props = $props();
|
|
@@ -71,11 +73,11 @@
|
|
|
71
73
|
|
|
72
74
|
{#if href}
|
|
73
75
|
<a class={mergedClasses()} {href} class:disabled {onclick} {...props}>
|
|
74
|
-
|
|
76
|
+
{@render children?.()}
|
|
75
77
|
</a>
|
|
76
78
|
{:else}
|
|
77
79
|
<button class={mergedClasses()} {type} {disabled} {onclick} {...props}>
|
|
78
|
-
|
|
80
|
+
{@render children?.()}
|
|
79
81
|
</button>
|
|
80
82
|
{/if}
|
|
81
83
|
|
|
@@ -16,17 +16,32 @@
|
|
|
16
16
|
import Button from './Button.svelte';
|
|
17
17
|
|
|
18
18
|
// Props for the web component
|
|
19
|
+
interface Props {
|
|
20
|
+
variant?: 'primary' | 'secondary' | 'unstyled';
|
|
21
|
+
theme?: 'default' | 'inverted';
|
|
22
|
+
href?: string;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
size?: 'small' | 'medium' | 'large';
|
|
25
|
+
type?: 'button' | 'submit' | 'reset';
|
|
26
|
+
onclick?: (event: MouseEvent) => void;
|
|
27
|
+
class?: string;
|
|
28
|
+
children?: import('svelte').Snippet;
|
|
29
|
+
[key: `data-${string}`]: string;
|
|
30
|
+
[key: `aria-${string}`]: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
19
33
|
let {
|
|
20
34
|
variant = 'primary',
|
|
21
35
|
theme = 'default',
|
|
22
|
-
href =
|
|
36
|
+
href = undefined,
|
|
23
37
|
disabled = false,
|
|
24
38
|
size = 'medium',
|
|
25
39
|
type = 'button',
|
|
26
40
|
onclick,
|
|
27
41
|
class: className = '',
|
|
42
|
+
children,
|
|
28
43
|
...props
|
|
29
|
-
} = $props();
|
|
44
|
+
}: Props = $props();
|
|
30
45
|
</script>
|
|
31
46
|
|
|
32
47
|
<Button
|
|
@@ -37,8 +52,7 @@
|
|
|
37
52
|
{size}
|
|
38
53
|
{type}
|
|
39
54
|
{onclick}
|
|
55
|
+
{children}
|
|
40
56
|
class={className}
|
|
41
57
|
{...props}
|
|
42
|
-
>
|
|
43
|
-
<slot />
|
|
44
|
-
</Button>
|
|
58
|
+
></Button>
|
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
isExpanded,
|
|
5
5
|
selectedSubNavItemTitle,
|
|
6
6
|
} from '../stores/navigationStore.js';
|
|
7
|
-
import { onMount } from 'svelte';
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
let { customHeaderText = '' } = $props();
|
|
10
9
|
|
|
11
|
-
let isLayoutTransitioning = false;
|
|
10
|
+
let isLayoutTransitioning = $state(false);
|
|
12
11
|
|
|
13
12
|
const showHeader = $derived(
|
|
14
13
|
() =>
|
|
@@ -21,20 +20,19 @@
|
|
|
21
20
|
() => customHeaderText || $selectedSubNavItemTitle
|
|
22
21
|
);
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
$effect(() => {
|
|
24
|
+
if (typeof window === 'undefined') return;
|
|
25
25
|
const handleSectionVisible = (event: Event) => {
|
|
26
|
-
if (
|
|
26
|
+
if (window.innerWidth <= 640) {
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
let topLevelTitle = null;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/* ignore parse error */
|
|
37
|
-
}
|
|
30
|
+
topLevelTitle = sessionStorage.getItem('subNav-topLevelTitle');
|
|
31
|
+
if (topLevelTitle) {
|
|
32
|
+
try {
|
|
33
|
+
topLevelTitle = JSON.parse(topLevelTitle);
|
|
34
|
+
} catch {
|
|
35
|
+
/* ignore parse error */
|
|
38
36
|
}
|
|
39
37
|
}
|
|
40
38
|
if (typeof topLevelTitle === 'string' && topLevelTitle.trim() !== '') {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
3
|
+
* Initial selection for SSR/first paint.
|
|
4
|
+
* - Discover page: set ssrSelectedItem="discover".
|
|
5
|
+
* - Other pages: set ssrSelectedItem to the nav key (e.g., "api", "chat", "console").
|
|
6
|
+
* - Omit to default to expanded/open on SSR.
|
|
7
|
+
*
|
|
8
|
+
* controlledSelectedItem overrides the nav store after hydration.
|
|
9
|
+
* - Use for web component integrations where bundle/app stores can diverge.
|
|
10
|
+
* - In Svelte/SvelteKit apps, prefer NavigationInitializer and omit
|
|
11
|
+
* controlledSelectedItem to keep store-driven behavior.
|
|
7
12
|
*/
|
|
8
13
|
import { onMount } from 'svelte';
|
|
9
14
|
import {
|
|
@@ -21,8 +26,9 @@
|
|
|
21
26
|
import ProfileMenu from './ProfileMenu.svelte';
|
|
22
27
|
import HelpIcon from './icons/HelpIcon.svelte';
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
let {
|
|
25
30
|
ssrSelectedItem = undefined,
|
|
31
|
+
controlledSelectedItem = undefined,
|
|
26
32
|
onNavToggle = undefined,
|
|
27
33
|
onNavItemClick = undefined,
|
|
28
34
|
showAppIcons = true,
|
|
@@ -33,9 +39,10 @@
|
|
|
33
39
|
consoleUrl,
|
|
34
40
|
discoverUrl,
|
|
35
41
|
supplementalMenuData,
|
|
42
|
+
children,
|
|
36
43
|
} = $props();
|
|
37
44
|
|
|
38
|
-
const helpUrl = supplementalMenuData?.help?.url?.trim();
|
|
45
|
+
const helpUrl = $derived(() => supplementalMenuData?.help?.url?.trim());
|
|
39
46
|
|
|
40
47
|
// Check if we're on the help page by comparing URLs (help is not a nav/subnav item)
|
|
41
48
|
function getPathname(url: string, baseOrigin: string) {
|
|
@@ -47,23 +54,24 @@
|
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
function checkIsHelpPage(): boolean {
|
|
50
|
-
|
|
57
|
+
const url = helpUrl();
|
|
58
|
+
if (!url || url === '#' || url.startsWith('#')) {
|
|
51
59
|
return false;
|
|
52
60
|
}
|
|
53
61
|
const location = (
|
|
54
62
|
globalThis as { location?: { pathname?: string; origin?: string } }
|
|
55
63
|
).location;
|
|
56
64
|
if (location?.origin && location?.pathname) {
|
|
57
|
-
const helpPath = getPathname(
|
|
65
|
+
const helpPath = getPathname(url, location.origin);
|
|
58
66
|
return helpPath !== null && location.pathname === helpPath;
|
|
59
67
|
}
|
|
60
68
|
return false;
|
|
61
69
|
}
|
|
62
70
|
|
|
63
|
-
const isHelpSelected = $derived(checkIsHelpPage());
|
|
71
|
+
const isHelpSelected = $derived(() => checkIsHelpPage());
|
|
64
72
|
|
|
65
|
-
let hydrated = false;
|
|
66
|
-
let navContainer
|
|
73
|
+
let hydrated = $state(false);
|
|
74
|
+
let navContainer = $state<HTMLDivElement | null>(null);
|
|
67
75
|
let hoveredItem = $state<string | null>(null);
|
|
68
76
|
let menuButtonHovered = $state(false);
|
|
69
77
|
let canHover = $state(false);
|
|
@@ -79,8 +87,16 @@
|
|
|
79
87
|
return hydrated ? hydratedValue : ssrValue;
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
const selectedForUi = $derived(() => controlledSelectedItem ?? $selectedItem);
|
|
91
|
+
const ssrSelectedForUi = $derived(
|
|
92
|
+
() => controlledSelectedItem ?? ssrSelectedItem
|
|
93
|
+
);
|
|
94
|
+
const ssrSelected = $derived(() => ssrSelectedForUi());
|
|
95
|
+
const ssrIsDiscover = $derived(() => ssrSelected() === 'discover');
|
|
96
|
+
const isMenuDisabledForUi = $derived(() => selectedForUi() === 'discover');
|
|
97
|
+
|
|
82
98
|
function isNavSelected(key: NavigationItem) {
|
|
83
|
-
return ssrOrHydrated(
|
|
99
|
+
return ssrOrHydrated(selectedForUi() === key, ssrSelectedForUi() === key);
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
function isNavHovered(key: string) {
|
|
@@ -88,31 +104,36 @@
|
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
const sideNavExpanded = $derived(() =>
|
|
91
|
-
ssrOrHydrated(
|
|
92
|
-
$isExpanded,
|
|
93
|
-
ssrSelectedItem ? ssrSelectedItem !== 'discover' : true
|
|
94
|
-
)
|
|
107
|
+
ssrOrHydrated($isExpanded, ssrSelected() ? !ssrIsDiscover() : true)
|
|
95
108
|
);
|
|
96
109
|
const menuBtnDisabled = $derived(() =>
|
|
97
110
|
ssrOrHydrated(
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
isMenuDisabledForUi(),
|
|
112
|
+
ssrSelected() ? ssrIsDiscover() : false
|
|
100
113
|
)
|
|
101
114
|
);
|
|
102
115
|
const menuBtnOpen = $derived(() =>
|
|
103
|
-
ssrOrHydrated(
|
|
104
|
-
$isExpanded,
|
|
105
|
-
ssrSelectedItem ? ssrSelectedItem !== 'discover' : true
|
|
106
|
-
)
|
|
116
|
+
ssrOrHydrated($isExpanded, ssrSelected() ? !ssrIsDiscover() : true)
|
|
107
117
|
);
|
|
108
118
|
const menuBtnHovered = $derived(
|
|
109
119
|
() => canHover && menuButtonHovered && !menuBtnDisabled()
|
|
110
120
|
);
|
|
111
121
|
|
|
112
122
|
onMount(() => {
|
|
123
|
+
if (typeof window === 'undefined') return;
|
|
124
|
+
|
|
113
125
|
hydrated = true;
|
|
126
|
+
|
|
127
|
+
if (
|
|
128
|
+
controlledSelectedItem === undefined &&
|
|
129
|
+
ssrSelectedItem &&
|
|
130
|
+
$selectedItem === null
|
|
131
|
+
) {
|
|
132
|
+
navigationStore.setSelectedItem(ssrSelectedItem);
|
|
133
|
+
}
|
|
114
134
|
// Remove the CSS class since reactive state takes over
|
|
115
135
|
document.documentElement.classList.remove('nav-initially-expanded');
|
|
136
|
+
|
|
116
137
|
// Remove the custom property since reactive CSS takes over
|
|
117
138
|
document.documentElement.style.removeProperty('--initial-main-top-padding');
|
|
118
139
|
// Add hydrated class to re-enable transitions
|
|
@@ -121,106 +142,120 @@
|
|
|
121
142
|
}
|
|
122
143
|
|
|
123
144
|
// Detect if device supports hover (desktop only)
|
|
124
|
-
|
|
125
|
-
|
|
145
|
+
const mq =
|
|
146
|
+
typeof window.matchMedia === 'function'
|
|
147
|
+
? window.matchMedia('(hover: hover)')
|
|
148
|
+
: null;
|
|
149
|
+
const handleHoverChange = (e: MediaQueryListEvent) => {
|
|
150
|
+
canHover = e.matches;
|
|
151
|
+
};
|
|
152
|
+
if (mq) {
|
|
126
153
|
canHover = mq.matches;
|
|
127
|
-
mq.addEventListener('change',
|
|
128
|
-
|
|
129
|
-
|
|
154
|
+
mq.addEventListener('change', handleHoverChange);
|
|
155
|
+
} else {
|
|
156
|
+
canHover = false;
|
|
157
|
+
}
|
|
130
158
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
159
|
+
// Listen for layout transitions
|
|
160
|
+
const handleLayoutTransition = (event: Event) => {
|
|
161
|
+
const customEvent = event as CustomEvent;
|
|
162
|
+
const { isMobile } = customEvent.detail;
|
|
135
163
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
164
|
+
if (isMobile) {
|
|
165
|
+
// Switching to mobile: close navigation
|
|
166
|
+
navigationStore.setExpanded(false);
|
|
167
|
+
} else {
|
|
168
|
+
// Switching to desktop: open navigation if on non-discover page
|
|
169
|
+
const currentItem = selectedForUi();
|
|
170
|
+
|
|
171
|
+
if (currentItem && currentItem !== 'discover') {
|
|
172
|
+
navigationStore.setExpanded(true);
|
|
139
173
|
} else {
|
|
140
|
-
// Switching to desktop: open navigation if on non-discover page
|
|
141
|
-
const currentItem = $selectedItem;
|
|
142
|
-
|
|
143
|
-
if (currentItem && currentItem !== 'discover') {
|
|
144
|
-
navigationStore.setExpanded(true);
|
|
145
|
-
} else {
|
|
146
|
-
navigationStore.setExpanded(false);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
window.addEventListener('layoutTransition', handleLayoutTransition);
|
|
152
|
-
|
|
153
|
-
// Add Escape key handler for nav collapse, but ignore if focus is in profile context menu
|
|
154
|
-
const handleEscape = (e: KeyboardEvent) => {
|
|
155
|
-
const active = document.activeElement;
|
|
156
|
-
if (
|
|
157
|
-
e.key === 'Escape' &&
|
|
158
|
-
$isExpanded &&
|
|
159
|
-
!(active && active.closest('.profile-context-menu'))
|
|
160
|
-
) {
|
|
161
174
|
navigationStore.setExpanded(false);
|
|
162
175
|
}
|
|
163
|
-
}
|
|
164
|
-
|
|
176
|
+
}
|
|
177
|
+
};
|
|
165
178
|
|
|
166
|
-
|
|
167
|
-
const handleDesktopNavControlEvent = (event: Event) => {
|
|
168
|
-
const customEvent = event as CustomEvent;
|
|
169
|
-
const { action, isOpen: eventIsOpen } = customEvent.detail;
|
|
179
|
+
window.addEventListener('layoutTransition', handleLayoutTransition);
|
|
170
180
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
// Add Escape key handler for nav collapse, but ignore if focus is in profile context menu
|
|
182
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
183
|
+
const active = document.activeElement;
|
|
184
|
+
const profileMenuOpen = Boolean(
|
|
185
|
+
document.querySelector('.profile-context-menu')
|
|
186
|
+
);
|
|
187
|
+
if (
|
|
188
|
+
e.key === 'Escape' &&
|
|
189
|
+
$isExpanded &&
|
|
190
|
+
!profileMenuOpen &&
|
|
191
|
+
!(active && active.closest('.profile-context-menu'))
|
|
192
|
+
) {
|
|
193
|
+
navigationStore.setExpanded(false);
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
window.addEventListener('keydown', handleEscape);
|
|
197
|
+
|
|
198
|
+
// Custom event system for external control
|
|
199
|
+
const handleDesktopNavControlEvent = (event: Event) => {
|
|
200
|
+
const customEvent = event as CustomEvent;
|
|
201
|
+
const { action, isOpen: eventIsOpen } = customEvent.detail;
|
|
202
|
+
|
|
203
|
+
if (action === 'toggle') {
|
|
204
|
+
navigationStore.toggleExpanded();
|
|
205
|
+
} else if (action === 'open') {
|
|
206
|
+
navigationStore.setExpanded(true);
|
|
207
|
+
} else if (action === 'close') {
|
|
208
|
+
navigationStore.setExpanded(false);
|
|
209
|
+
} else if (action === 'set' && typeof eventIsOpen === 'boolean') {
|
|
210
|
+
navigationStore.setExpanded(eventIsOpen);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// Listen for custom desktop navigation control events
|
|
215
|
+
window.addEventListener(
|
|
216
|
+
'graymatter-desktop-nav-control',
|
|
217
|
+
handleDesktopNavControlEvent
|
|
218
|
+
);
|
|
181
219
|
|
|
182
|
-
|
|
183
|
-
window.
|
|
220
|
+
eventListenerCleanup = () => {
|
|
221
|
+
window.removeEventListener(
|
|
184
222
|
'graymatter-desktop-nav-control',
|
|
185
223
|
handleDesktopNavControlEvent
|
|
186
224
|
);
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
eventListenerCleanup();
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Listen for navigation changes to update help button state
|
|
228
|
+
const handleNavigationChange = () => {
|
|
229
|
+
// Force reactivity update by accessing the derived value
|
|
230
|
+
// Accessing isHelpSelected triggers reactivity
|
|
231
|
+
void isHelpSelected();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
window.addEventListener('popstate', handleNavigationChange);
|
|
235
|
+
|
|
236
|
+
return () => {
|
|
237
|
+
window.removeEventListener('layoutTransition', handleLayoutTransition);
|
|
238
|
+
window.removeEventListener('keydown', handleEscape);
|
|
239
|
+
window.removeEventListener('popstate', handleNavigationChange);
|
|
240
|
+
if (mq) {
|
|
241
|
+
mq.removeEventListener('change', handleHoverChange);
|
|
242
|
+
}
|
|
243
|
+
if (eventListenerCleanup) {
|
|
244
|
+
eventListenerCleanup();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
213
247
|
});
|
|
214
248
|
|
|
215
249
|
function handleHelpClick(event: MouseEvent) {
|
|
216
250
|
// If it's a hash link, prevent default to stay on same page
|
|
217
|
-
|
|
251
|
+
const url = helpUrl();
|
|
252
|
+
if (url === '#' || url?.startsWith('#')) {
|
|
218
253
|
event.preventDefault();
|
|
219
254
|
}
|
|
220
255
|
}
|
|
221
256
|
|
|
222
257
|
function toggleNav() {
|
|
223
|
-
if (
|
|
258
|
+
if (menuBtnDisabled()) return;
|
|
224
259
|
navigationStore.toggleExpanded();
|
|
225
260
|
onNavToggle?.({ expanded: $isExpanded });
|
|
226
261
|
}
|
|
@@ -231,7 +266,9 @@
|
|
|
231
266
|
sessionStorage.setItem('navigatingToMainNav', 'true');
|
|
232
267
|
}
|
|
233
268
|
|
|
234
|
-
|
|
269
|
+
if (controlledSelectedItem === undefined) {
|
|
270
|
+
navigationStore.setSelectedItem(item);
|
|
271
|
+
}
|
|
235
272
|
|
|
236
273
|
// Clear header state for main navigation items (including discover)
|
|
237
274
|
if (item !== 'discover') {
|
|
@@ -470,16 +507,16 @@
|
|
|
470
507
|
|
|
471
508
|
{#if profileMenuData}
|
|
472
509
|
<div class="nav-footer" part="nav-footer">
|
|
473
|
-
{#if helpUrl}
|
|
510
|
+
{#if helpUrl()}
|
|
474
511
|
<div
|
|
475
512
|
class="nav-footer-icon-container"
|
|
476
513
|
part="nav-footer-icon-container"
|
|
477
514
|
>
|
|
478
515
|
<a
|
|
479
516
|
class="help-link"
|
|
480
|
-
class:selected={isHelpSelected}
|
|
481
|
-
aria-current={isHelpSelected ? 'page' : undefined}
|
|
482
|
-
href={helpUrl}
|
|
517
|
+
class:selected={isHelpSelected()}
|
|
518
|
+
aria-current={isHelpSelected() ? 'page' : undefined}
|
|
519
|
+
href={helpUrl()}
|
|
483
520
|
part="help-link"
|
|
484
521
|
title="Help"
|
|
485
522
|
aria-label="Help"
|
|
@@ -487,7 +524,7 @@
|
|
|
487
524
|
>
|
|
488
525
|
<div class="icon-container" part="help-icon-container">
|
|
489
526
|
<div class="icon-wrapper">
|
|
490
|
-
<HelpIcon isSelected={isHelpSelected} />
|
|
527
|
+
<HelpIcon isSelected={isHelpSelected()} />
|
|
491
528
|
</div>
|
|
492
529
|
</div>
|
|
493
530
|
</a>
|
|
@@ -513,7 +550,7 @@
|
|
|
513
550
|
{#if sideNavExpanded()}
|
|
514
551
|
<div class="drawer-content" part="drawer-content">
|
|
515
552
|
<nav aria-label="Sub navigation" part="sub-navigation">
|
|
516
|
-
|
|
553
|
+
{@render children?.()}
|
|
517
554
|
</nav>
|
|
518
555
|
</div>
|
|
519
556
|
{/if}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// Props for the web component
|
|
11
11
|
let {
|
|
12
12
|
ssrSelectedItem,
|
|
13
|
+
controlledSelectedItem,
|
|
13
14
|
onNavToggle,
|
|
14
15
|
onNavItemClick,
|
|
15
16
|
showAppIcons = true,
|
|
@@ -20,11 +21,13 @@
|
|
|
20
21
|
consoleUrl,
|
|
21
22
|
discoverUrl,
|
|
22
23
|
supplementalMenuData,
|
|
24
|
+
children,
|
|
23
25
|
} = $props();
|
|
24
26
|
</script>
|
|
25
27
|
|
|
26
28
|
<DesktopSideNav
|
|
27
29
|
{ssrSelectedItem}
|
|
30
|
+
{controlledSelectedItem}
|
|
28
31
|
{onNavToggle}
|
|
29
32
|
{onNavItemClick}
|
|
30
33
|
{showAppIcons}
|
|
@@ -35,6 +38,5 @@
|
|
|
35
38
|
{consoleUrl}
|
|
36
39
|
{discoverUrl}
|
|
37
40
|
{supplementalMenuData}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</DesktopSideNav>
|
|
41
|
+
{children}
|
|
42
|
+
/>
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
...restProps
|
|
9
9
|
} = $props();
|
|
10
10
|
|
|
11
|
-
const logoColor =
|
|
12
|
-
? 'var(--ai-color-white)'
|
|
13
|
-
|
|
11
|
+
const logoColor = $derived(() =>
|
|
12
|
+
isInverted ? 'var(--ai-color-white)' : 'var(--ai-color-black)'
|
|
13
|
+
);
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
{#if !showServiceMark}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
viewBox="0 0 102 30"
|
|
21
21
|
fill="none"
|
|
22
22
|
xmlns="http://www.w3.org/2000/svg"
|
|
23
|
-
style="fill: {logoColor};"
|
|
23
|
+
style="fill: {logoColor()};"
|
|
24
24
|
{...restProps}
|
|
25
25
|
>
|
|
26
26
|
<g>
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
viewBox="0 0 75 20"
|
|
45
45
|
fill="none"
|
|
46
46
|
xmlns="http://www.w3.org/2000/svg"
|
|
47
|
-
style="fill: {logoColor};"
|
|
47
|
+
style="fill: {logoColor()};"
|
|
48
48
|
{...restProps}
|
|
49
49
|
>
|
|
50
50
|
<path
|