release: bump version to 0.50.0

This commit is contained in:
rayd1o
2026-05-10 22:06:01 +08:00
parent e1984c7a35
commit 455b8360d0
80 changed files with 10936 additions and 298 deletions

View File

@@ -0,0 +1,183 @@
import { createConnectorPath } from "./callout-connector.js";
const DEFAULT_CONNECTOR_READY_TIMEOUT_MS = 1200;
const DEFAULT_CONNECTOR_DRAW_MS = 420;
function nextAnimationFrame() {
return new Promise((resolve) => {
window.requestAnimationFrame(() => resolve());
});
}
function isCurrentContext(context) {
return !context || typeof context.isCurrent !== "function" || context.isCurrent();
}
function waitForContext(context, durationMs) {
if (context && typeof context.wait === "function") {
return context.wait(durationMs);
}
return new Promise((resolve) => {
window.setTimeout(() => resolve(isCurrentContext(context)), durationMs);
});
}
function getDefaultCardTarget() {
// TODO: replace the singleton card fallback with a presentation/card token check
// before BGP/News migrate here, so connectors only attach to their owning card.
const mobilePopup = document.getElementById("earth-mobile-popup");
if (mobilePopup instanceof HTMLElement && !mobilePopup.hasAttribute("hidden")) {
return mobilePopup;
}
const infoPanel = document.getElementById("info-panel");
if (infoPanel instanceof HTMLElement && !infoPanel.hasAttribute("hidden")) {
return infoPanel;
}
return null;
}
export class PresentationController {
constructor({
connector,
pathFactory = createConnectorPath,
requestAnimationFrame = nextAnimationFrame,
} = {}) {
this.connector = connector;
this.pathFactory = pathFactory;
this.requestAnimationFrame = requestAnimationFrame;
this.active = null;
this.timeoutId = null;
}
isActive(owner = null) {
if (!this.active) return false;
return owner ? this.active.request.owner === owner : true;
}
clearTimeout() {
if (!this.timeoutId) return;
window.clearTimeout(this.timeoutId);
this.timeoutId = null;
}
dismiss(reason = "dismiss") {
if (!this.active) return false;
const active = this.active;
this.active = null;
this.clearTimeout();
active.request.connector?.instance?.hide?.();
this.connector?.hide?.();
active.request.card?.hide?.();
active.request.onDismiss?.(reason);
return true;
}
scheduleLifetime(request) {
this.clearTimeout();
if (request.lifetime?.mode !== "timeout") return;
const timeoutMs = Number(request.lifetime.timeoutMs);
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return;
this.timeoutId = window.setTimeout(() => {
this.dismiss("timeout");
}, timeoutMs);
}
getConnectorInstance(request) {
return request.connector?.instance || this.connector || null;
}
getConnectorPath(request) {
const connectorRequest = request.connector;
if (!connectorRequest?.enabled) return null;
const source = connectorRequest.sourceProvider?.();
if (!source) return null;
const target = connectorRequest.targetProvider?.() || getDefaultCardTarget();
if (!target) return null;
const sourceIsRect =
Number.isFinite(source.x) &&
Number.isFinite(source.y) &&
Number.isFinite(source.width) &&
Number.isFinite(source.height);
const sourceAnchor = sourceIsRect
? { x: source.x + source.width * 0.5, y: source.y + source.height * 0.5 }
: source;
const options = {
...(connectorRequest.options || {}),
...(sourceIsRect && !connectorRequest.options?.sourceRect ? { sourceRect: source } : {}),
};
return this.pathFactory(sourceAnchor, target, options);
}
update({ animate = false } = {}) {
if (!this.active) return false;
const { request } = this.active;
const connector = this.getConnectorInstance(request);
if (!connector || request.connector?.enabled !== true) return false;
const path = this.getConnectorPath(request);
if (!path) {
connector.hide?.();
return false;
}
return connector.render(path, { animate }) === true;
}
async waitForConnector(request, context) {
const timeoutMs =
Number(request.connector?.readyTimeoutMs) || DEFAULT_CONNECTOR_READY_TIMEOUT_MS;
const startedAt = performance.now();
let ready = false;
while (isCurrentContext(context) && this.active?.request === request) {
ready = this.update({ animate: !ready });
if (ready) break;
if (performance.now() - startedAt >= timeoutMs) break;
await this.requestAnimationFrame();
}
return ready;
}
async present(request, { context } = {}) {
if (!request?.id || !request.card || !isCurrentContext(context)) return false;
this.dismiss("replace");
this.active = { id: request.id, request };
request.card.render?.({ reveal: false });
await this.requestAnimationFrame();
if (!isCurrentContext(context) || this.active?.request !== request) {
this.dismiss("interrupted");
return false;
}
const connectorReady =
request.connector?.enabled === true
? await this.waitForConnector(request, context)
: false;
if (!isCurrentContext(context) || this.active?.request !== request) {
this.dismiss("interrupted");
return false;
}
if (connectorReady) {
const drawMs = Number(request.connector?.drawMs) || DEFAULT_CONNECTOR_DRAW_MS;
const drawCompleted = await waitForContext(context, drawMs);
if (!drawCompleted || this.active?.request !== request) {
this.dismiss("interrupted");
return false;
}
}
request.card.render?.({ reveal: true });
await this.requestAnimationFrame();
if (!isCurrentContext(context) || this.active?.request !== request) {
this.dismiss("interrupted");
return false;
}
this.update({ animate: !connectorReady });
this.scheduleLifetime(request);
return true;
}
}