@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.
@@ -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,6 +18,8 @@
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
24
  const { ssrSelectedItem = undefined, onNavToggle = undefined, onNavItemClick = undefined, onProfileClick = undefined, children } = $props();
24
25
 
@@ -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"
@@ -211,9 +204,9 @@
211
204
  </button>
212
205
  </div>
213
206
 
214
- <div class="nav-items">
207
+ <div class="nav-items" part="nav-items">
215
208
  <a
216
- href="/discover"
209
+ href={urls.discover}
217
210
  class="nav-item discover-item"
218
211
  class:selected={isNavSelected('discover')}
219
212
  class:hovered={isNavHovered('discover')}
@@ -232,7 +225,7 @@
232
225
  <span class="nav-text">Discover</span>
233
226
  </a>
234
227
  <a
235
- href="/#chat"
228
+ href={urls.chat}
236
229
  class="nav-item chat-item"
237
230
  class:selected={isNavSelected('chat')}
238
231
  class:hovered={isNavHovered('chat')}
@@ -252,7 +245,7 @@
252
245
  </a>
253
246
 
254
247
  <a
255
- href="/"
248
+ href={urls.console}
256
249
  class="nav-item console-item"
257
250
  class:selected={isNavSelected('console')}
258
251
  class:hovered={isNavHovered('console')}
@@ -272,7 +265,7 @@
272
265
  </a>
273
266
 
274
267
  <a
275
- href="/api"
268
+ href={urls.api}
276
269
  class="nav-item api-item"
277
270
  class:selected={isNavSelected('api')}
278
271
  class:hovered={isNavHovered('api')}
@@ -293,7 +286,7 @@
293
286
  </div>
294
287
  </div>
295
288
 
296
- <div class="nav-footer">
289
+ <div class="nav-footer" part="nav-footer">
297
290
  <button
298
291
  class="user-profile-button"
299
292
  tabindex="0"
@@ -306,7 +299,7 @@
306
299
  }}
307
300
  >
308
301
  <div class="user-profile-container">
309
- <div class="user-profile-icon">
302
+ <div class="user-profile-icon" part="user-profile-icon">
310
303
  <span class="user-initials">ZW</span>
311
304
  </div>
312
305
  </div>
@@ -314,62 +307,339 @@
314
307
  </div>
315
308
  </div>
