@gsa-tts/graymatter-ui 0.5.1 → 0.6.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/README.md +3 -0
- package/dist/graymatter-ui.js +502 -491
- package/dist/graymatter-ui.umd.cjs +9 -7
- package/package.json +1 -1
- package/src/components/Button.svelte +14 -0
- package/src/components/Modal.svelte +374 -0
- package/src/components/index.ts +1 -0
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - href?: string (optional - renders as <a> if provided)
|
|
11
11
|
* - disabled?: boolean (default: false)
|
|
12
12
|
* - type?: 'button' | 'submit' | 'reset' (default: 'button')
|
|
13
|
+
* - hasBoxShadow?: boolean (default: true) - Whether to show box shadow on square buttons
|
|
13
14
|
*
|
|
14
15
|
* Examples:
|
|
15
16
|
* - Round primary button: <Button>Click me</Button>
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
* - Small button (any shape): <Button size="small">Small Button</Button>
|
|
18
19
|
* - Small square button: <Button variant="secondary" shape="square" size="small">Small Square</Button>
|
|
19
20
|
* - Inverted square button: <Button theme="inverted" shape="square">Inverted Square</Button>
|
|
21
|
+
* - Button without shadow: <Button shape="square" hasBoxShadow={false}>No Shadow</Button>
|
|
20
22
|
*/
|
|
21
23
|
interface Props {
|
|
22
24
|
variant?: 'primary' | 'secondary' | 'unstyled';
|
|
@@ -27,6 +29,7 @@
|
|
|
27
29
|
size?: 'small' | 'medium' | 'large';
|
|
28
30
|
type?: 'button' | 'submit' | 'reset';
|
|
29
31
|
onclick?: (event: MouseEvent) => void;
|
|
32
|
+
hasBoxShadow?: boolean;
|
|
30
33
|
class?: string;
|
|
31
34
|
children?: import('svelte').Snippet;
|
|
32
35
|
[key: `data-${string}`]: string;
|
|
@@ -42,6 +45,7 @@
|
|
|
42
45
|
onclick,
|
|
43
46
|
size = 'medium',
|
|
44
47
|
type = 'button',
|
|
48
|
+
hasBoxShadow = true,
|
|
45
49
|
class: className = '',
|
|
46
50
|
children,
|
|
47
51
|
// eslint-disable-next-line svelte/valid-compile
|
|
@@ -63,6 +67,7 @@
|
|
|
63
67
|
variant === 'secondary' ? 'usa-button--outline ai-button--outline' : '',
|
|
64
68
|
variant === 'unstyled' ? 'ai-button--unstyled' : '',
|
|
65
69
|
shape === 'square' ? 'ai-button--square' : '',
|
|
70
|
+
!hasBoxShadow ? 'ai-button--no-shadow' : '',
|
|
66
71
|
]
|
|
67
72
|
.filter(Boolean)
|
|
68
73
|
.join(' ');
|
|
@@ -387,5 +392,14 @@
|
|
|
387
392
|
color: inherit;
|
|
388
393
|
opacity: 0.75;
|
|
389
394
|
}
|
|
395
|
+
|
|
396
|
+
/* No shadow modifier */
|
|
397
|
+
.ai-button--no-shadow {
|
|
398
|
+
box-shadow: none !important;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.ai-button--no-shadow:is(:hover, :focus) {
|
|
402
|
+
box-shadow: none !important;
|
|
403
|
+
}
|
|
390
404
|
/*! purgecss end ignore */
|
|
391
405
|
</style>
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Modal Component
|
|
4
|
+
*
|
|
5
|
+
* A flexible modal component that can be used for dialogs, confirmations, and custom overlays.
|
|
6
|
+
*
|
|
7
|
+
* Props:
|
|
8
|
+
* - open: boolean - Controls whether the modal is visible
|
|
9
|
+
* - onClose?: () => void - Callback when modal should close (backdrop click or ESC key)
|
|
10
|
+
* - closeOnBackdropClick?: boolean - Whether clicking backdrop closes modal (default: true)
|
|
11
|
+
* - closeOnEscape?: boolean - Whether ESC key closes modal (default: true)
|
|
12
|
+
* - size?: 'small' | 'medium' | 'large' - Modal width (default: 'medium')
|
|
13
|
+
* - showCloseButton?: boolean - Whether to show X close button (default: true)
|
|
14
|
+
* - centered?: boolean - Center content text (default: true)
|
|
15
|
+
* - icon?: Snippet - Optional icon to display at top
|
|
16
|
+
* - title?: string - Optional title text
|
|
17
|
+
* - content?: string[] - Array of paragraph texts to display
|
|
18
|
+
* - buttons?: Array<ButtonConfig> - Optional buttons to display
|
|
19
|
+
* - children?: Snippet - Custom content (overrides title/content/buttons if provided)
|
|
20
|
+
* - class?: string - Additional CSS classes for the modal
|
|
21
|
+
*
|
|
22
|
+
* Example (Simple):
|
|
23
|
+
* ```svelte
|
|
24
|
+
* <Modal
|
|
25
|
+
* open={isOpen}
|
|
26
|
+
* title="Delete Item"
|
|
27
|
+
* content={["This action cannot be undone.", "Are you sure?"]}
|
|
28
|
+
* buttons={[
|
|
29
|
+
* { text: 'Delete', variant: 'primary', onclick: handleDelete },
|
|
30
|
+
* { text: 'Cancel', variant: 'secondary', onclick: () => isOpen = false }
|
|
31
|
+
* ]}
|
|
32
|
+
* />
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Example (Custom):
|
|
36
|
+
* ```svelte
|
|
37
|
+
* <Modal open={isOpen} onClose={() => isOpen = false}>
|
|
38
|
+
* <h2>Custom Content</h2>
|
|
39
|
+
* <p>Your custom HTML here</p>
|
|
40
|
+
* </Modal>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
import { CloseIcon } from './icons/index.js';
|
|
44
|
+
import Button from './Button.svelte';
|
|
45
|
+
|
|
46
|
+
interface ButtonConfig {
|
|
47
|
+
text: string;
|
|
48
|
+
variant?: 'primary' | 'secondary' | 'unstyled';
|
|
49
|
+
onclick: (event: MouseEvent) => void;
|
|
50
|
+
class?: string;
|
|
51
|
+
disabled?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface Props {
|
|
55
|
+
open: boolean;
|
|
56
|
+
onClose?: () => void;
|
|
57
|
+
closeOnBackdropClick?: boolean;
|
|
58
|
+
closeOnEscape?: boolean;
|
|
59
|
+
size?: 'small' | 'medium' | 'large';
|
|
60
|
+
showCloseButton?: boolean;
|
|
61
|
+
centered?: boolean;
|
|
62
|
+
icon?: import('svelte').Snippet;
|
|
63
|
+
title?: string;
|
|
64
|
+
content?: string[];
|
|
65
|
+
buttons?: ButtonConfig[];
|
|
66
|
+
children?: import('svelte').Snippet;
|
|
67
|
+
class?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let {
|
|
71
|
+
open = $bindable(),
|
|
72
|
+
onClose,
|
|
73
|
+
closeOnBackdropClick = true,
|
|
74
|
+
closeOnEscape = true,
|
|
75
|
+
size = 'medium',
|
|
76
|
+
showCloseButton = true,
|
|
77
|
+
centered = true,
|
|
78
|
+
icon,
|
|
79
|
+
title,
|
|
80
|
+
content,
|
|
81
|
+
buttons,
|
|
82
|
+
children,
|
|
83
|
+
class: className = '',
|
|
84
|
+
}: Props = $props();
|
|
85
|
+
|
|
86
|
+
function handleBackdropClick(event: MouseEvent) {
|
|
87
|
+
if (closeOnBackdropClick && event.target === event.currentTarget) {
|
|
88
|
+
open = false;
|
|
89
|
+
onClose?.();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
94
|
+
if (closeOnEscape && event.key === 'Escape') {
|
|
95
|
+
open = false;
|
|
96
|
+
onClose?.();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function handleCloseClick() {
|
|
101
|
+
open = false;
|
|
102
|
+
onClose?.();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Add/remove body scroll lock when modal is open
|
|
106
|
+
$effect(() => {
|
|
107
|
+
if (open) {
|
|
108
|
+
document.body.style.overflow = 'hidden';
|
|
109
|
+
return () => {
|
|
110
|
+
document.body.style.overflow = '';
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
117
|
+
|
|
118
|
+
{#if open}
|
|
119
|
+
<div
|
|
120
|
+
class="ai-modal-backdrop"
|
|
121
|
+
onclick={handleBackdropClick}
|
|
122
|
+
role="presentation"
|
|
123
|
+
>
|
|
124
|
+
<div
|
|
125
|
+
class="ai-modal ai-modal--{size} {className}"
|
|
126
|
+
class:ai-modal--centered={centered}
|
|
127
|
+
role="dialog"
|
|
128
|
+
aria-modal="true"
|
|
129
|
+
>
|
|
130
|
+
{#if showCloseButton}
|
|
131
|
+
<button
|
|
132
|
+
class="ai-modal__close"
|
|
133
|
+
onclick={handleCloseClick}
|
|
134
|
+
aria-label="Close modal"
|
|
135
|
+
type="button"
|
|
136
|
+
>
|
|
137
|
+
<CloseIcon />
|
|
138
|
+
</button>
|
|
139
|
+
{/if}
|
|
140
|
+
<div class="ai-modal__content">
|
|
141
|
+
{#if children}
|
|
142
|
+
{@render children()}
|
|
143
|
+
{:else}
|
|
144
|
+
<div class="ai-modal__structured-content">
|
|
145
|
+
{#if icon}
|
|
146
|
+
<div class="ai-modal__icon-container">
|
|
147
|
+
{@render icon()}
|
|
148
|
+
</div>
|
|
149
|
+
{/if}
|
|
150
|
+
|
|
151
|
+
{#if title}
|
|
152
|
+
<h2 class="ai-modal__title">
|
|
153
|
+
{title}
|
|
154
|
+
</h2>
|
|
155
|
+
{/if}
|
|
156
|
+
|
|
157
|
+
{#if content && content.length > 0}
|
|
158
|
+
<div class="ai-modal__text">
|
|
159
|
+
{#each content as paragraph}
|
|
160
|
+
<p class="ai-modal__paragraph">
|
|
161
|
+
{paragraph}
|
|
162
|
+
</p>
|
|
163
|
+
{/each}
|
|
164
|
+
</div>
|
|
165
|
+
{/if}
|
|
166
|
+
|
|
167
|
+
{#if buttons && buttons.length > 0}
|
|
168
|
+
<div class="ai-modal__actions">
|
|
169
|
+
{#each buttons as button}
|
|
170
|
+
<Button
|
|
171
|
+
variant={button.variant || 'primary'}
|
|
172
|
+
shape="square"
|
|
173
|
+
onclick={button.onclick}
|
|
174
|
+
disabled={button.disabled}
|
|
175
|
+
hasBoxShadow={false}
|
|
176
|
+
class="ai-modal__button {button.class || ''}"
|
|
177
|
+
>
|
|
178
|
+
{button.text}
|
|
179
|
+
</Button>
|
|
180
|
+
{/each}
|
|
181
|
+
</div>
|
|
182
|
+
{/if}
|
|
183
|
+
</div>
|
|
184
|
+
{/if}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
{/if}
|
|
189
|
+
|
|
190
|
+
<style>
|
|
191
|
+
/*! purgecss start ignore */
|
|
192
|
+
:root,
|
|
193
|
+
:host {
|
|
194
|
+
--ai-modal-backdrop-bg: rgba(0, 0, 0, 0.5);
|
|
195
|
+
--ai-modal-bg: var(--ai-color-white);
|
|
196
|
+
--ai-modal-border-radius: 1.0625rem;
|
|
197
|
+
--ai-modal-border: 0.0625rem solid var(--ai-color-steel-200);
|
|
198
|
+
--ai-modal-padding: 2rem;
|
|
199
|
+
--ai-modal-gap: 1.5rem;
|
|
200
|
+
--ai-modal-content-width: 22.4375rem;
|
|
201
|
+
--ai-modal-width: 26.4375rem; /* 22.4375rem content + 2rem padding * 2 */
|
|
202
|
+
--ai-modal-max-width-small: 25rem;
|
|
203
|
+
--ai-modal-max-width-large: 50rem;
|
|
204
|
+
--ai-modal-shadow: 0 0.1875rem 1.25rem 0.0625rem rgba(50, 54, 61, 0.15);
|
|
205
|
+
--ai-modal-close-size: var(--ai-size-32, 2rem);
|
|
206
|
+
--ai-modal-close-top: var(--ai-size-16, 1rem);
|
|
207
|
+
--ai-modal-close-right: var(--ai-size-16, 1rem);
|
|
208
|
+
|
|
209
|
+
/* Typography tokens */
|
|
210
|
+
--ai-modal-title-size: var(--ai-size-20, 1.25rem);
|
|
211
|
+
--ai-modal-title-weight: var(--ai-font-weight-semibold, 600);
|
|
212
|
+
--ai-modal-text-size: 1rem;
|
|
213
|
+
--ai-modal-text-line-height: 1.3;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.ai-modal-backdrop {
|
|
217
|
+
position: fixed;
|
|
218
|
+
inset: 0;
|
|
219
|
+
background-color: var(--ai-modal-backdrop-bg);
|
|
220
|
+
display: flex;
|
|
221
|
+
align-items: center;
|
|
222
|
+
justify-content: center;
|
|
223
|
+
padding: var(--ai-size-16);
|
|
224
|
+
z-index: 9999;
|
|
225
|
+
animation: fadeIn 0.2s ease-out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@keyframes fadeIn {
|
|
229
|
+
from {
|
|
230
|
+
opacity: 0;
|
|
231
|
+
}
|
|
232
|
+
to {
|
|
233
|
+
opacity: 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.ai-modal {
|
|
238
|
+
position: relative;
|
|
239
|
+
display: inline-flex;
|
|
240
|
+
padding: var(--ai-modal-padding);
|
|
241
|
+
flex-direction: column;
|
|
242
|
+
align-items: flex-start;
|
|
243
|
+
gap: var(--ai-modal-gap);
|
|
244
|
+
background-color: var(--ai-modal-bg);
|
|
245
|
+
border-radius: var(--ai-modal-border-radius);
|
|
246
|
+
border: var(--ai-modal-border);
|
|
247
|
+
box-shadow: var(--ai-modal-shadow);
|
|
248
|
+
max-height: calc(100vh - var(--ai-size-32, 2rem));
|
|
249
|
+
overflow-y: auto;
|
|
250
|
+
animation: slideUp 0.3s ease-out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@keyframes slideUp {
|
|
254
|
+
from {
|
|
255
|
+
transform: translateY(var(--ai-size-32));
|
|
256
|
+
opacity: 0;
|
|
257
|
+
}
|
|
258
|
+
to {
|
|
259
|
+
transform: translateY(0);
|
|
260
|
+
opacity: 1;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.ai-modal--small {
|
|
265
|
+
max-width: var(--ai-modal-max-width-small);
|
|
266
|
+
width: 100%;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.ai-modal--medium {
|
|
270
|
+
width: var(--ai-modal-width);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.ai-modal--large {
|
|
274
|
+
max-width: var(--ai-modal-max-width-large);
|
|
275
|
+
width: 100%;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.ai-modal__close {
|
|
279
|
+
position: absolute;
|
|
280
|
+
top: var(--ai-modal-close-top);
|
|
281
|
+
right: var(--ai-modal-close-right);
|
|
282
|
+
width: var(--ai-modal-close-size);
|
|
283
|
+
height: var(--ai-modal-close-size);
|
|
284
|
+
padding: var(--ai-size-6);
|
|
285
|
+
background: transparent;
|
|
286
|
+
border: none;
|
|
287
|
+
cursor: pointer;
|
|
288
|
+
display: flex;
|
|
289
|
+
align-items: center;
|
|
290
|
+
justify-content: center;
|
|
291
|
+
border-radius: var(--ai-size-4);
|
|
292
|
+
transition: background-color 0.2s ease;
|
|
293
|
+
color: var(--ai-color-neutral-600);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.ai-modal__close:hover {
|
|
297
|
+
background-color: var(--ai-color-neutral-100);
|
|
298
|
+
color: var(--ai-color-black);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.ai-modal__close:focus {
|
|
302
|
+
outline: 0.125rem solid var(--ai-color-black);
|
|
303
|
+
outline-offset: 0.125rem;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.ai-modal__content {
|
|
307
|
+
width: 100%;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.ai-modal--centered .ai-modal__content {
|
|
311
|
+
text-align: center;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* Structured content layout */
|
|
315
|
+
.ai-modal__structured-content {
|
|
316
|
+
display: flex;
|
|
317
|
+
flex-direction: column;
|
|
318
|
+
align-items: flex-start;
|
|
319
|
+
gap: var(--ai-modal-gap);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.ai-modal--centered .ai-modal__structured-content {
|
|
323
|
+
align-items: center;
|
|
324
|
+
text-align: center;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* Icon container - just for layout, no styling */
|
|
328
|
+
.ai-modal__icon-container {
|
|
329
|
+
display: flex;
|
|
330
|
+
align-items: center;
|
|
331
|
+
justify-content: center;
|
|
332
|
+
flex-shrink: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* Title */
|
|
336
|
+
.ai-modal__title {
|
|
337
|
+
font-size: var(--ai-modal-title-size);
|
|
338
|
+
font-weight: var(--ai-modal-title-weight);
|
|
339
|
+
margin: 0;
|
|
340
|
+
color: var(--ai-color-black, #000);
|
|
341
|
+
line-height: 1.3;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* Text content */
|
|
345
|
+
.ai-modal__text {
|
|
346
|
+
display: flex;
|
|
347
|
+
flex-direction: column;
|
|
348
|
+
gap: calc(var(--ai-modal-gap) / 2);
|
|
349
|
+
width: 100%;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.ai-modal__paragraph {
|
|
353
|
+
font-size: var(--ai-modal-text-size);
|
|
354
|
+
font-style: normal;
|
|
355
|
+
font-weight: 400;
|
|
356
|
+
line-height: var(--ai-modal-text-line-height);
|
|
357
|
+
margin: 0;
|
|
358
|
+
color: var(--ai-color-black);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Actions */
|
|
362
|
+
.ai-modal__actions {
|
|
363
|
+
display: flex;
|
|
364
|
+
flex-direction: column;
|
|
365
|
+
gap: var(--ai-size-12, 0.75rem);
|
|
366
|
+
width: 100%;
|
|
367
|
+
margin-top: calc(var(--ai-modal-gap) / 2);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.ai-modal__button {
|
|
371
|
+
width: 100%;
|
|
372
|
+
}
|
|
373
|
+
/*! purgecss end ignore */
|
|
374
|
+
</style>
|
package/src/components/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { default as ApiDocSubNavList } from './ApiDocSubNavList.svelte';
|
|
|
14
14
|
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
|
+
export { default as Modal } from './Modal.svelte';
|
|
17
18
|
|
|
18
19
|
// Icon Components
|
|
19
20
|
export * from './icons/index.js';
|