Doc Presence
Collaboration marks for a shared document: overlapping collaborator avatars, a peer's inline text caret with a name flag, and a block outlined in the peer's colour.
npx shadcn@latest add @velaui/doc-presencepnpm dlx velaui add @velaui/doc-presenceyarn dlx shadcn@latest add @velaui/doc-presencebunx --bun shadcn@latest add @velaui/doc-presenceveni add doc-presenceInstalls the copy of vela-ui that veni-builder serves to Venizia apps; veni-builder serves the Radix UI copy only, so this tab exists for that base alone.
npx shadcn@latest add @velaui/doc-presencepnpm dlx velaui add @velaui/doc-presenceyarn dlx shadcn@latest add @velaui/doc-presencebunx --bun shadcn@latest add @velaui/doc-presenceExamples
- Collaborators
Usage
Props
DocPresenceBar
| Prop | Type | Default |
|---|---|---|
className | string | — |
peersrequired | DocPeer[] | — |
Takes these and every prop of { peers: DocPeer[]; className?: string }.
DocPeerCaret
| Prop | Type | Default |
|---|---|---|
peerrequired | DocPeer | — |
Takes these and every prop of { peer: DocPeer }.
DocPeerBlock
| Prop | Type | Default |
|---|---|---|
childrenrequired | string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | — |
className | string | — |
peerrequired | DocPeer | — |
Takes these and every prop of { peer: DocPeer; className?: string; children: React.ReactNode }.
Dependencies
- npm
- none besides React
- Installs with it
- nothing else
Source
components/ui/doc-presence.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
export interface DocPeer {
id: string;
name: string;
/** Any CSS colour; the caret, flag and avatar use it, with black or white text chosen for contrast. */
color: string;
}
/** Black or white ink, whichever contrasts with `color` (CSS relative colour: light backgrounds get black). */
const ink = (color: string) => `oklch(from ${color} clamp(0, (0.7 - l) * 1000, 1) 0 0)`;
/** Who else is in the document: overlapping initials, with the full name on hover. */
function DocPresenceBar({ peers, className }: { peers: DocPeer[]; className?: string }) {
return (
<div className={cn("flex items-center -space-x-1.5", className)} role="list" aria-label="Collaborators">
{peers.map((peer) => (
<span
key={peer.id}
role="listitem"
title={peer.name}
style={{ backgroundColor: peer.color, color: ink(peer.color) }}
className="flex size-6 items-center justify-center rounded-full border-2 border-bg-main text-xs font-semibold"
>
{peer.name.slice(0, 1).toUpperCase()}
</span>
))}
</div>
);
}
/** A peer's text caret, drawn inline between two runs of text, with their name on a flag above it. */
function DocPeerCaret({ peer }: { peer: DocPeer }) {
return (
<span
aria-label={`${peer.name}'s cursor`}
style={{ borderColor: peer.color }}
className="relative mx-px inline-block h-[1.1em] translate-y-[0.2em] border-l-2"
>
<span
style={{ backgroundColor: peer.color, color: ink(peer.color) }}
className="absolute bottom-full left-0 mb-px whitespace-nowrap rounded-sm rounded-bl-none px-1 text-xs leading-4 font-medium"
>
{peer.name}
</span>
</span>
);
}
/** A block a peer is working in: outlined in their colour and tagged with their name. */
function DocPeerBlock({ peer, className, children }: { peer: DocPeer; className?: string; children: React.ReactNode }) {
return (
<div style={{ outlineColor: peer.color }} className={cn("relative rounded-sm outline-1 outline-offset-2", className)}>
<span
style={{ backgroundColor: peer.color, color: ink(peer.color) }}
className="absolute -top-4 right-0 rounded-sm px-1 text-xs leading-4 font-medium"
>
{peer.name}
</span>
{children}
</div>
);
}
export { DocPresenceBar, DocPeerCaret, DocPeerBlock };