@fleetbase/ember-ui 0.3.24 → 0.3.26

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.
@@ -0,0 +1,30 @@
1
+ <div
2
+ class="fleetbase-event-calendar"
3
+ ...attributes
4
+ {{did-insert this.setup}}
5
+ {{did-update this.update
6
+ @view
7
+ @resources
8
+ @events
9
+ @editable
10
+ @droppable
11
+ @selectable
12
+ @slotMinTime
13
+ @slotMaxTime
14
+ @slotDuration
15
+ @slotLabelInterval
16
+ @slotWidth
17
+ @height
18
+ @headerToolbar
19
+ @locale
20
+ @scrollTime
21
+ @nowIndicator
22
+ @date
23
+ @eventContent
24
+ @resourceLabelContent
25
+ @dayCellContent
26
+ @slotLabelContent
27
+ @options
28
+ }}
29
+ {{will-destroy this.teardown}}
30
+ ></div>
@@ -0,0 +1,333 @@
1
+ import Component from '@glimmer/component';
2
+ import { tracked } from '@glimmer/tracking';
3
+ import { action } from '@ember/object';
4
+ import { scheduleOnce } from '@ember/runloop';
5
+ import { createCalendar, destroyCalendar, ResourceTimeline, ResourceTimeGrid, TimeGrid, DayGrid, List, Interaction } from '@event-calendar/core';
6
+ import '@event-calendar/core/index.css';
7
+
8
+ /**
9
+ * EventCalendar component wrapping @event-calendar/core (MIT licensed).
10
+ *
11
+ * This is the preferred calendar component for resource-timeline views in
12
+ * Fleetbase. It replaces the need for FullCalendar Premium plugins
13
+ * (@fullcalendar/resource-timeline etc.) which carry a commercial license
14
+ * incompatible with Fleetbase's dual AGPL v3 / commercial licensing model.
15
+ *
16
+ * @see https://github.com/vkurko/calendar
17
+ *
18
+ * Supported views (all MIT, no license key required):
19
+ * - resourceTimelineDay / resourceTimelineWeek / resourceTimelineMonth
20
+ * - resourceTimeGridDay / resourceTimeGridWeek
21
+ * - timeGridDay / timeGridWeek
22
+ * - dayGridMonth / dayGridWeek / dayGridDay
23
+ * - listDay / listWeek / listMonth / listYear
24
+ *
25
+ * Usage:
26
+ * ```hbs
27
+ * <EventCalendar
28
+ * @view="resourceTimelineDay"
29
+ * @resources={{this.calendarResources}}
30
+ * @events={{this.calendarEvents}}
31
+ * @editable={{true}}
32
+ * @droppable={{true}}
33
+ * @onEventDrop={{this.handleEventDrop}}
34
+ * @onEventReceive={{this.handleEventReceive}}
35
+ * @onEventClick={{this.handleEventClick}}
36
+ * @onDateClick={{this.handleDateClick}}
37
+ * @resourceLabelContent={{this.renderResourceLabel}}
38
+ * @eventContent={{this.renderEventContent}}
39
+ * @onCalendarReady={{this.onCalendarReady}}
40
+ * @options={{this.extraCalendarOptions}}
41
+ * />
42
+ * ```
43
+ *
44
+ * The `@options` arg is merged last, allowing full override of any option.
45
+ *
46
+ * Callback args:
47
+ * @onEventDrop(info) — info.event, info.oldResource, info.newResource, info.revert
48
+ * @onEventReceive(info) — info.event, info.revert (external drop)
49
+ * @onEventClick(info) — info.event, info.el, info.jsEvent
50
+ * @onDateClick(info) — info.date, info.resource, info.jsEvent
51
+ * @onEventResize(info) — info.event, info.revert
52
+ * @onEventMouseEnter(info) — info.event, info.el, info.jsEvent
53
+ * @onEventMouseLeave(info) — info.event, info.el, info.jsEvent
54
+ * @onDatesSet(info) — info.start, info.end, info.view
55
+ * @onLoading(isLoading) — boolean
56
+ *
57
+ * Render hook args (no 'on' prefix — these return content descriptors):
58
+ * @eventContent — function(info) returning { html } or { domNodes }
59
+ * @resourceLabelContent — function(info) returning { html } or { domNodes }
60
+ * @dayCellContent — function(info) returning { html } or { domNodes }
61
+ * @slotLabelContent — function(info) returning { html } or { domNodes }
62
+ */
63
+ export default class EventCalendarComponent extends Component {
64
+ /**
65
+ * All plugins enabled by default. Consumers can override via @plugins.
66
+ * @type {Array}
67
+ */
68
+ defaultPlugins = [ResourceTimeline, ResourceTimeGrid, TimeGrid, DayGrid, List, Interaction];
69
+
70
+ /**
71
+ * Reference to the DOM element the calendar is mounted on.
72
+ * @type {HTMLElement}
73
+ */
74
+ @tracked calendarEl = null;
75
+
76
+ /**
77
+ * The EventCalendar instance returned by createCalendar().
78
+ * Exposes .setOption(name, value) and .getOption(name).
79
+ * @type {Object}
80
+ */
81
+ @tracked calendar = null;
82
+
83
+ /**
84
+ * Callback arg names that map to @event-calendar/core event options.
85
+ * Each entry is the raw option name; the corresponding @arg is prefixed
86
+ * with 'on' (e.g. 'eventDrop' → @onEventDrop).
87
+ * @type {string[]}
88
+ */
89
+ callbackOptions = [
90
+ 'eventClick',
91
+ 'eventDrop',
92
+ 'eventResize',
93
+ 'eventReceive',
94
+ 'eventLeave',
95
+ 'eventMouseEnter',
96
+ 'eventMouseLeave',
97
+ 'dateClick',
98
+ 'datesSet',
99
+ 'loading',
100
+ 'viewDidMount',
101
+ 'eventDidMount',
102
+ 'eventWillUnmount',
103
+ 'select',
104
+ 'unselect',
105
+ ];
106
+
107
+ /**
108
+ * Render hook option names. These are passed directly (no 'on' prefix)
109
+ * because they return content descriptors, not fire-and-forget callbacks.
110
+ * @type {string[]}
111
+ */
112
+ renderHooks = ['eventContent', 'resourceLabelContent', 'resourceLabelDidMount', 'dayCellContent', 'dayCellDidMount', 'slotLabelContent', 'slotLabelDidMount', 'nowIndicatorContent'];
113
+
114
+ // -------------------------------------------------------------------------
115
+ // Lifecycle
116
+ // -------------------------------------------------------------------------
117
+
118
+ /**
119
+ * Initialises the EventCalendar instance on the container element.
120
+ * Called via {{did-insert this.setup}} in the template.
121
+ *
122
+ * @param {HTMLElement} el
123
+ */
124
+ @action setup(el) {
125
+ this.calendarEl = el;
126
+ const plugins = this.args.plugins ?? this.defaultPlugins;
127
+ const options = this._buildOptions();
128
+ this.calendar = createCalendar(el, plugins, options);
129
+
130
+ if (typeof this.args.onCalendarReady === 'function') {
131
+ this.args.onCalendarReady(this.calendar);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Responds to tracked arg changes and updates the calendar options.
137
+ * Called via {{did-update this.update ...watchedArgs}} in the template.
138
+ */
139
+ @action update() {
140
+ if (!this.calendar) {
141
+ return;
142
+ }
143
+ scheduleOnce('afterRender', this, this._applyDynamicOptions);
144
+ }
145
+
146
+ /**
147
+ * Destroys the EventCalendar instance when the component is torn down.
148
+ * Called via {{will-destroy this.teardown}} in the template.
149
+ */
150
+ @action teardown() {
151
+ if (this.calendar) {
152
+ destroyCalendar(this.calendar);
153
+ }
154
+ this.calendar = null;
155
+ this.calendarEl = null;
156
+ }
157
+
158
+ // -------------------------------------------------------------------------
159
+ // Public API helpers (callable by parent via @onCalendarReady)
160
+ // -------------------------------------------------------------------------
161
+
162
+ /**
163
+ * Programmatically change the view type.
164
+ * @param {string} viewName e.g. 'resourceTimelineWeek'
165
+ */
166
+ @action changeView(viewName) {
167
+ this._setOption('view', viewName);
168
+ }
169
+
170
+ /**
171
+ * Navigate the calendar to today.
172
+ */
173
+ @action today() {
174
+ this._setOption('date', new Date());
175
+ }
176
+
177
+ /**
178
+ * Refetch events from the events source.
179
+ */
180
+ @action refetchEvents() {
181
+ this._setOption('events', this.args.events ?? []);
182
+ }
183
+
184
+ /**
185
+ * Refetch resources from the resources source.
186
+ */
187
+ @action refetchResources() {
188
+ this._setOption('resources', this.args.resources ?? []);
189
+ }
190
+
191
+ // -------------------------------------------------------------------------
192
+ // Private helpers
193
+ // -------------------------------------------------------------------------
194
+
195
+ /**
196
+ * Build the full options object passed to createCalendar().
197
+ * @returns {Object}
198
+ */
199
+ _buildOptions() {
200
+ const {
201
+ view,
202
+ resources,
203
+ events,
204
+ editable,
205
+ droppable,
206
+ selectable,
207
+ nowIndicator,
208
+ slotMinTime,
209
+ slotMaxTime,
210
+ slotDuration,
211
+ slotLabelInterval,
212
+ slotWidth,
213
+ firstDay,
214
+ height,
215
+ headerToolbar,
216
+ locale,
217
+ scrollTime,
218
+ date,
219
+ options: extraOptions,
220
+ } = this.args;
221
+
222
+ const base = {
223
+ view: view ?? 'resourceTimelineDay',
224
+ resources: resources ?? [],
225
+ events: events ?? [],
226
+ editable: editable !== false,
227
+ droppable: droppable !== false,
228
+ selectable: selectable ?? false,
229
+ nowIndicator: nowIndicator !== false,
230
+ slotMinTime: slotMinTime ?? '00:00:00',
231
+ slotMaxTime: slotMaxTime ?? '24:00:00',
232
+ firstDay: firstDay ?? 0,
233
+ height: height ?? '100%',
234
+ headerToolbar: headerToolbar ?? {
235
+ start: 'prev,next today',
236
+ center: 'title',
237
+ end: 'resourceTimelineDay,resourceTimelineWeek',
238
+ },
239
+ locale: locale ?? 'en',
240
+ scrollTime: scrollTime ?? '06:00:00',
241
+ };
242
+
243
+ if (slotDuration !== undefined) base.slotDuration = slotDuration;
244
+ if (slotLabelInterval !== undefined) base.slotLabelInterval = slotLabelInterval;
245
+ if (slotWidth !== undefined) base.slotWidth = slotWidth;
246
+ if (date !== undefined) base.date = date;
247
+
248
+ // Wire up callback args (@onEventDrop → eventDrop option)
249
+ for (const name of this.callbackOptions) {
250
+ const argName = `on${name.charAt(0).toUpperCase()}${name.slice(1)}`;
251
+ if (typeof this.args[argName] === 'function') {
252
+ base[name] = this.args[argName];
253
+ }
254
+ }
255
+
256
+ // Wire up render hooks (passed directly, no 'on' prefix)
257
+ for (const hook of this.renderHooks) {
258
+ if (typeof this.args[hook] === 'function') {
259
+ base[hook] = this.args[hook];
260
+ }
261
+ }
262
+
263
+ // Merge extra options last — allows full override of any option
264
+ return { ...base, ...(extraOptions ?? {}) };
265
+ }
266
+
267
+ /**
268
+ * Re-apply all dynamic options to the live calendar instance.
269
+ * Batched via scheduleOnce('afterRender') to coalesce multiple arg changes.
270
+ */
271
+ _applyDynamicOptions() {
272
+ if (!this.calendar) {
273
+ return;
274
+ }
275
+
276
+ const dynamicKeys = [
277
+ 'view',
278
+ 'resources',
279
+ 'events',
280
+ 'editable',
281
+ 'droppable',
282
+ 'selectable',
283
+ 'slotMinTime',
284
+ 'slotMaxTime',
285
+ 'slotDuration',
286
+ 'height',
287
+ 'headerToolbar',
288
+ 'locale',
289
+ 'scrollTime',
290
+ 'nowIndicator',
291
+ 'date',
292
+ ];
293
+
294
+ for (const key of dynamicKeys) {
295
+ if (this.args[key] !== undefined) {
296
+ this._setOption(key, this.args[key]);
297
+ }
298
+ }
299
+
300
+ // Re-wire render hooks in case they changed
301
+ for (const hook of this.renderHooks) {
302
+ if (typeof this.args[hook] === 'function') {
303
+ this._setOption(hook, this.args[hook]);
304
+ }
305
+ }
306
+
307
+ // Re-wire callback options in case they changed
308
+ for (const name of this.callbackOptions) {
309
+ const argName = `on${name.charAt(0).toUpperCase()}${name.slice(1)}`;
310
+ if (typeof this.args[argName] === 'function') {
311
+ this._setOption(name, this.args[argName]);
312
+ }
313
+ }
314
+
315
+ // Merge any extra options override
316
+ if (this.args.options) {
317
+ for (const [key, value] of Object.entries(this.args.options)) {
318
+ this._setOption(key, value);
319
+ }
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Safely call calendar.setOption().
325
+ * @param {string} key
326
+ * @param {*} value
327
+ */
328
+ _setOption(key, value) {
329
+ if (this.calendar && typeof this.calendar.setOption === 'function') {
330
+ this.calendar.setOption(key, value);
331
+ }
332
+ }
333
+ }
@@ -161,8 +161,14 @@ export default class LayoutHeaderSmartNavMenuComponent extends Component {
161
161
  const raw = this.universe.headerMenuItems ?? [];
162
162
  const visible = [];
163
163
  for (const item of raw) {
164
+ // Shortcuts are not standalone extensions — they should be visible
165
+ // if and only if their parent extension is visible. Use _parentId
166
+ // for the ability check so the shortcut inherits the parent's
167
+ // permission rather than being checked against its own (non-existent)
168
+ // extension ability, which would always throw and default to visible.
169
+ const abilityId = item._isShortcut && item._parentId ? item._parentId : item.id;
164
170
  try {
165
- if (this.abilities.can(`${item.id} see extension`)) {
171
+ if (this.abilities.can(`${abilityId} see extension`)) {
166
172
  visible.push(item);
167
173
  }
168
174
  } catch (_) {
@@ -33,9 +33,9 @@
33
33
  {{component @options.footerComponent options=@options confirm=modal.submit modal=modal}}
34
34
  {{else}}
35
35
  {{#unless @options.hideFooterActions}}
36
- <div class="modal-footer-actions {{@options.modalFooterActionsClass}}">
36
+ <div class="modal-footer-actions space-x-2 {{@options.modalFooterActionsClass}}">
37
37
  <Button
38
- class="mr-2 {{if @options.hideDeclineButton 'hidden'}}"
38
+ class="{{if @options.hideDeclineButton 'hidden'}}"
39
39
  @type={{or @options.declineButtonScheme @options.declineButtonType "default"}}
40
40
  @size={{or @options.buttonSize "md"}}
41
41
  @icon={{or @options.declineButtonIcon "times"}}
@@ -44,6 +44,85 @@
44
44
  @onClick={{modal.close}}
45
45
  @disabled={{or @options.declineButtonDisabled @options.isLoading}}
46
46
  />
47
+ {{#each @options.actionButtons as |actionButton|}}
48
+ {{#if actionButton.items}}
49
+ <DropdownButton
50
+ @icon={{or actionButton.icon "ellipsis"}}
51
+ @size={{or actionButton.size "md"}}
52
+ @iconPrefix={{actionButton.prefix}}
53
+ @triggerClass={{actionButton.triggerClass}}
54
+ @renderInPlace={{or actionButton.renderInPlace true}}
55
+ as |dd|
56
+ >
57
+ <div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
58
+ {{#each actionButton.items as |item|}}
59
+ {{#if item.separator}}
60
+ <div class="next-dd-menu-seperator"></div>
61
+ {{else}}
62
+ <div class="px-1 {{item.wrapperClass}}">
63
+ <a
64
+ href="javascript:;"
65
+ class="next-dd-item {{item.class}} {{if item.disabled 'disabled'}}"
66
+ disabled={{item.disabled}}
67
+ {{on "click" (dropdown-fn dd (or item.fn item.onClick))}}
68
+ >
69
+ <div class="w-7 flex-grow-0 flex-shrink-0">
70
+ <FaIcon @icon={{item.icon}} />
71
+ </div>
72
+ <span>{{or item.text item.label}}</span>
73
+ </a>
74
+ </div>
75
+ {{/if}}
76
+ {{/each}}
77
+ </div>
78
+ </DropdownButton>
79
+ {{else if actionButton.component}}
80
+ {{component
81
+ actionButton.component
82
+ icon=actionButton.icon
83
+ iconPrefix=actionButton.iconPrefix
84
+ iconComponent=actionButton.iconComponent
85
+ size=(or actionButton.size "md")
86
+ text=actionButton.text
87
+ options=actionButton.options
88
+ items=actionButton.items
89
+ permission=actionButton.permission
90
+ isLoading=actionButton.isLoading
91
+ renderInPlace=actionButton.renderInPlace
92
+ triggerClass=actionButton.triggerClass
93
+ disabled=actionButton.disabled
94
+ onClick=actionButton.onClick
95
+ fn=actionButton.fn
96
+ perform=actionButton.perform
97
+ onSelect=actionButton.onSelect
98
+ onChange=actionButton.onChange
99
+ }}
100
+ {{else}}
101
+ <Button
102
+ @type={{actionButton.type}}
103
+ @text={{actionButton.text}}
104
+ @onClick={{if actionButton.perform (perform actionButton.perform) (or actionButton.onClick actionButton.fn)}}
105
+ @size={{or actionButton.size "md"}}
106
+ @buttonType={{actionButton.buttonType}}
107
+ @disabled={{actionButton.disabled}}
108
+ @exampleText={{actionButton.exampleText}}
109
+ @helpText={{actionButton.helpText}}
110
+ @icon={{actionButton.icon}}
111
+ @iconClass={{actionButton.iconClass}}
112
+ @iconFlip={{actionButton.iconFlip}}
113
+ @iconPrefix={{actionButton.iconPrefix}}
114
+ @iconRotation={{actionButton.iconRotation}}
115
+ @iconSize={{actionButton.iconSize}}
116
+ @isLoading={{actionButton.isLoading}}
117
+ @outline={{actionButton.outline}}
118
+ @permission={{actionButton.permission}}
119
+ @wrapperClass={{actionButton.wrapperClass}}
120
+ @textClass={{actionButton.textClass}}
121
+ @tooltipPlacement={{actionButton.tooltipPlacement}}
122
+ @visible={{actionButton.visible}}
123
+ />
124
+ {{/if}}
125
+ {{/each}}
47
126
  <Button
48
127
  class="{{if @options.hideAcceptButton 'hidden'}}"
49
128
  @type={{or @options.acceptButtonScheme @options.acceptButtonType "primary"}}
@@ -78,6 +78,7 @@ export default class ResourceContextPanelComponent extends Component {
78
78
  resource,
79
79
  component: tab.component ?? tab.render,
80
80
  model: resource,
81
+ key: tab.key ?? tab.id ?? dasherize(tab.label ?? tab.title),
81
82
  id: tab.id ?? tab.key ?? dasherize(tab.label ?? tab.title),
82
83
  label: tab.label ?? tab.title,
83
84
  icon: tab.icon,
@@ -0,0 +1,57 @@
1
+ import { modifier } from 'ember-modifier';
2
+
3
+ const SIDEBAR_SELECTOR = 'nav.next-sidebar';
4
+ const SECTION_SELECTOR = 'section.next-view-section';
5
+ const TABLIST_SELECTOR = '[role="tablist"]';
6
+
7
+ function getSidebarWidth() {
8
+ return document.querySelector(SIDEBAR_SELECTOR)?.offsetWidth ?? 0;
9
+ }
10
+
11
+ function updateTablistMaxWidth(container, sidebarWidth) {
12
+ const tablist = container.querySelector(TABLIST_SELECTOR);
13
+
14
+ if (!tablist) {
15
+ return;
16
+ }
17
+
18
+ const pageWidth = document.body.offsetWidth;
19
+ tablist.style.maxWidth = `${Math.max(0, pageWidth - sidebarWidth)}px`;
20
+ }
21
+
22
+ function updateSectionWidth(sidebarWidth) {
23
+ const section = document.querySelector(SECTION_SELECTOR);
24
+
25
+ if (!section) {
26
+ return;
27
+ }
28
+
29
+ section.style.width = `calc(100vw - ${sidebarWidth}px)`;
30
+ }
31
+
32
+ function updateLayout(container) {
33
+ const sidebarWidth = getSidebarWidth();
34
+
35
+ updateTablistMaxWidth(container, sidebarWidth);
36
+ updateSectionWidth(sidebarWidth);
37
+ }
38
+
39
+ export default modifier(function constrainViewSectionWidth(element) {
40
+ const applyLayout = () => updateLayout(element);
41
+
42
+ applyLayout();
43
+
44
+ window.addEventListener('resize', applyLayout);
45
+
46
+ const sidebar = document.querySelector(SIDEBAR_SELECTOR);
47
+ const observer = new ResizeObserver(applyLayout);
48
+
49
+ if (sidebar) {
50
+ observer.observe(sidebar);
51
+ }
52
+
53
+ return () => {
54
+ window.removeEventListener('resize', applyLayout);
55
+ observer.disconnect();
56
+ };
57
+ });
@@ -199,7 +199,7 @@ export default class ResourceContextPanelService extends Service {
199
199
  throw new Error(`Overlay with ID ${id} does not have tabs`);
200
200
  }
201
201
 
202
- const tab = overlay.tabs.find((t) => t.key === tabKey);
202
+ const tab = overlay.tabs.find((t) => t.key === tabKey || t.id === tabKey);
203
203
  if (!tab) {
204
204
  throw new Error(`Tab with key ${tabKey} not found in overlay ${id}`);
205
205
  }
@@ -4,6 +4,10 @@ import getUrlParam from './get-url-param';
4
4
  export default function isMenuItemActive(section, slug, view = null) {
5
5
  let path = window.location.pathname;
6
6
  let segments = path.replace(/^\/|\/$/g, '').split('/');
7
+ // Hack for now for fleet-ops until we can refactor the menu system to be more route-aware and not rely on URL parsing
8
+ if (segments.length === 3 && segments[0] === 'fleet-ops') {
9
+ segments = segments.slice(1);
10
+ }
7
11
  let sectionMatch = segments[0] === section;
8
12
  let slugOnly = segments[0] === slug && section === slug && view === null;
9
13
  let slugMatch = segments.includes(slug);
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/ember-ui/components/event-calendar';
@@ -0,0 +1 @@
1
+ export { default } from '@fleetbase/ember-ui/modifiers/constrain-view-section-width';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fleetbase/ember-ui",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "description": "Fleetbase UI provides all the interface components, helpers, services and utilities for building a Fleetbase extension into the Console.",
5
5
  "keywords": [
6
6
  "fleetbase-ui",
@@ -39,6 +39,7 @@
39
39
  "@ember/string": "^3.0.1",
40
40
  "@embroider/addon": "^0.30.0",
41
41
  "@embroider/macros": "^1.8.3",
42
+ "@event-calendar/core": "^5.6.0",
42
43
  "@fleetbase/ember-accounting": "^0.0.1",
43
44
  "@floating-ui/dom": "^1.0.1",
44
45
  "@fortawesome/ember-fontawesome": "^2.0.0",
@@ -106,6 +107,7 @@
106
107
  "ember-wormhole": "^0.6.0",
107
108
  "gridstack": "^7.3.0",
108
109
  "imask": "^6.4.3",
110
+ "interactjs": "^1.10.27",
109
111
  "intl-tel-input": "^22.0.2",
110
112
  "leaflet": "^1.9.4",
111
113
  "postcss-at-rules-variables": "^0.3.0",
@@ -114,8 +116,7 @@
114
116
  "postcss-import": "^15.1.0",
115
117
  "postcss-mixins": "^9.0.4",
116
118
  "postcss-preset-env": "^9.1.1",
117
- "tailwindcss": "^3.1.8",
118
- "interactjs": "^1.10.27"
119
+ "tailwindcss": "^3.1.8"
119
120
  },
120
121
  "devDependencies": {
121
122
  "@babel/eslint-parser": "^7.22.15",