feat: add long-press map interaction and improve create post UI
- Add long-press (1 second) on map to open create post popup - Add close button (X) to mode selector and create post popups - Improve create post popup design with glass-morphism effect - Fix chat: mark messages as read when received in open chat - Fix avatar styling for guest users (remove nested circles) - Add userId to auth context for stable chat identification 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f9e1505147
commit
4705fc03b1
|
|
@ -155,6 +155,7 @@ async function me(accessToken) {
|
||||||
status: res.status,
|
status: res.status,
|
||||||
json,
|
json,
|
||||||
text,
|
text,
|
||||||
|
userId: u && typeof u.user_id === "number" ? u.user_id : null,
|
||||||
username: (u && (u.username || u.user || u.preferred_username)) || "",
|
username: (u && (u.username || u.user || u.preferred_username)) || "",
|
||||||
emailVerified,
|
emailVerified,
|
||||||
avatarUrl: u && typeof u.avatar_url === "string" ? u.avatar_url : "",
|
avatarUrl: u && typeof u.avatar_url === "string" ? u.avatar_url : "",
|
||||||
|
|
@ -212,7 +213,7 @@ function nowSec() {
|
||||||
|
|
||||||
export function AuthProvider({ children }) {
|
export function AuthProvider({ children }) {
|
||||||
const [status, setStatus] = useState("loading"); // loading | anon | auth
|
const [status, setStatus] = useState("loading"); // loading | anon | auth
|
||||||
const [user, setUser] = useState(null); // { username }
|
const [user, setUser] = useState(null); // { username, userId }
|
||||||
const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... }
|
const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... }
|
||||||
const [avatarUrl, setAvatarUrl] = useState("");
|
const [avatarUrl, setAvatarUrl] = useState("");
|
||||||
const [profileLoaded, setProfileLoaded] = useState(false);
|
const [profileLoaded, setProfileLoaded] = useState(false);
|
||||||
|
|
@ -261,7 +262,11 @@ export function AuthProvider({ children }) {
|
||||||
setAvatarUrl(mr.avatarUrl);
|
setAvatarUrl(mr.avatarUrl);
|
||||||
}
|
}
|
||||||
if (mr.username) {
|
if (mr.username) {
|
||||||
setUser({ username: mr.username });
|
setUser((prev) => ({
|
||||||
|
...prev,
|
||||||
|
username: mr.username,
|
||||||
|
userId: mr.userId || prev?.userId || null,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
if (typeof mr.needsUsername === "boolean") {
|
if (typeof mr.needsUsername === "boolean") {
|
||||||
setNeedsUsername(mr.needsUsername);
|
setNeedsUsername(mr.needsUsername);
|
||||||
|
|
@ -742,6 +747,7 @@ export function AuthProvider({ children }) {
|
||||||
const loading = status === "loading";
|
const loading = status === "loading";
|
||||||
const authenticated = status === "auth";
|
const authenticated = status === "auth";
|
||||||
const username = user?.username || "";
|
const username = user?.username || "";
|
||||||
|
const userId = user?.userId || null;
|
||||||
const token = tokens?.access_token || "";
|
const token = tokens?.access_token || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -758,6 +764,7 @@ export function AuthProvider({ children }) {
|
||||||
loading,
|
loading,
|
||||||
authenticated,
|
authenticated,
|
||||||
username,
|
username,
|
||||||
|
userId,
|
||||||
token,
|
token,
|
||||||
|
|
||||||
lastError: error || "",
|
lastError: error || "",
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,9 @@ export default function ChatContainer() {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Reference to markAsRead function (will be set after useChat)
|
||||||
|
const markAsReadRef = useRef(null);
|
||||||
|
|
||||||
// Handle new incoming message
|
// Handle new incoming message
|
||||||
const handleNewMessage = useCallback((msg) => {
|
const handleNewMessage = useCallback((msg) => {
|
||||||
const senderName = msg.sender_name;
|
const senderName = msg.sender_name;
|
||||||
|
|
@ -110,18 +113,24 @@ export default function ChatContainer() {
|
||||||
const isChatMinimized = minimizedChatsRef.current[senderName];
|
const isChatMinimized = minimizedChatsRef.current[senderName];
|
||||||
const isDocumentFocused = document.hasFocus();
|
const isDocumentFocused = document.hasFocus();
|
||||||
|
|
||||||
|
// Chat is visible = open + not minimized + document focused
|
||||||
|
const isChatVisible = isChatOpen && !isChatMinimized && isDocumentFocused;
|
||||||
|
|
||||||
|
if (isChatVisible) {
|
||||||
|
// Chat is open and user can see the message - mark as read immediately
|
||||||
|
if (markAsReadRef.current) {
|
||||||
|
markAsReadRef.current(senderName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
// Update unread count if chat is not visible
|
// Update unread count if chat is not visible
|
||||||
if (!isChatOpen || isChatMinimized || !isDocumentFocused) {
|
|
||||||
setUnreadCounts((prev) => ({
|
setUnreadCounts((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[senderName]: (prev[senderName] || 0) + 1,
|
[senderName]: (prev[senderName] || 0) + 1,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show notification if:
|
// Show notification if chat is not visible
|
||||||
// - Chat is not open, or is minimized, or document is not focused
|
const shouldNotify = !isChatVisible;
|
||||||
// - Notification permission is granted
|
|
||||||
const shouldNotify = !isChatOpen || isChatMinimized || !isDocumentFocused;
|
|
||||||
|
|
||||||
if (shouldNotify) {
|
if (shouldNotify) {
|
||||||
// Play notification sound
|
// Play notification sound
|
||||||
|
|
@ -168,6 +177,11 @@ export default function ChatContainer() {
|
||||||
onNewMessage: handleNewMessage,
|
onNewMessage: handleNewMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keep markAsReadRef updated so handleNewMessage can use it
|
||||||
|
useEffect(() => {
|
||||||
|
markAsReadRef.current = markAsRead;
|
||||||
|
}, [markAsRead]);
|
||||||
|
|
||||||
// Load friends when authenticated
|
// Load friends when authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
|
|
@ -243,22 +257,28 @@ export default function ChatContainer() {
|
||||||
// Refresh conversations to update unread counts/badges
|
// Refresh conversations to update unread counts/badges
|
||||||
const convs = await fetchConversations();
|
const convs = await fetchConversations();
|
||||||
if (convs && convs.length > 0) {
|
if (convs && convs.length > 0) {
|
||||||
const newUnread = {};
|
// Build new unread counts from server (source of truth)
|
||||||
|
const serverUnread = {};
|
||||||
convs.forEach((c) => {
|
convs.forEach((c) => {
|
||||||
if (c.unread_count > 0) {
|
// Use case-insensitive key matching
|
||||||
newUnread[c.username] = c.unread_count;
|
serverUnread[c.username.toLowerCase()] = { username: c.username, count: c.unread_count || 0 };
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setUnreadCounts((prev) => {
|
setUnreadCounts((prev) => {
|
||||||
// Merge but don't overwrite zeros (already read in this session)
|
const updated = {};
|
||||||
const merged = { ...prev };
|
// For each conversation from server, use server count
|
||||||
Object.entries(newUnread).forEach(([user, count]) => {
|
// unless the chat is currently open and not minimized (user is viewing it)
|
||||||
// Only update if we don't have a local zero (user already read it)
|
Object.values(serverUnread).forEach(({ username, count }) => {
|
||||||
if (merged[user] === undefined || merged[user] > 0) {
|
const key = username;
|
||||||
merged[user] = count;
|
const isOpenAndVisible = activeChatsRef.current.some(
|
||||||
}
|
(c) => c.username.toLowerCase() === username.toLowerCase()
|
||||||
|
) && !minimizedChatsRef.current[username];
|
||||||
|
|
||||||
|
// If chat is open and visible, keep it at 0 (user can see messages)
|
||||||
|
// Otherwise use server count
|
||||||
|
updated[key] = isOpenAndVisible ? 0 : count;
|
||||||
});
|
});
|
||||||
return merged;
|
return updated;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -388,6 +408,10 @@ export default function ChatContainer() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh conversations when opening to get latest activity for sorting
|
||||||
|
if (!showContacts) {
|
||||||
|
fetchConversations();
|
||||||
|
}
|
||||||
setShowContacts(!showContacts);
|
setShowContacts(!showContacts);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -468,7 +492,22 @@ export default function ChatContainer() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="contacts-list">
|
<div className="contacts-list">
|
||||||
{friends.map((friend) => {
|
{/* Sort friends by last activity: conversations first (by date), then others */}
|
||||||
|
{[...friends].sort((a, b) => {
|
||||||
|
const aUsername = a.username || a.name;
|
||||||
|
const bUsername = b.username || b.name;
|
||||||
|
const aConv = conversations.find(c => c.username?.toLowerCase() === aUsername?.toLowerCase());
|
||||||
|
const bConv = conversations.find(c => c.username?.toLowerCase() === bUsername?.toLowerCase());
|
||||||
|
|
||||||
|
// Friends with conversations come first, sorted by last_message_at
|
||||||
|
if (aConv && bConv) {
|
||||||
|
return new Date(bConv.last_message_at) - new Date(aConv.last_message_at);
|
||||||
|
}
|
||||||
|
if (aConv) return -1;
|
||||||
|
if (bConv) return 1;
|
||||||
|
// Friends without conversations sorted alphabetically
|
||||||
|
return aUsername.localeCompare(bUsername);
|
||||||
|
}).map((friend) => {
|
||||||
const friendUsername = friend.username || friend.name;
|
const friendUsername = friend.username || friend.name;
|
||||||
const avatar = friend.avatar_url || friend.avatar;
|
const avatar = friend.avatar_url || friend.avatar;
|
||||||
const isOnline = presence[friendUsername] || false;
|
const isOnline = presence[friendUsername] || false;
|
||||||
|
|
|
||||||
|
|
@ -532,7 +532,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="sw-userchip" title={t('common.guest')}>
|
<div className="sw-userchip" title={t('common.guest')}>
|
||||||
<div className="sw-avatar">
|
<div className="sw-avatar sw-avatar-guest">
|
||||||
<i className="fa-solid fa-user" />
|
<i className="fa-solid fa-user" />
|
||||||
</div>
|
</div>
|
||||||
<div className="sw-usertext">
|
<div className="sw-usertext">
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,70 @@ export default function MapView({
|
||||||
[mapRef, theme]
|
[mapRef, theme]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Long-press detection for opening create post popup (1 second)
|
||||||
|
const longPressTimerRef = useRef(null);
|
||||||
|
const longPressStartRef = useRef(null);
|
||||||
|
const LONG_PRESS_DURATION = 1000; // 1 second
|
||||||
|
|
||||||
|
const handleLongPressStart = useCallback((e) => {
|
||||||
|
// Only trigger on map container, not on overlays/buttons
|
||||||
|
if (e.target.closest('.map-overlay') || e.target.closest('.sw-marker') || e.target.closest('button')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
longPressStartRef.current = { x: e.clientX || e.touches?.[0]?.clientX, y: e.clientY || e.touches?.[0]?.clientY };
|
||||||
|
longPressTimerRef.current = setTimeout(() => {
|
||||||
|
handleOpenCreate();
|
||||||
|
}, LONG_PRESS_DURATION);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLongPressEnd = useCallback(() => {
|
||||||
|
if (longPressTimerRef.current) {
|
||||||
|
clearTimeout(longPressTimerRef.current);
|
||||||
|
longPressTimerRef.current = null;
|
||||||
|
}
|
||||||
|
longPressStartRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLongPressMove = useCallback((e) => {
|
||||||
|
if (!longPressStartRef.current) return;
|
||||||
|
const x = e.clientX || e.touches?.[0]?.clientX;
|
||||||
|
const y = e.clientY || e.touches?.[0]?.clientY;
|
||||||
|
const dx = Math.abs(x - longPressStartRef.current.x);
|
||||||
|
const dy = Math.abs(y - longPressStartRef.current.y);
|
||||||
|
// If moved more than 10px, cancel the long press
|
||||||
|
if (dx > 10 || dy > 10) {
|
||||||
|
handleLongPressEnd();
|
||||||
|
}
|
||||||
|
}, [handleLongPressEnd]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.addEventListener('mousedown', handleLongPressStart);
|
||||||
|
container.addEventListener('mouseup', handleLongPressEnd);
|
||||||
|
container.addEventListener('mouseleave', handleLongPressEnd);
|
||||||
|
container.addEventListener('mousemove', handleLongPressMove);
|
||||||
|
container.addEventListener('touchstart', handleLongPressStart, { passive: true });
|
||||||
|
container.addEventListener('touchend', handleLongPressEnd);
|
||||||
|
container.addEventListener('touchcancel', handleLongPressEnd);
|
||||||
|
container.addEventListener('touchmove', handleLongPressMove, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('mousedown', handleLongPressStart);
|
||||||
|
container.removeEventListener('mouseup', handleLongPressEnd);
|
||||||
|
container.removeEventListener('mouseleave', handleLongPressEnd);
|
||||||
|
container.removeEventListener('mousemove', handleLongPressMove);
|
||||||
|
container.removeEventListener('touchstart', handleLongPressStart);
|
||||||
|
container.removeEventListener('touchend', handleLongPressEnd);
|
||||||
|
container.removeEventListener('touchcancel', handleLongPressEnd);
|
||||||
|
container.removeEventListener('touchmove', handleLongPressMove);
|
||||||
|
if (longPressTimerRef.current) {
|
||||||
|
clearTimeout(longPressTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [containerRef, handleLongPressStart, handleLongPressEnd, handleLongPressMove]);
|
||||||
|
|
||||||
const fitBoundsWhenReady = useCallback(
|
const fitBoundsWhenReady = useCallback(
|
||||||
(bounds, opts = {}) => {
|
(bounds, opts = {}) => {
|
||||||
const tryFit = () => {
|
const tryFit = () => {
|
||||||
|
|
@ -1671,8 +1735,11 @@ export default function MapView({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{modeSelectorVisible && (
|
{modeSelectorVisible && (
|
||||||
<div className="map-overlay mode-selector-widget">
|
<div className="map-overlay mode-selector-widget mode-selector-v2">
|
||||||
<div className="mode-selector-bg" />
|
<button className="mode-selector-close" onClick={closeCreateModal} title="Close">
|
||||||
|
<i className="fa-solid fa-xmark" />
|
||||||
|
</button>
|
||||||
|
<div className="mode-selector-title">Create a post</div>
|
||||||
<div className="mode-selector-chips">
|
<div className="mode-selector-chips">
|
||||||
{CONTENT_CREATOR_MODES.map((mode) => (
|
{CONTENT_CREATOR_MODES.map((mode) => (
|
||||||
<button
|
<button
|
||||||
|
|
@ -1692,12 +1759,13 @@ export default function MapView({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCreating && modeChosen && (
|
{isCreating && modeChosen && (
|
||||||
<div className="map-overlay create-panel-compact">
|
<div className="map-overlay create-panel-compact create-panel-v2">
|
||||||
<div className="create-panel-header">
|
<button className="create-close-btn" onClick={closeCreateModal} title="Close">
|
||||||
<span>{`Share as ${currentContentMode.label}`}</span>
|
<i className="fa-solid fa-xmark" />
|
||||||
<button className="create-close" onClick={closeCreateModal}>
|
|
||||||
✕
|
|
||||||
</button>
|
</button>
|
||||||
|
<div className="create-panel-header">
|
||||||
|
<i className={currentContentMode.icon + " create-header-icon"} />
|
||||||
|
<span>{currentContentMode.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="create-panel-body">
|
<div className="create-panel-body">
|
||||||
<div className="create-step-label">Step {createStep + 1} / 3</div>
|
<div className="create-step-label">Step {createStep + 1} / 3</div>
|
||||||
|
|
|
||||||
|
|
@ -1350,3 +1350,442 @@ body[data-theme="light"] .map-action-btn * {
|
||||||
.map-overlay.mode-selector-widget{
|
.map-overlay.mode-selector-widget{
|
||||||
pointer-events:none;
|
pointer-events:none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== NEW CREATE PANEL V2 - IMPROVED DESIGN ===== */
|
||||||
|
|
||||||
|
.create-panel-v2{
|
||||||
|
position:absolute;
|
||||||
|
left:50%;
|
||||||
|
top:50%;
|
||||||
|
transform:translate(-50%,-50%);
|
||||||
|
width:min(340px, 92vw);
|
||||||
|
max-height:80vh;
|
||||||
|
overflow:auto;
|
||||||
|
background: linear-gradient(145deg, rgba(15,23,42,0.97), rgba(7,10,14,0.98));
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
border-radius:24px;
|
||||||
|
border:1px solid rgba(56,189,248,0.35);
|
||||||
|
box-shadow:
|
||||||
|
0 25px 60px rgba(0,0,0,0.65),
|
||||||
|
0 0 40px rgba(56,189,248,0.15),
|
||||||
|
inset 0 1px 0 rgba(255,255,255,0.08);
|
||||||
|
padding: 0;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-header{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
gap:0.6rem;
|
||||||
|
padding: 1rem 1.2rem 0.6rem;
|
||||||
|
font-size:1rem;
|
||||||
|
font-weight:700;
|
||||||
|
letter-spacing:0.02em;
|
||||||
|
text-transform:none;
|
||||||
|
color:#f8fafc;
|
||||||
|
border-bottom: 1px solid rgba(148,163,184,0.15);
|
||||||
|
margin-bottom:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-header-icon{
|
||||||
|
font-size:1.1rem;
|
||||||
|
color:#38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-body{
|
||||||
|
padding: 0.8rem 1rem 1rem;
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-step-label{
|
||||||
|
font-size:0.7rem;
|
||||||
|
text-transform:uppercase;
|
||||||
|
letter-spacing:0.18em;
|
||||||
|
color:#64748b;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-hero{
|
||||||
|
font-size:0.9rem;
|
||||||
|
color:#cbd5e1;
|
||||||
|
margin:0;
|
||||||
|
text-align:center;
|
||||||
|
line-height:1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-note{
|
||||||
|
font-size:0.8rem;
|
||||||
|
color:#94a3b8;
|
||||||
|
margin:0;
|
||||||
|
text-align:center;
|
||||||
|
padding:0.5rem;
|
||||||
|
background:rgba(56,189,248,0.08);
|
||||||
|
border-radius:12px;
|
||||||
|
border:1px solid rgba(56,189,248,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-field{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-field label{
|
||||||
|
font-size:0.75rem;
|
||||||
|
font-weight:600;
|
||||||
|
color:#94a3b8;
|
||||||
|
text-transform:uppercase;
|
||||||
|
letter-spacing:0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-selects{
|
||||||
|
display:flex;
|
||||||
|
gap:0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-selects select{
|
||||||
|
flex:1;
|
||||||
|
background:rgba(15,23,42,0.8);
|
||||||
|
color:#e5e7eb;
|
||||||
|
border-radius:12px;
|
||||||
|
border:1px solid rgba(148,163,184,0.3);
|
||||||
|
padding:0.55rem 0.75rem;
|
||||||
|
font-size:0.85rem;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-selects select:focus{
|
||||||
|
outline:none;
|
||||||
|
border-color:rgba(56,189,248,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-input-narrow{
|
||||||
|
background:rgba(15,23,42,0.8);
|
||||||
|
color:#e5e7eb;
|
||||||
|
border-radius:12px;
|
||||||
|
border:1px solid rgba(148,163,184,0.3);
|
||||||
|
padding:0.6rem 0.9rem;
|
||||||
|
font-size:0.9rem;
|
||||||
|
transition:border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-input-narrow:focus{
|
||||||
|
outline:none;
|
||||||
|
border-color:rgba(56,189,248,0.6);
|
||||||
|
box-shadow:0 0 0 3px rgba(56,189,248,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-input-narrow::placeholder{
|
||||||
|
color:#64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-hint-tight{
|
||||||
|
font-size:0.75rem;
|
||||||
|
color:#64748b;
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-textarea-tight{
|
||||||
|
background:rgba(15,23,42,0.8);
|
||||||
|
border:1px solid rgba(148,163,184,0.3);
|
||||||
|
color:#e5e7eb;
|
||||||
|
border-radius:14px;
|
||||||
|
padding:0.7rem 0.9rem;
|
||||||
|
font-size:0.85rem;
|
||||||
|
min-height:90px;
|
||||||
|
resize:none;
|
||||||
|
transition:border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-textarea-tight:focus{
|
||||||
|
outline:none;
|
||||||
|
border-color:rgba(56,189,248,0.6);
|
||||||
|
box-shadow:0 0 0 3px rgba(56,189,248,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-actions{
|
||||||
|
display:flex;
|
||||||
|
gap:0.6rem;
|
||||||
|
margin-top:0.3rem;
|
||||||
|
padding-top:0.8rem;
|
||||||
|
border-top:1px solid rgba(148,163,184,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-btn{
|
||||||
|
flex:1;
|
||||||
|
padding:0.65rem 1rem;
|
||||||
|
font-size:0.85rem;
|
||||||
|
font-weight:600;
|
||||||
|
border-radius:14px;
|
||||||
|
border:1px solid rgba(148,163,184,0.3);
|
||||||
|
background:rgba(15,23,42,0.6);
|
||||||
|
color:#e5e7eb;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-btn:hover:not(:disabled){
|
||||||
|
background:rgba(30,41,59,0.8);
|
||||||
|
border-color:rgba(148,163,184,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-btn:disabled{
|
||||||
|
opacity:0.4;
|
||||||
|
cursor:not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-btn.primary{
|
||||||
|
background:linear-gradient(135deg, #38bdf8, #22d3ee);
|
||||||
|
border:none;
|
||||||
|
color:#0f172a;
|
||||||
|
box-shadow:0 4px 14px rgba(56,189,248,0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-panel-btn.primary:hover:not(:disabled){
|
||||||
|
background:linear-gradient(135deg, #7dd3fc, #67e8f9);
|
||||||
|
box-shadow:0 6px 20px rgba(56,189,248,0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close button - positioned top right */
|
||||||
|
.create-close-btn{
|
||||||
|
position:absolute;
|
||||||
|
top:0.6rem;
|
||||||
|
right:0.6rem;
|
||||||
|
width:32px;
|
||||||
|
height:32px;
|
||||||
|
border-radius:50%;
|
||||||
|
border:1px solid rgba(148,163,184,0.3);
|
||||||
|
background:rgba(15,23,42,0.8);
|
||||||
|
color:#94a3b8;
|
||||||
|
font-size:1rem;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:all 0.2s ease;
|
||||||
|
z-index:10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-close-btn:hover{
|
||||||
|
background:rgba(239,68,68,0.15);
|
||||||
|
border-color:rgba(239,68,68,0.5);
|
||||||
|
color:#f87171;
|
||||||
|
transform:scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-close-btn:active{
|
||||||
|
transform:scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image choice buttons improved */
|
||||||
|
.create-panel-v2 .create-image-choice{
|
||||||
|
display:flex;
|
||||||
|
gap:0.5rem;
|
||||||
|
margin:0.3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-image-choice-btn{
|
||||||
|
flex:1;
|
||||||
|
padding:0.5rem 0.7rem;
|
||||||
|
border-radius:12px;
|
||||||
|
border:1px solid rgba(148,163,184,0.3);
|
||||||
|
background:rgba(15,23,42,0.6);
|
||||||
|
color:#cbd5e1;
|
||||||
|
font-size:0.8rem;
|
||||||
|
font-weight:500;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-image-choice-btn.is-active{
|
||||||
|
background:rgba(56,189,248,0.15);
|
||||||
|
border-color:rgba(56,189,248,0.5);
|
||||||
|
color:#7dd3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File buttons improved */
|
||||||
|
.create-panel-v2 .create-image-row{
|
||||||
|
display:flex;
|
||||||
|
gap:0.5rem;
|
||||||
|
flex-wrap:wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-file-btn{
|
||||||
|
padding:0.5rem 0.8rem;
|
||||||
|
border-radius:12px;
|
||||||
|
border:1px solid rgba(148,163,184,0.3);
|
||||||
|
background:rgba(15,23,42,0.6);
|
||||||
|
color:#e5e7eb;
|
||||||
|
font-size:0.8rem;
|
||||||
|
font-weight:500;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-file-btn:hover{
|
||||||
|
background:rgba(30,41,59,0.8);
|
||||||
|
border-color:rgba(148,163,184,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-file-btn--camera{
|
||||||
|
border-color:rgba(56,189,248,0.4);
|
||||||
|
color:#7dd3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-muted{
|
||||||
|
font-size:0.75rem;
|
||||||
|
color:#64748b;
|
||||||
|
text-align:center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link preview card improved */
|
||||||
|
.create-panel-v2 .create-link-preview-card{
|
||||||
|
border-radius:16px;
|
||||||
|
background:rgba(15,23,42,0.6);
|
||||||
|
border:1px solid rgba(148,163,184,0.25);
|
||||||
|
overflow:hidden;
|
||||||
|
margin-bottom:0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-link-preview-card img{
|
||||||
|
width:100%;
|
||||||
|
height:120px;
|
||||||
|
object-fit:cover;
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-panel-v2 .create-link-preview-meta{
|
||||||
|
padding:0.5rem 0.7rem;
|
||||||
|
font-size:0.75rem;
|
||||||
|
color:#94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 400px){
|
||||||
|
.create-panel-v2{
|
||||||
|
width:calc(100vw - 20px);
|
||||||
|
border-radius:18px;
|
||||||
|
}
|
||||||
|
.create-panel-v2 .create-panel-header{
|
||||||
|
padding:0.8rem 1rem 0.5rem;
|
||||||
|
font-size:0.95rem;
|
||||||
|
}
|
||||||
|
.create-panel-v2 .create-panel-body{
|
||||||
|
padding:0.6rem 0.8rem 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MODE SELECTOR V2 - IMPROVED DESIGN ===== */
|
||||||
|
|
||||||
|
.mode-selector-v2{
|
||||||
|
position:absolute;
|
||||||
|
left:50%;
|
||||||
|
top:50%;
|
||||||
|
transform:translate(-50%,-50%);
|
||||||
|
pointer-events:auto;
|
||||||
|
background: linear-gradient(145deg, rgba(15,23,42,0.97), rgba(7,10,14,0.98));
|
||||||
|
backdrop-filter: blur(24px);
|
||||||
|
border-radius:24px;
|
||||||
|
border:1px solid rgba(56,189,248,0.35);
|
||||||
|
box-shadow:
|
||||||
|
0 25px 60px rgba(0,0,0,0.65),
|
||||||
|
0 0 40px rgba(56,189,248,0.15),
|
||||||
|
inset 0 1px 0 rgba(255,255,255,0.08);
|
||||||
|
padding:1rem;
|
||||||
|
min-width:280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-v2 .mode-selector-bg{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-close{
|
||||||
|
position:absolute;
|
||||||
|
top:0.6rem;
|
||||||
|
right:0.6rem;
|
||||||
|
width:32px;
|
||||||
|
height:32px;
|
||||||
|
border-radius:50%;
|
||||||
|
border:1px solid rgba(148,163,184,0.3);
|
||||||
|
background:rgba(15,23,42,0.8);
|
||||||
|
color:#94a3b8;
|
||||||
|
font-size:1rem;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:all 0.2s ease;
|
||||||
|
z-index:10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-close:hover{
|
||||||
|
background:rgba(239,68,68,0.15);
|
||||||
|
border-color:rgba(239,68,68,0.5);
|
||||||
|
color:#f87171;
|
||||||
|
transform:scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-title{
|
||||||
|
text-align:center;
|
||||||
|
font-size:1.1rem;
|
||||||
|
font-weight:700;
|
||||||
|
color:#f8fafc;
|
||||||
|
margin-bottom:0.8rem;
|
||||||
|
padding-right:2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-v2 .mode-selector-chips{
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:0.5rem;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-v2 .mode-selector-chip{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:0.7rem;
|
||||||
|
padding:0.7rem 1rem;
|
||||||
|
border-radius:14px;
|
||||||
|
border:1px solid rgba(148,163,184,0.25);
|
||||||
|
background:rgba(15,23,42,0.6);
|
||||||
|
color:#e5e7eb;
|
||||||
|
font-size:0.9rem;
|
||||||
|
font-weight:500;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:all 0.2s ease;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-v2 .mode-selector-chip i{
|
||||||
|
font-size:1.15rem;
|
||||||
|
width:24px;
|
||||||
|
text-align:center;
|
||||||
|
color:#38bdf8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-v2 .mode-selector-chip:hover{
|
||||||
|
background:rgba(30,41,59,0.8);
|
||||||
|
border-color:rgba(56,189,248,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector-v2 .mode-selector-chip.is-active{
|
||||||
|
background:rgba(56,189,248,0.2);
|
||||||
|
border-color:rgba(56,189,248,0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px){
|
||||||
|
.mode-selector-v2{
|
||||||
|
min-width:auto;
|
||||||
|
width:calc(100vw - 24px);
|
||||||
|
padding:0.8rem;
|
||||||
|
}
|
||||||
|
.mode-selector-v2 .mode-selector-chip{
|
||||||
|
padding:0.6rem 0.8rem;
|
||||||
|
font-size:0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -496,7 +496,7 @@ body[data-theme="light"] .user-avatar{
|
||||||
.sw-avatar{
|
.sw-avatar{
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
border-radius: 8px;
|
border-radius: 50%;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
background: rgba(56,189,248,0.18);
|
background: rgba(56,189,248,0.18);
|
||||||
|
|
@ -507,6 +507,10 @@ body[data-theme="light"] .user-avatar{
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.sw-avatar i{ font-size: 12px; }
|
.sw-avatar i{ font-size: 12px; }
|
||||||
|
.sw-avatar-guest{
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
.sw-avatar img{
|
.sw-avatar img{
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue