harebear-ui 0.8.1 → 0.10.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.
Files changed (83) hide show
  1. package/AGENTS.md +2 -2
  2. package/dist/components/admin/app-nav.d.ts +15 -1
  3. package/dist/components/admin/app-nav.js +28 -11
  4. package/dist/components/admin/avatar-upload.d.ts +32 -0
  5. package/dist/components/admin/avatar-upload.js +121 -0
  6. package/dist/components/admin/category.d.ts +17 -0
  7. package/dist/components/admin/category.js +35 -0
  8. package/dist/components/admin/celebration.d.ts +7 -0
  9. package/dist/components/admin/celebration.js +61 -0
  10. package/dist/components/admin/checklist-editor.d.ts +20 -0
  11. package/dist/components/admin/checklist-editor.js +69 -0
  12. package/dist/components/admin/color-swatch-picker.d.ts +15 -0
  13. package/dist/components/admin/color-swatch-picker.js +68 -0
  14. package/dist/components/admin/comment-thread.d.ts +26 -0
  15. package/dist/components/admin/comment-thread.js +68 -0
  16. package/dist/components/admin/copy-field.d.ts +3 -1
  17. package/dist/components/admin/copy-field.js +10 -5
  18. package/dist/components/admin/date-time-picker.d.ts +20 -0
  19. package/dist/components/admin/date-time-picker.js +97 -0
  20. package/dist/components/admin/detail-sheet.d.ts +18 -0
  21. package/dist/components/admin/detail-sheet.js +80 -0
  22. package/dist/components/admin/markdown-editor.d.ts +12 -0
  23. package/dist/components/admin/markdown-editor.js +46 -0
  24. package/dist/components/admin/page-header.d.ts +3 -1
  25. package/dist/components/admin/page-header.js +17 -9
  26. package/dist/components/admin/page.d.ts +16 -0
  27. package/dist/components/admin/page.js +42 -0
  28. package/dist/components/admin/prompt-dialog.d.ts +19 -0
  29. package/dist/components/admin/prompt-dialog.js +83 -0
  30. package/dist/components/admin/settings.d.ts +4 -2
  31. package/dist/components/admin/settings.js +37 -30
  32. package/dist/components/admin/time-zone-picker.d.ts +12 -0
  33. package/dist/components/admin/time-zone-picker.js +31 -0
  34. package/dist/components/admin/user-chip.d.ts +34 -3
  35. package/dist/components/admin/user-chip.js +66 -7
  36. package/dist/components/admin/user-menu.d.ts +6 -1
  37. package/dist/components/admin/user-menu.js +22 -10
  38. package/dist/components/ai/ask-box.d.ts +14 -2
  39. package/dist/components/ai/ask-box.js +90 -54
  40. package/dist/components/ai/assistant.d.ts +13 -3
  41. package/dist/components/ai/assistant.js +36 -8
  42. package/dist/components/ai/code-block.d.ts +10 -3
  43. package/dist/components/ai/code-block.js +57 -8
  44. package/dist/components/ai/conversation-list.d.ts +13 -3
  45. package/dist/components/ai/conversation-list.js +15 -26
  46. package/dist/components/ai/markdown.d.ts +13 -0
  47. package/dist/components/ai/markdown.js +41 -0
  48. package/dist/components/auth/auth.d.ts +41 -5
  49. package/dist/components/auth/auth.js +167 -159
  50. package/dist/components/data/agenda-row.d.ts +26 -0
  51. package/dist/components/data/agenda-row.js +62 -0
  52. package/dist/components/data/day-strip.d.ts +27 -0
  53. package/dist/components/data/day-strip.js +78 -0
  54. package/dist/components/data/sectioned-list.d.ts +21 -6
  55. package/dist/components/data/sectioned-list.js +44 -21
  56. package/dist/components/data/task-row.d.ts +3 -0
  57. package/dist/components/data/task-row.js +6 -14
  58. package/dist/components/layout/collapse.js +1 -1
  59. package/dist/components/layout/text.d.ts +3 -3
  60. package/dist/components/layout/text.js +6 -1
  61. package/dist/components/ui/avatar.js +1 -1
  62. package/dist/components/ui/button.js +1 -1
  63. package/dist/components/ui/check-circle.js +1 -1
  64. package/dist/components/ui/code-block.js +2 -2
  65. package/dist/components/ui/combobox.js +1 -0
  66. package/dist/components/ui/input-group.js +1 -1
  67. package/dist/components/ui/input.js +1 -1
  68. package/dist/components/ui/kbd.js +1 -1
  69. package/dist/components/ui/password-input.d.ts +8 -0
  70. package/dist/components/ui/password-input.js +32 -0
  71. package/dist/components/ui/sonner.d.ts +2 -2
  72. package/dist/components/ui/sonner.js +1 -1
  73. package/dist/components/ui/toggle.js +1 -1
  74. package/dist/index.d.ts +23 -7
  75. package/dist/index.js +21 -5
  76. package/dist/lib/internal/date.js +8 -0
  77. package/dist/lib/{toast-clearance.js → internal/toast-clearance.js} +1 -1
  78. package/dist/styles.css +31 -1
  79. package/docs/COMPONENTS.md +302 -28
  80. package/docs/ROADMAP.md +13 -11
  81. package/llms.txt +10 -8
  82. package/package.json +3 -1
  83. package/skills/harebear-ui/SKILL.md +5 -3
@@ -29,8 +29,8 @@ function SettingsSection({ title, description, className, children, ...props })
29
29
  })]
30
30
  });
31
31
  }
32
- /** One setting: label and description at the left, a control at the right, optional expandable content. */
33
- function SettingsRow({ label, description, htmlFor, control, content, open: openProp, defaultOpen = false, onOpenChange, expandLabel = "Change", className, ...props }) {
32
+ /** One setting: label and description at the left, a control at the right, optional body and expandable content. */
33
+ function SettingsRow({ label, description, htmlFor, control, body, content, open: openProp, defaultOpen = false, onOpenChange, expandLabel = "Change", className, ...props }) {
34
34
  const [openState, setOpenState] = React.useState(defaultOpen);
35
35
  const open = openProp ?? openState;
36
36
  const setOpen = (next) => {
@@ -44,35 +44,42 @@ function SettingsRow({ label, description, htmlFor, control, content, open: open
44
44
  "data-state": content ? open ? "open" : "closed" : void 0,
45
45
  className: cn("py-3.5", className),
46
46
  ...props,
47
- children: [/* @__PURE__ */ jsxs("div", {
48
- className: "flex flex-wrap items-center gap-x-4 gap-y-2",
49
- children: [/* @__PURE__ */ jsxs("div", {
50
- className: "flex min-w-0 flex-1 basis-48 flex-col",
51
- children: [/* @__PURE__ */ jsx(Label, {
52
- htmlFor,
53
- className: "text-sm font-medium",
54
- children: label
55
- }), description && /* @__PURE__ */ jsx("span", {
56
- className: "text-[13px] text-muted-foreground",
57
- children: description
58
- })]
59
- }), control ?? (content ? /* @__PURE__ */ jsx(Button, {
60
- type: "button",
61
- variant: "outline",
62
- size: "sm",
63
- "aria-expanded": open,
64
- "aria-controls": contentId,
65
- onClick: () => setOpen(!open),
66
- children: open ? "Cancel" : expandLabel
67
- }) : null)]
68
- }), content && /* @__PURE__ */ jsx(Collapse, {
69
- open,
70
- id: contentId,
71
- children: /* @__PURE__ */ jsx("div", {
72
- className: "pt-3.5",
73
- children: content
47
+ children: [
48
+ /* @__PURE__ */ jsxs("div", {
49
+ className: "flex flex-wrap items-center gap-x-4 gap-y-2",
50
+ children: [/* @__PURE__ */ jsxs("div", {
51
+ className: "flex min-w-0 flex-1 basis-48 flex-col",
52
+ children: [/* @__PURE__ */ jsx(Label, {
53
+ htmlFor,
54
+ className: "text-sm font-medium",
55
+ children: label
56
+ }), description && /* @__PURE__ */ jsx("span", {
57
+ className: "text-[13px] text-muted-foreground",
58
+ children: description
59
+ })]
60
+ }), control ?? (content ? /* @__PURE__ */ jsx(Button, {
61
+ type: "button",
62
+ variant: "outline",
63
+ size: "sm",
64
+ "aria-expanded": open,
65
+ "aria-controls": contentId,
66
+ onClick: () => setOpen(!open),
67
+ children: open ? "Cancel" : expandLabel
68
+ }) : null)]
69
+ }),
70
+ body && /* @__PURE__ */ jsx("div", {
71
+ className: "pt-3",
72
+ children: body
73
+ }),
74
+ content && /* @__PURE__ */ jsx(Collapse, {
75
+ open,
76
+ id: contentId,
77
+ children: /* @__PURE__ */ jsx("div", {
78
+ className: "pt-3.5",
79
+ children: content
80
+ })
74
81
  })
75
- })]
82
+ ]
76
83
  });
77
84
  }
78
85
  export { SettingsRow, SettingsSection };
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ export interface TimeZonePickerProps {
3
+ /** IANA name, e.g. "America/Vancouver". */
4
+ value: string;
5
+ onValueChange: (value: string) => void;
6
+ /** id of the search input, for a FieldLabel or SettingsRow `htmlFor`. */
7
+ id?: string;
8
+ className?: string;
9
+ }
10
+ /** Searchable IANA time zone list (Intl.supportedValuesOf("timeZone")), built on Combobox. */
11
+ declare function TimeZonePicker({ value, onValueChange, id, className }: TimeZonePickerProps): React.JSX.Element;
12
+ export { TimeZonePicker };
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "../ui/combobox.js";
3
+ import { cn } from "../../lib/utils.js";
4
+ import * as React from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ // src/components/admin/time-zone-picker.tsx
7
+ const label = (zone) => zone.replaceAll("_", " ");
8
+ /** Searchable IANA time zone list (Intl.supportedValuesOf("timeZone")), built on Combobox. */
9
+ function TimeZonePicker({ value, onValueChange, id, className }) {
10
+ const zones = React.useMemo(() => {
11
+ const all = Intl.supportedValuesOf("timeZone");
12
+ return value && !all.includes(value) ? [value, ...all] : all;
13
+ }, [value]);
14
+ return /* @__PURE__ */ jsxs(Combobox, {
15
+ items: zones,
16
+ value,
17
+ onValueChange: (next) => next && onValueChange(next),
18
+ itemToStringLabel: label,
19
+ children: [/* @__PURE__ */ jsx(ComboboxInput, {
20
+ id,
21
+ placeholder: "Search time zones",
22
+ showTrigger: false,
23
+ className: cn("w-full", className)
24
+ }), /* @__PURE__ */ jsxs(ComboboxContent, { children: [/* @__PURE__ */ jsx(ComboboxEmpty, { children: "No matching time zone." }), /* @__PURE__ */ jsx(ComboboxList, { children: (zone) => /* @__PURE__ */ jsx(ComboboxItem, {
25
+ value: zone,
26
+ className: "pointer-coarse:min-h-11",
27
+ children: label(zone)
28
+ }, zone) })] })]
29
+ });
30
+ }
31
+ export { TimeZonePicker };
@@ -1,14 +1,45 @@
1
+ import { CategoryColor } from "./category.js";
2
+ import { Avatar } from "../ui/avatar.js";
1
3
  import * as React from "react";
2
4
  /** Up to two initials from a name: "Maya Chen" -> "MC". */
3
5
  export declare function initials(name: string): string;
4
- export interface UserChipProps extends React.ComponentProps<"div"> {
6
+ export interface MemberAvatarProps extends Omit<React.ComponentProps<typeof Avatar>, "children" | "color"> {
7
+ name: string;
8
+ avatarUrl?: string;
9
+ /** Category colour behind the initials when there is no photo. Default: the soft primary tint. */
10
+ color?: CategoryColor;
11
+ }
12
+ /**
13
+ * A person's photo, or their initials on their category colour. It is an image named after the person; pass
14
+ * `aria-hidden` when the name is already shown next to it.
15
+ */
16
+ declare function MemberAvatar({ name, avatarUrl, color, ...props }: MemberAvatarProps): React.JSX.Element;
17
+ export interface AvatarStackProps extends React.ComponentProps<"div"> {
18
+ members: Pick<MemberAvatarProps, "name" | "avatarUrl" | "color">[];
19
+ /** Total slots, default 4. Past it, `max - 1` faces and a "+N" chip. */
20
+ max?: number;
21
+ }
22
+ /** Overlapping small avatars, e.g. who a list is shared with. Each face is named; the overflow reads "N more". */
23
+ declare function AvatarStack({ members, max, className, ...props }: AvatarStackProps): React.JSX.Element;
24
+ export interface VisibilityBadgeProps extends React.ComponentProps<"div"> {
25
+ visibility: "shared" | "private";
26
+ /** Faces shown when shared. */
27
+ members?: AvatarStackProps["members"];
28
+ /** Default "Shared with everyone" or "Only you". */
29
+ label?: React.ReactNode;
30
+ }
31
+ /** Who can see something: faces and "Shared with everyone", or a lock and "Only you". */
32
+ declare function VisibilityBadge({ visibility, members, label, className, ...props }: VisibilityBadgeProps): React.JSX.Element;
33
+ export interface UserChipProps extends Omit<React.ComponentProps<"div">, "color"> {
5
34
  name: string;
6
35
  subtitle?: React.ReactNode;
7
36
  avatarUrl?: string;
37
+ /** Category colour behind the initials when there is no photo. */
38
+ color?: CategoryColor;
8
39
  /** Trailing control, e.g. a menu button. */
9
40
  action?: React.ReactNode;
10
41
  size?: "sm" | "default" | "lg";
11
42
  }
12
43
  /** Avatar + name + secondary line. In a sidebar collapsed to its icon rail, only the avatar shows. */
13
- declare function UserChip({ name, subtitle, avatarUrl, action, size, className, ...props }: UserChipProps): React.JSX.Element;
14
- export { UserChip };
44
+ declare function UserChip({ name, subtitle, avatarUrl, color, action, size, className, ...props }: UserChipProps): React.JSX.Element;
45
+ export { AvatarStack, MemberAvatar, UserChip, VisibilityBadge };
@@ -1,25 +1,84 @@
1
1
  "use client";
2
+ import { categoryBg } from "./category.js";
2
3
  import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar.js";
3
4
  import { cn } from "../../lib/utils.js";
4
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { LockIcon } from "lucide-react";
5
7
  // src/components/admin/user-chip.tsx
6
8
  /** Up to two initials from a name: "Maya Chen" -> "MC". */
7
9
  function initials(name) {
8
10
  return name.split(/\s+/).filter(Boolean).slice(0, 2).map((p) => p[0]?.toUpperCase()).join("");
9
11
  }
12
+ /**
13
+ * A person's photo, or their initials on their category colour. It is an image named after the person; pass
14
+ * `aria-hidden` when the name is already shown next to it.
15
+ */
16
+ function MemberAvatar({ name, avatarUrl, color, ...props }) {
17
+ return /* @__PURE__ */ jsxs(Avatar, {
18
+ role: "img",
19
+ "aria-label": name,
20
+ "data-color": color,
21
+ ...props,
22
+ children: [avatarUrl && /* @__PURE__ */ jsx(AvatarImage, {
23
+ src: avatarUrl,
24
+ alt: ""
25
+ }), /* @__PURE__ */ jsx(AvatarFallback, {
26
+ "aria-hidden": "true",
27
+ className: cn(color && [categoryBg[color], "text-category-foreground"]),
28
+ children: initials(name)
29
+ })]
30
+ });
31
+ }
32
+ /** Overlapping small avatars, e.g. who a list is shared with. Each face is named; the overflow reads "N more". */
33
+ function AvatarStack({ members, max = 4, className, ...props }) {
34
+ const shown = members.length > max ? members.slice(0, max - 1) : members;
35
+ const extra = members.length - shown.length;
36
+ return /* @__PURE__ */ jsxs("div", {
37
+ "data-slot": "avatar-stack",
38
+ className: cn("flex -space-x-1.5", className),
39
+ ...props,
40
+ children: [shown.map((m, i) => /* @__PURE__ */ jsx(MemberAvatar, {
41
+ size: "sm",
42
+ ...m,
43
+ className: "ring-2 ring-background"
44
+ }, `${m.name}-${i}`)), extra > 0 && /* @__PURE__ */ jsxs("span", {
45
+ role: "img",
46
+ "aria-label": `${extra} more`,
47
+ className: "relative flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-semibold text-muted-foreground ring-2 ring-background",
48
+ children: ["+", extra]
49
+ })]
50
+ });
51
+ }
52
+ /** Who can see something: faces and "Shared with everyone", or a lock and "Only you". */
53
+ function VisibilityBadge({ visibility, members, label, className, ...props }) {
54
+ const shared = visibility === "shared";
55
+ return /* @__PURE__ */ jsxs("div", {
56
+ "data-slot": "visibility-badge",
57
+ "data-visibility": visibility,
58
+ className: cn("inline-flex items-center gap-2 text-[13px] text-muted-foreground", className),
59
+ ...props,
60
+ children: [shared ? members?.length ? /* @__PURE__ */ jsx(AvatarStack, {
61
+ members,
62
+ "aria-hidden": "true"
63
+ }) : null : /* @__PURE__ */ jsx(LockIcon, {
64
+ "aria-hidden": "true",
65
+ className: "size-4 shrink-0"
66
+ }), /* @__PURE__ */ jsx("span", { children: label ?? (shared ? "Shared with everyone" : "Only you") })]
67
+ });
68
+ }
10
69
  /** Avatar + name + secondary line. In a sidebar collapsed to its icon rail, only the avatar shows. */
11
- function UserChip({ name, subtitle, avatarUrl, action, size = "default", className, ...props }) {
70
+ function UserChip({ name, subtitle, avatarUrl, color, action, size = "default", className, ...props }) {
12
71
  return /* @__PURE__ */ jsxs("div", {
13
72
  "data-slot": "user-chip",
14
73
  className: cn("flex min-w-0 items-center gap-3 group-data-[collapsible=icon]:justify-center", className),
15
74
  ...props,
16
75
  children: [
17
- /* @__PURE__ */ jsxs(Avatar, {
76
+ /* @__PURE__ */ jsx(MemberAvatar, {
77
+ name,
78
+ avatarUrl,
79
+ color,
18
80
  size,
19
- children: [avatarUrl && /* @__PURE__ */ jsx(AvatarImage, {
20
- src: avatarUrl,
21
- alt: ""
22
- }), /* @__PURE__ */ jsx(AvatarFallback, { children: initials(name) })]
81
+ "aria-hidden": "true"
23
82
  }),
24
83
  /* @__PURE__ */ jsxs("div", {
25
84
  className: "min-w-0 flex-1 leading-snug group-data-[collapsible=icon]:hidden",
@@ -38,4 +97,4 @@ function UserChip({ name, subtitle, avatarUrl, action, size = "default", classNa
38
97
  ]
39
98
  });
40
99
  }
41
- export { UserChip, initials };
100
+ export { AvatarStack, MemberAvatar, UserChip, VisibilityBadge, initials };
@@ -1,9 +1,12 @@
1
+ import { CategoryColor } from "./category.js";
1
2
  import * as React from "react";
2
3
  export interface UserMenuProps {
3
4
  name: string;
4
5
  /** Second line, e.g. the email or "Signed in". */
5
6
  subtitle?: React.ReactNode;
6
7
  avatarUrl?: string;
8
+ /** Category colour behind the initials when there is no photo. */
9
+ color?: CategoryColor;
7
10
  /** Extra items between the header and the theme choice, e.g. a Settings DropdownMenuItem. */
8
11
  children?: React.ReactNode;
9
12
  /** Show the Light / Dark / System choice. Default true. Needs ThemeProvider. */
@@ -12,11 +15,13 @@ export interface UserMenuProps {
12
15
  onSignOut?: () => void;
13
16
  /** Default "Sign out". */
14
17
  signOutLabel?: React.ReactNode;
18
+ /** Trigger is just the avatar (44px on touch screens) and the menu opens below it, e.g. in a phone page header. */
19
+ compact?: boolean;
15
20
  className?: string;
16
21
  }
17
22
  /**
18
23
  * The signed-in user as a menu button, for a sidebar footer or top bar: theme choice, app items such as
19
24
  * Settings, and sign out. In a sidebar icon rail the trigger is just the avatar and the menu opens to the side.
20
25
  */
21
- declare function UserMenu({ name, subtitle, avatarUrl, children, themeSwitch, onSignOut, signOutLabel, className }: UserMenuProps): React.JSX.Element;
26
+ declare function UserMenu({ name, subtitle, avatarUrl, color, children, themeSwitch, onSignOut, signOutLabel, compact, className }: UserMenuProps): React.JSX.Element;
22
27
  export { UserMenu };
@@ -1,6 +1,8 @@
1
1
  "use client";
2
- import { UserChip } from "./user-chip.js";
2
+ import { categoryBg } from "./category.js";
3
+ import { UserChip, initials } from "./user-chip.js";
3
4
  import { useTheme } from "../theme/theme-provider.js";
5
+ import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar.js";
4
6
  import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
5
7
  import { useSidebarOptional } from "../ui/sidebar.js";
6
8
  import { cn } from "../../lib/utils.js";
@@ -28,25 +30,35 @@ const themes = [
28
30
  * The signed-in user as a menu button, for a sidebar footer or top bar: theme choice, app items such as
29
31
  * Settings, and sign out. In a sidebar icon rail the trigger is just the avatar and the menu opens to the side.
30
32
  */
31
- function UserMenu({ name, subtitle, avatarUrl, children, themeSwitch = true, onSignOut, signOutLabel = "Sign out", className }) {
33
+ function UserMenu({ name, subtitle, avatarUrl, color, children, themeSwitch = true, onSignOut, signOutLabel = "Sign out", compact = false, className }) {
32
34
  const { mode, setMode } = useTheme();
33
35
  const sidebar = useSidebarOptional();
34
36
  const rail = sidebar?.state === "collapsed" && !sidebar.isMobile;
35
- return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsxs(DropdownMenuTrigger, {
37
+ return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
36
38
  "data-slot": "user-menu-trigger",
37
- "aria-label": `Account menu for ${name}`,
38
- className: cn("flex w-full min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring/40 data-[state=open]:bg-muted group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:size-10 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:rounded-full group-data-[collapsible=icon]:p-0", className),
39
- children: [/* @__PURE__ */ jsx(UserChip, {
39
+ "aria-label": compact ? `Account menu for ${name}` : rail ? name : void 0,
40
+ className: cn(compact ? "grid size-10 shrink-0 place-items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background pointer-coarse:size-11" : "flex w-full min-w-0 items-center gap-2 rounded-lg px-2 py-1.5 text-left transition-colors outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring data-[state=open]:bg-muted group-data-[collapsible=icon]:mx-auto group-data-[collapsible=icon]:size-10 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:rounded-full group-data-[collapsible=icon]:p-0", className),
41
+ children: compact || rail ? /* @__PURE__ */ jsxs(Avatar, {
42
+ "aria-hidden": "true",
43
+ children: [avatarUrl && /* @__PURE__ */ jsx(AvatarImage, {
44
+ src: avatarUrl,
45
+ alt: ""
46
+ }), /* @__PURE__ */ jsx(AvatarFallback, {
47
+ "data-initials": initials(name),
48
+ className: cn("after:content-[attr(data-initials)]", color && [categoryBg[color], "text-category-foreground"])
49
+ })]
50
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(UserChip, {
40
51
  name,
41
52
  subtitle,
42
53
  avatarUrl,
54
+ color,
43
55
  className: "flex-1"
44
- }), /* @__PURE__ */ jsx(ChevronsUpDownIcon, { className: "size-4 shrink-0 text-muted-foreground group-data-[collapsible=icon]:hidden" })]
56
+ }), /* @__PURE__ */ jsx(ChevronsUpDownIcon, { className: "size-4 shrink-0 text-muted-foreground group-data-[collapsible=icon]:hidden" })] })
45
57
  }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
46
58
  "data-slot": "user-menu",
47
- side: rail ? "right" : "top",
48
- align: rail ? "end" : "start",
49
- className: "w-(--radix-dropdown-menu-trigger-width) min-w-60",
59
+ side: compact ? "bottom" : rail ? "right" : "top",
60
+ align: compact || rail ? "end" : "start",
61
+ className: compact ? "min-w-60" : "w-(--radix-dropdown-menu-trigger-width) min-w-60",
50
62
  children: [
51
63
  /* @__PURE__ */ jsxs(DropdownMenuLabel, {
52
64
  className: "flex min-w-0 flex-col font-normal",
@@ -18,10 +18,22 @@ export interface AskBoxProps extends Omit<React.ComponentProps<"form">, "onSubmi
18
18
  lang?: string;
19
19
  disabled?: boolean;
20
20
  autoFocus?: boolean;
21
+ /** The assistant is working: sending is blocked and, with `onStop`, the send button becomes Stop. */
22
+ pending?: boolean;
23
+ /** Stops the assistant. Shown as a Stop button while `pending`. */
24
+ onStop?: () => void;
25
+ /**
26
+ * The answer, shown under the box (text, Prose, ActionReceipts). Passing it, even `null`, turns the reply area on:
27
+ * while `pending` with no answer yet it shows a typing indicator. `useAssistant().reply` fits here.
28
+ */
29
+ reply?: React.ReactNode;
30
+ /** Link under the reply to continue in the full chat, labelled "Open chat". */
31
+ chatHref?: string;
21
32
  }
22
33
  /**
23
34
  * The large rounded input an assistant starts from: auto-growing text, chips while typing, suggestion pills and the
24
- * DictationButton (mic, stop, send). The focus ring is on the whole box.
35
+ * DictationButton (mic, stop, send). The focus ring is on the whole box. With `reply` the answer shows under the box;
36
+ * with `pending` and `onStop` the send button becomes Stop.
25
37
  */
26
- declare function AskBox({ value, onValueChange, onSubmit, placeholder, chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, className, ...props }: AskBoxProps): React.JSX.Element;
38
+ declare function AskBox({ value, onValueChange, onSubmit, placeholder, chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, pending, onStop, reply, chatHref, className, ...props }: AskBoxProps): React.JSX.Element;
27
39
  export { AskBox };
@@ -1,9 +1,11 @@
1
1
  "use client";
2
+ import { TypingIndicator } from "./chat.js";
2
3
  import { DictationButton } from "./dictation.js";
3
4
  import { Button } from "../ui/button.js";
4
5
  import { cn } from "../../lib/utils.js";
5
6
  import * as React from "react";
6
7
  import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { ArrowRightIcon, SquareIcon } from "lucide-react";
7
9
  // src/components/ai/ask-box.tsx
8
10
  const noSpeech = {
9
11
  isSupported: () => false,
@@ -11,15 +13,20 @@ const noSpeech = {
11
13
  };
12
14
  /**
13
15
  * The large rounded input an assistant starts from: auto-growing text, chips while typing, suggestion pills and the
14
- * DictationButton (mic, stop, send). The focus ring is on the whole box.
16
+ * DictationButton (mic, stop, send). The focus ring is on the whole box. With `reply` the answer shows under the box;
17
+ * with `pending` and `onStop` the send button becomes Stop.
15
18
  */
16
- function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything", chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, className, ...props }) {
19
+ function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything", chips, suggestions, onSuggestion, dictation, lang, disabled, autoFocus, pending, onStop, reply, chatHref, className, ...props }) {
17
20
  const id = React.useId();
18
21
  const submit = () => {
19
22
  const text = value.trim();
20
- if (text && !disabled) onSubmit(text);
23
+ if (!text || disabled || pending) return;
24
+ onSubmit(text);
25
+ document.getElementById(id)?.focus();
21
26
  };
22
27
  const hasText = value.trim().length > 0;
28
+ const hasReply = reply != null && reply !== false;
29
+ const showReply = hasReply || pending && reply !== void 0;
23
30
  return /* @__PURE__ */ jsxs("form", {
24
31
  "data-slot": "ask-box",
25
32
  className: cn("flex w-full min-w-0 flex-col gap-3", className),
@@ -28,58 +35,87 @@ function AskBox({ value, onValueChange, onSubmit, placeholder = "Ask anything",
28
35
  submit();
29
36
  },
30
37
  ...props,
31
- children: [/* @__PURE__ */ jsxs("div", {
32
- className: "focus-box flex flex-col gap-2 rounded-3xl border border-transparent bg-muted py-2.5 pr-2.5 pl-5",
33
- children: [/* @__PURE__ */ jsxs("div", {
34
- className: "flex items-end gap-2",
35
- children: [
36
- /* @__PURE__ */ jsx("label", {
37
- htmlFor: id,
38
- className: "sr-only",
39
- children: placeholder
40
- }),
41
- /* @__PURE__ */ jsx("textarea", {
42
- id,
43
- rows: 1,
44
- value,
45
- disabled,
46
- autoFocus,
47
- placeholder,
48
- onChange: (e) => onValueChange(e.target.value),
49
- onKeyDown: (e) => {
50
- if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
51
- e.preventDefault();
52
- submit();
53
- }
54
- },
55
- className: "field-sizing-content max-h-48 min-h-10 min-w-0 flex-1 resize-none border-0 bg-transparent py-2 text-base leading-6 text-foreground outline-none placeholder:text-muted-foreground"
56
- }),
57
- /* @__PURE__ */ jsx(DictationButton, {
58
- value,
59
- disabled,
60
- source: dictation === false ? noSpeech : dictation,
61
- lang,
62
- onTranscript: (text) => onValueChange(value ? `${value.replace(/\s+$/, "")} ${text}` : text)
38
+ children: [
39
+ /* @__PURE__ */ jsxs("div", {
40
+ className: "focus-box flex flex-col gap-2 rounded-3xl border border-transparent bg-muted py-2.5 pr-2.5 pl-5",
41
+ children: [/* @__PURE__ */ jsxs("div", {
42
+ className: "flex items-end gap-2",
43
+ children: [
44
+ /* @__PURE__ */ jsx("label", {
45
+ htmlFor: id,
46
+ className: "sr-only",
47
+ children: placeholder
48
+ }),
49
+ /* @__PURE__ */ jsx("textarea", {
50
+ id,
51
+ rows: 1,
52
+ value,
53
+ disabled,
54
+ autoFocus,
55
+ placeholder,
56
+ onChange: (e) => onValueChange(e.target.value),
57
+ onKeyDown: (e) => {
58
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
59
+ e.preventDefault();
60
+ submit();
61
+ }
62
+ },
63
+ className: "field-sizing-content max-h-48 min-h-10 min-w-0 flex-1 resize-none border-0 bg-transparent py-2 text-base leading-6 text-foreground outline-none placeholder:text-muted-foreground"
64
+ }),
65
+ pending && onStop ? /* @__PURE__ */ jsx(Button, {
66
+ type: "button",
67
+ size: "icon",
68
+ shape: "pill",
69
+ className: "size-10 shrink-0",
70
+ "aria-label": "Stop",
71
+ onClick: () => {
72
+ onStop();
73
+ document.getElementById(id)?.focus();
74
+ },
75
+ children: /* @__PURE__ */ jsx(SquareIcon, { className: "size-3.5 fill-current" })
76
+ }) : /* @__PURE__ */ jsx(DictationButton, {
77
+ value,
78
+ disabled: disabled || pending,
79
+ source: dictation === false ? noSpeech : dictation,
80
+ lang,
81
+ onTranscript: (text) => onValueChange(value ? `${value.replace(/\s+$/, "")} ${text}` : text)
82
+ })
83
+ ]
84
+ }), hasText && chips && /* @__PURE__ */ jsx("div", {
85
+ "data-slot": "ask-box-chips",
86
+ className: "flex flex-wrap gap-1.5 pb-1",
87
+ children: chips
88
+ })]
89
+ }),
90
+ showReply && /* @__PURE__ */ jsxs("div", {
91
+ "data-slot": "ask-box-reply",
92
+ "aria-live": "polite",
93
+ "aria-busy": pending || void 0,
94
+ className: "flex min-w-0 flex-col gap-3 px-2 text-[15px] leading-relaxed",
95
+ children: [hasReply ? reply : /* @__PURE__ */ jsx(TypingIndicator, { label: "Working" }), chatHref && /* @__PURE__ */ jsx(Button, {
96
+ asChild: true,
97
+ variant: "link",
98
+ className: "self-start px-0 max-md:min-h-11",
99
+ children: /* @__PURE__ */ jsxs("a", {
100
+ href: chatHref,
101
+ children: ["Open chat", /* @__PURE__ */ jsx(ArrowRightIcon, {})]
63
102
  })
64
- ]
65
- }), hasText && chips && /* @__PURE__ */ jsx("div", {
66
- "data-slot": "ask-box-chips",
67
- className: "flex flex-wrap gap-1.5 pb-1",
68
- children: chips
69
- })]
70
- }), suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx("div", {
71
- "data-slot": "ask-box-suggestions",
72
- className: "flex flex-wrap gap-2",
73
- children: suggestions.map((s) => /* @__PURE__ */ jsx(Button, {
74
- type: "button",
75
- variant: "outline",
76
- size: "sm",
77
- shape: "pill",
78
- disabled,
79
- onClick: () => onSuggestion ? onSuggestion(s) : onSubmit(s),
80
- children: s
81
- }, s))
82
- })]
103
+ })]
104
+ }),
105
+ suggestions && suggestions.length > 0 && /* @__PURE__ */ jsx("div", {
106
+ "data-slot": "ask-box-suggestions",
107
+ className: "flex flex-wrap gap-2",
108
+ children: suggestions.map((s) => /* @__PURE__ */ jsx(Button, {
109
+ type: "button",
110
+ variant: "outline",
111
+ size: "sm",
112
+ shape: "pill",
113
+ disabled,
114
+ onClick: () => onSuggestion ? onSuggestion(s) : onSubmit(s),
115
+ children: s
116
+ }, s))
117
+ })
118
+ ]
83
119
  });
84
120
  }
85
121
  export { AskBox };
@@ -25,6 +25,12 @@ export interface AssistantSendContext {
25
25
  }) => string;
26
26
  /** Replace a message's content, e.g. while a reply streams in. */
27
27
  update: (id: string, content: React.ReactNode) => void;
28
+ /** Aborted when the user presses Stop. Pass it to `fetch`, or listen for `abort` to tell your server to stop. */
29
+ signal: AbortSignal;
30
+ }
31
+ export interface AssistantSendOptions {
32
+ /** Open the dock. Default true; false for an AskBox that shows the answer inline. */
33
+ open?: boolean;
28
34
  }
29
35
  export interface AssistantContextValue {
30
36
  open: boolean;
@@ -33,10 +39,14 @@ export interface AssistantContextValue {
33
39
  messages: AssistantMessage[];
34
40
  /** True while the send handler runs. */
35
41
  pending: boolean;
42
+ /** The assistant messages after the latest user message, ready for `AskBox` `reply`; null until there is one. */
43
+ reply: React.ReactNode;
36
44
  page: AssistantPage | null;
37
45
  setPage: (page: AssistantPage | null) => void;
38
- /** Adds the user message, opens the dock and runs the app's onSend. */
39
- send: (text: string) => Promise<void>;
46
+ /** Adds the user message, opens the dock (unless `{ open: false }`) and runs the app's onSend. */
47
+ send: (text: string, options?: AssistantSendOptions) => Promise<void>;
48
+ /** Stops the running send: aborts its `signal`, ends `pending` and drops its return value and later `append`/`update` calls. */
49
+ stop: () => void;
40
50
  append: AssistantSendContext["append"];
41
51
  update: AssistantSendContext["update"];
42
52
  clear: () => void;
@@ -44,7 +54,7 @@ export interface AssistantContextValue {
44
54
  export interface AssistantProviderProps {
45
55
  /**
46
56
  * The app's send handler. Return the reply (text or nodes) to append it, or call `append` / `update` yourself
47
- * (for streaming or receipts) and return nothing. A thrown error is shown as the reply.
57
+ * (for streaming or receipts) and return nothing. A thrown error is shown as the reply. Stop aborts `signal`.
48
58
  */
49
59
  onSend: (text: string, context: AssistantSendContext) => React.ReactNode | Promise<React.ReactNode>;
50
60
  initialMessages?: AssistantMessage[];