refactor: redesign create post floating UI with compact layout
- Centered stacked layout at screen center - Purple accent color instead of blue - Compact padding and smaller fonts - Glass effect with blur and transparency - Theme support (dark/light/blue) 🤖 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
4705fc03b1
commit
f9d7df0c96
|
|
@ -8,6 +8,7 @@ import ChatWindow from "./ChatWindow";
|
||||||
import "./ChatContainer.css";
|
import "./ChatContainer.css";
|
||||||
|
|
||||||
const ACTIVE_CHATS_KEY = "sociowire:activeChats";
|
const ACTIVE_CHATS_KEY = "sociowire:activeChats";
|
||||||
|
const MINIMIZED_CHATS_KEY = "sociowire:minimizedChats";
|
||||||
|
|
||||||
// Play notification sound using Web Audio API
|
// Play notification sound using Web Audio API
|
||||||
function playNotificationSound() {
|
function playNotificationSound() {
|
||||||
|
|
@ -221,16 +222,23 @@ export default function ChatContainer() {
|
||||||
loadConversations();
|
loadConversations();
|
||||||
}, [authenticated, connected, fetchConversations]);
|
}, [authenticated, connected, fetchConversations]);
|
||||||
|
|
||||||
// Load active chats from localStorage
|
// Load active chats and minimized state from localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem(ACTIVE_CHATS_KEY);
|
const savedChats = localStorage.getItem(ACTIVE_CHATS_KEY);
|
||||||
if (saved) {
|
if (savedChats) {
|
||||||
const parsed = JSON.parse(saved);
|
const parsed = JSON.parse(savedChats);
|
||||||
if (Array.isArray(parsed)) {
|
if (Array.isArray(parsed)) {
|
||||||
setActiveChats(parsed);
|
setActiveChats(parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const savedMinimized = localStorage.getItem(MINIMIZED_CHATS_KEY);
|
||||||
|
if (savedMinimized) {
|
||||||
|
const parsed = JSON.parse(savedMinimized);
|
||||||
|
if (typeof parsed === 'object' && parsed !== null) {
|
||||||
|
setMinimizedChats(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -241,6 +249,24 @@ export default function ChatContainer() {
|
||||||
} catch {}
|
} catch {}
|
||||||
}, [activeChats]);
|
}, [activeChats]);
|
||||||
|
|
||||||
|
// Save minimized state to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(MINIMIZED_CHATS_KEY, JSON.stringify(minimizedChats));
|
||||||
|
} catch {}
|
||||||
|
}, [minimizedChats]);
|
||||||
|
|
||||||
|
// Fetch messages for restored active chats when connection is established
|
||||||
|
const hasLoadedRestoredChats = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (connected && activeChats.length > 0 && !hasLoadedRestoredChats.current) {
|
||||||
|
hasLoadedRestoredChats.current = true;
|
||||||
|
activeChats.forEach((chat) => {
|
||||||
|
fetchMessages(chat.username);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [connected, activeChats, fetchMessages]);
|
||||||
|
|
||||||
// Refresh messages and unread counts when page becomes visible (mobile resume)
|
// Refresh messages and unread counts when page becomes visible (mobile resume)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleVisibilityChange = async () => {
|
const handleVisibilityChange = async () => {
|
||||||
|
|
|
||||||
|
|
@ -212,69 +212,153 @@ export default function MapView({
|
||||||
[mapRef, theme]
|
[mapRef, theme]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Long-press detection for opening create post popup (1 second)
|
// Long-press detection for creating posts (2.5 seconds)
|
||||||
|
// Cancels on: zoom, pan, multi-touch, finger movement > 10px
|
||||||
const longPressTimerRef = useRef(null);
|
const longPressTimerRef = useRef(null);
|
||||||
const longPressStartRef = useRef(null);
|
const longPressStartPos = useRef(null);
|
||||||
const LONG_PRESS_DURATION = 1000; // 1 second
|
const longPressCallbackRef = useRef(null);
|
||||||
|
const LONG_PRESS_DURATION = 1000;
|
||||||
|
const LONG_PRESS_MOVE_THRESHOLD = 10; // pixels
|
||||||
|
|
||||||
const handleLongPressStart = useCallback((e) => {
|
const cancelLongPress = useCallback(() => {
|
||||||
// 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) {
|
if (longPressTimerRef.current) {
|
||||||
clearTimeout(longPressTimerRef.current);
|
clearTimeout(longPressTimerRef.current);
|
||||||
longPressTimerRef.current = null;
|
longPressTimerRef.current = null;
|
||||||
}
|
}
|
||||||
longPressStartRef.current = null;
|
longPressStartPos.current = null;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLongPressMove = useCallback((e) => {
|
// Cancel long press on ANY map interaction
|
||||||
if (!longPressStartRef.current) return;
|
useEffect(() => {
|
||||||
const x = e.clientX || e.touches?.[0]?.clientX;
|
const map = mapRef.current;
|
||||||
const y = e.clientY || e.touches?.[0]?.clientY;
|
if (!map) return;
|
||||||
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]);
|
|
||||||
|
|
||||||
|
const cancel = () => cancelLongPress();
|
||||||
|
|
||||||
|
map.on('zoomstart', cancel);
|
||||||
|
map.on('movestart', cancel);
|
||||||
|
map.on('pitchstart', cancel);
|
||||||
|
map.on('rotatestart', cancel);
|
||||||
|
map.on('zoom', cancel);
|
||||||
|
map.on('move', cancel);
|
||||||
|
map.on('pitch', cancel);
|
||||||
|
map.on('rotate', cancel);
|
||||||
|
map.on('dragstart', cancel);
|
||||||
|
map.on('drag', cancel);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
map.off('zoomstart', cancel);
|
||||||
|
map.off('movestart', cancel);
|
||||||
|
map.off('pitchstart', cancel);
|
||||||
|
map.off('rotatestart', cancel);
|
||||||
|
map.off('zoom', cancel);
|
||||||
|
map.off('move', cancel);
|
||||||
|
map.off('pitch', cancel);
|
||||||
|
map.off('rotate', cancel);
|
||||||
|
map.off('dragstart', cancel);
|
||||||
|
map.off('drag', cancel);
|
||||||
|
};
|
||||||
|
}, [mapRef.current, cancelLongPress]);
|
||||||
|
|
||||||
|
// Touch/mouse event handlers for long-press
|
||||||
|
// Re-run when viewParams changes (indicates map is ready)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
container.addEventListener('mousedown', handleLongPressStart);
|
const handleTouchStart = (e) => {
|
||||||
container.addEventListener('mouseup', handleLongPressEnd);
|
// Multi-touch = cancel immediately
|
||||||
container.addEventListener('mouseleave', handleLongPressEnd);
|
if (e.touches.length !== 1) {
|
||||||
container.addEventListener('mousemove', handleLongPressMove);
|
cancelLongPress();
|
||||||
container.addEventListener('touchstart', handleLongPressStart, { passive: true });
|
return;
|
||||||
container.addEventListener('touchend', handleLongPressEnd);
|
}
|
||||||
container.addEventListener('touchcancel', handleLongPressEnd);
|
|
||||||
container.addEventListener('touchmove', handleLongPressMove, { passive: true });
|
|
||||||
|
|
||||||
return () => {
|
// Ignore UI elements
|
||||||
container.removeEventListener('mousedown', handleLongPressStart);
|
if (e.target.closest('.map-overlay') || e.target.closest('.sw-marker') || e.target.closest('button')) {
|
||||||
container.removeEventListener('mouseup', handleLongPressEnd);
|
return;
|
||||||
container.removeEventListener('mouseleave', handleLongPressEnd);
|
}
|
||||||
container.removeEventListener('mousemove', handleLongPressMove);
|
|
||||||
container.removeEventListener('touchstart', handleLongPressStart);
|
const touch = e.touches[0];
|
||||||
container.removeEventListener('touchend', handleLongPressEnd);
|
longPressStartPos.current = { x: touch.clientX, y: touch.clientY };
|
||||||
container.removeEventListener('touchcancel', handleLongPressEnd);
|
|
||||||
container.removeEventListener('touchmove', handleLongPressMove);
|
longPressTimerRef.current = setTimeout(() => {
|
||||||
if (longPressTimerRef.current) {
|
if (longPressStartPos.current && longPressCallbackRef.current) {
|
||||||
clearTimeout(longPressTimerRef.current);
|
longPressCallbackRef.current();
|
||||||
|
}
|
||||||
|
cancelLongPress();
|
||||||
|
}, LONG_PRESS_DURATION);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchMove = (e) => {
|
||||||
|
// Multi-touch = cancel
|
||||||
|
if (e.touches.length !== 1) {
|
||||||
|
cancelLongPress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if finger moved too far from start position
|
||||||
|
if (longPressStartPos.current && e.touches[0]) {
|
||||||
|
const dx = e.touches[0].clientX - longPressStartPos.current.x;
|
||||||
|
const dy = e.touches[0].clientY - longPressStartPos.current.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (distance > LONG_PRESS_MOVE_THRESHOLD) {
|
||||||
|
cancelLongPress();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [containerRef, handleLongPressStart, handleLongPressEnd, handleLongPressMove]);
|
|
||||||
|
const handleTouchEnd = () => cancelLongPress();
|
||||||
|
|
||||||
|
const handleMouseDown = (e) => {
|
||||||
|
if (e.target.closest('.map-overlay') || e.target.closest('.sw-marker') || e.target.closest('button')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
longPressStartPos.current = { x: e.clientX, y: e.clientY };
|
||||||
|
longPressTimerRef.current = setTimeout(() => {
|
||||||
|
if (longPressStartPos.current && longPressCallbackRef.current) {
|
||||||
|
longPressCallbackRef.current();
|
||||||
|
}
|
||||||
|
cancelLongPress();
|
||||||
|
}, LONG_PRESS_DURATION);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e) => {
|
||||||
|
if (longPressStartPos.current) {
|
||||||
|
const dx = e.clientX - longPressStartPos.current.x;
|
||||||
|
const dy = e.clientY - longPressStartPos.current.y;
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
if (distance > LONG_PRESS_MOVE_THRESHOLD) {
|
||||||
|
cancelLongPress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => cancelLongPress();
|
||||||
|
const handleWheel = () => cancelLongPress();
|
||||||
|
|
||||||
|
container.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||||
|
container.addEventListener('touchmove', handleTouchMove, { passive: true });
|
||||||
|
container.addEventListener('touchend', handleTouchEnd);
|
||||||
|
container.addEventListener('touchcancel', handleTouchEnd);
|
||||||
|
container.addEventListener('mousedown', handleMouseDown);
|
||||||
|
container.addEventListener('mousemove', handleMouseMove);
|
||||||
|
container.addEventListener('mouseup', handleMouseUp);
|
||||||
|
container.addEventListener('mouseleave', handleMouseUp);
|
||||||
|
container.addEventListener('wheel', handleWheel, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener('touchstart', handleTouchStart);
|
||||||
|
container.removeEventListener('touchmove', handleTouchMove);
|
||||||
|
container.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
container.removeEventListener('touchcancel', handleTouchEnd);
|
||||||
|
container.removeEventListener('mousedown', handleMouseDown);
|
||||||
|
container.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
container.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
container.removeEventListener('mouseleave', handleMouseUp);
|
||||||
|
container.removeEventListener('wheel', handleWheel);
|
||||||
|
cancelLongPress();
|
||||||
|
};
|
||||||
|
}, [viewParams, cancelLongPress]);
|
||||||
|
|
||||||
const fitBoundsWhenReady = useCallback(
|
const fitBoundsWhenReady = useCallback(
|
||||||
(bounds, opts = {}) => {
|
(bounds, opts = {}) => {
|
||||||
|
|
@ -1198,6 +1282,11 @@ export default function MapView({
|
||||||
setModeChosen(false);
|
setModeChosen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update ref for long-press callback
|
||||||
|
useEffect(() => {
|
||||||
|
longPressCallbackRef.current = handleOpenCreate;
|
||||||
|
}, [authenticated, needsEmailVerification, mainFilter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setUseCustomImage(contentMode !== "link");
|
setUseCustomImage(contentMode !== "link");
|
||||||
}, [contentMode]);
|
}, [contentMode]);
|
||||||
|
|
@ -1735,6 +1824,8 @@ export default function MapView({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{modeSelectorVisible && (
|
{modeSelectorVisible && (
|
||||||
|
<>
|
||||||
|
<div className="mode-selector-backdrop" onClick={closeCreateModal} />
|
||||||
<div className="map-overlay mode-selector-widget mode-selector-v2">
|
<div className="map-overlay mode-selector-widget mode-selector-v2">
|
||||||
<button className="mode-selector-close" onClick={closeCreateModal} title="Close">
|
<button className="mode-selector-close" onClick={closeCreateModal} title="Close">
|
||||||
<i className="fa-solid fa-xmark" />
|
<i className="fa-solid fa-xmark" />
|
||||||
|
|
@ -1756,255 +1847,143 @@ export default function MapView({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCreating && modeChosen && (
|
{isCreating && modeChosen && (
|
||||||
<div className="map-overlay create-panel-compact create-panel-v2">
|
<div className="create-floating-ui">
|
||||||
<button className="create-close-btn" onClick={closeCreateModal} title="Close">
|
{/* Close button */}
|
||||||
|
<button className="cfloat-close" onClick={closeCreateModal}>
|
||||||
<i className="fa-solid fa-xmark" />
|
<i className="fa-solid fa-xmark" />
|
||||||
</button>
|
</button>
|
||||||
<div className="create-panel-header">
|
|
||||||
<i className={currentContentMode.icon + " create-header-icon"} />
|
{/* Step indicator */}
|
||||||
<span>{currentContentMode.label}</span>
|
<div className="cfloat-box cfloat-step">
|
||||||
|
<i className={currentContentMode.icon} />
|
||||||
|
<span>Step {createStep + 1}/3</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="create-panel-body">
|
|
||||||
<div className="create-step-label">Step {createStep + 1} / 3</div>
|
{/* STEP 0 - Position hint */}
|
||||||
<p className="create-panel-hero">{currentFlow.hero}</p>
|
|
||||||
<div className="create-panel-step">
|
|
||||||
{createStep === 0 && (
|
{createStep === 0 && (
|
||||||
<p className="create-panel-note">{currentFlow.hint}</p>
|
<div className="cfloat-box cfloat-hint">
|
||||||
|
<p>{currentFlow.hint}</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* STEP 1 - Category */}
|
||||||
{createStep === 1 && (
|
{createStep === 1 && (
|
||||||
<>
|
<div className="cfloat-box cfloat-category">
|
||||||
<div className="create-panel-field">
|
|
||||||
<label>Category</label>
|
<label>Category</label>
|
||||||
<div className="create-panel-selects">
|
<div className="cfloat-selects">
|
||||||
<select
|
<select value={draftCategory} onChange={(e) => {
|
||||||
value={draftCategory}
|
setDraftCategory(e.target.value);
|
||||||
onChange={(e) => {
|
setDraftSubCategory((CREATE_CATEGORY_MAP[e.target.value] || CREATE_CATEGORY_MAP.News)[0]);
|
||||||
const cat = e.target.value;
|
}}>
|
||||||
setDraftCategory(cat);
|
{['News', 'Friends', 'Events', 'Market'].map((c) => <option key={c}>{c}</option>)}
|
||||||
setDraftSubCategory(
|
|
||||||
(CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News)[0]
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{['News', 'Friends', 'Events', 'Market'].map((cat) => (
|
|
||||||
<option key={cat}>{cat}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select value={draftSubCategory} onChange={(e) => setDraftSubCategory(e.target.value)}>
|
||||||
value={draftSubCategory}
|
{(CREATE_CATEGORY_MAP[draftCategory] || CREATE_CATEGORY_MAP.News).map((s) => <option key={s}>{s}</option>)}
|
||||||
onChange={(e) => setDraftSubCategory(e.target.value)}
|
|
||||||
>
|
|
||||||
{(CREATE_CATEGORY_MAP[draftCategory] ||
|
|
||||||
CREATE_CATEGORY_MAP.News
|
|
||||||
).map((sub) => (
|
|
||||||
<option key={sub}>{sub}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="create-panel-field">
|
)}
|
||||||
<label>Title or link</label>
|
|
||||||
|
{/* STEP 1 - Title */}
|
||||||
|
{createStep === 1 && (
|
||||||
|
<div className="cfloat-box cfloat-title">
|
||||||
|
<label>Title or Link</label>
|
||||||
<input
|
<input
|
||||||
className="create-input create-input-narrow"
|
|
||||||
placeholder={currentFlow.placeholder}
|
placeholder={currentFlow.placeholder}
|
||||||
value={draftTitle}
|
value={draftTitle}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const next = e.target.value;
|
const v = e.target.value;
|
||||||
if (isLikelyUrl(next)) {
|
if (isLikelyUrl(v)) {
|
||||||
setDraftTitle(next);
|
setDraftTitle(v); setTitleTouched(false); setDraftUrl(v.trim());
|
||||||
setTitleTouched(false);
|
setUrlPreviewError(""); setDraftSource(""); setDraftPublishedAt("");
|
||||||
setDraftUrl(next.trim());
|
handleUrlPreview(v);
|
||||||
setUrlPreviewError("");
|
} else {
|
||||||
setDraftSource("");
|
setDraftTitle(v); setTitleTouched(true);
|
||||||
setDraftPublishedAt("");
|
|
||||||
handleUrlPreview(next);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
setDraftTitle(next);
|
|
||||||
setTitleTouched(true);
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="create-hint create-hint-tight">
|
{draftUrl && !draftSource && <span className="cfloat-meta">Fetching preview...</span>}
|
||||||
{currentFlow.hint}
|
{draftSource && <span className="cfloat-meta">{draftSource}</span>}
|
||||||
</div>
|
{urlPreviewError && <span className="cfloat-error">{urlPreviewError}</span>}
|
||||||
{draftUrl && !draftSource && !draftPublishedAt && (
|
|
||||||
<div className="create-link-meta">
|
|
||||||
<span>Link detected · fetching preview...</span>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(draftSource || draftPublishedAt) && (
|
|
||||||
<div className="create-link-meta">
|
{/* STEP 2 - Preview */}
|
||||||
{draftSource ? <span>{draftSource}</span> : null}
|
{createStep === 2 && shouldShowPreviewCard && (
|
||||||
{draftPublishedAt ? (
|
<div className="cfloat-box cfloat-preview">
|
||||||
<span>{formatPreviewDate(draftPublishedAt)}</span>
|
<img src={galleryPreviewImage || LINK_PREVIEW_PLACEHOLDER} alt="Preview" />
|
||||||
) : null}
|
{draftSource && <span>{draftSource}</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{urlPreviewError && (
|
|
||||||
<div className="create-link-error">{urlPreviewError}</div>
|
{/* STEP 2 - Description */}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{createStep === 2 && (
|
{createStep === 2 && (
|
||||||
<>
|
<div className="cfloat-box cfloat-desc">
|
||||||
{shouldShowPreviewCard && (
|
<label>Description</label>
|
||||||
<div className="create-link-preview-card">
|
|
||||||
<img
|
|
||||||
src={galleryPreviewImage || LINK_PREVIEW_PLACEHOLDER}
|
|
||||||
alt="Post preview"
|
|
||||||
/>
|
|
||||||
<div className="create-link-preview-meta">
|
|
||||||
{draftSource ? <span>{draftSource}</span> : null}
|
|
||||||
{draftPublishedAt ? (
|
|
||||||
<span>{formatPreviewDate(draftPublishedAt)}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{galleryThumbnails.length > 0 && (
|
|
||||||
<div className="create-gallery-row create-gallery-row--bottom">
|
|
||||||
{galleryThumbnails.map((url, idx) => (
|
|
||||||
<figure
|
|
||||||
key={`${url}-${idx}`}
|
|
||||||
className="create-gallery-item create-gallery-static"
|
|
||||||
>
|
|
||||||
<img src={url} alt={`Preview image ${idx + 2}`} />
|
|
||||||
</figure>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<textarea
|
<textarea
|
||||||
className="create-textarea create-textarea-tight"
|
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder={currentFlow.bodyPlaceholder}
|
placeholder={currentFlow.bodyPlaceholder}
|
||||||
value={draftBody}
|
value={draftBody}
|
||||||
onChange={(e) => {
|
onChange={(e) => { setDraftBody(e.target.value); setBodyTouched(true); }}
|
||||||
setDraftBody(e.target.value);
|
|
||||||
setBodyTouched(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div className="create-image-choice">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={
|
|
||||||
"create-image-choice-btn" + (!useCustomImage ? " is-active" : "")
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setUseCustomImage(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Use category icon
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={
|
|
||||||
"create-image-choice-btn" + (useCustomImage ? " is-active" : "")
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setUseCustomImage(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Add an image
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="create-muted">
|
)}
|
||||||
We’ll use the category icon if you skip images.
|
|
||||||
|
{/* STEP 2 - Image */}
|
||||||
|
{createStep === 2 && (
|
||||||
|
<div className="cfloat-box cfloat-image">
|
||||||
|
<div className="cfloat-img-toggle">
|
||||||
|
<button className={!useCustomImage ? "active" : ""} onClick={() => setUseCustomImage(false)}>
|
||||||
|
<i className="fa-solid fa-icons" /> Icon
|
||||||
|
</button>
|
||||||
|
<button className={useCustomImage ? "active" : ""} onClick={() => setUseCustomImage(true)}>
|
||||||
|
<i className="fa-solid fa-image" /> Image
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{useCustomImage && (
|
{useCustomImage && (
|
||||||
<div className="create-image-panel">
|
<div className="cfloat-img-upload">
|
||||||
<div className="create-image-row">
|
<label>
|
||||||
<label className="create-file-btn">
|
<i className="fa-solid fa-upload" /> Upload
|
||||||
Choose file
|
<input type="file" accept="image/*" multiple onChange={handleImageFileChange} disabled={uploadingImage} hidden />
|
||||||
<input
|
|
||||||
className="create-file-input"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
multiple
|
|
||||||
onChange={handleImageFileChange}
|
|
||||||
disabled={uploadingImage}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
<label className="create-file-btn create-file-btn--camera">
|
<label>
|
||||||
Take photo
|
<i className="fa-solid fa-camera" /> Camera
|
||||||
<input
|
<input type="file" accept="image/*" capture="environment" onChange={handleImageFileChange} disabled={uploadingImage} hidden />
|
||||||
className="create-file-input"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
capture="environment"
|
|
||||||
multiple
|
|
||||||
onChange={handleImageFileChange}
|
|
||||||
disabled={uploadingImage}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
{uploadingImage && (
|
{uploadingImage && <span>Uploading...</span>}
|
||||||
<span className="create-uploading">Uploading...</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{!hasCustomImages && (
|
|
||||||
<>
|
|
||||||
<div className="create-muted">Or paste an image URL:</div>
|
|
||||||
<div className="create-image-url-row">
|
|
||||||
<input
|
|
||||||
className="create-input"
|
|
||||||
placeholder="https://example.com/image.jpg"
|
|
||||||
value={draftImage}
|
|
||||||
onChange={(e) => {
|
|
||||||
setDraftImage(e.target.value);
|
|
||||||
setDraftImageFile(null);
|
|
||||||
setDraftImagePreview("");
|
|
||||||
setImageTouched(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="create-gallery-add"
|
|
||||||
onClick={handleAddCustomImageUrl}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
{/* Navigation buttons */}
|
||||||
</div>
|
<div className="cfloat-box cfloat-nav">
|
||||||
<div className="create-panel-actions">
|
<button className="cfloat-btn" onClick={handlePrevStep} disabled={createStep === 0}>
|
||||||
<button
|
<i className="fa-solid fa-arrow-left" /> Back
|
||||||
type="button"
|
|
||||||
className="chip-pill create-panel-btn"
|
|
||||||
onClick={handlePrevStep}
|
|
||||||
disabled={createStep === 0}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
className="cfloat-btn cfloat-btn-primary"
|
||||||
className="chip-pill create-panel-btn primary"
|
disabled={isSaving}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// For live mode, open broadcast modal after step 0 (crosshair placement)
|
|
||||||
if (contentMode === "live" && createStep === 0) {
|
if (contentMode === "live" && createStep === 0) {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (map) {
|
if (map) {
|
||||||
const stage = stageRef.current;
|
const stage = stageRef.current;
|
||||||
const crosshairEl = stage ? stage.querySelector(".crosshair-aim") : null;
|
const crosshairEl = stage?.querySelector(".crosshair-aim");
|
||||||
const containerEl = map.getContainer();
|
const containerEl = map.getContainer();
|
||||||
let coords = null;
|
let coords;
|
||||||
if (crosshairEl && containerEl) {
|
if (crosshairEl && containerEl) {
|
||||||
const crossRect = crosshairEl.getBoundingClientRect();
|
const cr = crosshairEl.getBoundingClientRect();
|
||||||
const containerRect = containerEl.getBoundingClientRect();
|
const co = containerEl.getBoundingClientRect();
|
||||||
const px = crossRect.left + crossRect.width / 2 - containerRect.left;
|
const ll = map.unproject([cr.left + cr.width/2 - co.left, cr.top + cr.height/2 - co.top]);
|
||||||
const py = crossRect.top + crossRect.height / 2 - containerRect.top;
|
coords = [ll.lng, ll.lat];
|
||||||
const correctedLngLat = map.unproject([px, py]);
|
|
||||||
coords = [correctedLngLat.lng, correctedLngLat.lat];
|
|
||||||
} else {
|
} else {
|
||||||
const center = map.getCenter();
|
const c = map.getCenter();
|
||||||
coords = [center.lng, center.lat];
|
coords = [c.lng, c.lat];
|
||||||
}
|
}
|
||||||
setLiveCoords(coords);
|
setLiveCoords(coords);
|
||||||
setShowLiveBroadcast(true);
|
setShowLiveBroadcast(true);
|
||||||
|
|
@ -2013,19 +1992,15 @@ export default function MapView({
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (createStep < 2) {
|
if (createStep < 2) setCreateStep((p) => p + 1);
|
||||||
setCreateStep((prev) => prev + 1);
|
else handleSubmitPost();
|
||||||
} else {
|
|
||||||
handleSubmitPost();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
disabled={isSaving}
|
|
||||||
>
|
>
|
||||||
{createStep === 2 ? (isSaving ? "Posting..." : "Post") : (contentMode === "live" && createStep === 0 ? "Go Live" : "Next")}
|
{createStep === 2 ? (isSaving ? "Posting..." : "Post") : (contentMode === "live" && createStep === 0 ? "Go Live" : "Next")}
|
||||||
|
<i className="fa-solid fa-arrow-right" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1351,352 +1351,408 @@ body[data-theme="light"] .map-action-btn * {
|
||||||
pointer-events:none;
|
pointer-events:none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== NEW CREATE PANEL V2 - IMPROVED DESIGN ===== */
|
/* ===== FLOATING CREATE UI ===== */
|
||||||
|
.create-floating-ui{
|
||||||
|
--cf-bg: rgba(15,20,30,0.82);
|
||||||
|
--cf-border: rgba(100,116,139,0.3);
|
||||||
|
--cf-text: #f1f5f9;
|
||||||
|
--cf-muted: #94a3b8;
|
||||||
|
--cf-accent: #a78bfa;
|
||||||
|
--cf-glow: rgba(167,139,250,0.15);
|
||||||
|
--cf-input: rgba(30,41,59,0.85);
|
||||||
|
|
||||||
.create-panel-v2{
|
position:fixed;
|
||||||
position:absolute;
|
bottom:50%;
|
||||||
left:50%;
|
left:50%;
|
||||||
top:50%;
|
transform:translate(-50%, 50%);
|
||||||
transform:translate(-50%,-50%);
|
z-index:9000;
|
||||||
width:min(340px, 92vw);
|
display:flex;
|
||||||
max-height:80vh;
|
flex-direction:column;
|
||||||
overflow:auto;
|
gap:4px;
|
||||||
background: linear-gradient(145deg, rgba(15,23,42,0.97), rgba(7,10,14,0.98));
|
width:min(340px, calc(100vw - 32px));
|
||||||
backdrop-filter: blur(24px);
|
pointer-events:none;
|
||||||
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{
|
body[data-theme="light"] .create-floating-ui{
|
||||||
|
--cf-bg: rgba(255,255,255,0.88);
|
||||||
|
--cf-border: rgba(148,163,184,0.35);
|
||||||
|
--cf-text: #1e293b;
|
||||||
|
--cf-muted: #64748b;
|
||||||
|
--cf-accent: #7c3aed;
|
||||||
|
--cf-glow: rgba(124,58,237,0.1);
|
||||||
|
--cf-input: rgba(241,245,249,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="blue"] .create-floating-ui{
|
||||||
|
--cf-bg: rgba(15,23,42,0.85);
|
||||||
|
--cf-border: rgba(71,85,105,0.35);
|
||||||
|
--cf-text: #e2e8f0;
|
||||||
|
--cf-muted: #94a3b8;
|
||||||
|
--cf-accent: #a78bfa;
|
||||||
|
--cf-glow: rgba(167,139,250,0.12);
|
||||||
|
--cf-input: rgba(30,41,59,0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Close button - inline top right */
|
||||||
|
.cfloat-close{
|
||||||
|
position:absolute;
|
||||||
|
top:-32px;
|
||||||
|
right:0;
|
||||||
|
width:28px;
|
||||||
|
height:28px;
|
||||||
|
border-radius:8px;
|
||||||
|
background: var(--cf-bg);
|
||||||
|
border:1px solid var(--cf-border);
|
||||||
|
color: var(--cf-muted);
|
||||||
|
font-size:0.85rem;
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
justify-content:center;
|
justify-content:center;
|
||||||
gap:0.6rem;
|
cursor:pointer;
|
||||||
padding: 1rem 1.2rem 0.6rem;
|
pointer-events:auto;
|
||||||
font-size:1rem;
|
backdrop-filter:blur(20px) saturate(1.2);
|
||||||
font-weight:700;
|
-webkit-backdrop-filter:blur(20px) saturate(1.2);
|
||||||
letter-spacing:0.02em;
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
text-transform:none;
|
transition:all 0.2s;
|
||||||
color:#f8fafc;
|
|
||||||
border-bottom: 1px solid rgba(148,163,184,0.15);
|
|
||||||
margin-bottom:0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-panel-v2 .create-header-icon{
|
.cfloat-close:hover{
|
||||||
font-size:1.1rem;
|
background:rgba(239,68,68,0.15);
|
||||||
color:#38bdf8;
|
border-color:rgba(239,68,68,0.5);
|
||||||
|
color:#ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-panel-v2 .create-panel-body{
|
/* Box base - stacked */
|
||||||
padding: 0.8rem 1rem 1rem;
|
.cfloat-box{
|
||||||
|
background: var(--cf-bg);
|
||||||
|
border:1px solid var(--cf-border);
|
||||||
|
border-radius:10px;
|
||||||
|
padding:6px 10px;
|
||||||
|
backdrop-filter:blur(20px) saturate(1.2);
|
||||||
|
-webkit-backdrop-filter:blur(20px) saturate(1.2);
|
||||||
|
pointer-events:auto;
|
||||||
|
color: var(--cf-text);
|
||||||
|
animation: cfloat-in 0.2s ease-out;
|
||||||
|
box-shadow: 0 2px 12px rgba(0,0,0,0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes cfloat-in{
|
||||||
|
from{ opacity:0; transform:translateY(8px); }
|
||||||
|
to{ opacity:1; transform:translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step indicator */
|
||||||
|
.cfloat-step{
|
||||||
display:flex;
|
display:flex;
|
||||||
flex-direction:column;
|
align-items:center;
|
||||||
gap:0.7rem;
|
justify-content:center;
|
||||||
}
|
gap:6px;
|
||||||
|
font-weight:600;
|
||||||
.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;
|
font-size:0.8rem;
|
||||||
color:#94a3b8;
|
padding:4px 8px;
|
||||||
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{
|
.cfloat-step i{
|
||||||
display:flex;
|
color: var(--cf-accent);
|
||||||
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;
|
font-size:0.9rem;
|
||||||
transition:border-color 0.2s ease, box-shadow 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-panel-v2 .create-input-narrow:focus{
|
/* Hint */
|
||||||
outline:none;
|
.cfloat-hint{
|
||||||
border-color:rgba(56,189,248,0.6);
|
text-align:center;
|
||||||
box-shadow:0 0 0 3px rgba(56,189,248,0.12);
|
padding:6px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-panel-v2 .create-input-narrow::placeholder{
|
.cfloat-hint p{
|
||||||
color:#64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.create-panel-v2 .create-hint-tight{
|
|
||||||
font-size:0.75rem;
|
|
||||||
color:#64748b;
|
|
||||||
margin:0;
|
margin:0;
|
||||||
|
font-size:0.82rem;
|
||||||
|
line-height:1.3;
|
||||||
|
color: var(--cf-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-panel-v2 .create-textarea-tight{
|
/* Category */
|
||||||
background:rgba(15,23,42,0.8);
|
.cfloat-category label{
|
||||||
border:1px solid rgba(148,163,184,0.3);
|
display:block;
|
||||||
color:#e5e7eb;
|
font-size:0.65rem;
|
||||||
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;
|
font-weight:600;
|
||||||
border-radius:14px;
|
text-transform:uppercase;
|
||||||
border:1px solid rgba(148,163,184,0.3);
|
letter-spacing:0.05em;
|
||||||
background:rgba(15,23,42,0.6);
|
color: var(--cf-muted);
|
||||||
color:#e5e7eb;
|
margin-bottom:4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-selects{
|
||||||
|
display:flex;
|
||||||
|
gap:4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-selects select{
|
||||||
|
flex:1;
|
||||||
|
background: var(--cf-input);
|
||||||
|
color: var(--cf-text);
|
||||||
|
border:1px solid var(--cf-border);
|
||||||
|
border-radius:6px;
|
||||||
|
padding:5px 8px;
|
||||||
|
font-size:0.8rem;
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
transition:all 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-panel-v2 .create-panel-btn:hover:not(:disabled){
|
.cfloat-selects select:focus{
|
||||||
background:rgba(30,41,59,0.8);
|
outline:none;
|
||||||
border-color:rgba(148,163,184,0.5);
|
border-color: var(--cf-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-panel-v2 .create-panel-btn:disabled{
|
/* Title */
|
||||||
|
.cfloat-title label{
|
||||||
|
display:block;
|
||||||
|
font-size:0.65rem;
|
||||||
|
font-weight:600;
|
||||||
|
text-transform:uppercase;
|
||||||
|
letter-spacing:0.05em;
|
||||||
|
color: var(--cf-muted);
|
||||||
|
margin-bottom:4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-title input{
|
||||||
|
width:100%;
|
||||||
|
background: var(--cf-input);
|
||||||
|
color: var(--cf-text);
|
||||||
|
border:1px solid var(--cf-border);
|
||||||
|
border-radius:6px;
|
||||||
|
padding:6px 10px;
|
||||||
|
font-size:0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-title input:focus{
|
||||||
|
outline:none;
|
||||||
|
border-color: var(--cf-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-meta{
|
||||||
|
display:block;
|
||||||
|
margin-top:3px;
|
||||||
|
font-size:0.65rem;
|
||||||
|
color: var(--cf-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-error{
|
||||||
|
display:block;
|
||||||
|
margin-top:3px;
|
||||||
|
font-size:0.65rem;
|
||||||
|
color:#ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview - compact */
|
||||||
|
.cfloat-preview{
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
gap:8px;
|
||||||
|
padding:4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-preview img{
|
||||||
|
width:50px;
|
||||||
|
height:38px;
|
||||||
|
object-fit:cover;
|
||||||
|
border-radius:5px;
|
||||||
|
flex-shrink:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-preview span{
|
||||||
|
font-size:0.7rem;
|
||||||
|
color: var(--cf-muted);
|
||||||
|
overflow:hidden;
|
||||||
|
text-overflow:ellipsis;
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Description */
|
||||||
|
.cfloat-desc label{
|
||||||
|
display:block;
|
||||||
|
font-size:0.65rem;
|
||||||
|
font-weight:600;
|
||||||
|
text-transform:uppercase;
|
||||||
|
letter-spacing:0.05em;
|
||||||
|
color: var(--cf-muted);
|
||||||
|
margin-bottom:4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-desc textarea{
|
||||||
|
width:100%;
|
||||||
|
background: var(--cf-input);
|
||||||
|
color: var(--cf-text);
|
||||||
|
border:1px solid var(--cf-border);
|
||||||
|
border-radius:6px;
|
||||||
|
padding:5px 8px;
|
||||||
|
font-size:0.8rem;
|
||||||
|
resize:none;
|
||||||
|
height:50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-desc textarea:focus{
|
||||||
|
outline:none;
|
||||||
|
border-color: var(--cf-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image */
|
||||||
|
.cfloat-image{
|
||||||
|
padding:4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-img-toggle{
|
||||||
|
display:flex;
|
||||||
|
gap:4px;
|
||||||
|
margin-bottom:6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-img-toggle button{
|
||||||
|
flex:1;
|
||||||
|
padding:4px 6px;
|
||||||
|
border-radius:5px;
|
||||||
|
border:1px solid var(--cf-border);
|
||||||
|
background:transparent;
|
||||||
|
color: var(--cf-muted);
|
||||||
|
font-size:0.7rem;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-img-toggle button.active{
|
||||||
|
background: var(--cf-accent);
|
||||||
|
border-color: var(--cf-accent);
|
||||||
|
color:#fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-img-toggle button i{
|
||||||
|
margin-right:3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-img-upload{
|
||||||
|
display:flex;
|
||||||
|
gap:6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-img-upload label{
|
||||||
|
flex:1;
|
||||||
|
padding:8px 6px;
|
||||||
|
border-radius:6px;
|
||||||
|
border:1px dashed var(--cf-border);
|
||||||
|
background: var(--cf-input);
|
||||||
|
color: var(--cf-muted);
|
||||||
|
font-size:0.7rem;
|
||||||
|
text-align:center;
|
||||||
|
cursor:pointer;
|
||||||
|
transition:all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-img-upload label:hover{
|
||||||
|
border-color: var(--cf-accent);
|
||||||
|
color: var(--cf-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-img-upload label i{
|
||||||
|
display:block;
|
||||||
|
font-size:1rem;
|
||||||
|
margin-bottom:2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.cfloat-nav{
|
||||||
|
display:flex;
|
||||||
|
gap:6px;
|
||||||
|
padding:4px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-btn{
|
||||||
|
flex:1;
|
||||||
|
padding:6px 12px;
|
||||||
|
border-radius:6px;
|
||||||
|
border:1px solid var(--cf-border);
|
||||||
|
background: var(--cf-input);
|
||||||
|
color: var(--cf-text);
|
||||||
|
font-size:0.78rem;
|
||||||
|
font-weight:600;
|
||||||
|
cursor:pointer;
|
||||||
|
display:flex;
|
||||||
|
align-items:center;
|
||||||
|
justify-content:center;
|
||||||
|
gap:5px;
|
||||||
|
transition:all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cfloat-btn:disabled{
|
||||||
opacity:0.4;
|
opacity:0.4;
|
||||||
cursor:not-allowed;
|
cursor:not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-panel-v2 .create-panel-btn.primary{
|
.cfloat-btn:hover:not(:disabled){
|
||||||
background:linear-gradient(135deg, #38bdf8, #22d3ee);
|
border-color: var(--cf-accent);
|
||||||
border:none;
|
}
|
||||||
|
|
||||||
|
.cfloat-btn-primary{
|
||||||
|
background: var(--cf-accent);
|
||||||
|
border-color: var(--cf-accent);
|
||||||
color:#0f172a;
|
color:#0f172a;
|
||||||
box-shadow:0 4px 14px rgba(56,189,248,0.35);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.create-panel-v2 .create-panel-btn.primary:hover:not(:disabled){
|
.cfloat-btn-primary:hover:not(:disabled){
|
||||||
background:linear-gradient(135deg, #7dd3fc, #67e8f9);
|
filter:brightness(1.1);
|
||||||
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 - IMPROVED DESIGN ===== */
|
||||||
|
|
||||||
|
.mode-selector-backdrop{
|
||||||
|
position:fixed;
|
||||||
|
inset:0;
|
||||||
|
background:rgba(0,0,0,0.5);
|
||||||
|
z-index:9998;
|
||||||
|
backdrop-filter:blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
.mode-selector-v2{
|
.mode-selector-v2{
|
||||||
|
--ms-bg: rgba(8,12,24,0.95);
|
||||||
|
--ms-glass: rgba(15,23,42,0.85);
|
||||||
|
--ms-border: rgba(56,189,248,0.3);
|
||||||
|
--ms-text: #f1f5f9;
|
||||||
|
--ms-text-muted: #94a3b8;
|
||||||
|
--ms-accent: #38bdf8;
|
||||||
|
--ms-glow: rgba(56,189,248,0.25);
|
||||||
|
|
||||||
position:absolute;
|
position:absolute;
|
||||||
left:50%;
|
left:50%;
|
||||||
top:50%;
|
top:50%;
|
||||||
transform:translate(-50%,-50%);
|
transform:translate(-50%,-50%);
|
||||||
pointer-events:auto;
|
pointer-events:auto;
|
||||||
background: linear-gradient(145deg, rgba(15,23,42,0.97), rgba(7,10,14,0.98));
|
background: var(--ms-bg);
|
||||||
backdrop-filter: blur(24px);
|
backdrop-filter: blur(20px);
|
||||||
border-radius:24px;
|
border-radius:18px;
|
||||||
border:1px solid rgba(56,189,248,0.35);
|
border:1px solid var(--ms-border);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 25px 60px rgba(0,0,0,0.65),
|
0 25px 60px rgba(0,0,0,0.5),
|
||||||
0 0 40px rgba(56,189,248,0.15),
|
0 0 40px var(--ms-glow);
|
||||||
inset 0 1px 0 rgba(255,255,255,0.08);
|
|
||||||
padding:1rem;
|
padding:1rem;
|
||||||
min-width:280px;
|
min-width:260px;
|
||||||
|
z-index:9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="light"] .mode-selector-v2{
|
||||||
|
--ms-bg: rgba(248,250,252,0.97);
|
||||||
|
--ms-glass: rgba(255,255,255,0.9);
|
||||||
|
--ms-border: rgba(148,163,184,0.5);
|
||||||
|
--ms-text: #1e293b;
|
||||||
|
--ms-text-muted: #475569;
|
||||||
|
--ms-accent: #0ea5e9;
|
||||||
|
--ms-glow: rgba(14,165,233,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="blue"] .mode-selector-v2{
|
||||||
|
--ms-bg: rgba(15,23,42,0.97);
|
||||||
|
--ms-glass: rgba(30,41,59,0.9);
|
||||||
|
--ms-border: rgba(71,85,105,0.5);
|
||||||
|
--ms-text: #e2e8f0;
|
||||||
|
--ms-text-muted: #94a3b8;
|
||||||
|
--ms-accent: #60a5fa;
|
||||||
|
--ms-glow: rgba(96,165,250,0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-selector-v2 .mode-selector-bg{
|
.mode-selector-v2 .mode-selector-bg{
|
||||||
|
|
@ -1707,13 +1763,13 @@ body[data-theme="light"] .map-action-btn * {
|
||||||
position:absolute;
|
position:absolute;
|
||||||
top:0.6rem;
|
top:0.6rem;
|
||||||
right:0.6rem;
|
right:0.6rem;
|
||||||
width:32px;
|
width:28px;
|
||||||
height:32px;
|
height:28px;
|
||||||
border-radius:50%;
|
border-radius:50%;
|
||||||
border:1px solid rgba(148,163,184,0.3);
|
border:1px solid var(--ms-border);
|
||||||
background:rgba(15,23,42,0.8);
|
background: var(--ms-glass);
|
||||||
color:#94a3b8;
|
color: var(--ms-text-muted);
|
||||||
font-size:1rem;
|
font-size:0.9rem;
|
||||||
display:flex;
|
display:flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
justify-content:center;
|
justify-content:center;
|
||||||
|
|
@ -1731,11 +1787,11 @@ body[data-theme="light"] .map-action-btn * {
|
||||||
|
|
||||||
.mode-selector-title{
|
.mode-selector-title{
|
||||||
text-align:center;
|
text-align:center;
|
||||||
font-size:1.1rem;
|
font-size:1rem;
|
||||||
font-weight:700;
|
font-weight:700;
|
||||||
color:#f8fafc;
|
color: var(--ms-text);
|
||||||
margin-bottom:0.8rem;
|
margin-bottom:0.8rem;
|
||||||
padding-right:2rem;
|
padding-right:1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-selector-v2 .mode-selector-chips{
|
.mode-selector-v2 .mode-selector-chips{
|
||||||
|
|
@ -1750,10 +1806,10 @@ body[data-theme="light"] .map-action-btn * {
|
||||||
align-items:center;
|
align-items:center;
|
||||||
gap:0.7rem;
|
gap:0.7rem;
|
||||||
padding:0.7rem 1rem;
|
padding:0.7rem 1rem;
|
||||||
border-radius:14px;
|
border-radius:12px;
|
||||||
border:1px solid rgba(148,163,184,0.25);
|
border:1px solid var(--ms-border);
|
||||||
background:rgba(15,23,42,0.6);
|
background: var(--ms-glass);
|
||||||
color:#e5e7eb;
|
color: var(--ms-text);
|
||||||
font-size:0.9rem;
|
font-size:0.9rem;
|
||||||
font-weight:500;
|
font-weight:500;
|
||||||
cursor:pointer;
|
cursor:pointer;
|
||||||
|
|
@ -1762,20 +1818,20 @@ body[data-theme="light"] .map-action-btn * {
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-selector-v2 .mode-selector-chip i{
|
.mode-selector-v2 .mode-selector-chip i{
|
||||||
font-size:1.15rem;
|
font-size:1.1rem;
|
||||||
width:24px;
|
width:22px;
|
||||||
text-align:center;
|
text-align:center;
|
||||||
color:#38bdf8;
|
color: var(--ms-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-selector-v2 .mode-selector-chip:hover{
|
.mode-selector-v2 .mode-selector-chip:hover{
|
||||||
background:rgba(30,41,59,0.8);
|
border-color: var(--ms-accent);
|
||||||
border-color:rgba(56,189,248,0.4);
|
transform:translateX(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mode-selector-v2 .mode-selector-chip.is-active{
|
.mode-selector-v2 .mode-selector-chip.is-active{
|
||||||
background:rgba(56,189,248,0.2);
|
background: var(--ms-glow);
|
||||||
border-color:rgba(56,189,248,0.6);
|
border-color: var(--ms-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 360px){
|
@media (max-width: 360px){
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue