@gsa-tts/graymatter-ui 0.2.7 → 0.3.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.
Files changed (38) hide show
  1. package/README.md +81 -0
  2. package/assets/images/favicon-dark.ico +0 -0
  3. package/assets/images/favicon-dark.svg +7 -0
  4. package/assets/images/favicon-light.svg +7 -0
  5. package/assets/images/favicon.ico +0 -0
  6. package/assets/images/favicon.svg +5 -6
  7. package/dist/graymatter-ui.js +1663 -1628
  8. package/dist/graymatter-ui.umd.cjs +80 -33
  9. package/package.json +4 -4
  10. package/src/components/Button.svelte +27 -31
  11. package/src/components/Button.webcomponent.svelte +13 -11
  12. package/src/components/DesktopHeader.svelte +70 -53
  13. package/src/components/DesktopHeader.webcomponent.svelte +13 -9
  14. package/src/components/DesktopSideNav.svelte +209 -166
  15. package/src/components/DesktopSideNav.webcomponent.svelte +7 -5
  16. package/src/components/Head.astro +24 -2
  17. package/src/components/Logo.svelte +48 -41
  18. package/src/components/Logo.webcomponent.svelte +16 -16
  19. package/src/components/MobileBottomNav.svelte +102 -69
  20. package/src/components/MobileBottomNav.webcomponent.svelte +13 -4
  21. package/src/components/MobileSideMenu.svelte +70 -42
  22. package/src/components/MobileSideMenu.webcomponent.svelte +6 -4
  23. package/src/components/MobileTopNav.svelte +24 -19
  24. package/src/components/MobileTopNav.webcomponent.svelte +6 -6
  25. package/src/components/NavigationInitializer.svelte +10 -2
  26. package/src/components/ProfileMenu.svelte +116 -27
  27. package/src/components/UsaSkipNav.svelte +23 -0
  28. package/src/components/icons/ApiIcon.svelte +182 -33
  29. package/src/components/icons/ChatIcon.svelte +10 -10
  30. package/src/components/icons/CloseIcon.svelte +10 -10
  31. package/src/components/icons/ConsoleIcon.svelte +68 -13
  32. package/src/components/icons/DiscoverIcon.svelte +14 -14
  33. package/src/components/icons/HelpIcon.svelte +2 -1
  34. package/src/components/icons/MenuIcon.svelte +47 -7
  35. package/src/components/index.ts +1 -1
  36. package/src/layouts/BaseLayout.astro +1 -1
  37. package/src/styles/base/global.css +7 -1
  38. package/src/components/UsaSkipNav.astro +0 -5
@@ -1,12 +1,16 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
- import { selectedItem, isExpanded, navigationStore } from '../stores/navigationStore.js';
3
+ import {
4
+ selectedItem,
5
+ isExpanded,
6
+ navigationStore,
7
+ } from '../stores/navigationStore.js';
4
8
  import Logo from './Logo.svelte';
5
9
  import CloseIcon from './icons/CloseIcon.svelte';
6
10
 
7
11
  let sideMenuElement: HTMLDivElement;
8
12
  let pendingMobileHash: string | null = null;
9
-
13
+
10
14
  let { logoLinkUrl, logoLinkLabel } = $props();
11
15
 
