@crowdstrike/glide-core 0.30.1 → 0.31.1
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/dropdown.js +2 -2
- package/dist/menu.js +35 -55
- package/dist/option.styles.js +3 -1
- package/dist/options.group.d.ts +30 -0
- package/dist/options.group.js +84 -0
- package/dist/options.group.styles.d.ts +2 -0
- package/dist/options.group.styles.js +31 -0
- package/dist/options.js +2 -1
- package/package.json +3 -3
package/dist/dropdown.js
CHANGED
@@ -1556,8 +1556,8 @@ let Dropdown = class Dropdown extends LitElement {
|
|
1556
1556
|
// Prevent page scroll. When filterable, also prevent the insertion point from
|
1557
1557
|
// moving to the beginning of the field.
|
1558
1558
|
event.preventDefault();
|
1559
|
-
const firstOption =
|
1560
|
-
.
|
1559
|
+
const firstOption = this.#optionElementsNotHiddenIncludingSelectAll
|
1560
|
+
.toReversed()
|
1561
1561
|
.findLast((option) => !option.disabled);
|
1562
1562
|
if (firstOption) {
|
1563
1563
|
this.#previouslyActiveOption = this.activeOption;
|
package/dist/menu.js
CHANGED
@@ -13,6 +13,7 @@ import { customElement, property } from 'lit/decorators.js';
|
|
13
13
|
import packageJson from '../package.json' with { type: 'json' };
|
14
14
|
import { LocalizeController } from './library/localize.js';
|
15
15
|
import Options from './options.js';
|
16
|
+
import OptionsGroup from './options.group.js';
|
16
17
|
import Option from './option.js';
|
17
18
|
import Input from './input.js';
|
18
19
|
import assertSlot from './library/assert-slot.js';
|
@@ -54,7 +55,7 @@ let Menu = class Menu extends LitElement {
|
|
54
55
|
// `#onComponentFocusOut()` to decide if Menu should close. Also used in
|
55
56
|
// `#onTargetAndDefaultSlotKeyDown()` to decide if we need to move focus.
|
56
57
|
this.#hasVoiceOverMovedFocusToOptionsOrAnOption = false;
|
57
|
-
// Set in `#
|
58
|
+
// Set in `#onDefaultSlotMouseUp()`. Used in `#onDocumentClick()` to guard against
|
58
59
|
// Menu closing when any number of things that are not an Option are clicked. Those
|
59
60
|
// "click" events will be retargeted to Menu's host the moment they bubble out of
|
60
61
|
// Menu. So checking in `#onDocumentClick()` if the click's `event.target` came
|
@@ -67,8 +68,8 @@ let Menu = class Menu extends LitElement {
|
|
67
68
|
// used in `connectedCallback()` to guard against listening for document clicks for
|
68
69
|
// sub-Menus.
|
69
70
|
this.#isSubMenuOpen = false;
|
70
|
-
// Set in `#
|
71
|
-
// `#onDocumentClick()`:
|
71
|
+
// Similar situation as with `isDefaultSlotClick`. Set in `#onTargetSlotClick()`
|
72
|
+
// and `#onDocumentClick()`. Used in `#onDocumentClick()`:
|
72
73
|
//
|
73
74
|
// 1. Menu is open.
|
74
75
|
// 2. User clicks Menu's target.
|
@@ -76,19 +77,9 @@ let Menu = class Menu extends LitElement {
|
|
76
77
|
// 4. `#onTargetSlotClick()` sets `open` to true`.
|
77
78
|
// 5. Menu never closes.
|
78
79
|
//
|
79
|
-
// Setting `#
|
80
|
+
// Setting `#isTargetSlotClick` to `true` in `#onTargetSlotClick()` gives
|
80
81
|
// `#onDocumentClick()` the information it needs to not set `open` to `false`.
|
81
|
-
|
82
|
-
// The normal approach would be to set an `#isTargetSlotClick` property in
|
83
|
-
// `#onTargetSlotClick()`. But `#onDocumentClick()` listens for "click" events in
|
84
|
-
// their capture phase. So `#onDocumentClick()` would be called before
|
85
|
-
// `#onTargetSlotClick()`.
|
86
|
-
//
|
87
|
-
// Note too that `#onDocumentClick()` sets `#isTargetSlotMouseUp` to `false`
|
88
|
-
// instead of `#onTargetSlotClick()` doing it. That's so `#isTargetSlotMouseUp`
|
89
|
-
// is set to `false` even if the user mouses down on Menu's target then moves the
|
90
|
-
// mouse outside of Menu before mousing up.
|
91
|
-
this.#isTargetSlotMouseUp = false;
|
82
|
+
this.#isTargetSlotClick = false;
|
92
83
|
this.#localize = new LocalizeController(this);
|
93
84
|
this.#targetSlotElementRef = createRef();
|
94
85
|
// An arrow function field instead of a method so `this` is closed over and set to
|
@@ -98,8 +89,8 @@ let Menu = class Menu extends LitElement {
|
|
98
89
|
this.#isDefaultSlotClick = false;
|
99
90
|
return;
|
100
91
|
}
|
101
|
-
if (this.#
|
102
|
-
this.#
|
92
|
+
if (this.#isTargetSlotClick) {
|
93
|
+
this.#isTargetSlotClick = false;
|
103
94
|
return;
|
104
95
|
}
|
105
96
|
if (this.#optionsElement) {
|
@@ -175,20 +166,7 @@ let Menu = class Menu extends LitElement {
|
|
175
166
|
// easier to understand because developers don't have to think about sub-Menus when
|
176
167
|
// looking at `#onDocumentClick()`.
|
177
168
|
if (!this.#isSubMenu) {
|
178
|
-
|
179
|
-
// 2. The user clicks that element.
|
180
|
-
// 3. The handler is called. It sets `open` to `true`.
|
181
|
-
// 4. The "click" bubbles up and is handled by `#onDocumentClick()`.
|
182
|
-
// 5. `#onDocumentClick()` sets `open` to `false` because the click came from
|
183
|
-
// outside Menu.
|
184
|
-
// 6. Menu is opened then immediately closed and so never opens.
|
185
|
-
//
|
186
|
-
// `capture` ensures `#onDocumentClick()` is called before #3, so that `open` set
|
187
|
-
// `true` in the consumer's handler isn't overwritten by `#onDocumentClick()`
|
188
|
-
// handler setting it to `false`.
|
189
|
-
document.addEventListener('click', this.#onDocumentClick, {
|
190
|
-
capture: true,
|
191
|
-
});
|
169
|
+
document.addEventListener('click', this.#onDocumentClick);
|
192
170
|
}
|
193
171
|
}
|
194
172
|
createRenderRoot() {
|
@@ -197,9 +175,7 @@ let Menu = class Menu extends LitElement {
|
|
197
175
|
}
|
198
176
|
disconnectedCallback() {
|
199
177
|
super.disconnectedCallback();
|
200
|
-
document.removeEventListener('click', this.#onDocumentClick
|
201
|
-
capture: true,
|
202
|
-
});
|
178
|
+
document.removeEventListener('click', this.#onDocumentClick);
|
203
179
|
}
|
204
180
|
firstUpdated() {
|
205
181
|
if (this.#optionsElement && this.#targetElement) {
|
@@ -293,7 +269,6 @@ let Menu = class Menu extends LitElement {
|
|
293
269
|
name="target"
|
294
270
|
@click=${this.#onTargetSlotClick}
|
295
271
|
@keydown=${this.#onTargetAndDefaultSlotKeyDown}
|
296
|
-
@mouseup=${this.#onTargetSlotMouseUp}
|
297
272
|
@input=${this.#onTargetSlotInput}
|
298
273
|
@slotchange=${this.#onTargetSlotChange}
|
299
274
|
${assertSlot([Element])}
|
@@ -341,7 +316,7 @@ let Menu = class Menu extends LitElement {
|
|
341
316
|
// `#onComponentFocusOut()` to decide if Menu should close. Also used in
|
342
317
|
// `#onTargetAndDefaultSlotKeyDown()` to decide if we need to move focus.
|
343
318
|
#hasVoiceOverMovedFocusToOptionsOrAnOption;
|
344
|
-
// Set in `#
|
319
|
+
// Set in `#onDefaultSlotMouseUp()`. Used in `#onDocumentClick()` to guard against
|
345
320
|
// Menu closing when any number of things that are not an Option are clicked. Those
|
346
321
|
// "click" events will be retargeted to Menu's host the moment they bubble out of
|
347
322
|
// Menu. So checking in `#onDocumentClick()` if the click's `event.target` came
|
@@ -354,8 +329,8 @@ let Menu = class Menu extends LitElement {
|
|
354
329
|
// used in `connectedCallback()` to guard against listening for document clicks for
|
355
330
|
// sub-Menus.
|
356
331
|
#isSubMenuOpen;
|
357
|
-
// Set in `#
|
358
|
-
// `#onDocumentClick()`:
|
332
|
+
// Similar situation as with `isDefaultSlotClick`. Set in `#onTargetSlotClick()`
|
333
|
+
// and `#onDocumentClick()`. Used in `#onDocumentClick()`:
|
359
334
|
//
|
360
335
|
// 1. Menu is open.
|
361
336
|
// 2. User clicks Menu's target.
|
@@ -363,19 +338,9 @@ let Menu = class Menu extends LitElement {
|
|
363
338
|
// 4. `#onTargetSlotClick()` sets `open` to true`.
|
364
339
|
// 5. Menu never closes.
|
365
340
|
//
|
366
|
-
// Setting `#
|
341
|
+
// Setting `#isTargetSlotClick` to `true` in `#onTargetSlotClick()` gives
|
367
342
|
// `#onDocumentClick()` the information it needs to not set `open` to `false`.
|
368
|
-
|
369
|
-
// The normal approach would be to set an `#isTargetSlotClick` property in
|
370
|
-
// `#onTargetSlotClick()`. But `#onDocumentClick()` listens for "click" events in
|
371
|
-
// their capture phase. So `#onDocumentClick()` would be called before
|
372
|
-
// `#onTargetSlotClick()`.
|
373
|
-
//
|
374
|
-
// Note too that `#onDocumentClick()` sets `#isTargetSlotMouseUp` to `false`
|
375
|
-
// instead of `#onTargetSlotClick()` doing it. That's so `#isTargetSlotMouseUp`
|
376
|
-
// is set to `false` even if the user mouses down on Menu's target then moves the
|
377
|
-
// mouse outside of Menu before mousing up.
|
378
|
-
#isTargetSlotMouseUp;
|
343
|
+
#isTargetSlotClick;
|
379
344
|
#localize;
|
380
345
|
#offset;
|
381
346
|
// Used in various situations to reactivate the previously active Option.
|
@@ -435,7 +400,9 @@ let Menu = class Menu extends LitElement {
|
|
435
400
|
.flatMap((element) => {
|
436
401
|
return element instanceof HTMLSlotElement
|
437
402
|
? element.assignedElements({ flatten: true })
|
438
|
-
: element
|
403
|
+
: element instanceof OptionsGroup
|
404
|
+
? [...element.children]
|
405
|
+
: element;
|
439
406
|
})
|
440
407
|
?.filter((element) => {
|
441
408
|
return element instanceof Option;
|
@@ -451,8 +418,14 @@ let Menu = class Menu extends LitElement {
|
|
451
418
|
// The "content" slot case.
|
452
419
|
':scope > glide-core-options > glide-core-option > [slot="content"] > glide-core-menu'),
|
453
420
|
...this.querySelectorAll(
|
421
|
+
// The "content" slot and Options Group case.
|
422
|
+
':scope > glide-core-options > glide-core-options-group > glide-core-option > [slot="content"] > glide-core-menu'),
|
423
|
+
...this.querySelectorAll(
|
454
424
|
// The "content" slot fallback case.
|
455
425
|
':scope > glide-core-options > glide-core-option > [slot="submenu"]'),
|
426
|
+
...this.querySelectorAll(
|
427
|
+
// The "content" slot fallback and Options Group case.
|
428
|
+
':scope > glide-core-options > glide-core-options-group > glide-core-option > [slot="submenu"]'),
|
456
429
|
];
|
457
430
|
}
|
458
431
|
get #targetElement() {
|
@@ -532,6 +505,15 @@ let Menu = class Menu extends LitElement {
|
|
532
505
|
if (this.#isSubMenu && event.target === this.#defaultSlotElementRef.value) {
|
533
506
|
event.stopPropagation();
|
534
507
|
}
|
508
|
+
// When Menu is in the shadow DOM of another component, `event.target` will be
|
509
|
+
// retargeted to the host of that component the moment the event bubbles out of
|
510
|
+
// it.
|
511
|
+
//
|
512
|
+
// So, when the timeout callback below is called, `event.target` will have been
|
513
|
+
// retargeted and the `instanceof` check will wrongly fail when an Option is
|
514
|
+
// clicked, and Menu will never close. So we store a reference to the original
|
515
|
+
// `event.target` and use it in the `instanceof` condition.
|
516
|
+
const originalEventTarget = event.target;
|
535
517
|
// The timeout gives consumers a chance to cancel the event to prevent Menu from
|
536
518
|
// closing.
|
537
519
|
setTimeout(() => {
|
@@ -546,7 +528,7 @@ let Menu = class Menu extends LitElement {
|
|
546
528
|
//
|
547
529
|
// When the default slot's padding is clicked, Menu should remain open because the
|
548
530
|
// user most likely meant to click an Option but missed.
|
549
|
-
if (!event.defaultPrevented &&
|
531
|
+
if (!event.defaultPrevented && originalEventTarget instanceof Option) {
|
550
532
|
this.open = false;
|
551
533
|
}
|
552
534
|
});
|
@@ -1117,6 +1099,7 @@ let Menu = class Menu extends LitElement {
|
|
1117
1099
|
}
|
1118
1100
|
}
|
1119
1101
|
#onTargetSlotClick(event) {
|
1102
|
+
this.#isTargetSlotClick = true;
|
1120
1103
|
const closestOption = event.target instanceof Element &&
|
1121
1104
|
event.target.closest('glide-core-option');
|
1122
1105
|
const isSubMenuTarget = event.target instanceof Element && Boolean(closestOption);
|
@@ -1179,9 +1162,6 @@ let Menu = class Menu extends LitElement {
|
|
1179
1162
|
#onTargetSlotInput() {
|
1180
1163
|
this.open = true;
|
1181
1164
|
}
|
1182
|
-
#onTargetSlotMouseUp() {
|
1183
|
-
this.#isTargetSlotMouseUp = true;
|
1184
|
-
}
|
1185
1165
|
#show() {
|
1186
1166
|
this.#cleanUpFloatingUi?.();
|
1187
1167
|
if (this.#previouslyActiveOption &&
|
package/dist/option.styles.js
CHANGED
@@ -15,9 +15,11 @@ export default [
|
|
15
15
|
font-weight: var(--glide-core-typography-weight-regular);
|
16
16
|
inline-size: 100%;
|
17
17
|
max-inline-size: 21.875rem;
|
18
|
+
min-block-size: 1.75rem;
|
18
19
|
padding-block: var(--glide-core-spacing-base-xxs);
|
19
20
|
padding-inline: var(--glide-core-spacing-base-sm);
|
20
|
-
transition: background-color var(--glide-core-duration-fast-02)
|
21
|
+
transition: background-color var(--glide-core-duration-fast-02)
|
22
|
+
ease-in-out;
|
21
23
|
user-select: none;
|
22
24
|
|
23
25
|
&.active {
|
@@ -0,0 +1,30 @@
|
|
1
|
+
import { LitElement } from 'lit';
|
2
|
+
declare global {
|
3
|
+
interface HTMLElementTagNameMap {
|
4
|
+
'glide-core-options-group': OptionsGroup;
|
5
|
+
}
|
6
|
+
}
|
7
|
+
/**
|
8
|
+
* @attr {string} label
|
9
|
+
* @attr {boolean} [hide-label=false]
|
10
|
+
* @attr {'group'|'optgroup'} [role='group']
|
11
|
+
*
|
12
|
+
* @readonly
|
13
|
+
* @attr {string} [version]
|
14
|
+
*
|
15
|
+
* @slot {Option}
|
16
|
+
*/
|
17
|
+
export default class OptionsGroup extends LitElement {
|
18
|
+
#private;
|
19
|
+
static shadowRootOptions: ShadowRootInit;
|
20
|
+
static styles: import("lit").CSSResult[];
|
21
|
+
/**
|
22
|
+
* @default undefined
|
23
|
+
*/
|
24
|
+
get label(): string | undefined;
|
25
|
+
set label(label: string);
|
26
|
+
hideLabel: boolean;
|
27
|
+
role: 'group' | 'optgroup';
|
28
|
+
readonly version: string;
|
29
|
+
render(): import("lit").TemplateResult<1>;
|
30
|
+
}
|
@@ -0,0 +1,84 @@
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
6
|
+
};
|
7
|
+
import { html, LitElement } from 'lit';
|
8
|
+
import { customElement, property } from 'lit/decorators.js';
|
9
|
+
import { classMap } from 'lit/directives/class-map.js';
|
10
|
+
import packageJson from '../package.json' with { type: 'json' };
|
11
|
+
import styles from './options.group.styles.js';
|
12
|
+
import shadowRootMode from './library/shadow-root-mode.js';
|
13
|
+
import final from './library/final.js';
|
14
|
+
import assertSlot from './library/assert-slot.js';
|
15
|
+
import Option from './option.js';
|
16
|
+
import required from './library/required.js';
|
17
|
+
/**
|
18
|
+
* @attr {string} label
|
19
|
+
* @attr {boolean} [hide-label=false]
|
20
|
+
* @attr {'group'|'optgroup'} [role='group']
|
21
|
+
*
|
22
|
+
* @readonly
|
23
|
+
* @attr {string} [version]
|
24
|
+
*
|
25
|
+
* @slot {Option}
|
26
|
+
*/
|
27
|
+
let OptionsGroup = class OptionsGroup extends LitElement {
|
28
|
+
constructor() {
|
29
|
+
super(...arguments);
|
30
|
+
this.hideLabel = false;
|
31
|
+
this.role = 'group';
|
32
|
+
this.version = packageJson.version;
|
33
|
+
}
|
34
|
+
static { this.shadowRootOptions = {
|
35
|
+
...LitElement.shadowRootOptions,
|
36
|
+
mode: shadowRootMode,
|
37
|
+
}; }
|
38
|
+
static { this.styles = styles; }
|
39
|
+
/**
|
40
|
+
* @default undefined
|
41
|
+
*/
|
42
|
+
get label() {
|
43
|
+
return this.#label;
|
44
|
+
}
|
45
|
+
set label(label) {
|
46
|
+
this.#label = label;
|
47
|
+
this.ariaLabel = label;
|
48
|
+
}
|
49
|
+
render() {
|
50
|
+
return html `
|
51
|
+
<div class="component">
|
52
|
+
<div
|
53
|
+
aria-hidden="true"
|
54
|
+
class=${classMap({ label: true, 'visually-hidden': this.hideLabel })}
|
55
|
+
>
|
56
|
+
${this.label}
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<slot ${assertSlot([Option])}>
|
60
|
+
<!-- @type {Option} -->
|
61
|
+
</slot>
|
62
|
+
</div>
|
63
|
+
`;
|
64
|
+
}
|
65
|
+
#label;
|
66
|
+
};
|
67
|
+
__decorate([
|
68
|
+
property({ reflect: true }),
|
69
|
+
required
|
70
|
+
], OptionsGroup.prototype, "label", null);
|
71
|
+
__decorate([
|
72
|
+
property({ attribute: 'hide-label', reflect: true, type: Boolean })
|
73
|
+
], OptionsGroup.prototype, "hideLabel", void 0);
|
74
|
+
__decorate([
|
75
|
+
property({ reflect: true })
|
76
|
+
], OptionsGroup.prototype, "role", void 0);
|
77
|
+
__decorate([
|
78
|
+
property({ reflect: true })
|
79
|
+
], OptionsGroup.prototype, "version", void 0);
|
80
|
+
OptionsGroup = __decorate([
|
81
|
+
customElement('glide-core-options-group'),
|
82
|
+
final
|
83
|
+
], OptionsGroup);
|
84
|
+
export default OptionsGroup;
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import { css } from 'lit';
|
2
|
+
import visuallyHidden from './styles/visually-hidden.js';
|
3
|
+
export default [
|
4
|
+
css `
|
5
|
+
${visuallyHidden('.label.visually-hidden')}
|
6
|
+
`,
|
7
|
+
css `
|
8
|
+
:host(:not(:last-of-type)) .component::after {
|
9
|
+
background-color: var(--glide-core-color-static-stroke-secondary);
|
10
|
+
block-size: 1px;
|
11
|
+
border-radius: var(--glide-core-rounding-base-radius-sm);
|
12
|
+
content: '';
|
13
|
+
display: block;
|
14
|
+
margin-block: var(--glide-core-spacing-base-xxxs);
|
15
|
+
}
|
16
|
+
|
17
|
+
.label {
|
18
|
+
block-size: 1.75rem;
|
19
|
+
box-sizing: border-box;
|
20
|
+
font-family: var(--glide-core-typography-family-primary);
|
21
|
+
font-size: var(--glide-core-typography-size-body-small);
|
22
|
+
font-weight: var(--glide-core-typography-weight-bold);
|
23
|
+
max-inline-size: 21.875rem;
|
24
|
+
overflow: hidden;
|
25
|
+
padding-block: var(--glide-core-spacing-base-xxs);
|
26
|
+
padding-inline: var(--glide-core-spacing-base-sm);
|
27
|
+
text-overflow: ellipsis;
|
28
|
+
white-space: nowrap;
|
29
|
+
}
|
30
|
+
`,
|
31
|
+
];
|
package/dist/options.js
CHANGED
@@ -17,6 +17,7 @@ import final from './library/final.js';
|
|
17
17
|
import uniqueId from './library/unique-id.js';
|
18
18
|
import assertSlot from './library/assert-slot.js';
|
19
19
|
import Option from './option.js';
|
20
|
+
import OptionsGroup from './options.group.js';
|
20
21
|
// This component exists because Menu's target and its menu (`"role="menu"` or
|
21
22
|
// `role="listbox"`) both need to be in the light DOM so the target and menu can
|
22
23
|
// reference each other's IDs in ARIA attributes.
|
@@ -82,7 +83,7 @@ let Options = class Options extends LitElement {
|
|
82
83
|
loading: this.privateLoading,
|
83
84
|
})}
|
84
85
|
@slotchange=${this.#onDefaultSlotChange}
|
85
|
-
${assertSlot([Option, Text], true)}
|
86
|
+
${assertSlot([OptionsGroup, Option, Text], true)}
|
86
87
|
>
|
87
88
|
<!-- @type {Option | Text} -->
|
88
89
|
</slot>
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@crowdstrike/glide-core",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.31.1",
|
4
4
|
"description": "A Web Component design system",
|
5
5
|
"author": "CrowdStrike UX Team",
|
6
6
|
"license": "Apache-2.0",
|
@@ -94,13 +94,13 @@
|
|
94
94
|
"comment-parser": "^1.4.1",
|
95
95
|
"custom-elements-manifest": "^2.1.0",
|
96
96
|
"esbuild": "^0.25.0",
|
97
|
-
"eslint": "^9.
|
97
|
+
"eslint": "^9.31.0",
|
98
98
|
"eslint-config-prettier": "^10.1.5",
|
99
99
|
"eslint-plugin-import": "^2.31.0",
|
100
100
|
"eslint-plugin-lit": "^1.15.0",
|
101
101
|
"eslint-plugin-lit-a11y": "^4.1.4",
|
102
102
|
"eslint-plugin-sort-class-members": "^1.21.0",
|
103
|
-
"eslint-plugin-unicorn": "^
|
103
|
+
"eslint-plugin-unicorn": "^60.0.0",
|
104
104
|
"globals": "^15.13.0",
|
105
105
|
"globby": "^14.0.2",
|
106
106
|
"husky": "^8.0.3",
|