@gsa-tts/graymatter-ui 0.3.2 → 0.3.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsa-tts/graymatter-ui",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -54,8 +54,8 @@
54
54
  "typescript": "5.7.3",
55
55
  "vite": "^6.3.5",
56
56
  "vitest": "^3.0.7",
57
- "@gsa-tts/graymatter-typescript-config": "0.0.1",
58
- "@gsa-tts/graymatter-eslint": "0.0.1",
57
+ "@gsa-tts/graymatter-eslint": "0.0.2",
58
+ "@gsa-tts/graymatter-typescript-config": "0.0.2",
59
59
  "@gsa-tts/graymatter-vitest-config": "0.0.1"
60
60
  },
61
61
  "dependencies": {
@@ -21,7 +21,6 @@
21
21
  import ProfileMenu from './ProfileMenu.svelte';
22
22
  import { getAppUrls } from '../utils/getAppUrls';
23
23
  // import HelpIcon from './icons/HelpIcon.svelte';
24
- const urls = getAppUrls();
25
24
 
26
25
  const {
27
26
  ssrSelectedItem = undefined,
@@ -36,6 +35,12 @@
36
35
  discoverUrl,
37
36
  } = $props();
38
37
 
38
+ // Only call getAppUrls if not all URLs are provided via props
39
+ const urls =
40
+ apiUrl && chatUrl && consoleUrl && discoverUrl
41
+ ? { api: '#', chat: '#', console: '#', discover: '#' }
42
+ : getAppUrls();
43
+
39
44
  let hydrated = false;
40
45
  let navContainer: HTMLDivElement;
41
46
  let hoveredItem = $state<string | null>(null);
@@ -47,6 +52,9 @@
47
52
  let consoleNavRef = $state<HTMLAnchorElement | null>(null);
48
53
  let apiNavRef = $state<HTMLAnchorElement | null>(null);
49
54
 
55
+ // Event system for external control
56
+ let eventListenerCleanup: (() => void) | null = null;
57
+
50
58
  function ssrOrHydrated<T>(hydratedValue: T, ssrValue: T): T {
51
59
  return hydrated ? hydratedValue : ssrValue;
52
60
  }
@@ -134,9 +142,40 @@
134
142
  };
135
143
  window.addEventListener('keydown', handleEscape);
136
144
 
145
+ // Custom event system for external control
146
+ const handleDesktopNavControlEvent = (event: CustomEvent) => {
147
+ const { action, isOpen: eventIsOpen } = event.detail;
148
+
149
+ if (action === 'toggle') {
150
+ navigationStore.toggleExpanded();
151
+ } else if (action === 'open') {
152
+ navigationStore.setExpanded(true);
153
+ } else if (action === 'close') {
154
+ navigationStore.setExpanded(false);
155
+ } else if (action === 'set' && typeof eventIsOpen === 'boolean') {
156
+ navigationStore.setExpanded(eventIsOpen);
157
+ }
158
+ };
159
+
160
+ // Listen for custom desktop navigation control events
161
+ window.addEventListener(
162
+ 'graymatter-desktop-nav-control',
163
+ handleDesktopNavControlEvent
164
+ );
165
+
166
+ eventListenerCleanup = () => {
167
+ window.removeEventListener(
168
+ 'graymatter-desktop-nav-control',
169
+ handleDesktopNavControlEvent
170
+ );
171
+ };
172
+
137
173
  return () => {
138
174
  window.removeEventListener('layoutTransition', handleLayoutTransition);
139
175
  window.removeEventListener('keydown', handleEscape);
176
+ if (eventListenerCleanup) {
177
+ eventListenerCleanup();
178
+ }
140
179
  };
141
180
  }
142
181
  });
@@ -255,6 +294,7 @@
255
294
  class:expanded={sideNavExpanded()}
256
295
  bind:this={navContainer}
257
296
  part="side-nav-container"
297
+ data-testid="side-nav-container"
258
298
  >
259
299
  <div class="nav-content" part="nav-content">
260
300
  <div class="nav-main" part="nav-main">
@@ -27,7 +27,11 @@
27
27
  discoverUrl?: string;
