v3: added a chatbox
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
Some checks failed
Build and Publish Docker Image / build-and-push (push) Has been cancelled
This commit is contained in:
340
src/App.css
340
src/App.css
@@ -1162,6 +1162,75 @@ body.has-fab .page-content {
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
/* Chat FAB — mirrors .dump-fab and stacks directly above it (the "+" dump
|
||||
button keeps the bottom slot; chat sits one button-height up) so the two
|
||||
never overlap. */
|
||||
.chat-fab {
|
||||
position: fixed;
|
||||
--fab-size: 3.5rem;
|
||||
/* Share the dump-fab's column. */
|
||||
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) - 1rem));
|
||||
/* One button-height + gap above the dump-fab's bottom-anchored resting spot. */
|
||||
top: calc(100vh - var(--fab-size) * 2 - 2rem - env(safe-area-inset-bottom, 0px));
|
||||
top: calc(100dvh - var(--fab-size) * 2 - 2rem - env(safe-area-inset-bottom, 0px));
|
||||
z-index: 900;
|
||||
width: var(--fab-size);
|
||||
height: var(--fab-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: var(--fab-radius, 50%);
|
||||
background: var(--color-accent);
|
||||
color: var(--color-on-accent);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 16px color-mix(in srgb, var(--color-accent) 45%, transparent),
|
||||
0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transform: translateY(1rem) scale(0.9);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, background 0.15s ease,
|
||||
top 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.chat-fab-icon {
|
||||
width: 1.6rem;
|
||||
height: 1.6rem;
|
||||
}
|
||||
|
||||
.chat-fab--visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.chat-fab:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: translateY(-2px) scale(1);
|
||||
box-shadow: 0 6px 20px color-mix(in srgb, var(--color-accent) 55%, transparent),
|
||||
0 2px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.chat-fab:active {
|
||||
transform: translateY(0) scale(0.96);
|
||||
}
|
||||
|
||||
/* Match the dump-fab's clearance shift when a player is mounted. */
|
||||
body.has-player .chat-fab {
|
||||
top: calc(1.25rem + env(safe-area-inset-top, 0px));
|
||||
/* Stack below the dump-fab in the top-right corner to avoid overlap. */
|
||||
right: max(1.25rem, calc(50% - var(--fab-lane, 860px) / 2 - var(--fab-size) - 1rem));
|
||||
top: calc(1.25rem + var(--fab-size) + 0.75rem + env(safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
/* The badge sits on the circular FAB; nudge it onto the rim. */
|
||||
.chat-fab .notification-badge {
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
box-shadow: 0 0 0 2px var(--color-accent);
|
||||
}
|
||||
|
||||
.rich-content-thumbnail-btn {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
@@ -2148,6 +2217,7 @@ body.has-fab .page-content {
|
||||
/* Icon-only controls are square, so the row reads as a tidy set. */
|
||||
.app-header-nav .nav-search-btn,
|
||||
.app-header-nav .notification-bell,
|
||||
.app-header-nav .chat-button,
|
||||
.app-header-nav .user-menu-trigger {
|
||||
width: 2.25rem;
|
||||
padding: 0;
|
||||
@@ -3221,6 +3291,255 @@ body.has-fab .page-content {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* ── Live chat ── */
|
||||
.chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: min(60vh, 480px);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
/* Reserve a gutter for the scrollbar so it never sits on top of the
|
||||
messages — without this, overlay scrollbars overlap the content. */
|
||||
scrollbar-gutter: stable;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-empty {
|
||||
margin: auto 0;
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.chat-load-older {
|
||||
text-align: center;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
align-items: flex-start;
|
||||
/* Each message animates once, when its element first mounts — so newly
|
||||
arriving messages slide in while existing ones stay put (React reuses
|
||||
keyed DOM nodes). */
|
||||
animation: chat-msg-in 0.18s ease-out;
|
||||
}
|
||||
|
||||
@keyframes chat-msg-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.chat-message {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-message-meta {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.chat-message-author {
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.chat-message-author:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.chat-message-time {
|
||||
font-size: 0.7rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.chat-message-text {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.chat-message-text.md {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.chat-message-edited {
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Edit/Delete actions: bare inline icons that follow the author/time in the
|
||||
meta row, sitting right next to the message they belong to. Faded in on hover
|
||||
(or focus, for keyboard use) so they stay unobtrusive. */
|
||||
.chat-message-actions {
|
||||
margin-left: 0.35rem;
|
||||
display: inline-flex;
|
||||
gap: 0.15rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.chat-message:hover .chat-message-actions,
|
||||
.chat-message:focus-within .chat-message-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (hover: none) {
|
||||
/* No hover on touch devices — keep the actions visible. */
|
||||
.chat-message-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-icon-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0 0.1rem;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.chat-icon-btn:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.chat-icon-btn--delete:hover {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.chat-edit {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.chat-edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
/* Compact buttons for the inline edit box — the full-size form buttons are too
|
||||
bulky for the narrow message column. */
|
||||
.chat-edit-actions .btn-primary,
|
||||
.chat-edit-actions .btn-secondary {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0.7rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.chat-composer {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-composer .mention-textarea-wrap {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Reserve room on the input's bottom-right for the char counter. `display:
|
||||
block` removes the inline-block baseline descender gap below the textarea —
|
||||
that phantom space was making the wrapper taller than the textarea, so the
|
||||
bottom-aligned Send button landed a few px low. */
|
||||
.chat-input {
|
||||
padding-right: 3.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Pin the counter inside the textarea instead of letting it add a row below —
|
||||
that extra height is what knocked the Send button out of alignment. */
|
||||
.chat-composer .text-editor-count {
|
||||
position: absolute;
|
||||
right: 0.55rem;
|
||||
bottom: 0.4rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-send-btn {
|
||||
flex-shrink: 0;
|
||||
/* Match the input's resting height so the two line up exactly (both edges),
|
||||
while the input is free to grow taller as you type. */
|
||||
height: 2.4rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* The composer's popovers (mention list, emoji picker) anchor to the input,
|
||||
which sits at the very bottom of the modal. The modal body must not clip them
|
||||
— let them escape the body entirely so they're never cropped. The message
|
||||
list keeps its own scroll, so nothing else relies on the body clipping. */
|
||||
.modal-card:has(.chat) .modal-body {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Open the mention list upward, into the roomy message area above the input. */
|
||||
.chat-composer .mention-dropdown {
|
||||
top: auto;
|
||||
bottom: 100%;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
/* Very narrow viewports: stack the input and Send button. */
|
||||
@media (max-width: 400px) {
|
||||
.chat-composer {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.chat-send-btn {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* The author/time/edited row gets cramped when squeezed side-by-side here —
|
||||
stack the metadata instead so each piece stays readable. Keep the stacked
|
||||
lines tight so they don't balloon the message height. */
|
||||
.chat-message-meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
/* No longer trailing the time inline; align it with the stacked metadata. */
|
||||
.chat-message-actions {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-modal-message {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
@@ -4359,6 +4678,27 @@ body.has-fab .page-content {
|
||||
box-shadow: 0 0 0 2px var(--color-bg);
|
||||
}
|
||||
|
||||
/* ── Header chat button ── */
|
||||
.chat-button {
|
||||
position: relative;
|
||||
background: var(--color-header-user-bg);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
padding: 0.35rem 0.85rem;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.chat-button:hover {
|
||||
background: var(--color-header-user-bg-hover);
|
||||
}
|
||||
.chat-button-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ── Notifications page ── */
|
||||
.notifications-page {
|
||||
max-width: 680px;
|
||||
|
||||
Reference in New Issue
Block a user