From 9944872acd4b3b9052ab8bd8f021f0bbbedb75e4 Mon Sep 17 00:00:00 2001 From: Davide Mininni <101575400+DavideMininni-Fincons@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:45:30 +0100 Subject: [PATCH 1/6] fix(sbb-radio-button-panel): remove extension clause in mixin which cause incorrect manifest generation (#3288) --- src/elements/core/mixins/form-associated-mixin.ts | 2 +- src/elements/radio-button/common/radio-button-common.spec.ts | 4 +++- .../radio-button/radio-button-panel/radio-button-panel.ts | 5 +---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/elements/core/mixins/form-associated-mixin.ts b/src/elements/core/mixins/form-associated-mixin.ts index 24573f5e53..1dd4c97fc0 100644 --- a/src/elements/core/mixins/form-associated-mixin.ts +++ b/src/elements/core/mixins/form-associated-mixin.ts @@ -3,7 +3,7 @@ import { property, state } from 'lit/decorators.js'; import type { AbstractConstructor } from './constructor.js'; -export declare abstract class SbbFormAssociatedMixinType extends LitElement { +export declare abstract class SbbFormAssociatedMixinType { public get form(): HTMLFormElement | null; public get name(): string; public set name(value: string); diff --git a/src/elements/radio-button/common/radio-button-common.spec.ts b/src/elements/radio-button/common/radio-button-common.spec.ts index 5f001d33b0..576e7dbb9e 100644 --- a/src/elements/radio-button/common/radio-button-common.spec.ts +++ b/src/elements/radio-button/common/radio-button-common.spec.ts @@ -604,7 +604,9 @@ describe(`radio-button common behaviors`, () => { }); it('should be sorted in DOM order', async () => { - const group1Set = radioButtonRegistry.get(form)!.get('sbb-group-1')!; + const group1Set = radioButtonRegistry + .get(form)! + .get('sbb-group-1')! as unknown as Set; let group1Radios = Array.from(form.querySelectorAll(selector)); // Assert the order is the correct diff --git a/src/elements/radio-button/radio-button-panel/radio-button-panel.ts b/src/elements/radio-button/radio-button-panel/radio-button-panel.ts index 0e554ad5fe..1f0337a1a1 100644 --- a/src/elements/radio-button/radio-button-panel/radio-button-panel.ts +++ b/src/elements/radio-button/radio-button-panel/radio-button-panel.ts @@ -12,7 +12,6 @@ import { getOverride, slotState } from '../../core/decorators.js'; import { isLean } from '../../core/dom.js'; import { panelCommonStyle, - type SbbFormAssociatedRadioButtonMixinType, SbbPanelMixin, type SbbPanelSize, SbbUpdateSchedulerMixin, @@ -87,9 +86,7 @@ class SbbRadioButtonPanelElement extends SbbPanelMixin( /** * As an exception, radio-panels with an expansion-panel attached are not checked automatically when navigating by keyboard */ - protected override async navigateByKeyboard( - next: SbbFormAssociatedRadioButtonMixinType, - ): Promise { + protected override async navigateByKeyboard(next: SbbRadioButtonPanelElement): Promise { if (!this._hasSelectionExpansionPanelElement) { await super.navigateByKeyboard(next); } else { From 7db5b7e79d47b207a997f44c34f4f600ec542492 Mon Sep 17 00:00:00 2001 From: Jeri Peier Date: Thu, 12 Dec 2024 11:37:11 +0100 Subject: [PATCH 2/6] fix(sbb-teaser): fix image related issues (#3293) --- src/elements/core/styles/core.scss | 32 ++-- src/elements/teaser/readme.md | 4 +- src/elements/teaser/teaser.scss | 5 +- src/elements/teaser/teaser.stories.ts | 18 +- src/elements/teaser/teaser.visual.spec.ts | 215 +++++++++++----------- 5 files changed, 138 insertions(+), 136 deletions(-) diff --git a/src/elements/core/styles/core.scss b/src/elements/core/styles/core.scss index 220a7f68e3..29e1f538fc 100644 --- a/src/elements/core/styles/core.scss +++ b/src/elements/core/styles/core.scss @@ -162,8 +162,8 @@ img { // TODO: Move back to the sbb-container components when the global css refactoring happens sbb-container { - [slot='image']:is(sbb-image, img), - [slot='image'] :is(sbb-image, img) { + > [slot='image']:is(sbb-image, img), + > [slot='image'] :is(sbb-image, img) { --sbb-image-object-fit: cover; border-radius: var(--sbb-container-background-border-radius); @@ -174,8 +174,8 @@ sbb-container { // TODO: Move back to the sbb-flip-card-summary components when the global css refactoring happens sbb-flip-card-summary { - [slot='image']:is(sbb-image, img), - [slot='image'] :is(sbb-image, img) { + > [slot='image']:is(sbb-image, img), + > [slot='image'] :is(sbb-image, img) { --sbb-image-aspect-ratio: auto; --sbb-image-object-fit: cover; @@ -187,8 +187,8 @@ sbb-flip-card-summary { // TODO: Move back to the sbb-lead-container components when the global css refactoring happens sbb-lead-container { - [slot='image']:is(sbb-image, img, picture), - [slot='image'] :is(sbb-image, img, picture) { + > [slot='image']:is(sbb-image, img, picture), + > [slot='image'] :is(sbb-image, img, picture) { --sbb-image-aspect-ratio: var(--sbb-lead-container-image-ratio); --sbb-image-object-fit: cover; @@ -213,8 +213,7 @@ sbb-lead-container { } } - [slot='image']:is(sbb-image, img), - [slot='image'] :is(sbb-image, img) { + :is(sbb-image, img) { will-change: filter; filter: brightness(var(--sbb-teaser-image-brightness, 1)); transition: filter var(--sbb-teaser-image-animation-duration) @@ -237,18 +236,13 @@ sbb-lead-container { } // TODO: Move back to the teaser components when the global css refactoring happens -:is(sbb-teaser) { - [slot='image']:is(sbb-image, img), - [slot='image'] :is(sbb-image, img) { - transition-property: filter, scale; - will-change: filter, scale; - scale: var(--sbb-teaser-scale, 1); - } +sbb-teaser :is(sbb-image, img) { + --sbb-image-object-fit: cover; + --sbb-image-aspect-ratio: 4 / 3; - :is(sbb-image, img) { - --sbb-image-object-fit: cover; - --sbb-image-aspect-ratio: 4 / 3; - } + transition-property: filter, scale; + will-change: filter, scale; + scale: var(--sbb-teaser-scale, 1); } // TODO: Move back to the teaser-hero components when the global css refactoring happens diff --git a/src/elements/teaser/readme.md b/src/elements/teaser/readme.md index 4804cea8f7..8d36c04038 100644 --- a/src/elements/teaser/readme.md +++ b/src/elements/teaser/readme.md @@ -9,9 +9,7 @@ Simple teaser example: title-content="Title" chip-content="Chip label" > -
- 400x300 -
+ 400x300 A brief description. ``` diff --git a/src/elements/teaser/teaser.scss b/src/elements/teaser/teaser.scss index 8cc5dceaaf..b0bbb9d158 100644 --- a/src/elements/teaser/teaser.scss +++ b/src/elements/teaser/teaser.scss @@ -89,16 +89,17 @@ ::slotted([slot='image']) { width: #{sbb.px-to-rem-build(300)}; + display: block; } .sbb-teaser__image-wrapper { flex-shrink: 0; overflow: hidden; border-radius: var(--sbb-teaser-border-radius); - transition: var(--sbb-teaser-animation-duration) var(--sbb-animation-easing); + transition: box-shadow var(--sbb-teaser-image-animation-duration) var(--sbb-animation-easing); @include sbb.hover-mq($hover: true) { - .sbb-teaser__wrapper:hover & { + :host(:hover) & { @include sbb.shadow-level-9-hard; } } diff --git a/src/elements/teaser/teaser.stories.ts b/src/elements/teaser/teaser.stories.ts index e8a9895468..236f0cf3af 100644 --- a/src/elements/teaser/teaser.stories.ts +++ b/src/elements/teaser/teaser.stories.ts @@ -102,9 +102,7 @@ const TemplateDefault = ({ description, ...remainingArgs }: Args): TemplateResul const TemplateDefaultFixedWidth = ({ description, ...remainingArgs }: Args): TemplateResult => { return html` -
- 400x300 -
+ 400x300 ${description}
`; @@ -113,9 +111,13 @@ const TemplateDefaultFixedWidth = ({ description, ...remainingArgs }: Args): Tem const TemplateCustom = ({ description, ...remainingArgs }: Args): TemplateResult => { return html` -
- 200x100 -
+ 200x100 ${description}
`; @@ -129,9 +131,7 @@ const TemplateSlots = ({ }: Args): TemplateResult => { return html` -
- 400x300 -
+ 400x300 ${chipContent} ${titleContent} ${description} diff --git a/src/elements/teaser/teaser.visual.spec.ts b/src/elements/teaser/teaser.visual.spec.ts index 1c79c87128..4ac11e763f 100644 --- a/src/elements/teaser/teaser.visual.spec.ts +++ b/src/elements/teaser/teaser.visual.spec.ts @@ -12,6 +12,8 @@ import { import { waitForImageReady } from '../core/testing.js'; import './teaser.js'; +import '../chip-label.js'; +import '../container.js'; import '../image.js'; const imageUrl = import.meta.resolve('../core/testing/assets/placeholder-image.png'); @@ -27,6 +29,37 @@ describe(`sbb-teaser`, () => { const longChip: string = 'This is a chip which has a very long content and should receive ellipsis.'; + const imgCases = [ + { + title: 'imageSlot=sbb-image', + imgSelector: 'sbb-image', + imgTemplate: () => html``, + }, + { + title: 'imageSlot=img', + imgSelector: 'img', + imgTemplate: () => html``, + }, + { + title: 'imageSlot=figure_and_sbb-image', + imgSelector: 'sbb-image', + imgTemplate: () => + html`
+ + AI chip +
`, + }, + { + title: 'imageSlot=figure_and_img', + imgSelector: 'img', + imgTemplate: () => + html`
+ + AI chip +
`, + }, + ]; + const visualStates = { hasChip: [false, true], withLongContent: [false, true], @@ -41,28 +74,31 @@ describe(`sbb-teaser`, () => { describeViewports({ viewports: [screenCombination.viewport] }, () => { for (const alignment of screenCombination.alignments) { describe(`alignment=${alignment}`, () => { - for (const visualDiffStandardState of [ - visualDiffDefault, - visualDiffFocus, - visualDiffHover, - ]) { - it( - `state=${visualDiffStandardState.name}`, - visualDiffStandardState.with(async (setup) => { - await setup.withFixture( - html` - -
- -
- This is a paragraph -
- `, - { maxWidth: '760px' }, + for (const imgCase of imgCases) { + describe(imgCase.title, () => { + for (const visualDiffStandardState of [ + visualDiffDefault, + visualDiffFocus, + visualDiffHover, + ]) { + it( + `state=${visualDiffStandardState.name}`, + visualDiffStandardState.with(async (setup) => { + await setup.withFixture( + html` + + ${imgCase.imgTemplate()} This is a paragraph + + `, + { maxWidth: '760px' }, + ); + await waitForImageReady( + setup.snapshotElement.querySelector(imgCase.imgSelector)!, + ); + }), ); - await waitForImageReady(setup.snapshotElement.querySelector('img')!); - }), - ); + } + }); } describeEach(visualStates, ({ hasChip, withLongContent }) => { @@ -80,9 +116,9 @@ describe(`sbb-teaser`, () => {
${hasChip - ? html`AI chip` + ? html` + AI chip + ` : nothing}
${withLongContent ? loremIpsum : 'This is a paragraph'} @@ -94,93 +130,44 @@ describe(`sbb-teaser`, () => { }), ); }); - - it( - `longChip=true`, - visualDiffDefault.with(async (setup) => { - await setup.withFixture( - html` - -
- -
- This is a paragraph -
- `, - { maxWidth: '760px' }, - ); - await waitForImageReady(setup.snapshotElement.querySelector('img')!); - }), - ); - - it( - `list=true`, - visualDiffDefault.with(async (setup) => { - const count = 5; - await setup.withFixture(html` -
    - ${repeat( - new Array(count), - (_, i) => html` -
  • - -
    - -
    - This is the paragraph n.${i + 1} -
    -
  • - `, - )} -
- `); - await Promise.all( - new Array(count).map((_, i) => - waitForImageReady(setup.snapshotElement.querySelector(`#img${i}`)!), - ), - ); - }), - ); }); } it( 'grid with sbb-image', visualDiffDefault.with(async (setup) => { + const count = 2; await setup.withFixture(html` -
- ${repeat( - new Array(2), - () => html` - -
- - AI chip -
- This is a paragraph -
- `, - )} -
+ +
+ ${repeat( + new Array(count), + (_, i) => html` + +
+ + + AI chip + +
+ This is a paragraph +
+ `, + )} +
+
`); - await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); + + await Promise.all( + new Array(count).map((_, i) => + waitForImageReady(setup.snapshotElement.querySelector(`#img${i}`)!), + ), + ); }), ); @@ -200,6 +187,28 @@ describe(`sbb-teaser`, () => { await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); }), ); + + it( + `longChip`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture( + html` + + + This is a paragraph + + `, + { maxWidth: '760px' }, + ); + await waitForImageReady(setup.snapshotElement.querySelector('img')!); + }), + ); }); } }); From cd3f18960df0bf7b84035e06dac6d6f37060e872 Mon Sep 17 00:00:00 2001 From: Tommaso Menga Date: Thu, 12 Dec 2024 11:48:40 +0100 Subject: [PATCH 3/6] fix(sbb-message): support the use of `figure` as image (#3294) --- src/elements/core/styles/core.scss | 8 +++ src/elements/message/message.scss | 4 +- src/elements/message/message.visual.spec.ts | 70 +++++++++++++++------ 3 files changed, 63 insertions(+), 19 deletions(-) diff --git a/src/elements/core/styles/core.scss b/src/elements/core/styles/core.scss index 29e1f538fc..fea33f6988 100644 --- a/src/elements/core/styles/core.scss +++ b/src/elements/core/styles/core.scss @@ -196,6 +196,14 @@ sbb-lead-container { } } +// TODO: Move back to the sbb-message components when the global CSS refactoring happens +sbb-message { + > [slot='image']:is(sbb-image, img), + > [slot='image'] :is(sbb-image, img) { + border-radius: var(--sbb-message-image-border-radius); + } +} + // Target the slotted `sbb-image` which are generally wrapped by a
(therefore are not reachable with the :slotted) // Apply the brightness effect on mouse hover // TODO: Move back to the teaser components when the global css refactoring happens diff --git a/src/elements/message/message.scss b/src/elements/message/message.scss index 62c14cf321..d54d86adbf 100644 --- a/src/elements/message/message.scss +++ b/src/elements/message/message.scss @@ -6,6 +6,7 @@ :host { --sbb-message-subtitle-color: var(--sbb-color-granite); --sbb-message-image-margin-block: 0 var(--sbb-spacing-responsive-s); + --sbb-message-image-border-radius: var(--sbb-border-radius-4x); --sbb-message-legend-margin-block: var(--sbb-spacing-responsive-xxxs) 0; --sbb-message-action-margin-block: var(--sbb-spacing-responsive-xxxs) 0; @@ -26,7 +27,8 @@ } ::slotted([slot='image']) { - margin-block: var(--sbb-message-image-margin-block); + display: block; + margin-block: var(--sbb-message-image-margin-block) !important; // overrides '.sbb-figure' margin width: 100%; } diff --git a/src/elements/message/message.visual.spec.ts b/src/elements/message/message.visual.spec.ts index 156b928829..dfab902ca9 100644 --- a/src/elements/message/message.visual.spec.ts +++ b/src/elements/message/message.visual.spec.ts @@ -4,32 +4,66 @@ import { describeViewports, visualDiffDefault } from '../core/testing/private.js import { waitForImageReady } from '../core/testing.js'; import './message.js'; +import '../chip-label.js'; import '../image.js'; import '../button/secondary-button.js'; const imageUrl = import.meta.resolve('../core/testing/assets/placeholder-image.png'); +const imgTestCases = [ + { + title: 'with sbb-image', + imgSelector: 'sbb-image', + imgTemplate: () => html``, + }, + { + title: 'with img tag', + imgSelector: 'img', + imgTemplate: () => html``, + }, + { + title: 'with figure_sbb-image', + imgSelector: 'sbb-image', + imgTemplate: () => + html`
+ + AI generated +
`, + }, + { + title: 'with figure_img', + imgSelector: 'img', + imgTemplate: () => + html`
+ + AI generated +
`, + }, +]; + describe(`sbb-message`, () => { describeViewports({ viewports: ['zero', 'medium'] }, () => { - it( - 'default', - visualDiffDefault.with(async (setup) => { - await setup.withFixture(html` - - -

Please reload the page or try your search again later.

-

Error code: 0001

- -
- `); + for (const testCase of imgTestCases) { + it( + `default ${testCase.title}`, + visualDiffDefault.with(async (setup) => { + await setup.withFixture(html` + + ${testCase.imgTemplate()} +

Please reload the page or try your search again later.

+

Error code: 0001

+ +
+ `); - await waitForImageReady(setup.snapshotElement.querySelector('sbb-image')!); - }), - ); + await waitForImageReady(setup.snapshotElement.querySelector(testCase.imgSelector)!); + }), + ); + } it( 'no image', From a796c857888801e00645718ae4b15bb453664517 Mon Sep 17 00:00:00 2001 From: Davide Mininni <101575400+DavideMininni-Fincons@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:48:56 +0100 Subject: [PATCH 4/6] feat: add class generic type param in manifest (#3292) The datepicker and the calendar components use generic type parameter to allow custom implementation of Date, eg. ``` export class SbbDatepickerElement { private _min?: T | null; // and so on } ``` This needs to be taken in account in Angular classes generation, so a new param has been added to the manifest during its generation, eg. ``` { "kind": "javascript-module", "path": "datepicker/datepicker.js", "declarations": [ { "kind": "class", "description": "Combined with a native input, it displays the input's value as a formatted date.", "name": "SbbDatepickerElement", "members": [], "events": [], "attributes": [], "superclass": {}, "classGenerics": "T = Date", <--------------------------------- new property "tagName": "sbb-datepicker", "customElement": true } ], exports: [] ``` --- .../manifest/custom-elements-manifest.config.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tools/manifest/custom-elements-manifest.config.js b/tools/manifest/custom-elements-manifest.config.js index 8dcb6b15cd..398f5fddd2 100644 --- a/tools/manifest/custom-elements-manifest.config.js +++ b/tools/manifest/custom-elements-manifest.config.js @@ -1,4 +1,5 @@ const overrideTypeKey = 'overrideType'; +const classGenericsTypeKey = 'classGenerics'; /** * Docs: https://custom-elements-manifest.open-wc.org/analyzer/getting-started/ @@ -96,6 +97,19 @@ export function createManifestConfig(library = '') { } if (ts.isClassDeclaration(node)) { + const classDeclaration = moduleDoc.declarations.find( + (declaration) => declaration.name === node.name.getText(), + ); + + /** + * If the class uses a generic type parameter, add it to the class declaration. + * It will be used in the Angular wrapper to correctly generate classes. + * Mainly used for datepicker and calendar components. + */ + if (node.typeParameters && node.typeParameters.length > 0) { + classDeclaration[classGenericsTypeKey] = node.typeParameters[0].getText(); + } + /** * When a generic T type is used in a superclass declaration, it overrides the type defined in derived class * during the doc generation (as the `value` property in the `SbbFormAssociatedMixinType`). @@ -109,9 +123,6 @@ export function createManifestConfig(library = '') { // eslint-disable-next-line lyne/local-name-rule if (tag.tagName.getText() === overrideTypeKey) { const [memberName, memberOverrideType] = tag.comment.split(' - '); - const classDeclaration = moduleDoc.declarations.find( - (declaration) => declaration.name === node.name.getText(), - ); if (!classDeclaration[overrideTypeKey]) { classDeclaration[overrideTypeKey] = [{ memberName, memberOverrideType }]; } else { From 5a4a08dc6495a7f260632041f674232ecddc006e Mon Sep 17 00:00:00 2001 From: github-actions Date: Thu, 12 Dec 2024 11:15:20 +0000 Subject: [PATCH 5/6] chore: update changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2563bc2a2e..cde7375709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.0.1](https://github.com/sbb-design-systems/lyne-components/compare/v2.0.0...v2.0.1) (2024-12-12) + + +### Features + +* add class generic type param in manifest ([#3292](https://github.com/sbb-design-systems/lyne-components/issues/3292)) ([3d872c1](https://github.com/sbb-design-systems/lyne-components/commit/3d872c11fa6e8bd9be3750cce1a8d42458064d30)) + + +### Bug Fixes + +* **sbb-message:** support the use of `figure` as image ([#3294](https://github.com/sbb-design-systems/lyne-components/issues/3294)) ([1d64853](https://github.com/sbb-design-systems/lyne-components/commit/1d64853ad20155c07f8c07af76bbe774b519aa58)) +* **sbb-radio-button-panel:** remove extension clause in mixin which cause incorrect manifest generation ([#3288](https://github.com/sbb-design-systems/lyne-components/issues/3288)) ([b5457a7](https://github.com/sbb-design-systems/lyne-components/commit/b5457a77f260d48e3a151c88b6c815c81df241c8)) +* **sbb-teaser:** fix image related issues ([#3293](https://github.com/sbb-design-systems/lyne-components/issues/3293)) ([e6f517b](https://github.com/sbb-design-systems/lyne-components/commit/e6f517bbf8b42b96777193347860c315c457a6be)) + + +### Miscellaneous Chores + +* release 2.0.1 ([2a43d06](https://github.com/sbb-design-systems/lyne-components/commit/2a43d0688e95c8bbfd87c91025eea59dd66bd769)) + ## [2.0.0](https://github.com/sbb-design-systems/lyne-components/compare/v1.14.0...v2.0.0) (2024-12-11) From d614fc37e5c5725e7371ba28748441fadfd251ce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 13 Dec 2024 02:07:46 +0000 Subject: [PATCH 6/6] chore(deps): update dependency sass to v1.83.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 20e26d691a..c750f1e5ea 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,7 @@ "prettier": "3.4.2", "react": "19.0.0", "rollup-plugin-postcss-lit": "2.1.0", - "sass": "1.82.0", + "sass": "1.83.0", "sinon": "19.0.2", "storybook": "8.4.7", "stylelint": "16.11.0", diff --git a/yarn.lock b/yarn.lock index a92cb00c09..e043755ab2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7657,10 +7657,10 @@ sass-lookup@^6.0.1: dependencies: commander "^12.0.0" -sass@1.82.0: - version "1.82.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.82.0.tgz#30da277af3d0fa6042e9ceabd0d984ed6d07df70" - integrity sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q== +sass@1.83.0: + version "1.83.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.83.0.tgz#e36842c0b88a94ed336fd16249b878a0541d536f" + integrity sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw== dependencies: chokidar "^4.0.0" immutable "^5.0.2"