Design
llms.txt digitalcrew.tech

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.

MaxData displayDigitalCrew.TimeLens
TimeLens · liveOpen

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#

GroupData display
ProductMax
Globalwindow.DigitalCrew.TimeLens
Sourcemax-agent: src/components/ui/time-lens.tsx
Import in the appimport { 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>