316
309
 
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>
310
+ <div class="expanded-content drawer-anim" class:open={sideNavExpanded()} part="expanded-content">
311
+ {#if sideNavExpanded()}
312
+ <div class="drawer-content" part="drawer-content">
313
+ <div class="logo-placeholder"></div>
314
+ <nav aria-label="Sub navigation">
315
+ {@render children?.()}
316
+ </nav>
327
317
  </div>
328
- <nav aria-label="Sub navigation">
329
- {@render children?.()}
330
- </nav>
331
- </div>
318
+ {/if}
332
319
  </div>
333
320
  </div>
334
321
 
335
322
  <style>
336
323
  .expanded-content.drawer-anim {
337
- width: 0;
338
- min-width: 0;
339
324
  max-width: 0;
340
325
  overflow: hidden;
341
- opacity: 0.7;
342
326
  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;
327
+ transition:
328
+ max-width 0.2s cubic-bezier(.4, 0, .2, 1);
348
329
  }
330
+
349
331
  .expanded-content.drawer-anim.open {
350
- width: 12.5rem;
351
- min-width: 12.5rem;
352
332
  max-width: 12.5rem;
353
- opacity: 1;
354
333
  pointer-events: auto;
355
334
  }
335
+
356
336
  .drawer-content {
337
+ width: 12.5rem;
338
+ height: 100%;
339
+ }
340
+
341
+ .logo-placeholder {
342
+ height: var(--ai-size-72);
343
+ }
344
+
345
+ .sr-only {
346
+ position: absolute;
347
+ width: 1px;
348
+ height: 1px;
349
+ padding: 0;
350
+ margin: -1px;
351
+ overflow: hidden;
352
+ clip: rect(0, 0, 0, 0);
353
+ border: 0;
354
+ white-space: nowrap;
355
+ }
356
+
357
+ .user-profile-container,
358
+ .user-profile-icon {
359
+ display: flex;
360
+ align-items: center;
361
+ justify-content: center;
362
+ }
363
+
364
+ .menu-button {
365
+ display: flex;
366
+ align-items: center;
367
+ justify-content: center;
368
+ border: none;
369
+ background: none;
370
+ cursor: pointer;
371
+ border-radius: var(--ai-size-8);
372
+ transition: background-color 0.2s ease;
373
+ }
374
+
375
+ .menu-button:disabled,
376
+ .menu-button.disabled {
377
+ cursor: not-allowed;
378
+ opacity: 0.5;
379
+ background-color: transparent;
380
+ }
381
+
382
+ .user-profile-icon {
383
+ border-radius: var(--ai-size-6);
384
+ background: var(--ai-color-steel-700);
385
+ }
386
+
387
+ .user-initials {
388
+ color: var(--ai-color-white);
389
+ text-align: center;
390
+ font-weight: 400;
391
+ line-height: 1.3;
392
+ }
393
+
394
+ .side-nav-container {
395
+ display: flex;
396
+ flex-shrink: 0;
397
+ position: relative;
398
+ z-index: var(--ai-layer-2);
399
+ height: 100%;
400
+ }
401
+
402
+ .side-nav-container.expanded {
403
+ width: 16.875rem;
404
+ min-width: 12.5rem;
405
+ max-width: 16.875rem;
406
+ height: 100%;
407
+ }
408
+
409
+ .expanded-content {
410
+ display: flex;
411
+ width: 12.5rem;
412
+ opacity: 1;
413
+ flex: none;
414
+ height: 100%;
415
+ background: var(--ai-color-steel-100);
416
+ border-right: 0.0625rem solid var(--ai-color-steel-100);
417
+ overflow: hidden;
418
+ flex-direction: column;
419
+ }
420
+
421
+ .side-nav-container.expanded .expanded-content {
422
+ width: 12.5rem;
423
+ opacity: 1;
424
+ overflow-y: auto;
425
+ }
426
+
427
+ .side-nav-container:not(.expanded) .expanded-content {
428
+ width: 0;
357
429
  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
430
  }
363
- .drawer-content.visible {
431
+
432
+ .nav-content {
433
+ display: flex;
434
+ width: var(--ai-size-72);
435
+ height: 100%;
436
+ padding-bottom: var(--ai-size-16);
437
+ flex-direction: column;
438
+ align-items: flex-start;
439
+ gap: var(--ai-size-4);
440
+ flex: none;
441
+ background: var(--ai-color-steel-200);
442
+ border-right: 0.0625rem solid var(--ai-color-steel-200);
443
+ }
444
+
445
+ .nav-main {
446
+ display: flex;
447
+ flex-direction: column;
448
+ align-items: flex-start;
449
+ gap: var(--ai-size-4);
450
+ flex: 1 0 0;
451
+ align-self: stretch;
452
+ }
453
+
454
+ .nav-footer {
455
+ display: flex;
456
+ padding: var(--ai-size-8) var(--ai-size-4);
457
+ flex-direction: column;
458
+ justify-content: center;
459
+ align-items: center;
460
+ gap: var(--ai-size-4);
461
+ align-self: stretch;
462
+ margin-top: auto;
463
+ }
464
+
465
+ .user-profile-button {
466
+ background: none;
467
+ border: none;
468
+ padding: 0;
469
+ cursor: pointer;
470
+ display: flex;
471
+ justify-content: center;
472
+ align-items: center;
473
+ }
474
+
475
+ .user-profile-button:focus {
476
+ outline: var(--ai-size-2) solid var(--ai-color-blue-600);
477
+ outline-offset: var(--ai-size-2);
478
+ border-radius: var(--ai-size-4);
479
+ }
480
+
481
+ .user-profile-container {
482
+ width: var(--ai-size-40);
483
+ height: var(--ai-size-40);
484
+ aspect-ratio: 1/1;
485
+ }
486
+
487
+ .user-profile-icon {
488
+ padding: 0.5556rem 0.4931rem 0.5694rem 0.3819rem;
489
+ }
490
+
491
+ .user-initials {
492
+ font-size: var(--ai-size-16);
493
+ font-style: normal;
494
+ }
495
+
496
+ .menu-section {
497
+ display: flex;
498
+ padding: var(--ai-size-4);
499
+ justify-content: center;
500
+ align-items: center;
501
+ align-self: stretch;
502
+ }
503
+
504
+ .menu-button {
505
+ display: flex;
506
+ padding: var(--ai-size-4);
507
+ justify-content: center;
508
+ align-items: center;
509
+ background: none;
510
+ border: none;
511
+ cursor: pointer;
364
512
  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);
513
+ transition: opacity 0.2s ease;
514
+ will-change: opacity;
515
+ }
516
+
517
+ .menu-button.disabled {
518
+ cursor: not-allowed;
519
+ opacity: 0.5;
520
+ will-change: opacity;
521
+ }
522
+
523
+ .menu-button:focus,
524
+ .nav-item:focus {
525
+ outline: none;
526
+ }
527
+
528
+ .menu-button:focus .icon-container,
529
+ .nav-item:focus .icon-container {
530
+ outline: var(--ai-size-2) solid var(--ai-color-blue-600);
531
+ outline-offset: var(--ai-size-2);
532
+ border-radius: var(--ai-size-4);
369
533
  }
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);
534
+
535
+ .nav-items {
536
+ display: flex;
537
+ width: var(--ai-size-72);
538
+ flex-direction: column;
539
+ align-items: flex-start;
540
+ gap: var(--ai-size-4);
541
+ }
542
+
543
+ .nav-item {
544
+ display: flex;
545
+ height: 4.125rem;
546
+ padding: var(--ai-size-4);
547
+ flex-direction: column;
548
+ justify-content: center;
549
+ align-items: center;
550
+ gap: var(--ai-size-2);
551
+ align-self: stretch;
552
+ cursor: pointer;
553
+ text-decoration: none;
554
+ color: inherit;
555
+ position: relative;
556
+ flex-shrink: 0;
557
+ }
558
+
559
+ .icon-container {
560
+ display: flex;
561
+ width: var(--ai-size-40);
562
+ height: var(--ai-size-40);
563
+ padding: var(--ai-size-8);
564
+ justify-content: center;
565
+ align-items: center;
566
+ flex-shrink: 0;
567
+ position: relative;
568
+ contain: layout style;
569
+ transition: background-color 0.3s ease,
570
+ border-radius 0.3s ease;
571
+ }
572
+
573
+ .icon-wrapper {
574
+ display: flex;
575
+ width: var(--ai-size-28);
576
+ height: var(--ai-size-28);
577
+ justify-content: center;
578
+ align-items: center;
579
+ flex-shrink: 0;
580
+ aspect-ratio: 1/1;
581
+ position: relative;
582
+ overflow: hidden;
583
+ transition: transform 0.3s ease;
584
+ }
585
+
586
+ .icon-wrapper :global(svg) {
587
+ width: var(--ai-size-24);
588
+ height: var(--ai-size-24);
589
+ flex-shrink: 0;
590
+ position: absolute;
591
+ top: 50%;
592
+ left: 50%;
593
+ transform: translate(-50%, -50%);
594
+ transition: all 0.3s ease;
595
+ }
596
+
597
+ .icon-wrapper :global(svg path) {
598
+ stroke-width: 1.5;
599
+ vector-effect: non-scaling-stroke;
600
+ transition: stroke 0.3s ease,
601
+ fill 0.3s ease,
602
+ stroke-width 0.3s ease;
374
603
  }
