src/lib/form/field-overlay.ts

Total Symbols
3
Lines of Code
139
Avg Complexity
3.3
Symbol Types
1

Symbols by Kind

function 3

All Symbols

Name Kind Visibility Status Lines Signature
ensureStyles function - 61-90 ensureStyles(): : void
showDetectionBadge function exported- 96-131 showDetectionBadge( element: Element, fieldType: string, method?: string, ): : void
clearAllBadges function exported- 136-138 clearAllBadges(): : void

Full Source

/**
 * Field Detection Overlay
 *
 * Renders ephemeral type-badge annotations on detected form fields,
 * providing real-time visual feedback during streaming detection.
 *
 * Usage:
 *   for await (const field of streamAllFields()) {
 *     showDetectionBadge(field.element, field.fieldType, field.detectionMethod);
 *   }
 *   // badges auto-fade — or call clearAllBadges() to remove immediately
 */

const BADGE_ATTR = "data-fill-all-badge";
const STYLE_ID = "fill-all-overlay-styles";
const BADGE_LIFETIME_MS = 3500;

/** Color per field type — maps to the same palette used in the inspect modal. */
const TYPE_COLOR: Record<string, string> = {
  cpf: "#dc2626",
  cnpj: "#b91c1c",
  rg: "#ef4444",
  email: "#2563eb",
  phone: "#7c3aed",
  "full-name": "#15803d",
  "first-name": "#16a34a",
  "last-name": "#4ade80",
  name: "#22c55e",
  address: "#d97706",
  street: "#b45309",
  city: "#a16207",
  state: "#854d0e",
  cep: "#ca8a04",
  "zip-code": "#92400e",
  date: "#0891b2",
  "birth-date": "#0e7490",
  password: "#6d28d9",
  username: "#8b5cf6",
  company: "#9333ea",
  money: "#16a34a",
  number: "#475569",
  text: "#64748b",
  select: "#06b6d4",
  checkbox: "#0891b2",
  radio: "#0e7490",
  unknown: "#94a3b8",
};

/** Small icon indicating which classifier detected the type. */
const METHOD_ICON: Partial<Record<string, string>> = {
  "html-type": "⚡",
  keyword: "🔑",
  tensorflow: "🧠",
  "chrome-ai": "✨",
  "html-fallback": "❓",
  "custom-select": "📋",
  interactive: "🎛",
  "user-override": "👤",
};

function ensureStyles(): void {
  if (document.getElementById(STYLE_ID)) return;

  const style = document.createElement("style");
  style.id = STYLE_ID;
  style.textContent = `
    [${BADGE_ATTR}] {
      position: fixed;
      z-index: 2147483645;
      font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
      font-size: 9px;
      font-weight: 700;
      line-height: 1;
      padding: 2px 5px 3px;
      border-radius: 3px;
      color: #fff;
      pointer-events: none;
      white-space: nowrap;
      opacity: 0;
      transition: opacity 0.15s ease;
      box-shadow: 0 1px 4px rgba(0,0,0,0.3);
      letter-spacing: 0.3px;
      text-transform: uppercase;
    }
    [${BADGE_ATTR}].fa-badge-in] {
      opacity: 1;
    }
  `;
  document.head.appendChild(style);
}

/**
 * Shows a colored type badge anchored just above the top-right corner of the
 * given element. The badge fades out automatically after BADGE_LIFETIME_MS.
 */
export function showDetectionBadge(
  element: Element,
  fieldType: string,
  method?: string,
): void {
  ensureStyles();

  const rect = element.getBoundingClientRect();
  if (rect.width === 0 && rect.height === 0) return;

  const badge = document.createElement("div");
  badge.setAttribute(BADGE_ATTR, "");
  badge.style.background = TYPE_COLOR[fieldType] ?? "#64748b";

  const icon = method ? (METHOD_ICON[method] ?? "") : "";
  badge.textContent = icon ? `${icon} ${fieldType}` : fieldType;

  // Anchor: just above the top-right corner of the field
  badge.style.top = `${Math.max(0, rect.top - 20)}px`;
  badge.style.left = `${rect.right}px`;
  badge.style.transform = "translateX(-100%)";

  document.body.appendChild(badge);

  // Animate in on next frame
  requestAnimationFrame(() => {
    badge.style.opacity = "1";
  });

  // Auto-fade and remove
  setTimeout(() => {
    badge.style.transition = "opacity 0.3s ease";
    badge.style.opacity = "0";
    setTimeout(() => badge.remove(), 320);
  }, BADGE_LIFETIME_MS);
}

/**
 * Immediately removes all active detection badges from the page.
 */
export function clearAllBadges(): void {
  document.querySelectorAll(`[${BADGE_ATTR}]`).forEach((el) => el.remove());
}