From cf60049a884925ca143bbd9a38bd252a03d30459 Mon Sep 17 00:00:00 2001 From: Kumar Nirupam Date: Thu, 27 Aug 2026 11:54:02 +0000 Subject: [PATCH 1/4] add notistack-based progress snackbar Signed-off-by: Kumar Nirupam --- package.json | 2 +- .../ProgressBar/ProgressBar.stories.tsx | 170 ++++++++++++++++++ src/custom/ProgressBar/ProgressBar.tsx | 124 +++++++++++++ src/custom/ProgressBar/index.tsx | 4 + src/custom/ProgressBar/style.tsx | 73 ++++++++ src/custom/ProgressBar/useProgressBar.ts | 115 ++++++++++++ src/custom/index.tsx | 2 + src/index.tsx | 9 + 8 files changed, 498 insertions(+), 1 deletion(-) create mode 100644 src/custom/ProgressBar/ProgressBar.stories.tsx create mode 100644 src/custom/ProgressBar/ProgressBar.tsx create mode 100644 src/custom/ProgressBar/index.tsx create mode 100644 src/custom/ProgressBar/style.tsx create mode 100644 src/custom/ProgressBar/useProgressBar.ts diff --git a/package.json b/package.json index bf3a72522..c33185451 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "jest": "^30.3.0", "jest-environment-jsdom": "^30.4.1", "lint-staged": "^17.0.3", - "notistack": "^3.0.2", "prettier": "^3.8.3", "prettier-plugin-organize-imports": "^4.3.0", "react": "^19.2.7", @@ -133,6 +132,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "notistack": "^3.0.2", "@layer5/meshery-design-embed": "^0.6.0", "@meshery/schemas": "^1.3.44", "@sistent/mui-datatables": "^8.0.0", diff --git a/src/custom/ProgressBar/ProgressBar.stories.tsx b/src/custom/ProgressBar/ProgressBar.stories.tsx new file mode 100644 index 000000000..142aa6ca4 --- /dev/null +++ b/src/custom/ProgressBar/ProgressBar.stories.tsx @@ -0,0 +1,170 @@ +import { SnackbarProvider } from 'notistack'; +import React, { useEffect, useState } from 'react'; +import { Button } from '../../base'; +import { SistentThemeProvider } from '../../theme'; +import { ProgressBar } from './ProgressBar'; +import { useProgressBar } from './useProgressBar'; + +const meta = { + title: 'Custom/ProgressBar', + component: ProgressBar +}; + +export default meta; + +type Story = { name?: string; render: () => React.ReactElement }; + +// Linear determinate with live updates +const LinearDeterminateDemo = (): React.ReactElement => { + const { show, update, close } = useProgressBar(); + const [activeKey, setActiveKey] = useState(null); + + const handleStart = () => { + const key = show({ message: 'Uploading files...', progress: 0, persist: true }); + setActiveKey(key); + let p = 0; + const id = setInterval(() => { + p += 10; + if (p > 100) { + clearInterval(id); + close(key); + setActiveKey(null); + return; + } + update(key, { progress: p, message: `Uploading files... ${p}%` }); + }, 400); + }; + + return ( +
+ + {activeKey !== null && ( + + )} +
+ ); +}; + +const LinearIndeterminateDemo = (): React.ReactElement => { + const { show, close } = useProgressBar(); + const [key, setKey] = useState(null); + return ( +
+ + {key !== null && ( + + )} +
+ ); +}; + +const CircularDemo = (): React.ReactElement => { + const { show, update, close } = useProgressBar(); + const handleClick = () => { + const k = show({ message: 'Syncing...', progress: 0, variant: 'circular', persist: true }); + let p = 0; + const id = setInterval(() => { + p += 15; + if (p > 100) { + clearInterval(id); + close(k); + return; + } + update(k, { progress: p }); + }, 350); + }; + return ( + + ); +}; + +// Direct render without notistack provider (visual QA only) +const StandaloneDemo = (): React.ReactElement => { + const [progress, setProgress] = useState(35); + useEffect(() => { + const id = setInterval(() => setProgress((prev) => (prev >= 100 ? 0 : prev + 5)), 600); + return () => clearInterval(id); + }, []); + return ( + +
+ {/* Standalone preview - ProgressBar normally renders via enqueueSnackbar; this shows the visual only */} +
+

+ Standalone preview (without snackbar positioning): +

