@gsa-tts/graymatter-ui 0.0.2 → 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.
Files changed (43) hide show
  1. package/assets/images/ai-icons/chevron-up-down.svg +4 -0
  2. package/dist/graymatter-ui.js +4082 -0
  3. package/dist/graymatter-ui.umd.cjs +40 -0
  4. package/index.ts +3 -0
  5. package/package.json +10 -5
  6. package/src/components/Button.svelte +11 -23
  7. package/src/components/Button.webcomponent.svelte +44 -0
  8. package/src/components/DesktopHeader.svelte +101 -0
  9. package/src/components/DesktopHeader.webcomponent.svelte +17 -0
  10. package/src/components/DesktopSideNav.svelte +645 -0
  11. package/src/components/DesktopSideNav.webcomponent.svelte +19 -0
  12. package/src/components/GoogleTagManager.astro +16 -0
  13. package/src/components/Logo.svelte +8 -7
  14. package/src/components/Logo.webcomponent.svelte +26 -0
  15. package/src/components/MobileBottomNav.svelte +145 -0
  16. package/src/components/MobileBottomNav.webcomponent.svelte +14 -0
  17. package/src/components/MobileSideMenu.svelte +225 -0
  18. package/src/components/MobileSideMenu.webcomponent.svelte +11 -0
  19. package/src/components/MobileTopNav.svelte +235 -0
  20. package/src/components/MobileTopNav.webcomponent.svelte +14 -0
  21. package/src/components/NavigationInitializer.svelte +19 -0
  22. package/src/components/icons/ApiIcon.svelte +49 -0
  23. package/src/components/icons/ChatIcon.svelte +27 -0
  24. package/src/components/icons/CloseIcon.svelte +18 -0
  25. package/src/components/icons/ConsoleIcon.svelte +25 -0
  26. package/src/components/icons/DiscoverIcon.svelte +30 -0
  27. package/src/components/icons/MenuIcon.svelte +38 -0
  28. package/src/components/icons/index.ts +6 -0
  29. package/src/components/index.ts +9 -0
  30. package/src/index.ts +16 -0
  31. package/src/layouts/AppLayout.astro +1 -1
  32. package/src/layouts/BaseLayout.astro +4 -0
  33. package/src/layouts/GlobalAppLayout.astro +594 -0
  34. package/src/stores/navigationStore.ts +138 -0
  35. package/src/stores/subNavReadyStore.ts +3 -0
  36. package/src/styles/base/global.css +8 -1
  37. package/src/styles/base/typography.css +15 -14
  38. package/src/styles/components/globalAppLayout.css +73 -0
  39. package/src/styles/components/globalAppNav.css +95 -0
  40. package/src/utils/getAppUrls.ts +29 -0
  41. package/src/utils/index.ts +1 -0
  42. package/assets/images/gsai-logo-black.svg +0 -18
  43. package/assets/images/gsai-logo-white.svg +0 -18
@@ -2,8 +2,8 @@
2
2
  export let isInverted: boolean = false;
3
3
  export let width: number | undefined;
4
4
  export let height: number | undefined;
5
- const defaultWidth = 105;
6
- const defaultHeight = 33;
5
+ const defaultWidth = 102;
6
+ const defaultHeight = 30;
7
7
  width = width || defaultWidth;
8
8
  height = height || defaultHeight;
9
9
 
@@ -13,7 +13,7 @@
13
13
  <svg
14
14
  {width}
15
15
  {height}
16
- viewBox="0 0 105 33"
16
+ viewBox="0 0 102 30"
17
17
  fill="none"
18
18
  xmlns="http://www.w3.org/2000/svg"
19
19
  style="fill: {logoColor};"
@@ -21,14 +21,15 @@
21
21
  >
22
22
  <g>
23
23
  <path
