From 36d967ecfd1874a550f7c0f6b5afba16fbda25af Mon Sep 17 00:00:00 2001 From: Sergei Garin Date: Mon, 3 Feb 2025 15:24:50 +0300 Subject: [PATCH] New icon component (#12091) This PR adds support for icons generated from figma and deprecates the imports from `#/assets/` folder Closes: https://github.com/enso-org/enso/issues/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. --- .github/workflows/gui-checks.yml | 5 + .github/workflows/storybook.yml | 3 + app/gui/env.d.ts | 11 ++ app/gui/scripts/generateIconMetadata.mjs | 2 +- .../AriaComponents/Button/Button.stories.tsx | 4 +- .../AriaComponents/Button/Button.tsx | 16 ++- .../AriaComponents/Button/CloseButton.tsx | 9 +- .../AriaComponents/Button/CopyButton.tsx | 5 +- .../components/AriaComponents/Button/types.ts | 10 +- .../AriaComponents/Dialog/Close.tsx | 20 ++- .../AriaComponents/Dialog/Dialog.tsx | 1 + .../AriaComponents/Dialog/DialogDismiss.tsx | 17 ++- .../AriaComponents/Form/components/Reset.tsx | 5 +- .../AriaComponents/Form/components/Submit.tsx | 13 +- .../AriaComponents/Menu/MenuItem.tsx | 18 +-- .../components/AriaComponents/types.ts | 43 ++++++- .../components/Breadcrumbs/BreadcrumbItem.tsx | 21 +--- .../Breadcrumbs/Breadcrumbs.stories.tsx | 13 +- .../components/Breadcrumbs/Breadcrumbs.tsx | 9 +- .../components/Icon/Icon.stories.tsx | 74 ++++++----- .../src/dashboard/components/Icon/Icon.tsx | 117 ++++++++++++++---- .../src/dashboard/components/Icon/index.ts | 4 +- .../components/Paywall/PaywallAlert.tsx | 9 +- .../Paywall/PaywallDialogButton.tsx | 13 +- .../components/Paywall/UpgradeButton.tsx | 13 +- .../Paywall/components/PaywallButton.tsx | 6 +- app/gui/src/dashboard/components/SvgIcon.tsx | 33 ----- app/gui/src/dashboard/components/SvgMask.tsx | 8 +- .../components/styled/SidebarTabButton.tsx | 2 +- eslint.config.mjs | 2 +- package.json | 2 +- 31 files changed, 315 insertions(+), 193 deletions(-) delete mode 100644 app/gui/src/dashboard/components/SvgIcon.tsx diff --git a/.github/workflows/gui-checks.yml b/.github/workflows/gui-checks.yml index 1a114b3f4f7b..accd40d7648d 100644 --- a/.github/workflows/gui-checks.yml +++ b/.github/workflows/gui-checks.yml @@ -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 diff --git a/.github/workflows/storybook.yml b/.github/workflows/storybook.yml index 1a7dec273de7..bb9bfa61ba51 100644 --- a/.github/workflows/storybook.yml +++ b/.github/workflows/storybook.yml @@ -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 diff --git a/app/gui/env.d.ts b/app/gui/env.d.ts index 9dfeb5ffa61f..0a84bf999156 100644 --- a/app/gui/env.d.ts +++ b/app/gui/env.d.ts @@ -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 + } +} diff --git a/app/gui/scripts/generateIconMetadata.mjs b/app/gui/scripts/generateIconMetadata.mjs index 242b2c76f8fd..e93936267f65 100644 --- a/app/gui/scripts/generateIconMetadata.mjs +++ b/app/gui/scripts/generateIconMetadata.mjs @@ -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 diff --git a/app/gui/src/dashboard/components/AriaComponents/Button/Button.stories.tsx b/app/gui/src/dashboard/components/AriaComponents/Button/Button.stories.tsx index 6eec46bd6c40..801eafa992be 100644 --- a/app/gui/src/dashboard/components/AriaComponents/Button/Button.stories.tsx +++ b/app/gui/src/dashboard/components/AriaComponents/Button/Button.stories.tsx @@ -9,7 +9,7 @@ import { expect, userEvent, within } from '@storybook/test' import { Button, type BaseButtonProps } from '.' import { Badge } from '../../Badge' -type Story = StoryObj> +type Story = StoryObj> const variants = [ 'primary', @@ -40,7 +40,7 @@ export default { addonStart: { control: false }, addonEnd: { control: false }, }, -} as Meta> +} satisfies Meta> export const Variants: Story = { render: () => ( diff --git a/app/gui/src/dashboard/components/AriaComponents/Button/Button.tsx b/app/gui/src/dashboard/components/AriaComponents/Button/Button.tsx index 327ea8fd4791..cee8ed33c59b 100644 --- a/app/gui/src/dashboard/components/AriaComponents/Button/Button.tsx +++ b/app/gui/src/dashboard/components/AriaComponents/Button/Button.tsx @@ -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' @@ -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) { + forwardRef(function Button( + props: ButtonProps, + ref: ForwardedRef, + ) { props = useMergedButtonStyles(props) const { className, @@ -252,7 +254,9 @@ export const Button = memo( ) }), -) as unknown as ((props: ButtonProps & { ref?: ForwardedRef }) => ReactNode) & { +) as unknown as (( + props: ButtonProps & { ref?: ForwardedRef }, +) => ReactNode) & { // eslint-disable-next-line @typescript-eslint/naming-convention Group: typeof ButtonGroup // eslint-disable-next-line @typescript-eslint/naming-convention @@ -382,7 +386,7 @@ const Icon = memo(function Icon(props: IconProps) { const actualIcon = (() => { return typeof icon === 'string' ? - + {icon} : {icon} })() diff --git a/app/gui/src/dashboard/components/AriaComponents/Button/CloseButton.tsx b/app/gui/src/dashboard/components/AriaComponents/Button/CloseButton.tsx index 120d13d2dbe5..265ae955eca5 100644 --- a/app/gui/src/dashboard/components/AriaComponents/Button/CloseButton.tsx +++ b/app/gui/src/dashboard/components/AriaComponents/Button/CloseButton.tsx @@ -8,10 +8,15 @@ import { Button } from './Button' import type { ButtonProps } from './types' /** Props for a {@link CloseButton}. */ -export type CloseButtonProps = Omit +export type CloseButtonProps = Omit< + ButtonProps, + '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( + props: CloseButtonProps, +) { const { getText } = useText() const { diff --git a/app/gui/src/dashboard/components/AriaComponents/Button/CopyButton.tsx b/app/gui/src/dashboard/components/AriaComponents/Button/CopyButton.tsx index e2567b6837a7..5a5f57801211 100644 --- a/app/gui/src/dashboard/components/AriaComponents/Button/CopyButton.tsx +++ b/app/gui/src/dashboard/components/AriaComponents/Button/CopyButton.tsx @@ -17,7 +17,8 @@ import type { ButtonProps } from './types' // ================== /** Props for a {@link CopyButton}. */ -export interface CopyButtonProps extends Omit { +export interface CopyButtonProps + extends Omit, 'icon' | 'loading' | 'onPress'> { /** The text to copy to the clipboard. */ readonly copyText: string /** @@ -38,7 +39,7 @@ export interface CopyButtonProps extends Omit(props: CopyButtonProps) { const { variant = 'icon', copyIcon = CopyIcon, diff --git a/app/gui/src/dashboard/components/AriaComponents/Button/types.ts b/app/gui/src/dashboard/components/AriaComponents/Button/types.ts index 797a6f5957c0..72a1a7c5761b 100644 --- a/app/gui/src/dashboard/components/AriaComponents/Button/types.ts +++ b/app/gui/src/dashboard/components/AriaComponents/Button/types.ts @@ -42,11 +42,11 @@ export interface LinkRenderProps extends aria.LinkRenderProps { } /** Props for a Button. */ -export type ButtonProps = - | (BaseButtonProps & +export type ButtonProps = + | (BaseButtonProps & Omit & PropsWithoutHref) - | (BaseButtonProps & + | (BaseButtonProps & Omit & PropsWithHref) @@ -61,7 +61,7 @@ interface PropsWithoutHref { } /** Base props for a button. */ -export interface BaseButtonProps +export interface BaseButtonProps extends Omit, TestIdProps { /** If `true`, the loader will not be shown. */ @@ -70,7 +70,7 @@ export interface BaseButtonProps readonly tooltip?: ReactElement | string | false | null readonly tooltipPlacement?: aria.Placement /** The icon to display in the button */ - readonly icon?: IconProp + readonly icon?: IconProp /** When `true`, icon will be shown only when hovered. */ readonly showIconOnHover?: boolean /** diff --git a/app/gui/src/dashboard/components/AriaComponents/Dialog/Close.tsx b/app/gui/src/dashboard/components/AriaComponents/Dialog/Close.tsx index f49ea796f9c8..61eaad4d3c3e 100644 --- a/app/gui/src/dashboard/components/AriaComponents/Dialog/Close.tsx +++ b/app/gui/src/dashboard/components/AriaComponents/Dialog/Close.tsx @@ -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 = ButtonProps /** 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(props: CloseProps) { + const dialogContext = dialogProvider.useDialogStrictContext() - const onPressCallback = useEventCallback>((event) => { - dialogContext.close() - return props.onPress?.(event) - }) + const onPressCallback = useEventCallback['onPress']>>( + (event) => { + dialogContext.close() + return props.onPress?.(event) + }, + ) return