diff --git a/packages/ui/src/components/ImageLightbox.tsx b/packages/ui/src/components/ImageLightbox.tsx
index 491db252..0cee39e6 100644
--- a/packages/ui/src/components/ImageLightbox.tsx
+++ b/packages/ui/src/components/ImageLightbox.tsx
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef } from 'react';
+import { createPortal } from 'react-dom';
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
import { ArrowUp, ArrowDown, Upload, Info, FileText, Sparkles, Mic, MicOff, Plus, Trash2, Save, History, Wand2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -266,11 +267,8 @@ export default function ImageLightbox({
alt={imageTitle}
sizes={`${Math.ceil(scale * 100)}vw`}
responsiveSizes={[640, 1024]}
- imgClassName={isMobile
- ? "max-w-[100dvw] max-h-[100dvh] object-contain pointer-events-auto"
- : "max-w-[90vw] max-h-[90vh] object-contain cursor-grab active:cursor-grabbing pointer-events-auto"
- }
- className="w-full h-full flex items-center justify-center"
+ imgClassName="max-w-[100vw] max-h-[100dvh] object-contain pointer-events-auto md:max-w-[90vw] md:max-h-[90vh] md:cursor-grab md:active:cursor-grabbing"
+ className="flex items-center justify-center"
loading="eager"
draggable={false}
onLoad={() => setLightboxLoaded(true)}
@@ -292,7 +290,7 @@ export default function ImageLightbox({
/>
);
- return (
+ return createPortal(
{
@@ -320,8 +318,8 @@ export default function ImageLightbox({
}
}}
>
-
-
+
+
{isMobile ? responsiveImageEl : (
-
+
,
+ document.body
);
}
diff --git a/packages/ui/src/components/MarkdownRenderer.tsx b/packages/ui/src/components/MarkdownRenderer.tsx
index 5ad7c9f6..ea6b9df8 100644
--- a/packages/ui/src/components/MarkdownRenderer.tsx
+++ b/packages/ui/src/components/MarkdownRenderer.tsx
@@ -1,5 +1,6 @@
import React, { useMemo, useEffect, useRef, useState, Suspense } from 'react';
import ReactMarkdown from 'react-markdown';
+import remarkGfm from 'remark-gfm';
import HashtagText from './HashtagText';
import Prism from 'prismjs';
import ResponsiveImage from './ResponsiveImage';
@@ -166,6 +167,7 @@ const MarkdownRenderer = React.memo(({ content, className = "", variables }: Mar
className={`prose prose-sm max-w-none dark:prose-invert ${className}`}
>
{
// Basic implementation of ResponsiveImage
@@ -245,6 +247,20 @@ const MarkdownRenderer = React.memo(({ content, className = "", variables }: Mar
}
return {children}
;
},
+ table: ({ node, ...props }) => (
+
+ ),
+ thead: ({ node, ...props }) => (
+
+ ),
+ th: ({ node, ...props }) => (
+ |
+ ),
+ td: ({ node, ...props }) => (
+ |
+ ),
}}
>
{finalContent}
diff --git a/packages/ui/src/components/PhotoCard.tsx b/packages/ui/src/components/PhotoCard.tsx
index 38f0352b..8d48411b 100644
--- a/packages/ui/src/components/PhotoCard.tsx
+++ b/packages/ui/src/components/PhotoCard.tsx
@@ -73,7 +73,7 @@ const PhotoCard = ({
apiUrl,
versionCount,
isExternal = false,
- imageFit = 'cover',
+ imageFit = 'contain',
className,
preset
}: PhotoCardProps) => {
@@ -335,7 +335,7 @@ const PhotoCard = ({
return (
{/* Image */}
diff --git a/packages/ui/src/components/ResponsiveImage.tsx b/packages/ui/src/components/ResponsiveImage.tsx
index e712edfd..3a033d3f 100644
--- a/packages/ui/src/components/ResponsiveImage.tsx
+++ b/packages/ui/src/components/ResponsiveImage.tsx
@@ -29,13 +29,16 @@ export interface ResponsiveData {
stats?: any;
}
-const ResponsiveImage: React.FC
= ({
+const DEFAULT_RESPONSIVE_SIZES = [640, 1280];
+const DEFAULT_FORMATS = ['avif'];
+
+const ResponsiveImage: React.FC = React.memo(({
src,
sizes = '(max-width: 1024px) 100vw, 50vw',
className,
imgClassName,
- responsiveSizes = [640, 1280],
- formats = ['avif'],
+ responsiveSizes = DEFAULT_RESPONSIVE_SIZES,
+ formats = DEFAULT_FORMATS,
alt,
onDataLoaded,
rootMargin = '800px',
@@ -89,6 +92,7 @@ const ResponsiveImage: React.FC = ({
useEffect(() => {
if (data && onDataLoaded) {
+ console.log('onDataLoaded data ', data)
onDataLoaded(data);
}
}, [data, onDataLoaded]);
@@ -100,10 +104,14 @@ const ResponsiveImage: React.FC = ({
// Check if image is already loaded (from cache)
useEffect(() => {
- if (imgRef.current?.complete) {
+ const img = imgRef.current;
+ if (img?.complete && img.naturalWidth > 0) {
setImgLoaded(true);
}
- }, [data, imgLoaded]);
+ }, [data]);
+
+
+ console.log('class name ', className, data)
// If we are enabled (isInView) but have no data and no error yet,
// we are effectively in a "pending load" state.
@@ -124,7 +132,7 @@ const ResponsiveImage: React.FC = ({
}
return (
-
+
{(data.sources || []).map((source, index) => (
@@ -158,6 +166,17 @@ const ResponsiveImage: React.FC = ({
)}
);
-};
+}, (prev, next) => {
+ // Only compare props that affect visual output — ignore callbacks
+ return prev.src === next.src &&
+ prev.sizes === next.sizes &&
+ prev.className === next.className &&
+ prev.imgClassName === next.imgClassName &&
+ prev.alt === next.alt &&
+ prev.loading === next.loading &&
+ prev.data === next.data &&
+ prev.responsiveSizes === next.responsiveSizes &&
+ prev.formats === next.formats;
+});
export default ResponsiveImage;
diff --git a/packages/ui/src/hooks/useResponsiveImage.ts b/packages/ui/src/hooks/useResponsiveImage.ts
index 0ed99b21..e9c4c4bd 100644
--- a/packages/ui/src/hooks/useResponsiveImage.ts
+++ b/packages/ui/src/hooks/useResponsiveImage.ts
@@ -1,5 +1,5 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useMemo } from 'react';
import { ResponsiveData } from '@/components/ResponsiveImage';
interface UseResponsiveImageProps {
@@ -10,14 +10,17 @@ interface UseResponsiveImageProps {
apiUrl?: string;
}
+const DEFAULT_SIZES = [180, 640, 1024];
+const DEFAULT_FORMATS = ['avif', 'webp', 'jpeg'];
+
// Module-level cache to deduplicate requests
// Key: stringified request params, Value: Promise
const requestCache = new Map>();
export const useResponsiveImage = ({
src,
- responsiveSizes = [180, 640, 1024],
- formats = ['avif', 'webp', 'jpeg'],
+ responsiveSizes = DEFAULT_SIZES,
+ formats = DEFAULT_FORMATS,
enabled = true,
apiUrl,
}: UseResponsiveImageProps) => {
@@ -25,6 +28,10 @@ export const useResponsiveImage = ({
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
+ // Stable key for deps — only changes when array contents actually change
+ const sizesKey = useMemo(() => JSON.stringify(responsiveSizes), [responsiveSizes]);
+ const formatsKey = useMemo(() => JSON.stringify(formats), [formats]);
+
useEffect(() => {
let isMounted = true;
const generateResponsiveImages = async () => {
@@ -139,7 +146,7 @@ export const useResponsiveImage = ({
return () => {
isMounted = false;
};
- }, [src, JSON.stringify(responsiveSizes), JSON.stringify(formats), enabled]);
+ }, [src, sizesKey, formatsKey, enabled]);
return { data, loading, error };
};
diff --git a/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx b/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx
index 08555624..d582cead 100644
--- a/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx
+++ b/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx
@@ -51,14 +51,13 @@ export const UserPageTypeFields: React.FC = ({
}
}, [pageMeta]);
- const handleFormChange = (typeId: string, data: any) => {
+ const handleFormChange = async (typeId: string, data: any) => {
setFormData(prev => ({
...prev,
[typeId]: data
}));
- };
- const handleFormSubmit = async (typeId: string, data: any) => {
+ // Fire command immediately so page.meta stays in sync for global save
const newTypeValues = {
...(pageMeta?.typeValues || {}),
[typeId]: data
@@ -75,10 +74,9 @@ export const UserPageTypeFields: React.FC = ({
{ meta: newMeta },
(updatedData) => {
if (onMetaUpdate) onMetaUpdate(updatedData.meta);
- toast.success("Fields updated");
}
);
-
+ console.log("command", command);
await executeCommand(command);
};
@@ -188,17 +186,11 @@ export const UserPageTypeFields: React.FC = ({
widgets={customWidgets}
templates={customTemplates}
onChange={(e) => isEditMode && handleFormChange(type.id, e.formData)}
- onSubmit={(e) => handleFormSubmit(type.id, e.formData)}
readonly={!isEditMode}
className={isEditMode ? "" : "pointer-events-none opacity-80"}
>
- {isEditMode ? (
-
-
-
- ) : <>>}
+ {/* No submit button — changes are saved via the global save (Ctrl+S) */}
+ <>>
diff --git a/packages/ui/src/pages/Post/renderers/CompactRenderer.tsx b/packages/ui/src/pages/Post/renderers/CompactRenderer.tsx
index 21734064..b5ee98b9 100644
--- a/packages/ui/src/pages/Post/renderers/CompactRenderer.tsx
+++ b/packages/ui/src/pages/Post/renderers/CompactRenderer.tsx
@@ -71,7 +71,7 @@ export const CompactRenderer: React.FC = (props) => {
{/* Left Column - Media */}
-
+
{/* Desktop Gallery - Combines Media Viewer + Filmstrip */}
diff --git a/packages/ui/src/pages/SearchResults.tsx b/packages/ui/src/pages/SearchResults.tsx
index f12c27c4..58da2da2 100644
--- a/packages/ui/src/pages/SearchResults.tsx
+++ b/packages/ui/src/pages/SearchResults.tsx
@@ -1,9 +1,10 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import { useAuth } from "@/hooks/useAuth";
import { usePostNavigation } from "@/contexts/PostNavigationContext";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
import { ArrowLeft, Search, Image as ImageIcon, FileText, MessageSquare } from "lucide-react";
import { ThemeToggle } from "@/components/ThemeToggle";
import MediaCard from "@/components/MediaCard";
@@ -24,9 +25,38 @@ const SearchResults = () => {
const [userLikes, setUserLikes] = useState
>(new Set());
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('all');
+ const [inputQuery, setInputQuery] = useState('');
+ const searchInputRef = useRef(null);
const query = searchParams.get('q') || '';
+ // Sync input with URL query
+ useEffect(() => {
+ setInputQuery(query);
+ }, [query]);
+
+ // Auto-focus on mount when no query
+ useEffect(() => {
+ if (!query.trim()) {
+ searchInputRef.current?.focus();
+ }
+ }, []);
+
+ const handleSearchSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (inputQuery.trim()) {
+ navigate(`/search?q=${encodeURIComponent(inputQuery.trim())}`);
+ searchInputRef.current?.blur();
+ }
+ };
+
+ const handleSearchKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Escape') {
+ setInputQuery('');
+ searchInputRef.current?.blur();
+ }
+ };
+
useEffect(() => {
if (query.trim()) {
performSearch();
@@ -184,7 +214,21 @@ const SearchResults = () => {
return (
-
+
+
+
+
Enter a search term
Search for pages, posts, and pictures
@@ -198,21 +242,33 @@ const SearchResults = () => {
{/* Header */}
-
+
- {/* Search Header */}
-
-
-
-
Search results for "{query}"
-
-
- {feedPosts.length} {feedPosts.length === 1 ? 'result' : 'results'} found
+ {/* Search Input */}
+
+
+
+
+ {/* Results Summary */}
+
+
+ {feedPosts.length} {feedPosts.length === 1 ? 'result' : 'results'} for "{query}"