@rencar-dev/feature-modules-public 0.0.7 → 1.2.0

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.
@@ -210,7 +210,7 @@ function usePresenceAvatarsState(options) {
210
210
  const [hoveredIndex, setHoveredIndex] = useState3(null);
211
211
  const hoveredUser = hoveredIndex != null ? visibleUsers[hoveredIndex] : null;
212
212
  const baseTopZ = visibleUsers.length + 1;
213
- const getInitial = useCallback3((user) => {
213
+ const getInitial2 = useCallback3((user) => {
214
214
  return getInitialFromUser(user);
215
215
  }, []);
216
216
  const getZIndex = useCallback3(
@@ -225,7 +225,7 @@ function usePresenceAvatarsState(options) {
225
225
  hoveredUser,
226
226
  hoveredIndex,
227
227
  setHoveredIndex,
228
- getInitial,
228
+ getInitial: getInitial2,
229
229
  getZIndex
230
230
  };
231
231
  }
@@ -412,7 +412,233 @@ function usePresenceFloatingState(options) {
412
412
  onAvatarLeave
413
413
  };
414
414
  }
415
+
416
+ // src/presence/components/PresenceAvatars.tsx
417
+ import { useState as useState5, useCallback as useCallback5 } from "react";
418
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
419
+ function PresenceAvatars({
420
+ users,
421
+ maxVisible = 3,
422
+ className,
423
+ renderAvatar,
424
+ renderTooltip,
425
+ renderMore
426
+ }) {
427
+ const {
428
+ visibleUsers,
429
+ moreCount,
430
+ hoveredUser,
431
+ hoveredIndex,
432
+ setHoveredIndex,
433
+ getInitial: getInitial2,
434
+ getZIndex
435
+ } = usePresenceAvatarsState({ users, maxVisible });
436
+ const [tooltipPos, setTooltipPos] = useState5(null);
437
+ const updateTooltipPos = useCallback5((el) => {
438
+ const rect = el.getBoundingClientRect();
439
+ const tooltipWidth = 200;
440
+ const half = tooltipWidth / 2;
441
+ const centerX = rect.left + rect.width / 2;
442
+ const left = Math.min(
443
+ Math.max(centerX, half + 8),
444
+ window.innerWidth - half - 8
445
+ );
446
+ const top = Math.min(rect.bottom + 8, window.innerHeight - 8);
447
+ setTooltipPos({ top, left });
448
+ }, []);
449
+ const handleMouseEnter = useCallback5(
450
+ (e, idx) => {
451
+ setHoveredIndex(idx);
452
+ updateTooltipPos(e.currentTarget);
453
+ },
454
+ [setHoveredIndex, updateTooltipPos]
455
+ );
456
+ const handleMouseLeave = useCallback5(() => {
457
+ setHoveredIndex(null);
458
+ }, [setHoveredIndex]);
459
+ const defaultTooltipRenderer = useCallback5(
460
+ ({ user, position }) => /* @__PURE__ */ jsxs(
461
+ "div",
462
+ {
463
+ className: "presence-avatars__tooltip",
464
+ style: { top: position.top, left: position.left },
465
+ children: [
466
+ /* @__PURE__ */ jsxs("div", { className: "presence-avatars__tooltip-row", children: [
467
+ /* @__PURE__ */ jsx("span", { className: "presence-avatars__tooltip-key", children: "Name" }),
468
+ /* @__PURE__ */ jsx("span", { className: "presence-avatars__tooltip-val", children: user.name || "-" })
469
+ ] }),
470
+ /* @__PURE__ */ jsxs("div", { className: "presence-avatars__tooltip-row", children: [
471
+ /* @__PURE__ */ jsx("span", { className: "presence-avatars__tooltip-key", children: "ID" }),
472
+ /* @__PURE__ */ jsx("span", { className: "presence-avatars__tooltip-val", children: user.userId || "-" })
473
+ ] })
474
+ ]
475
+ }
476
+ ),
477
+ []
478
+ );
479
+ const defaultAvatarRenderer = useCallback5(
480
+ ({ initial, isHovered }) => /* @__PURE__ */ jsx(Fragment, { children: initial }),
481
+ []
482
+ );
483
+ return /* @__PURE__ */ jsxs("div", { className: `presence-avatars ${className ?? ""}`, children: [
484
+ /* @__PURE__ */ jsxs("div", { className: "presence-avatars__stack", children: [
485
+ visibleUsers.map((user, idx) => {
486
+ const isHovered = hoveredIndex === idx;
487
+ const initial = getInitial2(user);
488
+ const zIndex = getZIndex(idx, isHovered);
489
+ const renderProps = {
490
+ user,
491
+ index: idx,
492
+ initial,
493
+ isHovered,
494
+ zIndex
495
+ };
496
+ return /* @__PURE__ */ jsx(
497
+ "span",
498
+ {
499
+ className: `presence-avatars__avatar ${isHovered ? "is-hovered" : ""}`,
500
+ style: { zIndex },
501
+ onMouseEnter: (e) => handleMouseEnter(e, idx),
502
+ onMouseLeave: handleMouseLeave,
503
+ "aria-label": user.name || user.userId || "",
504
+ children: renderAvatar ? renderAvatar(renderProps) : defaultAvatarRenderer(renderProps)
505
+ },
506
+ `${user.userId}-${idx}`
507
+ );
508
+ }),
509
+ moreCount > 0 && (renderMore ? renderMore(moreCount) : /* @__PURE__ */ jsxs("span", { className: "presence-avatars__more", children: [
510
+ "+",
511
+ moreCount
512
+ ] }))
513
+ ] }),
514
+ hoveredUser && tooltipPos && (renderTooltip ? renderTooltip({ user: hoveredUser, position: tooltipPos }) : defaultTooltipRenderer({
515
+ user: hoveredUser,
516
+ position: tooltipPos
517
+ }))
518
+ ] });
519
+ }
520
+
521
+ // src/presence/components/PresenceFloating.tsx
522
+ import { useCallback as useCallback6 } from "react";
523
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
524
+ function getInitial(user) {
525
+ const base = (user?.name || user?.userId || "").trim();
526
+ if (!base) return "?";
527
+ const ch = base.charAt(0);
528
+ return /[a-z]/i.test(ch) ? ch.toUpperCase() : ch;
529
+ }
530
+ function PresenceFloating({
531
+ users,
532
+ maxVisible = 8,
533
+ className,
534
+ style,
535
+ initialPosition,
536
+ title = "\uC5F4\uB78C\uC911",
537
+ emptyText = "\uC5C6\uC74C",
538
+ moreText = (n) => `+${n}\uBA85`,
539
+ renderAvatar,
540
+ renderTooltip
541
+ }) {
542
+ const {
543
+ containerRef,
544
+ inlineStyle,
545
+ isDragging,
546
+ visibleUsers,
547
+ moreCount,
548
+ hoveredUser,
549
+ tooltipTop,
550
+ onMouseDownHeader,
551
+ onTouchStartHeader,
552
+ onAvatarEnter,
553
+ onAvatarLeave
554
+ } = usePresenceFloatingState({ users, maxVisible, initialPosition });
555
+ const defaultTooltipRenderer = useCallback6(
556
+ ({ user, top }) => /* @__PURE__ */ jsxs2(
557
+ "div",
558
+ {
559
+ className: "presence-floating__tooltip",
560
+ style: { top: Math.max(0, top - 6) },
561
+ onMouseLeave: onAvatarLeave,
562
+ children: [
563
+ /* @__PURE__ */ jsxs2("div", { className: "presence-floating__tooltip-row", children: [
564
+ /* @__PURE__ */ jsx2("span", { className: "presence-floating__tooltip-key", children: "Name" }),
565
+ /* @__PURE__ */ jsx2("span", { className: "presence-floating__tooltip-val", children: user.name || "-" })
566
+ ] }),
567
+ /* @__PURE__ */ jsxs2("div", { className: "presence-floating__tooltip-row", children: [
568
+ /* @__PURE__ */ jsx2("span", { className: "presence-floating__tooltip-key", children: "ID" }),
569
+ /* @__PURE__ */ jsx2("span", { className: "presence-floating__tooltip-val", children: user.userId || "-" })
570
+ ] })
571
+ ]
572
+ }
573
+ ),
574
+ [onAvatarLeave]
575
+ );
576
+ const defaultAvatarRenderer = useCallback6(
577
+ ({ initial }) => /* @__PURE__ */ jsx2(Fragment2, { children: initial }),
578
+ []
579
+ );
580
+ const combinedStyle = {
581
+ ...inlineStyle,
582
+ ...style
583
+ };
584
+ return /* @__PURE__ */ jsxs2(
585
+ "div",
586
+ {
587
+ ref: containerRef,
588
+ className: `presence-floating ${isDragging ? "is-dragging" : ""} ${className ?? ""}`,
589
+ style: combinedStyle,
590
+ children: [
591
+ /* @__PURE__ */ jsxs2(
592
+ "div",
593
+ {
594
+ className: "presence-floating__header",
595
+ onMouseDown: onMouseDownHeader,
596
+ onTouchStart: onTouchStartHeader,
597
+ children: [
598
+ /* @__PURE__ */ jsx2(
599
+ "span",
600
+ {
601
+ className: "presence-floating__title",
602
+ title,
603
+ "aria-label": title,
604
+ children: title
605
+ }
606
+ ),
607
+ /* @__PURE__ */ jsx2("span", { className: "presence-floating__count", children: users.length })
608
+ ]
609
+ }
610
+ ),
611
+ hoveredUser && (renderTooltip ? renderTooltip({ user: hoveredUser, top: tooltipTop }) : defaultTooltipRenderer({ user: hoveredUser, top: tooltipTop })),
612
+ /* @__PURE__ */ jsxs2("div", { className: "presence-floating__body", children: [
613
+ visibleUsers.length === 0 ? /* @__PURE__ */ jsx2("span", { className: "presence-floating__empty", children: emptyText }) : visibleUsers.map((user, idx) => {
614
+ const initial = getInitial(user);
615
+ const label = user.name || user.userId || "";
616
+ const renderProps = {
617
+ user,
618
+ initial
619
+ };
620
+ return /* @__PURE__ */ jsx2(
621
+ "span",
622
+ {
623
+ className: "presence-floating__avatar",
624
+ title: label,
625
+ "aria-label": label,
626
+ onMouseEnter: (e) => onAvatarEnter(e, user),
627
+ onMouseLeave: onAvatarLeave,
628
+ children: renderAvatar ? renderAvatar(renderProps) : defaultAvatarRenderer(renderProps)
629
+ },
630
+ `${user.userId}-${idx}`
631
+ );
632
+ }),
633
+ moreCount > 0 && /* @__PURE__ */ jsx2("span", { className: "presence-floating__more", children: moreText(moreCount) })
634
+ ] })
635
+ ]
636
+ }
637
+ );
638
+ }
415
639
  export {
640
+ PresenceAvatars,
641
+ PresenceFloating,
416
642
  disconnectPresenceSocket,
417
643
  getPresenceSocket,
418
644
  initPresenceSocket,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/presence/core/socket.ts","../../src/presence/hooks/usePresence.ts","../../src/presence/hooks/usePresenceWatch.ts","../../src/presence/headless/usePresenceAvatarsState.ts","../../src/presence/headless/usePresenceFloatingState.ts"],"sourcesContent":["import { io, Socket } from 'socket.io-client';\nimport type { PresenceConfig } from './types';\n\nlet config: PresenceConfig | null = null;\nlet sharedSocket: Socket | null = null;\n\n/**\n * Initialize the presence socket with configuration\n * Must be called before using any presence hooks\n */\nexport function initPresenceSocket(presenceConfig: PresenceConfig): void {\n config = presenceConfig;\n\n // If socket already exists, disconnect and recreate\n if (sharedSocket) {\n sharedSocket.disconnect();\n sharedSocket = null;\n }\n}\n\n/**\n * Get the shared socket instance\n * @throws Error if socket is not initialized\n */\nexport function getPresenceSocket(): Socket {\n if (!config) {\n throw new Error(\n '[presence-module] Socket not initialized. Call initPresenceSocket() first.',\n );\n }\n\n if (sharedSocket && sharedSocket.connected) {\n return sharedSocket;\n }\n\n if (!sharedSocket) {\n sharedSocket = io(config.url, {\n transports: config.transports ?? ['websocket'],\n });\n } else if (!sharedSocket.connected) {\n sharedSocket.connect();\n }\n\n return sharedSocket;\n}\n\n/**\n * Disconnect and cleanup the socket connection\n */\nexport function disconnectPresenceSocket(): void {\n if (sharedSocket) {\n sharedSocket.disconnect();\n sharedSocket = null;\n }\n}\n\n/**\n * Check if socket is initialized\n */\nexport function isPresenceSocketInitialized(): boolean {\n return config !== null;\n}\n","import { useEffect, useState, useCallback } from 'react';\nimport { getPresenceSocket } from '../core/socket';\nimport type { PresenceUser, UsePresenceOptions } from '../core/types';\n\nconst DEFAULT_HEARTBEAT_INTERVAL = 10 * 60 * 1000; // 10 minutes\n\n/**\n * Hook to join a presence room and track users in that room\n *\n * @param options - Configuration options\n * @returns Array of users currently in the room\n *\n * @example\n * ```tsx\n * const users = usePresence({\n * roomId: 'my-app:page-123',\n * currentUser: { userId: '1', name: 'John' },\n * });\n * ```\n */\nexport function usePresence(options: UsePresenceOptions): PresenceUser[] {\n const {\n roomId,\n currentUser,\n enabled = true,\n heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,\n } = options;\n\n const [users, setUsers] = useState<PresenceUser[]>([]);\n\n // Normalize users from server response\n const normalizeUsers = useCallback((incoming: unknown): PresenceUser[] => {\n if (Array.isArray(incoming)) {\n return incoming as PresenceUser[];\n }\n if (\n incoming &&\n typeof incoming === 'object' &&\n Array.isArray((incoming as { users?: unknown }).users)\n ) {\n return (incoming as { users: unknown[] }).users as PresenceUser[];\n }\n return [];\n }, []);\n\n // Clear users when disabled\n useEffect(() => {\n if (!enabled) {\n setUsers([]);\n }\n }, [enabled]);\n\n useEffect(() => {\n if (!enabled) {\n return;\n }\n\n const socket = getPresenceSocket();\n\n const joinRoom = () => {\n socket.emit('presence:join', { roomId, user: currentUser });\n };\n\n // Join immediately if connected, otherwise wait for connect event\n if (socket.connected) {\n joinRoom();\n }\n\n socket.on('connect', joinRoom);\n\n // Heartbeat to extend TTL\n const heartbeat = () => {\n socket.emit('presence:heartbeat', { roomId });\n };\n heartbeat();\n const heartbeatIntervalId = window.setInterval(\n heartbeat,\n heartbeatInterval,\n );\n\n // Handle presence updates from server\n const handlePresenceUpdate = (incoming: unknown) => {\n setUsers(normalizeUsers(incoming));\n };\n socket.on('presence:update', handlePresenceUpdate);\n\n // Notify server when tab/window closes\n const handleBeforeUnload = () => {\n socket.emit('presence:leave', { roomId, userId: currentUser.userId });\n };\n window.addEventListener('beforeunload', handleBeforeUnload);\n\n return () => {\n socket.off('connect', joinRoom);\n socket.off('presence:update', handlePresenceUpdate);\n window.removeEventListener('beforeunload', handleBeforeUnload);\n window.clearInterval(heartbeatIntervalId);\n socket.emit('presence:leave', { roomId, userId: currentUser.userId });\n };\n }, [\n roomId,\n currentUser.userId,\n currentUser.name,\n enabled,\n heartbeatInterval,\n normalizeUsers,\n ]);\n\n return users;\n}\n","import { useEffect, useMemo, useRef, useState, useCallback } from 'react';\nimport { getPresenceSocket } from '../core/socket';\nimport type { PresenceUser, UsePresenceWatchOptions } from '../core/types';\n\ntype SnapshotPayload = {\n rooms?: Record<string, unknown>;\n};\n\ntype UpdatePayload = {\n roomId?: string;\n users?: unknown;\n};\n\nfunction normalizeUsers(value: unknown): PresenceUser[] {\n if (Array.isArray(value)) return value as PresenceUser[];\n if (\n value &&\n typeof value === 'object' &&\n Array.isArray((value as { users?: unknown }).users)\n ) {\n return (value as { users: unknown[] }).users as PresenceUser[];\n }\n return [];\n}\n\n/**\n * Hook to watch multiple rooms without joining them\n * (read-only observation, current user is not added to the rooms)\n *\n * @param options - Configuration options\n * @returns Map of roomId to array of users in that room\n *\n * @example\n * ```tsx\n * const roomsUsers = usePresenceWatch({\n * roomIds: ['room-1', 'room-2', 'room-3'],\n * });\n * // roomsUsers = { 'room-1': [...], 'room-2': [...], ... }\n * ```\n */\nexport function usePresenceWatch(\n options: UsePresenceWatchOptions,\n): Record<string, PresenceUser[]> {\n const { roomIds, enabled = true } = options;\n\n const [roomsUsers, setRoomsUsers] = useState<Record<string, PresenceUser[]>>(\n {},\n );\n\n const normalizedRoomIds = useMemo(() => {\n const uniq = Array.from(new Set((roomIds ?? []).filter(Boolean)));\n return uniq;\n }, [roomIds]);\n\n const watchedRef = useRef<Set<string>>(new Set());\n\n // Handle watch/unwatch based on roomIds changes\n useEffect(() => {\n if (!enabled) {\n setRoomsUsers({});\n watchedRef.current = new Set();\n return;\n }\n\n const socket = getPresenceSocket();\n\n const toWatch = normalizedRoomIds.filter((r) => !watchedRef.current.has(r));\n const toUnwatch = Array.from(watchedRef.current).filter(\n (r) => !normalizedRoomIds.includes(r),\n );\n\n if (toWatch.length > 0) {\n socket.emit('presence:watch', { roomIds: toWatch });\n for (const r of toWatch) watchedRef.current.add(r);\n }\n\n if (toUnwatch.length > 0) {\n socket.emit('presence:unwatch', { roomIds: toUnwatch });\n for (const r of toUnwatch) watchedRef.current.delete(r);\n setRoomsUsers((prev) => {\n const next = { ...prev };\n for (const r of toUnwatch) delete next[r];\n return next;\n });\n }\n }, [enabled, normalizedRoomIds]);\n\n // Handle socket events\n useEffect(() => {\n if (!enabled) return;\n\n const socket = getPresenceSocket();\n\n const handleSnapshot = (payload: SnapshotPayload) => {\n const rooms = payload?.rooms ?? {};\n setRoomsUsers((prev) => {\n const next = { ...prev };\n for (const [roomId, users] of Object.entries(rooms)) {\n next[roomId] = normalizeUsers(users);\n }\n return next;\n });\n };\n\n const handleUpdate = (payload: unknown) => {\n if (!payload || typeof payload !== 'object') return;\n const { roomId, users } = payload as UpdatePayload;\n if (!roomId) return;\n if (!watchedRef.current.has(roomId)) return;\n setRoomsUsers((prev) => ({ ...prev, [roomId]: normalizeUsers(users) }));\n };\n\n socket.on('presence:snapshot', handleSnapshot);\n socket.on('presence:update', handleUpdate);\n\n // Re-watch on reconnect\n const handleConnect = () => {\n const ids = Array.from(watchedRef.current);\n if (ids.length > 0) socket.emit('presence:watch', { roomIds: ids });\n };\n socket.on('connect', handleConnect);\n\n return () => {\n socket.off('presence:snapshot', handleSnapshot);\n socket.off('presence:update', handleUpdate);\n socket.off('connect', handleConnect);\n\n const ids = Array.from(watchedRef.current);\n if (ids.length > 0) socket.emit('presence:unwatch', { roomIds: ids });\n watchedRef.current = new Set();\n };\n }, [enabled]);\n\n return roomsUsers;\n}\n","import { useMemo, useState, useCallback } from 'react';\nimport type {\n PresenceUser,\n UsePresenceAvatarsStateOptions,\n UsePresenceAvatarsStateReturn,\n} from '../core/types';\n\n/**\n * Extract initial character from user name/id\n */\nfunction getInitialFromUser(user: PresenceUser): string {\n const base = (user?.name || user?.userId || '').trim();\n if (!base) return '?';\n const ch = base.charAt(0);\n return /[a-z]/i.test(ch) ? ch.toUpperCase() : ch;\n}\n\n/**\n * Headless hook for presence avatars UI state management\n *\n * This hook provides all the state and logic needed to build\n * a presence avatars component without any styling.\n *\n * @param options - Configuration options\n * @returns State and handlers for building presence avatars UI\n *\n * @example\n * ```tsx\n * const {\n * visibleUsers,\n * moreCount,\n * hoveredUser,\n * setHoveredIndex,\n * getInitial,\n * getZIndex,\n * } = usePresenceAvatarsState({ users, maxVisible: 3 });\n *\n * return (\n * <div>\n * {visibleUsers.map((user, idx) => (\n * <span\n * key={user.userId}\n * style={{ zIndex: getZIndex(idx, hoveredUser?.userId === user.userId) }}\n * onMouseEnter={() => setHoveredIndex(idx)}\n * onMouseLeave={() => setHoveredIndex(null)}\n * >\n * {getInitial(user)}\n * </span>\n * ))}\n * {moreCount > 0 && <span>+{moreCount}</span>}\n * </div>\n * );\n * ```\n */\nexport function usePresenceAvatarsState(\n options: UsePresenceAvatarsStateOptions,\n): UsePresenceAvatarsStateReturn {\n const { users, maxVisible = 3 } = options;\n\n const safeUsers = Array.isArray(users) ? users : [];\n\n const visibleUsers = useMemo(\n () => safeUsers.slice(0, maxVisible),\n [safeUsers, maxVisible],\n );\n\n const moreCount = Math.max(0, safeUsers.length - visibleUsers.length);\n\n const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n\n const hoveredUser = hoveredIndex != null ? visibleUsers[hoveredIndex] : null;\n\n const baseTopZ = visibleUsers.length + 1;\n\n const getInitial = useCallback((user: PresenceUser): string => {\n return getInitialFromUser(user);\n }, []);\n\n const getZIndex = useCallback(\n (index: number, isHovered: boolean): number => {\n return isHovered ? 100 : baseTopZ - index;\n },\n [baseTopZ],\n );\n\n return {\n visibleUsers,\n moreCount,\n hoveredUser,\n hoveredIndex,\n setHoveredIndex,\n getInitial,\n getZIndex,\n };\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n PresenceUser,\n UsePresenceFloatingStateOptions,\n UsePresenceFloatingStateReturn,\n} from \"../core/types\";\n\n/**\n * Headless hook for presence floating UI state management\n *\n * This hook provides all the state and logic needed to build\n * a draggable floating presence panel without any styling.\n *\n * @param options - Configuration options\n * @returns State and handlers for building floating presence UI\n *\n * @example\n * ```tsx\n * const {\n * containerRef,\n * inlineStyle,\n * isDragging,\n * visibleUsers,\n * moreCount,\n * hoveredUser,\n * tooltipTop,\n * onMouseDownHeader,\n * onAvatarEnter,\n * onAvatarLeave,\n * } = usePresenceFloatingState({ users, maxVisible: 8 });\n *\n * return (\n * <div ref={containerRef} style={inlineStyle} onMouseDown={onMouseDownHeader}>\n * <div className=\"header\">열람중 {users.length}</div>\n * <div className=\"body\">\n * {visibleUsers.map((user) => (\n * <span\n * key={user.userId}\n * onMouseEnter={(e) => onAvatarEnter(e, user)}\n * onMouseLeave={onAvatarLeave}\n * >\n * {user.name}\n * </span>\n * ))}\n * </div>\n * </div>\n * );\n * ```\n */\nexport function usePresenceFloatingState(\n options: UsePresenceFloatingStateOptions\n): UsePresenceFloatingStateReturn {\n const { users, maxVisible = 8, initialPosition = { top: 166 } } = options;\n\n const containerRef = useRef<HTMLDivElement>(null);\n const [dragging, setDragging] = useState(false);\n const [hasDragged, setHasDragged] = useState(false);\n const [dragOffset, setDragOffset] = useState<{ x: number; y: number }>({\n x: 0,\n y: 0,\n });\n const [position, setPosition] = useState<{ top: number; left: number }>({\n top: initialPosition.top,\n left: 0,\n });\n const [hoveredUser, setHoveredUser] = useState<PresenceUser | null>(null);\n const [tooltipTop, setTooltipTop] = useState<number>(0);\n\n const safeUsers = Array.isArray(users) ? users : [];\n const visibleUsers = safeUsers.slice(0, maxVisible);\n const moreCount = Math.max(0, safeUsers.length - visibleUsers.length);\n\n // Position adjustment after drag (resize handling)\n useEffect(() => {\n if (!hasDragged) return;\n\n const el = containerRef.current;\n if (!el) return;\n\n const adjustPosition = () => {\n const rect = el.getBoundingClientRect();\n const maxLeft = window.innerWidth - rect.width;\n const maxTop = window.innerHeight - rect.height - 8;\n setPosition((prev) => ({\n left: Math.min(Math.max(0, prev.left), Math.max(0, maxLeft)),\n top: Math.min(Math.max(8, prev.top), Math.max(8, maxTop)),\n }));\n };\n\n const resizeObserver = new ResizeObserver(() => {\n adjustPosition();\n });\n resizeObserver.observe(el);\n\n window.addEventListener(\"resize\", adjustPosition);\n\n return () => {\n resizeObserver.disconnect();\n window.removeEventListener(\"resize\", adjustPosition);\n };\n }, [hasDragged]);\n\n // Helper to get coordinates from mouse or touch event\n const getEventCoordinates = useCallback(\n (e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => {\n if (\"touches\" in e && e.touches.length > 0) {\n return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY };\n }\n if (\"changedTouches\" in e && e.changedTouches.length > 0) {\n return {\n clientX: e.changedTouches[0].clientX,\n clientY: e.changedTouches[0].clientY,\n };\n }\n if (\"clientX\" in e) {\n return { clientX: e.clientX, clientY: e.clientY };\n }\n return { clientX: 0, clientY: 0 };\n },\n []\n );\n\n const onMouseDownHeader = useCallback(\n (e: React.MouseEvent) => {\n const el = containerRef.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n\n // On first drag, switch to left-based positioning\n if (!hasDragged) {\n setHasDragged(true);\n setPosition({\n top: rect.top,\n left: rect.left,\n });\n }\n\n setDragOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top });\n setDragging(true);\n e.preventDefault();\n },\n [hasDragged]\n );\n\n const onTouchStartHeader = useCallback(\n (e: React.TouchEvent) => {\n const el = containerRef.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n const { clientX, clientY } = getEventCoordinates(e);\n\n // On first drag, switch to left-based positioning\n if (!hasDragged) {\n setHasDragged(true);\n setPosition({\n top: rect.top,\n left: rect.left,\n });\n }\n\n setDragOffset({ x: clientX - rect.left, y: clientY - rect.top });\n setDragging(true);\n // Note: Don't prevent default to allow scrolling detection\n },\n [hasDragged, getEventCoordinates]\n );\n\n // Handle drag movement (mouse and touch)\n useEffect(() => {\n if (!dragging) return;\n\n const el = containerRef.current;\n\n const onMove = (e: MouseEvent | TouchEvent) => {\n if (!el) return;\n const rect = el.getBoundingClientRect();\n const { clientX, clientY } = getEventCoordinates(e);\n const newLeft = clientX - dragOffset.x;\n const newTop = clientY - dragOffset.y;\n const maxLeft = window.innerWidth - rect.width;\n const maxTop = window.innerHeight - rect.height - 8;\n setPosition({\n left: Math.min(Math.max(0, newLeft), Math.max(0, maxLeft)),\n top: Math.min(Math.max(8, newTop), Math.max(8, maxTop)),\n });\n\n // Prevent scrolling while dragging on touch\n if (\"touches\" in e) {\n e.preventDefault();\n }\n };\n\n const onUp = () => setDragging(false);\n\n // Mouse events\n window.addEventListener(\"mousemove\", onMove);\n window.addEventListener(\"mouseup\", onUp, { once: true });\n\n // Touch events\n window.addEventListener(\"touchmove\", onMove, { passive: false });\n window.addEventListener(\"touchend\", onUp, { once: true });\n window.addEventListener(\"touchcancel\", onUp, { once: true });\n\n return () => {\n window.removeEventListener(\"mousemove\", onMove);\n window.removeEventListener(\"mouseup\", onUp);\n window.removeEventListener(\"touchmove\", onMove);\n window.removeEventListener(\"touchend\", onUp);\n window.removeEventListener(\"touchcancel\", onUp);\n };\n }, [dragging, dragOffset.x, dragOffset.y, getEventCoordinates]);\n\n const inlineStyle = useMemo<React.CSSProperties>(() => {\n if (hasDragged) {\n return {\n position: \"fixed\",\n top: position.top,\n left: position.left,\n right: \"auto\",\n };\n } else {\n return {\n position: \"fixed\",\n top: position.top,\n right: 0,\n left: \"auto\",\n };\n }\n }, [position.top, position.left, hasDragged]);\n\n const onAvatarEnter = useCallback(\n (e: React.MouseEvent, user: PresenceUser) => {\n const container = containerRef.current;\n if (!container) {\n setHoveredUser(user);\n setTooltipTop(0);\n return;\n }\n const containerRect = container.getBoundingClientRect();\n const targetRect = (\n e.currentTarget as HTMLElement\n ).getBoundingClientRect();\n const topWithinContainer = targetRect.top - containerRect.top;\n setHoveredUser(user);\n setTooltipTop(topWithinContainer);\n },\n []\n );\n\n const onAvatarLeave = useCallback(() => {\n setHoveredUser(null);\n }, []);\n\n return {\n containerRef,\n position,\n isDragging: dragging,\n hasDragged,\n inlineStyle,\n visibleUsers,\n moreCount,\n hoveredUser,\n tooltipTop,\n onMouseDownHeader,\n onTouchStartHeader,\n onAvatarEnter,\n onAvatarLeave,\n };\n}\n"],"mappings":"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","names":["normalizeUsers","useEffect","useState","useState","useEffect","useMemo","useState","useCallback","useCallback","useEffect","useMemo","useRef","useState"]}
1
+ {"version":3,"sources":["../../src/presence/core/socket.ts","../../src/presence/hooks/usePresence.ts","../../src/presence/hooks/usePresenceWatch.ts","../../src/presence/headless/usePresenceAvatarsState.ts","../../src/presence/headless/usePresenceFloatingState.ts","../../src/presence/components/PresenceAvatars.tsx","../../src/presence/components/PresenceFloating.tsx"],"sourcesContent":["import { io, Socket } from 'socket.io-client';\nimport type { PresenceConfig } from './types';\n\nlet config: PresenceConfig | null = null;\nlet sharedSocket: Socket | null = null;\n\n/**\n * Initialize the presence socket with configuration\n * Must be called before using any presence hooks\n */\nexport function initPresenceSocket(presenceConfig: PresenceConfig): void {\n config = presenceConfig;\n\n // If socket already exists, disconnect and recreate\n if (sharedSocket) {\n sharedSocket.disconnect();\n sharedSocket = null;\n }\n}\n\n/**\n * Get the shared socket instance\n * @throws Error if socket is not initialized\n */\nexport function getPresenceSocket(): Socket {\n if (!config) {\n throw new Error(\n '[presence-module] Socket not initialized. Call initPresenceSocket() first.',\n );\n }\n\n if (sharedSocket && sharedSocket.connected) {\n return sharedSocket;\n }\n\n if (!sharedSocket) {\n sharedSocket = io(config.url, {\n transports: config.transports ?? ['websocket'],\n });\n } else if (!sharedSocket.connected) {\n sharedSocket.connect();\n }\n\n return sharedSocket;\n}\n\n/**\n * Disconnect and cleanup the socket connection\n */\nexport function disconnectPresenceSocket(): void {\n if (sharedSocket) {\n sharedSocket.disconnect();\n sharedSocket = null;\n }\n}\n\n/**\n * Check if socket is initialized\n */\nexport function isPresenceSocketInitialized(): boolean {\n return config !== null;\n}\n","import { useEffect, useState, useCallback } from 'react';\nimport { getPresenceSocket } from '../core/socket';\nimport type { PresenceUser, UsePresenceOptions } from '../core/types';\n\nconst DEFAULT_HEARTBEAT_INTERVAL = 10 * 60 * 1000; // 10 minutes\n\n/**\n * Hook to join a presence room and track users in that room\n *\n * @param options - Configuration options\n * @returns Array of users currently in the room\n *\n * @example\n * ```tsx\n * const users = usePresence({\n * roomId: 'my-app:page-123',\n * currentUser: { userId: '1', name: 'John' },\n * });\n * ```\n */\nexport function usePresence(options: UsePresenceOptions): PresenceUser[] {\n const {\n roomId,\n currentUser,\n enabled = true,\n heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,\n } = options;\n\n const [users, setUsers] = useState<PresenceUser[]>([]);\n\n // Normalize users from server response\n const normalizeUsers = useCallback((incoming: unknown): PresenceUser[] => {\n if (Array.isArray(incoming)) {\n return incoming as PresenceUser[];\n }\n if (\n incoming &&\n typeof incoming === 'object' &&\n Array.isArray((incoming as { users?: unknown }).users)\n ) {\n return (incoming as { users: unknown[] }).users as PresenceUser[];\n }\n return [];\n }, []);\n\n // Clear users when disabled\n useEffect(() => {\n if (!enabled) {\n setUsers([]);\n }\n }, [enabled]);\n\n useEffect(() => {\n if (!enabled) {\n return;\n }\n\n const socket = getPresenceSocket();\n\n const joinRoom = () => {\n socket.emit('presence:join', { roomId, user: currentUser });\n };\n\n // Join immediately if connected, otherwise wait for connect event\n if (socket.connected) {\n joinRoom();\n }\n\n socket.on('connect', joinRoom);\n\n // Heartbeat to extend TTL\n const heartbeat = () => {\n socket.emit('presence:heartbeat', { roomId });\n };\n heartbeat();\n const heartbeatIntervalId = window.setInterval(\n heartbeat,\n heartbeatInterval,\n );\n\n // Handle presence updates from server\n const handlePresenceUpdate = (incoming: unknown) => {\n setUsers(normalizeUsers(incoming));\n };\n socket.on('presence:update', handlePresenceUpdate);\n\n // Notify server when tab/window closes\n const handleBeforeUnload = () => {\n socket.emit('presence:leave', { roomId, userId: currentUser.userId });\n };\n window.addEventListener('beforeunload', handleBeforeUnload);\n\n return () => {\n socket.off('connect', joinRoom);\n socket.off('presence:update', handlePresenceUpdate);\n window.removeEventListener('beforeunload', handleBeforeUnload);\n window.clearInterval(heartbeatIntervalId);\n socket.emit('presence:leave', { roomId, userId: currentUser.userId });\n };\n }, [\n roomId,\n currentUser.userId,\n currentUser.name,\n enabled,\n heartbeatInterval,\n normalizeUsers,\n ]);\n\n return users;\n}\n","import { useEffect, useMemo, useRef, useState, useCallback } from 'react';\nimport { getPresenceSocket } from '../core/socket';\nimport type { PresenceUser, UsePresenceWatchOptions } from '../core/types';\n\ntype SnapshotPayload = {\n rooms?: Record<string, unknown>;\n};\n\ntype UpdatePayload = {\n roomId?: string;\n users?: unknown;\n};\n\nfunction normalizeUsers(value: unknown): PresenceUser[] {\n if (Array.isArray(value)) return value as PresenceUser[];\n if (\n value &&\n typeof value === 'object' &&\n Array.isArray((value as { users?: unknown }).users)\n ) {\n return (value as { users: unknown[] }).users as PresenceUser[];\n }\n return [];\n}\n\n/**\n * Hook to watch multiple rooms without joining them\n * (read-only observation, current user is not added to the rooms)\n *\n * @param options - Configuration options\n * @returns Map of roomId to array of users in that room\n *\n * @example\n * ```tsx\n * const roomsUsers = usePresenceWatch({\n * roomIds: ['room-1', 'room-2', 'room-3'],\n * });\n * // roomsUsers = { 'room-1': [...], 'room-2': [...], ... }\n * ```\n */\nexport function usePresenceWatch(\n options: UsePresenceWatchOptions,\n): Record<string, PresenceUser[]> {\n const { roomIds, enabled = true } = options;\n\n const [roomsUsers, setRoomsUsers] = useState<Record<string, PresenceUser[]>>(\n {},\n );\n\n const normalizedRoomIds = useMemo(() => {\n const uniq = Array.from(new Set((roomIds ?? []).filter(Boolean)));\n return uniq;\n }, [roomIds]);\n\n const watchedRef = useRef<Set<string>>(new Set());\n\n // Handle watch/unwatch based on roomIds changes\n useEffect(() => {\n if (!enabled) {\n setRoomsUsers({});\n watchedRef.current = new Set();\n return;\n }\n\n const socket = getPresenceSocket();\n\n const toWatch = normalizedRoomIds.filter((r) => !watchedRef.current.has(r));\n const toUnwatch = Array.from(watchedRef.current).filter(\n (r) => !normalizedRoomIds.includes(r),\n );\n\n if (toWatch.length > 0) {\n socket.emit('presence:watch', { roomIds: toWatch });\n for (const r of toWatch) watchedRef.current.add(r);\n }\n\n if (toUnwatch.length > 0) {\n socket.emit('presence:unwatch', { roomIds: toUnwatch });\n for (const r of toUnwatch) watchedRef.current.delete(r);\n setRoomsUsers((prev) => {\n const next = { ...prev };\n for (const r of toUnwatch) delete next[r];\n return next;\n });\n }\n }, [enabled, normalizedRoomIds]);\n\n // Handle socket events\n useEffect(() => {\n if (!enabled) return;\n\n const socket = getPresenceSocket();\n\n const handleSnapshot = (payload: SnapshotPayload) => {\n const rooms = payload?.rooms ?? {};\n setRoomsUsers((prev) => {\n const next = { ...prev };\n for (const [roomId, users] of Object.entries(rooms)) {\n next[roomId] = normalizeUsers(users);\n }\n return next;\n });\n };\n\n const handleUpdate = (payload: unknown) => {\n if (!payload || typeof payload !== 'object') return;\n const { roomId, users } = payload as UpdatePayload;\n if (!roomId) return;\n if (!watchedRef.current.has(roomId)) return;\n setRoomsUsers((prev) => ({ ...prev, [roomId]: normalizeUsers(users) }));\n };\n\n socket.on('presence:snapshot', handleSnapshot);\n socket.on('presence:update', handleUpdate);\n\n // Re-watch on reconnect\n const handleConnect = () => {\n const ids = Array.from(watchedRef.current);\n if (ids.length > 0) socket.emit('presence:watch', { roomIds: ids });\n };\n socket.on('connect', handleConnect);\n\n return () => {\n socket.off('presence:snapshot', handleSnapshot);\n socket.off('presence:update', handleUpdate);\n socket.off('connect', handleConnect);\n\n const ids = Array.from(watchedRef.current);\n if (ids.length > 0) socket.emit('presence:unwatch', { roomIds: ids });\n watchedRef.current = new Set();\n };\n }, [enabled]);\n\n return roomsUsers;\n}\n","import { useMemo, useState, useCallback } from 'react';\nimport type {\n PresenceUser,\n UsePresenceAvatarsStateOptions,\n UsePresenceAvatarsStateReturn,\n} from '../core/types';\n\n/**\n * Extract initial character from user name/id\n */\nfunction getInitialFromUser(user: PresenceUser): string {\n const base = (user?.name || user?.userId || '').trim();\n if (!base) return '?';\n const ch = base.charAt(0);\n return /[a-z]/i.test(ch) ? ch.toUpperCase() : ch;\n}\n\n/**\n * Headless hook for presence avatars UI state management\n *\n * This hook provides all the state and logic needed to build\n * a presence avatars component without any styling.\n *\n * @param options - Configuration options\n * @returns State and handlers for building presence avatars UI\n *\n * @example\n * ```tsx\n * const {\n * visibleUsers,\n * moreCount,\n * hoveredUser,\n * setHoveredIndex,\n * getInitial,\n * getZIndex,\n * } = usePresenceAvatarsState({ users, maxVisible: 3 });\n *\n * return (\n * <div>\n * {visibleUsers.map((user, idx) => (\n * <span\n * key={user.userId}\n * style={{ zIndex: getZIndex(idx, hoveredUser?.userId === user.userId) }}\n * onMouseEnter={() => setHoveredIndex(idx)}\n * onMouseLeave={() => setHoveredIndex(null)}\n * >\n * {getInitial(user)}\n * </span>\n * ))}\n * {moreCount > 0 && <span>+{moreCount}</span>}\n * </div>\n * );\n * ```\n */\nexport function usePresenceAvatarsState(\n options: UsePresenceAvatarsStateOptions,\n): UsePresenceAvatarsStateReturn {\n const { users, maxVisible = 3 } = options;\n\n const safeUsers = Array.isArray(users) ? users : [];\n\n const visibleUsers = useMemo(\n () => safeUsers.slice(0, maxVisible),\n [safeUsers, maxVisible],\n );\n\n const moreCount = Math.max(0, safeUsers.length - visibleUsers.length);\n\n const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);\n\n const hoveredUser = hoveredIndex != null ? visibleUsers[hoveredIndex] : null;\n\n const baseTopZ = visibleUsers.length + 1;\n\n const getInitial = useCallback((user: PresenceUser): string => {\n return getInitialFromUser(user);\n }, []);\n\n const getZIndex = useCallback(\n (index: number, isHovered: boolean): number => {\n return isHovered ? 100 : baseTopZ - index;\n },\n [baseTopZ],\n );\n\n return {\n visibleUsers,\n moreCount,\n hoveredUser,\n hoveredIndex,\n setHoveredIndex,\n getInitial,\n getZIndex,\n };\n}\n","import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport type {\n PresenceUser,\n UsePresenceFloatingStateOptions,\n UsePresenceFloatingStateReturn,\n} from \"../core/types\";\n\n/**\n * Headless hook for presence floating UI state management\n *\n * This hook provides all the state and logic needed to build\n * a draggable floating presence panel without any styling.\n *\n * @param options - Configuration options\n * @returns State and handlers for building floating presence UI\n *\n * @example\n * ```tsx\n * const {\n * containerRef,\n * inlineStyle,\n * isDragging,\n * visibleUsers,\n * moreCount,\n * hoveredUser,\n * tooltipTop,\n * onMouseDownHeader,\n * onAvatarEnter,\n * onAvatarLeave,\n * } = usePresenceFloatingState({ users, maxVisible: 8 });\n *\n * return (\n * <div ref={containerRef} style={inlineStyle} onMouseDown={onMouseDownHeader}>\n * <div className=\"header\">열람중 {users.length}</div>\n * <div className=\"body\">\n * {visibleUsers.map((user) => (\n * <span\n * key={user.userId}\n * onMouseEnter={(e) => onAvatarEnter(e, user)}\n * onMouseLeave={onAvatarLeave}\n * >\n * {user.name}\n * </span>\n * ))}\n * </div>\n * </div>\n * );\n * ```\n */\nexport function usePresenceFloatingState(\n options: UsePresenceFloatingStateOptions\n): UsePresenceFloatingStateReturn {\n const { users, maxVisible = 8, initialPosition = { top: 166 } } = options;\n\n const containerRef = useRef<HTMLDivElement>(null);\n const [dragging, setDragging] = useState(false);\n const [hasDragged, setHasDragged] = useState(false);\n const [dragOffset, setDragOffset] = useState<{ x: number; y: number }>({\n x: 0,\n y: 0,\n });\n const [position, setPosition] = useState<{ top: number; left: number }>({\n top: initialPosition.top,\n left: 0,\n });\n const [hoveredUser, setHoveredUser] = useState<PresenceUser | null>(null);\n const [tooltipTop, setTooltipTop] = useState<number>(0);\n\n const safeUsers = Array.isArray(users) ? users : [];\n const visibleUsers = safeUsers.slice(0, maxVisible);\n const moreCount = Math.max(0, safeUsers.length - visibleUsers.length);\n\n // Position adjustment after drag (resize handling)\n useEffect(() => {\n if (!hasDragged) return;\n\n const el = containerRef.current;\n if (!el) return;\n\n const adjustPosition = () => {\n const rect = el.getBoundingClientRect();\n const maxLeft = window.innerWidth - rect.width;\n const maxTop = window.innerHeight - rect.height - 8;\n setPosition((prev) => ({\n left: Math.min(Math.max(0, prev.left), Math.max(0, maxLeft)),\n top: Math.min(Math.max(8, prev.top), Math.max(8, maxTop)),\n }));\n };\n\n const resizeObserver = new ResizeObserver(() => {\n adjustPosition();\n });\n resizeObserver.observe(el);\n\n window.addEventListener(\"resize\", adjustPosition);\n\n return () => {\n resizeObserver.disconnect();\n window.removeEventListener(\"resize\", adjustPosition);\n };\n }, [hasDragged]);\n\n // Helper to get coordinates from mouse or touch event\n const getEventCoordinates = useCallback(\n (e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => {\n if (\"touches\" in e && e.touches.length > 0) {\n return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY };\n }\n if (\"changedTouches\" in e && e.changedTouches.length > 0) {\n return {\n clientX: e.changedTouches[0].clientX,\n clientY: e.changedTouches[0].clientY,\n };\n }\n if (\"clientX\" in e) {\n return { clientX: e.clientX, clientY: e.clientY };\n }\n return { clientX: 0, clientY: 0 };\n },\n []\n );\n\n const onMouseDownHeader = useCallback(\n (e: React.MouseEvent) => {\n const el = containerRef.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n\n // On first drag, switch to left-based positioning\n if (!hasDragged) {\n setHasDragged(true);\n setPosition({\n top: rect.top,\n left: rect.left,\n });\n }\n\n setDragOffset({ x: e.clientX - rect.left, y: e.clientY - rect.top });\n setDragging(true);\n e.preventDefault();\n },\n [hasDragged]\n );\n\n const onTouchStartHeader = useCallback(\n (e: React.TouchEvent) => {\n const el = containerRef.current;\n if (!el) return;\n const rect = el.getBoundingClientRect();\n const { clientX, clientY } = getEventCoordinates(e);\n\n // On first drag, switch to left-based positioning\n if (!hasDragged) {\n setHasDragged(true);\n setPosition({\n top: rect.top,\n left: rect.left,\n });\n }\n\n setDragOffset({ x: clientX - rect.left, y: clientY - rect.top });\n setDragging(true);\n // Note: Don't prevent default to allow scrolling detection\n },\n [hasDragged, getEventCoordinates]\n );\n\n // Handle drag movement (mouse and touch)\n useEffect(() => {\n if (!dragging) return;\n\n const el = containerRef.current;\n\n const onMove = (e: MouseEvent | TouchEvent) => {\n if (!el) return;\n const rect = el.getBoundingClientRect();\n const { clientX, clientY } = getEventCoordinates(e);\n const newLeft = clientX - dragOffset.x;\n const newTop = clientY - dragOffset.y;\n const maxLeft = window.innerWidth - rect.width;\n const maxTop = window.innerHeight - rect.height - 8;\n setPosition({\n left: Math.min(Math.max(0, newLeft), Math.max(0, maxLeft)),\n top: Math.min(Math.max(8, newTop), Math.max(8, maxTop)),\n });\n\n // Prevent scrolling while dragging on touch\n if (\"touches\" in e) {\n e.preventDefault();\n }\n };\n\n const onUp = () => setDragging(false);\n\n // Mouse events\n window.addEventListener(\"mousemove\", onMove);\n window.addEventListener(\"mouseup\", onUp, { once: true });\n\n // Touch events\n window.addEventListener(\"touchmove\", onMove, { passive: false });\n window.addEventListener(\"touchend\", onUp, { once: true });\n window.addEventListener(\"touchcancel\", onUp, { once: true });\n\n return () => {\n window.removeEventListener(\"mousemove\", onMove);\n window.removeEventListener(\"mouseup\", onUp);\n window.removeEventListener(\"touchmove\", onMove);\n window.removeEventListener(\"touchend\", onUp);\n window.removeEventListener(\"touchcancel\", onUp);\n };\n }, [dragging, dragOffset.x, dragOffset.y, getEventCoordinates]);\n\n const inlineStyle = useMemo<React.CSSProperties>(() => {\n if (hasDragged) {\n return {\n position: \"fixed\",\n top: position.top,\n left: position.left,\n right: \"auto\",\n };\n } else {\n return {\n position: \"fixed\",\n top: position.top,\n right: 0,\n left: \"auto\",\n };\n }\n }, [position.top, position.left, hasDragged]);\n\n const onAvatarEnter = useCallback(\n (e: React.MouseEvent, user: PresenceUser) => {\n const container = containerRef.current;\n if (!container) {\n setHoveredUser(user);\n setTooltipTop(0);\n return;\n }\n const containerRect = container.getBoundingClientRect();\n const targetRect = (\n e.currentTarget as HTMLElement\n ).getBoundingClientRect();\n const topWithinContainer = targetRect.top - containerRect.top;\n setHoveredUser(user);\n setTooltipTop(topWithinContainer);\n },\n []\n );\n\n const onAvatarLeave = useCallback(() => {\n setHoveredUser(null);\n }, []);\n\n return {\n containerRef,\n position,\n isDragging: dragging,\n hasDragged,\n inlineStyle,\n visibleUsers,\n moreCount,\n hoveredUser,\n tooltipTop,\n onMouseDownHeader,\n onTouchStartHeader,\n onAvatarEnter,\n onAvatarLeave,\n };\n}\n","import React, { useState, useCallback } from \"react\";\nimport { usePresenceAvatarsState } from \"../headless/usePresenceAvatarsState\";\nimport type {\n PresenceUser,\n PresenceAvatarsProps,\n PresenceAvatarRenderProps,\n PresenceAvatarsTooltipRenderProps,\n} from \"../core/types\";\n\n/**\n * PresenceAvatars - Displays a stack of user avatars with hover tooltips\n *\n * @example\n * ```tsx\n * // Basic usage\n * <PresenceAvatars users={users} />\n *\n * // Custom avatar\n * <PresenceAvatars\n * users={users}\n * renderAvatar={({ user, initial }) => (\n * <img src={user.avatarUrl} alt={user.name} />\n * )}\n * />\n *\n * // Custom tooltip\n * <PresenceAvatars\n * users={users}\n * renderTooltip={({ user, position }) => (\n * <div style={{ top: position.top, left: position.left }}>\n * {user.name}\n * </div>\n * )}\n * />\n * ```\n */\nexport function PresenceAvatars({\n users,\n maxVisible = 3,\n className,\n renderAvatar,\n renderTooltip,\n renderMore,\n}: PresenceAvatarsProps) {\n const {\n visibleUsers,\n moreCount,\n hoveredUser,\n hoveredIndex,\n setHoveredIndex,\n getInitial,\n getZIndex,\n } = usePresenceAvatarsState({ users, maxVisible });\n\n const [tooltipPos, setTooltipPos] = useState<{\n top: number;\n left: number;\n } | null>(null);\n\n const updateTooltipPos = useCallback((el: HTMLElement) => {\n const rect = el.getBoundingClientRect();\n const tooltipWidth = 200;\n const half = tooltipWidth / 2;\n const centerX = rect.left + rect.width / 2;\n const left = Math.min(\n Math.max(centerX, half + 8),\n window.innerWidth - half - 8\n );\n const top = Math.min(rect.bottom + 8, window.innerHeight - 8);\n setTooltipPos({ top, left });\n }, []);\n\n const handleMouseEnter = useCallback(\n (e: React.MouseEvent, idx: number) => {\n setHoveredIndex(idx);\n updateTooltipPos(e.currentTarget as HTMLElement);\n },\n [setHoveredIndex, updateTooltipPos]\n );\n\n const handleMouseLeave = useCallback(() => {\n setHoveredIndex(null);\n }, [setHoveredIndex]);\n\n // Default tooltip renderer\n const defaultTooltipRenderer = useCallback(\n ({ user, position }: PresenceAvatarsTooltipRenderProps) => (\n <div\n className=\"presence-avatars__tooltip\"\n style={{ top: position.top, left: position.left }}\n >\n <div className=\"presence-avatars__tooltip-row\">\n <span className=\"presence-avatars__tooltip-key\">Name</span>\n <span className=\"presence-avatars__tooltip-val\">\n {user.name || \"-\"}\n </span>\n </div>\n <div className=\"presence-avatars__tooltip-row\">\n <span className=\"presence-avatars__tooltip-key\">ID</span>\n <span className=\"presence-avatars__tooltip-val\">\n {user.userId || \"-\"}\n </span>\n </div>\n </div>\n ),\n []\n );\n\n // Default avatar renderer\n const defaultAvatarRenderer = useCallback(\n ({ initial, isHovered }: PresenceAvatarRenderProps) => <>{initial}</>,\n []\n );\n\n return (\n <div className={`presence-avatars ${className ?? \"\"}`}>\n <div className=\"presence-avatars__stack\">\n {visibleUsers.map((user, idx) => {\n const isHovered = hoveredIndex === idx;\n const initial = getInitial(user);\n const zIndex = getZIndex(idx, isHovered);\n\n const renderProps: PresenceAvatarRenderProps = {\n user,\n index: idx,\n initial,\n isHovered,\n zIndex,\n };\n\n return (\n <span\n key={`${user.userId}-${idx}`}\n className={`presence-avatars__avatar ${\n isHovered ? \"is-hovered\" : \"\"\n }`}\n style={{ zIndex }}\n onMouseEnter={(e) => handleMouseEnter(e, idx)}\n onMouseLeave={handleMouseLeave}\n aria-label={user.name || user.userId || \"\"}\n >\n {renderAvatar\n ? renderAvatar(renderProps)\n : defaultAvatarRenderer(renderProps)}\n </span>\n );\n })}\n {moreCount > 0 &&\n (renderMore ? (\n renderMore(moreCount)\n ) : (\n <span className=\"presence-avatars__more\">+{moreCount}</span>\n ))}\n </div>\n\n {hoveredUser &&\n tooltipPos &&\n (renderTooltip\n ? renderTooltip({ user: hoveredUser, position: tooltipPos })\n : defaultTooltipRenderer({\n user: hoveredUser,\n position: tooltipPos,\n }))}\n </div>\n );\n}\n\nexport default PresenceAvatars;\n","import React, { useCallback } from \"react\";\nimport { usePresenceFloatingState } from \"../headless/usePresenceFloatingState\";\nimport type {\n PresenceUser,\n PresenceFloatingProps,\n PresenceFloatingAvatarRenderProps,\n PresenceFloatingTooltipRenderProps,\n} from \"../core/types\";\n\n/**\n * Extract initial character from user name/id\n */\nfunction getInitial(user: PresenceUser): string {\n const base = (user?.name || user?.userId || \"\").trim();\n if (!base) return \"?\";\n const ch = base.charAt(0);\n return /[a-z]/i.test(ch) ? ch.toUpperCase() : ch;\n}\n\n/**\n * PresenceFloating - Draggable floating panel showing presence users\n *\n * @example\n * ```tsx\n * // Basic usage\n * <PresenceFloating users={users} />\n *\n * // Custom text\n * <PresenceFloating\n * users={users}\n * title=\"Viewing\"\n * emptyText=\"No one\"\n * moreText={(n) => `+${n} more`}\n * />\n *\n * // Custom avatar\n * <PresenceFloating\n * users={users}\n * renderAvatar={({ user, initial }) => (\n * <img src={user.avatarUrl} alt={user.name} />\n * )}\n * />\n * ```\n */\nexport function PresenceFloating({\n users,\n maxVisible = 8,\n className,\n style,\n initialPosition,\n title = \"열람중\",\n emptyText = \"없음\",\n moreText = (n) => `+${n}명`,\n renderAvatar,\n renderTooltip,\n}: PresenceFloatingProps) {\n const {\n containerRef,\n inlineStyle,\n isDragging,\n visibleUsers,\n moreCount,\n hoveredUser,\n tooltipTop,\n onMouseDownHeader,\n onTouchStartHeader,\n onAvatarEnter,\n onAvatarLeave,\n } = usePresenceFloatingState({ users, maxVisible, initialPosition });\n\n // Default tooltip renderer\n const defaultTooltipRenderer = useCallback(\n ({ user, top }: PresenceFloatingTooltipRenderProps) => (\n <div\n className=\"presence-floating__tooltip\"\n style={{ top: Math.max(0, top - 6) }}\n onMouseLeave={onAvatarLeave}\n >\n <div className=\"presence-floating__tooltip-row\">\n <span className=\"presence-floating__tooltip-key\">Name</span>\n <span className=\"presence-floating__tooltip-val\">\n {user.name || \"-\"}\n </span>\n </div>\n <div className=\"presence-floating__tooltip-row\">\n <span className=\"presence-floating__tooltip-key\">ID</span>\n <span className=\"presence-floating__tooltip-val\">\n {user.userId || \"-\"}\n </span>\n </div>\n </div>\n ),\n [onAvatarLeave]\n );\n\n // Default avatar renderer\n const defaultAvatarRenderer = useCallback(\n ({ initial }: PresenceFloatingAvatarRenderProps) => <>{initial}</>,\n []\n );\n\n const combinedStyle: React.CSSProperties = {\n ...inlineStyle,\n ...style,\n };\n\n return (\n <div\n ref={containerRef}\n className={`presence-floating ${isDragging ? \"is-dragging\" : \"\"} ${\n className ?? \"\"\n }`}\n style={combinedStyle}\n >\n <div\n className=\"presence-floating__header\"\n onMouseDown={onMouseDownHeader}\n onTouchStart={onTouchStartHeader}\n >\n <span\n className=\"presence-floating__title\"\n title={title}\n aria-label={title}\n >\n {title}\n </span>\n <span className=\"presence-floating__count\">{users.length}</span>\n </div>\n\n {hoveredUser &&\n (renderTooltip\n ? renderTooltip({ user: hoveredUser, top: tooltipTop })\n : defaultTooltipRenderer({ user: hoveredUser, top: tooltipTop }))}\n\n <div className=\"presence-floating__body\">\n {visibleUsers.length === 0 ? (\n <span className=\"presence-floating__empty\">{emptyText}</span>\n ) : (\n visibleUsers.map((user, idx) => {\n const initial = getInitial(user);\n const label = user.name || user.userId || \"\";\n\n const renderProps: PresenceFloatingAvatarRenderProps = {\n user,\n initial,\n };\n\n return (\n <span\n key={`${user.userId}-${idx}`}\n className=\"presence-floating__avatar\"\n title={label}\n aria-label={label}\n onMouseEnter={(e) => onAvatarEnter(e, user)}\n onMouseLeave={onAvatarLeave}\n >\n {renderAvatar\n ? renderAvatar(renderProps)\n : defaultAvatarRenderer(renderProps)}\n </span>\n );\n })\n )}\n {moreCount > 0 && (\n <span className=\"presence-floating__more\">{moreText(moreCount)}</span>\n )}\n </div>\n </div>\n );\n}\n\nexport default PresenceFloating;\n"],"mappings":"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","names":["normalizeUsers","useEffect","useState","useState","useEffect","useMemo","useState","useCallback","getInitial","useCallback","useEffect","useMemo","useRef","useState","useState","useCallback","getInitial","useState","useCallback","useCallback","Fragment","jsx","jsxs","useCallback"]}
@@ -0,0 +1,240 @@
1
+ /* ========================================
2
+ Presence Components Styles
3
+ ======================================== */
4
+
5
+ /* ----------------------------------------
6
+ PresenceAvatars
7
+ ---------------------------------------- */
8
+
9
+ .presence-avatars {
10
+ position: relative;
11
+ display: flex;
12
+ justify-content: center;
13
+ margin-top: 4px;
14
+ overflow: visible;
15
+ }
16
+
17
+ .presence-avatars__stack {
18
+ position: relative;
19
+ display: inline-flex;
20
+ align-items: center;
21
+ overflow: visible;
22
+ }
23
+
24
+ .presence-avatars__avatar {
25
+ width: 18px;
26
+ height: 18px;
27
+ display: inline-flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ border-radius: 50%;
31
+ background: #eef2f6;
32
+ border: 1px solid #cfd6dd;
33
+ color: #333d4b;
34
+ font-size: 11px;
35
+ font-weight: 700;
36
+ line-height: 18px;
37
+ text-align: center;
38
+ user-select: none;
39
+ position: relative;
40
+ cursor: pointer;
41
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
42
+ }
43
+
44
+ .presence-avatars__avatar + .presence-avatars__avatar {
45
+ margin-left: -8px;
46
+ }
47
+
48
+ .presence-avatars__avatar.is-hovered {
49
+ border-color: #adb5bd;
50
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
51
+ transform: scale(1.1);
52
+ }
53
+
54
+ .presence-avatars__more {
55
+ margin-left: 4px;
56
+ font-size: 11px;
57
+ color: #4e5968;
58
+ align-self: center;
59
+ }
60
+
61
+ .presence-avatars__tooltip {
62
+ position: fixed;
63
+ transform: translateX(-50%);
64
+ width: 200px;
65
+ padding: 8px 10px;
66
+ background: #ffffff;
67
+ border: 1px solid #e5e8eb;
68
+ border-radius: 8px;
69
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
70
+ z-index: 9999;
71
+ pointer-events: none;
72
+ }
73
+
74
+ .presence-avatars__tooltip-row {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 8px;
78
+ font-size: 12px;
79
+ color: #333d4b;
80
+ }
81
+
82
+ .presence-avatars__tooltip-row + .presence-avatars__tooltip-row {
83
+ margin-top: 4px;
84
+ }
85
+
86
+ .presence-avatars__tooltip-key {
87
+ width: 42px;
88
+ color: #6b7684;
89
+ flex-shrink: 0;
90
+ }
91
+
92
+ .presence-avatars__tooltip-val {
93
+ color: #111;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ white-space: nowrap;
97
+ }
98
+
99
+ /* ----------------------------------------
100
+ PresenceFloating
101
+ ---------------------------------------- */
102
+
103
+ .presence-floating {
104
+ position: fixed;
105
+ z-index: 2000;
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 8px;
109
+ width: 100px;
110
+ max-height: 300px;
111
+ padding: 10px 12px;
112
+ background: rgba(255, 255, 255, 0.85);
113
+ border: 1px solid #e5e8eb;
114
+ border-radius: 10px;
115
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
116
+ overflow: visible;
117
+ backdrop-filter: blur(8px);
118
+ -webkit-backdrop-filter: blur(8px);
119
+ }
120
+
121
+ .presence-floating__header {
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: space-between;
125
+ gap: 8px;
126
+ cursor: grab;
127
+ user-select: none;
128
+ }
129
+
130
+ .presence-floating.is-dragging .presence-floating__header {
131
+ cursor: grabbing;
132
+ }
133
+
134
+ .presence-floating__title {
135
+ font-weight: 700;
136
+ font-size: 12px;
137
+ color: #111;
138
+ flex: 1;
139
+ min-width: 0;
140
+ white-space: nowrap;
141
+ overflow: hidden;
142
+ text-overflow: ellipsis;
143
+ }
144
+
145
+ .presence-floating__count {
146
+ min-width: 20px;
147
+ height: 20px;
148
+ padding: 0 6px;
149
+ display: inline-flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ font-size: 11px;
153
+ font-weight: 600;
154
+ color: #111;
155
+ background: #f2f4f6;
156
+ border: 1px solid #e5e8eb;
157
+ border-radius: 10px;
158
+ }
159
+
160
+ .presence-floating__body {
161
+ display: flex;
162
+ flex-direction: column;
163
+ align-items: flex-start;
164
+ gap: 8px;
165
+ overflow-y: auto;
166
+ padding-right: 2px;
167
+ flex: 1;
168
+ min-height: 0;
169
+ }
170
+
171
+ .presence-floating__tooltip {
172
+ position: absolute;
173
+ right: calc(100% + 8px);
174
+ top: 0;
175
+ width: 200px;
176
+ padding: 8px 10px;
177
+ background: #ffffff;
178
+ border: 1px solid #e5e8eb;
179
+ border-radius: 8px;
180
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
181
+ z-index: 2100;
182
+ }
183
+
184
+ .presence-floating__tooltip-row {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 8px;
188
+ font-size: 12px;
189
+ color: #333d4b;
190
+ }
191
+
192
+ .presence-floating__tooltip-row + .presence-floating__tooltip-row {
193
+ margin-top: 4px;
194
+ }
195
+
196
+ .presence-floating__tooltip-key {
197
+ width: 42px;
198
+ color: #6b7684;
199
+ flex-shrink: 0;
200
+ }
201
+
202
+ .presence-floating__tooltip-val {
203
+ color: #111;
204
+ overflow: hidden;
205
+ text-overflow: ellipsis;
206
+ white-space: nowrap;
207
+ }
208
+
209
+ .presence-floating__avatar {
210
+ width: 36px;
211
+ height: 36px;
212
+ display: inline-flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ border-radius: 50%;
216
+ background: #eef2f6;
217
+ border: 1px solid #e5e8eb;
218
+ color: #333d4b;
219
+ font-size: 14px;
220
+ font-weight: 700;
221
+ line-height: 1;
222
+ user-select: none;
223
+ cursor: pointer;
224
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
225
+ }
226
+
227
+ .presence-floating__avatar:hover {
228
+ transform: scale(1.05);
229
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
230
+ }
231
+
232
+ .presence-floating__more {
233
+ font-size: 12px;
234
+ color: #4e5968;
235
+ }
236
+
237
+ .presence-floating__empty {
238
+ font-size: 12px;
239
+ color: #8b95a1;
240
+ }