From 1cf2972f4598870d27f30787aec6fcaf3659fad0 Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Wed, 14 Jan 2026 18:30:10 +0100 Subject: [PATCH 01/14] feat: add backstage/ui hooks documentation Signed-off-by: Antony Bouyon --- docs-ui/src/app/hooks/[slug]/page.tsx | 23 +++++ docs-ui/src/app/hooks/page.mdx | 34 +++++++ .../src/components/Navigation/Navigation.tsx | 15 +++- .../content/hooks/use-breakpoint.example.tsx | 28 ++++++ docs-ui/src/content/hooks/use-breakpoint.mdx | 44 +++++++++ .../src/content/hooks/use-breakpoint.props.ts | 37 ++++++++ .../content/hooks/use-media-query.example.tsx | 42 +++++++++ docs-ui/src/content/hooks/use-media-query.mdx | 87 ++++++++++++++++++ .../content/hooks/use-media-query.props.ts | 90 +++++++++++++++++++ docs-ui/src/utils/data.ts | 23 +++++ 10 files changed, 422 insertions(+), 1 deletion(-) create mode 100644 docs-ui/src/app/hooks/[slug]/page.tsx create mode 100644 docs-ui/src/app/hooks/page.mdx create mode 100644 docs-ui/src/content/hooks/use-breakpoint.example.tsx create mode 100644 docs-ui/src/content/hooks/use-breakpoint.mdx create mode 100644 docs-ui/src/content/hooks/use-breakpoint.props.ts create mode 100644 docs-ui/src/content/hooks/use-media-query.example.tsx create mode 100644 docs-ui/src/content/hooks/use-media-query.mdx create mode 100644 docs-ui/src/content/hooks/use-media-query.props.ts diff --git a/docs-ui/src/app/hooks/[slug]/page.tsx b/docs-ui/src/app/hooks/[slug]/page.tsx new file mode 100644 index 0000000000..4f750a917d --- /dev/null +++ b/docs-ui/src/app/hooks/[slug]/page.tsx @@ -0,0 +1,23 @@ +import { hooks } from '@/utils/data'; + +export default async function Page({ + params, +}: { + params: Promise<{ slug: string }>; +}) { + const { slug } = await params; + + const { default: Component } = await import(`@/content/hooks/${slug}.mdx`); + + return ; +} + +export function generateStaticParams() { + const list = [...hooks]; + + return list.map(hook => ({ + slug: hook.slug, + })); +} + +export const dynamicParams = false; diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx new file mode 100644 index 0000000000..b2e2b81e7d --- /dev/null +++ b/docs-ui/src/app/hooks/page.mdx @@ -0,0 +1,34 @@ +import { CodeBlock } from '@/components/CodeBlock'; +import { ComponentCard, ComponentCards } from '@/components/ComponentCards'; + +# Hooks + +Backstage UI custom hooks provide easy access to design system features, style management, and responsive interface creation. + + + + + + + + diff --git a/docs-ui/src/components/Navigation/Navigation.tsx b/docs-ui/src/components/Navigation/Navigation.tsx index bad77e942e..f4fc51993f 100644 --- a/docs-ui/src/components/Navigation/Navigation.tsx +++ b/docs-ui/src/components/Navigation/Navigation.tsx @@ -11,13 +11,26 @@ import { RiServiceLine, RiStackLine, } from '@remixicon/react'; -import { components } from '@/utils/data'; +import { components, hooks } from '@/utils/data'; import styles from './Navigation.module.css'; interface NavigationProps { onLinkClick?: () => void; } +const data = [ + { + title: 'Components', + content: components, + url: '/components', + }, + { + title: 'Hooks', + content: hooks, + url: '/hooks', + }, +]; + export const Navigation = ({ onLinkClick }: NavigationProps) => { const pathname = usePathname(); diff --git a/docs-ui/src/content/hooks/use-breakpoint.example.tsx b/docs-ui/src/content/hooks/use-breakpoint.example.tsx new file mode 100644 index 0000000000..d4d21861bb --- /dev/null +++ b/docs-ui/src/content/hooks/use-breakpoint.example.tsx @@ -0,0 +1,28 @@ +'use client'; + +import { useBreakpoint } from '@backstage/ui'; +import { useEffect, useState } from 'react'; + +export function UseBreakpointExample() { + const { breakpoint, up, down } = useBreakpoint(); + const [isMounted, setIsMounted] = useState(false); + + // prevent hydration mismatch by rendering only on the client + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) { + return null; + } + + return ( +
+

Current Breakpoint: {breakpoint}

+ {(up('md') &&

The viewport is larger than 1024px.

) || + (down('sm') &&

The viewport is smaller than 768px.

) || ( +

The viewport is between 768px and 1024px.

+ )} +
+ ); +} diff --git a/docs-ui/src/content/hooks/use-breakpoint.mdx b/docs-ui/src/content/hooks/use-breakpoint.mdx new file mode 100644 index 0000000000..89f5a7f757 --- /dev/null +++ b/docs-ui/src/content/hooks/use-breakpoint.mdx @@ -0,0 +1,44 @@ +import { ChangelogComponent } from '@/components/ChangelogComponent'; +import { CodeBlock } from '@/components/CodeBlock'; +import { PageTitle } from '@/components/PageTitle'; +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { UseBreakpointExample } from './use-breakpoint.example'; +import { + useBreakpointExampleSnippet, + useBreakpointReturnDefs, +} from './use-breakpoint.props'; + + + +## Usage + +The `useBreakpoint` hook returns the active breakpoint and two functions, `up` and `down`, to check if the viewport is above, or below a given breakpoint, letting you adjust your UI responsively. + +} + code={useBreakpointExampleSnippet} +/> + +## Breakpoints + +The default breakpoints are: + +- **initial**: < 640px +- **xs**: ≥ 640px < 768px +- **sm**: ≥ 768px < 1024px +- **md**: ≥ 1024px < 1280px +- **lg**: ≥ 1280px < 1536px +- **xl**: ≥ 1536px + +## Return Value + + + + diff --git a/docs-ui/src/content/hooks/use-breakpoint.props.ts b/docs-ui/src/content/hooks/use-breakpoint.props.ts new file mode 100644 index 0000000000..298b31b3e3 --- /dev/null +++ b/docs-ui/src/content/hooks/use-breakpoint.props.ts @@ -0,0 +1,37 @@ +import { type PropDef } from '@/utils/propDefs'; + +const Breakpoint = ['"initial"', '"xs"', '"sm"', '"md"', '"lg"', '"xl"']; + +export const useBreakpointReturnDefs: Record = { + breakpoint: { + type: 'enum', + values: Breakpoint, + description: 'The current active breakpoint based on screen width', + }, + up: { + type: 'enum', + values: [`(breakpoint: ${Breakpoint.join(' | ')}) => boolean`], + description: + 'Function that takes a breakpoint and returns true if the screen width is at or above that breakpoint', + }, + down: { + type: 'enum', + values: [`(breakpoint: ${Breakpoint.join(' | ')}) => boolean`], + description: + 'Function that takes a breakpoint and returns true if the screen width is at or below that breakpoint', + }, +}; + +export const useBreakpointExampleSnippet = `import { useBreakpoint } from '@backstage/ui'; + +function ResponsiveComponent() { + const { breakpoint, up, down } = useBreakpoint(); + + return ( +
+

Current Breakpoint: {breakpoint}

