@upbound/monarch-blocks 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/chart.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/chart.tsx","../src/lib/utils.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport * as RechartsPrimitive from 'recharts';\nimport type { TooltipValueType } from 'recharts';\n\nimport { cn } from '@/lib/utils';\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: '', dark: '.dark' } as const;\n\nconst INITIAL_DIMENSION = { width: 320, height: 200 } as const;\ntype TooltipNameType = number | string;\n\nexport type ChartConfig = Record<\n string,\n {\n label?: React.ReactNode;\n icon?: React.ComponentType;\n } & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })\n>;\n\ntype ChartContextProps = {\n config: ChartConfig;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n const context = React.useContext(ChartContext);\n\n if (!context) {\n throw new Error('useChart must be used within a <ChartContainer />');\n }\n\n return context;\n}\n\nfunction ChartContainer({\n id,\n className,\n children,\n config,\n initialDimension = INITIAL_DIMENSION,\n ...props\n}: React.ComponentProps<'div'> & {\n config: ChartConfig;\n children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'];\n initialDimension?: {\n width: number;\n height: number;\n };\n}) {\n const uniqueId = React.useId();\n const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}`;\n\n return (\n <ChartContext.Provider value={{ config }}>\n <div\n data-slot=\"chart\"\n data-chart={chartId}\n className={cn(\n \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n className,\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <RechartsPrimitive.ResponsiveContainer initialDimension={initialDimension}>\n {children}\n </RechartsPrimitive.ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(([, config]) => config.theme ?? config.color);\n\n if (!colorConfig.length) {\n return null;\n }\n\n return (\n <style\n dangerouslySetInnerHTML={{\n __html: Object.entries(THEMES)\n .map(\n ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ?? itemConfig.color;\n return color ? ` --color-${key}: ${color};` : null;\n })\n .join('\\n')}\n}\n`,\n )\n .join('\\n'),\n }}\n />\n );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\nfunction ChartTooltipContent({\n active,\n payload,\n className,\n indicator = 'dot',\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n React.ComponentProps<'div'> & {\n hideLabel?: boolean;\n hideIndicator?: boolean;\n indicator?: 'line' | 'dot' | 'dashed';\n nameKey?: string;\n labelKey?: string;\n } & Omit<RechartsPrimitive.DefaultTooltipContentProps<TooltipValueType, TooltipNameType>, 'accessibilityLayer'>) {\n const { config } = useChart();\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null;\n }\n\n const [item] = payload;\n const key = `${labelKey ?? item?.dataKey ?? item?.name ?? 'value'}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n const value = !labelKey && typeof label === 'string' ? (config[label]?.label ?? label) : itemConfig?.label;\n\n if (labelFormatter) {\n return <div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>;\n }\n\n if (!value) {\n return null;\n }\n\n return <div className={cn('font-medium', labelClassName)}>{value}</div>;\n }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);\n\n if (!active || !payload?.length) {\n return null;\n }\n\n const nestLabel = payload.length === 1 && indicator !== 'dot';\n\n return (\n <div\n className={cn(\n 'border-border/50 bg-background text-body-sm grid min-w-32 items-start gap-1.5 rounded-lg border px-2.5 py-1.5 shadow-xl',\n className,\n )}\n >\n {!nestLabel ? tooltipLabel : null}\n <div className=\"grid gap-1.5\">\n {payload\n .filter(item => item.type !== 'none')\n .map((item, index) => {\n const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n const indicatorColor = color ?? item.payload?.fill ?? item.color;\n\n return (\n <div\n key={index}\n className={cn(\n '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',\n indicator === 'dot' && 'items-center',\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n !hideIndicator && (\n <div\n className={cn('border-border shrink-0 rounded-full bg-(--color-bg)', {\n 'h-2 w-2': indicator === 'dot',\n 'w-1': indicator === 'line',\n 'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed',\n 'my-0.5': nestLabel && indicator === 'dashed',\n })}\n style={\n {\n '--color-bg': indicatorColor,\n '--color-border': indicatorColor,\n } as React.CSSProperties\n }\n />\n )\n )}\n <div\n className={cn(\n 'flex flex-1 justify-between leading-none',\n nestLabel ? 'items-end' : 'items-center',\n )}\n >\n <div className=\"grid gap-1.5\">\n {nestLabel ? tooltipLabel : null}\n <span className=\"text-muted-foreground\">{itemConfig?.label ?? item.name}</span>\n </div>\n {item.value != null && (\n <span className=\"text-foreground font-mono font-medium tabular-nums\">\n {typeof item.value === 'number' ? item.value.toLocaleString() : String(item.value)}\n </span>\n )}\n </div>\n </>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nfunction ChartLegendContent({\n className,\n hideIcon = false,\n payload,\n verticalAlign = 'bottom',\n nameKey,\n}: React.ComponentProps<'div'> & {\n hideIcon?: boolean;\n nameKey?: string;\n} & RechartsPrimitive.DefaultLegendContentProps) {\n const { config } = useChart();\n\n if (!payload?.length) {\n return null;\n }\n\n return (\n <div className={cn('flex items-center justify-center gap-4', verticalAlign === 'top' ? 'pb-3' : 'pt-3', className)}>\n {payload\n .filter(item => item.type !== 'none')\n .map((item, index) => {\n const key = `${nameKey ?? item.dataKey ?? 'value'}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n return (\n <div\n key={index}\n className={cn('[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3')}\n >\n {itemConfig?.icon && !hideIcon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{\n backgroundColor: item.color,\n }}\n />\n )}\n {itemConfig?.label}\n </div>\n );\n })}\n </div>\n );\n}\n\nfunction getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n if (typeof payload !== 'object' || payload === null) {\n return undefined;\n }\n\n const payloadPayload =\n 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null\n ? payload.payload\n : undefined;\n\n let configLabelKey: string = key;\n\n if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {\n configLabelKey = payload[key as keyof typeof payload] as string;\n } else if (\n payloadPayload &&\n key in payloadPayload &&\n typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'\n ) {\n configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;\n }\n\n return configLabelKey in config ? config[configLabelKey] : config[key];\n}\n\nfunction ChartHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div data-slot=\"chart-header\" className={cn('flex items-center justify-between pb-4', className)} {...props} />\n );\n}\n\nfunction ChartTitle({ className, ...props }: React.ComponentProps<'div'>) {\n return <div data-slot=\"chart-title\" className={cn('text-h3', className)} {...props} />;\n}\n\nfunction ChartDescription({ className, ...props }: React.ComponentProps<'div'>) {\n return <div data-slot=\"chart-description\" className={cn('text-body text-muted-foreground', className)} {...props} />;\n}\n\nfunction ChartAction({ className, ...props }: React.ComponentProps<'div'>) {\n return <div data-slot=\"chart-action\" className={cn(className)} {...props} />;\n}\n\nfunction ChartFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"chart-footer\"\n className={cn('text-body-sm text-muted-foreground flex items-center gap-2 pt-4', className)}\n {...props}\n />\n );\n}\n\nexport {\n ChartAction,\n ChartContainer,\n ChartDescription,\n ChartFooter,\n ChartHeader,\n ChartLegend,\n ChartLegendContent,\n ChartStyle,\n ChartTitle,\n ChartTooltip,\n ChartTooltipContent,\n};\n","import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [\n {\n text: [\n 'display-hero',\n 'display-kpi-sm',\n 'display-kpi',\n 'display-kpi-lg',\n 'display-feature',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'body-lg',\n 'body',\n 'body-sm',\n 'caption',\n 'eyebrow',\n ],\n },\n ],\n },\n },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,YAAY,WAAW;AACvB,YAAY,uBAAuB;;;ACHnC,SAAS,YAA6B;AACtC,SAAS,2BAA2B;AAEpC,IAAM,UAAU,oBAAoB;AAAA,EAClC,QAAQ;AAAA,IACN,aAAa;AAAA,MACX,aAAa;AAAA,QACX;AAAA,UACE,MAAM;AAAA,YACJ;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF,CAAC;AAEM,SAAS,MAAM,QAAsB;AAC1C,SAAO,QAAQ,KAAK,MAAM,CAAC;AAC7B;;;AD0BM,SA+HY,UAtHV,KATF;AAjDN,IAAM,SAAS,EAAE,OAAO,IAAI,MAAM,QAAQ;AAE1C,IAAM,oBAAoB,EAAE,OAAO,KAAK,QAAQ,IAAI;AAepD,IAAM,eAAqB,oBAAwC,IAAI;AAEvE,SAAS,WAAW;AAClB,QAAM,UAAgB,iBAAW,YAAY;AAE7C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,mDAAmD;AAAA,EACrE;AAEA,SAAO;AACT;AAEA,SAAS,eAAe,IAcrB;AAdqB,eACtB;AAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,mBAAmB;AAAA,EA3CrB,IAsCwB,IAMnB,kBANmB,IAMnB;AAAA,IALH;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA;AAUA,QAAM,WAAiB,YAAM;AAC7B,QAAM,UAAU,SAAS,kBAAM,SAAS,QAAQ,MAAM,EAAE,CAAC;AAEzD,SACE,oBAAC,aAAa,UAAb,EAAsB,OAAO,EAAE,OAAO,GACrC;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,cAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,OACI,QAPL;AAAA,MASC;AAAA,4BAAC,cAAW,IAAI,SAAS,QAAgB;AAAA,QACzC,oBAAmB,uCAAlB,EAAsC,kBACpC,UACH;AAAA;AAAA;AAAA,EACF,GACF;AAEJ;AAEA,IAAM,aAAa,CAAC,EAAE,IAAI,OAAO,MAA2C;AAC1E,QAAM,cAAc,OAAO,QAAQ,MAAM,EAAE,OAAO,CAAC,CAAC,EAAEA,OAAM,MAAG;AA7EjE;AA6EoE,iBAAAA,QAAO,UAAP,YAAgBA,QAAO;AAAA,GAAK;AAE9F,MAAI,CAAC,YAAY,QAAQ;AACvB,WAAO;AAAA,EACT;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,yBAAyB;AAAA,QACvB,QAAQ,OAAO,QAAQ,MAAM,EAC1B;AAAA,UACC,CAAC,CAAC,OAAO,MAAM,MAAM;AAAA,EAC/B,MAAM,gBAAgB,EAAE;AAAA,EACxB,YACC,IAAI,CAAC,CAAC,KAAK,UAAU,MAAM;AA3F9B;AA4FI,kBAAM,SAAQ,sBAAW,UAAX,mBAAmB,WAAnB,YAA8D,WAAW;AACvF,mBAAO,QAAQ,aAAa,GAAG,KAAK,KAAK,MAAM;AAAA,UACjD,CAAC,EACA,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA,QAGH,EACC,KAAK,IAAI;AAAA,MACd;AAAA;AAAA,EACF;AAEJ;AAEA,IAAM,eAAiC;AAEvC,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOmH;AACjH,QAAM,EAAE,OAAO,IAAI,SAAS;AAE5B,QAAM,eAAqB,cAAQ,MAAM;AAnI3C;AAoII,QAAI,aAAa,EAAC,mCAAS,SAAQ;AACjC,aAAO;AAAA,IACT;AAEA,UAAM,CAAC,IAAI,IAAI;AACf,UAAM,MAAM,IAAG,yCAAY,6BAAM,YAAlB,YAA6B,6BAAM,SAAnC,YAA2C,OAAO;AACjE,UAAM,aAAa,4BAA4B,QAAQ,MAAM,GAAG;AAChE,UAAM,QAAQ,CAAC,YAAY,OAAO,UAAU,YAAY,kBAAO,KAAK,MAAZ,mBAAe,UAAf,YAAwB,QAAS,yCAAY;AAErG,QAAI,gBAAgB;AAClB,aAAO,oBAAC,SAAI,WAAW,GAAG,eAAe,cAAc,GAAI,yBAAe,OAAO,OAAO,GAAE;AAAA,IAC5F;AAEA,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AAEA,WAAO,oBAAC,SAAI,WAAW,GAAG,eAAe,cAAc,GAAI,iBAAM;AAAA,EACnE,GAAG,CAAC,OAAO,gBAAgB,SAAS,WAAW,gBAAgB,QAAQ,QAAQ,CAAC;AAEhF,MAAI,CAAC,UAAU,EAAC,mCAAS,SAAQ;AAC/B,WAAO;AAAA,EACT;AAEA,QAAM,YAAY,QAAQ,WAAW,KAAK,cAAc;AAExD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEC;AAAA,SAAC,YAAY,eAAe;AAAA,QAC7B,oBAAC,SAAI,WAAU,gBACZ,kBACE,OAAO,UAAQ,KAAK,SAAS,MAAM,EACnC,IAAI,CAAC,MAAM,UAAU;AAzKhC;AA0KY,gBAAM,MAAM,IAAG,uCAAW,KAAK,SAAhB,YAAwB,KAAK,YAA7B,YAAwC,OAAO;AAC9D,gBAAM,aAAa,4BAA4B,QAAQ,MAAM,GAAG;AAChE,gBAAM,kBAAiB,8BAAS,UAAK,YAAL,mBAAc,SAAvB,YAA+B,KAAK;AAE3D,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,WAAW;AAAA,gBACT;AAAA,gBACA,cAAc,SAAS;AAAA,cACzB;AAAA,cAEC,wBAAa,6BAAM,WAAU,UAAa,KAAK,OAC9C,UAAU,KAAK,OAAO,KAAK,MAAM,MAAM,OAAO,KAAK,OAAO,IAE1D,iCACG;AAAA,0DAAY,QACX,oBAAC,WAAW,MAAX,EAAgB,IAEjB,CAAC,iBACC;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW,GAAG,uDAAuD;AAAA,sBACnE,WAAW,cAAc;AAAA,sBACzB,OAAO,cAAc;AAAA,sBACrB,mDAAmD,cAAc;AAAA,sBACjE,UAAU,aAAa,cAAc;AAAA,oBACvC,CAAC;AAAA,oBACD,OACE;AAAA,sBACE,cAAc;AAAA,sBACd,kBAAkB;AAAA,oBACpB;AAAA;AAAA,gBAEJ;AAAA,gBAGJ;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAW;AAAA,sBACT;AAAA,sBACA,YAAY,cAAc;AAAA,oBAC5B;AAAA,oBAEA;AAAA,2CAAC,SAAI,WAAU,gBACZ;AAAA,oCAAY,eAAe;AAAA,wBAC5B,oBAAC,UAAK,WAAU,yBAAyB,yDAAY,UAAZ,YAAqB,KAAK,MAAK;AAAA,yBAC1E;AAAA,sBACC,KAAK,SAAS,QACb,oBAAC,UAAK,WAAU,sDACb,iBAAO,KAAK,UAAU,WAAW,KAAK,MAAM,eAAe,IAAI,OAAO,KAAK,KAAK,GACnF;AAAA;AAAA;AAAA,gBAEJ;AAAA,iBACF;AAAA;AAAA,YA9CG;AAAA,UAgDP;AAAA,QAEJ,CAAC,GACL;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,IAAM,cAAgC;AAEtC,SAAS,mBAAmB;AAAA,EAC1B;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA,gBAAgB;AAAA,EAChB;AACF,GAGiD;AAC/C,QAAM,EAAE,OAAO,IAAI,SAAS;AAE5B,MAAI,EAAC,mCAAS,SAAQ;AACpB,WAAO;AAAA,EACT;AAEA,SACE,oBAAC,SAAI,WAAW,GAAG,0CAA0C,kBAAkB,QAAQ,SAAS,QAAQ,SAAS,GAC9G,kBACE,OAAO,UAAQ,KAAK,SAAS,MAAM,EACnC,IAAI,CAAC,MAAM,UAAU;AA9P9B;AA+PU,UAAM,MAAM,IAAG,iCAAW,KAAK,YAAhB,YAA2B,OAAO;AACjD,UAAM,aAAa,4BAA4B,QAAQ,MAAM,GAAG;AAEhE,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,WAAW,GAAG,iFAAiF;AAAA,QAE9F;AAAA,oDAAY,SAAQ,CAAC,WACpB,oBAAC,WAAW,MAAX,EAAgB,IAEjB;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,iBAAiB,KAAK;AAAA,cACxB;AAAA;AAAA,UACF;AAAA,UAED,yCAAY;AAAA;AAAA;AAAA,MAbR;AAAA,IAcP;AAAA,EAEJ,CAAC,GACL;AAEJ;AAEA,SAAS,4BAA4B,QAAqB,SAAkB,KAAa;AACvF,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,WAAO;AAAA,EACT;AAEA,QAAM,iBACJ,aAAa,WAAW,OAAO,QAAQ,YAAY,YAAY,QAAQ,YAAY,OAC/E,QAAQ,UACR;AAEN,MAAI,iBAAyB;AAE7B,MAAI,OAAO,WAAW,OAAO,QAAQ,GAA2B,MAAM,UAAU;AAC9E,qBAAiB,QAAQ,GAA2B;AAAA,EACtD,WACE,kBACA,OAAO,kBACP,OAAO,eAAe,GAAkC,MAAM,UAC9D;AACA,qBAAiB,eAAe,GAAkC;AAAA,EACpE;AAEA,SAAO,kBAAkB,SAAS,OAAO,cAAc,IAAI,OAAO,GAAG;AACvE;AAEA,SAAS,YAAY,IAAsD;AAAtD,eAAE,YAlTvB,IAkTqB,IAAgB,kBAAhB,IAAgB,CAAd;AACrB,SACE,oBAAC,wBAAI,aAAU,gBAAe,WAAW,GAAG,0CAA0C,SAAS,KAAO,MAAO;AAEjH;AAEA,SAAS,WAAW,IAAsD;AAAtD,eAAE,YAxTtB,IAwToB,IAAgB,kBAAhB,IAAgB,CAAd;AACpB,SAAO,oBAAC,wBAAI,aAAU,eAAc,WAAW,GAAG,WAAW,SAAS,KAAO,MAAO;AACtF;AAEA,SAAS,iBAAiB,IAAsD;AAAtD,eAAE,YA5T5B,IA4T0B,IAAgB,kBAAhB,IAAgB,CAAd;AAC1B,SAAO,oBAAC,wBAAI,aAAU,qBAAoB,WAAW,GAAG,mCAAmC,SAAS,KAAO,MAAO;AACpH;AAEA,SAAS,YAAY,IAAsD;AAAtD,eAAE,YAhUvB,IAgUqB,IAAgB,kBAAhB,IAAgB,CAAd;AACrB,SAAO,oBAAC,wBAAI,aAAU,gBAAe,WAAW,GAAG,SAAS,KAAO,MAAO;AAC5E;AAEA,SAAS,YAAY,IAAsD;AAAtD,eAAE,YApUvB,IAoUqB,IAAgB,kBAAhB,IAAgB,CAAd;AACrB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACV,WAAW,GAAG,mEAAmE,SAAS;AAAA,OACtF;AAAA,EACN;AAEJ;","names":["config"]}
1
+ {"version":3,"sources":["../src/chart.tsx","../src/lib/utils.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport * as RechartsPrimitive from 'recharts';\nimport type { TooltipValueType } from 'recharts';\n\nimport { cn } from '@/lib/utils';\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: '', dark: '.dark' } as const;\n\nconst INITIAL_DIMENSION = { width: 320, height: 200 } as const;\ntype TooltipNameType = number | string;\n\nexport type ChartConfig = Record<\n string,\n {\n label?: React.ReactNode;\n icon?: React.ComponentType;\n } & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })\n>;\n\ntype ChartContextProps = {\n config: ChartConfig;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n const context = React.useContext(ChartContext);\n\n if (!context) {\n throw new Error('useChart must be used within a <ChartContainer />');\n }\n\n return context;\n}\n\nfunction ChartContainer({\n id,\n className,\n children,\n config,\n initialDimension = INITIAL_DIMENSION,\n ...props\n}: React.ComponentProps<'div'> & {\n config: ChartConfig;\n children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'];\n initialDimension?: {\n width: number;\n height: number;\n };\n}) {\n const uniqueId = React.useId();\n const chartId = `chart-${id ?? uniqueId.replace(/:/g, '')}`;\n\n return (\n <ChartContext.Provider value={{ config }}>\n <div\n data-slot=\"chart\"\n data-chart={chartId}\n className={cn(\n \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n className,\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={config} />\n <RechartsPrimitive.ResponsiveContainer initialDimension={initialDimension}>\n {children}\n </RechartsPrimitive.ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n const colorConfig = Object.entries(config).filter(([, config]) => config.theme ?? config.color);\n\n if (!colorConfig.length) {\n return null;\n }\n\n return (\n <style\n dangerouslySetInnerHTML={{\n __html: Object.entries(THEMES)\n .map(\n ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n .map(([key, itemConfig]) => {\n const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ?? itemConfig.color;\n return color ? ` --color-${key}: ${color};` : null;\n })\n .join('\\n')}\n}\n`,\n )\n .join('\\n'),\n }}\n />\n );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\nfunction ChartTooltipContent({\n active,\n payload,\n className,\n indicator = 'dot',\n hideLabel = false,\n hideIndicator = false,\n label,\n labelFormatter,\n labelClassName,\n formatter,\n color,\n nameKey,\n labelKey,\n}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &\n React.ComponentProps<'div'> & {\n hideLabel?: boolean;\n hideIndicator?: boolean;\n indicator?: 'line' | 'dot' | 'dashed';\n nameKey?: string;\n labelKey?: string;\n } & Omit<RechartsPrimitive.DefaultTooltipContentProps<TooltipValueType, TooltipNameType>, 'accessibilityLayer'>) {\n const { config } = useChart();\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null;\n }\n\n const [item] = payload;\n const key = `${labelKey ?? item?.dataKey ?? item?.name ?? 'value'}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n const value = !labelKey && typeof label === 'string' ? (config[label]?.label ?? label) : itemConfig?.label;\n\n if (labelFormatter) {\n return <div className={cn('font-medium', labelClassName)}>{labelFormatter(value, payload)}</div>;\n }\n\n if (!value) {\n return null;\n }\n\n return <div className={cn('font-medium', labelClassName)}>{value}</div>;\n }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);\n\n if (!active || !payload?.length) {\n return null;\n }\n\n const nestLabel = payload.length === 1 && indicator !== 'dot';\n\n return (\n <div\n className={cn(\n 'border-border/50 bg-background text-body-sm grid min-w-32 items-start gap-1.5 rounded-lg border px-2.5 py-1.5 shadow-xl',\n className,\n )}\n >\n {!nestLabel ? tooltipLabel : null}\n <div className=\"grid gap-1.5\">\n {payload\n .filter(item => item.type !== 'none')\n .map((item, index) => {\n const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n const indicatorColor = color ?? item.payload?.fill ?? item.color;\n\n return (\n <div\n key={index}\n className={cn(\n '[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',\n indicator === 'dot' && 'items-center',\n )}\n >\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, item.payload)\n ) : (\n <>\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n !hideIndicator && (\n <div\n className={cn('border-border shrink-0 rounded-full bg-(--color-bg)', {\n 'h-2 w-2': indicator === 'dot',\n 'w-1': indicator === 'line',\n 'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed',\n 'my-0.5': nestLabel && indicator === 'dashed',\n })}\n style={\n {\n '--color-bg': indicatorColor,\n '--color-border': indicatorColor,\n } as React.CSSProperties\n }\n />\n )\n )}\n <div\n className={cn(\n 'flex flex-1 justify-between leading-none',\n nestLabel ? 'items-end' : 'items-center',\n )}\n >\n <div className=\"grid gap-1.5\">\n {nestLabel ? tooltipLabel : null}\n <span className=\"text-muted-foreground\">{itemConfig?.label ?? item.name}</span>\n </div>\n {item.value != null && (\n <span className=\"text-foreground font-mono font-medium tabular-nums\">\n {typeof item.value === 'number' ? item.value.toLocaleString() : String(item.value)}\n </span>\n )}\n </div>\n </>\n )}\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\nfunction ChartLegendContent({\n className,\n hideIcon = false,\n payload,\n verticalAlign = 'bottom',\n nameKey,\n}: React.ComponentProps<'div'> & {\n hideIcon?: boolean;\n nameKey?: string;\n} & RechartsPrimitive.DefaultLegendContentProps) {\n const { config } = useChart();\n\n if (!payload?.length) {\n return null;\n }\n\n return (\n <div className={cn('flex items-center justify-center gap-4', verticalAlign === 'top' ? 'pb-3' : 'pt-3', className)}>\n {payload\n .filter(item => item.type !== 'none')\n .map((item, index) => {\n const key = `${nameKey ?? item.dataKey ?? 'value'}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n\n return (\n <div\n key={index}\n className={cn('[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3')}\n >\n {itemConfig?.icon && !hideIcon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{\n backgroundColor: item.color,\n }}\n />\n )}\n {itemConfig?.label}\n </div>\n );\n })}\n </div>\n );\n}\n\nfunction getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n if (typeof payload !== 'object' || payload === null) {\n return undefined;\n }\n\n const payloadPayload =\n 'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null\n ? payload.payload\n : undefined;\n\n let configLabelKey: string = key;\n\n if (key in payload && typeof payload[key as keyof typeof payload] === 'string') {\n configLabelKey = payload[key as keyof typeof payload] as string;\n } else if (\n payloadPayload &&\n key in payloadPayload &&\n typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'\n ) {\n configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;\n }\n\n return configLabelKey in config ? config[configLabelKey] : config[key];\n}\n\nfunction ChartHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div data-slot=\"chart-header\" className={cn('flex items-center justify-between pb-4', className)} {...props} />\n );\n}\n\nfunction ChartTitle({ className, ...props }: React.ComponentProps<'div'>) {\n return <div data-slot=\"chart-title\" className={cn('text-h3', className)} {...props} />;\n}\n\nfunction ChartDescription({ className, ...props }: React.ComponentProps<'div'>) {\n return <div data-slot=\"chart-description\" className={cn('text-body text-muted-foreground', className)} {...props} />;\n}\n\nfunction ChartAction({ className, ...props }: React.ComponentProps<'div'>) {\n return <div data-slot=\"chart-action\" className={cn(className)} {...props} />;\n}\n\nfunction ChartFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"chart-footer\"\n className={cn('text-body-sm text-muted-foreground flex items-center gap-2 pt-4', className)}\n {...props}\n />\n );\n}\n\nexport {\n ChartAction,\n ChartContainer,\n ChartDescription,\n ChartFooter,\n ChartHeader,\n ChartLegend,\n ChartLegendContent,\n ChartStyle,\n ChartTitle,\n ChartTooltip,\n ChartTooltipContent,\n};\n","import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [\n {\n text: [\n 'display-hero',\n 'display-kpi-sm',\n 'display-kpi',\n 'display-kpi-lg',\n 'display-feature',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'body-lg',\n 'body',\n 'body-sm',\n 'caption',\n 'eyebrow',\n ],\n },\n ],\n },\n },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport function sortBy<T>(items: T[], getKey: (item: T) => string): T[] {\n return [...items].sort((a, b) => getKey(a).localeCompare(getKey(b)));\n}\n"],"mappings":"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","names":["config"]}
@@ -543,7 +543,7 @@ function ChatThread({
543
543
  ] }, msg.id)),
