@gsa-tts/graymatter-ui 1.2.0 → 1.3.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.
- package/dist/graymatter-ui.js +2525 -2339
- package/dist/graymatter-ui.umd.cjs +17 -7
- package/package.json +2 -2
- package/src/component-options/componentOptions.ts +2 -2
- package/src/components/DesktopSideNav.svelte +17 -15
- package/src/components/MobileTopNav.svelte +15 -13
- package/src/components/ProfileMenu.svelte +22 -15
- package/src/components/Tooltip.svelte +288 -0
- package/src/components/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gsa-tts/graymatter-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"typescript": "5.7.3",
|
|
68
68
|
"vite": "^7.3.2",
|
|
69
69
|
"vitest": "^3.2.4",
|
|
70
|
-
"@gsa-tts/graymatter-eslint": "0.0.2",
|
|
71
70
|
"@gsa-tts/graymatter-typescript-config": "0.0.3",
|
|
71
|
+
"@gsa-tts/graymatter-eslint": "0.0.2",
|
|
72
72
|
"@gsa-tts/graymatter-vitest-config": "0.0.2"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
@@ -45,8 +45,8 @@ export const componentOptions: ComponentOptions = {
|
|
|
45
45
|
header: { theme: 'inverse' },
|
|
46
46
|
},
|
|
47
47
|
partnershipsSubpage: {
|
|
48
|
-
usaBanner: { theme: '
|
|
49
|
-
header: { theme: '
|
|
48
|
+
usaBanner: { theme: 'inverse' },
|
|
49
|
+
header: { theme: 'inverse' },
|
|
50
50
|
},
|
|
51
51
|
website: {
|
|
52
52
|
usaBanner: { theme: 'inverse' },
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import MenuIcon from './icons/MenuIcon.svelte';
|
|
26
26
|
import ProfileMenu from './ProfileMenu.svelte';
|
|
27
27
|
import HelpIcon from './icons/HelpIcon.svelte';
|
|
28
|
+
import Tooltip from './Tooltip.svelte';
|
|
28
29
|
|
|
29
30
|
let {
|
|
30
31
|
ssrSelectedItem = undefined,
|
|
@@ -530,22 +531,23 @@
|
|
|
530
531
|
class="nav-footer-icon-container"
|
|
531
532
|
part="nav-footer-icon-container"
|
|
532
533
|
>
|
|
533
|
-
<
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
534
|
+
<Tooltip text="User Guide" position="right">
|
|
535
|
+
<a
|
|
536
|
+
class="help-link"
|
|
537
|
+
class:selected={isHelpSelected()}
|
|
538
|
+
aria-current={isHelpSelected() ? 'page' : undefined}
|
|
539
|
+
href={helpUrl()}
|
|
540
|
+
part="help-link"
|
|
541
|
+
aria-label="User Guide"
|
|
542
|
+
onclick={handleHelpClick}
|
|
543
|
+
>
|
|
544
|
+
<div class="icon-container" part="help-icon-container">
|
|
545
|
+
<div class="icon-wrapper">
|
|
546
|
+
<HelpIcon isSelected={isHelpSelected()} />
|
|
547
|
+
</div>
|
|
546
548
|
</div>
|
|
547
|
-
</
|
|
548
|
-
</
|
|
549
|
+
</a>
|
|
550
|
+
</Tooltip>
|
|
549
551
|
</div>
|
|
550
552
|
{/if}
|
|
551
553
|
<div class="nav-footer-profile-container">
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import Logo from './Logo.svelte';
|
|
8
8
|
import MenuIcon from './icons/MenuIcon.svelte';
|
|
9
9
|
import HelpIcon from './icons/HelpIcon.svelte';
|
|
10
|
+
import Tooltip from './Tooltip.svelte';
|
|
10
11
|
|
|
11
12
|
let { profileMenuData, logoLinkUrl, logoLinkLabel, supplementalMenuData } =
|
|
12
13
|
$props();
|
|
@@ -141,19 +142,20 @@
|
|
|
141
142
|
</div>
|
|
142
143
|
|
|
143
144
|
{#if helpUrl()}
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
145
|
+
<Tooltip text="User Guide" position="bottom">
|
|
146
|
+
<a
|
|
147
|
+
class="help-button"
|
|
148
|
+
class:selected={isHelpSelected()}
|
|
149
|
+
aria-current={isHelpSelected() ? 'page' : undefined}
|
|
150
|
+
href={helpUrl()}
|
|
151
|
+
aria-label="User Guide"
|
|
152
|
+
onclick={handleHelpClick}
|
|
153
|
+
>
|
|
154
|
+
<span class="icon-container">
|
|
155
|
+
<HelpIcon isSelected={isHelpSelected()} />
|
|
156
|
+
</span>
|
|
157
|
+
</a>
|
|
158
|
+
</Tooltip>
|
|
157
159
|
{/if}
|
|
158
160
|
|
|
159
161
|
{#if profileMenuData}
|
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
* - composed: true (allows event to cross Shadow DOM boundaries)
|
|
66
66
|
*/
|
|
67
67
|
|
|
68
|
+
import Tooltip from './Tooltip.svelte';
|
|
69
|
+
|
|
68
70
|
/**
|
|
69
71
|
* Array of menu items to render in the profile menu.
|
|
70
72
|
* Each item: { label: string, icon: string, action: string }
|
|
@@ -167,23 +169,28 @@
|
|
|
167
169
|
</script>
|
|
168
170
|
|
|
169
171
|
<div style="position: relative; display: inline-block;" data-profile-menu>
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
aria-label={ariaLabel}
|
|
174
|
-
aria-haspopup="menu"
|
|
175
|
-
aria-expanded={menuOpen}
|
|
176
|
-
aria-controls={menuId}
|
|
177
|
-
onclick={toggleMenu}
|
|
178
|
-
onblur={handleBlur}
|
|
179
|
-
bind:this={profileButton}
|
|
172
|
+
<Tooltip
|
|
173
|
+
text="Account"
|
|
174
|
+
position={menuAlign === 'desktop' ? 'right' : 'bottom'}
|
|
180
175
|
>
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
176
|
+
<button
|
|
177
|
+
class="user-profile-button"
|
|
178
|
+
tabindex="0"
|
|
179
|
+
aria-label={ariaLabel}
|
|
180
|
+
aria-haspopup="menu"
|
|
181
|
+
aria-expanded={menuOpen}
|
|
182
|
+
aria-controls={menuId}
|
|
183
|
+
onclick={toggleMenu}
|
|
184
|
+
onblur={handleBlur}
|
|
185
|
+
bind:this={profileButton}
|
|
186
|
+
>
|
|
187
|
+
<div class="user-profile-container">
|
|
188
|
+
<div class="user-profile-icon" part="user-profile-icon">
|
|
189
|
+
<span class="user-initials">{userMeta?.initials}</span>
|
|
190
|
+
</div>
|
|
184
191
|
</div>
|
|
185
|
-
</
|
|
186
|
-
</
|
|
192
|
+
</button>
|
|
193
|
+
</Tooltip>
|
|
187
194
|
|
|
188
195
|
{#if menuOpen}
|
|
189
196
|
<div
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Tooltip Component
|
|
4
|
+
*
|
|
5
|
+
* Ported from usai-frontend's Tooltip:
|
|
6
|
+
* https://github.com/GSA-TTS/usai-frontend/blob/4050b68aeccf21572c3897a0b2e01c4719b9456a/src/lib/components/shared/Tooltip.svelte
|
|
7
|
+
* (kept behaviorally identical; renamed `.tooltip*` classes to `.ai-tooltip*`
|
|
8
|
+
* and `--tooltip-*` vars to `--ai-tooltip-*` to match this package's naming).
|
|
9
|
+
*
|
|
10
|
+
* The bubble is portaled to <body> and positioned with `position: fixed` +
|
|
11
|
+
* JS coordinates so it never gets clipped by an `overflow: hidden` ancestor.
|
|
12
|
+
*
|
|
13
|
+
* Appearance is driven entirely by `--ai-tooltip-*` CSS custom properties. To
|
|
14
|
+
* restyle per app, override those variables at `:root` (or any scope that
|
|
15
|
+
* reaches <body>) — no component changes needed.
|
|
16
|
+
*
|
|
17
|
+
* `children` is a snippet that receives accessibility props. Callers that
|
|
18
|
+
* don't need them can pass plain content; the extra args are simply ignored.
|
|
19
|
+
*
|
|
20
|
+
* Props:
|
|
21
|
+
* - text: string - Text shown inside the tooltip bubble
|
|
22
|
+
* - position?: 'top' | 'bottom' | 'left' | 'right' (default: 'top')
|
|
23
|
+
* - align?: 'center' | 'left' | 'right' (default: 'center')
|
|
24
|
+
* - showOn?: 'hover' | 'disabled' (default: 'hover')
|
|
25
|
+
* - disabled?: boolean (default: false)
|
|
26
|
+
* - showArrow?: boolean (default: false)
|
|
27
|
+
* - minWidth?: string
|
|
28
|
+
* - maxWidth?: string
|
|
29
|
+
* - tooltipId?: string - Auto-generated if omitted
|
|
30
|
+
* - wrapperClass?: string
|
|
31
|
+
* - children?: Snippet<[TooltipChildrenProps]>
|
|
32
|
+
*
|
|
33
|
+
* Example:
|
|
34
|
+
* ```svelte
|
|
35
|
+
* <Tooltip text="Help" position="right">
|
|
36
|
+
* <a href="/help" aria-label="Help"><HelpIcon /></a>
|
|
37
|
+
* </Tooltip>
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
import { onMount, onDestroy } from 'svelte';
|
|
41
|
+
|
|
42
|
+
interface TooltipChildrenProps {
|
|
43
|
+
disabled: boolean;
|
|
44
|
+
tooltipId: string;
|
|
45
|
+
ariaDisabled: 'true' | 'false';
|
|
46
|
+
ariaDescribedby: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Props {
|
|
50
|
+
text: string;
|
|
51
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
52
|
+
align?: 'center' | 'left' | 'right';
|
|
53
|
+
showOn?: 'hover' | 'disabled';
|
|
54
|
+
disabled?: boolean;
|
|
55
|
+
showArrow?: boolean;
|
|
56
|
+
minWidth?: string;
|
|
57
|
+
maxWidth?: string;
|
|
58
|
+
tooltipId?: string;
|
|
59
|
+
wrapperClass?: string;
|
|
60
|
+
children?: import('svelte').Snippet<[TooltipChildrenProps]>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let {
|
|
64
|
+
text,
|
|
65
|
+
position = 'top',
|
|
66
|
+
align = 'center',
|
|
67
|
+
showOn = 'hover',
|
|
68
|
+
disabled = false,
|
|
69
|
+
showArrow = false,
|
|
70
|
+
minWidth,
|
|
71
|
+
maxWidth,
|
|
72
|
+
tooltipId = `tooltip-${Math.random().toString(36).substring(2, 9)}`,
|
|
73
|
+
wrapperClass = '',
|
|
74
|
+
children,
|
|
75
|
+
}: Props = $props();
|
|
76
|
+
|
|
77
|
+
let isVisible = $state(false);
|
|
78
|
+
let tooltipElement: HTMLDivElement;
|
|
79
|
+
let wrapperElement: HTMLDivElement;
|
|
80
|
+
|
|
81
|
+
// Gap in px between the trigger and the bubble.
|
|
82
|
+
const GAP = 6;
|
|
83
|
+
|
|
84
|
+
// 'hover' → always available; 'disabled' → only when the trigger is disabled.
|
|
85
|
+
let shouldShow = $derived(
|
|
86
|
+
showOn === 'hover' || (showOn === 'disabled' && disabled)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
let customStyle = $derived(
|
|
90
|
+
[minWidth && `min-width: ${minWidth}`, maxWidth && `max-width: ${maxWidth}`]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join('; ') || undefined
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
function updatePosition() {
|
|
96
|
+
if (!tooltipElement || !wrapperElement || typeof window === 'undefined') {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const wrap = wrapperElement.getBoundingClientRect();
|
|
101
|
+
const tip = tooltipElement.getBoundingClientRect();
|
|
102
|
+
const centerX = wrap.left + wrap.width / 2;
|
|
103
|
+
const centerY = wrap.top + wrap.height / 2;
|
|
104
|
+
|
|
105
|
+
// Clear any previously-set coordinates so only the ones we set below apply.
|
|
106
|
+
tooltipElement.style.top = '';
|
|
107
|
+
tooltipElement.style.left = '';
|
|
108
|
+
tooltipElement.style.transform = 'none';
|
|
109
|
+
|
|
110
|
+
let top = 0;
|
|
111
|
+
let left = 0;
|
|
112
|
+
|
|
113
|
+
if (position === 'top' || position === 'bottom') {
|
|
114
|
+
top =
|
|
115
|
+
position === 'top' ? wrap.top - tip.height - GAP : wrap.bottom + GAP;
|
|
116
|
+
if (align === 'left') left = wrap.left;
|
|
117
|
+
else if (align === 'right') left = wrap.right - tip.width;
|
|
118
|
+
else left = centerX - tip.width / 2;
|
|
119
|
+
} else {
|
|
120
|
+
left =
|
|
121
|
+
position === 'left' ? wrap.left - tip.width - GAP : wrap.right + GAP;
|
|
122
|
+
top = centerY - tip.height / 2;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
tooltipElement.style.top = `${top}px`;
|
|
126
|
+
tooltipElement.style.left = `${left}px`;
|
|
127
|
+
|
|
128
|
+
// Point the arrow at the trigger's center regardless of alignment.
|
|
129
|
+
if (showArrow && arrowElement) {
|
|
130
|
+
if (position === 'top' || position === 'bottom') {
|
|
131
|
+
arrowElement.style.left = `${centerX - left}px`;
|
|
132
|
+
arrowElement.style.top = '';
|
|
133
|
+
} else {
|
|
134
|
+
arrowElement.style.top = `${centerY - top}px`;
|
|
135
|
+
arrowElement.style.left = '';
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let arrowElement = $state<HTMLDivElement>();
|
|
141
|
+
|
|
142
|
+
$effect(() => {
|
|
143
|
+
if (isVisible && tooltipElement) {
|
|
144
|
+
requestAnimationFrame(updatePosition);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
onMount(() => {
|
|
149
|
+
if (typeof window !== 'undefined' && tooltipElement) {
|
|
150
|
+
document.body.appendChild(tooltipElement);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
onDestroy(() => {
|
|
155
|
+
if (typeof window !== 'undefined' && tooltipElement?.parentNode) {
|
|
156
|
+
tooltipElement.parentNode.removeChild(tooltipElement);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
function open() {
|
|
161
|
+
if (shouldShow) isVisible = true;
|
|
162
|
+
}
|
|
163
|
+
function close() {
|
|
164
|
+
isVisible = false;
|
|
165
|
+
}
|
|
166
|
+
</script>
|
|
167
|
+
|
|
168
|
+
<div
|
|
169
|
+
bind:this={wrapperElement}
|
|
170
|
+
class={`ai-tooltip-trigger ${wrapperClass}`}
|
|
171
|
+
role="none"
|
|
172
|
+
onmouseenter={open}
|
|
173
|
+
onmouseleave={close}
|
|
174
|
+
onfocusin={open}
|
|
175
|
+
onfocusout={close}
|
|
176
|
+
>
|
|
177
|
+
{@render children?.({
|
|
178
|
+
disabled,
|
|
179
|
+
tooltipId,
|
|
180
|
+
ariaDisabled: disabled ? 'true' : 'false',
|
|
181
|
+
ariaDescribedby: shouldShow ? tooltipId : '',
|
|
182
|
+
})}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<div
|
|
186
|
+
bind:this={tooltipElement}
|
|
187
|
+
id={tooltipId}
|
|
188
|
+
class={`ai-tooltip pos-${position}`}
|
|
189
|
+
class:visible={isVisible && shouldShow}
|
|
190
|
+
style={customStyle}
|
|
191
|
+
role="tooltip"
|
|
192
|
+
aria-live="polite"
|
|
193
|
+
>
|
|
194
|
+
{text}{#if showArrow}<div
|
|
195
|
+
bind:this={arrowElement}
|
|
196
|
+
class="ai-tooltip-arrow"
|
|
197
|
+
aria-hidden="true"
|
|
198
|
+
></div>{/if}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<style>
|
|
202
|
+
/*! purgecss start ignore */
|
|
203
|
+
:root,
|
|
204
|
+
:host {
|
|
205
|
+
--ai-tooltip-bg: var(--ai-color-black);
|
|
206
|
+
--ai-tooltip-fg: var(--ai-color-white);
|
|
207
|
+
--ai-tooltip-radius: var(--ai-size-8, 0.625rem);
|
|
208
|
+
--ai-tooltip-padding: var(--ai-size-8, 0.5rem);
|
|
209
|
+
--ai-tooltip-font-size: 0.8125rem;
|
|
210
|
+
--ai-tooltip-font-weight: 500;
|
|
211
|
+
--ai-tooltip-line-height: 1.4;
|
|
212
|
+
--ai-tooltip-shadow: 0 0.25rem 0.375rem rgba(0, 0, 0, 0.3);
|
|
213
|
+
--ai-tooltip-max-width: 21.875rem;
|
|
214
|
+
--ai-tooltip-max-width-mobile: 18rem;
|
|
215
|
+
--ai-tooltip-arrow-size: 0.3125rem;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.ai-tooltip-trigger {
|
|
219
|
+
position: relative;
|
|
220
|
+
display: inline-block;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.ai-tooltip {
|
|
224
|
+
position: fixed;
|
|
225
|
+
top: 0;
|
|
226
|
+
left: 0;
|
|
227
|
+
z-index: var(--ai-layer-100);
|
|
228
|
+
display: none;
|
|
229
|
+
max-width: var(--ai-tooltip-max-width);
|
|
230
|
+
padding: var(--ai-tooltip-padding);
|
|
231
|
+
border-radius: var(--ai-tooltip-radius);
|
|
232
|
+
background-color: var(--ai-tooltip-bg);
|
|
233
|
+
box-shadow: var(--ai-tooltip-shadow);
|
|
234
|
+
color: var(--ai-tooltip-fg);
|
|
235
|
+
font-size: var(--ai-tooltip-font-size);
|
|
236
|
+
font-weight: var(--ai-tooltip-font-weight);
|
|
237
|
+
line-height: var(--ai-tooltip-line-height);
|
|
238
|
+
pointer-events: none;
|
|
239
|
+
white-space: normal;
|
|
240
|
+
word-wrap: break-word;
|
|
241
|
+
overflow-wrap: break-word;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.ai-tooltip.visible {
|
|
245
|
+
display: block;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* Arrow — JS positions it along the trigger-facing edge; here we only place
|
|
249
|
+
* it on the correct side and point it in the right direction. */
|
|
250
|
+
.ai-tooltip-arrow {
|
|
251
|
+
position: absolute;
|
|
252
|
+
width: 0;
|
|
253
|
+
height: 0;
|
|
254
|
+
border: var(--ai-tooltip-arrow-size) solid transparent;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.ai-tooltip.pos-top .ai-tooltip-arrow {
|
|
258
|
+
top: 100%;
|
|
259
|
+
transform: translateX(-50%);
|
|
260
|
+
border-top-color: var(--ai-tooltip-bg);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.ai-tooltip.pos-bottom .ai-tooltip-arrow {
|
|
264
|
+
bottom: 100%;
|
|
265
|
+
transform: translateX(-50%);
|
|
266
|
+
border-bottom-color: var(--ai-tooltip-bg);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.ai-tooltip.pos-left .ai-tooltip-arrow {
|
|
270
|
+
left: 100%;
|
|
271
|
+
transform: translateY(-50%);
|
|
272
|
+
border-left-color: var(--ai-tooltip-bg);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.ai-tooltip.pos-right .ai-tooltip-arrow {
|
|
276
|
+
right: 100%;
|
|
277
|
+
transform: translateY(-50%);
|
|
278
|
+
border-right-color: var(--ai-tooltip-bg);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* Mobile: cap width so long messages stay readable. */
|
|
282
|
+
@media (max-width: 48rem) {
|
|
283
|
+
.ai-tooltip {
|
|
284
|
+
max-width: var(--ai-tooltip-max-width-mobile);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/*! purgecss end ignore */
|
|
288
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { default as ApiDocSubNavMenuItem } from './ApiDocSubNavMenuItem.svelte';
|
|
|
15
15
|
export { default as PlatformFooter } from './PlatformFooter.svelte';
|
|
16
16
|
export { default as PlatformFooterMenu } from './PlatformFooterMenu.svelte';
|
|
17
17
|
export { default as Modal } from './Modal.svelte';
|
|
18
|
+
export { default as Tooltip } from './Tooltip.svelte';
|
|
18
19
|
|
|
19
20
|
// Icon Components
|
|
20
21
|
export * from './icons/index.js';
|