+ {up('md') &&

The viewport is medium or larger.

} + {down('sm') &&

The viewport is small or smaller.

} +
+ ); +}`; diff --git a/docs-ui/src/content/hooks/use-media-query.example.tsx b/docs-ui/src/content/hooks/use-media-query.example.tsx new file mode 100644 index 0000000000..0d8edd944b --- /dev/null +++ b/docs-ui/src/content/hooks/use-media-query.example.tsx @@ -0,0 +1,42 @@ +'use client'; +import { useMediaQuery } from '@backstage/ui/src/hooks/useMediaQuery'; + +export function UseMediaQueryThemeExample() { + const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + return ( +
+ {isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'} +
+ ); +} + +export function UseMediaQueryResponsiveExample() { + const isMobile = useMediaQuery('(max-width: 768px)'); + const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)'); + const isDesktop = useMediaQuery('(min-width: 1025px)'); + + return ( +
+ {isMobile && ''} + {isTablet && ''} + {isDesktop && ''} +
+ ); +} + +export function UseMediaQueryPreferencesExample() { + const prefersReducedMotion = useMediaQuery( + '(prefers-reduced-motion: reduce)', + ); + + return ( +
Content
+ ); +} + +export function UseMediaQueryOrientationExample() { + const isPortrait = useMediaQuery('(orientation: portrait)'); + + return
{isPortrait ? 'Portrait mode' : 'Landscape mode'}
; +} diff --git a/docs-ui/src/content/hooks/use-media-query.mdx b/docs-ui/src/content/hooks/use-media-query.mdx new file mode 100644 index 0000000000..6941bc0d68 --- /dev/null +++ b/docs-ui/src/content/hooks/use-media-query.mdx @@ -0,0 +1,87 @@ +import { ChangelogComponent } from '@/components/ChangelogComponent'; +import { CodeBlock } from '@/components/CodeBlock'; +import { PageTitle } from '@/components/PageTitle'; +import { PropsTable } from '@/components/PropsTable'; +import { + useMediaQueryOrientationSnippet, + useMediaQueryParamDefs, + useMediaQueryPreferencesSnippet, + useMediaQueryResponsiveSnippet, + useMediaQueryReturnDefs, + useMediaQueryUsageSnippet, +} from './use-media-query.props'; +import { Snippet } from '@/components/Snippet'; +import { + UseMediaQueryThemeExample, + UseMediaQueryPreferencesExample, + UseMediaQueryResponsiveExample, + UseMediaQueryOrientationExample, +} from './use-media-query.example'; + + + +## Usage + +The `useMediaQuery` hook allows you to evaluate CSS media queries in your components, enabling responsive design and behavior. + +} + code={useMediaQueryUsageSnippet} +/> + +## Parameters + + + +## Return Value + + +## Examples + +### Responsive Layouts + +Adapt layout based on different screen sizes. + +} + code={useMediaQueryResponsiveSnippet} +/> + +### User Preferences + +Respect user system preferences. + +} + code={useMediaQueryPreferencesSnippet} +/> + +### Device Orientation + +Detect screen orientation. + +} + code={useMediaQueryOrientationSnippet} +/> + + diff --git a/docs-ui/src/content/hooks/use-media-query.props.ts b/docs-ui/src/content/hooks/use-media-query.props.ts new file mode 100644 index 0000000000..4a3bb1ca73 --- /dev/null +++ b/docs-ui/src/content/hooks/use-media-query.props.ts @@ -0,0 +1,90 @@ +import { type PropDef } from '@/utils/propDefs'; + +export const useMediaQueryParamDefs: Record = { + query: { + type: 'string', + description: 'The CSS media query to evaluate', + }, + options: { + type: 'complex', + complexType: { + name: 'UseMediaQueryOptions', + properties: { + defaultValue: { + type: 'boolean', + required: false, + description: + 'Default value to use when rendering on the server, defaults to false', + }, + initializeWithValue: { + type: 'boolean', + required: false, + description: + 'Whether to initialize with the current value of the media query, or use the default value initially', + }, + }, + }, + default: 'defaultValue: false\ninitializeWithValue: true', + }, +}; + +export const useMediaQueryReturnDefs: Record = { + matches: { + type: 'boolean', + description: 'True if the media query currently matches', + }, +}; + +export const useMediaQueryUsageSnippet = `import { useMediaQuery } from '@backstage/ui'; + +function MyComponent() { + const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); + + return ( +
+ {isDarkMode ? 'Dark mode enabled' : 'Light mode enabled'} +
+ ); +}`; + +export const useMediaQueryResponsiveSnippet = `import { useMediaQuery } from '@backstage/ui'; + +function ResponsiveLayout() { + const isMobile = useMediaQuery('(max-width: 768px)'); + const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)'); + const isDesktop = useMediaQuery('(min-width: 1025px)'); + + return ( +
+ {isMobile && } + {isTablet && } + {isDesktop && } +
+ ); +}`; + +export const useMediaQueryPreferencesSnippet = `import { useMediaQuery } from '@backstage/ui'; + +function AccessibleComponent() { + const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); + + return ( +
+ Content +
+ ); +}`; + +export const useMediaQueryOrientationSnippet = `import { useMediaQuery } from '@backstage/ui'; + +function OrientationAware() { + const isPortrait = useMediaQuery('(orientation: portrait)'); + + return ( +
+ {isPortrait ? 'Portrait mode' : 'Landscape mode'} +
+ ); +}`; diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index 7426b16130..bb1ea40dfb 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -138,3 +138,26 @@ export const components: Page[] = [ slug: 'visually-hidden', }, ]; + +export const hooks: Page[] = [ + { + title: 'useBreakpoint', + slug: 'use-breakpoint', + }, + { + title: 'useIsomorphicLayoutEffect', + slug: 'use-isomorphic-layout-effect', + }, + { + title: 'useMediaQuery', + slug: 'use-media-query', + }, + { + title: 'useStyles', + slug: 'use-styles', + }, + { + title: 'useSurface', + slug: 'use-surface', + }, +]; From d61a7953831983ee342a094a108dffbb84fedefc Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Fri, 16 Jan 2026 17:56:30 +0100 Subject: [PATCH 02/14] refactor: migrate to the new format Signed-off-by: Antony Bouyon --- docs-ui/src/app/hooks/[slug]/page.tsx | 23 ------------ .../hooks/use-breakpoint/components.tsx} | 0 .../hooks/use-breakpoint/page.mdx} | 8 ++-- .../hooks/use-breakpoint/props-definition.ts} | 14 ------- .../src/app/hooks/use-breakpoint/snippets.ts | 13 +++++++ .../hooks/use-media-query/components.tsx} | 0 .../hooks/use-media-query/page.mdx} | 24 ++++++------ .../hooks/use-media-query/props-definition.ts | 36 ++++++++++++++++++ .../hooks/use-media-query/snippets.ts} | 37 ------------------- 9 files changed, 67 insertions(+), 88 deletions(-) delete mode 100644 docs-ui/src/app/hooks/[slug]/page.tsx rename docs-ui/src/{content/hooks/use-breakpoint.example.tsx => app/hooks/use-breakpoint/components.tsx} (100%) rename docs-ui/src/{content/hooks/use-breakpoint.mdx => app/hooks/use-breakpoint/page.mdx} (88%) rename docs-ui/src/{content/hooks/use-breakpoint.props.ts => app/hooks/use-breakpoint/props-definition.ts} (66%) create mode 100644 docs-ui/src/app/hooks/use-breakpoint/snippets.ts rename docs-ui/src/{content/hooks/use-media-query.example.tsx => app/hooks/use-media-query/components.tsx} (100%) rename docs-ui/src/{content/hooks/use-media-query.mdx => app/hooks/use-media-query/page.mdx} (95%) create mode 100644 docs-ui/src/app/hooks/use-media-query/props-definition.ts rename docs-ui/src/{content/hooks/use-media-query.props.ts => app/hooks/use-media-query/snippets.ts} (58%) diff --git a/docs-ui/src/app/hooks/[slug]/page.tsx b/docs-ui/src/app/hooks/[slug]/page.tsx deleted file mode 100644 index 4f750a917d..0000000000 --- a/docs-ui/src/app/hooks/[slug]/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { hooks } from '@/utils/data'; - -export default async function Page({ - params, -}: { - params: Promise<{ slug: string }>; -}) { - const { slug } = await params; - - const { default: Component } = await import(`@/content/hooks/${slug}.mdx`); - - return ; -} - -export function generateStaticParams() { - const list = [...hooks]; - - return list.map(hook => ({ - slug: hook.slug, - })); -} - -export const dynamicParams = false; diff --git a/docs-ui/src/content/hooks/use-breakpoint.example.tsx b/docs-ui/src/app/hooks/use-breakpoint/components.tsx similarity index 100% rename from docs-ui/src/content/hooks/use-breakpoint.example.tsx rename to docs-ui/src/app/hooks/use-breakpoint/components.tsx diff --git a/docs-ui/src/content/hooks/use-breakpoint.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx similarity index 88% rename from docs-ui/src/content/hooks/use-breakpoint.mdx rename to docs-ui/src/app/hooks/use-breakpoint/page.mdx index 89f5a7f757..47ac3ab84f 100644 --- a/docs-ui/src/content/hooks/use-breakpoint.mdx +++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx @@ -3,11 +3,13 @@ import { CodeBlock } from '@/components/CodeBlock'; import { PageTitle } from '@/components/PageTitle'; import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; -import { UseBreakpointExample } from './use-breakpoint.example'; +import { UseBreakpointExample } from './components'; import { - useBreakpointExampleSnippet, useBreakpointReturnDefs, -} from './use-breakpoint.props'; +} from './props-definition'; +import { + useBreakpointExampleSnippet +} from './snippets'; = { 'Function that takes a breakpoint and returns true if the screen width is at or below that breakpoint', }, }; - -export const useBreakpointExampleSnippet = `import { useBreakpoint } from '@backstage/ui'; - -function ResponsiveComponent() { - const { breakpoint, up, down } = useBreakpoint(); - - return ( -
-

Current Breakpoint: {breakpoint}

- {up('md') &&

The viewport is medium or larger.

} - {down('sm') &&

The viewport is small or smaller.

} -
- ); -}`; diff --git a/docs-ui/src/app/hooks/use-breakpoint/snippets.ts b/docs-ui/src/app/hooks/use-breakpoint/snippets.ts new file mode 100644 index 0000000000..19e6debe65 --- /dev/null +++ b/docs-ui/src/app/hooks/use-breakpoint/snippets.ts @@ -0,0 +1,13 @@ +export const useBreakpointExampleSnippet = `import { useBreakpoint } from '@backstage/ui'; + +function ResponsiveComponent() { + const { breakpoint, up, down } = useBreakpoint(); + + return ( +
+

Current Breakpoint: {breakpoint}

+ {up('md') &&

The viewport is medium or larger.

} + {down('sm') &&

The viewport is small or smaller.

} +
+ ); +}`; diff --git a/docs-ui/src/content/hooks/use-media-query.example.tsx b/docs-ui/src/app/hooks/use-media-query/components.tsx similarity index 100% rename from docs-ui/src/content/hooks/use-media-query.example.tsx rename to docs-ui/src/app/hooks/use-media-query/components.tsx diff --git a/docs-ui/src/content/hooks/use-media-query.mdx b/docs-ui/src/app/hooks/use-media-query/page.mdx similarity index 95% rename from docs-ui/src/content/hooks/use-media-query.mdx rename to docs-ui/src/app/hooks/use-media-query/page.mdx index 6941bc0d68..84668b2a3e 100644 --- a/docs-ui/src/content/hooks/use-media-query.mdx +++ b/docs-ui/src/app/hooks/use-media-query/page.mdx @@ -2,21 +2,23 @@ import { ChangelogComponent } from '@/components/ChangelogComponent'; import { CodeBlock } from '@/components/CodeBlock'; import { PageTitle } from '@/components/PageTitle'; import { PropsTable } from '@/components/PropsTable'; -import { - useMediaQueryOrientationSnippet, - useMediaQueryParamDefs, - useMediaQueryPreferencesSnippet, - useMediaQueryResponsiveSnippet, - useMediaQueryReturnDefs, - useMediaQueryUsageSnippet, -} from './use-media-query.props'; import { Snippet } from '@/components/Snippet'; import { - UseMediaQueryThemeExample, + UseMediaQueryOrientationExample, UseMediaQueryPreferencesExample, UseMediaQueryResponsiveExample, - UseMediaQueryOrientationExample, -} from './use-media-query.example'; + UseMediaQueryThemeExample, +} from './components'; +import { + useMediaQueryParamDefs, + useMediaQueryReturnDefs, +} from './props-definition'; +import { + useMediaQueryOrientationSnippet, + useMediaQueryPreferencesSnippet, + useMediaQueryResponsiveSnippet, + useMediaQueryUsageSnippet, +} from './snippets'; = { + query: { + type: 'string', + description: 'The CSS media query to evaluate', + }, + options: { + type: 'complex', + complexType: { + name: 'UseMediaQueryOptions', + properties: { + defaultValue: { + type: 'boolean', + required: false, + description: + 'Default value to use when rendering on the server, defaults to false', + }, + initializeWithValue: { + type: 'boolean', + required: false, + description: + 'Whether to initialize with the current value of the media query, or use the default value initially', + }, + }, + }, + default: 'defaultValue: false\ninitializeWithValue: true', + }, +}; + +export const useMediaQueryReturnDefs: Record = { + matches: { + type: 'boolean', + description: 'True if the media query currently matches', + }, +}; diff --git a/docs-ui/src/content/hooks/use-media-query.props.ts b/docs-ui/src/app/hooks/use-media-query/snippets.ts similarity index 58% rename from docs-ui/src/content/hooks/use-media-query.props.ts rename to docs-ui/src/app/hooks/use-media-query/snippets.ts index 4a3bb1ca73..78423ef516 100644 --- a/docs-ui/src/content/hooks/use-media-query.props.ts +++ b/docs-ui/src/app/hooks/use-media-query/snippets.ts @@ -1,40 +1,3 @@ -import { type PropDef } from '@/utils/propDefs'; - -export const useMediaQueryParamDefs: Record = { - query: { - type: 'string', - description: 'The CSS media query to evaluate', - }, - options: { - type: 'complex', - complexType: { - name: 'UseMediaQueryOptions', - properties: { - defaultValue: { - type: 'boolean', - required: false, - description: - 'Default value to use when rendering on the server, defaults to false', - }, - initializeWithValue: { - type: 'boolean', - required: false, - description: - 'Whether to initialize with the current value of the media query, or use the default value initially', - }, - }, - }, - default: 'defaultValue: false\ninitializeWithValue: true', - }, -}; - -export const useMediaQueryReturnDefs: Record = { - matches: { - type: 'boolean', - description: 'True if the media query currently matches', - }, -}; - export const useMediaQueryUsageSnippet = `import { useMediaQuery } from '@backstage/ui'; function MyComponent() { From 9c7b3dbf1cdf72829c8e340ed65819d828caf0bb Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Fri, 16 Jan 2026 18:36:03 +0100 Subject: [PATCH 03/14] chore: run prettier Signed-off-by: Antony Bouyon --- docs-ui/src/app/hooks/use-breakpoint/page.mdx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs-ui/src/app/hooks/use-breakpoint/page.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx index 47ac3ab84f..4ee410fc74 100644 --- a/docs-ui/src/app/hooks/use-breakpoint/page.mdx +++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx @@ -4,12 +4,8 @@ import { PageTitle } from '@/components/PageTitle'; import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; import { UseBreakpointExample } from './components'; -import { - useBreakpointReturnDefs, -} from './props-definition'; -import { - useBreakpointExampleSnippet -} from './snippets'; +import { useBreakpointReturnDefs } from './props-definition'; +import { useBreakpointExampleSnippet } from './snippets'; Date: Wed, 21 Jan 2026 13:16:08 +0100 Subject: [PATCH 04/14] fix: improve banner readability Signed-off-by: Antony Bouyon --- docs-ui/src/components/Banner/styles.module.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs-ui/src/components/Banner/styles.module.css b/docs-ui/src/components/Banner/styles.module.css index 9666c5d521..d9d3323376 100644 --- a/docs-ui/src/components/Banner/styles.module.css +++ b/docs-ui/src/components/Banner/styles.module.css @@ -16,13 +16,13 @@ .info { background-color: #f2f2f2; border-color: #cdcdcd; - color: #888888; + color: #4b4b4b; } .warning { background-color: #fff2b9; - border-color: #ffd000; - color: #d79927; + border-color: #d79927; + color: #8a3d09; } .icon { From a57041b196a0c5cd31dbeae15d0c851a4968b942 Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Wed, 21 Jan 2026 13:16:46 +0100 Subject: [PATCH 05/14] feat: add useStyles hook documentation & disclaimer for internal hooks Signed-off-by: Antony Bouyon --- docs-ui/src/app/hooks/page.mdx | 12 +- .../src/app/hooks/use-media-query/page.mdx | 6 + docs-ui/src/app/hooks/use-styles/page.mdx | 59 ++++++++++ .../app/hooks/use-styles/props-definition.ts | 58 ++++++++++ docs-ui/src/app/hooks/use-styles/snippets.ts | 105 ++++++++++++++++++ 5 files changed, 229 insertions(+), 11 deletions(-) create mode 100644 docs-ui/src/app/hooks/use-styles/page.mdx create mode 100644 docs-ui/src/app/hooks/use-styles/props-definition.ts create mode 100644 docs-ui/src/app/hooks/use-styles/snippets.ts diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx index b2e2b81e7d..75bee70743 100644 --- a/docs-ui/src/app/hooks/page.mdx +++ b/docs-ui/src/app/hooks/page.mdx @@ -18,17 +18,7 @@ Backstage UI custom hooks provide easy access to design system features, style m /> - - diff --git a/docs-ui/src/app/hooks/use-media-query/page.mdx b/docs-ui/src/app/hooks/use-media-query/page.mdx index 84668b2a3e..bd01152581 100644 --- a/docs-ui/src/app/hooks/use-media-query/page.mdx +++ b/docs-ui/src/app/hooks/use-media-query/page.mdx @@ -3,6 +3,7 @@ import { CodeBlock } from '@/components/CodeBlock'; import { PageTitle } from '@/components/PageTitle'; import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; +import { Banner } from '@/components/Banner'; import { UseMediaQueryOrientationExample, UseMediaQueryPreferencesExample, @@ -25,6 +26,11 @@ import { description="Hook to evaluate and react to CSS media queries programmatically." /> + + ## Usage The `useMediaQuery` hook allows you to evaluate CSS media queries in your components, enabling responsive design and behavior. diff --git a/docs-ui/src/app/hooks/use-styles/page.mdx b/docs-ui/src/app/hooks/use-styles/page.mdx new file mode 100644 index 0000000000..ca235c4778 --- /dev/null +++ b/docs-ui/src/app/hooks/use-styles/page.mdx @@ -0,0 +1,59 @@ +import { ChangelogComponent } from '@/components/ChangelogComponent'; +import { CodeBlock } from '@/components/CodeBlock'; +import { Banner } from '@/components/Banner'; +import { PageTitle } from '@/components/PageTitle'; +import { PropsTable } from '@/components/PropsTable'; +import { + useStylesUsageSnippet, + useStylesDefsSnippet, + useStylesAttributesSnippet, +} from './snippets'; +import { useStylesPropsDefs, useStylesReturnDefs } from './props-definition'; + + + + + +## Usage + +The `useStyles` hook is the core styling utility that processes component definitions and props to generate: + +- **Class names** +- **Data attributes** for state-based styling (e.g., `data-disabled`, `data-variant`) +- **Utility classes** from utility props like `padding`, `margin`, `display` (with responsive support) +- **CSS custom properties** for custom values not in predefined utility lists +- **Cleaned props** with utility props removed (safe to spread onto DOM elements) + + + +## Parameters + + +## Return Value + + + +## How It Works + +### Component Definition + +A component definition describes how a component should be styled: + + + + + +### Data Attributes + +Data attributes enable state-based styling through CSS selectors. The hook automatically adds `data-*` to the attributes based on the defined props: + + + + + diff --git a/docs-ui/src/app/hooks/use-styles/props-definition.ts b/docs-ui/src/app/hooks/use-styles/props-definition.ts new file mode 100644 index 0000000000..0c8a2a1728 --- /dev/null +++ b/docs-ui/src/app/hooks/use-styles/props-definition.ts @@ -0,0 +1,58 @@ +import { type PropDef } from '@/utils/propDefs'; + +export const useStylesPropsDefs: Record = { + componentDefinition: { + type: 'complex', + complexType: { + name: 'ComponentDefinition', + properties: { + classNames: { + type: 'Record', + required: true, + }, + dataAttributes: { + type: 'Record', + required: false, + }, + utilityProps: { + type: 'string[]', + required: false, + }, + }, + }, + }, + props: { + type: 'enum', + description: 'All component props', + values: ['{ [key: string]: any; }'], + }, +}; + +export const useStylesReturnDefs: Record = { + classNames: { + type: 'enum', + values: ['Record'], + description: + "The component's class names mapped to their style definitions", + }, + dataAttributes: { + type: 'enum', + values: ['Record'], + description: + 'Data attributes generated from the component definition and props', + }, + utilityClasses: { + type: 'string', + description: 'Combined utility classes based on utility props', + }, + style: { + type: 'enum', + values: ['React.CSSProperties'], + description: 'The combined style object for the component', + }, + cleanedProps: { + type: 'enum', + values: ['Record'], + description: 'The original props with utility props removed', + }, +}; diff --git a/docs-ui/src/app/hooks/use-styles/snippets.ts b/docs-ui/src/app/hooks/use-styles/snippets.ts new file mode 100644 index 0000000000..55cf7ddee7 --- /dev/null +++ b/docs-ui/src/app/hooks/use-styles/snippets.ts @@ -0,0 +1,105 @@ +export const useStylesUsageSnippet = `import { useStyles } from '@backstage/ui'; +import type { + ComponentDefinition, + SpaceProps, + UtilityProps, +} from '@backstage/ui'; + +// Define your component's styling configuration +const buttonDefinition: ComponentDefinition = { + classNames: { + root: 'bui-button', + icon: 'bui-button-icon', + }, + dataAttributes: { + variant: ['primary', 'secondary', 'ghost'] as const, + size: ['small', 'medium', 'large'] as const, + 'is-disabled': [true, false] as const, + }, + utilityProps: ['gap', 'mb', 'mt', 'ml', 'mr'], +} as const satisfies ComponentDefinition; + +// type the component props +interface ButtonProps { + variant?: 'primary' | 'secondary' | 'ghost'; + size?: 'small' | 'medium' | 'large'; + isDisabled?: boolean; + gap?: UtilityProps['gap']; + mb?: SpaceProps['mb']; + mt?: SpaceProps['mt']; + ml?: SpaceProps['ml']; + mr?: SpaceProps['mr']; + icon?: React.ReactElement; + children?: React.ReactNode; +} + +// Use the useStyles hook in your component +export function Button({ + variant = 'primary', + size = 'medium', + isDisabled, + ...props +}: Readonly) { + const { classNames, dataAttributes, utilityClasses, cleanedProps } = + useStyles(buttonDefinition, { + variant, + size, + 'is-disabled': isDisabled, + ...props, + }); + + return ( + + ); +}`; + +export const useStylesDefsSnippet = `const componentDefinition = { + // CSS class names for component parts + classNames: { + root: 'bui-button', + icon: 'bui-button-icon', + }, + + // Props that become data attributes for CSS targeting + // data-attribute must be kebab-case to respect HTML standards + // And casing matters here, lowercase only: + // 'variant' not 'Variant', 'is-disabled' not 'isDisabled'.. + dataAttributes: { + variant: ['primary', 'secondary'] as const, + size: ['small', 'large'] as const, + 'is-disabled': [true, false] as const, + }, + + // Props that become utility classes (spacing, layout, etc.) + utilityProps: ['m', 'mb', 'mt', 'ml', 'mr'], +};`; + +export const useStylesAttributesSnippet = `/* Component CSS can use data attributes for variants */ +.bui-button { + padding: 0.5rem 1rem; + border-radius: 0.25rem; +} + +/* Variant styles */ +.bui-button[data-variant="primary"] { + background: blue; + color: white; +} + +.bui-button[data-variant="secondary"] { + background: gray; + color: black; +} + +/* State styles */ +.bui-button[data-is-disabled="true"] { + opacity: 0.5; + pointer-events: none; +}`; From 8317533ea978b00b86f9e633c108a614f4f24164 Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Wed, 21 Jan 2026 13:49:16 +0100 Subject: [PATCH 06/14] fix: improve useMediaQuery examples Signed-off-by: Antony Bouyon --- .../app/hooks/use-media-query/components.tsx | 15 +++++------ .../src/app/hooks/use-media-query/example.css | 25 +++++++++++++++++++ .../src/app/hooks/use-media-query/snippets.ts | 23 +++++++++-------- docs-ui/src/app/hooks/use-styles/page.mdx | 7 ++++-- 4 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 docs-ui/src/app/hooks/use-media-query/example.css diff --git a/docs-ui/src/app/hooks/use-media-query/components.tsx b/docs-ui/src/app/hooks/use-media-query/components.tsx index 0d8edd944b..1ebcd64dfa 100644 --- a/docs-ui/src/app/hooks/use-media-query/components.tsx +++ b/docs-ui/src/app/hooks/use-media-query/components.tsx @@ -1,13 +1,12 @@ 'use client'; import { useMediaQuery } from '@backstage/ui/src/hooks/useMediaQuery'; +import './example.css'; export function UseMediaQueryThemeExample() { const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); return ( -
- {isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'} -
+

{isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'}

); } @@ -17,11 +16,11 @@ export function UseMediaQueryResponsiveExample() { const isDesktop = useMediaQuery('(min-width: 1025px)'); return ( -
+

{isMobile && ''} {isTablet && ''} {isDesktop && ''} -

+

); } @@ -31,12 +30,14 @@ export function UseMediaQueryPreferencesExample() { ); return ( -
Content
+

+ Content +

); } export function UseMediaQueryOrientationExample() { const isPortrait = useMediaQuery('(orientation: portrait)'); - return
{isPortrait ? 'Portrait mode' : 'Landscape mode'}
; + return

{isPortrait ? 'Portrait mode' : 'Landscape mode'}

; } diff --git a/docs-ui/src/app/hooks/use-media-query/example.css b/docs-ui/src/app/hooks/use-media-query/example.css new file mode 100644 index 0000000000..5fb132f9f4 --- /dev/null +++ b/docs-ui/src/app/hooks/use-media-query/example.css @@ -0,0 +1,25 @@ +@keyframes borderSlide { + 0%, + 100% { + transform: translateX(-90%); + } + 50% { + transform: translateX(190%); + } +} + +.animated-border { + position: relative; + overflow: hidden; +} + +.animated-border::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 50%; + height: 2px; + background-color: currentColor; + animation: borderSlide 5s ease-in-out infinite; +} diff --git a/docs-ui/src/app/hooks/use-media-query/snippets.ts b/docs-ui/src/app/hooks/use-media-query/snippets.ts index 78423ef516..157da945f0 100644 --- a/docs-ui/src/app/hooks/use-media-query/snippets.ts +++ b/docs-ui/src/app/hooks/use-media-query/snippets.ts @@ -4,9 +4,12 @@ function MyComponent() { const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); return ( -
- {isDarkMode ? 'Dark mode enabled' : 'Light mode enabled'} -
+

+ {isDarkMode + ? 'User prefers Dark mode' + : 'User prefers Light mode' + } +

); }`; @@ -18,11 +21,11 @@ function ResponsiveLayout() { const isDesktop = useMediaQuery('(min-width: 1025px)'); return ( -
+

{isMobile && } {isTablet && } {isDesktop && } -

+

); }`; @@ -32,11 +35,9 @@ function AccessibleComponent() { const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); return ( -
+

Content -

+

); }`; @@ -46,8 +47,8 @@ function OrientationAware() { const isPortrait = useMediaQuery('(orientation: portrait)'); return ( -
+

{isPortrait ? 'Portrait mode' : 'Landscape mode'} -

+

); }`; diff --git a/docs-ui/src/app/hooks/use-styles/page.mdx b/docs-ui/src/app/hooks/use-styles/page.mdx index ca235c4778..080a283dcc 100644 --- a/docs-ui/src/app/hooks/use-styles/page.mdx +++ b/docs-ui/src/app/hooks/use-styles/page.mdx @@ -33,6 +33,7 @@ The `useStyles` hook is the core styling utility that processes component defini ## Parameters + ## Return Value @@ -47,7 +48,10 @@ A component definition describes how a component should be styled: - + ### Data Attributes @@ -55,5 +59,4 @@ Data attributes enable state-based styling through CSS selectors. The hook autom - From d682eef1173332a8f80987cdcc5b2abf09ef55bf Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Wed, 21 Jan 2026 14:15:17 +0100 Subject: [PATCH 07/14] chore: remove undocumented hooks from utils/data.ts Signed-off-by: Antony Bouyon --- docs-ui/src/utils/data.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index bb1ea40dfb..a750f75bd6 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -144,10 +144,6 @@ export const hooks: Page[] = [ title: 'useBreakpoint', slug: 'use-breakpoint', }, - { - title: 'useIsomorphicLayoutEffect', - slug: 'use-isomorphic-layout-effect', - }, { title: 'useMediaQuery', slug: 'use-media-query', @@ -156,8 +152,4 @@ export const hooks: Page[] = [ title: 'useStyles', slug: 'use-styles', }, - { - title: 'useSurface', - slug: 'use-surface', - }, ]; From 50cdc709206534e73ef966cdebb87e7b94de6bf5 Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Thu, 22 Jan 2026 10:10:33 +0100 Subject: [PATCH 08/14] fix: wrong logic operator Signed-off-by: Antony Bouyon --- docs-ui/src/app/hooks/use-styles/snippets.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-ui/src/app/hooks/use-styles/snippets.ts b/docs-ui/src/app/hooks/use-styles/snippets.ts index 55cf7ddee7..60c5a66a9b 100644 --- a/docs-ui/src/app/hooks/use-styles/snippets.ts +++ b/docs-ui/src/app/hooks/use-styles/snippets.ts @@ -54,7 +54,7 @@ export function Button({ {...dataAttributes} {...cleanedProps} > - {props.icon ?? {props.icon}} + {props.icon && {props.icon}} {props.children} ); From 6325d6b307d889d7884858ed13c1b0fea81117ce Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Tue, 3 Feb 2026 12:49:22 +0100 Subject: [PATCH 09/14] feat: remove internal hooks & add performance disclaimer to useBreakpoint Signed-off-by: Antony Bouyon --- docs-ui/src/app/hooks/page.mdx | 12 +- docs-ui/src/app/hooks/use-breakpoint/page.mdx | 8 +- .../app/hooks/use-media-query/components.tsx | 43 ------- .../src/app/hooks/use-media-query/example.css | 25 ----- .../src/app/hooks/use-media-query/page.mdx | 95 ---------------- .../hooks/use-media-query/props-definition.ts | 36 ------ .../src/app/hooks/use-media-query/snippets.ts | 54 --------- docs-ui/src/app/hooks/use-styles/page.mdx | 62 ----------- .../app/hooks/use-styles/props-definition.ts | 58 ---------- docs-ui/src/app/hooks/use-styles/snippets.ts | 105 ------------------ docs-ui/src/utils/data.ts | 8 -- 11 files changed, 8 insertions(+), 498 deletions(-) delete mode 100644 docs-ui/src/app/hooks/use-media-query/components.tsx delete mode 100644 docs-ui/src/app/hooks/use-media-query/example.css delete mode 100644 docs-ui/src/app/hooks/use-media-query/page.mdx delete mode 100644 docs-ui/src/app/hooks/use-media-query/props-definition.ts delete mode 100644 docs-ui/src/app/hooks/use-media-query/snippets.ts delete mode 100644 docs-ui/src/app/hooks/use-styles/page.mdx delete mode 100644 docs-ui/src/app/hooks/use-styles/props-definition.ts delete mode 100644 docs-ui/src/app/hooks/use-styles/snippets.ts diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx index 75bee70743..f4b77c3a0c 100644 --- a/docs-ui/src/app/hooks/page.mdx +++ b/docs-ui/src/app/hooks/page.mdx @@ -8,17 +8,7 @@ Backstage UI custom hooks provide easy access to design system features, style m - - diff --git a/docs-ui/src/app/hooks/use-breakpoint/page.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx index 4ee410fc74..a5c1a64e92 100644 --- a/docs-ui/src/app/hooks/use-breakpoint/page.mdx +++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx @@ -3,6 +3,7 @@ import { CodeBlock } from '@/components/CodeBlock'; import { PageTitle } from '@/components/PageTitle'; import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; +import { Banner } from '@/components/Banner'; import { UseBreakpointExample } from './components'; import { useBreakpointReturnDefs } from './props-definition'; import { useBreakpointExampleSnippet } from './snippets'; @@ -14,6 +15,11 @@ import { useBreakpointExampleSnippet } from './snippets'; ## Usage + + The `useBreakpoint` hook returns the active breakpoint and two functions, `up` and `down`, to check if the viewport is above, or below a given breakpoint, letting you adjust your UI responsively. - + diff --git a/docs-ui/src/app/hooks/use-media-query/components.tsx b/docs-ui/src/app/hooks/use-media-query/components.tsx deleted file mode 100644 index 1ebcd64dfa..0000000000 --- a/docs-ui/src/app/hooks/use-media-query/components.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; -import { useMediaQuery } from '@backstage/ui/src/hooks/useMediaQuery'; -import './example.css'; - -export function UseMediaQueryThemeExample() { - const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - - return ( -

