From a8b36f0e1d16810214d921245c59b81857af69a2 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Fri, 25 Jul 2025 16:46:46 -0400 Subject: [PATCH 01/30] first stab at multiple icons in anchors --- packages/gamut/src/Anchor/index.tsx | 155 +++--------------- .../gamut/src/helpers/appendIconToContent.tsx | 92 ++++++++++- .../lib/Typography/Anchor/Anchor.stories.tsx | 9 +- 3 files changed, 117 insertions(+), 139 deletions(-) diff --git a/packages/gamut/src/Anchor/index.tsx b/packages/gamut/src/Anchor/index.tsx index 287ff0e6d37..dcee7f8afff 100644 --- a/packages/gamut/src/Anchor/index.tsx +++ b/packages/gamut/src/Anchor/index.tsx @@ -1,123 +1,8 @@ -import { styledOptions, system, variant } from '@codecademy/gamut-styles'; -import { StyleProps, variance } from '@codecademy/variance'; -import styled from '@emotion/styled'; -import { ComponentProps, forwardRef, HTMLProps, RefObject } from 'react'; +import { forwardRef, RefObject } from 'react'; -import { ButtonBase, ButtonSelectors } from '../ButtonBase/ButtonBase'; -import { AppendedIconProps, appendIconToContent } from '../helpers'; - -export interface AnchorProps - extends StyleProps, - StyleProps { - onClick?: HTMLProps['onClick']; -} - -const outlineFocusVisible = { - [ButtonSelectors.OUTLINE]: { - content: "''", - position: 'absolute', - inset: -4, - borderRadius: 'md', - border: 2, - borderColor: 'primary', - opacity: 0, - zIndex: 0, - }, - - [ButtonSelectors.OUTLINE_FOCUS_VISIBLE]: { - opacity: 1, - }, -} as const; - -const underlineFocusVisible = { - [ButtonSelectors.FOCUS_VISIBLE]: { - outline: 'currentColor solid 2px', - borderRadius: 'sm', - outlineOffset: '1.5px', - textDecoration: 'underline', - }, -} as const; - -const anchorVariants = variant({ - base: { - display: 'inline-block', - bg: 'transparent', - boxShadow: 'none', - border: 'none', - p: 0, - fontSize: 'inherit', - position: 'relative', - color: 'primary', - whiteSpace: 'nowrap', - [ButtonSelectors.HOVER]: { - textDecoration: 'none', - cursor: 'pointer', - }, - [ButtonSelectors.DISABLED]: { - cursor: 'not-allowed', - textDecoration: 'none', - color: 'text-disabled', - }, - }, - variants: { - standard: { - color: 'primary', - fontWeight: 'bold', - WebkitFontSmoothing: 'antialiased', - MozOsxFontSmoothing: 'grayscale', - [ButtonSelectors.HOVER]: { - textDecoration: 'underline', - }, - [ButtonSelectors.FOCUS_VISIBLE]: { - WebkitFontSmoothing: 'antialiased', - MozOsxFontSmoothing: 'grayscale', - outline: 'none', - }, - ...outlineFocusVisible, - }, - inline: { - display: 'inline', - whiteSpace: 'initial', - textDecoration: 'underline', - ...underlineFocusVisible, - }, - interface: { - color: 'text', - whiteSpace: 'initial', - [ButtonSelectors.HOVER]: { - color: 'primary', - }, - [ButtonSelectors.FOCUS_VISIBLE]: { - color: 'primary', - outline: 'none', - }, - ...outlineFocusVisible, - }, - 'standard-secondary': { - color: 'text', - textDecoration: 'underline', - ...underlineFocusVisible, - }, - }, -}); - -const anchorProps = variance.compose( - system.layout, - system.space, - system.typography -); - -export const AnchorBase = styled('a', styledOptions<'a'>())( - anchorVariants, - anchorProps -); - -type AnchorBaseProps = - | ComponentProps - | (Exclude, 'ref'> & - ComponentProps); - -type AnchorExtProps = Partial & AnchorBaseProps; +import { ButtonBase } from '../ButtonBase/ButtonBase'; +import { appendIconToContent, appendMultiIconsToContent } from '../helpers'; +import { AnchorBase, AnchorExtProps } from './types'; export const Anchor = forwardRef< HTMLAnchorElement | HTMLButtonElement, @@ -137,15 +22,29 @@ export const Anchor = forwardRef< }, ref ) => { - const content = appendIconToContent({ - children, - icon, - iconOffset, - iconPosition, - iconSize, - iconAndTextGap, - isInlineIcon, - }); + let content; + if (icon && Array.isArray(icon)) { + content = appendMultiIconsToContent({ + children, + icon, + iconOffset, + iconSize, + iconAndTextGap, + isInlineIcon, + }); + } else { + // Use appendIconToContent for single icon + content = appendIconToContent({ + children, + icon, + iconOffset, + iconSize, + iconAndTextGap, + isInlineIcon, + iconPosition, + }); + } + if (!rest.href) { return ( { +interface BaseAppendedIconProps extends WithChildrenProp { /** * This provides the space between the icon and the children */ @@ -13,10 +13,6 @@ export interface AppendedIconProps * This value adds a padding to the icon's parent container and bumps up the icon's height by the offset */ iconOffset?: number; - /** - * Can set the positioning of the icon relative to children, default is `left` - */ - iconPosition?: 'left' | 'right'; /** * This value determines the size of the icon */ @@ -26,6 +22,30 @@ export interface AppendedIconProps */ isInlineIcon?: boolean; } +export interface AppendedSingleIconProps extends BaseAppendedIconProps { + icon?: React.ComponentType; + /** + * Can set the positioning of the icon relative to children, default is `left` + */ + iconPosition?: 'left' | 'right'; +} + +export interface AppendedMultipleIconsProps extends BaseAppendedIconProps { + icon?: [ + React.ComponentType, + React.ComponentType + ]; + iconPosition?: never; +} + +export type AppendedIconProps = + | AppendedSingleIconProps + | AppendedMultipleIconsProps; + +// // Helper to check if a value is a valid React component +const isValidComponent = (Component: any) => + typeof Component === 'function' || + (typeof Component === 'object' && Component !== null); export const appendIconToContent = ({ children, @@ -35,7 +55,7 @@ export const appendIconToContent = ({ iconPosition, iconSize = 12, isInlineIcon = false, -}: AppendedIconProps) => { +}: AppendedSingleIconProps) => { if (!Icon) return <>{children}; const iconSpacing = iconPosition === 'left' ? 'mr' : 'ml'; @@ -87,3 +107,57 @@ export const appendIconToContent = ({ ); }; + +export const appendMultiIconsToContent = ({ + children, + icon: Icon, + iconAndTextGap = 8, + iconOffset, + iconSize = 12, + isInlineIcon = false, +}: AppendedMultipleIconsProps) => { + if (!Icon) return <>{children}; + // console.log('hi'); + const [LeftIcon, RightIcon] = Icon; + + if (typeof iconOffset !== 'number') { + iconOffset = isInlineIcon ? 2 : 4; + } + + const iconOffsetInEm = pixelToEm(iconOffset); + const heightOffset = pixelToEm(iconSize + iconOffset); + + const iconProps = { + 'aria-hidden': true, + size: iconSize, + } as const; + + const renderIcon = (Component: any, spacing: 'mr' | 'ml', order: number) => + isValidComponent(Component) ? ( + + ) : null; + + const content = ( + <> + {renderIcon(LeftIcon, 'mr', 0)} + {children} + {renderIcon(RightIcon, 'ml', 1)} + + ); + + return isInlineIcon ? ( + {content} + ) : ( + + {content} + + ); +}; diff --git a/packages/styleguide/src/lib/Typography/Anchor/Anchor.stories.tsx b/packages/styleguide/src/lib/Typography/Anchor/Anchor.stories.tsx index d9b2703ac0b..cf6ac8bec4b 100644 --- a/packages/styleguide/src/lib/Typography/Anchor/Anchor.stories.tsx +++ b/packages/styleguide/src/lib/Typography/Anchor/Anchor.stories.tsx @@ -29,13 +29,18 @@ export const Default: Story = { export const IconAnchor: Story = { render: (args) => ( - + Left-aligned icon anchor From 465a796e69603ff121760ddbc416e1cf92647d9e Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Fri, 25 Jul 2025 16:50:42 -0400 Subject: [PATCH 02/30] let claude refactor functions for dryness --- .../gamut/src/helpers/appendIconToContent.tsx | 160 ++++++++++-------- 1 file changed, 94 insertions(+), 66 deletions(-) diff --git a/packages/gamut/src/helpers/appendIconToContent.tsx b/packages/gamut/src/helpers/appendIconToContent.tsx index afdd4d53f56..8d8a6c34eb2 100644 --- a/packages/gamut/src/helpers/appendIconToContent.tsx +++ b/packages/gamut/src/helpers/appendIconToContent.tsx @@ -42,11 +42,63 @@ export type AppendedIconProps = | AppendedSingleIconProps | AppendedMultipleIconsProps; -// // Helper to check if a value is a valid React component +// Helper to check if a value is a valid React component const isValidComponent = (Component: any) => typeof Component === 'function' || (typeof Component === 'object' && Component !== null); +// Common helper to calculate icon offsets and create base icon props +const createIconOffsets = ( + iconOffset: number | undefined, + iconSize: number, + isInlineIcon: boolean +) => { + const finalIconOffset = iconOffset ?? (isInlineIcon ? 2 : 4); + const iconOffsetInEm = pixelToEm(finalIconOffset); + const heightOffset = pixelToEm(iconSize + finalIconOffset); + + return { iconOffsetInEm, heightOffset }; +}; + +// Common helper to create base icon props +const createBaseIconProps = (iconSize: number) => + ({ + 'aria-hidden': true, + size: iconSize, + } as const); + +// Common helper to render an icon with all styling applied +const renderStyledIcon = ( + Component: React.ComponentType, + baseProps: ReturnType, + spacing: 'mr' | 'ml', + iconAndTextGap: number, + order: number, + iconOffsetInEm: string, + heightOffset: string, + iconSize: number +) => ( + +); + +// Common wrapper to handle inline vs flex layout +const wrapContent = (content: React.ReactNode, isInlineIcon: boolean) => + isInlineIcon ? ( + {content} + ) : ( + + {content} + + ); + export const appendIconToContent = ({ children, icon: Icon, @@ -58,54 +110,41 @@ export const appendIconToContent = ({ }: AppendedSingleIconProps) => { if (!Icon) return <>{children}; - const iconSpacing = iconPosition === 'left' ? 'mr' : 'ml'; - const iconPositioning = iconPosition === 'left' ? 0 : 1; - - if (typeof iconOffset !== 'number') { - iconOffset = isInlineIcon ? 2 : 4; - } - - const iconOffsetInEm = pixelToEm(iconOffset); - const heightOffset = pixelToEm(iconSize + iconOffset); + const { iconOffsetInEm, heightOffset } = createIconOffsets( + iconOffset, + iconSize, + isInlineIcon + ); + const baseIconProps = createBaseIconProps(iconSize); - const iconProps = { - 'aria-hidden': true, - size: iconSize, - [iconSpacing]: iconAndTextGap, - order: iconPositioning, - } as const; - - const InlineCenteredIcon = ( - + const iconSpacing = iconPosition === 'left' ? 'mr' : 'ml'; + const iconOrder = iconPosition === 'left' ? 0 : 1; + + const styledIcon = renderStyledIcon( + Icon, + baseIconProps, + iconSpacing, + iconAndTextGap, + iconOrder, + iconOffsetInEm, + heightOffset, + iconSize ); const content = iconPosition === 'left' ? ( <> - {InlineCenteredIcon} + {styledIcon} {children} ) : ( <> {children} - {InlineCenteredIcon} + {styledIcon} ); - return isInlineIcon ? ( - {content} - ) : ( - - {Icon && } - {children} - - ); + return wrapContent(content, isInlineIcon); }; export const appendMultiIconsToContent = ({ @@ -117,33 +156,28 @@ export const appendMultiIconsToContent = ({ isInlineIcon = false, }: AppendedMultipleIconsProps) => { if (!Icon) return <>{children}; - // console.log('hi'); - const [LeftIcon, RightIcon] = Icon; - - if (typeof iconOffset !== 'number') { - iconOffset = isInlineIcon ? 2 : 4; - } - const iconOffsetInEm = pixelToEm(iconOffset); - const heightOffset = pixelToEm(iconSize + iconOffset); - - const iconProps = { - 'aria-hidden': true, - size: iconSize, - } as const; + const [LeftIcon, RightIcon] = Icon; + const { iconOffsetInEm, heightOffset } = createIconOffsets( + iconOffset, + iconSize, + isInlineIcon + ); + const baseIconProps = createBaseIconProps(iconSize); const renderIcon = (Component: any, spacing: 'mr' | 'ml', order: number) => - isValidComponent(Component) ? ( - - ) : null; + isValidComponent(Component) + ? renderStyledIcon( + Component, + baseIconProps, + spacing, + iconAndTextGap, + order, + iconOffsetInEm, + heightOffset, + iconSize + ) + : null; const content = ( <> @@ -153,11 +187,5 @@ export const appendMultiIconsToContent = ({ ); - return isInlineIcon ? ( - {content} - ) : ( - - {content} - - ); + return wrapContent(content, isInlineIcon); }; From b9a28b17a6465282fea2b115a1f8b36632bb910a Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Fri, 25 Jul 2025 16:52:34 -0400 Subject: [PATCH 03/30] more refactoring --- packages/gamut/src/Anchor/index.tsx | 34 ++++++++++------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/packages/gamut/src/Anchor/index.tsx b/packages/gamut/src/Anchor/index.tsx index dcee7f8afff..d949848deba 100644 --- a/packages/gamut/src/Anchor/index.tsx +++ b/packages/gamut/src/Anchor/index.tsx @@ -22,28 +22,18 @@ export const Anchor = forwardRef< }, ref ) => { - let content; - if (icon && Array.isArray(icon)) { - content = appendMultiIconsToContent({ - children, - icon, - iconOffset, - iconSize, - iconAndTextGap, - isInlineIcon, - }); - } else { - // Use appendIconToContent for single icon - content = appendIconToContent({ - children, - icon, - iconOffset, - iconSize, - iconAndTextGap, - isInlineIcon, - iconPosition, - }); - } + const commonIconProps = { + children, + iconOffset, + iconSize, + iconAndTextGap, + isInlineIcon, + }; + + const content = + icon && Array.isArray(icon) + ? appendMultiIconsToContent({ ...commonIconProps }) + : appendIconToContent({ ...commonIconProps, iconPosition }); if (!rest.href) { return ( From 54455c015cbc11612d5a538c19d35cb0cb99c8eb Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Fri, 25 Jul 2025 16:53:27 -0400 Subject: [PATCH 04/30] add new files --- packages/gamut/src/Anchor/styles.tsx | 94 ++++++++++++++++++++++++++++ packages/gamut/src/Anchor/types.ts | 34 ++++++++++ 2 files changed, 128 insertions(+) create mode 100644 packages/gamut/src/Anchor/styles.tsx create mode 100644 packages/gamut/src/Anchor/types.ts diff --git a/packages/gamut/src/Anchor/styles.tsx b/packages/gamut/src/Anchor/styles.tsx new file mode 100644 index 00000000000..68fe7959beb --- /dev/null +++ b/packages/gamut/src/Anchor/styles.tsx @@ -0,0 +1,94 @@ +import { variant } from '@codecademy/gamut-styles'; + +import { ButtonSelectors } from '../ButtonBase/ButtonBase'; + +const outlineFocusVisible = { + [ButtonSelectors.OUTLINE]: { + content: "''", + position: 'absolute', + inset: -4, + borderRadius: 'md', + border: 2, + borderColor: 'primary', + opacity: 0, + zIndex: 0, + }, + + [ButtonSelectors.OUTLINE_FOCUS_VISIBLE]: { + opacity: 1, + }, +} as const; + +const underlineFocusVisible = { + [ButtonSelectors.FOCUS_VISIBLE]: { + outline: 'currentColor solid 2px', + borderRadius: 'sm', + outlineOffset: '1.5px', + textDecoration: 'underline', + }, +} as const; + +export const anchorVariants = variant({ + base: { + display: 'inline-block', + bg: 'transparent', + boxShadow: 'none', + border: 'none', + p: 0, + fontSize: 'inherit', + position: 'relative', + color: 'primary', + whiteSpace: 'nowrap', + [ButtonSelectors.HOVER]: { + textDecoration: 'none', + cursor: 'pointer', + }, + [ButtonSelectors.DISABLED]: { + cursor: 'not-allowed', + textDecoration: 'none', + color: 'text-disabled', + }, + }, + variants: { + standard: { + color: 'primary', + fontWeight: 'bold', + WebkitFontSmoothing: 'antialiased', + MozOsxFontSmoothing: 'grayscale', + [ButtonSelectors.HOVER]: { + textDecoration: 'underline', + }, + [ButtonSelectors.FOCUS_VISIBLE]: { + WebkitFontSmoothing: 'antialiased', + MozOsxFontSmoothing: 'grayscale', + outline: 'none', + }, + ...outlineFocusVisible, + }, + inline: { + display: 'inline', + whiteSpace: 'initial', + textDecoration: 'underline', + ...underlineFocusVisible, + }, + interface: { + color: 'text', + whiteSpace: 'initial', + [ButtonSelectors.HOVER]: { + color: 'primary', + }, + [ButtonSelectors.FOCUS_VISIBLE]: { + color: 'primary', + outline: 'none', + }, + ...outlineFocusVisible, + }, + 'standard-secondary': { + color: 'text', + textDecoration: 'underline', + ...underlineFocusVisible, + }, + }, +}); + + diff --git a/packages/gamut/src/Anchor/types.ts b/packages/gamut/src/Anchor/types.ts new file mode 100644 index 00000000000..088de7e704f --- /dev/null +++ b/packages/gamut/src/Anchor/types.ts @@ -0,0 +1,34 @@ +import { styledOptions,system } from "@codecademy/gamut-styles"; +import { StyleProps, variance } from "@codecademy/variance"; +import styled from "@emotion/styled"; +import { ComponentProps,HTMLProps } from "react"; + +import { ButtonBase } from "../ButtonBase"; +import { AppendedIconProps } from "../helpers"; +import { anchorVariants } from "./styles"; + +export interface AnchorProps + extends StyleProps, + StyleProps { + onClick?: HTMLProps['onClick']; +} + +const anchorProps = variance.compose( + system.layout, + system.space, + system.typography +); + +export const AnchorBase = styled('a', styledOptions<'a'>())( + anchorVariants, + anchorProps +); + +type AnchorBaseProps = + | ComponentProps + | (Exclude, 'ref'> & + ComponentProps); + +export type AnchorExtProps = Partial & AnchorBaseProps; + + From 001db405cbbab0786ee1d224b9d3d64515085ea2 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Fri, 25 Jul 2025 16:56:01 -0400 Subject: [PATCH 05/30] minor touch ups --- packages/gamut/src/helpers/appendIconToContent.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gamut/src/helpers/appendIconToContent.tsx b/packages/gamut/src/helpers/appendIconToContent.tsx index 8d8a6c34eb2..a3e261f8845 100644 --- a/packages/gamut/src/helpers/appendIconToContent.tsx +++ b/packages/gamut/src/helpers/appendIconToContent.tsx @@ -69,7 +69,7 @@ const createBaseIconProps = (iconSize: number) => // Common helper to render an icon with all styling applied const renderStyledIcon = ( - Component: React.ComponentType, + Icon: React.ComponentType, baseProps: ReturnType, spacing: 'mr' | 'ml', iconAndTextGap: number, @@ -78,7 +78,7 @@ const renderStyledIcon = ( heightOffset: string, iconSize: number ) => ( - isInlineIcon ? ( {content} ) : ( - + {content} ); From 765dcd92e1c6263ca716516742eb46f89925351f Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 28 Jul 2025 08:40:20 -0400 Subject: [PATCH 06/30] refactor to use appendIconToContent inside appendMultiIconsToContent --- .../gamut/src/helpers/appendIconToContent.tsx | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/gamut/src/helpers/appendIconToContent.tsx b/packages/gamut/src/helpers/appendIconToContent.tsx index a3e261f8845..a515ef44d0a 100644 --- a/packages/gamut/src/helpers/appendIconToContent.tsx +++ b/packages/gamut/src/helpers/appendIconToContent.tsx @@ -42,12 +42,7 @@ export type AppendedIconProps = | AppendedSingleIconProps | AppendedMultipleIconsProps; -// Helper to check if a value is a valid React component -const isValidComponent = (Component: any) => - typeof Component === 'function' || - (typeof Component === 'object' && Component !== null); - -// Common helper to calculate icon offsets and create base icon props +// Calculate icon offsets for centering and spacing const createIconOffsets = ( iconOffset: number | undefined, iconSize: number, @@ -67,7 +62,6 @@ const createBaseIconProps = (iconSize: number) => size: iconSize, } as const); -// Common helper to render an icon with all styling applied const renderStyledIcon = ( Icon: React.ComponentType, baseProps: ReturnType, @@ -158,32 +152,32 @@ export const appendMultiIconsToContent = ({ if (!Icon) return <>{children}; const [LeftIcon, RightIcon] = Icon; - const { iconOffsetInEm, heightOffset } = createIconOffsets( + + const leftIconContent = appendIconToContent({ + children: null, + icon: LeftIcon, + iconAndTextGap, iconOffset, iconSize, - isInlineIcon - ); - const baseIconProps = createBaseIconProps(iconSize); + isInlineIcon, + iconPosition: 'left', + }); - const renderIcon = (Component: any, spacing: 'mr' | 'ml', order: number) => - isValidComponent(Component) - ? renderStyledIcon( - Component, - baseIconProps, - spacing, - iconAndTextGap, - order, - iconOffsetInEm, - heightOffset, - iconSize - ) - : null; + const rightIconContent = appendIconToContent({ + children: null, + icon: RightIcon, + iconAndTextGap, + iconOffset, + iconSize, + isInlineIcon, + iconPosition: 'right', + }); const content = ( <> - {renderIcon(LeftIcon, 'mr', 0)} + {leftIconContent} {children} - {renderIcon(RightIcon, 'ml', 1)} + {rightIconContent} ); From 2309e7bafb21c04118ea525030a850b90ba07c04 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 28 Jul 2025 08:49:48 -0400 Subject: [PATCH 07/30] fix importing and exporting --- packages/gamut/src/Anchor/{index.tsx => Anchor.tsx} | 0 packages/gamut/src/Anchor/index.ts | 2 ++ packages/gamut/src/Anchor/{styles.tsx => styles.ts} | 2 -- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/gamut/src/Anchor/{index.tsx => Anchor.tsx} (100%) create mode 100644 packages/gamut/src/Anchor/index.ts rename packages/gamut/src/Anchor/{styles.tsx => styles.ts} (99%) diff --git a/packages/gamut/src/Anchor/index.tsx b/packages/gamut/src/Anchor/Anchor.tsx similarity index 100% rename from packages/gamut/src/Anchor/index.tsx rename to packages/gamut/src/Anchor/Anchor.tsx diff --git a/packages/gamut/src/Anchor/index.ts b/packages/gamut/src/Anchor/index.ts new file mode 100644 index 00000000000..40e0aa2c777 --- /dev/null +++ b/packages/gamut/src/Anchor/index.ts @@ -0,0 +1,2 @@ +export * from './Anchor'; +export * from './types'; diff --git a/packages/gamut/src/Anchor/styles.tsx b/packages/gamut/src/Anchor/styles.ts similarity index 99% rename from packages/gamut/src/Anchor/styles.tsx rename to packages/gamut/src/Anchor/styles.ts index 68fe7959beb..fa61679e4a6 100644 --- a/packages/gamut/src/Anchor/styles.tsx +++ b/packages/gamut/src/Anchor/styles.ts @@ -90,5 +90,3 @@ export const anchorVariants = variant({ }, }, }); - - From f1bf241fcc120efba5a9fbb428d2e9cc6f790b96 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 28 Jul 2025 08:51:56 -0400 Subject: [PATCH 08/30] fix more imports --- packages/gamut/src/Anchor/types.ts | 16 +++++++--------- .../gamut/src/DataList/Controls/SortControl.tsx | 2 +- packages/gamut/src/Tip/PreviewTip/index.tsx | 3 ++- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/gamut/src/Anchor/types.ts b/packages/gamut/src/Anchor/types.ts index 088de7e704f..02f24117f81 100644 --- a/packages/gamut/src/Anchor/types.ts +++ b/packages/gamut/src/Anchor/types.ts @@ -1,11 +1,11 @@ -import { styledOptions,system } from "@codecademy/gamut-styles"; -import { StyleProps, variance } from "@codecademy/variance"; -import styled from "@emotion/styled"; -import { ComponentProps,HTMLProps } from "react"; +import { styledOptions, system } from '@codecademy/gamut-styles'; +import { StyleProps, variance } from '@codecademy/variance'; +import styled from '@emotion/styled'; +import { ComponentProps, HTMLProps } from 'react'; -import { ButtonBase } from "../ButtonBase"; -import { AppendedIconProps } from "../helpers"; -import { anchorVariants } from "./styles"; +import { ButtonBase } from '../ButtonBase'; +import { AppendedIconProps } from '../helpers'; +import { anchorVariants } from './styles'; export interface AnchorProps extends StyleProps, @@ -30,5 +30,3 @@ type AnchorBaseProps = ComponentProps); export type AnchorExtProps = Partial & AnchorBaseProps; - - diff --git a/packages/gamut/src/DataList/Controls/SortControl.tsx b/packages/gamut/src/DataList/Controls/SortControl.tsx index 90aa11a6d8b..0e5208979c9 100644 --- a/packages/gamut/src/DataList/Controls/SortControl.tsx +++ b/packages/gamut/src/DataList/Controls/SortControl.tsx @@ -3,7 +3,7 @@ import { css, states } from '@codecademy/gamut-styles'; import styled from '@emotion/styled'; import * as React from 'react'; -import { Anchor } from '../..'; +import { Anchor } from '../../Anchor'; import { FlexBox } from '../../Box'; import { useListState } from '../hooks/useListState'; import { OnSort, SortDirection, SortOrder } from '../types'; diff --git a/packages/gamut/src/Tip/PreviewTip/index.tsx b/packages/gamut/src/Tip/PreviewTip/index.tsx index 88421963e23..d309441a553 100644 --- a/packages/gamut/src/Tip/PreviewTip/index.tsx +++ b/packages/gamut/src/Tip/PreviewTip/index.tsx @@ -8,7 +8,8 @@ import { useState, } from 'react'; -import { Anchor, Text } from '../..'; +import { Text } from '../..'; +import { Anchor } from '../../Anchor'; import { FloatingTip } from '../shared/FloatingTip'; import { InlineTip } from '../shared/InlineTip'; import { From c9dd68224eb3c4234713970184af763868cbc1c4 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 28 Jul 2025 09:35:55 -0400 Subject: [PATCH 09/30] touched up stories --- packages/gamut/src/Anchor/Anchor.tsx | 21 ++++++++- .../gamut/src/helpers/appendIconToContent.tsx | 7 +-- .../src/lib/Typography/Anchor/Anchor.mdx | 10 +++- .../lib/Typography/Anchor/Anchor.stories.tsx | 47 +++++++++++++------ 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/packages/gamut/src/Anchor/Anchor.tsx b/packages/gamut/src/Anchor/Anchor.tsx index d949848deba..10fe29b5d6d 100644 --- a/packages/gamut/src/Anchor/Anchor.tsx +++ b/packages/gamut/src/Anchor/Anchor.tsx @@ -1,3 +1,4 @@ +import { MiniInfoOutlineIcon } from '@codecademy/gamut-icons'; import { forwardRef, RefObject } from 'react'; import { ButtonBase } from '../ButtonBase/ButtonBase'; @@ -32,8 +33,8 @@ export const Anchor = forwardRef< const content = icon && Array.isArray(icon) - ? appendMultiIconsToContent({ ...commonIconProps }) - : appendIconToContent({ ...commonIconProps, iconPosition }); + ? appendMultiIconsToContent({ ...commonIconProps, icon }) + : appendIconToContent({ ...commonIconProps, icon, iconPosition }); if (!rest.href) { return ( @@ -59,3 +60,19 @@ export const Anchor = forwardRef< ); } ); + +const Test = () => { + return ( + + Test Anchor + + ); +}; diff --git a/packages/gamut/src/helpers/appendIconToContent.tsx b/packages/gamut/src/helpers/appendIconToContent.tsx index a515ef44d0a..102160a528d 100644 --- a/packages/gamut/src/helpers/appendIconToContent.tsx +++ b/packages/gamut/src/helpers/appendIconToContent.tsx @@ -144,10 +144,10 @@ export const appendIconToContent = ({ export const appendMultiIconsToContent = ({ children, icon: Icon, - iconAndTextGap = 8, + iconAndTextGap, iconOffset, - iconSize = 12, - isInlineIcon = false, + iconSize, + isInlineIcon, }: AppendedMultipleIconsProps) => { if (!Icon) return <>{children}; @@ -177,6 +177,7 @@ export const appendMultiIconsToContent = ({ <> {leftIconContent} {children} + {rightIconContent} ); diff --git a/packages/styleguide/src/lib/Typography/Anchor/Anchor.mdx b/packages/styleguide/src/lib/Typography/Anchor/Anchor.mdx index 9ab4dff4c49..058150e8ba2 100644 --- a/packages/styleguide/src/lib/Typography/Anchor/Anchor.mdx +++ b/packages/styleguide/src/lib/Typography/Anchor/Anchor.mdx @@ -86,9 +86,15 @@ Use outside of a paragraph or `menu` instance when a lower level of prominence i ## Icons -Anchors can be rendered with leading and trailing icons to provide additional context or visual distinction. +Anchors can be rendered with leading, trailing, or icons on both sides to provide additional context or visual distinction. - +By default the icons will be NOT be inline. + + + +You can set `variant="inline"` to render the icons inline with the text. This is useful for links that are part of a paragraph or text-heavy content. + + Icons are also responsive to ColorMode. diff --git a/packages/styleguide/src/lib/Typography/Anchor/Anchor.stories.tsx b/packages/styleguide/src/lib/Typography/Anchor/Anchor.stories.tsx index cf6ac8bec4b..cb9c4566575 100644 --- a/packages/styleguide/src/lib/Typography/Anchor/Anchor.stories.tsx +++ b/packages/styleguide/src/lib/Typography/Anchor/Anchor.stories.tsx @@ -1,7 +1,12 @@ -import { Anchor, GridBox, Text } from '@codecademy/gamut'; +import { Anchor, FlexBox, Text } from '@codecademy/gamut'; import { + BulbIcon, + MiniArrowLeftIcon, MiniArrowRightIcon, MiniInfoOutlineIcon, + MiniOpenIcon, + SmileySadIcon, + StudyBookIcon, } from '@codecademy/gamut-icons'; import type { Meta, StoryObj } from '@storybook/react'; @@ -26,50 +31,64 @@ export const Default: Story = { args: {}, }; -export const IconAnchor: Story = { +export const IconFlexAnchor: Story = { render: (args) => ( - + + These anchors with icons are in a FlexBox: + + Left-aligned icon anchor + - Left-aligned icon anchor + Has both left and right-aligned icons Right-aligned icon anchor - + ), }; -export const IconAnchorExample: Story = { +export const IconInlineAnchorExample: Story = { render: (args) => ( - I started painting as a hobby when I was little. I didn't know I had - any talent. I believe talent is just a pursued interest.e{' '} + I started painting as a hobby when I was little.{' '} + I didn't know I had any talent. + {' '} + I believe talent is just a pursued interest.{' '} + Anybody can do what I do. {' '} Just go back and put one little more happy tree in there. Everybody's - different.{' '} + different.
From fe9a1dd1025ba24aa686f4eed11826d94f0f4950 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 28 Jul 2025 09:37:00 -0400 Subject: [PATCH 10/30] remove extra test --- packages/gamut/src/Anchor/Anchor.tsx | 16 ---------------- .../gamut/src/helpers/appendIconToContent.tsx | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/gamut/src/Anchor/Anchor.tsx b/packages/gamut/src/Anchor/Anchor.tsx index 10fe29b5d6d..0877002a456 100644 --- a/packages/gamut/src/Anchor/Anchor.tsx +++ b/packages/gamut/src/Anchor/Anchor.tsx @@ -60,19 +60,3 @@ export const Anchor = forwardRef< ); } ); - -const Test = () => { - return ( - - Test Anchor - - ); -}; diff --git a/packages/gamut/src/helpers/appendIconToContent.tsx b/packages/gamut/src/helpers/appendIconToContent.tsx index 102160a528d..d406ba912f2 100644 --- a/packages/gamut/src/helpers/appendIconToContent.tsx +++ b/packages/gamut/src/helpers/appendIconToContent.tsx @@ -147,7 +147,7 @@ export const appendMultiIconsToContent = ({ iconAndTextGap, iconOffset, iconSize, - isInlineIcon, + isInlineIcon = false, }: AppendedMultipleIconsProps) => { if (!Icon) return <>{children}; From 524e5b2bf5ffe792c3d8a4015ecfe4f317ccde94 Mon Sep 17 00:00:00 2001 From: Kenny Lin Date: Mon, 28 Jul 2025 10:35:54 -0400 Subject: [PATCH 11/30] updated button to allow multiple icons --- packages/gamut/src/Anchor/Anchor.tsx | 1 - .../src/Button/shared/InlineIconButton.tsx | 34 ++++++++++++++----- .../src/lib/Atoms/Buttons/Button/Button.mdx | 4 ++- .../Atoms/Buttons/Button/Button.stories.tsx | 8 +++-- .../src/lib/Typography/Anchor/Anchor.mdx | 3 +- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/gamut/src/Anchor/Anchor.tsx b/packages/gamut/src/Anchor/Anchor.tsx index 0877002a456..151395cd9e1 100644 --- a/packages/gamut/src/Anchor/Anchor.tsx +++ b/packages/gamut/src/Anchor/Anchor.tsx @@ -1,4 +1,3 @@ -import { MiniInfoOutlineIcon } from '@codecademy/gamut-icons'; import { forwardRef, RefObject } from 'react'; import { ButtonBase } from '../ButtonBase/ButtonBase'; diff --git a/packages/gamut/src/Button/shared/InlineIconButton.tsx b/packages/gamut/src/Button/shared/InlineIconButton.tsx index 7425b89a5fd..42750833792 100644 --- a/packages/gamut/src/Button/shared/InlineIconButton.tsx +++ b/packages/gamut/src/Button/shared/InlineIconButton.tsx @@ -1,7 +1,11 @@ import { forwardRef } from 'react'; import { ButtonBaseElements } from '../../ButtonBase/ButtonBase'; -import { appendIconToContent } from '../../helpers'; +import { + AppendedIconProps, + appendIconToContent, + appendMultiIconsToContent, +} from '../../helpers'; import { FillButtonProps } from '../FillButton'; import { StrokeButtonProps } from '../StrokeButton'; import { TextButtonProps } from '../TextButton'; @@ -11,10 +15,11 @@ type InlineIconButtonComponents = | StrokeButtonProps | TextButtonProps; -type InlineIconButtonType = InlineIconButtonComponents & { - button: React.ComponentType; - iconSize?: number; -}; +type InlineIconButtonType = InlineIconButtonComponents & + AppendedIconProps & { + button: React.ComponentType; + iconSize?: number; + }; export const InlineIconButton = forwardRef< ButtonBaseElements, @@ -31,12 +36,23 @@ export const InlineIconButton = forwardRef< }, ref ) => { - const content = appendIconToContent({ - iconPosition, - icon, + const commonIconProps = { iconSize: props.size === 'small' ? 12 : 16, children, - }); + }; + + const content = + icon && Array.isArray(icon) + ? appendMultiIconsToContent({ + ...commonIconProps, + icon, + }) + : appendIconToContent({ + ...commonIconProps, + icon, + iconPosition, + }); + return (