diff --git a/.changeset/typography-scale-headings.md b/.changeset/typography-scale-headings.md new file mode 100644 index 0000000000..e3b3fc2567 --- /dev/null +++ b/.changeset/typography-scale-headings.md @@ -0,0 +1,20 @@ +--- +"@cloudoperators/juno-ui-components": minor +--- + +Add h1–h6 base typography styles aligned with the Juno design system scale. + +- `global.css` and `theme.css`: h1–h6 defined in `@layer base` with IBM Plex Sans Bold, rem font sizes, and plain CSS `line-height` values. `theme.css` uses `var(--font-sans)` instead of a hardcoded font stack. +- `FormattedText`: heading sizes and line-heights aligned with the scale; h5 corrected (1.03rem → 1.125rem); h6 added; redundant `font-weight`/`font-style` declarations removed. +- `ContentHeading`: removed `jn:font-bold` and `jn:text-lg` overrides that were overriding the h1 base style with a smaller size. + +Fix h1 misuse in components that rendered UI labels as `

`, which caused unintended size changes after the global h1 style was introduced. Heading levels were chosen to reflect both visual size and accessibility (correct heading outline for screen readers). + +- `Modal`: title changed from `

` to `

`. The modal has `role="dialog"` which creates an isolated landmark; the title is referenced via `aria-labelledby`. A heading inside a dialog is semantically correct and independent of the page outline. `

` matches the previous visual size (~20px). +- `Form`: title changed from `

` to `

`. A form is a named content section; a heading is appropriate. `

` matches the previous visual size (~24px). +- `FormSection`: title changed from `

` to `

`. One level below `Form` (`

`) in the heading hierarchy. `

` was chosen over `

` (visual match) to maintain correct nesting. +- `SignInForm`: title changed from `

` to `

`. The form is often the primary content of a page but may also be embedded; `

` is a safe default for both cases. +- `IntroBox`: title changed from `

` to `

`. An info box is not a navigable section; adding it to the heading outline would confuse screen reader users navigating by heading. Bold styling preserved via existing `introboxHeading` class. +- `global.css`: replaced `font-weight: 700` with `@apply jn:font-bold` in h1–h6 base styles for consistency with Tailwind conventions. +- `theme.css`: same — replaced `font-weight: 700` with `@apply font-bold` (no `jn:` prefix, consistent with existing `@apply` usage in that file). +- `Modal`: ReactNode titles now render as `

` instead of `

` to avoid invalid HTML (block elements inside heading elements). String titles remain `

`. Both approaches are equivalent for assistive technologies. diff --git a/packages/ui-components/src/components/ContentHeading/ContentHeading.component.tsx b/packages/ui-components/src/components/ContentHeading/ContentHeading.component.tsx index 87aaf3a080..cf22f9c7d1 100644 --- a/packages/ui-components/src/components/ContentHeading/ContentHeading.component.tsx +++ b/packages/ui-components/src/components/ContentHeading/ContentHeading.component.tsx @@ -6,8 +6,6 @@ import React, { HTMLAttributes, ReactNode } from "react" const baseHeadingStyles = ` - jn:font-bold - jn:text-lg jn:text-theme-high jn:pb-2 ` diff --git a/packages/ui-components/src/components/Form/Form.component.tsx b/packages/ui-components/src/components/Form/Form.component.tsx index e6b610b800..b252ede711 100644 --- a/packages/ui-components/src/components/Form/Form.component.tsx +++ b/packages/ui-components/src/components/Form/Form.component.tsx @@ -10,8 +10,6 @@ const formBaseStyles = ` ` const formTitleStyles = ` - jn:text-2xl - jn:font-bold jn:mb-4 ` @@ -45,7 +43,7 @@ export interface FormProps extends FormHTMLAttributes { export const Form = ({ title = "", className = "", children, ...props }: FormProps): ReactNode => { return (
- {title ?

{title}

: null} + {title ?

{title}

: null} {children}
) diff --git a/packages/ui-components/src/components/FormSection/FormSection.component.tsx b/packages/ui-components/src/components/FormSection/FormSection.component.tsx index 38f160985e..df39d46e60 100644 --- a/packages/ui-components/src/components/FormSection/FormSection.component.tsx +++ b/packages/ui-components/src/components/FormSection/FormSection.component.tsx @@ -11,8 +11,6 @@ const formSectionBaseStyles = ` ` const headingStyles = ` - jn:text-lg - jn:font-bold jn:mb-4 ` @@ -45,7 +43,7 @@ export interface FormSectionProps extends HTMLAttributes { export const FormSection = ({ title = "", children, className = "", ...props }: FormSectionProps): ReactNode => { return (
- {title ?

{title}

: null} + {title ?

{title}

: null} {children}
) diff --git a/packages/ui-components/src/components/FormattedText/formatted-text.css b/packages/ui-components/src/components/FormattedText/formatted-text.css index 68d5bddfec..0ef051be29 100644 --- a/packages/ui-components/src/components/FormattedText/formatted-text.css +++ b/packages/ui-components/src/components/FormattedText/formatted-text.css @@ -36,42 +36,40 @@ h1 { font-size: 1.69rem; - font-weight: 700; - line-height: 1.11; /* round(40 / 36) */ + line-height: 1.375; margin-top: 0; margin-bottom: 2rem; /* rem(32) */ } h2 { font-size: 1.56rem; - font-weight: 700; - line-height: 160%; + line-height: 1.375; margin-top: 3rem; /* rem(48) */ margin-bottom: 1.5rem; /* rem(24) */ } h3 { font-size: 1.44rem; - font-weight: 700; - line-height: 160%; + line-height: 1.375; margin-top: 1rem; margin-bottom: 0.75rem; /* rem(12) */ } h4 { font-size: 1.28rem; - font-style: normal; - font-weight: 700; - line-height: 160%; + line-height: 1.625; margin-top: 1.5rem; /* rem(24) */ margin-bottom: 0.5rem; /* rem(8) */ } h5 { - font-size: 1.03rem; - font-style: normal; - font-weight: 700; - line-height: 160%; + font-size: 1.125rem; + line-height: 1.625; + } + + h6 { + font-size: 1rem; + line-height: 1.5; } img, diff --git a/packages/ui-components/src/components/IntroBox/IntroBox.component.tsx b/packages/ui-components/src/components/IntroBox/IntroBox.component.tsx index 0a719dddab..776519aa57 100644 --- a/packages/ui-components/src/components/IntroBox/IntroBox.component.tsx +++ b/packages/ui-components/src/components/IntroBox/IntroBox.component.tsx @@ -119,7 +119,7 @@ export const IntroBox = ({ >
- {title ?

{title}

: ""} + {title ?

{title}

: ""} {children ? children :

{text}

}

diff --git a/packages/ui-components/src/components/Message/Message.component.tsx b/packages/ui-components/src/components/Message/Message.component.tsx index 47294c3dbc..990406fcca 100644 --- a/packages/ui-components/src/components/Message/Message.component.tsx +++ b/packages/ui-components/src/components/Message/Message.component.tsx @@ -264,7 +264,7 @@ export const Message = ({
- {title &&

{title}

} + {title && {title}}
{children || text}
{dismissible && ( diff --git a/packages/ui-components/src/components/Modal/Modal.component.tsx b/packages/ui-components/src/components/Modal/Modal.component.tsx index 71839c3e5d..327487bbe1 100644 --- a/packages/ui-components/src/components/Modal/Modal.component.tsx +++ b/packages/ui-components/src/components/Modal/Modal.component.tsx @@ -57,7 +57,9 @@ const headerstyles = ` ` const titlestyles = ` - jn:text-xl + jn:font-sans + jn:text-[1.28rem] + jn:leading-relaxed jn:font-bold ` @@ -177,13 +179,13 @@ export const Modal = ({ } if (typeof modalTitle === "string") { return ( -

+

{modalTitle} -

+

) } return ( -
+
{modalTitle}
) diff --git a/packages/ui-components/src/components/Modal/Modal.test.tsx b/packages/ui-components/src/components/Modal/Modal.test.tsx index 30034466f9..f3b9306703 100644 --- a/packages/ui-components/src/components/Modal/Modal.test.tsx +++ b/packages/ui-components/src/components/Modal/Modal.test.tsx @@ -53,7 +53,7 @@ describe("Modal", () => { ) const dialog = screen.getByRole("dialog") - const titleWrapper = screen.getByText("Node Title").closest("div") + const titleWrapper = screen.getByRole("heading", { level: 4 }) expect(dialog).toBeInTheDocument() expect(titleWrapper).toBeInTheDocument() diff --git a/packages/ui-components/src/components/PopupMenu/PopupMenu.component.tsx b/packages/ui-components/src/components/PopupMenu/PopupMenu.component.tsx index ca657b0cfe..9c40e50799 100644 --- a/packages/ui-components/src/components/PopupMenu/PopupMenu.component.tsx +++ b/packages/ui-components/src/components/PopupMenu/PopupMenu.component.tsx @@ -398,7 +398,7 @@ export const PopupMenuSectionHeading = ({ }: PopupMenuSectionHeadingProps): ReactNode => { return (
-

{label && label.length ? label : children}

+ {label && label.length ? label : children}
) } diff --git a/packages/ui-components/src/components/SignInForm/SignInForm.component.tsx b/packages/ui-components/src/components/SignInForm/SignInForm.component.tsx index 469ee20ce1..c813fae5d2 100644 --- a/packages/ui-components/src/components/SignInForm/SignInForm.component.tsx +++ b/packages/ui-components/src/components/SignInForm/SignInForm.component.tsx @@ -8,8 +8,6 @@ import { Message } from "../Message" import { Stack } from "../Stack" const signInFormTitleStyles = ` - jn:text-xl - jn:font-bold jn:text-theme-highest jn:mb-4 ` @@ -75,7 +73,7 @@ export const SignInForm = ({ return (
- {title !== false &&

{title}

} + {title !== false &&

{title}

} {errorMessage && } diff --git a/packages/ui-components/src/components/SignInForm/SignInForm.test.tsx b/packages/ui-components/src/components/SignInForm/SignInForm.test.tsx index d371b13c0f..31e4a26f75 100644 --- a/packages/ui-components/src/components/SignInForm/SignInForm.test.tsx +++ b/packages/ui-components/src/components/SignInForm/SignInForm.test.tsx @@ -43,15 +43,15 @@ describe("SignInForm Component Tests", () => { describe("Title Prop", () => { test("renders default title 'Sign In' when no title prop is passed", () => { render() - expect(screen.getByRole("heading")).toBeInTheDocument() - expect(screen.getByRole("heading")).toHaveClass("juno-sign-in-form-heading") - expect(screen.getByRole("heading")).toHaveTextContent("Sign In") + expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument() + expect(screen.getByRole("heading", { level: 2 })).toHaveClass("juno-sign-in-form-heading") + expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent("Sign In") }) test("renders custom title when string is passed", () => { render() - expect(screen.getByRole("heading")).toBeInTheDocument() - expect(screen.getByRole("heading")).toHaveTextContent("Welcome Back") + expect(screen.getByRole("heading", { level: 2 })).toBeInTheDocument() + expect(screen.getByRole("heading", { level: 2 })).toHaveTextContent("Welcome Back") }) test("does not render title when title={false}", () => { @@ -61,16 +61,14 @@ describe("SignInForm Component Tests", () => { test("renders empty string title as empty heading", () => { render() - const heading = screen.queryByRole("heading") + const heading = screen.queryByRole("heading", { level: 2 }) expect(heading).toBeInTheDocument() expect(heading).toHaveTextContent("") }) test("applies correct styling to title", () => { render() - const heading = screen.getByRole("heading") - expect(heading).toHaveClass("jn:text-xl") - expect(heading).toHaveClass("jn:font-bold") + const heading = screen.getByRole("heading", { level: 2 }) expect(heading).toHaveClass("jn:text-theme-highest") }) }) diff --git a/packages/ui-components/src/global.css b/packages/ui-components/src/global.css index 0765870951..c458c72043 100644 --- a/packages/ui-components/src/global.css +++ b/packages/ui-components/src/global.css @@ -1052,6 +1052,45 @@ @apply jn:text-theme-link; } + h1, + h2, + h3, + h4, + h5, + h6 { + @apply jn:font-sans jn:font-bold; + } + + h1 { + font-size: 1.69rem; + line-height: 1.375; + } + + h2 { + font-size: 1.56rem; + line-height: 1.375; + } + + h3 { + font-size: 1.44rem; + line-height: 1.375; + } + + h4 { + font-size: 1.28rem; + line-height: 1.625; + } + + h5 { + font-size: 1.125rem; + line-height: 1.625; + } + + h6 { + font-size: 1rem; + line-height: 1.5; + } + /* adds pointer cursor to buttons to restore tw3 behavior */ button:not(:disabled), [role="button"]:not(:disabled) { diff --git a/packages/ui-components/src/theme.css b/packages/ui-components/src/theme.css index f81b5234c0..3281b96937 100644 --- a/packages/ui-components/src/theme.css +++ b/packages/ui-components/src/theme.css @@ -1122,3 +1122,45 @@ --tw-content: ""; } } + +@layer base { + h1, + h2, + h3, + h4, + h5, + h6 { + font-family: var(--font-sans); + @apply font-bold; + } + + h1 { + font-size: 1.69rem; + line-height: 1.375; + } + + h2 { + font-size: 1.56rem; + line-height: 1.375; + } + + h3 { + font-size: 1.44rem; + line-height: 1.375; + } + + h4 { + font-size: 1.28rem; + line-height: 1.625; + } + + h5 { + font-size: 1.125rem; + line-height: 1.625; + } + + h6 { + font-size: 1rem; + line-height: 1.5; + } +}