{isDarkMode ? 'User prefers Dark mode' : 'User prefers Light mode'}

- ); -} - -export function UseMediaQueryResponsiveExample() { - const isMobile = useMediaQuery('(max-width: 768px)'); - const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)'); - const isDesktop = useMediaQuery('(min-width: 1025px)'); - - return ( -

- {isMobile && ''} - {isTablet && ''} - {isDesktop && ''} -

- ); -} - -export function UseMediaQueryPreferencesExample() { - const prefersReducedMotion = useMediaQuery( - '(prefers-reduced-motion: reduce)', - ); - - return ( -

- Content -

- ); -} - -export function UseMediaQueryOrientationExample() { - const isPortrait = useMediaQuery('(orientation: portrait)'); - - return

{isPortrait ? 'Portrait mode' : 'Landscape mode'}

; -} diff --git a/docs-ui/src/app/hooks/use-media-query/example.css b/docs-ui/src/app/hooks/use-media-query/example.css deleted file mode 100644 index 5fb132f9f4..0000000000 --- a/docs-ui/src/app/hooks/use-media-query/example.css +++ /dev/null @@ -1,25 +0,0 @@ -@keyframes borderSlide { - 0%, - 100% { - transform: translateX(-90%); - } - 50% { - transform: translateX(190%); - } -} - -.animated-border { - position: relative; - overflow: hidden; -} - -.animated-border::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 50%; - height: 2px; - background-color: currentColor; - animation: borderSlide 5s ease-in-out infinite; -} diff --git a/docs-ui/src/app/hooks/use-media-query/page.mdx b/docs-ui/src/app/hooks/use-media-query/page.mdx deleted file mode 100644 index bd01152581..0000000000 --- a/docs-ui/src/app/hooks/use-media-query/page.mdx +++ /dev/null @@ -1,95 +0,0 @@ -import { ChangelogComponent } from '@/components/ChangelogComponent'; -import { CodeBlock } from '@/components/CodeBlock'; -import { PageTitle } from '@/components/PageTitle'; -import { PropsTable } from '@/components/PropsTable'; -import { Snippet } from '@/components/Snippet'; -import { Banner } from '@/components/Banner'; -import { - UseMediaQueryOrientationExample, - UseMediaQueryPreferencesExample, - UseMediaQueryResponsiveExample, - UseMediaQueryThemeExample, -} from './components'; -import { - useMediaQueryParamDefs, - useMediaQueryReturnDefs, -} from './props-definition'; -import { - useMediaQueryOrientationSnippet, - useMediaQueryPreferencesSnippet, - useMediaQueryResponsiveSnippet, - useMediaQueryUsageSnippet, -} from './snippets'; - - - - - -## Usage - -The `useMediaQuery` hook allows you to evaluate CSS media queries in your components, enabling responsive design and behavior. - -} - code={useMediaQueryUsageSnippet} -/> - -## Parameters - - - -## Return Value - - -## Examples - -### Responsive Layouts - -Adapt layout based on different screen sizes. - -} - code={useMediaQueryResponsiveSnippet} -/> - -### User Preferences - -Respect user system preferences. - -} - code={useMediaQueryPreferencesSnippet} -/> - -### Device Orientation - -Detect screen orientation. - -} - code={useMediaQueryOrientationSnippet} -/> - - diff --git a/docs-ui/src/app/hooks/use-media-query/props-definition.ts b/docs-ui/src/app/hooks/use-media-query/props-definition.ts deleted file mode 100644 index 315696e74a..0000000000 --- a/docs-ui/src/app/hooks/use-media-query/props-definition.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { type PropDef } from '@/utils/propDefs'; - -export const useMediaQueryParamDefs: Record = { - query: { - type: 'string', - description: 'The CSS media query to evaluate', - }, - options: { - type: 'complex', - complexType: { - name: 'UseMediaQueryOptions', - properties: { - defaultValue: { - type: 'boolean', - required: false, - description: - 'Default value to use when rendering on the server, defaults to false', - }, - initializeWithValue: { - type: 'boolean', - required: false, - description: - 'Whether to initialize with the current value of the media query, or use the default value initially', - }, - }, - }, - default: 'defaultValue: false\ninitializeWithValue: true', - }, -}; - -export const useMediaQueryReturnDefs: Record = { - matches: { - type: 'boolean', - description: 'True if the media query currently matches', - }, -}; diff --git a/docs-ui/src/app/hooks/use-media-query/snippets.ts b/docs-ui/src/app/hooks/use-media-query/snippets.ts deleted file mode 100644 index 157da945f0..0000000000 --- a/docs-ui/src/app/hooks/use-media-query/snippets.ts +++ /dev/null @@ -1,54 +0,0 @@ -export const useMediaQueryUsageSnippet = `import { useMediaQuery } from '@backstage/ui'; - -function MyComponent() { - const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); - - return ( -

