@inkandswitch/patchwork-pkg-base 0.0.37 → 0.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,29 +4,9 @@ import {
4
4
  setAvatarFallback,
5
5
  setAvatarImage
6
6
  } from "./chunk-MOZI4Y4O.js";
7
-
8
- // src/ui.ts
9
- var USER_COLOR_PALETTE = [
10
- { name: "Sky Blue", value: "hsl(200, 70%, 50%)" },
11
- { name: "Coral", value: "hsl(10, 75%, 58%)" },
12
- { name: "Emerald", value: "hsl(145, 70%, 45%)" },
13
- { name: "Violet", value: "hsl(270, 70%, 55%)" },
14
- { name: "Amber", value: "hsl(38, 85%, 50%)" },
15
- { name: "Rose", value: "hsl(350, 70%, 55%)" },
16
- { name: "Teal", value: "hsl(178, 70%, 45%)" },
17
- { name: "Indigo", value: "hsl(235, 70%, 58%)" },
18
- { name: "Lime", value: "hsl(85, 70%, 45%)" },
19
- { name: "Fuchsia", value: "hsl(310, 70%, 55%)" },
20
- { name: "Orange", value: "hsl(25, 80%, 52%)" },
21
- { name: "Cyan", value: "hsl(188, 75%, 48%)" }
22
- ];
23
- function generateColorFromString(str) {
24
- const hash = Math.abs(
25
- str.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)
26
- );
27
- const colorIndex = hash % USER_COLOR_PALETTE.length;
28
- return USER_COLOR_PALETTE[colorIndex].value;
29
- }
7
+ import {
8
+ generateColorFromString
9
+ } from "./chunk-PZ7ORBQU.js";
30
10
 
31
11
  // src/components/ContactAvatar.ts
32
12
  import { getImportableUrlFromAutomergeUrl } from "@inkandswitch/patchwork-filesystem";
