@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
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
chatUrl,
|
|
15
15
|
consoleUrl,
|
|
16
16
|
discoverUrl,
|
|
17
|
+
controlledSelectedItem = undefined,
|
|
17
18
|
}: {
|
|
18
19
|
onNavItemClick?: (data: {
|
|
19
20
|
item: NavigationItem;
|
|
@@ -25,29 +26,37 @@
|
|
|
25
26
|
chatUrl?: string;
|
|
26
27
|
consoleUrl?: string;
|
|
27
28
|
discoverUrl?: string;
|
|
29
|
+
controlledSelectedItem?: NavigationItem;
|
|
28
30
|
} = $props();
|
|
29
31
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
32
|
+
const selectedForUi = $derived(() => controlledSelectedItem ?? $selectedItem);
|
|
33
|
+
|
|
34
|
+
const allNavItems = $derived(
|
|
35
|
+
() =>
|
|
36
|
+
[
|
|
37
|
+
{
|
|
38
|
+
id: 'discover',
|
|
39
|
+
label: 'Discover',
|
|
40
|
+
icon: DiscoverIcon,
|
|
41
|
+
href: discoverUrl || '#',
|
|
42
|
+
},
|
|
43
|
+
{ id: 'chat', label: 'Chat', icon: ChatIcon, href: chatUrl || '#' },
|
|
44
|
+
{
|
|
45
|
+
id: 'console',
|
|
46
|
+
label: 'Console',
|
|
47
|
+
icon: ConsoleIcon,
|
|
48
|
+
href: consoleUrl || '#',
|
|
49
|
+
},
|
|
50
|
+
{ id: 'api', label: 'API', icon: ApiIcon, href: apiUrl || '#' },
|
|
51
|
+
] as const
|
|
52
|
+
);
|
|
46
53
|
|
|
47
54
|
// Filter out discover item if hideDiscover is true
|
|
48
|
-
const navItems =
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
const navItems = $derived(() =>
|
|
56
|
+
hideDiscover
|
|
57
|
+
? allNavItems().filter(item => item.id !== 'discover')
|
|
58
|
+
: allNavItems()
|
|
59
|
+
);
|
|
51
60
|
|
|
52
61
|
function handleNavItemClick(item: NavigationItem) {
|
|
53
62
|
// Set navigation flag immediately to prevent header restoration
|
|
@@ -55,7 +64,9 @@
|
|
|
55
64
|
sessionStorage.setItem('navigatingToMainNav', 'true');
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
|
|
67
|
+
if (controlledSelectedItem === undefined) {
|
|
68
|
+
navigationStore.setSelectedItem(item);
|
|
69
|
+
}
|
|
59
70
|
|
|
60
71
|
// Clear header state for main navigation items (including discover)
|
|
61
72
|
if (item !== 'discover') {
|
|
@@ -126,16 +137,16 @@
|
|
|
126
137
|
|
|
127
138
|
{#if showAppIcons}
|
|
128
139
|
<nav class="mobile-bottom-nav" part="mobile-bottom-nav">
|
|
129
|
-
{#each navItems as navItem}
|
|
140
|
+
{#each navItems() as navItem}
|
|
130
141
|
<a
|
|
131
142
|
href={navItem.href}
|
|
132
143
|
class={`nav-item ${navItem.id}-item`}
|
|
133
|
-
class:active={
|
|
144
|
+
class:active={selectedForUi() === navItem.id}
|
|
134
145
|
onclick={() => handleNavItemClick(navItem.id)}
|
|
135
146
|
part={`${navItem.id}-item`}
|
|
136
147
|
>
|
|
137
148
|
<div class="icon">
|
|
138
|
-
<navItem.icon isSelected={
|
|
149
|
+
<navItem.icon isSelected={selectedForUi() === navItem.id} />
|
|
139
150
|
</div>
|
|
140
151
|
<span class="label">{navItem.label}</span>
|
|
141
152
|
</a>
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
2
|
import {
|
|
4
3
|
selectedItem,
|
|
5
4
|
isExpanded,
|
|
@@ -8,10 +7,10 @@
|
|
|
8
7
|
import Logo from './Logo.svelte';
|
|
9
8
|
import MenuIcon from './icons/MenuIcon.svelte';
|
|
10
9
|
|
|
11
|
-
let sideMenuElement
|
|
10
|
+
let sideMenuElement = $state<HTMLDivElement | null>(null);
|
|
12
11
|
let pendingMobileHash: string | null = null;
|
|
13
12
|
|
|
14
|
-
let { logoLinkUrl, logoLinkLabel } = $props();
|
|
13
|
+
let { logoLinkUrl, logoLinkLabel, children } = $props();
|
|
15
14
|
|
|
16
15
|
function handleMobileAnchorNavigate(event: Event) {
|
|
17
16
|
const customEvent = event as CustomEvent;
|
|
@@ -82,7 +81,8 @@
|
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
$effect(() => {
|
|
85
|
+
if (typeof window === 'undefined') return;
|
|
86
86
|
// Listen for the custom event globally
|
|
87
87
|
window.addEventListener('mobileAnchorNavigate', handleMobileAnchorNavigate);
|
|
88
88
|
|
|
@@ -180,7 +180,7 @@
|
|
|
180
180
|
<!-- Show submenu items if available and not on discover -->
|
|
181
181
|
{#if $selectedItem && $selectedItem !== 'discover'}
|
|
182
182
|
<div class="subnav-section" part="subnav-section">
|
|
183
|
-
|
|
183
|
+
{@render children?.()}
|
|
184
184
|
</div>
|
|
185
185
|
{:else}
|
|
186
186
|
<!-- Show message when no submenu is available -->
|
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
<script lang="ts">
|
|
8
8
|
import MobileSideMenu from './MobileSideMenu.svelte';
|
|
9
9
|
|
|
10
|
-
let { logoLinkUrl, logoLinkLabel } = $props();
|
|
10
|
+
let { logoLinkUrl, logoLinkLabel, children } = $props();
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
|
-
<MobileSideMenu {logoLinkUrl} {logoLinkLabel}
|
|
14
|
-
<slot />
|
|
15
|
-
</MobileSideMenu>
|
|
13
|
+
<MobileSideMenu {logoLinkUrl} {logoLinkLabel} {children} />
|
|
@@ -7,14 +7,13 @@
|
|
|
7
7
|
import Logo from './Logo.svelte';
|
|
8
8
|
import MenuIcon from './icons/MenuIcon.svelte';
|
|
9
9
|
import HelpIcon from './icons/HelpIcon.svelte';
|
|
10
|
-
import { onMount } from 'svelte';
|
|
11
10
|
|
|
12
11
|
let { profileMenuData, logoLinkUrl, logoLinkLabel, supplementalMenuData } =
|
|
13
12
|
$props();
|
|
14
13
|
|
|
15
14
|
import ProfileMenu from './ProfileMenu.svelte';
|
|
16
15
|
|
|
17
|
-
const helpUrl = supplementalMenuData?.help?.url?.trim();
|
|
16
|
+
const helpUrl = $derived(() => supplementalMenuData?.help?.url?.trim());
|
|
18
17
|
|
|
19
18
|
// Check if we're on the help page by comparing URLs (help is not a nav/subnav item)
|
|
20
19
|
function getPathname(url: string, baseOrigin: string) {
|
|
@@ -26,7 +25,8 @@
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
function checkIsHelpPage(): boolean {
|
|
29
|
-
|
|
28
|
+
const url = helpUrl();
|
|
29
|
+
if (!url || url === '#' || url.startsWith('#')) {
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
32
|
const location = (
|
|
@@ -35,33 +35,30 @@
|
|
|
35
35
|
}
|
|
36
36
|
).location;
|
|
37
37
|
if (location?.origin && location?.pathname) {
|
|
38
|
-
const helpPath = getPathname(
|
|
38
|
+
const helpPath = getPathname(url, location.origin);
|
|
39
39
|
return helpPath !== null && location.pathname === helpPath;
|
|
40
40
|
}
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const isHelpSelected = $derived(checkIsHelpPage());
|
|
44
|
+
const isHelpSelected = $derived(() => checkIsHelpPage());
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
$effect(() => {
|
|
47
|
+
if (typeof window === 'undefined') return;
|
|
47
48
|
// Listen for section visibility changes to update header
|
|
48
49
|
const handleSectionVisible = (event: Event) => {
|
|
49
50
|
const customEvent = event as CustomEvent;
|
|
50
51
|
if (customEvent.detail && customEvent.detail.title) {
|
|
51
52
|
selectedSubNavItemTitle.set(customEvent.detail.title);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
}
|
|
53
|
+
sessionStorage.setItem(
|
|
54
|
+
'subNav-selectedTitle',
|
|
55
|
+
JSON.stringify(customEvent.detail.title)
|
|
56
|
+
);
|
|
58
57
|
}
|
|
59
58
|
};
|
|
60
59
|
|
|
61
60
|
// Set default header based on first submenu item
|
|
62
61
|
const setDefaultHeaderFromFirstSubmenu = () => {
|
|
63
|
-
if (typeof window === 'undefined') return;
|
|
64
|
-
|
|
65
62
|
const mainContent = document.getElementById('main-content');
|
|
66
63
|
if (!mainContent) return;
|
|
67
64
|
|
|
@@ -75,12 +72,10 @@
|
|
|
75
72
|
// Only set if no header is currently set
|
|
76
73
|
if (!$selectedSubNavItemTitle) {
|
|
77
74
|
selectedSubNavItemTitle.set(firstSectionTitle);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
}
|
|
75
|
+
sessionStorage.setItem(
|
|
76
|
+
'subNav-selectedTitle',
|
|
77
|
+
JSON.stringify(firstSectionTitle)
|
|
78
|
+
);
|
|
84
79
|
}
|
|
85
80
|
}
|
|
86
81
|
};
|
|
@@ -98,7 +93,7 @@
|
|
|
98
93
|
// Listen for navigation changes to update help button state
|
|
99
94
|
const handleNavigationChange = () => {
|
|
100
95
|
// Force reactivity update by accessing the derived value
|
|
101
|
-
void isHelpSelected;
|
|
96
|
+
void isHelpSelected();
|
|
102
97
|
};
|
|
103
98
|
|
|
104
99
|
window.addEventListener('popstate', handleNavigationChange);
|
|
@@ -115,7 +110,7 @@
|
|
|
115
110
|
|
|
116
111
|
function handleHelpClick(event: MouseEvent) {
|
|
117
112
|
// If it's a hash link, prevent default to stay on same page
|
|
118
|
-
if (helpUrl === '#' || helpUrl?.startsWith('#')) {
|
|
113
|
+
if (helpUrl() === '#' || helpUrl()?.startsWith('#')) {
|
|
119
114
|
event.preventDefault();
|
|
120
115
|
}
|
|
121
116
|
}
|
|
@@ -145,18 +140,18 @@
|
|
|
145
140
|
<span class="brand-text">{$selectedSubNavItemTitle || ''}</span>
|
|
146
141
|
</div>
|
|
147
142
|
|
|
148
|
-
{#if helpUrl}
|
|
143
|
+
{#if helpUrl()}
|
|
149
144
|
<a
|
|
150
145
|
class="help-button"
|
|
151
|
-
class:selected={isHelpSelected}
|
|
152
|
-
aria-current={isHelpSelected ? 'page' : undefined}
|
|
153
|
-
href={helpUrl}
|
|
146
|
+
class:selected={isHelpSelected()}
|
|
147
|
+
aria-current={isHelpSelected() ? 'page' : undefined}
|
|
148
|
+
href={helpUrl()}
|
|
154
149
|
title="Help"
|
|
155
150
|
aria-label="Help"
|
|
156
151
|
onclick={handleHelpClick}
|
|
157
152
|
>
|
|
158
153
|
<span class="icon-container">
|
|
159
|
-
<HelpIcon isSelected={isHelpSelected} />
|
|
154
|
+
<HelpIcon isSelected={isHelpSelected()} />
|
|
160
155
|
</span>
|
|
161
156
|
</a>
|
|
162
157
|
{/if}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
2
|
import {
|
|
4
3
|
navigationStore,
|
|
5
4
|
type NavigationItem,
|
|
6
5
|
} from '../stores/navigationStore.js';
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
interface Props {
|
|
8
|
+
item: NavigationItem;
|
|
9
|
+
autoExpand?: boolean;
|
|
10
|
+
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
let { item, autoExpand = false }: Props = $props();
|
|
13
|
+
|
|
14
|
+
$effect(() => {
|
|
12
15
|
if (item) {
|
|
13
16
|
navigationStore.setSelectedItem(item);
|
|
14
17
|
// Auto-expand for non-discover pages on desktop
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
3
|
-
|
|
4
2
|
/**
|
|
5
3
|
* ProfileMenu Component - Event-Driven Architecture with Shadow DOM Support
|
|
6
4
|
*
|
|
@@ -71,14 +69,22 @@
|
|
|
71
69
|
* Array of menu items to render in the profile menu.
|
|
72
70
|
* Each item: { label: string, icon: string, action: string }
|
|
73
71
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
interface Props {
|
|
73
|
+
menuItems?: Array<{ label: string; icon: string; action: string }>;
|
|
74
|
+
userMeta: { initials: string };
|
|
75
|
+
ariaLabel?: string;
|
|
76
|
+
menuAlign?: 'desktop' | 'mobile';
|
|
77
|
+
}
|
|
79
78
|
|
|
80
|
-
let
|
|
81
|
-
|
|
79
|
+
let {
|
|
80
|
+
menuItems = [],
|
|
81
|
+
userMeta,
|
|
82
|
+
ariaLabel = 'Open profile menu',
|
|
83
|
+
menuAlign = 'desktop',
|
|
84
|
+
}: Props = $props();
|
|
85
|
+
|
|
86
|
+
const menuId = `profile-menu-${Math.random().toString(36).slice(2, 10)}`;
|
|
87
|
+
let menuOpen = $state(false);
|
|
82
88
|
function toggleMenu() {
|
|
83
89
|
menuOpen = !menuOpen;
|
|
84
90
|
}
|
|
@@ -118,12 +124,14 @@
|
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
let profileButton
|
|
127
|
+
let profileButton = $state<HTMLButtonElement | null>(null);
|
|
122
128
|
|
|
123
129
|
// For keyboard navigation, keep refs to all menu item buttons
|
|
124
|
-
let menuItemButtons
|
|
125
|
-
let btns
|
|
126
|
-
|
|
130
|
+
let menuItemButtons = $state<HTMLButtonElement[]>([]);
|
|
131
|
+
let btns = $state<(HTMLButtonElement | null)[]>([]);
|
|
132
|
+
$effect(() => {
|
|
133
|
+
menuItemButtons = btns.filter(Boolean) as HTMLButtonElement[];
|
|
134
|
+
});
|
|
127
135
|
|
|
128
136
|
function handleMenuKeydown(e: KeyboardEvent, idx: number) {
|
|
129
137
|
if (e.key === 'ArrowDown') {
|
|
@@ -149,7 +157,8 @@
|
|
|
149
157
|
}
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
|
|
160
|
+
$effect(() => {
|
|
161
|
+
if (typeof window === 'undefined') return;
|
|
153
162
|
window.addEventListener('keydown', handleGlobalEscape, true);
|
|
154
163
|
return () => {
|
|
155
164
|
window.removeEventListener('keydown', handleGlobalEscape, true);
|
|
@@ -165,8 +174,8 @@
|
|
|
165
174
|
aria-haspopup="menu"
|
|
166
175
|
aria-expanded={menuOpen}
|
|
167
176
|
aria-controls={menuId}
|
|
168
|
-
|
|
169
|
-
|
|
177
|
+
onclick={toggleMenu}
|
|
178
|
+
onblur={handleBlur}
|
|
170
179
|
bind:this={profileButton}
|
|
171
180
|
>
|
|
172
181
|
<div class="user-profile-container">
|
|
@@ -189,12 +198,12 @@
|
|
|
189
198
|
<button
|
|
190
199
|
type="button"
|
|
191
200
|
class="menu-item"
|
|
192
|
-
|
|
201
|
+
onclick={() => {
|
|
193
202
|
executeMenuAction(item.action);
|
|
194
203
|
closeMenu();
|
|
195
204
|
}}
|
|
196
205
|
bind:this={btns[idx]}
|
|
197
|
-
|
|
206
|
+
onkeydown={e => handleMenuKeydown(e, idx)}
|
|
198
207
|
role="menuitem"
|
|
199
208
|
>
|
|
200
209
|
{#if item.icon}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
widescreen: 'ai-maxw-widescreen',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const mappedSize = sizeMap[maxWidth] || sizeMap.none;
|
|
16
|
+
const mappedSize = $derived(() => sizeMap[maxWidth] || sizeMap.none);
|
|
17
17
|
</script>
|
|
18
18
|
|
|
19
19
|
<section
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
class="usa-banner__header ai-banner__header"
|
|
27
27
|
aria-label="USA banner"
|
|
28
28
|
>
|
|
29
|
-
<div class="usa-banner__inner {mappedSize}">
|
|
29
|
+
<div class="usa-banner__inner {mappedSize()}">
|
|
30
30
|
<div class="grid-col-auto">
|
|
31
31
|
<img
|
|
32
32
|
aria-hidden="true"
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
</div>
|
|
55
55
|
</header>
|
|
56
56
|
<div
|
|
57
|
-
class="usa-banner__content usa-accordion__content ai-banner__content {mappedSize}"
|
|
57
|
+
class="usa-banner__content usa-accordion__content ai-banner__content {mappedSize()}"
|
|
58
58
|
id="gov-banner-default"
|
|
59
59
|
>
|
|
60
60
|
<div class="grid-row grid-gap-lg">
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
interface Props {
|
|
3
|
+
isDisabled?: boolean;
|
|
4
|
+
isHovered?: boolean;
|
|
5
|
+
isOpen?: boolean;
|
|
6
|
+
[key: string]: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
isDisabled = false,
|
|
11
|
+
isHovered = false,
|
|
12
|
+
isOpen = false,
|
|
13
|
+
// eslint-disable-next-line svelte/valid-compile -- allow passthrough svg props.
|
|
14
|
+
...restProps
|
|
15
|
+
}: Props = $props();
|
|
5
16
|
</script>
|
|
6
17
|
|
|
7
18
|
<svg
|
|
@@ -10,7 +21,7 @@
|
|
|
10
21
|
height="24"
|
|
11
22
|
viewBox="0 0 24 24"
|
|
12
23
|
fill="none"
|
|
13
|
-
{
|
|
24
|
+
{...restProps}
|
|
14
25
|
>
|
|
15
26
|
{#if isDisabled}
|
|
16
27
|
<!-- Disabled state -->
|