From a4937762381f9496ea7d9ce407b04315bcc62579 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 14 Aug 2025 16:27:26 -0400 Subject: [PATCH 01/21] feat(CodeEditor): enhance copy button functionality and styling - Introduced `copyButtonAppearance` prop to control the visibility of the copy button in the CodeEditor. - Implemented styles for different copy button appearances: 'hover', 'persist', and 'none'. - Updated tests to verify the rendering behavior of the copy button based on the new prop. - Refactored CodeEditor component to integrate the new copy button functionality and ensure proper rendering based on user interactions. --- packages/code-editor/src/CodeEditor/CodeEditor.types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts index 6f62d25bcb..d8bc0b3d30 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts +++ b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts @@ -5,6 +5,7 @@ import { type EditorView } from '@codemirror/view'; import { type DarkModeProps } from '@leafygreen-ui/lib'; import { type LanguageName } from './hooks/extensions/useLanguageExtension'; +import { copyButtonClassName } from './CodeEditor.styles'; /** * Re-export of CodeMirror's {@link Extension} type. From 705f5de098201f91d658a42fb05eaf79ffa7a88f Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 14 Aug 2025 16:39:37 -0400 Subject: [PATCH 02/21] refactor(CodeEditor): remove unused import from CodeEditor.types.ts - Eliminated the unused `copyButtonClassName` import to clean up the code and improve maintainability. --- packages/code-editor/src/CodeEditor/CodeEditor.types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts index d8bc0b3d30..6f62d25bcb 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts +++ b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts @@ -5,7 +5,6 @@ import { type EditorView } from '@codemirror/view'; import { type DarkModeProps } from '@leafygreen-ui/lib'; import { type LanguageName } from './hooks/extensions/useLanguageExtension'; -import { copyButtonClassName } from './CodeEditor.styles'; /** * Re-export of CodeMirror's {@link Extension} type. From 20e4ba37c5be5c3b2c972922397c712868aee5ec Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 15 Aug 2025 13:06:53 -0400 Subject: [PATCH 03/21] feat(CodeEditor): add CodeEditorContext for internal state management - Introduced a new `CodeEditorContext` to provide internal context values to child components, enhancing state management within the CodeEditor. - Implemented `CodeEditorProvider` for context provisioning and `useCodeEditorContext` hook for easy access to context values. - This addition supports better organization and encapsulation of editor-related functionalities. --- .../src/CodeEditor/CodeEditorContext.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 packages/code-editor/src/CodeEditor/CodeEditorContext.tsx diff --git a/packages/code-editor/src/CodeEditor/CodeEditorContext.tsx b/packages/code-editor/src/CodeEditor/CodeEditorContext.tsx new file mode 100644 index 0000000000..e4a660d88c --- /dev/null +++ b/packages/code-editor/src/CodeEditor/CodeEditorContext.tsx @@ -0,0 +1,46 @@ +import React, { createContext, useContext } from 'react'; + +/** + * Internal context values provided by CodeEditor to its children (like Panel). + */ +export interface CodeEditorContextValue { + /** + * Function to retrieve the current editor contents. + */ + getContents: () => string; +} + +// Default context value for when Panel is used standalone +const defaultContextValue: CodeEditorContextValue = { + getContents: () => '', +}; + +const CodeEditorContext = + createContext(defaultContextValue); + +/** + * Provider component that makes CodeEditor internal values available to child components. + * This is used internally and should not be used by consumers. + */ +export function CodeEditorProvider({ + children, + value, +}: { + children: React.ReactNode; + value: CodeEditorContextValue; +}) { + return ( + + {children} + + ); +} + +/** + * Hook to access CodeEditor internal context values. + * This is intended for use by internal components like Panel. + * When used outside of CodeEditor, returns default values. + */ +export function useCodeEditorContext(): CodeEditorContextValue { + return useContext(CodeEditorContext); +} From fcaa74a19f447a958fc9c4f8fc2565eb0eaf5e9d Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 15 Aug 2025 13:07:10 -0400 Subject: [PATCH 04/21] feat(CodeEditor): introduce Panel component for enhanced toolbar functionality - Added a new `Panel` component to the CodeEditor, providing a toolbar interface with options for formatting, copying, and custom actions. - Integrated the `Panel` into the CodeEditor, allowing for a customizable user experience with buttons for common actions like undo, redo, and download. - Updated the README to include documentation for the new `Panel` component and its properties. - Enhanced TypeScript definitions to support the new component and its props. - Included storybook examples to demonstrate the usage of the `Panel` component. --- packages/code-editor/README.md | 55 ++++++ packages/code-editor/package.json | 1 + .../code-editor/src/Panel/Panel.stories.tsx | 85 +++++++++ .../code-editor/src/Panel/Panel.styles.ts | 50 +++++ packages/code-editor/src/Panel/Panel.tsx | 180 ++++++++++++++++++ packages/code-editor/src/Panel/Panel.types.ts | 118 ++++++++++++ packages/code-editor/src/Panel/index.ts | 2 + packages/code-editor/src/index.ts | 1 + packages/code-editor/tsconfig.json | 3 + pnpm-lock.yaml | 123 ++++++------ 10 files changed, 558 insertions(+), 60 deletions(-) create mode 100644 packages/code-editor/src/Panel/Panel.stories.tsx create mode 100644 packages/code-editor/src/Panel/Panel.styles.ts create mode 100644 packages/code-editor/src/Panel/Panel.tsx create mode 100644 packages/code-editor/src/Panel/Panel.types.ts create mode 100644 packages/code-editor/src/Panel/index.ts diff --git a/packages/code-editor/README.md b/packages/code-editor/README.md index 3e6249e67e..3d5c10c95e 100644 --- a/packages/code-editor/README.md +++ b/packages/code-editor/README.md @@ -74,6 +74,60 @@ console.log(greet('MongoDB user'));`; | `value` _(optional)_ | Controlled value of the editor. If set, the editor will be controlled and will not update its value on change. Use `onChange` to update the value externally. | `string` | `undefined` | | `width` _(optional)_ | Sets the editor's width. If not set, the editor will be 100% width of its parent container. | `string` | `undefined` | +### `` + +The Panel component provides a toolbar interface for the CodeEditor with formatting, copying, and custom action buttons. It displays at the top of the CodeEditor and can include a title, action buttons, and custom content. + +#### Example + +```tsx +import { CodeEditor, Panel, LanguageName } from '@leafygreen-ui/code-editor'; +import CloudIcon from '@leafygreen-ui/icon'; + +, + onClick: () => console.log('Deploy clicked'), + 'aria-label': 'Deploy code to cloud', + }, + ]} + onFormatClick={() => console.log('Format clicked')} + onCopyClick={() => console.log('Copy clicked')} + onDownloadClick={() => console.log('Download clicked')} + /> + } +/>; +``` + +#### Properties + +| Name | Description | Type | Default | +| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ----------- | +| `baseFontSize` _(optional)_ | Font size of text in the panel. Controls the typography scale used for the panel title and other text elements. | `14 \| 16` | `14` | +| `customSecondaryButtons` _(optional)_ | Array of custom secondary buttons to display in the secondary menu. Each button can include a label, icon, click handler, href, and aria-label for accessibility. | `Array` | `undefined` | +| `darkMode` _(optional)_ | Determines if the component appears in dark mode. When not provided, the component will inherit the dark mode state from the LeafyGreen Provider. | `boolean` | `undefined` | +| `innerContent` _(optional)_ | React node to render between the title and the buttons. Can be used to add custom controls to the panel. | `React.ReactNode` | `undefined` | +| `onCopyClick` _(optional)_ | Callback fired when the copy button is clicked. Called after the copy operation is attempted. | `() => void` | `undefined` | +| `onDownloadClick` _(optional)_ | Callback fired when the download button in the secondary menu is clicked. Called after the download operation is attempted. | `() => void` | `undefined` | +| `onFormatClick` _(optional)_ | Callback fired when the format button is clicked. Called after the formatting operation is attempted. | `() => void` | `undefined` | +| `onRedoClick` _(optional)_ | Callback fired when the redo button in the secondary menu is clicked. Called after the redo operation is attempted. | `() => void` | `undefined` | +| `onUndoClick` _(optional)_ | Callback fired when the undo button in the secondary menu is clicked. Called after the undo operation is attempted. | `() => void` | `undefined` | +| `onViewShortcutsClick` _(optional)_ | Callback fired when the view shortcuts button in the secondary menu is clicked. Called after the view shortcuts operation is attempted. | `() => void` | `undefined` | +| `showCopyButton` _(optional)_ | Determines whether to show the copy button in the panel. When enabled, users can copy the editor content to their clipboard. | `boolean` | `undefined` | +| `showFormatButton` _(optional)_ | Determines whether to show the format button in the panel. When enabled and formatting is available for the current language, users can format/prettify their code. | `boolean` | `undefined` | +| `showSecondaryMenuButton` _(optional)_ | Determines whether to show the secondary menu button (ellipsis icon) in the panel. When enabled, displays a menu with additional actions like undo, redo, download, and view shortcuts. | `boolean` | `undefined` | +| `title` _(optional)_ | Title text to display in the panel header. Typically used to show the current language or content description. | `string` | `undefined` | + ## Types and Variables | Name | Description | @@ -89,6 +143,7 @@ console.log(greet('MongoDB user'));`; | `IndentUnits` | Constant object defining indent unit options (`space`, `tab`) for the `indentUnit` prop. | | `LanguageName` | Constant object containing all supported programming languages for syntax highlighting. | | `CodeEditorModules` | TypeScript interface defining the structure of lazy-loaded CodeMirror modules used by extension hooks. | +| `PanelProps` | TypeScript interface defining all props that can be passed to the `Panel` component. | ## Test Utilities diff --git a/packages/code-editor/package.json b/packages/code-editor/package.json index c92d878d10..893b263c2d 100644 --- a/packages/code-editor/package.json +++ b/packages/code-editor/package.json @@ -52,6 +52,7 @@ "@leafygreen-ui/icon-button": "workspace:^", "@leafygreen-ui/leafygreen-provider": "workspace:^", "@leafygreen-ui/lib": "workspace:^", + "@leafygreen-ui/menu": "workspace:^", "@leafygreen-ui/palette": "workspace:^", "@leafygreen-ui/tokens": "workspace:^", "@leafygreen-ui/tooltip": "workspace:^", diff --git a/packages/code-editor/src/Panel/Panel.stories.tsx b/packages/code-editor/src/Panel/Panel.stories.tsx new file mode 100644 index 0000000000..d49e54170d --- /dev/null +++ b/packages/code-editor/src/Panel/Panel.stories.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { + storybookArgTypes, + storybookExcludedControlParams, + StoryMetaType, +} from '@lg-tools/storybook-utils'; +import type { StoryFn } from '@storybook/react'; + +import { BaseFontSize } from '@leafygreen-ui/tokens'; + +import { Panel } from './Panel'; + +const meta: StoryMetaType = { + title: 'Components/CodeEditor/Panel', + component: Panel, + parameters: { + default: 'LiveExample', + controls: { + exclude: [...storybookExcludedControlParams, 'extensions'], + }, + }, + decorators: [StoryFn => ], + args: { + darkMode: false, + title: 'Panel', + showCopyButton: true, + showFormatButton: true, + showSecondaryMenuButton: true, + onCopyClick: () => {}, + onFormatClick: () => {}, + onUndoClick: () => {}, + onRedoClick: () => {}, + onDownloadClick: () => {}, + onViewShortcutsClick: () => {}, + customSecondaryButtons: [], + children: null, + baseFontSize: 13, + }, + argTypes: { + darkMode: storybookArgTypes.darkMode, + showCopyButton: { + control: { type: 'boolean' }, + }, + showFormatButton: { + control: { type: 'boolean' }, + }, + showSecondaryMenuButton: { + control: { type: 'boolean' }, + }, + onCopyClick: { + control: { type: 'function' }, + }, + onFormatClick: { + control: { type: 'function' }, + }, + onUndoClick: { + control: { type: 'function' }, + }, + onRedoClick: { + control: { type: 'function' }, + }, + onDownloadClick: { + control: { type: 'function' }, + }, + onViewShortcutsClick: { + control: { type: 'function' }, + }, + customSecondaryButtons: { + control: { type: 'array' }, + }, + children: { + control: { type: 'object' }, + }, + baseFontSize: { + control: { type: 'select' }, + options: Object.values(BaseFontSize), + }, + }, +}; + +export default meta; + +const Template: StoryFn = args => ; + +export const LiveExample = Template.bind({}); diff --git a/packages/code-editor/src/Panel/Panel.styles.ts b/packages/code-editor/src/Panel/Panel.styles.ts new file mode 100644 index 0000000000..4f975c64c7 --- /dev/null +++ b/packages/code-editor/src/Panel/Panel.styles.ts @@ -0,0 +1,50 @@ +import { css } from '@leafygreen-ui/emotion'; +import { Theme } from '@leafygreen-ui/lib'; +import { + borderRadius, + color, + InteractionState, + Variant, +} from '@leafygreen-ui/tokens'; + +export const getPanelStyles = (theme: Theme) => { + return css` + background-color: ${color[theme].background[Variant.Secondary][ + InteractionState.Default + ]}; + height: 36px; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 16px 0 10px; + border: 1px solid + ${color[theme].border[Variant.Secondary][InteractionState.Default]}; + border-bottom: none; + border-top-left-radius: ${borderRadius[300]}px; + border-top-right-radius: ${borderRadius[300]}px; + display: grid; + grid-template-columns: auto 1fr auto; + grid-template-areas: 'title children buttons'; + `; +}; + +export const getPanelTitleStyles = (theme: Theme, baseFontSize: number) => { + return css` + grid-area: title; + font-size: ${baseFontSize}px; + color: ${color[theme].text[Variant.Secondary][InteractionState.Default]}; + `; +}; + +export const getPanelInnerContentStyles = () => { + return css` + grid-area: children; + `; +}; + +export const getPanelButtonsStyles = () => { + return css` + grid-area: buttons; + `; +}; diff --git a/packages/code-editor/src/Panel/Panel.tsx b/packages/code-editor/src/Panel/Panel.tsx new file mode 100644 index 0000000000..f6a0a9c75f --- /dev/null +++ b/packages/code-editor/src/Panel/Panel.tsx @@ -0,0 +1,180 @@ +import React from 'react'; + +// @ts-ignore LG icons don't currently support TS +import DownloadIcon from '@leafygreen-ui/icon/dist/Download'; +// @ts-ignore LG icons don't currently support TS +import EllipsisIcon from '@leafygreen-ui/icon/dist/Ellipsis'; +// @ts-ignore LG icons don't currently support TS +import FormatIcon from '@leafygreen-ui/icon/dist/Format'; +// @ts-ignore LG icons don't currently support TS +import QuestionMarkWithCircleIcon from '@leafygreen-ui/icon/dist/QuestionMarkWithCircle'; +// @ts-ignore LG icons don't currently support TS +import RedoIcon from '@leafygreen-ui/icon/dist/Redo'; +// @ts-ignore LG icons don't currently support TS +import UndoIcon from '@leafygreen-ui/icon/dist/Undo'; +import IconButton from '@leafygreen-ui/icon-button'; +import { + useBaseFontSize, + useDarkMode, +} from '@leafygreen-ui/leafygreen-provider'; +import { Menu, MenuItem, MenuVariant } from '@leafygreen-ui/menu'; +import Tooltip from '@leafygreen-ui/tooltip'; + +import { useCodeEditorContext } from '../CodeEditor/CodeEditorContext'; +import { CodeEditorCopyButton } from '../CodeEditorCopyButton'; +import { CopyButtonVariant } from '../CodeEditorCopyButton/CodeEditorCopyButton.types'; + +import { + getPanelButtonsStyles, + getPanelInnerContentStyles, + getPanelStyles, + getPanelTitleStyles, +} from './Panel.styles'; +import { PanelProps } from './Panel.types'; + +/** + * Panel component provides a toolbar interface for the CodeEditor with formatting, copying, and custom action buttons. + * + * The Panel displays at the top of the CodeEditor and can include: + * - A title for the code language or content description + * - Format button for code prettification (when formatting is available) + * - Copy button for copying code to clipboard + * - Secondary menu with additional actions (undo, redo, download, view shortcuts) + * - Custom secondary buttons for application-specific actions + * - Custom children content in the left slot + * + * @example + * ```tsx + * , + * onClick: () => console.log('custom button clicked'), + * 'aria-label': 'Do custom action', + * }, + * ]} + * /> + * } + * /> + * ``` + */ +export function Panel({ + title, + showCopyButton, + showFormatButton, + showSecondaryMenuButton, + onCopyClick, + onFormatClick, + onUndoClick, + onRedoClick, + onDownloadClick, + onViewShortcutsClick, + customSecondaryButtons, + innerContent, + baseFontSize: baseFontSizeProp, + darkMode, +}: PanelProps) { + const { theme } = useDarkMode(darkMode); + const baseFontSize = useBaseFontSize(); + + // Get internal values from CodeEditor context + // This will only work when Panel is used within CodeEditor + const { getContents } = useCodeEditorContext(); + + return ( +
+
+ {title} +
+
{innerContent}
+
+ {showFormatButton && ( + + + + } + triggerEvent="hover" + darkMode={darkMode} + > + Prettify code + + )} + {showCopyButton && ( + '')} + onCopy={onCopyClick} + /> + )} + {showSecondaryMenuButton && ( + + + + } + variant={MenuVariant.Compact} + darkMode={darkMode} + > + } + onClick={onUndoClick} + aria-label="Undo changes" + > + Undo + + } + onClick={onRedoClick} + aria-label="Redo changes" + > + Redo + + } + onClick={onDownloadClick} + aria-label="Download code" + > + Download + + } + onClick={onViewShortcutsClick} + aria-label="View shortcuts" + > + View shortcuts + + {customSecondaryButtons?.map( + ({ label, glyph, onClick, href, 'aria-label': ariaLabel }) => ( + + {label} + + ), + )} + + )} +
+
+ ); +} diff --git a/packages/code-editor/src/Panel/Panel.types.ts b/packages/code-editor/src/Panel/Panel.types.ts new file mode 100644 index 0000000000..f4a8ea12ce --- /dev/null +++ b/packages/code-editor/src/Panel/Panel.types.ts @@ -0,0 +1,118 @@ +import { DarkModeProps } from '@leafygreen-ui/lib'; +import { type MenuItemProps } from '@leafygreen-ui/menu'; + +/** + * Configuration object for custom secondary buttons that appear in the Panel's secondary menu. + * Extends MenuItemProps to inherit all MenuItem functionality while adding required label. + */ +type SecondaryButtonConfig = MenuItemProps & { + /** + * Text label for the button that will be displayed in the menu item. + */ + label: string; + + /** + * Optional icon element to display alongside the button label. + */ + glyph?: React.ReactElement; + + /** + * Optional URL to navigate to when the button is clicked. + * If provided, the button will behave as a link. + */ + href?: string; + + /** + * Optional callback function that will be called when the button is clicked. + */ + onClick?: () => void; + + /** + * Optional accessible label for the button to provide additional context for screen readers. + * If not provided, the label will be used as the aria-label. + */ + 'aria-label'?: string; +}; + +export interface PanelProps extends DarkModeProps { + /** + * Font size of text in the panel. + * Controls the typography scale used for the panel title and other text elements. + * + * @default 14 + */ + baseFontSize?: 14 | 16; + + /** + * Optional array of custom secondary buttons to display in the secondary menu. + * Each button can include a label, icon, click handler, href, and aria-label for accessibility. + */ + customSecondaryButtons?: Array; + + /** + * Optional React node to render between the title and the buttons. + * Can be used to add custom controls to the panel. + */ + innerContent?: React.ReactNode; + + /** + * Optional callback fired when the copy button is clicked. + * Called after the copy operation is attempted. + */ + onCopyClick?: () => void; + + /** + * Optional callback fired when the download button in the secondary menu is clicked. + * Called after the download operation is attempted. + */ + onDownloadClick?: () => void; + + /** + * Optional callback fired when the format button is clicked. + * Called after the formatting operation is attempted. + */ + onFormatClick?: () => void; + + /** + * Optional callback fired when the redo button in the secondary menu is clicked. + * Called after the redo operation is attempted. + */ + onRedoClick?: () => void; + + /** + * Optional callback fired when the undo button in the secondary menu is clicked. + * Called after the undo operation is attempted. + */ + onUndoClick?: () => void; + + /** + * Optional callback fired when the view shortcuts button in the secondary menu is clicked. + * Called after the view shortcuts operation is attempted. + */ + onViewShortcutsClick?: () => void; + + /** + * Determines whether to show the copy button in the panel. + * When enabled, users can copy the editor content to their clipboard. + */ + showCopyButton?: boolean; + + /** + * Determines whether to show the format button in the panel. + * When enabled and formatting is available for the current language, + * users can format/prettify their code. + */ + showFormatButton?: boolean; + + /** + * Determines whether to show the secondary menu button (ellipsis icon) in the panel. + * When enabled, displays a menu with additional actions like undo, redo, download, and view shortcuts. + */ + showSecondaryMenuButton?: boolean; + + /** + * Optional title text to display in the panel header. + * Typically used to show the current language or content description. + */ + title?: string; +} diff --git a/packages/code-editor/src/Panel/index.ts b/packages/code-editor/src/Panel/index.ts new file mode 100644 index 0000000000..8103a76a0a --- /dev/null +++ b/packages/code-editor/src/Panel/index.ts @@ -0,0 +1,2 @@ +export { Panel } from './Panel'; +export { type PanelProps } from './Panel.types'; diff --git a/packages/code-editor/src/index.ts b/packages/code-editor/src/index.ts index 72da68b364..aeacb5aacb 100644 --- a/packages/code-editor/src/index.ts +++ b/packages/code-editor/src/index.ts @@ -26,3 +26,4 @@ export { useThemeExtension, useTooltipExtension, } from './CodeEditor'; +export { Panel, type PanelProps } from './Panel'; diff --git a/packages/code-editor/tsconfig.json b/packages/code-editor/tsconfig.json index 37c1123ccf..c8db44899f 100644 --- a/packages/code-editor/tsconfig.json +++ b/packages/code-editor/tsconfig.json @@ -24,6 +24,9 @@ { "path": "../lib" }, + { + "path": "../menu" + }, { "path": "../palette" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eff8fa8d90..acb4b892cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1359,6 +1359,9 @@ importers: '@leafygreen-ui/lib': specifier: workspace:^ version: link:../lib + '@leafygreen-ui/menu': + specifier: workspace:^ + version: link:../menu '@leafygreen-ui/palette': specifier: workspace:^ version: link:../palette @@ -2084,7 +2087,7 @@ importers: version: 4.16.1 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814) + version: 10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815) xml2json: specifier: ^0.12.0 version: 0.12.0 @@ -4156,10 +4159,10 @@ importers: version: 8.6.12(storybook@8.6.14(prettier@2.8.8)) '@storybook/react': specifier: 8.6.12 - version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814) + version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815) '@storybook/react-webpack5': specifier: 8.6.12 - version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(esbuild@0.25.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814) + version: 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(esbuild@0.25.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815) '@storybook/test': specifier: 8.6.12 version: 8.6.12(storybook@8.6.14(prettier@2.8.8)) @@ -4168,7 +4171,7 @@ importers: version: 8.6.12(storybook@8.6.14(prettier@2.8.8)) '@svgr/webpack': specifier: 8.0.1 - version: 8.0.1(typescript@6.0.0-dev.20250814) + version: 8.0.1(typescript@6.0.0-dev.20250815) assert: specifier: ^2.1.0 version: 2.1.0 @@ -4210,7 +4213,7 @@ importers: version: 18.3.1 react-docgen-typescript: specifier: 2.2.2 - version: 2.2.2(typescript@6.0.0-dev.20250814) + version: 2.2.2(typescript@6.0.0-dev.20250815) react-dom: specifier: ^17.0.0 || ^18.0.0 version: 18.3.1(react@18.3.1) @@ -4361,7 +4364,7 @@ importers: version: 11.1.1 jest: specifier: 29.6.2 - version: 29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)) + version: 29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)) jest-axe: specifier: 8.0.0 version: 8.0.0 @@ -11165,8 +11168,8 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@6.0.0-dev.20250814: - resolution: {integrity: sha512-qOUcNivJk/oqLBnjhvkUtDMuFjBnG8a3L32cM0qdRfm5x/+OZ6sYwGrfCQcsx+dnd3UW7iOPdOIfDt/Pa1428g==} + typescript@6.0.0-dev.20250815: + resolution: {integrity: sha512-pUWzMiOyuu14oNnD+6FYpdZzMBzUyucgmKtCCXR6lE5H+ndo59vxUx8MZaB+QDEfSSobIzAtaLwW/kRzfhV2CA==} engines: {node: '>=14.17'} hasBin: true @@ -13878,7 +13881,7 @@ snapshots: - ts-node optional: true - '@jest/core@29.6.2(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814))': + '@jest/core@29.6.2(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -13892,7 +13895,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)) + jest-config: 29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -13949,7 +13952,7 @@ snapshots: - ts-node optional: true - '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814))': + '@jest/core@29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -13963,7 +13966,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)) + jest-config: 29.7.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -14629,7 +14632,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@storybook/builder-webpack5@8.6.12(esbuild@0.25.8)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814)': + '@storybook/builder-webpack5@8.6.12(esbuild@0.25.8)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815)': dependencies: '@storybook/core-webpack': 8.6.12(storybook@8.6.14(prettier@2.8.8)) '@types/semver': 7.7.0 @@ -14639,7 +14642,7 @@ snapshots: constants-browserify: 1.0.0 css-loader: 6.11.0(webpack@5.88.0(esbuild@0.25.8)) es-module-lexer: 1.7.0 - fork-ts-checker-webpack-plugin: 8.0.0(typescript@6.0.0-dev.20250814)(webpack@5.88.0(esbuild@0.25.8)) + fork-ts-checker-webpack-plugin: 8.0.0(typescript@6.0.0-dev.20250815)(webpack@5.88.0(esbuild@0.25.8)) html-webpack-plugin: 5.6.3(webpack@5.88.0(esbuild@0.25.8)) magic-string: 0.30.17 path-browserify: 1.0.1 @@ -14657,7 +14660,7 @@ snapshots: webpack-hot-middleware: 2.26.1 webpack-virtual-modules: 0.6.2 optionalDependencies: - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 transitivePeerDependencies: - '@rspack/core' - '@swc/core' @@ -14741,11 +14744,11 @@ snapshots: dependencies: storybook: 8.6.14(prettier@2.8.8) - '@storybook/preset-react-webpack@8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(esbuild@0.25.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814)': + '@storybook/preset-react-webpack@8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(esbuild@0.25.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815)': dependencies: '@storybook/core-webpack': 8.6.12(storybook@8.6.14(prettier@2.8.8)) - '@storybook/react': 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814) - '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@6.0.0-dev.20250814)(webpack@5.88.0(esbuild@0.25.8)) + '@storybook/react': 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815) + '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@6.0.0-dev.20250815)(webpack@5.88.0(esbuild@0.25.8)) '@types/semver': 7.7.0 find-up: 5.0.0 magic-string: 0.30.17 @@ -14758,7 +14761,7 @@ snapshots: tsconfig-paths: 4.2.0 webpack: 5.88.0(esbuild@0.25.8) optionalDependencies: - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 transitivePeerDependencies: - '@storybook/test' - '@swc/core' @@ -14771,16 +14774,16 @@ snapshots: dependencies: storybook: 8.6.14(prettier@2.8.8) - '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@6.0.0-dev.20250814)(webpack@5.88.0(esbuild@0.25.8))': + '@storybook/react-docgen-typescript-plugin@1.0.6--canary.9.0c3f3b7.0(typescript@6.0.0-dev.20250815)(webpack@5.88.0(esbuild@0.25.8))': dependencies: debug: 4.4.1 endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.2.0 micromatch: 4.0.8 - react-docgen-typescript: 2.2.2(typescript@6.0.0-dev.20250814) + react-docgen-typescript: 2.2.2(typescript@6.0.0-dev.20250815) tslib: 2.8.1 - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 webpack: 5.88.0(esbuild@0.25.8) transitivePeerDependencies: - supports-color @@ -14791,16 +14794,16 @@ snapshots: react-dom: 18.3.1(react@18.3.1) storybook: 8.6.14(prettier@2.8.8) - '@storybook/react-webpack5@8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(esbuild@0.25.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814)': + '@storybook/react-webpack5@8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(esbuild@0.25.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815)': dependencies: - '@storybook/builder-webpack5': 8.6.12(esbuild@0.25.8)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814) - '@storybook/preset-react-webpack': 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(esbuild@0.25.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814) - '@storybook/react': 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814) + '@storybook/builder-webpack5': 8.6.12(esbuild@0.25.8)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815) + '@storybook/preset-react-webpack': 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(esbuild@0.25.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815) + '@storybook/react': 8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) storybook: 8.6.14(prettier@2.8.8) optionalDependencies: - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 transitivePeerDependencies: - '@rspack/core' - '@storybook/test' @@ -14825,7 +14828,7 @@ snapshots: '@storybook/test': 8.6.12(storybook@8.6.14(prettier@2.8.8)) typescript: 5.8.3 - '@storybook/react@8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250814)': + '@storybook/react@8.6.12(@storybook/test@8.6.12(storybook@8.6.14(prettier@2.8.8)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.6.14(prettier@2.8.8))(typescript@6.0.0-dev.20250815)': dependencies: '@storybook/components': 8.6.12(storybook@8.6.14(prettier@2.8.8)) '@storybook/global': 5.0.0 @@ -14838,7 +14841,7 @@ snapshots: storybook: 8.6.14(prettier@2.8.8) optionalDependencies: '@storybook/test': 8.6.12(storybook@8.6.14(prettier@2.8.8)) - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 '@storybook/test@8.5.3(storybook@8.6.14(prettier@2.8.8))': dependencies: @@ -15004,12 +15007,12 @@ snapshots: - supports-color - typescript - '@svgr/core@8.0.0(typescript@6.0.0-dev.20250814)': + '@svgr/core@8.0.0(typescript@6.0.0-dev.20250815)': dependencies: '@babel/core': 7.24.3 '@svgr/babel-preset': 8.0.0(@babel/core@7.24.3) camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@6.0.0-dev.20250814) + cosmiconfig: 8.3.6(typescript@6.0.0-dev.20250815) snake-case: 3.0.4 transitivePeerDependencies: - supports-color @@ -15054,11 +15057,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@svgr/plugin-jsx@8.0.1(@svgr/core@8.0.0(typescript@6.0.0-dev.20250814))': + '@svgr/plugin-jsx@8.0.1(@svgr/core@8.0.0(typescript@6.0.0-dev.20250815))': dependencies: '@babel/core': 7.24.3 '@svgr/babel-preset': 8.0.0(@babel/core@7.24.3) - '@svgr/core': 8.0.0(typescript@6.0.0-dev.20250814) + '@svgr/core': 8.0.0(typescript@6.0.0-dev.20250815) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: @@ -15089,10 +15092,10 @@ snapshots: transitivePeerDependencies: - typescript - '@svgr/plugin-svgo@8.0.1(@svgr/core@8.0.0(typescript@6.0.0-dev.20250814))(typescript@6.0.0-dev.20250814)': + '@svgr/plugin-svgo@8.0.1(@svgr/core@8.0.0(typescript@6.0.0-dev.20250815))(typescript@6.0.0-dev.20250815)': dependencies: - '@svgr/core': 8.0.0(typescript@6.0.0-dev.20250814) - cosmiconfig: 8.3.6(typescript@6.0.0-dev.20250814) + '@svgr/core': 8.0.0(typescript@6.0.0-dev.20250815) + cosmiconfig: 8.3.6(typescript@6.0.0-dev.20250815) deepmerge: 4.3.1 svgo: 3.3.2 transitivePeerDependencies: @@ -15123,16 +15126,16 @@ snapshots: - supports-color - typescript - '@svgr/webpack@8.0.1(typescript@6.0.0-dev.20250814)': + '@svgr/webpack@8.0.1(typescript@6.0.0-dev.20250815)': dependencies: '@babel/core': 7.24.3 '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.24.3) '@babel/preset-env': 7.24.3(@babel/core@7.24.3) '@babel/preset-react': 7.24.1(@babel/core@7.24.3) '@babel/preset-typescript': 7.24.1(@babel/core@7.24.3) - '@svgr/core': 8.0.0(typescript@6.0.0-dev.20250814) - '@svgr/plugin-jsx': 8.0.1(@svgr/core@8.0.0(typescript@6.0.0-dev.20250814)) - '@svgr/plugin-svgo': 8.0.1(@svgr/core@8.0.0(typescript@6.0.0-dev.20250814))(typescript@6.0.0-dev.20250814) + '@svgr/core': 8.0.0(typescript@6.0.0-dev.20250815) + '@svgr/plugin-jsx': 8.0.1(@svgr/core@8.0.0(typescript@6.0.0-dev.20250815)) + '@svgr/plugin-svgo': 8.0.1(@svgr/core@8.0.0(typescript@6.0.0-dev.20250815))(typescript@6.0.0-dev.20250815) transitivePeerDependencies: - supports-color - typescript @@ -16472,14 +16475,14 @@ snapshots: optionalDependencies: typescript: 5.8.3 - cosmiconfig@8.3.6(typescript@6.0.0-dev.20250814): + cosmiconfig@8.3.6(typescript@6.0.0-dev.20250815): dependencies: import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 create-ecdh@4.0.4: dependencies: @@ -16861,7 +16864,7 @@ snapshots: dependencies: semver: 7.7.2 shelljs: 0.8.5 - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 dunder-proto@1.0.1: dependencies: @@ -17481,7 +17484,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@8.0.0(typescript@6.0.0-dev.20250814)(webpack@5.88.0(esbuild@0.25.8)): + fork-ts-checker-webpack-plugin@8.0.0(typescript@6.0.0-dev.20250815)(webpack@5.88.0(esbuild@0.25.8)): dependencies: '@babel/code-frame': 7.27.1 chalk: 4.1.2 @@ -17495,7 +17498,7 @@ snapshots: schema-utils: 3.3.0 semver: 7.7.2 tapable: 2.2.2 - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 webpack: 5.88.0(esbuild@0.25.8) form-data@2.5.5: @@ -18179,16 +18182,16 @@ snapshots: - ts-node optional: true - jest-cli@29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)): + jest-cli@29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)): dependencies: - '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)) + '@jest/core': 29.7.0(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 import-local: 3.2.0 - jest-config: 29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)) + jest-config: 29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)) jest-util: 29.7.0 jest-validate: 29.7.0 prompts: 2.4.2 @@ -18231,7 +18234,7 @@ snapshots: - supports-color optional: true - jest-config@29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)): + jest-config@29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)): dependencies: '@babel/core': 7.24.3 '@jest/test-sequencer': 29.7.0 @@ -18257,7 +18260,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.19.9 - ts-node: 10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814) + ts-node: 10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -18294,7 +18297,7 @@ snapshots: - supports-color optional: true - jest-config@29.7.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)): + jest-config@29.7.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)): dependencies: '@babel/core': 7.24.3 '@jest/test-sequencer': 29.7.0 @@ -18320,7 +18323,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.19.9 - ts-node: 10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814) + ts-node: 10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -18588,12 +18591,12 @@ snapshots: - ts-node optional: true - jest@29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)): + jest@29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)): dependencies: - '@jest/core': 29.6.2(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)) + '@jest/core': 29.6.2(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814)) + jest-cli: 29.6.2(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -19986,9 +19989,9 @@ snapshots: dependencies: typescript: 5.8.3 - react-docgen-typescript@2.2.2(typescript@6.0.0-dev.20250814): + react-docgen-typescript@2.2.2(typescript@6.0.0-dev.20250815): dependencies: - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 react-docgen@7.1.1: dependencies: @@ -20930,7 +20933,7 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250814): + ts-node@10.9.2(@types/node@20.19.9)(typescript@6.0.0-dev.20250815): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -20944,7 +20947,7 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 - typescript: 6.0.0-dev.20250814 + typescript: 6.0.0-dev.20250815 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -21056,7 +21059,7 @@ snapshots: typescript@5.8.3: {} - typescript@6.0.0-dev.20250814: {} + typescript@6.0.0-dev.20250815: {} unbox-primitive@1.1.0: dependencies: From 740b9dbe64f77823c5146ad8eb4a2bc6d1639f34 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 15 Aug 2025 13:24:45 -0400 Subject: [PATCH 05/21] changeset --- .changeset/plenty-cities-buy.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/plenty-cities-buy.md diff --git a/.changeset/plenty-cities-buy.md b/.changeset/plenty-cities-buy.md new file mode 100644 index 0000000000..c5fc84cff3 --- /dev/null +++ b/.changeset/plenty-cities-buy.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/code-editor': minor +--- + +Adds a **Panel component**: A toolbar interface for CodeEditor with formatting, copying, and custom action buttons From 72f1988fcde43296c15f58a1c43db61e21c34461 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 15 Aug 2025 14:49:53 -0400 Subject: [PATCH 06/21] fix(Panel): disable dark menu rendering in the Panel component - Updated the Panel component to set `renderDarkMenu` to false, ensuring the dark menu is not rendered. - This change improves the consistency of the user interface in light mode. --- packages/code-editor/src/Panel/Panel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/code-editor/src/Panel/Panel.tsx b/packages/code-editor/src/Panel/Panel.tsx index f6a0a9c75f..2aa6026398 100644 --- a/packages/code-editor/src/Panel/Panel.tsx +++ b/packages/code-editor/src/Panel/Panel.tsx @@ -130,6 +130,7 @@ export function Panel({ } variant={MenuVariant.Compact} darkMode={darkMode} + renderDarkMenu={false} > } From 85ccbf08d99f9cd73ed89c924a97733feccd7acb Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 15 Aug 2025 15:09:01 -0400 Subject: [PATCH 07/21] feat(CodeEditor): integrate Panel component and enhance CodeEditor functionality - Added the `Panel` component to the `CodeEditor`, allowing for a customizable toolbar with options for formatting, copying, and additional actions. - Updated `CodeEditor` to accept a `panel` prop, enabling the rendering of the `Panel` within the editor. - Enhanced TypeScript definitions to support the new `panel` prop and its interaction with the `copyButtonAppearance` prop. - Modified styling in `useThemeExtension` to accommodate the new panel layout. - Removed the `Panel.stories.tsx` file as part of the integration process. --- .../code-editor/src/CodeEditor.stories.tsx | 16 +++- .../code-editor/src/CodeEditor/CodeEditor.tsx | 29 +++++-- .../src/CodeEditor/CodeEditor.types.ts | 64 ++++++++++---- .../hooks/extensions/useThemeExtension.ts | 11 ++- .../code-editor/src/Panel/Panel.stories.tsx | 85 ------------------- 5 files changed, 90 insertions(+), 115 deletions(-) delete mode 100644 packages/code-editor/src/Panel/Panel.stories.tsx diff --git a/packages/code-editor/src/CodeEditor.stories.tsx b/packages/code-editor/src/CodeEditor.stories.tsx index 5e842e5186..0b57610b74 100644 --- a/packages/code-editor/src/CodeEditor.stories.tsx +++ b/packages/code-editor/src/CodeEditor.stories.tsx @@ -13,7 +13,7 @@ import { CopyButtonAppearance } from './CodeEditor/CodeEditor.types'; import { LanguageName } from './CodeEditor/hooks/extensions/useLanguageExtension'; import { codeSnippets } from './CodeEditor/testing'; import { IndentUnits } from './CodeEditor'; -import { CodeEditor } from '.'; +import { CodeEditor, Panel } from '.'; const MyTooltip = ({ line, @@ -180,6 +180,20 @@ const Template: StoryFn = args => ; export const LiveExample = Template.bind({}); +export const WithPanel = Template.bind({}); +WithPanel.args = { + language: 'typescript', + defaultValue: codeSnippets.typescript, + panel: ( + + ), +}; + export const TooltipOnHover: StoryObj<{}> = { render: () => { return ( diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index fc5923b944..baf73708b7 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -27,6 +27,7 @@ import { CopyButtonLgId, type HTMLElementWithCodeMirror, } from './CodeEditor.types'; +import { CodeEditorProvider } from './CodeEditorContext'; import { useExtensions, useLazyModules, useModuleLoaders } from './hooks'; export const CodeEditor = forwardRef( @@ -58,6 +59,7 @@ export const CodeEditor = forwardRef( readOnly, tooltips, copyButtonAppearance, + panel, ...rest } = props; @@ -147,8 +149,13 @@ export const CodeEditor = forwardRef( useImperativeHandle(forwardedRef, () => ({ getEditorViewInstance: () => editorViewRef.current, + getContents, })); + const contextValue = { + getContents, + }; + return (
( })} {...rest} > - {(copyButtonAppearance === CopyButtonAppearance.Hover || - copyButtonAppearance === CopyButtonAppearance.Persist) && ( - + {panel && ( + {panel} )} + {!panel && + (copyButtonAppearance === CopyButtonAppearance.Hover || + copyButtonAppearance === CopyButtonAppearance.Persist) && ( + + )} {(isLoadingProp || isLoading) && (
` sub-component which will render the top panel with a language switcher, custom action buttons, and copy button. If no props are passed to the panel sub-component, the panel will render with only the copy button. Note: `copyButtonAppearance` cannot be used with `panel`. Either use `copyButtonAppearance` or `panel`, not both. + * + */ + panel?: never; + } + | { + /** + * Determines the appearance of the copy button without a panel. The copy button allows the code block to be copied to the user's clipboard by clicking the button. + * + * If `hover`, the copy button will only appear when the user hovers over the code block. On mobile devices, the copy button will always be visible. + * + * If `persist`, the copy button will always be visible. + * + * If `none`, the copy button will not be rendered. + * + * Note: 'panel' cannot be used with `copyButtonAppearance`. Either use `copyButtonAppearance` or `panel`, not both. + * + * @default `hover` + */ + copyButtonAppearance?: never; + + /** + * Slot to pass the `` sub-component which will render the top panel with a language switcher, custom action buttons, and copy button. If no props are passed to the panel sub-component, the panel will render with only the copy button. Note: `copyButtonAppearance` cannot be used with `panel`. Either use `copyButtonAppearance` or `panel`, not both. + * + */ + panel?: React.ReactNode; + } + ); /** * Imperative handle for the CodeEditor component. diff --git a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts index 25d4ab57b0..9d205d7d46 100644 --- a/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts +++ b/packages/code-editor/src/CodeEditor/hooks/extensions/useThemeExtension.ts @@ -67,10 +67,15 @@ export function useThemeExtension({ color[theme].background[Variant.Primary][ InteractionState.Default ], - color: color[theme].text[Variant.Primary][InteractionState.Default], border: `1px solid - ${color[theme].border[Variant.Secondary][InteractionState.Default]}`, - borderRadius: `${borderRadius[300]}px`, + ${ + color[theme].border[Variant.Secondary][InteractionState.Default] + }`, + borderBottomLeftRadius: `${borderRadius[300]}px`, + borderBottomRightRadius: `${borderRadius[300]}px`, + borderTopLeftRadius: props.panel ? 0 : `${borderRadius[300]}px`, + borderTopRightRadius: props.panel ? 0 : `${borderRadius[300]}px`, + color: color[theme].text[Variant.Primary][InteractionState.Default], paddingTop: `${spacing[200]}px`, paddingBottom: `${spacing[200]}px`, }, diff --git a/packages/code-editor/src/Panel/Panel.stories.tsx b/packages/code-editor/src/Panel/Panel.stories.tsx deleted file mode 100644 index d49e54170d..0000000000 --- a/packages/code-editor/src/Panel/Panel.stories.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; -import { - storybookArgTypes, - storybookExcludedControlParams, - StoryMetaType, -} from '@lg-tools/storybook-utils'; -import type { StoryFn } from '@storybook/react'; - -import { BaseFontSize } from '@leafygreen-ui/tokens'; - -import { Panel } from './Panel'; - -const meta: StoryMetaType = { - title: 'Components/CodeEditor/Panel', - component: Panel, - parameters: { - default: 'LiveExample', - controls: { - exclude: [...storybookExcludedControlParams, 'extensions'], - }, - }, - decorators: [StoryFn => ], - args: { - darkMode: false, - title: 'Panel', - showCopyButton: true, - showFormatButton: true, - showSecondaryMenuButton: true, - onCopyClick: () => {}, - onFormatClick: () => {}, - onUndoClick: () => {}, - onRedoClick: () => {}, - onDownloadClick: () => {}, - onViewShortcutsClick: () => {}, - customSecondaryButtons: [], - children: null, - baseFontSize: 13, - }, - argTypes: { - darkMode: storybookArgTypes.darkMode, - showCopyButton: { - control: { type: 'boolean' }, - }, - showFormatButton: { - control: { type: 'boolean' }, - }, - showSecondaryMenuButton: { - control: { type: 'boolean' }, - }, - onCopyClick: { - control: { type: 'function' }, - }, - onFormatClick: { - control: { type: 'function' }, - }, - onUndoClick: { - control: { type: 'function' }, - }, - onRedoClick: { - control: { type: 'function' }, - }, - onDownloadClick: { - control: { type: 'function' }, - }, - onViewShortcutsClick: { - control: { type: 'function' }, - }, - customSecondaryButtons: { - control: { type: 'array' }, - }, - children: { - control: { type: 'object' }, - }, - baseFontSize: { - control: { type: 'select' }, - options: Object.values(BaseFontSize), - }, - }, -}; - -export default meta; - -const Template: StoryFn = args => ; - -export const LiveExample = Template.bind({}); From e8dbc88d47da3e7b070a7bcfe6960ca599aafc96 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 15 Aug 2025 15:11:24 -0400 Subject: [PATCH 08/21] refactor(CodeEditor, Panel): reorganize props for improved clarity and consistency - Rearranged the props in both `CodeEditor` and `Panel` components for better readability and logical grouping. - Ensured that related props are positioned together, enhancing the overall structure of the components. - Maintained existing functionality while improving the code organization. --- .../code-editor/src/CodeEditor/CodeEditor.tsx | 34 +++++++++---------- packages/code-editor/src/Panel/Panel.tsx | 20 +++++------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index baf73708b7..11982f209f 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -33,33 +33,33 @@ import { useExtensions, useLazyModules, useModuleLoaders } from './hooks'; export const CodeEditor = forwardRef( (props, forwardedRef) => { const { - defaultValue, - value, - forceParsing: forceParsingProp = false, - onChange: onChangeProp, - isLoading: isLoadingProp = false, - extensions: consumerExtensions = [], - darkMode: darkModeProp, baseFontSize: baseFontSizeProp, className, - height, - maxHeight, - maxWidth, - minHeight, - minWidth, - width, - language, + copyButtonAppearance, + darkMode: darkModeProp, + defaultValue, enableClickableUrls, enableCodeFolding, enableLineNumbers, enableLineWrapping, - indentUnit, + extensions: consumerExtensions = [], + forceParsing: forceParsingProp = false, + height, indentSize, + indentUnit, + isLoading: isLoadingProp = false, + language, + maxHeight, + maxWidth, + minHeight, + minWidth, + onChange: onChangeProp, + panel, placeholder, readOnly, tooltips, - copyButtonAppearance, - panel, + value, + width, ...rest } = props; diff --git a/packages/code-editor/src/Panel/Panel.tsx b/packages/code-editor/src/Panel/Panel.tsx index 2aa6026398..6a817ababe 100644 --- a/packages/code-editor/src/Panel/Panel.tsx +++ b/packages/code-editor/src/Panel/Panel.tsx @@ -68,20 +68,20 @@ import { PanelProps } from './Panel.types'; * ``` */ export function Panel({ - title, - showCopyButton, - showFormatButton, - showSecondaryMenuButton, + baseFontSize: baseFontSizeProp, + customSecondaryButtons, + darkMode, + innerContent, onCopyClick, + onDownloadClick, onFormatClick, - onUndoClick, onRedoClick, - onDownloadClick, + onUndoClick, onViewShortcutsClick, - customSecondaryButtons, - innerContent, - baseFontSize: baseFontSizeProp, - darkMode, + showCopyButton, + showFormatButton, + showSecondaryMenuButton, + title, }: PanelProps) { const { theme } = useDarkMode(darkMode); const baseFontSize = useBaseFontSize(); From 3d2670d10ac723cd0f62a3f1b4aeabe5da7619c7 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 15 Aug 2025 15:12:45 -0400 Subject: [PATCH 09/21] docs(CodeEditor): update README to include new `panel` prop details - Added documentation for the optional `panel` prop in the CodeEditor, which allows rendering a toolbar interface at the top of the editor. - Provided a description of the `Panel` component's functionality and its available options, enhancing the clarity of the README. --- packages/code-editor/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/code-editor/README.md b/packages/code-editor/README.md index 3d5c10c95e..38f78fc078 100644 --- a/packages/code-editor/README.md +++ b/packages/code-editor/README.md @@ -68,6 +68,7 @@ console.log(greet('MongoDB user'));`; | `minHeight` _(optional)_ | Sets the editor's minimum height. | `string` | `undefined` | | `minWidth` _(optional)_ | Sets the editor's minimum width. | `string` | `undefined` | | `onChange` _(optional)_ | Callback that receives the updated editor value when changes are made. | `(value: string) => void` | `undefined` | +| `panel` _(optional)_ | Panel component to render at the top of the CodeEditor. Provides a toolbar interface with formatting, copying, and custom action buttons. See the Panel component documentation for available options. | `React.ReactNode` | `undefined` | | `placeholder` _(optional)_ | Value to display in the editor when it is empty. | `HTMLElement \| string` | `undefined` | | `readOnly` _(optional)_ | Enables read only mode, making the contents uneditable. | `boolean` | `false` | | `tooltips` _(optional)_ | Add tooltips to the editor content that appear on hover. | `Array` | `undefined` | From 76c646d77d06d57e9a75164d210c46bbaec1ab51 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 15 Aug 2025 15:41:25 -0400 Subject: [PATCH 10/21] test(Panel): add comprehensive unit tests for Panel component functionality - Introduced a new test suite for the Panel component, covering various aspects including rendering, button functionality, and accessibility. - Implemented tests for the format, copy, and secondary menu buttons, ensuring they behave as expected when interacted with. - Verified the integration with the CodeEditor context and confirmed proper handling of props like `showFormatButton`, `showCopyButton`, and custom secondary buttons. - Included tests for edge cases, such as handling undefined callback functions and empty custom button arrays, to enhance robustness. --- packages/code-editor/src/Panel/Panel.spec.tsx | 487 ++++++++++++++++++ 1 file changed, 487 insertions(+) create mode 100644 packages/code-editor/src/Panel/Panel.spec.tsx diff --git a/packages/code-editor/src/Panel/Panel.spec.tsx b/packages/code-editor/src/Panel/Panel.spec.tsx new file mode 100644 index 0000000000..abbad5fdf0 --- /dev/null +++ b/packages/code-editor/src/Panel/Panel.spec.tsx @@ -0,0 +1,487 @@ +import React from 'react'; +import { act, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import LeafyGreenProvider from '@leafygreen-ui/leafygreen-provider'; + +import '@testing-library/jest-dom'; + +import { CodeEditorProvider } from '../CodeEditor/CodeEditorContext'; + +import { Panel } from './Panel'; +import { PanelProps } from './Panel.types'; + +const TestIcon = () =>
; +TestIcon.displayName = 'TestIcon'; + +const defaultProps: Partial = { + title: 'Test Panel', +}; + +const mockGetContents = jest.fn(() => 'test content'); + +const renderPanel = (props: Partial = {}) => { + const mergedProps = { ...defaultProps, ...props }; + + return render( + + + + + , + ); +}; + +// Mock the clipboard API +const mockWriteText = jest.fn(); +Object.assign(navigator, { + clipboard: { + writeText: mockWriteText, + }, +}); + +// Mock document.execCommand for fallback testing +const mockExecCommand = jest.fn(); +Object.assign(document, { + execCommand: mockExecCommand, +}); + +describe('Panel', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Basic Rendering', () => { + it('renders with title', () => { + renderPanel({ title: 'JavaScript Editor' }); + expect(screen.getByText('JavaScript Editor')).toBeInTheDocument(); + }); + + it('renders without title when not provided', () => { + const { container } = renderPanel({ title: undefined }); + // The title div should still exist but be empty + const titleElement = container.querySelector('.leafygreen-ui-12f55h'); + expect(titleElement).toBeInTheDocument(); + expect(titleElement).toHaveTextContent(''); + }); + + it('renders inner content when provided', () => { + const innerContent = ( + Custom Content + ); + renderPanel({ innerContent }); + expect(screen.getByTestId('inner-content')).toBeInTheDocument(); + expect(screen.getByText('Custom Content')).toBeInTheDocument(); + }); + }); + + describe('Format Button', () => { + it('renders format button when showFormatButton is true', () => { + renderPanel({ showFormatButton: true }); + expect(screen.getByLabelText('Format code')).toBeInTheDocument(); + }); + + it('does not render format button when showFormatButton is false', () => { + renderPanel({ showFormatButton: false }); + expect(screen.queryByLabelText('Format code')).not.toBeInTheDocument(); + }); + + it('calls onFormatClick when format button is clicked', async () => { + const onFormatClick = jest.fn(); + renderPanel({ showFormatButton: true, onFormatClick }); + + const formatButton = screen.getByLabelText('Format code'); + + await act(async () => { + await userEvent.click(formatButton); + }); + + expect(onFormatClick).toHaveBeenCalledTimes(1); + }); + + it('shows tooltip on format button hover', async () => { + renderPanel({ showFormatButton: true }); + + const formatButton = screen.getByLabelText('Format code'); + + await act(async () => { + await userEvent.hover(formatButton); + }); + + // Wait for tooltip to appear + await waitFor(async () => { + expect(screen.getByText('Prettify code')).toBeInTheDocument(); + }); + }); + }); + + describe('Copy Button', () => { + it('renders copy button when showCopyButton is true', () => { + renderPanel({ showCopyButton: true }); + expect(screen.getByLabelText('Copy text')).toBeInTheDocument(); + }); + + it('does not render copy button when showCopyButton is false', () => { + renderPanel({ showCopyButton: false }); + expect(screen.queryByLabelText('Copy text')).not.toBeInTheDocument(); + }); + + it('calls onCopyClick when copy button is clicked', async () => { + const onCopyClick = jest.fn(); + renderPanel({ showCopyButton: true, onCopyClick }); + + const copyButton = screen.getByLabelText('Copy text'); + + await act(async () => { + await userEvent.click(copyButton); + }); + + expect(onCopyClick).toHaveBeenCalledTimes(1); + }); + + it('shows tooltip on copy button hover', async () => { + renderPanel({ showCopyButton: true }); + + const copyButton = screen.getByLabelText('Copy text'); + + await act(async () => { + await userEvent.hover(copyButton); + }); + + // Wait for tooltip to appear + await waitFor(async () => { + expect(screen.getByText('Copy')).toBeInTheDocument(); + }); + }); + + it('shows success state after copying', async () => { + renderPanel({ showCopyButton: true }); + + const copyButton = screen.getByLabelText('Copy text'); + + await act(async () => { + await userEvent.click(copyButton); + }); + + // Wait for success tooltip to appear + await waitFor(async () => { + expect(screen.getByText('Copied!')).toBeInTheDocument(); + }); + }); + }); + + describe('Secondary Menu', () => { + it('renders secondary menu button when showSecondaryMenuButton is true', () => { + renderPanel({ showSecondaryMenuButton: true }); + expect(screen.getByLabelText('Show more actions')).toBeInTheDocument(); + }); + + it('does not render secondary menu button when showSecondaryMenuButton is false', () => { + renderPanel({ showSecondaryMenuButton: false }); + expect( + screen.queryByLabelText('Show more actions'), + ).not.toBeInTheDocument(); + }); + + it('opens menu when secondary menu button is clicked', async () => { + renderPanel({ showSecondaryMenuButton: true }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + await waitFor(() => { + expect(screen.getByText('Undo')).toBeInTheDocument(); + expect(screen.getByText('Redo')).toBeInTheDocument(); + expect(screen.getByText('Download')).toBeInTheDocument(); + expect(screen.getByText('View shortcuts')).toBeInTheDocument(); + }); + }); + + it('calls onUndoClick when undo menu item is clicked', async () => { + const onUndoClick = jest.fn(); + renderPanel({ showSecondaryMenuButton: true, onUndoClick }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + const undoItem = await screen.findByLabelText('Undo changes'); + + await act(async () => { + await userEvent.click(undoItem); + }); + + expect(onUndoClick).toHaveBeenCalledTimes(1); + }); + + it('calls onRedoClick when redo menu item is clicked', async () => { + const onRedoClick = jest.fn(); + renderPanel({ showSecondaryMenuButton: true, onRedoClick }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + const redoItem = await screen.findByLabelText('Redo changes'); + + await act(async () => { + await userEvent.click(redoItem); + }); + + expect(onRedoClick).toHaveBeenCalledTimes(1); + }); + + it('calls onDownloadClick when download menu item is clicked', async () => { + const onDownloadClick = jest.fn(); + renderPanel({ showSecondaryMenuButton: true, onDownloadClick }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + const downloadItem = await screen.findByLabelText('Download code'); + + await act(async () => { + await userEvent.click(downloadItem); + }); + + expect(onDownloadClick).toHaveBeenCalledTimes(1); + }); + + it('calls onViewShortcutsClick when view shortcuts menu item is clicked', async () => { + const onViewShortcutsClick = jest.fn(); + renderPanel({ showSecondaryMenuButton: true, onViewShortcutsClick }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + const shortcutsItem = await screen.findByLabelText('View shortcuts'); + + await act(async () => { + await userEvent.click(shortcutsItem); + }); + + expect(onViewShortcutsClick).toHaveBeenCalledTimes(1); + }); + }); + + describe('Custom Secondary Buttons', () => { + const customButtons = [ + { + label: 'Custom Action 1', + glyph: , + onClick: jest.fn(), + 'aria-label': 'Perform custom action 1', + }, + { + label: 'Custom Action 2', + glyph: , + onClick: jest.fn(), + href: 'https://example.com', + }, + ]; + + it('renders custom secondary buttons in the menu', async () => { + renderPanel({ + showSecondaryMenuButton: true, + customSecondaryButtons: customButtons, + }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + await waitFor(() => { + expect(screen.getByText('Custom Action 1')).toBeInTheDocument(); + expect(screen.getByText('Custom Action 2')).toBeInTheDocument(); + }); + }); + + it('calls custom button onClick handler', async () => { + const customButtonsWithMock = [ + { + ...customButtons[0], + onClick: jest.fn(), + }, + ]; + + renderPanel({ + showSecondaryMenuButton: true, + customSecondaryButtons: customButtonsWithMock, + }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + const customButton = await screen.findByLabelText( + 'Perform custom action 1', + ); + + await act(async () => { + await userEvent.click(customButton); + }); + + expect(customButtonsWithMock[0].onClick).toHaveBeenCalledTimes(1); + }); + + it('uses label as aria-label when aria-label is not provided', async () => { + const buttonWithoutAriaLabel = [ + { + label: 'Button Without Aria Label', + glyph: , + onClick: jest.fn(), + }, + ]; + + renderPanel({ + showSecondaryMenuButton: true, + customSecondaryButtons: buttonWithoutAriaLabel, + }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + await waitFor(() => { + expect( + screen.getByLabelText('Button Without Aria Label'), + ).toBeInTheDocument(); + }); + }); + }); + + describe('Dark Mode', () => { + it('passes darkMode prop to child components', () => { + renderPanel({ + showFormatButton: true, + showSecondaryMenuButton: true, + darkMode: true, + }); + + // This is more of an integration test - we're verifying the prop is passed down + // The actual dark mode styling would be tested in the styles tests + expect(screen.getByLabelText('Format code')).toBeInTheDocument(); + expect(screen.getByLabelText('Show more actions')).toBeInTheDocument(); + }); + }); + + describe('Font Size', () => { + it('accepts custom baseFontSize prop', () => { + renderPanel({ baseFontSize: 16 }); + // The font size would affect styling, which is tested in styles tests + expect(screen.getByText('Test Panel')).toBeInTheDocument(); + }); + }); + + describe('Integration with CodeEditor Context', () => { + it('uses getContents from context for copy button', () => { + renderPanel({ showCopyButton: true }); + + // The copy button should receive the getContents function from context + expect(screen.getByLabelText('Copy text')).toBeInTheDocument(); + // The actual integration works with real components now + }); + }); + + describe('Accessibility', () => { + it('has proper aria-labels for all buttons', () => { + renderPanel({ + showFormatButton: true, + showCopyButton: true, + showSecondaryMenuButton: true, + }); + + expect(screen.getByLabelText('Format code')).toBeInTheDocument(); + expect(screen.getByLabelText('Show more actions')).toBeInTheDocument(); + }); + + it('has proper aria-labels for menu items', async () => { + renderPanel({ showSecondaryMenuButton: true }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + await waitFor(() => { + expect(screen.getByLabelText('Undo changes')).toBeInTheDocument(); + expect(screen.getByLabelText('Redo changes')).toBeInTheDocument(); + expect(screen.getByLabelText('Download code')).toBeInTheDocument(); + expect(screen.getByLabelText('View shortcuts')).toBeInTheDocument(); + }); + }); + }); + + describe('Edge Cases', () => { + it('handles empty customSecondaryButtons array', async () => { + renderPanel({ + showSecondaryMenuButton: true, + customSecondaryButtons: [], + }); + + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + await waitFor(() => { + // Should only show default menu items + expect(screen.getByText('Undo')).toBeInTheDocument(); + expect(screen.getByText('Redo')).toBeInTheDocument(); + expect(screen.getByText('Download')).toBeInTheDocument(); + expect(screen.getByText('View shortcuts')).toBeInTheDocument(); + }); + }); + + it('handles undefined callback functions gracefully', async () => { + renderPanel({ + showFormatButton: true, + showSecondaryMenuButton: true, + // All callbacks are undefined + }); + + // Format button should render and be clickable without errors + const formatButton = screen.getByLabelText('Format code'); + + await act(async () => { + await userEvent.click(formatButton); + }); + + // Menu should open and items should be clickable without errors + const menuButton = screen.getByLabelText('Show more actions'); + + await act(async () => { + await userEvent.click(menuButton); + }); + + const undoItem = await screen.findByLabelText('Undo changes'); + + await act(async () => { + await userEvent.click(undoItem); + }); + + // No errors should be thrown + }); + }); +}); From 2d03e1c16006a4029d809654f3a7b93b54002e68 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Fri, 15 Aug 2025 15:56:00 -0400 Subject: [PATCH 11/21] Updated changeset --- .changeset/plenty-cities-buy.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.changeset/plenty-cities-buy.md b/.changeset/plenty-cities-buy.md index c5fc84cff3..79368c75bd 100644 --- a/.changeset/plenty-cities-buy.md +++ b/.changeset/plenty-cities-buy.md @@ -2,4 +2,13 @@ '@leafygreen-ui/code-editor': minor --- -Adds a **Panel component**: A toolbar interface for CodeEditor with formatting, copying, and custom action buttons +Adds **Panel component**: A comprehensive toolbar interface for CodeEditor with formatting, copying, and custom action buttons + +- **New Panel component**: Provides a configurable toolbar that displays at the top of CodeEditor +- **Built-in actions**: Includes format button, copy button, and secondary menu with undo/redo/download/shortcuts +- **Customizable**: Supports custom secondary buttons and inner content for application-specific needs +- **Fully accessible**: All buttons and menu items include proper ARIA labels and keyboard navigation +- **Context integration**: Seamlessly integrates with CodeEditor context for copy functionality +- **Styling**: Matches CodeEditor theme with proper grid layout and responsive design + +The Panel component enhances the CodeEditor user experience by providing easy access to common editor actions through a clean, organized interface. From 288c313e7f99f6b12c9f6c74e0191d104a8a650d Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 18 Aug 2025 19:54:41 -0400 Subject: [PATCH 12/21] feat(CodeEditorCopyButton): enhance icon fill color handling and simplify styles - Introduced a new `getIconFill` function to dynamically determine the icon fill color based on the theme and copied state. - Simplified the CSS styles for the copy button by removing redundant selectors and consolidating transition properties. - Updated the `CopyButtonTrigger` component to accept an `iconFill` prop, allowing for more flexible icon color management. --- .../CodeEditorCopyButton.styles.ts | 30 ++++--------------- .../CodeEditorCopyButton.tsx | 14 +++++++++ .../CodeEditorCopyButtonTrigger.tsx | 12 ++++++-- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/packages/code-editor/src/CodeEditorCopyButton/CodeEditorCopyButton.styles.ts b/packages/code-editor/src/CodeEditorCopyButton/CodeEditorCopyButton.styles.ts index b3faed98ec..5e0db7acc0 100644 --- a/packages/code-editor/src/CodeEditorCopyButton/CodeEditorCopyButton.styles.ts +++ b/packages/code-editor/src/CodeEditorCopyButton/CodeEditorCopyButton.styles.ts @@ -42,10 +42,7 @@ export const getCopyButtonStyles = ({ height: 26px; } - &, - & > div > svg { - transition: all ${transitionDuration.default}ms ease-in-out; - } + transition: all ${transitionDuration.default}ms ease-in-out; `, { [copiedThemeStyle[theme]]: copied, @@ -60,41 +57,26 @@ export const getCopyButtonStyles = ({ */ export const copiedThemeStyle: Record = { [Theme.Light]: css` - &, - & > div > svg { - color: ${palette.white}; - - &:focus, - &:hover { - color: ${palette.white}; - } - } - + color: ${palette.white}; background-color: ${palette.green.dark1}; &:focus, &:hover { + color: ${palette.white}; background-color: ${palette.green.dark1}; + &::before { background-color: ${palette.green.dark1}; } } `, [Theme.Dark]: css` - &, - & > div > svg { - color: ${palette.gray.dark3}; - - &:focus, - &:hover { - color: ${palette.gray.dark3}; - } - } - + color: ${palette.gray.dark3}; background-color: ${palette.green.base}; &:focus, &:hover { + color: ${palette.gray.dark3}; background-color: ${palette.green.base}; &::before { diff --git a/packages/code-editor/src/CodeEditorCopyButton/CodeEditorCopyButton.tsx b/packages/code-editor/src/CodeEditorCopyButton/CodeEditorCopyButton.tsx index de66bbf0db..7e812d23d5 100644 --- a/packages/code-editor/src/CodeEditorCopyButton/CodeEditorCopyButton.tsx +++ b/packages/code-editor/src/CodeEditorCopyButton/CodeEditorCopyButton.tsx @@ -3,6 +3,8 @@ import React, { useRef, useState } from 'react'; import { useBackdropClick } from '@leafygreen-ui/hooks'; import { useDarkMode } from '@leafygreen-ui/leafygreen-provider'; import { keyMap } from '@leafygreen-ui/lib'; +import { palette } from '@leafygreen-ui/palette'; +import { color } from '@leafygreen-ui/tokens'; import Tooltip, { Align, hoverDelay, @@ -55,6 +57,17 @@ export function CodeEditorCopyButton({ const timeoutRef = useRef(null); const { theme } = useDarkMode(); + /** + * Gets the appropriate icon fill color based on theme and copied state + */ + const getIconFill = () => { + if (copied) { + return theme === 'light' ? palette.white : palette.gray.dark3; + } + + return disabled ? undefined : color[theme].icon.primary.default; + }; + /** * toggles `tooltipOpen` state */ @@ -168,6 +181,7 @@ export function CodeEditorCopyButton({ diff --git a/packages/code-editor/src/CodeEditorCopyButtonTrigger/CodeEditorCopyButtonTrigger.tsx b/packages/code-editor/src/CodeEditorCopyButtonTrigger/CodeEditorCopyButtonTrigger.tsx index b91b041006..2011a820ac 100644 --- a/packages/code-editor/src/CodeEditorCopyButtonTrigger/CodeEditorCopyButtonTrigger.tsx +++ b/packages/code-editor/src/CodeEditorCopyButtonTrigger/CodeEditorCopyButtonTrigger.tsx @@ -12,6 +12,7 @@ import { COPIED_TEXT } from '../CodeEditorCopyButton/constants'; export interface CopyButtonTriggerProps { variant: CopyButtonVariant; copied: boolean; + iconFill?: string; // All the props that will be cloned by Tooltip children?: React.ReactNode; className?: string; @@ -24,8 +25,15 @@ export interface CopyButtonTriggerProps { export const CopyButtonTrigger = React.forwardRef< HTMLButtonElement, CopyButtonTriggerProps ->(function CopyButtonTrigger({ variant, copied, children, ...props }, ref) { - const icon = copied ? : ; +>(function CopyButtonTrigger( + { variant, copied, iconFill, children, ...props }, + ref, +) { + const icon = copied ? ( + + ) : ( + + ); const copiedAlert = copied && ( {COPIED_TEXT} ); From 60ab5b42a9ee09e4aa0179494d4445eb724b7ac1 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 18 Aug 2025 19:56:02 -0400 Subject: [PATCH 13/21] refactor(CodeEditor): rename getContents prop for clarity - Updated the `CodeEditor` component to rename the `getContents` prop to `getContentsToCopy`, improving clarity regarding its functionality. - Ensured consistency with the recent changes in the `CodeEditorCopyButton` component. --- packages/code-editor/src/CodeEditor/CodeEditor.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 11982f209f..7dd5d0d839 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -174,6 +174,16 @@ export const CodeEditor = forwardRef( {panel && ( {panel} )} + {(copyButtonAppearance === CopyButtonAppearance.Hover || + copyButtonAppearance === CopyButtonAppearance.Persist) && ( + + )} {!panel && (copyButtonAppearance === CopyButtonAppearance.Hover || copyButtonAppearance === CopyButtonAppearance.Persist) && ( From 3bfad7cea7fb621fa3b7c4310f3451775568e3e5 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 14 Aug 2025 16:27:26 -0400 Subject: [PATCH 14/21] feat(CodeEditor): enhance copy button functionality and styling - Introduced `copyButtonAppearance` prop to control the visibility of the copy button in the CodeEditor. - Implemented styles for different copy button appearances: 'hover', 'persist', and 'none'. - Updated tests to verify the rendering behavior of the copy button based on the new prop. - Refactored CodeEditor component to integrate the new copy button functionality and ensure proper rendering based on user interactions. --- .../code-editor/src/CodeEditor/CodeEditor.tsx | 10 ---------- .../src/CodeEditor/CodeEditor.types.ts | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.tsx b/packages/code-editor/src/CodeEditor/CodeEditor.tsx index 7dd5d0d839..11982f209f 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.tsx +++ b/packages/code-editor/src/CodeEditor/CodeEditor.tsx @@ -174,16 +174,6 @@ export const CodeEditor = forwardRef( {panel && ( {panel} )} - {(copyButtonAppearance === CopyButtonAppearance.Hover || - copyButtonAppearance === CopyButtonAppearance.Persist) && ( - - )} {!panel && (copyButtonAppearance === CopyButtonAppearance.Hover || copyButtonAppearance === CopyButtonAppearance.Persist) && ( diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts index b2026b13bc..a0d5cb856a 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts +++ b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts @@ -5,6 +5,7 @@ import { type EditorView } from '@codemirror/view'; import { type DarkModeProps } from '@leafygreen-ui/lib'; import { type LanguageName } from './hooks/extensions/useLanguageExtension'; +import { copyButtonClassName } from './CodeEditor.styles'; /** * Re-export of CodeMirror's {@link Extension} type. @@ -130,6 +131,21 @@ export type CodeEditorProps = DarkModeProps & { */ className?: string; + /** + * Determines the appearance of the copy button without a panel. The copy button allows the code block to be copied to the user's clipboard by clicking the button. + * + * If `hover`, the copy button will only appear when the user hovers over the code block. On mobile devices, the copy button will always be visible. + * + * If `persist`, the copy button will always be visible. + * + * If `none`, the copy button will not be rendered. + * + * Note: 'panel' cannot be used with `copyButtonAppearance`. Either use `copyButtonAppearance` or `panel`, not both. + * + * @default `hover` + */ + copyButtonAppearance?: CopyButtonAppearance; + /** * Initial value to render in the editor. */ From d0978ea85a2cd0336f69d88588c63a9eb1f211ea Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Thu, 14 Aug 2025 16:39:37 -0400 Subject: [PATCH 15/21] refactor(CodeEditor): remove unused import from CodeEditor.types.ts - Eliminated the unused `copyButtonClassName` import to clean up the code and improve maintainability. --- packages/code-editor/src/CodeEditor/CodeEditor.types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts index a0d5cb856a..bfbf1c8a08 100644 --- a/packages/code-editor/src/CodeEditor/CodeEditor.types.ts +++ b/packages/code-editor/src/CodeEditor/CodeEditor.types.ts @@ -5,7 +5,6 @@ import { type EditorView } from '@codemirror/view'; import { type DarkModeProps } from '@leafygreen-ui/lib'; import { type LanguageName } from './hooks/extensions/useLanguageExtension'; -import { copyButtonClassName } from './CodeEditor.styles'; /** * Re-export of CodeMirror's {@link Extension} type. From 665a0f17c2529d1607a0e129aafc7f1e769303b4 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 18 Aug 2025 20:41:26 -0400 Subject: [PATCH 16/21] refactor(CodeEditor): rename getContents prop to getContentsToCopy - Updated the `getContents` prop in the `CodeEditorCopyButton` to `getContentsToCopy` for improved clarity regarding its purpose. - This change aligns with recent naming conventions and enhances the overall consistency of the CodeEditor component. --- packages/code-editor/src/Panel/Panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/code-editor/src/Panel/Panel.tsx b/packages/code-editor/src/Panel/Panel.tsx index 6a817ababe..d233b288df 100644 --- a/packages/code-editor/src/Panel/Panel.tsx +++ b/packages/code-editor/src/Panel/Panel.tsx @@ -117,7 +117,7 @@ export function Panel({ {showCopyButton && ( '')} + getContentsToCopy={getContents ?? (() => '')} onCopy={onCopyClick} /> )} From cb12287cf9c5b37e2a4dc97a6748cd266d2de523 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 18 Aug 2025 20:52:24 -0400 Subject: [PATCH 17/21] docs(CodeEditor): add SecondaryButtonConfig section to README - Introduced a new section in the README for `SecondaryButtonConfig`, detailing its properties including `aria-label`, `glyph`, `href`, `label`, and `onClick`. - This addition enhances documentation clarity and provides developers with comprehensive information on configuring secondary buttons in the CodeEditor component. --- packages/code-editor/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/code-editor/README.md b/packages/code-editor/README.md index 38f78fc078..82ca300c50 100644 --- a/packages/code-editor/README.md +++ b/packages/code-editor/README.md @@ -129,6 +129,16 @@ import CloudIcon from '@leafygreen-ui/icon'; | `showSecondaryMenuButton` _(optional)_ | Determines whether to show the secondary menu button (ellipsis icon) in the panel. When enabled, displays a menu with additional actions like undo, redo, download, and view shortcuts. | `boolean` | `undefined` | | `title` _(optional)_ | Title text to display in the panel header. Typically used to show the current language or content description. | `string` | `undefined` | +#### `SecondaryButtonConfig` + +| Name | Description | Type | Default | +| ------------------------- | ---------------------------------------------------------------------- | -------------------- | ----------- | +| `aria-label` _(optional)_ | Accessible label for the button to provide context for screen readers. | `string` | `undefined` | +| `glyph` _(optional)_ | Icon element to display in the button. | `React.ReactElement` | `undefined` | +| `href` _(optional)_ | URL to navigate to when the button is clicked. | `string` | `undefined` | +| `label` | Text label for the button. | `string` | — | +| `onClick` _(optional)_ | Callback fired when the button is clicked. | `() => void` | `undefined` | + ## Types and Variables | Name | Description | From a3b0f25fe4719a060fa3c2830e704975f4dffa9b Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Mon, 18 Aug 2025 22:06:28 -0400 Subject: [PATCH 18/21] fix(Panel): update copy button accessibility labels in tests - Changed the accessibility label for the copy button in tests from 'Copy text' to 'Copy' to align with the actual implementation. - Updated related test cases to ensure consistency and accuracy in verifying the copy button's presence and functionality. --- packages/code-editor/src/Panel/Panel.spec.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/code-editor/src/Panel/Panel.spec.tsx b/packages/code-editor/src/Panel/Panel.spec.tsx index abbad5fdf0..469b5f6a82 100644 --- a/packages/code-editor/src/Panel/Panel.spec.tsx +++ b/packages/code-editor/src/Panel/Panel.spec.tsx @@ -118,19 +118,19 @@ describe('Panel', () => { describe('Copy Button', () => { it('renders copy button when showCopyButton is true', () => { renderPanel({ showCopyButton: true }); - expect(screen.getByLabelText('Copy text')).toBeInTheDocument(); + expect(screen.getByLabelText('Copy')).toBeInTheDocument(); }); it('does not render copy button when showCopyButton is false', () => { renderPanel({ showCopyButton: false }); - expect(screen.queryByLabelText('Copy text')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Copy')).not.toBeInTheDocument(); }); it('calls onCopyClick when copy button is clicked', async () => { const onCopyClick = jest.fn(); renderPanel({ showCopyButton: true, onCopyClick }); - const copyButton = screen.getByLabelText('Copy text'); + const copyButton = screen.getByLabelText('Copy'); await act(async () => { await userEvent.click(copyButton); @@ -142,7 +142,7 @@ describe('Panel', () => { it('shows tooltip on copy button hover', async () => { renderPanel({ showCopyButton: true }); - const copyButton = screen.getByLabelText('Copy text'); + const copyButton = screen.getByLabelText('Copy'); await act(async () => { await userEvent.hover(copyButton); @@ -157,7 +157,7 @@ describe('Panel', () => { it('shows success state after copying', async () => { renderPanel({ showCopyButton: true }); - const copyButton = screen.getByLabelText('Copy text'); + const copyButton = screen.getByLabelText('Copy'); await act(async () => { await userEvent.click(copyButton); @@ -165,7 +165,7 @@ describe('Panel', () => { // Wait for success tooltip to appear await waitFor(async () => { - expect(screen.getByText('Copied!')).toBeInTheDocument(); + expect(screen.getByRole('tooltip')).toHaveTextContent('Copied!'); }); }); }); @@ -397,7 +397,7 @@ describe('Panel', () => { renderPanel({ showCopyButton: true }); // The copy button should receive the getContents function from context - expect(screen.getByLabelText('Copy text')).toBeInTheDocument(); + expect(screen.getByLabelText('Copy')).toBeInTheDocument(); // The actual integration works with real components now }); }); From 7da72c8dae96c15d7260dae82b3ee7a4c8eb869d Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 20 Aug 2025 15:48:39 -0400 Subject: [PATCH 19/21] refactor(Panel): update panel styles for consistency and clarity - Replaced hardcoded height with a constant for better maintainability. - Adjusted padding values to use spacing tokens for consistency with design system. - Renamed grid area from 'children' to 'innerContent' to improve clarity in layout structure. --- packages/code-editor/src/Panel/Panel.styles.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/code-editor/src/Panel/Panel.styles.ts b/packages/code-editor/src/Panel/Panel.styles.ts index 4f975c64c7..c18bc02a00 100644 --- a/packages/code-editor/src/Panel/Panel.styles.ts +++ b/packages/code-editor/src/Panel/Panel.styles.ts @@ -4,20 +4,23 @@ import { borderRadius, color, InteractionState, + spacing, Variant, } from '@leafygreen-ui/tokens'; +const PANEL_HEIGHT = 36; + export const getPanelStyles = (theme: Theme) => { return css` background-color: ${color[theme].background[Variant.Secondary][ InteractionState.Default ]}; - height: 36px; + height: ${PANEL_HEIGHT}px; width: 100%; display: flex; align-items: center; justify-content: space-between; - padding: 0 16px 0 10px; + padding: 0 ${spacing[400]}px 0 ${spacing[300]}px; border: 1px solid ${color[theme].border[Variant.Secondary][InteractionState.Default]}; border-bottom: none; @@ -25,7 +28,7 @@ export const getPanelStyles = (theme: Theme) => { border-top-right-radius: ${borderRadius[300]}px; display: grid; grid-template-columns: auto 1fr auto; - grid-template-areas: 'title children buttons'; + grid-template-areas: 'title innerContent buttons'; `; }; @@ -39,7 +42,7 @@ export const getPanelTitleStyles = (theme: Theme, baseFontSize: number) => { export const getPanelInnerContentStyles = () => { return css` - grid-area: children; + grid-area: innerContent; `; }; From d551e73d5adbad84196fd9f4cac48e7268601020 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 20 Aug 2025 15:48:47 -0400 Subject: [PATCH 20/21] feat(Panel): add support for custom secondary button properties - Introduced a new `disabled` property to the `SecondaryButtonConfig` interface, allowing buttons to be disabled. - Updated the `Panel` component to handle the `disabled` state for custom secondary buttons. - Enhanced the README documentation for `SecondaryButtonConfig` to include the new `disabled` property, improving clarity for developers. --- packages/code-editor/README.md | 1 + .../code-editor/src/CodeEditor.stories.tsx | 11 ++++++++++ packages/code-editor/src/Panel/Panel.tsx | 10 ++++++++- packages/code-editor/src/Panel/Panel.types.ts | 22 ++++++++++--------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/packages/code-editor/README.md b/packages/code-editor/README.md index 82ca300c50..3950285d46 100644 --- a/packages/code-editor/README.md +++ b/packages/code-editor/README.md @@ -134,6 +134,7 @@ import CloudIcon from '@leafygreen-ui/icon'; | Name | Description | Type | Default | | ------------------------- | ---------------------------------------------------------------------- | -------------------- | ----------- | | `aria-label` _(optional)_ | Accessible label for the button to provide context for screen readers. | `string` | `undefined` | +| `disabled` _(optional)_ | Whether the button is disabled. | `boolean` | `undefined` | | `glyph` _(optional)_ | Icon element to display in the button. | `React.ReactElement` | `undefined` | | `href` _(optional)_ | URL to navigate to when the button is clicked. | `string` | `undefined` | | `label` | Text label for the button. | `string` | — | diff --git a/packages/code-editor/src/CodeEditor.stories.tsx b/packages/code-editor/src/CodeEditor.stories.tsx index 0b57610b74..51c570a5ac 100644 --- a/packages/code-editor/src/CodeEditor.stories.tsx +++ b/packages/code-editor/src/CodeEditor.stories.tsx @@ -8,6 +8,8 @@ import type { StoryFn, StoryObj } from '@storybook/react'; import { expect, waitFor } from '@storybook/test'; import { css } from '@leafygreen-ui/emotion'; +// @ts-ignore LG icons don't currently support TS +import CloudIcon from '@leafygreen-ui/icon/dist/Cloud'; import { CopyButtonAppearance } from './CodeEditor/CodeEditor.types'; import { LanguageName } from './CodeEditor/hooks/extensions/useLanguageExtension'; @@ -190,6 +192,15 @@ WithPanel.args = { showFormatButton showSecondaryMenuButton title="index.tsx" + customSecondaryButtons={[ + { + label: 'Custom Button', + onClick: () => {}, + 'aria-label': 'Custom Button', + glyph: , + href: 'https://www.mongodb.com', + }, + ]} /> ), }; diff --git a/packages/code-editor/src/Panel/Panel.tsx b/packages/code-editor/src/Panel/Panel.tsx index d233b288df..0c42bfe557 100644 --- a/packages/code-editor/src/Panel/Panel.tsx +++ b/packages/code-editor/src/Panel/Panel.tsx @@ -161,13 +161,21 @@ export function Panel({ View shortcuts {customSecondaryButtons?.map( - ({ label, glyph, onClick, href, 'aria-label': ariaLabel }) => ( + ({ + label, + glyph, + onClick, + href, + 'aria-label': ariaLabel, + disabled, + }) => ( {label} diff --git a/packages/code-editor/src/Panel/Panel.types.ts b/packages/code-editor/src/Panel/Panel.types.ts index f4a8ea12ce..44bd657ee9 100644 --- a/packages/code-editor/src/Panel/Panel.types.ts +++ b/packages/code-editor/src/Panel/Panel.types.ts @@ -1,21 +1,24 @@ import { DarkModeProps } from '@leafygreen-ui/lib'; -import { type MenuItemProps } from '@leafygreen-ui/menu'; /** * Configuration object for custom secondary buttons that appear in the Panel's secondary menu. - * Extends MenuItemProps to inherit all MenuItem functionality while adding required label. */ -type SecondaryButtonConfig = MenuItemProps & { +export interface SecondaryButtonConfig { /** * Text label for the button that will be displayed in the menu item. */ label: string; /** - * Optional icon element to display alongside the button label. + * Optional icon element to display in the button. */ glyph?: React.ReactElement; + /** + * Optional callback fired when the button is clicked. + */ + onClick?: () => void; + /** * Optional URL to navigate to when the button is clicked. * If provided, the button will behave as a link. @@ -23,16 +26,15 @@ type SecondaryButtonConfig = MenuItemProps & { href?: string; /** - * Optional callback function that will be called when the button is clicked. + * Optional accessible label for the button. */ - onClick?: () => void; + 'aria-label'?: string; /** - * Optional accessible label for the button to provide additional context for screen readers. - * If not provided, the label will be used as the aria-label. + * Optional boolean to disable the button. */ - 'aria-label'?: string; -}; + disabled?: boolean; +} export interface PanelProps extends DarkModeProps { /** From bc925b72cceef5207a77ba863de2d5ec720b7155 Mon Sep 17 00:00:00 2001 From: Terrence Keane Date: Wed, 20 Aug 2025 15:55:16 -0400 Subject: [PATCH 21/21] fix(CodeEditor): remove href from custom secondary button configuration - Eliminated the `href` property from the custom secondary button configuration in the CodeEditor stories, streamlining the button setup. - This change enhances clarity and aligns with the updated button properties. --- packages/code-editor/src/CodeEditor.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/code-editor/src/CodeEditor.stories.tsx b/packages/code-editor/src/CodeEditor.stories.tsx index 51c570a5ac..85154f6026 100644 --- a/packages/code-editor/src/CodeEditor.stories.tsx +++ b/packages/code-editor/src/CodeEditor.stories.tsx @@ -198,7 +198,6 @@ WithPanel.args = { onClick: () => {}, 'aria-label': 'Custom Button', glyph: , - href: 'https://www.mongodb.com', }, ]} />