Skip to content

feat(CollabStatus): transport-agnostic live-collaboration status chip - #350

Open
horner wants to merge 3 commits into
mainfrom
feat/collab-status
Open

feat(CollabStatus): transport-agnostic live-collaboration status chip#350
horner wants to merge 3 commits into
mainfrom
feat/collab-status

Conversation

@horner

@horner horner commented Aug 1, 2026

Copy link
Copy Markdown
Member

Adds CollabStatus, a connection dot + Live/Connecting… label + "who is editing" summary with a click-to-open panel showing room identity and a rolling event log. The component is transport-agnostic — it renders whatever presence state it is handed.

image image

useYjsCollabStatus binds it to a Yjs y-websocket room in three lines. The doc/awareness/provider are typed structurally, so the library keeps a zero runtime dependency on Yjs (yjs, y-protocols and y-websocket are devDependencies used only by the stories and tests).

Stories include a simulated room (a real Y.Doc + Awareness relayed in memory, so it works with no server and in CI) and a live server room, plus autodocs covering the binding recipe.

Adds `CollabStatus`, a connection dot + `Live`/`Connecting…` label + "who is
editing" summary with a click-to-open panel showing room identity and a rolling
event log. The component is transport-agnostic — it renders whatever presence
state it is handed.

`useYjsCollabStatus` binds it to a Yjs `y-websocket` room in three lines. The
doc/awareness/provider are typed structurally, so the library keeps a zero
runtime dependency on Yjs (yjs, y-protocols and y-websocket are devDependencies
used only by the stories and tests).

Stories include a simulated room (a real Y.Doc + Awareness relayed in memory, so
it works with no server and in CI) and a live server room, plus autodocs
covering the binding recipe.
Copilot AI review requested due to automatic review settings August 1, 2026 09:05
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploying ui with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6b24f22
Status: ✅  Deploy successful!
Preview URL: https://3bb916d3.ui-6d0.pages.dev
Branch Preview URL: https://feat-collab-status.ui-6d0.pages.dev

View logs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new transport-agnostic CollabStatus status chip component (with optional debug panel) plus a useYjsCollabStatus hook that binds it to Yjs rooms without introducing a runtime Yjs dependency in the library.

Changes:

  • Export new CollabStatus component + types and useYjsCollabStatus hook from the public API and build entrypoints.
  • Add Storybook stories (including an in-memory simulated Yjs room) and Vitest coverage for the component + hook.
  • Add Yjs-related packages as devDependencies to support stories/tests.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tsup.config.ts Adds a tsup entrypoint for the new component package export.
src/index.ts Re-exports CollabStatus from the library root.
src/components/CollabStatus/index.ts Barrel exports for component, types, and Yjs binding hook.
src/components/CollabStatus/CollabStatus.tsx New UI: status chip + optional portal-based panel + log rendering.
src/components/CollabStatus/useYjsCollabStatus.ts New hook: observes Yjs doc/provider/awareness structurally and builds CollabStatus props + log.
src/components/CollabStatus/storyData.ts In-memory Yjs room simulation used by stories/tests.
src/components/CollabStatus/CollabStatus.stories.tsx Storybook docs + simulated room + live-server demo story.
src/components/CollabStatus/CollabStatus.test.tsx Tests for UI text behavior and hook integration via the simulated room.
package.json Adds Yjs packages (for story/test support).
pnpm-lock.yaml Lockfile updates for added Yjs packages.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment on lines +174 to +179
const KIND_TONE: Record<CollabLogKind, string> = {
doc: 'text-muted-foreground',
awareness: 'text-primary-600 dark:text-primary-400',
sync: 'text-success-700 dark:text-success-400',
patch: 'text-muted-foreground',
};
Comment on lines +120 to +142
const member: LocalYjsMember = {
doc,
awareness,
provider,
leave: () => {
this.members.delete(member);
doc.off('update', onDocUpdate);
awareness.off('update', onAwareness);
// Clear presence, then push that removal so peers log "left the room".
awareness.setLocalState(null);
const removal = encodeAwarenessUpdate(awareness, [awareness.clientID]);
for (const peer of this.members) {
applyAwarenessUpdate(peer.awareness, removal, peer.provider);
}
provider.markSynced(false);
awareness.destroy();
doc.destroy();
},
};

this.members.add(member);
window.setTimeout(() => provider.markSynced(true), syncDelay);
return member;
horner added a commit to mieweb/yorm that referenced this pull request Aug 1, 2026
The header's wide connection/projection badge is replaced by two dots:

- a projection dot next to the autosave picker (green saved / amber pending),
  with the wording kept for screen readers;
- `CollabStatus` in compact mode (RoomStatus.tsx) whose popup lists the room
  occupants and a merged activity log: peers joining/leaving, doc updates,
  sync transitions, field edits, policy changes, proposals, and SQL
  projection commits.

`@mieweb/ui` now builds from the new vendor/ui submodule (mieweb/ui#350),
which carries the `compact` + occupants additions this demo needs.
horner added 2 commits August 2, 2026 09:42
- `compact` prop renders the dot alone; status and editing text move into
  the trigger's title/accessible name so the chip costs no header space.
- The panel gains an "In the room (N)" section listing occupants with their
  presence colors, with an `alone` fallback.
- New `peersTitle`/`alone` labels; `peerLabels()` now returns label+color.
- Tests + a `Compact` story.
An app-level condition (unsaved work, a pending save) had no way to reach
the header without adding a second dot beside this one, so `attention`
lets it share this dot: amber while set, joined into the trigger's
accessible name, and headlining the panel.

The panel stays narrow by clipping long values (socket URL, log details)
to one line, which left them unreadable. Every clipped value now carries
its full text in a `title`, and a `Wrap long values` toggle in the panel
header unclips all of them at once for the cases where a tooltip is not
enough.
Copilot AI review requested due to automatic review settings August 2, 2026 09:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (3)

src/components/CollabStatus/CollabStatus.tsx:361

  • This component introduces several arbitrary-value Tailwind classes (e.g. w-[26rem], max-w-[calc(100vw-1rem)], grid-cols-[auto_1fr], grid-cols-[auto_auto_1fr]). These are not currently present in miewebUISafelist (src/tailwind-preset.ts), so Tailwind CSS 3 consumers who rely on the preset safelist (and don’t scan node_modules) may miss critical styles for this panel/layout.
            className={cn(
              'bg-card text-card-foreground border-border fixed z-50 w-[26rem] max-w-[calc(100vw-1rem)]',
              'rounded-lg border p-3 text-xs shadow-xl'
            )}

src/components/CollabStatus/CollabStatus.tsx:300

  • In compact mode, triggerLabel is built by joining an array that can include attention (typed as React.ReactNode). If attention is a React element, it will be coerced to the string "[object Object]" and end up in aria-label/title, which breaks accessibility names/tooltips.
        attention,
        names.length > 0 && labels.editing(names),
      ]
        .filter(Boolean)
        .join(' — ')

src/components/CollabStatus/CollabStatus.stories.tsx:288

  • LiveServerDemo keeps error state across serverUrl/roomName changes; once an import/connection fails, the story will continue to render the error even if the next effect run succeeds. Resetting error at the start of the effect avoids a stale error UI.
  React.useEffect(() => {
    let disposed = false;
    let cleanup = () => {};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants