@rizvanua/contact-chat 0.1.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.
- package/LICENSE +21 -0
- package/README.md +496 -0
- package/dist/index.cjs +704 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +688 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +326 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +118 -0
- package/dist/react/index.d.ts +118 -0
- package/dist/react/index.js +306 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/index.cjs +704 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.d.cts +98 -0
- package/dist/server/index.d.ts +98 -0
- package/dist/server/index.js +688 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/nextjs.cjs +10 -0
- package/dist/server/nextjs.cjs.map +1 -0
- package/dist/server/nextjs.d.cts +17 -0
- package/dist/server/nextjs.d.ts +17 -0
- package/dist/server/nextjs.js +8 -0
- package/dist/server/nextjs.js.map +1 -0
- package/dist/stores/index.cjs +244 -0
- package/dist/stores/index.cjs.map +1 -0
- package/dist/stores/index.d.cts +58 -0
- package/dist/stores/index.d.ts +58 -0
- package/dist/stores/index.js +241 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/transports/index.cjs +156 -0
- package/dist/transports/index.cjs.map +1 -0
- package/dist/transports/index.d.cts +48 -0
- package/dist/transports/index.d.ts +48 -0
- package/dist/transports/index.js +151 -0
- package/dist/transports/index.js.map +1 -0
- package/dist/types-BcoqxSLg.d.cts +42 -0
- package/dist/types-BcoqxSLg.d.ts +42 -0
- package/dist/ui/index.cjs +609 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +127 -0
- package/dist/ui/index.d.ts +127 -0
- package/dist/ui/index.js +604 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +146 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/react/atoms.ts","../../src/core/name.ts","../../src/react/client.ts","../../src/core/types.ts","../../src/react/config.tsx","../../src/react/use-chat-actions.ts","../../src/react/chime.ts","../../src/react/use-chat-sync.ts","../../src/ui/MessageBubble.tsx","../../src/ui/NameGate.tsx","../../src/ui/ChatDialog.tsx","../../src/ui/ChatLauncher.tsx"],"names":["useAtomValue","useSetAtom","useAtom","useCallback","jsx","DEFAULT_CONTAINER_CLASSES","useState","twMerge","useRef","useEffect","jsxs","Fragment"],"mappings":"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","file":"index.js","sourcesContent":["import { atom } from 'jotai';\nimport type { ChatMessage } from '../core/types.js';\n\nexport type ChatStatus = 'idle' | 'sending' | 'error' | 'unavailable';\n\nexport const chatOpenAtom = atom(false);\nexport const chatSessionIdAtom = atom<string | null>(null);\nexport const chatNameAtom = atom<string | null>(null);\nexport const chatMessagesAtom = atom<ChatMessage[]>([]);\nexport const chatCursorAtom = atom(0);\nexport const chatUnreadAtom = atom(0);\nexport const chatStatusAtom = atom<ChatStatus>('idle');\nexport const chatErrorAtom = atom<string | null>(null);\n","export const MAX_NAME_LENGTH = 60;\n\n/**\n * Normalises a visitor-supplied name for display in the owner's Telegram.\n * Returns '' when nothing usable remains — emptiness is the only failure.\n *\n * Not an injection defence: Telegram does not parse topic names as markup.\n * This exists so control characters and runaway length cannot reach the\n * owner's topic list.\n */\nexport function sanitizeName(raw: unknown): string {\n if (typeof raw !== 'string') return '';\n return raw\n // Strip control characters, including newlines and tabs.\n .replace(/[\\u0000-\\u001F\\u007F]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .slice(0, MAX_NAME_LENGTH);\n}\n","import type { ChatMessage } from '../core/types.js';\n\nexport function getStoredName(nameStorageKey: string): string | null {\n if (typeof window === 'undefined') return null;\n return window.localStorage.getItem(nameStorageKey);\n}\n\nexport function storeName(nameStorageKey: string, name: string): void {\n window.localStorage.setItem(nameStorageKey, name);\n}\n\nexport function getStoredSessionId(sessionStorageKey: string): string | null {\n if (typeof window === 'undefined') return null;\n return window.localStorage.getItem(sessionStorageKey);\n}\n\nexport function ensureSessionId(sessionStorageKey: string): string {\n const existing = getStoredSessionId(sessionStorageKey);\n if (existing) return existing;\n const created = window.crypto.randomUUID();\n window.localStorage.setItem(sessionStorageKey, created);\n return created;\n}\n\n/**\n * Merges polled messages into the local list, deduplicating by id.\n *\n * Deduplication is what lets send() append its message optimistically without\n * touching the cursor. Advancing the cursor from the send response would skip\n * anything already sitting at a lower index — an owner reply that arrived\n * between polls would be lost. Instead the cursor advances only from polling,\n * and the visitor's own message is recognised by id when it comes back.\n *\n * Sorted by server timestamp so an owner reply that arrives at a lower index\n * still displays before the visitor's newer message.\n */\nexport function mergeMessages(\n current: ChatMessage[],\n incoming: ChatMessage[]\n): ChatMessage[] {\n const seen = new Set(current.map((message) => message.id));\n const fresh = incoming.filter((message) => !seen.has(message.id));\n if (fresh.length === 0) return current;\n return [...current, ...fresh].sort((a, b) => a.ts - b.ts);\n}\n","export type ChatAuthor = 'visitor' | 'owner';\n\nexport interface ChatMessage {\n id: string;\n from: ChatAuthor;\n text: string;\n ts: number;\n}\n\nexport interface ChatSession {\n topicId: number;\n createdAt: number;\n msgCount: number;\n // Optional because sessions created before the name gate shipped have none.\n name?: string;\n}\n\nexport interface SendRequestBody {\n sessionId: string;\n text: string;\n name?: string;\n honeypot?: string;\n}\n\nexport interface SendResponseBody {\n message: ChatMessage;\n cursor: number;\n}\n\nexport interface PollResponseBody {\n messages: ChatMessage[];\n cursor: number;\n}\n\nexport type ChatErrorCode =\n | 'validation'\n | 'rate_limited'\n | 'store_unavailable'\n | 'telegram_unavailable';\n\nexport interface ChatErrorBody {\n code: ChatErrorCode;\n error: string;\n}\n\n// @deprecated Use DEFAULT_CHAT_LIMITS.maxMessageLength instead.\nexport const MAX_MESSAGE_LENGTH = 1000;\n// @deprecated Use DEFAULT_CHAT_LIMITS.maxMessagesPerSession instead.\nexport const MAX_MESSAGES_PER_SESSION = 40;\n// @deprecated Use DEFAULT_CHAT_LIMITS.sessionTtlSeconds instead.\nexport const SESSION_TTL_SECONDS = 604800; // 7 days\n// @deprecated Use DEFAULT_CHAT_LIMITS.ratePerMinute instead.\nexport const RATE_PER_MINUTE = 5;\n// @deprecated Use DEFAULT_CHAT_LIMITS.ratePerDay instead.\nexport const RATE_PER_DAY = 30;\n\nexport interface ChatLimits {\n maxMessageLength: number;\n maxMessagesPerSession: number;\n sessionTtlSeconds: number;\n ratePerMinute: number;\n ratePerDay: number;\n}\n\nexport const DEFAULT_CHAT_LIMITS: ChatLimits = {\n maxMessageLength: MAX_MESSAGE_LENGTH,\n maxMessagesPerSession: MAX_MESSAGES_PER_SESSION,\n sessionTtlSeconds: SESSION_TTL_SECONDS,\n ratePerMinute: RATE_PER_MINUTE,\n ratePerDay: RATE_PER_DAY,\n};\n","'use client';\n\nimport React, { createContext, useContext, useMemo } from 'react';\nimport { DEFAULT_CHAT_LIMITS } from '../core/types.js';\n\nexport type ChatClientEvent =\n | { type: 'chat.open' }\n | { type: 'chat.close' }\n | { type: 'chat.send.attempt' }\n | { type: 'chat.send.success' }\n | { type: 'chat.send.error'; code: string };\n\nexport interface ChatClientConfig {\n /** Base path for the three route files. Default: '/api/chat'. */\n basePath: string;\n /** localStorage key for the session id. Default: 'contact-chat-session'. */\n sessionStorageKey: string;\n /** localStorage key for the visitor's name. Default: 'contact-chat-name'. */\n nameStorageKey: string;\n /** Play a chime when an owner reply arrives. Default: true. */\n chime: boolean;\n /** Client-side max message length; matches the server's `ChatLimits.maxMessageLength`. Default: DEFAULT_CHAT_LIMITS.maxMessageLength. */\n maxMessageLength: number;\n /** Optional analytics callback fired on lifecycle events. */\n onEvent?: (event: ChatClientEvent) => void;\n}\n\nexport const DEFAULT_CHAT_CLIENT_CONFIG: ChatClientConfig = {\n basePath: '/api/chat',\n sessionStorageKey: 'contact-chat-session',\n nameStorageKey: 'contact-chat-name',\n chime: true,\n maxMessageLength: DEFAULT_CHAT_LIMITS.maxMessageLength,\n};\n\nconst Ctx = createContext<ChatClientConfig>(DEFAULT_CHAT_CLIENT_CONFIG);\n\nexport function ChatClientProvider({\n children,\n ...overrides\n}: Partial<ChatClientConfig> & { children: React.ReactNode }): React.JSX.Element {\n const merged = useMemo<ChatClientConfig>(\n () => ({ ...DEFAULT_CHAT_CLIENT_CONFIG, ...overrides }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n overrides.basePath,\n overrides.sessionStorageKey,\n overrides.nameStorageKey,\n overrides.chime,\n overrides.maxMessageLength,\n overrides.onEvent,\n ]\n );\n\n return <Ctx.Provider value={merged}>{children}</Ctx.Provider>;\n}\n\nexport function useChatConfig(): ChatClientConfig {\n return useContext(Ctx);\n}\n","'use client';\n\nimport { useAtom, useAtomValue, useSetAtom } from 'jotai';\nimport { useCallback } from 'react';\nimport {\n chatErrorAtom,\n chatMessagesAtom,\n chatNameAtom,\n chatOpenAtom,\n chatSessionIdAtom,\n chatStatusAtom,\n chatUnreadAtom,\n} from './atoms.js';\nimport type { ChatErrorBody, SendResponseBody } from '../core/types.js';\nimport { sanitizeName } from '../core/name.js';\nimport {\n ensureSessionId,\n getStoredName,\n mergeMessages,\n storeName,\n} from './client.js';\nimport { useChatConfig } from './config.js';\n\n/**\n * Read chat state and act on it. Contains no effects, so it is safe to call\n * from any number of components. The polling lifecycle lives in useChatSync.\n */\nexport function useChatActions() {\n const { basePath, sessionStorageKey, nameStorageKey, maxMessageLength, onEvent } =\n useChatConfig();\n\n const [open, originalSetOpen] = useAtom(chatOpenAtom);\n const messages = useAtomValue(chatMessagesAtom);\n const setMessages = useSetAtom(chatMessagesAtom);\n const unread = useAtomValue(chatUnreadAtom);\n const [status, setStatus] = useAtom(chatStatusAtom);\n const [error, setError] = useAtom(chatErrorAtom);\n const [name, setNameAtomValue] = useAtom(chatNameAtom);\n const setSessionId = useSetAtom(chatSessionIdAtom);\n\n const setOpen = useCallback(\n (v: boolean | ((prev: boolean) => boolean)) => {\n const next = typeof v === 'function' ? v(open) : v;\n originalSetOpen(next);\n if (next === true) onEvent?.({ type: 'chat.open' });\n else if (next === false) onEvent?.({ type: 'chat.close' });\n },\n [open, originalSetOpen, onEvent]\n );\n\n /** Returns false when the name is unusable, so the gate can stay open. */\n const setName = useCallback(\n (raw: string): boolean => {\n const clean = sanitizeName(raw);\n if (!clean) return false;\n storeName(nameStorageKey, clean);\n setNameAtomValue(clean);\n return true;\n },\n [nameStorageKey, setNameAtomValue]\n );\n\n const send = useCallback(\n async (text: string) => {\n const trimmed = text.trim();\n if (trimmed.length === 0 || trimmed.length > maxMessageLength) return;\n\n const sessionId = ensureSessionId(sessionStorageKey);\n setSessionId(sessionId);\n setStatus('sending');\n setError(null);\n\n onEvent?.({ type: 'chat.send.attempt' });\n\n try {\n const response = await fetch(`${basePath}/send`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n sessionId,\n text: trimmed,\n name: getStoredName(nameStorageKey) ?? '',\n honeypot: '',\n }),\n });\n\n if (!response.ok) {\n const body = (await response.json()) as ChatErrorBody;\n setError(body.error);\n setStatus(\n body.code === 'store_unavailable' ? 'unavailable' : 'error'\n );\n onEvent?.({ type: 'chat.send.error', code: body.code });\n return;\n }\n\n const data = (await response.json()) as SendResponseBody;\n // Appended optimistically and deduplicated by id on the next poll. The\n // cursor is deliberately NOT advanced here — doing so would skip an\n // owner reply sitting at a lower index.\n setMessages((current) => mergeMessages(current, [data.message]));\n setStatus('idle');\n onEvent?.({ type: 'chat.send.success' });\n } catch {\n setError('Chat is unavailable right now.');\n setStatus('unavailable');\n onEvent?.({ type: 'chat.send.error', code: 'network' });\n }\n },\n [\n basePath,\n maxMessageLength,\n nameStorageKey,\n onEvent,\n sessionStorageKey,\n setError,\n setMessages,\n setSessionId,\n setStatus,\n ]\n );\n\n return {\n open,\n setOpen,\n messages,\n unread,\n status,\n error,\n name,\n setName,\n send,\n canSend: status !== 'sending',\n };\n}\n","let context: AudioContext | null = null;\n\nfunction getContext(): AudioContext | null {\n if (typeof window === 'undefined') return null;\n const Ctor =\n window.AudioContext ??\n (window as unknown as { webkitAudioContext?: typeof AudioContext })\n .webkitAudioContext;\n if (!Ctor) return null;\n if (!context) context = new Ctor();\n return context;\n}\n\n/**\n * Two-note arrival chime, synthesized rather than loaded from a file: no asset\n * to host or license, no third-party request, and it works offline.\n *\n * Autoplay policy is satisfied in practice — the visitor must click, name\n * themselves and send before any reply can arrive, so a gesture has occurred.\n *\n * Every failure is swallowed. Audio must never interfere with message delivery.\n */\nexport function playChime(): void {\n try {\n const audio = getContext();\n if (!audio) return;\n if (audio.state === 'suspended') void audio.resume();\n\n const startedAt = audio.currentTime;\n const notes = [\n { frequency: 880, offset: 0 },\n { frequency: 1320, offset: 0.1 },\n ];\n\n for (const { frequency, offset } of notes) {\n const oscillator = audio.createOscillator();\n const gain = audio.createGain();\n oscillator.type = 'sine';\n oscillator.frequency.value = frequency;\n\n const start = startedAt + offset;\n const end = start + 0.09;\n // Ramp from and to zero so neither edge clicks.\n gain.gain.setValueAtTime(0, start);\n gain.gain.linearRampToValueAtTime(0.15, start + 0.01);\n gain.gain.linearRampToValueAtTime(0, end);\n\n oscillator.connect(gain).connect(audio.destination);\n oscillator.start(start);\n oscillator.stop(end + 0.02);\n }\n } catch {\n // Deliberately silent.\n }\n}\n","'use client';\n\nimport { useAtom, useAtomValue, useSetAtom } from 'jotai';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n chatCursorAtom,\n chatMessagesAtom,\n chatNameAtom,\n chatOpenAtom,\n chatSessionIdAtom,\n chatUnreadAtom,\n} from './atoms.js';\nimport type { PollResponseBody } from '../core/types.js';\nimport { playChime } from './chime.js';\nimport { getStoredName, getStoredSessionId, mergeMessages } from './client.js';\nimport { useChatConfig } from './config.js';\n\nconst FAST_INTERVAL_MS = 4000;\nconst SLOW_INTERVAL_MS = 15000;\nconst IDLE_BEFORE_SLOW_MS = 120000;\n\n/**\n * Owns the chat polling lifecycle. Mount this EXACTLY ONCE in the tree.\n *\n * Every effect here is instance-scoped, so mounting it from two components\n * would run two polling loops racing on the same cursor. Components that only\n * need to read state or send a message use useChatActions instead.\n */\nexport function useChatSync() {\n const { basePath, sessionStorageKey, nameStorageKey, chime } = useChatConfig();\n\n const open = useAtomValue(chatOpenAtom);\n const setSessionId = useSetAtom(chatSessionIdAtom);\n const setName = useSetAtom(chatNameAtom);\n const setMessages = useSetAtom(chatMessagesAtom);\n const [cursor, setCursor] = useAtom(chatCursorAtom);\n const setUnread = useSetAtom(chatUnreadAtom);\n\n const [active, setActive] = useState(true);\n const cursorRef = useRef(cursor);\n const lastMessageAtRef = useRef(0);\n const hasHydratedRef = useRef(false);\n\n useEffect(() => {\n cursorRef.current = cursor;\n }, [cursor]);\n\n // Load any existing session id and name client-side only. Reading\n // localStorage during atom initialisation would desync server and client\n // renders.\n useEffect(() => {\n const storedSession = getStoredSessionId(sessionStorageKey);\n if (storedSession) setSessionId(storedSession);\n const storedName = getStoredName(nameStorageKey);\n if (storedName) setName(storedName);\n }, [sessionStorageKey, nameStorageKey, setSessionId, setName]);\n\n /**\n * Tracks whether the visitor is plausibly looking at the page.\n *\n * visibilityState alone is not sufficient: some embeddings report 'hidden'\n * for a tab that genuinely has focus, which silently disabled polling\n * entirely and meant owner replies never arrived. Treating a focused\n * document as active fixes that while still standing down for a truly\n * backgrounded tab. focus/blur are listened to as well because hasFocus()\n * changing does not emit visibilitychange.\n */\n useEffect(() => {\n const recompute = () =>\n setActive(document.visibilityState === 'visible' || document.hasFocus());\n recompute();\n document.addEventListener('visibilitychange', recompute);\n window.addEventListener('focus', recompute);\n window.addEventListener('blur', recompute);\n return () => {\n document.removeEventListener('visibilitychange', recompute);\n window.removeEventListener('focus', recompute);\n window.removeEventListener('blur', recompute);\n };\n }, []);\n\n const poll = useCallback(async () => {\n const sessionId = getStoredSessionId(sessionStorageKey);\n if (!sessionId) return;\n\n try {\n const response = await fetch(\n `${basePath}/poll?sessionId=${encodeURIComponent(sessionId)}&since=${cursorRef.current}`,\n { cache: 'no-store' }\n );\n if (!response.ok) return;\n\n const data = (await response.json()) as PollResponseBody;\n if (data.messages.length === 0) return;\n\n lastMessageAtRef.current = Date.now();\n setMessages((current) => mergeMessages(current, data.messages));\n cursorRef.current = data.cursor;\n setCursor(data.cursor);\n\n const fromOwner = data.messages.filter(\n (message) => message.from === 'owner'\n ).length;\n\n // The first poll of a session replays the whole transcript from cursor 0,\n // so chiming here would fire for replies received on a previous visit.\n const isHydrating = !hasHydratedRef.current;\n hasHydratedRef.current = true;\n\n if (fromOwner > 0 && !isHydrating && chime) playChime();\n if (fromOwner > 0 && !open) setUnread((count) => count + fromOwner);\n } catch {\n // Silent: transient poll failures must not surface as errors.\n }\n }, [basePath, chime, open, sessionStorageKey, setCursor, setMessages, setUnread]);\n\n useEffect(() => {\n // Backgrounded tab: poll nothing at all. Becoming active again re-runs this\n // effect, which polls immediately.\n if (!active) return;\n\n if (!open) {\n // Panel closed: a single fetch, enough to light the unread badge.\n void poll();\n return;\n }\n\n let timer: ReturnType<typeof setTimeout> | null = null;\n let cancelled = false;\n\n const tick = async () => {\n if (cancelled) return;\n await poll();\n if (cancelled) return;\n\n const since = lastMessageAtRef.current || Date.now();\n const delay =\n Date.now() - since > IDLE_BEFORE_SLOW_MS\n ? SLOW_INTERVAL_MS\n : FAST_INTERVAL_MS;\n timer = setTimeout(tick, delay);\n };\n\n lastMessageAtRef.current = Date.now();\n void tick();\n\n return () => {\n cancelled = true;\n if (timer) clearTimeout(timer);\n };\n }, [open, active, poll]);\n\n // Opening the panel means the visitor has seen the replies.\n useEffect(() => {\n if (open) setUnread(0);\n }, [open, setUnread]);\n}\n","'use client';\n\nimport type React from 'react';\nimport { twMerge } from 'tailwind-merge';\nimport type { ChatMessage } from '../core/types.js';\n\nexport interface MessageBubbleClassNames {\n container?: string;\n bubble?: string;\n visitorBubble?: string;\n ownerBubble?: string;\n}\n\nexport interface MessageBubbleProps {\n message: ChatMessage;\n classNames?: MessageBubbleClassNames;\n}\n\nconst DEFAULT_CONTAINER_CLASSES = 'flex';\nconst DEFAULT_BUBBLE_CLASSES =\n 'max-w-[80%] whitespace-pre-wrap break-words rounded-lg px-3 py-2 text-sm';\nconst DEFAULT_VISITOR_BUBBLE_CLASSES =\n 'rounded-br-sm bg-purple-600 text-white';\nconst DEFAULT_OWNER_BUBBLE_CLASSES =\n 'rounded-bl-sm bg-gray-700 text-gray-100';\n\n/**\n * Renders a single chat message bubble.\n *\n * Text is always a React child, never dangerouslySetInnerHTML. Unlike FAQ\n * answers, chat messages are untrusted input, so they are not parsed at all.\n */\nexport function MessageBubble({ message, classNames = {} }: MessageBubbleProps): React.JSX.Element {\n const isVisitor = message.from === 'visitor';\n\n return (\n <div\n className={twMerge(\n DEFAULT_CONTAINER_CLASSES,\n isVisitor ? 'justify-end' : 'justify-start',\n classNames.container\n )}\n >\n <div\n className={twMerge(\n DEFAULT_BUBBLE_CLASSES,\n isVisitor\n ? twMerge(DEFAULT_VISITOR_BUBBLE_CLASSES, classNames.visitorBubble)\n : twMerge(DEFAULT_OWNER_BUBBLE_CLASSES, classNames.ownerBubble),\n classNames.bubble\n )}\n >\n {message.text}\n </div>\n </div>\n );\n}\n","'use client';\n\nimport type React from 'react';\nimport { useState } from 'react';\nimport { twMerge } from 'tailwind-merge';\nimport { MAX_NAME_LENGTH } from '../core/name.js';\nimport { useChatActions } from '../react/use-chat-actions.js';\n\nexport interface NameGateLabels {\n /** Default: \"Before we start — what's your name?\" */\n prompt?: string;\n /** Default: 'Your name' */\n placeholder?: string;\n /** Default: 'Start chat' */\n submitButton?: string;\n /** Default: 'Your name' */\n inputAriaLabel?: string;\n /** Default: 'Please enter your name.' */\n error?: string;\n}\n\nexport interface NameGateClassNames {\n container?: string;\n prompt?: string;\n inputRow?: string;\n input?: string;\n submitButton?: string;\n error?: string;\n}\n\nexport interface NameGateProps {\n labels?: NameGateLabels;\n classNames?: NameGateClassNames;\n}\n\nconst DEFAULT_CONTAINER_CLASSES =\n 'space-y-3 p-3 rounded-lg border border-gray-700 bg-gray-800 text-gray-300';\nconst DEFAULT_INPUT_CLASSES =\n 'flex-1 rounded border border-gray-600 bg-gray-900 px-2 py-1 text-base text-white focus:outline-none focus:ring-1 focus:ring-purple-500 sm:text-sm';\nconst DEFAULT_SUBMIT_CLASSES =\n 'whitespace-nowrap rounded bg-purple-600 px-2 py-1 text-sm font-semibold text-white hover:bg-purple-700';\nconst DEFAULT_ERROR_CLASSES = 'text-xs text-amber-400';\n\nexport function NameGate({ labels = {}, classNames = {} }: NameGateProps = {}): React.JSX.Element {\n const { setName } = useChatActions();\n const [value, setValue] = useState('');\n const [showError, setShowError] = useState(false);\n\n const promptText = labels.prompt ?? \"Before we start — what’s your name?\";\n const placeholder = labels.placeholder ?? 'Your name';\n const submitLabel = labels.submitButton ?? 'Start chat';\n const inputAriaLabel = labels.inputAriaLabel ?? 'Your name';\n const errorText = labels.error ?? 'Please enter your name.';\n\n const submit = () => {\n if (!setName(value)) {\n setShowError(true);\n return;\n }\n setShowError(false);\n };\n\n return (\n <div className={twMerge(DEFAULT_CONTAINER_CLASSES, classNames.container)}>\n <p className={twMerge('text-sm', classNames.prompt)}>{promptText}</p>\n <div className={twMerge('flex gap-2', classNames.inputRow)}>\n <input\n type=\"text\"\n value={value}\n autoFocus\n maxLength={MAX_NAME_LENGTH}\n onChange={(e) => {\n setValue(e.target.value);\n if (showError) setShowError(false);\n }}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n submit();\n }\n }}\n placeholder={placeholder}\n aria-label={inputAriaLabel}\n // text-base (16px) on mobile prevents iOS Safari's focus auto-zoom.\n className={twMerge(DEFAULT_INPUT_CLASSES, classNames.input)}\n />\n <button\n type=\"button\"\n onClick={submit}\n className={twMerge(DEFAULT_SUBMIT_CLASSES, classNames.submitButton)}\n >\n {submitLabel}\n </button>\n </div>\n {showError && (\n <p className={twMerge(DEFAULT_ERROR_CLASSES, classNames.error)}>{errorText}</p>\n )}\n </div>\n );\n}\n","'use client';\n\nimport type React from 'react';\nimport { Fragment, useEffect, useRef, useState } from 'react';\nimport { Dialog, Transition } from '@headlessui/react';\nimport { Send, X } from 'lucide-react';\nimport { twMerge } from 'tailwind-merge';\nimport { useChatActions } from '../react/use-chat-actions.js';\nimport { useChatConfig } from '../react/config.js';\nimport type { ChatMessage } from '../core/types.js';\nimport { MessageBubble } from './MessageBubble.js';\nimport { NameGate } from './NameGate.js';\n\nexport interface ChatDialogLabels {\n /** Title shown in the header. Default: 'Chat'. */\n title?: string;\n /** Suffix appended to title when a name is set. Default: (name) => ` · ${name}`. */\n titleWithName?: (name: string) => string;\n /** Greeting shown once a name is accepted. Default: (name) => `Hi ${name} — type your message here… I usually reply within a few minutes.`. */\n greeting?: (name: string) => string;\n /** Placeholder inside the textarea. Default: (nameOk) => nameOk ? 'Type a message…' : 'Enter your name first'. */\n composerPlaceholder?: (nameOk: boolean) => string;\n /** ARIA label on the message textarea. Default: 'Message'. */\n messageAriaLabel?: string;\n /** ARIA label on the send button. Default: 'Send message'. */\n sendAriaLabel?: string;\n /** ARIA label on the close (X) button. Default: 'Close chat'. */\n closeAriaLabel?: string;\n /**\n * Rendered on 'unavailable' status. Default: a paragraph telling the visitor\n * chat is unreachable; consumers who want an email fallback CTA pass a React node.\n */\n unavailableFallback?: React.ReactNode;\n}\n\nexport interface ChatDialogClassNames {\n overlay?: string;\n panel?: string;\n header?: string;\n title?: string;\n closeButton?: string;\n body?: string;\n greeting?: string;\n unavailable?: string;\n errorText?: string;\n composer?: string;\n /**\n * Overrides the honeypot input's className. Default: 'hidden'.\n * Consumers who override MUST keep it visually hidden; this input must\n * never be visible or reachable by real users.\n */\n honeypot?: string;\n textarea?: string;\n sendButton?: string;\n}\n\nexport interface ChatDialogProps {\n labels?: ChatDialogLabels;\n classNames?: ChatDialogClassNames;\n /** Custom message renderer. Default: <MessageBubble />. */\n renderMessage?: (message: ChatMessage) => React.ReactNode;\n /** Custom NameGate. Default: <NameGate />. Consumers can pass one with their own labels/classNames. */\n nameGate?: React.ReactNode;\n}\n\nconst DEFAULT_OVERLAY_CLASSES = 'fixed inset-0 bg-black/50';\nconst DEFAULT_PANEL_CLASSES =\n 'flex w-full max-w-md flex-col overflow-hidden rounded-lg border border-gray-700 bg-gray-900 text-left shadow-xl transition-all';\nconst DEFAULT_HEADER_CLASSES =\n 'flex items-start justify-between px-4 py-3 bg-purple-600';\nconst DEFAULT_TITLE_CLASSES = 'min-w-0 truncate text-base font-semibold text-white';\nconst DEFAULT_CLOSE_BUTTON_CLASSES =\n 'shrink-0 rounded-md p-1 text-gray-300 hover:text-white focus:outline-none focus:ring-2 focus:ring-purple-500';\nconst DEFAULT_BODY_CLASSES = 'flex-1 space-y-3 overflow-y-auto p-4';\nconst DEFAULT_GREETING_CLASSES = 'text-xs font-bold text-gray-400';\nconst DEFAULT_UNAVAILABLE_CLASSES = 'text-xs text-amber-400';\nconst DEFAULT_ERROR_TEXT_CLASSES = 'text-xs text-amber-400';\nconst DEFAULT_COMPOSER_CLASSES =\n 'flex items-end gap-2 border-t border-gray-700 p-3';\nconst DEFAULT_TEXTAREA_CLASSES =\n 'flex-1 resize-none rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-base text-white focus:outline-none focus:ring-1 focus:ring-purple-500 sm:text-sm';\nconst DEFAULT_SEND_BUTTON_CLASSES =\n 'rounded-full bg-purple-600 p-2 text-white hover:bg-purple-700 disabled:opacity-40';\nconst DEFAULT_HONEYPOT_CLASSES = 'hidden';\n\nexport function ChatDialog({\n labels = {},\n classNames = {},\n renderMessage,\n nameGate,\n}: ChatDialogProps = {}): React.JSX.Element {\n const { open, setOpen, messages, status, error, send, canSend, name } =\n useChatActions();\n const { maxMessageLength } = useChatConfig();\n const [draft, setDraft] = useState('');\n const bottomRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n bottomRef.current?.scrollIntoView({ behavior: 'smooth' });\n }, [messages.length]);\n\n const titleText = labels.title ?? 'Chat';\n const titleWithName = labels.titleWithName ?? ((n: string) => ` · ${n}`);\n const greeting =\n labels.greeting ??\n ((n: string) =>\n `Hi ${n} — type your message here… I usually reply within a few minutes.`);\n const composerPlaceholder =\n labels.composerPlaceholder ??\n ((nameOk: boolean) => (nameOk ? 'Type a message…' : 'Enter your name first'));\n const messageAriaLabel = labels.messageAriaLabel ?? 'Message';\n const sendAriaLabel = labels.sendAriaLabel ?? 'Send message';\n const closeAriaLabel = labels.closeAriaLabel ?? 'Close chat';\n const unavailableFallback =\n labels.unavailableFallback ?? (\n <p className={twMerge(DEFAULT_UNAVAILABLE_CLASSES, classNames.unavailable)}>\n Chat is unreachable right now — please try again later.\n </p>\n );\n\n const submit = async () => {\n // Guarded so Enter cannot bypass the name gate.\n if (!name || !canSend || draft.trim().length === 0) return;\n const text = draft;\n setDraft('');\n await send(text);\n };\n\n return (\n <Transition show={open} as={Fragment}>\n <Dialog as=\"div\" className=\"relative z-50\" onClose={() => setOpen(false)}>\n <Transition.Child\n as={Fragment}\n enter=\"ease-out duration-200\"\n enterFrom=\"opacity-0\"\n enterTo=\"opacity-100\"\n leave=\"ease-in duration-150\"\n leaveFrom=\"opacity-100\"\n leaveTo=\"opacity-0\"\n >\n <div className={twMerge(DEFAULT_OVERLAY_CLASSES, classNames.overlay)} />\n </Transition.Child>\n\n <div className=\"fixed inset-0 overflow-y-auto\">\n <div className=\"flex min-h-full items-center justify-center p-4\">\n <Transition.Child\n as={Fragment}\n enter=\"ease-out duration-200\"\n enterFrom=\"opacity-0 scale-95\"\n enterTo=\"opacity-100 scale-100\"\n leave=\"ease-in duration-150\"\n leaveFrom=\"opacity-100 scale-100\"\n leaveTo=\"opacity-0 scale-95\"\n >\n <Dialog.Panel\n className={twMerge(DEFAULT_PANEL_CLASSES, classNames.panel)}\n >\n <div className={twMerge(DEFAULT_HEADER_CLASSES, classNames.header)}>\n {/* The visitor's name is appended once set: a persistent\n signal that the chat has actually started. truncate keeps\n a 60-character name from pushing out the close button. */}\n <Dialog.Title\n className={twMerge(DEFAULT_TITLE_CLASSES, classNames.title)}\n >\n {titleText}\n {name ? titleWithName(name) : ''}\n </Dialog.Title>\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n aria-label={closeAriaLabel}\n className={twMerge(\n DEFAULT_CLOSE_BUTTON_CLASSES,\n classNames.closeButton\n )}\n >\n <X size={18} />\n </button>\n </div>\n\n <div className={twMerge(DEFAULT_BODY_CLASSES, classNames.body)}>\n {/* Greets the visitor by name the moment it is accepted.\n This is the immediate confirmation that the name went\n through — the previous silent unlock read as a failure.\n Rendered as a React child, so the name is never parsed. */}\n {name && (\n <p\n className={twMerge(\n DEFAULT_GREETING_CLASSES,\n classNames.greeting\n )}\n >\n {greeting(name)}\n </p>\n )}\n\n {messages.map((message) =>\n renderMessage ? (\n renderMessage(message)\n ) : (\n <MessageBubble key={message.id} message={message} />\n )\n )}\n\n {status === 'unavailable' && unavailableFallback}\n {status === 'error' && error && (\n <p\n className={twMerge(\n DEFAULT_ERROR_TEXT_CLASSES,\n classNames.errorText\n )}\n >\n {error}\n </p>\n )}\n\n {!name && (nameGate !== undefined ? nameGate : <NameGate />)}\n <div ref={bottomRef} />\n </div>\n\n <div\n className={twMerge(DEFAULT_COMPOSER_CLASSES, classNames.composer)}\n >\n {/* Honeypot: hidden from humans, tempting to naive bots. */}\n <input\n type=\"text\"\n name=\"website\"\n tabIndex={-1}\n autoComplete=\"off\"\n aria-hidden=\"true\"\n className={twMerge(DEFAULT_HONEYPOT_CLASSES, classNames.honeypot)}\n />\n <textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n void submit();\n }\n }}\n rows={2}\n maxLength={maxMessageLength}\n disabled={!name}\n placeholder={composerPlaceholder(!!name)}\n aria-label={messageAriaLabel}\n className={twMerge(\n // text-base (16px) on mobile: iOS Safari auto-zooms the\n // page when a focused field is smaller than 16px.\n // sm:text-sm restores the desktop size.\n DEFAULT_TEXTAREA_CLASSES,\n classNames.textarea\n )}\n />\n <button\n type=\"button\"\n onClick={() => void submit()}\n disabled={!name || !canSend || draft.trim().length === 0}\n aria-label={sendAriaLabel}\n className={twMerge(\n DEFAULT_SEND_BUTTON_CLASSES,\n classNames.sendButton\n )}\n >\n <Send size={16} />\n </button>\n </div>\n </Dialog.Panel>\n </Transition.Child>\n </div>\n </div>\n </Dialog>\n </Transition>\n );\n}\n","'use client';\n\nimport type React from 'react';\nimport { twMerge } from 'tailwind-merge';\nimport { useChatActions } from '../react/use-chat-actions.js';\nimport { useChatSync } from '../react/use-chat-sync.js';\nimport { ChatDialog } from './ChatDialog.js';\n\nexport interface ChatLauncherLabels {\n /** Default: 'Contact me' */\n button?: string;\n /** Default: `${n} unread ${n === 1 ? 'reply' : 'replies'}` */\n unreadAriaLabel?: (n: number) => string;\n}\n\nexport interface ChatLauncherClassNames {\n button?: string;\n unreadBadge?: string;\n}\n\nexport type ChatLauncherEvent = { type: 'ui.launcher.open' };\n\nexport interface ChatLauncherProps {\n labels?: ChatLauncherLabels;\n classNames?: ChatLauncherClassNames;\n /** Icon rendered inside the button. Default: none — consumers pass their own (lucide-react's <MessageCircle />, an SVG, an emoji, etc.). */\n icon?: React.ReactNode;\n /** Custom analytics/side-effects on launcher-open. */\n onEvent?: (e: ChatLauncherEvent) => void;\n /**\n * Optional child dialog. Default: <ChatDialog />. Passing null skips the\n * dialog mount (useful if the consumer renders the dialog elsewhere).\n */\n children?: React.ReactNode;\n}\n\nconst DEFAULT_BUTTON_CLASSES =\n 'relative flex items-center justify-center gap-2 rounded-lg bg-purple-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 print:hidden';\nconst DEFAULT_BADGE_CLASSES =\n 'absolute right-2 top-1/2 h-2.5 w-2.5 -translate-y-1/2 rounded-full bg-white';\n\nexport function ChatLauncher({\n labels = {},\n classNames = {},\n icon,\n onEvent,\n children,\n}: ChatLauncherProps = {}): React.JSX.Element {\n const { setOpen, unread } = useChatActions();\n\n // The single mount point for the polling lifecycle. Do not call useChatSync\n // anywhere else — see the note in useChatSync.ts.\n useChatSync();\n\n const buttonText = labels.button ?? 'Contact me';\n const unreadA11y =\n labels.unreadAriaLabel ??\n ((n: number) => `${n} unread ${n === 1 ? 'reply' : 'replies'}`);\n\n const handleOpen = () => {\n setOpen(true);\n onEvent?.({ type: 'ui.launcher.open' });\n };\n\n return (\n <>\n <button\n type=\"button\"\n onClick={handleOpen}\n className={twMerge(DEFAULT_BUTTON_CLASSES, classNames.button)}\n >\n {icon}\n {buttonText}\n {unread > 0 && (\n <span\n aria-label={unreadA11y(unread)}\n className={twMerge(DEFAULT_BADGE_CLASSES, classNames.unreadBadge)}\n />\n )}\n </button>\n {children === undefined ? <ChatDialog /> : children}\n </>\n );\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rizvanua/contact-chat",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Telegram-backed contact-chat widget: framework-agnostic server core, headless React hooks, and unstyled UI.",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"require": {
|
|
22
|
+
"types": "./dist/index.d.cts",
|
|
23
|
+
"default": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"./server": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./dist/server/index.d.ts",
|
|
29
|
+
"default": "./dist/server/index.js"
|
|
30
|
+
},
|
|
31
|
+
"require": {
|
|
32
|
+
"types": "./dist/server/index.d.cts",
|
|
33
|
+
"default": "./dist/server/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"./server/nextjs": {
|
|
37
|
+
"import": {
|
|
38
|
+
"types": "./dist/server/nextjs.d.ts",
|
|
39
|
+
"default": "./dist/server/nextjs.js"
|
|
40
|
+
},
|
|
41
|
+
"require": {
|
|
42
|
+
"types": "./dist/server/nextjs.d.cts",
|
|
43
|
+
"default": "./dist/server/nextjs.cjs"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"./react": {
|
|
47
|
+
"import": {
|
|
48
|
+
"types": "./dist/react/index.d.ts",
|
|
49
|
+
"default": "./dist/react/index.js"
|
|
50
|
+
},
|
|
51
|
+
"require": {
|
|
52
|
+
"types": "./dist/react/index.d.cts",
|
|
53
|
+
"default": "./dist/react/index.cjs"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"./ui": {
|
|
57
|
+
"import": {
|
|
58
|
+
"types": "./dist/ui/index.d.ts",
|
|
59
|
+
"default": "./dist/ui/index.js"
|
|
60
|
+
},
|
|
61
|
+
"require": {
|
|
62
|
+
"types": "./dist/ui/index.d.cts",
|
|
63
|
+
"default": "./dist/ui/index.cjs"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"./stores": {
|
|
67
|
+
"import": {
|
|
68
|
+
"types": "./dist/stores/index.d.ts",
|
|
69
|
+
"default": "./dist/stores/index.js"
|
|
70
|
+
},
|
|
71
|
+
"require": {
|
|
72
|
+
"types": "./dist/stores/index.d.cts",
|
|
73
|
+
"default": "./dist/stores/index.cjs"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"./transports": {
|
|
77
|
+
"import": {
|
|
78
|
+
"types": "./dist/transports/index.d.ts",
|
|
79
|
+
"default": "./dist/transports/index.js"
|
|
80
|
+
},
|
|
81
|
+
"require": {
|
|
82
|
+
"types": "./dist/transports/index.d.cts",
|
|
83
|
+
"default": "./dist/transports/index.cjs"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"peerDependencies": {
|
|
88
|
+
"@headlessui/react": "^2.0.0",
|
|
89
|
+
"@upstash/redis": "^1.28.0",
|
|
90
|
+
"jotai": "^2.0.0",
|
|
91
|
+
"lucide-react": "^0.400.0",
|
|
92
|
+
"react": "^18.0.0",
|
|
93
|
+
"react-dom": "^18.0.0",
|
|
94
|
+
"tailwind-merge": "^2.0.0"
|
|
95
|
+
},
|
|
96
|
+
"peerDependenciesMeta": {
|
|
97
|
+
"react": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"react-dom": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"jotai": {
|
|
104
|
+
"optional": true
|
|
105
|
+
},
|
|
106
|
+
"@upstash/redis": {
|
|
107
|
+
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"@headlessui/react": {
|
|
110
|
+
"optional": true
|
|
111
|
+
},
|
|
112
|
+
"lucide-react": {
|
|
113
|
+
"optional": true
|
|
114
|
+
},
|
|
115
|
+
"tailwind-merge": {
|
|
116
|
+
"optional": true
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"devDependencies": {
|
|
120
|
+
"@changesets/cli": "^2.27.0",
|
|
121
|
+
"@headlessui/react": "^2",
|
|
122
|
+
"@testing-library/react": "^16.3.2",
|
|
123
|
+
"@types/node": "^20.19.43",
|
|
124
|
+
"@types/react": "^18.3.31",
|
|
125
|
+
"@types/react-dom": "^18.3.7",
|
|
126
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
127
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
128
|
+
"@upstash/redis": "^1.28",
|
|
129
|
+
"eslint": "^9.0.0",
|
|
130
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
131
|
+
"jsdom": "^24.1.3",
|
|
132
|
+
"lucide-react": "^0.400",
|
|
133
|
+
"react": "^18",
|
|
134
|
+
"react-dom": "^18",
|
|
135
|
+
"tailwind-merge": "^2",
|
|
136
|
+
"tsup": "^8.0.0",
|
|
137
|
+
"typescript": "^5.5.0",
|
|
138
|
+
"vitest": "^2.0.0"
|
|
139
|
+
},
|
|
140
|
+
"scripts": {
|
|
141
|
+
"build": "tsup",
|
|
142
|
+
"lint": "eslint src",
|
|
143
|
+
"test": "vitest run",
|
|
144
|
+
"typecheck": "tsc --noEmit"
|
|
145
|
+
}
|
|
146
|
+
}
|