@gsa-tts/graymatter-ui 0.3.21 → 0.3.23
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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { siteName } from '@gsa-tts/graymatter-ui/constants';
|
|
3
|
+
import { getUrlFromBase } from '@gsa-tts/graymatter-ui/helpers';
|
|
4
|
+
import Logo from '@gsa-tts/graymatter-ui/components/Logo.svelte';
|
|
5
|
+
import PlatformFooterMenu from './PlatformFooterMenu.svelte';
|
|
6
|
+
import Button from '@gsa-tts/graymatter-ui/components/Button.svelte';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* PlatformFooter Component
|
|
10
|
+
*
|
|
11
|
+
* The main footer component for the platform, featuring:
|
|
12
|
+
* - Agency branding and logo
|
|
13
|
+
* - Call-to-action section
|
|
14
|
+
* - Footer navigation menus
|
|
15
|
+
*
|
|
16
|
+
* Props:
|
|
17
|
+
* - menus?: FooterMenu[] (optional array of footer menus)
|
|
18
|
+
* - ctaHeading?: string (optional CTA heading text)
|
|
19
|
+
* - ctaButtonText?: string (optional CTA button text)
|
|
20
|
+
* - ctaButtonHref?: string (optional CTA button URL)
|
|
21
|
+
* - showCta?: boolean (whether to show the CTA section)
|
|
22
|
+
* - class?: string (optional additional CSS classes)
|
|
23
|
+
*
|
|
24
|
+
* Example:
|
|
25
|
+
* ```svelte
|
|
26
|
+
* <PlatformFooter {menus} />
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
interface FooterLink {
|
|
30
|
+
title: string;
|
|
31
|
+
url: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface FooterMenu {
|
|
35
|
+
title: string;
|
|
36
|
+
items: Array<FooterLink>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Props {
|
|
40
|
+
menus?: FooterMenu[];
|
|
41
|
+
ctaHeading?: string;
|
|
42
|
+
ctaButtonText?: string;
|
|
43
|
+
ctaButtonHref?: string;
|
|
44
|
+
showCta?: boolean;
|
|
45
|
+
class?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let {
|
|
49
|
+
menus = [],
|
|
50
|
+
ctaHeading = 'Get your team started today.',
|
|
51
|
+
ctaButtonText = 'Partnerships',
|
|
52
|
+
ctaButtonHref = getUrlFromBase('/partnerships'),
|
|
53
|
+
showCta = true,
|
|
54
|
+
class: className = '',
|
|
55
|
+
}: Props = $props();
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<footer class="ai-footer {className}">
|
|
59
|
+
<div class="ai-maxw-widescreen grid-container">
|
|
60
|
+
{#if showCta}
|
|
61
|
+
<section
|
|
62
|
+
class="usa-section ai-section ai-footer-top"
|
|
63
|
+
aria-labelledby="ai-footer-cta"
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
class="usa-identifier__identity ai-identifier__identity"
|
|
67
|
+
aria-label="Agency description"
|
|
68
|
+
>
|
|
69
|
+
<p
|
|
70
|
+
class="usa-identifier__identity-domain ai-identifier__identity-domain"
|
|
71
|
+
>
|
|
72
|
+
<a href={getUrlFromBase('/')} aria-label="Home">
|
|
73
|
+
<Logo
|
|
74
|
+
aria-label={siteName}
|
|
75
|
+
isInverted={true}
|
|
76
|
+
showServiceMark={true}
|
|
77
|
+
width={68}
|
|
78
|
+
/>
|
|
79
|
+
</a>
|
|
80
|
+
</p>
|
|
81
|
+
<p class="usa-identifier__identity-disclaimer text-normal">
|
|
82
|
+
An official website of the U.S.
|
|
83
|
+
<a class="ai-footer-link" href="https://www.gsa.gov/"
|
|
84
|
+
>General Services Administration</a
|
|
85
|
+
>
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
<p class="ai-footer-cta__heading" id="ai-footer-cta">
|
|
89
|
+
{ctaHeading}
|
|
90
|
+
</p>
|
|
91
|
+
<div class="ai-footer-cta__buttons">
|
|
92
|
+
<Button href={ctaButtonHref} theme="inverted" variant="secondary">
|
|
93
|
+
{ctaButtonText}
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
{#if menus.length > 0}
|
|
100
|
+
<section class="usa-section ai-section">
|
|
101
|
+
<div class="grid-row grid-gap">
|
|
102
|
+
<div class="tablet:grid-col-12">
|
|
103
|
+
<nav aria-label="Footer navigation">
|
|
104
|
+
<div class="grid-row grid-gap">
|
|
105
|
+
{#each menus as menu}
|
|
106
|
+
<div class="tablet:grid-col-4">
|
|
107
|
+
<PlatformFooterMenu content={menu} />
|
|
108
|
+
</div>
|
|
109
|
+
{/each}
|
|
110
|
+
</div>
|
|
111
|
+
</nav>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
{/if}
|
|
116
|
+
</div>
|
|
117
|
+
</footer>
|
|
118
|
+
|
|
119
|
+
<style>
|
|
120
|
+
.ai-footer-link {
|
|
121
|
+
color: var(--ai-footer-color-link-rest);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.ai-footer-link:hover {
|
|
125
|
+
color: var(--ai-footer-color-link-hover);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.ai-footer {
|
|
129
|
+
background: var(--ai-footer-color-background);
|
|
130
|
+
color: var(--ai-footer-color-text);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.ai-footer-cta__heading {
|
|
134
|
+
margin-block: 0 var(--ai-size-24);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.ai-footer-top {
|
|
138
|
+
border-bottom: 1px solid var(--ai-footer-color-separator);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.ai-identifier__identity {
|
|
142
|
+
padding-bottom: var(--ai-layout-section-spacing-y);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.ai-identifier__identity-domain {
|
|
146
|
+
color: var(--ai-footer-color-text);
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
}
|
|
149
|
+
</style>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PlatformFooterMenu Component
|
|
6
|
+
*
|
|
7
|
+
* A footer menu component that displays a title and list of links.
|
|
8
|
+
* Used within the PlatformFooter component to create organized footer navigation.
|
|
9
|
+
*
|
|
10
|
+
* Props:
|
|
11
|
+
* - content: FooterMenu object containing title and items
|
|
12
|
+
* - class?: string (optional additional CSS classes)
|
|
13
|
+
*
|
|
14
|
+
* Example:
|
|
15
|
+
* ```svelte
|
|
16
|
+
* <PlatformFooterMenu content={menuData} />
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
interface FooterLink {
|
|
20
|
+
title: string;
|
|
21
|
+
url: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface FooterMenu {
|
|
25
|
+
title: string;
|
|
26
|
+
items: Array<FooterLink>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Props {
|
|
30
|
+
content: FooterMenu;
|
|
31
|
+
class?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let { content, class: className = '' }: Props = $props();
|
|
35
|
+
|
|
36
|
+
// Generate unique ID for accessibility
|
|
37
|
+
const menuLabel = `platform-footer-${uuidv4()}`;
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class={className}>
|
|
41
|
+
<p id={menuLabel}>{content.title}</p>
|
|
42
|
+
<ul class="add-list-reset" aria-labelledby={menuLabel}>
|
|
43
|
+
{#each content.items as item}
|
|
44
|
+
<li>
|
|
45
|
+
<a href={item.url}>{item.title}</a>
|
|
46
|
+
</li>
|
|
47
|
+
{/each}
|
|
48
|
+
</ul>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
p {
|
|
53
|
+
color: var(--ai-footer-color-text-muted);
|
|
54
|
+
margin-top: 0;
|
|
55
|
+
margin-bottom: var(--ai-size-16);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ul {
|
|
59
|
+
margin-bottom: var(--ai-size-32);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
li {
|
|
63
|
+
display: block;
|
|
64
|
+
margin-bottom: var(--ai-size-16);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
a {
|
|
68
|
+
color: var(--ai-footer-color-link-rest);
|
|
69
|
+
display: block;
|
|
70
|
+
text-decoration: none;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
a:hover {
|
|
74
|
+
color: var(--ai-footer-color-link-hover);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
a:is(:focus, :hover, :active) {
|
|
78
|
+
text-decoration: underline;
|
|
79
|
+
}
|
|
80
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { default as UsaSkipNav } from './UsaSkipNav.svelte';
|
|
|
12
12
|
export { default as ApiDocSubNavMenu } from './ApiDocSubNavMenu.svelte';
|
|
13
13
|
export { default as ApiDocSubNavList } from './ApiDocSubNavList.svelte';
|
|
14
14
|
export { default as ApiDocSubNavMenuItem } from './ApiDocSubNavMenuItem.svelte';
|
|
15
|
+
export { default as PlatformFooter } from './PlatformFooter.svelte';
|
|
16
|
+
export { default as PlatformFooterMenu } from './PlatformFooterMenu.svelte';
|
|
15
17
|
|
|
16
18
|
// Icon Components
|
|
17
19
|
export * from './icons/index.js';
|
|
@@ -8,7 +8,8 @@ import MobileBottomNav from '../components/MobileBottomNav.svelte';
|
|
|
8
8
|
import MobileSideMenu from '../components/MobileSideMenu.svelte';
|
|
9
9
|
import Logo from '../components/Logo.svelte';
|
|
10
10
|
import { siteName } from '../constants';
|
|
11
|
-
import {
|
|
11
|
+
import { getUrlFromBase } from '../helpers/url.js';
|
|
12
|
+
import { getSecret } from 'astro:env/server';
|
|
12
13
|
|
|
13
14
|
const {
|
|
14
15
|
componentOptions = {},
|
|
@@ -24,7 +25,17 @@ const {
|
|
|
24
25
|
hideDiscover = true,
|
|
25
26
|
} = Astro.props;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
// Use getSecret directly for better environment variable handling
|
|
29
|
+
const chatUrl = getSecret('CHAT_URL') || getSecret('PUBLIC_CHAT_URL');
|
|
30
|
+
const consoleUrl = getSecret('CONSOLE_URL') || getSecret('PUBLIC_CONSOLE_URL');
|
|
31
|
+
const apiUrl = getSecret('API_URL') || getSecret('PUBLIC_API_URL');
|
|
32
|
+
const discoverUrl =
|
|
33
|
+
getSecret('DISCOVER_URL') || getSecret('PUBLIC_DISCOVER_URL');
|
|
34
|
+
|
|
35
|
+
const chat = chatUrl || '#';
|
|
36
|
+
const console = consoleUrl || '#';
|
|
37
|
+
const api = apiUrl || getUrlFromBase('/api/documentation');
|
|
38
|
+
const discover = discoverUrl || '#';
|
|
28
39
|
---
|
|
29
40
|
|
|
30
41
|
<BaseLayout title={pageTitle} {gtmID} {description} {openGraphImage}>
|
package/src/utils/getAppUrls.ts
CHANGED
|
@@ -66,7 +66,7 @@ function processUrl(url: string | undefined, fallback: string): string {
|
|
|
66
66
|
*
|
|
67
67
|
* @param envArg - Optional environment object to use instead of `import.meta.env`.
|
|
68
68
|
* Useful for testing or when you want to override environment variables.
|
|
69
|
-
* If not provided, uses
|
|
69
|
+
* If not provided, uses `process.env` (server) or `import.meta.env` (client).
|
|
70
70
|
*
|
|
71
71
|
* @returns An object containing processed URLs/paths for each application section:
|
|
72
72
|
* - `chat`: Chat application URL or path
|
|
@@ -102,31 +102,51 @@ function processUrl(url: string | undefined, fallback: string): string {
|
|
|
102
102
|
* ```
|
|
103
103
|
*
|
|
104
104
|
* @remarks
|
|
105
|
+
* - Uses `process.env` in server context, `import.meta.env` in client context
|
|
106
|
+
* - Supports both `CHAT_URL` and `PUBLIC_CHAT_URL` naming conventions
|
|
105
107
|
* - URLs are processed through `processUrl()` which handles both absolute and relative URLs
|
|
106
|
-
* - Relative URLs are processed through `getUrlFromBase()` to create full URLs
|
|
107
108
|
* - Missing environment variables result in fallback values (`#` for most, `/api/documentation` for API)
|
|
108
109
|
* - Warnings are logged in development when environment variables are missing
|
|
109
|
-
* - The function is designed to work with Vite's `import.meta.env`
|
|
110
|
+
* - The function is designed to work with both Vite's `import.meta.env` and Node.js `process.env`
|
|
111
|
+
* - Note: Astro layouts (like GlobalAppLayout.astro) use `getSecret()` directly for better performance
|
|
110
112
|
*
|
|
111
113
|
* @see {@link processUrl} - URL processing logic
|
|
112
114
|
* @see {@link getUrlFromBase} - Base URL resolution
|
|
113
115
|
*/
|
|
114
116
|
export function getAppUrls(envArg?: Record<string, string | undefined>) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
// If envArg is provided, use it directly (for testing)
|
|
118
|
+
if (envArg) {
|
|
119
|
+
const chatUrl = envArg.CHAT_URL || envArg.PUBLIC_CHAT_URL;
|
|
120
|
+
const consoleUrl = envArg.CONSOLE_URL || envArg.PUBLIC_CONSOLE_URL;
|
|
121
|
+
const apiUrl = envArg.API_URL || envArg.PUBLIC_API_URL;
|
|
122
|
+
const discoverUrl = envArg.DISCOVER_URL || envArg.PUBLIC_DISCOVER_URL;
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
warnIfMissing('chat', chatUrl);
|
|
125
|
+
warnIfMissing('console', consoleUrl);
|
|
126
|
+
warnIfMissing('api', apiUrl);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
chat: processUrl(chatUrl, '#'),
|
|
130
|
+
console: processUrl(consoleUrl, '#'),
|
|
131
|
+
api: processUrl(apiUrl, getUrlFromBase('/api/documentation')),
|
|
132
|
+
discover: processUrl(discoverUrl, '#'),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Use process.env in server context, import.meta.env in client context
|
|
137
|
+
const env = typeof window === 'undefined' ? process.env : import.meta.env;
|
|
138
|
+
const chatUrl = env.CHAT_URL || env.PUBLIC_CHAT_URL;
|
|
139
|
+
const consoleUrl = env.CONSOLE_URL || env.PUBLIC_CONSOLE_URL;
|
|
140
|
+
const apiUrl = env.API_URL || env.PUBLIC_API_URL;
|
|
141
|
+
const discoverUrl = env.DISCOVER_URL || env.PUBLIC_DISCOVER_URL;
|
|
122
142
|
|
|
123
143
|
warnIfMissing('chat', chatUrl);
|
|
124
144
|
warnIfMissing('console', consoleUrl);
|
|
125
145
|
warnIfMissing('api', apiUrl);
|
|
126
146
|
|
|
127
147
|
return {
|
|
128
|
-
chat: processUrl(chatUrl,
|
|
129
|
-
console: processUrl(consoleUrl,
|
|
148
|
+
chat: processUrl(chatUrl, '#'),
|
|
149
|
+
console: processUrl(consoleUrl, '#'),
|
|
130
150
|
api: processUrl(apiUrl, getUrlFromBase('/api/documentation')),
|
|
131
151
|
discover: processUrl(discoverUrl, '#'),
|
|
132
152
|
};
|