timeline-panel 1.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/README.md +158 -0
- package/dist/Legend.d.ts +5 -0
- package/dist/Legend.js +21 -0
- package/dist/TimelinePanel.d.ts +4 -0
- package/dist/TimelinePanel.js +63 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +6 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +93 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Timeline Panel
|
|
2
|
+
|
|
3
|
+
A flexible React component for displaying time-based tariff/schedule panels with dynamic grouping and legends.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🕐 **Flexible Time Display** - Display time ranges with visual timeline
|
|
8
|
+
- 🎨 **Customizable Legends** - Support custom color schemes with fallback to built-in defaults
|
|
9
|
+
- 📊 **Dynamic Grouping** - Group data by any criteria using custom filter functions
|
|
10
|
+
- 🔧 **Dynamic Properties** - Support for arbitrary additional properties in data items
|
|
11
|
+
- ⚡ **Performance Optimized** - Uses React.memo for efficient re-rendering
|
|
12
|
+
- 🎯 **TypeScript Support** - Full TypeScript support with type safety
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install timeline-panel
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Basic Usage
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import React, { useState } from 'react';
|
|
24
|
+
import TimelinePanel, { TariffItem } from 'timeline-panel';
|
|
25
|
+
|
|
26
|
+
const testData: TariffItem[] = [
|
|
27
|
+
{
|
|
28
|
+
start_time: '00:00',
|
|
29
|
+
end_time: '09:00',
|
|
30
|
+
name: 'summer',
|
|
31
|
+
tou_desc: 'work(A)'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
start_time: '08:00',
|
|
35
|
+
end_time: '18:00',
|
|
36
|
+
name: 'rain',
|
|
37
|
+
tou_desc: 'sleep(B)'
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function App() {
|
|
42
|
+
const [errorMap, setErrorMap] = useState<TariffItem[]>([]);
|
|
43
|
+
|
|
44
|
+
const customLegend = {
|
|
45
|
+
'work(A)': '#ff0000',
|
|
46
|
+
'sleep(B)': '#ffbf00',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const groupConfig = {
|
|
50
|
+
summer: {
|
|
51
|
+
title: 'summer',
|
|
52
|
+
filterFn: (item: TariffItem) => item.name === 'summer'
|
|
53
|
+
},
|
|
54
|
+
rain: {
|
|
55
|
+
title: 'rain',
|
|
56
|
+
filterFn: (item: TariffItem) => item.name === 'rain'
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<TimelinePanel
|
|
62
|
+
testData={testData}
|
|
63
|
+
setErrorMap={setErrorMap}
|
|
64
|
+
legendData={customLegend}
|
|
65
|
+
groupConfig={groupConfig}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API Reference
|
|
72
|
+
|
|
73
|
+
### TimelinePanel Props
|
|
74
|
+
|
|
75
|
+
| Property | Type | Default | Description |
|
|
76
|
+
|----------|------|---------|-------------|
|
|
77
|
+
| `testData` | `TariffItem[]` | **Required** - Array of time data items |
|
|
78
|
+
| `setErrorMap` | `React.Dispatch<React.SetStateAction<TariffItem[]>>` | **Required** - State setter for error items |
|
|
79
|
+
| `width` | `string \| number` | `'100%'` | Component width |
|
|
80
|
+
| `height` | `string \| number` | `'auto'` | Component height |
|
|
81
|
+
| `mt` | `string \| number` | `undefined` | Margin top |
|
|
82
|
+
| `legendData` | `Record<string, string>` | `undefined` | Custom legend colors |
|
|
83
|
+
| `groupConfig` | `Record<string, GroupConfig>` | `undefined` | Grouping configuration |
|
|
84
|
+
|
|
85
|
+
### TariffItem Interface
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
interface TariffItem {
|
|
89
|
+
id?: number;
|
|
90
|
+
start_mn?: string; // "MM-DD"
|
|
91
|
+
end_mn?: string; // "MM-DD"
|
|
92
|
+
start_week?: number; // 1=Mon ... 7=Sun
|
|
93
|
+
end_week?: number; // 1=Mon ... 7=Sun
|
|
94
|
+
start_time?: string; // "HH:mm"
|
|
95
|
+
end_time?: string; // "HH:mm"
|
|
96
|
+
tou_desc?: string; // Time period description
|
|
97
|
+
tou_type?: string; // Short code
|
|
98
|
+
[key: string]: any; // Additional properties
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### GroupConfig Interface
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
interface GroupConfig {
|
|
106
|
+
title: string; // Display title
|
|
107
|
+
filterFn: (item: TariffItem) => boolean; // Filter function
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Advanced Features
|
|
112
|
+
|
|
113
|
+
### Custom Grouping
|
|
114
|
+
|
|
115
|
+
Group your data by any criteria:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
const groupConfig = {
|
|
119
|
+
workingHours: {
|
|
120
|
+
title: 'Working Hours',
|
|
121
|
+
filterFn: (item) => item.start_time >= '09:00' && item.end_time <= '17:00'
|
|
122
|
+
},
|
|
123
|
+
weekends: {
|
|
124
|
+
title: 'Weekends',
|
|
125
|
+
filterFn: (item) => item.start_week >= 6
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Dynamic Properties
|
|
131
|
+
|
|
132
|
+
Add any custom properties to your data:
|
|
133
|
+
|
|
134
|
+
```tsx
|
|
135
|
+
const testData = [
|
|
136
|
+
{
|
|
137
|
+
start_time: '09:00',
|
|
138
|
+
end_time: '17:00',
|
|
139
|
+
tou_desc: 'Work',
|
|
140
|
+
priority: 'high', // Custom property
|
|
141
|
+
department: 'IT', // Custom property
|
|
142
|
+
cost: 150.75 // Custom property
|
|
143
|
+
}
|
|
144
|
+
];
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Built-in Legend Support
|
|
148
|
+
|
|
149
|
+
The component includes built-in legend items for common scenarios:
|
|
150
|
+
|
|
151
|
+
- `'空缺(V)'` - Empty slots (gray)
|
|
152
|
+
- `'重複(R)'` - Overlapping slots (black)
|
|
153
|
+
|
|
154
|
+
These will automatically display even if not provided in `legendData`.
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
package/dist/Legend.d.ts
ADDED
package/dist/Legend.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { LEGEND } from './utils';
|
|
3
|
+
const Legend = ({ legend }) => {
|
|
4
|
+
const mergedLegend = Object.assign(Object.assign({}, LEGEND), legend);
|
|
5
|
+
return (_jsx("div", { style: { marginTop: '15px', width: '150px', textAlign: 'center' }, children: _jsx("div", { style: { border: '1px solid #000' }, children: Object.entries(mergedLegend).map(([label, color]) => (_jsxs("div", { style: {
|
|
6
|
+
display: 'flex',
|
|
7
|
+
justifyContent: 'space-between',
|
|
8
|
+
alignItems: 'center',
|
|
9
|
+
border: '1px solid #000',
|
|
10
|
+
}, children: [_jsx("div", { style: {
|
|
11
|
+
borderRight: '2px solid #000',
|
|
12
|
+
width: '150px',
|
|
13
|
+
padding: '2px',
|
|
14
|
+
}, children: label }), _jsx("div", { style: { padding: '2px', width: '30px' }, children: _jsx("div", { style: {
|
|
15
|
+
width: '100%',
|
|
16
|
+
height: '10px',
|
|
17
|
+
border: '1px solid #000',
|
|
18
|
+
backgroundColor: color,
|
|
19
|
+
} }) })] }, label))) }) }));
|
|
20
|
+
};
|
|
21
|
+
export default Legend;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useEffect, useState } from 'react';
|
|
3
|
+
import { LEGEND, tou_descColor, splitTariffData, markOverlap, timeRangePercentage } from './utils';
|
|
4
|
+
import Legend from './Legend';
|
|
5
|
+
const TimelinePanel = ({ width = '100%', height = 'auto', mt, testData, setErrorMap, legendData, groupConfig }) => {
|
|
6
|
+
const [tariffGroups, setTariffGroups] = useState({});
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const groups = splitTariffData(testData, groupConfig);
|
|
9
|
+
const markedGroups = {};
|
|
10
|
+
const allErrorItems = [];
|
|
11
|
+
Object.entries(groups).forEach(([key, group]) => {
|
|
12
|
+
markedGroups[key] = {
|
|
13
|
+
title: group.title,
|
|
14
|
+
data: markOverlap(group.data)
|
|
15
|
+
};
|
|
16
|
+
const errorItems = markedGroups[key].data.filter((item) => item.tou_type === 'r' || item.tou_type === 'v');
|
|
17
|
+
allErrorItems.push(...errorItems);
|
|
18
|
+
});
|
|
19
|
+
setErrorMap(allErrorItems);
|
|
20
|
+
setTariffGroups(markedGroups);
|
|
21
|
+
}, [testData]);
|
|
22
|
+
return (_jsxs("div", { style: {
|
|
23
|
+
width,
|
|
24
|
+
height,
|
|
25
|
+
marginTop: mt,
|
|
26
|
+
padding: 16,
|
|
27
|
+
border: '1px solid #ddd',
|
|
28
|
+
borderRadius: 8,
|
|
29
|
+
display: 'flex',
|
|
30
|
+
flexDirection: 'row',
|
|
31
|
+
}, children: [_jsx("div", { style: { width: '90%' }, children: testData.length < 1 ? (_jsx("div", { style: { textAlign: 'center' }, children: "No Data" })) : (_jsx(_Fragment, { children: Object.values(tariffGroups).map((group) => {
|
|
32
|
+
const { title, data } = group;
|
|
33
|
+
if (!data.length)
|
|
34
|
+
return null;
|
|
35
|
+
return (_jsxs("div", { style: { display: 'flex', width: '90%', height: '60px', padding: '0 5%', marginBottom: '5px' }, children: [_jsx("h3", { style: { width: '20%', alignSelf: 'center' }, children: title }), data.map((item, index) => {
|
|
36
|
+
const isLast = index === data.length - 1;
|
|
37
|
+
const widthPercent = timeRangePercentage(item.start_time, item.end_time);
|
|
38
|
+
const bgColor = tou_descColor(item.tou_desc, legendData || LEGEND);
|
|
39
|
+
return (_jsxs("div", { style: {
|
|
40
|
+
width: `${widthPercent}%`,
|
|
41
|
+
display: 'flex',
|
|
42
|
+
justifyContent: 'center',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
position: 'relative',
|
|
45
|
+
}, children: [_jsx("div", { style: { height: '50px', background: 'black', width: '5px', position: 'absolute', left: 0 } }), _jsx("div", { style: {
|
|
46
|
+
height: '10px',
|
|
47
|
+
background: 'white',
|
|
48
|
+
position: 'absolute',
|
|
49
|
+
top: 0,
|
|
50
|
+
left: '-18px',
|
|
51
|
+
lineHeight: '10px',
|
|
52
|
+
}, title: item.start_time || '', children: item.start_time || '' }), _jsx("div", { style: { height: '10px', width: '100%', background: bgColor, cursor: 'pointer' }, title: item.tou_desc || '' }), isLast && (_jsxs(_Fragment, { children: [_jsx("div", { style: { height: '50px', background: 'black', width: '5px' } }), _jsx("div", { style: {
|
|
53
|
+
height: '10px',
|
|
54
|
+
background: 'white',
|
|
55
|
+
position: 'absolute',
|
|
56
|
+
top: 0,
|
|
57
|
+
right: '-18px',
|
|
58
|
+
lineHeight: '10px',
|
|
59
|
+
}, children: "24:00" })] }))] }, `${index + (item.start_time || '') + (item.end_time || '')}`));
|
|
60
|
+
})] }, title));
|
|
61
|
+
}) })) }), _jsx(Legend, { legend: legendData || LEGEND })] }));
|
|
62
|
+
};
|
|
63
|
+
export default memo(TimelinePanel);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { default as TimelinePanel } from './TimelinePanel';
|
|
2
|
+
export type { TariffItem, TimelinePanelProps, GroupConfig } from './types';
|
|
3
|
+
export { LEGEND, tou_descColor, splitTariffData, markOverlap, timeRangePercentage } from './utils';
|
|
4
|
+
export { default as Legend } from './Legend';
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface TariffItem {
|
|
2
|
+
id?: number;
|
|
3
|
+
start_mn?: string;
|
|
4
|
+
end_mn?: string;
|
|
5
|
+
start_week?: number;
|
|
6
|
+
end_week?: number;
|
|
7
|
+
start_time?: string;
|
|
8
|
+
end_time?: string;
|
|
9
|
+
tou_desc?: string;
|
|
10
|
+
tou_type?: string;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
export type GroupConfig = {
|
|
14
|
+
title: string;
|
|
15
|
+
filterFn: (item: TariffItem) => boolean;
|
|
16
|
+
};
|
|
17
|
+
export type TimelinePanelProps = {
|
|
18
|
+
width?: string | number;
|
|
19
|
+
height?: string | number;
|
|
20
|
+
mt?: string | number;
|
|
21
|
+
testData: TariffItem[];
|
|
22
|
+
setErrorMap: React.Dispatch<React.SetStateAction<TariffItem[]>>;
|
|
23
|
+
legendData?: Record<string, string>;
|
|
24
|
+
groupConfig?: Record<string, GroupConfig>;
|
|
25
|
+
[key: string]: any;
|
|
26
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { TariffItem, GroupConfig } from './types';
|
|
2
|
+
export declare const LEGEND: Record<string, string>;
|
|
3
|
+
export declare const toMinutes: (time: string) => number;
|
|
4
|
+
export declare const timeRangePercentage: (start_time: string | undefined, end_time: string | undefined) => number;
|
|
5
|
+
export declare const tou_descColor: (tou_desc: string | undefined, legend: Record<string, string>) => string;
|
|
6
|
+
export declare const splitTariffData: (data: TariffItem[], groupConfig?: Record<string, GroupConfig>) => Record<string, {
|
|
7
|
+
title: string;
|
|
8
|
+
data: TariffItem[];
|
|
9
|
+
}>;
|
|
10
|
+
export declare const markOverlap: (tou_descs: TariffItem[]) => TariffItem[];
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Built-in legend colors
|
|
2
|
+
export const LEGEND = {
|
|
3
|
+
'empty(V)': '#BBB',
|
|
4
|
+
'overlap(R)': '#000',
|
|
5
|
+
};
|
|
6
|
+
// Time utilities
|
|
7
|
+
export const toMinutes = (time) => {
|
|
8
|
+
const [h, m] = time.split(':').map(Number);
|
|
9
|
+
return h * 60 + m;
|
|
10
|
+
};
|
|
11
|
+
export const timeRangePercentage = (start_time, end_time) => {
|
|
12
|
+
if (!start_time || !end_time)
|
|
13
|
+
return 0;
|
|
14
|
+
const startMinutes = toMinutes(start_time);
|
|
15
|
+
let endMinutes = toMinutes(end_time);
|
|
16
|
+
if (endMinutes <= startMinutes)
|
|
17
|
+
endMinutes += 24 * 60;
|
|
18
|
+
return ((endMinutes - startMinutes) / (24 * 60)) * 100;
|
|
19
|
+
};
|
|
20
|
+
// Color utilities
|
|
21
|
+
export const tou_descColor = (tou_desc, legend) => {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
if (!tou_desc)
|
|
24
|
+
return 'gray';
|
|
25
|
+
return (_b = (_a = legend[tou_desc]) !== null && _a !== void 0 ? _a : LEGEND[tou_desc]) !== null && _b !== void 0 ? _b : 'gray';
|
|
26
|
+
};
|
|
27
|
+
// Data processing utilities
|
|
28
|
+
export const splitTariffData = (data, groupConfig) => {
|
|
29
|
+
const result = {};
|
|
30
|
+
if (groupConfig) {
|
|
31
|
+
Object.entries(groupConfig).forEach(([groupKey, config]) => {
|
|
32
|
+
const filteredData = data.filter(config.filterFn);
|
|
33
|
+
if (filteredData.length > 0) {
|
|
34
|
+
result[groupKey] = {
|
|
35
|
+
title: config.title,
|
|
36
|
+
data: filteredData
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const groups = {};
|
|
43
|
+
data.forEach((d) => {
|
|
44
|
+
const groupName = d.tou_desc || 'default';
|
|
45
|
+
if (!groups[groupName]) {
|
|
46
|
+
groups[groupName] = [];
|
|
47
|
+
}
|
|
48
|
+
groups[groupName].push(d);
|
|
49
|
+
});
|
|
50
|
+
Object.entries(groups).forEach(([groupName, groupData]) => {
|
|
51
|
+
result[groupName] = {
|
|
52
|
+
title: groupName,
|
|
53
|
+
data: groupData
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
export const markOverlap = (tou_descs) => {
|
|
60
|
+
var _a, _b, _c, _d;
|
|
61
|
+
if (!tou_descs.length)
|
|
62
|
+
return [];
|
|
63
|
+
const timePoints = new Set();
|
|
64
|
+
tou_descs.forEach((p) => {
|
|
65
|
+
if (p.start_time)
|
|
66
|
+
timePoints.add(p.start_time);
|
|
67
|
+
if (p.end_time)
|
|
68
|
+
timePoints.add(p.end_time);
|
|
69
|
+
});
|
|
70
|
+
const sortedTimes = Array.from(timePoints).sort((a, b) => toMinutes(a) - toMinutes(b));
|
|
71
|
+
const result = [];
|
|
72
|
+
for (let i = 0; i < sortedTimes.length - 1; i++) {
|
|
73
|
+
const segStart = sortedTimes[i];
|
|
74
|
+
const segEnd = sortedTimes[i + 1];
|
|
75
|
+
const covering = tou_descs.filter((p) => p.start_time && p.end_time && p.start_time < segEnd && p.end_time > segStart);
|
|
76
|
+
if (covering.length === 0) {
|
|
77
|
+
result.push(Object.assign(Object.assign({}, tou_descs[0]), { start_time: segStart, end_time: segEnd, tou_desc: 'empty(V)', tou_type: 'v' }));
|
|
78
|
+
}
|
|
79
|
+
else if (covering.length === 1) {
|
|
80
|
+
result.push(Object.assign(Object.assign({}, covering[0]), { start_time: segStart, end_time: segEnd }));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
result.push(Object.assign(Object.assign({}, covering[0]), { start_time: segStart, end_time: segEnd, tou_desc: 'overlap(R)', tou_type: 'r' }));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (((_a = result[0]) === null || _a === void 0 ? void 0 : _a.start_time) !== '00:00') {
|
|
87
|
+
result.unshift(Object.assign(Object.assign({}, tou_descs[0]), { start_time: '00:00', end_time: ((_b = result[0]) === null || _b === void 0 ? void 0 : _b.start_time) || '00:00', tou_desc: 'empty(V)', tou_type: 'v' }));
|
|
88
|
+
}
|
|
89
|
+
if (((_c = result[result.length - 1]) === null || _c === void 0 ? void 0 : _c.end_time) !== '24:00') {
|
|
90
|
+
result.push(Object.assign(Object.assign({}, tou_descs[0]), { start_time: ((_d = result[result.length - 1]) === null || _d === void 0 ? void 0 : _d.end_time) || '24:00', end_time: '24:00', tou_desc: 'empty(V)', tou_type: 'v' }));
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "timeline-panel",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A flexible React component for displaying time-based tariff/schedule panels",
|
|
5
|
+
"main": "dist/TimelinePanel.js",
|
|
6
|
+
"types": "dist/TimelinePanel.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"react",
|
|
16
|
+
"typescript",
|
|
17
|
+
"timeline",
|
|
18
|
+
"schedule",
|
|
19
|
+
"ui-component"
|
|
20
|
+
],
|
|
21
|
+
"author": "Your Name",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": ">=16.8.0",
|
|
25
|
+
"react-dom": ">=16.8.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/react": "^18.2.0",
|
|
29
|
+
"@types/react-dom": "^18.2.0",
|
|
30
|
+
"typescript": "^5.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|