604
+
605
+ .nav-text {
606
+ font-size: var(--ai-size-12);
607
+ text-align: center;
608
+ color: var(--ai-color-neutral-900);
609
+ line-height: 1.3;
610
+ }
611
+
612
+ .menu-button.disabled:hover .icon-container,
613
+ .menu-button:disabled:hover .icon-container {
614
+ background: none !important;
615
+ border-radius: var(--ai-size-4);
616
+ }
617
+
618
+ .nav-item.hovered .icon-container,
619
+ .nav-item:active .icon-container,
620
+ .nav-item.selected .icon-container,
621
+ .nav-item.selected.hovered .icon-container,
622
+ .menu-button:hover .icon-container,
623
+ .menu-button:active .icon-container {
624
+ background-color: var(--ai-color-steel-300);
625
+ border-radius: var(--ai-size-4);
626
+ }
627
+
628
+ .nav-item {
629
+ flex: 1;
630
+ display: flex;
631
+ flex-direction: column;
632
+ align-items: center;
633
+ justify-content: center;
634
+ padding: var(--ai-size-4);
635
+ text-decoration: none;
636
+ color: var(--ai-color-neutral-600);
637
+ transition: color 0.2s ease;
638
+ height: 100%;
639
+ }
640
+
641
+ .nav-item:hover {
642
+ color: var(--ai-color-neutral-700);
643
+ }
644
+
375
645
  </style>
