From 1e4dfdb0aec2cfd3c81d65ea29443e967b6df341 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 16 Mar 2025 06:54:21 +0000 Subject: [PATCH] Add new IconButton component Signed-off-by: Charles de Dreuille --- .changeset/fair-oranges-leave.md | 5 + packages/canon/report.api.md | 34 +++++ .../components/IconButton/IconButton.props.ts | 41 +++++ .../IconButton/IconButton.stories.tsx | 140 ++++++++++++++++++ .../src/components/IconButton/IconButton.tsx | 57 +++++++ .../canon/src/components/IconButton/index.tsx | 20 +++ .../src/components/IconButton/styles.css | 105 +++++++++++++ .../canon/src/components/IconButton/types.ts | 42 ++++++ packages/canon/src/css/components.css | 1 + packages/canon/src/index.ts | 1 + 10 files changed, 446 insertions(+) create mode 100644 .changeset/fair-oranges-leave.md create mode 100644 packages/canon/src/components/IconButton/IconButton.props.ts create mode 100644 packages/canon/src/components/IconButton/IconButton.stories.tsx create mode 100644 packages/canon/src/components/IconButton/IconButton.tsx create mode 100644 packages/canon/src/components/IconButton/index.tsx create mode 100644 packages/canon/src/components/IconButton/styles.css create mode 100644 packages/canon/src/components/IconButton/types.ts diff --git a/.changeset/fair-oranges-leave.md b/.changeset/fair-oranges-leave.md new file mode 100644 index 0000000000..43fd97f925 --- /dev/null +++ b/.changeset/fair-oranges-leave.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': minor +--- + +We added a new IconButton component with fixed sizes showcasing a single icon. diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index 4f4a52ffd3..5262120284 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -589,6 +589,40 @@ export type HeightProps = GetPropDefTypes; // @public (undocumented) export const Icon: (props: IconProps) => React_2.JSX.Element; +// @public (undocumented) +export const IconButton: React_2.ForwardRefExoticComponent< + IconButtonProps & React_2.RefAttributes +>; + +// @public (undocumented) +export type IconButtonOwnProps = GetPropDefTypes; + +// @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; + }; +}; + +// @public +export interface IconButtonProps + extends Omit, 'children'> { + icon: IconNames; + size?: IconButtonOwnProps['size']; + variant?: IconButtonOwnProps['variant']; +} + // @public (undocumented) export const IconContext: Context; diff --git a/packages/canon/src/components/IconButton/IconButton.props.ts b/packages/canon/src/components/IconButton/IconButton.props.ts new file mode 100644 index 0000000000..e584997ef5 --- /dev/null +++ b/packages/canon/src/components/IconButton/IconButton.props.ts @@ -0,0 +1,41 @@ +/* + * 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 new file mode 100644 index 0000000000..74e058430c --- /dev/null +++ b/packages/canon/src/components/IconButton/IconButton.stories.tsx @@ -0,0 +1,140 @@ +/* + * 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 React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { IconButton } from './IconButton'; +import { Flex } from '../Flex'; +import { Text } from '../Text'; +import { IconButtonProps } from './types'; + +const meta = { + title: 'Components/IconButton', + component: IconButton, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium'], + }, + variant: { + control: 'select', + options: ['primary', 'secondary'], + }, + }, + args: { + size: 'medium', + variant: 'primary', + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Variants: Story = { + args: { + icon: 'cloud', + }, + parameters: { + argTypes: { + variant: { + control: false, + }, + }, + }, + render: args => ( + + + + + ), +}; + +export const Sizes: Story = { + args: { + icon: 'cloud', + }, + render: args => ( + + + + + ), +}; + +export const Disabled: Story = { + args: { + icon: 'cloud', + disabled: true, + }, + render: args => ( + + + + + ), +}; + +export const Responsive: Story = { + args: { + icon: 'cloud', + variant: { + initial: 'primary', + sm: 'secondary', + }, + size: { + xs: 'small', + sm: 'medium', + }, + }, +}; + +const variants: string[] = ['primary', 'secondary']; + +export const Playground: Story = { + args: { + icon: 'cloud', + }, + render: args => ( + + {variants.map(variant => ( + + {variant} + {['small', 'medium'].map(size => ( + + + + + + ))} + + ))} + + ), +}; diff --git a/packages/canon/src/components/IconButton/IconButton.tsx b/packages/canon/src/components/IconButton/IconButton.tsx new file mode 100644 index 0000000000..0f28ed963f --- /dev/null +++ b/packages/canon/src/components/IconButton/IconButton.tsx @@ -0,0 +1,57 @@ +/* + * 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 React, { forwardRef } from 'react'; +import { Icon } from '../Icon'; +import clsx from 'clsx'; +import { useResponsiveValue } from '../../hooks/useResponsiveValue'; + +import type { IconButtonProps } from './types'; + +/** @public */ +export const IconButton = forwardRef( + (props: IconButtonProps, ref) => { + const { + size = 'medium', + variant = 'primary', + icon, + className, + style, + ...rest + } = props; + + const responsiveSize = useResponsiveValue(size); + const responsiveVariant = useResponsiveValue(variant); + + return ( + + ); + }, +); + +export default IconButton; diff --git a/packages/canon/src/components/IconButton/index.tsx b/packages/canon/src/components/IconButton/index.tsx new file mode 100644 index 0000000000..5f9943557e --- /dev/null +++ b/packages/canon/src/components/IconButton/index.tsx @@ -0,0 +1,20 @@ +/* + * 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 { IconButton } from './IconButton'; +export type { IconButtonProps } from './types'; +export { iconButtonPropDefs } from './IconButton.props'; +export type { IconButtonOwnProps } from './IconButton.props'; diff --git a/packages/canon/src/components/IconButton/styles.css b/packages/canon/src/components/IconButton/styles.css new file mode 100644 index 0000000000..aa4c2d0523 --- /dev/null +++ b/packages/canon/src/components/IconButton/styles.css @@ -0,0 +1,105 @@ +/* + * 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--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--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--size-medium { + font-size: var(--canon-font-size-4); + height: 40px; + width: 40px; +} + +.canon-IconButton--size-small { + font-size: var(--canon-font-size-3); + height: 32px; + width: 32px; +} + +.canon-IconButton--size-small .canon-IconButton--icon { + width: 1rem; + height: 1rem; +} + +.canon-IconButton--size-medium .canon-IconButton--icon { + width: 1.5rem; + height: 1.5rem; +} diff --git a/packages/canon/src/components/IconButton/types.ts b/packages/canon/src/components/IconButton/types.ts new file mode 100644 index 0000000000..90505d3352 --- /dev/null +++ b/packages/canon/src/components/IconButton/types.ts @@ -0,0 +1,42 @@ +/* + * 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 { IconNames } from '../Icon'; +import type { IconButtonOwnProps } from './IconButton.props'; + +/** + * 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 at the start of the button + */ + icon: IconNames; +} diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index d7d99bbe91..d9033a447c 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -24,6 +24,7 @@ @import '../components/Table/styles.css'; @import '../components/Text/styles.css'; @import '../components/Heading/styles.css'; +@import '../components/IconButton/styles.css'; @import '../components/Input/Input.styles.css'; @import '../components/Field/Field.styles.css'; @import '../components/Link/styles.css'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 18dad55e89..4750ca1b02 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -34,6 +34,7 @@ export * from './components/Heading'; // UI components export * from './components/Button'; export * from './components/Icon'; +export * from './components/IconButton'; export * from './components/Checkbox'; export * from './components/Table'; export * from './components/Input';