@gsa-tts/graymatter-ui 0.4.9 → 0.4.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsa-tts/graymatter-ui",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -38,6 +38,7 @@
38
38
  } = $props();
39
39
 
40
40
  const helpUrl = supplementalMenuData?.help?.url?.trim();
41
+ const isHelpSelected = $derived(() => get(selectedSubNavItemId) === 'help');
41
42
 
42
43
  let hydrated = false;
43
44
  let navContainer: HTMLDivElement;
@@ -169,9 +170,40 @@
169
170
  );
170
171
  };
171
172
 
173
+ // Detect help page and set selection state
174
+ const checkHelpPage = () => {
175
+ // Skip check for hash links - they stay on the same page
176
+ if (!helpUrl || helpUrl === '#' || helpUrl.startsWith('#')) {
177
+ return;
178
+ }
179
+
180
+ const location = (
181
+ globalThis as { location?: { pathname?: string; origin?: string } }
182
+ ).location;
183
+ if (location?.origin && location?.pathname) {
184
+ const helpPath = getPathname(helpUrl, location.origin);
185
+ if (helpPath && location.pathname === helpPath) {
186
+ selectedSubNavItemId.set('help');
187
+ } else if (
188
+ get(selectedSubNavItemId) === 'help' &&
189
+ helpPath &&
190
+ location.pathname !== helpPath
191
+ ) {
192
+ // Clear help selection if we navigated away from help page
193
+ selectedSubNavItemId.set(null);
194
+ }
195
+ }
196
+ };
197
+
198
+ checkHelpPage();
199
+
200
+ // Also listen for popstate events (back/forward navigation)
201
+ window.addEventListener('popstate', checkHelpPage);
202
+
172
203
  return () => {
173
204
  window.removeEventListener('layoutTransition', handleLayoutTransition);
174
205
  window.removeEventListener('keydown', handleEscape);
206
+ window.removeEventListener('popstate', checkHelpPage);
175
207
  if (eventListenerCleanup) {
176
208
  eventListenerCleanup();
177
209
  }
@@ -179,6 +211,25 @@
179
211
  }
180
212
  });
181
213
 