24
- d="M13.4937 19.864H26.5932C25.3427 24.7736 21.4655 27.9119 16.5655 27.9119C10.167 27.9119 4.96116 22.7532 4.96116 16.4125C4.96116 10.0717 10.1636 4.91296 16.5621 4.91296C20.1777 4.91296 23.5248 6.53939 25.7437 9.37806L27.2626 11.321L31.184 8.31061L29.665 6.36765C26.4981 2.3201 21.7238 0 16.5621 0C7.43155 0 0 7.36102 0 16.4125C0 25.4639 7.43155 32.8249 16.5621 32.8249C24.9553 32.8249 31.4218 26.4 31.9383 17.5506L32.0913 14.951H13.4937V19.864Z"
24
+ d="M57.5791 20.4177C57.5791 15.5645 53.7309 11.6187 48.9988 11.6187H40.188C38.6688 11.6187 37.4304 10.3567 37.4304 8.80197C37.4304 7.24729 38.6658 5.98522 40.188 5.98522H56.2373V0H38.8757C34.1466 0 30.2983 3.94877 30.2983 8.80197C30.2983 13.6552 34.1466 17.6039 38.8757 17.6039H47.6865C49.2057 17.6039 50.4442 18.866 50.4442 20.4207C50.4442 21.9754 49.2087 23.2374 47.6865 23.2374H31.0284V29.2197H48.9988C53.7309 29.2197 57.5791 25.2709 57.5791 20.4207V20.4177Z"
25
25
  />
26
26
  <path
27
- d="M52.9238 13.9544H44.0379C41.6559 13.9544 39.7156 12.035 39.7156 9.67113C39.7156 7.30725 41.6559 5.38786 44.0379 5.38786H61.0282V0.478271H44.0379C38.9204 0.478271 34.7578 4.60327 34.7578 9.6745C34.7578 14.7457 38.9204 18.8707 44.0379 18.8707H52.9238C55.3059 18.8707 57.2462 20.7901 57.2462 23.154C57.2462 25.5179 55.3059 27.4373 52.9238 27.4373H35.4V32.3502H52.9238C58.0413 32.3502 62.2039 28.2252 62.2039 23.154C62.2039 18.0828 58.0413 13.9578 52.9238 13.9578V13.9544Z"
27
+ d="M82.1466 24.7153L82.188 24.7685L83.6747 29.2197H90.6914L80.917 0H68.3525L58.5781 29.2167H65.5949L67.0993 24.7153H82.1466ZM69.186 18.4818L73.3653 5.98522H75.9013L75.919 6.03547L80.1161 18.5793H69.1535L69.186 18.4818Z"
28
28
  />
29
- <path d="M104.809 0.478271H99.8516V32.3468H104.809V0.478271Z" />
30
29
  <path
31
- d="M76.2292 0.478271L62.7559 32.3536H68.1316L79.5287 5.39123H79.9874L91.3811 32.3536H96.7568L83.2835 0.478271H76.2292Z"
30
+ d="M27.198 0H20.5626V15.8158C20.5626 21.4345 16.9626 23.4355 13.5961 23.4355C9.03842 23.4355 6.62956 20.799 6.62956 15.8158V0H0V17.0069C0 20.6985 1.46305 24.0532 4.11724 26.4532C6.64729 28.7409 10.0138 30 13.599 30C20.3586 30 27.198 25.5369 27.198 17.0039V0Z"
32
31
  />
32
+ <path d="M101.708 9.78915H93.8662V29.2197H101.708V9.78915Z" />
33
+ <path d="M101.823 0H93.8662V5.98227H101.823V0Z" />
33
34
  </g>
34
35
  </svg>
