@stll/ui 0.34.2 → 0.34.5
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/calendar/resource-calendar.js +4 -3
- package/dist/calendar/resource-calendar.logic.js +9 -9
- package/dist/components/button.d.ts +2 -2
- package/dist/components/color-picker.js +7 -7
- package/dist/components/destructive-confirm-dialog.js +1 -1
- package/dist/components/input-group.d.ts +1 -1
- package/dist/components/outline-rail.js +17 -10
- package/dist/components/sidebar.d.ts +1 -1
- package/dist/lib/control-size.d.ts +1 -1
- package/dist/lib/week.js +4 -2
- package/package.json +7 -3
|
@@ -3,6 +3,7 @@ import { CalendarCell, CalendarEntryButton, CalendarEntrySurface, CalendarHeader
|
|
|
3
3
|
import { assertConsecutiveCalendarDates, layoutResourceCalendarEntries, nextCalendarDate } from "./resource-calendar.logic.js";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { useId } from "react";
|
|
6
|
+
import { panic } from "better-result";
|
|
6
7
|
//#region src/calendar/resource-calendar.tsx
|
|
7
8
|
const ResourceCalendar = ({ ariaLabel, columns, empty, entries, onSelectEntry, resourceHeader, resources }) => {
|
|
8
9
|
assertResourceCalendarContract({
|
|
@@ -68,7 +69,7 @@ const ResourceCalendar = ({ ariaLabel, columns, empty, entries, onSelectEntry, r
|
|
|
68
69
|
const visibleEntriesByColumn = /* @__PURE__ */ new Map();
|
|
69
70
|
for (const placement of layout.placements) {
|
|
70
71
|
const entry = resourceEntryById.get(placement.entryId);
|
|
71
|
-
if (entry === void 0)
|
|
72
|
+
if (entry === void 0) panic(`Resource calendar layout references unknown entry ${placement.entryId}`);
|
|
72
73
|
const existing = visibleEntriesByColumn.get(placement.columnStart);
|
|
73
74
|
if (existing === void 0) {
|
|
74
75
|
visibleEntriesByColumn.set(placement.columnStart, [{
|
|
@@ -170,7 +171,7 @@ const ResourceCalendarEntryView = ({ entry, labelledBy, onSelectEntry, placement
|
|
|
170
171
|
});
|
|
171
172
|
};
|
|
172
173
|
const assertUniqueIds = (ids, label) => {
|
|
173
|
-
if (new Set(ids).size !== ids.length)
|
|
174
|
+
if (new Set(ids).size !== ids.length) panic(`${label} ids must be unique`);
|
|
174
175
|
};
|
|
175
176
|
const assertResourceCalendarContract = ({ columns, entries, resources }) => {
|
|
176
177
|
assertConsecutiveCalendarDates(columns.map(({ date }) => date));
|
|
@@ -178,7 +179,7 @@ const assertResourceCalendarContract = ({ columns, entries, resources }) => {
|
|
|
178
179
|
assertUniqueIds(entries.map(({ id }) => id), "Resource calendar entry");
|
|
179
180
|
const resourceIds = new Set(resources.map(({ id }) => id));
|
|
180
181
|
const orphan = entries.find(({ resourceId }) => !resourceIds.has(resourceId));
|
|
181
|
-
if (orphan !== void 0)
|
|
182
|
+
if (orphan !== void 0) panic(`Resource calendar entry ${orphan.id} references an unknown resource`);
|
|
182
183
|
};
|
|
183
184
|
//#endregion
|
|
184
185
|
export { ResourceCalendar };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Result } from "better-result";
|
|
1
|
+
import { Result, panic } from "better-result";
|
|
2
2
|
import { Temporal } from "temporal-polyfill/full";
|
|
3
3
|
//#region src/calendar/resource-calendar.logic.ts
|
|
4
4
|
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
|
|
@@ -9,13 +9,13 @@ const toPlainDate = (value) => {
|
|
|
9
9
|
const differenceInCalendarDays = (later, earlier) => {
|
|
10
10
|
const laterDate = toPlainDate(later);
|
|
11
11
|
const earlierDate = toPlainDate(earlier);
|
|
12
|
-
if (laterDate === null || earlierDate === null)
|
|
12
|
+
if (laterDate === null || earlierDate === null) panic("Calendar dates must use normalized YYYY-MM-DD values");
|
|
13
13
|
return laterDate.since(earlierDate, { largestUnit: "days" }).days;
|
|
14
14
|
};
|
|
15
15
|
const getResourceCalendarPlacement = ({ entry, visibleRange }) => {
|
|
16
16
|
const visibleDayCount = differenceInCalendarDays(visibleRange.endDateExclusive, visibleRange.startDate);
|
|
17
17
|
const entryDayCount = differenceInCalendarDays(entry.endDateExclusive, entry.startDate);
|
|
18
|
-
if (visibleDayCount <= 0 || entryDayCount <= 0)
|
|
18
|
+
if (visibleDayCount <= 0 || entryDayCount <= 0) panic("Calendar date ranges must be non-empty and half-open");
|
|
19
19
|
if (entry.startDate >= visibleRange.endDateExclusive || entry.endDateExclusive <= visibleRange.startDate) return null;
|
|
20
20
|
const clippedStart = entry.startDate < visibleRange.startDate ? visibleRange.startDate : entry.startDate;
|
|
21
21
|
const clippedEnd = entry.endDateExclusive > visibleRange.endDateExclusive ? visibleRange.endDateExclusive : entry.endDateExclusive;
|
|
@@ -25,27 +25,27 @@ const getResourceCalendarPlacement = ({ entry, visibleRange }) => {
|
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
27
|
const assertConsecutiveCalendarDates = (dates) => {
|
|
28
|
-
if (dates.length === 0)
|
|
28
|
+
if (dates.length === 0) panic("A resource calendar needs at least one date column");
|
|
29
29
|
const first = dates.at(0);
|
|
30
|
-
if (first === void 0 || toPlainDate(first) === null)
|
|
30
|
+
if (first === void 0 || toPlainDate(first) === null) panic("Resource calendar date columns must be consecutive normalized dates");
|
|
31
31
|
for (let index = 1; index < dates.length; index += 1) {
|
|
32
32
|
const previous = dates.at(index - 1);
|
|
33
33
|
const current = dates.at(index);
|
|
34
|
-
if (previous === void 0 || current === void 0 || differenceInCalendarDays(current, previous) !== 1)
|
|
34
|
+
if (previous === void 0 || current === void 0 || differenceInCalendarDays(current, previous) !== 1) panic("Resource calendar date columns must be consecutive normalized dates");
|
|
35
35
|
}
|
|
36
36
|
};
|
|
37
37
|
const nextCalendarDate = (value) => {
|
|
38
38
|
const date = toPlainDate(value);
|
|
39
|
-
if (date === null)
|
|
39
|
+
if (date === null) panic("Calendar dates must use normalized YYYY-MM-DD values");
|
|
40
40
|
const nextDate = date.add({ days: 1 }).toString();
|
|
41
|
-
if (toPlainDate(nextDate) === null)
|
|
41
|
+
if (toPlainDate(nextDate) === null) panic("Calendar dates must have a following normalized YYYY-MM-DD value");
|
|
42
42
|
return nextDate;
|
|
43
43
|
};
|
|
44
44
|
const layoutResourceCalendarEntries = (entries, visibleRange) => {
|
|
45
45
|
const visibleEntries = [];
|
|
46
46
|
const entryIds = /* @__PURE__ */ new Set();
|
|
47
47
|
for (const entry of entries) {
|
|
48
|
-
if (entryIds.has(entry.id))
|
|
48
|
+
if (entryIds.has(entry.id)) panic("Resource calendar entry ids must be unique");
|
|
49
49
|
entryIds.add(entry.id);
|
|
50
50
|
const placement = getResourceCalendarPlacement({
|
|
51
51
|
entry,
|
|
@@ -13,8 +13,8 @@ import { VariantProps } from "class-variance-authority";
|
|
|
13
13
|
*/
|
|
14
14
|
declare const buttonAccessibleDisabledClass = "cursor-not-allowed opacity-64";
|
|
15
15
|
declare const buttonVariants: (props?: ({
|
|
16
|
-
size?: "
|
|
17
|
-
variant?: "link" | "default" | "
|
|
16
|
+
size?: "default" | "sm" | "lg" | "row" | "chip" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "xl" | "xs" | null | undefined;
|
|
17
|
+
variant?: "link" | "default" | "destructive" | "destructive-outline" | "destructive-ghost" | "ghost" | "muted" | "outline" | "secondary" | null | undefined;
|
|
18
18
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
19
19
|
type ButtonProps = {
|
|
20
20
|
variant?: VariantProps<typeof buttonVariants>["variant"];
|
|
@@ -170,8 +170,9 @@ const CustomColorControls = ({ handleInputChange, handlePickerChange, inputHex,
|
|
|
170
170
|
})
|
|
171
171
|
]
|
|
172
172
|
})] });
|
|
173
|
-
const InlineCustomColor = ({
|
|
174
|
-
const
|
|
173
|
+
const InlineCustomColor = ({ customHex, handleInputChange, handlePickerChange, inputHex, label, pickerHex, presets }) => {
|
|
174
|
+
const customSelected = customHex !== null;
|
|
175
|
+
const customColor = customSelected ? `#${customHex}` : void 0;
|
|
175
176
|
const customGradient = `conic-gradient(${presets.map((preset) => swatchColor(preset)).join(", ")})`;
|
|
176
177
|
return /* @__PURE__ */ jsxs(Popover.Root, { children: [/* @__PURE__ */ jsx(Popover.Trigger, {
|
|
177
178
|
render: /* @__PURE__ */ jsx("button", {
|
|
@@ -199,11 +200,11 @@ const InlineCustomColor = ({ customSelected, handleInputChange, handlePickerChan
|
|
|
199
200
|
})
|
|
200
201
|
}) })] });
|
|
201
202
|
};
|
|
202
|
-
const ColorPickerContent = ({ value, onSelect, onClear, presets, columns, defaultExpanded, moreLabel, presentation
|
|
203
|
+
const ColorPickerContent = ({ value, onSelect, onClear, presets, columns, defaultExpanded, moreLabel, presentation }) => {
|
|
203
204
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
204
205
|
const [pickerHex, setPickerHex] = useState(() => (looksLikeHex(value ?? "") ? value : "000000") ?? "000000");
|
|
205
206
|
const [inputHex, setInputHex] = useState("");
|
|
206
|
-
const
|
|
207
|
+
const customHex = !presets.some((preset) => preset.value === value) && value !== void 0 && looksLikeHex(value) ? value : null;
|
|
207
208
|
/** Called when the visual picker (SB square / hue strip) emits a color. */
|
|
208
209
|
const handlePickerChange = (hex) => {
|
|
209
210
|
const normalized = normalizeHex(hex);
|
|
@@ -231,14 +232,13 @@ const ColorPickerContent = ({ value, onSelect, onClear, presets, columns, defaul
|
|
|
231
232
|
presentation: "inline",
|
|
232
233
|
selected: value === preset.value
|
|
233
234
|
}, preset.value)), /* @__PURE__ */ jsx(InlineCustomColor, {
|
|
234
|
-
|
|
235
|
+
customHex,
|
|
235
236
|
handleInputChange,
|
|
236
237
|
handlePickerChange,
|
|
237
238
|
inputHex,
|
|
238
239
|
label: moreLabel,
|
|
239
240
|
pickerHex,
|
|
240
|
-
presets
|
|
241
|
-
value
|
|
241
|
+
presets
|
|
242
242
|
})]
|
|
243
243
|
});
|
|
244
244
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -18,7 +18,7 @@ const DestructiveConfirmDialog = ({ cancelLabel, confirmLabel, confirmation, des
|
|
|
18
18
|
} catch {}
|
|
19
19
|
};
|
|
20
20
|
const handleConfirmClick = () => {
|
|
21
|
-
handleConfirm().catch(
|
|
21
|
+
handleConfirm().catch(reportError);
|
|
22
22
|
};
|
|
23
23
|
return /* @__PURE__ */ jsx(AlertDialog, {
|
|
24
24
|
onOpenChange: handleOpenChange,
|
|
@@ -5,7 +5,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
5
5
|
//#region src/components/input-group.d.ts
|
|
6
6
|
declare const InputGroup: ({ className, ...props }: React$1.ComponentProps<"div">) => React$1.JSX.Element;
|
|
7
7
|
declare const inputGroupAddonVariants: (props?: ({
|
|
8
|
-
align?: "inline-
|
|
8
|
+
align?: "inline-start" | "block-end" | "block-start" | "inline-end" | null | undefined;
|
|
9
9
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
10
10
|
declare const InputGroupAddon: ({ className, align, ...props }: React$1.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) => React$1.JSX.Element;
|
|
11
11
|
declare const InputGroupText: ({ className, ...props }: React$1.ComponentProps<"span">) => React$1.JSX.Element;
|
|
@@ -88,7 +88,7 @@ const Chevron = ({ open }) => /* @__PURE__ */ jsx(DirectionalIcon, {
|
|
|
88
88
|
icon: ChevronRight
|
|
89
89
|
});
|
|
90
90
|
const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId, header, collapsedFromLevel, topOffset = 0, panelWidth = 300, ariaLabel = "Outline" }) => {
|
|
91
|
-
const [pctById, setPctById] = useState(
|
|
91
|
+
const [pctById, setPctById] = useState(/* @__PURE__ */ new Map());
|
|
92
92
|
const [derivedActive, setDerivedActive] = useState(null);
|
|
93
93
|
const [hovered, setHovered] = useState(false);
|
|
94
94
|
const [pinned, setPinned] = useState(false);
|
|
@@ -133,10 +133,10 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
|
|
|
133
133
|
const recalc = useCallback(() => {
|
|
134
134
|
const container = scrollContainerRef.current;
|
|
135
135
|
if (!container) return;
|
|
136
|
-
const next =
|
|
136
|
+
const next = /* @__PURE__ */ new Map();
|
|
137
137
|
for (const item of items) {
|
|
138
138
|
const pct = resolvePct(item.id, container);
|
|
139
|
-
if (pct !== null) next
|
|
139
|
+
if (pct !== null) next.set(item.id, pct);
|
|
140
140
|
}
|
|
141
141
|
setPctById(next);
|
|
142
142
|
}, [
|
|
@@ -324,10 +324,17 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
|
|
|
324
324
|
children: node.children.map(renderNode)
|
|
325
325
|
})] }, `${node.item.id}-${node.index}`);
|
|
326
326
|
};
|
|
327
|
-
const visibleTicks = items.
|
|
327
|
+
const visibleTicks = items.flatMap((item) => {
|
|
328
|
+
const pct = pctById.get(item.id);
|
|
329
|
+
return pct !== void 0 && item.level <= railLevelCap ? [{
|
|
330
|
+
item,
|
|
331
|
+
pct
|
|
332
|
+
}] : [];
|
|
333
|
+
});
|
|
328
334
|
if (items.length < 2 && header === void 0) return null;
|
|
329
|
-
const visibleTickIds = new Set(visibleTicks.map((item) => item.id));
|
|
330
|
-
const
|
|
335
|
+
const visibleTickIds = new Set(visibleTicks.map(({ item }) => item.id));
|
|
336
|
+
const ghostPct = hoveredId !== null && !visibleTickIds.has(hoveredId) ? pctById.get(hoveredId) : void 0;
|
|
337
|
+
const ghostItem = ghostPct === void 0 ? void 0 : items.find((item) => item.id === hoveredId);
|
|
331
338
|
return /* @__PURE__ */ jsxs("div", {
|
|
332
339
|
"aria-label": ariaLabel,
|
|
333
340
|
className: "absolute end-0 z-20 max-lg:hidden",
|
|
@@ -361,7 +368,7 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
|
|
|
361
368
|
onWheel: (event) => {
|
|
362
369
|
scrollContainerRef.current?.scrollBy(0, event.deltaY);
|
|
363
370
|
},
|
|
364
|
-
children: [visibleTicks.map((item) => {
|
|
371
|
+
children: [visibleTicks.map(({ item, pct }) => {
|
|
365
372
|
const isActive = active === item.id;
|
|
366
373
|
const isHovered = hoveredId === item.id;
|
|
367
374
|
return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, { render: /* @__PURE__ */ jsx("button", {
|
|
@@ -372,7 +379,7 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
|
|
|
372
379
|
onMouseEnter: () => setHoveredId(item.id),
|
|
373
380
|
onMouseLeave: () => setHoveredId(null),
|
|
374
381
|
style: {
|
|
375
|
-
top: `${
|
|
382
|
+
top: `${pct}%`,
|
|
376
383
|
transform: "translateY(-50%)",
|
|
377
384
|
width: isHovered ? tickWidth(item.level) + 8 : tickWidth(item.level),
|
|
378
385
|
height: tickHeight(isHovered, isActive),
|
|
@@ -380,11 +387,11 @@ const OutlineRail = ({ items, scrollContainerRef, resolvePct, onJump, activeId,
|
|
|
380
387
|
},
|
|
381
388
|
type: "button"
|
|
382
389
|
}) }), /* @__PURE__ */ jsx(TooltipPopup, { children: item.label })] }, item.id);
|
|
383
|
-
}), ghostItem && /* @__PURE__ */ jsx("span", {
|
|
390
|
+
}), ghostItem && ghostPct !== void 0 && /* @__PURE__ */ jsx("span", {
|
|
384
391
|
"aria-hidden": true,
|
|
385
392
|
className: "absolute end-0 rounded-full opacity-100",
|
|
386
393
|
style: {
|
|
387
|
-
top: `${
|
|
394
|
+
top: `${ghostPct}%`,
|
|
388
395
|
transform: "translateY(-50%)",
|
|
389
396
|
width: tickWidth(ghostItem.level) + 8,
|
|
390
397
|
height: 4,
|
|
@@ -83,7 +83,7 @@ declare const SidebarMenu: ({ className, ...props }: React.ComponentProps<"ul">)
|
|
|
83
83
|
declare const SidebarMenuItem: ({ className, ...props }: React.ComponentProps<"li">) => import("react").JSX.Element;
|
|
84
84
|
declare const sidebarMenuButtonVariants: (props?: ({
|
|
85
85
|
variant?: "default" | "outline" | null | undefined;
|
|
86
|
-
size?: "
|
|
86
|
+
size?: "default" | "sm" | "lg" | "rail" | null | undefined;
|
|
87
87
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
88
88
|
declare const SidebarMenuButton: ({ asChild, isActive, variant, size, tooltip, className, ...props }: React.ComponentProps<"button"> & {
|
|
89
89
|
asChild?: boolean;
|
|
@@ -5,6 +5,6 @@ declare const CONTROL_SIZE: Readonly<{
|
|
|
5
5
|
readonly lg: "lg";
|
|
6
6
|
}>;
|
|
7
7
|
type ControlSize = (typeof CONTROL_SIZE)[keyof typeof CONTROL_SIZE];
|
|
8
|
-
declare const CONTROL_SIZES: readonly ("
|
|
8
|
+
declare const CONTROL_SIZES: readonly ("default" | "sm" | "lg")[];
|
|
9
9
|
//#endregion
|
|
10
10
|
export { CONTROL_SIZE, CONTROL_SIZES, type ControlSize };
|
package/dist/lib/week.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Locale week data (first weekday, weekend) derived from CLDR via Intl.
|
|
4
4
|
* Shared so the app shell and the date picker resolve week starts identically.
|
|
5
5
|
*/
|
|
6
|
+
const isWeekInfo = (value) => typeof value === "object" && value !== null && "firstDay" in value && typeof value.firstDay === "number" && "weekend" in value && Array.isArray(value.weekend);
|
|
6
7
|
/**
|
|
7
8
|
* Read CLDR week info for a locale, tolerating the legacy `weekInfo` accessor
|
|
8
9
|
* on engines that predate `getWeekInfo()`. Returns undefined when unavailable
|
|
@@ -11,8 +12,9 @@
|
|
|
11
12
|
const getLocaleWeekInfo = (locale) => {
|
|
12
13
|
try {
|
|
13
14
|
const loc = new Intl.Locale(locale);
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
if (typeof loc.getWeekInfo === "function") return loc.getWeekInfo();
|
|
16
|
+
const legacy = "weekInfo" in loc ? loc.weekInfo : void 0;
|
|
17
|
+
return isWeekInfo(legacy) ? legacy : void 0;
|
|
16
18
|
} catch {
|
|
17
19
|
return;
|
|
18
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/ui",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.5",
|
|
4
4
|
"description": "Stella's design system: bidi-aware React primitives built on Base UI, the dockable inspector pane, and the Tailwind v4 theme they are styled with.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"base-ui",
|
|
@@ -349,6 +349,10 @@
|
|
|
349
349
|
"types": "./dist/inspector/index.d.ts",
|
|
350
350
|
"import": "./dist/inspector/index.js"
|
|
351
351
|
},
|
|
352
|
+
"./layout-tokens": {
|
|
353
|
+
"types": "./dist/inspector/layout-tokens.d.ts",
|
|
354
|
+
"import": "./dist/inspector/layout-tokens.js"
|
|
355
|
+
},
|
|
352
356
|
"./kanban": {
|
|
353
357
|
"types": "./dist/kanban/index.d.ts",
|
|
354
358
|
"import": "./dist/kanban/index.js"
|
|
@@ -607,7 +611,7 @@
|
|
|
607
611
|
"test:browser": "playwright test --config playwright.config.ts",
|
|
608
612
|
"pack:check": "cd ../.. && bun scripts/check-published-exports.ts packages/ui",
|
|
609
613
|
"typecheck": "bun ../../packages/scripts/src/tsc-native.ts --noEmit",
|
|
610
|
-
"lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --
|
|
614
|
+
"lint": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --report-unused-disable-directives-severity=error --type-aware packages/ui",
|
|
611
615
|
"lint:fix": "cd ../.. && bun --bun oxlint -c oxlint.config.ts --type-aware --fix packages/ui",
|
|
612
616
|
"format": "bun ../../scripts/run-oxfmt.ts .",
|
|
613
617
|
"prepack": "bun run build"
|
|
@@ -619,7 +623,7 @@
|
|
|
619
623
|
"input-otp": "^1.5.0",
|
|
620
624
|
"lucide-react": "1.41.0",
|
|
621
625
|
"tailwind-merge": "^3.7.0",
|
|
622
|
-
"temporal-polyfill": "1.0.
|
|
626
|
+
"temporal-polyfill": "1.0.5"
|
|
623
627
|
},
|
|
624
628
|
"devDependencies": {
|
|
625
629
|
"@atlaskit/pragmatic-drag-and-drop": "^3.1.0",
|