214
+ function getPathname(url: string, baseOrigin: string) {
215
+ try {
216
+ return new URL(url, baseOrigin).pathname;
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ function handleHelpClick(event: MouseEvent) {
223
+ selectedSubNavItemId.set('help');
224
+
225
+ // If it's a hash link, prevent default to maintain state on same page
226
+ if (helpUrl === '#' || helpUrl?.startsWith('#')) {
227
+ event.preventDefault();
228
+ // State is already set above, so it will persist
229
+ return;
230
+ }
231
+ }
232
+
182
233
  function toggleNav() {
183
234
  if ($isMenuButtonDisabled) return;
184
235
  navigationStore.toggleExpanded();
@@ -437,14 +488,19 @@
437
488
  >
438
489
  <a
439
490
  class="help-link"
440
- class:selected={get(selectedSubNavItemId) === 'help'}
441
- aria-current={get(selectedSubNavItemId) === 'help'}
491
+ class:selected={isHelpSelected()}
492
+ aria-current={isHelpSelected()}
442
493
  href={helpUrl}
443
494
  part="help-link"
444
495
  title="Help"
445
496
  aria-label="Help"
497
+ onclick={handleHelpClick}
446
498
  >
447
- <HelpIcon />
499
+ <div class="icon-container" part="help-icon-container">
500
+ <div class="icon-wrapper">
501
+ <HelpIcon isSelected={isHelpSelected()} />
502
+ </div>
503
+ </div>
448
504
  </a>
449
505
  </div>
450
506
  {/if}
@@ -624,7 +680,8 @@
624
680
  }
625
681
 
626
682
  .menu-button:focus .icon-container,
627
- .nav-item:focus .icon-container {
683
+ .nav-item:focus .icon-container,
684
+ .help-link:focus .icon-container {
628
685
  outline: var(--ai-size-2) solid var(--ai-color-blue-600);
629
686
  outline-offset: var(--ai-size-2);
630
687
  border-radius: var(--ai-size-4);
@@ -632,7 +689,8 @@
632
689
 
633
690
  /* Hide focus outline on click */
634
691
  .menu-button:focus:not(:focus-visible) .icon-container,
635
- .nav-item:focus:not(:focus-visible) .icon-container {
692
+ .nav-item:focus:not(:focus-visible) .icon-container,
693
+ .help-link:focus:not(:focus-visible) .icon-container {
636
694
  outline: none;
637
695
  }
638
696
 
@@ -726,7 +784,9 @@
726
784
  .nav-item.selected .icon-container,
727
785
  .nav-item.selected.hovered .icon-container,
728
786
  .menu-button:hover .icon-container,
729
- .menu-button:active .icon-container {
787
+ .menu-button:active .icon-container,
788
+ .help-link:hover .icon-container,
789
+ .help-link.selected .icon-container {
730
790
  background-color: var(--ai-color-steel-300);
731
791
  border-radius: var(--ai-size-4);
732
792
  }
@@ -767,20 +827,14 @@
767
827
  }
768
828
 
769
829
  .help-link {
770
- border-radius: var(--ai-size-4);
771
- padding: var(--ai-size-4);
772
- transition: background-color var(--ai-transition-ease-medium);
830
+ display: flex;
831
+ align-items: center;
832
+ justify-content: center;
833
+ text-decoration: none;
834
+ color: inherit;
773
835
  }
774
836
 
775
837
  .help-link :global(svg) {
776
838
  display: block;
777
839
  }
778
-
779
- .help-link:hover {
780
- background: var(--ai-color-steel-300);
781
- }
782
-
783
- .help-link:is(.selected) {
784
- background-color: var(--ai-color-steel-300);
785
- }
786
840
  </style>
@@ -162,7 +162,7 @@
162
162
  }
163
163
 
164
164
  .nav-item.active .icon {
165
- background-color: var(--ai-color-neutral-200);
165
+ background-color: var(--ai-color-steel-300);
166
166
  border-radius: var(--ai-size-8);
167
167
  }
168
168
 
@@ -3,15 +3,22 @@
3
3
  isMenuButtonDisabled,
4
4
  navigationStore,
5
5
  selectedSubNavItemTitle,
6
+ selectedSubNavItemId,
6
7
  } from '../stores/navigationStore.js';
7
8
  import Logo from './Logo.svelte';
8
9
  import MenuIcon from './icons/MenuIcon.svelte';
10
+ import HelpIcon from './icons/HelpIcon.svelte';
9
11
  import { onMount } from 'svelte';
12
+ import { get } from 'svelte/store';
10
13
 
11
- let { profileMenuData, logoLinkUrl, logoLinkLabel } = $props();
14
+ let { profileMenuData, logoLinkUrl, logoLinkLabel, supplementalMenuData } =
15
+ $props();
12
16
 
13
17
  import ProfileMenu from './ProfileMenu.svelte';
14
18
 
19
+ const helpUrl = supplementalMenuData?.help?.url?.trim();
20
+ const isHelpSelected = $derived(() => get(selectedSubNavItemId) === 'help');
21
+
15
22
  onMount(() => {
16
23
  // Listen for section visibility changes to update header
17
24
  const handleSectionVisible = (event: Event) => {
@@ -67,14 +74,66 @@
67
74
  }, 100);
68
75
  }
69
76
 
77
+ // Detect help page and set selection state (aligned with DesktopSideNav logic)
78
+ const checkHelpPage = () => {
79
+ // Skip check for hash links - they stay on the same page
80
+ if (!helpUrl || helpUrl === '#' || helpUrl.startsWith('#')) {
81
+ return;
82
+ }
83
+
84
+ const location = (
85
+ globalThis as {
86
+ location?: { pathname?: string; origin?: string };
87
+ }
88
+ ).location;
89
+ if (location?.origin && location?.pathname) {
90
+ const helpPath = getPathname(helpUrl, location.origin);
91
+ if (helpPath && location.pathname === helpPath) {
92
+ selectedSubNavItemId.set('help');
93
+ } else if (
94
+ get(selectedSubNavItemId) === 'help' &&
95
+ helpPath &&
96
+ location.pathname !== helpPath
97
+ ) {
98
+ // Clear help selection if we navigated away from help page
99
+ selectedSubNavItemId.set(null);
100
+ }
101
+ }
102
+ };
103
+
104
+ checkHelpPage();
105
+
106
+ // Also listen for popstate events (back/forward navigation)
107
+ window.addEventListener('popstate', checkHelpPage);
108
+
70
109
  return () => {
71
110
  window.removeEventListener('sectionVisible', handleSectionVisible);
111
+ window.removeEventListener('popstate', checkHelpPage);
72
112
  };
73
113
  });
74
114
 
115
+ function getPathname(url: string, baseOrigin: string) {
116
+ try {
117
+ return new URL(url, baseOrigin).pathname;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
75
123
  function toggleMobileNav() {
76
124
  navigationStore.toggleExpanded();
77
125
  }
126
+
127
+ function handleHelpClick(event: MouseEvent) {
128
+ selectedSubNavItemId.set('help');
129
+
130
+ // If it's a hash link, prevent default to maintain state on same page
131
+ if (helpUrl === '#' || helpUrl?.startsWith('#')) {
132
+ event.preventDefault();
133
+ // State is already set above, so it will persist
134
+ return;
135
+ }
136
+ }
78
137
  </script>
79
138
 
80
139
  <nav class="mobile-top-nav" part="mobile-top-nav">
@@ -101,6 +160,22 @@
101
160
  <span class="brand-text">{$selectedSubNavItemTitle || ''}</span>
102
161
  </div>
103
162
 
163
+ {#if helpUrl}
164
+ <a
165
+ class="help-button"
166
+ class:selected={isHelpSelected()}
167
+ aria-current={isHelpSelected()}
168
+ href={helpUrl}
169
+ title="Help"
170
+ aria-label="Help"
171
+ onclick={handleHelpClick}
172
+ >
173
+ <span class="icon-container">
174
+ <HelpIcon isSelected={isHelpSelected()} />
175
+ </span>
176
+ </a>
177
+ {/if}
178
+
104
179
  {#if profileMenuData}
105
180
  <ProfileMenu
106
181
  userMeta={profileMenuData?.userMeta}
@@ -184,4 +259,28 @@
184
259
  .menu-button:focus {
185
260
  outline: none;
186
261
  }
262
+
263
+ .help-button {
264
+ display: flex;
265
+ align-items: center;
266
+ justify-content: center;
267
+ margin-right: var(--ai-size-4);
268
+ text-decoration: none;
269
+ color: inherit;
270
+ }
271
+
272
+ .help-button .icon-container {
273
+ display: flex;
274
+ align-items: center;
275
+ justify-content: center;
276
+ width: var(--ai-size-40);
277
+ height: var(--ai-size-40);
278
+ border-radius: var(--ai-size-4);
279
+ transition: background-color var(--ai-transition-ease-fast);
280
+ }
281
+
282
+ .help-button:hover .icon-container,
283
+ .help-button.selected .icon-container {
284
+ background-color: var(--ai-color-steel-300);
285
+ }
187
286
  </style>
@@ -8,7 +8,13 @@
8
8
  import MobileTopNav from './MobileTopNav.svelte';
9
9
 
10
10
  // Props for the web component
11
- let { profileMenuData, logoLinkUrl, logoLinkLabel } = $props();
11
+ let { profileMenuData, logoLinkUrl, logoLinkLabel, supplementalMenuData } =
12
+ $props();
12
13
  </script>
13
14
 
14
- <MobileTopNav {profileMenuData} {logoLinkUrl} {logoLinkLabel} />
15
+ <MobileTopNav
16
+ {profileMenuData}
17
+ {logoLinkUrl}
18
+ {logoLinkLabel}
19
+ {supplementalMenuData}
20
+ />
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- export let isHovered: boolean = false;
2
+ export let isSelected = false;
3
3
  </script>
4
4
 
5
5
  <svg
@@ -11,7 +11,7 @@
11
11
  >
12
12
  <path
13
13
  d="M9.87891 7.51884C11.0505 6.49372 12.95 6.49372 14.1215 7.51884C15.2931 8.54397 15.2931 10.206 14.1215 11.2312C13.9176 11.4096 13.6917 11.5569 13.4513 11.6733C12.7056 12.0341 12.0002 12.6716 12.0002 13.5V14.25M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12ZM12 17.25H12.0075V17.2575H12V17.25Z"
14
- stroke={isHovered ? '#000' : '#585C63'}
14
+ stroke={isSelected ? '#000' : '#585C63'}
15
15
  stroke-width="1.5"
16
16
  stroke-linecap="round"
17
17
  stroke-linejoin="round"
@@ -77,6 +77,7 @@ const discover = discoverUrl || '#';
77
77
  profileMenuData={profileMenuData}
78
78
  logoLinkUrl={logoLinkUrl}
79
79
  logoLinkLabel={logoLinkLabel}
80
+ {supplementalMenuData}
80
81
  />
81
82
  <MobileSideMenu client:load {logoLinkUrl} {logoLinkLabel}>
82
83
  <slot name="sub-nav" />
@@ -87,7 +88,7 @@ const discover = discoverUrl || '#';
87
88
  hideDiscover={hideDiscover}
88
89
  apiUrl={api}
89
90
  chatUrl={chat}
90
- consoleUrl={console}
91
+ consoleUrl={consoleApp}
91
92
  discoverUrl={discover}
92
93
  />
93
94
  </nav>