feat(notes): presence as avatar stack, drop guide lines, fix stuck bullet focus

- Leaving a block now clears its focus, so the bullet no longer stays highlighted
- Remove the vertical indentation guide lines
- Who has the note open: up to 3 overlapping avatars, "+n", full list on hover
- Shared AvatarStack component; UserAvatar gets xxs size and optional title

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-28 20:26:55 +01:00
1 parent 3540b4b2ae
commit 1bd38f4d50
6 files changed
+134 -21

No files matched your search

+60
View File
@@ -0,0 +1,60 @@
import React from 'react';
import { UserAvatar } from './UserAvatar';
export interface AvatarPerson {
id: string;
name: string;
avatarUrl?: string;
}
interface AvatarStackProps {
people: AvatarPerson[];
max?: number;
size?: 'xxs' | 'xs';
// Ring in the color of the surface behind the stack, so overlapping circles stay separated
ringClass?: string;
title?: string;
className?: string;
}
const OVERFLOW_SIZE = {
xxs: 'w-4 h-4 text-[7px]',
xs: 'w-5 h-5 text-[9px]',
};
// Small overlapping profile pictures: at most `max` circles, the rest as a monochrome "+n" circle.
// Used for presence in notes, list/task-list tabs, task assignees and list item authors (Doku/ui-patterns.md).
export const AvatarStack: React.FC<AvatarStackProps> = ({
people,
max = 3,
size = 'xs',
ringClass = 'ring-[var(--bg-app)]',
title,
className = '',
}) => {
if (people.length === 0) return null;
const visible = people.slice(0, max);
const hidden = people.length - visible.length;
return (
<span className={`inline-flex items-center -space-x-1.5 shrink-0 ${className}`} title={title}>
{visible.map((p) => (
<UserAvatar
key={p.id}
name={p.name}
avatarUrl={p.avatarUrl || ''}
size={size}
showTitle={false}
className={`ring-2 ${ringClass}`}
/>
))}
{hidden > 0 && (
<span
className={`relative inline-flex items-center justify-center rounded-full font-bold leading-none bg-muted text-panel ring-2 ${ringClass} ${OVERFLOW_SIZE[size]}`}
>
+{hidden}
</span>
)}
</span>
);
};
+5 -2
View File
@@ -8,12 +8,14 @@ export interface UserAvatarProps {
} | null;
name?: string;
avatarUrl?: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
size?: 'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
className?: string;
alt?: string;
showTitle?: boolean; // native name tooltip on hover
}
const SIZE_MAP = {
xxs: 'w-4 h-4 text-[7px]',
xs: 'w-5 h-5 text-[10px]',
sm: 'w-7 h-7 text-xs',
md: 'w-9 h-9 text-sm',
@@ -68,6 +70,7 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
size = 'md',
className = '',
alt,
showTitle = true,
}) => {
const [imageError, setImageError] = useState(false);
@@ -94,7 +97,7 @@ export const UserAvatar: React.FC<UserAvatarProps> = ({
return (
<div
className={`inline-flex items-center justify-center rounded-full aspect-square shrink-0 select-none overflow-hidden font-semibold transition-all ${sizeClass} ${className}`}
title={effectiveName || alt}
title={showTitle ? effectiveName || alt : undefined}
>
{hasImage ? (
<img