@meldkit/svelte-ui
v0.1.2
Published
Ready-made collaboration UI for @meldkit/svelte: presence avatars, connection status, remote carets, live pointer cursors
Readme
@meldkit/svelte-ui
Ready-made collaboration UI for @meldkit/svelte: presence avatars, a
connection indicator, remote carets and live pointer cursors.
Entirely optional. The SDK has no idea this package exists, and everything here is a few dozen lines you could write yourself — the point is that you shouldn't have to write them twice.
npm install @meldkit/svelte-ui<script>
import { Avatars, ConnectionStatus } from '@meldkit/svelte-ui';
import '@meldkit/svelte-ui/styles.css';
</script>
<Avatars />
<ConnectionStatus />Import the stylesheet once, near your app's entry — or from your global CSS:
@import '@meldkit/svelte-ui/styles.css';Components
<Avatars /> — who's in the room, as a row of circles with each
person's initials. Reads presence from the room in scope, so it takes no inputs
in the common case. Pass me / others to render a list you've already
filtered or sorted.
The row is bounded in both directions. Past stackAfter (3) the circles overlap
instead of pushing the layout sideways; past max (5) the remainder collapses
into a +N whose tooltip names them. A room of thirty people is a real thing,
and a presence bar that tries to draw thirty of anything has stopped being a
presence bar.
Your own circle leads the row and carries an accent ring, so it never ends up
inside the overflow count. A green dot marks anyone whose cursor or pointer
says they're active right now.
<ConnectionStatus /> — connection state, and errors. It reports
both, because the two disagree in the case that matters: with the wrong room key
the socket is perfectly healthy and the document is empty, so a plain status dot
would say "connected" next to a blank screen.
<CaretOverlay text={…}> — remote carets over a text input. A
textarea can't contain elements, so this wraps yours in a mirror layer holding
the same text with the same font and padding, its own text transparent, sitting
on top. The browser works out where character N lands — no measurement code,
correct through wrapping and resizing.
<CaretOverlay text={body.text}>
<textarea use:body.bind></textarea>
</CaretOverlay>It draws carets and nothing else. The value, the edits and the CRDT stay with
sharedText from @meldkit/svelte.
<InlineCarets text={…} others={…} /> — the same carets in
text you render yourself: a list row, a heading, a cell.
<Cursors space={{ width, height }} /> — everyone else's pointer,
drawn over a shared surface. Reads pointer from presence and places it as a
percentage of its container, so the same position lands in the same place in
windows of different sizes. Its wrapper has to be positioned, and has to have
the same aspect ratio as space.
Motion is the part that takes care. Each cursor moves by transform rather than
left/top, because offsets are a layout change and animating them
re-lays-out and repaints whatever is underneath on every frame. And each glides
for as long as that person's own last gap between updates took, rather than a
fixed duration — too short and a cursor sprints to each position then sits still
until the next one, which is what reads as jitter; too long and it lags behind
the person driving it.
Pass status to put a word next to someone's name — "drawing", "typing" — from
whatever field of your presence answers that.
<SetupNotice /> — first-run instructions for when the token
endpoint isn't configured yet. Render it in place of your app:
{#if meldkit.error?.code === 'config'}
<SetupNotice />
{:else}
<Board />
{/if}randomIdentity() — a throwaway display name and colour for apps that don't
have accounts yet. A const in a component's <script> runs once and stays
put, so unlike the React binding there is no hook to wrap it in. Only for
display; the userId in your access token is who you are to MeldKit, and it
comes from your server.
Presence shape
The components read three fields:
type CollaboratorPresence = {
name: string;
color: string;
cursor?: number | null; // character offset, not a pixel position
pointer?: { x: number; y: number } | null;
};A floor, not a ceiling — carry whatever else you need alongside them. An app
that adds an editing field naming the row someone is in can point the same
caret component at one list row or at a whole document.
Offsets rather than coordinates is the load-bearing decision: a pixel position means nothing in a window of a different width, an offset means the same thing everywhere.
Styling
The class names, custom properties and stylesheet are the same as
@meldkit/react-ui's, deliberately: the markup differs between the two
packages, the design doesn't. In order of how much you want to change:
- Retheme with the custom properties (
--mk-accent,--mk-line,--mk-doc-font, …) — set them on:root, or on any ancestor to scope it. - Restyle with your own rules against the
mk-class names, or skip the stylesheet entirely and style the markup yourself.
Each component renders one element carrying the class it is styled by, so there
is nothing of ours between your CSS and the thing it is styling. Every component
takes a class prop if you want to add your own alongside.
One caveat, and it's the only part of the CSS that isn't cosmetic: the mirror
layer and the text input must agree on every property that affects where a
character lands, or carets drift further off with every line. Both read the same
--mk-doc-* variables from one rule so they can't diverge. Retheme those rather
than setting font or padding on the textarea directly.
