tembro 4.0.1 → 4.2.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 (75) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/components/display/chat.cjs +1 -0
  3. package/dist/components/display/chat.d.ts +94 -0
  4. package/dist/components/display/chat.js +1 -0
  5. package/dist/components/display/index.d.ts +1 -0
  6. package/dist/components/display/kanban.d.ts +33 -1
  7. package/dist/components/inputs/combobox.cjs +1 -0
  8. package/dist/components/inputs/combobox.d.ts +46 -0
  9. package/dist/components/inputs/combobox.js +1 -0
  10. package/dist/components/inputs/index.d.ts +1 -0
  11. package/dist/components/inputs/public.d.ts +1 -0
  12. package/dist/components/layout/sidebar.d.ts +24 -2
  13. package/dist/components/modern/rich-text-editor.d.ts +13 -1
  14. package/dist/components/theme-provider.d.ts +19 -6
  15. package/dist/components/ui/badge/index.d.ts +8 -2
  16. package/dist/src/components/display/activity-feed.cjs +1 -1
  17. package/dist/src/components/display/activity-feed.js +4 -1
  18. package/dist/src/components/display/chat.cjs +1 -0
  19. package/dist/src/components/display/chat.js +372 -0
  20. package/dist/src/components/display/index.cjs +1 -1
  21. package/dist/src/components/display/index.js +17 -16
  22. package/dist/src/components/display/kanban.cjs +1 -1
  23. package/dist/src/components/display/kanban.js +321 -176
  24. package/dist/src/components/display/timeline.cjs +1 -1
  25. package/dist/src/components/display/timeline.js +5 -5
  26. package/dist/src/components/inputs/combobox.cjs +1 -0
  27. package/dist/src/components/inputs/combobox.js +162 -0
  28. package/dist/src/components/inputs/index.cjs +1 -1
  29. package/dist/src/components/inputs/index.js +10 -9
  30. package/dist/src/components/inputs/public.cjs +1 -1
  31. package/dist/src/components/inputs/public.js +10 -9
  32. package/dist/src/components/inputs/tag-input.cjs +1 -1
  33. package/dist/src/components/inputs/tag-input.js +6 -2
  34. package/dist/src/components/layout/index.cjs +1 -1
  35. package/dist/src/components/layout/index.js +4 -4
  36. package/dist/src/components/layout/public.cjs +1 -1
  37. package/dist/src/components/layout/public.js +4 -4
  38. package/dist/src/components/layout/sidebar.cjs +1 -1
  39. package/dist/src/components/layout/sidebar.js +326 -228
  40. package/dist/src/components/modern/rich-text-editor.cjs +1 -1
  41. package/dist/src/components/modern/rich-text-editor.js +216 -89
  42. package/dist/src/components/theme-provider.cjs +1 -1
  43. package/dist/src/components/theme-provider.js +58 -36
  44. package/dist/src/components/ui/badge/index.cjs +1 -1
  45. package/dist/src/components/ui/badge/index.js +59 -47
  46. package/dist/src/components/ui/input/clearable.cjs +1 -1
  47. package/dist/src/components/ui/input/clearable.js +5 -2
  48. package/dist/src/components/ui/input/primitive.cjs +1 -1
  49. package/dist/src/components/ui/input/primitive.js +1 -1
  50. package/dist/src/index.cjs +1 -1
  51. package/dist/src/index.js +88 -86
  52. package/dist/src/public-component-surface.cjs +1 -1
  53. package/dist/src/public-component-surface.js +107 -41
  54. package/package.json +1 -1
  55. package/packages/cli/dist/index.cjs +37 -37
  56. package/packages/cli/vendor/src/components/display/activity-feed.tsx +1 -1
  57. package/packages/cli/vendor/src/components/display/chat.tsx +237 -0
  58. package/packages/cli/vendor/src/components/display/index.ts +1 -0
  59. package/packages/cli/vendor/src/components/display/kanban.tsx +219 -72
  60. package/packages/cli/vendor/src/components/display/timeline.tsx +10 -10
  61. package/packages/cli/vendor/src/components/inputs/combobox.tsx +303 -0
  62. package/packages/cli/vendor/src/components/inputs/index.ts +1 -0
  63. package/packages/cli/vendor/src/components/inputs/public.ts +1 -0
  64. package/packages/cli/vendor/src/components/inputs/tag-input.tsx +3 -2
  65. package/packages/cli/vendor/src/components/layout/sidebar.tsx +212 -51
  66. package/packages/cli/vendor/src/components/modern/rich-text-editor.tsx +115 -32
  67. package/packages/cli/vendor/src/components/theme-provider.tsx +82 -36
  68. package/packages/cli/vendor/src/components/ui/badge/index.tsx +29 -8
  69. package/packages/cli/vendor/src/components/ui/input/clearable.tsx +2 -2
  70. package/packages/cli/vendor/src/components/ui/input/primitive.tsx +1 -1
  71. package/packages/cli/vendor/src/public-component-surface.ts +36 -22
  72. package/packages/cli/vendor/templates/showcase/src/showcase/layout/HeroSection.tsx +2 -2
  73. package/packages/cli/vendor/templates/showcase/src/showcase/layout/WorkbenchSidebar.tsx +1 -1
  74. package/packages/cli/vendor/templates/styles/globals.css +91 -10
  75. package/registry.json +28 -11
