Skip to content

Commit

Permalink
New icon component (#12091)
Browse files Browse the repository at this point in the history
This PR adds support for icons generated from figma and deprecates the imports from `#/assets/` folder

Closes: #12062

---
Note: This PR ***does not*** migrate any icons. It only deprecates the legacy approach. Migrations will be done in future PRs.
---


Please refer to Storybook for visual review. Also this PR adds a canvas with all icons.
  • Loading branch information
MrFlashAccount authored Feb 3, 2025
1 parent 7cbe777 commit 36d967e
Show file tree
Hide file tree
Showing 31 changed files with 315 additions and 193 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/gui-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ permissions:
statuses: write # Write access to commit statuses
checks: write

env:
# Workaround for https://github.com/nodejs/corepack/issues/612
# See: https://github.com/nodejs/corepack/blob/main/README.md#environment-variables
COREPACK_DEFAULT_TO_LATEST: 0

jobs:
lint:
name: 👮 Lint GUI
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/storybook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ permissions:
statuses: write # Write access to commit statuses

env:
# Workaround for https://github.com/nodejs/corepack/issues/612
# See: https://github.com/nodejs/corepack/blob/main/README.md#environment-variables
COREPACK_DEFAULT_TO_LATEST: 0
ENSO_BUILD_SKIP_VERSION_CHECK: "true"
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1

Expand Down
11 changes: 11 additions & 0 deletions app/gui/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,3 +199,14 @@ declare global {
(message: string, projectId?: string | null, metadata?: object | null): void
}
}

