Data display
TimeLens
Max's agenda: hour marks, ticks and tasks in one scrolling column, read through a fixed lens that shows the time under it.
Guidelines#
Use it for the day plan in the Actions cockpit — tasks and meetings in time order, with proposals (overdue, unplanned) that can be allocated at the time under the lens.
What you provide: rows sorted by time — divider (hour labels), tick, moment (a task or meeting with status: done, current, overdue, suggested, scheduled; people; minutes; executor human or AI) and wheel (a deck of unplaced work) — plus currentId, pick (count, minutes, cost at the crew's rates), timeZones, dayScale, and onSelect/onAllocate/onSkip. now pins the clock for screenshots.
Behaviour: rows swell as they pass the lens and quiet as they leave (a raised-cosine wave driven by scroll). At rest the lens follows the live clock; scrolling unlocks it and a Now button returns. The current moment takes primary; suggested ones are violet. Give it a fixed height (className="h-full" in a sized flex column).
Facts#
| Group | Data display |
|---|---|
| Product | Max |
| Global | window.DigitalCrew.TimeLens |
| Source | max-agent: src/components/ui/time-lens.tsx |
| Import in the app | import { TimeLens } from "@/components/ui/time-lens"; |
API#
TypeScript declarations emitted from src/components/ui/time-lens.tsx.
export type TimeLensStatus = "done" | "current" | "overdue" | "suggested" | "scheduled";
/** Who would carry a task out, and on what terms. The transparency line. */
export interface TimeLensExecutor {
kind: "human" | "ai";
name: string;
hourlyRate: number;
automatable: boolean;
runCount: number;
}
/** One person a moment is FOR or WITH — a prospect, or a meeting attendee. */
export interface TimeLensPerson {
name: string;
detail: string | null;
}
export interface TimeLensMoment {
id: string;
taskId: string | null;
calendarItemId: string | null;
kind: "task" | "meeting";
title: string;
description: string | null;
at: string;
endsAt: string | null;
timeLabel: string;
statusLabel: string;
status: TimeLensStatus;
proposal: boolean;
people: TimeLensPerson[];
minutes: number | null;
cost: number | null;
currency: string | null;
executor: TimeLensExecutor | null;
}
export type TimeLensRow = {
kind: "divider";
key: string;
at: string;
label: string;
isNow: boolean;
} | {
kind: "tick";
key: string;
at: string;
} | {
kind: "moment";
key: string;
at: string;
moment: TimeLensMoment;
}
/** The Task Wheel: unplaced work dealt at now as one swipeable deck. */
| {
kind: "wheel";
key: string;
at: string;
moments: TimeLensMoment[];
};
/** What is on offer: count, time to execute, money at the crew's rates. */
export interface TimeLensPick {
count: number;
minutes: number;
cost: number;
currency: string;
}
export interface TimeLensProps {
/** Sorted ascending by instant. */
rows: TimeLensRow[];
/** The header's headline: what is waiting to be won, at the crew's rates. */
pick?: TimeLensPick | null;
/** The moment under way — gets the primary accent. */
currentId?: string | null;
/** Extra clocks beside the reader's own, as IANA zone names. */
timeZones?: string[];
/**
* True when rows carry clock times (a single day); false when they carry
* dates. Drives the lens readout and how an allocation time is rounded.
*/
dayScale?: boolean;
/** Pin the clock (tests, screenshots). Omit for the live second hand. */
now?: Date;
onSelect?: (moment: TimeLensMoment) => void;
/** A proposal was accepted — booked at `atIso`, the time under the lens. */
onAllocate?: (moment: TimeLensMoment, atIso: string) => void;
onSkip?: (moment: TimeLensMoment) => void;
label?: string;
className?: string;
}
/**
* The time↔offset mapping of a stream: piecewise-linear between the rows'
* instants and their vertical centers. This is what lets the lens READ the
* instant under it, and the stream place the NOW line and center on now.
* Exported for tests.
*/
export declare function buildTimeScale(rows: readonly TimeLensRow[], centers: readonly number[]): {
msForY: (y: number) => number | null;
yForMs: (value: number) => number | null;
empty: boolean;
};
/** The instant an Allocate press books, from the instant under the lens. */
export declare function allocationInstant(lensMs: number, nowMs: number, dayScale: boolean): string;
export declare function TimeLens({ rows, pick, currentId, timeZones, dayScale, now, onSelect, onAllocate, onSkip, label, className, }: TimeLensProps): any;
export default TimeLens;Example#
The code of the preview above: plain React.createElement against window.DigitalCrew, with realistic data.
<div id="root" class="p-6"></div>
<script>
(function () {
var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
var DAY = "2026-10-02T";
function at(hm) { return new Date(DAY + hm + ":00").toISOString(); }
function M(id, hm, title, status, extra) {
return Object.assign({ id: id, taskId: id, calendarItemId: null, kind: "task", title: title, description: null, at: at(hm), endsAt: null,
timeLabel: hm, statusLabel: { done: "Done", current: "In progress", overdue: "Overdue", suggested: "Suggested", scheduled: "Scheduled" }[status],
status: status, proposal: false, people: [], minutes: 15, cost: null, currency: "EUR", executor: null }, extra || {});
}
var ROWS = [];
for (var hr = 8; hr <= 14; hr++) {
var hh = (hr < 10 ? "0" : "") + hr;
ROWS.push({ kind: "divider", key: "d" + hr, at: at(hh + ":00"), label: hh + ":00", isNow: false });
ROWS.push({ kind: "tick", key: "t" + hr, at: at(hh + ":30") });
}
ROWS.push({ kind: "moment", key: "m1", at: at("09:10"), moment: M("m1", "09:10", "Reply to Claire Dubois", "done", { people: [{ name: "Claire Dubois", detail: "CFO · Northwind" }] }) });
ROWS.push({ kind: "moment", key: "m2", at: at("10:40"), moment: M("m2", "10:40", "Discovery call — Northwind", "current", { kind: "meeting", minutes: 30, people: [{ name: "Marcus Webb", detail: "VP Sales" }] }) });
ROWS.push({ kind: "moment", key: "m3", at: at("11:50"), moment: M("m3", "11:50", "Send the pricing one-pager", "scheduled", { executor: { kind: "ai", name: "Max", hourlyRate: 0, automatable: true, runCount: 14 } }) });
ROWS.push({ kind: "moment", key: "m4", at: at("13:20"), moment: M("m4", "13:20", "Qualify 12 new inbound leads", "suggested", { minutes: 40 }) });
ROWS.sort(function (a, b) { return Date.parse(a.at) - Date.parse(b.at); });
function App() {
return h("div", { className: "flex h-[430px] flex-col rounded-lg border overflow-hidden" },
h(DC.TimeLens, { rows: ROWS, currentId: "m2", now: new Date(DAY + "10:52:00"), label: "Today", className: "h-full" }));
}
ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>