@botiverse/raft-ui 0.20.0 → 0.21.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.
- package/CHANGELOG.md +157 -0
- package/codemods/button-sizes.mjs +340 -0
- package/dist/cn.mjs +8 -1
- package/dist/foundation.css +13 -0
- package/dist/{touch-drag-BA_Fxs2Z.mjs → inbox-item.recipe-DelZxRmY.mjs} +525 -32
- package/dist/{index-RFu5pOr7.d.mts → index-jTmbN7ov.d.mts} +95 -3
- package/dist/index.d.mts +2 -44
- package/dist/index.mjs +45 -291
- package/dist/styles.css +67 -0
- package/dist/tokens.json +24 -0
- package/dist/wip.d.mts +112 -3
- package/dist/wip.mjs +370 -3
- package/package.json +8 -1
package/dist/styles.css
CHANGED
|
@@ -382,6 +382,32 @@
|
|
|
382
382
|
MessageAttachment. */
|
|
383
383
|
--spacing-control-embedded: var(--control-embedded-size);
|
|
384
384
|
--spacing-embedded-inset: var(--control-embedded-inset);
|
|
385
|
+
/* ─── Timeline rhythm ───────────────────────────────────────────────────────
|
|
386
|
+
Two steps between the lines of a conversation, measured from text to text
|
|
387
|
+
(design/foundations/spacing.md#timeline-rhythm): `timeline-message` (16px)
|
|
388
|
+
between turns, `timeline-section` (24px) at a boundary. Every row carries
|
|
389
|
+
`timeline-row` (4px) above and below its text, the reach of its highlight;
|
|
390
|
+
a turn's first row adds `timeline-turn` (8px, a transparent border the
|
|
391
|
+
highlight doesn't paint) above that, so it starts `timeline-lead` (12px)
|
|
392
|
+
above its text. A date line takes those insets back out of its own
|
|
393
|
+
padding, so it sits a section from the text on both sides.
|
|
394
|
+
A product uses only two of them, around its list: `timeline-start`
|
|
395
|
+
over the first row (a section less the lead every row brings, the
|
|
396
|
+
notice included) and `timeline-end` under the last row (a section less
|
|
397
|
+
its row inset); `turn` and `lead` stay inside the components.
|
|
398
|
+
|
|
399
|
+
Consumers: MessageItem, SystemMessage, MessageListDateDivider,
|
|
400
|
+
MessageListNotice, ThreadPanelReplySummary, and the product's list top
|
|
401
|
+
and end. */
|
|
402
|
+
--spacing-timeline-message: var(--timeline-gap-message);
|
|
403
|
+
--spacing-timeline-section: var(--timeline-gap-section);
|
|
404
|
+
--spacing-timeline-row: var(--timeline-row-inset);
|
|
405
|
+
--spacing-timeline-turn: calc(var(--timeline-gap-message) - 2 * var(--timeline-row-inset));
|
|
406
|
+
--spacing-timeline-lead: calc(var(--timeline-gap-message) - var(--timeline-row-inset));
|
|
407
|
+
--spacing-timeline-end: calc(var(--timeline-gap-section) - var(--timeline-row-inset));
|
|
408
|
+
--spacing-timeline-start: calc(
|
|
409
|
+
var(--timeline-gap-section) - var(--timeline-gap-message) + var(--timeline-row-inset)
|
|
410
|
+
);
|
|
385
411
|
/* ─── Option metric axis ────────────────────────────────────────────────────
|
|
386
412
|
An option in a popup list (a Select, Combobox or ComboboxSelect option, a
|
|
387
413
|
menu item) is as tall as its content, with the same space above and below
|
|
@@ -486,6 +512,14 @@
|
|
|
486
512
|
initial-value: 0px;
|
|
487
513
|
}
|
|
488
514
|
|
|
515
|
+
/* A reference chip's own type size, kept as a length so a smaller note inside
|
|
516
|
+
it can measure itself against the label (message-reference.recipe.ts). */
|
|
517
|
+
@property --message-reference-em {
|
|
518
|
+
syntax: "<length>";
|
|
519
|
+
inherits: true;
|
|
520
|
+
initial-value: 0px;
|
|
521
|
+
}
|
|
522
|
+
|
|
489
523
|
*,
|
|
490
524
|
::before,
|
|
491
525
|
::after {
|
|
@@ -591,6 +625,39 @@ textarea:-webkit-autofill:disabled {
|
|
|
591
625
|
background-image: linear-gradient(var(--fill-medium), var(--fill-medium));
|
|
592
626
|
}
|
|
593
627
|
|
|
628
|
+
/* A mark that leads a line of text: the icon or status light at the start of
|
|
629
|
+
a banner, a toast, a panel notice or a chip sits on the first line of its
|
|
630
|
+
text, centred on it, and stays there when the text wraps
|
|
631
|
+
(design/foundations/alignment.md#a-mark-that-leads-text). In Space
|
|
632
|
+
Grotesk's metrics the centre of the line box is the centre of the cap
|
|
633
|
+
height (to 0.006em); Chromium's rounding can lift the letters 0.5px, which
|
|
634
|
+
is left alone, as the alignment page explains. Only
|
|
635
|
+
the place is shared: each component keeps its own icon size and gap.
|
|
636
|
+
|
|
637
|
+
`lead-icon` centres an icon of any size on the line its parent sets, by
|
|
638
|
+
moving it half a line down and back up by half its own height. Set the
|
|
639
|
+
type on the row that holds the mark and the text, as PanelNotice does:
|
|
640
|
+
the mark reads `1lh` from it.
|
|
641
|
+
|
|
642
|
+
`lead-dot` places a status light the same way, in a box one line tall,
|
|
643
|
+
and keeps it at its one size beside text, 9px with a 1.5px ink edge
|
|
644
|
+
(status.md#sizes). It is kept at 9px inside components (Banner,
|
|
645
|
+
PanelNotice, ToastIcon, Badge, PanelActivity); when placing Status
|
|
646
|
+
yourself, don't pass a size. */
|
|
647
|
+
@utility lead-icon {
|
|
648
|
+
flex-shrink: 0;
|
|
649
|
+
align-self: start;
|
|
650
|
+
translate: 0 calc(0.5lh - 50%);
|
|
651
|
+
}
|
|
652
|
+
@utility lead-dot {
|
|
653
|
+
--lead-dot: 9px;
|
|
654
|
+
flex-shrink: 0;
|
|
655
|
+
width: var(--lead-dot);
|
|
656
|
+
height: var(--lead-dot);
|
|
657
|
+
border-width: 1.5px;
|
|
658
|
+
margin-block: calc((1lh - var(--lead-dot)) / 2);
|
|
659
|
+
}
|
|
660
|
+
|
|
594
661
|
@utility press-overlay-sticker {
|
|
595
662
|
background-image: linear-gradient(var(--sticker-medium), var(--sticker-medium));
|
|
596
663
|
}
|
package/dist/tokens.json
CHANGED
|
@@ -2686,6 +2686,30 @@
|
|
|
2686
2686
|
}
|
|
2687
2687
|
}
|
|
2688
2688
|
},
|
|
2689
|
+
"timeline": {
|
|
2690
|
+
"$type": "dimension",
|
|
2691
|
+
"gap-message": {
|
|
2692
|
+
"$value": {
|
|
2693
|
+
"value": 16,
|
|
2694
|
+
"unit": "px"
|
|
2695
|
+
},
|
|
2696
|
+
"$description": "The message step of a timeline: from the last line of one turn to the first line of the next (a sender line, an event line). Every row keeps the row inset above and below its text; the first row of a turn adds the rest above it."
|
|
2697
|
+
},
|
|
2698
|
+
"gap-section": {
|
|
2699
|
+
"$value": {
|
|
2700
|
+
"value": 24,
|
|
2701
|
+
"unit": "px"
|
|
2702
|
+
},
|
|
2703
|
+
"$description": "The section step of a timeline, one step wider than the message step: either side of a date line, from the top of the list to the first line, and from the last line to the composer, so a boundary reads as one and the last message never sits on the composer."
|
|
2704
|
+
},
|
|
2705
|
+
"row-inset": {
|
|
2706
|
+
"$value": {
|
|
2707
|
+
"value": 4,
|
|
2708
|
+
"unit": "px"
|
|
2709
|
+
},
|
|
2710
|
+
"$description": "How far a timeline row's hover and selected highlight reaches above and below its text. Two rows of one turn sit two insets apart."
|
|
2711
|
+
}
|
|
2712
|
+
},
|
|
2689
2713
|
"sticker": {
|
|
2690
2714
|
"$type": "color",
|
|
2691
2715
|
"ink": {
|
package/dist/wip.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CSSProperties, ComponentPropsWithRef, PointerEvent, ReactNode } from "react";
|
|
1
|
+
import { Ar as SegmentedControlNav, E as MessageQuotedPreviewAuthorDelegatedByProps, Or as SegmentedControlLink, T as MessageQuotedPreviewAuthorDelegatedBy, Vi as Dialog, Wi as DialogClose, _n as MessageItemDelegatedByButton, a as MessageListJumpPillDismissProps, ba as StyledProps, bn as MessageItemDelegatedByNameProps, c as MessageListJumpPillProps, da as DialogTrigger, gn as MessageItemDelegatedBy, hn as MessageItemDelegatedAvatarProps, i as MessageListJumpPillDismiss, jr as SegmentedControlNavProps, kr as SegmentedControlLinkProps, ma as ButtonProps, mn as MessageItemDelegatedAvatar, n as MessageListJumpControlsProps, o as MessageListJumpPillGroup, qi as DialogContentProps, r as MessageListJumpPill, s as MessageListJumpPillGroupProps, t as MessageListJumpControls, ua as DialogTitleProps, vn as MessageItemDelegatedByButtonProps, xn as MessageItemDelegatedByProps, ya as DataAttributes, yn as MessageItemDelegatedByName, zi as TextLinkProps } from "./index-jTmbN7ov.mjs";
|
|
2
|
+
import { CSSProperties, ComponentProps, ComponentPropsWithRef, PointerEvent, ReactNode, RefObject } from "react";
|
|
3
3
|
import { Button } from "@base-ui/react/button";
|
|
4
4
|
import { useRender } from "@base-ui/react/use-render";
|
|
5
5
|
import { Combobox } from "@base-ui/react/combobox";
|
|
@@ -282,4 +282,113 @@ type HoldThenMoveOptions = {
|
|
|
282
282
|
*/
|
|
283
283
|
export declare function useHoldThenMoveTouchSensor({ holdMs, distance, tolerance }?: HoldThenMoveOptions): import("@dnd-kit/core").SensorDescriptor<TouchSensorOptions>;
|
|
284
284
|
//#endregion
|
|
285
|
-
|
|
285
|
+
//#region src/components/ellipsis/ellipsis.d.ts
|
|
286
|
+
/** What a function passed as `after` receives. */
|
|
287
|
+
interface EllipsisState {
|
|
288
|
+
/** The text is cut off right now. `false` while it is expanded. */
|
|
289
|
+
truncated: boolean;
|
|
290
|
+
expanded: boolean;
|
|
291
|
+
/** Expands the text, or collapses it again. */
|
|
292
|
+
toggle: () => void;
|
|
293
|
+
}
|
|
294
|
+
interface EllipsisProps extends Omit<ComponentProps<"span">, "children"> {
|
|
295
|
+
/**
|
|
296
|
+
* The text. Inline content only (text, links, code, mentions): a block
|
|
297
|
+
* child such as `<p>` or `<div>` breaks the clamp.
|
|
298
|
+
*/
|
|
299
|
+
children: ReactNode;
|
|
300
|
+
/** How many lines the text may take before it is cut. */
|
|
301
|
+
lines?: number;
|
|
302
|
+
/**
|
|
303
|
+
* The tooltip shown on hover, and on keyboard focus when the text is a
|
|
304
|
+
* button, while the text is cut: `true` shows the children, a node shows
|
|
305
|
+
* that node, `false` shows none. It never opens on a text that fits, or on
|
|
306
|
+
* a touch screen.
|
|
307
|
+
*/
|
|
308
|
+
tooltip?: ReactNode;
|
|
309
|
+
/**
|
|
310
|
+
* Shown after the text, at the end of the last visible line, only while the
|
|
311
|
+
* text is cut or expanded; it takes no room while the text fits. A function
|
|
312
|
+
* receives the state, for an expand control (`EllipsisToggle`).
|
|
313
|
+
*/
|
|
314
|
+
after?: ReactNode | ((state: EllipsisState) => ReactNode);
|
|
315
|
+
/** Whether the whole text shows (controlled). */
|
|
316
|
+
expanded?: boolean;
|
|
317
|
+
/** Whether the whole text shows at first (uncontrolled). */
|
|
318
|
+
defaultExpanded?: boolean;
|
|
319
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
320
|
+
/**
|
|
321
|
+
* Makes the cut text itself the button that expands it and collapses it
|
|
322
|
+
* again, for a keyboard and a touch screen as well as a pointer. Only plain
|
|
323
|
+
* text with no `after` takes it: a text with a link or another control
|
|
324
|
+
* inside stays text, and so does one with an `after`, whose control is the
|
|
325
|
+
* button instead (a button can't hold another).
|
|
326
|
+
*/
|
|
327
|
+
expandOnClick?: boolean;
|
|
328
|
+
}
|
|
329
|
+
export declare function Ellipsis({ children, lines, tooltip, after, expanded: expandedProp, defaultExpanded, onExpandedChange, expandOnClick, className, style, ...props }: EllipsisProps): import("react").JSX.Element;
|
|
330
|
+
type EllipsisToggleProps = Omit<TextLinkProps, "aria-expanded" | "aria-controls">;
|
|
331
|
+
/**
|
|
332
|
+
* The control that expands an Ellipsis and collapses it again, for its
|
|
333
|
+
* `after`. A real button, with `aria-expanded` and `aria-controls` pointing
|
|
334
|
+
* at the text. The label is the caller's: "Show More" / "Show Less".
|
|
335
|
+
*/
|
|
336
|
+
export declare function EllipsisToggle({ onClick, ...props }: EllipsisToggleProps): import("react").JSX.Element;
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/lib/truncation.d.ts
|
|
339
|
+
/**
|
|
340
|
+
* Whether a text is cut off, and when to look again: the one way raft-ui
|
|
341
|
+
* measures truncation (Ellipsis, `useTruncated`, and the `title` that
|
|
342
|
+
* fields give a cut value).
|
|
343
|
+
*
|
|
344
|
+
* The truncation itself is CSS (`text-overflow: ellipsis`, `line-clamp`);
|
|
345
|
+
* this module only answers "did it cut?".
|
|
346
|
+
*
|
|
347
|
+
* - It compares fractional geometry with a 0.5px tolerance. `scrollWidth` and
|
|
348
|
+
* `clientWidth` are whole pixels, so at some widths (and page zooms) a text
|
|
349
|
+
* that shows an ellipsis reports that it fits.
|
|
350
|
+
* - Every measured element on a page shares one ResizeObserver, one
|
|
351
|
+
* MutationObserver and one font-loading listener. A web font that finishes
|
|
352
|
+
* loading widens the text without resizing its box, which no
|
|
353
|
+
* ResizeObserver would notice.
|
|
354
|
+
* - Requests are batched into one animation frame, before paint: every
|
|
355
|
+
* target is isolated (an Ellipsis hides its `after` so the text is measured
|
|
356
|
+
* alone), then every target is read, then restored, then every result is
|
|
357
|
+
* committed in one synchronous React update. Reads never interleave with
|
|
358
|
+
* writes, and a result shows in the same frame it was measured in. A
|
|
359
|
+
* request made while a frame is handled is queued for the next one.
|
|
360
|
+
*/
|
|
361
|
+
/** `undefined`: not known yet, or the element has no box (hidden, not laid out). */
|
|
362
|
+
type Truncation = boolean | undefined;
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/lib/use-truncated.d.ts
|
|
365
|
+
/**
|
|
366
|
+
* Whether the text in the element `ref` points at is cut off by its CSS
|
|
367
|
+
* (`truncate`, `line-clamp`, a fixed size with `overflow: hidden`): `true`,
|
|
368
|
+
* `false`, or `undefined` until it is measured or while it has no box.
|
|
369
|
+
*
|
|
370
|
+
* It measures fractional sizes with a 0.5px tolerance, past either inline
|
|
371
|
+
* edge and below the bottom edge, so it needs no line count. It measures again
|
|
372
|
+
* when the element changes size, when its text changes and when a web font
|
|
373
|
+
* finishes loading, in one batched frame for every measured element on the
|
|
374
|
+
* page (lib/truncation.ts), before that frame paints.
|
|
375
|
+
*
|
|
376
|
+
* The element is read when the component mounts: keep `ref` on one element
|
|
377
|
+
* for the component's life, or give the component a `key` that changes with
|
|
378
|
+
* the element.
|
|
379
|
+
*/
|
|
380
|
+
export declare function useTruncated(ref: RefObject<Element | null>): Truncation;
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/components/inbox-item/inbox-item-leading-badge.d.ts
|
|
383
|
+
type InboxItemLeadingBadgeProps = useRender.ComponentProps<"span">;
|
|
384
|
+
/**
|
|
385
|
+
* A small badge in the bottom-right corner of `InboxItemLeading`, the corner a
|
|
386
|
+
* presence light takes: on a thread row, the thread mark, on the channel's
|
|
387
|
+
* `IconTile` or on the direct message's `Avatar` alike. Put it inside
|
|
388
|
+
* `InboxItemLeading`, after the tile or avatar, with one icon as its child;
|
|
389
|
+
* leave the avatar's `AvatarBadge` out, since the corner holds one mark. It
|
|
390
|
+
* is hidden from assistive technology unless given a `role`.
|
|
391
|
+
*/
|
|
392
|
+
export declare function InboxItemLeadingBadge({ className, render, ...props }: InboxItemLeadingBadgeProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
393
|
+
//#endregion
|
|
394
|
+
export { Dialog as AnnouncementDialog, DialogClose as AnnouncementDialogClose, DialogTrigger as AnnouncementDialogTrigger, type ComboboxSelectGroup, type ComboboxSelectMessages, type ComboboxSelectOption, type ComboboxSelectProps, type ComboboxSelectSelection, type ComboboxSelectValue, type CommentCardMetaProps, type CommentCardProps, type CommentCardQuoteProps, type CommentCardTriggerProps, type CommentEntryAuthorProps, type CommentEntryBodyProps, type CommentEntryHeaderProps, type CommentEntryProps, type CommentEntryTimeProps, type CommentMarginItemProps, type CommentMarginLayout, type CommentMarginLayoutItem, type CommentMarginLayoutOptions, type CommentMarginProps, type CommentMarkerProps, type EllipsisProps, type EllipsisState, type EllipsisToggleProps, type HoldThenMoveOptions, type InboxItemLeadingBadgeProps, MessageItemDelegatedAvatar, type MessageItemDelegatedAvatarProps, MessageItemDelegatedBy, MessageItemDelegatedByButton, type MessageItemDelegatedByButtonProps, MessageItemDelegatedByName, type MessageItemDelegatedByNameProps, type MessageItemDelegatedByProps, MessageListJumpControls, type MessageListJumpControlsProps, MessageListJumpPill, MessageListJumpPillDismiss, type MessageListJumpPillDismissProps, MessageListJumpPillGroup, type MessageListJumpPillGroupProps, type MessageListJumpPillProps, MessageQuotedPreviewAuthorDelegatedBy, type MessageQuotedPreviewAuthorDelegatedByProps, SegmentedControlLink, type SegmentedControlLinkProps, SegmentedControlNav, type SegmentedControlNavProps, type UseCommentMarginFitsOptions };
|
package/dist/wip.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $t as observeTruncation, Bn as TooltipContent, Jt as SegmentedControlLink, Qt as measureElement, Un as TooltipTrigger, Wn as TextLink, Xn as DialogContent, Xt as useOverflowTitle, Yn as DialogClose, Yt as SegmentedControlNav, Zt as measureContent, _ as MessageQuotedPreviewAuthorDelegatedBy, _t as MessageItemDelegatedByName, a as MessageListJumpPill, ar as DialogTrigger, en as readNow, gr as tv, gt as MessageItemDelegatedByButton, ht as MessageItemDelegatedBy, i as MessageListJumpControls, ir as DialogTitle, lr as defaultNativeButton, mr as useTheme, mt as MessageItemDelegatedAvatar, n as useHoldThenMoveTouchSensor, o as MessageListJumpPillDismiss, or as Button$1, pr as useThemeScopeProps, qn as Dialog, s as MessageListJumpPillGroup, t as inboxItem, zn as Tooltip } from "./inbox-item.recipe-DelZxRmY.mjs";
|
|
2
2
|
import { Check, ChevronDown, LoaderCircle, Plus, X } from "lucide-react";
|
|
3
|
-
import { createContext, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { createContext, use, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { Button } from "@base-ui/react/button";
|
|
6
6
|
import { mergeProps } from "@base-ui/react/merge-props";
|
|
@@ -1123,4 +1123,371 @@ function useCommentMarginFits(element, { minWidth = 784 } = {}) {
|
|
|
1123
1123
|
return desktop && element !== null && width >= minWidth;
|
|
1124
1124
|
}
|
|
1125
1125
|
//#endregion
|
|
1126
|
-
|
|
1126
|
+
//#region src/components/ellipsis/ellipsis.tsx
|
|
1127
|
+
const ellipsis = tv({
|
|
1128
|
+
slots: {
|
|
1129
|
+
root: ["group/ellipsis min-w-0 max-w-full"],
|
|
1130
|
+
text: ["min-w-0"],
|
|
1131
|
+
content: [],
|
|
1132
|
+
spacer: ["float-end h-[calc(100%-1lh)] w-0", "data-hidden:hidden group-data-measuring/ellipsis:hidden"],
|
|
1133
|
+
after: ["data-hidden:hidden group-data-measuring/ellipsis:hidden"]
|
|
1134
|
+
},
|
|
1135
|
+
variants: {
|
|
1136
|
+
multiline: {
|
|
1137
|
+
true: {
|
|
1138
|
+
root: ["flex"],
|
|
1139
|
+
text: ["flex-1", "wrap-break-word"],
|
|
1140
|
+
after: ["float-end clear-end ps-1"]
|
|
1141
|
+
},
|
|
1142
|
+
false: {
|
|
1143
|
+
root: ["inline-flex items-baseline gap-x-1 align-bottom"],
|
|
1144
|
+
after: ["shrink-0"]
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
expanded: {
|
|
1148
|
+
true: {},
|
|
1149
|
+
false: {}
|
|
1150
|
+
},
|
|
1151
|
+
clickable: {
|
|
1152
|
+
true: { text: ["cursor-pointer"] },
|
|
1153
|
+
false: {}
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
compoundVariants: [
|
|
1157
|
+
{
|
|
1158
|
+
multiline: false,
|
|
1159
|
+
expanded: false,
|
|
1160
|
+
class: { text: ["truncate", "print:overflow-visible print:whitespace-normal"] }
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
multiline: false,
|
|
1164
|
+
expanded: true,
|
|
1165
|
+
class: { text: ["wrap-anywhere whitespace-normal"] }
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
multiline: true,
|
|
1169
|
+
expanded: false,
|
|
1170
|
+
class: { text: ["line-clamp-(--ellipsis-lines)", "print:line-clamp-none"] }
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
multiline: true,
|
|
1174
|
+
expanded: true,
|
|
1175
|
+
class: { text: ["line-clamp-none"] }
|
|
1176
|
+
}
|
|
1177
|
+
]
|
|
1178
|
+
});
|
|
1179
|
+
const EllipsisContext = createContext(null);
|
|
1180
|
+
const INTERACTIVE = "a[href], button, input, select, textarea, summary, [tabindex], [contenteditable]:not([contenteditable='false']), [role='button'], [role='link']";
|
|
1181
|
+
const TOUCH_MEDIA_QUERY = "(hover: none), (pointer: coarse)";
|
|
1182
|
+
function isTouchScreen() {
|
|
1183
|
+
return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia(TOUCH_MEDIA_QUERY).matches;
|
|
1184
|
+
}
|
|
1185
|
+
function contains(ref, target) {
|
|
1186
|
+
return target instanceof Node && ref.current?.contains(target) === true;
|
|
1187
|
+
}
|
|
1188
|
+
function hasSelection(within) {
|
|
1189
|
+
const selection = within.ownerDocument.defaultView?.getSelection();
|
|
1190
|
+
return !!selection && !selection.isCollapsed && selection.anchorNode != null && within.contains(selection.anchorNode);
|
|
1191
|
+
}
|
|
1192
|
+
function useExpanded(expandedProp, defaultExpanded, onExpandedChange) {
|
|
1193
|
+
const [expandedState, setExpandedState] = useState(defaultExpanded);
|
|
1194
|
+
const expanded = expandedProp ?? expandedState;
|
|
1195
|
+
return [expanded, useCallback(() => {
|
|
1196
|
+
const next = !expanded;
|
|
1197
|
+
if (expandedProp === void 0) setExpandedState(next);
|
|
1198
|
+
onExpandedChange?.(next);
|
|
1199
|
+
}, [
|
|
1200
|
+
expanded,
|
|
1201
|
+
expandedProp,
|
|
1202
|
+
onExpandedChange
|
|
1203
|
+
])];
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Registers the text with the shared measurer (lib/truncation.ts) and keeps
|
|
1207
|
+
* its answer: whether the text, alone, is cut, and whether it has a control
|
|
1208
|
+
* inside. While it is expanded the last collapsed answer is kept, so the
|
|
1209
|
+
* text and its control stay what they were until it collapses and is
|
|
1210
|
+
* measured again. `layout` names what remounts or reclamps the text.
|
|
1211
|
+
*/
|
|
1212
|
+
function useTruncation(expanded, layout, onNotTruncated) {
|
|
1213
|
+
const rootRef = useRef(null);
|
|
1214
|
+
const textRef = useRef(null);
|
|
1215
|
+
const contentRef = useRef(null);
|
|
1216
|
+
const afterRef = useRef(null);
|
|
1217
|
+
const setTextRef = useCallback((node) => {
|
|
1218
|
+
textRef.current = node;
|
|
1219
|
+
}, []);
|
|
1220
|
+
const [truncation, setTruncation] = useState(void 0);
|
|
1221
|
+
const [interactive, setInteractive] = useState(false);
|
|
1222
|
+
const expandedRef = useRef(expanded);
|
|
1223
|
+
const targetRef = useRef(null);
|
|
1224
|
+
const commit = useCallback((next) => {
|
|
1225
|
+
setInteractive(contentRef.current?.querySelector(INTERACTIVE) != null);
|
|
1226
|
+
if (expandedRef.current) return;
|
|
1227
|
+
setTruncation(next);
|
|
1228
|
+
if (next !== true) onNotTruncated();
|
|
1229
|
+
}, [onNotTruncated]);
|
|
1230
|
+
useLayoutEffect(() => {
|
|
1231
|
+
expandedRef.current = expanded;
|
|
1232
|
+
const root = rootRef.current;
|
|
1233
|
+
const text = textRef.current;
|
|
1234
|
+
const content = contentRef.current;
|
|
1235
|
+
if (!root || !text || !content) return void 0;
|
|
1236
|
+
const target = {
|
|
1237
|
+
observe: () => [text, afterRef.current],
|
|
1238
|
+
watch: () => content,
|
|
1239
|
+
isolate: () => {
|
|
1240
|
+
root.setAttribute("data-measuring", "");
|
|
1241
|
+
return () => root.removeAttribute("data-measuring");
|
|
1242
|
+
},
|
|
1243
|
+
read: () => measureContent(text, content),
|
|
1244
|
+
commit
|
|
1245
|
+
};
|
|
1246
|
+
targetRef.current = target;
|
|
1247
|
+
return observeTruncation(target);
|
|
1248
|
+
}, [
|
|
1249
|
+
commit,
|
|
1250
|
+
expanded,
|
|
1251
|
+
layout
|
|
1252
|
+
]);
|
|
1253
|
+
return {
|
|
1254
|
+
rootRef,
|
|
1255
|
+
textRef,
|
|
1256
|
+
setTextRef,
|
|
1257
|
+
contentRef,
|
|
1258
|
+
afterRef,
|
|
1259
|
+
truncation,
|
|
1260
|
+
interactive,
|
|
1261
|
+
measureNow: useCallback(() => {
|
|
1262
|
+
const target = targetRef.current;
|
|
1263
|
+
if (!target) return void 0;
|
|
1264
|
+
const next = readNow(target);
|
|
1265
|
+
commit(next);
|
|
1266
|
+
return next;
|
|
1267
|
+
}, [commit])
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
/** The tooltip's `onOpenChange`: it opens only on a text measured as cut, now. */
|
|
1271
|
+
function useOpenWhenTruncated(setOpen, expanded, afterRef, measureNow) {
|
|
1272
|
+
return useCallback((next, details) => {
|
|
1273
|
+
if (!next) {
|
|
1274
|
+
setOpen(false);
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
if (isTouchScreen() || expanded) return;
|
|
1278
|
+
if (contains(afterRef, details.event?.target)) return;
|
|
1279
|
+
if (measureNow() === true) setOpen(true);
|
|
1280
|
+
}, [
|
|
1281
|
+
afterRef,
|
|
1282
|
+
expanded,
|
|
1283
|
+
measureNow,
|
|
1284
|
+
setOpen
|
|
1285
|
+
]);
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Makes the text the button that expands and collapses it, while
|
|
1289
|
+
* `clickable`. It stays one while expanded, `aria-expanded="true"`, so focus
|
|
1290
|
+
* stays on it and it collapses again the same way.
|
|
1291
|
+
*/
|
|
1292
|
+
function useExpandOnClick(clickable, expanded, toggle, textRef) {
|
|
1293
|
+
const onClick = useCallback(() => {
|
|
1294
|
+
if (!clickable) return;
|
|
1295
|
+
if (textRef.current && hasSelection(textRef.current)) return;
|
|
1296
|
+
toggle();
|
|
1297
|
+
}, [
|
|
1298
|
+
clickable,
|
|
1299
|
+
textRef,
|
|
1300
|
+
toggle
|
|
1301
|
+
]);
|
|
1302
|
+
const onKeyDown = useCallback((event) => {
|
|
1303
|
+
if (!clickable || event.target !== event.currentTarget) return;
|
|
1304
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1305
|
+
event.preventDefault();
|
|
1306
|
+
toggle();
|
|
1307
|
+
}
|
|
1308
|
+
}, [clickable, toggle]);
|
|
1309
|
+
if (!clickable) return {};
|
|
1310
|
+
return {
|
|
1311
|
+
role: "button",
|
|
1312
|
+
tabIndex: 0,
|
|
1313
|
+
"aria-expanded": expanded,
|
|
1314
|
+
onClick,
|
|
1315
|
+
onKeyDown
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
function useRootStyle(style, lines) {
|
|
1319
|
+
return useMemo(() => lines > 1 ? {
|
|
1320
|
+
...style,
|
|
1321
|
+
["--ellipsis-lines"]: String(lines)
|
|
1322
|
+
} : style, [style, lines]);
|
|
1323
|
+
}
|
|
1324
|
+
function Ellipsis({ children, lines = 1, tooltip = true, after, expanded: expandedProp, defaultExpanded = false, onExpandedChange, expandOnClick = false, className, style, ...props }) {
|
|
1325
|
+
const textId = useId();
|
|
1326
|
+
const [open, setOpen] = useState(false);
|
|
1327
|
+
const closeTooltip = useCallback(() => setOpen(false), []);
|
|
1328
|
+
const [expanded, toggle] = useExpanded(expandedProp, defaultExpanded, onExpandedChange);
|
|
1329
|
+
const multiline = lines > 1;
|
|
1330
|
+
const hasTooltip = tooltip !== false && tooltip != null;
|
|
1331
|
+
const hasAfter = after != null;
|
|
1332
|
+
const measured = useTruncation(expanded, `${lines} ${hasAfter} ${hasTooltip}`, closeTooltip);
|
|
1333
|
+
const { rootRef, textRef, setTextRef, contentRef, afterRef, truncation } = measured;
|
|
1334
|
+
const handleOpenChange = useOpenWhenTruncated(setOpen, expanded, afterRef, measured.measureNow);
|
|
1335
|
+
const truncated = truncation === true && !expanded;
|
|
1336
|
+
const clickable = expandOnClick && !hasAfter && !measured.interactive && (truncation === true || expanded);
|
|
1337
|
+
const afterNode = typeof after === "function" ? after({
|
|
1338
|
+
truncated,
|
|
1339
|
+
expanded,
|
|
1340
|
+
toggle
|
|
1341
|
+
}) : after;
|
|
1342
|
+
const afterHidden = !truncated && !expanded;
|
|
1343
|
+
const slots = ellipsis({
|
|
1344
|
+
multiline,
|
|
1345
|
+
expanded,
|
|
1346
|
+
clickable
|
|
1347
|
+
});
|
|
1348
|
+
const context = useMemo(() => ({
|
|
1349
|
+
expanded,
|
|
1350
|
+
toggle,
|
|
1351
|
+
textId
|
|
1352
|
+
}), [
|
|
1353
|
+
expanded,
|
|
1354
|
+
toggle,
|
|
1355
|
+
textId
|
|
1356
|
+
]);
|
|
1357
|
+
const rootStyle = useRootStyle(style, lines);
|
|
1358
|
+
const clickProps = useExpandOnClick(clickable, expanded, toggle, textRef);
|
|
1359
|
+
const afterElement = hasAfter ? /* @__PURE__ */ jsx("span", {
|
|
1360
|
+
ref: afterRef,
|
|
1361
|
+
"data-slot": "ellipsis-after",
|
|
1362
|
+
"data-hidden": afterHidden ? "" : void 0,
|
|
1363
|
+
inert: afterHidden,
|
|
1364
|
+
className: slots.after(),
|
|
1365
|
+
onPointerEnter: closeTooltip,
|
|
1366
|
+
children: afterNode
|
|
1367
|
+
}) : null;
|
|
1368
|
+
const floatingAfter = multiline && hasAfter;
|
|
1369
|
+
const textChildren = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1370
|
+
floatingAfter && /* @__PURE__ */ jsx("span", {
|
|
1371
|
+
"aria-hidden": true,
|
|
1372
|
+
"data-slot": "ellipsis-spacer",
|
|
1373
|
+
"data-hidden": afterHidden ? "" : void 0,
|
|
1374
|
+
className: slots.spacer()
|
|
1375
|
+
}),
|
|
1376
|
+
floatingAfter && afterElement,
|
|
1377
|
+
/* @__PURE__ */ jsx("span", {
|
|
1378
|
+
ref: contentRef,
|
|
1379
|
+
"data-slot": "ellipsis-content",
|
|
1380
|
+
className: slots.content(),
|
|
1381
|
+
children
|
|
1382
|
+
})
|
|
1383
|
+
] });
|
|
1384
|
+
const textProps = {
|
|
1385
|
+
id: textId,
|
|
1386
|
+
"data-slot": "ellipsis-text",
|
|
1387
|
+
"data-truncated": truncated ? "" : void 0,
|
|
1388
|
+
className: slots.text(),
|
|
1389
|
+
...clickProps
|
|
1390
|
+
};
|
|
1391
|
+
const text = hasTooltip ? /* @__PURE__ */ jsxs(Tooltip, {
|
|
1392
|
+
open: open && truncated,
|
|
1393
|
+
onOpenChange: handleOpenChange,
|
|
1394
|
+
children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
1395
|
+
ref: setTextRef,
|
|
1396
|
+
render: /* @__PURE__ */ jsx("span", {}),
|
|
1397
|
+
...textProps,
|
|
1398
|
+
className: slots.text({ class: "select-auto" }),
|
|
1399
|
+
children: textChildren
|
|
1400
|
+
}), /* @__PURE__ */ jsx(TooltipContent, { children: tooltip === true ? children : tooltip })]
|
|
1401
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
1402
|
+
ref: setTextRef,
|
|
1403
|
+
...textProps,
|
|
1404
|
+
children: textChildren
|
|
1405
|
+
});
|
|
1406
|
+
return /* @__PURE__ */ jsx(EllipsisContext.Provider, {
|
|
1407
|
+
value: context,
|
|
1408
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
1409
|
+
ref: rootRef,
|
|
1410
|
+
"data-slot": "ellipsis",
|
|
1411
|
+
className: slots.root({ className }),
|
|
1412
|
+
style: rootStyle,
|
|
1413
|
+
...props,
|
|
1414
|
+
children: [text, !multiline ? afterElement : null]
|
|
1415
|
+
})
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* The control that expands an Ellipsis and collapses it again, for its
|
|
1420
|
+
* `after`. A real button, with `aria-expanded` and `aria-controls` pointing
|
|
1421
|
+
* at the text. The label is the caller's: "Show More" / "Show Less".
|
|
1422
|
+
*/
|
|
1423
|
+
function EllipsisToggle({ onClick, ...props }) {
|
|
1424
|
+
const context = useContext(EllipsisContext);
|
|
1425
|
+
if (!context) throw new Error("EllipsisToggle must be inside an Ellipsis `after`.");
|
|
1426
|
+
const { expanded, toggle, textId } = context;
|
|
1427
|
+
const handleClick = useCallback((event) => {
|
|
1428
|
+
onClick?.(event);
|
|
1429
|
+
if (!event.defaultPrevented) toggle();
|
|
1430
|
+
}, [onClick, toggle]);
|
|
1431
|
+
return /* @__PURE__ */ jsx(TextLink, {
|
|
1432
|
+
"aria-expanded": expanded,
|
|
1433
|
+
"aria-controls": textId,
|
|
1434
|
+
onClick: handleClick,
|
|
1435
|
+
...props
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
//#endregion
|
|
1439
|
+
//#region src/lib/use-truncated.ts
|
|
1440
|
+
/**
|
|
1441
|
+
* Whether the text in the element `ref` points at is cut off by its CSS
|
|
1442
|
+
* (`truncate`, `line-clamp`, a fixed size with `overflow: hidden`): `true`,
|
|
1443
|
+
* `false`, or `undefined` until it is measured or while it has no box.
|
|
1444
|
+
*
|
|
1445
|
+
* It measures fractional sizes with a 0.5px tolerance, past either inline
|
|
1446
|
+
* edge and below the bottom edge, so it needs no line count. It measures again
|
|
1447
|
+
* when the element changes size, when its text changes and when a web font
|
|
1448
|
+
* finishes loading, in one batched frame for every measured element on the
|
|
1449
|
+
* page (lib/truncation.ts), before that frame paints.
|
|
1450
|
+
*
|
|
1451
|
+
* The element is read when the component mounts: keep `ref` on one element
|
|
1452
|
+
* for the component's life, or give the component a `key` that changes with
|
|
1453
|
+
* the element.
|
|
1454
|
+
*/
|
|
1455
|
+
function useTruncated(ref) {
|
|
1456
|
+
const [truncated, setTruncated] = useState(void 0);
|
|
1457
|
+
useLayoutEffect(() => {
|
|
1458
|
+
const element = ref.current;
|
|
1459
|
+
if (!element) return void 0;
|
|
1460
|
+
return observeTruncation({
|
|
1461
|
+
observe: () => [element],
|
|
1462
|
+
watch: () => element,
|
|
1463
|
+
read: () => measureElement(element),
|
|
1464
|
+
commit: setTruncated
|
|
1465
|
+
});
|
|
1466
|
+
}, [ref]);
|
|
1467
|
+
return truncated;
|
|
1468
|
+
}
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region src/components/inbox-item/inbox-item-leading-badge.tsx
|
|
1471
|
+
/**
|
|
1472
|
+
* A small badge in the bottom-right corner of `InboxItemLeading`, the corner a
|
|
1473
|
+
* presence light takes: on a thread row, the thread mark, on the channel's
|
|
1474
|
+
* `IconTile` or on the direct message's `Avatar` alike. Put it inside
|
|
1475
|
+
* `InboxItemLeading`, after the tile or avatar, with one icon as its child;
|
|
1476
|
+
* leave the avatar's `AvatarBadge` out, since the corner holds one mark. It
|
|
1477
|
+
* is hidden from assistive technology unless given a `role`.
|
|
1478
|
+
*/
|
|
1479
|
+
function InboxItemLeadingBadge({ className, render, ...props }) {
|
|
1480
|
+
const { itemLeadingBadge } = inboxItem();
|
|
1481
|
+
const defaultProps = {
|
|
1482
|
+
"data-slot": "inbox-item-leading-badge",
|
|
1483
|
+
"aria-hidden": props.role ? void 0 : true,
|
|
1484
|
+
className: itemLeadingBadge({ className })
|
|
1485
|
+
};
|
|
1486
|
+
return useRender({
|
|
1487
|
+
defaultTagName: "span",
|
|
1488
|
+
render,
|
|
1489
|
+
props: mergeProps(defaultProps, props)
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
//#endregion
|
|
1493
|
+
export { Dialog as AnnouncementDialog, AnnouncementDialogAction, AnnouncementDialogBody, DialogClose as AnnouncementDialogClose, AnnouncementDialogContent, AnnouncementDialogFooter, AnnouncementDialogHeader, AnnouncementDialogLabel, AnnouncementDialogPicture, AnnouncementDialogSecondaryAction, AnnouncementDialogText, AnnouncementDialogTitle, DialogTrigger as AnnouncementDialogTrigger, COMMENT_HIGHLIGHT, COMMENT_HIGHLIGHT_ACTIVE, ComboboxSelect, CommentCard, CommentCardMeta, CommentCardQuote, CommentCardTrigger, CommentEntry, CommentEntryAuthor, CommentEntryBody, CommentEntryHeader, CommentEntryTime, CommentMargin, CommentMarginItem, CommentMarker, Ellipsis, EllipsisToggle, InboxItemLeadingBadge, MessageItemDelegatedAvatar, MessageItemDelegatedBy, MessageItemDelegatedByButton, MessageItemDelegatedByName, MessageListJumpControls, MessageListJumpPill, MessageListJumpPillDismiss, MessageListJumpPillGroup, MessageQuotedPreviewAuthorDelegatedBy, SegmentedControlLink, SegmentedControlNav, resolveCommentMarginLayout, setCommentHighlights, useCommentMarginFits, useHoldThenMoveTouchSensor, useTruncated };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botiverse/raft-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.1",
|
|
4
4
|
"description": "React components and CSS foundations for the Raft Design System.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"tailwind-variants": "^3.3.1"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
|
+
"@babel/parser": "^7.29.9",
|
|
65
66
|
"@tailwindcss/vite": "^4.3.3",
|
|
66
67
|
"@testing-library/dom": "^10.4.2",
|
|
67
68
|
"@testing-library/react": "^16.3.3",
|
|
@@ -82,9 +83,15 @@
|
|
|
82
83
|
"vitest": "5.0.1"
|
|
83
84
|
},
|
|
84
85
|
"peerDependencies": {
|
|
86
|
+
"@babel/parser": "^7.29.9",
|
|
85
87
|
"react": "^19",
|
|
86
88
|
"react-dom": "^19"
|
|
87
89
|
},
|
|
90
|
+
"peerDependenciesMeta": {
|
|
91
|
+
"@babel/parser": {
|
|
92
|
+
"optional": true
|
|
93
|
+
}
|
|
94
|
+
},
|
|
88
95
|
"scripts": {
|
|
89
96
|
"bench:virtual-list": "node bench/virtual-list/run.mjs",
|
|
90
97
|
"build:tokens": "node scripts/build-tokens.mjs",
|