diff --git a/community/components/navigation/vertical-stepper/index.ts b/community/components/navigation/vertical-stepper/index.ts
new file mode 100644
index 00000000..18dbe5e8
--- /dev/null
+++ b/community/components/navigation/vertical-stepper/index.ts
@@ -0,0 +1,2 @@
+export * from "./vertical-stepper.component";
+export * from "./vertical-stepper-item/vertical-stepper-item.component";
diff --git a/community/components/navigation/vertical-stepper/vertical-stepper-item/vertical-stepper-item.component.html b/community/components/navigation/vertical-stepper/vertical-stepper-item/vertical-stepper-item.component.html
new file mode 100644
index 00000000..28ce0f9b
--- /dev/null
+++ b/community/components/navigation/vertical-stepper/vertical-stepper-item/vertical-stepper-item.component.html
@@ -0,0 +1,79 @@
+
+ @if (subItem() || !compact()) {
+ @if (completed()) {
+
+ } @else if (error()) {
+
+ }
+ }
+
+
+
+ @if (!selected() && compact() && !subItem()) {
+ @if (completed()) {
+
+ } @else if (error()) {
+
+ }
+ }
+
+
+
+ @if (hasSubItems()) {
+
+ } @else {
+
+ @if (route()) {
+ {{ title() }}
+ } @else {
+
+ }
+
+
+ }
+
+
+
+
+@if (opened()) {
+
+}
diff --git a/community/components/navigation/vertical-stepper/vertical-stepper-item/vertical-stepper-item.component.scss b/community/components/navigation/vertical-stepper/vertical-stepper-item/vertical-stepper-item.component.scss
new file mode 100644
index 00000000..2215c69c
--- /dev/null
+++ b/community/components/navigation/vertical-stepper/vertical-stepper-item/vertical-stepper-item.component.scss
@@ -0,0 +1,271 @@
+.tedi-vertical-stepper-item {
+ $host-class: &;
+ display: grid;
+ position: relative;
+
+ --_step-indicator-size: 24px;
+ --_step-indicator-color: var(--general-text-secondary, #4b4e62);
+ --_step-indicator-border: var(--stepper-step-default-border, #9293a4);
+ --_step-indicator-border-hover: var(
+ --stepper-step-default-border-hover,
+ #005aa3
+ );
+ --_step-indicator-border-width: 1px;
+ --_step-indicator-background: var(--stepper-step-default-bg, #fff);
+ --_step-indicator-background-hover: var(--stepper-step-default-bg, #fff);
+
+ --_step-padding: var(--stepper-item-vertical-lg-padding-top, 8px);
+ --_step-inner-spacing: var(--stepper-item-vertical-lg-inner-spacing, 8px);
+ --_step-line-color: var(--stepper-item-vertical-line, #9293a4);
+
+ --_step-title-color: var(--stepper-item-vertical-text-default, #151926);
+ --_step-title-color-hover: var(--stepper-item-vertical-text-hover, #004277);
+ --_step-title-size: var(--body-regular-size, 16px);
+ --_step-title-weight: var(--body-regular-weight, 400);
+ --_step-title-line-height: var(--body-regular-line-height, 24px);
+ --_step-title-spacing: var(--layout-grid-gutters-08, 8px);
+
+ grid-template-areas:
+ "indicator title"
+ ". description";
+ grid-template-columns: auto 1fr;
+ grid-auto-columns: auto;
+ column-gap: var(--_step-inner-spacing);
+ z-index: 0;
+
+ &__indicator {
+ grid-area: indicator;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: var(--_step-indicator-size);
+ width: var(--_step-indicator-size);
+ border: var(--_step-indicator-border) solid
+ var(--_step-indicator-border-width);
+ background-color: var(--_step-indicator-background);
+ color: var(--_step-indicator-color);
+ border-radius: 9999px;
+ font-size: var(--body-small-bold-size, 14px);
+ font-weight: var(--body-bold-weight, 700);
+ align-self: center;
+ justify-self: center;
+
+ &::before {
+ content: counter(step-number);
+ }
+ }
+
+ &:has(> &__title :is(button, a):hover) > &__indicator {
+ background-color: var(--_step-indicator-background-hover);
+ border-color: var(--_step-indicator-border-hover);
+ }
+
+ &:not(&--sub-item) {
+ counter-increment: step-number;
+ }
+
+ &__title {
+ grid-area: title;
+ padding: var(--_step-padding) 0;
+
+ a,
+ button {
+ padding: 0;
+ background: none;
+ border: none;
+ text-decoration: none;
+ font-family: var(--family-default, Roboto);
+ font-size: var(--_step-title-size);
+ font-weight: var(--_step-title-weight);
+ line-height: var(--_step-title-line-height);
+ cursor: pointer;
+ color: var(--_step-title-color);
+
+ &:hover {
+ color: var(--_step-title-color-hover);
+ &:not(#{$host-class}__toggle) {
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+
+ &__toggle {
+ display: flex;
+ width: 100%;
+ align-items: center;
+
+ &:hover > span {
+ text-decoration: underline;
+ }
+ }
+
+ &__toggle-icon {
+ margin-left: auto;
+ transition: transform 0.3s;
+ &--opened {
+ transform: rotateX(180deg);
+ }
+ }
+
+ &__status-icon {
+ margin-inline: var(--_step-title-spacing);
+ }
+
+ &__description {
+ grid-area: description;
+
+ &:empty {
+ display: none;
+ }
+ }
+
+ &__line {
+ &::before,
+ &::after {
+ content: "";
+ grid-row: 1 / span 1;
+ grid-column: 1 / span 1;
+ left: 50%;
+ transform: translateX(-50%);
+ position: absolute;
+ width: 1px;
+ background-color: var(--_step-line-color);
+ top: 0;
+ bottom: 0;
+ z-index: -1;
+ }
+
+ &::before {
+ grid-row: 1 / span 1;
+ }
+
+ &::after {
+ grid-row: 2 / span 1;
+ }
+ }
+
+ &:not(&--sub-item):first-child > &__line {
+ &::before {
+ top: 50%;
+ }
+ }
+
+ &:not(&--sub-item):last-child {
+ &:not(:has(#{$host-class}--sub-item)) > #{$host-class}__line::before {
+ bottom: 50%;
+ }
+ & > #{$host-class}__line::after {
+ content: none;
+ }
+
+ #{$host-class}--sub-item:last-child #{$host-class}__line {
+ &::before {
+ bottom: 50%;
+ }
+ &::after {
+ content: none;
+ }
+ }
+ }
+
+ &--compact {
+ --_step-indicator-size: var(--stepper-item-vertical-step-size-md, 16px);
+ --_step-padding: var(--stepper-item-vertical-compact-padding-top, 3px);
+ --_step-inner-spacing: var(
+ --stepper-item-vertical-compact-inner-spacing,
+ 6px
+ );
+ --_step-title-spacing: var(--layout-grid-gutters-04, 4px);
+ #{$host-class}__indicator::before {
+ content: none;
+ }
+
+ {$host-class}--enumerated:not(#{$host-class}--sub-item)
+ > #{$host-class}__title
+ :is(a, button)::before {
+ content: counter(step-number) ".";
+ display: inline-block;
+ }
+ }
+
+ &--sub-item {
+ grid-column: 1 / span 2;
+ grid-template-columns: subgrid;
+ --_step-indicator-size: var(--stepper-item-vertical-step-size-sm, 9px);
+ --_step-padding: var(--stepper-item-vertical-padding-y-sm, 2px);
+
+ #{$host-class}__indicator::before {
+ content: none;
+ }
+ }
+
+ &--completed {
+ --_step-indicator-background: var(--stepper-step-completed-bg, #266b42);
+ --_step-indicator-border: var(--stepper-step-completed-bg, #266b42);
+ --_step-indicator-color: var(--general-text-white, #fff);
+ --_step-indicator-border-hover: var(
+ --stepper-step-completed-bg-hover,
+ #1d5032
+ );
+ --_step-indicator-background-hover: var(
+ --stepper-step-completed-bg-hover,
+ #1d5032
+ );
+ }
+
+ &--error {
+ --_step-indicator-background: var(--stepper-step-danger-bg, #ac3232);
+ --_step-indicator-border: var(--stepper-step-danger-bg, #ac3232);
+ --_step-indicator-color: var(--general-text-white, #fff);
+ --_step-indicator-border-hover: var(
+ --stepper-step-danger-bg-hover,
+ #812525
+ );
+ --_step-indicator-background-hover: var(
+ --stepper-step-danger-bg-hover,
+ #812525
+ );
+ }
+
+ &--selected {
+ --_step-indicator-background: var(--stepper-step-selected-bg, #fff);
+ --_step-indicator-color: var(
+ --stepper-item-vertical-text-selected,
+ #004277
+ );
+ --_step-indicator-border: var(--stepper-step-selected-border, #004277);
+ --_step-indicator-border-width: 2px;
+ --_step-indicator-border-hover: var(
+ --stepper-step-selected-border-hover,
+ #003662
+ );
+ --_step-title-color: var(--stepper-item-vertical-text-selected, #004277);
+ --_step-title-weight: var(--body-bold-weight, 700);
+ {$host-class}--compact:not(#{$host-class}--sub-item) {
+ --_step-indicator-border-width: 4px;
+ }
+ }
+
+ &--disabled {
+ --_step-indicator-background: var(--stepper-step-disabled-bg, #d2d3d8);
+ --_step-indicator-color: var(
+ --stepper-item-vertical-text-disabled,
+ #9293a4
+ );
+ --_step-indicator-border: var(--stepper-step-disabled-border, #9293a4);
+ --_step-title-color: var(--stepper-item-vertical-text-disabled, #9293a4);
+ #{$host-class}__title {
+ pointer-events: none;
+ }
+ }
+
+ &--informative {
+ --_step-indicator-background: var(--stepper-step-disabled-bg, #d2d3d8);
+ --_step-indicator-border: var(--stepper-step-disabled-border, #9293a4);
+ --_step-title-color: var(--general-text-tertiary, #5d6071);
+ #{$host-class}__title {
+ pointer-events: none;
+ }
+ }
+}
diff --git a/community/components/navigation/vertical-stepper/vertical-stepper-item/vertical-stepper-item.component.ts b/community/components/navigation/vertical-stepper/vertical-stepper-item/vertical-stepper-item.component.ts
new file mode 100644
index 00000000..b8d1af51
--- /dev/null
+++ b/community/components/navigation/vertical-stepper/vertical-stepper-item/vertical-stepper-item.component.ts
@@ -0,0 +1,88 @@
+import {
+ booleanAttribute,
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ contentChildren,
+ effect,
+ inject,
+ input,
+ model,
+ output,
+ signal,
+ ViewEncapsulation,
+} from "@angular/core";
+import { VerticalStepperComponent } from "../vertical-stepper.component";
+import { RouterLink, RouterLinkActive } from "@angular/router";
+import { NgTemplateOutlet } from "@angular/common";
+import {
+ IconComponent,
+ TediTranslationPipe,
+} from "@tedi-design-system/angular/tedi";
+
+@Component({
+ selector: "tedi-vertical-stepper-item",
+ imports: [
+ IconComponent,
+ RouterLink,
+ RouterLinkActive,
+ NgTemplateOutlet,
+ TediTranslationPipe,
+ ],
+ templateUrl: "./vertical-stepper-item.component.html",
+ styleUrl: "./vertical-stepper-item.component.scss",
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ host: {
+ "[class.tedi-vertical-stepper-item]": "true",
+ "[class.tedi-vertical-stepper-item--completed]": "completed()",
+ "[class.tedi-vertical-stepper-item--error]": "error()",
+ "[class.tedi-vertical-stepper-item--selected]": "selected()",
+ "[class.tedi-vertical-stepper-item--disabled]": "disabled()",
+ "[class.tedi-vertical-stepper-item--informative]": "informative()",
+ "[class.tedi-vertical-stepper-item--sub-item]": "subItem()",
+ "[class.tedi-vertical-stepper-item--compact]": "compact()",
+ "[class.tedi-vertical-stepper-item--enumerated]": "enumerated()",
+ "[attr.role]": "'listitem'",
+ },
+})
+export class VerticalStepperItemComponent {
+ completed = input(false, { transform: booleanAttribute });
+ error = input(false, { transform: booleanAttribute });
+ selected = input(false, { transform: booleanAttribute });
+ disabled = input(false, { transform: booleanAttribute });
+ informative = input(false, { transform: booleanAttribute });
+ title = input.required();
+ route = input(undefined);
+ opened = model(false); // for items with children
+
+ itemSelect = output();
+
+ private stepperContext = inject(VerticalStepperComponent, { optional: true });
+ subItem = signal(false);
+ subItems = contentChildren(VerticalStepperItemComponent);
+ compact = computed(() => this.stepperContext?.compact());
+ enumerated = computed(() => this.stepperContext?.enumerated());
+ hasSubItems = computed(() => !!this.subItems().length);
+
+ onSubItemSelect = effect(() => {
+ const subItemSelected = this.subItems().some((item) => item.selected());
+ if (subItemSelected) {
+ this.opened.set(true);
+ }
+ });
+
+ onSubItemChanges = effect(() => {
+ this.subItems().forEach((item) => item.subItem.set(true));
+ });
+
+ toggleOpen() {
+ this.opened.update((previouslyOpened) => !previouslyOpened);
+ }
+
+ routerLinkActiveChange(isActive: boolean) {
+ if (isActive) {
+ this.itemSelect.emit();
+ }
+ }
+}
diff --git a/community/components/navigation/vertical-stepper/vertical-stepper.component.html b/community/components/navigation/vertical-stepper/vertical-stepper.component.html
new file mode 100644
index 00000000..8c590a16
--- /dev/null
+++ b/community/components/navigation/vertical-stepper/vertical-stepper.component.html
@@ -0,0 +1,5 @@
+
diff --git a/community/components/navigation/vertical-stepper/vertical-stepper.component.scss b/community/components/navigation/vertical-stepper/vertical-stepper.component.scss
new file mode 100644
index 00000000..d757cebf
--- /dev/null
+++ b/community/components/navigation/vertical-stepper/vertical-stepper.component.scss
@@ -0,0 +1,3 @@
+.tedi-vertical-stepper {
+ counter-reset: step-number;
+}
diff --git a/community/components/navigation/vertical-stepper/vertical-stepper.component.ts b/community/components/navigation/vertical-stepper/vertical-stepper.component.ts
new file mode 100644
index 00000000..a3dd76b3
--- /dev/null
+++ b/community/components/navigation/vertical-stepper/vertical-stepper.component.ts
@@ -0,0 +1,25 @@
+import {
+ booleanAttribute,
+ ChangeDetectionStrategy,
+ Component,
+ input,
+ ViewEncapsulation,
+} from "@angular/core";
+
+@Component({
+ selector: "tedi-vertical-stepper",
+ imports: [],
+ templateUrl: "./vertical-stepper.component.html",
+ styleUrl: "./vertical-stepper.component.scss",
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ host: {
+ "[class.tedi-vertical-stepper]": "true",
+ "[class.tedi-vertical-stepper--compact]": "compact()",
+ },
+})
+export class VerticalStepperComponent {
+ ariaLabel = input();
+ compact = input(false, { transform: booleanAttribute });
+ enumerated = input(false, { transform: booleanAttribute });
+}
diff --git a/community/components/navigation/vertical-stepper/vertical-stepper.stories.ts b/community/components/navigation/vertical-stepper/vertical-stepper.stories.ts
new file mode 100644
index 00000000..e2da25b1
--- /dev/null
+++ b/community/components/navigation/vertical-stepper/vertical-stepper.stories.ts
@@ -0,0 +1,346 @@
+import {
+ argsToTemplate,
+ Meta,
+ moduleMetadata,
+ StoryObj,
+} from "@storybook/angular";
+import { VerticalStepperComponent } from "./vertical-stepper.component";
+import { VerticalStepperItemComponent } from "./vertical-stepper-item/vertical-stepper-item.component";
+import { StatusBadgeComponent } from "../../tags/status-badge/status-badge.component";
+
+/**
+ * The `vertical-stepper` component is stepper where steps are displayed in vertical sequence.
+ *
+ * Vertical-stepper component consists of individual `vertical-stepper-item` components. Steps have title and can be used as routes or buttons for non-routed navigation.
+ *
+ * Step title must be provided as input. Title template can be also provided as element with `item-title` attribute for cases with custom routing logic etc.
+ *
+ * Steps can also have description/action. They can be provided as element with `item-description` attribute.
+ *
+ * Steps can also have sub steps. They can be provided as nested `vertical-stepper-item` components.
+ */
+
+export default {
+ title: "Community/Navigation/VerticalStepper",
+ component: VerticalStepperComponent,
+ decorators: [
+ moduleMetadata({
+ imports: [
+ VerticalStepperComponent,
+ VerticalStepperItemComponent,
+ StatusBadgeComponent,
+ ],
+ }),
+ ],
+ argTypes: {
+ compact: {
+ description: "Whether it's the compact variant",
+ control: "boolean",
+ table: {
+ category: "vertical-stepper",
+ type: { summary: "boolean" },
+ defaultValue: { summary: "false" },
+ },
+ },
+ enumerated: {
+ description:
+ "Used for compact variant, displays step number infront of the step title",
+ control: "boolean",
+ table: {
+ category: "vertical-stepper",
+ type: { summary: "boolean" },
+ defaultValue: { summary: "false" },
+ },
+ },
+ ariaLabel: {
+ description: "Aria label for stepper",
+ control: "text",
+ table: {
+ category: "vertical-stepper",
+ type: { summary: "string" },
+ },
+ },
+ itemTitle: {
+ name: "title",
+ description:
+ "Item title. Title can also be provided by element with `item-title` attribute. Input is required for mobile view",
+ control: "text",
+ table: {
+ category: "vertical-stepper-item",
+ type: { summary: "string" },
+ },
+ },
+ itemCompleted: {
+ name: "completed",
+ description: "Is vertical stepper item completed",
+ control: "boolean",
+ table: {
+ category: "vertical-stepper-item",
+ type: { summary: "boolean" },
+ defaultValue: { summary: "false" },
+ },
+ },
+ itemError: {
+ name: "error",
+ description: "Does vertical stepper item have error",
+ control: "boolean",
+ table: {
+ category: "vertical-stepper-item",
+ type: { summary: "boolean" },
+ defaultValue: { summary: "false" },
+ },
+ },
+ itemSelected: {
+ name: "selected",
+ description: "Is vertical stepper item selected",
+ control: "boolean",
+ table: {
+ category: "vertical-stepper-item",
+ type: { summary: "boolean" },
+ defaultValue: { summary: "false" },
+ },
+ },
+ itemDisabled: {
+ name: "disabled",
+ description: "Is vertical stepper item disabled",
+ control: "boolean",
+ table: {
+ category: "vertical-stepper-item",
+ type: { summary: "boolean" },
+ defaultValue: { summary: "false" },
+ },
+ },
+ itemInformative: {
+ name: "informative",
+ description:
+ "Is vertical stepper item informative item. For sub items only",
+ control: "boolean",
+ table: {
+ category: "vertical-stepper-item",
+ type: { summary: "boolean" },
+ defaultValue: { summary: "false" },
+ },
+ },
+ itemOpened: {
+ name: "opened",
+ description:
+ "Is vertical stepper item opened. For parent items with sub items only",
+ control: "boolean",
+ table: {
+ category: "vertical-stepper-item",
+ type: { summary: "boolean" },
+ defaultValue: { summary: "false" },
+ },
+ },
+ itemRoute: {
+ name: "route",
+ description:
+ "Router link for item. If provided, leaf items title will be an anchor element, else button",
+ control: "text",
+ table: {
+ category: "vertical-stepper-item",
+ type: { summary: "RouterLink.routerLink: string | any[] | UrlTree" },
+ },
+ },
+ itemSelect: {
+ description: "Event for when item is selected",
+ table: {
+ category: "vertical-stepper-item",
+ type: { summary: "output" },
+ },
+ },
+ },
+} as Meta;
+
+export const Default: StoryObj = {
+ args: {},
+ render: (args) => ({
+ props: args,
+ template: `
+
+
+ Description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ }),
+};
+
+export const Compact: StoryObj = {
+ args: {
+ compact: true,
+ },
+ render: (args) => ({
+ props: args,
+ template: `
+
+
+ Description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ }),
+};
+
+export const EnumeratedCompact: StoryObj = {
+ args: {
+ compact: true,
+ enumerated: true,
+ },
+ render: (args) => ({
+ props: args,
+ template: `
+
+
+ Description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ }),
+};
+
+export const WithRouterlinks: StoryObj = {
+ args: {},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "For cases when steps are on multiple routes. Item emits `itemSelect` event when its routerLink becomes active",
+ },
+ },
+ },
+ render: (args) => ({
+ props: args,
+ template: `
+
+
+
+
+
+
+
+
+ `,
+ }),
+};
+
+export const ProjectedTitleTemplates: StoryObj = {
+ args: {},
+ parameters: {
+ docs: {
+ description: {
+ story:
+ "For cases when step titles need custom templates or advanced routing logic. For example fragmented navigation. `button` and `a` elements inherit styles",
+ },
+ },
+ },
+ render: (args) => ({
+ props: args,
+ template: `
+
+
+ Link 1
+
+
+ Link 2
+
+
+ Link 3
+
+
+ Link 4
+
+
+ Link 5
+
+
+ `,
+ }),
+};
diff --git a/tedi/services/translation/translations.ts b/tedi/services/translation/translations.ts
index fd9800fa..1a01fe10 100644
--- a/tedi/services/translation/translations.ts
+++ b/tedi/services/translation/translations.ts
@@ -936,6 +936,22 @@ export const translationsMap = {
en: "Next years",
ru: "Следующие годы",
},
+ "vertical-stepper.completed": {
+ description:
+ "Label for screen-reader that this step is completed (visually hidden)",
+ components: ["VerticalStepper"],
+ et: "Lõpetatud",
+ en: "Completed",
+ ru: "Завершено",
+ },
+ "vertical-stepper.error": {
+ description:
+ "Label for screen-reader that this step has error (visually hidden)",
+ components: ["VerticalStepper"],
+ et: "Puudulik",
+ en: "Error",
+ ru: "Oшибка",
+ },
};
export type TediTranslationsMap = {