@mhosaic/feedback 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/qa-meter/mount.tsx","../src/qa-meter/i18n.ts","../src/qa-meter/resolver.ts","../src/qa-meter/styles.ts","../src/qa-meter/Pastille.tsx","../src/qa-meter/ui.ts","../src/qa-meter/TestPanel.tsx","../src/qa-meter/LaneIcons.tsx","../src/qa-meter/SitemapModal.tsx","../src/qa-meter/index.ts"],"sourcesContent":["import { h, render } from 'preact'\nimport { useCallback, useEffect, useState } from 'preact/hooks'\n\nimport type { QaColor, QaPageStatus, QaStatusArtifact } from './types'\nimport type { QaMeterHandle, QaMeterOptions } from './index'\nimport { resolveStrings } from './i18n'\nimport { resolvePage } from './resolver'\nimport { QA_METER_STYLES } from './styles'\nimport { Pastille } from './Pastille'\nimport { TestPanel } from './TestPanel'\nimport { SitemapModal } from './SitemapModal'\n\n/** The custom event the patched History API dispatches so the meter can\n * re-resolve the current page on SPA navigations (pushState/replaceState\n * don't fire `popstate`). Module-scoped name so every instance listens to\n * the same channel. */\nconst LOCATION_CHANGE_EVENT = 'mqa:locationchange'\n\n// ── History API patch (module-level, ref-counted) ──────────────────────────\n// We monkey-patch pushState/replaceState ONCE across all instances so a\n// `mqa:locationchange` CustomEvent fires on SPA navigations. A ref-count lets\n// the LAST disposed instance restore the originals; guarding on `patched`\n// prevents a second instance from double-wrapping (which would dispatch twice\n// and, worse, capture an already-wrapped \"original\").\nlet historyPatched = false\nlet historyRefCount = 0\nlet originalPushState: History['pushState'] | null = null\nlet originalReplaceState: History['replaceState'] | null = null\n\nfunction patchHistory(): void {\n historyRefCount++\n if (historyPatched) return\n historyPatched = true\n originalPushState = history.pushState\n originalReplaceState = history.replaceState\n const fire = (): void => {\n window.dispatchEvent(new CustomEvent(LOCATION_CHANGE_EVENT))\n }\n history.pushState = function patchedPushState(\n this: History,\n ...args: Parameters<History['pushState']>\n ) {\n const ret = originalPushState!.apply(this, args)\n fire()\n return ret\n }\n history.replaceState = function patchedReplaceState(\n this: History,\n ...args: Parameters<History['replaceState']>\n ) {\n const ret = originalReplaceState!.apply(this, args)\n fire()\n return ret\n }\n}\n\nfunction unpatchHistory(): void {\n if (historyRefCount > 0) historyRefCount--\n if (historyRefCount > 0 || !historyPatched) return\n historyPatched = false\n if (originalPushState) history.pushState = originalPushState\n if (originalReplaceState) history.replaceState = originalReplaceState\n originalPushState = null\n originalReplaceState = null\n}\n\n/** Default offset, matching the donor `qa-pastille.vue` (sits near the feedback\n * FAB). `.qa-pastille` anchors bottom-right via the `--mqa-right`/`--mqa-bottom`\n * custom properties, which default to `1.5rem` in the stylesheet. CSS custom\n * properties inherit from the host element THROUGH the shadow boundary, so a\n * host can nudge the pastille by setting those vars on the host (done below\n * when `options.position` is provided) — setting `host.style.right/bottom`\n * directly would NOT work, since `.qa-pastille` is itself `position: fixed`\n * and anchors to the viewport, not the host. */\n\n// ── Format validation ───────────────────────────────────────────────────────\nconst QA_ARTIFACT_FORMAT = 1\n\nfunction isValidArtifact(value: unknown): value is QaStatusArtifact {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { format?: unknown }).format === QA_ARTIFACT_FORMAT\n )\n}\n\nlet warnedBadFormat = false\nfunction warnBadFormatOnce(): void {\n if (warnedBadFormat) return\n warnedBadFormat = true\n if (typeof console !== 'undefined') {\n console.warn('[mqa] QA Meter artifact has an unsupported `format` — refusing to mount.')\n }\n}\n\nconst NOOP_HANDLE: QaMeterHandle = {\n open() {},\n close() {},\n dispose() {},\n}\n\n/** Every path_pattern an artifact knows — `pages[].page?.path_pattern` ∪\n * `sitemap[].path_pattern`. Feeds the default routerless resolver. */\nfunction patternsFromArtifact(artifact: QaStatusArtifact): string[] {\n const set = new Set<string>()\n for (const page of artifact.pages) {\n if (page.page) set.add(page.page.path_pattern)\n }\n for (const node of artifact.sitemap) {\n set.add(node.path_pattern)\n }\n return [...set]\n}\n\ninterface Resolved {\n state: 'known' | 'no-tests' | 'unknown-page'\n color: QaColor\n pattern: string | null\n page: QaPageStatus | null\n}\n\nconst UNKNOWN: Resolved = { state: 'unknown-page', color: 'gray', pattern: null, page: null }\n\n/** State machine (task spec): a matched `pages[]` entry → known/its color;\n * a pattern present only in the sitemap → no-tests/gray; no match (or no\n * artifact yet) → unknown-page/gray. Never throws. */\nfunction resolveCurrent(artifact: QaStatusArtifact | null, pattern: string | null): Resolved {\n if (!artifact || !pattern) return UNKNOWN\n const page = artifact.pages.find((p) => p.page && p.page.path_pattern === pattern)\n if (page && page.page) {\n return { state: 'known', color: page.color, pattern, page }\n }\n const inSitemap = artifact.sitemap.some((n) => n.path_pattern === pattern)\n if (inSitemap) {\n return { state: 'no-tests', color: 'gray', pattern, page: null }\n }\n return { state: 'unknown-page', color: 'gray', pattern, page: null }\n}\n\nexport function createQaMeter(options: QaMeterOptions): QaMeterHandle {\n const { source } = options\n const strings = resolveStrings(options.locale ?? 'fr')\n\n // For an INLINE artifact we must validate synchronously at create time, so a\n // wrong-format object never produces a host (the test asserts this after a\n // single microtask).\n const inlineArtifact = typeof source === 'object' ? source : null\n if (inlineArtifact && !isValidArtifact(inlineArtifact)) {\n warnBadFormatOnce()\n return NOOP_HANDLE\n }\n\n // Loaded artifact cache + lazy loader state. Inline artifacts are ready now;\n // string/function sources load lazily on first open/hover, exactly once.\n let artifact: QaStatusArtifact | null = inlineArtifact\n let loadStarted = false\n let loadPromise: Promise<void> | null = null\n\n function load(): void {\n if (artifact || loadStarted) return\n loadStarted = true\n const got: Promise<unknown> =\n typeof source === 'function'\n ? source()\n : typeof source === 'string'\n ? fetch(source).then((r) => r.json())\n : Promise.resolve(source)\n loadPromise = got\n .then((value) => {\n if (isValidArtifact(value)) {\n artifact = value\n forceRender()\n } else {\n warnBadFormatOnce()\n }\n })\n .catch(() => {\n // Never throw to the host: stay gray.\n })\n }\n\n // ── Host + shadow root + styles (mirrors widget/mount.tsx) ────────────────\n const host = document.createElement('div')\n host.setAttribute('data-mqa-host', '')\n host.setAttribute('data-mqa-size', options.size ?? 'md')\n // Offset knobs as CSS custom properties: they inherit through the shadow\n // boundary down to `.qa-pastille`, which reads them with a 1.5rem fallback.\n // Only set each when provided so the stylesheet default applies otherwise.\n if (options.position?.right !== undefined) {\n host.style.setProperty('--mqa-right', `${options.position.right}px`)\n }\n if (options.position?.bottom !== undefined) {\n host.style.setProperty('--mqa-bottom', `${options.position.bottom}px`)\n }\n document.body.appendChild(host)\n\n const shadow = host.attachShadow({ mode: 'open' })\n const style = document.createElement('style')\n style.textContent = QA_METER_STYLES\n shadow.appendChild(style)\n const mountPoint = document.createElement('div')\n shadow.appendChild(mountPoint)\n\n // Imperative re-render hook into the Preact tree. The container component\n // registers its dispatcher here so the loader/navigation callbacks can poke\n // it without React-ish prop threading.\n let forceRender: () => void = () => {}\n\n function getCurrentPattern(): string | null {\n if (options.getCurrentPage) return options.getCurrentPage()\n if (!artifact) return null\n return resolvePage(window.location.pathname, patternsFromArtifact(artifact))\n }\n\n function Container() {\n // A bump-counter is the simplest \"force re-render on external change\"\n // primitive (loader resolves, navigation, open/close).\n const [tick, setTick] = useState(0)\n void tick\n const bump = useCallback(() => setTick((n) => n + 1), [])\n forceRender = bump\n\n // open/close lives in module scope (so the handle can drive it) but we\n // read it via a closure variable bumped through `forceRender`.\n const resolved = resolveCurrent(artifact, getCurrentPattern())\n\n const meta = artifact\n ? {\n generated_at: artifact.generated_at,\n app_version: artifact.app_version,\n environment: artifact.environment,\n }\n : { generated_at: '', app_version: '', environment: '' }\n\n const onHover = useCallback(() => {\n // Lazy-load on first hover too (not just open), so the panel shows real\n // data before the user clicks.\n load()\n }, [])\n\n const onOpenModal = useCallback(() => {\n load()\n uiOpen = true\n bump()\n }, [])\n\n const closeModal = useCallback(() => {\n uiOpen = false\n bump()\n }, [])\n\n // Re-resolve current page on navigation. popstate covers back/forward;\n // the patched History API covers SPA pushState/replaceState.\n useEffect(() => {\n const onNav = (): void => bump()\n window.addEventListener('popstate', onNav)\n window.addEventListener(LOCATION_CHANGE_EVENT, onNav)\n return () => {\n window.removeEventListener('popstate', onNav)\n window.removeEventListener(LOCATION_CHANGE_EVENT, onNav)\n }\n }, [])\n\n return h(\n 'div',\n {\n class: 'qa-pastille-wrap',\n // Re-entering the wrap (the dot OR the panel above it — both live\n // inside) cancels a pending close; leaving schedules a debounced close.\n onMouseEnter: cancelHoverClose,\n onMouseLeave: scheduleHoverClose,\n },\n uiHover\n ? h(\n 'div',\n { class: 'qa-panel-anchor' },\n h(TestPanel, { page: resolved.page, state: resolved.state, meta, strings }),\n )\n : null,\n h(Pastille, {\n color: resolved.color,\n state: resolved.state,\n strings,\n onHover: () => {\n cancelHoverClose()\n uiHover = true\n onHover()\n bump()\n },\n onOpenModal,\n }),\n uiOpen && artifact\n ? h(SitemapModal, {\n artifact,\n currentPattern: resolved.pattern,\n strings,\n onClose: closeModal,\n })\n : null,\n )\n }\n\n // UI-local flags kept in closure scope so the imperative handle (open/close)\n // can flip them and `forceRender()` reflects the change. The container reads\n // them on each render.\n let uiOpen = false\n let uiHover = false\n\n // Closing the hover panel is debounced (rather than firing on the raw\n // mouseleave) so sweeping the cursor across the small gap between the dot and\n // the panel — or a jittery mouse at the dot's edge — doesn't unmount it\n // mid-sweep. Any mouseenter back into the wrap cancels the pending close.\n let hoverCloseTimer: ReturnType<typeof setTimeout> | null = null\n const HOVER_CLOSE_MS = 140\n function cancelHoverClose(): void {\n if (hoverCloseTimer !== null) {\n clearTimeout(hoverCloseTimer)\n hoverCloseTimer = null\n }\n }\n function scheduleHoverClose(): void {\n cancelHoverClose()\n hoverCloseTimer = setTimeout(() => {\n hoverCloseTimer = null\n if (disposed) return\n uiHover = false\n forceRender()\n }, HOVER_CLOSE_MS)\n }\n\n function mountTree(): void {\n render(h(Container, {}), mountPoint)\n }\n\n // Initial mount.\n mountTree()\n\n patchHistory()\n\n let disposed = false\n\n return {\n open() {\n if (disposed) return\n load()\n uiOpen = true\n forceRender()\n },\n close() {\n if (disposed) return\n uiOpen = false\n forceRender()\n },\n dispose() {\n if (disposed) return\n disposed = true\n cancelHoverClose()\n render(null, mountPoint)\n host.remove()\n unpatchHistory()\n void loadPromise\n },\n }\n}\n","/**\n * Bundled FR/EN strings for the QA Meter widget.\n *\n * Keys are the donor's `qaMeter.*` keys with the `qaMeter.` prefix stripped.\n * Values are copied verbatim from the donor locale files.\n *\n * Two new keys not present in the donor are appended at the end of each record:\n * - `panel.noTests`\n * - `panel.unknownPage`\n */\n\nexport const QA_STRINGS_FR = {\n 'pastille.label': 'Suivi des tests',\n 'pastille.loading': 'Chargement…',\n 'pastille.unavailable': 'Statuts indisponibles',\n 'pastille.unknownPage': 'Page inconnue',\n 'pastille.unknownPageHint': \"Cette route n’est pas encore au plan du site des tests.\",\n 'panel.freshness': 'Statuts g\\xe9n\\xe9r\\xe9s le {date} \\xb7 version {version} \\xb7 env {env}',\n 'panel.noScenarios': 'Aucun sc\\xe9nario li\\xe9 \\xe0 cette page.',\n 'panel.openSitemap': 'Voir le plan du site',\n 'panel.sitemapLink': 'Plan du site',\n 'panel.scenarioCountOne': '{count} sc\\xe9nario',\n 'panel.scenarioCountMany': '{count} sc\\xe9narios',\n 'panel.laneAutomated': 'Automatis\\xe9',\n 'panel.laneDev': 'D\\xe9v',\n 'panel.laneClient': 'Client',\n 'lane.notTested': 'Non test\\xe9',\n 'lane.validatedBy': 'Valid\\xe9 par {by}',\n 'lane.rejectedBy': 'Rejet\\xe9 par {by}',\n 'lane.stale': 'Valid\\xe9 en v{version}, version actuelle v{current}',\n 'lane.clientPending': 'Non test\\xe9 — plateforme requise',\n 'lane.bot': 'Automatis\\xe9 (bot)',\n 'lane.code': 'D\\xe9veloppeur (code)',\n 'lane.human': 'Humain',\n 'laneState.ok': '\\xc0 jour',\n 'laneState.issue': 'Probl\\xe8me',\n 'laneState.none': 'Non fait / p\\xe9rim\\xe9',\n 'color.green': 'Valid\\xe9',\n 'color.yellow': 'En attente',\n 'color.red': 'En \\xe9chec',\n 'color.gray': 'Non couvert',\n 'automated.passing': 'R\\xe9ussi',\n 'automated.failing': 'En \\xe9chec',\n 'automated.flaky': 'Instable',\n 'automated.neverRun': 'Jamais ex\\xe9cut\\xe9',\n 'source.testgen': 'g\\xe9n\\xe9r\\xe9',\n 'source.testgenHint': 'Sc\\xe9nario g\\xe9n\\xe9r\\xe9 automatiquement (testgen)',\n 'suite.cases': '{count} cas',\n 'case.covered': 'auto',\n 'case.manual': 'manuel',\n 'case.blocked': 'bloqu\\xe9',\n 'case.openQuestion': 'question produit',\n 'grounding.verified': 'attendu v\\xe9rifi\\xe9',\n 'grounding.confirmLive': '\\xe0 confirmer en live',\n 'grounding.productQuestion': 'd\\xe9cision produit',\n 'modal.title': 'Plan du site — suivi des tests',\n 'modal.close': 'Fermer',\n 'modal.tabPages': 'Pages',\n 'modal.tabScenarios': 'Sc\\xe9narios',\n 'modal.youAreHere': 'vous \\xeates ici',\n 'modal.search': 'Filtrer les sc\\xe9narios…',\n 'modal.noMatch': 'Aucun sc\\xe9nario ne correspond au filtre.',\n 'modal.traversed': 'Travers\\xe9es',\n 'modal.appliesTo': '{count} pages',\n 'modal.kpiPagesCovered': 'Pages avec ≥ 1 sc\\xe9nario',\n 'modal.kpiScenariosPassing': 'Sc\\xe9narios passants',\n // New keys (not in donor)\n 'panel.noTests': 'Aucun test ne couvre encore cette page.',\n 'panel.unknownPage': 'Page inconnue du registre QA.',\n} as const\n\nexport const QA_STRINGS_EN = {\n 'pastille.label': 'Test tracking',\n 'pastille.loading': 'Loading…',\n 'pastille.unavailable': 'Statuses unavailable',\n 'pastille.unknownPage': 'Unknown page',\n 'pastille.unknownPageHint': \"This route isn’t in the test sitemap yet.\",\n 'panel.freshness': 'Statuses generated on {date} \\xb7 version {version} \\xb7 env {env}',\n 'panel.noScenarios': 'No scenario linked to this page.',\n 'panel.openSitemap': 'View sitemap',\n 'panel.sitemapLink': 'Sitemap',\n 'panel.scenarioCountOne': '{count} scenario',\n 'panel.scenarioCountMany': '{count} scenarios',\n 'panel.laneAutomated': 'Automated',\n 'panel.laneDev': 'Dev',\n 'panel.laneClient': 'Client',\n 'lane.notTested': 'Not tested',\n 'lane.validatedBy': 'Validated by {by}',\n 'lane.rejectedBy': 'Rejected by {by}',\n 'lane.stale': 'Validated in v{version}, current version v{current}',\n 'lane.clientPending': 'Not tested — platform required',\n 'lane.bot': 'Automated (bot)',\n 'lane.code': 'Developer (code)',\n 'lane.human': 'Human',\n 'laneState.ok': 'Up to date',\n 'laneState.issue': 'Issue',\n 'laneState.none': 'Not done / stale',\n 'color.green': 'Validated',\n 'color.yellow': 'Pending',\n 'color.red': 'Failing',\n 'color.gray': 'Not covered',\n 'automated.passing': 'Passing',\n 'automated.failing': 'Failing',\n 'automated.flaky': 'Flaky',\n 'automated.neverRun': 'Never run',\n 'source.testgen': 'generated',\n 'source.testgenHint': 'Auto-generated scenario (testgen)',\n 'suite.cases': '{count} cases',\n 'case.covered': 'auto',\n 'case.manual': 'manual',\n 'case.blocked': 'blocked',\n 'case.openQuestion': 'product question',\n 'grounding.verified': 'expected verified',\n 'grounding.confirmLive': 'confirm live',\n 'grounding.productQuestion': 'product decision',\n 'modal.title': 'Sitemap — test tracking',\n 'modal.close': 'Close',\n 'modal.tabPages': 'Pages',\n 'modal.tabScenarios': 'Scenarios',\n 'modal.youAreHere': 'you are here',\n 'modal.search': 'Filter scenarios…',\n 'modal.noMatch': 'No scenario matches the filter.',\n 'modal.traversed': 'Traversed',\n 'modal.appliesTo': '{count} pages',\n 'modal.kpiPagesCovered': 'Pages with ≥ 1 scenario',\n 'modal.kpiScenariosPassing': 'Passing scenarios',\n // New keys (not in donor)\n 'panel.noTests': 'No tests cover this page yet.',\n 'panel.unknownPage': 'Page unknown to the QA registry.',\n} as const\n\nexport type QaStringKey = keyof typeof QA_STRINGS_FR\nexport type QaStrings = Record<QaStringKey, string>\n\nexport function resolveStrings(locale: 'fr' | 'en'): QaStrings {\n return locale === 'en' ? QA_STRINGS_EN : QA_STRINGS_FR\n}\n","/** Routerless page resolution: match window.location.pathname against the\n * artifact's path_patterns. Most-specific match wins (static segments beat\n * params; longer beats shorter). The host can override with a router-aware\n * getCurrentPage — this is only the default. */\n\nexport function normalizePattern(pattern: string | undefined | null): string {\n if (!pattern) return '/'\n let n = pattern.startsWith('/') ? pattern : `/${pattern}`\n n = n.replace(/\\/{2,}/g, '/')\n if (n.length > 1) n = n.replace(/\\/+$/, '')\n return n.length > 0 ? n : '/'\n}\n\nconst segments = (p: string): string[] => p.split('/').filter(Boolean)\n\nfunction matchesPattern(pathSegs: string[], patternSegs: string[]): boolean {\n let required = patternSegs.length\n while (required > 0) {\n const last = patternSegs[required - 1]\n if (last !== undefined && last.startsWith(':') && last.endsWith('?')) required--\n else break\n }\n if (pathSegs.length < required || pathSegs.length > patternSegs.length) return false\n return pathSegs.every((seg, i) => {\n const pat = patternSegs[i]\n return pat !== undefined && (pat.startsWith(':') || pat === seg)\n })\n}\n\nexport function resolvePage(pathname: string, patterns: ReadonlyArray<string>): string | null {\n const pathSegs = segments(normalizePattern(pathname))\n let best: string | null = null\n let bestScore = -1\n for (const pattern of patterns) {\n const patSegs = segments(pattern)\n if (!matchesPattern(pathSegs, patSegs)) continue\n const statics = patSegs.filter((s) => !s.startsWith(':')).length\n const score = statics * 100 + patSegs.length\n if (score > bestScore) { best = pattern; bestScore = score }\n }\n return best\n}\n","/**\n * QA Meter CSS styles — a `:host` block with CSS variables prefixed `--mqa-`.\n *\n * The QA Meter z-index is set one below the feedback widget's `2147483640`\n * so the feedback FAB wins overlaps.\n *\n * Color tokens (hardcoded to remove dependency on DaisyUI HSL variables):\n * green #16a34a\n * yellow #eab308\n * red #dc2626\n * gray #9ca3af\n *\n * Selectors ported from qa-pastille.vue, qa-test-panel.vue, qa-lane-icons.vue,\n * and qa-sitemap-modal.vue — adapted for standalone Shadow DOM use.\n */\nexport const QA_METER_STYLES = `\n:host {\n --mqa-z-index: 2147483639;\n --mqa-z-panel: 2147483638;\n\n /* Color tokens */\n --mqa-green: #16a34a;\n --mqa-yellow: #eab308;\n --mqa-red: #dc2626;\n --mqa-gray: #9ca3af;\n\n /* Surface */\n --mqa-bg: #ffffff;\n --mqa-surface: #f9fafb;\n --mqa-border: #e5e7eb;\n --mqa-text: #111827;\n --mqa-text-muted: #6b7280;\n --mqa-primary: #3b82f6;\n --mqa-shadow: 0 4px 12px rgba(0,0,0,0.08), 0 2px 4px rgba(0,0,0,0.12);\n --mqa-shadow-xl: 0 24px 56px rgba(0,0,0,0.18), 0 8px 20px rgba(0,0,0,0.10);\n\n /* Font */\n --mqa-font: system-ui, -apple-system, sans-serif;\n --mqa-radius: 8px;\n\n all: initial;\n font-family: var(--mqa-font);\n color: var(--mqa-text);\n position: fixed;\n z-index: var(--mqa-z-index);\n}\n\n@media (prefers-color-scheme: dark) {\n :host {\n --mqa-bg: #1e293b;\n --mqa-surface: #0f172a;\n --mqa-border: #334155;\n --mqa-text: #f8fafc;\n --mqa-text-muted: #94a3b8;\n --mqa-shadow-xl: 0 24px 56px rgba(0,0,0,0.55), 0 8px 20px rgba(0,0,0,0.40);\n }\n}\n\n/* ── Semaphore modifier classes ─────────────────────────────────────────── */\n\n.qa--green { background: var(--mqa-green); }\n.qa--yellow { background: var(--mqa-yellow); }\n.qa--red { background: var(--mqa-red); }\n.qa--gray { background: var(--mqa-gray); }\n\n/* Priority modifiers (color only — components set sizing). */\n.qa-prio--p0 { color: var(--mqa-red); }\n.qa-prio--p1 { color: var(--mqa-yellow); }\n.qa-prio--p2 { color: var(--mqa-green); }\n.qa-prio--p3 { color: var(--mqa-gray); }\n\n/* ── Pastille (FAB circle) ──────────────────────────────────────────────── */\n\n/* The wrap is the fixed, bottom-right anchor for the whole pastille cluster.\n It MUST be a positioning context: the hover panel .qa-panel is\n position:absolute and is a SIBLING of the pastille, so without a positioned\n wrap it would anchor to :host (fixed, no offsets — i.e. the top-left of the\n screen) instead of sitting above the FAB. The --mqa-right/--mqa-bottom knobs\n (set on the host, inherited here) keep driving the on-screen position. The\n fixed sitemap modal inside the wrap anchors to the viewport regardless, so\n it's unaffected. */\n.qa-pastille-wrap {\n position: fixed;\n right: var(--mqa-right, 1.5rem);\n bottom: var(--mqa-bottom, 1.5rem);\n z-index: var(--mqa-z-index);\n}\n\n.qa-pastille {\n position: relative;\n}\n\n.qa-pastille__dot {\n width: 48px;\n height: 48px;\n border-radius: 9999px;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n box-shadow: var(--mqa-shadow);\n color: rgba(0, 0, 0, 0.5);\n /* No idle animation — the pastille stays static until interacted with, like\n the feedback FAB. (Previously pulsed on a perpetual keyframe loop.) Only\n the hover transition below animates. */\n transition: transform 0.15s ease, box-shadow 0.15s ease;\n border: none;\n}\n\n.qa-pastille__dot:hover {\n transform: scale(1.12);\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);\n}\n\n.qa-pastille__ecg {\n width: 30px;\n height: 15px;\n}\n\n/* Secondary size: half-scale pastille for the integrated second-FAB use. */\n:host([data-mqa-size=\"sm\"]) .qa-pastille__dot { width: 24px; height: 24px; }\n:host([data-mqa-size=\"sm\"]) .qa-pastille__ecg { width: 15px; height: 8px; }\n\n/* Color overrides: on colored backgrounds the ECG trace needs a dark stroke. */\n.qa-pastille__dot.qa--green,\n.qa-pastille__dot.qa--yellow,\n.qa-pastille__dot.qa--red {\n color: rgba(0, 0, 0, 0.6);\n}\n\n.qa-pastille__dot.qa--gray {\n background: rgba(156, 163, 175, 0.35);\n color: rgba(0, 0, 0, 0.45);\n}\n\n.qa-fade-enter-active,\n.qa-fade-leave-active {\n transition: opacity 0.15s ease, transform 0.15s ease;\n}\n.qa-fade-enter-from,\n.qa-fade-leave-to {\n opacity: 0;\n transform: translateY(4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .qa-pastille__dot { transition: none; }\n}\n\n/* ── Test panel ─────────────────────────────────────────────────────────── */\n\n/* The hover panel anchors above the pastille inside the fixed\n .qa-pastille-wrap. The ANCHOR (not the panel) is what's taken out of flow:\n it MUST be absolute so the wrap shrink-wraps to the pastille dot — an in-flow\n panel would stretch the wrap to 340px and either push the dot aside or (on a\n tall, scrolled host page) drive a scrollbar-reflow feedback loop that makes\n the FAB jump. (The donor's positioning lived on an orphaned\n .qa-pastille__panel selector that matched nothing after the Vue→Preact port\n renamed the root to .qa-panel.)\n\n The anchor's padding-bottom renders the 0.5rem visual gap between the dot\n and the panel AS PART of the hover region, so sweeping the cursor from the\n dot up onto the panel never crosses dead space → never fires the wrap's\n mouseleave → no flicker. (Paired with a small close-delay in mount.tsx.) */\n.qa-panel-anchor {\n position: absolute;\n bottom: 100%;\n right: 0;\n padding-bottom: 0.5rem;\n z-index: var(--mqa-z-panel);\n}\n\n.qa-panel {\n background: var(--mqa-bg);\n border-radius: var(--mqa-radius);\n border: 1px solid var(--mqa-border);\n box-shadow: var(--mqa-shadow-xl);\n width: 340px;\n max-height: 60vh;\n overflow: hidden auto;\n font-size: 0.8125rem;\n color: var(--mqa-text);\n}\n\n.qa-panel__empty {\n padding: 0.75rem;\n opacity: 0.7;\n}\n\n.qa-panel__list,\n.qa-panel__groups {\n margin: 0;\n padding: 0.25rem 0;\n list-style: none;\n}\n\n.qa-panel__foot {\n border-top: 1px solid var(--mqa-border);\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n padding: 0.5rem 0.75rem;\n position: sticky;\n bottom: 0;\n background: var(--mqa-bg);\n}\n\n.qa-panel__count {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n font-size: 0.6875rem;\n opacity: 0.65;\n}\n\n.qa-panel__link {\n flex: none;\n font-weight: 600;\n cursor: pointer;\n white-space: nowrap;\n color: var(--mqa-primary);\n background: none;\n border: none;\n font: inherit;\n}\n\n.qa-panel__link:hover { text-decoration: underline; }\n\n/* ── Suite groups ───────────────────────────────────────────────────────── */\n\n.qa-suite + .qa-suite { border-top: 1px solid var(--mqa-border); }\n\n.qa-suite__head {\n width: 100%;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.5rem 0.75rem;\n cursor: pointer;\n text-align: left;\n background: none;\n border: none;\n font: inherit;\n color: inherit;\n}\n\n.qa-suite__head:hover { background: var(--mqa-surface); }\n\n.qa-suite__chev {\n flex: none;\n font-size: 0.625rem;\n opacity: 0.6;\n transition: transform 0.12s ease;\n}\n\n.qa-suite__chev.is-open { transform: rotate(90deg); }\n\n.qa-suite__key {\n flex: none;\n width: 1.25rem;\n height: 1.25rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n font-size: 0.6875rem;\n font-weight: 700;\n background: var(--mqa-surface);\n border-radius: 4px;\n}\n\n.qa-suite__label {\n flex: 1;\n min-width: 0;\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.qa-suite__count {\n flex: none;\n font-size: 0.6875rem;\n opacity: 0.55;\n}\n\n.qa-suite__list {\n background: var(--mqa-surface);\n margin: 0;\n padding: 0 0 0.25rem;\n list-style: none;\n}\n\n/* ── Scenario rows ──────────────────────────────────────────────────────── */\n\n.qa-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n padding: 0.5rem 0.75rem;\n}\n\n.qa-row + .qa-row { border-top: 1px solid var(--mqa-border); }\n\n.qa-row__title {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n min-width: 0;\n}\n\n.qa-row__name {\n flex: 1;\n min-width: 0;\n font-weight: 600;\n line-height: 1.25;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.qa-suite__list .qa-row { padding-left: 1.5rem; }\n\n.qa-tag {\n flex: none;\n font-size: 0.625rem;\n padding: 0 0.25rem;\n text-transform: uppercase;\n letter-spacing: 0.02em;\n opacity: 0.8;\n background: var(--mqa-surface);\n border-radius: 9999px;\n}\n\n/* ── Lane icons ─────────────────────────────────────────────────────────── */\n\n.qa-lanes {\n display: inline-flex;\n align-items: center;\n gap: 0.3125rem;\n flex: none;\n padding: 0.1875rem 0.4375rem;\n border-radius: 9999px;\n background: rgba(17, 24, 39, 0.06);\n}\n\n.qa-lanes--sm { font-size: 0.875rem; }\n.qa-lanes--md { font-size: 1rem; }\n\n.qa-lanes__icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: rgba(17, 24, 39, 0.4);\n}\n\n.qa-lanes__svg {\n width: 1em;\n height: 1em;\n display: block;\n}\n\n.qa-lanes__icon.qa--green { color: var(--mqa-green); background: none; }\n.qa-lanes__icon.qa--yellow { color: var(--mqa-yellow); background: none; }\n.qa-lanes__icon.qa--gray { color: rgba(17, 24, 39, 0.4); background: none; }\n\n/* ── Sitemap modal backdrop ─────────────────────────────────────────────── */\n\n.qa-modal__backdrop {\n background-color: rgba(17, 24, 39, 0.4);\n backdrop-filter: blur(1px);\n position: fixed;\n inset: 0;\n z-index: var(--mqa-z-index);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1rem;\n}\n\n/* ── Sitemap modal ──────────────────────────────────────────────────────── */\n\n.qa-modal {\n background: var(--mqa-bg);\n border-radius: var(--mqa-radius);\n box-shadow: var(--mqa-shadow-xl);\n border: 1px solid var(--mqa-border);\n width: min(720px, 92vw);\n max-height: 86vh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n color: var(--mqa-text);\n}\n\n.qa-modal__head {\n background: var(--mqa-primary);\n color: #ffffff;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0.5rem 0.75rem;\n}\n\n.qa-modal__head h3 {\n font-weight: 700;\n font-size: 1rem;\n margin: 0;\n}\n\n.qa-modal__close {\n border: 1px solid rgba(255, 255, 255, 0.3);\n background: transparent;\n color: #ffffff;\n width: 24px;\n height: 24px;\n padding: 0;\n border-radius: var(--mqa-radius);\n cursor: pointer;\n display: grid;\n place-items: center;\n}\n\n.qa-modal__close:hover { background: var(--mqa-red); }\n\n.qa-modal__freshness {\n background: var(--mqa-surface);\n padding: 0.375rem 0.75rem;\n font-size: 0.6875rem;\n opacity: 0.8;\n}\n\n.qa-modal__body {\n overflow: auto;\n padding: 0.5rem 0;\n}\n\n/* ── KPI strip ──────────────────────────────────────────────────────────── */\n\n.qa-kpi {\n display: flex;\n gap: 1.5rem;\n padding: 0.625rem 0.75rem;\n}\n\n.qa-kpi__item {\n display: flex;\n flex-direction: column;\n}\n\n.qa-kpi__value {\n font-size: 1.25rem;\n font-weight: 700;\n line-height: 1;\n}\n\n.qa-kpi__label {\n font-size: 0.6875rem;\n opacity: 0.7;\n}\n\n/* ── Tabs ───────────────────────────────────────────────────────────────── */\n\n.qa-tabs {\n border-bottom: 1px solid var(--mqa-border);\n display: flex;\n gap: 0.25rem;\n padding: 0 0.5rem;\n}\n\n.qa-tabs__tab {\n padding: 0.375rem 0.75rem;\n font-weight: 600;\n font-size: 0.8125rem;\n border-bottom: 2px solid transparent;\n cursor: pointer;\n opacity: 0.6;\n background: none;\n border-left: none;\n border-right: none;\n border-top: none;\n font: inherit;\n color: inherit;\n}\n\n.qa-tabs__tab--active {\n color: var(--mqa-primary);\n border-bottom-color: var(--mqa-primary);\n opacity: 1;\n}\n\n/* ── Sitemap page tree ──────────────────────────────────────────────────── */\n\n.qa-tree {\n margin: 0;\n padding: 0;\n list-style: none;\n font-size: 0.8125rem;\n}\n\n.qa-tree__row {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding-top: 0.25rem;\n padding-bottom: 0.25rem;\n padding-right: 0.75rem;\n}\n\n.qa-tree__row--here { background: var(--mqa-surface); }\n\n.qa-tree__caret {\n width: 1rem;\n text-align: center;\n cursor: pointer;\n opacity: 0.6;\n font-size: 0.6875rem;\n background: none;\n border: none;\n font: inherit;\n color: inherit;\n}\n\n.qa-tree__caret--leaf { cursor: default; }\n\n.qa-tree__title { flex: 1; }\n.qa-tree__title--structural { font-style: italic; opacity: 0.75; }\n\n.qa-tree__here {\n color: var(--mqa-primary);\n font-size: 0.625rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.qa-tree__count {\n background: var(--mqa-surface);\n border-radius: 9999px;\n font-size: 0.6875rem;\n padding: 0 0.375rem;\n min-width: 1.25rem;\n text-align: center;\n}\n\n/* ── Scenarios tab ──────────────────────────────────────────────────────── */\n\n.qa-scenarios { padding: 0 0.75rem; }\n\n.qa-scenarios__search {\n border: 1px solid var(--mqa-border);\n border-radius: var(--mqa-radius);\n width: 100%;\n height: 2rem;\n padding: 0 0.75rem;\n font-size: 0.8125rem;\n margin-bottom: 0.5rem;\n background: var(--mqa-bg);\n color: var(--mqa-text);\n font-family: inherit;\n box-sizing: border-box;\n}\n\n.qa-scenarios__empty {\n opacity: 0.7;\n padding: 0.5rem 0;\n}\n\n.qa-scenarios__list {\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.qa-scenarios__item {\n border-top: 1px solid var(--mqa-border);\n padding: 0.5rem 0;\n}\n\n.qa-scenarios__line {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n}\n\n.qa-scenarios__title {\n flex: 1;\n font-weight: 600;\n font-size: 0.8125rem;\n}\n\n.qa-scenarios__pages {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n font-size: 0.6875rem;\n opacity: 0.7;\n margin-top: 0.125rem;\n padding-left: 1rem;\n}\n\n/* ── Scenario dot (color indicator) ────────────────────────────────────── */\n\n.qa-dot {\n width: 0.625rem;\n height: 0.625rem;\n border-radius: 9999px;\n flex: none;\n background: rgba(17, 24, 39, 0.6);\n}\n\n/* Override modifier background for dot — dot uses background not text color. */\n.qa-dot.qa--green { background: var(--mqa-green); }\n.qa-dot.qa--yellow { background: var(--mqa-yellow); }\n.qa-dot.qa--red { background: var(--mqa-red); }\n.qa-dot.qa--gray { background: var(--mqa-gray); }\n\n/* ── Suite key badge ────────────────────────────────────────────────────── */\n\n.qa-suitekey {\n flex: none;\n font-size: 0.625rem;\n font-weight: 700;\n padding: 0 0.3125rem;\n line-height: 1.4;\n background: var(--mqa-surface);\n border-radius: 4px;\n}\n`\n","import { h } from 'preact'\n\nimport type { QaColor } from './types'\nimport type { QaStrings } from './i18n'\nimport { QA_COLOR_LABEL_KEY, QA_COLOR_MODIFIER } from './ui'\n\n/**\n * The heartbeat/ECG pastille — purely presentational. It renders the page color\n * as a semaphore modifier and emits hover/open callbacks; it owns NO state, NO\n * router, NO store. State (current page, fetched artifact) lives in the mount\n * container task that wraps this.\n *\n * Heartbeat/ECG SVG copied verbatim from the donor `qa-pastille.vue`.\n */\nexport interface PastilleProps {\n color: QaColor\n state: 'known' | 'no-tests' | 'unknown-page'\n strings: QaStrings\n onOpenModal: () => void\n onHover: () => void\n}\n\nexport function Pastille({ color, state, strings, onOpenModal, onHover }: PastilleProps) {\n // The color → class mapping is the same for every state; only the title text\n // differs slightly so a hover hint can explain the no-tests / unknown-page cases.\n const base = strings['pastille.label']\n let detail = strings[QA_COLOR_LABEL_KEY[color]]\n if (state === 'no-tests') {\n detail = strings['panel.noTests']\n } else if (state === 'unknown-page') {\n detail = strings['pastille.unknownPage']\n }\n const label = `${base} — ${detail}`\n\n // Mirror the donor's two-element structure: `.qa-pastille` is the fixed-position\n // CONTAINER; `.qa-pastille__dot` carries the size/background/animation (the\n // `.qa-pastille__dot.qa--*` rules in styles.ts are where color resolves).\n return h(\n 'div',\n { class: 'qa-pastille' },\n h(\n 'button',\n {\n type: 'button',\n 'data-testid': 'qa-pastille',\n class: `qa-pastille__dot ${QA_COLOR_MODIFIER[color]}`,\n 'aria-label': label,\n title: label,\n onClick: onOpenModal,\n onMouseEnter: onHover,\n onFocus: onHover,\n },\n h(\n 'svg',\n { class: 'qa-pastille__ecg', viewBox: '0 0 24 12', 'aria-hidden': 'true' },\n h('polyline', {\n points: '1,6 7,6 9,2 12,10 15,4 17,6 23,6',\n fill: 'none',\n stroke: 'currentColor',\n 'stroke-width': '1.5',\n 'stroke-linejoin': 'round',\n 'stroke-linecap': 'round',\n }),\n ),\n ),\n )\n}\n","/**\n * Helpers de PRÉSENTATION du QA Meter (aucune logique de statut).\n *\n * La couleur d'un scénario / d'une page est calculée à la construction de\n * l'artefact (doc 03 §9.3) et arrive déjà décidée. Ici on ne fait que la\n * traduire en classe CSS + clé i18n, et formater les dates/versions.\n *\n * Ported from the donor `qa-meter.ui.ts`; type imports point to ./types,\n * i18n keys are unprefixed (no `qaMeter.` prefix), Vue-specific imports dropped.\n */\nimport type {\n QaAutomatedLane,\n QaAutomatedStatus,\n QaCaseStatus,\n QaColor,\n QaGrounding,\n QaHumanLane,\n QaPriority,\n QaTriColor,\n} from './types';\nimport type { QaStringKey } from './i18n';\n\n/** Classe modificatrice BEM appliquée aux pastilles/puces (cf. styles des composants). */\nexport const QA_COLOR_MODIFIER: Record<QaColor, string> = {\n green: 'qa--green',\n yellow: 'qa--yellow',\n red: 'qa--red',\n gray: 'qa--gray',\n};\n\n/** Clé i18n du libellé d'une couleur (référencée dynamiquement → présente dans les deux locales). */\nexport const QA_COLOR_LABEL_KEY: Record<QaColor, QaStringKey> = {\n green: 'color.green',\n yellow: 'color.yellow',\n red: 'color.red',\n gray: 'color.gray',\n};\n\n/** Clé i18n du libellé d'un statut automated. */\nexport const QA_AUTOMATED_LABEL_KEY: Record<QaAutomatedStatus, QaStringKey> = {\n passing: 'automated.passing',\n failing: 'automated.failing',\n flaky: 'automated.flaky',\n never_run: 'automated.neverRun',\n};\n\n/** Modificateur sémaphore d'un volet automated (le volet, pas le scénario). */\nexport const QA_AUTOMATED_MODIFIER: Record<QaAutomatedStatus, string> = {\n passing: 'qa--green',\n failing: 'qa--red',\n flaky: 'qa--yellow',\n never_run: 'qa--gray',\n};\n\n/**\n * Modificateur sémaphore d'un volet humain (dev/client) :\n * - `rejected` → rouge ; non validé → gris ; validé mais périmé/contredit → jaune ; validé → vert.\n */\nexport function humanLaneModifier(lane: { validated: boolean; rejected?: boolean; stale: boolean }): string {\n if (lane.rejected) {\n return 'qa--red';\n }\n if (!lane.validated) {\n return 'qa--gray';\n }\n return lane.stale ? 'qa--yellow' : 'qa--green';\n}\n\n/** Modificateur d'une puce de priorité (P0 = critique → rouge ; décroît vers neutre). */\nexport const QA_PRIORITY_MODIFIER: Record<QaPriority, string> = {\n P0: 'qa-prio--p0',\n P1: 'qa-prio--p1',\n P2: 'qa-prio--p2',\n P3: 'qa-prio--p3',\n};\n\n/** Clé i18n du tag de statut d'un cas (couvert / manuel / bloqué / question produit). */\nexport const QA_CASE_STATUS_LABEL_KEY: Record<QaCaseStatus, QaStringKey> = {\n covered: 'case.covered',\n manual: 'case.manual',\n blocked: 'case.blocked',\n 'open-question': 'case.openQuestion',\n};\n\n/** Clé i18n de l'ancrage de l'attendu (vérifié / à confirmer / décision produit). */\nexport const QA_GROUNDING_LABEL_KEY: Record<QaGrounding, QaStringKey> = {\n verified: 'grounding.verified',\n 'confirm-live': 'grounding.confirmLive',\n 'product-question': 'grounding.productQuestion',\n};\n\n// ── Voies code/bot/human (3 icônes indépendantes, couleur 3-états) ───────────\n\n/** Voie « bot » (exécution automatisée) : passe → vert ; échec/instable → jaune ; jamais exécuté → gris. */\nexport function botTriColor(lane: QaAutomatedLane): QaTriColor {\n if (lane.status === 'passing') {\n return 'green';\n }\n if (lane.status === 'failing' || lane.status === 'flaky') {\n return 'yellow';\n }\n return 'gray';\n}\n\n/** Voie humaine (« code » = dev, « human » = client) : validé & frais → vert ; rejeté → jaune ; non validé/périmé → gris. */\nexport function humanTriColor(lane: QaHumanLane): QaTriColor {\n if (lane.rejected) {\n return 'yellow';\n }\n if (lane.validated) {\n return lane.stale ? 'gray' : 'green';\n }\n return 'gray';\n}\n\n/** Agrège une voie sur un ensemble de cas : un jaune → jaune ; sinon tout vert → vert ; sinon gris. */\nexport function aggregateTri(colors: ReadonlyArray<QaTriColor>): QaTriColor {\n if (colors.some((c) => c === 'yellow')) {\n return 'yellow';\n }\n if (colors.length > 0 && colors.every((c) => c === 'green')) {\n return 'green';\n }\n return 'gray';\n}\n\n/** Rang de gravité d'une couleur, pour résumer un groupe par son « pire » état. */\nconst QA_COLOR_RANK: Record<QaColor, number> = { red: 3, yellow: 2, green: 1, gray: 0 };\n\n/** Couleur résumé d'un groupe : rouge domine ; sinon jaune ; sinon vert ; sinon gris. */\nexport function worstColor(colors: ReadonlyArray<QaColor>): QaColor {\n return colors.reduce<QaColor>((acc, c) => (QA_COLOR_RANK[c] > QA_COLOR_RANK[acc] ? c : acc), 'gray');\n}\n\n/** Priorité la plus haute (P0 < P1 < P2 < P3) parmi une liste, ou `undefined`. */\nexport function topPriority(priorities: ReadonlyArray<QaPriority | undefined>): QaPriority | undefined {\n return priorities.filter((p): p is QaPriority => Boolean(p)).sort()[0];\n}\n\n/**\n * Formate une estampille ISO en date+heure locale courte (fr-CA → AAAA-MM-JJ HH:MM).\n * Retourne `''` pour une valeur absente/invalide (l'appelant masque alors le segment).\n */\nexport function formatQaDate(iso: string | undefined | null): string {\n if (!iso) {\n return '';\n }\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) {\n return '';\n }\n return new Intl.DateTimeFormat('fr-CA', {\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n }).format(date);\n}\n\n/**\n * Version d'app = chaîne opaque (doc 03 §1.1, jamais parsée). Tronquée pour\n * l'affichage : un timestamp de build complet est illisible dans une puce.\n */\nexport function shortVersion(version: string | undefined | null): string {\n if (!version) {\n return '';\n }\n return version.length > 19 ? `${version.slice(0, 19)}…` : version;\n}\n","import { h } from 'preact'\nimport { useState } from 'preact/hooks'\n\nimport type { QaPageStatus, QaScenario, QaTriColor } from './types'\nimport type { QaStrings } from './i18n'\nimport { LaneIcons } from './LaneIcons'\nimport {\n aggregateTri,\n botTriColor,\n formatQaDate,\n humanTriColor,\n QA_CASE_STATUS_LABEL_KEY,\n QA_GROUNDING_LABEL_KEY,\n QA_PRIORITY_MODIFIER,\n shortVersion,\n} from './ui'\n\n/**\n * Preact port of the donor `qa-test-panel.vue`.\n *\n * Presentational (no data-fetching, router, or store). The container task feeds\n * it the resolved page-status, a `state` discriminator (so the two new empty\n * states can render even when `page` is null), and the run metadata for the\n * freshness banner. Local UI state (which suite groups are expanded) lives in a\n * `useState` hook — matching the donor's `expanded`/`toggleGroup` interaction.\n *\n * The donor's `<script setup>` computed properties (lane tri-color derivation and\n * suite grouping) are extracted here as the pure exported helpers `laneTriColor`\n * and `groupScenarios` so they're unit-testable and reusable by the mount task.\n */\nexport interface TestPanelProps {\n page: QaPageStatus | null\n state: 'known' | 'no-tests' | 'unknown-page'\n meta: { generated_at: string; app_version: string; environment: string }\n strings: QaStrings\n}\n\n/** The three independent lane tri-colors of a scenario — single source of the\n * dev/bot/human derivation (donor `lanesFor`). The client lane with no event is\n * gray (never green), because `humanTriColor` returns gray for an unvalidated lane. */\nexport interface ScenarioLanes {\n dev: QaTriColor\n bot: QaTriColor\n human: QaTriColor\n}\n\nexport function laneTriColor(scenario: QaScenario): ScenarioLanes {\n return {\n dev: humanTriColor(scenario.dev),\n bot: botTriColor(scenario.automated),\n human: humanTriColor(scenario.client),\n }\n}\n\nexport interface ScenarioGroupView {\n key: string\n label: string\n scenarios: QaScenario[]\n}\n\n/** Group scenarios by `scenario.group.key` (donor `grouped`). Scenarios with no\n * group fall into a `'·'` bucket. Keys are alphabetical with the inter-module\n * `X` suite forced last, matching the donor ordering. */\nexport function groupScenarios(scenarios: ReadonlyArray<QaScenario>): ScenarioGroupView[] {\n const map = new Map<string, QaScenario[]>()\n for (const scenario of scenarios) {\n const key = scenario.group?.key ?? '·'\n const bucket = map.get(key) ?? []\n bucket.push(scenario)\n map.set(key, bucket)\n }\n const keys = [...map.keys()].sort((a, b) => (a === 'X' ? 1 : b === 'X' ? -1 : a.localeCompare(b)))\n return keys.map((key) => {\n const bucket = map.get(key)!\n return { key, label: bucket[0]?.group?.label ?? '', scenarios: bucket }\n })\n}\n\nfunction interpolate(template: string, params: Record<string, string | number>): string {\n return template.replace(/\\{(\\w+)\\}/g, (_, k: string) => String(params[k] ?? ''))\n}\n\n/** One scenario row: title, priority chip, provenance + tags, and the three lanes. */\nfunction ScenarioRow({ scenario, strings }: { scenario: QaScenario; strings: QaStrings }) {\n const lanes = laneTriColor(scenario)\n const provenance = scenario.automated.provenance\n return h(\n 'li',\n { class: 'qa-row', 'data-testid': 'qa-scenario-row' },\n h(\n 'span',\n { class: 'qa-row__title' },\n scenario.priority\n ? h(\n 'span',\n { class: `qa-tag ${QA_PRIORITY_MODIFIER[scenario.priority]}`, 'data-testid': 'qa-priority' },\n scenario.priority,\n )\n : null,\n h('span', { class: 'qa-row__name' }, scenario.title),\n scenario.source === 'testgen'\n ? h('span', { class: 'qa-tag', title: strings['source.testgenHint'] }, strings['source.testgen'])\n : null,\n scenario.caseStatus\n ? h('span', { class: 'qa-tag' }, strings[QA_CASE_STATUS_LABEL_KEY[scenario.caseStatus]])\n : null,\n scenario.grounding\n ? h('span', { class: 'qa-tag' }, strings[QA_GROUNDING_LABEL_KEY[scenario.grounding]])\n : null,\n provenance ? h('span', { class: 'qa-tag', 'data-testid': 'qa-provenance' }, provenance) : null,\n ),\n h(LaneIcons, { dev: lanes.dev, bot: lanes.bot, human: lanes.human, strings }),\n )\n}\n\nexport function TestPanel({ page, state, meta, strings }: TestPanelProps) {\n // Local UI state: which suite-group keys are expanded. Matches the donor's\n // `expanded` ref, which starts empty → all groups COLLAPSED by default (so a\n // heavily-loaded page folds into a few theme headers, per QaScenarioGroup).\n const [expanded, setExpanded] = useState<ReadonlySet<string>>(new Set())\n const toggleGroup = (key: string): void => {\n setExpanded((prev) => {\n const next = new Set(prev)\n if (next.has(key)) {\n next.delete(key)\n } else {\n next.add(key)\n }\n return next\n })\n }\n\n const freshness = interpolate(strings['panel.freshness'], {\n date: formatQaDate(meta.generated_at),\n version: shortVersion(meta.app_version),\n env: meta.environment,\n })\n\n let body\n if (state === 'no-tests') {\n body = h('p', { class: 'qa-panel__empty' }, strings['panel.noTests'])\n } else if (state === 'unknown-page') {\n body = h('p', { class: 'qa-panel__empty' }, strings['panel.unknownPage'])\n } else {\n const scenarios = page?.scenarios ?? []\n const hasGroups = scenarios.some((s) => s.group)\n if (scenarios.length === 0) {\n body = h('p', { class: 'qa-panel__empty' }, strings['panel.noScenarios'])\n } else if (!hasGroups) {\n // Synthetic / ungrouped page (donor `hasGroups === false`): flat list, no\n // suite header — avoids a spurious empty-label group wrapper.\n body = h(\n 'ul',\n { class: 'qa-panel__list' },\n scenarios.map((scenario) => h(ScenarioRow, { key: scenario.id, scenario, strings })),\n )\n } else {\n const groups = groupScenarios(scenarios)\n body = h(\n 'ul',\n { class: 'qa-panel__groups', 'data-testid': 'qa-suite-groups' },\n groups.map((group) => {\n const isOpen = expanded.has(group.key)\n const agg = {\n dev: aggregateTri(group.scenarios.map((s) => humanTriColor(s.dev))),\n bot: aggregateTri(group.scenarios.map((s) => botTriColor(s.automated))),\n human: aggregateTri(group.scenarios.map((s) => humanTriColor(s.client))),\n }\n return h(\n 'li',\n { key: group.key, class: 'qa-suite', 'data-testid': 'qa-panel-group' },\n h(\n 'button',\n {\n type: 'button',\n class: 'qa-suite__head',\n 'aria-expanded': isOpen,\n 'data-testid': 'qa-suite-group',\n onClick: () => toggleGroup(group.key),\n },\n h(\n 'span',\n { class: `qa-suite__chev ${isOpen ? 'is-open' : ''}`, 'aria-hidden': 'true' },\n '▸',\n ),\n h('span', { class: 'qa-suite__key' }, group.key),\n h('span', { class: 'qa-suite__label' }, group.label),\n h(\n 'span',\n { class: 'qa-suite__count' },\n interpolate(strings['suite.cases'], { count: group.scenarios.length }),\n ),\n h(LaneIcons, { dev: agg.dev, bot: agg.bot, human: agg.human, strings }),\n ),\n isOpen\n ? h(\n 'ul',\n { class: 'qa-suite__list' },\n group.scenarios.map((scenario) =>\n h(ScenarioRow, { key: scenario.id, scenario, strings }),\n ),\n )\n : null,\n )\n }),\n )\n }\n }\n\n return h(\n 'section',\n { class: 'qa-panel', 'data-testid': 'qa-panel' },\n body,\n h(\n 'footer',\n { class: 'qa-panel__foot' },\n h(\n 'span',\n { class: 'qa-panel__count', title: freshness, 'data-testid': 'qa-panel-freshness' },\n freshness,\n ),\n ),\n )\n}\n","import { h } from 'preact'\n\nimport type { QaTriColor } from './types'\nimport type { QaStrings } from './i18n'\n\n/**\n * A row of three independent lane chips for a scenario (or a suite aggregate):\n * - dev (`code`) = developer validation (the `</>` chevrons);\n * - bot (`bot`) = automated execution (Playwright);\n * - client (`human`) = human/client validation.\n *\n * Each chip carries its OWN tri-color (green/yellow/gray) — they are independent:\n * a human may have validated while the bot never ran, etc. Pure presentational:\n * no hooks, no state, no fetching.\n *\n * SVG path data copied verbatim from the donor `qa-lane-icons.vue`.\n */\nexport interface LaneIconsProps {\n dev: QaTriColor\n bot: QaTriColor\n human: QaTriColor\n strings: QaStrings\n}\n\n/** Tri-color → BEM modifier (donor mapping: `qa--green|qa--yellow|qa--gray`). */\nconst TRI_MODIFIER: Record<QaTriColor, string> = {\n green: 'qa--green',\n yellow: 'qa--yellow',\n gray: 'qa--gray',\n}\n\nexport function LaneIcons({ dev, bot, human, strings }: LaneIconsProps) {\n return h(\n 'span',\n { class: 'qa-lanes', 'data-testid': 'qa-lane-icons' },\n // dev — chevrons </>\n h(\n 'span',\n {\n class: `qa-lanes__icon qa-lane ${TRI_MODIFIER[dev]}`,\n title: strings['lane.code'],\n 'aria-label': strings['lane.code'],\n 'data-testid': 'qa-lane-dev',\n 'data-state': dev,\n },\n h(\n 'svg',\n { class: 'qa-lanes__svg', viewBox: '0 0 24 24', 'aria-hidden': 'true' },\n h('path', {\n d: 'M8 7l-5 5 5 5M16 7l5 5-5 5',\n fill: 'none',\n stroke: 'currentColor',\n 'stroke-width': '2.2',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n }),\n ),\n ),\n // bot — robot head\n h(\n 'span',\n {\n class: `qa-lanes__icon qa-lane ${TRI_MODIFIER[bot]}`,\n title: strings['lane.bot'],\n 'aria-label': strings['lane.bot'],\n 'data-testid': 'qa-lane-bot',\n 'data-state': bot,\n },\n h(\n 'svg',\n {\n class: 'qa-lanes__svg',\n viewBox: '0 0 24 24',\n 'aria-hidden': 'true',\n fill: 'none',\n stroke: 'currentColor',\n 'stroke-width': '2',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n },\n h('rect', { x: '4.5', y: '8.5', width: '15', height: '10.5', rx: '2.5' }),\n h('path', { d: 'M12 4.5v4' }),\n h('circle', { cx: '12', cy: '3.4', r: '1.3', fill: 'currentColor', stroke: 'none' }),\n h('path', { d: 'M2.5 12.5v3M21.5 12.5v3' }),\n h('circle', { cx: '9', cy: '13.5', r: '1.2', fill: 'currentColor', stroke: 'none' }),\n h('circle', { cx: '15', cy: '13.5', r: '1.2', fill: 'currentColor', stroke: 'none' }),\n ),\n ),\n // client — person\n h(\n 'span',\n {\n class: `qa-lanes__icon qa-lane ${TRI_MODIFIER[human]}`,\n title: strings['lane.human'],\n 'aria-label': strings['lane.human'],\n 'data-testid': 'qa-lane-client',\n 'data-state': human,\n },\n h(\n 'svg',\n {\n class: 'qa-lanes__svg',\n viewBox: '0 0 24 24',\n 'aria-hidden': 'true',\n fill: 'none',\n stroke: 'currentColor',\n 'stroke-width': '2',\n 'stroke-linecap': 'round',\n 'stroke-linejoin': 'round',\n },\n h('circle', { cx: '12', cy: '8', r: '3.6' }),\n h('path', { d: 'M5 20c0-3.6 3.1-6 7-6s7 2.4 7 6' }),\n ),\n ),\n )\n}\n","import { h } from 'preact'\nimport { useEffect, useMemo, useState } from 'preact/hooks'\n\nimport type { QaScenario, QaSitemapNode, QaStatusArtifact } from './types'\nimport type { QaStrings } from './i18n'\nimport { LaneIcons } from './LaneIcons'\nimport { normalizePattern } from './resolver'\nimport { botTriColor, formatQaDate, humanTriColor, QA_COLOR_MODIFIER, shortVersion } from './ui'\n\n/**\n * Preact port of the donor `qa-sitemap-modal.vue`.\n *\n * Presentational apart from the Escape-key listener (the one place a hook is\n * warranted — the donor used a `watch` + `window.addEventListener`). Data comes\n * in as the already-fetched artifact; this component owns no store or fetching.\n *\n * The donor's `<script setup>` tree reconstruction is extracted as the pure\n * exported `buildTree` helper so it's unit-testable and reusable by the mount\n * task.\n */\nexport interface SitemapModalProps {\n artifact: QaStatusArtifact\n currentPattern: string | null\n strings: QaStrings\n onClose: () => void\n}\n\n/** A node of the rebuilt sitemap tree, flattened depth-first for rendering. The\n * flat `artifact.sitemap` links children to parents via `parent_id`; we rebuild\n * the hierarchy and emit `depth` + `hasChildren` (donor `flatTree`). */\nexport interface TreeRow {\n node: QaSitemapNode\n depth: number\n hasChildren: boolean\n}\n\nexport function buildTree(nodes: ReadonlyArray<QaSitemapNode>): TreeRow[] {\n const childrenByParent = new Map<string | null, QaSitemapNode[]>()\n for (const node of nodes) {\n const bucket = childrenByParent.get(node.parent_id) ?? []\n bucket.push(node)\n childrenByParent.set(node.parent_id, bucket)\n }\n for (const bucket of childrenByParent.values()) {\n bucket.sort((a, b) => a.order - b.order)\n }\n const out: TreeRow[] = []\n const visit = (parentId: string | null, depth: number): void => {\n for (const node of childrenByParent.get(parentId) ?? []) {\n const children = childrenByParent.get(node.id) ?? []\n out.push({ node, depth, hasChildren: children.length > 0 })\n if (children.length > 0) {\n visit(node.id, depth + 1)\n }\n }\n }\n visit(null, 0)\n return out\n}\n\ninterface CaseEntry {\n scenario: QaScenario\n primaries: Set<string>\n traversed: Set<string>\n}\n\n/** Dedupe scenarios across pages by `external_key ?? id` (donor `dedupedCases`):\n * a cross-module/traversed case that appears on several pages lists ONCE, with\n * its primary/traversed pattern sets merged. */\nfunction dedupeScenarios(artifact: QaStatusArtifact): CaseEntry[] {\n const byKey = new Map<string, CaseEntry>()\n for (const page of artifact.pages) {\n for (const scenario of page.scenarios) {\n const key = scenario.external_key ?? scenario.id\n const entry = byKey.get(key) ?? { scenario, primaries: new Set<string>(), traversed: new Set<string>() }\n for (const ref of scenario.pages ?? []) {\n ;(ref.role === 'primary' ? entry.primaries : entry.traversed).add(ref.pattern)\n }\n byKey.set(key, entry)\n }\n }\n const rank = (p?: string): number => (p ? Number(p.slice(1)) : 9)\n return [...byKey.values()].sort((a, b) => {\n const ga = a.scenario.group?.key ?? '~'\n const gb = b.scenario.group?.key ?? '~'\n if (ga !== gb) {\n return ga.localeCompare(gb)\n }\n return rank(a.scenario.priority) - rank(b.scenario.priority) || a.scenario.title.localeCompare(b.scenario.title)\n })\n}\n\nfunction interpolate(template: string, params: Record<string, string | number>): string {\n return template.replace(/\\{(\\w+)\\}/g, (_, k: string) => String(params[k] ?? ''))\n}\n\n/** KPI formatting: the metric values are already whole percentages (0–100), as\n * emitted by `qa build` (`cli/src/qa/build.ts` rounds `100*num/den`). Render\n * them directly — do NOT multiply by 100, or a CLI artifact's `25` becomes\n * \"2500%\". See the QaMetric contract in `types.ts`. */\nfunction formatPct(pct: number): string {\n return `${Math.round(pct)}%`\n}\n\nexport function SitemapModal({ artifact, currentPattern, strings, onClose }: SitemapModalProps) {\n const [activeTab, setActiveTab] = useState<'pages' | 'scenarios'>('pages')\n const [search, setSearch] = useState('')\n\n // The one place a hook is warranted: close on Escape, cleaned up on unmount.\n useEffect(() => {\n const onKeydown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') {\n onClose()\n }\n }\n window.addEventListener('keydown', onKeydown)\n return () => window.removeEventListener('keydown', onKeydown)\n }, [onClose])\n\n const here = currentPattern ? normalizePattern(currentPattern) : null\n const tree = useMemo(() => buildTree(artifact.sitemap), [artifact])\n const cases = useMemo(() => dedupeScenarios(artifact), [artifact])\n\n const titleByPattern = useMemo(() => {\n const map = new Map<string, string>()\n for (const node of artifact.sitemap) {\n map.set(node.path_pattern, node.title)\n }\n return map\n }, [artifact])\n const patternTitle = (pattern: string): string => titleByPattern.get(pattern) ?? pattern\n\n const query = search.trim().toLowerCase()\n const filteredCases = query\n ? cases.filter(\n (c) =>\n c.scenario.title.toLowerCase().includes(query) ||\n (c.scenario.group?.label ?? '').toLowerCase().includes(query),\n )\n : cases\n\n const freshness = interpolate(strings['panel.freshness'], {\n date: formatQaDate(artifact.generated_at),\n version: shortVersion(artifact.app_version),\n env: artifact.environment,\n })\n\n return h(\n 'div',\n {\n class: 'qa-modal__backdrop',\n 'data-testid': 'qa-sitemap-modal',\n onClick: (e: MouseEvent) => {\n if (e.target === e.currentTarget) onClose()\n },\n },\n h(\n 'div',\n { class: 'qa-modal', role: 'dialog', 'aria-modal': 'true' },\n h(\n 'header',\n { class: 'qa-modal__head' },\n h('h3', null, strings['modal.title']),\n h(\n 'button',\n {\n type: 'button',\n class: 'qa-modal__close',\n title: strings['modal.close'],\n 'aria-label': strings['modal.close'],\n 'data-testid': 'qa-modal-close',\n onClick: onClose,\n },\n '×',\n ),\n ),\n h('div', { class: 'qa-modal__freshness' }, freshness),\n // Dual metric (doc 01 D12) — rendered as rounded percentages.\n h(\n 'div',\n { class: 'qa-kpi', 'data-testid': 'qa-kpi' },\n h(\n 'div',\n { class: 'qa-kpi__item' },\n h('span', { class: 'qa-kpi__value' }, formatPct(artifact.metric.pages_with_scenarios_pct)),\n h('span', { class: 'qa-kpi__label' }, strings['modal.kpiPagesCovered']),\n ),\n h(\n 'div',\n { class: 'qa-kpi__item' },\n h('span', { class: 'qa-kpi__value' }, formatPct(artifact.metric.scenarios_passing_pct)),\n h('span', { class: 'qa-kpi__label' }, strings['modal.kpiScenariosPassing']),\n ),\n ),\n h(\n 'nav',\n { class: 'qa-tabs' },\n h(\n 'button',\n {\n type: 'button',\n class: `qa-tabs__tab ${activeTab === 'pages' ? 'qa-tabs__tab--active' : ''}`,\n 'data-testid': 'qa-tab-pages',\n onClick: () => setActiveTab('pages'),\n },\n strings['modal.tabPages'],\n ),\n h(\n 'button',\n {\n type: 'button',\n class: `qa-tabs__tab ${activeTab === 'scenarios' ? 'qa-tabs__tab--active' : ''}`,\n 'data-testid': 'qa-tab-scenarios',\n onClick: () => setActiveTab('scenarios'),\n },\n strings['modal.tabScenarios'],\n ),\n ),\n h(\n 'div',\n { class: 'qa-modal__body' },\n activeTab === 'pages'\n ? h(\n 'ul',\n { class: 'qa-tree' },\n tree.map((row) => {\n const isHere = here !== null && row.node.path_pattern === here\n return h(\n 'li',\n {\n key: row.node.id,\n class: `qa-tree__row ${isHere ? 'qa-tree__row--here' : ''}`,\n 'data-testid': isHere ? 'qa-tree-current' : 'qa-sitemap-node',\n style: { paddingLeft: `${0.5 + row.depth * 1.1}rem` },\n },\n h(\n 'span',\n {\n class: `qa-dot ${QA_COLOR_MODIFIER[row.node.color]}`,\n 'aria-hidden': 'true',\n },\n ),\n h(\n 'span',\n {\n class: `qa-tree__title ${row.node.structural ? 'qa-tree__title--structural' : ''}`,\n },\n row.node.title || row.node.path_pattern,\n ),\n isHere ? h('span', { class: 'qa-tree__here' }, ` ${row.node.path_pattern} `) : null,\n isHere ? h('span', { class: 'qa-tree__here' }, strings['modal.youAreHere']) : null,\n h('span', { class: 'qa-tree__count' }, String(row.node.counts.scenarios)),\n )\n }),\n )\n : h(\n 'div',\n { class: 'qa-scenarios' },\n h('input', {\n type: 'search',\n class: 'qa-scenarios__search',\n placeholder: strings['modal.search'],\n 'data-testid': 'qa-scenarios-search',\n value: search,\n onInput: (e: Event) => setSearch((e.target as HTMLInputElement).value),\n }),\n filteredCases.length === 0\n ? h('p', { class: 'qa-scenarios__empty' }, strings['modal.noMatch'])\n : h(\n 'ul',\n { class: 'qa-scenarios__list' },\n filteredCases.map((entry) => {\n const scenario = entry.scenario\n const lanes = {\n dev: humanTriColor(scenario.dev),\n bot: botTriColor(scenario.automated),\n human: humanTriColor(scenario.client),\n }\n const traversed = [...entry.traversed].map(patternTitle)\n return h(\n 'li',\n {\n key: scenario.external_key ?? scenario.id,\n class: 'qa-scenarios__item',\n 'data-testid': 'qa-scenario-row',\n },\n h(\n 'div',\n { class: 'qa-scenarios__line' },\n h('span', { class: `qa-dot ${QA_COLOR_MODIFIER[scenario.color]}`, 'aria-hidden': 'true' }),\n scenario.group?.key\n ? h('span', { class: 'qa-suitekey' }, scenario.group.key)\n : null,\n h('span', { class: 'qa-scenarios__title' }, scenario.title),\n scenario.source === 'testgen'\n ? h('span', { class: 'qa-tag' }, strings['source.testgen'])\n : null,\n h(LaneIcons, { dev: lanes.dev, bot: lanes.bot, human: lanes.human, strings }),\n ),\n h(\n 'div',\n { class: 'qa-scenarios__pages' },\n entry.primaries.size\n ? h(\n 'span',\n { class: 'qa-scenarios__primary' },\n interpolate(strings['modal.appliesTo'], { count: entry.primaries.size }),\n )\n : null,\n traversed.length\n ? h(\n 'span',\n { class: 'qa-scenarios__traversed' },\n `${strings['modal.traversed']}: ${traversed.join(', ')}`,\n )\n : null,\n ),\n )\n }),\n ),\n ),\n ),\n ),\n )\n}\n","import type { QaStatusArtifact } from './types'\nimport { createQaMeter as createQaMeterImpl } from './mount'\n\nexport interface QaMeterOptions {\n /** Where the QA status artifact comes from. An inline object renders\n * immediately; a URL is `fetch`ed lazily on first open/hover; a function is\n * invoked lazily likewise. */\n source: string | QaStatusArtifact | (() => Promise<QaStatusArtifact>)\n /** Router-aware override. Defaults to a routerless resolver over the\n * artifact's path_patterns + `window.location.pathname`. */\n getCurrentPage?: () => string | null\n locale?: 'fr' | 'en'\n /** Pastille size. 'md' (48px, default) for standalone; the feedback-widget\n * integration passes 'sm' (24px) so the QA FAB reads as secondary. */\n size?: 'sm' | 'md'\n position?: { right?: number; bottom?: number }\n}\n\nexport interface QaMeterHandle {\n open(): void\n close(): void\n dispose(): void\n}\n\nexport function createQaMeter(options: QaMeterOptions): QaMeterHandle {\n return createQaMeterImpl(options)\n}\n\nexport type {\n QaStatusArtifact,\n QaColor,\n QaScenario,\n QaPageStatus,\n QaSitemapNode,\n} from './types'\n"],"mappings":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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","names":["h","useEffect","useState","h","h","h","h","useState","interpolate","useState","h","useState","useEffect","h","createQaMeter"]}
package/dist/react.d.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode } from 'react';
3
- import { I as InternalConfig } from './index-Bq4QI0xa.js';
3
+ import { I as InternalConfig } from './index-CmHNkJsj.js';
4
4
  import { ErrorTrackingOptions } from './error-tracking.js';
