@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/README.md +173 -74
- package/dist/graymatter-ui.js +1348 -1301
- package/dist/graymatter-ui.umd.cjs +6 -6
- package/package.json +3 -3
- package/src/components/DesktopSideNav.svelte +41 -1
- package/src/components/MobileBottomNav.svelte +5 -1
- package/src/components/{UsaBanner.astro → UsaBanner.svelte} +33 -31
- package/src/components/index.ts +1 -1
- package/src/index.ts +3 -0
- package/src/styles/components/heroCards.css +1 -7
- package/src/utils/navigationControl.ts +78 -0
- package/src/utils/setupMocks.ts +10 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsa-tts/graymatter-ui",
|
|
3
|
-
"version": "0.3.
|
|
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-
|
|
58
|
-
"@gsa-tts/graymatter-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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=
|
|
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
|
|
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
|
|
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=
|
|
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=
|
|
39
|
-
Here
|
|
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=
|
|
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">Here
|
|
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=
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 you
|
|
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>
|
package/src/components/index.ts
CHANGED
|
@@ -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-
|
|
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
|
+
}
|
package/src/utils/setupMocks.ts
CHANGED
|
@@ -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
|
});
|