Exclusivobeta

Foresee <mv-foresee>

Prevê qual controle o usuário está prestes a clicar a partir da trajetória do ponteiro, antes do clique, e mantém a contagem dos próprios acertos. Envolve qualquer região de links e botões e acompanha a aproximação desde até 240px fora dela; a direção (o raio do movimento cruza o alvo?), a distância e a frenagem (as pessoas desaceleram ao chegar ao alvo: o ponto de parada é estimado a partir da velocidade e da desaceleração) são pontuadas por candidato, e quando o melhor se mantém acima do limite ele recebe data-foresee="foreseen", uma variável --mv-foresee-confidence e um anel discreto que fica mais firme com a confiança. mv-foresee (cancelável) permite que o app prepare qualquer coisa com antecedência (buscar dados, carregar o código de um diálogo), e o atributo prefetch insere um <link rel="prefetch"> por link da mesma origem. Cada clique é então comparado com a previsão vigente (mv-verdict: hit ou dissent, vantagem em ms) e stats resume a precisão e a vantagem média. O foco do teclado e os toques contam como previsões certas, então todo método de entrada ganha a vantagem; show-indicator="false" o executa de forma invisível, como pura inteligência de prefetch.

CategoriaCursores
TipoWeb Component (<mv-foresee>)
Statusbeta
Keywordsexclusive, culture, cursor, prediction, prefetch, preload, intent, trajectory, pointer, performance, navigation, speculation, analytics, anticipation

When to use

  • A navigation grid or dashboard should start loading the next page or its data before the user actually clicks
  • A heavy dialog, editor or route chunk should be warmed up only when the user is clearly heading for its trigger
  • A team wants to measure how predictable its navigation is (accuracy and head start per click) before investing in prefetching
  • Hover-based prefetching fires too late or too often and a trajectory-based signal is needed

Avoid when

  • The targets are few, far apart and cheap to load: plain links already feel instant
  • The goal is a decorative effect that follows the cursor → use Cursor Follower instead
  • Prefetching has side effects (GET links that mutate state, metered data): only use the event, never the prefetch attribute

Instalação

node scripts/add.mjs foresee --out ./src/marvelous

Agente de IA com o servidor MCP do Marvelous UI: install_components({ slugs: ["foresee"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Arquivos copiados (dependências incluídas): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, core/observe.js, components/foresee/foresee.js, components/foresee/foresee.css.

Uso

Marcação de referência, para usar como ponto de partida e personalizar com atributos, data-* e variáveis CSS:

<div id="pc-demo" style="width:min(100%,64rem);margin-inline:auto">
  <style>
    #pc-demo { --pc-gap: .875rem }
    #pc-demo .pc-layout { display:grid; grid-template-columns:minmax(0,1fr) 17rem; gap:1.25rem; align-items:start }
    #pc-demo .pc-top { display:flex; align-items:center; justify-content:space-between; gap:.75rem 1rem; flex-wrap:wrap; margin:0 0 1rem }
    #pc-demo .pc-top h3 { margin:0; font-size:1.125rem; letter-spacing:-.01em }
    #pc-demo .pc-top p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #pc-demo .pc-actions { display:flex; gap:.5rem; flex-wrap:wrap }
    #pc-demo .pc-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--pc-gap) }
    #pc-demo .pc-card { display:grid; grid-template-columns:auto minmax(0,1fr); gap:.25rem .75rem; align-items:start; padding:1rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface); color:inherit; text-decoration:none; transition:border-color var(--mv-duration-fast), background-color var(--mv-duration-fast) }
    #pc-demo .pc-card:hover { border-color:var(--mv-border-strong) }
    #pc-demo .pc-card:focus-visible { outline:none; box-shadow:var(--mv-focus-ring) }
    #pc-demo .pc-card[data-foresee="foreseen"] { background:color-mix(in oklab, var(--mv-accent) calc(3% * var(--mv-foresee-confidence, 0) + 1%), var(--mv-surface)) }
    #pc-demo .pc-ico { grid-row:span 2; display:grid; place-items:center; width:2.25rem; height:2.25rem; border-radius:var(--mv-radius-lg); background:var(--mv-bg-muted); color:var(--mv-fg-muted) }
    #pc-demo .pc-ico svg { width:1.125rem; height:1.125rem }
    #pc-demo .pc-card b { font-size:.875rem; font-weight:600 }
    #pc-demo .pc-card span.pc-d { color:var(--mv-fg-muted); font-size:.75rem; line-height:1.4 }
    #pc-demo .pc-hud { position:sticky; top:1rem; display:grid; gap:1rem; padding:1.125rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface-raised); box-shadow:var(--mv-shadow-sm); font-size:.8125rem }
    #pc-demo .pc-hud h4 { display:flex; align-items:center; justify-content:space-between; margin:0; font-size:.8125rem; font-weight:600 }
    #pc-demo .pc-k { color:var(--mv-fg-muted); font-size:.6875rem; letter-spacing:.04em; text-transform:uppercase; font-weight:600 }
    #pc-demo .pc-now { display:grid; gap:.375rem }
    #pc-demo .pc-target { font-size:1rem; font-weight:600; letter-spacing:-.01em; min-height:1.5rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #pc-demo .pc-target[data-empty] { color:var(--mv-fg-subtle); font-weight:500; font-size:.875rem }
    #pc-demo .pc-bar { position:relative; height:6px; border-radius:3px; background:var(--mv-bg-muted); overflow:hidden }
    #pc-demo .pc-bar i { position:absolute; inset:0; border-radius:inherit; background:var(--mv-accent); transform-origin:left; scale:var(--v,0) 1; transition:scale 120ms linear }
    #pc-demo .pc-bar-row { display:flex; justify-content:space-between; color:var(--mv-fg-muted); font-size:.75rem; font-variant-numeric:tabular-nums }
    #pc-demo .pc-stats { display:grid; grid-template-columns:1fr 1fr; gap:.75rem; margin:0 }
    #pc-demo .pc-stats div { display:grid; gap:.125rem; padding:.625rem .75rem; border-radius:var(--mv-radius-lg); background:var(--mv-bg-subtle) }
    #pc-demo .pc-stats dt { color:var(--mv-fg-muted); font-size:.6875rem }
    #pc-demo .pc-stats dd { margin:0; font-size:1.25rem; font-weight:650; letter-spacing:-.02em; font-variant-numeric:tabular-nums }
    #pc-demo .pc-stats dd small { font-size:.6875rem; font-weight:500; color:var(--mv-fg-muted); letter-spacing:0 }
    #pc-demo .pc-log { list-style:none; margin:0; padding:0; display:grid; gap:.375rem }
    #pc-demo .pc-log li { display:flex; justify-content:space-between; gap:.5rem; font-size:.75rem; color:var(--mv-fg-muted) }
    #pc-demo .pc-log li b { color:var(--mv-fg); font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    #pc-demo .pc-log li[data-hit="false"] b::after { content:" · dissent"; color:var(--mv-fg-subtle); font-weight:400 }
    #pc-demo .pc-log li span { font-variant-numeric:tabular-nums; white-space:nowrap }
    #pc-demo .pc-log .pc-empty { color:var(--mv-fg-subtle) }
    #pc-demo .pc-sep { height:1px; background:var(--mv-border) }
    #pc-demo .pc-hint { margin:.875rem 0 0; color:var(--mv-fg-muted); font-size:.75rem; text-align:center }
    @media (max-width:52rem) {
      #pc-demo .pc-layout { grid-template-columns:minmax(0,1fr) }
      #pc-demo .pc-hud { position:static }
    }
    @media (max-width:36rem) { #pc-demo .pc-grid { grid-template-columns:minmax(0,1fr) } }
  </style>

  <div class="pc-layout">
    <mv-foresee id="pc-region" prefetch>
      <div class="pc-top">
        <div>
          <h3>Workspace</h3>
          <p>Northwind Labs · 14 members</p>
        </div>
        <div class="pc-actions">
          <button class="mv-button" data-variant="outline" data-size="sm" type="button">Invite</button>
          <button class="mv-button" data-variant="outline" data-size="sm" type="button">Export</button>
          <button class="mv-button" data-size="sm" type="button">New project</button>
        </div>
      </div>
      <nav class="pc-grid" aria-label="Workspace sections">
        <a class="pc-card" href="#overview"><span class="pc-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg></span><b>Overview</b><span class="pc-d">Key metrics and recent activity</span></a>
        <a class="pc-card" href="#projects"><span class="pc-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></span><b>Projects</b><span class="pc-d">12 active · 3 due this week</span></a>
        <a class="pc-card" href="#analytics"><span class="pc-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20V10"/><path d="M10 20V4"/><path d="M16 20v-7"/><path d="M22 20H2"/></svg></span><b>Analytics</b><span class="pc-d">Traffic, funnels and retention</span></a>
        <a class="pc-card" href="#docs"><span class="pc-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M9 13h6M9 17h4"/></svg></span><b>Documentation</b><span class="pc-d">Guides, API reference, changelog</span></a>
        <a class="pc-card" href="#billing"><span class="pc-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2.5" y="5" width="19" height="14" rx="2"/><path d="M2.5 10h19"/><path d="M6.5 15h3"/></svg></span><b>Billing</b><span class="pc-d">Pro plan · $240 / month</span></a>
        <a class="pc-card" href="#team"><span class="pc-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0"/><path d="M16 4.5a3.5 3.5 0 0 1 0 7"/><path d="M18.5 14.5a6.5 6.5 0 0 1 3 5.5"/></svg></span><b>Team</b><span class="pc-d">Roles, invitations and access</span></a>
        <a class="pc-card" href="#integrations"><span class="pc-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 2v6M15 2v6"/><path d="M6 8h12v4a6 6 0 0 1-12 0z"/><path d="M12 18v4"/></svg></span><b>Integrations</b><span class="pc-d">Slack, GitHub and 20 more</span></a>
        <a class="pc-card" href="#security"><span class="pc-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2.5 4 5.5v6c0 4.8 3.4 8.7 8 10 4.6-1.3 8-5.2 8-10v-6z"/><path d="m8.8 12 2.2 2.2 4.2-4.4"/></svg></span><b>Security</b><span class="pc-d">SSO, audit log, API keys</span></a>
        <a class="pc-card" href="#settings"><span class="pc-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg></span><b>Settings</b><span class="pc-d">Workspace name, region, defaults</span></a>
      </nav>
    </mv-foresee>

    <aside class="pc-hud" aria-label="Foresight monitor">
      <h4>Foresight <span class="mv-badge" data-variant="secondary" data-shape="pill" id="pc-source">idle</span></h4>
      <div class="pc-now">
        <span class="pc-k">Foreseen</span>
        <span class="pc-target" id="pc-target" data-empty>Move toward a card…</span>
        <div class="pc-bar" aria-hidden="true"><i id="pc-bar"></i></div>
        <div class="pc-bar-row"><span>Confidence</span><span id="pc-conf">-</span></div>
      </div>
      <dl class="pc-stats">
        <div><dt>Accuracy</dt><dd id="pc-acc">-</dd></div>
        <div><dt>Last head start</dt><dd id="pc-lead">-</dd></div>
        <div><dt>Hits</dt><dd id="pc-hits">0 <small>of 0</small></dd></div>
        <div><dt>Avg head start</dt><dd id="pc-avg">-</dd></div>
      </dl>
      <div class="pc-sep"></div>
      <div class="pc-now">
        <span class="pc-k">Warmed up before the click</span>
        <ol class="pc-log" id="pc-log"><li class="pc-empty">Nothing yet: click any card</li></ol>
      </div>
      <label class="mv-choice" data-control="end">
        <input type="checkbox" role="switch" class="mv-switch" id="pc-ring" checked>
        <span class="mv-choice-text"><span class="mv-choice-title">Show foresight ring</span></span>
      </label>
      <span class="mv-sr-only" role="status" id="pc-status"></span>
    </aside>
  </div>
  <p class="pc-hint">Head for a card and slow down as you approach: it is foreseen before you click · Tab through the cards for keyboard foresight</p>

  <script type="module">
    const pc = document.getElementById("pc-region");
    await customElements.whenDefined("mv-foresee");
    const $ = (id) => document.getElementById(id);
    const target = $("pc-target"), bar = $("pc-bar"), conf = $("pc-conf"), source = $("pc-source");
    const log = $("pc-log");
    let raf = 0;

    const labels = { pointer: "trajectory", keyboard: "keyboard", press: "press", touch: "touch", api: "app" };
    const paint = () => {
      raf = 0;
      if (!pc.isConnected) return;
      const f = pc.foreseen;
      if (!f) return;
      bar.style.setProperty("--v", f.confidence.toFixed(2));
      conf.textContent = `${Math.round(f.confidence * 100)}%`;
      raf = requestAnimationFrame(paint);
    };

    pc.addEventListener("mv-foresee", (e) => {
      const { label, source: src } = e.detail;
      target.textContent = e.detail.target.querySelector("b")?.textContent ?? label;
      target.removeAttribute("data-empty");
      source.textContent = labels[src] ?? src;
      source.dataset.variant = src === "pointer" ? "info" : "secondary";
      // Here the app would warm up whatever the section needs (data, code, a dialog…).
      if (!raf) raf = requestAnimationFrame(paint);
    });
    pc.addEventListener("mv-foresee-end", () => {
      cancelAnimationFrame(raf); raf = 0;
      target.textContent = "Move toward a card…";
      target.setAttribute("data-empty", "");
      source.textContent = "idle"; source.dataset.variant = "secondary";
      bar.style.setProperty("--v", 0);
      conf.textContent = "-";
    });
    pc.addEventListener("click", (e) => { if (e.target.closest("a[href^='#']")) e.preventDefault(); });

    const ms = (v) => (v == null ? "-" : `${v} ms`);
    pc.addEventListener("mv-verdict", (e) => {
      const { hit, actual, predicted, leadTime, source: src } = e.detail;
      const s = pc.stats;
      $("pc-acc").textContent = s.accuracy == null ? "-" : `${Math.round(s.accuracy * 100)}%`;
      $("pc-hits").replaceChildren(`${s.hits} `, Object.assign(document.createElement("small"), { textContent: `of ${s.predictions}` }));
      $("pc-avg").textContent = ms(s.avgLeadTime);
      if (hit) $("pc-lead").textContent = ms(leadTime);
      const name = (el) => el?.querySelector("b")?.textContent ?? el?.textContent.trim() ?? "";
      const li = document.createElement("li");
      li.dataset.hit = String(hit);
      const b = document.createElement("b");
      b.textContent = name(actual);
      const t = document.createElement("span");
      t.textContent = hit ? `${leadTime} ms early` : predicted ? `foresaw ${name(predicted)}` : "not foreseen";
      li.append(b, t);
      log.querySelector(".pc-empty")?.remove();
      log.prepend(li);
      while (log.children.length > 4) log.lastElementChild.remove();
      $("pc-status").textContent = hit ? `${name(actual)} was foreseen ${leadTime} milliseconds early (${src}).` : `${name(actual)} was not foreseen.`;
    });
    $("pc-ring").addEventListener("change", (e) => { pc.showIndicator = e.target.checked; });
  </script>
</div>

Referência cultural

Minority Report: A Nova Lei, Steven Spielberg (a partir do conto de 1956 de Philip K. Dick) (2002, filme). Videntes precognitivos preveem um ato antes que ele aconteça, e uma visão divergente é arquivada como relatório da minoria. Na interface, a trajetória do ponteiro prevê o clique antes que ele aconteça, o app age com antecedência, e cada previsão errada é registrada como uma divergência na pontuação do próprio componente.

API

Attributes

NameTipoDefaultDescription
thresholdnumber (0..1)0.55Confidence above which the best candidate is foreseen (it must also stay there for dwell ms). Lower = earlier but more dissents; the foresight is dropped when its confidence falls below 60% of the threshold for 200 ms or another target wins.
targetsCSS selectora[href], button, [role='button'], [data-foresee]Candidates inside the region. Disabled ones (:disabled, aria-disabled="true", inside [inert]) and invisible ones are skipped. Rects are cached and refreshed only on scroll, resize or DOM changes.
prefetchbooleanfalseWhen a same-origin http(s) link is foreseen and mv-foresee is not canceled, inserts <link rel="prefetch"> once per URL (deduplicated across the document). Skipped for in-page anchors, download links, data-foresee-prefetch="false" and when the browser asks to save data.
show-indicator"true" | "false"true"false" hides the ring: the component still foresees, emits events and prefetches, invisibly.
dwellnumber (ms)40How long the best candidate must stay above the threshold before it is foreseen (filters out fly-overs).
data-foresee"foreseen" | anyOn a candidate: set to "foreseen" by the component while it is foreseen (its previous value is restored afterwards). Any element carrying the attribute is also a candidate by default.
data-foresee-labelstringOn a candidate: name reported in events and the foreseen property (default: aria-label, aria-labelledby, then text).
data-foresee-prefetch"false"On a link: never prefetched, even when foreseen.

Properties

NameTipoDescription
foreseen{ target, confidence, source, since, label } | nullCurrent foresight (read-only). source: "pointer" (trajectory), "keyboard" (focus), "press" (mouse/pen down), "touch" or "api". confidence is updated every frame while the pointer moves.
stats{ predictions, hits, misses, accuracy, avgLeadTime }Score of the pointer foresight (read-only). predictions = pointer clicks judged, hits = clicks on the foreseen target, accuracy = hits / predictions (null before the first click), avgLeadTime = mean head start of hits in ms. Keyboard and touch foresights are certain by construction and are not counted.
threshold / targets / prefetch / showIndicator / dwellreflectedMirror the attributes.

Methods

NameDescription
foresee(target, { confidence? })Forces a foresight on a candidate (element or selector) from app logic, e.g. the next step of a wizard. Emits mv-foresee like any other foresight. Returns false if the element is not a candidate.
resetStats()Clears the score.

Events

NameDescription
mv-foreseeA target is foreseen. detail: { target, confidence, leadTime (estimated ms until the click, null when unknown), distance (px left to travel, 0 once on the target), source, label }. Cancelable: preventDefault() skips the built-in prefetch; warm up anything else here.
mv-foresee-endThe foresight ended. detail: { target, reason: "lost" | "replaced" | "leave" | "click" | "blur" | "disconnect" }.
mv-verdictA candidate was clicked (or activated with the keyboard). detail: { hit, predicted (element or null), actual, leadTime (ms between foresight and click, hits only), confidence, source, stats }. A miss is a dissent: the prediction standing at pointerdown is the one judged.

Content structure

NameDescription
(content)Any region containing links and buttons. The ring is drawn in an overlay layer appended to the element; the app's DOM is only touched through data-foresee and --mv-foresee-confidence on the foreseen target.

CSS classes

NameDescription
mv-foresee-layerOverlay (aria-hidden, pointer-events: none) holding the indicator; hidden when show-indicator="false".
mv-foresee-indicator / -ringThe foresight ring. data-active while visible, data-source, data-verdict="hit | miss" for half a second after a click, --_c = confidence.

CSS variables

NameDefaultDescription
--mv-foresee-confidence0..1Set on the foreseen target: style it by confidence (subtle tint, raised shadow…).
--mv-foresee-colorvar(--mv-accent)Ring tint.
--mv-foresee-offset3pxGap between the target and the ring (the ring's corner radius follows the target's plus this gap).

Accessibility

Purely additive: nothing is announced and focus is never moved, the ring is aria-hidden with pointer-events: none, and the foreseen target keeps its role, name and behavior (only data-foresee and a CSS variable are added, then restored). Keyboard users get the same benefit as pointer users: moving focus to a candidate is a foresight with confidence 1 (mv-foresee fires, links are prefetched), while the visible focus ring stays the only indicator so the two never compete. Touch has no hover, so a press foresees immediately and gives ~100 ms of head start before the click. The verdict is observed in the capture phase without calling preventDefault, so links and buttons work exactly as before. Reduced motion (prefers-reduced-motion or data-motion="reduce"): the ring jumps between targets with no glide and no pulse; confidence is still shown as a static change of intensity. Forced colors: the ring uses the Highlight system color. Performance: pointer events only store coordinates; scoring runs in a frameLoop that sleeps when the pointer is still, candidate rects are read once per invalidation (scroll, resize, DOM changes) and never inside the loop's write phase; everything stops when the element is removed.

Esta página foi traduzida com IA. Informar um problema de tradução