5
- import { F as FeedbackApi } from './types-2dZ9bHAn.js';
5
+ import { Q as QaMeterConfig, F as FeedbackApi } from './types-CfSqqfkw.js';
6
+ import './types-CRbb2Pp0.js';
6
7
 
7
8
  interface FeedbackProviderProps extends InternalConfig {
8
9
  children?: ReactNode;
10
+ /**
11
+ * Opt-in QA Meter second FAB — set `qaMeter={{ source: '/qa-status.json' }}`.
12
+ * Inherited from FeedbackConfig and forwarded via `...config`; redeclared
13
+ * here so editors surface the docs on hover.
14
+ */
15
+ qaMeter?: QaMeterConfig;
9
16
  /**
10
17
  * Auto-capture uncaught errors + unhandled rejections as synthetic
11
18
  * reports (see `withErrorTracking`). On by default — the happy path is
package/dist/react.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createFeedback
3
- } from "./chunk-YV7LMW22.mjs";
3
+ } from "./chunk-6OAG72JW.mjs";
4
4
  import {
5
5
  withErrorTracking
6
6
  } from "./chunk-QJ562NTG.mjs";
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/react/FeedbackProvider.tsx"],"sourcesContent":["/** @jsxImportSource react */\nimport { createContext, useContext, useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\n\nimport { createFeedback, type InternalConfig } from '../core'\nimport { withErrorTracking, type ErrorTrackingOptions } from '../modules/error-tracking'\nimport type { FeedbackApi } from '../types'\n\nconst FeedbackContext = createContext<FeedbackApi | null>(null)\n\ninterface FeedbackProviderProps extends InternalConfig {\n children?: ReactNode\n /**\n * Auto-capture uncaught errors + unhandled rejections as synthetic\n * reports (see `withErrorTracking`). On by default — the happy path is\n * batteries-included. Pass `false` to opt out, or an options object to\n * tune the cooldown / sampling / rate caps.\n */\n errorTracking?: boolean | ErrorTrackingOptions\n}\n\nexport function FeedbackProvider({\n children,\n errorTracking = true,\n ...config\n}: FeedbackProviderProps) {\n const apiRef = useRef<FeedbackApi | null>(null)\n const [api, setApi] = useState<FeedbackApi | null>(null)\n\n useEffect(() => {\n const instance = createFeedback(config)\n if (errorTracking !== false) {\n withErrorTracking(instance, errorTracking === true ? undefined : errorTracking)\n }\n apiRef.current = instance\n setApi(instance)\n return () => {\n instance.shutdown()\n apiRef.current = null\n setApi(null)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.apiKey])\n\n // Only render children once the api instance is ready so useFeedback()\n // never sees a null context value.\n if (!api) return null\n\n return <FeedbackContext.Provider value={api}>{children}</FeedbackContext.Provider>\n}\n\nexport { FeedbackContext }\n\nexport function useFeedback(): FeedbackApi {\n const api = useContext(FeedbackContext)\n if (!api) throw new Error('useFeedback must be used inside <FeedbackProvider>')\n return api\n}\n"],"mappings":";;;;;;;;;;AACA,SAAS,eAAe,YAAY,WAAW,QAAQ,gBAAgB;AA+C9D;AAxCT,IAAM,kBAAkB,cAAkC,IAAI;AAavD,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,gBAAgB;AAAA,EAChB,GAAG;AACL,GAA0B;AACxB,QAAM,SAAS,OAA2B,IAAI;AAC9C,QAAM,CAAC,KAAK,MAAM,IAAI,SAA6B,IAAI;AAEvD,YAAU,MAAM;AACd,UAAM,WAAW,eAAe,MAAM;AACtC,QAAI,kBAAkB,OAAO;AAC3B,wBAAkB,UAAU,kBAAkB,OAAO,SAAY,aAAa;AAAA,IAChF;AACA,WAAO,UAAU;AACjB,WAAO,QAAQ;AACf,WAAO,MAAM;AACX,eAAS,SAAS;AAClB,aAAO,UAAU;AACjB,aAAO,IAAI;AAAA,IACb;AAAA,EAEF,GAAG,CAAC,OAAO,MAAM,CAAC;AAIlB,MAAI,CAAC,IAAK,QAAO;AAEjB,SAAO,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,KAAM,UAAS;AACzD;AAIO,SAAS,cAA2B;AACzC,QAAM,MAAM,WAAW,eAAe;AACtC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,oDAAoD;AAC9E,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../src/react/FeedbackProvider.tsx"],"sourcesContent":["/** @jsxImportSource react */\nimport { createContext, useContext, useEffect, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\n\nimport { createFeedback, type InternalConfig } from '../core'\nimport { withErrorTracking, type ErrorTrackingOptions } from '../modules/error-tracking'\nimport type { FeedbackApi, QaMeterConfig } from '../types'\n\nconst FeedbackContext = createContext<FeedbackApi | null>(null)\n\ninterface FeedbackProviderProps extends InternalConfig {\n children?: ReactNode\n /**\n * Opt-in QA Meter second FAB — set `qaMeter={{ source: '/qa-status.json' }}`.\n * Inherited from FeedbackConfig and forwarded via `...config`; redeclared\n * here so editors surface the docs on hover.\n */\n qaMeter?: QaMeterConfig\n /**\n * Auto-capture uncaught errors + unhandled rejections as synthetic\n * reports (see `withErrorTracking`). On by default — the happy path is\n * batteries-included. Pass `false` to opt out, or an options object to\n * tune the cooldown / sampling / rate caps.\n */\n errorTracking?: boolean | ErrorTrackingOptions\n}\n\nexport function FeedbackProvider({\n children,\n errorTracking = true,\n ...config\n}: FeedbackProviderProps) {\n const apiRef = useRef<FeedbackApi | null>(null)\n const [api, setApi] = useState<FeedbackApi | null>(null)\n\n useEffect(() => {\n const instance = createFeedback(config)\n if (errorTracking !== false) {\n withErrorTracking(instance, errorTracking === true ? undefined : errorTracking)\n }\n apiRef.current = instance\n setApi(instance)\n return () => {\n instance.shutdown()\n apiRef.current = null\n setApi(null)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [config.apiKey])\n\n // Only render children once the api instance is ready so useFeedback()\n // never sees a null context value.\n if (!api) return null\n\n return <FeedbackContext.Provider value={api}>{children}</FeedbackContext.Provider>\n}\n\nexport { FeedbackContext }\n\nexport function useFeedback(): FeedbackApi {\n const api = useContext(FeedbackContext)\n if (!api) throw new Error('useFeedback must be used inside <FeedbackProvider>')\n return api\n}\n"],"mappings":";;;;;;;;;;AACA,SAAS,eAAe,YAAY,WAAW,QAAQ,gBAAgB;AAqD9D;AA9CT,IAAM,kBAAkB,cAAkC,IAAI;AAmBvD,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,gBAAgB;AAAA,EAChB,GAAG;AACL,GAA0B;AACxB,QAAM,SAAS,OAA2B,IAAI;AAC9C,QAAM,CAAC,KAAK,MAAM,IAAI,SAA6B,IAAI;AAEvD,YAAU,MAAM;AACd,UAAM,WAAW,eAAe,MAAM;AACtC,QAAI,kBAAkB,OAAO;AAC3B,wBAAkB,UAAU,kBAAkB,OAAO,SAAY,aAAa;AAAA,IAChF;AACA,WAAO,UAAU;AACjB,WAAO,QAAQ;AACf,WAAO,MAAM;AACX,eAAS,SAAS;AAClB,aAAO,UAAU;AACjB,aAAO,IAAI;AAAA,IACb;AAAA,EAEF,GAAG,CAAC,OAAO,MAAM,CAAC;AAIlB,MAAI,CAAC,IAAK,QAAO;AAEjB,SAAO,oBAAC,gBAAgB,UAAhB,EAAyB,OAAO,KAAM,UAAS;AACzD;AAIO,SAAS,cAA2B;AACzC,QAAM,MAAM,WAAW,eAAe;AACtC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,oDAAoD;AAC9E,SAAO;AACT;","names":[]}
package/dist/replay.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { F as FeedbackApi } from './types-2dZ9bHAn.js';
1
+ import { F as FeedbackApi } from './types-CfSqqfkw.js';
2
+ import './types-CRbb2Pp0.js';
2
3
 
3
4
  interface ReplayOptions {
4
5
  durationMs?: number;
@@ -1,4 +1,5 @@
1
- import { F as FeedbackApi } from './types-2dZ9bHAn.js';
1
+ import { F as FeedbackApi } from './types-CfSqqfkw.js';
2
+ import './types-CRbb2Pp0.js';
2
3
 
3
4
  /**
4
5
  * withTelemetryStream — opt-in module that streams already-captured browser
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Contrat de données du QA Meter temporaire.
3
+ *
4
+ * Calqué à l'identique sur les réponses de la spec plateforme (doc 03 §5.1 :
5
+ * `page-status` + `sitemap`). Le backend temporaire (`backend/src/qa-meter/`)
6
+ * sert un artefact qui BUNDLE ces deux formes — au swap-out, le widget
7
+ * plateforme renverra exactement les mêmes objets, endpoint par endpoint, donc
8
+ * ces types survivent au retrait du meter temporaire.
9
+ *
10
+ * AUCUNE couleur n'est calculée ici : la couleur est calculée à la construction
11
+ * de l'artefact (fonction pure §9.3, côté `qa-build-status`) et stockée. L'UI ne
12
+ * fait que la rendre, et lit les flags de staleness (`stale`) pour le style
13
+ * « contredit ».
14
+ */
15
+ type QaColor = 'green' | 'yellow' | 'red' | 'gray';
16
+ type QaAutomatedStatus = 'passing' | 'failing' | 'flaky' | 'never_run';
17
+ /** Volet « automated » d'un scénario (doc 03 §5.1). */
18
+ interface QaAutomatedLane {
19
+ status: QaAutomatedStatus;
20
+ at?: string;
21
+ app_version?: string;
22
+ /** Résumé de provenance affiché dans le tooltip — ex. « laptop · clone · healed » (doc 03 §1.5). */
23
+ provenance?: string;
24
+ }
25
+ /** Volet humain (« dev » ou « client ») d'un scénario. */
26
+ interface QaHumanLane {
27
+ validated: boolean;
28
+ /** Calculé (jamais stocké) : la validation précède un run en échec ou une nouvelle version (doc 03 §9.2/§9.4). */
29
+ stale: boolean;
30
+ /** Posé quand le dernier événement humain est `rejected`. */
31
+ rejected?: boolean;
32
+ by?: string;
33
+ at?: string;
34
+ app_version?: string;
35
+ note?: string;
36
+ }
37
+ interface QaScenarioPageRef {
38
+ pattern: string;
39
+ role: 'primary' | 'traversed';
40
+ }
41
+ /** Priorité d'un cas de la suite (doc QA `TEST-SUITE-FORMAT.md` §1). */
42
+ type QaPriority = 'P0' | 'P1' | 'P2' | 'P3';
43
+ /** Statut d'un cas dans la suite (`TEST-SUITE-FORMAT.md` §3) — distinct de la couleur. */
44
+ type QaCaseStatus = 'covered' | 'manual' | 'blocked' | 'open-question';
45
+ /** Ancrage de l'attendu (`TEST-SUITE-FORMAT.md` §2). */
46
+ type QaGrounding = 'verified' | 'confirm-live' | 'product-question';
47
+ /**
48
+ * Groupe d'un scénario = la « suite » de son identifiant `TC-<Suite><n>` (lettre +
49
+ * thème). Sert à replier les pages chargées (ex. un onglet ingrédient ≈ 30 cas) en
50
+ * quelques en-têtes par thème (nominal, bornes, réseau…) — cf. demande « taille ».
51
+ */
52
+ interface QaScenarioGroup {
53
+ key: string;
54
+ label: string;
55
+ }
56
+ /** Un scénario QA avec sa trinité de volets (doc 03 §5.1). */
57
+ interface QaScenario {
58
+ id: string;
59
+ external_key?: string;
60
+ title: string;
61
+ color: QaColor;
62
+ source?: 'reporter' | 'testgen' | 'manual';
63
+ spec_path?: string;
64
+ pages?: QaScenarioPageRef[];
65
+ /** Présent pour les cas issus des suites réelles (`test-suite/`) ; absent pour le synthétique. */
66
+ group?: QaScenarioGroup;
67
+ priority?: QaPriority;
68
+ caseStatus?: QaCaseStatus;
69
+ grounding?: QaGrounding;
70
+ automated: QaAutomatedLane;
71
+ dev: QaHumanLane;
72
+ client: QaHumanLane;
73
+ }
74
+ interface QaCounts {
75
+ scenarios: number;
76
+ passing: number;
77
+ dev_validated: number;
78
+ client_validated: number;
79
+ }
80
+ /** Réponse `GET …/widget/page-status/` (doc 03 §5.1), une par page dans l'artefact temporaire. */
81
+ interface QaPageStatus {
82
+ page: {
83
+ id: string;
84
+ path_pattern: string;
85
+ title: string;
86
+ } | null;
87
+ color: QaColor;
88
+ counts: QaCounts;
89
+ scenarios: QaScenario[];
90
+ }
91
+ interface QaSitemapRollup {
92
+ pages_total: number;
93
+ pages_green: number;
94
+ scenarios_total: number;
95
+ scenarios_green: number;
96
+ }
97
+ /** Un nœud plat de l'arbre `GET …/widget/sitemap/` (doc 03 §5.1) ; le client reconstruit l'arbre. */
98
+ interface QaSitemapNode {
99
+ id: string;
100
+ path_pattern: string;
101
+ title: string;
102
+ parent_id: string | null;
103
+ order: number;
104
+ structural?: boolean;
105
+ color: QaColor;
106
+ counts: QaCounts;
107
+ rollup: QaSitemapRollup;
108
+ }
109
+ /** Métrique double (doc 01 D12) : % de pages couvertes / % de scénarios passants à la version courante. */
110
+ interface QaMetric {
111
+ pages_with_scenarios_pct: number;
112
+ scenarios_passing_pct: number;
113
+ }
114
+ /**
115
+ * L'artefact servi par `GET /qa-meter/status` — bundle de la forme §5.1.
116
+ * `format: 1` est validé par le backend (B4) et par le store avant rendu.
117
+ */
118
+ interface QaStatusArtifact {
119
+ format: number;
120
+ environment: string;
121
+ app_version: string;
122
+ generated_at: string;
123
+ pages: QaPageStatus[];
124
+ sitemap: QaSitemapNode[];
125
+ metric: QaMetric;
126
+ }
127
+
128
+ export type { QaStatusArtifact as Q, QaColor as a, QaPageStatus as b, QaScenario as c, QaSitemapNode as d };
@@ -1,3 +1,5 @@
1
+ import { Q as QaStatusArtifact } from './types-CRbb2Pp0.js';
2
+
1
3
  type FeedbackEnv = 'dev' | 'staging' | 'prod' | (string & {});
2
4
  declare global {
3
5
  var __MFB_VERSION__: string | undefined;
@@ -21,6 +23,24 @@ interface UserIdentity {
21
23
  /** Unix-seconds expiry of the signature, e.g. `Math.floor(Date.now()/1000) + 3600`. */
22
24
  exp?: number;
23
25
  }
26
+ /** Opt-in QA Meter (second FAB) configuration. Presence of `source` is what
27
+ * enables the FAB. The host serves its own `qa-status.json` (built by
28
+ * `mhosaic-feedback qa refresh`); no backend involved. */
29
+ interface QaMeterConfig {
30
+ /** URL string | inline artifact | async loader. */
31
+ source: string | QaStatusArtifact | (() => Promise<QaStatusArtifact>);
32
+ /** Defaults to the feedback widget's resolved locale. */
33
+ locale?: 'fr' | 'en';
34
+ /** Override the auto-stack offset (px from the viewport edges). */
35
+ position?: {
36
+ right?: number;
37
+ bottom?: number;
38
+ };
39
+ /** Pastille size. Defaults to 'sm' in the integrated path. */
40
+ size?: 'sm' | 'md';
41
+ /** Router-aware page override; defaults to the routerless resolver. */
42
+ getCurrentPage?: () => string | null;
43
+ }
24
44
  interface FeedbackConfig {
25
45
  apiKey: string;
26
46
  /**
@@ -47,6 +67,8 @@ interface FeedbackConfig {
47
67
  locale?: string;
48
68
  translations?: Record<string, string>;
49
69
  theme?: 'light' | 'dark' | 'auto' | 'none';
70
+ /** Opt-in QA Meter second FAB. Set `qaMeter.source` to enable it. */
71
+ qaMeter?: QaMeterConfig;
50
72
  }
51
73
  interface ConsoleEntry {
52
74
  level: string;
@@ -170,4 +192,4 @@ interface FeedbackApi {
170
192
  }
171
193
  type ReportTransformer = (payload: ReportPayload) => ReportPayload | Promise<ReportPayload>;
172
194
 
173
- export type { CapturedContext as C, FeedbackApi as F, ReportTransformer as R, SubmittedReport as S, UserIdentity as U, FeedbackConfig as a, FeedbackEnv as b, FeedbackSeverity as c, FeedbackType as d, ReportPayload as e };
195
+ export type { CapturedContext as C, FeedbackApi as F, QaMeterConfig as Q, ReportTransformer as R, SubmittedReport as S, UserIdentity as U, FeedbackConfig as a, FeedbackEnv as b, FeedbackSeverity as c, FeedbackType as d, ReportPayload as e };
@@ -1,4 +1,5 @@
1
- import { F as FeedbackApi } from './types-2dZ9bHAn.js';
1
+ import { F as FeedbackApi } from './types-CfSqqfkw.js';
2
+ import './types-CRbb2Pp0.js';
2
3
 
3
4
  interface WebVitalsOptions {
4
5
  reportAllChanges?: boolean;