@fx-/chat-shadcn 0.0.0-stage → 0.1.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/index.d.ts +143 -0
- package/dist/index.js +741 -0
- package/package.json +59 -4
- package/src/theme.css +190 -0
- package/README.md +0 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ClassValue } from "clsx";
|
|
2
|
+
import { ActivityItem, ActivitySummaryProps, ChatClassNames, ChatComponents, CodeBlockProps, IconComponent, IconRegistry, JsonViewProps, MarkdownProps, MessageDetailsProps } from "@fx-/chat";
|
|
3
|
+
import { ButtonHTMLAttributes, ComponentPropsWithoutRef, FC, ForwardRefExoticComponent, HTMLAttributes, JSX, RefAttributes } from "react";
|
|
4
|
+
import { Drawer as Drawer$1, Portal } from "vaul";
|
|
5
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
6
|
+
import { VariantProps } from "class-variance-authority";
|
|
7
|
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
8
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
9
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
10
|
+
import { ClassProp } from "class-variance-authority/types";
|
|
11
|
+
import { DialogCloseProps, DialogContentProps, DialogDescriptionProps, DialogOverlayProps, DialogTitleProps } from "@radix-ui/react-dialog";
|
|
12
|
+
|
|
13
|
+
//#region src/class-names.d.ts
|
|
14
|
+
/** Options for {@link createShadcnClassNames}. */
|
|
15
|
+
interface ShadcnClassNamesOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Touch-first sizing: disclosure rows are 40px tall with 14px text, the size
|
|
18
|
+
* of the answer text (the row header is the tap target), instead of 28px with 12px text. Pair it with
|
|
19
|
+
* `createShadcnComponents({ touch: true })`. Default false.
|
|
20
|
+
*/
|
|
21
|
+
touch?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build the shadcn `classNames` theme, tuned by `options`.
|
|
25
|
+
*
|
|
26
|
+
* Every disclosure row — static (the pending "Starting…"), collapsible
|
|
27
|
+
* (reasoning, tool) and a group's summary — has the same header height, and a
|
|
28
|
+
* static row reserves the caret's width, so a live row changing kind keeps its
|
|
29
|
+
* label in place. Row bodies indent to the label.
|
|
30
|
+
*/
|
|
31
|
+
declare function createShadcnClassNames({ touch, }?: ShadcnClassNamesOptions): ChatClassNames;
|
|
32
|
+
/** The desktop (pointer-first) theme. */
|
|
33
|
+
declare const shadcnClassNames: ChatClassNames;
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/cn.d.ts
|
|
37
|
+
declare function cn(...inputs: ClassValue[]): string;
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
//#region src/code.d.ts
|
|
41
|
+
declare function CodeBlock({ code, language, className }: CodeBlockProps): JSX.Element;
|
|
42
|
+
declare function JsonView({ value, className }: JsonViewProps): JSX.Element;
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/icons.d.ts
|
|
46
|
+
/** The full lucide icon registry, one component per {@link IconName}. */
|
|
47
|
+
declare const lucideIcons: IconRegistry;
|
|
48
|
+
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/markdown.d.ts
|
|
51
|
+
declare function MarkdownView({ content, streaming }: MarkdownProps): JSX.Element;
|
|
52
|
+
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/activity-tiles.d.ts
|
|
55
|
+
/** The small text inside a tile: a live rate, the time so far or taken, a plan's progress. */
|
|
56
|
+
declare function tileText(item: ActivityItem, now: number): string | undefined;
|
|
57
|
+
declare function createActivityTiles(toolIcons?: Record<string, IconComponent>): ({ items, live, onActivate }: ActivitySummaryProps) => JSX.Element;
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/message-details.d.ts
|
|
61
|
+
declare function MessageDetailsSheet({ open, onOpenChange, details, outcome, copyText, actions, activity, focusId, }: MessageDetailsProps): JSX.Element;
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/registry.d.ts
|
|
65
|
+
interface ShadcnComponentsOptions {
|
|
66
|
+
/** Tooltip open delay in ms. Default 150. */
|
|
67
|
+
tooltipDelay?: number;
|
|
68
|
+
/**
|
|
69
|
+
* How long the copy button shows its "copied" check, in ms. `0` keeps it
|
|
70
|
+
* until the button remounts. Default 1500.
|
|
71
|
+
*/
|
|
72
|
+
copyResetMs?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Touch-first tuning: disclosure triggers drop their hover tint (touch has
|
|
75
|
+
* no hover); row height comes from the classNames theme
|
|
76
|
+
* (`createShadcnClassNames({ touch: true })`); a compact footer's "…" opens
|
|
77
|
+
* the message's details and actions in a bottom sheet instead of revealing
|
|
78
|
+
* them inline, and activity shown as a summary renders as tiles. Icon-only
|
|
79
|
+
* buttons always get a 44×44 target. Default false.
|
|
80
|
+
*/
|
|
81
|
+
touch?: boolean;
|
|
82
|
+
/** Icons for tool calls by tool name (activity tiles); others get the wrench. */
|
|
83
|
+
toolIcons?: Record<string, IconComponent>;
|
|
84
|
+
}
|
|
85
|
+
/** Build the shadcn registry, tuned by `options`. */
|
|
86
|
+
declare function createShadcnComponents({ tooltipDelay, copyResetMs, touch, toolIcons, }?: ShadcnComponentsOptions): ChatComponents;
|
|
87
|
+
/** The shadcn registry with default (pointer-first) tuning. */
|
|
88
|
+
declare const shadcnComponents: ChatComponents;
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/ui/avatar.d.ts
|
|
92
|
+
declare const Avatar: ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarProps & RefAttributes<HTMLSpanElement>, "ref"> & RefAttributes<HTMLSpanElement>>;
|
|
93
|
+
declare const AvatarFallback: ForwardRefExoticComponent<Omit<AvatarPrimitive.AvatarFallbackProps & RefAttributes<HTMLSpanElement>, "ref"> & RefAttributes<HTMLSpanElement>>;
|
|
94
|
+
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/ui/badge.d.ts
|
|
97
|
+
declare const badgeVariants: (props?: ({
|
|
98
|
+
variant?: "default" | "destructive" | "success" | null | undefined;
|
|
99
|
+
} & ClassProp) | undefined) => string;
|
|
100
|
+
interface BadgeProps extends HTMLAttributes<HTMLSpanElement>, VariantProps<typeof badgeVariants> {
|
|
101
|
+
}
|
|
102
|
+
declare function Badge({ className, variant, ...props }: BadgeProps): JSX.Element;
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/ui/button.d.ts
|
|
106
|
+
declare const buttonVariants: (props?: ({
|
|
107
|
+
variant?: "default" | "ghost" | "outline" | null | undefined;
|
|
108
|
+
size?: "default" | "sm" | "icon" | "icon-sm" | null | undefined;
|
|
109
|
+
} & ClassProp) | undefined) => string;
|
|
110
|
+
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
111
|
+
}
|
|
112
|
+
declare function Button({ className, variant, size, type, ...props }: ButtonProps): JSX.Element;
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/ui/collapsible.d.ts
|
|
116
|
+
declare const Collapsible: ForwardRefExoticComponent<CollapsiblePrimitive.CollapsibleProps & RefAttributes<HTMLDivElement>>;
|
|
117
|
+
declare const CollapsibleTrigger: ForwardRefExoticComponent<CollapsiblePrimitive.CollapsibleTriggerProps & RefAttributes<HTMLButtonElement>>;
|
|
118
|
+
declare const CollapsibleContent: ForwardRefExoticComponent<CollapsiblePrimitive.CollapsibleContentProps & RefAttributes<HTMLDivElement>>;
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/ui/drawer.d.ts
|
|
122
|
+
declare const Drawer: ({ shouldScaleBackground, ...props }: ComponentPropsWithoutRef<typeof Drawer$1.Root>) => JSX.Element;
|
|
123
|
+
declare const DrawerPortal: typeof Portal;
|
|
124
|
+
declare const DrawerClose: ForwardRefExoticComponent<DialogCloseProps & RefAttributes<HTMLButtonElement>>;
|
|
125
|
+
declare const DrawerOverlay: ForwardRefExoticComponent<Omit<Omit<DialogOverlayProps & RefAttributes<HTMLDivElement>, "ref"> & RefAttributes<HTMLDivElement>, "ref"> & RefAttributes<HTMLDivElement>>;
|
|
126
|
+
declare const DrawerContent: ForwardRefExoticComponent<Omit<Omit<Omit<DialogContentProps & RefAttributes<HTMLDivElement>, "ref"> & RefAttributes<HTMLDivElement>, "ref">, "ref"> & RefAttributes<HTMLDivElement>>;
|
|
127
|
+
declare const DrawerHeader: ({ className, ...props }: HTMLAttributes<HTMLDivElement>) => JSX.Element;
|
|
128
|
+
declare const DrawerTitle: ForwardRefExoticComponent<Omit<DialogTitleProps & RefAttributes<HTMLHeadingElement>, "ref"> & RefAttributes<HTMLHeadingElement>>;
|
|
129
|
+
declare const DrawerDescription: ForwardRefExoticComponent<Omit<DialogDescriptionProps & RefAttributes<HTMLParagraphElement>, "ref"> & RefAttributes<HTMLParagraphElement>>;
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/ui/scroll-area.d.ts
|
|
133
|
+
declare const ScrollArea: ForwardRefExoticComponent<Omit<ScrollAreaPrimitive.ScrollAreaProps & RefAttributes<HTMLDivElement>, "ref"> & RefAttributes<HTMLDivElement>>;
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/ui/tooltip.d.ts
|
|
137
|
+
declare const TooltipProvider: FC<TooltipPrimitive.TooltipProviderProps>;
|
|
138
|
+
declare const Tooltip: FC<TooltipPrimitive.TooltipProps>;
|
|
139
|
+
declare const TooltipTrigger: ForwardRefExoticComponent<TooltipPrimitive.TooltipTriggerProps & RefAttributes<HTMLButtonElement>>;
|
|
140
|
+
declare const TooltipContent: ForwardRefExoticComponent<Omit<Omit<TooltipPrimitive.TooltipContentProps & RefAttributes<HTMLDivElement>, "ref">, "ref"> & RefAttributes<HTMLDivElement>>;
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
export { Avatar, AvatarFallback, Badge, BadgeProps, Button, ButtonProps, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, JsonView, MarkdownView, MessageDetailsSheet, ScrollArea, ShadcnClassNamesOptions, ShadcnComponentsOptions, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, cn, createActivityTiles, createShadcnClassNames, createShadcnComponents, lucideIcons, shadcnClassNames, shadcnComponents, tileText };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,741 @@
|
|
|
1
|
+
import { clsx } from "clsx";
|
|
2
|
+
import { twMerge } from "tailwind-merge";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { AudioLines, Brain, Check, ChevronDown, ChevronRight, Copy, Download, FileText, GitFork, ImageOff, Info, ListChecks, Loader2, Maximize2, MoreHorizontal, Paperclip, RefreshCw, RotateCcw, SendHorizontal, Settings, Square, Video, Wrench, X } from "lucide-react";
|
|
5
|
+
import ReactMarkdown, { defaultUrlTransform } from "react-markdown";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
import { formatDuration, formatTokensPerSecond } from "@fx-/chat";
|
|
8
|
+
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
import { Drawer as Drawer$1 } from "vaul";
|
|
10
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
11
|
+
import { cva } from "class-variance-authority";
|
|
12
|
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
13
|
+
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
|
14
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
15
|
+
|
|
16
|
+
//#region src/cn.ts
|
|
17
|
+
function cn(...inputs) {
|
|
18
|
+
return twMerge(clsx(inputs));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/tile.ts
|
|
23
|
+
/**
|
|
24
|
+
* The one tile size on touch: activity tiles, the footer's activity facts and
|
|
25
|
+
* its icon buttons are all 44px squares with a 16px icon and 10px text inside.
|
|
26
|
+
*/
|
|
27
|
+
const TILE = "inline-flex size-11 shrink-0 flex-col items-center justify-center gap-1 rounded-lg bg-muted text-[0.625rem] leading-none tabular-nums text-muted-foreground";
|
|
28
|
+
/** A tile's icon. */
|
|
29
|
+
const TILE_ICON = "size-4 shrink-0";
|
|
30
|
+
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/class-names.ts
|
|
33
|
+
/**
|
|
34
|
+
* Build the shadcn `classNames` theme, tuned by `options`.
|
|
35
|
+
*
|
|
36
|
+
* Every disclosure row — static (the pending "Starting…"), collapsible
|
|
37
|
+
* (reasoning, tool) and a group's summary — has the same header height, and a
|
|
38
|
+
* static row reserves the caret's width, so a live row changing kind keeps its
|
|
39
|
+
* label in place. Row bodies indent to the label.
|
|
40
|
+
*/
|
|
41
|
+
function createShadcnClassNames({ touch = false } = {}) {
|
|
42
|
+
const rowText = touch ? "text-sm" : "text-xs";
|
|
43
|
+
const metricText = touch ? "text-[0.8125rem]" : "text-xs";
|
|
44
|
+
return {
|
|
45
|
+
log: "flex flex-col gap-3 data-[minimal=true]:gap-[0.625rem] text-foreground",
|
|
46
|
+
message: "group/msg flex flex-col gap-1.5 data-[minimal=true]:gap-[3px] text-foreground",
|
|
47
|
+
messageBlocks: "flex flex-col gap-1.5 group-data-[minimal=true]/msg:gap-px",
|
|
48
|
+
gutterRow: "flex items-start gap-2",
|
|
49
|
+
gutterTime: "w-16 shrink-0 text-right text-xs leading-[1.4] text-muted-foreground tabular-nums select-none",
|
|
50
|
+
gutterContent: "min-w-0 flex-1",
|
|
51
|
+
row: cn(rowText, "font-medium text-muted-foreground [&>.chat-row-header]:pl-5"),
|
|
52
|
+
rowError: "border-destructive text-destructive",
|
|
53
|
+
rowHeader: cn("flex min-w-0 items-center gap-1.5 font-medium leading-[1.4] text-muted-foreground", rowText, touch ? "min-h-10" : "min-h-7"),
|
|
54
|
+
rowIcon: "size-[1em] shrink-0 [.chat-row-affordance+&]:hidden",
|
|
55
|
+
rowAffordance: "inline-flex size-[1em] items-center justify-center text-muted-foreground data-[affordance=success]:text-green-600",
|
|
56
|
+
rowLabel: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
|
|
57
|
+
rowTimer: cn("shrink-0 text-muted-foreground tabular-nums", metricText),
|
|
58
|
+
rowTps: cn("shrink-0 text-muted-foreground tabular-nums", metricText),
|
|
59
|
+
group: "!p-0 !m-0",
|
|
60
|
+
groupBody: "ml-[6px] flex flex-col gap-0 border-l-2 border-border/60 pl-3",
|
|
61
|
+
groupSummary: "inline-flex min-w-0 items-center gap-1.5",
|
|
62
|
+
groupSummaryLabel: "min-w-0 overflow-hidden text-ellipsis whitespace-nowrap",
|
|
63
|
+
groupMore: "shrink-0 text-xs text-muted-foreground tabular-nums",
|
|
64
|
+
text: "w-fit max-w-[85%] rounded-md px-[0.9375rem] py-[0.625rem]",
|
|
65
|
+
textUser: "bg-primary text-primary-foreground",
|
|
66
|
+
textAssistant: "bg-muted text-foreground",
|
|
67
|
+
textPlain: "text-sm leading-5 whitespace-pre-wrap [overflow-wrap:anywhere] [word-break:break-word]",
|
|
68
|
+
mention: "font-medium text-primary group-data-[role=user]/msg:text-primary-foreground",
|
|
69
|
+
toolBody: "flex flex-col gap-2 pl-5 pb-2",
|
|
70
|
+
toolSection: "flex flex-col gap-1",
|
|
71
|
+
toolSectionLabel: "text-xs text-muted-foreground uppercase tracking-wider",
|
|
72
|
+
toolMedia: "flex flex-wrap gap-1.5",
|
|
73
|
+
toolMediaImage: "max-w-full h-auto rounded-md",
|
|
74
|
+
reasoningBody: "pl-5 pb-2 text-sm text-muted-foreground",
|
|
75
|
+
systemBody: "pl-5 pb-2 text-sm text-muted-foreground",
|
|
76
|
+
plan: "m-0 flex list-none flex-col gap-1 pl-5 pb-2 text-sm",
|
|
77
|
+
rawJson: "text-xs",
|
|
78
|
+
inertBlock: "text-sm text-muted-foreground",
|
|
79
|
+
md: "text-sm leading-relaxed [&_p]:my-1 [&_pre]:my-2 [&_ul]:my-1 [&_ol]:my-1",
|
|
80
|
+
mdPre: "overflow-x-auto rounded-md bg-muted p-3 text-xs",
|
|
81
|
+
mdLink: "text-primary underline underline-offset-2",
|
|
82
|
+
mdCodeInline: "rounded bg-muted px-1 py-0.5 text-[0.85em]",
|
|
83
|
+
footer: cn("mt-1 flex flex-wrap items-center gap-1.5 min-w-0 text-muted-foreground", touch ? "text-xs" : "data-[compact=true]:gap-[3px] text-[0.625rem]"),
|
|
84
|
+
footerMetrics: cn("min-w-0 [overflow-wrap:anywhere]", touch && "basis-full"),
|
|
85
|
+
footerIcon: touch ? TILE_ICON : "size-2.5",
|
|
86
|
+
...touch ? {
|
|
87
|
+
footerReveal: TILE,
|
|
88
|
+
footerCopy: TILE,
|
|
89
|
+
footerRegenerate: TILE,
|
|
90
|
+
footerFork: TILE,
|
|
91
|
+
footerReset: TILE
|
|
92
|
+
} : {},
|
|
93
|
+
footerActivity: touch ? "contents" : "inline-flex items-center gap-1.5",
|
|
94
|
+
footerFact: touch ? "contents" : "inline-flex",
|
|
95
|
+
footerFactBody: touch ? cn(TILE, "[[data-failed]>&]:text-destructive", "[[data-expanded=true]_&]:bg-primary/15 [[data-expanded=true]_&]:text-foreground") : "inline-flex items-center gap-1 tabular-nums [[data-failed]>&]:text-destructive",
|
|
96
|
+
footerFactIcon: touch ? TILE_ICON : "size-3",
|
|
97
|
+
footerActivityItems: touch ? "contents [&>.chat-tiles]:contents" : "inline-flex",
|
|
98
|
+
detailsActivity: "flex flex-col gap-0.5",
|
|
99
|
+
detailsActivityItem: "rounded-lg px-2 -mx-2 transition-colors data-[focused=true]:bg-primary/10 data-[focused=true]:ring-1 data-[focused=true]:ring-primary/25",
|
|
100
|
+
attachments: "mt-2 flex flex-col gap-2",
|
|
101
|
+
attachmentGrid: "grid grid-cols-[repeat(auto-fill,minmax(7rem,1fr))] gap-2 list-none p-0 m-0",
|
|
102
|
+
attachmentGridItem: "m-0 min-w-0",
|
|
103
|
+
attachmentThumb: "relative block w-full aspect-square p-0 overflow-hidden rounded-md border border-border bg-muted text-foreground cursor-pointer",
|
|
104
|
+
attachmentThumbImage: "block w-full h-full object-cover",
|
|
105
|
+
attachmentThumbExpand: "absolute top-0.5 right-0.5 inline-flex items-center justify-center p-0.5 rounded bg-background/70 text-foreground",
|
|
106
|
+
attachmentChips: "flex flex-wrap gap-1.5 list-none p-0 m-0",
|
|
107
|
+
attachmentChip: "inline-flex items-center gap-1.5 px-1.5 py-0.5 rounded-md border border-border text-foreground text-[0.8125rem]",
|
|
108
|
+
attachmentChipIcon: "inline-flex items-center text-muted-foreground",
|
|
109
|
+
attachmentChipName: "min-w-0 max-w-[14rem] overflow-hidden text-ellipsis whitespace-nowrap",
|
|
110
|
+
attachmentChipDownload: "inline-flex items-center justify-center min-w-11 min-h-11 text-primary",
|
|
111
|
+
input: "flex flex-col gap-1.5",
|
|
112
|
+
inputAttachments: "flex flex-wrap gap-1.5 list-none p-0 m-0",
|
|
113
|
+
inputAttachment: "inline-flex items-center gap-1 rounded-md border border-border px-1.5 py-1 text-[0.8125rem]",
|
|
114
|
+
inputAttachmentName: "max-w-48 overflow-hidden text-ellipsis whitespace-nowrap",
|
|
115
|
+
inputTextarea: "rounded-md !border-border bg-background text-foreground placeholder:text-muted-foreground focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-0",
|
|
116
|
+
inputMention: "rounded-sm bg-primary/20 text-primary",
|
|
117
|
+
inputActions: "flex items-center justify-end gap-1.5",
|
|
118
|
+
completionPopup: "min-w-48 max-w-96 rounded-md border border-border bg-background text-foreground shadow-md overflow-hidden",
|
|
119
|
+
completionList: "list-none m-0 p-1",
|
|
120
|
+
completionOption: "flex items-baseline gap-1.5 px-2 py-1 rounded cursor-pointer data-[active=true]:bg-primary/15",
|
|
121
|
+
completionDescription: "text-muted-foreground text-[0.8125rem]",
|
|
122
|
+
completionLoading: "flex items-center justify-center p-3 text-muted-foreground",
|
|
123
|
+
lightbox: "p-3 bg-black/70",
|
|
124
|
+
lightboxDialog: "rounded-md bg-background",
|
|
125
|
+
lightboxImage: "rounded-md",
|
|
126
|
+
lightboxClose: "inline-flex items-center justify-center min-w-11 min-h-11 p-1.5 rounded-md border border-border bg-background text-foreground cursor-pointer"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** The desktop (pointer-first) theme. */
|
|
130
|
+
const shadcnClassNames = createShadcnClassNames();
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/code.tsx
|
|
134
|
+
function CodeBlock({ code, language, className }) {
|
|
135
|
+
return /* @__PURE__ */ jsx("pre", {
|
|
136
|
+
className: cn("overflow-x-auto rounded-md bg-muted p-3 text-xs leading-relaxed", className),
|
|
137
|
+
...language ? { "data-language": language } : {},
|
|
138
|
+
children: /* @__PURE__ */ jsx("code", { children: code })
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
function JsonView({ value, className }) {
|
|
142
|
+
let text;
|
|
143
|
+
try {
|
|
144
|
+
text = JSON.stringify(value, null, 2) ?? String(value);
|
|
145
|
+
} catch {
|
|
146
|
+
text = String(value);
|
|
147
|
+
}
|
|
148
|
+
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
149
|
+
code: text,
|
|
150
|
+
language: "json",
|
|
151
|
+
...className ? { className } : {}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/icons.tsx
|
|
157
|
+
/** Wrap a lucide component as a library {@link IconProps} icon. */
|
|
158
|
+
function wrap(Icon) {
|
|
159
|
+
return function WrappedIcon({ className, size }) {
|
|
160
|
+
return /* @__PURE__ */ jsx(Icon, {
|
|
161
|
+
className,
|
|
162
|
+
size: size ?? 16,
|
|
163
|
+
"aria-hidden": true
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const MAP = {
|
|
168
|
+
checklist: ListChecks,
|
|
169
|
+
info: Info,
|
|
170
|
+
spinner: Loader2,
|
|
171
|
+
check: Check,
|
|
172
|
+
caretRight: ChevronRight,
|
|
173
|
+
caretDown: ChevronDown,
|
|
174
|
+
copy: Copy,
|
|
175
|
+
wrench: Wrench,
|
|
176
|
+
brain: Brain,
|
|
177
|
+
gear: Settings,
|
|
178
|
+
dotsThree: MoreHorizontal,
|
|
179
|
+
regenerate: RefreshCw,
|
|
180
|
+
fork: GitFork,
|
|
181
|
+
reset: RotateCcw,
|
|
182
|
+
file: FileText,
|
|
183
|
+
audio: AudioLines,
|
|
184
|
+
video: Video,
|
|
185
|
+
send: SendHorizontal,
|
|
186
|
+
stop: Square,
|
|
187
|
+
paperclip: Paperclip,
|
|
188
|
+
close: X,
|
|
189
|
+
download: Download,
|
|
190
|
+
expand: Maximize2
|
|
191
|
+
};
|
|
192
|
+
/** The full lucide icon registry, one component per {@link IconName}. */
|
|
193
|
+
const lucideIcons = Object.fromEntries(Object.keys(MAP).map((name) => [name, wrap(MAP[name])]));
|
|
194
|
+
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/markdown.tsx
|
|
197
|
+
/** Inline image data: drawn without any request. */
|
|
198
|
+
const DATA_IMAGE = /^data:image\//i;
|
|
199
|
+
/** Keep `data:` image sources; every other URL gets the default sanitising. */
|
|
200
|
+
const urlTransform = (url, key) => key === "src" && DATA_IMAGE.test(url) ? url : defaultUrlTransform(url);
|
|
201
|
+
/** The host a URL points at, or the URL itself when it does not parse. */
|
|
202
|
+
function hostOf(src) {
|
|
203
|
+
try {
|
|
204
|
+
return new URL(src).host || src;
|
|
205
|
+
} catch {
|
|
206
|
+
return src;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function ExternalLink({ node: _node,...props }) {
|
|
210
|
+
return /* @__PURE__ */ jsx("a", {
|
|
211
|
+
...props,
|
|
212
|
+
target: "_blank",
|
|
213
|
+
rel: "noopener noreferrer"
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Markdown image: a `data:` image is drawn; any other source becomes a
|
|
218
|
+
* placeholder showing the alt text and source host, linking to the image so
|
|
219
|
+
* the user can open it in the browser.
|
|
220
|
+
*/
|
|
221
|
+
function SafeImage({ node: _node, src, alt, title }) {
|
|
222
|
+
if (src && DATA_IMAGE.test(src)) return /* @__PURE__ */ jsx("img", {
|
|
223
|
+
src,
|
|
224
|
+
alt,
|
|
225
|
+
title
|
|
226
|
+
});
|
|
227
|
+
const label = alt || "image";
|
|
228
|
+
const body = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
229
|
+
/* @__PURE__ */ jsx(ImageOff, {
|
|
230
|
+
size: 14,
|
|
231
|
+
"aria-hidden": true
|
|
232
|
+
}),
|
|
233
|
+
/* @__PURE__ */ jsx("span", { children: label }),
|
|
234
|
+
src ? /* @__PURE__ */ jsxs("span", {
|
|
235
|
+
className: "text-muted-foreground",
|
|
236
|
+
children: [
|
|
237
|
+
"(",
|
|
238
|
+
hostOf(src),
|
|
239
|
+
")"
|
|
240
|
+
]
|
|
241
|
+
}) : null
|
|
242
|
+
] });
|
|
243
|
+
const className = "inline-flex items-center gap-1 rounded border border-border px-1.5 py-0.5 text-xs no-underline";
|
|
244
|
+
return src ? /* @__PURE__ */ jsx("a", {
|
|
245
|
+
href: src,
|
|
246
|
+
title,
|
|
247
|
+
target: "_blank",
|
|
248
|
+
rel: "noopener noreferrer",
|
|
249
|
+
className,
|
|
250
|
+
"data-image-placeholder": "",
|
|
251
|
+
children: body
|
|
252
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
253
|
+
title,
|
|
254
|
+
className,
|
|
255
|
+
"data-image-placeholder": "",
|
|
256
|
+
children: body
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
function MarkdownView({ content, streaming }) {
|
|
260
|
+
return /* @__PURE__ */ jsx("div", {
|
|
261
|
+
className: cn("text-sm leading-relaxed [&_p]:my-2 [&_ul]:my-2 [&_ul]:list-disc [&_ul]:pl-5", "[&_a]:text-primary [&_a]:underline [&_code]:rounded [&_code]:bg-muted [&_code]:px-1 [&_code]:py-0.5 [&_code]:text-xs", "[&_ol]:my-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_table]:my-2 [&_table]:block [&_table]:overflow-x-auto [&_th]:border [&_th]:border-border [&_th]:px-2 [&_td]:border [&_td]:border-border [&_td]:px-2", "[&_pre]:my-2 [&_pre]:overflow-x-auto [&_pre]:rounded-md [&_pre]:bg-muted [&_pre]:p-3 [&_pre>code]:bg-transparent [&_pre>code]:p-0", "[&_img]:inline-block [&_img]:max-w-full"),
|
|
262
|
+
"data-streaming": streaming ? "true" : void 0,
|
|
263
|
+
children: /* @__PURE__ */ jsx(ReactMarkdown, {
|
|
264
|
+
remarkPlugins: [remarkGfm],
|
|
265
|
+
skipHtml: true,
|
|
266
|
+
urlTransform,
|
|
267
|
+
components: {
|
|
268
|
+
a: ExternalLink,
|
|
269
|
+
img: SafeImage
|
|
270
|
+
},
|
|
271
|
+
children: content
|
|
272
|
+
})
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/activity-tiles.tsx
|
|
278
|
+
/** How often a live tile's time advances, in ms. */
|
|
279
|
+
const TILE_TICK_MS = 250;
|
|
280
|
+
const KIND_ICONS = {
|
|
281
|
+
reasoning: lucideIcons.brain,
|
|
282
|
+
plan: lucideIcons.checklist,
|
|
283
|
+
waiting: lucideIcons.spinner,
|
|
284
|
+
other: lucideIcons.gear
|
|
285
|
+
};
|
|
286
|
+
/** The small text inside a tile: a live rate, the time so far or taken, a plan's progress. */
|
|
287
|
+
function tileText(item, now) {
|
|
288
|
+
if (item.detail) return item.detail;
|
|
289
|
+
const rate = formatTokensPerSecond(item.tokensPerSecond);
|
|
290
|
+
if (rate && item.status === "running") return rate.replace(" tok/s", "/s");
|
|
291
|
+
if (item.durationMs !== void 0) return formatDuration(item.durationMs);
|
|
292
|
+
if (item.status === "running" && item.startedAt !== void 0) return formatDuration(Math.max(0, now - item.startedAt));
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
const STATUS_WORDS = {
|
|
296
|
+
running: "running",
|
|
297
|
+
done: "done",
|
|
298
|
+
error: "failed",
|
|
299
|
+
awaiting: "waiting for approval"
|
|
300
|
+
};
|
|
301
|
+
/** A clock that ticks only while `active`. */
|
|
302
|
+
function useNow(active) {
|
|
303
|
+
const [now, setNow] = useState(() => Date.now());
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (!active) return;
|
|
306
|
+
setNow(Date.now());
|
|
307
|
+
const timer = setInterval(() => setNow(Date.now()), TILE_TICK_MS);
|
|
308
|
+
return () => clearInterval(timer);
|
|
309
|
+
}, [active]);
|
|
310
|
+
return now;
|
|
311
|
+
}
|
|
312
|
+
function Tile({ item, now, Icon, onActivate }) {
|
|
313
|
+
const text = tileText(item, now);
|
|
314
|
+
const spinning = item.status === "running" || item.status === "awaiting";
|
|
315
|
+
const label = [
|
|
316
|
+
item.label,
|
|
317
|
+
STATUS_WORDS[item.status],
|
|
318
|
+
text
|
|
319
|
+
].filter(Boolean).join(", ");
|
|
320
|
+
const className = cn("chat-tile relative", TILE, item.status === "done" && "text-foreground/80", item.status === "error" && "bg-destructive/10 text-destructive", item.status === "awaiting" && "text-amber-600", onActivate && "cursor-pointer transition-transform active:scale-95");
|
|
321
|
+
const body = [
|
|
322
|
+
spinning ? /* @__PURE__ */ jsx("span", {
|
|
323
|
+
"aria-hidden": true,
|
|
324
|
+
className: "chat-tile-ring"
|
|
325
|
+
}, "ring") : null,
|
|
326
|
+
/* @__PURE__ */ jsx(Icon, { className: cn(
|
|
327
|
+
TILE_ICON,
|
|
328
|
+
// The waiting kind's icon is a spinner: it turns while it waits.
|
|
329
|
+
item.kind === "waiting" && item.status === "running" && "motion-safe:animate-spin"
|
|
330
|
+
) }, "icon"),
|
|
331
|
+
text ? /* @__PURE__ */ jsx("span", {
|
|
332
|
+
className: "max-w-full truncate px-1",
|
|
333
|
+
children: text
|
|
334
|
+
}, "text") : null
|
|
335
|
+
];
|
|
336
|
+
const data = {
|
|
337
|
+
"data-kind": item.kind,
|
|
338
|
+
"data-status": item.status,
|
|
339
|
+
"aria-label": label
|
|
340
|
+
};
|
|
341
|
+
return onActivate ? /* @__PURE__ */ jsx("button", {
|
|
342
|
+
type: "button",
|
|
343
|
+
className,
|
|
344
|
+
...data,
|
|
345
|
+
onClick: () => onActivate(item.id),
|
|
346
|
+
children: body
|
|
347
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
348
|
+
role: "img",
|
|
349
|
+
className,
|
|
350
|
+
...data,
|
|
351
|
+
children: body
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function createActivityTiles(toolIcons = {}) {
|
|
355
|
+
return function ActivityTiles({ items, live, onActivate }) {
|
|
356
|
+
const now = useNow(live && items.some((item) => item.status === "running"));
|
|
357
|
+
return /* @__PURE__ */ jsx("div", {
|
|
358
|
+
className: "chat-tiles flex flex-wrap gap-1.5",
|
|
359
|
+
"data-live": live ? "true" : void 0,
|
|
360
|
+
children: items.map((item) => /* @__PURE__ */ jsx(Tile, {
|
|
361
|
+
item,
|
|
362
|
+
now,
|
|
363
|
+
Icon: item.kind === "tool" ? (item.name !== void 0 ? toolIcons[item.name] : void 0) ?? lucideIcons.wrench : KIND_ICONS[item.kind],
|
|
364
|
+
onActivate
|
|
365
|
+
}, item.id))
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/ui/drawer.tsx
|
|
372
|
+
const Drawer = ({ shouldScaleBackground = false,...props }) => /* @__PURE__ */ jsx(Drawer$1.Root, {
|
|
373
|
+
shouldScaleBackground,
|
|
374
|
+
...props
|
|
375
|
+
});
|
|
376
|
+
const DrawerPortal = Drawer$1.Portal;
|
|
377
|
+
const DrawerClose = Drawer$1.Close;
|
|
378
|
+
const DrawerOverlay = forwardRef(({ className,...props }, ref) => /* @__PURE__ */ jsx(Drawer$1.Overlay, {
|
|
379
|
+
ref,
|
|
380
|
+
className: cn("fixed inset-0 z-50 bg-black/60", className),
|
|
381
|
+
...props
|
|
382
|
+
}));
|
|
383
|
+
DrawerOverlay.displayName = "DrawerOverlay";
|
|
384
|
+
const DrawerContent = forwardRef(({ className, children,...props }, ref) => /* @__PURE__ */ jsxs(DrawerPortal, { children: [/* @__PURE__ */ jsx(DrawerOverlay, {}), /* @__PURE__ */ jsxs(Drawer$1.Content, {
|
|
385
|
+
ref,
|
|
386
|
+
className: cn("fixed inset-x-0 bottom-0 z-50 flex max-h-[85vh] flex-col rounded-t-xl border-t border-border bg-background text-foreground pb-[env(safe-area-inset-bottom)] outline-none", className),
|
|
387
|
+
...props,
|
|
388
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
389
|
+
"aria-hidden": true,
|
|
390
|
+
className: "mx-auto mt-3 mb-1 h-1.5 w-12 shrink-0 rounded-full bg-muted"
|
|
391
|
+
}), children]
|
|
392
|
+
})] }));
|
|
393
|
+
DrawerContent.displayName = "DrawerContent";
|
|
394
|
+
const DrawerHeader = ({ className,...props }) => /* @__PURE__ */ jsx("div", {
|
|
395
|
+
className: cn("flex flex-col gap-1 px-4 pt-2 pb-3", className),
|
|
396
|
+
...props
|
|
397
|
+
});
|
|
398
|
+
const DrawerTitle = forwardRef(({ className,...props }, ref) => /* @__PURE__ */ jsx(Drawer$1.Title, {
|
|
399
|
+
ref,
|
|
400
|
+
className: cn("text-base font-semibold leading-tight", className),
|
|
401
|
+
...props
|
|
402
|
+
}));
|
|
403
|
+
DrawerTitle.displayName = "DrawerTitle";
|
|
404
|
+
const DrawerDescription = forwardRef(({ className,...props }, ref) => /* @__PURE__ */ jsx(Drawer$1.Description, {
|
|
405
|
+
ref,
|
|
406
|
+
className: cn("text-sm text-muted-foreground", className),
|
|
407
|
+
...props
|
|
408
|
+
}));
|
|
409
|
+
DrawerDescription.displayName = "DrawerDescription";
|
|
410
|
+
|
|
411
|
+
//#endregion
|
|
412
|
+
//#region src/message-details.tsx
|
|
413
|
+
const CopyIcon$1 = lucideIcons.copy;
|
|
414
|
+
const CheckIcon$1 = lucideIcons.check;
|
|
415
|
+
/** How long the copy action shows its check before the sheet closes, in ms. */
|
|
416
|
+
const COPIED_CLOSE_MS = 600;
|
|
417
|
+
const ACTION = "flex h-12 w-full items-center gap-3 rounded-lg px-3 text-left text-[0.9375rem] font-medium text-foreground outline-none active:bg-muted focus-visible:bg-muted cursor-pointer";
|
|
418
|
+
/** Scroll the activity item `id` within `area` into view, if both exist. */
|
|
419
|
+
function scrollToItem(area, id) {
|
|
420
|
+
if (!area || id === void 0) return;
|
|
421
|
+
const items = area.querySelectorAll("[data-block-id]");
|
|
422
|
+
const target = [...items].find((item) => item.getAttribute("data-block-id") === id);
|
|
423
|
+
target?.scrollIntoView({ block: "nearest" });
|
|
424
|
+
}
|
|
425
|
+
function MessageDetailsSheet({ open, onOpenChange, details, outcome, copyText, actions, activity, focusId }) {
|
|
426
|
+
const [copied, setCopied] = useState(false);
|
|
427
|
+
const scroller = useRef(null);
|
|
428
|
+
const timer = useRef(void 0);
|
|
429
|
+
const isOpen = useRef(open);
|
|
430
|
+
isOpen.current = open;
|
|
431
|
+
useEffect(() => () => {
|
|
432
|
+
isOpen.current = false;
|
|
433
|
+
clearTimeout(timer.current);
|
|
434
|
+
}, []);
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
if (open) return;
|
|
437
|
+
clearTimeout(timer.current);
|
|
438
|
+
setCopied(false);
|
|
439
|
+
}, [open]);
|
|
440
|
+
const focus = useRef(focusId);
|
|
441
|
+
focus.current = focusId;
|
|
442
|
+
const attach = useCallback((area) => {
|
|
443
|
+
scroller.current = area;
|
|
444
|
+
scrollToItem(area, focus.current);
|
|
445
|
+
}, []);
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
if (open) scrollToItem(scroller.current, focusId);
|
|
448
|
+
}, [open, focusId]);
|
|
449
|
+
const model = details.find((d) => d.key === "model")?.value;
|
|
450
|
+
const facts = details.filter((d) => d.key !== "model");
|
|
451
|
+
const status = outcome?.kind === "error" ? `Failed: ${outcome.message}` : outcome?.kind === "stopped" ? "Stopped before it finished" : void 0;
|
|
452
|
+
const copy = () => {
|
|
453
|
+
const done = () => {
|
|
454
|
+
if (!isOpen.current) return;
|
|
455
|
+
setCopied(true);
|
|
456
|
+
clearTimeout(timer.current);
|
|
457
|
+
timer.current = setTimeout(() => onOpenChange(false), COPIED_CLOSE_MS);
|
|
458
|
+
};
|
|
459
|
+
const clip = navigator.clipboard;
|
|
460
|
+
if (clip?.writeText) clip.writeText(copyText).then(done, done);
|
|
461
|
+
else done();
|
|
462
|
+
};
|
|
463
|
+
return /* @__PURE__ */ jsx(Drawer, {
|
|
464
|
+
open,
|
|
465
|
+
onOpenChange,
|
|
466
|
+
children: /* @__PURE__ */ jsxs(DrawerContent, { children: [
|
|
467
|
+
/* @__PURE__ */ jsxs(DrawerHeader, { children: [/* @__PURE__ */ jsx(DrawerTitle, {
|
|
468
|
+
className: "truncate",
|
|
469
|
+
children: model ?? "Message"
|
|
470
|
+
}), /* @__PURE__ */ jsx(DrawerDescription, {
|
|
471
|
+
className: cn(!status && "sr-only", outcome?.kind === "error" && "text-destructive"),
|
|
472
|
+
children: status ?? "Details and actions for this message"
|
|
473
|
+
})] }),
|
|
474
|
+
/* @__PURE__ */ jsxs("div", {
|
|
475
|
+
ref: attach,
|
|
476
|
+
className: "min-h-0 flex-1 overflow-y-auto",
|
|
477
|
+
children: [facts.length > 0 ? /* @__PURE__ */ jsx("dl", {
|
|
478
|
+
className: "mx-4 mb-2 rounded-lg bg-muted/60 px-4 text-sm",
|
|
479
|
+
children: facts.map((d) => /* @__PURE__ */ jsxs("div", {
|
|
480
|
+
"data-detail": d.key,
|
|
481
|
+
className: "flex items-baseline justify-between gap-4 border-b border-border/60 py-2.5 last:border-0",
|
|
482
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
483
|
+
className: "text-muted-foreground",
|
|
484
|
+
children: d.label
|
|
485
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
486
|
+
className: "m-0 min-w-0 text-right tabular-nums [overflow-wrap:anywhere]",
|
|
487
|
+
children: d.value
|
|
488
|
+
})]
|
|
489
|
+
}, d.key))
|
|
490
|
+
}) : null, activity ? /* @__PURE__ */ jsxs("section", {
|
|
491
|
+
className: "chat-details-activity-section mx-4 mb-2",
|
|
492
|
+
"aria-label": "Activity",
|
|
493
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
494
|
+
className: "mt-2 mb-1 text-xs font-medium uppercase tracking-wider text-muted-foreground",
|
|
495
|
+
children: "Activity"
|
|
496
|
+
}), activity]
|
|
497
|
+
}) : null]
|
|
498
|
+
}),
|
|
499
|
+
/* @__PURE__ */ jsxs("div", {
|
|
500
|
+
className: "flex shrink-0 flex-col border-t border-border/60 px-2 pt-1 pb-3 empty:hidden",
|
|
501
|
+
children: [copyText ? /* @__PURE__ */ jsxs("button", {
|
|
502
|
+
type: "button",
|
|
503
|
+
className: ACTION,
|
|
504
|
+
onClick: copy,
|
|
505
|
+
children: [copied ? /* @__PURE__ */ jsx(CheckIcon$1, { size: 18 }) : /* @__PURE__ */ jsx(CopyIcon$1, { size: 18 }), copied ? "Copied" : "Copy text"]
|
|
506
|
+
}) : null, actions.map((action) => /* @__PURE__ */ jsxs("button", {
|
|
507
|
+
type: "button",
|
|
508
|
+
"data-action": action.id,
|
|
509
|
+
className: ACTION,
|
|
510
|
+
onClick: () => {
|
|
511
|
+
onOpenChange(false);
|
|
512
|
+
action.onSelect();
|
|
513
|
+
},
|
|
514
|
+
children: [/* @__PURE__ */ jsx(action.icon, { size: 18 }), action.label]
|
|
515
|
+
}, action.id))]
|
|
516
|
+
})
|
|
517
|
+
] })
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/ui/avatar.tsx
|
|
523
|
+
const Avatar = forwardRef(({ className,...props }, ref) => /* @__PURE__ */ jsx(AvatarPrimitive.Root, {
|
|
524
|
+
ref,
|
|
525
|
+
className: cn("relative flex h-7 w-7 shrink-0 overflow-hidden rounded-full", className),
|
|
526
|
+
...props
|
|
527
|
+
}));
|
|
528
|
+
Avatar.displayName = "Avatar";
|
|
529
|
+
const AvatarFallback = forwardRef(({ className,...props }, ref) => /* @__PURE__ */ jsx(AvatarPrimitive.Fallback, {
|
|
530
|
+
ref,
|
|
531
|
+
className: cn("flex h-full w-full items-center justify-center rounded-full bg-muted text-xs font-medium", className),
|
|
532
|
+
...props
|
|
533
|
+
}));
|
|
534
|
+
AvatarFallback.displayName = "AvatarFallback";
|
|
535
|
+
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/ui/badge.tsx
|
|
538
|
+
const badgeVariants = cva("inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors", {
|
|
539
|
+
variants: { variant: {
|
|
540
|
+
default: "border-transparent bg-secondary text-secondary-foreground",
|
|
541
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground",
|
|
542
|
+
success: "border-transparent bg-emerald-500/15 text-emerald-700 dark:text-emerald-400"
|
|
543
|
+
} },
|
|
544
|
+
defaultVariants: { variant: "default" }
|
|
545
|
+
});
|
|
546
|
+
function Badge({ className, variant,...props }) {
|
|
547
|
+
return /* @__PURE__ */ jsx("span", {
|
|
548
|
+
className: cn(badgeVariants({ variant }), className),
|
|
549
|
+
...props
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
//#endregion
|
|
554
|
+
//#region src/ui/button.tsx
|
|
555
|
+
const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", {
|
|
556
|
+
variants: {
|
|
557
|
+
variant: {
|
|
558
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
559
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
560
|
+
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground"
|
|
561
|
+
},
|
|
562
|
+
size: {
|
|
563
|
+
default: "h-9 px-4 py-2",
|
|
564
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
565
|
+
icon: "h-9 w-9",
|
|
566
|
+
"icon-sm": "h-7 w-7"
|
|
567
|
+
}
|
|
568
|
+
},
|
|
569
|
+
defaultVariants: {
|
|
570
|
+
variant: "default",
|
|
571
|
+
size: "default"
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
function Button({ className, variant, size, type,...props }) {
|
|
575
|
+
return /* @__PURE__ */ jsx("button", {
|
|
576
|
+
type: type ?? "button",
|
|
577
|
+
className: cn(buttonVariants({
|
|
578
|
+
variant,
|
|
579
|
+
size
|
|
580
|
+
}), className),
|
|
581
|
+
...props
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/ui/collapsible.tsx
|
|
587
|
+
const Collapsible = CollapsiblePrimitive.Root;
|
|
588
|
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
|
589
|
+
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
|
590
|
+
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/ui/scroll-area.tsx
|
|
593
|
+
const ScrollArea = forwardRef(({ className, children,...props }, ref) => /* @__PURE__ */ jsxs(ScrollAreaPrimitive.Root, {
|
|
594
|
+
ref,
|
|
595
|
+
className: cn("relative overflow-hidden", className),
|
|
596
|
+
...props,
|
|
597
|
+
children: [
|
|
598
|
+
/* @__PURE__ */ jsx(ScrollAreaPrimitive.Viewport, {
|
|
599
|
+
className: "h-full w-full rounded-[inherit]",
|
|
600
|
+
children
|
|
601
|
+
}),
|
|
602
|
+
/* @__PURE__ */ jsx(ScrollAreaPrimitive.Scrollbar, {
|
|
603
|
+
orientation: "vertical",
|
|
604
|
+
className: "flex touch-none select-none transition-colors w-2 p-0.5",
|
|
605
|
+
children: /* @__PURE__ */ jsx(ScrollAreaPrimitive.Thumb, { className: "relative flex-1 rounded-full bg-border" })
|
|
606
|
+
}),
|
|
607
|
+
/* @__PURE__ */ jsx(ScrollAreaPrimitive.Corner, {})
|
|
608
|
+
]
|
|
609
|
+
}));
|
|
610
|
+
ScrollArea.displayName = "ScrollArea";
|
|
611
|
+
|
|
612
|
+
//#endregion
|
|
613
|
+
//#region src/ui/tooltip.tsx
|
|
614
|
+
const TooltipProvider = TooltipPrimitive.Provider;
|
|
615
|
+
const Tooltip = TooltipPrimitive.Root;
|
|
616
|
+
const TooltipTrigger = TooltipPrimitive.Trigger;
|
|
617
|
+
const TooltipContent = forwardRef(({ className, sideOffset = 4,...props }, ref) => /* @__PURE__ */ jsx(TooltipPrimitive.Portal, { children: /* @__PURE__ */ jsx(TooltipPrimitive.Content, {
|
|
618
|
+
ref,
|
|
619
|
+
sideOffset,
|
|
620
|
+
className: cn("z-50 overflow-hidden rounded-md border bg-popover px-2.5 py-1 text-xs text-popover-foreground shadow-md", className),
|
|
621
|
+
...props
|
|
622
|
+
}) }));
|
|
623
|
+
TooltipContent.displayName = "TooltipContent";
|
|
624
|
+
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/registry.tsx
|
|
627
|
+
function toneVariant(tone) {
|
|
628
|
+
if (tone === "error") return "destructive";
|
|
629
|
+
if (tone === "success") return "success";
|
|
630
|
+
return "default";
|
|
631
|
+
}
|
|
632
|
+
const Caret = lucideIcons.caretRight;
|
|
633
|
+
const CaretDown = lucideIcons.caretDown;
|
|
634
|
+
const CheckIcon = lucideIcons.check;
|
|
635
|
+
const CopyIcon = lucideIcons.copy;
|
|
636
|
+
/**
|
|
637
|
+
* `icon-sm` is 28×28 — below the 44×44 minimum touch target. Icon-only
|
|
638
|
+
* buttons get a 44×44 tappable area; the glyph stays small. The width is not
|
|
639
|
+
* fixed: a button the package gives a label (a revealed footer action) grows
|
|
640
|
+
* to fit it instead of spilling over its neighbours.
|
|
641
|
+
*/
|
|
642
|
+
const ICON_TOUCH = "min-h-11 min-w-11 w-auto px-2";
|
|
643
|
+
/** Build the shadcn registry, tuned by `options`. */
|
|
644
|
+
function createShadcnComponents({ tooltipDelay = 150, copyResetMs = 1500, touch = false, toolIcons } = {}) {
|
|
645
|
+
const triggerClass = cn("flex w-full cursor-pointer items-center gap-1.5 text-left text-sm", !touch && "hover:text-foreground/80");
|
|
646
|
+
return {
|
|
647
|
+
Button: ({ onClick, disabled, className, children, "aria-label": ariaLabel }) => /* @__PURE__ */ jsx(Button, {
|
|
648
|
+
variant: "ghost",
|
|
649
|
+
size: "icon-sm",
|
|
650
|
+
onClick,
|
|
651
|
+
disabled,
|
|
652
|
+
"aria-label": ariaLabel,
|
|
653
|
+
className: cn(ICON_TOUCH, className),
|
|
654
|
+
children
|
|
655
|
+
}),
|
|
656
|
+
Badge: ({ tone, className, children }) => /* @__PURE__ */ jsx(Badge, {
|
|
657
|
+
variant: toneVariant(tone),
|
|
658
|
+
className,
|
|
659
|
+
children
|
|
660
|
+
}),
|
|
661
|
+
Tooltip: ({ content, className, children }) => /* @__PURE__ */ jsx(TooltipProvider, {
|
|
662
|
+
delayDuration: tooltipDelay,
|
|
663
|
+
children: /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
664
|
+
asChild: true,
|
|
665
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
666
|
+
className,
|
|
667
|
+
children
|
|
668
|
+
})
|
|
669
|
+
}), /* @__PURE__ */ jsx(TooltipContent, { children: content })] })
|
|
670
|
+
}),
|
|
671
|
+
Collapsible: ({ open, defaultOpen, onOpenChange, header, className, children }) => {
|
|
672
|
+
const controlled = open !== void 0;
|
|
673
|
+
const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);
|
|
674
|
+
const isOpen = controlled ? open : internalOpen;
|
|
675
|
+
const ChevronIcon = isOpen ? CaretDown : Caret;
|
|
676
|
+
return /* @__PURE__ */ jsxs(Collapsible, {
|
|
677
|
+
open: isOpen,
|
|
678
|
+
onOpenChange: (next) => {
|
|
679
|
+
if (!controlled) setInternalOpen(next);
|
|
680
|
+
onOpenChange?.(next);
|
|
681
|
+
},
|
|
682
|
+
className,
|
|
683
|
+
children: [/* @__PURE__ */ jsxs(CollapsibleTrigger, {
|
|
684
|
+
className: triggerClass,
|
|
685
|
+
children: [/* @__PURE__ */ jsx(ChevronIcon, {
|
|
686
|
+
size: 14,
|
|
687
|
+
className: "shrink-0 text-muted-foreground"
|
|
688
|
+
}), header]
|
|
689
|
+
}), /* @__PURE__ */ jsx(CollapsibleContent, { children })]
|
|
690
|
+
});
|
|
691
|
+
},
|
|
692
|
+
CodeBlock,
|
|
693
|
+
JsonView,
|
|
694
|
+
ScrollArea: ({ className, children }) => /* @__PURE__ */ jsx(ScrollArea, {
|
|
695
|
+
className,
|
|
696
|
+
children
|
|
697
|
+
}),
|
|
698
|
+
Avatar: ({ role, className }) => /* @__PURE__ */ jsx(Avatar, {
|
|
699
|
+
className,
|
|
700
|
+
"aria-label": role,
|
|
701
|
+
children: /* @__PURE__ */ jsx(AvatarFallback, {
|
|
702
|
+
"data-role": role,
|
|
703
|
+
children: role.charAt(0).toUpperCase()
|
|
704
|
+
})
|
|
705
|
+
}),
|
|
706
|
+
CopyButton: ({ value, className, "aria-label": ariaLabel }) => {
|
|
707
|
+
const [copied, setCopied] = useState(false);
|
|
708
|
+
const reset = useRef(void 0);
|
|
709
|
+
useEffect(() => () => clearTimeout(reset.current), []);
|
|
710
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
711
|
+
variant: "ghost",
|
|
712
|
+
size: "icon-sm",
|
|
713
|
+
"aria-label": ariaLabel ?? "Copy",
|
|
714
|
+
"data-copied": copied ? "true" : void 0,
|
|
715
|
+
className: cn(ICON_TOUCH, className),
|
|
716
|
+
onClick: () => {
|
|
717
|
+
const done = () => {
|
|
718
|
+
setCopied(true);
|
|
719
|
+
clearTimeout(reset.current);
|
|
720
|
+
if (copyResetMs > 0) reset.current = setTimeout(() => setCopied(false), copyResetMs);
|
|
721
|
+
};
|
|
722
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
723
|
+
if (clip?.writeText) clip.writeText(value).then(done, done);
|
|
724
|
+
else done();
|
|
725
|
+
},
|
|
726
|
+
children: copied ? /* @__PURE__ */ jsx(CheckIcon, { size: 14 }) : /* @__PURE__ */ jsx(CopyIcon, { size: 14 })
|
|
727
|
+
});
|
|
728
|
+
},
|
|
729
|
+
Markdown: MarkdownView,
|
|
730
|
+
...touch ? {
|
|
731
|
+
MessageDetails: MessageDetailsSheet,
|
|
732
|
+
ActivitySummary: createActivityTiles(toolIcons)
|
|
733
|
+
} : {},
|
|
734
|
+
icons: lucideIcons
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
/** The shadcn registry with default (pointer-first) tuning. */
|
|
738
|
+
const shadcnComponents = createShadcnComponents();
|
|
739
|
+
|
|
740
|
+
//#endregion
|
|
741
|
+
export { Avatar, AvatarFallback, Badge, Button, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, JsonView, MarkdownView, MessageDetailsSheet, ScrollArea, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, badgeVariants, buttonVariants, cn, createActivityTiles, createShadcnClassNames, createShadcnComponents, lucideIcons, shadcnClassNames, shadcnComponents, tileText };
|
package/package.json
CHANGED
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fx-/chat-shadcn",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A ready-made shadcn/ui look for @fx-/chat: component registry, classNames theme and Tailwind v4 design tokens.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"**/*.css"
|
|
9
|
+
],
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"src/theme.css"
|
|
13
|
+
],
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./theme.css": "./src/theme.css"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsdown",
|
|
23
|
+
"typecheck": "tsc --build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@radix-ui/react-avatar": "1.1.2",
|
|
27
|
+
"@radix-ui/react-collapsible": "1.1.2",
|
|
28
|
+
"@radix-ui/react-dialog": "1.2.0",
|
|
29
|
+
"@radix-ui/react-scroll-area": "1.2.2",
|
|
30
|
+
"@radix-ui/react-tooltip": "1.1.6",
|
|
31
|
+
"class-variance-authority": "0.7.1",
|
|
32
|
+
"clsx": "2.1.1",
|
|
33
|
+
"lucide-react": "0.469.0",
|
|
34
|
+
"react-markdown": "9.0.3",
|
|
35
|
+
"remark-gfm": "4.0.0",
|
|
36
|
+
"tailwind-merge": "2.6.0",
|
|
37
|
+
"vaul": "1.1.2"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@fx-/chat": "0.0.0",
|
|
41
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
42
|
+
"react-dom": "^18.2.0 || ^19.0.0",
|
|
43
|
+
"tailwindcss": "^4.0.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@fx-/chat": "0.0.0"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"provenance": true
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "git+https://github.com/fx/chat.git",
|
|
55
|
+
"directory": "packages/shadcn"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/fx/chat#readme",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/fx/chat/issues"
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/theme.css
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@fx-/chat-shadcn/theme.css` — the shadcn design tokens and the Tailwind v4
|
|
3
|
+
* wiring for `@fx-/chat-shadcn`.
|
|
4
|
+
*
|
|
5
|
+
* Import it after Tailwind, alongside `@fx-/chat`'s structural CSS:
|
|
6
|
+
*
|
|
7
|
+
* @import "tailwindcss";
|
|
8
|
+
* @import "@fx-/chat-shadcn/theme.css";
|
|
9
|
+
*
|
|
10
|
+
* Dark mode follows the system by default. A `.dark` or `.light` class on any
|
|
11
|
+
* element forces that scheme for its subtree (so a page can offer a toggle or
|
|
12
|
+
* show both side by side). Tokens are HSL channel triplets, surfaced to
|
|
13
|
+
* Tailwind via `@theme inline` so `bg-primary`, `text-muted-foreground`, …
|
|
14
|
+
* resolve; override any `--token` to re-skin.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Tailwind v4 does not scan `node_modules`; generate this package's utilities
|
|
19
|
+
* from its built bundle (the path resolves relative to this file).
|
|
20
|
+
*/
|
|
21
|
+
@source "../dist";
|
|
22
|
+
|
|
23
|
+
@custom-variant dark {
|
|
24
|
+
&:where(.dark, .dark *) {
|
|
25
|
+
@slot;
|
|
26
|
+
}
|
|
27
|
+
@media (prefers-color-scheme: dark) {
|
|
28
|
+
&:where(:not(.light, .light *)) {
|
|
29
|
+
@slot;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:root,
|
|
35
|
+
.light {
|
|
36
|
+
--background: 0 0% 100%;
|
|
37
|
+
--foreground: 240 10% 3.9%;
|
|
38
|
+
--card: 0 0% 100%;
|
|
39
|
+
--card-foreground: 240 10% 3.9%;
|
|
40
|
+
--popover: 0 0% 100%;
|
|
41
|
+
--popover-foreground: 240 10% 3.9%;
|
|
42
|
+
--primary: 240 5.9% 10%;
|
|
43
|
+
--primary-foreground: 0 0% 98%;
|
|
44
|
+
--secondary: 240 4.8% 95.9%;
|
|
45
|
+
--secondary-foreground: 240 5.9% 10%;
|
|
46
|
+
--muted: 240 4.8% 95.9%;
|
|
47
|
+
--muted-foreground: 240 3.8% 42%;
|
|
48
|
+
--accent: 240 4.8% 95.9%;
|
|
49
|
+
--accent-foreground: 240 5.9% 10%;
|
|
50
|
+
--destructive: 0 72% 45%;
|
|
51
|
+
--destructive-foreground: 0 0% 98%;
|
|
52
|
+
--border: 240 5.9% 90%;
|
|
53
|
+
--input: 240 5.9% 90%;
|
|
54
|
+
--ring: 240 5.9% 10%;
|
|
55
|
+
--radius: 0.5rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.dark {
|
|
59
|
+
--background: 240 10% 3.9%;
|
|
60
|
+
--foreground: 0 0% 98%;
|
|
61
|
+
--card: 240 10% 5.9%;
|
|
62
|
+
--card-foreground: 0 0% 98%;
|
|
63
|
+
--popover: 240 10% 3.9%;
|
|
64
|
+
--popover-foreground: 0 0% 98%;
|
|
65
|
+
--primary: 0 0% 98%;
|
|
66
|
+
--primary-foreground: 240 5.9% 10%;
|
|
67
|
+
--secondary: 240 3.7% 15.9%;
|
|
68
|
+
--secondary-foreground: 0 0% 98%;
|
|
69
|
+
--muted: 240 3.7% 15.9%;
|
|
70
|
+
--muted-foreground: 240 5% 70%;
|
|
71
|
+
--accent: 240 3.7% 15.9%;
|
|
72
|
+
--accent-foreground: 0 0% 98%;
|
|
73
|
+
--destructive: 0 62.8% 50%;
|
|
74
|
+
--destructive-foreground: 0 0% 98%;
|
|
75
|
+
--border: 240 3.7% 15.9%;
|
|
76
|
+
--input: 240 3.7% 15.9%;
|
|
77
|
+
--ring: 240 4.9% 83.9%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* The same dark values, applied to the page when the system asks for dark. */
|
|
81
|
+
@media (prefers-color-scheme: dark) {
|
|
82
|
+
:root:not(.light) {
|
|
83
|
+
--background: 240 10% 3.9%;
|
|
84
|
+
--foreground: 0 0% 98%;
|
|
85
|
+
--card: 240 10% 5.9%;
|
|
86
|
+
--card-foreground: 0 0% 98%;
|
|
87
|
+
--popover: 240 10% 3.9%;
|
|
88
|
+
--popover-foreground: 0 0% 98%;
|
|
89
|
+
--primary: 0 0% 98%;
|
|
90
|
+
--primary-foreground: 240 5.9% 10%;
|
|
91
|
+
--secondary: 240 3.7% 15.9%;
|
|
92
|
+
--secondary-foreground: 0 0% 98%;
|
|
93
|
+
--muted: 240 3.7% 15.9%;
|
|
94
|
+
--muted-foreground: 240 5% 70%;
|
|
95
|
+
--accent: 240 3.7% 15.9%;
|
|
96
|
+
--accent-foreground: 0 0% 98%;
|
|
97
|
+
--destructive: 0 62.8% 50%;
|
|
98
|
+
--destructive-foreground: 0 0% 98%;
|
|
99
|
+
--border: 240 3.7% 15.9%;
|
|
100
|
+
--input: 240 3.7% 15.9%;
|
|
101
|
+
--ring: 240 4.9% 83.9%;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@theme inline {
|
|
106
|
+
--color-background: hsl(var(--background));
|
|
107
|
+
--color-foreground: hsl(var(--foreground));
|
|
108
|
+
--color-card: hsl(var(--card));
|
|
109
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
110
|
+
--color-popover: hsl(var(--popover));
|
|
111
|
+
--color-popover-foreground: hsl(var(--popover-foreground));
|
|
112
|
+
--color-primary: hsl(var(--primary));
|
|
113
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
114
|
+
--color-secondary: hsl(var(--secondary));
|
|
115
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
116
|
+
--color-muted: hsl(var(--muted));
|
|
117
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
118
|
+
--color-accent: hsl(var(--accent));
|
|
119
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
120
|
+
--color-destructive: hsl(var(--destructive));
|
|
121
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
122
|
+
--color-border: hsl(var(--border));
|
|
123
|
+
--color-input: hsl(var(--input));
|
|
124
|
+
--color-ring: hsl(var(--ring));
|
|
125
|
+
--radius-md: var(--radius);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* Activity tiles (`ActivitySummary`): a live tile's ring is a 2px outline
|
|
130
|
+
* (a mask following the tile's rounded corners) through which a conic
|
|
131
|
+
* gradient sweeps. The sweep turns by `transform`, so the compositor animates
|
|
132
|
+
* it without per-frame style work. Reduced motion gets a still ring.
|
|
133
|
+
*/
|
|
134
|
+
@keyframes chat-tile-spin {
|
|
135
|
+
to {
|
|
136
|
+
transform: rotate(1turn);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@keyframes chat-tile-in {
|
|
141
|
+
from {
|
|
142
|
+
opacity: 0;
|
|
143
|
+
transform: scale(0.6);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.chat-tile {
|
|
148
|
+
animation: chat-tile-in 0.18s ease-out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.chat-tile-ring {
|
|
152
|
+
--chat-tile-ring: var(--primary);
|
|
153
|
+
position: absolute;
|
|
154
|
+
inset: 0;
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
border-radius: inherit;
|
|
157
|
+
padding: 2px;
|
|
158
|
+
pointer-events: none;
|
|
159
|
+
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
160
|
+
-webkit-mask-composite: xor;
|
|
161
|
+
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
|
162
|
+
mask-composite: exclude;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.chat-tile-ring::before {
|
|
166
|
+
content: "";
|
|
167
|
+
position: absolute;
|
|
168
|
+
inset: -50%;
|
|
169
|
+
background: conic-gradient(transparent 0deg 200deg, hsl(var(--chat-tile-ring)) 360deg);
|
|
170
|
+
animation: chat-tile-spin 1.1s linear infinite;
|
|
171
|
+
will-change: transform;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.chat-tile[data-status="awaiting"] .chat-tile-ring {
|
|
175
|
+
--chat-tile-ring: 38 92% 50%;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.chat-tile[data-status="awaiting"] .chat-tile-ring::before {
|
|
179
|
+
animation-duration: 2.4s;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@media (prefers-reduced-motion: reduce) {
|
|
183
|
+
.chat-tile {
|
|
184
|
+
animation: none;
|
|
185
|
+
}
|
|
186
|
+
.chat-tile-ring::before {
|
|
187
|
+
animation: none;
|
|
188
|
+
background: hsl(var(--chat-tile-ring) / 0.6);
|
|
189
|
+
}
|
|
190
|
+
}
|
package/README.md
DELETED