@vvedantb/ui 0.17.1 → 0.18.1

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 (130) hide show
  1. package/README.md +251 -3
  2. package/package.json +13 -5
  3. package/shell/src/SidebarResize.tsx +1 -4
  4. package/src/ai-elements/__snapshots__/queue.test.tsx.snap +11 -0
  5. package/src/ai-elements/activity-question-utils.ts +26 -0
  6. package/src/ai-elements/activity-question.tsx +159 -0
  7. package/src/ai-elements/activity-shared.tsx +459 -0
  8. package/src/ai-elements/activity-step-detail.tsx +115 -0
  9. package/src/ai-elements/activity-step-label.ts +981 -0
  10. package/src/ai-elements/activity-tasks-utils.ts +199 -0
  11. package/src/ai-elements/activity-tasks.tsx +770 -0
  12. package/src/ai-elements/artifact.tsx +162 -52
  13. package/src/ai-elements/attachments.tsx +62 -32
  14. package/src/ai-elements/code-block.tsx +76 -22
  15. package/src/ai-elements/context.tsx +370 -0
  16. package/src/ai-elements/conversation.tsx +186 -62
  17. package/src/ai-elements/file-chip.tsx +53 -0
  18. package/src/ai-elements/message.tsx +161 -50
  19. package/src/ai-elements/model-picker-content.tsx +505 -0
  20. package/src/ai-elements/model-picker-types.ts +56 -0
  21. package/src/ai-elements/model-picker.tsx +301 -0
  22. package/src/ai-elements/model-selector.tsx +102 -21
  23. package/src/ai-elements/plan.tsx +130 -0
  24. package/src/ai-elements/prompt-input-settings.tsx +144 -0
  25. package/src/ai-elements/prompt-input-speech.tsx +189 -0
  26. package/src/ai-elements/prompt-input-traits.tsx +281 -0
  27. package/src/ai-elements/prompt-input.tsx +320 -134
  28. package/src/ai-elements/provider-icon.tsx +133 -0
  29. package/src/ai-elements/queue.tsx +293 -9
  30. package/src/ai-elements/shimmer.tsx +72 -1
  31. package/src/ai-elements/simple-model-ladder.tsx +108 -0
  32. package/src/ai-elements/speech-input.tsx +5 -3
  33. package/src/ai-elements/test-results.tsx +510 -0
  34. package/src/ai-elements/trace.tsx +243 -0
  35. package/src/ai-elements/use-streaming-disclosure.ts +50 -0
  36. package/src/ai-elements/web-preview.tsx +349 -0
  37. package/src/composer/input-area/ComposerSlashMenu.tsx +1 -1
  38. package/src/index.ts +66 -12
  39. package/src/kibo/avatar-stack.tsx +40 -0
  40. package/src/kibo/contribution-graph.tsx +539 -0
  41. package/src/kibo/cursor.tsx +72 -0
  42. package/src/kibo/gantt-features.tsx +439 -0
  43. package/src/kibo/gantt-header.tsx +181 -0
  44. package/src/kibo/gantt-provider.tsx +657 -0
  45. package/src/kibo/gantt-sidebar.tsx +157 -0
  46. package/src/kibo/gantt-timeline.tsx +154 -0
  47. package/src/kibo/index.ts +151 -0
  48. package/src/kibo/kanban.tsx +338 -0
  49. package/src/kibo/kanbanDropTarget.ts +25 -0
  50. package/src/kibo/list.tsx +162 -0
  51. package/src/kibo/theme-switcher.tsx +109 -0
  52. package/src/kibo/video-player.tsx +237 -0
  53. package/src/markdown/Markdown.tsx +112 -0
  54. package/src/markdown/MarkdownBlockquote.tsx +45 -0
  55. package/src/markdown/MarkdownCodeBlock.tsx +197 -0
  56. package/src/markdown/MarkdownInlineCode.tsx +40 -0
  57. package/src/markdown/MarkdownLink.tsx +92 -0
  58. package/src/markdown/codeFence.ts +57 -0
  59. package/src/markdown/codeHighlight.ts +138 -0
  60. package/src/markdown/incrementalBlocks.ts +182 -0
  61. package/src/markdown/index.ts +3 -0
  62. package/src/markdown/markdownClipboard.ts +178 -0
  63. package/src/markdown/mermaid.ts +155 -0
  64. package/src/markdown/rehypeGithubAlerts.ts +80 -0
  65. package/src/motion/presets.ts +24 -0
  66. package/src/ui/__snapshots__/alt-wrappers.test.tsx.snap +19 -0
  67. package/src/ui/__snapshots__/sonner-dom.test.tsx.snap +5 -0
  68. package/src/ui/_menu-parts.tsx +29 -0
  69. package/src/ui/accordion.alt.tsx +46 -53
  70. package/src/ui/accordion.tsx +59 -10
  71. package/src/ui/avatar.tsx +56 -0
  72. package/src/ui/badge.tsx +45 -21
  73. package/src/ui/border-beam.tsx +97 -0
  74. package/src/ui/button-group.tsx +50 -21
  75. package/src/ui/button.tsx +67 -45
  76. package/src/ui/calendar.tsx +48 -5
  77. package/src/ui/card.tsx +77 -8
  78. package/src/ui/carousel.tsx +60 -0
  79. package/src/ui/centered-spinner.tsx +30 -0
  80. package/src/ui/checkbox.tsx +34 -16
  81. package/src/ui/clear-input.alt.tsx +33 -75
  82. package/src/ui/clear-input.tsx +140 -15
  83. package/src/ui/collapsible.tsx +40 -2
  84. package/src/ui/command.tsx +143 -70
  85. package/src/ui/context-menu.tsx +159 -93
  86. package/src/ui/crossfade-icon.tsx +123 -0
  87. package/src/ui/dialog.tsx +154 -70
  88. package/src/ui/dropdown-menu.tsx +177 -117
  89. package/src/ui/hover-card.tsx +59 -17
  90. package/src/ui/input-group.alt.tsx +83 -83
  91. package/src/ui/input-group.tsx +129 -58
  92. package/src/ui/input.tsx +13 -4
  93. package/src/ui/label.tsx +11 -8
  94. package/src/ui/list-row.tsx +232 -0
  95. package/src/ui/loading-state.tsx +215 -0
  96. package/src/ui/popover.tsx +56 -23
  97. package/src/ui/progress.tsx +28 -16
  98. package/src/ui/refresh-spin-icon.tsx +41 -0
  99. package/src/ui/search-input.tsx +66 -15
  100. package/src/ui/select.tsx +171 -100
  101. package/src/ui/separator.tsx +23 -14
  102. package/src/ui/slider.tsx +21 -7
  103. package/src/ui/sonner.tsx +68 -30
  104. package/src/ui/spinner.tsx +229 -11
  105. package/src/ui/status-dot.tsx +84 -0
  106. package/src/ui/surface.tsx +49 -0
  107. package/src/ui/switch.tsx +29 -15
  108. package/src/ui/table.tsx +38 -16
  109. package/src/ui/tabs.tsx +123 -35
  110. package/src/ui/tabsSliding.ts +33 -1
  111. package/src/ui/textarea.tsx +13 -4
  112. package/src/ui/tooltip.tsx +50 -14
  113. package/src/ui/ui-config.tsx +149 -0
  114. package/src/utils/cat-color.ts +123 -0
  115. package/src/utils/dayjs.ts +6 -0
  116. package/src/utils/gesture.ts +59 -0
  117. package/src/utils/runtimeVisibility.ts +167 -0
  118. package/src/utils/sharedClock.ts +96 -0
  119. package/src/utils/surface-radius.ts +16 -0
  120. package/src/utils/useDragSensors.ts +58 -0
  121. package/tokens/examples/tokens-moved/Fixture.tsx +283 -0
  122. package/tokens/examples/tokens-moved/README.md +20 -0
  123. package/tokens/examples/tokens-moved/index.html +12 -0
  124. package/tokens/examples/tokens-moved/main.tsx +6 -0
  125. package/tokens/examples/tokens-moved/package.json +14 -0
  126. package/tokens/examples/tokens-moved/styles.css +12 -0
  127. package/tokens/examples/tokens-moved/tsconfig.json +14 -0
  128. package/tokens/examples/tokens-moved/vite.config.ts +11 -0
  129. package/tokens/src/css/border-beam.css +116 -0
  130. package/tokens/src/css/markdown.css +318 -0
@@ -0,0 +1,133 @@
1
+ import type { ComponentProps } from "react";
2
+ import { useCn } from "../ui/ui-config";
3
+
4
+ interface ProviderIconProps extends Omit<ComponentProps<"svg">, "children"> {
5
+ provider: string;
6
+ size?: number;
7
+ }
8
+
9
+ /**
10
+ * Renders the brand icon for an AI provider.
11
+ * Supports "claude" (Claude sparkle mark), "codex" (OpenAI mark), "opencode" (OpenCode mark), and "cursor" (Cursor mark).
12
+ */
13
+ export function ProviderIcon({
14
+ provider,
15
+ size = 14,
16
+ className,
17
+ ...props
18
+ }: ProviderIconProps) {
19
+ const shared = { size, className, ...props };
20
+ switch (provider) {
21
+ case "claude":
22
+ return <ClaudeMark {...shared} />;
23
+ case "codex":
24
+ return <OpenAIMark {...shared} />;
25
+ case "opencode":
26
+ return <OpencodeMark {...shared} />;
27
+ case "cursor":
28
+ return <CursorMark {...shared} />;
29
+ default:
30
+ return <ClaudeMark {...shared} />;
31
+ }
32
+ }
33
+
34
+ function ClaudeMark({
35
+ size = 14,
36
+ className,
37
+ ...props
38
+ }: Omit<ProviderIconProps, "provider">) {
39
+ const merge = useCn();
40
+ return (
41
+ <svg
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ width={size}
44
+ height={size}
45
+ viewBox="0 0 24 24"
46
+ fill="#D97757"
47
+ className={merge("shrink-0", className)}
48
+ aria-hidden="true"
49
+ {...props}
50
+ >
51
+ <path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z" />
52
+ </svg>
53
+ );
54
+ }
55
+
56
+ /** OpenAI mark — path from svgl `openai.svg` / `openai_dark.svg` (current B&W brand). */
57
+ function OpenAIMark({
58
+ size = 14,
59
+ className,
60
+ ...props
61
+ }: Omit<ProviderIconProps, "provider">) {
62
+ const merge = useCn();
63
+ return (
64
+ <svg
65
+ xmlns="http://www.w3.org/2000/svg"
66
+ width={size}
67
+ height={size}
68
+ viewBox="0 0 256 260"
69
+ className={merge("shrink-0 text-black dark:text-white", className)}
70
+ aria-hidden="true"
71
+ {...props}
72
+ >
73
+ <path
74
+ fill="currentColor"
75
+ d="M239.184 106.203a64.716 64.716 0 0 0-5.576-53.103C219.452 28.459 191 15.784 163.213 21.74A65.586 65.586 0 0 0 52.096 45.22a64.716 64.716 0 0 0-43.23 31.36c-14.31 24.602-11.061 55.634 8.033 76.74a64.665 64.665 0 0 0 5.525 53.102c14.174 24.65 42.644 37.324 70.446 31.36a64.72 64.72 0 0 0 48.754 21.744c28.481.025 53.714-18.361 62.414-45.481a64.767 64.767 0 0 0 43.229-31.36c14.137-24.558 10.875-55.423-8.083-76.483Zm-97.56 136.338a48.397 48.397 0 0 1-31.105-11.255l1.535-.87 51.67-29.825a8.595 8.595 0 0 0 4.247-7.367v-72.85l21.845 12.636c.218.111.37.32.409.563v60.367c-.056 26.818-21.783 48.545-48.601 48.601Zm-104.466-44.61a48.345 48.345 0 0 1-5.781-32.589l1.534.921 51.722 29.826a8.339 8.339 0 0 0 8.441 0l63.181-36.425v25.221a.87.87 0 0 1-.358.665l-52.335 30.184c-23.257 13.398-52.97 5.431-66.404-17.803ZM23.549 85.38a48.499 48.499 0 0 1 25.58-21.333v61.39a8.288 8.288 0 0 0 4.195 7.316l62.874 36.272-21.845 12.636a.819.819 0 0 1-.767 0L41.353 151.53c-23.211-13.454-31.171-43.144-17.804-66.405v.256Zm179.466 41.695-63.08-36.63L161.73 77.86a.819.819 0 0 1 .768 0l52.233 30.184a48.6 48.6 0 0 1-7.316 87.635v-61.391a8.544 8.544 0 0 0-4.4-7.213Zm21.742-32.69-1.535-.922-51.619-30.081a8.39 8.39 0 0 0-8.492 0L99.98 99.808V74.587a.716.716 0 0 1 .307-.665l52.233-30.133a48.652 48.652 0 0 1 72.236 50.391v.205ZM88.061 139.097l-21.845-12.585a.87.87 0 0 1-.41-.614V65.685a48.652 48.652 0 0 1 79.757-37.346l-1.535.87-51.67 29.825a8.595 8.595 0 0 0-4.246 7.367l-.051 72.697Zm11.868-25.58 28.138-16.217 28.188 16.218v32.434l-28.086 16.218-28.188-16.218-.052-32.434Z"
76
+ />
77
+ </svg>
78
+ );
79
+ }
80
+
81
+ function OpencodeMark({
82
+ size = 14,
83
+ className,
84
+ ...props
85
+ }: Omit<ProviderIconProps, "provider">) {
86
+ const merge = useCn();
87
+ return (
88
+ <svg
89
+ xmlns="http://www.w3.org/2000/svg"
90
+ width={size}
91
+ height={size}
92
+ viewBox="0 0 512 512"
93
+ fill="none"
94
+ className={merge("shrink-0", className)}
95
+ aria-hidden="true"
96
+ {...props}
97
+ >
98
+ <rect width="512" height="512" fill="#FDFCFC" />
99
+ <path d="M320 224V352H192V224H320Z" fill="#E6E5E6" />
100
+ <path
101
+ fillRule="evenodd"
102
+ clipRule="evenodd"
103
+ d="M384 416H128V96H384V416ZM320 160H192V352H320V160Z"
104
+ fill="#17181C"
105
+ />
106
+ </svg>
107
+ );
108
+ }
109
+
110
+ /** Cursor glyph — path from svgl `cursor_light.svg` / `cursor_dark.svg`. */
111
+ function CursorMark({
112
+ size = 14,
113
+ className,
114
+ ...props
115
+ }: Omit<ProviderIconProps, "provider">) {
116
+ const merge = useCn();
117
+ return (
118
+ <svg
119
+ xmlns="http://www.w3.org/2000/svg"
120
+ width={size}
121
+ height={size}
122
+ viewBox="0 0 466.73 532.09"
123
+ className={merge("shrink-0 text-[#26251E] dark:text-[#EDECEC]", className)}
124
+ aria-hidden="true"
125
+ {...props}
126
+ >
127
+ <path
128
+ fill="currentColor"
129
+ d="M457.43,125.94L244.42,2.96c-6.84-3.95-15.28-3.95-22.12,0L9.3,125.94c-5.75,3.32-9.3,9.46-9.3,16.11v247.99c0,6.65,3.55,12.79,9.3,16.11l213.01,122.98c6.84,3.95,15.28,3.95,22.12,0l213.01-122.98c5.75-3.32,9.3-9.46,9.3-16.11v-247.99c0-6.65-3.55-12.79-9.3-16.11h-.01ZM444.05,151.99l-205.63,356.16c-1.39,2.4-5.06,1.42-5.06-1.36v-233.21c0-4.66-2.49-8.97-6.53-11.31L24.87,145.67c-2.4-1.39-1.42-5.06,1.36-5.06h411.26c5.84,0,9.49,6.33,6.57,11.39h-.01Z"
130
+ />
131
+ </svg>
132
+ );
133
+ }
@@ -1,6 +1,12 @@
1
1
  "use client";
