@pond-ts/charts 0.32.0 → 0.33.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/CHANGELOG.md +37 -1
- package/dist/Layers.js +6 -4
- package/dist/annotations.d.ts +25 -6
- package/dist/annotations.js +53 -17
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,8 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`, and
|
|
|
8
8
|
them all. Pre-1.0: minor bumps may include new features and type-level changes;
|
|
9
9
|
patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.33.0...HEAD
|
|
12
|
+
[0.33.0]: https://github.com/pjm17971/pond-ts/compare/v0.32.0...v0.33.0
|
|
12
13
|
[0.32.0]: https://github.com/pjm17971/pond-ts/compare/v0.31.2...v0.32.0
|
|
13
14
|
[0.31.2]: https://github.com/pjm17971/pond-ts/compare/v0.31.1...v0.31.2
|
|
14
15
|
[0.31.1]: https://github.com/pjm17971/pond-ts/compare/v0.30.0...v0.31.1
|
|
@@ -27,6 +28,41 @@ patch bumps are strictly additive.
|
|
|
27
28
|
[0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
|
|
28
29
|
[0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
|
|
29
30
|
|
|
31
|
+
## [0.33.0] — 2026-06-30
|
|
32
|
+
|
|
33
|
+
A `@pond-ts/charts` release: a label opt-out for annotations plus interaction
|
|
34
|
+
fixes from an adversarial review of the #306 annotation system. `pond-ts`,
|
|
35
|
+
`@pond-ts/react`, and `@pond-ts/fit` carry no code changes — republished in
|
|
36
|
+
lock-step (their `pond-ts` / `@pond-ts/react` peer ranges widen to `^0.33.0`).
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Charts — annotation label opt-out.** `<Region>`, `<Marker>`, and
|
|
41
|
+
`<Baseline>` accept `label={false}` (or `label=""`) to render **no label
|
|
42
|
+
chip** — for an inert background mark (e.g. a `selectable={false}` highlight
|
|
43
|
+
band) where the auto-label would only show a raw axis value. Omitting `label`
|
|
44
|
+
still auto-labels; a string still renders it. `label` widens to
|
|
45
|
+
`string | false`. New `Highlight` story + Linux e2e baselines for the
|
|
46
|
+
`Annotations` stories. (#308)
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **Charts — `<Region>` / `<Marker>` / `<Baseline>` with `label=""` now render
|
|
51
|
+
no chip** (previously an empty, zero-width chip). The label-less path; pass a
|
|
52
|
+
non-empty string for a visible label. (#308)
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- **Charts — annotation edge-resize no longer inverts.** Dragging a `<Region>`
|
|
57
|
+
edge past the opposite one previously reported `{ from > to }`; it now pivots
|
|
58
|
+
around the fixed opposite edge, so the reported span stays ordered and a drag
|
|
59
|
+
either way re-opens the region instead of dead-ending at zero width. (#308)
|
|
60
|
+
- **Charts — annotation drag releases on `pointercancel`.** A system gesture
|
|
61
|
+
takeover (which fires `pointercancel`, not `pointerup`) no longer leaves a mark
|
|
62
|
+
stuck mid-drag. (#308)
|
|
63
|
+
- **Charts — single-annotation edit exits on an empty-plot click** even when the
|
|
64
|
+
mark is `editing` but not `selected`. (#308)
|
|
65
|
+
|
|
30
66
|
## [0.32.0] — 2026-06-29
|
|
31
67
|
|
|
32
68
|
A `@pond-ts/charts` release: value-axis support across the fill/bar layers, plus
|
package/dist/Layers.js
CHANGED
|
@@ -354,10 +354,12 @@ export function Layers({ children }) {
|
|
|
354
354
|
const c = containerRef.current;
|
|
355
355
|
// A click that reached the plot (no mark's DragArea claimed it) is an empty
|
|
356
356
|
// click. Deselect / exit edit when the consumer is tracking annotations — in
|
|
357
|
-
// global edit mode, or whenever a mark is currently active
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
|
|
357
|
+
// global edit mode, or whenever a mark is currently active: selected, OR the
|
|
358
|
+
// single-edit target (`editing`). Checking `editing` too means a consumer that
|
|
359
|
+
// sets `editing` without also setting `selected` still gets the exit signal.
|
|
360
|
+
// Marks stop their own clicks in DragArea, so this only fires on true empty space.
|
|
361
|
+
if (c.editAnnotations ||
|
|
362
|
+
c.annotations.some((a) => a.selected || a.editing)) {
|
|
361
363
|
c.onSelectAnnotation?.(null);
|
|
362
364
|
return;
|
|
363
365
|
}
|
package/dist/annotations.d.ts
CHANGED
|
@@ -7,13 +7,28 @@ import { type AnnotationSpec } from './context.js';
|
|
|
7
7
|
* at the left at their own y, don't participate.
|
|
8
8
|
*/
|
|
9
9
|
export declare function computeLabelLanes(annotations: readonly AnnotationSpec[], toPixel: (axisX: number) => number): Map<symbol, number>;
|
|
10
|
+
/**
|
|
11
|
+
* Order two region bounds so `from ≤ to`. A region **edge resize** pivots around
|
|
12
|
+
* the *opposite* (fixed) edge: the dragged value `v` and the pivot are ordered
|
|
13
|
+
* here, so dragging an edge past the pivot never emits an inverted span — instead
|
|
14
|
+
* the edges meet (zero width) and a continued drag **re-opens the region the other
|
|
15
|
+
* way** (the grabbed handle becomes the far edge). The pivot is captured at
|
|
16
|
+
* drag-start (a ref) so it stays fixed even as the emitted bounds swap on cross.
|
|
17
|
+
*/
|
|
18
|
+
export declare function orderRegion(v: number, pivot: number): {
|
|
19
|
+
from: number;
|
|
20
|
+
to: number;
|
|
21
|
+
};
|
|
10
22
|
export interface MarkerProps {
|
|
11
23
|
/** x position in axis units — epoch ms on a time axis, the value on a value
|
|
12
24
|
* axis. (The generalisation of the mockup's "time line": a mark at an x, time
|
|
13
25
|
* or value.) */
|
|
14
26
|
at: number;
|
|
15
|
-
/** Chip label
|
|
16
|
-
|
|
27
|
+
/** Chip label. Omit to auto-label with the shared x formatter (the axis's);
|
|
28
|
+
* pass `false` (or `''`) to render **no label chip** — for an inert background
|
|
29
|
+
* mark you don't want labelled (where the auto-label would just show a raw
|
|
30
|
+
* axis value). */
|
|
31
|
+
label?: string | false;
|
|
17
32
|
/** Stable consumer id — a click reports it via the container's
|
|
18
33
|
* `onSelectAnnotation`, so the consumer can track which mark is selected. */
|
|
19
34
|
id?: string;
|
|
@@ -44,8 +59,9 @@ export interface BaselineProps {
|
|
|
44
59
|
value: number;
|
|
45
60
|
/** Which `<YAxis>` (by id) to measure against; omit for the row's default axis. */
|
|
46
61
|
axis?: string;
|
|
47
|
-
/** Chip label
|
|
48
|
-
|
|
62
|
+
/** Chip label. Omit to format `value` with that axis's formatter; pass `false`
|
|
63
|
+
* (or `''`) to render **no label chip**. */
|
|
64
|
+
label?: string | false;
|
|
49
65
|
/** Stable consumer id — a click reports it via `onSelectAnnotation`. */
|
|
50
66
|
id?: string;
|
|
51
67
|
/** Controlled selection — brightens to the front (level 1). Handles are an
|
|
@@ -75,8 +91,11 @@ export interface RegionProps {
|
|
|
75
91
|
from: number;
|
|
76
92
|
/** End x in axis units. */
|
|
77
93
|
to: number;
|
|
78
|
-
/** Chip label
|
|
79
|
-
|
|
94
|
+
/** Chip label. Omit to auto-label `from–to` with the shared x formatter; pass
|
|
95
|
+
* `false` (or `''`) to render **no label chip** — e.g. an inert
|
|
96
|
+
* `selectable={false}` highlight band, where the auto-label would just show
|
|
97
|
+
* raw axis values. */
|
|
98
|
+
label?: string | false;
|
|
80
99
|
/** Stable consumer id — a click (or double-click outside edit) reports it via
|
|
81
100
|
* `onSelectAnnotation`. */
|
|
82
101
|
id?: string;
|
package/dist/annotations.js
CHANGED
|
@@ -226,6 +226,17 @@ function snapToGuides(container, selfKey, px) {
|
|
|
226
226
|
}
|
|
227
227
|
return best;
|
|
228
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Order two region bounds so `from ≤ to`. A region **edge resize** pivots around
|
|
231
|
+
* the *opposite* (fixed) edge: the dragged value `v` and the pivot are ordered
|
|
232
|
+
* here, so dragging an edge past the pivot never emits an inverted span — instead
|
|
233
|
+
* the edges meet (zero width) and a continued drag **re-opens the region the other
|
|
234
|
+
* way** (the grabbed handle becomes the far edge). The pivot is captured at
|
|
235
|
+
* drag-start (a ref) so it stays fixed even as the emitted bounds swap on cross.
|
|
236
|
+
*/
|
|
237
|
+
export function orderRegion(v, pivot) {
|
|
238
|
+
return v <= pivot ? { from: v, to: pivot } : { from: pivot, to: v };
|
|
239
|
+
}
|
|
229
240
|
/** A label chip — the cursor value flag's shape (shared {@link flagChipStyle}:
|
|
230
241
|
* filled, no outline) with text in the annotation register. */
|
|
231
242
|
function Chip({ theme, color, style, children, }) {
|
|
@@ -301,6 +312,20 @@ function DragArea({ x, y, w, h, cursor, editable, onHover, onSelect, onEdit, onD
|
|
|
301
312
|
dragging.current = false;
|
|
302
313
|
onHover(false);
|
|
303
314
|
},
|
|
315
|
+
// A system gesture takeover fires pointercancel, not pointerup — clear the
|
|
316
|
+
// same drag/hover state so the mark doesn't stay stuck "grabbed".
|
|
317
|
+
onPointerCancel: (e) => {
|
|
318
|
+
if (!dragging.current)
|
|
319
|
+
return;
|
|
320
|
+
try {
|
|
321
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
/* ignore */
|
|
325
|
+
}
|
|
326
|
+
dragging.current = false;
|
|
327
|
+
onHover(false);
|
|
328
|
+
},
|
|
304
329
|
// Click (no drag) selects; double-click edits. Stop both so a mark click
|
|
305
330
|
// never reaches the plot's data-select / deselect.
|
|
306
331
|
onClick: (e) => {
|
|
@@ -323,7 +348,8 @@ export function Marker({ at, label, id, selected = false, selectable = true, hov
|
|
|
323
348
|
container.creating === null &&
|
|
324
349
|
onChange !== undefined;
|
|
325
350
|
const xs = useMemo(() => [at], [at]);
|
|
326
|
-
|
|
351
|
+
// `label === false` (or '') ⇒ no chip; omitted ⇒ auto-label off the x formatter.
|
|
352
|
+
const text = label === false ? '' : (label ?? container.formatTime(at));
|
|
327
353
|
useRegisterAnnotation(container, selfKey, id, row.rowKey, 'marker', xs, selected, selectable, editing, text);
|
|
328
354
|
// No select/edit while a create tool is armed — the chart is in draw mode then.
|
|
329
355
|
const select = id !== undefined && container.creating === null
|
|
@@ -338,10 +364,10 @@ export function Marker({ at, label, id, selected = false, selectable = true, hov
|
|
|
338
364
|
const showHandle = editable && (editing || hovering);
|
|
339
365
|
const lane = container.labelLanes.get(selfKey) ?? 0;
|
|
340
366
|
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: container.plotWidth, height: h, style: overlayStyle, children: [_jsx("line", { x1: x, y1: 0, x2: x, y2: h, stroke: ann.color, strokeWidth: 1, opacity: opacity, shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: x, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color })), selectable && (_jsx(DragArea, { x: x - HIT_PAD, y: 0, w: 2 * HIT_PAD, h: h, cursor: editing ? 'ew-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (px) => onChange?.(snapToGuides(container, selfKey, px) ??
|
|
341
|
-
+container.xScale.invert(px)) }))] }), _jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
367
|
+
+container.xScale.invert(px)) }))] }), text && (_jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
342
368
|
top: `${FLAG_TOP + lane * LANE_H}px`,
|
|
343
369
|
...flagChipX(x, container.plotWidth),
|
|
344
|
-
}, children: text })] }));
|
|
370
|
+
}, children: text }))] }));
|
|
345
371
|
}
|
|
346
372
|
/** A horizontal line at a y value, scaled against one row axis (RTC's `Baseline`).
|
|
347
373
|
* Its label anchors at the left, at the line's height. */
|
|
@@ -358,7 +384,11 @@ export function Baseline({ value, axis, label, id, selected = false, selectable
|
|
|
358
384
|
// tracked for ordering / future use). No snap target either (the guidelines are
|
|
359
385
|
// vertical; a baseline drags vertically).
|
|
360
386
|
const xs = useMemo(() => [], []);
|
|
361
|
-
useRegisterAnnotation(container, selfKey, id, row.rowKey, 'baseline', xs, selected, selectable, editing,
|
|
387
|
+
useRegisterAnnotation(container, selfKey, id, row.rowKey, 'baseline', xs, selected, selectable, editing,
|
|
388
|
+
// Baselines don't lane-pack (the label anchors at their y, not the top), so
|
|
389
|
+
// this registered string is unused by `computeLabelLanes` — `|| ''` just
|
|
390
|
+
// keeps it a string for `false`/'' (which mean "no label").
|
|
391
|
+
label || '');
|
|
362
392
|
// No select/edit while a create tool is armed — the chart is in draw mode then.
|
|
363
393
|
const select = id !== undefined && container.creating === null
|
|
364
394
|
? () => container.onSelectAnnotation?.(id)
|
|
@@ -377,10 +407,11 @@ export function Baseline({ value, axis, label, id, selected = false, selectable
|
|
|
377
407
|
const opacity = rampAt(ann.depth, lineLevel(selectable, editable, hovering, selected));
|
|
378
408
|
const showHandle = editable && (editing || hovering);
|
|
379
409
|
const fmt = row.formats.get(axisId);
|
|
380
|
-
|
|
410
|
+
// `label === false` (or '') ⇒ no chip; omitted ⇒ format `value` off the axis.
|
|
411
|
+
const text = label === false ? '' : (label ?? (fmt ? fmt(value) : String(value)));
|
|
381
412
|
// Handle pill near the right end (clears the left-anchored label).
|
|
382
413
|
const handleX = w - 14;
|
|
383
|
-
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: w, height: row.height, style: overlayStyle, children: [_jsx("line", { x1: 0, y1: y, x2: w, y2: y, stroke: ann.color, strokeWidth: 1, opacity: opacity, shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: handleX, cy: y, w: HANDLE_LONG, h: HANDLE_SHORT, color: ann.color })), selectable && (_jsx(DragArea, { x: 0, y: y - HIT_PAD, w: w, h: 2 * HIT_PAD, cursor: editing ? 'ns-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (_px, py) => onChange?.(yScale.invert(py)) }))] }), _jsx(Chip, { theme: container.theme, color: ann.color, style: { top: `${y}px`, left: '2px', transform: 'translateY(-50%)' }, children: text })] }));
|
|
414
|
+
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: w, height: row.height, style: overlayStyle, children: [_jsx("line", { x1: 0, y1: y, x2: w, y2: y, stroke: ann.color, strokeWidth: 1, opacity: opacity, shapeRendering: "crispEdges" }), showHandle && (_jsx(Pill, { cx: handleX, cy: y, w: HANDLE_LONG, h: HANDLE_SHORT, color: ann.color })), selectable && (_jsx(DragArea, { x: 0, y: y - HIT_PAD, w: w, h: 2 * HIT_PAD, cursor: editing ? 'ns-resize' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDrag: (_px, py) => onChange?.(yScale.invert(py)) }))] }), text && (_jsx(Chip, { theme: container.theme, color: ann.color, style: { top: `${y}px`, left: '2px', transform: 'translateY(-50%)' }, children: text }))] }));
|
|
384
415
|
}
|
|
385
416
|
/** A shaded span over an x range — a lap, a zone, a selected interval. Its label
|
|
386
417
|
* flies as a flag off the left edge. */
|
|
@@ -394,7 +425,10 @@ export function Region({ from, to, label, id, selected = false, selectable = tru
|
|
|
394
425
|
container.creating === null &&
|
|
395
426
|
onChange !== undefined;
|
|
396
427
|
const xs = useMemo(() => [from, to], [from, to]);
|
|
397
|
-
|
|
428
|
+
// `label === false` (or '') ⇒ no chip; omitted ⇒ auto-label the `from–to` span.
|
|
429
|
+
const text = label === false
|
|
430
|
+
? ''
|
|
431
|
+
: (label ?? `${container.formatTime(from)}–${container.formatTime(to)}`);
|
|
398
432
|
useRegisterAnnotation(container, selfKey, id, row.rowKey, 'region', xs, selected, selectable, editing, text);
|
|
399
433
|
// No select/edit while a create tool is armed — the chart is in draw mode then.
|
|
400
434
|
const select = id !== undefined && container.creating === null
|
|
@@ -421,6 +455,10 @@ export function Region({ from, to, label, id, selected = false, selectable = tru
|
|
|
421
455
|
// accumulator — so once you drag past SNAP_PX the region releases cleanly
|
|
422
456
|
// instead of re-snapping on every small move.
|
|
423
457
|
const dragRef = useRef(null);
|
|
458
|
+
// Edge resize pivots around the OPPOSITE edge, captured on press so it stays put
|
|
459
|
+
// even after the dragged edge crosses it — {@link orderRegion} then re-opens the
|
|
460
|
+
// region the other way instead of dead-ending at zero width.
|
|
461
|
+
const edgeRef = useRef(null);
|
|
424
462
|
const edge = (atX) => (_jsx("line", { x1: atX, y1: 0, x2: atX, y2: h, stroke: ann.color, strokeWidth: 1, opacity: edgeOpacity, shapeRendering: "crispEdges" }));
|
|
425
463
|
return (_jsxs(_Fragment, { children: [_jsxs("svg", { width: container.plotWidth, height: h, style: overlayStyle, children: [_jsx("rect", { x: left, y: 0, width: spanW, height: h, fill: ann.color, opacity: fillOpacity }), edge(xa), edge(xb), showHandles && (_jsxs(_Fragment, { children: [_jsx(Pill, { cx: xa, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color }), _jsx(Pill, { cx: xb, cy: h / 2, w: HANDLE_SHORT, h: HANDLE_LONG, color: ann.color })] })), selectable && (_jsxs(_Fragment, { children: [_jsx(DragArea, { x: left, y: 0, w: spanW, h: h, cursor: editing ? 'grab' : 'inherit', editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragStart: (px) => {
|
|
426
464
|
dragRef.current = { from, to, startPx: px };
|
|
@@ -447,17 +485,15 @@ export function Region({ from, to, label, id, selected = false, selectable = tru
|
|
|
447
485
|
nt = st;
|
|
448
486
|
}
|
|
449
487
|
onChange?.({ from: nf, to: nt });
|
|
450
|
-
} }), editable && (_jsxs(_Fragment, { children: [_jsx(DragArea, { x: xa - EDGE_GRAB / 2, y: 0, w: EDGE_GRAB, h: h, cursor: "ew-resize", editable: editable, onHover: reportHover, onSelect: select, onEdit: edit,
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
to,
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
+container.xScale.invert(px),
|
|
458
|
-
}) })] }))] }))] }), _jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
488
|
+
} }), editable && (_jsxs(_Fragment, { children: [_jsx(DragArea, { x: xa - EDGE_GRAB / 2, y: 0, w: EDGE_GRAB, h: h, cursor: "ew-resize", editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragStart: () => {
|
|
489
|
+
edgeRef.current = to; // the fixed pivot = the far edge
|
|
490
|
+
}, onDrag: (px) => onChange?.(orderRegion(snapToGuides(container, selfKey, px) ??
|
|
491
|
+
+container.xScale.invert(px), edgeRef.current ?? to)) }), _jsx(DragArea, { x: xb - EDGE_GRAB / 2, y: 0, w: EDGE_GRAB, h: h, cursor: "ew-resize", editable: editable, onHover: reportHover, onSelect: select, onEdit: edit, onDragStart: () => {
|
|
492
|
+
edgeRef.current = from; // the fixed pivot = the near edge
|
|
493
|
+
}, onDrag: (px) => onChange?.(orderRegion(snapToGuides(container, selfKey, px) ??
|
|
494
|
+
+container.xScale.invert(px), edgeRef.current ?? from)) })] }))] }))] }), text && (_jsx(Chip, { theme: container.theme, color: ann.color, style: {
|
|
459
495
|
top: `${FLAG_TOP + lane * LANE_H}px`,
|
|
460
496
|
...flagChipX(left, container.plotWidth),
|
|
461
|
-
}, children: text })] }));
|
|
497
|
+
}, children: text }))] }));
|
|
462
498
|
}
|
|
463
499
|
//# sourceMappingURL=annotations.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@pond-ts/react": "^0.
|
|
42
|
-
"pond-ts": "^0.
|
|
41
|
+
"@pond-ts/react": "^0.33.0",
|
|
42
|
+
"pond-ts": "^0.33.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|