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:
SocioWire 2026-01-10 02:34:42 +00:00
parent 4705fc03b1
commit f9d7df0c96
3 changed files with 703 additions and 646 deletions

View File

@ -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 () => {

View File

@ -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"> )}
Well 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>

View File

@ -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){