// Add additional types for svg imports from `#/assets/*.svg`
declare module 'vite/client' {
declare module '#/assets/*.svg' {
/**
* @deprecated Prefer defined keys over importing from `#/assets/*.svg
*/
const src: string
export default src
}
}
2 changes: 1 addition & 1 deletion app/gui/scripts/generateIconMetadata.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ await fs.writeFile(
// Please run \`bazel run //:write_all\` to regenerate this file whenever \`icons.svg\` is changed.
/** All icon names present in icons.svg. */
const iconNames = [
export const iconNames = [
${iconNames?.map((name) => ` '${name}',`).join('\n')}
] as const
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { expect, userEvent, within } from '@storybook/test'
import { Button, type BaseButtonProps } from '.'
import { Badge } from '../../Badge'

type Story = StoryObj<BaseButtonProps<aria.ButtonRenderProps>>
type Story = StoryObj<BaseButtonProps<string, aria.ButtonRenderProps>>

const variants = [
'primary',
Expand Down Expand Up @@ -40,7 +40,7 @@ export default {
addonStart: { control: false },
addonEnd: { control: false },
},
} as Meta<BaseButtonProps<aria.ButtonRenderProps>>
} satisfies Meta<BaseButtonProps<string, aria.ButtonRenderProps>>

export const Variants: Story = {
render: () => (
Expand Down
16 changes: 10 additions & 6 deletions app/gui/src/dashboard/components/AriaComponents/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import {
} from 'react'

import * as aria from '#/components/aria'
import { StatelessSpinner } from '#/components/StatelessSpinner'
import SvgMask from '#/components/SvgMask'

import { useVisualTooltip } from '#/components/AriaComponents/Text'
import { Tooltip, TooltipTrigger } from '#/components/AriaComponents/Tooltip'
import { Icon as IconComponent } from '#/components/Icon'
import { StatelessSpinner } from '#/components/StatelessSpinner'
import { useEventCallback } from '#/hooks/eventCallbackHooks'
import { forwardRef } from '#/utilities/react'
import { ButtonGroup, ButtonGroupJoin } from './ButtonGroup'
Expand All @@ -28,7 +27,10 @@ const ICON_LOADER_DELAY = 150
// Manually casting types to make TS infer the final type correctly (e.g. RenderProps in icon)
// eslint-disable-next-line no-restricted-syntax
export const Button = memo(
forwardRef(function Button(props: ButtonProps, ref: ForwardedRef<HTMLButtonElement>) {
forwardRef(function Button<IconType extends string>(
props: ButtonProps<IconType>,
ref: ForwardedRef<HTMLButtonElement>,
) {
props = useMergedButtonStyles(props)
const {
className,
Expand Down Expand Up @@ -252,7 +254,9 @@ export const Button = memo(
</TooltipTrigger>
)
}),
) as unknown as ((props: ButtonProps & { ref?: ForwardedRef<HTMLButtonElement> }) => ReactNode) & {
) as unknown as (<IconType extends string>(
props: ButtonProps<IconType> & { ref?: ForwardedRef<HTMLButtonElement> },
) => ReactNode) & {
// eslint-disable-next-line @typescript-eslint/naming-convention
Group: typeof ButtonGroup
// eslint-disable-next-line @typescript-eslint/naming-convention
Expand Down Expand Up @@ -382,7 +386,7 @@ const Icon = memo(function Icon(props: IconProps) {

const actualIcon = (() => {
return typeof icon === 'string' ?
<SvgMask src={icon} className={styles.icon()} />
<IconComponent className={styles.icon()}>{icon}</IconComponent>
: <span className={styles.icon()}>{icon}</span>
})()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import { Button } from './Button'
import type { ButtonProps } from './types'

/** Props for a {@link CloseButton}. */
export type CloseButtonProps = Omit<ButtonProps, 'children' | 'rounding' | 'size' | 'variant'>
export type CloseButtonProps<IconType extends string> = Omit<
ButtonProps<IconType>,
'children' | 'rounding' | 'size' | 'variant'
>

/** A styled button with a close icon that appears on hover. */
export const CloseButton = memo(function CloseButton(props: CloseButtonProps) {
export const CloseButton = memo(function CloseButton<IconType extends string>(
props: CloseButtonProps<IconType>,
) {
const { getText } = useText()

const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import type { ButtonProps } from './types'
// ==================

/** Props for a {@link CopyButton}. */
export interface CopyButtonProps extends Omit<ButtonProps, 'icon' | 'loading' | 'onPress'> {
export interface CopyButtonProps<IconType extends string>
extends Omit<ButtonProps<IconType>, 'icon' | 'loading' | 'onPress'> {
/** The text to copy to the clipboard. */
readonly copyText: string
/**
Expand All @@ -38,7 +39,7 @@ export interface CopyButtonProps extends Omit<ButtonProps, 'icon' | 'loading' |
}

/** A button that copies text to the clipboard. */
export function CopyButton(props: CopyButtonProps) {
export function CopyButton<IconType extends string>(props: CopyButtonProps<IconType>) {
const {
variant = 'icon',
copyIcon = CopyIcon,
Expand Down
10 changes: 5 additions & 5 deletions app/gui/src/dashboard/components/AriaComponents/Button/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export interface LinkRenderProps extends aria.LinkRenderProps {
}

/** Props for a Button. */
export type ButtonProps =
| (BaseButtonProps<ButtonRenderProps> &
export type ButtonProps<IconType extends string = string> =
| (BaseButtonProps<IconType, ButtonRenderProps> &
Omit<aria.ButtonProps, 'children' | 'isPending' | 'onPress'> &
PropsWithoutHref)
| (BaseButtonProps<LinkRenderProps> &
| (BaseButtonProps<IconType, LinkRenderProps> &
Omit<aria.LinkProps, 'children' | 'onPress'> &
PropsWithHref)

Expand All @@ -61,7 +61,7 @@ interface PropsWithoutHref {
}

/** Base props for a button. */
export interface BaseButtonProps<Render>
export interface BaseButtonProps<IconType extends string, Render>
extends Omit<ButtonVariants, 'iconOnly' | 'isJoined' | 'position'>,
TestIdProps {
/** If `true`, the loader will not be shown. */
Expand All @@ -70,7 +70,7 @@ export interface BaseButtonProps<Render>
readonly tooltip?: ReactElement | string | false | null
readonly tooltipPlacement?: aria.Placement
/** The icon to display in the button */
readonly icon?: IconProp<Render>
readonly icon?: IconProp<IconType, Render>
/** When `true`, icon will be shown only when hovered. */
readonly showIconOnHover?: boolean
/**
Expand Down
20 changes: 9 additions & 11 deletions app/gui/src/dashboard/components/AriaComponents/Dialog/Close.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,23 @@
* Close button for a dialog.
*/

import invariant from 'tiny-invariant'

import { useEventCallback } from '#/hooks/eventCallbackHooks'
import { type ButtonProps, Button } from '../Button'
import * as dialogProvider from './DialogProvider'

/** Props for {@link Close} component. */
export type CloseProps = ButtonProps
export type CloseProps<IconType extends string> = ButtonProps<IconType>

/** Close button for a dialog. */
export function Close(props: CloseProps) {
const dialogContext = dialogProvider.useDialogContext()

invariant(dialogContext, 'Close must be used inside a DialogProvider')
export function Close<IconType extends string>(props: CloseProps<IconType>) {
const dialogContext = dialogProvider.useDialogStrictContext()

const onPressCallback = useEventCallback<NonNullable<ButtonProps['onPress']>>((event) => {
dialogContext.close()
return props.onPress?.(event)
})
const onPressCallback = useEventCallback<NonNullable<ButtonProps<IconType>['onPress']>>(
(event) => {
dialogContext.close()
return props.onPress?.(event)
},
)

return <Button {...props} onPress={onPressCallback} />
}
Original file line number Diff line number Diff line change
Expand Up @@ -573,5 +573,6 @@ const DialogHeader = React.memo(function DialogHeader(props: DialogHeaderProps)
})

Dialog.Close = Close
/** @deprecated Use {@link Dialog.Close} instead. */
Dialog.Dismiss = DialogDismiss
Dialog.Trigger = DialogTrigger
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ import { Button, type ButtonProps } from '../Button'
import { useDialogContext } from './DialogProvider'

/** Additional props for the Cancel component. */
interface DialogDismissBaseProps {
readonly variant?: ButtonProps['variant']
interface DialogDismissBaseProps<IconType extends string> {
readonly variant?: ButtonProps<IconType>['variant']
}

/** Props for a {@link DialogDismiss}. */
export type DialogDismissProps = DialogDismissBaseProps &
Omit<ButtonProps, 'formnovalidate' | 'href' | 'variant'>
export type DialogDismissProps<IconType extends string> = DialogDismissBaseProps<IconType> &
Omit<ButtonProps<IconType>, 'formnovalidate' | 'href' | 'variant'>

/** Dismiss button for dialogs. */
export function DialogDismiss(props: DialogDismissProps): JSX.Element {
/**
* Dismiss button for dialogs.
* @deprecated Use {@link Close} instead.
*/
export function DialogDismiss<IconType extends string>(
props: DialogDismissProps<IconType>,
): JSX.Element {
const { getText } = useText()

const { size = 'medium', ...buttonProps } = props
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import * as formContext from './FormProvider'
import type * as types from './types'

/** Props for the Reset component. */
export interface ResetProps extends Omit<ButtonProps, 'href' | 'loading'> {
export interface ResetProps<IconType extends string>
extends Omit<ButtonProps<IconType>, 'href' | 'loading'> {
/**
* Connects the reset button to a form.
* If not provided, the button will use the nearest form context.
Expand All @@ -20,7 +21,7 @@ export interface ResetProps extends Omit<ButtonProps, 'href' | 'loading'> {
}

/** Reset button for forms. */
export function Reset(props: ResetProps): React.JSX.Element {
export function Reset<IconType extends string>(props: ResetProps<IconType>): React.JSX.Element {
const { getText } = useText()
const {
variant = 'outline',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { useFormContext } from './FormProvider'
import type { FormInstance } from './types'

/** Additional props for the Submit component. */
interface SubmitButtonBaseProps {
readonly variant?: ButtonProps['variant']
interface SubmitButtonBaseProps<IconType extends string> {
readonly variant?: ButtonProps<IconType>['variant']
/**
* Connects the submit button to a form.
* If not provided, the button will use the nearest form context.
Expand All @@ -27,15 +27,18 @@ interface SubmitButtonBaseProps {
}

/** Props for the Submit component. */
export type SubmitProps = Omit<ButtonProps, 'formnovalidate' | 'href' | 'variant'> &
SubmitButtonBaseProps
export type SubmitProps<IconType extends string> = Omit<
ButtonProps<IconType>,
'formnovalidate' | 'href' | 'variant'
> &
SubmitButtonBaseProps<IconType>

/**
* Submit button for forms.
*
* Manages the form state and displays a loading spinner when the form is submitting.
*/
export function Submit(props: SubmitProps): JSX.Element {
export function Submit<IconType extends string>(props: SubmitProps<IconType>): JSX.Element {
const { getText } = useText()

const {
Expand Down
18 changes: 11 additions & 7 deletions app/gui/src/dashboard/components/AriaComponents/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const MENU_ITEM_STYLES = tv({
})

/** Props for {@link MenuItem} */
export type MenuItemProps<T extends object> = MenuItemBaseProps &
export type MenuItemProps<T extends object, IconType extends string> = MenuItemBaseProps<IconType> &
Omit<AriaMenuItemProps<T>, 'children'> &
TestIdProps &
VariantProps<typeof MENU_ITEM_STYLES> &
Expand All @@ -56,9 +56,9 @@ export type MenuItemProps<T extends object> = MenuItemBaseProps &
/**
* Base props for the menu item.
*/
export interface MenuItemBaseProps {
export interface MenuItemBaseProps<IconType extends string> {
/** Icon to display before the menu item text. Can be a string (path to SVG), ReactElement, or a render function */
readonly icon?: IconProp<MenuItemRenderProps>
readonly icon?: IconProp<IconType, MenuItemRenderProps>
/** Keyboard shortcut text to display */
readonly shortcut?: string
/** Additional class name */
Expand Down Expand Up @@ -87,7 +87,9 @@ export interface MenuItemCustomContentProps {
/**
* An item within a menu that represents a single action or option.
*/
export const MenuItem = memo(function MenuItem<T extends object>(props: MenuItemProps<T>) {
export const MenuItem = memo(function MenuItem<T extends object, IconType extends string>(
props: MenuItemProps<T, IconType>,
) {
const {
icon,
shortcut,
Expand Down Expand Up @@ -154,14 +156,16 @@ export const MenuItem = memo(function MenuItem<T extends object>(props: MenuItem
})

/** Props for {@link MenuItemIcon} */
interface MenuItemIconProps extends MenuItemRenderProps {
readonly icon: MenuItemProps<object>['icon']
interface MenuItemIconProps<IconType extends string> extends MenuItemRenderProps {
readonly icon: MenuItemProps<object, IconType>['icon']
readonly className?: string
}

/** Renders the icon for the menu item */
// eslint-disable-next-line no-restricted-syntax
const MenuItemIcon = memo(function MenuItemIcon(props: MenuItemIconProps) {
const MenuItemIcon = memo(function MenuItemIcon<IconType extends string>(
props: MenuItemIconProps<IconType>,
) {
const { icon, className, ...renderProps } = props

return (
Expand Down
Loading

0 comments on commit 36d967e

Please sign in to comment.