12
16
  function handleMobileAnchorNavigate(event: Event) {
@@ -19,23 +23,27 @@
19
23
  if (el) {
20
24
  // Check if the anchor exists in mobile content
21
25
  const mobileContent = document.getElementById('main-content-mobile');
22
- const mobileAnchor = mobileContent ? mobileContent.querySelector(`[id="${pendingMobileHash}"]`) : null;
23
-
26
+ const mobileAnchor = mobileContent
27
+ ? mobileContent.querySelector(`[id="${pendingMobileHash}"]`)
28
+ : null;
29
+
24
30
  if (mobileAnchor && mobileContent) {
25
31
  // For mobile: show section title in both header and content
26
32
  const rect = mobileAnchor.getBoundingClientRect();
27
33
  const mobileRect = mobileContent.getBoundingClientRect();
28
34
  const scrollTop = mobileContent.scrollTop + rect.top - mobileRect.top;
29
35
  mobileContent.scrollTo({ top: scrollTop, behavior: 'auto' });
30
-
36
+
31
37
  // Update header after scroll completes
32
38
  setTimeout(() => {
33
- window.dispatchEvent(new CustomEvent('sectionVisible', {
34
- detail: {
35
- title: mobileAnchor.textContent?.trim(),
36
- id: mobileAnchor.id
37
- }
38
- }));
39
+ window.dispatchEvent(
40
+ new CustomEvent('sectionVisible', {
41
+ detail: {
42
+ title: mobileAnchor.textContent?.trim(),
43
+ id: mobileAnchor.id,
44
+ },
45
+ })
46
+ );
39
47
  }, 100);
40
48
  } else {
41
49
  // Fallback to desktop content
@@ -47,18 +55,20 @@
47
55
  }
48
56
  scrollableParent = scrollableParent.parentElement;
49
57
  }
50
-
58
+
51
59
  if (scrollableParent && scrollableParent !== document.body) {
52
60
  const desktopMain = document.getElementById('main-content');
53
61
  if (desktopMain) {
54
62
  el.scrollIntoView({ behavior: 'auto', block: 'start' });
55
63
  setTimeout(() => {
56
- window.dispatchEvent(new CustomEvent('sectionVisible', {
57
- detail: {
58
- title: el.textContent?.trim(),
59
- id: el.id
60
- }
61
- }));
64
+ window.dispatchEvent(
65
+ new CustomEvent('sectionVisible', {
66
+ detail: {
67
+ title: el.textContent?.trim(),
68
+ id: el.id,
69
+ },
70
+ })
71
+ );
62
72
  }, 100);
63
73
  } else {
64
74
  el.scrollIntoView({ behavior: 'auto', block: 'start' });
@@ -75,16 +85,16 @@
75
85
  onMount(() => {
76
86
  // Listen for the custom event globally
77
87
  window.addEventListener('mobileAnchorNavigate', handleMobileAnchorNavigate);
78
-
88
+
79
89
  const handleCloseMobileMenu = () => {
80
90
  closeMobileMenu();
81
91
  };
82
92
  window.addEventListener('closeMobileMenu', handleCloseMobileMenu);
83
-
93
+
84
94
  const handleLayoutTransition = (event: Event) => {
85
95
  const customEvent = event as CustomEvent;
86
96
  const { isMobile } = customEvent.detail;
87
-
97
+
88
98
  if (isMobile) {
89
99
  navigationStore.setExpanded(false);
90
100
  }
@@ -102,9 +112,12 @@
102
112
  }
103
113
  };
104
114
  window.addEventListener('keydown', handleEscape);
105
-
115
+
106
116
  return () => {
107
- window.removeEventListener('mobileAnchorNavigate', handleMobileAnchorNavigate);
117
+ window.removeEventListener(
118
+ 'mobileAnchorNavigate',
119
+ handleMobileAnchorNavigate
120
+ );
108
121
  window.removeEventListener('closeMobileMenu', handleCloseMobileMenu);
109
122
  window.removeEventListener('layoutTransition', handleLayoutTransition);
110
123
  window.removeEventListener('keydown', handleEscape);
@@ -118,36 +131,51 @@
118
131
 
119
132
  <!-- Mobile overlay -->
120
133
  {#if $isExpanded}
121
- <div
122
- class="mobile-overlay"
134
+ <div
135
+ class="mobile-overlay"
123
136
  role="button"
124
137
  tabindex="0"
125
138
  aria-label="Close navigation menu (Escape key supported)"
126
139
  part="mobile-overlay"
127
140
  onclick={closeMobileMenu}
128
- onkeydown={(e) => {
141
+ onkeydown={e => {
129
142
  if (e.key === 'Enter' || e.key === ' ') closeMobileMenu();
130
143
  }}
131
144
  ></div>
132
145
  {/if}
133
146
 
134
147
  <!-- Mobile side menu -->
135
- <div class="mobile-side-menu" class:open={$isExpanded} bind:this={sideMenuElement} part="mobile-side-menu">
148
+ <div
149
+ class="mobile-side-menu"
150
+ class:open={$isExpanded}
151
+ bind:this={sideMenuElement}
152
+ part="mobile-side-menu"
153
+ >
136
154
  <div class="menu-header" part="menu-header">
137
155
  <div class="logo-container" part="logo-container">
138
156
  {#if logoLinkUrl}
139
- <a href={logoLinkUrl} part="logo-link" class="logo-link" aria-label={logoLinkLabel}>
140
- <Logo width={75} height={22}/>
157
+ <a
158
+ href={logoLinkUrl}
159
+ part="logo-link"
160
+ class="logo-link"
161
+ aria-label={logoLinkLabel}
162
+ >
163
+ <Logo width={75} height={22} />
141
164
  </a>
142
165
  {:else}
143
- <Logo width={75} height={22}/>
166
+ <Logo width={75} height={22} />
144
167
  {/if}
145
168
  </div>
146
- <button class="close-button" onclick={closeMobileMenu} aria-label="Close navigation menu" part="close-button">
169
+ <button
170
+ class="close-button"
171
+ onclick={closeMobileMenu}
172
+ aria-label="Close navigation menu"
173
+ part="close-button"
174
+ >
147
175
  <CloseIcon />
148
176
  </button>
149
177
  </div>
150
-
178
+
151
179
  <div class="menu-content" part="menu-content">
152
180
  <!-- Show submenu items if available and not on discover -->
153
181
  {#if $selectedItem && $selectedItem !== 'discover'}
@@ -170,11 +198,11 @@
170
198
  color: var(--ai-color-steel-600);
171
199
  font-size: var(--ai-size-14);
172
200
  }
173
-
201
+
174
202
  .no-subnav-message p {
175
203
  margin: 0;
176
204
  }
177
-
205
+
178
206
  .mobile-side-menu {
179
207
  position: fixed;
180
208
  top: 0;
@@ -184,16 +212,16 @@
184
212
  max-width: 85vw;
185
213
  background: var(--ai-color-white);
186
214
  transform: translateX(-100%);
187
- transition: transform 0.3s ease;
215
+ transition: transform var(--ai-transition-ease-medium);
188
216
  z-index: var(--ai-layer-50);
189
217
  flex-direction: column;
190
218
  box-shadow: var(--ai-size-2) 0 var(--ai-size-10) #0000001a;
191
219
  }
192
-
220
+
193
221
  .mobile-side-menu.open {
194
222
  transform: translateX(0);
195
223
  }
196
-
224
+
197
225
  .menu-header {
198
226
  display: flex;
199
227
  align-items: center;
@@ -201,11 +229,11 @@
201
229
  padding: var(--ai-size-16) var(--ai-size-20);
202
230
  border-bottom: 0.0625rem solid var(--ai-color-neutral-200);
203
231
  }
204
-
232
+
205
233
  .mobile-side-menu .logo-container {
206
234
  display: flex;
207
235
  }
208
-
236
+
209
237
  .logo-link {
210
238
  display: flex;
211
239
  }
@@ -221,19 +249,19 @@
221
249
  color: var(--ai-color-neutral-700);
222
250
  cursor: pointer;
223
251
  border-radius: var(--ai-size-6);
224
- transition: background-color 0.2s ease;
252
+ transition: background-color var(--ai-transition-ease-fast);
225
253
  }
226
254
 
227
255
  .close-button:hover {
228
256
  background-color: var(--ai-color-neutral-100);
229
257
  }
230
-
258
+
231
259
  .menu-content {
232
260
  flex: 1;
233
261
  overflow-y: auto;
234
262
  padding: 0;
235
263
  }
236
-
264
+
237
265
  .subnav-section {
238
266
  padding: var(--ai-size-16) 0;
239
267
  overflow-y: auto;
@@ -1,10 +1,12 @@
1
- <svelte:options customElement={{
2
- tag: 'graymatter-mobile-side-menu'
3
- }} />
1
+ <svelte:options
2
+ customElement={{
3
+ tag: 'graymatter-mobile-side-menu',
4
+ }}
5
+ />
4
6
 
5
7
  <script lang="ts">
6
8
  import MobileSideMenu from './MobileSideMenu.svelte';
7
-
9
+
8
10
  let { logoLinkUrl, logoLinkLabel } = $props();
9
11
  </script>
10
12
 
@@ -22,7 +22,10 @@
22
22
  if (customEvent.detail && customEvent.detail.title) {
23
23
  selectedSubNavItemTitle.set(customEvent.detail.title);
24
24
  if (typeof window !== 'undefined') {
25
- sessionStorage.setItem('subNav-selectedTitle', JSON.stringify(customEvent.detail.title));
25
+ sessionStorage.setItem(
26
+ 'subNav-selectedTitle',
27
+ JSON.stringify(customEvent.detail.title)
28
+ );
26
29
  }
27
30
  }
28
31
  };
@@ -30,27 +33,32 @@
30
33
  // Set default header based on first submenu item
31
34
  const setDefaultHeaderFromFirstSubmenu = () => {
32
35
  if (typeof window === 'undefined') return;
33
-
36
+
34
37
  const mainContent = document.getElementById('main-content');
35
38
  if (!mainContent) return;
36
-
39
+
37
40
  // Find the first heading with an ID (first submenu item)
38
- const firstSection = mainContent.querySelector('h2[id], h3[id], h4[id], h5[id], h6[id]');
41
+ const firstSection = mainContent.querySelector(
42
+ 'h2[id], h3[id], h4[id], h5[id], h6[id]'
43
+ );
39
44
  if (firstSection && firstSection.textContent) {
40
45
  const firstSectionTitle = firstSection.textContent.trim();
41
-
46
+
42
47
  // Only set if no header is currently set
43
48
  if (!$selectedSubNavItemTitle) {
44
49
  selectedSubNavItemTitle.set(firstSectionTitle);
45
50
  if (typeof window !== 'undefined') {
46
- sessionStorage.setItem('subNav-selectedTitle', JSON.stringify(firstSectionTitle));
51
+ sessionStorage.setItem(
52
+ 'subNav-selectedTitle',
53
+ JSON.stringify(firstSectionTitle)
54
+ );
47
55
  }
48
56
  }
49
57
  }
50
58
  };
51
59
 
52
60
  window.addEventListener('sectionVisible', handleSectionVisible);
53
-
61
+
54
62
  // Set default header on mount only if no stored title exists
55
63
  const storedTitle = sessionStorage.getItem('subNav-selectedTitle');
56
64
  if (!storedTitle || storedTitle === '""' || storedTitle === 'null') {
@@ -58,7 +66,7 @@
58
66
  setDefaultHeaderFromFirstSubmenu();
59
67
  }, 100);
60
68
  }
61
-
69
+
62
70
  return () => {
63
71
  window.removeEventListener('sectionVisible', handleSectionVisible);
64
72
  };
@@ -67,7 +75,6 @@
67
75
  function toggleMobileNav() {
68
76
  navigationStore.toggleExpanded();
69
77
  }
70
-
71
78
  </script>
72
79
 
73
80
  <nav class="mobile-top-nav" part="mobile-top-nav">
@@ -88,17 +95,16 @@
88
95
  </div>
89
96
 
90
97
  {#if profileMenuData}
91
- <ProfileMenu
92
- userMeta={profileMenuData?.userMeta}
93
- menuItems={profileMenuData?.menuItems}
94
- ariaLabel="Open profile menu"
95
- menuAlign="mobile"
96
- />
98
+ <ProfileMenu
99
+ userMeta={profileMenuData?.userMeta}
100
+ menuItems={profileMenuData?.menuItems}
101
+ ariaLabel="Open profile menu"
102
+ menuAlign="mobile"
103
+ />
97
104
  {/if}
98
105
  </nav>
99
106
 
100
107
  <style>
101
-
102
108
  .menu-button {
103
109
  display: flex;
104
110
  align-items: center;
@@ -107,7 +113,7 @@
107
113
  background: none;
108
114
  cursor: pointer;
109
115
  border-radius: var(--ai-size-8);
110
- transition: background-color 0.2s ease;
116
+ transition: background-color var(--ai-transition-ease-fast);
111
117
  }
112
118
 
113
119
  .menu-button:disabled,
@@ -158,7 +164,7 @@
158
164
  border: none;
159
165
  cursor: pointer;
160
166
  opacity: 1;
161
- transition: opacity 0.2s ease;
167
+ transition: opacity var(--ai-transition-ease-fast);
162
168
  will-change: opacity;
163
169
  }
164
170
 
@@ -171,5 +177,4 @@
171
177
  .menu-button:focus {
172
178
  outline: none;
173
179
  }
174
-
175
180
  </style>
@@ -1,6 +1,8 @@
1
- <svelte:options customElement={{
2
- tag: 'graymatter-mobile-top-nav'
3
- }} />
1
+ <svelte:options
2
+ customElement={{
3
+ tag: 'graymatter-mobile-top-nav',
4
+ }}
5
+ />
4
6
 
5
7
  <script lang="ts">
6
8
  import MobileTopNav from './MobileTopNav.svelte';
@@ -9,6 +11,4 @@
9
11
  let { profileMenuData } = $props();
10
12
  </script>
11
13
 
12
- <MobileTopNav
13
- {profileMenuData}
14
- />
14
+ <MobileTopNav {profileMenuData} />
@@ -1,6 +1,9 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
- import { navigationStore, type NavigationItem } from '../stores/navigationStore.js';
3
+ import {
4
+ navigationStore,
5
+ type NavigationItem,
6
+ } from '../stores/navigationStore.js';
4
7
 
5
8
  export let item: NavigationItem;
6
9
  export let autoExpand: boolean = false;
@@ -9,7 +12,12 @@
9
12
  if (item) {
10
13
  navigationStore.setSelectedItem(item);
11
14
  // Auto-expand for non-discover pages on desktop
12
- if (autoExpand && item !== 'discover' && typeof window !== 'undefined' && window.innerWidth > 640) {
15
+ if (
16
+ autoExpand &&
17
+ item !== 'discover' &&
18
+ typeof window !== 'undefined' &&
19
+ window.innerWidth > 640
20
+ ) {
13
21
  navigationStore.setExpanded(true);
14
22
  }
15
23
  }
@@ -1,34 +1,112 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
+
4
+ /**
5
+ * ProfileMenu Component - Event-Driven Architecture with Shadow DOM Support
6
+ *
7
+ * This component uses a custom event system to handle menu item actions.
8
+ * It dispatches 'profileMenuAction' events that consumers can listen to,
9
+ * providing a clean separation between the UI component and business logic.
10
+ *
11
+ * SHADOW DOM COMPATIBILITY:
12
+ * - Events are configured with bubbles: true and composed: true
13
+ * - Works in both regular DOM and Shadow DOM contexts
14
+ * - Compatible with web components and Astro apps
15
+ *
16
+ * USAGE:
17
+ * 1. Pass menu items with 'action' strings instead of functions
18
+ * 2. Listen for 'profileMenuAction' events in your app
19
+ * 3. Handle actions based on the 'action' property in event.detail
20
+ *
21
+ * Examples:
22
+ *
23
+ * // Astro App (Regular DOM)
24
+ * // In your Astro layout or component
25
+ * window.addEventListener('profileMenuAction', (event) => {
26
+ * const { action, userMeta, timestamp } = event.detail;
27
+ * switch (action) {
28
+ * case 'settings':
29
+ * // Handle settings action
30
+ * console.log('Settings clicked for user:', userMeta.initials);
31
+ * break;
32
+ * case 'logout':
33
+ * // Handle logout action
34
+ * console.log('Logout clicked for user:', userMeta.initials);
35
+ * break;
36
+ * }
37
+ * });
38
+ *
39
+ * // Web Component App (Shadow DOM)
40
+ * // In your Svelte app using web components
41
+ * document.addEventListener('profileMenuAction', (event) => {
42
+ * const { action, userMeta, timestamp } = event.detail;
43
+ * // Same handling as above - events bubble through Shadow DOM
44
+ * });
45
+ *
46
+ * MENU ITEMS FORMAT:
47
+ * ```
48
+ * menuItems: [
49
+ * {
50
+ * label: 'Settings',
51
+ * icon: '/path/to/icon.svg',
52
+ * action: 'settings' // String identifier for the action
53
+ * },
54
+ * {
55
+ * label: 'Log out',
56
+ * icon: '/path/to/logout.svg',
57
+ * action: 'logout' // String identifier for the action
58
+ * }
59
+ * ]
60
+ * ```
61
+ *
62
+ * EVENT DETAILS:
63
+ * - action: String identifier for the menu item action
64
+ * - userMeta: User metadata object
65
+ * - timestamp: When the action was triggered
66
+ * - bubbles: true (allows event to bubble through DOM)
67
+ * - composed: true (allows event to cross Shadow DOM boundaries)
68
+ */
69
+
3
70
  /**
4
71
  * Array of menu items to render in the profile menu.
5
- * Each item: { label: string, icon: string, onClick: string | (() => void) }
72
+ * Each item: { label: string, icon: string, action: string }
6
73
  */
7
- export let menuItems: Array<{ label: string; icon: string; onClick: string | (() => void) }> = [];
8
- export let userMeta: { initials: string};
74
+ export let menuItems: Array<{ label: string; icon: string; action: string }> =
75
+ [];
76
+ export let userMeta: { initials: string };
9
77
  export let ariaLabel: string = 'Open profile menu';
10
78
  export let menuAlign: 'desktop' | 'mobile' = 'desktop';
11
79
 
12
80
  let menuId = `profile-menu-${Math.random().toString(36).slice(2, 10)}`;
13
81
  let menuOpen = false;
14
- function toggleMenu() { menuOpen = !menuOpen; }
15
- function closeMenu() { menuOpen = false; }
82
+ function toggleMenu() {
83
+ menuOpen = !menuOpen;
84
+ }
85
+ function closeMenu() {
86
+ menuOpen = false;
87
+ }
16
88
 
17
- // Execute the onClick function - handles both string and function types
18
- function executeOnClick(onClick: string | (() => void)) {
89
+ function executeMenuAction(action: string) {
19
90
  try {
20
- if (typeof onClick === 'string') {
21
- // Handle string-based function (for Astro apps)
22
- const onClickFunction = new Function('return ' + onClick)();
23
- onClickFunction();
24
- } else if (typeof onClick === 'function') {
25
- // Handle actual function (for Svelte apps)
26
- onClick();
27
- } else {
28
- console.error('Invalid onClick type:', typeof onClick);
29
- }
91
+ const event = new CustomEvent('profileMenuAction', {
92
+ detail: {
93
+ action,
94
+ timestamp: Date.now(),
95
+ userMeta,
96
+ },
97
+ bubbles: true, // Allow event to bubble up through DOM
98
+ composed: true, // Allow event to cross Shadow DOM boundaries
99
+ });
100
+
101
+ // Try to dispatch from the component element first (for web components)
102
+ // Fall back to window if component element is not available
103
+ const target =
104
+ document
105
+ .querySelector('.profile-context-menu')
106
+ ?.closest('[data-profile-menu]') || window;
107
+ target.dispatchEvent(event);
30
108
  } catch (error) {
31
- console.error('Error executing onClick function:', error);
109
+ console.error('Error dispatching profile menu action:', error);
32
110
  }
33
111
  }
34
112
 
@@ -79,7 +157,7 @@
79
157
  });
80
158
  </script>
81
159
 
82
- <div style="position: relative; display: inline-block;">
160
+ <div style="position: relative; display: inline-block;" data-profile-menu>
83
161
  <button
84
162
  class="user-profile-button"
85
163
  tabindex="0"
@@ -100,7 +178,9 @@
100
178
 
101
179
  {#if menuOpen}
102
180
  <div
103
- class="profile-context-menu {menuAlign === 'desktop' ? 'desktop-align' : 'mobile-align'}"
181
+ class="profile-context-menu {menuAlign === 'desktop'
182
+ ? 'desktop-align'
183
+ : 'mobile-align'}"
104
184
  tabindex="-1"
105
185
  id={menuId}
106
186
  role="menu"
@@ -109,15 +189,24 @@
109
189
  <button
110
190
  type="button"
111
191
  class="menu-item"
112
- on:click={() => { executeOnClick(item.onClick); closeMenu(); }}
192
+ on:click={() => {
193
+ executeMenuAction(item.action);
194
+ closeMenu();
195
+ }}
113
196
  bind:this={btns[idx]}
114
- on:keydown={(e) => handleMenuKeydown(e, idx)}
197
+ on:keydown={e => handleMenuKeydown(e, idx)}
115
198
  role="menuitem"
116
199
  >
117
200
  {#if item.icon}
118
- <span class="icon">
119
- <img src={item.icon} alt="" aria-hidden="true" width="20" height="20" />
120
- </span>
201
+ <span class="icon">
202
+ <img
203
+ src={item.icon}
204
+ alt=""
205
+ aria-hidden="true"
206
+ width="20"
207
+ height="20"
208
+ />
209
+ </span>
121
210
  {/if}
122
211
  <div class="menu-text">{item.label}</div>
123
212
  </button>
@@ -198,7 +287,7 @@
198
287
  border-radius: var(--ai-size-8);
199
288
  border: 0.0625rem solid var(--ai-color-steel-200);
200
289
  background: var(--ai-color-white);
201
- box-shadow: 0 0.1875rem var(--ai-size-6) 0 rgba(50, 54, 61, 0.20);
290
+ box-shadow: 0 0.1875rem var(--ai-size-6) 0 rgba(50, 54, 61, 0.2);
202
291
  position: absolute;
203
292
  z-index: var(--ai-layer-50);
204
293
  }
@@ -259,4 +348,4 @@
259
348
  font-weight: 400;
260
349
  line-height: 130%;
261
350
  }
262
- </style>
351
+ </style>
@@ -0,0 +1,23 @@
1
+ <script>
2
+ import Button from '@gsa-tts/graymatter-ui/components/Button.svelte';
3
+
4
+ let { mainContentId = '#main-content', skipLinkTheme = 'inverted' } =
5
+ $props();
6
+ </script>
7
+
8
+ <Button
9
+ href={mainContentId}
10
+ theme={skipLinkTheme}
11
+ size="small"
12
+ class="usa-skipnav ai-skipnav"
13
+ >
14
+ Skip to main content
15
+ </Button>
16
+
17
+ <style>
18
+ :global(.ai-skipnav:focus) {
19
+ margin-block-start: var(--ai-size-12);
20
+ margin-inline-start: var(--ai-size-12);
21
+ text-decoration: none;
22
+ }
23
+ </style>