@timmo001/groundwork-ui 0.0.0-stage → 0.0.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/LICENSE +201 -0
- package/README.md +32 -2
- package/package.json +38 -4
- package/src/dismiss.ts +165 -0
- package/src/element.ts +375 -0
- package/src/field-info.ts +56 -0
- package/src/fields.ts +346 -0
- package/src/gw-alert.ts +117 -0
- package/src/gw-badge.ts +171 -0
- package/src/gw-banner.ts +120 -0
- package/src/gw-button.ts +438 -0
- package/src/gw-card.ts +187 -0
- package/src/gw-checkbox.ts +113 -0
- package/src/gw-chip.ts +122 -0
- package/src/gw-command-menu.ts +283 -0
- package/src/gw-dialog.ts +246 -0
- package/src/gw-dot.ts +89 -0
- package/src/gw-empty-state.ts +107 -0
- package/src/gw-expansion.ts +224 -0
- package/src/gw-facts.ts +141 -0
- package/src/gw-fade-in.ts +108 -0
- package/src/gw-gauge.ts +328 -0
- package/src/gw-input.ts +127 -0
- package/src/gw-menu-button.ts +386 -0
- package/src/gw-meter.ts +194 -0
- package/src/gw-notification-center.ts +155 -0
- package/src/gw-notification.ts +222 -0
- package/src/gw-option-list.ts +616 -0
- package/src/gw-page-header.ts +125 -0
- package/src/gw-popover.ts +382 -0
- package/src/gw-section-title.ts +71 -0
- package/src/gw-segmented.ts +301 -0
- package/src/gw-select.ts +361 -0
- package/src/gw-setting-row.ts +168 -0
- package/src/gw-skeleton.ts +178 -0
- package/src/gw-spinner.ts +84 -0
- package/src/gw-toast.ts +167 -0
- package/src/index.ts +76 -0
- package/src/layout.ts +182 -0
- package/src/motion.ts +105 -0
- package/src/styles.ts +122 -0
package/src/fields.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the form fields share: the light DOM base and the styles for labels,
|
|
3
|
+
* hints, errors and the select's trigger and list.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css } from "./element.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Field components render their native control in light DOM, never in a
|
|
11
|
+
* shadow root: password managers and `<label for>` can't reach across one,
|
|
12
|
+
* and a form only submits controls it can see. Their styles are adopted into
|
|
13
|
+
* whichever document or shadow root they sit in.
|
|
14
|
+
*/
|
|
15
|
+
export const fieldStyles = css`
|
|
16
|
+
:where(gw-input, gw-select, gw-switch, gw-checkbox) {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
gap: var(--gw-space-2);
|
|
20
|
+
font-family: var(--gw-font-sans);
|
|
21
|
+
color: var(--gw-color-text-primary);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:where(gw-input, gw-select) .gw-field-heading {
|
|
25
|
+
display: flex;
|
|
26
|
+
gap: var(--gw-space-1);
|
|
27
|
+
align-items: center;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:where(gw-input, gw-select) .gw-field-heading > label {
|
|
31
|
+
font-size: var(--gw-font-size-s);
|
|
32
|
+
font-weight: var(--gw-font-weight-strong);
|
|
33
|
+
color: var(--gw-color-text-strong);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* The (i) button by the label that opens a field's longer help */
|
|
37
|
+
:where(gw-input, gw-select) .gw-info {
|
|
38
|
+
display: inline-grid;
|
|
39
|
+
place-items: center;
|
|
40
|
+
width: 1.5rem;
|
|
41
|
+
height: 1.5rem;
|
|
42
|
+
padding: 0;
|
|
43
|
+
color: var(--gw-color-text-secondary);
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
background: none;
|
|
46
|
+
border: 0;
|
|
47
|
+
border-radius: var(--gw-radius-pill);
|
|
48
|
+
transition:
|
|
49
|
+
color var(--gw-motion-fast) var(--gw-motion-easing),
|
|
50
|
+
background-color var(--gw-motion-fast) var(--gw-motion-easing);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
:where(gw-input, gw-select) .gw-info:hover {
|
|
54
|
+
color: var(--gw-color-text-strong);
|
|
55
|
+
background: var(--gw-color-fill-neutral-hovered);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:where(gw-input, gw-select) .gw-info:focus-visible {
|
|
59
|
+
outline: var(--gw-focus-width) solid var(--gw-color-focus);
|
|
60
|
+
outline-offset: var(--gw-focus-offset);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
:where(gw-input, gw-select) .gw-info[hidden] {
|
|
64
|
+
display: none;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:where(gw-input, gw-select) .gw-info > .icon {
|
|
68
|
+
width: 1rem;
|
|
69
|
+
height: 1rem;
|
|
70
|
+
fill: currentColor;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:where(gw-input, gw-select) :where(input, .gw-select-trigger) {
|
|
74
|
+
width: 100%;
|
|
75
|
+
min-height: 2.5rem;
|
|
76
|
+
padding: 0 var(--gw-space-3);
|
|
77
|
+
font: inherit;
|
|
78
|
+
font-size: var(--gw-font-size-s);
|
|
79
|
+
color: var(--gw-color-text-strong);
|
|
80
|
+
background: var(--gw-color-surface-lower);
|
|
81
|
+
border: var(--gw-border-width) solid var(--gw-color-border-normal);
|
|
82
|
+
border-radius: var(--gw-radius-control);
|
|
83
|
+
transition: border-color var(--gw-motion-fast) var(--gw-motion-easing);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* The trigger shows the chosen option, with its icon, and a chevron with
|
|
87
|
+
room around it that turns over while the list is open */
|
|
88
|
+
gw-select .gw-select-trigger {
|
|
89
|
+
display: flex;
|
|
90
|
+
gap: var(--gw-space-2);
|
|
91
|
+
align-items: center;
|
|
92
|
+
padding-inline-end: var(--gw-space-3);
|
|
93
|
+
text-align: start;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
gw-select .gw-select-value {
|
|
98
|
+
flex: 1;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
overflow: hidden;
|
|
101
|
+
text-overflow: ellipsis;
|
|
102
|
+
white-space: nowrap;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* With an inline label the select sits among toolbar controls, so it's a
|
|
106
|
+
pill that fits its content, like the toggles and buttons beside it */
|
|
107
|
+
gw-select[inline-label] {
|
|
108
|
+
display: inline-flex;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
gw-select[inline-label] .gw-select-trigger {
|
|
112
|
+
width: auto;
|
|
113
|
+
padding-inline: var(--gw-space-4) var(--gw-space-3);
|
|
114
|
+
border-radius: var(--gw-radius-pill);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* An inline label reads as a quiet prefix to the value */
|
|
118
|
+
gw-select .gw-select-inline-label {
|
|
119
|
+
flex: none;
|
|
120
|
+
color: var(--gw-color-text-secondary);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
gw-select .gw-select-inline-label[hidden] {
|
|
124
|
+
display: none;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
gw-select .gw-select-value[data-placeholder] {
|
|
128
|
+
color: var(--gw-color-text-secondary);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
gw-select .gw-select-trigger > .icon {
|
|
132
|
+
flex: none;
|
|
133
|
+
width: var(--gw-icon-size, 1.25rem);
|
|
134
|
+
height: var(--gw-icon-size, 1.25rem);
|
|
135
|
+
fill: currentColor;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
gw-select .gw-select-trigger > .icon:last-child {
|
|
139
|
+
color: var(--gw-color-text-secondary);
|
|
140
|
+
transition: rotate var(--gw-motion-normal) var(--gw-motion-easing);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
gw-select[open] .gw-select-trigger > .icon:last-child {
|
|
144
|
+
rotate: 180deg;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
gw-select .gw-select-trigger > .icon[hidden] {
|
|
148
|
+
display: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* The list opens under the trigger, growing from it, or as a bottom sheet
|
|
152
|
+
sliding up on phones */
|
|
153
|
+
gw-select .gw-select-popup {
|
|
154
|
+
position: fixed;
|
|
155
|
+
inset: unset;
|
|
156
|
+
box-sizing: border-box;
|
|
157
|
+
max-width: calc(100vw - 12px);
|
|
158
|
+
padding: 0;
|
|
159
|
+
margin: 0;
|
|
160
|
+
overflow: hidden;
|
|
161
|
+
color: var(--gw-color-text-primary);
|
|
162
|
+
background: var(--gw-color-surface-overlay);
|
|
163
|
+
border: max(var(--gw-border-width), 1px) solid var(--gw-color-border-normal);
|
|
164
|
+
border-radius: var(--gw-radius-panel);
|
|
165
|
+
box-shadow: var(--gw-shadow-l);
|
|
166
|
+
opacity: 0;
|
|
167
|
+
scale: 1 0.96;
|
|
168
|
+
transform-origin: top;
|
|
169
|
+
transition:
|
|
170
|
+
opacity var(--gw-motion-fast) var(--gw-motion-easing),
|
|
171
|
+
scale var(--gw-motion-normal) var(--gw-motion-easing),
|
|
172
|
+
translate var(--gw-motion-normal) var(--gw-motion-easing),
|
|
173
|
+
display var(--gw-motion-normal) allow-discrete,
|
|
174
|
+
overlay var(--gw-motion-normal) allow-discrete;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
gw-select .gw-select-popup[data-placement="above"] {
|
|
178
|
+
transform-origin: bottom;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
gw-select .gw-select-popup:popover-open {
|
|
182
|
+
opacity: 1;
|
|
183
|
+
scale: 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@starting-style {
|
|
187
|
+
gw-select .gw-select-popup:popover-open {
|
|
188
|
+
opacity: 0;
|
|
189
|
+
scale: 1 0.96;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
gw-select .gw-select-popup[data-placement="sheet"] {
|
|
194
|
+
inset: auto 0 0;
|
|
195
|
+
width: 100%;
|
|
196
|
+
max-width: none;
|
|
197
|
+
padding-block-end: env(safe-area-inset-bottom);
|
|
198
|
+
border-end-start-radius: 0;
|
|
199
|
+
border-end-end-radius: 0;
|
|
200
|
+
scale: 1;
|
|
201
|
+
translate: 0 100%;
|
|
202
|
+
--gw-option-list-height: 60vh;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
gw-select .gw-select-popup[data-placement="sheet"]:popover-open {
|
|
206
|
+
translate: 0 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@starting-style {
|
|
210
|
+
gw-select .gw-select-popup[data-placement="sheet"]:popover-open {
|
|
211
|
+
translate: 0 100%;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
gw-select .gw-select-popup[data-placement="sheet"]::backdrop {
|
|
216
|
+
background: rgb(0 0 0 / 40%);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
:where(gw-input, gw-select) :where(input, .gw-select-trigger):hover {
|
|
220
|
+
border-color: var(--gw-color-border-loud);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
:where(gw-input, gw-select, gw-switch, gw-checkbox)
|
|
224
|
+
:where(input, .gw-select-trigger):focus-visible {
|
|
225
|
+
outline: var(--gw-focus-width) solid var(--gw-color-focus);
|
|
226
|
+
outline-offset: var(--gw-focus-offset);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
:where(gw-input, gw-select) [aria-invalid="true"] {
|
|
230
|
+
border-color: var(--gw-color-border-danger);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
:where(gw-input, gw-select, gw-switch, gw-checkbox) :disabled {
|
|
234
|
+
color: var(--gw-color-text-disabled);
|
|
235
|
+
cursor: not-allowed;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
:where(gw-input, gw-select, gw-switch, gw-checkbox) .gw-hint {
|
|
239
|
+
margin: 0;
|
|
240
|
+
font-size: var(--gw-font-size-xs);
|
|
241
|
+
color: var(--gw-color-text-secondary);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
:where(gw-input, gw-select, gw-switch, gw-checkbox) .gw-error {
|
|
245
|
+
margin: 0;
|
|
246
|
+
font-size: var(--gw-font-size-xs);
|
|
247
|
+
color: var(--gw-color-danger);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
:where(gw-switch, gw-checkbox) > label {
|
|
251
|
+
display: inline-flex;
|
|
252
|
+
gap: var(--gw-space-3);
|
|
253
|
+
align-items: center;
|
|
254
|
+
font-size: var(--gw-font-size-s);
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
:where(gw-switch, gw-checkbox, gw-select) .gw-visually-hidden {
|
|
259
|
+
position: absolute;
|
|
260
|
+
width: 1px;
|
|
261
|
+
height: 1px;
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
clip-path: inset(50%);
|
|
264
|
+
white-space: nowrap;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
gw-checkbox input {
|
|
268
|
+
width: 1.125rem;
|
|
269
|
+
height: 1.125rem;
|
|
270
|
+
margin: 0;
|
|
271
|
+
accent-color: var(--gw-color-fill-accent-resting);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
gw-switch input {
|
|
275
|
+
position: relative;
|
|
276
|
+
flex: none;
|
|
277
|
+
width: 2.5rem;
|
|
278
|
+
height: 1.5rem;
|
|
279
|
+
margin: 0;
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
appearance: none;
|
|
282
|
+
background: var(--gw-color-surface-lower);
|
|
283
|
+
border: var(--gw-border-width) solid var(--gw-color-border-loud);
|
|
284
|
+
border-radius: var(--gw-radius-pill);
|
|
285
|
+
transition:
|
|
286
|
+
background-color var(--gw-motion-normal) var(--gw-motion-easing),
|
|
287
|
+
border-color var(--gw-motion-normal) var(--gw-motion-easing);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
gw-switch input::after {
|
|
291
|
+
position: absolute;
|
|
292
|
+
inset-block-start: 0.1875rem;
|
|
293
|
+
inset-inline-start: 0.1875rem;
|
|
294
|
+
width: 1rem;
|
|
295
|
+
height: 1rem;
|
|
296
|
+
content: "";
|
|
297
|
+
background: var(--gw-color-text-secondary);
|
|
298
|
+
border-radius: 50%;
|
|
299
|
+
transition:
|
|
300
|
+
translate var(--gw-motion-normal) var(--gw-motion-easing),
|
|
301
|
+
background-color var(--gw-motion-normal) var(--gw-motion-easing);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
gw-switch input:checked {
|
|
305
|
+
background: var(--gw-color-fill-accent-resting);
|
|
306
|
+
border-color: transparent;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
gw-switch input:checked::after {
|
|
310
|
+
translate: 1rem 0;
|
|
311
|
+
background: var(--gw-color-on-accent);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
gw-switch input:disabled {
|
|
315
|
+
opacity: 0.5;
|
|
316
|
+
}
|
|
317
|
+
`;
|
|
318
|
+
|
|
319
|
+
const adoptStyles = (element: Element, sheet: CSSStyleSheet) => {
|
|
320
|
+
const root = element.getRootNode();
|
|
321
|
+
|
|
322
|
+
if (root instanceof Document || root instanceof ShadowRoot) {
|
|
323
|
+
if (!root.adoptedStyleSheets.includes(sheet)) {
|
|
324
|
+
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
let lastId = 0;
|
|
330
|
+
|
|
331
|
+
/** An id for a control, unique on the page, for labels and descriptions. */
|
|
332
|
+
export const fieldId = () => {
|
|
333
|
+
lastId += 1;
|
|
334
|
+
|
|
335
|
+
return `gw-field-${lastId}`;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/** A component that renders into itself, so its native control stays in light DOM. */
|
|
339
|
+
export class LightElement extends GwElement {
|
|
340
|
+
static override shadow = false;
|
|
341
|
+
|
|
342
|
+
override connectedCallback() {
|
|
343
|
+
super.connectedCallback();
|
|
344
|
+
adoptStyles(this, fieldStyles);
|
|
345
|
+
}
|
|
346
|
+
}
|
package/src/gw-alert.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-alert`: a message in the flow of the page.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/display/#alert
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h } from "./element.js";
|
|
8
|
+
import { base, type Tone } from "./styles.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A message in the flow of the page. `urgent` announces it at once, for
|
|
12
|
+
* errors that need attention; otherwise it's announced politely.
|
|
13
|
+
*
|
|
14
|
+
* @element gw-alert
|
|
15
|
+
* @summary A message in the flow of the page.
|
|
16
|
+
*
|
|
17
|
+
* @attr {"neutral"|"accent"|"info"|"success"|"warning"|"danger"|"important"} tone - The color and default icon. Defaults to `info`.
|
|
18
|
+
* @attr {string} heading - A heading above the message.
|
|
19
|
+
* @attr {boolean} urgent - Announces it at once.
|
|
20
|
+
*
|
|
21
|
+
* @slot - The message.
|
|
22
|
+
* @slot icon - An icon in place of the tone's.
|
|
23
|
+
* @slot actions - Buttons beside the message.
|
|
24
|
+
*
|
|
25
|
+
* @csspart alert - The alert.
|
|
26
|
+
*/
|
|
27
|
+
export class GwAlert extends GwElement {
|
|
28
|
+
static override properties = {
|
|
29
|
+
tone: { reflect: true },
|
|
30
|
+
heading: {},
|
|
31
|
+
urgent: { type: "boolean" },
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
static override styles = [
|
|
35
|
+
base,
|
|
36
|
+
css`
|
|
37
|
+
:host {
|
|
38
|
+
display: block;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.alert {
|
|
42
|
+
display: flex;
|
|
43
|
+
gap: var(--gw-space-2);
|
|
44
|
+
align-items: flex-start;
|
|
45
|
+
padding: var(--gw-space-2) var(--gw-space-3);
|
|
46
|
+
font-size: var(--gw-font-size-s);
|
|
47
|
+
color: var(--gw-color-text-primary);
|
|
48
|
+
background: var(--gw-tone-fill, var(--gw-color-fill-info-quiet));
|
|
49
|
+
border: var(--gw-border-width) solid
|
|
50
|
+
var(--gw-tone-border, var(--gw-color-border-info));
|
|
51
|
+
border-inline-start-width: calc(var(--gw-border-width) * 4);
|
|
52
|
+
border-radius: var(--gw-radius-control);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
::slotted(p) {
|
|
56
|
+
margin: 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
::slotted([slot="actions"]) {
|
|
60
|
+
align-self: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
::slotted([slot="icon"]) {
|
|
64
|
+
color: var(--gw-tone, var(--gw-color-info));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.body {
|
|
68
|
+
flex: 1;
|
|
69
|
+
min-width: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
strong {
|
|
73
|
+
display: block;
|
|
74
|
+
font-weight: var(--gw-font-weight-strong);
|
|
75
|
+
color: var(--gw-color-text-strong);
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
declare tone: Tone;
|
|
81
|
+
declare heading: string;
|
|
82
|
+
declare urgent: boolean;
|
|
83
|
+
|
|
84
|
+
readonly #alert = h("div", { class: "alert", part: "alert" });
|
|
85
|
+
|
|
86
|
+
readonly #heading = h("strong");
|
|
87
|
+
|
|
88
|
+
constructor() {
|
|
89
|
+
super();
|
|
90
|
+
this.tone = "info";
|
|
91
|
+
this.heading = "";
|
|
92
|
+
this.urgent = false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
protected override create(root: ShadowRoot) {
|
|
96
|
+
this.#alert.append(
|
|
97
|
+
h("slot", { name: "icon" }),
|
|
98
|
+
h("div", { class: "body" }, this.#heading, h("slot")),
|
|
99
|
+
h("slot", { name: "actions" }),
|
|
100
|
+
);
|
|
101
|
+
root.append(this.#alert);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
protected override update() {
|
|
105
|
+
this.#alert.setAttribute("role", this.urgent ? "alert" : "status");
|
|
106
|
+
this.#heading.textContent = this.heading;
|
|
107
|
+
this.#heading.hidden = !this.heading;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
GwAlert.define("gw-alert");
|
|
112
|
+
|
|
113
|
+
declare global {
|
|
114
|
+
interface HTMLElementTagNameMap {
|
|
115
|
+
"gw-alert": GwAlert;
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/gw-badge.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gw-badge`: a pill with an icon, a value and an optional label.
|
|
3
|
+
* Docs: https://groundwork.timmo.dev/components/display/#badge
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { GwElement, css, h, setAttribute } from "./element.js";
|
|
8
|
+
import "./gw-skeleton.js";
|
|
9
|
+
import { fadeIn } from "./motion.js";
|
|
10
|
+
import { base, createIcon, setIconPath, type Tone } from "./styles.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A pill with an icon, a value and an optional small label above it. The
|
|
14
|
+
* icon takes the `tone`'s color. While `loading`, a skeleton stands in for
|
|
15
|
+
* the value, and for the icon if it has none yet, and the value fades in
|
|
16
|
+
* once it's loaded.
|
|
17
|
+
*
|
|
18
|
+
* @element gw-badge
|
|
19
|
+
* @summary A pill with an icon, a value and an optional label.
|
|
20
|
+
*
|
|
21
|
+
* @attr {string} icon - SVG path data for the icon.
|
|
22
|
+
* @attr {string} label - A small label above the value.
|
|
23
|
+
* @attr {"neutral"|"accent"|"info"|"success"|"warning"|"danger"|"important"} tone - The icon's color. Defaults to `accent`.
|
|
24
|
+
* @attr {"small"|"medium"} size - The size. Defaults to `medium`.
|
|
25
|
+
* @attr {boolean} loading - Shows a skeleton for the value, and the icon if it has none.
|
|
26
|
+
*
|
|
27
|
+
* @slot - The value.
|
|
28
|
+
*
|
|
29
|
+
* @csspart badge - The badge.
|
|
30
|
+
*/
|
|
31
|
+
export class GwBadge extends GwElement {
|
|
32
|
+
static override properties = {
|
|
33
|
+
icon: {},
|
|
34
|
+
label: {},
|
|
35
|
+
tone: { reflect: true },
|
|
36
|
+
size: { reflect: true },
|
|
37
|
+
loading: { type: "boolean", reflect: true },
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
static override styles = [
|
|
41
|
+
base,
|
|
42
|
+
css`
|
|
43
|
+
:host {
|
|
44
|
+
display: inline-flex;
|
|
45
|
+
max-width: 100%;
|
|
46
|
+
vertical-align: middle;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* 36px tall, 12px padding, 8px between the 18px icon and the text */
|
|
50
|
+
.badge {
|
|
51
|
+
--gw-icon-size: 1.125rem;
|
|
52
|
+
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
gap: var(--gw-space-2);
|
|
55
|
+
align-items: center;
|
|
56
|
+
box-sizing: border-box;
|
|
57
|
+
max-width: 100%;
|
|
58
|
+
height: 2.25rem;
|
|
59
|
+
padding: 0 var(--gw-space-4) 0 var(--gw-space-3);
|
|
60
|
+
vertical-align: middle;
|
|
61
|
+
color: var(--gw-color-text-strong);
|
|
62
|
+
background: var(--gw-color-surface-raised);
|
|
63
|
+
border: var(--gw-border-width) solid var(--gw-color-border-normal);
|
|
64
|
+
border-radius: var(--gw-radius-pill);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:host([size="small"]) .badge {
|
|
68
|
+
--gw-icon-size: 1rem;
|
|
69
|
+
|
|
70
|
+
gap: var(--gw-space-1);
|
|
71
|
+
height: 1.75rem;
|
|
72
|
+
padding: 0 var(--gw-space-3) 0 var(--gw-space-2);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.icon {
|
|
76
|
+
color: var(--gw-tone, var(--gw-color-accent));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.info {
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
min-width: 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.label {
|
|
86
|
+
font-size: var(--gw-font-size-2xs);
|
|
87
|
+
line-height: 1.1;
|
|
88
|
+
color: var(--gw-color-text-secondary);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.content {
|
|
92
|
+
font-size: var(--gw-font-size-xs);
|
|
93
|
+
font-weight: var(--gw-font-weight-strong);
|
|
94
|
+
line-height: 1.2;
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.value {
|
|
99
|
+
display: inline-block;
|
|
100
|
+
}
|
|
101
|
+
`,
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
declare icon: string;
|
|
105
|
+
declare label: string;
|
|
106
|
+
declare tone: Tone;
|
|
107
|
+
declare size: "small" | "medium";
|
|
108
|
+
declare loading: boolean;
|
|
109
|
+
|
|
110
|
+
readonly #badge = h("span", { class: "badge", part: "badge" });
|
|
111
|
+
|
|
112
|
+
readonly #icon = createIcon("");
|
|
113
|
+
|
|
114
|
+
readonly #iconSkeleton = h("gw-skeleton-icon");
|
|
115
|
+
|
|
116
|
+
readonly #label = h("span", { class: "label" });
|
|
117
|
+
|
|
118
|
+
readonly #skeleton = h("gw-skeleton-text", {
|
|
119
|
+
style: "--gw-skeleton-width: 3rem",
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
readonly #value = h("span", { class: "value" }, h("slot"));
|
|
123
|
+
|
|
124
|
+
constructor() {
|
|
125
|
+
super();
|
|
126
|
+
this.icon = "";
|
|
127
|
+
this.label = "";
|
|
128
|
+
this.tone = "accent";
|
|
129
|
+
this.size = "medium";
|
|
130
|
+
this.loading = false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
protected override create(root: ShadowRoot) {
|
|
134
|
+
this.#badge.append(
|
|
135
|
+
this.#icon,
|
|
136
|
+
this.#iconSkeleton,
|
|
137
|
+
h(
|
|
138
|
+
"span",
|
|
139
|
+
{ class: "info" },
|
|
140
|
+
this.#label,
|
|
141
|
+
h("span", { class: "content" }, this.#skeleton, this.#value),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
root.append(this.#badge);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
protected override update(changed: ReadonlySet<string>) {
|
|
148
|
+
const wasLoading = this.#value.hidden;
|
|
149
|
+
|
|
150
|
+
setAttribute(this.#badge, "aria-busy", this.loading ? "true" : null);
|
|
151
|
+
setIconPath(this.#icon, this.icon);
|
|
152
|
+
this.#icon.style.display = this.icon ? "" : "none";
|
|
153
|
+
this.#iconSkeleton.hidden = this.icon !== "" || !this.loading;
|
|
154
|
+
this.#label.textContent = this.label;
|
|
155
|
+
this.#label.hidden = !this.label;
|
|
156
|
+
this.#skeleton.hidden = !this.loading;
|
|
157
|
+
this.#value.hidden = this.loading;
|
|
158
|
+
|
|
159
|
+
if (changed.has("loading") && wasLoading && !this.loading) {
|
|
160
|
+
fadeIn(this.#value);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
GwBadge.define("gw-badge");
|
|
166
|
+
|
|
167
|
+
declare global {
|
|
168
|
+
interface HTMLElementTagNameMap {
|
|
169
|
+
"gw-badge": GwBadge;
|
|
170
|
+
}
|
|
171
|
+
}
|