From 1cf2972f4598870d27f30787aec6fcaf3659fad0 Mon Sep 17 00:00:00 2001 From: Antony Bouyon Date: Wed, 14 Jan 2026 18:30:10 +0100 Subject: [PATCH] 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', + }, +];