@gsa-tts/graymatter-ui 0.1.0 → 0.2.1

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,17 @@
1
+ <svelte:options customElement={{
2
+ tag: 'graymatter-desktop-header',
3
+ props: {
4
+ customHeaderText: { reflect: true, type: 'String', attribute: 'custom-header-text' }
5
+ }
6
+ }} />
7
+
8
+ <script lang="ts">
9
+ import DesktopHeader from './DesktopHeader.svelte';
10
+
11
+ // Props for the web component
12
+ let { customHeaderText = '' } = $props();
13
+ </script>
14
+
15
+ <DesktopHeader
16
+ {customHeaderText}
17
+ />
@@ -6,7 +6,6 @@
6
6
  * - If not provided, the navigation will default to expanded/open on SSR.
7
7
  */
8
8
  import { onMount } from 'svelte';
9
- import Logo from './Logo.svelte';
10
9
  import {
11
10
  isExpanded,
12
11
  selectedItem,
@@ -19,8 +18,10 @@
19
18
  import ConsoleIcon from './icons/ConsoleIcon.svelte';
20
19
  import ApiIcon from './icons/ApiIcon.svelte';
21
20
  import MenuIcon from './icons/MenuIcon.svelte';
21
+ import { getAppUrls } from '../utils/getAppUrls';
22
+ const urls = getAppUrls();
22
23
 
23
- const { ssrSelectedItem = undefined, onNavToggle = undefined, onNavItemClick = undefined, onProfileClick = undefined, children } = $props();
24
+ const { ssrSelectedItem = undefined, onNavToggle = undefined, onNavItemClick = undefined, onProfileClick = undefined, children, apiUrl, chatUrl, consoleUrl, discoverUrl } = $props();
24
25
 
25
26
  let hydrated = false;
26
27
  let navContainer: HTMLDivElement;
@@ -75,22 +76,17 @@
75
76
  const handleLayoutTransition = (event: Event) => {
76
77
  const customEvent = event as CustomEvent;
77
78
  const { isMobile } = customEvent.detail;
78
- console.log('[DesktopSideNav] Layout transition detected:', { isMobile });
79
79
 
80
80
  if (isMobile) {
81
81
  // Switching to mobile: close navigation
82
- console.log('[DesktopSideNav] Closing navigation for mobile');
83
82
  navigationStore.setExpanded(false);
84
83
  } else {
85
84
  // Switching to desktop: open navigation if on non-discover page
86
85
  const currentItem = $selectedItem;
87
- console.log('[DesktopSideNav] Desktop transition - current item:', currentItem);
88
86
 
89
87
  if (currentItem && currentItem !== 'discover') {
90
- console.log('[DesktopSideNav] Opening navigation for desktop (non-discover page)');
91
88
  navigationStore.setExpanded(true);
92
89
  } else {
93
- console.log('[DesktopSideNav] Keeping navigation closed for desktop (discover page or no selection)');
94
90
  navigationStore.setExpanded(false);
95
91
  }
96
92
  }
@@ -110,13 +106,10 @@
110
106
  onNavToggle?.({ expanded: $isExpanded });
111
107
  }
112
108
 
113
- function handleNavItemClick(item: NavigationItem) {
114
- console.log('[DesktopSideNav] handleNavItemClick called for:', item, 'at:', Date.now());
115
-
109
+ function handleNavItemClick(item: NavigationItem) {
116
110
  // Set navigation flag immediately to prevent header restoration
117
111
  if (item !== 'discover' && typeof window !== 'undefined') {
118
112
  sessionStorage.setItem('navigatingToMainNav', 'true');
119
- console.log('[DesktopSideNav] Set navigation flag for:', item, 'at:', Date.now());
120
113
  }
121
114
 
122
115
  navigationStore.setSelectedItem(item);
@@ -139,7 +132,6 @@
139
132
  const firstSection = mainContent.querySelector('h2[id], h3[id], h4[id], h5[id], h6[id]');
140
133
  if (firstSection && firstSection.textContent) {
141
134
  const firstSectionTitle = firstSection.textContent.trim();
142
- console.log('[DesktopSideNav] Setting default header for', item, ':', firstSectionTitle);
143
135
  selectedSubNavItemTitle.set(firstSectionTitle);
144
136
  if (typeof window !== 'undefined') {
145
137
  sessionStorage.setItem('subNav-selectedTitle', JSON.stringify(firstSectionTitle));
@@ -185,9 +177,10 @@
185
177
  class="side-nav-container"
186
178
  class:expanded={sideNavExpanded()}
187
179
  bind:this={navContainer}
180
+ part="side-nav-container"
188
181
  >
189
182
  <div class="nav-content">
190
- <div class="nav-main">
183
+ <div class="nav-main" part="nav-main">
191
184
  <div class="menu-section">
192
185
  <button
193
186
  class="menu-button"
@@ -196,33 +189,35 @@
196
189
  onclick={toggleNav}
197
190
  onmouseenter={() => (menuButtonHovered = true)}
198
191
  onmouseleave={() => (menuButtonHovered = false)}
199
- aria-label="Open navigation menu"
192
+ onfocus={() => (menuButtonHovered = true)}
193
+ onblur={() => (menuButtonHovered = false)}
194
+ aria-label={sideNavExpanded() ? 'Close navigation menu' : 'Open navigation menu'}
195
+ aria-expanded={sideNavExpanded() ? 'true' : 'false'}
200
196
  >
201
- <span class="sr-only">Open navigation menu</span>
202
197
  <div class="icon-container">
203
198
  <div class="icon-wrapper">
204
199
  <MenuIcon
205
- isHovered={menuBtnHovered()}
206
- isDisabled={menuBtnDisabled()}
207
- isOpen={menuBtnOpen()}
208
- />
200
+ isHovered={menuBtnHovered()}
201
+ isDisabled={menuBtnDisabled()}
202
+ isOpen={menuBtnOpen()}
203
+ />
209
204
  </div>
210
205
  </div>
211
206
  </button>
212
207
  </div>
213
208
 
214
- <div class="nav-items">
209
+ <div class="nav-items" part="nav-items">
215
210
  <a
216
- href="/discover"
211
+ href={discoverUrl || urls.discover}
217
212
  class="nav-item discover-item"
218
213
  class:selected={isNavSelected('discover')}
219
214
  class:hovered={isNavHovered('discover')}
220
- role="button"
221
- tabindex="0"
222
215
  onclick={() => handleNavItemClick('discover')}
223
216
  onkeydown={e => handleKeyDown(e, 'discover')}
224
217
  onmouseenter={() => (hoveredItem = 'discover')}
225
218
  onmouseleave={() => (hoveredItem = null)}
219
+ aria-current={isNavSelected('discover') ? 'page' : undefined}
220
+ part="discover-item"
226
221
  >
227
222
  <div class="icon-container">
228
223
  <div class="icon-wrapper">
@@ -232,16 +227,16 @@
232
227
  <span class="nav-text">Discover</span>
233
228
  </a>
234
229
  <a
235
- href="/#chat"
230
+ href={chatUrl || urls.chat}
236
231
  class="nav-item chat-item"
237
232
  class:selected={isNavSelected('chat')}
238
233
  class:hovered={isNavHovered('chat')}
239
- role="button"
240
- tabindex="0"
241
234
  onclick={() => handleNavItemClick('chat')}
242
235
  onkeydown={e => handleKeyDown(e, 'chat')}
243
236
  onmouseenter={() => (hoveredItem = 'chat')}
244
237
  onmouseleave={() => (hoveredItem = null)}
238
+ aria-current={isNavSelected('chat') ? 'page' : undefined}
239
+ part="chat-item"
245
240
  >
246
241
  <div class="icon-container">
247
242
  <div class="icon-wrapper">
@@ -252,16 +247,16 @@
252
247
  </a>
253
248
 
254
249
  <a
255
- href="/"
250
+ href={consoleUrl || urls.console}
256
251
  class="nav-item console-item"
257
252
  class:selected={isNavSelected('console')}
258
253
  class:hovered={isNavHovered('console')}
259
- role="button"
260
- tabindex="0"
261
254
  onclick={() => handleNavItemClick('console')}
262
255
  onkeydown={e => handleKeyDown(e, 'console')}
263
256
  onmouseenter={() => (hoveredItem = 'console')}
264
257
  onmouseleave={() => (hoveredItem = null)}
258
+ aria-current={isNavSelected('console') ? 'page' : undefined}
259
+ part="console-item"
265
260
  >
266
261
  <div class="icon-container">
267
262
  <div class="icon-wrapper">
@@ -272,16 +267,16 @@
272
267
  </a>
273
268
 
274
269
  <a
275
- href="/api"
270
+ href={apiUrl || urls.api}
276
271
  class="nav-item api-item"
277
272
  class:selected={isNavSelected('api')}
278
273
  class:hovered={isNavHovered('api')}
279
- role="button"
280
- tabindex="0"
281
274
  onclick={() => handleNavItemClick('api')}
282
275
  onkeydown={e => handleKeyDown(e, 'api')}
283
276
  onmouseenter={() => (hoveredItem = 'api')}
284
277
  onmouseleave={() => (hoveredItem = null)}
278
+ aria-current={isNavSelected('api') ? 'page' : undefined}
279
+ part="api-item"
285
280
  >
286
281
  <div class="icon-container">
287
282
  <div class="icon-wrapper">
@@ -293,7 +288,7 @@
293
288
  </div>
294
289
  </div>
295
290
 
296
- <div class="nav-footer">
291
+ <div class="nav-footer" part="nav-footer">
297
292
  <button
298
293
  class="user-profile-button"
299
294
  tabindex="0"
@@ -306,7 +301,7 @@
306
301
  }}
307
302
  >
308
303
  <div class="user-profile-container">
309
- <div class="user-profile-icon">
304
+ <div class="user-profile-icon" part="user-profile-icon">
310
305
  <span class="user-initials">ZW</span>
311
306
  </div>
312
307
  </div>
@@ -314,62 +309,327 @@
314
309
  </div>
315
310
  </div>
316
311
 
317
- <div class="expanded-content drawer-anim" class:open={sideNavExpanded()}>
318
- <div class="drawer-content" class:visible={sideNavExpanded()}>
319
- <div class="logo-container">
320
- <div class="logo-wrapper">
321
- <div class="logo-inner">
322
- <div class="logo-component padding-left-2">
323
- <Logo width={75} height={22} />
324
- </div>
325
- </div>
326
- </div>
312
+ <div class="expanded-content drawer-anim" class:open={sideNavExpanded()} part="expanded-content">
313
+ {#if sideNavExpanded()}
314
+ <div class="drawer-content" part="drawer-content">
315
+ <div class="logo-placeholder"></div>
316
+ <nav aria-label="Sub navigation">
317
+ {@render children?.()}
318
+ </nav>
327
319
  </div>
328
- <nav aria-label="Sub navigation">
329
- {@render children?.()}
330
- </nav>
331
- </div>
320
+ {/if}
332
321
  </div>
333
322
  </div>
334
323
 
335
324
  <style>
336
325
  .expanded-content.drawer-anim {
337
- width: 0;
338
- min-width: 0;
339
326
  max-width: 0;
340
327
  overflow: hidden;
341
- opacity: 0.7;
342
328
  pointer-events: none;
343
- transition:
344
- width 0.4s cubic-bezier(.4,0,.2,1),
345
- min-width 0.4s cubic-bezier(.4,0,.2,1),
346
- max-width 0.4s cubic-bezier(.4,0,.2,1),
347
- opacity 0.2s;
329
+ transition:
330
+ max-width 0.2s cubic-bezier(.4, 0, .2, 1);
348
331
  }
332
+
349
333
  .expanded-content.drawer-anim.open {
350
- width: 12.5rem;
351
- min-width: 12.5rem;
352
334
  max-width: 12.5rem;
353
- opacity: 1;
354
335
  pointer-events: auto;
355
336
  }
337
+
356
338
  .drawer-content {
339
+ width: 12.5rem;
340
+ height: 100%;
341
+ }
342
+
343
+ .logo-placeholder {
344
+ height: var(--ai-size-72);
345
+ }
346
+
347
+ .user-profile-container,
348
+ .user-profile-icon {
349
+ display: flex;
350
+ align-items: center;
351
+ justify-content: center;
352
+ }
353
+
354
+ .menu-button {
355
+ display: flex;
356
+ align-items: center;
357
+ justify-content: center;
358
+ border: none;
359
+ background: none;
360
+ cursor: pointer;
361
+ border-radius: var(--ai-size-8);
362
+ transition: background-color 0.2s ease;
363
+ }
364
+
365
+ .menu-button:disabled,
366
+ .menu-button.disabled {
367
+ cursor: not-allowed;
368
+ opacity: 0.5;
369
+ background-color: transparent;
370
+ }
371
+
372
+ .user-profile-icon {
373
+ border-radius: var(--ai-size-6);
374
+ background: var(--ai-color-steel-700);
375
+ }
376
+
377
+ .user-initials {
378
+ color: var(--ai-color-white);
379
+ text-align: center;
380
+ font-weight: 400;
381
+ line-height: 1.3;
382
+ }
383
+
384
+ .side-nav-container {
385
+ display: flex;
386
+ flex-shrink: 0;
387
+ position: relative;
388
+ z-index: var(--ai-layer-2);
389
+ height: 100%;
390
+ }
391
+
392
+ .side-nav-container.expanded {
393
+ width: 16.875rem;
394
+ min-width: 12.5rem;
395
+ max-width: 16.875rem;
396
+ height: 100%;
397
+ }
398
+
399
+ .expanded-content {
400
+ display: flex;
401
+ width: 12.5rem;
402
+ opacity: 1;
403
+ flex: none;
404
+ height: 100%;
405
+ background: var(--ai-color-steel-100);
406
+ border-right: 0.0625rem solid var(--ai-color-steel-100);
407
+ overflow: hidden;
408
+ flex-direction: column;
409
+ }
410
+
411
+ .side-nav-container.expanded .expanded-content {
412
+ width: 12.5rem;
413
+ opacity: 1;
414
+ overflow-y: auto;
415
+ }
416
+
417
+ .side-nav-container:not(.expanded) .expanded-content {
418
+ width: 0;
357
419
  opacity: 0;
358
- transform: translateX(-10px);
359
- transition:
360
- opacity 0.4s 0.20s cubic-bezier(.4,0,.2,1),
361
- transform 0.4s 0.20s cubic-bezier(.4,0,.2,1);
362
420
  }
363
- .drawer-content.visible {
421
+
422
+ .nav-content {
423
+ display: flex;
424
+ width: var(--ai-size-72);
425
+ height: 100%;
426
+ padding-bottom: var(--ai-size-16);
427
+ flex-direction: column;
428
+ align-items: flex-start;
429
+ gap: var(--ai-size-4);
430
+ flex: none;
431
+ background: var(--ai-color-steel-200);
432
+ border-right: 0.0625rem solid var(--ai-color-steel-200);
433
+ }
434
+
435
+ .nav-main {
436
+ display: flex;
437
+ flex-direction: column;
438
+ align-items: flex-start;
439
+ gap: var(--ai-size-4);
440
+ flex: 1 0 0;
441
+ align-self: stretch;
442
+ }
443
+
444
+ .nav-footer {
445
+ display: flex;
446
+ padding: var(--ai-size-8) var(--ai-size-4);
447
+ flex-direction: column;
448
+ justify-content: center;
449
+ align-items: center;
450
+ gap: var(--ai-size-4);
451
+ align-self: stretch;
452
+ margin-top: auto;
453
+ }
454
+
455
+ .user-profile-button {
456
+ background: none;
457
+ border: none;
458
+ padding: 0;
459
+ cursor: pointer;
460
+ display: flex;
461
+ justify-content: center;
462
+ align-items: center;
463
+ }
464
+
465
+ .user-profile-button:focus {
466
+ outline: var(--ai-size-2) solid var(--ai-color-blue-600);
467
+ outline-offset: var(--ai-size-2);
468
+ border-radius: var(--ai-size-4);
469
+ }
470
+
471
+ .user-profile-container {
472
+ width: var(--ai-size-40);
473
+ height: var(--ai-size-40);
474
+ aspect-ratio: 1/1;
475
+ }
476
+
477
+ .user-profile-icon {
478
+ padding: 0.5556rem 0.4931rem 0.5694rem 0.3819rem;
479
+ }
480
+
481
+ .user-initials {
482
+ font-size: var(--ai-size-16);
483
+ font-style: normal;
484
+ }
485
+
486
+ .menu-section {
487
+ display: flex;
488
+ padding: var(--ai-size-4);
489
+ justify-content: center;
490
+ align-items: center;
491
+ align-self: stretch;
492
+ }
493
+
494
+ .menu-button {
495
+ display: flex;
496
+ padding: var(--ai-size-4);
497
+ justify-content: center;
498
+ align-items: center;
499
+ background: none;
500
+ border: none;
501
+ cursor: pointer;
364
502
  opacity: 1;
365
- transform: translateX(0);
366
- transition:
367
- opacity 0.4s 0.20s cubic-bezier(.4,0,.2,1),
368
- transform 0.4s 0.20s cubic-bezier(.4,0,.2,1);
503
+ transition: opacity 0.2s ease;
504
+ will-change: opacity;
505
+ }
506
+
507
+ .menu-button.disabled {
508
+ cursor: not-allowed;
509
+ opacity: 0.5;
510
+ will-change: opacity;
511
+ }
512
+
513
+ .menu-button:focus,
514
+ .nav-item:focus {
515
+ outline: none;
516
+ }
517
+
518
+ .menu-button:focus .icon-container,
519
+ .nav-item:focus .icon-container {
520
+ outline: var(--ai-size-2) solid var(--ai-color-blue-600);
521
+ outline-offset: var(--ai-size-2);
522
+ border-radius: var(--ai-size-4);
523
+ }
524
+
525
+ .nav-items {
526
+ display: flex;
527
+ width: var(--ai-size-72);
528
+ flex-direction: column;
529
+ align-items: flex-start;
530
+ gap: var(--ai-size-4);
531
+ }
532
+
533
+ .nav-item {
534
+ display: flex;
535
+ height: 4.125rem;
536
+ padding: var(--ai-size-4);
537
+ flex-direction: column;
538
+ justify-content: center;
539
+ align-items: center;
540
+ gap: var(--ai-size-2);
541
+ align-self: stretch;
542
+ cursor: pointer;
543
+ text-decoration: none;
544
+ color: inherit;
545
+ position: relative;
546
+ flex-shrink: 0;
547
+ }
548
+
549
+ .icon-container {
550
+ display: flex;
551
+ width: var(--ai-size-40);
552
+ height: var(--ai-size-40);
553
+ padding: var(--ai-size-8);
554
+ justify-content: center;
555
+ align-items: center;
556
+ flex-shrink: 0;
557
+ position: relative;
558
+ contain: layout style;
559
+ transition: background-color 0.3s ease,
560
+ border-radius 0.3s ease;
561
+ }
562
+
563
+ .icon-wrapper {
564
+ display: flex;
565
+ width: var(--ai-size-28);
566
+ height: var(--ai-size-28);
567
+ justify-content: center;
568
+ align-items: center;
569
+ flex-shrink: 0;
570
+ aspect-ratio: 1/1;
571
+ position: relative;
572
+ overflow: hidden;
573
+ transition: transform 0.3s ease;
574
+ }
575
+
576
+ .icon-wrapper :global(svg) {
577
+ width: var(--ai-size-24);
578
+ height: var(--ai-size-24);
579
+ flex-shrink: 0;
580
+ position: absolute;
581
+ top: 50%;
582
+ left: 50%;
583
+ transform: translate(-50%, -50%);
584
+ transition: all 0.3s ease;
585
+ }
586
+
587
+ .icon-wrapper :global(svg path) {
588
+ stroke-width: 1.5;
589
+ vector-effect: non-scaling-stroke;
590
+ transition: stroke 0.3s ease,
591
+ fill 0.3s ease,
592
+ stroke-width 0.3s ease;
593
+ }
594
+
595
+ .nav-text {
596
+ font-size: var(--ai-size-12);
597
+ text-align: center;
598
+ color: var(--ai-color-neutral-900);
599
+ line-height: 1.3;
369
600
  }
370
- .expanded-content.drawer-anim:not(.open) .drawer-content {
371
- transition:
372
- opacity 0.20s cubic-bezier(.4,0,.2,1),
373
- transform 0.20s cubic-bezier(.4,0,.2,1);
601
+
602
+ .menu-button.disabled:hover .icon-container,
603
+ .menu-button:disabled:hover .icon-container {
604
+ background: none !important;
605
+ border-radius: var(--ai-size-4);
606
+ }
607
+
608
+ .nav-item.hovered .icon-container,
609
+ .nav-item:active .icon-container,
610
+ .nav-item.selected .icon-container,
611
+ .nav-item.selected.hovered .icon-container,
612
+ .menu-button:hover .icon-container,
613
+ .menu-button:active .icon-container {
614
+ background-color: var(--ai-color-steel-300);
615
+ border-radius: var(--ai-size-4);
374
616
  }
617
+
618
+ .nav-item {
619
+ flex: 1;
620
+ display: flex;
621
+ flex-direction: column;
622
+ align-items: center;
623
+ justify-content: center;
624
+ padding: var(--ai-size-4);
625
+ text-decoration: none;
626
+ color: var(--ai-color-neutral-600);
627
+ transition: color 0.2s ease;
628
+ height: 100%;
629
+ }
630
+
631
+ .nav-item:hover {
632
+ color: var(--ai-color-neutral-700);
633
+ }
634
+
375
635
  </style>
@@ -0,0 +1,32 @@
1
+ <svelte:options customElement={{
2
+ tag: 'graymatter-desktop-side-nav'
3
+ }} />
4
+
5
+ <script lang="ts">
6
+ import DesktopSideNav from './DesktopSideNav.svelte';
7
+
8
+ // Props for the web component
9
+ let {
10
+ ssrSelectedItem,
11
+ onNavToggle,
12
+ onNavItemClick,
13
+ onProfileClick,
14
+ apiUrl,
15
+ chatUrl,
16
+ consoleUrl,
17
+ discoverUrl
18
+ } = $props();
19
+ </script>
20
+
21
+ <DesktopSideNav
22
+ {ssrSelectedItem}
23
+ {onNavToggle}
24
+ {onNavItemClick}
25
+ {onProfileClick}
26
+ {apiUrl}
27
+ {chatUrl}
28
+ {consoleUrl}
29
+ {discoverUrl}
30
+ >
31
+ <slot />
32
+ </DesktopSideNav>
@@ -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
+ />