- {isDarkMode - ? 'User prefers Dark mode' - : 'User prefers Light mode' - } -

- ); -}`; - -export const useMediaQueryResponsiveSnippet = `import { useMediaQuery } from '@backstage/ui'; - -function ResponsiveLayout() { - const isMobile = useMediaQuery('(max-width: 768px)'); - const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)'); - const isDesktop = useMediaQuery('(min-width: 1025px)'); - - return ( -

- {isMobile && } - {isTablet && } - {isDesktop && } -

- ); -}`; - -export const useMediaQueryPreferencesSnippet = `import { useMediaQuery } from '@backstage/ui'; - -function AccessibleComponent() { - const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)'); - - return ( -

- Content -

- ); -}`; - -export const useMediaQueryOrientationSnippet = `import { useMediaQuery } from '@backstage/ui'; - -function OrientationAware() { - const isPortrait = useMediaQuery('(orientation: portrait)'); - - return ( -

- {isPortrait ? 'Portrait mode' : 'Landscape mode'} -

- ); -}`; diff --git a/docs-ui/src/app/hooks/use-styles/page.mdx b/docs-ui/src/app/hooks/use-styles/page.mdx deleted file mode 100644 index 080a283dcc..0000000000 --- a/docs-ui/src/app/hooks/use-styles/page.mdx +++ /dev/null @@ -1,62 +0,0 @@ -import { ChangelogComponent } from '@/components/ChangelogComponent'; -import { CodeBlock } from '@/components/CodeBlock'; -import { Banner } from '@/components/Banner'; -import { PageTitle } from '@/components/PageTitle'; -import { PropsTable } from '@/components/PropsTable'; -import { - useStylesUsageSnippet, - useStylesDefsSnippet, - useStylesAttributesSnippet, -} from './snippets'; -import { useStylesPropsDefs, useStylesReturnDefs } from './props-definition'; - - - - - -## Usage - -The `useStyles` hook is the core styling utility that processes component definitions and props to generate: - -- **Class names** -- **Data attributes** for state-based styling (e.g., `data-disabled`, `data-variant`) -- **Utility classes** from utility props like `padding`, `margin`, `display` (with responsive support) -- **CSS custom properties** for custom values not in predefined utility lists -- **Cleaned props** with utility props removed (safe to spread onto DOM elements) - - - -## Parameters - - - -## Return Value - - - -## How It Works - -### Component Definition - -A component definition describes how a component should be styled: - - - - - -### Data Attributes - -Data attributes enable state-based styling through CSS selectors. The hook automatically adds `data-*` to the attributes based on the defined props: - - - - diff --git a/docs-ui/src/app/hooks/use-styles/props-definition.ts b/docs-ui/src/app/hooks/use-styles/props-definition.ts deleted file mode 100644 index 0c8a2a1728..0000000000 --- a/docs-ui/src/app/hooks/use-styles/props-definition.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { type PropDef } from '@/utils/propDefs'; - -export const useStylesPropsDefs: Record = { - componentDefinition: { - type: 'complex', - complexType: { - name: 'ComponentDefinition', - properties: { - classNames: { - type: 'Record', - required: true, - }, - dataAttributes: { - type: 'Record', - required: false, - }, - utilityProps: { - type: 'string[]', - required: false, - }, - }, - }, - }, - props: { - type: 'enum', - description: 'All component props', - values: ['{ [key: string]: any; }'], - }, -}; - -export const useStylesReturnDefs: Record = { - classNames: { - type: 'enum', - values: ['Record'], - description: - "The component's class names mapped to their style definitions", - }, - dataAttributes: { - type: 'enum', - values: ['Record'], - description: - 'Data attributes generated from the component definition and props', - }, - utilityClasses: { - type: 'string', - description: 'Combined utility classes based on utility props', - }, - style: { - type: 'enum', - values: ['React.CSSProperties'], - description: 'The combined style object for the component', - }, - cleanedProps: { - type: 'enum', - values: ['Record'], - description: 'The original props with utility props removed', - }, -}; diff --git a/docs-ui/src/app/hooks/use-styles/snippets.ts b/docs-ui/src/app/hooks/use-styles/snippets.ts deleted file mode 100644 index 60c5a66a9b..0000000000 --- a/docs-ui/src/app/hooks/use-styles/snippets.ts +++ /dev/null @@ -1,105 +0,0 @@ -export const useStylesUsageSnippet = `import { useStyles } from '@backstage/ui'; -import type { - ComponentDefinition, - SpaceProps, - UtilityProps, -} from '@backstage/ui'; - -// Define your component's styling configuration -const buttonDefinition: ComponentDefinition = { - classNames: { - root: 'bui-button', - icon: 'bui-button-icon', - }, - dataAttributes: { - variant: ['primary', 'secondary', 'ghost'] as const, - size: ['small', 'medium', 'large'] as const, - 'is-disabled': [true, false] as const, - }, - utilityProps: ['gap', 'mb', 'mt', 'ml', 'mr'], -} as const satisfies ComponentDefinition; - -// type the component props -interface ButtonProps { - variant?: 'primary' | 'secondary' | 'ghost'; - size?: 'small' | 'medium' | 'large'; - isDisabled?: boolean; - gap?: UtilityProps['gap']; - mb?: SpaceProps['mb']; - mt?: SpaceProps['mt']; - ml?: SpaceProps['ml']; - mr?: SpaceProps['mr']; - icon?: React.ReactElement; - children?: React.ReactNode; -} - -// Use the useStyles hook in your component -export function Button({ - variant = 'primary', - size = 'medium', - isDisabled, - ...props -}: Readonly) { - const { classNames, dataAttributes, utilityClasses, cleanedProps } = - useStyles(buttonDefinition, { - variant, - size, - 'is-disabled': isDisabled, - ...props, - }); - - return ( - - ); -}`; - -export const useStylesDefsSnippet = `const componentDefinition = { - // CSS class names for component parts - classNames: { - root: 'bui-button', - icon: 'bui-button-icon', - }, - - // Props that become data attributes for CSS targeting - // data-attribute must be kebab-case to respect HTML standards - // And casing matters here, lowercase only: - // 'variant' not 'Variant', 'is-disabled' not 'isDisabled'.. - dataAttributes: { - variant: ['primary', 'secondary'] as const, - size: ['small', 'large'] as const, - 'is-disabled': [true, false] as const, - }, - - // Props that become utility classes (spacing, layout, etc.) - utilityProps: ['m', 'mb', 'mt', 'ml', 'mr'], -};`; - -export const useStylesAttributesSnippet = `/* Component CSS can use data attributes for variants */ -.bui-button { - padding: 0.5rem 1rem; - border-radius: 0.25rem; -} - -/* Variant styles */ -.bui-button[data-variant="primary"] { - background: blue; - color: white; -} - -.bui-button[data-variant="secondary"] { - background: gray; - color: black; -} - -/* State styles */ -.bui-button[data-is-disabled="true"] { - opacity: 0.5; - pointer-events: none; -}`; diff --git a/docs-ui/src/utils/data.ts b/docs-ui/src/utils/data.ts index a750f75bd6..a1a6672135 100644 --- a/docs-ui/src/utils/data.ts +++ b/docs-ui/src/utils/data.ts @@ -144,12 +144,4 @@ export const hooks: Page[] = [ title: 'useBreakpoint', slug: 'use-breakpoint', }, - { - title: 'useMediaQuery', - slug: 'use-media-query', - }, - { - title: 'useStyles', - slug: 'use-styles', - }, ]; From 5b84e40b1bdcd0774fab92d8767010e5530114fc Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Tue, 3 Feb 2026 13:04:02 +0100 Subject: [PATCH 10/14] feat: add support for hooks in changelogs Signed-off-by: Antony Bouyon --- .../src/components/ChangelogComponent/index.tsx | 14 ++++++++++---- docs-ui/src/utils/types.ts | 14 +++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/docs-ui/src/components/ChangelogComponent/index.tsx b/docs-ui/src/components/ChangelogComponent/index.tsx index 92068d04f8..1ba5c45a0b 100644 --- a/docs-ui/src/components/ChangelogComponent/index.tsx +++ b/docs-ui/src/components/ChangelogComponent/index.tsx @@ -1,16 +1,22 @@ import { changelog } from '@/utils/changelog'; import { MDXRemote } from 'next-mdx-remote-client/rsc'; import { formattedMDXComponents } from '@/mdx-components'; -import type { Component } from '@/utils/changelog'; +import type { Component, Hook } from '@/utils/changelog'; import { Badge, BreakingBadge, generateChangelogMarkdown, } from '../Changelog/utils'; -export const ChangelogComponent = ({ component }: { component: Component }) => { - const componentChangelog = changelog.filter(c => - c.components.includes(component), +export const ChangelogComponent = ({ + component, + hook, +}: { + component: Component; + hook: Hook; +}) => { + const componentChangelog = changelog.filter( + c => c.components?.includes(component) || c.hooks?.includes(hook), ); const content = `## Changelog diff --git a/docs-ui/src/utils/types.ts b/docs-ui/src/utils/types.ts index 21acc950a0..4fa0d8b1c3 100644 --- a/docs-ui/src/utils/types.ts +++ b/docs-ui/src/utils/types.ts @@ -35,14 +35,22 @@ export type Component = | 'tooltip' | 'visually-hidden'; +export type Hook = 'use-breakpoint'; + export type Version = `${number}.${number}.${number}`; -export interface ChangelogProps { - components: Component[]; +type AtLeastOne = K extends string + ? Pick & Partial> + : never; + +export type ChangelogProps = { description: string; version: Version; prs: string[]; breaking?: boolean; commitSha?: string; migration?: string; -} +} & AtLeastOne<{ + components: Component[]; + hooks: Hook[]; +}>; From 1cd738e636fce041aef19d21cb0e8b3fc0e73500 Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Tue, 3 Feb 2026 13:34:21 +0100 Subject: [PATCH 11/14] fix: changelog expects component and/or hook Signed-off-by: Antony Bouyon --- docs-ui/src/components/ChangelogComponent/index.tsx | 12 +++++++----- docs-ui/src/utils/types.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs-ui/src/components/ChangelogComponent/index.tsx b/docs-ui/src/components/ChangelogComponent/index.tsx index 1ba5c45a0b..cca75a9515 100644 --- a/docs-ui/src/components/ChangelogComponent/index.tsx +++ b/docs-ui/src/components/ChangelogComponent/index.tsx @@ -1,20 +1,22 @@ import { changelog } from '@/utils/changelog'; import { MDXRemote } from 'next-mdx-remote-client/rsc'; import { formattedMDXComponents } from '@/mdx-components'; -import type { Component, Hook } from '@/utils/changelog'; +import type { AtLeastOne, Component, Hook } from '@/utils/changelog'; import { Badge, BreakingBadge, generateChangelogMarkdown, } from '../Changelog/utils'; +type ChangelogComponentProps = AtLeastOne<{ + component: Component; + hook: Hook; +}>; + export const ChangelogComponent = ({ component, hook, -}: { - component: Component; - hook: Hook; -}) => { +}: Readonly) => { const componentChangelog = changelog.filter( c => c.components?.includes(component) || c.hooks?.includes(hook), ); diff --git a/docs-ui/src/utils/types.ts b/docs-ui/src/utils/types.ts index 4fa0d8b1c3..512609d1ea 100644 --- a/docs-ui/src/utils/types.ts +++ b/docs-ui/src/utils/types.ts @@ -39,7 +39,7 @@ export type Hook = 'use-breakpoint'; export type Version = `${number}.${number}.${number}`; -type AtLeastOne = K extends string +export type AtLeastOne = K extends string ? Pick & Partial> : never; From 15da6ee3597cbac44e5601edc8714796f315ca23 Mon Sep 17 00:00:00 2001 From: Antony Date: Thu, 12 Feb 2026 11:59:56 +0100 Subject: [PATCH 12/14] Update docs-ui/src/app/hooks/page.mdx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Antony --- docs-ui/src/app/hooks/page.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx index f4b77c3a0c..e6fbde7b1a 100644 --- a/docs-ui/src/app/hooks/page.mdx +++ b/docs-ui/src/app/hooks/page.mdx @@ -1,4 +1,3 @@ -import { CodeBlock } from '@/components/CodeBlock'; import { ComponentCard, ComponentCards } from '@/components/ComponentCards'; # Hooks From 0022eb1e0ab2e1a21172282da490be4fb83ece26 Mon Sep 17 00:00:00 2001 From: Antony Date: Thu, 12 Feb 2026 12:00:06 +0100 Subject: [PATCH 13/14] Update docs-ui/src/app/hooks/use-breakpoint/page.mdx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Antony --- docs-ui/src/app/hooks/use-breakpoint/page.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs-ui/src/app/hooks/use-breakpoint/page.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx index a5c1a64e92..5a077a3d73 100644 --- a/docs-ui/src/app/hooks/use-breakpoint/page.mdx +++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx @@ -1,5 +1,4 @@ import { ChangelogComponent } from '@/components/ChangelogComponent'; -import { CodeBlock } from '@/components/CodeBlock'; import { PageTitle } from '@/components/PageTitle'; import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; From 175176729d4a141f07814d4e942bff0f66556c89 Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Thu, 12 Feb 2026 17:24:29 +0100 Subject: [PATCH 14/14] fix: hopefully fix everything that was broken/changed Signed-off-by: Antony Bouyon --- docs-ui/src/app/hooks/page.mdx | 10 ++---- .../app/hooks/use-breakpoint/components.tsx | 11 ------ docs-ui/src/app/hooks/use-breakpoint/page.mdx | 2 +- .../components/HookGrid/HookGrid.module.css | 29 +++++++++++++++ docs-ui/src/components/HookGrid/HookGrid.tsx | 21 +++++++++++ docs-ui/src/components/HookGrid/index.ts | 1 + .../src/components/Navigation/Navigation.tsx | 36 ++++++++++++------- 7 files changed, 77 insertions(+), 33 deletions(-) create mode 100644 docs-ui/src/components/HookGrid/HookGrid.module.css create mode 100644 docs-ui/src/components/HookGrid/HookGrid.tsx create mode 100644 docs-ui/src/components/HookGrid/index.ts diff --git a/docs-ui/src/app/hooks/page.mdx b/docs-ui/src/app/hooks/page.mdx index e6fbde7b1a..ea73f07ce3 100644 --- a/docs-ui/src/app/hooks/page.mdx +++ b/docs-ui/src/app/hooks/page.mdx @@ -1,13 +1,7 @@ -import { ComponentCard, ComponentCards } from '@/components/ComponentCards'; +import { HookGrid } from '@/components/HookGrid'; # Hooks Backstage UI custom hooks provide easy access to design system features, style management, and responsive interface creation. - - - + diff --git a/docs-ui/src/app/hooks/use-breakpoint/components.tsx b/docs-ui/src/app/hooks/use-breakpoint/components.tsx index d4d21861bb..c58010c66f 100644 --- a/docs-ui/src/app/hooks/use-breakpoint/components.tsx +++ b/docs-ui/src/app/hooks/use-breakpoint/components.tsx @@ -1,20 +1,9 @@ 'use client'; import { useBreakpoint } from '@backstage/ui'; -import { useEffect, useState } from 'react'; export function UseBreakpointExample() { const { breakpoint, up, down } = useBreakpoint(); - const [isMounted, setIsMounted] = useState(false); - - // prevent hydration mismatch by rendering only on the client - useEffect(() => { - setIsMounted(true); - }, []); - - if (!isMounted) { - return null; - } return (
diff --git a/docs-ui/src/app/hooks/use-breakpoint/page.mdx b/docs-ui/src/app/hooks/use-breakpoint/page.mdx index 5a077a3d73..f7f3378a41 100644 --- a/docs-ui/src/app/hooks/use-breakpoint/page.mdx +++ b/docs-ui/src/app/hooks/use-breakpoint/page.mdx @@ -3,9 +3,9 @@ import { PageTitle } from '@/components/PageTitle'; import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; import { Banner } from '@/components/Banner'; -import { UseBreakpointExample } from './components'; import { useBreakpointReturnDefs } from './props-definition'; import { useBreakpointExampleSnippet } from './snippets'; +import { UseBreakpointExample } from './components'; { + return ( +
+ {hooks.map(item => ( + + {item.title} + + ))} +
+ ); +}; diff --git a/docs-ui/src/components/HookGrid/index.ts b/docs-ui/src/components/HookGrid/index.ts new file mode 100644 index 0000000000..934f73aacf --- /dev/null +++ b/docs-ui/src/components/HookGrid/index.ts @@ -0,0 +1 @@ +export * from './HookGrid'; diff --git a/docs-ui/src/components/Navigation/Navigation.tsx b/docs-ui/src/components/Navigation/Navigation.tsx index f4fc51993f..ac62c03d7a 100644 --- a/docs-ui/src/components/Navigation/Navigation.tsx +++ b/docs-ui/src/components/Navigation/Navigation.tsx @@ -18,19 +18,6 @@ interface NavigationProps { onLinkClick?: () => void; } -const data = [ - { - title: 'Components', - content: components, - url: '/components', - }, - { - title: 'Hooks', - content: hooks, - url: '/hooks', - }, -]; - export const Navigation = ({ onLinkClick }: NavigationProps) => { const pathname = usePathname(); @@ -105,6 +92,29 @@ export const Navigation = ({ onLinkClick }: NavigationProps) => { ); })} +
+ Hooks +
+ {hooks.map(item => { + const isActive = pathname === `/hooks/${item.slug}`; + return ( + +
{item.title}
+
+ {item.status === 'alpha' && 'Alpha'} + {item.status === 'beta' && 'Beta'} + {item.status === 'inProgress' && 'In Progress'} + {item.status === 'stable' && 'Stable'} + {item.status === 'deprecated' && 'Deprecated'} +
+ + ); + })} ); };