;
+
+export const Default: Story = {
+ render: (props) => ({
+ props,
+ template: `
+
+
+
+
+
+
+ `,
+ }),
+ args: {
+ ariaLabel: "Form progress",
+ background: "default",
+ compact: "sm",
+ },
+};
+
+export const SecondStep: Story = {
+ render: (props) => ({
+ props,
+ template: `
+
+
+
+
+
+
+ `,
+ }),
+ args: {
+ background: "default",
+ },
+};
+
+export const ThirdStep: Story = {
+ render: (props) => ({
+ props,
+ template: `
+
+
+
+
+
+
+ `,
+ }),
+ args: {
+ background: "default",
+ },
+};
+
+export const WithErrors: Story = {
+ render: (props) => ({
+ props,
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ }),
+ args: {
+ background: "default",
+ },
+};
+
+export const WithDescriptions: Story = {
+ render: (props) => ({
+ props,
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ }),
+ args: {
+ background: "default",
+ },
+};
+
+export const TransparentBackground: Story = {
+ render: (props) => ({
+ props,
+ template: `
+
+
+
+
+
+
+ `,
+ }),
+};
+
+/**
+ * Collapsed — only indicators plus the selected step's label are shown.
+ * Use `[compact]="true"` for always-on, or pass a breakpoint (e.g. `compact="md"`)
+ * to collapse only below that viewport width. Each indicator is clickable so the
+ * user can jump between steps.
+ */
+export const Compact: Story = {
+ render: () => ({
+ moduleMetadata: { imports: [CompactNavigationDemoComponent] },
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ source: {
+ type: "code",
+ language: "ts",
+ code: `@Component({
+ imports: [HorizontalStepperComponent, HorizontalStepperItemComponent],
+ template: \`
+
+ @for (label of steps; track label; let i = $index) {
+
+ }
+
+ \`,
+})
+export class FormWizardComponent {
+ steps = ["Kutse", "Tahteavaldus", "Geenianalüüs", "Vastus"];
+ current = signal(1);
+}`,
+ },
+ },
+ },
+};
+
+/**
+ * Validation runs at the end of the form — every step is reachable via the
+ * header. Each item listens to `stepSelect` and updates the active step.
+ * Past steps render as `completed`, future steps stay default.
+ */
+export const ClickToNavigate: Story = {
+ render: () => ({
+ moduleMetadata: { imports: [StepClickNavigationDemoComponent] },
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ source: {
+ type: "code",
+ language: "ts",
+ code: `@Component({
+ imports: [HorizontalStepperComponent, HorizontalStepperItemComponent],
+ template: \`
+
+ @for (label of steps; track label; let i = $index) {
+
+ }
+
+ \`,
+})
+export class FormWizardComponent {
+ steps = ["Kutse", "Tahteavaldus", "Geenianalüüs", "Vastus"];
+ current = signal(1);
+}`,
+ },
+ },
+ },
+};
+
+/**
+ * Step-by-step validation — the user advances with `Edasi`/`Tagasi`.
+ * Past steps render as `completed` and are clickable for back-navigation;
+ * future steps are `disabled` so the user can't skip ahead from the header.
+ */
+export const ExternalNavigation: Story = {
+ render: () => ({
+ moduleMetadata: { imports: [ExternalNavigationDemoComponent] },
+ template: ``,
+ }),
+ parameters: {
+ docs: {
+ source: {
+ type: "code",
+ language: "ts",
+ code: `@Component({
+ imports: [
+ HorizontalStepperComponent,
+ HorizontalStepperItemComponent,
+ ButtonComponent,
+ ],
+ template: \`
+
+ @for (label of steps; track label; let i = $index) {
+ current()"
+ (stepSelect)="current.set(i)"
+ />
+ }
+
+
+
+ \`,
+})
+export class FormWizardComponent {
+ steps = ["Kutse", "Tahteavaldus", "Geenianalüüs", "Vastus"];
+ current = signal(0);
+
+ back(): void {
+ this.current.update((s) => Math.max(0, s - 1));
+ }
+
+ next(): void {
+ this.current.update((s) => Math.min(this.steps.length - 1, s + 1));
+ }
+}`,
+ },
+ },
+ },
+};
diff --git a/tedi/components/navigation/horizontal-stepper/index.ts b/tedi/components/navigation/horizontal-stepper/index.ts
new file mode 100644
index 000000000..17a879dd9
--- /dev/null
+++ b/tedi/components/navigation/horizontal-stepper/index.ts
@@ -0,0 +1,2 @@
+export * from "./horizontal-stepper.component";
+export * from "./horizontal-stepper-item";
diff --git a/tedi/components/navigation/index.ts b/tedi/components/navigation/index.ts
index b7336cf96..4cae8b5eb 100644
--- a/tedi/components/navigation/index.ts
+++ b/tedi/components/navigation/index.ts
@@ -1 +1,2 @@
+export * from "./horizontal-stepper";
export * from "./link/link.component";
\ No newline at end of file
diff --git a/tedi/components/overlay/modal/modal.component.scss b/tedi/components/overlay/modal/modal.component.scss
index 3111f2877..6888a9aee 100644
--- a/tedi/components/overlay/modal/modal.component.scss
+++ b/tedi/components/overlay/modal/modal.component.scss
@@ -323,7 +323,7 @@ $modal-widths: (xs, sm, md, lg, xl);
}
}
- @each $bp in (sm, md, lg, xl) {
+ @each $bp in (sm, md, lg, xl, xxl) {
&--fullscreen-#{$bp} {
@include breakpoints.media-breakpoint-down($bp) {
--_tedi-modal-max-width: 100vw;
diff --git a/tedi/components/overlay/modal/modal.stories.ts b/tedi/components/overlay/modal/modal.stories.ts
index 2123b5358..76b55296d 100644
--- a/tedi/components/overlay/modal/modal.stories.ts
+++ b/tedi/components/overlay/modal/modal.stories.ts
@@ -6,7 +6,7 @@ import { ModalContentComponent } from "./modal-content/modal-content.component";
import { ModalFooterComponent } from "./modal-footer/modal-footer.component";
import { ModalService } from "./modal.service";
import { ModalRef } from "./modal-ref";
-import { MODAL_DATA } from "./modal.types";
+import { MODAL_DATA, ModalFullscreen } from "./modal.types";
import { ButtonComponent } from "../../buttons/button/button.component";
import { LabelComponent } from "../../form/label/label.component";
import { IconComponent } from "../../base/icon/icon.component";
@@ -496,7 +496,7 @@ export default {
description: "Scroll behavior when content overflows. `'content'` scrolls inside the modal, `'page'` scrolls the full overlay.",
},
fullscreen: {
- table: { type: { summary: "boolean | 'sm' | 'md' | 'lg' | 'xl'" }, defaultValue: { summary: "false" }, category: "ModalConfig" },
+ table: { type: { summary: "boolean | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'" }, defaultValue: { summary: "false" }, category: "ModalConfig" },
description: "Fullscreen mode. `true` = always fullscreen. A breakpoint string (e.g. `'md'`) makes the modal fullscreen below that breakpoint.",
},
closeOnBackdropClick: {
@@ -544,7 +544,7 @@ export const Default: StoryObj = {
maxWidth: { control: "text" },
position: { control: "select", options: ["center", "top", "left", "right"] },
scrollBehavior: { control: "select", options: ["content", "page"] },
- fullscreen: { control: "text", description: "Set `true` for always fullscreen or a breakpoint string (`sm`, `md`, `lg`, `xl`)." },
+ fullscreen: { control: "text", description: "Set `true` for always fullscreen or a breakpoint string (`sm`, `md`, `lg`, `xl`, `xxl`)." },
closeOnBackdropClick: { control: "boolean" },
closeOnEscape: { control: "boolean" },
showClose: { control: "boolean" },
@@ -635,7 +635,7 @@ class MyModalContent {
maxWidth: this.maxWidth || undefined,
position: this.position as "center" | "top" | "left" | "right",
scrollBehavior: this.scrollBehavior as "content" | "page",
- fullscreen: this.parseFullscreen() as boolean | "sm" | "md" | "lg" | "xl",
+ fullscreen: this.parseFullscreen() as ModalFullscreen,
closeOnBackdropClick: this.closeOnBackdropClick,
closeOnEscape: this.closeOnEscape,
});
diff --git a/tedi/components/overlay/modal/modal.types.ts b/tedi/components/overlay/modal/modal.types.ts
index 69906b8f1..e790b54e3 100644
--- a/tedi/components/overlay/modal/modal.types.ts
+++ b/tedi/components/overlay/modal/modal.types.ts
@@ -1,11 +1,12 @@
import { InjectionToken } from "@angular/core";
+import { BreakpointFlag } from "../../../services/breakpoint/breakpoint.service";
export type ModalSize = "default" | "small";
export type ModalWidthPreset = "xs" | "sm" | "md" | "lg" | "xl";
export type ModalWidth = ModalWidthPreset | (string & {});
export type ModalPosition = "center" | "top" | "left" | "right";
export type ModalScrollBehavior = "content" | "page";
-export type ModalFullscreen = boolean | "sm" | "md" | "lg" | "xl";
+export type ModalFullscreen = BreakpointFlag;
export interface ModalConfig {
/** Data to pass to the modal content component. Accessible via `inject(MODAL_DATA)`. */
@@ -24,7 +25,7 @@ export interface ModalConfig {
closeOnEscape?: boolean;
/** Whether to show a close button in the header. @default true */
showClose?: boolean;
- /** Fullscreen mode. `true` = always fullscreen, `'sm'`/`'md'`/etc = fullscreen below that breakpoint. @default false */
+ /** Fullscreen mode. `true` = always fullscreen, a breakpoint (`'sm'`, `'md'`, `'lg'`, `'xl'`, `'xxl'`) = fullscreen below that breakpoint. @default false */
fullscreen?: ModalFullscreen;
/** Max-width cap (e.g. '75%', '60vw'). Overrides the default 95vw limit. */
maxWidth?: string;
diff --git a/tedi/services/breakpoint/breakpoint.service.ts b/tedi/services/breakpoint/breakpoint.service.ts
index 5abdf65a5..23624df08 100644
--- a/tedi/services/breakpoint/breakpoint.service.ts
+++ b/tedi/services/breakpoint/breakpoint.service.ts
@@ -32,6 +32,15 @@ export type BreakpointObject = { xs: T } & Partial<
>;
export type BreakpointInput = T | BreakpointObject;
+/**
+ * Flag that toggles a feature on/off, optionally only below a breakpoint.
+ * `true` — always on. `false` — always off. A breakpoint name — on below that breakpoint.
+ *
+ * `'xs'` is intentionally excluded: "below xs" has no meaningful viewport and would
+ * be a confusing duplicate of `true`. Use `true` for always-on.
+ */
+export type BreakpointFlag = boolean | Exclude;
+
@Injectable({
providedIn: "root",
})
diff --git a/tedi/services/translation/translations.ts b/tedi/services/translation/translations.ts
index 7665e5351..c42513236 100644
--- a/tedi/services/translation/translations.ts
+++ b/tedi/services/translation/translations.ts
@@ -289,6 +289,14 @@ export const translationsMap = {
en: "Not completed",
ru: "Не завершено",
},
+ "stepper.error": {
+ description:
+ "Label for screen-reader that this step has errors (visually hidden)",
+ components: ["HorizontalStepper"],
+ et: "Viga",
+ en: "Error",
+ ru: "Ошибка",
+ },
"skeleton.loading": {
description: "Announced by screen-readers when skeleton is loading",
components: ["Skeleton"],