diff --git a/frontend/src/core/components/StyledDialog.tsx b/frontend/src/core/components/StyledDialog.tsx index 1262c01..7fc2da8 100644 --- a/frontend/src/core/components/StyledDialog.tsx +++ b/frontend/src/core/components/StyledDialog.tsx @@ -8,6 +8,7 @@ interface StyledDialogProps { children: ReactNode; onBackdropClick?: () => void; className?: string; + afterChildren?: ReactNode; } export function StyledDialog({ @@ -15,7 +16,8 @@ export function StyledDialog({ onOpenChange, children, onBackdropClick, - className = "" + className = "", + afterChildren }: StyledDialogProps) { const transition: Transition = { duration: 0.3, type: "tween", ease: "easeInOut" }; @@ -60,6 +62,7 @@ export function StyledDialog({
{children}
+ {afterChildren} )} diff --git a/frontend/src/core/components/css/_styled-dialog.scss b/frontend/src/core/components/css/_styled-dialog.scss index 50386eb..1a73f1a 100644 --- a/frontend/src/core/components/css/_styled-dialog.scss +++ b/frontend/src/core/components/css/_styled-dialog.scss @@ -2,6 +2,9 @@ @use "../../../css/material" as *; @use "sass:color"; +// Dialog padding variable +$dialog-padding: 30px; + // Base Styled Dialog Styles .styled-dialog-backdrop { position: fixed; @@ -15,14 +18,15 @@ display: flex; align-items: center; justify-content: center; - padding: 30px; + padding: $dialog-padding; box-sizing: border-box; .styled-dialog { width: 100%; max-width: 500px; - max-height: calc(100vh - 60px); - background: $color-dark-surface-container; + max-height: calc(100vh - $dialog-padding * 2); + background: rgba($color-dark-surface-container, 0.75); + border: 1px solid rgba($color-dark-outline-variant, 0.3); border-radius: 16px; box-shadow: 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12), @@ -30,8 +34,7 @@ overflow: hidden; display: flex; flex-direction: column; - // Framer Motion handles all animations - // Removed CSS transitions to prevent interference + position: relative; .styled-dialog-content { flex: 1; diff --git a/frontend/src/pages/chat/css/_animations.scss b/frontend/src/pages/chat/css/_animations.scss index d837774..23ff0a3 100644 --- a/frontend/src/pages/chat/css/_animations.scss +++ b/frontend/src/pages/chat/css/_animations.scss @@ -105,3 +105,80 @@ opacity: 1; } } + +@keyframes emojiMenuEnterDown { + from { + opacity: 0; + transform: scaleY(0); + transform-origin: top; + } + + to { + opacity: 1; + transform: scaleY(1); + } +} + +@keyframes emojiMenuEnterUp { + from { + opacity: 0; + transform: scaleY(0); + transform-origin: bottom; + } + + to { + opacity: 1; + transform: scaleY(1); + } +} + +// Reaction bar animations +@keyframes reactionBarEnter { + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes reactionBarEnterLeft { + to { + opacity: 1; + transform: translateX(0) translateY(0) scale(1); + } +} + +@keyframes reactionBarEnterUp { + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes reactionBarEnterRight { + to { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +@keyframes reactionBarClose { + to { + opacity: 0; + transform: translateY(-20px) scale(0.9); + } +} + +@keyframes reactionBarCloseLeft { + to { + opacity: 0; + transform: translateX(-20px) translateY(-20px) scale(0.9); + } +} + +@keyframes reactionBarCloseUp { + to { + opacity: 0; + transform: translateY(20px) scale(0.9); + } +} + diff --git a/frontend/src/pages/chat/css/_chat-input.scss b/frontend/src/pages/chat/css/_chat-input.scss index 717ca4f..9be72b7 100644 --- a/frontend/src/pages/chat/css/_chat-input.scss +++ b/frontend/src/pages/chat/css/_chat-input.scss @@ -5,14 +5,18 @@ .chat-input-wrapper { position: relative; margin: 0 10px 10px 10px; + position: sticky; + bottom: 10px; + z-index: 1; .input-group { display: flex; - background: $color-dark-surface-container; + background: rgba($color-dark-surface-container, 0.7); border-radius: 30px; flex-direction: column; border: 1px solid rgba($color-dark-outline-variant, 0.4); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(20px); .contextual-preview { padding: 12px 16px 0 16px; @@ -143,83 +147,71 @@ } .emoji-menu-header { - background: $color-dark-surface-container-high; - border-bottom: 1px solid rgba($color-dark-outline-variant, 0.2); position: sticky; top: 0; z-index: 1; .emoji-category-tabs { + $scrollbar-height: 2px; + display: flex; gap: 4px; overflow-x: auto; overflow-y: hidden; scroll-behavior: smooth; padding: 8px; + padding-bottom: 8px - $scrollbar-height; &::-webkit-scrollbar { - height: 4px; + height: $scrollbar-height; // slightly taller to accommodate inner padding } &::-webkit-scrollbar-track { background: transparent; + margin: 0 6px; // add space at both ends (left/right) } &::-webkit-scrollbar-thumb { - background-color: $color-dark-surface-container; - border-radius: 2px; - } - - &::-webkit-scrollbar-thumb:hover { background-color: $color-dark-surface-container-high; + border-radius: 2px; + border: 0 solid transparent; // remove vertical padding + border-left: 2px solid transparent; // keep horizontal padding (left/right) + border-right: 2px solid transparent; + background-clip: padding-box; // keep color inside the border } .emoji-category-tab { - background: transparent; + background-color: transparent; border: none; border-radius: 10px; - padding: 8px; + padding: 16px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - font-size: 1.2rem; - min-width: 40px; - height: 40px; + font-size: 16px; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; + aspect-ratio: 1 / 1; + transform: scale(1); &:hover { - background: $color-dark-surface-container; + background-color: $color-dark-surface-container-high; + } + + &:active { + transform: scale(0.8); } &.active { - background: $color-dark-primary-container; - color: $color-dark-on-primary-container; - transform: scale(1.05); - } - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: $color-dark-primary-container; - opacity: 0; - transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); - border-radius: 8px; - } - - &.active::before { - opacity: 1; + background-color: $color-dark-primary-container; } span { position: relative; z-index: 1; + display: flex; } } } @@ -248,17 +240,17 @@ .emoji-category-section { .emoji-category-title { position: sticky; - top: 0; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 12px; - padding-right: 12px; + top: 5px; + padding: 5px 12px; font-size: 0.85rem; font-weight: 600; color: $color-dark-on-surface-variant; z-index: 2; margin: 0; backdrop-filter: blur(10px); + border-radius: 10px; + margin: 5px; + background-color: rgba($color-dark-surface-container-high, 0.7); } .emoji-category-grid { diff --git a/frontend/src/pages/chat/css/_context-menu.scss b/frontend/src/pages/chat/css/_context-menu.scss index 1648c43..e3380e0 100644 --- a/frontend/src/pages/chat/css/_context-menu.scss +++ b/frontend/src/pages/chat/css/_context-menu.scss @@ -2,95 +2,184 @@ @use "../../../css/material" as *; @use "sass:color"; -// Reaction bar styles (standalone) -.reaction-bar { - background: $color-dark-surface-container; - border: 1px solid $color-dark-outline; - border-radius: 24px; - padding: 8px; - opacity: 1; - transition: all 0.15s ease; - backdrop-filter: blur(8px); - transform: translateY(0); - - &.closing { - opacity: 0; - transform: scale(0.8); - } +// Glassmorphism background mixin +%background { + background: rgba($color-dark-surface-container, 0.7); + backdrop-filter: blur(20px); + border: 1px solid rgba($color-dark-outline-variant, 0.3); } -// Emoji menu wrapper inside reaction bar -.emoji-menu-wrapper { - width: 320px; - height: 400px; +// Context menu reaction bar - main container +.context-menu-reaction-bar { + position: fixed; display: flex; align-items: center; - justify-content: center; -} - -// Context menu wrapper with animations -.context-menu-wrapper { - position: relative; - display: block; + gap: 4px; + padding: 8px 12px; + @extend %background; + border-radius: 16px; + z-index: 1001; + transition: width 0.3s ease-out, height 0.3s ease-out; + user-select: none; // Animation states &.entering { opacity: 0; - transform: scale(0.8); - animation: contextMenuEnter 0.2s ease forwards; + transform: translateY(20px) scale(0.9); + animation: reactionBarEnter 0.2s ease forwards; } &.entering-left { opacity: 0; - transform: translateX(-20px) scale(0.8); - animation: contextMenuEnterLeft 0.2s ease forwards; + transform: translateX(-20px) translateY(20px) scale(0.9); + animation: reactionBarEnterLeft 0.2s ease forwards; } &.entering-up { opacity: 0; - transform: translateY(20px) scale(0.8); - animation: contextMenuEnterUp 0.2s ease forwards; + transform: translateY(-20px) scale(0.9); + animation: reactionBarEnterUp 0.2s ease forwards; } - &.entering-up-left { + &.entering-right { opacity: 0; - transform: translateX(-20px) translateY(20px) scale(0.8); - animation: contextMenuEnterUpLeft 0.2s ease forwards; + transform: translateX(20px) scale(0.9); + animation: reactionBarEnterRight 0.2s ease forwards; } &.closing { opacity: 1; - transform: scale(1); - animation: contextMenuClose 0.2s ease forwards; + transform: translateY(0) scale(1); + animation: reactionBarClose 0.2s ease forwards; } &.closing-left { opacity: 1; - transform: translateX(0) scale(1); - animation: contextMenuCloseLeft 0.2s ease forwards; + transform: translateX(0) translateY(0) scale(1); + animation: reactionBarCloseLeft 0.2s ease forwards; } &.closing-up { opacity: 1; transform: translateY(0) scale(1); - animation: contextMenuCloseUp 0.2s ease forwards; + animation: reactionBarCloseUp 0.2s ease forwards; } - &.closing-up-left { - opacity: 1; - transform: translateX(0) translateY(0) scale(1); - animation: contextMenuCloseUpLeft 0.2s ease forwards; + + // Expanded state - contains emoji menu + &.expanded { + padding: 0; + overflow: hidden; + width: 320px; + height: 400px; + border-radius: 16px; + + // Emoji menu wrapper inside expanded reaction bar + .emoji-menu-wrapper { + width: 320px; + height: 400px; + display: flex; + align-items: center; + justify-content: center; + animation: emojiMenuEnterDown 0.3s ease; + } + + // Upward expansion animation + &.expand-upward .emoji-menu-wrapper { + animation: emojiMenuEnterUp 0.3s ease !important; + } + } + + // Reaction bar content - contains emoji buttons + .reaction-bar-content { + display: flex; + align-items: center; + gap: 4px; + transition: opacity 0.3s ease-out; + + &.faded { + opacity: 0; + } + } + + // Individual emoji buttons + .reaction-emoji-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 16px; + background: transparent; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + font-size: 18px; + + &:hover { + background: var(--mdui-color-surface-container-high); + transform: scale(1.3); + box-shadow: var(--mdui-elevation-1); + } + + &:active { + transform: scale(0.95); + transition: transform 0.1s ease; + } + } + + // Expand button for emoji menu + .reaction-expand-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid var(--mdui-color-outline); + border-radius: 16px; + background: var(--mdui-color-surface); + cursor: pointer; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + + &:hover { + background: var(--mdui-color-surface-container-high); + border-color: var(--mdui-color-primary); + transform: scale(1.1); + box-shadow: var(--mdui-elevation-1); + } + + &:active { + transform: scale(0.95); + transition: transform 0.1s ease; + } + + .material-symbols { + font-size: 18px; + color: var(--mdui-color-on-surface); + transition: transform 0.2s ease; + } + + &:hover .material-symbols { + transform: rotate(90deg); + } } } +// Context menu - separate element .context-menu { - background: $color-dark-surface; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - padding: 0.5rem 0; + position: fixed; + @extend %background; + border-radius: 16px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2), 0 8px 32px rgba(0, 0, 0, 0.15); + padding: 8px; min-width: 160px; z-index: 1000; + user-select: none; + gap: 4px; + display: flex; + flex-direction: column; + // Animation states &.entering { animation: fadeInDown 0.2s ease forwards; } @@ -123,148 +212,32 @@ animation: fadeOutDownRight 0.2s ease forwards; } + &.faded { + opacity: 0; + } + + // Context menu items .context-menu-item { display: flex; align-items: center; gap: 0.75rem; - padding: 0.75rem 1rem; + padding: 4px 6px; cursor: pointer; color: $color-dark-on-surface; font-size: 0.9rem; - transition: background-color 0.2s ease; + transition: background-color 0.25s ease, transform 0.25s ease; + border-radius: 8px; &:hover { - background-color: $color-dark-surface-container; + background-color: rgba($color-dark-surface-container, 0.5); + } + + &:active { + transform: scale(0.95); } .material-symbols { - font-size: 1.1rem; - color: $color-dark-on-surface-variant; + font-size: 18px; } } } - -// Reaction bar inside context menu wrapper -.context-menu-reaction-bar { - display: flex; - align-items: center; - gap: 4px; - padding: 8px 12px; - background: $color-dark-surface-container; - border: 1px solid $color-dark-outline; - border-radius: 16px; - position: absolute; - bottom: 100%; - justify-content: center; - margin-bottom: 10px; - transition: width 0.3s ease-out, height 0.3s ease-out; - - &.left { - left: 0; - transform: translateX(0); - } - - &.right { - right: 0; - transform: translateX(0); - } - - &.expanded { - padding: 0; - overflow: hidden; - width: 320px; - height: 400px; - border-radius: 16px; - - // Default: expand downward from the reaction bar's bottom edge - position: absolute; - bottom: auto; - top: 0; - left: 0; - transform: translateY(0); - - &.expand-upward { - // Expand upward from the reaction bar's top edge - bottom: 100%; - top: auto; - margin-bottom: 10px; - margin-top: 0; - transform: translateY(0); - } - - .emoji-menu-wrapper { - animation: emojiMenuEnter 0.5s ease; - } - } -} - -.reaction-bar-content { - display: flex; - align-items: center; - gap: 4px; - transition: opacity 0.3s ease-out; - - &.faded { - opacity: 0; - } -} - -.reaction-emoji-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 16px; - background: transparent; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); - font-size: 18px; - - &:hover { - background: var(--mdui-color-surface-container-high); - transform: scale(1.3); - box-shadow: var(--mdui-elevation-1); - } - - &:active { - transform: scale(0.95); - transition: transform 0.1s ease; - } -} - -.reaction-expand-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 1px solid var(--mdui-color-outline); - border-radius: 16px; - background: var(--mdui-color-surface); - cursor: pointer; - transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); - - &:hover { - background: var(--mdui-color-surface-container-high); - border-color: var(--mdui-color-primary); - transform: scale(1.1); - box-shadow: var(--mdui-elevation-1); - } - - &:active { - transform: scale(0.95); - transition: transform 0.1s ease; - } - - .material-symbols { - font-size: 18px; - color: var(--mdui-color-on-surface); - transition: transform 0.2s ease; - } - - &:hover .material-symbols { - transform: rotate(90deg); - } -} diff --git a/frontend/src/pages/chat/css/_layout.scss b/frontend/src/pages/chat/css/_layout.scss index 561eb7f..2d4f023 100644 --- a/frontend/src/pages/chat/css/_layout.scss +++ b/frontend/src/pages/chat/css/_layout.scss @@ -34,7 +34,7 @@ flex-direction: column; height: 100%; position: relative; - overflow: hidden; + overflow-y: auto; } } } diff --git a/frontend/src/pages/chat/css/_profile-dialog.scss b/frontend/src/pages/chat/css/_profile-dialog.scss index 0341f24..3118de4 100644 --- a/frontend/src/pages/chat/css/_profile-dialog.scss +++ b/frontend/src/pages/chat/css/_profile-dialog.scss @@ -2,173 +2,170 @@ @use "../../../css/material" as *; @use "sass:color"; -// Profile Dialog Specific Styles -// Base dialog styles are now in _styled-dialog.scss - -.styled-dialog-content { - align-items: center; - - .error-message { - color: $color-dark-error; - font-size: small; - } - - .profile-picture-section { - position: relative; - display: flex; - justify-content: center; +.profile-dialog { + .styled-dialog-content { align-items: center; - margin: 16px; - .profile-picture { - width: 120px; - height: 120px; - border-radius: 60px; - object-fit: cover; - border: 3px solid $color-dark-outline; + .error-message { + color: $color-dark-error; + font-size: small; } - .profile-picture-edit-overlay { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 60px; - background: rgba(0, 0, 0, 0.6); + .profile-picture-section { + position: relative; + display: flex; + justify-content: center; + align-items: center; + margin: 16px; + + .profile-picture { + width: 120px; + height: 120px; + border-radius: 60px; + object-fit: cover; + border: 3px solid $color-dark-outline; + } + + .profile-picture-edit-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 60px; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s ease; + cursor: pointer; + + &:hover { + opacity: 1; + } + } + } + + .username-section { + text-align: center; + + .username-with-badge { + gap: 0; + + .username-input { + background: none; + border: none; + font-size: 1.5rem; + font-weight: 500; + color: $color-dark-on-surface; + text-align: center; + outline: none; + padding: 8px; + border-radius: 4px; + transition: background-color 0.2s ease; + cursor: text; + } + } + } + + .online-status-section { display: flex; align-items: center; justify-content: center; - opacity: 0; - transition: opacity 0.2s ease; - cursor: pointer; + gap: 8px; - &:hover { - opacity: 1; + .online-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: $color-dark-primary; + + &.offline { + background: $color-dark-on-surface-variant; + } } - } - } - .username-section { - text-align: center; - - .username-with-badge { - gap: 0; - - .username-input { - background: none; - border: none; - font-size: 1.5rem; - font-weight: 500; + .status-text { + font-size: 0.875rem; color: $color-dark-on-surface; - text-align: center; - outline: none; - padding: 8px; - border-radius: 4px; - transition: background-color 0.2s ease; - cursor: text; - } - } - } - - .online-status-section { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - - .online-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - background: $color-dark-primary; - - &.offline { - background: $color-dark-on-surface-variant; } } - .status-text { - font-size: 0.875rem; - color: $color-dark-on-surface; - } - } - - .profile-sections { - margin: 16px; - display: flex; - flex-direction: column; - gap: 4px; - width: calc(100% - (16px * 2)); - box-sizing: border-box; - - .section { - $edge-radius: 24px; - - background: $color-dark-surface-container-high; - border-radius: 10px; - padding: 8px 16px; + .profile-sections { + margin: 16px; display: flex; - flex-direction: row; - gap: 16px; - align-items: center; - transition: outline 0.1s ease; - outline: 0px solid transparent; - outline-offset: -1px; + flex-direction: column; + gap: 4px; + width: calc(100% - (16px * 2)); + box-sizing: border-box; - .content-container { + .section { + $edge-radius: 24px; + + background: $color-dark-surface-container-high; + border-radius: 10px; + padding: 8px 16px; display: flex; - flex-direction: column; - gap: 4px; - width: 100%; + flex-direction: row; + gap: 16px; + align-items: center; + transition: outline 0.1s ease; + outline: 0px solid transparent; + outline-offset: -1px; - .label { - font-size: small; - color: $color-dark-on-surface-variant; - user-select: none; + .content-container { + display: flex; + flex-direction: column; + gap: 4px; + width: 100%; + + .label { + font-size: small; + color: $color-dark-on-surface-variant; + user-select: none; + } + + .value { + color: $color-dark-on-surface; + font-size: medium; + width: 100%; + line-height: 1.4; + font-family: inherit; + cursor: text; + outline: none; + background: transparent; + border: none; + caret-color: $color-dark-primary; + + &::placeholder { + color: $color-dark-on-surface-variant; + } + } } - .value { - color: $color-dark-on-surface; - font-size: medium; - width: 100%; - line-height: 1.4; - font-family: inherit; - cursor: text; - outline: none; - background: transparent; - border: none; - caret-color: $color-dark-primary; + // First and last section + &:first-child { + border-top-left-radius: $edge-radius; + border-top-right-radius: $edge-radius; + } - &::placeholder { - color: $color-dark-on-surface-variant; + &:last-child { + border-bottom-left-radius: $edge-radius; + border-bottom-right-radius: $edge-radius; + } + + &.error { + outline: 1px solid $color-dark-error; + + .error-message { + margin-top: 4px; } } } - - // First and last section - &:first-child { - border-top-left-radius: $edge-radius; - border-top-right-radius: $edge-radius; - } - - &:last-child { - border-bottom-left-radius: $edge-radius; - border-bottom-right-radius: $edge-radius; - } - - &.error { - outline: 1px solid $color-dark-error; - - .error-message { - margin-top: 4px; - } - } } } -} -.styled-dialog { .profile-dialog-fab { position: absolute; bottom: 24px; diff --git a/frontend/src/pages/chat/css/_right-panel.scss b/frontend/src/pages/chat/css/_right-panel.scss index b7fa2a3..dadbe0f 100644 --- a/frontend/src/pages/chat/css/_right-panel.scss +++ b/frontend/src/pages/chat/css/_right-panel.scss @@ -11,7 +11,9 @@ align-items: center; box-shadow: 0 4px 20px rgba($color-dark-primary, 0.1); position: relative; - z-index: 1; + z-index: 5; + position: sticky; + top: 0; .chat-header-avatar { width: 45px; @@ -65,7 +67,6 @@ .chat-messages { flex: 1; padding: 10px 20px; - overflow-y: auto; position: relative; z-index: 1; diff --git a/frontend/src/pages/chat/css/_settings-dialog.scss b/frontend/src/pages/chat/css/_settings-dialog.scss index 679e967..e025850 100644 --- a/frontend/src/pages/chat/css/_settings-dialog.scss +++ b/frontend/src/pages/chat/css/_settings-dialog.scss @@ -1,98 +1,103 @@ @use "../../../css/colors" as *; @use "../../../css/material" as *; +@use "../../../core/components/css/styled-dialog" as *; @use "sass:color"; // Settings styles -#settings-dialog { - .fullscreen-wrapper { - display: flex; - align-items: center; - justify-content: center; - height: calc(100vh - (1.5rem * 2)); +.styled-dialog-backdrop.settings-dialog { + .styled-dialog { + width: calc(100vw - $dialog-padding * 2); + height: calc(100vh - $dialog-padding * 2); + max-width: none; + max-height: none; + } + + #settings-dialog-inner { width: 100%; - position: relative; + height: 100%; + display: flex; + flex-direction: column; + padding: 24px; - #settings-dialog-inner { - max-width: 1200px; - max-height: 1000px; - width: 100%; - height: 100%; + .header { + display: flex; + flex-direction: row; + gap: 10px; + margin-bottom: 16px; + flex-shrink: 0; + } - .header { - display: flex; - flex-direction: row; - gap: 10px; - margin-bottom: 16px; + #settings-menu { + display: flex; + flex-direction: row; + gap: 16px; + flex: 1; + min-height: 0; + + mdui-list { + max-width: 280px; + padding-right: 16px; + overflow-y: auto; + flex-shrink: 0; } - #settings-menu { - display: flex; - flex-direction: row; - gap: 16px; + .screen { + flex: 1; + overflow-y: auto; + position: relative; + min-width: 0; - mdui-list { - max-width: 280px; - padding-right: 16px; - overflow-y: auto; - } + .settings-panel { + display: flex; + flex-direction: column; + gap: 16px; + opacity: 0; + visibility: hidden; + transform: translateY(20px); + transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease; + position: absolute; + top: 0; + left: 0; + width: 100%; - .screen { - flex: 1; - overflow-y: auto; - position: relative; + &.active { + opacity: 1; + visibility: visible; + transform: translateY(0); + position: relative; + } - .settings-panel { + h3 { + margin: 0 0 16px 0; + color: $color-dark-on-surface; + } + + mdui-text-field, + mdui-select, + mdui-switch, + mdui-button { + margin-bottom: 8px; + } + + mdui-switch { display: flex; - flex-direction: column; - gap: 16px; - opacity: 0; - visibility: hidden; - transform: translateY(20px); - transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease; - position: absolute; - top: 0; - left: 0; - width: 100%; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid $color-dark-outline; - &.active { - opacity: 1; - visibility: visible; - transform: translateY(0); - position: relative; + &:last-child { + border-bottom: none; } + } - h3 { - margin: 0 0 16px 0; - color: $color-dark-on-surface; - } + p { + margin: 8px 0; + color: $color-dark-on-surface-variant; + } - mdui-text-field, - mdui-select, - mdui-switch, - mdui-button { - margin-bottom: 8px; - } - - mdui-switch { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 0; - border-bottom: 1px solid $color-dark-outline; - - &:last-child { - border-bottom: none; - } - } - - p { - margin: 8px 0; - color: $color-dark-on-surface-variant; - } - - mdui-linear-progress { - margin: 16px 0; - } + mdui-linear-progress { + margin: 16px 0; } } } diff --git a/frontend/src/pages/chat/ui/ProfileDialog.tsx b/frontend/src/pages/chat/ui/ProfileDialog.tsx index 412e5a4..6785c30 100644 --- a/frontend/src/pages/chat/ui/ProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/ProfileDialog.tsx @@ -439,160 +439,164 @@ export function ProfileDialog() { } }} onBackdropClick={handleClose} - > -
- Profile Picture { - const target = e.target as HTMLImageElement; - target.src = defaultAvatar; - }} - /> - {currentData.isOwnProfile && ( -
- -
- )} -
- -
-
- - -
- {errors.display_name && ( -
{errors.display_name}
- )} -
- - {(currentData?.userId || currentData?.isOwnProfile) && !currentData.deleted && ( -
- -
- )} - - {/* Admin Actions Section - Hide for deleted users */} - {!currentData.isOwnProfile && user.currentUser?.id === 1 && !currentData.deleted && ( -
-

Admin Actions

-
- - {currentData.suspended ? "Unsuspend Account" : "Suspend Account"} - - - Delete Account - - { - setCurrentData({ ...currentData, verified }); - }} - /> -
-
- )} - - {/* Verify button for non-admin owner */} - {!currentData.isOwnProfile && currentData.userId && user.currentUser?.id !== 1 && ( -
- { - setCurrentData({ ...currentData, verified }); - }} - /> -
- )} - - {/* Hide profile sections for deleted users */} - {!currentData.deleted && ( -
-
- - {currentData.bio !== undefined && ( -
- )} - - {currentData.memberSince && ( -
- )} - - {currentData.verified && ( -
- )} -
- )} - - {currentData.isOwnProfile && ( + className="profile-dialog" + afterChildren={ + currentData.isOwnProfile && ( - )} - - +
+ Profile Picture { + const target = e.target as HTMLImageElement; + target.src = defaultAvatar; + }} /> + {currentData.isOwnProfile && ( +
+ +
+ )} +
+ +
+
+ + +
+ {errors.display_name && ( +
{errors.display_name}
+ )} +
+ + {(currentData?.userId || currentData?.isOwnProfile) && !currentData.deleted && ( +
+ +
+ )} + + {/* Admin Actions Section - Hide for deleted users */} + {!currentData.isOwnProfile && user.currentUser?.id === 1 && !currentData.deleted && ( +
+

Admin Actions

+
+ + {currentData.suspended ? "Unsuspend Account" : "Suspend Account"} + + + Delete Account + + { + setCurrentData({ ...currentData, verified }); + }} + /> +
+
+ )} + + {/* Verify button for non-admin owner */} + {!currentData.isOwnProfile && currentData.userId && user.currentUser?.id !== 1 && ( +
+ { + setCurrentData({ ...currentData, verified }); + }} + /> +
+ )} + + {/* Hide profile sections for deleted users */} + {!currentData.deleted && ( +
+
+ + {currentData.bio !== undefined && ( +
+ )} + + {currentData.memberSince && ( +
+ )} + + {currentData.verified && ( +
+ )} +
+ )} + + + + ); } diff --git a/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx b/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx index e517209..4713646 100644 --- a/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx +++ b/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { PRODUCT_NAME, API_BASE_URL } from "@/core/config"; import type { DialogProps } from "@/core/types"; -import { MaterialDialog } from "@/core/components/Dialog"; +import { StyledDialog } from "@/core/components/StyledDialog"; import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "@/core/push-notifications/push-notifications"; import { isElectron } from "@/core/electron/electron"; import { useAppState } from "@/pages/chat/state"; @@ -62,151 +62,149 @@ export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) { }; return ( - -
-
-
- onOpenChange(false)}> - Настройки -
-
- - handlePanelChange("notifications-settings")} - style={{ cursor: "pointer" }} - > - Уведомления - - handlePanelChange("appearance-settings")} - style={{ cursor: "pointer" }} - > - Внешний вид - - handlePanelChange("security-settings")} - style={{ cursor: "pointer" }} - > - Безопасность - - handlePanelChange("language-settings")} - style={{ cursor: "pointer" }} - > - Язык - - handlePanelChange("storage-settings")} - style={{ cursor: "pointer" }} - > - Хранилище - - handlePanelChange("help-settings")} - style={{ cursor: "pointer" }} - > - Помощь - - handlePanelChange("about-settings")} - style={{ cursor: "pointer" }} - > - О приложении - - -
-
-

