@gnist/design-system 3.5.6 → 3.7.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 +12 -0
- package/dist/components/feedback/alerts/AlertBanner.cjs +1 -1
- package/dist/components/feedback/alerts/AlertBanner.d.ts +3 -2
- package/dist/components/feedback/alerts/AlertBanner.d.ts.map +1 -1
- package/dist/components/feedback/alerts/AlertBanner.js +1 -1
- package/dist/components/inputs/textFields/TextField.cjs +27 -4
- package/dist/components/inputs/textFields/TextField.d.ts +10 -11
- package/dist/components/inputs/textFields/TextField.d.ts.map +1 -1
- package/dist/components/inputs/textFields/TextField.js +27 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [3.7.0](https://github.com/mollerdigital/design-system-design-system/compare/@gnist/design-system@3.6.0...@gnist/design-system@3.7.0) (2025-07-11)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add mode prop to TextField ([015a59b](https://github.com/mollerdigital/design-system-design-system/commit/015a59b522fc04d0526a67de9d0e2ecc0fd3396f))
|
|
11
|
+
|
|
12
|
+
## [3.6.0](https://github.com/mollerdigital/design-system-design-system/compare/@gnist/design-system@3.5.6...@gnist/design-system@3.6.0) (2025-07-07)
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* AlertBanner message-prop allows React Node ([c911154](https://github.com/mollerdigital/design-system-design-system/commit/c911154f4a3c2b8bea410bf963a9c4b14943a508))
|
|
17
|
+
|
|
6
18
|
## [3.5.6](https://github.com/mollerdigital/design-system-design-system/compare/@gnist/design-system@3.5.5...@gnist/design-system@3.5.6) (2025-07-04)
|
|
7
19
|
|
|
8
20
|
### Bug Fixes
|
|
@@ -23,6 +23,6 @@ const classNames__default = /* @__PURE__ */ _interopDefaultCompat(classNames);
|
|
|
23
23
|
const BannerHeading = componentUtils.component("BannerHeading", AlertBanner_css.bannerHeading, "h2");
|
|
24
24
|
const AlertBanner = ({ type, heading, message, dismiss, action, density = "default", headingLevel = "h2", className }) => {
|
|
25
25
|
const text = index.useTranslation((t) => t.components.feedback.alerts);
|
|
26
|
-
return jsxRuntime.jsxs("div", { className: classNames__default.default(className, AlertBanner_css.banner({ type, density })), children: [jsxRuntime.jsx(Icon.Icon, { type, icon: type, className: AlertBanner_css.icon({ type }) }), jsxRuntime.jsxs("div", { className: AlertBanner_css.mainContentContainer({ density }), children: [heading && jsxRuntime.jsx(BannerHeading, { "$as": headingLevel, children: heading }), dismiss && jsxRuntime.jsx(IconButton.IconButton, { className: AlertBanner_css.closeButton, icon: "close", label: text.dismissLabel, onClick: dismiss }), jsxRuntime.jsx(index$1.TextContainer, { className: AlertBanner_css.messageContainer, children: message }), action && jsxRuntime.jsx(TextButton.TextButton, { density: "compact", className: AlertBanner_css.actionButton, onClick: action.onClick, underline: true, children: action.label })] })] });
|
|
26
|
+
return jsxRuntime.jsxs("div", { className: classNames__default.default(className, AlertBanner_css.banner({ type, density })), children: [jsxRuntime.jsx(Icon.Icon, { type, icon: type, className: AlertBanner_css.icon({ type }) }), jsxRuntime.jsxs("div", { className: AlertBanner_css.mainContentContainer({ density }), children: [heading && jsxRuntime.jsx(BannerHeading, { "$as": headingLevel, children: heading }), dismiss && jsxRuntime.jsx(IconButton.IconButton, { className: AlertBanner_css.closeButton, icon: "close", label: text.dismissLabel, onClick: dismiss }), typeof message === "string" ? jsxRuntime.jsx(index$1.TextContainer, { className: AlertBanner_css.messageContainer, children: message }) : jsxRuntime.jsx("div", { className: AlertBanner_css.messageContainer, children: message }), action && jsxRuntime.jsx(TextButton.TextButton, { density: "compact", className: AlertBanner_css.actionButton, onClick: action.onClick, underline: true, children: action.label })] })] });
|
|
27
27
|
};
|
|
28
28
|
exports.AlertBanner = AlertBanner;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { DensitySizes } from "../../../foundation/typography";
|
|
2
|
+
import { ReactNode } from "react";
|
|
2
3
|
export interface AlertBannerProps {
|
|
3
4
|
type: "info" | "success" | "warning" | "error";
|
|
4
5
|
heading?: string | undefined;
|
|
5
|
-
message:
|
|
6
|
+
message: ReactNode;
|
|
6
7
|
dismiss?: () => void;
|
|
7
8
|
action?: {
|
|
8
9
|
label: string;
|
|
@@ -14,7 +15,7 @@ export interface AlertBannerProps {
|
|
|
14
15
|
}
|
|
15
16
|
/** An alert banner displays an important and high-signal message, and provides actions for users to address (or dismiss the banner). They’re meant to be noticed and often requires a user action to be dismissed.
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Message is required for all alert banners. The message should be concise and, if applicable, describe the next step that a user can take. The message prop can be string or other React Node.
|
|
18
19
|
|
|
19
20
|
Title and button with a contextual action are optional.
|
|
20
21
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AlertBanner.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/alerts/AlertBanner.tsx"],"names":[],"mappings":"AAEA,OAAO,EACH,YAAY,EAEf,uCAAmD;
|
|
1
|
+
{"version":3,"file":"AlertBanner.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/alerts/AlertBanner.tsx"],"names":[],"mappings":"AAEA,OAAO,EACH,YAAY,EAEf,uCAAmD;AAapD,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElC,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IAC/C,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE;QACL,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,IAAI,CAAC;KACvB,CAAC;IACF,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,YAAY,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAID;;;;;;;GAOG;AAEH,eAAO,MAAM,WAAW,mFASrB,gBAAgB,4CAuClB,CAAC"}
|
|
@@ -19,7 +19,7 @@ import { bannerHeading, icon, mainContentContainer, closeButton, messageContaine
|
|
|
19
19
|
const BannerHeading = component("BannerHeading", bannerHeading, "h2");
|
|
20
20
|
const AlertBanner = ({ type, heading, message, dismiss, action, density = "default", headingLevel = "h2", className }) => {
|
|
21
21
|
const text = useTranslation((t) => t.components.feedback.alerts);
|
|
22
|
-
return jsxs("div", { className: classNames(className, banner({ type, density })), children: [jsx(Icon, { type, icon: type, className: icon({ type }) }), jsxs("div", { className: mainContentContainer({ density }), children: [heading && jsx(BannerHeading, { "$as": headingLevel, children: heading }), dismiss && jsx(IconButton, { className: closeButton, icon: "close", label: text.dismissLabel, onClick: dismiss }), jsx(TextContainer, { className: messageContainer, children: message }), action && jsx(TextButton, { density: "compact", className: actionButton, onClick: action.onClick, underline: true, children: action.label })] })] });
|
|
22
|
+
return jsxs("div", { className: classNames(className, banner({ type, density })), children: [jsx(Icon, { type, icon: type, className: icon({ type }) }), jsxs("div", { className: mainContentContainer({ density }), children: [heading && jsx(BannerHeading, { "$as": headingLevel, children: heading }), dismiss && jsx(IconButton, { className: closeButton, icon: "close", label: text.dismissLabel, onClick: dismiss }), typeof message === "string" ? jsx(TextContainer, { className: messageContainer, children: message }) : jsx("div", { className: messageContainer, children: message }), action && jsx(TextButton, { density: "compact", className: actionButton, onClick: action.onClick, underline: true, children: action.label })] })] });
|
|
23
23
|
};
|
|
24
24
|
export {
|
|
25
25
|
AlertBanner
|
|
@@ -23,17 +23,40 @@ const shared = require("../shared.cjs");
|
|
|
23
23
|
const _interopDefaultCompat = (e) => e && typeof e === "object" && "default" in e ? e : { default: e };
|
|
24
24
|
const classNames__default = /* @__PURE__ */ _interopDefaultCompat(classNames);
|
|
25
25
|
const TextField = React.forwardRef(function TextField2(props, ref) {
|
|
26
|
-
var _a, _b;
|
|
26
|
+
var _a, _b, _c, _d;
|
|
27
27
|
const { wrapperProps, contentProps, commonInputProps } = shared.useInputFieldLogic(props);
|
|
28
28
|
const { inputProps } = shared.getInputFieldProps(props);
|
|
29
|
+
const isModeCurrencyOrNumberFormatting = ((_a = props.mode) == null ? void 0 : _a.style) === "currency" || ((_b = props.mode) == null ? void 0 : _b.style) === "numberSpacing";
|
|
30
|
+
const onChangeHandler = (event) => {
|
|
31
|
+
var _a2;
|
|
32
|
+
if (isModeCurrencyOrNumberFormatting) {
|
|
33
|
+
event.target.value = event.target.value.replace(new RegExp("(?<!^)-|[^\\d\\W.,]+|[.,]|\\s+", "g"), "");
|
|
34
|
+
}
|
|
35
|
+
(_a2 = inputProps.onChange) == null ? void 0 : _a2.call(inputProps, event);
|
|
36
|
+
};
|
|
37
|
+
if (isModeCurrencyOrNumberFormatting && !!inputProps.value) {
|
|
38
|
+
inputProps.type = "text";
|
|
39
|
+
inputProps.inputMode = "numeric";
|
|
40
|
+
const number = inputProps.value;
|
|
41
|
+
const isNegativeNumber = number[0] === "-";
|
|
42
|
+
inputProps.value = isNegativeNumber ? inputProps.value.slice(1) : inputProps.value;
|
|
43
|
+
const numberPrefix = isNegativeNumber ? "-" : "";
|
|
44
|
+
const formattedValue = new Intl.NumberFormat("nb-NB", {
|
|
45
|
+
style: "currency",
|
|
46
|
+
currency: "NOK",
|
|
47
|
+
minimumFractionDigits: 0,
|
|
48
|
+
maximumFractionDigits: 5
|
|
49
|
+
}).format(parseFloat(inputProps.value)).replaceAll(/[^0-9.,\W]/g, "").trim();
|
|
50
|
+
inputProps.value = numberPrefix + formattedValue;
|
|
51
|
+
}
|
|
29
52
|
return jsxRuntime.jsxs("span", { ref, className: classNames__default.default(wrapperProps.className, inputField_css.wrapperStyle), children: [props.leadingIcon && shared.isClickableIcon(props.leadingIcon) && jsxRuntime.jsx("button", { className: iconButtonOverlay_css.iconButtonOverlayRecipe({
|
|
30
53
|
placement: "left",
|
|
31
54
|
density: wrapperProps.density
|
|
32
55
|
}), title: props.leadingIcon.title, onClick: props.leadingIcon.onClick }), jsxRuntime.jsxs("label", { htmlFor: props.id, className: inputField_css.inputFieldWrapperRecipe({
|
|
33
56
|
disabled: props.disabled,
|
|
34
57
|
density: props.density,
|
|
35
|
-
validityType: (
|
|
36
|
-
}), children: [jsxRuntime.jsx("input", { ...commonInputProps, ...inputProps, className: classNames__default.default(inputField_css.inputFieldStyle, inputProps.className), style: dynamic.assignInlineVars({
|
|
58
|
+
validityType: (_c = props.validity) == null ? void 0 : _c.type
|
|
59
|
+
}), children: [jsxRuntime.jsx("input", { ...commonInputProps, ...inputProps, value: inputProps.value, onChange: onChangeHandler, className: classNames__default.default(inputField_css.inputFieldStyle, inputProps.className), style: dynamic.assignInlineVars({
|
|
37
60
|
[inputFieldConstants_css.preInputWidth]: wrapperProps.preInputWidth,
|
|
38
61
|
[inputFieldConstants_css.postInputWidth]: wrapperProps.postInputWidth
|
|
39
62
|
}), onFocus: (event) => {
|
|
@@ -44,7 +67,7 @@ const TextField = React.forwardRef(function TextField2(props, ref) {
|
|
|
44
67
|
(_b2 = inputProps.onFocus) == null ? void 0 : _b2.call(inputProps, event);
|
|
45
68
|
} }), jsxRuntime.jsxs("span", { className: inputField_css.inputContentWrapper({ density: props.density }), children: [props.leadingIcon && jsxRuntime.jsx(Icon.Icon, { icon: shared.isClickableIcon(props.leadingIcon) ? props.leadingIcon.icon : props.leadingIcon, className: atoms_css_js.atoms({ paddingRight: "xxs" }) }), props.prefix && jsxRuntime.jsx("span", { ref: contentProps.prefixRef, "aria-hidden": true, className: suffixPrefix_css.prefixStyle, children: props.prefix }), jsxRuntime.jsx("label", { id: contentProps.labelId, className: labelStyles_css.labelStyle({
|
|
46
69
|
density: props.density,
|
|
47
|
-
validityType: (
|
|
70
|
+
validityType: (_d = props.validity) == null ? void 0 : _d.type,
|
|
48
71
|
isElevated: !!props.value || props.type === "date" || props.type === "time",
|
|
49
72
|
disabled: props.disabled
|
|
50
73
|
}), style: {
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { InputHTMLAttributes } from "react";
|
|
2
2
|
import { TextInputProps } from "../shared.js";
|
|
3
|
-
export type
|
|
3
|
+
export type TextFieldModeProps = {
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @default text
|
|
5
|
+
* Applies number spacing and numeric input mode
|
|
7
6
|
*/
|
|
8
|
-
|
|
7
|
+
mode?: {
|
|
8
|
+
style: "numberSpacing";
|
|
9
|
+
} | {
|
|
10
|
+
style: "currency";
|
|
11
|
+
currency: "NOK";
|
|
12
|
+
};
|
|
9
13
|
};
|
|
14
|
+
export type TextFieldProps = TextInputProps & TextFieldModeProps & Omit<InputHTMLAttributes<HTMLInputElement>, "placeholder" | "value">;
|
|
10
15
|
/**
|
|
11
16
|
A text field is an input that allows a user to write or edit text. Text fields typically appear in forms and dialogs.
|
|
12
17
|
|
|
@@ -18,11 +23,5 @@ _Accessibility note:_ The leading/trailing icon and prefix/suffix text will not
|
|
|
18
23
|
|
|
19
24
|
Documentation: [TextField](https://gnist.moller.no/developers/components/latest/?path=/docs/components-inputs-textfields-textfield--docs)
|
|
20
25
|
*/
|
|
21
|
-
export declare const TextField: import("react").ForwardRefExoticComponent<TextInputProps & Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "
|
|
22
|
-
/**
|
|
23
|
-
* Input type
|
|
24
|
-
* @default text
|
|
25
|
-
*/
|
|
26
|
-
type?: "email" | "search" | "tel" | "text" | "url" | "password" | "number" | "date" | "time";
|
|
27
|
-
} & import("react").RefAttributes<HTMLSpanElement>>;
|
|
26
|
+
export declare const TextField: import("react").ForwardRefExoticComponent<TextInputProps & TextFieldModeProps & Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "placeholder"> & import("react").RefAttributes<HTMLSpanElement>>;
|
|
28
27
|
//# sourceMappingURL=TextField.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../../src/components/inputs/textFields/TextField.tsx"],"names":[],"mappings":"AAIA,OAAO,EAA4B,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAetE,OAAO,EAGH,cAAc,EAEjB,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"TextField.d.ts","sourceRoot":"","sources":["../../../../src/components/inputs/textFields/TextField.tsx"],"names":[],"mappings":"AAIA,OAAO,EAA4B,mBAAmB,EAAE,MAAM,OAAO,CAAC;AAetE,OAAO,EAGH,cAAc,EAEjB,MAAM,cAAc,CAAC;AAEtB,MAAM,MAAM,kBAAkB,GAAG;IAC7B;;OAEG;IACH,IAAI,CAAC,EAAE;QAAE,KAAK,EAAE,eAAe,CAAA;KAAE,GAAG;QAAE,KAAK,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,KAAK,CAAA;KAAE,CAAC;CAC9E,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,cAAc,GACvC,kBAAkB,GAClB,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,CAAC;AAEzE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,wMAqLpB,CAAC"}
|
|
@@ -19,17 +19,40 @@ import { labelStyle } from "../shared-styles/labelStyles.css.js";
|
|
|
19
19
|
import { prefixStyle, suffixStyle } from "../shared-styles/suffixPrefix.css.js";
|
|
20
20
|
import { useInputFieldLogic, getInputFieldProps, isClickableIcon } from "../shared.js";
|
|
21
21
|
const TextField = forwardRef(function TextField2(props, ref) {
|
|
22
|
-
var _a, _b;
|
|
22
|
+
var _a, _b, _c, _d;
|
|
23
23
|
const { wrapperProps, contentProps, commonInputProps } = useInputFieldLogic(props);
|
|
24
24
|
const { inputProps } = getInputFieldProps(props);
|
|
25
|
+
const isModeCurrencyOrNumberFormatting = ((_a = props.mode) == null ? void 0 : _a.style) === "currency" || ((_b = props.mode) == null ? void 0 : _b.style) === "numberSpacing";
|
|
26
|
+
const onChangeHandler = (event) => {
|
|
27
|
+
var _a2;
|
|
28
|
+
if (isModeCurrencyOrNumberFormatting) {
|
|
29
|
+
event.target.value = event.target.value.replace(new RegExp("(?<!^)-|[^\\d\\W.,]+|[.,]|\\s+", "g"), "");
|
|
30
|
+
}
|
|
31
|
+
(_a2 = inputProps.onChange) == null ? void 0 : _a2.call(inputProps, event);
|
|
32
|
+
};
|
|
33
|
+
if (isModeCurrencyOrNumberFormatting && !!inputProps.value) {
|
|
34
|
+
inputProps.type = "text";
|
|
35
|
+
inputProps.inputMode = "numeric";
|
|
36
|
+
const number = inputProps.value;
|
|
37
|
+
const isNegativeNumber = number[0] === "-";
|
|
38
|
+
inputProps.value = isNegativeNumber ? inputProps.value.slice(1) : inputProps.value;
|
|
39
|
+
const numberPrefix = isNegativeNumber ? "-" : "";
|
|
40
|
+
const formattedValue = new Intl.NumberFormat("nb-NB", {
|
|
41
|
+
style: "currency",
|
|
42
|
+
currency: "NOK",
|
|
43
|
+
minimumFractionDigits: 0,
|
|
44
|
+
maximumFractionDigits: 5
|
|
45
|
+
}).format(parseFloat(inputProps.value)).replaceAll(/[^0-9.,\W]/g, "").trim();
|
|
46
|
+
inputProps.value = numberPrefix + formattedValue;
|
|
47
|
+
}
|
|
25
48
|
return jsxs("span", { ref, className: classNames(wrapperProps.className, wrapperStyle), children: [props.leadingIcon && isClickableIcon(props.leadingIcon) && jsx("button", { className: iconButtonOverlayRecipe({
|
|
26
49
|
placement: "left",
|
|
27
50
|
density: wrapperProps.density
|
|
28
51
|
}), title: props.leadingIcon.title, onClick: props.leadingIcon.onClick }), jsxs("label", { htmlFor: props.id, className: inputFieldWrapperRecipe({
|
|
29
52
|
disabled: props.disabled,
|
|
30
53
|
density: props.density,
|
|
31
|
-
validityType: (
|
|
32
|
-
}), children: [jsx("input", { ...commonInputProps, ...inputProps, className: classNames(inputFieldStyle, inputProps.className), style: assignInlineVars({
|
|
54
|
+
validityType: (_c = props.validity) == null ? void 0 : _c.type
|
|
55
|
+
}), children: [jsx("input", { ...commonInputProps, ...inputProps, value: inputProps.value, onChange: onChangeHandler, className: classNames(inputFieldStyle, inputProps.className), style: assignInlineVars({
|
|
33
56
|
[preInputWidth]: wrapperProps.preInputWidth,
|
|
34
57
|
[postInputWidth]: wrapperProps.postInputWidth
|
|
35
58
|
}), onFocus: (event) => {
|
|
@@ -40,7 +63,7 @@ const TextField = forwardRef(function TextField2(props, ref) {
|
|
|
40
63
|
(_b2 = inputProps.onFocus) == null ? void 0 : _b2.call(inputProps, event);
|
|
41
64
|
} }), jsxs("span", { className: inputContentWrapper({ density: props.density }), children: [props.leadingIcon && jsx(Icon, { icon: isClickableIcon(props.leadingIcon) ? props.leadingIcon.icon : props.leadingIcon, className: atoms({ paddingRight: "xxs" }) }), props.prefix && jsx("span", { ref: contentProps.prefixRef, "aria-hidden": true, className: prefixStyle, children: props.prefix }), jsx("label", { id: contentProps.labelId, className: labelStyle({
|
|
42
65
|
density: props.density,
|
|
43
|
-
validityType: (
|
|
66
|
+
validityType: (_d = props.validity) == null ? void 0 : _d.type,
|
|
44
67
|
isElevated: !!props.value || props.type === "date" || props.type === "time",
|
|
45
68
|
disabled: props.disabled
|
|
46
69
|
}), style: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gnist/design-system",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -102,5 +102,5 @@
|
|
|
102
102
|
"optional": true
|
|
103
103
|
}
|
|
104
104
|
},
|
|
105
|
-
"gitHead": "
|
|
105
|
+
"gitHead": "14c41444b303c99e0b9eca32b48848f84a07213c"
|
|
106
106
|
}
|