@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,153 @@
1
+ // Store for selected subnav item id and title (for all apps)
2
+ import { writable, type Writable, derived } from 'svelte/store';
3
+
4
+ export type NavigationItem = 'discover' | 'chat' | 'console' | 'api' | null;
5
+
6
+ const browser = typeof window !== 'undefined';
7
+
8
+ // Persistent subnav item id and title in sessionStorage
9
+ function getSessionValue<T>(key: string, defaultValue: T): T {
10
+ if (browser && typeof sessionStorage !== 'undefined') {
11
+ const stored = sessionStorage.getItem(key);
12
+ if (stored !== null) {
13
+ try {
14
+ return JSON.parse(stored) as T;
15
+ } catch (e) {
16
+ console.warn(`Failed to parse stored value for ${key}:`, e);
17
+ return defaultValue;
18
+ }
19
+ }
20
+ }
21
+ return defaultValue;
22
+ }
23
+
24
+ function createSessionStore<T>(key: string, defaultValue: T): Writable<T> {
25
+ const initialValue = getSessionValue(key, defaultValue);
26
+ const store = writable<T>(initialValue);
27
+
28
+ if (browser) {
29
+ store.subscribe((value: T) => {
30
+ try {
31
+ sessionStorage.setItem(key, JSON.stringify(value));
32
+ } catch (e) {
33
+ console.warn(`Failed to persist ${key} to sessionStorage:`, e);
34
+ }
35
+ });
36
+ }
37
+
38
+ return store;
39
+ }
40
+
41
+ export const selectedSubNavItemId: Writable<string | null> = createSessionStore(
42
+ 'subNav-selectedId',
43
+ null
44
+ );
45
+ export const selectedSubNavItemTitle: Writable<string | null> =
46
+ createSessionStore('subNav-selectedTitle', '');
47
+
48
+ function getInitialValue<T>(key: string, defaultValue: T): T {
49
+ if (browser && typeof localStorage !== 'undefined') {
50
+ const stored = localStorage.getItem(key);
51
+ if (stored !== null) {
52
+ try {
53
+ return JSON.parse(stored) as T;
54
+ } catch (e) {
55
+ console.warn(`Failed to parse stored value for ${key}:`, e);
56
+ return defaultValue;
57
+ }
58
+ }
59
+ }
60
+ return defaultValue;
61
+ }
62
+
63
+ function createPersistentStore<T>(key: string, defaultValue: T): Writable<T> {
64
+ const initialValue = getInitialValue(key, defaultValue);
65
+ const store = writable<T>(initialValue);
66
+
67
+ if (browser) {
68
+ store.subscribe((value: T) => {
69
+ try {
70
+ localStorage.setItem(key, JSON.stringify(value));
71
+ } catch (e) {
72
+ console.warn(`Failed to persist ${key} to localStorage:`, e);
73
+ }
74
+ });
75
+ }
76
+
77
+ return store;
78
+ }
79
+
80
+ export const isExpanded: Writable<boolean> = createPersistentStore(
81
+ 'globalSideNav-isExpanded',
82
+ false
83
+ );
84
+ export const selectedItem: Writable<NavigationItem> = createPersistentStore(
85
+ 'globalSideNav-selectedItem',
86
+ null
87
+ );
88
+
89
+ // Derived store to determine if menu button should be disabled
90
+ export const isMenuButtonDisabled = derived(
91
+ selectedItem,
92
+ $selectedItem => $selectedItem === 'discover'
93
+ );
94
+
95
+ export interface NavigationStore {
96
+ toggleExpanded: () => void;
97
+ setExpanded: (value: boolean) => void;
98
+ setSelectedItem: (item: NavigationItem) => void;
99
+ handleLayoutTransition: (isMobile: boolean) => void;
100
+ }
101
+
102
+ export const navigationStore: NavigationStore = {
103
+ toggleExpanded: (): void => {
104
+ isExpanded.update((expanded: boolean) => !expanded);
105
+ },
106
+
107
+ setExpanded: (value: boolean): void => {
108
+ isExpanded.set(value);
109
+ },
110
+
111
+ setSelectedItem: (item: NavigationItem): void => {
112
+ selectedItem.set(item);
113
+ if (item === 'discover') {
114
+ isExpanded.set(false);
115
+ } else {
116
+ // For non-discover items (API, Console, Chat), ALWAYS expand on desktop
117
+ if (browser && window.innerWidth > 640) {
118
+ isExpanded.set(true);
119
+ }
120
+ }
121
+ },
122
+
123
+ handleLayoutTransition: (isMobile: boolean): void => {
124
+ console.log('[NavigationStore] handleLayoutTransition called:', {
125
+ isMobile,
126
+ });
127
+
128
+ if (isMobile) {
129
+ // Switching to mobile: always close navigation
130
+ console.log('[NavigationStore] Closing navigation for mobile');
131
+ isExpanded.set(false);
132
+ } else {
133
+ // Switching to desktop: open navigation if on non-discover page
134
+ const currentItem = getInitialValue('globalSideNav-selectedItem', null);
135
+ console.log(
136
+ '[NavigationStore] Desktop transition - current item:',
137
+ currentItem
138
+ );
139
+
140
+ if (currentItem && currentItem !== 'discover') {
141
+ console.log(
142
+ '[NavigationStore] Opening navigation for desktop (non-discover page)'
143
+ );
144
+ isExpanded.set(true);
145
+ } else {
146
+ console.log(
147
+ '[NavigationStore] Keeping navigation closed for desktop (discover page or no selection)'
148
+ );
149
+ isExpanded.set(false);
150
+ }
151
+ }
152
+ },
153
+ };
@@ -0,0 +1,3 @@
1
+ import { writable } from 'svelte/store';
2
+
3
+ export const subNavReady = writable(false);
@@ -1,4 +1,4 @@
1
- @layer designsystem, theme, buttons;
1
+ @layer designsystem, theme, buttons, appnav, applayout;
2
2
  @import '../../../static/uswds/styles/styles.css' layer(designsystem);
