import type { PlatformWebModule } from "../types"; export type HelpContextKind = "page" | "field" | "action" | "dialog" | "interface"; export type HelpContext = { id: string; title: string; route: string; kind: HelpContextKind; moduleId?: string; parentId?: string; parentTitle?: string; documentationTopicId?: string; documentationType?: "user" | "admin"; }; type ContextDefinition = Omit; const campaignSectionContexts: Record = { data: { id: "campaign.settings", title: "i18n:govoplan-core.campaign_settings.efffec26", moduleId: "campaigns" }, campaign: { id: "campaign.settings", title: "i18n:govoplan-core.campaign_settings.efffec26", moduleId: "campaigns" }, fields: { id: "campaign.fields", title: "i18n:govoplan-core.campaign_fields.969e7d80", moduleId: "campaigns" }, template: { id: "campaign.template", title: "i18n:govoplan-core.template.3ec1ae06", moduleId: "campaigns" }, files: { id: "campaign.attachments", title: "i18n:govoplan-core.attachments.6771ade6", moduleId: "campaigns" }, attachments: { id: "campaign.attachments", title: "i18n:govoplan-core.attachments.6771ade6", moduleId: "campaigns" }, recipients: { id: "campaign.recipients", title: "i18n:govoplan-core.sender_recipients.922c6d24", moduleId: "campaigns" }, "recipient-data": { id: "campaign.recipient-data", title: "i18n:govoplan-core.recipient_data.c2baaf10", moduleId: "campaigns" }, "mail-settings": { id: "campaign.server-settings", title: "i18n:govoplan-core.server_settings.28af3cc7", moduleId: "campaigns" }, "server-settings": { id: "campaign.server-settings", title: "i18n:govoplan-core.server_settings.28af3cc7", moduleId: "campaigns" }, mail: { id: "campaign.server-settings", title: "i18n:govoplan-core.server_settings.28af3cc7", moduleId: "campaigns" }, "global-settings": { id: "campaign.global-settings", title: "i18n:govoplan-core.policies.8d611849", moduleId: "campaigns" }, settings: { id: "campaign.global-settings", title: "i18n:govoplan-core.policies.8d611849", moduleId: "campaigns" }, review: { id: "campaign.review-send", title: "i18n:govoplan-core.review_send.1627617d", moduleId: "campaigns" }, send: { id: "campaign.review-send", title: "i18n:govoplan-core.review_send.1627617d", moduleId: "campaigns" }, report: { id: "campaign.report", title: "i18n:govoplan-core.report.ee45c303", moduleId: "campaigns" }, reports: { id: "campaign.report", title: "i18n:govoplan-core.report.ee45c303", moduleId: "campaigns" }, audit: { id: "campaign.audit", title: "i18n:govoplan-core.audit_log.3cfc5f1c", moduleId: "campaigns" }, json: { id: "campaign.json", title: "i18n:govoplan-core.json.031a4e76", moduleId: "campaigns" } }; const topLevelContexts: Record = { dashboard: { id: "dashboard.page", title: "i18n:govoplan-core.dashboard.d87f47b4", moduleId: "dashboard" }, campaigns: { id: "campaigns.list", title: "i18n:govoplan-core.campaigns.01a23a28", moduleId: "campaigns" }, templates: { id: "templates.page", title: "i18n:govoplan-core.templates.f25b700e", moduleId: "templates" }, files: { id: "files.list", title: "i18n:govoplan-core.files.6ce6c512", moduleId: "files" }, mail: { id: "mail.list", title: "i18n:govoplan-mail.mail.92379cbb", moduleId: "mail" }, "address-book": { id: "addresses.page", title: "i18n:govoplan-core.address_book.f6327f59", moduleId: "addresses" }, settings: { id: "app.settings", title: "i18n:govoplan-core.settings.c7f73bb5", moduleId: "core", documentationType: "user" }, admin: { id: "app.admin", title: "i18n:govoplan-core.admin.4e7afebc", moduleId: "admin", documentationType: "admin" } }; export function helpContextForPathname( pathname: string, search = "", modules: readonly PlatformWebModule[] = [] ): HelpContext { const route = pathname || "/"; const routeWithSearch = `${route}${search}`; const segments = route.split("/").filter(Boolean); if (segments[0] === "settings" || segments[0] === "admin") { const area = segments[0]; const requestedSection = new URLSearchParams(search).get("section"); const section = requestedSection || (area === "admin" ? "overview" : "interface"); if (area === "settings" && section === "mail-profiles") { return pageContext( { id: "mail.profiles", title: "i18n:govoplan-core.mail_profiles.8a8018b7", moduleId: "mail" }, routeWithSearch, modules ); } const surfaceContext = sectionContext(modules, section, area); if (surfaceContext) return pageContext(surfaceContext, routeWithSearch, modules); const fallback = topLevelContexts[area]; return pageContext({ ...fallback, id: `${fallback.id}.${stableSlug(section)}`, title: humanize(section), documentationType: area === "admin" ? "admin" : "user" }, routeWithSearch, modules); } if (segments[0] === "campaigns" && segments[1]) { if (segments[1] === "queue") { return pageContext({ id: "campaign.operator-queue", title: "Operator queue", moduleId: "campaigns" }, routeWithSearch, modules); } if (segments[1] === "reports") { return pageContext({ id: "campaign.report", title: "i18n:govoplan-core.reports.88bc3fe3", moduleId: "campaigns" }, routeWithSearch, modules); } if (!segments[2]) { return pageContext({ id: "campaign.overview", title: "i18n:govoplan-core.campaign_overview.43c3d159", moduleId: "campaigns" }, routeWithSearch, modules); } if (segments[2] === "wizard") { const step = segments[3] || "create"; return pageContext({ id: `campaign.wizard.${stableSlug(step)}`, title: `${humanize(step)} wizard`, moduleId: "campaigns" }, routeWithSearch, modules); } const context = campaignSectionContexts[segments[2]]; if (context) return pageContext(context, routeWithSearch, modules); return pageContext({ id: "campaign.workspace", title: "i18n:govoplan-core.campaign_workspace.c345580f", moduleId: "campaigns" }, routeWithSearch, modules); } if (segments.length <= 1) { const context = topLevelContexts[segments[0] || "campaigns"]; if (context) return pageContext(context, routeWithSearch, modules); } const matchedRoute = moduleRouteContext(route, modules); if (matchedRoute) return pageContext(matchedRoute, routeWithSearch, modules); const root = stableSlug(segments[0] || "application"); return pageContext({ id: root === "application" ? "app.general" : `${root}.page`, title: root === "application" ? "i18n:govoplan-core.application.b291beb8" : humanize(segments[segments.length - 1] || root), moduleId: root === "application" ? "core" : root }, routeWithSearch, modules); } export function helpContextForTarget( base: HelpContext, target: EventTarget | null, modules: readonly PlatformWebModule[] = [] ): HelpContext { if (typeof Element === "undefined" || !(target instanceof Element)) return base; const explicit = target.closest( "[data-help-context-id], [data-help-context], [data-help-topic-id], [data-interface-id]" ); if (explicit) { const contextId = explicit.dataset.helpContextId || explicit.dataset.helpContext; const topicId = explicit.dataset.helpTopicId; const interfaceId = explicit.dataset.interfaceId; const title = helpLabel(explicit) || base.title; const kind = helpKind(explicit, target); if (contextId || topicId || interfaceId) { return withDeclaredDocumentation(childContext(base, { id: contextId || interfaceId || topicId || base.id, title, kind, moduleId: explicit.dataset.helpModuleId || base.moduleId, documentationTopicId: topicId || undefined, documentationType: documentationType(explicit) ?? base.documentationType }), modules); } } const field = target.closest( ".form-field, .toggle-switch-row, .searchable-select, .email-address-input, .date-field, .time-field, .date-time-field" ); if (field) { const label = helpLabel(field) || helpLabel(target) || "Field"; return withDeclaredDocumentation(derivedChildContext(base, field, label, "field"), modules); } const control = target.closest( "button, a, input, select, textarea, [role='button'], [role='checkbox'], [role='combobox'], [role='menuitem'], [role='option'], [role='radio'], [role='switch'], [role='tab']" ); const dialog = target.closest("[role='dialog'], [role='alertdialog'], [data-help-scope='dialog']"); if (control) { const isField = control.matches("input, select, textarea, [role='checkbox'], [role='combobox'], [role='radio'], [role='switch']"); const label = helpLabel(control) || (isField ? "Field" : "Action"); const slug = stableHelpKey(control, label); if (dialog && ["close", "cancel"].includes(slug)) { const dialogLabel = helpLabel(dialog) || "Dialog"; return withDeclaredDocumentation(derivedChildContext(base, dialog, dialogLabel, "dialog"), modules); } return withDeclaredDocumentation(derivedChildContext(base, control, label, isField ? "field" : "action"), modules); } if (dialog) { return withDeclaredDocumentation(derivedChildContext(base, dialog, helpLabel(dialog) || "Dialog", "dialog"), modules); } const contextualRegion = target.closest("[data-help-scope], .card, .admin-section-page"); if (contextualRegion) { return withDeclaredDocumentation(derivedChildContext(base, contextualRegion, helpLabel(contextualRegion) || base.title, "interface"), modules); } return base; } export function helpQueryForContext(context: HelpContext): string { const params = new URLSearchParams(); if (context.documentationTopicId) params.set("topic", context.documentationTopicId); else params.set("context", context.id); if (context.parentId && context.parentId !== context.id) params.set("fallback_context", context.parentId); if (context.moduleId) params.set("module", context.moduleId); return params.toString(); } function pageContext( definition: ContextDefinition, route: string, modules: readonly PlatformWebModule[] ): HelpContext { return withDeclaredDocumentation({ ...definition, route, kind: "page" }, modules); } function childContext( base: HelpContext, child: Pick & Partial ): HelpContext { return { ...child, route: base.route, moduleId: child.moduleId ?? base.moduleId, parentId: child.parentId ?? base.id, parentTitle: child.parentTitle ?? base.title, documentationType: child.documentationType ?? base.documentationType }; } function withDeclaredDocumentation( context: HelpContext, modules: readonly PlatformWebModule[] ): HelpContext { if (context.documentationTopicId) return context; const candidates = context.moduleId ? modules.filter((module) => module.id === context.moduleId) : modules; const aliases = [ context.id, context.id.replace(".section.", "."), context.id.replace(".route.", ".") ]; for (const module of candidates) { const match = module.helpContexts?.find((item) => aliases.includes(item.id)); if (!match) continue; const documentationType = context.documentationType ?? (match.documentation_types.includes("user") ? "user" : match.documentation_types[0]); return { ...context, id: match.id, title: context.title || match.title, moduleId: module.id, documentationTopicId: match.topic_id, documentationType }; } return context; } function derivedChildContext(base: HelpContext, element: HTMLElement, label: string, kind: HelpContextKind): HelpContext { const prefix = contextPrefix(base); const key = stableHelpKey(element, label); return childContext(base, { id: `${prefix}.${kind === "field" ? "field" : kind === "action" ? "action" : kind}.${key}`, title: label, kind, documentationType: documentationType(element) ?? base.documentationType }); } function contextPrefix(context: HelpContext): string { const idPrefix = context.id.split(".", 1)[0]; if (idPrefix && !["app", "admin"].includes(idPrefix)) return idPrefix; return stableSlug(context.moduleId || idPrefix || "app"); } function sectionContext( modules: readonly PlatformWebModule[], section: string, area: "settings" | "admin" ): ContextDefinition | null { const normalizedSection = stableSlug(section); let best: { score: number; definition: ContextDefinition } | null = null; for (const module of modules) { for (const surface of module.viewSurfaces ?? []) { if (surface.kind !== "section") continue; const normalizedId = surface.id.replace(/_/g, "-").toLowerCase(); const normalizedLabel = stableSlug(surface.label); let score = 0; if (normalizedId.includes(`.${area}.${normalizedSection}`)) score = 120; else if (normalizedId.includes(`.section.${normalizedSection}`)) score = 110; else if (normalizedId.endsWith(`.${normalizedSection}`)) score = 100; else if (normalizedLabel === normalizedSection) score = 80; if (!score || (best && best.score >= score)) continue; best = { score, definition: { id: surface.id, title: surface.label, moduleId: surface.moduleId || module.id, documentationType: area === "admin" ? "admin" : "user" } }; } } return best?.definition ?? null; } function moduleRouteContext(pathname: string, modules: readonly PlatformWebModule[]): ContextDefinition | null { let best: { score: number; definition: ContextDefinition } | null = null; for (const module of modules) { for (const route of [...(module.routes ?? []), ...(module.publicRoutes ?? [])]) { const score = routeMatchScore(route.path, pathname); if (score < 0 || (best && best.score >= score)) continue; const surfaceId = "surfaceId" in route ? route.surfaceId : undefined; const surface = surfaceId ? module.viewSurfaces?.find((item) => item.id === surfaceId) : undefined; const navItem = module.navItems?.find((item) => item.to === route.path || item.to === pathname); best = { score, definition: { id: route.helpContextId || surfaceId || `${module.id}.page.${stableSlug(route.path)}`, title: surface?.label || navItem?.label || module.label || humanize(pathname), moduleId: module.id, documentationTopicId: route.helpTopicId } }; } } return best?.definition ?? null; } function routeMatchScore(pattern: string, pathname: string): number { const patternSegments = pattern.split("/").filter(Boolean); const pathSegments = pathname.split("/").filter(Boolean); let score = 0; let wildcard = false; for (let index = 0; index < patternSegments.length; index += 1) { const expected = patternSegments[index]; if (expected === "*") { wildcard = true; score += 1; break; } const actual = pathSegments[index]; if (actual === undefined) return -1; if (expected.startsWith(":")) score += 10; else if (expected === actual) score += 100; else return -1; } if (!wildcard && patternSegments.length !== pathSegments.length) return -1; return score + patternSegments.length; } function helpKind(element: HTMLElement, target: Element): HelpContextKind { const explicit = element.dataset.helpScope as HelpContextKind | undefined; if (explicit && ["page", "field", "action", "dialog", "interface"].includes(explicit)) return explicit; if (element.matches(".form-field, .toggle-switch-row, .searchable-select, .email-address-input, .date-field, .time-field, .date-time-field")) return "field"; if (target.closest("[role='dialog'], [role='alertdialog']")) return "dialog"; return "interface"; } function helpLabel(element: Element): string { const own = element as HTMLElement; const direct = own.dataset?.helpLabel || own.getAttribute("aria-label") || own.getAttribute("title"); if (direct?.trim()) return direct.trim(); const labelledBy = own.getAttribute("aria-labelledby"); if (labelledBy && typeof document !== "undefined") { const text = labelledBy .split(/\s+/) .map((id) => document.getElementById(id)?.textContent?.trim() || "") .filter(Boolean) .join(" "); if (text) return text; } const fieldLabel = own.querySelector(".form-label, .toggle-switch-label"); if (fieldLabel?.textContent?.trim()) return fieldLabel.textContent.trim(); const dialogTitle = own.querySelector(".dialog-title"); if (dialogTitle?.textContent?.trim()) return dialogTitle.textContent.trim(); const heading = own.querySelector("h1, h2, h3"); if (heading?.textContent?.trim()) return heading.textContent.trim(); const associatedLabel = own.id && typeof document !== "undefined" ? document.querySelector(`label[for="${cssEscape(own.id)}"]`) : null; if (associatedLabel?.textContent?.trim()) return associatedLabel.textContent.trim(); return own.textContent?.replace(/\s+/g, " ").trim().slice(0, 120) || ""; } function stableHelpKey(element: HTMLElement, label: string): string { const key = element.dataset.helpKey || element.getAttribute("name") || stableElementId(element.id) || element.getAttribute("aria-label") || element.getAttribute("title") || label; return stableSlug(key || "item"); } function stableElementId(value: string): string { if (!value || /(?:^|[-_])(?:r\d+|\d{3,}|[0-9a-f]{8,})(?:$|[-_])/i.test(value)) return ""; return value; } function documentationType(element: HTMLElement): "user" | "admin" | undefined { return element.dataset.helpDocumentationType === "admin" ? "admin" : element.dataset.helpDocumentationType === "user" ? "user" : undefined; } function stableSlug(value: string): string { const i18nMatch = value.match(/^i18n:[^.]+\.([^.]+)(?:\.[0-9a-f]{8})?$/i); const source = i18nMatch?.[1] || value; return source .replace(/^\/+|\/+$/g, "") .replace(/:[^/]+/g, "item") .replace(/\*/g, "all") .replace(/[_\s/]+/g, "-") .replace(/[^a-zA-Z0-9.-]+/g, "-") .replace(/-{2,}/g, "-") .replace(/^-|-$/g, "") .toLowerCase() || "item"; } function humanize(value: string): string { return value .replace(/^\/+|\/+$/g, "") .replace(/[-_]+/g, " ") .replace(/\b\w/g, (letter) => letter.toUpperCase()) || "Application"; } function cssEscape(value: string): string { if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value); return value.replace(/["\\]/g, "\\$&"); }