Уведомления

- {pushSupported && ( - handlePushNotificationToggle((e.target as Switch).checked)} - > - Push уведомления - - )} - Новые сообщения - Звуковые уведомления - Уведомления о статусе - Email уведомления -
+ +
+
+ onOpenChange(false)}> + Настройки +
+
+ + handlePanelChange("notifications-settings")} + style={{ cursor: "pointer" }} + > + Уведомления + + handlePanelChange("appearance-settings")} + style={{ cursor: "pointer" }} + > + Внешний вид + + handlePanelChange("security-settings")} + style={{ cursor: "pointer" }} + > + Безопасность + + handlePanelChange("language-settings")} + style={{ cursor: "pointer" }} + > + Язык + + handlePanelChange("storage-settings")} + style={{ cursor: "pointer" }} + > + Хранилище + + handlePanelChange("help-settings")} + style={{ cursor: "pointer" }} + > + Помощь + + handlePanelChange("about-settings")} + style={{ cursor: "pointer" }} + > + О приложении + + +
+
+

Уведомления

+ {pushSupported && ( + handlePushNotificationToggle((e.target as Switch).checked)} + > + Push уведомления + + )} + Новые сообщения + Звуковые уведомления + Уведомления о статусе + Email уведомления +
-
-

Внешний вид

