registry.tsx3.8 KBView on GitHub
'use client';

import { Activity, Bot, CalendarDays, DollarSign } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import type { ComponentType } from 'react';

import type { HomeOverview } from '@/modules/home/hooks/use-home-overview';
import { HomeMeetingsWidget } from './HomeMeetingsWidget';
import { HomeAgentsWidget } from './HomeAgentsWidget';
import { PipelineWidget, StatisticsWidget } from './metric-widgets';

export interface HomeWidgetRenderProps {
  overview: HomeOverview | undefined;
  isLoading: boolean;
}

export interface HomeWidgetDef {
  id: string;
  title: string;
  icon: LucideIcon;
  /** One line, shown in the "Add widget" picker. */
  description: string;
  Render: ComponentType<HomeWidgetRenderProps>;
  /** True when this tile projects `statistics.getOverview` — see use-home-overview. */
  needsOverview?: boolean;
  /**
   * Not offered: absent from the "Edit widgets" picker, so nobody can put it on a rail that
   * does not already name it. It still RESOLVES — a rail that already stores the id keeps
   * rendering it, which is what lets the tile stay in use while it is not yet shipped.
   *
   * TEMPORARY. The agents surface is unfinished, and this tile is its only entry point
   * (`/agents/:agentId` is a deep link nothing links to), so leaving it out of the picker and
   * out of `DEFAULT_HOME_WIDGETS` is what keeps it out of everyone else's way. Shipping it is
   * this flag going away.
   */
  unlisted?: boolean;
}

/**
 * Every widget the rail can hold, keyed by the id stored in `userSettings.homeWidgets`.
 *
 * The registry is CLIENT-SIDE and the settings store only ids, so restyling, renaming or
 * re-icon-ing a widget never needs a settings migration — and an id from a build that no
 * longer has that widget is skipped on read (see `resolveHomeWidgets`) rather than throwing
 * and taking the whole rail down with it.
 */
export const HOME_WIDGETS: readonly HomeWidgetDef[] = [
  {
    id: 'meetings',
    title: 'Meetings',
    icon: CalendarDays,
    description: "Today's calendar, each meeting opening its conversation.",
    Render: () => <HomeMeetingsWidget />,
  },
  {
    id: 'agents',
    title: 'Agents',
    icon: Bot,
    description: 'A shortlist of agents you pin yourself.',
    Render: () => <HomeAgentsWidget />,
    unlisted: true,
  },
  {
    id: 'pipeline',
    icon: DollarSign,
    title: 'Pipeline',
    description: 'Open deals and ACV by stage, scoped to one pipeline.',
    Render: PipelineWidget,
    needsOverview: true,
  },
  {
    id: 'statistics',
    icon: Activity,
    title: 'Statistics',
    description: 'Response time, follow-up time, emails sent and total actions.',
    Render: StatisticsWidget,
    needsOverview: true,
  },
];

/** The widgets the "Edit widgets" picker offers — everything except the `unlisted` ones. */
export const LISTED_HOME_WIDGETS: readonly HomeWidgetDef[] = HOME_WIDGETS.filter(
  (w) => !w.unlisted,
);

/** The rail a user who has never configured one gets. */
export const DEFAULT_HOME_WIDGETS: readonly string[] = ['meetings', 'pipeline', 'statistics'];

// Every widget, `unlisted` included: not being offered is not the same as not resolving, and
// a rail that already names an unlisted id must keep rendering it.
const BY_ID = new Map(HOME_WIDGETS.map((w) => [w.id, w]));

/**
 * Stored ids → widget definitions, in the stored order, silently dropping ids this build does
 * not know. A rail that threw (or blanked) because of one retired id would be unrecoverable
 * from the UI, since the only way to remove an id is the rail itself.
 *
 * `unlisted` widgets resolve here like any other — the picker is where they are withheld.
 */
export function resolveHomeWidgets(ids: readonly string[]): HomeWidgetDef[] {
  return ids.map((id) => BY_ID.get(id)).filter((w): w is HomeWidgetDef => !!w);
}