@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
@@ -0,0 +1,594 @@
1
+ ---
2
+ import BaseLayout from '@gsa-tts/graymatter-ui/layouts/BaseLayout.astro';
3
+ import '../styles/base/global.css';
4
+ import { getPageTitle } from '@gsa-tts/graymatter-ui/helpers';
5
+ import UsaBanner from '../components/UsaBanner.astro';
6
+ import DesktopSideNav from '../components/DesktopSideNav.svelte';
7
+ import DesktopHeader from '../components/DesktopHeader.svelte';
8
+ import MobileTopNav from '../components/MobileTopNav.svelte';
9
+ import MobileBottomNav from '../components/MobileBottomNav.svelte';
10
+ import MobileSideMenu from '../components/MobileSideMenu.svelte';
11
+ import Logo from '../components/Logo.svelte';
12
+
13
+ const { componentOptions = {}, ssrSelectedItem, gtmID } = Astro.props;
14
+
15
+ const title = getPageTitle(Astro);
16
+ ---
17
+
18
+ <BaseLayout title={title} gtmID={gtmID}>
19
+ <div class="app">
20
+ <div class="banner">
21
+ <UsaBanner isInverted={componentOptions.usaBanner?.theme === 'inverse'} />
22
+ </div>
23
+ <!-- Desktop Navigation -->
24
+ <div class="layout-container">
25
+ <nav aria-label="Main navigation">
26
+ <DesktopSideNav client:load ssrSelectedItem={ssrSelectedItem}>
27
+ <slot name="sub-nav" />
28
+ </DesktopSideNav>
29
+ </nav>
30
+ <div class="main-container">
31
+ <header>
32
+ <DesktopHeader client:load />
33
+ </header>
34
+ <main id="main-content">
35
+ <slot />
36
+ </main>
37
+ </div>
38
+ </div>
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
+
54
+ <!-- Mobile Main Content -->
55
+ <main id="main-content-mobile" class="main-content-mobile">
56
+ <slot />
57
+ </main>
58
+ </div>
59
+ </BaseLayout>
60
+
61
+ <script is:inline>
62
+ if (typeof window !== 'undefined') {
63
+ // Handle URL hash scrolling immediately to prevent initial render at top
64
+ if (window.location.hash) {
65
+ const anchor = window.location.hash.substring(1);
66
+
67
+ // Set a flag to prevent SubNavMenu from also scrolling
68
+ sessionStorage.setItem('globalLayoutScrolling', 'true');
69
+
70
+ // Use requestAnimationFrame to scroll as early as possible
71
+ requestAnimationFrame(() => {
72
+ if (window.innerWidth <= 640) {
73
+ // Mobile: scroll in mobile content container
74
+ const mobileContent = document.getElementById('main-content-mobile');
75
+ if (mobileContent) {
76
+ const mobileAnchor = mobileContent.querySelector(
77
+ `[id="${anchor}"]`
78
+ );
79
+ if (mobileAnchor) {
80
+ const rect = mobileAnchor.getBoundingClientRect();
81
+ const mobileRect = mobileContent.getBoundingClientRect();
82
+ const scrollTop =
83
+ mobileContent.scrollTop + rect.top - mobileRect.top;
84
+ console.log(
85
+ '[GlobalAppLayout] Immediate mobile scroll to:',
86
+ scrollTop
87
+ );
88
+ mobileContent.scrollTo({ top: scrollTop, behavior: 'auto' });
89
+
90
+ setTimeout(() => {
91
+ let topLevelTitle = null;
92
+ if (typeof window !== 'undefined') {
93
+ topLevelTitle = sessionStorage.getItem(
94
+ 'subNav-topLevelTitle'
95
+ );
96
+ if (topLevelTitle) {
97
+ try {
98
+ topLevelTitle = JSON.parse(topLevelTitle);
99
+ } catch {}
100
+ }
101
+ }
102
+ window.dispatchEvent(
103
+ new CustomEvent('sectionVisible', {
104
+ detail: {
105
+ title: topLevelTitle || mobileAnchor.textContent?.trim(),
106
+ id: mobileAnchor.id,
107
+ },
108
+ })
109
+ );
110
+ sessionStorage.removeItem('globalLayoutScrolling');
111
+ }, 100);
112
+ }
113
+ }
114
+ } else {
115
+ // Desktop: scroll in desktop content container
116
+ const mainContent = document.getElementById('main-content');
117
+ if (mainContent) {
118
+ const desktopAnchor = mainContent.querySelector(`[id="${anchor}"]`);
119
+ if (desktopAnchor) {
120
+ console.log(
121
+ '[GlobalAppLayout] Immediate desktop scroll for:',
122
+ anchor
123
+ );
124
+
125
+ try {
126
+ // Add temporary scroll margin to account for header
127
+ const originalScrollMargin =
128
+ desktopAnchor.style.scrollMarginTop;
129
+ desktopAnchor.style.scrollMarginTop = '56px';
130
+
131
+ desktopAnchor.scrollIntoView({
132
+ behavior: 'auto',
133
+ block: 'start',
134
+ inline: 'nearest',
135
+ });
136
+
137
+ // Restore original scroll margin after a delay
138
+ setTimeout(() => {
139
+ desktopAnchor.style.scrollMarginTop = originalScrollMargin;
140
+ }, 1000);
141
+ } catch (e) {
142
+ // Fallback to manual calculation
143
+ const headerHeight = 56;
144
+ const rect = desktopAnchor.getBoundingClientRect();
145
+ const scrollTop = window.scrollY + rect.top - headerHeight;
146
+ const finalScrollTop = Math.max(0, scrollTop);
147
+ window.scrollTo({ top: finalScrollTop, behavior: 'auto' });
148
+ }
149
+
150
+ setTimeout(() => {
151
+ let topLevelTitle = null;
152
+ if (typeof window !== 'undefined') {
153
+ topLevelTitle = sessionStorage.getItem(
154
+ 'subNav-topLevelTitle'
155
+ );
156
+ if (topLevelTitle) {
157
+ try {
158
+ topLevelTitle = JSON.parse(topLevelTitle);
159
+ } catch {}
160
+ }
161
+ }
162
+ window.dispatchEvent(
163
+ new CustomEvent('sectionVisible', {
164
+ detail: {
165
+ title: topLevelTitle || desktopAnchor.textContent?.trim(),
166
+ id: desktopAnchor.id,
167
+ },
168
+ })
169
+ );
170
+ sessionStorage.removeItem('globalLayoutScrolling');
171
+ }, 100);
172
+ }
173
+ }
174
+ }
175
+ });
176
+ }
177
+
178
+ window.addEventListener('DOMContentLoaded', () => {
179
+ // Check navigation flag first, before any components mount
180
+ const isMainNavNavigation =
181
+ sessionStorage.getItem('navigatingToMainNav') === 'true';
182
+ console.log(
183
+ '[GlobalAppLayout] DOMContentLoaded - navigation flag:',
184
+ isMainNavNavigation
185
+ );
186
+
187
+ if (isMainNavNavigation) {
188
+ // Clear the flag immediately
189
+ sessionStorage.removeItem('navigatingToMainNav');
190
+ console.log(
191
+ '[GlobalAppLayout] Cleared navigation flag for main nav navigation'
192
+ );
193
+ // Don't clear header state here - let navigation components handle it
194
+ }
195
+
196
+ // Handle URL hash scrolling (fallback for cases where immediate scroll didn't work)
197
+ if (window.location.hash) {
198
+ const anchor = window.location.hash.substring(1);
199
+ console.log(
200
+ '[GlobalAppLayout] DOMContentLoaded fallback hash scroll for:',
201
+ anchor
202
+ );
203
+
204
+ // Only proceed if we haven't already scrolled
205
+ if (sessionStorage.getItem('globalLayoutScrolling') !== 'true') {
206
+ sessionStorage.setItem('globalLayoutScrolling', 'true');
207
+
208
+ if (window.innerWidth <= 640) {
209
+ // Mobile: scroll in mobile content container
210
+ const mobileContent = document.getElementById(
211
+ 'main-content-mobile'
212
+ );
213
+ if (mobileContent) {
214
+ const mobileAnchor = mobileContent.querySelector(
215
+ `[id="${anchor}"]`
216
+ );
217
+ if (mobileAnchor) {
218
+ setTimeout(() => {
219
+ const rect = mobileAnchor.getBoundingClientRect();
220
+ const mobileRect = mobileContent.getBoundingClientRect();
221
+ const scrollTop =
222
+ mobileContent.scrollTop + rect.top - mobileRect.top;
223
+ console.log(
224
+ '[GlobalAppLayout] Fallback mobile scroll to:',
225
+ scrollTop
226
+ );
227
+ mobileContent.scrollTo({ top: scrollTop, behavior: 'auto' });
228
+
229
+ setTimeout(() => {
230
+ let topLevelTitle = null;
231
+ if (typeof window !== 'undefined') {
232
+ topLevelTitle = sessionStorage.getItem(
233
+ 'subNav-topLevelTitle'
234
+ );
235
+ if (topLevelTitle) {
236
+ try {
237
+ topLevelTitle = JSON.parse(topLevelTitle);
238
+ } catch {}
239
+ }
240
+ }
241
+ window.dispatchEvent(
242
+ new CustomEvent('sectionVisible', {
243
+ detail: {
244
+ title:
245
+ topLevelTitle || mobileAnchor.textContent?.trim(),
246
+ id: mobileAnchor.id,
247
+ },
248
+ })
249
+ );
250
+ // Clear the flag after scrolling
251
+ if (typeof window !== 'undefined') {
252
+ sessionStorage.removeItem('globalLayoutScrolling');
253
+ }
254
+ }, 100);
255
+ }, 100);
256
+ }
257
+ }
258
+ } else {
259
+ // Desktop: scroll in desktop content container
260
+ const mainContent = document.getElementById('main-content');
261
+ if (mainContent) {
262
+ const desktopAnchor = mainContent.querySelector(
263
+ `[id="${anchor}"]`
264
+ );
265
+ if (desktopAnchor) {
266
+ setTimeout(() => {
267
+ console.log(
268
+ '[GlobalAppLayout] Fallback desktop scroll for:',
269
+ anchor
270
+ );
271
+
272
+ // Try scrollIntoView first with a temporary offset
273
+ try {
274
+ // Add temporary scroll margin to account for header
275
+ const originalScrollMargin =
276
+ desktopAnchor.style.scrollMarginTop;
277
+ desktopAnchor.style.scrollMarginTop = '56px';
278
+
279
+ desktopAnchor.scrollIntoView({
280
+ behavior: 'auto',
281
+ block: 'start',
282
+ inline: 'nearest',
283
+ });
284
+
285
+ console.log(
286
+ '[GlobalAppLayout] Used fallback scrollIntoView for desktop'
287
+ );
288
+
289
+ // Restore original scroll margin after a delay
290
+ setTimeout(() => {
291
+ desktopAnchor.style.scrollMarginTop =
292
+ originalScrollMargin;
293
+ }, 1000);
294
+ } catch (e) {
295
+ console.log(
296
+ '[GlobalAppLayout] Fallback scrollIntoView failed, using manual calculation:',
297
+ e
298
+ );
299
+
300
+ // Fallback to manual calculation
301
+ const headerHeight = 56;
302
+ const rect = desktopAnchor.getBoundingClientRect();
303
+ const scrollTop = window.scrollY + rect.top - headerHeight;
304
+ console.log(
305
+ '[GlobalAppLayout] Fallback desktop scroll calculation:',
306
+ {
307
+ anchor,
308
+ rectTop: rect.top,
309
+ windowScrollY: window.scrollY,
310
+ headerHeight,
311
+ scrollTop,
312
+ elementText: desktopAnchor.textContent?.trim(),
313
+ }
314
+ );
315
+
316
+ // Ensure scrollTop is not negative
317
+ const finalScrollTop = Math.max(0, scrollTop);
318
+ console.log(
319
+ '[GlobalAppLayout] Fallback final scroll position:',
320
+ finalScrollTop
321
+ );
322
+
323
+ window.scrollTo({ top: finalScrollTop, behavior: 'auto' });
324
+ }
325
+
326
+ setTimeout(() => {
327
+ let topLevelTitle = null;
328
+ if (typeof window !== 'undefined') {
329
+ topLevelTitle = sessionStorage.getItem(
330
+ 'subNav-topLevelTitle'
331
+ );
332
+ if (topLevelTitle) {
333
+ try {
334
+ topLevelTitle = JSON.parse(topLevelTitle);
335
+ } catch {}
336
+ }
337
+ }
338
+ window.dispatchEvent(
339
+ new CustomEvent('sectionVisible', {
340
+ detail: {
341
+ title:
342
+ topLevelTitle || desktopAnchor.textContent?.trim(),
343
+ id: desktopAnchor.id,
344
+ },
345
+ })
346
+ );
347
+ // Clear the flag after scrolling
348
+ if (typeof window !== 'undefined') {
349
+ sessionStorage.removeItem('globalLayoutScrolling');
350
+ }
351
+ }, 100);
352
+ }, 100);
353
+ } else {
354
+ console.log(
355
+ '[GlobalAppLayout] Fallback desktop anchor not found in main-content container:',
356
+ anchor
357
+ );
358
+ // Log all available anchors for debugging
359
+ const allAnchors = mainContent.querySelectorAll('[id]');
360
+ console.log(
361
+ '[GlobalAppLayout] Available anchors in main-content:',
362
+ Array.from(allAnchors).map(el => el.id)
363
+ );
364
+ // Clear the flag if anchor not found
365
+ if (typeof window !== 'undefined') {
366
+ sessionStorage.removeItem('globalLayoutScrolling');
367
+ }
368
+ }
369
+ } else {
370
+ console.log(
371
+ '[GlobalAppLayout] Fallback desktop main-content container not found'
372
+ );
373
+ // Clear the flag if container not found
374
+ if (typeof window !== 'undefined') {
375
+ sessionStorage.removeItem('globalLayoutScrolling');
376
+ }
377
+ }
378
+ }
379
+ }
380
+ }
381
+ });
382
+ }
383
+
384
+ // Handle layout changes (mobile ↔ desktop)
385
+ if (typeof window !== 'undefined') {
386
+ let lastWidth = window.innerWidth;
387
+ let resizeTimeout = null;
388
+
389
+ window.addEventListener('resize', () => {
390
+ // Debounce resize events
391
+ if (resizeTimeout) {
392
+ clearTimeout(resizeTimeout);
393
+ }
394
+
395
+ resizeTimeout = setTimeout(() => {
396
+ const currentWidth = window.innerWidth;
397
+ const wasMobile = lastWidth <= 640;
398
+ const isMobile = currentWidth <= 640;
399
+
400
+ console.log('[GlobalAppLayout] Resize detected:', {
401
+ lastWidth,
402
+ currentWidth,
403
+ wasMobile,
404
+ isMobile,
405
+ changed: wasMobile !== isMobile,
406
+ });
407
+
408
+ if (wasMobile !== isMobile) {
409
+ console.log(
410
+ '[GlobalAppLayout] Layout changed from',
411
+ wasMobile ? 'mobile' : 'desktop',
412
+ 'to',
413
+ isMobile ? 'mobile' : 'desktop'
414
+ );
415
+
416
+ // Handle anchor scrolling for layout changes
417
+ if (window.location.hash) {
418
+ const anchor = window.location.hash.substring(1);
419
+ console.log(
420
+ '[GlobalAppLayout] Layout change with URL hash:',
421
+ anchor
422
+ );
423
+
424
+ // Wait for navigation state to update, then scroll
425
+ setTimeout(() => {
426
+ if (isMobile) {
427
+ // Mobile: scroll in mobile content container
428
+ const mobileContent = document.getElementById(
429
+ 'main-content-mobile'
430
+ );
431
+ if (mobileContent) {
432
+ const mobileAnchor = mobileContent.querySelector(
433
+ `[id="${anchor}"]`
434
+ );
435
+ if (mobileAnchor) {
436
+ const rect = mobileAnchor.getBoundingClientRect();
437
+ const mobileRect = mobileContent.getBoundingClientRect();
438
+ const scrollTop =
439
+ mobileContent.scrollTop + rect.top - mobileRect.top;
440
+ console.log(
441
+ '[GlobalAppLayout] Scrolling mobile to anchor:',
442
+ scrollTop
443
+ );
444
+ mobileContent.scrollTo({
445
+ top: scrollTop,
446
+ behavior: 'auto',
447
+ });
448
+
449
+ setTimeout(() => {
450
+ let topLevelTitle = null;
451
+ if (typeof window !== 'undefined') {
452
+ topLevelTitle = sessionStorage.getItem(
453
+ 'subNav-topLevelTitle'
454
+ );
455
+ if (topLevelTitle) {
456
+ try {
457
+ topLevelTitle = JSON.parse(topLevelTitle);
458
+ } catch {}
459
+ }
460
+ }
461
+ window.dispatchEvent(
462
+ new CustomEvent('sectionVisible', {
463
+ detail: {
464
+ title:
465
+ topLevelTitle || mobileAnchor.textContent?.trim(),
466
+ id: mobileAnchor.id,
467
+ },
468
+ })
469
+ );
470
+ }, 500);
471
+ }
472
+ }
473
+ } else {
474
+ // Desktop: scroll in desktop content container
475
+ const mainContent = document.getElementById('main-content');
476
+ if (mainContent) {
477
+ const desktopAnchor = mainContent.querySelector(
478
+ `[id="${anchor}"]`
479
+ );
480
+ if (desktopAnchor) {
481
+ console.log(
482
+ '[GlobalAppLayout] Desktop anchor found for layout change:',
483
+ {
484
+ anchor,
485
+ elementText: desktopAnchor.textContent?.trim(),
486
+ elementId: desktopAnchor.id,
487
+ }
488
+ );
489
+
490
+ // Try scrollIntoView first with a temporary offset
491
+ try {
492
+ // Add temporary scroll margin to account for header
493
+ const originalScrollMargin =
494
+ desktopAnchor.style.scrollMarginTop;
495
+ desktopAnchor.style.scrollMarginTop = '56px';
496
+
497
+ desktopAnchor.scrollIntoView({
498
+ behavior: 'auto',
499
+ block: 'start',
500
+ inline: 'nearest',
501
+ });
502
+
503
+ console.log(
504
+ '[GlobalAppLayout] Used scrollIntoView for desktop layout change with scroll margin'
505
+ );
506
+
507
+ // Restore original scroll margin after a delay
508
+ setTimeout(() => {
509
+ desktopAnchor.style.scrollMarginTop =
510
+ originalScrollMargin;
511
+ }, 1000);
512
+ } catch (e) {
513
+ console.log(
514
+ '[GlobalAppLayout] scrollIntoView failed for layout change, using manual calculation:',
515
+ e
516
+ );
517
+
518
+ // Fallback to manual calculation
519
+ const headerHeight = 56;
520
+ const rect = desktopAnchor.getBoundingClientRect();
521
+ const scrollTop =
522
+ window.scrollY + rect.top - headerHeight;
523
+ console.log(
524
+ '[GlobalAppLayout] Desktop scroll calculation for layout change:',
525
+ {
526
+ anchor,
527
+ rectTop: rect.top,
528
+ windowScrollY: window.scrollY,
529
+ headerHeight,
530
+ scrollTop,
531
+ elementText: desktopAnchor.textContent?.trim(),
532
+ }
533
+ );
534
+
535
+ // Ensure scrollTop is not negative
536
+ const finalScrollTop = Math.max(0, scrollTop);
537
+ console.log(
538
+ '[GlobalAppLayout] Final scroll position for layout change:',
539
+ finalScrollTop
540
+ );
541
+
542
+ window.scrollTo({
543
+ top: finalScrollTop,
544
+ behavior: 'auto',
545
+ });
546
+ }
547
+
548
+ setTimeout(() => {
549
+ let topLevelTitle = null;
550
+ if (typeof window !== 'undefined') {
551
+ topLevelTitle = sessionStorage.getItem(
552
+ 'subNav-topLevelTitle'
553
+ );
554
+ if (topLevelTitle) {
555
+ try {
556
+ topLevelTitle = JSON.parse(topLevelTitle);
557
+ } catch {}
558
+ }
559
+ }
560
+ window.dispatchEvent(
561
+ new CustomEvent('sectionVisible', {
562
+ detail: {
563
+ title:
564
+ topLevelTitle ||
565
+ desktopAnchor.textContent?.trim(),
566
+ id: desktopAnchor.id,
567
+ },
568
+ })
569
+ );
570
+ }, 500);
571
+ }
572
+ }
573
+ }
574
+ }, 350); // Wait for navigation state to settle
575
+ }
576
+
577
+ // Dispatch custom event for layout transition
578
+ window.dispatchEvent(
579
+ new CustomEvent('layoutTransition', {
580
+ detail: { isMobile },
581
+ })
582
+ );
583
+ console.log(
584
+ '[GlobalAppLayout] Dispatched layoutTransition event for',
585
+ isMobile ? 'mobile' : 'desktop',
586
+ 'layout'
587
+ );
588
+ }
589
+
590
+ lastWidth = currentWidth;
591
+ }, 250); // Debounce delay
592
+ });
593
+ }
594
+ </script>