@unhingged/vizu-core 0.1.1 → 0.1.3

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.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/live-collab.ts","../src/cursor-colors.ts","../src/presence-stack.ts","../src/selection-overlay.ts"],"sourcesContent":["import { createClient, type Client, type Room } from '@liveblocks/client';\nimport { PresenceStack, type PresenceUser } from './presence-stack';\nimport { SelectionOverlay, type SelectionUser } from './selection-overlay';\nimport { colorForUser, prefersReducedMotion } from './cursor-colors';\nimport type { ElementFingerprint } from './types';\n\n/**\n * Live cursors + presence module.\n *\n * Vanilla DOM, no React. Renders other users' cursors as plain `<div>`s\n * with CSS transforms (GPU-accelerated). Smoothing via a single CSS\n * transition on `transform` — no RAF loop needed.\n *\n * Coords are viewport-relative percents — at different viewport widths,\n * a 50%×30% cursor lands at different physical positions, which we\n * accept as the trade for not having to anchor cursors to fingerprinted\n * DOM elements (see plans/live-collab.md → \"viewport-relative %\" pick).\n * Selection-sync in Slice 5 uses DOM-anchored coords for accuracy.\n *\n * Lifecycle:\n * start() → create client, enter room, attach mouse listener\n * destroy() → leave room, remove cursors, drop listeners\n *\n * Plan-gate (Pro+ for `live_collab` feature) happens server-side at the\n * auth endpoint. If that returns 402, `enterRoom` throws and we leave\n * the cursor surface empty — silently. No 402 → no rendering, but no\n * crash either; live collab is opt-in upgrade UX, not a gate.\n *\n * See plans/live-collab.md → Slice 2.\n */\n\n/* ─────────────────────── presence types ────────────────────── */\n\nexport type LivePresence = {\n cursor: { xPct: number; yPct: number } | null;\n /**\n * Absolute scroll position of the user's window. Broadcast at ~10Hz\n * while they scroll. Consumed by follow mode (Slice 4) to keep a\n * follower's viewport locked to the followee's.\n */\n scrollY: number;\n /**\n * The element the user is currently hovering in highlighter mode —\n * fingerprinted so the receiving client resolves it against its own\n * DOM regardless of viewport differences. Null when not in\n * highlighter mode or hovering nothing. Slice 5 of live-collab.md.\n *\n * Wire format is JSON string because Liveblocks presence values must\n * satisfy JsonObject (strict index signature), and ElementFingerprint\n * doesn't expose one. JSON-encoding sidesteps the type wrangling.\n */\n selecting: { fingerprintJson: string } | null;\n};\n\nexport type LiveUserMeta = {\n id: string;\n info: {\n name: string;\n avatar?: string;\n role?: string;\n };\n};\n\n/* ──────────────────────── tuning knobs ─────────────────────── */\n\nconst MOUSEMOVE_THROTTLE_MS = 50; // 20 Hz — matches Liveblocks' recommendation\nconst SCROLL_THROTTLE_MS = 100; // 10 Hz — follow mode tolerates more lag than cursors\nconst INACTIVITY_FADE_MS = 5000;\nconst CURSOR_TRANSITION_MS = 110; // a touch slower than the throttle for smooth feel\nconst Z_INDEX = 2147483641; // 1 above @vizu/core's other UI\n/** Minimum delta in px before a followee's scrollY triggers our scroll. */\nconst FOLLOW_SCROLL_THRESHOLD_PX = 8;\n\n/* ──────────────────────── room id ──────────────────────────── */\n\n/**\n * 32-bit FNV-1a → 8-char lowercase hex. Must match the server's\n * `fnv1a32Hex` in apps/landing/lib/liveblocks.ts so client and server\n * compute the same room id from the same URL.\n */\nfunction fnv1a32Hex(input: string): string {\n let h = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h ^= input.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return (h >>> 0).toString(16).padStart(8, '0');\n}\n\nexport function roomIdFor(workspaceSlug: string, pageUrl: string): string {\n return `ws_${workspaceSlug}__pg_${fnv1a32Hex(pageUrl)}`;\n}\n\n/* ──────────────────────── module ───────────────────────────── */\n\ntype CursorEntry = {\n el: HTMLDivElement;\n fadeTimer: number | null;\n};\n\nexport class LiveCollab {\n private client: Client<LiveUserMeta> | null = null;\n private room: Room<LivePresence, never, LiveUserMeta> | null = null;\n private leave: (() => void) | null = null;\n private container: HTMLDivElement | null = null;\n private cursors = new Map<number, CursorEntry>(); // keyed by connectionId\n private presenceStack: PresenceStack | null = null;\n private followPill: FollowPill | null = null;\n private selectionOverlay: SelectionOverlay | null = null;\n private mouseListener: ((e: MouseEvent) => void) | null = null;\n private scrollListener: (() => void) | null = null;\n private visibilityListener: (() => void) | null = null;\n private keydownListener: ((e: KeyboardEvent) => void) | null = null;\n private statusIndicator: ConnectionIndicator | null = null;\n private throttleTimer: number | null = null;\n private scrollThrottleTimer: number | null = null;\n private pendingCursor: { xPct: number; yPct: number } | null = null;\n private lastBroadcast = 0;\n private lastScrollBroadcast = 0;\n private unsubscribers: Array<() => void> = [];\n private destroyed = false;\n // Follow mode state\n /** Clerk user id we're currently following, or null. */\n private followingUserId: string | null = null;\n /** Last scrollY we received from the followee — for delta gating. */\n private lastFolloweeScrollY: number | null = null;\n\n constructor(\n private opts: {\n publicKey: string;\n authEndpoint: string;\n workspaceSlug: string;\n /** Page URL the room scopes to. Defaults to `location.origin + location.pathname`. */\n pageUrl?: string;\n },\n ) {}\n\n async start(): Promise<void> {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n if (this.destroyed) return;\n\n try {\n // Authenticated client: only authEndpoint is wired. The server's\n // /api/liveblocks-auth signs a scoped token using LIVEBLOCKS_SECRET_KEY,\n // so the browser never needs the public key for this flow. We still\n // accept `publicKey` in the config — useful for future Storage mode\n // or as a parity hint with Liveblocks vocabulary — but it's not\n // passed to createClient (the two modes are mutually exclusive).\n void this.opts.publicKey;\n this.client = createClient<LiveUserMeta>({\n authEndpoint: async (room) => {\n const res = await fetch(this.opts.authEndpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ room }),\n });\n if (!res.ok) {\n // 402 plan_limit / 401 unauthorized / 503 not configured all land\n // here. Throwing tells Liveblocks not to retry; the room stays\n // unentered and the cursor surface is silently empty.\n throw new Error(`vizu-liveblocks-auth: HTTP ${res.status}`);\n }\n return res.json();\n },\n });\n\n const pageUrl = this.opts.pageUrl ?? window.location.origin + window.location.pathname;\n const room = this.opts.workspaceSlug ? roomIdFor(this.opts.workspaceSlug, pageUrl) : null;\n if (!room) return;\n\n const result = this.client.enterRoom<LivePresence, never>(room, {\n initialPresence: {\n cursor: null,\n scrollY: typeof window !== 'undefined' ? window.scrollY : 0,\n selecting: null,\n },\n });\n this.room = result.room as Room<LivePresence, never, LiveUserMeta>;\n this.leave = result.leave;\n\n this.mountContainer();\n this.presenceStack = new PresenceStack({\n onAvatarClick: (userId) => this.setFollowing(userId),\n });\n this.presenceStack.mount();\n this.followPill = new FollowPill({\n onStop: () => this.setFollowing(null),\n });\n this.selectionOverlay = new SelectionOverlay();\n this.selectionOverlay.mount();\n this.statusIndicator = new ConnectionIndicator();\n this.statusIndicator.mount();\n this.subscribeOthers();\n this.subscribeStatus();\n this.attachMouseListener();\n this.attachScrollListener();\n this.attachVisibilityListener();\n this.attachKeyboardListener();\n } catch (err) {\n if (typeof console !== 'undefined') {\n console.warn('[vizu/live-collab] start failed; live cursors disabled:', err);\n }\n this.cleanup();\n }\n }\n\n destroy(): void {\n this.destroyed = true;\n this.cleanup();\n }\n\n /* ────────────────── private ────────────────── */\n\n private cleanup(): void {\n if (this.mouseListener) {\n window.removeEventListener('mousemove', this.mouseListener);\n this.mouseListener = null;\n }\n if (this.scrollListener) {\n window.removeEventListener('scroll', this.scrollListener);\n this.scrollListener = null;\n }\n if (this.visibilityListener) {\n document.removeEventListener('visibilitychange', this.visibilityListener);\n this.visibilityListener = null;\n }\n if (this.keydownListener) {\n document.removeEventListener('keydown', this.keydownListener);\n this.keydownListener = null;\n }\n if (this.throttleTimer != null) {\n window.clearTimeout(this.throttleTimer);\n this.throttleTimer = null;\n }\n if (this.scrollThrottleTimer != null) {\n window.clearTimeout(this.scrollThrottleTimer);\n this.scrollThrottleTimer = null;\n }\n for (const unsub of this.unsubscribers) unsub();\n this.unsubscribers = [];\n for (const c of this.cursors.values()) {\n if (c.fadeTimer != null) window.clearTimeout(c.fadeTimer);\n c.el.remove();\n }\n this.cursors.clear();\n this.presenceStack?.destroy();\n this.presenceStack = null;\n this.followPill?.destroy();\n this.followPill = null;\n this.selectionOverlay?.destroy();\n this.selectionOverlay = null;\n this.statusIndicator?.destroy();\n this.statusIndicator = null;\n this.followingUserId = null;\n this.lastFolloweeScrollY = null;\n this.container?.remove();\n this.container = null;\n this.leave?.();\n this.leave = null;\n this.room = null;\n this.client = null;\n }\n\n /**\n * Public API: broadcast the local user's currently-hovered element\n * (highlighter mode) so other clients can render a ghost outline on\n * it. Pass null to clear (highlighter stopped, popover opened, etc.).\n * Idempotent on no-op transitions. Slice 5 of live-collab.md.\n */\n setLocalSelection(fingerprint: ElementFingerprint | null): void {\n if (!this.room) return;\n this.room.updatePresence({\n selecting: fingerprint ? { fingerprintJson: JSON.stringify(fingerprint) } : null,\n });\n }\n\n /**\n * Public API: start/stop following a user by Clerk id. Null clears.\n * Called by the avatar click handler and by the FollowPill stop button.\n * Idempotent — same id again is a no-op.\n */\n setFollowing(userId: string | null): void {\n if (this.followingUserId === userId) return;\n this.followingUserId = userId;\n this.lastFolloweeScrollY = null;\n this.presenceStack?.setFollowing(userId);\n this.followPill?.setFollowing(userId ? this.lookupUserForPill(userId) : null);\n // If we just started following someone whose scroll position we\n // already know from the latest others snapshot, jump to it now.\n if (userId) this.scrollToFolloweeIfKnown();\n }\n\n private lookupUserForPill(userId: string): PresenceUser | null {\n if (!this.room) return null;\n const others = this.room.getOthers();\n for (const o of others) {\n const meta = o as unknown as { id?: string; info?: LiveUserMeta['info'] };\n if ((meta.id ?? `c${o.connectionId}`) === userId) {\n return {\n id: userId,\n name: meta.info?.name ?? 'Anonymous',\n color: colorForUser(userId),\n avatar: meta.info?.avatar,\n };\n }\n }\n return null;\n }\n\n private scrollToFolloweeIfKnown(): void {\n if (!this.followingUserId || !this.room) return;\n const others = this.room.getOthers();\n for (const o of others) {\n const meta = o as unknown as { id?: string };\n if ((meta.id ?? `c${o.connectionId}`) === this.followingUserId) {\n const presence = o.presence as LivePresence;\n if (typeof presence.scrollY === 'number') {\n this.followScrollTo(presence.scrollY);\n }\n return;\n }\n }\n }\n\n private followScrollTo(scrollY: number): void {\n if (this.lastFolloweeScrollY !== null && Math.abs(this.lastFolloweeScrollY - scrollY) < FOLLOW_SCROLL_THRESHOLD_PX) {\n return;\n }\n this.lastFolloweeScrollY = scrollY;\n window.scrollTo({ top: scrollY, behavior: 'smooth' });\n }\n\n private mountContainer(): void {\n this.container = document.createElement('div');\n this.container.setAttribute('data-vizu-live-cursors', '');\n this.container.style.cssText = `\n position: fixed; inset: 0;\n pointer-events: none;\n z-index: ${Z_INDEX};\n contain: strict;\n `;\n document.body.appendChild(this.container);\n }\n\n private attachMouseListener(): void {\n this.mouseListener = (e: MouseEvent) => {\n const xPct = (e.clientX / window.innerWidth) * 100;\n const yPct = (e.clientY / window.innerHeight) * 100;\n this.pendingCursor = { xPct, yPct };\n this.flushThrottled();\n };\n window.addEventListener('mousemove', this.mouseListener, { passive: true });\n }\n\n private attachScrollListener(): void {\n // Followers track our scroll position via this broadcast. Throttled\n // to 10 Hz — scroll-chasing tolerates more lag than cursor motion\n // and it keeps our presence-write rate well under Liveblocks limits.\n this.scrollListener = () => {\n const now = performance.now();\n const elapsed = now - this.lastScrollBroadcast;\n if (elapsed >= SCROLL_THROTTLE_MS) {\n this.flushScroll();\n return;\n }\n if (this.scrollThrottleTimer == null) {\n this.scrollThrottleTimer = window.setTimeout(() => {\n this.scrollThrottleTimer = null;\n this.flushScroll();\n }, SCROLL_THROTTLE_MS - elapsed);\n }\n };\n window.addEventListener('scroll', this.scrollListener, { passive: true });\n }\n\n private flushScroll(): void {\n if (!this.room) return;\n this.room.updatePresence({ scrollY: window.scrollY });\n this.lastScrollBroadcast = performance.now();\n }\n\n private attachVisibilityListener(): void {\n this.visibilityListener = () => {\n if (document.hidden && this.room) {\n // Stop broadcasting so other tabs don't see a stale cursor stuck\n // in place after we alt-tabbed away.\n this.room.updatePresence({ cursor: null });\n }\n };\n document.addEventListener('visibilitychange', this.visibilityListener);\n }\n\n private attachKeyboardListener(): void {\n // Esc cancels follow mode — a single, well-known keybind for \"let me out\".\n // We deliberately don't preventDefault / stopPropagation so other Esc\n // handlers on the page still fire; they just see the key after we react.\n this.keydownListener = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return;\n if (!this.followingUserId) return;\n this.setFollowing(null);\n };\n document.addEventListener('keydown', this.keydownListener);\n }\n\n private subscribeStatus(): void {\n if (!this.room) return;\n // Initial paint based on the current status, then subscribe for\n // changes. Liveblocks exposes 'initial' | 'connecting' | 'connected'\n // | 'reconnecting' | 'disconnected'. Indicator hides itself on\n // 'connected'; surfaces a small amber chip otherwise.\n this.statusIndicator?.setStatus(this.room.getStatus());\n const unsub = this.room.subscribe('status', (status) => {\n this.statusIndicator?.setStatus(status);\n });\n this.unsubscribers.push(unsub);\n }\n\n private flushThrottled(): void {\n const now = performance.now();\n const elapsed = now - this.lastBroadcast;\n if (elapsed >= MOUSEMOVE_THROTTLE_MS) {\n this.flush();\n return;\n }\n if (this.throttleTimer == null) {\n this.throttleTimer = window.setTimeout(() => {\n this.throttleTimer = null;\n this.flush();\n }, MOUSEMOVE_THROTTLE_MS - elapsed);\n }\n }\n\n private flush(): void {\n if (!this.pendingCursor || !this.room) return;\n this.room.updatePresence({ cursor: this.pendingCursor });\n this.pendingCursor = null;\n this.lastBroadcast = performance.now();\n }\n\n private subscribeOthers(): void {\n if (!this.room) return;\n const unsub = this.room.subscribe('others', (others) => {\n const seen = new Set<number>();\n // De-dupe presence-stack entries by user id (one Clerk user might\n // have multiple browser tabs open → multiple connections → one\n // avatar). The cursor layer keeps per-connection entries because\n // cursors are per-mouse, but the stack is per-person.\n const stackUsers = new Map<string, PresenceUser>();\n const selections: SelectionUser[] = [];\n let followeeScrollY: number | null = null;\n let followeeStillPresent = false;\n\n for (const other of others) {\n seen.add(other.connectionId);\n const meta = other as unknown as { id?: string; info?: LiveUserMeta['info'] };\n const userId = meta.id ?? `c${other.connectionId}`;\n const name = meta.info?.name ?? 'Anonymous';\n const color = colorForUser(userId);\n\n if (!stackUsers.has(userId)) {\n stackUsers.set(userId, {\n id: userId,\n name,\n color,\n avatar: meta.info?.avatar,\n });\n }\n\n const presence = other.presence as LivePresence;\n\n // Track our followee's scroll for the follow-mode handler below.\n // First connection wins if the followee has multiple tabs open —\n // arbitrary but consistent.\n if (this.followingUserId === userId && followeeScrollY === null) {\n followeeStillPresent = true;\n if (typeof presence.scrollY === 'number') {\n followeeScrollY = presence.scrollY;\n }\n }\n\n // Selection sync: collect this user's in-flight hover element\n // for the SelectionOverlay below. De-dupe by user id (multi-tab\n // collapses to one overlay; first non-null wins).\n if (presence.selecting && !selections.find((s) => s.id === userId)) {\n try {\n const fp = JSON.parse(presence.selecting.fingerprintJson) as ElementFingerprint;\n selections.push({ id: userId, name, color, fingerprint: fp });\n } catch {\n /* malformed payload from a future client; skip */\n }\n }\n\n if (!presence.cursor) {\n this.removeCursor(other.connectionId);\n continue;\n }\n this.upsertCursor(other.connectionId, userId, name, presence.cursor);\n }\n\n // Remove cursors for connections no longer present.\n for (const id of Array.from(this.cursors.keys())) {\n if (!seen.has(id)) this.removeCursor(id);\n }\n\n // Drive the presence stack with the de-duped list.\n this.presenceStack?.setOthers(Array.from(stackUsers.values()));\n // Drive the selection ghost-outlines.\n this.selectionOverlay?.setSelections(selections);\n\n // Follow mode: scroll to the followee if their position moved, or\n // auto-unfollow if they left the room.\n if (this.followingUserId) {\n if (!followeeStillPresent) {\n this.setFollowing(null);\n } else if (followeeScrollY !== null) {\n this.followScrollTo(followeeScrollY);\n }\n }\n });\n this.unsubscribers.push(unsub);\n }\n\n private upsertCursor(\n connectionId: number,\n userId: string,\n name: string,\n cursor: { xPct: number; yPct: number },\n ): void {\n if (!this.container) return;\n let entry = this.cursors.get(connectionId);\n if (!entry) {\n const color = colorForUser(userId);\n const el = createCursorElement(name, color);\n this.container.appendChild(el);\n entry = { el, fadeTimer: null };\n this.cursors.set(connectionId, entry);\n }\n // Clip cursors that wander outside the viewport — visible at the edge\n // looks worse than just hidden.\n const inViewport = cursor.xPct >= 0 && cursor.xPct <= 100 && cursor.yPct >= 0 && cursor.yPct <= 100;\n if (!inViewport) {\n entry.el.style.opacity = '0';\n return;\n }\n const x = (window.innerWidth * cursor.xPct) / 100;\n const y = (window.innerHeight * cursor.yPct) / 100;\n entry.el.style.transform = `translate(${x}px, ${y}px)`;\n entry.el.style.opacity = '1';\n if (entry.fadeTimer != null) window.clearTimeout(entry.fadeTimer);\n entry.fadeTimer = window.setTimeout(() => {\n if (entry) entry.el.style.opacity = '0';\n }, INACTIVITY_FADE_MS);\n }\n\n private removeCursor(connectionId: number): void {\n const entry = this.cursors.get(connectionId);\n if (!entry) return;\n if (entry.fadeTimer != null) window.clearTimeout(entry.fadeTimer);\n entry.el.style.opacity = '0';\n // Defer DOM removal until the fade transition completes so it doesn't\n // pop. 220 = transition (110) + a little slack.\n window.setTimeout(() => entry.el.remove(), 220);\n this.cursors.delete(connectionId);\n }\n}\n\n/* ──────────────────────── cursor DOM ───────────────────────── */\n\n/**\n * Build the cursor element — SVG pointer + colored name label.\n * Returns a positioned wrapper; caller updates `transform` to move it.\n */\nfunction createCursorElement(name: string, color: string): HTMLDivElement {\n const wrap = document.createElement('div');\n wrap.setAttribute('data-vizu-cursor', '');\n // Reduced-motion drops the cursor smoothing so the pointer snaps instead\n // of easing — opacity transitions stay because they're not vestibular.\n const transformTransition = prefersReducedMotion()\n ? 'none'\n : `transform ${CURSOR_TRANSITION_MS}ms cubic-bezier(0.16, 1, 0.3, 1)`;\n wrap.style.cssText = `\n position: absolute; top: 0; left: 0;\n pointer-events: none;\n opacity: 0;\n transform: translate(0px, 0px);\n transition: ${transformTransition}, opacity 220ms ease-out;\n will-change: transform;\n `;\n\n // SVG cursor pointer — classic arrow shape with a white stroke for contrast.\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', '18');\n svg.setAttribute('height', '20');\n svg.setAttribute('viewBox', '0 0 16 18');\n svg.style.cssText = 'display: block; filter: drop-shadow(0 1px 1.5px rgba(0,0,0,0.3));';\n const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n path.setAttribute('d', 'M0 0 L0 13 L4 9 L7 16 L9 15 L6 8 L11 8 Z');\n path.setAttribute('fill', color);\n path.setAttribute('stroke', '#ffffff');\n path.setAttribute('stroke-width', '1.2');\n path.setAttribute('stroke-linejoin', 'round');\n svg.appendChild(path);\n wrap.appendChild(svg);\n\n const label = document.createElement('div');\n label.textContent = name;\n label.style.cssText = `\n margin-top: 2px;\n margin-left: 10px;\n padding: 2px 7px;\n background: ${color};\n color: #ffffff;\n font: 500 11px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n border-radius: 4px;\n white-space: nowrap;\n box-shadow: 0 2px 6px rgba(0,0,0,0.18);\n max-width: 160px;\n overflow: hidden;\n text-overflow: ellipsis;\n `;\n wrap.appendChild(label);\n return wrap;\n}\n\n/* ──────────────────────── connection indicator ────────────── */\n\ntype LiveStatus = 'initial' | 'connecting' | 'connected' | 'reconnecting' | 'disconnected';\n\n/**\n * Tiny inline chip that surfaces non-healthy Liveblocks connection\n * states. Hidden when connected so it doesn't clutter; appears just\n * to the left of the presence stack during connect / reconnect /\n * disconnect.\n *\n * Auto-reconnection is handled by the Liveblocks SDK — this is\n * purely an \"I see you noticed\" surface for the human while the\n * SDK does its thing.\n */\nclass ConnectionIndicator {\n private el: HTMLDivElement | null = null;\n private label: HTMLSpanElement | null = null;\n\n mount(): void {\n if (typeof document === 'undefined') return;\n if (this.el) return;\n this.el = document.createElement('div');\n this.el.setAttribute('data-vizu-connection-status', '');\n this.el.style.cssText = `\n position: fixed;\n top: 18px;\n right: 200px;\n z-index: ${Z_INDEX};\n display: none;\n align-items: center;\n gap: 7px;\n padding: 4px 10px 4px 8px;\n background: rgba(10, 10, 10, 0.88);\n color: #fafafa;\n font: 500 11px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n border-radius: 999px;\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);\n pointer-events: none;\n `;\n const dot = document.createElement('span');\n dot.style.cssText = `\n display: inline-block;\n width: 6px; height: 6px;\n border-radius: 50%;\n background: #F59E0B;\n box-shadow: 0 0 6px #F59E0B;\n `;\n const label = document.createElement('span');\n this.label = label;\n this.el.appendChild(dot);\n this.el.appendChild(label);\n document.body.appendChild(this.el);\n }\n\n setStatus(status: LiveStatus): void {\n if (!this.el || !this.label) return;\n if (status === 'connected') {\n this.el.style.display = 'none';\n return;\n }\n this.label.textContent = labelForStatus(status);\n this.el.style.display = 'inline-flex';\n }\n\n destroy(): void {\n this.el?.remove();\n this.el = null;\n this.label = null;\n }\n}\n\nfunction labelForStatus(status: LiveStatus): string {\n switch (status) {\n case 'connecting':\n return 'Connecting…';\n case 'reconnecting':\n return 'Reconnecting…';\n case 'disconnected':\n return 'Offline';\n case 'initial':\n return 'Connecting…';\n case 'connected':\n return ''; // hidden anyway\n }\n}\n\n/* ──────────────────────── follow pill ──────────────────────── */\n\n/**\n * Top-right floating pill that shows the active followee + a stop\n * button. Sibling to the PresenceStack — appears below it when\n * follow mode is active, hides otherwise.\n *\n * Pure vanilla DOM; takes a stop callback from LiveCollab.\n */\nclass FollowPill {\n private el: HTMLDivElement | null = null;\n private onStop: () => void;\n\n constructor(opts: { onStop: () => void }) {\n this.onStop = opts.onStop;\n }\n\n setFollowing(user: PresenceUser | null): void {\n if (typeof document === 'undefined') return;\n if (!user) {\n this.el?.remove();\n this.el = null;\n return;\n }\n if (!this.el) this.el = this.mount();\n this.paint(user);\n }\n\n destroy(): void {\n this.el?.remove();\n this.el = null;\n }\n\n private mount(): HTMLDivElement {\n const pill = document.createElement('div');\n pill.setAttribute('data-vizu-follow-pill', '');\n pill.style.cssText = `\n position: fixed;\n top: 56px;\n right: 16px;\n z-index: ${Z_INDEX};\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 6px 6px 12px;\n background: rgba(10, 10, 10, 0.92);\n color: #ffffff;\n border-radius: 999px;\n font: 500 12px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n box-shadow: 0 10px 24px rgba(0, 0, 0, 0.28);\n pointer-events: auto;\n `;\n document.body.appendChild(pill);\n return pill;\n }\n\n private paint(user: PresenceUser): void {\n if (!this.el) return;\n this.el.textContent = '';\n\n const dot = document.createElement('span');\n dot.style.cssText = `\n display: inline-block;\n width: 8px; height: 8px;\n border-radius: 50%;\n background: ${user.color};\n box-shadow: 0 0 8px ${user.color};\n `;\n this.el.appendChild(dot);\n\n const label = document.createElement('span');\n label.textContent = `Following ${user.name}`;\n label.style.cssText = 'max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';\n this.el.appendChild(label);\n\n const stop = document.createElement('button');\n stop.type = 'button';\n stop.setAttribute('aria-label', `Stop following ${user.name}`);\n stop.innerHTML =\n '<svg width=\"11\" height=\"11\" viewBox=\"0 0 12 12\" aria-hidden=\"true\"><path d=\"M1 1 L11 11 M11 1 L1 11\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" /></svg>';\n stop.style.cssText = `\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 22px; height: 22px;\n border: none;\n background: rgba(255, 255, 255, 0.12);\n color: #ffffff;\n border-radius: 50%;\n cursor: pointer;\n transition: background 120ms ease-out;\n `;\n stop.addEventListener('mouseenter', () => (stop.style.background = 'rgba(255, 255, 255, 0.22)'));\n stop.addEventListener('mouseleave', () => (stop.style.background = 'rgba(255, 255, 255, 0.12)'));\n stop.addEventListener('click', (e) => {\n e.preventDefault();\n this.onStop();\n });\n this.el.appendChild(stop);\n }\n}\n","/**\n * Live-collab cursor palette + a11y helpers.\n *\n * 8 distinct colors covering the hue wheel, all darkened just enough\n * that white text rendered on top of them meets WCAG-AA contrast\n * (≥4.5:1 for normal-size text). The earlier draft used the brighter\n * \"500\" shades (amber, sky-cyan, plain red) which fail AA for white\n * labels — see audit table in the comment block below.\n *\n * Coral (`#FF6647`) is deliberately excluded — it's Vizu's brand accent\n * and reusing it for one user's cursor would confuse cursor identity\n * with Vizu UI affordances.\n *\n * The same color drives:\n * - cursor SVG fill (live-collab.ts CursorEntry)\n * - avatar ring + initials background (presence-stack.ts)\n * - selection-sync ghost-outline border + name pill (selection-overlay.ts)\n *\n * Color assignment is deterministic: `hash(userId) % 8`, so the same\n * Clerk user always gets the same color across sessions and devices.\n */\n\n/**\n * | Color | Hex | White-text contrast | AA-normal ≥4.5:1 |\n * |---------|----------|---------------------|------------------|\n * | indigo | #4F46E5 | 6.96 | ✓ |\n * | forest | #15803D | 5.84 | ✓ |\n * | orange | #C2410C | 5.85 | ✓ |\n * | rose | #BE185D | 7.32 | ✓ |\n * | cyan | #0E7490 | 6.43 | ✓ |\n * | plum | #7E22CE | 7.41 | ✓ |\n * | red | #DC2626 | 4.83 | ✓ |\n * | teal | #0F766E | 5.95 | ✓ |\n */\nexport const CURSOR_COLORS = [\n '#4F46E5', // indigo\n '#15803D', // forest\n '#C2410C', // orange\n '#BE185D', // rose\n '#0E7490', // cyan\n '#7E22CE', // plum\n '#DC2626', // red\n '#0F766E', // teal\n] as const;\n\n/**\n * Stable color for a Clerk user id. Same input → same color across\n * sessions, devices, and dashboard sidebar contexts.\n */\nexport function colorForUser(userId: string): string {\n let h = 0;\n for (let i = 0; i < userId.length; i++) {\n h = (h * 31 + userId.charCodeAt(i)) | 0;\n }\n return CURSOR_COLORS[Math.abs(h) % CURSOR_COLORS.length];\n}\n\n/* ──────────────────────── motion preference ────────────────── */\n\n/**\n * Respects the user's OS-level reduced-motion preference. When true,\n * call sites collapse animated CSS transitions to `none` so motion-\n * sensitive users don't have to track smooth-easing cursors, sliding\n * selection outlines, or lifting avatars.\n *\n * SSR-safe: returns false when window isn't available so initial\n * render matches the most common case (motion enabled).\n */\nexport function prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || !window.matchMedia) return false;\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n","import { prefersReducedMotion } from './cursor-colors';\n\n/**\n * Top-right presence stack — avatars of other users currently in the\n * same Liveblocks room. Up to 5 visible; the rest collapse into a \"+N\"\n * chip. Hover an avatar to see the user's name in a tooltip.\n *\n * Vanilla DOM, no React. Driven by LiveCollab's `others` subscription\n * via {@link setOthers}.\n *\n * Local user is intentionally not shown — Liveblocks' `others` array\n * already excludes them. The stack rendering empty when you're alone\n * is the right signal (\"nobody else here\").\n *\n * See plans/live-collab.md → Slice 3. Follow action on hover lands in\n * Slice 4.\n */\n\nconst Z_INDEX = 2147483640; // just below the live-cursor layer\nconst MAX_VISIBLE = 5;\nconst AVATAR_SIZE = 28;\nconst OVERLAP_PX = 6;\n\nexport type PresenceUser = {\n /** Stable per-Clerk-user identifier. Drives avatar diffing. */\n id: string;\n /** Display name shown in the tooltip and used for the initials fallback. */\n name: string;\n /** Cursor color (also drives the avatar ring). Hex string. */\n color: string;\n /** Optional avatar image URL — shown when present, initials otherwise. */\n avatar?: string;\n};\n\nexport class PresenceStack {\n private container: HTMLDivElement | null = null;\n private avatars = new Map<string, HTMLDivElement>();\n private overflowEl: HTMLDivElement | null = null;\n private followingUserId: string | null = null;\n private onAvatarClick: ((userId: string) => void) | null = null;\n\n constructor(opts?: { onAvatarClick?: (userId: string) => void }) {\n this.onAvatarClick = opts?.onAvatarClick ?? null;\n }\n\n mount(): void {\n if (typeof document === 'undefined') return;\n if (this.container) return;\n this.container = document.createElement('div');\n this.container.setAttribute('data-vizu-presence-stack', '');\n this.container.style.cssText = `\n position: fixed;\n top: 16px;\n right: 16px;\n z-index: ${Z_INDEX};\n display: flex;\n align-items: center;\n pointer-events: auto;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n `;\n document.body.appendChild(this.container);\n }\n\n destroy(): void {\n for (const el of this.avatars.values()) el.remove();\n this.avatars.clear();\n this.overflowEl?.remove();\n this.overflowEl = null;\n this.container?.remove();\n this.container = null;\n }\n\n setOthers(others: PresenceUser[]): void {\n if (!this.container) return;\n\n const visible = others.slice(0, MAX_VISIBLE);\n const overflow = Math.max(0, others.length - visible.length);\n const nextIds = new Set(visible.map((u) => u.id));\n\n // Drop avatars for users that left or fell past the visible cutoff.\n for (const [id, el] of Array.from(this.avatars)) {\n if (!nextIds.has(id)) {\n el.remove();\n this.avatars.delete(id);\n }\n }\n\n // Insert / update visible avatars. Re-appending in order keeps\n // them in stable left-to-right order even when users come and go.\n for (const user of visible) {\n let el = this.avatars.get(user.id);\n if (!el) {\n el = createAvatar(user, () => this.onAvatarClick?.(user.id));\n this.avatars.set(user.id, el);\n } else {\n updateAvatar(el, user);\n }\n paintAvatarFollowState(el, user, this.followingUserId === user.id);\n this.container.appendChild(el);\n }\n\n this.renderOverflow(overflow);\n }\n\n /**\n * Mark a user as the active followee so its avatar can render the\n * coral ring highlight. Pass null to clear.\n */\n setFollowing(userId: string | null): void {\n this.followingUserId = userId;\n for (const [id, el] of this.avatars) {\n const user = avatarMeta(el);\n if (user) paintAvatarFollowState(el, user, id === userId);\n }\n }\n\n /* ────────────────── private ────────────────── */\n\n private renderOverflow(count: number): void {\n if (!this.container) return;\n if (count <= 0) {\n this.overflowEl?.remove();\n this.overflowEl = null;\n return;\n }\n if (!this.overflowEl) {\n this.overflowEl = createOverflowChip();\n }\n this.overflowEl.textContent = `+${count}`;\n this.container.appendChild(this.overflowEl);\n }\n}\n\n/* ──────────────────────── avatar DOM ───────────────────────── */\n\nfunction createAvatar(user: PresenceUser, onClick: () => void): HTMLDivElement {\n const wrap = document.createElement('div');\n wrap.setAttribute('data-vizu-presence-avatar', user.id);\n wrap.setAttribute('data-vizu-presence-name', user.name);\n wrap.setAttribute('data-vizu-presence-color', user.color);\n if (user.avatar) wrap.setAttribute('data-vizu-presence-img', user.avatar);\n // Reduced motion drops the hover lift+scale so cursor-tracking users\n // don't see avatars springing when they accidentally pass over them.\n const reduced = prefersReducedMotion();\n const hoverTransition = reduced\n ? 'none'\n : 'transform 130ms cubic-bezier(0.16, 1, 0.3, 1), box-shadow 130ms ease-out';\n wrap.style.cssText = `\n position: relative;\n width: ${AVATAR_SIZE}px;\n height: ${AVATAR_SIZE}px;\n margin-left: -${OVERLAP_PX}px;\n border-radius: 50%;\n background: ${user.color};\n padding: 1.5px;\n box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.95), 0 2px 6px rgba(0, 0, 0, 0.15);\n cursor: pointer;\n flex-shrink: 0;\n transition: ${hoverTransition};\n `;\n wrap.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();\n onClick();\n });\n if (!reduced) {\n wrap.addEventListener(\n 'mouseenter',\n () => (wrap.style.transform = 'translateY(-1px) scale(1.06)'),\n );\n wrap.addEventListener('mouseleave', () => (wrap.style.transform = ''));\n }\n\n const inner = document.createElement('div');\n inner.setAttribute('data-vizu-presence-inner', '');\n inner.style.cssText = `\n width: 100%;\n height: 100%;\n border-radius: 50%;\n overflow: hidden;\n background: ${user.color};\n color: #ffffff;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 10.5px;\n font-weight: 600;\n letter-spacing: -0.01em;\n line-height: 1;\n user-select: none;\n `;\n paintAvatarContent(inner, user);\n wrap.appendChild(inner);\n\n // Tooltip\n const tip = createTooltip(user.name);\n wrap.appendChild(tip);\n\n wrap.addEventListener('mouseenter', () => {\n tip.style.opacity = '1';\n tip.style.transform = 'translateY(0)';\n });\n wrap.addEventListener('mouseleave', () => {\n tip.style.opacity = '0';\n tip.style.transform = 'translateY(-2px)';\n });\n\n return wrap;\n}\n\nfunction updateAvatar(wrap: HTMLDivElement, user: PresenceUser): void {\n // Color is per-userId and stable, so we don't refresh the ring on each\n // update — but the name / avatar URL can change (e.g., Clerk profile\n // edit). Re-paint the inner + tooltip text + dataset only.\n const inner = wrap.querySelector<HTMLDivElement>('[data-vizu-presence-inner]');\n if (inner) paintAvatarContent(inner, user);\n const tip = wrap.querySelector<HTMLDivElement>('[data-vizu-presence-tip]');\n if (tip) tip.textContent = user.name;\n wrap.setAttribute('data-vizu-presence-name', user.name);\n if (user.avatar) wrap.setAttribute('data-vizu-presence-img', user.avatar);\n else wrap.removeAttribute('data-vizu-presence-img');\n}\n\n/**\n * Read back the metadata we stashed on the wrap so setFollowing() can\n * repaint without needing the full PresenceUser list.\n */\nfunction avatarMeta(wrap: HTMLDivElement): PresenceUser | null {\n const id = wrap.getAttribute('data-vizu-presence-avatar');\n const name = wrap.getAttribute('data-vizu-presence-name');\n const color = wrap.getAttribute('data-vizu-presence-color');\n if (!id || !name || !color) return null;\n const avatar = wrap.getAttribute('data-vizu-presence-img') ?? undefined;\n return { id, name, color, avatar };\n}\n\n/**\n * Swap the avatar's shadow rings to mark it as the followee. Coral\n * outer ring is the visual signature. Stays in sync with the\n * FollowPill across the top-right cluster.\n */\nfunction paintAvatarFollowState(\n wrap: HTMLDivElement,\n user: PresenceUser,\n isFollowing: boolean,\n): void {\n if (isFollowing) {\n wrap.style.boxShadow = `0 0 0 2px rgba(255, 255, 255, 0.95), 0 0 0 4px var(--vizu-coral, #FF6647), 0 4px 12px rgba(255, 102, 71, 0.4)`;\n } else {\n wrap.style.boxShadow = `0 0 0 2px rgba(255, 255, 255, 0.95), 0 2px 6px rgba(0, 0, 0, 0.15)`;\n }\n // Silence the unused-arg warning when user isn't read; kept for symmetry.\n void user;\n}\n\nfunction paintAvatarContent(inner: HTMLDivElement, user: PresenceUser): void {\n inner.textContent = '';\n if (user.avatar) {\n const img = document.createElement('img');\n img.src = user.avatar;\n img.alt = user.name;\n img.referrerPolicy = 'no-referrer';\n img.style.cssText = 'width: 100%; height: 100%; object-fit: cover; display: block;';\n // If the image fails (CSP, expired Clerk url, broken link), fall back\n // to initials. Without this the avatar shows the broken-image glyph.\n img.addEventListener(\n 'error',\n () => {\n img.remove();\n inner.textContent = initialsOf(user.name);\n },\n { once: true },\n );\n inner.appendChild(img);\n } else {\n inner.textContent = initialsOf(user.name);\n }\n}\n\nfunction createTooltip(text: string): HTMLDivElement {\n const tip = document.createElement('div');\n tip.setAttribute('data-vizu-presence-tip', '');\n tip.textContent = text;\n tip.style.cssText = `\n position: absolute;\n top: calc(100% + 6px);\n right: 0;\n background: rgba(10, 10, 10, 0.92);\n color: #ffffff;\n font-size: 11px;\n font-weight: 500;\n padding: 4px 8px;\n border-radius: 4px;\n white-space: nowrap;\n pointer-events: none;\n opacity: 0;\n transform: translateY(-2px);\n transition: opacity 130ms ease-out, transform 130ms ease-out;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);\n max-width: 220px;\n overflow: hidden;\n text-overflow: ellipsis;\n `;\n return tip;\n}\n\nfunction createOverflowChip(): HTMLDivElement {\n const chip = document.createElement('div');\n chip.setAttribute('data-vizu-presence-overflow', '');\n chip.style.cssText = `\n width: ${AVATAR_SIZE}px;\n height: ${AVATAR_SIZE}px;\n margin-left: -${OVERLAP_PX}px;\n border-radius: 50%;\n background: rgba(82, 82, 82, 0.95);\n color: #ffffff;\n box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.95), 0 2px 6px rgba(0, 0, 0, 0.15);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 10.5px;\n font-weight: 600;\n letter-spacing: -0.02em;\n line-height: 1;\n user-select: none;\n flex-shrink: 0;\n `;\n return chip;\n}\n\nfunction initialsOf(name: string): string {\n const trimmed = name.trim();\n if (!trimmed) return '?';\n const parts = trimmed.split(/\\s+/);\n if (parts.length >= 2 && parts[0] && parts[parts.length - 1]) {\n return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();\n }\n return trimmed.slice(0, 2).toUpperCase();\n}\n","import { findByFingerprint } from './fingerprint';\nimport { prefersReducedMotion } from './cursor-colors';\nimport type { ElementFingerprint } from './types';\n\n/**\n * Ghost outlines for other users' in-flight element selections.\n *\n * When another reviewer hovers an element in highlighter mode, their\n * cursor's fingerprint comes through LivePresence. This overlay resolves\n * that fingerprint against the local DOM via Vizu's 6-rung matcher and\n * draws a colored dashed outline on the matched element, with the\n * user's name in a small pill at top-left.\n *\n * Coords here are DOM-anchored (not viewport-%) — the right call for\n * \"which element are they about to comment on\" because viewport-% would\n * land on different elements at different viewport widths. See\n * plans/live-collab.md → \"selection sync coord override\".\n *\n * Repositioning happens on scroll/resize via a single RAF — the\n * outline tracks its element as the page shifts under it.\n */\n\nconst Z_INDEX = 2147483639; // below cursors (~641), above the rest\n\nexport type SelectionUser = {\n /** Stable Clerk user id, used to diff overlay entries. */\n id: string;\n /** Display name shown in the small color pill. */\n name: string;\n /** Cursor color (matches their cursor + avatar). */\n color: string;\n /** The fingerprint to match against the local DOM. */\n fingerprint: ElementFingerprint;\n};\n\nexport class SelectionOverlay {\n private container: HTMLDivElement | null = null;\n private overlays = new Map<string, OverlayEntry>();\n private scrollListener: ((e: Event) => void) | null = null;\n private resizeListener: (() => void) | null = null;\n private rafScheduled = false;\n\n mount(): void {\n if (typeof document === 'undefined') return;\n if (this.container) return;\n this.container = document.createElement('div');\n this.container.setAttribute('data-vizu-selections', '');\n this.container.style.cssText = `\n position: fixed; inset: 0;\n pointer-events: none;\n z-index: ${Z_INDEX};\n contain: strict;\n `;\n document.body.appendChild(this.container);\n\n this.scrollListener = () => this.scheduleReposition();\n this.resizeListener = () => this.scheduleReposition();\n // Capture-phase scroll so we catch scrolls in nested scrollable\n // containers, not just window scroll. Matches Vizu's existing\n // anchor outline behavior.\n window.addEventListener('scroll', this.scrollListener, { passive: true, capture: true });\n window.addEventListener('resize', this.resizeListener);\n }\n\n destroy(): void {\n if (this.scrollListener) {\n window.removeEventListener('scroll', this.scrollListener, true);\n this.scrollListener = null;\n }\n if (this.resizeListener) {\n window.removeEventListener('resize', this.resizeListener);\n this.resizeListener = null;\n }\n for (const e of this.overlays.values()) e.el.remove();\n this.overlays.clear();\n this.container?.remove();\n this.container = null;\n }\n\n setSelections(selections: SelectionUser[]): void {\n if (!this.container) return;\n\n const nextIds = new Set(selections.map((s) => s.id));\n for (const [id, entry] of Array.from(this.overlays)) {\n if (!nextIds.has(id)) {\n entry.el.remove();\n this.overlays.delete(id);\n }\n }\n\n for (const sel of selections) {\n let entry = this.overlays.get(sel.id);\n if (!entry) {\n const el = createOverlay(sel.color, sel.name);\n this.container.appendChild(el);\n entry = { el, fingerprint: sel.fingerprint, color: sel.color, name: sel.name };\n this.overlays.set(sel.id, entry);\n } else {\n // Update if user moved to a different element or changed color/name.\n entry.fingerprint = sel.fingerprint;\n if (entry.color !== sel.color) {\n entry.color = sel.color;\n entry.el.style.borderColor = sel.color;\n }\n if (entry.name !== sel.name) {\n entry.name = sel.name;\n const label = entry.el.querySelector<HTMLDivElement>('[data-selection-label]');\n if (label) {\n label.textContent = sel.name;\n label.style.background = sel.color;\n }\n }\n }\n this.positionOverlay(entry);\n }\n }\n\n /* ────────────────── private ────────────────── */\n\n private positionOverlay(entry: OverlayEntry): void {\n const match = findByFingerprint(entry.fingerprint);\n const target = (match as { element?: Element | null } | null)?.element ?? null;\n if (!target) {\n entry.el.style.display = 'none';\n return;\n }\n const rect = target.getBoundingClientRect();\n if (rect.width === 0 || rect.height === 0) {\n entry.el.style.display = 'none';\n return;\n }\n entry.el.style.display = 'block';\n entry.el.style.left = `${rect.left}px`;\n entry.el.style.top = `${rect.top}px`;\n entry.el.style.width = `${rect.width}px`;\n entry.el.style.height = `${rect.height}px`;\n }\n\n private scheduleReposition(): void {\n if (this.rafScheduled) return;\n this.rafScheduled = true;\n requestAnimationFrame(() => {\n this.rafScheduled = false;\n for (const entry of this.overlays.values()) this.positionOverlay(entry);\n });\n }\n}\n\ntype OverlayEntry = {\n el: HTMLDivElement;\n fingerprint: ElementFingerprint;\n color: string;\n name: string;\n};\n\nfunction createOverlay(color: string, name: string): HTMLDivElement {\n const el = document.createElement('div');\n el.setAttribute('data-vizu-selection-overlay', '');\n // Reduced motion snaps the outline to its new position instead of\n // sliding — the outline tracking another user's hover would otherwise\n // be one of the more vestibular-triggering motions in the UI.\n const positionTransition = prefersReducedMotion()\n ? 'none'\n : 'left 90ms ease-out, top 90ms ease-out, width 90ms ease-out, height 90ms ease-out';\n el.style.cssText = `\n position: fixed;\n pointer-events: none;\n display: none;\n border: 2px dashed ${color};\n border-radius: 4px;\n background: ${color}10;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.85);\n transition: ${positionTransition};\n will-change: left, top, width, height;\n `;\n\n const label = document.createElement('div');\n label.setAttribute('data-selection-label', '');\n label.textContent = name;\n label.style.cssText = `\n position: absolute;\n top: -22px;\n left: -2px;\n padding: 2px 7px;\n background: ${color};\n color: #ffffff;\n font: 500 10.5px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n border-radius: 3px 3px 3px 0;\n white-space: nowrap;\n max-width: 180px;\n overflow: hidden;\n text-overflow: ellipsis;\n box-shadow: 0 2px 4px rgba(0,0,0,0.15);\n `;\n el.appendChild(label);\n return el;\n}\n"],"mappings":"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","names":["Z_INDEX","Z_INDEX","room"]}