+
+ )}
+
+ ),
+};
diff --git a/src/tedi/components/layout/header/header.tsx b/src/tedi/components/layout/header/header.tsx
new file mode 100644
index 000000000..55d1425d8
--- /dev/null
+++ b/src/tedi/components/layout/header/header.tsx
@@ -0,0 +1,142 @@
+import cn from 'classnames';
+import React from 'react';
+
+import { useTheme } from '../../../providers/theme-provider/theme-provider';
+import Print from '../../misc/print/print';
+import HeaderLanguage from './components/header-language/header-language';
+import HeaderLogin from './components/header-login/header-login';
+import HeaderLogout from './components/header-logout/header-logout';
+import HeaderProfile from './components/header-profile/header-profile';
+import HeaderRole from './components/header-role/header-role';
+import HeaderSearch from './components/header-search/header-search';
+import styles from './header.module.scss';
+
+export interface HeaderProps {
+ /**
+ * Content rendered inside the header, typically Header.Logo, Header.Center, and Header.Actions subcomponents.
+ */
+ children: React.ReactNode;
+ /**
+ * Toggle element for the mobile side navigation menu.
+ * Typically a SideNav.Toggle component.
+ */
+ toggle?: React.ReactNode;
+ /**
+ * Content rendered below the main header bar on mobile viewports (below `md` breakpoint).
+ * Commonly used for a mobile-specific search bar or other compact navigation elements.
+ */
+ bottom?: React.ReactNode;
+ /** Additional CSS class name applied to the header wrapper. */
+ className?: string;
+}
+
+export const Header = (props: HeaderProps) => {
+ const { children, toggle, bottom, className } = props;
+
+ return (
+
+
+
+ {toggle}
+
{children}
+
+
+ {bottom &&
{bottom}
}
+
+
+ );
+};
+
+Header.displayName = 'Header';
+
+export interface HeaderLogoProps {
+ /**
+ * The default logo to display (typically used in light theme).
+ */
+ logo: React.ReactNode;
+ /**
+ * Optional logo variant for dark theme.
+ * If provided, it will be used when the current theme is dark.
+ */
+ logoDark?: React.ReactNode;
+ /**
+ * Controls visibility of the logo.
+ * Useful for conditionally hiding the logo based on application state, feature flags,
+ * or custom media queries that fall between standard breakpoints (e.g. 420px).
+ * For responsive hiding at standard breakpoints, prefer wrapping Header.Logo with HideAt/ShowAt.
+ * @default true
+ */
+ showLogo?: boolean;
+ /**
+ * Optional link URL.
+ * If provided, the logo will be wrapped in an anchor element.
+ */
+ href?: string;
+ /** Additional CSS class name applied to the logo wrapper. */
+ className?: string;
+}
+
+export const HeaderLogo = (props: HeaderLogoProps) => {
+ const { logo, logoDark, href, showLogo = true, className } = props;
+ const { theme } = useTheme();
+
+ const resolvedLogo = theme === 'dark' && logoDark ? logoDark : logo;
+
+ if (!showLogo) return null;
+
+ const content = href ? {resolvedLogo} : resolvedLogo;
+
+ return
{content}
;
+};
+
+HeaderLogo.displayName = 'Header.Logo';
+
+export interface HeaderCenterProps {
+ /** Content rendered in the center area of the header, typically navigation links or a search bar. */
+ children: React.ReactNode;
+ /**
+ * Controls the horizontal alignment of center content.
+ * @default center
+ */
+ alignment?: 'flex-start' | 'center' | 'space-between';
+ /** Additional CSS class name applied to the center content area. */
+ className?: string;
+}
+
+export const HeaderCenter = (props: HeaderCenterProps) => {
+ const { children, className, alignment = 'center' } = props;
+
+ return (
+
+ {children}
+
+ );
+};
+
+HeaderCenter.displayName = 'Header.Center';
+
+export interface HeaderActionsProps {
+ /** Action elements rendered on the right side of the header (e.g. language selector, login, profile). */
+ children: React.ReactNode;
+ /** Additional CSS class name applied to the actions wrapper. */
+ className?: string;
+}
+
+export const HeaderActions = (props: HeaderActionsProps) => {
+ const { children, className } = props;
+ return
{children}
;
+};
+
+HeaderActions.displayName = 'Header.Actions';
+
+Header.Logo = HeaderLogo;
+Header.Center = HeaderCenter;
+Header.Actions = HeaderActions;
+Header.Language = HeaderLanguage;
+Header.Login = HeaderLogin;
+Header.Logout = HeaderLogout;
+Header.Profile = HeaderProfile;
+Header.Role = HeaderRole;
+Header.Search = HeaderSearch;
+
+export default Header;
diff --git a/src/tedi/components/layout/header/index.ts b/src/tedi/components/layout/header/index.ts
new file mode 100644
index 000000000..54cc85f6b
--- /dev/null
+++ b/src/tedi/components/layout/header/index.ts
@@ -0,0 +1,7 @@
+export * from './header';
+export * from './components/header-language/header-language';
+export * from './components/header-login/header-login';
+export * from './components/header-logout/header-logout';
+export * from './components/header-profile/header-profile';
+export * from './components/header-role/header-role';
+export * from './components/header-search/header-search';
diff --git a/src/tedi/components/layout/hide-at/hide-at.spec.tsx b/src/tedi/components/layout/hide-at/hide-at.spec.tsx
new file mode 100644
index 000000000..4a437ad77
--- /dev/null
+++ b/src/tedi/components/layout/hide-at/hide-at.spec.tsx
@@ -0,0 +1,108 @@
+import { render, screen } from '@testing-library/react';
+
+import { isBreakpointBelow, useBreakpoint } from '../../../helpers';
+import { HideAt } from './hide-at';
+
+import '@testing-library/jest-dom';
+
+jest.mock('../../../helpers', () => ({
+ ...jest.requireActual('../../../helpers'),
+ useBreakpoint: jest.fn(),
+ isBreakpointBelow: jest.fn(),
+}));
+
+describe('HideAt component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders children when no breakpoint matches', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('sm');
+ (isBreakpointBelow as jest.Mock).mockReturnValue(true);
+
+ render(
+
+ Visible content
+
+ );
+
+ expect(screen.getByText('Visible content')).toBeInTheDocument();
+ });
+
+ it('hides children when current breakpoint is at or above the specified breakpoint', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('lg');
+ (isBreakpointBelow as jest.Mock).mockReturnValue(false);
+
+ render(
+
+ Hidden content
+
+ );
+
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
+ });
+
+ it('renders children when breakpoint prop is false', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('lg');
+ (isBreakpointBelow as jest.Mock).mockReturnValue(false);
+
+ render(
+
+ Visible content
+
+ );
+
+ expect(screen.getByText('Visible content')).toBeInTheDocument();
+ });
+
+ it('hides when any of multiple breakpoints match', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('lg');
+ (isBreakpointBelow as jest.Mock).mockReturnValueOnce(true).mockReturnValueOnce(false);
+
+ render(
+
+ Hidden content
+
+ );
+
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
+ });
+
+ it('hides children when all specified breakpoints are below the current breakpoint', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('xxl');
+ (isBreakpointBelow as jest.Mock).mockReturnValue(false);
+
+ render(
+
+ Hidden content
+
+ );
+
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
+ });
+
+ it('renders children when current breakpoint is xs (SSR default)', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('xs');
+ (isBreakpointBelow as jest.Mock).mockReturnValue(true);
+
+ render(
+
+ Visible content
+
+ );
+
+ expect(screen.getByText('Visible content')).toBeInTheDocument();
+ });
+
+ it('renders children when no breakpoint props are passed', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('md');
+
+ render(
+
+ Always visible
+
+ );
+
+ expect(screen.getByText('Always visible')).toBeInTheDocument();
+ });
+});
diff --git a/src/tedi/components/layout/hide-at/hide-at.stories.tsx b/src/tedi/components/layout/hide-at/hide-at.stories.tsx
new file mode 100644
index 000000000..ccfb06207
--- /dev/null
+++ b/src/tedi/components/layout/hide-at/hide-at.stories.tsx
@@ -0,0 +1,95 @@
+import { Meta, StoryFn, StoryObj } from '@storybook/react';
+
+import { Text } from '../../base/typography/text/text';
+import { HideAt } from './hide-at';
+
+const meta: Meta = {
+ component: HideAt,
+ title: 'TEDI-Ready/Layout/HideAt',
+ parameters: {
+ status: {
+ type: ['devComponent'],
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const Box = ({ children, color = '#e8f4fd' }: { children: React.ReactNode; color?: string }) => (
+
+
+ The colored box below is wrapped in HideAt md. It is hidden at the md breakpoint and above. Resize the viewport
+ below md to see it appear.
+
+
+ This content is hidden at md and above.
+
+
+
+ The colored box below is wrapped in HideAt sm lg. It is hidden at sm and above, and also at lg and above. It is
+ only visible below sm (xs).
+
+
+ This content is hidden at sm and above, and also at lg and above.
+
+
+
+ Each colored box is hidden at the specified breakpoint and above. Resize the viewport to see boxes appear as you
+ go below their threshold.
+
+
+ Hidden at xs and above
+
+
+ Hidden at sm and above
+
+
+ Hidden at md and above
+
+
+ Hidden at lg and above
+
+
+ Hidden at xl and above
+
+
+ Hidden at xxl and above
+
+
+);
+
+export const BreakpointOverview: Story = {
+ render: BreakpointOverviewTemplate,
+};
diff --git a/src/tedi/components/layout/hide-at/hide-at.tsx b/src/tedi/components/layout/hide-at/hide-at.tsx
new file mode 100644
index 000000000..110cc351b
--- /dev/null
+++ b/src/tedi/components/layout/hide-at/hide-at.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+import { Breakpoint, isBreakpointBelow, useBreakpoint } from '../../../helpers';
+
+type HideAtProps = {
+ children: React.ReactNode;
+} & Partial>;
+
+export const HideAt = ({ children, ...breakpoints }: HideAtProps) => {
+ const current = useBreakpoint();
+
+ const shouldHide = Object.entries(breakpoints).some(([bp, value]) => {
+ if (!value) return false;
+ return !isBreakpointBelow(current, bp as Breakpoint);
+ });
+
+ if (shouldHide) return null;
+
+ return <>{children}>;
+};
+
+HideAt.displayName = 'HideAt';
diff --git a/src/tedi/components/layout/show-at/show-at.spec.tsx b/src/tedi/components/layout/show-at/show-at.spec.tsx
new file mode 100644
index 000000000..3c234acd5
--- /dev/null
+++ b/src/tedi/components/layout/show-at/show-at.spec.tsx
@@ -0,0 +1,95 @@
+import { render, screen } from '@testing-library/react';
+
+import { isBreakpointBelow, useBreakpoint } from '../../../helpers';
+import { ShowAt } from './show-at';
+
+import '@testing-library/jest-dom';
+
+jest.mock('../../../helpers', () => ({
+ ...jest.requireActual('../../../helpers'),
+ useBreakpoint: jest.fn(),
+ isBreakpointBelow: jest.fn(),
+}));
+
+describe('ShowAt component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('shows children when current breakpoint is at or above the specified breakpoint', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('lg');
+ (isBreakpointBelow as jest.Mock).mockReturnValue(false);
+
+ render(
+
+ Visible content
+
+ );
+
+ expect(screen.getByText('Visible content')).toBeInTheDocument();
+ });
+
+ it('hides children when current breakpoint is below the specified breakpoint', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('sm');
+ (isBreakpointBelow as jest.Mock).mockReturnValue(true);
+
+ render(
+
+ Hidden content
+
+ );
+
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
+ });
+
+ it('hides children when breakpoint prop is false', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('lg');
+ (isBreakpointBelow as jest.Mock).mockReturnValue(false);
+
+ render(
+
+ Hidden content
+
+ );
+
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
+ });
+
+ it('shows when any of multiple breakpoints match', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('md');
+ (isBreakpointBelow as jest.Mock).mockReturnValueOnce(true).mockReturnValueOnce(false);
+
+ render(
+
+ Visible content
+
+ );
+
+ expect(screen.getByText('Visible content')).toBeInTheDocument();
+ });
+
+ it('hides children when all specified breakpoints are above current', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('xs');
+ (isBreakpointBelow as jest.Mock).mockReturnValue(true);
+
+ render(
+
+ Hidden content
+
+ );
+
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
+ });
+
+ it('hides children when no breakpoint props are passed', () => {
+ (useBreakpoint as jest.Mock).mockReturnValue('md');
+
+ render(
+
+ Hidden content
+
+ );
+
+ expect(screen.queryByText('Hidden content')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/tedi/components/layout/show-at/show-at.stories.tsx b/src/tedi/components/layout/show-at/show-at.stories.tsx
new file mode 100644
index 000000000..0de35497c
--- /dev/null
+++ b/src/tedi/components/layout/show-at/show-at.stories.tsx
@@ -0,0 +1,95 @@
+import { Meta, StoryFn, StoryObj } from '@storybook/react';
+
+import { Text } from '../../base/typography/text/text';
+import { ShowAt } from './show-at';
+
+const meta: Meta = {
+ component: ShowAt,
+ title: 'TEDI-Ready/Layout/ShowAt',
+ parameters: {
+ status: {
+ type: ['devComponent'],
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const Box = ({ children, color = '#e8f4fd' }: { children: React.ReactNode; color?: string }) => (
+
+
+ The colored box below is wrapped in ShowAt md. It is only visible at the md breakpoint and above. Resize the
+ viewport below md to see it disappear.
+
+
+ This content is only visible at md and above.
+
+
+
+ The colored box below is wrapped in ShowAt sm lg. It is visible at sm and above, or at lg and above. It is
+ hidden below sm (xs).
+
+
+ This content is visible at sm and above, or at lg and above.
+
+
+
+ Each colored box is visible at the specified breakpoint and above. Resize the viewport to see boxes disappear as
+ you go below their threshold.
+
+
+ Visible at xs and above
+
+
+ Visible at sm and above
+
+
+ Visible at md and above
+
+
+ Visible at lg and above
+
+
+ Visible at xl and above
+
+
+ Visible at xxl and above
+
+