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