@@ -83,8 +63,7 @@ function renderContactAvatar(handle, element) {
83
63
  const isPresent = Object.entries(heartbeats).some(
84
64
  ([id, ts]) => id !== selfName && now - ts < timeout
85
65
  );
86
- const contact = handle.doc();
87
- const userColor = contact && "color" in contact && contact.color ? contact.color : generateColorFromString(docUrl);
66
+ const userColor = generateColorFromString(docUrl);
88
67
  if (isPresent) {
89
68
  avatar.classList.add("contact-avatar--present");
90
69
  avatar.style.setProperty("--contact-presence-color", userColor);
@@ -158,4 +137,4 @@ function renderContactAvatar(handle, element) {
158
137
  export {
159
138
  renderContactAvatar
160
139
  };
161
- //# sourceMappingURL=ContactAvatar-MKNZ5KEX.js.map
140
+ //# sourceMappingURL=ContactAvatar-B4QQC47I.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/components/ContactAvatar.ts", "../../node_modules/.pnpm/@inkandswitch+patchwork-providers@0.2.2_@automerge+automerge-repo@2.6.0-subduction.39_supports-color@7.2.0_/node_modules/@inkandswitch/patchwork-providers/dist/index.js"],
4
+ "sourcesContent": ["import type { AutomergeUrl, DocHandle } from \"@automerge/automerge-repo/slim\";\nimport type { ToolElement } from \"@inkandswitch/patchwork-plugins\";\nimport type { ContactDoc } from \"../types\";\nimport { createAvatar, setAvatarImage, setAvatarFallback, getInitials } from \"./Avatar\";\nimport { generateColorFromString } from \"../ui\";\nimport { getImportableUrlFromAutomergeUrl } from \"@inkandswitch/patchwork-filesystem\";\nimport { subscribe } from \"@inkandswitch/patchwork-providers\";\n\ndeclare global {\n interface Window {\n accountDocHandle?: { url: AutomergeUrl };\n }\n}\n\ntype TinyPatchworkLayoutDoc = {\n contactUrl: AutomergeUrl;\n};\n\nexport function renderContactAvatar(\n handle: DocHandle<ContactDoc>,\n element: ToolElement\n) {\n const avatar = createAvatar(\"default\");\n element.appendChild(avatar);\n\n let selfName = \"me\";\n const cleanups: (() => void)[] = [];\n\n // Track presence via ephemeral messages\n const heartbeats: Record<string, number> = {};\n let presenceTimer: ReturnType<typeof setInterval>;\n\n function onEphemeralMessage(message: { senderId: string; message: unknown }) {\n // useRemoteAwareness tracks heartbeats from ephemeral messages\n if (message.senderId) {\n heartbeats[message.senderId] = Date.now();\n }\n updatePresence();\n }\n\n function updatePresence() {\n const docUrl = handle.url;\n const now = Date.now();\n const timeout = 30000;\n const isPresent = Object.entries(heartbeats).some(\n ([id, ts]) => id !== selfName && now - ts < timeout\n );\n\n const userColor = generateColorFromString(docUrl);\n\n if (isPresent) {\n avatar.classList.add(\"contact-avatar--present\");\n avatar.style.setProperty(\"--contact-presence-color\", userColor);\n } else {\n avatar.classList.remove(\"contact-avatar--present\");\n avatar.style.removeProperty(\"--contact-presence-color\");\n }\n }\n\n handle.on(\"ephemeral-message\", onEphemeralMessage);\n cleanups.push(() => handle.off(\"ephemeral-message\", onEphemeralMessage));\n\n // Periodically clean up stale heartbeats\n presenceTimer = setInterval(updatePresence, 5000);\n cleanups.push(() => clearInterval(presenceTimer));\n\n // Get own contact via provider subscription\n const unsubscribe = subscribe<string>(\n element,\n { type: \"patchwork:contact\" },\n (contactUrl) => {\n element.repo\n .find<ContactDoc>(contactUrl as AutomergeUrl)\n .then((selfHandle) => {\n const selfDoc = selfHandle.doc();\n if (selfDoc?.type === \"registered\") {\n selfName = selfDoc.name;\n }\n });\n }\n );\n cleanups.push(unsubscribe);\n\n // Fallback: use window.accountDocHandle\n if (window.accountDocHandle) {\n element.repo\n .find<TinyPatchworkLayoutDoc>(window.accountDocHandle.url)\n .then((accountHandle) => {\n const accountDoc = accountHandle.doc();\n if (accountDoc?.contactUrl) {\n element.repo\n .find<ContactDoc>(accountDoc.contactUrl)\n .then((selfHandle) => {\n const selfDoc = selfHandle.doc();\n if (selfDoc?.type === \"registered\") {\n selfName = selfDoc.name;\n }\n });\n }\n });\n }\n\n async function update() {\n const contact = handle.doc();\n if (!contact) {\n avatar.style.display = \"none\";\n return;\n }\n avatar.style.display = \"\";\n\n const isRegistered = contact.type === \"registered\";\n const name = isRegistered ? contact.name : \"Anonymous\";\n\n // avatar image\n let avatarImgUrl: string | undefined;\n if (isRegistered && contact.avatarUrl) {\n try {\n const avatarHandle = await element.repo.find(contact.avatarUrl);\n avatarImgUrl = getImportableUrlFromAutomergeUrl(avatarHandle.url);\n } catch {\n // ignore failed avatar lookup\n }\n }\n setAvatarImage(avatar, avatarImgUrl, name);\n\n // fallback\n if (isRegistered && name) {\n setAvatarFallback(avatar, { initials: getInitials(name) });\n } else {\n setAvatarFallback(avatar, { iconSize: 20 });\n }\n\n updatePresence();\n }\n\n handle.on(\"change\", update);\n cleanups.push(() => handle.off(\"change\", update));\n update();\n\n return () => {\n for (const cleanup of cleanups) cleanup();\n };\n}\n", "/**\n * Open a streaming subscription. Dispatches a `patchwork:subscribe` for the\n * given `selector` carrying a fresh `MessageChannel` port; the answering\n * provider pushes values back over that channel for as long as the\n * subscription is live. `listener` is invoked once per emission (the first\n * delivery is always async because `MessagePort.postMessage` queues a task).\n *\n * The event is dispatched from the nearest enclosing `<patchwork-view>` so\n * callers can pass any node inside a provider subtree. An unclaimed\n * subscription is never settled: if no provider answers, `listener` simply\n * never fires.\n *\n * Returns an unsubscribe function. Calling it tells the provider to tear down\n * (via an `unsubscribe` message) and closes the consumer's port; any values\n * the provider emits after that are dropped.\n */\nexport function subscribe(element, selector, listener) {\n const view = element.closest(\"patchwork-view\");\n const dispatchEl = view ?? element;\n const channel = new MessageChannel();\n const port = channel.port2;\n const controller = new AbortController();\n const { signal } = controller;\n port.addEventListener(\"message\", (event) => {\n if (event.data?.type === \"change\")\n listener(event.data.value);\n }, { signal });\n // addEventListener (unlike assigning onmessage) does not implicitly start\n // the port, so message delivery has to be kicked off explicitly.\n port.start();\n const detail = {\n selector,\n port: channel.port1,\n };\n dispatchEl.dispatchEvent(new CustomEvent(\"patchwork:subscribe\", {\n detail,\n bubbles: true,\n composed: true,\n }));\n return () => {\n if (signal.aborted)\n return;\n controller.abort();\n port.postMessage({ type: \"unsubscribe\" });\n port.close();\n };\n}\n/**\n * Answer a `patchwork:subscribe`. The `producer` receives a `respond`\n * callback it can call any number of times to push values to the consumer,\n * and may return a teardown that runs when the consumer unsubscribes. Stops\n * propagation so ancestor providers don't double-answer. Values emitted after\n * the consumer unsubscribes are dropped.\n */\nexport function accept(event, producer) {\n event.stopPropagation();\n const port = event.detail.port;\n let alive = true;\n const respond = (value) => {\n if (!alive)\n return;\n port.postMessage({ type: \"change\", value });\n };\n let teardown;\n try {\n teardown = producer(respond);\n }\n catch (err) {\n console.error(\"[patchwork-providers] subscribe producer threw:\", err);\n }\n const stop = () => {\n if (!alive)\n return;\n alive = false;\n try {\n teardown?.();\n }\n catch (err) {\n console.error(\"[patchwork-providers] subscribe teardown threw:\", err);\n }\n port.close();\n };\n port.onmessage = (event) => {\n if (event.data?.type === \"unsubscribe\")\n stop();\n };\n}\n/**\n * One-shot convenience wrapper over {@link subscribe}: opens a subscription\n * for `selector`, resolves with the first value a provider emits, then\n * immediately unsubscribes. Because there is no fallback provider, an\n * unclaimed selector never resolves \u2014 use `subscribe` directly if you need to\n * handle the \"no provider\" case.\n */\nexport function request(element, selector) {\n return new Promise((resolve) => {\n const unsubscribe = subscribe(element, selector, (value) => {\n // The first emission is always async (postMessage queues a task), so\n // `unsubscribe` is assigned by the time this fires.\n unsubscribe();\n resolve(value);\n });\n });\n}\nexport { registerRepoProviderElement, } from \"./repo-provider.js\";\n"],
5
+ "mappings": ";;;;;;;;;;;AAKA,SAAS,wCAAwC;;;ACW1C,SAAS,UAAU,SAAS,UAAU,UAAU;AACnD,QAAM,OAAO,QAAQ,QAAQ,gBAAgB;AAC7C,QAAM,aAAa,QAAQ;AAC3B,QAAM,UAAU,IAAI,eAAe;AACnC,QAAM,OAAO,QAAQ;AACrB,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,EAAE,OAAO,IAAI;AACnB,OAAK,iBAAiB,WAAW,CAAC,UAAU;AACxC,QAAI,MAAM,MAAM,SAAS;AACrB,eAAS,MAAM,KAAK,KAAK;AAAA,EACjC,GAAG,EAAE,OAAO,CAAC;AAGb,OAAK,MAAM;AACX,QAAM,SAAS;AAAA,IACX;AAAA,IACA,MAAM,QAAQ;AAAA,EAClB;AACA,aAAW,cAAc,IAAI,YAAY,uBAAuB;AAAA,IAC5D;AAAA,IACA,SAAS;AAAA,IACT,UAAU;AAAA,EACd,CAAC,CAAC;AACF,SAAO,MAAM;AACT,QAAI,OAAO;AACP;AACJ,eAAW,MAAM;AACjB,SAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AACxC,SAAK,MAAM;AAAA,EACf;AACJ;;;AD5BO,SAAS,oBACd,QACA,SACA;AACA,QAAM,SAAS,aAAa,SAAS;AACrC,UAAQ,YAAY,MAAM;AAE1B,MAAI,WAAW;AACf,QAAM,WAA2B,CAAC;AAGlC,QAAM,aAAqC,CAAC;AAC5C,MAAI;AAEJ,WAAS,mBAAmB,SAAiD;AAE3E,QAAI,QAAQ,UAAU;AACpB,iBAAW,QAAQ,QAAQ,IAAI,KAAK,IAAI;AAAA,IAC1C;AACA,mBAAe;AAAA,EACjB;AAEA,WAAS,iBAAiB;AACxB,UAAM,SAAS,OAAO;AACtB,UAAM,MAAM,KAAK,IAAI;AACrB,UAAM,UAAU;AAChB,UAAM,YAAY,OAAO,QAAQ,UAAU,EAAE;AAAA,MAC3C,CAAC,CAAC,IAAI,EAAE,MAAM,OAAO,YAAY,MAAM,KAAK;AAAA,IAC9C;AAEA,UAAM,YAAY,wBAAwB,MAAM;AAEhD,QAAI,WAAW;AACb,aAAO,UAAU,IAAI,yBAAyB;AAC9C,aAAO,MAAM,YAAY,4BAA4B,SAAS;AAAA,IAChE,OAAO;AACL,aAAO,UAAU,OAAO,yBAAyB;AACjD,aAAO,MAAM,eAAe,0BAA0B;AAAA,IACxD;AAAA,EACF;AAEA,SAAO,GAAG,qBAAqB,kBAAkB;AACjD,WAAS,KAAK,MAAM,OAAO,IAAI,qBAAqB,kBAAkB,CAAC;AAGvE,kBAAgB,YAAY,gBAAgB,GAAI;AAChD,WAAS,KAAK,MAAM,cAAc,aAAa,CAAC;AAGhD,QAAM,cAAc;AAAA,IAClB;AAAA,IACA,EAAE,MAAM,oBAAoB;AAAA,IAC5B,CAAC,eAAe;AACd,cAAQ,KACL,KAAiB,UAA0B,EAC3C,KAAK,CAAC,eAAe;AACpB,cAAM,UAAU,WAAW,IAAI;AAC/B,YAAI,SAAS,SAAS,cAAc;AAClC,qBAAW,QAAQ;AAAA,QACrB;AAAA,MACF,CAAC;AAAA,IACL;AAAA,EACF;AACA,WAAS,KAAK,WAAW;AAGzB,MAAI,OAAO,kBAAkB;AAC3B,YAAQ,KACL,KAA6B,OAAO,iBAAiB,GAAG,EACxD,KAAK,CAAC,kBAAkB;AACvB,YAAM,aAAa,cAAc,IAAI;AACrC,UAAI,YAAY,YAAY;AAC1B,gBAAQ,KACL,KAAiB,WAAW,UAAU,EACtC,KAAK,CAAC,eAAe;AACpB,gBAAM,UAAU,WAAW,IAAI;AAC/B,cAAI,SAAS,SAAS,cAAc;AAClC,uBAAW,QAAQ;AAAA,UACrB;AAAA,QACF,CAAC;AAAA,MACL;AAAA,IACF,CAAC;AAAA,EACL;AAEA,iBAAe,SAAS;AACtB,UAAM,UAAU,OAAO,IAAI;AAC3B,QAAI,CAAC,SAAS;AACZ,aAAO,MAAM,UAAU;AACvB;AAAA,IACF;AACA,WAAO,MAAM,UAAU;AAEvB,UAAM,eAAe,QAAQ,SAAS;AACtC,UAAM,OAAO,eAAe,QAAQ,OAAO;AAG3C,QAAI;AACJ,QAAI,gBAAgB,QAAQ,WAAW;AACrC,UAAI;AACF,cAAM,eAAe,MAAM,QAAQ,KAAK,KAAK,QAAQ,SAAS;AAC9D,uBAAe,iCAAiC,aAAa,GAAG;AAAA,MAClE,QAAQ;AAAA,MAER;AAAA,IACF;AACA,mBAAe,QAAQ,cAAc,IAAI;AAGzC,QAAI,gBAAgB,MAAM;AACxB,wBAAkB,QAAQ,EAAE,UAAU,YAAY,IAAI,EAAE,CAAC;AAAA,IAC3D,OAAO;AACL,wBAAkB,QAAQ,EAAE,UAAU,GAAG,CAAC;AAAA,IAC5C;AAEA,mBAAe;AAAA,EACjB;AAEA,SAAO,GAAG,UAAU,MAAM;AAC1B,WAAS,KAAK,MAAM,OAAO,IAAI,UAAU,MAAM,CAAC;AAChD,SAAO;AAEP,SAAO,MAAM;AACX,eAAW,WAAW,SAAU,SAAQ;AAAA,EAC1C;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ generateColorFromString
3
+ } from "./chunk-PZ7ORBQU.js";
4
+
5
+ // src/components/ContactCursor.ts
6
+ function renderContactCursor(handle, element) {
7
+ const token = document.createElement("div");
8
+ token.className = "contact-cursor-token";
9
+ element.appendChild(token);
10
+ function update() {
11
+ const contact = handle.doc();
12
+ if (!contact) {
13
+ token.style.display = "none";
14
+ return;
15
+ }
16
+ token.style.display = "";
17
+ const name = contact.type === "registered" ? contact.name : "Anonymous";
18
+ const color = generateColorFromString(handle.url);
19
+ token.style.setProperty("--contact-cursor-color", color);
20
+ token.textContent = name;
21
+ token.title = name;
22
+ }
23
+ handle.on("change", update);
24
+ update();
25
+ return () => {
26
+ handle.off("change", update);
27
+ token.remove();
28
+ };
29
+ }
30
+ export {
31
+ renderContactCursor
32
+ };
33
+ //# sourceMappingURL=ContactCursor-DTVFOQDW.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/components/ContactCursor.ts"],
4
+ "sourcesContent": ["import type { DocHandle } from \"@automerge/automerge-repo/slim\";\nimport type { ToolElement } from \"@inkandswitch/patchwork-plugins\";\nimport type { ContactDoc } from \"../types\";\nimport { generateColorFromString } from \"../ui\";\n\n/**\n * A compact cursor token: the contact's name on a pill in their presence\n * color. Hosts that render remote cursors (text editors, canvases) mount it\n * next to their own cursor primitive (caret line, pointer arrow) via\n * `<patchwork-view doc-url={contactUrl} tool-id=\"contact-cursor\">`.\n */\nexport function renderContactCursor(\n handle: DocHandle<ContactDoc>,\n element: ToolElement\n) {\n const token = document.createElement(\"div\");\n token.className = \"contact-cursor-token\";\n element.appendChild(token);\n\n function update() {\n const contact = handle.doc();\n if (!contact) {\n token.style.display = \"none\";\n return;\n }\n token.style.display = \"\";\n const name = contact.type === \"registered\" ? contact.name : \"Anonymous\";\n const color = generateColorFromString(handle.url);\n token.style.setProperty(\"--contact-cursor-color\", color);\n token.textContent = name;\n token.title = name;\n }\n\n handle.on(\"change\", update);\n update();\n\n return () => {\n handle.off(\"change\", update);\n token.remove();\n };\n}\n"],
5
+ "mappings": ";;;;;AAWO,SAAS,oBACd,QACA,SACA;AACA,QAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,QAAM,YAAY;AAClB,UAAQ,YAAY,KAAK;AAEzB,WAAS,SAAS;AAChB,UAAM,UAAU,OAAO,IAAI;AAC3B,QAAI,CAAC,SAAS;AACZ,YAAM,MAAM,UAAU;AACtB;AAAA,IACF;AACA,UAAM,MAAM,UAAU;AACtB,UAAM,OAAO,QAAQ,SAAS,eAAe,QAAQ,OAAO;AAC5D,UAAM,QAAQ,wBAAwB,OAAO,GAAG;AAChD,UAAM,MAAM,YAAY,0BAA0B,KAAK;AACvD,UAAM,cAAc;AACpB,UAAM,QAAQ;AAAA,EAChB;AAEA,SAAO,GAAG,UAAU,MAAM;AAC1B,SAAO;AAEP,SAAO,MAAM;AACX,WAAO,IAAI,UAAU,MAAM;AAC3B,UAAM,OAAO;AAAA,EACf;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,27 @@
1
+ // src/ui.ts
2
+ var USER_COLOR_PALETTE = [
3
+ { name: "Sky Blue", value: "hsl(200, 70%, 50%)" },
4
+ { name: "Coral", value: "hsl(10, 75%, 58%)" },
5
+ { name: "Emerald", value: "hsl(145, 70%, 45%)" },
6
+ { name: "Violet", value: "hsl(270, 70%, 55%)" },
7
+ { name: "Amber", value: "hsl(38, 85%, 50%)" },
8
+ { name: "Rose", value: "hsl(350, 70%, 55%)" },
9
+ { name: "Teal", value: "hsl(178, 70%, 45%)" },
10
+ { name: "Indigo", value: "hsl(235, 70%, 58%)" },
11
+ { name: "Lime", value: "hsl(85, 70%, 45%)" },
12
+ { name: "Fuchsia", value: "hsl(310, 70%, 55%)" },
13
+ { name: "Orange", value: "hsl(25, 80%, 52%)" },
14
+ { name: "Cyan", value: "hsl(188, 75%, 48%)" }
15
+ ];
16
+ function generateColorFromString(str) {
17
+ const hash = Math.abs(
18
+ str.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)
19
+ );
20
+ const colorIndex = hash % USER_COLOR_PALETTE.length;
21
+ return USER_COLOR_PALETTE[colorIndex].value;
22
+ }
23
+
24
+ export {
25
+ generateColorFromString
26
+ };
27
+ //# sourceMappingURL=chunk-PZ7ORBQU.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/ui.ts"],
4
+ "sourcesContent": ["/**\n * TODO: establish a system for referencing styles that\n * are shared across multiple tools.\n * This palette is also defined in the account-picker.\n *\n * User color palette for presence indicators (cursors, avatars, etc.)\n *\n * Colors are selected to:\n * - Be easily distinguishable from each other\n * - Work well on both light and dark backgrounds\n * - Be accessible and readable\n * - Look professional and modern\n */\nexport const USER_COLOR_PALETTE = [\n { name: \"Sky Blue\", value: \"hsl(200, 70%, 50%)\" },\n { name: \"Coral\", value: \"hsl(10, 75%, 58%)\" },\n { name: \"Emerald\", value: \"hsl(145, 70%, 45%)\" },\n { name: \"Violet\", value: \"hsl(270, 70%, 55%)\" },\n { name: \"Amber\", value: \"hsl(38, 85%, 50%)\" },\n { name: \"Rose\", value: \"hsl(350, 70%, 55%)\" },\n { name: \"Teal\", value: \"hsl(178, 70%, 45%)\" },\n { name: \"Indigo\", value: \"hsl(235, 70%, 58%)\" },\n { name: \"Lime\", value: \"hsl(85, 70%, 45%)\" },\n { name: \"Fuchsia\", value: \"hsl(310, 70%, 55%)\" },\n { name: \"Orange\", value: \"hsl(25, 80%, 52%)\" },\n { name: \"Cyan\", value: \"hsl(188, 75%, 48%)\" },\n] as const;\n\n/**\n * Generate a deterministic color from a string (like a contact url).\n * The single source of presence colors: cursors, avatars, and selection\n * tints all hash the contact url through this palette.\n */\nexport function generateColorFromString(str: string): string {\n const hash = Math.abs(\n str.split(\"\").reduce((acc, char) => acc + char.charCodeAt(0), 0)\n );\n const colorIndex = hash % USER_COLOR_PALETTE.length;\n return USER_COLOR_PALETTE[colorIndex].value;\n}\n"],
5
+ "mappings": ";AAaO,IAAM,qBAAqB;AAAA,EAChC,EAAE,MAAM,YAAY,OAAO,qBAAqB;AAAA,EAChD,EAAE,MAAM,SAAS,OAAO,oBAAoB;AAAA,EAC5C,EAAE,MAAM,WAAW,OAAO,qBAAqB;AAAA,EAC/C,EAAE,MAAM,UAAU,OAAO,qBAAqB;AAAA,EAC9C,EAAE,MAAM,SAAS,OAAO,oBAAoB;AAAA,EAC5C,EAAE,MAAM,QAAQ,OAAO,qBAAqB;AAAA,EAC5C,EAAE,MAAM,QAAQ,OAAO,qBAAqB;AAAA,EAC5C,EAAE,MAAM,UAAU,OAAO,qBAAqB;AAAA,EAC9C,EAAE,MAAM,QAAQ,OAAO,oBAAoB;AAAA,EAC3C,EAAE,MAAM,WAAW,OAAO,qBAAqB;AAAA,EAC/C,EAAE,MAAM,UAAU,OAAO,oBAAoB;AAAA,EAC7C,EAAE,MAAM,QAAQ,OAAO,qBAAqB;AAC9C;AAOO,SAAS,wBAAwB,KAAqB;AAC3D,QAAM,OAAO,KAAK;AAAA,IAChB,IAAI,MAAM,EAAE,EAAE,OAAO,CAAC,KAAK,SAAS,MAAM,KAAK,WAAW,CAAC,GAAG,CAAC;AAAA,EACjE;AACA,QAAM,aAAa,OAAO,mBAAmB;AAC7C,SAAO,mBAAmB,UAAU,EAAE;AACxC;",
6
+ "names": []
7
+ }
@@ -8,6 +8,7 @@
8
8
  --contact-border: var(--editor-fill-offset-10, #e5e5e5);
9
9
  --contact-fallback-bg: var(--editor-fill-offset-20, #ccc);
10
10
  --contact-fallback-fg: var(--editor-line-offset-30, #4d4d4d);
11
+ --contact-family: var(--editor-family-sans, system-ui, sans-serif);
11
12
  }
12
13
  }
13
14
  .contact-avatar {
@@ -78,4 +79,21 @@
78
79
  font-size: 0.875rem;
79
80
  color: var(--contact-muted-fg);
80
81
  }
82
+ .contact-cursor-token {
83
+ display: inline-block;
84
+ max-width: 160px;
85
+ padding: 2px 6px;
86
+ border-radius: 4px;
87
+ background: var(--contact-cursor-color, #888);
88
+ color: white;
89
+ font-family: var(--contact-family, system-ui, sans-serif);
90
+ font-size: 11px;
91
+ font-weight: 500;
92
+ line-height: 1.3;
93
+ white-space: nowrap;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ user-select: none;
97
+ pointer-events: none;
98
+ }
81
99
  /*# sourceMappingURL=tool.css.map */
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/styles.css"],
4
- "sourcesContent": ["@layer package {\n:root,\n:host,\n[theme] {\n --contact-fg: var(--editor-line, black);\n --contact-muted-fg: var(--editor-line-offset-50, #999);\n --contact-border: var(--editor-fill-offset-10, #e5e5e5);\n --contact-fallback-bg: var(--editor-fill-offset-20, #ccc);\n --contact-fallback-fg: var(--editor-line-offset-30, #4d4d4d);\n}\n}\n\n/* Avatar */\n\n.contact-avatar {\n position: relative;\n display: inline-flex;\n flex-shrink: 0;\n overflow: hidden;\n border-radius: 9999px;\n border: 1px solid var(--contact-border);\n min-width: 2rem;\n min-height: 2rem;\n max-width: 2rem;\n max-height: 2rem;\n height: 100%;\n width: 100%;\n}\n\n.contact-avatar--sm {\n min-width: initial;\n min-height: initial;\n max-width: initial;\n max-height: initial;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.contact-avatar--lg {\n min-width: initial;\n min-height: initial;\n max-width: initial;\n max-height: initial;\n width: 64px;\n height: 64px;\n}\n\n.contact-avatar--present {\n box-shadow: 0 0 0 2px var(--contact-presence-color);\n}\n\n.contact-avatar-image {\n position: absolute;\n inset: 0;\n z-index: 1;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.contact-avatar-fallback {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n background: var(--contact-fallback-bg);\n color: var(--contact-fallback-fg);\n}\n\n/* Contact Viewer */\n\n.contact-viewer {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.5rem;\n color: var(--contact-fg);\n}\n\n.contact-viewer-name {\n margin: 0;\n font-size: 1.25rem;\n font-weight: 600;\n}\n\n.contact-viewer-label {\n margin: 0;\n font-size: 0.875rem;\n color: var(--contact-muted-fg);\n}\n"],
5
- "mappings": ";AAAA;AACA;AAAA,EACA;AAAA,EACA,CAAC;AACC,kBAAc,IAAI,aAAa,EAAE;AACjC,wBAAoB,IAAI,uBAAuB,EAAE;AACjD,sBAAkB,IAAI,uBAAuB,EAAE;AAC/C,2BAAuB,IAAI,uBAAuB,EAAE;AACpD,2BAAuB,IAAI,uBAAuB,EAAE;AACtD;AACA;AAIA,CAAC;AACC,YAAU;AACV,WAAS;AACT,eAAa;AACb,YAAU;AACV,iBAAe;AACf,UAAQ,IAAI,MAAM,IAAI;AACtB,aAAW;AACX,cAAY;AACZ,aAAW;AACX,cAAY;AACZ,UAAQ;AACR,SAAO;AACT;AAEA,CAAC;AACC,aAAW;AACX,cAAY;AACZ,aAAW;AACX,cAAY;AACZ,SAAO;AACP,UAAQ;AACV;AAEA,CAAC;AACC,aAAW;AACX,cAAY;AACZ,aAAW;AACX,cAAY;AACZ,SAAO;AACP,UAAQ;AACV;AAEA,CAAC;AACC,cAAY,EAAE,EAAE,EAAE,IAAI,IAAI;AAC5B;AAEA,CAAC;AACC,YAAU;AACV,SAAO;AACP,WAAS;AACT,SAAO;AACP,UAAQ;AACR,cAAY;AACd;AAEA,CAAC;AACC,YAAU;AACV,SAAO;AACP,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,iBAAe;AACf,cAAY,IAAI;AAChB,SAAO,IAAI;AACb;AAIA,CAAC;AACC,WAAS;AACT,kBAAgB;AAChB,eAAa;AACb,OAAK;AACL,SAAO,IAAI;AACb;AAEA,CAAC;AACC,UAAQ;AACR,aAAW;AACX,eAAa;AACf;AAEA,CAAC;AACC,UAAQ;AACR,aAAW;AACX,SAAO,IAAI;AACb;",
4
+ "sourcesContent": ["@layer package {\n:root,\n:host,\n[theme] {\n --contact-fg: var(--editor-line, black);\n --contact-muted-fg: var(--editor-line-offset-50, #999);\n --contact-border: var(--editor-fill-offset-10, #e5e5e5);\n --contact-fallback-bg: var(--editor-fill-offset-20, #ccc);\n --contact-fallback-fg: var(--editor-line-offset-30, #4d4d4d);\n --contact-family: var(--editor-family-sans, system-ui, sans-serif);\n}\n}\n\n/* Avatar */\n\n.contact-avatar {\n position: relative;\n display: inline-flex;\n flex-shrink: 0;\n overflow: hidden;\n border-radius: 9999px;\n border: 1px solid var(--contact-border);\n min-width: 2rem;\n min-height: 2rem;\n max-width: 2rem;\n max-height: 2rem;\n height: 100%;\n width: 100%;\n}\n\n.contact-avatar--sm {\n min-width: initial;\n min-height: initial;\n max-width: initial;\n max-height: initial;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.contact-avatar--lg {\n min-width: initial;\n min-height: initial;\n max-width: initial;\n max-height: initial;\n width: 64px;\n height: 64px;\n}\n\n.contact-avatar--present {\n box-shadow: 0 0 0 2px var(--contact-presence-color);\n}\n\n.contact-avatar-image {\n position: absolute;\n inset: 0;\n z-index: 1;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.contact-avatar-fallback {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: 9999px;\n background: var(--contact-fallback-bg);\n color: var(--contact-fallback-fg);\n}\n\n/* Contact Viewer */\n\n.contact-viewer {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.5rem;\n color: var(--contact-fg);\n}\n\n.contact-viewer-name {\n margin: 0;\n font-size: 1.25rem;\n font-weight: 600;\n}\n\n.contact-viewer-label {\n margin: 0;\n font-size: 0.875rem;\n color: var(--contact-muted-fg);\n}\n\n/* Cursor token (tool id \"contact-cursor\") \u2014 the contact's name on a pill in\n their presence color. Hosts overlay it next to their own cursor primitive,\n so it must not intercept pointer events. The pill background is the user's\n presence color (not a theme surface), so the label uses white ink, which is\n legible across the whole USER_COLOR_PALETTE. */\n\n.contact-cursor-token {\n display: inline-block;\n max-width: 160px;\n padding: 2px 6px;\n border-radius: 4px;\n background: var(--contact-cursor-color, #888);\n color: white;\n font-family: var(--contact-family, system-ui, sans-serif);\n font-size: 11px;\n font-weight: 500;\n line-height: 1.3;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n user-select: none;\n pointer-events: none;\n}\n"],
5
+ "mappings": ";AAAA;AACA;AAAA,EACA;AAAA,EACA,CAAC;AACC,kBAAc,IAAI,aAAa,EAAE;AACjC,wBAAoB,IAAI,uBAAuB,EAAE;AACjD,sBAAkB,IAAI,uBAAuB,EAAE;AAC/C,2BAAuB,IAAI,uBAAuB,EAAE;AACpD,2BAAuB,IAAI,uBAAuB,EAAE;AACpD,sBAAkB,IAAI,oBAAoB,EAAE,SAAS,EAAE;AACzD;AACA;AAIA,CAAC;AACC,YAAU;AACV,WAAS;AACT,eAAa;AACb,YAAU;AACV,iBAAe;AACf,UAAQ,IAAI,MAAM,IAAI;AACtB,aAAW;AACX,cAAY;AACZ,aAAW;AACX,cAAY;AACZ,UAAQ;AACR,SAAO;AACT;AAEA,CAAC;AACC,aAAW;AACX,cAAY;AACZ,aAAW;AACX,cAAY;AACZ,SAAO;AACP,UAAQ;AACV;AAEA,CAAC;AACC,aAAW;AACX,cAAY;AACZ,aAAW;AACX,cAAY;AACZ,SAAO;AACP,UAAQ;AACV;AAEA,CAAC;AACC,cAAY,EAAE,EAAE,EAAE,IAAI,IAAI;AAC5B;AAEA,CAAC;AACC,YAAU;AACV,SAAO;AACP,WAAS;AACT,SAAO;AACP,UAAQ;AACR,cAAY;AACd;AAEA,CAAC;AACC,YAAU;AACV,SAAO;AACP,WAAS;AACT,eAAa;AACb,mBAAiB;AACjB,iBAAe;AACf,cAAY,IAAI;AAChB,SAAO,IAAI;AACb;AAIA,CAAC;AACC,WAAS;AACT,kBAAgB;AAChB,eAAa;AACb,OAAK;AACL,SAAO,IAAI;AACb;AAEA,CAAC;AACC,UAAQ;AACR,aAAW;AACX,eAAa;AACf;AAEA,CAAC;AACC,UAAQ;AACR,aAAW;AACX,SAAO,IAAI;AACb;AAQA,CAAC;AACC,WAAS;AACT,aAAW;AACX,WAAS,IAAI;AACb,iBAAe;AACf,cAAY,IAAI,sBAAsB,EAAE;AACxC,SAAO;AACP,eAAa,IAAI,gBAAgB,EAAE,SAAS,EAAE;AAC9C,aAAW;AACX,eAAa;AACb,eAAa;AACb,eAAa;AACb,YAAU;AACV,iBAAe;AACf,eAAa;AACb,kBAAgB;AAClB;",
6
6
  "names": []
7
7
  }
@@ -41,7 +41,7 @@ var plugins = [
41
41
  name: "Contact Avatar",
42
42
  supportedDatatypes: ["contact"],
43
43
  async load() {
44
- const { renderContactAvatar } = await import("./ContactAvatar-MKNZ5KEX.js");
44
+ const { renderContactAvatar } = await import("./ContactAvatar-B4QQC47I.js");
45
45
  const css = await loadStyles();
46
46
  return (handle, element) => {
47
47
  addStyles(document.head, css);
@@ -62,6 +62,20 @@ var plugins = [
62
62
  return renderInlineContactAvatar(handle, element);
63
63
  };
64
64
  }
65
+ },
66
+ {
67
+ type: "patchwork:tool",
68
+ id: "contact-cursor",
69
+ name: "Contact Cursor",
70
+ supportedDatatypes: ["contact"],
71
+ async load() {
72
+ const { renderContactCursor } = await import("./ContactCursor-DTVFOQDW.js");
73
+ const css = await loadStyles();
74
+ return (handle, element) => {
75
+ addStyles(document.head, css);
76
+ return renderContactCursor(handle, element);
77
+ };
78
+ }
65
79
  }
66
80
  ];
67
81
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/tool.ts"],
4
- "sourcesContent": ["import type { ToolImplementation } from \"@inkandswitch/patchwork-plugins\";\nimport \"./styles.css\";\n\nfunction addStyles(element: HTMLElement, textContent: string) {\n const id = \"contact-styles\";\n const el = element.querySelector(`#${id}`) ?? document.createElement(\"style\");\n Object.assign(el, { textContent, id });\n element.append(el);\n}\n\nasync function loadStyles() {\n const url = new URL(\"./tool.css\", import.meta.url);\n return (await fetch(url)).text();\n}\n\nexport const plugins = [\n {\n type: \"patchwork:datatype\",\n id: \"contact\",\n name: \"Contact\",\n icon: \"User\",\n unlisted: true,\n async load() {\n const { ContactDatatype } = await import(\"./datatype\");\n return ContactDatatype;\n },\n },\n {\n type: \"patchwork:tool\",\n id: \"contact\",\n name: \"Contact Viewer\",\n supportedDatatypes: [\"contact\"],\n async load(): Promise<ToolImplementation> {\n const { renderContactViewer } =\n await import(\"./components/ContactViewer\");\n const css = await loadStyles();\n return (handle, element) => {\n addStyles(document.head, css);\n return renderContactViewer(handle, element);\n };\n },\n },\n {\n type: \"patchwork:tool\",\n id: \"contact-avatar\",\n name: \"Contact Avatar\",\n supportedDatatypes: [\"contact\"],\n async load(): Promise<ToolImplementation> {\n const { renderContactAvatar } =\n await import(\"./components/ContactAvatar\");\n const css = await loadStyles();\n return (handle, element) => {\n addStyles(document.head, css);\n return renderContactAvatar(handle, element);\n };\n },\n },\n {\n type: \"patchwork:tool\",\n id: \"contact-inline\",\n name: \"Inline Contact Avatar\",\n supportedDatatypes: [\"contact\"],\n async load(): Promise<ToolImplementation> {\n const { renderInlineContactAvatar } =\n await import(\"./components/InlineContactAvatar\");\n const css = await loadStyles();\n return (handle, element) => {\n addStyles(document.head, css);\n return renderInlineContactAvatar(handle, element);\n };\n },\n },\n];\n"],
5
- "mappings": ";AAGA,SAAS,UAAU,SAAsB,aAAqB;AAC5D,QAAM,KAAK;AACX,QAAM,KAAK,QAAQ,cAAc,IAAI,EAAE,EAAE,KAAK,SAAS,cAAc,OAAO;AAC5E,SAAO,OAAO,IAAI,EAAE,aAAa,GAAG,CAAC;AACrC,UAAQ,OAAO,EAAE;AACnB;AAEA,eAAe,aAAa;AAC1B,QAAM,MAAM,IAAI,IAAI,cAAc,YAAY,GAAG;AACjD,UAAQ,MAAM,MAAM,GAAG,GAAG,KAAK;AACjC;AAEO,IAAM,UAAU;AAAA,EACrB;AAAA,IACE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM,OAAO;AACX,YAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,wBAAY;AACrD,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,oBAAoB,CAAC,SAAS;AAAA,IAC9B,MAAM,OAAoC;AACxC,YAAM,EAAE,oBAAoB,IAC1B,MAAM,OAAO,6BAA4B;AAC3C,YAAM,MAAM,MAAM,WAAW;AAC7B,aAAO,CAAC,QAAQ,YAAY;AAC1B,kBAAU,SAAS,MAAM,GAAG;AAC5B,eAAO,oBAAoB,QAAQ,OAAO;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,oBAAoB,CAAC,SAAS;AAAA,IAC9B,MAAM,OAAoC;AACxC,YAAM,EAAE,oBAAoB,IAC1B,MAAM,OAAO,6BAA4B;AAC3C,YAAM,MAAM,MAAM,WAAW;AAC7B,aAAO,CAAC,QAAQ,YAAY;AAC1B,kBAAU,SAAS,MAAM,GAAG;AAC5B,eAAO,oBAAoB,QAAQ,OAAO;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,oBAAoB,CAAC,SAAS;AAAA,IAC9B,MAAM,OAAoC;AACxC,YAAM,EAAE,0BAA0B,IAChC,MAAM,OAAO,mCAAkC;AACjD,YAAM,MAAM,MAAM,WAAW;AAC7B,aAAO,CAAC,QAAQ,YAAY;AAC1B,kBAAU,SAAS,MAAM,GAAG;AAC5B,eAAO,0BAA0B,QAAQ,OAAO;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { ToolImplementation } from \"@inkandswitch/patchwork-plugins\";\nimport \"./styles.css\";\n\nfunction addStyles(element: HTMLElement, textContent: string) {\n const id = \"contact-styles\";\n const el = element.querySelector(`#${id}`) ?? document.createElement(\"style\");\n Object.assign(el, { textContent, id });\n element.append(el);\n}\n\nasync function loadStyles() {\n const url = new URL(\"./tool.css\", import.meta.url);\n return (await fetch(url)).text();\n}\n\nexport const plugins = [\n {\n type: \"patchwork:datatype\",\n id: \"contact\",\n name: \"Contact\",\n icon: \"User\",\n unlisted: true,\n async load() {\n const { ContactDatatype } = await import(\"./datatype\");\n return ContactDatatype;\n },\n },\n {\n type: \"patchwork:tool\",\n id: \"contact\",\n name: \"Contact Viewer\",\n supportedDatatypes: [\"contact\"],\n async load(): Promise<ToolImplementation> {\n const { renderContactViewer } =\n await import(\"./components/ContactViewer\");\n const css = await loadStyles();\n return (handle, element) => {\n addStyles(document.head, css);\n return renderContactViewer(handle, element);\n };\n },\n },\n {\n type: \"patchwork:tool\",\n id: \"contact-avatar\",\n name: \"Contact Avatar\",\n supportedDatatypes: [\"contact\"],\n async load(): Promise<ToolImplementation> {\n const { renderContactAvatar } =\n await import(\"./components/ContactAvatar\");\n const css = await loadStyles();\n return (handle, element) => {\n addStyles(document.head, css);\n return renderContactAvatar(handle, element);\n };\n },\n },\n {\n type: \"patchwork:tool\",\n id: \"contact-inline\",\n name: \"Inline Contact Avatar\",\n supportedDatatypes: [\"contact\"],\n async load(): Promise<ToolImplementation> {\n const { renderInlineContactAvatar } =\n await import(\"./components/InlineContactAvatar\");\n const css = await loadStyles();\n return (handle, element) => {\n addStyles(document.head, css);\n return renderInlineContactAvatar(handle, element);\n };\n },\n },\n {\n type: \"patchwork:tool\",\n id: \"contact-cursor\",\n name: \"Contact Cursor\",\n supportedDatatypes: [\"contact\"],\n async load(): Promise<ToolImplementation> {\n const { renderContactCursor } =\n await import(\"./components/ContactCursor\");\n const css = await loadStyles();\n return (handle, element) => {\n addStyles(document.head, css);\n return renderContactCursor(handle, element);\n };\n },\n },\n];\n"],
5
+ "mappings": ";AAGA,SAAS,UAAU,SAAsB,aAAqB;AAC5D,QAAM,KAAK;AACX,QAAM,KAAK,QAAQ,cAAc,IAAI,EAAE,EAAE,KAAK,SAAS,cAAc,OAAO;AAC5E,SAAO,OAAO,IAAI,EAAE,aAAa,GAAG,CAAC;AACrC,UAAQ,OAAO,EAAE;AACnB;AAEA,eAAe,aAAa;AAC1B,QAAM,MAAM,IAAI,IAAI,cAAc,YAAY,GAAG;AACjD,UAAQ,MAAM,MAAM,GAAG,GAAG,KAAK;AACjC;AAEO,IAAM,UAAU;AAAA,EACrB;AAAA,IACE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,UAAU;AAAA,IACV,MAAM,OAAO;AACX,YAAM,EAAE,gBAAgB,IAAI,MAAM,OAAO,wBAAY;AACrD,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,oBAAoB,CAAC,SAAS;AAAA,IAC9B,MAAM,OAAoC;AACxC,YAAM,EAAE,oBAAoB,IAC1B,MAAM,OAAO,6BAA4B;AAC3C,YAAM,MAAM,MAAM,WAAW;AAC7B,aAAO,CAAC,QAAQ,YAAY;AAC1B,kBAAU,SAAS,MAAM,GAAG;AAC5B,eAAO,oBAAoB,QAAQ,OAAO;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,oBAAoB,CAAC,SAAS;AAAA,IAC9B,MAAM,OAAoC;AACxC,YAAM,EAAE,oBAAoB,IAC1B,MAAM,OAAO,6BAA4B;AAC3C,YAAM,MAAM,MAAM,WAAW;AAC7B,aAAO,CAAC,QAAQ,YAAY;AAC1B,kBAAU,SAAS,MAAM,GAAG;AAC5B,eAAO,oBAAoB,QAAQ,OAAO;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,oBAAoB,CAAC,SAAS;AAAA,IAC9B,MAAM,OAAoC;AACxC,YAAM,EAAE,0BAA0B,IAChC,MAAM,OAAO,mCAAkC;AACjD,YAAM,MAAM,MAAM,WAAW;AAC7B,aAAO,CAAC,QAAQ,YAAY;AAC1B,kBAAU,SAAS,MAAM,GAAG;AAC5B,eAAO,0BAA0B,QAAQ,OAAO;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,oBAAoB,CAAC,SAAS;AAAA,IAC9B,MAAM,OAAoC;AACxC,YAAM,EAAE,oBAAoB,IAC1B,MAAM,OAAO,6BAA4B;AAC3C,YAAM,MAAM,MAAM,WAAW;AAC7B,aAAO,CAAC,QAAQ,YAAY;AAC1B,kBAAU,SAAS,MAAM,GAAG;AAC5B,eAAO,oBAAoB,QAAQ,OAAO;AAAA,MAC5C;AAAA,IACF;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -3358,8 +3358,114 @@ function urlFromText(line) {
3358
3358
  return null;