+ +
+ +
+ +
+
+ + ); +}; + +const WithProvider = (children: React.ReactElement): React.ReactElement => ( + + + {children} + + +); + +export const LinearDeterminate: Story = { + name: 'Linear - determinate (updatable)', + render: () => WithProvider() +}; + +export const LinearIndeterminate: Story = { + name: 'Linear - indeterminate (persistent)', + render: () => WithProvider() +}; + +export const Circular: Story = { + name: 'Circular - determinate (updatable)', + render: () => WithProvider() +}; + +export const Standalone: Story = { + name: 'Standalone preview', + render: () => ( + + + + ) +}; diff --git a/src/custom/ProgressBar/ProgressBar.tsx b/src/custom/ProgressBar/ProgressBar.tsx new file mode 100644 index 000000000..894e38d41 --- /dev/null +++ b/src/custom/ProgressBar/ProgressBar.tsx @@ -0,0 +1,124 @@ +import { SnackbarContent, useSnackbar, type CustomContentProps } from 'notistack'; +import React from 'react'; +import { CircularProgress } from '../../base/CircularProgress'; +import { LinearProgress } from '../../base/LinearProgress'; +import { CloseIcon } from '../../icons'; +import { + CloseButtonWrapper, + ProgressBarContent, + ProgressBarHeader, + ProgressBarLabel, + ProgressBarMessage, + ProgressBarTrack, + ProgressBarWrapper +} from './style'; + +export type ProgressBarVariant = 'linear' | 'circular'; + +export interface ProgressBarProps extends Omit, 'variant'> { + id: CustomContentProps['id']; + /** + * Progress value 0-100. When undefined the bar renders indeterminate. + * @default undefined (indeterminate) + */ + progress?: number; + /** + * Message / title shown alongside the progress indicator. + */ + message?: React.ReactNode; + /** + * Which base primitive to use for progress rendering. + * @default 'linear' + */ + variant?: ProgressBarVariant; + /** + * Show numeric percentage label next to the message (linear variant only). + * @default true when progress is determinate + */ + showProgressLabel?: boolean; + /** + * Allow dismiss via close button. When false the close button is hidden. + * @default true + */ + dismissible?: boolean; + /** + * Additional sx for the outer wrapper. + */ + sx?: React.ComponentProps['sx']; +} + +const clampProgress = (value: number): number => Math.min(100, Math.max(0, value)); + +export const ProgressBar = React.forwardRef( + ( + { + id, + progress, + message, + variant = 'linear', + showProgressLabel, + dismissible = true, + sx, + style, + ...props + }, + ref + ) => { + const { closeSnackbar } = useSnackbar(); + + const isDeterminate = typeof progress === 'number' && Number.isFinite(progress); + const normalizedProgress = isDeterminate ? clampProgress(progress as number) : undefined; + const shouldShowLabel = showProgressLabel ?? isDeterminate; + + const handleClose = React.useCallback(() => { + closeSnackbar(id); + }, [closeSnackbar, id]); + + const renderProgress = () => { + if (variant === 'circular') { + if (isDeterminate) { + return ; + } + return ; + } + + if (isDeterminate) { + return ; + } + return ; + }; + + return ( + + + {variant === 'circular' && renderProgress()} + + {(message || (shouldShowLabel && isDeterminate)) && ( + + {message && {message}} + {shouldShowLabel && isDeterminate && ( + {`${Math.round(normalizedProgress as number)}%`} + )} + + )} + {variant === 'linear' && {renderProgress()}} + + {dismissible && ( + + + + )} + + + ); + } +); + +ProgressBar.displayName = 'ProgressBar'; + +export default ProgressBar; diff --git a/src/custom/ProgressBar/index.tsx b/src/custom/ProgressBar/index.tsx new file mode 100644 index 000000000..1e5550b0d --- /dev/null +++ b/src/custom/ProgressBar/index.tsx @@ -0,0 +1,4 @@ +export { ProgressBar, default } from './ProgressBar'; +export type { ProgressBarProps, ProgressBarVariant } from './ProgressBar'; +export { useProgressBar, default as useProgressBarDefault } from './useProgressBar'; +export type { ShowProgressBarOptions, UseProgressBarReturn } from './useProgressBar'; diff --git a/src/custom/ProgressBar/style.tsx b/src/custom/ProgressBar/style.tsx new file mode 100644 index 000000000..44828f9a7 --- /dev/null +++ b/src/custom/ProgressBar/style.tsx @@ -0,0 +1,73 @@ +import { styled } from '@mui/material'; + +export const ProgressBarWrapper = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1.5), + padding: theme.spacing(1.5, 2), + minWidth: 300, + maxWidth: 480, + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + borderRadius: Number(theme.shape.borderRadius) * 1.5, + boxShadow: theme.shadows[6], + border: `1px solid ${theme.palette.divider}` +})); + +export const ProgressBarContent = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + flex: 1, + gap: theme.spacing(1), + minWidth: 0 +})); + +export const ProgressBarHeader = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + gap: theme.spacing(1) +})); + +export const ProgressBarMessage = styled('span')(({ theme }) => ({ + fontSize: theme.typography.body2.fontSize, + fontWeight: 500, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + flex: 1 +})); + +export const ProgressBarLabel = styled('span')(({ theme }) => ({ + fontSize: theme.typography.caption.fontSize, + color: theme.palette.text.secondary, + fontWeight: 500, + flexShrink: 0 +})); + +export const ProgressBarTrack = styled('div')({ + width: '100%' +}); + +export const CloseButtonWrapper = styled('button')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + width: 28, + height: 28, + padding: 0, + border: 'none', + borderRadius: '50%', + backgroundColor: 'transparent', + color: theme.palette.text.secondary, + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.action.hover, + color: theme.palette.text.primary + }, + '&:focus-visible': { + outline: `2px solid ${theme.palette.primary.main}`, + outlineOffset: 1 + } +})); diff --git a/src/custom/ProgressBar/useProgressBar.ts b/src/custom/ProgressBar/useProgressBar.ts new file mode 100644 index 000000000..d751b553b --- /dev/null +++ b/src/custom/ProgressBar/useProgressBar.ts @@ -0,0 +1,115 @@ +import { useSnackbar, type OptionsObject, type SnackbarKey } from 'notistack'; +import React from 'react'; +import { ProgressBar, type ProgressBarProps } from './ProgressBar'; + +export interface ShowProgressBarOptions extends Omit { + /** + * Initial progress 0-100. Omit for indeterminate. + */ + progress?: number; + message?: React.ReactNode; + /** + * Snackbar key. Auto-generated when not provided. + */ + key?: SnackbarKey; + variant?: ProgressBarProps['variant']; + showProgressLabel?: boolean; + dismissible?: boolean; + sx?: ProgressBarProps['sx']; +} + +export interface UseProgressBarReturn { + /** + * Show a persistent progress snackbar. Returns its key. + */ + show: (options: ShowProgressBarOptions) => SnackbarKey; + /** + * Update progress/message of an open snackbar by key. + */ + update: ( + key: SnackbarKey, + options: Partial> + ) => void; + /** + * Close snackbar by key. + */ + close: (key?: SnackbarKey) => void; +} + +/** + * Imperative helper for the ProgressBar snackbar pattern. + * Wraps notistack's enqueueSnackbar/closeSnackbar so progress can be + * updated while the toast is visible without the caller managing keys manually. + * + * @example + * const { show, update, close } = useProgressBar(); + * const key = show({ message: 'Uploading...', progress: 0, persist: true }); + * update(key, { progress: 42 }); + * close(key); + */ +export const useProgressBar = (): UseProgressBarReturn => { + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + + const show = React.useCallback( + ({ + progress, + message, + key, + persist = true, + variant, + showProgressLabel, + dismissible, + sx, + ...rest + }: ShowProgressBarOptions): SnackbarKey => { + const content = (id: SnackbarKey) => + React.createElement(ProgressBar, { + id, + progress, + message, + variant, + showProgressLabel, + dismissible, + sx + }); + + return enqueueSnackbar((message as string) ?? '', { + key, + persist, + content: content as unknown as OptionsObject['content'], + ...rest + }); + }, + [enqueueSnackbar] + ); + + const update = React.useCallback( + (key: SnackbarKey, options: Partial>) => { + const { progress, message } = options; + const content = (id: SnackbarKey) => + React.createElement(ProgressBar, { + id, + progress, + message + }); + + enqueueSnackbar((message as string) ?? '', { + key, + persist: true, + content: content as unknown as OptionsObject['content'] + }); + }, + [enqueueSnackbar] + ); + + const close = React.useCallback( + (key?: SnackbarKey) => { + closeSnackbar(key); + }, + [closeSnackbar] + ); + + return { show, update, close }; +}; + +export default useProgressBar; diff --git a/src/custom/index.tsx b/src/custom/index.tsx index bff31634c..6bd1c9a3f 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -74,6 +74,8 @@ export { NavigationNavbar } from './NavigationNavbar'; export type { NavigationItem } from './NavigationNavbar'; export { Note } from './Note'; export { Panel } from './Panel'; +export { ProgressBar, useProgressBar } from './ProgressBar'; +export type { ProgressBarProps, ProgressBarVariant, ShowProgressBarOptions, UseProgressBarReturn } from './ProgressBar'; export { OpenLeaderBoardButton, PerformersSection, diff --git a/src/index.tsx b/src/index.tsx index 33a25a51f..0db04cbdb 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -116,6 +116,15 @@ export { export { BottomSheet, type BottomSheetProps } from './custom/BottomSheet'; +export { + ProgressBar, + useProgressBar, + type ProgressBarProps, + type ProgressBarVariant, + type ShowProgressBarOptions, + type UseProgressBarReturn +} from './custom/ProgressBar'; + export { ActionButton, type ActionButtonProps, type Option } from './custom/ActionButton'; // Same nested-barrel dts-drop quirk as FeedbackButton above. The share/revoke From c1916d0442d386b50b3116a7dc60ac301eb12c3f Mon Sep 17 00:00:00 2001 From: Kumar Nirupam Date: Thu, 27 Aug 2026 12:20:15 +0000 Subject: [PATCH 2/4] fix(ProgressBar): preserve config on update, fix message typing and docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit update() now stores the original ShowProgressBarOptions per SnackbarKey and merges on each update, preserving variant, dismissible, sx, showProgressLabel and all notistack OptionsObject fields (persist, anchorOrigin, autoHideDuration, etc.) instead of recreating with only progress/message and hardcoding persist:true. This fixes the reported regression where a circular snackbar became linear after the first update. show()/update() now pass SnackbarMessage directly instead of casting ReactNode to string. Document non-finite progress as indeterminate (clamped 0-100) and clarify that completion does not auto-close — caller must close programmatically. Fix Standalone story comment that incorrectly claimed no SnackbarProvider was needed. Signed-off-by: Kumar Nirupam --- .../ProgressBar/ProgressBar.stories.tsx | 2 +- src/custom/ProgressBar/ProgressBar.tsx | 3 +- src/custom/ProgressBar/useProgressBar.ts | 70 +++++++++++++++---- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/src/custom/ProgressBar/ProgressBar.stories.tsx b/src/custom/ProgressBar/ProgressBar.stories.tsx index 142aa6ca4..6d131fe04 100644 --- a/src/custom/ProgressBar/ProgressBar.stories.tsx +++ b/src/custom/ProgressBar/ProgressBar.stories.tsx @@ -101,7 +101,7 @@ const CircularDemo = (): React.ReactElement => { ); }; -// Direct render without notistack provider (visual QA only) +// Direct render of ProgressBar without enqueueSnackbar (visual QA only) — still requires SnackbarProvider because ProgressBar uses useSnackbar() const StandaloneDemo = (): React.ReactElement => { const [progress, setProgress] = useState(35); useEffect(() => { diff --git a/src/custom/ProgressBar/ProgressBar.tsx b/src/custom/ProgressBar/ProgressBar.tsx index 894e38d41..8d2b9f726 100644 --- a/src/custom/ProgressBar/ProgressBar.tsx +++ b/src/custom/ProgressBar/ProgressBar.tsx @@ -18,7 +18,8 @@ export type ProgressBarVariant = 'linear' | 'circular'; export interface ProgressBarProps extends Omit, 'variant'> { id: CustomContentProps['id']; /** - * Progress value 0-100. When undefined the bar renders indeterminate. + * Progress value 0-100. When undefined or non-finite (NaN/Infinity) the bar renders indeterminate. + * Values outside 0-100 are clamped. * @default undefined (indeterminate) */ progress?: number; diff --git a/src/custom/ProgressBar/useProgressBar.ts b/src/custom/ProgressBar/useProgressBar.ts index d751b553b..0c4d48728 100644 --- a/src/custom/ProgressBar/useProgressBar.ts +++ b/src/custom/ProgressBar/useProgressBar.ts @@ -1,10 +1,10 @@ -import { useSnackbar, type OptionsObject, type SnackbarKey } from 'notistack'; +import { useSnackbar, type OptionsObject, type SnackbarKey, type SnackbarMessage } from 'notistack'; import React from 'react'; import { ProgressBar, type ProgressBarProps } from './ProgressBar'; export interface ShowProgressBarOptions extends Omit { /** - * Initial progress 0-100. Omit for indeterminate. + * Initial progress 0-100. Omit or pass a non-finite value (NaN/Infinity) for indeterminate. */ progress?: number; message?: React.ReactNode; @@ -21,14 +21,19 @@ export interface ShowProgressBarOptions extends Omit SnackbarKey; /** - * Update progress/message of an open snackbar by key. + * Update progress/message of an open snackbar by key. Preserves the original + * ProgressBar configuration (variant, dismissible, sx, showProgressLabel) and + * snackbar options (persist, anchorOrigin, autoHideDuration, etc.) from the + * initial `show` call. */ update: ( key: SnackbarKey, - options: Partial> + options: Partial> & + Partial ) => void; /** * Close snackbar by key. @@ -40,15 +45,18 @@ export interface UseProgressBarReturn { * Imperative helper for the ProgressBar snackbar pattern. * Wraps notistack's enqueueSnackbar/closeSnackbar so progress can be * updated while the toast is visible without the caller managing keys manually. + * `update` merges with the original `show` options so variant/sx/persist etc. + * are not lost. * * @example * const { show, update, close } = useProgressBar(); - * const key = show({ message: 'Uploading...', progress: 0, persist: true }); + * const key = show({ message: 'Uploading...', progress: 0, variant: 'circular', persist: true }); * update(key, { progress: 42 }); * close(key); */ export const useProgressBar = (): UseProgressBarReturn => { const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const storeRef = React.useRef>(new Map()); const show = React.useCallback( ({ @@ -62,6 +70,18 @@ export const useProgressBar = (): UseProgressBarReturn => { sx, ...rest }: ShowProgressBarOptions): SnackbarKey => { + const storedOptions: ShowProgressBarOptions = { + progress, + message, + key, + persist, + variant, + showProgressLabel, + dismissible, + sx, + ...rest + }; + const content = (id: SnackbarKey) => React.createElement(ProgressBar, { id, @@ -73,37 +93,63 @@ export const useProgressBar = (): UseProgressBarReturn => { sx }); - return enqueueSnackbar((message as string) ?? '', { + const returnedKey = enqueueSnackbar((message as SnackbarMessage) ?? '', { key, persist, content: content as unknown as OptionsObject['content'], ...rest }); + + const storeKey = key ?? returnedKey; + storeRef.current.set(storeKey, storedOptions); + + return returnedKey; }, [enqueueSnackbar] ); const update = React.useCallback( - (key: SnackbarKey, options: Partial>) => { - const { progress, message } = options; + ( + key: SnackbarKey, + options: Partial> & + Partial + ) => { + const stored = storeRef.current.get(key) ?? {}; + const merged: ShowProgressBarOptions = { + ...stored, + ...options, + key + }; + + const { progress, message, variant, showProgressLabel, dismissible, sx, persist, ...rest } = + merged; + const content = (id: SnackbarKey) => React.createElement(ProgressBar, { id, progress, - message + message, + variant, + showProgressLabel, + dismissible, + sx }); - enqueueSnackbar((message as string) ?? '', { + enqueueSnackbar((message as SnackbarMessage) ?? '', { key, - persist: true, - content: content as unknown as OptionsObject['content'] + persist, + content: content as unknown as OptionsObject['content'], + ...rest }); + + storeRef.current.set(key, merged); }, [enqueueSnackbar] ); const close = React.useCallback( (key?: SnackbarKey) => { + if (key !== undefined) storeRef.current.delete(key); closeSnackbar(key); }, [closeSnackbar] From 0a7243664e0cfebc6e7612aebf3d4a85cce8ab31 Mon Sep 17 00:00:00 2001 From: Kumar Nirupam Date: Thu, 27 Aug 2026 12:28:05 +0000 Subject: [PATCH 3/4] chore(ProgressBar): remove unnecessary comments Signed-off-by: Kumar Nirupam --- src/custom/ProgressBar/ProgressBar.stories.tsx | 3 --- src/custom/ProgressBar/useProgressBar.ts | 13 ------------- 2 files changed, 16 deletions(-) diff --git a/src/custom/ProgressBar/ProgressBar.stories.tsx b/src/custom/ProgressBar/ProgressBar.stories.tsx index 6d131fe04..c9ad5f42b 100644 --- a/src/custom/ProgressBar/ProgressBar.stories.tsx +++ b/src/custom/ProgressBar/ProgressBar.stories.tsx @@ -14,7 +14,6 @@ export default meta; type Story = { name?: string; render: () => React.ReactElement }; -// Linear determinate with live updates const LinearDeterminateDemo = (): React.ReactElement => { const { show, update, close } = useProgressBar(); const [activeKey, setActiveKey] = useState(null); @@ -101,7 +100,6 @@ const CircularDemo = (): React.ReactElement => { ); }; -// Direct render of ProgressBar without enqueueSnackbar (visual QA only) — still requires SnackbarProvider because ProgressBar uses useSnackbar() const StandaloneDemo = (): React.ReactElement => { const [progress, setProgress] = useState(35); useEffect(() => { @@ -111,7 +109,6 @@ const StandaloneDemo = (): React.ReactElement => { return (
- {/* Standalone preview - ProgressBar normally renders via enqueueSnackbar; this shows the visual only */}

Standalone preview (without snackbar positioning): diff --git a/src/custom/ProgressBar/useProgressBar.ts b/src/custom/ProgressBar/useProgressBar.ts index 0c4d48728..467e848a5 100644 --- a/src/custom/ProgressBar/useProgressBar.ts +++ b/src/custom/ProgressBar/useProgressBar.ts @@ -41,19 +41,6 @@ export interface UseProgressBarReturn { close: (key?: SnackbarKey) => void; } -/** - * Imperative helper for the ProgressBar snackbar pattern. - * Wraps notistack's enqueueSnackbar/closeSnackbar so progress can be - * updated while the toast is visible without the caller managing keys manually. - * `update` merges with the original `show` options so variant/sx/persist etc. - * are not lost. - * - * @example - * const { show, update, close } = useProgressBar(); - * const key = show({ message: 'Uploading...', progress: 0, variant: 'circular', persist: true }); - * update(key, { progress: 42 }); - * close(key); - */ export const useProgressBar = (): UseProgressBarReturn => { const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const storeRef = React.useRef>(new Map()); From 81a5887a8573c146c3f04e871e691712e110519e Mon Sep 17 00:00:00 2001 From: Kumar Nirupam Date: Thu, 27 Aug 2026 13:00:04 +0000 Subject: [PATCH 4/4] fix(ProgressBar): address coderabbit review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Limit progress label to linear variant only (variant==='linear' gate) so circular determinate no longer shows percentage label despite docs. - Omit notistack action from ShowProgressBarOptions since custom ProgressBar does not render an action slot, making the API explicit. Deferred: style.tsx semantic tokens and story preview colors are theming consistency follow-ups that would touch the broader palette (matching existing MUI palette usage in BookmarkNotification etc.); notistack same-key update lifecycle is now store-merged to preserve variant/sx/persist (circular bug fixed) — full state-driven controller refactor is heavy lift and deferred pending integration test coverage. Signed-off-by: Kumar Nirupam --- src/custom/ProgressBar/ProgressBar.tsx | 2 +- src/custom/ProgressBar/useProgressBar.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/custom/ProgressBar/ProgressBar.tsx b/src/custom/ProgressBar/ProgressBar.tsx index 8d2b9f726..85fd2babd 100644 --- a/src/custom/ProgressBar/ProgressBar.tsx +++ b/src/custom/ProgressBar/ProgressBar.tsx @@ -69,7 +69,7 @@ export const ProgressBar = React.forwardRef( const isDeterminate = typeof progress === 'number' && Number.isFinite(progress); const normalizedProgress = isDeterminate ? clampProgress(progress as number) : undefined; - const shouldShowLabel = showProgressLabel ?? isDeterminate; + const shouldShowLabel = (showProgressLabel ?? isDeterminate) && variant === 'linear'; const handleClose = React.useCallback(() => { closeSnackbar(id); diff --git a/src/custom/ProgressBar/useProgressBar.ts b/src/custom/ProgressBar/useProgressBar.ts index 467e848a5..4494fee25 100644 --- a/src/custom/ProgressBar/useProgressBar.ts +++ b/src/custom/ProgressBar/useProgressBar.ts @@ -2,7 +2,10 @@ import { useSnackbar, type OptionsObject, type SnackbarKey, type SnackbarMessage import React from 'react'; import { ProgressBar, type ProgressBarProps } from './ProgressBar'; -export interface ShowProgressBarOptions extends Omit { +export interface ShowProgressBarOptions extends Omit< + OptionsObject, + 'content' | 'key' | 'variant' | 'action' +> { /** * Initial progress 0-100. Omit or pass a non-finite value (NaN/Infinity) for indeterminate. */