From 99522b6a4aac7ea370291594a478e75c9314e71f Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 26 Apr 2026 15:20:59 +0200 Subject: [PATCH 01/25] Add react-transitioning --- package-lock.json | 10 ++ package.json | 1 + src/components/generics/NotificationBar.jsx | 146 ++++++++++++++++++ src/style/components/generics/_index.scss | 2 +- .../generics/_notification_bar.scss | 76 +++++++++ 5 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 src/components/generics/NotificationBar.jsx create mode 100644 src/style/components/generics/_notification_bar.scss diff --git a/package-lock.json b/package-lock.json index 75661595..83f96750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react-redux": "^9.2.0", "react-router": "^7.12.0", "react-transition-group": "^4.4.5", + "react-transitioning": "^1.0.9", "redux": "^5.0.1", "redux-localstorage": "^0.4.1", "redux-thunk": "^3.1.0", @@ -6310,6 +6311,15 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-transitioning": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/react-transitioning/-/react-transitioning-1.0.9.tgz", + "integrity": "sha512-XnICsOdR/B5C0nyrM66MzF5OFSrigbCpwDNyNLgkMoyk93fFvehNgLc9ByIUHKcvJK7FR3BvM2cFMccJzO80Fg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", diff --git a/package.json b/package.json index 0cefda78..a59dae0f 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "react-redux": "^9.2.0", "react-router": "^7.12.0", "react-transition-group": "^4.4.5", + "react-transitioning": "^1.0.9", "redux": "^5.0.1", "redux-localstorage": "^0.4.1", "redux-thunk": "^3.1.0", diff --git a/src/components/generics/NotificationBar.jsx b/src/components/generics/NotificationBar.jsx new file mode 100644 index 00000000..ec1cf036 --- /dev/null +++ b/src/components/generics/NotificationBar.jsx @@ -0,0 +1,146 @@ +import classNames from 'classnames' +import PropTypes from 'prop-types' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' + +import { useDefaultTransitionState } from 'hooks/transitions' +import { + alterationResponsePropType, + Status, +} from 'reducers/alterationsResponse' + +const types = { + [Status.pending]: 'success', + [Status.successful]: 'success', + [Status.failed]: 'danger', +} + +export default function NotificationBar({ + alterationResponse = {}, + failedDuration = 5000, + failedMessage = 'Failure', + pendingMessage = 'Pending…', + successfulDuration = 3000, + successfulMessage = 'Success', + noDisplayOnMount = false, +}) { + const [state, toggle] = useDefaultTransitionState() + + const durations = useMemo( + () => ({ + [Status.successful]: successfulDuration, + [Status.failed]: failedDuration, + }), + [successfulDuration, failedDuration] + ) + const messages = useMemo( + () => ({ + [Status.pending]: pendingMessage, + [Status.successful]: successfulMessage, + [Status.failed]: failedMessage, + }), + [pendingMessage, successfulMessage, failedMessage] + ) + + const { status, date } = alterationResponse + + const getMessage = useCallback( + (alterationResponse) => { + const { status, fields, message } = alterationResponse + + // specific case if the alteration response contains field errors + if (fields && Object.keys(fields).length) { + return 'There are field errors' + } + + // specific case if the alteration contains a message + if (message) { + return message + } + + // default to specified messages + return messages[status] + }, + [messages] + ) + + useEffect(() => { + if (!status || !date) { + return + } + + // display if status and date changed and have a valid value + toggle(true) + + // request to hide success or failure message only after a certain time + let timeout + if (status !== Status.pending && durations[status]) { + timeout = setTimeout(() => { + toggle(false) + }, durations[status]) + } + + return () => { + if (timeout) { + clearTimeout(timeout) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [status, date]) + + const notificationMessage = getMessage(alterationResponse) + + if (notificationMessage && state.isMounted) { + return ( +
+
+
{getMessage(alterationResponse)}
+
+
+ ) + } + + return null +} + +NotificationBar.propTypes = { + alterationResponse: alterationResponsePropType, + failedDuration: PropTypes.number, + failedMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + pendingMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + successfulDuration: PropTypes.number, + successfulMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), + noDisplayOnMount: PropTypes.bool, +} + +/** + * Helper to create a notifiable area in a table + * + * Must be used on the cell of the first column. + */ +export function NotifiableForTable({ className, children }) { + const elementRef = useRef() + + const [parentTableElement, setParentTableElement] = useState(null) + + useEffect(() => { + // find parent table DOM node + const element = elementRef.current + setParentTableElement(element.closest('table')) + }, []) + + // get the width of the element with the width of the closest table + const width = parentTableElement?.clientWidth + + return ( +
+
+ {children} +
+
+ ) +} + +NotifiableForTable.propTypes = { + children: PropTypes.node, + className: PropTypes.string, +} diff --git a/src/style/components/generics/_index.scss b/src/style/components/generics/_index.scss index 9d8b69e2..0c783967 100644 --- a/src/style/components/generics/_index.scss +++ b/src/style/components/generics/_index.scss @@ -9,7 +9,7 @@ @use 'form'; @use 'listing'; @use 'navigator'; -@use 'notification'; +@use 'notification_bar'; @use 'permission_text'; @use 'searchbox'; @use 'shapes'; diff --git a/src/style/components/generics/_notification_bar.scss b/src/style/components/generics/_notification_bar.scss new file mode 100644 index 00000000..121fd146 --- /dev/null +++ b/src/style/components/generics/_notification_bar.scss @@ -0,0 +1,76 @@ +// +// Dakara project +// +// Notifications style file +// + +@use '~/abstracts/colors'; +@use '~/abstracts/sizes'; +@use '~/abstracts/support'; +@use '~/abstracts/transitions'; + +// `notification` class: +// +// The notification class defines the look and feel of notification messages. +// It does not defines how the notification should appear. For this, use the +// `notified` class on the element and the `notifiable` class on its parent +// element. +.notification { + box-sizing: border-box; + + .message { + @include sizes.make-gap(padding, horizontal); + } +} + +// `notifiable` class: +// +// The notifiable class provides the way to make the notifiation appear. It also +// contains rules for animations in the right corner of the notification banner. +.notifiable { + overflow: hidden; + position: relative; + + .notified { + height: 100%; + left: 0; + position: absolute; + top: 0; + width: 100%; + + .notification { + display: flex; + min-height: 100%; + + .message { + align-items: center; + display: flex; + flex: 1; + } + } + } +} + +// `notifiable-for-table` class: +// +// The notifiable class for table provides the equivalent notifiable class +// rules, adapted for a table. +// +// This class must be used in the cell of the first column. +.notifiable-for-table { + height: 100%; + position: relative; + + .notifiable { + // make the element ensensitive to clicks and restore it right after + height: 100%; + left: 0; + pointer-events: none; + position: absolute; + top: 0; + + .notified { + pointer-events: initial; + } + } +} From 148f634c80af2a6fc235fac5f4205e0accf0d0b2 Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 26 Apr 2026 15:21:27 +0200 Subject: [PATCH 02/25] Update NotificationBar transition --- src/components/generics/Form.jsx | 4 +- src/components/generics/Notification.jsx | 162 ------------------ src/components/generics/NotificationBar.jsx | 26 +-- src/components/generics/TokenWidget.jsx | 4 +- .../generics/listing/FetchWrapper.jsx | 4 +- src/components/library/song/Entry.jsx | 6 +- .../library/widgets/SongExpanded.jsx | 4 +- src/components/playlist/queuing/Entry.jsx | 6 +- src/components/settings/Tokens.jsx | 8 +- src/components/settings/songTags/Entry.jsx | 8 +- src/components/settings/users/Entry.jsx | 6 +- .../components/generics/_notification.scss | 97 ----------- .../generics/_notification_bar.scss | 23 ++- 13 files changed, 61 insertions(+), 297 deletions(-) delete mode 100644 src/components/generics/Notification.jsx delete mode 100644 src/style/components/generics/_notification.scss diff --git a/src/components/generics/Form.jsx b/src/components/generics/Form.jsx index 0315d98d..b65d38d2 100644 --- a/src/components/generics/Form.jsx +++ b/src/components/generics/Form.jsx @@ -9,7 +9,7 @@ import { setAlterationValidationErrors, submitAlteration, } from 'actions/alterations' -import Notification from 'components/generics/Notification' +import NotificationBar from 'components/generics/NotificationBar' import { alterationResponsePropType, Status, @@ -387,7 +387,7 @@ class FormBlock extends Form { {header} {fieldsSet}
- ({ - [Status.successful]: successfulDuration, - [Status.failed]: failedDuration, - }), - [successfulDuration, failedDuration] - ) - const messages = useMemo( - () => ({ - [Status.pending]: pendingMessage, - [Status.successful]: successfulMessage, - [Status.failed]: failedMessage, - }), - [pendingMessage, successfulMessage, failedMessage] - ) - - const { - status, - date, - message: messageInState, - fields: fieldsInState, - } = alterationResponse || {} - - useEffect(() => { - if (!status || !date) { - return - } - - // display if status and date changed and have a valid value - setDisplay(true) - - // request to hide success or failure message only after a certain time - let timeout - if (status !== Status.pending && durations[status]) { - timeout = setTimeout(() => { - setDisplay(false) - }, durations[status]) - } - - return () => { - if (timeout) { - clearTimeout(timeout) - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [status, date]) - - let notification - if (display && alterationResponse) { - // if there is a message in the state, keep it - // if there is a field in the state, consider there was an error with fields - // otherwise, use the messages passed to the compenent - let message - if (messageInState) { - message = messageInState - } else if (fieldsInState && Object.keys(fieldsInState).length > 0) { - message = 'There are field errors.' - } else { - message = messages[status] - } - - // if there is no message to display, do not show any notification - if (message) { - notification = ( - -
-
-
{message}
-
-
-
- ) - } - } - - return ( - - {notification} - - ) -} - -Notification.propTypes = { - alterationResponse: alterationResponsePropType, - failedDuration: PropTypes.number, - failedMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - pendingMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - successfulDuration: PropTypes.number, - successfulMessage: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), - noDisplayOnMount: PropTypes.bool, -} - -/** - * Helper to create a notifiable area in a table - * - * Must be used on the cell of the first column. - */ -export function NotifiableForTable({ className, children }) { - const elementRef = useRef() - - const [parentTableElement, setParentTableElement] = useState(null) - - useEffect(() => { - // find parent table DOM node - const element = elementRef.current - setParentTableElement(element.closest('table')) - }, []) - - // get the width of the element with the width of the closest table - const width = parentTableElement?.clientWidth - - return ( -
-
- {children} -
-
- ) -} - -NotifiableForTable.propTypes = { - children: PropTypes.node, - className: PropTypes.string, -} diff --git a/src/components/generics/NotificationBar.jsx b/src/components/generics/NotificationBar.jsx index ec1cf036..60a19d06 100644 --- a/src/components/generics/NotificationBar.jsx +++ b/src/components/generics/NotificationBar.jsx @@ -1,8 +1,8 @@ import classNames from 'classnames' import PropTypes from 'prop-types' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { CSSTransition } from 'react-transitioning' -import { useDefaultTransitionState } from 'hooks/transitions' import { alterationResponsePropType, Status, @@ -23,7 +23,7 @@ export default function NotificationBar({ successfulMessage = 'Success', noDisplayOnMount = false, }) { - const [state, toggle] = useDefaultTransitionState() + const [show, setShow] = useState(false) const durations = useMemo( () => ({ @@ -69,13 +69,13 @@ export default function NotificationBar({ } // display if status and date changed and have a valid value - toggle(true) + setShow(true) // request to hide success or failure message only after a certain time let timeout if (status !== Status.pending && durations[status]) { timeout = setTimeout(() => { - toggle(false) + setShow(false) }, durations[status]) } @@ -89,17 +89,19 @@ export default function NotificationBar({ const notificationMessage = getMessage(alterationResponse) - if (notificationMessage && state.isMounted) { - return ( -
+ if (!notificationMessage) { + return null + } + + return ( + +
-
{getMessage(alterationResponse)}
+
{notificationMessage}
- ) - } - - return null +
+ ) } NotificationBar.propTypes = { diff --git a/src/components/generics/TokenWidget.jsx b/src/components/generics/TokenWidget.jsx index 85356d90..4db79790 100644 --- a/src/components/generics/TokenWidget.jsx +++ b/src/components/generics/TokenWidget.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types' import { useState } from 'react' -import Notification from 'components/generics/Notification' +import NotificationBar from 'components/generics/NotificationBar' import { Status } from 'reducers/alterationsResponse' export default function TokenWidget({ token }) { @@ -32,7 +32,7 @@ export default function TokenWidget({ token }) {
- {children} - , - , - , - -

Create a token that can be used to authenticate the player.

- - - - - Date: Sun, 26 Apr 2026 19:50:40 +0200 Subject: [PATCH 03/25] Create Collapse and Slide transitions --- src/components/generics/NotificationBar.jsx | 8 +- src/components/generics/listing/Entry.jsx | 19 ++--- src/components/transitions/Collapse.jsx | 16 ++++ src/components/transitions/Slide.jsx | 16 ++++ src/style/abstracts/_transitions.scss | 77 +++++++++++++++++++ src/style/components/_index.scss | 1 + .../generics/_notification_bar.scss | 22 ------ .../components/generics/listing/_listing.scss | 24 +----- src/style/components/library/_artist.scss | 2 +- src/style/components/library/_song.scss | 4 +- src/style/components/library/_work.scss | 2 +- .../components/transitions/_collapse.scss | 17 ++++ src/style/components/transitions/_index.scss | 2 + src/style/components/transitions/_slide.scss | 3 + 14 files changed, 148 insertions(+), 65 deletions(-) create mode 100644 src/components/transitions/Collapse.jsx create mode 100644 src/components/transitions/Slide.jsx create mode 100644 src/style/abstracts/_transitions.scss create mode 100644 src/style/components/transitions/_collapse.scss create mode 100644 src/style/components/transitions/_index.scss create mode 100644 src/style/components/transitions/_slide.scss diff --git a/src/components/generics/NotificationBar.jsx b/src/components/generics/NotificationBar.jsx index 60a19d06..875d3e91 100644 --- a/src/components/generics/NotificationBar.jsx +++ b/src/components/generics/NotificationBar.jsx @@ -1,8 +1,8 @@ import classNames from 'classnames' import PropTypes from 'prop-types' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { CSSTransition } from 'react-transitioning' +import Slide from 'components/transitions/Slide' import { alterationResponsePropType, Status, @@ -94,13 +94,13 @@ export default function NotificationBar({ } return ( - -
+ +
{notificationMessage}
- +
) } diff --git a/src/components/generics/listing/Entry.jsx b/src/components/generics/listing/Entry.jsx index 082c7741..7c707f6d 100644 --- a/src/components/generics/listing/Entry.jsx +++ b/src/components/generics/listing/Entry.jsx @@ -4,7 +4,7 @@ import { useEffect } from 'react' import { useSearchParams } from 'react-router' import { CSSTransition, TransitionGroup } from 'react-transition-group' -import { CSSTransitionLazy } from 'thirdpartyExtensions/ReactTransitionGroup' +import Collapse from 'components/transitions/Collapse' import { isDisplayable } from 'utils' export function ListingEntry({ @@ -67,7 +67,7 @@ export function ListingEntry({ return (
  • {notifications}
    )}
  • - - <>{entryExpanded} - + +
    {entryExpanded}
    +
    ) } @@ -159,7 +152,7 @@ export function ListingEntryExpanded({ notifications, }) { return ( -
    +
    {isDisplayable(extra) &&
      {extra}
    }
    {children}
    {(isDisplayable(controls) || isDisplayable(notifications)) && ( diff --git a/src/components/transitions/Collapse.jsx b/src/components/transitions/Collapse.jsx new file mode 100644 index 00000000..bf93e234 --- /dev/null +++ b/src/components/transitions/Collapse.jsx @@ -0,0 +1,16 @@ +import PropTypes from 'prop-types' +import { CSSTransition } from 'react-transitioning' + +export default function Collapse({ in: inProp, duration = 300, children }) { + return ( + + {children} + + ) +} + +Collapse.propTypes = { + in: PropTypes.bool, + duration: PropTypes.number, + children: PropTypes.node, +} diff --git a/src/components/transitions/Slide.jsx b/src/components/transitions/Slide.jsx new file mode 100644 index 00000000..91e85302 --- /dev/null +++ b/src/components/transitions/Slide.jsx @@ -0,0 +1,16 @@ +import PropTypes from 'prop-types' +import { CSSTransition } from 'react-transitioning' + +export default function Slide({ in: inProp, duration = 300, children }) { + return ( + + {children} + + ) +} + +Slide.propTypes = { + in: PropTypes.bool, + duration: PropTypes.number, + children: PropTypes.node, +} diff --git a/src/style/abstracts/_transitions.scss b/src/style/abstracts/_transitions.scss new file mode 100644 index 00000000..ca87b559 --- /dev/null +++ b/src/style/abstracts/_transitions.scss @@ -0,0 +1,77 @@ +// +// Dakara Project +// +// Transitions style file +// + +// helper for creating transition styles +// @param $name name of the transitiion +// @param $property property to transition +// @param $from value of the property before entered and after exiting +// @param $to value of the property after entered and before exiting +// @param $duration-enter timing of the entering step +// @param $duration-exit timing of the exiting step +// @param $enter enable enter step +// @param $enter-active enable enterActive step +// @param $enter-done enable enterDone step +// @param $exit enable exit step +// @param $exit-active enable exitActive step +// @param $exit-done enable exitDone step +// step +@mixin make-transition( + $name, + $property, + $from, + $to, + $duration-enter: 300ms, + $duration-exit: 150ms, + $enter: true, + $enter-active: true, + $enter-done: true, + $exit: true, + $exit-active: true, + $exit-done: true +) { + .transition { + @if ($enter) { + &.#{$name}-appear, + &.#{$name}-enter { + #{$property}: $from; + + @if ($enter-active) { + &.#{$name}-appear-active, + &.#{$name}-enter-active { + #{$property}: $to; + transition: $property $duration-enter ease-out; + } + } + } + } + + @if ($enter-done) { + &.#{$name}-appear-done, + &.#{$name}-enter-done { + #{$property}: $to; + } + } + + @if ($exit) { + &.#{$name}-exit { + #{$property}: $to; + + @if ($exit-active) { + &.#{$name}-exit-active { + #{$property}: $from; + transition: $property $duration-exit ease-in; + } + } + } + } + + @if ($exit-done) { + &.#{$name}-exit-done { + #{$property}: $from; + } + } + } +} diff --git a/src/style/components/_index.scss b/src/style/components/_index.scss index 2e383a42..b33a1824 100644 --- a/src/style/components/_index.scss +++ b/src/style/components/_index.scss @@ -19,4 +19,5 @@ @use 'navigation'; @use 'playlist'; @use 'settings'; +@use 'transitions'; @use 'user'; diff --git a/src/style/components/generics/_notification_bar.scss b/src/style/components/generics/_notification_bar.scss index 1c9fabe2..b88edfdd 100644 --- a/src/style/components/generics/_notification_bar.scss +++ b/src/style/components/generics/_notification_bar.scss @@ -37,28 +37,6 @@ top: 0; width: 100%; - // notification transition appearance - &.slide-appear, - &.slide-enter { - left: 100%; - - &.slide-appear-active, - &.slide-enter-active { - left: 0; - transition: left 300ms ease-out; - } - } - - // notification transition disappearance - &.slide-exit { - left: 0; - - &.slide-exit-active { - left: 100%; - transition: left 150ms ease-out; - } - } - .notification { display: flex; min-height: 100%; diff --git a/src/style/components/generics/listing/_listing.scss b/src/style/components/generics/listing/_listing.scss index 021c19dc..5c1b9539 100644 --- a/src/style/components/generics/listing/_listing.scss +++ b/src/style/components/generics/listing/_listing.scss @@ -129,7 +129,7 @@ $listing-entry-control-icon-size: sizes.$row-icon-font-size; padding: 0; } - > .one-line { + .listing-entry-compact { display: flex; .expander { @@ -201,29 +201,9 @@ $listing-entry-control-icon-size: sizes.$row-icon-font-size; } } - > .expanded { + .listing-entry-expanded { @include sizes.make-gap(padding, vertical); - &.expand-collapse-enter { - max-height: 0; - overflow: hidden; - - &.expand-collapse-enter-active { - max-height: 15 * sizes.$subrow-height; - transition: max-height 600ms ease-out; - } - } - - &.expand-collapse-exit { - max-height: 15 * sizes.$subrow-height; - overflow: hidden; - - &.expand-collapse-exit-active { - max-height: 0; - transition: max-height 300ms ease-in; - } - } - // space between every element & > * + * { @include sizes.make-gap(margin, top); diff --git a/src/style/components/library/_artist.scss b/src/style/components/library/_artist.scss index bfaa665b..4dc73cbb 100644 --- a/src/style/components/library/_artist.scss +++ b/src/style/components/library/_artist.scss @@ -7,7 +7,7 @@ @use '~/abstracts/sizes'; #artist-library { - .one-line { + .listing-entry-compact { .main { .artist-widget { flex-grow: 1; diff --git a/src/style/components/library/_song.scss b/src/style/components/library/_song.scss index f171b6f1..e19e9897 100644 --- a/src/style/components/library/_song.scss +++ b/src/style/components/library/_song.scss @@ -10,7 +10,7 @@ // `listing` module. It is aimed to stylize the specific elements of a list of // songs. // -// The song can be represented in a compact, one-line view or in an expanded, +// The song can be represented in a compact, listing-entry-compact view or in an expanded, // multi-lines view. More elements are present in the expanded view, which takes // usually more space and have larger margins compared to the compact view. The // compact view stays even if the expandes view is displayed, it only contains @@ -20,7 +20,7 @@ // This list entry is not hoverizable, in order to make only the compact view // react to hover. #song-library { - .one-line { + .listing-entry-compact { .main { position: relative; diff --git a/src/style/components/library/_work.scss b/src/style/components/library/_work.scss index f0dbbd8b..519f90a5 100644 --- a/src/style/components/library/_work.scss +++ b/src/style/components/library/_work.scss @@ -7,7 +7,7 @@ @use '~/abstracts/sizes'; #work-library { - .one-line { + .listing-entry-compact { .main { .work-widget { flex-grow: 1; diff --git a/src/style/components/transitions/_collapse.scss b/src/style/components/transitions/_collapse.scss new file mode 100644 index 00000000..6b6eeda2 --- /dev/null +++ b/src/style/components/transitions/_collapse.scss @@ -0,0 +1,17 @@ +@use '~/abstracts/transitions'; +@use '~/abstracts/sizes'; + +@include transitions.make-transition( + 'collapse', + max-height, + $from: 0, + $to: calc(15 * sizes.$subrow-height), + $enter-done: false +); + +.transition { + &.collapse-enter, + &.collapse-exit { + overflow-y: hidden; + } +} diff --git a/src/style/components/transitions/_index.scss b/src/style/components/transitions/_index.scss new file mode 100644 index 00000000..f54e3fe3 --- /dev/null +++ b/src/style/components/transitions/_index.scss @@ -0,0 +1,2 @@ +@use 'collapse'; +@use 'slide'; diff --git a/src/style/components/transitions/_slide.scss b/src/style/components/transitions/_slide.scss new file mode 100644 index 00000000..c685cdc6 --- /dev/null +++ b/src/style/components/transitions/_slide.scss @@ -0,0 +1,3 @@ +@use '~/abstracts/transitions'; + +@include transitions.make-transition('slide', left, $from: 100%, $to: 0); From 461062b52f996646e1c88eb0422cf4bf4e512e50 Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 26 Apr 2026 20:22:28 +0200 Subject: [PATCH 04/25] Update ConfirmationBar transition Partially fixes #81. --- src/components/generics/ConfirmationBar.jsx | 76 ++++++++++++-------- src/components/playlist/queuing/Entry.jsx | 22 ++---- src/components/settings/Tokens.jsx | 77 ++++++++------------- src/components/settings/users/Entry.jsx | 23 ++---- 4 files changed, 88 insertions(+), 110 deletions(-) diff --git a/src/components/generics/ConfirmationBar.jsx b/src/components/generics/ConfirmationBar.jsx index 71f53326..ce3f6fcc 100644 --- a/src/components/generics/ConfirmationBar.jsx +++ b/src/components/generics/ConfirmationBar.jsx @@ -1,43 +1,65 @@ import PropTypes from 'prop-types' +import Slide from 'components/transitions/Slide' + export default function ConfirmationBar({ message = 'Are you sure?', + show, + setShow, onCancel, onConfirm, + hideOnCancel = true, + hideOnConfirm = false, }) { return ( -
    -
    -
    {message}
    -
    - - + +
    +
    +
    {message}
    +
    + + +
    -
    + ) } ConfirmationBar.propTypes = { message: PropTypes.string, - onCancel: PropTypes.func.isRequired, - onConfirm: PropTypes.func.isRequired, + show: PropTypes.bool, + setShow: PropTypes.func.isRequired, + onCancel: PropTypes.func, + onConfirm: PropTypes.func, + hideOnCancel: PropTypes.bool, + hideOnConfirm: PropTypes.bool, } diff --git a/src/components/playlist/queuing/Entry.jsx b/src/components/playlist/queuing/Entry.jsx index 269d1904..059c164d 100644 --- a/src/components/playlist/queuing/Entry.jsx +++ b/src/components/playlist/queuing/Entry.jsx @@ -286,24 +286,14 @@ export default function QueuingEntry({ entry, positions }) { ] const notifications = [ - { + dispatch(removeEntryFromPlaylist(entry.id)) }} - > - { - dispatch(removeEntryFromPlaylist(entry.id)) - }} - onCancel={() => { - setConfirmDisplayed(false) - }} - /> - , + />, { - setConfirmDisplayed(true) - }, []) - - const clearConfirm = useCallback(() => { - setConfirmDisplayed(false) - }, []) - return (
    @@ -43,28 +34,25 @@ function PlayerTokenBoxDisplay({ playerToken, karaoke }) {

    - { + dispatch(revokePlayerToken(karaoke.id)) }} - > - { - dispatch(revokePlayerToken(karaoke.id)) - }} - onCancel={clearConfirm} - /> - + /> -
    @@ -188,14 +176,6 @@ export default function Tokens() { const dispatch = useDispatch() - const displayConfirm = useCallback(() => { - setConfirmDisplayed(true) - }, []) - - const clearConfirm = useCallback(() => { - setConfirmDisplayed(false) - }, []) - return (
    @@ -209,30 +189,27 @@ export default function Tokens() {
    - { + dispatch(revokeToken()) }} - > - { - dispatch(revokeToken()) - }} - onCancel={clearConfirm} - /> - + /> -
    diff --git a/src/components/settings/users/Entry.jsx b/src/components/settings/users/Entry.jsx index f10b4a86..33ce8008 100644 --- a/src/components/settings/users/Entry.jsx +++ b/src/components/settings/users/Entry.jsx @@ -13,7 +13,6 @@ import PermissionText from 'components/generics/PermissionText' import { Checkmark } from 'components/generics/Shapes' import { IsNotSelf, IsUsersManager } from 'permissions/components/Users' import { userPropType } from 'serverPropTypes/users' -import { CSSTransitionLazy } from 'thirdpartyExtensions/ReactTransitionGroup' export default function UsersEntry({ user }) { const query = useSelector((state) => state.settings.users.list.data.query) @@ -39,23 +38,13 @@ export default function UsersEntry({ user }) { - { + dispatch(deleteUser(user.id)) }} - > - { - dispatch(deleteUser(user.id)) - }} - onCancel={() => { - setConfirmDisplayed(false) - }} - /> - + /> Date: Sun, 26 Apr 2026 20:47:03 +0200 Subject: [PATCH 05/25] Update SearchBox transition --- src/components/generics/SearchBox.jsx | 23 +++++++---------- src/style/components/generics/_searchbox.scss | 25 ++++++------------- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/components/generics/SearchBox.jsx b/src/components/generics/SearchBox.jsx index 5b420b42..70a51d63 100644 --- a/src/components/generics/SearchBox.jsx +++ b/src/components/generics/SearchBox.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types' import { useEffect, useState } from 'react' import { useSearchParams } from 'react-router' -import { CSSTransitionLazy } from 'thirdpartyExtensions/ReactTransitionGroup' +import Collapse from 'components/transitions/Collapse' function SearchBoxHelp({ example, fields, withHash }) { return ( @@ -53,12 +53,12 @@ SearchBoxHelp.propTypes = { * Note that the query and its setter are owned by a parent component. */ export default function SearchBox({ help, placeholder, query, setQuery }) { - const [displayHelp, setDisplayHelp] = useState(false) - const [searchParams, setSearchParams] = useSearchParams() const queryFromParams = searchParams.get('query') + const [displayed, setDisplayed] = useState(false) + useEffect( () => { // update query from URL immediately @@ -79,7 +79,7 @@ export default function SearchBox({ help, placeholder, query, setQuery }) { className="control square transparent" type="button" onClick={() => { - setDisplayHelp(!displayHelp) + setDisplayed(!displayed) }} > @@ -89,16 +89,11 @@ export default function SearchBox({ help, placeholder, query, setQuery }) { ) helpBox = ( - - - + +
    + +
    +
    ) } diff --git a/src/style/components/generics/_searchbox.scss b/src/style/components/generics/_searchbox.scss index c7a4a35e..4d8a2308 100644 --- a/src/style/components/generics/_searchbox.scss +++ b/src/style/components/generics/_searchbox.scss @@ -27,26 +27,17 @@ } } - .help { - &.help-enter { - max-height: 0; - overflow: hidden; + .transition { + margin: 0; - &.help-enter-active { - max-height: (3 * sizes.$row-height); - transition: max-height 300ms ease-out; - } + &.collapse-enter.collapse-enter-active, + &.collapse-exit:not(.collapse-exit-active) { + max-height: 4 * sizes.$row-height; } + } - &.help-exit { - max-height: (3 * sizes.$row-height); - overflow: hidden; - - &.help-exit-active { - max-height: 0; - transition: max-height 150ms ease-out; - } - } + .help { + @include sizes.make-gap(padding, top); q { &::before, From effbf16d7e2793a8de8fa79050301e789660622e Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 26 Apr 2026 21:17:38 +0200 Subject: [PATCH 06/25] Update Player transitions --- src/components/karaoke/player/Player.jsx | 138 ++++++++---------- .../components/karaoke/player/_player.scss | 42 ++---- 2 files changed, 74 insertions(+), 106 deletions(-) diff --git a/src/components/karaoke/player/Player.jsx b/src/components/karaoke/player/Player.jsx index 0a9e05df..80dc8dee 100644 --- a/src/components/karaoke/player/Player.jsx +++ b/src/components/karaoke/player/Player.jsx @@ -1,5 +1,4 @@ import classNames from 'classnames' -import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { sendPlayerCommand } from 'actions/playlist' @@ -11,9 +10,9 @@ import { } from 'components/karaoke/player/Carousel' import ManageButton from 'components/karaoke/player/ManageButton' import PlayerNotification from 'components/karaoke/player/Notification' +import Collapse from 'components/transitions/Collapse' import { isPlaylistManagerOrOwner } from 'permissions/playlist' import { Status } from 'reducers/alterationsResponse' -import { CSSTransitionLazy } from 'thirdpartyExtensions/ReactTransitionGroup' export default function Player() { const user = useSelector((state) => state.authenticatedUser) @@ -25,22 +24,14 @@ export default function Player() { (state) => state.alterationsResponse.multiple.sendPlayerCommands ) - const [animationsEnabled, setAnimationsEnabled] = useState(false) - const dispatch = useDispatch() const { data: playerStatus } = playerStatusState - const withControls = isPlaylistManagerOrOwner( + const hasControls = isPlaylistManagerOrOwner( user, playerStatus.playlist_entry ) - useEffect(() => { - // enable animations if the user, the player status, or the possibility to - // use controls changes - setAnimationsEnabled(true) - }, [user, playerStatus, withControls]) - const { playerErrors } = playerErrorsDigestState.data const fetchError = playerStatusState.status === Status.failed const isPlaying = !!playerStatus.playlist_entry @@ -83,74 +74,69 @@ export default function Player() { )} - -
    - { - dispatch(sendPlayerCommand('restart')) - }} - disabled={controlDisabled} - error={fetchError} - icon="step-backward" - /> - { - dispatch(sendPlayerCommand('rewind')) - }} - disabled={controlDisabled} - error={fetchError} - icon="backward" - /> - { - if (!isPlaying) return + +
    +
    + { + dispatch(sendPlayerCommand('restart')) + }} + disabled={controlDisabled} + error={fetchError} + icon="step-backward" + /> + { + dispatch(sendPlayerCommand('rewind')) + }} + disabled={controlDisabled} + error={fetchError} + icon="backward" + /> + { + if (!isPlaying) return - if (playerStatus.paused) { - dispatch(sendPlayerCommand('resume')) - } else { - dispatch(sendPlayerCommand('pause')) + if (playerStatus.paused) { + dispatch(sendPlayerCommand('resume')) + } else { + dispatch(sendPlayerCommand('pause')) + } + }} + disabled={controlDisabled} + error={fetchError} + icon={ + isPlaying ? (playerStatus.paused ? 'play' : 'pause') : 'stop' } - }} - disabled={controlDisabled} - error={fetchError} - icon={isPlaying ? (playerStatus.paused ? 'play' : 'pause') : 'stop'} - /> - { - dispatch(sendPlayerCommand('fast_forward')) - }} - disabled={controlDisabled} - error={fetchError} - icon="forward" - /> - { - dispatch(sendPlayerCommand('skip', true)) - }} - disabled={controlDisabled} - error={fetchError} - icon="step-forward" - /> + /> + { + dispatch(sendPlayerCommand('fast_forward')) + }} + disabled={controlDisabled} + error={fetchError} + icon="forward" + /> + { + dispatch(sendPlayerCommand('skip', true)) + }} + disabled={controlDisabled} + error={fetchError} + icon="step-forward" + /> +
    - +
    Date: Mon, 27 Apr 2026 00:18:28 +0200 Subject: [PATCH 07/25] Update QueuingEntry transition --- src/components/playlist/queuing/Entry.jsx | 28 +++++-------------- src/components/transitions/Collapse.jsx | 26 +++++++++++++++-- src/style/components/generics/_searchbox.scss | 4 +-- .../components/karaoke/player/_player.scss | 4 +-- src/style/components/playlist/_queuing.scss | 23 +++------------ .../components/transitions/_collapse.scss | 21 ++++++++++++-- 6 files changed, 57 insertions(+), 49 deletions(-) diff --git a/src/components/playlist/queuing/Entry.jsx b/src/components/playlist/queuing/Entry.jsx index 059c164d..fb1c7cce 100644 --- a/src/components/playlist/queuing/Entry.jsx +++ b/src/components/playlist/queuing/Entry.jsx @@ -15,12 +15,12 @@ import { } from 'components/generics/listing/Entry' import NotificationBar from 'components/generics/NotificationBar' import PlaylistEntryWidget from 'components/playlist/widgets/PlaylistEntry' +import Collapse from 'components/transitions/Collapse' import { IsPlaylistManager, IsPlaylistManagerOrOwner, } from 'permissions/components/Playlist' import { playlistEntryPropType } from 'serverPropTypes/playlist' -import { CSSTransitionLazy } from 'thirdpartyExtensions/ReactTransitionGroup' import { formatDateLong } from 'utils' function ReorderButton({ handleReorder, className, id }) { @@ -212,15 +212,8 @@ export default function QueuingEntry({ entry, positions }) { const controlsExpanded = [ - -
    + +
    {!positions.isFirstPage && ( )}
    - +
    - -
    + +
    {reorderIndex > positions.position ? ( )}
    - +
    , controlSearch, ] diff --git a/src/components/transitions/Collapse.jsx b/src/components/transitions/Collapse.jsx index bf93e234..50840ee4 100644 --- a/src/components/transitions/Collapse.jsx +++ b/src/components/transitions/Collapse.jsx @@ -1,9 +1,30 @@ import PropTypes from 'prop-types' import { CSSTransition } from 'react-transitioning' -export default function Collapse({ in: inProp, duration = 300, children }) { +export default function Collapse({ + in: inProp, + duration = 300, + horizontal = false, + children, +}) { + if (horizontal) { + return ( + + {children} + + ) + } + return ( - + {children} ) @@ -12,5 +33,6 @@ export default function Collapse({ in: inProp, duration = 300, children }) { Collapse.propTypes = { in: PropTypes.bool, duration: PropTypes.number, + horizontal: PropTypes.bool, children: PropTypes.node, } diff --git a/src/style/components/generics/_searchbox.scss b/src/style/components/generics/_searchbox.scss index 4d8a2308..1d2f1603 100644 --- a/src/style/components/generics/_searchbox.scss +++ b/src/style/components/generics/_searchbox.scss @@ -30,8 +30,8 @@ .transition { margin: 0; - &.collapse-enter.collapse-enter-active, - &.collapse-exit:not(.collapse-exit-active) { + &.collapse-vertical-enter.collapse-vertical-enter-active, + &.collapse-vertical-exit:not(.collapse-vertical-exit-active) { max-height: 4 * sizes.$row-height; } } diff --git a/src/style/components/karaoke/player/_player.scss b/src/style/components/karaoke/player/_player.scss index de6d2e4f..b2cc446b 100644 --- a/src/style/components/karaoke/player/_player.scss +++ b/src/style/components/karaoke/player/_player.scss @@ -37,8 +37,8 @@ $player-progressbar-height: 0.4rem; } .transition { - &.collapse-enter.collapse-enter-active, - &.collapse-exit:not(.collapse-exit-active) { + &.collapse-vertical-enter.collapse-vertical-enter-active, + &.collapse-vertical-exit:not(.collapse-vertical-exit-active) { max-height: $player-controls-height + sizes.$gap-vertical; @include support.make-smartphone { diff --git a/src/style/components/playlist/_queuing.scss b/src/style/components/playlist/_queuing.scss index 617f5593..1389b395 100644 --- a/src/style/components/playlist/_queuing.scss +++ b/src/style/components/playlist/_queuing.scss @@ -13,25 +13,10 @@ gap: inherit; overflow: hidden; - // display transition appearance - &.show-hide-appear, - &.show-hide-enter { - max-width: 0; - - &.show-hide-appear-active, - &.show-hide-enter-active { - max-width: 3 * sizes.$row-height; - transition: max-width 300ms ease-out; - } - } - - // display transition disappearance - &.show-hide-exit { - max-width: 3 * sizes.$row-height; - - &.show-hide-exit-active { - max-width: 0; - transition: max-width 150ms ease-in; + &.transition { + &.collapse-horizontal-enter.collapse-horizontal-enter-active, + &.collapse-horizontal-exit:not(.collapse-horizontal-exit-active) { + max-width: calc(2 * sizes.$row-height + sizes.$gap-horizontal); } } } diff --git a/src/style/components/transitions/_collapse.scss b/src/style/components/transitions/_collapse.scss index 6b6eeda2..492f8d17 100644 --- a/src/style/components/transitions/_collapse.scss +++ b/src/style/components/transitions/_collapse.scss @@ -2,7 +2,7 @@ @use '~/abstracts/sizes'; @include transitions.make-transition( - 'collapse', + 'collapse-vertical', max-height, $from: 0, $to: calc(15 * sizes.$subrow-height), @@ -10,8 +10,23 @@ ); .transition { - &.collapse-enter, - &.collapse-exit { + &.collapse-vertical-enter, + &.collapse-vertical-exit { overflow-y: hidden; } } + +@include transitions.make-transition( + 'collapse-horizontal', + max-width, + $from: 0, + $to: calc(sizes.$row-height), + $enter-done: false +); + +.transition { + &.collapse-horizontal-enter, + &.collapse-horizontal-exit { + overflow-x: hidden; + } +} From 9d73997c1479e0ad7c0086de5efca5c53e98be1c Mon Sep 17 00:00:00 2001 From: Neraste Date: Fri, 1 May 2026 20:09:30 +0200 Subject: [PATCH 08/25] Update ListingList transition --- eslint.config.js | 2 +- src/components/generics/listing/Entry.jsx | 17 +++++- src/components/generics/listing/List.jsx | 49 +++++++-------- src/components/library/artist/Entry.jsx | 4 +- src/components/library/artist/List.jsx | 8 ++- src/components/library/song/Entry.jsx | 3 +- src/components/library/song/List.jsx | 8 ++- .../library/widgets/SongExpanded.jsx | 8 +-- src/components/library/work/Entry.jsx | 4 +- src/components/library/work/List.jsx | 8 ++- src/components/playlist/played/Entry.jsx | 3 +- src/components/playlist/played/List.jsx | 5 +- .../playlist/playerErrors/Entry.jsx | 3 +- src/components/playlist/playerErrors/List.jsx | 5 +- src/components/playlist/queuing/Entry.jsx | 3 +- src/components/playlist/queuing/List.jsx | 5 +- src/components/transitions/Collapse.jsx | 3 + src/components/transitions/Slide.jsx | 9 ++- src/contexts/listing.js | 3 + .../components/generics/listing/_listing.scss | 59 +++++++------------ .../components/transitions/_collapse.scss | 4 +- 21 files changed, 107 insertions(+), 106 deletions(-) create mode 100644 src/contexts/listing.js diff --git a/eslint.config.js b/eslint.config.js index e2f1709d..fcf352e8 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -70,7 +70,7 @@ export default [ ['^\\u0000'], ['^@?\\w'], [ - '^(actions|components|eventManagers|middleware|permissions|reducers|serverPropTypes|style|thirdpartyExtensions|utils)', + '^(actions|components|contexts|eventManagers|middleware|permissions|reducers|serverPropTypes|style|thirdpartyExtensions|utils)', ], ['^'], ['^\\.'], diff --git a/src/components/generics/listing/Entry.jsx b/src/components/generics/listing/Entry.jsx index 7c707f6d..2920886f 100644 --- a/src/components/generics/listing/Entry.jsx +++ b/src/components/generics/listing/Entry.jsx @@ -1,14 +1,16 @@ import classNames from 'classnames' import PropTypes from 'prop-types' -import { useEffect } from 'react' +import { useContext, useEffect } from 'react' import { useSearchParams } from 'react-router' import { CSSTransition, TransitionGroup } from 'react-transition-group' import Collapse from 'components/transitions/Collapse' +import { ListingNoTransitionContext } from 'contexts/listing' import { isDisplayable } from 'utils' export function ListingEntry({ children, + className, entryExpanded, extra, controls, @@ -19,6 +21,8 @@ export function ListingEntry({ }) { const [searchParams, setSearchParams] = useSearchParams() + const noTransition = useContext(ListingNoTransitionContext) + const expandable = !!entryExpanded // expanded state is stored in the search parameters, not in the state of the // component @@ -26,7 +30,8 @@ export function ListingEntry({ useEffect( () => { - if (onToggle) { + // manage on toggle callback if any + if (typeof onToggle === 'function') { onToggle(expanded) } }, @@ -65,7 +70,12 @@ export function ListingEntry({ } return ( -
  • +
  • {children} - } else { - content = ( - - {children.map((item, index) => ( - - {item} - - ))} - - ) - } + const content = ( + +
      + {noTransition ? ( + entries + ) : ( + + {entries.map((item) => ( + {item} + ))} + + )} +
    +
    + ) if (fetchStatus) { return ( @@ -71,7 +62,7 @@ export default function ListingList({ } ListingList.propTypes = { - children: PropTypes.node, + entries: PropTypes.arrayOf(PropTypes.node).isRequired, fetchStatus: PropTypes.symbol, free: PropTypes.bool, mini: PropTypes.bool, diff --git a/src/components/library/artist/Entry.jsx b/src/components/library/artist/Entry.jsx index 2feade02..49d1ce61 100644 --- a/src/components/library/artist/Entry.jsx +++ b/src/components/library/artist/Entry.jsx @@ -6,7 +6,7 @@ import { ListingEntry } from 'components/generics/listing/Entry' import ArtistWidget from 'components/library/widgets/Artist' import { artistPropType } from 'serverPropTypes/library' -export default function ArtistEntry({ artist, query }) { +export default function ArtistEntry({ artist, query, ...rest }) { const navigate = useNavigate() const controls = ( @@ -27,7 +27,7 @@ export default function ArtistEntry({ artist, query }) { ) return ( - + ) diff --git a/src/components/library/artist/List.jsx b/src/components/library/artist/List.jsx index 32b2f16e..b8739367 100644 --- a/src/components/library/artist/List.jsx +++ b/src/components/library/artist/List.jsx @@ -46,9 +46,11 @@ export default function ArtistList() { example: 'artist', }} /> - - {libraryEntryArtistList} - + state.library.song.data.query) const responseOfAddSong = useSelector( (state) => state.alterationsResponse.multiple.addSongToPlaylist?.[song.id] @@ -205,6 +205,7 @@ export default function SongEntry({ song, karaokeRemainingSeconds }) { notifications={notifications} entryExpanded={entryExpanded} onToggle={clearNotificationAlterations} + {...rest} > {isExpanded ? ( diff --git a/src/components/library/song/List.jsx b/src/components/library/song/List.jsx index 320149b8..f7e2fad5 100644 --- a/src/components/library/song/List.jsx +++ b/src/components/library/song/List.jsx @@ -66,9 +66,11 @@ export default function SongList() { withHash: true, }} /> - - {libraryEntrySongList} - + 1 ? workType.name_plural : workType.name} key={workTypeKey} > - - {worksList} - + ) }) @@ -129,9 +127,7 @@ export default function SongExpanded({ query, song }) { icon="la-microphone-alt" name={song.artists.length > 1 ? 'Artists' : 'Artist'} > - - {artistsList} - + ) } diff --git a/src/components/library/work/Entry.jsx b/src/components/library/work/Entry.jsx index 2b3f09cd..cce50142 100644 --- a/src/components/library/work/Entry.jsx +++ b/src/components/library/work/Entry.jsx @@ -6,7 +6,7 @@ import { ListingEntry } from 'components/generics/listing/Entry' import WorkWidget from 'components/library/widgets/Work' import { workPropType } from 'serverPropTypes/library' -export default function WorkEntry({ work, workType, query }) { +export default function WorkEntry({ work, workType, query, ...rest }) { const navigate = useNavigate() const controls = ( @@ -28,7 +28,7 @@ export default function WorkEntry({ work, workType, query }) { ) return ( - + ) diff --git a/src/components/library/work/List.jsx b/src/components/library/work/List.jsx index 65dc5c1e..adacd199 100644 --- a/src/components/library/work/List.jsx +++ b/src/components/library/work/List.jsx @@ -84,9 +84,11 @@ export default function WorkList() { example: workType.name.toLowerCase(), }} /> - - {libraryEntryWorkList} - + state.playlist.played.data.query) const playerErrorsDigestState = useSelector( (state) => state.playlist.digest.playerErrors @@ -80,6 +80,7 @@ export default function PlayedEntry({ entry }) { controls={controls} extra={extra} entryExpanded={entryExpanded} + {...rest} > {expanded ? ( - {playedComponent} - + /> state.playlist.playerErrors.data.query) const { @@ -92,6 +92,7 @@ export default function PlayerErrorsEntry({ playerError }) { id={playerError.id} controls={controls} entryExpanded={entryExpanded} + {...rest} > diff --git a/src/components/playlist/playerErrors/List.jsx b/src/components/playlist/playerErrors/List.jsx index c859cdde..6bdf388e 100644 --- a/src/components/playlist/playerErrors/List.jsx +++ b/src/components/playlist/playerErrors/List.jsx @@ -55,11 +55,10 @@ export default function PlayerErrorsList() { }} /> - {errorsList} - + /> state.playlist.queuing.data.query) const queuingEntriesDigest = useSelector( (state) => state.playlist.digest.entries.data.queuingEntries @@ -337,6 +337,7 @@ export default function QueuingEntry({ entry, positions }) { cancelReorder() } }} + {...rest} > {expanded ? ( - {queuingComponents} - + /> {children} @@ -24,6 +26,7 @@ export default function Collapse({ in={inProp} classNames="collapse-vertical" duration={duration} + {...rest} > {children} diff --git a/src/components/transitions/Slide.jsx b/src/components/transitions/Slide.jsx index 91e85302..e7074f0b 100644 --- a/src/components/transitions/Slide.jsx +++ b/src/components/transitions/Slide.jsx @@ -1,9 +1,14 @@ import PropTypes from 'prop-types' import { CSSTransition } from 'react-transitioning' -export default function Slide({ in: inProp, duration = 300, children }) { +export default function Slide({ + in: inProp, + duration = 300, + children, + ...rest +}) { return ( - + {children} ) diff --git a/src/contexts/listing.js b/src/contexts/listing.js new file mode 100644 index 00000000..25dc1ccd --- /dev/null +++ b/src/contexts/listing.js @@ -0,0 +1,3 @@ +import { createContext } from 'react' + +export const ListingNoTransitionContext = createContext(false) diff --git a/src/style/components/generics/listing/_listing.scss b/src/style/components/generics/listing/_listing.scss index 5c1b9539..59cb1dfe 100644 --- a/src/style/components/generics/listing/_listing.scss +++ b/src/style/components/generics/listing/_listing.scss @@ -88,42 +88,6 @@ $listing-entry-control-icon-size: sizes.$row-icon-font-size; } } - // appearance transition - &.add-remove-enter { - max-height: 0; - min-height: initial; - overflow: hidden; - - &.add-remove-enter-active { - max-height: $listing-entry-height; - transition: max-height 300ms ease-out; - } - } - - // disappearance transition - &.add-remove-exit { - max-height: $listing-entry-height; - min-height: initial; - overflow: hidden; - - &.expanded { - max-height: 15 * sizes.$subrow-height; - } - - &.add-remove-exit-active { - max-height: 0; - transition: max-height 150ms ease-in; - - &.expanded { - transition: max-height 300ms ease-in; - } - - &.delayed { - transition-delay: 500ms; - } - } - } - .extra { // remove list left padding padding: 0; @@ -201,6 +165,21 @@ $listing-entry-control-icon-size: sizes.$row-icon-font-size; } } + // transition for the entry + &.transition { + &.collapse-vertical-exit:not(.collapse-vertical-exit-active) { + max-height: 10 * sizes.$subrow-height; + } + } + + // transition for the expanded entry + .transition { + &.collapse-vertical-enter.collapse-vertical-enter-active, + &.collapse-vertical-exit:not(.collapse-vertical-exit-active) { + max-height: 10 * sizes.$subrow-height; + } + } + .listing-entry-expanded { @include sizes.make-gap(padding, vertical); @@ -226,12 +205,16 @@ ul.listing { } .listing-entry { - min-height: $listing-entry-height; + .listing-entry-compact { + min-height: $listing-entry-height; + } } &.mini { .listing-entry { - min-height: sizes.$subrow-height; + .listing-entry-compact { + min-height: sizes.$subrow-height; + } } } } diff --git a/src/style/components/transitions/_collapse.scss b/src/style/components/transitions/_collapse.scss index 492f8d17..81cb4b77 100644 --- a/src/style/components/transitions/_collapse.scss +++ b/src/style/components/transitions/_collapse.scss @@ -5,11 +5,12 @@ 'collapse-vertical', max-height, $from: 0, - $to: calc(15 * sizes.$subrow-height), + $to: calc(sizes.$row-height), $enter-done: false ); .transition { + &.collapse-vertical-appear, &.collapse-vertical-enter, &.collapse-vertical-exit { overflow-y: hidden; @@ -25,6 +26,7 @@ ); .transition { + &.collapse-horizontal-appear, &.collapse-horizontal-enter, &.collapse-horizontal-exit { overflow-x: hidden; From 0194cdee8d8ee1acd9db7c9563a46576fcee8a36 Mon Sep 17 00:00:00 2001 From: Neraste Date: Fri, 1 May 2026 20:40:50 +0200 Subject: [PATCH 09/25] Update ListingEntry transition --- src/components/generics/listing/Entry.jsx | 34 ++++++------------- src/components/generics/listing/List.jsx | 2 +- .../library/status/ExceedsKaraStopTime.jsx | 13 ++++--- src/components/library/status/InPlaylist.jsx | 4 ++- src/components/library/status/MaskedByTag.jsx | 13 ++++--- src/components/playlist/status/HasError.jsx | 5 +-- 6 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/components/generics/listing/Entry.jsx b/src/components/generics/listing/Entry.jsx index 2920886f..9d22f983 100644 --- a/src/components/generics/listing/Entry.jsx +++ b/src/components/generics/listing/Entry.jsx @@ -2,7 +2,7 @@ import classNames from 'classnames' import PropTypes from 'prop-types' import { useContext, useEffect } from 'react' import { useSearchParams } from 'react-router' -import { CSSTransition, TransitionGroup } from 'react-transition-group' +import { TransitionGroup } from 'react-transitioning' import Collapse from 'components/transitions/Collapse' import { ListingNoTransitionContext } from 'contexts/listing' @@ -49,26 +49,6 @@ export function ListingEntry({ setSearchParams(searchParams) } - /** - * Add transition to extra - */ - - let extraTransition - if (extra) { - extraTransition = [extra].flat().map((item, index) => ( - - {item} - - )) - } - return (
  • {children}
  • )} {!expanded && ( - - {extraTransition} - +
      + + {[extra].flat().map((item, index) => ( + + {item} + + ))} + +
    )}
    {!expanded && isDisplayable(controls) && ( diff --git a/src/components/generics/listing/List.jsx b/src/components/generics/listing/List.jsx index 5d601373..2a53175e 100644 --- a/src/components/generics/listing/List.jsx +++ b/src/components/generics/listing/List.jsx @@ -42,7 +42,7 @@ export default function ListingList({ {noTransition ? ( entries ) : ( - + {entries.map((item) => ( {item} ))} diff --git a/src/components/library/status/ExceedsKaraStopTime.jsx b/src/components/library/status/ExceedsKaraStopTime.jsx index 5a499627..27bfa481 100644 --- a/src/components/library/status/ExceedsKaraStopTime.jsx +++ b/src/components/library/status/ExceedsKaraStopTime.jsx @@ -1,7 +1,7 @@ import classNames from 'classnames' import PropTypes from 'prop-types' -export default function ExceedsKaraStopTime({ expanded }) { +export default function ExceedsKaraStopTime({ expanded, className }) { let message if (expanded) { message = ( @@ -12,9 +12,13 @@ export default function ExceedsKaraStopTime({ expanded }) { } return (
  • @@ -26,4 +30,5 @@ export default function ExceedsKaraStopTime({ expanded }) { ExceedsKaraStopTime.propTypes = { expanded: PropTypes.bool, + className: PropTypes.string, } diff --git a/src/components/library/status/InPlaylist.jsx b/src/components/library/status/InPlaylist.jsx index 73fa260e..4b875d43 100644 --- a/src/components/library/status/InPlaylist.jsx +++ b/src/components/library/status/InPlaylist.jsx @@ -64,6 +64,7 @@ export default function InPlaylist({ playingEntries, queuingEntries, expanded, + className, }) { const playerStatus = useSelector((state) => state.playlist.playerStatus.data) const { entry, position } = getMostPertinentEntry( @@ -144,7 +145,7 @@ export default function InPlaylist({ return (
  • @@ -159,4 +160,5 @@ InPlaylist.propTypes = { playingEntries: PropTypes.arrayOf(playlistEntryPropType), queuingEntries: PropTypes.arrayOf(playlistEntryPropType), expanded: PropTypes.bool, + className: PropTypes.string, } diff --git a/src/components/library/status/MaskedByTag.jsx b/src/components/library/status/MaskedByTag.jsx index 3c18d82d..158ae48d 100644 --- a/src/components/library/status/MaskedByTag.jsx +++ b/src/components/library/status/MaskedByTag.jsx @@ -1,7 +1,7 @@ import classNames from 'classnames' import PropTypes from 'prop-types' -export default function MaskedByTag({ expanded }) { +export default function MaskedByTag({ expanded, className }) { let message if (expanded) { message = ( @@ -12,9 +12,13 @@ export default function MaskedByTag({ expanded }) { } return (
  • @@ -26,4 +30,5 @@ export default function MaskedByTag({ expanded }) { MaskedByTag.propTypes = { expanded: PropTypes.bool, + className: PropTypes.string, } diff --git a/src/components/playlist/status/HasError.jsx b/src/components/playlist/status/HasError.jsx index 8218329f..925fae7c 100644 --- a/src/components/playlist/status/HasError.jsx +++ b/src/components/playlist/status/HasError.jsx @@ -5,7 +5,7 @@ import { Link } from 'react-router' import { playerErrorPropType } from 'serverPropTypes/playlist' -export default function HasError({ playerError, expanded }) { +export default function HasError({ playerError, expanded, className }) { let message if (expanded) { message = ( @@ -27,7 +27,7 @@ export default function HasError({ playerError, expanded }) { } return (
  • @@ -42,4 +42,5 @@ export default function HasError({ playerError, expanded }) { HasError.propTypes = { expanded: PropTypes.bool, playerError: playerErrorPropType.isRequired, + className: PropTypes.string, } From cf1d8430b5d4c3b39932cee7d5821ebb15e06b08 Mon Sep 17 00:00:00 2001 From: Neraste Date: Fri, 1 May 2026 20:46:15 +0200 Subject: [PATCH 10/25] Update SongTagEntry transition --- src/components/settings/songTags/Entry.jsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/components/settings/songTags/Entry.jsx b/src/components/settings/songTags/Entry.jsx index e44dceb5..0c4f8ffc 100644 --- a/src/components/settings/songTags/Entry.jsx +++ b/src/components/settings/songTags/Entry.jsx @@ -11,9 +11,9 @@ import NotificationBar, { NotifiableForTable, } from 'components/generics/NotificationBar' import { Checkmark } from 'components/generics/Shapes' +import Slide from 'components/transitions/Slide' import { Status } from 'reducers/alterationsResponse' import { songTagPropType } from 'serverPropTypes/library' -import { CSSTransitionLazy } from 'thirdpartyExtensions/ReactTransitionGroup' export default function SongTagsEntry({ tag, editable }) { const query = useSelector((state) => state.settings.songTags.data.query) @@ -124,7 +124,7 @@ export default function SongTagsEntry({ tag, editable }) { ) const colorForm = ( -
    +
    - - {colorForm} - + {colorForm} From c8434ce2ec7b8a3c50a00f2134d68aed77177c99 Mon Sep 17 00:00:00 2001 From: Neraste Date: Fri, 1 May 2026 21:10:06 +0200 Subject: [PATCH 11/25] Update ManageButton transition --- .../karaoke/player/ManageButton.jsx | 15 ++++------- src/components/transitions/Swipe.jsx | 21 ++++++++++++++++ src/style/abstracts/_transitions.scss | 16 +++++++++--- .../karaoke/player/_manage_button.scss | 25 +++---------------- src/style/components/transitions/_index.scss | 1 + src/style/components/transitions/_swipe.scss | 11 ++++++++ 6 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 src/components/transitions/Swipe.jsx create mode 100644 src/style/components/transitions/_swipe.scss diff --git a/src/components/karaoke/player/ManageButton.jsx b/src/components/karaoke/player/ManageButton.jsx index 9c088a08..adfa529d 100644 --- a/src/components/karaoke/player/ManageButton.jsx +++ b/src/components/karaoke/player/ManageButton.jsx @@ -2,11 +2,11 @@ import classNames from 'classnames' import PropTypes from 'prop-types' import { Component } from 'react' +import Swipe from 'components/transitions/Swipe' import { alterationResponsePropType, Status, } from 'reducers/alterationsResponse' -import { CSSTransitionLazy } from 'thirdpartyExtensions/ReactTransitionGroup' /** * ManageButton class for a button connected to a player manage command @@ -121,8 +121,7 @@ export default class ManageButton extends Component { } render() { - const { onClick, disabled, className, timeout, iconDisabled, error } = - this.props + const { onClick, disabled, className, iconDisabled, error } = this.props const onClickControlled = (e) => { // do not manage any other click during the transition @@ -150,15 +149,11 @@ export default class ManageButton extends Component { onClick={onClickControlled} disabled={disabled} > - -
    + +
    - +
    ) } diff --git a/src/components/transitions/Swipe.jsx b/src/components/transitions/Swipe.jsx new file mode 100644 index 00000000..e7ce2163 --- /dev/null +++ b/src/components/transitions/Swipe.jsx @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types' +import { CSSTransition } from 'react-transitioning' + +export default function Swipe({ + in: inProp, + duration = 300, + children, + ...rest +}) { + return ( + + {children} + + ) +} + +Swipe.propTypes = { + in: PropTypes.bool, + duration: PropTypes.number, + children: PropTypes.node, +} diff --git a/src/style/abstracts/_transitions.scss b/src/style/abstracts/_transitions.scss index ca87b559..27934f9a 100644 --- a/src/style/abstracts/_transitions.scss +++ b/src/style/abstracts/_transitions.scss @@ -7,8 +7,9 @@ // helper for creating transition styles // @param $name name of the transitiion // @param $property property to transition -// @param $from value of the property before entered and after exiting +// @param $from value of the property before entered, and after exiting if $away is not set // @param $to value of the property after entered and before exiting +// @param $away value of the property after exiting // @param $duration-enter timing of the entering step // @param $duration-exit timing of the exiting step // @param $enter enable enter step @@ -23,6 +24,7 @@ $property, $from, $to, + $away: false, $duration-enter: 300ms, $duration-exit: 150ms, $enter: true, @@ -61,7 +63,11 @@ @if ($exit-active) { &.#{$name}-exit-active { - #{$property}: $from; + @if ($away) { + #{$property}: $away; + } @else { + #{$property}: $from; + } transition: $property $duration-exit ease-in; } } @@ -70,7 +76,11 @@ @if ($exit-done) { &.#{$name}-exit-done { - #{$property}: $from; + @if ($away) { + #{$property}: $away; + } @else { + #{$property}: $from; + } } } } diff --git a/src/style/components/karaoke/player/_manage_button.scss b/src/style/components/karaoke/player/_manage_button.scss index 1768092f..232c64d9 100644 --- a/src/style/components/karaoke/player/_manage_button.scss +++ b/src/style/components/karaoke/player/_manage_button.scss @@ -2,8 +2,9 @@ overflow: hidden; position: relative; - &.managed-error .managed.managed-enter { - left: -100%; + // transition for error + &.managed-error .managed.swipe-enter { + left: 100%; } .managed { @@ -15,25 +16,5 @@ position: absolute; top: 0; width: 100%; - - // appearance transition - &.managed-enter { - left: 100%; - - &.managed-enter-active { - left: 0; - transition: left 150ms ease-out; - } - } - - // disappearance transition - &.managed-exit { - left: 0; - - &.managed-exit-active { - left: -100%; - transition: left 150ms ease-out; - } - } } } diff --git a/src/style/components/transitions/_index.scss b/src/style/components/transitions/_index.scss index f54e3fe3..ec63ca74 100644 --- a/src/style/components/transitions/_index.scss +++ b/src/style/components/transitions/_index.scss @@ -1,2 +1,3 @@ @use 'collapse'; @use 'slide'; +@use 'swipe'; diff --git a/src/style/components/transitions/_swipe.scss b/src/style/components/transitions/_swipe.scss new file mode 100644 index 00000000..21e64f6d --- /dev/null +++ b/src/style/components/transitions/_swipe.scss @@ -0,0 +1,11 @@ +@use '~/abstracts/transitions'; + +@include transitions.make-transition( + 'swipe', + left, + $from: -100%, + $to: 0, + $away: 100%, + $duration-enter: 150ms, + $duration-exit: 150ms +); From de1ab2317f0e2a23e7cf76a59448165f3d782a43 Mon Sep 17 00:00:00 2001 From: Neraste Date: Fri, 1 May 2026 21:11:37 +0200 Subject: [PATCH 12/25] Remove CSSTransitionLazy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit You won’t be missed. --- eslint.config.js | 2 +- .../ReactTransitionGroup.jsx | 27 ------------------- 2 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 src/thirdpartyExtensions/ReactTransitionGroup.jsx diff --git a/eslint.config.js b/eslint.config.js index fcf352e8..22a75c26 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -70,7 +70,7 @@ export default [ ['^\\u0000'], ['^@?\\w'], [ - '^(actions|components|contexts|eventManagers|middleware|permissions|reducers|serverPropTypes|style|thirdpartyExtensions|utils)', + '^(actions|components|contexts|eventManagers|middleware|permissions|reducers|serverPropTypes|style|utils)', ], ['^'], ['^\\.'], diff --git a/src/thirdpartyExtensions/ReactTransitionGroup.jsx b/src/thirdpartyExtensions/ReactTransitionGroup.jsx deleted file mode 100644 index 5df252ff..00000000 --- a/src/thirdpartyExtensions/ReactTransitionGroup.jsx +++ /dev/null @@ -1,27 +0,0 @@ -import PropTypes from 'prop-types' -import { CSSTransition } from 'react-transition-group' - -/** - * The CSSTransitionLazy component is a sanitized version of CSSTransition - * - * It is aimed to fix its dysfunctionnal behavior related in - * https://github.com/reactjs/react-transition-group/issues/156 - * The problem is that the child component is always visible, either before the - * enter transition starts or after the exit transition finishes. The solution - * is to force the mounting of the children component only when the enter - * transition is bound to start and its unmounting when the exit transition has - * finished, hence resulting in a "lazy" mounting. - * - * The problem is present in the example of the OFFICIAL DOCUMENTATION, which - * makes it plainly NOT WORKING. This demonstrates how STUPID devoloppers are in - * the FUCKING JS COMMUNITY. - */ - -export const CSSTransitionLazy = ({ children, ...props }) => ( - - {children} - -) -CSSTransitionLazy.propTypes = { - children: PropTypes.node, -} From b3d11c7d2340a3a825c6596dfe141ba92679b44f Mon Sep 17 00:00:00 2001 From: Neraste Date: Fri, 1 May 2026 21:12:17 +0200 Subject: [PATCH 13/25] Update browser database --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83f96750..072536f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2786,9 +2786,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001716", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz", - "integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==", + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", "dev": true, "funding": [ { From c071ddaaae9d58a411e759ec030e9a237c94a678 Mon Sep 17 00:00:00 2001 From: Neraste Date: Sat, 2 May 2026 23:26:59 +0200 Subject: [PATCH 14/25] Update Details transition --- src/components/generics/Details.jsx | 14 +++-------- src/style/components/generics/_details.scss | 27 ++++++++++----------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/components/generics/Details.jsx b/src/components/generics/Details.jsx index f6d9e908..2779270c 100644 --- a/src/components/generics/Details.jsx +++ b/src/components/generics/Details.jsx @@ -1,8 +1,8 @@ import classNames from 'classnames' import PropTypes from 'prop-types' import { useState } from 'react' -import { CSSTransition } from 'react-transition-group' +import Collapse from 'components/transitions/Collapse' import { isDisplayable } from 'utils' export function Details({ children }) { @@ -69,17 +69,11 @@ export function DetailLongText({ notifiable: isDisplayable(notifications), })} > - -
    + +

    {children}

    - +
    {!revealed && (
    ) diff --git a/src/style/components/generics/_form.scss b/src/style/components/generics/_form.scss index b76918a8..ee08c5cb 100644 --- a/src/style/components/generics/_form.scss +++ b/src/style/components/generics/_form.scss @@ -158,24 +158,10 @@ $form-field-height: sizes.$row-height; @include sizes.make-vertical-row-padding(horizontal); } - &.error-container-enter { - max-height: 0; - overflow-y: hidden; - - &.error-container-enter-active { + &.transitiion { + &.collapse-vertical-enter-active, + &.collapse-vertical-exit:not(.collapse-vertical-exit-active) { max-height: calc(1em + #{sizes.$gap-vertical} * 2); - transition: max-height 300ms ease-out; - } - } - - // notification transition disappearance - &.error-container-exit { - max-height: calc(1em + #{sizes.$gap-vertical} * 2); - - &.error-container-exit-active { - max-height: 0; - overflow-y: hidden; - transition: max-height 100ms ease-out; } } } From 08fdb091ca9dfc35e68f8398e6b1a3776997bf2d Mon Sep 17 00:00:00 2001 From: Neraste Date: Sat, 2 May 2026 23:42:57 +0200 Subject: [PATCH 16/25] Update PlayerNotification transition --- .../karaoke/player/Notification.jsx | 24 ++++++++----------- .../karaoke/player/_notification.scss | 19 +++------------ 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/components/karaoke/player/Notification.jsx b/src/components/karaoke/player/Notification.jsx index 44e67a57..5b2dd3e7 100644 --- a/src/components/karaoke/player/Notification.jsx +++ b/src/components/karaoke/player/Notification.jsx @@ -1,7 +1,8 @@ import PropTypes from 'prop-types' import { Component } from 'react' -import { CSSTransition, TransitionGroup } from 'react-transition-group' +import { TransitionGroup } from 'react-transitioning' +import Collapse from 'components/transitions/Collapse' import { alterationResponsePropType, Status, @@ -128,25 +129,20 @@ export default class PlayerNotification extends Component { let notification if (message) { notification = ( - -
    + +
    {message}
    - +
    ) } return ( - - {notification} - +
    + + {notification} + +
    ) } } diff --git a/src/style/components/karaoke/player/_notification.scss b/src/style/components/karaoke/player/_notification.scss index 2ebc836b..5c036133 100644 --- a/src/style/components/karaoke/player/_notification.scss +++ b/src/style/components/karaoke/player/_notification.scss @@ -8,23 +8,10 @@ .player-notification { .player-notified { - &.player-notified-enter { - max-height: 0; - overflow-y: hidden; - - &.player-notified-enter-active { + &.transition { + &.collapse-vertical-enter-active, + &.collapse-vertical-exit:not(.collapse-vertical-exit-active) { max-height: 3em; - transition: max-height 300ms ease-out; - } - } - - &.player-notified-exit { - max-height: 3em; - - &.player-notified-exit-active { - max-height: 0; - overflow-y: hidden; - transition: max-height 150ms ease-in; } } } From bac3fdee08689cf67cfac1f248972f002d4814eb Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 3 May 2026 00:12:51 +0200 Subject: [PATCH 17/25] Update PlayerTokenBox transition --- src/components/settings/Tokens.jsx | 28 +++++++++-------- src/components/transitions/Collapse.jsx | 6 ++-- src/style/components/settings/_tokens.scss | 36 ++++++++-------------- 3 files changed, 32 insertions(+), 38 deletions(-) diff --git a/src/components/settings/Tokens.jsx b/src/components/settings/Tokens.jsx index 55fb8372..26b9ed11 100644 --- a/src/components/settings/Tokens.jsx +++ b/src/components/settings/Tokens.jsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' -import { CSSTransition } from 'react-transition-group' import { createPlayerToken, @@ -11,6 +10,7 @@ import { revokeToken } from 'actions/token' import ConfirmationBar from 'components/generics/ConfirmationBar' import NotificationBar from 'components/generics/NotificationBar' import TokenWidget from 'components/generics/TokenWidget' +import Collapse from 'components/transitions/Collapse' import { IsLibraryManager } from 'permissions/components/Library' import { IsPlaylistManager } from 'permissions/components/Playlist' import { Status } from 'reducers/alterationsResponse' @@ -128,20 +128,22 @@ function PlayerTokenBox() { let playerTokenBox if (playerTokenStatus === Status.successful) { playerTokenBox = ( - - {keyExists ? ( - - ) : ( - - )} - +
    + {keyExists ? ( + + ) : ( + + )} +
    + ) } else if (playerTokenStatus === Status.failed) { playerTokenBox = ( diff --git a/src/components/transitions/Collapse.jsx b/src/components/transitions/Collapse.jsx index ee73acbc..766e19c7 100644 --- a/src/components/transitions/Collapse.jsx +++ b/src/components/transitions/Collapse.jsx @@ -3,6 +3,7 @@ import { CSSTransition } from 'react-transitioning' export default function Collapse({ in: inProp, + classNames = 'collapse', duration = 300, horizontal = false, children, @@ -12,7 +13,7 @@ export default function Collapse({ return ( @@ -24,7 +25,7 @@ export default function Collapse({ return ( @@ -35,6 +36,7 @@ export default function Collapse({ Collapse.propTypes = { in: PropTypes.bool, + classNames: PropTypes.string, duration: PropTypes.number, horizontal: PropTypes.bool, children: PropTypes.node, diff --git a/src/style/components/settings/_tokens.scss b/src/style/components/settings/_tokens.scss index 6e60cac6..3aa51391 100644 --- a/src/style/components/settings/_tokens.scss +++ b/src/style/components/settings/_tokens.scss @@ -6,34 +6,24 @@ @use '~/abstracts/controls'; @use '~/abstracts/sizes'; +@use '~/abstracts/transitions'; #tokens { .token-box { - // approximate min and max height - $token-player-min-height: calc( - 1.25em + sizes.$row-height + sizes.$gap-vertical + @include transitions.make-transition( + 'collapse-height-vertical', + height, + $from: calc(1.4em + sizes.$row-height + sizes.$gap-vertical), + $to: calc(0.15em + 3 * sizes.$row-height + 2 * sizes.$gap-vertical), + $enter-done: false, + $exit-done: false ); - $token-player-max-height: calc( - 3 * sizes.$row-height + 2 * sizes.$gap-vertical - ); - - .token-player-enter { - height: $token-player-min-height; - overflow-y: hidden; - - &.token-player-enter-active { - height: $token-player-max-height; - transition: height 300ms ease-out; - } - } - - .token-player-exit { - height: $token-player-max-height; - overflow-y: hidden; - &.token-player-exit-active { - height: $token-player-min-height; - transition: height 150ms ease-out; + .transition { + &.collapse-height-vertical-appear, + &.collapse-height-vertical-enter, + &.collapse-height-vertical-exit { + overflow-y: hidden; } } } From 925085d012651e11872877c3f1147f54f8d12d1b Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 3 May 2026 00:16:27 +0200 Subject: [PATCH 18/25] Remove leftover transition styles --- .../components/generics/listing/_listing.scss | 22 ------------------- .../karaoke/player/_manage_button.scss | 2 +- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/src/style/components/generics/listing/_listing.scss b/src/style/components/generics/listing/_listing.scss index 59cb1dfe..fe0ab1a5 100644 --- a/src/style/components/generics/listing/_listing.scss +++ b/src/style/components/generics/listing/_listing.scss @@ -140,28 +140,6 @@ $listing-entry-control-icon-size: sizes.$row-icon-font-size; position: absolute; right: 0; } - - // appearance transition - .add-remove-enter { - max-width: 0; - overflow: hidden; - - &.add-remove-enter-active { - max-width: $listing-entry-height; - transition: max-width 300ms ease-out; - } - } - - // disappearance transition - .add-remove-exit { - max-width: $listing-entry-height; - overflow: hidden; - - &.add-remove-exit-active { - max-width: 0; - transition: max-width 150ms ease-in; - } - } } } diff --git a/src/style/components/karaoke/player/_manage_button.scss b/src/style/components/karaoke/player/_manage_button.scss index 232c64d9..6067cf60 100644 --- a/src/style/components/karaoke/player/_manage_button.scss +++ b/src/style/components/karaoke/player/_manage_button.scss @@ -3,7 +3,7 @@ position: relative; // transition for error - &.managed-error .managed.swipe-enter { + &.managed-error .transition.swipe-enter { left: 100%; } From 1cdec5bac347f0aa65ffb98ea1af458287336001 Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 3 May 2026 00:17:51 +0200 Subject: [PATCH 19/25] Remove dependency --- package-lock.json | 42 ------------------------------------------ package.json | 1 - 2 files changed, 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index 072536f4..4fc52f3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,6 @@ "react-highlight-words": "^0.21.0", "react-redux": "^9.2.0", "react-router": "^7.12.0", - "react-transition-group": "^4.4.5", "react-transitioning": "^1.0.9", "redux": "^5.0.1", "redux-localstorage": "^0.4.1", @@ -99,15 +98,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@csstools/css-parser-algorithms": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", @@ -3027,12 +3017,6 @@ "node": ">=4" } }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -3213,16 +3197,6 @@ "node": ">=0.10.0" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -6295,22 +6269,6 @@ } } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/react-transitioning": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/react-transitioning/-/react-transitioning-1.0.9.tgz", diff --git a/package.json b/package.json index a59dae0f..a6b01ae6 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "react-highlight-words": "^0.21.0", "react-redux": "^9.2.0", "react-router": "^7.12.0", - "react-transition-group": "^4.4.5", "react-transitioning": "^1.0.9", "redux": "^5.0.1", "redux-localstorage": "^0.4.1", From b6b728a077b1216589e27a341ef0d947f59f6564 Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 3 May 2026 01:20:26 +0200 Subject: [PATCH 20/25] Fix shapes being above confirmation bar --- src/style/abstracts/_shapes.scss | 1 + src/style/components/generics/_notification_bar.scss | 1 + 2 files changed, 2 insertions(+) diff --git a/src/style/abstracts/_shapes.scss b/src/style/abstracts/_shapes.scss index 3f71fdc4..1532b488 100644 --- a/src/style/abstracts/_shapes.scss +++ b/src/style/abstracts/_shapes.scss @@ -10,6 +10,7 @@ @include preboot.square($width); transform: translateX(-$width * 0.5) translateY(-$width * 0.5); + z-index: 100; } @mixin make-checkmark($background) { diff --git a/src/style/components/generics/_notification_bar.scss b/src/style/components/generics/_notification_bar.scss index b88edfdd..2b41f628 100644 --- a/src/style/components/generics/_notification_bar.scss +++ b/src/style/components/generics/_notification_bar.scss @@ -36,6 +36,7 @@ position: absolute; top: 0; width: 100%; + z-index: 1000; .notification { display: flex; From 08abba1885231d8c667a6155696c9afc9185ef66 Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 3 May 2026 14:03:18 +0200 Subject: [PATCH 21/25] Improve Collapse transition --- src/components/settings/Tokens.jsx | 6 +--- src/components/transitions/Collapse.jsx | 19 ++---------- src/style/components/settings/_tokens.scss | 31 ++++++++++--------- .../components/transitions/_collapse.scss | 13 +++++++- 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/components/settings/Tokens.jsx b/src/components/settings/Tokens.jsx index 26b9ed11..ea86f284 100644 --- a/src/components/settings/Tokens.jsx +++ b/src/components/settings/Tokens.jsx @@ -128,11 +128,7 @@ function PlayerTokenBox() { let playerTokenBox if (playerTokenStatus === Status.successful) { playerTokenBox = ( - +
    {keyExists ? ( - {children} - - ) - } - return ( @@ -36,8 +23,8 @@ export default function Collapse({ Collapse.propTypes = { in: PropTypes.bool, - classNames: PropTypes.string, duration: PropTypes.number, horizontal: PropTypes.bool, + force: PropTypes.bool, children: PropTypes.node, } diff --git a/src/style/components/settings/_tokens.scss b/src/style/components/settings/_tokens.scss index 3aa51391..569e8bfb 100644 --- a/src/style/components/settings/_tokens.scss +++ b/src/style/components/settings/_tokens.scss @@ -6,24 +6,27 @@ @use '~/abstracts/controls'; @use '~/abstracts/sizes'; -@use '~/abstracts/transitions'; #tokens { .token-box { - @include transitions.make-transition( - 'collapse-height-vertical', - height, - $from: calc(1.4em + sizes.$row-height + sizes.$gap-vertical), - $to: calc(0.15em + 3 * sizes.$row-height + 2 * sizes.$gap-vertical), - $enter-done: false, - $exit-done: false - ); - .transition { - &.collapse-height-vertical-appear, - &.collapse-height-vertical-enter, - &.collapse-height-vertical-exit { - overflow-y: hidden; + // stylelint-disable @stylistic/indentation + &.collapse-force-vertical-enter:not( + .collapse-force-vertical-enter-active + ), + // stylelint-enable @stylistic/indentation + &.collapse-force-vertical-exit-active { + height: calc(1.4em + sizes.$row-height + sizes.$gap-vertical); + } + + &.collapse-force-vertical-enter-done, + &.collapse-force-vertical-exit-done { + height: unset; + } + + &.collapse-force-vertical-enter-active, + &.collapse-force-vertical-exit:not(.collapse-force-vertical-exit-active) { + height: calc(0.15em + 3 * sizes.$row-height + 2 * sizes.$gap-vertical); } } } diff --git a/src/style/components/transitions/_collapse.scss b/src/style/components/transitions/_collapse.scss index 81cb4b77..21e6ee5a 100644 --- a/src/style/components/transitions/_collapse.scss +++ b/src/style/components/transitions/_collapse.scss @@ -9,10 +9,21 @@ $enter-done: false ); +@include transitions.make-transition( + 'collapse-force-vertical', + height, + $from: 0, + $to: calc(sizes.$row-height), + $enter-done: false +); + .transition { &.collapse-vertical-appear, &.collapse-vertical-enter, - &.collapse-vertical-exit { + &.collapse-vertical-exit, + &.collapse-force-vertical-appear, + &.collapse-force-vertical-enter, + &.collapse-force-vertical-exit { overflow-y: hidden; } } From b5f6ad770c30c46543a886025a9e2c9c92479a35 Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 3 May 2026 14:06:06 +0200 Subject: [PATCH 22/25] Fix users delayed refresh --- src/actions/users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/actions/users.js b/src/actions/users.js index ba6e5a9c..08b123cc 100644 --- a/src/actions/users.js +++ b/src/actions/users.js @@ -28,7 +28,7 @@ export const USER_LIST_FAILURE = 'USER_LIST_FAILURE' */ const refreshUsersDelayed = (dispatch, getState) => { const page = getState().settings.users.list.data.pagination.current - return dispatch(delay(loadUsers({ page }), 3000)) + return dispatch(delay(loadUsers(page), 3000)) } /** From fd0cc2f34896254673782349eaf0a0ee6051c2fd Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 3 May 2026 17:43:16 +0200 Subject: [PATCH 23/25] Fix listing collapse when expanded --- .../components/generics/listing/_listing.scss | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/style/components/generics/listing/_listing.scss b/src/style/components/generics/listing/_listing.scss index fe0ab1a5..16aea4f6 100644 --- a/src/style/components/generics/listing/_listing.scss +++ b/src/style/components/generics/listing/_listing.scss @@ -145,8 +145,16 @@ $listing-entry-control-icon-size: sizes.$row-icon-font-size; // transition for the entry &.transition { - &.collapse-vertical-exit:not(.collapse-vertical-exit-active) { - max-height: 10 * sizes.$subrow-height; + &.expanded { + &.collapse-vertical-exit:not(.collapse-vertical-exit-active) { + max-height: 10 * sizes.$subrow-height; + } + } + + &.collapse-vertical-appear, + &.collapse-vertical-enter, + &.collapse-vertical-exit { + min-height: unset; } } @@ -183,16 +191,12 @@ ul.listing { } .listing-entry { - .listing-entry-compact { - min-height: $listing-entry-height; - } + min-height: $listing-entry-height; } &.mini { .listing-entry { - .listing-entry-compact { - min-height: sizes.$subrow-height; - } + min-height: sizes.$subrow-height; } } } From a91b414803aa5984b77355c0a500e9a9fa9d5671 Mon Sep 17 00:00:00 2001 From: Neraste Date: Sun, 3 May 2026 18:18:02 +0200 Subject: [PATCH 24/25] Simplify transition options --- src/components/generics/Details.jsx | 2 +- src/components/generics/Form.jsx | 2 +- src/components/generics/listing/Entry.jsx | 2 +- src/components/karaoke/player/Notification.jsx | 2 +- src/components/settings/Tokens.jsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/generics/Details.jsx b/src/components/generics/Details.jsx index 2779270c..c9fb79dd 100644 --- a/src/components/generics/Details.jsx +++ b/src/components/generics/Details.jsx @@ -69,7 +69,7 @@ export function DetailLongText({ notifiable: isDisplayable(notifications), })} > - +

    {children}

    diff --git a/src/components/generics/Form.jsx b/src/components/generics/Form.jsx index 4ac2b5e0..2906b222 100644 --- a/src/components/generics/Form.jsx +++ b/src/components/generics/Form.jsx @@ -607,7 +607,7 @@ class Field extends Component {
    {this.subRender(props)} - + {fieldErrorMessages}
    diff --git a/src/components/generics/listing/Entry.jsx b/src/components/generics/listing/Entry.jsx index 9d22f983..afafda59 100644 --- a/src/components/generics/listing/Entry.jsx +++ b/src/components/generics/listing/Entry.jsx @@ -72,7 +72,7 @@ export function ListingEntry({ )} {!expanded && (
      - + {[extra].flat().map((item, index) => ( {item} diff --git a/src/components/karaoke/player/Notification.jsx b/src/components/karaoke/player/Notification.jsx index 5b2dd3e7..bbeeb0c4 100644 --- a/src/components/karaoke/player/Notification.jsx +++ b/src/components/karaoke/player/Notification.jsx @@ -139,7 +139,7 @@ export default class PlayerNotification extends Component { return (
      - + {notification}
      diff --git a/src/components/settings/Tokens.jsx b/src/components/settings/Tokens.jsx index ea86f284..470ee888 100644 --- a/src/components/settings/Tokens.jsx +++ b/src/components/settings/Tokens.jsx @@ -128,7 +128,7 @@ function PlayerTokenBox() { let playerTokenBox if (playerTokenStatus === Status.successful) { playerTokenBox = ( - +
      {keyExists ? ( Date: Sun, 3 May 2026 18:48:03 +0200 Subject: [PATCH 25/25] Update comments --- src/style/abstracts/_transitions.scss | 1 - src/style/components/library/_song.scss | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/style/abstracts/_transitions.scss b/src/style/abstracts/_transitions.scss index 27934f9a..ac193bd5 100644 --- a/src/style/abstracts/_transitions.scss +++ b/src/style/abstracts/_transitions.scss @@ -18,7 +18,6 @@ // @param $exit enable exit step // @param $exit-active enable exitActive step // @param $exit-done enable exitDone step -// step @mixin make-transition( $name, $property, diff --git a/src/style/components/library/_song.scss b/src/style/components/library/_song.scss index e19e9897..e0e920a4 100644 --- a/src/style/components/library/_song.scss +++ b/src/style/components/library/_song.scss @@ -10,7 +10,7 @@ // `listing` module. It is aimed to stylize the specific elements of a list of // songs. // -// The song can be represented in a compact, listing-entry-compact view or in an expanded, +// The song can be represented in a compact, one-line view or in an expanded, // multi-lines view. More elements are present in the expanded view, which takes // usually more space and have larger margins compared to the compact view. The // compact view stays even if the expandes view is displayed, it only contains