let ofetch = require("ofetch"); //#region src/ofetch-client.ts /** * How stale (in seconds) `userHashTimestamp` is allowed to be before we call the host's * `refreshIdentity` callback. 3 minutes against the default 10-minute backend TTL leaves * enough headroom that clock skew between the host server and the backend, a slow refresh * endpoint, or a just-woke-from-suspend tab don't cause the signature to land stale. If * the signature still expires mid-flight, we auto-retry once after forcing a refresh — see * `widgetFetch` below. */ const STALENESS_THRESHOLD_SECONDS = 180; /** * Minimum gap between identical `console.warn` lines emitted when `refreshIdentity()` throws. * A broken host endpoint shouldn't spam the console every 200ms on a queued burst. */ const REFRESH_WARN_DEDUPE_MS = 6e4; /** * Proper Error subclass so devtools stack traces are useful and `instanceof Error` checks work. * Carries the normalized `WidgetError` surface (`code`, `message`, `status`, `errors`) as own * properties — callers that previously caught `WidgetError` keep working unchanged because the * shape is a strict superset. */ var WidgetFetchError = class extends Error { code; status; errors; constructor(init) { super(init.message); this.name = "WidgetFetchError"; this.code = init.code; this.status = init.status; this.errors = init.errors; } }; function parseWidgetErrorBody(body, status, statusText) { const fieldErrors = (Array.isArray(body?.errors) ? body.errors : []).map((e) => { const pointer = typeof e.source === "object" && e.source !== null ? e.source.pointer : void 0; const field = pointer?.startsWith("/data/attributes/") ? pointer.slice(17) : pointer?.split("/").filter(Boolean).pop(); return { code: e.code ?? "UNKNOWN", detail: e.detail ?? "", pointer, field }; }); return { code: body?.responseCode ?? `HTTP_${status}`, message: body?.responseMessage ?? statusText, status, errors: fieldErrors.length > 0 ? fieldErrors : void 0 }; } let apiKey = ""; let baseUrl = ""; let oidcTokenProvider = null; let currentCustomer = null; /** * Single in-flight refresh promise so parallel `onRequest` invocations during a refresh don't * stampede the host endpoint. Cleared once the promise settles. */ let inFlightRefresh = null; let lastRefreshWarnAt = 0; function configureWidgetClient(url, key) { baseUrl = url.replace(/\/$/, ""); apiKey = key; } function setOidcTokenProvider(provider) { oidcTokenProvider = provider; } /** * Records the customer the host app has identified. When the customer has a `userHash`, subsequent * requests automatically carry the `X-Widget-User-Signature` and `X-Widget-User-Timestamp` headers * so the backend can verify the host-asserted identity. A new customer resets the refresh * dedupe state so a stale in-flight refresh can't clobber a freshly-set signature. */ function setWidgetCustomer(customer) { currentCustomer = customer; inFlightRefresh = null; } function isSignatureStale() { const ts = currentCustomer?.userHashTimestamp; if (typeof ts !== "number") return true; return Math.floor(Date.now() / 1e3) - ts > STALENESS_THRESHOLD_SECONDS; } function isValidRefreshResult(result) { if (!result || typeof result !== "object") return false; const candidate = result; return typeof candidate.userHash === "string" && candidate.userHash.length > 0 && typeof candidate.userHashTimestamp === "number" && Number.isFinite(candidate.userHashTimestamp); } /** * Call the host's `refreshIdentity` callback and patch `currentCustomer` in place. Swallows * errors (logged once per minute) so a broken host endpoint can't stall request dispatch — * the stale signature falls through and the backend rejects it cleanly with 401 * `INVALID_SIGNATURE`, which the global MutationCache/QueryCache handler surfaces as a toast. */ async function refreshSignedIdentity() { const customer = currentCustomer; const callback = customer?.refreshIdentity; if (!customer || !callback) return; if (inFlightRefresh) { await inFlightRefresh; return; } inFlightRefresh = (async () => { try { const result = await callback(); if (!isValidRefreshResult(result)) { throwRefreshWarning("refreshIdentity() returned an invalid SignedIdentity (expected { userHash: string, userHashTimestamp: number })"); return; } const ageSeconds = Math.floor(Date.now() / 1e3) - result.userHashTimestamp; if (ageSeconds > STALENESS_THRESHOLD_SECONDS) { throwRefreshWarning(`refreshIdentity() returned a pair already ${ageSeconds}s old (threshold ${STALENESS_THRESHOLD_SECONDS}s) — check host-app clock skew and cache headers`); return; } customer.userHash = result.userHash; customer.userHashTimestamp = result.userHashTimestamp; } catch (err) { throwRefreshWarning(err); } finally { inFlightRefresh = null; } })(); await inFlightRefresh; } /** * Force a refresh on the next outbound request by zero-ing the cached timestamp and clearing * any in-flight refresh. Used by the 401-retry path so a re-dispatch doesn't silently reuse * the same stale signature that just got rejected. */ function forceNextRequestToRefresh() { if (currentCustomer) currentCustomer.userHashTimestamp = 0; inFlightRefresh = null; } function throwRefreshWarning(reason) { const now = Date.now(); if (now - lastRefreshWarnAt < REFRESH_WARN_DEDUPE_MS) return; lastRefreshWarnAt = now; console.warn("[reqdesk-widget] refreshIdentity() failed; falling back to stale signature:", reason); } async function applySignedIdentityHeaders(headers) { if (currentCustomer?.refreshIdentity && isSignatureStale()) await refreshSignedIdentity(); if (!currentCustomer?.userHash) return; headers.set("X-Widget-User-Signature", currentCustomer.userHash); if (typeof currentCustomer.userHashTimestamp === "number") headers.set("X-Widget-User-Timestamp", String(currentCustomer.userHashTimestamp)); if (currentCustomer.email) headers.set("X-Widget-User-Email", currentCustomer.email); } const innerWidgetFetch = ofetch.ofetch.create({ timeout: 15e3, retry: false, async onRequest({ options }) { options.baseURL = `${baseUrl}/api/v1`; const headers = new Headers(options.headers); headers.set("X-API-Key", apiKey); if (oidcTokenProvider) try { const tokens = await oidcTokenProvider(); if (tokens?.accessToken) headers.set("Authorization", `Bearer ${tokens.accessToken}`); else console.warn("[reqdesk-widget] OIDC token provider returned empty accessToken", tokens); } catch (err) { console.warn("[reqdesk-widget] Failed to get OIDC access token:", err); } await applySignedIdentityHeaders(headers); options.headers = headers; }, onResponseError({ response }) { const body = response._data; throw new WidgetFetchError(parseWidgetErrorBody(body, response.status, response.statusText)); } }); /** * Public fetch wrapper. Auto-retries exactly once on a `401 INVALID_SIGNATURE` response by * forcing a fresh `refreshIdentity()` call and re-dispatching the same request. This closes * the race window where: * - the widget's staleness check (`STALENESS_THRESHOLD_SECONDS`) thought the sig was fresh, * - but by the time the request landed on the backend the server clock had moved past the * project's `SignedIdentityTtlMinutes` window (clock skew / tab throttling / just-woke-up * from suspend / a slow refresh endpoint returning an already-aged pair). * * The retry only fires when a `refreshIdentity` callback is configured. Hosts that don't * provide one see the same one-shot failure behaviour as before — no silent endless retries. * Any non-`INVALID_SIGNATURE` error propagates on the first attempt. */ const widgetFetch = (async (request, options) => { try { return await innerWidgetFetch(request, options); } catch (err) { if (!(err instanceof WidgetFetchError && err.status === 401 && err.code === "INVALID_SIGNATURE" && !!currentCustomer?.refreshIdentity && !options?.__signatureRetried)) throw err; forceNextRequestToRefresh(); await refreshSignedIdentity(); return await innerWidgetFetch(request, { ...options ?? {}, __signatureRetried: true }); } }); async function uploadWithProgress(path, file, onProgress) { if (currentCustomer?.refreshIdentity && isSignatureStale()) await refreshSignedIdentity(); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open("POST", `${baseUrl}${path}`); xhr.setRequestHeader("X-API-Key", apiKey); if (currentCustomer?.userHash) { xhr.setRequestHeader("X-Widget-User-Signature", currentCustomer.userHash); if (typeof currentCustomer.userHashTimestamp === "number") xhr.setRequestHeader("X-Widget-User-Timestamp", String(currentCustomer.userHashTimestamp)); if (currentCustomer.email) xhr.setRequestHeader("X-Widget-User-Email", currentCustomer.email); } if (oidcTokenProvider) oidcTokenProvider().then((tokens) => { xhr.setRequestHeader("Authorization", `Bearer ${tokens.accessToken}`); sendXhr(); }).catch(() => sendXhr()); else sendXhr(); function sendXhr() { if (onProgress) xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) onProgress(Math.round(e.loaded / e.total * 100)); }); xhr.addEventListener("load", () => { if (xhr.status >= 200 && xhr.status < 300) try { resolve(JSON.parse(xhr.responseText)); } catch { resolve({}); } else { let body; try { body = JSON.parse(xhr.responseText); } catch {} reject(new WidgetFetchError(parseWidgetErrorBody(body, xhr.status, xhr.statusText))); } }); xhr.addEventListener("error", () => { reject(new WidgetFetchError({ code: "NETWORK_ERROR", message: "Network error during upload" })); }); const form = new FormData(); form.append("file", file); xhr.send(form); } }); } //#endregion //#region src/api-client.ts function generateIdempotencyKey() { return crypto.randomUUID(); } async function submitTicket(projectId, data) { const res = await widgetFetch(`/projects/${projectId}/tickets`, { method: "POST", body: { title: data.title, description: data.description, priority: data.priority, categoryId: data.categoryId, email: data.email, clientMetadata: data.clientMetadata, tagIds: data.tagIds && data.tagIds.length > 0 ? data.tagIds : void 0 }, headers: { "Idempotency-Key": generateIdempotencyKey() } }); return { id: res.data.id, ticketNumber: res.data.attributes.ticketNumber, trackingToken: res.meta?.trackingToken, status: res.data.attributes.status }; } async function trackTicket(token) { const res = await widgetFetch(`/tickets/track?token=${encodeURIComponent(token)}`); const included = res.included ?? []; return { id: res.data.id, ticketNumber: res.data.attributes.ticketNumber, title: res.data.attributes.title, status: res.data.attributes.status, priority: res.data.attributes.priority, createdAt: res.data.attributes.createdAt, replies: included.filter((i) => i.type === "public-reply").map((i) => ({ id: i.id, body: i.attributes.body, authorName: i.attributes.authorName, isStaff: i.attributes.isStaff, createdAt: i.attributes.createdAt })) }; } async function submitTrackingReply(token, body) { await widgetFetch("/tickets/track/reply", { method: "POST", body: { trackingToken: token, body } }); } async function getCategories(projectId, parentId) { const params = new URLSearchParams({ "filter[active]": "true" }); if (parentId === null || parentId === void 0) params.set("filter[parentId]", "root"); else params.set("filter[parentId]", parentId); const res = await widgetFetch(`/projects/${projectId}/categories?${params}`); return (Array.isArray(res.data) ? res.data : []).map((c) => ({ id: c.id, name: c.attributes.name, parentId: c.attributes.parentId, hasChildren: c.attributes.hasChildren })); } async function createCategory(projectId, name, parentId) { const res = await widgetFetch(`/projects/${projectId}/categories/widget`, { method: "POST", body: { name, parentId: parentId ?? void 0 } }); return { id: res.data.id, name: res.data.attributes.name, parentId: res.data.attributes.parentId, hasChildren: res.data.attributes.hasChildren }; } async function listProjectTags(projectId) { const res = await widgetFetch(`/projects/${projectId}/tags?sort=sort_order`); return (Array.isArray(res.data) ? res.data : []).map((t) => ({ id: t.id, name: t.attributes.name, color: t.attributes.color || "#6b7280" })); } async function createTag(projectId, name, color) { const res = await widgetFetch(`/projects/${projectId}/tags/widget`, { method: "POST", body: { name, color } }); return { id: res.data.id, name: res.data.attributes.name, color: res.data.attributes.color || "#6b7280" }; } async function getWidgetConfig(projectId) { const res = await widgetFetch(`/projects/${projectId}/widget-config`); const attrs = res.data.attributes; return { id: res.data.id, ...attrs, allowedAuthModes: Array.isArray(attrs.allowedAuthModes) && attrs.allowedAuthModes.length > 0 ? attrs.allowedAuthModes : ["sso", "email"], signedIdentityTtlMinutes: typeof attrs.signedIdentityTtlMinutes === "number" && attrs.signedIdentityTtlMinutes > 0 ? attrs.signedIdentityTtlMinutes : 10 }; } async function listProjects(workspaceId) { const params = new URLSearchParams({ "page[size]": "50" }); if (workspaceId) params.set("filter[workspaceId]", workspaceId); const res = await widgetFetch(`/projects?${params}`); return (Array.isArray(res.data) ? res.data : []).map((p) => ({ id: p.id, name: p.attributes.name, slug: p.attributes.slug })); } async function closeTicket(ticketId) { await widgetFetch(`/tickets/${ticketId}/status`, { method: "PUT", body: { status: "resolved" } }); } function uploadAttachment(ticketId, file, onProgress) { return uploadWithProgress(`/api/v1/tickets/${ticketId}/attachments`, file, onProgress); } function uploadReplyAttachment(ticketId, replyId, file, onProgress) { return uploadWithProgress(`/api/v1/tickets/${ticketId}/replies/${replyId}/attachments`, file, onProgress); } async function getTicketDetail(ticketId, includeMedia = true) { const res = await widgetFetch(`/tickets/${ticketId}${includeMedia ? "?include=media" : ""}`); const included = res.included ?? []; const allAttachments = included.filter((i) => i.type === "attachment").map((i) => ({ raw: i, meta: { id: i.id, fileName: i.attributes.fileName, contentType: i.attributes.contentType, fileSize: i.attributes.fileSize, downloadUrl: i.attributes.downloadUrl, downloadUrlExpiresAt: i.attributes.downloadUrlExpiresAt } })); const attachmentsByReplyId = /* @__PURE__ */ new Map(); const ticketAttachments = []; for (const entry of allAttachments) { const attrReplyId = entry.raw.attributes?.replyId; const replyRel = entry.raw.relationships?.reply?.data; const replyId = attrReplyId ?? (replyRel && replyRel.id && (replyRel.type === "reply" || replyRel.type === "ticket-reply") ? replyRel.id : void 0); if (replyId) { const list = attachmentsByReplyId.get(replyId) ?? []; list.push(entry.meta); attachmentsByReplyId.set(replyId, list); } else ticketAttachments.push(entry.meta); } const replies = included.filter((i) => i.type === "reply" || i.type === "ticket-reply").map((i) => { const attachments = attachmentsByReplyId.get(i.id); return { id: i.id, body: i.attributes.body, authorName: i.attributes.authorName ?? "Anonymous", isStaff: i.attributes.isStaff ?? false, createdAt: i.attributes.timestamps?.created ?? "", attachments: attachments && attachments.length > 0 ? attachments : void 0 }; }); const tags = included.filter((i) => i.type === "tag").map((i) => ({ id: i.id, name: i.attributes.name, color: i.attributes.color || "#6b7280" })); return { id: res.data.id, ticketNumber: res.data.attributes.ticketNumber, title: res.data.attributes.title, description: res.data.attributes.description, status: res.data.attributes.status, priority: res.data.attributes.priority, createdAt: res.data.attributes.timestamps?.created ?? "", replies, attachments: ticketAttachments, tags }; } async function submitReply(ticketId, body) { return { id: (await widgetFetch(`/tickets/${ticketId}/replies`, { method: "POST", body: { body, isInternal: false } })).data.id }; } async function resolveWidgetUser(projectId, email) { try { return await widgetFetch(`/projects/${projectId}/tickets/widget-users?email=${encodeURIComponent(email)}`); } catch { return null; } } async function listMyTickets(projectId, userId, page = 1, pageSize = 20, showAll = false) { const params = new URLSearchParams({ "sort": "-created_at", "page[number]": String(page), "page[size]": String(pageSize) }); if (!showAll && userId) params.set("filter[createdBy]", userId); const res = await widgetFetch(`/projects/${projectId}/tickets?${params}`); return (Array.isArray(res.data) ? res.data : []).map((t) => ({ id: t.id, ticketNumber: t.attributes.ticketNumber, title: t.attributes.title, status: t.attributes.status, priority: t.attributes.priority, createdAt: t.attributes.timestamps?.created ?? "" })); } //#endregion //#region src/i18n/en.ts const en = { "widget.title": "Support", "widget.newTicket": "New Ticket", "widget.trackTicket": "Track Ticket", "form.title": "Title", "form.titlePlaceholder": "Brief summary of your issue", "form.description": "Description", "form.descriptionPlaceholder": "Describe your issue in detail...", "form.email": "Email", "form.emailPlaceholder": "your@email.com", "form.priority": "Priority", "form.priorityLow": "Low", "form.priorityMedium": "Medium", "form.priorityHigh": "High", "form.priorityCritical": "Critical", "form.priorityUrgent": "Critical", "form.category": "Category", "form.categoryNone": "Select a category", "form.submit": "Submit Ticket", "form.submitting": "Submitting...", "form.cancel": "Cancel", "form.attachment": "Attach files", "success.title": "Ticket Submitted!", "success.ticketNumber": "Ticket #", "success.trackingToken": "Your tracking token:", "success.trackingHint": "Save this token to track your ticket status.", "success.copyToken": "Copy Token", "success.copied": "Copied!", "success.close": "Close", "success.trackNow": "Track Ticket", "tracker.title": "Track Your Ticket", "tracker.tokenPlaceholder": "Enter your tracking token (trk_...)", "tracker.submit": "Track", "tracker.tracking": "Tracking...", "tracker.status": "Status", "tracker.priority": "Priority", "tracker.created": "Created", "tracker.replies": "Replies", "tracker.noReplies": "No replies yet.", "tracker.replyPlaceholder": "Write a reply...", "tracker.sendReply": "Send Reply", "tracker.sending": "Sending...", "tracker.back": "Back", "tracker.staff": "Staff", "error.generic": "Something went wrong. Please try again.", "error.validation": "Please fix the highlighted fields and try again.", "error.network": "Network error. Check your connection.", "error.server": "The server is having trouble. Please try again in a moment.", "error.forbidden": "You don’t have permission to perform this action.", "error.unauthenticated": "Please sign in to continue.", "error.notFound": "We couldn’t find what you were looking for.", "notification.dismiss": "Dismiss", "error.tokenInvalid": "Invalid or expired tracking token.", "error.required": "This field is required.", "error.emailInvalid": "Please enter a valid email address.", "error.titleMin": "Title must be at least 5 characters.", "portal.title": "Support Portal", "portal.myTickets": "My Tickets", "portal.noTickets": "No tickets yet.", "portal.newTicket": "New Ticket", "widget.close": "Close", "menu.newTicket": "Submit a Ticket", "menu.newTicketDesc": "Create a new support request", "menu.myTickets": "My Tickets", "menu.myTicketsDesc": "View and manage your tickets", "menu.trackTicket": "Track a Ticket", "menu.trackTicketDesc": "Check status with your tracking token", "menu.knowledgeBase": "Knowledge Base", "menu.knowledgeBaseDesc": "Browse help articles and guides", "menu.myTicketsPlaceholder": "Sign in to view your tickets.", "menu.trackPlaceholder": "Ticket tracking is coming soon.", "menu.kbPlaceholder": "Knowledge base is coming soon.", "menu.preferences": "Preferences", "menu.preferencesDesc": "Language and appearance settings", "prefs.title": "Preferences", "prefs.language": "Language", "prefs.theme": "Theme", "prefs.light": "Light", "prefs.dark": "Dark", "prefs.auto": "System", "branding.poweredBy": "Powered by", "attach.dropzone": "Drop files here or click to browse", "attach.dropzoneActive": "Drop files here", "attach.maxFiles": "Maximum {max} files", "attach.remove": "Remove", "attach.uploading": "Uploading files...", "attach.uploadProgress": "Uploading {name}... {percent}%", "attach.invalidType": "File type not allowed", "attach.tooLarge": "File exceeds maximum size", "attach.tooMany": "Maximum number of files reached", "attach.download": "Download", "mytickets.title": "My Tickets", "mytickets.emailPrompt": "Enter your email to view your tickets", "mytickets.emailPlaceholder": "your@email.com", "mytickets.rememberMe": "Remember me", "mytickets.lookup": "Find My Tickets", "mytickets.lookingUp": "Looking up...", "mytickets.noTickets": "No tickets yet.", "mytickets.submitFirst": "Submit your first ticket", "mytickets.noAccount": "No tickets found for this email.", "detail.description": "Description", "detail.attachments": "Attachments", "detail.noAttachments": "No attachments", "detail.replies": "Conversation", "detail.noReplies": "No replies yet.", "detail.replyPlaceholder": "Write a reply...", "detail.sendReply": "Send", "detail.sending": "Sending...", "detail.replyAttachLabel": "Attach files to this reply", "detail.replyAttachHint": "Attach files", "detail.staff": "Staff", "detail.you": "You", "detail.loading": "Loading ticket...", "track.title": "Track a Ticket", "track.tokenPlaceholder": "Enter tracking token (trk_...)", "track.submit": "Track", "track.tracking": "Tracking...", "track.recentTickets": "Recent Tickets", "track.invalidToken": "Invalid or expired tracking token.", "prefs.clearEmail": "Clear saved email", "prefs.emailCleared": "Email cleared", "auth.login": "Login", "auth.logout": "Logout", "auth.sessionExpired": "Session expired. Please log in again.", "auth.loginRequired": "Please log in to select a project.", "auth.noAuthConfigured": "Authentication is not configured for this widget.", "auth.noAuthConfiguredHint": "Please contact the site owner to enable at least one sign-in option.", "widget.expand": "Expand", "widget.collapse": "Collapse", "detail.originalRequest": "Original request", "detail.status": "Status", "detail.priority": "Priority", "detail.created": "Created", "detail.reference": "Reference", "prefs.accentColor": "Accent Color", "prefs.position": "Widget position", "prefs.topStart": "Top start", "prefs.topEnd": "Top end", "prefs.bottomStart": "Bottom start", "prefs.bottomEnd": "Bottom end", "prefs.displayMode": "Display", "prefs.display.popover": "Popover", "prefs.display.side-sheet": "Side sheet", "prefs.display.bottom-sheet": "Bottom sheet", "prefs.sheetSide": "Side", "prefs.side.start": "Start", "prefs.side.end": "End", "form.categoryPlaceholder": "Select a category", "form.categoryBack": "Back", "form.categorySelected": "Category", "form.tags": "Tags", "tag.addNew": "Add tag", "tag.addFirst": "Add your first tag", "tag.namePlaceholder": "New tag name", "tag.exists": "A tag with that name already exists.", "diag.title": "Share diagnostic info", "diag.shareAll": "Share diagnostic info with support", "diag.hint": "Help us resolve your issue faster — nothing sensitive", "diag.showIncluded": "See what’s included", "diag.hideIncluded": "Hide what’s included", "diag.screenResolution": "Screen resolution", "diag.deviceType": "Device type", "diag.timezone": "Timezone", "diag.referrerUrl": "Referrer URL", "diag.language": "Browser language", "diag.platform": "Platform", "detail.resolve": "Mark as Resolved", "detail.resolving": "Resolving...", "detail.resolved": "Resolved", "detail.sla": "SLA", "category.addNew": "Add category", "category.addUnder": "under", "category.namePlaceholder": "Category name", "form.categorySearch": "Search categories", "form.categoryRoot": "All", "form.categoryNoMatch": "No categories match your search.", "form.categoryLeaf": "No sub-categories here.", "form.categoryClear": "Clear", "category.exists": "This category already exists", "category.limitReached": "Category limit reached — upgrade your plan", "project.select": "Select Project", "project.switch": "Switch Project", "project.switchDesc": "Change the active project", "project.loading": "Loading projects...", "project.noProjects": "No projects available", "project.current": "Current project", "prefs.resetToDefaults": "Reset to defaults", "prefs.resetConfirm": "Reset all preferences to the host defaults?" }; //#endregion //#region src/i18n/ar.ts const ar = { "widget.title": "الدعم", "widget.newTicket": "تذكرة جديدة", "widget.trackTicket": "تتبع التذكرة", "form.title": "العنوان", "form.titlePlaceholder": "ملخص موجز لمشكلتك", "form.description": "الوصف", "form.descriptionPlaceholder": "صف مشكلتك بالتفصيل...", "form.email": "البريد الإلكتروني", "form.emailPlaceholder": "your@email.com", "form.priority": "الأولوية", "form.priorityLow": "منخفضة", "form.priorityMedium": "متوسطة", "form.priorityHigh": "عالية", "form.priorityCritical": "حرجة", "form.priorityUrgent": "حرجة", "form.category": "الفئة", "form.categoryNone": "اختر فئة", "form.submit": "إرسال التذكرة", "form.submitting": "جارِ الإرسال...", "form.cancel": "إلغاء", "form.attachment": "إرفاق ملفات", "success.title": "تم إرسال التذكرة!", "success.ticketNumber": "تذكرة #", "success.trackingToken": "رمز التتبع الخاص بك:", "success.trackingHint": "احفظ هذا الرمز لتتبع حالة تذكرتك.", "success.copyToken": "نسخ الرمز", "success.copied": "تم النسخ!", "success.close": "إغلاق", "success.trackNow": "تتبع التذكرة", "tracker.title": "تتبع تذكرتك", "tracker.tokenPlaceholder": "أدخل رمز التتبع الخاص بك (trk_...)", "tracker.submit": "تتبع", "tracker.tracking": "جارِ التتبع...", "tracker.status": "الحالة", "tracker.priority": "الأولوية", "tracker.created": "تاريخ الإنشاء", "tracker.replies": "الردود", "tracker.noReplies": "لا توجد ردود بعد.", "tracker.replyPlaceholder": "اكتب رداً...", "tracker.sendReply": "إرسال الرد", "tracker.sending": "جارِ الإرسال...", "tracker.back": "رجوع", "tracker.staff": "فريق الدعم", "error.generic": "حدث خطأ ما. يرجى المحاولة مرة أخرى.", "error.validation": "يرجى تصحيح الحقول المميزة والمحاولة مرة أخرى.", "error.network": "خطأ في الشبكة. تحقق من اتصالك.", "error.server": "الخادم يواجه مشكلة. يرجى المحاولة بعد قليل.", "error.forbidden": "ليس لديك صلاحية لتنفيذ هذا الإجراء.", "error.unauthenticated": "يرجى تسجيل الدخول للمتابعة.", "error.notFound": "تعذّر العثور على ما تبحث عنه.", "notification.dismiss": "إغلاق", "error.tokenInvalid": "رمز التتبع غير صالح أو منتهي الصلاحية.", "error.required": "هذا الحقل مطلوب.", "error.emailInvalid": "يرجى إدخال بريد إلكتروني صالح.", "error.titleMin": "يجب أن يكون العنوان 5 أحرف على الأقل.", "portal.title": "بوابة الدعم", "portal.myTickets": "تذاكري", "portal.noTickets": "لا توجد تذاكر بعد.", "portal.newTicket": "تذكرة جديدة", "widget.close": "إغلاق", "menu.newTicket": "إرسال تذكرة", "menu.newTicketDesc": "إنشاء طلب دعم جديد", "menu.myTickets": "تذاكري", "menu.myTicketsDesc": "عرض وإدارة تذاكرك", "menu.trackTicket": "تتبع تذكرة", "menu.trackTicketDesc": "تحقق من الحالة باستخدام رمز التتبع", "menu.knowledgeBase": "قاعدة المعرفة", "menu.knowledgeBaseDesc": "تصفح مقالات المساعدة والأدلة", "menu.myTicketsPlaceholder": "سجّل الدخول لعرض تذاكرك.", "menu.trackPlaceholder": "تتبع التذاكر قريبًا.", "menu.kbPlaceholder": "قاعدة المعرفة قريبًا.", "menu.preferences": "التفضيلات", "menu.preferencesDesc": "إعدادات اللغة والمظهر", "prefs.title": "التفضيلات", "prefs.language": "اللغة", "prefs.theme": "المظهر", "prefs.light": "فاتح", "prefs.dark": "داكن", "prefs.auto": "النظام", "branding.poweredBy": "مدعوم من", "attach.dropzone": "اسحب الملفات هنا أو انقر للتصفح", "attach.dropzoneActive": "أفلت الملفات هنا", "attach.maxFiles": "بحد أقصى {max} ملفات", "attach.remove": "إزالة", "attach.uploading": "جارِ رفع الملفات...", "attach.uploadProgress": "جارِ رفع {name}... {percent}%", "attach.invalidType": "نوع الملف غير مسموح", "attach.tooLarge": "حجم الملف يتجاوز الحد الأقصى", "attach.tooMany": "تم الوصول للحد الأقصى لعدد الملفات", "attach.download": "تحميل", "mytickets.title": "تذاكري", "mytickets.emailPrompt": "أدخل بريدك الإلكتروني لعرض تذاكرك", "mytickets.emailPlaceholder": "your@email.com", "mytickets.rememberMe": "تذكرني", "mytickets.lookup": "البحث عن تذاكري", "mytickets.lookingUp": "جارِ البحث...", "mytickets.noTickets": "لا توجد تذاكر بعد.", "mytickets.submitFirst": "أرسل تذكرتك الأولى", "mytickets.noAccount": "لم يتم العثور على تذاكر لهذا البريد.", "detail.description": "الوصف", "detail.attachments": "المرفقات", "detail.noAttachments": "لا توجد مرفقات", "detail.replies": "المحادثة", "detail.noReplies": "لا توجد ردود بعد.", "detail.replyPlaceholder": "اكتب رداً...", "detail.sendReply": "إرسال", "detail.sending": "جارِ الإرسال...", "detail.replyAttachLabel": "إرفاق ملفات بهذا الرد", "detail.replyAttachHint": "إرفاق ملفات", "detail.staff": "فريق الدعم", "detail.you": "أنت", "detail.loading": "جارِ تحميل التذكرة...", "track.title": "تتبع تذكرة", "track.tokenPlaceholder": "أدخل رمز التتبع (trk_...)", "track.submit": "تتبع", "track.tracking": "جارِ التتبع...", "track.recentTickets": "التذاكر الأخيرة", "track.invalidToken": "رمز التتبع غير صالح أو منتهي.", "prefs.clearEmail": "مسح البريد المحفوظ", "prefs.emailCleared": "تم مسح البريد", "auth.login": "تسجيل الدخول", "auth.logout": "تسجيل الخروج", "auth.sessionExpired": "انتهت الجلسة. يرجى تسجيل الدخول مرة أخرى.", "auth.loginRequired": "يرجى تسجيل الدخول لاختيار مشروع.", "auth.noAuthConfigured": "لم يتم تكوين المصادقة لهذه الأداة.", "auth.noAuthConfiguredHint": "يرجى التواصل مع مالك الموقع لتفعيل خيار تسجيل دخول واحد على الأقل.", "widget.expand": "توسيع", "widget.collapse": "تصغير", "detail.originalRequest": "الطلب الأصلي", "detail.status": "الحالة", "detail.priority": "الأولوية", "detail.created": "تاريخ الإنشاء", "detail.reference": "المرجع", "prefs.accentColor": "لون التمييز", "prefs.position": "موضع الأداة", "prefs.topStart": "أعلى البداية", "prefs.topEnd": "أعلى النهاية", "prefs.bottomStart": "أسفل البداية", "prefs.bottomEnd": "أسفل النهاية", "prefs.displayMode": "طريقة العرض", "prefs.display.popover": "نافذة منبثقة", "prefs.display.side-sheet": "لوحة جانبية", "prefs.display.bottom-sheet": "لوحة سفلية", "prefs.sheetSide": "الجانب", "prefs.side.start": "البداية", "prefs.side.end": "النهاية", "form.categoryPlaceholder": "اختر فئة", "form.categoryBack": "رجوع", "form.categorySelected": "الفئة", "form.tags": "الوسوم", "tag.addNew": "إضافة وسم", "tag.addFirst": "أضف أول وسم", "tag.namePlaceholder": "اسم الوسم الجديد", "tag.exists": "يوجد وسم بهذا الاسم بالفعل.", "diag.title": "مشاركة معلومات التشخيص", "diag.shareAll": "مشاركة معلومات التشخيص مع الدعم", "diag.hint": "ساعدنا في حل مشكلتك بشكل أسرع — لا شيء حساس", "diag.showIncluded": "عرض المعلومات المضمّنة", "diag.hideIncluded": "إخفاء المعلومات المضمّنة", "diag.screenResolution": "دقة الشاشة", "diag.deviceType": "نوع الجهاز", "diag.timezone": "المنطقة الزمنية", "diag.referrerUrl": "رابط المصدر", "diag.language": "لغة المتصفح", "diag.platform": "المنصة", "detail.resolve": "تحديد كمحلول", "detail.resolving": "جارِ الحل...", "detail.resolved": "محلول", "detail.sla": "اتفاقية الخدمة", "category.addNew": "إضافة فئة", "category.addUnder": "ضمن", "category.namePlaceholder": "اسم الفئة", "form.categorySearch": "بحث في الفئات", "form.categoryRoot": "الكل", "form.categoryNoMatch": "لا توجد فئات مطابقة لبحثك.", "form.categoryLeaf": "لا توجد فئات فرعية هنا.", "form.categoryClear": "مسح", "category.exists": "هذه الفئة موجودة بالفعل", "category.limitReached": "تم الوصول لحد الفئات — قم بترقية خطتك", "project.select": "اختر المشروع", "project.switch": "تبديل المشروع", "project.switchDesc": "تغيير المشروع النشط", "project.loading": "جاري تحميل المشاريع...", "project.noProjects": "لا توجد مشاريع متاحة", "project.current": "المشروع الحالي", "prefs.resetToDefaults": "إعادة إلى الافتراضي", "prefs.resetConfirm": "إعادة تعيين جميع التفضيلات إلى افتراضيات المستضيف؟" }; //#endregion //#region src/theme.ts const DEFAULT_THEME = { primaryColor: "#0F5E56", mode: "auto", borderRadius: "6px", fontFamily: "'Geist', 'Rubik', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", zIndex: 9999 }; function resolveMode(mode) { if (mode === "auto") { if (typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches) return "dark"; return "light"; } return mode ?? "light"; } function themeToVars(theme = {}) { const merged = { ...DEFAULT_THEME, ...theme }; if (!merged.primaryColor) merged.primaryColor = DEFAULT_THEME.primaryColor; if (!merged.mode) merged.mode = DEFAULT_THEME.mode; const t = { ...merged, mode: resolveMode(merged.mode) }; const hsl = hexToHsl(t.primaryColor); const lightL = Math.min(hsl.l + 30, 95); const darkL = Math.max(hsl.l - 15, 10); return [ `--rqd-primary: ${t.primaryColor}`, `--rqd-primary-h: ${hsl.h}`, `--rqd-primary-s: ${hsl.s}%`, `--rqd-primary-l: ${hsl.l}%`, `--rqd-primary-light: hsl(${hsl.h}, ${hsl.s}%, ${lightL}%)`, `--rqd-primary-dark: hsl(${hsl.h}, ${hsl.s}%, ${darkL}%)`, `--rqd-radius: ${t.borderRadius}`, `--rqd-font: ${t.fontFamily}`, `--rqd-z: ${t.zIndex}`, `--rqd-bg: ${t.mode === "dark" ? "#0A0E0F" : "#FAF7F0"}`, `--rqd-bg-secondary: ${t.mode === "dark" ? "#14191B" : "#F2EEE5"}`, `--rqd-text: ${t.mode === "dark" ? "#F2EEE5" : "#0A0E0F"}`, `--rqd-text-secondary: ${t.mode === "dark" ? "rgba(242,238,229,0.62)" : "#5A6468"}`, `--rqd-border: ${t.mode === "dark" ? "rgba(242,238,229,0.10)" : "rgba(10,14,15,0.10)"}`, `--rqd-input-bg: ${t.mode === "dark" ? "#1D2427" : "#FFFFFF"}`, `--rqd-shadow: ${t.mode === "dark" ? "0 20px 50px rgba(0,0,0,0.5)" : "0 8px 32px rgba(10,14,15,0.10)"}`, `--rqd-signal: #E3511E` ].join("; "); } /** * Returns CSS custom properties as a React-compatible style object. * React's `style` prop supports custom properties (--var) as keys directly. */ function themeToStyle(theme = {}) { const merged = { ...DEFAULT_THEME, ...theme }; if (!merged.primaryColor) merged.primaryColor = DEFAULT_THEME.primaryColor; if (!merged.mode) merged.mode = DEFAULT_THEME.mode; const t = { ...merged, mode: resolveMode(merged.mode) }; const hsl = hexToHsl(t.primaryColor); const lightL = Math.min(hsl.l + 30, 95); const darkL = Math.max(hsl.l - 15, 10); return { "--rqd-primary": t.primaryColor, "--rqd-primary-h": String(hsl.h), "--rqd-primary-s": `${hsl.s}%`, "--rqd-primary-l": `${hsl.l}%`, "--rqd-primary-light": `hsl(${hsl.h}, ${hsl.s}%, ${lightL}%)`, "--rqd-primary-dark": `hsl(${hsl.h}, ${hsl.s}%, ${darkL}%)`, "--rqd-radius": t.borderRadius, "--rqd-font": t.fontFamily, "--rqd-z": t.zIndex, "--rqd-bg": t.mode === "dark" ? "#0A0E0F" : "#FAF7F0", "--rqd-bg-secondary": t.mode === "dark" ? "#14191B" : "#F2EEE5", "--rqd-text": t.mode === "dark" ? "#F2EEE5" : "#0A0E0F", "--rqd-text-secondary": t.mode === "dark" ? "rgba(242,238,229,0.62)" : "#5A6468", "--rqd-border": t.mode === "dark" ? "rgba(242,238,229,0.10)" : "rgba(10,14,15,0.10)", "--rqd-input-bg": t.mode === "dark" ? "#1D2427" : "#FFFFFF", "--rqd-shadow": t.mode === "dark" ? "0 20px 50px rgba(0,0,0,0.5)" : "0 8px 32px rgba(10,14,15,0.10)", "--rqd-signal": "#E3511E" }; } function hexToHsl(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!result) return { h: 173, s: 72, l: 21 }; const r = parseInt(result[1], 16) / 255; const g = parseInt(result[2], 16) / 255; const b = parseInt(result[3], 16) / 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); let h = 0; let s = 0; const l = (max + min) / 2; if (max !== min) { const d = max - min; s = l > .5 ? d / (2 - max - min) : d / (max + min); if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6; else if (max === g) h = ((b - r) / d + 2) / 6; else h = ((r - g) / d + 4) / 6; } return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }; } function getWidgetStyles() { return WIDGET_CSS; } const WIDGET_CSS = `:host { all: initial; } .rqd-root { font-family: var(--rqd-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); font-size: 14px; line-height: 1.5; color: var(--rqd-text); -webkit-font-smoothing: antialiased; } .rqd-fab { position: fixed; width: 56px; height: 56px; padding: 0; border-radius: 28px; background: var(--rqd-primary); color: #fff; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 0; box-shadow: 0 4px 16px rgba(0,0,0,0.2); transition: width 0.22s cubic-bezier(0.2,0.8,0.2,1), padding 0.22s cubic-bezier(0.2,0.8,0.2,1), gap 0.22s cubic-bezier(0.2,0.8,0.2,1), transform 0.2s, box-shadow 0.2s, opacity 0.12s ease; pointer-events: auto; z-index: var(--rqd-z, 9999); overflow: hidden; white-space: nowrap; } .rqd-fab:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.25); } .rqd-fab:not(.rqd-fab-labeled):hover { transform: scale(1.08); } .rqd-fab.rqd-bottom-end { inset-block-end: 20px; inset-block-start: auto; inset-inline-end: 20px; inset-inline-start: auto; } .rqd-fab.rqd-bottom-start { inset-block-end: 20px; inset-block-start: auto; inset-inline-start: 20px; inset-inline-end: auto; } .rqd-fab.rqd-top-end { inset-block-start: 20px; inset-block-end: auto; inset-inline-end: 20px; inset-inline-start: auto; } .rqd-fab.rqd-top-start { inset-block-start: 20px; inset-block-end: auto; inset-inline-start: 20px; inset-inline-end: auto; } .rqd-fab svg { width: 24px; height: 24px; fill: currentColor; flex-shrink: 0; } /* Collapsed labeled FAB keeps circle geometry (no padding, no gap) so the icon stays geometrically centered. Hover/focus reveals label; only then does padding and gap expand to accommodate it. */ .rqd-fab-labeled { padding: 0; width: 56px; } .rqd-fab-labeled.rqd-fab-side-start { flex-direction: row; } .rqd-fab-labeled.rqd-fab-side-end { flex-direction: row-reverse; } .rqd-fab-label { display: inline-block; font-size: 13px; font-weight: 600; color: #fff; max-width: 0; opacity: 0; overflow: hidden; transition: max-width 0.22s cubic-bezier(0.2,0.8,0.2,1), opacity 0.18s ease; } .rqd-fab-labeled:hover, .rqd-fab-labeled:focus-visible { padding: 0 18px; width: auto; gap: 8px; } .rqd-fab-labeled:hover .rqd-fab-label, .rqd-fab-labeled:focus-visible .rqd-fab-label { max-width: 180px; opacity: 1; transition-delay: 0s, 0.08s; } .rqd-fab.rqd-repositioning, .rqd-panel.rqd-repositioning { opacity: 0; } .rqd-panel { position: fixed; width: 380px; max-height: calc(100vh - 120px); background: var(--rqd-bg); border-radius: var(--rqd-radius); box-shadow: var(--rqd-shadow); overflow: hidden; display: flex; flex-direction: column; pointer-events: auto; animation: rqd-slide-up 0.25s ease-out; border: 1px solid var(--rqd-border); z-index: var(--rqd-z, 9999); transition: opacity 0.12s ease; } .rqd-panel.rqd-bottom-end { inset-block-end: 88px; inset-block-start: auto; inset-inline-end: 20px; inset-inline-start: auto; } .rqd-panel.rqd-bottom-start { inset-block-end: 88px; inset-block-start: auto; inset-inline-start: 20px; inset-inline-end: auto; } .rqd-panel.rqd-top-end { inset-block-start: 88px; inset-block-end: auto; inset-inline-end: 20px; inset-inline-start: auto; animation-name: rqd-slide-down; } .rqd-panel.rqd-top-start { inset-block-start: 88px; inset-block-end: auto; inset-inline-start: 20px; inset-inline-end: auto; animation-name: rqd-slide-down; } .rqd-panel.rqd-hidden { display: none; } @media (prefers-reduced-motion: reduce) { .rqd-fab, .rqd-fab-label, .rqd-panel, .rqd-panel.rqd-mode-side-sheet, .rqd-panel.rqd-mode-bottom-sheet, .rqd-backdrop-sheet { transition: none !important; animation: none !important; } .rqd-fab-labeled:hover .rqd-fab-label, .rqd-fab-labeled:focus-visible .rqd-fab-label { max-width: 180px; opacity: 1; } } /* ── Sheet display modes (side-sheet, bottom-sheet) ──────────────────────────── */ .rqd-panel.rqd-mode-side-sheet { position: fixed; inset-block: 0; width: var(--rqd-sheet-width, 420px); max-width: 100vw; max-height: 100vh; height: 100vh; border-radius: 0; border: none; box-shadow: var(--rqd-shadow); animation: rqd-sheet-slide-end 0.24s cubic-bezier(0.22, 1, 0.36, 1); } .rqd-panel.rqd-mode-side-sheet.rqd-side-end { inset-inline-end: 0; inset-inline-start: auto; border-inline-start: 1px solid var(--rqd-border); animation-name: rqd-sheet-slide-end; } .rqd-panel.rqd-mode-side-sheet.rqd-side-start { inset-inline-start: 0; inset-inline-end: auto; border-inline-end: 1px solid var(--rqd-border); animation-name: rqd-sheet-slide-start; } .rqd-panel.rqd-mode-bottom-sheet { position: fixed; inset-inline: 0; inset-block-end: 0; inset-block-start: auto; width: 100vw; max-width: 100vw; height: var(--rqd-sheet-height, 55vh); max-height: 92vh; border-radius: calc(var(--rqd-radius, 8px) * 2) calc(var(--rqd-radius, 8px) * 2) 0 0; border: none; border-top: 1px solid var(--rqd-border); box-shadow: var(--rqd-shadow); animation: rqd-sheet-slide-up 0.24s cubic-bezier(0.22, 1, 0.36, 1); } @keyframes rqd-sheet-slide-end { from { transform: translateX(100%); } to { transform: translateX(0); } } @keyframes rqd-sheet-slide-start { from { transform: translateX(-100%); } to { transform: translateX(0); } } @keyframes rqd-sheet-slide-up { from { transform: translateY(100%); } to { transform: translateY(0); } } /* RTL automatically flips start/end via logical properties; explicit keyframe override so translateX direction matches visual edge. */ :host([dir="rtl"]) .rqd-panel.rqd-mode-side-sheet.rqd-side-end { animation-name: rqd-sheet-slide-start; } :host([dir="rtl"]) .rqd-panel.rqd-mode-side-sheet.rqd-side-start { animation-name: rqd-sheet-slide-end; } /* Backdrop for sheet modes (viewports ≥ 640px; see index.ts / FloatingWidget for render gate) */ .rqd-backdrop-sheet { position: fixed; inset: 0; background: rgba(10, 14, 15, 0.28); z-index: calc(var(--rqd-z, 9999) - 1); pointer-events: auto; animation: rqd-backdrop-fade 0.18s ease-out; } @keyframes rqd-backdrop-fade { from { opacity: 0; } to { opacity: 0.28 / 0.28; } } /* Contained mode: sheets anchor inside the container, not the viewport */ .rqd-contained.rqd-panel.rqd-mode-side-sheet, .rqd-contained.rqd-panel.rqd-mode-bottom-sheet { position: absolute; height: 100%; max-height: 100%; } /* Expanded mode — full-height side sheet, width controlled by --rqd-expanded-width (default 60vw) */ .rqd-panel.rqd-expanded { width: var(--rqd-expanded-width, 60vw); max-width: var(--rqd-expanded-width, 60vw); max-height: 100vh; height: 100vh; inset-block: 0 !important; inset-inline-end: 0 !important; inset-inline-start: auto !important; border-radius: 0; border: none; border-inline-start: 1px solid var(--rqd-border); box-shadow: none; animation: rqd-slide-in 0.25s ease-out; } .rqd-panel.rqd-expanded.rqd-bottom-start, .rqd-panel.rqd-expanded.rqd-top-start { inset-inline-end: auto !important; inset-inline-start: 0 !important; border-inline-start: none; border-inline-end: 1px solid var(--rqd-border); } @media (max-width: 640px) { .rqd-panel.rqd-expanded { width: 100vw; max-width: 100vw; } } @keyframes rqd-slide-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } .rqd-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.35); z-index: calc(var(--rqd-z, 9999) - 1); pointer-events: auto; animation: rqd-fade-in 0.2s ease-out; } @keyframes rqd-fade-in { from { opacity: 0; } to { opacity: 1; } } /* User identity + logout in header — separate clickable zones for clarity */ .rqd-auth-user { display: inline-flex; align-items: center; gap: 4px; padding: 2px 6px 2px 10px; background: rgba(255,255,255,0.12); border-radius: 16px; } .rqd-auth-user-name { font-size: 12px; color: #fff; max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rqd-auth-user .rqd-header-close { width: 22px; height: 22px; } @keyframes rqd-slide-up { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } } @keyframes rqd-slide-down { from { opacity: 0; transform: translateY(-16px); } to { opacity: 1; transform: translateY(0); } } .rqd-header { display: flex; align-items: center; justify-content: space-between; padding: 16px; background: var(--rqd-primary); color: #fff; } .rqd-header-title { font-size: 16px; font-weight: 600; } .rqd-header-close { background: none; border: none; color: #fff; cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border-radius: 50%; transition: background 0.15s; } .rqd-header-close:hover { background: rgba(255,255,255,0.2); } .rqd-tabs { display: flex; border-bottom: 1px solid var(--rqd-border); } .rqd-tab { flex: 1; padding: 10px; text-align: center; background: none; border: none; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--rqd-text-secondary); border-bottom: 2px solid transparent; transition: color 0.15s, border-color 0.15s; } .rqd-tab.rqd-active { color: var(--rqd-primary); border-bottom-color: var(--rqd-primary); } .rqd-body { flex: 1; overflow-y: auto; padding: 16px; } .rqd-form-group { margin-bottom: 14px; } .rqd-label { display: block; font-size: 13px; font-weight: 500; margin-bottom: 4px; color: var(--rqd-text); } .rqd-input, .rqd-textarea, .rqd-select { width: 100%; padding: 8px 12px; border: 1px solid var(--rqd-border); border-radius: calc(var(--rqd-radius, 8px) / 2); font-size: 14px; font-family: inherit; background: var(--rqd-input-bg); color: var(--rqd-text); outline: none; transition: border-color 0.15s; box-sizing: border-box; } .rqd-input:focus, .rqd-textarea:focus, .rqd-select:focus { border-color: var(--rqd-primary); } .rqd-textarea { resize: vertical; min-height: 80px; } .rqd-error-text { color: #e74c3c; font-size: 12px; margin-top: 2px; } .rqd-input.rqd-field-error, .rqd-textarea.rqd-field-error, .rqd-select.rqd-field-error { border-color: #e74c3c; } .rqd-error-banner { background: rgba(231,76,60,0.10); border: 1px solid rgba(231,76,60,0.35); color: #c0392b; padding: 10px 12px; border-radius: calc(var(--rqd-radius, 8px) / 2); font-size: 13px; margin-bottom: 12px; display: flex; gap: 8px; align-items: flex-start; } .rqd-error-banner-icon { font-size: 16px; line-height: 1; flex-shrink: 0; margin-top: 1px; } .rqd-error-banner-body { flex: 1; min-width: 0; } .rqd-error-banner-title { font-weight: 600; margin: 0 0 2px; font-size: 13px; } .rqd-error-banner-list { margin: 2px 0 0; padding: 0 0 0 16px; font-size: 12px; color: #a93226; } :host([dir="rtl"]) .rqd-error-banner-list { padding: 0 16px 0 0; } .rqd-field-hint { font-size: 12px; color: var(--rqd-text-secondary); margin-top: 2px; } /* Global notification stack — sits at the top of the widget body above all view content. */ .rqd-notification-stack { display: flex; flex-direction: column; gap: 8px; padding: 0 0 12px; } .rqd-notification { display: flex; gap: 8px; align-items: flex-start; padding: 10px 12px; border-radius: calc(var(--rqd-radius, 8px) / 2); font-size: 13px; border: 1px solid transparent; animation: rqd-notif-slide 0.18s ease-out; } .rqd-notification-icon { font-size: 15px; line-height: 1; flex-shrink: 0; margin-top: 1px; } .rqd-notification-body { flex: 1; min-width: 0; } .rqd-notification-title { margin: 0; font-weight: 600; font-size: 13px; line-height: 1.4; } .rqd-notification-list { margin: 4px 0 0; padding: 0 0 0 16px; font-size: 12px; opacity: 0.9; } :host([dir="rtl"]) .rqd-notification-list { padding: 0 16px 0 0; } .rqd-notification-dismiss { background: transparent; border: none; color: inherit; cursor: pointer; font-size: 14px; line-height: 1; padding: 2px 4px; opacity: 0.65; border-radius: 4px; flex-shrink: 0; transition: opacity 0.12s, background 0.12s; } .rqd-notification-dismiss:hover { opacity: 1; background: rgba(0,0,0,0.06); } .rqd-notification-error { background: rgba(231,76,60,0.10); border-color: rgba(231,76,60,0.35); color: #c0392b; } .rqd-notification-warning { background: rgba(230,167,45,0.12); border-color: rgba(230,167,45,0.40); color: #a77010; } .rqd-notification-info { background: rgba(52,152,219,0.10); border-color: rgba(52,152,219,0.35); color: #1f6d96; } .rqd-notification-success { background: rgba(46,204,113,0.10); border-color: rgba(46,204,113,0.35); color: #1e8449; } @keyframes rqd-notif-slide { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } } @media (prefers-reduced-motion: reduce) { .rqd-notification { animation: none !important; } } .rqd-btn { width: 100%; padding: 10px; border: none; border-radius: calc(var(--rqd-radius, 8px) / 2); font-size: 14px; font-weight: 600; cursor: pointer; transition: opacity 0.15s; } .rqd-btn:disabled { opacity: 0.6; cursor: not-allowed; } .rqd-btn-primary { background: var(--rqd-primary); color: #fff; } .rqd-btn-primary:hover:not(:disabled) { opacity: 0.9; } .rqd-btn-secondary { background: transparent; color: var(--rqd-primary); border: 1px solid var(--rqd-primary); } .rqd-success { text-align: center; padding: 24px 0; } .rqd-success-icon { font-size: 48px; margin-bottom: 12px; } .rqd-success h3 { margin: 0 0 8px; font-size: 18px; color: var(--rqd-text); } .rqd-token-box { background: var(--rqd-bg-secondary); border: 1px solid var(--rqd-border); border-radius: calc(var(--rqd-radius, 8px) / 2); padding: 8px 12px; font-family: monospace; font-size: 12px; word-break: break-all; margin: 8px 0; color: var(--rqd-text); } .rqd-ticket-info { background: var(--rqd-bg-secondary); border-radius: calc(var(--rqd-radius, 8px) / 2); padding: 12px; margin-bottom: 12px; } .rqd-ticket-row { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 13px; } .rqd-ticket-label { color: var(--rqd-text-secondary); } .rqd-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 600; background: var(--rqd-primary-light); color: var(--rqd-primary-dark); } .rqd-reply { padding: 10px 0; border-bottom: 1px solid var(--rqd-border); } .rqd-reply:last-child { border-bottom: none; } .rqd-reply-header { display: flex; justify-content: space-between; font-size: 12px; color: var(--rqd-text-secondary); margin-bottom: 4px; } .rqd-reply-staff { color: var(--rqd-primary); font-weight: 600; } .rqd-reply-body { font-size: 14px; white-space: pre-wrap; color: var(--rqd-text); } .rqd-inline { pointer-events: auto; background: var(--rqd-bg); border: 1px solid var(--rqd-border); border-radius: var(--rqd-radius); overflow: hidden; } .rqd-inline .rqd-body { max-height: none; } :host([dir="rtl"]) .rqd-root { direction: rtl; text-align: right; } .rqd-menu { display: flex; flex-direction: column; gap: 6px; } .rqd-menu-item { display: flex; align-items: center; gap: 14px; width: 100%; padding: 14px 12px; background: var(--rqd-bg-secondary); border: 1px solid transparent; border-radius: calc(var(--rqd-radius, 8px) / 1.5); cursor: pointer; text-align: start; transition: border-color 0.15s, background 0.15s, transform 0.1s; font-family: inherit; color: var(--rqd-text); } .rqd-menu-item:hover { border-color: var(--rqd-primary); background: var(--rqd-bg); transform: translateY(-1px); } .rqd-menu-icon { width: 42px; height: 42px; display: flex; align-items: center; justify-content: center; border-radius: calc(var(--rqd-radius, 8px) / 2); background: var(--rqd-primary-light); color: var(--rqd-primary-dark); flex-shrink: 0; } .rqd-menu-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .rqd-menu-label { font-size: 14px; font-weight: 600; line-height: 1.3; } .rqd-menu-desc { font-size: 12px; color: var(--rqd-text-secondary); line-height: 1.3; } .rqd-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 40px 16px; color: var(--rqd-text-secondary); text-align: center; } .rqd-placeholder p { margin: 0; font-size: 14px; } .rqd-header-brand { display: flex; align-items: center; gap: 8px; min-width: 0; } .rqd-header-logo { width: 24px; height: 24px; border-radius: 4px; object-fit: contain; flex-shrink: 0; } .rqd-footer { display: flex; align-items: center; justify-content: center; gap: 4px; padding: 8px 16px; border-top: 1px solid var(--rqd-border); font-size: 11px; color: var(--rqd-text-secondary); } .rqd-footer a { color: var(--rqd-text-secondary); text-decoration: none; font-weight: 600; transition: color 0.15s; } .rqd-footer a:hover { color: var(--rqd-primary); } .rqd-prefs { display: flex; flex-direction: column; gap: 16px; } .rqd-prefs-group { display: flex; flex-direction: column; gap: 6px; } .rqd-prefs-label { font-size: 13px; font-weight: 600; color: var(--rqd-text); } .rqd-prefs-options { display: flex; gap: 6px; } .rqd-prefs-option { flex: 1; padding: 8px 12px; background: var(--rqd-bg-secondary); border: 1px solid var(--rqd-border); border-radius: calc(var(--rqd-radius, 8px) / 2); cursor: pointer; text-align: center; font-size: 13px; font-family: inherit; color: var(--rqd-text); transition: border-color 0.15s, background 0.15s; } .rqd-prefs-option:hover { border-color: var(--rqd-primary); } .rqd-prefs-option.rqd-active { border-color: var(--rqd-primary); background: var(--rqd-primary-light); color: var(--rqd-primary-dark); font-weight: 600; } .rqd-dropzone { border: 2px dashed var(--rqd-border); border-radius: calc(var(--rqd-radius, 8px) / 2); padding: 16px; text-align: center; cursor: pointer; transition: border-color 0.15s, background 0.15s; color: var(--rqd-text-secondary); font-size: 13px; margin-bottom: 14px; } .rqd-dropzone:hover { border-color: var(--rqd-primary); } .rqd-dropzone.rqd-dropzone-active { border-color: var(--rqd-primary); background: var(--rqd-primary-light); color: var(--rqd-primary-dark); } .rqd-file-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; } .rqd-file-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; background: var(--rqd-bg-secondary); border-radius: calc(var(--rqd-radius, 8px) / 2); font-size: 13px; } .rqd-file-item-info { display: flex; flex-direction: column; gap: 1px; min-width: 0; } .rqd-file-item-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--rqd-text); } .rqd-file-item-size { font-size: 11px; color: var(--rqd-text-secondary); } .rqd-file-remove { background: none; border: none; color: var(--rqd-text-secondary); cursor: pointer; padding: 4px; font-size: 16px; line-height: 1; flex-shrink: 0; } .rqd-file-remove:hover { color: #e74c3c; } .rqd-progress { width: 100%; height: 6px; background: var(--rqd-bg-secondary); border-radius: 3px; overflow: hidden; margin-top: 6px; } .rqd-progress-bar { height: 100%; background: var(--rqd-primary); border-radius: 3px; transition: width 0.2s ease; } .rqd-upload-status { font-size: 12px; color: var(--rqd-text-secondary); margin-top: 4px; } .rqd-attachment-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } .rqd-attachment-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 10px; background: var(--rqd-bg-secondary); border-radius: calc(var(--rqd-radius, 8px) / 2); font-size: 13px; } .rqd-attachment-item a { color: var(--rqd-primary); text-decoration: none; font-weight: 500; font-size: 12px; flex-shrink: 0; } .rqd-attachment-item a:hover { text-decoration: underline; } .rqd-ticket-header { margin-bottom: 12px; } .rqd-ticket-header h3 { margin: 0 0 6px; font-size: 16px; font-weight: 600; color: var(--rqd-text); } .rqd-ticket-header-meta { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; } .rqd-ticket-desc { font-size: 14px; color: var(--rqd-text); white-space: pre-wrap; margin-bottom: 16px; padding: 10px; background: var(--rqd-bg-secondary); border-radius: calc(var(--rqd-radius, 8px) / 2); } .rqd-section-title { font-size: 13px; font-weight: 600; color: var(--rqd-text); margin-bottom: 8px; } /* ── Ticket Detail layout ─────────────────────────────────────────────────── */ .rqd-detail { display: flex; flex-direction: column; gap: 14px; position: relative; } .rqd-detail-main { display: flex; flex-direction: column; gap: 12px; min-width: 0; padding-bottom: 80px; /* room for sticky composer */ } /* Collapsible "Original request" — progressive disclosure keeps conversation front-and-center */ .rqd-detail-original { border: 1px solid var(--rqd-border); border-radius: calc(var(--rqd-radius, 8px)); background: var(--rqd-bg-secondary); overflow: hidden; } .rqd-detail-original-summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px; padding: 10px 12px; font-size: 13px; color: var(--rqd-text); user-select: none; background: transparent; border: none; width: 100%; text-align: start; font-family: inherit; } .rqd-detail-original-summary:hover { background: rgba(127,127,127,0.05); } .rqd-detail-original-label { font-weight: 600; flex: 1; } .rqd-detail-original-count { font-size: 11px; color: var(--rqd-text-secondary); padding: 2px 8px; border-radius: 10px; background: var(--rqd-bg); border: 1px solid var(--rqd-border); } .rqd-detail-original-chevron { transition: transform 0.2s ease; color: var(--rqd-text-secondary); flex-shrink: 0; } .rqd-detail-original.rqd-open .rqd-detail-original-chevron { transform: rotate(180deg); } .rqd-detail-original-body { display: none; padding: 0 12px 12px; flex-direction: column; gap: 10px; } .rqd-detail-original.rqd-open .rqd-detail-original-body { display: flex; } .rqd-detail-original-body .rqd-ticket-desc { background: transparent; padding: 0; margin: 0; } /* Sticky composer — always visible at bottom of the body scroll */ .rqd-reply-compose { position: sticky; bottom: 0; background: var(--rqd-bg); padding: 10px 0; margin: 0 -16px -16px; padding-left: 16px; padding-right: 16px; border-top: 1px solid var(--rqd-border); z-index: 2; } .rqd-compose-actions { display: flex; align-items: center; gap: 8px; justify-content: space-between; } .rqd-compose-send { flex: 1; } /* Ghost / link-style resolve — subtle, doesn't compete with primary send button */ .rqd-btn.rqd-btn-ghost { background: transparent; border: 1px solid transparent; color: var(--rqd-text-secondary); font-weight: 500; padding: 8px 10px; } .rqd-btn.rqd-btn-ghost:hover { color: var(--rqd-primary); background: var(--rqd-primary-light); } .rqd-btn.rqd-btn-ghost:disabled { opacity: 0.5; cursor: not-allowed; } .rqd-resolve-link { font-size: 12px; white-space: nowrap; } .rqd-detail-headline { display: flex; flex-direction: column; gap: 8px; padding-bottom: 10px; border-bottom: 1px solid var(--rqd-border); } .rqd-detail-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--rqd-text); line-height: 1.35; word-break: break-word; } .rqd-detail-meta-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; font-size: 12px; color: var(--rqd-text-secondary); } .rqd-badge-muted { background: var(--rqd-bg-secondary); color: var(--rqd-text-secondary); } /* Tag chips — used on the submit form (interactive multi-select) and the detail view (readonly indicator). The per-tag color is injected via the --rqd-tag-color custom property so a single class set handles every tag. Selected state uses color-mix for a tinted background without needing a per-tag class. */ .rqd-tag-picker { display: flex; flex-wrap: wrap; gap: 6px; } .rqd-tag-chip { --rqd-tag-color: var(--rqd-text-secondary); display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--rqd-border); background: var(--rqd-bg-secondary); color: var(--rqd-text); font-size: 12px; font-family: inherit; cursor: pointer; transition: border-color 0.15s, background 0.15s, box-shadow 0.15s; } .rqd-tag-chip:hover { border-color: var(--rqd-tag-color); } .rqd-tag-chip:focus-visible { outline: 2px solid var(--rqd-primary); outline-offset: 2px; } .rqd-tag-chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--rqd-tag-color); flex-shrink: 0; } .rqd-tag-chip-selected { border-color: var(--rqd-tag-color); background: color-mix(in srgb, var(--rqd-tag-color) 15%, var(--rqd-bg-secondary)); font-weight: 600; } .rqd-tag-chip-readonly { cursor: default; } .rqd-tag-chip-readonly:hover { border-color: var(--rqd-border); } .rqd-tag-chip-add { border-style: dashed; color: var(--rqd-text-secondary); background: transparent; gap: 4px; } .rqd-tag-chip-add:hover { border-color: var(--rqd-primary); color: var(--rqd-primary); } .rqd-tag-add-inline { display: inline-flex; align-items: center; gap: 4px; } .rqd-tag-add-input { height: 28px; font-size: 12px; padding: 2px 8px; min-width: 140px; max-width: 200px; border-radius: 999px; } .rqd-tag-add-confirm, .rqd-tag-add-cancel { width: 24px; height: 24px; border-radius: 50%; border: none; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; font-family: inherit; flex-shrink: 0; transition: background 0.15s; } .rqd-tag-add-confirm { background: var(--rqd-primary); color: #fff; } .rqd-tag-add-confirm:hover { opacity: 0.9; } .rqd-tag-add-confirm:disabled { opacity: 0.5; cursor: not-allowed; } .rqd-tag-add-cancel { background: var(--rqd-bg-secondary); color: var(--rqd-text-secondary); } .rqd-tag-add-cancel:hover { background: var(--rqd-border); } .rqd-tag-add-error { display: block; margin-top: 6px; font-size: 12px; color: #ef4444; } .rqd-detail-description { display: flex; flex-direction: column; gap: 6px; } .rqd-detail-empty { color: var(--rqd-text-secondary); font-size: 13px; margin: 0 0 12px; } .rqd-compose-actions { display: flex; gap: 8px; } .rqd-resolve-inline { flex: 0; white-space: nowrap; padding: 10px 16px; } /* Chat bubble conversation */ .rqd-conversation { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; } .rqd-bubble { padding: 10px 12px; border-radius: 12px; max-width: 85%; font-size: 14px; line-height: 1.45; } .rqd-bubble-header { display: flex; align-items: center; gap: 6px; font-size: 11px; margin-bottom: 4px; opacity: 0.85; } .rqd-bubble-avatar { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; font-size: 11px; font-weight: 700; flex-shrink: 0; background: rgba(255,255,255,0.22); color: inherit; } .rqd-bubble-staff .rqd-bubble-avatar { background: var(--rqd-primary-light); color: var(--rqd-primary-dark); } .rqd-bubble-author { font-weight: 600; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rqd-bubble-staff-badge { display: inline-flex; align-items: center; padding: 0 6px; height: 16px; border-radius: 8px; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.2); color: inherit; text-transform: uppercase; letter-spacing: 0.05em; flex-shrink: 0; } .rqd-bubble-staff .rqd-bubble-staff-badge { background: var(--rqd-primary); color: #fff; } .rqd-bubble-time { margin-inline-start: auto; font-size: 11px; opacity: 0.75; white-space: nowrap; flex-shrink: 0; } .rqd-bubble-body { color: inherit; white-space: pre-wrap; word-break: break-word; } .rqd-bubble-attachments { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; padding-top: 6px; border-top: 1px solid rgba(255,255,255,0.12); } .rqd-bubble-staff .rqd-bubble-attachments { border-top-color: var(--rqd-border); } .rqd-bubble-attachment { display: flex; align-items: center; gap: 6px; padding: 4px 6px; border-radius: calc(var(--rqd-radius, 8px) / 2); background: rgba(255,255,255,0.10); font-size: 12px; } .rqd-bubble-staff .rqd-bubble-attachment { background: var(--rqd-bg); } .rqd-bubble-attachment .rqd-file-item-info { flex: 1; min-width: 0; } .rqd-bubble-attachment a { color: inherit; text-decoration: underline; font-size: 11px; flex-shrink: 0; } .rqd-bubble-attachment-icon { flex-shrink: 0; font-size: 13px; } .rqd-dropzone-compact { padding: 8px 10px; margin: 6px 0 8px; display: inline-flex; align-items: center; gap: 6px; font-size: 12px; } .rqd-dropzone-compact .rqd-dropzone-icon { font-size: 14px; } .rqd-file-item-error { border: 1px solid rgba(231,76,60,0.35); background: rgba(231,76,60,0.06); } .rqd-bubble-customer { align-self: flex-end; background: var(--rqd-primary); color: #fff; border-bottom-right-radius: 4px; } .rqd-bubble-customer .rqd-bubble-header { color: rgba(255,255,255,0.85); } .rqd-bubble-staff { align-self: flex-start; background: var(--rqd-bg-secondary); color: var(--rqd-text); border-bottom-left-radius: 4px; } :host([dir="rtl"]) .rqd-bubble-customer { align-self: flex-start; border-bottom-right-radius: 12px; border-bottom-left-radius: 4px; } :host([dir="rtl"]) .rqd-bubble-staff { align-self: flex-end; border-bottom-left-radius: 12px; border-bottom-right-radius: 4px; } /* Sidebar: hidden by default (compact mode) */ .rqd-detail-sidebar { display: none; } /* Expanded mode: two-column grid, sidebar visible */ .rqd-panel.rqd-expanded .rqd-detail { display: grid; grid-template-columns: 1fr 300px; gap: 24px; align-items: start; max-height: 100%; } .rqd-panel.rqd-expanded .rqd-detail-main { min-height: 0; } .rqd-panel.rqd-expanded .rqd-detail-main .rqd-reply-compose { position: sticky; bottom: 0; background: var(--rqd-bg); padding-top: 12px; margin-top: 8px; z-index: 1; } .rqd-panel.rqd-expanded .rqd-detail-main { padding-bottom: 20px; } /* In expanded mode: no collapsible, sidebar takes over attachments + resolve */ .rqd-panel.rqd-expanded .rqd-detail-original { border: none; background: transparent; border-radius: 0; } .rqd-panel.rqd-expanded .rqd-detail-original-summary { display: none; } .rqd-panel.rqd-expanded .rqd-detail-original-body { display: flex !important; padding: 0; } .rqd-panel.rqd-expanded .rqd-detail-attachments-inline { display: none; } .rqd-panel.rqd-expanded .rqd-resolve-link { display: none; } .rqd-panel.rqd-expanded .rqd-reply-compose { position: static; margin: 0; padding: 0; border-top: none; background: transparent; } .rqd-panel.rqd-expanded .rqd-detail-sidebar { display: flex; flex-direction: column; gap: 16px; padding: 16px; background: var(--rqd-bg-secondary); border: 1px solid var(--rqd-border); border-radius: calc(var(--rqd-radius, 8px)); position: sticky; top: 0; max-height: calc(100vh - 140px); overflow-y: auto; } .rqd-panel.rqd-expanded .rqd-detail-title { font-size: 22px; } .rqd-panel.rqd-expanded .rqd-bubble { max-width: 75%; font-size: 14.5px; } .rqd-sidebar-section { display: flex; flex-direction: column; gap: 6px; } .rqd-sidebar-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--rqd-text-secondary); } .rqd-sidebar-value { font-size: 14px; } .rqd-sidebar-text { color: var(--rqd-text); } .rqd-sidebar-mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; } .rqd-sidebar-attachments .rqd-attachment-list { margin-bottom: 0; } /* Below 900px the expanded sheet collapses back to single column */ @media (max-width: 900px) { .rqd-panel.rqd-expanded .rqd-detail { grid-template-columns: 1fr; } .rqd-panel.rqd-expanded .rqd-detail-sidebar { display: none; } .rqd-panel.rqd-expanded .rqd-detail-attachments-inline { display: flex; flex-direction: column; gap: 6px; } .rqd-panel.rqd-expanded .rqd-resolve-inline { display: inline-flex; } } .rqd-email-form { display: flex; flex-direction: column; gap: 12px; padding: 24px 0; } .rqd-email-form p { margin: 0; font-size: 14px; color: var(--rqd-text-secondary); text-align: center; } .rqd-checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--rqd-text-secondary); cursor: pointer; } .rqd-checkbox-label input[type="checkbox"] { accent-color: var(--rqd-primary); width: 16px; height: 16px; cursor: pointer; } .rqd-ticket-card { display: flex; flex-direction: column; gap: 4px; padding: 12px; background: var(--rqd-bg-secondary); border: 1px solid transparent; border-radius: calc(var(--rqd-radius, 8px) / 1.5); cursor: pointer; transition: border-color 0.15s, transform 0.1s; } .rqd-ticket-card:hover { border-color: var(--rqd-primary); transform: translateY(-1px); } .rqd-ticket-card-top { display: flex; justify-content: space-between; align-items: center; } .rqd-ticket-card-number { font-size: 12px; font-weight: 600; color: var(--rqd-text-secondary); } .rqd-ticket-card-title { font-size: 14px; font-weight: 500; color: var(--rqd-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .rqd-ticket-card-date { font-size: 11px; color: var(--rqd-text-secondary); } .rqd-ticket-list { display: flex; flex-direction: column; gap: 6px; } .rqd-loading { text-align: center; padding: 24px 0; color: var(--rqd-text-secondary); font-size: 14px; } .rqd-reply-compose { margin-top: 12px; display: flex; flex-direction: column; gap: 8px; } /* Category picker — search + scrollable tree with breadcrumb, keyboard navigable. */ .rqd-category-picker { display: flex; flex-direction: column; gap: 8px; border: 1px solid var(--rqd-border); border-radius: calc(var(--rqd-radius, 8px) / 2); background: var(--rqd-bg); overflow: hidden; } .rqd-category-picker-header { display: flex; flex-direction: column; gap: 8px; padding: 10px 10px 8px; background: var(--rqd-bg); border-bottom: 1px solid var(--rqd-border); position: sticky; top: 0; z-index: 1; } .rqd-category-search { height: 34px; font-size: 13px; padding: 6px 10px; } .rqd-category-list { display: flex; flex-direction: column; gap: 2px; padding: 8px; max-height: 220px; overflow-y: auto; overscroll-behavior: contain; } .rqd-category-item { display: flex; align-items: center; justify-content: space-between; padding: 9px 12px; background: transparent; border: 1px solid transparent; border-radius: calc(var(--rqd-radius, 8px) / 2); cursor: pointer; font-size: 14px; font-family: inherit; color: var(--rqd-text); transition: border-color 0.12s, background 0.12s; text-align: start; width: 100%; } .rqd-category-item:hover, .rqd-category-item-active { background: var(--rqd-bg-secondary); border-color: var(--rqd-primary); } .rqd-category-item-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rqd-category-item-chevron { color: var(--rqd-text-secondary); font-size: 16px; flex-shrink: 0; margin-inline-start: 8px; } .rqd-category-empty { padding: 12px; font-size: 13px; color: var(--rqd-text-secondary); text-align: center; } .rqd-category-breadcrumb { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; font-size: 12px; color: var(--rqd-text-secondary); min-height: 18px; } .rqd-category-breadcrumb-segment { display: inline-flex; align-items: center; gap: 4px; } .rqd-category-breadcrumb-crumb { background: none; border: none; color: var(--rqd-primary); cursor: pointer; font-size: 12px; font-family: inherit; padding: 2px 4px; border-radius: 4px; } .rqd-category-breadcrumb-crumb:hover { background: var(--rqd-bg-secondary); text-decoration: underline; } .rqd-category-breadcrumb-crumb[aria-current="location"] { color: var(--rqd-text); font-weight: 600; cursor: default; text-decoration: none; } .rqd-category-breadcrumb-crumb[aria-current="location"]:hover { background: transparent; text-decoration: none; } .rqd-category-breadcrumb-sep { color: var(--rqd-text-secondary); } .rqd-category-selected { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--rqd-primary-light); border-radius: calc(var(--rqd-radius, 8px) / 2); font-size: 13px; color: var(--rqd-primary-dark); margin-bottom: 4px; } .rqd-category-selected-path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; direction: ltr; /* paths read left-to-right even in RTL */ text-align: start; } .rqd-category-selected-clear { background: none; border: none; color: var(--rqd-primary-dark); cursor: pointer; font-size: 14px; padding: 0 4px; flex-shrink: 0; } .rqd-category-add-row { padding: 6px 8px 10px; border-top: 1px solid var(--rqd-border); background: var(--rqd-bg); } .rqd-category-add-btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; border: 1px dashed var(--rqd-border); background: transparent; color: var(--rqd-text-secondary); font-size: 13px; font-family: inherit; border-radius: calc(var(--rqd-radius, 8px) / 2); cursor: pointer; width: 100%; justify-content: center; transition: border-color 0.12s, color 0.12s; } .rqd-category-add-btn:hover { border-color: var(--rqd-primary); color: var(--rqd-primary); } .rqd-category-add-inline { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; } .rqd-category-add-input { flex: 1; min-width: 0; height: 36px; font-size: 13px; } .rqd-category-add-confirm, .rqd-category-add-cancel { width: 32px; height: 32px; border-radius: 50%; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 14px; font-family: inherit; transition: background 0.15s; flex-shrink: 0; } .rqd-category-add-confirm { background: var(--rqd-primary); color: #fff; } .rqd-category-add-confirm:hover { opacity: 0.9; } .rqd-category-add-confirm:disabled { opacity: 0.5; cursor: not-allowed; } .rqd-category-add-cancel { background: var(--rqd-bg-secondary); color: var(--rqd-text-secondary); } .rqd-category-add-cancel:hover { background: var(--rqd-border); } .rqd-category-add-error { width: 100%; font-size: 12px; color: #ef4444; margin-top: 2px; } .rqd-diag-master { display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; background: var(--rqd-bg-secondary); border: 1px solid var(--rqd-border); border-radius: calc(var(--rqd-radius, 8px) / 2); cursor: pointer; font-family: inherit; } .rqd-diag-master input[type="checkbox"] { accent-color: var(--rqd-primary); width: 16px; height: 16px; margin-top: 1px; flex-shrink: 0; } .rqd-diag-master-label { display: flex; flex-direction: column; gap: 2px; min-width: 0; } .rqd-diag-master-title { font-size: 13px; font-weight: 500; color: var(--rqd-text); } .rqd-diag-master-hint { font-size: 11px; color: var(--rqd-text-secondary); line-height: 1.35; } .rqd-diag-details-toggle { display: flex; align-items: center; justify-content: space-between; gap: 8px; background: transparent; border: none; cursor: pointer; color: var(--rqd-text-secondary); font-family: inherit; font-size: 12px; padding: 8px 12px 0; margin: 0; width: 100%; } .rqd-diag-details-toggle:hover { color: var(--rqd-primary); } .rqd-diag-preview { list-style: none; margin: 6px 0 0; padding: 6px 12px; display: flex; flex-direction: column; gap: 4px; } .rqd-diag-preview-item { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--rqd-text-secondary); } .rqd-diag-preview-label { flex: 0 0 auto; } .rqd-diag-preview-value { color: var(--rqd-text); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; margin-inline-start: auto; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rqd-auth-btn { background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.4); color: #fff; cursor: pointer; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500; font-family: inherit; transition: background 0.15s; white-space: nowrap; } .rqd-auth-btn:hover { background: rgba(255,255,255,0.3); } .rqd-color-presets { display: flex; gap: 8px; flex-wrap: wrap; } .rqd-color-preset { width: 28px; height: 28px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; transition: transform 0.15s, box-shadow 0.15s; padding: 0; } .rqd-color-preset:hover { transform: scale(1.15); } .rqd-color-preset.rqd-active { box-shadow: 0 0 0 3px var(--rqd-bg), 0 0 0 5px currentColor; } .rqd-corner-picker { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; max-width: 200px; } .rqd-corner-tile { position: relative; aspect-ratio: 3 / 2; background: var(--rqd-bg-secondary); border: 1px solid var(--rqd-border); border-radius: calc(var(--rqd-radius, 8px) / 2); cursor: pointer; padding: 0; transition: border-color 0.15s, background 0.15s; } .rqd-corner-tile:hover { border-color: var(--rqd-primary); } .rqd-corner-tile.rqd-active { border-color: var(--rqd-primary); background: var(--rqd-primary-light); } .rqd-corner-tile::after { content: ''; position: absolute; width: 10px; height: 10px; border-radius: 50%; background: var(--rqd-primary); } .rqd-corner-tile.rqd-bottom-end::after { inset-block-end: 6px; inset-inline-end: 6px; } .rqd-corner-tile.rqd-bottom-start::after { inset-block-end: 6px; inset-inline-start: 6px; } .rqd-corner-tile.rqd-top-end::after { inset-block-start: 6px; inset-inline-end: 6px; } .rqd-corner-tile.rqd-top-start::after { inset-block-start: 6px; inset-inline-start: 6px; } .rqd-contained.rqd-fab { position: absolute; } .rqd-contained.rqd-panel { position: absolute; } @media (max-width: 440px) { .rqd-panel { width: calc(100vw - 24px); inset-inline-end: 12px !important; inset-inline-start: 12px !important; } .rqd-panel.rqd-bottom-end, .rqd-panel.rqd-bottom-start { inset-block-end: 76px; inset-block-start: auto; } .rqd-panel.rqd-top-end, .rqd-panel.rqd-top-start { inset-block-start: 76px; inset-block-end: auto; } .rqd-fab.rqd-bottom-end { inset-block-end: 12px; inset-inline-end: 12px; } .rqd-fab.rqd-bottom-start { inset-block-end: 12px; inset-inline-start: 12px; } .rqd-fab.rqd-top-end { inset-block-start: 12px; inset-inline-end: 12px; } .rqd-fab.rqd-top-start { inset-block-start: 12px; inset-inline-start: 12px; } } `; //#endregion //#region src/position.ts const LEGACY_MAP = { "bottom-right": "bottom-end", "bottom-left": "bottom-start" }; const CANONICAL = new Set([ "bottom-end", "bottom-start", "top-end", "top-start" ]); let legacyWarned = false; function normalizePosition(input) { if (!input) return "bottom-end"; if (CANONICAL.has(input)) return input; if (input in LEGACY_MAP) { if (!legacyWarned && typeof console !== "undefined") { legacyWarned = true; console.warn(`[reqdesk] position="${input}" is deprecated — use "${LEGACY_MAP[input]}" (logical side).`); } return LEGACY_MAP[input]; } return "bottom-end"; } function positionSide(pos) { return pos.endsWith("-end") ? "end" : "start"; } //#endregion //#region src/ui/fab-icons.ts /** Path data for each preset. Values are the `d` attribute of an SVG path. */ const FAB_ICON_PATHS = { chat: "M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z", help: "M11.5 2C6.81 2 3 5.81 3 10.5S6.81 19 11.5 19h.5v3c4.86-2.34 8-7 8-11.5C20 5.81 16.19 2 11.5 2zm1 14.5h-2v-2h2v2zm0-3.5h-2c0-3.25 3-3 3-5 0-1.1-.9-2-2-2s-2 .9-2 2h-2c0-2.21 1.79-4 4-4s4 1.79 4 4c0 2.5-3 2.75-3 5z" }; /** Close glyph shown when the widget is open. Not part of the public preset surface. */ const FAB_CLOSE_PATH = "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"; const DEFAULT_FAB_ICON = "help"; /** * Resolves a user-provided `fabIcon` value to an SVG path string. Accepts a preset key, * a raw SVG path (`M…` string), or `undefined`/unknown (falls back to the default). * Paths are surfaced via `` inside a 24×24 viewBox, so custom shapes should * target that coordinate space. */ function resolveFabIconPath(fabIcon) { if (!fabIcon) return FAB_ICON_PATHS[DEFAULT_FAB_ICON]; if (fabIcon in FAB_ICON_PATHS) return FAB_ICON_PATHS[fabIcon]; if (/^[Mm]\s*[-\d.]/.test(fabIcon.trim())) return fabIcon; return FAB_ICON_PATHS[DEFAULT_FAB_ICON]; } //#endregion //#region src/storage.ts const STORAGE_PREFIX = "reqdesk_"; const PREFS_KEY_PREFIX = `${STORAGE_PREFIX}prefs_`; const KNOWN_PREF_KEYS = [ "language", "themeMode", "primaryColor", "position", "display" ]; const unknownKeyWarnings = /* @__PURE__ */ new Set(); function filterKnownKeys(input, sourceLabel) { if (!input || typeof input !== "object" || Array.isArray(input)) return {}; const out = {}; const knownSet = new Set(KNOWN_PREF_KEYS); for (const [key, value] of Object.entries(input)) if (knownSet.has(key)) out[key] = value; else if (!unknownKeyWarnings.has(key)) { unknownKeyWarnings.add(key); if (typeof console !== "undefined" && typeof console.warn === "function") console.warn(`[reqdesk] preferences: unknown key "${key}" (from ${sourceLabel}); ignoring`); } return out; } function getStorage() { try { const s = window.localStorage; s.setItem("__test__", "1"); s.removeItem("__test__"); return s; } catch { try { return window.sessionStorage; } catch { return null; } } } function saveTrackingToken(projectSlug, token) { const storage = getStorage(); if (!storage) return; const key = `${STORAGE_PREFIX}tokens_${projectSlug}`; try { const existing = JSON.parse(storage.getItem(key) ?? "[]"); if (!existing.includes(token)) { existing.push(token); storage.setItem(key, JSON.stringify(existing)); } } catch { storage.setItem(key, JSON.stringify([token])); } } function getTrackingTokens(projectSlug) { const storage = getStorage(); if (!storage) return []; const key = `${STORAGE_PREFIX}tokens_${projectSlug}`; try { return JSON.parse(storage.getItem(key) ?? "[]"); } catch { return []; } } function saveWidgetConfig(apiKey, config) { const storage = getStorage(); if (!storage) return; const key = `${STORAGE_PREFIX}config_${apiKey}`; try { const existing = loadWidgetConfig(apiKey); const merged = { ...existing, ...config }; if (config.theme && existing?.theme) merged.theme = { ...existing.theme, ...config.theme }; storage.setItem(key, JSON.stringify(merged)); } catch {} } function saveWidgetEmail(apiKey, email) { const storage = getStorage(); if (!storage) return; try { storage.setItem(`${STORAGE_PREFIX}email_${apiKey}`, email); } catch {} } function loadWidgetEmail(apiKey) { const storage = getStorage(); if (!storage) return null; try { return storage.getItem(`${STORAGE_PREFIX}email_${apiKey}`); } catch { return null; } } function clearWidgetEmail(apiKey) { const storage = getStorage(); if (!storage) return; try { storage.removeItem(`${STORAGE_PREFIX}email_${apiKey}`); } catch {} } function saveSelectedProject(apiKey, userIdentifier, projectId) { const storage = getStorage(); if (!storage) return; try { storage.setItem(`${STORAGE_PREFIX}project_${apiKey}_${userIdentifier}`, projectId); } catch {} } function loadSelectedProject(apiKey, userIdentifier) { const storage = getStorage(); if (!storage) return null; try { return storage.getItem(`${STORAGE_PREFIX}project_${apiKey}_${userIdentifier}`); } catch { return null; } } function loadWidgetConfig(apiKey) { const storage = getStorage(); if (!storage) return null; const key = `${STORAGE_PREFIX}config_${apiKey}`; try { const raw = storage.getItem(key); if (!raw) return null; return JSON.parse(raw); } catch { return null; } } function loadPreferencesFromStorage(apiKey) { const storage = getStorage(); if (!storage) return {}; try { const raw = storage.getItem(PREFS_KEY_PREFIX + apiKey); if (!raw) return {}; return filterKnownKeys(JSON.parse(raw), "localStorage"); } catch { return {}; } } function resolvePreferences(input) { const initial = filterKnownKeys(input.initialPreferences ?? {}, "initialPreferences"); const stored = loadPreferencesFromStorage(input.apiKey); const user = filterKnownKeys(input.userPreferences ?? {}, "userPreferences"); return { ...initial, ...stored, ...user }; } //#endregion //#region src/registry.ts const ACTION_ID_PATTERN = /^[a-z][a-z0-9-]{0,63}$/; function defaultDisplayMode() { return { mode: "popover", side: "end", dismissOnBackdrop: true }; } function resolveModeFromConfig(cfg) { if (!cfg || !cfg.mode) return null; const mode = cfg.mode; return { mode, side: cfg.side ?? "end", width: cfg.width ?? (mode === "side-sheet" ? "420px" : void 0), height: cfg.height ?? (mode === "bottom-sheet" ? "55vh" : void 0), dismissOnBackdrop: cfg.dismissOnBackdrop ?? true }; } var WidgetRegistry = class { snapshot; listeners = /* @__PURE__ */ new Set(); eventListeners = /* @__PURE__ */ new Map(); activatingElementRef = null; menuCloseOnAction = true; hostOnPreferencesChange; initialPreferences = {}; configDisplay; constructor() { this.snapshot = { isOpen: false, currentView: "home", currentDisplayMode: defaultDisplayMode(), actions: [], preferences: {}, activeActionId: null, previousDisplayMode: null, user: null, projectId: "_current", locale: "en" }; this.getSnapshot = this.getSnapshot.bind(this); this.subscribe = this.subscribe.bind(this); this.on = this.on.bind(this); this.off = this.off.bind(this); this.open = this.open.bind(this); this.close = this.close.bind(this); this.toggle = this.toggle.bind(this); this.openMenu = this.openMenu.bind(this); this.openAction = this.openAction.bind(this); this.setDisplayMode = this.setDisplayMode.bind(this); this.setPreferences = this.setPreferences.bind(this); this.getPreferences = this.getPreferences.bind(this); this.addAction = this.addAction.bind(this); this.removeAction = this.removeAction.bind(this); this.recordActivator = this.recordActivator.bind(this); this.restoreFocus = this.restoreFocus.bind(this); } getSnapshot() { return this.snapshot; } subscribe(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; } updateSnapshot(partial) { this.snapshot = { ...this.snapshot, ...partial }; this.emitChange(); } emitChange() { for (const listener of this.listeners) try { listener(this.snapshot); } catch {} } on(event, cb) { let set = this.eventListeners.get(event); if (!set) { set = /* @__PURE__ */ new Set(); this.eventListeners.set(event, set); } set.add(cb); return () => { set.delete(cb); }; } off(event, cb) { this.eventListeners.get(event)?.delete(cb); } emit(event, data) { const set = this.eventListeners.get(event); if (!set) return; for (const cb of set) try { cb(data); } catch {} } emitEvent(event, data) { this.emit(event, data); } emitError(code, message, actionId, cause) { const err = { code, message }; if (actionId !== void 0) err.actionId = actionId; if (cause !== void 0) err.cause = cause; this.emit("error", err); } setContext(ctx) { const partial = {}; if (ctx.user !== void 0) partial.user = ctx.user; if (ctx.projectId !== void 0) partial.projectId = ctx.projectId; if (ctx.locale !== void 0) partial.locale = ctx.locale; if (Object.keys(partial).length > 0) this.updateSnapshot(partial); } setMenuCloseOnAction(value) { this.menuCloseOnAction = value; } setHostPreferencesCallback(cb) { this.hostOnPreferencesChange = cb; } setApiKeyForStorage(_apiKey) {} setInitialPreferences(initial) { this.initialPreferences = initial; } setConfigDisplay(cfg) { this.configDisplay = cfg; if (this.snapshot.activeActionId === null) this.updateSnapshot({ currentDisplayMode: this.resolveEffectiveDisplayMode() }); } initPreferencesSnapshot(preferences) { this.updateSnapshot({ preferences, currentDisplayMode: this.resolveEffectiveDisplayMode(preferences) }); } hasInitialPreferences() { return Object.keys(this.initialPreferences).length > 0; } getInitialPreferences() { return this.initialPreferences; } normalizeAction(input) { if (!input || typeof input !== "object") { this.emitError("action-missing-handler", "Action input must be an object"); return null; } const id = input.id; if (!id || typeof id !== "string" || !ACTION_ID_PATTERN.test(id)) { this.emitError("action-missing-handler", `Action id "${String(id)}" is invalid. Expected pattern ^[a-z][a-z0-9-]{0,63}$`, typeof id === "string" ? id : void 0); return null; } if (this.snapshot.actions.some((a) => a.id === id)) { this.emitError("action-id-conflict", `Action "${id}" is already registered`, id); return null; } const hasHandler = typeof input.onClick === "function"; const hasTrigger = !!input.trigger; if (!hasHandler && !hasTrigger) { this.emitError("action-missing-handler", `Action "${id}" requires onClick or trigger`, id); return null; } let trigger = input.trigger; if (hasHandler && hasTrigger) { if (typeof console !== "undefined" && typeof console.debug === "function") console.debug(`[reqdesk] action "${id}" has both onClick and trigger; handler takes precedence (trigger ignored)`); trigger = void 0; } const section = input.section ?? "bottom"; return { id, label: input.label, description: input.description, icon: input.icon, section, anchor: input.anchor, closeOnClick: input.closeOnClick, onClick: input.onClick, trigger, visible: input.visible, badge: input.badge ?? null, display: input.display }; } addAction(input) { const normalized = this.normalizeAction(input); if (!normalized) return () => {}; const nextActions = this.orderActions([...this.snapshot.actions, normalized]); this.updateSnapshot({ actions: nextActions }); return () => { this.removeAction(normalized.id); }; } removeAction(id) { const nextActions = this.snapshot.actions.filter((a) => a.id !== id); if (nextActions.length !== this.snapshot.actions.length) this.updateSnapshot({ actions: nextActions }); } orderActions(actions) { return actions.slice(); } resolveRenderOrder(builtInIds) { const result = []; const custom = this.snapshot.actions; const placed = /* @__PURE__ */ new Set(); const insertAnchored = (anchorId, slotIndex) => { const before = custom.filter((a) => !placed.has(a.id) && a.anchor?.before === anchorId); for (const a of before) { result.splice(slotIndex++, 0, { kind: "custom", id: a.id }); placed.add(a.id); } }; const appendAfterAnchored = (anchorId) => { const after = custom.filter((a) => !placed.has(a.id) && !a.anchor?.before && a.anchor?.after === anchorId); for (const a of after) { result.push({ kind: "custom", id: a.id }); placed.add(a.id); } }; for (const builtInId of builtInIds) { insertAnchored(builtInId, result.length); result.push({ kind: "built-in", id: builtInId }); appendAfterAnchored(builtInId); } const unplacedTop = []; const unplacedBottom = []; for (const a of custom) { if (placed.has(a.id)) continue; if (a.section === "top") unplacedTop.push(a); else unplacedBottom.push(a); } for (let i = 0; i < unplacedTop.length; i++) { result.splice(i, 0, { kind: "custom", id: unplacedTop[i].id }); placed.add(unplacedTop[i].id); } for (const a of unplacedBottom) { result.push({ kind: "custom", id: a.id }); placed.add(a.id); } return result; } getActionById(id) { return this.snapshot.actions.find((a) => a.id === id); } setBadge(actionId, value) { const idx = this.snapshot.actions.findIndex((a) => a.id === actionId); if (idx === -1) return; const nextActions = this.snapshot.actions.slice(); nextActions[idx] = { ...nextActions[idx], badge: value }; this.updateSnapshot({ actions: nextActions }); } open() { if (this.snapshot.isOpen) return; this.updateSnapshot({ isOpen: true }); this.emit("open"); } close() { if (!this.snapshot.isOpen) return; const prev = this.snapshot.previousDisplayMode; const partial = { isOpen: false, activeActionId: null, previousDisplayMode: null, currentView: "home" }; if (prev) { partial.currentDisplayMode = prev; this.emit("display-mode:changed", { mode: prev.mode, side: prev.side, reason: "per-action-restore" }); } this.updateSnapshot(partial); this.restoreFocus(); this.emit("close"); } toggle() { if (this.snapshot.isOpen) this.close(); else this.open(); } openMenu() { this.updateSnapshot({ currentView: "home" }); if (!this.snapshot.isOpen) this.open(); this.emit("menu:opened"); } openAction(id, input) { const action = this.getActionById(id); if (!action) { this.emitError("action-not-found", `Action "${id}" is not registered`, id); return; } if (!this.snapshot.isOpen) this.open(); if (action.display) { const override = resolveModeFromConfig(action.display); if (override) { this.updateSnapshot({ previousDisplayMode: this.snapshot.previousDisplayMode ?? this.snapshot.currentDisplayMode, currentDisplayMode: override, activeActionId: action.id }); this.emit("display-mode:changed", { mode: override.mode, side: override.side, reason: "per-action" }); } } if (action.onClick) this.invokeHandler(action, input); else if (action.trigger) this.dispatchTrigger(action, input); } invokeHandler(action, input) { let keepOpenFlag = false; const ctx = { actionId: action.id, input, close: () => this.close(), keepOpen: () => { keepOpenFlag = true; }, setBadge: (v) => this.setBadge(action.id, v), setLoading: (_loading) => {}, user: this.snapshot.user, projectId: this.snapshot.projectId, locale: this.snapshot.locale, openAction: (nextId, nextInput) => this.openAction(nextId, nextInput) }; this.emit("action:triggered", { actionId: action.id, viaTrigger: false, input }); try { const ret = action.onClick(ctx); if (ret && typeof ret.then === "function") ret.then(() => {}, (cause) => { this.emitError("action-handler-failed", `Handler for "${action.id}" rejected`, action.id, cause); }); } catch (cause) { this.emitError("action-handler-failed", `Handler for "${action.id}" threw`, action.id, cause); } queueMicrotask(() => this.applyCloseBehavior(action, keepOpenFlag)); } applyCloseBehavior(action, keepOpenFlag) { if (keepOpenFlag) return; if (action.closeOnClick ?? this.menuCloseOnAction) this.close(); } dispatchTrigger(action, input) { const trigger = action.trigger; this.emit("action:triggered", { actionId: action.id, viaTrigger: true, input }); switch (trigger.kind) { case "custom-event": this.resolveCustomEvent(trigger, input); break; case "call-global": this.resolveCallGlobal(action.id, trigger, input); break; case "url": this.resolveUrl(trigger, input); break; } queueMicrotask(() => this.applyCloseBehavior(action, false)); } resolveCustomEvent(trigger, input) { const originalDetail = trigger.detail ?? void 0; let detail; if (input && typeof input === "object" && !Array.isArray(input)) detail = { ...originalDetail ?? {}, ...input }; else if (input !== void 0) detail = originalDetail ? { ...originalDetail, input } : { input }; else detail = originalDetail; if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent(trigger.name, { detail })); } resolveCallGlobal(actionId, trigger, input) { if (typeof window === "undefined") return; const parts = trigger.path.split("."); let target = window; for (const segment of parts) if (target && typeof target === "object" && segment in target) target = target[segment]; else { this.emitError("action-trigger-not-found", `Global path "${trigger.path}" did not resolve`, actionId); return; } if (typeof target !== "function") { this.emitError("action-trigger-not-found", `Global path "${trigger.path}" did not resolve to a function`, actionId); return; } const args = trigger.args ? trigger.args.slice() : []; if (input !== void 0) args[0] = input; try { target(...args); } catch (cause) { this.emitError("action-handler-failed", `Global "${trigger.path}" threw`, actionId, cause); } } resolveUrl(trigger, input) { if (typeof window === "undefined") return; let href = trigger.href; if (input !== void 0) try { const params = new URLSearchParams(); if (input && typeof input === "object") for (const [k, v] of Object.entries(input)) params.set(k, String(v)); else params.set("input", String(input)); href += (href.includes("?") ? "&" : "?") + params.toString(); } catch {} if ((trigger.target ?? "_self") === "_blank") window.open(href, "_blank", "noopener,noreferrer"); else window.location.href = href; } setDisplayMode(mode, side) { const resolved = resolveModeFromConfig({ mode, side }) ?? defaultDisplayMode(); const isInOverride = this.snapshot.activeActionId !== null; const partial = { currentDisplayMode: resolved }; if (isInOverride) partial.previousDisplayMode = resolved; this.updateSnapshot(partial); this.emit("display-mode:changed", { mode: resolved.mode, side: resolved.side, reason: "programmatic" }); } resolveEffectiveDisplayMode(preferences) { const prefs = preferences ?? this.snapshot.preferences; let resolved = defaultDisplayMode(); const fromConfig = resolveModeFromConfig(this.configDisplay); if (fromConfig) resolved = fromConfig; const fromPrefs = resolveModeFromConfig(prefs.display); if (fromPrefs) resolved = fromPrefs; return resolved; } getPreferences() { return this.snapshot.preferences; } setPreferences(partial) { const next = { ...this.snapshot.preferences, ...partial }; this.updateSnapshot({ preferences: next, currentDisplayMode: this.snapshot.activeActionId === null ? this.resolveEffectiveDisplayMode(next) : this.snapshot.currentDisplayMode }); this.emit("prefs:changed", next); if (this.hostOnPreferencesChange) try { this.hostOnPreferencesChange(next); } catch {} } resetToInitialPreferences() { this.setPreferences(this.initialPreferences); } setView(view) { this.updateSnapshot({ currentView: view }); } recordActivator(el) { if (!el) { this.activatingElementRef = null; return; } if (typeof WeakRef === "function") this.activatingElementRef = new WeakRef(el); else this.activatingElementRef = { deref: () => el }; } restoreFocus() { const el = this.activatingElementRef?.deref(); this.activatingElementRef = null; if (el && typeof el.focus === "function" && el.isConnected !== false) try { el.focus(); } catch {} } destroy() { this.listeners.clear(); this.eventListeners.clear(); this.activatingElementRef = null; } }; let globalRegistry = null; function getRegistry() { if (!globalRegistry) globalRegistry = new WidgetRegistry(); return globalRegistry; } //#endregion //#region src/ui/trigger.css.ts const STYLE_ID = "rqd-trigger-styles"; const TRIGGER_CSS = ` .rqd-trigger { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 8px 16px; border-radius: var(--rqd-radius, 6px); border: 1px solid transparent; font-family: var(--rqd-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); font-size: 14px; font-weight: 600; line-height: 1.2; cursor: pointer; background: var(--rqd-primary, #0F5E56); color: #fff; transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease; user-select: none; } .rqd-trigger:hover { filter: brightness(1.06); } .rqd-trigger:active { transform: translateY(1px); } .rqd-trigger:focus-visible { outline: 2px solid var(--rqd-primary, #0F5E56); outline-offset: 2px; } .rqd-trigger svg { width: 18px; height: 18px; flex-shrink: 0; fill: currentColor; } .rqd-trigger--pill { border-radius: 999px; padding: 8px 18px; } .rqd-trigger--ghost { background: transparent; color: var(--rqd-primary, #0F5E56); border-color: var(--rqd-primary, #0F5E56); } .rqd-trigger--ghost:hover { background: color-mix(in srgb, var(--rqd-primary, #0F5E56) 12%, transparent); } .rqd-trigger--icon { padding: 8px; width: 36px; height: 36px; border-radius: 999px; } .rqd-trigger--icon .rqd-trigger-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } `; function injectTriggerStyles() { if (typeof document === "undefined") return; if (document.getElementById(STYLE_ID)) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = TRIGGER_CSS; document.head.appendChild(style); } //#endregion Object.defineProperty(exports, "FAB_CLOSE_PATH", { enumerable: true, get: function() { return FAB_CLOSE_PATH; } }); Object.defineProperty(exports, "WidgetFetchError", { enumerable: true, get: function() { return WidgetFetchError; } }); Object.defineProperty(exports, "ar", { enumerable: true, get: function() { return ar; } }); Object.defineProperty(exports, "clearWidgetEmail", { enumerable: true, get: function() { return clearWidgetEmail; } }); Object.defineProperty(exports, "closeTicket", { enumerable: true, get: function() { return closeTicket; } }); Object.defineProperty(exports, "configureWidgetClient", { enumerable: true, get: function() { return configureWidgetClient; } }); Object.defineProperty(exports, "createCategory", { enumerable: true, get: function() { return createCategory; } }); Object.defineProperty(exports, "createTag", { enumerable: true, get: function() { return createTag; } }); Object.defineProperty(exports, "en", { enumerable: true, get: function() { return en; } }); Object.defineProperty(exports, "getCategories", { enumerable: true, get: function() { return getCategories; } }); Object.defineProperty(exports, "getRegistry", { enumerable: true, get: function() { return getRegistry; } }); Object.defineProperty(exports, "getTicketDetail", { enumerable: true, get: function() { return getTicketDetail; } }); Object.defineProperty(exports, "getTrackingTokens", { enumerable: true, get: function() { return getTrackingTokens; } }); Object.defineProperty(exports, "getWidgetConfig", { enumerable: true, get: function() { return getWidgetConfig; } }); Object.defineProperty(exports, "getWidgetStyles", { enumerable: true, get: function() { return getWidgetStyles; } }); Object.defineProperty(exports, "injectTriggerStyles", { enumerable: true, get: function() { return injectTriggerStyles; } }); Object.defineProperty(exports, "listMyTickets", { enumerable: true, get: function() { return listMyTickets; } }); Object.defineProperty(exports, "listProjectTags", { enumerable: true, get: function() { return listProjectTags; } }); Object.defineProperty(exports, "listProjects", { enumerable: true, get: function() { return listProjects; } }); Object.defineProperty(exports, "loadSelectedProject", { enumerable: true, get: function() { return loadSelectedProject; } }); Object.defineProperty(exports, "loadWidgetConfig", { enumerable: true, get: function() { return loadWidgetConfig; } }); Object.defineProperty(exports, "loadWidgetEmail", { enumerable: true, get: function() { return loadWidgetEmail; } }); Object.defineProperty(exports, "normalizePosition", { enumerable: true, get: function() { return normalizePosition; } }); Object.defineProperty(exports, "positionSide", { enumerable: true, get: function() { return positionSide; } }); Object.defineProperty(exports, "resolveFabIconPath", { enumerable: true, get: function() { return resolveFabIconPath; } }); Object.defineProperty(exports, "resolvePreferences", { enumerable: true, get: function() { return resolvePreferences; } }); Object.defineProperty(exports, "resolveWidgetUser", { enumerable: true, get: function() { return resolveWidgetUser; } }); Object.defineProperty(exports, "saveSelectedProject", { enumerable: true, get: function() { return saveSelectedProject; } }); Object.defineProperty(exports, "saveTrackingToken", { enumerable: true, get: function() { return saveTrackingToken; } }); Object.defineProperty(exports, "saveWidgetConfig", { enumerable: true, get: function() { return saveWidgetConfig; } }); Object.defineProperty(exports, "saveWidgetEmail", { enumerable: true, get: function() { return saveWidgetEmail; } }); Object.defineProperty(exports, "setOidcTokenProvider", { enumerable: true, get: function() { return setOidcTokenProvider; } }); Object.defineProperty(exports, "setWidgetCustomer", { enumerable: true, get: function() { return setWidgetCustomer; } }); Object.defineProperty(exports, "submitReply", { enumerable: true, get: function() { return submitReply; } }); Object.defineProperty(exports, "submitTicket", { enumerable: true, get: function() { return submitTicket; } }); Object.defineProperty(exports, "submitTrackingReply", { enumerable: true, get: function() { return submitTrackingReply; } }); Object.defineProperty(exports, "themeToStyle", { enumerable: true, get: function() { return themeToStyle; } }); Object.defineProperty(exports, "themeToVars", { enumerable: true, get: function() { return themeToVars; } }); Object.defineProperty(exports, "trackTicket", { enumerable: true, get: function() { return trackTicket; } }); Object.defineProperty(exports, "uploadAttachment", { enumerable: true, get: function() { return uploadAttachment; } }); Object.defineProperty(exports, "uploadReplyAttachment", { enumerable: true, get: function() { return uploadReplyAttachment; } });