- - Тёмная - Светлая - Авто - - - Маленький - Средний - Большой - -
+
+

Внешний вид

+ + Тёмная + Светлая + Авто + + + Маленький + Средний + Большой + +
-
-

Безопасность

- Изменить пароль - Двухфакторная аутентификация - Автоматический выход -
+
+

Безопасность

+ Изменить пароль + Двухфакторная аутентификация + Автоматический выход +
-
-

Язык

- - Русский - English - Español - -
+
+

Язык

+ + Русский + English + Español + +
-
-

Хранилище

-

Использовано: 2.5 ГБ из 10 ГБ

- - Очистить кэш -
+
+

Хранилище

+

Использовано: 2.5 ГБ из 10 ГБ

+ + Очистить кэш +
-
-

Помощь

- Руководство пользователя - Связаться с поддержкой - FAQ -
+
+

Помощь

+ Руководство пользователя + Связаться с поддержкой + FAQ +
-
-

О приложении

-

Версия: 1.0.0

-

© 2025 {PRODUCT_NAME}. Все права защищены.

- Политика конфиденциальности - Условия использования -
+
+

О приложении

+

Версия: 1.0.0

+

© 2025 {PRODUCT_NAME}. Все права защищены.

+ Политика конфиденциальности + Условия использования
- +
); } diff --git a/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx b/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx index c2e862c..e5e06a9 100644 --- a/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx +++ b/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx @@ -37,16 +37,16 @@ export function MessageContextMenu({ const { user } = useAppState(); // Internal state for closing animation const [isClosing, setIsClosing] = useState(false); - const [calculatedPosition, setCalculatedPosition] = useState(position); + const [reactionBarPosition, setReactionBarPosition] = useState({ x: 0, y: 0 }); + const [contextMenuPosition, setContextMenuPosition] = useState(position); const [animationClass, setAnimationClass] = useState('entering'); - const [reactionBarPosition, setReactionBarPosition] = useState<'left' | 'right'>('left'); + const [reactionBarAnimationClass, setReactionBarAnimationClass] = useState('entering'); + const [reactionBarSide, setReactionBarSide] = useState<'left' | 'right'>('left'); const [isEmojiMenuExpanded, setIsEmojiMenuExpanded] = useState(false); const [initialDimensions, setInitialDimensions] = useState<{ width: number; height: number } | null>(null); const [expandUpward, setExpandUpward] = useState(false); - const [contextMenuHeight, setContextMenuHeight] = useState(null); // Refs for measuring actual dimensions - const wrapperRef = useRef(null); const reactionBarRef = useRef(null); const contextMenuRef = useRef(null); const emojiMenuRef = useRef(null); @@ -56,7 +56,7 @@ export function MessageContextMenu({ if (isOpen) { // Use a small delay to ensure elements are rendered before measuring const frameId = requestAnimationFrame(() => { - if (wrapperRef.current && reactionBarRef.current && contextMenuRef.current) { + if (reactionBarRef.current && contextMenuRef.current) { // Get actual dimensions from DOM elements const reactionBarRect = reactionBarRef.current.getBoundingClientRect(); const contextMenuRect = contextMenuRef.current.getBoundingClientRect(); @@ -67,37 +67,73 @@ export function MessageContextMenu({ // Calculate shared/combined rect dimensions const sharedRect = { width: Math.max(reactionBarRect.width, contextMenuRect.width), - height: reactionBarRect.height + contextMenuRect.height + height: reactionBarRect.height + contextMenuRect.height + 10 // 10px margin }; - let x = position.x; - let y = position.y; + let menuX = position.x; + let menuY = position.y; + let reactionX = position.x; + let reactionY = position.y - reactionBarRect.height - 10; // Position above menu let animation = 'entering'; - let reactionPosition: 'left' | 'right' = 'left'; + let reactionSide: 'left' | 'right' = 'left'; + let reactionPositionedRight = false; - // Check if shared rect would overflow and adjust position - if (x + sharedRect.width > viewportWidth) { - x = position.x - contextMenuRect.width - 25; - animation = 'entering-left'; - reactionPosition = 'right'; + // Check if reaction bar would overflow at the top + if (reactionY < 0) { + // Position reaction bar to the right side of the context menu instead + reactionX = menuX + contextMenuRect.width + 10; + reactionY = menuY; // Align with menu top + reactionSide = 'right'; + reactionPositionedRight = true; + animation = 'entering-right'; // Use right-side animation } else { - reactionPosition = 'left'; + // Try positioning above menu first + reactionSide = 'left'; + + // Check if shared rect would overflow horizontally + if (menuX + sharedRect.width > viewportWidth) { + menuX = position.x - contextMenuRect.width; + reactionX = menuX; + animation = 'entering-left'; + reactionSide = 'right'; + } } // Ensure menu doesn't go off the left edge - if (x < 0) { - x = 0; + if (menuX < 0) { + menuX = 0; + if (!reactionPositionedRight) { + reactionX = menuX; + } } - // Check if shared rect would overflow bottom edge - if (y + sharedRect.height > viewportHeight) { - y = viewportHeight - sharedRect.height; + // Check if reaction bar positioned to the right would overflow + if (reactionPositionedRight && reactionX + reactionBarRect.width > viewportWidth) { + // Position to the left side instead + reactionX = menuX - reactionBarRect.width - 10; + reactionSide = 'left'; + } + + // Check if shared rect would overflow bottom edge (only if reaction bar is above) + if (!reactionPositionedRight && menuY + sharedRect.height > viewportHeight) { + menuY = viewportHeight - sharedRect.height; + reactionY = menuY - reactionBarRect.height - 10; animation = 'entering-up'; } - setCalculatedPosition({ x, y }); + // Ensure menu doesn't go off the right edge + if (menuX + contextMenuRect.width > viewportWidth) { + menuX = viewportWidth - contextMenuRect.width; + if (!reactionPositionedRight) { + reactionX = menuX; + } + } + + setContextMenuPosition({ x: menuX, y: menuY }); + setReactionBarPosition({ x: reactionX, y: reactionY }); setAnimationClass(animation); - setReactionBarPosition(reactionPosition); + setReactionBarAnimationClass(animation); + setReactionBarSide(reactionSide); } }); @@ -148,17 +184,18 @@ export function MessageContextMenu({ // Set appropriate closing animation based on opening animation const closingAnimation = animationClass.replace('entering', 'closing'); setAnimationClass(closingAnimation); + setReactionBarAnimationClass(closingAnimation); // Wait for animation to complete before calling onOpenChange setTimeout(() => { onOpenChange(false); setIsClosing(false); setAnimationClass('entering'); // Reset for next opening + setReactionBarAnimationClass('entering'); // Reset for next opening // Reset emoji menu state after context menu animation completes setIsEmojiMenuExpanded(false); setInitialDimensions(null); setExpandUpward(false); - setContextMenuHeight(null); }, 200); // Match the animation duration from _animations.scss } @@ -226,14 +263,12 @@ export function MessageContextMenu({ } function handleExpandClick() { - if (!reactionBarRef.current || !wrapperRef.current) return; + if (!reactionBarRef.current || !contextMenuRef.current) return; // Measure the actual dimensions of the reaction bar content const reactionBarRect = reactionBarRef.current.getBoundingClientRect(); - const wrapperRect = wrapperRef.current.getBoundingClientRect(); setInitialDimensions({ width: reactionBarRect.width, height: reactionBarRect.height }); - setContextMenuHeight(wrapperRect.height); // Check if expanding downward would cause overflow // Calculate space from the reaction bar's bottom edge downward @@ -259,31 +294,28 @@ export function MessageContextMenu({ } return isOpen && ( -
e.stopPropagation()}> - + <> {/* Reaction Bar */}
+ }} + onClick={(e) => e.stopPropagation()}> {!isEmojiMenuExpanded ? (
{QUICK_REACTIONS.map((emoji, index) => ( @@ -321,21 +353,26 @@ export function MessageContextMenu({ {/* Context Menu */}
+ className={`context-menu ${animationClass} ${isEmojiMenuExpanded ? "faded" : ""}`} + style={{ + position: 'fixed', + top: `${contextMenuPosition.y}px`, + left: `${contextMenuPosition.x}px`, + zIndex: 1000 + }} + onClick={(e) => e.stopPropagation()}> {actions.map((action, i) => ( action.show && (
+ key={i}> {action.icon} {action.label}
) ))}
- -
+ ) }