3
3
  @import '../../../node_modules/@gsa-tts/graymatter-style-tokens/dist/css/variables.css'
4
4
  layer(theme);
@@ -6,3 +6,10 @@
6
6
  @import 'typography.css' layer(theme);
7
7
  @import '../utilities/layout.css' layer(theme);
8
8
  @import '../components/heroCards.css' layer(theme);
9
+ @import '../components/globalAppNav.css' layer(appnav);
10
+ @import '../components/globalAppLayout.css' layer(applayout);
11
+
12
+ /* Ensure anchor targets are not hidden behind fixed header */
13
+ [id] {
14
+ scroll-margin-top: 56px;
15
+ }
@@ -9,43 +9,44 @@
9
9
 
10
10
  h1,
11
11
  .ai-heading-size-1 {
12
- font-size: clamp(var(--ai-size-36), 5vw, var(--ai-size-48));
12
+ font-size: clamp(var(--ai-size-30), 5vw, var(--ai-size-32));
13
13
  font-weight: var(--ai-font-weight-semibold);
14
- margin-bottom: var(--ai-size-24);
14
+ margin-bottom: var(--ai-size-20);
15
15
  }
16
16
 
17
17
  h2,
18
18
  .ai-heading-size-2 {
19
- font-size: clamp(var(--ai-size-32), 5vw, var(--ai-size-36));
20
- margin-bottom: var(--ai-size-20);
19
+ font-size: var(--ai-size-26);
20
+ font-weight: var(--ai-font-weight-semibold);
21
+ margin-bottom: var(--ai-size-16);
21
22
  }
22
23
 
23
24
  h3,