3359
3359
  }
3360
3360
 
3361
- // src/tool.tsx
3361
+ // src/CollaboratorCursor.tsx
3362
3362
  var import_jsx_runtime4 = __toESM(require_jsx_runtime(), 1);
3363
+ var USER_COLOR_PALETTE = [
3364
+ "hsl(200, 70%, 50%)",
3365
+ "hsl(10, 75%, 58%)",
3366
+ "hsl(145, 70%, 45%)",
3367
+ "hsl(270, 70%, 55%)",
3368
+ "hsl(38, 85%, 50%)",
3369
+ "hsl(350, 70%, 55%)",
3370
+ "hsl(178, 70%, 45%)",
3371
+ "hsl(235, 70%, 58%)",
3372
+ "hsl(85, 70%, 45%)",
3373
+ "hsl(310, 70%, 55%)",
3374
+ "hsl(25, 80%, 52%)",
3375
+ "hsl(188, 75%, 48%)"
3376
+ ];
3377
+ function generateColorFromString(str) {
3378
+ const hash = Math.abs(
3379
+ str.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0)
3380
+ );
3381
+ return USER_COLOR_PALETTE[hash % USER_COLOR_PALETTE.length];
3382
+ }
3383
+ function splitPresenceUserId(userId) {
3384
+ if (!userId.startsWith("automerge:")) return { actorId: userId };
3385
+ const i = userId.indexOf("-");
3386
+ if (i === -1) return { contactUrl: userId };
3387
+ return {
3388
+ contactUrl: userId.slice(0, i),
3389
+ actorId: userId.slice(i + 1)
3390
+ };
3391
+ }
3392
+ function ContactCollaboratorCursor({
3393
+ className,
3394
+ point,
3395
+ zoom,
3396
+ color,
3397
+ name,
3398
+ userId
3399
+ }) {
3400
+ if (!point) return null;
3401
+ const { contactUrl } = splitPresenceUserId(userId);
3402
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
3403
+ "div",
3404
+ {
3405
+ className: className ? `tl-overlays__item ${className}` : "tl-overlays__item",
3406
+ style: {
3407
+ transform: `translate(${point.x}px, ${point.y}px) scale(${1 / zoom})`
3408
+ },
3409
+ children: [
3410
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
3411
+ "svg",
3412
+ {
3413
+ className: "tl-cursor",
3414
+ width: "32",
3415
+ height: "32",
3416
+ viewBox: "0 0 32 32",
3417
+ fill: "none",
3418
+ "aria-hidden": "true",
3419
+ children: [
3420
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { fill: "rgba(0,0,0,.2)", transform: "translate(-11,-11)", children: [
3421
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z" }),
3422
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z" })
3423
+ ] }),
3424
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("g", { style: { color }, transform: "translate(-12,-12)", children: [
3425
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3426
+ "path",
3427
+ {
3428
+ d: "m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z",
3429
+ fill: "white"
3430
+ }
3431
+ ),
3432
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3433
+ "path",
3434
+ {
3435
+ d: "m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z",
3436
+ fill: "white"
3437
+ }
3438
+ ),
3439
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3440
+ "path",
3441
+ {
3442
+ d: "m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z",
3443
+ fill: "currentColor"
3444
+ }
3445
+ ),
3446
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3447
+ "path",
3448
+ {
3449
+ d: "m13 10.814v11.188l2.969-2.866.428-.139h4.768z",
3450
+ fill: "currentColor"
3451
+ }
3452
+ )
3453
+ ] })
3454
+ ]
3455
+ }
3456
+ ),
3457
+ contactUrl ? (
3458
+ // Same offset as tldraw's .tl-nametag so the token hangs off the
3459
+ // arrow tip.
3460
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { position: "absolute", top: 16, left: 13 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("patchwork-view", { "doc-url": contactUrl, "tool-id": "contact-cursor" }) })
3461
+ ) : name && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "tl-nametag", style: { backgroundColor: color }, children: name })
3462
+ ]
3463
+ }
3464
+ );
3465
+ }
3466
+
3467
+ // src/tool.tsx
3468
+ var import_jsx_runtime5 = __toESM(require_jsx_runtime(), 1);
3363
3469
  var customShapeUtils = [PatchworkDocShapeUtil];
