@giddaa-housing/ui 2.0.0 → 3.0.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/css/generated/shared.css +1 -1
- package/css/theme.css +24 -0
- package/dist/accordion.js +1 -1
- package/dist/{button-BZIeyUw6.d.ts → button-DW5OijoR.d.ts} +1 -1
- package/dist/button-group.d.ts +1 -1
- package/dist/button-group.js +2 -2
- package/dist/button.d.ts +1 -1
- package/dist/button.js +4 -3
- package/dist/call-card.d.ts +131 -0
- package/dist/call-card.js +300 -0
- package/dist/card.d.ts +4 -2
- package/dist/card.js +7 -7
- package/dist/carousel.js +12 -9
- package/dist/chat.d.ts +208 -0
- package/dist/chat.js +528 -0
- package/dist/checkbox.js +1 -1
- package/dist/combobox.d.ts +48 -1
- package/dist/combobox.js +78 -5
- package/dist/cta.d.ts +1 -1
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js +1 -1
- package/dist/date-picker.d.ts +5 -2
- package/dist/date-picker.js +11 -6
- package/dist/dialog.js +2 -2
- package/dist/dropdown-menu.js +1 -1
- package/dist/file-upload.js +3 -3
- package/dist/infinite-scroll.d.ts +40 -0
- package/dist/infinite-scroll.js +125 -0
- package/dist/infotip.js +1 -1
- package/dist/input-group.d.ts +1 -1
- package/dist/media-player.js +15 -8
- package/dist/message.d.ts +263 -0
- package/dist/message.js +524 -0
- package/dist/multi-step-form.js +10 -6
- package/dist/page-header.d.ts +6 -6
- package/dist/page-header.js +21 -7
- package/dist/pagination.d.ts +1 -1
- package/dist/picker-xSTzeYhc.js +50 -0
- package/dist/popover.d.ts +2 -2
- package/dist/popover.js +3 -2
- package/dist/price-tag.js +1 -1
- package/dist/property-listing-card.d.ts +1 -1
- package/dist/radio-group.js +1 -1
- package/dist/rich-text-editor.js +3 -3
- package/dist/select.js +1 -1
- package/dist/sheet.js +1 -1
- package/dist/sidebar.d.ts +1 -1
- package/dist/sidebar.js +5 -5
- package/dist/skeleton.js +1 -1
- package/dist/slider.js +3 -2
- package/dist/styles.css +1893 -160
- package/dist/switch.js +3 -3
- package/dist/table.js +2 -2
- package/dist/tabs.js +4 -4
- package/dist/time-picker.d.ts +4 -1
- package/dist/time-picker.js +93 -12
- package/dist/toast.js +1 -1
- package/dist/tooltip.d.ts +2 -1
- package/dist/tooltip.js +9 -2
- package/dist/use-reduced-motion-BDvOK14x.js +17 -0
- package/package.json +18 -1
- package/dist/picker-lzgmsNfG.js +0 -32
package/dist/chat.js
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { t as cn } from "./cn-BI_4DMBf.js";
|
|
3
|
+
import { Button } from "./button.js";
|
|
4
|
+
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "./input-group.js";
|
|
5
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./dropdown-menu.js";
|
|
6
|
+
import { FileText, Image, Plus, Send, Video } from "lucide-react";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import * as React from "react";
|
|
9
|
+
//#region src/chat.tsx
|
|
10
|
+
/**
|
|
11
|
+
* The chat shell — a column of header, body and footer.
|
|
12
|
+
*
|
|
13
|
+
* **It sets no height on purpose.** The consumer decides: `h-[708px]`, `h-full`,
|
|
14
|
+
* `h-dvh`. Note that `ChatBody` can only scroll inside a bounded height — give
|
|
15
|
+
* this element one, or the body grows with its content and the page scrolls
|
|
16
|
+
* instead.
|
|
17
|
+
*
|
|
18
|
+
* Header and footer sit outside the scrolling region, so they never move and
|
|
19
|
+
* never need measuring. Anything that should pin *within* the scroll — tabs, a
|
|
20
|
+
* filter row — belongs in `ChatBodyBar`.
|
|
21
|
+
*/
|
|
22
|
+
function Chat({ className, ...props }) {
|
|
23
|
+
return /* @__PURE__ */ jsx("section", {
|
|
24
|
+
"data-slot": "chat",
|
|
25
|
+
className: cn("relative flex min-h-0 flex-col overflow-hidden rounded-[40px] border border-line-subtle bg-surface", className),
|
|
26
|
+
...props
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The fixed header. Composes anything — avatar, title, actions, a sentiment bar.
|
|
31
|
+
*
|
|
32
|
+
* Its own height is free: it can wrap, gain a row, or lose one, and nothing
|
|
33
|
+
* downstream cares. Only the body scrolls.
|
|
34
|
+
*/
|
|
35
|
+
function ChatHeader({ className, ...props }) {
|
|
36
|
+
return /* @__PURE__ */ jsx("header", {
|
|
37
|
+
"data-slot": "chat-header",
|
|
38
|
+
className: cn("flex min-h-20 shrink-0 items-center gap-3 border-b border-line-subtle bg-canvas px-3.5", className),
|
|
39
|
+
...props
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** The header's primary line — who the conversation is with. */
|
|
43
|
+
function ChatHeaderTitle({ className, ...props }) {
|
|
44
|
+
return /* @__PURE__ */ jsx("h2", {
|
|
45
|
+
"data-slot": "chat-header-title",
|
|
46
|
+
className: cn("truncate text-gdt-md leading-[22px] font-semibold text-fg-primary", className),
|
|
47
|
+
...props
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** The header's secondary line — timezone, status, last seen. */
|
|
51
|
+
function ChatHeaderDescription({ className, ...props }) {
|
|
52
|
+
return /* @__PURE__ */ jsx("p", {
|
|
53
|
+
"data-slot": "chat-header-description",
|
|
54
|
+
className: cn("truncate text-gdt-xs leading-[18px] text-fg-secondary", className),
|
|
55
|
+
...props
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Avatar and contact copy. Fills the space between navigation and actions. */
|
|
59
|
+
function ChatHeaderIdentity({ className, ...props }) {
|
|
60
|
+
return /* @__PURE__ */ jsx("div", {
|
|
61
|
+
"data-slot": "chat-header-identity",
|
|
62
|
+
className: cn("flex min-w-0 flex-1 items-center gap-3", className),
|
|
63
|
+
...props
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Stacks the contact name and presence line at the Figma one-pixel gap. */
|
|
67
|
+
function ChatHeaderText({ className, ...props }) {
|
|
68
|
+
return /* @__PURE__ */ jsx("div", {
|
|
69
|
+
"data-slot": "chat-header-text",
|
|
70
|
+
className: cn("flex min-w-0 flex-col gap-px", className),
|
|
71
|
+
...props
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** Trailing icon actions such as video and voice calling. */
|
|
75
|
+
function ChatHeaderActions({ className, ...props }) {
|
|
76
|
+
return /* @__PURE__ */ jsx("div", {
|
|
77
|
+
"data-slot": "chat-header-actions",
|
|
78
|
+
className: cn("flex shrink-0 items-center gap-3", className),
|
|
79
|
+
...props
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** A 24px header affordance. The caller supplies the icon and behaviour. */
|
|
83
|
+
function ChatHeaderAction({ className, size = "icon-xs", variant = "ghost", ...props }) {
|
|
84
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
85
|
+
"data-slot": "chat-header-action",
|
|
86
|
+
className: cn("text-fg-primary [&_svg:not([class*='size-'])]:size-6", className),
|
|
87
|
+
size,
|
|
88
|
+
type: "button",
|
|
89
|
+
variant,
|
|
90
|
+
...props
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The fixed footer. Wraps `ChatInput`, and owns only the chrome around it —
|
|
95
|
+
* the rule and the surface. The composer owns its internal bar padding.
|
|
96
|
+
*/
|
|
97
|
+
function ChatFooter({ className, ...props }) {
|
|
98
|
+
return /* @__PURE__ */ jsx("footer", {
|
|
99
|
+
"data-slot": "chat-footer",
|
|
100
|
+
className: cn("shrink-0 border-t border-line-subtle bg-canvas", className),
|
|
101
|
+
...props
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Distance from the bottom, in px, that still counts as being at the bottom.
|
|
106
|
+
*
|
|
107
|
+
* Big enough to absorb sub-pixel layout rounding, small enough that it never
|
|
108
|
+
* feels sticky.
|
|
109
|
+
*/
|
|
110
|
+
const SCROLL_EDGE_THRESHOLD = 8;
|
|
111
|
+
/** Movement below this is layout jitter, not deliberate reader input. */
|
|
112
|
+
const SUBPIXEL_EPSILON = .5;
|
|
113
|
+
const ChatBodyContext = React.createContext(null);
|
|
114
|
+
function assignRef(ref, value) {
|
|
115
|
+
if (typeof ref === "function") {
|
|
116
|
+
ref(value);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (ref) ref.current = value;
|
|
120
|
+
}
|
|
121
|
+
function getStableAnchor(element, content) {
|
|
122
|
+
const stable = element.closest("[data-scroll-anchor],[data-message-id]");
|
|
123
|
+
if (!stable || !content.contains(stable)) return { element };
|
|
124
|
+
const scrollAnchor = stable.getAttribute("data-scroll-anchor");
|
|
125
|
+
if (scrollAnchor !== null) return {
|
|
126
|
+
element: stable,
|
|
127
|
+
attribute: "data-scroll-anchor",
|
|
128
|
+
value: scrollAnchor
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
element: stable,
|
|
132
|
+
attribute: "data-message-id",
|
|
133
|
+
value: stable.getAttribute("data-message-id") ?? void 0
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function findAnchorByAttribute(content, attribute, value) {
|
|
137
|
+
for (const element of content.querySelectorAll(`[${attribute}]`)) if (element.getAttribute(attribute) === value) return element;
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Read the body's scroll state — for a "jump to bottom" or "N new messages"
|
|
142
|
+
* affordance. Must be called inside `<ChatBody>`.
|
|
143
|
+
*/
|
|
144
|
+
function useChatBody() {
|
|
145
|
+
const context = React.useContext(ChatBodyContext);
|
|
146
|
+
if (!context) throw new Error("useChatBody must be called inside <ChatBody>.");
|
|
147
|
+
return context;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The scrolling region of a chat. Composes anything — messages, a table, an
|
|
151
|
+
* empty state. It owns scrolling and nothing else.
|
|
152
|
+
*
|
|
153
|
+
* Two behaviours, and they are the whole point:
|
|
154
|
+
*
|
|
155
|
+
* **Following.** New content keeps the bottom in view — but only while the
|
|
156
|
+
* reader is already there. Any deliberate scroll upward hands control over
|
|
157
|
+
* immediately; returning to the bottom hands it back. It never yanks.
|
|
158
|
+
*
|
|
159
|
+
* **Prepend.** When older messages load in above, the row you were reading
|
|
160
|
+
* stays exactly where it was. This works by remembering an element and its
|
|
161
|
+
* offset from the viewport, then putting it back after the layout changes —
|
|
162
|
+
* which holds no matter where the content grew.
|
|
163
|
+
*
|
|
164
|
+
* Both are driven by one `ResizeObserver` on the content, so they also cover an
|
|
165
|
+
* image finishing its load or a bubble reflowing — things a dependency on a
|
|
166
|
+
* message array cannot see. It fires once on observe, which is what opens the
|
|
167
|
+
* view at the latest message.
|
|
168
|
+
*
|
|
169
|
+
* Children may use `sticky top-0` (see `ChatBodyBar`) — this is their scroll
|
|
170
|
+
* container.
|
|
171
|
+
*/
|
|
172
|
+
function ChatBody({ className, children, onScroll, ref, ...props }) {
|
|
173
|
+
const viewportRef = React.useRef(null);
|
|
174
|
+
const contentRef = React.useRef(null);
|
|
175
|
+
const [isPinned, setIsPinned] = React.useState(true);
|
|
176
|
+
const setViewportRef = React.useCallback((node) => {
|
|
177
|
+
viewportRef.current = node;
|
|
178
|
+
assignRef(ref, node);
|
|
179
|
+
}, [ref]);
|
|
180
|
+
const followingRef = React.useRef(true);
|
|
181
|
+
const lastScrollTopRef = React.useRef(0);
|
|
182
|
+
const anchorRef = React.useRef(null);
|
|
183
|
+
const scrollToBottom = React.useCallback((behavior = "smooth") => {
|
|
184
|
+
const viewport = viewportRef.current;
|
|
185
|
+
if (!viewport) return;
|
|
186
|
+
followingRef.current = true;
|
|
187
|
+
setIsPinned(true);
|
|
188
|
+
viewport.scrollTo({
|
|
189
|
+
top: viewport.scrollHeight - viewport.clientHeight,
|
|
190
|
+
behavior
|
|
191
|
+
});
|
|
192
|
+
}, []);
|
|
193
|
+
/**
|
|
194
|
+
* Remember the element under the middle of the viewport, and how far below
|
|
195
|
+
* the viewport's top edge it sits.
|
|
196
|
+
*
|
|
197
|
+
* The middle rather than the top on purpose: `ChatBodyBar` is sticky at the
|
|
198
|
+
* top, so probing there would anchor to a bar that never moves and defeat
|
|
199
|
+
* the whole mechanism.
|
|
200
|
+
*/
|
|
201
|
+
const captureAnchor = React.useCallback(() => {
|
|
202
|
+
const viewport = viewportRef.current;
|
|
203
|
+
const content = contentRef.current;
|
|
204
|
+
if (!viewport || !content || followingRef.current) {
|
|
205
|
+
anchorRef.current = null;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const bounds = viewport.getBoundingClientRect();
|
|
209
|
+
const element = document.elementFromPoint(bounds.left + bounds.width / 2, bounds.top + bounds.height / 2);
|
|
210
|
+
if (!element || !viewport.contains(element)) {
|
|
211
|
+
anchorRef.current = null;
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const anchor = getStableAnchor(element, content);
|
|
215
|
+
anchorRef.current = {
|
|
216
|
+
...anchor,
|
|
217
|
+
top: anchor.element.getBoundingClientRect().top - bounds.top
|
|
218
|
+
};
|
|
219
|
+
}, []);
|
|
220
|
+
/** Put the remembered element back where it was. */
|
|
221
|
+
const restoreAnchor = React.useCallback(() => {
|
|
222
|
+
const viewport = viewportRef.current;
|
|
223
|
+
const content = contentRef.current;
|
|
224
|
+
const anchor = anchorRef.current;
|
|
225
|
+
if (!viewport || !content || !anchor) return;
|
|
226
|
+
let element = anchor.element;
|
|
227
|
+
if (!element.isConnected && anchor.attribute && anchor.value !== void 0) element = findAnchorByAttribute(content, anchor.attribute, anchor.value) ?? element;
|
|
228
|
+
if (!element.isConnected || !content.contains(element)) return;
|
|
229
|
+
const viewportTop = viewport.getBoundingClientRect().top;
|
|
230
|
+
const delta = element.getBoundingClientRect().top - viewportTop - anchor.top;
|
|
231
|
+
if (Math.abs(delta) <= SUBPIXEL_EPSILON) return;
|
|
232
|
+
viewport.scrollTop += delta;
|
|
233
|
+
anchor.element = element;
|
|
234
|
+
anchor.top = element.getBoundingClientRect().top - viewportTop;
|
|
235
|
+
}, []);
|
|
236
|
+
const handleScroll = React.useCallback((event) => {
|
|
237
|
+
onScroll?.(event);
|
|
238
|
+
if (event.defaultPrevented) return;
|
|
239
|
+
const viewport = event.currentTarget;
|
|
240
|
+
const previous = lastScrollTopRef.current;
|
|
241
|
+
const current = viewport.scrollTop;
|
|
242
|
+
lastScrollTopRef.current = current;
|
|
243
|
+
if (viewport.scrollHeight - current - viewport.clientHeight <= SCROLL_EDGE_THRESHOLD) followingRef.current = true;
|
|
244
|
+
else if (current < previous - SUBPIXEL_EPSILON) followingRef.current = false;
|
|
245
|
+
captureAnchor();
|
|
246
|
+
setIsPinned(followingRef.current);
|
|
247
|
+
}, [captureAnchor, onScroll]);
|
|
248
|
+
React.useLayoutEffect(() => {
|
|
249
|
+
scrollToBottom("instant");
|
|
250
|
+
}, [scrollToBottom]);
|
|
251
|
+
React.useEffect(() => {
|
|
252
|
+
const content = contentRef.current;
|
|
253
|
+
if (!content) return;
|
|
254
|
+
const observer = new ResizeObserver(() => {
|
|
255
|
+
if (followingRef.current) scrollToBottom("instant");
|
|
256
|
+
else restoreAnchor();
|
|
257
|
+
});
|
|
258
|
+
observer.observe(content);
|
|
259
|
+
return () => observer.disconnect();
|
|
260
|
+
}, [scrollToBottom, restoreAnchor]);
|
|
261
|
+
const context = React.useMemo(() => ({
|
|
262
|
+
isPinned,
|
|
263
|
+
scrollToBottom
|
|
264
|
+
}), [isPinned, scrollToBottom]);
|
|
265
|
+
return /* @__PURE__ */ jsx(ChatBodyContext.Provider, {
|
|
266
|
+
value: context,
|
|
267
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
268
|
+
...props,
|
|
269
|
+
ref: setViewportRef,
|
|
270
|
+
"data-slot": "chat-body",
|
|
271
|
+
"data-pinned": isPinned || void 0,
|
|
272
|
+
className: cn("min-h-0 flex-1 overflow-y-auto overscroll-contain bg-surface px-3.5 py-3 [background-image:radial-gradient(circle,color-mix(in_srgb,var(--color-line)_55%,transparent)_0.75px,transparent_0.75px)] [background-position:0_0] [background-size:24px_24px] [overflow-anchor:none]", className),
|
|
273
|
+
onScroll: handleScroll,
|
|
274
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
275
|
+
ref: contentRef,
|
|
276
|
+
"data-slot": "chat-body-content",
|
|
277
|
+
children
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* A bar pinned to the top of the scrolling region — tabs, a filter row, a date
|
|
284
|
+
* header. Content scrolls behind it.
|
|
285
|
+
*
|
|
286
|
+
* It needs an opaque background to hide what passes under it; `bg-canvas`
|
|
287
|
+
* matches the panel surface and can be overridden.
|
|
288
|
+
*/
|
|
289
|
+
function ChatBodyBar({ className, ...props }) {
|
|
290
|
+
return /* @__PURE__ */ jsx("div", {
|
|
291
|
+
"data-slot": "chat-body-bar",
|
|
292
|
+
className: cn("sticky top-0 z-10 bg-canvas", className),
|
|
293
|
+
...props
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Bounds one dated group so its sticky divider is pushed away by the next
|
|
298
|
+
* section instead of stacking with it.
|
|
299
|
+
*/
|
|
300
|
+
function ChatDateSection({ className, ...props }) {
|
|
301
|
+
return /* @__PURE__ */ jsx("section", {
|
|
302
|
+
"data-slot": "chat-date-section",
|
|
303
|
+
className: cn("relative flex min-w-0 flex-col gap-3 [&+&]:mt-5", className),
|
|
304
|
+
...props
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
/** Sticky date pill for the messages in its containing `ChatDateSection`. */
|
|
308
|
+
function ChatDateDivider({ className, ...props }) {
|
|
309
|
+
return /* @__PURE__ */ jsx("time", {
|
|
310
|
+
"data-slot": "chat-date-divider",
|
|
311
|
+
className: cn("sticky top-3 z-10 self-center rounded-[14px] bg-canvas px-3 py-1.25 text-gdt-sm font-bold text-fg-secondary shadow-1", className),
|
|
312
|
+
...props
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The default menu — File, Video, Image. Spread it to *extend* rather than
|
|
317
|
+
* replace: `items={[...defaultChatInputAttachItems, { kind: "document", … }]}`.
|
|
318
|
+
*/
|
|
319
|
+
const defaultChatInputAttachItems = [
|
|
320
|
+
{
|
|
321
|
+
kind: "file",
|
|
322
|
+
label: "File",
|
|
323
|
+
icon: /* @__PURE__ */ jsx(FileText, {}),
|
|
324
|
+
accept: "*/*"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
kind: "video",
|
|
328
|
+
label: "Video",
|
|
329
|
+
icon: /* @__PURE__ */ jsx(Video, {}),
|
|
330
|
+
accept: "video/*"
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
kind: "image",
|
|
334
|
+
label: "Image",
|
|
335
|
+
icon: /* @__PURE__ */ jsx(Image, {}),
|
|
336
|
+
accept: "image/*"
|
|
337
|
+
}
|
|
338
|
+
];
|
|
339
|
+
const ChatInputContext = React.createContext(null);
|
|
340
|
+
function useChatInputContext() {
|
|
341
|
+
const context = React.useContext(ChatInputContext);
|
|
342
|
+
if (!context) throw new Error("ChatInput parts must be rendered inside <ChatInput>.");
|
|
343
|
+
return context;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The chat composer. A real `<form>`, so Enter and the send button are one
|
|
347
|
+
* submit path rather than two.
|
|
348
|
+
*
|
|
349
|
+
* Compose the parts inside it — the layout is yours, the behaviour is ours.
|
|
350
|
+
*/
|
|
351
|
+
function ChatInput({ value, defaultValue, onValueChange, onSend, clearOnSend = true, disabled = false, className, children, ...props }) {
|
|
352
|
+
const [uncontrolled, setUncontrolled] = React.useState(defaultValue ?? "");
|
|
353
|
+
const isControlled = value !== void 0;
|
|
354
|
+
const text = isControlled ? value : uncontrolled;
|
|
355
|
+
const canSend = onSend !== void 0;
|
|
356
|
+
const setValue = React.useCallback((next) => {
|
|
357
|
+
if (!isControlled) setUncontrolled(next);
|
|
358
|
+
onValueChange?.(next);
|
|
359
|
+
}, [isControlled, onValueChange]);
|
|
360
|
+
const submit = React.useCallback(() => {
|
|
361
|
+
const trimmed = text.trim();
|
|
362
|
+
if (!trimmed || disabled || !onSend) return;
|
|
363
|
+
const accepted = onSend(trimmed);
|
|
364
|
+
if (clearOnSend && accepted !== false) setValue("");
|
|
365
|
+
}, [
|
|
366
|
+
text,
|
|
367
|
+
disabled,
|
|
368
|
+
onSend,
|
|
369
|
+
clearOnSend,
|
|
370
|
+
setValue
|
|
371
|
+
]);
|
|
372
|
+
const context = React.useMemo(() => ({
|
|
373
|
+
value: text,
|
|
374
|
+
setValue,
|
|
375
|
+
disabled,
|
|
376
|
+
canSend,
|
|
377
|
+
submit
|
|
378
|
+
}), [
|
|
379
|
+
text,
|
|
380
|
+
setValue,
|
|
381
|
+
disabled,
|
|
382
|
+
canSend,
|
|
383
|
+
submit
|
|
384
|
+
]);
|
|
385
|
+
return /* @__PURE__ */ jsx(ChatInputContext.Provider, {
|
|
386
|
+
value: context,
|
|
387
|
+
children: /* @__PURE__ */ jsx("form", {
|
|
388
|
+
"data-slot": "chat-input",
|
|
389
|
+
"data-disabled": disabled || void 0,
|
|
390
|
+
className: cn("flex min-h-19 w-full min-w-0 items-center gap-2.5 bg-canvas px-3.5", className),
|
|
391
|
+
...props,
|
|
392
|
+
onSubmit: (event) => {
|
|
393
|
+
event.preventDefault();
|
|
394
|
+
submit();
|
|
395
|
+
},
|
|
396
|
+
children
|
|
397
|
+
})
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
/** The `+` button. Opens a dropdown of file kinds, each opening a filtered picker. */
|
|
401
|
+
function ChatInputAttach({ onAttach, items = defaultChatInputAttachItems, className, contentClassName, disabled: disabledProp = false, "aria-label": ariaLabel = "Add attachment", children, size = "icon-xs", variant = "ghost", ...props }) {
|
|
402
|
+
const { disabled } = useChatInputContext();
|
|
403
|
+
const fileRef = React.useRef(null);
|
|
404
|
+
const kindRef = React.useRef("");
|
|
405
|
+
const [open, setOpen] = React.useState(false);
|
|
406
|
+
const hasActionableItem = items.some((item) => item.onSelect !== void 0 || onAttach !== void 0);
|
|
407
|
+
const isDisabled = disabled || disabledProp || !hasActionableItem;
|
|
408
|
+
const handleSelect = (item) => {
|
|
409
|
+
setOpen(false);
|
|
410
|
+
if (item.onSelect) {
|
|
411
|
+
item.onSelect();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const input = fileRef.current;
|
|
415
|
+
if (!input || onAttach === void 0) return;
|
|
416
|
+
kindRef.current = item.kind;
|
|
417
|
+
input.accept = item.accept ?? "*/*";
|
|
418
|
+
input.value = "";
|
|
419
|
+
input.click();
|
|
420
|
+
};
|
|
421
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
|
|
422
|
+
ref: fileRef,
|
|
423
|
+
className: "hidden",
|
|
424
|
+
disabled: isDisabled,
|
|
425
|
+
multiple: true,
|
|
426
|
+
onChange: (event) => {
|
|
427
|
+
const files = Array.from(event.target.files ?? []);
|
|
428
|
+
if (files.length > 0) onAttach?.(files, kindRef.current);
|
|
429
|
+
},
|
|
430
|
+
tabIndex: -1,
|
|
431
|
+
type: "file"
|
|
432
|
+
}), /* @__PURE__ */ jsxs(DropdownMenu, {
|
|
433
|
+
onOpenChange: (nextOpen) => setOpen(isDisabled ? false : nextOpen),
|
|
434
|
+
open: isDisabled ? false : open,
|
|
435
|
+
children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
436
|
+
disabled: isDisabled,
|
|
437
|
+
render: /* @__PURE__ */ jsx(Button, {
|
|
438
|
+
...props,
|
|
439
|
+
"aria-label": ariaLabel,
|
|
440
|
+
"data-slot": "chat-input-attach",
|
|
441
|
+
className: cn("shrink-0 text-fg-secondary data-popup-open:bg-action-tertiary [&_svg:not([class*='size-'])]:size-6", className),
|
|
442
|
+
disabled: isDisabled,
|
|
443
|
+
size,
|
|
444
|
+
type: "button",
|
|
445
|
+
variant
|
|
446
|
+
}),
|
|
447
|
+
children: children ?? /* @__PURE__ */ jsx(Plus, {})
|
|
448
|
+
}), /* @__PURE__ */ jsx(DropdownMenuContent, {
|
|
449
|
+
align: "start",
|
|
450
|
+
className: cn("w-auto min-w-28 rounded-lg bg-surface-overlay shadow-2", contentClassName),
|
|
451
|
+
side: "top",
|
|
452
|
+
sideOffset: 8,
|
|
453
|
+
children: items.map((item) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
454
|
+
className: "gap-2.5 px-2.5 py-2 text-gdt-sm [&_svg:not([class*='size-'])]:size-6",
|
|
455
|
+
onClick: () => handleSelect(item),
|
|
456
|
+
children: [item.icon, item.label]
|
|
457
|
+
}, item.kind))
|
|
458
|
+
})]
|
|
459
|
+
})] });
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* The single-line message field.
|
|
463
|
+
*
|
|
464
|
+
* Enter sends. The `isComposing` guard stops Enter from sending while an IME
|
|
465
|
+
* candidate is being confirmed.
|
|
466
|
+
*/
|
|
467
|
+
function ChatInputField({ className, children, disabled: disabledProp = false, onKeyDown, type = "text", ...props }) {
|
|
468
|
+
const { value, setValue, disabled, canSend, submit } = useChatInputContext();
|
|
469
|
+
const isDisabled = disabled || disabledProp || !canSend;
|
|
470
|
+
return /* @__PURE__ */ jsxs(InputGroup, {
|
|
471
|
+
"data-slot": "chat-input-field",
|
|
472
|
+
className: "h-11 rounded-[22px] border-[1.5px] border-transparent bg-surface-raised focus-within:border-line-focus focus-within:shadow-[0_0_0_3px_var(--color-line-focus)] focus-within:inset-ring-0",
|
|
473
|
+
children: [/* @__PURE__ */ jsx(InputGroupInput, {
|
|
474
|
+
...props,
|
|
475
|
+
className: cn("px-4 text-gdt-sm", className),
|
|
476
|
+
disabled: isDisabled,
|
|
477
|
+
onChange: (event) => setValue(event.target.value),
|
|
478
|
+
onKeyDown: (event) => {
|
|
479
|
+
onKeyDown?.(event);
|
|
480
|
+
if (event.defaultPrevented) return;
|
|
481
|
+
if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
|
|
482
|
+
event.preventDefault();
|
|
483
|
+
submit();
|
|
484
|
+
},
|
|
485
|
+
type,
|
|
486
|
+
value
|
|
487
|
+
}), children]
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* An accessible button inside the field — emoji, mic, whatever. The caller
|
|
492
|
+
* owns the action, while the component owns the input-group placement.
|
|
493
|
+
*/
|
|
494
|
+
function ChatInputAction({ align = "inline-start", addonClassName, className, disabled: disabledProp = false, size = "icon-xs", ...props }) {
|
|
495
|
+
const { disabled, canSend } = useChatInputContext();
|
|
496
|
+
return /* @__PURE__ */ jsx(InputGroupAddon, {
|
|
497
|
+
"data-slot": "chat-input-action",
|
|
498
|
+
align,
|
|
499
|
+
className: cn("p-0 has-[>button]:m-0", align === "inline-start" ? "pl-4" : "pr-4", addonClassName),
|
|
500
|
+
children: /* @__PURE__ */ jsx(InputGroupButton, {
|
|
501
|
+
...props,
|
|
502
|
+
className: cn("text-fg-secondary [&_svg:not([class*='size-'])]:size-6", className),
|
|
503
|
+
disabled: disabled || disabledProp || !canSend,
|
|
504
|
+
size,
|
|
505
|
+
type: "button"
|
|
506
|
+
})
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
/** Submit button. Disabled while empty — there is nothing to send. */
|
|
510
|
+
function ChatInputSend({ className, disabled: disabledProp = false, "aria-label": ariaLabel = "Send message", size = "icon", variant = "primary", ...props }) {
|
|
511
|
+
const { value, disabled, canSend } = useChatInputContext();
|
|
512
|
+
const unavailable = disabled || disabledProp || !canSend;
|
|
513
|
+
const empty = value.trim().length === 0;
|
|
514
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
515
|
+
...props,
|
|
516
|
+
"aria-label": ariaLabel,
|
|
517
|
+
"data-slot": "chat-input-send",
|
|
518
|
+
"data-empty": empty && !unavailable ? true : void 0,
|
|
519
|
+
className: cn("size-11 shrink-0 rounded-full disabled:bg-action-tertiary disabled:text-fg-caption-placeholder disabled:opacity-100 [&_svg:not([class*='size-'])]:size-5.5", className),
|
|
520
|
+
disabled: unavailable || empty,
|
|
521
|
+
size,
|
|
522
|
+
type: "submit",
|
|
523
|
+
variant,
|
|
524
|
+
children: /* @__PURE__ */ jsx(Send, {})
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
//#endregion
|
|
528
|
+
export { Chat, ChatBody, ChatBodyBar, ChatDateDivider, ChatDateSection, ChatFooter, ChatHeader, ChatHeaderAction, ChatHeaderActions, ChatHeaderDescription, ChatHeaderIdentity, ChatHeaderText, ChatHeaderTitle, ChatInput, ChatInputAction, ChatInputAttach, ChatInputField, ChatInputSend, defaultChatInputAttachItems, useChatBody };
|
package/dist/checkbox.js
CHANGED
|
@@ -7,7 +7,7 @@ import { cva } from "class-variance-authority";
|
|
|
7
7
|
import { Checkbox as Checkbox$1 } from "@base-ui/react/checkbox";
|
|
8
8
|
import { CheckboxGroup } from "@base-ui/react/checkbox-group";
|
|
9
9
|
//#region src/checkbox.tsx
|
|
10
|
-
const checkboxVariants = cva("peer group/checkbox relative flex shrink-0 items-center justify-center rounded-sm border bg-canvas text-transparent outline-none transition-[background-color,border-color,box-shadow,color] after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-line-focus focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-focus)] aria-invalid:border-line-danger aria-invalid:ring-3 aria-invalid:ring-line-danger/20 aria-invalid:focus-visible:border-line-danger aria-invalid:focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-danger)] border-line data-unchecked:hover:border-line-focus data-unchecked:hover:bg-surface-brand-subtle data-checked:border-action-primary data-checked:bg-action-primary data-checked:text-fg-on-brand data-disabled:cursor-not-allowed data-disabled:border-line-subtle data-disabled:bg-surface data-disabled:text-fg-caption-placeholder size-5 [--checkbox-icon-size:0.75rem] group-data-[size=sm]/field:size-4 group-data-[size=sm]/field:[--checkbox-icon-size:0.625rem] group-data-[size=md]/field:size-5 group-data-[size=md]/field:[--checkbox-icon-size:0.75rem] group-data-[size=lg]/field:size-6 group-data-[size=lg]/field:[--checkbox-icon-size:0.875rem]", { variants: { size: {
|
|
10
|
+
const checkboxVariants = cva("peer group/checkbox relative flex shrink-0 items-center justify-center rounded-sm border bg-canvas text-transparent outline-none transition-[background-color,border-color,box-shadow,color,scale] duration-[var(--duration-motion-press)] ease-gdt-out active:scale-[0.97] motion-reduce:active:scale-100 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-line-focus focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-focus)] aria-invalid:border-line-danger aria-invalid:ring-3 aria-invalid:ring-line-danger/20 aria-invalid:focus-visible:border-line-danger aria-invalid:focus-visible:shadow-[0px_0px_0px_2px_var(--color-line-danger)] border-line data-unchecked:hover:border-line-focus data-unchecked:hover:bg-surface-brand-subtle data-checked:border-action-primary data-checked:bg-action-primary data-checked:text-fg-on-brand data-disabled:cursor-not-allowed data-disabled:border-line-subtle data-disabled:bg-surface data-disabled:text-fg-caption-placeholder data-disabled:active:scale-100 size-5 [--checkbox-icon-size:0.75rem] group-data-[size=sm]/field:size-4 group-data-[size=sm]/field:[--checkbox-icon-size:0.625rem] group-data-[size=md]/field:size-5 group-data-[size=md]/field:[--checkbox-icon-size:0.75rem] group-data-[size=lg]/field:size-6 group-data-[size=lg]/field:[--checkbox-icon-size:0.875rem]", { variants: { size: {
|
|
11
11
|
sm: "size-4 [--checkbox-icon-size:0.625rem]",
|
|
12
12
|
md: "size-5 [--checkbox-icon-size:0.75rem]",
|
|
13
13
|
lg: "size-6 [--checkbox-icon-size:0.875rem]"
|
package/dist/combobox.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { t as ComponentSize } from "./size-context-D4mYvcg8.js";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { Combobox as Combobox$1 } from "@base-ui/react";
|
|
4
|
+
import { Virtualizer } from "@tanstack/react-virtual";
|
|
4
5
|
//#region src/combobox.d.ts
|
|
5
6
|
type ComboboxSize = ComponentSize;
|
|
6
7
|
declare function Combobox<Value, Multiple extends boolean | undefined = false>({ multiple, children, size, ...props }: Combobox$1.Root.Props<Value, Multiple> & {
|
|
@@ -26,5 +27,51 @@ declare function ComboboxChip({ className, children, showRemove, ...props }: Com
|
|
|
26
27
|
}): React.JSX.Element;
|
|
27
28
|
declare function ComboboxChipsInput({ className, ...props }: Combobox$1.Input.Props): React.JSX.Element;
|
|
28
29
|
declare function useComboboxAnchor(): React.RefObject<HTMLDivElement | null>;
|
|
30
|
+
type ComboboxVirtualizer = Virtualizer<HTMLDivElement, Element>;
|
|
31
|
+
type ComboboxHighlightDetails = {
|
|
32
|
+
reason: "keyboard" | "pointer" | "none";
|
|
33
|
+
index: number;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Wires up an external virtualizer for a large `Combobox`. Spread the returned
|
|
37
|
+
* `onItemHighlighted` onto `<Combobox virtualized items={...}>` and pass
|
|
38
|
+
* `virtualizerRef` to `<ComboboxVirtualizedList>`. Keeps keyboard navigation in
|
|
39
|
+
* sync by scrolling the highlighted (off-screen) item into view.
|
|
40
|
+
*/
|
|
41
|
+
declare function useComboboxVirtualizer(): {
|
|
42
|
+
virtualizerRef: React.RefObject<ComboboxVirtualizer | null>;
|
|
43
|
+
onItemHighlighted: (item: unknown, { reason, index }: ComboboxHighlightDetails) => void;
|
|
44
|
+
};
|
|
45
|
+
type ComboboxVirtualItemProps = {
|
|
46
|
+
index: number;
|
|
47
|
+
"data-index": number;
|
|
48
|
+
"aria-setsize": number;
|
|
49
|
+
"aria-posinset": number;
|
|
50
|
+
style: React.CSSProperties;
|
|
51
|
+
};
|
|
52
|
+
declare function ComboboxVirtualizedList<Item>({ virtualizerRef, estimateSize, overscan, paddingStart, paddingEnd, maxHeight, className, children }: {
|
|
53
|
+
/** Ref returned by {@link useComboboxVirtualizer}. */
|
|
54
|
+
virtualizerRef: React.RefObject<ComboboxVirtualizer | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Fixed row height in px (or a per-index function). Rows render at this
|
|
57
|
+
* exact height, so match it to the item size variant (roughly sm: 32,
|
|
58
|
+
* md: 40, lg: 44). Defaults to 40.
|
|
59
|
+
*/
|
|
60
|
+
estimateSize?: number | ((index: number) => number);
|
|
61
|
+
overscan?: number;
|
|
62
|
+
paddingStart?: number;
|
|
63
|
+
paddingEnd?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Fixed max height of the scroll viewport (px number or CSS length).
|
|
66
|
+
* Must be a stable value — do NOT wire this to `--available-height` or any
|
|
67
|
+
* value the popup positioner writes, or react-virtual's resize observer and
|
|
68
|
+
* the positioner will feed back into each other and freeze the page on open.
|
|
69
|
+
* Defaults to `"22rem"`.
|
|
70
|
+
*/
|
|
71
|
+
maxHeight?: number | string;
|
|
72
|
+
className?: string;
|
|
73
|
+
/** Render a `ComboboxItem`, spreading the provided props onto it. */
|
|
74
|
+
children: (item: Item, props: ComboboxVirtualItemProps) => React.ReactNode;
|
|
75
|
+
}): React.JSX.Element | null;
|
|
29
76
|
//#endregion
|
|
30
|
-
export { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, ComboboxSeparator, type ComboboxSize, ComboboxTrigger, ComboboxValue, useComboboxAnchor };
|
|
77
|
+
export { Combobox, ComboboxChip, ComboboxChips, ComboboxChipsInput, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxGroup, ComboboxInput, ComboboxItem, ComboboxLabel, ComboboxList, ComboboxSeparator, type ComboboxSize, ComboboxTrigger, ComboboxValue, type ComboboxVirtualItemProps, ComboboxVirtualizedList, type ComboboxVirtualizer, useComboboxAnchor, useComboboxVirtualizer };
|