|
| 1 | +import type { |
| 2 | + WidgetClient, |
| 3 | + WidgetInstance, |
| 4 | + WidgetQueryNamespace, |
| 5 | +} from "./types"; |
| 6 | + |
| 7 | +const queryNamespaces: WidgetQueryNamespace[] = [ |
| 8 | + "metrics", "sessions", "categories", "projects", "tags", |
| 9 | + "goals", "rules", "focus", "todos", "browser", |
| 10 | +]; |
| 11 | + |
| 12 | +function write(output: HTMLElement, label: string, value: unknown): void { |
| 13 | + const line = document.createElement("div"); |
| 14 | + line.textContent = `${label}: ${JSON.stringify(value)}`; |
| 15 | + output.appendChild(line); |
| 16 | +} |
| 17 | + |
| 18 | +function button(label: string, onClick: () => void): HTMLButtonElement { |
| 19 | + const element = document.createElement("button"); |
| 20 | + element.textContent = label; |
| 21 | + element.style.cssText = "padding:6px 8px;border:0;border-radius:6px;cursor:pointer;"; |
| 22 | + element.addEventListener("click", onClick); |
| 23 | + return element; |
| 24 | +} |
| 25 | + |
| 26 | +export function createWidget(): WidgetInstance { |
| 27 | + let root: HTMLDivElement | null = null; |
| 28 | + let client: WidgetClient | null = null; |
| 29 | + let subscription: number | null = null; |
| 30 | + |
| 31 | + return { |
| 32 | + async mount(container, context) { |
| 33 | + client = context.client; |
| 34 | + root = document.createElement("div"); |
| 35 | + root.style.cssText = "box-sizing:border-box;height:100%;overflow:auto;padding:14px;color:#e5eaf6;background:#182033;font-family:sans-serif;font-size:12px;"; |
| 36 | + |
| 37 | + const title = document.createElement("h3"); |
| 38 | + title.textContent = `Widget API Showcase: ${context.widgetType}`; |
| 39 | + title.style.margin = "0 0 10px"; |
| 40 | + |
| 41 | + const controls = document.createElement("div"); |
| 42 | + controls.style.cssText = "display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;"; |
| 43 | + const output = document.createElement("pre"); |
| 44 | + output.style.cssText = "white-space:pre-wrap;word-break:break-word;margin:0;padding:8px;background:#0f1524;"; |
| 45 | + root.append(title, controls, output); |
| 46 | + container.appendChild(root); |
| 47 | + |
| 48 | + const runClientReads = async () => { |
| 49 | + output.textContent = "Running WidgetClient query and state APIs...\n"; |
| 50 | + for (const namespace of queryNamespaces) { |
| 51 | + try { write(output, `client.query(${namespace})`, await context.client.query(namespace)); } |
| 52 | + catch (error) { write(output, `client.query(${namespace}) error`, String(error)); } |
| 53 | + } |
| 54 | + try { write(output, "client.getBrowserActivity", await context.client.getBrowserActivity()); } |
| 55 | + catch (error) { write(output, "client.getBrowserActivity error", String(error)); } |
| 56 | + try { |
| 57 | + await context.client.setState("showcase", "visited"); |
| 58 | + write(output, "client.getState", await context.client.getState("showcase")); |
| 59 | + await context.client.deleteState("showcase"); |
| 60 | + write(output, "client.deleteState", "ok"); |
| 61 | + } catch (error) { write(output, "client state error", String(error)); } |
| 62 | + }; |
| 63 | + |
| 64 | + const runLegacyReads = async () => { |
| 65 | + output.textContent = "Running legacy channel read APIs...\n"; |
| 66 | + const calls: Array<[string, () => Promise<unknown>]> = [ |
| 67 | + ["channel.getTodayAppTotals", () => context.channel.getTodayAppTotals()], |
| 68 | + ["channel.getAppTotalsInRange", () => context.channel.getAppTotalsInRange("2026-01-01", "2026-12-31")], |
| 69 | + ["channel.getCategoryTotalsInRange", () => context.channel.getCategoryTotalsInRange("2026-01-01", "2026-12-31")], |
| 70 | + ["channel.getHourlyForDate", () => context.channel.getHourlyForDate("2026-08-27")], |
| 71 | + ["channel.getRecentDailyTotalsRange", () => context.channel.getRecentDailyTotalsRange("2026-08-01", "2026-08-27")], |
| 72 | + ["channel.getAppCategoryMap", () => context.channel.getAppCategoryMap()], |
| 73 | + ["channel.getTodos", () => context.channel.getTodos()], |
| 74 | + ["channel.getUsageGoals", () => context.channel.getUsageGoals()], |
| 75 | + ["channel.listFocusSessions", () => context.channel.listFocusSessions()], |
| 76 | + ]; |
| 77 | + for (const [label, call] of calls) { |
| 78 | + try { write(output, label, await call()); } |
| 79 | + catch (error) { write(output, `${label} error`, String(error)); } |
| 80 | + } |
| 81 | + }; |
| 82 | + |
| 83 | + const runTodoWrites = async () => { |
| 84 | + output.textContent = "Running todo write APIs...\n"; |
| 85 | + try { |
| 86 | + const todo = await context.client.addTodo("TimeLens WidgetClient showcase"); |
| 87 | + write(output, "client.addTodo", todo); |
| 88 | + await context.client.toggleTodo(todo.id); |
| 89 | + write(output, "client.toggleTodo", "ok"); |
| 90 | + await context.client.reorderTodos([todo.id]); |
| 91 | + write(output, "client.reorderTodos", "ok"); |
| 92 | + await context.client.deleteTodo(todo.id); |
| 93 | + write(output, "client.deleteTodo", "ok"); |
| 94 | + } catch (error) { write(output, "client todo error", String(error)); } |
| 95 | + }; |
| 96 | + |
| 97 | + controls.append( |
| 98 | + button("Client queries/state", () => void runClientReads()), |
| 99 | + button("Legacy read channel", () => void runLegacyReads()), |
| 100 | + button("Todo write lifecycle", () => void runTodoWrites()), |
| 101 | + button("Local API call", () => void context.channel.localApiCall({ |
| 102 | + method: "GET", path: "/api/screen-time/today", scopes: ["screen-time:read"], |
| 103 | + }).then((value) => write(output, "channel.localApiCall", value)).catch((error: unknown) => write(output, "local API error", String(error)))), |
| 104 | + button("Focus/settings writes", () => void Promise.all([ |
| 105 | + context.channel.setFocusModeActive(false), |
| 106 | + context.channel.setMonitoringActive(true), |
| 107 | + ]).then(() => write(output, "channel settings", "ok")).catch((error: unknown) => write(output, "settings error", String(error)))), |
| 108 | + button("Request consent", () => void context.client.requestConsent("screen-time:read").then(() => write(output, "client.requestConsent", "ok")).catch((error: unknown) => write(output, "consent error", String(error)))), |
| 109 | + button("Test reserved network/media", () => void Promise.allSettled([ |
| 110 | + context.client.fetch("https://example.com"), |
| 111 | + context.client.loadMedia("https://example.com/image.png"), |
| 112 | + ]).then((results) => write(output, "client.fetch/loadMedia", results.map((result) => result.status)))), |
| 113 | + ); |
| 114 | + |
| 115 | + try { |
| 116 | + subscription = await context.client.subscribe("active-window-changed", (payload) => { |
| 117 | + title.textContent = `Widget API Showcase: ${JSON.stringify(payload)}`; |
| 118 | + }); |
| 119 | + write(output, "client.subscribe", `handle ${subscription}`); |
| 120 | + } catch (error) { write(output, "client.subscribe error", String(error)); } |
| 121 | + }, |
| 122 | + |
| 123 | + async unmount() { |
| 124 | + if (subscription !== null && client) await client.unsubscribe(subscription); |
| 125 | + subscription = null; |
| 126 | + root?.remove(); |
| 127 | + root = null; |
| 128 | + client = null; |
| 129 | + }, |
| 130 | + }; |
| 131 | +} |
0 commit comments