544
544
  isThinking && /* @__PURE__ */ jsx3(ChatThinkingIndicator, {})
545
545
  ] }) }),
546
- /* @__PURE__ */ jsx3(MessageScrollerButton, { direction: "end", children: /* @__PURE__ */ jsx3(Icon, { name: "arrow-down" }) })
546
+ /* @__PURE__ */ jsx3(MessageScrollerButton, { direction: "end", variant: "secondary", size: "icon-sm", children: /* @__PURE__ */ jsx3(Icon, { name: "arrow-down" }) })
547
547
  ] }) }),
548
548
  /* @__PURE__ */ jsx3(
549
549
  ChatInput,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/chat-interface.tsx","../src/lib/utils.ts","../src/section-nav.tsx","../src/relative-time.tsx","../src/hooks/use-mounted.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport { Badge } from '@upbound/monarch-core';\nimport { Bubble, BubbleContent } from '@upbound/monarch-core';\nimport { Button } from '@upbound/monarch-core';\nimport { Message, MessageContent } from '@upbound/monarch-core';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@upbound/monarch-core';\nimport { InputGroup, InputGroupAddon, InputGroupTextarea } from '@upbound/monarch-core';\nimport { Item, ItemActions, ItemContent, ItemGroup, ItemTitle } from '@upbound/monarch-core';\nimport { Kbd } from '@upbound/monarch-core';\nimport {\n MessageScrollerProvider,\n MessageScroller,\n MessageScrollerViewport,\n MessageScrollerContent,\n MessageScrollerItem,\n MessageScrollerButton,\n} from '@upbound/monarch-core';\nimport { Icon } from '@upbound/monarch-core';\nimport { SectionNav, SectionNavItem, SectionNavList } from '@/section-nav';\nimport { AbsoluteTimestamp } from '@/relative-time';\nimport { Marker, MarkerContent, MarkerIcon } from '@upbound/monarch-core';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@upbound/monarch-core';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an ISO timestamp as a compact relative-time string (\"5m ago\",\n * \"18h ago\", \"3d ago\"). Mirrors what Slack, Linear, and GitHub use under\n * messages — readable at a glance, exact time available via tooltip.\n */\nfunction formatRelativeTime(iso: string): string {\n const diffMs = Date.now() - new Date(iso).getTime();\n const s = Math.round(diffMs / 1000);\n if (s < 60) return 'now';\n const m = Math.round(s / 60);\n if (m < 60) return `${m}m ago`;\n const h = Math.round(m / 60);\n if (h < 24) return `${h}h ago`;\n const d = Math.round(h / 24);\n if (d < 7) return `${d}d ago`;\n const w = Math.round(d / 7);\n if (w < 5) return `${w}w ago`;\n const mo = Math.round(d / 30);\n if (mo < 12) return `${mo}mo ago`;\n const y = Math.round(d / 365);\n return `${y}y ago`;\n}\n\n/** Format an ISO timestamp for the hover tooltip. */\nfunction formatExactTime(iso: string): string {\n return new Date(iso).toLocaleString(undefined, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n });\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ChatMessage {\n id: string;\n role: 'user' | 'assistant';\n content: string;\n toolCalls?: ChatToolCall[];\n timestamp?: string;\n /**\n * When set on a user message, the thread renders a Marker directly below\n * that turn showing the page that was in scope when the message was sent\n * (per Monarch's \"Use page as context\" toggle — see Q8/Q9 of the chat\n * reconciliation memory). Per-turn, not sticky across turns.\n */\n pageContext?: string;\n}\n\nexport interface ChatToolCall {\n name: string;\n description?: string;\n apiUsed?: string;\n isNew?: boolean;\n}\n\nexport interface ChatSession {\n id: string;\n title: string;\n surface?: string;\n messageCount?: number;\n updatedAt?: string;\n}\n\n// ---------------------------------------------------------------------------\n// ChatSessionList — sidebar listing sessions\n// ---------------------------------------------------------------------------\n\nexport interface ChatSessionListProps {\n sessions: ChatSession[];\n activeSessionId?: string;\n onSelectSession: (id: string) => void;\n className?: string;\n}\n\nfunction ChatSessionList({ sessions, activeSessionId, onSelectSession, className }: ChatSessionListProps) {\n return (\n <div className={cn('bg-background flex h-full flex-col', className)}>\n <div className=\"flex-1 overflow-y-auto\">\n <ItemGroup className=\"gap-1 p-2\">\n {sessions.map(session => {\n const isActive = session.id === activeSessionId;\n return (\n <Item\n key={session.id}\n role=\"button\"\n tabIndex={0}\n aria-pressed={isActive}\n data-active={isActive || undefined}\n onClick={() => onSelectSession(session.id)}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelectSession(session.id);\n }\n }}\n className={cn(\n 'group/session-item hover:bg-muted/50 data-[active=true]:bg-accent data-[active=true]:text-accent-foreground cursor-pointer flex-nowrap',\n )}\n >\n <ItemContent className=\"min-w-0\">\n <ItemTitle className=\"truncate\">{session.title}</ItemTitle>\n <div className=\"flex items-center gap-2\">\n {session.surface && (\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {session.surface}\n </Badge>\n )}\n {session.messageCount != null && (\n <span className=\"text-muted-foreground text-[10px]\">{session.messageCount} msgs</span>\n )}\n {session.updatedAt && (\n <AbsoluteTimestamp\n date={session.updatedAt}\n variant=\"date\"\n className=\"text-muted-foreground text-[10px]\"\n />\n )}\n </div>\n </ItemContent>\n {/* Hover-revealed actions menu. Lives in `ItemActions` so\n layout is owned by Item — no absolute positioning. The\n inner div handles fade-in on row hover and keeps the\n menu visible while open via `data-[state=open]`. */}\n <ItemActions className=\"opacity-0 group-hover/session-item:opacity-100 has-data-[state=open]:opacity-100 motion-safe:transition-opacity\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label={`More actions for ${session.title}`}\n onClick={e => e.stopPropagation()}\n >\n <Icon name=\"ellipsis\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem>Pin</DropdownMenuItem>\n <DropdownMenuItem>Rename</DropdownMenuItem>\n <DropdownMenuItem>Share</DropdownMenuItem>\n <DropdownMenuItem>Archive</DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\">Delete</DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </ItemActions>\n </Item>\n );\n })}\n </ItemGroup>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatMessageBubble — individual message\n// ---------------------------------------------------------------------------\n\nfunction ChatMessageBubble({\n message,\n userName: _userName,\n assistantName = 'Upbound AI',\n}: {\n message: ChatMessage;\n /**\n * Kept for API stability. Monarch's canonical chat surface renders user\n * turns without an avatar (single-user product, no multi-user chat) — the\n * prop no longer feeds any visible affordance but consumers passing it are\n * still valid.\n */\n userName?: string;\n /**\n * Label rendered in the assistant identity header. Defaults to the fixed\n * \"Upbound AI\" per the widget's canonical convention; overridable for\n * multi-agent scenarios (e.g. named provider agents in a group chat).\n */\n assistantName?: string;\n}) {\n const isUser = message.role === 'user';\n\n // Hover-revealed footer — copy button + relative timestamp with exact-time\n // tooltip. Mirrors the Slack/Linear pattern: contextual actions don't\n // compete with message content until you reach for them.\n const messageFooter = (\n <div\n className={cn(\n 'mt-1 flex items-center gap-1 opacity-0 group-hover/chat-message:opacity-100 motion-safe:transition-opacity',\n isUser && 'justify-end',\n )}\n >\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n onClick={() => navigator.clipboard.writeText(message.content)}\n aria-label=\"Copy message\"\n >\n <Icon name=\"copy\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Copy</TooltipContent>\n </Tooltip>\n {message.timestamp && (\n <Tooltip>\n <TooltipTrigger asChild>\n <span className=\"text-caption text-muted-foreground\">{formatRelativeTime(message.timestamp)}</span>\n </TooltipTrigger>\n <TooltipContent>{formatExactTime(message.timestamp)}</TooltipContent>\n </Tooltip>\n )}\n </div>\n );\n\n // User turn — tinted Bubble surface, right-aligned. No avatar. Per Q3 of\n // the reconciliation.\n if (isUser) {\n return (\n <Message align=\"end\" className=\"group/chat-message\">\n <MessageContent className=\"max-w-2xl\">\n <Bubble variant=\"default\" align=\"end\">\n <BubbleContent className=\"whitespace-pre-wrap\">{message.content}</BubbleContent>\n </Bubble>\n {messageFooter}\n </MessageContent>\n </Message>\n );\n }\n\n // Assistant turn — identity header (sparkles + label) + plain paragraph\n // text. No Bubble surface. Per Q1/Q4 of the reconciliation. Tool calls\n // keep their existing bordered chip treatment per Q2 (deferred — inherits\n // current custom pattern until we reconcile tool-call rendering).\n return (\n <Message align=\"start\" className=\"group/chat-message\">\n <MessageContent className=\"max-w-2xl\">\n <div className=\"text-body text-foreground mb-2 flex items-center gap-1.5 font-semibold\">\n <Icon name=\"sparkles\" size=\"default\" className=\"text-primary\" />\n {assistantName}\n </div>\n {message.toolCalls && message.toolCalls.length > 0 && (\n <div className=\"mb-3 space-y-1.5\">\n {message.toolCalls.map((tc, i) => (\n <div key={i} className=\"bg-muted/50 text-body-sm flex items-start gap-2 rounded border px-3 py-2\">\n <Icon name=\"wrench\" size=\"sm\" className=\"text-muted-foreground mt-0.5 shrink-0\" />\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">{tc.name}</span>\n {tc.isNew && (\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n New API\n </Badge>\n )}\n </div>\n {tc.description && (\n <div className=\"text-muted-foreground mt-0.5 font-mono text-[10px]\">{tc.description}</div>\n )}\n {tc.apiUsed && <div className=\"text-muted-foreground mt-0.5\">via {tc.apiUsed}</div>}\n </div>\n </div>\n ))}\n </div>\n )}\n <div className=\"text-body text-foreground leading-relaxed whitespace-pre-wrap\">{message.content}</div>\n {messageFooter}\n </MessageContent>\n </Message>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatThinkingIndicator — assistant pending state\n// ---------------------------------------------------------------------------\n\n/**\n * Inline \"thinking\" indicator rendered while the assistant is generating\n * a response. Styled to match the assistant message treatment (no bubble,\n * no avatar) so it reads as a placeholder for the message that's about to\n * appear, not a separate UI element.\n */\nfunction ChatThinkingIndicator() {\n // Reconciled to compose the shadcn Marker atom with the shimmer utility —\n // the intended Monarch replacement for the old Spinner + \"Thinking...\" pair.\n // See MONARCH-PATCHES.md (marker.tsx entry) for the chat convention.\n return (\n <Marker>\n <MarkerContent className=\"shimmer\">Thinking…</MarkerContent>\n </Marker>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatInput — message input with suggestions\n// ---------------------------------------------------------------------------\n\nexport interface ChatRoutine {\n id: string;\n name: string;\n description?: string;\n}\n\nexport interface ChatInputProps {\n onSend?: (message: string) => void;\n placeholder?: string;\n /**\n * User-defined workflows surfaced in the input's left-side dropdown.\n * When provided (even as an empty array), the Workflows trigger renders.\n * When undefined, the dropdown is hidden. Prop name stays `routines` for\n * backward compatibility; the visible label is \"Workflows\" per the current\n * design vocabulary (see Q6 of the chat reconciliation memory).\n */\n routines?: ChatRoutine[];\n onSelectRoutine?: (id: string) => void;\n /** Handler for the \"Manage workflows\" link at the bottom of the dropdown. */\n onManageRoutines?: () => void;\n /**\n * Whether the \"Use page as context\" toggle is currently active. When\n * true, the toggle button renders with a subtle brand-tinted background,\n * and the consumer's onSend should attach the current page reference to\n * the outgoing message via `ChatMessage.pageContext`. See Q8/Q9 of the\n * memory for the pattern.\n */\n usePageAsContext?: boolean;\n /**\n * Handler for the \"Use page as context\" toggle. Parent owns the state;\n * this callback should flip `usePageAsContext` on the parent.\n */\n onUsePageAsContext?: () => void;\n /** Handler for the attachment affordance. Optional placeholder. */\n onAttach?: () => void;\n className?: string;\n}\n\nfunction ChatInput({\n onSend,\n placeholder = 'Ask about your infrastructure...',\n routines,\n onSelectRoutine,\n onManageRoutines,\n usePageAsContext = false,\n onUsePageAsContext,\n onAttach,\n className,\n}: ChatInputProps) {\n const [value, setValue] = React.useState('');\n\n function handleSend() {\n if (!value.trim()) return;\n onSend?.(value.trim());\n setValue('');\n }\n\n return (\n <div className={cn('shrink-0 p-3', className)}>\n {/* InputGroup with a block-end addon — textarea on top, action row below.\n Enter sends; Shift+Enter inserts a newline. Focus ring is suppressed\n (the text cursor still indicates focus state). Send button stays at\n rest-state styling whether the input is empty or not — the handler\n short-circuits on empty input. */}\n <InputGroup className=\"has-[[data-slot=input-group-control]:focus-visible]:border-input! has-[[data-slot=input-group-control]:focus-visible]:ring-0!\">\n <InputGroupTextarea\n autoFocus\n value={value}\n onChange={e => setValue(e.target.value)}\n onKeyDown={e => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n }}\n placeholder={placeholder}\n rows={1}\n />\n <InputGroupAddon align=\"block-end\">\n {/* Left side — Workflows dropdown (renamed from Routines per Q6 of\n the chat reconciliation; the `routines` prop name stays for\n backward compatibility). */}\n {routines !== undefined && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\">\n <Icon name=\"wand-magic-sparkles\" data-icon=\"inline-start\" />\n Workflows\n <Icon name=\"chevron-down\" data-icon=\"inline-end\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n {/* Workflow items live in their own scroll container so the\n Manage link below stays pinned at the bottom regardless of\n how many workflows the user has. */}\n <div className=\"max-h-64 overflow-y-auto\">\n {routines.length === 0 ? (\n <DropdownMenuItem disabled>No workflows yet</DropdownMenuItem>\n ) : (\n routines.map(w => (\n <DropdownMenuItem key={w.id} onSelect={() => onSelectRoutine?.(w.id)}>\n {w.name}\n </DropdownMenuItem>\n ))\n )}\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onSelect={onManageRoutines}>\n <Icon name=\"gear\" />\n Manage workflows\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n\n {/* Right side — context toggle, attach, send. */}\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n className={cn(\n 'ml-auto',\n usePageAsContext && 'bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary',\n )}\n onClick={onUsePageAsContext}\n aria-label=\"Use page as context\"\n aria-pressed={usePageAsContext}\n >\n <Icon name=\"expand\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>{usePageAsContext ? 'Using page as context' : 'Use page as context'}</TooltipContent>\n </Tooltip>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"ghost\" size=\"icon-sm\" onClick={onAttach} aria-label=\"Attach image, file, or video\">\n <Icon name=\"paperclip\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Attach image, file, or video</TooltipContent>\n </Tooltip>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"outline\" size=\"icon-sm\" onClick={handleSend} aria-label=\"Send message\">\n <Icon name=\"arrow-up\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>\n Send message\n <Kbd>⏎</Kbd>\n </TooltipContent>\n </Tooltip>\n </InputGroupAddon>\n </InputGroup>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatThread — conversation surface (header + messages + input)\n// ---------------------------------------------------------------------------\n//\n// The reusable conversation surface, with no assumptions about the outer\n// layout (no session sidebar, no page chrome). Consumed by `ChatInterface`\n// (chat-page layout, wraps it with `ChatSessionList`) AND by `FloatingWidget`\n// (uses it directly as body content). Changes to header / message rendering /\n// input behavior land here and propagate to both surfaces.\n\nexport interface ChatThreadProps {\n messages: ChatMessage[];\n onSend?: (message: string) => void;\n title?: string;\n subtitle?: string;\n placeholder?: string;\n suggestions?: string[];\n /**\n * Name of the AI assistant — used in the empty-state greeting:\n * \"👋 Hi, I'm {assistantName}\". Falls back to a neutral phrasing when\n * omitted so the greeting still reads correctly.\n */\n assistantName?: string;\n /**\n * Name of the current user — used to derive 2-letter initials for the\n * user message avatar. Mirrors the sidebar's `NavUser` pattern so the\n * user reads as the same person across surfaces. Falls back to the\n * generic user icon when omitted.\n */\n userName?: string;\n routines?: ChatRoutine[];\n onSelectRoutine?: (id: string) => void;\n onManageRoutines?: () => void;\n /** Whether \"Use page as context\" is currently active. See ChatInputProps. */\n usePageAsContext?: boolean;\n onUsePageAsContext?: () => void;\n onAttach?: () => void;\n callout?: React.ReactNode;\n headerActions?: React.ReactNode;\n className?: string;\n /**\n * When `true`, renders a \"Thinking…\" indicator below the last message\n * (typically a user message awaiting an assistant response). Hides\n * automatically when the assistant message lands and `isThinking`\n * flips back to `false`.\n */\n isThinking?: boolean;\n}\n\nfunction ChatThread({\n messages,\n onSend,\n title,\n subtitle,\n placeholder,\n suggestions,\n assistantName,\n userName,\n routines,\n onSelectRoutine,\n onManageRoutines,\n usePageAsContext,\n onUsePageAsContext,\n onAttach,\n callout,\n headerActions,\n className,\n isThinking,\n}: ChatThreadProps) {\n return (\n // `bg-muted/30` lives on the root so messages area and input area share\n // the same backdrop — the input then floats on the muted bg instead of\n // sitting in its own white-bordered footer container.\n <div className={cn('bg-muted/30 flex h-full flex-col', className)}>\n {/* Header — `shrink-0` so it never collapses; flex chain below relies\n on `min-h-0` on the ScrollArea for the messages to scroll instead\n of pushing the input out of the container. */}\n {(title || headerActions) && (\n <div className=\"bg-background flex shrink-0 items-center justify-between border-b px-6 py-3\">\n <div className=\"min-w-0\">\n {title && <div className=\"text-h4 truncate\">{title}</div>}\n {subtitle && <div className=\"text-body-sm text-muted-foreground truncate\">{subtitle}</div>}\n </div>\n <div className=\"flex shrink-0 items-center gap-1\">\n {/* Session actions — mirrors the hover-revealed kebab on each\n session row, surfaced here as an always-visible affordance\n since the chat header is the single primary surface for the\n open session (not a list of peers). */}\n {title && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Session actions\">\n <Icon name=\"ellipsis\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem>Pin</DropdownMenuItem>\n <DropdownMenuItem>Rename</DropdownMenuItem>\n <DropdownMenuItem>Share</DropdownMenuItem>\n <DropdownMenuItem>Archive</DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\">Delete</DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n {headerActions}\n </div>\n </div>\n )}\n\n {/* Messages — MessageScroller composition handles autoscroll to the\n live edge, anchor-based scroll restoration around user turns, and\n a jump-to-latest button that appears when the reader scrolls up.\n `min-h-0 flex-1` on the root is load-bearing: without it, flex\n children default to `min-height: auto`, growing past the parent\n and pushing the input out. Padding lives on MessageScrollerContent\n so message shadows/rings render inside the padding zone rather\n than being clipped at the viewport edge. */}\n <MessageScrollerProvider autoScroll defaultScrollPosition=\"end\">\n <MessageScroller className=\"min-h-0 flex-1\">\n <MessageScrollerViewport>\n <MessageScrollerContent className=\"gap-4 px-6 py-4\">\n {callout}\n {messages.length === 0 && suggestions && suggestions.length > 0 ? (\n // Starting state — shown when no messages yet AND suggestions\n // are provided. Disappears the moment the user sends anything\n // (either via the input or by clicking a suggestion card).\n <div className=\"flex flex-col gap-4 pt-2\">\n {/* Greeting — friendly emoji + identity, then a one-liner\n pointing at the suggestion cards below. Emoji is content\n (not UI chrome) so it sits alongside the FA-based system\n iconography without conflict. */}\n <div className=\"flex flex-col gap-1\">\n <div className=\"text-h4\">👋 Hi, I&apos;m {assistantName ?? 'your AI assistant'}</div>\n <div className=\"text-body-sm text-muted-foreground\">\n Pick a prompt to get started, or ask me anything.\n </div>\n </div>\n {/* Vertical stack; `items-start` so each button hugs its\n text width rather than stretching the full container.\n Long suggestions wrap up to the container's width. */}\n <div className=\"flex flex-col items-start gap-2\">\n {suggestions.map((s, i) => (\n <Button\n key={i}\n variant=\"outline\"\n onClick={() => onSend?.(s)}\n className=\"text-body-sm h-auto max-w-full justify-start py-2 text-left whitespace-normal\"\n >\n {s}\n </Button>\n ))}\n </div>\n </div>\n ) : null}\n {messages.map(msg => (\n <React.Fragment key={msg.id}>\n <MessageScrollerItem messageId={msg.id} scrollAnchor={msg.role === 'user'}>\n <ChatMessageBubble message={msg} userName={userName} assistantName={assistantName} />\n </MessageScrollerItem>\n {/* Per-turn page-context Marker — rendered directly below a\n user message that was sent with \"Use page as context\"\n toggled on. See Q9 of the chat reconciliation memory. */}\n {msg.role === 'user' && msg.pageContext && (\n <MessageScrollerItem messageId={`${msg.id}-ctx`}>\n <Marker>\n <MarkerIcon>\n <Icon name=\"expand\" />\n </MarkerIcon>\n <MarkerContent>Sent with page context: {msg.pageContext}</MarkerContent>\n </Marker>\n </MessageScrollerItem>\n )}\n </React.Fragment>\n ))}\n {isThinking && <ChatThinkingIndicator />}\n </MessageScrollerContent>\n </MessageScrollerViewport>\n <MessageScrollerButton direction=\"end\">\n <Icon name=\"arrow-down\" />\n </MessageScrollerButton>\n </MessageScroller>\n </MessageScrollerProvider>\n\n {/* Input — `suggestions` is intentionally not passed; starting-state\n cards above replace the inline `Try: \"...\"` hint pattern. */}\n <ChatInput\n onSend={onSend}\n placeholder={placeholder}\n routines={routines}\n onSelectRoutine={onSelectRoutine}\n onManageRoutines={onManageRoutines}\n usePageAsContext={usePageAsContext}\n onUsePageAsContext={onUsePageAsContext}\n onAttach={onAttach}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatNavRail — persistent chat-level actions to the left of the session list\n// ---------------------------------------------------------------------------\n//\n// Hosts the chat-level actions that aren't scoped to a single session:\n// starting a new chat, opening routines, managing connectors, customizing\n// the surface. Uses Monarch's `SectionNav` so the rail's interaction model\n// matches every other in-page navigation rail in the system.\n\ninterface ChatNavRailProps {\n onNewSession?: () => void;\n onOpenRoutines?: () => void;\n onOpenConnectors?: () => void;\n onOpenCustomize?: () => void;\n}\n\nfunction ChatNavRail({ onNewSession, onOpenRoutines, onOpenConnectors, onOpenCustomize }: ChatNavRailProps) {\n return (\n <SectionNav className=\"border-b p-3\">\n <SectionNavList>\n <SectionNavItem\n href=\"#\"\n onClick={e => {\n e.preventDefault();\n onNewSession?.();\n }}\n >\n <Icon name=\"plus\" />\n New session\n </SectionNavItem>\n <SectionNavItem\n href=\"#\"\n onClick={e => {\n e.preventDefault();\n onOpenRoutines?.();\n }}\n >\n <Icon name=\"bolt\" />\n Workflows\n </SectionNavItem>\n <SectionNavItem\n href=\"#\"\n onClick={e => {\n e.preventDefault();\n onOpenConnectors?.();\n }}\n >\n <Icon name=\"plug\" />\n Connectors\n </SectionNavItem>\n <SectionNavItem\n href=\"#\"\n onClick={e => {\n e.preventDefault();\n onOpenCustomize?.();\n }}\n >\n <Icon name=\"toolbox\" />\n Customize\n </SectionNavItem>\n </SectionNavList>\n </SectionNav>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatInterface — full chat layout (nav rail + session list + ChatThread)\n// ---------------------------------------------------------------------------\n//\n// Page-level composer for the chat-page block. Wraps `ChatNavRail` (left),\n// `ChatSessionList` (middle), and `ChatThread` (right). For surfaces that\n// don't need the rail or session sidebar (e.g. `FloatingWidget`), use\n// `ChatThread` directly.\n\nexport interface ChatInterfaceProps extends ChatThreadProps, ChatNavRailProps {\n sessions: ChatSession[];\n activeSessionId?: string;\n onSelectSession?: (id: string) => void;\n}\n\nfunction ChatInterface({\n sessions,\n activeSessionId,\n onSelectSession,\n onNewSession,\n onOpenRoutines,\n onOpenConnectors,\n onOpenCustomize,\n className,\n ...threadProps\n}: ChatInterfaceProps) {\n return (\n <div className={cn('flex h-full', className)}>\n <div className=\"bg-background flex w-72 shrink-0 flex-col border-r\">\n <ChatNavRail\n onNewSession={onNewSession}\n onOpenRoutines={onOpenRoutines}\n onOpenConnectors={onOpenConnectors}\n onOpenCustomize={onOpenCustomize}\n />\n <ChatSessionList\n sessions={sessions}\n activeSessionId={activeSessionId}\n onSelectSession={onSelectSession ?? (() => {})}\n className=\"flex-1\"\n />\n </div>\n <ChatThread {...threadProps} className=\"flex-1\" />\n </div>\n );\n}\n\nexport { ChatInterface, ChatSessionList, ChatThread, ChatMessageBubble, ChatInput };\n","import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [\n {\n text: [\n 'display-hero',\n 'display-kpi-sm',\n 'display-kpi',\n 'display-kpi-lg',\n 'display-feature',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'body-lg',\n 'body',\n 'body-sm',\n 'caption',\n 'eyebrow',\n ],\n },\n ],\n },\n },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","'use client';\n\nimport * as React from 'react';\nimport { Slot } from 'radix-ui';\n\nimport { cn } from '@/lib/utils';\n\n/**\n * SectionNav — vertical navigation for in-page section routing.\n *\n * Distinct from `Sidebar`: Sidebar is the app-level chrome (org switcher,\n * primary nav, user menu, mobile drawer). SectionNav is a context-free\n * navigation list that lives inside a page or section — settings sub-nav,\n * resource detail tabs (route-based), profile sub-pages, etc.\n *\n * Distinct from `Tabs (vertical orientation)`: Tabs swap views within a\n * single route (UI state); SectionNav navigates between routes (URL changes,\n * `aria-current=\"page\"` on the active item).\n *\n * Composition:\n *\n * <SectionNav>\n * <SectionNavList>\n * <SectionNavItem href=\"/settings\" isActive>Account</SectionNavItem>\n * <SectionNavItem href=\"/settings/notifications\">\n * Notifications\n * </SectionNavItem>\n * </SectionNavList>\n * </SectionNav>\n *\n * Wrap with Next `<Link>` via `asChild`:\n *\n * <SectionNavItem asChild isActive>\n * <Link href=\"/settings\">Account</Link>\n * </SectionNavItem>\n */\n\n// ── Root ──────────────────────────────────────────────\n\nfunction SectionNav({ className, ...props }: React.ComponentProps<'nav'>) {\n return <nav data-slot=\"section-nav\" className={cn('flex flex-col gap-4', className)} {...props} />;\n}\n\n// ── List ──────────────────────────────────────────────\n\nfunction SectionNavList({ className, ...props }: React.ComponentProps<'ul'>) {\n return <ul data-slot=\"section-nav-list\" className={cn('flex flex-col gap-px', className)} {...props} />;\n}\n\n// ── Group (heading + list pair) ───────────────────────\n\nfunction SectionNavGroup({ className, ...props }: React.ComponentProps<'div'>) {\n return <div data-slot=\"section-nav-group\" className={cn('flex flex-col gap-2', className)} {...props} />;\n}\n\nfunction SectionNavGroupLabel({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"section-nav-group-label\"\n className={cn('text-caption text-muted-foreground px-3 font-medium tracking-wide uppercase', className)}\n {...props}\n />\n );\n}\n\n// ── Item ──────────────────────────────────────────────\n\ninterface SectionNavItemProps extends React.ComponentProps<'a'> {\n /**\n * Marks the item as the current page. Applies active styling and sets\n * `aria-current=\"page\"`. Active state is fully controlled — wire to your\n * router's pathname match.\n */\n isActive?: boolean;\n asChild?: boolean;\n}\n\nfunction SectionNavItem({ className, isActive, asChild, children, ...props }: SectionNavItemProps) {\n const Comp = asChild ? Slot.Root : 'a';\n return (\n <li data-slot=\"section-nav-item\">\n <Comp\n data-active={isActive || undefined}\n aria-current={isActive ? 'page' : undefined}\n className={cn(\n \"group/section-nav-item text-body text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground data-[active=true]:bg-accent data-[active=true]:text-accent-foreground flex w-full items-center gap-2 rounded-md px-3 py-1.5 font-medium outline-hidden motion-safe:transition-colors [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--icon-default) [&_svg:not([class*='size-'])]:w-(--icon-default)!\",\n className,\n )}\n {...props}\n >\n {children}\n </Comp>\n </li>\n );\n}\n\n// ── Item description (for rich items with label + sub-text) ────\n\nfunction SectionNavItemDescription({ className, ...props }: React.ComponentProps<'span'>) {\n return (\n <span\n data-slot=\"section-nav-item-description\"\n className={cn(\n 'text-body-sm text-muted-foreground group-hover/section-nav-item:text-accent-foreground/80 group-data-[active=true]/section-nav-item:text-accent-foreground/80 block font-normal',\n className,\n )}\n {...props}\n />\n );\n}\n\n// ── Sub navigation (nested under an item) ─────────────\n\nfunction SectionNavSub({ className, ...props }: React.ComponentProps<'ul'>) {\n return (\n <ul\n data-slot=\"section-nav-sub\"\n className={cn('border-border mt-1 ml-7 flex flex-col gap-px border-l pl-2', className)}\n {...props}\n />\n );\n}\n\nexport {\n SectionNav,\n SectionNavList,\n SectionNavGroup,\n SectionNavGroupLabel,\n SectionNavItem,\n SectionNavItemDescription,\n SectionNavSub,\n};\n","'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport { useMounted } from '@/hooks/use-mounted';\n\n// ── Format helpers ─────────────────────────────────────────────────────────────\n//\n// `locale` is `undefined` (browser/runtime default — the viewer's actual\n// locale, per Monarch's \"use locale-aware formatting\" content principle) once\n// mounted client-side, and a fixed `'en-US'` for the server-rendered/first-\n// paint value — the server can't know the viewer's locale ahead of time, and\n// passing a locale that differs between server and client render would\n// hydration-mismatch. Callers gate this via `useMounted()` below, so the\n// locale-aware value replaces the fixed one right after mount (see\n// RelativeTime/AbsoluteTimestamp).\n\n/** Returns a human-readable relative label for `date` vs now. */\nfunction formatRelativeDefault(date: Date, locale: string | undefined): string {\n const now = new Date();\n const diffMs = now.getTime() - date.getTime();\n\n // Future dates\n if (diffMs < 0) {\n const abs = Math.abs(diffMs);\n const mins = Math.floor(abs / 60000);\n const hrs = Math.floor(mins / 60);\n const days = Math.floor(hrs / 24);\n if (abs < 60000) return 'just now';\n if (mins < 60) return `in ${mins} minute${mins === 1 ? '' : 's'}`;\n if (hrs < 24) return `in ${hrs} hour${hrs === 1 ? '' : 's'}`;\n if (days < 7) return `in ${days} day${days === 1 ? '' : 's'}`;\n if (days < 30) return `in ${Math.floor(days / 7)} week${Math.floor(days / 7) === 1 ? '' : 's'}`;\n return `on ${date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n ...(date.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),\n })}`;\n }\n\n // Past dates\n const secs = Math.floor(diffMs / 1000);\n const mins = Math.floor(secs / 60);\n const hrs = Math.floor(mins / 60);\n const days = Math.floor(hrs / 24);\n\n if (secs < 60) return 'just now';\n if (mins < 60) return `${mins} minute${mins === 1 ? '' : 's'} ago`;\n if (hrs < 24) return `${hrs} hour${hrs === 1 ? '' : 's'} ago`;\n if (days === 1) return 'yesterday';\n if (days < 7) return `${days} days ago`;\n if (days < 14) return 'last week';\n if (days < 30) return `${Math.floor(days / 7)} weeks ago`;\n\n // > 30 days: fall back to absolute date\n return `on ${date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n ...(date.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),\n })}`;\n}\n\n/**\n * Compact relative label — abbreviates units for tight horizontal space\n * (tables, KPI cards). Preserves \"ago\", \"in\", \"on\", and \"just now\" so\n * directionality and reference points stay legible at a glance.\n */\nfunction formatRelativeCompact(date: Date, locale: string | undefined): string {\n const now = new Date();\n const diffMs = now.getTime() - date.getTime();\n\n // Future dates\n if (diffMs < 0) {\n const abs = Math.abs(diffMs);\n const mins = Math.floor(abs / 60000);\n const hrs = Math.floor(mins / 60);\n const days = Math.floor(hrs / 24);\n if (abs < 60000) return 'just now';\n if (mins < 60) return `in ${mins}m`;\n if (hrs < 24) return `in ${hrs}h`;\n if (days < 7) return `in ${days}d`;\n if (days < 30) return `in ${Math.floor(days / 7)}w`;\n return `on ${date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n ...(date.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),\n })}`;\n }\n\n // Past dates\n const secs = Math.floor(diffMs / 1000);\n const mins = Math.floor(secs / 60);\n const hrs = Math.floor(mins / 60);\n const days = Math.floor(hrs / 24);\n\n if (secs < 60) return 'just now';\n if (mins < 60) return `${mins}m ago`;\n if (hrs < 24) return `${hrs}h ago`;\n if (days < 7) return `${days}d ago`;\n if (days < 30) return `${Math.floor(days / 7)}w ago`;\n\n return `on ${date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n ...(date.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),\n })}`;\n}\n\nfunction formatRelative(date: Date, variant: 'default' | 'compact', locale: string | undefined): string {\n return variant === 'compact' ? formatRelativeCompact(date, locale) : formatRelativeDefault(date, locale);\n}\n\n/** Returns the update interval in ms appropriate for the current age. */\nfunction updateInterval(date: Date): number {\n const diffMs = Math.abs(Date.now() - date.getTime());\n const diffMin = diffMs / 60000;\n if (diffMin < 60) return 60_000; // update every minute while < 1 hour old\n if (diffMin < 1440) return 300_000; // every 5 min while < 1 day old\n return 3_600_000; // every hour otherwise\n}\n\n/** Formats an absolute tooltip title. */\nfunction absoluteTitle(date: Date, locale: string | undefined): string {\n return date.toLocaleString(locale, {\n weekday: 'long',\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n });\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nexport interface RelativeTimeProps extends Omit<React.TimeHTMLAttributes<HTMLTimeElement>, 'dateTime' | 'title'> {\n /**\n * The timestamp to display. Accepts a `Date`, ISO string, or unix milliseconds.\n */\n date: Date | string | number;\n /**\n * Format variant. `default` produces full-word output (\"5 minutes ago\",\n * \"yesterday\"). `compact` abbreviates units (\"5m ago\", \"1d ago\") for tight\n * horizontal space like tables, KPI cards, or inline metadata.\n */\n variant?: 'default' | 'compact';\n /**\n * Override the tooltip shown on hover. Defaults to the full localized\n * absolute date-time string (e.g. \"Monday, January 20, 2025 at 3:22 PM PST\").\n */\n title?: string;\n}\n\n/**\n * Displays a timestamp as a live-updating relative string (\"5 minutes ago\",\n * \"yesterday\", \"on Nov 18\") with the absolute date in the native tooltip.\n *\n * Falls back to an absolute \"on [Month Day]\" format for dates older than 30 days\n * so that stale timestamps remain legible at a glance.\n *\n * @see https://design.upbound.io/components/date-and-time\n */\nexport function RelativeTime({ date, variant = 'default', title, className, ...props }: RelativeTimeProps) {\n const d = React.useMemo(() => new Date(date), [date]);\n const mounted = useMounted();\n // Fixed locale for the server-rendered/first-paint value (the server can't\n // know the viewer's locale); the viewer's actual locale once mounted.\n const locale = mounted ? undefined : 'en-US';\n\n const [label, setLabel] = React.useState<string>(() => formatRelative(d, variant, locale));\n\n React.useEffect(() => {\n // Re-compute immediately on mount (picks up the viewer's real locale, and\n // avoids the relative label going stale)\n setLabel(formatRelative(d, variant, locale));\n\n const schedule = () => {\n const id = window.setInterval(() => {\n setLabel(formatRelative(d, variant, locale));\n }, updateInterval(d));\n return id;\n };\n const id = schedule();\n return () => window.clearInterval(id);\n }, [d, variant, locale]);\n\n return (\n <time\n dateTime={d.toISOString()}\n title={title ?? absoluteTitle(d, locale)}\n className={cn('tabular-nums', className)}\n {...props}\n >\n {label}\n </time>\n );\n}\n\n// ── Absolute Timestamp ────────────────────────────────────────────────────────\n\n/** Formats am/pm per Content Principles: lowercase, space before, no periods. */\nfunction formatAmPm(date: Date, options: Intl.DateTimeFormatOptions, locale: string | undefined): string {\n const parts = new Intl.DateTimeFormat(locale, options).formatToParts(date);\n let result = '';\n for (const part of parts) {\n if (part.type === 'dayPeriod') {\n result += part.value.toLowerCase().replace(/\\./g, '');\n } else {\n result += part.value;\n }\n }\n return result;\n}\n\nfunction formatAbsolute(date: Date, variant: string, end: Date | undefined, locale: string | undefined): string {\n switch (variant) {\n case 'deadline': {\n const weekday = date.toLocaleDateString(locale, { weekday: 'short' });\n const datePart = date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n const timePart = formatAmPm(date, { hour: 'numeric', minute: '2-digit' }, locale);\n const tz = date.toLocaleTimeString(locale, { timeZoneName: 'short' }).split(' ').pop() ?? '';\n return `${weekday}, ${datePart} \\u00b7 ${timePart} ${tz}`;\n }\n case 'log': {\n const datePart = date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n const timePart = formatAmPm(date, { hour: 'numeric', minute: '2-digit' }, locale);\n const tz = date.toLocaleTimeString(locale, { timeZoneName: 'short' }).split(' ').pop() ?? '';\n return `${datePart}, ${timePart} ${tz}`;\n }\n case 'range': {\n if (!end) return date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n const startYear = date.getFullYear();\n const endYear = end.getFullYear();\n if (startYear === endYear) {\n const start = date.toLocaleDateString(locale, { month: 'short', day: 'numeric' });\n const endStr = end.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n return `${start} \\u2013 ${endStr}`;\n }\n const start = date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n const endStr = end.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n return `${start} \\u2013 ${endStr}`;\n }\n case 'date':\n default:\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n }\n}\n\nexport interface AbsoluteTimestampProps extends Omit<React.TimeHTMLAttributes<HTMLTimeElement>, 'dateTime'> {\n /** The timestamp to display. */\n date: Date | string | number;\n /** Format variant matching the Content Principles absolute timestamp table. */\n variant?: 'deadline' | 'log' | 'date' | 'range';\n /** End date for range variant. */\n end?: Date | string | number;\n}\n\n/**\n * Formats a date as an absolute timestamp following the Content Principles.\n * Use for deadlines, certificate expiry, audit logs, and date-only displays.\n */\nexport function AbsoluteTimestamp({ date, variant = 'date', end, className, ...props }: AbsoluteTimestampProps) {\n const d = React.useMemo(() => new Date(date), [date]);\n const e = React.useMemo(() => (end ? new Date(end) : undefined), [end]);\n const mounted = useMounted();\n // Fixed locale for the server-rendered/first-paint value (the server can't\n // know the viewer's locale); the viewer's actual locale once mounted.\n const locale = mounted ? undefined : 'en-US';\n\n return (\n <time\n dateTime={d.toISOString()}\n title={absoluteTitle(d, locale)}\n className={cn('tabular-nums', className)}\n {...props}\n >\n {formatAbsolute(d, variant, e, locale)}\n </time>\n );\n}\n\n// ── Elapsed Time ──────────────────────────────────────────────────────────────\n\nfunction formatElapsed(totalSeconds: number, variant: string): string {\n const days = Math.floor(totalSeconds / 86400);\n const hours = Math.floor((totalSeconds % 86400) / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = Math.floor(totalSeconds % 60);\n\n switch (variant) {\n case 'expanded': {\n const parts: string[] = [];\n if (days > 0) parts.push(`${days} day${days === 1 ? '' : 's'}`);\n if (hours > 0) parts.push(`${hours} hour${hours === 1 ? '' : 's'}`);\n if (minutes > 0) parts.push(`${minutes} minute${minutes === 1 ? '' : 's'}`);\n if (seconds > 0 || parts.length === 0) parts.push(`${seconds} second${seconds === 1 ? '' : 's'}`);\n return parts.join(', ');\n }\n case 'long': {\n if (days > 0) return `${days}d ${hours}h`;\n if (hours > 0) return `${hours}h ${minutes}m`;\n return `${minutes}m`;\n }\n case 'compact':\n default: {\n if (days > 0) return `${days}d ${hours}h`;\n if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;\n if (minutes > 0) return `${minutes}m ${seconds}s`;\n return `${seconds}s`;\n }\n }\n}\n\nexport interface ElapsedTimeProps extends Omit<React.TimeHTMLAttributes<HTMLTimeElement>, 'dateTime'> {\n /** Total elapsed seconds. Ignored if startedAt is provided. */\n seconds?: number;\n /** Format variant matching the Content Principles elapsed time table. */\n variant?: 'compact' | 'expanded' | 'long';\n /** Live-tick every second for active operations. */\n live?: boolean;\n /** Compute elapsed from a start time instead of a fixed seconds value. */\n startedAt?: Date | string | number;\n}\n\n/**\n * Formats a duration as elapsed time following the Content Principles.\n * Use for reconciliation loops, build jobs, health checks, and running operations.\n */\nexport function ElapsedTime({\n seconds: secondsProp,\n variant = 'compact',\n live = false,\n startedAt,\n className,\n ...props\n}: ElapsedTimeProps) {\n const startDate = React.useMemo(() => (startedAt ? new Date(startedAt) : undefined), [startedAt]);\n\n const computeSeconds = React.useCallback(() => {\n if (startDate) return Math.floor((Date.now() - startDate.getTime()) / 1000);\n return secondsProp ?? 0;\n }, [startDate, secondsProp]);\n\n const [elapsed, setElapsed] = React.useState(computeSeconds);\n\n React.useEffect(() => {\n setElapsed(computeSeconds());\n\n if (!live && !startDate) return;\n\n const id = window.setInterval(() => {\n setElapsed(computeSeconds());\n }, 1000);\n return () => window.clearInterval(id);\n }, [live, startDate, computeSeconds]);\n\n const iso = `PT${elapsed}S`;\n\n return (\n <time dateTime={iso} className={cn('tabular-nums', className)} {...props}>\n {formatElapsed(elapsed, variant)}\n </time>\n );\n}\n","import { useSyncExternalStore } from 'react';\n\nfunction subscribe() {\n return () => {};\n}\n\n/**\n * Returns `false` on the server and during initial client hydration, then\n * `true` once mounted — for values that are only valid client-side (e.g. the\n * viewer's actual `Intl` locale, vs. a fixed locale used for the server-\n * rendered/first-paint value) without a hydration mismatch. Implemented via\n * `useSyncExternalStore` rather than `useEffect` + `setState` — the latter\n * works, but re-derives the same \"true\" value on every mount and trips\n * `react-hooks/set-state-in-effect` since there's no actual subscription.\n */\nexport function useMounted() {\n return useSyncExternalStore(\n subscribe,\n () => true,\n () => false,\n );\n}\n"],"mappings":"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","names":["React","jsx","start","endStr","jsx","jsx"]}
1
+ {"version":3,"sources":["../src/chat-interface.tsx","../src/lib/utils.ts","../src/section-nav.tsx","../src/relative-time.tsx","../src/hooks/use-mounted.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport { Badge } from '@upbound/monarch-core';\nimport { Bubble, BubbleContent } from '@upbound/monarch-core';\nimport { Button } from '@upbound/monarch-core';\nimport { Message, MessageContent } from '@upbound/monarch-core';\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '@upbound/monarch-core';\nimport { InputGroup, InputGroupAddon, InputGroupTextarea } from '@upbound/monarch-core';\nimport { Item, ItemActions, ItemContent, ItemGroup, ItemTitle } from '@upbound/monarch-core';\nimport { Kbd } from '@upbound/monarch-core';\nimport {\n MessageScrollerProvider,\n MessageScroller,\n MessageScrollerViewport,\n MessageScrollerContent,\n MessageScrollerItem,\n MessageScrollerButton,\n} from '@upbound/monarch-core';\nimport { Icon } from '@upbound/monarch-core';\nimport { SectionNav, SectionNavItem, SectionNavList } from '@/section-nav';\nimport { AbsoluteTimestamp } from '@/relative-time';\nimport { Marker, MarkerContent, MarkerIcon } from '@upbound/monarch-core';\nimport { Tooltip, TooltipContent, TooltipTrigger } from '@upbound/monarch-core';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an ISO timestamp as a compact relative-time string (\"5m ago\",\n * \"18h ago\", \"3d ago\"). Mirrors what Slack, Linear, and GitHub use under\n * messages — readable at a glance, exact time available via tooltip.\n */\nfunction formatRelativeTime(iso: string): string {\n const diffMs = Date.now() - new Date(iso).getTime();\n const s = Math.round(diffMs / 1000);\n if (s < 60) return 'now';\n const m = Math.round(s / 60);\n if (m < 60) return `${m}m ago`;\n const h = Math.round(m / 60);\n if (h < 24) return `${h}h ago`;\n const d = Math.round(h / 24);\n if (d < 7) return `${d}d ago`;\n const w = Math.round(d / 7);\n if (w < 5) return `${w}w ago`;\n const mo = Math.round(d / 30);\n if (mo < 12) return `${mo}mo ago`;\n const y = Math.round(d / 365);\n return `${y}y ago`;\n}\n\n/** Format an ISO timestamp for the hover tooltip. */\nfunction formatExactTime(iso: string): string {\n return new Date(iso).toLocaleString(undefined, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n });\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ChatMessage {\n id: string;\n role: 'user' | 'assistant';\n content: string;\n toolCalls?: ChatToolCall[];\n timestamp?: string;\n /**\n * When set on a user message, the thread renders a Marker directly below\n * that turn showing the page that was in scope when the message was sent\n * (per Monarch's \"Use page as context\" toggle — see Q8/Q9 of the chat\n * reconciliation memory). Per-turn, not sticky across turns.\n */\n pageContext?: string;\n}\n\nexport interface ChatToolCall {\n name: string;\n description?: string;\n apiUsed?: string;\n isNew?: boolean;\n}\n\nexport interface ChatSession {\n id: string;\n title: string;\n surface?: string;\n messageCount?: number;\n updatedAt?: string;\n}\n\n// ---------------------------------------------------------------------------\n// ChatSessionList — sidebar listing sessions\n// ---------------------------------------------------------------------------\n\nexport interface ChatSessionListProps {\n sessions: ChatSession[];\n activeSessionId?: string;\n onSelectSession: (id: string) => void;\n className?: string;\n}\n\nfunction ChatSessionList({ sessions, activeSessionId, onSelectSession, className }: ChatSessionListProps) {\n return (\n <div className={cn('bg-background flex h-full flex-col', className)}>\n <div className=\"flex-1 overflow-y-auto\">\n <ItemGroup className=\"gap-1 p-2\">\n {sessions.map(session => {\n const isActive = session.id === activeSessionId;\n return (\n <Item\n key={session.id}\n role=\"button\"\n tabIndex={0}\n aria-pressed={isActive}\n data-active={isActive || undefined}\n onClick={() => onSelectSession(session.id)}\n onKeyDown={e => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelectSession(session.id);\n }\n }}\n className={cn(\n 'group/session-item hover:bg-muted/50 data-[active=true]:bg-accent data-[active=true]:text-accent-foreground cursor-pointer flex-nowrap',\n )}\n >\n <ItemContent className=\"min-w-0\">\n <ItemTitle className=\"truncate\">{session.title}</ItemTitle>\n <div className=\"flex items-center gap-2\">\n {session.surface && (\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {session.surface}\n </Badge>\n )}\n {session.messageCount != null && (\n <span className=\"text-muted-foreground text-[10px]\">{session.messageCount} msgs</span>\n )}\n {session.updatedAt && (\n <AbsoluteTimestamp\n date={session.updatedAt}\n variant=\"date\"\n className=\"text-muted-foreground text-[10px]\"\n />\n )}\n </div>\n </ItemContent>\n {/* Hover-revealed actions menu. Lives in `ItemActions` so\n layout is owned by Item — no absolute positioning. The\n inner div handles fade-in on row hover and keeps the\n menu visible while open via `data-[state=open]`. */}\n <ItemActions className=\"opacity-0 group-hover/session-item:opacity-100 has-data-[state=open]:opacity-100 motion-safe:transition-opacity\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label={`More actions for ${session.title}`}\n onClick={(e: React.MouseEvent) => e.stopPropagation()}\n >\n <Icon name=\"ellipsis\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem>Pin</DropdownMenuItem>\n <DropdownMenuItem>Rename</DropdownMenuItem>\n <DropdownMenuItem>Share</DropdownMenuItem>\n <DropdownMenuItem>Archive</DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\">Delete</DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </ItemActions>\n </Item>\n );\n })}\n </ItemGroup>\n </div>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatMessageBubble — individual message\n// ---------------------------------------------------------------------------\n\nfunction ChatMessageBubble({\n message,\n userName: _userName,\n assistantName = 'Upbound AI',\n}: {\n message: ChatMessage;\n /**\n * Kept for API stability. Monarch's canonical chat surface renders user\n * turns without an avatar (single-user product, no multi-user chat) — the\n * prop no longer feeds any visible affordance but consumers passing it are\n * still valid.\n */\n userName?: string;\n /**\n * Label rendered in the assistant identity header. Defaults to the fixed\n * \"Upbound AI\" per the widget's canonical convention; overridable for\n * multi-agent scenarios (e.g. named provider agents in a group chat).\n */\n assistantName?: string;\n}) {\n const isUser = message.role === 'user';\n\n // Hover-revealed footer — copy button + relative timestamp with exact-time\n // tooltip. Mirrors the Slack/Linear pattern: contextual actions don't\n // compete with message content until you reach for them.\n const messageFooter = (\n <div\n className={cn(\n 'mt-1 flex items-center gap-1 opacity-0 group-hover/chat-message:opacity-100 motion-safe:transition-opacity',\n isUser && 'justify-end',\n )}\n >\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-xs\"\n onClick={() => navigator.clipboard.writeText(message.content)}\n aria-label=\"Copy message\"\n >\n <Icon name=\"copy\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Copy</TooltipContent>\n </Tooltip>\n {message.timestamp && (\n <Tooltip>\n <TooltipTrigger asChild>\n <span className=\"text-caption text-muted-foreground\">{formatRelativeTime(message.timestamp)}</span>\n </TooltipTrigger>\n <TooltipContent>{formatExactTime(message.timestamp)}</TooltipContent>\n </Tooltip>\n )}\n </div>\n );\n\n // User turn — tinted Bubble surface, right-aligned. No avatar. Per Q3 of\n // the reconciliation.\n if (isUser) {\n return (\n <Message align=\"end\" className=\"group/chat-message\">\n <MessageContent className=\"max-w-2xl\">\n <Bubble variant=\"default\" align=\"end\">\n <BubbleContent className=\"whitespace-pre-wrap\">{message.content}</BubbleContent>\n </Bubble>\n {messageFooter}\n </MessageContent>\n </Message>\n );\n }\n\n // Assistant turn — identity header (sparkles + label) + plain paragraph\n // text. No Bubble surface. Per Q1/Q4 of the reconciliation. Tool calls\n // keep their existing bordered chip treatment per Q2 (deferred — inherits\n // current custom pattern until we reconcile tool-call rendering).\n return (\n <Message align=\"start\" className=\"group/chat-message\">\n <MessageContent className=\"max-w-2xl\">\n <div className=\"text-body text-foreground mb-2 flex items-center gap-1.5 font-semibold\">\n <Icon name=\"sparkles\" size=\"default\" className=\"text-primary\" />\n {assistantName}\n </div>\n {message.toolCalls && message.toolCalls.length > 0 && (\n <div className=\"mb-3 space-y-1.5\">\n {message.toolCalls.map((tc, i) => (\n <div key={i} className=\"bg-muted/50 text-body-sm flex items-start gap-2 rounded border px-3 py-2\">\n <Icon name=\"wrench\" size=\"sm\" className=\"text-muted-foreground mt-0.5 shrink-0\" />\n <div>\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium\">{tc.name}</span>\n {tc.isNew && (\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n New API\n </Badge>\n )}\n </div>\n {tc.description && (\n <div className=\"text-muted-foreground mt-0.5 font-mono text-[10px]\">{tc.description}</div>\n )}\n {tc.apiUsed && <div className=\"text-muted-foreground mt-0.5\">via {tc.apiUsed}</div>}\n </div>\n </div>\n ))}\n </div>\n )}\n <div className=\"text-body text-foreground leading-relaxed whitespace-pre-wrap\">{message.content}</div>\n {messageFooter}\n </MessageContent>\n </Message>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatThinkingIndicator — assistant pending state\n// ---------------------------------------------------------------------------\n\n/**\n * Inline \"thinking\" indicator rendered while the assistant is generating\n * a response. Styled to match the assistant message treatment (no bubble,\n * no avatar) so it reads as a placeholder for the message that's about to\n * appear, not a separate UI element.\n */\nfunction ChatThinkingIndicator() {\n // Reconciled to compose the shadcn Marker atom with the shimmer utility —\n // the intended Monarch replacement for the old Spinner + \"Thinking...\" pair.\n // See MONARCH-PATCHES.md (marker.tsx entry) for the chat convention.\n return (\n <Marker>\n <MarkerContent className=\"shimmer\">Thinking…</MarkerContent>\n </Marker>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatInput — message input with suggestions\n// ---------------------------------------------------------------------------\n\nexport interface ChatRoutine {\n id: string;\n name: string;\n description?: string;\n}\n\nexport interface ChatInputProps {\n onSend?: (message: string) => void;\n placeholder?: string;\n /**\n * User-defined workflows surfaced in the input's left-side dropdown.\n * When provided (even as an empty array), the Workflows trigger renders.\n * When undefined, the dropdown is hidden. Prop name stays `routines` for\n * backward compatibility; the visible label is \"Workflows\" per the current\n * design vocabulary (see Q6 of the chat reconciliation memory).\n */\n routines?: ChatRoutine[];\n onSelectRoutine?: (id: string) => void;\n /** Handler for the \"Manage workflows\" link at the bottom of the dropdown. */\n onManageRoutines?: () => void;\n /**\n * Whether the \"Use page as context\" toggle is currently active. When\n * true, the toggle button renders with a subtle brand-tinted background,\n * and the consumer's onSend should attach the current page reference to\n * the outgoing message via `ChatMessage.pageContext`. See Q8/Q9 of the\n * memory for the pattern.\n */\n usePageAsContext?: boolean;\n /**\n * Handler for the \"Use page as context\" toggle. Parent owns the state;\n * this callback should flip `usePageAsContext` on the parent.\n */\n onUsePageAsContext?: () => void;\n /** Handler for the attachment affordance. Optional placeholder. */\n onAttach?: () => void;\n className?: string;\n}\n\nfunction ChatInput({\n onSend,\n placeholder = 'Ask about your infrastructure...',\n routines,\n onSelectRoutine,\n onManageRoutines,\n usePageAsContext = false,\n onUsePageAsContext,\n onAttach,\n className,\n}: ChatInputProps) {\n const [value, setValue] = React.useState('');\n\n function handleSend() {\n if (!value.trim()) return;\n onSend?.(value.trim());\n setValue('');\n }\n\n return (\n <div className={cn('shrink-0 p-3', className)}>\n {/* InputGroup with a block-end addon — textarea on top, action row below.\n Enter sends; Shift+Enter inserts a newline. Focus ring is suppressed\n (the text cursor still indicates focus state). Send button stays at\n rest-state styling whether the input is empty or not — the handler\n short-circuits on empty input. */}\n <InputGroup className=\"has-[[data-slot=input-group-control]:focus-visible]:border-input! has-[[data-slot=input-group-control]:focus-visible]:ring-0!\">\n <InputGroupTextarea\n autoFocus\n value={value}\n onChange={e => setValue(e.target.value)}\n onKeyDown={e => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n }}\n placeholder={placeholder}\n rows={1}\n />\n <InputGroupAddon align=\"block-end\">\n {/* Left side — Workflows dropdown (renamed from Routines per Q6 of\n the chat reconciliation; the `routines` prop name stays for\n backward compatibility). */}\n {routines !== undefined && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\">\n <Icon name=\"wand-magic-sparkles\" data-icon=\"inline-start\" />\n Workflows\n <Icon name=\"chevron-down\" data-icon=\"inline-end\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\">\n {/* Workflow items live in their own scroll container so the\n Manage link below stays pinned at the bottom regardless of\n how many workflows the user has. */}\n <div className=\"max-h-64 overflow-y-auto\">\n {routines.length === 0 ? (\n <DropdownMenuItem disabled>No workflows yet</DropdownMenuItem>\n ) : (\n routines.map(w => (\n <DropdownMenuItem key={w.id} onSelect={() => onSelectRoutine?.(w.id)}>\n {w.name}\n </DropdownMenuItem>\n ))\n )}\n </div>\n <DropdownMenuSeparator />\n <DropdownMenuItem onSelect={onManageRoutines}>\n <Icon name=\"gear\" />\n Manage workflows\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n\n {/* Right side — context toggle, attach, send. */}\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n className={cn(\n 'ml-auto',\n usePageAsContext && 'bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary',\n )}\n onClick={onUsePageAsContext}\n aria-label=\"Use page as context\"\n aria-pressed={usePageAsContext}\n >\n <Icon name=\"expand\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>{usePageAsContext ? 'Using page as context' : 'Use page as context'}</TooltipContent>\n </Tooltip>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"ghost\" size=\"icon-sm\" onClick={onAttach} aria-label=\"Attach image, file, or video\">\n <Icon name=\"paperclip\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>Attach image, file, or video</TooltipContent>\n </Tooltip>\n <Tooltip>\n <TooltipTrigger asChild>\n <Button variant=\"outline\" size=\"icon-sm\" onClick={handleSend} aria-label=\"Send message\">\n <Icon name=\"arrow-up\" />\n </Button>\n </TooltipTrigger>\n <TooltipContent>\n Send message\n <Kbd>⏎</Kbd>\n </TooltipContent>\n </Tooltip>\n </InputGroupAddon>\n </InputGroup>\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatThread — conversation surface (header + messages + input)\n// ---------------------------------------------------------------------------\n//\n// The reusable conversation surface, with no assumptions about the outer\n// layout (no session sidebar, no page chrome). Consumed by `ChatInterface`\n// (chat-page layout, wraps it with `ChatSessionList`) AND by `FloatingWidget`\n// (uses it directly as body content). Changes to header / message rendering /\n// input behavior land here and propagate to both surfaces.\n\nexport interface ChatThreadProps {\n messages: ChatMessage[];\n onSend?: (message: string) => void;\n title?: string;\n subtitle?: string;\n placeholder?: string;\n suggestions?: string[];\n /**\n * Name of the AI assistant — used in the empty-state greeting:\n * \"👋 Hi, I'm {assistantName}\". Falls back to a neutral phrasing when\n * omitted so the greeting still reads correctly.\n */\n assistantName?: string;\n /**\n * Name of the current user — used to derive 2-letter initials for the\n * user message avatar. Mirrors the sidebar's `NavUser` pattern so the\n * user reads as the same person across surfaces. Falls back to the\n * generic user icon when omitted.\n */\n userName?: string;\n routines?: ChatRoutine[];\n onSelectRoutine?: (id: string) => void;\n onManageRoutines?: () => void;\n /** Whether \"Use page as context\" is currently active. See ChatInputProps. */\n usePageAsContext?: boolean;\n onUsePageAsContext?: () => void;\n onAttach?: () => void;\n callout?: React.ReactNode;\n headerActions?: React.ReactNode;\n className?: string;\n /**\n * When `true`, renders a \"Thinking…\" indicator below the last message\n * (typically a user message awaiting an assistant response). Hides\n * automatically when the assistant message lands and `isThinking`\n * flips back to `false`.\n */\n isThinking?: boolean;\n}\n\nfunction ChatThread({\n messages,\n onSend,\n title,\n subtitle,\n placeholder,\n suggestions,\n assistantName,\n userName,\n routines,\n onSelectRoutine,\n onManageRoutines,\n usePageAsContext,\n onUsePageAsContext,\n onAttach,\n callout,\n headerActions,\n className,\n isThinking,\n}: ChatThreadProps) {\n return (\n // `bg-muted/30` lives on the root so messages area and input area share\n // the same backdrop — the input then floats on the muted bg instead of\n // sitting in its own white-bordered footer container.\n <div className={cn('bg-muted/30 flex h-full flex-col', className)}>\n {/* Header — `shrink-0` so it never collapses; flex chain below relies\n on `min-h-0` on the ScrollArea for the messages to scroll instead\n of pushing the input out of the container. */}\n {(title || headerActions) && (\n <div className=\"bg-background flex shrink-0 items-center justify-between border-b px-6 py-3\">\n <div className=\"min-w-0\">\n {title && <div className=\"text-h4 truncate\">{title}</div>}\n {subtitle && <div className=\"text-body-sm text-muted-foreground truncate\">{subtitle}</div>}\n </div>\n <div className=\"flex shrink-0 items-center gap-1\">\n {/* Session actions — mirrors the hover-revealed kebab on each\n session row, surfaced here as an always-visible affordance\n since the chat header is the single primary surface for the\n open session (not a list of peers). */}\n {title && (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Session actions\">\n <Icon name=\"ellipsis\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem>Pin</DropdownMenuItem>\n <DropdownMenuItem>Rename</DropdownMenuItem>\n <DropdownMenuItem>Share</DropdownMenuItem>\n <DropdownMenuItem>Archive</DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\">Delete</DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )}\n {headerActions}\n </div>\n </div>\n )}\n\n {/* Messages — MessageScroller composition handles autoscroll to the\n live edge, anchor-based scroll restoration around user turns, and\n a jump-to-latest button that appears when the reader scrolls up.\n `min-h-0 flex-1` on the root is load-bearing: without it, flex\n children default to `min-height: auto`, growing past the parent\n and pushing the input out. Padding lives on MessageScrollerContent\n so message shadows/rings render inside the padding zone rather\n than being clipped at the viewport edge. */}\n <MessageScrollerProvider autoScroll defaultScrollPosition=\"end\">\n <MessageScroller className=\"min-h-0 flex-1\">\n <MessageScrollerViewport>\n <MessageScrollerContent className=\"gap-4 px-6 py-4\">\n {callout}\n {messages.length === 0 && suggestions && suggestions.length > 0 ? (\n // Starting state — shown when no messages yet AND suggestions\n // are provided. Disappears the moment the user sends anything\n // (either via the input or by clicking a suggestion card).\n <div className=\"flex flex-col gap-4 pt-2\">\n {/* Greeting — friendly emoji + identity, then a one-liner\n pointing at the suggestion cards below. Emoji is content\n (not UI chrome) so it sits alongside the FA-based system\n iconography without conflict. */}\n <div className=\"flex flex-col gap-1\">\n <div className=\"text-h4\">👋 Hi, I&apos;m {assistantName ?? 'your AI assistant'}</div>\n <div className=\"text-body-sm text-muted-foreground\">\n Pick a prompt to get started, or ask me anything.\n </div>\n </div>\n {/* Vertical stack; `items-start` so each button hugs its\n text width rather than stretching the full container.\n Long suggestions wrap up to the container's width. */}\n <div className=\"flex flex-col items-start gap-2\">\n {suggestions.map((s, i) => (\n <Button\n key={i}\n variant=\"outline\"\n onClick={() => onSend?.(s)}\n className=\"text-body-sm h-auto max-w-full justify-start py-2 text-left whitespace-normal\"\n >\n {s}\n </Button>\n ))}\n </div>\n </div>\n ) : null}\n {messages.map(msg => (\n <React.Fragment key={msg.id}>\n <MessageScrollerItem messageId={msg.id} scrollAnchor={msg.role === 'user'}>\n <ChatMessageBubble message={msg} userName={userName} assistantName={assistantName} />\n </MessageScrollerItem>\n {/* Per-turn page-context Marker — rendered directly below a\n user message that was sent with \"Use page as context\"\n toggled on. See Q9 of the chat reconciliation memory. */}\n {msg.role === 'user' && msg.pageContext && (\n <MessageScrollerItem messageId={`${msg.id}-ctx`}>\n <Marker>\n <MarkerIcon>\n <Icon name=\"expand\" />\n </MarkerIcon>\n <MarkerContent>Sent with page context: {msg.pageContext}</MarkerContent>\n </Marker>\n </MessageScrollerItem>\n )}\n </React.Fragment>\n ))}\n {isThinking && <ChatThinkingIndicator />}\n </MessageScrollerContent>\n </MessageScrollerViewport>\n <MessageScrollerButton direction=\"end\" variant=\"secondary\" size=\"icon-sm\">\n <Icon name=\"arrow-down\" />\n </MessageScrollerButton>\n </MessageScroller>\n </MessageScrollerProvider>\n\n {/* Input — `suggestions` is intentionally not passed; starting-state\n cards above replace the inline `Try: \"...\"` hint pattern. */}\n <ChatInput\n onSend={onSend}\n placeholder={placeholder}\n routines={routines}\n onSelectRoutine={onSelectRoutine}\n onManageRoutines={onManageRoutines}\n usePageAsContext={usePageAsContext}\n onUsePageAsContext={onUsePageAsContext}\n onAttach={onAttach}\n />\n </div>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatNavRail — persistent chat-level actions to the left of the session list\n// ---------------------------------------------------------------------------\n//\n// Hosts the chat-level actions that aren't scoped to a single session:\n// starting a new chat, opening routines, managing connectors, customizing\n// the surface. Uses Monarch's `SectionNav` so the rail's interaction model\n// matches every other in-page navigation rail in the system.\n\ninterface ChatNavRailProps {\n onNewSession?: () => void;\n onOpenRoutines?: () => void;\n onOpenConnectors?: () => void;\n onOpenCustomize?: () => void;\n}\n\nfunction ChatNavRail({ onNewSession, onOpenRoutines, onOpenConnectors, onOpenCustomize }: ChatNavRailProps) {\n return (\n <SectionNav className=\"border-b p-3\">\n <SectionNavList>\n <SectionNavItem\n href=\"#\"\n onClick={e => {\n e.preventDefault();\n onNewSession?.();\n }}\n >\n <Icon name=\"plus\" />\n New session\n </SectionNavItem>\n <SectionNavItem\n href=\"#\"\n onClick={e => {\n e.preventDefault();\n onOpenRoutines?.();\n }}\n >\n <Icon name=\"bolt\" />\n Workflows\n </SectionNavItem>\n <SectionNavItem\n href=\"#\"\n onClick={e => {\n e.preventDefault();\n onOpenConnectors?.();\n }}\n >\n <Icon name=\"plug\" />\n Connectors\n </SectionNavItem>\n <SectionNavItem\n href=\"#\"\n onClick={e => {\n e.preventDefault();\n onOpenCustomize?.();\n }}\n >\n <Icon name=\"toolbox\" />\n Customize\n </SectionNavItem>\n </SectionNavList>\n </SectionNav>\n );\n}\n\n// ---------------------------------------------------------------------------\n// ChatInterface — full chat layout (nav rail + session list + ChatThread)\n// ---------------------------------------------------------------------------\n//\n// Page-level composer for the chat-page block. Wraps `ChatNavRail` (left),\n// `ChatSessionList` (middle), and `ChatThread` (right). For surfaces that\n// don't need the rail or session sidebar (e.g. `FloatingWidget`), use\n// `ChatThread` directly.\n\nexport interface ChatInterfaceProps extends ChatThreadProps, ChatNavRailProps {\n sessions: ChatSession[];\n activeSessionId?: string;\n onSelectSession?: (id: string) => void;\n}\n\nfunction ChatInterface({\n sessions,\n activeSessionId,\n onSelectSession,\n onNewSession,\n onOpenRoutines,\n onOpenConnectors,\n onOpenCustomize,\n className,\n ...threadProps\n}: ChatInterfaceProps) {\n return (\n <div className={cn('flex h-full', className)}>\n <div className=\"bg-background flex w-72 shrink-0 flex-col border-r\">\n <ChatNavRail\n onNewSession={onNewSession}\n onOpenRoutines={onOpenRoutines}\n onOpenConnectors={onOpenConnectors}\n onOpenCustomize={onOpenCustomize}\n />\n <ChatSessionList\n sessions={sessions}\n activeSessionId={activeSessionId}\n onSelectSession={onSelectSession ?? (() => {})}\n className=\"flex-1\"\n />\n </div>\n <ChatThread {...threadProps} className=\"flex-1\" />\n </div>\n );\n}\n\nexport { ChatInterface, ChatSessionList, ChatThread, ChatMessageBubble, ChatInput };\n","import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [\n {\n text: [\n 'display-hero',\n 'display-kpi-sm',\n 'display-kpi',\n 'display-kpi-lg',\n 'display-feature',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'body-lg',\n 'body',\n 'body-sm',\n 'caption',\n 'eyebrow',\n ],\n },\n ],\n },\n },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport function sortBy<T>(items: T[], getKey: (item: T) => string): T[] {\n return [...items].sort((a, b) => getKey(a).localeCompare(getKey(b)));\n}\n","'use client';\n\nimport * as React from 'react';\nimport { Slot } from 'radix-ui';\n\nimport { cn } from '@/lib/utils';\n\n/**\n * SectionNav — vertical navigation for in-page section routing.\n *\n * Distinct from `Sidebar`: Sidebar is the app-level chrome (org switcher,\n * primary nav, user menu, mobile drawer). SectionNav is a context-free\n * navigation list that lives inside a page or section — settings sub-nav,\n * resource detail tabs (route-based), profile sub-pages, etc.\n *\n * Distinct from `Tabs (vertical orientation)`: Tabs swap views within a\n * single route (UI state); SectionNav navigates between routes (URL changes,\n * `aria-current=\"page\"` on the active item).\n *\n * Composition:\n *\n * <SectionNav>\n * <SectionNavList>\n * <SectionNavItem href=\"/settings\" isActive>Account</SectionNavItem>\n * <SectionNavItem href=\"/settings/notifications\">\n * Notifications\n * </SectionNavItem>\n * </SectionNavList>\n * </SectionNav>\n *\n * Wrap with Next `<Link>` via `asChild`:\n *\n * <SectionNavItem asChild isActive>\n * <Link href=\"/settings\">Account</Link>\n * </SectionNavItem>\n */\n\n// ── Root ──────────────────────────────────────────────\n\nfunction SectionNav({ className, ...props }: React.ComponentProps<'nav'>) {\n return <nav data-slot=\"section-nav\" className={cn('flex flex-col gap-4', className)} {...props} />;\n}\n\n// ── List ──────────────────────────────────────────────\n\nfunction SectionNavList({ className, ...props }: React.ComponentProps<'ul'>) {\n return <ul data-slot=\"section-nav-list\" className={cn('flex flex-col gap-px', className)} {...props} />;\n}\n\n// ── Group (heading + list pair) ───────────────────────\n\nfunction SectionNavGroup({ className, ...props }: React.ComponentProps<'div'>) {\n return <div data-slot=\"section-nav-group\" className={cn('flex flex-col gap-2', className)} {...props} />;\n}\n\nfunction SectionNavGroupLabel({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <div\n data-slot=\"section-nav-group-label\"\n className={cn('text-caption text-muted-foreground px-3 font-medium tracking-wide uppercase', className)}\n {...props}\n />\n );\n}\n\n// ── Item ──────────────────────────────────────────────\n\ninterface SectionNavItemProps extends React.ComponentProps<'a'> {\n /**\n * Marks the item as the current page. Applies active styling and sets\n * `aria-current=\"page\"`. Active state is fully controlled — wire to your\n * router's pathname match.\n */\n isActive?: boolean;\n asChild?: boolean;\n}\n\nfunction SectionNavItem({ className, isActive, asChild, children, ...props }: SectionNavItemProps) {\n const Comp = asChild ? Slot.Root : 'a';\n return (\n <li data-slot=\"section-nav-item\">\n <Comp\n data-active={isActive || undefined}\n aria-current={isActive ? 'page' : undefined}\n className={cn(\n \"group/section-nav-item text-body text-muted-foreground hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground data-[active=true]:bg-accent data-[active=true]:text-accent-foreground flex w-full items-center gap-2 rounded-md px-3 py-1.5 font-medium outline-hidden motion-safe:transition-colors [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-(--icon-default) [&_svg:not([class*='size-'])]:w-(--icon-default)!\",\n className,\n )}\n {...props}\n >\n {children}\n </Comp>\n </li>\n );\n}\n\n// ── Item description (for rich items with label + sub-text) ────\n\nfunction SectionNavItemDescription({ className, ...props }: React.ComponentProps<'span'>) {\n return (\n <span\n data-slot=\"section-nav-item-description\"\n className={cn(\n 'text-body-sm text-muted-foreground group-hover/section-nav-item:text-accent-foreground/80 group-data-[active=true]/section-nav-item:text-accent-foreground/80 block font-normal',\n className,\n )}\n {...props}\n />\n );\n}\n\n// ── Sub navigation (nested under an item) ─────────────\n\nfunction SectionNavSub({ className, ...props }: React.ComponentProps<'ul'>) {\n return (\n <ul\n data-slot=\"section-nav-sub\"\n className={cn('border-border mt-1 ml-7 flex flex-col gap-px border-l pl-2', className)}\n {...props}\n />\n );\n}\n\nexport {\n SectionNav,\n SectionNavList,\n SectionNavGroup,\n SectionNavGroupLabel,\n SectionNavItem,\n SectionNavItemDescription,\n SectionNavSub,\n};\n","'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport { useMounted } from '@/hooks/use-mounted';\n\n// ── Format helpers ─────────────────────────────────────────────────────────────\n//\n// `locale` is `undefined` (browser/runtime default — the viewer's actual\n// locale, per Monarch's \"use locale-aware formatting\" content principle) once\n// mounted client-side, and a fixed `'en-US'` for the server-rendered/first-\n// paint value — the server can't know the viewer's locale ahead of time, and\n// passing a locale that differs between server and client render would\n// hydration-mismatch. Callers gate this via `useMounted()` below, so the\n// locale-aware value replaces the fixed one right after mount (see\n// RelativeTime/AbsoluteTimestamp).\n\n/** Returns a human-readable relative label for `date` vs now. */\nfunction formatRelativeDefault(date: Date, locale: string | undefined): string {\n const now = new Date();\n const diffMs = now.getTime() - date.getTime();\n\n // Future dates\n if (diffMs < 0) {\n const abs = Math.abs(diffMs);\n const mins = Math.floor(abs / 60000);\n const hrs = Math.floor(mins / 60);\n const days = Math.floor(hrs / 24);\n if (abs < 60000) return 'just now';\n if (mins < 60) return `in ${mins} minute${mins === 1 ? '' : 's'}`;\n if (hrs < 24) return `in ${hrs} hour${hrs === 1 ? '' : 's'}`;\n if (days < 7) return `in ${days} day${days === 1 ? '' : 's'}`;\n if (days < 30) return `in ${Math.floor(days / 7)} week${Math.floor(days / 7) === 1 ? '' : 's'}`;\n return `on ${date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n ...(date.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),\n })}`;\n }\n\n // Past dates\n const secs = Math.floor(diffMs / 1000);\n const mins = Math.floor(secs / 60);\n const hrs = Math.floor(mins / 60);\n const days = Math.floor(hrs / 24);\n\n if (secs < 60) return 'just now';\n if (mins < 60) return `${mins} minute${mins === 1 ? '' : 's'} ago`;\n if (hrs < 24) return `${hrs} hour${hrs === 1 ? '' : 's'} ago`;\n if (days === 1) return 'yesterday';\n if (days < 7) return `${days} days ago`;\n if (days < 14) return 'last week';\n if (days < 30) return `${Math.floor(days / 7)} weeks ago`;\n\n // > 30 days: fall back to absolute date\n return `on ${date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n ...(date.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),\n })}`;\n}\n\n/**\n * Compact relative label — abbreviates units for tight horizontal space\n * (tables, KPI cards). Preserves \"ago\", \"in\", \"on\", and \"just now\" so\n * directionality and reference points stay legible at a glance.\n */\nfunction formatRelativeCompact(date: Date, locale: string | undefined): string {\n const now = new Date();\n const diffMs = now.getTime() - date.getTime();\n\n // Future dates\n if (diffMs < 0) {\n const abs = Math.abs(diffMs);\n const mins = Math.floor(abs / 60000);\n const hrs = Math.floor(mins / 60);\n const days = Math.floor(hrs / 24);\n if (abs < 60000) return 'just now';\n if (mins < 60) return `in ${mins}m`;\n if (hrs < 24) return `in ${hrs}h`;\n if (days < 7) return `in ${days}d`;\n if (days < 30) return `in ${Math.floor(days / 7)}w`;\n return `on ${date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n ...(date.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),\n })}`;\n }\n\n // Past dates\n const secs = Math.floor(diffMs / 1000);\n const mins = Math.floor(secs / 60);\n const hrs = Math.floor(mins / 60);\n const days = Math.floor(hrs / 24);\n\n if (secs < 60) return 'just now';\n if (mins < 60) return `${mins}m ago`;\n if (hrs < 24) return `${hrs}h ago`;\n if (days < 7) return `${days}d ago`;\n if (days < 30) return `${Math.floor(days / 7)}w ago`;\n\n return `on ${date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n ...(date.getFullYear() !== now.getFullYear() ? { year: 'numeric' } : {}),\n })}`;\n}\n\nfunction formatRelative(date: Date, variant: 'default' | 'compact', locale: string | undefined): string {\n return variant === 'compact' ? formatRelativeCompact(date, locale) : formatRelativeDefault(date, locale);\n}\n\n/** Returns the update interval in ms appropriate for the current age. */\nfunction updateInterval(date: Date): number {\n const diffMs = Math.abs(Date.now() - date.getTime());\n const diffMin = diffMs / 60000;\n if (diffMin < 60) return 60_000; // update every minute while < 1 hour old\n if (diffMin < 1440) return 300_000; // every 5 min while < 1 day old\n return 3_600_000; // every hour otherwise\n}\n\n/** Formats an absolute tooltip title. */\nfunction absoluteTitle(date: Date, locale: string | undefined): string {\n return date.toLocaleString(locale, {\n weekday: 'long',\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n hour: 'numeric',\n minute: '2-digit',\n timeZoneName: 'short',\n });\n}\n\n// ── Component ──────────────────────────────────────────────────────────────────\n\nexport interface RelativeTimeProps extends Omit<React.TimeHTMLAttributes<HTMLTimeElement>, 'dateTime' | 'title'> {\n /**\n * The timestamp to display. Accepts a `Date`, ISO string, or unix milliseconds.\n */\n date: Date | string | number;\n /**\n * Format variant. `default` produces full-word output (\"5 minutes ago\",\n * \"yesterday\"). `compact` abbreviates units (\"5m ago\", \"1d ago\") for tight\n * horizontal space like tables, KPI cards, or inline metadata.\n */\n variant?: 'default' | 'compact';\n /**\n * Override the tooltip shown on hover. Defaults to the full localized\n * absolute date-time string (e.g. \"Monday, January 20, 2025 at 3:22 PM PST\").\n */\n title?: string;\n}\n\n/**\n * Displays a timestamp as a live-updating relative string (\"5 minutes ago\",\n * \"yesterday\", \"on Nov 18\") with the absolute date in the native tooltip.\n *\n * Falls back to an absolute \"on [Month Day]\" format for dates older than 30 days\n * so that stale timestamps remain legible at a glance.\n *\n * @see https://design.upbound.io/components/date-and-time\n */\nexport function RelativeTime({ date, variant = 'default', title, className, ...props }: RelativeTimeProps) {\n const d = React.useMemo(() => new Date(date), [date]);\n const mounted = useMounted();\n // Fixed locale for the server-rendered/first-paint value (the server can't\n // know the viewer's locale); the viewer's actual locale once mounted.\n const locale = mounted ? undefined : 'en-US';\n\n const [label, setLabel] = React.useState<string>(() => formatRelative(d, variant, locale));\n\n React.useEffect(() => {\n // Re-compute immediately on mount (picks up the viewer's real locale, and\n // avoids the relative label going stale)\n setLabel(formatRelative(d, variant, locale));\n\n const schedule = () => {\n const id = window.setInterval(() => {\n setLabel(formatRelative(d, variant, locale));\n }, updateInterval(d));\n return id;\n };\n const id = schedule();\n return () => window.clearInterval(id);\n }, [d, variant, locale]);\n\n return (\n <time\n dateTime={d.toISOString()}\n title={title ?? absoluteTitle(d, locale)}\n className={cn('tabular-nums', className)}\n {...props}\n >\n {label}\n </time>\n );\n}\n\n// ── Absolute Timestamp ────────────────────────────────────────────────────────\n\n/** Formats am/pm per Content Principles: lowercase, space before, no periods. */\nfunction formatAmPm(date: Date, options: Intl.DateTimeFormatOptions, locale: string | undefined): string {\n const parts = new Intl.DateTimeFormat(locale, options).formatToParts(date);\n let result = '';\n for (const part of parts) {\n if (part.type === 'dayPeriod') {\n result += part.value.toLowerCase().replace(/\\./g, '');\n } else {\n result += part.value;\n }\n }\n return result;\n}\n\nfunction formatAbsolute(date: Date, variant: string, end: Date | undefined, locale: string | undefined): string {\n switch (variant) {\n case 'deadline': {\n const weekday = date.toLocaleDateString(locale, { weekday: 'short' });\n const datePart = date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n const timePart = formatAmPm(date, { hour: 'numeric', minute: '2-digit' }, locale);\n const tz = date.toLocaleTimeString(locale, { timeZoneName: 'short' }).split(' ').pop() ?? '';\n return `${weekday}, ${datePart} \\u00b7 ${timePart} ${tz}`;\n }\n case 'log': {\n const datePart = date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n const timePart = formatAmPm(date, { hour: 'numeric', minute: '2-digit' }, locale);\n const tz = date.toLocaleTimeString(locale, { timeZoneName: 'short' }).split(' ').pop() ?? '';\n return `${datePart}, ${timePart} ${tz}`;\n }\n case 'range': {\n if (!end) return date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n const startYear = date.getFullYear();\n const endYear = end.getFullYear();\n if (startYear === endYear) {\n const start = date.toLocaleDateString(locale, { month: 'short', day: 'numeric' });\n const endStr = end.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n return `${start} \\u2013 ${endStr}`;\n }\n const start = date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n const endStr = end.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n return `${start} \\u2013 ${endStr}`;\n }\n case 'date':\n default:\n return date.toLocaleDateString(locale, { month: 'short', day: 'numeric', year: 'numeric' });\n }\n}\n\nexport interface AbsoluteTimestampProps extends Omit<React.TimeHTMLAttributes<HTMLTimeElement>, 'dateTime'> {\n /** The timestamp to display. */\n date: Date | string | number;\n /** Format variant matching the Content Principles absolute timestamp table. */\n variant?: 'deadline' | 'log' | 'date' | 'range';\n /** End date for range variant. */\n end?: Date | string | number;\n}\n\n/**\n * Formats a date as an absolute timestamp following the Content Principles.\n * Use for deadlines, certificate expiry, audit logs, and date-only displays.\n */\nexport function AbsoluteTimestamp({ date, variant = 'date', end, className, ...props }: AbsoluteTimestampProps) {\n const d = React.useMemo(() => new Date(date), [date]);\n const e = React.useMemo(() => (end ? new Date(end) : undefined), [end]);\n const mounted = useMounted();\n // Fixed locale for the server-rendered/first-paint value (the server can't\n // know the viewer's locale); the viewer's actual locale once mounted.\n const locale = mounted ? undefined : 'en-US';\n\n return (\n <time\n dateTime={d.toISOString()}\n title={absoluteTitle(d, locale)}\n className={cn('tabular-nums', className)}\n {...props}\n >\n {formatAbsolute(d, variant, e, locale)}\n </time>\n );\n}\n\n// ── Elapsed Time ──────────────────────────────────────────────────────────────\n\nfunction formatElapsed(totalSeconds: number, variant: string): string {\n const days = Math.floor(totalSeconds / 86400);\n const hours = Math.floor((totalSeconds % 86400) / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const seconds = Math.floor(totalSeconds % 60);\n\n switch (variant) {\n case 'expanded': {\n const parts: string[] = [];\n if (days > 0) parts.push(`${days} day${days === 1 ? '' : 's'}`);\n if (hours > 0) parts.push(`${hours} hour${hours === 1 ? '' : 's'}`);\n if (minutes > 0) parts.push(`${minutes} minute${minutes === 1 ? '' : 's'}`);\n if (seconds > 0 || parts.length === 0) parts.push(`${seconds} second${seconds === 1 ? '' : 's'}`);\n return parts.join(', ');\n }\n case 'long': {\n if (days > 0) return `${days}d ${hours}h`;\n if (hours > 0) return `${hours}h ${minutes}m`;\n return `${minutes}m`;\n }\n case 'compact':\n default: {\n if (days > 0) return `${days}d ${hours}h`;\n if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;\n if (minutes > 0) return `${minutes}m ${seconds}s`;\n return `${seconds}s`;\n }\n }\n}\n\nexport interface ElapsedTimeProps extends Omit<React.TimeHTMLAttributes<HTMLTimeElement>, 'dateTime'> {\n /** Total elapsed seconds. Ignored if startedAt is provided. */\n seconds?: number;\n /** Format variant matching the Content Principles elapsed time table. */\n variant?: 'compact' | 'expanded' | 'long';\n /** Live-tick every second for active operations. */\n live?: boolean;\n /** Compute elapsed from a start time instead of a fixed seconds value. */\n startedAt?: Date | string | number;\n}\n\n/**\n * Formats a duration as elapsed time following the Content Principles.\n * Use for reconciliation loops, build jobs, health checks, and running operations.\n */\nexport function ElapsedTime({\n seconds: secondsProp,\n variant = 'compact',\n live = false,\n startedAt,\n className,\n ...props\n}: ElapsedTimeProps) {\n const startDate = React.useMemo(() => (startedAt ? new Date(startedAt) : undefined), [startedAt]);\n\n const computeSeconds = React.useCallback(() => {\n if (startDate) return Math.floor((Date.now() - startDate.getTime()) / 1000);\n return secondsProp ?? 0;\n }, [startDate, secondsProp]);\n\n const [elapsed, setElapsed] = React.useState(computeSeconds);\n\n React.useEffect(() => {\n setElapsed(computeSeconds());\n\n if (!live && !startDate) return;\n\n const id = window.setInterval(() => {\n setElapsed(computeSeconds());\n }, 1000);\n return () => window.clearInterval(id);\n }, [live, startDate, computeSeconds]);\n\n const iso = `PT${elapsed}S`;\n\n return (\n <time dateTime={iso} className={cn('tabular-nums', className)} {...props}>\n {formatElapsed(elapsed, variant)}\n </time>\n );\n}\n","import { useSyncExternalStore } from 'react';\n\nfunction subscribe() {\n return () => {};\n}\n\n/**\n * Returns `false` on the server and during initial client hydration, then\n * `true` once mounted — for values that are only valid client-side (e.g. the\n * viewer's actual `Intl` locale, vs. a fixed locale used for the server-\n * rendered/first-paint value) without a hydration mismatch. Implemented via\n * `useSyncExternalStore` rather than `useEffect` + `setState` — the latter\n * works, but re-derives the same \"true\" value on every mount and trips\n * `react-hooks/set-state-in-effect` since there's no actual subscription.\n */\nexport function useMounted() {\n return useSyncExternalStore(\n subscribe,\n () => true,\n () => false,\n );\n}\n"],"mappings":"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","names":["React","jsx","start","endStr","jsx","jsx"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/code-block.tsx","../src/lib/utils.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useState } from 'react';\nimport { Icon } from '@upbound/monarch-core';\n\nimport { cn } from '@/lib/utils';\nimport {\n Card,\n CardContent,\n CardFooter,\n CardHeader,\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from '@upbound/monarch-core';\n\nfunction CodeBlock({\n className,\n variant = 'default',\n size = 'default',\n ...props\n}: React.ComponentProps<'div'> & {\n variant?: 'default' | 'terminal';\n size?: 'default' | 'sm';\n}) {\n return (\n <Card\n data-slot=\"code-block\"\n data-variant={variant}\n data-size={size}\n role=\"region\"\n className={cn(\n 'group/code-block gap-0 overflow-hidden py-0',\n size === 'default' && 'text-[13px]',\n size === 'sm' && 'text-body-sm',\n variant === 'terminal' && 'border-terminal-border bg-terminal-bg text-terminal-fg ring-0',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CodeBlockHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <CardHeader\n className={cn(\n 'group-data-[variant=terminal]/code-block:border-terminal-border group-data-[variant=terminal]/code-block:bg-terminal-bg group-data-[variant=terminal]/code-block:text-terminal-muted flex flex-row items-center gap-2 border-b bg-[color-mix(in_oklch,var(--muted)_50%,var(--card))] py-4',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CodeBlockBody({\n className,\n code,\n html,\n lineNumbers,\n maxHeight,\n copyButton,\n downloadButton,\n ...props\n}: React.ComponentProps<'div'> & {\n code: string;\n html?: string;\n lineNumbers?: boolean;\n maxHeight?: string;\n copyButton?: React.ReactNode;\n downloadButton?: React.ReactNode;\n}) {\n // Layout note: the copy/download buttons must stay anchored to the\n // top-right of the visible code area when the content scrolls\n // horizontally. To make that work the relative containing block\n // (CardContent) must NOT be the scrollable element — otherwise\n // `right`-positioning is computed against the scrolled content edge and\n // the buttons move with the scroll. So the overflow lives on an inner\n // wrapper and the absolute button group is its sibling under CardContent.\n return (\n <CardContent data-line-numbers={lineNumbers || undefined} className={cn('relative p-0!', className)} {...props}>\n {(copyButton || downloadButton) && (\n <div className=\"absolute top-2 right-2 z-10 flex items-center gap-1 opacity-0 transition-opacity group-hover/code-block:opacity-100\">\n {copyButton}\n {downloadButton}\n </div>\n )}\n <div\n className={cn('overflow-x-auto', maxHeight && 'overflow-y-auto')}\n style={maxHeight ? { maxHeight } : undefined}\n >\n {html ? (\n <div\n className=\"[&_code]:font-mono [&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:p-4 group-data-[size=sm]/code-block:[&_pre]:p-3\"\n dangerouslySetInnerHTML={{ __html: html }}\n />\n ) : lineNumbers ? (\n <pre className=\"p-4 group-data-[size=sm]/code-block:p-3\">\n <code className=\"font-mono\">\n {code.split('\\n').map((line, i) => (\n <span key={i} className=\"line\">\n {line}\n {'\\n'}\n </span>\n ))}\n </code>\n </pre>\n ) : (\n <pre className=\"p-4 group-data-[size=sm]/code-block:p-3\">\n <code className=\"font-mono whitespace-pre\">{code}</code>\n </pre>\n )}\n </div>\n </CardContent>\n );\n}\n\nfunction CodeBlockFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <CardFooter\n className={cn(\n 'text-body-sm group-data-[variant=terminal]/code-block:border-terminal-border group-data-[variant=terminal]/code-block:bg-terminal-footer-bg group-data-[variant=terminal]/code-block:text-terminal-muted',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CodeBlockCopyButton({\n code,\n className,\n ...props\n}: Omit<React.ComponentProps<'button'>, 'children'> & {\n code: string;\n}) {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n data-slot=\"code-block-copy-button\"\n onClick={handleCopy}\n className={cn(\n 'border-border bg-background inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border motion-safe:transition-colors',\n 'text-muted-foreground hover:bg-accent hover:text-foreground',\n 'focus-visible:ring-ring/30 focus-visible:ring-2 focus-visible:outline-none',\n 'group-data-[variant=terminal]/code-block:border-terminal-border group-data-[variant=terminal]/code-block:bg-terminal-bg group-data-[variant=terminal]/code-block:text-terminal-muted group-data-[variant=terminal]/code-block:hover:bg-terminal-muted-hover-bg group-data-[variant=terminal]/code-block:hover:text-terminal-muted-hover-fg',\n className,\n )}\n aria-label={copied ? 'Copied' : 'Copy code'}\n {...props}\n >\n {copied ? <Icon name=\"check\" size=\"sm\" className=\"text-green-500\" /> : <Icon name=\"copy\" size=\"sm\" />}\n </button>\n </TooltipTrigger>\n <TooltipContent>{copied ? 'Copied' : 'Copy'}</TooltipContent>\n </Tooltip>\n );\n}\n\nfunction CodeBlockDownloadButton({\n code,\n fileName,\n mimeType = 'text/plain',\n className,\n ...props\n}: Omit<React.ComponentProps<'button'>, 'children'> & {\n code: string;\n fileName: string;\n mimeType?: string;\n}) {\n const [downloaded, setDownloaded] = useState(false);\n\n const handleDownload = () => {\n const blob = new Blob([code], { type: mimeType });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = fileName;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n setDownloaded(true);\n setTimeout(() => setDownloaded(false), 2000);\n };\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n data-slot=\"code-block-download-button\"\n onClick={handleDownload}\n className={cn(\n 'border-border bg-background inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border motion-safe:transition-colors',\n 'text-muted-foreground hover:bg-accent hover:text-foreground',\n 'focus-visible:ring-ring/30 focus-visible:ring-2 focus-visible:outline-none',\n 'group-data-[variant=terminal]/code-block:border-terminal-border group-data-[variant=terminal]/code-block:bg-terminal-bg group-data-[variant=terminal]/code-block:text-terminal-muted group-data-[variant=terminal]/code-block:hover:bg-terminal-muted-hover-bg group-data-[variant=terminal]/code-block:hover:text-terminal-muted-hover-fg',\n className,\n )}\n aria-label={downloaded ? 'Downloaded' : 'Download code'}\n {...props}\n >\n {downloaded ? <Icon name=\"check\" size=\"sm\" className=\"text-green-500\" /> : <Icon name=\"download\" size=\"sm\" />}\n </button>\n </TooltipTrigger>\n <TooltipContent>{downloaded ? 'Downloaded' : 'Download'}</TooltipContent>\n </Tooltip>\n );\n}\n\nexport { CodeBlock, CodeBlockHeader, CodeBlockBody, CodeBlockFooter, CodeBlockCopyButton, CodeBlockDownloadButton };\n","import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [\n {\n text: [\n 'display-hero',\n 'display-kpi-sm',\n 'display-kpi',\n 'display-kpi-lg',\n 'display-feature',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'body-lg',\n 'body',\n 'body-sm',\n 'caption',\n 'eyebrow',\n ],\n },\n ],\n },\n },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/code-block.tsx","../src/lib/utils.ts"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport { useState } from 'react';\nimport { Icon } from '@upbound/monarch-core';\n\nimport { cn } from '@/lib/utils';\nimport {\n Card,\n CardContent,\n CardFooter,\n CardHeader,\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from '@upbound/monarch-core';\n\nfunction CodeBlock({\n className,\n variant = 'default',\n size = 'default',\n ...props\n}: React.ComponentProps<'div'> & {\n variant?: 'default' | 'terminal';\n size?: 'default' | 'sm';\n}) {\n return (\n <Card\n data-slot=\"code-block\"\n data-variant={variant}\n data-size={size}\n role=\"region\"\n className={cn(\n 'group/code-block gap-0 overflow-hidden py-0',\n size === 'default' && 'text-[13px]',\n size === 'sm' && 'text-body-sm',\n variant === 'terminal' && 'border-terminal-border bg-terminal-bg text-terminal-fg ring-0',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CodeBlockHeader({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <CardHeader\n className={cn(\n 'group-data-[variant=terminal]/code-block:border-terminal-border group-data-[variant=terminal]/code-block:bg-terminal-bg group-data-[variant=terminal]/code-block:text-terminal-muted flex flex-row items-center gap-2 border-b bg-[color-mix(in_oklch,var(--muted)_50%,var(--card))] py-4',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CodeBlockBody({\n className,\n code,\n html,\n lineNumbers,\n maxHeight,\n copyButton,\n downloadButton,\n ...props\n}: React.ComponentProps<'div'> & {\n code: string;\n html?: string;\n lineNumbers?: boolean;\n maxHeight?: string;\n copyButton?: React.ReactNode;\n downloadButton?: React.ReactNode;\n}) {\n // Layout note: the copy/download buttons must stay anchored to the\n // top-right of the visible code area when the content scrolls\n // horizontally. To make that work the relative containing block\n // (CardContent) must NOT be the scrollable element — otherwise\n // `right`-positioning is computed against the scrolled content edge and\n // the buttons move with the scroll. So the overflow lives on an inner\n // wrapper and the absolute button group is its sibling under CardContent.\n return (\n <CardContent data-line-numbers={lineNumbers || undefined} className={cn('relative p-0!', className)} {...props}>\n {(copyButton || downloadButton) && (\n <div className=\"absolute top-2 right-2 z-10 flex items-center gap-1 opacity-0 transition-opacity group-hover/code-block:opacity-100\">\n {copyButton}\n {downloadButton}\n </div>\n )}\n <div\n className={cn('overflow-x-auto', maxHeight && 'overflow-y-auto')}\n style={maxHeight ? { maxHeight } : undefined}\n >\n {html ? (\n <div\n className=\"[&_code]:font-mono [&_pre]:m-0 [&_pre]:bg-transparent! [&_pre]:p-4 group-data-[size=sm]/code-block:[&_pre]:p-3\"\n dangerouslySetInnerHTML={{ __html: html }}\n />\n ) : lineNumbers ? (\n <pre className=\"p-4 group-data-[size=sm]/code-block:p-3\">\n <code className=\"font-mono\">\n {code.split('\\n').map((line, i) => (\n <span key={i} className=\"line\">\n {line}\n {'\\n'}\n </span>\n ))}\n </code>\n </pre>\n ) : (\n <pre className=\"p-4 group-data-[size=sm]/code-block:p-3\">\n <code className=\"font-mono whitespace-pre\">{code}</code>\n </pre>\n )}\n </div>\n </CardContent>\n );\n}\n\nfunction CodeBlockFooter({ className, ...props }: React.ComponentProps<'div'>) {\n return (\n <CardFooter\n className={cn(\n 'text-body-sm group-data-[variant=terminal]/code-block:border-terminal-border group-data-[variant=terminal]/code-block:bg-terminal-footer-bg group-data-[variant=terminal]/code-block:text-terminal-muted',\n className,\n )}\n {...props}\n />\n );\n}\n\nfunction CodeBlockCopyButton({\n code,\n className,\n ...props\n}: Omit<React.ComponentProps<'button'>, 'children'> & {\n code: string;\n}) {\n const [copied, setCopied] = useState(false);\n\n const handleCopy = async () => {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n };\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n data-slot=\"code-block-copy-button\"\n onClick={handleCopy}\n className={cn(\n 'border-border bg-background inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border motion-safe:transition-colors',\n 'text-muted-foreground hover:bg-accent hover:text-foreground',\n 'focus-visible:ring-ring/30 focus-visible:ring-2 focus-visible:outline-none',\n 'group-data-[variant=terminal]/code-block:border-terminal-border group-data-[variant=terminal]/code-block:bg-terminal-bg group-data-[variant=terminal]/code-block:text-terminal-muted group-data-[variant=terminal]/code-block:hover:bg-terminal-muted-hover-bg group-data-[variant=terminal]/code-block:hover:text-terminal-muted-hover-fg',\n className,\n )}\n aria-label={copied ? 'Copied' : 'Copy code'}\n {...props}\n >\n {copied ? <Icon name=\"check\" size=\"sm\" className=\"text-green-500\" /> : <Icon name=\"copy\" size=\"sm\" />}\n </button>\n </TooltipTrigger>\n <TooltipContent>{copied ? 'Copied' : 'Copy'}</TooltipContent>\n </Tooltip>\n );\n}\n\nfunction CodeBlockDownloadButton({\n code,\n fileName,\n mimeType = 'text/plain',\n className,\n ...props\n}: Omit<React.ComponentProps<'button'>, 'children'> & {\n code: string;\n fileName: string;\n mimeType?: string;\n}) {\n const [downloaded, setDownloaded] = useState(false);\n\n const handleDownload = () => {\n const blob = new Blob([code], { type: mimeType });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = fileName;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n setDownloaded(true);\n setTimeout(() => setDownloaded(false), 2000);\n };\n\n return (\n <Tooltip>\n <TooltipTrigger asChild>\n <button\n data-slot=\"code-block-download-button\"\n onClick={handleDownload}\n className={cn(\n 'border-border bg-background inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md border motion-safe:transition-colors',\n 'text-muted-foreground hover:bg-accent hover:text-foreground',\n 'focus-visible:ring-ring/30 focus-visible:ring-2 focus-visible:outline-none',\n 'group-data-[variant=terminal]/code-block:border-terminal-border group-data-[variant=terminal]/code-block:bg-terminal-bg group-data-[variant=terminal]/code-block:text-terminal-muted group-data-[variant=terminal]/code-block:hover:bg-terminal-muted-hover-bg group-data-[variant=terminal]/code-block:hover:text-terminal-muted-hover-fg',\n className,\n )}\n aria-label={downloaded ? 'Downloaded' : 'Download code'}\n {...props}\n >\n {downloaded ? <Icon name=\"check\" size=\"sm\" className=\"text-green-500\" /> : <Icon name=\"download\" size=\"sm\" />}\n </button>\n </TooltipTrigger>\n <TooltipContent>{downloaded ? 'Downloaded' : 'Download'}</TooltipContent>\n </Tooltip>\n );\n}\n\nexport { CodeBlock, CodeBlockHeader, CodeBlockBody, CodeBlockFooter, CodeBlockCopyButton, CodeBlockDownloadButton };\n","import { clsx, type ClassValue } from 'clsx';\nimport { extendTailwindMerge } from 'tailwind-merge';\n\nconst twMerge = extendTailwindMerge({\n extend: {\n classGroups: {\n 'font-size': [\n {\n text: [\n 'display-hero',\n 'display-kpi-sm',\n 'display-kpi',\n 'display-kpi-lg',\n 'display-feature',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'body-lg',\n 'body',\n 'body-sm',\n 'caption',\n 'eyebrow',\n ],\n },\n ],\n },\n },\n});\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport function sortBy<T>(items: T[], getKey: (item: T) => string): T[] {\n return [...items].sort((a, b) => getKey(a).localeCompare(getKey(b)));\n}\n"],"mappings":"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","names":[]}