From 233936366b8d294181f63875f6b59fc2734cdd62 Mon Sep 17 00:00:00 2001 From: Thomas Cardonne Date: Tue, 29 Apr 2025 18:31:00 +0200 Subject: [PATCH 01/27] feat(catalog)!: remove catalog.useUrlReadersSearch and always use search Signed-off-by: Thomas Cardonne --- .changeset/cyan-islands-swim.md | 14 ++++++ app-config.yaml | 1 - .../templates/default-app/app-config.yaml.hbs | 3 -- plugins/catalog-backend/config.d.ts | 14 ------ plugins/catalog-backend/package.json | 1 - plugins/catalog-backend/report.api.md | 6 +-- .../src/processors/UrlReaderProcessor.test.ts | 38 +-------------- .../src/processors/UrlReaderProcessor.ts | 47 +++---------------- .../src/service/CatalogBuilder.ts | 2 +- yarn.lock | 1 - 10 files changed, 24 insertions(+), 103 deletions(-) create mode 100644 .changeset/cyan-islands-swim.md diff --git a/.changeset/cyan-islands-swim.md b/.changeset/cyan-islands-swim.md new file mode 100644 index 0000000000..396cc6fc68 --- /dev/null +++ b/.changeset/cyan-islands-swim.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-catalog-backend': major +'@backstage/create-app': patch +--- + +**BREAKING:** The experimental `catalog.useUrlReadersSearch` configuration flag (introduced in v1.36) has been removed. + +The `UrlReaderProcessor` now always uses the `search` method of `UrlReaders`. Built-in `UrlReaderService` implementations have been updated accordingly. +If you use custom `UrlReaderService` implementations, you need to adapt their `search` method to correctly handle both specific URLs and potential +search patterns (see changes on built-in readers [in the original PR](https://github.com/backstage/backstage/pull/28379/files#diff-68b0452f173ee54bdd40f7b5e047a9cb8bb59200425622c212c217b76dac1d1b)). + +Previous behavior was to call the `search` method only if the parsed Git URL's filename contained a wildcard and use `readUrl` otherwise. Each `UrlReaderService` must implement this logic in the `search` method instead. + +This allows each `UrlReaderService` implementation to check whether it's a search URL (that contains a wildcard pattern) or not using logic that is specific to each provider. diff --git a/app-config.yaml b/app-config.yaml index 89e417fad3..64c2fa5c4c 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -244,7 +244,6 @@ integrations: secretAccessKey: ${AWS_SECRET_ACCESS_KEY} catalog: - useUrlReadersSearch: true import: entityFilename: catalog-info.yaml pullRequestBranchName: backstage-integration diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index fc6918acea..ca52ec530c 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -103,9 +103,6 @@ catalog: # target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml # rules: # - allow: [User, Group] - # Experimental: Always use the search method in UrlReaderProcessor. - # New adopters are encouraged to enable it as this behavior will be the default in a future release. - useUrlReadersSearch: true kubernetes: # see https://backstage.io/docs/features/kubernetes/configuration for kubernetes configuration options diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 7e68c4bea9..3ba6047ada 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -218,19 +218,5 @@ export interface Config { * housing catalog-info files. */ processingInterval?: HumanDuration | false; - - /** - * Defines if the UrlReaderProcessor should always call the search method of the - * different UrlReaders. - * - * If set to false, the UrlReaderProcessor will use the legacy behavior that tries to - * parse a Git URL and calls search if there's wildcard patterns and readUrl otherwise. - * - * If set to true, the UrlReaderProcessor always call the search method and lets each UrlReader - * determine if it's a search pattern or not. - * - * This flag is temporary and will be enabled by default in future releases. - */ - useUrlReadersSearch?: boolean; }; } diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 8f2f8afe94..1b68e7d58d 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -99,7 +99,6 @@ "@backstage/cli": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/repo-tools": "workspace:^", - "@backstage/test-utils": "workspace:^", "@types/core-js": "^2.5.4", "@types/express": "^4.17.6", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-backend/report.api.md b/plugins/catalog-backend/report.api.md index 9bb94b2dd5..6e5ca956bc 100644 --- a/plugins/catalog-backend/report.api.md +++ b/plugins/catalog-backend/report.api.md @@ -135,11 +135,7 @@ export function transformLegacyPolicyToProcessor( // @public (undocumented) export class UrlReaderProcessor implements CatalogProcessor { - constructor(options: { - reader: UrlReaderService; - logger: LoggerService; - config?: Config; - }); + constructor(options: { reader: UrlReaderService; logger: LoggerService }); // (undocumented) getProcessorName(): string; // (undocumented) diff --git a/plugins/catalog-backend/src/processors/UrlReaderProcessor.test.ts b/plugins/catalog-backend/src/processors/UrlReaderProcessor.test.ts index 2d7268eb33..0b17ecb19f 100644 --- a/plugins/catalog-backend/src/processors/UrlReaderProcessor.test.ts +++ b/plugins/catalog-backend/src/processors/UrlReaderProcessor.test.ts @@ -30,8 +30,6 @@ import { import { defaultEntityDataParser } from '../util/parse'; import { UrlReaderProcessor } from './UrlReaderProcessor'; import { UrlReaders } from '@backstage/backend-defaults/urlReader'; -import { UrlReaderService } from '@backstage/backend-plugin-api'; -import { mockApis } from '@backstage/test-utils'; describe('UrlReaderProcessor', () => { const mockApiOrigin = 'http://localhost'; @@ -191,11 +189,11 @@ describe('UrlReaderProcessor', () => { expect(generated.location).toBe(spec); expect(generated.error.name).toBe('NotFoundError'); expect(generated.error.message).toBe( - `Unable to read url, NotFoundError: could not read ${mockApiOrigin}/component-notfound.yaml, 404 Not Found`, + `Unable to read url, no matching files found for ${mockApiOrigin}/component-notfound.yaml`, ); }); - it('uses search when there are globs', async () => { + it("uses reader' search method", async () => { const logger = mockServices.logger.mock(); const reader = mockServices.urlReader.mock({ @@ -206,38 +204,6 @@ describe('UrlReaderProcessor', () => { const emit = jest.fn(); - await processor.readLocation( - { type: 'url', target: 'https://github.com/a/b/blob/x/**/b.yaml' }, - false, - emit, - defaultEntityDataParser, - mockCache, - ); - - expect(reader.search).toHaveBeenCalledTimes(1); - }); - - it('uses search when catalog.useUrlReadersSearch flag is set to true', async () => { - const logger = mockServices.logger.mock(); - - const reader: jest.Mocked = { - readUrl: jest.fn(), - readTree: jest.fn(), - search: jest.fn().mockImplementation(async () => []), - }; - - const config = mockApis.config({ - data: { - catalog: { - useUrlReadersSearch: true, - }, - }, - }); - - const processor = new UrlReaderProcessor({ reader, logger, config }); - - const emit = jest.fn(); - await processor.readLocation( { type: 'url', target: 'https://github.com/a/b/blob/x/b.yaml' }, false, diff --git a/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts b/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts index bc383512be..4440a81bae 100644 --- a/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts +++ b/plugins/catalog-backend/src/processors/UrlReaderProcessor.ts @@ -18,7 +18,6 @@ import { Entity } from '@backstage/catalog-model'; import { assertError } from '@backstage/errors'; import limiterFactory, { Limit } from 'p-limit'; import { LocationSpec } from '@backstage/plugin-catalog-common'; -import parseGitUrl from 'git-url-parse'; import { CatalogProcessor, CatalogProcessorCache, @@ -29,7 +28,6 @@ import { processingResult, } from '@backstage/plugin-catalog-node'; import { LoggerService, UrlReaderService } from '@backstage/backend-plugin-api'; -import { Config } from '@backstage/config'; const CACHE_KEY = 'v1'; @@ -48,25 +46,14 @@ export class UrlReaderProcessor implements CatalogProcessor { // This limiter is used for only consuming a limited number of read streams // concurrently. #limiter: Limit; - #useUrlReadersSearch: boolean; constructor( private readonly options: { reader: UrlReaderService; logger: LoggerService; - config?: Config; }, ) { this.#limiter = limiterFactory(5); - - this.#useUrlReadersSearch = - this.options.config?.getOptionalBoolean('catalog.useUrlReadersSearch') || - false; - if (!this.#useUrlReadersSearch) { - this.options.logger.warn( - 'UrlReaderProcessor uses the legacy readUrl/search behavior which will be removed in a future release. Set catalog.useUrlReadersSearch to true to adopt the new behavior.', - ); - } } getProcessorName() { @@ -149,35 +136,13 @@ export class UrlReaderProcessor implements CatalogProcessor { location: string, etag?: string, ): Promise<{ response: { data: Buffer; url: string }[]; etag?: string }> { - // New behavior: always use the search method - if (this.#useUrlReadersSearch) { - const response = await this.options.reader.search(location, { etag }); + const response = await this.options.reader.search(location, { etag }); - const output = response.files.map(async file => ({ - url: file.url, - data: await this.#limiter(file.content), - })); + const output = response.files.map(async file => ({ + url: file.url, + data: await this.#limiter(file.content), + })); - return { response: await Promise.all(output), etag: response.etag }; - } - - // Old behavior: Does it contain globs? I.e. does it contain asterisks or question marks - // (no curly braces for now) - - const { filepath } = parseGitUrl(location); - if (filepath?.match(/[*?]/)) { - const response = await this.options.reader.search(location, { etag }); - const output = response.files.map(async file => ({ - url: file.url, - data: await this.#limiter(file.content), - })); - return { response: await Promise.all(output), etag: response.etag }; - } - - const data = await this.options.reader.readUrl(location, { etag }); - return { - response: [{ url: location, data: await data.buffer() }], - etag: data.etag, - }; + return { response: await Promise.all(output), etag: response.etag }; } } diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index c9d75298c8..e50aa84d80 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -356,7 +356,7 @@ export class CatalogBuilder { return [ new FileReaderProcessor(), - new UrlReaderProcessor({ reader, logger, config }), + new UrlReaderProcessor({ reader, logger }), new AnnotateLocationEntityProcessor({ integrations }), ]; } diff --git a/yarn.lock b/yarn.lock index 6d4aa9e768..4081495be9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6135,7 +6135,6 @@ __metadata: "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" "@backstage/repo-tools": "workspace:^" - "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@opentelemetry/api": "npm:^1.9.0" "@types/core-js": "npm:^2.5.4" From 4c6d891e5aad36d6e2b415e69602c90addd25826 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 17 Jun 2025 14:22:54 +0100 Subject: [PATCH 02/27] Improve support for button links Signed-off-by: Charles de Dreuille --- .changeset/public-nails-melt.md | 5 + canon-docs/src/content/components/button.mdx | 13 ++ .../src/content/components/button.props.ts | 4 + packages/canon/report.api.md | 47 +++--- .../src/components/Button/Button.props.ts | 41 ------ .../src/components/Button/Button.stories.tsx | 8 +- .../canon/src/components/Button/Button.tsx | 134 ++++++++++-------- .../components/Button/{index.tsx => index.ts} | 7 +- packages/canon/src/components/Button/types.ts | 57 ++++---- 9 files changed, 160 insertions(+), 156 deletions(-) create mode 100644 .changeset/public-nails-melt.md delete mode 100644 packages/canon/src/components/Button/Button.props.ts rename packages/canon/src/components/Button/{index.tsx => index.ts} (76%) diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md new file mode 100644 index 0000000000..a8d916950a --- /dev/null +++ b/.changeset/public-nails-melt.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': minor +--- + +**Breaking** We are removing the render prop on the Button component. If you want to use Button as a link, simply add the href prop and we will automaticlly use it as an anchor tag underneath. diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx index aff74fe60b..dceabde154 100644 --- a/canon-docs/src/content/components/button.mdx +++ b/canon-docs/src/content/components/button.mdx @@ -11,6 +11,7 @@ import { buttonFullWidthSnippet, buttonDisabledSnippet, buttonResponsiveSnippet, + buttonAsLinkSnippet, } from './button.props'; import { ComponentInfos } from '@/components/ComponentInfos'; @@ -102,3 +103,15 @@ Here's a view when buttons are disabled. Here's a view when buttons are responsive. + +### As Link + +Use the `href` prop to make a button act as a link. + +} + code={buttonAsLinkSnippet} +/> diff --git a/canon-docs/src/content/components/button.props.ts b/canon-docs/src/content/components/button.props.ts index fc833a5f73..9cbfc4a80c 100644 --- a/canon-docs/src/content/components/button.props.ts +++ b/canon-docs/src/content/components/button.props.ts @@ -61,3 +61,7 @@ export const buttonDisabledSnippet = ` export const buttonResponsiveSnippet = ``; + +export const buttonAsLinkSnippet = ``; diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 4db3a81bbc..df15e5c20d 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -3,8 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnchorHTMLAttributes } from 'react'; import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; +import { ButtonHTMLAttributes } from 'react'; import { ChangeEvent } from 'react'; import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { ComponentProps } from 'react'; @@ -152,37 +154,36 @@ export const breakpoints: Breakpoint[]; // @public (undocumented) export const Button: ForwardRefExoticComponent< - Omit & RefAttributes + ButtonProps & RefAttributes >; // @public (undocumented) -export type ButtonOwnProps = GetPropDefTypes; +export type ButtonAnchorProps = ButtonCommonProps & + Omit, 'type' | 'onClick'> & { + href: string; + onClick?: React.MouseEventHandler; + }; // @public (undocumented) -export const buttonPropDefs: { - variant: { - type: 'enum'; - values: ('primary' | 'secondary')[]; - className: string; - default: 'primary'; - responsive: true; - }; - size: { - type: 'enum'; - values: ('small' | 'medium')[]; - className: string; - default: 'medium'; - responsive: true; - }; +export type ButtonCommonProps = { + size?: 'small' | 'medium'; + variant?: 'primary' | 'secondary'; + iconStart?: ReactElement; + iconEnd?: ReactElement; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; }; +// @public (undocumented) +export type ButtonNativeProps = ButtonCommonProps & + Omit, 'href'> & { + href?: undefined; + onClick?: React.MouseEventHandler; + }; + // @public -export interface ButtonProps extends useRender.ComponentProps<'button'> { - iconEnd?: ReactElement; - iconStart?: ReactElement; - size?: ButtonOwnProps['size']; - variant?: ButtonOwnProps['variant']; -} +export type ButtonProps = ButtonAnchorProps | ButtonNativeProps; // @public (undocumented) export const Checkbox: ForwardRefExoticComponent< diff --git a/packages/canon/src/components/Button/Button.props.ts b/packages/canon/src/components/Button/Button.props.ts deleted file mode 100644 index e363360afa..0000000000 --- a/packages/canon/src/components/Button/Button.props.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { PropDef, GetPropDefTypes } from '../../props/prop-def'; - -/** @public */ -export const buttonPropDefs = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - className: 'canon-Button--variant', - default: 'primary', - responsive: true, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - className: 'canon-Button--size', - default: 'medium', - responsive: true, - }, -} satisfies { - variant: PropDef<'primary' | 'secondary'>; - size: PropDef<'small' | 'medium'>; -}; - -/** @public */ -export type ButtonOwnProps = GetPropDefTypes; diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index 2d79da7e29..8c9cc521f0 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -134,13 +134,9 @@ export const Disabled: Story = { export const AsLink: Story = { args: { children: 'I am a link', + href: 'https://canon.backstage.io', + target: '_blank', }, - render: args => ( - + ); + } +}); export default Button; diff --git a/packages/canon/src/components/Button/index.tsx b/packages/canon/src/components/Button/index.ts similarity index 76% rename from packages/canon/src/components/Button/index.tsx rename to packages/canon/src/components/Button/index.ts index 9051f1145e..b9c497ae1f 100644 --- a/packages/canon/src/components/Button/index.tsx +++ b/packages/canon/src/components/Button/index.ts @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Button } from './Button'; -export type { ButtonProps } from './types'; -export { buttonPropDefs } from './Button.props'; -export type { ButtonOwnProps } from './Button.props'; + +export * from './Button'; +export * from './types'; diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index e91538299e..c7ac2b88b7 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -14,35 +14,40 @@ * limitations under the License. */ -import type { ButtonOwnProps } from './Button.props'; -import { ReactElement } from 'react'; -import type { useRender } from '@base-ui-components/react/use-render'; +import { + ReactElement, + AnchorHTMLAttributes, + ButtonHTMLAttributes, +} from 'react'; + +/** @public */ +export type ButtonCommonProps = { + size?: 'small' | 'medium'; + variant?: 'primary' | 'secondary'; + iconStart?: ReactElement; + iconEnd?: ReactElement; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; +}; + +/** @public */ +export type ButtonAnchorProps = ButtonCommonProps & + Omit, 'type' | 'onClick'> & { + href: string; + onClick?: React.MouseEventHandler; + }; + +/** @public */ +export type ButtonNativeProps = ButtonCommonProps & + Omit, 'href'> & { + href?: undefined; + onClick?: React.MouseEventHandler; + }; /** * Properties for {@link Button} * * @public */ -export interface ButtonProps extends useRender.ComponentProps<'button'> { - /** - * The size of the button - * @defaultValue 'medium' - */ - size?: ButtonOwnProps['size']; - - /** - * The visual variant of the button - * @defaultValue 'primary' - */ - variant?: ButtonOwnProps['variant']; - - /** - * Optional icon to display at the start of the button - */ - iconStart?: ReactElement; - - /** - * Optional icon to display at the end of the button - */ - iconEnd?: ReactElement; -} +export type ButtonProps = ButtonAnchorProps | ButtonNativeProps; From 0a5d96e7bbd1c9c0fd2925c4bc5db48a4af37d9b Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 17 Jun 2025 14:37:57 +0100 Subject: [PATCH 03/27] Fix types Signed-off-by: Charles de Dreuille --- packages/canon/report.api.md | 7 +++++-- packages/canon/src/components/Button/types.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index df15e5c20d..5361e8ef2f 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -166,8 +166,11 @@ export type ButtonAnchorProps = ButtonCommonProps & // @public (undocumented) export type ButtonCommonProps = { - size?: 'small' | 'medium'; - variant?: 'primary' | 'secondary'; + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; iconStart?: ReactElement; iconEnd?: ReactElement; className?: string; diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index c7ac2b88b7..b4f1df2fad 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { Breakpoint } from '@backstage/canon'; import { ReactElement, AnchorHTMLAttributes, @@ -22,8 +23,11 @@ import { /** @public */ export type ButtonCommonProps = { - size?: 'small' | 'medium'; - variant?: 'primary' | 'secondary'; + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; iconStart?: ReactElement; iconEnd?: ReactElement; className?: string; From 078a3f64e5233c19e0c9a0e8b2b8032947853119 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 17 Jun 2025 18:51:10 +0100 Subject: [PATCH 04/27] Update IconButton Signed-off-by: Charles de Dreuille --- packages/canon/report.api.md | 47 ++++++------- .../canon/src/components/Button/Button.tsx | 6 +- .../components/IconButton/IconButton.props.ts | 41 ----------- .../IconButton/IconButton.stories.tsx | 9 +++ .../src/components/IconButton/IconButton.tsx | 68 +++++++++++++------ .../canon/src/components/IconButton/index.tsx | 6 +- .../canon/src/components/IconButton/types.ts | 55 +++++++++------ 7 files changed, 119 insertions(+), 113 deletions(-) delete mode 100644 packages/canon/src/components/IconButton/IconButton.props.ts diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 5361e8ef2f..872719f8c8 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -697,37 +697,38 @@ export const Icon: (props: IconProps) => JSX_2.Element | null; // @public (undocumented) export const IconButton: ForwardRefExoticComponent< - IconButtonProps & RefAttributes + IconButtonProps & RefAttributes >; // @public (undocumented) -export type IconButtonOwnProps = GetPropDefTypes; +export type IconButtonAnchorProps = IconButtonCommonProps & + Omit, 'type' | 'onClick'> & { + href: string; + onClick?: React.MouseEventHandler; + }; // @public (undocumented) -export const iconButtonPropDefs: { - variant: { - type: 'enum'; - values: ('primary' | 'secondary')[]; - className: string; - default: 'primary'; - responsive: true; - }; - size: { - type: 'enum'; - values: ('small' | 'medium')[]; - className: string; - default: 'medium'; - responsive: true; - }; +export type IconButtonCommonProps = { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; }; +// @public (undocumented) +export type IconButtonNativeProps = IconButtonCommonProps & + Omit, 'href'> & { + href?: undefined; + onClick?: React.MouseEventHandler; + }; + // @public -export interface IconButtonProps - extends Omit, 'children'> { - icon: ReactElement; - size?: IconButtonOwnProps['size']; - variant?: IconButtonOwnProps['variant']; -} +export type IconButtonProps = IconButtonAnchorProps | IconButtonNativeProps; // @public (undocumented) export const IconContext: Context; diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx index aae20c74e4..8693c1e016 100644 --- a/packages/canon/src/components/Button/Button.tsx +++ b/packages/canon/src/components/Button/Button.tsx @@ -33,9 +33,7 @@ export const Button = forwardRef((props, ref) => { ...rest } = props; - function isAnchor(props: { href?: unknown }): props is { href: string } { - return typeof props.href === 'string'; - } + const isAnchor = typeof props.href === 'string'; const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); @@ -64,7 +62,7 @@ export const Button = forwardRef((props, ref) => { ); - if (isAnchor(props)) { + if (isAnchor) { const { onClick, ...anchorRest } = rest as AnchorHTMLAttributes; return ( diff --git a/packages/canon/src/components/IconButton/IconButton.props.ts b/packages/canon/src/components/IconButton/IconButton.props.ts deleted file mode 100644 index e584997ef5..0000000000 --- a/packages/canon/src/components/IconButton/IconButton.props.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2025 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { PropDef, GetPropDefTypes } from '../../props/prop-def'; - -/** @public */ -export const iconButtonPropDefs = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - className: 'canon-Button--variant', - default: 'primary', - responsive: true, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - className: 'canon-Button--size', - default: 'medium', - responsive: true, - }, -} satisfies { - variant: PropDef<'primary' | 'secondary'>; - size: PropDef<'small' | 'medium'>; -}; - -/** @public */ -export type IconButtonOwnProps = GetPropDefTypes; diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index 7a7272ad05..97778e56a2 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -83,6 +83,15 @@ export const Disabled: Story = { ), }; +export const AsLink: Story = { + args: { + icon: , + href: 'https://canon.backstage.io', + target: '_blank', + 'aria-label': 'Cloud icon button', + }, +}; + export const Responsive: Story = { args: { icon: , diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx index fd80c91ddd..e84599874c 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/IconButton/IconButton.tsx @@ -14,46 +14,74 @@ * limitations under the License. */ -import { forwardRef } from 'react'; +import { AnchorHTMLAttributes, ButtonHTMLAttributes, forwardRef } from 'react'; import clsx from 'clsx'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; - import type { IconButtonProps } from './types'; /** @public */ -export const IconButton = forwardRef( - (props: IconButtonProps, ref) => { +export const IconButton = forwardRef( + (props, ref) => { const { size = 'small', variant = 'primary', icon, className, + href, style, ...rest } = props; + const isAnchor = typeof props.href === 'string'; const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); - return ( - + {icon} + ); + + if (isAnchor) { + const { onClick, ...anchorRest } = + rest as AnchorHTMLAttributes; + return ( + } + onClick={onClick} + {...anchorRest} + > + {content} + + ); + } else { + const { onClick, ...buttonRest } = + rest as ButtonHTMLAttributes; + return ( + + ); + } }, ); -export default IconButton; +IconButton.displayName = 'IconButton'; diff --git a/packages/canon/src/components/IconButton/index.tsx b/packages/canon/src/components/IconButton/index.tsx index 5f9943557e..1d30c871a4 100644 --- a/packages/canon/src/components/IconButton/index.tsx +++ b/packages/canon/src/components/IconButton/index.tsx @@ -14,7 +14,5 @@ * limitations under the License. */ -export { IconButton } from './IconButton'; -export type { IconButtonProps } from './types'; -export { iconButtonPropDefs } from './IconButton.props'; -export type { IconButtonOwnProps } from './IconButton.props'; +export * from './IconButton'; +export * from './types'; diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/IconButton/types.ts index ece1950d1b..145893991e 100644 --- a/packages/canon/src/components/IconButton/types.ts +++ b/packages/canon/src/components/IconButton/types.ts @@ -14,30 +14,43 @@ * limitations under the License. */ -import type { IconButtonOwnProps } from './IconButton.props'; -import { ReactElement } from 'react'; +import { + AnchorHTMLAttributes, + ButtonHTMLAttributes, + ReactElement, +} from 'react'; +import { Breakpoint } from '@backstage/canon'; + +/** @public */ +export type IconButtonCommonProps = { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; +}; + +/** @public */ +export type IconButtonAnchorProps = IconButtonCommonProps & + Omit, 'type' | 'onClick'> & { + href: string; + onClick?: React.MouseEventHandler; + }; + +/** @public */ +export type IconButtonNativeProps = IconButtonCommonProps & + Omit, 'href'> & { + href?: undefined; + onClick?: React.MouseEventHandler; + }; /** * Properties for {@link IconButton} * * @public */ -export interface IconButtonProps - extends Omit, 'children'> { - /** - * The size of the button - * @defaultValue 'medium' - */ - size?: IconButtonOwnProps['size']; - - /** - * The visual variant of the button - * @defaultValue 'primary' - */ - variant?: IconButtonOwnProps['variant']; - - /** - * Icon to display in the button - */ - icon: ReactElement; -} +export type IconButtonProps = IconButtonAnchorProps | IconButtonNativeProps; From 3c1752b83935110114a8b6abaa913ff6455bc9fc Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Tue, 17 Jun 2025 20:24:45 +0100 Subject: [PATCH 05/27] Update Button.tsx Signed-off-by: Charles de Dreuille --- packages/canon/src/components/Button/Button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx index 8693c1e016..e293eeb0f9 100644 --- a/packages/canon/src/components/Button/Button.tsx +++ b/packages/canon/src/components/Button/Button.tsx @@ -99,4 +99,4 @@ export const Button = forwardRef((props, ref) => { } }); -export default Button; +Button.displayName = 'Button'; From be519bc5ff0ee2efeb51a38fe8c87ff62489212f Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 18 Jun 2025 12:16:21 +0100 Subject: [PATCH 06/27] Improve types for both Button and IconButton Signed-off-by: Charles de Dreuille --- canon-docs/src/content/components/button.mdx | 2 +- .../src/content/components/button.props.ts | 8 +- packages/canon/report.api.md | 116 ++++++++---------- .../src/components/Button/Button.stories.tsx | 44 ++++--- .../canon/src/components/Button/Button.tsx | 62 +++------- packages/canon/src/components/Button/types.ts | 48 +++----- .../IconButton/IconButton.stories.tsx | 34 +++-- .../src/components/IconButton/IconButton.tsx | 83 ++++--------- .../canon/src/components/IconButton/types.ts | 50 +++----- packages/canon/src/types.ts | 18 +++ 10 files changed, 200 insertions(+), 265 deletions(-) diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx index dceabde154..6690a90e23 100644 --- a/canon-docs/src/content/components/button.mdx +++ b/canon-docs/src/content/components/button.mdx @@ -106,7 +106,7 @@ Here's a view when buttons are responsive. ### As Link -Use the `href` prop to make a button act as a link. +You can use the `as` prop to make a button act as a link. `; -export const buttonAsLinkSnippet = ` + +// Using a custom component +`; diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 872719f8c8..b648025251 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -3,10 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { AnchorHTMLAttributes } from 'react'; import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; -import { ButtonHTMLAttributes } from 'react'; import { ChangeEvent } from 'react'; import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { ComponentProps } from 'react'; @@ -44,6 +42,11 @@ export type ArbitraryStylingPropDef = { parseValue?: (value: string) => string | undefined; }; +// @public (undocumented) +export type AsProp = { + as?: C; +}; + // @public (undocumented) export type AsProps = | 'div' @@ -153,40 +156,26 @@ export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export const breakpoints: Breakpoint[]; // @public (undocumented) -export const Button: ForwardRefExoticComponent< - ButtonProps & RefAttributes ->; - -// @public (undocumented) -export type ButtonAnchorProps = ButtonCommonProps & - Omit, 'type' | 'onClick'> & { - href: string; - onClick?: React.MouseEventHandler; - }; - -// @public (undocumented) -export type ButtonCommonProps = { - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - iconStart?: ReactElement; - iconEnd?: ReactElement; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -// @public (undocumented) -export type ButtonNativeProps = ButtonCommonProps & - Omit, 'href'> & { - href?: undefined; - onClick?: React.MouseEventHandler; - }; +export const Button: ( + props: ButtonProps, +) => JSX_2.Element; // @public -export type ButtonProps = ButtonAnchorProps | ButtonNativeProps; +export type ButtonProps = PolymorphicComponentProp< + C, + { + size?: + | 'small' + | 'medium' + | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + } +>; // @public (undocumented) export const Checkbox: ForwardRefExoticComponent< @@ -696,39 +685,26 @@ export type HeightProps = GetPropDefTypes; export const Icon: (props: IconProps) => JSX_2.Element | null; // @public (undocumented) -export const IconButton: ForwardRefExoticComponent< - IconButtonProps & RefAttributes ->; - -// @public (undocumented) -export type IconButtonAnchorProps = IconButtonCommonProps & - Omit, 'type' | 'onClick'> & { - href: string; - onClick?: React.MouseEventHandler; - }; - -// @public (undocumented) -export type IconButtonCommonProps = { - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - icon?: ReactElement; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -// @public (undocumented) -export type IconButtonNativeProps = IconButtonCommonProps & - Omit, 'href'> & { - href?: undefined; - onClick?: React.MouseEventHandler; - }; +export const IconButton: ( + props: IconButtonProps, +) => JSX_2.Element; // @public -export type IconButtonProps = IconButtonAnchorProps | IconButtonNativeProps; +export type IconButtonProps = + PolymorphicComponentProp< + C, + { + size?: + | 'small' + | 'medium' + | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; + } + >; // @public (undocumented) export const IconContext: Context; @@ -1025,6 +1001,13 @@ export const paddingPropDefs: (spacingValues: string[]) => { // @public (undocumented) export type PaddingProps = GetPropDefTypes; +// @public +export type PolymorphicComponentProp< + C extends React.ElementType, + Props = {}, +> = React.PropsWithChildren> & + Omit, PropsToOmit>; + // @public (undocumented) export const positionPropDefs: { position: { @@ -1041,6 +1024,9 @@ export type PositionProps = GetPropDefTypes; // @public (undocumented) export type PropDef = RegularPropDef | ResponsivePropDef; +// @public (undocumented) +export type PropsToOmit = keyof (AsProp & P); + // @public (undocumented) export type ReactNodePropDef = { type: 'ReactNode'; diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index 8c9cc521f0..30559d2698 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -133,12 +133,26 @@ export const Disabled: Story = { export const AsLink: Story = { args: { - children: 'I am a link', + children: 'Button', href: 'https://canon.backstage.io', target: '_blank', }, }; +export const AsComponent: Story = { + render: () => { + const Link = (props: { children: React.ReactNode; to: string }) => ( + + ); + + return ( + + ); + }, +}; + export const Responsive: Story = { args: { children: 'Button', @@ -167,22 +181,22 @@ export const Playground: Story = { {['small', 'medium'].map(size => ( @@ -190,30 +204,30 @@ export const Playground: Story = { iconStart={} iconEnd={} style={{ width: '200px' }} - variant={variant as ButtonProps['variant']} - size={size as ButtonProps['size']} + variant={variant as ButtonProps['variant']} + size={size as ButtonProps['size']} > Button - ); - } -}); - -Button.displayName = 'Button'; +}; diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index b4f1df2fad..1ff4e19cb0 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -15,43 +15,23 @@ */ import { Breakpoint } from '@backstage/canon'; -import { - ReactElement, - AnchorHTMLAttributes, - ButtonHTMLAttributes, -} from 'react'; - -/** @public */ -export type ButtonCommonProps = { - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - iconStart?: ReactElement; - iconEnd?: ReactElement; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -/** @public */ -export type ButtonAnchorProps = ButtonCommonProps & - Omit, 'type' | 'onClick'> & { - href: string; - onClick?: React.MouseEventHandler; - }; - -/** @public */ -export type ButtonNativeProps = ButtonCommonProps & - Omit, 'href'> & { - href?: undefined; - onClick?: React.MouseEventHandler; - }; +import { ReactElement } from 'react'; +import { PolymorphicComponentProp } from '../../types'; /** * Properties for {@link Button} * * @public */ -export type ButtonProps = ButtonAnchorProps | ButtonNativeProps; +export type ButtonProps = PolymorphicComponentProp< + C, + { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + } +>; diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index 97778e56a2..d74e7ebf1a 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -84,11 +84,23 @@ export const Disabled: Story = { }; export const AsLink: Story = { - args: { - icon: , - href: 'https://canon.backstage.io', - target: '_blank', - 'aria-label': 'Cloud icon button', + render: () => ( + } + /> + ), +}; + +export const AsComponent: Story = { + render: () => { + const Link = (props: { children?: React.ReactNode; to: string }) => ( + + ); + + return } />; }, }; @@ -123,22 +135,22 @@ export const Playground: Story = { ['variant']} + size={size as IconButtonProps['size']} /> } aria-label="Chevron right icon button" - variant={variant as IconButtonProps['variant']} - size={size as IconButtonProps['size']} + variant={variant as IconButtonProps['variant']} + size={size as IconButtonProps['size']} /> } aria-label="Chevron right icon button" - variant={variant as IconButtonProps['variant']} - size={size as IconButtonProps['size']} + variant={variant as IconButtonProps['variant']} + size={size as IconButtonProps['size']} /> ))} diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx index e84599874c..079aa2e6f4 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/IconButton/IconButton.tsx @@ -14,29 +14,36 @@ * limitations under the License. */ -import { AnchorHTMLAttributes, ButtonHTMLAttributes, forwardRef } from 'react'; import clsx from 'clsx'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import type { IconButtonProps } from './types'; /** @public */ -export const IconButton = forwardRef( - (props, ref) => { - const { - size = 'small', - variant = 'primary', - icon, - className, - href, - style, - ...rest - } = props; +export const IconButton = ( + props: IconButtonProps, +) => { + const { + as, + size = 'small', + variant = 'primary', + icon, + className, + href, + style, + ...rest + } = props; - const isAnchor = typeof props.href === 'string'; - const responsiveSize = useResponsiveValue(size); - const responsiveVariant = useResponsiveValue(variant); + const Component = as || 'button'; + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); - const content = ( + return ( + - ); - - if (isAnchor) { - const { onClick, ...anchorRest } = - rest as AnchorHTMLAttributes; - return ( - } - onClick={onClick} - {...anchorRest} - > - {content} - - ); - } else { - const { onClick, ...buttonRest } = - rest as ButtonHTMLAttributes; - return ( - - ); - } - }, -); - -IconButton.displayName = 'IconButton'; + + ); +}; diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/IconButton/types.ts index 145893991e..f15831e4a4 100644 --- a/packages/canon/src/components/IconButton/types.ts +++ b/packages/canon/src/components/IconButton/types.ts @@ -14,43 +14,27 @@ * limitations under the License. */ -import { - AnchorHTMLAttributes, - ButtonHTMLAttributes, - ReactElement, -} from 'react'; import { Breakpoint } from '@backstage/canon'; - -/** @public */ -export type IconButtonCommonProps = { - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - icon?: ReactElement; - className?: string; - style?: React.CSSProperties; - children?: React.ReactNode; -}; - -/** @public */ -export type IconButtonAnchorProps = IconButtonCommonProps & - Omit, 'type' | 'onClick'> & { - href: string; - onClick?: React.MouseEventHandler; - }; - -/** @public */ -export type IconButtonNativeProps = IconButtonCommonProps & - Omit, 'href'> & { - href?: undefined; - onClick?: React.MouseEventHandler; - }; +import { ReactElement } from 'react'; +import { PolymorphicComponentProp } from '../../types'; /** * Properties for {@link IconButton} * * @public */ -export type IconButtonProps = IconButtonAnchorProps | IconButtonNativeProps; +export type IconButtonProps = + PolymorphicComponentProp< + C, + { + size?: + | 'small' + | 'medium' + | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; + } + >; diff --git a/packages/canon/src/types.ts b/packages/canon/src/types.ts index ee5d954576..df3959e368 100644 --- a/packages/canon/src/types.ts +++ b/packages/canon/src/types.ts @@ -131,3 +131,21 @@ export interface UtilityProps extends SpaceProps { justifyContent?: Responsive; rowSpan?: Responsive; } + +/** @public */ +export type AsProp = { + as?: C; +}; + +/** @public */ +export type PropsToOmit = keyof (AsProp & P); + +/** + * This is the first reusable type utility we built + * @public + */ +export type PolymorphicComponentProp< + C extends React.ElementType, + Props = {}, +> = React.PropsWithChildren> & + Omit, PropsToOmit>; From ae661604970c8472a8d6cd06ddf63dab1ac41446 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 18 Jun 2025 12:41:08 +0100 Subject: [PATCH 07/27] Improve docs Signed-off-by: Charles de Dreuille --- .../src/content/components/button.props.ts | 3 ++ .../src/content/components/icon-button.mdx | 13 +++++ .../content/components/icon-button.props.ts | 29 +++++++---- .../IconButton/IconButton.stories.tsx | 50 ++++++------------- 4 files changed, 52 insertions(+), 43 deletions(-) diff --git a/canon-docs/src/content/components/button.props.ts b/canon-docs/src/content/components/button.props.ts index 02ed9e9134..ef72a70596 100644 --- a/canon-docs/src/content/components/button.props.ts +++ b/canon-docs/src/content/components/button.props.ts @@ -14,6 +14,9 @@ export const buttonPropDefs: Record = { default: 'medium', responsive: true, }, + iconStart: { type: 'enum', values: ['ReactNode'], responsive: false }, + iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false }, + children: { type: 'enum', values: ['ReactNode'], responsive: false }, ...classNamePropDefs, ...stylePropDefs, }; diff --git a/canon-docs/src/content/components/icon-button.mdx b/canon-docs/src/content/components/icon-button.mdx index 80e7e5a46d..d8eb3beab2 100644 --- a/canon-docs/src/content/components/icon-button.mdx +++ b/canon-docs/src/content/components/icon-button.mdx @@ -10,6 +10,7 @@ import { iconButtonSizesSnippet, iconButtonDisabledSnippet, iconButtonResponsiveSnippet, + iconButtonAsLinkSnippet, } from './icon-button.props'; import { ComponentInfos } from '@/components/ComponentInfos'; @@ -77,3 +78,15 @@ Here's a view when buttons are disabled. Here's a view when buttons are responsive. + +### As Link + +You can use the `as` prop to make a button act as a link. + +} + code={iconButtonAsLinkSnippet} +/> diff --git a/canon-docs/src/content/components/icon-button.props.ts b/canon-docs/src/content/components/icon-button.props.ts index 8fdfaba6b9..9e3a2818fe 100644 --- a/canon-docs/src/content/components/icon-button.props.ts +++ b/canon-docs/src/content/components/icon-button.props.ts @@ -17,7 +17,7 @@ export const iconButtonPropDefs: Record = { default: 'medium', responsive: true, }, - icon: { type: 'enum', values: 'icon', responsive: false }, + icon: { type: 'enum', values: ['ReactNode'], responsive: false }, ...classNamePropDefs, ...stylePropDefs, }; @@ -27,20 +27,31 @@ export const iconButtonUsageSnippet = `import { IconButton } from '@backstage/ca `; export const iconButtonDefaultSnippet = ` - - + } variant="primary" /> + } variant="secondary" /> `; export const iconButtonVariantsSnippet = ` - - + } variant="primary" /> + } variant="secondary" /> `; export const iconButtonSizesSnippet = ` - - + } size="small" /> + } size="medium" /> `; -export const iconButtonDisabledSnippet = ``; +export const iconButtonDisabledSnippet = `} disabled />`; -export const iconButtonResponsiveSnippet = ``; +export const iconButtonResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; + +export const iconButtonAsLinkSnippet = `// Using the \`as\` prop +} +/> + +// Using a custom component +} />`; diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index d74e7ebf1a..110db0aa89 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -40,58 +40,44 @@ export default meta; type Story = StoryObj; export const Default: Story = { - args: { - icon: , - }, + render: args => } />, }; export const Variants: Story = { - args: { - ...Default.args, - }, render: args => ( - - + } variant="primary" /> + } variant="secondary" /> ), }; export const Sizes: Story = { - args: { - icon: , - }, render: args => ( - - + } size="small" /> + } size="medium" /> ), }; export const Disabled: Story = { - args: { - icon: , - disabled: true, - 'aria-label': 'Cloud icon button', - }, + args: { disabled: true }, render: args => ( - - + } variant="primary" /> + } variant="secondary" /> ), }; export const AsLink: Story = { - render: () => ( - } - /> - ), + args: { + as: 'a', + href: 'https://canon.backstage.io', + target: '_blank', + }, + render: args => } />, }; export const AsComponent: Story = { @@ -106,8 +92,6 @@ export const AsComponent: Story = { export const Responsive: Story = { args: { - icon: , - 'aria-label': 'Cloud icon button', variant: { initial: 'primary', sm: 'secondary', @@ -117,15 +101,12 @@ export const Responsive: Story = { sm: 'medium', }, }, + render: args => } />, }; const variants: string[] = ['primary', 'secondary']; export const Playground: Story = { - args: { - icon: , - 'aria-label': 'Cloud icon button', - }, render: args => ( {variants.map(variant => ( @@ -137,6 +118,7 @@ export const Playground: Story = { {...args} variant={variant as IconButtonProps['variant']} size={size as IconButtonProps['size']} + icon={} /> Date: Wed, 18 Jun 2025 12:46:12 +0100 Subject: [PATCH 08/27] Update public-nails-melt.md Signed-off-by: Charles de Dreuille --- .changeset/public-nails-melt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md index a8d916950a..169bf7fbfb 100644 --- a/.changeset/public-nails-melt.md +++ b/.changeset/public-nails-melt.md @@ -2,4 +2,4 @@ '@backstage/canon': minor --- -**Breaking** We are removing the render prop on the Button component. If you want to use Button as a link, simply add the href prop and we will automaticlly use it as an anchor tag underneath. +**Breaking** We are transforming the way we handle custom tags for both Button and IconButton. We are introducing a new as prop for both of these components and we are removing support for the render prop. From 1debf7fa3541cde8284a9f06a15e5a677f49d3c6 Mon Sep 17 00:00:00 2001 From: Chris Suich Date: Wed, 18 Jun 2025 11:42:38 -0400 Subject: [PATCH 09/27] fix(techdocs): handle undefined defaultPath in techdocs initial redirect When handling the initial redirect for techdocs deep linking an undefined value for defaultPath was being included in the url resulting in an error page. https://github.com/backstage/backstage/issues/30300 Signed-off-by: Chris Suich --- .changeset/chubby-dots-kiss.md | 5 ++ .../TechDocsReaderPageContent/dom.test.tsx | 72 +++++++++++++++++++ .../TechDocsReaderPageContent/dom.tsx | 6 +- 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 .changeset/chubby-dots-kiss.md create mode 100644 plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx diff --git a/.changeset/chubby-dots-kiss.md b/.changeset/chubby-dots-kiss.md new file mode 100644 index 0000000000..b23fe2fcef --- /dev/null +++ b/.changeset/chubby-dots-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Handle undefined defaultPath in TechDocs initial redirect. diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx new file mode 100644 index 0000000000..80b19f0308 --- /dev/null +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { render } from '@testing-library/react'; + +// We need to mock react-router-dom hooks used by useInitialRedirect +import { useLocation, useNavigate, useParams } from 'react-router-dom'; + +// Import the module from which the hook is defined +import { useInitialRedirect } from './dom'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn(), + useNavigate: jest.fn(), + useParams: jest.fn(), +})); + +describe('useInitialRedirect', () => { + const mockNavigate = jest.fn(); + + beforeEach(() => { + // Reset mocks before each test + mockNavigate.mockReset(); + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); + (useLocation as jest.Mock).mockReturnValue({ + pathname: '/docs/default/Component/backstage-demo', + }); + // Simulate that no current path is provided + (useParams as jest.Mock).mockReturnValue({ '*': '' }); + }); + + const TestComponent: React.FC<{ defaultPath?: string }> = ({ + defaultPath, + }) => { + // Call hook that should trigger a redirect on mount only if defaultPath is a non-empty string. + useInitialRedirect(defaultPath); + return
Test
; + }; + + it('should not navigate when defaultPath is undefined', () => { + render(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('should navigate when defaultPath is a non-empty string', () => { + render(); + expect(mockNavigate).toHaveBeenCalledWith( + '/docs/default/Component/backstage-demo/overview', + { replace: true }, + ); + }); + + it('should not navigate if currPath is non-empty', () => { + // Override useParams to simulate a non-empty currPath + (useParams as jest.Mock).mockReturnValue({ '*': 'existing-path' }); + render(); + expect(mockNavigate).not.toHaveBeenCalled(); + }); +}); diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index 0c4d6dee3e..0837710d7a 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -61,15 +61,13 @@ const MOBILE_MEDIA_QUERY = 'screen and (max-width: 76.1875em)'; // If a defaultPath is specified then we should navigate to that path replacing the // current location in the history. This should only happen on the initial load so // navigating to the root of the docs doesn't also redirect. -const useInitialRedirect = (defaultPath?: string) => { - // const hasRun = useRef(false); - +export const useInitialRedirect = (defaultPath?: string) => { const location = useLocation(); const navigate = useNavigate(); const { '*': currPath = '' } = useParams(); useLayoutEffect(() => { - if (currPath === '' && defaultPath !== '') { + if (currPath === '' && defaultPath && defaultPath !== '') { navigate(`${location.pathname}${defaultPath}`, { replace: true }); } }, []); // eslint-disable-line react-hooks/exhaustive-deps From ad187bde2ec62927f77cd2196bce335fdcffcee7 Mon Sep 17 00:00:00 2001 From: Chris Suich Date: Wed, 18 Jun 2025 12:30:10 -0400 Subject: [PATCH 10/27] just defaultPath Signed-off-by: Chris Suich --- .../src/reader/components/TechDocsReaderPageContent/dom.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx index 0837710d7a..35e847a091 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/dom.tsx @@ -67,7 +67,7 @@ export const useInitialRedirect = (defaultPath?: string) => { const { '*': currPath = '' } = useParams(); useLayoutEffect(() => { - if (currPath === '' && defaultPath && defaultPath !== '') { + if (currPath === '' && defaultPath) { navigate(`${location.pathname}${defaultPath}`, { replace: true }); } }, []); // eslint-disable-line react-hooks/exhaustive-deps From 6f9b502232161193be9c6556bdaf46cd18fd4225 Mon Sep 17 00:00:00 2001 From: James Brooks Date: Wed, 18 Jun 2025 17:49:05 +0100 Subject: [PATCH 11/27] Attempting to add forwardRef Signed-off-by: James Brooks --- .../canon/src/components/Button/Button.tsx | 104 ++++++++++-------- packages/canon/src/components/Button/types.ts | 9 +- packages/canon/src/types.ts | 20 ++-- 3 files changed, 75 insertions(+), 58 deletions(-) diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx index 19e89171e7..905e81111b 100644 --- a/packages/canon/src/components/Button/Button.tsx +++ b/packages/canon/src/components/Button/Button.tsx @@ -15,54 +15,70 @@ */ import clsx from 'clsx'; +import { + ComponentPropsWithRef, + ElementType, + forwardRef, + ReactElement, + Ref, +} from 'react'; +import { Button as RAButton } from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import type { ButtonProps } from './types'; /** @public */ -export const Button = ( - props: ButtonProps, -) => { - const { - as, - size = 'small', - variant = 'primary', - iconStart, - iconEnd, - children, - className, - ...rest - } = props; +export const Button = forwardRef( + (props: ButtonProps, ref: Ref) => { + const { + as, + size = 'small', + variant = 'primary', + iconStart, + iconEnd, + children, + className, + ...rest + } = props; - const Component = as || 'button'; - const responsiveSize = useResponsiveValue(size); - const responsiveVariant = useResponsiveValue(variant); + const Component = as || RAButton; + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); - return ( - - {iconStart && ( - - )} - {children} - {iconEnd && ( - - )} - - ); + return ( + + {iconStart && ( + + )} + {children} + {iconEnd && ( + + )} + + ); + }, +) as { + ( + props: ButtonProps & { ref?: ComponentPropsWithRef['ref'] }, + ): ReactElement; + displayName: string; }; + +Button.displayName = 'Button'; diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index 1ff4e19cb0..327fa27bb6 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -15,17 +15,18 @@ */ import { Breakpoint } from '@backstage/canon'; -import { ReactElement } from 'react'; -import { PolymorphicComponentProp } from '../../types'; +import { ElementType, ReactElement, ReactNode } from 'react'; +import { PolymorphicComponentProps } from '../../types'; /** * Properties for {@link Button} * * @public */ -export type ButtonProps = PolymorphicComponentProp< - C, +export type ButtonProps = PolymorphicComponentProps< + TAs, { + children?: ReactNode; size?: 'small' | 'medium' | Partial>; variant?: | 'primary' diff --git a/packages/canon/src/types.ts b/packages/canon/src/types.ts index df3959e368..7e542f3635 100644 --- a/packages/canon/src/types.ts +++ b/packages/canon/src/types.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { ComponentPropsWithoutRef, ElementType } from 'react'; + /** @public */ export type AsProps = | 'div' @@ -133,19 +135,17 @@ export interface UtilityProps extends SpaceProps { } /** @public */ -export type AsProp = { - as?: C; +export type As = { + as?: TAs; }; -/** @public */ -export type PropsToOmit = keyof (AsProp & P); - /** * This is the first reusable type utility we built * @public */ -export type PolymorphicComponentProp< - C extends React.ElementType, - Props = {}, -> = React.PropsWithChildren> & - Omit, PropsToOmit>; +export type PolymorphicComponentProps< + TAs extends ElementType, + TProps = {}, +> = TProps & + As & + Omit, keyof (As & TProps)>; From 13dbfce65e2bb2c7eb95b6c5a2a6586df5999440 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 18 Jun 2025 18:18:27 +0100 Subject: [PATCH 12/27] Fixed IconButton Signed-off-by: Charles de Dreuille --- packages/canon/report.api.md | 57 ++++++++------ .../src/components/Button/Button.stories.tsx | 6 +- .../IconButton/IconButton.stories.tsx | 35 ++++----- .../src/components/IconButton/IconButton.tsx | 75 +++++++++++-------- .../canon/src/components/IconButton/types.ts | 10 +-- 5 files changed, 105 insertions(+), 78 deletions(-) diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index b648025251..96e9e31235 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -5,13 +5,15 @@ ```ts import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; +import { Button as Button_2 } from 'react-aria-components'; import { ChangeEvent } from 'react'; import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { ComponentProps } from 'react'; -import type { ComponentPropsWithRef } from 'react'; +import { ComponentPropsWithoutRef } from 'react'; +import { ComponentPropsWithRef } from 'react'; import { Context } from 'react'; import type { CSSProperties } from 'react'; -import type { ElementType } from 'react'; +import { ElementType } from 'react'; import { FC } from 'react'; import { FocusEvent as FocusEvent_2 } from 'react'; import { ForwardRefExoticComponent } from 'react'; @@ -43,8 +45,8 @@ export type ArbitraryStylingPropDef = { }; // @public (undocumented) -export type AsProp = { - as?: C; +export type As = { + as?: TAs; }; // @public (undocumented) @@ -156,14 +158,20 @@ export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export const breakpoints: Breakpoint[]; // @public (undocumented) -export const Button: ( - props: ButtonProps, -) => JSX_2.Element; +export const Button: { + ( + props: ButtonProps & { + ref?: ComponentPropsWithRef['ref']; + }, + ): ReactElement; + displayName: string; +}; // @public -export type ButtonProps = PolymorphicComponentProp< - C, +export type ButtonProps = PolymorphicComponentProps< + TAs, { + children?: ReactNode; size?: | 'small' | 'medium' @@ -685,14 +693,19 @@ export type HeightProps = GetPropDefTypes; export const Icon: (props: IconProps) => JSX_2.Element | null; // @public (undocumented) -export const IconButton: ( - props: IconButtonProps, -) => JSX_2.Element; +export const IconButton: { + ( + props: IconButtonProps & { + ref?: ComponentPropsWithRef['ref']; + }, + ): ReactElement; + displayName: string; +}; // @public -export type IconButtonProps = - PolymorphicComponentProp< - C, +export type IconButtonProps = + PolymorphicComponentProps< + TAs, { size?: | 'small' @@ -1002,11 +1015,12 @@ export const paddingPropDefs: (spacingValues: string[]) => { export type PaddingProps = GetPropDefTypes; // @public -export type PolymorphicComponentProp< - C extends React.ElementType, - Props = {}, -> = React.PropsWithChildren> & - Omit, PropsToOmit>; +export type PolymorphicComponentProps< + TAs extends ElementType, + TProps = {}, +> = TProps & + As & + Omit, keyof (As & TProps)>; // @public (undocumented) export const positionPropDefs: { @@ -1024,9 +1038,6 @@ export type PositionProps = GetPropDefTypes; // @public (undocumented) export type PropDef = RegularPropDef | ResponsivePropDef; -// @public (undocumented) -export type PropsToOmit = keyof (AsProp & P); - // @public (undocumented) export type ReactNodePropDef = { type: 'ReactNode'; diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index 30559d2698..7baff061b3 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -212,7 +212,7 @@ export const Playground: Story = { @@ -220,7 +220,7 @@ export const Playground: Story = { iconStart={} variant={variant as ButtonProps['variant']} size={size as ButtonProps['size']} - disabled + isDisabled > Button @@ -228,7 +228,7 @@ export const Playground: Story = { iconEnd={} variant={variant as ButtonProps['variant']} size={size as ButtonProps['size']} - disabled + isDisabled > Button diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index 110db0aa89..4065b61cb2 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -40,44 +40,45 @@ export default meta; type Story = StoryObj; export const Default: Story = { - render: args => } />, + render: () => } />, }; export const Variants: Story = { - render: args => ( + render: () => ( - } variant="primary" /> - } variant="secondary" /> + } variant="primary" /> + } variant="secondary" /> ), }; export const Sizes: Story = { - render: args => ( + render: () => ( - } size="small" /> - } size="medium" /> + } size="small" /> + } size="medium" /> ), }; export const Disabled: Story = { - args: { disabled: true }, - render: args => ( + render: () => ( - } variant="primary" /> - } variant="secondary" /> + } variant="primary" /> + } variant="secondary" /> ), }; export const AsLink: Story = { - args: { - as: 'a', - href: 'https://canon.backstage.io', - target: '_blank', - }, - render: args => } />, + render: () => ( + } + /> + ), }; export const AsComponent: Story = { diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx index 079aa2e6f4..dfa06cb612 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/IconButton/IconButton.tsx @@ -15,42 +15,57 @@ */ import clsx from 'clsx'; +import { + ComponentPropsWithRef, + ElementType, + forwardRef, + ReactElement, + Ref, +} from 'react'; +import { Button as RAButton } from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import type { IconButtonProps } from './types'; /** @public */ -export const IconButton = ( - props: IconButtonProps, -) => { - const { - as, - size = 'small', - variant = 'primary', - icon, - className, - href, - style, - ...rest - } = props; +export const IconButton = forwardRef( + (props: IconButtonProps, ref: Ref) => { + const { + as, + size = 'small', + variant = 'primary', + icon, + className, + style, + ...rest + } = props; - const Component = as || 'button'; - const responsiveSize = useResponsiveValue(size); - const responsiveVariant = useResponsiveValue(variant); + const Component = as || RAButton; + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); - return ( - - - - ); + + + ); + }, +) as { + ( + props: IconButtonProps & { ref?: ComponentPropsWithRef['ref'] }, + ): ReactElement; + displayName: string; }; + +IconButton.displayName = 'IconButton'; diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/IconButton/types.ts index f15831e4a4..1d3deb94c2 100644 --- a/packages/canon/src/components/IconButton/types.ts +++ b/packages/canon/src/components/IconButton/types.ts @@ -15,17 +15,17 @@ */ import { Breakpoint } from '@backstage/canon'; -import { ReactElement } from 'react'; -import { PolymorphicComponentProp } from '../../types'; +import { ElementType, ReactElement } from 'react'; +import { PolymorphicComponentProps } from '../../types'; /** * Properties for {@link IconButton} * * @public */ -export type IconButtonProps = - PolymorphicComponentProp< - C, +export type IconButtonProps = + PolymorphicComponentProps< + TAs, { size?: | 'small' From 67737f222138099a070d8aa31e6e3b57968ff70e Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Wed, 18 Jun 2025 18:25:44 +0100 Subject: [PATCH 13/27] Update DataTablePagination.tsx Signed-off-by: Charles de Dreuille --- .../components/DataTable/Pagination/DataTablePagination.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx index 2efa73bf14..452e0c34fd 100644 --- a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx +++ b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx @@ -75,14 +75,14 @@ const DataTablePagination = forwardRef( variant="secondary" size="small" onClick={() => table?.previousPage()} - disabled={!table?.getCanPreviousPage()} + isDisabled={!table?.getCanPreviousPage()} icon={} /> table?.nextPage()} - disabled={!table?.getCanNextPage()} + isDisabled={!table?.getCanNextPage()} icon={} /> From 6bc0799e1f99efc4a019097fda8272572452376a Mon Sep 17 00:00:00 2001 From: Niall Thomson Date: Wed, 18 Jun 2025 12:59:16 -0600 Subject: [PATCH 14/27] docs: Fix MCP backend token subject Signed-off-by: Niall Thomson --- .changeset/rich-eagles-rule.md | 5 +++++ plugins/mcp-actions-backend/README.md | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/rich-eagles-rule.md diff --git a/.changeset/rich-eagles-rule.md b/.changeset/rich-eagles-rule.md new file mode 100644 index 0000000000..100ccafb77 --- /dev/null +++ b/.changeset/rich-eagles-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-mcp-actions-backend': patch +--- + +Fixed the example in the README for generating a static token by adding a subject field diff --git a/plugins/mcp-actions-backend/README.md b/plugins/mcp-actions-backend/README.md index b14f252bce..c44bd7f0a6 100644 --- a/plugins/mcp-actions-backend/README.md +++ b/plugins/mcp-actions-backend/README.md @@ -89,6 +89,7 @@ backend: - type: static options: token: ${MCP_TOKEN} + subject: mcp-clients accessRestrictions: - plugin: mcp-actions - plugin: catalog From 7cb7bc0203827950261ce715bc1d55599fac6d58 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 18 Jun 2025 17:39:32 -0400 Subject: [PATCH 15/27] Improve changeset wording Co-authored-by: Vincenzo Scamporlino Signed-off-by: Chris --- .changeset/chubby-dots-kiss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/chubby-dots-kiss.md b/.changeset/chubby-dots-kiss.md index b23fe2fcef..7a28467725 100644 --- a/.changeset/chubby-dots-kiss.md +++ b/.changeset/chubby-dots-kiss.md @@ -2,4 +2,4 @@ '@backstage/plugin-techdocs': patch --- -Handle undefined defaultPath in TechDocs initial redirect. +Fixed an issue causing TechDocs to not properly handle initial redirect. From b4db4d190d3bebf0ad999a8051975fc8b213ad8f Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 19 Jun 2025 09:07:55 +0100 Subject: [PATCH 16/27] Improve story types Signed-off-by: James Brooks --- .../IconButton/IconButton.stories.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/IconButton/IconButton.stories.tsx index 4065b61cb2..c32ddb878a 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -18,7 +18,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { IconButton } from './IconButton'; import { Flex } from '../Flex'; import { Text } from '../Text'; -import { IconButtonProps } from './types'; import { Icon } from '../Icon'; const meta = { @@ -105,7 +104,8 @@ export const Responsive: Story = { render: args => } />, }; -const variants: string[] = ['primary', 'secondary']; +const variants = ['primary', 'secondary'] as const; +const sizes = ['small', 'medium'] as const; export const Playground: Story = { render: args => ( @@ -113,27 +113,27 @@ export const Playground: Story = { {variants.map(variant => ( {variant} - {['small', 'medium'].map(size => ( + {sizes.map(size => ( ['variant']} - size={size as IconButtonProps['size']} + variant={variant} + size={size} icon={} /> } aria-label="Chevron right icon button" - variant={variant as IconButtonProps['variant']} - size={size as IconButtonProps['size']} + variant={variant} + size={size} /> } aria-label="Chevron right icon button" - variant={variant as IconButtonProps['variant']} - size={size as IconButtonProps['size']} + variant={variant} + size={size} /> ))} From aae98c9747662997d34fa9b9095ab8993ab67cfa Mon Sep 17 00:00:00 2001 From: James Brooks Date: Thu, 19 Jun 2025 09:10:45 +0100 Subject: [PATCH 17/27] Fix class name Signed-off-by: James Brooks --- packages/canon/src/components/IconButton/IconButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx index dfa06cb612..73c95ee131 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/IconButton/IconButton.tsx @@ -45,7 +45,7 @@ export const IconButton = forwardRef( return ( Date: Thu, 19 Jun 2025 09:12:33 +0100 Subject: [PATCH 18/27] Improve more story types Signed-off-by: James Brooks --- .../src/components/Button/Button.stories.tsx | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index 7baff061b3..82709f1577 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -18,7 +18,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Button } from './Button'; import { Flex } from '../Flex'; import { Text } from '../Text'; -import { ButtonProps } from './types'; import { Icon } from '../Icon'; const meta = { @@ -167,7 +166,8 @@ export const Responsive: Story = { }, }; -const variants: string[] = ['primary', 'secondary']; +const variants = ['primary', 'secondary'] as const; +const sizes = ['small', 'medium'] as const; export const Playground: Story = { args: { @@ -178,25 +178,22 @@ export const Playground: Story = { {variants.map(variant => ( {variant} - {['small', 'medium'].map(size => ( + {sizes.map(size => ( - @@ -204,30 +201,26 @@ export const Playground: Story = { iconStart={} iconEnd={} style={{ width: '200px' }} - variant={variant as ButtonProps['variant']} - size={size as ButtonProps['size']} + variant={variant} + size={size} > Button - + ), }; export const AsLink: Story = { args: { - children: 'Button', + as: 'a', + children: 'I am a link', href: 'https://canon.backstage.io', target: '_blank', }, From cf9ba6f0ab261b29c683144b737cdb23bb85e8f7 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 19 Jun 2025 11:29:50 +0200 Subject: [PATCH 20/27] chore: fixing cookiecutter input values Signed-off-by: benjdlambert --- .changeset/sixty-ties-matter.md | 5 +++++ .../src/actions/fetch/cookiecutter.ts | 11 ++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .changeset/sixty-ties-matter.md diff --git a/.changeset/sixty-ties-matter.md b/.changeset/sixty-ties-matter.md new file mode 100644 index 0000000000..3be0bb08a8 --- /dev/null +++ b/.changeset/sixty-ties-matter.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-cookiecutter': patch +--- + +Fixing the typescript issue with using `z.unknown()` diff --git a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts index 3a4ee8c3d7..9a9df95dea 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts +++ b/plugins/scaffolder-backend-module-cookiecutter/src/actions/fetch/cookiecutter.ts @@ -169,9 +169,14 @@ export function createFetchCookiecutterAction(options: { }) .optional(), values: z => - z.record(z.unknown(), { - description: 'Values to pass on to cookiecutter for templating', - }), + z + .object( + {}, + { + description: 'Values to pass on to cookiecutter for templating', + }, + ) + .passthrough(), copyWithoutRender: z => z .array(z.string(), { From 105722fe8ec8e40aa794763a1822a667910c4720 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 19 Jun 2025 11:36:19 +0200 Subject: [PATCH 21/27] chore: enter Signed-off-by: benjdlambert --- .changeset/pre.json | 207 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..60eb6ca6ce --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,207 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "example-app": "0.2.110", + "@backstage/app-defaults": "1.6.3", + "example-app-next": "0.0.24", + "app-next-example-plugin": "0.0.24", + "example-backend": "0.0.39", + "@backstage/backend-app-api": "1.2.4", + "@backstage/backend-defaults": "0.11.0", + "@backstage/backend-dev-utils": "0.1.5", + "@backstage/backend-dynamic-feature-service": "0.7.1", + "@backstage/backend-openapi-utils": "0.5.4", + "@backstage/backend-plugin-api": "1.4.0", + "@backstage/backend-test-utils": "1.6.0", + "@backstage/canon": "0.5.0", + "@backstage/catalog-client": "1.10.1", + "@backstage/catalog-model": "1.7.4", + "@backstage/cli": "0.33.0", + "@backstage/cli-common": "0.1.15", + "@backstage/cli-node": "0.2.13", + "@backstage/codemods": "0.1.52", + "@backstage/config": "1.3.2", + "@backstage/config-loader": "1.10.1", + "@backstage/core-app-api": "1.17.1", + "@backstage/core-compat-api": "0.4.3", + "@backstage/core-components": "0.17.3", + "@backstage/core-plugin-api": "1.10.8", + "@backstage/create-app": "0.7.0", + "@backstage/dev-utils": "1.1.11", + "e2e-test": "0.2.29", + "@backstage/e2e-test-utils": "0.1.1", + "@backstage/errors": "1.2.7", + "@backstage/eslint-plugin": "0.1.11", + "@backstage/frontend-app-api": "0.11.3", + "@backstage/frontend-defaults": "0.2.3", + "@backstage/frontend-dynamic-feature-loader": "0.1.2", + "@internal/frontend": "0.0.10", + "@backstage/frontend-plugin-api": "0.10.3", + "@backstage/frontend-test-utils": "0.3.3", + "@backstage/integration": "1.17.0", + "@backstage/integration-aws-node": "0.1.16", + "@backstage/integration-react": "1.2.8", + "@internal/opaque": "0.0.1", + "@backstage/release-manifests": "0.0.13", + "@backstage/repo-tools": "0.14.0", + "@internal/scaffolder": "0.0.10", + "@techdocs/cli": "1.9.4", + "techdocs-cli-embedded-app": "0.2.109", + "@backstage/test-utils": "1.7.9", + "@backstage/theme": "0.6.6", + "@backstage/types": "1.2.1", + "@backstage/version-bridge": "1.0.11", + "yarn-plugin-backstage": "0.0.6", + "@backstage/plugin-api-docs": "0.12.8", + "@backstage/plugin-api-docs-module-protoc-gen-doc": "0.1.10", + "@backstage/plugin-app": "0.1.10", + "@backstage/plugin-app-backend": "0.5.3", + "@backstage/plugin-app-node": "0.1.34", + "@backstage/plugin-app-visualizer": "0.1.20", + "@backstage/plugin-auth-backend": "0.25.1", + "@backstage/plugin-auth-backend-module-atlassian-provider": "0.4.4", + "@backstage/plugin-auth-backend-module-auth0-provider": "0.2.4", + "@backstage/plugin-auth-backend-module-aws-alb-provider": "0.4.4", + "@backstage/plugin-auth-backend-module-azure-easyauth-provider": "0.2.9", + "@backstage/plugin-auth-backend-module-bitbucket-provider": "0.3.4", + "@backstage/plugin-auth-backend-module-bitbucket-server-provider": "0.2.4", + "@backstage/plugin-auth-backend-module-cloudflare-access-provider": "0.4.4", + "@backstage/plugin-auth-backend-module-gcp-iap-provider": "0.4.4", + "@backstage/plugin-auth-backend-module-github-provider": "0.3.4", + "@backstage/plugin-auth-backend-module-gitlab-provider": "0.3.4", + "@backstage/plugin-auth-backend-module-google-provider": "0.3.4", + "@backstage/plugin-auth-backend-module-guest-provider": "0.2.9", + "@backstage/plugin-auth-backend-module-microsoft-provider": "0.3.4", + "@backstage/plugin-auth-backend-module-oauth2-provider": "0.4.4", + "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "0.2.9", + "@backstage/plugin-auth-backend-module-oidc-provider": "0.4.4", + "@backstage/plugin-auth-backend-module-okta-provider": "0.2.4", + "@backstage/plugin-auth-backend-module-onelogin-provider": "0.3.4", + "@backstage/plugin-auth-backend-module-pinniped-provider": "0.3.4", + "@backstage/plugin-auth-backend-module-vmware-cloud-provider": "0.5.4", + "@backstage/plugin-auth-node": "0.6.4", + "@backstage/plugin-auth-react": "0.1.16", + "@backstage/plugin-bitbucket-cloud-common": "0.3.0", + "@backstage/plugin-catalog": "1.31.0", + "@backstage/plugin-catalog-backend": "2.1.0", + "@backstage/plugin-catalog-backend-module-aws": "0.4.12", + "@backstage/plugin-catalog-backend-module-azure": "0.3.6", + "@backstage/plugin-catalog-backend-module-backstage-openapi": "0.5.3", + "@backstage/plugin-catalog-backend-module-bitbucket-cloud": "0.5.0", + "@backstage/plugin-catalog-backend-module-bitbucket-server": "0.5.0", + "@backstage/plugin-catalog-backend-module-gcp": "0.3.9", + "@backstage/plugin-catalog-backend-module-gerrit": "0.3.3", + "@backstage/plugin-catalog-backend-module-gitea": "0.1.1", + "@backstage/plugin-catalog-backend-module-github": "0.10.0", + "@backstage/plugin-catalog-backend-module-github-org": "0.3.11", + "@backstage/plugin-catalog-backend-module-gitlab": "0.7.0", + "@backstage/plugin-catalog-backend-module-gitlab-org": "0.2.10", + "@backstage/plugin-catalog-backend-module-incremental-ingestion": "0.7.1", + "@backstage/plugin-catalog-backend-module-ldap": "0.11.6", + "@backstage/plugin-catalog-backend-module-logs": "0.1.11", + "@backstage/plugin-catalog-backend-module-msgraph": "0.7.1", + "@backstage/plugin-catalog-backend-module-openapi": "0.2.11", + "@backstage/plugin-catalog-backend-module-puppetdb": "0.2.11", + "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "0.2.9", + "@backstage/plugin-catalog-backend-module-unprocessed": "0.6.1", + "@backstage/plugin-catalog-common": "1.1.4", + "@backstage/plugin-catalog-graph": "0.4.20", + "@backstage/plugin-catalog-import": "0.13.1", + "@backstage/plugin-catalog-node": "1.17.1", + "@backstage/plugin-catalog-react": "1.19.0", + "@backstage/plugin-catalog-unprocessed-entities": "0.2.18", + "@backstage/plugin-catalog-unprocessed-entities-common": "0.0.8", + "@backstage/plugin-config-schema": "0.1.69", + "@backstage/plugin-devtools": "0.1.28", + "@backstage/plugin-devtools-backend": "0.5.6", + "@backstage/plugin-devtools-common": "0.1.16", + "@backstage/plugin-events-backend": "0.5.3", + "@backstage/plugin-events-backend-module-aws-sqs": "0.4.12", + "@backstage/plugin-events-backend-module-azure": "0.2.21", + "@backstage/plugin-events-backend-module-bitbucket-cloud": "0.2.21", + "@backstage/plugin-events-backend-module-bitbucket-server": "0.1.2", + "@backstage/plugin-events-backend-module-gerrit": "0.2.21", + "@backstage/plugin-events-backend-module-github": "0.4.1", + "@backstage/plugin-events-backend-module-gitlab": "0.3.2", + "@backstage/plugin-events-backend-module-google-pubsub": "0.1.1", + "@backstage/plugin-events-backend-module-kafka": "0.1.0", + "@backstage/plugin-events-backend-test-utils": "0.1.45", + "@backstage/plugin-events-node": "0.4.12", + "@internal/plugin-todo-list": "1.0.40", + "@internal/plugin-todo-list-backend": "1.0.40", + "@internal/plugin-todo-list-common": "1.0.25", + "@backstage/plugin-gateway-backend": "1.0.2", + "@backstage/plugin-home": "0.8.9", + "@backstage/plugin-home-react": "0.1.27", + "@backstage/plugin-kubernetes": "0.12.8", + "@backstage/plugin-kubernetes-backend": "0.19.7", + "@backstage/plugin-kubernetes-cluster": "0.0.26", + "@backstage/plugin-kubernetes-common": "0.9.5", + "@backstage/plugin-kubernetes-node": "0.3.1", + "@backstage/plugin-kubernetes-react": "0.5.8", + "@backstage/plugin-mcp-actions-backend": "0.1.0", + "@backstage/plugin-notifications": "0.5.6", + "@backstage/plugin-notifications-backend": "0.5.7", + "@backstage/plugin-notifications-backend-module-email": "0.3.10", + "@backstage/plugin-notifications-backend-module-slack": "0.1.2", + "@backstage/plugin-notifications-common": "0.0.9", + "@backstage/plugin-notifications-node": "0.2.16", + "@backstage/plugin-org": "0.6.40", + "@backstage/plugin-org-react": "0.1.39", + "@backstage/plugin-permission-backend": "0.7.1", + "@backstage/plugin-permission-backend-module-allow-all-policy": "0.2.9", + "@backstage/plugin-permission-common": "0.9.0", + "@backstage/plugin-permission-node": "0.10.1", + "@backstage/plugin-permission-react": "0.4.35", + "@backstage/plugin-proxy-backend": "0.6.3", + "@backstage/plugin-proxy-node": "0.1.5", + "@backstage/plugin-scaffolder": "1.32.0", + "@backstage/plugin-scaffolder-backend": "2.0.0", + "@backstage/plugin-scaffolder-backend-module-azure": "0.2.10", + "@backstage/plugin-scaffolder-backend-module-bitbucket": "0.3.11", + "@backstage/plugin-scaffolder-backend-module-bitbucket-cloud": "0.2.10", + "@backstage/plugin-scaffolder-backend-module-bitbucket-server": "0.2.10", + "@backstage/plugin-scaffolder-backend-module-confluence-to-markdown": "0.3.10", + "@backstage/plugin-scaffolder-backend-module-cookiecutter": "0.3.11", + "@backstage/plugin-scaffolder-backend-module-gcp": "0.2.10", + "@backstage/plugin-scaffolder-backend-module-gerrit": "0.2.10", + "@backstage/plugin-scaffolder-backend-module-gitea": "0.2.10", + "@backstage/plugin-scaffolder-backend-module-github": "0.8.0", + "@backstage/plugin-scaffolder-backend-module-gitlab": "0.9.2", + "@backstage/plugin-scaffolder-backend-module-notifications": "0.1.11", + "@backstage/plugin-scaffolder-backend-module-rails": "0.5.10", + "@backstage/plugin-scaffolder-backend-module-sentry": "0.2.10", + "@backstage/plugin-scaffolder-backend-module-yeoman": "0.4.11", + "@backstage/plugin-scaffolder-common": "1.5.11", + "@backstage/plugin-scaffolder-node": "0.9.0", + "@backstage/plugin-scaffolder-node-test-utils": "0.3.0", + "@backstage/plugin-scaffolder-react": "1.17.0", + "@backstage/plugin-search": "1.4.27", + "@backstage/plugin-search-backend": "2.0.3", + "@backstage/plugin-search-backend-module-catalog": "0.3.5", + "@backstage/plugin-search-backend-module-elasticsearch": "1.7.3", + "@backstage/plugin-search-backend-module-explore": "0.3.3", + "@backstage/plugin-search-backend-module-pg": "0.5.45", + "@backstage/plugin-search-backend-module-stack-overflow-collator": "0.3.10", + "@backstage/plugin-search-backend-module-techdocs": "0.4.3", + "@backstage/plugin-search-backend-node": "1.3.12", + "@backstage/plugin-search-common": "1.2.18", + "@backstage/plugin-search-react": "1.9.1", + "@backstage/plugin-signals": "0.0.20", + "@backstage/plugin-signals-backend": "0.3.5", + "@backstage/plugin-signals-node": "0.1.21", + "@backstage/plugin-signals-react": "0.0.14", + "@backstage/plugin-techdocs": "1.13.0", + "@backstage/plugin-techdocs-addons-test-utils": "1.0.49", + "@backstage/plugin-techdocs-backend": "2.0.3", + "@backstage/plugin-techdocs-common": "0.1.1", + "@backstage/plugin-techdocs-module-addons-contrib": "1.1.25", + "@backstage/plugin-techdocs-node": "1.13.4", + "@backstage/plugin-techdocs-react": "1.3.0", + "@backstage/plugin-user-settings": "0.8.23", + "@backstage/plugin-user-settings-backend": "0.3.3", + "@backstage/plugin-user-settings-common": "0.0.1" + }, + "changesets": [] +} From 4b0189d67252177de684548726c87c72b0d5186e Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Thu, 19 Jun 2025 12:44:27 +0200 Subject: [PATCH 22/27] chore: fixing types for cookiecutter Signed-off-by: benjdlambert --- plugins/scaffolder-backend-module-cookiecutter/report.api.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/scaffolder-backend-module-cookiecutter/report.api.md b/plugins/scaffolder-backend-module-cookiecutter/report.api.md index 2dfba3f42f..a60a8c0396 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/report.api.md +++ b/plugins/scaffolder-backend-module-cookiecutter/report.api.md @@ -4,10 +4,12 @@ ```ts import { BackendFeature } from '@backstage/backend-plugin-api'; +import { objectOutputType } from 'zod'; import { ScmIntegrations } from '@backstage/integration'; import { TemplateAction } from '@backstage/plugin-scaffolder-node'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { Writable } from 'stream'; +import { ZodTypeAny } from 'zod'; // @public export interface ContainerRunner { @@ -47,7 +49,7 @@ export function createFetchCookiecutterAction(options: { }): TemplateAction< { url: string; - values: Record; + values: objectOutputType<{}, ZodTypeAny, 'passthrough'>; targetPath?: string | undefined; copyWithoutRender?: string[] | undefined; extensions?: string[] | undefined; From 5c805d338e9ef327e9df4873f1a1278879e20b02 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 15:37:33 +0100 Subject: [PATCH 23/27] Removed support for the as prop Signed-off-by: Charles de Dreuille --- packages/canon/css/button.css | 8 +- packages/canon/css/buttonicon.css | 14 ++ packages/canon/css/components.css | 113 ++-------- packages/canon/css/styles.css | 113 ++-------- packages/canon/report.api.md | 107 ++++----- .../src/components/Button/Button.stories.tsx | 23 -- .../canon/src/components/Button/Button.tsx | 43 +--- .../canon/src/components/Button/styles.css | 10 +- packages/canon/src/components/Button/types.ts | 27 +-- .../ButtonIcon.stories.tsx} | 51 ++--- .../ButtonIcon.tsx} | 41 +--- .../{IconButton => ButtonIcon}/index.tsx | 2 +- .../src/components/ButtonIcon/styles.css | 30 +++ .../{IconButton => ButtonIcon}/types.ts | 29 +-- .../ButtonLink/ButtonLink.stories.tsx | 212 ++++++++++++++++++ .../src/components/ButtonLink/ButtonLink.tsx | 71 ++++++ .../canon/src/components/ButtonLink/index.ts | 18 ++ .../canon/src/components/ButtonLink/types.ts | 35 +++ .../Pagination/DataTablePagination.tsx | 6 +- .../src/components/IconButton/styles.css | 107 --------- packages/canon/src/css/components.css | 2 +- packages/canon/src/index.ts | 2 +- packages/canon/src/types.ts | 18 -- 23 files changed, 528 insertions(+), 554 deletions(-) create mode 100644 packages/canon/css/buttonicon.css rename packages/canon/src/components/{IconButton/IconButton.stories.tsx => ButtonIcon/ButtonIcon.stories.tsx} (69%) rename packages/canon/src/components/{IconButton/IconButton.tsx => ButtonIcon/ButtonIcon.tsx} (59%) rename packages/canon/src/components/{IconButton => ButtonIcon}/index.tsx (95%) create mode 100644 packages/canon/src/components/ButtonIcon/styles.css rename packages/canon/src/components/{IconButton => ButtonIcon}/types.ts (56%) create mode 100644 packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx create mode 100644 packages/canon/src/components/ButtonLink/ButtonLink.tsx create mode 100644 packages/canon/src/components/ButtonLink/index.ts create mode 100644 packages/canon/src/components/ButtonLink/types.ts delete mode 100644 packages/canon/src/components/IconButton/styles.css diff --git a/packages/canon/css/button.css b/packages/canon/css/button.css index 7aaad3ec48..7f15f4a541 100644 --- a/packages/canon/css/button.css +++ b/packages/canon/css/button.css @@ -69,21 +69,21 @@ .canon-Button[data-size="medium"] { font-size: var(--canon-font-size-4); padding: 0 var(--canon-space-3); - height: 40px; + height: 2.5rem; } .canon-Button[data-size="small"] { font-size: var(--canon-font-size-3); padding: 0 var(--canon-space-2); - height: 32px; + height: 2rem; } -.canon-ButtonIcon[data-size="small"], .canon-ButtonIcon[data-size="small"] svg { +.canon-Button[data-size="small"] svg { width: 1rem; height: 1rem; } -.canon-ButtonIcon[data-size="medium"], .canon-ButtonIcon[data-size="medium"] svg { +.canon-Button[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } diff --git a/packages/canon/css/buttonicon.css b/packages/canon/css/buttonicon.css new file mode 100644 index 0000000000..1a791b481f --- /dev/null +++ b/packages/canon/css/buttonicon.css @@ -0,0 +1,14 @@ +.canon-ButtonIcon { + justify-content: center; + align-items: center; +} + +.canon-ButtonIcon[data-size="small"] { + width: 2rem; + padding: 0; +} + +.canon-ButtonIcon[data-size="medium"] { + width: 2.5rem; + padding: 0; +} diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index d420031ada..327f168696 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -125,25 +125,40 @@ .canon-Button[data-size="medium"] { font-size: var(--canon-font-size-4); padding: 0 var(--canon-space-3); - height: 40px; + height: 2.5rem; } .canon-Button[data-size="small"] { font-size: var(--canon-font-size-3); padding: 0 var(--canon-space-2); - height: 32px; + height: 2rem; } -.canon-ButtonIcon[data-size="small"], .canon-ButtonIcon[data-size="small"] svg { +.canon-Button[data-size="small"] svg { width: 1rem; height: 1rem; } -.canon-ButtonIcon[data-size="medium"], .canon-ButtonIcon[data-size="medium"] svg { +.canon-Button[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } +.canon-ButtonIcon { + justify-content: center; + align-items: center; +} + +.canon-ButtonIcon[data-size="small"] { + width: 2rem; + padding: 0; +} + +.canon-ButtonIcon[data-size="medium"] { + width: 2.5rem; + padding: 0; +} + .canon-CheckboxRoot { width: 1rem; height: 1rem; @@ -351,96 +366,6 @@ height: 1rem; } -.canon-IconButton { - user-select: none; - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-bold); - cursor: pointer; - border-radius: var(--canon-radius-2); - justify-content: center; - align-items: center; - gap: var(--canon-space-1_5); - border: none; - padding: 0; - display: inline-flex; - - &:disabled { - cursor: not-allowed; - } -} - -.canon-IconButton[data-variant="primary"] { - background-color: var(--canon-bg-solid); - color: var(--canon-fg-solid); - transition: background-color .15s, box-shadow .15s; - - &:hover { - background-color: var(--canon-bg-solid-hover); - } - - &:active { - background-color: var(--canon-bg-solid-pressed); - } - - &:focus-visible { - outline: 2px solid var(--canon-ring); - outline-offset: 2px; - } - - &:disabled { - background-color: var(--canon-bg-solid-disabled); - color: var(--canon-fg-solid-disabled); - } -} - -.canon-IconButton[data-variant="secondary"] { - background-color: var(--canon-bg-surface-1); - box-shadow: inset 0 0 0 1px var(--canon-border); - color: var(--canon-fg-primary); - transition: box-shadow .15s; - - &:hover { - box-shadow: inset 0 0 0 1px var(--canon-border-hover); - } - - &:active { - box-shadow: inset 0 0 0 1px var(--canon-border-pressed); - } - - &:focus-visible { - box-shadow: inset 0 0 0 2px var(--canon-ring); - outline: none; - transition: none; - } - - &:disabled { - box-shadow: inset 0 0 0 1px var(--canon-border-disabled); - color: var(--canon-fg-disabled); - } -} - -.canon-IconButton[data-size="medium"] { - font-size: var(--canon-font-size-4); - width: 40px; - height: 40px; -} - -.canon-IconButton[data-size="small"] { - font-size: var(--canon-font-size-3); - width: 32px; - height: 32px; -} - -.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg { - width: 1rem; - height: 1rem; -} - -.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg { - width: 1.25rem; - height: 1.25rem; -} - .canon-Link { font-family: var(--canon-font-regular); color: var(--canon-fg-link); diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index b2d111b185..9f32da5d66 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9349,25 +9349,40 @@ .canon-Button[data-size="medium"] { font-size: var(--canon-font-size-4); padding: 0 var(--canon-space-3); - height: 40px; + height: 2.5rem; } .canon-Button[data-size="small"] { font-size: var(--canon-font-size-3); padding: 0 var(--canon-space-2); - height: 32px; + height: 2rem; } -.canon-ButtonIcon[data-size="small"], .canon-ButtonIcon[data-size="small"] svg { +.canon-Button[data-size="small"] svg { width: 1rem; height: 1rem; } -.canon-ButtonIcon[data-size="medium"], .canon-ButtonIcon[data-size="medium"] svg { +.canon-Button[data-size="medium"] svg { width: 1.25rem; height: 1.25rem; } +.canon-ButtonIcon { + justify-content: center; + align-items: center; +} + +.canon-ButtonIcon[data-size="small"] { + width: 2rem; + padding: 0; +} + +.canon-ButtonIcon[data-size="medium"] { + width: 2.5rem; + padding: 0; +} + .canon-CheckboxRoot { width: 1rem; height: 1rem; @@ -9575,96 +9590,6 @@ height: 1rem; } -.canon-IconButton { - user-select: none; - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-bold); - cursor: pointer; - border-radius: var(--canon-radius-2); - justify-content: center; - align-items: center; - gap: var(--canon-space-1_5); - border: none; - padding: 0; - display: inline-flex; - - &:disabled { - cursor: not-allowed; - } -} - -.canon-IconButton[data-variant="primary"] { - background-color: var(--canon-bg-solid); - color: var(--canon-fg-solid); - transition: background-color .15s, box-shadow .15s; - - &:hover { - background-color: var(--canon-bg-solid-hover); - } - - &:active { - background-color: var(--canon-bg-solid-pressed); - } - - &:focus-visible { - outline: 2px solid var(--canon-ring); - outline-offset: 2px; - } - - &:disabled { - background-color: var(--canon-bg-solid-disabled); - color: var(--canon-fg-solid-disabled); - } -} - -.canon-IconButton[data-variant="secondary"] { - background-color: var(--canon-bg-surface-1); - box-shadow: inset 0 0 0 1px var(--canon-border); - color: var(--canon-fg-primary); - transition: box-shadow .15s; - - &:hover { - box-shadow: inset 0 0 0 1px var(--canon-border-hover); - } - - &:active { - box-shadow: inset 0 0 0 1px var(--canon-border-pressed); - } - - &:focus-visible { - box-shadow: inset 0 0 0 2px var(--canon-ring); - outline: none; - transition: none; - } - - &:disabled { - box-shadow: inset 0 0 0 1px var(--canon-border-disabled); - color: var(--canon-fg-disabled); - } -} - -.canon-IconButton[data-size="medium"] { - font-size: var(--canon-font-size-4); - width: 40px; - height: 40px; -} - -.canon-IconButton[data-size="small"] { - font-size: var(--canon-font-size-3); - width: 32px; - height: 32px; -} - -.canon-IconButtonIcon[data-size="small"], .canon-IconButtonIcon[data-size="small"] svg { - width: 1rem; - height: 1rem; -} - -.canon-IconButtonIcon[data-size="medium"], .canon-IconButtonIcon[data-size="medium"] svg { - width: 1.25rem; - height: 1.25rem; -} - .canon-Link { font-family: var(--canon-font-regular); color: var(--canon-fg-link); diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 96e9e31235..bd0b9cbee0 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -5,15 +5,14 @@ ```ts import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar'; import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; -import { Button as Button_2 } from 'react-aria-components'; +import { ButtonProps as ButtonProps_2 } from 'react-aria-components'; import { ChangeEvent } from 'react'; import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible'; import { ComponentProps } from 'react'; -import { ComponentPropsWithoutRef } from 'react'; -import { ComponentPropsWithRef } from 'react'; +import type { ComponentPropsWithRef } from 'react'; import { Context } from 'react'; import type { CSSProperties } from 'react'; -import { ElementType } from 'react'; +import type { ElementType } from 'react'; import { FC } from 'react'; import { FocusEvent as FocusEvent_2 } from 'react'; import { ForwardRefExoticComponent } from 'react'; @@ -44,11 +43,6 @@ export type ArbitraryStylingPropDef = { parseValue?: (value: string) => string | undefined; }; -// @public (undocumented) -export type As = { - as?: TAs; -}; - // @public (undocumented) export type AsProps = | 'div' @@ -158,32 +152,44 @@ export type Breakpoint = 'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; export const breakpoints: Breakpoint[]; // @public (undocumented) -export const Button: { - ( - props: ButtonProps & { - ref?: ComponentPropsWithRef['ref']; - }, - ): ReactElement; - displayName: string; -}; +export const Button: ForwardRefExoticComponent< + ButtonProps & RefAttributes +>; + +// @public (undocumented) +export const ButtonIcon: ForwardRefExoticComponent< + ButtonIconProps & RefAttributes +>; // @public -export type ButtonProps = PolymorphicComponentProps< - TAs, - { - children?: ReactNode; - size?: - | 'small' - | 'medium' - | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - iconStart?: ReactElement; - iconEnd?: ReactElement; - } ->; +export interface ButtonIconProps extends ButtonProps_2 { + // (undocumented) + icon?: ReactElement; + // (undocumented) + size?: 'small' | 'medium' | Partial>; + // (undocumented) + variant?: + | 'primary' + | 'secondary' + | Partial>; +} + +// @public +export interface ButtonProps extends ButtonProps_2 { + // (undocumented) + children?: ReactNode; + // (undocumented) + iconEnd?: ReactElement; + // (undocumented) + iconStart?: ReactElement; + // (undocumented) + size?: 'small' | 'medium' | Partial>; + // (undocumented) + variant?: + | 'primary' + | 'secondary' + | Partial>; +} // @public (undocumented) export const Checkbox: ForwardRefExoticComponent< @@ -692,33 +698,6 @@ export type HeightProps = GetPropDefTypes; // @public (undocumented) export const Icon: (props: IconProps) => JSX_2.Element | null; -// @public (undocumented) -export const IconButton: { - ( - props: IconButtonProps & { - ref?: ComponentPropsWithRef['ref']; - }, - ): ReactElement; - displayName: string; -}; - -// @public -export type IconButtonProps = - PolymorphicComponentProps< - TAs, - { - size?: - | 'small' - | 'medium' - | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - icon?: ReactElement; - } - >; - // @public (undocumented) export const IconContext: Context; @@ -1014,14 +993,6 @@ export const paddingPropDefs: (spacingValues: string[]) => { // @public (undocumented) export type PaddingProps = GetPropDefTypes; -// @public -export type PolymorphicComponentProps< - TAs extends ElementType, - TProps = {}, -> = TProps & - As & - Omit, keyof (As & TProps)>; - // @public (undocumented) export const positionPropDefs: { position: { diff --git a/packages/canon/src/components/Button/Button.stories.tsx b/packages/canon/src/components/Button/Button.stories.tsx index f28ebccc28..e020ebb64d 100644 --- a/packages/canon/src/components/Button/Button.stories.tsx +++ b/packages/canon/src/components/Button/Button.stories.tsx @@ -130,29 +130,6 @@ export const Disabled: Story = { ), }; -export const AsLink: Story = { - args: { - as: 'a', - children: 'I am a link', - href: 'https://canon.backstage.io', - target: '_blank', - }, -}; - -export const AsComponent: Story = { - render: () => { - const Link = (props: { children: React.ReactNode; to: string }) => ( - - ); - - return ( - - ); - }, -}; - export const Responsive: Story = { args: { children: 'Button', diff --git a/packages/canon/src/components/Button/Button.tsx b/packages/canon/src/components/Button/Button.tsx index 905e81111b..9d256a5815 100644 --- a/packages/canon/src/components/Button/Button.tsx +++ b/packages/canon/src/components/Button/Button.tsx @@ -15,22 +15,15 @@ */ import clsx from 'clsx'; -import { - ComponentPropsWithRef, - ElementType, - forwardRef, - ReactElement, - Ref, -} from 'react'; +import { forwardRef, Ref } from 'react'; import { Button as RAButton } from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import type { ButtonProps } from './types'; /** @public */ export const Button = forwardRef( - (props: ButtonProps, ref: Ref) => { + (props: ButtonProps, ref: Ref) => { const { - as, size = 'small', variant = 'primary', iconStart, @@ -40,45 +33,23 @@ export const Button = forwardRef( ...rest } = props; - const Component = as || RAButton; const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); return ( - - {iconStart && ( - - )} + {iconStart} {children} - {iconEnd && ( - - )} - + {iconEnd} + ); }, -) as { - ( - props: ButtonProps & { ref?: ComponentPropsWithRef['ref'] }, - ): ReactElement; - displayName: string; -}; +); Button.displayName = 'Button'; diff --git a/packages/canon/src/components/Button/styles.css b/packages/canon/src/components/Button/styles.css index a4afd85c9b..a2274fd907 100644 --- a/packages/canon/src/components/Button/styles.css +++ b/packages/canon/src/components/Button/styles.css @@ -85,23 +85,21 @@ .canon-Button[data-size='medium'] { font-size: var(--canon-font-size-4); padding: 0 var(--canon-space-3); - height: 40px; + height: 2.5rem; } .canon-Button[data-size='small'] { font-size: var(--canon-font-size-3); padding: 0 var(--canon-space-2); - height: 32px; + height: 2rem; } -.canon-ButtonIcon[data-size='small'], -.canon-ButtonIcon[data-size='small'] svg { +.canon-Button[data-size='small'] svg { width: 1rem; height: 1rem; } -.canon-ButtonIcon[data-size='medium'], -.canon-ButtonIcon[data-size='medium'] svg { +.canon-Button[data-size='medium'] svg { width: 1.25rem; height: 1.25rem; } diff --git a/packages/canon/src/components/Button/types.ts b/packages/canon/src/components/Button/types.ts index 327fa27bb6..ad89dbfe50 100644 --- a/packages/canon/src/components/Button/types.ts +++ b/packages/canon/src/components/Button/types.ts @@ -15,24 +15,21 @@ */ import { Breakpoint } from '@backstage/canon'; -import { ElementType, ReactElement, ReactNode } from 'react'; -import { PolymorphicComponentProps } from '../../types'; +import { ReactElement, ReactNode } from 'react'; +import { ButtonProps as RAButtonProps } from 'react-aria-components'; /** * Properties for {@link Button} * * @public */ -export type ButtonProps = PolymorphicComponentProps< - TAs, - { - children?: ReactNode; - size?: 'small' | 'medium' | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - iconStart?: ReactElement; - iconEnd?: ReactElement; - } ->; +export interface ButtonProps extends RAButtonProps { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + children?: ReactNode; +} diff --git a/packages/canon/src/components/IconButton/IconButton.stories.tsx b/packages/canon/src/components/ButtonIcon/ButtonIcon.stories.tsx similarity index 69% rename from packages/canon/src/components/IconButton/IconButton.stories.tsx rename to packages/canon/src/components/ButtonIcon/ButtonIcon.stories.tsx index c32ddb878a..c4c811eb92 100644 --- a/packages/canon/src/components/IconButton/IconButton.stories.tsx +++ b/packages/canon/src/components/ButtonIcon/ButtonIcon.stories.tsx @@ -15,14 +15,14 @@ */ import type { Meta, StoryObj } from '@storybook/react'; -import { IconButton } from './IconButton'; +import { ButtonIcon } from './ButtonIcon'; import { Flex } from '../Flex'; import { Text } from '../Text'; import { Icon } from '../Icon'; const meta = { - title: 'Components/IconButton', - component: IconButton, + title: 'Components/ButtonIcon', + component: ButtonIcon, argTypes: { size: { control: 'select', @@ -33,20 +33,20 @@ const meta = { options: ['primary', 'secondary'], }, }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; export const Default: Story = { - render: () => } />, + render: () => } />, }; export const Variants: Story = { render: () => ( - } variant="primary" /> - } variant="secondary" /> + } variant="primary" /> + } variant="secondary" /> ), }; @@ -54,8 +54,8 @@ export const Variants: Story = { export const Sizes: Story = { render: () => ( - } size="small" /> - } size="medium" /> + } size="small" /> + } size="medium" /> ), }; @@ -63,33 +63,12 @@ export const Sizes: Story = { export const Disabled: Story = { render: () => ( - } variant="primary" /> - } variant="secondary" /> + } variant="primary" /> + } variant="secondary" /> ), }; -export const AsLink: Story = { - render: () => ( - } - /> - ), -}; - -export const AsComponent: Story = { - render: () => { - const Link = (props: { children?: React.ReactNode; to: string }) => ( - - ); - - return } />; - }, -}; - export const Responsive: Story = { args: { variant: { @@ -101,7 +80,7 @@ export const Responsive: Story = { sm: 'medium', }, }, - render: args => } />, + render: args => } />, }; const variants = ['primary', 'secondary'] as const; @@ -115,20 +94,20 @@ export const Playground: Story = { {variant} {sizes.map(size => ( - } /> - } aria-label="Chevron right icon button" variant={variant} size={size} /> - } aria-label="Chevron right icon button" diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx similarity index 59% rename from packages/canon/src/components/IconButton/IconButton.tsx rename to packages/canon/src/components/ButtonIcon/ButtonIcon.tsx index 73c95ee131..0fc61acaf7 100644 --- a/packages/canon/src/components/IconButton/IconButton.tsx +++ b/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx @@ -15,22 +15,15 @@ */ import clsx from 'clsx'; -import { - ComponentPropsWithRef, - ElementType, - forwardRef, - ReactElement, - Ref, -} from 'react'; +import { forwardRef, Ref } from 'react'; import { Button as RAButton } from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; -import type { IconButtonProps } from './types'; +import type { ButtonIconProps } from './types'; /** @public */ -export const IconButton = forwardRef( - (props: IconButtonProps, ref: Ref) => { +export const ButtonIcon = forwardRef( + (props: ButtonIconProps, ref: Ref) => { const { - as, size = 'small', variant = 'primary', icon, @@ -39,33 +32,23 @@ export const IconButton = forwardRef( ...rest } = props; - const Component = as || RAButton; const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); + console.log(clsx('canon-Button', 'canon-ButtonIcon', className)); + return ( - - - + {icon} + ); }, -) as { - ( - props: IconButtonProps & { ref?: ComponentPropsWithRef['ref'] }, - ): ReactElement; - displayName: string; -}; +); -IconButton.displayName = 'IconButton'; +ButtonIcon.displayName = 'ButtonIcon'; diff --git a/packages/canon/src/components/IconButton/index.tsx b/packages/canon/src/components/ButtonIcon/index.tsx similarity index 95% rename from packages/canon/src/components/IconButton/index.tsx rename to packages/canon/src/components/ButtonIcon/index.tsx index 1d30c871a4..686fd844e8 100644 --- a/packages/canon/src/components/IconButton/index.tsx +++ b/packages/canon/src/components/ButtonIcon/index.tsx @@ -14,5 +14,5 @@ * limitations under the License. */ -export * from './IconButton'; +export * from './ButtonIcon'; export * from './types'; diff --git a/packages/canon/src/components/ButtonIcon/styles.css b/packages/canon/src/components/ButtonIcon/styles.css new file mode 100644 index 0000000000..27c96e696e --- /dev/null +++ b/packages/canon/src/components/ButtonIcon/styles.css @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.canon-ButtonIcon { + align-items: center; + justify-content: center; +} + +.canon-ButtonIcon[data-size='small'] { + padding: 0; + width: 2rem; +} + +.canon-ButtonIcon[data-size='medium'] { + padding: 0; + width: 2.5rem; +} diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/ButtonIcon/types.ts similarity index 56% rename from packages/canon/src/components/IconButton/types.ts rename to packages/canon/src/components/ButtonIcon/types.ts index 1d3deb94c2..7f8b845de4 100644 --- a/packages/canon/src/components/IconButton/types.ts +++ b/packages/canon/src/components/ButtonIcon/types.ts @@ -15,26 +15,19 @@ */ import { Breakpoint } from '@backstage/canon'; -import { ElementType, ReactElement } from 'react'; -import { PolymorphicComponentProps } from '../../types'; +import { ReactElement } from 'react'; +import { ButtonProps as RAButtonProps } from 'react-aria-components'; /** - * Properties for {@link IconButton} + * Properties for {@link ButtonIcon} * * @public */ -export type IconButtonProps = - PolymorphicComponentProps< - TAs, - { - size?: - | 'small' - | 'medium' - | Partial>; - variant?: - | 'primary' - | 'secondary' - | Partial>; - icon?: ReactElement; - } - >; +export interface ButtonIconProps extends RAButtonProps { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + icon?: ReactElement; +} diff --git a/packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx b/packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx new file mode 100644 index 0000000000..db2883c984 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/ButtonLink.stories.tsx @@ -0,0 +1,212 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import { ButtonLink } from './ButtonLink'; +import { Flex } from '../Flex'; +import { Text } from '../Text'; +import { Icon } from '../Icon'; + +const meta = { + title: 'Components/ButtonLink', + component: ButtonLink, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium'], + }, + variant: { + control: 'select', + options: ['primary', 'secondary'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: 'Button', + }, +}; + +export const Variants: Story = { + args: { + children: 'Button', + }, + parameters: { + argTypes: { + variant: { + control: false, + }, + }, + }, + render: () => ( + + } variant="primary"> + Button + + } variant="secondary"> + Button + + + ), +}; + +export const Sizes: Story = { + args: { + children: 'Button', + }, + render: () => ( + + }> + Small + + }> + Medium + + + ), +}; + +export const WithIcons: Story = { + args: { + children: 'Button', + }, + render: args => ( + + } /> + } /> + } + iconEnd={} + /> + + ), +}; + +export const FullWidth: Story = { + args: { + children: 'Button', + }, + render: args => ( + + } /> + } /> + } + iconEnd={} + /> + + ), +}; + +export const Disabled: Story = { + render: () => ( + + + Primary + + + Secondary + + + ), +}; + +export const Responsive: Story = { + args: { + children: 'Button', + variant: { + initial: 'primary', + sm: 'secondary', + }, + size: { + xs: 'small', + sm: 'medium', + }, + }, +}; + +const variants = ['primary', 'secondary'] as const; +const sizes = ['small', 'medium'] as const; + +export const Playground: Story = { + args: { + children: 'Button', + }, + render: () => ( + + {variants.map(variant => ( + + {variant} + {sizes.map(size => ( + + + Button + + } + variant={variant} + size={size} + > + Button + + } + variant={variant} + size={size} + > + Button + + } + iconEnd={} + style={{ width: '200px' }} + variant={variant} + size={size} + > + Button + + + Button + + } + variant={variant} + size={size} + isDisabled + > + Button + + } + variant={variant} + size={size} + isDisabled + > + Button + + + ))} + + ))} + + ), +}; diff --git a/packages/canon/src/components/ButtonLink/ButtonLink.tsx b/packages/canon/src/components/ButtonLink/ButtonLink.tsx new file mode 100644 index 0000000000..72eb78efe3 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/ButtonLink.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import clsx from 'clsx'; +import { forwardRef, Ref } from 'react'; +import { Link as RALink } from 'react-aria-components'; +import { useResponsiveValue } from '../../hooks/useResponsiveValue'; +import type { ButtonLinkProps } from './types'; + +/** @public */ +export const ButtonLink = forwardRef( + (props: ButtonLinkProps, ref: Ref) => { + const { + size = 'small', + variant = 'primary', + iconStart, + iconEnd, + children, + className, + ...rest + } = props; + + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); + + return ( + + {iconStart && ( + + )} + {children} + {iconEnd && ( + + )} + + ); + }, +); + +ButtonLink.displayName = 'ButtonLink'; diff --git a/packages/canon/src/components/ButtonLink/index.ts b/packages/canon/src/components/ButtonLink/index.ts new file mode 100644 index 0000000000..bbfc696c67 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './ButtonLink'; +export * from './types'; diff --git a/packages/canon/src/components/ButtonLink/types.ts b/packages/canon/src/components/ButtonLink/types.ts new file mode 100644 index 0000000000..f6524fbe62 --- /dev/null +++ b/packages/canon/src/components/ButtonLink/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Breakpoint } from '@backstage/canon'; +import { ReactElement, ReactNode } from 'react'; +import { LinkProps as RALinkProps } from 'react-aria-components'; + +/** + * Properties for {@link ButtonLink} + * + * @public + */ +export interface ButtonLinkProps extends RALinkProps { + size?: 'small' | 'medium' | Partial>; + variant?: + | 'primary' + | 'secondary' + | Partial>; + iconStart?: ReactElement; + iconEnd?: ReactElement; + children?: ReactNode; +} diff --git a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx index 452e0c34fd..ee9dda7ab5 100644 --- a/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx +++ b/packages/canon/src/components/DataTable/Pagination/DataTablePagination.tsx @@ -17,7 +17,7 @@ import { forwardRef } from 'react'; import { Text } from '../../Text'; import { DataTablePaginationProps } from './types'; -import { IconButton } from '../../IconButton'; +import { ButtonIcon } from '../../ButtonIcon'; import clsx from 'clsx'; import { Select } from '../../Select'; import { useDataTable } from '../Root/DataTableRoot'; @@ -71,14 +71,14 @@ const DataTablePagination = forwardRef(
{`${fromCount} - ${toCount} of ${rowCount}`} - table?.previousPage()} isDisabled={!table?.getCanPreviousPage()} icon={} /> - table?.nextPage()} diff --git a/packages/canon/src/components/IconButton/styles.css b/packages/canon/src/components/IconButton/styles.css deleted file mode 100644 index 066b0fbad5..0000000000 --- a/packages/canon/src/components/IconButton/styles.css +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -.canon-IconButton { - border: none; - display: inline-flex; - align-items: center; - justify-content: center; - user-select: none; - font-family: var(--canon-font-regular); - font-weight: var(--canon-font-weight-bold); - padding: 0; - cursor: pointer; - border-radius: var(--canon-radius-2); - gap: var(--canon-space-1_5); - - &:disabled { - cursor: not-allowed; - } -} - -.canon-IconButton[data-variant='primary'] { - background-color: var(--canon-bg-solid); - color: var(--canon-fg-solid); - transition: background-color 150ms ease, box-shadow 150ms ease; - - &:hover { - background-color: var(--canon-bg-solid-hover); - } - - &:active { - background-color: var(--canon-bg-solid-pressed); - } - - &:focus-visible { - outline: 2px solid var(--canon-ring); - outline-offset: 2px; - } - - &:disabled { - background-color: var(--canon-bg-solid-disabled); - color: var(--canon-fg-solid-disabled); - } -} - -.canon-IconButton[data-variant='secondary'] { - background-color: var(--canon-bg-surface-1); - box-shadow: inset 0 0 0 1px var(--canon-border); - color: var(--canon-fg-primary); - transition: box-shadow 150ms ease; - - &:hover { - box-shadow: inset 0 0 0 1px var(--canon-border-hover); - } - - &:active { - box-shadow: inset 0 0 0 1px var(--canon-border-pressed); - } - - &:focus-visible { - outline: none; - transition: none; - box-shadow: inset 0 0 0 2px var(--canon-ring); - } - - &:disabled { - box-shadow: inset 0 0 0 1px var(--canon-border-disabled); - color: var(--canon-fg-disabled); - } -} - -.canon-IconButton[data-size='medium'] { - font-size: var(--canon-font-size-4); - height: 40px; - width: 40px; -} - -.canon-IconButton[data-size='small'] { - font-size: var(--canon-font-size-3); - height: 32px; - width: 32px; -} - -.canon-IconButtonIcon[data-size='small'], -.canon-IconButtonIcon[data-size='small'] svg { - width: 1rem; - height: 1rem; -} - -.canon-IconButtonIcon[data-size='medium'], -.canon-IconButtonIcon[data-size='medium'] svg { - width: 1.25rem; - height: 1.25rem; -} diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index c85c1bbdd9..aa453f7314 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -17,6 +17,7 @@ @import '../components/Avatar/Avatar.styles.css'; @import '../components/Box/styles.css'; @import '../components/Button/styles.css'; +@import '../components/ButtonIcon/styles.css'; @import '../components/Checkbox/styles.css'; @import '../components/Collapsible/Collapsible.styles.css'; @import '../components/Container/styles.css'; @@ -27,7 +28,6 @@ @import '../components/Grid/styles.css'; @import '../components/Heading/styles.css'; @import '../components/Icon/styles.css'; -@import '../components/IconButton/styles.css'; @import '../components/Link/styles.css'; @import '../components/Menu/Menu.styles.css'; @import '../components/Table/styles.css'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 9b5a454254..69c302bdc5 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -38,7 +38,7 @@ export * from './components/Collapsible'; export * from './components/DataTable'; export * from './components/FieldLabel'; export * from './components/Icon'; -export * from './components/IconButton'; +export * from './components/ButtonIcon'; export * from './components/Checkbox'; export * from './components/Table'; export * from './components/Tabs'; diff --git a/packages/canon/src/types.ts b/packages/canon/src/types.ts index 7e542f3635..ee5d954576 100644 --- a/packages/canon/src/types.ts +++ b/packages/canon/src/types.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { ComponentPropsWithoutRef, ElementType } from 'react'; - /** @public */ export type AsProps = | 'div' @@ -133,19 +131,3 @@ export interface UtilityProps extends SpaceProps { justifyContent?: Responsive; rowSpan?: Responsive; } - -/** @public */ -export type As = { - as?: TAs; -}; - -/** - * This is the first reusable type utility we built - * @public - */ -export type PolymorphicComponentProps< - TAs extends ElementType, - TProps = {}, -> = TProps & - As & - Omit, keyof (As & TProps)>; From 65993b0a74ed110e4a5456645ae40e40b1f14803 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 17:17:38 +0100 Subject: [PATCH 24/27] Improve docs Signed-off-by: Charles de Dreuille --- .../src/components/Sidebar/Sidebar.module.css | 4 - .../src/components/Sidebar/playground.tsx | 5 +- .../src/content/components/button-icon.mdx | 80 ++++++++++++++++ .../content/components/button-icon.props.ts | 59 ++++++++++++ .../src/content/components/button-link.mdx | 91 ++++++++++++++++++ .../content/components/button-link.props.ts | 73 +++++++++++++++ canon-docs/src/content/components/button.mdx | 19 +--- .../src/content/components/button.props.ts | 36 ++++---- .../src/content/components/icon-button.mdx | 92 ------------------- .../content/components/icon-button.props.ts | 57 ------------ canon-docs/src/snippets/stories-snippets.tsx | 36 +++++--- canon-docs/src/utils/data.ts | 15 ++- .../src/components/ButtonIcon/ButtonIcon.tsx | 2 - .../ButtonLink/ButtonLink.stories.tsx | 24 ++--- .../src/components/ButtonLink/ButtonLink.tsx | 20 +--- 15 files changed, 372 insertions(+), 241 deletions(-) create mode 100644 canon-docs/src/content/components/button-icon.mdx create mode 100644 canon-docs/src/content/components/button-icon.props.ts create mode 100644 canon-docs/src/content/components/button-link.mdx create mode 100644 canon-docs/src/content/components/button-link.props.ts delete mode 100644 canon-docs/src/content/components/icon-button.mdx delete mode 100644 canon-docs/src/content/components/icon-button.props.ts diff --git a/canon-docs/src/components/Sidebar/Sidebar.module.css b/canon-docs/src/components/Sidebar/Sidebar.module.css index e558621670..4eb49682df 100644 --- a/canon-docs/src/components/Sidebar/Sidebar.module.css +++ b/canon-docs/src/components/Sidebar/Sidebar.module.css @@ -55,10 +55,6 @@ gap: 2px; } -.playgroundSection { - position: absolute; -} - .sectionTitle { font-size: var(--canon-font-size-3); font-weight: var(--canon-font-weight-bold); diff --git a/canon-docs/src/components/Sidebar/playground.tsx b/canon-docs/src/components/Sidebar/playground.tsx index eb1c3213cc..2aaa4fe2c6 100644 --- a/canon-docs/src/components/Sidebar/playground.tsx +++ b/canon-docs/src/components/Sidebar/playground.tsx @@ -38,9 +38,7 @@ export const Playground = () => { return ( { visibility: isPlayground ? 'visible' : 'hidden', }} transition={{ duration: 0.2 }} + style={{ position: 'absolute' }} >
Components
{components.map(({ slug, title }) => ( diff --git a/canon-docs/src/content/components/button-icon.mdx b/canon-docs/src/content/components/button-icon.mdx new file mode 100644 index 0000000000..2281814a89 --- /dev/null +++ b/canon-docs/src/content/components/button-icon.mdx @@ -0,0 +1,80 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { CodeBlock } from '@/components/CodeBlock'; +import { ButtonIconSnippet } from '@/snippets/stories-snippets'; +import { + buttonIconPropDefs, + buttonIconUsageSnippet, + buttonIconDefaultSnippet, + buttonIconVariantsSnippet, + buttonIconSizesSnippet, + buttonIconDisabledSnippet, + buttonIconResponsiveSnippet, + buttonIconAsLinkSnippet, +} from './button-icon.props'; +import { ComponentInfos } from '@/components/ComponentInfos'; + +# Icon Button + +A button component with a single icon that can be used to trigger actions. + +} + code={buttonIconDefaultSnippet} +/> + + + +## API reference + + + +## Examples + +### Variants + +Here's a view when buttons have different variants. + +} + code={buttonIconVariantsSnippet} +/> + +### Sizes + +Here's a view when buttons have different sizes. + +} + code={buttonIconSizesSnippet} +/> + +### Disabled + +Here's a view when buttons are disabled. + +} + code={buttonIconDisabledSnippet} +/> + +### Responsive + +Here's a view when buttons are responsive. + + diff --git a/canon-docs/src/content/components/button-icon.props.ts b/canon-docs/src/content/components/button-icon.props.ts new file mode 100644 index 0000000000..160690a29e --- /dev/null +++ b/canon-docs/src/content/components/button-icon.props.ts @@ -0,0 +1,59 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const buttonIconPropDefs: Record = { + variant: { + type: 'enum', + values: ['primary', 'secondary'], + default: 'primary', + responsive: true, + }, + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'medium', + responsive: true, + }, + icon: { type: 'enum', values: ['ReactNode'], responsive: false }, + isDisabled: { type: 'boolean', default: 'false', responsive: false }, + type: { + type: 'enum', + values: ['button', 'submit', 'reset'], + default: 'button', + responsive: false, + }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const buttonIconUsageSnippet = `import { ButtonIcon } from '@backstage/canon'; + +`; + +export const buttonIconDefaultSnippet = ` + } variant="primary" /> + } variant="secondary" /> +`; + +export const buttonIconVariantsSnippet = ` + } variant="primary" /> + } variant="secondary" /> +`; + +export const buttonIconSizesSnippet = ` + } size="small" /> + } size="medium" /> +`; + +export const buttonIconDisabledSnippet = `} disabled />`; + +export const buttonIconResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; + +export const buttonIconAsLinkSnippet = `import { ButtonLink } from '@backstage/canon'; + + + Button +`; diff --git a/canon-docs/src/content/components/button-link.mdx b/canon-docs/src/content/components/button-link.mdx new file mode 100644 index 0000000000..3f24874220 --- /dev/null +++ b/canon-docs/src/content/components/button-link.mdx @@ -0,0 +1,91 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { CodeBlock } from '@/components/CodeBlock'; +import { ButtonLinkSnippet } from '@/snippets/stories-snippets'; +import { + buttonLinkPropDefs, + buttonLinkSnippetUsage, + buttonLinkVariantsSnippet, + buttonLinkSizesSnippet, + buttonLinkIconsSnippet, + buttonLinkDisabledSnippet, + buttonLinkResponsiveSnippet, +} from './button-link.props'; +import { ComponentInfos } from '@/components/ComponentInfos'; + +# ButtonLink + +A button component that can be used as a link. + +} + code={buttonLinkVariantsSnippet} +/> + + + +## API reference + + + +## Examples + +### Variants + +Here's a view when buttons have different variants. + +} + code={buttonLinkVariantsSnippet} +/> + +### Sizes + +Here's a view when buttons have different sizes. + +} + code={buttonLinkSizesSnippet} +/> + +### With Icons + +Here's a view when buttons have icons. + +} + code={buttonLinkIconsSnippet} +/> + +### Disabled + +Here's a view when buttons are disabled. + +} + code={buttonLinkDisabledSnippet} +/> + +### Responsive + +Here's a view when buttons are responsive. + + diff --git a/canon-docs/src/content/components/button-link.props.ts b/canon-docs/src/content/components/button-link.props.ts new file mode 100644 index 0000000000..94d696b736 --- /dev/null +++ b/canon-docs/src/content/components/button-link.props.ts @@ -0,0 +1,73 @@ +import { classNamePropDefs, stylePropDefs } from '../../utils/propDefs'; +import type { PropDef } from '../../utils/propDefs'; + +export const buttonLinkPropDefs: Record = { + variant: { + type: 'enum', + values: ['primary', 'secondary'], + default: 'primary', + responsive: true, + }, + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'medium', + responsive: true, + }, + iconStart: { type: 'enum', values: ['ReactNode'], responsive: false }, + iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false }, + isDisabled: { type: 'boolean', default: 'false', responsive: false }, + href: { type: 'string', responsive: false }, + hrefLang: { type: 'string', responsive: false }, + target: { + type: 'enum', + values: ['HTMLAttributeAnchorTarget'], + default: '_self', + responsive: false, + }, + rel: { type: 'string', responsive: false }, + children: { type: 'enum', values: ['ReactNode'], responsive: false }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/canon'; + +`; + +export const buttonLinkVariantsSnippet = ` + + Button + + + Button + +`; + +export const buttonLinkSizesSnippet = ` + Small + Medium +`; + +export const buttonLinkIconsSnippet = ` + }>Button + }>Button + } + iconEnd={}> + Button + +`; + +export const buttonLinkDisabledSnippet = ` + + Primary + + + Secondary + +`; + +export const buttonLinkResponsiveSnippet = ` + Responsive Button +`; diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx index 6690a90e23..89d3c3f612 100644 --- a/canon-docs/src/content/components/button.mdx +++ b/canon-docs/src/content/components/button.mdx @@ -1,14 +1,13 @@ import { PropsTable } from '@/components/PropsTable'; import { Snippet } from '@/components/Snippet'; import { CodeBlock } from '@/components/CodeBlock'; -import { ButtonSnippet } from '@/snippets/stories-snippets'; +import { ButtonSnippet, ButtonLinkSnippet } from '@/snippets/stories-snippets'; import { buttonPropDefs, buttonSnippetUsage, buttonVariantsSnippet, buttonSizesSnippet, buttonIconsSnippet, - buttonFullWidthSnippet, buttonDisabledSnippet, buttonResponsiveSnippet, buttonAsLinkSnippet, @@ -74,18 +73,6 @@ Here's a view when buttons have icons. code={buttonIconsSnippet} /> -### Full width - -Here's a view when buttons are full width. - -} - code={buttonFullWidthSnippet} -/> - ### Disabled Here's a view when buttons are disabled. @@ -106,12 +93,12 @@ Here's a view when buttons are responsive. ### As Link -You can use the `as` prop to make a button act as a link. +If you want to use a button as a link, please use the `ButtonLink` component. } + preview={} code={buttonAsLinkSnippet} /> diff --git a/canon-docs/src/content/components/button.props.ts b/canon-docs/src/content/components/button.props.ts index ef72a70596..b32cba4725 100644 --- a/canon-docs/src/content/components/button.props.ts +++ b/canon-docs/src/content/components/button.props.ts @@ -16,7 +16,14 @@ export const buttonPropDefs: Record = { }, iconStart: { type: 'enum', values: ['ReactNode'], responsive: false }, iconEnd: { type: 'enum', values: ['ReactNode'], responsive: false }, + isDisabled: { type: 'boolean', default: 'false', responsive: false }, children: { type: 'enum', values: ['ReactNode'], responsive: false }, + type: { + type: 'enum', + values: ['button', 'submit', 'reset'], + default: 'button', + responsive: false, + }, ...classNamePropDefs, ...stylePropDefs, }; @@ -32,9 +39,6 @@ export const buttonVariantsSnippet = ` - `; export const buttonSizesSnippet = ` @@ -43,20 +47,16 @@ export const buttonSizesSnippet = ` `; export const buttonIconsSnippet = ` - - - -`; - -export const buttonFullWidthSnippet = ` - + + + `; export const buttonDisabledSnippet = ` - - `; @@ -65,12 +65,8 @@ export const buttonResponsiveSnippet = ` +export const buttonAsLinkSnippet = `import { ButtonLink } from '@backstage/canon'; -// Using a custom component -`; + + Button +`; diff --git a/canon-docs/src/content/components/icon-button.mdx b/canon-docs/src/content/components/icon-button.mdx deleted file mode 100644 index d8eb3beab2..0000000000 --- a/canon-docs/src/content/components/icon-button.mdx +++ /dev/null @@ -1,92 +0,0 @@ -import { PropsTable } from '@/components/PropsTable'; -import { Snippet } from '@/components/Snippet'; -import { CodeBlock } from '@/components/CodeBlock'; -import { IconButtonSnippet } from '@/snippets/stories-snippets'; -import { - iconButtonPropDefs, - iconButtonUsageSnippet, - iconButtonDefaultSnippet, - iconButtonVariantsSnippet, - iconButtonSizesSnippet, - iconButtonDisabledSnippet, - iconButtonResponsiveSnippet, - iconButtonAsLinkSnippet, -} from './icon-button.props'; -import { ComponentInfos } from '@/components/ComponentInfos'; - -# Icon Button - -A button component with a single icon that can be used to trigger actions. - -} - code={iconButtonDefaultSnippet} -/> - - - -## API reference - - - -## Examples - -### Variants - -Here's a view when buttons have different variants. - -} - code={iconButtonVariantsSnippet} -/> - -### Sizes - -Here's a view when buttons have different sizes. - -} - code={iconButtonSizesSnippet} -/> - -### Disabled - -Here's a view when buttons are disabled. - -} - code={iconButtonDisabledSnippet} -/> - -### Responsive - -Here's a view when buttons are responsive. - - - -### As Link - -You can use the `as` prop to make a button act as a link. - -} - code={iconButtonAsLinkSnippet} -/> diff --git a/canon-docs/src/content/components/icon-button.props.ts b/canon-docs/src/content/components/icon-button.props.ts deleted file mode 100644 index 9e3a2818fe..0000000000 --- a/canon-docs/src/content/components/icon-button.props.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - classNamePropDefs, - stylePropDefs, - type PropDef, -} from '@/utils/propDefs'; - -export const iconButtonPropDefs: Record = { - variant: { - type: 'enum', - values: ['primary', 'secondary'], - default: 'primary', - responsive: true, - }, - size: { - type: 'enum', - values: ['small', 'medium'], - default: 'medium', - responsive: true, - }, - icon: { type: 'enum', values: ['ReactNode'], responsive: false }, - ...classNamePropDefs, - ...stylePropDefs, -}; - -export const iconButtonUsageSnippet = `import { IconButton } from '@backstage/canon'; - -`; - -export const iconButtonDefaultSnippet = ` - } variant="primary" /> - } variant="secondary" /> -`; - -export const iconButtonVariantsSnippet = ` - } variant="primary" /> - } variant="secondary" /> -`; - -export const iconButtonSizesSnippet = ` - } size="small" /> - } size="medium" /> -`; - -export const iconButtonDisabledSnippet = `} disabled />`; - -export const iconButtonResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; - -export const iconButtonAsLinkSnippet = `// Using the \`as\` prop -} -/> - -// Using a custom component -} />`; diff --git a/canon-docs/src/snippets/stories-snippets.tsx b/canon-docs/src/snippets/stories-snippets.tsx index 95738a6330..e5a7464b43 100644 --- a/canon-docs/src/snippets/stories-snippets.tsx +++ b/canon-docs/src/snippets/stories-snippets.tsx @@ -3,11 +3,12 @@ import { composeStories } from '@storybook/react'; import * as BoxStories from '../../../packages/canon/src/components/Box/Box.stories'; import * as ButtonStories from '../../../packages/canon/src/components/Button/Button.stories'; +import * as ButtonIconStories from '../../../packages/canon/src/components/ButtonIcon/ButtonIcon.stories'; +import * as ButtonLinkStories from '../../../packages/canon/src/components/ButtonLink/ButtonLink.stories'; import * as CheckboxStories from '../../../packages/canon/src/components/Checkbox/Checkbox.stories'; import * as ContainerStories from '../../../packages/canon/src/components/Container/Container.stories'; import * as GridStories from '../../../packages/canon/src/components/Grid/Grid.stories'; import * as HeadingStories from '../../../packages/canon/src/components/Heading/Heading.stories'; -import * as IconButtonStories from '../../../packages/canon/src/components/IconButton/IconButton.stories'; import * as IconStories from '../../../packages/canon/src/components/Icon/Icon.stories'; import * as TextFieldStories from '../../../packages/canon/src/components/TextField/TextField.stories'; import * as TextStories from '../../../packages/canon/src/components/Text/Text.stories'; @@ -38,6 +39,28 @@ export const ButtonSnippet = ({ return StoryComponent ? : null; }; +export const ButtonIconSnippet = ({ + story, +}: { + story: keyof typeof ButtonIconStories; +}) => { + const stories = composeStories(ButtonIconStories); + const StoryComponent = stories[story as keyof typeof stories]; + + return StoryComponent ? : null; +}; + +export const ButtonLinkSnippet = ({ + story, +}: { + story: keyof typeof ButtonLinkStories; +}) => { + const stories = composeStories(ButtonLinkStories); + const StoryComponent = stories[story as keyof typeof stories]; + + return StoryComponent ? : null; +}; + export const CheckboxSnippet = ({ story, }: { @@ -96,17 +119,6 @@ export const HeadingSnippet = ({ return StoryComponent ? : null; }; -export const IconButtonSnippet = ({ - story, -}: { - story: keyof typeof IconButtonStories; -}) => { - const stories = composeStories(IconButtonStories); - const StoryComponent = stories[story as keyof typeof stories]; - - return StoryComponent ? : null; -}; - export const IconSnippet = ({ story }: { story: keyof typeof IconStories }) => { const stories = composeStories(IconStories); const StoryComponent = stories[story as keyof typeof stories]; diff --git a/canon-docs/src/utils/data.ts b/canon-docs/src/utils/data.ts index 20ff9146a7..a741ad5160 100644 --- a/canon-docs/src/utils/data.ts +++ b/canon-docs/src/utils/data.ts @@ -76,6 +76,16 @@ export const components: Page[] = [ slug: 'button', status: 'alpha', }, + { + title: 'ButtonIcon', + slug: 'button-icon', + status: 'alpha', + }, + { + title: 'ButtonLink', + slug: 'button-link', + status: 'alpha', + }, { title: 'Checkbox', slug: 'checkbox', @@ -96,11 +106,6 @@ export const components: Page[] = [ slug: 'icon', status: 'alpha', }, - { - title: 'IconButton', - slug: 'icon-button', - status: 'alpha', - }, { title: 'Link', slug: 'link', diff --git a/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx b/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx index 0fc61acaf7..43c2f598ac 100644 --- a/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx +++ b/packages/canon/src/components/ButtonIcon/ButtonIcon.tsx @@ -35,8 +35,6 @@ export const ButtonIcon = forwardRef( const responsiveSize = useResponsiveValue(size); const responsiveVariant = useResponsiveValue(variant); - console.log(clsx('canon-Button', 'canon-ButtonIcon', className)); - return ( ( - } variant="primary"> + } + variant="primary" + href="https://canon.backstage.io" + target="_blank" + > Button - } variant="secondary"> + } + variant="secondary" + href="https://canon.backstage.io" + target="_blank" + > Button diff --git a/packages/canon/src/components/ButtonLink/ButtonLink.tsx b/packages/canon/src/components/ButtonLink/ButtonLink.tsx index 72eb78efe3..cb68401160 100644 --- a/packages/canon/src/components/ButtonLink/ButtonLink.tsx +++ b/packages/canon/src/components/ButtonLink/ButtonLink.tsx @@ -44,25 +44,9 @@ export const ButtonLink = forwardRef( ref={ref} {...rest} > - {iconStart && ( - - )} + {iconStart} {children} - {iconEnd && ( - - )} + {iconEnd} ); }, From 71105b9bc4e93b04ba8fe4f1860e857fe3d89e33 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 17:22:02 +0100 Subject: [PATCH 25/27] Fix some props Signed-off-by: Charles de Dreuille --- canon-docs/src/content/components/button-icon.mdx | 4 ++-- canon-docs/src/content/components/button-icon.props.ts | 2 +- canon-docs/src/content/components/button-link.mdx | 2 +- canon-docs/src/content/components/button-link.props.ts | 4 ++-- canon-docs/src/content/components/button.mdx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/canon-docs/src/content/components/button-icon.mdx b/canon-docs/src/content/components/button-icon.mdx index 2281814a89..b020b19f33 100644 --- a/canon-docs/src/content/components/button-icon.mdx +++ b/canon-docs/src/content/components/button-icon.mdx @@ -14,7 +14,7 @@ import { } from './button-icon.props'; import { ComponentInfos } from '@/components/ComponentInfos'; -# Icon Button +# ButtonIcon A button component with a single icon that can be used to trigger actions. @@ -27,7 +27,7 @@ A button component with a single icon that can be used to trigger actions. diff --git a/canon-docs/src/content/components/button-icon.props.ts b/canon-docs/src/content/components/button-icon.props.ts index 160690a29e..a21bf3cd57 100644 --- a/canon-docs/src/content/components/button-icon.props.ts +++ b/canon-docs/src/content/components/button-icon.props.ts @@ -48,7 +48,7 @@ export const buttonIconSizesSnippet = ` } size="medium" /> `; -export const buttonIconDisabledSnippet = `} disabled />`; +export const buttonIconDisabledSnippet = `} isDisabled />`; export const buttonIconResponsiveSnippet = `} variant={{ initial: 'primary', lg: 'secondary' }} />`; diff --git a/canon-docs/src/content/components/button-link.mdx b/canon-docs/src/content/components/button-link.mdx index 3f24874220..f6da987941 100644 --- a/canon-docs/src/content/components/button-link.mdx +++ b/canon-docs/src/content/components/button-link.mdx @@ -26,7 +26,7 @@ A button component that can be used as a link. diff --git a/canon-docs/src/content/components/button-link.props.ts b/canon-docs/src/content/components/button-link.props.ts index 94d696b736..b92b3154e5 100644 --- a/canon-docs/src/content/components/button-link.props.ts +++ b/canon-docs/src/content/components/button-link.props.ts @@ -36,10 +36,10 @@ export const buttonLinkSnippetUsage = `import { ButtonLink } from '@backstage/ca `; export const buttonLinkVariantsSnippet = ` - + } variant="primary"> Button - + } variant="secondary"> Button `; diff --git a/canon-docs/src/content/components/button.mdx b/canon-docs/src/content/components/button.mdx index 89d3c3f612..c0053d0f5e 100644 --- a/canon-docs/src/content/components/button.mdx +++ b/canon-docs/src/content/components/button.mdx @@ -27,7 +27,7 @@ A button component that can be used to trigger actions. From afb728d8b7bce848fcd9103dc7c3e00de1ec5eed Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 17:30:20 +0100 Subject: [PATCH 26/27] Update public-nails-melt.md Signed-off-by: Charles de Dreuille --- .changeset/public-nails-melt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md index 169bf7fbfb..b4c77e764e 100644 --- a/.changeset/public-nails-melt.md +++ b/.changeset/public-nails-melt.md @@ -2,4 +2,4 @@ '@backstage/canon': minor --- -**Breaking** We are transforming the way we handle custom tags for both Button and IconButton. We are introducing a new as prop for both of these components and we are removing support for the render prop. +**Breaking** We are transforming our Button component to allow for beatter support for button links. Instead of using the render prop, we are introducing the ButtonLin component and renaming IconButton to ButtonIcon to make it consistent. We are also removing the render prop on all button components. From 072c25dd51bc7b835483ff8171a3272b29ee23cb Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 17:37:49 +0100 Subject: [PATCH 27/27] Update public-nails-melt.md Signed-off-by: Charles de Dreuille --- .changeset/public-nails-melt.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.changeset/public-nails-melt.md b/.changeset/public-nails-melt.md index b4c77e764e..1529ee94d0 100644 --- a/.changeset/public-nails-melt.md +++ b/.changeset/public-nails-melt.md @@ -2,4 +2,12 @@ '@backstage/canon': minor --- -**Breaking** We are transforming our Button component to allow for beatter support for button links. Instead of using the render prop, we are introducing the ButtonLin component and renaming IconButton to ButtonIcon to make it consistent. We are also removing the render prop on all button components. +**BREAKING CHANGES** + +We’re updating our Button component to provide better support for button links. + +- We’re introducing a new `ButtonLink` component, which replaces the previous render prop pattern. +- To maintain naming consistency across components, `IconButton` is being renamed to `ButtonIcon`. +- Additionally, the render prop will be removed from all button-related components. + +These changes aim to simplify usage and improve clarity in our component API.