diff --git a/frontend/documentation/components/Button.stories.tsx b/frontend/documentation/components/Button.stories.tsx index d7be200780d3..3e5533f5bbef 100644 --- a/frontend/documentation/components/Button.stories.tsx +++ b/frontend/documentation/components/Button.stories.tsx @@ -7,6 +7,7 @@ import { sizeClassNames, } from 'components/base/forms/Button' import type { ButtonType } from 'components/base/forms/Button' +import { Icon } from 'components/icons' const themeOptions = Object.keys(themeClassNames) as Array< keyof typeof themeClassNames @@ -19,7 +20,7 @@ const meta: Meta = { argTypes: { children: { control: 'text', - description: 'Button label content.', + description: 'Button label content. Compose icons via `` children.', }, disabled: { control: 'boolean', @@ -60,7 +61,7 @@ export const Variants: Story = { docs: { description: { story: - 'All available button themes. Use `primary` for main actions, `secondary` for alternatives, `outline` for low-emphasis actions, `danger` for destructive actions, and `success` for positive confirmations. `icon` is for icon-only buttons (copy, action triggers in tables); `project` is the avatar-style button used in the project picker.', + 'All available button themes. Use `primary` for main actions, `secondary` for alternatives, `outline` for low-emphasis actions, `danger` for destructive actions, and `success` for positive confirmations.', }, }, }, @@ -73,10 +74,9 @@ export const Variants: Story = { - - ), } @@ -85,7 +85,7 @@ export const Sizes: Story = { parameters: { docs: { description: { - story: 'Button sizes from large to extra small.', + story: 'Button sizes from large to extra-extra small.', }, }, }, @@ -95,6 +95,7 @@ export const Sizes: Story = { + ), } @@ -130,20 +131,23 @@ export const WithIcons: Story = { docs: { description: { story: - 'Buttons support `iconLeft` and `iconRight` props. Pass any `IconName` from the icon system.', + 'Icons compose via children — pass `` JSX directly alongside the label. Button handles icon+label spacing internally.', }, }, }, render: () => (
- - -
), diff --git a/frontend/web/components/EditIdentity.tsx b/frontend/web/components/EditIdentity.tsx index e614fc12e615..f9e3272bdaa0 100644 --- a/frontend/web/components/EditIdentity.tsx +++ b/frontend/web/components/EditIdentity.tsx @@ -2,6 +2,7 @@ import React, { FC, useEffect, useRef, useState } from 'react' import { Identity } from 'common/types/responses' import { useUpdateIdentityMutation } from 'common/services/useIdentity' import Button from './base/forms/Button' +import { Icon } from './icons' import ErrorMessage from './ErrorMessage' import GhostInput from './base/forms/GhostInput' @@ -62,15 +63,12 @@ const EditIdentity: FC = ({ data, environmentId }) => { /> {error} diff --git a/frontend/web/components/GoogleButton.tsx b/frontend/web/components/GoogleButton.tsx index 0ba3b208a811..2c3eab18e5ee 100644 --- a/frontend/web/components/GoogleButton.tsx +++ b/frontend/web/components/GoogleButton.tsx @@ -1,6 +1,7 @@ import React, { FC } from 'react' import { TokenResponse, useGoogleLogin } from '@react-oauth/google' import Button from './base/forms/Button' +import { Icon } from './icons' type GoogleButtonProps = { className?: string @@ -22,10 +23,10 @@ const GoogleButton: FC = ({ className, onSuccess }) => { ) diff --git a/frontend/web/components/JSONReference.tsx b/frontend/web/components/JSONReference.tsx index 017cc3120809..53ab4463328c 100644 --- a/frontend/web/components/JSONReference.tsx +++ b/frontend/web/components/JSONReference.tsx @@ -5,7 +5,7 @@ import Highlight from './Highlight' import Button from './base/forms/Button' import Switch from './Switch' import flagsmith from '@flagsmith/flagsmith' -import Icon from './icons/Icon' +import { Icon } from './icons' import Utils from 'common/utils/utils' type JSONReferenceType = { @@ -146,9 +146,8 @@ const JSONReference: FC = ({ Utils.copyToClipboard(condensed ? idsOnly : value) }} size='xSmall' - iconLeft='copy' - iconLeftColour='white' > + Copy JSON diff --git a/frontend/web/components/ViewDocs.tsx b/frontend/web/components/ViewDocs.tsx index 7936c7c2d6be..ada7d4169e5c 100644 --- a/frontend/web/components/ViewDocs.tsx +++ b/frontend/web/components/ViewDocs.tsx @@ -7,8 +7,8 @@ type ViewDocsType = ButtonType & {} const ViewDocs: FC = (props) => { return ( ) } diff --git a/frontend/web/components/base/forms/Button.tsx b/frontend/web/components/base/forms/Button.tsx index 18f941483b4b..611348647e22 100644 --- a/frontend/web/components/base/forms/Button.tsx +++ b/frontend/web/components/base/forms/Button.tsx @@ -1,23 +1,14 @@ import React from 'react' import cn from 'classnames' import { ButtonHTMLAttributes, HTMLAttributeAnchorTarget } from 'react' -import Icon, { IconName } from 'components/icons/Icon' - -const iconColours = { - primary: '#6837fc', - white: '#ffffff', -} as const - -export type IconColour = keyof typeof iconColours export const themeClassNames = { - danger: 'btn btn-danger', + danger: 'btn-danger', icon: 'btn-icon', outline: 'btn--outline', primary: 'btn-primary', - project: 'btn-project', - secondary: 'btn btn-secondary', - success: 'btn btn-success', + secondary: 'btn-secondary', + success: 'btn-success', tertiary: 'btn-tertiary', text: 'btn-link', } @@ -31,15 +22,10 @@ export const sizeClassNames = { } export type ButtonType = ButtonHTMLAttributes & { - iconRight?: IconName - iconRightColour?: IconColour - iconLeftColour?: IconColour - iconLeft?: IconName href?: string target?: HTMLAttributeAnchorTarget theme?: keyof typeof themeClassNames size?: keyof typeof sizeClassNames - iconSize?: number } export const Button = React.forwardRef< @@ -51,11 +37,6 @@ export const Button = React.forwardRef< children, className, href, - iconLeft, - iconLeftColour, - iconRight, - iconRightColour, - iconSize = 24, onMouseUp, size = 'default', target, @@ -65,64 +46,32 @@ export const Button = React.forwardRef< }, ref, ) => { + const classes = cn( + 'btn', + className, + themeClassNames[theme], + sizeClassNames[size], + ) return href ? ( } > -
- {!!iconLeft && ( - - )} - {children} -
- {!!iconRight && ( - - )} + {children}
) : ( ) }, diff --git a/frontend/web/components/icons/index.ts b/frontend/web/components/icons/index.ts new file mode 100644 index 000000000000..906700268806 --- /dev/null +++ b/frontend/web/components/icons/index.ts @@ -0,0 +1,2 @@ +export { default as Icon } from './Icon' +export type { IconName } from './Icon' diff --git a/frontend/web/components/modals/InviteUsers.tsx b/frontend/web/components/modals/InviteUsers.tsx index 5f48e66113f3..214763ecb8fd 100644 --- a/frontend/web/components/modals/InviteUsers.tsx +++ b/frontend/web/components/modals/InviteUsers.tsx @@ -3,7 +3,6 @@ import Button from 'components/base/forms/Button' import ConfigProvider from 'common/providers/ConfigProvider' import Constants from 'common/constants' import Icon from 'components/icons/Icon' -import { add } from 'ionicons/icons' import { IonIcon } from '@ionic/react' import { getPlanBasedOption } from 'components/PlanBasedAccess' import InputGroup from 'components/base/forms/InputGroup' @@ -259,7 +258,7 @@ const InviteUsers: FC = () => { onClick={() => deleteInvite(invite.temporaryId)} className='btn btn-with-icon mb-2' > - + ) : ( @@ -282,14 +281,8 @@ const InviteUsers: FC = () => { ]) } > - - - - - - {isSaving ? 'Sending' : 'Invite additional member'} - - + + {isSaving ? 'Sending' : 'Invite additional member'} diff --git a/frontend/web/components/modals/base/ModalConfirm.tsx b/frontend/web/components/modals/base/ModalConfirm.tsx index c885d7c7cc1f..5c866168e5ef 100644 --- a/frontend/web/components/modals/base/ModalConfirm.tsx +++ b/frontend/web/components/modals/base/ModalConfirm.tsx @@ -68,7 +68,6 @@ const Confirm: FC = ({ theme='danger' id='confirm-btn-yes' disabled={disabled || disabledYes} - iconRight='fas fa-trash' onClick={yes} > {yesText} diff --git a/frontend/web/components/pages/HomePage.tsx b/frontend/web/components/pages/HomePage.tsx index d0e3f941edd4..a575b45cda19 100644 --- a/frontend/web/components/pages/HomePage.tsx +++ b/frontend/web/components/pages/HomePage.tsx @@ -11,6 +11,7 @@ import Button from 'components/base/forms/Button' import PasswordRequirements from 'components/PasswordRequirements' import { informationCircleOutline } from 'ionicons/icons' import { IonIcon } from '@ionic/react' +import { Icon } from 'components/icons' import classNames from 'classnames' import InfoMessage from 'components/InfoMessage' import OnboardingPage from './OnboardingPage' @@ -206,9 +207,9 @@ const HomePage: React.FC = () => { , diff --git a/frontend/web/components/pages/IdentityPage.tsx b/frontend/web/components/pages/IdentityPage.tsx index 7878102d927d..41b16b903fb0 100644 --- a/frontend/web/components/pages/IdentityPage.tsx +++ b/frontend/web/components/pages/IdentityPage.tsx @@ -167,7 +167,7 @@ const IdentityPage: FC = () => { /> {showAliases && ( -
+
Alias: diff --git a/frontend/web/components/release-pipelines/PipelineStageActions.tsx b/frontend/web/components/release-pipelines/PipelineStageActions.tsx index 59f2457f7953..c6867e214d8b 100644 --- a/frontend/web/components/release-pipelines/PipelineStageActions.tsx +++ b/frontend/web/components/release-pipelines/PipelineStageActions.tsx @@ -4,6 +4,7 @@ import { StageActionBody, StageActionRequest } from 'common/types/requests' import { useMemo } from 'react' import SinglePipelineStageAction from './SinglePipelineStageAction' import Button from 'components/base/forms/Button' +import { Icon } from 'components/icons' import { StageActionType } from 'common/types/responses' interface PipelineStageActionsProps { @@ -60,7 +61,8 @@ const PipelineStageActions = ({ onRemoveAction={index > 0 ? onRemoveAction : undefined} /> ))} - diff --git a/frontend/web/styles/project/_buttons.scss b/frontend/web/styles/project/_buttons.scss index 50bba7d7e9fe..5885e9d75dbe 100644 --- a/frontend/web/styles/project/_buttons.scss +++ b/frontend/web/styles/project/_buttons.scss @@ -2,21 +2,17 @@ .btn, button.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + height: $btn-line-height; white-space: nowrap; color: white; - // Project button, to remove with project select bar - &:hover, - &:focus { - background-color: $btn-hover-bg; - } &:focus-visible { box-shadow: none; } - &.btn:active { - background-color: $btn-active-bg; - } - &-link { color: $primary; &:hover, @@ -40,6 +36,9 @@ button.btn { color: white; background-color: $btn-danger-active; } + &:disabled { + color: white; + } } &-success { @@ -126,15 +125,18 @@ button.btn { } } &-success { - background-color: $success; + background-color: $btn-success; &:hover, &:focus { - background-color: $success400; + background-color: $btn-success-hover; color: $text-icon-light; } &.btn:active { - background-color: $success600; + background-color: $btn-success-active; + color: $text-icon-light; + } + &:disabled { color: $text-icon-light; } } @@ -152,12 +154,12 @@ button.btn { height: 18px; } path { - fill: $text-muted; + fill: var(--color-icon-secondary); } &:hover,&:focus { background-color: $bg-light300; path { - fill: $body-color; + fill: var(--color-icon-default); } } } @@ -165,6 +167,9 @@ button.btn { padding: 0 14px; background-color: $basic-alpha-8; border-radius: $border-radius; + svg path { + fill: var(--color-icon-secondary); + } &:hover, &:active, &:disabled { @@ -191,6 +196,7 @@ button.btn { } } &-project { + display: inline-block; background-color: $bg-light200; color: $body-color; padding: 0px 8px; @@ -271,8 +277,11 @@ button.btn { } } -.btn-link { - display: inline-block; +.btn-link, +button.btn-link { + display: inline-flex; + align-items: center; + gap: 0.25rem; text-decoration: none !important; vertical-align: baseline; font-family: $font-family; @@ -282,6 +291,7 @@ button.btn { border: none; box-shadow: none; padding: 0; + height: auto; line-height: $line-height-base; text-transform: inherit; font-weight: 500; @@ -340,23 +350,10 @@ $add-btn-size: 34px; .dark { .btn { - &:hover, - &:focus { - background-color: $btn-hover-bg-dark; - } - &:active { - background-color: $primary; - } &.btn-icon { color: $body-color-dark; - path { - fill: $text-muted; - } &:hover,&:focus { background-color: $bg-dark300; - path { - fill: $body-color-dark; - } } } &.btn-secondary { @@ -372,9 +369,14 @@ $add-btn-size: 34px; } } &.btn-success { + background-color: $btn-success; &:hover, &:focus { - background-color: $success400; + background-color: $btn-success-hover; + color: $text-icon-light; + } + &:active { + background-color: $btn-success-active; color: $text-icon-light; } } @@ -394,6 +396,10 @@ $add-btn-size: 34px; &:focus { background-color: $btn-outline-focus-bg-dark; } + &:active { + background-color: $btn-outline-active-bg; + color: $primary400; + } &-danger { border-color: $danger400 !important;