3364
3470
  var customTools = [NewDocShapeTool];
3365
3471
  var MIME_TO_EXT = {
@@ -3395,16 +3501,9 @@ function useContactInfo() {
3395
3501
  // this prevents tldraw from hiding local cursors across multiple sessions under the same account.
3396
3502
  userId: contactUrl ? `${contactUrl}-${repo.peerId}` : repo.peerId,
3397
3503
  name: contactDoc?.name ?? "Anonymous",
3398
- color: contactDoc?.color
3399
- };
3400
- }
3401
- function splitPresenceUserId(userId) {
3402
- if (!userId.startsWith("automerge:")) return { actorId: userId };
3403
- const i = userId.indexOf("-");
3404
- if (i === -1) return { contactUrl: userId };
3405
- return {
3406
- contactUrl: userId.slice(0, i),
3407
- actorId: userId.slice(i + 1)
3504
+ // Derived from the contact url with the same rule the contact-cursor
3505
+ // token uses, so the cursor arrow and the token always match.
3506
+ color: contactUrl ? generateColorFromString(contactUrl) : void 0
3408
3507
  };
3409
3508
  }
3410
3509
  function TldrawTool({
@@ -3440,10 +3539,14 @@ function TldrawTool({
3440
3539
  }, [diff, statusAtom]);
3441
3540
  useDeletedGhosts(store, handle, diff);
3442
3541
  const components = (0, import_react10.useMemo)(
3443
- () => ({ ShapeWrapper: DiffShapeWrapper, Toolbar: NewDocToolbar }),
3542
+ () => ({
3543
+ ShapeWrapper: DiffShapeWrapper,
3544
+ Toolbar: NewDocToolbar,
3545
+ CollaboratorCursor: ContactCollaboratorCursor
3546
+ }),
3444
3547
  []
3445
3548
  );
3446
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(DiffStatusContext.Provider, { value: statusAtom, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3549
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(DiffStatusContext.Provider, { value: statusAtom, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3447
3550
  Tldraw,
3448
3551
  {
3449
3552
  inferDarkMode: true,
@@ -3453,7 +3556,7 @@ function TldrawTool({
3453
3556
  tools: customTools,
3454
3557
  overrides: newDocUiOverrides,
3455
3558
  components,
3456
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TldrawInner, { docUrl, element, readOnly })
3559
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TldrawInner, { docUrl, element, readOnly })
3457
3560
  }
3458
3561
  ) });
3459
3562
  }
@@ -3681,7 +3784,7 @@ function usePatchworkDrop(element) {
3681
3784
  function render(handle, element) {
3682
3785
  const root = (0, import_client.createRoot)(element);
3683
3786
  root.render(
3684
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(RepoContext.Provider, { value: element.repo, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TldrawTool, { docUrl: handle.url, element }) })
3787
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(RepoContext.Provider, { value: element.repo, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(TldrawTool, { docUrl: handle.url, element }) })
3685
3788
  );
3686
3789
  return () => root.unmount();
3687
3790
  }