2
2
 
3
- import { useState, type ReactNode } from "react";
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ type ComponentProps,
8
+ type ReactNode,
9
+ } from "react";
4
10
  import { motion, AnimatePresence } from "motion/react";
5
11
  import { cn } from "../utils/cn";
6
12
  import {
@@ -9,10 +15,17 @@ import {
9
15
  } from "../ui/surface-classes";
10
16
  import { Button } from "../ui/button";
11
17
  import { Badge } from "../ui/badge";
18
+ import {
19
+ Collapsible,
20
+ CollapsibleContent,
21
+ CollapsibleTrigger,
22
+ } from "../ui/collapsible";
12
23
  import { ScrollArea } from "../ui/scroll-area";
13
24
  import { Textarea } from "../ui/textarea";
25
+ import { useCn, useUiConfig } from "../ui/ui-config";
14
26
  import {
15
27
  IconChevronDown,
28
+ IconChevronRight,
16
29
  IconEdit,
17
30
  IconTrash,
18
31
  IconCheck,
@@ -20,13 +33,65 @@ import {
20
33
  IconStack2,
21
34
  } from "@tabler/icons-react";
22
35
 
36
+ // One Queue family, used in one of two ways:
37
+ // - The queued-messages widget: `Queue` with `count`, `QueueHeader`,
38
+ // `QueueList` and `QueueItem` with `text`. It animates with `motion.*`.
39
+ // - Composable parts (the AI Elements `Queue`): `Queue` without `count`, the
40
+ // `QueueSection*` parts, `QueueList`, `QueueItem` with children and the
41
+ // `QueueItem*` parts. Plain elements only, no `motion.*`, so it renders
42
+ // under a strict `LazyMotion`.
43
+ // `QueueList` inside a composable `Queue` is the plain list; anywhere else it
44
+ // is the widget's animated list.
45
+ //
46
+ // The widget keeps its own markup: its rows, actions and list have other
47
+ // elements and classes than the parts, so building it on them would change
48
+ // its DOM.
49
+
50
+ /** `UiConfig.Queue`: `classes.sectionTrigger` replaces the section header's classes. */
51
+ export interface QueueConfig {
52
+ classes?: {
53
+ sectionTrigger?: string;
54
+ };
55
+ }
56
+
57
+ /** True inside a composable `Queue` (one without `count`). */
58
+ const QueuePartsContext = createContext(false);
59
+
60
+ /** The queued-messages widget: animates in while `count > 0`. */
23
61
  export interface QueueProps {
24
62
  className?: string;
25
63
  children?: ReactNode;
26
64
  count: number;
27
65
  }
28
66
 
29
- export function Queue({ className, children, count }: QueueProps) {
67
+ /** The composable root: a plain container for `QueueSection`s. */
68
+ export type QueueDivProps = ComponentProps<"div"> & { count?: never };
69
+
70
+ /**
71
+ * With `count`, the queued-messages widget. Without it, the root container
72
+ * of a collapsible queue of pending items.
73
+ */
74
+ export function Queue(props: QueueProps | QueueDivProps) {
75
+ if (props.count === undefined) return <QueueRoot {...props} />;
76
+ return <QueueWidget {...props} />;
77
+ }
78
+
79
+ function QueueRoot({ className, count: _count, ...props }: QueueDivProps) {
80
+ const merge = useCn();
81
+ return (
82
+ <QueuePartsContext.Provider value>
83
+ <div
84
+ className={merge(
85
+ "flex flex-col rounded-xl border border-border bg-card/40 px-2.5 py-2",
86
+ className,
87
+ )}
88
+ {...props}
89
+ />
90
+ </QueuePartsContext.Provider>
91
+ );
92
+ }
93
+
94
+ function QueueWidget({ className, children, count }: QueueProps) {
30
95
  return (
31
96
  <AnimatePresence initial={false}>
32
97
  {count > 0 ? (
@@ -52,6 +117,7 @@ export interface QueueHeaderProps {
52
117
  className?: string;
53
118
  }
54
119
 
120
+ /** The widget's header: a toggle with the message count. */
55
121
  export function QueueHeader({
56
122
  count,
57
123
  isOpen,
@@ -92,16 +158,41 @@ export function QueueHeader({
92
158
  );
93
159
  }
94
160
 
95
- export interface QueueListProps {
96
- className?: string;
97
- children?: ReactNode;
161
+ export interface QueueListProps extends ComponentProps<"div"> {
162
+ /** Widget list only: animates the list in and out. Default `true`. */
98
163
  open?: boolean;
99
164
  }
100
165
 
101
- export function QueueList({
166
+ /**
167
+ * Inside a composable `Queue`, a scrollable list capped at `max-h-48`.
168
+ * Anywhere else, the widget's animated list capped at 240px.
169
+ */
170
+ export function QueueList({ open = true, ...props }: QueueListProps) {
171
+ const inParts = useContext(QueuePartsContext);
172
+ if (inParts) return <QueuePartsList {...props} />;
173
+ return <QueueWidgetList open={open} {...props} />;
174
+ }
175
+
176
+ function QueuePartsList({
177
+ children,
178
+ className,
179
+ ...props
180
+ }: ComponentProps<"div">) {
181
+ const merge = useCn();
182
+ return (
183
+ <div className={merge("mt-1", className)} {...props}>
184
+ <div className="max-h-48 overflow-y-auto">
185
+ <ul className="flex flex-col gap-1">{children}</ul>
186
+ </div>
187
+ </div>
188
+ );
189
+ }
190
+
191
+ function QueueWidgetList({
102
192
  className,
103
193
  children,
104
- open = true,
194
+ open,
195
+ ...props
105
196
  }: QueueListProps) {
106
197
  return (
107
198
  <AnimatePresence initial={false}>
@@ -114,7 +205,10 @@ export function QueueList({
114
205
  transition={{ type: "spring", bounce: 0, duration: 0.35 }}
115
206
  className="overflow-hidden"
116
207
  >
117
- <div className={cn(flatPanelSubtleClass, "rounded-b-2xl", className)}>
208
+ <div
209
+ className={cn(flatPanelSubtleClass, "rounded-b-2xl", className)}
210
+ {...props}
211
+ >
118
212
  <ScrollArea className="max-h-[240px]">
119
213
  <div className="p-2 space-y-1.5">{children}</div>
120
214
  </ScrollArea>
@@ -125,6 +219,7 @@ export function QueueList({
125
219
  );
126
220
  }
127
221
 
222
+ /** A widget row: one queued message with edit and remove. */
128
223
  export interface QueueItemProps {
129
224
  text: string;
130
225
  model?: string;
@@ -136,7 +231,29 @@ export interface QueueItemProps {
136
231
  className?: string;
137
232
  }
138
233
 
139
- export function QueueItem({
234
+ /** A composable row: an `li` for your own content and the `QueueItem*` parts. */
235
+ export type QueueItemLiProps = ComponentProps<"li"> & { text?: never };
236
+
237
+ /** With `text`, a widget row. Without it, a plain `li` row. */
238
+ export function QueueItem(props: QueueItemProps | QueueItemLiProps) {
239
+ if (props.text === undefined) return <QueuePartsItem {...props} />;
240
+ return <QueueWidgetItem {...props} />;
241
+ }
242
+
243
+ function QueuePartsItem({ className, text: _text, ...props }: QueueItemLiProps) {
244
+ const merge = useCn();
245
+ return (
246
+ <li
247
+ className={merge(
248
+ "group flex flex-col gap-1 rounded-lg px-2 py-1.5 text-sm transition-colors hover:bg-surface-secondary/50",
249
+ className,
250
+ )}
251
+ {...props}
252
+ />
253
+ );
254
+ }
255
+
256
+ function QueueWidgetItem({
140
257
  text,
141
258
  model,
142
259
  personalityName,
@@ -249,3 +366,170 @@ export function QueueItem({
249
366
  </div>
250
367
  );
251
368
  }
369
+
370
+ export type QueueSectionProps = ComponentProps<typeof Collapsible>;
371
+
372
+ export const QueueSection = ({
373
+ className,
374
+ defaultOpen = true,
375
+ ...props
376
+ }: QueueSectionProps) => {
377
+ const merge = useCn();
378
+ return <Collapsible className={merge(className)} defaultOpen={defaultOpen} {...props} />;
379
+ };
380
+
381
+ export type QueueSectionTriggerProps = ComponentProps<"button">;
382
+
383
+ export const QueueSectionTrigger = ({
384
+ children,
385
+ className,
386
+ ...props
387
+ }: QueueSectionTriggerProps) => {
388
+ const sectionTrigger = useUiConfig("Queue")?.classes?.sectionTrigger;
389
+ const merge = useCn();
390
+ return (
391
+ <CollapsibleTrigger asChild>
392
+ <button
393
+ className={merge(
394
+ sectionTrigger ??
395
+ "group flex w-full items-center rounded-md px-1 py-1 text-left text-xs font-medium text-muted-foreground transition-[color,scale] duration-150 ease-out hover:text-foreground active:scale-[0.99]",
396
+ className,
397
+ )}
398
+ type="button"
399
+ {...props}
400
+ >
401
+ {children}
402
+ </button>
403
+ </CollapsibleTrigger>
404
+ );
405
+ };
406
+
407
+ export type QueueSectionLabelProps = ComponentProps<"span"> & {
408
+ count?: number;
409
+ label: string;
410
+ icon?: ReactNode;
411
+ };
412
+
413
+ export const QueueSectionLabel = ({
414
+ count,
415
+ label,
416
+ icon,
417
+ className,
418
+ ...props
419
+ }: QueueSectionLabelProps) => {
420
+ const merge = useCn();
421
+ return (
422
+ <span className={merge("flex items-center gap-1.5", className)} {...props}>
423
+ {icon}
424
+ <span>
425
+ {count} {label}
426
+ </span>
427
+ {/* After the text, pointing right, turning 90° when the section is open. */}
428
+ <IconChevronRight className="-ml-0.5 size-3.5 opacity-70 transition-transform group-data-[state=open]:rotate-90" />
429
+ </span>
430
+ );
431
+ };
432
+
433
+ export type QueueSectionContentProps = ComponentProps<
434
+ typeof CollapsibleContent
435
+ >;
436
+
437
+ export const QueueSectionContent = ({
438
+ className,
439
+ ...props
440
+ }: QueueSectionContentProps) => {
441
+ const merge = useCn();
442
+ return <CollapsibleContent className={merge(className)} {...props} />;
443
+ };
444
+
445
+ export type QueueItemIndicatorProps = ComponentProps<"span"> & {
446
+ completed?: boolean;
447
+ };
448
+
449
+ export const QueueItemIndicator = ({
450
+ completed = false,
451
+ className,
452
+ ...props
453
+ }: QueueItemIndicatorProps) => {
454
+ const merge = useCn();
455
+ return (
456
+ <span
457
+ className={merge(
458
+ "mt-1 inline-block size-2.5 shrink-0 rounded-full border",
459
+ completed
460
+ ? "border-muted-foreground/20 bg-muted-foreground/10"
461
+ : "border-muted-foreground/45",
462
+ className,
463
+ )}
464
+ {...props}
465
+ />
466
+ );
467
+ };
468
+
469
+ export type QueueItemContentProps = ComponentProps<"span"> & {
470
+ completed?: boolean;
471
+ };
472
+
473
+ export const QueueItemContent = ({
474
+ completed = false,
475
+ className,
476
+ ...props
477
+ }: QueueItemContentProps) => {
478
+ const merge = useCn();
479
+ return (
480
+ <span
481
+ className={merge(
482
+ "line-clamp-2 min-w-0 grow wrap-break-word leading-snug",
483
+ completed
484
+ ? "text-muted-foreground/50 line-through"
485
+ : "text-foreground/90",
486
+ className,
487
+ )}
488
+ {...props}
489
+ />
490
+ );
491
+ };
492
+
493
+ export type QueueItemActionsProps = ComponentProps<"div">;
494
+
495
+ export const QueueItemActions = ({
496
+ className,
497
+ ...props
498
+ }: QueueItemActionsProps) => {
499
+ const merge = useCn();
500
+ return (
501
+ <div
502
+ className={merge(
503
+ // `reveal-on-hover`: hidden until row hover on pointer devices, always
504
+ // visible below `sm` where there is no hover.
505
+ "flex shrink-0 items-start gap-0.5 reveal-on-hover transition-opacity",
506
+ className,
507
+ )}
508
+ {...props}
509
+ />
510
+ );
511
+ };
512
+
513
+ export type QueueItemActionProps = Omit<
514
+ ComponentProps<typeof Button>,
515
+ "variant" | "size"
516
+ >;
517
+
518
+ export const QueueItemAction = ({
519
+ className,
520
+ ...props
521
+ }: QueueItemActionProps) => {
522
+ const merge = useCn();
523
+ return (
524
+ <Button
525
+ className={merge(
526
+ "size-auto rounded-md p-1 text-muted-foreground hover:bg-surface-secondary hover:text-foreground",
527
+ className,
528
+ )}
529
+ size="icon"
530
+ type="button"
531
+ variant="ghost"
532
+ {...props}
533
+ />
534
+ );
535
+ };
@@ -2,18 +2,54 @@
2
2
  import { type CSSProperties, type ElementType } from "react";
3
3
  import { motion } from "motion/react";
4
4
  import { cn } from "../utils/cn";
5
+ import { useAnimOffscreenRef } from "../utils/runtimeVisibility";
6
+ import { useCn, useUiConfig } from "../ui/ui-config";
5
7
  interface ShimmerCSSProperties extends CSSProperties {
6
8
  "--spread"?: string;
7
9
  "--bg"?: string;
8
10
  }
11
+ /** Custom properties the `mask` sweep reads from the app's `.shimmer-*` CSS. */
12
+ interface ShimmerMaskStyle extends CSSProperties {
13
+ "--shimmer-spread": string;
14
+ "--shimmer-duration": string;
15
+ }
16
+ /**
17
+ * `UiConfig.Shimmer`. `mode: "mask"` sweeps a masked copy of the text on the
18
+ * compositor (no repaint per frame, paused offscreen) instead of animating a
19
+ * `background-position`. The app defines the `overlay`, `band` and `copy`
20
+ * classes (default `shimmer-overlay`, `shimmer-band`, `shimmer-copy`).
21
+ */
22
+ export interface ShimmerConfig {
23
+ mode?: "gradient" | "mask";
24
+ /** Default element. */
25
+ as?: ElementType;
26
+ classes?: {
27
+ /** Root element in `mask` mode. */
28
+ base?: string;
29
+ overlay?: string;
30
+ band?: string;
31
+ copy?: string;
32
+ };
33
+ }
9
34
  interface ShimmerProps {
10
35
  children: string;
11
36
  as?: ElementType;
12
37
  className?: string;
13
38
  duration?: number;
14
39
  spread?: number;
40
+ /** `"mask"`: compositor sweep, see `ShimmerConfig`. Default `"gradient"`. */
41
+ mode?: "gradient" | "mask";
42
+ }
43
+ function Shimmer({ mode, as, ...props }: ShimmerProps) {
44
+ const config = useUiConfig("Shimmer");
45
+ const Component = as ?? config?.as ?? "span";
46
+ return (mode ?? config?.mode) === "mask" ? (
47
+ <MaskShimmer as={Component} {...props} />
48
+ ) : (
49
+ <GradientShimmer as={Component} {...props} />
50
+ );
15
51
  }
16
- function Shimmer({
52
+ function GradientShimmer({
17
53
  children,
18
54
  as: Component = "span",
19
55
  className,
@@ -53,4 +89,39 @@ function Shimmer({
53
89
  </MotionComponent>
54
90
  );
55
91
  }
92
+ /**
93
+ * The text renders once in the base colour; a copy in the highlight colour
94
+ * sits over it, masked to a soft band. The band translates one way while the
95
+ * copy inside it translates the other by the same amount, so only the mask
96
+ * moves: two transform animations, no repaint. The copy is `aria-hidden`.
97
+ */
98
+ function MaskShimmer({
99
+ children,
100
+ as: Component = "span",
101
+ className,
102
+ duration = 2,
103
+ spread = 2,
104
+ }: ShimmerProps) {
105
+ const classes = useUiConfig("Shimmer")?.classes;
106
+ const merge = useCn();
107
+ const setOffscreen = useAnimOffscreenRef<HTMLElement>();
108
+ const style: ShimmerMaskStyle = {
109
+ "--shimmer-spread": `${children.length * spread}px`,
110
+ "--shimmer-duration": `${duration}s`,
111
+ };
112
+ return (
113
+ <Component
114
+ ref={setOffscreen}
115
+ className={merge(classes?.base ?? "relative inline-block text-muted-foreground", className)}
116
+ style={style}
117
+ >
118
+ {children}
119
+ <span aria-hidden="true" data-anim-chrome="" className={classes?.overlay ?? "shimmer-overlay"}>
120
+ <span className={classes?.band ?? "shimmer-band"}>
121
+ <span className={classes?.copy ?? "shimmer-copy"}>{children}</span>
122
+ </span>
123
+ </span>
124
+ </Component>
125
+ );
126
+ }
56
127
  export { Shimmer, type ShimmerProps };