28
28
  } = $props();
29
29
 
30
- const urls = getAppUrls();
30
+ // Only call getAppUrls if not all URLs are provided via props
31
+ const urls =
32
+ apiUrl && chatUrl && consoleUrl && discoverUrl
33
+ ? { api: '#', chat: '#', console: '#', discover: '#' }
34
+ : getAppUrls();
31
35
 
32
36
  const navItems = [
33
37
  {
@@ -1,20 +1,24 @@
1
- ---
2
- import { getUrlFromBase } from '@gsa-tts/graymatter-ui/helpers/url.js';
3
- const { isInverted = false, maxWidth = 'none' } = Astro.props as {
4
- isInverted?: boolean;
5
- maxWidth?: 'none' | 'widescreen';
6
- };
1
+ <script lang="ts">
2
+ import { getUrlFromBase } from '@gsa-tts/graymatter-ui/helpers/url.js';
7
3
 
8
- const sizeMap: Record<'widescreen' | 'none', string> = {
9
- none: 'ai-maxw-none',
10
- widescreen: 'ai-maxw-widescreen',
11
- };
4
+ interface Props {
5
+ isInverted?: boolean;
6
+ maxWidth?: 'none' | 'widescreen';
7
+ }
8
+
9
+ let { isInverted = false, maxWidth = 'none' }: Props = $props();
10
+
11
+ const sizeMap: Record<'widescreen' | 'none', string> = {
12
+ none: 'ai-maxw-none',
13
+ widescreen: 'ai-maxw-widescreen',
14
+ };
12
15
 
13
- const mappedSize = sizeMap[maxWidth] || sizeMap.none;
14
- ---
16
+ const mappedSize = sizeMap[maxWidth] || sizeMap.none;
17
+ </script>
15
18
 
16
19
  <section
17
- class={`usa-banner site-banner ${isInverted ? 'inverted' : ''}`}
20
+ class="usa-banner site-banner"
21
+ class:inverted={isInverted}
18
22
  aria-label="Official website of the United States government"
19
23
  >
20
24
  <div class="usa-accordion">
@@ -22,43 +26,42 @@ const mappedSize = sizeMap[maxWidth] || sizeMap.none;
22
26
  class="usa-banner__header ai-banner__header"
23
27
  aria-label="USA banner"
24
28
  >
25
- <div class=`usa-banner__inner ${mappedSize}`>
29
+ <div class="usa-banner__inner {mappedSize}">
26
30
  <div class="grid-col-auto">
27
31
  <img
28
32
  aria-hidden="true"
29
33
  class="usa-banner__header-flag"
30
- src=`${getUrlFromBase('uswds/img/us_flag_small.png')}`
34
+ src={getUrlFromBase('uswds/img/us_flag_small.png')}
31
35
  alt=""
32
36
  />
33
37
  </div>
34
38
  <div class="grid-col-fill tablet:grid-col-auto" aria-hidden="true">
35
- <p class={'usa-banner__header-text ai-banner__header-text'}>
39
+ <p class="usa-banner__header-text ai-banner__header-text">
36
40
  An official website of the United States government
37
41
  </p>
38
- <p class={'usa-banner__header-action ai-banner__header-action'}>
39
- Heres how you know
42
+ <p class="usa-banner__header-action ai-banner__header-action">
43
+ Here's how you know
40
44
  </p>
41
45
  </div>
42
46
  <button
43
47
  type="button"
44
- class={'usa-accordion__button usa-banner__button ai-banner__button'}
48
+ class="usa-accordion__button usa-banner__button ai-banner__button"
45
49
  aria-expanded="false"
46
50
  aria-controls="gov-banner-default"
47
51
  >
48
- <span class="usa-banner__button-text">Heres how you know</span>
52
+ <span class="usa-banner__button-text">Here's how you know</span>
49
53
  </button>
50
54
  </div>
51
55
  </header>
52
56
  <div
53
- class={`usa-banner__content usa-accordion__content ai-banner__content ${mappedSize}`}
57
+ class="usa-banner__content usa-accordion__content ai-banner__content {mappedSize}"
54
58
  id="gov-banner-default"
55
59
  >
56
60
  <div class="grid-row grid-gap-lg">
57
61
  <div class="usa-banner__guidance tablet:grid-col-6">
58
62
  <img
59
63
  class="usa-banner__icon ai-banner__icon usa-media-block__img"
60
- src=`${getUrlFromBase('uswds/img/icon-dot-gov.svg')}`
61
- role="img"
64
+ src={getUrlFromBase('uswds/img/icon-dot-gov.svg')}
62
65
  alt=""
63
66
  aria-hidden="true"
64
67
  />
@@ -73,8 +76,7 @@ const mappedSize = sizeMap[maxWidth] || sizeMap.none;
73
76
  <div class="usa-banner__guidance tablet:grid-col-6">
74
77
  <img
75
78
  class="usa-banner__icon ai-banner__icon usa-media-block__img"
76
- src=`${getUrlFromBase('uswds/img/icon-https.svg')}`
77
- role="img"
79
+ src={getUrlFromBase('uswds/img/icon-https.svg')}
78
80
  alt=""
79
81
  aria-hidden="true"
80
82
  />
@@ -82,8 +84,8 @@ const mappedSize = sizeMap[maxWidth] || sizeMap.none;
82
84
  <p>
83
85
  <strong>Secure .gov websites use HTTPS</strong><br />A
84
86
  <strong>lock</strong> (
85
- <span class="icon-lock"
86
- ><svg
87
+ <span class="icon-lock">
88
+ <svg
87
89
  xmlns="http://www.w3.org/2000/svg"
88
90
  width="52"
89
91
  height="64"
@@ -94,16 +96,16 @@ const mappedSize = sizeMap[maxWidth] || sizeMap.none;
94
96
  focusable="false"
95
97
  >
96
98
  <title id="banner-lock-title-default">Lock</title>
97
- <desc id="banner-lock-description-default"
98
- >Locked padlock icon</desc
99
- >
99
+ <desc id="banner-lock-description-default">
100
+ Locked padlock icon
101
+ </desc>
100
102
  <path
101
103
  fill="#000000"
102
104
  fill-rule="evenodd"
103
105
  d="M26 0c10.493 0 19 8.507 19 19v9h3a4 4 0 0 1 4 4v28a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4h3v-9C7 8.507 15.507 0 26 0zm0 8c-5.979 0-10.843 4.77-10.996 10.712L15 19v9h22v-9c0-6.075-4.925-11-11-11z"
104
106
  ></path>
105
107
  </svg>
106
- </span>) or <strong>https://</strong> means youve safely connected
108
+ </span>) or <strong>https://</strong> means you've safely connected
107
109
  to the .gov website. Share sensitive information only on official,
108
110
  secure websites.
109
111
  </p>
@@ -1,8 +1,8 @@
1
1
  // Astro Components
2
2
  export { default as Head } from './Head.astro';
3
- export { default as UsaBanner } from './UsaBanner.astro';
4
3
 
5
4
  // Svelte Components
5
+ export { default as UsaBanner } from './UsaBanner.svelte';
6
6
  export { default as Logo } from './Logo.svelte';
7
7
  export { default as Button } from './Button.svelte';
8
8
  export { default as DesktopSideNav } from './DesktopSideNav.svelte';
package/src/index.ts CHANGED
@@ -5,6 +5,9 @@ import Logo from './components/Logo.webcomponent.svelte';
5
5
  import MobileBottomNav from './components/MobileBottomNav.webcomponent.svelte';
6
6
  import MobileSideMenu from './components/MobileSideMenu.webcomponent.svelte';
7
7
  import MobileTopNav from './components/MobileTopNav.webcomponent.svelte';
8
+
9
+ export * from './utils/navigationControl';
10
+
8
11
  export {
9
12
  Button,
10
13
  DesktopHeader,
@@ -65,16 +65,10 @@
65
65
  }
66
66
 
67
67
  .ai-hero-card__title {
68
- font-size: var(--ai-size-24);
68
+ font-size: var(--ai-size-32);
69
69
  }
70
70
 
71
71
  .ai-hero-card__content {
72
72
  order: 1;
73
73
  }
74
74
  }