@@ -117,7 +117,7 @@ function ActivityFeed({
117
117
  className={cn("relative flex gap-3 pb-4 last:pb-0", isCompact && "gap-2 pb-3", itemClassName, item.className)}
118
118
  >
119
119
  {showConnector && index < visibleItems.length - 1 && (
120
- <div className={cn("absolute left-3 top-7 h-[calc(100%-1.75rem)] w-px bg-border", connectorClassName)} />
120
+ <div data-slot="activity-feed-connector" className={cn("absolute left-5 top-8 h-[calc(100%-2rem)] w-px bg-border", connectorClassName)} />
121
121
  )}
122
122
  {item.href ? (
123
123
  <a
@@ -0,0 +1,237 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import {
5
+ CheckCheckIcon,
6
+ CheckIcon,
7
+ FileIcon,
8
+ MoreHorizontalIcon,
9
+ PaperclipIcon,
10
+ SearchIcon,
11
+ SendIcon,
12
+ SmileIcon,
13
+ XIcon,
14
+ } from "lucide-react"
15
+
16
+ import { Avatar } from "@/components/display/avatar"
17
+ import { Badge } from "@/components/ui/badge"
18
+ import { Button } from "@/components/ui/button"
19
+ import { Input } from "@/components/ui/input"
20
+ import { Textarea } from "@/components/ui/textarea"
21
+ import { cn } from "@/lib/utils"
22
+
23
+ export type ChatMessageStatus = "sending" | "sent" | "delivered" | "read" | "failed"
24
+
25
+ export type ChatAttachmentData = {
26
+ key: string
27
+ name: string
28
+ size?: React.ReactNode
29
+ type?: React.ReactNode
30
+ href?: string
31
+ preview?: string
32
+ }
33
+
34
+ export type ChatParticipant = {
35
+ name: string
36
+ avatar?: string
37
+ fallback?: React.ReactNode
38
+ status?: "online" | "offline" | "busy" | "away"
39
+ }
40
+
41
+ export type ChatShellProps = React.ComponentProps<"section"> & {
42
+ sidebar?: React.ReactNode
43
+ details?: React.ReactNode
44
+ }
45
+
46
+ function ChatShell({ sidebar, details, children, className, ...props }: ChatShellProps) {
47
+ return (
48
+ <section data-slot="chat-shell" className={cn("grid min-h-[34rem] overflow-hidden rounded-lg border bg-background shadow-sm lg:grid-cols-[18rem_minmax(0,1fr)]", details && "xl:grid-cols-[18rem_minmax(0,1fr)_17rem]", className)} {...props}>
49
+ {sidebar ? <aside data-slot="chat-sidebar" className="min-h-0 border-b lg:border-b-0 lg:border-r">{sidebar}</aside> : null}
50
+ <div data-slot="chat-main" className="grid min-h-0 min-w-0 grid-rows-[auto_minmax(0,1fr)_auto]">{children}</div>
51
+ {details ? <aside data-slot="chat-details" className="hidden min-h-0 border-l xl:block">{details}</aside> : null}
52
+ </section>
53
+ )
54
+ }
55
+
56
+ export type ChatHeaderProps = React.ComponentProps<"header"> & {
57
+ participant: ChatParticipant
58
+ description?: React.ReactNode
59
+ actions?: React.ReactNode
60
+ }
61
+
62
+ function ChatHeader({ participant, description, actions, className, ...props }: ChatHeaderProps) {
63
+ return (
64
+ <header data-slot="chat-header" className={cn("flex min-h-16 items-center gap-3 border-b px-4 py-2.5", className)} {...props}>
65
+ <Avatar size="sm" name={participant.name} src={participant.avatar} fallback={participant.fallback} status={participant.status} />
66
+ <div className="min-w-0 flex-1">
67
+ <div className="truncate text-sm font-semibold">{participant.name}</div>
68
+ <div className="truncate text-xs text-muted-foreground">{description ?? (participant.status === "online" ? "Online" : participant.status)}</div>
69
+ </div>
70
+ <div className="flex shrink-0 items-center gap-1">{actions}</div>
71
+ </header>
72
+ )
73
+ }
74
+
75
+ export type ConversationItem = {
76
+ key: string
77
+ participant: ChatParticipant
78
+ preview?: React.ReactNode
79
+ time?: React.ReactNode
80
+ unread?: number
81
+ muted?: boolean
82
+ pinned?: boolean
83
+ }
84
+
85
+ export type ConversationListProps = Omit<React.ComponentProps<"div">, "onSelect"> & {
86
+ items: ConversationItem[]
87
+ selectedKey?: string
88
+ defaultSelectedKey?: string
89
+ onSelect?: (item: ConversationItem) => void
90
+ searchable?: boolean
91
+ searchPlaceholder?: string
92
+ empty?: React.ReactNode
93
+ }
94
+
95
+ function ConversationList({ items, selectedKey, defaultSelectedKey, onSelect, searchable = true, searchPlaceholder = "Search conversations...", empty = "No conversations found.", className, ...props }: ConversationListProps) {
96
+ const [internalKey, setInternalKey] = React.useState(defaultSelectedKey)
97
+ const [query, setQuery] = React.useState("")
98
+ const activeKey = selectedKey ?? internalKey
99
+ const filtered = items.filter((item) => `${item.participant.name} ${String(item.preview ?? "")}`.toLowerCase().includes(query.trim().toLowerCase()))
100
+
101
+ return (
102
+ <div data-slot="conversation-list" className={cn("grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)]", className)} {...props}>
103
+ {searchable ? <div className="border-b p-3"><Input type="search" value={query} onValueChange={setQuery} placeholder={searchPlaceholder} leading={<SearchIcon />} clearable aria-label={searchPlaceholder} /></div> : <span />}
104
+ <div role="listbox" aria-label="Conversations" className="min-h-0 overflow-y-auto p-2">
105
+ {filtered.length ? filtered.map((item) => {
106
+ const selected = item.key === activeKey
107
+ return (
108
+ <button key={item.key} type="button" role="option" aria-selected={selected} data-selected={selected || undefined} className="flex w-full items-start gap-3 rounded-md px-2.5 py-2.5 text-left outline-none transition-colors hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring data-[selected=true]:bg-accent" onClick={() => { if (selectedKey === undefined) setInternalKey(item.key); onSelect?.(item) }}>
109
+ <Avatar size="sm" name={item.participant.name} src={item.participant.avatar} fallback={item.participant.fallback} status={item.participant.status} />
110
+ <span className="min-w-0 flex-1">
111
+ <span className="flex items-center gap-2"><strong className="min-w-0 flex-1 truncate text-sm font-medium">{item.participant.name}</strong><span className="shrink-0 text-[11px] text-muted-foreground">{item.time}</span></span>
112
+ <span className="mt-0.5 flex items-center gap-2"><span className="min-w-0 flex-1 truncate text-xs text-muted-foreground">{item.preview}</span>{item.unread ? <Badge size="sm" label={item.unread} /> : null}</span>
113
+ </span>
114
+ </button>
115
+ )
116
+ }) : <div className="grid min-h-32 place-items-center px-4 text-center text-sm text-muted-foreground">{empty}</div>}
117
+ </div>
118
+ </div>
119
+ )
120
+ }
121
+
122
+ export type ChatMessageListProps = React.ComponentProps<"div"> & {
123
+ autoScroll?: boolean
124
+ empty?: React.ReactNode
125
+ }
126
+
127
+ function ChatMessageList({ autoScroll = true, empty = "No messages yet.", children, className, ...props }: ChatMessageListProps) {
128
+ const ref = React.useRef<HTMLDivElement>(null)
129
+ React.useEffect(() => {
130
+ const element = ref.current
131
+ if (!autoScroll || !element) return
132
+ if (typeof element.scrollTo === "function") element.scrollTo({ top: element.scrollHeight, behavior: "smooth" })
133
+ else element.scrollTop = element.scrollHeight
134
+ }, [autoScroll, children])
135
+ return <div ref={ref} data-slot="chat-message-list" role="log" aria-live="polite" className={cn("min-h-0 overflow-y-auto bg-muted/15 px-4 py-5", className)} {...props}>{React.Children.count(children) ? <div className="mx-auto grid w-full max-w-3xl gap-4">{children}</div> : <div className="grid h-full place-items-center text-sm text-muted-foreground">{empty}</div>}</div>
136
+ }
137
+
138
+ export type ChatAttachmentProps = React.ComponentProps<"div"> & Omit<ChatAttachmentData, "key"> & {
139
+ attachmentKey?: string
140
+ onRemove?: () => void
141
+ }
142
+
143
+ function ChatAttachment({ name, size, type, href, preview, onRemove, attachmentKey: _attachmentKey, className, ...props }: ChatAttachmentProps) {
144
+ const content = <><span className="grid size-9 shrink-0 place-items-center overflow-hidden rounded-md bg-muted">{preview ? <img src={preview} alt="" className="size-full object-cover" /> : <FileIcon className="size-4" />}</span><span className="min-w-0 flex-1"><span className="block truncate text-xs font-medium text-foreground">{name}</span><span className="block truncate text-[11px] text-muted-foreground">{[type, size].filter(Boolean).join(" · ")}</span></span></>
145
+ return <div data-slot="chat-attachment" className={cn("flex min-w-0 items-center gap-2 rounded-md border bg-background p-2", className)} {...props}>{href ? <a href={href} className="flex min-w-0 flex-1 items-center gap-2 outline-none focus-visible:ring-2 focus-visible:ring-ring">{content}</a> : content}{onRemove ? <Button type="button" size="icon-xs" variant="ghost" iconOnly aria-label={`Remove ${name}`} onClick={onRemove}><XIcon /></Button> : null}</div>
146
+ }
147
+
148
+ export type ChatMessageProps = React.ComponentProps<"article"> & {
149
+ participant?: ChatParticipant
150
+ outgoing?: boolean
151
+ time?: React.ReactNode
152
+ status?: ChatMessageStatus
153
+ replyTo?: React.ReactNode
154
+ attachments?: ChatAttachmentData[]
155
+ reactions?: Array<{ key: string; label: React.ReactNode; count?: number; active?: boolean }>
156
+ onReaction?: (key: string) => void
157
+ actions?: React.ReactNode
158
+ }
159
+
160
+ function ChatMessage({ participant, outgoing = false, time, status, replyTo, attachments, reactions, onReaction, actions, children, className, ...props }: ChatMessageProps) {
161
+ const StatusIcon = status === "delivered" || status === "read" ? CheckCheckIcon : CheckIcon
162
+ return (
163
+ <article data-slot="chat-message" data-outgoing={outgoing || undefined} className={cn("group flex max-w-[88%] items-end gap-2 justify-self-start data-[outgoing=true]:justify-self-end data-[outgoing=true]:flex-row-reverse sm:max-w-[75%]", className)} {...props}>
164
+ {participant ? <Avatar size="xs" name={participant.name} src={participant.avatar} fallback={participant.fallback} /> : null}
165
+ <div className="min-w-0">
166
+ {participant && !outgoing ? <div className="mb-1 px-1 text-[11px] font-medium text-muted-foreground">{participant.name}</div> : null}
167
+ <div className={cn("rounded-lg rounded-bl-sm border bg-background px-3 py-2 text-sm leading-5 shadow-sm", outgoing && "rounded-bl-lg rounded-br-sm border-primary bg-primary text-primary-foreground")}>
168
+ {replyTo ? <div className={cn("mb-2 border-l-2 pl-2 text-xs opacity-75", outgoing ? "border-primary-foreground/60" : "border-primary")}>{replyTo}</div> : null}
169
+ <div className="whitespace-pre-wrap break-words">{children}</div>
170
+ {attachments?.length ? <div className="mt-2 grid gap-1.5">{attachments.map(({ key, ...attachment }) => <ChatAttachment key={key} attachmentKey={key} {...attachment} className={outgoing ? "border-primary-foreground/20 bg-primary-foreground/10 text-primary-foreground" : undefined} />)}</div> : null}
171
+ <div className={cn("mt-1 flex items-center justify-end gap-1 text-[10px] text-muted-foreground", outgoing && "text-primary-foreground/75")}><span>{time}</span>{status && status !== "failed" && status !== "sending" ? <StatusIcon className={cn("size-3", status === "read" && "text-sky-300")} /> : null}{status === "sending" ? <span>Sending...</span> : null}{status === "failed" ? <span className="font-medium text-destructive">Failed</span> : null}</div>
172
+ </div>
173
+ {reactions?.length ? <div className={cn("mt-1 flex flex-wrap gap-1", outgoing && "justify-end")}>{reactions.map((reaction) => <button key={reaction.key} type="button" aria-pressed={reaction.active} className="rounded-full border bg-background px-2 py-0.5 text-[11px] shadow-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring aria-pressed:border-primary aria-pressed:bg-primary/10" onClick={() => onReaction?.(reaction.key)}>{reaction.label}{reaction.count ? ` ${reaction.count}` : ""}</button>)}</div> : null}
174
+ </div>
175
+ {actions ? <div className="self-center opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">{actions}</div> : null}
176
+ </article>
177
+ )
178
+ }
179
+
180
+ function ChatTypingIndicator({ participant, className, ...props }: React.ComponentProps<"div"> & { participant?: ChatParticipant }) {
181
+ return <div data-slot="chat-typing" className={cn("flex items-center gap-2 text-xs text-muted-foreground", className)} {...props}>{participant ? <Avatar size="xs" name={participant.name} src={participant.avatar} fallback={participant.fallback} /> : null}<span className="flex items-center gap-1 rounded-full border bg-background px-3 py-2"><span className="size-1.5 animate-bounce rounded-full bg-current [animation-delay:-0.2s]" /><span className="size-1.5 animate-bounce rounded-full bg-current [animation-delay:-0.1s]" /><span className="size-1.5 animate-bounce rounded-full bg-current" /></span></div>
182
+ }
183
+
184
+ export type ChatComposerProps = Omit<React.ComponentProps<"form">, "onSubmit"> & {
185
+ value?: string
186
+ defaultValue?: string
187
+ onValueChange?: (value: string) => void
188
+ onSend: (value: string) => void | Promise<void>
189
+ placeholder?: string
190
+ disabled?: boolean
191
+ sending?: boolean
192
+ attachments?: ChatAttachmentData[]
193
+ onRemoveAttachment?: (key: string) => void
194
+ onAttachmentClick?: () => void
195
+ onEmojiClick?: () => void
196
+ maxLength?: number
197
+ submitLabel?: string
198
+ }
199
+
200
+ function ChatComposer({ value, defaultValue = "", onValueChange, onSend, placeholder = "Write a message...", disabled = false, sending = false, attachments, onRemoveAttachment, onAttachmentClick, onEmojiClick, maxLength, submitLabel = "Send message", className, ...props }: ChatComposerProps) {
201
+ const [internalValue, setInternalValue] = React.useState(defaultValue)
202
+ const currentValue = value ?? internalValue
203
+ const setValue = (next: string) => { if (value === undefined) setInternalValue(next); onValueChange?.(next) }
204
+ const send = async () => {
205
+ const next = currentValue.trim()
206
+ if (!next || disabled || sending) return
207
+ await onSend(next)
208
+ setValue("")
209
+ }
210
+ return (
211
+ <form data-slot="chat-composer" className={cn("border-t bg-background p-3", className)} onSubmit={(event) => { event.preventDefault(); void send() }} {...props}>
212
+ {attachments?.length ? <div className="mb-2 grid gap-2 sm:grid-cols-2">{attachments.map(({ key, ...attachment }) => <ChatAttachment key={key} attachmentKey={key} {...attachment} onRemove={onRemoveAttachment ? () => onRemoveAttachment(key) : undefined} />)}</div> : null}
213
+ <div className="flex items-end gap-2 rounded-lg border bg-background p-1.5 shadow-sm transition-[border-color,box-shadow] focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/20">
214
+ {onAttachmentClick ? <Button type="button" size="icon-sm" variant="ghost" iconOnly aria-label="Attach file" onClick={onAttachmentClick}><PaperclipIcon /></Button> : null}
215
+ <Textarea value={currentValue} onValueChange={setValue} rows={1} maxLength={maxLength} disabled={disabled} placeholder={placeholder} aria-label={placeholder} className="max-h-36 min-h-9 resize-none border-0 bg-transparent px-2 py-2 font-normal shadow-none focus-visible:shadow-none" onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); void send() } }} />
216
+ {onEmojiClick ? <Button type="button" size="icon-sm" variant="ghost" iconOnly aria-label="Choose emoji" onClick={onEmojiClick}><SmileIcon /></Button> : null}
217
+ <Button type="submit" size="icon-sm" iconOnly aria-label={submitLabel} loading={sending} disabled={disabled || !currentValue.trim()}><SendIcon /></Button>
218
+ </div>
219
+ </form>
220
+ )
221
+ }
222
+
223
+ function ChatHeaderActions() {
224
+ return <><Button size="icon-sm" variant="ghost" iconOnly aria-label="Search conversation"><SearchIcon /></Button><Button size="icon-sm" variant="ghost" iconOnly aria-label="More conversation actions"><MoreHorizontalIcon /></Button></>
225
+ }
226
+
227
+ export {
228
+ ChatAttachment,
229
+ ChatComposer,
230
+ ChatHeader,
231
+ ChatHeaderActions,
232
+ ChatMessage,
233
+ ChatMessageList,
234
+ ChatShell,
235
+ ChatTypingIndicator,
236
+ ConversationList,
237
+ }
@@ -15,5 +15,6 @@ export * from "./info-card"
15
15
  export * from "./virtual-list"
16
16
  export * from "./qr-code"
17
17
  export * from "./carousel"
18
+ export * from "./chat"
18
19
  export * from "./tag"
19
20
  export * from "./typography"