24
25
  .ai-heading-size-3 {
25
- font-size: clamp(var(--ai-size-28), 5vw, var(--ai-size-32));
26
- font-weight: var(--ai-font-weight-normal);
27
- margin-bottom: var(--ai-size-18);
26
+ font-size: var(--ai-size-24);
27
+ font-weight: var(--ai-font-weight-semibold);
28
+ margin-bottom: var(--ai-size-16);
28
29
  }
29
30
 
30
31
  h4,
31
32
  .ai-heading-size-4 {
32
- font-size: var(--ai-size-24);
33
- font-weight: var(--ai-font-weight-normal);
34
- margin-bottom: var(--ai-size-16);
33
+ font-size: var(--ai-size-22);
34
+ font-weight: var(--ai-font-weight-semibold);
35
+ margin-bottom: var(--ai-size-10);
35
36
  }
36
37
 
37
38
  h5,
38
39
  .ai-heading-size-5 {
39
40
  font-size: var(--ai-size-22);
40
41
  font-weight: var(--ai-font-weight-normal);
41
- margin-bottom: var(--ai-size-12);
42
+ margin-bottom: var(--ai-size-10);
42
43
  }
43
44
 
44
45
  h6,
45
46
  .ai-heading-size-6 {
46
- font-size: var(--ai-size-20);
47
- font-weight: var(--ai-font-weight-normal);
48
- margin-bottom: var(--ai-size-8);
47
+ font-size: var(--ai-size-18);
48
+ font-weight: var(--ai-font-weight-semibold);
49
+ margin-bottom: var(--ai-size-6);
49
50
  }
50
51
 
51
52
  pre {
@@ -0,0 +1,76 @@
1
+ .app {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100vh;
5
+ }
6
+
7
+ .main-content-mobile {
8
+ flex: 1;
9
+ padding: var(--initial-main-top-padding, calc(3.5rem + 1rem)) 1rem
10
+ var(--initial-main-bottom-padding, calc(5rem + 1rem)) 1rem;
11
+ overflow-y: auto;
12
+ display: block;
13
+ }
14
+
15
+ .layout-container {
16
+ display: none;
17
+ }
18
+
19
+ .main-container {
20
+ display: flex;
21
+ flex-direction: column;
22
+ flex: 1;
23
+ overflow: hidden;
24
+ position: relative;
25
+ }
26
+
27
+ .app main {
28
+ flex: 1;
29
+ padding: var(--initial-main-top-padding, calc(3.5rem + 1rem)) 1rem
30
+ var(--initial-main-bottom-padding, calc(5rem + 1rem)) 1rem;
31
+ overflow-y: auto;
32
+ }
33
+
34
+ @media (--ai-size-breakpoint-tablet) {
35
+ .main-content-mobile {
36
+ display: none;
37
+ }
38
+ .layout-container {
39
+ display: flex;
40
+ flex: 1;
41
+ overflow: hidden;
42
+ flex-direction: row;
43
+ }
44
+ .side-nav-container {
45
+ display: flex;
46
+ flex-shrink: 0;
47
+ position: relative;
48
+ z-index: 2;
49
+ }
50
+ .side-nav-container.expanded {
51
+ width: 12.5rem;
52
+ min-width: 12.5rem;
53
+ max-width: 12.5rem;
54
+ }
55
+ .main-container {
56
+ flex: 1 1 0%;
57
+ min-width: 0;
58
+ position: relative;
59
+ z-index: 1;
60
+ }
61
+ .app main {
62
+ padding: var(--ai-size-40) var(--ai-size-32) var(--ai-size-8) 4.875rem;
63
+ }
64
+
65
+ :global(.side-nav-container.expanded) ~ .main-container main {
66
+ padding-top: var(--ai-size-8);
67
+ }
68
+ :global(.side-nav-container),
69
+ :global(.side-nav-container .expanded-content) {
70
+ transition: none;
71
+ }
72
+ :global(body) {
73
+ overflow-y: hidden;
74
+ height: 100%;
75
+ }
76
+ }