Widget Development Guide

This guide explains how to build third-party JavaScript widgets for TimeLens v1.1.0.


Scope (v1.1.0)

Supported:

Not included yet:

Directory Layout

Each third-party widget lives in its own folder under the app data widgets directory:

%APPDATA%\com.timelens.app\widgets\
  my-widget/
    manifest.json
    index.js
    assets/
      icon.png
      styles.css

manifest.json

The manifest describes your widget to the TimeLens registry.

{
  "widget_type": "sample_hello",
  "name": "Sample Hello Widget",
  "description": "Minimal third-party widget example",
  "version": "1.0.0",
  "author": "Your Name",
  "entry": "index.js",
  "default_size": {
    "width": 360,
    "height": 240
  },
  "permissions": [
    "screen-time:read",
    "active-window:subscribe"
  ]
}

Required Fields

FieldTypeDescription
widget_typestringUnique identifier ([a-zA-Z0-9_-]+)
namestringDisplay name in widget center
entrystringEntry JS file path (relative to manifest folder)

Optional Fields

FieldTypeDescription
descriptionstringShort description
versionstringSemver version
authorstringAuthor name or email
iconstringIcon path (relative)
default_size.widthnumberDefault window width
default_size.heightnumberDefault window height
permissionsstring[]Required permissions (see below)

Permission System

Third-party widgets run in a sandboxed iframe. They must declare required permissions in the manifest. Users are prompted to grant them when the widget is first created.

PermissionAccess
screen-time:readRead screen time statistics (today, hourly, trends)
active-window:subscribeSubscribe to active window change events
goals:readRead usage goals and progress
focus:readRead focus session status
widgets:readRead widget registry information

Render Interface Contract

TimeLens loads your entry as an ESM module. Two export patterns are supported:

Pattern A — createWidget() factory

export function createWidget() {
  let root;

  return {
    mount(container, context) {
      root = document.createElement("div");
      root.textContent = `Hello from ${context.widgetType}`;
      container.appendChild(root);
    },
    unmount() {
      if (root && root.parentNode) {
        root.parentNode.removeChild(root);
      }
    },
  };
}

Pattern B — direct mount() export

let root;

export function mount(container, context) {
  root = document.createElement("div");
  root.innerHTML = `
    
    

${context.displayName}

Type: ${context.widgetType}

`; container.appendChild(root); } export function unmount() { if (root && root.parentNode) { root.parentNode.removeChild(root); } }

Context Object

PropertyTypeDescription
widgetTypestringWidget type identifier
displayNamestringHuman-readable name
widgetIdstringUnique instance ID
permissionsstring[]Granted permissions

Data Channel API

Widgets communicate with TimeLens core via the window.parent.postMessage API. The parent window provides a request/response bridge.

Request Format

window.parent.postMessage({
  type: "timelens:request",
  id: "req-123",           // unique request ID
  method: "getTodayAppTotals",
  params: {}
}, "*");

Response Format

window.addEventListener("message", (e) => {
  if (e.data.type === "timelens:response" && e.data.id === "req-123") {
    console.log(e.data.result);   // success
    console.log(e.data.error);    // if failed
  }
});

Available Methods

All methods from API Reference with read permissions are available via the data channel. Common methods:

Styling Guidelines

Widgets run inside an iframe with a transparent background. Follow these guidelines for visual consistency:

Complete Example

See examples/third-party-widget-template/ for a working starter template.

// index.js
export function createWidget() {
  let root;
  let interval;

  return {
    mount(container, context) {
      root = document.createElement("div");
      root.style.cssText = `
        padding: 16px;
        color: #e6edf3;
        font-family: Inter, system-ui, sans-serif;
      `;
      root.innerHTML = `

${context.displayName}

Loading...
`; container.appendChild(root); // Fetch data every 30s const fetchData = async () => { const reqId = "req-" + Date.now(); window.parent.postMessage({ type: "timelens:request", id: reqId, method: "getTodayAppTotals", params: {} }, "*"); }; const handler = (e) => { if (e.data.type === "timelens:response") { const list = e.data.result?.slice(0, 3) || []; const html = list.map(s => `
${s.app_name}: ${Math.round(s.total_seconds / 60)}m
` ).join(""); const el = root.querySelector("#stats"); if (el) el.innerHTML = html || "No data"; } }; window.addEventListener("message", handler); fetchData(); interval = setInterval(fetchData, 30000); }, unmount() { clearInterval(interval); if (root && root.parentNode) { root.parentNode.removeChild(root); } } }; }

Debugging Tips


最后更新:2025-05-03 · TimeLens v1.1.0