feat(collab): presence P1 — awareness transport + who's-here roster (collab-presence 0001)

- provider.ts: expose awareness on YjsProvider (partykit/hocuspocus surface
  their own; BroadcastChannel gets a sibling-channel relay via awareness-bc.ts)
- presence.ts: createPresence — publish local PresenceState, subscribable
  deduped peers() roster, setCursor/setSelection for P2, pagehide fast removal
- PresenceRoster.tsx + WasmTool wiring: facepile chip next to SourceChip;
  eeschema rebinds per active sheet (sheet-manager ActiveSheet now carries the
  room's provider); config presenceUser() = slug + colorForUser
- new dep y-protocols; unit tests (presence.test.ts) + e2e
  tests/web/presence-roster.spec.ts (two tabs, verified vs real partykit)
- bump pcbjam-shared: presence-wire schema (MIT)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvqUd4QsJSGHN28aunJRTq
This commit is contained in:
Gergő Törcsvári 2026-07-06 14:28:10 +02:00
commit 22528f450b
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
12 changed files with 538 additions and 15 deletions

View file

@ -98,6 +98,7 @@ export const PROJECT_MANIFEST_URL = import.meta.env.VITE_PROJECT_MANIFEST_URL ||
export const LOCAL_PROJECTS_ENABLED =
import.meta.env.VITE_LOCAL_PROJECTS === "idb";
import { colorForUser, type PresenceUser } from "@pcbjam/shared";
import type { ProviderConfig, ProviderKind } from "@/wasm/collab";
import { cdnLibsSource } from "@/wasm/libs/cdn-source";
import { cdnModelsSource, type Model3dSource } from "@/wasm/libs/models-source";
@ -169,6 +170,17 @@ export function userSlug(): string {
);
}
/**
* The local user's presence identity (collab-presence 0001): the pre-auth slug
* doubles as id + display name, color is the deterministic palette hash — so
* every peer computes the same identity for this user with no coordination.
* Real auth/avatars later replace only how this object is built.
*/
export function presenceUser(): PresenceUser {
const slug = userSlug();
return { id: slug, name: slug, color: colorForUser(slug) };
}
/**
* The active scope (first URL segment) for API calls. Mirrors how `userSlug()`
* reads the URL, so the source layer scopes requests without threading scope