minor UI fixes
This commit is contained in:
@@ -5,7 +5,7 @@ import { PenTool, Trash2, FolderInput, ArrowRight, Check, Clock, Copy, Download,
|
||||
import type { DrawingSummary, Collection, Drawing } from '../types';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import clsx from 'clsx';
|
||||
import { exportToSvg } from "@excalidraw/excalidraw";
|
||||
// import { exportToSvg } from "@excalidraw/excalidraw"; // Lazy load this instead
|
||||
import { exportDrawingToFile } from '../utils/exportUtils';
|
||||
|
||||
import * as api from '../api';
|
||||
@@ -112,6 +112,11 @@ export const DrawingCard: React.FC<DrawingCardProps> = ({
|
||||
if (cancelled) return;
|
||||
if (!data?.elements || !data?.appState) return;
|
||||
|
||||
// Lazy load exportToSvg to keep the main bundle small
|
||||
const { exportToSvg } = await import("@excalidraw/excalidraw");
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: data.elements,
|
||||
appState: {
|
||||
@@ -243,18 +248,18 @@ export const DrawingCard: React.FC<DrawingCardProps> = ({
|
||||
|
||||
{previewSvg ? (
|
||||
<div
|
||||
className="w-full h-full p-6 flex items-center justify-center [&>svg]:w-full [&>svg]:h-full [&>svg]:object-contain [&>svg]:drop-shadow-sm dark:[&>svg]:invert dark:[&>svg_rect[fill='white']]:opacity-0 dark:[&>svg_rect[fill='#ffffff']]:opacity-0 transition-transform duration-500 group-hover:scale-105"
|
||||
className="w-full h-full p-3 sm:p-4 lg:p-5 flex items-center justify-center [&>svg]:w-full [&>svg]:h-full [&>svg]:object-contain [&>svg]:drop-shadow-sm dark:[&>svg]:invert dark:[&>svg_rect[fill='white']]:opacity-0 dark:[&>svg_rect[fill='#ffffff']]:opacity-0 transition-transform duration-500 group-hover:scale-105"
|
||||
dangerouslySetInnerHTML={{ __html: previewSvg }}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-24 h-24 bg-white dark:bg-neutral-900 rounded-2xl shadow-sm flex items-center justify-center text-neutral-300 dark:text-neutral-400 border border-slate-100 dark:border-neutral-700 transform group-hover:scale-110 group-hover:rotate-3 transition-all duration-500">
|
||||
<PenTool size={40} strokeWidth={1.5} />
|
||||
<div className="w-16 h-16 sm:w-20 sm:h-20 lg:w-24 lg:h-24 bg-white dark:bg-neutral-900 rounded-2xl shadow-sm flex items-center justify-center text-neutral-300 dark:text-neutral-400 border border-slate-100 dark:border-neutral-700 transform group-hover:scale-110 group-hover:rotate-3 transition-all duration-500">
|
||||
<PenTool size={32} strokeWidth={1.5} className="sm:w-9 sm:h-9 lg:w-10 lg:h-10" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-5 bg-white dark:bg-neutral-900 rounded-b-2xl relative z-10">
|
||||
<div className="p-3 sm:p-4 lg:p-5 bg-white dark:bg-neutral-900 rounded-b-2xl relative z-10">
|
||||
{isRenaming ? (
|
||||
<form
|
||||
onSubmit={handleRenameSubmit}
|
||||
@@ -270,12 +275,12 @@ export const DrawingCard: React.FC<DrawingCardProps> = ({
|
||||
onBlur={() => setIsRenaming(false)}
|
||||
onDragStart={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="w-full px-2 py-1 -ml-2 text-base font-bold text-slate-900 dark:text-white border-2 border-black dark:border-neutral-600 rounded-lg focus:outline-none shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] bg-white dark:bg-neutral-800"
|
||||
className="w-full px-2 py-1 -ml-2 text-sm sm:text-base font-bold text-slate-900 dark:text-white border-2 border-black dark:border-neutral-600 rounded-lg focus:outline-none shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] dark:shadow-[2px_2px_0px_0px_rgba(255,255,255,0.2)] bg-white dark:bg-neutral-800"
|
||||
/>
|
||||
</form>
|
||||
) : (
|
||||
<h3
|
||||
className="text-base font-bold text-slate-800 dark:text-neutral-100 truncate cursor-text select-none group-hover:text-neutral-900 dark:group-hover:text-white transition-colors"
|
||||
className="text-sm sm:text-base font-bold text-slate-800 dark:text-neutral-100 truncate cursor-text select-none group-hover:text-neutral-900 dark:group-hover:text-white transition-colors"
|
||||
title={drawing.name}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -285,9 +290,9 @@ export const DrawingCard: React.FC<DrawingCardProps> = ({
|
||||
{drawing.name}
|
||||
</h3>
|
||||
)}
|
||||
<div className="flex items-center justify-between mt-3 relative">
|
||||
<p className="text-[11px] font-medium text-slate-400 dark:text-neutral-500 flex items-center gap-1.5">
|
||||
<Clock size={11} />
|
||||
<div className="flex items-center justify-between mt-2.5 sm:mt-3 relative">
|
||||
<p className="text-[10px] sm:text-[11px] font-medium text-slate-400 dark:text-neutral-500 flex items-center gap-1 sm:gap-1.5">
|
||||
<Clock size={10} className="sm:w-[11px] sm:h-[11px]" />
|
||||
{formatDistanceToNow(drawing.updatedAt)} ago
|
||||
</p>
|
||||
|
||||
@@ -451,4 +456,3 @@ export const DrawingCard: React.FC<DrawingCardProps> = ({
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,20 +1,5 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
const DEVICE_ID_KEY = 'excalidash-device-id';
|
||||
|
||||
const getOrCreateDeviceId = (): string => {
|
||||
if (typeof window === 'undefined') return 'server';
|
||||
const existing = localStorage.getItem(DEVICE_ID_KEY);
|
||||
if (existing) return existing;
|
||||
|
||||
const generated =
|
||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||
? crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
localStorage.setItem(DEVICE_ID_KEY, generated);
|
||||
return generated;
|
||||
};
|
||||
import { getOrCreateBrowserFingerprint, getFingerprintInitials } from '../utils/identity';
|
||||
|
||||
const fnv1a = (input: string): number => {
|
||||
let hash = 0x811c9dc5;
|
||||
@@ -27,99 +12,38 @@ const fnv1a = (input: string): number => {
|
||||
|
||||
const toHsl = (n: number) => {
|
||||
const hue = n % 360;
|
||||
const sat = 60 + (n % 20);
|
||||
const light = 45 + (n % 10);
|
||||
const sat = 55 + (n % 20);
|
||||
const light = 42 + (n % 12);
|
||||
return `hsl(${hue} ${sat}% ${light}%)`;
|
||||
};
|
||||
|
||||
const buildPattern = (seed: string) => {
|
||||
let x = fnv1a(seed);
|
||||
const nextBit = () => {
|
||||
// xorshift32
|
||||
x ^= x << 13;
|
||||
x ^= x >>> 17;
|
||||
x ^= x << 5;
|
||||
return (x >>> 0) & 1;
|
||||
};
|
||||
|
||||
const cells: boolean[][] = Array.from({ length: 5 }, () => Array.from({ length: 5 }, () => false));
|
||||
|
||||
// Generate left 3 columns, mirror to 5.
|
||||
for (let row = 0; row < 5; row += 1) {
|
||||
for (let col = 0; col < 3; col += 1) {
|
||||
const on = nextBit() === 1;
|
||||
cells[row][col] = on;
|
||||
cells[row][4 - col] = on;
|
||||
}
|
||||
}
|
||||
|
||||
const foreground = toHsl(x);
|
||||
const background = 'hsl(0 0% 98%)';
|
||||
const backgroundDark = 'hsl(0 0% 12%)';
|
||||
|
||||
return { cells, foreground, background, backgroundDark };
|
||||
};
|
||||
|
||||
export const FingerprintAvatar: React.FC<{
|
||||
size?: number;
|
||||
seed?: string;
|
||||
title?: string;
|
||||
className?: string;
|
||||
}> = ({ size = 32, seed, title = 'Device fingerprint', className }) => {
|
||||
const [deviceId] = useState(() => getOrCreateDeviceId());
|
||||
}> = ({ size = 32, seed, title = 'Browser fingerprint avatar', className }) => {
|
||||
const [deviceId] = useState(() => getOrCreateBrowserFingerprint());
|
||||
const effectiveSeed = seed || deviceId;
|
||||
|
||||
const { cells, foreground, background, backgroundDark } = useMemo(
|
||||
() => buildPattern(effectiveSeed),
|
||||
[effectiveSeed]
|
||||
);
|
||||
|
||||
const padding = 0.5;
|
||||
const viewBox = `${-padding} ${-padding} ${5 + padding * 2} ${5 + padding * 2}`;
|
||||
const initials = useMemo(() => getFingerprintInitials(effectiveSeed), [effectiveSeed]);
|
||||
const background = useMemo(() => toHsl(fnv1a(effectiveSeed)), [effectiveSeed]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox={viewBox}
|
||||
role="img"
|
||||
<div
|
||||
title={title}
|
||||
aria-label={title}
|
||||
className={className}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: 10,
|
||||
background,
|
||||
}}
|
||||
>
|
||||
<title>{title}</title>
|
||||
<rect
|
||||
x={-padding}
|
||||
y={-padding}
|
||||
width={5 + padding * 2}
|
||||
height={5 + padding * 2}
|
||||
rx={1.4}
|
||||
fill={background}
|
||||
className="dark:hidden"
|
||||
/>
|
||||
<rect
|
||||
x={-padding}
|
||||
y={-padding}
|
||||
width={5 + padding * 2}
|
||||
height={5 + padding * 2}
|
||||
rx={1.4}
|
||||
fill={backgroundDark}
|
||||
className="hidden dark:block"
|
||||
/>
|
||||
{cells.map((row, r) =>
|
||||
row.map((on, c) =>
|
||||
on ? <rect key={`${r}-${c}`} x={c} y={r} width={1} height={1} rx={0.2} fill={foreground} /> : null
|
||||
)
|
||||
)}
|
||||
<rect
|
||||
x={-padding}
|
||||
y={-padding}
|
||||
width={5 + padding * 2}
|
||||
height={5 + padding * 2}
|
||||
rx={1.4}
|
||||
fill="none"
|
||||
stroke="rgba(0,0,0,0.25)"
|
||||
className="dark:stroke-neutral-700"
|
||||
/>
|
||||
</svg>
|
||||
<div className="w-full h-full flex items-center justify-center font-bold text-white text-xs select-none">
|
||||
{initials}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ import clsx from 'clsx';
|
||||
import { ConfirmModal } from './ConfirmModal';
|
||||
import { Logo } from './Logo';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { FingerprintAvatar } from './FingerprintAvatar';
|
||||
import { readImpersonationState, stopImpersonation as restoreImpersonation, type ImpersonationState } from '../utils/impersonation';
|
||||
|
||||
interface SidebarProps {
|
||||
@@ -112,6 +111,16 @@ const SidebarItem: React.FC<SidebarItemProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const getInitialsFromName = (name: string): string => {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return 'U';
|
||||
const parts = trimmed.split(/\s+/).filter(Boolean);
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
|
||||
}
|
||||
return trimmed.slice(0, 2).toUpperCase();
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({
|
||||
@@ -374,14 +383,16 @@ export const Sidebar: React.FC<SidebarProps> = ({
|
||||
</div>
|
||||
)}
|
||||
{user && (
|
||||
<div className="px-3 py-2 text-xs text-slate-500 dark:text-neutral-500 mb-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<FingerprintAvatar size={28} className="flex-shrink-0 sm:hidden" title="Browser profile" />
|
||||
<FingerprintAvatar size={32} className="flex-shrink-0 hidden sm:block" title="Browser profile" />
|
||||
<div className="min-w-0">
|
||||
<div className="py-2 text-xs text-slate-500 dark:text-neutral-500 mb-2">
|
||||
<div className="grid grid-cols-[auto_1fr_auto] items-center gap-3">
|
||||
<div className="w-7 h-7 sm:w-8 sm:h-8 rounded-lg bg-indigo-600 text-white font-bold flex items-center justify-center">
|
||||
{getInitialsFromName(user.name)}
|
||||
</div>
|
||||
<div className="min-w-0 text-left">
|
||||
<div className="font-semibold text-slate-700 dark:text-neutral-300 truncate leading-tight">{user.name}</div>
|
||||
<div className="truncate leading-tight">{user.email}</div>
|
||||
</div>
|
||||
<div className="w-7 h-7 sm:w-8 sm:h-8 invisible" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user