@@ -0,0 +1,19 @@
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 { ssrSelectedItem, onNavToggle, onNavItemClick, onProfileClick } = $props();
10
+ </script>
11
+
12
+ <DesktopSideNav
13
+ {ssrSelectedItem}
14
+ {onNavToggle}
15
+ {onNavItemClick}
16
+ {onProfileClick}
17
+ >
18
+ <slot />
19
+ </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
+ />
@@ -5,23 +5,23 @@
5
5
  import ChatIcon from './icons/ChatIcon.svelte';
6
6
  import ConsoleIcon from './icons/ConsoleIcon.svelte';
7
7
  import ApiIcon from './icons/ApiIcon.svelte';
8
+ import { getAppUrls } from '../utils/getAppUrls';
8
9
 
9
10
  export let onNavItemClick: ((data: { item: NavigationItem; expanded: boolean }) => void) | undefined = undefined;
10
11
 
12
+ const urls = getAppUrls();
13
+
11
14
  const navItems = [
12
- { id: 'discover', label: 'Discover', icon: DiscoverIcon, href: '/discover' },
13
- { id: 'chat', label: 'Chat', icon: ChatIcon, href: '/' },
14
- { id: 'console', label: 'Console', icon: ConsoleIcon, href: '/' },
15
- { id: 'api', label: 'API', icon: ApiIcon, href: '/api' }
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 }
16
19
  ] as const;
17
20
 
18
- function handleNavItemClick(item: NavigationItem) {
19
- console.log('[MobileBottomNav] handleNavItemClick called for:', item, 'at:', Date.now());
20
-
21
+ function handleNavItemClick(item: NavigationItem) {
21
22
  // Set navigation flag immediately to prevent header restoration
22
23
  if (item !== 'discover' && typeof window !== 'undefined') {
23
24
  sessionStorage.setItem('navigatingToMainNav', 'true');
24
- console.log('[MobileBottomNav] Set navigation flag for:', item, 'at:', Date.now());
25
25
  }
26
26
 
27
27
  navigationStore.setSelectedItem(item);
@@ -44,7 +44,6 @@
44
44
  const firstSection = mainContent.querySelector('h2[id], h3[id], h4[id], h5[id], h6[id]');
45
45
  if (firstSection && firstSection.textContent) {
46
46
  const firstSectionTitle = firstSection.textContent.trim();
47
- console.log('[MobileBottomNav] Setting default header for', item, ':', firstSectionTitle);
48
47
  selectedSubNavItemTitle.set(firstSectionTitle);
49
48
  if (typeof window !== 'undefined') {
50
49
  sessionStorage.setItem('subNav-selectedTitle', JSON.stringify(firstSectionTitle));
@@ -79,21 +78,68 @@
79
78
  }
80
79
  </script>
81
80
 
82
- <nav class="mobile-bottom-nav">
81
+ <nav class="mobile-bottom-nav" part="mobile-bottom-nav">
83
82
  {#each navItems as navItem}
84
83
  <a
85
84
  href={navItem.href}
86
85
  class="nav-item"
87
86
  class:active={$selectedItem === navItem.id}
88
87
  on:click={() => handleNavItemClick(navItem.id)}
88
+ part="nav-item"
89
89
  >
90
90
  <div class="icon">
91
91
  <svelte:component
92
92
  this={navItem.icon}
93
- isActive={$selectedItem === navItem.id}
93
+ isSelected={$selectedItem === navItem.id}
94
94
  />
95
95
  </div>
96
96
  <span class="label">{navItem.label}</span>
97
97
  </a>
98
98
  {/each}
99
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
+ />