@gsa-tts/graymatter-ui 0.1.0 → 0.2.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.
@@ -74,28 +74,21 @@
74
74
  // Listen for the custom event globally
75
75
  window.addEventListener('mobileAnchorNavigate', handleMobileAnchorNavigate);
76
76
 
77
- // Listen for close mobile menu event
78
77
  const handleCloseMobileMenu = () => {
79
- console.log('[MobileSideMenu] Received closeMobileMenu event');
80
78
  closeMobileMenu();
81
79
  };
82
80
  window.addEventListener('closeMobileMenu', handleCloseMobileMenu);
83
81
 
84
- // Listen for layout transitions
85
82
  const handleLayoutTransition = (event: Event) => {
86
83
  const customEvent = event as CustomEvent;
87
84
  const { isMobile } = customEvent.detail;
88
- console.log('[MobileSideMenu] Layout transition detected:', { isMobile });
89
85
 
90
86
  if (isMobile) {
91
- // Switching to mobile: close navigation
92
- console.log('[MobileSideMenu] Closing navigation for mobile');
93
87
  navigationStore.setExpanded(false);
94
88
  }
95
89
  };
96
90
  window.addEventListener('layoutTransition', handleLayoutTransition);
97
91
 
98
- // Listen for Escape key globally to close menu
99
92
  const handleEscape = (e: KeyboardEvent) => {
100
93
  if (e.key === 'Escape' && $isExpanded) {
101
94
  closeMobileMenu();
@@ -123,6 +116,7 @@
123
116
  role="button"
124
117
  tabindex="0"
125
118
  aria-label="Close navigation menu (Escape key supported)"
119
+ part="mobile-overlay"
126
120
  on:click={closeMobileMenu}
127
121
  on:keydown={(e) => {
128
122
  if (e.key === 'Enter' || e.key === ' ') closeMobileMenu();
@@ -131,25 +125,25 @@
131
125
  {/if}
132
126
 
133
127
  <!-- Mobile side menu -->
134
- <div class="mobile-side-menu" class:open={$isExpanded} bind:this={sideMenuElement}>
135
- <div class="menu-header">
136
- <div class="logo-container">
128
+ <div class="mobile-side-menu" class:open={$isExpanded} bind:this={sideMenuElement} part="mobile-side-menu">
129
+ <div class="menu-header" part="menu-header">
130
+ <div class="logo-container" part="logo-container">
137
131
  <Logo width={75} height={22} />
138
132
  </div>
139
- <button class="close-button" on:click={closeMobileMenu} aria-label="Close navigation menu">
133
+ <button class="close-button" on:click={closeMobileMenu} aria-label="Close navigation menu" part="close-button">
140
134
  <CloseIcon />
141
135
  </button>
142
136
  </div>
143
137
 
144
- <div class="menu-content">
138
+ <div class="menu-content" part="menu-content">
145
139
  <!-- Show submenu items if available and not on discover -->
146
140
  {#if $selectedItem && $selectedItem !== 'discover'}
147
- <div class="subnav-section">
141
+ <div class="subnav-section" part="subnav-section">
148
142
  <slot />
149
143
  </div>
150
144
  {:else}
151
145
  <!-- Show message when no submenu is available -->
152
- <div class="no-subnav-message">
146
+ <div class="no-subnav-message" part="no-subnav-message">
153
147
  <p>Select a section from the bottom navigation</p>
154
148
  </div>
155
149
  {/if}
@@ -167,4 +161,65 @@
167
161
  .no-subnav-message p {
168
162
  margin: 0;
169
163
  }
164
+
165
+ .mobile-side-menu {
166
+ position: fixed;
167
+ top: 0;
168
+ left: 0;
169
+ bottom: 0;
170
+ width: 15rem;
171
+ max-width: 85vw;
172
+ background: var(--ai-color-white);
173
+ transform: translateX(-100%);
174
+ transition: transform 0.3s ease;
175
+ z-index: var(--ai-layer-50);
176
+ flex-direction: column;
177
+ box-shadow: var(--ai-size-2) 0 var(--ai-size-10) #0000001a;
178
+ }
179
+
180
+ .mobile-side-menu.open {
181
+ transform: translateX(0);
182
+ }
183
+
184
+ .menu-header {
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: space-between;
188
+ padding: var(--ai-size-16) var(--ai-size-20);
189
+ border-bottom: 0.0625rem solid var(--ai-color-neutral-200);
190
+ }
191
+
192
+ .mobile-side-menu .logo-container {
193
+ display: flex;
194
+ }
195
+
196
+ .close-button {
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ width: var(--ai-size-32);
201
+ height: var(--ai-size-32);
202
+ border: none;
203
+ background: none;
204
+ color: var(--ai-color-neutral-700);
205
+ cursor: pointer;
206
+ border-radius: var(--ai-size-6);
207
+ transition: background-color 0.2s ease;
208
+ }
209
+
210
+ .close-button:hover {
211
+ background-color: var(--ai-color-neutral-100);
212
+ }
213
+
214
+ .menu-content {
215
+ flex: 1;
216
+ overflow-y: auto;
217
+ padding: 0;
218
+ }
219
+
220
+ .subnav-section {
221
+ padding: var(--ai-size-16) 0;
222
+ overflow-y: auto;
223
+ max-height: calc(100vh - 4rem); /* leave space for header/close */
224
+ }
170
225
  </style>
@@ -0,0 +1,11 @@
1
+ <svelte:options customElement={{
2
+ tag: 'graymatter-mobile-side-menu'
3
+ }} />
4
+
5
+ <script lang="ts">
6
+ import MobileSideMenu from './MobileSideMenu.svelte';
7
+ </script>
8
+
9
+ <MobileSideMenu>
10
+ <slot />
11
+ </MobileSideMenu>
@@ -71,13 +71,14 @@
71
71
  }
72
72
  </script>
73
73
 
74
- <nav class="mobile-top-nav">
74
+ <nav class="mobile-top-nav" part="mobile-top-nav">
75
75
  <button
76
76
  class="menu-button"
77
77
  class:disabled={$isMenuButtonDisabled}
78
78
  disabled={$isMenuButtonDisabled}
79
79
  on:click={toggleMobileNav}
80
80
  aria-label="Toggle navigation menu"
81
+ part="menu-button"
81
82
  >
82
83
  <MenuIcon />
83
84
  </button>
@@ -91,11 +92,144 @@
91
92
  class="profile-button"
92
93
  on:click={handleProfileClick}
93
94
  aria-label="Open profile menu"
95
+ part="profile-button"
94
96
  >
95
97
  <div class="user-profile-container">
96
- <div class="user-profile-icon">
98
+ <div class="user-profile-icon" part="user-profile-icon">
97
99
  <span class="user-initials">ZW</span>
98
100
  </div>
99
101
  </div>
100
102
  </button>
101
103
  </nav>
104
+
105
+ <style>
106
+ .user-profile-container,
107
+ .user-profile-icon {
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ }
112
+
113
+ .menu-button,
114
+ .profile-button {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ border: none;
119
+ background: none;
120
+ cursor: pointer;
121
+ border-radius: var(--ai-size-8);
122
+ transition: background-color 0.2s ease;
123
+ }
124
+
125
+ .menu-button:disabled,
126
+ .menu-button.disabled {
127
+ cursor: not-allowed;
128
+ opacity: 0.5;
129
+ background-color: transparent;
130
+ }
131
+
132
+ .user-profile-icon {
133
+ border-radius: var(--ai-size-6);
134
+ background: var(--ai-color-steel-700);
135
+ }
136
+
137
+ .user-initials {
138
+ color: var(--ai-color-white);
139
+ text-align: center;
140
+ font-weight: 400;
141
+ line-height: 1.3;
142
+ }
143
+
144
+ .mobile-top-nav .menu-button {
145
+ width: var(--ai-size-40);
146
+ height: var(--ai-size-40);
147
+ color: var(--ai-color-neutral-700);
148
+ }
149
+
150
+ .menu-button:hover {
151
+ background-color: var(--ai-color-neutral-100);
152
+ }
153
+
154
+ /* MobileTopNav specific disabled state */
155
+ .mobile-top-nav .menu-button.disabled,
156
+ .mobile-top-nav .menu-button:disabled {
157
+ cursor: not-allowed;
158
+ opacity: 0.5;
159
+ background-color: transparent;
160
+ }
161
+
162
+ .brand {
163
+ display: flex;
164
+ align-items: center;
165
+ gap: var(--ai-size-12);
166
+ flex: 1;
167
+ margin-left: var(--ai-size-8);
168
+ }
169
+
170
+ .brand-text {
171
+ color: var(--ai-color-neutral-900);
172
+ font-size: var(--ai-size-16);
173
+ font-weight: 600;
174
+ line-height: 1;
175
+ }
176
+
177
+ .profile-button {
178
+ width: var(--ai-size-40);
179
+ height: var(--ai-size-40);
180
+ }
181
+
182
+ .profile-button:hover {
183
+ background-color: var(--ai-color-neutral-100);
184
+ }
185
+
186
+ .mobile-top-nav .user-profile-icon {
187
+ width: var(--ai-size-32);
188
+ height: var(--ai-size-32);
189
+ border-radius: 0.3125rem;
190
+ }
191
+
192
+ .mobile-top-nav .user-initials {
193
+ font-size: var(--ai-size-14);
194
+ line-height: 1;
195
+ }
196
+
197
+ .user-profile-container {
198
+ width: var(--ai-size-40);
199
+ height: var(--ai-size-40);
200
+ aspect-ratio: 1/1;
201
+ }
202
+
203
+ .user-profile-icon {
204
+ padding: 0.5556rem 0.4931rem 0.5694rem 0.3819rem;
205
+ }
206
+
207
+ .user-initials {
208
+ font-size: var(--ai-size-16);
209
+ font-style: normal;
210
+ }
211
+
212
+ .menu-button {
213
+ display: flex;
214
+ padding: var(--ai-size-4);
215
+ justify-content: center;
216
+ align-items: center;
217
+ background: none;
218
+ border: none;
219
+ cursor: pointer;
220
+ opacity: 1;
221
+ transition: opacity 0.2s ease;
222
+ will-change: opacity;
223
+ }
224
+
225
+ .menu-button.disabled {
226
+ cursor: not-allowed;
227
+ opacity: 0.5;
228
+ will-change: opacity;
229
+ }
230
+
231
+ .menu-button:focus {
232
+ outline: none;
233
+ }
234
+
235
+ </style>
@@ -0,0 +1,14 @@
1
+ <svelte:options customElement={{
2
+ tag: 'graymatter-mobile-top-nav'
3
+ }} />
4
+
5
+ <script lang="ts">
6
+ import MobileTopNav from './MobileTopNav.svelte';
7
+
8
+ // Props for the web component
9
+ let { onMenuToggle } = $props();
10
+ </script>
11
+
12
+ <MobileTopNav
13
+ {onMenuToggle}
14
+ />
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ import Button from './components/Button.webcomponent.svelte';
2
+ import DesktopHeader from './components/DesktopHeader.webcomponent.svelte';
3
+ import DesktopSideNav from './components/DesktopSideNav.webcomponent.svelte';
4
+ import Logo from './components/Logo.webcomponent.svelte';
5
+ import MobileBottomNav from './components/MobileBottomNav.webcomponent.svelte';
6
+ import MobileSideMenu from './components/MobileSideMenu.webcomponent.svelte';
7
+ import MobileTopNav from './components/MobileTopNav.webcomponent.svelte';
8
+ export {
9
+ Button,
10
+ DesktopHeader,
11
+ DesktopSideNav,
12
+ Logo,
13
+ MobileBottomNav,
14
+ MobileSideMenu,
15
+ MobileTopNav,
16
+ };
@@ -8,6 +8,7 @@ import DesktopHeader from '../components/DesktopHeader.svelte';
8
8
  import MobileTopNav from '../components/MobileTopNav.svelte';
9
9
  import MobileBottomNav from '../components/MobileBottomNav.svelte';
10
10
  import MobileSideMenu from '../components/MobileSideMenu.svelte';
11
+ import Logo from '../components/Logo.svelte';
11
12
 
12
13
  const { componentOptions = {}, ssrSelectedItem, gtmID } = Astro.props;
13
14
 
@@ -19,31 +20,39 @@ const title = getPageTitle(Astro);
19
20
  <div class="banner">
20
21
  <UsaBanner isInverted={componentOptions.usaBanner?.theme === 'inverse'} />
21
22
  </div>
22
- <!-- Mobile Navigation -->
23
- <MobileTopNav client:load />
24
- <MobileSideMenu client:load>
25
- <slot name="sub-nav" />
26
- </MobileSideMenu>
27
- <MobileBottomNav client:load />
28
-
29
23
  <!-- Desktop Navigation -->
30
24
  <div class="layout-container">
31
- <DesktopSideNav client:load ssrSelectedItem={ssrSelectedItem}>
32
- <slot name="sub-nav" />
33
- </DesktopSideNav>
25
+ <nav aria-label="Main navigation">
26
+ <DesktopSideNav client:load ssrSelectedItem={ssrSelectedItem}>
27
+ <slot name="sub-nav" />
28
+ </DesktopSideNav>
29
+ </nav>
34
30
  <div class="main-container">
35
- <DesktopHeader client:load />
31
+ <header>
32
+ <DesktopHeader client:load />
33
+ </header>
36
34
  <main id="main-content">
37
35
  <slot />
38
36
  </main>
39
37
  </div>
40
38
  </div>
41
39
 
40
+ <!-- Mobile Navigation -->
41
+ <nav aria-label="Mobile navigation">
42
+ <MobileTopNav client:load />
43
+ <MobileSideMenu client:load>
44
+ <slot name="sub-nav" />
45
+ </MobileSideMenu>
46
+ <MobileBottomNav client:load />
47
+ </nav>
48
+
49
+ <!-- Fixed Logo (Desktop only) -->
50
+ <div class="logo-fixed-container display-none tablet:display-flex">
51
+ <Logo width={75} height={22} />
52
+ </div>
53
+
42
54
  <!-- Mobile Main Content -->
43
- <main
44
- id="main-content-mobile"
45
- class="main-content-mobile"
46
- >
55
+ <main id="main-content-mobile" class="main-content-mobile">
47
56
  <slot />
48
57
  </main>
49
58
  </div>
@@ -121,31 +121,16 @@ export const navigationStore: NavigationStore = {
121
121
  },
122
122
 
123
123
  handleLayoutTransition: (isMobile: boolean): void => {
124
- console.log('[NavigationStore] handleLayoutTransition called:', {
125
- isMobile,
126
- });
127
-
128
124
  if (isMobile) {
129
- // Switching to mobile: always close navigation
130
- console.log('[NavigationStore] Closing navigation for mobile');
131
125
  isExpanded.set(false);
132
126
  } else {
133
- // Switching to desktop: open navigation if on non-discover page
134
127
  const currentItem = getInitialValue('globalSideNav-selectedItem', null);
135
- console.log(
136
- '[NavigationStore] Desktop transition - current item:',
137
- currentItem
138
- );
139
-
140
128
  if (currentItem && currentItem !== 'discover') {
141
129
  console.log(
142
130
  '[NavigationStore] Opening navigation for desktop (non-discover page)'
143
131
  );
144
132
  isExpanded.set(true);
145
133
  } else {
146
- console.log(
147
- '[NavigationStore] Keeping navigation closed for desktop (discover page or no selection)'
148
- );
149
134
  isExpanded.set(false);
150
135
  }
151
136
  }
@@ -31,6 +31,14 @@
31
31
  overflow-y: auto;
32
32
  }
33
33
 
34
+ .logo-fixed-container {
35
+ position: fixed;
36
+ top: var(--ai-size-40);
37
+ left: var(--ai-size-88);
38
+ z-index: 500;
39
+ background: transparent;
40
+ }
41
+
34
42
  @media (--ai-size-breakpoint-tablet) {
35
43
  .main-content-mobile {
36
44
  display: none;
@@ -41,22 +49,11 @@
41
49
  overflow: hidden;
42
50
  flex-direction: row;
43
51
  }
44
- .side-nav-container {
45
- display: flex;
46
- flex-shrink: 0;
47
- position: relative;
48
- z-index: 2;
49
- }
50
- .side-nav-container.expanded {
51
- width: 12.5rem;
52
- min-width: 12.5rem;
53
- max-width: 12.5rem;
54
- }
55
52
  .main-container {
56
53
  flex: 1 1 0%;
57
54
  min-width: 0;
58
55
  position: relative;
59
- z-index: 1;
56
+ z-index: var(--ai-layer-1);
60
57
  }
61
58
  .app main {
62
59
  padding: var(--ai-size-40) var(--ai-size-32) var(--ai-size-8) 4.875rem;