@@ -0,0 +1,26 @@
1
+ <svelte:options customElement={{
2
+ tag: 'graymatter-logo',
3
+ props: {
4
+ isInverted: { reflect: true, type: 'Boolean', attribute: 'is-inverted' },
5
+ width: { reflect: true, type: 'Number', attribute: 'width' },
6
+ height: { reflect: true, type: 'Number', attribute: 'height' }
7
+ }
8
+ }} />
9
+
10
+ <script lang="ts">
11
+ import Logo from './Logo.svelte';
12
+
13
+ let {
14
+ isInverted = false,
15
+ width = undefined,
16
+ height = undefined,
17
+ ...props
18
+ } = $props();
19
+ </script>
20
+
21
+ <Logo
22
+ {isInverted}
23
+ {width}
24
+ {height}
25
+ {...props}
26
+ />
@@ -0,0 +1,145 @@
1
+ <script lang="ts">
2
+ import { selectedItem, navigationStore } from '../stores/navigationStore.js';
3
+ import type { NavigationItem } from '../stores/navigationStore.js';
4
+ import DiscoverIcon from './icons/DiscoverIcon.svelte';
5
+ import ChatIcon from './icons/ChatIcon.svelte';
6
+ import ConsoleIcon from './icons/ConsoleIcon.svelte';
7
+ import ApiIcon from './icons/ApiIcon.svelte';
8
+ import { getAppUrls } from '../utils/getAppUrls';
9
+
10
+ export let onNavItemClick: ((data: { item: NavigationItem; expanded: boolean }) => void) | undefined = undefined;
11
+
12
+ const urls = getAppUrls();
13
+
14
+ const navItems = [
15
+ { id: 'discover', label: 'Discover', icon: DiscoverIcon, href: urls.discover },
16
+ { id: 'chat', label: 'Chat', icon: ChatIcon, href: urls.chat },
17
+ { id: 'console', label: 'Console', icon: ConsoleIcon, href: urls.console },
18
+ { id: 'api', label: 'API', icon: ApiIcon, href: urls.api }
19
+ ] as const;
20
+
21
+ function handleNavItemClick(item: NavigationItem) {
22
+ // Set navigation flag immediately to prevent header restoration
23
+ if (item !== 'discover' && typeof window !== 'undefined') {
24
+ sessionStorage.setItem('navigatingToMainNav', 'true');
25
+ }
26
+
27
+ navigationStore.setSelectedItem(item);
28
+
29
+ // Clear header state for main navigation items (including discover)
30
+ if (item !== 'discover') {
31
+ try {
32
+ import('@gsa-tts/graymatter-ui/stores/navigationStore').then(({ selectedSubNavItemTitle }) => {
33
+ // Clear the store value immediately
34
+ selectedSubNavItemTitle.set('');
35
+ if (typeof window !== 'undefined') {
36
+ sessionStorage.removeItem('subNav-selectedTitle');
37
+ sessionStorage.removeItem('subNav-selectedId');
38
+ }
39
+
40
+ // Set default header from first submenu item after a short delay
41
+ setTimeout(() => {
42
+ const mainContent = document.getElementById('main-content');
43
+ if (mainContent) {
44
+ const firstSection = mainContent.querySelector('h2[id], h3[id], h4[id], h5[id], h6[id]');
45
+ if (firstSection && firstSection.textContent) {
46
+ const firstSectionTitle = firstSection.textContent.trim();
47
+ selectedSubNavItemTitle.set(firstSectionTitle);
48
+ if (typeof window !== 'undefined') {
49
+ sessionStorage.setItem('subNav-selectedTitle', JSON.stringify(firstSectionTitle));
50
+ }
51
+ }
52
+ }
53
+ }, 200); // Delay to ensure page content is loaded
54
+ }).catch((err) => {
55
+ console.log('[MobileBottomNav] Error clearing header state:', err);
56
+ });
57
+ } catch (err) {
58
+ console.log('[MobileBottomNav] Import failed for clearing state:', err);
59
+ }
60
+ } else {
61
+ // Clear header state for Discover page
62
+ try {
63
+ import('@gsa-tts/graymatter-ui/stores/navigationStore').then(({ selectedSubNavItemTitle }) => {
64
+ selectedSubNavItemTitle.set('');
65
+ if (typeof window !== 'undefined') {
66
+ sessionStorage.removeItem('subNav-selectedTitle');
67
+ sessionStorage.removeItem('subNav-selectedId');
68
+ }
69
+ }).catch((err) => {
70
+ console.log('[MobileBottomNav] Error clearing header state for Discover:', err);
71
+ });
72
+ } catch (err) {
73
+ console.log('[MobileBottomNav] Import failed for clearing Discover state:', err);
74
+ }
75
+ }
76
+
77
+ onNavItemClick?.({ item, expanded: false });
78
+ }
79
+ </script>
80
+
81
+ <nav class="mobile-bottom-nav" part="mobile-bottom-nav">
82
+ {#each navItems as navItem}
83
+ <a
84
+ href={navItem.href}
85
+ class="nav-item"
86
+ class:active={$selectedItem === navItem.id}
87
+ on:click={() => handleNavItemClick(navItem.id)}
88
+ part="nav-item"
89
+ >
90
+ <div class="icon">
91
+ <svelte:component
92
+ this={navItem.icon}
93
+ isSelected={$selectedItem === navItem.id}
94
+ />
95
+ </div>
96
+ <span class="label">{navItem.label}</span>
97
+ </a>
98
+ {/each}
99
+ </nav>
100
+
101
+ <style>
102
+ .nav-item {
103
+ flex: 1;
104
+ display: flex;
105
+ flex-direction: column;
106
+ align-items: center;
107
+ justify-content: center;
108
+ padding: var(--ai-size-4);
109
+ text-decoration: none;
110
+ color: var(--ai-color-neutral-600);
111
+ transition: color 0.2s ease;
112
+ height: 100%;
113
+ }
114
+
115
+ .nav-item.active {
116
+ color: var(--ai-color-neutral-900);
117
+ }
118
+
119
+ .nav-item.active .icon {
120
+ background-color: var(--ai-color-neutral-200);
121
+ border-radius: var(--ai-size-8);
122
+ }
123
+
124
+ .nav-item:hover {
125
+ color: var(--ai-color-neutral-700);
126
+ }
127
+
128
+ .icon {
129
+ width: var(--ai-size-24);
130
+ height: var(--ai-size-24);
131
+ margin-bottom: var(--ai-size-2);
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ padding: var(--ai-size-2);
136
+ transition: background-color 0.2s ease;
137
+ }
138
+
139
+ .label {
140
+ font-size: 0.6875rem; /* 11px */
141
+ font-weight: 500;
142
+ line-height: 1;
143
+ text-align: center;
144
+ }
145
+ </style>
@@ -0,0 +1,14 @@
1
+ <svelte:options customElement={{
2
+ tag: 'graymatter-mobile-bottom-nav'
3
+ }} />
4
+
5
+ <script lang="ts">
6
+ import MobileBottomNav from './MobileBottomNav.svelte';
7
+
8
+ // Props for the web component
9
+ let { onNavItemClick } = $props();
10
+ </script>
11
+
12
+ <MobileBottomNav
13
+ {onNavItemClick}
14
+ />
@@ -0,0 +1,225 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { selectedItem, isExpanded, navigationStore } from '../stores/navigationStore.js';
4
+ import Logo from './Logo.svelte';
5
+ import CloseIcon from './icons/CloseIcon.svelte';
6
+
7
+ let sideMenuElement: HTMLDivElement;
8
+ let pendingMobileHash: string | null = null;
9
+
10
+ function handleMobileAnchorNavigate(event: Event) {
11
+ const customEvent = event as CustomEvent;
12
+ pendingMobileHash = customEvent.detail.hash;
13
+ closeMobileMenu();
14
+ // Scroll immediately without delay
15
+ if (pendingMobileHash) {
16
+ const el = document.getElementById(pendingMobileHash);
17
+ if (el) {
18
+ // Check if the anchor exists in mobile content
19
+ const mobileContent = document.getElementById('main-content-mobile');
20
+ const mobileAnchor = mobileContent ? mobileContent.querySelector(`[id="${pendingMobileHash}"]`) : null;
21
+
22
+ if (mobileAnchor && mobileContent) {
23
+ // For mobile: show section title in both header and content
24
+ const rect = mobileAnchor.getBoundingClientRect();
25
+ const mobileRect = mobileContent.getBoundingClientRect();
26
+ const scrollTop = mobileContent.scrollTop + rect.top - mobileRect.top;
27
+ mobileContent.scrollTo({ top: scrollTop, behavior: 'auto' });
28
+
29
+ // Update header after scroll completes
30
+ setTimeout(() => {
31
+ window.dispatchEvent(new CustomEvent('sectionVisible', {
32
+ detail: {
33
+ title: mobileAnchor.textContent?.trim(),
34
+ id: mobileAnchor.id
35
+ }
36
+ }));
37
+ }, 100);
38
+ } else {
39
+ // Fallback to desktop content
40
+ let scrollableParent = el.parentElement;
41
+ while (scrollableParent && scrollableParent !== document.body) {
42
+ const style = window.getComputedStyle(scrollableParent);
43
+ if (style.overflowY === 'auto' || style.overflowY === 'scroll') {
44
+ break;
45
+ }
46
+ scrollableParent = scrollableParent.parentElement;
47
+ }
48
+
49
+ if (scrollableParent && scrollableParent !== document.body) {
50
+ const desktopMain = document.getElementById('main-content');
51
+ if (desktopMain) {
52
+ el.scrollIntoView({ behavior: 'auto', block: 'start' });
53
+ setTimeout(() => {
54
+ window.dispatchEvent(new CustomEvent('sectionVisible', {
55
+ detail: {
56
+ title: el.textContent?.trim(),
57
+ id: el.id
58
+ }
59
+ }));
60
+ }, 100);
61
+ } else {
62
+ el.scrollIntoView({ behavior: 'auto', block: 'start' });
63
+ }
64
+ } else {
65
+ el.scrollIntoView({ behavior: 'auto', block: 'start' });
66
+ }
67
+ }
68
+ }
69
+ pendingMobileHash = null;
70
+ }
71
+ }
72
+
73
+ onMount(() => {
74
+ // Listen for the custom event globally
75
+ window.addEventListener('mobileAnchorNavigate', handleMobileAnchorNavigate);
76
+
77
+ const handleCloseMobileMenu = () => {
78
+ closeMobileMenu();
79
+ };
80
+ window.addEventListener('closeMobileMenu', handleCloseMobileMenu);
81
+
82
+ const handleLayoutTransition = (event: Event) => {
83
+ const customEvent = event as CustomEvent;
84
+ const { isMobile } = customEvent.detail;
85
+
86
+ if (isMobile) {
87
+ navigationStore.setExpanded(false);
88
+ }
89
+ };
90
+ window.addEventListener('layoutTransition', handleLayoutTransition);
91
+
92
+ const handleEscape = (e: KeyboardEvent) => {
93
+ if (e.key === 'Escape' && $isExpanded) {
94
+ closeMobileMenu();
95
+ }
96
+ };
97
+ window.addEventListener('keydown', handleEscape);
98
+
99
+ return () => {
100
+ window.removeEventListener('mobileAnchorNavigate', handleMobileAnchorNavigate);
101
+ window.removeEventListener('closeMobileMenu', handleCloseMobileMenu);
102
+ window.removeEventListener('layoutTransition', handleLayoutTransition);
103
+ window.removeEventListener('keydown', handleEscape);
104
+ };
105
+ });
106
+
107
+ function closeMobileMenu() {
108
+ navigationStore.setExpanded(false);
109
+ }
110
+ </script>
111
+
112
+ <!-- Mobile overlay -->
113
+ {#if $isExpanded}
114
+ <div
115
+ class="mobile-overlay"
116
+ role="button"
117
+ tabindex="0"
118
+ aria-label="Close navigation menu (Escape key supported)"
119
+ part="mobile-overlay"
120
+ on:click={closeMobileMenu}
121
+ on:keydown={(e) => {
122
+ if (e.key === 'Enter' || e.key === ' ') closeMobileMenu();
123
+ }}
124
+ ></div>
125
+ {/if}
126
+
127
+ <!-- Mobile side menu -->
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">
131
+ <Logo width={75} height={22} />
132
+ </div>
133
+ <button class="close-button" on:click={closeMobileMenu} aria-label="Close navigation menu" part="close-button">
134
+ <CloseIcon />
135
+ </button>
136
+ </div>
137
+
138
+ <div class="menu-content" part="menu-content">
139
+ <!-- Show submenu items if available and not on discover -->
140
+ {#if $selectedItem && $selectedItem !== 'discover'}
141
+ <div class="subnav-section" part="subnav-section">
142
+ <slot />
143
+ </div>
144
+ {:else}
145
+ <!-- Show message when no submenu is available -->
146
+ <div class="no-subnav-message" part="no-subnav-message">
147
+ <p>Select a section from the bottom navigation</p>
148
+ </div>
149
+ {/if}
150
+ </div>
151
+ </div>
152
+
153
+ <style>
154
+ .no-subnav-message {
155
+ padding: var(--ai-size-16);
156
+ text-align: center;
157
+ color: var(--ai-color-steel-600);
158
+ font-size: var(--ai-size-14);
159
+ }
160
+
161
+ .no-subnav-message p {
162
+ margin: 0;
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
+ }
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>