75
-
76
- @media (--ai-size-breakpoint-desktop-lg) {
77
- .ai-hero-card__title {
78
- font-size: var(--ai-size-32);
79
- }
80
- }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Utility functions for controlling the DesktopSideNav component externally
3
+ * This allows external apps to control the desktop navigation state without direct prop access
4
+ */
5
+
6
+ export interface DesktopNavControlEventDetail {
7
+ action: 'toggle' | 'open' | 'close' | 'set';
8
+ isOpen?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Dispatches a custom event to control the DesktopSideNav component
13
+ * @param action - The action to perform ('toggle', 'open', 'close', or 'set')
14
+ * @param isOpen - Required when action is 'set', specifies the desired state
15
+ */
16
+ export function controlDesktopNavigation(
17
+ action: 'toggle' | 'open' | 'close',
18
+ isOpen?: never
19
+ ): void;
20
+ export function controlDesktopNavigation(action: 'set', isOpen: boolean): void;
21
+ export function controlDesktopNavigation(
22
+ action: DesktopNavControlEventDetail['action'],
23
+ isOpen?: boolean
24
+ ): void {
25
+ if (typeof window === 'undefined') {
26
+ console.warn(
27
+ '[DesktopNavControl] Cannot control desktop navigation on server side'
28
+ );
29
+ return;
30
+ }
31
+
32
+ if (action === 'set' && typeof isOpen !== 'boolean') {
33
+ console.error(
34
+ '[DesktopNavControl] isOpen parameter is required when action is "set"'
35
+ );
36
+ return;
37
+ }
38
+
39
+ const event = new CustomEvent<DesktopNavControlEventDetail>(
40
+ 'graymatter-desktop-nav-control',
41
+ {
42
+ detail: { action, isOpen },
43
+ bubbles: true,
44
+ cancelable: false,
45
+ }
46
+ );
47
+
48
+ window.dispatchEvent(event);
49
+ }
50
+
51
+ /**
52
+ * Toggle the desktop navigation state
53
+ */
54
+ export function toggleNavigation(): void {
55
+ controlDesktopNavigation('toggle');
56
+ }
57
+
58
+ /**
59
+ * Open the desktop navigation
60
+ */
61
+ export function openNavigation(): void {
62
+ controlDesktopNavigation('open');
63
+ }
64
+
65
+ /**
66
+ * Close the desktop navigation
67
+ */
68
+ export function closeNavigation(): void {
69
+ controlDesktopNavigation('close');
70
+ }
71
+
72
+ /**
73
+ * Set the desktop navigation to a specific state
74
+ * @param isOpen - The desired desktop navigation state
75
+ */
76
+ export function setNavigationState(isOpen: boolean): void {
77
+ controlDesktopNavigation('set', isOpen);
78
+ }
@@ -1,13 +1,18 @@
1
1
  import { vi } from 'vitest';
2
- import { writable } from 'svelte/store';
2
+ import { writable, derived } from 'svelte/store';
3
3
 
4
4
  export function setupCommonMocks() {
5
5
  // navigationStore.js mock
6
6
  vi.mock('../stores/navigationStore.js', () => {
7
7
  const isExpanded = writable(true);
8
8
  const selectedItem = writable('console');
9
- const isMenuButtonDisabled = writable(false);
10
9
  const selectedSubNavItemTitle = writable('');
10
+
11
+ const isMenuButtonDisabled = derived(
12
+ selectedItem,
13
+ $selectedItem => $selectedItem === 'discover'
14
+ );
15
+
11
16
  return {
12
17
  isExpanded,
13
18
  selectedItem,
@@ -16,6 +21,9 @@ export function setupCommonMocks() {
16
21
  navigationStore: {
17
22
  setSelectedItem: vi.fn(),
18
23
  setExpanded: (val: boolean) => isExpanded.set(val),
24
+ toggleExpanded: () =>
25
+ isExpanded.update((expanded: boolean) => !expanded),
26
+ handleLayoutTransition: vi.fn(),
19
27
  },
20
28
  };
21
29
  });